Elekstube usermod enhancements

Coloring of grayscale images
Dimming control from configurable segment
This commit is contained in:
cschwinne 2022-03-06 22:24:24 +01:00
parent 02b08939cd
commit ad301fd087
4 changed files with 164 additions and 59 deletions

View File

@ -35,6 +35,8 @@ private:
uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH]; uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH];
int16_t w = 135, h = 240, x = 0, y = 0, bufferedDigit = 255; 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() { void drawBuffer() {
bool oldSwapBytes = getSwapBytes(); bool oldSwapBytes = getSwapBytes();
@ -75,10 +77,6 @@ private:
if (!realtimeMode || realtimeOverride) strip.service(); if (!realtimeMode || realtimeOverride) strip.service();
#ifdef ELEKSTUBE_DIMMING
dimming=bri;
#endif
// 0,0 coordinates are top left // 0,0 coordinates are top left
for (row = 0; row < h; row++) { for (row = 0; row < h; row++) {
@ -88,7 +86,7 @@ private:
// Colors are already in 16-bit R5, G6, B5 format // Colors are already in 16-bit R5, G6, B5 format
for (col = 0; col < w; col++) for (col = 0; col < w; col++)
{ {
if (dimming == 255) { // not needed, copy directly if (dimming == 255 && !digitColor) { // not needed, copy directly
output_buffer[row][col] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]); output_buffer[row][col] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]);
} else { } else {
// 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB
@ -98,12 +96,14 @@ private:
r = (PixM) & 0xF8; r = (PixM) & 0xF8;
g = ((PixM << 5) | (PixL >> 3)) & 0xFC; g = ((PixM << 5) | (PixL >> 3)) & 0xFC;
b = (PixL << 3) & 0xF8; b = (PixL << 3) & 0xF8;
r *= dimming; r *= dimming; g *= dimming; b *= dimming;
g *= dimming; r = r >> 8; g = g >> 8; b = b >> 8;
b *= dimming; if (digitColor) { // grayscale pixel coloring
r = r >> 8; uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b);
g = g >> 8; r = g = b = l;
b = b >> 8; 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); output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
} }
} }
@ -176,9 +176,6 @@ private:
bmpFS.read(lineBuffer, sizeof(lineBuffer)); bmpFS.read(lineBuffer, sizeof(lineBuffer));
uint8_t* bptr = lineBuffer; uint8_t* bptr = lineBuffer;
#ifdef ELEKSTUBE_DIMMING
dimming=bri;
#endif
// Convert 24 to 16 bit colors while copying to output buffer. // Convert 24 to 16 bit colors while copying to output buffer.
for (uint16_t col = 0; col < w; col++) for (uint16_t col = 0; col < w; col++)
{ {
@ -202,12 +199,15 @@ private:
b = c; g = c >> 8; r = c >> 16; b = c; g = c >> 8; r = c >> 16;
} }
if (dimming != 255) { // only dimm when needed if (dimming != 255) { // only dimm when needed
b *= dimming; r *= dimming; g *= dimming; b *= dimming;
g *= dimming; r = r >> 8; g = g >> 8; b = b >> 8;
r *= dimming; }
b = b >> 8; if (digitColor) { // grayscale pixel coloring
g = g >> 8; uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b);
r = r >> 8; 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); output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xFF) >> 3);
} }
@ -250,10 +250,6 @@ private:
uint8_t lineBuffer[w * 2]; uint8_t lineBuffer[w * 2];
#ifdef ELEKSTUBE_DIMMING
dimming=bri;
#endif
if (!realtimeMode || realtimeOverride) strip.service(); if (!realtimeMode || realtimeOverride) strip.service();
// 0,0 coordinates are top left // 0,0 coordinates are top left
@ -265,7 +261,7 @@ private:
// Colors are already in 16-bit R5, G6, B5 format // Colors are already in 16-bit R5, G6, B5 format
for (col = 0; col < w; col++) for (col = 0; col < w; col++)
{ {
if (dimming == 255) { // not needed, copy directly if (dimming == 255 && !digitColor) { // not needed, copy directly
output_buffer[row][col+x] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]); output_buffer[row][col+x] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]);
} else { } else {
// 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB
@ -275,12 +271,14 @@ private:
r = (PixM) & 0xF8; r = (PixM) & 0xF8;
g = ((PixM << 5) | (PixL >> 3)) & 0xFC; g = ((PixM << 5) | (PixL >> 3)) & 0xFC;
b = (PixL << 3) & 0xF8; b = (PixL << 3) & 0xF8;
r *= dimming; r *= dimming; g *= dimming; b *= dimming;
g *= dimming; r = r >> 8; g = g >> 8; b = b >> 8;
b *= dimming; if (digitColor) { // grayscale pixel coloring
r = r >> 8; uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b);
g = g >> 8; r = g = b = l;
b = b >> 8; 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); output_buffer[row][col+x] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
} }
} }
@ -302,6 +300,9 @@ public:
// A digit of 0xFF means blank the screen. // A digit of 0xFF means blank the screen.
const static uint8_t blanked = 255; const static uint8_t blanked = 255;
uint8_t tubeSegment = 1;
uint8_t digitOffset = 0;
void begin() { void begin() {
pinMode(TFT_ENABLE_PIN, OUTPUT); pinMode(TFT_ENABLE_PIN, OUTPUT);
digitalWrite(TFT_ENABLE_PIN, HIGH); //enable displays on boot digitalWrite(TFT_ENABLE_PIN, HIGH); //enable displays on boot
@ -317,13 +318,15 @@ public:
void showDigit(uint8_t digit) { void showDigit(uint8_t digit) {
chip_select.setDigit(digit); chip_select.setDigit(digit);
uint8_t digitToDraw = digits[digit]; uint8_t digitToDraw = digits[digit];
if (digitToDraw < 10) digitToDraw += digitOffset;
if (digitToDraw == blanked) { if (digitToDraw == blanked) {
fillScreen(TFT_BLACK); return; fillScreen(TFT_BLACK); return;
} }
// if last digit was the same, skip loading from FS to buffer // if last digit was the same, skip loading from FS to buffer
if (digitToDraw == bufferedDigit) drawBuffer(); if (!digitColor && digitToDraw == bufferedDigit) drawBuffer();
digitR = R(digitColor); digitG = G(digitColor); digitB = B(digitColor);
// Filenames are no bigger than "254.bmp\0" // Filenames are no bigger than "254.bmp\0"
char file_name[10]; char file_name[10];
@ -349,13 +352,25 @@ public:
uint8_t old_value = digits[digit]; uint8_t old_value = digits[digit];
digits[digit] = value; 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)) { if (show != no && (old_value != value || show == force)) {
showDigit(digit); 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. // Making chip_select public so we don't have to proxy all methods, and the caller can just use it directly.
ChipSelect chip_select; 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! Code is largely based on https://github.com/SmittyHalibut/EleksTubeHAX by Mark Smith!
Supported: 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 - Background lighting
- Power button - All 4 hardware buttons
- RTC (with RTC usermod) - RTC (with RTC usermod)
- Standard WLED time features (NTP, DST, timezones) - Standard WLED time features (NTP, DST, timezones)
Not supported: 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 ## Installation
@ -25,7 +26,20 @@ Use LED pin 12, relay pin 27 and button pin 34.
## Use of RGB565 images ## 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. 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`) 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. 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,11 @@
class ElekstubeIPSUsermod : public Usermod { class ElekstubeIPSUsermod : public Usermod {
private: private:
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
static const char _tubeSeg[];
static const char _digitOffset[];
TFTs tfts; TFTs tfts;
void updateClockDisplay(TFTs::show_t show=TFTs::yes) { void updateClockDisplay(TFTs::show_t show=TFTs::yes) {
bool set[6] = {false}; bool set[6] = {false};
@ -40,6 +45,7 @@ class ElekstubeIPSUsermod : public Usermod {
public: public:
uint8_t lastBri; uint8_t lastBri;
uint32_t lastCols[6];
TFTs::show_t fshow=TFTs::yes; TFTs::show_t fshow=TFTs::yes;
void setup() { void setup() {
@ -52,17 +58,85 @@ class ElekstubeIPSUsermod : public Usermod {
} }
void loop() { void loop() {
if (toki.isTick()) { if (!toki.isTick()) return;
updateLocalTime(); updateLocalTime();
#ifdef ELEKSTUBE_DIMMING
if (bri != lastBri) { WS2812FX::Segment& seg1 = strip.getSegment(tfts.tubeSegment);
fshow=TFTs::force; if (seg1.isActive()) {
lastBri = bri; 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;
} }
#endif if (update) fshow=TFTs::force;
updateClockDisplay(fshow); } else if (lastCols[0] != 0) { // Segment 1 deleted
fshow=TFTs::yes; 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[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)
{
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() uint16_t getId()
@ -70,3 +144,8 @@ class ElekstubeIPSUsermod : public Usermod {
return USERMOD_ID_ELEKSTUBE_IPS; 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

@ -652,19 +652,16 @@ void sendSysInfoUDP()
uint8_t sequenceNumber = 0; // this needs to be shared across all outputs 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) { 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; WiFiUDP ddpUdp;
switch (type) { switch (type) {
case 0: // DDP 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 channelCount = length * 3; // 1 channel for every R,G,B value
uint16_t packetCount = channelCount / DDP_CHANNELS_PER_PACKET; uint16_t packetCount = ((channelCount-1) / DDP_CHANNELS_PER_PACKET) +1;
if (channelCount % DDP_CHANNELS_PER_PACKET) {
packetCount++;
}
// there are 3 channels per RGB pixel // there are 3 channels per RGB pixel
uint32_t channel = 0; // TODO: allow specifying the start channel uint32_t channel = 0; // TODO: allow specifying the start channel