ESPHome  2024.4.1
rotary_encoder.cpp
Go to the documentation of this file.
1 #include "rotary_encoder.h"
2 #include "esphome/core/log.h"
3 #include "esphome/core/helpers.h"
4 
5 namespace esphome {
6 namespace rotary_encoder {
7 
8 static const char *const TAG = "rotary_encoder";
9 
10 // based on https://github.com/jkDesignDE/MechInputs/blob/master/QEIx4.cpp
11 static const uint8_t STATE_LUT_MASK = 0x1C; // clears upper counter increment/decrement bits and pin states
12 static const uint16_t STATE_PIN_A_HIGH = 0x01;
13 static const uint16_t STATE_PIN_B_HIGH = 0x02;
14 static const uint16_t STATE_S0 = 0x00;
15 static const uint16_t STATE_S1 = 0x04;
16 static const uint16_t STATE_S2 = 0x08;
17 static const uint16_t STATE_S3 = 0x0C;
18 static const uint16_t STATE_CCW = 0x00;
19 static const uint16_t STATE_CW = 0x10;
20 static const uint16_t STATE_HAS_INCREMENTED = 0x0700;
21 static const uint16_t STATE_INCREMENT_COUNTER_4 = 0x0700;
22 static const uint16_t STATE_INCREMENT_COUNTER_2 = 0x0300;
23 static const uint16_t STATE_INCREMENT_COUNTER_1 = 0x0100;
24 static const uint16_t STATE_HAS_DECREMENTED = 0x7000;
25 static const uint16_t STATE_DECREMENT_COUNTER_4 = 0x7000;
26 static const uint16_t STATE_DECREMENT_COUNTER_2 = 0x3000;
27 static 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
41 static 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  arg->counter++;
98  rotation_dir = 1;
99  }
100  if ((new_state & arg->resolution & STATE_HAS_DECREMENTED) != 0) {
101  if (arg->counter > arg->min_value)
102  arg->counter--;
103  rotation_dir = -1;
104  }
105 
106  if (rotation_dir != 0 && !arg->first_read) {
107  auto *first_zero = std::find(arg->rotation_events.begin(), arg->rotation_events.end(), 0); // find first zero
108  if (first_zero == arg->rotation_events.begin() // are we at the start (first event this loop iteration)
109  || std::signbit(*std::prev(first_zero)) !=
110  std::signbit(rotation_dir) // or is the last stored event the wrong direction
111  || *std::prev(first_zero) == std::numeric_limits<int8_t>::lowest() // or the last event slot is full (negative)
112  || *std::prev(first_zero) == std::numeric_limits<int8_t>::max()) { // or the last event slot is full (positive)
113  if (first_zero != arg->rotation_events.end()) { // we have a free rotation slot
114  *first_zero += rotation_dir; // store the rotation into a new slot
115  } else {
116  arg->rotation_events_overflow = true;
117  }
118  } else {
119  *std::prev(first_zero) += rotation_dir; // store the rotation into the previous slot
120  }
121  }
122  arg->first_read = false;
123 
124  arg->state = new_state;
125 }
126 
128  ESP_LOGCONFIG(TAG, "Setting up Rotary Encoder '%s'...", this->name_.c_str());
129 
130  int32_t initial_value = 0;
131  switch (this->restore_mode_) {
133  this->rtc_ = global_preferences->make_preference<int32_t>(this->get_object_id_hash());
134  if (!this->rtc_.load(&initial_value)) {
135  initial_value = 0;
136  }
137  break;
139  initial_value = 0;
140  break;
141  }
142  initial_value = clamp(initial_value, this->store_.min_value, this->store_.max_value);
143 
144  this->store_.counter = initial_value;
145  this->store_.last_read = initial_value;
146 
147  this->pin_a_->setup();
148  this->store_.pin_a = this->pin_a_->to_isr();
149  this->pin_b_->setup();
150  this->store_.pin_b = this->pin_b_->to_isr();
151 
152  if (this->pin_i_ != nullptr) {
153  this->pin_i_->setup();
154  }
155 
156  this->pin_a_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE);
157  this->pin_b_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE);
158 }
160  LOG_SENSOR("", "Rotary Encoder", this);
161  LOG_PIN(" Pin A: ", this->pin_a_);
162  LOG_PIN(" Pin B: ", this->pin_b_);
163  LOG_PIN(" Pin I: ", this->pin_i_);
164 
165  const LogString *restore_mode = LOG_STR("");
166  switch (this->restore_mode_) {
168  restore_mode = LOG_STR("Restore (Defaults to zero)");
169  break;
171  restore_mode = LOG_STR("Always zero");
172  break;
173  }
174  ESP_LOGCONFIG(TAG, " Restore Mode: %s", LOG_STR_ARG(restore_mode));
175 
176  switch (this->store_.resolution) {
178  ESP_LOGCONFIG(TAG, " Resolution: 1 Pulse Per Cycle");
179  break;
181  ESP_LOGCONFIG(TAG, " Resolution: 2 Pulses Per Cycle");
182  break;
184  ESP_LOGCONFIG(TAG, " Resolution: 4 Pulse Per Cycle");
185  break;
186  }
187 }
189  std::array<int8_t, 8> rotation_events;
191  {
192  InterruptLock lock;
193  rotation_events = this->store_.rotation_events;
194  rotation_events_overflow = this->store_.rotation_events_overflow;
195 
196  this->store_.rotation_events.fill(0);
197  this->store_.rotation_events_overflow = false;
198  }
199 
200  if (rotation_events_overflow) {
201  ESP_LOGW(TAG, "Captured more rotation events than expected");
202  }
203 
204  for (auto events : rotation_events) {
205  if (events == 0) // we are at the end of the recorded events
206  break;
207 
208  if (events > 0) {
209  while (events--) {
210  this->on_clockwise_callback_.call();
211  }
212  } else {
213  while (events++) {
214  this->on_anticlockwise_callback_.call();
215  }
216  }
217  }
218 
219  if (this->pin_i_ != nullptr && this->pin_i_->digital_read()) {
220  this->store_.counter = 0;
221  }
222  int counter = this->store_.counter;
223  if (this->store_.last_read != counter || this->publish_initial_value_) {
224  if (this->restore_mode_ == ROTARY_ENCODER_RESTORE_DEFAULT_ZERO) {
225  this->rtc_.save(&counter);
226  }
227  this->store_.last_read = counter;
228  this->publish_state(counter);
229  this->listeners_.call(counter);
230  this->publish_initial_value_ = false;
231  }
232 }
233 
236  this->restore_mode_ = restore_mode;
237 }
239 void RotaryEncoderSensor::set_min_value(int32_t min_value) { this->store_.min_value = min_value; }
240 void RotaryEncoderSensor::set_max_value(int32_t max_value) { this->store_.max_value = max_value; }
241 
242 } // namespace rotary_encoder
243 } // namespace esphome
RotaryEncoderResolution
All possible resolutions for the rotary encoder.
const float DATA
For components that import data from directly connected sensors like DHT.
Definition: component.cpp:19
increment counter by 2 with every A-B cycle
static void gpio_intr(RotaryEncoderSensorStore *arg)
constexpr const T & clamp(const T &v, const T &lo, const T &hi, Compare comp)
Definition: helpers.h:92
RotaryEncoderRestoreMode
All possible restore modes for the rotary encoder.
try to restore counter, otherwise set to zero
ESPPreferences * global_preferences
BedjetMode mode
BedJet operating mode.
Definition: bedjet_codec.h:151
increment counter by 1 with every A-B cycle, slow response but accurate
Helper class to disable interrupts.
Definition: helpers.h:587
virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)=0
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
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.