ESPHome 2025.9.0-dev
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
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 int32_t initial_value = 0;
133 switch (this->restore_mode_) {
136 if (!this->rtc_.load(&initial_value)) {
137 initial_value = 0;
138 }
139 break;
141 initial_value = 0;
142 break;
143 }
144 initial_value = clamp(initial_value, this->store_.min_value, this->store_.max_value);
145
146 this->store_.counter = initial_value;
147 this->store_.last_read = initial_value;
148
149 this->pin_a_->setup();
150 this->store_.pin_a = this->pin_a_->to_isr();
151 this->pin_b_->setup();
152 this->store_.pin_b = this->pin_b_->to_isr();
153
154 if (this->pin_i_ != nullptr) {
155 this->pin_i_->setup();
156 }
157
160}
162 LOG_SENSOR("", "Rotary Encoder", this);
163 LOG_PIN(" Pin A: ", this->pin_a_);
164 LOG_PIN(" Pin B: ", this->pin_b_);
165 LOG_PIN(" Pin I: ", this->pin_i_);
166
167 const LogString *restore_mode;
168 switch (this->restore_mode_) {
170 restore_mode = LOG_STR("Restore (Defaults to zero)");
171 break;
173 restore_mode = LOG_STR("Always zero");
174 break;
175 default:
176 restore_mode = LOG_STR("");
177 }
178 ESP_LOGCONFIG(TAG, " Restore Mode: %s", LOG_STR_ARG(restore_mode));
179
180 switch (this->store_.resolution) {
182 ESP_LOGCONFIG(TAG, " Resolution: 1 Pulse Per Cycle");
183 break;
185 ESP_LOGCONFIG(TAG, " Resolution: 2 Pulses Per Cycle");
186 break;
188 ESP_LOGCONFIG(TAG, " Resolution: 4 Pulse Per Cycle");
189 break;
190 }
191}
193 std::array<int8_t, 8> rotation_events;
194 bool rotation_events_overflow;
195 {
196 InterruptLock lock;
197 rotation_events = this->store_.rotation_events;
198 rotation_events_overflow = this->store_.rotation_events_overflow;
199
200 this->store_.rotation_events.fill(0);
201 this->store_.rotation_events_overflow = false;
202 }
203
204 if (rotation_events_overflow) {
205 ESP_LOGW(TAG, "Captured more rotation events than expected");
206 }
207
208 for (auto events : rotation_events) {
209 if (events == 0) // we are at the end of the recorded events
210 break;
211
212 if (events > 0) {
213 while (events--) {
214 this->on_clockwise_callback_.call();
215 }
216 } else {
217 while (events++) {
218 this->on_anticlockwise_callback_.call();
219 }
220 }
221 }
222
223 if (this->pin_i_ != nullptr && this->pin_i_->digital_read()) {
224 this->store_.counter = 0;
225 }
226 int counter = this->store_.counter;
227 if (this->store_.last_read != counter || this->publish_initial_value_) {
229 this->rtc_.save(&counter);
230 }
231 this->store_.last_read = counter;
232 this->publish_state(counter);
233 this->listeners_.call(counter);
234 this->publish_initial_value_ = false;
235 }
236}
237
240 this->restore_mode_ = restore_mode;
241}
243void RotaryEncoderSensor::set_min_value(int32_t min_value) { this->store_.min_value = min_value; }
244void RotaryEncoderSensor::set_max_value(int32_t max_value) { this->store_.max_value = max_value; }
245
246} // namespace rotary_encoder
247} // 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:732
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:45
@ 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:50
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
ESPPreferences * global_preferences
static void gpio_intr(RotaryEncoderSensorStore *arg)
uint16_t x
Definition tt21100.cpp:5