8 Commits

5 changed files with 2609 additions and 161 deletions

View File

@@ -58,7 +58,7 @@ The sketch controls `GAUGE_COUNT` stepper-motor gauges using a trapezoidal veloc
### Key data structures ### Key data structures
- `GaugePins` — hardware pin mapping per gauge (dir, step, enable, active-high/low polarity flags, `ledCount`). Declared `constexpr` so `TOTAL_LEDS` can be computed from it at compile time. Configured in the `gaugePins[]` array at the top. - `GaugePins` — hardware pin mapping per gauge (dir, step, enable, active-high/low polarity flags, `ledOrder` string). Declared `constexpr` so `TOTAL_LEDS` can be computed from it at compile time. Configured in the `gaugePins[]` array at the top.
- `Gauge` — per-gauge runtime state: position, target, velocity, accel, homing state machine, sweep mode. - `Gauge` — per-gauge runtime state: position, target, velocity, accel, homing state machine, sweep mode.
### Motion control (`updateGauge`) ### Motion control (`updateGauge`)
@@ -76,7 +76,7 @@ When `sweepEnabled`, `updateSweepTarget` bounces `targetPos` between `minPos` an
### LED strip ### LED strip
One shared WS2812B strip is driven from `LED_DATA_PIN` (currently 22). Each gauge owns a contiguous segment of the strip; `gaugePins[i].ledCount` sets the segment length (0 = no LEDs). `TOTAL_LEDS` is computed at compile time via `constexpr sumLedCounts()` — no manual constant to keep in sync. Per-gauge offsets into the flat `leds[]` array are computed once in `setup()` into `gaugeLedOffset[]`. LED commands and effects mark the strip dirty, and `FastLED.show()` is called once per main-loop iteration if anything changed. One shared WS2812B strip is driven from `LED_DATA_PIN` (currently 22). Each gauge owns a contiguous segment of the strip; `gaugePins[i].ledOrder` is a per-LED type string (one char per LED, `'G'` = GRB-ordered, `'R'` = RGB-ordered) and its length defines the segment length (empty string = no LEDs). `TOTAL_LEDS` is computed at compile time via `constexpr sumLedCounts()` — no manual constant to keep in sync. Per-gauge offsets and counts are cached in `setup()` into `gaugeLedOffset[]` and `gaugeLedCount[]`. The strip is initialised as `GRB`; writes to RGB-ordered LEDs are R/G-swapped via the `writeLed`/`readLed` helpers so callers always work in logical RGB. LED commands and effects mark the strip dirty, and `FastLED.show()` is called once per main-loop iteration if anything changed.
### Serial command protocol ### Serial command protocol
@@ -104,6 +104,6 @@ All commands reply `OK` or `ERR BAD_ID` / `ERR BAD_CMD` etc.
### Adding gauges ### Adding gauges
1. Increment `GAUGE_COUNT`. 1. Increment `GAUGE_COUNT`.
2. Add a `constexpr GaugePins` entry to `gaugePins[]` (including `ledCount`). 2. Add a `constexpr GaugePins` entry to `gaugePins[]` (including the `ledOrder` string — one char per LED, `'G'` for GRB or `'R'` for RGB).
3. Tune `maxPos` and `homingBackoffSteps` in the corresponding `Gauge` default or at runtime. 3. Tune `maxPos` and `homingBackoffSteps` in the corresponding `Gauge` default or at runtime.
4. `TOTAL_LEDS` and `gaugeLedOffset[]` update automatically — no manual changes needed. 4. `TOTAL_LEDS`, `gaugeLedOffset[]`, and `gaugeLedCount[]` update automatically — no manual changes needed.

View File

