Files
meshtastic_firmware/src/input/RotaryEncoderInterruptBase.cpp
T
Thomas GöttgensGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Claude Sonnet 4.6
597f6767b5 Add Elecrow ThinkNode M8 board support (thinknode_m8) (#11226)
* Add Elecrow ThinkNode M8 variant scaffold (thinknode_m8)

nRF52840 + SX1262 + 2.4" e-paper + ATGM336H-5NR32 GPS.
All pins resolved from ThinkNode_M8_V0.3.sch; cross-checked
against meshtastic/firmware#9181 (Elecrow V0.1 reference).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add Elecrow ThinkNode M8 board support (nRF52840/SX1262, 1.54in e-ink, ATGM336H GNSS, SC7A20, EC04 encoder)

* Address review: keep the stored backlight level out of blanking, match only the SC7A20 WHO_AM_I byte, and transfer detents atomically

* Use std::atomic for the press-and-turn detent counter so native builds compile

* Drop the ThinkNode M8 LED_BUILTIN redefinition that warned on every translation unit

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 15:59:35 +00:00

198 lines
7.7 KiB
C++

#include "RotaryEncoderInterruptBase.h"
#include "configuration.h"
RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concurrency::OSThread(name)
{
this->_originName = name;
}
void RotaryEncoderInterruptBase::init(
uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
input_broker_event eventPressed, input_broker_event eventPressedLong,
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress) :
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)())
{
this->_pinA = pinA;
this->_pinB = pinB;
this->_pinPress = pinPress;
this->_eventCw = eventCw;
this->_eventCcw = eventCcw;
this->_eventPressed = eventPressed;
this->_eventPressedLong = eventPressedLong;
bool isRAK = false;
#ifdef RAK_4631
isRAK = true;
#endif
if (!isRAK || pinPress != 0) {
pinMode(pinPress, INPUT_PULLUP);
attachInterrupt(pinPress, onIntPress, CHANGE);
}
if (!isRAK || this->_pinA != 0) {
pinMode(this->_pinA, INPUT_PULLUP);
attachInterrupt(this->_pinA, onIntA, CHANGE);
}
if (!isRAK || this->_pinA != 0) {
pinMode(this->_pinB, INPUT_PULLUP);
attachInterrupt(this->_pinB, onIntB, CHANGE);
}
this->rotaryLevelA = digitalRead(this->_pinA);
this->rotaryLevelB = digitalRead(this->_pinB);
LOG_INFO("Rotary initialized (%d, %d, %d)", this->_pinA, this->_pinB, pinPress);
}
int32_t RotaryEncoderInterruptBase::runOnce()
{
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
unsigned long now = millis();
// Handle press long/short detection
if (this->action == ROTARY_ACTION_PRESSED) {
bool buttonPressed = !digitalRead(_pinPress);
if (!pressDetected && buttonPressed) {
pressDetected = true;
pressStartTime = now;
pressAndTurnFired = false;
}
if (pressDetected) {
// Press-and-turn takes precedence over the press itself.
if (pressAndTurnEnabled() && pressAndTurnDelta.load(std::memory_order_relaxed) != 0) {
// Drain in one pass: releasing the button would discard anything left over.
// Exchange, so a detent arriving from the ISR mid-drain is not lost.
int32_t pending = pressAndTurnDelta.exchange(0, std::memory_order_relaxed);
LOG_DEBUG("Rotary event Press %s (%d detents)", pending > 0 ? "CW" : "CCW", pending);
while (pending != 0) {
bool cw = pending > 0;
InputEvent turn = {};
turn.source = this->_originName;
turn.inputEvent = INPUT_BROKER_NONE;
turn.kbchar = cw ? _pressAndTurnCw : _pressAndTurnCcw;
pending -= cw ? 1 : -1;
this->notifyObservers(&turn);
}
pressAndTurnFired = true;
}
uint32_t duration = now - pressStartTime;
if (!buttonPressed) {
// released -> if short press, send short, else already sent long
if (!pressAndTurnFired && duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
lastPressKeyTime = now;
LOG_DEBUG("Rotary event Press short");
e.inputEvent = this->_eventPressed;
} else if (pressAndTurnEnabled() && !pressAndTurnFired && duration >= LONG_PRESS_DURATION &&
this->_eventPressedLong != INPUT_BROKER_NONE) {
// Held long enough and no turn came, so the long press stands
LOG_DEBUG("Rotary event Press long");
e.inputEvent = this->_eventPressedLong;
}
pressDetected = false;
pressStartTime = 0;
lastPressLongEventTime = 0;
pressAndTurnDelta.store(0, std::memory_order_relaxed);
pressAndTurnFired = false;
this->action = ROTARY_ACTION_NONE;
} else if (!pressAndTurnEnabled() && duration >= LONG_PRESS_DURATION &&
this->_eventPressedLong != INPUT_BROKER_NONE && lastPressLongEventTime == 0) {
// fire single-shot long press; press-and-turn encoders defer this to release
lastPressLongEventTime = now;
LOG_DEBUG("Rotary event Press long");
e.inputEvent = this->_eventPressedLong;
}
}
} else if (this->action == ROTARY_ACTION_CW) {
LOG_DEBUG("Rotary event CW");
e.inputEvent = this->_eventCw;
} else if (this->action == ROTARY_ACTION_CCW) {
LOG_DEBUG("Rotary event CCW");
e.inputEvent = this->_eventCcw;
}
if (e.inputEvent != INPUT_BROKER_NONE || e.kbchar != 0) {
this->notifyObservers(&e);
}
if (!pressDetected) {
this->action = ROTARY_ACTION_NONE;
} else if (now - pressStartTime < LONG_PRESS_DURATION) {
return (20); // keep checking for long/short until time expires
} else if (pressAndTurnEnabled()) {
// Keep polling while held, rather than relying on intHandler()'s reschedule from ISR.
return (20);
}
return INT32_MAX;
}
void RotaryEncoderInterruptBase::setPressAndTurnChars(unsigned char cw, unsigned char ccw)
{
this->_pressAndTurnCw = cw;
this->_pressAndTurnCcw = ccw;
}
void RotaryEncoderInterruptBase::intPressHandler()
{
this->action = ROTARY_ACTION_PRESSED;
setIntervalFromNow(20); // start checking for long/short
}
void RotaryEncoderInterruptBase::intAHandler()
{
// CW rotation (at least on most common rotary encoders)
int currentLevelA = digitalRead(this->_pinA);
if (this->rotaryLevelA == currentLevelA) {
return;
}
this->rotaryLevelA = currentLevelA;
this->rotaryStateCCW = intHandler(currentLevelA == HIGH, this->rotaryLevelB, ROTARY_ACTION_CCW, this->rotaryStateCCW);
}
void RotaryEncoderInterruptBase::intBHandler()
{
// CW rotation (at least on most common rotary encoders)
int currentLevelB = digitalRead(this->_pinB);
if (this->rotaryLevelB == currentLevelB) {
return;
}
this->rotaryLevelB = currentLevelB;
this->rotaryStateCW = intHandler(currentLevelB == HIGH, this->rotaryLevelA, ROTARY_ACTION_CW, this->rotaryStateCW);
}
/**
* @brief Rotary action implementation.
* We assume, the following pin setup:
* A --||
* GND --||]========
* B --||
*
* @return The new state for rotary pin.
*/
RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool actualPinRaising, int otherPinLevel,
RotaryEncoderInterruptBaseActionType action,
RotaryEncoderInterruptBaseStateType state)
{
RotaryEncoderInterruptBaseStateType newState = state;
if (actualPinRaising && (otherPinLevel == LOW)) {
if (state == ROTARY_EVENT_CLEARED) {
newState = ROTARY_EVENT_OCCURRED;
if (this->action == ROTARY_ACTION_PRESSED) {
// Turning while held; runOnce() ignores this unless press-and-turn is enabled.
pressAndTurnDelta.fetch_add((action == ROTARY_ACTION_CW) ? 1 : -1, std::memory_order_relaxed);
} else {
this->action = action;
}
}
} else if (!actualPinRaising && (otherPinLevel == HIGH)) {
// Logic to prevent bouncing.
newState = ROTARY_EVENT_CLEARED;
}
setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable!
return newState;
}