3 Commits

Author SHA1 Message Date
6209cb0b2d Troubleshooting 2026-04-12 02:42:07 +02:00
179f202dfe Troubleshooting 2026-04-12 02:38:14 +02:00
906922357d Troubleshooting 2026-04-12 02:24:36 +02:00
2 changed files with 174 additions and 159 deletions

View File

@@ -108,7 +108,7 @@ class Gauge:
else: else:
for _ in range(abs(delta)): for _ in range(abs(delta)):
self._step(1 if delta > 0 else -1) self._step(1 if delta > 0 else -1)
utime.sleep_us(self._step_us) utime.sleep_ms(self._step_us)
self._current_step = target_step self._current_step = target_step

View File

@@ -23,7 +23,6 @@ Serial log format: [HH:MM:SS.mmm] LEVEL message
import network import network
import utime import utime
import ujson import ujson
import urandom
from umqtt.robust import MQTTClient from umqtt.robust import MQTTClient
from machine import Pin from machine import Pin
from neopixel import NeoPixel from neopixel import NeoPixel
@@ -156,6 +155,7 @@ BL_UNIT = _cfg.get("backlight_unit", "%")
# Gauge initialization # Gauge initialization
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
info("Initialising gauge objects...")
gauge_objects = [] gauge_objects = []
for g in gauges: for g in gauges:
gauge_objects.append( gauge_objects.append(
@@ -170,6 +170,7 @@ for g in gauges:
info( info(
f"Gauge {g['id']}: {g['name']} pins={g['pins']} mode={g['mode']} range=[{g['min']}, {g['max']}]" f"Gauge {g['id']}: {g['name']} pins={g['pins']} mode={g['mode']} range=[{g['min']}, {g['max']}]"
) )
info("Gauge objects done")
gauge_targets = [g["min"] for g in gauges] # target value per gauge gauge_targets = [g["min"] for g in gauges] # target value per gauge
gauge_last_rezero = [utime.ticks_ms() for _ in gauges] gauge_last_rezero = [utime.ticks_ms() for _ in gauges]
@@ -206,7 +207,6 @@ def make_gauge_topics(prefix, gauge_id):
gauge_topics = [make_gauge_topics(MQTT_PREFIX, g["id"]) for g in gauges] gauge_topics = [make_gauge_topics(MQTT_PREFIX, g["id"]) for g in gauges]
T_SET = f"{MQTT_PREFIX}/set" T_SET = f"{MQTT_PREFIX}/set"
T_STATE = f"{MQTT_PREFIX}/state" T_STATE = f"{MQTT_PREFIX}/state"
T_STATUS = f"{MQTT_PREFIX}/status" T_STATUS = f"{MQTT_PREFIX}/status"
@@ -218,6 +218,31 @@ T_DISC_GREEN = f"homeassistant/switch/{MQTT_CLIENT_ID}_green/config"
T_DISC_BL = f"homeassistant/light/{MQTT_CLIENT_ID}_bl/config" T_DISC_BL = f"homeassistant/light/{MQTT_CLIENT_ID}_bl/config"
def make_gauge_topics(prefix, gauge_id):
return {
"set": f"{prefix}/gauge{gauge_id}/set",
"state": f"{prefix}/gauge{gauge_id}/state",
"status": f"{prefix}/gauge{gauge_id}/status",
"zero": f"{prefix}/gauge{gauge_id}/zero",
"disc": f"homeassistant/number/{MQTT_CLIENT_ID}_g{gauge_id}/config",
"led_red": f"{prefix}/gauge{gauge_id}/led/red/set",
"led_green": f"{prefix}/gauge{gauge_id}/led/green/set",
"led_bl": f"{prefix}/gauge{gauge_id}/led/backlight/set",
"led_red_state": f"{prefix}/gauge{gauge_id}/led/red/state",
"led_green_state": f"{prefix}/gauge{gauge_id}/led/green/state",
"led_bl_state": f"{prefix}/gauge{gauge_id}/led/backlight/state",
"led_red_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_g{gauge_id}_red/config",
"led_green_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_g{gauge_id}_green/config",
"led_bl_disc": f"homeassistant/light/{MQTT_CLIENT_ID}_g{gauge_id}_bl/config",
"status_red": f"{prefix}/gauge{gauge_id}/status_led/red/set",
"status_green": f"{prefix}/gauge{gauge_id}/status_led/green/set",
"status_red_state": f"{prefix}/gauge{gauge_id}/status_led/red/state",
"status_green_state": f"{prefix}/gauge{gauge_id}/status_led/green/state",
"status_red_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_g{gauge_id}_status_red/config",
"status_green_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_g{gauge_id}_status_green/config",
}
_DEVICE = { _DEVICE = {
"identifiers": [MQTT_CLIENT_ID], "identifiers": [MQTT_CLIENT_ID],
"name": DEVICE_NAME, "name": DEVICE_NAME,
@@ -235,6 +260,7 @@ _wifi_check_interval_ms = 30000
_last_wifi_check = 0 _last_wifi_check = 0
_wifi_sta = None _wifi_sta = None
def connect_wifi(ssid, password, timeout_s=15): def connect_wifi(ssid, password, timeout_s=15):
global _wifi_sta global _wifi_sta
_wifi_sta = network.WLAN(network.STA_IF) _wifi_sta = network.WLAN(network.STA_IF)
@@ -290,6 +316,11 @@ def check_wifi():
log_err(f"WiFi reconnect failed: {e}") log_err(f"WiFi reconnect failed: {e}")
# ---------------------------------------------------------------------------
# LEDs (per gauge)
# ---------------------------------------------------------------------------
info("Initialising LEDs...")
num_gauges = len(gauges) num_gauges = len(gauges)
leds_red = [] leds_red = []
leds_green = [] leds_green = []
@@ -299,7 +330,13 @@ for g in gauges:
leds_green.append(Pin(g["green_pin"], Pin.OUT, value=0)) leds_green.append(Pin(g["green_pin"], Pin.OUT, value=0))
total_backlight_leds = num_gauges * (BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE) total_backlight_leds = num_gauges * (BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE)
leds_bl = NeoPixel(Pin(BACKLIGHT_PIN), total_backlight_leds) info(f"Total backlight LEDs: {total_backlight_leds}")
leds_bl = (
NeoPixel(Pin(BACKLIGHT_PIN), total_backlight_leds)
if total_backlight_leds > 0
else None
)
info("LEDs done")
backlight_color = [(0, 0, 0) for _ in range(num_gauges)] backlight_color = [(0, 0, 0) for _ in range(num_gauges)]
backlight_brightness = [100 for _ in range(num_gauges)] backlight_brightness = [100 for _ in range(num_gauges)]
@@ -312,6 +349,24 @@ _bl_dirty_since = None
_BL_SAVE_DELAY_MS = 5000 _BL_SAVE_DELAY_MS = 5000
def _flush_backlight(client):
for i in range(num_gauges):
gt = gauge_topics[i]
payload = {
"state": "ON" if backlight_on[i] else "OFF",
"color": {
"r": backlight_color[i][0],
"g": backlight_color[i][1],
"b": backlight_color[i][2],
},
"brightness": int(backlight_brightness[i] * 2.55),
}
client.publish(gt["led_bl_state"], ujson.dumps(payload), retain=True)
info(
f"Gauge {i} backlight: {payload['state']} {backlight_color[i]} @ {backlight_brightness[i]}%"
)
def _backlight_changed(gauge_idx, new_color, new_on, new_brightness): def _backlight_changed(gauge_idx, new_color, new_on, new_brightness):
return ( return (
new_color != backlight_color[gauge_idx] new_color != backlight_color[gauge_idx]
@@ -341,8 +396,10 @@ def set_backlight_color(gauge_idx, r, g, b, brightness=None):
leds_per_gauge = BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE leds_per_gauge = BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE
base_idx = gauge_idx * leds_per_gauge base_idx = gauge_idx * leds_per_gauge
for j in range(BACKLIGHT_LEDS_PER_GAUGE): for j in range(BACKLIGHT_LEDS_PER_GAUGE):
if leds_bl:
leds_bl[base_idx + j] = (int(g * scale), int(r * scale), int(b * scale)) leds_bl[base_idx + j] = (int(g * scale), int(r * scale), int(b * scale))
_update_status_leds(gauge_idx) _update_status_leds(gauge_idx)
if leds_bl:
leds_bl.write() leds_bl.write()
_mark_bl_dirty() _mark_bl_dirty()
@@ -361,13 +418,17 @@ def set_backlight_brightness(gauge_idx, brightness):
leds_per_gauge = BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE leds_per_gauge = BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE
base_idx = gauge_idx * leds_per_gauge base_idx = gauge_idx * leds_per_gauge
for j in range(BACKLIGHT_LEDS_PER_GAUGE): for j in range(BACKLIGHT_LEDS_PER_GAUGE):
if leds_bl:
leds_bl[base_idx + j] = (int(g * scale), int(r * scale), int(b * scale)) leds_bl[base_idx + j] = (int(g * scale), int(r * scale), int(b * scale))
_update_status_leds(gauge_idx) _update_status_leds(gauge_idx)
if leds_bl:
leds_bl.write() leds_bl.write()
_mark_bl_dirty() _mark_bl_dirty()
def _update_status_leds(gauge_idx): def _update_status_leds(gauge_idx):
if not leds_bl:
return
leds_per_gauge = BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE leds_per_gauge = BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE
base_idx = gauge_idx * leds_per_gauge + BACKLIGHT_LEDS_PER_GAUGE base_idx = gauge_idx * leds_per_gauge + BACKLIGHT_LEDS_PER_GAUGE
@@ -393,38 +454,18 @@ def set_status_led(gauge_idx, led_type, state):
elif led_type == "green": elif led_type == "green":
status_led_green[gauge_idx] = state status_led_green[gauge_idx] = state
_update_status_leds(gauge_idx) _update_status_leds(gauge_idx)
if leds_bl:
leds_bl.write() leds_bl.write()
def publish_backlight_states(client): # ---------------------------------------------------------------------------
"""Publish current backlight state for all gauges as retained MQTT messages.""" # State
for i in range(num_gauges): # ---------------------------------------------------------------------------
gt = gauge_topics[i]
r, g, b = backlight_color[i]
brightness = backlight_brightness[i]
state = {
"state": "ON" if backlight_on[i] else "OFF",
"color_mode": "rgb",
"brightness": int(brightness * 2.55),
"color": {"r": r, "g": g, "b": b},
}
try:
client.publish(gt["led_bl_state"], ujson.dumps(state), retain=True)
except Exception as e:
log_err(f"Backlight state publish failed for gauge {i}: {e}")
_last_rezero_ms = None # set to ticks_ms() in main()
def _flush_backlight_state(): client_ref = None
global _bl_dirty_since _mqtt_connected = False
if _bl_dirty_since is None: _last_mqtt_check = 0
return
if utime.ticks_diff(utime.ticks_ms(), _bl_dirty_since) < _BL_SAVE_DELAY_MS:
return
if client_ref is None:
return
publish_backlight_states(client_ref)
_bl_dirty_since = None
info("Backlight state flushed to MQTT")
def _publish(topic, payload, retain=False): def _publish(topic, payload, retain=False):
@@ -445,7 +486,6 @@ def _publish(topic, payload, retain=False):
def on_message(topic, payload): def on_message(topic, payload):
global backlight_brightness, backlight_color
if client_ref is None: if client_ref is None:
return return
topic = topic.decode() topic = topic.decode()
@@ -469,6 +509,15 @@ def on_message(topic, payload):
warn(f"Invalid set value for gauge {i}: '{payload}'") warn(f"Invalid set value for gauge {i}: '{payload}'")
return return
if topic == T_ZERO:
for i, g in enumerate(gauge_objects):
info(f"Zeroing all gauges")
g.zero()
gauge_last_rezero[i] = utime.ticks_ms()
info("All gauges zeroed")
return
for i, gt in enumerate(gauge_topics):
if topic == gt["led_red"]: if topic == gt["led_red"]:
state = payload.upper() == "ON" state = payload.upper() == "ON"
leds_red[i].value(1 if state else 0) leds_red[i].value(1 if state else 0)
@@ -532,59 +581,12 @@ def on_message(topic, payload):
info(f"Gauge {i} status green → {'ON' if state else 'OFF'}") info(f"Gauge {i} status green → {'ON' if state else 'OFF'}")
return return
if topic == T_ZERO:
for i, g in enumerate(gauge_objects):
g.zero()
gauge_last_rezero[i] = utime.ticks_ms()
info("All gauges zeroed")
return
if topic == T_SET:
try:
data = ujson.loads(payload)
if isinstance(data, dict):
for i, val in enumerate(data.values()):
if i < len(gauges):
g = gauges[i]
gauge_targets[i] = max(g["min"], min(g["max"], float(val)))
info(f"Gauge {i} target → {gauge_targets[i]:.1f}")
else:
val = float(payload)
for i in range(len(gauges)):
gauge_targets[i] = max(gauges[i]["min"], min(gauges[i]["max"], val))
info(f"All gauges target → {val:.1f}")
except Exception:
try:
val = float(payload)
for i in range(len(gauges)):
gauge_targets[i] = max(gauges[i]["min"], min(gauges[i]["max"], val))
info(f"All gauges target → {val:.1f}")
except:
warn(f"Invalid set value: '{payload}'")
return
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# MQTT connect + discovery # MQTT connect + discovery
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _subscribe_all(c):
c.subscribe(f"{MQTT_PREFIX}/set")
c.subscribe(f"{MQTT_PREFIX}/zero")
for i in range(num_gauges):
prefix = f"{MQTT_PREFIX}/gauge{i}"
c.subscribe(f"{prefix}/set")
c.subscribe(f"{prefix}/zero")
c.subscribe(f"{prefix}/led/red/set")
c.subscribe(f"{prefix}/led/green/set")
c.subscribe(f"{prefix}/led/backlight/set")
c.subscribe(f"{prefix}/status_led/red/set")
c.subscribe(f"{prefix}/status_led/green/set")
def connect_mqtt(): def connect_mqtt():
global client_ref, _mqtt_connected global client_ref, _mqtt_connected
info(f"Connecting to MQTT broker {MQTT_BROKER}:{MQTT_PORT} ...") info(f"Connecting to MQTT broker {MQTT_BROKER}:{MQTT_PORT} ...")
@@ -594,22 +596,29 @@ def connect_mqtt():
port=MQTT_PORT, port=MQTT_PORT,
user=MQTT_USER, user=MQTT_USER,
password=MQTT_PASSWORD, password=MQTT_PASSWORD,
keepalive=30, keepalive=60,
) )
# Don't set last will - it might be causing issues client.set_last_will(gauge_topics[0]["status"], b"offline", retain=True, qos=0)
# client.set_last_will(T_STATUS, b"offline", retain=True, qos=0)
client.set_callback(on_message) client.set_callback(on_message)
client.connect() client.connect()
client_ref = client client_ref = client
client.subscribe(T_SET)
client.subscribe(T_ZERO)
for gt in gauge_topics:
client.subscribe(gt["set"])
client.subscribe(gt["zero"])
client.subscribe(gt["led_red"])
client.subscribe(gt["led_green"])
client.subscribe(gt["led_bl"])
client.subscribe(gt["status_red"])
client.subscribe(gt["status_green"])
_mqtt_connected = True _mqtt_connected = True
info(f"MQTT connected client_id={MQTT_CLIENT_ID}") info(f"MQTT connected client_id={MQTT_CLIENT_ID}")
return client
_mqtt_check_interval_ms = 30000 _mqtt_check_interval_ms = 30000
_last_mqtt_check = 0 _last_mqtt_check = 0
client_ref = None
_mqtt_connected = False
def check_mqtt(): def check_mqtt():
@@ -642,10 +651,21 @@ def check_mqtt():
password=MQTT_PASSWORD, password=MQTT_PASSWORD,
keepalive=60, keepalive=60,
) )
client_ref.set_last_will(T_STATUS, b"offline", retain=True, qos=0) client_ref.set_last_will(
gauge_topics[0]["status"], b"offline", retain=True, qos=0
)
client_ref.set_callback(on_message) client_ref.set_callback(on_message)
client_ref.connect() client_ref.connect()
_subscribe_all(client_ref) client_ref.subscribe(T_SET)
client_ref.subscribe(T_ZERO)
for gt in gauge_topics:
client_ref.subscribe(gt["set"])
client_ref.subscribe(gt["zero"])
client_ref.subscribe(gt["led_red"])
client_ref.subscribe(gt["led_green"])
client_ref.subscribe(gt["led_bl"])
client_ref.subscribe(gt["status_red"])
client_ref.subscribe(gt["status_green"])
_mqtt_connected = True _mqtt_connected = True
info("MQTT reconnected!") info("MQTT reconnected!")
publish_discovery(client_ref) publish_discovery(client_ref)
@@ -659,10 +679,15 @@ def check_mqtt():
return False return False
def publish_discovery(client): def publish_discovery(client):
"""Publish all HA MQTT discovery payloads for gauges and LEDs.""" """Publish all HA MQTT discovery payloads for gauges and LEDs."""
_dev_ref = _DEVICE _dev_ref = {
"identifiers": [MQTT_CLIENT_ID],
"name": DEVICE_NAME,
"model": DEVICE_MODEL,
"manufacturer": DEVICE_MFR,
"suggested_area": DEVICE_AREA,
}
for i, g in enumerate(gauges): for i, g in enumerate(gauges):
gt = gauge_topics[i] gt = gauge_topics[i]
@@ -688,11 +713,6 @@ def publish_discovery(client):
) )
info(f"Discovery: gauge {i} ({g['name']})") info(f"Discovery: gauge {i} ({g['name']})")
# Process MQTT messages between gauges
for _ in range(5):
client.check_msg()
utime.sleep_ms(10)
client.publish( client.publish(
gt["led_red_disc"], gt["led_red_disc"],
ujson.dumps( ujson.dumps(
@@ -731,11 +751,6 @@ def publish_discovery(client):
) )
info(f"Discovery: gauge {i} green LED") info(f"Discovery: gauge {i} green LED")
# Process MQTT messages
for _ in range(5):
client.check_msg()
utime.sleep_ms(10)
client.publish( client.publish(
gt["led_bl_disc"], gt["led_bl_disc"],
ujson.dumps( ujson.dumps(
@@ -793,17 +808,14 @@ def publish_discovery(client):
) )
info(f"Discovery: gauge {i} status green") info(f"Discovery: gauge {i} status green")
# Process between gauges to avoid MQTT blocking
for _ in range(5):
client.check_msg()
utime.sleep_ms(10)
def publish_state(client): def publish_state(client):
for i, g in enumerate(gauge_objects): for i, g in enumerate(gauge_objects):
gt = gauge_topics[i] gt = gauge_topics[i]
val = g.get() val = g.get()
client.publish(gt["state"], str(val)) client.publish(gt["state"], str(round(val, 1)), retain=True)
client.publish(gt["status"], "online", retain=True)
info(f"Gauge {i} state: {val:.1f} step={g._current_step}")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -812,58 +824,47 @@ def publish_state(client):
def main(): def main():
utime.sleep_ms(0)
info("=" * 48) info("=" * 48)
info("Gauge MQTT controller starting") info("Gauge MQTT controller starting")
info("=" * 48) info("=" * 48)
info("Connecting WiFi...")
connect_wifi(WIFI_SSID, WIFI_PASSWORD) connect_wifi(WIFI_SSID, WIFI_PASSWORD)
info("WiFi done")
# Connect MQTT (no subscriptions yet — keeps broker silent during discovery)
connect_mqtt()
# Publish discovery — broker has nothing to send back yet
info("Publishing discovery...")
publish_discovery(client_ref)
# Subscribe now — retained messages will start arriving from here
_subscribe_all(client_ref)
info("Draining initial retained messages...")
for _ in range(50):
client_ref.check_msg()
utime.sleep_ms(20)
# Now initialize gauges
info("Zeroing gauges on startup ...") info("Zeroing gauges on startup ...")
for i, g in enumerate(gauge_objects): for i, g in enumerate(gauge_objects):
g.zero() g.zero()
info(f"Zeroed gauge {i}") info(f"Zeroed gauge {i}")
info("Zero complete") info("Zero done")
info("Publishing state...") info("Connecting MQTT...")
connect_mqtt()
info("MQTT done")
info("Publishing discovery...")
publish_discovery(client_ref)
publish_state(client_ref) publish_state(client_ref)
utime.sleep_ms(50) info("Discovery done")
for _ in range(5):
client_ref.check_msg()
utime.sleep_ms(20)
info("Entering main loop") info("Entering main loop")
info("-" * 48) info("-" * 48)
try: try:
import ota import ota
ota.mark_ok() ota.mark_ok()
except: info("OTA OK flag set")
except ImportError:
pass pass
# Initialize variables for main loop global _bl_dirty_since
last_heartbeat = utime.ticks_ms() last_heartbeat = utime.ticks_ms()
now = 0
was_moving = False
while True: while True:
utime.sleep_ms(0)
try: try:
now = utime.ticks_ms()
check_wifi() check_wifi()
if not check_mqtt(): if not check_mqtt():
@@ -872,35 +873,49 @@ def main():
client_ref.check_msg() client_ref.check_msg()
pending = [] now = utime.ticks_ms()
for i, g in enumerate(gauge_objects):
delta = g._val_to_step(gauge_targets[i]) - g._current_step
steps = max(-5, min(5, delta))
pending.append(steps)
moved_any = any(s != 0 for s in pending) moved_any = False
if moved_any:
was_moving = True
delay_us = 1_000_000 // MICROSTEPS_PER_SECOND
for tick in range(max(abs(s) for s in pending)):
for i, g in enumerate(gauge_objects): for i, g in enumerate(gauge_objects):
if tick < abs(pending[i]): current_target = g._val_to_step(gauge_targets[i])
g.step(1 if pending[i] > 0 else -1) if current_target != g._current_step:
direction = 1 if current_target > g._current_step else -1
steps_to_move = current_target - g._current_step
steps_to_move = max(-5, min(5, steps_to_move))
for _ in range(abs(steps_to_move)):
g.step(direction)
moved_any = True
if moved_any:
delay_us = 1_000_000 // MICROSTEPS_PER_SECOND
utime.sleep_us(delay_us) utime.sleep_us(delay_us)
else:
if was_moving: if (
REZERO_INTERVAL_MS > 0
and utime.ticks_diff(now, gauge_last_rezero[0]) >= REZERO_INTERVAL_MS
):
for i, g in enumerate(gauge_objects):
info(f"Auto-rezero gauge {i}")
saved = gauge_targets[i]
g.zero()
if saved > gauges[i]["min"]:
g.set(saved)
gauge_last_rezero[i] = now
publish_state(client_ref) publish_state(client_ref)
was_moving = False info("Auto-rezero complete")
utime.sleep_ms(10)
if (
_bl_dirty_since is not None
and utime.ticks_diff(now, _bl_dirty_since) >= _BL_SAVE_DELAY_MS
):
_flush_backlight(client_ref)
_bl_dirty_since = None
if utime.ticks_diff(now, last_heartbeat) >= HEARTBEAT_MS: if utime.ticks_diff(now, last_heartbeat) >= HEARTBEAT_MS:
info(f"Heartbeat: {gauge_targets}")
publish_state(client_ref) publish_state(client_ref)
last_heartbeat = now last_heartbeat = now
except Exception as e: except Exception as e:
import sys
sys.print_exception(e)
log_err(f"Main loop error: {e} — continuing") log_err(f"Main loop error: {e} — continuing")
utime.sleep_ms(100) utime.sleep_ms(100)