16 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
7 changed files with 2323 additions and 872 deletions

View File

@@ -4,7 +4,7 @@ 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`. The ESP32 bridge runs ESPHome; the config is in `gaugecontroller.yaml`.
@@ -16,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)
@@ -27,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:
@@ -60,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, `ledOrder` string). 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`)
@@ -76,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
Two LED strips are driven: main backlight/status LEDs on `LED_DATA_PIN` (currently 22) and dial indicator LEDs on `INDICATOR_LED_DATA_PIN` (currently 36). The serial protocol still exposes one logical per-gauge LED segment: `0-2` backlight, `3-4` indicators, `5-6` status. `gaugePins[i].ledOrder` is a per-LED type string (one char per LED, `'G'` = GRB-ordered, `'R'` = RGB-ordered) and its length defines the logical LED count. `TOTAL_LEDS`, `TOTAL_MAIN_LEDS`, and `TOTAL_INDICATOR_LEDS` are computed at compile time. Per-gauge logical and physical offsets are cached in `setup()`. LED writes dirty only their physical strip, and the loop flushes each FastLED controller independently with `showLeds()`.
### Serial command protocol ### 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:
@@ -94,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 the `ledOrder` string — one char per LED, `'G'` for GRB or `'R'` for RGB). 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`, `gaugeLedOffset[]`, and `gaugeLedCount[]` 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

View File

@@ -4,19 +4,21 @@
#include <stdlib.h> #include <stdlib.h>
#include <FastLED.h> #include <FastLED.h>
static const uint8_t GAUGE_COUNT = 4; static const uint8_t GAUGE_COUNT = 5;
// Backlight/status LEDs and indicator LEDs use separate data strips because // Backlight/status LEDs use an addressable strip. Indicator LEDs are
// their LED chipsets are not compatible on one chain. The command protocol // single-colour active-high PWM outputs on per-gauge pins. The command protocol
// still exposes one logical LED segment per gauge. // still exposes one logical LED segment per gauge.
static const uint8_t LED_DATA_PIN = 22; static const uint8_t LED_DATA_PIN = A8;
static const uint8_t INDICATOR_LED_DATA_PIN = 36;
static const uint8_t BREATHE_FRAME_MS = 16; static const uint8_t BREATHE_FRAME_MS = 16;
static const uint8_t LED_SHOW_MIN_INTERVAL_MS = 16; static const uint8_t LED_SHOW_MIN_INTERVAL_MS = 16;
static const uint8_t LED_SHOW_MOTION_INTERVAL_MS = 50;
static const uint8_t RX_LINE_MAX = 120; static const uint8_t RX_LINE_MAX = 120;
static const uint16_t STEPPER_TIMER_HZ = 20000; static const uint16_t STEPPER_TIMER_HZ = 20000;
static const uint8_t STEPPER_TIMER_PRESCALER = 8; static const uint8_t STEPPER_TIMER_PRESCALER = 8;
static const uint32_t STEPPER_RATE_SCALE = 65536UL; static const uint32_t STEPPER_RATE_SCALE = 65536UL;
static const uint32_t LED_SHOW_FULL_RATE_LIMIT_Q16 = (500UL * STEPPER_RATE_SCALE) / STEPPER_TIMER_HZ;
static const uint32_t LED_SHOW_PAUSE_RATE_Q16 = (1500UL * STEPPER_RATE_SCALE) / STEPPER_TIMER_HZ;
// For now, command and debug traffic share the same serial port. // For now, command and debug traffic share the same serial port.
#define CMD_PORT Serial1 #define CMD_PORT Serial1
@@ -31,14 +33,17 @@ struct GaugePins {
bool stepActiveHigh; bool stepActiveHigh;
bool enableActiveLow; bool enableActiveLow;
const char* ledOrder; // one char per LED: 'G' = GRB, 'R' = RGB; length defines ledCount const char* ledOrder; // one char per LED: 'G' = GRB, 'R' = RGB; length defines ledCount
int8_t indicatorRedPin; // logical LED index 3; -1 means not fitted
int8_t indicatorGreenPin; // logical LED index 4; -1 means not fitted
}; };
constexpr GaugePins gaugePins[GAUGE_COUNT] = { constexpr GaugePins gaugePins[GAUGE_COUNT] = {
// dir, step, en, dirInv, stepHigh, enActiveLow, ledOrder // dir, step, en, dirInv, stepHigh, enActiveLow, ledOrder, indRed, indGreen
{48, 49, -1, false, true, true, "RRRGGRR"}, // Gauge 0 {44, 45, -1, false, true, true, "RRRGGRR", 2, 3}, // Gauge 0
{8, 9, -1, true, true, true, "GGGRRRR"}, // Gauge 1 {46, 47, -1, false, true, true, "RRRGGRR", 4, 5}, // Gauge 1
{52, 53, -1, false, true, true, "GGGRRRR"}, // Gauge 2 {48, 49, -1, true, true, true, "GGGRRRR", 6, 7}, // Gauge 2
{50, 51, -1, false, true, true, "GGGRRRR"}, // Gauge 3 {50, 51, -1, false, true, true, "GGGRRRR", 8, 9}, // Gauge 3
{52, 53, -1, false, true, true, "GGGRRRR", 10,11 }, // Gauge 4
}; };
constexpr uint8_t cstrLen(const char* s) { constexpr uint8_t cstrLen(const char* s) {
@@ -146,19 +151,20 @@ bool rxOverflowed = false;
CRGB logicalLeds[TOTAL_LEDS]; CRGB logicalLeds[TOTAL_LEDS];
CRGB mainLeds[TOTAL_MAIN_LEDS]; CRGB mainLeds[TOTAL_MAIN_LEDS];
CRGB indicatorLeds[TOTAL_INDICATOR_LEDS];
CLEDController* mainLedController = nullptr; CLEDController* mainLedController = nullptr;
CLEDController* indicatorLedController = nullptr;
uint8_t gaugeLedOffset[GAUGE_COUNT]; uint8_t gaugeLedOffset[GAUGE_COUNT];
uint8_t gaugeLedCount[GAUGE_COUNT]; uint8_t gaugeLedCount[GAUGE_COUNT];
uint8_t gaugeMainLedOffset[GAUGE_COUNT]; uint8_t gaugeMainLedOffset[GAUGE_COUNT];
uint8_t gaugeIndicatorLedOffset[GAUGE_COUNT];
uint8_t ledPhysicalIdx[TOTAL_LEDS]; uint8_t ledPhysicalIdx[TOTAL_LEDS];
uint8_t ledGaugeIdx[TOTAL_LEDS];
uint8_t ledLocalIdx[TOTAL_LEDS];
bool ledIsIndicator[TOTAL_LEDS]; bool ledIsIndicator[TOTAL_LEDS];
bool ledRgSwap[TOTAL_LEDS]; bool ledRgSwap[TOTAL_LEDS];
BlinkState blinkState[TOTAL_LEDS]; BlinkState blinkState[TOTAL_LEDS];
volatile uint8_t indicatorPwmLevel[TOTAL_INDICATOR_LEDS];
volatile uint8_t* indicatorPwmPort[TOTAL_INDICATOR_LEDS];
uint8_t indicatorPwmMask[TOTAL_INDICATOR_LEDS];
bool mainLedsDirty = false; bool mainLedsDirty = false;
bool indicatorLedsDirty = false;
unsigned long lastLedShowMs = 0; unsigned long lastLedShowMs = 0;
// FastLED drives the shared strip as RGB. Each gauge's ledOrder string marks per-LED // FastLED drives the shared strip as RGB. Each gauge's ledOrder string marks per-LED
@@ -176,12 +182,30 @@ inline CRGB encodeForStrip(uint8_t globalIdx, CRGB color) {
return color; return color;
} }
inline int8_t indicatorPinFor(uint8_t gaugeIdx, uint8_t localIdx) {
if (localIdx == 3) return gaugePins[gaugeIdx].indicatorRedPin;
if (localIdx == 4) return gaugePins[gaugeIdx].indicatorGreenPin;
return -1;
}
inline uint8_t indicatorLevel(uint8_t localIdx, CRGB color) {
if (localIdx == 3) return color.r;
if (localIdx == 4) return color.g;
return 0;
}
inline void writeIndicatorLed(uint8_t globalIdx, CRGB color) {
uint8_t pwmIdx = ledPhysicalIdx[globalIdx];
if (pwmIdx < TOTAL_INDICATOR_LEDS) {
indicatorPwmLevel[pwmIdx] = indicatorLevel(ledLocalIdx[globalIdx], color);
}
}
inline void writeLed(uint8_t globalIdx, CRGB color) { inline void writeLed(uint8_t globalIdx, CRGB color) {
logicalLeds[globalIdx] = color; logicalLeds[globalIdx] = color;
if (ledIsIndicator[globalIdx]) { if (ledIsIndicator[globalIdx]) {
indicatorLeds[ledPhysicalIdx[globalIdx]] = encodeForStrip(globalIdx, color); writeIndicatorLed(globalIdx, color);
indicatorLedsDirty = true;
} else { } else {
mainLeds[ledPhysicalIdx[globalIdx]] = encodeForStrip(globalIdx, color); mainLeds[ledPhysicalIdx[globalIdx]] = encodeForStrip(globalIdx, color);
mainLedsDirty = true; mainLedsDirty = true;
@@ -192,20 +216,39 @@ inline CRGB readLed(uint8_t globalIdx) {
return logicalLeds[globalIdx]; return logicalLeds[globalIdx];
} }
uint32_t maxStepperRateQ16() {
uint32_t maxRate = 0;
uint8_t oldSreg = SREG;
cli();
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
uint32_t rate = steppers[i].rateQ16;
if (steppers[i].enabled && steppers[i].dir != 0 && rate > maxRate) {
maxRate = rate;
}
}
SREG = oldSreg;
return maxRate;
}
void showDirtyLeds() { void showDirtyLeds() {
if (!mainLedsDirty && !indicatorLedsDirty) return; if (!mainLedsDirty) return;
uint32_t maxStepRate = maxStepperRateQ16();
if (maxStepRate >= LED_SHOW_PAUSE_RATE_Q16) return;
unsigned long nowMs = millis(); unsigned long nowMs = millis();
if (nowMs - lastLedShowMs < LED_SHOW_MIN_INTERVAL_MS) return; uint8_t intervalMs = (maxStepRate > LED_SHOW_FULL_RATE_LIMIT_Q16)
? LED_SHOW_MOTION_INTERVAL_MS
: LED_SHOW_MIN_INTERVAL_MS;
if (nowMs - lastLedShowMs < intervalMs) return;
if (mainLedsDirty && mainLedController != nullptr) { if (mainLedsDirty && mainLedController != nullptr) {
mainLedController->showLeds(255); mainLedController->showLeds(255);
mainLedsDirty = false; mainLedsDirty = false;
} else {
return;
} }
if (indicatorLedsDirty && indicatorLedController != nullptr) {
indicatorLedController->showLeds(255);
indicatorLedsDirty = false;
}
lastLedShowMs = nowMs; lastLedShowMs = nowMs;
} }
@@ -438,7 +481,29 @@ void setupStepperTimer() {
SREG = oldSreg; SREG = oldSreg;
} }
inline void updateIndicatorPwmIsr() {
static uint8_t phase = 0;
phase++;
for (uint8_t i = 0; i < TOTAL_INDICATOR_LEDS; i++) {
volatile uint8_t* port = indicatorPwmPort[i];
if (port == nullptr) continue;
uint8_t mask = indicatorPwmMask[i];
uint8_t level = indicatorPwmLevel[i];
if (level == 0) {
*port &= ~mask;
} else if (level == 255 || phase < level) {
*port |= mask;
} else {
*port &= ~mask;
}
}
}
ISR(TIMER1_COMPA_vect) { ISR(TIMER1_COMPA_vect) {
updateIndicatorPwmIsr();
for (uint8_t i = 0; i < GAUGE_COUNT; i++) { for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
StepperRuntime& s = steppers[i]; StepperRuntime& s = steppers[i];
bool pulseJustEnded = false; bool pulseJustEnded = false;
@@ -1159,14 +1224,22 @@ void setup() {
pinMode(gaugePins[i].enablePin, OUTPUT); pinMode(gaugePins[i].enablePin, OUTPUT);
setEnable(i, true); setEnable(i, true);
} }
if (gaugePins[i].indicatorRedPin >= 0) {
pinMode(gaugePins[i].indicatorRedPin, OUTPUT);
digitalWrite(gaugePins[i].indicatorRedPin, LOW);
}
if (gaugePins[i].indicatorGreenPin >= 0) {
pinMode(gaugePins[i].indicatorGreenPin, OUTPUT);
digitalWrite(gaugePins[i].indicatorGreenPin, LOW);
}
initStepperRuntime(i); initStepperRuntime(i);
setStepperLimits(i, gauges[i].minPos, gauges[i].maxPos); setStepperLimits(i, gauges[i].minPos, gauges[i].maxPos);
gauges[i].lastUpdateMicros = micros(); gauges[i].lastUpdateMicros = micros();
} }
// Flatten the per-gauge LED counts into logical offsets and separate // Flatten the per-gauge LED counts into logical offsets and physical
// physical offsets for the main and indicator strips. // offsets for the addressable main strip.
uint8_t ledOff = 0; uint8_t ledOff = 0;
uint8_t mainLedOff = 0; uint8_t mainLedOff = 0;
uint8_t indicatorLedOff = 0; uint8_t indicatorLedOff = 0;
@@ -1174,28 +1247,34 @@ void setup() {
gaugeLedCount[i] = cstrLen(gaugePins[i].ledOrder); gaugeLedCount[i] = cstrLen(gaugePins[i].ledOrder);
gaugeLedOffset[i] = ledOff; gaugeLedOffset[i] = ledOff;
gaugeMainLedOffset[i] = mainLedOff; gaugeMainLedOffset[i] = mainLedOff;
gaugeIndicatorLedOffset[i] = indicatorLedOff;
for (uint8_t localIdx = 0; localIdx < gaugeLedCount[i]; localIdx++) { for (uint8_t localIdx = 0; localIdx < gaugeLedCount[i]; localIdx++) {
uint8_t globalIdx = ledOff + localIdx; uint8_t globalIdx = ledOff + localIdx;
bool indicator = isIndicatorLedIndex(localIdx); bool indicator = isIndicatorLedIndex(localIdx);
ledGaugeIdx[globalIdx] = i;
ledLocalIdx[globalIdx] = localIdx;
ledIsIndicator[globalIdx] = indicator; ledIsIndicator[globalIdx] = indicator;
ledRgSwap[globalIdx] = gaugePins[i].ledOrder[localIdx] == 'G' || ledRgSwap[globalIdx] = gaugePins[i].ledOrder[localIdx] == 'G' ||
gaugePins[i].ledOrder[localIdx] == 'g'; gaugePins[i].ledOrder[localIdx] == 'g';
ledPhysicalIdx[globalIdx] = indicator if (indicator) {
? indicatorLedOff + (localIdx - 3) ledPhysicalIdx[globalIdx] = indicatorLedOff;
: mainLedOff + localIdx - (localIdx > 4 ? 2 : 0); int8_t pin = indicatorPinFor(i, localIdx);
if (pin >= 0) {
indicatorPwmPort[indicatorLedOff] = portOutputRegister(digitalPinToPort(pin));
indicatorPwmMask[indicatorLedOff] = digitalPinToBitMask(pin);
}
indicatorLedOff++;
} else {
ledPhysicalIdx[globalIdx] = mainLedOff + localIdx - (localIdx > 4 ? 2 : 0);
}
} }
ledOff += gaugeLedCount[i]; ledOff += gaugeLedCount[i];
indicatorLedOff += countIndicatorLedsForGauge(i);
mainLedOff += gaugeLedCount[i] - countIndicatorLedsForGauge(i); mainLedOff += gaugeLedCount[i] - countIndicatorLedsForGauge(i);
} }
mainLedController = &FastLED.addLeds<WS2812, LED_DATA_PIN, RGB>(mainLeds, TOTAL_MAIN_LEDS); mainLedController = &FastLED.addLeds<WS2812, LED_DATA_PIN, RGB>(mainLeds, TOTAL_MAIN_LEDS);
indicatorLedController = &FastLED.addLeds<WS2812B, INDICATOR_LED_DATA_PIN, RGB>(indicatorLeds, TOTAL_INDICATOR_LEDS);
FastLED.setBrightness(255); FastLED.setBrightness(255);
mainLedController->showLeds(255); mainLedController->showLeds(255);
indicatorLedController->showLeds(255);
setupStepperTimer(); setupStepperTimer();
requestHomeAll(); requestHomeAll();

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