52 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
ad50fd2ee5 Wifi bugs... 2026-04-21 21:07:42 +02:00
ae6d72b292 Now by hand... 2026-04-21 21:05:29 +02:00
2ea19b14f8 Attempt with tcp probe 2026-04-21 20:26:31 +02:00
1f8ba45685 Attempt with tcp probe 2026-04-21 20:10:38 +02:00
5a98a3c63b No more reconnect in main 2026-04-21 20:04:11 +02:00
afe70da24d MQTT trouble,still 2026-04-21 19:57:22 +02:00
0d7ae5cedc MQTT trouble 2026-04-21 18:52:53 +02:00
b3e2ef0f81 MQTT trouble 2026-04-21 18:41:10 +02:00
6068628d13 MQTT trouble 2026-04-21 18:40:10 +02:00
64b0aa482f MQTT trouble 2026-04-21 18:34:29 +02:00
2e5e410897 More garbage collection 2026-04-21 01:43:57 +02:00
4045da8964 WiFi changes again 2026-04-21 01:39:26 +02:00
81099d9887 Top speed lowered 2026-04-21 01:32:34 +02:00
355faadc31 WiFi made more resilient 2026-04-21 01:32:11 +02:00
109f97caa0 Baccklight bug fixed 2026-04-21 01:16:34 +02:00
9e7311fb7d Resistor divider added to wiring.md 2026-04-21 01:03:51 +02:00
9912fe82cc Documented wiring and layout 2026-04-21 00:52:18 +02:00
9f5deba0ed VFD functionality added to HA 2026-04-21 00:40:18 +02:00
21 changed files with 9788 additions and 804 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

@@ -1,3 +1,79 @@
# arduino_gauge_controller # arduino_gauge_controller
A dedicated gauge controller for Arduinos. A dedicated gauge controller for Arduinos.
## Overview
This repository contains:
- `Gaugecontroller/Gaugecontroller.ino`: the Arduino Mega firmware for the stepper gauges, LEDs, and integrated HV5812-based VFD
- `gaugecontroller.yaml`: the ESPHome-based ESP32 firmware that exposes the gauges and VFD to Home Assistant via the native API
## VFD Support
The integrated gauge controller now includes a 4-digit VFD with:
- 4 alphanumeric digits
- decimal point indicator
- alarm bell indicator
On the merged Arduino firmware, the HV5812 control pins are:
- `D46` -> `DATA`
- `D47` -> `CLOCK`
- `D48` -> `STROBE`
- `D49` -> `BLANK/OE`
The standalone VFD sketch used `D51/D52/D53/D49`, but `51/52/53` conflict with the gauge stepper pins in the integrated controller.
## Arduino Serial Commands
The merged Arduino firmware accepts:
- `VFD`
clears the display and turns off decimal point and alarm bell
- `VFD 1234`
- `VFD 0123`
- `VFD DEAD`
- `VFD 8888.`
- `VFD BEEF!`
- `VFD 12AF.!`
Rules:
- up to 4 characters are displayed
- valid characters are `0-9`, `A-F`, and `-`
- `.` enables the decimal point
- `!` enables the alarm bell
- shorter values are right-aligned
- leading zeroes are preserved if they are part of the input
## Home Assistant Integration
The ESPHome firmware in `gaugecontroller.yaml` exposes entities to Home Assistant via the native API:
### 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:
- leading zeroes are preserved
- hexadecimal values like `DEAD` or `BEEF` work
- clearing the display is possible with an empty value
### 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
### Diagnostics
- WiFi signal sensor
- Uptime sensor
- IP address and SSID text sensors
- Arduino Last Message sensor

323
Rewire_checklist.md Normal file
View File

