diff --git a/Gaugecontroller/Gaugecontroller.ino b/Gaugecontroller/Gaugecontroller.ino index 8ff7d50..e84be18 100644 --- a/Gaugecontroller/Gaugecontroller.ino +++ b/Gaugecontroller/Gaugecontroller.ino @@ -36,8 +36,6 @@ char displayBuffer[kDigitCount] = {' ', ' ', ' ', ' '}; bool pointEnabled = false; bool bellEnabled = false; uint8_t currentPhase = 0; -unsigned long phaseStartMicros = 0; -bool phaseActive = false; uint8_t encodeCharacter(char c) { switch (c) { @@ -112,12 +110,12 @@ void clear() { bellEnabled = false; } -bool parseCommand(const char* command) { +bool parseCommand(const String& command) { char displayText[16]; size_t inputIndex = 0; size_t displayIndex = 0; - if (command[0] == '\0') { + if (command.length() == 0) { return false; } @@ -129,7 +127,7 @@ bool parseCommand(const char* command) { } const size_t digitStart = inputIndex; - while (command[inputIndex] != '\0' && + while (inputIndex < static_cast(command.length()) && isxdigit(static_cast(command[inputIndex]))) { if (displayIndex + 1 >= sizeof(displayText)) { return false; @@ -144,7 +142,7 @@ bool parseCommand(const char* command) { bool newPointEnabled = false; bool newBellEnabled = false; - while (command[inputIndex] != '\0') { + while (inputIndex < static_cast(command.length())) { if (command[inputIndex] == '.') { newPointEnabled = true; } else if (command[inputIndex] == '!') { @@ -203,19 +201,8 @@ void begin() { shiftDriverWord(0); } -// Non-blocking. Returns immediately while the current digit is still being held; -// only does work when the hold time has elapsed and it is time to advance phases. void refresh() { - unsigned long now = micros(); - if (phaseActive && (now - phaseStartMicros) < kDigitHoldMicros) { - return; - } - setBlanked(true); - if (phaseActive) { - currentPhase = (currentPhase + 1) % (kDigitCount + 1); - } - if (currentPhase < kDigitCount) { renderDigit(currentPhase); } else if (pointEnabled || bellEnabled) { @@ -224,9 +211,10 @@ void refresh() { shiftDriverWord(0); } setBlanked(false); + delayMicroseconds(kDigitHoldMicros); + setBlanked(true); - phaseStartMicros = micros(); - phaseActive = true; + currentPhase = (currentPhase + 1) % (kDigitCount + 1); } } // namespace vfd @@ -292,10 +280,6 @@ struct Gauge { bool sweepEnabled = false; bool sweepTowardMax = true; - - // Cached direction-pin state. 0 = uninitialised, 1 = forward, -1 = reverse. - // Lets setDir() skip redundant digitalWrites when the direction hasn't flipped. - int8_t lastDir = 0; }; enum LedFx : uint8_t { FX_BLINK = 0, FX_BREATHE = 1, FX_DFLASH = 2 }; @@ -314,26 +298,29 @@ struct BlinkState { }; Gauge gauges[GAUGE_COUNT]; - -// Fixed receive buffer — Arduino String would heap-fragment on the Mega over time. -// kRxBufSize - 1 is the max payload (one byte reserved for the null terminator). -static const uint8_t kRxBufSize = 128; -char rxBuf[kRxBufSize]; -uint8_t rxLen = 0; +String rxLine; CRGB leds[TOTAL_LEDS]; uint8_t gaugeLedOffset[GAUGE_COUNT]; uint8_t gaugeLedCount[GAUGE_COUNT]; BlinkState blinkState[TOTAL_LEDS]; -// Precomputed in setup() from gaugePins[].ledOrder so per-LED writes don't -// have to walk the gauge table on every call. -bool ledNeedsSwap[TOTAL_LEDS]; bool ledsDirty = false; // FastLED drives the shared strip as RGB. Each gauge's ledOrder string marks per-LED // type ('R' = RGB, 'G' = GRB); writes to GRB-ordered LEDs pre-swap R and G to compensate. +inline bool ledNeedsRgSwap(uint8_t globalIdx) { + for (uint8_t i = 0; i < GAUGE_COUNT; i++) { + uint8_t off = gaugeLedOffset[i]; + if (globalIdx >= off && globalIdx < off + gaugeLedCount[i]) { + char c = gaugePins[i].ledOrder[globalIdx - off]; + return c == 'G' || c == 'g'; + } + } + return false; +} + inline CRGB encodeForStrip(uint8_t globalIdx, CRGB color) { - if (ledNeedsSwap[globalIdx]) { + if (ledNeedsRgSwap(globalIdx)) { uint8_t tmp = color.r; color.r = color.g; color.g = tmp; @@ -351,9 +338,6 @@ inline CRGB readLed(uint8_t globalIdx) { // Sends one-line command replies back over the control port. // -// All parse* functions take a null-terminated char* (no Arduino String) so the -// command pipeline never touches the heap. -// // Serial protocol summary. // // Host -> controller commands (newline-terminated ASCII): @@ -370,7 +354,7 @@ inline CRGB readLed(uint8_t globalIdx) { // LED // BLINK [ ] // BREATHE -// DFLASH +// DFLASH // VFD // PING // @@ -404,10 +388,15 @@ inline CRGB readLed(uint8_t globalIdx) { // 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 char* s) { +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; @@ -421,19 +410,9 @@ void setEnable(uint8_t id, bool en) { } // Applies the logical direction after accounting for per-gauge inversion. -// digitalWrite is skipped when the direction is already correct, which is the -// common case (a step run almost always reuses the previous direction). void setDir(uint8_t id, bool forward) { - Gauge& g = gauges[id]; - int8_t want = forward ? 1 : -1; - if (g.lastDir == want) return; - g.lastDir = want; - bool level = gaugePins[id].dirInverted ? !forward : forward; digitalWrite(gaugePins[id].dirPin, level ? HIGH : LOW); - // DIR-to-STEP setup time. Most A4988/DRV8825-class drivers want at least - // ~200 ns; 1 us is cheap insurance and only paid when the direction flips. - delayMicroseconds(1); } // Emits one step pulse with the polarity expected by the driver. @@ -542,13 +521,13 @@ void updateSweepTarget(uint8_t id) { if (g.sweepTowardMax) { g.targetPos = g.maxPos; - if (g.currentPos >= g.maxPos && fabsf(g.velocity) < 1.0f) { + if (g.currentPos >= g.maxPos && absf(g.velocity) < 1.0f) { g.sweepTowardMax = false; g.targetPos = g.minPos; } } else { g.targetPos = g.minPos; - if (g.currentPos <= g.minPos && fabsf(g.velocity) < 1.0f) { + if (g.currentPos <= g.minPos && absf(g.velocity) < 1.0f) { g.sweepTowardMax = true; g.targetPos = g.maxPos; } @@ -583,7 +562,7 @@ void updateGauge(uint8_t id) { long error = g.targetPos - g.currentPos; - if (error == 0 && fabsf(g.velocity) < 0.01f) { + if (error == 0 && absf(g.velocity) < 0.01f) { g.velocity = 0.0f; g.stepAccumulator = 0.0f; return; @@ -591,8 +570,7 @@ void updateGauge(uint8_t id) { float dir = (error > 0) ? 1.0f : (error < 0 ? -1.0f : 0.0f); // Basic trapezoidal profile: brake if the remaining travel is shorter than the stop distance. - // parseAccel rejects accel <= 0, so the divisor is always positive. - float brakingDistance = (g.velocity * g.velocity) / (2.0f * g.accel); + float brakingDistance = (g.velocity * g.velocity) / (2.0f * g.accel + 0.0001f); if ((float)labs(error) <= brakingDistance) { if (g.velocity > 0.0f) { @@ -656,10 +634,10 @@ void updateGauge(uint8_t id) { // Parses `SET ` and updates the target position. // Replies: `OK`, `ERR BAD_ID`. -bool parseSet(const char* line) { +bool parseSet(const String& line) { int id; long pos; - if (sscanf(line, "SET %d %ld", &id, &pos) == 2) { + if (sscanf(line.c_str(), "SET %d %ld", &id, &pos) == 2) { if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; @@ -677,15 +655,14 @@ bool parseSet(const char* line) { // Parses `SPEED ` and updates the max step rate. // Replies: `OK`, `ERR BAD_ID`, `ERR BAD_SPEED`. -// AVR libc sscanf doesn't support %f by default, so we hand-split with strchr/atof. -bool parseSpeed(const char* line) { - if (strncmp(line, "SPEED ", 6) != 0) return false; - const char* p = line + 6; - const char* sp = strchr(p, ' '); - if (!sp) return false; +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 = atoi(p); - float speed = atof(sp + 1); + 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"); @@ -703,14 +680,14 @@ bool parseSpeed(const char* line) { // Parses `ACCEL ` and updates the acceleration limit. // Replies: `OK`, `ERR BAD_ID`, `ERR BAD_ACCEL`. -bool parseAccel(const char* line) { - if (strncmp(line, "ACCEL ", 6) != 0) return false; - const char* p = line + 6; - const char* sp = strchr(p, ' '); - if (!sp) return false; +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 = atoi(p); - float accel = atof(sp + 1); + 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"); @@ -728,9 +705,9 @@ bool parseAccel(const char* line) { // Parses `ENABLE <0|1>` and toggles the selected driver. // Replies: `OK`, `ERR BAD_ID`. -bool parseEnable(const char* line) { +bool parseEnable(const String& line) { int id, en; - if (sscanf(line, "ENABLE %d %d", &id, &en) == 2) { + if (sscanf(line.c_str(), "ENABLE %d %d", &id, &en) == 2) { if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; @@ -745,9 +722,9 @@ bool parseEnable(const char* line) { // Parses `ZERO ` and declares the current position to be home. // Replies: `OK`, `ERR BAD_ID`. -bool parseZero(const char* line) { +bool parseZero(const String& line) { int id; - if (sscanf(line, "ZERO %d", &id) == 1) { + if (sscanf(line.c_str(), "ZERO %d", &id) == 1) { if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; @@ -768,9 +745,9 @@ bool parseZero(const char* line) { // 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 char* line) { +bool parseHome(const String& line) { int id; - if (sscanf(line, "HOME %d", &id) == 1) { + if (sscanf(line.c_str(), "HOME %d", &id) == 1) { if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; @@ -781,7 +758,7 @@ bool parseHome(const char* line) { return true; } - if (strcmp(line, "HOMEALL") == 0) { + if (line == "HOMEALL") { requestHomeAll(); sendReply("OK"); return true; @@ -792,17 +769,17 @@ bool parseHome(const char* line) { // Parses `SWEEP ` and enables or disables end-to-end motion. // Replies: `OK`, `ERR BAD_ID`. -bool parseSweep(const char* line) { - if (strncmp(line, "SWEEP ", 6) != 0) return false; - const char* p = line + 6; - const char* sp1 = strchr(p, ' '); - if (!sp1) return false; - const char* sp2 = strchr(sp1 + 1, ' '); - if (!sp2) return false; +bool parseSweep(const String& line) { + int firstSpace = line.indexOf(' '); + int secondSpace = line.indexOf(' ', firstSpace + 1); + int thirdSpace = line.indexOf(' ', secondSpace + 1); - int id = atoi(p); - float accel = atof(sp1 + 1); - float speed = atof(sp2 + 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"); @@ -831,8 +808,8 @@ bool parseSweep(const char* line) { // Answers `POS?` with current motion state for every gauge. // Emits one `POS ` line per gauge, // then replies `OK`. -bool parsePosQuery(const char* line) { - if (strcmp(line, "POS?") == 0) { +bool parsePosQuery(const String& line) { + if (line == "POS?") { for (uint8_t i = 0; i < GAUGE_COUNT; i++) { CMD_PORT.print("POS "); CMD_PORT.print(i); @@ -855,8 +832,8 @@ bool parsePosQuery(const char* line) { // Answers `CFG?` with speed and acceleration for every gauge. // Emits one `CFG ` line per gauge, then replies `OK`. -bool parseCfgQuery(const char* line) { - if (strcmp(line, "CFG?") == 0) { +bool parseCfgQuery(const String& line) { + if (line == "CFG?") { for (uint8_t i = 0; i < GAUGE_COUNT; i++) { CMD_PORT.print("CFG "); CMD_PORT.print(i); @@ -873,8 +850,8 @@ bool parseCfgQuery(const char* line) { // Answers the mandatory life question: are you there? // Reply: `PONG`. -bool parsePing(const char* line) { - if (strcmp(line, "PING") == 0) { +bool parsePing(const String& line) { + if (line == "PING") { sendReply("PONG"); return true; } @@ -883,17 +860,17 @@ bool parsePing(const char* line) { // Parses `VFD ` where is up to four hex characters with optional `.` and `!` suffixes. // Replies: `OK`, `ERR BAD_VFD`. -bool parseVfd(const char* line) { - if (strcmp(line, "VFD") == 0) { +bool parseVfd(const String& line) { + if (line == "VFD") { vfd::clear(); sendReply("OK"); return true; } - if (strncmp(line, "VFD ", 4) != 0) return false; + if (!line.startsWith("VFD ")) return false; - const char* payload = line + 4; - if (*payload == '\0') { + const String payload = line.substring(4); + if (payload.length() == 0) { vfd::clear(); sendReply("OK"); return true; @@ -909,8 +886,8 @@ bool parseVfd(const char* line) { // Answers `LED?` with the current RGB values for every configured LED. // Emits one `LED ` line per configured LED, then replies `OK`. -bool parseLedQuery(const char* line) { - if (strcmp(line, "LED?") == 0) { +bool parseLedQuery(const String& line) { + if (line == "LED?") { for (uint8_t i = 0; i < GAUGE_COUNT; i++) { for (uint8_t j = 0; j < gaugeLedCount[i]; j++) { CRGB c = readLed(gaugeLedOffset[i] + j); @@ -934,10 +911,10 @@ bool parseLedQuery(const char* line) { // Parses `LED ` and writes static colours. // Replies: `OK`, `ERR BAD_ID`, `ERR BAD_IDX`. -bool parseLed(const char* line) { +bool parseLed(const String& line) { int id, r, g, b; char idxToken[16]; - if (sscanf(line, "LED %d %15s %d %d %d", &id, idxToken, &r, &g, &b) == 5) { + if (sscanf(line.c_str(), "LED %d %15s %d %d %d", &id, idxToken, &r, &g, &b) == 5) { if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; } char* dash = strchr(idxToken, '-'); int idxFirst = atoi(idxToken); @@ -959,11 +936,11 @@ bool parseLed(const char* line) { // 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 char* line) { +bool parseBlink(const String& line) { int id, onMs, offMs, r, g, b; char idxToken[16]; // Optional RGB values let BLINK either reuse or replace the current colour. - int count = sscanf(line, "BLINK %d %15s %d %d %d %d %d", + int count = sscanf(line.c_str(), "BLINK %d %15s %d %d %d %d %d", &id, idxToken, &onMs, &offMs, &r, &g, &b); if (count != 4 && count != 7) return false; @@ -1007,10 +984,10 @@ bool parseBlink(const char* line) { // Parses `BREATHE ...` and assigns a triangle-wave fade effect. // Replies: `OK`, `ERR BAD_ID`, `ERR BAD_IDX`, `ERR BAD_TIME`. -bool parseBreathe(const char* line) { +bool parseBreathe(const String& line) { int id, periodMs, r, g, b; char idxToken[16]; - if (sscanf(line, "BREATHE %d %15s %d %d %d %d", + if (sscanf(line.c_str(), "BREATHE %d %15s %d %d %d %d", &id, idxToken, &periodMs, &r, &g, &b) != 6) return false; if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; } char* dash = strchr(idxToken, '-'); @@ -1040,10 +1017,10 @@ bool parseBreathe(const char* line) { // Parses `DFLASH ...` and assigns the double-flash pattern. // Replies: `OK`, `ERR BAD_ID`, `ERR BAD_IDX`. -bool parseDflash(const char* line) { +bool parseDflash(const String& line) { int id, r, g, b; char idxToken[16]; - if (sscanf(line, "DFLASH %d %15s %d %d %d", + if (sscanf(line.c_str(), "DFLASH %d %15s %d %d %d", &id, idxToken, &r, &g, &b) != 5) return false; if (id < 0 || id >= GAUGE_COUNT) { sendReply("ERR BAD_ID"); return true; } char* dash = strchr(idxToken, '-'); @@ -1127,7 +1104,7 @@ void updateBlink() { // Runs the command parsers in order until one claims the line. // Reply: `ERR BAD_CMD` when no parser accepts the line. -void processLine(const char* line) { +void processLine(const String& line) { if (parseSet(line)) return; if (parseSpeed(line)) return; if (parseAccel(line)) return; @@ -1155,24 +1132,16 @@ void readCommands() { char c = (char)CMD_PORT.read(); if (c == '\n') { - // Trim trailing whitespace. - while (rxLen > 0 && (rxBuf[rxLen - 1] == ' ' || rxBuf[rxLen - 1] == '\t')) { - rxLen--; + rxLine.trim(); + if (rxLine.length() > 0) { + processLine(rxLine); } - rxBuf[rxLen] = '\0'; - // Skip leading whitespace. - char* start = rxBuf; - while (*start == ' ' || *start == '\t') start++; - if (*start) { - processLine(start); - } - rxLen = 0; + rxLine = ""; } else if (c != '\r') { - if (rxLen >= kRxBufSize - 1) { - rxLen = 0; + rxLine += c; + if (rxLine.length() > 120) { + rxLine = ""; sendReply("ERR TOO_LONG"); - } else { - rxBuf[rxLen++] = c; } } } @@ -1199,17 +1168,11 @@ void setup() { gauges[i].lastUpdateMicros = micros(); } - // Flatten the per-gauge LED counts into offsets on the shared strip, - // and precompute the GRB-vs-RGB swap flag so writeLed/readLed don't have - // to walk the gauge table on every call. + // Flatten the per-gauge LED counts into offsets on the shared strip. uint8_t ledOff = 0; for (uint8_t i = 0; i < GAUGE_COUNT; i++) { gaugeLedCount[i] = cstrLen(gaugePins[i].ledOrder); gaugeLedOffset[i] = ledOff; - for (uint8_t j = 0; j < gaugeLedCount[i]; j++) { - char c = gaugePins[i].ledOrder[j]; - ledNeedsSwap[ledOff + j] = (c == 'G' || c == 'g'); - } ledOff += gaugeLedCount[i]; } FastLED.addLeds(leds, TOTAL_LEDS);