From 8d37c54a5ac1dcc3b089b4ced5640fe06bb1a434 Mon Sep 17 00:00:00 2001 From: Nik Orfanos Date: Sat, 8 Aug 2026 20:07:36 -0700 Subject: [PATCH] Add rich e-ink dashboard and button paging to E290 repeater The Heltec E290 repeater only showed node name + radio params on its 296x128 e-ink panel and didn't use the button. This adds a multi-page dashboard and wires up the Custom button on GPIO21. - variants/heltec_e290: PIN_USER_BTN 0 -> 21 (dedicated Custom button); keep the display live for the repeater (AUTO_OFF_MILLIS=0); construct user_btn with the internal pull-up (GPIO21 has no external pull and floats otherwise) - simple_repeater/UITask: e-ink-gated rich dashboard (Status / Radio / Traffic) built on the existing user_btn - short-press (CLICK) cycles pages, long-press keeps the existing power-off behaviour; gentler e-ink refresh cadence; honour AUTO_OFF_MILLIS=0 as never-auto-off - simple_repeater/MyMesh: expose getStats()/getNumNeighbours() for the UI (getStats also now backs the existing status request) - simple_repeater/main: pass the mesh into the UI task OLED repeater variants keep their original compact view (gated on DisplayDriver::isEink()). Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/simple_repeater/MyMesh.cpp | 52 +++++--- examples/simple_repeater/MyMesh.h | 5 + examples/simple_repeater/UITask.cpp | 181 ++++++++++++++++++++++++---- examples/simple_repeater/UITask.h | 10 +- examples/simple_repeater/main.cpp | 2 +- variants/heltec_e290/platformio.ini | 3 +- variants/heltec_e290/target.cpp | 4 +- 7 files changed, 210 insertions(+), 47 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index c93ba1a4cd..1ba8d4c0bc 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -208,6 +208,39 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender return 0; } +void MyMesh::getStats(RepeaterStats& stats) { + stats.batt_milli_volts = board.getBattMilliVolts(); + stats.curr_tx_queue_len = _mgr->getOutboundTotal(); + stats.noise_floor = (int16_t)_radio->getNoiseFloor(); + stats.last_rssi = (int16_t)radio_driver.getLastRSSI(); + stats.n_packets_recv = radio_driver.getPacketsRecv(); + stats.n_packets_sent = radio_driver.getPacketsSent(); + stats.total_air_time_secs = getTotalAirTime() / 1000; + stats.total_up_time_secs = uptime_millis / 1000; + stats.n_sent_flood = getNumSentFlood(); + stats.n_sent_direct = getNumSentDirect(); + stats.n_recv_flood = getNumRecvFlood(); + stats.n_recv_direct = getNumRecvDirect(); + stats.err_events = _err_flags; + stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); + stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); + stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); + stats.total_rx_air_time_secs = getReceiveAirTime() / 1000; + stats.n_recv_errors = radio_driver.getPacketsRecvErrors(); +} + +uint32_t MyMesh::getNumNeighbours() const { +#if MAX_NEIGHBOURS + uint32_t n = 0; + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (neighbours[i].heard_timestamp > 0) n++; + } + return n; +#else + return 0; +#endif +} + int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) { // uint32_t now = getRTCClock()->getCurrentTimeUnique(); // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp @@ -215,24 +248,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t if (payload[0] == REQ_TYPE_GET_STATUS) { // guests can also access this now RepeaterStats stats; - stats.batt_milli_volts = board.getBattMilliVolts(); - stats.curr_tx_queue_len = _mgr->getOutboundTotal(); - stats.noise_floor = (int16_t)_radio->getNoiseFloor(); - stats.last_rssi = (int16_t)radio_driver.getLastRSSI(); - stats.n_packets_recv = radio_driver.getPacketsRecv(); - stats.n_packets_sent = radio_driver.getPacketsSent(); - stats.total_air_time_secs = getTotalAirTime() / 1000; - stats.total_up_time_secs = uptime_millis / 1000; - stats.n_sent_flood = getNumSentFlood(); - stats.n_sent_direct = getNumSentDirect(); - stats.n_recv_flood = getNumRecvFlood(); - stats.n_recv_direct = getNumRecvDirect(); - stats.err_events = _err_flags; - stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); - stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); - stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); - stats.total_rx_air_time_secs = getReceiveAirTime() / 1000; - stats.n_recv_errors = radio_driver.getPacketsRecvErrors(); + getStats(stats); memcpy(&reply_data[4], &stats, sizeof(stats)); return 4 + sizeof(stats); // reply_len diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index e959f01a7d..a05af462fa 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -191,6 +191,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { return &_prefs; } + // Snapshot of live repeater stats (used by both the client status request and the UI) + void getStats(RepeaterStats& stats); + // Number of currently-known neighbours (non-empty entries in the neighbours table) + uint32_t getNumNeighbours() const; + void savePrefs() override { _cli.savePrefs(_fs); } diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index e7225557dd..2a9f869317 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -2,38 +2,72 @@ #include "target.h" #include #include +#include "MyMesh.h" #ifndef USER_BTN_PRESSED #define USER_BTN_PRESSED LOW #endif -#define AUTO_OFF_MILLIS 20000 // 20 seconds +#ifndef AUTO_OFF_MILLIS +#define AUTO_OFF_MILLIS 20000 // 20 seconds (0 = never auto-off, e.g. mains/solar repeaters) +#endif #define BOOT_SCREEN_MILLIS 4000 // 4 seconds #define POWEROFF_DELAY 3000 +#define EINK_REFRESH_MILLIS 30000 // e-ink: slow + wears with use, and CRC-gated in the driver +#define NUM_EINK_PAGES 3 // Status / Radio / Traffic + +// battery voltage -> percent range (override per-variant if needed) +#ifndef BATT_MIN_MILLIVOLTS +#define BATT_MIN_MILLIVOLTS 3000 +#endif +#ifndef BATT_MAX_MILLIVOLTS +#define BATT_MAX_MILLIVOLTS 4200 +#endif + // 'meshcore', 128x13px static const uint8_t meshcore_logo [] PROGMEM = { - 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, - 0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe, - 0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc, - 0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00, - 0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00, - 0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, - 0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, - 0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0, - 0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00, - 0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00, - 0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8, - 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8, - 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, + 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, + 0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe, + 0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc, + 0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00, + 0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00, + 0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0, + 0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00, + 0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00, + 0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, }; -void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { +static int battPercent(uint16_t mv) { + int pct = ((int)mv - BATT_MIN_MILLIVOLTS) * 100 / (BATT_MAX_MILLIVOLTS - BATT_MIN_MILLIVOLTS); + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + return pct; +} + +// human-readable uptime at minute granularity (keeps e-ink physical updates to ~1/min) +static void fmtUptime(char* out, size_t n, uint32_t secs) { + uint32_t d = secs / 86400; secs %= 86400; + uint32_t h = secs / 3600; secs %= 3600; + uint32_t m = secs / 60; + if (d) snprintf(out, n, "%lud %luh %lum", (unsigned long)d, (unsigned long)h, (unsigned long)m); + else if (h) snprintf(out, n, "%luh %lum", (unsigned long)h, (unsigned long)m); + else snprintf(out, n, "%lum", (unsigned long)m); +} + +void UITask::begin(MyMesh* mesh, NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { + _mesh = mesh; _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; _started_at = millis(); _node_prefs = node_prefs; + _is_eink = _display->isEink(); + _page = 0; _display->turnOn(); #if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) @@ -53,6 +87,97 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi free(version); } +// Rich, multi-page dashboard for large e-ink panels (e.g. E290 / E213). +void UITask::renderRichScreen() { + static const char* PAGE_NAMES[NUM_EINK_PAGES] = { "Status", "Radio", "Traffic" }; + char line[64]; + RepeaterStats st; + _mesh->getStats(st); + + const int w = _display->width(); + const int dy = 13; + int y; + + int batt_pct = battPercent(st.batt_milli_volts); + + // header: node name (large) + battery percent (top-right) + _display->setColor(UIColor::primary_txt); + _display->setTextSize(2); + _display->drawTextEllipsized(2, 0, w - 52, _node_prefs->node_name); + + _display->setTextSize(1); + snprintf(line, sizeof(line), "%d%%", batt_pct); + _display->drawTextRightAlign(w - 2, 2, line); + + // subtitle: role + page indicator + _display->setColor(UIColor::secondary_txt); + snprintf(line, sizeof(line), "Repeater [%d/%d] %s", _page + 1, (int)NUM_EINK_PAGES, PAGE_NAMES[_page]); + _display->drawTextLeftAlign(2, 20, line); + + // divider + _display->fillRect(0, 31, w, 1); + + _display->setColor(UIColor::primary_txt); + y = 38; + if (_page == 0) { // ---------- Status ---------- + snprintf(line, sizeof(line), "FW: %s", _version_info); + _display->drawTextLeftAlign(4, y, line); y += dy; + + char up[24]; + fmtUptime(up, sizeof(up), st.total_up_time_secs); + snprintf(line, sizeof(line), "Uptime: %s", up); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "Battery: %u mV (%d%%)", st.batt_milli_volts, batt_pct); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "Freq: %.3f MHz SF%u", _node_prefs->freq, _node_prefs->sf); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "BW: %.2f kHz CR: 4/%u", _node_prefs->bw, _node_prefs->cr); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "TX: %d dBm Neighbours: %lu", + (int)_node_prefs->tx_power_dbm, (unsigned long)_mesh->getNumNeighbours()); + _display->drawTextLeftAlign(4, y, line); y += dy; + + } else if (_page == 1) { // ---------- Radio ---------- + snprintf(line, sizeof(line), "Last RSSI: %d dBm", st.last_rssi); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "Last SNR: %.2f dB", st.last_snr / 4.0f); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "Noise floor: %d dBm", st.noise_floor); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "TX queue: %u", st.curr_tx_queue_len); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "Recv errors: %lu", (unsigned long)st.n_recv_errors); + _display->drawTextLeftAlign(4, y, line); y += dy; + + } else { // ---------- Traffic ---------- + snprintf(line, sizeof(line), "Rx: %lu (F:%lu D:%lu)", + (unsigned long)st.n_packets_recv, + (unsigned long)st.n_recv_flood, (unsigned long)st.n_recv_direct); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "Tx: %lu (F:%lu D:%lu)", + (unsigned long)st.n_packets_sent, + (unsigned long)st.n_sent_flood, (unsigned long)st.n_sent_direct); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "Airtime tx:%lus rx:%lus", + (unsigned long)st.total_air_time_secs, (unsigned long)st.total_rx_air_time_secs); + _display->drawTextLeftAlign(4, y, line); y += dy; + + snprintf(line, sizeof(line), "Dups F:%u D:%u Err:%u", + st.n_flood_dups, st.n_direct_dups, st.err_events); + _display->drawTextLeftAlign(4, y, line); y += dy; + } +} + void UITask::renderCurrScreen() { char tmp[80]; if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen @@ -91,7 +216,9 @@ void UITask::renderCurrScreen() { uint16_t poffWidth = _display->getTextWidth(poweroff_string); _display->setCursor((_display->width() - poffWidth) / 2, 48); _display->drawTextCentered(_display->width()/2, 48, poweroff_string); - } else { + } else if (_is_eink) { // large e-ink: rich, multi-page dashboard + renderRichScreen(); + } else { // small OLED: compact legacy layout _display->setCursor(0, 0); _display->setTextSize(1); _display->setColor(UIColor::primary_txt); @@ -113,16 +240,17 @@ void UITask::loop() { #if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { - if (_display->isOn()) { - // TODO: any action ? - } else { - _display->turnOn(); + if (!_display->isOn()) { + _display->turnOn(); // wake a sleeping display + } else if (_is_eink) { + _page = (_page + 1) % NUM_EINK_PAGES; // cycle info pages (Status/Radio/Traffic) + _next_refresh = 0; // redraw immediately } - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer } else if (ev == BUTTON_EVENT_LONG_PRESS) { _display->turnOn(); Serial.println("Powering Off"); - _powering_off_at = millis() + POWEROFF_DELAY; + _powering_off_at = millis() + POWEROFF_DELAY; } #endif @@ -132,9 +260,14 @@ void UITask::loop() { renderCurrScreen(); _display->endFrame(); - _next_refresh = millis() + 1000; // refresh every second + // e-ink content changes slowly (minute granularity) and the driver CRC-gates + // physical updates, so a gentle cadence avoids needless flashing/wear... + unsigned long interval = _is_eink ? EINK_REFRESH_MILLIS : 1000; + // ...but keep the boot splash / power-off countdown responsive. + if (millis() < _started_at + BOOT_SCREEN_MILLIS || _powering_off_at > 0) interval = 500; + _next_refresh = millis() + interval; } - if (millis() > _auto_off) { + if (AUTO_OFF_MILLIS > 0 && millis() > _auto_off) { _display->turnOff(); } } diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index d8e3ce1d2f..f6392fe73f 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -3,20 +3,26 @@ #include #include +class MyMesh; // fwd decl (defined in MyMesh.h) - keeps this header light + class UITask { mesh::MainBoard* _board; DisplayDriver* _display; + MyMesh* _mesh = nullptr; unsigned long _next_read, _next_refresh, _auto_off; int _prevBtnState; NodePrefs* _node_prefs; char _version_info[32]; unsigned long _powering_off_at = 0; unsigned long _started_at = 0; + bool _is_eink = false; // richer, multi-page layout is used on large e-ink panels + uint8_t _page = 0; // currently displayed page (e-ink only) void renderCurrScreen(); + void renderRichScreen(); // large e-ink multi-page dashboard public: UITask(mesh::MainBoard& board, DisplayDriver& display) : _board(&board), _display(&display) { _next_read = _next_refresh = 0; } - void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); + void begin(MyMesh* mesh, NodePrefs* node_prefs, const char* build_date, const char* firmware_version); void loop(); -}; \ No newline at end of file +}; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a714db68ec..fe8ee0fa99 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -107,7 +107,7 @@ void setup() { the_mesh.begin(fs); #ifdef DISPLAY_CLASS - ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); + ui_task.begin(&the_mesh, the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif #ifdef ETHERNET_ENABLED diff --git a/variants/heltec_e290/platformio.ini b/variants/heltec_e290/platformio.ini index c7cd5f21ea..9ed51b87b9 100644 --- a/variants/heltec_e290/platformio.ini +++ b/variants/heltec_e290/platformio.ini @@ -18,7 +18,7 @@ build_flags = -D P_LORA_MISO=11 -D P_LORA_MOSI=10 -D P_LORA_TX_LED=45 - -D PIN_USER_BTN=0 + -D PIN_USER_BTN=21 ; dedicated "Custom" button (GPIO21); Boot=0, Reset are separate -D PIN_VEXT_EN=18 -D PIN_VEXT_EN_ACTIVE=HIGH -D PIN_VBAT_READ=7 @@ -88,6 +88,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 + -D AUTO_OFF_MILLIS=0 ; keep e-ink display live (image persists w/o power anyway) build_src_filter = ${Heltec_E290_base.build_src_filter} + +<../examples/simple_repeater> diff --git a/variants/heltec_e290/target.cpp b/variants/heltec_e290/target.cpp index f23279548f..198a42c7d0 100644 --- a/variants/heltec_e290/target.cpp +++ b/variants/heltec_e290/target.cpp @@ -19,7 +19,9 @@ SensorManager sensors; #ifdef DISPLAY_CLASS DISPLAY_CLASS display(&board.periph_power); -MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +// pulldownup=true -> INPUT_PULLUP (active-low). The Custom button on GPIO21 has no +// external pull, so it floats and won't register presses without the internal pull-up. +MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); #endif bool radio_init() {