#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 onIntA, std::function onIntB, std::function 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; }