2 Commits

19 changed files with 762 additions and 7714 deletions

1
.gitignore vendored
View File

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

View File

@@ -6,8 +6,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
Main firmware lives in `Gaugecontroller/Gaugecontroller.ino`. Requires the **FastLED** library (`arduino-cli lib install FastLED`). Use the Arduino IDE or `arduino-cli`: Main firmware lives in `Gaugecontroller/Gaugecontroller.ino`. Requires the **FastLED** library (`arduino-cli lib install FastLED`). Use the Arduino IDE or `arduino-cli`:
The ESP32 bridge runs ESPHome; the config is in `gaugecontroller.yaml`.
```bash ```bash
# Compile (replace board/port as needed) # Compile (replace board/port as needed)
arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller
@@ -60,7 +58,7 @@ 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, `ledOrder` string). Declared `constexpr` so `TOTAL_LEDS` can be computed from it at compile time. Configured in the `gaugePins[]` array at the top. - `GaugePins` — hardware pin mapping per gauge (dir, step, enable, active-high/low polarity flags, `ledCount`). Declared `constexpr` so `TOTAL_LEDS` can be computed from it at compile time. Configured in the `gaugePins[]` array at the top.
- `Gauge` — per-gauge runtime state: position, target, velocity, accel, homing state machine, sweep mode. - `Gauge` — per-gauge runtime state: position, target, velocity, accel, homing state machine, sweep mode.
### Motion control (`updateGauge`) ### Motion control (`updateGauge`)
@@ -78,7 +76,7 @@ When `sweepEnabled`, `updateSweepTarget` bounces `targetPos` between `minPos` an
### LED strip ### LED strip
Two LED strips are driven: main backlight/status LEDs on `LED_DATA_PIN` (currently 22) and dial indicator LEDs on `INDICATOR_LED_DATA_PIN` (currently 36). The serial protocol still exposes one logical per-gauge LED segment: `0-2` backlight, `3-4` indicators, `5-6` status. `gaugePins[i].ledOrder` is a per-LED type string (one char per LED, `'G'` = GRB-ordered, `'R'` = RGB-ordered) and its length defines the logical LED count. `TOTAL_LEDS`, `TOTAL_MAIN_LEDS`, and `TOTAL_INDICATOR_LEDS` are computed at compile time. Per-gauge logical and physical offsets are cached in `setup()`. LED writes dirty only their physical strip, and the loop flushes each FastLED controller independently with `showLeds()`. 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
@@ -106,6 +104,6 @@ All commands reply `OK` or `ERR BAD_ID` / `ERR BAD_CMD` etc.
### Adding gauges ### Adding gauges
1. Increment `GAUGE_COUNT`. 1. Increment `GAUGE_COUNT`.
2. Add a `constexpr GaugePins` entry to `gaugePins[]` (including the `ledOrder` string — one char per LED, `'G'` for GRB or `'R'` for RGB). 2. Add a `constexpr GaugePins` entry to `gaugePins[]` (including `ledCount`).
3. Tune `maxPos` and `homingBackoffSteps` in the corresponding `Gauge` default or at runtime. 3. Tune `maxPos` and `homingBackoffSteps` in the corresponding `Gauge` default or at runtime.
4. `TOTAL_LEDS`, `gaugeLedOffset[]`, and `gaugeLedCount[]` update automatically — no manual changes needed. 4. `TOTAL_LEDS` and `gaugeLedOffset[]` update automatically — no manual changes needed.

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

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
- `gaugecontroller.yaml`: the ESPHome-based ESP32 firmware that exposes the gauges and VFD to Home Assistant via the native API - `gauge.py`: the ESP32 / MicroPython MQTT bridge that exposes the controller to Home Assistant
## VFD Support ## VFD Support
@@ -48,19 +48,16 @@ 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 Integration ## Home Assistant Entities
The ESPHome firmware in `gaugecontroller.yaml` exposes entities to Home Assistant via the native API: The MQTT bridge publishes Home Assistant discovery entities for the VFD:
### Gauge Controls - `VFD Display`
- Number entities for each gauge's target value (with unit conversion) text entity for the displayed value
- Number entities for speed and acceleration (diagnostic) - `VFD Decimal Point`
- Rezero buttons for each gauge and all gauges switch entity
- `VFD Alarm`
### VFD Display switch entity
- `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:
@@ -68,12 +65,27 @@ 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
### LED Controls ## MQTT Topics
- 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 Using the configured `mqtt_prefix` from `config.json`, the VFD topics are:
- WiFi signal sensor
- Uptime sensor - `<prefix>/vfd/set`
- IP address and SSID text sensors - `<prefix>/vfd/state`
- Arduino Last Message sensor - `<prefix>/vfd/decimal_point/set`
- `<prefix>/vfd/decimal_point/state`
- `<prefix>/vfd/alarm/set`
- `<prefix>/vfd/alarm/state`
Example with the default prefix `gauges`:
- `gauges/vfd/set`
- `gauges/vfd/decimal_point/set`
- `gauges/vfd/alarm/set`
Example payloads:
- publish `0123` to `gauges/vfd/set`
- publish `ON` to `gauges/vfd/decimal_point/set`
- publish `OFF` to `gauges/vfd/alarm/set`
The MQTT bridge then converts that into the correct Arduino serial command such as `VFD 0123.`.

View File

@@ -183,14 +183,13 @@ 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 WS2812 LEDs ## 14. Wire The WS2812B LEDs
Connect: Connect:
- `Mega D22` -> main backlight/status strip `DIN` - `Mega D22` -> `WS2812B DIN`
- `Mega D36` -> indicator strip `DIN` - `5V LED supply` -> `WS2812B 5V`
- `5V LED supply` -> both strip `5V` inputs - `WS2812B GND` -> common ground rail
- 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,10 +205,9 @@ If `D8` and `D9` come from separate fly wires to the stripboard, keep them in th
Route: Route:
- `D22` -> main backlight/status strip `DIN` - `D22` -> `WS2812 DIN`
- `D36` -> indicator strip `DIN` - `5V` -> `WS2812 5V`
- `5V` -> both strip `5V` inputs - `GND` -> `WS2812 GND`
- `GND` -> both strip `GND` inputs
Keep the LED connector in the low-voltage area. Keep the LED connector in the low-voltage area.

64
boot.py Normal file
View File

@@ -0,0 +1,64 @@
"""
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

View File

@@ -1,46 +0,0 @@
# 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

@@ -930,19 +930,6 @@ _last_discovery_ms = 0
_DISCOVERY_INTERVAL_MS = 350 _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()
@@ -997,7 +984,7 @@ def check_mqtt():
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(_compact_discovery_payload(payload)), retain=True) client.publish(topic, ujson.dumps(payload), retain=True)
info(log_msg) info(log_msg)

File diff suppressed because it is too large Load Diff

View File

@@ -163,22 +163,19 @@ 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.
## WS2812 LED Strips ## WS2812B LED Strip
The current sketch expects two LED data chains. Backlight and status LEDs stay The current sketch expects one shared WS2812B chain.
on the main strip; the red/green dial indicator LEDs are on their own strip.
| Mega Pin | LED Strip | | Mega Pin | WS2812B |
|---|---| |---|---|
| `D22` | main backlight/status `DIN` | | `D22` | `DIN` |
| `D36` | indicator `DIN` | | `5V` | `5V` |
| `5V` | both strips `5V` | | `GND` | `GND` |
| `GND` | both strips `GND` |
Notes: Notes:
- the command protocol still exposes `7 LEDs per gauge` - the code expects `7 LEDs per gauge`, so `21 LEDs total`
- 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