34 Commits

Author SHA1 Message Date
00b9287d23 docs: update CLAUDE.md for v2.0 — GaugeConfig, no LEDs, correct serial default 2026-05-21 22:32:47 +02:00
f270e4b83f fix: consistent check order and explicit error reply on bad tokens in parseSweep 2026-05-21 22:23:44 +02:00
352d47ef59 fix: use strtod with end-pointer validation for robust float parsing 2026-05-21 22:20:14 +02:00
61c1c733e9 fix: replace sscanf %f with %s+atof for AVR compatibility in parsers 2026-05-21 22:15:15 +02:00
e1849f0dd1 refactor: uniform sscanf parsing in parseSpeed, parseAccel, parseSweep 2026-05-21 22:10:43 +02:00
30dfcc59df refactor: wire gauge_config.h into sketch, remove GaugePins and hardcoded defaults 2026-05-21 22:05:38 +02:00
05b7137fcd refactor: improve column comment alignment in gaugeConfigs table 2026-05-21 21:56:09 +02:00
836af7e836 refactor: add gauge_config.h with centralised pin and motion defaults 2026-05-21 21:52:58 +02:00
a706838b57 docs: add Gaugecontroller v2.0 implementation plan 2026-05-21 21:51:27 +02:00
7c3068ff3a docs: add Gaugecontroller v2.0 design spec
Describes the two-part refactor: gauge_config.h for centralised pin and
motion defaults, and uniform sscanf parsing across all parse* functions.
2026-05-21 21:47:07 +02:00
e525dba0c4 Changed timing to timer interrupts - scoping data showed inconsistencies 2026-05-19 00:50:12 +02:00
1b699352ce LEDs removed from Gaugecontroller.ino, backup in aptly named directory 2026-05-18 16:04:37 +02:00
c32d208854 Single colour LEDs, I think, will be removed anyway 2026-05-17 18:00:19 +02:00
db05bc0864 5th gauge added 2026-05-03 17:34:09 +02:00
5f73e75f5b Indicator LEDs are now bog-standard red and green LEDs. Looks more original. 2026-05-03 15:59:20 +02:00
5656986768 Gauges take precedence over LEDs. 2026-05-03 14:18:54 +02:00
aa029587a4 Timing optimised 2026-05-02 21:57:30 +02:00
abbbd16b5c Version with no VFD built 2026-05-02 21:34:29 +02:00
e63867ba5e Pins changed 2026-05-02 13:19:01 +02:00
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
22 changed files with 8478 additions and 932 deletions

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@
*.out *.out
*.app *.app
.codex

View File

