From 58cb843106dfda7f8e298d2501cfa2daee0134af Mon Sep 17 00:00:00 2001 From: ThePendulum Date: Wed, 29 Apr 2026 02:48:17 +0200 Subject: [PATCH] Added mode switching and indicator animation. --- .QuadLightsV2.ino | 258 ++++++++++++++++++++++++++++++++++++++++++++++ QuadLightsV3.ino | 133 +++++++++++++++++------- 2 files changed, 352 insertions(+), 39 deletions(-) create mode 100644 .QuadLightsV2.ino diff --git a/.QuadLightsV2.ino b/.QuadLightsV2.ino new file mode 100644 index 0000000..cb2f995 --- /dev/null +++ b/.QuadLightsV2.ino @@ -0,0 +1,258 @@ +#include +#include +#include + +constexpr int PIN_STRIP = PA7; +constexpr int PIN_BUTTON_LEFT = PA10; +constexpr int PIN_BUTTON_CENTER = PA9; +constexpr int PIN_BUTTON_RIGHT = PA8; + +constexpr int LEDS_COUNT = 143; +constexpr int LEDS_SKIP = 130; +constexpr int LEDS_BRIGHTNESS = 20; + +// LEFT HEADLIGHT +constexpr int RING_LEFT_RANGE[] = {130, 143}; +constexpr int EDGE_LEFT_RANGE[] = {120, 129}; + +// RIGHT HEADLIGHT +constexpr int RING_RIGHT_RANGE[] = {91, 104}; +constexpr int EDGE_RIGHT_RANGE[] = {105, 114}; + +constexpr int UPDATE_INTERVAL = 33; // about 30fps + +const int stripRange[] = {EDGE_RIGHT_RANGE[0], RING_LEFT_RANGE[1]}; +const int ringLength = RING_LEFT_RANGE[1] - RING_LEFT_RANGE[0] + 1; // should be symmetrical +const int edgeLength = EDGE_LEFT_RANGE[1] - EDGE_LEFT_RANGE[0] + 1; + +Adafruit_NeoPixel strip(stripRange[1], PIN_STRIP, NEO_GRBW + NEO_KHZ800); + +Button buttonLeft(PIN_BUTTON_LEFT); +Button buttonCenter(PIN_BUTTON_CENTER); +Button buttonRight(PIN_BUTTON_RIGHT); + +// identical strip definitions, right strip is good as any for color +uint32_t colorOff = strip.Color(0, 0, 0, 0); +uint32_t colorIndicator = strip.Color(255, 40, 0, 0); +uint32_t colorPoliceRed = strip.Color(255, 0, 0, 0); +uint32_t colorPoliceBlue = strip.Color(0, 0, 255, 0); +uint32_t colorWhiteFull = strip.Color(255, 255, 255, 255); +uint32_t colorWhiteNative = strip.Color(0, 0, 0, 255); +uint32_t colorWhiteRgb = strip.Color(255, 150, 75, 0); + +enum Direction { LEFT, RIGHT }; + +unsigned long updateLast = 0; +unsigned long currentFrame = 0; +unsigned long animationFrame = 0; + +// relying only on start time would turn on the indicator when uptime < indicator duration +bool indicatorLeftActive = false; +bool indicatorRightActive = false; + +unsigned long indicatorLeftStart = 0; +unsigned long indicatorRightStart = 0; + +bool policeActive = false; + +void render() { + strip.show(); +} + +void clear() { + Serial.println("CLEAR"); + + for (int ledIndex = 0; ledIndex < stripRange[1]; ledIndex++) { + strip.setPixelColor(ledIndex, colorOff); + } + + render(); +} + +constexpr int INDICATOR_DURATION = 3000; // total ms +constexpr int INDICATOR_HOLD_FRAMES = 15; +constexpr int INDICATOR_OFF_FRAMES = 20; +constexpr float INDICATOR_SPEED = 2; + +void animateHeadlight(Direction direction) { + const int* activeRingRange = direction == LEFT + ? RING_LEFT_RANGE + : RING_RIGHT_RANGE; + + const int* activeEdgeRange = direction == LEFT + ? EDGE_LEFT_RANGE + : EDGE_RIGHT_RANGE; + + strip.fill(colorWhiteFull, activeRingRange[0], ringLength); + // strip.fill(colorWhiteFull, activeEdgeRange[0], edgeLength); +} + +void animateIndicator(Direction direction) { + const int* ringRange = direction == LEFT + ? RING_LEFT_RANGE + : RING_RIGHT_RANGE; + + const long indicatorFrame = lround(animationFrame * INDICATOR_SPEED) % (ringLength + INDICATOR_HOLD_FRAMES + INDICATOR_OFF_FRAMES); + + const long sweepEnd = ringLength; + const long holdEnd = ringLength + INDICATOR_HOLD_FRAMES; + + const long sweepIndex = direction == LEFT + ? ringRange[0] + (indicatorFrame % ringLength) + : ringRange[1] - (indicatorFrame % ringLength); + + for (int ledIndex = ringRange[0]; ledIndex < ringRange[1]; ledIndex++) { + if (indicatorFrame < sweepEnd) { + // SWEEP + const bool isLit = direction == LEFT + ? ledIndex < sweepIndex + : ledIndex >= sweepIndex; + + strip.setPixelColor(ledIndex, isLit ? colorIndicator : colorOff); + } else if (indicatorFrame < holdEnd) { + // HOLD + strip.setPixelColor(ledIndex, colorIndicator); + } else { + // OFF + strip.setPixelColor(ledIndex, colorOff); + } + } +} + +constexpr float POLICE_SPEED = 0.75; + +enum PoliceStates { POLICE_LEFT, POLICE_RIGHT, POLICE_OFF }; + +const PoliceStates policeBeaconFrames[12] = { + POLICE_LEFT, + POLICE_OFF, + POLICE_LEFT, + POLICE_OFF, + POLICE_OFF, + POLICE_OFF, + POLICE_RIGHT, + POLICE_OFF, + POLICE_RIGHT, + POLICE_OFF, + POLICE_OFF, + POLICE_OFF, +}; + +void animatePolice() { + const long policeFrame = lround(animationFrame * POLICE_SPEED) % 12; + + if (policeBeaconFrames[policeFrame] == POLICE_LEFT) { + strip.fill(colorPoliceRed, RING_LEFT_RANGE[0], ringLength); + strip.fill(colorOff, RING_RIGHT_RANGE[0], ringLength); + } else if (policeBeaconFrames[policeFrame] == POLICE_RIGHT) { + strip.fill(colorOff, RING_LEFT_RANGE[0], ringLength); + strip.fill(colorPoliceBlue, RING_RIGHT_RANGE[0], ringLength); + } else { + strip.fill(colorOff, RING_LEFT_RANGE[0], ringLength); + strip.fill(colorOff, RING_RIGHT_RANGE[0], ringLength); + } + + if (policeFrame < 6) { + strip.fill(colorOff, EDGE_LEFT_RANGE[0], edgeLength); + strip.fill(colorWhiteFull, EDGE_RIGHT_RANGE[0], edgeLength); + } else { + strip.fill(colorWhiteFull, EDGE_LEFT_RANGE[0], edgeLength); + strip.fill(colorOff, EDGE_RIGHT_RANGE[0], edgeLength); + } +} + +void setup() { + strip.begin(); + strip.setBrightness(LEDS_BRIGHTNESS); + + clear(); + + buttonLeft.begin(); + buttonCenter.begin(); + buttonRight.begin(); + + Serial.begin(9600); +} + +void loop() { + unsigned long now = millis(); + + if (buttonLeft.pressed()) { + Serial.println("BUTTON LEFT"); + + animationFrame = 0; // ensures animations start cleanly instead of halfway + + indicatorLeftActive = true; + indicatorLeftStart = now; + + indicatorRightActive = false; + indicatorRightStart = 0; + + Serial.println("INDICATOR LEFT"); + } + + if (buttonCenter.pressed()) { + Serial.println("BUTTON CENTER"); + + animationFrame = 0; + + /* + if (indicatorLeftActive || indicatorRightActive) { + indicatorLeftActive = false; + indicatorLeftStart = 0; + + indicatorRightActive = false; + indicatorRightStart = 0; + + Serial.println("INDICATOR OFF"); + } else { + policeActive = !policeActive; + + Serial.print("POLICE "); + Serial.println(policeActive ? "ON" : "OFF"); + } + */ + + policeActive = !policeActive; + + Serial.print("POLICE "); + Serial.println(policeActive ? "ON" : "OFF"); + } + + if (buttonRight.pressed()) { + Serial.println("BUTTON RIGHT"); + + animationFrame = 0; + + indicatorLeftActive = false; + indicatorLeftStart = 0; + + indicatorRightActive = true; + indicatorRightStart = now; + + Serial.println("INDICATOR RIGHT"); + } + + animateHeadlight(LEFT); + animateHeadlight(RIGHT); + + if (policeActive) { + animatePolice(); + } + + if (indicatorLeftActive && now - indicatorLeftStart < INDICATOR_DURATION) { + animateIndicator(LEFT); + } + + if (indicatorRightActive && now - indicatorRightStart < INDICATOR_DURATION) { + animateIndicator(RIGHT); + } + + if (now - updateLast >= UPDATE_INTERVAL) { + updateLast += UPDATE_INTERVAL; + render(); + + currentFrame += 1; + animationFrame += 1; + } +} diff --git a/QuadLightsV3.ino b/QuadLightsV3.ino index 7c5edc9..32e6d69 100644 --- a/QuadLightsV3.ino +++ b/QuadLightsV3.ino @@ -59,12 +59,35 @@ Adafruit_NeoPixel ledsHeadLeft(LEDS_HEAD_LENGTH, LEDS_LEFT_PIN, NEO_GRBW + NEO_K Adafruit_NeoPixel ledsHeadRight(LEDS_HEAD_LENGTH, LEDS_RIGHT_PIN, NEO_GRBW + NEO_KHZ800); Adafruit_NeoPixel ledsDash(LEDS_DASH_LENGTH, LEDS_DASH_PIN, NEO_GRBW + NEO_KHZ800); +const uint32_t COLOR_OFF = ledsTail.Color(0, 0, 0, 0); const uint32_t COLOR_WHITE = ledsTail.Color(0, 0, 0, 255); const uint32_t COLOR_RED = ledsTail.Color(255, 0, 0, 0); const uint32_t COLOR_AMBER = ledsTail.Color(255, 50, 0, 0); const uint32_t COLOR_POLICE_BLUE = ledsTail.Color(0, 0, 255, 0); const uint32_t COLOR_POLICE_RED = ledsTail.Color(255, 0, 0, 0); +enum Direction { LEFT, RIGHT }; + +enum Mode { + MODE_HEAD, + MODE_POLICE, + MODE_HAZARD, + MODE_RAINBOW, + MODE_COUNT, +}; + +Mode activeMode = MODE_HEAD; + +unsigned long animationFrame = 0; +unsigned long lastFrame = 0; + +unsigned long lastPedalPress = millis(); + +unsigned long indicatorLeftStart = 0; +unsigned long indicatorRightStart = 0; +bool indicatorLeftActive = false; +bool indicatorRightActive = false; + void setup() { // initialize digital pin LED_BUILTIN as an output. pinMode(LED_BUILTIN, OUTPUT); @@ -110,41 +133,60 @@ void setup() { ledsDash.clear(); } -enum Mode { - MODE_HEAD, - MODE_POLICE, - MODE_HAZARD, - MODE_RAINBOW, - MODE_COUNT, -}; - -Mode activeMode = MODE_HEAD; - -unsigned long animationFrame = 0; -unsigned long lastFrame = 0; - void animateHeadlights() { ledsHeadLeft.fill(COLOR_WHITE); ledsHeadRight.fill(COLOR_WHITE); - - ledsDash.fill(ledsDash.Color(0, 255, 0, 0)); } -void animateIndicator(Adafruit_NeoPixel& ledsTarget) { - Serial.println("ANIMATING INDICATOR"); +constexpr int INDICATOR_DURATION = 3000; // total ms +constexpr int INDICATOR_HOLD_FRAMES = 15; +constexpr int INDICATOR_OFF_FRAMES = 20; +constexpr float INDICATOR_SPEED = 2; - for (int ledIndex = LEDS_EDGE_LENGTH; ledIndex < LEDS_EDGE_LENGTH + LEDS_RING_LENGTH; ledIndex += 1) { - ledsTarget.setPixelColor(ledIndex, COLOR_AMBER); +void animateIndicator(Direction direction) { + Adafruit_NeoPixel& ledsTarget = direction == LEFT // reference, not pointer + ? ledsHeadLeft + : ledsHeadRight; + + const int ringStart = LEDS_EDGE_LENGTH; + const int ringEnd = LEDS_EDGE_LENGTH + LEDS_RING_LENGTH; + + const long indicatorFrame = + lround(animationFrame * INDICATOR_SPEED) + % (LEDS_RING_LENGTH + INDICATOR_HOLD_FRAMES + INDICATOR_OFF_FRAMES); + + const long sweepEnd = LEDS_RING_LENGTH; + const long holdEnd = LEDS_RING_LENGTH + INDICATOR_HOLD_FRAMES; + + const long sweepIndex = direction == LEFT + ? ringStart + (indicatorFrame % LEDS_RING_LENGTH) + : ringEnd - (indicatorFrame % LEDS_RING_LENGTH); + + for (int ledIndex = ringStart; ledIndex < ringEnd; ledIndex++) { + if (indicatorFrame < sweepEnd) { + // SWEEP + const bool isLit = direction == LEFT + ? ledIndex < sweepIndex + : ledIndex >= sweepIndex; + ledsTarget.setPixelColor(ledIndex, isLit ? COLOR_AMBER : COLOR_OFF); + + ledsDash.fill(COLOR_AMBER); + } else if (indicatorFrame < holdEnd) { + // HOLD + ledsTarget.setPixelColor(ledIndex, COLOR_AMBER); + } else { + // OFF + ledsTarget.setPixelColor(ledIndex, COLOR_OFF); + } + } + + if (indicatorFrame < holdEnd) { + ledsDash.fill(COLOR_AMBER); + } else { + ledsDash.fill(COLOR_OFF); } } -unsigned long lastPedalPress = millis(); - -unsigned long lastIndicatingLeft = 0; -unsigned long lastIndicatingRight = 0; -bool isIndicatingLeft = false; -bool isIndicatingRight = false; - void animate(long now) { if (pedalForward.read() == Button::PRESSED) { ledsTail.fill(ledsTail.Color(255, 0, 0, 0)); @@ -164,15 +206,22 @@ void animate(long now) { animateHeadlights(); // we need the boolean to prevent the indicators from turning on right after boot - if (isIndicatingLeft && animationFrame - lastIndicatingLeft < 100) { - animateIndicator(ledsHeadLeft); + if (indicatorLeftActive && now - indicatorLeftStart < INDICATOR_DURATION) { + animateIndicator(LEFT); } - if (isIndicatingRight && animationFrame - lastIndicatingRight < 100) { - animateIndicator(ledsHeadRight); + if (indicatorRightActive && now - indicatorRightStart < INDICATOR_DURATION) { + animateIndicator(RIGHT); } } +void render() { + ledsTail.show(); + ledsHeadLeft.show(); + ledsHeadRight.show(); + ledsDash.show(); +} + // the loop function runs over and over again forever void loop() { const unsigned long now = millis(); @@ -187,15 +236,25 @@ void loop() { } if (buttonLeft.pressed()) { - isIndicatingLeft = true; - lastIndicatingLeft = animationFrame; + animationFrame = 0; // ensures animations start cleanly instead of halfway + + indicatorLeftActive = true; + indicatorLeftStart = now; + + indicatorRightActive = false; + indicatorRightStart = 0; Serial.println("BUTTON LEFT"); } if (buttonRight.pressed()) { - isIndicatingRight = true; - lastIndicatingRight = animationFrame; + animationFrame = 0; // ensures animations start cleanly instead of halfway + + indicatorRightActive = true; + indicatorRightStart = now; + + indicatorLeftActive = false; + indicatorLeftStart = 0; Serial.println("BUTTON RIGHT"); } @@ -205,11 +264,7 @@ void loop() { animationFrame += 1; animate(now); - - ledsTail.show(); - ledsHeadLeft.show(); - ledsHeadRight.show(); - ledsDash.show(); + render(); } if (KEEPALIVE_ENABLE && now - lastPedalPress > KEEPALIVE_TIMEOUT) {