diff --git a/usermods/seven_segment_display/readme.md b/usermods/seven_segment_display/readme.md new file mode 100644 index 00000000..ca52383d --- /dev/null +++ b/usermods/seven_segment_display/readme.md @@ -0,0 +1,55 @@ +# Seven Segment Display + +Usermod that uses the overlay feature to create a configurable seven segment display. +This has only been tested on a single configuration. Colon support is entirely untested. + +## Installation + +Add the compile-time option `-D USERMOD_SEVEN_SEGMENT` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_SEVEN_SEGMENT` in `my_config.h`. + +## Settings +Settings can be controlled through both the usermod setting page and through MQTT with a raw payload. +##### Example + Topic ```/sevenSeg/perSegment/set``` + Payload ```3``` +#### perSegment -- ssLEDPerSegment +The number of individual LEDs per segment. There are 7 segments per digit. +#### perPeriod -- ssLEDPerPeriod +The number of individual LEDs per period. A ':' has 2x periods. +#### startIdx -- ssStartLED +Index of the LED that the display starts at. Allows a seven segment display to be in the middle of a string. +#### timeEnable -- ssTimeEnabled +When true, when displayMask is configured for a time output and no message is set the time will be displayed. +#### scrollSpd -- ssScrollSpeed +Time, in milliseconds, between message shifts when the length of displayMsg exceeds the length of the displayMask. +#### displayMask -- ssDisplayMask +This should represent the configuration of the physical display. +
+HH - 0-23. hh - 1-12, kk - 1-24 hours  
+MM or mm - 0-59 minutes  
+SS or ss = 0-59 seconds  
+: for a colon  
+All others for alpha numeric, (will be blank when displaying time)
+
+##### Example +```HHMMSS ``` +```hh:MM:SS ``` +#### displayMsg -- ssDisplayMessage +Message to be displayed across the display. If the length exceeds the length of the displayMask the message will scroll at scrollSpd. To 'remove' a message or revert back to time, if timeEnabled is true, set the message to '~'. +#### displayCfg -- ssDisplayConfig +The order that your LEDs are configured. All seven segments in the display need to be wired the same way. +
+           -------
+         /   A   /          0 - EDCGFAB
+        / F     / B         1 - EDCBAFG
+       /       /            2 - GCDEFAB
+       -------              3 - GBAFEDC
+     /   G   /              4 - FABGEDC
+    / E     / C             5 - FABCDEG
+   /       /
+   -------
+      D
+
+ +## Version +20211009 - Initial release diff --git a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h b/usermods/seven_segment_display/usermod_v2_seven_segment_display.h new file mode 100644 index 00000000..5c0022e0 --- /dev/null +++ b/usermods/seven_segment_display/usermod_v2_seven_segment_display.h @@ -0,0 +1,497 @@ +#pragma once + +#include "wled.h" + +class SevenSegmentDisplay : public Usermod +{ + +#define WLED_SS_BUFFLEN 6 +#define REFRESHTIME 497 +private: + //Runtime variables. + unsigned long lastRefresh = 0; + unsigned long lastCharacterStep = 0; + String ssDisplayBuffer = ""; + char ssCharacterMask[36] = {0x77, 0x11, 0x6B, 0x3B, 0x1D, 0x3E, 0x7E, 0x13, 0x7F, 0x1F, 0x5F, 0x7C, 0x66, 0x79, 0x6E, 0x4E, 0x76, 0x5D, 0x44, 0x71, 0x5E, 0x64, 0x27, 0x58, 0x77, 0x4F, 0x1F, 0x48, 0x3E, 0x6C, 0x75, 0x25, 0x7D, 0x2A, 0x3D, 0x6B}; + int ssDisplayMessageIdx = 0; //Position of the start of the message to be physically displayed. + bool ssDoDisplayTime = true; + int ssVirtualDisplayMessageIdxStart = 0; + int ssVirtualDisplayMessageIdxEnd = 0; + unsigned long resfreshTime = 497; + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + int ssLEDPerSegment = 1; //The number of LEDs in each segment of the 7 seg (total per digit is 7 * ssLedPerSegment) + int ssLEDPerPeriod = 1; //A Period will have 1x and a Colon will have 2x + int ssStartLED = 0; //The pixel that the display starts at. + /* HH - 0-23. hh - 1-12, kk - 1-24 hours + // MM or mm - 0-59 minutes + // SS or ss = 0-59 seconds + // : for a colon + // All others for alpha numeric, (will be blank when displaying time) + */ + String ssDisplayMask = "HHMMSS"; //Physical Display Mask, this should reflect physical equipment. + /* ssDisplayConfig + // ------- + // / A / 0 - EDCGFAB + // / F / B 1 - EDCBAFG + // / / 2 - GCDEFAB + // ------- 3 - GBAFEDC + // / G / 4 - FABGEDC + // / E / C 5 - FABCDEG + // / / + // ------- + // D + */ + int ssDisplayConfig = 5; //Physical configuration of the Seven segment display + String ssDisplayMessage = "~"; + bool ssTimeEnabled = true; //If not, display message. + unsigned int ssScrollSpeed = 1000; //Time between advancement of extended message scrolling, in milliseconds. + + //String to reduce flash memory usage + static const char _str_perSegment[]; + static const char _str_perPeriod[]; + static const char _str_startIdx[]; + static const char _str_displayCfg[]; + static const char _str_timeEnabled[]; + static const char _str_scrollSpd[]; + static const char _str_displayMask[]; + static const char _str_displayMsg[]; + static const char _str_sevenSeg[]; + static const char _str_subFormat[]; + static const char _str_topicFormat[]; + + unsigned long _overlaySevenSegmentProcess() + { + //Do time for now. + if (ssDoDisplayTime) + { + //Format the ssDisplayBuffer based on ssDisplayMask + int displayMaskLen = static_cast(ssDisplayMask.length()); + for (int index = 0; index < displayMaskLen; index++) + { + //Only look for time formatting if there are at least 2 characters left in the buffer. + if ((index < displayMaskLen - 1) && (ssDisplayMask[index] == ssDisplayMask[index + 1])) + { + int timeVar = 0; + switch (ssDisplayMask[index]) + { + case 'h': + timeVar = hourFormat12(localTime); + break; + case 'H': + timeVar = hour(localTime); + break; + case 'k': + timeVar = hour(localTime) + 1; + break; + case 'M': + case 'm': + timeVar = minute(localTime); + break; + case 'S': + case 's': + timeVar = second(localTime); + break; + } + + //Only want to leave a blank in the hour formatting. + if ((ssDisplayMask[index] == 'h' || ssDisplayMask[index] == 'H' || ssDisplayMask[index] == 'k') && timeVar < 10) + ssDisplayBuffer[index] = ' '; + else + ssDisplayBuffer[index] = 0x30 + (timeVar / 10); + ssDisplayBuffer[index + 1] = 0x30 + (timeVar % 10); + + //Need to increment the index because of the second digit. + index++; + } + else + { + ssDisplayBuffer[index] = (ssDisplayMask[index] == ':' ? ':' : ' '); + } + } + return REFRESHTIME; + } + else + { + /* This will handle displaying a message and the scrolling of the message if its longer than the buffer length */ + + //Check to see if the message has scrolled completely + int len = static_cast(ssDisplayMessage.length()); + if (ssDisplayMessageIdx > len) + { + //If it has scrolled the whole message, reset it. + setSevenSegmentMessage(ssDisplayMessage); + return REFRESHTIME; + } + //Display message + int displayMaskLen = static_cast(ssDisplayMask.length()); + for (int index = 0; index < displayMaskLen; index++) + { + if (ssDisplayMessageIdx + index < len && ssDisplayMessageIdx + index >= 0) + ssDisplayBuffer[index] = ssDisplayMessage[ssDisplayMessageIdx + index]; + else + ssDisplayBuffer[index] = ' '; + } + + //Increase the displayed message index to progress it one character if the length exceeds the display length. + if (len > displayMaskLen) + ssDisplayMessageIdx++; + + return ssScrollSpeed; + } + } + + void _overlaySevenSegmentDraw() + { + + //Start pixels at ssStartLED, Use ssLEDPerSegment, ssLEDPerPeriod, ssDisplayBuffer + int indexLED = ssStartLED; + int displayMaskLen = static_cast(ssDisplayMask.length()); + for (int indexBuffer = 0; indexBuffer < displayMaskLen; indexBuffer++) + { + if (ssDisplayBuffer[indexBuffer] == 0) + break; + else if (ssDisplayBuffer[indexBuffer] == '.') + { + //Won't ever turn off LED lights for a period. (or will we?) + indexLED += ssLEDPerPeriod; + continue; + } + else if (ssDisplayBuffer[indexBuffer] == ':') + { + //Turn off colon if odd second? + indexLED += ssLEDPerPeriod * 2; + } + else if (ssDisplayBuffer[indexBuffer] == ' ') + { + //Turn off all 7 segments. + _overlaySevenSegmentLEDOutput(0, indexLED); + indexLED += ssLEDPerSegment * 7; + } + else + { + //Turn off correct segments. + _overlaySevenSegmentLEDOutput(_overlaySevenSegmentGetCharMask(ssDisplayBuffer[indexBuffer]), indexLED); + indexLED += ssLEDPerSegment * 7; + } + } + } + + void _overlaySevenSegmentLEDOutput(char mask, int indexLED) + { + for (char index = 0; index < 7; index++) + { + if ((mask & (0x40 >> index)) != (0x40 >> index)) + { + for (int numPerSeg = 0; numPerSeg < ssLEDPerSegment; numPerSeg++) + { + strip.setPixelColor(indexLED + numPerSeg, 0x000000); + } + } + indexLED += ssLEDPerSegment; + } + } + + char _overlaySevenSegmentGetCharMask(char var) + { + if (var >= 0x30 && var <= 0x39) + { /*If its a number, shift to index 0.*/ + var -= 0x30; + } + else if (var >= 0x41 && var <= 0x5a) + { /*If its an Upper case, shift to index 0xA.*/ + var -= 0x37; + } + else if (var >= 0x61 && var <= 0x7A) + { /*If its a lower case, shift to index 0xA.*/ + var -= 0x57; + } + else + { /* Else unsupported, return 0; */ + return 0; + } + char mask = ssCharacterMask[static_cast(var)]; + /* + 0 - EDCGFAB + 1 - EDCBAFG + 2 - GCDEFAB + 3 - GBAFEDC + 4 - FABGEDC + 5 - FABCDEG + */ + switch (ssDisplayConfig) + { + case 1: + mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1); + mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1); + break; + case 2: + mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1); + mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1); + break; + case 3: + mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); + mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1); + mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1); + break; + case 4: + mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); + break; + case 5: + mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); + mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1); + mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1); + break; + } + return mask; + } + + char _overlaySevenSegmentSwapBits(char x, char p1, char p2, char n) + { + /* Move all bits of first set to rightmost side */ + char set1 = (x >> p1) & ((1U << n) - 1); + + /* Move all bits of second set to rightmost side */ + char set2 = (x >> p2) & ((1U << n) - 1); + + /* Xor the two sets */ + char Xor = (set1 ^ set2); + + /* Put the Xor bits back to their original positions */ + Xor = (Xor << p1) | (Xor << p2); + + /* Xor the 'Xor' with the original number so that the + two sets are swapped */ + char result = x ^ Xor; + + return result; + } + + void _publishMQTTint_P(const char *subTopic, int value) + { + if(mqtt == NULL) return; + + char buffer[64]; + char valBuffer[12]; + sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_sevenSeg, subTopic); + sprintf_P(valBuffer, PSTR("%d"), value); + mqtt->publish(buffer, 2, true, valBuffer); + } + + void _publishMQTTstr_P(const char *subTopic, String Value) + { + if(mqtt == NULL) return; + char buffer[64]; + sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_sevenSeg, subTopic); + mqtt->publish(buffer, 2, true, Value.c_str(), Value.length()); + } + + void _updateMQTT() + { + _publishMQTTint_P(_str_perSegment, ssLEDPerSegment); + _publishMQTTint_P(_str_perPeriod, ssLEDPerPeriod); + _publishMQTTint_P(_str_startIdx, ssStartLED); + _publishMQTTint_P(_str_displayCfg, ssDisplayConfig); + _publishMQTTint_P(_str_timeEnabled, ssTimeEnabled); + _publishMQTTint_P(_str_scrollSpd, ssScrollSpeed); + + _publishMQTTstr_P(_str_displayMask, ssDisplayMask); + _publishMQTTstr_P(_str_displayMsg, ssDisplayMessage); + } + + bool _cmpIntSetting_P(char *topic, char *payload, const char *setting, void *value) + { + if (strcmp_P(topic, setting) == 0) + { + *((int *)value) = strtol(payload, NULL, 10); + _publishMQTTint_P(setting, *((int *)value)); + return true; + } + return false; + } + + bool _handleSetting(char *topic, char *payload) + { + if (_cmpIntSetting_P(topic, payload, _str_perSegment, &ssLEDPerSegment)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_perPeriod, &ssLEDPerPeriod)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_startIdx, &ssStartLED)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_displayCfg, &ssDisplayConfig)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_timeEnabled, &ssTimeEnabled)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_scrollSpd, &ssScrollSpeed)) + return true; + if (strcmp_P(topic, _str_displayMask) == 0) + { + ssDisplayMask = String(payload); + ssDisplayBuffer = ssDisplayMask; + _publishMQTTstr_P(_str_displayMask, ssDisplayMask); + return true; + } + if (strcmp_P(topic, _str_displayMsg) == 0) + { + setSevenSegmentMessage(String(payload)); + return true; + } + return false; + } + +public: + void setSevenSegmentMessage(String message) + { + //If the message isn't blank display it otherwise show time, if enabled. + if (message.length() < 1 || message == "~") + ssDoDisplayTime = ssTimeEnabled; + else + ssDoDisplayTime = false; + + //Determine is the message is longer than the display, if it is configure it to scroll the message. + if (message.length() > ssDisplayMask.length()) + ssDisplayMessageIdx = -ssDisplayMask.length(); + else + ssDisplayMessageIdx = 0; + + //If the message isn't the same, update runtime/mqtt (most calls will be resetting message scroll) + if (!ssDisplayMessage.equals(message)) + { + _publishMQTTstr_P(_str_displayMsg, message); + ssDisplayMessage = message; + } + } + //Functions called by WLED + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() + { + ssDisplayBuffer = ssDisplayMask; + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() + { + if (millis() - lastRefresh > resfreshTime) + { + //In theory overlaySevenSegmentProcess should return the amount of time until it changes next. + //So we should be okay to trigger the stripi on every process loop. + resfreshTime = _overlaySevenSegmentProcess(); + lastRefresh = millis(); + strip.trigger(); + } + } + + void handleOverlayDraw() + { + _overlaySevenSegmentDraw(); + } + + void onMqttConnect(bool sessionPresent) + { + char subBuffer[48]; + if (mqttDeviceTopic[0] != 0) + { + _updateMQTT(); + //subscribe for sevenseg messages on the device topic + sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttDeviceTopic, _str_sevenSeg); + mqtt->subscribe(subBuffer, 2); + } + + if (mqttGroupTopic[0] != 0) + { + //subcribe for sevenseg messages on the group topic + sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttGroupTopic, _str_sevenSeg); + mqtt->subscribe(subBuffer, 2); + } + } + + bool onMqttMessage(char *topic, char *payload) + { + //If topic beings iwth sevenSeg cut it off, otherwise not our message. + size_t topicPrefixLen = strlen_P(PSTR("/sevenSeg/")); + if (strncmp_P(topic, PSTR("/sevenSeg/"), topicPrefixLen) == 0) + topic += topicPrefixLen; + else + return false; + //We only care if the topic ends with /set + size_t topicLen = strlen(topic); + if (topicLen > 4 && + topic[topicLen - 4] == '/' && + topic[topicLen - 3] == 's' && + topic[topicLen - 2] == 'e' && + topic[topicLen - 1] == 't') + { + //Trim /set and handle it + topic[topicLen - 4] = '\0'; + _handleSetting(topic, payload); + } + return true; + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root[FPSTR(_str_sevenSeg)]; + if (top.isNull()) + { + top = root.createNestedObject(FPSTR(_str_sevenSeg)); + } + top[FPSTR(_str_perSegment)] = ssLEDPerSegment; + top[FPSTR(_str_perPeriod)] = ssLEDPerPeriod; + top[FPSTR(_str_startIdx)] = ssStartLED; + top[FPSTR(_str_displayMask)] = ssDisplayMask; + top[FPSTR(_str_displayCfg)] = ssDisplayConfig; + top[FPSTR(_str_displayMsg)] = ssDisplayMessage; + top[FPSTR(_str_timeEnabled)] = ssTimeEnabled; + top[FPSTR(_str_scrollSpd)] = ssScrollSpeed; + } + + bool readFromConfig(JsonObject &root) + { + JsonObject top = root[FPSTR(_str_sevenSeg)]; + + bool configComplete = !top.isNull(); + + //if sevenseg section doesn't exist return + if (!configComplete) + return configComplete; + + configComplete &= getJsonValue(top[FPSTR(_str_perSegment)], ssLEDPerSegment); + configComplete &= getJsonValue(top[FPSTR(_str_perPeriod)], ssLEDPerPeriod); + configComplete &= getJsonValue(top[FPSTR(_str_startIdx)], ssStartLED); + configComplete &= getJsonValue(top[FPSTR(_str_displayMask)], ssDisplayMask); + configComplete &= getJsonValue(top[FPSTR(_str_displayCfg)], ssDisplayConfig); + + String newDisplayMessage; + configComplete &= getJsonValue(top[FPSTR(_str_displayMsg)], newDisplayMessage); + setSevenSegmentMessage(newDisplayMessage); + + configComplete &= getJsonValue(top[FPSTR(_str_timeEnabled)], ssTimeEnabled); + configComplete &= getJsonValue(top[FPSTR(_str_scrollSpd)], ssScrollSpeed); + return configComplete; + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_SEVEN_SEGMENT_DISPLAY; + } +}; + +const char SevenSegmentDisplay::_str_perSegment[] PROGMEM = "perSegment"; +const char SevenSegmentDisplay::_str_perPeriod[] PROGMEM = "perPeriod"; +const char SevenSegmentDisplay::_str_startIdx[] PROGMEM = "startIdx"; +const char SevenSegmentDisplay::_str_displayCfg[] PROGMEM = "displayCfg"; +const char SevenSegmentDisplay::_str_timeEnabled[] PROGMEM = "timeEnabled"; +const char SevenSegmentDisplay::_str_scrollSpd[] PROGMEM = "scrollSpd"; +const char SevenSegmentDisplay::_str_displayMask[] PROGMEM = "displayMask"; +const char SevenSegmentDisplay::_str_displayMsg[] PROGMEM = "displayMsg"; +const char SevenSegmentDisplay::_str_sevenSeg[] PROGMEM = "sevenSeg"; \ No newline at end of file diff --git a/wled00/const.h b/wled00/const.h index a4388104..50780082 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -61,6 +61,7 @@ #define USERMOD_ID_BATTERY_STATUS_BASIC 18 //Usermod "usermod_v2_battery_status_basic.h" #define USERMOD_ID_PWM_FAN 19 //Usermod "usermod_PWM_fan.h" #define USERMOD_ID_BH1750 20 //Usermod "usermod_bh1750.h" +#define USERMOD_ID_SEVEN_SEGMENT_DISPLAY 21 //Usermod "usermod_v2_seven_segment_display.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 62f40a50..a796848c 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -217,6 +217,7 @@ void WiFiEvent(WiFiEvent_t event); class Usermod { public: virtual void loop() {} + virtual void handleOverlayDraw() {} virtual void setup() {} virtual void connected() {} virtual void addToJsonState(JsonObject& obj) {} @@ -236,6 +237,8 @@ class UsermodManager { public: void loop(); + void handleOverlayDraw(); + void setup(); void connected(); void addToJsonState(JsonObject& obj); diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index 98339686..2c631495 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -18,6 +18,7 @@ void initCronixie() } +//handleOverlays is essentially the equivalent of usermods.loop void handleOverlays() { initCronixie(); @@ -111,8 +112,8 @@ void _overlayAnalogCountdown() } } - void handleOverlayDraw() { + usermods.handleOverlayDraw(); if (!overlayCurrent) return; switch (overlayCurrent) { @@ -121,7 +122,6 @@ void handleOverlayDraw() { } } - /* * Support for the Cronixie clock */ diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 040a8450..a9aac784 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -4,7 +4,8 @@ */ //Usermod Manager internals -void UsermodManager::loop() { for (byte i = 0; i < numMods; i++) ums[i]->loop(); } +void UsermodManager::loop() { for (byte i = 0; i < numMods; i++) ums[i]->loop(); } +void UsermodManager::handleOverlayDraw() { for (byte i = 0; i < numMods; i++) ums[i]->handleOverlayDraw(); } void UsermodManager::setup() { for (byte i = 0; i < numMods; i++) ums[i]->setup(); } void UsermodManager::connected() { for (byte i = 0; i < numMods; i++) ums[i]->connected(); } diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 8cc63328..72807165 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -100,6 +100,10 @@ #include "../usermods/ST7789_display/ST7789_Display.h" #endif +#ifdef USERMOD_SEVEN_SEGMENT +#include "../usermods/seven_segment_display/usermod_v2_seven_segment_display.h" +#endif + void registerUsermods() { /* @@ -187,4 +191,8 @@ void registerUsermods() #ifdef USERMOD_ST7789_DISPLAY usermods.add(new St7789DisplayUsermod()); #endif + + #ifdef USERMOD_SEVEN_SEGMENT + usermods.add(new SevenSegmentDisplay()); + #endif }