From 88d8cfe6a2f78c6d18060b9b726da52bc4c07ef9 Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:20:55 +1000 Subject: [PATCH] [tm1651] Remove dependency on Arduino Library (#9645) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Keith Burzinski --- CODEOWNERS | 2 +- esphome/components/tm1651/__init__.py | 93 +++--- esphome/components/tm1651/tm1651.cpp | 268 ++++++++++++++---- esphome/components/tm1651/tm1651.h | 69 ++--- .../components/tm1651/test.esp32-c3-idf.yaml | 1 + tests/components/tm1651/test.esp32-idf.yaml | 1 + 6 files changed, 303 insertions(+), 131 deletions(-) create mode 100644 tests/components/tm1651/test.esp32-c3-idf.yaml create mode 100644 tests/components/tm1651/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index dbd3d2c592..244e204ab6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -472,7 +472,7 @@ esphome/components/tlc5971/* @IJIJI esphome/components/tm1621/* @Philippe12 esphome/components/tm1637/* @glmnet esphome/components/tm1638/* @skykingjwc -esphome/components/tm1651/* @freekode +esphome/components/tm1651/* @mrtoy-me esphome/components/tmp102/* @timsavage esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp117/* @Azimath diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index 153cc690e7..49796d9b42 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -10,26 +10,28 @@ from esphome.const import ( CONF_LEVEL, ) -CODEOWNERS = ["@freekode"] +CODEOWNERS = ["@mrtoy-me"] + +CONF_LEVEL_PERCENT = "level_percent" tm1651_ns = cg.esphome_ns.namespace("tm1651") TM1651Brightness = tm1651_ns.enum("TM1651Brightness") TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component) -SetLevelPercentAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) -SetLevelAction = tm1651_ns.class_("SetLevelAction", automation.Action) SetBrightnessAction = tm1651_ns.class_("SetBrightnessAction", automation.Action) -TurnOnAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) -TurnOffAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) - -CONF_LEVEL_PERCENT = "level_percent" +SetLevelAction = tm1651_ns.class_("SetLevelAction", automation.Action) +SetLevelPercentAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) +TurnOnAction = tm1651_ns.class_("TurnOnAction", automation.Action) +TurnOffAction = tm1651_ns.class_("TurnOffAction", automation.Action) TM1651_BRIGHTNESS_OPTIONS = { - 1: TM1651Brightness.TM1651_BRIGHTNESS_LOW, - 2: TM1651Brightness.TM1651_BRIGHTNESS_MEDIUM, - 3: TM1651Brightness.TM1651_BRIGHTNESS_HIGH, + 1: TM1651Brightness.TM1651_DARKEST, + 2: TM1651Brightness.TM1651_TYPICAL, + 3: TM1651Brightness.TM1651_BRIGHTEST, } +MULTI_CONF = True + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -38,26 +40,21 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, } ), - cv.only_with_arduino, ) -validate_level_percent = cv.All(cv.int_range(min=0, max=100)) -validate_level = cv.All(cv.int_range(min=0, max=7)) -validate_brightness = cv.enum(TM1651_BRIGHTNESS_OPTIONS, int=True) - async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - clk_pin = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) cg.add(var.set_clk_pin(clk_pin)) dio_pin = await cg.gpio_pin_expression(config[CONF_DIO_PIN]) cg.add(var.set_dio_pin(dio_pin)) - # https://platformio.org/lib/show/6865/TM1651 - cg.add_library("freekode/TM1651", "1.0.1") +validate_brightness = cv.enum(TM1651_BRIGHTNESS_OPTIONS, int=True) +validate_level = cv.All(cv.int_range(min=0, max=7)) +validate_level_percent = cv.All(cv.int_range(min=0, max=100)) BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( { @@ -66,38 +63,22 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) -async def output_turn_on_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var - - @automation.register_action( - "tm1651.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA -) -async def output_turn_off_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var - - -@automation.register_action( - "tm1651.set_level_percent", - SetLevelPercentAction, + "tm1651.set_brightness", + SetBrightnessAction, cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(TM1651Display), - cv.Required(CONF_LEVEL_PERCENT): cv.templatable(validate_level_percent), + cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_brightness), }, - key=CONF_LEVEL_PERCENT, + key=CONF_BRIGHTNESS, ), ) -async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): +async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_ = await cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8) - cg.add(var.set_level_percent(template_)) + template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) + cg.add(var.set_brightness(template_)) return var @@ -121,19 +102,35 @@ async def tm1651_set_level_to_code(config, action_id, template_arg, args): @automation.register_action( - "tm1651.set_brightness", - SetBrightnessAction, + "tm1651.set_level_percent", + SetLevelPercentAction, cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(TM1651Display), - cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_brightness), + cv.Required(CONF_LEVEL_PERCENT): cv.templatable(validate_level_percent), }, - key=CONF_BRIGHTNESS, + key=CONF_LEVEL_PERCENT, ), ) -async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): +async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8) + cg.add(var.set_level_percent(template_)) + return var + + +@automation.register_action( + "tm1651.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA +) +async def output_turn_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action("tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +async def output_turn_on_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) - cg.add(var.set_brightness(template_)) return var diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp index 1173bf0e35..15ada0f8ff 100644 --- a/esphome/components/tm1651/tm1651.cpp +++ b/esphome/components/tm1651/tm1651.cpp @@ -1,7 +1,54 @@ -#ifdef USE_ARDUINO +// This Esphome TM1651 component for use with Mini Battery Displays (7 LED levels) +// and removes the Esphome dependency on the TM1651 Arduino library. +// It was largely based on the work of others as set out below. +// @mrtoy-me July 2025 +// ============================================================================================== +// Original Arduino TM1651 library: +// Author:Fred.Chu +// Date:14 August, 2014 +// Applicable Module: Battery Display v1.0 +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// This library 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 +// Lesser General Public License for more details. +// Modified record: +// Author: Detlef Giessmann Germany +// Mail: mydiyp@web.de +// Demo for the new 7 LED Battery-Display 2017 +// IDE: Arduino-1.6.5 +// Type: OPEN-SMART CX10*4RY68 4Color +// Date: 01.05.2017 +// ============================================================================================== +// Esphome component using arduino TM1651 library: +// MIT License +// Copyright (c) 2019 freekode +// ============================================================================================== +// Library and command-line (python) program to control mini battery displays on Raspberry Pi: +// MIT License +// Copyright (c) 2020 Koen Vervloese +// ============================================================================================== +// MIT License +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. #include "tm1651.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -9,84 +56,205 @@ namespace tm1651 { static const char *const TAG = "tm1651.display"; -static const uint8_t MAX_INPUT_LEVEL_PERCENT = 100; -static const uint8_t TM1651_MAX_LEVEL = 7; +static const bool LINE_HIGH = true; +static const bool LINE_LOW = false; -static const uint8_t TM1651_BRIGHTNESS_LOW_HW = 0; -static const uint8_t TM1651_BRIGHTNESS_MEDIUM_HW = 2; -static const uint8_t TM1651_BRIGHTNESS_HIGH_HW = 7; +// TM1651 maximum frequency is 500 kHz (duty ratio 50%) = 2 microseconds / cycle +static const uint8_t CLOCK_CYCLE = 8; + +static const uint8_t HALF_CLOCK_CYCLE = CLOCK_CYCLE / 2; +static const uint8_t QUARTER_CLOCK_CYCLE = CLOCK_CYCLE / 4; + +static const uint8_t ADDR_FIXED = 0x44; // fixed address mode +static const uint8_t ADDR_START = 0xC0; // address of the display register + +static const uint8_t DISPLAY_OFF = 0x80; +static const uint8_t DISPLAY_ON = 0x88; + +static const uint8_t MAX_DISPLAY_LEVELS = 7; + +static const uint8_t PERCENT100 = 100; +static const uint8_t PERCENT50 = 50; + +static const uint8_t TM1651_BRIGHTNESS_DARKEST = 0; +static const uint8_t TM1651_BRIGHTNESS_TYPICAL = 2; +static const uint8_t TM1651_BRIGHTNESS_BRIGHTEST = 7; + +static const uint8_t TM1651_LEVEL_TAB[] = {0b00000000, 0b00000001, 0b00000011, 0b00000111, + 0b00001111, 0b00011111, 0b00111111, 0b01111111}; + +// public void TM1651Display::setup() { - uint8_t clk = clk_pin_->get_pin(); - uint8_t dio = dio_pin_->get_pin(); + this->clk_pin_->setup(); + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); - battery_display_ = make_unique(clk, dio); - battery_display_->init(); - battery_display_->clearDisplay(); + this->dio_pin_->setup(); + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); + + this->brightness_ = TM1651_BRIGHTNESS_TYPICAL; + + // clear display + this->display_level_(); + this->update_brightness_(DISPLAY_ON); } void TM1651Display::dump_config() { - ESP_LOGCONFIG(TAG, "TM1651 Battery Display"); + ESP_LOGCONFIG(TAG, "Battery Display"); LOG_PIN(" CLK: ", clk_pin_); LOG_PIN(" DIO: ", dio_pin_); } -void TM1651Display::set_level_percent(uint8_t new_level) { - this->level_ = calculate_level_(new_level); - this->repaint_(); +void TM1651Display::set_brightness(uint8_t new_brightness) { + this->brightness_ = this->remap_brightness_(new_brightness); + if (this->display_on_) { + this->update_brightness_(DISPLAY_ON); + } } void TM1651Display::set_level(uint8_t new_level) { + if (new_level > MAX_DISPLAY_LEVELS) + new_level = MAX_DISPLAY_LEVELS; this->level_ = new_level; - this->repaint_(); + if (this->display_on_) { + this->display_level_(); + } } -void TM1651Display::set_brightness(uint8_t new_brightness) { - this->brightness_ = calculate_brightness_(new_brightness); - this->repaint_(); -} - -void TM1651Display::turn_on() { - this->is_on_ = true; - this->repaint_(); +void TM1651Display::set_level_percent(uint8_t percentage) { + this->level_ = this->calculate_level_(percentage); + if (this->display_on_) { + this->display_level_(); + } } void TM1651Display::turn_off() { - this->is_on_ = false; - battery_display_->displayLevel(0); + this->display_on_ = false; + this->update_brightness_(DISPLAY_OFF); } -void TM1651Display::repaint_() { - if (!this->is_on_) { - return; - } - - battery_display_->set(this->brightness_); - battery_display_->displayLevel(this->level_); +void TM1651Display::turn_on() { + this->display_on_ = true; + // display level as it could have been changed when display turned off + this->display_level_(); + this->update_brightness_(DISPLAY_ON); } -uint8_t TM1651Display::calculate_level_(uint8_t new_level) { - if (new_level == 0) { - return 0; - } +// protected - float calculated_level = TM1651_MAX_LEVEL / (float) (MAX_INPUT_LEVEL_PERCENT / (float) new_level); - return (uint8_t) roundf(calculated_level); +uint8_t TM1651Display::calculate_level_(uint8_t percentage) { + if (percentage > PERCENT100) + percentage = PERCENT100; + // scale 0-100% to 0-7 display levels + // use integer arithmetic with rounding + uint16_t initial_scaling = (percentage * MAX_DISPLAY_LEVELS) + PERCENT50; + return (uint8_t) (initial_scaling / PERCENT100); } -uint8_t TM1651Display::calculate_brightness_(uint8_t new_brightness) { - if (new_brightness <= 1) { - return TM1651_BRIGHTNESS_LOW_HW; - } else if (new_brightness == 2) { - return TM1651_BRIGHTNESS_MEDIUM_HW; - } else if (new_brightness >= 3) { - return TM1651_BRIGHTNESS_HIGH_HW; +void TM1651Display::display_level_() { + this->start_(); + this->write_byte_(ADDR_FIXED); + this->stop_(); + + this->start_(); + this->write_byte_(ADDR_START); + this->write_byte_(TM1651_LEVEL_TAB[this->level_]); + this->stop_(); +} + +uint8_t TM1651Display::remap_brightness_(uint8_t new_brightness) { + if (new_brightness <= 1) + return TM1651_BRIGHTNESS_DARKEST; + if (new_brightness == 2) + return TM1651_BRIGHTNESS_TYPICAL; + + // new_brightness >= 3 + return TM1651_BRIGHTNESS_BRIGHTEST; +} + +void TM1651Display::update_brightness_(uint8_t on_off_control) { + this->start_(); + this->write_byte_(on_off_control | this->brightness_); + this->stop_(); +} + +// low level functions + +bool TM1651Display::write_byte_(uint8_t data) { + // data bit written to DIO when CLK is low + for (uint8_t i = 0; i < 8; i++) { + this->half_cycle_clock_low_((bool) (data & 0x01)); + this->half_cycle_clock_high_(); + data >>= 1; } - return TM1651_BRIGHTNESS_LOW_HW; + // start 9th cycle, setting DIO high and look for ack + this->half_cycle_clock_low_(LINE_HIGH); + return this->half_cycle_clock_high_ack_(); +} + +void TM1651Display::half_cycle_clock_low_(bool data_bit) { + // first half cycle, clock low and write data bit + this->clk_pin_->digital_write(LINE_LOW); + delayMicroseconds(QUARTER_CLOCK_CYCLE); + + this->dio_pin_->digital_write(data_bit); + delayMicroseconds(QUARTER_CLOCK_CYCLE); +} + +void TM1651Display::half_cycle_clock_high_() { + // second half cycle, clock high + this->clk_pin_->digital_write(LINE_HIGH); + delayMicroseconds(HALF_CLOCK_CYCLE); +} + +bool TM1651Display::half_cycle_clock_high_ack_() { + // second half cycle, clock high and check for ack + this->clk_pin_->digital_write(LINE_HIGH); + delayMicroseconds(QUARTER_CLOCK_CYCLE); + + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); + // valid ack on DIO is low + bool ack = (!this->dio_pin_->digital_read()); + + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); + + // ack should be set DIO low by now + // if its not, set DIO low before the next cycle + if (!ack) { + this->dio_pin_->digital_write(LINE_LOW); + } + delayMicroseconds(QUARTER_CLOCK_CYCLE); + + // begin next cycle + this->clk_pin_->digital_write(LINE_LOW); + + return ack; +} + +void TM1651Display::start_() { + // start data transmission + this->delineate_transmission_(LINE_HIGH); +} + +void TM1651Display::stop_() { + // stop data transmission + this->delineate_transmission_(LINE_LOW); +} + +void TM1651Display::delineate_transmission_(bool dio_state) { + // delineate data transmission + // DIO changes its value while CLK is high + + this->dio_pin_->digital_write(dio_state); + delayMicroseconds(HALF_CLOCK_CYCLE); + + this->clk_pin_->digital_write(LINE_HIGH); + delayMicroseconds(QUARTER_CLOCK_CYCLE); + + this->dio_pin_->digital_write(!dio_state); + delayMicroseconds(QUARTER_CLOCK_CYCLE); } } // namespace tm1651 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index fe7b7d9c6f..7079910adf 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -1,22 +1,16 @@ #pragma once -#ifdef USE_ARDUINO - -#include - +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/core/automation.h" - -#include namespace esphome { namespace tm1651 { enum TM1651Brightness : uint8_t { - TM1651_BRIGHTNESS_LOW = 1, - TM1651_BRIGHTNESS_MEDIUM = 2, - TM1651_BRIGHTNESS_HIGH = 3, + TM1651_DARKEST = 1, + TM1651_TYPICAL = 2, + TM1651_BRIGHTEST = 3, }; class TM1651Display : public Component { @@ -27,36 +21,49 @@ class TM1651Display : public Component { void setup() override; void dump_config() override; - void set_level_percent(uint8_t new_level); - void set_level(uint8_t new_level); void set_brightness(uint8_t new_brightness); void set_brightness(TM1651Brightness new_brightness) { this->set_brightness(static_cast(new_brightness)); } - void turn_on(); + void set_level(uint8_t new_level); + void set_level_percent(uint8_t percentage); + void turn_off(); + void turn_on(); protected: - std::unique_ptr battery_display_; + uint8_t calculate_level_(uint8_t percentage); + void display_level_(); + + uint8_t remap_brightness_(uint8_t new_brightness); + void update_brightness_(uint8_t on_off_control); + + // low level functions + bool write_byte_(uint8_t data); + + void half_cycle_clock_low_(bool data_bit); + void half_cycle_clock_high_(); + bool half_cycle_clock_high_ack_(); + + void start_(); + void stop_(); + + void delineate_transmission_(bool dio_state); + InternalGPIOPin *clk_pin_; InternalGPIOPin *dio_pin_; - bool is_on_ = true; - uint8_t brightness_; - uint8_t level_; - - void repaint_(); - - uint8_t calculate_level_(uint8_t new_level); - uint8_t calculate_brightness_(uint8_t new_brightness); + bool display_on_{true}; + uint8_t brightness_{}; + uint8_t level_{0}; }; -template class SetLevelPercentAction : public Action, public Parented { +template class SetBrightnessAction : public Action, public Parented { public: - TEMPLATABLE_VALUE(uint8_t, level_percent) + TEMPLATABLE_VALUE(uint8_t, brightness) void play(Ts... x) override { - auto level_percent = this->level_percent_.value(x...); - this->parent_->set_level_percent(level_percent); + auto brightness = this->brightness_.value(x...); + this->parent_->set_brightness(brightness); } }; @@ -70,13 +77,13 @@ template class SetLevelAction : public Action, public Par } }; -template class SetBrightnessAction : public Action, public Parented { +template class SetLevelPercentAction : public Action, public Parented { public: - TEMPLATABLE_VALUE(uint8_t, brightness) + TEMPLATABLE_VALUE(uint8_t, level_percent) void play(Ts... x) override { - auto brightness = this->brightness_.value(x...); - this->parent_->set_brightness(brightness); + auto level_percent = this->level_percent_.value(x...); + this->parent_->set_level_percent(level_percent); } }; @@ -92,5 +99,3 @@ template class TurnOffAction : public Action, public Pare } // namespace tm1651 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/tests/components/tm1651/test.esp32-c3-idf.yaml b/tests/components/tm1651/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1651/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1651/test.esp32-idf.yaml b/tests/components/tm1651/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1651/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml