From caa2d20b0ee70d793ac755e98bc70f27f8cc2035 Mon Sep 17 00:00:00 2001 From: Dorfman Date: Sun, 26 Apr 2026 18:45:29 -0700 Subject: [PATCH] feat(rak3401): AIN1 button for status screen and shutdown --- examples/simple_repeater/UITask.cpp | 189 ++++++++++++++++++++++------ examples/simple_repeater/UITask.h | 8 +- examples/simple_repeater/main.cpp | 38 ++++++ variants/rak3401/RAK3401Board.cpp | 14 +++ variants/rak3401/RAK3401Board.h | 4 + variants/rak3401/platformio.ini | 1 + 6 files changed, 212 insertions(+), 42 deletions(-) diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index acb4632581..3dd94c7783 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -8,6 +8,18 @@ #define AUTO_OFF_MILLIS 20000 // 20 seconds #define BOOT_SCREEN_MILLIS 4000 // 4 seconds +#define STATUS_SCREEN_MILLIS 5000 // 5 seconds + +// Electric plug icon 16x7px (side profile, cord left, prongs right) +static const uint8_t charging_icon [] PROGMEM = { + 0x01, 0xE0, // .......####..... + 0x03, 0xE0, // ......#####..... + 0x03, 0xFE, // ......#########. + 0xFF, 0xE0, // ###########..... + 0x03, 0xFE, // ......#########. + 0x03, 0xE0, // ......#####..... + 0x01, 0xE0, // .......####..... +}; // 'meshcore', 128x13px static const uint8_t meshcore_logo [] PROGMEM = { @@ -26,61 +38,156 @@ static const uint8_t meshcore_logo [] PROGMEM = { 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, }; +// Draw a horizontal rule +static void drawHRule(DisplayDriver* d, int x, int y, int w) { + d->setColor(DisplayDriver::LIGHT); + d->fillRect(x, y, w, 1); +} + void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; _node_prefs = node_prefs; _display->turnOn(); - // strip off dash and commit hash by changing dash to null terminator - // e.g: v1.2.3-abcdef -> v1.2.3 char *version = strdup(firmware_version); char *dash = strchr(version, '-'); - if(dash){ - *dash = 0; - } + if(dash) *dash = 0; - // v1.2.3 (1 Jan 2025) sprintf(_version_info, "%s (%s)", version, build_date); } +void UITask::showStatus(uint16_t batt_mv, unsigned long uptime_ms, bool charging) { + _status_batt_mv = batt_mv; + _status_uptime_ms = uptime_ms; + _status_charging = charging; + _status_until = millis() + STATUS_SCREEN_MILLIS; + _auto_off = _status_until + AUTO_OFF_MILLIS; + _next_refresh = 0; + _display->turnOn(); +} + void UITask::renderCurrScreen() { char tmp[80]; - if (millis() < BOOT_SCREEN_MILLIS) { // boot screen - // meshcore logo - _display->setColor(DisplayDriver::BLUE); - int logoWidth = 128; - _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); + int W = _display->width(); - // version info + if (millis() < BOOT_SCREEN_MILLIS) { + // ── BOOT SCREEN ── + + // logo centered _display->setColor(DisplayDriver::LIGHT); + _display->drawXbm((W - 128) / 2, 6, meshcore_logo, 128, 13); + + // thin rule under logo + drawHRule(_display, 10, 22, W - 20); + + // version centered _display->setTextSize(1); - uint16_t versionWidth = _display->getTextWidth(_version_info); - _display->setCursor((_display->width() - versionWidth) / 2, 22); - _display->print(_version_info); - - // node type - const char* node_type = "< Repeater >"; - uint16_t typeWidth = _display->getTextWidth(node_type); - _display->setCursor((_display->width() - typeWidth) / 2, 35); - _display->print(node_type); - } else { // home screen - // node name - _display->setCursor(0, 0); + _display->setColor(DisplayDriver::LIGHT); + _display->drawTextCentered(W / 2, 27, _version_info); + + // role label in brackets + drawHRule(_display, 10, 39, W - 20); + _display->drawTextCentered(W / 2, 44, "[ REPEATER ]"); + + } else if (_status_until > 0 && millis() < _status_until) { + // ── STATUS SCREEN ── + + // header bar: inverted "STATUS" label + _display->setColor(DisplayDriver::LIGHT); + _display->fillRect(0, 0, W, 11); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); - _display->print(_node_prefs->node_name); - - // freq / sf - _display->setCursor(0, 20); - _display->setColor(DisplayDriver::YELLOW); - sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); - _display->print(tmp); - - // bw / cr - _display->setCursor(0, 30); - sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); - _display->print(tmp); + _display->setColor(DisplayDriver::DARK); + _display->drawTextCentered(W / 2, 2, "STATUS"); + + int pct = 0; + if (_status_batt_mv >= 4200) pct = 100; + else if (_status_batt_mv > 3000) pct = (_status_batt_mv - 3000) * 100 / 1200; + + // uptime row + _display->setColor(DisplayDriver::LIGHT); + unsigned long secs = _status_uptime_ms / 1000; + unsigned long mins = secs / 60; + unsigned long hrs = mins / 60; + unsigned long days = hrs / 24; + + _display->setCursor(4, 15); + _display->print("UPTIME"); + if (days > 0) { + sprintf(tmp, "%lud %luh %lum", days, hrs % 24, mins % 60); + } else if (hrs > 0) { + sprintf(tmp, "%luh %lum %lus", hrs, mins % 60, secs % 60); + } else { + sprintf(tmp, "%lum %lus", mins, secs % 60); + } + _display->drawTextRightAlign(W - 4, 15, tmp); + + drawHRule(_display, 4, 25, W - 8); + + // battery voltage row + _display->setCursor(4, 29); + _display->print("BATT"); + sprintf(tmp, "%u.%02uV %d%%", _status_batt_mv / 1000, (_status_batt_mv % 1000) / 10, pct); + _display->drawTextRightAlign(W - 4, 29, tmp); + + drawHRule(_display, 4, 39, W - 8); + + // battery bar (full width gauge) + int barX = 4, barY = 43, barW = W - 8, barH = 8; + _display->drawRect(barX, barY, barW, barH); + int fillW = (pct * (barW - 4)) / 100; + if (fillW > 0) _display->fillRect(barX + 2, barY + 2, fillW, barH - 4); + + // percentage label centered under bar, with charging icon if applicable + sprintf(tmp, "%d%%", pct); + if (_status_charging) { + int txtW = _display->getTextWidth(tmp); + int totalW = 18 + 2 + txtW + 4 + _display->getTextWidth("CHG"); + int startX = (W - totalW) / 2; + _display->drawXbm(startX, 55, charging_icon, 16, 7); + _display->setCursor(startX + 20, 54); + _display->print(tmp); + _display->print(" CHG"); + } else { + _display->drawTextCentered(W / 2, 54, tmp); + } + + } else { + // ── HOME SCREEN ── + + // header bar: inverted node name + _display->setColor(DisplayDriver::LIGHT); + _display->fillRect(0, 0, W, 11); + _display->setTextSize(1); + _display->setColor(DisplayDriver::DARK); + _display->drawTextCentered(W / 2, 2, _node_prefs->node_name); + + // radio params + _display->setColor(DisplayDriver::LIGHT); + + _display->setCursor(4, 16); + _display->print("FREQ"); + sprintf(tmp, "%06.3f MHz", _node_prefs->freq); + _display->drawTextRightAlign(W - 4, 16, tmp); + + drawHRule(_display, 4, 26, W - 8); + + _display->setCursor(4, 30); + _display->print("SF"); + sprintf(tmp, "%d", _node_prefs->sf); + _display->drawTextRightAlign(W / 2 - 4, 30, tmp); + + _display->setCursor(W / 2 + 4, 30); + _display->print("CR"); + sprintf(tmp, "%d", _node_prefs->cr); + _display->drawTextRightAlign(W - 4, 30, tmp); + + drawHRule(_display, 4, 40, W - 8); + + _display->setCursor(4, 44); + _display->print("BW"); + sprintf(tmp, "%03.1f kHz", _node_prefs->bw); + _display->drawTextRightAlign(W - 4, 44, tmp); } } @@ -89,17 +196,17 @@ void UITask::loop() { if (millis() >= _next_read) { int btnState = digitalRead(PIN_USER_BTN); if (btnState != _prevBtnState) { - if (btnState == USER_BTN_PRESSED) { // pressed? + if (btnState == USER_BTN_PRESSED) { if (_display->isOn()) { // TODO: any action ? } else { _display->turnOn(); } - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + _auto_off = millis() + AUTO_OFF_MILLIS; } _prevBtnState = btnState; } - _next_read = millis() + 200; // 5 reads per second + _next_read = millis() + 200; } #endif @@ -109,7 +216,7 @@ void UITask::loop() { renderCurrScreen(); _display->endFrame(); - _next_refresh = millis() + 1000; // refresh every second + _next_refresh = millis() + 1000; } if (millis() > _auto_off) { _display->turnOff(); diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index a27259f117..14ebc2ccd6 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -10,10 +10,16 @@ class UITask { NodePrefs* _node_prefs; char _version_info[32]; + unsigned long _status_until; // millis when status screen expires + uint16_t _status_batt_mv; + unsigned long _status_uptime_ms; + bool _status_charging; + void renderCurrScreen(); public: - UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; _status_until = 0; } void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); + void showStatus(uint16_t batt_mv, unsigned long uptime_ms, bool charging); void loop(); }; \ No newline at end of file diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..19e113fb43 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -28,6 +28,13 @@ static unsigned long userBtnDownAt = 0; #define USER_BTN_HOLD_OFF_MILLIS 1500 #endif +#if defined(PIN_USER_BTN_ANA) && !defined(_SEEED_SENSECAP_SOLAR_H_) +static unsigned long anaBtnDownAt = 0; +#define ANA_BTN_HOLD_OFF_MILLIS 5000 +#define ANA_BTN_SHORT_MILLIS 500 +static bool anaBtnShortHandled = false; +#endif + void setup() { Serial.begin(115200); delay(1000); @@ -147,6 +154,37 @@ void loop() { } #endif +#if defined(PIN_USER_BTN_ANA) && !defined(_SEEED_SENSECAP_SOLAR_H_) + bool anaPressed = (digitalRead(PIN_USER_BTN_ANA) == LOW); + if (anaPressed) { + if (anaBtnDownAt == 0) { + anaBtnDownAt = millis(); + anaBtnShortHandled = false; + } + unsigned long held = (unsigned long)(millis() - anaBtnDownAt); + if (held >= ANA_BTN_HOLD_OFF_MILLIS) { + Serial.println("AIN1 shutdown triggered"); +#ifdef DISPLAY_CLASS + display.turnOn(); + display.startFrame(); + display.setCursor(0, 20); + display.print("Shutting down..."); + display.endFrame(); + delay(1500); + display.turnOff(); +#endif + board.powerOff(); + } else if (!anaBtnShortHandled && held >= ANA_BTN_SHORT_MILLIS) { + anaBtnShortHandled = true; +#ifdef DISPLAY_CLASS + ui_task.showStatus(board.getBattMilliVolts(), millis(), board.isExternalPowered()); +#endif + } + } else { + anaBtnDownAt = 0; + } +#endif + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index cbf7c1087d..5d6e26e1bf 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -24,6 +24,20 @@ void RAK3401Board::initiateShutdown(uint8_t reason) { configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); } +#ifdef PIN_USER_BTN_ANA + // Configure AIN1 button as GPIO SENSE wake source (active LOW) + // Wait for button release first — SENSE is level-triggered + while (digitalRead(PIN_USER_BTN_ANA) == LOW) delay(10); + delay(50); // debounce + + // Configure pin for SENSE LOW wake via nRF52 GPIO registers + uint32_t pin = (uint32_t)PIN_USER_BTN_ANA; + NRF_GPIO->PIN_CNF[pin] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) + | (GPIO_PIN_CNF_INPUT_Connect << GPIO_PIN_CNF_INPUT_Pos) + | (GPIO_PIN_CNF_PULL_Pullup << GPIO_PIN_CNF_PULL_Pos) + | (GPIO_PIN_CNF_SENSE_Low << GPIO_PIN_CNF_SENSE_Pos); +#endif + enterSystemOff(reason); } #endif diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h index 3a080d5e2c..6b1d398735 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -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 { diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 3d2d4a3ec5..2c5cfd8ed2 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -29,6 +29,7 @@ extends = rak3401 build_flags = ${rak3401.build_flags} -D DISPLAY_CLASS=SSD1306Display + -D PIN_USER_BTN_ANA=31 -D ADVERT_NAME='"RAK3401 1W Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0