Update docs and firmware for ESPHome bridge migration

- Replace gauge.py (MicroPython) references with gaugecontroller.yaml (ESPHome)
- Update CLAUDE.md and README.md to document ESPHome-native API integration
- Update LED wiring docs for separate main/indicator strips (D22/D36)
- Refactor Arduino firmware to drive two WS2812 strips independently
- Add per-gauge physical offset caching for main and indicator LEDs
- Frame-limit breathe effect (16ms) to reduce unnecessary strip refreshes
This commit is contained in:
2026-04-29 19:03:22 +02:00
parent 361cf52252
commit 8bdae1da9b
13 changed files with 130 additions and 72 deletions

View File

@@ -5,8 +5,12 @@
static const uint8_t GAUGE_COUNT = 4;
// One shared WS2812B strip, split into per-gauge segments.
// Backlight/status LEDs and indicator LEDs use separate data strips because
// their LED chipsets are not compatible on one chain. The command protocol
// still exposes one logical LED segment per gauge.
static const uint8_t LED_DATA_PIN = 22;
static const uint8_t INDICATOR_LED_DATA_PIN = 36;
static const uint8_t BREATHE_FRAME_MS = 16;
// For now, command and debug traffic share the same serial port.
#define CMD_PORT Serial1
@@ -246,6 +250,21 @@ constexpr uint8_t sumLedCounts(uint8_t i = 0) {
}
static const uint8_t TOTAL_LEDS = sumLedCounts();
constexpr bool isIndicatorLedIndex(uint8_t localIdx) {
return localIdx == 3 || localIdx == 4;
}
constexpr uint8_t countIndicatorLedsForGauge(uint8_t gaugeIdx) {
return (cstrLen(gaugePins[gaugeIdx].ledOrder) > 3 ? 1 : 0) +
(cstrLen(gaugePins[gaugeIdx].ledOrder) > 4 ? 1 : 0);
}
constexpr uint8_t sumIndicatorLedCounts(uint8_t i = 0) {
return i >= GAUGE_COUNT ? 0 : countIndicatorLedsForGauge(i) + sumIndicatorLedCounts(i + 1);
}
static const uint8_t TOTAL_INDICATOR_LEDS = sumIndicatorLedCounts();
static const uint8_t TOTAL_MAIN_LEDS = TOTAL_LEDS - TOTAL_INDICATOR_LEDS;
enum HomingState : uint8_t {
HS_IDLE,
HS_START,
@@ -300,11 +319,18 @@ struct BlinkState {
Gauge gauges[GAUGE_COUNT];
String rxLine;
CRGB leds[TOTAL_LEDS];
CRGB logicalLeds[TOTAL_LEDS];
CRGB mainLeds[TOTAL_MAIN_LEDS];
CRGB indicatorLeds[TOTAL_INDICATOR_LEDS];
CLEDController* mainLedController = nullptr;
CLEDController* indicatorLedController = nullptr;
uint8_t gaugeLedOffset[GAUGE_COUNT];
uint8_t gaugeLedCount[GAUGE_COUNT];
uint8_t gaugeMainLedOffset[GAUGE_COUNT];
uint8_t gaugeIndicatorLedOffset[GAUGE_COUNT];
BlinkState blinkState[TOTAL_LEDS];
bool ledsDirty = false;
bool mainLedsDirty = false;
bool indicatorLedsDirty = 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.
@@ -328,12 +354,52 @@ inline CRGB encodeForStrip(uint8_t globalIdx, CRGB color) {
return color;
}
bool ledPhysicalIndex(uint8_t globalIdx, bool& indicatorStrip, uint8_t& physicalIdx) {
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
uint8_t off = gaugeLedOffset[i];
if (globalIdx < off || globalIdx >= off + gaugeLedCount[i]) continue;
uint8_t localIdx = globalIdx - off;
indicatorStrip = isIndicatorLedIndex(localIdx);
if (indicatorStrip) {
physicalIdx = gaugeIndicatorLedOffset[i] + (localIdx - 3);
} else {
physicalIdx = gaugeMainLedOffset[i] + localIdx - (localIdx > 4 ? 2 : 0);
}
return true;
}
return false;
}
inline void writeLed(uint8_t globalIdx, CRGB color) {
leds[globalIdx] = encodeForStrip(globalIdx, color);
logicalLeds[globalIdx] = color;
bool indicatorStrip = false;
uint8_t physicalIdx = 0;
if (!ledPhysicalIndex(globalIdx, indicatorStrip, physicalIdx)) return;
if (indicatorStrip) {
indicatorLeds[physicalIdx] = encodeForStrip(globalIdx, color);
indicatorLedsDirty = true;
} else {
mainLeds[physicalIdx] = encodeForStrip(globalIdx, color);
mainLedsDirty = true;
}
}
inline CRGB readLed(uint8_t globalIdx) {
return encodeForStrip(globalIdx, leds[globalIdx]);
return logicalLeds[globalIdx];
}
void showDirtyLeds() {
if (mainLedsDirty && mainLedController != nullptr) {
mainLedController->showLeds(255);
mainLedsDirty = false;
}
if (indicatorLedsDirty && indicatorLedController != nullptr) {
indicatorLedController->showLeds(255);
indicatorLedsDirty = false;
}
}
// Sends one-line command replies back over the control port.
@@ -927,7 +993,6 @@ bool parseLed(const String& line) {
blinkState[gaugeLedOffset[id] + i].active = false;
writeLed(gaugeLedOffset[id] + i, color);
}
ledsDirty = true;
sendReply("OK");
return true;
}
@@ -977,7 +1042,6 @@ bool parseBlink(const String& line) {
bs.active = true;
writeLed(globalIdx, bs.onColor);
}
ledsDirty = true;
sendReply("OK");
return true;
}
@@ -1010,7 +1074,6 @@ bool parseBreathe(const String& line) {
bs.active = true;
writeLed(gi, CRGB::Black);
}
ledsDirty = true;
sendReply("OK");
return true;
}
@@ -1041,15 +1104,13 @@ bool parseDflash(const String& line) {
bs.active = true;
writeLed(gi, color); // phase 0 = on
}
ledsDirty = true;
sendReply("OK");
return true;
}
// Advances all active LED effects and marks the strip dirty when something changed.
// Advances all active LED effects. writeLed() marks the affected physical strip dirty.
void updateBlink() {
unsigned long nowMs = millis();
bool changed = false;
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
for (uint8_t j = 0; j < gaugeLedCount[i]; j++) {
@@ -1064,17 +1125,17 @@ void updateBlink() {
bs.currentlyOn = !bs.currentlyOn;
bs.lastMs = nowMs;
writeLed(gi, bs.currentlyOn ? bs.onColor : CRGB::Black);
changed = true;
}
break;
}
case FX_BREATHE: {
unsigned long dt = nowMs - bs.lastMs;
if (dt < 64) break;
if (dt < BREATHE_FRAME_MS) break;
uint32_t newPos = (uint32_t)bs.cyclePos + dt;
bs.cyclePos = (uint16_t)(newPos % bs.periodMs);
bs.lastMs = nowMs;
// Cheap triangle wave. It does the job and nobody has complained yet.
// Triangle wave brightness; frame-limited so breathe remains smooth
// without refreshing the LED strips on every service-loop pass.
uint16_t half = bs.periodMs >> 1;
uint8_t bri = (bs.cyclePos < half)
? (uint8_t)((uint32_t)bs.cyclePos * 255 / half)
@@ -1082,7 +1143,6 @@ void updateBlink() {
CRGB scaled = bs.onColor;
scaled.nscale8(bri ? bri : 1);
writeLed(gi, scaled);
changed = true;
break;
}
case FX_DFLASH: {
@@ -1091,15 +1151,12 @@ void updateBlink() {
bs.lastMs = nowMs;
bs.dphase = (bs.dphase + 1) & 3;
writeLed(gi, (bs.dphase == 0 || bs.dphase == 2) ? bs.onColor : CRGB::Black);
changed = true;
}
break;
}
}
}
}
if (changed) ledsDirty = true;
}
// Runs the command parsers in order until one claims the line.
@@ -1168,16 +1225,25 @@ void setup() {
gauges[i].lastUpdateMicros = micros();
}
// Flatten the per-gauge LED counts into offsets on the shared strip.
// Flatten the per-gauge LED counts into logical offsets and separate
// physical offsets for the main and indicator strips.
uint8_t ledOff = 0;
uint8_t mainLedOff = 0;
uint8_t indicatorLedOff = 0;
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
gaugeLedCount[i] = cstrLen(gaugePins[i].ledOrder);
gaugeLedOffset[i] = ledOff;
gaugeMainLedOffset[i] = mainLedOff;
gaugeIndicatorLedOffset[i] = indicatorLedOff;
ledOff += gaugeLedCount[i];
indicatorLedOff += countIndicatorLedsForGauge(i);
mainLedOff += gaugeLedCount[i] - countIndicatorLedsForGauge(i);
}
FastLED.addLeds<WS2812B, LED_DATA_PIN, RGB>(leds, TOTAL_LEDS);
mainLedController = &FastLED.addLeds<WS2812, LED_DATA_PIN, RGB>(mainLeds, TOTAL_MAIN_LEDS);
indicatorLedController = &FastLED.addLeds<WS2812B, INDICATOR_LED_DATA_PIN, RGB>(indicatorLeds, TOTAL_INDICATOR_LEDS);
FastLED.setBrightness(255);
FastLED.show();
mainLedController->showLeds(255);
indicatorLedController->showLeds(255);
vfd::begin();
@@ -1198,10 +1264,7 @@ void loop() {
updateGauge(i);
}
if (ledsDirty) {
FastLED.show();
ledsDirty = false;
}
showDirtyLeds();
}