/* xdsp_13_tm1640.ino - TM1640B LED display controller support for Tasmota Copyright (C) 2024 Stefan Oskamp This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_DISPLAY #ifdef USE_DISPLAY_TM1640 /*********************************************************************************************\ This driver enables the display of the current time, numbers (both integers and floats) and basic text on the IoTTimer clock. Template {"NAME":"IoTTimer Lo","GPIO":[32,33,0,34,3872,1312,0,0,10944,10912,640,480,608,4768],"FLAG":0,"BASE":18} In addition, it is possible to set brightness (seven levels plus off) and clear the display. To use, compile Tasmota with USE_DISPLAY, USE_DISPLAY_TM1640 and USE_IOTTIMER, or build the tasmota-display firmware. Either use following template: Template {"NAME":"IoTTimer Lo","GPIO":[32,33,0,34,3872,1312,0,0,10944,10912,640,480,608,4768],"FLAG":0,"BASE":18} or configure manually: For the IoTTimer clock assign the pins as follows from Tasmota's GUI: GPIO12 --> "TM1640 DIN" GPIO13 --> "TM1640 CLK" Once the GPIO configuration is saved and the ESP8266/ESP32 module restarts, set the Display Model to 13 and Display Mode to 0 using the command "Backlog DisplayModel 13 ; DisplayMode 0;" Before using it, set the Display Type to 1 (for IOTTIMER) using the "DisplayType 1" command. After the ESP8266 restarts again, turn ON the display with the command "Power 1" Now, the following "Display" commands can be used: DisplayClear Clears the display, command: "DisplayClear" DisplayFloat num Clears and then displays float (with decimal point) command e.g., "DisplayFloat 12.3" See function description below for more details. DisplayFloatNC num Same as DisplayFloatNC DisplayClock 1|2|3|4 Displays a clock. Commands "DisplayClock 1" // 12 hr format "DisplayClock 2" // 24 hr format "DisplayClock 3" // 12-hour without seconds "DisplayClock 4" // 24-hour without seconds In addition, if you compile using USE_DISPLAY_MODES1TO5, setting DisplayMode to 1 shows the time, setting it to 2 shows the date and setting it to 3 alternates between time and date (using "DisplayRefresh [1..7]" for the time and seconds you want to show the time before displaying the date) \*********************************************************************************************/ #define XDSP_13 13 #define TM1640_CMD_DATA_AUTO 0x40 #define TM1640_CMD_DATA_FIXED 0x44 #define TM1640_CMD_DISPLAY 0x80 #define TM1640_CMD_ADDRESS 0xC0 #define TM1640_CLOCK_DELAY 1 // uSec #define LEVEL_MIN 0 #define LEVEL_MAX 100 enum tm1640_display_options_types { TM1640_DEFAULT, TM1640_IOTTIMER // IOTTIMER WiFi clock }; typedef struct Tm1640_t { int8_t clock_pin; int8_t data_pin; uint8_t brightness; bool show_clock; bool clock_24; bool clock_seconds; } Tm1640_t; Tm1640_t* Tm1640 = nullptr; /*********************************************************************************************\ * TM1640 low level functions \*********************************************************************************************/ void TM1640Start (void) { digitalWrite(Tm1640->data_pin, LOW); digitalWrite(Tm1640->clock_pin, LOW); delayMicroseconds(TM1640_CLOCK_DELAY); } void TM1640Stop (void) { digitalWrite(Tm1640->clock_pin, HIGH); digitalWrite(Tm1640->data_pin, HIGH); delayMicroseconds(TM1640_CLOCK_DELAY); } void TM1640Send(uint8_t data) { for (uint32_t i = 0; i < 8; i++) { // 8 bits digitalWrite(Tm1640->data_pin, data & 1 ? HIGH : LOW); delayMicroseconds(TM1640_CLOCK_DELAY); data >>= 1; digitalWrite(Tm1640->clock_pin, HIGH); delayMicroseconds(TM1640_CLOCK_DELAY); digitalWrite(Tm1640->clock_pin, LOW); delayMicroseconds(TM1640_CLOCK_DELAY); } digitalWrite(Tm1640->data_pin, LOW); delayMicroseconds(TM1640_CLOCK_DELAY); } void TM1640SendData(uint8_t address, uint8_t data) { // First, send data command using FIXED addressing: TM1640Start(); TM1640Send(TM1640_CMD_DATA_FIXED); TM1640Stop(); // Then, send address and one data byte: TM1640Start(); TM1640Send(TM1640_CMD_ADDRESS | address); TM1640Send(data); TM1640Stop(); } void TM1640SendDataArray(uint8_t address, uint8_t *data, uint8_t count) { // First, send data command using AUTO addressing: TM1640Start(); TM1640Send(TM1640_CMD_DATA_AUTO); TM1640Stop(); // Then, send address and all data bytes: TM1640Start(); TM1640Send(TM1640_CMD_ADDRESS | address); while (count-- > 0) { TM1640Send(*data++); } TM1640Stop(); } void TM1640SetBrightness(uint8_t level) { // level can be 0 to 8. // 0 means off // // Other levels are mapped to TM1640 levels 0 ... 7 // The mapping to the PWM level is non-linear, according to the data sheet // level | TM1640 | PWM // 1 | 0 | 1/16 // 2 | 1 | 2/16 // 3 | 2 | 4/16 // 4 | 3 | 10/16 // 5 | 4 | 11/16 // 6 | 5 | 12/16 // 7 | 6 | 13/16 // 8 | 7 | 14/16 if (level) { Tm1640->brightness = (level -1) & 7; } uint8_t cmd = TM1640_CMD_DISPLAY | (level > 0 ? 0x8 : 0) | Tm1640->brightness; TM1640Start(); TM1640Send (cmd); TM1640Stop(); } void TM1640SetPower(int8_t state) { uint8_t level = (1 == state) ? Tm1640->brightness +1 : 0; TM1640SetBrightness(level); } /*********************************************************************************************\ * Init function \*********************************************************************************************/ void TM1640Init(void) { if (PinUsed(GPIO_TM1640CLK) && PinUsed(GPIO_TM1640DIN)) { Tm1640 = (Tm1640_t*)calloc(sizeof(Tm1640_t), 1); // Need calloc to reset registers to 0/false if (nullptr == Tm1640) { return; } Tm1640->clock_pin = Pin(GPIO_TM1640CLK); Tm1640->data_pin = Pin(GPIO_TM1640DIN); pinMode(Tm1640->data_pin, OUTPUT); pinMode(Tm1640->clock_pin, OUTPUT); digitalWrite(Tm1640->clock_pin, HIGH); digitalWrite(Tm1640->data_pin, HIGH); Settings->display_model = XDSP_13; #ifdef USE_IOTTIMER Settings->display_options.type = TM1640_IOTTIMER; Settings->display_cols[0] = 9; // 4 (left) + 2 (lower right) + 3 (upper right). Settings->display_rows = 1; Settings->display_width = Settings->display_cols[0]; Settings->display_height = Settings->display_rows; Tm1640->clock_24 = true; Tm1640->clock_seconds = true; IoTTimerDim(); IoTTimerClearDisplay(); #endif // USE_IOTTIMER AddLog(LOG_LEVEL_INFO, PSTR("DSP: TM1640 with %d digits (type %d)"), Settings->display_width, Settings->display_options.type); } } /*********************************************************************************************\ * IotTimer \*********************************************************************************************/ #ifdef USE_IOTTIMER /* (specifically for its use in the IOTTIMER WiFi clock) The WiFi LED clock called IOTTIMER has the following characteristics: - Controlled by an ESP-12F - Display with four 35 mm (1 12/32 in), two 21 mm (26/32 in), and three 12 mm (~1/2 in), seven-segment LED digits, plus special symbols (alarm, AM/PM) - TM1640B LED controller - R8010 RTC with CR1220 battery - Temperature sensor M1601 - Ambient light sensor (analog voltage) - Buzzer - Three buttons on the backside - USB C port for power supply (only) The TM1640B chip is a controller for a sixteen-digit seven-segment (plus dot) LED display. It is also sometimes used to control a 16 x 8 LED matrix. The controller is controlled through a proprietary two-wire serial interface bearing some similarities with I2C. The two wires are called CLK and DIN. We use two GPIO pins and one-microsecond sleeps to implement the required timing. The wiring of the LEDs in the IOTTIMER clock has been optimized for a simple routing of the traces on the display board. The enumeration of the digit segments is non-standard, but consistent across all digits. The bigger digits have two LEDs per segment, controlled by separate digit lines of the LED controller. From the software perspective, they appear as two layers of four digits each. The brightness of the LEDs can be controlled in seven steps (plus off). In theory, the brightness of the segments with two LEDs could be set in fifteen levels (plus off). To keep things simple and to avoid brightness gradients within segments, both LEDs of a segment will always be set to the same level. The intention of this display driver (together with the drivers for the other components) is to be able to use the IOTTIMER as an alarm clock that can be fully integrated in your home automation using Tasmota and rules. This driver is not a generic TM1640B driver as use cases of the TM1640B in different devices will differ significantly. */ #define IOTTIMER_DIGITS 16 #define IOTTIMER_DOT_BIT 2 static unsigned char IoTTimerDisplay[IOTTIMER_DIGITS]; // Wiring of the LEDs (per digit): // // Seg# Bit Hex // 07 06 40 // 08 01 07 00 80 01 // 02 01 02 // 06 04 05 03 20 08 // 05 03 04 02 10 04 // // Font as per wiring: static const byte IoTTimerFont[128] { //0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x10 // SP ! " # $ % & ' ( ) * + , - . / 0x00, 0xA0, 0x81, 0x00, 0x00, 0x00, 0x00, 0x01, 0xF0, 0x59, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, // 0x20 //0 1 2 3 4 5 6 7 8 9 : ; < = > ? 0xF9, 0x09, 0x73, 0x5B, 0x8B, 0xDA, 0xFA, 0x49, 0xFB, 0xDB, 0x00, 0x00, 0x00, 0x12, 0x00, 0x63, // 0x30 // @ A B C D E F G H I J K L M N O 0x00, 0xEB, 0xBA, 0xF0, 0x3B, 0xF2, 0xE2, 0xFA, 0xAB, 0x09, 0x19, 0x00, 0xB0, 0x00, 0xE9, 0xF9, // 0x40 // P Q R S T U V W X Y Z [ \ ] ^(°) _ 0xE3, 0xAB, 0x22, 0xDA, 0xB2, 0xB9, 0x00, 0x00, 0x00, 0x4B, 0x00, 0xF0, 0x00, 0x59, 0xC3, 0x10, // 0x50 // `=° a b c d e f g h i j k l m n o 0x01, 0x7B, 0xBA, 0x32, 0x3B, 0xF3, 0xE2, 0xDB, 0xAA, 0x08, 0x19, 0x00, 0x09, 0x00, 0x2A, 0x3A, // 0x60 // p q r s t u v w x y z { | } ~ DEL 0xE3, 0xAB, 0x22, 0xDA, 0xB2, 0x38, 0x00, 0x00, 0x00, 0x4B, 0x00, 0x0B, 0x09, 0xA2, 0x00, 0x00 // 0x70 }; void IoTTimerDim(void) { TM1640SetBrightness (changeUIntScale(GetDisplayDimmer(), 0, 100, 0, 8)); } void IoTTimerDisplayOnOff(void) { TM1640SetPower(disp_power); } void IoTTimerClearDisplay (void) { for (int i = 0; i < IOTTIMER_DIGITS; i++) { IoTTimerDisplay[i] = 0; } TM1640SendDataArray(0, IoTTimerDisplay, IOTTIMER_DIGITS); } /*********************************************************************************************\ * Init function \*********************************************************************************************/ void IoTTimerInit(uint8_t mode) { switch(mode) { case DISPLAY_INIT_MODE: IoTTimerDim(); IoTTimerClearDisplay(); break; case DISPLAY_INIT_PARTIAL: case DISPLAY_INIT_FULL: IoTTimerDim(); IoTTimerClearDisplay(); break; } } void IoTTimerDrawStringAt(uint32_t x, uint32_t y, const char *str, uint32_t color = 0, uint32_t flag = 0); void IoTTimerDrawStringAt(uint32_t x, uint32_t y, const char *str, uint32_t color, uint32_t flag) { // displaytext [x] = 0123456789 // Considers display as 1111223334 where 1111 is large white display, // 22 is small white display, // 333 is green display, // 4 is "1" = pm, "2" = alarm, "3" = both // Following also works - notice scattered dot(.), colon(:), minus(-) or plus(+) // displaytext 12:34:56.7.8.9ab - Show all lights including "pm" (=a) and "alarm" (=b) // displaytext 12:34:56 - Show time // displaytext 05-11-24 - Show date // displaytext [x6]12.3 - Show value in green leds // displaytext [ztS] - Clear display and show current time with seconds bool alarm = false; bool pm = false; bool dot_left_up = false; bool dot_left_dn = false; bool dot_right_dn = false; bool dot_right_up_left = false; bool dot_right_up_right = false; bool dash_left = false; bool dash_right = false; char chr; uint32_t idx = x; const char *pos = str; while (*pos) { chr = *pos & 0x7F; // We only support 0 to 127 switch (idx) { case 0: IoTTimerDisplay[12] = IoTTimerDisplay[13] = IoTTimerFont[chr]; // col 0 break; case 1: IoTTimerDisplay[14] = IoTTimerDisplay[15] = IoTTimerFont[chr]; // col 1 break; case 2: if (('.' == chr) || (':' == chr) || ('-' == chr) || ('+' == chr)) { if ('.' == chr) { dot_left_dn = true; } else if (':' == chr) { dot_left_up = true; dot_left_dn = true; } else if ('-' == chr) { dash_left = true; dash_right = true; } else if ('+' == chr) { dot_left_up = true; dot_left_dn = true; dash_left = true; dash_right = true; } idx--; } else { IoTTimerDisplay[4] = IoTTimerDisplay[5] = IoTTimerFont[chr]; // col 2 } break; case 3: IoTTimerDisplay[11] = IoTTimerDisplay[1] = IoTTimerFont[chr]; // col 3 break; case 4: if (('.' == chr) || (':' == chr) || ('-' == chr)) { idx--; // Skip } else { IoTTimerDisplay[6] = IoTTimerFont[chr]; // col 4 } break; case 5: IoTTimerDisplay[7] = IoTTimerFont[chr]; // col 5 break; case 6: if ('.' == chr) { dot_right_dn = true; idx--; } else { // Upper right (green) IoTTimerDisplay[9] = IoTTimerFont[chr]; // col 6 } break; case 7: if ('.' == chr) { dot_right_up_left = true; idx--; } else { IoTTimerDisplay[10] = IoTTimerFont[chr]; // col 7 } break; case 8: if ('.' == chr) { dot_right_up_right = true; idx--; } else { IoTTimerDisplay[8] = IoTTimerFont[chr]; // col 8 } break; case 9: // col 9 if (chr & 0x01) { // 1, A, a pm = true; } if (chr & 0x02) { // 2, B, b alarm = true; } break; } idx++; pos++; } // Dots and dash if (alarm) { IoTTimerDisplay[12] |= 1 << IOTTIMER_DOT_BIT; // Alarm symbol } if (pm) { IoTTimerDisplay[13] |= 1 << IOTTIMER_DOT_BIT; // PM } if (dot_left_up) { IoTTimerDisplay[14] |= 1 << IOTTIMER_DOT_BIT; // Upper dot } if (dot_left_dn) { IoTTimerDisplay[4] |= 1 << IOTTIMER_DOT_BIT; // Lower dot } if (dot_right_dn) { IoTTimerDisplay[7] |= 1 << IOTTIMER_DOT_BIT; // Blue dot } if (dot_right_up_left) { IoTTimerDisplay[10] |= 1 << IOTTIMER_DOT_BIT; // Green dot left } if (dot_right_up_right) { IoTTimerDisplay[8] |= 1 << IOTTIMER_DOT_BIT; // Green dot right } if (dash_left) { IoTTimerDisplay[5] |= 1 << IOTTIMER_DOT_BIT; } if (dash_right) { IoTTimerDisplay[15] |= 1 << IOTTIMER_DOT_BIT; } TM1640SendDataArray(0, IoTTimerDisplay, IOTTIMER_DIGITS); } /*********************************************************************************************\ * Displays floating point number in the upper right sub-display of the IOTTIMER. * Format is always "[n]n[.]n" (negative number is "-n[.]n") \*********************************************************************************************/ void IoTTimerShowFloat(float f) { /* char buffer[16]; ext_snprintf_P(buffer, sizeof(buffer),PSTR("%1_f"), &f); IoTTimerDrawStringAt(6, 0, buffer); */ bool negative = false; float threshold = 99.95; if (f < 0.0) { f = -f; negative = true; threshold = 9.95; } uint8_t precision = 0; if (f < threshold) { f *= 10.0; precision++; } uint32_t n = (uint32_t) (f + 0.5); char buffer[5] = { 0 }; if (negative) { if (n > 99) { n = 99; } buffer[0] = '-'; } else { if (n > 999) { n = 999; } if (n / 100 != 0) { buffer[0] = '0' + n / 100; } } buffer[1] = '0' + n % 100 / 10; uint32_t idx = 2; if (precision == 1) { buffer[2] = '.'; idx++; } buffer[idx] = '0' + n % 10; IoTTimerDrawStringAt(6, 0, buffer); } /*********************************************************************************************\ * Update the temperature in the upper right corner. \*********************************************************************************************/ void IoTTimerUpdateTemperature(void) { IoTTimerShowFloat(ConvertTempToFahrenheit(TasmotaGlobal.temperature_celsius)); } /*********************************************************************************************\ * Adjust the brightness based on the photo diode voltage. \*********************************************************************************************/ void IoTTimerAdjustBrightness(void) { // Max ADC value is 3400 [lx], but that is only reached in direct sun light. // 20 is already quite bright. // Illuminance value of 0 should map to level 1 (level 0 is off) static float filteredLevel = 1.0; float level = (float) AdcGetLux(0) / (20.0 / 7.0) + 1.0; if (level > 8.0) level = 8.0; if (level < 1.0) level = 1.0; filteredLevel = 0.9 * filteredLevel + 0.1 * (float) level; TM1640SetBrightness ((int) (filteredLevel + 0.5)); } #ifdef USE_DISPLAY_MODES1TO5 /*********************************************************************************************\ * Show the current time \*********************************************************************************************/ void IoTTimerShowTime(void) { uint8_t hour = RtcTime.hour; uint8_t min = RtcTime.minute; uint8_t sec = RtcTime.second; uint8_t symbol = 0; if (!Tm1640->clock_24) { if (hour > 12) { hour -= 12; } if (hour == 0) { hour = 12; } symbol |= 1; } char buffer[16]; snprintf_P(buffer, sizeof(buffer), PSTR("%2d:%02d"), hour, min); if (Tm1640->clock_seconds) { snprintf_P(buffer, sizeof(buffer), PSTR("%s:%02d"), buffer, sec); } else { snprintf_P(buffer, sizeof(buffer), PSTR("%s "), buffer); // Erase seconds in case toggling between date/time } IoTTimerDrawStringAt(0, 0, buffer); if (Settings->timer[0].arm) { symbol |= 2; } snprintf_P(buffer, sizeof(buffer), PSTR("%d"), symbol); IoTTimerDrawStringAt(9, 0, buffer); } /*********************************************************************************************\ * Show the current date \*********************************************************************************************/ void IoTTimerShowDate(void) { uint8_t left = RtcTime.day_of_month; uint8_t middle = RtcTime.month; uint8_t right = RtcTime.year % 100; if (!Tm1640->clock_24) { // Use U.S. date format. left = RtcTime.month; middle = RtcTime.day_of_month; } char buffer[16]; snprintf_P(buffer, sizeof(buffer), PSTR("%02d-%02d-%02d"), left, middle, right); IoTTimerDrawStringAt(0, 0, buffer); } void IoTTimerRefresh(void) { // Every second if (!disp_power) { return; } // Update temperature display content: IoTTimerUpdateTemperature(); // Auto-adjust brightness: IoTTimerAdjustBrightness(); if (Settings->display_mode) { // Mode 0 is User text switch (Settings->display_mode) { case 1: // Time IoTTimerShowTime(); break; case 2: // Date IoTTimerShowDate(); break; case 3: // Time/Date if (TasmotaGlobal.uptime % Settings->display_refresh) { IoTTimerShowTime(); } else { IoTTimerShowDate(); } break; case 4: case 5: // not in use break; } } } #endif // USE_DISPLAY_MODES1TO5 /*********************************************************************************************\ * Displays a clock. * Command: DisplayClock 1 // 12-hour format * DisplayClock 2 // 24-hour format * DisplayClock 3 // 12-hour without seconds * DisplayClock 4 // 24-hour without seconds \*********************************************************************************************/ void CmndIoTTimerClock(void) { uint16_t val = XdrvMailbox.payload; if (ArgC() == 0) val = 0; if ((val < 1) || (val > 4)) return; if (val == 1) { Tm1640->show_clock = true; Tm1640->clock_24 = false; Tm1640->clock_seconds = true; } else if (val == 2) { Tm1640->show_clock = true; Tm1640->clock_24 = true; Tm1640->clock_seconds = true; } else if (val == 3) { Tm1640->show_clock = true; Tm1640->clock_24 = false; Tm1640->clock_seconds = false; } else if (val == 4) { Tm1640->show_clock = true; Tm1640->clock_24 = true; Tm1640->clock_seconds = false; } else { Tm1640->show_clock = false; Tm1640->clock_24 = false; } IoTTimerClearDisplay(); } #endif // USE_IOTTIMER /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdsp13(uint32_t function) { bool result = false; if (FUNC_DISPLAY_INIT_DRIVER == function) { TM1640Init(); } else if (Tm1640 && (XDSP_13 == Settings->display_model)) { #ifdef USE_IOTTIMER switch (function) { case FUNC_DISPLAY_INIT: IoTTimerInit(dsp_init); break; #ifdef USE_DISPLAY_MODES1TO5 case FUNC_DISPLAY_EVERY_SECOND: IoTTimerRefresh(); break; #endif // USE_DISPLAY_MODES1TO5 case FUNC_DISPLAY_MODEL: result = true; break; case FUNC_DISPLAY_CLOCK: CmndIoTTimerClock(); break; case FUNC_DISPLAY_CLEAR: IoTTimerClearDisplay(); break; case FUNC_DISPLAY_NUMBER: case FUNC_DISPLAY_NUMBERNC: case FUNC_DISPLAY_FLOAT: case FUNC_DISPLAY_FLOATNC: IoTTimerShowFloat(CharToFloat(XdrvMailbox.data)); break; case FUNC_DISPLAY_DIM: IoTTimerDim(); break; case FUNC_DISPLAY_POWER: IoTTimerDisplayOnOff(); break; case FUNC_DISPLAY_DRAW_STRING: IoTTimerDrawStringAt(dsp_x, dsp_y, dsp_str, dsp_color, dsp_flag); break; } #endif // USE_IOTTIMER } return result; } #endif // USE_DISPLAY_TM1640 #endif // USE_DISPLAY