@@ -3,7 +3,7 @@
#include <math.h> #include <math.h>
#include <FastLED.h> #include <FastLED.h>
static const uint8_t GAUGE_COUNT = 3; static const uint8_t GAUGE_COUNT = 4;
// One shared WS2812B strip, split into per-gauge segments. // One shared WS2812B strip, split into per-gauge segments.
static const uint8_t LED_DATA_PIN = 22; static const uint8_t LED_DATA_PIN = 22;
@@ -226,18 +226,23 @@ struct GaugePins {
bool dirInverted; bool dirInverted;
bool stepActiveHigh; bool stepActiveHigh;
bool enableActiveLow; bool enableActiveLow;
uint8_t ledCount; // LEDs assigned to this gauge const char* ledOrder; // one char per LED: 'G' = GRB, 'R' = RGB; length defines ledCount
}; };
constexpr GaugePins gaugePins[GAUGE_COUNT] = { constexpr GaugePins gaugePins[GAUGE_COUNT] = {
// dir, step, en, dirInv, stepHigh, enActiveLow, leds // dir, step, en, dirInv, stepHigh, enActiveLow, ledOrder
{50, 51, -1, false, true, true, 7}, // Gauge 0 {50, 51, -1, false, true, true, "RRRGGRR"}, // Gauge 0
{8, 9, -1, true, true, true, 7}, // Gauge 1 {8, 9, -1, true, true, true, "GGGRRRR"}, // Gauge 1
{52, 53, -1, false, true, true, 7}, // Gauge 2 {52, 53, -1, false, true, true, "GGGRRRR"}, // Gauge 2
{48, 49, -1, false, true, true, "GGGRRRR"}, // Gauge 3
}; };
constexpr uint8_t cstrLen(const char* s) {
return *s ? uint8_t(1 + cstrLen(s + 1)) : uint8_t(0);
}
constexpr uint8_t sumLedCounts(uint8_t i = 0) { constexpr uint8_t sumLedCounts(uint8_t i = 0) {
return i >= GAUGE_COUNT ? 0 : gaugePins[i].ledCount + sumLedCounts(i + 1); return i >= GAUGE_COUNT ? 0 : cstrLen(gaugePins[i].ledOrder) + sumLedCounts(i + 1);
} }
static const uint8_t TOTAL_LEDS = sumLedCounts(); static const uint8_t TOTAL_LEDS = sumLedCounts();
@@ -297,9 +302,40 @@ String rxLine;
CRGB leds[TOTAL_LEDS]; CRGB leds[TOTAL_LEDS];
uint8_t gaugeLedOffset[GAUGE_COUNT]; uint8_t gaugeLedOffset[GAUGE_COUNT];
uint8_t gaugeLedCount[GAUGE_COUNT];
BlinkState blinkState[TOTAL_LEDS]; BlinkState blinkState[TOTAL_LEDS];
bool ledsDirty = false; bool ledsDirty = false;
// FastLED drives the shared strip as RGB. Each gauge's ledOrder string marks per-LED
// type ('R' = RGB, 'G' = GRB); writes to GRB-ordered LEDs pre-swap R and G to compensate.
inline bool ledNeedsRgSwap(uint8_t globalIdx) {
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
uint8_t off = gaugeLedOffset[i];
if (globalIdx >= off && globalIdx < off + gaugeLedCount[i]) {
char c = gaugePins[i].ledOrder[globalIdx - off];
return c == 'G' || c == 'g';
}
}
return false;
}
inline CRGB encodeForStrip(uint8_t globalIdx, CRGB color) {
if (ledNeedsRgSwap(globalIdx)) {
uint8_t tmp = color.r;
color.r = color.g;
color.g = tmp;
}
return color;
}
inline void writeLed(uint8_t globalIdx, CRGB color) {
leds[globalIdx] = encodeForStrip(globalIdx, color);
}
inline CRGB readLed(uint8_t globalIdx) {
return encodeForStrip(globalIdx, leds[globalIdx]);
}
// Sends one-line command replies back over the control port. // Sends one-line command replies back over the control port.
// //
// Serial protocol summary. // Serial protocol summary.
@@ -794,6 +830,24 @@ bool parsePosQuery(const String& line) {
return false; return false;
} }
// Answers `CFG?` with speed and acceleration for every gauge.
// Emits one `CFG <id> <maxSpeed> <accel>` line per gauge, then replies `OK`.
bool parseCfgQuery(const String& line) {
if (line == "CFG?") {
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
CMD_PORT.print("CFG ");
CMD_PORT.print(i);
CMD_PORT.print(' ');
CMD_PORT.print((int)gauges[i].maxSpeed);
CMD_PORT.print(' ');
CMD_PORT.println((int)gauges[i].accel);
}
sendReply("OK");
return true;
}
return false;
}
// Answers the mandatory life question: are you there? // Answers the mandatory life question: are you there?
// Reply: `PONG`. // Reply: `PONG`.
bool parsePing(const String& line) { bool parsePing(const String& line) {
@@ -835,8 +889,8 @@ bool parseVfd(const String& line) {
bool parseLedQuery(const String& line) { bool parseLedQuery(const String& line) {
if (line == "LED?") { if (line == "LED?") {
for (uint8_t i = 0; i < GAUGE_COUNT; i++) { for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
for (uint8_t j = 0; j < gaugePins[i].ledCount; j++) { for (uint8_t j = 0; j < gaugeLedCount[i]; j++) {
const CRGB& c = leds[gaugeLedOffset[i] + j]; CRGB c = readLed(gaugeLedOffset[i] + j);
CMD_PORT.print("LED "); CMD_PORT.print("LED ");
CMD_PORT.print(i); CMD_PORT.print(i);
CMD_PORT.print(' '); CMD_PORT.print(' ');
@@ -865,13 +919,13 @@ bool parseLed(const String& line) {
char* dash = strchr(idxToken, '-'); char* dash = strchr(idxToken, '-');
int idxFirst = atoi(idxToken); int idxFirst = atoi(idxToken);
int idxLast = dash ? atoi(dash + 1) : idxFirst; int idxLast = dash ? atoi(dash + 1) : idxFirst;
if (idxFirst < 0 || idxLast >= gaugePins[id].ledCount || idxFirst > idxLast) { if (idxFirst < 0 || idxLast >= gaugeLedCount[id] || idxFirst > idxLast) {
sendReply("ERR BAD_IDX"); return true; sendReply("ERR BAD_IDX"); return true;
} }
CRGB color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255)); CRGB color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
for (int i = idxFirst; i <= idxLast; i++) { for (int i = idxFirst; i <= idxLast; i++) {
blinkState[gaugeLedOffset[id] + i].active = false; blinkState[gaugeLedOffset[id] + i].active = false;
leds[gaugeLedOffset[id] + i] = color; writeLed(gaugeLedOffset[id] + i, color);
} }
ledsDirty = true; ledsDirty = true;
sendReply("OK"); sendReply("OK");
@@ -894,7 +948,7 @@ bool parseBlink(const String& line) {
char* dash = strchr(idxToken, '-'); char* dash = strchr(idxToken, '-');
int idxFirst = atoi(idxToken); int idxFirst = atoi(idxToken);
int idxLast = dash ? atoi(dash + 1) : idxFirst; int idxLast = dash ? atoi(dash + 1) : idxFirst;
if (idxFirst < 0 || idxLast >= gaugePins[id].ledCount || idxFirst > idxLast) { if (idxFirst < 0 || idxLast >= gaugeLedCount[id] || idxFirst > idxLast) {
sendReply("ERR BAD_IDX"); return true; sendReply("ERR BAD_IDX"); return true;
} }
@@ -915,13 +969,13 @@ bool parseBlink(const String& line) {
uint8_t globalIdx = gaugeLedOffset[id] + i; uint8_t globalIdx = gaugeLedOffset[id] + i;
BlinkState& bs = blinkState[globalIdx]; BlinkState& bs = blinkState[globalIdx];
bs.fx = FX_BLINK; bs.fx = FX_BLINK;
bs.onColor = (count == 7) ? color : leds[globalIdx]; bs.onColor = (count == 7) ? color : readLed(globalIdx);
bs.onMs = (uint16_t)onMs; bs.onMs = (uint16_t)onMs;
bs.offMs = (uint16_t)offMs; bs.offMs = (uint16_t)offMs;
bs.currentlyOn = true; bs.currentlyOn = true;
bs.lastMs = nowMs; bs.lastMs = nowMs;
bs.active = true; bs.active = true;
leds[globalIdx] = bs.onColor; writeLed(globalIdx, bs.onColor);
} }
ledsDirty = true; ledsDirty = true;
sendReply("OK"); sendReply("OK");
@@ -939,7 +993,7 @@ bool parseBreathe(const String& line) {
char* dash = strchr(idxToken, '-'); char* dash = strchr(idxToken, '-');
int idxFirst = atoi(idxToken); int idxFirst = atoi(idxToken);
int idxLast = dash ? atoi(dash + 1) : idxFirst; int idxLast = dash ? atoi(dash + 1) : idxFirst;
if (idxFirst < 0 || idxLast >= gaugePins[id].ledCount || idxFirst > idxLast) { if (idxFirst < 0 || idxLast >= gaugeLedCount[id] || idxFirst > idxLast) {
sendReply("ERR BAD_IDX"); return true; sendReply("ERR BAD_IDX"); return true;
} }
if (periodMs <= 0) { sendReply("ERR BAD_TIME"); return true; } if (periodMs <= 0) { sendReply("ERR BAD_TIME"); return true; }
@@ -954,7 +1008,7 @@ bool parseBreathe(const String& line) {
bs.cyclePos = 0; bs.cyclePos = 0;
bs.lastMs = nowMs; bs.lastMs = nowMs;
bs.active = true; bs.active = true;
leds[gi] = CRGB::Black; writeLed(gi, CRGB::Black);
} }
ledsDirty = true; ledsDirty = true;
sendReply("OK"); sendReply("OK");
@@ -972,7 +1026,7 @@ bool parseDflash(const String& line) {
char* dash = strchr(idxToken, '-'); char* dash = strchr(idxToken, '-');
int idxFirst = atoi(idxToken); int idxFirst = atoi(idxToken);
int idxLast = dash ? atoi(dash + 1) : idxFirst; int idxLast = dash ? atoi(dash + 1) : idxFirst;
if (idxFirst < 0 || idxLast >= gaugePins[id].ledCount || idxFirst > idxLast) { if (idxFirst < 0 || idxLast >= gaugeLedCount[id] || idxFirst > idxLast) {
sendReply("ERR BAD_IDX"); return true; sendReply("ERR BAD_IDX"); return true;
} }
CRGB color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255)); CRGB color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
@@ -985,7 +1039,7 @@ bool parseDflash(const String& line) {
bs.dphase = 0; bs.dphase = 0;
bs.lastMs = nowMs; bs.lastMs = nowMs;
bs.active = true; bs.active = true;
leds[gi] = color; // phase 0 = on writeLed(gi, color); // phase 0 = on
} }
ledsDirty = true; ledsDirty = true;
sendReply("OK"); sendReply("OK");
@@ -998,7 +1052,7 @@ void updateBlink() {
bool changed = false; bool changed = false;
for (uint8_t i = 0; i < GAUGE_COUNT; i++) { for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
for (uint8_t j = 0; j < gaugePins[i].ledCount; j++) { for (uint8_t j = 0; j < gaugeLedCount[i]; j++) {
uint8_t gi = gaugeLedOffset[i] + j; uint8_t gi = gaugeLedOffset[i] + j;
BlinkState& bs = blinkState[gi]; BlinkState& bs = blinkState[gi];
if (!bs.active) continue; if (!bs.active) continue;
@@ -1009,7 +1063,7 @@ void updateBlink() {
if ((nowMs - bs.lastMs) >= period) { if ((nowMs - bs.lastMs) >= period) {
bs.currentlyOn = !bs.currentlyOn; bs.currentlyOn = !bs.currentlyOn;
bs.lastMs = nowMs; bs.lastMs = nowMs;
leds[gi] = bs.currentlyOn ? bs.onColor : CRGB::Black; writeLed(gi, bs.currentlyOn ? bs.onColor : CRGB::Black);
changed = true; changed = true;
} }
break; break;
@@ -1025,8 +1079,9 @@ void updateBlink() {
uint8_t bri = (bs.cyclePos < half) uint8_t bri = (bs.cyclePos < half)
? (uint8_t)((uint32_t)bs.cyclePos * 255 / half) ? (uint8_t)((uint32_t)bs.cyclePos * 255 / half)
: (uint8_t)((uint32_t)(bs.periodMs - bs.cyclePos) * 255 / half); : (uint8_t)((uint32_t)(bs.periodMs - bs.cyclePos) * 255 / half);
leds[gi] = bs.onColor; CRGB scaled = bs.onColor;
leds[gi].nscale8(bri ? bri : 1); scaled.nscale8(bri ? bri : 1);
writeLed(gi, scaled);
changed = true; changed = true;
break; break;
} }
@@ -1035,7 +1090,7 @@ void updateBlink() {
if ((nowMs - bs.lastMs) >= dur[bs.dphase]) { if ((nowMs - bs.lastMs) >= dur[bs.dphase]) {
bs.lastMs = nowMs; bs.lastMs = nowMs;
bs.dphase = (bs.dphase + 1) & 3; bs.dphase = (bs.dphase + 1) & 3;
leds[gi] = (bs.dphase == 0 || bs.dphase == 2) ? bs.onColor : CRGB::Black; writeLed(gi, (bs.dphase == 0 || bs.dphase == 2) ? bs.onColor : CRGB::Black);
changed = true; changed = true;
} }
break; break;
@@ -1058,6 +1113,7 @@ void processLine(const String& line) {
if (parseHome(line)) return; if (parseHome(line)) return;
if (parseSweep(line)) return; if (parseSweep(line)) return;
if (parsePosQuery(line)) return; if (parsePosQuery(line)) return;
if (parseCfgQuery(line)) return;
if (parseLedQuery(line)) return; if (parseLedQuery(line)) return;
if (parseLed(line)) return; if (parseLed(line)) return;
if (parseBlink(line)) return; if (parseBlink(line)) return;
@@ -1115,10 +1171,11 @@ void setup() {
// Flatten the per-gauge LED counts into offsets on the shared strip. // Flatten the per-gauge LED counts into offsets on the shared strip.
uint8_t ledOff = 0; uint8_t ledOff = 0;
for (uint8_t i = 0; i < GAUGE_COUNT; i++) { for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
gaugeLedCount[i] = cstrLen(gaugePins[i].ledOrder);
gaugeLedOffset[i] = ledOff; gaugeLedOffset[i] = ledOff;
ledOff += gaugePins[i].ledCount; ledOff += gaugeLedCount[i];
} }
FastLED.addLeds<WS2812B, LED_DATA_PIN, GRB>(leds, TOTAL_LEDS); FastLED.addLeds<WS2812B, LED_DATA_PIN, RGB>(leds, TOTAL_LEDS);
FastLED.setBrightness(255); FastLED.setBrightness(255);
FastLED.show(); FastLED.show();

62
boot.py
View File

@@ -1,62 +0,0 @@
"""
boot.py — runs before main.py on every ESP32 boot
Connects WiFi, runs OTA update, then hands off to main.py.
Keep this file as simple as possible — it is never OTA-updated itself
(it lives outside the repo folder) so bugs here require USB to fix.
"""
#import gauge
import network
import gc
import utime
import sys
import ota
ota.load_config()
WIFI_SSID, WIFI_PASSWORD = ota.WIFI_SSID, ota.WIFI_PASSWORD
def _connect_wifi(timeout_s=20):
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.config(txpower=15)
if sta.isconnected():
return True
sta.connect(WIFI_SSID, WIFI_PASSWORD)
deadline = utime.time() + timeout_s
while not sta.isconnected():
if utime.time() > deadline:
return False
utime.sleep_ms(300)
return True
if WIFI_SSID is None:
print("[boot] No WiFi credentials — cannot connect, skipping OTA")
elif _connect_wifi():
ip = network.WLAN(network.STA_IF).ifconfig()[0]
print(f"[boot] WiFi connected — {ip}")
try:
ota.update()
except Exception as e:
print(f"[boot] OTA error: {e} — continuing with existing files")
sys.print_exception(e)
utime.sleep_ms(5000)
ota._fetch_commit_sha = None
ota._fetch_manifest = None
ota._fetch_dir = None
ota._api_get = None
ota._download = None
ota.urequests = None
del ota.urequests
del ota
gc.collect()
del sys.modules["ota"]
gc.collect()
else:
print("[boot] WiFi failed — skipping OTA, booting with existing files")
# main.py runs automatically after boot.py

2469
esp-home-rewrite.yaml Normal file

File diff suppressed because it is too large Load Diff

118
gauge.py
View File

@@ -28,6 +28,16 @@ import gc
from umqtt.robust import MQTTClient from umqtt.robust import MQTTClient
from machine import UART from machine import UART
# Activate WiFi driver before any heavy heap allocation so it can claim its
# contiguous DRAM block before the Python heap fragments the address space.
# Only activate if not already running (e.g. boot.py may have started it).
gc.collect()
_early_wlan = network.WLAN(network.STA_IF)
if not _early_wlan.active():
_early_wlan.active(True)
del _early_wlan
gc.collect()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Logging # Logging
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -151,27 +161,19 @@ ARDUINO_TX_PIN = int(_cfg.get("arduino_tx_pin", 17))
ARDUINO_RX_PIN = int(_cfg.get("arduino_rx_pin", 16)) ARDUINO_RX_PIN = int(_cfg.get("arduino_rx_pin", 16))
ARDUINO_BAUD = int(_cfg.get("arduino_baud", 115200)) ARDUINO_BAUD = int(_cfg.get("arduino_baud", 115200))
_arduino = None _arduino = UART(ARDUINO_UART_ID, baudrate=ARDUINO_BAUD, tx=ARDUINO_TX_PIN, rx=ARDUINO_RX_PIN, timeout=10)
def _ensure_arduino():
global _arduino
if _arduino is None:
_arduino = UART(ARDUINO_UART_ID, baudrate=ARDUINO_BAUD, tx=ARDUINO_TX_PIN, rx=ARDUINO_RX_PIN, timeout=10)
return _arduino
def arduino_send(cmd): def arduino_send(cmd):
"""Send a newline-terminated command to the Arduino.""" """Send a newline-terminated command to the Arduino."""
_ensure_arduino().write((cmd + "\n").encode()) _arduino.write((cmd + "\n").encode())
info(f"Arduino → {cmd}") info(f"Arduino → {cmd}")
def arduino_recv(): def arduino_recv():
"""Print any lines waiting in the Arduino RX buffer.""" """Print any lines waiting in the Arduino RX buffer."""
uart = _ensure_arduino() while _arduino.any():
while uart.any(): line = _arduino.readline()
line = uart.readline()
if line: if line:
print(f"[{_ts()}] ARDU {line.decode().strip()}") print(f"[{_ts()}] ARDU {line.decode().strip()}")
@@ -538,14 +540,11 @@ _WIFI_CONNECT_ATTEMPTS = 3
def _reset_wifi_interface(): def _reset_wifi_interface():
global _wifi_sta global _wifi_sta
_wifi_sta = network.WLAN(network.STA_IF) _wifi_sta = network.WLAN(network.STA_IF)
if not _wifi_sta.active(): if _wifi_sta.active():
_wifi_sta.active(False)
utime.sleep_ms(200)
_wifi_sta.active(True) _wifi_sta.active(True)
utime.sleep_ms(500) utime.sleep_ms(500)
try:
_wifi_sta.disconnect()
except Exception:
pass
utime.sleep_ms(1000)
def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False): def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False):
@@ -562,7 +561,6 @@ def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False):
last_error = None last_error = None
for attempt in range(_WIFI_CONNECT_ATTEMPTS): for attempt in range(_WIFI_CONNECT_ATTEMPTS):
info(f"WiFi connecting to '{ssid}' (attempt {attempt + 1}/{_WIFI_CONNECT_ATTEMPTS}) ...") info(f"WiFi connecting to '{ssid}' (attempt {attempt + 1}/{_WIFI_CONNECT_ATTEMPTS}) ...")
if not _wifi_sta.isconnected():
_reset_wifi_interface() _reset_wifi_interface()
try: try:
_wifi_sta.connect(ssid, password) _wifi_sta.connect(ssid, password)
@@ -578,7 +576,7 @@ def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False):
info(f" SSID : {ssid}") info(f" SSID : {ssid}")
info(f" MAC : {mac}") info(f" MAC : {mac}")
info(f" IP : {ip} mask:{mask} gw:{gw} dns:{dns}") info(f" IP : {ip} mask:{mask} gw:{gw} dns:{dns}")
utime.sleep_ms(2000) utime.sleep_ms(500)
return ip return ip
except Exception as e: except Exception as e:
last_error = e last_error = e
@@ -603,7 +601,7 @@ def check_wifi():
log_err("WiFi lost connection — attempting reconnect...") log_err("WiFi lost connection — attempting reconnect...")
try: try:
ip = connect_wifi(WIFI_SSID, WIFI_PASSWORD, timeout_s=15) ip = connect_wifi(WIFI_SSID, WIFI_PASSWORD, timeout_s=15, force_reconnect=True)
info(f"WiFi reconnected! IP:{ip}") info(f"WiFi reconnected! IP:{ip}")
except Exception as e: except Exception as e:
log_err(f"WiFi reconnect failed: {e}") log_err(f"WiFi reconnect failed: {e}")
@@ -913,6 +911,10 @@ def connect_mqtt():
except Exception as e: except Exception as e:
last_error = e last_error = e
log_err(f"MQTT connect attempt {attempt + 1} failed: {type(e).__name__}: {e}") log_err(f"MQTT connect attempt {attempt + 1} failed: {type(e).__name__}: {e}")
try:
client.sock.close()
except Exception:
pass
gc.collect() gc.collect()
utime.sleep_ms(1000) utime.sleep_ms(1000)
@@ -920,6 +922,27 @@ def connect_mqtt():
raise last_error raise last_error
_mqtt_check_interval_ms = 30000
_last_mqtt_check = 0
_discovery_queue = []
_discovery_idx = 0
_last_discovery_ms = 0
_DISCOVERY_INTERVAL_MS = 350
def _compact_discovery_payload(payload):
"""Trim optional HA discovery fields when RAM is tight."""
compact = dict(payload)
# Light entities are the largest payloads because they repeat effect metadata.
# Keep core functionality, but omit optional effect declarations to reduce heap use.
if compact.get("schema") == "json":
compact.pop("effect", None)
compact.pop("effect_list", None)
return compact
def check_mqtt(): def check_mqtt():
global client_ref, _mqtt_connected, _last_mqtt_check global client_ref, _mqtt_connected, _last_mqtt_check
now = utime.ticks_ms() now = utime.ticks_ms()
@@ -961,6 +984,10 @@ def check_mqtt():
return True return True
except Exception as e2: except Exception as e2:
log_err(f"MQTT reconnect attempt {attempt + 1} failed: {e2}") log_err(f"MQTT reconnect attempt {attempt + 1} failed: {e2}")
try:
client_ref.sock.close()
except Exception:
pass
gc.collect() gc.collect()
utime.sleep_ms(2000) utime.sleep_ms(2000)
@@ -968,17 +995,9 @@ def check_mqtt():
return False return False
_mqtt_check_interval_ms = 30000
_last_mqtt_check = 0
_discovery_queue = []
_discovery_idx = 0
_last_discovery_ms = 0
_DISCOVERY_INTERVAL_MS = 350
def _publish_discovery_entity(client, topic, payload, log_msg): def _publish_discovery_entity(client, topic, payload, log_msg):
gc.collect() gc.collect()
client.publish(topic, ujson.dumps(payload), retain=True) client.publish(topic, ujson.dumps(_compact_discovery_payload(payload)), retain=True)
info(log_msg) info(log_msg)
@@ -1286,42 +1305,12 @@ def apply_motion_defaults():
send_vfd_state() send_vfd_state()
def _restore_led_states():
for i in range(num_gauges):
gt = gauge_topics[i]
info(f" red={_red_effect[i]} green={_green_effect[i]} status_r={_status_red_effect[i]} status_g={_status_green_effect[i]}")
for led_key, led_idx, color, effect_arr, state_topic in [
("red", _LED_RED, gauges[i]["ws2812_red"], _red_effect, gt["led_red_state"]),
("green", _LED_GREEN, gauges[i]["ws2812_green"], _green_effect, gt["led_green_state"]),
("status_red", _LED_STATUS_RED, gauges[i]["ws2812_red"], _status_red_effect, gt["status_red_state"]),
("status_green", _LED_STATUS_GREEN, gauges[i]["ws2812_green"], _status_green_effect, gt["status_green_state"]),
]:
if effect_arr[i]:
pub = {"state": "ON", "effect": effect_arr[i]}
_publish(state_topic, ujson.dumps(pub), retain=True)
if _red_effect[i]:
_apply_blink_or_led(i, _LED_RED, gauges[i]["ws2812_red"], _red_effect[i])
if _green_effect[i]:
_apply_blink_or_led(i, _LED_GREEN, gauges[i]["ws2812_green"], _green_effect[i])
if _status_red_effect[i]:
_apply_blink_or_led(i, _LED_STATUS_RED, gauges[i]["ws2812_red"], _status_red_effect[i])
if _status_green_effect[i]:
_apply_blink_or_led(i, _LED_STATUS_GREEN, gauges[i]["ws2812_green"], _status_green_effect[i])
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Main # Main
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def main(): def main():
gc.collect()
_w = network.WLAN(network.STA_IF)
if not _w.active():
_w.active(True)
del _w
gc.collect()
_ensure_arduino()
gc.collect() gc.collect()
info("=" * 48) info("=" * 48)
info("Gauge MQTT controller starting") info("Gauge MQTT controller starting")
@@ -1329,7 +1318,7 @@ def main():
info("=" * 48) info("=" * 48)
gc.collect() gc.collect()
connect_wifi(WIFI_SSID, WIFI_PASSWORD) connect_wifi(WIFI_SSID, WIFI_PASSWORD, force_reconnect=True)
mqtt_attempts = 0 mqtt_attempts = 0
while True: while True:
@@ -1342,14 +1331,13 @@ def main():
if mqtt_attempts % 3 == 0: if mqtt_attempts % 3 == 0:
log_err("WiFi may be stale — forcing reconnect...") log_err("WiFi may be stale — forcing reconnect...")
try: try:
connect_wifi(WIFI_SSID, WIFI_PASSWORD) connect_wifi(WIFI_SSID, WIFI_PASSWORD, force_reconnect=True)
except Exception as we: except Exception as we:
log_err(f"WiFi reconnect failed: {we}") log_err(f"WiFi reconnect failed: {we}")
utime.sleep_ms(5000) utime.sleep_ms(5000)
_subscribe_all(client_ref) _subscribe_all(client_ref)
schedule_discovery() schedule_discovery()
publish_backlight_states(client_ref)
apply_motion_defaults() apply_motion_defaults()
info("Draining initial retained messages...") info("Draining initial retained messages...")
for _ in range(50): for _ in range(50):
@@ -1362,10 +1350,6 @@ def main():
gauge_last_rezero[i] = utime.ticks_ms() gauge_last_rezero[i] = utime.ticks_ms()
info("Home command sent") info("Home command sent")
utime.sleep_ms(100)
_restore_led_states()
info("LED effects restored")
info("Publishing state...") info("Publishing state...")
publish_online(client_ref) publish_online(client_ref)
publish_state(client_ref) publish_state(client_ref)