From f77118a90cf7e0cad80fd226b59db0a1ca7f7408 Mon Sep 17 00:00:00 2001 From: Philippe FOUQUET Date: Thu, 8 Sep 2022 01:40:27 +0200 Subject: [PATCH] Add support to tm1621 display (#3737) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/tm1621/__init__.py | 1 + esphome/components/tm1621/display.py | 47 +++++ esphome/components/tm1621/tm1621.cpp | 283 ++++++++++++++++++++++++++ esphome/components/tm1621/tm1621.h | 74 +++++++ esphome/const.py | 2 + tests/test1.yaml | 13 ++ 7 files changed, 421 insertions(+) create mode 100644 esphome/components/tm1621/__init__.py create mode 100644 esphome/components/tm1621/display.py create mode 100644 esphome/components/tm1621/tm1621.cpp create mode 100644 esphome/components/tm1621/tm1621.h diff --git a/CODEOWNERS b/CODEOWNERS index ae112b3330..f8efe79c81 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -225,6 +225,7 @@ esphome/components/teleinfo/* @0hax esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter esphome/components/tlc5947/* @rnauber +esphome/components/tm1621/* @Philippe12 esphome/components/tm1637/* @glmnet esphome/components/tmp102/* @timsavage esphome/components/tmp117/* @Azimath diff --git a/esphome/components/tm1621/__init__.py b/esphome/components/tm1621/__init__.py new file mode 100644 index 0000000000..2e88d4f366 --- /dev/null +++ b/esphome/components/tm1621/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Philippe12"] diff --git a/esphome/components/tm1621/display.py b/esphome/components/tm1621/display.py new file mode 100644 index 0000000000..edbc5f6928 --- /dev/null +++ b/esphome/components/tm1621/display.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import ( + CONF_DATA_PIN, + CONF_CS_PIN, + CONF_ID, + CONF_LAMBDA, + CONF_READ_PIN, + CONF_WRITE_PIN, +) + +tm1621_ns = cg.esphome_ns.namespace("tm1621") +TM1621Display = tm1621_ns.class_("TM1621Display", cg.PollingComponent) +TM1621DisplayRef = TM1621Display.operator("ref") + +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TM1621Display), + cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_READ_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_WRITE_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.polling_component_schema("1s")) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await display.register_display(var, config) + + cs = await cg.gpio_pin_expression(config[CONF_CS_PIN]) + cg.add(var.set_cs_pin(cs)) + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + read = await cg.gpio_pin_expression(config[CONF_READ_PIN]) + cg.add(var.set_read_pin(read)) + write = await cg.gpio_pin_expression(config[CONF_WRITE_PIN]) + cg.add(var.set_write_pin(write)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(TM1621DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/tm1621/tm1621.cpp b/esphome/components/tm1621/tm1621.cpp new file mode 100644 index 0000000000..ebaa5a3457 --- /dev/null +++ b/esphome/components/tm1621/tm1621.cpp @@ -0,0 +1,283 @@ +#include "tm1621.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tm1621 { + +static const char *const TAG = "tm1621"; + +const uint8_t TM1621_PULSE_WIDTH = 10; // microseconds (Sonoff = 100) + +const uint8_t TM1621_SYS_EN = 0x01; // 0b00000001 +const uint8_t TM1621_LCD_ON = 0x03; // 0b00000011 +const uint8_t TM1621_TIMER_DIS = 0x04; // 0b00000100 +const uint8_t TM1621_WDT_DIS = 0x05; // 0b00000101 +const uint8_t TM1621_TONE_OFF = 0x08; // 0b00001000 +const uint8_t TM1621_BIAS = 0x29; // 0b00101001 = LCD 1/3 bias 4 commons option +const uint8_t TM1621_IRQ_DIS = 0x80; // 0b100x0xxx + +enum Tm1621Device { TM1621_USER, TM1621_POWR316D, TM1621_THR316D }; + +const uint8_t TM1621_COMMANDS[] = {TM1621_SYS_EN, TM1621_LCD_ON, TM1621_BIAS, TM1621_TIMER_DIS, + TM1621_WDT_DIS, TM1621_TONE_OFF, TM1621_IRQ_DIS}; + +const char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; +// 0 1 2 3 4 5 6 7 8 9 - off +const uint8_t TM1621_DIGIT_ROW[2][12] = {{0x5F, 0x50, 0x3D, 0x79, 0x72, 0x6B, 0x6F, 0x51, 0x7F, 0x7B, 0x20, 0x00}, + {0xF5, 0x05, 0xB6, 0x97, 0x47, 0xD3, 0xF3, 0x85, 0xF7, 0xD7, 0x02, 0x00}}; + +void TM1621Display::setup() { + ESP_LOGCONFIG(TAG, "Setting up TM1621..."); + + this->cs_pin_->setup(); // OUTPUT + this->cs_pin_->digital_write(true); + this->data_pin_->setup(); // OUTPUT + this->data_pin_->digital_write(true); + this->read_pin_->setup(); // OUTPUT + this->read_pin_->digital_write(true); + this->write_pin_->setup(); // OUTPUT + this->write_pin_->digital_write(true); + + this->state_ = 100; + + this->cs_pin_->digital_write(false); + delayMicroseconds(80); + this->read_pin_->digital_write(false); + delayMicroseconds(15); + this->write_pin_->digital_write(false); + delayMicroseconds(25); + this->data_pin_->digital_write(false); + delayMicroseconds(TM1621_PULSE_WIDTH); + this->data_pin_->digital_write(true); + + for (uint8_t tm1621_command : TM1621_COMMANDS) { + this->send_command_(tm1621_command); + } + + this->send_address_(0x00); + for (uint32_t segment = 0; segment < 16; segment++) { + this->send_common_(0); + } + this->stop_(); + + snprintf(this->row_[0], sizeof(this->row_[0]), "----"); + snprintf(this->row_[1], sizeof(this->row_[1]), "----"); + + this->display(); +} +void TM1621Display::dump_config() { + ESP_LOGCONFIG(TAG, "TM1621:"); + LOG_PIN(" CS Pin: ", this->cs_pin_); + LOG_PIN(" DATA Pin: ", this->data_pin_); + LOG_PIN(" READ Pin: ", this->read_pin_); + LOG_PIN(" WRITE Pin: ", this->write_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void TM1621Display::update() { + // memset(this->row, 0, sizeof(this->row)); + if (this->writer_.has_value()) + (*this->writer_)(*this); + this->display(); +} + +float TM1621Display::get_setup_priority() const { return setup_priority::PROCESSOR; } +void TM1621Display::bit_delay_() { delayMicroseconds(100); } + +void TM1621Display::stop_() { + this->cs_pin_->digital_write(true); // Stop command sequence + delayMicroseconds(TM1621_PULSE_WIDTH / 2); + this->data_pin_->digital_write(true); // Reset data +} + +void TM1621Display::display() { + // Tm1621.row[x] = "text", "----", " " or a number with one decimal like "0.4", "237.5", "123456.7" + // "123456.7" will be shown as "9999" being a four digit overflow + + // AddLog(LOG_LEVEL_DEBUG, PSTR("TM1: Row1 '%s', Row2 '%s'"), Tm1621.row[0], Tm1621.row[1]); + + uint8_t buffer[8] = {0}; // TM1621 16-segment 4-bit common buffer + char row[4]; + for (uint32_t j = 0; j < 2; j++) { + // 0.4V => " 04", 0.0A => " ", 1234.5V => "1234" + uint32_t len = strlen(this->row_[j]); + char *dp = nullptr; // Expect number larger than "123" + int row_idx = len - 3; // "1234.5" + if (len <= 5) { // "----", " ", "0.4", "237.5" + dp = strchr(this->row_[j], '.'); + row_idx = len - 1; + } else if (len > 6) { // "12345.6" + snprintf(this->row_[j], sizeof(this->row_[j]), "9999"); + row_idx = 3; + } + row[3] = (row_idx >= 0) ? this->row_[j][row_idx--] : ' '; + if ((row_idx >= 0) && dp) { + row_idx--; + } + row[2] = (row_idx >= 0) ? this->row_[j][row_idx--] : ' '; + row[1] = (row_idx >= 0) ? this->row_[j][row_idx--] : ' '; + row[0] = (row_idx >= 0) ? this->row_[j][row_idx--] : ' '; + + // AddLog(LOG_LEVEL_DEBUG, PSTR("TM1: Dump%d %4_H"), j +1, row); + + char command[10]; + char needle[2] = {0}; + for (uint32_t i = 0; i < 4; i++) { + needle[0] = row[i]; + int index = this->get_command_code_(command, sizeof(command), (const char *) needle, TM1621_KCHAR); + if (-1 == index) { + index = 11; + } + uint32_t bidx = (0 == j) ? i : 7 - i; + buffer[bidx] = TM1621_DIGIT_ROW[j][index]; + } + if (dp) { + if (0 == j) { + buffer[2] |= 0x80; // Row 1 decimal point + } else { + buffer[5] |= 0x08; // Row 2 decimal point + } + } + } + + if (this->fahrenheit_) { + buffer[1] |= 0x80; + } + if (this->celsius_) { + buffer[3] |= 0x80; + } + if (this->kwh_) { + buffer[4] |= 0x08; + } + if (this->humidity_) { + buffer[6] |= 0x08; + } + if (this->voltage_) { + buffer[7] |= 0x08; + } + + // AddLog(LOG_LEVEL_DEBUG, PSTR("TM1: Dump3 %8_H"), buffer); + + this->send_address_(0x10); // Sonoff only uses the upper 16 Segments + for (uint8_t i : buffer) { + this->send_common_(i); + } + this->stop_(); +} + +bool TM1621Display::send_command_(uint16_t command) { + uint16_t full_command = (0x0400 | command) << 5; // 0b100cccccccc00000 + this->cs_pin_->digital_write(false); // Start command sequence + delayMicroseconds(TM1621_PULSE_WIDTH / 2); + for (uint32_t i = 0; i < 12; i++) { + this->write_pin_->digital_write(false); // Start write sequence + if (full_command & 0x8000) { + this->data_pin_->digital_write(true); // Set data + } else { + this->data_pin_->digital_write(false); // Set data + } + delayMicroseconds(TM1621_PULSE_WIDTH); + this->write_pin_->digital_write(true); // Read data + delayMicroseconds(TM1621_PULSE_WIDTH); + full_command <<= 1; + } + this->stop_(); + return true; +} + +bool TM1621Display::send_common_(uint8_t common) { + for (uint32_t i = 0; i < 8; i++) { + this->write_pin_->digital_write(false); // Start write sequence + if (common & 1) { + this->data_pin_->digital_write(true); // Set data + } else { + this->data_pin_->digital_write(false); // Set data + } + delayMicroseconds(TM1621_PULSE_WIDTH); + this->write_pin_->digital_write(true); // Read data + delayMicroseconds(TM1621_PULSE_WIDTH); + common >>= 1; + } + return true; +} + +bool TM1621Display::send_address_(uint16_t address) { + uint16_t full_address = (address | 0x0140) << 7; // 0b101aaaaaa0000000 + this->cs_pin_->digital_write(false); // Start command sequence + delayMicroseconds(TM1621_PULSE_WIDTH / 2); + for (uint32_t i = 0; i < 9; i++) { + this->write_pin_->digital_write(false); // Start write sequence + if (full_address & 0x8000) { + this->data_pin_->digital_write(true); // Set data + } else { + this->data_pin_->digital_write(false); // Set data + } + delayMicroseconds(TM1621_PULSE_WIDTH); + this->write_pin_->digital_write(true); // Read data + delayMicroseconds(TM1621_PULSE_WIDTH); + full_address <<= 1; + } + return true; +} + +uint8_t TM1621Display::print(uint8_t start_pos, const char *str) { + // ESP_LOGD(TAG, "Print at %d: %s", start_pos, str); + return snprintf(this->row_[start_pos], sizeof(this->row_[start_pos]), "%s", str); +} +uint8_t TM1621Display::print(const char *str) { return this->print(0, str); } +uint8_t TM1621Display::printf(uint8_t pos, const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->print(pos, buffer); + return 0; +} +uint8_t TM1621Display::printf(const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->print(buffer); + return 0; +} + +int TM1621Display::get_command_code_(char *destination, size_t destination_size, const char *needle, + const char *haystack) { + // Returns -1 of not found + // Returns index and command if found + int result = -1; + const char *read = haystack; + char *write = destination; + + while (true) { + result++; + size_t size = destination_size - 1; + write = destination; + char ch = '.'; + while ((ch != '\0') && (ch != '|')) { + ch = *(read++); + if (size && (ch != '|')) { + *write++ = ch; + size--; + } + } + *write = '\0'; + if (!strcasecmp(needle, destination)) { + break; + } + if (0 == ch) { + result = -1; + break; + } + } + return result; +} +} // namespace tm1621 +} // namespace esphome diff --git a/esphome/components/tm1621/tm1621.h b/esphome/components/tm1621/tm1621.h new file mode 100644 index 0000000000..b9f330e96e --- /dev/null +++ b/esphome/components/tm1621/tm1621.h @@ -0,0 +1,74 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tm1621 { + +class TM1621Display; + +using tm1621_writer_t = std::function; + +class TM1621Display : public PollingComponent { + public: + void set_writer(tm1621_writer_t &&writer) { this->writer_ = writer; } + + void setup() override; + + void dump_config() override; + + void set_cs_pin(GPIOPin *pin) { cs_pin_ = pin; } + void set_data_pin(GPIOPin *pin) { data_pin_ = pin; } + void set_read_pin(GPIOPin *pin) { read_pin_ = pin; } + void set_write_pin(GPIOPin *pin) { write_pin_ = pin; } + + void display_celsius(bool d) { celsius_ = d; } + void display_fahrenheit(bool d) { fahrenheit_ = d; } + void display_humidity(bool d) { humidity_ = d; } + void display_voltage(bool d) { voltage_ = d; } + void display_kwh(bool d) { kwh_ = d; } + + float get_setup_priority() const override; + + void update() override; + + /// Evaluate the printf-format and print the result at the given position. + uint8_t printf(uint8_t pos, const char *format, ...) __attribute__((format(printf, 3, 4))); + /// Evaluate the printf-format and print the result at position 0. + uint8_t printf(const char *format, ...) __attribute__((format(printf, 2, 3))); + + /// Print `str` at the given position. + uint8_t print(uint8_t pos, const char *str); + /// Print `str` at position 0. + uint8_t print(const char *str); + + void display(); + + protected: + void bit_delay_(); + void setup_pins_(); + bool send_command_(uint16_t command); + bool send_common_(uint8_t common); + bool send_address_(uint16_t address); + void stop_(); + int get_command_code_(char *destination, size_t destination_size, const char *needle, const char *haystack); + + GPIOPin *data_pin_; + GPIOPin *cs_pin_; + GPIOPin *read_pin_; + GPIOPin *write_pin_; + optional writer_{}; + char row_[2][12]; + uint8_t state_; + uint8_t device_; + bool celsius_; + bool fahrenheit_; + bool humidity_; + bool voltage_; + bool kwh_; +}; + +} // namespace tm1621 +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 8245ed26f7..adbd20c620 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -558,6 +558,7 @@ CONF_RAW_DATA_ID = "raw_data_id" CONF_RC_CODE_1 = "rc_code_1" CONF_RC_CODE_2 = "rc_code_2" CONF_REACTIVE_POWER = "reactive_power" +CONF_READ_PIN = "read_pin" CONF_REBOOT_TIMEOUT = "reboot_timeout" CONF_RECEIVE_TIMEOUT = "receive_timeout" CONF_RED = "red" @@ -769,6 +770,7 @@ CONF_WILL_MESSAGE = "will_message" CONF_WIND_DIRECTION_DEGREES = "wind_direction_degrees" CONF_WIND_SPEED = "wind_speed" CONF_WINDOW_SIZE = "window_size" +CONF_WRITE_PIN = "write_pin" CONF_X_GRID = "x_grid" CONF_Y_GRID = "y_grid" CONF_ZERO = "zero" diff --git a/tests/test1.yaml b/tests/test1.yaml index 274b25548b..20209923aa 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -547,8 +547,10 @@ sensor: - platform: dht pin: GPIO26 temperature: + id: dht_temperature name: Living Room Temperature 3 humidity: + id: dht_humidity name: Living Room Humidity 3 model: AM2302 update_interval: 15s @@ -2514,6 +2516,17 @@ display: it.print_sad(true); it.print_bracket(true); it.print_battery(true); + - platform: tm1621 + id: tm1621_display + cs_pin: GPIO17 + data_pin: GPIO5 + read_pin: GPIO23 + write_pin: GPIO18 + lambda: |- + it.printf(0, "%.1f", id(dht_temperature).state); + it.display_celsius(true); + it.printf(1, "%.1f", id(dht_humidity).state); + it.display_humidity(true); tm1651: id: tm1651_battery