@@ -4,7 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build & Upload ## Build & Upload
Main firmware lives in `Gaugecontroller/Gaugecontroller.ino`. Requires the **FastLED** library (`arduino-cli lib install FastLED`). Use the Arduino IDE or `arduino-cli`: Main firmware lives in `Gaugecontroller/Gaugecontroller.ino`. No external libraries required on the `Stepper-Only` branch. Use the Arduino IDE or `arduino-cli`:
The ESP32 bridge runs ESPHome; the config is in `gaugecontroller.yaml`.
```bash ```bash
# Compile (replace board/port as needed) # Compile (replace board/port as needed)
@@ -14,7 +16,7 @@ arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega Gaugecontroller arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega Gaugecontroller
``` ```
Current default serial setup: `CMD_PORT` and `DEBUG_PORT` both point to `Serial1` at 38400 baud. Current default serial setup: `CMD_PORT` and `DEBUG_PORT` both point to `Serial` (USB) at 38400 baud.
## Switching serial ports (debug → production) ## Switching serial ports (debug → production)
@@ -25,7 +27,7 @@ Two `#define`s at the top of `Gaugecontroller.ino` control where commands and de
#define DEBUG_PORT Serial1 // diagnostic prints (homing, boot messages) #define DEBUG_PORT Serial1 // diagnostic prints (homing, boot messages)
``` ```
**Current default:** both point to `Serial1`, so command and debug traffic share Mega pins TX1=18 / RX1=19 at 38400 baud. **Current default:** both point to `Serial` (USB), so command and debug traffic go over the Arduino USB port at 38400 baud.
**USB-only debug setup:** point both defines back at `Serial` if you want to talk to the sketch over the Arduino USB port instead: **USB-only debug setup:** point both defines back at `Serial` if you want to talk to the sketch over the Arduino USB port instead:
@@ -58,8 +60,8 @@ 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. - `GaugeConfig` — compile-time config per gauge: pin assignments (dir, step, enable, polarity flags) and motion defaults (minPos, maxPos, homingBackoffSteps, maxSpeed, accel, homingSpeed). All gauges are defined in `gauge_config.h` as `constexpr GaugeConfig gaugeConfigs[]`. `GAUGE_COUNT` is derived automatically from the array length.
- `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. Initialised from `gaugeConfigs[]` in `setup()`.
### Motion control (`updateGauge`) ### Motion control (`updateGauge`)
@@ -74,10 +76,6 @@ Backs up `homingBackoffSteps` at `homingSpeed`, waits 100 ms settle, then declar
When `sweepEnabled`, `updateSweepTarget` bounces `targetPos` between `minPos` and `maxPos` autonomously. When `sweepEnabled`, `updateSweepTarget` bounces `targetPos` between `minPos` and `maxPos` autonomously.
### 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.
### Serial command protocol ### Serial command protocol
Commands arrive as newline-terminated ASCII lines. Each `parse*` function in `processLine` handles one command family: Commands arrive as newline-terminated ASCII lines. Each `parse*` function in `processLine` handles one command family:
@@ -92,18 +90,12 @@ Commands arrive as newline-terminated ASCII lines. Each `parse*` function in `pr
| `HOME` | `HOME <id>` / `HOMEALL` | Run homing sequence | | `HOME` | `HOME <id>` / `HOMEALL` | Run homing sequence |
| `SWEEP` | `SWEEP <id> <accel> <speed>` | Start sweep (0/0 stops) | | `SWEEP` | `SWEEP <id> <accel> <speed>` | Start sweep (0/0 stops) |
| `POS?` | `POS?` | Query all gauges: `POS <id> <cur> <tgt> <homed> <homingState> <sweep>` | | `POS?` | `POS?` | Query all gauges: `POS <id> <cur> <tgt> <homed> <homingState> <sweep>` |
| `LED` | `LED <id> <idx> <r> <g> <b>` | Set one LED (0-based index within gauge segment) to RGB colour (0255 each); `<idx>` may be a range `N-M` to set LEDs N through M in one command; also stops any active effect on those LEDs | | `CFG?` | `CFG?` | Query all gauges: `CFG <id> <maxSpeed> <accel>` per gauge |
| `LED?` | `LED?` | Query all LEDs: one `LED <id> <idx> <r> <g> <b>` line per LED, then `OK` |
| `BLINK` | `BLINK <id> <idx> <on_ms> <off_ms> <r> <g> <b>` | Blink LED(s) at given colour; `<idx>` may be a range `N-M`; `on_ms`/`off_ms` both 0 stops blinking. 4-arg form (no colour) uses current LED colour |
| `BREATHE` | `BREATHE <id> <idx> <period_ms> <r> <g> <b>` | Smooth triangle-wave fade between black and the given colour; `<idx>` may be a range `N-M` |
| `DFLASH` | `DFLASH <id> <idx> <r> <g> <b>` | Two quick flashes (100 ms on/off each) followed by a 700 ms pause, then repeats; `<idx>` may be a range `N-M` |
| `PING` | `PING` | Responds `PONG` | | `PING` | `PING` | Responds `PONG` |
All commands reply `OK` or `ERR BAD_ID` / `ERR BAD_CMD` etc. All commands reply `OK` or `ERR BAD_ID` / `ERR BAD_CMD` etc.
### Adding gauges ### Adding gauges
1. Increment `GAUGE_COUNT`. 1. Open `Gaugecontroller/gauge_config.h` and append one row to `gaugeConfigs[]`.
2. Add a `constexpr GaugePins` entry to `gaugePins[]` (including `ledCount`). 2. `GAUGE_COUNT` updates automatically — no other changes needed.
3. Tune `maxPos` and `homingBackoffSteps` in the corresponding `Gauge` default or at runtime.
4. `TOTAL_LEDS` and `gaugeLedOffset[]` update automatically — no manual changes needed.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
#pragma once
#include <stdint.h>
struct GaugeConfig {
// Hardware
uint8_t dirPin;
uint8_t stepPin;
int8_t enablePin; // -1 = no enable pin
bool dirInverted;
bool stepActiveHigh;
bool enableActiveLow;
// Motion defaults (integers; cast to float in setup())
long minPos;
long maxPos;
long homingBackoffSteps;
int maxSpeed; // steps/s
int accel; // steps/s²
int homingSpeed; // steps/s
};
constexpr GaugeConfig gaugeConfigs[] = {
// dir step en dirInv stpHi enLow min max backoff speed accel hmSpd
{ 48, 49, -1, false, true, true, 0, 3780, 3800, 4000, 6000, 500 },
{ 8, 9, -1, true, true, true, 0, 3780, 3800, 4000, 6000, 500 },
{ 52, 53, -1, false, true, true, 0, 3780, 3800, 4000, 6000, 500 },
{ 50, 51, -1, false, true, true, 0, 3780, 3800, 4000, 6000, 500 },
};
static const uint8_t GAUGE_COUNT =
sizeof(gaugeConfigs) / sizeof(gaugeConfigs[0]);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ A dedicated gauge controller for Arduinos.
This repository contains: This repository contains:
- `Gaugecontroller/Gaugecontroller.ino`: the Arduino Mega firmware for the stepper gauges, LEDs, and integrated HV5812-based VFD - `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 ## VFD Support
@@ -48,16 +48,19 @@ Rules:
- shorter values are right-aligned - shorter values are right-aligned
- leading zeroes are preserved if they are part of the input - 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` ### Gauge Controls
text entity for the displayed value - Number entities for each gauge's target value (with unit conversion)
- `VFD Decimal Point` - Number entities for speed and acceleration (diagnostic)
switch entity - Rezero buttons for each gauge and all gauges
- `VFD Alarm`
switch entity ### 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: 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 - hexadecimal values like `DEAD` or `BEEF` work
- clearing the display is possible with an empty value - 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: ### Diagnostics
- WiFi signal sensor
- `<prefix>/vfd/set` - Uptime sensor
- `<prefix>/vfd/state` - IP address and SSID text sensors
- `<prefix>/vfd/decimal_point/set` - Arduino Last Message sensor
- `<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.`.

