Compare commits
8 Commits
review
...
27597bceab
| Author | SHA1 | Date | |
|---|---|---|---|
| 27597bceab | |||
| 016de2ccb4 | |||
| 15257ae6f2 | |||
|
795eb0ecf3
|
|||
|
558c5b18c2
|
|||
|
fa66dd70d4
|
|||
|
b14bdf7fc3
|
|||
|
427dde8c72
|
@@ -58,7 +58,7 @@ The sketch controls `GAUGE_COUNT` stepper-motor gauges using a trapezoidal veloc
|
||||
|
||||
### 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.
|
||||
|
||||
### Motion control (`updateGauge`)
|
||||
@@ -76,7 +76,7 @@ When `sweepEnabled`, `updateSweepTarget` bounces `targetPos` between `minPos` an
|
||||
|
||||
### 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
|
||||
|
||||
@@ -104,6 +104,6 @@ All commands reply `OK` or `ERR BAD_ID` / `ERR BAD_CMD` etc.
|
||||
### Adding gauges
|
||||
|
||||
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.
|
||||
4. `TOTAL_LEDS` and `gaugeLedOffset[]` update automatically — no manual changes needed.
|
||||
4. `TOTAL_LEDS`, `gaugeLedOffset[]`, and `gaugeLedCount[]` update automatically — no manual changes needed.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <math.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.
|
||||
static const uint8_t LED_DATA_PIN = 22;
|
||||
@@ -226,18 +226,23 @@ struct GaugePins {
|
||||
bool dirInverted;
|
||||
bool stepActiveHigh;
|
||||
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] = {
|
||||
// dir, step, en, dirInv, stepHigh, enActiveLow, leds
|
||||
{50, 51, -1, false, true, true, 7}, // Gauge 0
|
||||
{8, 9, -1, true, true, true, 7}, // Gauge 1
|
||||
{52, 53, -1, false, true, true, 7}, // Gauge 2
|
||||
// dir, step, en, dirInv, stepHigh, enActiveLow, ledOrder
|
||||
{50, 51, -1, false, true, true, "RRRGGRR"}, // Gauge 0
|
||||
{8, 9, -1, true, true, true, "GGGRRRR"}, // Gauge 1
|
||||
{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) {
|
||||
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();
|
||||
|
||||
@@ -297,9 +302,40 @@ String rxLine;
|
||||
|
||||
CRGB leds[TOTAL_LEDS];
|
||||
uint8_t gaugeLedOffset[GAUGE_COUNT];
|
||||
uint8_t gaugeLedCount[GAUGE_COUNT];
|
||||
BlinkState blinkState[TOTAL_LEDS];
|
||||
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.
|
||||
//
|
||||
// Serial protocol summary.
|
||||
@@ -794,6 +830,24 @@ bool parsePosQuery(const String& line) {
|
||||
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?
|
||||
// Reply: `PONG`.
|
||||
bool parsePing(const String& line) {
|
||||
@@ -835,8 +889,8 @@ bool parseVfd(const String& line) {
|
||||
bool parseLedQuery(const String& line) {
|
||||
if (line == "LED?") {
|
||||
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
||||
for (uint8_t j = 0; j < gaugePins[i].ledCount; j++) {
|
||||
const CRGB& c = leds[gaugeLedOffset[i] + j];
|
||||
for (uint8_t j = 0; j < gaugeLedCount[i]; j++) {
|
||||
CRGB c = readLed(gaugeLedOffset[i] + j);
|
||||
CMD_PORT.print("LED ");
|
||||
CMD_PORT.print(i);
|
||||
CMD_PORT.print(' ');
|
||||
@@ -865,13 +919,13 @@ bool parseLed(const String& line) {
|
||||
char* dash = strchr(idxToken, '-');
|
||||
int idxFirst = atoi(idxToken);
|
||||
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;
|
||||
}
|
||||
CRGB color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
|
||||
for (int i = idxFirst; i <= idxLast; i++) {
|
||||
blinkState[gaugeLedOffset[id] + i].active = false;
|
||||
leds[gaugeLedOffset[id] + i] = color;
|
||||
writeLed(gaugeLedOffset[id] + i, color);
|
||||
}
|
||||
ledsDirty = true;
|
||||
sendReply("OK");
|
||||
@@ -894,7 +948,7 @@ bool parseBlink(const String& line) {
|
||||
char* dash = strchr(idxToken, '-');
|
||||
int idxFirst = atoi(idxToken);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -915,13 +969,13 @@ bool parseBlink(const String& line) {
|
||||
uint8_t globalIdx = gaugeLedOffset[id] + i;
|
||||
BlinkState& bs = blinkState[globalIdx];
|
||||
bs.fx = FX_BLINK;
|
||||
bs.onColor = (count == 7) ? color : leds[globalIdx];
|
||||
bs.onColor = (count == 7) ? color : readLed(globalIdx);
|
||||
bs.onMs = (uint16_t)onMs;
|
||||
bs.offMs = (uint16_t)offMs;
|
||||
bs.currentlyOn = true;
|
||||
bs.lastMs = nowMs;
|
||||
bs.active = true;
|
||||
leds[globalIdx] = bs.onColor;
|
||||
writeLed(globalIdx, bs.onColor);
|
||||
}
|
||||
ledsDirty = true;
|
||||
sendReply("OK");
|
||||
@@ -939,7 +993,7 @@ bool parseBreathe(const String& line) {
|
||||
char* dash = strchr(idxToken, '-');
|
||||
int idxFirst = atoi(idxToken);
|
||||
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;
|
||||
}
|
||||
if (periodMs <= 0) { sendReply("ERR BAD_TIME"); return true; }
|
||||
@@ -954,7 +1008,7 @@ bool parseBreathe(const String& line) {
|
||||
bs.cyclePos = 0;
|
||||
bs.lastMs = nowMs;
|
||||
bs.active = true;
|
||||
leds[gi] = CRGB::Black;
|
||||
writeLed(gi, CRGB::Black);
|
||||
}
|
||||
ledsDirty = true;
|
||||
sendReply("OK");
|
||||
@@ -972,7 +1026,7 @@ bool parseDflash(const String& line) {
|
||||
char* dash = strchr(idxToken, '-');
|
||||
int idxFirst = atoi(idxToken);
|
||||
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;
|
||||
}
|
||||
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.lastMs = nowMs;
|
||||
bs.active = true;
|
||||
leds[gi] = color; // phase 0 = on
|
||||
writeLed(gi, color); // phase 0 = on
|
||||
}
|
||||
ledsDirty = true;
|
||||
sendReply("OK");
|
||||
@@ -998,7 +1052,7 @@ void updateBlink() {
|
||||
bool changed = false;
|
||||
|
||||
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;
|
||||
BlinkState& bs = blinkState[gi];
|
||||
if (!bs.active) continue;
|
||||
@@ -1009,7 +1063,7 @@ void updateBlink() {
|
||||
if ((nowMs - bs.lastMs) >= period) {
|
||||
bs.currentlyOn = !bs.currentlyOn;
|
||||
bs.lastMs = nowMs;
|
||||
leds[gi] = bs.currentlyOn ? bs.onColor : CRGB::Black;
|
||||
writeLed(gi, bs.currentlyOn ? bs.onColor : CRGB::Black);
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
@@ -1025,8 +1079,9 @@ void updateBlink() {
|
||||
uint8_t bri = (bs.cyclePos < half)
|
||||
? (uint8_t)((uint32_t)bs.cyclePos * 255 / half)
|
||||
: (uint8_t)((uint32_t)(bs.periodMs - bs.cyclePos) * 255 / half);
|
||||
leds[gi] = bs.onColor;
|
||||
leds[gi].nscale8(bri ? bri : 1);
|
||||
CRGB scaled = bs.onColor;
|
||||
scaled.nscale8(bri ? bri : 1);
|
||||
writeLed(gi, scaled);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
@@ -1035,7 +1090,7 @@ void updateBlink() {
|
||||
if ((nowMs - bs.lastMs) >= dur[bs.dphase]) {
|
||||
bs.lastMs = nowMs;
|
||||
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;
|
||||
}
|
||||
break;
|
||||
@@ -1058,6 +1113,7 @@ void processLine(const String& line) {
|
||||
if (parseHome(line)) return;
|
||||
if (parseSweep(line)) return;
|
||||
if (parsePosQuery(line)) return;
|
||||
if (parseCfgQuery(line)) return;
|
||||
if (parseLedQuery(line)) return;
|
||||
if (parseLed(line)) return;
|
||||
if (parseBlink(line)) return;
|
||||
@@ -1115,10 +1171,11 @@ void setup() {
|
||||
// Flatten the per-gauge LED counts into offsets on the shared strip.
|
||||
uint8_t ledOff = 0;
|
||||
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
||||
gaugeLedCount[i] = cstrLen(gaugePins[i].ledOrder);
|
||||
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.show();
|
||||
|
||||
|
||||
64
boot.py
64
boot.py
@@ -1,64 +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
2469
esp-home-rewrite.yaml
Normal file
File diff suppressed because it is too large
Load Diff
15
gauge.py
15
gauge.py
@@ -930,6 +930,19 @@ _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():
|
||||
global client_ref, _mqtt_connected, _last_mqtt_check
|
||||
now = utime.ticks_ms()
|
||||
@@ -984,7 +997,7 @@ def check_mqtt():
|
||||
|
||||
def _publish_discovery_entity(client, topic, payload, log_msg):
|
||||
gc.collect()
|
||||
client.publish(topic, ujson.dumps(payload), retain=True)
|
||||
client.publish(topic, ujson.dumps(_compact_discovery_payload(payload)), retain=True)
|
||||
info(log_msg)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user