Merge branch 'master' into master-merge

This commit is contained in:
Blaž Kristan 2022-03-07 09:44:35 +01:00
commit e561304645
33 changed files with 3886 additions and 3453 deletions

View File

@ -2,6 +2,22 @@
### Builds after release 0.12.0
#### Build 2203060
- Dynamic hiding of unused color controls in UI (PR #2567)
- Removed native Cronixie support and added Cronixie usermod
- Fixed disabled timed preset expanding calendar
- Fixed Color Order setting shown for analog busses
- Fixed incorrect operator (#2566)
#### Build 2203011
- IR rewrite (PR #2561), supports CCT
- Added locate button to Time settings
- CSS fixes and adjustments
- Consistent Tab indentation in index JS and CSS
- Added initial contribution style guideline
#### Build 2202222
- Version bump to 0.13.0-b7 "Toki"

78
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,78 @@
## Thank you for making WLED better!
Here are a few suggestions to make it easier for you to contribute!
### Code style
When in doubt, it is easiest to replicate the code style you find in the files you want to edit :)
Below are the guidelines we use in the WLED repository.
#### Indentation
We use tabs for Indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files.
You are all set if you have enabled `Editor: Detect Indentation` in VS Code.
#### Blocks
Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block braches is acceptable.
Good:
```cpp
if (a == b) {
doStuff(a);
}
```
```cpp
if (a == b)
{
doStuff(a);
}
```
```cpp
if (a == b) doStuff(a);
```
There should always be a space between a keyword and its condition and between the condition and brace.
Within the condition, no space should be between the paranthesis and variables.
Spaces between variables and operators are up to the authors discretion.
There should be no space between function names and their argument parenthesis.
Good:
```cpp
if (a == b) {
doStuff(a);
}
```
Not good:
```cpp
if( a==b ){
doStuff ( a);
}
```
#### Comments
Comments should have a space between the delimiting characters (e.g. `//`) and the comment text.
Note: This is a recent change, the majority of the codebase still has comments without spaces.
Good:
```
// This is a comment.
/* This is a CSS inline comment */
/*
* This is a comment
* wrapping over multiple lines,
* used in WLED for file headers and function explanations
*/
<!-- This is an HTML comment -->
```
There is no set character limit for a comment within a line,
though as a rule of thumb you should wrap your comment if it exceeds the width of your editor window.
Inline comments are OK if they describe that line only and are not exceedingly wide.

View File

@ -337,10 +337,10 @@ build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D
lib_deps = ${esp32.lib_deps}
board_build.partitions = ${esp32.default_partitions}
# ESP32 ETH build that fits in old 1M app space (disables Blynk, Cronixie, and Hue sync)
# ESP32 ETH build that fits in old 1M app space (disables Blynk and Hue sync)
[env:esp32_eth_ota1mapp]
extends = env:esp32_eth
build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet_OTA -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D WLED_DISABLE_BLYNK -D WLED_DISABLE_CRONIXIE -D WLED_DISABLE_HUESYNC
build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet_OTA -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D WLED_DISABLE_BLYNK -D WLED_DISABLE_HUESYNC
[env:esp32s2_saola]
board = esp32-s2-saola-1
@ -536,7 +536,7 @@ build_flags = ${common.build_flags_esp32} -D WLED_DISABLE_BROWNOUT_DET -D WLED_D
-D LEDPIN=12
-D RLYPIN=27
-D BTNPIN=34
-D WLED_DISABLE_INFRARED
-D WLED_DISABLE_BLYNK
-D DEFAULT_LED_COUNT=6
# Display config
-D ST7789_DRIVER

View File

@ -22,7 +22,6 @@ build_flags = ${common.build_flags_esp8266}
; -D WLED_DISABLE_OTA
; -D WLED_DISABLE_ALEXA
; -D WLED_DISABLE_BLYNK
; -D WLED_DISABLE_CRONIXIE
; -D WLED_DISABLE_HUESYNC
; -D WLED_DISABLE_INFRARED
; -D WLED_DISABLE_WEBSOCKETS

View File

@ -27,7 +27,7 @@ A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control
- Presets can be used to automatically execute API calls
- Nightlight function (gradually dims down)
- Full OTA software updatability (HTTP + ArduinoOTA), password protectable
- Configurable analog clock + support for the Cronixie kit by Diamex
- Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods)
- Configurable Auto Brightness limit for safer operation
- Filesystem-based config for easier backup of presets and settings

View File

@ -0,0 +1,8 @@
# Cronixie clock usermod
This usermod supports driving the Cronixie M and L clock kits by Diamex.
## Installation
Compile and upload after adding `-D USERMOD_CRONIXIE` to `build_flags` of your PlatformIO environment.
Make sure the Auto Brightness Limiter is enabled at 420mA (!) and configure 60 WS281x LEDs.

View File

@ -0,0 +1,301 @@
#pragma once
#include "wled.h"
class UsermodCronixie : public Usermod {
private:
unsigned long lastTime = 0;
char cronixieDisplay[7] = "HHMMSS";
byte _digitOut[6] = {10,10,10,10,10,10};
byte dP[6] = {255, 255, 255, 255, 255, 255};
// set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer)
bool backlight = true;
public:
void initCronixie()
{
if (dP[0] == 255) // if dP[0] is 255, cronixie is not yet init'ed
{
setCronixie();
strip.getSegment(0).grouping = 10; // 10 LEDs per digit
}
}
void setup() {
}
void loop() {
if (!toki.isTick()) return;
initCronixie();
_overlayCronixie();
strip.trigger();
}
byte getSameCodeLength(char code, int index, char const cronixieDisplay[])
{
byte counter = 0;
for (int i = index+1; i < 6; i++)
{
if (cronixieDisplay[i] == code)
{
counter++;
} else {
return counter;
}
}
return counter;
}
void setCronixie()
{
/*
* digit purpose index
* 0-9 | 0-9 (incl. random)
* 10 | blank
* 11 | blank, bg off
* 12 | test upw.
* 13 | test dnw.
* 14 | binary AM/PM
* 15 | BB upper +50 for no trailing 0
* 16 | BBB
* 17 | BBBB
* 18 | BBBBB
* 19 | BBBBBB
* 20 | H
* 21 | HH
* 22 | HHH
* 23 | HHHH
* 24 | M
* 25 | MM
* 26 | MMM
* 27 | MMMM
* 28 | MMMMM
* 29 | MMMMMM
* 30 | S
* 31 | SS
* 32 | SSS
* 33 | SSSS
* 34 | SSSSS
* 35 | SSSSSS
* 36 | Y
* 37 | YY
* 38 | YYYY
* 39 | I
* 40 | II
* 41 | W
* 42 | WW
* 43 | D
* 44 | DD
* 45 | DDD
* 46 | V
* 47 | VV
* 48 | VVV
* 49 | VVVV
* 50 | VVVVV
* 51 | VVVVVV
* 52 | v
* 53 | vv
* 54 | vvv
* 55 | vvvv
* 56 | vvvvv
* 57 | vvvvvv
*/
//H HourLower | HH - Hour 24. | AH - Hour 12. | HHH Hour of Month | HHHH Hour of Year
//M MinuteUpper | MM Minute of Hour | MMM Minute of 12h | MMMM Minute of Day | MMMMM Minute of Month | MMMMMM Minute of Year
//S SecondUpper | SS Second of Minute | SSS Second of 10 Minute | SSSS Second of Hour | SSSSS Second of Day | SSSSSS Second of Week
//B AM/PM | BB 0-6/6-12/12-18/18-24 | BBB 0-3... | BBBB 0-1.5... | BBBBB 0-1 | BBBBBB 0-0.5
//Y YearLower | YY - Year LU | YYYY - Std.
//I MonthLower | II - Month of Year
//W Week of Month | WW Week of Year
//D Day of Week | DD Day Of Month | DDD Day Of Year
DEBUG_PRINT("cset ");
DEBUG_PRINTLN(cronixieDisplay);
for (int i = 0; i < 6; i++)
{
dP[i] = 10;
switch (cronixieDisplay[i])
{
case '_': dP[i] = 10; break;
case '-': dP[i] = 11; break;
case 'r': dP[i] = random(1,7); break; //random btw. 1-6
case 'R': dP[i] = random(0,10); break; //random btw. 0-9
//case 't': break; //Test upw.
//case 'T': break; //Test dnw.
case 'b': dP[i] = 14 + getSameCodeLength('b',i,cronixieDisplay); i = i+dP[i]-14; break;
case 'B': dP[i] = 14 + getSameCodeLength('B',i,cronixieDisplay); i = i+dP[i]-14; break;
case 'h': dP[i] = 70 + getSameCodeLength('h',i,cronixieDisplay); i = i+dP[i]-70; break;
case 'H': dP[i] = 20 + getSameCodeLength('H',i,cronixieDisplay); i = i+dP[i]-20; break;
case 'A': dP[i] = 108; i++; break;
case 'a': dP[i] = 58; i++; break;
case 'm': dP[i] = 74 + getSameCodeLength('m',i,cronixieDisplay); i = i+dP[i]-74; break;
case 'M': dP[i] = 24 + getSameCodeLength('M',i,cronixieDisplay); i = i+dP[i]-24; break;
case 's': dP[i] = 80 + getSameCodeLength('s',i,cronixieDisplay); i = i+dP[i]-80; break; //refresh more often bc. of secs
case 'S': dP[i] = 30 + getSameCodeLength('S',i,cronixieDisplay); i = i+dP[i]-30; break;
case 'Y': dP[i] = 36 + getSameCodeLength('Y',i,cronixieDisplay); i = i+dP[i]-36; break;
case 'y': dP[i] = 86 + getSameCodeLength('y',i,cronixieDisplay); i = i+dP[i]-86; break;
case 'I': dP[i] = 39 + getSameCodeLength('I',i,cronixieDisplay); i = i+dP[i]-39; break; //Month. Don't ask me why month and minute both start with M.
case 'i': dP[i] = 89 + getSameCodeLength('i',i,cronixieDisplay); i = i+dP[i]-89; break;
//case 'W': break;
//case 'w': break;
case 'D': dP[i] = 43 + getSameCodeLength('D',i,cronixieDisplay); i = i+dP[i]-43; break;
case 'd': dP[i] = 93 + getSameCodeLength('d',i,cronixieDisplay); i = i+dP[i]-93; break;
case '0': dP[i] = 0; break;
case '1': dP[i] = 1; break;
case '2': dP[i] = 2; break;
case '3': dP[i] = 3; break;
case '4': dP[i] = 4; break;
case '5': dP[i] = 5; break;
case '6': dP[i] = 6; break;
case '7': dP[i] = 7; break;
case '8': dP[i] = 8; break;
case '9': dP[i] = 9; break;
//case 'V': break; //user var0
//case 'v': break; //user var1
}
}
DEBUG_PRINT("result ");
for (int i = 0; i < 5; i++)
{
DEBUG_PRINT((int)dP[i]);
DEBUG_PRINT(" ");
}
DEBUG_PRINTLN((int)dP[5]);
_overlayCronixie(); // refresh
}
void _overlayCronixie()
{
byte h = hour(localTime);
byte h0 = h;
byte m = minute(localTime);
byte s = second(localTime);
byte d = day(localTime);
byte mi = month(localTime);
int y = year(localTime);
//this has to be changed in time for 22nd century
y -= 2000; if (y<0) y += 30; //makes countdown work
if (useAMPM && !countdownMode)
{
if (h>12) h-=12;
else if (h==0) h+=12;
}
for (int i = 0; i < 6; i++)
{
if (dP[i] < 12) _digitOut[i] = dP[i];
else {
if (dP[i] < 65)
{
switch(dP[i])
{
case 21: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; i++; break; //HH
case 25: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; i++; break; //MM
case 31: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; i++; break; //SS
case 20: _digitOut[i] = h- (h/10)*10; break; //H
case 24: _digitOut[i] = m/10; break; //M
case 30: _digitOut[i] = s/10; break; //S
case 43: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //D
case 44: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; i++; break; //DD
case 40: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; i++; break; //II
case 37: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //YY
case 39: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //YYYY
//case 16: _digitOut[i+2] = ((h0/3)&1)?1:0; i++; //BBB (BBBB NI)
//case 15: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:0; i++; //BB
case 14: _digitOut[i] = (h0>11)?1:0; break; //B
}
} else
{
switch(dP[i])
{
case 71: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //hh
case 75: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //mm
case 81: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ss
//case 66: _digitOut[i+2] = ((h0/3)&1)?1:10; i++; //bbb (bbbb NI)
//case 65: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:10; i++; //bb
case 64: _digitOut[i] = (h0>11)?1:10; break; //b
case 93: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //d
case 94: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //dd
case 90: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ii
case 87: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //yy
case 89: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //yyyy
}
}
}
}
}
void handleOverlayDraw()
{
byte offsets[] = {5, 0, 6, 1, 7, 2, 8, 3, 9, 4};
for (uint16_t i = 0; i < 6; i++)
{
byte o = 10*i;
byte excl = 10;
if(_digitOut[i] < 10) excl = offsets[_digitOut[i]];
excl += o;
if (backlight && _digitOut[i] <11)
{
uint32_t col = strip.gamma32(strip.getSegment(0).colors[1]);
for (uint16_t j=o; j< o+10; j++) {
if (j != excl) strip.setPixelColor(j, col);
}
} else
{
for (uint16_t j=o; j< o+10; j++) {
if (j != excl) strip.setPixelColor(j, 0);
}
}
}
}
void addToJsonState(JsonObject& root)
{
root["nx"] = cronixieDisplay;
}
void readFromJsonState(JsonObject& root)
{
if (root["nx"].is<const char*>()) {
strncpy(cronixieDisplay, root["nx"], 6);
}
}
void addToConfig(JsonObject& root)
{
JsonObject top = root.createNestedObject(F("Cronixie"));
top["backlight"] = backlight;
}
bool readFromConfig(JsonObject& root)
{
// default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor
// setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed)
JsonObject top = root[F("Cronixie")];
bool configComplete = !top.isNull();
configComplete &= getJsonValue(top["backlight"], backlight);
return configComplete;
}
uint16_t getId()
{
return USERMOD_ID_CRONIXIE;
}
};

View File

@ -12,6 +12,7 @@ class TFTs : public TFT_eSPI {
private:
uint8_t digits[NUM_DIGITS];
// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.
@ -33,7 +34,16 @@ private:
}
uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH];
int16_t w = 135, h = 240, x = 0, y = 0, bufferedDigit = 255;
uint16_t digitR, digitG, digitB, dimming = 255;
uint32_t digitColor = 0;
void drawBuffer() {
bool oldSwapBytes = getSwapBytes();
setSwapBytes(true);
pushImage(x, y, w, h, (uint16_t *)output_buffer);
setSwapBytes(oldSwapBytes);
}
// These BMP functions are stolen directly from the TFT_SPIFFS_BMP example in the TFT_eSPI library.
// Unfortunately, they aren't part of the library itself, so I had to copy them.
@ -41,44 +51,69 @@ private:
//// BEGIN STOLEN CODE
// Draw directly from file stored in RGB565 format
// Draw directly from file stored in RGB565 format. Fastest
bool drawBin(const char *filename) {
fs::File bmpFS;
// Open requested file on SD card
bmpFS = WLED_FS.open(filename, "r");
if (!bmpFS)
{
Serial.print(F("File not found: "));
Serial.println(filename);
return(false);
size_t sz = bmpFS.size();
if (sz > 64800) {
bmpFS.close();
return false;
}
size_t sz = bmpFS.size();
if (sz <= 64800)
{
bool oldSwapBytes = getSwapBytes();
setSwapBytes(true);
int16_t h = sz / (135 * 2);
uint16_t r, g, b, dimming = 255;
int16_t row, col;
//draw img that is shorter than 240pix into the center
int16_t y = (height() - h) /2;
w = 135;
h = sz / (w * 2);
x = 0;
y = (height() - h) /2;
bmpFS.read((uint8_t *) output_buffer,sz);
uint8_t lineBuffer[w * 2];
if (!realtimeMode || realtimeOverride) strip.service();
pushImage(0, y, 135, h, (uint16_t *)output_buffer);
// 0,0 coordinates are top left
for (row = 0; row < h; row++) {
setSwapBytes(oldSwapBytes);
bmpFS.read(lineBuffer, sizeof(lineBuffer));
uint8_t PixM, PixL;
// Colors are already in 16-bit R5, G6, B5 format
for (col = 0; col < w; col++)
{
if (dimming == 255 && !digitColor) { // not needed, copy directly
output_buffer[row][col] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]);
} else {
// 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB
PixM = lineBuffer[col*2+1];
PixL = lineBuffer[col*2];
// align to 8-bit value (MSB left aligned)
r = (PixM) & 0xF8;
g = ((PixM << 5) | (PixL >> 3)) & 0xFC;
b = (PixL << 3) & 0xF8;
r *= dimming; g *= dimming; b *= dimming;
r = r >> 8; g = g >> 8; b = b >> 8;
if (digitColor) { // grayscale pixel coloring
uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b);
r = g = b = l;
r *= digitR; g *= digitG; b *= digitB;
r = r >> 8; g = g >> 8; b = b >> 8;
}
output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
}
}
drawBuffer();
bmpFS.close();
return(true);
return true;
}
bool drawBmp(const char *filename) {
@ -87,53 +122,52 @@ private:
// Open requested file on SD card
bmpFS = WLED_FS.open(filename, "r");
if (!bmpFS)
{
Serial.print(F("File not found: "));
Serial.println(filename);
return(false);
}
uint32_t seekOffset;
int16_t w, h, row;
uint8_t r, g, b;
uint32_t seekOffset, headerSize, paletteSize = 0;
int16_t row;
uint16_t r, g, b, dimming = 255, bitDepth;
uint16_t magic = read16(bmpFS);
if (magic == 0xFFFF) {
if (magic != ('B' | ('M' << 8))) { // File not found or not a BMP
Serial.println(F("BMP not found!"));
bmpFS.close();
return(false);
return false;
}
if (magic != 0x4D42) {
Serial.print(F("File not a BMP. Magic: "));
Serial.println(magic);
bmpFS.close();
return(false);
}
read32(bmpFS); // filesize in bytes
read32(bmpFS); // reserved
seekOffset = read32(bmpFS); // start of bitmap
headerSize = read32(bmpFS); // header size
w = read32(bmpFS); // width
h = read32(bmpFS); // height
read16(bmpFS); // color planes (must be 1)
bitDepth = read16(bmpFS);
read32(bmpFS);
read32(bmpFS);
seekOffset = read32(bmpFS);
read32(bmpFS);
w = read32(bmpFS);
h = read32(bmpFS);
if ((read16(bmpFS) != 1) || (read16(bmpFS) != 24) || (read32(bmpFS) != 0)) {
if (read32(bmpFS) != 0 || (bitDepth != 24 && bitDepth != 1 && bitDepth != 4 && bitDepth != 8)) {
Serial.println(F("BMP format not recognized."));
bmpFS.close();
return(false);
return false;
}
//draw img that is shorter than 240pix into the center
int16_t y = (height() - h) /2;
uint32_t palette[256];
if (bitDepth <= 8) // 1,4,8 bit bitmap: read color palette
{
read32(bmpFS); read32(bmpFS); read32(bmpFS); // size, w resolution, h resolution
paletteSize = read32(bmpFS);
if (paletteSize == 0) paletteSize = bitDepth * bitDepth; //if 0, size is 2^bitDepth
bmpFS.seek(14 + headerSize); // start of color palette
for (uint16_t i = 0; i < paletteSize; i++) {
palette[i] = read32(bmpFS);
}
}
// draw img that is shorter than 240pix into the center
x = (width() - w) /2;
y = (height() - h) /2;
bool oldSwapBytes = getSwapBytes();
setSwapBytes(true);
bmpFS.seek(seekOffset);
uint16_t padding = (4 - ((w * 3) & 3)) & 3;
uint8_t lineBuffer[w * 3 + padding];
uint32_t lineSize = ((bitDepth * w +31) >> 5) * 4;
uint8_t lineBuffer[lineSize];
uint8_t serviceStrip = (!realtimeMode || realtimeOverride) ? 7 : 0;
// row is decremented as the BMP image is drawn bottom up
@ -142,23 +176,121 @@ private:
bmpFS.read(lineBuffer, sizeof(lineBuffer));
uint8_t* bptr = lineBuffer;
// Convert 24 to 16 bit colours while copying to output buffer.
// Convert 24 to 16 bit colors while copying to output buffer.
for (uint16_t col = 0; col < w; col++)
{
if (bitDepth == 24) {
b = *bptr++;
g = *bptr++;
r = *bptr++;
output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
} else {
uint32_t c = 0;
if (bitDepth == 8) {
c = palette[*bptr++];
}
else if (bitDepth == 4) {
c = palette[(*bptr >> ((col & 0x01)?0:4)) & 0x0F];
if (col & 0x01) bptr++;
}
else { // bitDepth == 1
c = palette[(*bptr >> (7 - (col & 0x07))) & 0x01];
if ((col & 0x07) == 0x07) bptr++;
}
b = c; g = c >> 8; r = c >> 16;
}
if (dimming != 255) { // only dimm when needed
r *= dimming; g *= dimming; b *= dimming;
r = r >> 8; g = g >> 8; b = b >> 8;
}
if (digitColor) { // grayscale pixel coloring
uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b);
r = g = b = l;
r *= digitR; g *= digitG; b *= digitB;
r = r >> 8; g = g >> 8; b = b >> 8;
}
output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xFF) >> 3);
}
}
pushImage(0, y, w, h, (uint16_t *)output_buffer);
setSwapBytes(oldSwapBytes);
drawBuffer();
bmpFS.close();
return(true);
return true;
}
bool drawClk(const char *filename) {
fs::File bmpFS;
// Open requested file on SD card
bmpFS = WLED_FS.open(filename, "r");
if (!bmpFS)
{
Serial.print("File not found: ");
Serial.println(filename);
return false;
}
uint16_t r, g, b, dimming = 255, magic;
int16_t row, col;
magic = read16(bmpFS);
if (magic != 0x4B43) { // look for "CK" header
Serial.print(F("File not a CLK. Magic: "));
Serial.println(magic);
bmpFS.close();
return false;
}
w = read16(bmpFS);
h = read16(bmpFS);
x = (width() - w) / 2;
y = (height() - h) / 2;
uint8_t lineBuffer[w * 2];
if (!realtimeMode || realtimeOverride) strip.service();
// 0,0 coordinates are top left
for (row = 0; row < h; row++) {
bmpFS.read(lineBuffer, sizeof(lineBuffer));
uint8_t PixM, PixL;
// Colors are already in 16-bit R5, G6, B5 format
for (col = 0; col < w; col++)
{
if (dimming == 255 && !digitColor) { // not needed, copy directly
output_buffer[row][col+x] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]);
} else {
// 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB
PixM = lineBuffer[col*2+1];
PixL = lineBuffer[col*2];
// align to 8-bit value (MSB left aligned)
r = (PixM) & 0xF8;
g = ((PixM << 5) | (PixL >> 3)) & 0xFC;
b = (PixL << 3) & 0xF8;
r *= dimming; g *= dimming; b *= dimming;
r = r >> 8; g = g >> 8; b = b >> 8;
if (digitColor) { // grayscale pixel coloring
uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b);
r = g = b = l;
r *= digitR; g *= digitG; b *= digitB;
r = r >> 8; g = g >> 8; b = b >> 8;
}
output_buffer[row][col+x] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
}
}
drawBuffer();
bmpFS.close();
return true;
}
public:
TFTs() : TFT_eSPI(), chip_select()
{ for (uint8_t digit=0; digit < NUM_DIGITS; digit++) digits[digit] = 0; }
@ -168,6 +300,9 @@ public:
// A digit of 0xFF means blank the screen.
const static uint8_t blanked = 255;
uint8_t tubeSegment = 1;
uint8_t digitOffset = 0;
void begin() {
pinMode(TFT_ENABLE_PIN, OUTPUT);
digitalWrite(TFT_ENABLE_PIN, HIGH); //enable displays on boot
@ -182,34 +317,60 @@ public:
void showDigit(uint8_t digit) {
chip_select.setDigit(digit);
uint8_t digitToDraw = digits[digit];
if (digitToDraw < 10) digitToDraw += digitOffset;
if (digits[digit] == blanked) {
fillScreen(TFT_BLACK);
if (digitToDraw == blanked) {
fillScreen(TFT_BLACK); return;
}
else {
// Filenames are no bigger than "255.bmp\0"
// if last digit was the same, skip loading from FS to buffer
if (!digitColor && digitToDraw == bufferedDigit) drawBuffer();
digitR = R(digitColor); digitG = G(digitColor); digitB = B(digitColor);
// Filenames are no bigger than "254.bmp\0"
char file_name[10];
sprintf(file_name, "/%d.bmp", digits[digit]);
// Fastest, raw RGB565
sprintf(file_name, "/%d.bin", digitToDraw);
if (WLED_FS.exists(file_name)) {
drawBmp(file_name);
} else {
sprintf(file_name, "/%d.bin", digits[digit]);
drawBin(file_name);
if (drawBin(file_name)) bufferedDigit = digitToDraw;
return;
}
// Fast, raw RGB565, see https://github.com/aly-fly/EleksTubeHAX on how to create this clk format
sprintf(file_name, "/%d.clk", digitToDraw);
if (WLED_FS.exists(file_name)) {
if (drawClk(file_name)) bufferedDigit = digitToDraw;
return;
}
// Slow, regular RGB888 or 1,4,8 bit palette BMP
sprintf(file_name, "/%d.bmp", digitToDraw);
if (drawBmp(file_name)) bufferedDigit = digitToDraw;
return;
}
void setDigit(uint8_t digit, uint8_t value, show_t show=yes) {
uint8_t old_value = digits[digit];
digits[digit] = value;
// Color in grayscale bitmaps if Segment 1 exists
// TODO If secondary and tertiary are black, color all in primary,
// else color first three from Seg 1 color slots and last three from Seg 2 color slots
WS2812FX::Segment& seg1 = strip.getSegment(tubeSegment);
if (seg1.isActive()) {
digitColor = strip.getPixelColor(seg1.start + digit);
dimming = seg1.opacity;
} else {
digitColor = 0;
dimming = 255;
}
if (show != no && (old_value != value || show == force)) {
showDigit(digit);
}
}
uint8_t getDigit(uint8_t digit) { return digits[digit]; }
uint8_t getDigit(uint8_t digit) {return digits[digit];}
void showAllDigits() { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) showDigit(digit); }
void showAllDigits() {for (uint8_t digit=0; digit < NUM_DIGITS; digit++) showDigit(digit);}
// Making chip_select public so we don't have to proxy all methods, and the caller can just use it directly.
ChipSelect chip_select;

View File

@ -5,16 +5,17 @@ It enables running all WLED effects on the background SK6812 lighting, while dis
Code is largely based on https://github.com/SmittyHalibut/EleksTubeHAX by Mark Smith!
Supported:
- Display with custom bitmaps or raw RGB565 images (.bin) from filesystem
- Display with custom bitmaps (.bmp) or raw RGB565 images (.bin) from filesystem
- Background lighting
- Power button
- All 4 hardware buttons
- RTC (with RTC usermod)
- Standard WLED time features (NTP, DST, timezones)
Not supported:
- 3 navigation buttons, on-device setup
- On-device setup with buttons (WiFi setup only)
Your images must be exactly 135 pixels wide and 1-240 pixels high.
Your images must be 1-135 pixels wide and 1-240 pixels high.
For BMP, 1, 4, 8, and 24 bits per pixel formats are supported.
## Installation
@ -25,7 +26,20 @@ Use LED pin 12, relay pin 27 and button pin 34.
## Use of RGB565 images
Binary 16-bit per pixel RGB565 format `.bin` images are now supported. This has the benefit of only using 2/3rds of the file size a `.bmp` has.
Binary 16-bit per pixel RGB565 format `.bin` and `.clk` images are now supported. This has the benefit of only using 2/3rds of the file size a 24 BPP `.bmp` has.
The drawback is that this format cannot be handled by common image programs and that an extra conversion step is needed.
You can use https://lvgl.io/tools/imageconverter to convert your .bmp to a .bin file (settings `True color` and `Binary RGB565`)
Thank you to @RedNax67 for adding .bin support.
You can use https://lvgl.io/tools/imageconverter to convert your .bmp to a .bin file (settings `True color` and `Binary RGB565`).
Thank you to @RedNax67 for adding .bin and .clk support.
For most clockface designs, using 4 or 8 BPP BMP formats will save even more file size:
| Bits per pixel | File size in kB (for 135x240 img) | % of 24 BPP BMP | Max unique colors
| --- | --- | --- | --- |
24 | 98 | 100% | 16M (66K)
16 (.clk) | 64.8 | 66% | 66K
8 | 33.7 | 34% | 256
4 | 16.4 | 17% | 16
1 | 4.9 | 5% | 2
Comparison 1 vs. 4 vs. 8 vs. 24 BPP. With this clockface on the actual clock, 4 bit looks good, and 8 bit is almost indistinguishable from 24 bit.
![comparison](https://user-images.githubusercontent.com/21045690/156899667-5b55ed9f-6e03-4066-b2aa-1260e9570369.png)

View File

@ -6,6 +6,13 @@
class ElekstubeIPSUsermod : public Usermod {
private:
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
static const char _tubeSeg[];
static const char _digitOffset[];
char cronixieDisplay[7] = "HHMMSS";
TFTs tfts;
void updateClockDisplay(TFTs::show_t show=TFTs::yes) {
bool set[6] = {false};
@ -21,6 +28,8 @@ class ElekstubeIPSUsermod : public Usermod {
set[i] = false; //display HHMMSS time
}
}
uint8_t hr = hour(localTime);
uint8_t hrTens = hr/10;
uint8_t mi = minute(localTime);
@ -37,6 +46,10 @@ class ElekstubeIPSUsermod : public Usermod {
unsigned long lastTime = 0;
public:
uint8_t lastBri;
uint32_t lastCols[6];
TFTs::show_t fshow=TFTs::yes;
void setup() {
tfts.begin();
tfts.fillScreen(TFT_BLACK);
@ -47,10 +60,90 @@ class ElekstubeIPSUsermod : public Usermod {
}
void loop() {
if (toki.isTick()) {
if (!toki.isTick()) return;
updateLocalTime();
updateClockDisplay();
WS2812FX::Segment& seg1 = strip.getSegment(tfts.tubeSegment);
if (seg1.isActive()) {
bool update = false;
if (seg1.opacity != lastBri) update = true;
lastBri = seg1.opacity;
for (uint8_t i = 0; i < 6; i++) {
uint32_t c = strip.getPixelColor(seg1.start + i);
if (c != lastCols[i]) update = true;
lastCols[i] = c;
}
if (update) fshow=TFTs::force;
} else if (lastCols[0] != 0) { // Segment 1 deleted
fshow=TFTs::force;
lastCols[0] = 0;
}
updateClockDisplay(fshow);
fshow=TFTs::yes;
}
/**
* addToConfig() (called from set.cpp) stores persistent properties to cfg.json
*/
void addToConfig(JsonObject &root) {
// we add JSON object: {"EleksTubeIPS": {"tubeSegment": 1, "digitOffset": 0}}
JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
top[FPSTR(_tubeSeg)] = tfts.tubeSegment;
top[FPSTR(_digitOffset)] = tfts.digitOffset;
DEBUG_PRINTLN(F("EleksTube config saved."));
}
/**
* readFromConfig() is called before setup() to populate properties from values stored in cfg.json
*
* The function should return true if configuration was successfully loaded or false if there was no configuration.
*/
bool readFromConfig(JsonObject &root) {
// we look for JSON object: {"EleksTubeIPS": {"tubeSegment": 1, "digitOffset": 0}}
DEBUG_PRINT(FPSTR(_name));
JsonObject top = root[FPSTR(_name)];
if (top.isNull()) {
DEBUG_PRINTLN(F(": No config found. (Using defaults.)"));
return false;
}
tfts.tubeSegment = top[FPSTR(_tubeSeg)] | tfts.tubeSegment;
uint8_t digitOffsetPrev = tfts.digitOffset;
tfts.digitOffset = top[FPSTR(_digitOffset)] | tfts.digitOffset;
if (tfts.digitOffset > 240) tfts.digitOffset = 240;
if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force;
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
return !top[FPSTR(_digitOffset)].isNull();
}
/*
* addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void addToJsonState(JsonObject& root)
{
root["nx"] = cronixieDisplay;
root[FPSTR(_digitOffset)] = tfts.digitOffset;
}
/*
* readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void readFromJsonState(JsonObject& root)
{
if (root["nx"].is<const char*>()) {
strncpy(cronixieDisplay, root["nx"], 6);
}
uint8_t digitOffsetPrev = tfts.digitOffset;
tfts.digitOffset = root[FPSTR(_digitOffset)] | tfts.digitOffset;
if (tfts.digitOffset > 240) tfts.digitOffset = 240;
if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force;
}
uint16_t getId()
@ -58,3 +151,8 @@ class ElekstubeIPSUsermod : public Usermod {
return USERMOD_ID_ELEKSTUBE_IPS;
}
};
// strings to reduce flash memory usage (used more than twice)
const char ElekstubeIPSUsermod::_name[] PROGMEM = "EleksTubeIPS";
const char ElekstubeIPSUsermod::_tubeSeg[] PROGMEM = "tubeSegment";
const char ElekstubeIPSUsermod::_digitOffset[] PROGMEM = "digitOffset";

View File

@ -2784,16 +2784,9 @@ uint16_t WS2812FX::mode_popcorn(void) {
if (numPopcorn == 0) numPopcorn = 1;
for(uint8_t i = 0; i < numPopcorn; i++) {
bool isActive = popcorn[i].pos >= 0.0f;
if (isActive) { // if kernel is active, update its position
if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position
popcorn[i].pos += popcorn[i].vel;
popcorn[i].vel += gravity;
uint32_t col = color_wheel(popcorn[i].colIndex);
if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex);
uint16_t ledIndex = popcorn[i].pos;
if (ledIndex < SEGLEN) setPixelColor(ledIndex, col);
} else { // if kernel is inactive, randomly pop it
if (random8() < 2) { // POP!!!
popcorn[i].pos = 0.01f;
@ -2812,6 +2805,13 @@ uint16_t WS2812FX::mode_popcorn(void) {
}
}
}
if (popcorn[i].pos >= 0.0f) { // draw now active popcorn (either active before or just popped)
uint32_t col = color_wheel(popcorn[i].colIndex);
if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex);
uint16_t ledIndex = popcorn[i].pos;
if (ledIndex < SEGLEN) setPixelColor(ledIndex, col);
}
}
return FRAMETIME;

View File

@ -247,7 +247,7 @@ class WS2812FX {
// segment parameters
public:
typedef struct Segment { // 30 (32 in memory) bytes
typedef struct Segment { // 31 (32 in memory) bytes
uint16_t start;
uint16_t stop; //segment invalid if stop == 0
uint16_t offset;
@ -260,6 +260,7 @@ class WS2812FX {
uint8_t opacity;
uint32_t colors[NUM_COLORS];
uint8_t cct; //0==1900K, 255==10091K
uint8_t _capabilities;
char *name;
bool setColor(uint8_t slot, uint32_t c, uint8_t segn) { //returns true if changed
if (slot >= NUM_COLORS || segn >= MAX_NUM_SEGMENTS) return false;
@ -335,7 +336,8 @@ class WS2812FX {
return vLength;
}
uint8_t differs(Segment& b);
uint8_t getLightCapabilities();
inline uint8_t getLightCapabilities() {return _capabilities;}
void refreshLightCapabilities();
} segment;
// segment runtime parameters
@ -887,14 +889,15 @@ class WS2812FX {
uint32_t _colors_t[3];
uint8_t _bri_t;
bool _no_rgb = false;
uint8_t _segment_index = 0;
uint8_t _segment_index_palette_last = 99;
uint8_t _mainSegment;
segment _segments[MAX_NUM_SEGMENTS] = { // SRAM footprint: 24 bytes per element
// start, stop, offset, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[]
{0, 7, 0, DEFAULT_SPEED, 128, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}}
// start, stop, offset, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[], capabilities
{0, 7, 0, DEFAULT_SPEED, 128, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}, 0}
};
segment_runtime _segment_runtimes[MAX_NUM_SEGMENTS]; // SRAM footprint: 28 bytes per element
friend class Segment_runtime;

View File

@ -152,7 +152,14 @@ void WS2812FX::service() {
_colors_t[slot] = transitions[t].currentColor(SEGMENT.colors[slot]);
}
if (!cctFromRgb || correctWB) busses.setSegmentCCT(_cct_t, correctWB);
for (uint8_t c = 0; c < 3; c++) _colors_t[c] = gamma32(_colors_t[c]);
_no_rgb = !(SEGMENT.getLightCapabilities() & 0x01);
for (uint8_t c = 0; c < NUM_COLORS; c++) {
// if segment is not RGB capable, treat RGB channels of main segment colors as if 0
// this prevents Dual mode with white value 0 from setting White channel from inaccessible RGB values
// If not RGB capable, also treat palette as if default (0), as palettes set white channel to 0
if (_no_rgb) _colors_t[c] = _colors_t[c] & 0xFF000000;
_colors_t[c] = gamma32(_colors_t[c]);
}
handle_palette();
delay = (this->*_mode[SEGMENT.mode])(); //effect function
if (SEGMENT.mode != FX_MODE_HALLOWEEN_EYES) SEGENV.call++;
@ -561,8 +568,10 @@ uint8_t WS2812FX::Segment::differs(Segment& b) {
return d;
}
uint8_t WS2812FX::Segment::getLightCapabilities() {
if (!isActive()) return 0;
void WS2812FX::Segment::refreshLightCapabilities() {
if (!isActive()) {
_capabilities = 0; return;
}
uint8_t capabilities = 0;
uint8_t awm = Bus::getAutoWhiteMode();
bool whiteSlider = (awm == RGBW_MODE_DUAL || awm == RGBW_MODE_MANUAL_ONLY);
@ -589,7 +598,7 @@ uint8_t WS2812FX::Segment::getLightCapabilities() {
}
if (correctWB && type != TYPE_ANALOG_1CH) capabilities |= 0x04; //white balance correction (uses CCT slider)
}
return capabilities;
_capabilities = capabilities;
}
//used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw.
@ -628,7 +637,8 @@ void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping,
Segment& seg = _segments[n];
//return if neither bounds nor grouping have changed
if (seg.start == i1 && seg.stop == i2
bool boundsUnchanged = (seg.start == i1 && seg.stop == i2);
if (boundsUnchanged
&& (!grouping || (seg.grouping == grouping && seg.spacing == spacing))
&& (offset == UINT16_MAX || offset == seg.offset)) return;
@ -653,6 +663,7 @@ void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping,
}
if (offset < UINT16_MAX) seg.offset = offset;
_segment_runtimes[n].markForReset();
if (!boundsUnchanged) seg.refreshLightCapabilities();
}
void WS2812FX::restartRuntime() {
@ -748,6 +759,8 @@ void WS2812FX::fixInvalidSegments() {
{
if (_segments[i].start >= _length) setSegment(i, 0, 0);
if (_segments[i].stop > _length) setSegment(i, _segments[i].start, _length);
// this is always called as the last step after finalizeInit(), update covered bus types
getSegment(i).refreshLightCapabilities();
}
}
@ -1123,22 +1136,17 @@ void WS2812FX::handle_palette(void)
*/
uint32_t IRAM_ATTR WS2812FX::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri)
{
if (SEGMENT.palette == 0 && mcol < 3) {
if ((SEGMENT.palette == 0 && mcol < 3) || _no_rgb) {
uint32_t color = SEGCOLOR(mcol);
if (pbri != 255) {
CRGB crgb_color = col_to_crgb(color);
crgb_color.nscale8_video(pbri);
return crgb_to_col(crgb_color);
} else {
return color;
}
if (pbri == 255) return color;
return RGBW32(scale8_video(R(color),pbri), scale8_video(G(color),pbri), scale8_video(B(color),pbri), scale8_video(W(color),pbri));
}
uint8_t paletteIndex = i;
if (mapping && SEGLEN > 1) paletteIndex = (i*255)/(SEGLEN -1);
if (!wrap) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end"
CRGB fastled_col;
fastled_col = ColorFromPalette( currentPalette, paletteIndex, pbri, (paletteBlend == 3)? NOBLEND:LINEARBLEND);
fastled_col = ColorFromPalette(currentPalette, paletteIndex, pbri, (paletteBlend == 3)? NOBLEND:LINEARBLEND);
return crgb_to_col(fastled_col);
}

View File

@ -84,6 +84,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(strip.ablMilliampsMax, hw_led[F("maxpwr")]);
CJSON(strip.milliampsPerLed, hw_led[F("ledma")]);
Bus::setAutoWhiteMode(hw_led[F("rgbwm")] | Bus::getAutoWhiteMode());
strip.fixInvalidSegments(); // refreshes segment light capabilities (in case auto white mode changed)
CJSON(correctWB, hw_led["cct"]);
CJSON(cctFromRgb, hw_led[F("cr")]);
CJSON(strip.cctBlending, hw_led[F("cb")]);
@ -94,7 +95,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
if (fromFS || !ins.isNull()) {
uint8_t s = 0; // bus iterator
busses.removeAll();
if (fromFS) busses.removeAll(); // can't safely manipulate busses directly in network callback
uint32_t mem = 0;
for (JsonObject elm : ins) {
if (s >= WLED_MAX_BUSSES) break;
@ -118,9 +119,15 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
bool refresh = elm["ref"] | false;
ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh
s++;
if (fromFS) {
BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst);
mem += BusManager::memUsage(bc);
if (mem <= MAX_LED_MEMORY && busses.getNumBusses() <= WLED_MAX_BUSSES) busses.add(bc); // finalization will be done in WLED::beginStrip()
} else {
if (busConfigs[s] != nullptr) delete busConfigs[s];
busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst);
doInitBusses = true;
}
}
// finalization done in beginStrip()
}
@ -359,10 +366,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(latitude, if_ntp[F("lt")]);
JsonObject ol = doc[F("ol")];
prev = overlayDefault;
CJSON(overlayDefault ,ol[F("clock")]); // 0
CJSON(overlayCurrent ,ol[F("clock")]); // 0
CJSON(countdownMode, ol[F("cntdwn")]);
if (prev != overlayDefault) overlayCurrent = overlayDefault;
CJSON(overlayMin, ol["min"]);
CJSON(overlayMax, ol[F("max")]);
@ -761,7 +766,7 @@ void serializeConfig() {
if_ntp[F("lt")] = latitude;
JsonObject ol = doc.createNestedObject("ol");
ol[F("clock")] = overlayDefault;
ol[F("clock")] = overlayCurrent;
ol[F("cntdwn")] = countdownMode;
ol["min"] = overlayMin;

View File

@ -71,6 +71,7 @@
#define USERMOD_RGB_ROTARY_ENCODER 22 //Usermod "rgb-rotary-encoder.h"
#define USERMOD_ID_QUINLED_AN_PENTA 23 //Usermod "quinled-an-penta.h"
#define USERMOD_ID_SSDR 24 //Usermod "usermod_v2_seven_segment_display_reloaded.h"
#define USERMOD_ID_CRONIXIE 25 //Usermod "usermod_cronixie.h"
//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot

View File

@ -692,7 +692,10 @@ input[type=range]::-moz-range-thumb {
}
#qcs-w {
margin-top: 10px;
display: none;
}
/* Quick color select buttons */
.qcs {
margin: 2px;
border-radius: 14px;
@ -701,12 +704,16 @@ input[type=range]::-moz-range-thumb {
height: 28px;
line-height: 28px;
}
/* Quick color select Black button (has white border) */
.qcsb {
width: 26px;
height: 26px;
line-height: 26px;
border: 1px solid var(--c-f);
}
/* Hex color input wrapper div */
#hexw {
margin-top: 5px;
display: none;
@ -829,6 +836,12 @@ input[type=number]::-webkit-outer-spin-button {
.segpwr {
padding: 4px 0 0 8px;
}
.pname {
top: 1px;
}
.plname {
top:0;
}
.pid {
position: absolute;
@ -887,8 +900,6 @@ input[type=number]::-webkit-outer-spin-button {
position: absolute;
bottom: 8px;
right: 8px;
color: var(--c-f);
cursor: pointer;
}
.check, .radio {
@ -942,7 +953,7 @@ input[type=number]::-webkit-outer-spin-button {
.radio:hover input ~ .radiomark,
.check:hover input ~ .checkmark {
background-color: var(--c-4);
background-color: var(--c-5);
}
.checkmark:after, .radiomark:after {

View File

@ -187,7 +187,6 @@
</span>
</div>
</div>
</div>
</div>
</div>
@ -326,7 +325,6 @@
<button class="btn" onclick="setLor(2)">Override until reboot</button><br>
<span class="h">For best performance, it is recommended to turn off the streaming source when not in use.</span>
</div>
<i id="roverstar" class="icons huge" onclick="setLor(0)">&#xe410;</i><br>
<script src="index.js"></script>
</body>

View File

@ -1,9 +1,9 @@
//page js
var loc = false, locip;
var noNewSegs = false;
var isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false, syncTglRecv = true, isRgbw = false, cct = false;
var isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false, syncTglRecv = true;
var hasWhite = false, hasRGB = false, hasCCT = false;
var whites = [0,0,0];
var colors = [[0,0,0],[0,0,0],[0,0,0]];
var expanded = [false];
var powered = [true];
var nlDur = 60, nlTar = 0;
@ -44,7 +44,10 @@ function gEBCN(c) {return d.getElementsByClassName(c);}
function isEmpty(o) {return Object.keys(o).length === 0;}
function isObj(i) {return (i && typeof i === 'object' && !Array.isArray(i));}
// returns RGB color from a given slot s 0-2 from color array a
// returns true if dataset R, G & B values are 0
function isRgbBlack(a) {return (parseInt(a.r) == 0 && parseInt(a.g) == 0 && parseInt(a.b) == 0);}
// returns RGB color from a given dataset
function rgbStr(a) {return "rgb(" + a.r + "," + a.g + "," + a.b + ")";}
// brightness approximation for selecting white as text color if background bri < 127, and black if higher
@ -53,8 +56,18 @@ function rgbBri(a) {return 0.2126*parseInt(a.r) + 0.7152*parseInt(a.g) + 0.0722*
// sets background of color slot selectors
function setCSL(cs)
{
let w = whites[parseInt(cs.id.substr(3))];
if (hasRGB && !isRgbBlack(cs.dataset)) {
cs.style.backgroundColor = rgbStr(cs.dataset);
cs.style.color = rgbBri(cs.dataset) > 127 ? "#000":"#fff"; // if text has no CSS "shadow"
if (hasWhite && w > 0) {
cs.style.background = `linear-gradient(180deg, ${rgbStr(cs.dataset)} 30%, rgb(${w},${w},${w}))`;
}
} else {
if (!hasWhite) w = 0;
cd.style.background = `rgb(${w},${w},${w})`;
cd.style.color = w > 127 ? "#000":"#fff";
}
}
function applyCfg()
@ -62,13 +75,17 @@ function applyCfg()
cTheme(cfg.theme.base === "light");
var bg = cfg.theme.color.bg;
if (bg) sCol('--c-1', bg);
if (lastinfo.leds) updateUI(); // update component visibility
/*
var ccfg = cfg.comp.colors;
gId('hexw').style.display = ccfg.hex ? "block":"none";
gId('picker').style.display = ccfg.picker ? "block":"none";
gId('vwrap').style.display = ccfg.picker ? "block":"none";
//gId('kwrap').style.display = ccfg.picker ? "block":"none";
gId('rgbwrap').style.display = ccfg.rgb ? "block":"none";
gId('qcs-w').style.display = ccfg.quick ? "block":"none";
gId('picker').style.display = (hasRGB && ccfg.picker) ? "block":"none";
gId('vwrap').style.display = (hasRGB && ccfg.picker) ? "block":"none";
gId('kwrap').style.display = (hasRGB && !hasCCT && ccfg.picker) ? "block":"none";
gId('rgbwrap').style.display = (hasRGB && ccfg.rgb) ? "block":"none";
gId('qcs-w').style.display = (hasRGB && ccfg.quick) ? "block":"none";
gId('palw').style.display = hasRGB ? "block":"none";
*/
var l = cfg.comp.labels;
var e = d.querySelectorAll('.tab-label');
for (let i of e) i.style.display = l ? "block":"none";
@ -583,12 +600,10 @@ function parseInfo() {
if (li.live) name = "(Live) " + name;
if (loc) name = "(L) " + name;
d.title = name;
isRgbw = li.leds.wv;
ledCount = li.leds.count;
syncTglRecv = li.str;
maxSeg = li.leds.maxseg;
pmt = li.fs.pmt;
cct = li.leds.cct;
}
function populateInfo(i)
@ -1013,9 +1028,16 @@ function updateUI()
updateTrail(gId('sliderC2'));
updateTrail(gId('sliderC3'));
gId('wwrap').style.display = (isRgbw) ? "block":"none";
gId("wbal").style.display = (cct) ? "block":"none";
gId('kwrap').style.display = (cct) ? "none":"block";
gId('wwrap').style.display = (hasWhite) ? "block":"none";
gId('wbal').style.display = (hasCCT) ? "block":"none";
var ccfg = cfg.comp.colors;
gId('hexw').style.display = ccfg.hex ? "block":"none";
gId('picker').style.display = (hasRGB && ccfg.picker) ? "block":"none";
gId('vwrap').style.display = (hasRGB && ccfg.picker) ? "block":"none";
gId('kwrap').style.display = (hasRGB && !hasCCT && ccfg.picker) ? "block":"none";
gId('rgbwrap').style.display = (hasRGB && ccfg.rgb) ? "block":"none";
gId('qcs-w').style.display = (hasRGB && ccfg.quick) ? "block":"none";
gId('palw').style.display = hasRGB ? "block":"none";
updatePA();
updatePSliders();
@ -1129,13 +1151,33 @@ function readState(s,command=false)
tr = s.transition;
gId('tt').value = tr/10;
var selc=0; var ind=0;
populateSegments(s);
var selc=0;
var sellvl=0; // 0: selc is invalid, 1: selc is mainseg, 2: selc is first selected
hasRGB = hasWhite = hasCCT = false;
for (let i = 0; i < (s.seg||[]).length; i++)
{
if(s.seg[i].sel) {selc = ind; break;} ind++;
if (sellvl == 0 && s.seg[i].id == s.mainseg) {
selc = i;
sellvl = 1;
}
if (s.seg[i].sel) {
if (sellvl < 2) selc = i; // get first selected segment
sellvl = 2;
var lc = lastinfo.leds.seglc[s.seg[i].id];
hasRGB |= lc & 0x01;
hasWhite |= lc & 0x02;
hasCCT |= lc & 0x04;
}
//if(s.seg[i].sel) {selc = ind; break;} ind++;
}
var i=s.seg[selc];
if (sellvl == 1) {
var lc = lastinfo.leds.seglc[i.id];
hasRGB = lc & 0x01;
hasWhite = lc & 0x02;
hasCCT = lc & 0x04;
}
if (!i) {
showToast('No Segments!', true);
updateUI();
@ -1151,7 +1193,7 @@ function readState(s,command=false)
if (isRgbw) { let w = cd[e].dataset.w = i.col[e][3]; whites[e] = parseInt(w); }
}
selectSlot(csel);
if (i.cct && i.cct>=0) gId("sliderA").value = i.cct;
if (i.cct != null && i.cct>=0) gId("sliderA").value = i.cct;
gId('sliderSpeed').value = i.sx;
gId('sliderIntensity').value = i.ix;
@ -1998,8 +2040,12 @@ function selectSlot(b)
for (let i of cd) i.classList.remove('xxs-w');
cd[b].classList.add('xxs-w');
setPicker(cd[b].style.backgroundColor);
//force slider update on initial load (picker "color:change" not fired if black)
if (cpick.color.value == 0) updatePSliders();
gId('sliderW').value = whites[b];
updatePSliders();
updateTrail(gId('sliderW'));
redrawPalPrev();
//updatePSliders();
}
//set the color from a hex string. Used by quick color selectors

View File

@ -157,7 +157,7 @@
}
gId("rf"+n).onclick = (t == 31) ? (function(){return false}) : (function(){}); // prevent change for TM1814
isRGBW |= (t == 30 || t == 31 || (t > 40 && t < 46 && t != 43)); // RGBW checkbox, TYPE_xxxx values from const.h
gId("co"+n).style.display = ((t>=80 && t<96) || t == 41 || t == 42) ? "none":"inline"; // hide color order for PWM W & WW/CW
gId("co"+n).style.display = ((t>=80 && t<96) || (t > 40 && t < 48)) ? "none":"inline"; // hide color order for PWM
gId("dig"+n+"c").style.display = (t > 40 && t < 48) ? "none":"inline"; // hide count for analog
gId("dig"+n+"r").style.display = (t>=80 && t<96) ? "none":"inline"; // hide reversed for virtual
gId("dig"+n+"s").style.display = ((t>=80 && t<96) || (t > 40 && t < 48)) ? "none":"inline"; // hide skip 1st for virtual & analog
@ -180,7 +180,7 @@
var n = LCs[i].name.substring(2); // bus number
// do we have a led count field
if (nm=="LC") {
var c=parseInt(LCs[i].value,10); //get LED gount
var c=parseInt(LCs[i].value,10); //get LED count
if (!customStarts || !startsDirty[n]) gId("ls"+n).value=sLC; //update start value
gId("ls"+n).disabled = !customStarts; //enable/disable field editing
if(c){

View File

@ -7,6 +7,7 @@
<title>Time Settings</title>
<script>
var d=document;
var el=false;
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
function H()
{
@ -35,22 +36,7 @@
}
function Cs()
{
gId("cac").style.display="none";
gId("coc").style.display="block";
gId("ccc").style.display="none";
if (gId("ca").selected)
{
gId("cac").style.display="block";
}
if (gId("cc").selected)
{
gId("coc").style.display="none";
gId("ccc").style.display="block";
}
if (gId("cn").selected)
{
gId("coc").style.display="none";
}
gId("cac").style.display=(gN("OL").checked)?"block":"none";
}
function BTa()
{
@ -128,6 +114,20 @@
td = tr.insertCell(3);
td.innerHTML = `<input name="MD${b}" type="number" class="s" min="0" max="250" value="${d}" required>`;
}
function getLoc() {
if (!el) {
window.addEventListener("message", (event) => {
if (event.origin !== "https://locate.wled.me") return;
if (event.data instanceof Object) {
d.Sf.LT.value = event.data.lat;
d.Sf.LN.value = event.data.lon;
updLoc();
}
}, false);
el = true;
}
window.open("https://locate.wled.me","_blank");
}
function updLoc(i) {
if (parseFloat(d.Sf.LT.value)<0) { d.Sf.LTR.value = "S"; d.Sf.LT.value = -1*parseFloat(d.Sf.LT.value); } else d.Sf.LTR.value = "N";
if (parseFloat(d.Sf.LN.value)<0) { d.Sf.LNR.value = "W"; d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); } else d.Sf.LNR.value = "E";
@ -177,27 +177,18 @@
UTC offset: <input name="UO" type="number" min="-65500" max="65500" required> seconds (max. 18 hours)<br>
Current local time is <span class="times">unknown</span>.<br>
Latitude: <select name="LTR"><option value="N">N</option><option value="S">S</option></select><input name="LT" type="number" class="xl" min="0" max="66.6" step="0.01"><br>
Longitude: <select name="LNR"><option value="E">E</option><option value="W">W</option></select><input name="LN" type="number" class="xl" min="0" max="180" step="0.01">
Longitude: <select name="LNR"><option value="E">E</option><option value="W">W</option></select><input name="LN" type="number" class="xl" min="0" max="180" step="0.01"><br>
<button type="button" id="locbtn" onclick="getLoc()">Get location</button>
<div><i>(opens new tab, only works in browser)</i></div>
<div id="sun" class="times"></div>
<h3>Clock</h3>
Clock Overlay:
<select name="OL" onchange="Cs()">
<option value="0" id="cn" selected>None</option>
<option value="1" id="ca">Analog Clock</option>
<option value="2">Single Digit Clock</option>
<option value="3" id="cc">Cronixie Clock</option>
</select><br>
<div id="coc">
First LED: <input name="O1" type="number" min="0" max="255" required> Last LED: <input name="O2" type="number" min="0" max="255" required><br>
Analog Clock overlay: <input type="checkbox" name="OL" onchange="Cs()"><br>
<div id="cac">
First LED: <input name="O1" type="number" min="0" max="255" required> Last LED: <input name="O2" type="number" min="0" max="255" required><br>
12h LED: <input name="OM" type="number" min="0" max="255" required><br>
Show 5min marks: <input type="checkbox" name="O5"><br></div>
Show 5min marks: <input type="checkbox" name="O5"><br>
Seconds (as trail): <input type="checkbox" name="OS"><br>
</div>
<div id="ccc">
Cronixie Display: <input name="CX" maxlength="6"><br>
Cronixie Backlight: <input type="checkbox" name="CB"><br>
</div>
Countdown Mode: <input type="checkbox" name="CE"><br>
Countdown Goal:<br>
Date:&nbsp;<nowrap>20<input name="CY" class="xs" type="number" min="0" max="99" required>-<input name="CI" class="xs" type="number" min="1" max="12" required>-<input name="CD" class="xs" type="number" min="1" max="31" required></nowrap><br>

View File

@ -179,17 +179,10 @@ void calculateSunriseAndSunset();
void setTimeFromAPI(uint32_t timein);
//overlay.cpp
void initCronixie();
void handleOverlays();
void handleOverlayDraw();
void _overlayAnalogCountdown();
void _overlayAnalogClock();
byte getSameCodeLength(char code, int index, char const cronixieDisplay[]);
void setCronixie();
void _overlayCronixie();
void _drawOverlayCronixie();
//playlist.cpp
void shufflePlaylist();
void unloadPlaylist();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -317,12 +317,6 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
}
}
#ifndef WLED_DISABLE_CRONIXIE
if (root["nx"].is<const char*>()) {
strncpy(cronixieDisplay, root["nx"], 6);
}
#endif
usermods.readFromJsonState(root);
loadLedmap = root[F("ledmap")] | loadLedmap;
@ -608,7 +602,7 @@ void serializeInfo(JsonObject root)
#ifndef WLED_DISABLE_BLYNK
os += 0x20;
#endif
#ifndef WLED_DISABLE_CRONIXIE
#ifdef USERMOD_CRONIXIE
os += 0x10;
#endif
#ifndef WLED_DISABLE_FILESYSTEM

View File

@ -169,7 +169,6 @@ void handleTime() {
updateLocalTime();
checkTimers();
checkCountdown();
handleOverlays();
}
}

View File

@ -4,31 +4,6 @@
* Used to draw clock overlays over the strip
*/
void initCronixie()
{
if (overlayCurrent == 3 && dP[0] == 255) //if dP[0] is 255, cronixie is not yet init'ed
{
setCronixie();
strip.getSegment(0).grouping = 10; //10 LEDs per digit
} else if (dP[0] < 255 && overlayCurrent != 3)
{
strip.getSegment(0).grouping = 1;
dP[0] = 255;
}
}
//handleOverlays is essentially the equivalent of usermods.loop
void handleOverlays()
{
initCronixie();
if (overlayCurrent == 3) {
_overlayCronixie();//Diamex cronixie clock kit
strip.trigger();
}
}
void _overlayAnalogClock()
{
int overlaySize = overlayMax - overlayMin +1;
@ -114,253 +89,9 @@ void _overlayAnalogCountdown()
void handleOverlayDraw() {
usermods.handleOverlayDraw();
if (!overlayCurrent) return;
switch (overlayCurrent)
{
case 1: _overlayAnalogClock(); break;
case 3: _drawOverlayCronixie(); break;
}
if (overlayCurrent == 1) _overlayAnalogClock();
}
/*
* Support for the Cronixie clock
* Support for the Cronixie clock has moved to a usermod, compile with "-D USERMOD_CRONIXIE" to enable
*/
#ifndef WLED_DISABLE_CRONIXIE
byte _digitOut[6] = {10,10,10,10,10,10};
byte getSameCodeLength(char code, int index, char const cronixieDisplay[])
{
byte counter = 0;
for (int i = index+1; i < 6; i++)
{
if (cronixieDisplay[i] == code)
{
counter++;
} else {
return counter;
}
}
return counter;
}
void setCronixie()
{
/*
* digit purpose index
* 0-9 | 0-9 (incl. random)
* 10 | blank
* 11 | blank, bg off
* 12 | test upw.
* 13 | test dnw.
* 14 | binary AM/PM
* 15 | BB upper +50 for no trailing 0
* 16 | BBB
* 17 | BBBB
* 18 | BBBBB
* 19 | BBBBBB
* 20 | H
* 21 | HH
* 22 | HHH
* 23 | HHHH
* 24 | M
* 25 | MM
* 26 | MMM
* 27 | MMMM
* 28 | MMMMM
* 29 | MMMMMM
* 30 | S
* 31 | SS
* 32 | SSS
* 33 | SSSS
* 34 | SSSSS
* 35 | SSSSSS
* 36 | Y
* 37 | YY
* 38 | YYYY
* 39 | I
* 40 | II
* 41 | W
* 42 | WW
* 43 | D
* 44 | DD
* 45 | DDD
* 46 | V
* 47 | VV
* 48 | VVV
* 49 | VVVV
* 50 | VVVVV
* 51 | VVVVVV
* 52 | v
* 53 | vv
* 54 | vvv
* 55 | vvvv
* 56 | vvvvv
* 57 | vvvvvv
*/
//H HourLower | HH - Hour 24. | AH - Hour 12. | HHH Hour of Month | HHHH Hour of Year
//M MinuteUpper | MM Minute of Hour | MMM Minute of 12h | MMMM Minute of Day | MMMMM Minute of Month | MMMMMM Minute of Year
//S SecondUpper | SS Second of Minute | SSS Second of 10 Minute | SSSS Second of Hour | SSSSS Second of Day | SSSSSS Second of Week
//B AM/PM | BB 0-6/6-12/12-18/18-24 | BBB 0-3... | BBBB 0-1.5... | BBBBB 0-1 | BBBBBB 0-0.5
//Y YearLower | YY - Year LU | YYYY - Std.
//I MonthLower | II - Month of Year
//W Week of Month | WW Week of Year
//D Day of Week | DD Day Of Month | DDD Day Of Year
DEBUG_PRINT("cset ");
DEBUG_PRINTLN(cronixieDisplay);
for (int i = 0; i < 6; i++)
{
dP[i] = 10;
switch (cronixieDisplay[i])
{
case '_': dP[i] = 10; break;
case '-': dP[i] = 11; break;
case 'r': dP[i] = random(1,7); break; //random btw. 1-6
case 'R': dP[i] = random(0,10); break; //random btw. 0-9
//case 't': break; //Test upw.
//case 'T': break; //Test dnw.
case 'b': dP[i] = 14 + getSameCodeLength('b',i,cronixieDisplay); i = i+dP[i]-14; break;
case 'B': dP[i] = 14 + getSameCodeLength('B',i,cronixieDisplay); i = i+dP[i]-14; break;
case 'h': dP[i] = 70 + getSameCodeLength('h',i,cronixieDisplay); i = i+dP[i]-70; break;
case 'H': dP[i] = 20 + getSameCodeLength('H',i,cronixieDisplay); i = i+dP[i]-20; break;
case 'A': dP[i] = 108; i++; break;
case 'a': dP[i] = 58; i++; break;
case 'm': dP[i] = 74 + getSameCodeLength('m',i,cronixieDisplay); i = i+dP[i]-74; break;
case 'M': dP[i] = 24 + getSameCodeLength('M',i,cronixieDisplay); i = i+dP[i]-24; break;
case 's': dP[i] = 80 + getSameCodeLength('s',i,cronixieDisplay); i = i+dP[i]-80; break; //refresh more often bc. of secs
case 'S': dP[i] = 30 + getSameCodeLength('S',i,cronixieDisplay); i = i+dP[i]-30; break;
case 'Y': dP[i] = 36 + getSameCodeLength('Y',i,cronixieDisplay); i = i+dP[i]-36; break;
case 'y': dP[i] = 86 + getSameCodeLength('y',i,cronixieDisplay); i = i+dP[i]-86; break;
case 'I': dP[i] = 39 + getSameCodeLength('I',i,cronixieDisplay); i = i+dP[i]-39; break; //Month. Don't ask me why month and minute both start with M.
case 'i': dP[i] = 89 + getSameCodeLength('i',i,cronixieDisplay); i = i+dP[i]-89; break;
//case 'W': break;
//case 'w': break;
case 'D': dP[i] = 43 + getSameCodeLength('D',i,cronixieDisplay); i = i+dP[i]-43; break;
case 'd': dP[i] = 93 + getSameCodeLength('d',i,cronixieDisplay); i = i+dP[i]-93; break;
case '0': dP[i] = 0; break;
case '1': dP[i] = 1; break;
case '2': dP[i] = 2; break;
case '3': dP[i] = 3; break;
case '4': dP[i] = 4; break;
case '5': dP[i] = 5; break;
case '6': dP[i] = 6; break;
case '7': dP[i] = 7; break;
case '8': dP[i] = 8; break;
case '9': dP[i] = 9; break;
//case 'V': break; //user var0
//case 'v': break; //user var1
}
}
DEBUG_PRINT("result ");
for (int i = 0; i < 5; i++)
{
DEBUG_PRINT((int)dP[i]);
DEBUG_PRINT(" ");
}
DEBUG_PRINTLN((int)dP[5]);
_overlayCronixie(); //refresh
}
void _overlayCronixie()
{
byte h = hour(localTime);
byte h0 = h;
byte m = minute(localTime);
byte s = second(localTime);
byte d = day(localTime);
byte mi = month(localTime);
int y = year(localTime);
//this has to be changed in time for 22nd century
y -= 2000; if (y<0) y += 30; //makes countdown work
if (useAMPM && !countdownMode)
{
if (h>12) h-=12;
else if (h==0) h+=12;
}
for (int i = 0; i < 6; i++)
{
if (dP[i] < 12) _digitOut[i] = dP[i];
else {
if (dP[i] < 65)
{
switch(dP[i])
{
case 21: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; i++; break; //HH
case 25: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; i++; break; //MM
case 31: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; i++; break; //SS
case 20: _digitOut[i] = h- (h/10)*10; break; //H
case 24: _digitOut[i] = m/10; break; //M
case 30: _digitOut[i] = s/10; break; //S
case 43: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //D
case 44: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; i++; break; //DD
case 40: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; i++; break; //II
case 37: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //YY
case 39: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //YYYY
//case 16: _digitOut[i+2] = ((h0/3)&1)?1:0; i++; //BBB (BBBB NI)
//case 15: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:0; i++; //BB
case 14: _digitOut[i] = (h0>11)?1:0; break; //B
}
} else
{
switch(dP[i])
{
case 71: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //hh
case 75: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //mm
case 81: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ss
//case 66: _digitOut[i+2] = ((h0/3)&1)?1:10; i++; //bbb (bbbb NI)
//case 65: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:10; i++; //bb
case 64: _digitOut[i] = (h0>11)?1:10; break; //b
case 93: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //d
case 94: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //dd
case 90: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ii
case 87: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //yy
case 89: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //yyyy
}
}
}
}
}
void _drawOverlayCronixie()
{
byte offsets[] = {5, 0, 6, 1, 7, 2, 8, 3, 9, 4};
for (uint16_t i = 0; i < 6; i++)
{
byte o = 10*i;
byte excl = 10;
if(_digitOut[i] < 10) excl = offsets[_digitOut[i]];
excl += o;
if (cronixieBacklight && _digitOut[i] <11)
{
uint32_t col = strip.gamma32(strip.getSegment(0).colors[1]);
for (uint16_t j=o; j< o+10; j++) {
if (j != excl) strip.setPixelColor(j, col);
}
} else
{
for (uint16_t j=o; j< o+10; j++) {
if (j != excl) strip.setPixelColor(j, 0);
}
}
}
}
#else // WLED_DISABLE_CRONIXIE
byte getSameCodeLength(char code, int index, char const cronixieDisplay[]) { return 0; }
void setCronixie() {}
void _overlayCronixie() {}
void _drawOverlayCronixie() {}
#endif

View File

@ -328,10 +328,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
// force a sunrise/sunset re-calculation
calculateSunriseAndSunset();
if (request->hasArg(F("OL"))) {
overlayDefault = request->arg(F("OL")).toInt();
overlayCurrent = overlayDefault;
}
overlayCurrent = request->hasArg(F("OL")) ? 1 : 0;
overlayMin = request->arg(F("O1")).toInt();
overlayMax = request->arg(F("O2")).toInt();
@ -339,10 +336,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
analogClock5MinuteMarks = request->hasArg(F("O5"));
analogClockSecondsTrail = request->hasArg(F("OS"));
#ifndef WLED_DISABLE_CRONIXIE
strlcpy(cronixieDisplay,request->arg(F("CX")).c_str(),7);
cronixieBacklight = request->hasArg(F("CB"));
#endif
countdownMode = request->hasArg(F("CE"));
countdownYear = request->arg(F("CY")).toInt();
countdownMonth = request->arg(F("CI")).toInt();
@ -787,7 +780,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply)
stateChanged |= (fxModeChanged || speedChanged || intensityChanged || paletteChanged);
//apply to main and all selected segments to prevent #1618.
// apply to main and all selected segments to prevent #1618.
for (uint8_t i = 0; i < strip.getMaxSegments(); i++) {
WS2812FX::Segment& seg = strip.getSegment(i);
if (i != selectedSeg && (singleSegment || !seg.isActive() || !seg.isSelected())) continue; // skip non main segments if not applying to all
@ -895,25 +888,10 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply)
pos = req.indexOf(F("RB"));
if (pos > 0) doReboot = true;
//cronixie
#ifndef WLED_DISABLE_CRONIXIE
//mode, 1 countdown
// clock mode, 0: normal, 1: countdown
pos = req.indexOf(F("NM="));
if (pos > 0) countdownMode = (req.charAt(pos+3) != '0');
pos = req.indexOf(F("NX=")); //sets digits to code
if (pos > 0) {
strlcpy(cronixieDisplay, req.substring(pos + 3, pos + 9).c_str(), 7);
setCronixie();
}
pos = req.indexOf(F("NB="));
if (pos > 0) //sets backlight
{
cronixieBacklight = (req.charAt(pos+3) != '0');
}
#endif
pos = req.indexOf(F("U0=")); //user var 0
if (pos > 0) {
userVar0 = getNumVal(&req, pos);

View File

@ -654,19 +654,16 @@ void sendSysInfoUDP()
uint8_t sequenceNumber = 0; // this needs to be shared across all outputs
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8_t *buffer, uint8_t bri, bool isRGBW) {
if (!interfacesInited) return 1; // network not initialised
if (!interfacesInited || !client[0] || !length) return 1; // network not initialised or dummy/unset IP address
WiFiUDP ddpUdp;
switch (type) {
case 0: // DDP
{
// calclate the number of UDP packets we need to send
// calculate the number of UDP packets we need to send
uint16_t channelCount = length * 3; // 1 channel for every R,G,B value
uint16_t packetCount = channelCount / DDP_CHANNELS_PER_PACKET;
if (channelCount % DDP_CHANNELS_PER_PACKET) {
packetCount++;
}
uint16_t packetCount = ((channelCount-1) / DDP_CHANNELS_PER_PACKET) +1;
// there are 3 channels per RGB pixel
uint32_t channel = 0; // TODO: allow specifying the start channel

View File

@ -108,6 +108,10 @@
#include "../usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h"
#endif
#ifdef USERMOD_CRONIXIE
#include "../usermods/Cronixie/usermod_cronixie.h"
#endif
#ifdef QUINLED_AN_PENTA
#include "../usermods/quinled-an-penta/quinled-an-penta.h"
#endif
@ -208,6 +212,10 @@ void registerUsermods()
usermods.add(new UsermodSSDR());
#endif
#ifdef USERMOD_CRONIXIE
usermods.add(new UsermodCronixie());
#endif
#ifdef QUINLED_AN_PENTA
usermods.add(new QuinLEDAnPentaUsermod());
#endif

View File

@ -8,7 +8,7 @@
*/
// version code in format yymmddb (b = daily build)
#define VERSION 2203043
#define VERSION 2203071
//uncomment this if you have a "my_config.h" file you'd like to use
//#define WLED_USE_MY_CONFIG
@ -26,7 +26,6 @@
// You can choose some of these features to disable:
//#define WLED_DISABLE_ALEXA // saves 11kb
//#define WLED_DISABLE_BLYNK // saves 6kb
//#define WLED_DISABLE_CRONIXIE // saves 3kb
//#define WLED_DISABLE_HUESYNC // saves 4kb
//#define WLED_DISABLE_INFRARED // there is no pin left for this on ESP8266-01, saves 12kb
#ifndef WLED_DISABLE_MQTT
@ -389,18 +388,13 @@ WLED_GLOBAL bool useAMPM _INIT(false); // 12h/24h clock format
WLED_GLOBAL byte currentTimezone _INIT(0); // Timezone ID. Refer to timezones array in wled10_ntp.ino
WLED_GLOBAL int utcOffsetSecs _INIT(0); // Seconds to offset from UTC before timzone calculation
WLED_GLOBAL byte overlayDefault _INIT(0); // 0: no overlay 1: analog clock 2: single-digit clock 3: cronixie
WLED_GLOBAL byte overlayCurrent _INIT(0); // 0: no overlay 1: analog clock 2: was single-digit clock 3: was cronixie
WLED_GLOBAL byte overlayMin _INIT(0), overlayMax _INIT(DEFAULT_LED_COUNT - 1); // boundaries of overlay mode
WLED_GLOBAL byte analogClock12pixel _INIT(0); // The pixel in your strip where "midnight" would be
WLED_GLOBAL bool analogClockSecondsTrail _INIT(false); // Display seconds as trail of LEDs instead of a single pixel
WLED_GLOBAL bool analogClock5MinuteMarks _INIT(false); // Light pixels at every 5-minute position
#ifndef WLED_DISABLE_CRONIXIE
WLED_GLOBAL char cronixieDisplay[7] _INIT("HHMMSS"); // Cronixie Display mask. See wled13_cronixie.ino
WLED_GLOBAL bool cronixieBacklight _INIT(true); // Allow digits to be back-illuminated
#endif
WLED_GLOBAL bool countdownMode _INIT(false); // Clock will count down towards date
WLED_GLOBAL byte countdownYear _INIT(20), countdownMonth _INIT(1); // Countdown target date, year is last two digits
WLED_GLOBAL byte countdownDay _INIT(1) , countdownHour _INIT(0);
@ -510,12 +504,6 @@ WLED_GLOBAL bool hueAuthRequired _INIT(false);
WLED_GLOBAL bool hueReceived _INIT(false);
WLED_GLOBAL bool hueStoreAllowed _INIT(false), hueNewKey _INIT(false);
// overlays
WLED_GLOBAL byte overlayCurrent _INIT(overlayDefault);
// cronixie
WLED_GLOBAL byte dP[] _INIT_N(({ 255, 255, 255, 255, 255, 255 }));
// countdown
WLED_GLOBAL unsigned long countdownTime _INIT(1514764800L);
WLED_GLOBAL bool countdownOverTriggered _INIT(true);

View File

@ -141,8 +141,7 @@ void loadSettingsFromEEPROM()
useAMPM = EEPROM.read(329);
strip.gammaCorrectBri = EEPROM.read(330);
strip.gammaCorrectCol = EEPROM.read(331);
overlayDefault = EEPROM.read(332);
if (lastEEPROMversion < 8 && overlayDefault > 0) overlayDefault--; //overlay mode 1 (solid) was removed
overlayCurrent = EEPROM.read(332);
alexaEnabled = EEPROM.read(333);
@ -199,11 +198,6 @@ void loadSettingsFromEEPROM()
countdownSec = EEPROM.read(2161);
setCountdown();
#ifndef WLED_DISABLE_CRONIXIE
readStringFromEEPROM(2165, cronixieDisplay, 6);
cronixieBacklight = EEPROM.read(2171);
#endif
//macroBoot = EEPROM.read(2175);
macroAlexaOn = EEPROM.read(2176);
macroAlexaOff = EEPROM.read(2177);
@ -365,8 +359,6 @@ void loadSettingsFromEEPROM()
//2551 - 2559 reserved for Usermods, usable by default
//2560 - 2943 usable, NOT reserved (need to increase EEPSIZE accordingly, new WLED core features may override this section)
//2944 - 3071 reserved for Usermods (need to increase EEPSIZE to 3072 in const.h)
overlayCurrent = overlayDefault;
}

View File

@ -257,7 +257,6 @@ void getSettingsJS(byte subPage, char* dest)
{
char nS[8];
// Pin reservations will become unnecessary when settings pages will read cfg.json directly
// add reserved and usermod pins as d.um_p array
oappend(SET_F("d.um_p=[6,7,8,9,10,11"));
@ -533,16 +532,13 @@ void getSettingsJS(byte subPage, char* dest)
sprintf_P(tm, PSTR("Sunrise: %02d:%02d Sunset: %02d:%02d"), hour(sunrise), minute(sunrise), hour(sunset), minute(sunset));
sappends('m',SET_F("(\"times\")[1]"),tm);
}
sappend('i',SET_F("OL"),overlayCurrent);
sappend('c',SET_F("OL"),overlayCurrent);
sappend('v',SET_F("O1"),overlayMin);
sappend('v',SET_F("O2"),overlayMax);
sappend('v',SET_F("OM"),analogClock12pixel);
sappend('c',SET_F("OS"),analogClockSecondsTrail);
sappend('c',SET_F("O5"),analogClock5MinuteMarks);
#ifndef WLED_DISABLE_CRONIXIE
sappends('s',SET_F("CX"),cronixieDisplay);
sappend('c',SET_F("CB"),cronixieBacklight);
#endif
sappend('c',SET_F("CE"),countdownMode);
sappend('v',SET_F("CY"),countdownYear);
sappend('v',SET_F("CI"),countdownMonth);