View File

@@ -183,13 +183,14 @@ Then connect the motor side of that driver to:
according to the driver board you are using. according to the driver board you are using.
## 14. Wire The WS2812B LEDs ## 14. Wire The WS2812 LEDs
Connect: Connect:
- `Mega D22` -> `WS2812B DIN` - `Mega D22` -> main backlight/status strip `DIN`
- `5V LED supply` -> `WS2812B 5V` - `Mega D36` -> indicator strip `DIN`
- `WS2812B GND` -> common ground rail - `5V LED supply` -> both strip `5V` inputs
- both strip `GND` inputs -> common ground rail
If the LED chain is long or bright: 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: Route:
- `D22` -> `WS2812 DIN` - `D22` -> main backlight/status strip `DIN`
- `5V` -> `WS2812 5V` - `D36` -> indicator strip `DIN`
- `GND` -> `WS2812 GND` - `5V` -> both strip `5V` inputs
- `GND` -> both strip `GND` inputs
Keep the LED connector in the low-voltage area. Keep the LED connector in the low-voltage area.

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(True) _wifi_sta.active(False)
utime.sleep_ms(500) utime.sleep_ms(200)
try: _wifi_sta.active(True)
_wifi_sta.disconnect() utime.sleep_ms(500)
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,8 +561,7 @@ 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)
deadline = utime.time() + timeout_s deadline = utime.time() + timeout_s
@@ -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)

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

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%).

View File

