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