#include "pulse_meter_sensor.h" #include #include "esphome/core/log.h" namespace esphome { namespace pulse_meter { static const char *const TAG = "pulse_meter"; void PulseMeterSensor::set_total_pulses(uint32_t pulses) { this->total_pulses_ = pulses; if (this->total_sensor_ != nullptr) { this->total_sensor_->publish_state(this->total_pulses_); } } void PulseMeterSensor::setup() { this->pin_->setup(); this->isr_pin_ = pin_->to_isr(); // Set the pin value to the current value to avoid a false edge this->last_pin_val_ = this->pin_->digital_read(); // Set the last processed edge to now for the first timeout this->last_processed_edge_us_ = micros(); if (this->filter_mode_ == FILTER_EDGE) { this->pin_->attach_interrupt(PulseMeterSensor::edge_intr, this, gpio::INTERRUPT_RISING_EDGE); } else if (this->filter_mode_ == FILTER_PULSE) { // Set the pin value to the current value to avoid a false edge this->pulse_state_.latched_ = this->last_pin_val_; this->pin_->attach_interrupt(PulseMeterSensor::pulse_intr, this, gpio::INTERRUPT_ANY_EDGE); } if (this->total_sensor_ != nullptr) { this->total_sensor_->publish_state(this->total_pulses_); } } void PulseMeterSensor::loop() { // Reset the count in get before we pass it back to the ISR as set this->get_->count_ = 0; { // Lock the interrupt so the interrupt code doesn't interfere with itself InterruptLock lock; // Sometimes ESP devices miss interrupts if the edge rises or falls too slowly. // See https://github.com/espressif/arduino-esp32/issues/4172 // If the edges are rising too slowly it also implies that the pulse rate is slow. // Therefore the update rate of the loop is likely fast enough to detect the edges. // When the main loop detects an edge that the ISR didn't it will run the ISR functions directly. bool current = this->pin_->digital_read(); if (this->filter_mode_ == FILTER_EDGE && current && !this->last_pin_val_) { PulseMeterSensor::edge_intr(this); } else if (this->filter_mode_ == FILTER_PULSE && current != this->last_pin_val_) { PulseMeterSensor::pulse_intr(this); } this->last_pin_val_ = current; // Swap out set and get to get the latest state from the ISR std::swap(this->set_, this->get_); } const uint32_t now = micros(); // If an edge was peeked, repay the debt if (this->peeked_edge_ && this->get_->count_ > 0) { this->peeked_edge_ = false; this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile) } // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ && now - this->get_->last_rising_edge_us_ >= this->filter_us_) { this->peeked_edge_ = true; this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Check if we detected a pulse this loop if (this->get_->count_ > 0) { // Keep a running total of pulses if a total sensor is configured if (this->total_sensor_ != nullptr) { this->total_pulses_ += this->get_->count_; const uint32_t total = this->total_pulses_; this->total_sensor_->publish_state(total); } // We need to detect at least two edges to have a valid pulse width switch (this->meter_state_) { case MeterState::INITIAL: case MeterState::TIMED_OUT: { this->meter_state_ = MeterState::RUNNING; } break; case MeterState::RUNNING: { uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; float pulse_width_us = delta_us / float(this->get_->count_); ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, this->get_->count_, pulse_width_us); this->publish_state((60.0f * 1000000.0f) / pulse_width_us); } break; } this->last_processed_edge_us_ = this->get_->last_detected_edge_us_; } // No detected edges this loop else { const uint32_t time_since_valid_edge_us = now - this->last_processed_edge_us_; switch (this->meter_state_) { // Running and initial states can timeout case MeterState::INITIAL: case MeterState::RUNNING: { if (time_since_valid_edge_us > this->timeout_us_) { this->meter_state_ = MeterState::TIMED_OUT; ESP_LOGD(TAG, "No pulse detected for %" PRIu32 "s, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); this->publish_state(0.0f); } } break; default: break; } } } float PulseMeterSensor::get_setup_priority() const { return setup_priority::DATA; } void PulseMeterSensor::dump_config() { LOG_SENSOR("", "Pulse Meter", this); LOG_PIN(" Pin: ", this->pin_); if (this->filter_mode_ == FILTER_EDGE) { ESP_LOGCONFIG(TAG, " Filtering rising edges less than %" PRIu32 " µs apart", this->filter_us_); } else { ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %" PRIu32 " µs", this->filter_us_); } ESP_LOGCONFIG(TAG, " Assuming 0 pulses/min after not receiving a pulse for %" PRIu32 "s", this->timeout_us_ / 1000000); } void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { // This is an interrupt handler - we can't call any virtual method from this method // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); auto &state = sensor->edge_state_; auto &set = *sensor->set_; if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) { state.last_sent_edge_us_ = now; set.last_detected_edge_us_ = now; set.last_rising_edge_us_ = now; set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // This ISR is bound to rising edges, so the pin is high sensor->last_pin_val_ = true; } void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { // This is an interrupt handler - we can't call any virtual method from this method // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); const bool pin_val = sensor->isr_pin_.digital_read(); auto &state = sensor->pulse_state_; auto &set = *sensor->set_; // Filter length has passed since the last interrupt const bool length = now - state.last_intr_ >= sensor->filter_us_; if (length && state.latched_ && !sensor->last_pin_val_) { // Long enough low edge state.latched_ = false; } else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge state.latched_ = true; set.last_detected_edge_us_ = state.last_intr_; set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Due to order of operations this includes // length && latched && rising (just reset from a long low edge) // !latched && (rising || high) (noise on the line resetting the potential rising edge) set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_; state.last_intr_ = now; sensor->last_pin_val_ = pin_val; } } // namespace pulse_meter } // namespace esphome