Compare commits

...
7 changed files with 133 additions and 6 deletions
@@ -133,6 +133,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_pin(pin))
if config[CONF_USE_INTERRUPT]:
cg.add_define("USE_GPIO_BINARY_SENSOR_INTERRUPT")
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
else:
cg.add(var.set_use_interrupt(False))
@@ -7,6 +7,7 @@ namespace esphome::gpio {
static const char *const TAG = "gpio.binary_sensor";
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
// Interrupt type strings indexed by edge-triggered InterruptType values:
// indices 1-3: RISING_EDGE, FALLING_EDGE, ANY_EDGE; other values (e.g. level-triggered) map to UNKNOWN (index 0).
PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_EDGE", "ANY_EDGE");
@@ -19,7 +20,9 @@ static const LogString *gpio_mode_to_string(bool use_interrupt) {
return use_interrupt ? LOG_STR("interrupt") : LOG_STR("polling");
}
#endif
#endif
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
bool new_state = arg->isr_pin_.digital_read();
if (new_state != arg->state_) {
@@ -43,28 +46,36 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
// Attach interrupt - from this point on, any changes will be caught by the interrupt
pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, this->interrupt_type_);
}
#endif // USE_GPIO_BINARY_SENSOR_INTERRUPT
void GPIOBinarySensor::setup() {
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
if (this->store_.use_interrupt_) {
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
this->store_.setup(internal_pin, this);
this->publish_initial_state(this->store_.get_state());
} else {
this->pin_->setup();
this->publish_initial_state(this->pin_->digital_read());
return;
}
#endif
this->pin_->setup();
this->publish_initial_state(this->pin_->digital_read());
}
void GPIOBinarySensor::dump_config() {
LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_)));
if (this->store_.use_interrupt_) {
ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->store_.interrupt_type_)));
}
#else
ESP_LOGCONFIG(TAG, " Mode: polling");
#endif
}
void GPIOBinarySensor::loop() {
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
if (this->store_.use_interrupt_) {
if (this->store_.is_changed()) {
// Clear the flag immediately to minimize the window where we might miss changes
@@ -78,9 +89,10 @@ void GPIOBinarySensor::loop() {
// No changes, disable the loop until the next interrupt
this->disable_loop();
}
} else {
this->publish_state(this->pin_->digital_read());
return;
}
#endif
this->publish_state(this->pin_->digital_read());
}
float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
@@ -10,6 +11,7 @@ namespace esphome::gpio {
// Store class for ISR data and configuration (no vtables, ISR-safe)
class GPIOBinarySensorStore {
public:
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
void setup(InternalGPIOPin *pin, Component *component);
static void gpio_intr(GPIOBinarySensorStore *arg);
@@ -29,15 +31,18 @@ class GPIOBinarySensorStore {
// Separate method to clear the flag
this->changed_ = false;
}
#endif
protected:
friend class GPIOBinarySensor;
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
ISRInternalGPIOPin isr_pin_;
Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context()
volatile bool state_{false};
volatile bool changed_{false};
bool use_interrupt_{true};
gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE};
bool use_interrupt_{true};
#endif
};
class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component {
@@ -46,8 +51,14 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon
// Interrupts are only detached on reboot when memory is cleared anyway.
void set_pin(GPIOPin *pin) { this->pin_ = pin; }
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; }
void set_interrupt_type(gpio::InterruptType type) { this->store_.interrupt_type_ = type; }
#else
// Polling-only build: codegen still emits set_use_interrupt(false) calls,
// so keep the setter as an inlined no-op instead of storing the flag.
void set_use_interrupt(bool /*use_interrupt*/) {}
#endif
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Setup pin
+1
View File
@@ -72,6 +72,7 @@
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT
#define USE_FAN
#define USE_GPIO_BINARY_SENSOR_INTERRUPT
#define USE_GPIO_SWITCH_INTERLOCK
#define USE_GRAPH
#define USE_GRAPHICAL_DISPLAY_MENU
@@ -3,10 +3,15 @@
from __future__ import annotations
from collections.abc import Callable
import logging
from pathlib import Path
import pytest
from esphome.core import CORE
INTERRUPT_DEFINE = "USE_GPIO_BINARY_SENSOR_INTERRUPT"
def test_gpio_binary_sensor_basic_setup(
generate_main: Callable[[str | Path], str],
@@ -69,3 +74,62 @@ def test_gpio_binary_sensor_explicit_polling_mode(
)
assert "bs_polling->set_use_interrupt(false);" in main_cpp
def test_gpio_binary_sensor_interrupt_emits_define(
generate_main: Callable[[str | Path], str],
) -> None:
"""
An interrupt-mode sensor must emit the define that compiles the ISR code,
since the platform ISR pin implementation is only built when needed
"""
generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml")
assert INTERRUPT_DEFINE in {d.name for d in CORE.defines}
def test_gpio_binary_sensor_polling_omits_define(
generate_main: Callable[[str | Path], str],
) -> None:
"""
A polling-only config must not emit the interrupt define, so the ISR code
(and its reference to ISRInternalGPIOPin) is compiled out
"""
generate_main("tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml")
assert INTERRUPT_DEFINE not in {d.name for d in CORE.defines}
def test_gpio_binary_sensor_mixed_modes_emit_define(
generate_main: Callable[[str | Path], str],
) -> None:
"""
With one interrupt and one polling sensor, the define is emitted and the
polling instance still opts out via its setter
"""
main_cpp = generate_main(
"tests/component_tests/gpio/test_gpio_binary_sensor_mixed.yaml"
)
assert INTERRUPT_DEFINE in {d.name for d in CORE.defines}
assert "bs_polling->set_use_interrupt(false);" in main_cpp
assert "bs_interrupt->set_use_interrupt" not in main_cpp
def test_gpio_binary_sensor_expander_pin_omits_define(
generate_main: Callable[[str | Path], str],
caplog: pytest.LogCaptureFixture,
) -> None:
"""
An expander pin can't use interrupts: final validation falls back to
polling and the interrupt define must not be emitted. This is the config
that fails to link if the ISR code is compiled without an internal pin
"""
with caplog.at_level(logging.INFO):
main_cpp = generate_main(
"tests/component_tests/gpio/test_gpio_binary_sensor_expander.yaml"
)
assert "bs_expander->set_use_interrupt(false);" in main_cpp
assert INTERRUPT_DEFINE not in {d.name for d in CORE.defines}
assert "falling back to polling mode" in caplog.text
@@ -0,0 +1,21 @@
esphome:
name: test
esp32:
board: esp32dev
i2c:
scl: 16
sda: 17
ch422g:
- id: ch422g_hub
binary_sensor:
- platform: gpio
name: "Expander Sensor"
id: bs_expander
pin:
ch422g: ch422g_hub
number: 1
mode: INPUT
@@ -0,0 +1,17 @@
esphome:
name: test
esp32:
board: esp32dev
binary_sensor:
- platform: gpio
pin: 5
name: "Interrupt Sensor"
id: bs_interrupt
- platform: gpio
pin: 4
name: "Polling Sensor"
id: bs_polling
use_interrupt: false