From 8bdae1da9be14d466216f7569d82dda1e14a3edc Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Wed, 29 Apr 2026 19:03:22 +0200 Subject: [PATCH] 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 --- CLAUDE.md | 4 +- Gaugecontroller/Gaugecontroller.ino | 113 ++++++++++++++---- README.md | 52 ++++---- Rewire_checklist.md | 9 +- Stripboard_layout.md | 7 +- .../config.example.json | 0 gauge.py => archive/gauge.py | 0 main.py => archive/main.py | 0 ota.py => archive/ota.py | 0 .../ota_config.example.json | 0 ota_manifest.txt => archive/ota_manifest.txt | 0 esp-home-rewrite.yaml => gaugecontroller.yaml | 0 wiring.md | 17 +-- 13 files changed, 130 insertions(+), 72 deletions(-) rename config.example.json => archive/config.example.json (100%) rename gauge.py => archive/gauge.py (100%) rename main.py => archive/main.py (100%) rename ota.py => archive/ota.py (100%) rename ota_config.example.json => archive/ota_config.example.json (100%) rename ota_manifest.txt => archive/ota_manifest.txt (100%) rename esp-home-rewrite.yaml => gaugecontroller.yaml (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 2f2773f..f703dc4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Main firmware lives in `Gaugecontroller/Gaugecontroller.ino`. Requires the **FastLED** library (`arduino-cli lib install FastLED`). Use the Arduino IDE or `arduino-cli`: +The ESP32 bridge runs ESPHome; the config is in `gaugecontroller.yaml`. + ```bash # Compile (replace board/port as needed) arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller @@ -76,7 +78,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].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. +Two LED strips are driven: main backlight/status LEDs on `LED_DATA_PIN` (currently 22) and dial indicator LEDs on `INDICATOR_LED_DATA_PIN` (currently 36). The serial protocol still exposes one logical per-gauge LED segment: `0-2` backlight, `3-4` indicators, `5-6` status. `gaugePins[i].ledOrder` is a per-LED type string (one char per LED, `'G'` = GRB-ordered, `'R'` = RGB-ordered) and its length defines the logical LED count. `TOTAL_LEDS`, `TOTAL_MAIN_LEDS`, and `TOTAL_INDICATOR_LEDS` are computed at compile time. Per-gauge logical and physical offsets are cached in `setup()`. LED writes dirty only their physical strip, and the loop flushes each FastLED controller independently with `showLeds()`. ### Serial command protocol diff --git a/Gaugecontroller/Gaugecontroller.ino b/Gaugecontroller/Gaugecontroller.ino index e84be18..d46d43e 100644 --- a/Gaugecontroller/Gaugecontroller.ino +++ b/Gaugecontroller/Gaugecontroller.ino @@ -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(leds, TOTAL_LEDS); + mainLedController = &FastLED.addLeds(mainLeds, TOTAL_MAIN_LEDS); + indicatorLedController = &FastLED.addLeds(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(); } diff --git a/README.md b/README.md index 2f3ebac..a3dbd7e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A dedicated gauge controller for Arduinos. This repository contains: - `Gaugecontroller/Gaugecontroller.ino`: the Arduino Mega firmware for the stepper gauges, LEDs, and integrated HV5812-based VFD -- `gauge.py`: the ESP32 / MicroPython MQTT bridge that exposes the controller to Home Assistant +- `gaugecontroller.yaml`: the ESPHome-based ESP32 firmware that exposes the gauges and VFD to Home Assistant via the native API ## VFD Support @@ -48,16 +48,19 @@ Rules: - shorter values are right-aligned - leading zeroes are preserved if they are part of the input -## Home Assistant Entities +## Home Assistant Integration -The MQTT bridge publishes Home Assistant discovery entities for the VFD: +The ESPHome firmware in `gaugecontroller.yaml` exposes entities to Home Assistant via the native API: -- `VFD Display` - text entity for the displayed value -- `VFD Decimal Point` - switch entity -- `VFD Alarm` - switch entity +### Gauge Controls +- Number entities for each gauge's target value (with unit conversion) +- Number entities for speed and acceleration (diagnostic) +- Rezero buttons for each gauge and all gauges + +### VFD Display +- `VFD Display`: text entity for the displayed value +- `VFD Decimal Point`: switch entity +- `VFD Alarm`: switch entity The display is intentionally exposed as a text entity rather than a numeric entity so that: @@ -65,27 +68,12 @@ The display is intentionally exposed as a text entity rather than a numeric enti - hexadecimal values like `DEAD` or `BEEF` work - clearing the display is possible with an empty value -## MQTT Topics +### LED Controls +- RGB light entity for each gauge's backlight with effects (Blink, Breathe, Double Flash) +- Binary light entities for each gauge's red/green indicators and status lights -Using the configured `mqtt_prefix` from `config.json`, the VFD topics are: - -- `/vfd/set` -- `/vfd/state` -- `/vfd/decimal_point/set` -- `/vfd/decimal_point/state` -- `/vfd/alarm/set` -- `/vfd/alarm/state` - -Example with the default prefix `gauges`: - -- `gauges/vfd/set` -- `gauges/vfd/decimal_point/set` -- `gauges/vfd/alarm/set` - -Example payloads: - -- publish `0123` to `gauges/vfd/set` -- publish `ON` to `gauges/vfd/decimal_point/set` -- publish `OFF` to `gauges/vfd/alarm/set` - -The MQTT bridge then converts that into the correct Arduino serial command such as `VFD 0123.`. +### Diagnostics +- WiFi signal sensor +- Uptime sensor +- IP address and SSID text sensors +- Arduino Last Message sensor diff --git a/Rewire_checklist.md b/Rewire_checklist.md index 1ef39fd..0887894 100644 --- a/Rewire_checklist.md +++ b/Rewire_checklist.md @@ -183,13 +183,14 @@ Then connect the motor side of that driver to: according to the driver board you are using. -## 14. Wire The WS2812B LEDs +## 14. Wire The WS2812 LEDs Connect: -- `Mega D22` -> `WS2812B DIN` -- `5V LED supply` -> `WS2812B 5V` -- `WS2812B GND` -> common ground rail +- `Mega D22` -> main backlight/status strip `DIN` +- `Mega D36` -> indicator strip `DIN` +- `5V LED supply` -> both strip `5V` inputs +- both strip `GND` inputs -> common ground rail If the LED chain is long or bright: diff --git a/Stripboard_layout.md b/Stripboard_layout.md index 2349647..90c59a3 100644 --- a/Stripboard_layout.md +++ b/Stripboard_layout.md @@ -205,9 +205,10 @@ If `D8` and `D9` come from separate fly wires to the stripboard, keep them in th Route: -- `D22` -> `WS2812 DIN` -- `5V` -> `WS2812 5V` -- `GND` -> `WS2812 GND` +- `D22` -> main backlight/status strip `DIN` +- `D36` -> indicator strip `DIN` +- `5V` -> both strip `5V` inputs +- `GND` -> both strip `GND` inputs Keep the LED connector in the low-voltage area. diff --git a/config.example.json b/archive/config.example.json similarity index 100% rename from config.example.json rename to archive/config.example.json diff --git a/gauge.py b/archive/gauge.py similarity index 100% rename from gauge.py rename to archive/gauge.py diff --git a/main.py b/archive/main.py similarity index 100% rename from main.py rename to archive/main.py diff --git a/ota.py b/archive/ota.py similarity index 100% rename from ota.py rename to archive/ota.py diff --git a/ota_config.example.json b/archive/ota_config.example.json similarity index 100% rename from ota_config.example.json rename to archive/ota_config.example.json diff --git a/ota_manifest.txt b/archive/ota_manifest.txt similarity index 100% rename from ota_manifest.txt rename to archive/ota_manifest.txt diff --git a/esp-home-rewrite.yaml b/gaugecontroller.yaml similarity index 100% rename from esp-home-rewrite.yaml rename to gaugecontroller.yaml diff --git a/wiring.md b/wiring.md index 7ce1239..5f37b0f 100644 --- a/wiring.md +++ b/wiring.md @@ -163,19 +163,22 @@ Also connect: If your driver boards need separate motor power, supply that from the proper motor supply. Do not power motors from the Mega `5V` pin. -## WS2812B LED Strip +## WS2812 LED Strips -The current sketch expects one shared WS2812B chain. +The current sketch expects two LED data chains. Backlight and status LEDs stay +on the main strip; the red/green dial indicator LEDs are on their own strip. -| Mega Pin | WS2812B | +| Mega Pin | LED Strip | |---|---| -| `D22` | `DIN` | -| `5V` | `5V` | -| `GND` | `GND` | +| `D22` | main backlight/status `DIN` | +| `D36` | indicator `DIN` | +| `5V` | both strips `5V` | +| `GND` | both strips `GND` | Notes: -- the code expects `7 LEDs per gauge`, so `21 LEDs total` +- the command protocol still exposes `7 LEDs per gauge` +- logical indices `0-2` are backlight, `3-4` are indicators, and `5-6` are status - use a proper 5V supply sized for the LED current - keep LED ground common with the Mega