From 2879be0adabea1481f7b0414ef57629219872749 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sun, 19 Apr 2026 23:14:50 +0200 Subject: [PATCH] Code commented, Serial speed moved into constant --- CLAUDE.md | 42 ++++++------ Gaugecontroller/Gaugecontroller.ino | 100 +++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 54540d7..3e37166 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,37 +4,47 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Build & Upload -This is a single-file Arduino sketch (`Gaugecontroller.ino`). Requires the **FastLED** library (`arduino-cli lib install FastLED`). Use the Arduino IDE or `arduino-cli`: +Main firmware lives in `Gaugecontroller/Gaugecontroller.ino`. Requires the **FastLED** library (`arduino-cli lib install FastLED`). Use the Arduino IDE or `arduino-cli`: ```bash # Compile (replace board/port as needed) -arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller.ino +arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller # Upload -arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega Gaugecontroller.ino +arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega Gaugecontroller ``` -Serial monitor: 115200 baud (`Serial` is both CMD_PORT and DEBUG_PORT). +Current default serial setup: `CMD_PORT` and `DEBUG_PORT` both point to `Serial1` at 38400 baud. ## Switching serial ports (debug → production) Two `#define`s at the top of `Gaugecontroller.ino` control where commands and debug output go: ```cpp -#define CMD_PORT Serial // command channel (host sends SET, HOME, etc.) -#define DEBUG_PORT Serial // diagnostic prints (homing, boot messages) +#define CMD_PORT Serial1 // command channel (host sends SET, HOME, etc.) +#define DEBUG_PORT Serial1 // diagnostic prints (homing, boot messages) ``` -**Debug / USB-only (default):** both point to `Serial` (the USB-CDC port). Connect via `minicom` or the Arduino IDE serial monitor at 115200 baud. +**Current default:** both point to `Serial1`, so command and debug traffic share Mega pins TX1=18 / RX1=19 at 38400 baud. -**Production (hardware UART):** change `CMD_PORT` to a hardware serial port so a host MCU or Raspberry Pi can drive it without occupying the USB port: +**USB-only debug setup:** point both defines back at `Serial` if you want to talk to the sketch over the Arduino USB port instead: ```cpp -#define CMD_PORT Serial1 // TX1=pin18, RX1=pin19 -#define DEBUG_PORT Serial // keep USB for monitoring, or silence it (see below) +#define CMD_PORT Serial +#define DEBUG_PORT Serial ``` -Arduino Mega hardware UARTs: +At that point the matching `begin()` call in `setup()` also needs to use the same baud rate you expect on the host side. + +**Split command/debug ports:** if `CMD_PORT` and `DEBUG_PORT` do not point to the same serial port, `setup()` must initialise both. Right now it only calls: + +```cpp +DEBUG_PORT.begin(38400); +``` + +If you split them, add a second `CMD_PORT.begin(...)` call. + +Arduino Mega hardware UARTs for reference: | Port | TX pin | RX pin | |---------|--------|--------| @@ -42,14 +52,6 @@ Arduino Mega hardware UARTs: | Serial2 | 16 | 17 | | Serial3 | 14 | 15 | -`setup()` calls `DEBUG_PORT.begin(115200)` only. If `CMD_PORT` differs from `DEBUG_PORT` you must also begin it — add a second `begin` call in `setup()`: - -```cpp -CMD_PORT.begin(115200); -``` - -**Silencing debug output entirely:** point `DEBUG_PORT` at a null stream, or wrap all `DEBUG_PORT` calls in an `#ifdef DEBUG` guard. The simplest option is to replace the define with a no-op object, but the easiest production approach is just to leave `DEBUG_PORT Serial` and ignore the USB output. - ## Architecture The sketch controls `GAUGE_COUNT` stepper-motor gauges using a trapezoidal velocity profile and a simple text serial protocol. @@ -74,7 +76,7 @@ When `sweepEnabled`, `updateSweepTarget` bounces `targetPos` between `minPos` an ### LED strip -One shared WS2812B strip is driven from `LED_DATA_PIN` (default 6). Each gauge owns a contiguous segment of the strip; `gaugePins[i].ledCount` sets the segment length (0 = no LEDs). `TOTAL_LEDS` is computed at compile time via `constexpr sumLedCounts()` — no manual constant to keep in sync. Per-gauge offsets into the flat `leds[]` array are computed once in `setup()` into `gaugeLedOffset[]`. `FastLED.show()` is called immediately after each `LED` command. +One shared WS2812B strip is driven from `LED_DATA_PIN` (currently 22). Each gauge owns a contiguous segment of the strip; `gaugePins[i].ledCount` sets the segment length (0 = no LEDs). `TOTAL_LEDS` is computed at compile time via `constexpr sumLedCounts()` — no manual constant to keep in sync. Per-gauge offsets into the flat `leds[]` array are computed once in `setup()` into `gaugeLedOffset[]`. LED commands and effects mark the strip dirty, and `FastLED.show()` is called once per main-loop iteration if anything changed. ### Serial command protocol diff --git a/Gaugecontroller/Gaugecontroller.ino b/Gaugecontroller/Gaugecontroller.ino index 1a5ec6a..64a7111 100644 --- a/Gaugecontroller/Gaugecontroller.ino +++ b/Gaugecontroller/Gaugecontroller.ino @@ -10,6 +10,7 @@ static const uint8_t LED_DATA_PIN = 22; // For now, command and debug traffic share the same serial port. #define CMD_PORT Serial1 #define DEBUG_PORT Serial1 +static const unsigned long SERIAL_BAUD = 38400; struct GaugePins { uint8_t dirPin; @@ -92,14 +93,65 @@ uint8_t gaugeLedOffset[GAUGE_COUNT]; BlinkState blinkState[TOTAL_LEDS]; bool ledsDirty = false; +// Sends one-line command replies back over the control port. +// +// Serial protocol summary. +// +// Host -> controller commands (newline-terminated ASCII): +// SET +// SPEED +// ACCEL +// ENABLE <0|1> +// ZERO +// HOME +// HOMEALL +// SWEEP +// POS? +// LED? +// LED +// BLINK [ ] +// BREATHE +// DFLASH +// PING +// +// Controller -> host replies / events: +// READY +// Sent once from setup() after boot completes. +// OK +// Sent after a valid mutating command, and after POS?/LED? once all data lines +// for that query have been emitted. +// PONG +// Sent in response to PING. +// ERR BAD_CMD +// Sent when a complete line matches no parser. +// ERR TOO_LONG +// Sent when an input line exceeds the receive buffer limit. +// ERR BAD_ID +// Sent by commands that take a gauge id when the id is outside 0..GAUGE_COUNT-1. +// ERR BAD_SPEED +// Sent by SPEED when the requested speed is <= 0. +// ERR BAD_ACCEL +// Sent by ACCEL when the requested acceleration is <= 0. +// ERR BAD_IDX +// Sent by LED/BLINK/BREATHE/DFLASH when an LED index or range is invalid. +// ERR BAD_TIME +// Sent by BLINK/BREATHE when the timing parameter is invalid. +// POS +// Emitted once per gauge before the trailing OK reply to POS?. +// LED +// Emitted once per configured LED before the trailing OK reply to LED?. +// HOMED +// Debug event printed on DEBUG_PORT when a homing sequence settles successfully. void sendReply(const String& s) { CMD_PORT.println(s); } +// Tiny float absolute-value helper to avoid dragging more machinery into the sketch. float absf(float x) { return (x < 0.0f) ? -x : x; } +// Updates the cached enable state and toggles the hardware pin if one exists. void setEnable(uint8_t id, bool en) { if (id >= GAUGE_COUNT) return; gauges[id].enabled = en; @@ -111,11 +163,13 @@ void setEnable(uint8_t id, bool en) { digitalWrite(pin, level ? HIGH : LOW); } +// Applies the logical direction after accounting for per-gauge inversion. void setDir(uint8_t id, bool forward) { bool level = gaugePins[id].dirInverted ? !forward : forward; digitalWrite(gaugePins[id].dirPin, level ? HIGH : LOW); } +// Emits one step pulse with the polarity expected by the driver. void pulseStep(uint8_t id) { bool active = gaugePins[id].stepActiveHigh; digitalWrite(gaugePins[id].stepPin, active ? HIGH : LOW); @@ -123,6 +177,7 @@ void pulseStep(uint8_t id) { digitalWrite(gaugePins[id].stepPin, active ? LOW : HIGH); } +// Moves the motor by one step if the requested direction is still within allowed travel. void doStep(uint8_t id, int dir, bool allowPastMin = false) { Gauge& g = gauges[id]; if (!g.enabled) return; @@ -140,6 +195,7 @@ void doStep(uint8_t id, int dir, bool allowPastMin = false) { } } +// Arms the homing state machine for one gauge and clears any in-flight motion. void requestHome(uint8_t id) { if (id >= GAUGE_COUNT) return; Gauge& g = gauges[id]; @@ -150,12 +206,14 @@ void requestHome(uint8_t id) { g.sweepEnabled = false; } +// Starts the same homing sequence on every configured gauge. void requestHomeAll() { for (uint8_t i = 0; i < GAUGE_COUNT; i++) { requestHome(i); } } +// Advances the simple homing state machine until the gauge is parked at logical zero. void updateHoming(uint8_t id) { Gauge& g = gauges[id]; unsigned long nowUs = micros(); @@ -210,6 +268,7 @@ void updateHoming(uint8_t id) { } } +// Flips the sweep destination when the gauge has settled at either end of travel. void updateSweepTarget(uint8_t id) { Gauge& g = gauges[id]; if (!g.sweepEnabled || !g.homed || g.homingState != HS_IDLE) return; @@ -229,6 +288,7 @@ void updateSweepTarget(uint8_t id) { } } +// Runs one gauge worth of motion control, including homing and optional sweeping. void updateGauge(uint8_t id) { Gauge& g = gauges[id]; @@ -326,6 +386,8 @@ void updateGauge(uint8_t id) { } } +// Parses `SET ` and updates the target position. +// Replies: `OK`, `ERR BAD_ID`. bool parseSet(const String& line) { int id; long pos; @@ -345,6 +407,8 @@ bool parseSet(const String& line) { return false; } +// Parses `SPEED ` and updates the max step rate. +// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_SPEED`. bool parseSpeed(const String& line) { int firstSpace = line.indexOf(' '); int secondSpace = line.indexOf(' ', firstSpace + 1); @@ -368,6 +432,8 @@ bool parseSpeed(const String& line) { return true; } +// Parses `ACCEL ` and updates the acceleration limit. +// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_ACCEL`. bool parseAccel(const String& line) { int firstSpace = line.indexOf(' '); int secondSpace = line.indexOf(' ', firstSpace + 1); @@ -391,6 +457,8 @@ bool parseAccel(const String& line) { return true; } +// Parses `ENABLE <0|1>` and toggles the selected driver. +// Replies: `OK`, `ERR BAD_ID`. bool parseEnable(const String& line) { int id, en; if (sscanf(line.c_str(), "ENABLE %d %d", &id, &en) == 2) { @@ -406,6 +474,8 @@ bool parseEnable(const String& line) { return false; } +// Parses `ZERO ` and declares the current position to be home. +// Replies: `OK`, `ERR BAD_ID`. bool parseZero(const String& line) { int id; if (sscanf(line.c_str(), "ZERO %d", &id) == 1) { @@ -427,6 +497,8 @@ bool parseZero(const String& line) { return false; } +// Parses `HOME ` or `HOMEALL` and kicks off the homing sequence. +// Replies: `OK`, `ERR BAD_ID`. Successful completion later emits debug line `HOMED `. bool parseHome(const String& line) { int id; if (sscanf(line.c_str(), "HOME %d", &id) == 1) { @@ -449,6 +521,8 @@ bool parseHome(const String& line) { return false; } +// Parses `SWEEP ` and enables or disables end-to-end motion. +// Replies: `OK`, `ERR BAD_ID`. bool parseSweep(const String& line) { int firstSpace = line.indexOf(' '); int secondSpace = line.indexOf(' ', firstSpace + 1); @@ -485,6 +559,9 @@ bool parseSweep(const String& line) { return true; } +// Answers `POS?` with current motion state for every gauge. +// Emits one `POS ` line per gauge, +// then replies `OK`. bool parsePosQuery(const String& line) { if (line == "POS?") { for (uint8_t i = 0; i < GAUGE_COUNT; i++) { @@ -507,6 +584,8 @@ bool parsePosQuery(const String& line) { return false; } +// Answers the mandatory life question: are you there? +// Reply: `PONG`. bool parsePing(const String& line) { if (line == "PING") { sendReply("PONG"); @@ -515,6 +594,8 @@ bool parsePing(const String& line) { return false; } +// Answers `LED?` with the current RGB values for every configured LED. +// Emits one `LED ` line per configured LED, then replies `OK`. bool parseLedQuery(const String& line) { if (line == "LED?") { for (uint8_t i = 0; i < GAUGE_COUNT; i++) { @@ -538,6 +619,8 @@ bool parseLedQuery(const String& line) { return false; } +// Parses `LED ` and writes static colours. +// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_IDX`. bool parseLed(const String& line) { int id, r, g, b; char idxToken[16]; @@ -561,6 +644,8 @@ bool parseLed(const String& line) { return false; } +// Parses `BLINK ...` and assigns a simple on/off effect to one LED or a range. +// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_IDX`, `ERR BAD_TIME`. bool parseBlink(const String& line) { int id, onMs, offMs, r, g, b; char idxToken[16]; @@ -607,6 +692,8 @@ bool parseBlink(const String& line) { return true; } +// Parses `BREATHE ...` and assigns a triangle-wave fade effect. +// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_IDX`, `ERR BAD_TIME`. bool parseBreathe(const String& line) { int id, periodMs, r, g, b; char idxToken[16]; @@ -638,6 +725,8 @@ bool parseBreathe(const String& line) { return true; } +// Parses `DFLASH ...` and assigns the double-flash pattern. +// Replies: `OK`, `ERR BAD_ID`, `ERR BAD_IDX`. bool parseDflash(const String& line) { int id, r, g, b; char idxToken[16]; @@ -667,6 +756,7 @@ bool parseDflash(const String& line) { return true; } +// Advances all active LED effects and marks the strip dirty when something changed. void updateBlink() { unsigned long nowMs = millis(); bool changed = false; @@ -721,6 +811,8 @@ void updateBlink() { if (changed) ledsDirty = true; } +// Runs the command parsers in order until one claims the line. +// Reply: `ERR BAD_CMD` when no parser accepts the line. void processLine(const String& line) { if (parseSet(line)) return; if (parseSpeed(line)) return; @@ -740,6 +832,8 @@ void processLine(const String& line) { sendReply("ERR BAD_CMD"); } +// Reads newline-delimited commands from serial and hands complete lines to the parser. +// Reply: `ERR TOO_LONG` when the buffered line exceeds the receive limit before newline. void readCommands() { while (CMD_PORT.available()) { char c = (char)CMD_PORT.read(); @@ -760,8 +854,10 @@ void readCommands() { } } +// Initialises pins, LED bookkeeping and the initial homing cycle. +// Reply/event: emits `READY` on CMD_PORT once boot is complete. void setup() { - DEBUG_PORT.begin(38400); + DEBUG_PORT.begin(SERIAL_BAUD); DEBUG_PORT.println("Gauge controller booting"); for (uint8_t i = 0; i < GAUGE_COUNT; i++) { @@ -792,9 +888,11 @@ void setup() { requestHomeAll(); DEBUG_PORT.println("READY"); + // Boot-complete handshake for the command channel. sendReply("READY"); } +// Main service loop: ingest commands, advance effects, move gauges, flush LEDs. void loop() { readCommands(); updateBlink();