diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py new file mode 100644 index 0000000000..211d78a9d9 --- /dev/null +++ b/esphome/components/tm1637/display.py @@ -0,0 +1,35 @@ +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_CLK_PIN, CONF_DIO_PIN, CONF_ID, CONF_LAMBDA, CONF_INTENSITY + +tm1637_ns = cg.esphome_ns.namespace('tm1637') +TM1637Display = tm1637_ns.class_('TM1637Display', cg.PollingComponent) +TM1637DisplayRef = TM1637Display.operator('ref') + +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TM1637Display), + + cv.Optional(CONF_INTENSITY, default=7): cv.All(cv.uint8_t, cv.Range(min=0, max=7)), + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, +}).extend(cv.polling_component_schema('1s')) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield display.register_display(var, config) + + clk = yield cg.gpio_pin_expression(config[CONF_CLK_PIN]) + cg.add(var.set_clk_pin(clk)) + dio = yield cg.gpio_pin_expression(config[CONF_DIO_PIN]) + cg.add(var.set_dio_pin(dio)) + + cg.add(var.set_intensity(config[CONF_INTENSITY])) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(TM1637DisplayRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp new file mode 100644 index 0000000000..ebf2fff9d6 --- /dev/null +++ b/esphome/components/tm1637/tm1637.cpp @@ -0,0 +1,299 @@ +#include "tm1637.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tm1637 { + +const char* TAG = "display.tm1637"; +const uint8_t TM1637_I2C_COMM1 = 0x40; +const uint8_t TM1637_I2C_COMM2 = 0xC0; +const uint8_t TM1637_I2C_COMM3 = 0x80; +const uint8_t TM1637_UNKNOWN_CHAR = 0b11111111; + +// +// A +// --- +// F | | B +// -G- +// E | | C +// --- +// D X +// XABCDEFG +const uint8_t TM1637_ASCII_TO_RAW[94] PROGMEM = { + 0b00000000, // ' ', ord 0x20 + 0b10110000, // '!', ord 0x21 + 0b00100010, // '"', ord 0x22 + TM1637_UNKNOWN_CHAR, // '#', ord 0x23 + TM1637_UNKNOWN_CHAR, // '$', ord 0x24 + 0b01001001, // '%', ord 0x25 + TM1637_UNKNOWN_CHAR, // '&', ord 0x26 + 0b00000010, // ''', ord 0x27 + 0b01001110, // '(', ord 0x28 + 0b01111000, // ')', ord 0x29 + 0b01000000, // '*', ord 0x2A + TM1637_UNKNOWN_CHAR, // '+', ord 0x2B + 0b00010000, // ',', ord 0x2C + 0b00000001, // '-', ord 0x2D + 0b10000000, // '.', ord 0x2E + TM1637_UNKNOWN_CHAR, // '/', ord 0x2F + 0b01111110, // '0', ord 0x30 + 0b00110000, // '1', ord 0x31 + 0b01101101, // '2', ord 0x32 + 0b01111001, // '3', ord 0x33 + 0b00110011, // '4', ord 0x34 + 0b01011011, // '5', ord 0x35 + 0b01011111, // '6', ord 0x36 + 0b01110000, // '7', ord 0x37 + 0b01111111, // '8', ord 0x38 + 0b01110011, // '9', ord 0x39 + 0b01001000, // ':', ord 0x3A + 0b01011000, // ';', ord 0x3B + TM1637_UNKNOWN_CHAR, // '<', ord 0x3C + TM1637_UNKNOWN_CHAR, // '=', ord 0x3D + TM1637_UNKNOWN_CHAR, // '>', ord 0x3E + 0b01100101, // '?', ord 0x3F + 0b01101111, // '@', ord 0x40 + 0b01110111, // 'A', ord 0x41 + 0b00011111, // 'B', ord 0x42 + 0b01001110, // 'C', ord 0x43 + 0b00111101, // 'D', ord 0x44 + 0b01001111, // 'E', ord 0x45 + 0b01000111, // 'F', ord 0x46 + 0b01011110, // 'G', ord 0x47 + 0b00110111, // 'H', ord 0x48 + 0b00110000, // 'I', ord 0x49 + 0b00111100, // 'J', ord 0x4A + TM1637_UNKNOWN_CHAR, // 'K', ord 0x4B + 0b00001110, // 'L', ord 0x4C + TM1637_UNKNOWN_CHAR, // 'M', ord 0x4D + 0b00010101, // 'N', ord 0x4E + 0b01111110, // 'O', ord 0x4F + 0b01100111, // 'P', ord 0x50 + 0b11111110, // 'Q', ord 0x51 + 0b00000101, // 'R', ord 0x52 + 0b01011011, // 'S', ord 0x53 + 0b00000111, // 'T', ord 0x54 + 0b00111110, // 'U', ord 0x55 + 0b00111110, // 'V', ord 0x56 + 0b00111111, // 'W', ord 0x57 + TM1637_UNKNOWN_CHAR, // 'X', ord 0x58 + 0b00100111, // 'Y', ord 0x59 + 0b01101101, // 'Z', ord 0x5A + 0b01001110, // '[', ord 0x5B + TM1637_UNKNOWN_CHAR, // '\', ord 0x5C + 0b01111000, // ']', ord 0x5D + TM1637_UNKNOWN_CHAR, // '^', ord 0x5E + 0b00001000, // '_', ord 0x5F + 0b00100000, // '`', ord 0x60 + 0b01110111, // 'a', ord 0x61 + 0b00011111, // 'b', ord 0x62 + 0b00001101, // 'c', ord 0x63 + 0b00111101, // 'd', ord 0x64 + 0b01001111, // 'e', ord 0x65 + 0b01000111, // 'f', ord 0x66 + 0b01011110, // 'g', ord 0x67 + 0b00010111, // 'h', ord 0x68 + 0b00010000, // 'i', ord 0x69 + 0b00111100, // 'j', ord 0x6A + TM1637_UNKNOWN_CHAR, // 'k', ord 0x6B + 0b00001110, // 'l', ord 0x6C + TM1637_UNKNOWN_CHAR, // 'm', ord 0x6D + 0b00010101, // 'n', ord 0x6E + 0b00011101, // 'o', ord 0x6F + 0b01100111, // 'p', ord 0x70 + TM1637_UNKNOWN_CHAR, // 'q', ord 0x71 + 0b00000101, // 'r', ord 0x72 + 0b01011011, // 's', ord 0x73 + 0b00000111, // 't', ord 0x74 + 0b00011100, // 'u', ord 0x75 + 0b00011100, // 'v', ord 0x76 + TM1637_UNKNOWN_CHAR, // 'w', ord 0x77 + TM1637_UNKNOWN_CHAR, // 'x', ord 0x78 + 0b00100111, // 'y', ord 0x79 + TM1637_UNKNOWN_CHAR, // 'z', ord 0x7A + 0b00110001, // '{', ord 0x7B + 0b00000110, // '|', ord 0x7C + 0b00000111, // '}', ord 0x7D +}; +void TM1637Display::setup() { + ESP_LOGCONFIG(TAG, "Setting up TM1637..."); + + this->clk_pin_->setup(); // OUTPUT + this->clk_pin_->digital_write(false); // LOW + this->dio_pin_->setup(); // OUTPUT + this->dio_pin_->digital_write(false); // LOW + + this->display(); +} +void TM1637Display::dump_config() { + ESP_LOGCONFIG(TAG, "TM1637:"); + ESP_LOGCONFIG(TAG, " INTENSITY: %d", this->intensity_); + LOG_PIN(" CLK Pin: ", this->clk_pin_); + LOG_PIN(" DIO Pin: ", this->dio_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void TM1637Display::update() { + for (uint8_t& i : this->buffer_) + i = 0; + if (this->writer_.has_value()) + (*this->writer_)(*this); + this->display(); +} + +float TM1637Display::get_setup_priority() const { return setup_priority::PROCESSOR; } +void TM1637Display::bit_delay_() { delayMicroseconds(100); } +void TM1637Display::start_() { + this->dio_pin_->pin_mode(OUTPUT); + this->bit_delay_(); +} + +void TM1637Display::stop_() { + this->dio_pin_->pin_mode(OUTPUT); + bit_delay_(); + this->clk_pin_->pin_mode(INPUT); + bit_delay_(); + this->dio_pin_->pin_mode(INPUT); + bit_delay_(); +} + +void TM1637Display::display() { + ESP_LOGVV(TAG, "Display %02X%02X%02X%02X", buffer_[0], buffer_[1], buffer_[2], buffer_[3]); + + // Write COMM1 + this->start_(); + this->send_byte_(TM1637_I2C_COMM1); + this->stop_(); + + // Write COMM2 + first digit address + this->start_(); + this->send_byte_(TM1637_I2C_COMM2); + + // Write the data bytes + for (auto b : this->buffer_) { + this->send_byte_(b); + } + + this->stop_(); + + // Write COMM3 + brightness + this->start_(); + this->send_byte_(TM1637_I2C_COMM3 + ((this->intensity_ & 0x7) | 0x08)); + this->stop_(); +} +bool TM1637Display::send_byte_(uint8_t b) { + uint8_t data = b; + + // 8 Data Bits + for (uint8_t i = 0; i < 8; i++) { + // CLK low + this->clk_pin_->pin_mode(OUTPUT); + this->bit_delay_(); + + // Set data bit + if (data & 0x01) + this->dio_pin_->pin_mode(INPUT); + else + this->dio_pin_->pin_mode(OUTPUT); + + this->bit_delay_(); + + // CLK high + this->clk_pin_->pin_mode(INPUT); + this->bit_delay_(); + data = data >> 1; + } + + // Wait for acknowledge + // CLK to zero + this->clk_pin_->pin_mode(OUTPUT); + this->dio_pin_->pin_mode(INPUT); + this->bit_delay_(); + + // CLK to high + this->clk_pin_->pin_mode(INPUT); + this->bit_delay_(); + uint8_t ack = this->dio_pin_->digital_read(); + if (ack == 0) { + this->dio_pin_->pin_mode(OUTPUT); + } + + this->bit_delay_(); + this->clk_pin_->pin_mode(OUTPUT); + this->bit_delay_(); + + return ack; +} + +uint8_t TM1637Display::print(uint8_t start_pos, const char* str) { + ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); + uint8_t pos = start_pos; + for (; *str != '\0'; str++) { + uint8_t data = TM1637_UNKNOWN_CHAR; + if (*str >= ' ' && *str <= '}') + data = pgm_read_byte(&TM1637_ASCII_TO_RAW[*str - ' ']); + + if (data == TM1637_UNKNOWN_CHAR) { + ESP_LOGW(TAG, "Encountered character '%c' with no TM1637 representation while translating string!", *str); + } + // Remap segments, for compatibility with MAX7219 segment definition which is + // XABCDEFG, but TM1637 is // XGFEDCBA + data = ((data & 0x80) ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x1 : 0) | // A + ((data & 0x20) ? 0x2 : 0) | // B + ((data & 0x10) ? 0x4 : 0) | // C + ((data & 0x8) ? 0x8 : 0) | // D + ((data & 0x4) ? 0x10 : 0) | // E + ((data & 0x2) ? 0x20 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G + if (*str == '.') { + if (pos != start_pos) + pos--; + this->buffer_[pos] |= 0b10000000; + } else { + if (pos >= 4) { + ESP_LOGE(TAG, "String is too long for the display!"); + break; + } + this->buffer_[pos] = data; + } + pos++; + } + return pos - start_pos; +} +uint8_t TM1637Display::print(const char* str) { return this->print(0, str); } +uint8_t TM1637Display::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 TM1637Display::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; +} + +#ifdef USE_TIME +uint8_t TM1637Display::strftime(uint8_t pos, const char* format, time::ESPTime time) { + char buffer[64]; + size_t ret = time.strftime(buffer, sizeof(buffer), format); + if (ret > 0) + return this->print(pos, buffer); + return 0; +} +uint8_t TM1637Display::strftime(const char* format, time::ESPTime time) { return this->strftime(0, format, time); } +#endif + +} // namespace tm1637 +} // namespace esphome diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h new file mode 100644 index 0000000000..91e8ba66c0 --- /dev/null +++ b/esphome/components/tm1637/tm1637.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/esphal.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace tm1637 { + +class TM1637Display; + +using tm1637_writer_t = std::function; + +class TM1637Display : public PollingComponent { + public: + void set_writer(tm1637_writer_t &&writer) { this->writer_ = writer; } + + void setup() override; + + void dump_config() override; + + void set_clk_pin(GPIOPin *pin) { clk_pin_ = pin; } + void set_dio_pin(GPIOPin *pin) { dio_pin_ = pin; } + + 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 set_intensity(uint8_t intensity) { this->intensity_ = intensity; } + + void display(); + +#ifdef USE_TIME + /// Evaluate the strftime-format and print the result at the given position. + uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + + /// Evaluate the strftime-format and print the result at position 0. + uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); +#endif + + protected: + void bit_delay_(); + void setup_pins_(); + bool send_byte_(uint8_t b); + void start_(); + void stop_(); + + GPIOPin *dio_pin_; + GPIOPin *clk_pin_; + uint8_t intensity_; + optional writer_{}; + uint8_t buffer_[4] = {0}; +}; + +} // namespace tm1637 +} // namespace esphome diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index 1c49287878..d83ef4b3b7 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation -from esphome.const import CONF_ID, CONF_CLK_PIN, CONF_LEVEL, CONF_BRIGHTNESS +from esphome.const import CONF_ID, CONF_CLK_PIN, CONF_DIO_PIN, CONF_LEVEL, CONF_BRIGHTNESS tm1651_ns = cg.esphome_ns.namespace('tm1651') TM1651Display = tm1651_ns.class_('TM1651Display', cg.Component) @@ -15,8 +15,6 @@ TM1651_BRIGHTNESS_OPTIONS = { 3: TM1651Display.TM1651_BRIGHTNESS_HIGH } -CONF_DIO_PIN = 'dio_pin' - CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(TM1651Display), cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, diff --git a/esphome/const.py b/esphome/const.py index f287a15734..0c5f286f30 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -125,6 +125,7 @@ CONF_DELTA = 'delta' CONF_DEVICE = 'device' CONF_DEVICE_CLASS = 'device_class' CONF_DIMENSIONS = 'dimensions' +CONF_DIO_PIN = 'dio_pin' CONF_DIR_PIN = 'dir_pin' CONF_DIRECTION = 'direction' CONF_DISCOVERY = 'discovery' diff --git a/tests/test1.yaml b/tests/test1.yaml index f0de50d9b6..fb15bc0a6b 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1426,6 +1426,12 @@ display: num_chips: 1 lambda: |- it.print("01234567"); +- platform: tm1637 + clk_pin: GPIO23 + dio_pin: GPIO25 + intensity: 3 + lambda: |- + it.print("1234"); - platform: nextion lambda: |- it.set_component_value("gauge", 50);