# =====================================================================
# AC Diagnostic POC - ESP32 firmware (MicroPython, runs on Wokwi)
# ---------------------------------------------------------------------
# Reads "sensors" (1x DHT22 + 6 potentiometers acting as fault knobs),
# computes HVAC diagnostic metrics (superheat, temperature split),
# runs a rules engine, prints to the serial monitor, and publishes
# everything as JSON over MQTT so the browser dashboard can show it.
#
# NOTE ON THE THERMODYNAMICS: the refrigerant math below is a
# SIMPLIFIED approximation, good enough to demonstrate the concept.
# A production tool would use full R-410A property tables.
# =====================================================================
import time, ujson, network, ubinascii, machine
from machine import Pin, ADC
import dht
# umqtt.simple ships with Wokwi's MicroPython build. Guarded so the
# sim still runs (serial only) even if the module is ever missing.
try:
from umqtt.simple import MQTTClient
HAVE_MQTT = True
except ImportError:
HAVE_MQTT = False
print("!! umqtt.simple not found - running serial-only (no dashboard)")
# --------------------------- CONFIG ----------------------------------
WIFI_SSID = "Wokwi-GUEST" # Wokwi's built-in virtual Wi-Fi
WIFI_PASS = ""
MQTT_BROKER = "broker.hivemq.com" # free public broker, no signup
MQTT_PORT = 1883
# IMPORTANT: this topic is shared on a PUBLIC broker. Change the middle
# part to something unique to you, and use the SAME value in dashboard.html
TOPIC = b"ac-diag-poc/sd-7f3a/telemetry"
CLIENT_ID = b"acdiag-" + ubinascii.hexlify(machine.unique_id())
# --------------------------- SENSORS ---------------------------------
dht_sensor = dht.DHT22(Pin(15)) # return-air temperature + humidity
def make_adc(gpio):
a = ADC(Pin(gpio))
a.atten(ADC.ATTN_11DB) # full 0 .. ~3.3V input range
return a
# Each potentiometer stands in for one real AC sensor. On the ESP32 only
# the ADC1 pins (GPIO 32,33,34,35,36,39) work while Wi-Fi is on - that is
# a real hardware constraint, not a simulator quirk.
POTS = {
"supply_air_f": make_adc(32),
"suction_psi": make_adc(33),
"suction_line_f": make_adc(34),
"discharge_psi": make_adc(35),
"compressor_amps": make_adc(36), # SENSOR_VP
"static_press_inwc": make_adc(39), # SENSOR_VN
}
def scaled(adc, lo, hi):
"""Map a raw 0..4095 ADC reading onto an engineering range."""
return lo + (adc.read() / 4095) * (hi - lo)
# ------------------ REFRIGERANT MATH (R-410A, SIMPLIFIED) ------------
# Pressure -> Temperature reference points: gauge psi -> saturation degF.
# We linearly interpolate between them. Superheat = measured suction line
# temp minus this saturation temp - the single most useful charge metric.
_PT_R410A = [
(60, 15), (100, 31), (118, 40), (135, 46), (150, 51),
(200, 70), (250, 84), (300, 97), (350, 108), (450, 128),
]
def sat_temp_f(psi):
pts = _PT_R410A
if psi <= pts[0][0]:
return pts[0][1]
if psi >= pts[-1][0]:
return pts[-1][1]
for i in range(1, len(pts)):
p0, t0 = pts[i - 1]
p1, t1 = pts[i]
if psi <= p1:
return t0 + (t1 - t0) * (psi - p0) / (p1 - p0)
return pts[-1][1]
def c_to_f(c):
return c * 9 / 5 + 32
# --------------------------- RULES ENGINE ----------------------------
# Each rule that fires appends (severity, fault, recommended action).
# Higher severity = more urgent; the top one drives the headline status.
def diagnose(m):
faults = []
sh, dt = m["superheat_f"], m["delta_t_f"]
sp, dp = m["suction_psi"], m["discharge_psi"]
amps = m["compressor_amps"]
stat = m["static_press_inwc"]
# --- electrical ---
if amps < 2:
faults.append((3, "Compressor not running",
"Check contactor / start capacitor / thermostat call"))
elif amps > 26:
faults.append((5, "Compressor locked-rotor / very high amp draw",
"Failing compressor - likely COMPRESSOR replacement"))
elif amps > 22:
faults.append((4, "High amp draw on compressor",
"Weak RUN CAPACITOR (most common) - replace capacitor"))
# --- airflow ---
if stat > 0.7:
faults.append((3, "High static pressure - restricted airflow",
"Replace air FILTER; check dirty evaporator / closed vents"))
# --- refrigerant charge ---
if sh > 20 and sp < 100:
faults.append((4, "Undercharge / refrigerant leak (high superheat, low suction)",
"Find & repair LEAK, then recharge refrigerant"))
elif sh < 5 and sp > 140:
faults.append((3, "Overcharge or flooding (low superheat, high suction)",
"Recover refrigerant / check metering device (TXV)"))
# --- condenser / head pressure ---
if dp > 400:
faults.append((3, "High head pressure",
"Dirty CONDENSER COIL, failed condenser FAN, or overcharge"))
# --- comfort cross-checks (only meaningful while running) ---
if amps >= 2 and dt > 25:
faults.append((2, "Very high temperature split - low airflow / possible frozen coil",
"Check FILTER & BLOWER; let coil thaw"))
if amps >= 2 and dt < 12 and sh < 20 and sp >= 100:
faults.append((2, "Low temperature split - weak cooling",
"Suspect low charge, airflow, or compressor efficiency"))
if not faults:
return {"status": "OK", "severity": 0,
"faults": [{"fault": "System operating normally",
"action": "No action needed"}]}
faults.sort(key=lambda f: f[0], reverse=True)
return {"status": "FAULT", "severity": faults[0][0],
"faults": [{"fault": f[1], "action": f[2]} for f in faults]}
# --------------------------- WIFI ------------------------------------
def wifi_connect():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print("Connecting to Wi-Fi...")
wlan.connect(WIFI_SSID, WIFI_PASS)
while not wlan.isconnected():
time.sleep(0.2)
print("Wi-Fi connected:", wlan.ifconfig()[0])
# --------------------------- READ + LOOP -----------------------------
# DHT22 in the simulator can throw on any single read, so we retry a few
# times and keep the last good value instead of snapping to a fallback.
_last_dht = {"t": 75.0, "h": 50.0}
def read_dht():
for _ in range(3):
try:
dht_sensor.measure()
_last_dht["t"] = c_to_f(dht_sensor.temperature())
_last_dht["h"] = dht_sensor.humidity()
return _last_dht["t"], _last_dht["h"]
except Exception:
time.sleep_ms(400)
return _last_dht["t"], _last_dht["h"] # reuse last good reading
def read_all():
return_f, humidity = read_dht()
supply_f = scaled(POTS["supply_air_f"], 40, 75)
suction = scaled(POTS["suction_psi"], 70, 170)
suc_line_f = scaled(POTS["suction_line_f"], 30, 75)
discharge = scaled(POTS["discharge_psi"], 150, 450)
amps = scaled(POTS["compressor_amps"], 0, 30)
static = scaled(POTS["static_press_inwc"], 0.0, 1.2)
superheat = suc_line_f - sat_temp_f(suction)
delta_t = return_f - supply_f
return {
"return_air_f": round(return_f, 1),
"humidity_pct": round(humidity, 1),
"supply_air_f": round(supply_f, 1),
"suction_psi": round(suction, 1),
"suction_line_f": round(suc_line_f, 1),
"discharge_psi": round(discharge, 1),
"compressor_amps": round(amps, 1),
"static_press_inwc": round(static, 2),
"superheat_f": round(superheat, 1),
"delta_t_f": round(delta_t, 1),
}
def main():
wifi_connect()
time.sleep(2) # let the DHT22 settle before the first read
client = None
if HAVE_MQTT:
try:
client = MQTTClient(CLIENT_ID, MQTT_BROKER, port=MQTT_PORT, keepalive=30)
client.connect()
print("MQTT connected to", MQTT_BROKER, "topic:", TOPIC)
except Exception as e:
print("MQTT connect failed:", e)
while True:
m = read_all()
dx = diagnose(m)
m["diagnosis"] = dx
print("-" * 52)
print("SuperheatF={} DeltaTF={} Suction={}psi Disch={}psi Amps={} Static={}\"".format(
m["superheat_f"], m["delta_t_f"], m["suction_psi"],
m["discharge_psi"], m["compressor_amps"], m["static_press_inwc"]))
print("STATUS:", dx["status"], "->", dx["faults"][0]["fault"])
print(" ACTION:", dx["faults"][0]["action"])
if client:
try:
client.publish(TOPIC, ujson.dumps(m))
except Exception as e:
print("Publish failed:", e)
time.sleep(2)
main()