@@ -0,0 +1,474 @@
# Gaugecontroller v2.0 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Centralise all gauge configuration in `gauge_config.h` and make the three inconsistently-written parsers use `sscanf` like the rest.
**Architecture:** A new header file holds one `constexpr GaugeConfig` table — pin assignments and motion defaults merged — and derives `GAUGE_COUNT` from its length. The sketch includes this header, removes the old `GaugePins` struct and all hardcoded defaults, and initialises `Gauge` runtime state from the table in `setup()`. Three parse functions are then rewritten from manual string splitting to `sscanf`.
**Tech Stack:** Arduino (AVR/Mega), `arduino-cli`, C++11 `constexpr`.
> **Note on testing:** This is a bare-metal Arduino sketch with no unit-test framework. Each task's verification step is a clean compile with `arduino-cli`. Functional testing requires the physical hardware; the plan notes what to check over serial when hardware is available.
---
## File Map
| File | Action | Responsibility |
|---|---|---|
| `Gaugecontroller/gauge_config.h` | **Create** | All pin assignments and motion defaults; `GAUGE_COUNT` |
| `Gaugecontroller/Gaugecontroller.ino` | **Modify** | Remove `GaugePins`, add include, strip `Gauge` defaults, update all references, rewrite 3 parsers |
---
## Task 1: Create `gauge_config.h`
**Files:**
- Create: `Gaugecontroller/gauge_config.h`
- [ ] **Step 1: Create the file**
Create `Gaugecontroller/gauge_config.h` with the following content:
```cpp
#pragma once
#include <stdint.h>
struct GaugeConfig {
// Hardware
uint8_t dirPin;
uint8_t stepPin;
int8_t enablePin; // -1 = no enable pin
bool dirInverted;
bool stepActiveHigh;
bool enableActiveLow;
// Motion defaults (integers; cast to float in setup())
long minPos;
long maxPos;
long homingBackoffSteps;
int maxSpeed; // steps/s
int accel; // steps/s²
int homingSpeed; // steps/s
};
constexpr GaugeConfig gaugeConfigs[] = {
// dir step en dirInv stepHi enLow min max backoff speed accel homeSpd
{ 48, 49, -1, false, true, true, 0, 3780, 3800, 4000, 6000, 500 },
{ 8, 9, -1, true, true, true, 0, 3780, 3800, 4000, 6000, 500 },
{ 52, 53, -1, false, true, true, 0, 3780, 3800, 4000, 6000, 500 },
{ 50, 51, -1, false, true, true, 0, 3780, 3800, 4000, 6000, 500 },
};
static const uint8_t GAUGE_COUNT =
sizeof(gaugeConfigs) / sizeof(gaugeConfigs[0]);
```
To add a fifth gauge later: append one row to `gaugeConfigs[]`. Nothing else changes.
---
## Task 2: Wire `gauge_config.h` into `Gaugecontroller.ino`
**Files:**
- Modify: `Gaugecontroller/Gaugecontroller.ino`
This task makes six targeted edits in order. Each edit is shown as old → new. Do them top-to-bottom so line numbers don't shift unexpectedly.
- [ ] **Step 1: Add the include**
After the existing three `#include` lines at the top, add:
```cpp
#include "gauge_config.h"
```
The top of the file should now read:
```cpp
#include <Arduino.h>
#include <avr/interrupt.h>
#include <math.h>
#include "gauge_config.h"
```
- [ ] **Step 2: Remove `GAUGE_COUNT` and `GaugePins`**
Delete these two blocks entirely (they are now in `gauge_config.h`):
```cpp
static const uint8_t GAUGE_COUNT = 4;
```
```cpp
struct GaugePins {
uint8_t dirPin;
uint8_t stepPin;
int8_t enablePin; // -1 means there is no enable pin
bool dirInverted;
bool stepActiveHigh;
bool enableActiveLow;
};
constexpr GaugePins gaugePins[GAUGE_COUNT] = {
// dir, step, en, dirInv, stepHigh, enActiveLow
{48, 49, -1, false, true, true}, // Gauge 0
{8, 9, -1, true, true, true}, // Gauge 1
{52, 53, -1, false, true, true}, // Gauge 2
{50, 51, -1, false, true, true}, // Gauge 3
};
```
- [ ] **Step 3: Strip hardcoded defaults from `struct Gauge`**
In the `Gauge` struct definition, remove the numeric defaults from the six motion fields. Change:
```cpp
long minPos = 0;
long maxPos = 3780;
long homingBackoffSteps = 3800; // Deliberately a touch past full reverse travel.
float velocity = 0.0f;
float maxSpeed = 4000.0f;
float accel = 6000.0f;
float homingSpeed = 500.0f;
```
To:
```cpp
long minPos = 0;
long maxPos = 0;
long homingBackoffSteps = 0;
float velocity = 0.0f;
float maxSpeed = 0.0f;
float accel = 0.0f;
float homingSpeed = 0.0f;
```
These will be populated from `gaugeConfigs[i]` in `setup()` (Step 5).
- [ ] **Step 4: Update `gaugePins` → `gaugeConfigs` references outside `setup()`**
Three functions reference `gaugePins`. Update each one:
**`writeDirectionPin` (~line 106):**
```cpp
// Before
bool level = gaugePins[id].dirInverted ? !forward : forward;
// After
bool level = gaugeConfigs[id].dirInverted ? !forward : forward;
```
**`writeStepPin` (~line 111):**
```cpp
// Before
bool level = gaugePins[id].stepActiveHigh ? active : !active;
// After
bool level = gaugeConfigs[id].stepActiveHigh ? active : !active;
```
**`configureStepperHardware` (~line 152):**
```cpp
// Before
stepperHardware[id].stepPort = portOutputRegister(digitalPinToPort(gaugePins[id].stepPin));
stepperHardware[id].stepMask = digitalPinToBitMask(gaugePins[id].stepPin);
stepperHardware[id].dirPort = portOutputRegister(digitalPinToPort(gaugePins[id].dirPin));
stepperHardware[id].dirMask = digitalPinToBitMask(gaugePins[id].dirPin);
// After
stepperHardware[id].stepPort = portOutputRegister(digitalPinToPort(gaugeConfigs[id].stepPin));
stepperHardware[id].stepMask = digitalPinToBitMask(gaugeConfigs[id].stepPin);
stepperHardware[id].dirPort = portOutputRegister(digitalPinToPort(gaugeConfigs[id].dirPin));
stepperHardware[id].dirMask = digitalPinToBitMask(gaugeConfigs[id].dirPin);
```
**`setEnable` (~line 291):**
```cpp
// Before
int8_t pin = gaugePins[id].enablePin;
if (pin < 0) return;
bool level = gaugePins[id].enableActiveLow ? !en : en;
// After
int8_t pin = gaugeConfigs[id].enablePin;
if (pin < 0) return;
bool level = gaugeConfigs[id].enableActiveLow ? !en : en;
```
- [ ] **Step 5: Update `setup()` — pin references and add motion default init**
In `setup()`, the `for` loop currently reads:
```cpp
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
pinMode(gaugePins[i].dirPin, OUTPUT);
pinMode(gaugePins[i].stepPin, OUTPUT);
configureStepperHardware(i);
digitalWrite(gaugePins[i].dirPin, LOW);
digitalWrite(gaugePins[i].stepPin, gaugePins[i].stepActiveHigh ? LOW : HIGH);
if (gaugePins[i].enablePin >= 0) {
pinMode(gaugePins[i].enablePin, OUTPUT);
setEnable(i, true);
}
gauges[i].lastUpdateMicros = micros();
}
```
Replace it with:
```cpp
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
pinMode(gaugeConfigs[i].dirPin, OUTPUT);
pinMode(gaugeConfigs[i].stepPin, OUTPUT);
configureStepperHardware(i);
digitalWrite(gaugeConfigs[i].dirPin, LOW);
digitalWrite(gaugeConfigs[i].stepPin, gaugeConfigs[i].stepActiveHigh ? LOW : HIGH);
if (gaugeConfigs[i].enablePin >= 0) {
pinMode(gaugeConfigs[i].enablePin, OUTPUT);
setEnable(i, true);
}
gauges[i].minPos = gaugeConfigs[i].minPos;
gauges[i].maxPos = gaugeConfigs[i].maxPos;
gauges[i].homingBackoffSteps = gaugeConfigs[i].homingBackoffSteps;
gauges[i].maxSpeed = (float)gaugeConfigs[i].maxSpeed;
gauges[i].accel = (float)gaugeConfigs[i].accel;
gauges[i].homingSpeed = (float)gaugeConfigs[i].homingSpeed;
gauges[i].lastUpdateMicros = micros();
}
```
- [ ] **Step 6: Compile and verify clean**
```bash
arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller
```
Expected: zero errors, zero warnings about `gaugePins` or `GAUGE_COUNT`. If the compiler reports "use of undeclared identifier 'gaugePins'", grep for any remaining reference:
```bash
grep -n "gaugePins" Gaugecontroller/Gaugecontroller.ino
```
Should return nothing.
- [ ] **Step 7: Commit**
```bash
git add Gaugecontroller/gauge_config.h Gaugecontroller/Gaugecontroller.ino
git commit -m "refactor: centralise gauge config in gauge_config.h"
```
---
## Task 3: Rewrite `parseSpeed`, `parseAccel`, `parseSweep` to use `sscanf`
**Files:**
- Modify: `Gaugecontroller/Gaugecontroller.ino`
- [ ] **Step 1: Replace `parseSpeed`**
Find and replace the entire `parseSpeed` function:
```cpp
// Before (~15 lines)
bool parseSpeed(const String& line) {
int firstSpace = line.indexOf(' ');
int secondSpace = line.indexOf(' ', firstSpace + 1);
if (firstSpace < 0 || secondSpace < 0) return false;
if (line.substring(0, firstSpace) != "SPEED") return false;
int id = line.substring(firstSpace + 1, secondSpace).toInt();
float speed = line.substring(secondSpace + 1).toFloat();
if (id < 0 || id >= GAUGE_COUNT) {
sendReply("ERR BAD_ID");
return true;
}
if (speed <= 0.0f) {
sendReply("ERR BAD_SPEED");
return true;
}
gauges[id].maxSpeed = speed;
sendReply("OK");
return true;
}
```
```cpp
// After
bool parseSpeed(const String& line) {
int id; float speed;
if (sscanf(line.c_str(), "SPEED %d %f", &id, &speed) == 2) {
if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; }
if (speed <= 0.0f) { sendReply("ERR BAD_SPEED"); return true; }
gauges[id].maxSpeed = speed;
sendReply("OK");
return true;
}
return false;
}
```
- [ ] **Step 2: Replace `parseAccel`**
```cpp
// Before (~15 lines)
bool parseAccel(const String& line) {
int firstSpace = line.indexOf(' ');
int secondSpace = line.indexOf(' ', firstSpace + 1);
if (firstSpace < 0 || secondSpace < 0) return false;
if (line.substring(0, firstSpace) != "ACCEL") return false;
int id = line.substring(firstSpace + 1, secondSpace).toInt();
float accel = line.substring(secondSpace + 1).toFloat();
if (id < 0 || id >= GAUGE_COUNT) {
sendReply("ERR BAD_ID");
return true;
}
if (accel <= 0.0f) {
sendReply("ERR BAD_ACCEL");
return true;
}
gauges[id].accel = accel;
sendReply("OK");
return true;
}
```
```cpp
// After
bool parseAccel(const String& line) {
int id; float accel;
if (sscanf(line.c_str(), "ACCEL %d %f", &id, &accel) == 2) {
if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; }
if (accel <= 0.0f) { sendReply("ERR BAD_ACCEL"); return true; }
gauges[id].accel = accel;
sendReply("OK");
return true;
}
return false;
}
```
- [ ] **Step 3: Replace `parseSweep`**
```cpp
// Before (~20 lines)
bool parseSweep(const String& line) {
int firstSpace = line.indexOf(' ');
int secondSpace = line.indexOf(' ', firstSpace + 1);
int thirdSpace = line.indexOf(' ', secondSpace + 1);
if (firstSpace < 0 || secondSpace < 0 || thirdSpace < 0) return false;
if (line.substring(0, firstSpace) != "SWEEP") return false;
int id = line.substring(firstSpace + 1, secondSpace).toInt();
float accel = line.substring(secondSpace + 1, thirdSpace).toFloat();
float speed = line.substring(thirdSpace + 1).toFloat();
if (id < 0 || id >= GAUGE_COUNT) {
sendReply("ERR BAD_ID");
return true;
}
Gauge& g = gauges[id];
if (accel <= 0.0f || speed <= 0.0f) {
g.sweepEnabled = false;
g.velocity = 0.0f;
stopTimerStepping(id);
sendReply("OK");
return true;
}
g.accel = accel;
g.maxSpeed = speed;
g.sweepEnabled = true;
g.sweepTowardMax = true;
atomicWriteLong(g.targetPos, g.maxPos);
sendReply("OK");
return true;
}
```
```cpp
// After
bool parseSweep(const String& line) {
int id; float accel, speed;
if (sscanf(line.c_str(), "SWEEP %d %f %f", &id, &accel, &speed) == 3) {
if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; }
Gauge& g = gauges[id];
if (accel <= 0.0f || speed <= 0.0f) {
g.sweepEnabled = false;
g.velocity = 0.0f;
stopTimerStepping(id);
sendReply("OK");
return true;
}
g.accel = accel;
g.maxSpeed = speed;
g.sweepEnabled = true;
g.sweepTowardMax = true;
atomicWriteLong(g.targetPos, g.maxPos);
sendReply("OK");
return true;
}
return false;
}
```
- [ ] **Step 4: Verify no `indexOf`/`substring` remain in any `parse*` function**
```bash
grep -n "indexOf\|substring" Gaugecontroller/Gaugecontroller.ino
```
Expected: no output. If any lines appear, check which function still uses the old pattern and redo that step.
- [ ] **Step 5: Compile and verify clean**
```bash
arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller
```
Expected: zero errors.
- [ ] **Step 6: Commit**
```bash
git add Gaugecontroller/Gaugecontroller.ino
git commit -m "refactor: uniform sscanf parsing in parseSpeed, parseAccel, parseSweep"
```
---
## Optional hardware smoke-test (when board is available)
After uploading, send these commands over serial and confirm expected replies:
```
PING → PONG
HOMEALL → OK (then each gauge homes; HOMED 0..3 appear on debug port)
POS? → POS 0 0 0 1 0 0 (×4, one per gauge)
SET 0 1000 → OK
SPEED 0 2000 → OK
ACCEL 0 8000 → OK
SWEEP 0 6000 4000 → OK
SWEEP 0 0 0 → OK (stops sweep)
```
No new error codes were introduced; all existing commands should behave identically to v1.

