mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-11 17:21:32 -04:00
* Initial firmware support for Axiometa Genesis Mini * Report the real AXIOMETA_GENESIS_MINI hardware model The protobufs now carry AXIOMETA_GENESIS_MINI = 148, so the board no longer has to masquerade as private hardware. On ESP32 the -D PRIVATE_HW flag never selected the model on its own - architecture.h has no arm for it, so the board fell through to the PRIVATE_HW default at the end of the chain. Give it its own arm and drop the flag. * fix(input): sample the encoder button after light-sleep wake The edge that wakes the device lands while beforeLightSleep() has the interrupts detached, and a button that is still held produces no further edge until it is released. The thread stayed parked at INT32_MAX, so the press was never sampled - no event was emitted, PowerFSM's GPIO-wake branch reads BUTTON_PIN rather than the encoder pin, and the node dropped straight back into light sleep with the press swallowed entirely. The second press worked, the first did not. Sample once on wake, and only when the button is asserted, so a timer or radio wake leaves the thread alone. The press then follows the ordinary path and InputBroker drops the event because the screen was off, so it wakes the screen and does nothing more - the same behaviour every other input device has. Rotation stays deliberately non-waking: only the button pin is armed in doLightSleep(), and the abState re-seed discards a shaft moved during sleep rather than replaying it as detents. Reported by CodeRabbit on #11852. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: rcarteraz <robert.l.carter2@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
217 lines
6.9 KiB
C++
217 lines
6.9 KiB
C++
#include "QuadratureEncoder.h"
|
|
|
|
#if defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 4)
|
|
|
|
#include "Throttle.h"
|
|
#include "main.h"
|
|
#include "sleep.h"
|
|
|
|
QuadratureEncoder *quadratureEncoder;
|
|
QuadratureEncoder *QuadratureEncoder::instance = nullptr;
|
|
|
|
extern bool osk_found;
|
|
|
|
QuadratureEncoder::QuadratureEncoder(const char *name) : concurrency::OSThread(name), _originName(name) {}
|
|
|
|
uint8_t QuadratureEncoder::readAB()
|
|
{
|
|
uint8_t a = digitalRead(INPUTDRIVER_ENCODER_A) ? 1 : 0;
|
|
uint8_t b = digitalRead(INPUTDRIVER_ENCODER_B) ? 1 : 0;
|
|
#if INPUTDRIVER_ENCODER_ACTIVE_LOW
|
|
a ^= 1;
|
|
b ^= 1;
|
|
#endif
|
|
return (uint8_t)((a << 1) | b);
|
|
}
|
|
|
|
#ifdef INPUTDRIVER_ENCODER_BTN
|
|
bool QuadratureEncoder::buttonAsserted()
|
|
{
|
|
const bool raw = digitalRead(INPUTDRIVER_ENCODER_BTN) ? true : false;
|
|
#if INPUTDRIVER_ENCODER_BTN_ACTIVE_LOW
|
|
return !raw;
|
|
#else
|
|
return raw;
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// Decode in the ISR and bank whole detents only. A missed edge is unrecoverable state loss, so
|
|
// the table has to run on every edge; keeping the sub-detent accumulator here as well means a
|
|
// fast spin cannot lose counts to scheduling latency. It is a table index and an add.
|
|
void QuadratureEncoder::sample()
|
|
{
|
|
const int8_t detents = quadratureFeed(abState, accum, readAB(), INPUTDRIVER_ENCODER_STEPS_PER_DETENT);
|
|
if (detents != 0)
|
|
pendingDetents.fetch_add(detents, std::memory_order_relaxed);
|
|
}
|
|
|
|
void QuadratureEncoder::handleInterrupt()
|
|
{
|
|
if (!instance)
|
|
return;
|
|
instance->sample();
|
|
instance->setIntervalFromNow(0);
|
|
runASAP = true;
|
|
#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
|
|
BaseType_t higherWake = 0;
|
|
concurrency::mainDelay.interruptFromISR(&higherWake);
|
|
#endif
|
|
}
|
|
|
|
void QuadratureEncoder::attachInterrupts()
|
|
{
|
|
attachInterrupt(INPUTDRIVER_ENCODER_A, QuadratureEncoder::handleInterrupt, CHANGE);
|
|
attachInterrupt(INPUTDRIVER_ENCODER_B, QuadratureEncoder::handleInterrupt, CHANGE);
|
|
#ifdef INPUTDRIVER_ENCODER_BTN
|
|
attachInterrupt(INPUTDRIVER_ENCODER_BTN, QuadratureEncoder::handleInterrupt, CHANGE);
|
|
#endif
|
|
}
|
|
|
|
void QuadratureEncoder::detachInterrupts()
|
|
{
|
|
detachInterrupt(INPUTDRIVER_ENCODER_A);
|
|
detachInterrupt(INPUTDRIVER_ENCODER_B);
|
|
#ifdef INPUTDRIVER_ENCODER_BTN
|
|
detachInterrupt(INPUTDRIVER_ENCODER_BTN);
|
|
#endif
|
|
}
|
|
|
|
bool QuadratureEncoder::init()
|
|
{
|
|
instance = this;
|
|
|
|
#if INPUTDRIVER_ENCODER_PULLUP
|
|
pinMode(INPUTDRIVER_ENCODER_A, INPUT_PULLUP);
|
|
pinMode(INPUTDRIVER_ENCODER_B, INPUT_PULLUP);
|
|
#else
|
|
pinMode(INPUTDRIVER_ENCODER_A, INPUT);
|
|
pinMode(INPUTDRIVER_ENCODER_B, INPUT);
|
|
#endif
|
|
|
|
#ifdef INPUTDRIVER_ENCODER_BTN
|
|
#if !INPUTDRIVER_ENCODER_PULLUP
|
|
pinMode(INPUTDRIVER_ENCODER_BTN, INPUT);
|
|
#elif INPUTDRIVER_ENCODER_BTN_ACTIVE_LOW
|
|
pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLUP);
|
|
#else
|
|
// Idles low, so a pull-up would fight the module's own resistor and hold the line asserted.
|
|
pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLDOWN);
|
|
#endif
|
|
#endif
|
|
|
|
// Seed from the resting position, so the first movement is scored against where the shaft
|
|
// actually is rather than against an assumed zero.
|
|
abState = readAB();
|
|
accum = 0;
|
|
|
|
attachInterrupts();
|
|
|
|
#ifdef ARCH_ESP32
|
|
lsObserver.observe(¬ifyLightSleep);
|
|
lsEndObserver.observe(¬ifyLightSleepEnd);
|
|
#endif
|
|
|
|
inputBroker->registerSource(this);
|
|
#ifndef HAS_PHYSICAL_KEYBOARD
|
|
osk_found = true;
|
|
#endif
|
|
|
|
#ifdef INPUTDRIVER_ENCODER_BTN
|
|
LOG_INFO("Quadrature encoder A=%d B=%d btn=%d (%d steps/detent)", INPUTDRIVER_ENCODER_A, INPUTDRIVER_ENCODER_B,
|
|
INPUTDRIVER_ENCODER_BTN, INPUTDRIVER_ENCODER_STEPS_PER_DETENT);
|
|
#else
|
|
LOG_INFO("Quadrature encoder A=%d B=%d, no switch (%d steps/detent)", INPUTDRIVER_ENCODER_A, INPUTDRIVER_ENCODER_B,
|
|
INPUTDRIVER_ENCODER_STEPS_PER_DETENT);
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
int32_t QuadratureEncoder::runOnce()
|
|
{
|
|
InputEvent e = {.source = _originName, .inputEvent = INPUT_BROKER_NONE, .kbchar = 0, .touchX = 0, .touchY = 0};
|
|
|
|
// exchange, not load-then-store: a detent arriving from the ISR mid-drain must not be lost.
|
|
int16_t detents = pendingDetents.exchange(0, std::memory_order_relaxed);
|
|
#if INPUTDRIVER_ENCODER_INVERT
|
|
detents = -detents;
|
|
#endif
|
|
// One event per detent, so a fast spin scrolls by the same amount it would have if every
|
|
// detent had been serviced individually.
|
|
while (detents != 0) {
|
|
const int8_t dir = (detents > 0) ? +1 : -1;
|
|
detents -= dir;
|
|
e.inputEvent = (dir > 0) ? INPUTDRIVER_ENCODER_EVENT_CW : INPUTDRIVER_ENCODER_EVENT_CCW;
|
|
if (e.inputEvent != INPUT_BROKER_NONE)
|
|
notifyObservers(&e);
|
|
}
|
|
e.inputEvent = INPUT_BROKER_NONE;
|
|
|
|
#ifdef INPUTDRIVER_ENCODER_BTN
|
|
const uint32_t now = millis();
|
|
const bool level = buttonAsserted();
|
|
|
|
if (level != pressed && Throttle::hasElapsed(lastPressChangeMs, INPUTDRIVER_ENCODER_BTN_DEBOUNCE_MS)) {
|
|
lastPressChangeMs = now;
|
|
pressed = level;
|
|
if (pressed) {
|
|
pressStartMs = now;
|
|
longFired = false;
|
|
} else if (!longFired) {
|
|
// A short press only resolves on release - until then it might still become a long one.
|
|
e.inputEvent = INPUTDRIVER_ENCODER_EVENT_PRESS;
|
|
if (e.inputEvent != INPUT_BROKER_NONE)
|
|
notifyObservers(&e);
|
|
e.inputEvent = INPUT_BROKER_NONE;
|
|
}
|
|
}
|
|
|
|
if (pressed && !longFired && Throttle::hasElapsed(pressStartMs, INPUTDRIVER_ENCODER_LONG_PRESS_MS)) {
|
|
// Single shot: a rotary select should not auto-repeat while held.
|
|
longFired = true;
|
|
e.inputEvent = INPUTDRIVER_ENCODER_EVENT_PRESS_LONG;
|
|
if (e.inputEvent != INPUT_BROKER_NONE)
|
|
notifyObservers(&e);
|
|
}
|
|
|
|
// Keep polling while held so the long press can mature without another edge.
|
|
if (pressed)
|
|
return 20;
|
|
#endif
|
|
|
|
return INT32_MAX;
|
|
}
|
|
|
|
#ifdef ARCH_ESP32
|
|
int QuadratureEncoder::beforeLightSleep(void *unused)
|
|
{
|
|
detachInterrupts();
|
|
return 0;
|
|
}
|
|
|
|
int QuadratureEncoder::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
|
{
|
|
// Re-seed before re-arming: the shaft can be turned while we are not watching, and decoding
|
|
// against a stale sample would invent a detent that never happened. Rotation is deliberately
|
|
// not a wake source, so a turn during sleep is discarded rather than replayed on wake.
|
|
abState = readAB();
|
|
accum = 0;
|
|
attachInterrupts();
|
|
|
|
#ifdef INPUTDRIVER_ENCODER_BTN
|
|
// The edge that woke us landed while the interrupts were detached, and a button that is still
|
|
// held produces no further edge until it is released - so without a nudge here the thread stays
|
|
// parked, the press is never sampled, and the node drops straight back into light sleep. The
|
|
// press itself only wakes the screen: InputBroker drops the event it eventually emits because
|
|
// the screen was off, which is how every other input device on every board behaves.
|
|
if (buttonAsserted()) {
|
|
setIntervalFromNow(0);
|
|
runASAP = true;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
#endif // INPUTDRIVER_ENCODER_TYPE == 4
|