LSH-Core
Deterministic firmware core for Controllino-based Labo Smart Home nodes
 
Loading...
Searching...
No Matches
clickable.hpp
Go to the documentation of this file.
1
21#ifndef LSH_CORE_PERIPHERALS_INPUT_CLICKABLE_HPP
22#define LSH_CORE_PERIPHERALS_INPUT_CLICKABLE_HPP
23
24#include <stdint.h>
25
27#include "internal/pin_tag.hpp"
29#ifdef CONFIG_USE_FAST_CLICKABLES
31#endif
36
42{
43private:
44 static constexpr uint8_t CLICKABLE_FLAG_STABLE_PRESSED = 0x01U;
45 static constexpr uint8_t CLICKABLE_FLAG_CANDIDATE_PRESSED = 0x02U;
46 static constexpr uint8_t CLICKABLE_FLAG_DEBOUNCING = 0x04U;
47 static constexpr uint8_t CLICKABLE_FLAG_LONG_FIRED = 0x08U;
48 static constexpr uint8_t CLICKABLE_FLAG_SUPER_LONG_FIRED = 0x10U;
49
50#ifndef CONFIG_USE_FAST_CLICKABLES
51 const uint8_t pinNumber;
52#else
53 const uint8_t pinMask;
54 const volatile uint8_t *const pinPort;
55
64 explicit Clickable(lsh::core::avr::FastInputPinBinding binding) noexcept : pinMask(binding.mask), pinPort(binding.pinPort)
65 {
66 // lsh-core expects externally pulled-down buttons. Explicitly disabling
67 // the AVR pull-up avoids inheriting a previous firmware's PORT latch
68 // state before the scan loop starts sampling the direct input register.
69 const uint8_t oldSREG = SREG;
70 cli();
71 *binding.outputPort &= ~this->pinMask;
72 *binding.modePort &= ~this->pinMask;
73 SREG = oldSREG;
74 }
75#endif
76 uint16_t pressAge_ms = 0U;
77 uint8_t debounceAge_ms = 0U;
78#if defined(LSH_DEBUG) || defined(LSH_STATIC_CONFIG_RUNTIME_CHECKS)
79 uint8_t index = UINT8_MAX;
80#endif
81 uint8_t flags = 0U;
82
83 [[nodiscard]] auto hasClickableFlag(uint8_t flag) const noexcept -> bool
84 {
85 return (this->flags & flag) != 0U;
86 }
87
88 void setClickableFlag(uint8_t flag, bool enabled) noexcept
89 {
90 if (enabled)
91 {
92 this->flags |= flag;
93 }
94 else
95 {
96 this->flags &= static_cast<uint8_t>(~flag);
97 }
98 }
99
100 [[nodiscard]] auto stablePressed() const noexcept -> bool
101 {
102 return this->hasClickableFlag(CLICKABLE_FLAG_STABLE_PRESSED);
103 }
104
105 [[nodiscard]] auto candidatePressed() const noexcept -> bool
106 {
107 return this->hasClickableFlag(CLICKABLE_FLAG_CANDIDATE_PRESSED);
108 }
109
110 [[nodiscard]] auto isDebouncing() const noexcept -> bool
111 {
112 return this->hasClickableFlag(CLICKABLE_FLAG_DEBOUNCING);
113 }
114
115 [[nodiscard]] auto releaseDebouncePending() const noexcept -> bool
116 {
117 return this->isDebouncing() && this->stablePressed() && !this->candidatePressed();
118 }
119
120 void clearTimedActionFlags() noexcept
121 {
122 this->flags &= static_cast<uint8_t>(~(CLICKABLE_FLAG_LONG_FIRED | CLICKABLE_FLAG_SUPER_LONG_FIRED));
123 }
124
125 void startDebounce(bool candidatePressed) noexcept
126 {
127 this->setClickableFlag(CLICKABLE_FLAG_DEBOUNCING, true);
128 this->setClickableFlag(CLICKABLE_FLAG_CANDIDATE_PRESSED, candidatePressed);
129 this->debounceAge_ms = 0U;
130 }
131
132 [[nodiscard]] auto confirmDebouncedEdge(bool pressed) noexcept -> constants::ClickResult
133 {
134 this->setClickableFlag(CLICKABLE_FLAG_DEBOUNCING, false);
135 this->debounceAge_ms = 0U;
136 this->setClickableFlag(CLICKABLE_FLAG_STABLE_PRESSED, pressed);
137 this->setClickableFlag(CLICKABLE_FLAG_CANDIDATE_PRESSED, pressed);
139 }
140
141 [[nodiscard]] auto updateDebouncedEdge(bool rawPressed, uint16_t elapsed_ms) noexcept -> constants::ClickResult
142 {
144 using constants::timings::CLICKABLE_DEBOUNCE_TIME_MS;
145
146 if (!this->isDebouncing())
147 {
148 if (rawPressed == this->stablePressed())
149 {
150 return ClickResult::NO_CLICK;
151 }
152 if (CLICKABLE_DEBOUNCE_TIME_MS == 0U)
153 {
154 return this->confirmDebouncedEdge(rawPressed);
155 }
156 this->startDebounce(rawPressed);
157 return ClickResult::NO_CLICK;
158 }
159
160 if (rawPressed != this->candidatePressed())
161 {
162 // The raw input bounced back to the stable level before the
163 // candidate edge became valid. Keep the same logical press, which
164 // prevents a noisy release from arming another long click.
165 this->setClickableFlag(CLICKABLE_FLAG_DEBOUNCING, false);
166 this->setClickableFlag(CLICKABLE_FLAG_CANDIDATE_PRESSED, this->stablePressed());
167 this->debounceAge_ms = 0U;
168 return ClickResult::NO_CLICK;
169 }
170
171 const uint16_t nextDebounceAge = timeUtils::addElapsedTimeSaturated(this->debounceAge_ms, elapsed_ms);
172 this->debounceAge_ms = nextDebounceAge > UINT8_MAX ? UINT8_MAX : static_cast<uint8_t>(nextDebounceAge);
173 if (this->debounceAge_ms >= CLICKABLE_DEBOUNCE_TIME_MS)
174 {
175 return this->confirmDebouncedEdge(rawPressed);
176 }
177 return ClickResult::NO_CLICK;
178 }
179
188 template <bool StaticConfigKnown, uint8_t StaticDetectionFlags, uint16_t StaticLongClick_ms, uint16_t StaticSuperLongClick_ms>
189 [[nodiscard]] auto clickDetectionImpl(uint16_t elapsed_ms, uint8_t detectionFlags, uint16_t longClick_ms, uint16_t superLongClick_ms)
191 {
193 using namespace constants::clickDetection;
194
195 const bool wasStablePressed = this->stablePressed();
196 static_cast<void>(this->updateDebouncedEdge(this->getState(), elapsed_ms));
197 const bool isStablePressed = this->stablePressed();
198
199 if (!wasStablePressed && isStablePressed)
200 {
201 this->pressAge_ms = 0U;
202 this->clearTimedActionFlags();
203 if constexpr (StaticConfigKnown)
204 {
205 if constexpr ((StaticDetectionFlags & QUICK_SHORT) != 0U)
206 {
207 return ClickResult::SHORT_CLICK_QUICK;
208 }
209 }
210 else if (hasFlag(detectionFlags, QUICK_SHORT))
211 {
212 return ClickResult::SHORT_CLICK_QUICK;
213 }
214 return ClickResult::NO_CLICK_KEEPING_CLICKED;
215 }
216
217 if (wasStablePressed && !isStablePressed)
218 {
219 const bool timedActionFired =
220 this->hasClickableFlag(CLICKABLE_FLAG_LONG_FIRED) || this->hasClickableFlag(CLICKABLE_FLAG_SUPER_LONG_FIRED);
221 this->pressAge_ms = 0U;
222 this->clearTimedActionFlags();
223 if constexpr (StaticConfigKnown)
224 {
225 if constexpr ((StaticDetectionFlags & QUICK_SHORT) != 0U)
226 {
227 return ClickResult::NO_CLICK;
228 }
229 if constexpr ((StaticDetectionFlags & SHORT_ENABLED) != 0U)
230 {
231 return timedActionFired ? ClickResult::NO_CLICK : ClickResult::SHORT_CLICK;
232 }
233 return ClickResult::NO_CLICK;
234 }
235 if (hasFlag(detectionFlags, QUICK_SHORT))
236 {
237 return ClickResult::NO_CLICK;
238 }
239 if (!timedActionFired)
240 {
241 return hasFlag(detectionFlags, SHORT_ENABLED) ? ClickResult::SHORT_CLICK : ClickResult::NO_CLICK_NOT_SHORT_CLICKABLE;
242 }
243 return ClickResult::NO_CLICK;
244 }
245
246 if (!isStablePressed)
247 {
248 return ClickResult::NO_CLICK;
249 }
250
251 if (this->releaseDebouncePending())
252 {
253 return ClickResult::NO_CLICK_KEEPING_CLICKED;
254 }
255
256 this->pressAge_ms = timeUtils::addElapsedTimeSaturated(this->pressAge_ms, elapsed_ms);
257 if constexpr (StaticConfigKnown)
258 {
259 if constexpr ((StaticDetectionFlags & LONG_ENABLED) != 0U)
260 {
261 if (!this->hasClickableFlag(CLICKABLE_FLAG_LONG_FIRED) && this->pressAge_ms >= StaticLongClick_ms)
262 {
263 this->setClickableFlag(CLICKABLE_FLAG_LONG_FIRED, true);
264 return ClickResult::LONG_CLICK;
265 }
266 }
267
268 if constexpr ((StaticDetectionFlags & SUPER_LONG_ENABLED) != 0U)
269 {
270 if (!this->hasClickableFlag(CLICKABLE_FLAG_SUPER_LONG_FIRED) && this->pressAge_ms >= StaticSuperLongClick_ms)
271 {
272 this->setClickableFlag(CLICKABLE_FLAG_SUPER_LONG_FIRED, true);
273 return ClickResult::SUPER_LONG_CLICK;
274 }
275 }
276 }
277 else
278 {
279 if (hasFlag(detectionFlags, LONG_ENABLED) && !this->hasClickableFlag(CLICKABLE_FLAG_LONG_FIRED) &&
280 this->pressAge_ms >= longClick_ms)
281 {
282 this->setClickableFlag(CLICKABLE_FLAG_LONG_FIRED, true);
283 return ClickResult::LONG_CLICK;
284 }
285
286 if (hasFlag(detectionFlags, SUPER_LONG_ENABLED) && !this->hasClickableFlag(CLICKABLE_FLAG_SUPER_LONG_FIRED) &&
287 this->pressAge_ms >= superLongClick_ms)
288 {
289 this->setClickableFlag(CLICKABLE_FLAG_SUPER_LONG_FIRED, true);
290 return ClickResult::SUPER_LONG_CLICK;
291 }
292 }
293 return ClickResult::NO_CLICK_KEEPING_CLICKED;
294 }
295
296public:
297#ifndef CONFIG_USE_FAST_CLICKABLES
303 explicit LSH_OPTIONAL_CONSTEXPR_CTOR Clickable(uint8_t pin) noexcept : pinNumber(pin)
304 {
305 pinMode(pin, INPUT);
306 digitalWrite(pin, LOW);
307 }
308
315 template <uint8_t Pin>
316 explicit LSH_OPTIONAL_CONSTEXPR_CTOR Clickable(lsh::core::PinTag<Pin>) noexcept : Clickable(static_cast<uint8_t>(Pin))
317 {}
318#else
324 explicit Clickable(uint8_t pin) noexcept : Clickable(lsh::core::avr::makeFastInputPinBinding(pin))
325 {}
326
333 template <uint8_t Pin>
334 explicit Clickable(lsh::core::PinTag<Pin>) noexcept : Clickable(lsh::core::avr::makeFastInputPinBinding(lsh::core::PinTag<Pin>{}))
335 {}
336#endif
337
338// Delete copy constructor, copy assignment operator, move constructor and move assignment operator
339#if LSH_USING_CPP17
340 Clickable(const Clickable &) = delete;
341 Clickable(Clickable &&) = delete;
342 auto operator=(const Clickable &) -> Clickable & = delete;
343 auto operator=(Clickable &&) -> Clickable & = delete;
344#endif // LSH_USING_CPP17
345
352 auto getState() const -> bool
353 {
354#ifdef CONFIG_USE_FAST_CLICKABLES
355 // The hot scan path reads the cached register directly. This is the
356 // whole point of the fast-I/O variant: no Arduino helper and no
357 // extra indirection while polling buttons continuously.
358 return (*this->pinPort & this->pinMask) != 0U;
359#else
360 return (static_cast<bool>(digitalRead(this->pinNumber)));
361#endif
362 }
363
364 void setIndex(uint8_t indexToSet); // Set the Clickable index on Clickables namespace Array
365
366 // Getters
367 [[nodiscard]] auto getIndex() const -> uint8_t; // Get the Clickable index on Clickables namespace Array
368
369 // Utilities
383 [[nodiscard]] auto clickDetection(uint16_t elapsed_ms, uint8_t detectionFlags, uint16_t longClick_ms, uint16_t superLongClick_ms)
385 {
386 return this->clickDetectionImpl<false, 0U, 0U, 0U>(elapsed_ms, detectionFlags, longClick_ms, superLongClick_ms);
387 }
388
392 template <uint8_t DetectionFlags, uint16_t LongClick_ms, uint16_t SuperLongClick_ms>
393 [[nodiscard]] auto clickDetection(uint16_t elapsed_ms) -> constants::ClickResult
394 {
395 return this->clickDetectionImpl<true, DetectionFlags, LongClick_ms, SuperLongClick_ms>(elapsed_ms, 0U, 0U, 0U);
396 }
397};
398
399#endif // LSH_CORE_PERIPHERALS_INPUT_CLICKABLE_HPP
Typed helpers for AVR fast I/O access without Arduino macro casts.
A class that represents a clickable object, like a button, and its associated logic.
Definition clickable.hpp:42
auto clickDetection(uint16_t elapsed_ms, uint8_t detectionFlags, uint16_t longClick_ms, uint16_t superLongClick_ms) -> constants::ClickResult
Advance the click FSM with fully generated static configuration.
Definition clickable.hpp:383
void setIndex(uint8_t indexToSet)
Store the dense runtime index assigned by the generated static profile.
Definition clickable.cpp:28
auto clickDetection(uint16_t elapsed_ms) -> constants::ClickResult
Advance the click FSM with fully generated compile-time constants.
Definition clickable.hpp:393
LSH_OPTIONAL_CONSTEXPR_CTOR Clickable(uint8_t pin) noexcept
Construct a new Clickable object, conventional IO version.
Definition clickable.hpp:303
auto getIndex() const -> uint8_t
Return the dense runtime index assigned by the generated static profile.
Definition clickable.cpp:45
auto getState() const -> bool
Get the state of the clickable if configured as INPUT with its external pulldown resistor (PIN -> BUT...
Definition clickable.hpp:352
LSH_OPTIONAL_CONSTEXPR_CTOR Clickable(lsh::core::PinTag< Pin >) noexcept
Construct a clickable from a compile-time pin tag on the slow-I/O path.
Definition clickable.hpp:316
Bit flags consumed by the generated clickable scan path.
Defines the ClickResult enum for the clickable state machine.
Centralized C++ feature-detection macros for lsh-core.
Namespace that groups compile-time constants.
Definition config.hpp:33
ClickResult
Result of clickable click detection via clickDetection().
Definition click_results.hpp:36
@ NO_CLICK_KEEPING_CLICKED
The clickable is kept pressed after a timed action.
@ NO_CLICK
No click detected.
Compile-time pin tag used to route peripheral construction through constant pin paths.
Declares tiny helpers for elapsed-time arithmetic that must never wrap.
Type-level wrapper around one Arduino pin number.
Definition pin_tag.hpp:41
Resolved fast-input binding for one Arduino pin.
Definition avr_fast_io.hpp:48
uint8_t mask
Final bit mask for the pin inside the AVR port.
Definition avr_fast_io.hpp:49
volatile uint8_t * outputPort
Final AVR output register used to disable the input pull-up.
Definition avr_fast_io.hpp:51
volatile const uint8_t * pinPort
Final AVR input register used to sample the pin state.
Definition avr_fast_io.hpp:50
volatile uint8_t * modePort
Final AVR DDR register used to force INPUT mode.
Definition avr_fast_io.hpp:52
Centralizes all build-time configurable timing constants for the framework.
Internal bridge that imports static profile resources into the library's scope.