Compare commits
10 Commits
e525dba0c4
...
00b9287d23
| Author | SHA1 | Date | |
|---|---|---|---|
| 00b9287d23 | |||
| f270e4b83f | |||
| 352d47ef59 | |||
| 61c1c733e9 | |||
| e1849f0dd1 | |||
| 30dfcc59df | |||
| 05b7137fcd | |||
| 836af7e836 | |||
| a706838b57 | |||
| 7c3068ff3a |
26
CLAUDE.md
26
CLAUDE.md
@@ -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 (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 |
|
| `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.
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <avr/interrupt.h>
|
#include <avr/interrupt.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
#include "gauge_config.h"
|
||||||
|
|
||||||
static const uint8_t GAUGE_COUNT = 4;
|
|
||||||
static const uint16_t STEPPER_TIMER_HZ = 20000;
|
static const uint16_t STEPPER_TIMER_HZ = 20000;
|
||||||
static const int32_t STEP_RATE_SCALE = 65536L;
|
static const int32_t STEP_RATE_SCALE = 65536L;
|
||||||
static const float MAX_TIMER_STEP_RATE = (float)STEPPER_TIMER_HZ / 2.0f;
|
static const float MAX_TIMER_STEP_RATE = (float)STEPPER_TIMER_HZ / 2.0f;
|
||||||
@@ -12,23 +13,6 @@ static const float MAX_TIMER_STEP_RATE = (float)STEPPER_TIMER_HZ / 2.0f;
|
|||||||
#define DEBUG_PORT Serial
|
#define DEBUG_PORT Serial
|
||||||
static const unsigned long SERIAL_BAUD = 38400;
|
static const unsigned long SERIAL_BAUD = 38400;
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
enum HomingState : uint8_t {
|
enum HomingState : uint8_t {
|
||||||
HS_IDLE,
|
HS_IDLE,
|
||||||
HS_START,
|
HS_START,
|
||||||
@@ -42,13 +26,13 @@ struct Gauge {
|
|||||||
volatile long targetPos = 0;
|
volatile long targetPos = 0;
|
||||||
|
|
||||||
long minPos = 0;
|
long minPos = 0;
|
||||||
long maxPos = 3780;
|
long maxPos = 0;
|
||||||
long homingBackoffSteps = 3800; // Deliberately a touch past full reverse travel.
|
long homingBackoffSteps = 0;
|
||||||
|
|
||||||
float velocity = 0.0f;
|
float velocity = 0.0f;
|
||||||
float maxSpeed = 4000.0f;
|
float maxSpeed = 0.0f;
|
||||||
float accel = 6000.0f;
|
float accel = 0.0f;
|
||||||
float homingSpeed = 500.0f;
|
float homingSpeed = 0.0f;
|
||||||
|
|
||||||
unsigned long lastUpdateMicros = 0;
|
unsigned long lastUpdateMicros = 0;
|
||||||
|
|
||||||
@@ -104,12 +88,12 @@ inline void writePortBit(volatile uint8_t* port, uint8_t mask, bool high) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void writeDirectionPin(uint8_t id, bool forward) {
|
inline void writeDirectionPin(uint8_t id, bool forward) {
|
||||||
bool level = gaugePins[id].dirInverted ? !forward : forward;
|
bool level = gaugeConfigs[id].dirInverted ? !forward : forward;
|
||||||
writePortBit(stepperHardware[id].dirPort, stepperHardware[id].dirMask, level);
|
writePortBit(stepperHardware[id].dirPort, stepperHardware[id].dirMask, level);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void writeStepPin(uint8_t id, bool active) {
|
inline void writeStepPin(uint8_t id, bool active) {
|
||||||
bool level = gaugePins[id].stepActiveHigh ? active : !active;
|
bool level = gaugeConfigs[id].stepActiveHigh ? active : !active;
|
||||||
writePortBit(stepperHardware[id].stepPort, stepperHardware[id].stepMask, level);
|
writePortBit(stepperHardware[id].stepPort, stepperHardware[id].stepMask, level);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,10 +133,10 @@ void stopTimerStepping(uint8_t id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void configureStepperHardware(uint8_t id) {
|
void configureStepperHardware(uint8_t id) {
|
||||||
stepperHardware[id].stepPort = portOutputRegister(digitalPinToPort(gaugePins[id].stepPin));
|
stepperHardware[id].stepPort = portOutputRegister(digitalPinToPort(gaugeConfigs[id].stepPin));
|
||||||
stepperHardware[id].stepMask = digitalPinToBitMask(gaugePins[id].stepPin);
|
stepperHardware[id].stepMask = digitalPinToBitMask(gaugeConfigs[id].stepPin);
|
||||||
stepperHardware[id].dirPort = portOutputRegister(digitalPinToPort(gaugePins[id].dirPin));
|
stepperHardware[id].dirPort = portOutputRegister(digitalPinToPort(gaugeConfigs[id].dirPin));
|
||||||
stepperHardware[id].dirMask = digitalPinToBitMask(gaugePins[id].dirPin);
|
stepperHardware[id].dirMask = digitalPinToBitMask(gaugeConfigs[id].dirPin);
|
||||||
}
|
}
|
||||||
|
|
||||||
void beginStepperTimer() {
|
void beginStepperTimer() {
|
||||||
@@ -288,10 +272,10 @@ void setEnable(uint8_t id, bool en) {
|
|||||||
if (id >= GAUGE_COUNT) return;
|
if (id >= GAUGE_COUNT) return;
|
||||||
gauges[id].enabled = en;
|
gauges[id].enabled = en;
|
||||||
|
|
||||||
int8_t pin = gaugePins[id].enablePin;
|
int8_t pin = gaugeConfigs[id].enablePin;
|
||||||
if (pin < 0) return;
|
if (pin < 0) return;
|
||||||
|
|
||||||
bool level = gaugePins[id].enableActiveLow ? !en : en;
|
bool level = gaugeConfigs[id].enableActiveLow ? !en : en;
|
||||||
digitalWrite(pin, level ? HIGH : LOW);
|
digitalWrite(pin, level ? HIGH : LOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,51 +454,33 @@ bool parseSet(const String& line) {
|
|||||||
// Parses `SPEED <id> <speed>` and updates the max step rate.
|
// Parses `SPEED <id> <speed>` and updates the max step rate.
|
||||||
// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_SPEED`.
|
// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_SPEED`.
|
||||||
bool parseSpeed(const String& line) {
|
bool parseSpeed(const String& line) {
|
||||||
int firstSpace = line.indexOf(' ');
|
int id; char token[20];
|
||||||
int secondSpace = line.indexOf(' ', firstSpace + 1);
|
if (sscanf(line.c_str(), "SPEED %d %19s", &id, token) == 2) {
|
||||||
if (firstSpace < 0 || secondSpace < 0) return false;
|
char* end; float speed = (float)strtod(token, &end);
|
||||||
if (line.substring(0, firstSpace) != "SPEED") return false;
|
if (end == token || *end != '\0') { sendReply("ERR BAD_SPEED"); return true; }
|
||||||
|
if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; }
|
||||||
int id = line.substring(firstSpace + 1, secondSpace).toInt();
|
if (speed <= 0.0f) { sendReply("ERR BAD_SPEED"); return true; }
|
||||||
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;
|
gauges[id].maxSpeed = speed;
|
||||||
sendReply("OK");
|
sendReply("OK");
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses `ACCEL <id> <accel>` and updates the acceleration limit.
|
// Parses `ACCEL <id> <accel>` and updates the acceleration limit.
|
||||||
// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_ACCEL`.
|
// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_ACCEL`.
|
||||||
bool parseAccel(const String& line) {
|
bool parseAccel(const String& line) {
|
||||||
int firstSpace = line.indexOf(' ');
|
int id; char token[20];
|
||||||
int secondSpace = line.indexOf(' ', firstSpace + 1);
|
if (sscanf(line.c_str(), "ACCEL %d %19s", &id, token) == 2) {
|
||||||
if (firstSpace < 0 || secondSpace < 0) return false;
|
char* end; float accel = (float)strtod(token, &end);
|
||||||
if (line.substring(0, firstSpace) != "ACCEL") return false;
|
if (end == token || *end != '\0') { sendReply("ERR BAD_ACCEL"); return true; }
|
||||||
|
if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; }
|
||||||
int id = line.substring(firstSpace + 1, secondSpace).toInt();
|
if (accel <= 0.0f) { sendReply("ERR BAD_ACCEL"); return true; }
|
||||||
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;
|
gauges[id].accel = accel;
|
||||||
sendReply("OK");
|
sendReply("OK");
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses `ENABLE <id> <0|1>` and toggles the selected driver.
|
// Parses `ENABLE <id> <0|1>` and toggles the selected driver.
|
||||||
@@ -584,24 +550,14 @@ bool parseHome(const String& line) {
|
|||||||
// Parses `SWEEP <id> <accel> <speed>` and enables or disables end-to-end motion.
|
// Parses `SWEEP <id> <accel> <speed>` and enables or disables end-to-end motion.
|
||||||
// Replies: `OK`, `ERR BAD_ID`.
|
// Replies: `OK`, `ERR BAD_ID`.
|
||||||
bool parseSweep(const String& line) {
|
bool parseSweep(const String& line) {
|
||||||
int firstSpace = line.indexOf(' ');
|
int id; char accelTok[20], speedTok[20];
|
||||||
int secondSpace = line.indexOf(' ', firstSpace + 1);
|
if (sscanf(line.c_str(), "SWEEP %d %19s %19s", &id, accelTok, speedTok) == 3) {
|
||||||
int thirdSpace = line.indexOf(' ', secondSpace + 1);
|
char* endA; float accel = (float)strtod(accelTok, &endA);
|
||||||
|
char* endS; float speed = (float)strtod(speedTok, &endS);
|
||||||
if (firstSpace < 0 || secondSpace < 0 || thirdSpace < 0) return false;
|
if (endA == accelTok || *endA != '\0') { sendReply("ERR BAD_ACCEL"); return true; }
|
||||||
if (line.substring(0, firstSpace) != "SWEEP") return false;
|
if (endS == speedTok || *endS != '\0') { sendReply("ERR BAD_SPEED"); return true; }
|
||||||
|
if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; }
|
||||||
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];
|
Gauge& g = gauges[id];
|
||||||
|
|
||||||
if (accel <= 0.0f || speed <= 0.0f) {
|
if (accel <= 0.0f || speed <= 0.0f) {
|
||||||
g.sweepEnabled = false;
|
g.sweepEnabled = false;
|
||||||
g.velocity = 0.0f;
|
g.velocity = 0.0f;
|
||||||
@@ -609,7 +565,6 @@ bool parseSweep(const String& line) {
|
|||||||
sendReply("OK");
|
sendReply("OK");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
g.accel = accel;
|
g.accel = accel;
|
||||||
g.maxSpeed = speed;
|
g.maxSpeed = speed;
|
||||||
g.sweepEnabled = true;
|
g.sweepEnabled = true;
|
||||||
@@ -617,6 +572,8 @@ bool parseSweep(const String& line) {
|
|||||||
atomicWriteLong(g.targetPos, g.maxPos);
|
atomicWriteLong(g.targetPos, g.maxPos);
|
||||||
sendReply("OK");
|
sendReply("OK");
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Answers `POS?` with current motion state for every gauge.
|
// Answers `POS?` with current motion state for every gauge.
|
||||||
@@ -718,18 +675,25 @@ void setup() {
|
|||||||
DEBUG_PORT.println("Gauge controller booting");
|
DEBUG_PORT.println("Gauge controller booting");
|
||||||
|
|
||||||
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
||||||
pinMode(gaugePins[i].dirPin, OUTPUT);
|
pinMode(gaugeConfigs[i].dirPin, OUTPUT);
|
||||||
pinMode(gaugePins[i].stepPin, OUTPUT);
|
pinMode(gaugeConfigs[i].stepPin, OUTPUT);
|
||||||
configureStepperHardware(i);
|
configureStepperHardware(i);
|
||||||
|
|
||||||
digitalWrite(gaugePins[i].dirPin, LOW);
|
digitalWrite(gaugeConfigs[i].dirPin, LOW);
|
||||||
digitalWrite(gaugePins[i].stepPin, gaugePins[i].stepActiveHigh ? LOW : HIGH);
|
digitalWrite(gaugeConfigs[i].stepPin, gaugeConfigs[i].stepActiveHigh ? LOW : HIGH);
|
||||||
|
|
||||||
if (gaugePins[i].enablePin >= 0) {
|
if (gaugeConfigs[i].enablePin >= 0) {
|
||||||
pinMode(gaugePins[i].enablePin, OUTPUT);
|
pinMode(gaugeConfigs[i].enablePin, OUTPUT);
|
||||||
setEnable(i, true);
|
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();
|
gauges[i].lastUpdateMicros = micros();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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]);
|
||||||
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
|
||||||
Reference in New Issue
Block a user