From 4cab19643534543667bb32366ce17d70ac41eb63 Mon Sep 17 00:00:00 2001 From: dereibims Date: Thu, 1 Jan 2026 01:11:06 +0100 Subject: [PATCH 1/9] Basics Working --- platformio.ini | 29 +-- .../usermod_7segment_countdown/library.json | 13 ++ .../usermod_7segment_countdown.cpp | 219 ++++++++++++++++++ 3 files changed, 238 insertions(+), 23 deletions(-) create mode 100644 usermods/usermod_7segment_countdown/library.json create mode 100644 usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp diff --git a/platformio.ini b/platformio.ini index ec73bc5658..01ed17f104 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,27 +10,8 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = nodemcuv2 - esp8266_2m - esp01_1m_full - nodemcuv2_160 - esp8266_2m_160 - esp01_1m_full_160 - nodemcuv2_compat - esp8266_2m_compat - esp01_1m_full_compat - esp32dev - esp32dev_debug - esp32_eth - esp32_wrover - lolin_s2_mini - esp32c3dev - esp32c3dev_qio - esp32S3_wroom2 - esp32s3dev_16MB_opi - esp32s3dev_8MB_opi - esp32s3_4M_qspi - usermods +default_envs = esp32dev + src_dir = ./wled00 data_dir = ./wled00/data @@ -148,7 +129,8 @@ framework = arduino board_build.flash_mode = dout monitor_speed = 115200 # slow upload speed but most compatible (use platformio_override.ini to use faster speed) -upload_speed = 115200 +upload_speed = 921600 +# upload_speed = 115200 # ------------------------------------------------------------------------------ # LIBRARIES: required dependencies @@ -456,9 +438,10 @@ board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} -custom_usermods = audioreactive +custom_usermods = usermod_7segment_countdown build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 + -D WLED_DEBUG lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} diff --git a/usermods/usermod_7segment_countdown/library.json b/usermods/usermod_7segment_countdown/library.json new file mode 100644 index 0000000000..548cea325a --- /dev/null +++ b/usermods/usermod_7segment_countdown/library.json @@ -0,0 +1,13 @@ +{ + "name": "usermod_7segment_countdown", + "version": "0.1.0", + "description": "7-segment clock and countdown usermod for WLED", + "authors": [ + { + "name": "Phips" + } + ], + "frameworks": ["arduino"], + "platforms": ["espressif32"], + "build": { "libArchive": false } +} diff --git a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp new file mode 100644 index 0000000000..a390efacd8 --- /dev/null +++ b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp @@ -0,0 +1,219 @@ +// usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp +#include "wled.h" + +/* + Step 2: + - LED layout (6x 7-seg digits, 2x separators) + - Segment mapping + - Apply mask: ON keeps current WLED effect/color, OFF becomes black + - Test display: 88:88:88 + - Info UI: collapsible group under [u] -> "7 Segment Counter" + - Enable/Disable switch: + * runtime via /json/state + * persistent via WLED config (readFromConfig/addToConfig) +*/ + +class Usermod7SegmentCountdown : public Usermod { +private: + // ---- Layout ---- + static constexpr uint16_t LEDS_PER_SEG = 5; + static constexpr uint8_t SEGS_PER_DIGIT = 7; + static constexpr uint16_t LEDS_PER_DIGIT = LEDS_PER_SEG * SEGS_PER_DIGIT; // 35 + + static constexpr uint16_t SEP_LEDS = 10; // 5 upper dot, 5 lower dot + + // Stream: Z1(35) Z2(35) Sep1(10) Z3(35) Z4(35) Sep2(10) Z5(35) Z6(35) + static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; // 230 + + // Phys segment order per digit (0..6): F-A-B-G-E-D-C + // Logical segment indices: A=0,B=1,C=2,D=3,E=4,F=5,G=6 + static inline constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { + 5, // F + 0, // A + 1, // B + 6, // G + 4, // E + 3, // D + 2 // C + }; + + // Digit bitmasks (bits A..G -> 0..6) + static inline constexpr uint8_t DIGIT_MASKS[10] = { + 0b00111111, // 0 (A..F) + 0b00000110, // 1 (B,C) + 0b01011011, // 2 (A,B,D,E,G) + 0b01001111, // 3 (A,B,C,D,G) + 0b01100110, // 4 (B,C,F,G) + 0b01101101, // 5 (A,C,D,F,G) + 0b01111101, // 6 (A,C,D,E,F,G) + 0b00000111, // 7 (A,B,C) + 0b01111111, // 8 (A..G) + 0b01101111 // 9 (A,B,C,D,F,G) + }; + + // Mask: 1 = on (keep current color), 0 = off (force black) + std::vector mask; + + bool enabled = true; // switchable + persistent + bool sepsOn = true; // will be configurable later; kept for now + + // ---- Index helpers ---- + static uint16_t digitBase(uint8_t d) { + // d: 0..5 (Z1..Z6) + // Bases: + // Z1 0 + // Z2 35 + // Sep1 70 + // Z3 80 + // Z4 115 + // Sep2 150 + // Z5 160 + // Z6 195 + switch (d) { + case 0: return 0; + case 1: return 35; + case 2: return 80; + case 3: return 115; + case 4: return 160; + case 5: return 195; + default: return 0; + } + } + + static constexpr uint16_t sep1Base() { return 70; } + static constexpr uint16_t sep2Base() { return 150; } + + void ensureMaskSize() { + if (mask.size() != TOTAL_PANEL_LEDS) mask.assign(TOTAL_PANEL_LEDS, 0); + } + + void clearMask() { + std::fill(mask.begin(), mask.end(), 0); + } + + void setRangeOn(uint16_t start, uint16_t len) { + for (uint16_t i = 0; i < len; i++) { + uint16_t idx = start + i; + if (idx < mask.size()) mask[idx] = 1; + } + } + + void setDigit(uint8_t digitIndex, int8_t value) { + // value: 0..9, -1 = blank + if (digitIndex > 5) return; + if (value < 0) return; + + uint16_t base = digitBase(digitIndex); + uint8_t bits = DIGIT_MASKS[(uint8_t)value]; + + for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) { + uint8_t logSeg = PHYS_TO_LOG[physSeg]; // A..G index + bool segOn = (bits >> logSeg) & 0x01; + + if (segOn) { + uint16_t segStart = base + (uint16_t)physSeg * LEDS_PER_SEG; + setRangeOn(segStart, LEDS_PER_SEG); + } + } + } + + void setSeparator(uint8_t which, bool on) { + uint16_t base = (which == 1) ? sep1Base() : sep2Base(); + if (on) setRangeOn(base, SEP_LEDS); + } + + void applyMaskToStrip() { + // OFF -> black, ON -> unchanged (keeps effect/color) + uint16_t stripLen = strip.getLengthTotal(); + uint16_t limit = (stripLen < (uint16_t)mask.size()) ? stripLen : (uint16_t)mask.size(); + + for (uint16_t i = 0; i < limit; i++) { + if (!mask[i]) strip.setPixelColor(i, 0); + } + } + +public: + void setup() override { + ensureMaskSize(); + Serial.print("7Segment Setup - MOD: "); + Serial.println(enabled ? "enabled" : "disabled"); + } + + void loop() override { + } + + void handleOverlayDraw(){ + if (!enabled) return; + clearMask(); + setDigit(0, hour(localTime) / 10); // Z1 + setDigit(1, hour(localTime) % 10); // Z2 + setDigit(2, minute(localTime) / 10); // Z3 + setDigit(3, minute(localTime) % 10); // Z4 + setDigit(4, second(localTime) / 10); // Z5 + setDigit(5, second(localTime) % 10); // Z6 + if(second(localTime) % 2){ + setSeparator(1, sepsOn); // Sep1 + setSeparator(2, sepsOn); // Sep2 + } + applyMaskToStrip(); + } + + // ---- Info UI (collapsible group under "u") ---- + void addToJsonInfo(JsonObject& root) override { + JsonObject user = root["u"].as(); + if (user.isNull()) user = root.createNestedObject("u"); + + // Parent group (collapsible in UI depending on WLED UI build) + JsonObject grp = user.createNestedObject(F("7 Segment Counter")); + + JsonArray state = grp.createNestedArray(F("state")); + state.add(enabled ? F("active") : F("disabled")); + state.add(""); + + JsonArray se = grp.createNestedArray(F("seps")); + se.add(sepsOn ? F("on") : F("off")); + se.add(""); + + JsonArray pl = grp.createNestedArray(F("panel leds")); + pl.add(TOTAL_PANEL_LEDS); + pl.add(F(" px")); + } + + // ---- Runtime control via /json/state ---- + void addToJsonState(JsonObject& root) override { + JsonObject s = root[F("7seg")].as(); + if (s.isNull()) s = root.createNestedObject(F("7seg")); + s[F("enabled")] = enabled; + } + + void readFromJsonState(JsonObject& root) override { + JsonObject s = root[F("7seg")].as(); + if (s.isNull()) return; + + if (s.containsKey(F("enabled"))) { + enabled = s[F("enabled")].as(); + } + } + + // ---- Persistent config (survives reboot) ---- + void addToConfig(JsonObject& root) override { + JsonObject s = root[F("7seg")].as(); + if (s.isNull()) s = root.createNestedObject(F("7seg")); + s[F("enabled")] = enabled; + } + + bool readFromConfig(JsonObject& root) override { + JsonObject s = root[F("7seg")].as(); + if (s.isNull()) return false; + + enabled = s[F("enabled")] | true; // default true + return true; + } + + uint16_t getId() override { + return 0x7A01; + } +}; + +static Usermod7SegmentCountdown usermod; +REGISTER_USERMOD(usermod); From 930033e76a4606e7a2069ad92d3ccaeaa1103102 Mon Sep 17 00:00:00 2001 From: dereibims Date: Fri, 2 Jan 2026 02:26:38 +0100 Subject: [PATCH 2/9] Added Countown calc stuff --- .../usermod_7segment_countdown.cpp | 247 ++++++++++-------- .../usermod_7segment_countdown.h | 100 +++++++ 2 files changed, 242 insertions(+), 105 deletions(-) create mode 100644 usermods/usermod_7segment_countdown/usermod_7segment_countdown.h diff --git a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp index a390efacd8..73af6db65f 100644 --- a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp +++ b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp @@ -1,33 +1,11 @@ -// usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp +// 7-segment countdown usermod overlay: builds a display mask over the LED strip #include "wled.h" +#include "usermod_7segment_countdown.h" -/* - Step 2: - - LED layout (6x 7-seg digits, 2x separators) - - Segment mapping - - Apply mask: ON keeps current WLED effect/color, OFF becomes black - - Test display: 88:88:88 - - Info UI: collapsible group under [u] -> "7 Segment Counter" - - Enable/Disable switch: - * runtime via /json/state - * persistent via WLED config (readFromConfig/addToConfig) -*/ - -class Usermod7SegmentCountdown : public Usermod { -private: - // ---- Layout ---- - static constexpr uint16_t LEDS_PER_SEG = 5; - static constexpr uint8_t SEGS_PER_DIGIT = 7; - static constexpr uint16_t LEDS_PER_DIGIT = LEDS_PER_SEG * SEGS_PER_DIGIT; // 35 - - static constexpr uint16_t SEP_LEDS = 10; // 5 upper dot, 5 lower dot - - // Stream: Z1(35) Z2(35) Sep1(10) Z3(35) Z4(35) Sep2(10) Z5(35) Z6(35) - static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; // 230 - - // Phys segment order per digit (0..6): F-A-B-G-E-D-C - // Logical segment indices: A=0,B=1,C=2,D=3,E=4,F=5,G=6 - static inline constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { +// Layout/Segment mapping (keep these comments) +// Phys segment order per digit (0..6): F-A-B-G-E-D-C +// Logical segment indices: A=0,B=1,C=2,D=3,E=4,F=5,G=6 +static constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { 5, // F 0, // A 1, // B @@ -35,62 +13,31 @@ class Usermod7SegmentCountdown : public Usermod { 4, // E 3, // D 2 // C - }; - - // Digit bitmasks (bits A..G -> 0..6) - static inline constexpr uint8_t DIGIT_MASKS[10] = { - 0b00111111, // 0 (A..F) - 0b00000110, // 1 (B,C) - 0b01011011, // 2 (A,B,D,E,G) - 0b01001111, // 3 (A,B,C,D,G) - 0b01100110, // 4 (B,C,F,G) - 0b01101101, // 5 (A,C,D,F,G) - 0b01111101, // 6 (A,C,D,E,F,G) - 0b00000111, // 7 (A,B,C) - 0b01111111, // 8 (A..G) - 0b01101111 // 9 (A,B,C,D,F,G) - }; - - // Mask: 1 = on (keep current color), 0 = off (force black) - std::vector mask; - - bool enabled = true; // switchable + persistent - bool sepsOn = true; // will be configurable later; kept for now - - // ---- Index helpers ---- - static uint16_t digitBase(uint8_t d) { - // d: 0..5 (Z1..Z6) - // Bases: - // Z1 0 - // Z2 35 - // Sep1 70 - // Z3 80 - // Z4 115 - // Sep2 150 - // Z5 160 - // Z6 195 - switch (d) { - case 0: return 0; - case 1: return 35; - case 2: return 80; - case 3: return 115; - case 4: return 160; - case 5: return 195; - default: return 0; - } - } +}; - static constexpr uint16_t sep1Base() { return 70; } - static constexpr uint16_t sep2Base() { return 150; } +// Digit bitmasks (bits A..G -> 0..6) (keep these comments) +static constexpr uint8_t DIGIT_MASKS[10] = { + 0b00111111, // 0 (A..F) + 0b00000110, // 1 (B,C) + 0b01011011, // 2 (A,B,D,E,G) + 0b01001111, // 3 (A,B,C,D,G) + 0b01100110, // 4 (B,C,F,G) + 0b01101101, // 5 (A,C,D,F,G) + 0b01111101, // 6 (A,C,D,E,F,G) + 0b00000111, // 7 (A,B,C) + 0b01111111, // 8 (A..G) + 0b01101111 // 9 (A,B,C,D,F,G) +}; +class Usermod7SegmentCountdown : public Usermod { +private: + // Mask helpers -------------------------------------------------------------- void ensureMaskSize() { if (mask.size() != TOTAL_PANEL_LEDS) mask.assign(TOTAL_PANEL_LEDS, 0); } - void clearMask() { std::fill(mask.begin(), mask.end(), 0); } - void setRangeOn(uint16_t start, uint16_t len) { for (uint16_t i = 0; i < len; i++) { uint16_t idx = start + i; @@ -98,8 +45,36 @@ class Usermod7SegmentCountdown : public Usermod { } } + // Drawing helpers ----------------------------------------------------------- + void drawClock() { + setDigit(0, hour(localTime) / 10); + setDigit(1, hour(localTime) % 10); + setDigit(2, minute(localTime) / 10); + setDigit(3, minute(localTime) % 10); + setDigit(4, second(localTime) / 10); + setDigit(5, second(localTime) % 10); + if (second(localTime) % 2) { + setSeparator(1, sepsOn); + setSeparator(2, sepsOn); + } + } + + // Compute remaining time to targetUnix; also provide full totals (h/min/sec) + void drawCountdown() { + int64_t diff = (int64_t)targetUnix - (int64_t)localTime; + + remDays = diff / 86400u; + remHours = (uint8_t)((diff % 86400u) / 3600u); + remMinutes = (uint8_t)((diff % 3600u) / 60u); + remSeconds = (uint8_t)(diff % 60u); + + fullHours = diff / 3600u; + fullMinutes = diff / 60u; + fullSeconds = diff; + } + + // Turn on segments for a single digit according to bitmask void setDigit(uint8_t digitIndex, int8_t value) { - // value: 0..9, -1 = blank if (digitIndex > 5) return; if (value < 0) return; @@ -107,9 +82,8 @@ class Usermod7SegmentCountdown : public Usermod { uint8_t bits = DIGIT_MASKS[(uint8_t)value]; for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) { - uint8_t logSeg = PHYS_TO_LOG[physSeg]; // A..G index + uint8_t logSeg = PHYS_TO_LOG[physSeg]; bool segOn = (bits >> logSeg) & 0x01; - if (segOn) { uint16_t segStart = base + (uint16_t)physSeg * LEDS_PER_SEG; setRangeOn(segStart, LEDS_PER_SEG); @@ -117,53 +91,74 @@ class Usermod7SegmentCountdown : public Usermod { } } + // Turn on both separator dots if requested void setSeparator(uint8_t which, bool on) { uint16_t base = (which == 1) ? sep1Base() : sep2Base(); if (on) setRangeOn(base, SEP_LEDS); } + // Apply mask to strip: 1 keeps color/effect, 0 forces black void applyMaskToStrip() { - // OFF -> black, ON -> unchanged (keeps effect/color) uint16_t stripLen = strip.getLengthTotal(); uint16_t limit = (stripLen < (uint16_t)mask.size()) ? stripLen : (uint16_t)mask.size(); - for (uint16_t i = 0; i < limit; i++) { if (!mask[i]) strip.setPixelColor(i, 0); } } + template + static T clampVal(T v, T lo, T hi) { + return (v < lo) ? lo : (v > hi ? hi : v); + } + + // Clamp target fields and derive targetUnix; optional debug on change + void validateTarget(bool changed = false) { + targetYear = clampVal(targetYear, 1970, 2099); + targetMonth = clampVal(targetMonth, 1, 12); + targetDay = clampVal(targetDay, 1, 31); + targetHour = clampVal(targetHour, 0, 23); + targetMinute = clampVal(targetMinute, 0, 59); + + tmElements_t tm; + tm.Second = 0; + tm.Minute = targetMinute; + tm.Hour = targetHour; + tm.Day = targetDay; + tm.Month = targetMonth; + tm.Year = CalendarYrToTm(targetYear); + targetUnix = makeTime(tm); + + if (changed) { + char buf[24]; + snprintf(buf, sizeof(buf), "%04d-%02u-%02u-%02u-%02u", + targetYear, targetMonth, targetDay, targetHour, targetMinute); + Serial.printf("[7seg] Target changed: %s | unix=%lu\r\n", buf, (unsigned long)targetUnix); + } + } + public: void setup() override { ensureMaskSize(); Serial.print("7Segment Setup - MOD: "); Serial.println(enabled ? "enabled" : "disabled"); } - - void loop() override { - } + void loop() override {} void handleOverlayDraw(){ if (!enabled) return; clearMask(); - setDigit(0, hour(localTime) / 10); // Z1 - setDigit(1, hour(localTime) % 10); // Z2 - setDigit(2, minute(localTime) / 10); // Z3 - setDigit(3, minute(localTime) % 10); // Z4 - setDigit(4, second(localTime) / 10); // Z5 - setDigit(5, second(localTime) % 10); // Z6 - if(second(localTime) % 2){ - setSeparator(1, sepsOn); // Sep1 - setSeparator(2, sepsOn); // Sep2 + if (showClock && !showCountdown) { + drawClock(); + } else { + drawCountdown(); } applyMaskToStrip(); } - // ---- Info UI (collapsible group under "u") ---- + // Info UI (u-group) void addToJsonInfo(JsonObject& root) override { JsonObject user = root["u"].as(); if (user.isNull()) user = root.createNestedObject("u"); - - // Parent group (collapsible in UI depending on WLED UI build) JsonObject grp = user.createNestedObject(F("7 Segment Counter")); JsonArray state = grp.createNestedArray(F("state")); @@ -177,42 +172,84 @@ class Usermod7SegmentCountdown : public Usermod { JsonArray pl = grp.createNestedArray(F("panel leds")); pl.add(TOTAL_PANEL_LEDS); pl.add(F(" px")); + + JsonArray tgt = grp.createNestedArray(F("target")); + char buf[24]; + snprintf(buf, sizeof(buf), "%04d-%02u-%02u %02u:%02u", + targetYear, targetMonth, targetDay, targetHour, targetMinute); + tgt.add(buf); + tgt.add(""); } - // ---- Runtime control via /json/state ---- + // JSON state/config --------------------------------------------------------- void addToJsonState(JsonObject& root) override { JsonObject s = root[F("7seg")].as(); if (s.isNull()) s = root.createNestedObject(F("7seg")); s[F("enabled")] = enabled; + + s[F("targetYear")] = targetYear; + s[F("targetMonth")] = targetMonth; + s[F("targetDay")] = targetDay; + s[F("targetHour")] = targetHour; + s[F("targetMinute")] = targetMinute; + + s[F("showClock")] = showClock; + s[F("showCountdown")] = showCountdown; } void readFromJsonState(JsonObject& root) override { JsonObject s = root[F("7seg")].as(); if (s.isNull()) return; - if (s.containsKey(F("enabled"))) { - enabled = s[F("enabled")].as(); - } + if (s.containsKey(F("enabled"))) enabled = s[F("enabled")].as(); + + bool changed = false; + if (s.containsKey(F("targetYear"))) { targetYear = s[F("targetYear")].as(); changed = true; } + if (s.containsKey(F("targetMonth"))) { targetMonth = s[F("targetMonth")].as(); changed = true; } + if (s.containsKey(F("targetDay"))) { targetDay = s[F("targetDay")].as(); changed = true; } + if (s.containsKey(F("targetHour"))) { targetHour = s[F("targetHour")].as(); changed = true; } + if (s.containsKey(F("targetMinute"))) { targetMinute = s[F("targetMinute")].as();changed = true; } + + if (s.containsKey(F("showClock"))) showClock = s[F("showClock")].as(); + if (s.containsKey(F("showCountdown"))) showCountdown = s[F("showCountdown")].as(); + + if (changed) validateTarget(true); } - // ---- Persistent config (survives reboot) ---- void addToConfig(JsonObject& root) override { JsonObject s = root[F("7seg")].as(); if (s.isNull()) s = root.createNestedObject(F("7seg")); s[F("enabled")] = enabled; + + s[F("targetYear")] = targetYear; + s[F("targetMonth")] = targetMonth; + s[F("targetDay")] = targetDay; + s[F("targetHour")] = targetHour; + s[F("targetMinute")] = targetMinute; + + s[F("showClock")] = showClock; + s[F("showCountdown")] = showCountdown; } bool readFromConfig(JsonObject& root) override { JsonObject s = root[F("7seg")].as(); if (s.isNull()) return false; - enabled = s[F("enabled")] | true; // default true + enabled = s[F("enabled")] | true; + targetYear = s[F("targetYear")] | year(localTime); + targetMonth = s[F("targetMonth")] | 1; + targetDay = s[F("targetDay")] | 1; + targetHour = s[F("targetHour")] | 0; + targetMinute = s[F("targetMinute")] | 0; + + showClock = s[F("showClock")] | true; + showCountdown = s[F("showCountdown")] | false; + + validateTarget(true); return true; } - uint16_t getId() override { - return 0x7A01; - } + uint16_t getId() override { return 0x7A01; } }; static Usermod7SegmentCountdown usermod; diff --git a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h new file mode 100644 index 0000000000..6c56154fae --- /dev/null +++ b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h @@ -0,0 +1,100 @@ +#pragma once +#include + +// Lookup tables are defined in the .cpp and used by the 7-seg renderer. +extern const uint8_t USERMOD_7SEG_PHYS_TO_LOG[7]; // phys idx -> logical A..G (0..6) +extern const uint8_t USERMOD_7SEG_DIGIT_MASKS[10]; // numeric digit to segment bits + +// Panel geometry (LED counts) +static const uint16_t LEDS_PER_SEG = 5; +static const uint8_t SEGS_PER_DIGIT = 7; +static const uint16_t SEP_LEDS = 10; +static constexpr uint16_t LEDS_PER_DIGIT = LEDS_PER_SEG * SEGS_PER_DIGIT; // 35 +static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; // 230 + +// Runtime state used by the implementation +std::vector mask; // 1=keep current color/effect, 0=force black +bool enabled = true; +bool sepsOn = true; + +// Display mode flags (no logic here, just state) +bool showClock = true; +bool showCountdown = false; + +// Countdown target (local time) and derived UNIX timestamp +int targetYear = 2026; +uint8_t targetMonth = 1; // 1-12 +uint8_t targetDay = 1; // 1-31 +uint8_t targetHour = 0; // 0-23 +uint8_t targetMinute = 0; // 0-59 +time_t targetUnix = 0; + +// Remaining time parts and totals +uint32_t fullHours = 0; // up to 8760 +uint32_t fullMinutes = 0; // up to 525600 +uint32_t fullSeconds = 0; // up to 31536000 +uint32_t remDays = 0; +uint8_t remHours = 0; +uint8_t remMinutes = 0; +uint8_t remSeconds = 0; + +// Index helpers into the linear LED stream +static uint16_t digitBase(uint8_t d) { + switch (d) { + case 0: return 0; // Z1 + case 1: return 35; // Z2 + case 2: return 80; // Z3 + case 3: return 115; // Z4 + case 4: return 160; // Z5 + case 5: return 195; // Z6 + default: return 0; + } +} +static constexpr uint16_t sep1Base() { return 70; } +static constexpr uint16_t sep2Base() { return 150; } + +// 7-seg letter helper: returns mask bits A..G (0..6). +// Fallback (unsupported): segments A + D + G. +uint8_t LETTER_MASK(char c) { + switch (c) { + // Uppercase + case 'A': return 0b01110111; // A,B,C,E,F,G + case 'B': return 0b01111100; // b-like (C,D,E,F,G) + case 'C': return 0b00111001; // A,D,E,F + case 'D': return 0b01011110; // d-like (B,C,D,E,G) + case 'E': return 0b01111001; // A,D,E,F,G + case 'F': return 0b01110001; // A,E,F,G + case 'H': return 0b01110110; // B,C,E,F,G + case 'J': return 0b00011110; // B,C,D + case 'L': return 0b00111000; // D,E,F + case 'O': return 0b00111111; // A,B,C,D,E,F (like '0') + case 'P': return 0b01110011; // A,B,E,F,G + case 'T': return 0b01111000; // D,E,F,G + case 'U': return 0b00111110; // B,C,D,E,F + case 'Y': return 0b01101110; // B,C,D,F,G + + // Lowercase + case 'a': return 0b01011111; // A,B,C,D,E,G + case 'b': return 0b01111100; // C,D,E,F,G + case 'c': return 0b01011000; // D,E,G + case 'd': return 0b01011110; // B,C,D,E,G + case 'e': return 0b01111011; // A,D,E,F,G (with C off) + case 'f': return 0b01110001; // A,E,F,G + case 'h': return 0b01110100; // C,E,F,G + case 'j': return 0b00001110; // B,C,D + case 'l': return 0b00110000; // E,F + case 'n': return 0b01010100; // C,E,G + case 'o': return 0b01011100; // C,D,E,G + case 'r': return 0b01010000; // E,G + case 't': return 0b01111000; // D,E,F,G + case 'u': return 0b00011100; // C,D,E + case 'y': return 0b01101110; // B,C,D,F,G + + // Symbols often useful on 7-seg + case '-': return 0b01000000; // G + case '_': return 0b00001000; // D + case ' ': return 0b00000000; // blank + + default: return 0b01001001; // fallback: A, D, G + } +} From e0b014bf5460f6fabc63eaecb0283dc1830a6e35 Mon Sep 17 00:00:00 2001 From: dereibims Date: Sun, 4 Jan 2026 21:17:43 +0100 Subject: [PATCH 3/9] Finished... --- .../usermod_7segment_countdown.cpp | 182 ++++++++++++++++-- .../usermod_7segment_countdown.h | 10 + wled00/FX.cpp | 2 +- wled00/FX.h | 2 +- 4 files changed, 175 insertions(+), 21 deletions(-) diff --git a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp index 73af6db65f..6ef1acc3a0 100644 --- a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp +++ b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp @@ -45,18 +45,59 @@ class Usermod7SegmentCountdown : public Usermod { } } + //Time helpers -------------------------------------------------------------- + //hundreds of seconds calculation + int getHundredths(int currentSeconds) { + unsigned long now = millis(); + + // Sekunde hat sich geändert → neu synchronisieren + if (currentSeconds != lastSecondValue) { + lastSecondValue = currentSeconds; + lastSecondMillis = now; + return 99; // start at 99 when a new second begins (counting down) + } + + unsigned long delta = now - lastSecondMillis; + + // Count down from 99 to 0 across the second + int hundredths = 99 - (delta / 10); + if (hundredths < 0) hundredths = 0; + if (hundredths > 99) hundredths = 99; + + return hundredths; +} // Drawing helpers ----------------------------------------------------------- void drawClock() { - setDigit(0, hour(localTime) / 10); - setDigit(1, hour(localTime) % 10); - setDigit(2, minute(localTime) / 10); - setDigit(3, minute(localTime) % 10); - setDigit(4, second(localTime) / 10); - setDigit(5, second(localTime) % 10); - if (second(localTime) % 2) { + setDigitInt(0, hour(localTime) / 10); + setDigitInt(1, hour(localTime) % 10); + setDigitInt(2, minute(localTime) / 10); + setDigitInt(3, minute(localTime) % 10); + setDigitInt(4, second(localTime) / 10); + setDigitInt(5, second(localTime) % 10); + // Separator behavior for clock view is controlled by SeperatorOn/SeperatorOff: + // - both true: blink (as before) + // - SeperatorOn true only: always on + // - SeperatorOff true only: always off + // - fallback: blink + if (SeperatorOn && SeperatorOff) { + if (second(localTime) % 2) { + setSeparator(1, sepsOn); + setSeparator(2, sepsOn); + } + } + else if (SeperatorOn) { setSeparator(1, sepsOn); setSeparator(2, sepsOn); } + else if (SeperatorOff) { + // explicitly off for clock view → do nothing + } + else { + if (second(localTime) % 2) { + setSeparator(1, sepsOn); + setSeparator(2, sepsOn); + } + } } // Compute remaining time to targetUnix; also provide full totals (h/min/sec) @@ -71,10 +112,59 @@ class Usermod7SegmentCountdown : public Usermod { fullHours = diff / 3600u; fullMinutes = diff / 60u; fullSeconds = diff; + + // > 99 Tage + if (remDays > 99) { + setDigitChar(0, ' '); + setDigitInt(1, (remDays / 100) % 10); + setDigitInt(2, (remDays / 10) % 10); + setDigitInt(3, remDays % 10); + setDigitChar(4, 't'); + setDigitChar(5, ' '); + return; + } + + // < 99 Tage + if (remDays <=99 && fullHours > 99) { + setDigitInt(0, remDays / 10); + setDigitInt(1, remDays % 10); + setSeparator(1, sepsOn); + setDigitInt(2, remHours / 10); + setDigitInt(3, remHours % 10); + setSeparator(2, sepsOn); + setDigitInt(4, remMinutes / 10); + setDigitInt(5, remMinutes % 10); + return; + } + + // < 99 Stunden + if (fullHours <=99 && fullMinutes > 99) { + setDigitInt(0, fullHours / 10); + setDigitInt(1, fullHours % 10); + setSeparator(1, sepsOn); + setDigitInt(2, remMinutes / 10); + setDigitInt(3, remMinutes % 10); + setSeparator(2, sepsOn); + setDigitInt(4, remSeconds / 10); + setDigitInt(5, remSeconds % 10); + return; + } + + // < 99 Minuten → MM SS HH (Hundertstel) + int hs = getHundredths(remSeconds); + + setDigitInt(0, fullMinutes / 10); + setDigitInt(1, fullMinutes % 10); + setSeparatorHalf(1, true, sepsOn); // upper dot + setDigitInt(2, remSeconds / 10); + setDigitInt(3, remSeconds % 10); + setSeparator(2, sepsOn); + setDigitInt(4, hs / 10); + setDigitInt(5, hs % 10); } // Turn on segments for a single digit according to bitmask - void setDigit(uint8_t digitIndex, int8_t value) { + void setDigitInt(uint8_t digitIndex, int8_t value) { if (digitIndex > 5) return; if (value < 0) return; @@ -90,6 +180,21 @@ class Usermod7SegmentCountdown : public Usermod { } } } + void setDigitChar(uint8_t digitIndex, char c) { + if (digitIndex > 5) return; + + uint16_t base = digitBase(digitIndex); + uint8_t bits = LETTER_MASK(c); + + for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) { + uint8_t logSeg = PHYS_TO_LOG[physSeg]; + bool segOn = (bits >> logSeg) & 0x01; + if (segOn) { + uint16_t segStart = base + (uint16_t)physSeg * LEDS_PER_SEG; + setRangeOn(segStart, LEDS_PER_SEG); + } + } + } // Turn on both separator dots if requested void setSeparator(uint8_t which, bool on) { @@ -97,6 +202,14 @@ class Usermod7SegmentCountdown : public Usermod { if (on) setRangeOn(base, SEP_LEDS); } + // Turn on a single half (upper/lower) of the separator (each half = SEP_LEDS/2) + void setSeparatorHalf(uint8_t which, bool upper, bool on) { + uint16_t base = (which == 1) ? sep1Base() : sep2Base(); + uint16_t halfLen = SEP_LEDS / 2; + uint16_t start = base + (upper ? 0 : halfLen); + if (on) setRangeOn(start, halfLen); + } + // Apply mask to strip: 1 keeps color/effect, 0 forces black void applyMaskToStrip() { uint16_t stripLen = strip.getLengthTotal(); @@ -139,21 +252,35 @@ class Usermod7SegmentCountdown : public Usermod { public: void setup() override { ensureMaskSize(); - Serial.print("7Segment Setup - MOD: "); - Serial.println(enabled ? "enabled" : "disabled"); } void loop() override {} - void handleOverlayDraw(){ - if (!enabled) return; - clearMask(); - if (showClock && !showCountdown) { - drawClock(); - } else { - drawCountdown(); - } - applyMaskToStrip(); + void handleOverlayDraw() { + if (!enabled) return; + + clearMask(); + + // Beide aktiv -> im alternatingTime-Sekunden-Takt wechseln + if (showClock && showCountdown) { + uint32_t period = (alternatingTime > 0) ? (uint32_t)alternatingTime : 10U; + + // Blockindex (0,1,2,...) über Unix-Sekunden + uint32_t block = (uint32_t)localTime / period; + + // Gerade Blöcke: Uhr, ungerade: Countdown (oder umgekehrt, wenn du willst) + if ((block & 1U) == 0U) drawClock(); + else drawCountdown(); + } + else if (showClock) { + drawClock(); } + else { // showCountdown oder Default + drawCountdown(); + } + + applyMaskToStrip(); +} + // Info UI (u-group) void addToJsonInfo(JsonObject& root) override { @@ -169,6 +296,10 @@ class Usermod7SegmentCountdown : public Usermod { se.add(sepsOn ? F("on") : F("off")); se.add(""); + JsonArray se_cfg = grp.createNestedArray(F("seps cfg")); + se_cfg.add(SeperatorOn ? F("on") : F("off")); + se_cfg.add(SeperatorOff ? F("on") : F("off")); + JsonArray pl = grp.createNestedArray(F("panel leds")); pl.add(TOTAL_PANEL_LEDS); pl.add(F(" px")); @@ -187,6 +318,9 @@ class Usermod7SegmentCountdown : public Usermod { if (s.isNull()) s = root.createNestedObject(F("7seg")); s[F("enabled")] = enabled; + s[F("SeperatorOn")] = SeperatorOn; + s[F("SeperatorOff")] = SeperatorOff; + s[F("targetYear")] = targetYear; s[F("targetMonth")] = targetMonth; s[F("targetDay")] = targetDay; @@ -195,6 +329,7 @@ class Usermod7SegmentCountdown : public Usermod { s[F("showClock")] = showClock; s[F("showCountdown")] = showCountdown; + s[F("alternatingTime")] = alternatingTime; } void readFromJsonState(JsonObject& root) override { @@ -212,6 +347,9 @@ class Usermod7SegmentCountdown : public Usermod { if (s.containsKey(F("showClock"))) showClock = s[F("showClock")].as(); if (s.containsKey(F("showCountdown"))) showCountdown = s[F("showCountdown")].as(); + if (s.containsKey(F("alternatingTime"))) alternatingTime = s[F("alternatingTime")].as(); + if (s.containsKey(F("SeperatorOn"))) SeperatorOn = s[F("SeperatorOn")].as(); + if (s.containsKey(F("SeperatorOff"))) SeperatorOff = s[F("SeperatorOff")].as(); if (changed) validateTarget(true); } @@ -229,6 +367,9 @@ class Usermod7SegmentCountdown : public Usermod { s[F("showClock")] = showClock; s[F("showCountdown")] = showCountdown; + s[F("alternatingTime")] = alternatingTime; // seconds to alternate when both modes enabled + s[F("SeperatorOn")] = SeperatorOn; + s[F("SeperatorOff")] = SeperatorOff; } bool readFromConfig(JsonObject& root) override { @@ -244,6 +385,9 @@ class Usermod7SegmentCountdown : public Usermod { showClock = s[F("showClock")] | true; showCountdown = s[F("showCountdown")] | false; + alternatingTime = s[F("alternatingTime")] | 10; // default 10s + SeperatorOn = s[F("SeperatorOn")] | true; + SeperatorOff = s[F("SeperatorOff")] | true; validateTarget(true); return true; diff --git a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h index 6c56154fae..47e71094f9 100644 --- a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h +++ b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h @@ -16,10 +16,15 @@ static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; std::vector mask; // 1=keep current color/effect, 0=force black bool enabled = true; bool sepsOn = true; +// New UI-configurable separator flags (only affect drawClock()) +bool SeperatorOn = true; // when true, separators are forced on in clock view +bool SeperatorOff = true; // when true, separators are forced off in clock view // Display mode flags (no logic here, just state) bool showClock = true; bool showCountdown = false; +// Seconds to alternate between clock and countdown when both are enabled +uint16_t alternatingTime = 10; // default 10s // Countdown target (local time) and derived UNIX timestamp int targetYear = 2026; @@ -38,6 +43,11 @@ uint8_t remHours = 0; uint8_t remMinutes = 0; uint8_t remSeconds = 0; +// Timekeeping +unsigned long lastSecondMillis = 0; +int lastSecondValue = -1; + + // Index helpers into the linear LED stream static uint16_t digitBase(uint8_t d) { switch (d) { diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 685df03879..c3dbca2f3f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -133,7 +133,7 @@ static um_data_t* getAudioData() { */ uint16_t mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); - return strip.isOffRefreshRequired() ? FRAMETIME : 350; + return FRAMETIME; } static const char _data_FX_MODE_STATIC[] PROGMEM = "Solid"; diff --git a/wled00/FX.h b/wled00/FX.h index bcbab69a59..c4d21ca6f1 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -46,7 +46,7 @@ #define DEFAULT_MODE (uint8_t)0 #define DEFAULT_SPEED (uint8_t)128 #define DEFAULT_INTENSITY (uint8_t)128 -#define DEFAULT_COLOR (uint32_t)0xFFAA00 +#define DEFAULT_COLOR (uint32_t)0xFF0000 #define DEFAULT_C1 (uint8_t)128 #define DEFAULT_C2 (uint8_t)128 #define DEFAULT_C3 (uint8_t)16 From fafd14bf41912cefcbb58c2af85ed7326b73af70 Mon Sep 17 00:00:00 2001 From: dereibims Date: Sun, 4 Jan 2026 22:56:58 +0100 Subject: [PATCH 4/9] Main functionality done --- usermods/usermod_7segment_countdown/README.md | 130 ++++++++ .../usermod_7segment_countdown.cpp | 298 ++++++++++-------- .../usermod_7segment_countdown.h | 92 ++++-- 3 files changed, 365 insertions(+), 155 deletions(-) create mode 100644 usermods/usermod_7segment_countdown/README.md diff --git a/usermods/usermod_7segment_countdown/README.md b/usermods/usermod_7segment_countdown/README.md new file mode 100644 index 0000000000..7fe9fd99c2 --- /dev/null +++ b/usermods/usermod_7segment_countdown/README.md @@ -0,0 +1,130 @@ +# WLED Usermod: 7‑Segment Countdown Overlay + +A usermod that renders a six‑digit, two‑separator seven‑segment display as an overlay mask on top of WLED’s normal effects/colors. Lit “segments” preserve the underlying pixel color; all other pixels are forced to black. This lets you show a clock or a countdown without losing the active effect. + +## What it shows +- Clock: HH:MM:SS with configurable separator behavior (on/off/blink) +- Countdown to a target date/time + - > 99 days: ` ddd d` + - ≤ 99 days: `dd:hh:mm` + - ≤ 99 hours: `hh:mm:ss` + - ≤ 99 minutes: `MM·SS:hh` (upper dot between minutes and seconds, plus hundredths 00–99) +- If the target time is in the past, it counts up: + - < 60 s after target: `xxSSxx` + - < 60 min after target: `xxMM:SS` + - ≥ 60 min after target: falls back to the current clock + +Hundredths update smoothly within the current second and are clamped to 00–99. + +## Panel geometry (defaults) +- 6 digits, each with 7 segments +- `LEDS_PER_SEG = 5` → 35 LEDs per digit +- 2 separators between digits 2/3 and 4/5 with `SEP_LEDS = 10` each +- Total panel LEDs: `6×35 + 2×10 = 230` + +A physical‑to‑logical segment map (`PHYS_TO_LOG`) allows arbitrary wiring order while you draw by logical segments A..G. + +> Note: The current implementation hard‑codes digit and separator base indices for the defaults above. If you change `LEDS_PER_SEG` or `SEP_LEDS`, update `digitBase()` and `sep1Base()/sep2Base()` accordingly in the header so indices remain correct. + +## How it works (overlay mask) +- The usermod builds a per‑LED mask (vector of 0/1). +- For each frame, digits/separators set mask entries to 1. +- During `applyMaskToStrip()`, pixels with mask 0 are cleared to black; mask 1 keeps the existing effect color. + +## Build and enable +This usermod is already placed under `usermods/usermod_7segment_countdown` and can be enabled via PlatformIO environments that specify it in `custom_usermods`. + +- Recommended environment: `env:esp32dev` (includes `custom_usermods = usermod_7segment_countdown`). +- To include all usermods, you can use `env:usermods` (`custom_usermods = *`). + +Follow the project’s standard workflow: +1) Build web UI first: `npm run build` +2) Optional: run tests: `npm test` +3) Build firmware: `pio run -e esp32dev` + +> Ensure the device time is correct (NTP/timezone) so clock/countdown displays accurately. + +### Required core tweak for smooth hundredths +To guarantee that the hundredths display updates every frame without visible lag, the static effect’s frame timing must not idle the render loop. Adjust the return value of `mode_static()` in [wled00/FX.cpp](wled00/FX.cpp) to always return `FRAMETIME`. + +This ensures the render loop runs continuously so the overlay’s hundredths (00–99) are refreshed in real time. + +## Runtime configuration +All settings live under the `7seg` object in state/config JSON. + +- `enabled` (bool): Master on/off (default: true) +- `showClock` (bool): Show clock view (default: true) +- `showCountdown` (bool): Show countdown view (default: false) +- `alternatingTime` (uint, seconds): When both views are enabled, alternates every N seconds (default: 10) +- `SeperatorOn` (bool): Force separators on in clock mode +- `SeperatorOff` (bool): Force separators off in clock mode + - Both true or both false → blinking separators once per second +- Target date/time (local time): + - `targetYear` (int, 1970–2099) + - `targetMonth` (1–12) + - `targetDay` (1–31) + - `targetHour` (0–23) + - `targetMinute` (0–59) + +On any target change, the module validates/clamps values and recomputes `targetUnix`. + +## JSON API examples +Send JSON to WLED (HTTP or WebSocket). Minimal examples below use HTTP POST to `/json/state`. + +Enable overlay and show clock only: +```json +{ + "7seg": { + "enabled": true, + "showClock": true, + "showCountdown": false, + "SeperatorOn": true, + "SeperatorOff": false + } +} +``` + +Set a countdown target and enable alternating views every 15 seconds: +```json +{ + "7seg": { + "enabled": true, + "targetYear": 2026, + "targetMonth": 1, + "targetDay": 1, + "targetHour": 0, + "targetMinute": 0, + "showClock": true, + "showCountdown": true, + "alternatingTime": 15 + } +} +``` + +Turn the overlay off: +```json +{ "7seg": { "enabled": false } } +``` + +## UI integration +- The usermod adds a compact block to the “Info” screen: + - A small toggle button to enable/disable the overlay + - Status line showing active/disabled + - Clock separator policy (On / Off / Blinking) + - Mode (Clock / Countdown / Both) + - Target date and total panel LEDs + +## Time source +The usermod uses WLED’s `localTime`. Configure NTP and timezone in WLED so the clock and countdown are correct. + +## Notes and limitations +- This is an overlay: it does not draw its own colors. +- For non‑default panel geometries, update the index helpers as noted above. +- Separator half‑lighting is used to place an upper dot between minutes and seconds in the ≤99‑minute view. + +## Files +- `usermod_7segment_countdown.h` — constants, geometry, masks, helpers +- `usermod_7segment_countdown.cpp` — rendering, JSON/config wiring, registration + +## License +This usermod follows the WLED project license (see repository `LICENSE`). diff --git a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp index 6ef1acc3a0..173fd9697e 100644 --- a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp +++ b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp @@ -1,43 +1,22 @@ -// 7-segment countdown usermod overlay: builds a display mask over the LED strip +// 7-segment countdown usermod overlay +// Renders a 6-digit, two-separator seven-segment display as an overlay mask over the +// underlying WLED pixels. Lit mask pixels keep the original effect/color, masked pixels +// are forced to black. #include "wled.h" #include "usermod_7segment_countdown.h" -// Layout/Segment mapping (keep these comments) -// Phys segment order per digit (0..6): F-A-B-G-E-D-C -// Logical segment indices: A=0,B=1,C=2,D=3,E=4,F=5,G=6 -static constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { - 5, // F - 0, // A - 1, // B - 6, // G - 4, // E - 3, // D - 2 // C -}; - -// Digit bitmasks (bits A..G -> 0..6) (keep these comments) -static constexpr uint8_t DIGIT_MASKS[10] = { - 0b00111111, // 0 (A..F) - 0b00000110, // 1 (B,C) - 0b01011011, // 2 (A,B,D,E,G) - 0b01001111, // 3 (A,B,C,D,G) - 0b01100110, // 4 (B,C,F,G) - 0b01101101, // 5 (A,C,D,F,G) - 0b01111101, // 6 (A,C,D,E,F,G) - 0b00000111, // 7 (A,B,C) - 0b01111111, // 8 (A..G) - 0b01101111 // 9 (A,B,C,D,F,G) -}; class Usermod7SegmentCountdown : public Usermod { private: - // Mask helpers -------------------------------------------------------------- + // Ensures the mask buffer matches the panel size. void ensureMaskSize() { if (mask.size() != TOTAL_PANEL_LEDS) mask.assign(TOTAL_PANEL_LEDS, 0); } + // Clears the mask to fully transparent (all 0 → cleared when applied). void clearMask() { std::fill(mask.begin(), mask.end(), 0); } + // Sets a contiguous LED range in the mask to 1 (keep underlying pixel). void setRangeOn(uint16_t start, uint16_t len) { for (uint16_t i = 0; i < len; i++) { uint16_t idx = start + i; @@ -45,28 +24,37 @@ class Usermod7SegmentCountdown : public Usermod { } } - //Time helpers -------------------------------------------------------------- - //hundreds of seconds calculation - int getHundredths(int currentSeconds) { - unsigned long now = millis(); + // Time helpers -------------------------------------------------------------- + // Calculates hundredths of a second (0..99) within the current second. + // If countDown=true: 99→0; otherwise: 0→99. + int getHundredths(int currentSeconds, bool countDown) { + unsigned long now = millis(); + + // Resynchronize on second changes + if (currentSeconds != lastSecondValue) { + lastSecondValue = currentSeconds; + lastSecondMillis = now; + return countDown ? 99 : 0; // start at boundary + } - // Sekunde hat sich geändert → neu synchronisieren - if (currentSeconds != lastSecondValue) { - lastSecondValue = currentSeconds; - lastSecondMillis = now; - return 99; // start at 99 when a new second begins (counting down) - } + unsigned long delta = now - lastSecondMillis; - unsigned long delta = now - lastSecondMillis; + int hundredths; + if (countDown) { + // 99 → 0 across the second + hundredths = 99 - (int)(delta / 10); + } else { + // 0 → 99 across the second + hundredths = (int)(delta / 10); + } - // Count down from 99 to 0 across the second - int hundredths = 99 - (delta / 10); - if (hundredths < 0) hundredths = 0; - if (hundredths > 99) hundredths = 99; + if (hundredths < 0) hundredths = 0; + if (hundredths > 99) hundredths = 99; - return hundredths; -} + return hundredths; + } // Drawing helpers ----------------------------------------------------------- + // Draws HH:MM:SS into the mask. Separator behavior is controlled by SeperatorOn/Off. void drawClock() { setDigitInt(0, hour(localTime) / 10); setDigitInt(1, hour(localTime) % 10); @@ -74,11 +62,11 @@ class Usermod7SegmentCountdown : public Usermod { setDigitInt(3, minute(localTime) % 10); setDigitInt(4, second(localTime) / 10); setDigitInt(5, second(localTime) % 10); - // Separator behavior for clock view is controlled by SeperatorOn/SeperatorOff: - // - both true: blink (as before) - // - SeperatorOn true only: always on - // - SeperatorOff true only: always off - // - fallback: blink + // Separator rules: + // - both true: blink + // - only SeperatorOn: always on + // - only SeperatorOff: always off + // - neither: blink if (SeperatorOn && SeperatorOff) { if (second(localTime) % 2) { setSeparator(1, sepsOn); @@ -90,7 +78,7 @@ class Usermod7SegmentCountdown : public Usermod { setSeparator(2, sepsOn); } else if (SeperatorOff) { - // explicitly off for clock view → do nothing + // explicitly off → do nothing } else { if (second(localTime) % 2) { @@ -100,31 +88,62 @@ class Usermod7SegmentCountdown : public Usermod { } } - // Compute remaining time to targetUnix; also provide full totals (h/min/sec) + // Draws the countdown or count-up (if target is in the past) into the mask. void drawCountdown() { - int64_t diff = (int64_t)targetUnix - (int64_t)localTime; + int64_t diff = (int64_t)targetUnix - (int64_t)localTime; // >0 remaining, <0 passed + + // If the target is in the past, count up from the moment it was reached + bool countingUp = (diff < 0); + int64_t absDiff = abs(diff); // absolute difference for display + + if (countingUp && absDiff < 60) { + setDigitChar(0, 'x'); + setDigitChar(1, 'x'); + setDigitInt(2, absDiff / 10); + setDigitInt(3, absDiff % 10); + setDigitChar(4, 'x'); + setDigitChar(5, 'x'); + return; + } - remDays = diff / 86400u; - remHours = (uint8_t)((diff % 86400u) / 3600u); - remMinutes = (uint8_t)((diff % 3600u) / 60u); - remSeconds = (uint8_t)(diff % 60u); + if (countingUp && absDiff < 3600) { + setDigitChar(0, 'x'); + setDigitChar(1, 'x'); + setDigitInt(2, absDiff / 600); + setDigitInt(3, (absDiff / 60) % 10); + setSeparator(2, sepsOn); + setDigitInt(4, (absDiff % 60) / 10); + setDigitInt(5, (absDiff % 60) % 10); + return; + } + + if (countingUp >= 3600) { + drawClock(); + return; + } - fullHours = diff / 3600u; - fullMinutes = diff / 60u; - fullSeconds = diff; + // Split absolute difference into parts and totals + remDays = (uint32_t)(absDiff / 86400); + remHours = (uint8_t)((absDiff % 86400) / 3600); + remMinutes = (uint8_t)((absDiff % 3600) / 60); + remSeconds = (uint8_t)(absDiff % 60); - // > 99 Tage + fullHours = (uint32_t)(absDiff / 3600); + fullMinutes = (uint32_t)(absDiff / 60); + fullSeconds = (uint32_t)absDiff; + + // > 99 days → show ddd d if (remDays > 99) { setDigitChar(0, ' '); setDigitInt(1, (remDays / 100) % 10); setDigitInt(2, (remDays / 10) % 10); setDigitInt(3, remDays % 10); - setDigitChar(4, 't'); + setDigitChar(4, 'd'); setDigitChar(5, ' '); return; } - // < 99 Tage + // ≤ 99 days → show dd:hh:mm if (remDays <=99 && fullHours > 99) { setDigitInt(0, remDays / 10); setDigitInt(1, remDays % 10); @@ -137,7 +156,7 @@ class Usermod7SegmentCountdown : public Usermod { return; } - // < 99 Stunden + // ≤ 99 hours → show hh:mm:ss if (fullHours <=99 && fullMinutes > 99) { setDigitInt(0, fullHours / 10); setDigitInt(1, fullHours % 10); @@ -150,12 +169,12 @@ class Usermod7SegmentCountdown : public Usermod { return; } - // < 99 Minuten → MM SS HH (Hundertstel) - int hs = getHundredths(remSeconds); + // ≤ 99 minutes → MM'SS:hh (hundredths) + int hs = getHundredths(remSeconds, /*countDown*/ !countingUp); setDigitInt(0, fullMinutes / 10); setDigitInt(1, fullMinutes % 10); - setSeparatorHalf(1, true, sepsOn); // upper dot + setSeparatorHalf(1, true, sepsOn); // place an upper dot between minutes and seconds setDigitInt(2, remSeconds / 10); setDigitInt(3, remSeconds % 10); setSeparator(2, sepsOn); @@ -163,7 +182,7 @@ class Usermod7SegmentCountdown : public Usermod { setDigitInt(5, hs % 10); } - // Turn on segments for a single digit according to bitmask + // Lights segments for a single digit based on a numeric value (0..9). void setDigitInt(uint8_t digitIndex, int8_t value) { if (digitIndex > 5) return; if (value < 0) return; @@ -180,6 +199,7 @@ class Usermod7SegmentCountdown : public Usermod { } } } + // Lights segments for a single digit using a letter/symbol mask. void setDigitChar(uint8_t digitIndex, char c) { if (digitIndex > 5) return; @@ -196,13 +216,13 @@ class Usermod7SegmentCountdown : public Usermod { } } - // Turn on both separator dots if requested + // Lights both dots of a separator when requested. void setSeparator(uint8_t which, bool on) { uint16_t base = (which == 1) ? sep1Base() : sep2Base(); if (on) setRangeOn(base, SEP_LEDS); } - // Turn on a single half (upper/lower) of the separator (each half = SEP_LEDS/2) + // Lights a single half (upper/lower) of the separator; each half is SEP_LEDS/2. void setSeparatorHalf(uint8_t which, bool upper, bool on) { uint16_t base = (which == 1) ? sep1Base() : sep2Base(); uint16_t halfLen = SEP_LEDS / 2; @@ -210,7 +230,7 @@ class Usermod7SegmentCountdown : public Usermod { if (on) setRangeOn(start, halfLen); } - // Apply mask to strip: 1 keeps color/effect, 0 forces black + // Applies the mask to the WLED strip: 1 keeps pixel, 0 clears it to black. void applyMaskToStrip() { uint16_t stripLen = strip.getLengthTotal(); uint16_t limit = (stripLen < (uint16_t)mask.size()) ? stripLen : (uint16_t)mask.size(); @@ -224,7 +244,7 @@ class Usermod7SegmentCountdown : public Usermod { return (v < lo) ? lo : (v > hi ? hi : v); } - // Clamp target fields and derive targetUnix; optional debug on change + // Validates target fields, computes targetUnix, and optionally logs changes. void validateTarget(bool changed = false) { targetYear = clampVal(targetYear, 1970, 2099); targetMonth = clampVal(targetMonth, 1, 12); @@ -250,31 +270,34 @@ class Usermod7SegmentCountdown : public Usermod { } public: + // Prepare the mask buffer on boot. void setup() override { ensureMaskSize(); } + // No periodic work; rendering is driven by handleOverlayDraw(). void loop() override {} + // Main entry point from WLED to draw the overlay. void handleOverlayDraw() { if (!enabled) return; clearMask(); - // Beide aktiv -> im alternatingTime-Sekunden-Takt wechseln + // If both views are enabled, alternate every "alternatingTime" seconds. if (showClock && showCountdown) { uint32_t period = (alternatingTime > 0) ? (uint32_t)alternatingTime : 10U; - // Blockindex (0,1,2,...) über Unix-Sekunden + // Block index based on current time uint32_t block = (uint32_t)localTime / period; - // Gerade Blöcke: Uhr, ungerade: Countdown (oder umgekehrt, wenn du willst) + // Even blocks: clock; odd blocks: countdown if ((block & 1U) == 0U) drawClock(); else drawCountdown(); } else if (showClock) { drawClock(); } - else { // showCountdown oder Default + else { // countdown only (or default) drawCountdown(); } @@ -282,37 +305,53 @@ class Usermod7SegmentCountdown : public Usermod { } - // Info UI (u-group) + // Adds a compact UI block to the info screen (u-group). void addToJsonInfo(JsonObject& root) override { - JsonObject user = root["u"].as(); + JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); - JsonObject grp = user.createNestedObject(F("7 Segment Counter")); - - JsonArray state = grp.createNestedArray(F("state")); - state.add(enabled ? F("active") : F("disabled")); - state.add(""); - - JsonArray se = grp.createNestedArray(F("seps")); - se.add(sepsOn ? F("on") : F("off")); - se.add(""); - - JsonArray se_cfg = grp.createNestedArray(F("seps cfg")); - se_cfg.add(SeperatorOn ? F("on") : F("off")); - se_cfg.add(SeperatorOff ? F("on") : F("off")); - JsonArray pl = grp.createNestedArray(F("panel leds")); - pl.add(TOTAL_PANEL_LEDS); - pl.add(F(" px")); - - JsonArray tgt = grp.createNestedArray(F("target")); + // Top-level array for this usermod + JsonArray infoArr = user.createNestedArray(F("7 Segment Counter")); + + // Enable/disable button + String uiDomString = F(""); + infoArr.add(uiDomString); + + // State + infoArr = user.createNestedArray(F("Status")); + infoArr.add(enabled ? F("active") : F("disabled")); + + infoArr = user.createNestedArray(F("Clock Seperators")); + if (SeperatorOn && SeperatorOff) infoArr.add(F("Blinking")); + else if (SeperatorOn) infoArr.add(F("Always On")); + else if (SeperatorOff) infoArr.add(F("Always Off")); + else infoArr.add(F("Blinking")); + + // Modes + infoArr = user.createNestedArray(F("Mode")); + if (showClock && showCountdown) infoArr.add(F("Clock & Countdown")); + else if (showClock) infoArr.add(F("Clock Only")); + else infoArr.add(F("Countdown Only")); + + // Target + infoArr = user.createNestedArray(F("Target Date")); char buf[24]; snprintf(buf, sizeof(buf), "%04d-%02u-%02u %02u:%02u", targetYear, targetMonth, targetDay, targetHour, targetMinute); - tgt.add(buf); - tgt.add(""); + infoArr.add(buf); + + // Panel LEDs + infoArr = user.createNestedArray(F("Total Panel LEDs")); + infoArr.add(TOTAL_PANEL_LEDS); + infoArr.add(F(" px")); } - // JSON state/config --------------------------------------------------------- + // JSON state/config: persist and apply overlay parameters via state/config. void addToJsonState(JsonObject& root) override { JsonObject s = root[F("7seg")].as(); if (s.isNull()) s = root.createNestedObject(F("7seg")); @@ -355,45 +394,46 @@ class Usermod7SegmentCountdown : public Usermod { } void addToConfig(JsonObject& root) override { - JsonObject s = root[F("7seg")].as(); - if (s.isNull()) s = root.createNestedObject(F("7seg")); - s[F("enabled")] = enabled; - - s[F("targetYear")] = targetYear; - s[F("targetMonth")] = targetMonth; - s[F("targetDay")] = targetDay; - s[F("targetHour")] = targetHour; - s[F("targetMinute")] = targetMinute; - - s[F("showClock")] = showClock; - s[F("showCountdown")] = showCountdown; - s[F("alternatingTime")] = alternatingTime; // seconds to alternate when both modes enabled - s[F("SeperatorOn")] = SeperatorOn; - s[F("SeperatorOff")] = SeperatorOff; + JsonObject top = root.createNestedObject(F("7seg")); + top[F("enabled")] = enabled; + + top[F("targetYear")] = targetYear; + top[F("targetMonth")] = targetMonth; + top[F("targetDay")] = targetDay; + top[F("targetHour")] = targetHour; + top[F("targetMinute")] = targetMinute; + + top[F("showClock")] = showClock; + top[F("showCountdown")] = showCountdown; + top[F("alternatingTime")] = alternatingTime; // seconds to alternate when both modes enabled + top[F("SeperatorOn")] = SeperatorOn; + top[F("SeperatorOff")] = SeperatorOff; } bool readFromConfig(JsonObject& root) override { - JsonObject s = root[F("7seg")].as(); - if (s.isNull()) return false; + JsonObject top = root[F("7seg")]; + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[F("enabled")], enabled, true); - enabled = s[F("enabled")] | true; - targetYear = s[F("targetYear")] | year(localTime); - targetMonth = s[F("targetMonth")] | 1; - targetDay = s[F("targetDay")] | 1; - targetHour = s[F("targetHour")] | 0; - targetMinute = s[F("targetMinute")] | 0; + configComplete &= getJsonValue(top[F("targetYear")], targetYear, year(localTime)); + configComplete &= getJsonValue(top[F("targetMonth")], targetMonth, (uint8_t)1); + configComplete &= getJsonValue(top[F("targetDay")], targetDay, (uint8_t)1); + configComplete &= getJsonValue(top[F("targetHour")], targetHour, (uint8_t)0); + configComplete &= getJsonValue(top[F("targetMinute")], targetMinute, (uint8_t)0); - showClock = s[F("showClock")] | true; - showCountdown = s[F("showCountdown")] | false; - alternatingTime = s[F("alternatingTime")] | 10; // default 10s - SeperatorOn = s[F("SeperatorOn")] | true; - SeperatorOff = s[F("SeperatorOff")] | true; + configComplete &= getJsonValue(top[F("showClock")], showClock, true); + configComplete &= getJsonValue(top[F("showCountdown")], showCountdown, false); + configComplete &= getJsonValue(top[F("alternatingTime")], alternatingTime, (uint16_t)10); + configComplete &= getJsonValue(top[F("SeperatorOn")], SeperatorOn, true); + configComplete &= getJsonValue(top[F("SeperatorOff")], SeperatorOff, true); validateTarget(true); - return true; + return configComplete; } - uint16_t getId() override { return 0x7A01; } + // Unique usermod id (arbitrary, but stable). + uint16_t getId() override { return 0x22B8; } }; static Usermod7SegmentCountdown usermod; diff --git a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h index 47e71094f9..15b3f59a33 100644 --- a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h +++ b/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h @@ -1,32 +1,71 @@ #pragma once #include -// Lookup tables are defined in the .cpp and used by the 7-seg renderer. -extern const uint8_t USERMOD_7SEG_PHYS_TO_LOG[7]; // phys idx -> logical A..G (0..6) -extern const uint8_t USERMOD_7SEG_DIGIT_MASKS[10]; // numeric digit to segment bits +// Seven-segment overlay constants and utilities for a 6-digit panel with two separators. -// Panel geometry (LED counts) +// Logical segment indices (A..G) used to build bitmasks for digits/letters. +#define SEG_A 0 +#define SEG_B 1 +#define SEG_C 2 +#define SEG_D 3 +#define SEG_E 4 +#define SEG_F 5 +#define SEG_G 6 + +// Geometry: LED counts for a single digit and the full panel. +// - One digit has 7 segments, each with LEDS_PER_SEG pixels +// - Two separator blocks exist between digit 2/3 and 4/5, each with SEP_LEDS pixels static const uint16_t LEDS_PER_SEG = 5; static const uint8_t SEGS_PER_DIGIT = 7; static const uint16_t SEP_LEDS = 10; static constexpr uint16_t LEDS_PER_DIGIT = LEDS_PER_SEG * SEGS_PER_DIGIT; // 35 static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; // 230 -// Runtime state used by the implementation -std::vector mask; // 1=keep current color/effect, 0=force black +// Physical-to-logical segment mapping per digit: physical order F-A-B-G-E-D-C → logical A..G. +// This allows drawing by logical segment index while wiring can remain arbitrary. +static constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { + SEG_F, + SEG_A, + SEG_B, + SEG_G, + SEG_E, + SEG_D, + SEG_C +}; + +// Digit bitmasks (bits A..G correspond to indices 0..6). 1 means the segment is lit. +static constexpr uint8_t DIGIT_MASKS[10] = { + 0b00111111, // 0 (A..F) + 0b00000110, // 1 (B,C) + 0b01011011, // 2 (A,B,D,E,G) + 0b01001111, // 3 (A,B,C,D,G) + 0b01100110, // 4 (B,C,F,G) + 0b01101101, // 5 (A,C,D,F,G) + 0b01111101, // 6 (A,C,D,E,F,G) + 0b00000111, // 7 (A,B,C) + 0b01111111, // 8 (A..G) + 0b01101111 // 9 (A,B,C,D,F,G) +}; + +// Runtime state used by the overlay renderer and UI. +// - mask: per-LED on/off mask (1 keeps original color/effect, 0 forces black) +// - enabled: master on/off for the overlay +// - sepsOn: helper for separator drawing +// - SeperatorOn/Off: user-configurable flags controlling separator behavior in clock mode +std::vector mask; // 1 = keep underlying pixel, 0 = clear to black bool enabled = true; bool sepsOn = true; -// New UI-configurable separator flags (only affect drawClock()) -bool SeperatorOn = true; // when true, separators are forced on in clock view -bool SeperatorOff = true; // when true, separators are forced off in clock view +bool SeperatorOn = true; // force separators on in clock mode +bool SeperatorOff = true; // force separators off in clock mode -// Display mode flags (no logic here, just state) +// Display mode flags and timing: +// - showClock/showCountdown: which views to render +// - alternatingTime: seconds between views when both are enabled bool showClock = true; bool showCountdown = false; -// Seconds to alternate between clock and countdown when both are enabled -uint16_t alternatingTime = 10; // default 10s +uint16_t alternatingTime = 10; // seconds -// Countdown target (local time) and derived UNIX timestamp +// Countdown target (local time) and derived UNIX timestamp used for math. int targetYear = 2026; uint8_t targetMonth = 1; // 1-12 uint8_t targetDay = 1; // 1-31 @@ -34,7 +73,9 @@ uint8_t targetHour = 0; // 0-23 uint8_t targetMinute = 0; // 0-59 time_t targetUnix = 0; -// Remaining time parts and totals +// Remaining time parts and totals (updated each draw): +// - remX: time parts modulo their units +// - fullX: monotonically increasing totals (minutes, seconds, etc.) uint32_t fullHours = 0; // up to 8760 uint32_t fullMinutes = 0; // up to 525600 uint32_t fullSeconds = 0; // up to 31536000 @@ -43,28 +84,27 @@ uint8_t remHours = 0; uint8_t remMinutes = 0; uint8_t remSeconds = 0; -// Timekeeping +// Second-boundary tracking to compute smooth hundredths display. unsigned long lastSecondMillis = 0; int lastSecondValue = -1; - -// Index helpers into the linear LED stream +// Index helpers into the linear LED stream for each digit and separator block. static uint16_t digitBase(uint8_t d) { switch (d) { - case 0: return 0; // Z1 - case 1: return 35; // Z2 - case 2: return 80; // Z3 - case 3: return 115; // Z4 - case 4: return 160; // Z5 - case 5: return 195; // Z6 + case 0: return 0; // digit 1 + case 1: return 35; // digit 2 + case 2: return 80; // digit 3 + case 3: return 115; // digit 4 + case 4: return 160; // digit 5 + case 5: return 195; // digit 6 default: return 0; } } static constexpr uint16_t sep1Base() { return 70; } static constexpr uint16_t sep2Base() { return 150; } -// 7-seg letter helper: returns mask bits A..G (0..6). -// Fallback (unsupported): segments A + D + G. +// Letter-to-segment mapping for a 7-seg display. +// Returns a bitmask A..G (0..6). If the character is unsupported, fallback lights A, D, G. uint8_t LETTER_MASK(char c) { switch (c) { // Uppercase @@ -100,7 +140,7 @@ uint8_t LETTER_MASK(char c) { case 'u': return 0b00011100; // C,D,E case 'y': return 0b01101110; // B,C,D,F,G - // Symbols often useful on 7-seg + // Symbols commonly used on 7-seg case '-': return 0b01000000; // G case '_': return 0b00001000; // D case ' ': return 0b00000000; // blank From bd51f1ced59d097c8f2c7c7eed1b558e3e627361 Mon Sep 17 00:00:00 2001 From: dereibims Date: Thu, 8 Jan 2026 20:38:04 +0100 Subject: [PATCH 5/9] Name Change --- platformio.ini | 4 +- .../7_seg_clock_countdown.cpp} | 272 +++++++++++++----- .../7_seg_clock_countdown.h} | 52 +++- .../README.md | 0 .../library.json | 6 +- 5 files changed, 246 insertions(+), 88 deletions(-) rename usermods/{usermod_7segment_countdown/usermod_7segment_countdown.cpp => 7_seg_clock_countdown/7_seg_clock_countdown.cpp} (67%) rename usermods/{usermod_7segment_countdown/usermod_7segment_countdown.h => 7_seg_clock_countdown/7_seg_clock_countdown.h} (84%) rename usermods/{usermod_7segment_countdown => 7_seg_clock_countdown}/README.md (100%) rename usermods/{usermod_7segment_countdown => 7_seg_clock_countdown}/library.json (54%) diff --git a/platformio.ini b/platformio.ini index 01ed17f104..d84e4c4f45 100644 --- a/platformio.ini +++ b/platformio.ini @@ -438,14 +438,14 @@ board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} -custom_usermods = usermod_7segment_countdown +custom_usermods = 7_seg_clock_countdown build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 - -D WLED_DEBUG lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = dio +upload_protocol = esptool [env:esp32dev_debug] extends = env:esp32dev diff --git a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp similarity index 67% rename from usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp rename to usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp index 173fd9697e..c61a90f2b8 100644 --- a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.cpp +++ b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp @@ -3,10 +3,10 @@ // underlying WLED pixels. Lit mask pixels keep the original effect/color, masked pixels // are forced to black. #include "wled.h" -#include "usermod_7segment_countdown.h" +#include "7_seg_clock_countdown.h" -class Usermod7SegmentCountdown : public Usermod { +class SevenSegClockCountdown : public Usermod { private: // Ensures the mask buffer matches the panel size. void ensureMaskSize() { @@ -53,9 +53,11 @@ class Usermod7SegmentCountdown : public Usermod { return hundredths; } + // Drawing helpers ----------------------------------------------------------- // Draws HH:MM:SS into the mask. Separator behavior is controlled by SeperatorOn/Off. void drawClock() { + clearMask(); setDigitInt(0, hour(localTime) / 10); setDigitInt(1, hour(localTime) % 10); setDigitInt(2, minute(localTime) / 10); @@ -88,25 +90,84 @@ class Usermod7SegmentCountdown : public Usermod { } } + void revertMode(){ + Segment& selseg = strip.getSegment(0); + selseg.setMode(prevMode, false); + selseg.setColor(0, prevColor); + selseg.speed=prevSpeed; + selseg.intensity=prevIntensity; + strip.setBrightness(prevBrightness); + } + + void SaveMode(){ + Segment& selseg = strip.getSegment(0); + prevMode=selseg.mode; + prevColor=selseg.colors[0]; + prevSpeed=selseg.speed; + prevIntensity=selseg.intensity; + prevBrightness=bri; + } + + void setMode(uint8_t mode, uint32_t color, uint8_t speed, uint8_t intensity, uint8_t brightness){ + Segment& selseg = strip.getSegment(0); + selseg.setMode(mode, false); + selseg.setColor(0, color); + selseg.speed=speed; + selseg.intensity=intensity; + strip.setBrightness(brightness); + } + // Draws the countdown or count-up (if target is in the past) into the mask. void drawCountdown() { + clearMask(); int64_t diff = (int64_t)targetUnix - (int64_t)localTime; // >0 remaining, <0 passed // If the target is in the past, count up from the moment it was reached bool countingUp = (diff < 0); int64_t absDiff = abs(diff); // absolute difference for display - if (countingUp && absDiff < 60) { + if(countingUp && absDiff > 3600) { + if(ModeChanged){ + revertMode(); + ModeChanged = false; + } + drawClock(); + return; + } + + if(countingUp){ + Segment& selseg = strip.getSegment(0); + if(!ModeChanged){ + SaveMode(); + setMode(FX_MODE_STATIC, 0xFF0000, 128, 128, 255); + ModeChanged = true; + IgnoreBlinking = false; + } + if(selseg.mode != FX_MODE_STATIC && ModeChanged && !IgnoreBlinking){ + IgnoreBlinking=true; + revertMode(); + } + + if(millis() - lastBlink >= countdownBlinkInterval){ + BlinkToggle = !BlinkToggle; + lastBlink = millis(); + } + + if(BlinkToggle && !IgnoreBlinking){ + strip.setBrightness(255); + } else if (!BlinkToggle && !IgnoreBlinking){ + strip.setBrightness(0); + } + + if (absDiff < 60) { setDigitChar(0, 'x'); setDigitChar(1, 'x'); setDigitInt(2, absDiff / 10); setDigitInt(3, absDiff % 10); setDigitChar(4, 'x'); setDigitChar(5, 'x'); - return; } - - if (countingUp && absDiff < 3600) { + if (absDiff >= 60) { setDigitChar(0, 'x'); setDigitChar(1, 'x'); setDigitInt(2, absDiff / 600); @@ -114,72 +175,76 @@ class Usermod7SegmentCountdown : public Usermod { setSeparator(2, sepsOn); setDigitInt(4, (absDiff % 60) / 10); setDigitInt(5, (absDiff % 60) % 10); - return; } + } + - if (countingUp >= 3600) { - drawClock(); - return; - } + if(!countingUp){ + // Cleanup from counting up mode + if(ModeChanged){ + revertMode(); + ModeChanged = false; + } - // Split absolute difference into parts and totals - remDays = (uint32_t)(absDiff / 86400); - remHours = (uint8_t)((absDiff % 86400) / 3600); - remMinutes = (uint8_t)((absDiff % 3600) / 60); - remSeconds = (uint8_t)(absDiff % 60); - - fullHours = (uint32_t)(absDiff / 3600); - fullMinutes = (uint32_t)(absDiff / 60); - fullSeconds = (uint32_t)absDiff; - - // > 99 days → show ddd d - if (remDays > 99) { - setDigitChar(0, ' '); - setDigitInt(1, (remDays / 100) % 10); - setDigitInt(2, (remDays / 10) % 10); - setDigitInt(3, remDays % 10); - setDigitChar(4, 'd'); - setDigitChar(5, ' '); - return; - } + // Split absolute difference into parts and totals + remDays = (uint32_t)(absDiff / 86400); + remHours = (uint8_t)((absDiff % 86400) / 3600); + remMinutes = (uint8_t)((absDiff % 3600) / 60); + remSeconds = (uint8_t)(absDiff % 60); + + fullHours = (uint32_t)(absDiff / 3600); + fullMinutes = (uint32_t)(absDiff / 60); + fullSeconds = (uint32_t)absDiff; + + // > 99 days → show ddd d + if (remDays > 99) { + setDigitChar(0, ' '); + setDigitInt(1, (remDays / 100) % 10); + setDigitInt(2, (remDays / 10) % 10); + setDigitInt(3, remDays % 10); + setDigitChar(4, 'd'); + setDigitChar(5, ' '); + return; + } - // ≤ 99 days → show dd:hh:mm - if (remDays <=99 && fullHours > 99) { - setDigitInt(0, remDays / 10); - setDigitInt(1, remDays % 10); - setSeparator(1, sepsOn); - setDigitInt(2, remHours / 10); - setDigitInt(3, remHours % 10); - setSeparator(2, sepsOn); - setDigitInt(4, remMinutes / 10); - setDigitInt(5, remMinutes % 10); - return; - } + // ≤ 99 days → show dd:hh:mm + if (remDays <=99 && fullHours > 99) { + setDigitInt(0, remDays / 10); + setDigitInt(1, remDays % 10); + setSeparator(1, sepsOn); + setDigitInt(2, remHours / 10); + setDigitInt(3, remHours % 10); + setSeparator(2, sepsOn); + setDigitInt(4, remMinutes / 10); + setDigitInt(5, remMinutes % 10); + return; + } - // ≤ 99 hours → show hh:mm:ss - if (fullHours <=99 && fullMinutes > 99) { - setDigitInt(0, fullHours / 10); - setDigitInt(1, fullHours % 10); - setSeparator(1, sepsOn); - setDigitInt(2, remMinutes / 10); - setDigitInt(3, remMinutes % 10); - setSeparator(2, sepsOn); - setDigitInt(4, remSeconds / 10); - setDigitInt(5, remSeconds % 10); - return; - } + // ≤ 99 hours → show hh:mm:ss + if (fullHours <=99 && fullMinutes > 99) { + setDigitInt(0, fullHours / 10); + setDigitInt(1, fullHours % 10); + setSeparator(1, sepsOn); + setDigitInt(2, remMinutes / 10); + setDigitInt(3, remMinutes % 10); + setSeparator(2, sepsOn); + setDigitInt(4, remSeconds / 10); + setDigitInt(5, remSeconds % 10); + return; + } - // ≤ 99 minutes → MM'SS:hh (hundredths) - int hs = getHundredths(remSeconds, /*countDown*/ !countingUp); - - setDigitInt(0, fullMinutes / 10); - setDigitInt(1, fullMinutes % 10); - setSeparatorHalf(1, true, sepsOn); // place an upper dot between minutes and seconds - setDigitInt(2, remSeconds / 10); - setDigitInt(3, remSeconds % 10); - setSeparator(2, sepsOn); - setDigitInt(4, hs / 10); - setDigitInt(5, hs % 10); + // ≤ 99 minutes → MM'SS:hh (hundredths) + int hs = getHundredths(remSeconds, /*countDown*/ !countingUp); + + setDigitInt(0, fullMinutes / 10); + setDigitInt(1, fullMinutes % 10); + setSeparatorHalf(1, true, sepsOn); // place an upper dot between minutes and seconds + setDigitInt(2, remSeconds / 10); + setDigitInt(3, remSeconds % 10); + setSeparator(2, sepsOn); + setDigitInt(4, hs / 10); + setDigitInt(5, hs % 10); + } } // Lights segments for a single digit based on a numeric value (0..9). @@ -281,8 +346,6 @@ class Usermod7SegmentCountdown : public Usermod { void handleOverlayDraw() { if (!enabled) return; - clearMask(); - // If both views are enabled, alternate every "alternatingTime" seconds. if (showClock && showCountdown) { uint32_t period = (alternatingTime > 0) ? (uint32_t)alternatingTime : 10U; @@ -432,9 +495,80 @@ class Usermod7SegmentCountdown : public Usermod { return configComplete; } + void onMqttConnect(bool sessionPresent) override { + String topic = mqttDeviceTopic; + topic += MQTT_Topic; + mqtt->subscribe(topic.c_str(), 0); + } + + bool onMqttMessage(char* topic, char* payload) { + String topicStr = String(topic); + + if (!topicStr.startsWith(F("/7seg/"))) return false; + + String subTopic = topicStr.substring(strlen("/7seg/")); + if (subTopic.indexOf(F("enabled")) >= 0) { + String payloadStr = String(payload); + enabled = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + if (subTopic.indexOf(F("targetYear")) >= 0) { + String payloadStr = String(payload); + targetYear = payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("targetMonth")) >= 0) { + String payloadStr = String(payload); + targetMonth = (uint8_t)payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("targetDay")) >= 0) { + String payloadStr = String(payload); + targetDay = (uint8_t)payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("targetHour")) >= 0) { + String payloadStr = String(payload); + targetHour = (uint8_t)payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("targetMinute")) >= 0) { + String payloadStr = String(payload); + targetMinute = (uint8_t)payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("showClock")) >= 0) { + String payloadStr = String(payload); + showClock = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + if (subTopic.indexOf(F("showCountdown")) >= 0) { + String payloadStr = String(payload); + showCountdown = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + if (subTopic.indexOf(F("alternatingTime")) >= 0) { + String payloadStr = String(payload); + alternatingTime = (uint16_t)payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("SeperatorOn")) >= 0) { + String payloadStr = String(payload); + SeperatorOn = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + if (subTopic.indexOf(F("SeperatorOff")) >= 0) { + String payloadStr = String(payload); + SeperatorOff = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + + return false; + } + // Unique usermod id (arbitrary, but stable). uint16_t getId() override { return 0x22B8; } }; -static Usermod7SegmentCountdown usermod; +static SevenSegClockCountdown usermod; REGISTER_USERMOD(usermod); diff --git a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h similarity index 84% rename from usermods/usermod_7segment_countdown/usermod_7segment_countdown.h rename to usermods/7_seg_clock_countdown/7_seg_clock_countdown.h index 15b3f59a33..e70fc9ec05 100644 --- a/usermods/usermod_7segment_countdown/usermod_7segment_countdown.h +++ b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h @@ -12,6 +12,10 @@ #define SEG_F 5 #define SEG_G 6 +/*-------------------------------Begin modifications down here!!!------------------------ +----------------------------------------------------------------------------------------- +-----------------------------------------------------------------------------------------*/ + // Geometry: LED counts for a single digit and the full panel. // - One digit has 7 segments, each with LEDS_PER_SEG pixels // - Two separator blocks exist between digit 2/3 and 4/5, each with SEP_LEDS pixels @@ -21,6 +25,8 @@ static const uint16_t SEP_LEDS = 10; static constexpr uint16_t LEDS_PER_DIGIT = LEDS_PER_SEG * SEGS_PER_DIGIT; // 35 static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; // 230 +static const char* MQTT_Topic = "7seg"; + // Physical-to-logical segment mapping per digit: physical order F-A-B-G-E-D-C → logical A..G. // This allows drawing by logical segment index while wiring can remain arbitrary. static constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { @@ -33,6 +39,28 @@ static constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { SEG_C }; +// Index helpers into the linear LED stream for each digit and separator block. +static uint16_t digitBase(uint8_t d) { + switch (d) { + case 0: return 0; // digit 1 + case 1: return 35; // digit 2 + case 2: return 80; // digit 3 + case 3: return 115; // digit 4 + case 4: return 160; // digit 5 + case 5: return 195; // digit 6 + default: return 0; + } +} +static constexpr uint16_t sep1Base() { return 70; } +static constexpr uint16_t sep2Base() { return 150; } + +// Interval the coundtown digits blink when counting up (in ms). +static unsigned long countdownBlinkInterval = 500; + +/*-------------------------------------------------------------------------------------- +--------------------------Do NOT edit anything below this line-------------------------- +--------------------------------------------------------------------------------------*/ + // Digit bitmasks (bits A..G correspond to indices 0..6). 1 means the segment is lit. static constexpr uint8_t DIGIT_MASKS[10] = { 0b00111111, // 0 (A..F) @@ -88,20 +116,16 @@ uint8_t remSeconds = 0; unsigned long lastSecondMillis = 0; int lastSecondValue = -1; -// Index helpers into the linear LED stream for each digit and separator block. -static uint16_t digitBase(uint8_t d) { - switch (d) { - case 0: return 0; // digit 1 - case 1: return 35; // digit 2 - case 2: return 80; // digit 3 - case 3: return 115; // digit 4 - case 4: return 160; // digit 5 - case 5: return 195; // digit 6 - default: return 0; - } -} -static constexpr uint16_t sep1Base() { return 70; } -static constexpr uint16_t sep2Base() { return 150; } +// To keep track of the mode before switching to counting up. +bool IgnoreBlinking = false; +bool ModeChanged = false; +uint8_t prevMode=0; +uint32_t prevColor=0; +uint8_t prevSpeed=0; +uint8_t prevIntensity=0; +uint8_t prevBrightness=0; +unsigned long lastBlink = 0; +bool BlinkToggle = false; // Letter-to-segment mapping for a 7-seg display. // Returns a bitmask A..G (0..6). If the character is unsupported, fallback lights A, D, G. diff --git a/usermods/usermod_7segment_countdown/README.md b/usermods/7_seg_clock_countdown/README.md similarity index 100% rename from usermods/usermod_7segment_countdown/README.md rename to usermods/7_seg_clock_countdown/README.md diff --git a/usermods/usermod_7segment_countdown/library.json b/usermods/7_seg_clock_countdown/library.json similarity index 54% rename from usermods/usermod_7segment_countdown/library.json rename to usermods/7_seg_clock_countdown/library.json index 548cea325a..59d224d92c 100644 --- a/usermods/usermod_7segment_countdown/library.json +++ b/usermods/7_seg_clock_countdown/library.json @@ -1,10 +1,10 @@ { - "name": "usermod_7segment_countdown", + "name": "7_seg_clock_countdown", "version": "0.1.0", - "description": "7-segment clock and countdown usermod for WLED", + "description": "7-segment clock and countdown", "authors": [ { - "name": "Phips" + "name": "DereIBims" } ], "frameworks": ["arduino"], From eaf0b9d44d922eb3bdbbb93133395c234df1100c Mon Sep 17 00:00:00 2001 From: dereibims Date: Sun, 11 Jan 2026 22:00:15 +0100 Subject: [PATCH 6/9] Usermod V0.1.0 Finished --- .../7_seg_clock_countdown.cpp | 1062 +++++++++-------- .../7_seg_clock_countdown.h | 384 +++--- usermods/7_seg_clock_countdown/README.md | 68 +- 3 files changed, 849 insertions(+), 665 deletions(-) diff --git a/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp index c61a90f2b8..2db60be5f2 100644 --- a/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp +++ b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp @@ -1,165 +1,297 @@ -// 7-segment countdown usermod overlay -// Renders a 6-digit, two-separator seven-segment display as an overlay mask over the -// underlying WLED pixels. Lit mask pixels keep the original effect/color, masked pixels -// are forced to black. +/* + 7-segment countdown usermod overlay + Renders a 6-digit, two-separator seven-segment display as a transparency mask + over the underlying WLED pixels. Mask bit 1 keeps the original effect/color; + mask bit 0 forces black. +*/ #include "wled.h" #include "7_seg_clock_countdown.h" +/* ===== Static member definitions ===== */ +constexpr uint8_t SevenSegClockCountdown::PHYS_TO_LOG[SevenSegClockCountdown::SEGS_PER_DIGIT]; +constexpr uint8_t SevenSegClockCountdown::DIGIT_MASKS[10]; -class SevenSegClockCountdown : public Usermod { -private: - // Ensures the mask buffer matches the panel size. - void ensureMaskSize() { - if (mask.size() != TOTAL_PANEL_LEDS) mask.assign(TOTAL_PANEL_LEDS, 0); +/* ===== Lifecycle ===== */ +// Prepare mask buffer on boot. +void SevenSegClockCountdown::setup() +{ + ensureMaskSize(); +} + +// Idle loop (no periodic work required). +void SevenSegClockCountdown::loop() +{ +} + +/* ===== Mask management ===== */ +// Ensure the mask buffer matches the panel size. +void SevenSegClockCountdown::ensureMaskSize() +{ + if (mask.size() != TOTAL_PANEL_LEDS) + mask.assign(TOTAL_PANEL_LEDS, 0); +} + +// Clear the mask to fully black. +void SevenSegClockCountdown::clearMask() +{ + std::fill(mask.begin(), mask.end(), 0); +} + +// Set a contiguous LED range in the mask to 1. +void SevenSegClockCountdown::setRangeOn(uint16_t start, uint16_t len) +{ + for (uint16_t i = 0; i < len; i++) + { + uint16_t idx = start + i; + if (idx < mask.size()) + mask[idx] = 1; + } +} + +/* ===== Time helpers ===== */ +// Return hundredths-of-second (0..99); descending in countdown mode. +int SevenSegClockCountdown::getHundredths(int currentSeconds, bool countDown) +{ + unsigned long now = millis(); + if (currentSeconds != lastSecondValue) + { + lastSecondValue = currentSeconds; + lastSecondMillis = now; + return countDown ? 99 : 0; } - // Clears the mask to fully transparent (all 0 → cleared when applied). - void clearMask() { - std::fill(mask.begin(), mask.end(), 0); + unsigned long delta = now - lastSecondMillis; + int hundredths; + if (countDown) + { + hundredths = 99 - (int)(delta / 10); } - // Sets a contiguous LED range in the mask to 1 (keep underlying pixel). - void setRangeOn(uint16_t start, uint16_t len) { - for (uint16_t i = 0; i < len; i++) { - uint16_t idx = start + i; - if (idx < mask.size()) mask[idx] = 1; - } + else + { + hundredths = (int)(delta / 10); } + if (hundredths < 0) + hundredths = 0; + if (hundredths > 99) + hundredths = 99; + return hundredths; +} - // Time helpers -------------------------------------------------------------- - // Calculates hundredths of a second (0..99) within the current second. - // If countDown=true: 99→0; otherwise: 0→99. - int getHundredths(int currentSeconds, bool countDown) { - unsigned long now = millis(); - - // Resynchronize on second changes - if (currentSeconds != lastSecondValue) { - lastSecondValue = currentSeconds; - lastSecondMillis = now; - return countDown ? 99 : 0; // start at boundary - } +/* ===== Mode management (save/restore base effect) ===== */ +// Restore previously saved effect mode and parameters. +void SevenSegClockCountdown::revertMode() +{ + Segment &selseg = strip.getSegment(0); + selseg.setMode(prevMode, false); + selseg.setColor(0, prevColor); + selseg.speed = prevSpeed; + selseg.intensity = prevIntensity; + strip.setBrightness(prevBrightness); +} - unsigned long delta = now - lastSecondMillis; +// Save current effect mode and parameters. +void SevenSegClockCountdown::SaveMode() +{ + Segment &selseg = strip.getSegment(0); + prevMode = selseg.mode; + prevColor = selseg.colors[0]; + prevSpeed = selseg.speed; + prevIntensity = selseg.intensity; + prevBrightness = bri; +} - int hundredths; - if (countDown) { - // 99 → 0 across the second - hundredths = 99 - (int)(delta / 10); - } else { - // 0 → 99 across the second - hundredths = (int)(delta / 10); - } +// Apply a specific effect mode and parameters. +void SevenSegClockCountdown::setMode(uint8_t mode, uint32_t color, uint8_t speed, uint8_t intensity, uint8_t brightness) +{ + Segment &selseg = strip.getSegment(0); + selseg.setMode(mode, false); + selseg.setColor(0, color); + selseg.speed = speed; + selseg.intensity = intensity; + strip.setBrightness(brightness); +} - if (hundredths < 0) hundredths = 0; - if (hundredths > 99) hundredths = 99; - - return hundredths; - } - - // Drawing helpers ----------------------------------------------------------- - // Draws HH:MM:SS into the mask. Separator behavior is controlled by SeperatorOn/Off. - void drawClock() { - clearMask(); - setDigitInt(0, hour(localTime) / 10); - setDigitInt(1, hour(localTime) % 10); - setDigitInt(2, minute(localTime) / 10); - setDigitInt(3, minute(localTime) % 10); - setDigitInt(4, second(localTime) / 10); - setDigitInt(5, second(localTime) % 10); - // Separator rules: - // - both true: blink - // - only SeperatorOn: always on - // - only SeperatorOff: always off - // - neither: blink - if (SeperatorOn && SeperatorOff) { - if (second(localTime) % 2) { - setSeparator(1, sepsOn); - setSeparator(2, sepsOn); - } - } - else if (SeperatorOn) { +/* ===== Drawing helpers ===== */ +// Draw HH:MM:SS into the mask with configurable separator behavior. +void SevenSegClockCountdown::drawClock() +{ + clearMask(); + setDigitInt(0, hour(localTime) / 10); + setDigitInt(1, hour(localTime) % 10); + setDigitInt(2, minute(localTime) / 10); + setDigitInt(3, minute(localTime) % 10); + setDigitInt(4, second(localTime) / 10); + setDigitInt(5, second(localTime) % 10); + + if (SeperatorOn && SeperatorOff) + { + if (second(localTime) % 2) + { setSeparator(1, sepsOn); setSeparator(2, sepsOn); } - else if (SeperatorOff) { - // explicitly off → do nothing - } - else { - if (second(localTime) % 2) { - setSeparator(1, sepsOn); - setSeparator(2, sepsOn); - } + } + else if (SeperatorOn) + { + setSeparator(1, sepsOn); + setSeparator(2, sepsOn); + } + else if (SeperatorOff) + { + } + else + { + if (second(localTime) % 2) + { + setSeparator(1, sepsOn); + setSeparator(2, sepsOn); } } +} - void revertMode(){ - Segment& selseg = strip.getSegment(0); - selseg.setMode(prevMode, false); - selseg.setColor(0, prevColor); - selseg.speed=prevSpeed; - selseg.intensity=prevIntensity; - strip.setBrightness(prevBrightness); +// Light segments for a single digit from a numeric value (0..9). +void SevenSegClockCountdown::setDigitInt(uint8_t digitIndex, int8_t value) +{ + if (digitIndex > 5) + return; + if (value < 0) + return; + uint16_t base = digitBase(digitIndex); + uint8_t bits = DIGIT_MASKS[(uint8_t)value]; + for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) + { + uint8_t logSeg = PHYS_TO_LOG[physSeg]; + bool segOn = (bits >> logSeg) & 0x01; + if (segOn) + { + uint16_t segStart = base + (uint16_t)physSeg * LEDS_PER_SEG; + setRangeOn(segStart, LEDS_PER_SEG); + } } +} - void SaveMode(){ - Segment& selseg = strip.getSegment(0); - prevMode=selseg.mode; - prevColor=selseg.colors[0]; - prevSpeed=selseg.speed; - prevIntensity=selseg.intensity; - prevBrightness=bri; +// Light segments for a single digit using a letter/symbol mask. +void SevenSegClockCountdown::setDigitChar(uint8_t digitIndex, char c) +{ + if (digitIndex > 5) + return; + uint16_t base = digitBase(digitIndex); + uint8_t bits = LETTER_MASK(c); + for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) + { + uint8_t logSeg = PHYS_TO_LOG[physSeg]; + bool segOn = (bits >> logSeg) & 0x01; + if (segOn) + { + uint16_t segStart = base + (uint16_t)physSeg * LEDS_PER_SEG; + setRangeOn(segStart, LEDS_PER_SEG); + } } +} - void setMode(uint8_t mode, uint32_t color, uint8_t speed, uint8_t intensity, uint8_t brightness){ - Segment& selseg = strip.getSegment(0); - selseg.setMode(mode, false); - selseg.setColor(0, color); - selseg.speed=speed; - selseg.intensity=intensity; - strip.setBrightness(brightness); - } +// Light both dots of a separator. +void SevenSegClockCountdown::setSeparator(uint8_t which, bool on) +{ + uint16_t base = (which == 1) ? sep1Base() : sep2Base(); + if (on) + setRangeOn(base, SEP_LEDS); +} - // Draws the countdown or count-up (if target is in the past) into the mask. - void drawCountdown() { - clearMask(); - int64_t diff = (int64_t)targetUnix - (int64_t)localTime; // >0 remaining, <0 passed +// Light a single half (upper/lower) of the separator. +void SevenSegClockCountdown::setSeparatorHalf(uint8_t which, bool upper, bool on) +{ + uint16_t base = (which == 1) ? sep1Base() : sep2Base(); + uint16_t halfLen = SEP_LEDS / 2; + uint16_t start = base + (upper ? 0 : halfLen); + if (on) + setRangeOn(start, halfLen); +} - // If the target is in the past, count up from the moment it was reached - bool countingUp = (diff < 0); - int64_t absDiff = abs(diff); // absolute difference for display +// Apply the mask to the strip (0 → black, 1 → keep pixel). +void SevenSegClockCountdown::applyMaskToStrip() +{ + uint16_t stripLen = strip.getLengthTotal(); + uint16_t limit = (stripLen < (uint16_t)mask.size()) ? stripLen : (uint16_t)mask.size(); + for (uint16_t i = 0; i < limit; i++) + { + if (!mask[i]) + strip.setPixelColor(i, 0); + } +} - if(countingUp && absDiff > 3600) { - if(ModeChanged){ - revertMode(); - ModeChanged = false; - } - drawClock(); - return; - } +/* ===== Target handling ===== */ +// Clamp target fields and compute targetUnix; optionally log changes. +void SevenSegClockCountdown::validateTarget(bool changed) +{ + targetYear = clampVal(targetYear, 1970, 2099); + targetMonth = clampVal(targetMonth, 1, 12); + targetDay = clampVal(targetDay, 1, 31); + targetHour = clampVal(targetHour, 0, 23); + targetMinute = clampVal(targetMinute, 0, 59); + tmElements_t tm; + tm.Second = 0; + tm.Minute = targetMinute; + tm.Hour = targetHour; + tm.Day = targetDay; + tm.Month = targetMonth; + tm.Year = CalendarYrToTm(targetYear); + targetUnix = makeTime(tm); + if (changed) + { + char buf[24]; + snprintf(buf, sizeof(buf), "%04d-%02u-%02u-%02u-%02u", + targetYear, targetMonth, targetDay, targetHour, targetMinute); + Serial.printf("[7seg] Target changed: %s | unix=%lu\r\n", buf, (unsigned long)targetUnix); + } +} - if(countingUp){ - Segment& selseg = strip.getSegment(0); - if(!ModeChanged){ - SaveMode(); - setMode(FX_MODE_STATIC, 0xFF0000, 128, 128, 255); - ModeChanged = true; - IgnoreBlinking = false; - } - if(selseg.mode != FX_MODE_STATIC && ModeChanged && !IgnoreBlinking){ - IgnoreBlinking=true; - revertMode(); - } - - if(millis() - lastBlink >= countdownBlinkInterval){ +/* ===== Countdown rendering ===== */ +// Draw countdown (or count-up when target passed) into the mask. +void SevenSegClockCountdown::drawCountdown() +{ + clearMask(); + int64_t diff = (int64_t)targetUnix - (int64_t)localTime; + bool countingUp = (diff < 0); + int64_t absDiff = abs(diff); + if (countingUp && absDiff > 3600) + { + if (ModeChanged) + { + revertMode(); + ModeChanged = false; + } + drawClock(); + return; + } + if (countingUp) + { + Segment &selseg = strip.getSegment(0); + if (!ModeChanged) + { + SaveMode(); + setMode(FX_MODE_STATIC, 0xFF0000, 128, 128, 255); + ModeChanged = true; + IgnoreBlinking = false; + } + if (selseg.mode != FX_MODE_STATIC && ModeChanged && !IgnoreBlinking) + { + IgnoreBlinking = true; + revertMode(); + } + if (millis() - lastBlink >= countdownBlinkInterval) + { BlinkToggle = !BlinkToggle; lastBlink = millis(); - } - - if(BlinkToggle && !IgnoreBlinking){ - strip.setBrightness(255); - } else if (!BlinkToggle && !IgnoreBlinking){ - strip.setBrightness(0); - } - - if (absDiff < 60) { + } + if (BlinkToggle && !IgnoreBlinking) + { + strip.setBrightness(255); + } + else if (!BlinkToggle && !IgnoreBlinking) + { + strip.setBrightness(0); + } + if (absDiff < 60) + { setDigitChar(0, 'x'); setDigitChar(1, 'x'); setDigitInt(2, absDiff / 10); @@ -167,7 +299,8 @@ class SevenSegClockCountdown : public Usermod { setDigitChar(4, 'x'); setDigitChar(5, 'x'); } - if (absDiff >= 60) { + if (absDiff >= 60) + { setDigitChar(0, 'x'); setDigitChar(1, 'x'); setDigitInt(2, absDiff / 600); @@ -177,398 +310,331 @@ class SevenSegClockCountdown : public Usermod { setDigitInt(5, (absDiff % 60) % 10); } } - - - if(!countingUp){ - // Cleanup from counting up mode - if(ModeChanged){ - revertMode(); - ModeChanged = false; - } - - // Split absolute difference into parts and totals - remDays = (uint32_t)(absDiff / 86400); - remHours = (uint8_t)((absDiff % 86400) / 3600); - remMinutes = (uint8_t)((absDiff % 3600) / 60); - remSeconds = (uint8_t)(absDiff % 60); - - fullHours = (uint32_t)(absDiff / 3600); - fullMinutes = (uint32_t)(absDiff / 60); - fullSeconds = (uint32_t)absDiff; - - // > 99 days → show ddd d - if (remDays > 99) { - setDigitChar(0, ' '); - setDigitInt(1, (remDays / 100) % 10); - setDigitInt(2, (remDays / 10) % 10); - setDigitInt(3, remDays % 10); - setDigitChar(4, 'd'); - setDigitChar(5, ' '); - return; - } - - // ≤ 99 days → show dd:hh:mm - if (remDays <=99 && fullHours > 99) { - setDigitInt(0, remDays / 10); - setDigitInt(1, remDays % 10); - setSeparator(1, sepsOn); - setDigitInt(2, remHours / 10); - setDigitInt(3, remHours % 10); - setSeparator(2, sepsOn); - setDigitInt(4, remMinutes / 10); - setDigitInt(5, remMinutes % 10); - return; - } - - // ≤ 99 hours → show hh:mm:ss - if (fullHours <=99 && fullMinutes > 99) { - setDigitInt(0, fullHours / 10); - setDigitInt(1, fullHours % 10); - setSeparator(1, sepsOn); - setDigitInt(2, remMinutes / 10); - setDigitInt(3, remMinutes % 10); - setSeparator(2, sepsOn); - setDigitInt(4, remSeconds / 10); - setDigitInt(5, remSeconds % 10); - return; - } - - // ≤ 99 minutes → MM'SS:hh (hundredths) - int hs = getHundredths(remSeconds, /*countDown*/ !countingUp); - - setDigitInt(0, fullMinutes / 10); - setDigitInt(1, fullMinutes % 10); - setSeparatorHalf(1, true, sepsOn); // place an upper dot between minutes and seconds - setDigitInt(2, remSeconds / 10); - setDigitInt(3, remSeconds % 10); - setSeparator(2, sepsOn); - setDigitInt(4, hs / 10); - setDigitInt(5, hs % 10); - } - } - - // Lights segments for a single digit based on a numeric value (0..9). - void setDigitInt(uint8_t digitIndex, int8_t value) { - if (digitIndex > 5) return; - if (value < 0) return; - uint16_t base = digitBase(digitIndex); - uint8_t bits = DIGIT_MASKS[(uint8_t)value]; - - for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) { - uint8_t logSeg = PHYS_TO_LOG[physSeg]; - bool segOn = (bits >> logSeg) & 0x01; - if (segOn) { - uint16_t segStart = base + (uint16_t)physSeg * LEDS_PER_SEG; - setRangeOn(segStart, LEDS_PER_SEG); - } + if (!countingUp) + { + if (ModeChanged) + { + revertMode(); + ModeChanged = false; } - } - // Lights segments for a single digit using a letter/symbol mask. - void setDigitChar(uint8_t digitIndex, char c) { - if (digitIndex > 5) return; - - uint16_t base = digitBase(digitIndex); - uint8_t bits = LETTER_MASK(c); - - for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) { - uint8_t logSeg = PHYS_TO_LOG[physSeg]; - bool segOn = (bits >> logSeg) & 0x01; - if (segOn) { - uint16_t segStart = base + (uint16_t)physSeg * LEDS_PER_SEG; - setRangeOn(segStart, LEDS_PER_SEG); - } + remDays = (uint32_t)(absDiff / 86400); + remHours = (uint8_t)((absDiff % 86400) / 3600); + remMinutes = (uint8_t)((absDiff % 3600) / 60); + remSeconds = (uint8_t)(absDiff % 60); + fullHours = (uint32_t)(absDiff / 3600); + fullMinutes = (uint32_t)(absDiff / 60); + fullSeconds = (uint32_t)absDiff; + + if (remDays > 99) + { + setDigitChar(0, ' '); + setDigitInt(1, (remDays / 100) % 10); + setDigitInt(2, (remDays / 10) % 10); + setDigitInt(3, remDays % 10); + setDigitChar(4, 'd'); + setDigitChar(5, ' '); + return; } - } - - // Lights both dots of a separator when requested. - void setSeparator(uint8_t which, bool on) { - uint16_t base = (which == 1) ? sep1Base() : sep2Base(); - if (on) setRangeOn(base, SEP_LEDS); - } - - // Lights a single half (upper/lower) of the separator; each half is SEP_LEDS/2. - void setSeparatorHalf(uint8_t which, bool upper, bool on) { - uint16_t base = (which == 1) ? sep1Base() : sep2Base(); - uint16_t halfLen = SEP_LEDS / 2; - uint16_t start = base + (upper ? 0 : halfLen); - if (on) setRangeOn(start, halfLen); - } - - // Applies the mask to the WLED strip: 1 keeps pixel, 0 clears it to black. - void applyMaskToStrip() { - uint16_t stripLen = strip.getLengthTotal(); - uint16_t limit = (stripLen < (uint16_t)mask.size()) ? stripLen : (uint16_t)mask.size(); - for (uint16_t i = 0; i < limit; i++) { - if (!mask[i]) strip.setPixelColor(i, 0); + if (remDays <= 99 && fullHours > 99) + { + setDigitInt(0, remDays / 10); + setDigitInt(1, remDays % 10); + setSeparator(1, sepsOn); + setDigitInt(2, remHours / 10); + setDigitInt(3, remHours % 10); + setSeparator(2, sepsOn); + setDigitInt(4, remMinutes / 10); + setDigitInt(5, remMinutes % 10); + return; } - } - - template - static T clampVal(T v, T lo, T hi) { - return (v < lo) ? lo : (v > hi ? hi : v); - } - - // Validates target fields, computes targetUnix, and optionally logs changes. - void validateTarget(bool changed = false) { - targetYear = clampVal(targetYear, 1970, 2099); - targetMonth = clampVal(targetMonth, 1, 12); - targetDay = clampVal(targetDay, 1, 31); - targetHour = clampVal(targetHour, 0, 23); - targetMinute = clampVal(targetMinute, 0, 59); - - tmElements_t tm; - tm.Second = 0; - tm.Minute = targetMinute; - tm.Hour = targetHour; - tm.Day = targetDay; - tm.Month = targetMonth; - tm.Year = CalendarYrToTm(targetYear); - targetUnix = makeTime(tm); - - if (changed) { - char buf[24]; - snprintf(buf, sizeof(buf), "%04d-%02u-%02u-%02u-%02u", - targetYear, targetMonth, targetDay, targetHour, targetMinute); - Serial.printf("[7seg] Target changed: %s | unix=%lu\r\n", buf, (unsigned long)targetUnix); + if (fullHours <= 99 && fullMinutes > 99) + { + setDigitInt(0, fullHours / 10); + setDigitInt(1, fullHours % 10); + setSeparator(1, sepsOn); + setDigitInt(2, remMinutes / 10); + setDigitInt(3, remMinutes % 10); + setSeparator(2, sepsOn); + setDigitInt(4, remSeconds / 10); + setDigitInt(5, remSeconds % 10); + return; } + int hs = getHundredths(remSeconds, /*countDown*/ !countingUp); + + setDigitInt(0, fullMinutes / 10); + setDigitInt(1, fullMinutes % 10); + setSeparatorHalf(1, true, sepsOn); + setDigitInt(2, remSeconds / 10); + setDigitInt(3, remSeconds % 10); + setSeparator(2, sepsOn); + setDigitInt(4, hs / 10); + setDigitInt(5, hs % 10); } +} -public: - // Prepare the mask buffer on boot. - void setup() override { - ensureMaskSize(); - } - // No periodic work; rendering is driven by handleOverlayDraw(). - void loop() override {} - - // Main entry point from WLED to draw the overlay. - void handleOverlayDraw() { - if (!enabled) return; - - // If both views are enabled, alternate every "alternatingTime" seconds. - if (showClock && showCountdown) { +/* ===== WLED overlay entrypoint ===== */ +// Draw the overlay (clock, countdown, or alternating) and apply mask. +void SevenSegClockCountdown::handleOverlayDraw() +{ + if (!enabled) + return; + if (showClock && showCountdown) + { uint32_t period = (alternatingTime > 0) ? (uint32_t)alternatingTime : 10U; - - // Block index based on current time uint32_t block = (uint32_t)localTime / period; - - // Even blocks: clock; odd blocks: countdown - if ((block & 1U) == 0U) drawClock(); - else drawCountdown(); + if ((block & 1U) == 0U) + drawClock(); + else + drawCountdown(); } - else if (showClock) { + else if (showClock) + { drawClock(); } - else { // countdown only (or default) + else + { drawCountdown(); } - applyMaskToStrip(); -} - - - // Adds a compact UI block to the info screen (u-group). - void addToJsonInfo(JsonObject& root) override { - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - // Top-level array for this usermod - JsonArray infoArr = user.createNestedArray(F("7 Segment Counter")); - - // Enable/disable button - String uiDomString = F(""); - infoArr.add(uiDomString); - - // State - infoArr = user.createNestedArray(F("Status")); - infoArr.add(enabled ? F("active") : F("disabled")); - - infoArr = user.createNestedArray(F("Clock Seperators")); - if (SeperatorOn && SeperatorOff) infoArr.add(F("Blinking")); - else if (SeperatorOn) infoArr.add(F("Always On")); - else if (SeperatorOff) infoArr.add(F("Always Off")); - else infoArr.add(F("Blinking")); - - // Modes - infoArr = user.createNestedArray(F("Mode")); - if (showClock && showCountdown) infoArr.add(F("Clock & Countdown")); - else if (showClock) infoArr.add(F("Clock Only")); - else infoArr.add(F("Countdown Only")); - - // Target - infoArr = user.createNestedArray(F("Target Date")); - char buf[24]; - snprintf(buf, sizeof(buf), "%04d-%02u-%02u %02u:%02u", - targetYear, targetMonth, targetDay, targetHour, targetMinute); - infoArr.add(buf); - - // Panel LEDs - infoArr = user.createNestedArray(F("Total Panel LEDs")); - infoArr.add(TOTAL_PANEL_LEDS); - infoArr.add(F(" px")); - } - - // JSON state/config: persist and apply overlay parameters via state/config. - void addToJsonState(JsonObject& root) override { - JsonObject s = root[F("7seg")].as(); - if (s.isNull()) s = root.createNestedObject(F("7seg")); - s[F("enabled")] = enabled; +}; - s[F("SeperatorOn")] = SeperatorOn; - s[F("SeperatorOff")] = SeperatorOff; +/* ===== UI / JSON state ===== */ +// Add compact info UI elements to the JSON info block. +void SevenSegClockCountdown::addToJsonInfo(JsonObject &root) +{ + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + JsonArray infoArr = user.createNestedArray(F("7 Segment Counter")); + String uiDomString = F(""); + infoArr.add(uiDomString); + infoArr = user.createNestedArray(F("Status")); + infoArr.add(enabled ? F("active") : F("disabled")); + infoArr = user.createNestedArray(F("Clock Seperators")); + if (SeperatorOn && SeperatorOff) + infoArr.add(F("Blinking")); + else if (SeperatorOn) + infoArr.add(F("Always On")); + else if (SeperatorOff) + infoArr.add(F("Always Off")); + else + infoArr.add(F("Blinking")); + infoArr = user.createNestedArray(F("Mode")); + if (showClock && showCountdown) + infoArr.add(F("Clock & Countdown")); + else if (showClock) + infoArr.add(F("Clock Only")); + else + infoArr.add(F("Countdown Only")); + infoArr = user.createNestedArray(F("Target Date")); + char buf[24]; + snprintf(buf, sizeof(buf), "%04d-%02u-%02u %02u:%02u", + targetYear, targetMonth, targetDay, targetHour, targetMinute); + infoArr.add(buf); + infoArr = user.createNestedArray(F("Total Panel LEDs")); + infoArr.add(TOTAL_PANEL_LEDS); + infoArr.add(F(" px")); +} - s[F("targetYear")] = targetYear; - s[F("targetMonth")] = targetMonth; - s[F("targetDay")] = targetDay; - s[F("targetHour")] = targetHour; - s[F("targetMinute")] = targetMinute; +// Serialize usermod state into JSON state. +void SevenSegClockCountdown::addToJsonState(JsonObject &root) +{ + JsonObject s = root[F("7seg")].as(); + if (s.isNull()) + s = root.createNestedObject(F("7seg")); + s[F("enabled")] = enabled; + s[F("SeperatorOn")] = SeperatorOn; + s[F("SeperatorOff")] = SeperatorOff; + s[F("targetYear")] = targetYear; + s[F("targetMonth")] = targetMonth; + s[F("targetDay")] = targetDay; + s[F("targetHour")] = targetHour; + s[F("targetMinute")] = targetMinute; + s[F("showClock")] = showClock; + s[F("showCountdown")] = showCountdown; + s[F("alternatingTime")] = alternatingTime; +} - s[F("showClock")] = showClock; - s[F("showCountdown")] = showCountdown; - s[F("alternatingTime")] = alternatingTime; +// Read usermod state from JSON state and validate target. +void SevenSegClockCountdown::readFromJsonState(JsonObject &root) +{ + JsonObject s = root[F("7seg")].as(); + if (s.isNull()) + return; + if (s.containsKey(F("enabled"))) + enabled = s[F("enabled")].as(); + bool changed = false; + if (s.containsKey(F("targetYear"))) + { + targetYear = s[F("targetYear")].as(); + changed = true; } - - void readFromJsonState(JsonObject& root) override { - JsonObject s = root[F("7seg")].as(); - if (s.isNull()) return; - - if (s.containsKey(F("enabled"))) enabled = s[F("enabled")].as(); - - bool changed = false; - if (s.containsKey(F("targetYear"))) { targetYear = s[F("targetYear")].as(); changed = true; } - if (s.containsKey(F("targetMonth"))) { targetMonth = s[F("targetMonth")].as(); changed = true; } - if (s.containsKey(F("targetDay"))) { targetDay = s[F("targetDay")].as(); changed = true; } - if (s.containsKey(F("targetHour"))) { targetHour = s[F("targetHour")].as(); changed = true; } - if (s.containsKey(F("targetMinute"))) { targetMinute = s[F("targetMinute")].as();changed = true; } - - if (s.containsKey(F("showClock"))) showClock = s[F("showClock")].as(); - if (s.containsKey(F("showCountdown"))) showCountdown = s[F("showCountdown")].as(); - if (s.containsKey(F("alternatingTime"))) alternatingTime = s[F("alternatingTime")].as(); - if (s.containsKey(F("SeperatorOn"))) SeperatorOn = s[F("SeperatorOn")].as(); - if (s.containsKey(F("SeperatorOff"))) SeperatorOff = s[F("SeperatorOff")].as(); - - if (changed) validateTarget(true); + if (s.containsKey(F("targetMonth"))) + { + targetMonth = s[F("targetMonth")].as(); + changed = true; } - - void addToConfig(JsonObject& root) override { - JsonObject top = root.createNestedObject(F("7seg")); - top[F("enabled")] = enabled; - - top[F("targetYear")] = targetYear; - top[F("targetMonth")] = targetMonth; - top[F("targetDay")] = targetDay; - top[F("targetHour")] = targetHour; - top[F("targetMinute")] = targetMinute; - - top[F("showClock")] = showClock; - top[F("showCountdown")] = showCountdown; - top[F("alternatingTime")] = alternatingTime; // seconds to alternate when both modes enabled - top[F("SeperatorOn")] = SeperatorOn; - top[F("SeperatorOff")] = SeperatorOff; + if (s.containsKey(F("targetDay"))) + { + targetDay = s[F("targetDay")].as(); + changed = true; } - - bool readFromConfig(JsonObject& root) override { - JsonObject top = root[F("7seg")]; - bool configComplete = !top.isNull(); - - configComplete &= getJsonValue(top[F("enabled")], enabled, true); - - configComplete &= getJsonValue(top[F("targetYear")], targetYear, year(localTime)); - configComplete &= getJsonValue(top[F("targetMonth")], targetMonth, (uint8_t)1); - configComplete &= getJsonValue(top[F("targetDay")], targetDay, (uint8_t)1); - configComplete &= getJsonValue(top[F("targetHour")], targetHour, (uint8_t)0); - configComplete &= getJsonValue(top[F("targetMinute")], targetMinute, (uint8_t)0); - - configComplete &= getJsonValue(top[F("showClock")], showClock, true); - configComplete &= getJsonValue(top[F("showCountdown")], showCountdown, false); - configComplete &= getJsonValue(top[F("alternatingTime")], alternatingTime, (uint16_t)10); - configComplete &= getJsonValue(top[F("SeperatorOn")], SeperatorOn, true); - configComplete &= getJsonValue(top[F("SeperatorOff")], SeperatorOff, true); - - validateTarget(true); - return configComplete; + if (s.containsKey(F("targetHour"))) + { + targetHour = s[F("targetHour")].as(); + changed = true; } - - void onMqttConnect(bool sessionPresent) override { - String topic = mqttDeviceTopic; - topic += MQTT_Topic; - mqtt->subscribe(topic.c_str(), 0); + if (s.containsKey(F("targetMinute"))) + { + targetMinute = s[F("targetMinute")].as(); + changed = true; } + if (s.containsKey(F("showClock"))) + showClock = s[F("showClock")].as(); + if (s.containsKey(F("showCountdown"))) + showCountdown = s[F("showCountdown")].as(); + if (s.containsKey(F("alternatingTime"))) + alternatingTime = s[F("alternatingTime")].as(); + if (s.containsKey(F("SeperatorOn"))) + SeperatorOn = s[F("SeperatorOn")].as(); + if (s.containsKey(F("SeperatorOff"))) + SeperatorOff = s[F("SeperatorOff")].as(); + if (changed) + validateTarget(true); +} - bool onMqttMessage(char* topic, char* payload) { - String topicStr = String(topic); +/* ===== Config persistence ===== */ +// Write persistent configuration to cfg.json. +void SevenSegClockCountdown::addToConfig(JsonObject &root) +{ + JsonObject top = root.createNestedObject(F("7seg")); + top[F("enabled")] = enabled; + top[F("targetYear")] = targetYear; + top[F("targetMonth")] = targetMonth; + top[F("targetDay")] = targetDay; + top[F("targetHour")] = targetHour; + top[F("targetMinute")] = targetMinute; + top[F("showClock")] = showClock; + top[F("showCountdown")] = showCountdown; + top[F("alternatingTime")] = alternatingTime; + top[F("SeperatorOn")] = SeperatorOn; + top[F("SeperatorOff")] = SeperatorOff; +} - if (!topicStr.startsWith(F("/7seg/"))) return false; +// Read persistent configuration from cfg.json. +bool SevenSegClockCountdown::readFromConfig(JsonObject &root) +{ + JsonObject top = root[F("7seg")]; + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top[F("enabled")], enabled, true); + configComplete &= getJsonValue(top[F("targetYear")], targetYear, year(localTime)); + configComplete &= getJsonValue(top[F("targetMonth")], targetMonth, (uint8_t)1); + configComplete &= getJsonValue(top[F("targetDay")], targetDay, (uint8_t)1); + configComplete &= getJsonValue(top[F("targetHour")], targetHour, (uint8_t)0); + configComplete &= getJsonValue(top[F("targetMinute")], targetMinute, (uint8_t)0); + configComplete &= getJsonValue(top[F("showClock")], showClock, true); + configComplete &= getJsonValue(top[F("showCountdown")], showCountdown, false); + configComplete &= getJsonValue(top[F("alternatingTime")], alternatingTime, (uint16_t)10); + configComplete &= getJsonValue(top[F("SeperatorOn")], SeperatorOn, true); + configComplete &= getJsonValue(top[F("SeperatorOff")], SeperatorOff, true); + validateTarget(true); + return configComplete; +} - String subTopic = topicStr.substring(strlen("/7seg/")); - if (subTopic.indexOf(F("enabled")) >= 0) { - String payloadStr = String(payload); - enabled = (payloadStr == F("true") || payloadStr == F("1")); - return true; - } - if (subTopic.indexOf(F("targetYear")) >= 0) { - String payloadStr = String(payload); - targetYear = payloadStr.toInt(); - return true; - } - if (subTopic.indexOf(F("targetMonth")) >= 0) { - String payloadStr = String(payload); - targetMonth = (uint8_t)payloadStr.toInt(); - return true; - } - if (subTopic.indexOf(F("targetDay")) >= 0) { - String payloadStr = String(payload); - targetDay = (uint8_t)payloadStr.toInt(); - return true; - } - if (subTopic.indexOf(F("targetHour")) >= 0) { - String payloadStr = String(payload); - targetHour = (uint8_t)payloadStr.toInt(); - return true; - } - if (subTopic.indexOf(F("targetMinute")) >= 0) { - String payloadStr = String(payload); - targetMinute = (uint8_t)payloadStr.toInt(); - return true; - } - if (subTopic.indexOf(F("showClock")) >= 0) { - String payloadStr = String(payload); - showClock = (payloadStr == F("true") || payloadStr == F("1")); - return true; - } - if (subTopic.indexOf(F("showCountdown")) >= 0) { - String payloadStr = String(payload); - showCountdown = (payloadStr == F("true") || payloadStr == F("1")); - return true; - } - if (subTopic.indexOf(F("alternatingTime")) >= 0) { - String payloadStr = String(payload); - alternatingTime = (uint16_t)payloadStr.toInt(); - return true; - } - if (subTopic.indexOf(F("SeperatorOn")) >= 0) { - String payloadStr = String(payload); - SeperatorOn = (payloadStr == F("true") || payloadStr == F("1")); - return true; - } - if (subTopic.indexOf(F("SeperatorOff")) >= 0) { - String payloadStr = String(payload); - SeperatorOff = (payloadStr == F("true") || payloadStr == F("1")); - return true; - } +/* ===== MQTT integration ===== */ +// Subscribe to usermod MQTT topic on connect. +void SevenSegClockCountdown::onMqttConnect(bool sessionPresent) +{ + String topic = mqttDeviceTopic; + topic += MQTT_Topic; + mqtt->subscribe(topic.c_str(), 0); +} +// Handle usermod MQTT messages (simple key/value). +bool SevenSegClockCountdown::onMqttMessage(char *topic, char *payload) +{ + String topicStr = String(topic); + if (!topicStr.startsWith(F("/7seg/"))) return false; + String subTopic = topicStr.substring(strlen("/7seg/")); + if (subTopic.indexOf(F("enabled")) >= 0) + { + String payloadStr = String(payload); + enabled = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + if (subTopic.indexOf(F("targetYear")) >= 0) + { + String payloadStr = String(payload); + targetYear = payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("targetMonth")) >= 0) + { + String payloadStr = String(payload); + targetMonth = (uint8_t)payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("targetDay")) >= 0) + { + String payloadStr = String(payload); + targetDay = (uint8_t)payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("targetHour")) >= 0) + { + String payloadStr = String(payload); + targetHour = (uint8_t)payloadStr.toInt(); + return true; } + if (subTopic.indexOf(F("targetMinute")) >= 0) + { + String payloadStr = String(payload); + targetMinute = (uint8_t)payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("showClock")) >= 0) + { + String payloadStr = String(payload); + showClock = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + if (subTopic.indexOf(F("showCountdown")) >= 0) + { + String payloadStr = String(payload); + showCountdown = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + if (subTopic.indexOf(F("alternatingTime")) >= 0) + { + String payloadStr = String(payload); + alternatingTime = (uint16_t)payloadStr.toInt(); + return true; + } + if (subTopic.indexOf(F("SeperatorOn")) >= 0) + { + String payloadStr = String(payload); + SeperatorOn = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + if (subTopic.indexOf(F("SeperatorOff")) >= 0) + { + String payloadStr = String(payload); + SeperatorOff = (payloadStr == F("true") || payloadStr == F("1")); + return true; + } + return false; +} - // Unique usermod id (arbitrary, but stable). - uint16_t getId() override { return 0x22B8; } -}; +/* ===== Identification & registration ===== */ +// Return unique usermod identifier. +uint16_t SevenSegClockCountdown::getId() { return 0x22B8; } static SevenSegClockCountdown usermod; REGISTER_USERMOD(usermod); diff --git a/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h index e70fc9ec05..22cda8bc36 100644 --- a/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h +++ b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h @@ -1,9 +1,23 @@ #pragma once #include -// Seven-segment overlay constants and utilities for a 6-digit panel with two separators. +/*-------------------------------------------------------------------------------------------------------------------------------------- + 7-Segment Clock & Countdown Usermod + + Usermod for WLED providing a configurable 7-segment style display + supporting clock and countdown modes. The display is rendered as a + logical overlay using a segment-to-LED mapping and integrates + seamlessly into the WLED usermod framework. + + Creator : DereIBims + Version : 0.1.0 + + Resources: + - 3D-print files (enclosures/mounts) and PCB design files will be available soon: + * 3D-print: MakerWorld + * PCB: GitHub, inside this usermod folder +----------------------------------------------------------------------*/ -// Logical segment indices (A..G) used to build bitmasks for digits/letters. #define SEG_A 0 #define SEG_B 1 #define SEG_C 2 @@ -16,159 +30,223 @@ ----------------------------------------------------------------------------------------- -----------------------------------------------------------------------------------------*/ -// Geometry: LED counts for a single digit and the full panel. -// - One digit has 7 segments, each with LEDS_PER_SEG pixels -// - Two separator blocks exist between digit 2/3 and 4/5, each with SEP_LEDS pixels -static const uint16_t LEDS_PER_SEG = 5; -static const uint8_t SEGS_PER_DIGIT = 7; -static const uint16_t SEP_LEDS = 10; -static constexpr uint16_t LEDS_PER_DIGIT = LEDS_PER_SEG * SEGS_PER_DIGIT; // 35 -static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; // 230 - -static const char* MQTT_Topic = "7seg"; - -// Physical-to-logical segment mapping per digit: physical order F-A-B-G-E-D-C → logical A..G. -// This allows drawing by logical segment index while wiring can remain arbitrary. -static constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { - SEG_F, - SEG_A, - SEG_B, - SEG_G, - SEG_E, - SEG_D, - SEG_C -}; - -// Index helpers into the linear LED stream for each digit and separator block. -static uint16_t digitBase(uint8_t d) { - switch (d) { - case 0: return 0; // digit 1 - case 1: return 35; // digit 2 - case 2: return 80; // digit 3 - case 3: return 115; // digit 4 - case 4: return 160; // digit 5 - case 5: return 195; // digit 6 - default: return 0; +#define MQTT_Topic "7seg" + +class SevenSegClockCountdown : public Usermod +{ +private: + // Geometry: LED counts for a single digit and the full panel. + // - One digit has 7 segments, each with LEDS_PER_SEG pixels + // - Two separator blocks exist between digit 2/3 and 4/5, each with SEP_LEDS pixels + static const uint16_t LEDS_PER_SEG = 5; + static const uint8_t SEGS_PER_DIGIT = 7; + static const uint16_t SEP_LEDS = 10; + static constexpr uint16_t LEDS_PER_DIGIT = LEDS_PER_SEG * SEGS_PER_DIGIT; // 35 + static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; // 230 + + // Physical-to-logical segment mapping per digit: physical order F-A-B-G-E-D-C + static constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { + SEG_F, + SEG_A, + SEG_B, + SEG_G, + SEG_E, + SEG_D, + SEG_C}; + + // Index helpers into the linear LED stream for each digit and separator block. + static uint16_t digitBase(uint8_t d) + { + switch (d) + { + case 0: + return 0; // digit 1 + case 1: + return 35; // digit 2 + case 2: + return 80; // digit 3 + case 3: + return 115; // digit 4 + case 4: + return 160; // digit 5 + case 5: + return 195; // digit 6 + default: + return 0; + } } -} -static constexpr uint16_t sep1Base() { return 70; } -static constexpr uint16_t sep2Base() { return 150; } - -// Interval the coundtown digits blink when counting up (in ms). -static unsigned long countdownBlinkInterval = 500; - -/*-------------------------------------------------------------------------------------- ---------------------------Do NOT edit anything below this line-------------------------- ---------------------------------------------------------------------------------------*/ - -// Digit bitmasks (bits A..G correspond to indices 0..6). 1 means the segment is lit. -static constexpr uint8_t DIGIT_MASKS[10] = { - 0b00111111, // 0 (A..F) - 0b00000110, // 1 (B,C) - 0b01011011, // 2 (A,B,D,E,G) - 0b01001111, // 3 (A,B,C,D,G) - 0b01100110, // 4 (B,C,F,G) - 0b01101101, // 5 (A,C,D,F,G) - 0b01111101, // 6 (A,C,D,E,F,G) - 0b00000111, // 7 (A,B,C) - 0b01111111, // 8 (A..G) - 0b01101111 // 9 (A,B,C,D,F,G) -}; - -// Runtime state used by the overlay renderer and UI. -// - mask: per-LED on/off mask (1 keeps original color/effect, 0 forces black) -// - enabled: master on/off for the overlay -// - sepsOn: helper for separator drawing -// - SeperatorOn/Off: user-configurable flags controlling separator behavior in clock mode -std::vector mask; // 1 = keep underlying pixel, 0 = clear to black -bool enabled = true; -bool sepsOn = true; -bool SeperatorOn = true; // force separators on in clock mode -bool SeperatorOff = true; // force separators off in clock mode - -// Display mode flags and timing: -// - showClock/showCountdown: which views to render -// - alternatingTime: seconds between views when both are enabled -bool showClock = true; -bool showCountdown = false; -uint16_t alternatingTime = 10; // seconds - -// Countdown target (local time) and derived UNIX timestamp used for math. -int targetYear = 2026; -uint8_t targetMonth = 1; // 1-12 -uint8_t targetDay = 1; // 1-31 -uint8_t targetHour = 0; // 0-23 -uint8_t targetMinute = 0; // 0-59 -time_t targetUnix = 0; - -// Remaining time parts and totals (updated each draw): -// - remX: time parts modulo their units -// - fullX: monotonically increasing totals (minutes, seconds, etc.) -uint32_t fullHours = 0; // up to 8760 -uint32_t fullMinutes = 0; // up to 525600 -uint32_t fullSeconds = 0; // up to 31536000 -uint32_t remDays = 0; -uint8_t remHours = 0; -uint8_t remMinutes = 0; -uint8_t remSeconds = 0; - -// Second-boundary tracking to compute smooth hundredths display. -unsigned long lastSecondMillis = 0; -int lastSecondValue = -1; - -// To keep track of the mode before switching to counting up. -bool IgnoreBlinking = false; -bool ModeChanged = false; -uint8_t prevMode=0; -uint32_t prevColor=0; -uint8_t prevSpeed=0; -uint8_t prevIntensity=0; -uint8_t prevBrightness=0; -unsigned long lastBlink = 0; -bool BlinkToggle = false; - -// Letter-to-segment mapping for a 7-seg display. -// Returns a bitmask A..G (0..6). If the character is unsupported, fallback lights A, D, G. -uint8_t LETTER_MASK(char c) { - switch (c) { + static constexpr uint16_t sep1Base() { return 70; } + static constexpr uint16_t sep2Base() { return 150; } + + static const unsigned long countdownBlinkInterval = 500; + + /*-------------------------------------------------------------------------------------- + --------------------------Do NOT edit anything below this line-------------------------- + --------------------------------------------------------------------------------------*/ + + /* ===== Digit bitmasks (A..G bits 0..6) ===== */ + static constexpr uint8_t DIGIT_MASKS[10] = { + 0b00111111, // 0 (A..F) + 0b00000110, // 1 (B,C) + 0b01011011, // 2 (A,B,D,E,G) + 0b01001111, // 3 (A,B,C,D,G) + 0b01100110, // 4 (B,C,F,G) + 0b01101101, // 5 (A,C,D,F,G) + 0b01111101, // 6 (A,C,D,E,F,G) + 0b00000111, // 7 (A,B,C) + 0b01111111, // 8 (A..G) + 0b01101111 // 9 (A,B,C,D,F,G) + }; + + /* ===== Runtime state (mask, flags, variables) ===== */ + std::vector mask; + bool enabled = true; + bool sepsOn = true; + bool SeperatorOn = true; + bool SeperatorOff = true; + + bool showClock = true; + bool showCountdown = false; + uint16_t alternatingTime = 10; + + int targetYear = year(localTime); + uint8_t targetMonth = month(localTime); + uint8_t targetDay = day(localTime); + uint8_t targetHour = 0; + uint8_t targetMinute = 0; + time_t targetUnix = 0; + + uint32_t fullHours = 0; + uint32_t fullMinutes = 0; + uint32_t fullSeconds = 0; + uint32_t remDays = 0; + uint8_t remHours = 0; + uint8_t remMinutes = 0; + uint8_t remSeconds = 0; + + unsigned long lastSecondMillis = 0; + int lastSecondValue = -1; + + bool IgnoreBlinking = false; + bool ModeChanged = false; + uint8_t prevMode = 0; + uint32_t prevColor = 0; + uint8_t prevSpeed = 0; + uint8_t prevIntensity = 0; + uint8_t prevBrightness = 0; + unsigned long lastBlink = 0; + bool BlinkToggle = false; + + // Returns a bitmask A..G (0..6). If the character is unsupported, fallback lights A, D, G. + uint8_t LETTER_MASK(char c) + { + switch (c) + { // Uppercase - case 'A': return 0b01110111; // A,B,C,E,F,G - case 'B': return 0b01111100; // b-like (C,D,E,F,G) - case 'C': return 0b00111001; // A,D,E,F - case 'D': return 0b01011110; // d-like (B,C,D,E,G) - case 'E': return 0b01111001; // A,D,E,F,G - case 'F': return 0b01110001; // A,E,F,G - case 'H': return 0b01110110; // B,C,E,F,G - case 'J': return 0b00011110; // B,C,D - case 'L': return 0b00111000; // D,E,F - case 'O': return 0b00111111; // A,B,C,D,E,F (like '0') - case 'P': return 0b01110011; // A,B,E,F,G - case 'T': return 0b01111000; // D,E,F,G - case 'U': return 0b00111110; // B,C,D,E,F - case 'Y': return 0b01101110; // B,C,D,F,G + case 'A': + return 0b01110111; // A,B,C,E,F,G + case 'B': + return 0b01111100; // b-like (C,D,E,F,G) + case 'C': + return 0b00111001; // A,D,E,F + case 'D': + return 0b01011110; // d-like (B,C,D,E,G) + case 'E': + return 0b01111001; // A,D,E,F,G + case 'F': + return 0b01110001; // A,E,F,G + case 'H': + return 0b01110110; // B,C,E,F,G + case 'J': + return 0b00011110; // B,C,D + case 'L': + return 0b00111000; // D,E,F + case 'O': + return 0b00111111; // A,B,C,D,E,F (like '0') + case 'P': + return 0b01110011; // A,B,E,F,G + case 'T': + return 0b01111000; // D,E,F,G + case 'U': + return 0b00111110; // B,C,D,E,F + case 'Y': + return 0b01101110; // B,C,D,F,G // Lowercase - case 'a': return 0b01011111; // A,B,C,D,E,G - case 'b': return 0b01111100; // C,D,E,F,G - case 'c': return 0b01011000; // D,E,G - case 'd': return 0b01011110; // B,C,D,E,G - case 'e': return 0b01111011; // A,D,E,F,G (with C off) - case 'f': return 0b01110001; // A,E,F,G - case 'h': return 0b01110100; // C,E,F,G - case 'j': return 0b00001110; // B,C,D - case 'l': return 0b00110000; // E,F - case 'n': return 0b01010100; // C,E,G - case 'o': return 0b01011100; // C,D,E,G - case 'r': return 0b01010000; // E,G - case 't': return 0b01111000; // D,E,F,G - case 'u': return 0b00011100; // C,D,E - case 'y': return 0b01101110; // B,C,D,F,G - - // Symbols commonly used on 7-seg - case '-': return 0b01000000; // G - case '_': return 0b00001000; // D - case ' ': return 0b00000000; // blank - - default: return 0b01001001; // fallback: A, D, G + case 'a': + return 0b01011111; // A,B,C,D,E,G + case 'b': + return 0b01111100; // C,D,E,F,G + case 'c': + return 0b01011000; // D,E,G + case 'd': + return 0b01011110; // B,C,D,E,G + case 'e': + return 0b01111011; // A,D,E,F,G (with C off) + case 'f': + return 0b01110001; // A,E,F,G + case 'h': + return 0b01110100; // C,E,F,G + case 'j': + return 0b00001110; // B,C,D + case 'l': + return 0b00110000; // E,F + case 'n': + return 0b01010100; // C,E,G + case 'o': + return 0b01011100; // C,D,E,G + case 'r': + return 0b01010000; // E,G + case 't': + return 0b01111000; // D,E,F,G + case 'u': + return 0b00011100; // C,D,E + case 'y': + return 0b01101110; // B,C,D,F,G + case '-': + return 0b01000000; // G + case '_': + return 0b00001000; // D + case ' ': + return 0b00000000; // blank + + default: + return 0b01001001; // fallback: A, D, G + } + } + + static uint8_t clampVal(int v, int lo, int hi) + { + return (v < lo) ? lo : (v > hi ? hi : v); } -} + + /* ===== Private methods ===== */ + void ensureMaskSize(); // ensure mask matches panel size + void clearMask(); // clear mask to transparent + void setRangeOn(uint16_t start, uint16_t len); // set contiguous mask range on + int getHundredths(int currentSeconds, bool countDown); // compute hundredths (0..99) + void drawClock(); // render HH:MM:SS into mask + void revertMode(); // restore previous effect mode + void SaveMode(); // save current effect mode + void setMode(uint8_t mode, uint32_t color, uint8_t speed, uint8_t intensity, uint8_t brightness); // apply effect mode + void drawCountdown(); // render countdown/count-up into mask + void setDigitInt(uint8_t digitIndex, int8_t value); // draw numeric digit + void setDigitChar(uint8_t digitIndex, char c); // draw character/symbol + void setSeparator(uint8_t which, bool on); // set both separator dots + void setSeparatorHalf(uint8_t which, bool upperDot, bool on); // set separator half + void applyMaskToStrip(); // apply mask to physical strip + void validateTarget(bool changed = false); // clamp fields and compute targetUnix + +public: + void setup() override; // prepare usermod + void loop() override; // periodic loop (unused) + void handleOverlayDraw(); // main overlay draw entrypoint + void addToJsonInfo(JsonObject &root) override; // add compact info UI + void addToJsonState(JsonObject &root) override; // serialize state + void readFromJsonState(JsonObject &root) override; // read state + void addToConfig(JsonObject &root) override; // write persistent config + bool readFromConfig(JsonObject &root) override; // read persistent config + void onMqttConnect(bool sessionPresent) override; // subscribe on connect + bool onMqttMessage(char *topic, char *payload) override; // handle mqtt messages + uint16_t getId() override; // usermod id +}; diff --git a/usermods/7_seg_clock_countdown/README.md b/usermods/7_seg_clock_countdown/README.md index 7fe9fd99c2..a13ba88f33 100644 --- a/usermods/7_seg_clock_countdown/README.md +++ b/usermods/7_seg_clock_countdown/README.md @@ -1,5 +1,12 @@ # WLED Usermod: 7‑Segment Countdown Overlay +## Status & resources +- 3D-print files (enclosures/mounts) and PCB design files will be published soon: + - 3D-print: MakerWorld + - PCB: GitHub, in this usermod’s folder +- Active development happens in the WLED fork by DereIBims: + - https://github.com/DereIBims/WLED + A usermod that renders a six‑digit, two‑separator seven‑segment display as an overlay mask on top of WLED’s normal effects/colors. Lit “segments” preserve the underlying pixel color; all other pixels are forced to black. This lets you show a clock or a countdown without losing the active effect. ## What it shows @@ -32,22 +39,15 @@ A physical‑to‑logical segment map (`PHYS_TO_LOG`) allows arbitrary wiring or - During `applyMaskToStrip()`, pixels with mask 0 are cleared to black; mask 1 keeps the existing effect color. ## Build and enable -This usermod is already placed under `usermods/usermod_7segment_countdown` and can be enabled via PlatformIO environments that specify it in `custom_usermods`. - -- Recommended environment: `env:esp32dev` (includes `custom_usermods = usermod_7segment_countdown`). -- To include all usermods, you can use `env:usermods` (`custom_usermods = *`). +This usermod is located at `usermods/7_seg_clock_countdown` and can be enabled by adding the folder name to your PlatformIO environment’s `custom_usermods` setting. -Follow the project’s standard workflow: -1) Build web UI first: `npm run build` -2) Optional: run tests: `npm test` -3) Build firmware: `pio run -e esp32dev` +- Example: `custom_usermods = 7_seg_clock_countdown` +- To include all usermods, you can also use an environment that sets `custom_usermods = *`. -> Ensure the device time is correct (NTP/timezone) so clock/countdown displays accurately. +> Ensure the device time is correct (NTP/timezone) so clock/countdown display accurately. ### Required core tweak for smooth hundredths -To guarantee that the hundredths display updates every frame without visible lag, the static effect’s frame timing must not idle the render loop. Adjust the return value of `mode_static()` in [wled00/FX.cpp](wled00/FX.cpp) to always return `FRAMETIME`. - -This ensures the render loop runs continuously so the overlay’s hundredths (00–99) are refreshed in real time. +To guarantee hundredths update every frame without lag, the static effect must not idle the render loop. In `wled00/FX.cpp`, adjust `mode_static()` to always return `FRAMETIME`. This keeps the render loop continuous so hundredths (00–99) refresh in real time. ## Runtime configuration All settings live under the `7seg` object in state/config JSON. @@ -106,6 +106,46 @@ Turn the overlay off: { "7seg": { "enabled": false } } ``` +## Custom MQTT API +Publish to your device topic with the `7seg` subpath. Topic format: +- {deviceTopic}/7seg/{key} + +Accepted keys and payloads: +- Boolean keys (payload: "true"/"1" or "false"/"0"): + - enabled + - showClock + - showCountdown + - SeperatorOn + - SeperatorOff +- Numeric keys (payload: integer): + - targetYear (1970–2099) + - targetMonth (1–12) + - targetDay (1–31) + - targetHour (0–23) + - targetMinute (0–59) + - alternatingTime (seconds) + +Examples (mosquitto_pub): +- Enable overlay: + - mosquitto_pub -h -t "wled/mystrip/7seg/enabled" -m "true" +- Clock only, separators always on: + - mosquitto_pub -h -t "wled/mystrip/7seg/showClock" -m "true" + - mosquitto_pub -h -t "wled/mystrip/7seg/showCountdown" -m "false" + - mosquitto_pub -h -t "wled/mystrip/7seg/SeperatorOn" -m "true" + - mosquitto_pub -h -t "wled/mystrip/7seg/SeperatorOff" -m "false" +- Set countdown target to 2026‑01‑01 00:00: + - mosquitto_pub -h -t "wled/mystrip/7seg/targetYear" -m "2026" + - mosquitto_pub -h -t "wled/mystrip/7seg/targetMonth" -m "1" + - mosquitto_pub -h -t "wled/mystrip/7seg/targetDay" -m "1" + - mosquitto_pub -h -t "wled/mystrip/7seg/targetHour" -m "0" + - mosquitto_pub -h -t "wled/mystrip/7seg/targetMinute" -m "0" +- Alternate clock/countdown every 15s: + - mosquitto_pub -h -t "wled/mystrip/7seg/alternatingTime" -m "15" + +Notes: +- Device topic is the WLED “Device Topic” you configured (e.g. wled/mystrip). +- Payloads are simple strings; booleans accept "true"/"1" and "false"/"0". + ## UI integration - The usermod adds a compact block to the “Info” screen: - A small toggle button to enable/disable the overlay @@ -123,8 +163,8 @@ The usermod uses WLED’s `localTime`. Configure NTP and timezone in WLED so the - Separator half‑lighting is used to place an upper dot between minutes and seconds in the ≤99‑minute view. ## Files -- `usermod_7segment_countdown.h` — constants, geometry, masks, helpers -- `usermod_7segment_countdown.cpp` — rendering, JSON/config wiring, registration +- `usermods/7_seg_clock_countdown/7_seg_clock_countdown.h` — constants, geometry, masks, helpers +- `usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp` — rendering, JSON/config wiring, registration ## License This usermod follows the WLED project license (see repository `LICENSE`). From cf72a97dfa09e1ce5c7426028b0843016cda4cca Mon Sep 17 00:00:00 2001 From: dereibims Date: Sun, 11 Jan 2026 23:05:52 +0100 Subject: [PATCH 7/9] Prep for merge --- platformio.ini | 29 +++++++++++++++++++++++------ wled00/FX.cpp | 4 ++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/platformio.ini b/platformio.ini index d84e4c4f45..ec73bc5658 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,8 +10,27 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = esp32dev - +default_envs = nodemcuv2 + esp8266_2m + esp01_1m_full + nodemcuv2_160 + esp8266_2m_160 + esp01_1m_full_160 + nodemcuv2_compat + esp8266_2m_compat + esp01_1m_full_compat + esp32dev + esp32dev_debug + esp32_eth + esp32_wrover + lolin_s2_mini + esp32c3dev + esp32c3dev_qio + esp32S3_wroom2 + esp32s3dev_16MB_opi + esp32s3dev_8MB_opi + esp32s3_4M_qspi + usermods src_dir = ./wled00 data_dir = ./wled00/data @@ -129,8 +148,7 @@ framework = arduino board_build.flash_mode = dout monitor_speed = 115200 # slow upload speed but most compatible (use platformio_override.ini to use faster speed) -upload_speed = 921600 -# upload_speed = 115200 +upload_speed = 115200 # ------------------------------------------------------------------------------ # LIBRARIES: required dependencies @@ -438,14 +456,13 @@ board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} -custom_usermods = 7_seg_clock_countdown +custom_usermods = audioreactive build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = dio -upload_protocol = esptool [env:esp32dev_debug] extends = env:esp32dev diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c3dbca2f3f..d60c525261 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -133,7 +133,7 @@ static um_data_t* getAudioData() { */ uint16_t mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); - return FRAMETIME; + return strip.isOffRefreshRequired() ? FRAMETIME : 350; } static const char _data_FX_MODE_STATIC[] PROGMEM = "Solid"; @@ -10276,7 +10276,7 @@ uint16_t mode_particleBalance(void) { if (SEGMENT.check3) // random, use perlin noise xgravity = ((int16_t)perlin8(SEGENV.aux0) - 128); else // sinusoidal - xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) + xgravity = (int16_t)cos8_t(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) // scale the force xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; // xgravity: -127 to +127 PartSys->applyForce(xgravity); From fe49b72fa32fa4fc57a8418688c5fc7176d66bb4 Mon Sep 17 00:00:00 2001 From: dereibims Date: Sun, 11 Jan 2026 23:13:07 +0100 Subject: [PATCH 8/9] replace files with WLEDs that not belong to the usermod --- wled00/FX.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.h b/wled00/FX.h index c4d21ca6f1..bcbab69a59 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -46,7 +46,7 @@ #define DEFAULT_MODE (uint8_t)0 #define DEFAULT_SPEED (uint8_t)128 #define DEFAULT_INTENSITY (uint8_t)128 -#define DEFAULT_COLOR (uint32_t)0xFF0000 +#define DEFAULT_COLOR (uint32_t)0xFFAA00 #define DEFAULT_C1 (uint8_t)128 #define DEFAULT_C2 (uint8_t)128 #define DEFAULT_C3 (uint8_t)16 From bfff0b6b01163332d26512692e81194bdfe9ebee Mon Sep 17 00:00:00 2001 From: dereibims Date: Thu, 15 Jan 2026 21:17:34 +0100 Subject: [PATCH 9/9] Implemented suggested changes --- .../7_seg_clock_countdown.cpp | 314 +++++-------- .../7_seg_clock_countdown.h | 422 +++++++++--------- usermods/7_seg_clock_countdown/README.md | 5 +- 3 files changed, 332 insertions(+), 409 deletions(-) diff --git a/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp index 2db60be5f2..8bf794d302 100644 --- a/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp +++ b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp @@ -4,8 +4,8 @@ over the underlying WLED pixels. Mask bit 1 keeps the original effect/color; mask bit 0 forces black. */ -#include "wled.h" #include "7_seg_clock_countdown.h" +#include "wled.h" /* ===== Static member definitions ===== */ constexpr uint8_t SevenSegClockCountdown::PHYS_TO_LOG[SevenSegClockCountdown::SEGS_PER_DIGIT]; @@ -13,35 +13,29 @@ constexpr uint8_t SevenSegClockCountdown::DIGIT_MASKS[10]; /* ===== Lifecycle ===== */ // Prepare mask buffer on boot. -void SevenSegClockCountdown::setup() -{ +void SevenSegClockCountdown::setup() { ensureMaskSize(); } // Idle loop (no periodic work required). -void SevenSegClockCountdown::loop() -{ +void SevenSegClockCountdown::loop() { } /* ===== Mask management ===== */ // Ensure the mask buffer matches the panel size. -void SevenSegClockCountdown::ensureMaskSize() -{ +void SevenSegClockCountdown::ensureMaskSize() { if (mask.size() != TOTAL_PANEL_LEDS) mask.assign(TOTAL_PANEL_LEDS, 0); } // Clear the mask to fully black. -void SevenSegClockCountdown::clearMask() -{ +void SevenSegClockCountdown::clearMask() { std::fill(mask.begin(), mask.end(), 0); } // Set a contiguous LED range in the mask to 1. -void SevenSegClockCountdown::setRangeOn(uint16_t start, uint16_t len) -{ - for (uint16_t i = 0; i < len; i++) - { +void SevenSegClockCountdown::setRangeOn(uint16_t start, uint16_t len) { + for (uint16_t i = 0; i < len; i++) { uint16_t idx = start + i; if (idx < mask.size()) mask[idx] = 1; @@ -50,23 +44,18 @@ void SevenSegClockCountdown::setRangeOn(uint16_t start, uint16_t len) /* ===== Time helpers ===== */ // Return hundredths-of-second (0..99); descending in countdown mode. -int SevenSegClockCountdown::getHundredths(int currentSeconds, bool countDown) -{ +int SevenSegClockCountdown::getHundredths(int currentSeconds, bool countDown) { unsigned long now = millis(); - if (currentSeconds != lastSecondValue) - { + if (currentSeconds != lastSecondValue) { lastSecondValue = currentSeconds; lastSecondMillis = now; return countDown ? 99 : 0; } unsigned long delta = now - lastSecondMillis; int hundredths; - if (countDown) - { + if (countDown) { hundredths = 99 - (int)(delta / 10); - } - else - { + } else { hundredths = (int)(delta / 10); } if (hundredths < 0) @@ -78,8 +67,7 @@ int SevenSegClockCountdown::getHundredths(int currentSeconds, bool countDown) /* ===== Mode management (save/restore base effect) ===== */ // Restore previously saved effect mode and parameters. -void SevenSegClockCountdown::revertMode() -{ +void SevenSegClockCountdown::revertMode() { Segment &selseg = strip.getSegment(0); selseg.setMode(prevMode, false); selseg.setColor(0, prevColor); @@ -89,8 +77,7 @@ void SevenSegClockCountdown::revertMode() } // Save current effect mode and parameters. -void SevenSegClockCountdown::SaveMode() -{ +void SevenSegClockCountdown::SaveMode() { Segment &selseg = strip.getSegment(0); prevMode = selseg.mode; prevColor = selseg.colors[0]; @@ -100,8 +87,7 @@ void SevenSegClockCountdown::SaveMode() } // Apply a specific effect mode and parameters. -void SevenSegClockCountdown::setMode(uint8_t mode, uint32_t color, uint8_t speed, uint8_t intensity, uint8_t brightness) -{ +void SevenSegClockCountdown::setMode(uint8_t mode, uint32_t color, uint8_t speed, uint8_t intensity, uint8_t brightness) { Segment &selseg = strip.getSegment(0); selseg.setMode(mode, false); selseg.setColor(0, color); @@ -112,8 +98,7 @@ void SevenSegClockCountdown::setMode(uint8_t mode, uint32_t color, uint8_t speed /* ===== Drawing helpers ===== */ // Draw HH:MM:SS into the mask with configurable separator behavior. -void SevenSegClockCountdown::drawClock() -{ +void SevenSegClockCountdown::drawClock() { clearMask(); setDigitInt(0, hour(localTime) / 10); setDigitInt(1, hour(localTime) % 10); @@ -122,26 +107,17 @@ void SevenSegClockCountdown::drawClock() setDigitInt(4, second(localTime) / 10); setDigitInt(5, second(localTime) % 10); - if (SeperatorOn && SeperatorOff) - { - if (second(localTime) % 2) - { + if (SeparatorOn && SeparatorOff) { + if (second(localTime) % 2) { setSeparator(1, sepsOn); setSeparator(2, sepsOn); } - } - else if (SeperatorOn) - { + } else if (SeparatorOn) { setSeparator(1, sepsOn); setSeparator(2, sepsOn); - } - else if (SeperatorOff) - { - } - else - { - if (second(localTime) % 2) - { + } else if (SeparatorOff) { + } else { + if (second(localTime) % 2) { setSeparator(1, sepsOn); setSeparator(2, sepsOn); } @@ -149,20 +125,17 @@ void SevenSegClockCountdown::drawClock() } // Light segments for a single digit from a numeric value (0..9). -void SevenSegClockCountdown::setDigitInt(uint8_t digitIndex, int8_t value) -{ +void SevenSegClockCountdown::setDigitInt(uint8_t digitIndex, int8_t value) { if (digitIndex > 5) return; if (value < 0) return; uint16_t base = digitBase(digitIndex); uint8_t bits = DIGIT_MASKS[(uint8_t)value]; - for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) - { + for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) { uint8_t logSeg = PHYS_TO_LOG[physSeg]; bool segOn = (bits >> logSeg) & 0x01; - if (segOn) - { + if (segOn) { uint16_t segStart = base + (uint16_t)physSeg * LEDS_PER_SEG; setRangeOn(segStart, LEDS_PER_SEG); } @@ -170,18 +143,15 @@ void SevenSegClockCountdown::setDigitInt(uint8_t digitIndex, int8_t value) } // Light segments for a single digit using a letter/symbol mask. -void SevenSegClockCountdown::setDigitChar(uint8_t digitIndex, char c) -{ +void SevenSegClockCountdown::setDigitChar(uint8_t digitIndex, char c) { if (digitIndex > 5) return; uint16_t base = digitBase(digitIndex); uint8_t bits = LETTER_MASK(c); - for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) - { + for (uint8_t physSeg = 0; physSeg < SEGS_PER_DIGIT; physSeg++) { uint8_t logSeg = PHYS_TO_LOG[physSeg]; bool segOn = (bits >> logSeg) & 0x01; - if (segOn) - { + if (segOn) { uint16_t segStart = base + (uint16_t)physSeg * LEDS_PER_SEG; setRangeOn(segStart, LEDS_PER_SEG); } @@ -189,16 +159,14 @@ void SevenSegClockCountdown::setDigitChar(uint8_t digitIndex, char c) } // Light both dots of a separator. -void SevenSegClockCountdown::setSeparator(uint8_t which, bool on) -{ +void SevenSegClockCountdown::setSeparator(uint8_t which, bool on) { uint16_t base = (which == 1) ? sep1Base() : sep2Base(); if (on) setRangeOn(base, SEP_LEDS); } // Light a single half (upper/lower) of the separator. -void SevenSegClockCountdown::setSeparatorHalf(uint8_t which, bool upper, bool on) -{ +void SevenSegClockCountdown::setSeparatorHalf(uint8_t which, bool upper, bool on) { uint16_t base = (which == 1) ? sep1Base() : sep2Base(); uint16_t halfLen = SEP_LEDS / 2; uint16_t start = base + (upper ? 0 : halfLen); @@ -207,12 +175,10 @@ void SevenSegClockCountdown::setSeparatorHalf(uint8_t which, bool upper, bool on } // Apply the mask to the strip (0 → black, 1 → keep pixel). -void SevenSegClockCountdown::applyMaskToStrip() -{ +void SevenSegClockCountdown::applyMaskToStrip() { uint16_t stripLen = strip.getLengthTotal(); uint16_t limit = (stripLen < (uint16_t)mask.size()) ? stripLen : (uint16_t)mask.size(); - for (uint16_t i = 0; i < limit; i++) - { + for (uint16_t i = 0; i < limit; i++) { if (!mask[i]) strip.setPixelColor(i, 0); } @@ -220,8 +186,7 @@ void SevenSegClockCountdown::applyMaskToStrip() /* ===== Target handling ===== */ // Clamp target fields and compute targetUnix; optionally log changes. -void SevenSegClockCountdown::validateTarget(bool changed) -{ +void SevenSegClockCountdown::validateTarget(bool changed) { targetYear = clampVal(targetYear, 1970, 2099); targetMonth = clampVal(targetMonth, 1, 12); targetDay = clampVal(targetDay, 1, 31); @@ -235,8 +200,7 @@ void SevenSegClockCountdown::validateTarget(bool changed) tm.Month = targetMonth; tm.Year = CalendarYrToTm(targetYear); targetUnix = makeTime(tm); - if (changed) - { + if (changed) { char buf[24]; snprintf(buf, sizeof(buf), "%04d-%02u-%02u-%02u-%02u", targetYear, targetMonth, targetDay, targetHour, targetMinute); @@ -246,61 +210,48 @@ void SevenSegClockCountdown::validateTarget(bool changed) /* ===== Countdown rendering ===== */ // Draw countdown (or count-up when target passed) into the mask. -void SevenSegClockCountdown::drawCountdown() -{ +void SevenSegClockCountdown::drawCountdown() { clearMask(); int64_t diff = (int64_t)targetUnix - (int64_t)localTime; bool countingUp = (diff < 0); - int64_t absDiff = abs(diff); - if (countingUp && absDiff > 3600) - { - if (ModeChanged) - { + int64_t absDiff = (diff < 0) ? -diff : diff; + if (countingUp && absDiff > 3600) { + if (ModeChanged) { revertMode(); ModeChanged = false; } drawClock(); return; } - if (countingUp) - { + if (countingUp) { Segment &selseg = strip.getSegment(0); - if (!ModeChanged) - { + if (!ModeChanged) { SaveMode(); setMode(FX_MODE_STATIC, 0xFF0000, 128, 128, 255); ModeChanged = true; IgnoreBlinking = false; } - if (selseg.mode != FX_MODE_STATIC && ModeChanged && !IgnoreBlinking) - { + if (selseg.mode != FX_MODE_STATIC && ModeChanged && !IgnoreBlinking) { IgnoreBlinking = true; revertMode(); } - if (millis() - lastBlink >= countdownBlinkInterval) - { + if (millis() - lastBlink >= countdownBlinkInterval) { BlinkToggle = !BlinkToggle; lastBlink = millis(); } - if (BlinkToggle && !IgnoreBlinking) - { + if (BlinkToggle && !IgnoreBlinking) { strip.setBrightness(255); - } - else if (!BlinkToggle && !IgnoreBlinking) - { + } else if (!BlinkToggle && !IgnoreBlinking) { strip.setBrightness(0); } - if (absDiff < 60) - { + if (absDiff < 60) { setDigitChar(0, 'x'); setDigitChar(1, 'x'); setDigitInt(2, absDiff / 10); setDigitInt(3, absDiff % 10); setDigitChar(4, 'x'); setDigitChar(5, 'x'); - } - if (absDiff >= 60) - { + } else if (absDiff >= 60) { setDigitChar(0, 'x'); setDigitChar(1, 'x'); setDigitInt(2, absDiff / 600); @@ -311,10 +262,8 @@ void SevenSegClockCountdown::drawCountdown() } } - if (!countingUp) - { - if (ModeChanged) - { + if (!countingUp) { + if (ModeChanged) { revertMode(); ModeChanged = false; } @@ -326,18 +275,21 @@ void SevenSegClockCountdown::drawCountdown() fullMinutes = (uint32_t)(absDiff / 60); fullSeconds = (uint32_t)absDiff; - if (remDays > 99) - { + if (remDays > 999) { + setDigitChar(0, 'o'); + setDigitInt(1, 9); + setDigitInt(2, 9); + setDigitInt(3, 9); + setDigitChar(4, 'd'); + setDigitChar(5, ' '); + } else if (remDays > 99 && remDays <= 999) { setDigitChar(0, ' '); setDigitInt(1, (remDays / 100) % 10); setDigitInt(2, (remDays / 10) % 10); setDigitInt(3, remDays % 10); setDigitChar(4, 'd'); setDigitChar(5, ' '); - return; - } - if (remDays <= 99 && fullHours > 99) - { + } else if (remDays <= 99 && fullHours > 99) { setDigitInt(0, remDays / 10); setDigitInt(1, remDays % 10); setSeparator(1, sepsOn); @@ -346,10 +298,7 @@ void SevenSegClockCountdown::drawCountdown() setSeparator(2, sepsOn); setDigitInt(4, remMinutes / 10); setDigitInt(5, remMinutes % 10); - return; - } - if (fullHours <= 99 && fullMinutes > 99) - { + } else if (fullHours <= 99 && fullMinutes > 99) { setDigitInt(0, fullHours / 10); setDigitInt(1, fullHours % 10); setSeparator(1, sepsOn); @@ -358,42 +307,36 @@ void SevenSegClockCountdown::drawCountdown() setSeparator(2, sepsOn); setDigitInt(4, remSeconds / 10); setDigitInt(5, remSeconds % 10); - return; + } else { + int hs = getHundredths(remSeconds, /*countDown*/ !countingUp); + + setDigitInt(0, fullMinutes / 10); + setDigitInt(1, fullMinutes % 10); + setSeparatorHalf(1, true, sepsOn); + setDigitInt(2, remSeconds / 10); + setDigitInt(3, remSeconds % 10); + setSeparator(2, sepsOn); + setDigitInt(4, hs / 10); + setDigitInt(5, hs % 10); } - int hs = getHundredths(remSeconds, /*countDown*/ !countingUp); - - setDigitInt(0, fullMinutes / 10); - setDigitInt(1, fullMinutes % 10); - setSeparatorHalf(1, true, sepsOn); - setDigitInt(2, remSeconds / 10); - setDigitInt(3, remSeconds % 10); - setSeparator(2, sepsOn); - setDigitInt(4, hs / 10); - setDigitInt(5, hs % 10); } } /* ===== WLED overlay entrypoint ===== */ // Draw the overlay (clock, countdown, or alternating) and apply mask. -void SevenSegClockCountdown::handleOverlayDraw() -{ +void SevenSegClockCountdown::handleOverlayDraw() { if (!enabled) return; - if (showClock && showCountdown) - { + if (showClock && showCountdown) { uint32_t period = (alternatingTime > 0) ? (uint32_t)alternatingTime : 10U; uint32_t block = (uint32_t)localTime / period; if ((block & 1U) == 0U) drawClock(); else drawCountdown(); - } - else if (showClock) - { + } else if (showClock) { drawClock(); - } - else - { + } else { drawCountdown(); } applyMaskToStrip(); @@ -401,8 +344,7 @@ void SevenSegClockCountdown::handleOverlayDraw() /* ===== UI / JSON state ===== */ // Add compact info UI elements to the JSON info block. -void SevenSegClockCountdown::addToJsonInfo(JsonObject &root) -{ +void SevenSegClockCountdown::addToJsonInfo(JsonObject &root) { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); @@ -416,12 +358,12 @@ void SevenSegClockCountdown::addToJsonInfo(JsonObject &root) infoArr.add(uiDomString); infoArr = user.createNestedArray(F("Status")); infoArr.add(enabled ? F("active") : F("disabled")); - infoArr = user.createNestedArray(F("Clock Seperators")); - if (SeperatorOn && SeperatorOff) + infoArr = user.createNestedArray(F("Clock Separators")); + if (SeparatorOn && SeparatorOff) infoArr.add(F("Blinking")); - else if (SeperatorOn) + else if (SeparatorOn) infoArr.add(F("Always On")); - else if (SeperatorOff) + else if (SeparatorOff) infoArr.add(F("Always Off")); else infoArr.add(F("Blinking")); @@ -443,14 +385,13 @@ void SevenSegClockCountdown::addToJsonInfo(JsonObject &root) } // Serialize usermod state into JSON state. -void SevenSegClockCountdown::addToJsonState(JsonObject &root) -{ +void SevenSegClockCountdown::addToJsonState(JsonObject &root) { JsonObject s = root[F("7seg")].as(); if (s.isNull()) s = root.createNestedObject(F("7seg")); s[F("enabled")] = enabled; - s[F("SeperatorOn")] = SeperatorOn; - s[F("SeperatorOff")] = SeperatorOff; + s[F("SeparatorOn")] = SeparatorOn; + s[F("SeparatorOff")] = SeparatorOff; s[F("targetYear")] = targetYear; s[F("targetMonth")] = targetMonth; s[F("targetDay")] = targetDay; @@ -462,36 +403,30 @@ void SevenSegClockCountdown::addToJsonState(JsonObject &root) } // Read usermod state from JSON state and validate target. -void SevenSegClockCountdown::readFromJsonState(JsonObject &root) -{ +void SevenSegClockCountdown::readFromJsonState(JsonObject &root) { JsonObject s = root[F("7seg")].as(); if (s.isNull()) return; if (s.containsKey(F("enabled"))) enabled = s[F("enabled")].as(); bool changed = false; - if (s.containsKey(F("targetYear"))) - { + if (s.containsKey(F("targetYear"))) { targetYear = s[F("targetYear")].as(); changed = true; } - if (s.containsKey(F("targetMonth"))) - { + if (s.containsKey(F("targetMonth"))) { targetMonth = s[F("targetMonth")].as(); changed = true; } - if (s.containsKey(F("targetDay"))) - { + if (s.containsKey(F("targetDay"))) { targetDay = s[F("targetDay")].as(); changed = true; } - if (s.containsKey(F("targetHour"))) - { + if (s.containsKey(F("targetHour"))) { targetHour = s[F("targetHour")].as(); changed = true; } - if (s.containsKey(F("targetMinute"))) - { + if (s.containsKey(F("targetMinute"))) { targetMinute = s[F("targetMinute")].as(); changed = true; } @@ -501,18 +436,17 @@ void SevenSegClockCountdown::readFromJsonState(JsonObject &root) showCountdown = s[F("showCountdown")].as(); if (s.containsKey(F("alternatingTime"))) alternatingTime = s[F("alternatingTime")].as(); - if (s.containsKey(F("SeperatorOn"))) - SeperatorOn = s[F("SeperatorOn")].as(); - if (s.containsKey(F("SeperatorOff"))) - SeperatorOff = s[F("SeperatorOff")].as(); + if (s.containsKey(F("SeparatorOn"))) + SeparatorOn = s[F("SeparatorOn")].as(); + if (s.containsKey(F("SeparatorOff"))) + SeparatorOff = s[F("SeparatorOff")].as(); if (changed) validateTarget(true); } /* ===== Config persistence ===== */ // Write persistent configuration to cfg.json. -void SevenSegClockCountdown::addToConfig(JsonObject &root) -{ +void SevenSegClockCountdown::addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(F("7seg")); top[F("enabled")] = enabled; top[F("targetYear")] = targetYear; @@ -523,13 +457,12 @@ void SevenSegClockCountdown::addToConfig(JsonObject &root) top[F("showClock")] = showClock; top[F("showCountdown")] = showCountdown; top[F("alternatingTime")] = alternatingTime; - top[F("SeperatorOn")] = SeperatorOn; - top[F("SeperatorOff")] = SeperatorOff; + top[F("SeparatorOn")] = SeparatorOn; + top[F("SeparatorOff")] = SeparatorOff; } // Read persistent configuration from cfg.json. -bool SevenSegClockCountdown::readFromConfig(JsonObject &root) -{ +bool SevenSegClockCountdown::readFromConfig(JsonObject &root) { JsonObject top = root[F("7seg")]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[F("enabled")], enabled, true); @@ -541,92 +474,85 @@ bool SevenSegClockCountdown::readFromConfig(JsonObject &root) configComplete &= getJsonValue(top[F("showClock")], showClock, true); configComplete &= getJsonValue(top[F("showCountdown")], showCountdown, false); configComplete &= getJsonValue(top[F("alternatingTime")], alternatingTime, (uint16_t)10); - configComplete &= getJsonValue(top[F("SeperatorOn")], SeperatorOn, true); - configComplete &= getJsonValue(top[F("SeperatorOff")], SeperatorOff, true); + configComplete &= getJsonValue(top[F("SeparatorOn")], SeparatorOn, true); + configComplete &= getJsonValue(top[F("SeparatorOff")], SeparatorOff, true); validateTarget(true); return configComplete; } /* ===== MQTT integration ===== */ // Subscribe to usermod MQTT topic on connect. -void SevenSegClockCountdown::onMqttConnect(bool sessionPresent) -{ +void SevenSegClockCountdown::onMqttConnect(bool sessionPresent) { String topic = mqttDeviceTopic; - topic += MQTT_Topic; + topic += "/"; topic += MQTT_Topic; topic += "/#"; mqtt->subscribe(topic.c_str(), 0); } // Handle usermod MQTT messages (simple key/value). -bool SevenSegClockCountdown::onMqttMessage(char *topic, char *payload) -{ +bool SevenSegClockCountdown::onMqttMessage(char *topic, char *payload) { String topicStr = String(topic); - if (!topicStr.startsWith(F("/7seg/"))) + String prefix = "/"; prefix += mqttDeviceTopic; prefix += "/"; + if (!topicStr.startsWith(prefix)) return false; - String subTopic = topicStr.substring(strlen("/7seg/")); - if (subTopic.indexOf(F("enabled")) >= 0) - { + String subTopic = topicStr.substring(strlen(prefix.c_str())); + if (subTopic.indexOf(F("enabled")) >= 0) { String payloadStr = String(payload); enabled = (payloadStr == F("true") || payloadStr == F("1")); return true; } - if (subTopic.indexOf(F("targetYear")) >= 0) - { + if (subTopic.indexOf(F("targetYear")) >= 0) { String payloadStr = String(payload); targetYear = payloadStr.toInt(); + validateTarget(true); return true; } - if (subTopic.indexOf(F("targetMonth")) >= 0) - { + if (subTopic.indexOf(F("targetMonth")) >= 0) { String payloadStr = String(payload); targetMonth = (uint8_t)payloadStr.toInt(); + validateTarget(true); return true; } - if (subTopic.indexOf(F("targetDay")) >= 0) - { + if (subTopic.indexOf(F("targetDay")) >= 0) { String payloadStr = String(payload); targetDay = (uint8_t)payloadStr.toInt(); + validateTarget(true); return true; } - if (subTopic.indexOf(F("targetHour")) >= 0) - { + if (subTopic.indexOf(F("targetHour")) >= 0) { String payloadStr = String(payload); targetHour = (uint8_t)payloadStr.toInt(); + validateTarget(true); return true; } - if (subTopic.indexOf(F("targetMinute")) >= 0) - { + if (subTopic.indexOf(F("targetMinute")) >= 0) { String payloadStr = String(payload); targetMinute = (uint8_t)payloadStr.toInt(); + validateTarget(true); return true; } - if (subTopic.indexOf(F("showClock")) >= 0) - { + if (subTopic.indexOf(F("showClock")) >= 0) { String payloadStr = String(payload); showClock = (payloadStr == F("true") || payloadStr == F("1")); return true; } - if (subTopic.indexOf(F("showCountdown")) >= 0) - { + if (subTopic.indexOf(F("showCountdown")) >= 0) { String payloadStr = String(payload); showCountdown = (payloadStr == F("true") || payloadStr == F("1")); return true; } - if (subTopic.indexOf(F("alternatingTime")) >= 0) - { + if (subTopic.indexOf(F("alternatingTime")) >= 0) { String payloadStr = String(payload); alternatingTime = (uint16_t)payloadStr.toInt(); return true; } - if (subTopic.indexOf(F("SeperatorOn")) >= 0) - { + if (subTopic.indexOf(F("SeparatorOn")) >= 0) { String payloadStr = String(payload); - SeperatorOn = (payloadStr == F("true") || payloadStr == F("1")); + SeparatorOn = (payloadStr == F("true") || payloadStr == F("1")); return true; } - if (subTopic.indexOf(F("SeperatorOff")) >= 0) - { + if (subTopic.indexOf(F("SeparatorOff")) >= 0) { String payloadStr = String(payload); - SeperatorOff = (payloadStr == F("true") || payloadStr == F("1")); + SeparatorOff = (payloadStr == F("true") || payloadStr == F("1")); return true; } return false; diff --git a/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h index 22cda8bc36..666b25e0d3 100644 --- a/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h +++ b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h @@ -1,5 +1,6 @@ #pragma once -#include +#include "wled.h" + /*-------------------------------------------------------------------------------------------------------------------------------------- 7-Segment Clock & Countdown Usermod @@ -32,221 +33,216 @@ #define MQTT_Topic "7seg" -class SevenSegClockCountdown : public Usermod -{ +class SevenSegClockCountdown : public Usermod { private: - // Geometry: LED counts for a single digit and the full panel. - // - One digit has 7 segments, each with LEDS_PER_SEG pixels - // - Two separator blocks exist between digit 2/3 and 4/5, each with SEP_LEDS pixels - static const uint16_t LEDS_PER_SEG = 5; - static const uint8_t SEGS_PER_DIGIT = 7; - static const uint16_t SEP_LEDS = 10; - static constexpr uint16_t LEDS_PER_DIGIT = LEDS_PER_SEG * SEGS_PER_DIGIT; // 35 - static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; // 230 - - // Physical-to-logical segment mapping per digit: physical order F-A-B-G-E-D-C - static constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { - SEG_F, - SEG_A, - SEG_B, - SEG_G, - SEG_E, - SEG_D, - SEG_C}; - - // Index helpers into the linear LED stream for each digit and separator block. - static uint16_t digitBase(uint8_t d) - { - switch (d) - { - case 0: - return 0; // digit 1 - case 1: - return 35; // digit 2 - case 2: - return 80; // digit 3 - case 3: - return 115; // digit 4 - case 4: - return 160; // digit 5 - case 5: - return 195; // digit 6 - default: - return 0; + // Geometry: LED counts for a single digit and the full panel. + // - One digit has 7 segments, each with LEDS_PER_SEG pixels + // - Two separator blocks exist between digit 2/3 and 4/5, each with SEP_LEDS pixels + static const uint16_t VERY_FIRST_LED = 0; + static const uint16_t LEDS_PER_SEG = 5; + static const uint8_t SEGS_PER_DIGIT = 7; + static const uint16_t SEP_LEDS = 10; + static constexpr uint16_t LEDS_PER_DIGIT = LEDS_PER_SEG * SEGS_PER_DIGIT; // 35 + static constexpr uint16_t TOTAL_PANEL_LEDS = 6 * LEDS_PER_DIGIT + 2 * SEP_LEDS; // 230 + + // Physical-to-logical segment mapping per digit: physical order F-A-B-G-E-D-C + static constexpr uint8_t PHYS_TO_LOG[SEGS_PER_DIGIT] = { + SEG_F, + SEG_A, + SEG_B, + SEG_G, + SEG_E, + SEG_D, + SEG_C}; + + // Index helpers into the linear LED stream for each digit and separator block. + static uint16_t digitBase(uint8_t d) { + switch (d) { + case 0: + return VERY_FIRST_LED; // digit 1 + case 1: + return VERY_FIRST_LED + LEDS_PER_DIGIT; // digit 2 + case 2: + return VERY_FIRST_LED + SEP_LEDS + LEDS_PER_DIGIT * 2; // digit 3 + case 3: + return VERY_FIRST_LED + SEP_LEDS + LEDS_PER_DIGIT * 3; // digit 4 + case 4: + return VERY_FIRST_LED + SEP_LEDS * 2 + LEDS_PER_DIGIT * 4; // digit 5 + case 5: + return VERY_FIRST_LED + SEP_LEDS * 2 + LEDS_PER_DIGIT * 5; // digit 6 + default: + return 0; + } + } + static constexpr uint16_t sep1Base() { return 70; } + static constexpr uint16_t sep2Base() { return 150; } + + static const unsigned long countdownBlinkInterval = 500; + + /*-------------------------------------------------------------------------------------- + --------------------------Do NOT edit anything below this line-------------------------- + --------------------------------------------------------------------------------------*/ + + /* ===== Digit bitmasks (A..G bits 0..6) ===== */ + static constexpr uint8_t DIGIT_MASKS[10] = { + 0b00111111, // 0 (A..F) + 0b00000110, // 1 (B,C) + 0b01011011, // 2 (A,B,D,E,G) + 0b01001111, // 3 (A,B,C,D,G) + 0b01100110, // 4 (B,C,F,G) + 0b01101101, // 5 (A,C,D,F,G) + 0b01111101, // 6 (A,C,D,E,F,G) + 0b00000111, // 7 (A,B,C) + 0b01111111, // 8 (A..G) + 0b01101111 // 9 (A,B,C,D,F,G) + }; + + /* ===== Runtime state (mask, flags, variables) ===== */ + std::vector mask; + bool enabled = true; + bool sepsOn = true; + bool SeparatorOn = true; + bool SeparatorOff = true; + + bool showClock = true; + bool showCountdown = false; + uint16_t alternatingTime = 10; + + int targetYear = year(localTime); + uint8_t targetMonth = month(localTime); + uint8_t targetDay = day(localTime); + uint8_t targetHour = 0; + uint8_t targetMinute = 0; + time_t targetUnix = 0; + + uint32_t fullHours = 0; + uint32_t fullMinutes = 0; + uint32_t fullSeconds = 0; + uint32_t remDays = 0; + uint8_t remHours = 0; + uint8_t remMinutes = 0; + uint8_t remSeconds = 0; + + unsigned long lastSecondMillis = 0; + int lastSecondValue = -1; + + bool IgnoreBlinking = false; + bool ModeChanged = false; + uint8_t prevMode = 0; + uint32_t prevColor = 0; + uint8_t prevSpeed = 0; + uint8_t prevIntensity = 0; + uint8_t prevBrightness = 0; + unsigned long lastBlink = 0; + bool BlinkToggle = false; + + // Returns a bitmask A..G (0..6). If the character is unsupported, fallback lights A, D, G. + uint8_t LETTER_MASK(char c) { + switch (c) { + // Uppercase + case 'A': + return 0b01110111; // A,B,C,E,F,G + case 'B': + return 0b01111100; // b-like (C,D,E,F,G) + case 'C': + return 0b00111001; // A,D,E,F + case 'D': + return 0b01011110; // d-like (B,C,D,E,G) + case 'E': + return 0b01111001; // A,D,E,F,G + case 'F': + return 0b01110001; // A,E,F,G + case 'H': + return 0b01110110; // B,C,E,F,G + case 'J': + return 0b00011110; // B,C,D + case 'L': + return 0b00111000; // D,E,F + case 'O': + return 0b00111111; // A,B,C,D,E,F (like '0') + case 'P': + return 0b01110011; // A,B,E,F,G + case 'T': + return 0b01111000; // D,E,F,G + case 'U': + return 0b00111110; // B,C,D,E,F + case 'Y': + return 0b01101110; // B,C,D,F,G + + // Lowercase + case 'a': + return 0b01011111; // A,B,C,D,E,G + case 'b': + return 0b01111100; // C,D,E,F,G + case 'c': + return 0b01011000; // D,E,G + case 'd': + return 0b01011110; // B,C,D,E,G + case 'e': + return 0b01111011; // A,D,E,F,G (with C off) + case 'f': + return 0b01110001; // A,E,F,G + case 'h': + return 0b01110100; // C,E,F,G + case 'j': + return 0b00001110; // B,C,D + case 'l': + return 0b00110000; // E,F + case 'n': + return 0b01010100; // C,E,G + case 'o': + return 0b01011100; // C,D,E,G + case 'r': + return 0b01010000; // E,G + case 't': + return 0b01111000; // D,E,F,G + case 'u': + return 0b00011100; // C,D,E + case 'y': + return 0b01101110; // B,C,D,F,G + case '-': + return 0b01000000; // G + case '_': + return 0b00001000; // D + case ' ': + return 0b00000000; // blank + + default: + return 0b01001001; // fallback: A, D, G + } } - } - static constexpr uint16_t sep1Base() { return 70; } - static constexpr uint16_t sep2Base() { return 150; } - - static const unsigned long countdownBlinkInterval = 500; - - /*-------------------------------------------------------------------------------------- - --------------------------Do NOT edit anything below this line-------------------------- - --------------------------------------------------------------------------------------*/ - - /* ===== Digit bitmasks (A..G bits 0..6) ===== */ - static constexpr uint8_t DIGIT_MASKS[10] = { - 0b00111111, // 0 (A..F) - 0b00000110, // 1 (B,C) - 0b01011011, // 2 (A,B,D,E,G) - 0b01001111, // 3 (A,B,C,D,G) - 0b01100110, // 4 (B,C,F,G) - 0b01101101, // 5 (A,C,D,F,G) - 0b01111101, // 6 (A,C,D,E,F,G) - 0b00000111, // 7 (A,B,C) - 0b01111111, // 8 (A..G) - 0b01101111 // 9 (A,B,C,D,F,G) - }; - - /* ===== Runtime state (mask, flags, variables) ===== */ - std::vector mask; - bool enabled = true; - bool sepsOn = true; - bool SeperatorOn = true; - bool SeperatorOff = true; - - bool showClock = true; - bool showCountdown = false; - uint16_t alternatingTime = 10; - - int targetYear = year(localTime); - uint8_t targetMonth = month(localTime); - uint8_t targetDay = day(localTime); - uint8_t targetHour = 0; - uint8_t targetMinute = 0; - time_t targetUnix = 0; - - uint32_t fullHours = 0; - uint32_t fullMinutes = 0; - uint32_t fullSeconds = 0; - uint32_t remDays = 0; - uint8_t remHours = 0; - uint8_t remMinutes = 0; - uint8_t remSeconds = 0; - - unsigned long lastSecondMillis = 0; - int lastSecondValue = -1; - - bool IgnoreBlinking = false; - bool ModeChanged = false; - uint8_t prevMode = 0; - uint32_t prevColor = 0; - uint8_t prevSpeed = 0; - uint8_t prevIntensity = 0; - uint8_t prevBrightness = 0; - unsigned long lastBlink = 0; - bool BlinkToggle = false; - - // Returns a bitmask A..G (0..6). If the character is unsupported, fallback lights A, D, G. - uint8_t LETTER_MASK(char c) - { - switch (c) - { - // Uppercase - case 'A': - return 0b01110111; // A,B,C,E,F,G - case 'B': - return 0b01111100; // b-like (C,D,E,F,G) - case 'C': - return 0b00111001; // A,D,E,F - case 'D': - return 0b01011110; // d-like (B,C,D,E,G) - case 'E': - return 0b01111001; // A,D,E,F,G - case 'F': - return 0b01110001; // A,E,F,G - case 'H': - return 0b01110110; // B,C,E,F,G - case 'J': - return 0b00011110; // B,C,D - case 'L': - return 0b00111000; // D,E,F - case 'O': - return 0b00111111; // A,B,C,D,E,F (like '0') - case 'P': - return 0b01110011; // A,B,E,F,G - case 'T': - return 0b01111000; // D,E,F,G - case 'U': - return 0b00111110; // B,C,D,E,F - case 'Y': - return 0b01101110; // B,C,D,F,G - - // Lowercase - case 'a': - return 0b01011111; // A,B,C,D,E,G - case 'b': - return 0b01111100; // C,D,E,F,G - case 'c': - return 0b01011000; // D,E,G - case 'd': - return 0b01011110; // B,C,D,E,G - case 'e': - return 0b01111011; // A,D,E,F,G (with C off) - case 'f': - return 0b01110001; // A,E,F,G - case 'h': - return 0b01110100; // C,E,F,G - case 'j': - return 0b00001110; // B,C,D - case 'l': - return 0b00110000; // E,F - case 'n': - return 0b01010100; // C,E,G - case 'o': - return 0b01011100; // C,D,E,G - case 'r': - return 0b01010000; // E,G - case 't': - return 0b01111000; // D,E,F,G - case 'u': - return 0b00011100; // C,D,E - case 'y': - return 0b01101110; // B,C,D,F,G - case '-': - return 0b01000000; // G - case '_': - return 0b00001000; // D - case ' ': - return 0b00000000; // blank - - default: - return 0b01001001; // fallback: A, D, G + + static int clampVal(int v, int lo, int hi) { + return (v < lo) ? lo : (v > hi ? hi : v); } - } - - static uint8_t clampVal(int v, int lo, int hi) - { - return (v < lo) ? lo : (v > hi ? hi : v); - } - - /* ===== Private methods ===== */ - void ensureMaskSize(); // ensure mask matches panel size - void clearMask(); // clear mask to transparent - void setRangeOn(uint16_t start, uint16_t len); // set contiguous mask range on - int getHundredths(int currentSeconds, bool countDown); // compute hundredths (0..99) - void drawClock(); // render HH:MM:SS into mask - void revertMode(); // restore previous effect mode - void SaveMode(); // save current effect mode - void setMode(uint8_t mode, uint32_t color, uint8_t speed, uint8_t intensity, uint8_t brightness); // apply effect mode - void drawCountdown(); // render countdown/count-up into mask - void setDigitInt(uint8_t digitIndex, int8_t value); // draw numeric digit - void setDigitChar(uint8_t digitIndex, char c); // draw character/symbol - void setSeparator(uint8_t which, bool on); // set both separator dots - void setSeparatorHalf(uint8_t which, bool upperDot, bool on); // set separator half - void applyMaskToStrip(); // apply mask to physical strip - void validateTarget(bool changed = false); // clamp fields and compute targetUnix + + /* ===== Private methods ===== */ + void ensureMaskSize(); // ensure mask matches panel size + void clearMask(); // clear mask to transparent + void setRangeOn(uint16_t start, uint16_t len); // set contiguous mask range on + int getHundredths(int currentSeconds, bool countDown); // compute hundredths (0..99) + void drawClock(); // render HH:MM:SS into mask + void revertMode(); // restore previous effect mode + void SaveMode(); // save current effect mode + void setMode(uint8_t mode, uint32_t color, uint8_t speed, uint8_t intensity, uint8_t brightness); // apply effect mode + void drawCountdown(); // render countdown/count-up into mask + void setDigitInt(uint8_t digitIndex, int8_t value); // draw numeric digit + void setDigitChar(uint8_t digitIndex, char c); // draw character/symbol + void setSeparator(uint8_t which, bool on); // set both separator dots + void setSeparatorHalf(uint8_t which, bool upperDot, bool on); // set separator half + void applyMaskToStrip(); // apply mask to physical strip + void validateTarget(bool changed = false); // clamp fields and compute targetUnix public: - void setup() override; // prepare usermod - void loop() override; // periodic loop (unused) - void handleOverlayDraw(); // main overlay draw entrypoint - void addToJsonInfo(JsonObject &root) override; // add compact info UI - void addToJsonState(JsonObject &root) override; // serialize state - void readFromJsonState(JsonObject &root) override; // read state - void addToConfig(JsonObject &root) override; // write persistent config - bool readFromConfig(JsonObject &root) override; // read persistent config - void onMqttConnect(bool sessionPresent) override; // subscribe on connect - bool onMqttMessage(char *topic, char *payload) override; // handle mqtt messages - uint16_t getId() override; // usermod id + void setup() override; // prepare usermod + void loop() override; // periodic loop (unused) + void handleOverlayDraw(); // main overlay draw entrypoint + void addToJsonInfo(JsonObject &root) override; // add compact info UI + void addToJsonState(JsonObject &root) override; // serialize state + void readFromJsonState(JsonObject &root) override; // read state + void addToConfig(JsonObject &root) override; // write persistent config + bool readFromConfig(JsonObject &root) override; // read persistent config + void onMqttConnect(bool sessionPresent) override; // subscribe on connect + bool onMqttMessage(char *topic, char *payload) override; // handle mqtt messages + uint16_t getId() override; // usermod id }; diff --git a/usermods/7_seg_clock_countdown/README.md b/usermods/7_seg_clock_countdown/README.md index a13ba88f33..8313d24c2f 100644 --- a/usermods/7_seg_clock_countdown/README.md +++ b/usermods/7_seg_clock_countdown/README.md @@ -5,14 +5,15 @@ - 3D-print: MakerWorld - PCB: GitHub, in this usermod’s folder - Active development happens in the WLED fork by DereIBims: - - https://github.com/DereIBims/WLED + - [github.com/DereIBims/WLED](https://github.com/DereIBims/WLED) A usermod that renders a six‑digit, two‑separator seven‑segment display as an overlay mask on top of WLED’s normal effects/colors. Lit “segments” preserve the underlying pixel color; all other pixels are forced to black. This lets you show a clock or a countdown without losing the active effect. ## What it shows - Clock: HH:MM:SS with configurable separator behavior (on/off/blink) - Countdown to a target date/time - - > 99 days: ` ddd d` + - > 999 days: `'o'ddd d` + - > 99 days: `' 'ddd d` - ≤ 99 days: `dd:hh:mm` - ≤ 99 hours: `hh:mm:ss` - ≤ 99 minutes: `MM·SS:hh` (upper dot between minutes and seconds, plus hundredths 00–99)