mirror of
https://github.com/esphome/esphome.git
synced 2025-07-29 14:46:40 +00:00
Add interrupt support to GPIO binary sensors (#9115)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
687cb1cd2b
commit
2289073a1e
@ -10,11 +10,24 @@ GPIOBinarySensor = gpio_ns.class_(
|
|||||||
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
|
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CONF_USE_INTERRUPT = "use_interrupt"
|
||||||
|
CONF_INTERRUPT_TYPE = "interrupt_type"
|
||||||
|
|
||||||
|
INTERRUPT_TYPES = {
|
||||||
|
"RISING": gpio_ns.INTERRUPT_RISING_EDGE,
|
||||||
|
"FALLING": gpio_ns.INTERRUPT_FALLING_EDGE,
|
||||||
|
"ANY": gpio_ns.INTERRUPT_ANY_EDGE,
|
||||||
|
}
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
CONFIG_SCHEMA = (
|
||||||
binary_sensor.binary_sensor_schema(GPIOBinarySensor)
|
binary_sensor.binary_sensor_schema(GPIOBinarySensor)
|
||||||
.extend(
|
.extend(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
|
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
|
||||||
|
cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
|
||||||
|
INTERRUPT_TYPES, upper=True
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(cv.COMPONENT_SCHEMA)
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
@ -27,3 +40,7 @@ async def to_code(config):
|
|||||||
|
|
||||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||||
cg.add(var.set_pin(pin))
|
cg.add(var.set_pin(pin))
|
||||||
|
|
||||||
|
cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT]))
|
||||||
|
if config[CONF_USE_INTERRUPT]:
|
||||||
|
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||||
|
@ -6,17 +6,91 @@ namespace gpio {
|
|||||||
|
|
||||||
static const char *const TAG = "gpio.binary_sensor";
|
static const char *const TAG = "gpio.binary_sensor";
|
||||||
|
|
||||||
|
void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
|
||||||
|
bool new_state = arg->isr_pin_.digital_read();
|
||||||
|
if (new_state != arg->last_state_) {
|
||||||
|
arg->state_ = new_state;
|
||||||
|
arg->last_state_ = new_state;
|
||||||
|
arg->changed_ = true;
|
||||||
|
// Wake up the component from its disabled loop state
|
||||||
|
if (arg->component_ != nullptr) {
|
||||||
|
arg->component_->enable_loop_soon_any_context();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) {
|
||||||
|
pin->setup();
|
||||||
|
this->isr_pin_ = pin->to_isr();
|
||||||
|
this->component_ = component;
|
||||||
|
|
||||||
|
// Read initial state
|
||||||
|
this->last_state_ = pin->digital_read();
|
||||||
|
this->state_ = this->last_state_;
|
||||||
|
|
||||||
|
// Attach interrupt - from this point on, any changes will be caught by the interrupt
|
||||||
|
pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type);
|
||||||
|
}
|
||||||
|
|
||||||
void GPIOBinarySensor::setup() {
|
void GPIOBinarySensor::setup() {
|
||||||
|
if (this->use_interrupt_ && !this->pin_->is_internal()) {
|
||||||
|
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
|
||||||
|
this->use_interrupt_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->use_interrupt_) {
|
||||||
|
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
|
||||||
|
this->store_.setup(internal_pin, this->interrupt_type_, this);
|
||||||
|
this->publish_initial_state(this->store_.get_state());
|
||||||
|
} else {
|
||||||
this->pin_->setup();
|
this->pin_->setup();
|
||||||
this->publish_initial_state(this->pin_->digital_read());
|
this->publish_initial_state(this->pin_->digital_read());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GPIOBinarySensor::dump_config() {
|
void GPIOBinarySensor::dump_config() {
|
||||||
LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this);
|
LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this);
|
||||||
LOG_PIN(" Pin: ", this->pin_);
|
LOG_PIN(" Pin: ", this->pin_);
|
||||||
|
const char *mode = this->use_interrupt_ ? "interrupt" : "polling";
|
||||||
|
ESP_LOGCONFIG(TAG, " Mode: %s", mode);
|
||||||
|
if (this->use_interrupt_) {
|
||||||
|
const char *interrupt_type;
|
||||||
|
switch (this->interrupt_type_) {
|
||||||
|
case gpio::INTERRUPT_RISING_EDGE:
|
||||||
|
interrupt_type = "RISING_EDGE";
|
||||||
|
break;
|
||||||
|
case gpio::INTERRUPT_FALLING_EDGE:
|
||||||
|
interrupt_type = "FALLING_EDGE";
|
||||||
|
break;
|
||||||
|
case gpio::INTERRUPT_ANY_EDGE:
|
||||||
|
interrupt_type = "ANY_EDGE";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
interrupt_type = "UNKNOWN";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GPIOBinarySensor::loop() { this->publish_state(this->pin_->digital_read()); }
|
void GPIOBinarySensor::loop() {
|
||||||
|
if (this->use_interrupt_) {
|
||||||
|
if (this->store_.is_changed()) {
|
||||||
|
// Clear the flag immediately to minimize the window where we might miss changes
|
||||||
|
this->store_.clear_changed();
|
||||||
|
// Read the state and publish it
|
||||||
|
// Note: If the ISR fires between clear_changed() and get_state(), that's fine -
|
||||||
|
// we'll process the new change on the next loop iteration
|
||||||
|
bool state = this->store_.get_state();
|
||||||
|
this->publish_state(state);
|
||||||
|
} else {
|
||||||
|
// No changes, disable the loop until the next interrupt
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this->publish_state(this->pin_->digital_read());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
|
@ -2,14 +2,51 @@
|
|||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace gpio {
|
namespace gpio {
|
||||||
|
|
||||||
|
// Store class for ISR data (no vtables, ISR-safe)
|
||||||
|
class GPIOBinarySensorStore {
|
||||||
|
public:
|
||||||
|
void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component);
|
||||||
|
|
||||||
|
static void gpio_intr(GPIOBinarySensorStore *arg);
|
||||||
|
|
||||||
|
bool get_state() const {
|
||||||
|
// No lock needed: state_ is atomically updated by ISR
|
||||||
|
// Volatile ensures we read the latest value
|
||||||
|
return this->state_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_changed() const {
|
||||||
|
// Simple read of volatile bool - no clearing here
|
||||||
|
return this->changed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_changed() {
|
||||||
|
// Separate method to clear the flag
|
||||||
|
this->changed_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
ISRInternalGPIOPin isr_pin_;
|
||||||
|
volatile bool state_{false};
|
||||||
|
volatile bool last_state_{false};
|
||||||
|
volatile bool changed_{false};
|
||||||
|
Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context()
|
||||||
|
};
|
||||||
|
|
||||||
class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component {
|
class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component {
|
||||||
public:
|
public:
|
||||||
|
// No destructor needed: ESPHome components are created at boot and live forever.
|
||||||
|
// Interrupts are only detached on reboot when memory is cleared anyway.
|
||||||
|
|
||||||
void set_pin(GPIOPin *pin) { pin_ = pin; }
|
void set_pin(GPIOPin *pin) { pin_ = pin; }
|
||||||
|
void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; }
|
||||||
|
void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; }
|
||||||
// ========== INTERNAL METHODS ==========
|
// ========== INTERNAL METHODS ==========
|
||||||
// (In most use cases you won't need these)
|
// (In most use cases you won't need these)
|
||||||
/// Setup pin
|
/// Setup pin
|
||||||
@ -22,6 +59,9 @@ class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
GPIOPin *pin_;
|
GPIOPin *pin_;
|
||||||
|
bool use_interrupt_{true};
|
||||||
|
gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE};
|
||||||
|
GPIOBinarySensorStore store_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace gpio
|
} // namespace gpio
|
||||||
|
Loading…
x
Reference in New Issue
Block a user