From 9d15088c02fdf872b696f636485e1c90b437f097 Mon Sep 17 00:00:00 2001 From: Dave Williams | DitroniX | G8PUO <20687856+DitroniX@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:55:24 +0100 Subject: [PATCH] In Review --- components/tca6408/README.md | 77 ++++++++++++++ components/tca6408/__init__.py | 27 +++++ components/tca6408/tca6408.cpp | 179 +++++++++++++++++++++++++++++++++ components/tca6408/tca6408.h | 74 ++++++++++++++ 4 files changed, 357 insertions(+) create mode 100644 components/tca6408/README.md create mode 100644 components/tca6408/__init__.py create mode 100644 components/tca6408/tca6408.cpp create mode 100644 components/tca6408/tca6408.h diff --git a/components/tca6408/README.md b/components/tca6408/README.md new file mode 100644 index 0000000..217331b --- /dev/null +++ b/components/tca6408/README.md @@ -0,0 +1,77 @@ +# TCA6408 ESPHome Component + +Full-featured custom component for the **TCA6408** 8-bit I²C I/O Expander. + +**This is work in progress and under test** + +## Features + +- 8 bidirectional GPIO pins +- Hardware polarity inversion support (inputs) +- Interrupt-driven inputs (active-low INT pin) +- Efficient cached GPIO expander +- Polling fallback mode +- Compatible with ESP32 (including C3/C5/C6), ESP8266, etc. + +The INT pin on TCA6408 is open-drain active-low — connect a pull-up resistor (or use internal pull-up on ESP). + +## Installation + +1. Download or clone this component into your ESPHome project: + ```bash + mkdir -p custom_components + cp -r TCA6408/custom_components/tca6408 custom_components/ +## Basic Configuration + + i2c: + sda: GPIO4 + scl: GPIO5 + scan: true + frequency: 400kHz + + custom_components: + - source: + type: local + path: custom_components/tca6408 + + tca6408: + - id: tca6408_hub + address: 0x20 # Default: 0x20 (0x20–0x27 depending on ADDR pins) + interrupt_pin: GPIO6 # Recommended for fast input response + +## Example: Outputs (Switches) + + switch: + - platform: gpio + name: "TCA6408 Output 0" + pin: + tca6408: tca6408_hub + number: 0 + mode: OUTPUT + + - platform: gpio + name: "TCA6408 Relay (Active Low)" + pin: + tca6408: tca6408_hub + number: 1 + mode: OUTPUT + inverted: true + +## Example: Inputs (Binary Sensors) with Interrupt + + binary_sensor: + - platform: gpio + name: "Door Sensor" + pin: + tca6408: tca6408_hub + number: 3 + mode: INPUT + inverted: true # Uses hardware polarity inversion + + - platform: gpio + name: "Push Button" + pin: + tca6408: tca6408_hub + number: 4 + mode: INPUT + diff --git a/components/tca6408/__init__.py b/components/tca6408/__init__.py new file mode 100644 index 0000000..b7bf571 --- /dev/null +++ b/components/tca6408/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_ADDRESS + +DEPENDENCIES = ["i2c"] + +tca6408_ns = cg.esphome_ns.namespace("tca6408") + +TCA6408Component = tca6408_ns.class_( + "TCA6408Component", + cg.Component, + i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TCA6408Component), + cv.Optional(CONF_ADDRESS, default=0x20): cv.i2c_address, + } +).extend(cv.COMPONENT_SCHEMA) + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + cg.add(var.set_address(config[CONF_ADDRESS])) diff --git a/components/tca6408/tca6408.cpp b/components/tca6408/tca6408.cpp new file mode 100644 index 0000000..e25e34d --- /dev/null +++ b/components/tca6408/tca6408.cpp @@ -0,0 +1,179 @@ +#include "tca6408.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tca6408 { + +static const char *const TAG = "tca6408"; + +// Registers +static const uint8_t REG_INPUT = 0x00; +static const uint8_t REG_OUTPUT = 0x01; +static const uint8_t REG_POLARITY = 0x02; +static const uint8_t REG_CONFIG = 0x03; + +void TCA6408Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TCA6408 @ 0x%02X", this->address_); + + // Default safe state: all inputs + this->mode_mask_ = 0xFF; + this->output_mask_ = 0x00; + this->input_mask_ = 0x00; + this->polarity_mask_ = 0x00; + + if (this->write_register(REG_POLARITY, 0x00) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to init polarity register"); + this->mark_failed(); + return; + } + + if (this->write_register(REG_CONFIG, this->mode_mask_) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to init config register"); + this->mark_failed(); + return; + } + + this->read_inputs_(); + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->attach_interrupt( + &TCA6408Component::gpio_intr, + this, + gpio::INTERRUPT_FALLING_EDGE); + + this->enable_loop(); + ESP_LOGCONFIG(TAG, "Interrupt mode enabled"); + } else { + this->enable_loop(); + ESP_LOGCONFIG(TAG, "Polling mode enabled"); + } + + ESP_LOGCONFIG(TAG, "TCA6408 setup complete"); +} + +void IRAM_ATTR TCA6408Component::gpio_intr(TCA6408Component *arg) { + arg->interrupt_triggered_ = true; + arg->enable_loop_soon_any_context(); +} + +void TCA6408Component::loop() { + if (this->interrupt_triggered_) { + this->interrupt_triggered_ = false; + + this->read_inputs_(); + this->reset_pin_cache_(); + + if (this->interrupt_pin_ && this->interrupt_pin_->digital_read()) { + // INT released -> stop looping until next interrupt + this->disable_loop(); + } + } +} + +void TCA6408Component::dump_config() { + ESP_LOGCONFIG(TAG, "TCA6408:"); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_PIN(" INT Pin: ", this->interrupt_pin_); +} + +bool TCA6408Component::read_inputs_() { + uint8_t data = 0; + + if (this->read_register(REG_INPUT, &data) != i2c::ERROR_OK) { + this->status_set_warning(); + return false; + } + + this->input_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool TCA6408Component::digital_read_hw(uint8_t pin) { + // full refresh for accuracy + return this->read_inputs_(); +} + +bool TCA6408Component::digital_read_cache(uint8_t pin) { + bool value = (this->input_mask_ >> pin) & 0x01; + + if (this->polarity_mask_ & (1 << pin)) { + value = !value; + } + + return value; +} + +void TCA6408Component::digital_write_hw(uint8_t pin, bool value) { + if (value) + this->output_mask_ |= (1 << pin); + else + this->output_mask_ &= ~(1 << pin); + + if (this->write_register(REG_OUTPUT, this->output_mask_) != i2c::ERROR_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } +} + +void TCA6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags & gpio::FLAG_OUTPUT) { + this->mode_mask_ &= ~(1 << pin); + } else { + this->mode_mask_ |= (1 << pin); + } + + this->write_register(REG_CONFIG, this->mode_mask_); +} + +void TCA6408Component::set_polarity_inversion(uint8_t pin, bool inverted) { + if (inverted) + this->polarity_mask_ |= (1 << pin); + else + this->polarity_mask_ &= ~(1 << pin); + + this->write_register(REG_POLARITY, this->polarity_mask_); +} + +// ================= GPIO PIN WRAPPER ================= + +void TCA6408GPIOPin::set_parent(TCA6408Component *parent) { + this->parent_ = parent; +} + +void TCA6408GPIOPin::set_pin(uint8_t pin) { + this->pin_ = pin; +} + +void TCA6408GPIOPin::setup() { + this->pin_mode(this->flags_); +} + +void TCA6408GPIOPin::pin_mode(gpio::Flags flags) { + this->flags_ = flags; + this->parent_->pin_mode(this->pin_, flags); +} + +bool TCA6408GPIOPin::digital_read() { + return this->parent_->digital_read_cache(this->pin_) != this->inverted_; +} + +void TCA6408GPIOPin::digital_write(bool value) { + this->parent_->digital_write_hw(this->pin_, value != this->inverted_); +} + +void TCA6408GPIOPin::set_inverted(bool inverted) { + this->inverted_ = inverted; + this->parent_->set_polarity_inversion(this->pin_, inverted); +} + +size_t TCA6408GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "TCA6408 pin %u", this->pin_); +} + +} // namespace tca6408 +} // namespace esphome diff --git a/components/tca6408/tca6408.h b/components/tca6408/tca6408.h new file mode 100644 index 0000000..eae3cfd --- /dev/null +++ b/components/tca6408/tca6408.h @@ -0,0 +1,74 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tca6408 { + +/** + * TCA6408 8-bit I²C I/O Expander + * + * Supports: + * - 8 bidirectional pins + * - Hardware polarity inversion for inputs + * - Interrupt-driven inputs (active-low open-drain INT) + * - Efficient cached access + */ +class TCA6408Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::IO; } + + void set_address(uint8_t address) { this->address_ = address; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + + void set_polarity_inversion(uint8_t pin, bool inverted); + + protected: + static void IRAM_ATTR gpio_intr(TCA6408Component *arg); + + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + void pin_mode(uint8_t pin, gpio::Flags flags) override; + + uint8_t mode_mask_{0xFF}; // 1 = input, 0 = output + uint8_t output_mask_{0x00}; + uint8_t input_mask_{0x00}; + uint8_t polarity_mask_{0x00}; // Hardware polarity inversion + + InternalGPIOPin *interrupt_pin_{nullptr}; + bool interrupt_triggered_{false}; + + private: + bool read_gpio_outputs_(); +}; + +class TCA6408GPIOPin : public gpio::GPIOPin, public Parented { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + size_t dump_summary(char *buffer, size_t len) const override; + + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted); + void set_flags(gpio::Flags flags) { flags_ = flags; } + gpio::Flags get_flags() const override { return flags_; } + + protected: + uint8_t pin_{0}; + bool inverted_{false}; + gpio::Flags flags_{0}; +}; + +} // namespace tca6408 +} // namespace esphome