15 Commits
review ... main

Author SHA1 Message Date
8bdae1da9b 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
2026-04-29 19:03:22 +02:00
361cf52252 More logs 2026-04-28 00:31:23 +02:00
2d63ec6006 Reverted - same problem again 2026-04-28 00:22:01 +02:00
511ee05712 check actual values for indicator LEDs 2026-04-28 00:18:32 +02:00
03ab8604ba write_action removed - worked before that... 2026-04-28 00:14:55 +02:00
edb973bb61 Reverted the whole AI bullshit 2026-04-27 23:46:45 +02:00
bffcf62cae Timing changed on Arduino script, latest version of ESP-Home script added 2026-04-27 19:20:30 +02:00
27597bceab configurable GRB/RGB LEDs per LED 2026-04-27 09:06:43 +02:00
016de2ccb4 4 Gauges 2026-04-26 23:26:00 +02:00
15257ae6f2 CFG? added 2026-04-23 00:31:17 +02:00
795eb0ecf3 Some fixes (integers for speed, Serial returns back to HA etc.) 2026-04-22 16:14:10 +02:00
558c5b18c2 extended to 4 gauges, zeroing for all gauges individually and collectively added 2026-04-22 15:54:53 +02:00
fa66dd70d4 Speed/Config and VFD added 2026-04-22 14:56:04 +02:00
b14bdf7fc3 Added "SET" implementation for gauge values 2026-04-22 14:49:59 +02:00
427dde8c72 Continued rewrite - Lights and light effects implemented 2026-04-22 14:39:01 +02:00
14 changed files with 2652 additions and 97 deletions

View File

@@ -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
@@ -58,7 +60,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 +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].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.
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
@@ -104,6 +106,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.

View File

@@ -3,10 +3,14 @@
#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.
// 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
@@ -226,21 +230,41 @@ 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();
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,
@@ -295,10 +319,88 @@ 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.
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;
}
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) {
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 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.
//
@@ -794,6 +896,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 +955,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,15 +985,14 @@ 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");
return true;
}
@@ -894,7 +1013,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,15 +1034,14 @@ 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");
return true;
}
@@ -939,7 +1057,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,9 +1072,8 @@ 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");
return true;
}
@@ -972,7 +1089,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,20 +1102,18 @@ 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");
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 < 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,25 +1124,25 @@ void updateBlink() {
if ((nowMs - bs.lastMs) >= period) {
bs.currentlyOn = !bs.currentlyOn;
bs.lastMs = nowMs;
leds[gi] = bs.currentlyOn ? bs.onColor : CRGB::Black;
changed = true;
writeLed(gi, bs.currentlyOn ? bs.onColor : CRGB::Black);
}
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)
: (uint8_t)((uint32_t)(bs.periodMs - bs.cyclePos) * 255 / half);
leds[gi] = bs.onColor;
leds[gi].nscale8(bri ? bri : 1);
changed = true;
CRGB scaled = bs.onColor;
scaled.nscale8(bri ? bri : 1);
writeLed(gi, scaled);
break;
}
case FX_DFLASH: {
@@ -1035,16 +1150,13 @@ 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;
changed = true;
writeLed(gi, (bs.dphase == 0 || bs.dphase == 2) ? bs.onColor : CRGB::Black);
}
break;
}
}
}
}
if (changed) ledsDirty = true;
}
// Runs the command parsers in order until one claims the line.
@@ -1058,6 +1170,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;
@@ -1112,15 +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;
ledOff += gaugePins[i].ledCount;
gaugeMainLedOffset[i] = mainLedOff;
gaugeIndicatorLedOffset[i] = indicatorLedOff;
ledOff += gaugeLedCount[i];
indicatorLedOff += countIndicatorLedsForGauge(i);
mainLedOff += gaugeLedCount[i] - countIndicatorLedsForGauge(i);
}
FastLED.addLeds<WS2812B, LED_DATA_PIN, GRB>(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();
@@ -1141,10 +1264,7 @@ void loop() {
updateGauge(i);
}
if (ledsDirty) {
FastLED.show();
ledsDirty = false;
}
showDirtyLeds();
}

View File

@@ -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:
- `<prefix>/vfd/set`
- `<prefix>/vfd/state`
- `<prefix>/vfd/decimal_point/set`
- `<prefix>/vfd/decimal_point/state`
- `<prefix>/vfd/alarm/set`
- `<prefix>/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

View File

@@ -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:

View File

@@ -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.

46
changes.md Normal file
View File

@@ -0,0 +1,46 @@
# Changes
## 2026-04-27 — Arduino firmware refactor (`Gaugecontroller/Gaugecontroller.ino`)
### Non-blocking VFD multiplexer
`vfd::refresh()` previously held each digit for 2000 µs via `delayMicroseconds`,
which capped the effective stepper pulse rate at roughly 500 Hz regardless of
`maxSpeed`. It now tracks `phaseStartMicros`/`phaseActive` and returns
immediately while the digit is still being held; the main loop runs at
microsecond cadence again and the configured `maxSpeed = 4000.0f` steps/s is
actually achievable.
### Fixed-buffer command parser (no more `String` heap churn)
Replaced `String rxLine` with `char rxBuf[128]` and converted the entire
command pipeline to take `const char*`:
- `processLine`, `sendReply`, `vfd::parseCommand`
- All `parse*` functions: `parseSet`, `parseSpeed`, `parseAccel`, `parseEnable`,
`parseZero`, `parseHome`, `parseSweep`, `parsePosQuery`, `parseCfgQuery`,
`parseLedQuery`, `parseLed`, `parseBlink`, `parseBreathe`, `parseDflash`,
`parseVfd`, `parsePing`.
`parseSpeed` / `parseAccel` / `parseSweep` use `strncmp` + `atof` because the
default AVR-libc `sscanf` doesn't support `%f`. No allocations on the command
path; the Mega's heap no longer fragments over time.
### Cached `ledNeedsSwap[TOTAL_LEDS]`
Per-LED RGB-vs-GRB swap flag is now precomputed once in `setup()` from
`gaugePins[].ledOrder`. `encodeForStrip` is a single array index instead of
walking the gauge table on every LED read/write.
### Cached step direction per gauge
Added `Gauge.lastDir`. `setDir()` skips the DIR-pin `digitalWrite` when the
direction hasn't flipped (the common case during a step run) and adds a 1 µs
DIR-to-STEP setup delay only when it actually flips.
### Cleanups
- Removed the `absf` helper; use `fabsf` consistently.
- Removed the `+ 0.0001f` epsilon in the trapezoidal braking-distance divisor.
`parseAccel` already rejects `accel <= 0`, so the divisor is always positive.
- Fixed the `<r> <ig> <b>` typo to `<r> <g> <b>` in the protocol comment for
`DFLASH`.
### Build verification
`arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller`:
17758 B flash (6%), 1845 B SRAM (22%).

2394
gaugecontroller.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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