From 8a72a4e984cdd50d5f1836ec3a21d2f5847777d5 Mon Sep 17 00:00:00 2001 From: Jonathan Giles Date: Fri, 13 Feb 2026 11:15:46 +1300 Subject: [PATCH 1/2] Support for booting up and running on eSpa v2 boards (with ESP32-C6-WROOM) --- .vscode/extensions.json | 2 + lib/MultiBlinker/MultiBlinker.cpp | 316 +++++++++++++++++++++++++++++- lib/MultiBlinker/MultiBlinker.h | 99 ++++++++-- lib/SpaInterface/SpaInterface.cpp | 19 ++ lib/SpaInterface/SpaInterface.h | 11 ++ platformio.ini | 35 +++- src/main.cpp | 40 +++- 7 files changed, 495 insertions(+), 27 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 8057bc7..5c5735c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,4 +1,6 @@ { + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format "recommendations": [ "pioarduino.pioarduino-ide", "platformio.platformio-ide" diff --git a/lib/MultiBlinker/MultiBlinker.cpp b/lib/MultiBlinker/MultiBlinker.cpp index 9cbf377..717ac00 100644 --- a/lib/MultiBlinker/MultiBlinker.cpp +++ b/lib/MultiBlinker/MultiBlinker.cpp @@ -2,12 +2,12 @@ // Define the on/off times for each state (-1 to 15) const LEDPattern LED_PATTERNS[17] = { - {2000, 2000}, //KNIGHT_RIDER + {2000, 2000}, // KNIGHT_RIDER: rainbow cycle {UINT_MAX, 0}, // STATE_NONE: Always off - {100, 100}, // STATE_WIFI_NOT_CONNECTED - {1000, 1000}, // STATE_WAITING_FOR_SPA + {100, 100}, // STATE_WIFI_NOT_CONNECTED: Red fast blink + {1000, 1000}, // STATE_WAITING_FOR_SPA: Yellow slow blink {0, 0}, // Reserved - {500, 500}, // STATE_MQTT_NOT_CONNECTED + {500, 500}, // STATE_MQTT_NOT_CONNECTED: Purple medium blink {0, 0}, // Reserved {0, 0}, // Reserved {0, 0}, // Reserved @@ -18,9 +18,313 @@ const LEDPattern LED_PATTERNS[17] = { {0, 0}, // Reserved {0, 0}, // Reserved {0, 0}, // Reserved - {0, UINT_MAX} // STATE_STARTED_WIFI_AP: Always on + {0, UINT_MAX} // STATE_STARTED_WIFI_AP: Always on (Blue) }; +#ifdef USE_RGB_LED + +// RGB LED implementation for ESPA_V2 + +MultiBlinker::MultiBlinker(int rgbPin) : rgbPin(rgbPin), pixel(nullptr) { + // Defer pixel initialization to start() - constructor runs before Arduino setup() +} + +uint32_t MultiBlinker::applyBrightness(uint8_t r, uint8_t g, uint8_t b, uint8_t brightness) { + // Scale RGB values by brightness (0-255) + return pixel->Color( + (r * brightness) / 255, + (g * brightness) / 255, + (b * brightness) / 255 + ); +} + +AnimationType MultiBlinker::getAnimationForState(int state) { + switch (state) { + case KNIGHT_RIDER: + return ANIM_RAINBOW; + case STATE_NONE: + return ANIM_SOLID; // Off + case STATE_WIFI_NOT_CONNECTED: + return ANIM_HEARTBEAT; // Urgent attention - heartbeat red + case STATE_WAITING_FOR_SPA: + return ANIM_BREATHE; // Waiting - gentle breathing yellow + case STATE_MQTT_NOT_CONNECTED: + return ANIM_SPARKLE; // Connection issue - sparkle purple + case STATE_STARTED_WIFI_AP: + return ANIM_COLOR_BLEND; // AP mode - blend blue/cyan + default: + return ANIM_SOLID; + } +} + +RGBColor MultiBlinker::getColorForState(int state) { + switch (state) { + case KNIGHT_RIDER: + return RGB_CYAN; // Placeholder, rainbow cycle handles this + case STATE_NONE: + return RGB_OFF; + case STATE_WIFI_NOT_CONNECTED: + return RGB_RED; + case STATE_WAITING_FOR_SPA: + return RGB_YELLOW; + case STATE_MQTT_NOT_CONNECTED: + return RGB_PURPLE; + case STATE_STARTED_WIFI_AP: + return RGB_BLUE; + default: + return RGB_OFF; + } +} + +void MultiBlinker::updateRGB() { + if (pixel == nullptr) return; + + // Handle STATE_NONE - turn off + if (currentState == STATE_NONE) { + pixel->setPixelColor(0, 0, 0, 0); + pixel->show(); + return; + } + + // Get animation type for current state and dispatch + AnimationType anim = getAnimationForState(currentState); + + switch (anim) { + case ANIM_RAINBOW: + rainbowCycle(); + break; + case ANIM_BREATHE: + breatheEffect(); + break; + case ANIM_HEARTBEAT: + heartbeatEffect(); + break; + case ANIM_COLOR_BLEND: + colorBlendEffect(); + break; + case ANIM_SPARKLE: + sparkleEffect(); + break; + case ANIM_SOLID: + case ANIM_BLINK: + default: { + // Simple solid color with brightness control + RGBColor color = getColorForState(currentState); + uint32_t c = applyBrightness(color.r, color.g, color.b, MAX_BRIGHTNESS); + pixel->setPixelColor(0, c); + pixel->show(); + break; + } + } +} + +void MultiBlinker::rainbowCycle() { + if (pixel == nullptr) return; + + // Smooth rainbow using 16-bit hue with gamma correction + rainbowHue += HUE_STEP; + + // ColorHSV with reduced brightness, gamma-corrected for smoother perception + uint32_t color = pixel->ColorHSV(rainbowHue, 255, MAX_BRIGHTNESS); + color = pixel->gamma32(color); + pixel->setPixelColor(0, color); + pixel->show(); +} + +void MultiBlinker::breatheEffect() { + if (pixel == nullptr) return; + + RGBColor color = getColorForState(currentState); + + // Smooth breathing: 2 second full cycle (100 frames at 20ms) + animPhase += 655; // 65536 / 100 = ~655 for 2 second cycle + + // Smooth sine-like curve using 16-bit precision + uint16_t phase = animPhase; + uint8_t breath; + if (phase < 32768) { + // Rising phase: 0 to 255 + breath = (phase * 255) / 32768; + } else { + // Falling phase: 255 to 0 + breath = ((65535 - phase) * 255) / 32768; + } + + // Apply gamma curve for more natural breathing perception + breath = (breath * breath) / 255; // Simple gamma approximation + + // Scale to max brightness + breath = (breath * MAX_BRIGHTNESS) / 255; + + uint32_t c = applyBrightness(color.r, color.g, color.b, breath); + pixel->setPixelColor(0, c); + pixel->show(); +} + +void MultiBlinker::heartbeatEffect() { + if (pixel == nullptr) return; + + RGBColor color = getColorForState(currentState); + + // Heartbeat: bump-bump...pause (~1.5 second cycle) + animPhase += 875; // ~1.5 second cycle + + // Divide cycle into 8 segments + uint8_t segment = animPhase >> 13; // 0-7 + uint16_t segmentPhase = (animPhase & 0x1FFF); // Position within segment (0-8191) + uint8_t brightness = 0; + + switch (segment) { + case 0: // First beat rise + brightness = (segmentPhase * 255) / 8192; + break; + case 1: // First beat fall + brightness = 255 - ((segmentPhase * 255) / 8192); + break; + case 2: // Second beat rise + brightness = (segmentPhase * 255) / 8192; + break; + case 3: // Second beat fall + brightness = 255 - ((segmentPhase * 255) / 8192); + break; + default: // Pause (segments 4-7) + brightness = 0; + break; + } + + brightness = (brightness * MAX_BRIGHTNESS) / 255; + + uint32_t c = applyBrightness(color.r, color.g, color.b, brightness); + pixel->setPixelColor(0, c); + pixel->show(); +} + +void MultiBlinker::colorBlendEffect() { + if (pixel == nullptr) return; + + // Blend between blue and cyan for AP mode + RGBColor color1 = RGB_BLUE; + RGBColor color2 = RGB_CYAN; + + animPhase += 400; // ~3 second blend cycle + + // Smooth triangle wave using 16-bit precision + uint16_t phase = animPhase; + uint8_t blend; + if (phase < 32768) { + blend = (phase * 255) / 32768; + } else { + blend = ((65535 - phase) * 255) / 32768; + } + + // Interpolate between colors + uint8_t r = color1.r + ((int)(color2.r - color1.r) * blend / 255); + uint8_t g = color1.g + ((int)(color2.g - color1.g) * blend / 255); + uint8_t b = color1.b + ((int)(color2.b - color1.b) * blend / 255); + + uint32_t c = applyBrightness(r, g, b, MAX_BRIGHTNESS); + pixel->setPixelColor(0, c); + pixel->show(); +} + +void MultiBlinker::sparkleEffect() { + if (pixel == nullptr) return; + + RGBColor color = getColorForState(currentState); + + // Random sparkle with base glow + animPhase++; + + // Base brightness with random sparkle overlay + uint8_t baseBrightness = MAX_BRIGHTNESS / 3; // Dim base + uint8_t sparkle = random(0, MAX_BRIGHTNESS); + + // Occasionally do a bright sparkle + uint8_t brightness; + if (random(100) < 15) { // 15% chance of sparkle + brightness = max(baseBrightness, sparkle); + } else { + brightness = baseBrightness + random(0, MAX_BRIGHTNESS / 4); + } + + if (brightness > MAX_BRIGHTNESS) brightness = MAX_BRIGHTNESS; + + uint32_t c = applyBrightness(color.r, color.g, color.b, brightness); + pixel->setPixelColor(0, c); + pixel->show(); +} + +void MultiBlinker::setState(int state) { + if (rgbPin == -1 || state < -1 || state >= 16) { + return; + } + if (state == currentState) { + return; + } + debugD("Changing RGB LED state to: %d\n", state); + currentState = state; +} + +void MultiBlinker::start() { + if (rgbPin == -1) { + return; + } + // Initialize NeoPixel here (deferred from constructor for ESP32-C6 compatibility) + if (pixel == nullptr) { + pixel = new Adafruit_NeoPixel(1, rgbPin, NEO_GRB + NEO_KHZ800); + pixel->begin(); + pixel->setBrightness(50); // 0-255, start at ~20% brightness + pixel->setPixelColor(0, 0, 0, 0); + pixel->show(); + } + running = true; + xTaskCreate(runTask, "MultiBlinkerTask", 4096, this, 1, &taskHandle); +} + +void MultiBlinker::stop() { + if (pixel == nullptr) { + return; + } + running = false; + if (taskHandle != NULL) { + vTaskDelete(taskHandle); + taskHandle = NULL; + } + // Turn off LED when stopped + pixel->setPixelColor(0, 0, 0, 0); + pixel->show(); +} + +void MultiBlinker::runTask(void *pvParameters) { + MultiBlinker *blinker = static_cast(pvParameters); + blinker->run(); +} + +void MultiBlinker::run() { + while (running) { + // Use fast 20ms interval for smooth animations (~50fps) + const int interval = 20; + + if (millis() - lastUpdate >= interval) { + updateRGB(); + lastUpdate = millis(); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void MultiBlinker::updateLEDs() { + updateRGB(); +} + +void MultiBlinker::knightRider() { + rainbowCycle(); +} + +#else + +// Original multi-LED implementation for ESPA_V1 and other boards + MultiBlinker::MultiBlinker(int led1, int led2, int led3, int led4) { ledPins[0] = led1; ledPins[1] = led2; @@ -118,3 +422,5 @@ void MultiBlinker::knightRider() { vTaskDelay(150 / portTICK_PERIOD_MS); } } + +#endif // USE_RGB_LED diff --git a/lib/MultiBlinker/MultiBlinker.h b/lib/MultiBlinker/MultiBlinker.h index 7f0e37e..f292492 100644 --- a/lib/MultiBlinker/MultiBlinker.h +++ b/lib/MultiBlinker/MultiBlinker.h @@ -1,35 +1,85 @@ #ifndef MULTIBLINKER_H #define MULTIBLINKER_H +/** + * @file MultiBlinker.h + * @brief LED status indicator for eSpa hardware variants. + * + * This library provides visual status feedback through LEDs: + * + * - ESPA_V1 (ESP32-S3): Four separate red LEDs in Knight Rider pattern + * - ESPA_V2 (ESP32-C6): Single WS2812 RGB NeoPixel with animated effects + * - Generic boards: Single LED on LED_PIN + * + * ESPA_V2 RGB LED Animations: + * - KNIGHT_RIDER: Smooth rainbow spectrum cycle + * - STATE_WIFI_NOT_CONNECTED: Red heartbeat (urgent) + * - STATE_WAITING_FOR_SPA: Yellow breathing (waiting) + * - STATE_MQTT_NOT_CONNECTED: Purple sparkle (connection issue) + * - STATE_STARTED_WIFI_AP: Blue/cyan color blend (AP mode) + * + * @note On ESP32-C6, NeoPixel initialization is deferred to start() method + * because global constructors run before Arduino framework is ready. + */ + #include #include +#if defined(ESPA_V2) && defined(RGB_LED_PIN) + #include + #define USE_RGB_LED +#endif + extern RemoteDebug Debug; -// These are the four LEDs on the PCB +// These are the four LEDs on the PCB (for ESPA_V1 and legacy boards) #if defined(ESPA_V1) const int PCB_LED1 = 14; const int PCB_LED2 = 41; const int PCB_LED3 = 42; const int PCB_LED4 = -1; // GPIO 43 conflicts with USB on ESP32-S3, disabled -#elif defined(ESPA_V2) - const int PCB_LED1 = 18; - const int PCB_LED2 = 21; - const int PCB_LED3 = 22; - const int PCB_LED4 = 23; +#elif !defined(ESPA_V2) + // Generic ESP32 dev boards - no PCB LEDs defined #endif // Define error state constants // LED State: -const int KNIGHT_RIDER = -1; // Knight Rider animation or 2000ms blink -const int STATE_NONE = 0; // ON: (nothing) -const int STATE_STARTED_WIFI_AP = 15; // ON: ALL or solid on -const int STATE_WIFI_NOT_CONNECTED = 1; // ON: LED 4 or 100ms blink -const int STATE_WAITING_FOR_SPA = 2; // ON: LED 3 or 1000ms blink -const int STATE_MQTT_NOT_CONNECTED = 4; // ON: LED 2 or 500ms blink +const int KNIGHT_RIDER = -1; // Knight Rider animation or rainbow cycle +const int STATE_NONE = 0; // OFF (nothing) +const int STATE_STARTED_WIFI_AP = 15; // Blue solid (AP mode active) +const int STATE_WIFI_NOT_CONNECTED = 1; // Red fast blink (100ms) +const int STATE_WAITING_FOR_SPA = 2; // Yellow slow blink (1000ms) +const int STATE_MQTT_NOT_CONNECTED = 4; // Purple medium blink (500ms) const int MULTI_BLINKER_INTERVAL = 100; +// RGB Color structure for addressable LEDs +struct RGBColor { + uint8_t r; + uint8_t g; + uint8_t b; +}; + +// Color definitions for RGB LED states +const RGBColor RGB_OFF = {0, 0, 0}; +const RGBColor RGB_RED = {255, 0, 0}; // WiFi not connected +const RGBColor RGB_YELLOW = {255, 180, 0}; // Waiting for spa +const RGBColor RGB_BLUE = {0, 0, 255}; // WiFi AP mode +const RGBColor RGB_PURPLE = {180, 0, 255}; // MQTT not connected +const RGBColor RGB_GREEN = {0, 255, 0}; // All good (for future use) +const RGBColor RGB_CYAN = {0, 255, 255}; // Rainbow cycle marker + +// Animation types for RGB LED +enum AnimationType { + ANIM_SOLID, // Static color + ANIM_BLINK, // Simple on/off blink + ANIM_BREATHE, // Smooth fade in/out (breathing) + ANIM_HEARTBEAT, // Two quick pulses then pause + ANIM_COLOR_BLEND, // Smooth transition between two colors + ANIM_SPARKLE, // Random brightness variations + ANIM_RAINBOW // Full spectrum cycle +}; + struct LEDPattern { u_int offTime; // Time in milliseconds the LED is off u_int onTime; // Time in milliseconds the LED is on @@ -37,7 +87,11 @@ struct LEDPattern { class MultiBlinker { public: +#ifdef USE_RGB_LED + MultiBlinker(int rgbPin); +#else MultiBlinker(int led1 = -1, int led2 = -1, int led3 = -1, int led4 = -1); +#endif void setState(int state); void start(); void stop(); @@ -48,8 +102,29 @@ class MultiBlinker { void updateLEDs(); void knightRider(); +#ifdef USE_RGB_LED + Adafruit_NeoPixel *pixel; + int rgbPin; + void updateRGB(); + void rainbowCycle(); + void breatheEffect(); + void heartbeatEffect(); + void colorBlendEffect(); + void sparkleEffect(); + RGBColor getColorForState(int state); + AnimationType getAnimationForState(int state); + uint32_t applyBrightness(uint8_t r, uint8_t g, uint8_t b, uint8_t brightness); + + uint16_t rainbowHue = 0; // 16-bit for smooth transitions (0-65535) + uint16_t animPhase = 0; // Animation phase counter + uint8_t heartbeatStage = 0; // Stage in heartbeat animation + static const uint16_t HUE_STEP = 128; // Smaller = slower, smoother cycle + static const uint8_t MAX_BRIGHTNESS = 40; // Max brightness (0-255), lowered to reduce eye strain +#else int ledPins[4]; int numLeds; +#endif + int currentState = STATE_NONE; bool running = false; ulong lastUpdate = 0; diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index b99be9d..0c88bcf 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -2,7 +2,26 @@ #define BAUD_RATE 38400 +/** + * @brief Constructor - intentionally does NOT initialize serial. + * + * On ESP32-C6 (ESPA_V2), global object constructors run before the Arduino + * framework is fully initialized. Serial initialization in constructors causes + * crashes. The begin() method must be called from setup() instead. + */ SpaInterface::SpaInterface() : port(SPA_SERIAL) { +} + +/** + * @brief Initialize serial communication with the spa controller. + * + * This method MUST be called from setup() after the Arduino framework is + * initialized. This deferred initialization pattern is required for ESP32-C6 + * compatibility but is safe to use on all platforms. + * + * @note On ESP32-C6, SPA_SERIAL is Serial1 (UART1). On ESP32-S3, it's Serial2. + */ +void SpaInterface::begin() { SPA_SERIAL.setRxBufferSize(1024); //required for unit testing SPA_SERIAL.setTxBufferSize(1024); //required for unit testing SPA_SERIAL.begin(BAUD_RATE, SERIAL_8N1, RX_PIN, TX_PIN); diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 9d035d4..1ba0a3f 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -112,6 +112,17 @@ class SpaInterface : public SpaProperties { /// @brief Init SpaInterface. SpaInterface(); + /// @brief Initialize serial communication with the spa controller. + /// + /// This method MUST be called from setup() after the Arduino framework + /// is initialized. On ESP32-C6 (ESPA_V2), serial initialization in + /// global constructors causes crashes, so this deferred initialization + /// pattern is required. It is safe to use on all platforms. + /// + /// @note Called automatically by legacy code, but explicit call in + /// setup() is now required for ESP32-C6 compatibility. + void begin(); + ~SpaInterface(); /// @brief configure how often the spa is polled in seconds. diff --git a/platformio.ini b/platformio.ini index 516c859..aae3ffb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -53,17 +53,36 @@ build_flags = [env:espa-v2] extends = env:spa-base -# Use the pioarduino fork of the espressif32 platform to enable Arduino framework support -# for the ESP32-C6, which is not fully supported in the official PlatformIO 6.12.0 release. -#platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip +# ESP32-C6 DevKitC-1 board (ESPA_V2 hardware) +# +# Key differences from ESPA_V1 (ESP32-S3): +# - Uses Hardware CDC (HWCDC) for USB serial instead of native USB CDC +# - Only has UART0 and UART1 (no UART2), so spa serial uses Serial1 +# - Single WS2812 RGB NeoPixel LED on GPIO10 instead of 4 separate LEDs +# - General purpose button on GPIO21 (GP_PIN) for future functionality +# +# USB Serial Configuration: +# - ARDUINO_USB_CDC_ON_BOOT=1: Routes Serial to USB CDC +# - ARDUINO_USB_MODE=1: Selects Hardware CDC (HWCDCSerial) over native USB +# +# Deferred Initialization: +# Global objects (SpaInterface, MultiBlinker) must defer hardware init to +# setup() because ESP32-C6 constructors run before Arduino framework is ready. +# framework = arduino board = esp32-c6-devkitc-1 board_build.flash_size = 4MB board_build.partitions = partitions/espa_v2_partitions.csv +lib_deps = + ${env:spa-base.lib_deps} + adafruit/Adafruit NeoPixel@^1.12.0 build_flags = -; -D ARDUINO_USB_CDC_ON_BOOT=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D ARDUINO_USB_MODE=1 -D ESPA_V2=1 - -D RX_PIN=19 - -D TX_PIN=20 - -D EN_PIN=9 - -D SPA_SERIAL=Serial2 \ No newline at end of file + -D RGB_LED_PIN=10 ; WS2812 NeoPixel on GPIO10 + -D RX_PIN=19 ; Spa serial RX + -D TX_PIN=20 ; Spa serial TX + -D EN_PIN=9 ; Enable/config button + -D GP_PIN=21 ; General purpose button (reserved) + -D SPA_SERIAL=Serial1 ; ESP32-C6 only has UART0/UART1, no UART2 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 4bc253e..2835140 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,7 +23,9 @@ RemoteDebug Debug; SpaInterface si; Config config; -#if defined(ESPA_V1) || defined(ESPA_V2) +#if defined(ESPA_V2) && defined(RGB_LED_PIN) + MultiBlinker blinker(RGB_LED_PIN); +#elif defined(ESPA_V1) MultiBlinker blinker(PCB_LED1, PCB_LED2, PCB_LED3, PCB_LED4); #elif defined(LED_PIN) MultiBlinker blinker(LED_PIN); @@ -104,7 +106,15 @@ void startWiFiManager(){ ESP.restart(); // Restart the ESP to apply the new settings } -// We check the EN_PIN every loop, to allow people to configure the system +/** + * @brief Check hardware buttons and handle their actions. + * + * Button assignments by hardware variant: + * - EN_PIN: "Enable" button - hold to start WiFi Manager config portal + * - GP_PIN: General purpose button (ESPA_V2 only, GPIO21) - reserved for future use + * + * @note Both buttons use INPUT_PULLUP, so pressed = LOW, released = HIGH. + */ void checkButton(){ #if defined(EN_PIN) if(digitalRead(EN_PIN) == LOW) { @@ -116,6 +126,17 @@ void checkButton(){ } } #endif +#if defined(GP_PIN) + // GP_PIN (GPIO21 on ESPA_V2) - general purpose button for future functionality + // Currently just logs button presses for debugging + static bool gpButtonPressed = false; + if(digitalRead(GP_PIN) == LOW && !gpButtonPressed) { + gpButtonPressed = true; + debugI("GP Button pressed"); + } else if(digitalRead(GP_PIN) == HIGH) { + gpButtonPressed = false; + } +#endif } void startWifiManagerCallback() { @@ -621,11 +642,26 @@ void setup() { #if defined(EN_PIN) pinMode(EN_PIN, INPUT_PULLUP); #endif + #if defined(GP_PIN) + pinMode(GP_PIN, INPUT_PULLUP); + #endif Serial.begin(115200); + + #if defined(ESPA_V2) + // ESP32-C6: Wait for USB CDC to connect (with timeout) + unsigned long startWait = millis(); + while (!Serial && millis() - startWait < 3000) { + delay(10); + } + delay(100); // Extra settling time for USB + #endif + Serial.setDebugOutput(true); Debug.setSerialEnabled(true); + si.begin(); // Initialize SpaInterface serial communication + blinker.setState(STATE_NONE); // start with all LEDs off blinker.start(); From 487b4ac67df036602589bc9ba3c66e5175e2eb56 Mon Sep 17 00:00:00 2001 From: Jonathan Giles Date: Fri, 13 Feb 2026 12:10:11 +1300 Subject: [PATCH 2/2] Try to get github to build as well --- platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index aae3ffb..904dfe6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -69,9 +69,10 @@ extends = env:spa-base # Global objects (SpaInterface, MultiBlinker) must defer hardware init to # setup() because ESP32-C6 constructors run before Arduino framework is ready. # +platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip framework = arduino board = esp32-c6-devkitc-1 -board_build.flash_size = 4MB +board_build.flash_size = 8MB board_build.partitions = partitions/espa_v2_partitions.csv lib_deps = ${env:spa-base.lib_deps}