Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23834d660c | |||
| a0821fed80 | |||
| 014346c442 | |||
| cf545cb849 | |||
| 9af28acb51 | |||
| af42a1c4de | |||
| ffd807ff45 | |||
| c57e0d8ae2 | |||
| a294ce1fd9 | |||
| 179fb77b95 | |||
| cf36a2c07f | |||
| 70a6719553 | |||
| cba7d907e9 | |||
| ee0d85fa73 | |||
| 0b4f285ddb | |||
| 415310b1f9 | |||
| b1afa2f06c | |||
| 423b9f91be |
10
CLAUDE.md
10
CLAUDE.md
@@ -6,8 +6,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
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`. Requires the **FastLED** library (`arduino-cli lib install FastLED`). Use the Arduino IDE or `arduino-cli`:
|
||||||
|
|
||||||
The ESP32 bridge runs ESPHome; the config is in `gaugecontroller.yaml`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Compile (replace board/port as needed)
|
# Compile (replace board/port as needed)
|
||||||
arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller
|
arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller
|
||||||
@@ -60,7 +58,7 @@ 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.
|
- `GaugePins` — hardware pin mapping per gauge (dir, step, enable, active-high/low polarity flags, `ledCount`). Declared `constexpr` so `TOTAL_LEDS` can be computed from it at compile time. Configured in the `gaugePins[]` array at the top.
|
||||||
- `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.
|
||||||
|
|
||||||
### Motion control (`updateGauge`)
|
### Motion control (`updateGauge`)
|
||||||
@@ -78,7 +76,7 @@ When `sweepEnabled`, `updateSweepTarget` bounces `targetPos` between `minPos` an
|
|||||||
|
|
||||||
### LED strip
|
### 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()`.
|
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
|
### Serial command protocol
|
||||||
|
|
||||||
@@ -106,6 +104,6 @@ All commands reply `OK` or `ERR BAD_ID` / `ERR BAD_CMD` etc.
|
|||||||
### Adding gauges
|
### Adding gauges
|
||||||
|
|
||||||
1. Increment `GAUGE_COUNT`.
|
1. Increment `GAUGE_COUNT`.
|
||||||
2. Add a `constexpr GaugePins` entry to `gaugePins[]` (including the `ledOrder` string — one char per LED, `'G'` for GRB or `'R'` for RGB).
|
2. Add a `constexpr GaugePins` entry to `gaugePins[]` (including `ledCount`).
|
||||||
3. Tune `maxPos` and `homingBackoffSteps` in the corresponding `Gauge` default or at runtime.
|
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.
|
4. `TOTAL_LEDS` and `gaugeLedOffset[]` update automatically — no manual changes needed.
|
||||||
|
|||||||
@@ -3,14 +3,10 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <FastLED.h>
|
#include <FastLED.h>
|
||||||
|
|
||||||
static const uint8_t GAUGE_COUNT = 4;
|
static const uint8_t GAUGE_COUNT = 3;
|
||||||
|
|
||||||
// Backlight/status LEDs and indicator LEDs use separate data strips because
|
// One shared WS2812B strip, split into per-gauge segments.
|
||||||
// their LED chipsets are not compatible on one chain. The command protocol
|
|
||||||
// still exposes one logical LED segment per gauge.
|
|
||||||
static const uint8_t LED_DATA_PIN = 22;
|
static const uint8_t LED_DATA_PIN = 22;
|
||||||
static const uint8_t INDICATOR_LED_DATA_PIN = 36;
|
|
||||||
static const uint8_t BREATHE_FRAME_MS = 16;
|
|
||||||
|
|
||||||
// For now, command and debug traffic share the same serial port.
|
// For now, command and debug traffic share the same serial port.
|
||||||
#define CMD_PORT Serial1
|
#define CMD_PORT Serial1
|
||||||
@@ -230,41 +226,21 @@ struct GaugePins {
|
|||||||
bool dirInverted;
|
bool dirInverted;
|
||||||
bool stepActiveHigh;
|
bool stepActiveHigh;
|
||||||
bool enableActiveLow;
|
bool enableActiveLow;
|
||||||
const char* ledOrder; // one char per LED: 'G' = GRB, 'R' = RGB; length defines ledCount
|
uint8_t ledCount; // LEDs assigned to this gauge
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr GaugePins gaugePins[GAUGE_COUNT] = {
|
constexpr GaugePins gaugePins[GAUGE_COUNT] = {
|
||||||
// dir, step, en, dirInv, stepHigh, enActiveLow, ledOrder
|
// dir, step, en, dirInv, stepHigh, enActiveLow, leds
|
||||||
{50, 51, -1, false, true, true, "RRRGGRR"}, // Gauge 0
|
{50, 51, -1, false, true, true, 7}, // Gauge 0
|
||||||
{8, 9, -1, true, true, true, "GGGRRRR"}, // Gauge 1
|
{8, 9, -1, true, true, true, 7}, // Gauge 1
|
||||||
{52, 53, -1, false, true, true, "GGGRRRR"}, // Gauge 2
|
{52, 53, -1, false, true, true, 7}, // Gauge 2
|
||||||
{48, 49, -1, false, true, true, "GGGRRRR"}, // Gauge 3
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr uint8_t cstrLen(const char* s) {
|
|
||||||
return *s ? uint8_t(1 + cstrLen(s + 1)) : uint8_t(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint8_t sumLedCounts(uint8_t i = 0) {
|
constexpr uint8_t sumLedCounts(uint8_t i = 0) {
|
||||||
return i >= GAUGE_COUNT ? 0 : cstrLen(gaugePins[i].ledOrder) + sumLedCounts(i + 1);
|
return i >= GAUGE_COUNT ? 0 : gaugePins[i].ledCount + sumLedCounts(i + 1);
|
||||||
}
|
}
|
||||||
static const uint8_t TOTAL_LEDS = sumLedCounts();
|
static const uint8_t TOTAL_LEDS = sumLedCounts();
|
||||||
|
|
||||||
constexpr bool isIndicatorLedIndex(uint8_t localIdx) {
|
|
||||||
return localIdx == 3 || localIdx == 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint8_t countIndicatorLedsForGauge(uint8_t gaugeIdx) {
|
|
||||||
return (cstrLen(gaugePins[gaugeIdx].ledOrder) > 3 ? 1 : 0) +
|
|
||||||
(cstrLen(gaugePins[gaugeIdx].ledOrder) > 4 ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint8_t sumIndicatorLedCounts(uint8_t i = 0) {
|
|
||||||
return i >= GAUGE_COUNT ? 0 : countIndicatorLedsForGauge(i) + sumIndicatorLedCounts(i + 1);
|
|
||||||
}
|
|
||||||
static const uint8_t TOTAL_INDICATOR_LEDS = sumIndicatorLedCounts();
|
|
||||||
static const uint8_t TOTAL_MAIN_LEDS = TOTAL_LEDS - TOTAL_INDICATOR_LEDS;
|
|
||||||
|
|
||||||
enum HomingState : uint8_t {
|
enum HomingState : uint8_t {
|
||||||
HS_IDLE,
|
HS_IDLE,
|
||||||
HS_START,
|
HS_START,
|
||||||
@@ -319,88 +295,10 @@ struct BlinkState {
|
|||||||
Gauge gauges[GAUGE_COUNT];
|
Gauge gauges[GAUGE_COUNT];
|
||||||
String rxLine;
|
String rxLine;
|
||||||
|
|
||||||
CRGB logicalLeds[TOTAL_LEDS];
|
CRGB leds[TOTAL_LEDS];
|
||||||
CRGB mainLeds[TOTAL_MAIN_LEDS];
|
|
||||||
CRGB indicatorLeds[TOTAL_INDICATOR_LEDS];
|
|
||||||
CLEDController* mainLedController = nullptr;
|
|
||||||
CLEDController* indicatorLedController = nullptr;
|
|
||||||
uint8_t gaugeLedOffset[GAUGE_COUNT];
|
uint8_t gaugeLedOffset[GAUGE_COUNT];
|
||||||
uint8_t gaugeLedCount[GAUGE_COUNT];
|
|
||||||
uint8_t gaugeMainLedOffset[GAUGE_COUNT];
|
|
||||||
uint8_t gaugeIndicatorLedOffset[GAUGE_COUNT];
|
|
||||||
BlinkState blinkState[TOTAL_LEDS];
|
BlinkState blinkState[TOTAL_LEDS];
|
||||||
bool mainLedsDirty = false;
|
bool ledsDirty = false;
|
||||||
bool indicatorLedsDirty = 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 (ledNeedsRgSwap(globalIdx)) {
|
|
||||||
uint8_t tmp = color.r;
|
|
||||||
color.r = color.g;
|
|
||||||
color.g = tmp;
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ledPhysicalIndex(uint8_t globalIdx, bool& indicatorStrip, uint8_t& physicalIdx) {
|
|
||||||
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
|
||||||
uint8_t off = gaugeLedOffset[i];
|
|
||||||
if (globalIdx < off || globalIdx >= off + gaugeLedCount[i]) continue;
|
|
||||||
|
|
||||||
uint8_t localIdx = globalIdx - off;
|
|
||||||
indicatorStrip = isIndicatorLedIndex(localIdx);
|
|
||||||
if (indicatorStrip) {
|
|
||||||
physicalIdx = gaugeIndicatorLedOffset[i] + (localIdx - 3);
|
|
||||||
} else {
|
|
||||||
physicalIdx = gaugeMainLedOffset[i] + localIdx - (localIdx > 4 ? 2 : 0);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void writeLed(uint8_t globalIdx, CRGB color) {
|
|
||||||
logicalLeds[globalIdx] = color;
|
|
||||||
|
|
||||||
bool indicatorStrip = false;
|
|
||||||
uint8_t physicalIdx = 0;
|
|
||||||
if (!ledPhysicalIndex(globalIdx, indicatorStrip, physicalIdx)) return;
|
|
||||||
|
|
||||||
if (indicatorStrip) {
|
|
||||||
indicatorLeds[physicalIdx] = encodeForStrip(globalIdx, color);
|
|
||||||
indicatorLedsDirty = true;
|
|
||||||
} else {
|
|
||||||
mainLeds[physicalIdx] = encodeForStrip(globalIdx, color);
|
|
||||||
mainLedsDirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline CRGB readLed(uint8_t globalIdx) {
|
|
||||||
return logicalLeds[globalIdx];
|
|
||||||
}
|
|
||||||
|
|
||||||
void showDirtyLeds() {
|
|
||||||
if (mainLedsDirty && mainLedController != nullptr) {
|
|
||||||
mainLedController->showLeds(255);
|
|
||||||
mainLedsDirty = false;
|
|
||||||
}
|
|
||||||
if (indicatorLedsDirty && indicatorLedController != nullptr) {
|
|
||||||
indicatorLedController->showLeds(255);
|
|
||||||
indicatorLedsDirty = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends one-line command replies back over the control port.
|
// Sends one-line command replies back over the control port.
|
||||||
//
|
//
|
||||||
@@ -896,24 +794,6 @@ bool parsePosQuery(const String& line) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Answers `CFG?` with speed and acceleration for every gauge.
|
|
||||||
// Emits one `CFG <id> <maxSpeed> <accel>` line per gauge, then replies `OK`.
|
|
||||||
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);
|
|
||||||
CMD_PORT.print(' ');
|
|
||||||
CMD_PORT.print((int)gauges[i].maxSpeed);
|
|
||||||
CMD_PORT.print(' ');
|
|
||||||
CMD_PORT.println((int)gauges[i].accel);
|
|
||||||
}
|
|
||||||
sendReply("OK");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Answers the mandatory life question: are you there?
|
// Answers the mandatory life question: are you there?
|
||||||
// Reply: `PONG`.
|
// Reply: `PONG`.
|
||||||
bool parsePing(const String& line) {
|
bool parsePing(const String& line) {
|
||||||
@@ -955,8 +835,8 @@ bool parseVfd(const String& line) {
|
|||||||
bool parseLedQuery(const String& line) {
|
bool parseLedQuery(const String& line) {
|
||||||
if (line == "LED?") {
|
if (line == "LED?") {
|
||||||
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
||||||
for (uint8_t j = 0; j < gaugeLedCount[i]; j++) {
|
for (uint8_t j = 0; j < gaugePins[i].ledCount; j++) {
|
||||||
CRGB c = readLed(gaugeLedOffset[i] + j);
|
const CRGB& c = leds[gaugeLedOffset[i] + j];
|
||||||
CMD_PORT.print("LED ");
|
CMD_PORT.print("LED ");
|
||||||
CMD_PORT.print(i);
|
CMD_PORT.print(i);
|
||||||
CMD_PORT.print(' ');
|
CMD_PORT.print(' ');
|
||||||
@@ -985,14 +865,15 @@ bool parseLed(const String& line) {
|
|||||||
char* dash = strchr(idxToken, '-');
|
char* dash = strchr(idxToken, '-');
|
||||||
int idxFirst = atoi(idxToken);
|
int idxFirst = atoi(idxToken);
|
||||||
int idxLast = dash ? atoi(dash + 1) : idxFirst;
|
int idxLast = dash ? atoi(dash + 1) : idxFirst;
|
||||||
if (idxFirst < 0 || idxLast >= gaugeLedCount[id] || idxFirst > idxLast) {
|
if (idxFirst < 0 || idxLast >= gaugePins[id].ledCount || idxFirst > idxLast) {
|
||||||
sendReply("ERR BAD_IDX"); return true;
|
sendReply("ERR BAD_IDX"); return true;
|
||||||
}
|
}
|
||||||
CRGB color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
|
CRGB color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
|
||||||
for (int i = idxFirst; i <= idxLast; i++) {
|
for (int i = idxFirst; i <= idxLast; i++) {
|
||||||
blinkState[gaugeLedOffset[id] + i].active = false;
|
blinkState[gaugeLedOffset[id] + i].active = false;
|
||||||
writeLed(gaugeLedOffset[id] + i, color);
|
leds[gaugeLedOffset[id] + i] = color;
|
||||||
}
|
}
|
||||||
|
ledsDirty = true;
|
||||||
sendReply("OK");
|
sendReply("OK");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1013,7 +894,7 @@ bool parseBlink(const String& line) {
|
|||||||
char* dash = strchr(idxToken, '-');
|
char* dash = strchr(idxToken, '-');
|
||||||
int idxFirst = atoi(idxToken);
|
int idxFirst = atoi(idxToken);
|
||||||
int idxLast = dash ? atoi(dash + 1) : idxFirst;
|
int idxLast = dash ? atoi(dash + 1) : idxFirst;
|
||||||
if (idxFirst < 0 || idxLast >= gaugeLedCount[id] || idxFirst > idxLast) {
|
if (idxFirst < 0 || idxLast >= gaugePins[id].ledCount || idxFirst > idxLast) {
|
||||||
sendReply("ERR BAD_IDX"); return true;
|
sendReply("ERR BAD_IDX"); return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1034,14 +915,15 @@ bool parseBlink(const String& line) {
|
|||||||
uint8_t globalIdx = gaugeLedOffset[id] + i;
|
uint8_t globalIdx = gaugeLedOffset[id] + i;
|
||||||
BlinkState& bs = blinkState[globalIdx];
|
BlinkState& bs = blinkState[globalIdx];
|
||||||
bs.fx = FX_BLINK;
|
bs.fx = FX_BLINK;
|
||||||
bs.onColor = (count == 7) ? color : readLed(globalIdx);
|
bs.onColor = (count == 7) ? color : leds[globalIdx];
|
||||||
bs.onMs = (uint16_t)onMs;
|
bs.onMs = (uint16_t)onMs;
|
||||||
bs.offMs = (uint16_t)offMs;
|
bs.offMs = (uint16_t)offMs;
|
||||||
bs.currentlyOn = true;
|
bs.currentlyOn = true;
|
||||||
bs.lastMs = nowMs;
|
bs.lastMs = nowMs;
|
||||||
bs.active = true;
|
bs.active = true;
|
||||||
writeLed(globalIdx, bs.onColor);
|
leds[globalIdx] = bs.onColor;
|
||||||
}
|
}
|
||||||
|
ledsDirty = true;
|
||||||
sendReply("OK");
|
sendReply("OK");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1057,7 +939,7 @@ bool parseBreathe(const String& line) {
|
|||||||
char* dash = strchr(idxToken, '-');
|
char* dash = strchr(idxToken, '-');
|
||||||
int idxFirst = atoi(idxToken);
|
int idxFirst = atoi(idxToken);
|
||||||
int idxLast = dash ? atoi(dash + 1) : idxFirst;
|
int idxLast = dash ? atoi(dash + 1) : idxFirst;
|
||||||
if (idxFirst < 0 || idxLast >= gaugeLedCount[id] || idxFirst > idxLast) {
|
if (idxFirst < 0 || idxLast >= gaugePins[id].ledCount || idxFirst > idxLast) {
|
||||||
sendReply("ERR BAD_IDX"); return true;
|
sendReply("ERR BAD_IDX"); return true;
|
||||||
}
|
}
|
||||||
if (periodMs <= 0) { sendReply("ERR BAD_TIME"); return true; }
|
if (periodMs <= 0) { sendReply("ERR BAD_TIME"); return true; }
|
||||||
@@ -1072,8 +954,9 @@ bool parseBreathe(const String& line) {
|
|||||||
bs.cyclePos = 0;
|
bs.cyclePos = 0;
|
||||||
bs.lastMs = nowMs;
|
bs.lastMs = nowMs;
|
||||||
bs.active = true;
|
bs.active = true;
|
||||||
writeLed(gi, CRGB::Black);
|
leds[gi] = CRGB::Black;
|
||||||
}
|
}
|
||||||
|
ledsDirty = true;
|
||||||
sendReply("OK");
|
sendReply("OK");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1089,7 +972,7 @@ bool parseDflash(const String& line) {
|
|||||||
char* dash = strchr(idxToken, '-');
|
char* dash = strchr(idxToken, '-');
|
||||||
int idxFirst = atoi(idxToken);
|
int idxFirst = atoi(idxToken);
|
||||||
int idxLast = dash ? atoi(dash + 1) : idxFirst;
|
int idxLast = dash ? atoi(dash + 1) : idxFirst;
|
||||||
if (idxFirst < 0 || idxLast >= gaugeLedCount[id] || idxFirst > idxLast) {
|
if (idxFirst < 0 || idxLast >= gaugePins[id].ledCount || idxFirst > idxLast) {
|
||||||
sendReply("ERR BAD_IDX"); return true;
|
sendReply("ERR BAD_IDX"); return true;
|
||||||
}
|
}
|
||||||
CRGB color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
|
CRGB color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
|
||||||
@@ -1102,18 +985,20 @@ bool parseDflash(const String& line) {
|
|||||||
bs.dphase = 0;
|
bs.dphase = 0;
|
||||||
bs.lastMs = nowMs;
|
bs.lastMs = nowMs;
|
||||||
bs.active = true;
|
bs.active = true;
|
||||||
writeLed(gi, color); // phase 0 = on
|
leds[gi] = color; // phase 0 = on
|
||||||
}
|
}
|
||||||
|
ledsDirty = true;
|
||||||
sendReply("OK");
|
sendReply("OK");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advances all active LED effects. writeLed() marks the affected physical strip dirty.
|
// Advances all active LED effects and marks the strip dirty when something changed.
|
||||||
void updateBlink() {
|
void updateBlink() {
|
||||||
unsigned long nowMs = millis();
|
unsigned long nowMs = millis();
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
||||||
for (uint8_t j = 0; j < gaugeLedCount[i]; j++) {
|
for (uint8_t j = 0; j < gaugePins[i].ledCount; j++) {
|
||||||
uint8_t gi = gaugeLedOffset[i] + j;
|
uint8_t gi = gaugeLedOffset[i] + j;
|
||||||
BlinkState& bs = blinkState[gi];
|
BlinkState& bs = blinkState[gi];
|
||||||
if (!bs.active) continue;
|
if (!bs.active) continue;
|
||||||
@@ -1124,25 +1009,25 @@ void updateBlink() {
|
|||||||
if ((nowMs - bs.lastMs) >= period) {
|
if ((nowMs - bs.lastMs) >= period) {
|
||||||
bs.currentlyOn = !bs.currentlyOn;
|
bs.currentlyOn = !bs.currentlyOn;
|
||||||
bs.lastMs = nowMs;
|
bs.lastMs = nowMs;
|
||||||
writeLed(gi, bs.currentlyOn ? bs.onColor : CRGB::Black);
|
leds[gi] = bs.currentlyOn ? bs.onColor : CRGB::Black;
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FX_BREATHE: {
|
case FX_BREATHE: {
|
||||||
unsigned long dt = nowMs - bs.lastMs;
|
unsigned long dt = nowMs - bs.lastMs;
|
||||||
if (dt < BREATHE_FRAME_MS) break;
|
if (dt < 64) break;
|
||||||
uint32_t newPos = (uint32_t)bs.cyclePos + dt;
|
uint32_t newPos = (uint32_t)bs.cyclePos + dt;
|
||||||
bs.cyclePos = (uint16_t)(newPos % bs.periodMs);
|
bs.cyclePos = (uint16_t)(newPos % bs.periodMs);
|
||||||
bs.lastMs = nowMs;
|
bs.lastMs = nowMs;
|
||||||
// Triangle wave brightness; frame-limited so breathe remains smooth
|
// Cheap triangle wave. It does the job and nobody has complained yet.
|
||||||
// without refreshing the LED strips on every service-loop pass.
|
|
||||||
uint16_t half = bs.periodMs >> 1;
|
uint16_t half = bs.periodMs >> 1;
|
||||||
uint8_t bri = (bs.cyclePos < half)
|
uint8_t bri = (bs.cyclePos < half)
|
||||||
? (uint8_t)((uint32_t)bs.cyclePos * 255 / half)
|
? (uint8_t)((uint32_t)bs.cyclePos * 255 / half)
|
||||||
: (uint8_t)((uint32_t)(bs.periodMs - bs.cyclePos) * 255 / half);
|
: (uint8_t)((uint32_t)(bs.periodMs - bs.cyclePos) * 255 / half);
|
||||||
CRGB scaled = bs.onColor;
|
leds[gi] = bs.onColor;
|
||||||
scaled.nscale8(bri ? bri : 1);
|
leds[gi].nscale8(bri ? bri : 1);
|
||||||
writeLed(gi, scaled);
|
changed = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FX_DFLASH: {
|
case FX_DFLASH: {
|
||||||
@@ -1150,13 +1035,16 @@ void updateBlink() {
|
|||||||
if ((nowMs - bs.lastMs) >= dur[bs.dphase]) {
|
if ((nowMs - bs.lastMs) >= dur[bs.dphase]) {
|
||||||
bs.lastMs = nowMs;
|
bs.lastMs = nowMs;
|
||||||
bs.dphase = (bs.dphase + 1) & 3;
|
bs.dphase = (bs.dphase + 1) & 3;
|
||||||
writeLed(gi, (bs.dphase == 0 || bs.dphase == 2) ? bs.onColor : CRGB::Black);
|
leds[gi] = (bs.dphase == 0 || bs.dphase == 2) ? bs.onColor : CRGB::Black;
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changed) ledsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs the command parsers in order until one claims the line.
|
// Runs the command parsers in order until one claims the line.
|
||||||
@@ -1170,7 +1058,6 @@ void processLine(const String& line) {
|
|||||||
if (parseHome(line)) return;
|
if (parseHome(line)) return;
|
||||||
if (parseSweep(line)) return;
|
if (parseSweep(line)) return;
|
||||||
if (parsePosQuery(line)) return;
|
if (parsePosQuery(line)) return;
|
||||||
if (parseCfgQuery(line)) return;
|
|
||||||
if (parseLedQuery(line)) return;
|
if (parseLedQuery(line)) return;
|
||||||
if (parseLed(line)) return;
|
if (parseLed(line)) return;
|
||||||
if (parseBlink(line)) return;
|
if (parseBlink(line)) return;
|
||||||
@@ -1225,25 +1112,15 @@ void setup() {
|
|||||||
gauges[i].lastUpdateMicros = micros();
|
gauges[i].lastUpdateMicros = micros();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flatten the per-gauge LED counts into logical offsets and separate
|
// Flatten the per-gauge LED counts into offsets on the shared strip.
|
||||||
// physical offsets for the main and indicator strips.
|
|
||||||
uint8_t ledOff = 0;
|
uint8_t ledOff = 0;
|
||||||
uint8_t mainLedOff = 0;
|
|
||||||
uint8_t indicatorLedOff = 0;
|
|
||||||
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
for (uint8_t i = 0; i < GAUGE_COUNT; i++) {
|
||||||
gaugeLedCount[i] = cstrLen(gaugePins[i].ledOrder);
|
|
||||||
gaugeLedOffset[i] = ledOff;
|
gaugeLedOffset[i] = ledOff;
|
||||||
gaugeMainLedOffset[i] = mainLedOff;
|
ledOff += gaugePins[i].ledCount;
|
||||||
gaugeIndicatorLedOffset[i] = indicatorLedOff;
|
|
||||||
ledOff += gaugeLedCount[i];
|
|
||||||
indicatorLedOff += countIndicatorLedsForGauge(i);
|
|
||||||
mainLedOff += gaugeLedCount[i] - countIndicatorLedsForGauge(i);
|
|
||||||
}
|
}
|
||||||
mainLedController = &FastLED.addLeds<WS2812, LED_DATA_PIN, RGB>(mainLeds, TOTAL_MAIN_LEDS);
|
FastLED.addLeds<WS2812B, LED_DATA_PIN, GRB>(leds, TOTAL_LEDS);
|
||||||
indicatorLedController = &FastLED.addLeds<WS2812B, INDICATOR_LED_DATA_PIN, RGB>(indicatorLeds, TOTAL_INDICATOR_LEDS);
|
|
||||||
FastLED.setBrightness(255);
|
FastLED.setBrightness(255);
|
||||||
mainLedController->showLeds(255);
|
FastLED.show();
|
||||||
indicatorLedController->showLeds(255);
|
|
||||||
|
|
||||||
vfd::begin();
|
vfd::begin();
|
||||||
|
|
||||||
@@ -1264,7 +1141,10 @@ void loop() {
|
|||||||
updateGauge(i);
|
updateGauge(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
showDirtyLeds();
|
if (ledsDirty) {
|
||||||
|
FastLED.show();
|
||||||
|
ledsDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
README.md
52
README.md
@@ -7,7 +7,7 @@ A dedicated gauge controller for Arduinos.
|
|||||||
This repository contains:
|
This repository contains:
|
||||||
|
|
||||||
- `Gaugecontroller/Gaugecontroller.ino`: the Arduino Mega firmware for the stepper gauges, LEDs, and integrated HV5812-based VFD
|
- `Gaugecontroller/Gaugecontroller.ino`: the Arduino Mega firmware for the stepper gauges, LEDs, and integrated HV5812-based VFD
|
||||||
- `gaugecontroller.yaml`: the ESPHome-based ESP32 firmware that exposes the gauges and VFD to Home Assistant via the native API
|
- `gauge.py`: the ESP32 / MicroPython MQTT bridge that exposes the controller to Home Assistant
|
||||||
|
|
||||||
## VFD Support
|
## VFD Support
|
||||||
|
|
||||||
@@ -48,19 +48,16 @@ Rules:
|
|||||||
- shorter values are right-aligned
|
- shorter values are right-aligned
|
||||||
- leading zeroes are preserved if they are part of the input
|
- leading zeroes are preserved if they are part of the input
|
||||||
|
|
||||||
## Home Assistant Integration
|
## Home Assistant Entities
|
||||||
|
|
||||||
The ESPHome firmware in `gaugecontroller.yaml` exposes entities to Home Assistant via the native API:
|
The MQTT bridge publishes Home Assistant discovery entities for the VFD:
|
||||||
|
|
||||||
### Gauge Controls
|
- `VFD Display`
|
||||||
- Number entities for each gauge's target value (with unit conversion)
|
text entity for the displayed value
|
||||||
- Number entities for speed and acceleration (diagnostic)
|
- `VFD Decimal Point`
|
||||||
- Rezero buttons for each gauge and all gauges
|
switch entity
|
||||||
|
- `VFD Alarm`
|
||||||
### VFD Display
|
switch entity
|
||||||
- `VFD Display`: text entity for the displayed value
|
|
||||||
- `VFD Decimal Point`: switch entity
|
|
||||||
- `VFD Alarm`: switch entity
|
|
||||||
|
|
||||||
The display is intentionally exposed as a text entity rather than a numeric entity so that:
|
The display is intentionally exposed as a text entity rather than a numeric entity so that:
|
||||||
|
|
||||||
@@ -68,12 +65,27 @@ The display is intentionally exposed as a text entity rather than a numeric enti
|
|||||||
- hexadecimal values like `DEAD` or `BEEF` work
|
- hexadecimal values like `DEAD` or `BEEF` work
|
||||||
- clearing the display is possible with an empty value
|
- clearing the display is possible with an empty value
|
||||||
|
|
||||||
### LED Controls
|
## MQTT Topics
|
||||||
- RGB light entity for each gauge's backlight with effects (Blink, Breathe, Double Flash)
|
|
||||||
- Binary light entities for each gauge's red/green indicators and status lights
|
|
||||||
|
|
||||||
### Diagnostics
|
Using the configured `mqtt_prefix` from `config.json`, the VFD topics are:
|
||||||
- WiFi signal sensor
|
|
||||||
- Uptime sensor
|
- `<prefix>/vfd/set`
|
||||||
- IP address and SSID text sensors
|
- `<prefix>/vfd/state`
|
||||||
- Arduino Last Message sensor
|
- `<prefix>/vfd/decimal_point/set`
|
||||||
|
- `<prefix>/vfd/decimal_point/state`
|
||||||
|
- `<prefix>/vfd/alarm/set`
|
||||||
|
- `<prefix>/vfd/alarm/state`
|
||||||
|
|
||||||
|
Example with the default prefix `gauges`:
|
||||||
|
|
||||||
|
- `gauges/vfd/set`
|
||||||
|
- `gauges/vfd/decimal_point/set`
|
||||||
|
- `gauges/vfd/alarm/set`
|
||||||
|
|
||||||
|
Example payloads:
|
||||||
|
|
||||||
|
- publish `0123` to `gauges/vfd/set`
|
||||||
|
- publish `ON` to `gauges/vfd/decimal_point/set`
|
||||||
|
- publish `OFF` to `gauges/vfd/alarm/set`
|
||||||
|
|
||||||
|
The MQTT bridge then converts that into the correct Arduino serial command such as `VFD 0123.`.
|
||||||
|
|||||||
@@ -183,14 +183,13 @@ Then connect the motor side of that driver to:
|
|||||||
|
|
||||||
according to the driver board you are using.
|
according to the driver board you are using.
|
||||||
|
|
||||||
## 14. Wire The WS2812 LEDs
|
## 14. Wire The WS2812B LEDs
|
||||||
|
|
||||||
Connect:
|
Connect:
|
||||||
|
|
||||||
- `Mega D22` -> main backlight/status strip `DIN`
|
- `Mega D22` -> `WS2812B DIN`
|
||||||
- `Mega D36` -> indicator strip `DIN`
|
- `5V LED supply` -> `WS2812B 5V`
|
||||||
- `5V LED supply` -> both strip `5V` inputs
|
- `WS2812B GND` -> common ground rail
|
||||||
- both strip `GND` inputs -> common ground rail
|
|
||||||
|
|
||||||
If the LED chain is long or bright:
|
If the LED chain is long or bright:
|
||||||
|
|
||||||
|
|||||||
@@ -205,10 +205,9 @@ If `D8` and `D9` come from separate fly wires to the stripboard, keep them in th
|
|||||||
|
|
||||||
Route:
|
Route:
|
||||||
|
|
||||||
- `D22` -> main backlight/status strip `DIN`
|
- `D22` -> `WS2812 DIN`
|
||||||
- `D36` -> indicator strip `DIN`
|
- `5V` -> `WS2812 5V`
|
||||||
- `5V` -> both strip `5V` inputs
|
- `GND` -> `WS2812 GND`
|
||||||
- `GND` -> both strip `GND` inputs
|
|
||||||
|
|
||||||
Keep the LED connector in the low-voltage area.
|
Keep the LED connector in the low-voltage area.
|
||||||
|
|
||||||
|
|||||||
62
boot.py
Normal file
62
boot.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
boot.py — runs before main.py on every ESP32 boot
|
||||||
|
|
||||||
|
Connects WiFi, runs OTA update, then hands off to main.py.
|
||||||
|
Keep this file as simple as possible — it is never OTA-updated itself
|
||||||
|
(it lives outside the repo folder) so bugs here require USB to fix.
|
||||||
|
"""
|
||||||
|
#import gauge
|
||||||
|
import network
|
||||||
|
import gc
|
||||||
|
import utime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import ota
|
||||||
|
|
||||||
|
ota.load_config()
|
||||||
|
WIFI_SSID, WIFI_PASSWORD = ota.WIFI_SSID, ota.WIFI_PASSWORD
|
||||||
|
|
||||||
|
def _connect_wifi(timeout_s=20):
|
||||||
|
sta = network.WLAN(network.STA_IF)
|
||||||
|
sta.active(True)
|
||||||
|
sta.config(txpower=15)
|
||||||
|
if sta.isconnected():
|
||||||
|
return True
|
||||||
|
sta.connect(WIFI_SSID, WIFI_PASSWORD)
|
||||||
|
deadline = utime.time() + timeout_s
|
||||||
|
while not sta.isconnected():
|
||||||
|
if utime.time() > deadline:
|
||||||
|
return False
|
||||||
|
utime.sleep_ms(300)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if WIFI_SSID is None:
|
||||||
|
print("[boot] No WiFi credentials — cannot connect, skipping OTA")
|
||||||
|
elif _connect_wifi():
|
||||||
|
ip = network.WLAN(network.STA_IF).ifconfig()[0]
|
||||||
|
print(f"[boot] WiFi connected — {ip}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ota.update()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[boot] OTA error: {e} — continuing with existing files")
|
||||||
|
sys.print_exception(e)
|
||||||
|
utime.sleep_ms(5000)
|
||||||
|
ota._fetch_commit_sha = None
|
||||||
|
ota._fetch_manifest = None
|
||||||
|
ota._fetch_dir = None
|
||||||
|
ota._api_get = None
|
||||||
|
ota._download = None
|
||||||
|
ota.urequests = None
|
||||||
|
del ota.urequests
|
||||||
|
del ota
|
||||||
|
gc.collect()
|
||||||
|
del sys.modules["ota"]
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("[boot] WiFi failed — skipping OTA, booting with existing files")
|
||||||
|
|
||||||
|
# main.py runs automatically after boot.py
|
||||||
|
|
||||||
|
|
||||||
46
changes.md
46
changes.md
@@ -1,46 +0,0 @@
|
|||||||
# Changes
|
|
||||||
|
|
||||||
## 2026-04-27 — Arduino firmware refactor (`Gaugecontroller/Gaugecontroller.ino`)
|
|
||||||
|
|
||||||
### Non-blocking VFD multiplexer
|
|
||||||
`vfd::refresh()` previously held each digit for 2000 µs via `delayMicroseconds`,
|
|
||||||
which capped the effective stepper pulse rate at roughly 500 Hz regardless of
|
|
||||||
`maxSpeed`. It now tracks `phaseStartMicros`/`phaseActive` and returns
|
|
||||||
immediately while the digit is still being held; the main loop runs at
|
|
||||||
microsecond cadence again and the configured `maxSpeed = 4000.0f` steps/s is
|
|
||||||
actually achievable.
|
|
||||||
|
|
||||||
### Fixed-buffer command parser (no more `String` heap churn)
|
|
||||||
Replaced `String rxLine` with `char rxBuf[128]` and converted the entire
|
|
||||||
command pipeline to take `const char*`:
|
|
||||||
|
|
||||||
- `processLine`, `sendReply`, `vfd::parseCommand`
|
|
||||||
- All `parse*` functions: `parseSet`, `parseSpeed`, `parseAccel`, `parseEnable`,
|
|
||||||
`parseZero`, `parseHome`, `parseSweep`, `parsePosQuery`, `parseCfgQuery`,
|
|
||||||
`parseLedQuery`, `parseLed`, `parseBlink`, `parseBreathe`, `parseDflash`,
|
|
||||||
`parseVfd`, `parsePing`.
|
|
||||||
|
|
||||||
`parseSpeed` / `parseAccel` / `parseSweep` use `strncmp` + `atof` because the
|
|
||||||
default AVR-libc `sscanf` doesn't support `%f`. No allocations on the command
|
|
||||||
path; the Mega's heap no longer fragments over time.
|
|
||||||
|
|
||||||
### Cached `ledNeedsSwap[TOTAL_LEDS]`
|
|
||||||
Per-LED RGB-vs-GRB swap flag is now precomputed once in `setup()` from
|
|
||||||
`gaugePins[].ledOrder`. `encodeForStrip` is a single array index instead of
|
|
||||||
walking the gauge table on every LED read/write.
|
|
||||||
|
|
||||||
### Cached step direction per gauge
|
|
||||||
Added `Gauge.lastDir`. `setDir()` skips the DIR-pin `digitalWrite` when the
|
|
||||||
direction hasn't flipped (the common case during a step run) and adds a 1 µs
|
|
||||||
DIR-to-STEP setup delay only when it actually flips.
|
|
||||||
|
|
||||||
### Cleanups
|
|
||||||
- Removed the `absf` helper; use `fabsf` consistently.
|
|
||||||
- Removed the `+ 0.0001f` epsilon in the trapezoidal braking-distance divisor.
|
|
||||||
`parseAccel` already rejects `accel <= 0`, so the divisor is always positive.
|
|
||||||
- Fixed the `<r> <ig> <b>` typo to `<r> <g> <b>` in the protocol comment for
|
|
||||||
`DFLASH`.
|
|
||||||
|
|
||||||
### Build verification
|
|
||||||
`arduino-cli compile --fqbn arduino:avr:mega Gaugecontroller`:
|
|
||||||
17758 B flash (6%), 1845 B SRAM (22%).
|
|
||||||
@@ -28,16 +28,6 @@ import gc
|
|||||||
from umqtt.robust import MQTTClient
|
from umqtt.robust import MQTTClient
|
||||||
from machine import UART
|
from machine import UART
|
||||||
|
|
||||||
# Activate WiFi driver before any heavy heap allocation so it can claim its
|
|
||||||
# contiguous DRAM block before the Python heap fragments the address space.
|
|
||||||
# Only activate if not already running (e.g. boot.py may have started it).
|
|
||||||
gc.collect()
|
|
||||||
_early_wlan = network.WLAN(network.STA_IF)
|
|
||||||
if not _early_wlan.active():
|
|
||||||
_early_wlan.active(True)
|
|
||||||
del _early_wlan
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -161,19 +151,27 @@ ARDUINO_TX_PIN = int(_cfg.get("arduino_tx_pin", 17))
|
|||||||
ARDUINO_RX_PIN = int(_cfg.get("arduino_rx_pin", 16))
|
ARDUINO_RX_PIN = int(_cfg.get("arduino_rx_pin", 16))
|
||||||
ARDUINO_BAUD = int(_cfg.get("arduino_baud", 115200))
|
ARDUINO_BAUD = int(_cfg.get("arduino_baud", 115200))
|
||||||
|
|
||||||
_arduino = UART(ARDUINO_UART_ID, baudrate=ARDUINO_BAUD, tx=ARDUINO_TX_PIN, rx=ARDUINO_RX_PIN, timeout=10)
|
_arduino = None
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_arduino():
|
||||||
|
global _arduino
|
||||||
|
if _arduino is None:
|
||||||
|
_arduino = UART(ARDUINO_UART_ID, baudrate=ARDUINO_BAUD, tx=ARDUINO_TX_PIN, rx=ARDUINO_RX_PIN, timeout=10)
|
||||||
|
return _arduino
|
||||||
|
|
||||||
|
|
||||||
def arduino_send(cmd):
|
def arduino_send(cmd):
|
||||||
"""Send a newline-terminated command to the Arduino."""
|
"""Send a newline-terminated command to the Arduino."""
|
||||||
_arduino.write((cmd + "\n").encode())
|
_ensure_arduino().write((cmd + "\n").encode())
|
||||||
info(f"Arduino → {cmd}")
|
info(f"Arduino → {cmd}")
|
||||||
|
|
||||||
|
|
||||||
def arduino_recv():
|
def arduino_recv():
|
||||||
"""Print any lines waiting in the Arduino RX buffer."""
|
"""Print any lines waiting in the Arduino RX buffer."""
|
||||||
while _arduino.any():
|
uart = _ensure_arduino()
|
||||||
line = _arduino.readline()
|
while uart.any():
|
||||||
|
line = uart.readline()
|
||||||
if line:
|
if line:
|
||||||
print(f"[{_ts()}] ARDU {line.decode().strip()}")
|
print(f"[{_ts()}] ARDU {line.decode().strip()}")
|
||||||
|
|
||||||
@@ -540,11 +538,14 @@ _WIFI_CONNECT_ATTEMPTS = 3
|
|||||||
def _reset_wifi_interface():
|
def _reset_wifi_interface():
|
||||||
global _wifi_sta
|
global _wifi_sta
|
||||||
_wifi_sta = network.WLAN(network.STA_IF)
|
_wifi_sta = network.WLAN(network.STA_IF)
|
||||||
if _wifi_sta.active():
|
if not _wifi_sta.active():
|
||||||
_wifi_sta.active(False)
|
_wifi_sta.active(True)
|
||||||
utime.sleep_ms(200)
|
utime.sleep_ms(500)
|
||||||
_wifi_sta.active(True)
|
try:
|
||||||
utime.sleep_ms(500)
|
_wifi_sta.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
utime.sleep_ms(1000)
|
||||||
|
|
||||||
|
|
||||||
def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False):
|
def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False):
|
||||||
@@ -561,7 +562,8 @@ def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False):
|
|||||||
last_error = None
|
last_error = None
|
||||||
for attempt in range(_WIFI_CONNECT_ATTEMPTS):
|
for attempt in range(_WIFI_CONNECT_ATTEMPTS):
|
||||||
info(f"WiFi connecting to '{ssid}' (attempt {attempt + 1}/{_WIFI_CONNECT_ATTEMPTS}) ...")
|
info(f"WiFi connecting to '{ssid}' (attempt {attempt + 1}/{_WIFI_CONNECT_ATTEMPTS}) ...")
|
||||||
_reset_wifi_interface()
|
if not _wifi_sta.isconnected():
|
||||||
|
_reset_wifi_interface()
|
||||||
try:
|
try:
|
||||||
_wifi_sta.connect(ssid, password)
|
_wifi_sta.connect(ssid, password)
|
||||||
deadline = utime.time() + timeout_s
|
deadline = utime.time() + timeout_s
|
||||||
@@ -576,7 +578,7 @@ def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False):
|
|||||||
info(f" SSID : {ssid}")
|
info(f" SSID : {ssid}")
|
||||||
info(f" MAC : {mac}")
|
info(f" MAC : {mac}")
|
||||||
info(f" IP : {ip} mask:{mask} gw:{gw} dns:{dns}")
|
info(f" IP : {ip} mask:{mask} gw:{gw} dns:{dns}")
|
||||||
utime.sleep_ms(500)
|
utime.sleep_ms(2000)
|
||||||
return ip
|
return ip
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
last_error = e
|
last_error = e
|
||||||
@@ -601,7 +603,7 @@ def check_wifi():
|
|||||||
|
|
||||||
log_err("WiFi lost connection — attempting reconnect...")
|
log_err("WiFi lost connection — attempting reconnect...")
|
||||||
try:
|
try:
|
||||||
ip = connect_wifi(WIFI_SSID, WIFI_PASSWORD, timeout_s=15, force_reconnect=True)
|
ip = connect_wifi(WIFI_SSID, WIFI_PASSWORD, timeout_s=15)
|
||||||
info(f"WiFi reconnected! IP:{ip}")
|
info(f"WiFi reconnected! IP:{ip}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_err(f"WiFi reconnect failed: {e}")
|
log_err(f"WiFi reconnect failed: {e}")
|
||||||
@@ -911,10 +913,6 @@ def connect_mqtt():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
last_error = e
|
last_error = e
|
||||||
log_err(f"MQTT connect attempt {attempt + 1} failed: {type(e).__name__}: {e}")
|
log_err(f"MQTT connect attempt {attempt + 1} failed: {type(e).__name__}: {e}")
|
||||||
try:
|
|
||||||
client.sock.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
gc.collect()
|
gc.collect()
|
||||||
utime.sleep_ms(1000)
|
utime.sleep_ms(1000)
|
||||||
|
|
||||||
@@ -922,27 +920,6 @@ def connect_mqtt():
|
|||||||
raise last_error
|
raise last_error
|
||||||
|
|
||||||
|
|
||||||
_mqtt_check_interval_ms = 30000
|
|
||||||
_last_mqtt_check = 0
|
|
||||||
_discovery_queue = []
|
|
||||||
_discovery_idx = 0
|
|
||||||
_last_discovery_ms = 0
|
|
||||||
_DISCOVERY_INTERVAL_MS = 350
|
|
||||||
|
|
||||||
|
|
||||||
def _compact_discovery_payload(payload):
|
|
||||||
"""Trim optional HA discovery fields when RAM is tight."""
|
|
||||||
compact = dict(payload)
|
|
||||||
|
|
||||||
# Light entities are the largest payloads because they repeat effect metadata.
|
|
||||||
# Keep core functionality, but omit optional effect declarations to reduce heap use.
|
|
||||||
if compact.get("schema") == "json":
|
|
||||||
compact.pop("effect", None)
|
|
||||||
compact.pop("effect_list", None)
|
|
||||||
|
|
||||||
return compact
|
|
||||||
|
|
||||||
|
|
||||||
def check_mqtt():
|
def check_mqtt():
|
||||||
global client_ref, _mqtt_connected, _last_mqtt_check
|
global client_ref, _mqtt_connected, _last_mqtt_check
|
||||||
now = utime.ticks_ms()
|
now = utime.ticks_ms()
|
||||||
@@ -984,10 +961,6 @@ def check_mqtt():
|
|||||||
return True
|
return True
|
||||||
except Exception as e2:
|
except Exception as e2:
|
||||||
log_err(f"MQTT reconnect attempt {attempt + 1} failed: {e2}")
|
log_err(f"MQTT reconnect attempt {attempt + 1} failed: {e2}")
|
||||||
try:
|
|
||||||
client_ref.sock.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
gc.collect()
|
gc.collect()
|
||||||
utime.sleep_ms(2000)
|
utime.sleep_ms(2000)
|
||||||
|
|
||||||
@@ -995,9 +968,17 @@ def check_mqtt():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
_mqtt_check_interval_ms = 30000
|
||||||
|
_last_mqtt_check = 0
|
||||||
|
_discovery_queue = []
|
||||||
|
_discovery_idx = 0
|
||||||
|
_last_discovery_ms = 0
|
||||||
|
_DISCOVERY_INTERVAL_MS = 350
|
||||||
|
|
||||||
|
|
||||||
def _publish_discovery_entity(client, topic, payload, log_msg):
|
def _publish_discovery_entity(client, topic, payload, log_msg):
|
||||||
gc.collect()
|
gc.collect()
|
||||||
client.publish(topic, ujson.dumps(_compact_discovery_payload(payload)), retain=True)
|
client.publish(topic, ujson.dumps(payload), retain=True)
|
||||||
info(log_msg)
|
info(log_msg)
|
||||||
|
|
||||||
|
|
||||||
@@ -1305,12 +1286,42 @@ def apply_motion_defaults():
|
|||||||
send_vfd_state()
|
send_vfd_state()
|
||||||
|
|
||||||
|
|
||||||
|
def _restore_led_states():
|
||||||
|
for i in range(num_gauges):
|
||||||
|
gt = gauge_topics[i]
|
||||||
|
info(f" red={_red_effect[i]} green={_green_effect[i]} status_r={_status_red_effect[i]} status_g={_status_green_effect[i]}")
|
||||||
|
for led_key, led_idx, color, effect_arr, state_topic in [
|
||||||
|
("red", _LED_RED, gauges[i]["ws2812_red"], _red_effect, gt["led_red_state"]),
|
||||||
|
("green", _LED_GREEN, gauges[i]["ws2812_green"], _green_effect, gt["led_green_state"]),
|
||||||
|
("status_red", _LED_STATUS_RED, gauges[i]["ws2812_red"], _status_red_effect, gt["status_red_state"]),
|
||||||
|
("status_green", _LED_STATUS_GREEN, gauges[i]["ws2812_green"], _status_green_effect, gt["status_green_state"]),
|
||||||
|
]:
|
||||||
|
if effect_arr[i]:
|
||||||
|
pub = {"state": "ON", "effect": effect_arr[i]}
|
||||||
|
_publish(state_topic, ujson.dumps(pub), retain=True)
|
||||||
|
if _red_effect[i]:
|
||||||
|
_apply_blink_or_led(i, _LED_RED, gauges[i]["ws2812_red"], _red_effect[i])
|
||||||
|
if _green_effect[i]:
|
||||||
|
_apply_blink_or_led(i, _LED_GREEN, gauges[i]["ws2812_green"], _green_effect[i])
|
||||||
|
if _status_red_effect[i]:
|
||||||
|
_apply_blink_or_led(i, _LED_STATUS_RED, gauges[i]["ws2812_red"], _status_red_effect[i])
|
||||||
|
if _status_green_effect[i]:
|
||||||
|
_apply_blink_or_led(i, _LED_STATUS_GREEN, gauges[i]["ws2812_green"], _status_green_effect[i])
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Main
|
# Main
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
gc.collect()
|
||||||
|
_w = network.WLAN(network.STA_IF)
|
||||||
|
if not _w.active():
|
||||||
|
_w.active(True)
|
||||||
|
del _w
|
||||||
|
gc.collect()
|
||||||
|
_ensure_arduino()
|
||||||
gc.collect()
|
gc.collect()
|
||||||
info("=" * 48)
|
info("=" * 48)
|
||||||
info("Gauge MQTT controller starting")
|
info("Gauge MQTT controller starting")
|
||||||
@@ -1318,7 +1329,7 @@ def main():
|
|||||||
info("=" * 48)
|
info("=" * 48)
|
||||||
|
|
||||||
gc.collect()
|
gc.collect()
|
||||||
connect_wifi(WIFI_SSID, WIFI_PASSWORD, force_reconnect=True)
|
connect_wifi(WIFI_SSID, WIFI_PASSWORD)
|
||||||
|
|
||||||
mqtt_attempts = 0
|
mqtt_attempts = 0
|
||||||
while True:
|
while True:
|
||||||
@@ -1331,13 +1342,14 @@ def main():
|
|||||||
if mqtt_attempts % 3 == 0:
|
if mqtt_attempts % 3 == 0:
|
||||||
log_err("WiFi may be stale — forcing reconnect...")
|
log_err("WiFi may be stale — forcing reconnect...")
|
||||||
try:
|
try:
|
||||||
connect_wifi(WIFI_SSID, WIFI_PASSWORD, force_reconnect=True)
|
connect_wifi(WIFI_SSID, WIFI_PASSWORD)
|
||||||
except Exception as we:
|
except Exception as we:
|
||||||
log_err(f"WiFi reconnect failed: {we}")
|
log_err(f"WiFi reconnect failed: {we}")
|
||||||
utime.sleep_ms(5000)
|
utime.sleep_ms(5000)
|
||||||
_subscribe_all(client_ref)
|
_subscribe_all(client_ref)
|
||||||
schedule_discovery()
|
schedule_discovery()
|
||||||
|
|
||||||
|
publish_backlight_states(client_ref)
|
||||||
apply_motion_defaults()
|
apply_motion_defaults()
|
||||||
info("Draining initial retained messages...")
|
info("Draining initial retained messages...")
|
||||||
for _ in range(50):
|
for _ in range(50):
|
||||||
@@ -1350,6 +1362,10 @@ def main():
|
|||||||
gauge_last_rezero[i] = utime.ticks_ms()
|
gauge_last_rezero[i] = utime.ticks_ms()
|
||||||
info("Home command sent")
|
info("Home command sent")
|
||||||
|
|
||||||
|
utime.sleep_ms(100)
|
||||||
|
_restore_led_states()
|
||||||
|
info("LED effects restored")
|
||||||
|
|
||||||
info("Publishing state...")
|
info("Publishing state...")
|
||||||
publish_online(client_ref)
|
publish_online(client_ref)
|
||||||
publish_state(client_ref)
|
publish_state(client_ref)
|
||||||
2394
gaugecontroller.yaml
2394
gaugecontroller.yaml
File diff suppressed because it is too large
Load Diff
17
wiring.md
17
wiring.md
@@ -163,22 +163,19 @@ Also connect:
|
|||||||
|
|
||||||
If your driver boards need separate motor power, supply that from the proper motor supply. Do not power motors from the Mega `5V` pin.
|
If your driver boards need separate motor power, supply that from the proper motor supply. Do not power motors from the Mega `5V` pin.
|
||||||
|
|
||||||
## WS2812 LED Strips
|
## WS2812B LED Strip
|
||||||
|
|
||||||
The current sketch expects two LED data chains. Backlight and status LEDs stay
|
The current sketch expects one shared WS2812B chain.
|
||||||
on the main strip; the red/green dial indicator LEDs are on their own strip.
|
|
||||||
|
|
||||||
| Mega Pin | LED Strip |
|
| Mega Pin | WS2812B |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `D22` | main backlight/status `DIN` |
|
| `D22` | `DIN` |
|
||||||
| `D36` | indicator `DIN` |
|
| `5V` | `5V` |
|
||||||
| `5V` | both strips `5V` |
|
| `GND` | `GND` |
|
||||||
| `GND` | both strips `GND` |
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- the command protocol still exposes `7 LEDs per gauge`
|
- the code expects `7 LEDs per gauge`, so `21 LEDs total`
|
||||||
- logical indices `0-2` are backlight, `3-4` are indicators, and `5-6` are status
|
|
||||||
- use a proper 5V supply sized for the LED current
|
- use a proper 5V supply sized for the LED current
|
||||||
- keep LED ground common with the Mega
|
- keep LED ground common with the Mega
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user