ESPHome 2025.5.0
Loading...
Searching...
No Matches
rotary_encoder.cpp
Go to the documentation of this file.
1#include "rotary_encoder.h"
2#include "esphome/core/log.h"
4
5namespace esphome {
6namespace rotary_encoder {
7
8static const char *const TAG = "rotary_encoder";
9
10// based on https://github.com/jkDesignDE/MechInputs/blob/master/QEIx4.cpp
11static const uint8_t STATE_LUT_MASK = 0x1C; // clears upper counter increment/decrement bits and pin states
12static const uint16_t STATE_PIN_A_HIGH = 0x01;
13static const uint16_t STATE_PIN_B_HIGH = 0x02;
14static const uint16_t STATE_S0 = 0x00;
15static const uint16_t STATE_S1 = 0x04;
16static const uint16_t STATE_S2 = 0x08;
17static const uint16_t STATE_S3 = 0x0C;
18static const uint16_t STATE_CCW = 0x00;
19static const uint16_t STATE_CW = 0x10;
20static const uint16_t STATE_HAS_INCREMENTED = 0x0700;
21static const uint16_t STATE_INCREMENT_COUNTER_4 = 0x0700;
22static const uint16_t STATE_INCREMENT_COUNTER_2 = 0x0300;
23static const uint16_t STATE_INCREMENT_COUNTER_1 = 0x0100;
24static const uint16_t STATE_HAS_DECREMENTED = 0x7000;
25static const uint16_t STATE_DECREMENT_COUNTER_4 = 0x7000;
26static const uint16_t STATE_DECREMENT_COUNTER_2 = 0x3000;
27static const uint16_t STATE_DECREMENT_COUNTER_1 = 0x1000;
28
29// State explanation: 8-bit uint
30// Bit 0 (0x01) encodes Pin A HIGH/LOW (reset before each read)
31// Bit 1 (0x02) encodes Pin B HIGH/LOW (reset before each read)
32// Bit 2&3 (0x0C) encodes state S0-S3
33// Bit 4 (0x10) encodes clockwise/counter-clockwise rotation
34
35// Only apply if DRAM_ATTR exists on this platform (exists on ESP32, not on ESP8266)
36#ifndef DRAM_ATTR
37#define DRAM_ATTR
38#endif
39// array needs to be placed in .dram1 for ESP32
40// otherwise it will automatically go into flash, and cause cache disabled issues
41static const uint16_t DRAM_ATTR STATE_LOOKUP_TABLE[32] = {
42 // act state S0 in CCW direction
43 STATE_CCW | STATE_S0, // 0x00: stay here
44 STATE_CW | STATE_S1 | STATE_INCREMENT_COUNTER_1, // 0x01: goto CW+S1 and increment counter (dir change)
45 STATE_CCW | STATE_S0, // 0x02: stay here
46 STATE_CCW | STATE_S3 | STATE_DECREMENT_COUNTER_4, // 0x03: goto CCW+S3 and decrement counter
47 // act state S1 in CCW direction
48 STATE_CCW | STATE_S1, // 0x04: stay here
49 STATE_CCW | STATE_S1, // 0x05: stay here
50 STATE_CCW | STATE_S0 | STATE_DECREMENT_COUNTER_1, // 0x06: goto CCW+S0 and decrement counter
51 STATE_CW | STATE_S2 | STATE_INCREMENT_COUNTER_4, // 0x07: goto CW+S2 and increment counter (dir change)
52 // act state S2 in CCW direction
53 STATE_CCW | STATE_S1 | STATE_DECREMENT_COUNTER_2, // 0x08: goto CCW+S1 and decrement counter
54 STATE_CCW | STATE_S2, // 0x09: stay here
55 STATE_CW | STATE_S3 | STATE_INCREMENT_COUNTER_1, // 0x0A: goto CW+S3 and increment counter (dir change)
56 STATE_CCW | STATE_S2, // 0x0B: stay here
57 // act state S3 in CCW direction
58 STATE_CW | STATE_S0 | STATE_INCREMENT_COUNTER_2, // 0x0C: goto CW+S0 and increment counter (dir change)
59 STATE_CCW | STATE_S2 | STATE_DECREMENT_COUNTER_1, // 0x0D: goto CCW+S2 and decrement counter
60 STATE_CCW | STATE_S3, // 0x0E: stay here
61 STATE_CCW | STATE_S3, // 0x0F: stay here
62
63 // act state S0 in CW direction
64 STATE_CW | STATE_S0, // 0x10: stay here
65 STATE_CW | STATE_S1 | STATE_INCREMENT_COUNTER_1, // 0x11: goto CW+S1 and increment counter
66 STATE_CW | STATE_S0, // 0x12: stay here
67 STATE_CCW | STATE_S3 | STATE_DECREMENT_COUNTER_4, // 0x13: goto CCW+S3 and decrement counter (dir change)
68 // act state S1 in CW direction
69 STATE_CW | STATE_S1, // 0x14: stay here
70 STATE_CW | STATE_S1, // 0x15: stay here
71 STATE_CCW | STATE_S0 | STATE_DECREMENT_COUNTER_1, // 0x16: goto CCW+S0 and decrement counter (dir change)
72 STATE_CW | STATE_S2 | STATE_INCREMENT_COUNTER_4, // 0x17: goto CW+S2 and increment counter
73 // act state S2 in CW direction
74 STATE_CCW | STATE_S1 | STATE_DECREMENT_COUNTER_2, // 0x18: goto CCW+S1 and decrement counter (dir change)
75 STATE_CW | STATE_S2, // 0x19: stay here
76 STATE_CW | STATE_S3 | STATE_INCREMENT_COUNTER_1, // 0x1A: goto CW+S3 and increment counter
77 STATE_CW | STATE_S2,
78 // act state S3 in CW direction
79 STATE_CW | STATE_S0 | STATE_INCREMENT_COUNTER_2, // 0x1C: goto CW+S0 and increment counter
80 STATE_CCW | STATE_S2 | STATE_DECREMENT_COUNTER_1, // 0x1D: goto CCW+S2 and decrement counter (dir change)
81 STATE_CW | STATE_S3, // 0x1E: stay here
82 STATE_CW | STATE_S3 // 0x1F: stay here
83};
84
86 // Forget upper bits and add pin states
87 uint8_t input_state = arg->state & STATE_LUT_MASK;
88 if (arg->pin_a.digital_read())
89 input_state |= STATE_PIN_A_HIGH;
90 if (arg->pin_b.digital_read())
91 input_state |= STATE_PIN_B_HIGH;
92
93 int8_t rotation_dir = 0;
94 uint16_t new_state = STATE_LOOKUP_TABLE[input_state];
95 if ((new_state & arg->resolution & STATE_HAS_INCREMENTED) != 0) {
96 if (arg->counter < arg->max_value) {
97 auto x = arg->counter + 1;
98 arg->counter = x;
99 }
100 rotation_dir = 1;
101 }
102 if ((new_state & arg->resolution & STATE_HAS_DECREMENTED) != 0) {
103 if (arg->counter > arg->min_value) {
104 auto x = arg->counter - 1;
105 arg->counter = x;
106 }
107 rotation_dir = -1;
108 }
109
110 if (rotation_dir != 0 && !arg->first_read) {
111 auto *first_zero = std::find(arg->rotation_events.begin(), arg->rotation_events.end(), 0); // find first zero
112 if (first_zero == arg->rotation_events.begin() // are we at the start (first event this loop iteration)
113 || std::signbit(*std::prev(first_zero)) !=
114 std::signbit(rotation_dir) // or is the last stored event the wrong direction
115 || *std::prev(first_zero) == std::numeric_limits<int8_t>::lowest() // or the last event slot is full (negative)
116 || *std::prev(first_zero) == std::numeric_limits<int8_t>::max()) { // or the last event slot is full (positive)
117 if (first_zero != arg->rotation_events.end()) { // we have a free rotation slot
118 *first_zero += rotation_dir; // store the rotation into a new slot
119 } else {
120 arg->rotation_events_overflow = true;
121 }
122 } else {
123 *std::prev(first_zero) += rotation_dir; // store the rotation into the previous slot
124 }
125 }
126 arg->first_read = false;
127
128 arg->state = new_state;
129}
130
132 ESP_LOGCONFIG(TAG, "Setting up Rotary Encoder '%s'...", this->name_.c_str());
133
134 int32_t initial_value = 0;
135 switch (this->restore_mode_) {
138 if (!this->rtc_.load(&initial_value)) {
139 initial_value = 0;
140 }
141 break;
143 initial_value = 0;
144 break;
145 }
146 initial_value = clamp(initial_value, this->store_.min_value, this->store_.max_value);
147
148 this->store_.counter = initial_value;
149 this->store_.last_read = initial_value;
150
151 this->pin_a_->setup();
152 this->store_.pin_a = this->pin_a_->to_isr();
153 this->pin_b_->setup();
154 this->store_.pin_b = this->pin_b_->to_isr();
155
156 if (this->pin_i_ != nullptr) {
157 this->pin_i_->setup();
158 }
159
162}
164 LOG_SENSOR("", "Rotary Encoder", this);
165 LOG_PIN(" Pin A: ", this->pin_a_);
166 LOG_PIN(" Pin B: ", this->pin_b_);
167 LOG_PIN(" Pin I: ", this->pin_i_);
168
169 const LogString *restore_mode;
170 switch (this->restore_mode_) {
172 restore_mode = LOG_STR("Restore (Defaults to zero)");
173 break;
175 restore_mode = LOG_STR("Always zero");
176 break;
177 default:
178 restore_mode = LOG_STR("");
179 }
180 ESP_LOGCONFIG(TAG, " Restore Mode: %s", LOG_STR_ARG(restore_mode));
181
182 switch (this->store_.resolution) {
184 ESP_LOGCONFIG(TAG, " Resolution: 1 Pulse Per Cycle");
185 break;
187 ESP_LOGCONFIG(TAG, " Resolution: 2 Pulses Per Cycle");
188 break;
190 ESP_LOGCONFIG(TAG, " Resolution: 4 Pulse Per Cycle");
191 break;
192 }
193}
195 std::array<int8_t, 8> rotation_events;
196 bool rotation_events_overflow;
197 {
198 InterruptLock lock;
199 rotation_events = this->store_.rotation_events;
200 rotation_events_overflow = this->store_.rotation_events_overflow;
201
202 this->store_.rotation_events.fill(0);
203 this->store_.rotation_events_overflow = false;
204 }
205
206 if (rotation_events_overflow) {
207 ESP_LOGW(TAG, "Captured more rotation events than expected");
208 }
209
210 for (auto events : rotation_events) {
211 if (events == 0) // we are at the end of the recorded events
212 break;
213
214 if (events > 0) {
215 while (events--) {
216 this->on_clockwise_callback_.call();
217 }
218 } else {
219 while (events++) {
220 this->on_anticlockwise_callback_.call();
221 }
222 }
223 }
224
225 if (this->pin_i_ != nullptr && this->pin_i_->digital_read()) {
226 this->store_.counter = 0;
227 }
228 int counter = this->store_.counter;
229 if (this->store_.last_read != counter || this->publish_initial_value_) {
231 this->rtc_.save(&counter);
232 }
233 this->store_.last_read = counter;
234 this->publish_state(counter);
235 this->listeners_.call(counter);
236 this->publish_initial_value_ = false;
237 }
238}
239
242 this->restore_mode_ = restore_mode;
243}
245void RotaryEncoderSensor::set_min_value(int32_t min_value) { this->store_.min_value = min_value; }
246void RotaryEncoderSensor::set_max_value(int32_t max_value) { this->store_.max_value = max_value; }
247
248} // namespace rotary_encoder
249} // namespace esphome
BedjetMode mode
BedJet operating mode.
bool save(const T *src)
Definition preferences.h:21
virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)=0
uint32_t get_object_id_hash()
virtual void setup()=0
virtual bool digital_read()=0
void attach_interrupt(void(*func)(T *), T *arg, gpio::InterruptType type) const
Definition gpio.h:88
virtual ISRInternalGPIOPin to_isr() const =0
Helper class to disable interrupts.
Definition helpers.h:614
constexpr const char * c_str() const
Definition string_ref.h:68
CallbackManager< void()> on_anticlockwise_callback_
CallbackManager< void(int32_t)> listeners_
bool publish_initial_value_
Index pin, if this is not nullptr, the counter will reset to 0 once this pin is HIGH.
void set_resolution(RotaryEncoderResolution mode)
Set the resolution of the rotary encoder.
void set_restore_mode(RotaryEncoderRestoreMode restore_mode)
Set the restore mode of the rotary encoder.
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:39
@ INTERRUPT_ANY_EDGE
Definition gpio.h:43
RotaryEncoderResolution
All possible resolutions for the rotary encoder.
@ ROTARY_ENCODER_2_PULSES_PER_CYCLE
increment counter by 1 with every A-B cycle, slow response but accurate
@ ROTARY_ENCODER_4_PULSES_PER_CYCLE
increment counter by 2 with every A-B cycle
RotaryEncoderRestoreMode
All possible restore modes for the rotary encoder.
@ ROTARY_ENCODER_ALWAYS_ZERO
try to restore counter, otherwise set to zero
const float DATA
For components that import data from directly connected sensors like DHT.
Definition component.cpp:19
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
ESPPreferences * global_preferences
constexpr const T & clamp(const T &v, const T &lo, const T &hi, Compare comp)
Definition helpers.h:101
static void gpio_intr(RotaryEncoderSensorStore *arg)
uint16_t x
Definition tt21100.cpp:5