#pragma once #include "wled.h" /* * Usermod for analog clock */ extern Timezone* tz; class AnalogClockUsermod : public Usermod { private: static constexpr uint32_t refreshRate = 50; // per second static constexpr uint32_t refreshDelay = 1000 / refreshRate; struct Segment { // config int16_t firstLed = 0; int16_t lastLed = 59; int16_t centerLed = 0; // runtime int16_t size; Segment() { update(); } void validateAndUpdate() { if (firstLed < 0 || firstLed >= strip.getLengthTotal() || lastLed < firstLed || lastLed >= strip.getLengthTotal()) { *this = {}; return; } if (centerLed < firstLed || centerLed > lastLed) { centerLed = firstLed; } update(); } void update() { size = lastLed - firstLed + 1; } }; // configuration (available in API and stored in flash) bool enabled = false; Segment mainSegment; bool hourMarksEnabled = true; uint32_t hourMarkColor = 0xFF0000; uint32_t hourColor = 0x0000FF; uint32_t minuteColor = 0x00FF00; bool secondsEnabled = true; Segment secondsSegment; uint32_t secondColor = 0xFF0000; bool blendColors = true; uint16_t secondsEffect = 0; // runtime bool initDone = false; uint32_t lastOverlayDraw = 0; void validateAndUpdate() { mainSegment.validateAndUpdate(); secondsSegment.validateAndUpdate(); if (secondsEffect < 0 || secondsEffect > 1) { secondsEffect = 0; } } int16_t adjustToSegment(double progress, Segment const& segment) { int16_t led = segment.centerLed + progress * segment.size; return led > segment.lastLed ? segment.firstLed + led - segment.lastLed - 1 : led; } void setPixelColor(uint16_t n, uint32_t c) { if (!blendColors) { strip.setPixelColor(n, c); } else { uint32_t oldC = strip.getPixelColor(n); strip.setPixelColor(n, qadd32(oldC, c)); } } String colorToHexString(uint32_t c) { char buffer[9]; sprintf(buffer, "%06X", c); return buffer; } bool hexStringToColor(String const& s, uint32_t& c, uint32_t def) { char *ep; unsigned long long r = strtoull(s.c_str(), &ep, 16); if (*ep == 0) { c = r; return true; } else { c = def; return false; } } void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) { uint32_t ms = time.ms % 1000; uint8_t b0 = (cos8(ms * 64 / 1000) - 128) * 2; setPixelColor(secondLed, gamma32(scale32(secondColor, b0))); uint8_t b1 = (sin8(ms * 64 / 1000) - 128) * 2; setPixelColor(inc(secondLed, 1, secondsSegment), gamma32(scale32(secondColor, b1))); } static inline uint32_t qadd32(uint32_t c1, uint32_t c2) { return RGBW32( qadd8(R(c1), R(c2)), qadd8(G(c1), G(c2)), qadd8(B(c1), B(c2)), qadd8(W(c1), W(c2)) ); } static inline uint32_t scale32(uint32_t c, fract8 scale) { return RGBW32( scale8(R(c), scale), scale8(G(c), scale), scale8(B(c), scale), scale8(W(c), scale) ); } static inline int16_t dec(int16_t n, int16_t i, Segment const& seg) { return n - seg.firstLed >= i ? n - i : seg.lastLed - seg.firstLed - i + n + 1; } static inline int16_t inc(int16_t n, int16_t i, Segment const& seg) { int16_t r = n + i; if (r > seg.lastLed) { return seg.firstLed + n - seg.lastLed; } return r; } public: AnalogClockUsermod() { } void setup() override { initDone = true; validateAndUpdate(); } void loop() override { if (millis() - lastOverlayDraw > refreshDelay) { strip.trigger(); } } void handleOverlayDraw() override { if (!enabled) { return; } lastOverlayDraw = millis(); auto time = toki.getTime(); double secondP = second(localTime) / 60.0; double minuteP = minute(localTime) / 60.0; double hourP = (hour(localTime) % 12) / 12.0 + minuteP / 12.0; if (hourMarksEnabled) { for (int Led = 0; Led <= 55; Led = Led + 5) { int16_t hourmarkled = adjustToSegment(Led / 60.0, mainSegment); setPixelColor(hourmarkled, hourMarkColor); } } if (secondsEnabled) { int16_t secondLed = adjustToSegment(secondP, secondsSegment); switch (secondsEffect) { case 0: // no effect setPixelColor(secondLed, secondColor); break; case 1: // fading seconds secondsEffectSineFade(secondLed, time); break; } // TODO: move to secondsTrailEffect // for (uint16_t i = 1; i < secondsTrail + 1; ++i) { // uint16_t trailLed = dec(secondLed, i, secondsSegment); // uint8_t trailBright = 255 / (secondsTrail + 1) * (secondsTrail - i + 1); // setPixelColor(trailLed, gamma32(scale32(secondColor, trailBright))); // } } setPixelColor(adjustToSegment(minuteP, mainSegment), minuteColor); setPixelColor(adjustToSegment(hourP, mainSegment), hourColor); } void addToConfig(JsonObject& root) override { validateAndUpdate(); JsonObject top = root.createNestedObject(F("Analog Clock")); top[F("Overlay Enabled")] = enabled; top[F("First LED (Main Ring)")] = mainSegment.firstLed; top[F("Last LED (Main Ring)")] = mainSegment.lastLed; top[F("Center/12h LED (Main Ring)")] = mainSegment.centerLed; top[F("Hour Marks Enabled")] = hourMarksEnabled; top[F("Hour Mark Color (RRGGBB)")] = colorToHexString(hourMarkColor); top[F("Hour Color (RRGGBB)")] = colorToHexString(hourColor); top[F("Minute Color (RRGGBB)")] = colorToHexString(minuteColor); top[F("Show Seconds")] = secondsEnabled; top[F("First LED (Seconds Ring)")] = secondsSegment.firstLed; top[F("Last LED (Seconds Ring)")] = secondsSegment.lastLed; top[F("Center/12h LED (Seconds Ring)")] = secondsSegment.centerLed; top[F("Second Color (RRGGBB)")] = colorToHexString(secondColor); top[F("Seconds Effect (0-1)")] = secondsEffect; top[F("Blend Colors")] = blendColors; } bool readFromConfig(JsonObject& root) override { JsonObject top = root[F("Analog Clock")]; bool configComplete = !top.isNull(); String color; configComplete &= getJsonValue(top[F("Overlay Enabled")], enabled, false); configComplete &= getJsonValue(top[F("First LED (Main Ring)")], mainSegment.firstLed, 0); configComplete &= getJsonValue(top[F("Last LED (Main Ring)")], mainSegment.lastLed, 59); configComplete &= getJsonValue(top[F("Center/12h LED (Main Ring)")], mainSegment.centerLed, 0); configComplete &= getJsonValue(top[F("Hour Marks Enabled")], hourMarksEnabled, false); configComplete &= getJsonValue(top[F("Hour Mark Color (RRGGBB)")], color, F("161616")) && hexStringToColor(color, hourMarkColor, 0x161616); configComplete &= getJsonValue(top[F("Hour Color (RRGGBB)")], color, F("0000FF")) && hexStringToColor(color, hourColor, 0x0000FF); configComplete &= getJsonValue(top[F("Minute Color (RRGGBB)")], color, F("00FF00")) && hexStringToColor(color, minuteColor, 0x00FF00); configComplete &= getJsonValue(top[F("Show Seconds")], secondsEnabled, true); configComplete &= getJsonValue(top[F("First LED (Seconds Ring)")], secondsSegment.firstLed, 0); configComplete &= getJsonValue(top[F("Last LED (Seconds Ring)")], secondsSegment.lastLed, 59); configComplete &= getJsonValue(top[F("Center/12h LED (Seconds Ring)")], secondsSegment.centerLed, 0); configComplete &= getJsonValue(top[F("Second Color (RRGGBB)")], color, F("FF0000")) && hexStringToColor(color, secondColor, 0xFF0000); configComplete &= getJsonValue(top[F("Seconds Effect (0-1)")], secondsEffect, 0); configComplete &= getJsonValue(top[F("Blend Colors")], blendColors, true); if (initDone) { validateAndUpdate(); } return configComplete; } uint16_t getId() override { return USERMOD_ID_ANALOG_CLOCK; } };