Compare commits
34 Commits
debug
...
00b9287d23
| Author | SHA1 | Date | |
|---|---|---|---|
| 00b9287d23 | |||
| f270e4b83f | |||
| 352d47ef59 | |||
| 61c1c733e9 | |||
| e1849f0dd1 | |||
| 30dfcc59df | |||
| 05b7137fcd | |||
| 836af7e836 | |||
| a706838b57 | |||
| 7c3068ff3a | |||
| e525dba0c4 | |||
| 1b699352ce | |||
| c32d208854 | |||
| db05bc0864 | |||
| 5f73e75f5b | |||
| 5656986768 | |||
| aa029587a4 | |||
| abbbd16b5c | |||
| e63867ba5e | |||
| 8bdae1da9b | |||
| 361cf52252 | |||
| 2d63ec6006 | |||
| 511ee05712 | |||
| 03ab8604ba | |||
| edb973bb61 | |||
|
bffcf62cae
|
|||
| 27597bceab | |||
| 016de2ccb4 | |||
| 15257ae6f2 | |||
|
795eb0ecf3
|
|||
|
558c5b18c2
|
|||
|
fa66dd70d4
|
|||
|
b14bdf7fc3
|
|||
|
427dde8c72
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,3 +32,4 @@
|
||||
*.out
|
||||
*.app
|
||||
|
||||
.codex
|
||||
|
||||
28
CLAUDE.md
28
CLAUDE.md
@@ -4,7 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 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
|
||||
# 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
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
**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:
|
||||
|
||||
@@ -58,8 +60,8 @@ The sketch controls `GAUGE_COUNT` stepper-motor gauges using a trapezoidal veloc
|
||||
|
||||
### Key data structures
|
||||
|
||||
- `GaugePins` — hardware pin mapping per gauge (dir, step, enable, active-high/low polarity flags, `ledCount`). Declared `constexpr` so `TOTAL_LEDS` can be computed from it at compile time. Configured in the `gaugePins[]` array at the top.
|
||||
- `Gauge` — per-gauge runtime state: position, target, velocity, accel, homing state machine, sweep mode.
|
||||
- `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. Initialised from `gaugeConfigs[]` in `setup()`.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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
|
||||
|
||||
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 |
|
||||
| `SWEEP` | `SWEEP <id> <accel> <speed>` | Start sweep (0/0 stops) |
|
||||
| `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 (0–255 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 |
|
||||
| `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` |
|
||||
| `CFG?` | `CFG?` | Query all gauges: `CFG <id> <maxSpeed> <accel>` per gauge |
|
||||
| `PING` | `PING` | Responds `PONG` |
|
||||
|
||||
All commands reply `OK` or `ERR BAD_ID` / `ERR BAD_CMD` etc.
|
||||
|
||||
### Adding gauges
|
||||
|
||||
1. Increment `GAUGE_COUNT`.
|
||||
2. Add a `constexpr GaugePins` entry to `gaugePins[]` (including `ledCount`).
|
||||
3. Tune `maxPos` and `homingBackoffSteps` in the corresponding `Gauge` default or at runtime.
|
||||
4. `TOTAL_LEDS` and `gaugeLedOffset[]` update automatically — no manual changes needed.
|
||||
1. Open `Gaugecontroller/gauge_config.h` and append one row to `gaugeConfigs[]`.
|
||||
2. `GAUGE_COUNT` updates automatically — no other changes needed.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
31
Gaugecontroller/gauge_config.h
Normal file
31
Gaugecontroller/gauge_config.h
Normal 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]);
|
||||
1270
Gaugecontroller_WithLeds/Gaugecontroller.ino
Normal file
1270
Gaugecontroller_WithLeds/Gaugecontroller.ino
Normal file
File diff suppressed because it is too large
Load Diff
1301
Gaugecontroller_no_VFD/Gaugecontroller_no_VFD.ino
Normal file
1301
Gaugecontroller_no_VFD/Gaugecontroller_no_VFD.ino
Normal file
File diff suppressed because it is too large
Load Diff
2395
Gaugecontroller_no_VFD/esp-home_no_VFD.yaml
Normal file
2395
Gaugecontroller_no_VFD/esp-home_no_VFD.yaml
Normal file
File diff suppressed because it is too large
Load Diff
52
README.md
52
README.md
@@ -7,7 +7,7 @@ A dedicated gauge controller for Arduinos.
|
||||
This repository contains:
|
||||
|
||||
- `Gaugecontroller/Gaugecontroller.ino`: the Arduino Mega firmware for the stepper gauges, LEDs, and integrated HV5812-based VFD
|
||||
- `gauge.py`: the ESP32 / MicroPython MQTT bridge that exposes the controller to Home Assistant
|
||||
- `gaugecontroller.yaml`: the ESPHome-based ESP32 firmware that exposes the gauges and VFD to Home Assistant via the native API
|
||||
|
||||
## VFD Support
|
||||
|
||||
@@ -48,16 +48,19 @@ Rules:
|
||||
- shorter values are right-aligned
|
||||
- leading zeroes are preserved if they are part of the input
|
||||
|
||||
## Home Assistant Entities
|
||||
## Home Assistant Integration
|
||||
|
||||
The MQTT bridge publishes Home Assistant discovery entities for the VFD:
|
||||
The ESPHome firmware in `gaugecontroller.yaml` exposes entities to Home Assistant via the native API:
|
||||
|
||||
- `VFD Display`
|
||||
text entity for the displayed value
|
||||
- `VFD Decimal Point`
|
||||
switch entity
|
||||
- `VFD Alarm`
|
||||
switch entity
|
||||
### Gauge Controls
|
||||
- Number entities for each gauge's target value (with unit conversion)
|
||||
- Number entities for speed and acceleration (diagnostic)
|
||||
- Rezero buttons for each gauge and all gauges
|
||||
|
||||
### VFD Display
|
||||
- `VFD Display`: text entity for the displayed value
|
||||
- `VFD Decimal Point`: switch entity
|
||||
- `VFD Alarm`: switch entity
|
||||
|
||||
The display is intentionally exposed as a text entity rather than a numeric entity so that:
|
||||
|
||||
@@ -65,27 +68,12 @@ The display is intentionally exposed as a text entity rather than a numeric enti
|
||||
- hexadecimal values like `DEAD` or `BEEF` work
|
||||
- clearing the display is possible with an empty value
|
||||
|
||||
## MQTT Topics
|
||||
### LED Controls
|
||||
- RGB light entity for each gauge's backlight with effects (Blink, Breathe, Double Flash)
|
||||
- Binary light entities for each gauge's red/green indicators and status lights
|
||||
|
||||
Using the configured `mqtt_prefix` from `config.json`, the VFD topics are:
|
||||
|
||||
- `<prefix>/vfd/set`
|
||||
- `<prefix>/vfd/state`
|
||||
- `<prefix>/vfd/decimal_point/set`
|
||||
- `<prefix>/vfd/decimal_point/state`
|
||||
- `<prefix>/vfd/alarm/set`
|
||||
- `<prefix>/vfd/alarm/state`
|
||||
|
||||
Example with the default prefix `gauges`:
|
||||
|
||||
- `gauges/vfd/set`
|
||||
- `gauges/vfd/decimal_point/set`
|
||||
- `gauges/vfd/alarm/set`
|
||||
|
||||
Example payloads:
|
||||
|
||||
- publish `0123` to `gauges/vfd/set`
|
||||
- publish `ON` to `gauges/vfd/decimal_point/set`
|
||||
- publish `OFF` to `gauges/vfd/alarm/set`
|
||||
|
||||
The MQTT bridge then converts that into the correct Arduino serial command such as `VFD 0123.`.
|
||||
### Diagnostics
|
||||
- WiFi signal sensor
|
||||
- Uptime sensor
|
||||
- IP address and SSID text sensors
|
||||
- Arduino Last Message sensor
|
||||
|
||||
@@ -183,13 +183,14 @@ Then connect the motor side of that driver to:
|
||||
|
||||
according to the driver board you are using.
|
||||
|
||||
## 14. Wire The WS2812B LEDs
|
||||
## 14. Wire The WS2812 LEDs
|
||||
|
||||
Connect:
|
||||
|
||||
- `Mega D22` -> `WS2812B DIN`
|
||||
- `5V LED supply` -> `WS2812B 5V`
|
||||
- `WS2812B GND` -> common ground rail
|
||||
- `Mega D22` -> main backlight/status strip `DIN`
|
||||
- `Mega D36` -> indicator strip `DIN`
|
||||
- `5V LED supply` -> both strip `5V` inputs
|
||||
- both strip `GND` inputs -> common ground rail
|
||||
|
||||
If the LED chain is long or bright:
|
||||
|
||||
|
||||
@@ -205,9 +205,10 @@ If `D8` and `D9` come from separate fly wires to the stripboard, keep them in th
|
||||
|
||||
Route:
|
||||
|
||||
- `D22` -> `WS2812 DIN`
|
||||
- `5V` -> `WS2812 5V`
|
||||
- `GND` -> `WS2812 GND`
|
||||
- `D22` -> main backlight/status strip `DIN`
|
||||
- `D36` -> indicator strip `DIN`
|
||||
- `5V` -> both strip `5V` inputs
|
||||
- `GND` -> both strip `GND` inputs
|
||||
|
||||
Keep the LED connector in the low-voltage area.
|
||||
|
||||
|
||||
@@ -28,6 +28,16 @@ import gc
|
||||
from umqtt.robust import MQTTClient
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -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_BAUD = int(_cfg.get("arduino_baud", 115200))
|
||||
|
||||
_arduino = None
|
||||
|
||||
|
||||
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
|
||||
_arduino = UART(ARDUINO_UART_ID, baudrate=ARDUINO_BAUD, tx=ARDUINO_TX_PIN, rx=ARDUINO_RX_PIN, timeout=10)
|
||||
|
||||
|
||||
def arduino_send(cmd):
|
||||
"""Send a newline-terminated command to the Arduino."""
|
||||
_ensure_arduino().write((cmd + "\n").encode())
|
||||
_arduino.write((cmd + "\n").encode())
|
||||
info(f"Arduino → {cmd}")
|
||||
|
||||
|
||||
def arduino_recv():
|
||||
"""Print any lines waiting in the Arduino RX buffer."""
|
||||
uart = _ensure_arduino()
|
||||
while uart.any():
|
||||
line = uart.readline()
|
||||
while _arduino.any():
|
||||
line = _arduino.readline()
|
||||
if line:
|
||||
print(f"[{_ts()}] ARDU {line.decode().strip()}")
|
||||
|
||||
@@ -538,14 +540,11 @@ _WIFI_CONNECT_ATTEMPTS = 3
|
||||
def _reset_wifi_interface():
|
||||
global _wifi_sta
|
||||
_wifi_sta = network.WLAN(network.STA_IF)
|
||||
if not _wifi_sta.active():
|
||||
_wifi_sta.active(True)
|
||||
utime.sleep_ms(500)
|
||||
try:
|
||||
_wifi_sta.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
utime.sleep_ms(1000)
|
||||
if _wifi_sta.active():
|
||||
_wifi_sta.active(False)
|
||||
utime.sleep_ms(200)
|
||||
_wifi_sta.active(True)
|
||||
utime.sleep_ms(500)
|
||||
|
||||
|
||||
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
|
||||
for attempt in range(_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:
|
||||
_wifi_sta.connect(ssid, password)
|
||||
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" MAC : {mac}")
|
||||
info(f" IP : {ip} mask:{mask} gw:{gw} dns:{dns}")
|
||||
utime.sleep_ms(2000)
|
||||
utime.sleep_ms(500)
|
||||
return ip
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
@@ -603,7 +601,7 @@ def check_wifi():
|
||||
|
||||
log_err("WiFi lost connection — attempting reconnect...")
|
||||
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}")
|
||||
except Exception as e:
|
||||
log_err(f"WiFi reconnect failed: {e}")
|
||||
@@ -913,6 +911,10 @@ def connect_mqtt():
|
||||
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)
|
||||
|
||||
@@ -920,6 +922,27 @@ def connect_mqtt():
|
||||
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():
|
||||
global client_ref, _mqtt_connected, _last_mqtt_check
|
||||
now = utime.ticks_ms()
|
||||
@@ -961,6 +984,10 @@ def check_mqtt():
|
||||
return True
|
||||
except Exception as 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)
|
||||
|
||||
@@ -968,17 +995,9 @@ def check_mqtt():
|
||||
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):
|
||||
gc.collect()
|
||||
client.publish(topic, ujson.dumps(payload), retain=True)
|
||||
client.publish(topic, ujson.dumps(_compact_discovery_payload(payload)), retain=True)
|
||||
info(log_msg)
|
||||
|
||||
|
||||
@@ -1286,42 +1305,12 @@ def apply_motion_defaults():
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
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()
|
||||
info("=" * 48)
|
||||
info("Gauge MQTT controller starting")
|
||||
@@ -1329,7 +1318,7 @@ def main():
|
||||
info("=" * 48)
|
||||
|
||||
gc.collect()
|
||||
connect_wifi(WIFI_SSID, WIFI_PASSWORD)
|
||||
connect_wifi(WIFI_SSID, WIFI_PASSWORD, force_reconnect=True)
|
||||
|
||||
mqtt_attempts = 0
|
||||
while True:
|
||||
@@ -1342,14 +1331,13 @@ def main():
|
||||
if mqtt_attempts % 3 == 0:
|
||||
log_err("WiFi may be stale — forcing reconnect...")
|
||||
try:
|
||||
connect_wifi(WIFI_SSID, WIFI_PASSWORD)
|
||||
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)
|
||||
schedule_discovery()
|
||||
|
||||
publish_backlight_states(client_ref)
|
||||
apply_motion_defaults()
|
||||
info("Draining initial retained messages...")
|
||||
for _ in range(50):
|
||||
@@ -1362,10 +1350,6 @@ def main():
|
||||
gauge_last_rezero[i] = utime.ticks_ms()
|
||||
info("Home command sent")
|
||||
|
||||
utime.sleep_ms(100)
|
||||
_restore_led_states()
|
||||
info("LED effects restored")
|
||||
|
||||
info("Publishing state...")
|
||||
publish_online(client_ref)
|
||||
publish_state(client_ref)
|
||||
62
boot.py
62
boot.py
@@ -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
46
changes.md
Normal 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%).
|
||||
474
docs/superpowers/plans/2026-05-21-gaugecontroller-v2.md
Normal file
474
docs/superpowers/plans/2026-05-21-gaugecontroller-v2.md
Normal 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.
|
||||
162
docs/superpowers/specs/2026-05-21-gaugecontroller-v2-design.md
Normal file
162
docs/superpowers/specs/2026-05-21-gaugecontroller-v2-design.md
Normal 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
2394
gaugecontroller.yaml
Normal file
File diff suppressed because it is too large
Load Diff
17
wiring.md
17
wiring.md
@@ -163,19 +163,22 @@ Also connect:
|
||||
|
||||
If your driver boards need separate motor power, supply that from the proper motor supply. Do not power motors from the Mega `5V` pin.
|
||||
|
||||
## WS2812B LED Strip
|
||||
## WS2812 LED Strips
|
||||
|
||||
The current sketch expects one shared WS2812B chain.
|
||||
The current sketch expects two LED data chains. Backlight and status LEDs stay
|
||||
on the main strip; the red/green dial indicator LEDs are on their own strip.
|
||||
|
||||
| Mega Pin | WS2812B |
|
||||
| Mega Pin | LED Strip |
|
||||
|---|---|
|
||||
| `D22` | `DIN` |
|
||||
| `5V` | `5V` |
|
||||
| `GND` | `GND` |
|
||||
| `D22` | main backlight/status `DIN` |
|
||||
| `D36` | indicator `DIN` |
|
||||
| `5V` | both strips `5V` |
|
||||
| `GND` | both strips `GND` |
|
||||
|
||||
Notes:
|
||||
|
||||
- the code expects `7 LEDs per gauge`, so `21 LEDs total`
|
||||
- the command protocol still exposes `7 LEDs per gauge`
|
||||
- logical indices `0-2` are backlight, `3-4` are indicators, and `5-6` are status
|
||||
- use a proper 5V supply sized for the LED current
|
||||
- keep LED ground common with the Mega
|
||||
|
||||
|
||||
Reference in New Issue
Block a user