View File

@@ -0,0 +1,162 @@
# Gaugecontroller v2.0 Design
**Date:** 2026-05-21
**Branch:** Stepper-Only
**Scope:** Code quality / architecture — same features, better structure. No behaviour change.
## Goal
Eliminate scattered magic constants and inconsistent parsing patterns. A developer adding or tuning a gauge should only need to edit one file.
## What is NOT changing
- ISR logic, Q16 fixed-point stepping, trapezoidal velocity profile
- Serial protocol commands and responses
- Runtime `Gauge` struct fields (stay `float` for velocity, speed, accel)
- LED code (absent on this branch; out of scope)
---
## Section 1: `gauge_config.h`
Create `Gaugecontroller/gauge_config.h` alongside the sketch.
### New struct
```cpp
struct GaugeConfig {
// Hardware
uint8_t dirPin;
uint8_t stepPin;
int8_t enablePin; // -1 = no enable pin
bool dirInverted;
bool stepActiveHigh;
bool enableActiveLow;
// Motion defaults (integers — cast to float in setup())
long minPos;
long maxPos;
long homingBackoffSteps;
int maxSpeed; // steps/s
int accel; // steps/s²
int homingSpeed; // steps/s
};
```
### Config table
```cpp
constexpr GaugeConfig gaugeConfigs[] = {
// dir step en dirInv stepHi enLow min max backoff speed accel homeSpd
{ 48, 49, -1, false, true, true, 0, 3780, 3800, 4000, 6000, 500 },
{ 8, 9, -1, true, true, true, 0, 3780, 3800, 4000, 6000, 500 },
{ 52, 53, -1, false, true, true, 0, 3780, 3800, 4000, 6000, 500 },
{ 50, 51, -1, false, true, true, 0, 3780, 3800, 4000, 6000, 500 },
};
static const uint8_t GAUGE_COUNT =
sizeof(gaugeConfigs) / sizeof(gaugeConfigs[0]);
```
Adding gauge 5 is one new table row. `GAUGE_COUNT` updates automatically.
### Changes to `Gaugecontroller.ino`
- Remove `constexpr GaugePins gaugePins[]`, `struct GaugePins`, and the hardcoded `GAUGE_COUNT`.
- Add `#include "gauge_config.h"`.
- In `setup()`, initialise each `Gauge`'s motion defaults from `gaugeConfigs[i]`:
```cpp
gauges[i].minPos = gaugeConfigs[i].minPos;
gauges[i].maxPos = gaugeConfigs[i].maxPos;
gauges[i].homingBackoffSteps = gaugeConfigs[i].homingBackoffSteps;
gauges[i].maxSpeed = (float)gaugeConfigs[i].maxSpeed;
gauges[i].accel = (float)gaugeConfigs[i].accel;
gauges[i].homingSpeed = (float)gaugeConfigs[i].homingSpeed;
```
- All existing references to `gaugePins[i].dirPin` etc. become `gaugeConfigs[i].dirPin` etc. (field names are identical).
- Remove the hardcoded default initialisers from the `Gauge` struct definition (`maxPos = 3780`, `homingBackoffSteps = 3800`, `maxSpeed = 4000.0f`, `accel = 6000.0f`, `homingSpeed = 500.0f`, `minPos = 0`). These fields become zero-initialised and are then set from `gaugeConfigs[i]` in `setup()`, eliminating the risk of the struct defaults and config table silently diverging.
---
## Section 2: Uniform `sscanf` parsing
Three `parse*` functions currently use manual `indexOf`/`substring`. Convert them to `sscanf` to match the rest of the parser.
### `parseSpeed`
```cpp
bool parseSpeed(const String& line) {
int id; float speed;
if (sscanf(line.c_str(), "SPEED %d %f", &id, &speed) == 2) {
if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; }
if (speed <= 0.0f) { sendReply("ERR BAD_SPEED"); return true; }
gauges[id].maxSpeed = speed;
sendReply("OK");
return true;
}
return false;
}
```
### `parseAccel`
```cpp
bool parseAccel(const String& line) {
int id; float accel;
if (sscanf(line.c_str(), "ACCEL %d %f", &id, &accel) == 2) {
if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; }
if (accel <= 0.0f) { sendReply("ERR BAD_ACCEL"); return true; }
gauges[id].accel = accel;
sendReply("OK");
return true;
}
return false;
}
```
### `parseSweep`
```cpp
bool parseSweep(const String& line) {
int id; float accel, speed;
if (sscanf(line.c_str(), "SWEEP %d %f %f", &id, &accel, &speed) == 3) {
if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; }
Gauge& g = gauges[id];
if (accel <= 0.0f || speed <= 0.0f) {
g.sweepEnabled = false;
g.velocity = 0.0f;
stopTimerStepping(id);
sendReply("OK");
return true;
}
g.accel = accel;
g.maxSpeed = speed;
g.sweepEnabled = true;
g.sweepTowardMax = true;
atomicWriteLong(g.targetPos, g.maxPos);
sendReply("OK");
return true;
}
return false;
}
```
No change to accepted syntax, error codes, or response format.
---
## File inventory
| File | Change |
|---|---|
| `Gaugecontroller/gauge_config.h` | New — all pin + motion defaults |
| `Gaugecontroller/Gaugecontroller.ino` | Remove `GaugePins`, add include, update `setup()`, rewrite 3 parsers |
## Success criteria
- Sketch compiles cleanly with `arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller`
- `GAUGE_COUNT` need not be edited when adding a gauge — only `gaugeConfigs[]` changes
- No `indexOf`/`substring` remain in any `parse*` function
- All existing protocol commands behave identically to v1

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. 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` | | `D22` | main backlight/status `DIN` |
| `5V` | `5V` | | `D36` | indicator `DIN` |
| `GND` | `GND` | | `5V` | both strips `5V` |
| `GND` | both strips `GND` |
Notes: 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 - use a proper 5V supply sized for the LED current
- keep LED ground common with the Mega - keep LED ground common with the Mega