@@ -0,0 +1,323 @@
# Rewire Checklist
This is a practical rebuild checklist for the current integrated setup:
- `Arduino Mega 2560`
- `ESP32` running `gauge.py`
- `HV5812P`
- 4-digit VFD with decimal point and alarm bell
- 3 gauge drivers
- WS2812B LED chain
Use this to rebuild the bench wiring from scratch.
## 1. Power Off Everything
- disconnect all power supplies
- disconnect USB power if it is currently feeding any board
- do not move wires while the VFD high-voltage supply is live
## 2. Place The Main Parts
- place the `Arduino Mega 2560`
- place the `ESP32`
- place the `HV5812P`
- place the 3 gauge driver boards
- place the WS2812B strip connection point
- place the VFD tube connection point
## 3. Establish A Common Ground First
Before anything else, create one common logic ground network.
Connect:
- `Mega GND` -> ground rail
- `ESP32 GND` -> same ground rail
- `HV5812P GND` -> same ground rail
- `Gauge driver 0 logic GND` -> same ground rail
- `Gauge driver 1 logic GND` -> same ground rail
- `Gauge driver 2 logic GND` -> same ground rail
- `WS2812B GND` -> same ground rail
If your VFD high-voltage supply has a ground/reference return:
- `VFD HV supply return` -> same common ground rail
Do not continue until this common ground is in place.
## 4. Wire The Arduino Mega Power
Connect:
- regulated `5V logic supply` -> `Mega 5V`
- ground rail -> `Mega GND`
Do not use the Mega to power motors.
## 5. Wire The ESP32 Power
Power the ESP32 in the way your board expects.
Typical options:
- via board USB
- via board `5V/VIN` if your ESP32 board has its own regulator
- via regulated `3.3V` if it is a bare module that requires that
Also connect:
- `ESP32 GND` -> common ground rail
Do not feed raw `5V` into a bare `3.3V-only` ESP32 module.
## 6. Wire The ESP32 UART To The Mega
Connect:
- `ESP32 GPIO17 (TX)` -> `Mega pin 19 (RX1)`
- `ESP32 GPIO16 (RX)` <- `Mega pin 18 (TX1)`
- `ESP32 GND` -> `Mega GND`
This UART link is used by `gauge.py`.
## 7. Wire The HV5812P Logic Side
Connect:
- `Mega D46` -> `HV5812P DATA IN / DIN`
- `Mega D47` -> `HV5812P CLOCK / CLK`
- `Mega D48` -> `HV5812P STROBE / LATCH`
- `Mega D49` -> `HV5812P BLANKING / OE`
- `Mega 5V` -> `HV5812P VDD`
- `Mega GND` -> `HV5812P GND`
Do not connect:
- `Mega 5V` -> `HV5812P VPP`
## 8. Wire The HV5812P High-Voltage Side
Connect:
- `VFD high-voltage positive supply` -> `HV5812P VPP`
- `VFD high-voltage supply return / reference` -> common ground rail
At this stage:
- `VDD` must be `5V`
- `VPP` must be your VFD high-voltage rail
## 9. Wire The HV5812P Outputs To The VFD
Connect these one by one:
- `HVOut1` -> VFD segment `A`
- `HVOut2` -> VFD segment `B`
- `HVOut3` -> VFD segment `C`
- `HVOut4` -> VFD segment `D`
- `HVOut5` -> VFD segment `E`
- `HVOut6` -> VFD segment `F`
- `HVOut7` -> VFD segment `G`
- `HVOut8` -> VFD decimal point segment
- `HVOut9` -> VFD alarm bell segment
- `HVOut10` -> VFD digit 1 grid
- `HVOut11` -> VFD digit 2 grid
- `HVOut12` -> VFD digit 3 grid
- `HVOut13` -> VFD digit 4 grid
- `HVOut14` -> VFD indicator grid between digits 2 and 3
## 10. Wire The VFD Filament
Wire the VFD filament/heater exactly as required by your tube.
This checklist cannot specify the exact filament supply because it depends on the actual tube.
Required reminder:
- do not power the filament from an Arduino GPIO
- use the correct filament supply for the tube
## 11. Wire Gauge Driver 0
Connect:
- `Mega D50` -> `Gauge driver 0 DIR`
- `Mega D51` -> `Gauge driver 0 STEP`
- `Mega GND` -> `Gauge driver 0 logic GND`
Then connect the motor side of that driver to:
- its motor power supply
- its gauge motor
according to the driver board you are using.
## 12. Wire Gauge Driver 1
Connect:
- `Mega D8` -> `Gauge driver 1 DIR`
- `Mega D9` -> `Gauge driver 1 STEP`
- `Mega GND` -> `Gauge driver 1 logic GND`
Then connect the motor side of that driver to:
- its motor power supply
- its gauge motor
according to the driver board you are using.
## 13. Wire Gauge Driver 2
Connect:
- `Mega D52` -> `Gauge driver 2 DIR`
- `Mega D53` -> `Gauge driver 2 STEP`
- `Mega GND` -> `Gauge driver 2 logic GND`
Then connect the motor side of that driver to:
- its motor power supply
- its gauge motor
according to the driver board you are using.
## 14. Wire The WS2812 LEDs
Connect:
- `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:
- do not power it from the Mega `5V`
- use a proper external `5V` supply
## 15. Verify The Pins That Changed For The Integrated VFD
The VFD is no longer on the old standalone pins.
Old standalone pins:
- `D51` -> DATA
- `D52` -> CLOCK
- `D53` -> STROBE
- `D49` -> BLANK
Current integrated pins:
- `D46` -> DATA
- `D47` -> CLOCK
- `D48` -> STROBE
- `D49` -> BLANK
So make sure:
- nothing VFD-related is still on `D51`
- nothing VFD-related is still on `D52`
- nothing VFD-related is still on `D53`
- only `BLANK/OE` remains on `D49`
## 16. Sanity Check Before Powering Logic
Check each item physically:
- `Mega D46` really goes to `HV5812 DATA`
- `Mega D47` really goes to `HV5812 CLOCK`
- `Mega D48` really goes to `HV5812 STROBE`
- `Mega D49` really goes to `HV5812 BLANKING`
- `Mega D50/D51` only go to gauge driver 0
- `Mega D8/D9` only go to gauge driver 1
- `Mega D52/D53` only go to gauge driver 2
- `Mega D22` only goes to WS2812B `DIN`
- `ESP32 GPIO17` goes to `Mega RX1`
- `ESP32 GPIO16` goes to `Mega TX1`
- all grounds are common
- `HV5812 VDD` is `5V`
- `HV5812 VPP` is high voltage, not `5V`
## 17. Power Logic Only First
Apply only logic power first:
- Mega power
- ESP32 power
- HV5812 `VDD`
- WS2812 `5V`
Leave motor supply and VFD high voltage off for the first check if possible.
Verify:
- Mega boots
- ESP32 boots
- UART communication works
## 18. Power The VFD High Voltage
Now apply the VFD high-voltage supply to `HV5812 VPP`.
Verify:
- `VDD` remains `5V`
- `VPP` is the expected high voltage
- no logic wire is heating
## 19. Power The Gauge Drivers
Now apply motor power to the gauge drivers.
Verify:
- no driver fault LEDs
- no motor heating or runaway movement immediately on power-up
## 20. First Functional Test
Test in this order:
1. confirm the ESP32 can talk to the Mega
2. send `VFD 8888`
3. send `VFD DEAD.!`
4. test one gauge movement from Home Assistant or MQTT
5. test one LED output
## 21. If Something Is Wrong
Use this triage order:
1. check grounds
2. check `VDD` and `VPP`
3. check Mega pin number mistakes
4. check crossed UART lines
5. check that the VFD is still on `46/47/48/49`, not `51/52/53/49`
## 22. Quick Reference
### Mega pins in use
- `D8` -> gauge 1 DIR
- `D9` -> gauge 1 STEP
- `D22` -> WS2812 DIN
- `D46` -> HV5812 DATA
- `D47` -> HV5812 CLOCK
- `D48` -> HV5812 STROBE
- `D49` -> HV5812 BLANKING
- `D50` -> gauge 0 DIR
- `D51` -> gauge 0 STEP
- `D52` -> gauge 2 DIR
- `D53` -> gauge 2 STEP
- `D18` -> UART TX1 to ESP32 RX
- `D19` -> UART RX1 from ESP32 TX
### VFD outputs
- `HVOut1..7` -> `A..G`
- `HVOut8` -> decimal point
- `HVOut9` -> alarm bell
- `HVOut10..13` -> digit grids 1..4
- `HVOut14` -> indicator grid

414
Stripboard_layout.md Normal file
View File

@@ -0,0 +1,414 @@
# Stripboard Layout Suggestion
This is a practical suggested layout for moving the current bench wiring onto stripboard. It is not a PCB netlist. It is a placement and routing plan intended to reduce wiring chaos while keeping the high-voltage VFD side separated from the low-voltage logic side.
Use this together with:
- [wiring.md](/home/adebaumann/development/arduino_gauge_controller/wiring.md:1)
- [Rewire_checklist.md](/home/adebaumann/development/arduino_gauge_controller/Rewire_checklist.md:1)
## Design Goals
- keep `5V logic` on one side
- keep `VFD high voltage` on the opposite side
- keep the `HV5812P` between those two domains
- bring all off-board wiring to clearly labeled edge connectors
- avoid crossing the gauge step/dir wiring through the VFD area
- make debugging possible with scope probes and a meter
## Recommended Board Strategy
Use one main stripboard for:
- Arduino Mega interface headers
- HV5812P and its support wiring
- connectors for the VFD tube
- connectors for the three gauge drivers
- connector for the WS2812 strip
- connector for the ESP32 UART link
Do not mount the Arduino Mega itself onto stripboard. Use pin headers or screw terminals so the Mega remains removable.
If possible, also do not mount the ESP32 directly unless you already have a reliable carrier board for it.
## Suggested Physical Zoning
Arrange the board in four zones from left to right:
1. `Mega / ESP32 low-voltage I/O zone`
2. `Gauge / LED connector zone`
3. `HV5812P driver zone`
4. `VFD high-voltage and tube connector zone`
That gives you a left-to-right flow like this:
```text
[ Mega / ESP32 ] [ Gauge + LED connectors ] [ HV5812P ] [ VFD + HV connectors ]
```
This is better than putting the HV5812 at the edge near the Mega, because the HV5812 is the boundary device between logic and high voltage.
## Board Orientation
Assume the stripboard copper tracks run horizontally.
Recommended use:
- horizontal tracks for local distribution
- vertical jumps made with insulated wire links
Cut tracks aggressively around the HV5812P so it does not accidentally join unrelated nets through the copper strips.
## Left Side: Mega / ESP32 Interface
Place a row of labeled pin headers or screw terminals for the signals coming from the Mega:
- `5V`
- `GND`
- `D22`
- `D46`
- `D47`
- `D48`
- `D49`
- `D50`
- `D51`
- `D52`
- `D53`
- `RX1`
- `TX1`
Place a second small header for the ESP32:
- `ESP32 TX`
- `ESP32 RX`
- `ESP32 GND`
Keep these headers near one board edge so you can unplug and rework them easily.
## Middle-Left: Gauge / LED Connectors
Place four connector groups near the Mega interface side:
1. `Gauge 0`
- `DIR`
- `STEP`
- `GND`
2. `Gauge 1`
- `DIR`
- `STEP`
- `GND`
3. `Gauge 2`
- `DIR`
- `STEP`
- `GND`
4. `WS2812`
- `DIN`
- `5V`
- `GND`
This keeps all low-voltage off-board connections together.
## Center: HV5812P Zone
Mount the `HV5812P` roughly in the center-right of the board.
Reason:
- logic-side control pins can approach from the left
- high-voltage outputs can leave to the right toward the VFD connector
Around the HV5812P:
- isolate each used pin with track cuts as needed
- keep short local links for `DATA`, `CLOCK`, `STROBE`, `BLANKING`
- keep `VDD` decoupling physically close to the chip
Recommended support parts close to the HV5812P:
- `100 nF` ceramic decoupling capacitor between `VDD` and `GND`
- one larger bulk capacitor on the `5V` rail nearby, for example `10 uF` to `47 uF`
If you already use any datasheet-recommended support parts for the HV side, place them in this same zone.
## Right Side: VFD Connector Zone
Put the VFD connectors on the far right side of the board, physically separated from the Mega headers.
Provide terminals or headers for:
- `HVOut1` -> `A`
- `HVOut2` -> `B`
- `HVOut3` -> `C`
- `HVOut4` -> `D`
- `HVOut5` -> `E`
- `HVOut6` -> `F`
- `HVOut7` -> `G`
- `HVOut8` -> `DP`
- `HVOut9` -> `BELL`
- `HVOut10` -> `GRID1`
- `HVOut11` -> `GRID2`
- `HVOut12` -> `GRID3`
- `HVOut13` -> `GRID4`
- `HVOut14` -> `GRID_IND`
Also provide separate terminals for:
- `VPP`
- `GND`
- filament connections
Keep the filament connections away from the logic-side headers.
## Suggested Power Buses
Use distinct buses and label them clearly:
- `5V LOGIC`
- `GND`
- `VPP`
Recommended physical arrangement:
- `5V` bus along the top-left area only
- `GND` bus available across the board
- `VPP` bus only on the far-right HV area
Do not run a long exposed `VPP` strip through the entire board. Keep the high-voltage distribution short and local to the HV5812 and VFD connector side.
## Suggested Routing
### Mega to HV5812
Route these as short direct runs:
- `D46` -> `DATA`
- `D47` -> `CLOCK`
- `D48` -> `STROBE`
- `D49` -> `BLANKING`
These should pass from the left interface zone into the HV5812 zone without crossing the VPP area.
### Mega to Gauges
Route these directly to the gauge connector blocks:
- `D50` -> gauge 0 `DIR`
- `D51` -> gauge 0 `STEP`
- `D8` and `D9`
If you are bringing these through the stripboard too, add them to the Mega header group and route them directly to gauge 1.
- `D52` -> gauge 2 `DIR`
- `D53` -> gauge 2 `STEP`
If `D8` and `D9` come from separate fly wires to the stripboard, keep them in the same low-voltage connector area as the rest of the gauge lines.
### Mega to WS2812
Route:
- `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.
### ESP32 to Mega
If the stripboard is acting as the interconnect backplane:
- `ESP32 TX` -> `Mega RX1`
- `ESP32 RX` -> `Mega TX1`
- `ESP32 GND` -> `GND`
## Track Cuts and Links
Recommended stripboard discipline:
- every IC pin should be visually checked for unintended strip continuity
- cut under or near pins wherever two adjacent pins must not share the strip
- use insulated jumpers for crossings instead of relying on long exposed component leads
- use a continuity meter after every 5-10 wires added
For the HV5812P specifically:
- assume most adjacent pins must not be left on the same uninterrupted strip
- cut first, then add intentional links
## Physical Separation Rules
Keep these separations:
- logic and UART wiring away from `VPP`
- VFD output traces away from ESP32 and Mega headers
- gauge step/dir traces away from VFD high-voltage outputs where possible
If you can, leave at least one empty strip gap between low-voltage and high-voltage routing regions, and more where practical.
## Labeling
Label the board directly with marker or printed tape.
At minimum label:
- `5V`
- `GND`
- `VPP`
- `DIN`
- `CLK`
- `STR`
- `BLK`
- `G0 DIR`
- `G0 STEP`
- `G1 DIR`
- `G1 STEP`
- `G2 DIR`
- `G2 STEP`
- `LED DIN`
- `RX1`
- `TX1`
- `A B C D E F G DP BELL G1 G2 G3 G4 GI`
This matters more than aesthetics. A labeled board is much easier to repair later.
## Practical Build Order
1. Place and mark the four physical zones.
2. Mount the low-voltage connector headers.
3. Mount the HV5812P.
4. Cut all required strips around the HV5812P before adding wires.
5. Add `GND` and `5V` low-voltage distribution.
6. Add Mega-to-HV5812 logic lines.
7. Add gauge and LED connector routing.
8. Add `VPP` and the VFD output connector routing.
9. Add the VFD filament connector.
10. Verify continuity and shorts before any power is applied.
## Recommended First Continuity Checks
Before power:
- `5V` is not shorted to `GND`
- `VPP` is not shorted to `GND`
- `VPP` is not shorted to `5V`
- `D46/D47/D48/D49` are only connected to the intended HV5812 pins
- `D50/D51/D52/D53/D8/D9` are only connected to the intended gauge connectors
- `D22` only goes to the WS2812 connector
- `RX1/TX1` are not swapped at the stripboard labels
## Suggested Board Size
For comfort rather than minimum size, use a board large enough to avoid crowding:
- roughly `100 x 160 mm` or larger if you want good service access
Smaller is possible, but with mixed logic, UART, gauge control, LEDs, and VFD high voltage, cramped stripboard becomes harder to debug than a rat's nest.
## Recommendation
If you want the cleanest result:
- use the stripboard only as an interconnect backplane
- keep the Mega, ESP32, and possibly the gauge drivers off-board on removable connectors
- keep the HV5812 and VFD connector area on the stripboard itself
That gives you most of the neatness benefit without forcing the whole system into one dense board.
## ASCII Top View
This is a suggested top-view arrangement, not a strict scale drawing.
```text
Top edge
+--------------------------------------------------------------------------------------------------+
| [Mega Header Block] [Gauge / LED Connectors] [HV5812P Zone] [VFD Zone] |
| |
| 5V GND D22 D46 D47 D48 D49 D50 D51 D52 D53 RX1 TX1 |
| o o o o o o o o o o o o o |
| |
| [ESP32 Header] |
| TX RX GND |
| o o o |
| |
| [Gauge 0] [Gauge 1] [Gauge 2] [WS2812] |
| DIR STEP G DIR STEP G DIR STEP G DIN 5V G |
| o o o o o o o o o o o o |
| |
| +----------------------+ |
| | HV5812P | |
| | | |
| 5V LOGIC BUS ============================================>| VDD | |
| GND BUS ============================================>| GND |==============|
| | DIN CLK STR BLK | |
| | ^ ^ ^ ^ | |
| +--|----|---|---|------+ |
| | | | | |
| | | | +---- D49 |
| | | +-------- D48 |
| | +------------ D47 |
| +----------------- D46 |
| |
| VPP terminal |
| o |
| | |
| HV AREA | |
| kept to right side only | |
| v |
| A B C D E F G DP BELL G1 G2 G3 G4 GI
| o o o o o o o o o o o o o o
| [VFD output connector block] |
| |
| FIL_A FIL_B GND/HVRET |
| o o o |
| [filament / HV return terminals] |
| |
+--------------------------------------------------------------------------------------------------+
Bottom edge
```
## Reading The Sketch
- left side:
all low-voltage headers from the Mega and ESP32
- center-left:
gauge and LED connector blocks
- center-right:
HV5812P
- far right:
VFD outputs, filament, and `VPP`
This keeps the dangerous and noisy wiring concentrated on one side of the board.
## Suggested Copper-Strip Use
If your strips run horizontally:
- use upper strips for low-voltage headers and distribution
- use middle strips for gauge and LED routing
- isolate the HV5812P pin rows heavily with track cuts
- use lower-right strips only for the VFD output area and `VPP`
## Suggested Connector Edge Placement
If you want the board to be easy to service:
- put Mega and ESP32 headers on the left edge
- put gauge and LED connectors on the bottom edge
- put VFD and high-voltage terminals on the right edge
That way:
- low-voltage control cables enter from the left and bottom
- high-voltage VFD wires leave only on the right
## Minimum Clearance Advice
On stripboard, do not pack the `VPP` and VFD output terminals tightly against the low-voltage headers.
Practical suggestion:
- leave at least several empty holes / one empty strip region between the HV5812 logic-side routing and the `VPP` / VFD connector zone
- if you have room, leave more than that
More separation is better than a dense layout here.

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
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -293,6 +303,34 @@ _status_red_effect = [None] * num_gauges
_status_green_effect= [None] * num_gauges _status_green_effect= [None] * num_gauges
_bl_effect = [None] * num_gauges _bl_effect = [None] * num_gauges
vfd_text = ""
vfd_decimal_point = False
vfd_alarm = False
def _build_vfd_command():
suffix = ""
if vfd_decimal_point:
suffix += "."
if vfd_alarm:
suffix += "!"
if vfd_text:
return f"VFD {vfd_text}{suffix}"
if suffix:
return f"VFD 0{suffix}"
return "VFD"
def send_vfd_state():
arduino_send(_build_vfd_command())
def publish_vfd_state(client):
client.publish(vfd_topics["state"], vfd_text, retain=True)
client.publish(vfd_topics["decimal_point_state"], b"ON" if vfd_decimal_point else b"OFF", retain=True)
client.publish(vfd_topics["alarm_state"], b"ON" if vfd_alarm else b"OFF", retain=True)
def _backlight_changed(gauge_idx, new_color, new_on, new_brightness): def _backlight_changed(gauge_idx, new_color, new_on, new_brightness):
return ( return (
@@ -356,6 +394,53 @@ def publish_backlight_states(client):
log_err(f"Backlight state publish failed for gauge {i}: {e}") log_err(f"Backlight state publish failed for gauge {i}: {e}")
def restore_backlight_state(gauge_idx, payload):
"""Restore retained backlight state without republishing it back to MQTT."""
global backlight_color, backlight_brightness, backlight_on, _bl_effect
try:
data = ujson.loads(payload)
except Exception as e:
warn(f"Invalid retained backlight state for gauge {gauge_idx}: '{payload}' ({e})")
return
state_on = data.get("state", "OFF").upper() != "OFF"
effect = data.get("effect")
if effect not in _EFFECTS:
effect = None
if not state_on:
_bl_effect[gauge_idx] = None
backlight_on[gauge_idx] = False
set_backlight_brightness(gauge_idx, 0)
return
color = data.get("color", {})
r = max(0, min(255, int(color.get("r", backlight_color[gauge_idx][0]))))
g = max(0, min(255, int(color.get("g", backlight_color[gauge_idx][1]))))
b = max(0, min(255, int(color.get("b", backlight_color[gauge_idx][2]))))
raw_br = data.get("brightness", None)
if raw_br is not None:
brightness = max(0, min(100, round(int(raw_br) / 2.55)))
elif backlight_brightness[gauge_idx] > 0:
brightness = backlight_brightness[gauge_idx]
else:
brightness = 100
_bl_effect[gauge_idx] = effect
if effect:
scale = brightness / 100
rs = int(r * scale)
gs = int(g * scale)
bs_ = int(b * scale)
_send_effect(gauge_idx, _LED_BACKLIGHT_RANGE, (rs, gs, bs_), effect)
backlight_color[gauge_idx] = (r, g, b)
backlight_brightness[gauge_idx] = brightness
backlight_on[gauge_idx] = True
else:
set_backlight_color(gauge_idx, r, g, b, brightness)
def _flush_backlight_state(): def _flush_backlight_state():
global _bl_dirty_since global _bl_dirty_since
if _bl_dirty_since is None: if _bl_dirty_since is None:
@@ -419,6 +504,18 @@ def make_gauge_topics(prefix, gauge_id):
gauge_topics = [make_gauge_topics(MQTT_PREFIX, g["id"]) for g in gauges] gauge_topics = [make_gauge_topics(MQTT_PREFIX, g["id"]) for g in gauges]
vfd_topics = {
"set": f"{MQTT_PREFIX}/vfd/set",
"state": f"{MQTT_PREFIX}/vfd/state",
"disc": f"homeassistant/text/{MQTT_CLIENT_ID}_vfd/config",
"decimal_point": f"{MQTT_PREFIX}/vfd/decimal_point/set",
"decimal_point_state": f"{MQTT_PREFIX}/vfd/decimal_point/state",
"decimal_point_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_vfd_decimal_point/config",
"alarm": f"{MQTT_PREFIX}/vfd/alarm/set",
"alarm_state": f"{MQTT_PREFIX}/vfd/alarm/state",
"alarm_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_vfd_alarm/config",
}
T_SET = f"{MQTT_PREFIX}/set" T_SET = f"{MQTT_PREFIX}/set"
T_ZERO = f"{MQTT_PREFIX}/zero" T_ZERO = f"{MQTT_PREFIX}/zero"
@@ -437,32 +534,56 @@ _DEVICE = {
_wifi_check_interval_ms = 30000 _wifi_check_interval_ms = 30000
_last_wifi_check = 0 _last_wifi_check = 0
_wifi_sta = None _wifi_sta = None
_WIFI_CONNECT_ATTEMPTS = 3
def connect_wifi(ssid, password, timeout_s=15): 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 _wifi_sta.active():
_wifi_sta.active(False)
utime.sleep_ms(200)
_wifi_sta.active(True) _wifi_sta.active(True)
if _wifi_sta.isconnected(): utime.sleep_ms(500)
def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False):
global _wifi_sta
_wifi_sta = network.WLAN(network.STA_IF)
if _wifi_sta.isconnected() and not force_reconnect:
ip, mask, gw, dns = _wifi_sta.ifconfig() ip, mask, gw, dns = _wifi_sta.ifconfig()
info("WiFi already connected") info("WiFi already connected")
info(f" IP:{ip} mask:{mask} gw:{gw} dns:{dns}") info(f" IP:{ip} mask:{mask} gw:{gw} dns:{dns}")
utime.sleep_ms(250)
return ip return ip
info(f"WiFi connecting to '{ssid}' ...")
last_error = None
for attempt in range(_WIFI_CONNECT_ATTEMPTS):
info(f"WiFi connecting to '{ssid}' (attempt {attempt + 1}/{_WIFI_CONNECT_ATTEMPTS}) ...")
_reset_wifi_interface()
try:
_wifi_sta.connect(ssid, password) _wifi_sta.connect(ssid, password)
deadline = utime.time() + timeout_s deadline = utime.time() + timeout_s
while not _wifi_sta.isconnected(): while not _wifi_sta.isconnected():
if utime.time() > deadline: if utime.time() > deadline:
log_err(f"WiFi connect timeout after {timeout_s}s")
raise OSError("WiFi connect timeout") raise OSError("WiFi connect timeout")
utime.sleep_ms(200) utime.sleep_ms(250)
ip, mask, gw, dns = _wifi_sta.ifconfig() ip, mask, gw, dns = _wifi_sta.ifconfig()
mac = ":".join(f"{b:02x}" for b in _wifi_sta.config("mac")) mac = ":".join(f"{b:02x}" for b in _wifi_sta.config("mac"))
info("WiFi connected!") info("WiFi connected!")
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(500)
return ip return ip
except Exception as e:
last_error = e
log_err(f"WiFi connect attempt {attempt + 1} failed: {e}")
utime.sleep_ms(1000)
raise last_error
def check_wifi(): def check_wifi():
@@ -480,15 +601,7 @@ def check_wifi():
log_err("WiFi lost connection — attempting reconnect...") log_err("WiFi lost connection — attempting reconnect...")
try: try:
_wifi_sta.active(True) ip = connect_wifi(WIFI_SSID, WIFI_PASSWORD, timeout_s=15, force_reconnect=True)
_wifi_sta.connect(WIFI_SSID, WIFI_PASSWORD)
deadline = utime.time() + 15
while not _wifi_sta.isconnected():
if utime.time() > deadline:
log_err("WiFi reconnect timeout")
return
utime.sleep_ms(200)
ip, mask, gw, dns = _wifi_sta.ifconfig()
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}")
@@ -500,13 +613,41 @@ def check_wifi():
def on_message(topic, payload): def on_message(topic, payload):
global vfd_text, vfd_decimal_point, vfd_alarm
if client_ref is None: if client_ref is None:
return return
topic = topic.decode() topic = topic.decode()
payload = payload.decode().strip() payload = payload.decode().strip()
info(f"MQTT rx {topic} {payload}") info(f"MQTT rx {topic} {payload}")
if topic == vfd_topics["set"]:
vfd_text = payload.upper()
send_vfd_state()
publish_vfd_state(client_ref)
info(f"VFD text -> {vfd_text}")
return
if topic == vfd_topics["decimal_point"]:
vfd_decimal_point = payload.upper() == "ON"
send_vfd_state()
publish_vfd_state(client_ref)
info(f"VFD decimal point -> {'ON' if vfd_decimal_point else 'OFF'}")
return
if topic == vfd_topics["alarm"]:
vfd_alarm = payload.upper() == "ON"
send_vfd_state()
publish_vfd_state(client_ref)
info(f"VFD alarm -> {'ON' if vfd_alarm else 'OFF'}")
return
for i, gt in enumerate(gauge_topics): for i, gt in enumerate(gauge_topics):
if topic == gt["led_bl_state"]:
restore_backlight_state(i, payload)
info(f"Gauge {i} backlight state restored")
return
if topic == gt["zero"]: if topic == gt["zero"]:
info(f"Home command received for gauge {i}") info(f"Home command received for gauge {i}")
gauge_home(i) gauge_home(i)
@@ -723,6 +864,9 @@ def on_message(topic, payload):
def _subscribe_all(c): def _subscribe_all(c):
c.subscribe(f"{MQTT_PREFIX}/set") c.subscribe(f"{MQTT_PREFIX}/set")
c.subscribe(f"{MQTT_PREFIX}/zero") c.subscribe(f"{MQTT_PREFIX}/zero")
c.subscribe(vfd_topics["set"])
c.subscribe(vfd_topics["decimal_point"])
c.subscribe(vfd_topics["alarm"])
for i in range(num_gauges): for i in range(num_gauges):
prefix = f"{MQTT_PREFIX}/gauge{i}" prefix = f"{MQTT_PREFIX}/gauge{i}"
c.subscribe(f"{prefix}/set") c.subscribe(f"{prefix}/set")
@@ -732,6 +876,7 @@ def _subscribe_all(c):
c.subscribe(f"{prefix}/led/red/set") c.subscribe(f"{prefix}/led/red/set")
c.subscribe(f"{prefix}/led/green/set") c.subscribe(f"{prefix}/led/green/set")
c.subscribe(f"{prefix}/led/backlight/set") c.subscribe(f"{prefix}/led/backlight/set")
c.subscribe(f"{prefix}/led/backlight/state")
c.subscribe(f"{prefix}/status_led/red/set") c.subscribe(f"{prefix}/status_led/red/set")
c.subscribe(f"{prefix}/status_led/green/set") c.subscribe(f"{prefix}/status_led/green/set")
@@ -739,6 +884,16 @@ def _subscribe_all(c):
def connect_mqtt(): def connect_mqtt():
global client_ref, _mqtt_connected global client_ref, _mqtt_connected
info(f"Connecting to MQTT broker {MQTT_BROKER}:{MQTT_PORT} ...") info(f"Connecting to MQTT broker {MQTT_BROKER}:{MQTT_PORT} ...")
last_error = None
for attempt in range(3):
gc.collect()
try:
if client_ref is not None:
try:
client_ref.disconnect()
except Exception:
pass
client = MQTTClient( client = MQTTClient(
client_id=MQTT_CLIENT_ID, client_id=MQTT_CLIENT_ID,
server=MQTT_BROKER, server=MQTT_BROKER,
@@ -752,6 +907,19 @@ def connect_mqtt():
client_ref = client client_ref = client
_mqtt_connected = True _mqtt_connected = True
info(f"MQTT connected client_id={MQTT_CLIENT_ID}") info(f"MQTT connected client_id={MQTT_CLIENT_ID}")
return
except Exception as e:
last_error = e
log_err(f"MQTT connect attempt {attempt + 1} failed: {type(e).__name__}: {e}")
try:
client.sock.close()
except Exception:
pass
gc.collect()
utime.sleep_ms(1000)
_mqtt_connected = False
raise last_error
_mqtt_check_interval_ms = 30000 _mqtt_check_interval_ms = 30000
@@ -759,7 +927,20 @@ _last_mqtt_check = 0
_discovery_queue = [] _discovery_queue = []
_discovery_idx = 0 _discovery_idx = 0
_last_discovery_ms = 0 _last_discovery_ms = 0
_DISCOVERY_INTERVAL_MS = 200 _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():
@@ -803,6 +984,11 @@ 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()
utime.sleep_ms(2000) utime.sleep_ms(2000)
log_err("MQTT reconnection failed after 3 attempts") log_err("MQTT reconnection failed after 3 attempts")
@@ -810,7 +996,8 @@ def check_mqtt():
def _publish_discovery_entity(client, topic, payload, log_msg): def _publish_discovery_entity(client, topic, payload, log_msg):
client.publish(topic, ujson.dumps(payload), retain=True) gc.collect()
client.publish(topic, ujson.dumps(_compact_discovery_payload(payload)), retain=True)
info(log_msg) info(log_msg)
@@ -995,6 +1182,58 @@ def _append_backlight_status_discovery(entries, dev_ref):
) )
def _append_vfd_discovery(entries, dev_ref):
entries.append(
(
vfd_topics["disc"],
{
"name": "VFD Display",
"unique_id": f"{MQTT_CLIENT_ID}_vfd",
"cmd_t": vfd_topics["set"],
"stat_t": vfd_topics["state"],
"avty_t": gauge_topics[0]["status"],
"icon": "mdi:alpha-box",
"dev": dev_ref,
},
"Discovery: VFD text",
)
)
entries.append(
(
vfd_topics["decimal_point_disc"],
{
"name": "VFD Decimal Point",
"unique_id": f"{MQTT_CLIENT_ID}_vfd_decimal_point",
"cmd_t": vfd_topics["decimal_point"],
"stat_t": vfd_topics["decimal_point_state"],
"avty_t": gauge_topics[0]["status"],
"pl_on": "ON",
"pl_off": "OFF",
"icon": "mdi:circle-small",
"dev": dev_ref,
},
"Discovery: VFD decimal point",
)
)
entries.append(
(
vfd_topics["alarm_disc"],
{
"name": "VFD Alarm",
"unique_id": f"{MQTT_CLIENT_ID}_vfd_alarm",
"cmd_t": vfd_topics["alarm"],
"stat_t": vfd_topics["alarm_state"],
"avty_t": gauge_topics[0]["status"],
"pl_on": "ON",
"pl_off": "OFF",
"icon": "mdi:alarm-bell",
"dev": dev_ref,
},
"Discovery: VFD alarm",
)
)
def schedule_discovery(): def schedule_discovery():
global _discovery_queue, _discovery_idx, _last_discovery_ms global _discovery_queue, _discovery_idx, _last_discovery_ms
_dev_ref = _DEVICE _dev_ref = _DEVICE
@@ -1005,6 +1244,7 @@ def schedule_discovery():
_append_acceleration_discovery(entries, _dev_ref) _append_acceleration_discovery(entries, _dev_ref)
_append_indicator_led_discovery(entries, _dev_ref) _append_indicator_led_discovery(entries, _dev_ref)
_append_backlight_status_discovery(entries, _dev_ref) _append_backlight_status_discovery(entries, _dev_ref)
_append_vfd_discovery(entries, _dev_ref)
_discovery_queue = entries _discovery_queue = entries
_discovery_idx = 0 _discovery_idx = 0
_last_discovery_ms = 0 _last_discovery_ms = 0
@@ -1030,14 +1270,17 @@ def service_discovery():
if _last_discovery_ms and utime.ticks_diff(now, _last_discovery_ms) < _DISCOVERY_INTERVAL_MS: if _last_discovery_ms and utime.ticks_diff(now, _last_discovery_ms) < _DISCOVERY_INTERVAL_MS:
return return
gc.collect()
topic, payload, log_msg = _discovery_queue[_discovery_idx] topic, payload, log_msg = _discovery_queue[_discovery_idx]
try:
if isinstance(payload, bytes): if isinstance(payload, bytes):
client_ref.publish(topic, payload, retain=True) client_ref.publish(topic, payload, retain=True)
else: else:
_publish_discovery_entity(client_ref, topic, payload, log_msg) _publish_discovery_entity(client_ref, topic, payload, log_msg)
except Exception as e:
log_err(f"Discovery publish failed for {topic}: {e}")
_discovery_idx += 1 _discovery_idx += 1
_last_discovery_ms = utime.ticks_ms() _last_discovery_ms = utime.ticks_ms()
if (_discovery_idx & 3) == 0:
gc.collect() gc.collect()
@@ -1052,12 +1295,14 @@ def publish_state(client):
client.publish(gt["state"], str(gauge_targets[i])) client.publish(gt["state"], str(gauge_targets[i]))
client.publish(gt["speed_state"], str(gauge_speeds[i]), retain=True) client.publish(gt["speed_state"], str(gauge_speeds[i]), retain=True)
client.publish(gt["acceleration_state"], str(gauge_accelerations[i]), retain=True) client.publish(gt["acceleration_state"], str(gauge_accelerations[i]), retain=True)
publish_vfd_state(client)
def apply_motion_defaults(): def apply_motion_defaults():
for i in range(num_gauges): for i in range(num_gauges):
gauge_set_speed(i, gauge_speeds[i]) gauge_set_speed(i, gauge_speeds[i])
gauge_set_acceleration(i, gauge_accelerations[i]) gauge_set_acceleration(i, gauge_accelerations[i])
send_vfd_state()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1066,13 +1311,30 @@ def apply_motion_defaults():
def main(): def main():
gc.collect()
info("=" * 48) info("=" * 48)
info("Gauge MQTT controller starting") info("Gauge MQTT controller starting")
info(f"Heap free: {gc.mem_free()} bytes")
info("=" * 48) info("=" * 48)
connect_wifi(WIFI_SSID, WIFI_PASSWORD) gc.collect()
connect_wifi(WIFI_SSID, WIFI_PASSWORD, force_reconnect=True)
mqtt_attempts = 0
while True:
try:
connect_mqtt() connect_mqtt()
break
except Exception as e:
mqtt_attempts += 1
log_err(f"MQTT connect failed: {e} (attempt {mqtt_attempts})")
if mqtt_attempts % 3 == 0:
log_err("WiFi may be stale — forcing reconnect...")
try:
connect_wifi(WIFI_SSID, WIFI_PASSWORD, force_reconnect=True)
except Exception as we:
log_err(f"WiFi reconnect failed: {we}")
utime.sleep_ms(5000)
_subscribe_all(client_ref) _subscribe_all(client_ref)
schedule_discovery() schedule_discovery()

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

278
wiring.md Normal file
View File

@@ -0,0 +1,278 @@
# Wiring
This document describes the wiring required for the current integrated system:
- `Arduino Mega 2560`
- `HV5812P` VFD driver
- 4-digit VFD tube with decimal point and alarm bell
- 3 stepper-driven gauges
- WS2812B LEDs
- ESP32 running `gauge.py` as the MQTT / Home Assistant bridge
It is intentionally based on the code that is in the repository now:
- [Gaugecontroller.ino](/home/adebaumann/development/arduino_gauge_controller/Gaugecontroller/Gaugecontroller.ino:1)
- [gauge.py](/home/adebaumann/development/arduino_gauge_controller/gauge.py:1)
## System Power
You effectively have three power domains:
1. `5V logic`
for the Arduino Mega logic, the HV5812 logic side, and usually the step/dir logic inputs
2. `high voltage for the VFD`
for `HV5812P VPP` and the VFD segment/grid drive
3. `motor / actuator power`
for the stepper gauges and their driver hardware
Minimum common rule:
- all logic grounds must be common
That means these must share `GND`:
- Arduino Mega `GND`
- ESP32 `GND`
- HV5812P logic `GND`
- stepper driver logic `GND`
- WS2812B `GND`
The VFD high-voltage supply still references the same ground, but its high-voltage nodes must never be connected directly to Arduino or ESP32 GPIO pins.
## Arduino Mega 2560
Use the Mega as the central logic controller.
Power:
- `Mega 5V` <- regulated `5V` logic supply
- `Mega GND` <- common logic ground
Serial bridge to ESP32:
- `Mega RX1` pin `19` <- `ESP32 TX` GPIO `17`
- `Mega TX1` pin `18` -> voltage divider -> `ESP32 RX` GPIO `16`
- `Mega GND` <-> `ESP32 GND`
This matches the current code:
- Arduino uses `Serial1` in [Gaugecontroller.ino](/home/adebaumann/development/arduino_gauge_controller/Gaugecontroller/Gaugecontroller.ino:12)
- ESP32 uses `UART(1, tx=17, rx=16)` in [gauge.py](/home/adebaumann/development/arduino_gauge_controller/gauge.py:149)
Because the Mega transmits `5V` logic and the ESP32 expects `3.3V` logic on RX, add a resistor divider on the `Mega TX1 -> ESP32 RX` line.
Suggested simple divider:
- `Mega TX1` -> `1 kOhm` resistor -> divider node
- divider node -> `2 kOhm` resistor -> `GND`
- divider node -> `ESP32 GPIO16 (RX)`
That scales the Mega's `5V` TX signal to roughly `3.3V` for the ESP32 RX input.
## VFD Control: Mega -> HV5812P
These are the integrated pin assignments used by the merged controller:
| Mega Pin | HV5812P Signal | Purpose |
|---|---|---|
| `D46` | `DATA IN / DIN` | serial data into HV5812P |
| `D47` | `CLOCK / CLK` | shift clock |
| `D48` | `STROBE / LATCH` | latch transfer |
| `D49` | `BLANKING / OE` | output blanking |
| `5V` | `VDD` | HV5812 logic supply |
| `GND` | `GND` | common reference |
| `VFD HV+` | `VPP` | HV5812 high-voltage rail |
Important:
- `VDD` is the low-voltage logic rail
- `VPP` is the high-voltage output rail
- do not connect Arduino `5V` to `VPP`
## HV5812P -> VFD Tube
The current output map is:
| HV5812 Output | Tube Function |
|---|---|
| `HVOut1` | segment `A` |
| `HVOut2` | segment `B` |
| `HVOut3` | segment `C` |
| `HVOut4` | segment `D` |
| `HVOut5` | segment `E` |
| `HVOut6` | segment `F` |
| `HVOut7` | segment `G` |
| `HVOut8` | decimal point segment |
| `HVOut9` | alarm bell segment |
| `HVOut10` | digit grid 1 |
| `HVOut11` | digit grid 2 |
| `HVOut12` | digit grid 3 |
| `HVOut13` | digit grid 4 |
| `HVOut14` | indicator grid between digits 2 and 3 |
Logical segment layout:
```text
---A---
| |
F B
|---G---|
E C
| |
---D---
```
Additional VFD wiring notes:
- the VFD filament/heater wiring is separate from the HV5812 outputs
- the exact filament supply depends on your tube
- the HV5812 only drives the segments and grids
## Gauge Control Pins
The current sketch drives three gauges.
Each gauge needs a driver or actuator input that accepts:
- `DIR`
- `STEP`
- optionally `ENABLE` if you later add one in code
Current assignments:
| Gauge | Mega DIR | Mega STEP |
|---|---|---|
| `Gauge 0` | `D50` | `D51` |
| `Gauge 1` | `D8` | `D9` |
| `Gauge 2` | `D52` | `D53` |
Connect each pair to the matching stepper driver inputs.
Example:
- `Mega D50` -> Gauge 0 driver `DIR`
- `Mega D51` -> Gauge 0 driver `STEP`
- `Mega D8` -> Gauge 1 driver `DIR`
- `Mega D9` -> Gauge 1 driver `STEP`
- `Mega D52` -> Gauge 2 driver `DIR`
- `Mega D53` -> Gauge 2 driver `STEP`
Also connect:
- `Mega GND` -> each driver logic ground
If your driver boards need separate motor power, supply that from the proper motor supply. Do not power motors from the Mega `5V` pin.
## WS2812 LED Strips
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 | LED Strip |
|---|---|
| `D22` | main backlight/status `DIN` |
| `D36` | indicator `DIN` |
| `5V` | both strips `5V` |
| `GND` | both strips `GND` |
Notes:
- 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
If the strip is powered from a separate 5V supply:
- connect external `5V` -> LED `5V`
- connect external `GND` -> LED `GND`
- connect that same `GND` to `Mega GND`
## ESP32 Bridge
The ESP32 runs `gauge.py` and talks to the Mega over UART and to Home Assistant over MQTT/Wi-Fi.
ESP32 to Mega:
| ESP32 Pin | Mega Pin | Purpose |
|---|---|---|
| `GPIO17` | `RX1` pin `19` | ESP32 TX -> Mega RX |
| `GPIO16` | `TX1` pin `18` | ESP32 RX <- Mega TX |
| `GND` | `GND` | common ground |
ESP32 power:
- power the ESP32 from a proper `3.3V` or board-supported USB/5V input, depending on your board
- do not feed raw `5V` into a bare `3.3V` ESP32 module unless the board has its own regulator
## One-Page Wiring Summary
### Power
- `5V logic supply` -> Mega `5V`
- `5V logic supply` -> HV5812 `VDD`
- `5V logic supply` -> WS2812B `5V`
- `motor supply` -> gauge driver motor power inputs
- `VFD high-voltage supply` -> HV5812 `VPP`
- all grounds common
### Mega to ESP32
- `Mega 19 (RX1)` <- `ESP32 GPIO17 (TX)`
- `Mega 18 (TX1)` -> resistor divider -> `ESP32 GPIO16 (RX)`
- `Mega GND` <-> `ESP32 GND`
Resistor divider on `Mega TX1`:
- `Mega TX1` -> `1 kOhm` -> divider node
- divider node -> `ESP32 GPIO16`
- divider node -> `2 kOhm` -> `GND`
### Mega to HV5812
- `D46` -> `DIN`
- `D47` -> `CLK`
- `D48` -> `STROBE`
- `D49` -> `BLANKING`
- `5V` -> `VDD`
- `GND` -> `GND`
- `VFD HV+` -> `VPP`
### HV5812 to Tube
- `HVOut1..7` -> segments `A..G`
- `HVOut8` -> decimal point
- `HVOut9` -> alarm bell
- `HVOut10..13` -> digit grids `1..4`
- `HVOut14` -> indicator grid
### Mega to Gauges
- `D50/D51` -> gauge 0 `DIR/STEP`
- `D8/D9` -> gauge 1 `DIR/STEP`
- `D52/D53` -> gauge 2 `DIR/STEP`
### Mega to LEDs
- `D22` -> WS2812B `DIN`
- `5V` -> WS2812B `5V`
- `GND` -> WS2812B `GND`
## Sanity Checklist Before Power-On
- Mega, ESP32, HV5812 logic, LED strip, and driver logic grounds are all common
- Mega `D46-D49` go to the HV5812, not to the gauge drivers
- Mega `D50-D53` and `D8-D9` go only to the gauge drivers
- HV5812 `VDD` is `5V`
- HV5812 `VPP` is the VFD high-voltage rail, not `5V`
- ESP32 UART is crossed correctly: TX -> RX, RX -> TX
- WS2812B has its own adequate 5V supply if current draw is significant
- motor power is not coming from the Mega
## What This Does Not Define
This document does not define:
- the exact VFD filament supply voltage/current
- the exact motor driver board power pins, because that depends on the driver hardware you are using
- the physical PDIP package pin numbers of the HV5812P