mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge branch 'binary_sensor_gpio_polling' into integration
This commit is contained in:
		| @@ -10,11 +10,24 @@ GPIOBinarySensor = gpio_ns.class_( | ||||
|     "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 = ( | ||||
|     binary_sensor.binary_sensor_schema(GPIOBinarySensor) | ||||
|     .extend( | ||||
|         { | ||||
|             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) | ||||
| @@ -27,3 +40,7 @@ async def to_code(config): | ||||
|  | ||||
|     pin = await cg.gpio_pin_expression(config[CONF_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(INTERRUPT_TYPES[config[CONF_INTERRUPT_TYPE]])) | ||||
|   | ||||
| @@ -6,17 +6,78 @@ namespace gpio { | ||||
|  | ||||
| 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; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type) { | ||||
|   pin->setup(); | ||||
|   this->isr_pin_ = pin->to_isr(); | ||||
|  | ||||
|   // 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() { | ||||
|   this->pin_->setup(); | ||||
|   this->publish_initial_state(this->pin_->digital_read()); | ||||
|   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->publish_initial_state(this->store_.get_state()); | ||||
|   } else { | ||||
|     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_); | ||||
|   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_.has_changed()) { | ||||
|       bool state = this->store_.get_state(); | ||||
|       this->publish_state(state); | ||||
|     } | ||||
|   } else { | ||||
|     this->publish_state(this->pin_->digital_read()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
|  | ||||
|   | ||||
| @@ -2,14 +2,57 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gpio { | ||||
|  | ||||
| // Store class for ISR data (no vtables, ISR-safe) | ||||
| class GPIOBinarySensorStore { | ||||
|  public: | ||||
|   void setup(InternalGPIOPin *pin, gpio::InterruptType type); | ||||
|  | ||||
|   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 has_changed() { | ||||
|     // No lock needed: single writer (ISR) / single reader (main loop) pattern | ||||
|     // Volatile bool operations are atomic on all ESPHome-supported platforms | ||||
|     // | ||||
|     // Note: There's a benign race where ISR could set changed_ = true between | ||||
|     // our read and clear. This is intentional and causes no issues because: | ||||
|     // 1. We'll process the state change on the next loop iteration | ||||
|     // 2. Multiple rapid changes between loop iterations would only result in | ||||
|     //    one update anyway (we only care about the final state) | ||||
|     // 3. This avoids the overhead of atomic operations in the ISR | ||||
|     if (!this->changed_) { | ||||
|       return false; | ||||
|     } | ||||
|     this->changed_ = false; | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   ISRInternalGPIOPin isr_pin_; | ||||
|   volatile bool state_{false}; | ||||
|   volatile bool last_state_{false}; | ||||
|   volatile bool changed_{false}; | ||||
| }; | ||||
|  | ||||
| class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { | ||||
|  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_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; } | ||||
|   void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; } | ||||
|   // ========== INTERNAL METHODS ========== | ||||
|   // (In most use cases you won't need these) | ||||
|   /// Setup pin | ||||
| @@ -22,6 +65,9 @@ class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { | ||||
|  | ||||
|  protected: | ||||
|   GPIOPin *pin_; | ||||
|   bool use_interrupt_{true}; | ||||
|   gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; | ||||
|   GPIOBinarySensorStore store_; | ||||
| }; | ||||
|  | ||||
| }  // namespace gpio | ||||
|   | ||||
		Reference in New Issue
	
	Block a user