First working Elekstube IPS and RTC mod
70
usermods/EleksTube_IPS/ChipSelect.h
Normal file
@@ -0,0 +1,70 @@
|
||||
#ifndef CHIP_SELECT_H
|
||||
#define CHIP_SELECT_H
|
||||
|
||||
#include "Hardware.h"
|
||||
|
||||
/*
|
||||
* `digit`s are as defined in Hardware.h, 0 == seconds ones, 5 == hours tens.
|
||||
*/
|
||||
|
||||
class ChipSelect {
|
||||
private:
|
||||
uint8_t digits_map;
|
||||
const uint8_t all_on = 0x3F;
|
||||
const uint8_t all_off = 0x00;
|
||||
public:
|
||||
ChipSelect() : digits_map(all_off) {}
|
||||
|
||||
void update() {
|
||||
// Documented in README.md. Q7 and Q6 are unused. Q5 is Seconds Ones, Q0 is Hours Tens.
|
||||
// Q7 is the first bit written, Q0 is the last. So we push two dummy bits, then start with
|
||||
// Seconds Ones and end with Hours Tens.
|
||||
// CS is Active Low, but digits_map is 1 for enable, 0 for disable. So we bit-wise NOT first.
|
||||
|
||||
uint8_t to_shift = (~digits_map) << 2;
|
||||
|
||||
digitalWrite(CSSR_LATCH_PIN, LOW);
|
||||
shiftOut(CSSR_DATA_PIN, CSSR_CLOCK_PIN, LSBFIRST, to_shift);
|
||||
digitalWrite(CSSR_LATCH_PIN, HIGH);
|
||||
}
|
||||
|
||||
void begin()
|
||||
{
|
||||
pinMode(CSSR_LATCH_PIN, OUTPUT);
|
||||
pinMode(CSSR_DATA_PIN, OUTPUT);
|
||||
pinMode(CSSR_CLOCK_PIN, OUTPUT);
|
||||
|
||||
digitalWrite(CSSR_DATA_PIN, LOW);
|
||||
digitalWrite(CSSR_CLOCK_PIN, LOW);
|
||||
digitalWrite(CSSR_LATCH_PIN, LOW);
|
||||
update();
|
||||
}
|
||||
|
||||
// These speak the indexes defined in Hardware.h.
|
||||
// So 0 is disabled, 1 is enabled (even though CS is active low, this gets mapped.)
|
||||
// So bit 0 (LSB), is index 0, is SECONDS_ONES
|
||||
// Translation to what the 74HC595 uses is done in update()
|
||||
void setDigitMap(uint8_t map, bool update_=true) { digits_map = map; if (update_) update(); }
|
||||
uint8_t getDigitMap() { return digits_map; }
|
||||
|
||||
// Helper functions
|
||||
// Sets just the one digit by digit number
|
||||
void setDigit(uint8_t digit, bool update_=true) { setDigitMap(0x01 << digit, update_); }
|
||||
void setAll(bool update_=true) { setDigitMap(all_on, update_); }
|
||||
void clear(bool update_=true) { setDigitMap(all_off, update_); }
|
||||
void setSecondsOnes() { setDigit(SECONDS_ONES); }
|
||||
void setSecondsTens() { setDigit(SECONDS_TENS); }
|
||||
void setMinutesOnes() { setDigit(MINUTES_ONES); }
|
||||
void setMinutesTens() { setDigit(MINUTES_TENS); }
|
||||
void setHoursOnes() { setDigit(HOURS_ONES); }
|
||||
void setHoursTens() { setDigit(HOURS_TENS); }
|
||||
bool isSecondsOnes() { return (digits_map&SECONDS_ONES_MAP > 0); }
|
||||
bool isSecondsTens() { return (digits_map&SECONDS_TENS_MAP > 0); }
|
||||
bool isMinutesOnes() { return (digits_map&MINUTES_ONES_MAP > 0); }
|
||||
bool isMinutesTens() { return (digits_map&MINUTES_TENS_MAP > 0); }
|
||||
bool isHoursOnes() { return (digits_map&HOURS_ONES_MAP > 0); }
|
||||
bool isHoursTens() { return (digits_map&HOURS_TENS_MAP > 0); }
|
||||
};
|
||||
|
||||
|
||||
#endif // CHIP_SELECT_H
|
||||
52
usermods/EleksTube_IPS/Hardware.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Define the hardware for the EleksTube IPS clock. Mostly pin definitions
|
||||
*/
|
||||
#ifndef ELEKSTUBEHAX_HARDWARE_H
|
||||
#define ELEKSTUBEHAX_HARDWARE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <Arduino.h> // for HIGH and LOW
|
||||
|
||||
// Common indexing scheme, used to identify the digit
|
||||
#define SECONDS_ONES (0)
|
||||
#define SECONDS_TENS (1)
|
||||
#define MINUTES_ONES (2)
|
||||
#define MINUTES_TENS (3)
|
||||
#define HOURS_ONES (4)
|
||||
#define HOURS_TENS (5)
|
||||
#define NUM_DIGITS (6)
|
||||
|
||||
#define SECONDS_ONES_MAP (0x01 << SECONDS_ONES)
|
||||
#define SECONDS_TENS_MAP (0x01 << SECONDS_TENS)
|
||||
#define MINUTES_ONES_MAP (0x01 << MINUTES_ONES)
|
||||
#define MINUTES_TENS_MAP (0x01 << MINUTES_TENS)
|
||||
#define HOURS_ONES_MAP (0x01 << HOURS_ONES)
|
||||
#define HOURS_TENS_MAP (0x01 << HOURS_TENS)
|
||||
|
||||
// WS2812 (or compatible) LEDs on the back of the display modules.
|
||||
#define BACKLIGHTS_PIN (12)
|
||||
|
||||
// Buttons, active low, externally pulled up (with actual resistors!)
|
||||
#define BUTTON_LEFT_PIN (33)
|
||||
#define BUTTON_MODE_PIN (32)
|
||||
#define BUTTON_RIGHT_PIN (35)
|
||||
#define BUTTON_POWER_PIN (34)
|
||||
|
||||
// I2C to DS3231 RTC.
|
||||
#define RTC_SCL_PIN (22)
|
||||
#define RTC_SDA_PIN (21)
|
||||
|
||||
// Chip Select shift register, to select the display
|
||||
#define CSSR_DATA_PIN (14)
|
||||
#define CSSR_CLOCK_PIN (16)
|
||||
#define CSSR_LATCH_PIN (17)
|
||||
|
||||
// SPI to displays
|
||||
// DEFINED IN User_Setup.h
|
||||
// Look for: TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, and TFT_RST
|
||||
|
||||
// Power for all TFT displays are grounded through a MOSFET so they can all be turned off.
|
||||
// Active HIGH.
|
||||
#define TFT_ENABLE_PIN (27)
|
||||
|
||||
#endif // ELEKSTUBEHAX_HARDWARE_H
|
||||
169
usermods/EleksTube_IPS/TFTs.h
Normal file
@@ -0,0 +1,169 @@
|
||||
#ifndef TFTS_H
|
||||
#define TFTS_H
|
||||
|
||||
#include <FS.h>
|
||||
|
||||
#include <TFT_eSPI.h>
|
||||
#include "Hardware.h"
|
||||
#include "ChipSelect.h"
|
||||
|
||||
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.
|
||||
|
||||
uint16_t read16(fs::File &f) {
|
||||
uint16_t result;
|
||||
((uint8_t *)&result)[0] = f.read(); // LSB
|
||||
((uint8_t *)&result)[1] = f.read(); // MSB
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t read32(fs::File &f) {
|
||||
uint32_t result;
|
||||
((uint8_t *)&result)[0] = f.read(); // LSB
|
||||
((uint8_t *)&result)[1] = f.read();
|
||||
((uint8_t *)&result)[2] = f.read();
|
||||
((uint8_t *)&result)[3] = f.read(); // MSB
|
||||
return result;
|
||||
}
|
||||
|
||||
uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH];
|
||||
|
||||
// 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.
|
||||
// I've modified drawBmp to buffer the whole image at once instead of doing it line-by-line.
|
||||
|
||||
//// BEGIN STOLEN CODE
|
||||
|
||||
bool drawBmp(const char *filename, int16_t x, int16_t y) {
|
||||
// Nothing to do.
|
||||
if ((x >= width()) || (y >= height())) return(true);
|
||||
|
||||
fs::File bmpFS;
|
||||
|
||||
// Open requested file on SD card
|
||||
bmpFS = WLED_FS.open(filename, "r");
|
||||
|
||||
if (!bmpFS)
|
||||
{
|
||||
Serial.println(F("File not found"));
|
||||
return(false);
|
||||
}
|
||||
|
||||
uint32_t seekOffset;
|
||||
int16_t w, h, row;
|
||||
uint8_t r, g, b;
|
||||
|
||||
uint16_t magic = read16(bmpFS);
|
||||
if (magic == 0xFFFF) {
|
||||
Serial.println(F("BMP not found!"));
|
||||
bmpFS.close();
|
||||
return(false);
|
||||
}
|
||||
|
||||
if (magic != 0x4D42) {
|
||||
Serial.print(F("File not a BMP. Magic: "));
|
||||
Serial.println(magic);
|
||||
bmpFS.close();
|
||||
return(false);
|
||||
}
|
||||
|
||||
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)) {
|
||||
Serial.println(F("BMP format not recognized."));
|
||||
bmpFS.close();
|
||||
return(false);
|
||||
}
|
||||
|
||||
bool oldSwapBytes = getSwapBytes();
|
||||
setSwapBytes(true);
|
||||
bmpFS.seek(seekOffset);
|
||||
|
||||
uint16_t padding = (4 - ((w * 3) & 3)) & 3;
|
||||
uint8_t lineBuffer[w * 3 + padding];
|
||||
|
||||
// row is decremented as the BMP image is drawn bottom up
|
||||
for (row = h-1; row >= 0; row--) {
|
||||
if (row & 0b00000111 == 7) strip.service(); //still refresh backlight to mitigate stutter every few rows
|
||||
bmpFS.read(lineBuffer, sizeof(lineBuffer));
|
||||
uint8_t* bptr = lineBuffer;
|
||||
|
||||
// Convert 24 to 16 bit colours while copying to output buffer.
|
||||
for (uint16_t col = 0; col < w; col++)
|
||||
{
|
||||
b = *bptr++;
|
||||
g = *bptr++;
|
||||
r = *bptr++;
|
||||
output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
|
||||
}
|
||||
}
|
||||
|
||||
pushImage(x, y, w, h, (uint16_t *)output_buffer);
|
||||
setSwapBytes(oldSwapBytes);
|
||||
|
||||
bmpFS.close();
|
||||
return(true);
|
||||
}
|
||||
|
||||
public:
|
||||
TFTs() : TFT_eSPI(), chip_select()
|
||||
{ for (uint8_t digit=0; digit < NUM_DIGITS; digit++) digits[digit] = 0; }
|
||||
|
||||
// no == Do not send to TFT. yes == Send to TFT if changed. force == Send to TFT.
|
||||
enum show_t { no, yes, force };
|
||||
// A digit of 0xFF means blank the screen.
|
||||
const static uint8_t blanked = 255;
|
||||
|
||||
void begin() {
|
||||
pinMode(TFT_ENABLE_PIN, OUTPUT);
|
||||
digitalWrite(TFT_ENABLE_PIN, HIGH); //enable displays on boot
|
||||
|
||||
// Start with all displays selected.
|
||||
chip_select.begin();
|
||||
chip_select.setAll();
|
||||
|
||||
// Initialize the super class.
|
||||
init();
|
||||
}
|
||||
|
||||
void showDigit(uint8_t digit) {
|
||||
chip_select.setDigit(digit);
|
||||
|
||||
if (digits[digit] == blanked) {
|
||||
fillScreen(TFT_BLACK);
|
||||
}
|
||||
else {
|
||||
// Filenames are no bigger than "255.bmp\0"
|
||||
char file_name[10];
|
||||
sprintf(file_name, "/%d.bmp", digits[digit]);
|
||||
drawBmp(file_name, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void setDigit(uint8_t digit, uint8_t value, show_t show=yes) {
|
||||
uint8_t old_value = digits[digit];
|
||||
digits[digit] = value;
|
||||
|
||||
if (show != no && (old_value != value || show == force)) {
|
||||
showDigit(digit);
|
||||
}
|
||||
}
|
||||
uint8_t getDigit(uint8_t digit) { return digits[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;
|
||||
};
|
||||
|
||||
#endif // TFTS_H
|
||||
47
usermods/EleksTube_IPS/User_Setup.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This is intended to over-ride `User_Setup.h` that comes with the TFT_eSPI library.
|
||||
* I hate having to modify the library code.
|
||||
*/
|
||||
|
||||
// ST7789 135 x 240 display with no chip select line
|
||||
|
||||
#define ST7789_DRIVER // Configure all registers
|
||||
|
||||
#define TFT_WIDTH 135
|
||||
#define TFT_HEIGHT 240
|
||||
|
||||
#define CGRAM_OFFSET // Library will add offsets required
|
||||
|
||||
//#define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue
|
||||
//#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red
|
||||
|
||||
//#define TFT_INVERSION_ON
|
||||
//#define TFT_INVERSION_OFF
|
||||
|
||||
// EleksTube IPS
|
||||
#define TFT_SDA_READ // Read and write on the MOSI/SDA pin, no separate MISO pin
|
||||
#define TFT_MOSI 23
|
||||
#define TFT_SCLK 18
|
||||
//#define TFT_CS -1 // Not connected
|
||||
#define TFT_DC 25 // Data Command, aka Register Select or RS
|
||||
#define TFT_RST 26 // Connect reset to ensure display initialises
|
||||
|
||||
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
|
||||
//#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
|
||||
//#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
|
||||
//#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
|
||||
//#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:.
|
||||
//#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
|
||||
//#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT
|
||||
//#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
|
||||
|
||||
//#define SMOOTH_FONT
|
||||
|
||||
|
||||
//#define SPI_FREQUENCY 27000000
|
||||
#define SPI_FREQUENCY 40000000
|
||||
|
||||
/*
|
||||
* To make the Library not over-write all this:
|
||||
*/
|
||||
#define USER_SETUP_LOADED
|
||||
BIN
usermods/EleksTube_IPS/bmp/0.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
usermods/EleksTube_IPS/bmp/1.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
usermods/EleksTube_IPS/bmp/2.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
usermods/EleksTube_IPS/bmp/3.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
usermods/EleksTube_IPS/bmp/4.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
usermods/EleksTube_IPS/bmp/5.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
usermods/EleksTube_IPS/bmp/6.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
usermods/EleksTube_IPS/bmp/7.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
usermods/EleksTube_IPS/bmp/8.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
usermods/EleksTube_IPS/bmp/9.bmp
Normal file
|
After Width: | Height: | Size: 96 KiB |
21
usermods/EleksTube_IPS/readme.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# EleksTube IPS Clock usermod
|
||||
|
||||
This usermod allows WLED to run on the EleksTube IPS clock.
|
||||
It enables running all WLED effects on the background SK6812 lighting, while displaying digit bitmaps on the 6 IPS screens.
|
||||
Code is largely based on https://github.com/SmittyHalibut/EleksTubeHAX by Mark Smith
|
||||
|
||||
Supported:
|
||||
- Display with custom bitmaps from filesystem
|
||||
- Background lighting
|
||||
- Power button
|
||||
- RTC (with RTC usermod)
|
||||
- Standard WLED time features (NTP, DST, timezones)
|
||||
|
||||
Not supported:
|
||||
- 3 navigation buttons, on-device setup
|
||||
|
||||
## Installation
|
||||
|
||||
Compile and upload to clock using the `elekstube_ips` PlatformIO environment
|
||||
Once uploaded (the clock can be flashed like any ESP32 module), go to `[WLED-IP]/edit` and upload the 0-9.bmp files from the bmp folder.
|
||||
Use LED pin 12, relay pin 27 and button pin 34.
|
||||
48
usermods/EleksTube_IPS/usermod_elekstube_ips.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include "TFTs.h"
|
||||
#include "wled.h"
|
||||
|
||||
class ElekstubeIPSUsermod : public Usermod {
|
||||
private:
|
||||
TFTs tfts;
|
||||
void updateClockDisplay(TFTs::show_t show=TFTs::yes) {
|
||||
uint8_t hr = hour(localTime);
|
||||
uint8_t hrTens = hr/10;
|
||||
uint8_t mi = minute(localTime);
|
||||
uint8_t mittens = mi/10;
|
||||
uint8_t s = second(localTime);
|
||||
uint8_t sTens = s/10;
|
||||
tfts.setDigit(HOURS_TENS, hrTens, show);
|
||||
tfts.setDigit(HOURS_ONES, hr - hrTens*10, show);
|
||||
tfts.setDigit(MINUTES_TENS, mittens, show);
|
||||
tfts.setDigit(MINUTES_ONES, mi - mittens*10, show);
|
||||
tfts.setDigit(SECONDS_TENS, sTens, show);
|
||||
tfts.setDigit(SECONDS_ONES, s - sTens*10, show);
|
||||
}
|
||||
unsigned long lastTime = 0;
|
||||
public:
|
||||
|
||||
void setup() {
|
||||
tfts.begin();
|
||||
tfts.fillScreen(TFT_BLACK);
|
||||
tfts.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tfts.setCursor(0, 0, 2);
|
||||
tfts.println("<STARTUP>");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (lastTime == 0) {
|
||||
tfts.fillScreen(TFT_BLACK);
|
||||
updateClockDisplay(TFTs::force);
|
||||
}
|
||||
if (millis() - lastTime > 100) {
|
||||
updateClockDisplay();
|
||||
lastTime = millis();
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t getId()
|
||||
{
|
||||
return USERMOD_ID_ELEKSTUBE_IPS;
|
||||
}
|
||||
};
|
||||