597f6767b5
* 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>
198 lines
7.7 KiB
C++
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;
|
|
}
|