Files
arduino_gauge_controller/docs/superpowers/plans/2026-05-21-gaugecontroller-v2.md

14 KiB
Raw Blame History

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:

    #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:

    #include "gauge_config.h"
    

    The top of the file should now read:

    #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):

    static const uint8_t GAUGE_COUNT = 4;
    
    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:

    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:

    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 gaugePinsgaugeConfigs references outside setup()

    Three functions reference gaugePins. Update each one:

    writeDirectionPin (~line 106):

    // Before
    bool level = gaugePins[id].dirInverted ? !forward : forward;
    
    // After
    bool level = gaugeConfigs[id].dirInverted ? !forward : forward;
    

    writeStepPin (~line 111):

    // Before
    bool level = gaugePins[id].stepActiveHigh ? active : !active;
    
    // After
    bool level = gaugeConfigs[id].stepActiveHigh ? active : !active;
    

    configureStepperHardware (~line 152):

    // 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):

    // 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:

    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:

    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

    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:

    grep -n "gaugePins" Gaugecontroller/Gaugecontroller.ino
    

    Should return nothing.

  • Step 7: Commit

    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:

    // 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;
    }
    
    // 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

    // 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;
    }
    
    // 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

    // 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;
    }
    
    // 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

    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

    arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller
    

    Expected: zero errors.

  • Step 6: Commit

    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.