diff --git a/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp
new file mode 100644
index 0000000000..8bf794d302
--- /dev/null
+++ b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.cpp
@@ -0,0 +1,566 @@
+/*
+ 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 "7_seg_clock_countdown.h"
+#include "wled.h"
+
+/* ===== Static member definitions ===== */
+constexpr uint8_t SevenSegClockCountdown::PHYS_TO_LOG[SevenSegClockCountdown::SEGS_PER_DIGIT];
+constexpr uint8_t SevenSegClockCountdown::DIGIT_MASKS[10];
+
+/* ===== 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;
+ }
+ unsigned long delta = now - lastSecondMillis;
+ int hundredths;
+ if (countDown) {
+ hundredths = 99 - (int)(delta / 10);
+ } else {
+ hundredths = (int)(delta / 10);
+ }
+ if (hundredths < 0)
+ hundredths = 0;
+ if (hundredths > 99)
+ hundredths = 99;
+ return hundredths;
+}
+
+/* ===== 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);
+}
+
+// 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;
+}
+
+// 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);
+}
+
+/* ===== 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 (SeparatorOn && SeparatorOff) {
+ if (second(localTime) % 2) {
+ setSeparator(1, sepsOn);
+ setSeparator(2, sepsOn);
+ }
+ } else if (SeparatorOn) {
+ setSeparator(1, sepsOn);
+ setSeparator(2, sepsOn);
+ } else if (SeparatorOff) {
+ } else {
+ if (second(localTime) % 2) {
+ setSeparator(1, sepsOn);
+ setSeparator(2, sepsOn);
+ }
+ }
+}
+
+// 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);
+ }
+ }
+}
+
+// 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);
+ }
+ }
+}
+
+// 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);
+}
+
+// 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);
+}
+
+// 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);
+ }
+}
+
+/* ===== 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);
+ }
+}
+
+/* ===== 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 = (diff < 0) ? -diff : 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) {
+ setDigitChar(0, 'x');
+ setDigitChar(1, 'x');
+ setDigitInt(2, absDiff / 10);
+ setDigitInt(3, absDiff % 10);
+ setDigitChar(4, 'x');
+ setDigitChar(5, 'x');
+ } else if (absDiff >= 60) {
+ 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);
+ }
+ }
+
+ if (!countingUp) {
+ if (ModeChanged) {
+ revertMode();
+ ModeChanged = false;
+ }
+ 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 > 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, ' ');
+ } else 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);
+ } else 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);
+ } 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);
+ }
+ }
+}
+
+/* ===== 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;
+ uint32_t block = (uint32_t)localTime / period;
+ if ((block & 1U) == 0U)
+ drawClock();
+ else
+ drawCountdown();
+ } else if (showClock) {
+ drawClock();
+ } else {
+ drawCountdown();
+ }
+ applyMaskToStrip();
+};
+
+/* ===== 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 Separators"));
+ if (SeparatorOn && SeparatorOff)
+ infoArr.add(F("Blinking"));
+ else if (SeparatorOn)
+ infoArr.add(F("Always On"));
+ else if (SeparatorOff)
+ 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"));
+}
+
+// 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("SeparatorOn")] = SeparatorOn;
+ s[F("SeparatorOff")] = SeparatorOff;
+ 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;
+}
+
+// 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;
+ }
+ 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("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) {
+ 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("SeparatorOn")] = SeparatorOn;
+ top[F("SeparatorOff")] = SeparatorOff;
+}
+
+// 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("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) {
+ String topic = mqttDeviceTopic;
+ 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) {
+ String topicStr = String(topic);
+ String prefix = "/"; prefix += mqttDeviceTopic; prefix += "/";
+ if (!topicStr.startsWith(prefix))
+ return false;
+ 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) {
+ String payloadStr = String(payload);
+ targetYear = payloadStr.toInt();
+ validateTarget(true);
+ return true;
+ }
+ 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) {
+ String payloadStr = String(payload);
+ targetDay = (uint8_t)payloadStr.toInt();
+ validateTarget(true);
+ return true;
+ }
+ 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) {
+ String payloadStr = String(payload);
+ targetMinute = (uint8_t)payloadStr.toInt();
+ validateTarget(true);
+ 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("SeparatorOn")) >= 0) {
+ String payloadStr = String(payload);
+ SeparatorOn = (payloadStr == F("true") || payloadStr == F("1"));
+ return true;
+ }
+ if (subTopic.indexOf(F("SeparatorOff")) >= 0) {
+ String payloadStr = String(payload);
+ SeparatorOff = (payloadStr == F("true") || payloadStr == F("1"));
+ return true;
+ }
+ return false;
+}
+
+/* ===== 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
new file mode 100644
index 0000000000..666b25e0d3
--- /dev/null
+++ b/usermods/7_seg_clock_countdown/7_seg_clock_countdown.h
@@ -0,0 +1,248 @@
+#pragma once
+#include "wled.h"
+
+
+/*--------------------------------------------------------------------------------------------------------------------------------------
+ 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
+----------------------------------------------------------------------*/
+
+#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
+
+/*-------------------------------Begin modifications down here!!!------------------------
+-----------------------------------------------------------------------------------------
+-----------------------------------------------------------------------------------------*/
+
+#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 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 int 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
new file mode 100644
index 0000000000..8313d24c2f
--- /dev/null
+++ b/usermods/7_seg_clock_countdown/README.md
@@ -0,0 +1,171 @@
+# 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:
+ - [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
+ - > 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)
+- 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 located at `usermods/7_seg_clock_countdown` and can be enabled by adding the folder name to your PlatformIO environment’s `custom_usermods` setting.
+
+- 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 display accurately.
+
+### Required core tweak for smooth hundredths
+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.
+
+- `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 } }
+```
+
+## 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
+ - 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
+- `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`).
diff --git a/usermods/7_seg_clock_countdown/library.json b/usermods/7_seg_clock_countdown/library.json
new file mode 100644
index 0000000000..59d224d92c
--- /dev/null
+++ b/usermods/7_seg_clock_countdown/library.json
@@ -0,0 +1,13 @@
+{
+ "name": "7_seg_clock_countdown",
+ "version": "0.1.0",
+ "description": "7-segment clock and countdown",
+ "authors": [
+ {
+ "name": "DereIBims"
+ }
+ ],
+ "frameworks": ["arduino"],
+ "platforms": ["espressif32"],
+ "build": { "libArchive": false }
+}