Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions examples/companion_radio/ui-new/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#endif
#if defined(PIN_USER_BTN_ANA)
analog_btn.begin();
// Arm a permanent edge interrupt so presses are never lost when the main loop stalls
// (radio/BLE/flash) and the poll misses the edge. This also wakes the MCU from light sleep.
analog_btn.enableInterrupt();
#endif

_node_prefs = node_prefs;
Expand Down Expand Up @@ -750,15 +753,23 @@ void UITask::loop() {
#endif
#if defined(PIN_USER_BTN_ANA)
if (abs(millis() - _analogue_pin_read_millis) > 10) {
int ev = analog_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
c = handleDoubleClick(KEY_PREV);
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
if (_display != NULL && !_display->isOn() && analog_btn.isPressed()) {
// Instant screen-wake: light the display on the press edge instead of waiting out
// the ~280ms multi-click window, then abandon the gesture so this press only wakes
// the screen and doesn't also navigate.
checkDisplayOn(0);
analog_btn.reset();
} else {
int ev = analog_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
c = handleDoubleClick(KEY_PREV);
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
}
_analogue_pin_read_millis = millis();
}
Expand Down
10 changes: 6 additions & 4 deletions src/helpers/NRF52Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ void NRF52Board::enterSystemOff(uint8_t reason) {
NVIC_SystemReset();
}

void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel, bool detect_down) {
// LPCOMP is not managed by SoftDevice - direct register access required
// Halt and disable before reconfiguration
NRF_LPCOMP->TASKS_STOP = 1;
Expand All @@ -189,8 +189,10 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
// Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;

// Detect UP events (voltage rises above threshold for battery recovery)
NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
// Crossing direction: UP for voltage recovery (rises above threshold),
// DOWN for an analog button press (pin pulled below threshold).
NRF_LPCOMP->ANADETECT = detect_down ? LPCOMP_ANADETECT_ANADETECT_Down
: LPCOMP_ANADETECT_ANADETECT_Up;

// Enable 50mV hysteresis for noise immunity
NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV;
Expand All @@ -202,7 +204,7 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
NRF_LPCOMP->EVENTS_CROSS = 0;

NRF_LPCOMP->INTENCLR = 0xFFFFFFFF;
NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
NRF_LPCOMP->INTENSET = detect_down ? LPCOMP_INTENSET_DOWN_Msk : LPCOMP_INTENSET_UP_Msk;

// Enable LPCOMP
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/NRF52Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ class NRF52Board : public mesh::MainBoard {

bool checkBootVoltage(const PowerMgtConfig* config);
void enterSystemOff(uint8_t reason);
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
// Arm LPCOMP as a SYSTEMOFF wake source on the given analog channel.
// detect_down=false wakes on an upward crossing (voltage recovery);
// detect_down=true wakes on a downward crossing (e.g. an analog button press).
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel, bool detect_down = false);
virtual void initiateShutdown(uint8_t reason);
#endif

Expand Down
79 changes: 75 additions & 4 deletions src/helpers/ui/MomentaryButton.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
#include "MomentaryButton.h"

#define MULTI_CLICK_WINDOW_MS 280
#define IRQ_DEBOUNCE_MS 30 // ignore edges closer than this (contact bounce)

MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup, bool multiclick) {
MomentaryButton* MomentaryButton::_irq_self = nullptr;

void MomentaryButton::_onIrq() {
MomentaryButton* self = _irq_self;
if (!self) return;
// Count at most one press until check() re-arms us after a clean release. This rejects the
// press- and release-bounce falling edges that would otherwise latch a phantom second press.
if (!self->_irq_armed) return;
unsigned long now = millis();
if (now - self->_irq_last_ms < IRQ_DEBOUNCE_MS) return; // also coalesce very fast chatter
self->_irq_last_ms = now;
self->_irq_armed = false;
if (self->_irq_events < 200) self->_irq_events++; // latch the press for check() to consume
}

void MomentaryButton::enableInterrupt() {
if (_pin < 0) return;
// Edge interrupts need the digital input buffer; on an analog-read (SAADC threshold) pin the
// GPIOTE channel and analogRead() conflict and the pin reads stuck-low. Digital buttons only.
if (_threshold > 0) return;
_irq_self = this; // single button per board; last caller wins
pinMode(_pin, INPUT_PULLUP); // active-low button
attachInterrupt(digitalPinToInterrupt(_pin), _onIrq, FALLING);
}

MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup, bool multiclick) {
_pin = pin;
_reverse = reverse;
_pull = pulldownup;
Expand All @@ -15,9 +40,14 @@ MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse
_last_click_time = 0;
_multi_click_window = multiclick ? MULTI_CLICK_WINDOW_MS : 0;
_pending_click = false;
_irq_events = 0;
_irq_last_ms = 0;
_irq_armed = true; // first press counts; ISR disarms until a clean release re-arms
_irq_held = false;
_irq_release_ms = 0;
}

MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, int analog_threshold) {
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, int analog_threshold, int multi_click_window) {
_pin = pin;
_reverse = false;
_pull = false;
Expand All @@ -28,8 +58,13 @@ MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, int analog_t
_threshold = analog_threshold;
_click_count = 0;
_last_click_time = 0;
_multi_click_window = MULTI_CLICK_WINDOW_MS;
_multi_click_window = multi_click_window; // 0 = single click acts immediately (no double/triple-click)
_pending_click = false;
_irq_events = 0;
_irq_last_ms = 0;
_irq_armed = true; // first press counts; ISR disarms until a clean release re-arms
_irq_held = false;
_irq_release_ms = 0;
}

void MomentaryButton::begin() {
Expand All @@ -51,6 +86,23 @@ void MomentaryButton::cancelClick() {
_pending_click = false;
}

void MomentaryButton::reset() {
// Resync 'prev' to the current physical level and drop all gesture state. With the
// button still held, prev becomes the pressed level and down_at stays 0, so the eventual
// release records no click (check() guards the click on down_at > 0) β€” the gesture is
// fully abandoned, including the click that would otherwise fire a multi-click window
// after release. Used to swallow the press that only wakes the display.
prev = _threshold > 0 ? (analogRead(_pin) < _threshold) : digitalRead(_pin);
down_at = 0;
cancel = 0;
_click_count = 0;
_last_click_time = 0;
_pending_click = false;
_irq_events = 0; // drop latched presses too: the wake-press shouldn't queue navigation
_irq_armed = true;
_irq_held = false;
}

bool MomentaryButton::isPressed(int level) const {
if (_threshold > 0) {
return level;
Expand All @@ -67,9 +119,20 @@ int MomentaryButton::check(bool repeat_click) {

int event = BUTTON_EVENT_NONE;
int btn = _threshold > 0 ? (analogRead(_pin) < _threshold) : digitalRead(_pin);

// Re-arm the IRQ latch only once the button has been cleanly released for the debounce
// period. This is the debounce: the ISR can't count a new press (bounce or real) until here.
if (isPressed(btn)) {
_irq_held = true;
} else {
if (_irq_held) { _irq_held = false; _irq_release_ms = millis(); }
if (!_irq_armed && (millis() - _irq_release_ms) >= IRQ_DEBOUNCE_MS) _irq_armed = true;
}

if (btn != prev) {
if (isPressed(btn)) {
down_at = millis();
if (_irq_events > 0) _irq_events--; // this live press was already latched by the ISR; claim it
} else {
// button UP
if (_long_millis > 0) {
Expand Down Expand Up @@ -141,5 +204,13 @@ int MomentaryButton::check(bool repeat_click) {
_pending_click = false;
}

// Flush a press the live poll never saw (loop stalled through its entire down→up window):
// the ISR latched it but no edge was detected here. Only when idle (released, nothing
// pending) so we never interfere with an in-progress long-press or multi-click.
if (event == BUTTON_EVENT_NONE && _irq_events > 0 && !isPressed(btn) && down_at == 0 && !_pending_click) {
_irq_events--;
event = BUTTON_EVENT_CLICK;
}

return event;
}
18 changes: 17 additions & 1 deletion src/helpers/ui/MomentaryButton.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include <Arduino.h>

#define MULTI_CLICK_WINDOW_MS 280 // delay before a single click acts, used to detect double/triple-click

#define BUTTON_EVENT_NONE 0
#define BUTTON_EVENT_CLICK 1
#define BUTTON_EVENT_LONG_PRESS 2
Expand All @@ -20,14 +22,28 @@ class MomentaryButton {
int _multi_click_window;
bool _pending_click;

// Interrupt-latch: presses caught by the edge ISR even when the main loop stalls (radio/
// BLE/flash) and the poll misses them. check() reconciles these with live polling.
// Debounce: the ISR counts at most one press, and only re-arms after check() has seen the
// button cleanly released for the debounce period β€” so press/release bounce can't double-count.
volatile uint8_t _irq_events;
volatile bool _irq_armed;
unsigned long _irq_last_ms;
bool _irq_held;
unsigned long _irq_release_ms;
static MomentaryButton* _irq_self;
static void _onIrq();

bool isPressed(int level) const;

public:
MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true);
MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold);
MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold, int multi_click_window = MULTI_CLICK_WINDOW_MS);
void begin();
void enableInterrupt(); // attach a permanent edge IRQ so presses are never lost to a stalled loop
int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state)
void reset(); // abandon the in-flight gesture: resync to current level, drop any pending click
uint8_t getPin() { return _pin; }
bool isPressed() const;
};
42 changes: 42 additions & 0 deletions variants/rak3401/RAK3401Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
#include "RAK3401Board.h"

#ifdef NRF52_POWER_MANAGEMENT
#ifdef PIN_USER_BTN_ANA
// LPCOMP wake config for the AIN user button. Defaults assume PIN_USER_BTN_ANA
// is pin 31 (P0.31 = AIN7); override via build flags if the button moves.
#ifndef PWRMGT_BTN_LPCOMP_AIN
#define PWRMGT_BTN_LPCOMP_AIN 7
#endif
#ifndef PWRMGT_BTN_LPCOMP_REFSEL
#define PWRMGT_BTN_LPCOMP_REFSEL 3 // 4/8 VDD (~1.5V) threshold
#endif
#endif

// Static configuration for power management
// Values set in variant.h defines
const PowerMgtConfig power_config = {
Expand All @@ -24,6 +35,37 @@ void RAK3401Board::initiateShutdown(uint8_t reason) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}

#ifdef PIN_USER_BTN_ANA
// Wake-from-SYSTEMOFF on the AIN user button (P0.31 = AIN7).
//
// This pin is wired as an *analog* button (see MomentaryButton in target.cpp:
// pressed == analogRead() < threshold). GPIO SENSE can't be used as the wake
// source: the digital input buffer reads this line as LOW even at the released
// idle level (verified on hardware β€” analogRead reports ~VDD while NRF_GPIO->IN
// reads 0 and SENSE_Low latches immediately), so a GPIO SENSE arm wakes the
// chip the instant we enter SYSTEMOFF and it can never stay off.
//
// LPCOMP works in the analog domain, so it sees the idle level correctly. Arm
// it for a DOWN crossing at ~1/2 VDD: released idles near VDD (above), a press
// pulls the pin toward 0V (below) -> downward crossing -> wake. The LPCOMP is
// otherwise unused for a USER shutdown (voltage wake is only armed for the
// low-voltage / boot-protect reasons handled above), so there is no conflict.
//
// Wait for release first so LPCOMP is armed while the level is above the
// threshold β€” otherwise the initial press generates no new downward crossing.
// Bounded by a timeout so a stuck/low reading can never wedge shutdown.
const int BTN_RELEASED_ADC = 1024; // well above the press threshold
uint32_t t0 = millis();
int released_streak = 0;
while (released_streak < 5 && (millis() - t0) < 5000) {
if (analogRead(PIN_USER_BTN_ANA) > BTN_RELEASED_ADC) released_streak++;
else released_streak = 0;
delay(10);
}

configureVoltageWake(PWRMGT_BTN_LPCOMP_AIN, PWRMGT_BTN_LPCOMP_REFSEL, /*detect_down=*/true);
#endif

enterSystemOff(reason);
}
#endif
Expand Down
4 changes: 4 additions & 0 deletions variants/rak3401/RAK3401Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class RAK3401Board : public NRF52BoardDCDC {
RAK3401Board() : NRF52Board("RAK3401_OTA") {}
void begin();

#ifdef NRF52_POWER_MANAGEMENT
void powerOff() override { initiateShutdown(SHUTDOWN_REASON_USER); }
#endif

#define BATTERY_SAMPLES 8

uint16_t getBattMilliVolts() override {
Expand Down
7 changes: 6 additions & 1 deletion variants/rak3401/target.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ RAK3401Board board;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);

#if defined(PIN_USER_BTN_ANA)
MomentaryButton analog_btn(PIN_USER_BTN_ANA, 1000, 20);
// Read the button digitally (not via SAADC/analogRead): it's a clean active-low input
// (internal pull-up + button to GND), and digitalRead coexists with the latch/wake edge
// interrupt (enableInterrupt) where analogRead does not (GPIOTE vs SAADC conflict).
// reverse=true (active-low), pulldownup=true (INPUT_PULLUP), multiclick=true enables the
// MULTI_CLICK_WINDOW_MS (280ms) window: single=NEXT / double=PREV / triple=SELECT / long=ENTER.
MomentaryButton analog_btn(PIN_USER_BTN_ANA, 1000, true, true, true);
#endif
#endif

Expand Down