ESPHome 2026.2.1
Loading...
Searching...
No Matches
pulse_meter_sensor.cpp
Go to the documentation of this file.
2#include <utility>
3#include "esphome/core/log.h"
4
5namespace esphome {
6namespace pulse_meter {
7
8static const char *const TAG = "pulse_meter";
9
11 this->total_pulses_ = pulses;
12 if (this->total_sensor_ != nullptr) {
14 }
15}
16
18 this->pin_->setup();
19 this->isr_pin_ = pin_->to_isr();
20
21 // Set the pin value to the current value to avoid a false edge
22 this->last_pin_val_ = this->pin_->digital_read();
23
24 // Set the last processed edge to now for the first timeout
26
27 if (this->filter_mode_ == FILTER_EDGE) {
29 } else if (this->filter_mode_ == FILTER_PULSE) {
30 // Set the pin value to the current value to avoid a false edge
33 }
34
35 if (this->total_sensor_ != nullptr) {
37 }
38}
39
42
43 {
44 // Lock the interrupt so the interrupt code doesn't interfere with itself
45 InterruptLock lock;
46
47 // Sometimes ESP devices miss interrupts if the edge rises or falls too slowly.
48 // See https://github.com/espressif/arduino-esp32/issues/4172
49 // If the edges are rising too slowly it also implies that the pulse rate is slow.
50 // Therefore the update rate of the loop is likely fast enough to detect the edges.
51 // When the main loop detects an edge that the ISR didn't it will run the ISR functions directly.
52 bool current = this->pin_->digital_read();
53 if (this->filter_mode_ == FILTER_EDGE && current && !this->last_pin_val_) {
55 } else if (this->filter_mode_ == FILTER_PULSE && current != this->last_pin_val_) {
57 }
58 this->last_pin_val_ = current;
59
60 // Get the latest state from the ISR and reset the count in the ISR
61 state.last_detected_edge_us_ = this->state_.last_detected_edge_us_;
63 state.count_ = this->state_.count_;
64 this->state_.count_ = 0;
65 }
66
67 const uint32_t now = micros();
68
69 // If an edge was peeked, repay the debt
70 if (this->peeked_edge_ && state.count_ > 0) {
71 this->peeked_edge_ = false;
72 state.count_--;
73 }
74
75 // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early.
76 // Wait for the debt to be repaid before counting another unprocessed edge early.
77 if (!this->peeked_edge_ && state.last_rising_edge_us_ != state.last_detected_edge_us_ &&
78 now - state.last_rising_edge_us_ >= this->filter_us_) {
79 this->peeked_edge_ = true;
80 state.last_detected_edge_us_ = state.last_rising_edge_us_;
81 state.count_++;
82 }
83
84 // Check if we detected a pulse this loop
85 if (state.count_ > 0) {
86 // Keep a running total of pulses if a total sensor is configured
87 if (this->total_sensor_ != nullptr) {
88 this->total_pulses_ += state.count_;
89 const uint32_t total = this->total_pulses_;
90 this->total_sensor_->publish_state(total);
91 }
92
93 // We need to detect at least two edges to have a valid pulse width
94 switch (this->meter_state_) {
98 } break;
100 uint32_t delta_us = state.last_detected_edge_us_ - this->last_processed_edge_us_;
101 float pulse_width_us = delta_us / float(state.count_);
102 ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, state.count_,
103 pulse_width_us);
104 this->publish_state((60.0f * 1000000.0f) / pulse_width_us);
105 } break;
106 }
107
109 }
110 // No detected edges this loop
111 else {
112 const uint32_t time_since_valid_edge_us = now - this->last_processed_edge_us_;
113
114 switch (this->meter_state_) {
115 // Running and initial states can timeout
117 case MeterState::RUNNING: {
118 if (time_since_valid_edge_us > this->timeout_us_) {
120 ESP_LOGD(TAG, "No pulse detected for %" PRIu32 "s, assuming 0 pulses/min",
121 time_since_valid_edge_us / 1000000);
122 this->publish_state(0.0f);
123 }
124 } break;
125 default:
126 break;
127 }
128 }
129}
130
132 LOG_SENSOR("", "Pulse Meter", this);
133 LOG_PIN(" Pin: ", this->pin_);
134 if (this->filter_mode_ == FILTER_EDGE) {
135 ESP_LOGCONFIG(TAG, " Filtering rising edges less than %" PRIu32 " µs apart", this->filter_us_);
136 } else {
137 ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %" PRIu32 " µs", this->filter_us_);
138 }
139 ESP_LOGCONFIG(TAG, " Assuming 0 pulses/min after not receiving a pulse for %" PRIu32 "s",
140 this->timeout_us_ / 1000000);
141}
142
144 // This is an interrupt handler - we can't call any virtual method from this method
145 // Get the current time before we do anything else so the measurements are consistent
146 const uint32_t now = micros();
147 auto &edge_state = sensor->edge_state_;
148 auto &state = sensor->state_;
149
150 if ((now - edge_state.last_sent_edge_us_) >= sensor->filter_us_) {
151 edge_state.last_sent_edge_us_ = now;
152 state.last_detected_edge_us_ = now;
153 state.last_rising_edge_us_ = now;
154 state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
155 }
156
157 // This ISR is bound to rising edges, so the pin is high
158 sensor->last_pin_val_ = true;
159}
160
162 // This is an interrupt handler - we can't call any virtual method from this method
163 // Get the current time before we do anything else so the measurements are consistent
164 const uint32_t now = micros();
165 const bool pin_val = sensor->isr_pin_.digital_read();
166 auto &pulse_state = sensor->pulse_state_;
167 auto &state = sensor->state_;
168
169 // Filter length has passed since the last interrupt
170 const bool length = now - pulse_state.last_intr_ >= sensor->filter_us_;
171
172 if (length && pulse_state.latched_ && !sensor->last_pin_val_) { // Long enough low edge
173 pulse_state.latched_ = false;
174 } else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge
175 pulse_state.latched_ = true;
176 state.last_detected_edge_us_ = pulse_state.last_intr_;
177 state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
178 }
179
180 // Due to order of operations this includes
181 // length && latched && rising (just reset from a long low edge)
182 // !latched && (rising || high) (noise on the line resetting the potential rising edge)
183 state.last_rising_edge_us_ = !pulse_state.latched_ && pin_val ? now : state.last_detected_edge_us_;
184
185 pulse_state.last_intr_ = now;
186 sensor->last_pin_val_ = pin_val;
187}
188
189} // namespace pulse_meter
190} // namespace esphome
virtual void setup()=0
virtual bool digital_read()=0
void attach_interrupt(void(*func)(T *), T *arg, gpio::InterruptType type) const
Definition gpio.h:107
virtual ISRInternalGPIOPin to_isr() const =0
Helper class to disable interrupts.
Definition helpers.h:1547
bool last_pin_val_
The last pin value seen.
static void edge_intr(PulseMeterSensor *sensor)
static void pulse_intr(PulseMeterSensor *sensor)
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:117
@ INTERRUPT_RISING_EDGE
Definition gpio.h:50
@ INTERRUPT_ANY_EDGE
Definition gpio.h:52
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
uint32_t IRAM_ATTR HOT micros()
Definition core.cpp:27
uint16_t length
Definition tt21100.cpp:0