From aeecc4d78951661409408c6634f8b6bf9a58f214 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:25:51 +1000 Subject: [PATCH 1/8] Fixes Meshcore issue LilyGO T-Echo Lite displaying incorrect battery voltage Fixes #1183 --- variants/lilygo_techo_lite/TechoBoard.cpp | 43 ++++++++++---- variants/lilygo_techo_lite/TechoBoard.h | 19 +++--- variants/lilygo_techo_lite/platformio.ini | 72 +++++++++++++++++++++++ variants/lilygo_techo_lite/variant.h | 23 ++++---- 4 files changed, 126 insertions(+), 31 deletions(-) diff --git a/variants/lilygo_techo_lite/TechoBoard.cpp b/variants/lilygo_techo_lite/TechoBoard.cpp index 81d3d0c9af..a11d31b27c 100644 --- a/variants/lilygo_techo_lite/TechoBoard.cpp +++ b/variants/lilygo_techo_lite/TechoBoard.cpp @@ -8,24 +8,47 @@ void TechoBoard::begin() { NRF52Board::begin(); + // Configure battery measurement control BEFORE Wire.begin() + // to ensure P0.02 is not claimed by another peripheral + pinMode(PIN_VBAT_MEAS_EN, OUTPUT); + digitalWrite(PIN_VBAT_MEAS_EN, LOW); + pinMode(PIN_VBAT_READ, INPUT); + Wire.begin(); pinMode(SX126X_POWER_EN, OUTPUT); digitalWrite(SX126X_POWER_EN, HIGH); - delay(10); // give sx1262 some time to power up + delay(10); } uint16_t TechoBoard::getBattMilliVolts() { - int adcvalue = 0; - + // Use LilyGo's exact ADC configuration analogReference(AR_INTERNAL_3_0); analogReadResolution(12); - delay(10); - // ADC range is 0..3000mV and resolution is 12-bit (0..4095) - adcvalue = analogRead(PIN_VBAT_READ); - // Convert the raw value to compensated mv, taking the resistor- - // divider into account (providing the actual LIPO voltage) - return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); + // Enable battery voltage divider (MOSFET gate on P0.31) + pinMode(PIN_VBAT_MEAS_EN, OUTPUT); + digitalWrite(PIN_VBAT_MEAS_EN, HIGH); + + // Reclaim P0.02 for analog input (in case another peripheral touched it) + pinMode(PIN_VBAT_READ, INPUT); + delay(10); // let divider + ADC settle + + // Read and average (matching LilyGo's approach) + uint32_t sum = 0; + for (int i = 0; i < 8; i++) { + sum += analogRead(PIN_VBAT_READ); + delayMicroseconds(100); + } + uint16_t adc = sum / 8; + + // Disable divider to save power + digitalWrite(PIN_VBAT_MEAS_EN, LOW); + + // LilyGo's exact formula: adc * (3000.0 / 4096.0) * 2.0 + // = adc * 0.73242188 * 2.0 = adc * 1.46484375 + uint16_t millivolts = (uint16_t)((float)adc * (3000.0f / 4096.0f) * 2.0f); + + return millivolts; } -#endif +#endif \ No newline at end of file diff --git a/variants/lilygo_techo_lite/TechoBoard.h b/variants/lilygo_techo_lite/TechoBoard.h index fda393e7f9..7a43fd8383 100644 --- a/variants/lilygo_techo_lite/TechoBoard.h +++ b/variants/lilygo_techo_lite/TechoBoard.h @@ -4,14 +4,12 @@ #include #include -// built-ins -#define VBAT_MV_PER_LSB (0.73242188F) // 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096 - -#define VBAT_DIVIDER (0.5F) // 150K + 150K voltage divider on VBAT -#define VBAT_DIVIDER_COMP (2.0F) // Compensation factor for the VBAT divider - -#define PIN_VBAT_READ (4) -#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) +// ============================================================ +// T-Echo Lite battery pins — hardcoded from LilyGo t_echo_lite_config.h +// NOT using any defines from variant.h for battery measurement +// ============================================================ +#define PIN_VBAT_READ _PINNUM(0, 2) // BATTERY_ADC_DATA +#define PIN_VBAT_MEAS_EN _PINNUM(0, 31) // BATTERY_MEASUREMENT_CONTROL class TechoBoard : public NRF52BoardDCDC { public: @@ -20,10 +18,11 @@ class TechoBoard : public NRF52BoardDCDC { uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override { - return "LilyGo T-Echo"; + return "LilyGo T-Echo Lite"; } void powerOff() override { + digitalWrite(PIN_VBAT_MEAS_EN, LOW); #ifdef LED_RED digitalWrite(LED_RED, LOW); #endif @@ -41,4 +40,4 @@ class TechoBoard : public NRF52BoardDCDC { #endif sd_power_system_off(); } -}; +}; \ No newline at end of file diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index 0ba6a19703..45d2edfbd8 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -96,3 +96,75 @@ build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} lib_deps = ${LilyGo_T-Echo-Lite.lib_deps} densaugeo/base64 @ ~1.4.0 + +; ── Headless (Core / screenless) variants ───────────────────────── + +[LilyGo_T-Echo-Lite-Core] +extends = nrf52_base +board = t-echo +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I variants/lilygo_techo_lite + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -D LILYGO_TECHO + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_POWER_EN=30 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D P_LORA_TX_LED=LED_GREEN + -D DISABLE_DIAGNOSTIC_OUTPUT + -D ENV_INCLUDE_GPS=1 + -D GPS_BAUD_RATE=9600 + -D PIN_GPS_EN=GPS_EN + -D AUTO_OFF_MILLIS=0 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + + + + + +<../variants/lilygo_techo_lite> +lib_deps = + ${nrf52_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + adafruit/Adafruit BME280 Library @ ^2.3.0 + bakercp/CRC32 @ ^2.0.0 +debug_tool = jlink +upload_protocol = nrfutil + +[env:LilyGo_T-Echo-Lite-Core_repeater] +extends = LilyGo_T-Echo-Lite-Core +build_src_filter = ${LilyGo_T-Echo-Lite-Core.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${LilyGo_T-Echo-Lite-Core.build_flags} + -D ADVERT_NAME='"T-Echo-Lite-Core Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + +[env:LilyGo_T-Echo-Lite-Core_companion_radio_ble] +extends = LilyGo_T-Echo-Lite-Core +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${LilyGo_T-Echo-Lite-Core.build_flags} + -D MAX_CONTACTS=500 + -D MAX_GROUP_CHANNELS=12 + -D BLE_PIN_CODE=234567 + -D OFFLINE_QUEUE_SIZE=64 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 +build_src_filter = ${LilyGo_T-Echo-Lite-Core.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = + ${LilyGo_T-Echo-Lite-Core.lib_deps} + densaugeo/base64 @ ~1.4.0 \ No newline at end of file diff --git a/variants/lilygo_techo_lite/variant.h b/variants/lilygo_techo_lite/variant.h index 16e0b5cb9d..0720216516 100644 --- a/variants/lilygo_techo_lite/variant.h +++ b/variants/lilygo_techo_lite/variant.h @@ -24,7 +24,7 @@ #define PIN_PWR_EN _PINNUM(0, 30) // RT9080_EN #define BATTERY_PIN _PINNUM(0, 2) -#define ADC_MULTIPLIER (4.90F) +#define ADC_MULTIPLIER (2.0F) #define ADC_RESOLUTION (14) #define BATTERY_SENSE_RES (12) @@ -47,13 +47,13 @@ //////////////////////////////////////////////////////////////////////////////// // I2C pin definition -#define PIN_WIRE_SDA _PINNUM(0, 4) // (SDA) -#define PIN_WIRE_SCL _PINNUM(0, 2) // (SCL) +#define PIN_WIRE_SDA _PINNUM(1, 4) // (SDA) - per LilyGo IIC_1_SDA +#define PIN_WIRE_SCL _PINNUM(1, 2) // (SCL) - per LilyGo IIC_1_SCL //////////////////////////////////////////////////////////////////////////////// // SPI pin definition -#define SPI_INTERFACES_COUNT _PINNUM(0, 2) +#define SPI_INTERFACES_COUNT (2) #define PIN_SPI_MISO _PINNUM(0, 17) // (MISO) #define PIN_SPI_MOSI _PINNUM(0, 15) // (MOSI) @@ -149,10 +149,11 @@ extern const int SCK; #define PIN_DISPLAY_BUSY DISP_BUSY //////////////////////////////////////////////////////////////////////////////// -// GPS - -#define PIN_GPS_RX _PINNUM(1, 13) // RXD -#define PIN_GPS_TX _PINNUM(1, 15) // TXD -#define GPS_EN _PINNUM(1, 11) // POWER_RT9080_EN -#define PIN_GPS_STANDBY _PINNUM(1, 10) -#define PIN_GPS_PPS _PINNUM(0, 29) // 1PPS +// GPS — per LilyGo t_echo_lite_config.h +// PIN_GPS_TX/RX named from GPS module's perspective + +#define PIN_GPS_TX _PINNUM(0, 29) // GPS UART TX → MCU RX +#define PIN_GPS_RX _PINNUM(1, 10) // GPS UART RX ← MCU TX +#define GPS_EN _PINNUM(1, 11) // GPS RT9080 power enable +#define PIN_GPS_STANDBY _PINNUM(1, 13) // GPS wake-up +#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS \ No newline at end of file From 7c64c435c08e67f4aeef478fda296d38f79c7460 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 17 Apr 2026 07:26:40 +1000 Subject: [PATCH 2/8] Meshpocket morse functionality port addition --- examples/companion_radio/ui-new/UITask.cpp | 72 +++- examples/companion_radio/ui-new/UITask.h | 5 +- variants/mesh_pocket/MeshPocket.cpp | 2 +- variants/mesh_pocket/MeshPocket.h | 6 +- variants/mesh_pocket/MorseScreen.h | 399 ++++++++++++++++++++ variants/mesh_pocket/Morse_Compose_Guide.md | 113 ++++++ variants/mesh_pocket/platformio.ini | 59 +-- 7 files changed, 597 insertions(+), 59 deletions(-) create mode 100644 variants/mesh_pocket/MorseScreen.h create mode 100644 variants/mesh_pocket/Morse_Compose_Guide.md diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 94a8ee3efa..ffbc74bd3a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -31,6 +31,10 @@ #include "icons.h" +#ifdef MORSE_COMPOSE_ENABLED + #include "MorseScreen.h" +#endif + class SplashScreen : public UIScreen { UITask* _task; unsigned long dismiss_after; @@ -560,6 +564,18 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no _node_prefs = node_prefs; +#if ENV_INCLUDE_GPS == 1 + // Apply GPS preferences from stored prefs + if (_sensors != NULL && _node_prefs != NULL) { + _sensors->setSettingValue("gps", _node_prefs->gps_enabled ? "1" : "0"); + if (_node_prefs->gps_interval > 0) { + char interval_str[12]; // Max: 24 hours = 86400 seconds (5 digits + null) + sprintf(interval_str, "%u", _node_prefs->gps_interval); + _sensors->setSettingValue("gps_interval", interval_str); + } + } +#endif + if (_display != NULL) { _display->turnOn(); } @@ -579,6 +595,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no splash = new SplashScreen(this); home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); msg_preview = new MsgPreviewScreen(this, &rtc_clock); +#ifdef MORSE_COMPOSE_ENABLED + morse_screen = new MorseScreen(&rtc_clock); +#endif setCurrScreen(splash); } @@ -628,8 +647,20 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _msgcount = msgcount; ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); +#ifdef MORSE_COMPOSE_ENABLED + // Don't switch away from MorseScreen — incoming messages are shown in its + // inbox instead. Switching mid-hold would break the exit gesture. + if (curr != morse_screen) +#endif setCurrScreen(msg_preview); +#ifdef MORSE_COMPOSE_ENABLED + // Feed all incoming messages to MorseScreen inbox for display + if (morse_screen) { + ((MorseScreen*)morse_screen)->notifyPublicMsg(from_name, text); + } +#endif + if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -728,6 +759,12 @@ void UITask::loop() { c = handleTripleClick(KEY_SELECT); } #elif defined(PIN_USER_BTN) +#ifdef MORSE_COMPOSE_ENABLED + // MorseScreen handles button timing directly via isPressed() in its poll(). + // Skip MomentaryButton event processing to avoid dot/dash presses being + // misinterpreted as clicks/double-clicks/triple-clicks. + if (curr != morse_screen) { +#endif int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_NEXT); @@ -738,6 +775,9 @@ void UITask::loop() { } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { c = handleTripleClick(KEY_SELECT); } +#ifdef MORSE_COMPOSE_ENABLED + } +#endif #endif #if defined(PIN_USER_BTN_ANA) if (abs(millis() - _analogue_pin_read_millis) > 10) { @@ -780,6 +820,28 @@ void UITask::loop() { if (curr) curr->poll(); +#ifdef MORSE_COMPOSE_ENABLED + if (curr == morse_screen) { + MorseScreen* ms = (MorseScreen*)morse_screen; + if (ms->wantsExit()) { + ms->acknowledgeExit(); + gotoHomeScreen(); + } + const char* sendText = nullptr; + if (ms->consumeSendRequest(&sendText) && sendText) { + ChannelDetails ch; + if (the_mesh.getChannel(0, ch)) { + the_mesh.sendGroupMessage( + rtc_clock.getCurrentTime(), ch.channel, + the_mesh.getNodeName(), sendText, strlen(sendText)); + showAlert("Sent!", 800); + } + ms->clearOutBuf(); + } + _next_refresh = 100; // keep refreshing while on MorseScreen + } +#endif + if (_display != NULL && _display->isOn()) { if (millis() >= _next_refresh && curr) { _display->startFrame(); @@ -865,6 +927,14 @@ char UITask::handleDoubleClick(char c) { char UITask::handleTripleClick(char c) { MESH_DEBUG_PRINTLN("UITask: triple click triggered"); checkDisplayOn(c); +#ifdef MORSE_COMPOSE_ENABLED + if (curr == home) { + ((MorseScreen*)morse_screen)->activate(); + setCurrScreen(morse_screen); + c = 0; + return c; + } +#endif toggleBuzzer(); c = 0; return c; @@ -920,4 +990,4 @@ void UITask::toggleBuzzer() { showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800); _next_refresh = 0; // trigger refresh #endif -} +} \ No newline at end of file diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index a77ad6e7ec..734e0cb686 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -51,6 +51,9 @@ class UITask : public AbstractUITask { UIScreen* splash; UIScreen* home; UIScreen* msg_preview; +#ifdef MORSE_COMPOSE_ENABLED + UIScreen* morse_screen; +#endif UIScreen* curr; void userLedHandler(); @@ -98,4 +101,4 @@ class UITask : public AbstractUITask { void loop() override; void shutdown(bool restart = false); -}; +}; \ No newline at end of file diff --git a/variants/mesh_pocket/MeshPocket.cpp b/variants/mesh_pocket/MeshPocket.cpp index 0c53121f2f..a9f3f40ce8 100644 --- a/variants/mesh_pocket/MeshPocket.cpp +++ b/variants/mesh_pocket/MeshPocket.cpp @@ -9,4 +9,4 @@ void HeltecMeshPocket::begin() { pinMode(PIN_VBAT_READ, INPUT); pinMode(PIN_USER_BTN, INPUT); -} +} \ No newline at end of file diff --git a/variants/mesh_pocket/MeshPocket.h b/variants/mesh_pocket/MeshPocket.h index 478bd56d71..9f0b45e683 100644 --- a/variants/mesh_pocket/MeshPocket.h +++ b/variants/mesh_pocket/MeshPocket.h @@ -11,14 +11,14 @@ class HeltecMeshPocket : public NRF52BoardDCDC { public: - HeltecMeshPocket() : NRF52Board("MESH_POCKET_OTA") {} + HeltecMeshPocket() : NRF52Board((char*)"MESH_POCKET_OTA") {} void begin(); uint16_t getBattMilliVolts() override { int adcvalue = 0; analogReadResolution(12); analogReference(AR_INTERNAL_3_0); - pinMode(PIN_BAT_CTL, OUTPUT); // battery adc can be read only ctrl pin set to high + pinMode(PIN_BAT_CTL, OUTPUT); pinMode(PIN_VBAT_READ, INPUT); digitalWrite(PIN_BAT_CTL, HIGH); @@ -36,4 +36,4 @@ class HeltecMeshPocket : public NRF52BoardDCDC { void powerOff() override { sd_power_system_off(); } -}; +}; \ No newline at end of file diff --git a/variants/mesh_pocket/MorseScreen.h b/variants/mesh_pocket/MorseScreen.h new file mode 100644 index 0000000000..02135da34d --- /dev/null +++ b/variants/mesh_pocket/MorseScreen.h @@ -0,0 +1,399 @@ +#pragma once + +// ============================================================================= +// MorseScreen — single-button Morse compose/receive for the Meshpocket +// +// Entered from the home screen via a triple-click on the USER button when +// MORSE_COMPOSE_ENABLED is defined (Meshpocket companion builds only). +// +// While active, this screen takes exclusive ownership of the USER button: +// - Short press -> dot (<240 ms by default) +// - Longer press -> dash (>=240 ms) +// - Letter gap (~360 ms silence) commits the staged pattern to the buffer +// - Word gap (~840 ms silence) inserts a space +// - `AR` prosign (.-.-.) -> send to Public (channel 0), clear buffer +// - `HH` prosign (........) -> backspace one character +// - 5 s continuous hold -> exit back to home screen +// +// The screen maintains its own tiny ring buffer of the most recent Public +// channel messages (populated from UITask::newMsg when channel_idx == 0) so +// that it does not need to reach into ChannelScreen internals. +// +// Sending is delegated to UITask via the consumeSendRequest() flag pattern +// so that this header has no dependency on MyMesh / BaseChatMesh types. +// ============================================================================= + +#ifdef MORSE_COMPOSE_ENABLED + +#include +#include +#include +#include +#include +#include + +// user_btn is instantiated in variants/mesh_pocket/target.cpp +extern MomentaryButton user_btn; + +// ----------------------------------------------------------------------------- +// Tunables +// ----------------------------------------------------------------------------- + +// Standard Morse timing: WPM = 1.2 / dot_seconds +// 10 WPM -> dot = 120 ms +#define MORSE_DOT_UNIT_MS 120 + +// Press shorter than this = dot, longer = dash. +// 2x dot is a common midpoint threshold (dash is nominally 3x dot). +#define MORSE_DOT_DASH_MS (MORSE_DOT_UNIT_MS * 2) + +// Inter-letter silence that commits the staged pattern (3 dot units). +#define MORSE_LETTER_GAP_MS (MORSE_DOT_UNIT_MS * 3) + +// Inter-word silence that inserts a space (7 dot units). +#define MORSE_WORD_GAP_MS (MORSE_DOT_UNIT_MS * 7) + +// Exit gesture — longer than any conceivable dash, dominant hand will tire. +#define MORSE_EXIT_HOLD_MS 5000 + +// Buffer sizes +#define MORSE_OUT_BUF_LEN 134 // MeshCore per-channel msg cap is ~133 +#define MORSE_STAGING_MAX 12 // longest pattern we accept (HH = 8) +#define MORSE_INBOX_SIZE 3 +#define MORSE_INBOX_TEXT_LEN 96 +#define MORSE_INBOX_NAME_LEN 32 + +// ----------------------------------------------------------------------------- +// Morse lookup — ITU minimal + basic punctuation +// Stored in flash; tiny (~400 bytes). RAM impact: zero. +// ----------------------------------------------------------------------------- +struct MorseEntry { + char c; + const char* pat; +}; + +static const MorseEntry MORSE_TABLE[] = { + {'A', ".-"}, {'B', "-..."}, {'C', "-.-."}, {'D', "-.."}, + {'E', "."}, {'F', "..-."}, {'G', "--."}, {'H', "...."}, + {'I', ".."}, {'J', ".---"}, {'K', "-.-"}, {'L', ".-.."}, + {'M', "--"}, {'N', "-."}, {'O', "---"}, {'P', ".--."}, + {'Q', "--.-"}, {'R', ".-."}, {'S', "..."}, {'T', "-"}, + {'U', "..-"}, {'V', "...-"}, {'W', ".--"}, {'X', "-..-"}, + {'Y', "-.--"}, {'Z', "--.."}, + {'0', "-----"}, {'1', ".----"}, {'2', "..---"}, {'3', "...--"}, + {'4', "....-"}, {'5', "....."}, {'6', "-...."}, {'7', "--..."}, + {'8', "---.."}, {'9', "----."}, + {'.', ".-.-.-"},{',', "--..--"},{'?', "..--.."}, + {0, nullptr} +}; + +// ----------------------------------------------------------------------------- +class MorseScreen : public UIScreen { + mesh::RTCClock* _rtc; + + // Outgoing composition + char _outBuf[MORSE_OUT_BUF_LEN]; + uint16_t _outLen; + + // Current letter staging (dots/dashes not yet decoded) + char _staging[MORSE_STAGING_MAX]; + uint8_t _stagingLen; + + // Key timing state + bool _btnPrevPressed; + unsigned long _pressStart; + unsigned long _releaseAt; // 0 if not yet released after last press + bool _letterDecoded; // set after commitStaging() — awaits word gap + bool _wordSpaceInserted; + bool _exitArmed; // hold threshold crossed; exits on release + + // Cross-screen requests (UITask polls these) + bool _wantsExit; + bool _wantsSend; + + // Incoming ring buffer — channel 0 (Public) only + struct InboxEntry { + uint32_t timestamp; + char from[MORSE_INBOX_NAME_LEN]; + char text[MORSE_INBOX_TEXT_LEN]; + bool valid; + }; + InboxEntry _inbox[MORSE_INBOX_SIZE]; + uint8_t _inboxNewest; // index of most recent entry + uint8_t _inboxCount; + + bool _dirty; + unsigned long _nextRender; + + // --------------------------------------------------------------------------- + // Morse decode + // Returns the ASCII character for a pattern, or: + // '\x01' = AR prosign ".-.-." (send) + // '\x02' = HH prosign "........" (backspace) + // 0 = no match (silently drop) + // --------------------------------------------------------------------------- + char decodeStaging() const { + if (_stagingLen == 0) return 0; + if (strcmp(_staging, ".-.-.") == 0) return '\x01'; + if (strcmp(_staging, "........") == 0) return '\x02'; + for (const MorseEntry* e = MORSE_TABLE; e->c != 0; e++) { + if (strcmp(_staging, e->pat) == 0) return e->c; + } + return 0; + } + + void commitStaging() { + if (_stagingLen == 0) return; + char decoded = decodeStaging(); + if (decoded == '\x01') { + // AR — request send from UITask + if (_outLen > 0) _wantsSend = true; + } else if (decoded == '\x02') { + // HH — backspace one character (skip trailing space if present) + if (_outLen > 0) { + _outLen--; + _outBuf[_outLen] = 0; + } + } else if (decoded != 0) { + if (_outLen < MORSE_OUT_BUF_LEN - 1) { + _outBuf[_outLen++] = decoded; + _outBuf[_outLen] = 0; + } + } + _stagingLen = 0; + _staging[0] = 0; + _letterDecoded = true; + _wordSpaceInserted = false; + _dirty = true; + } + + void insertWordSpace() { + if (_outLen > 0 && _outBuf[_outLen - 1] != ' ' + && _outLen < MORSE_OUT_BUF_LEN - 1) { + _outBuf[_outLen++] = ' '; + _outBuf[_outLen] = 0; + _dirty = true; + } + _wordSpaceInserted = true; + } + +public: + MorseScreen(mesh::RTCClock* rtc) + : _rtc(rtc), + _outLen(0), _stagingLen(0), + _btnPrevPressed(false), _pressStart(0), _releaseAt(0), + _letterDecoded(false), _wordSpaceInserted(false), _exitArmed(false), + _wantsExit(false), _wantsSend(false), + _inboxNewest(0), _inboxCount(0), + _dirty(true), _nextRender(0) + { + _outBuf[0] = 0; + _staging[0] = 0; + memset(_inbox, 0, sizeof(_inbox)); + } + + // Called by UITask when the screen is activated (on triple-click from home) + // Resets composition state so each session starts clean. + void activate() { + _outLen = 0; _outBuf[0] = 0; + _stagingLen = 0; _staging[0] = 0; + _btnPrevPressed = user_btn.isPressed(); + _pressStart = 0; + _releaseAt = 0; + _letterDecoded = false; + _wordSpaceInserted = false; + _exitArmed = false; + _wantsExit = false; + _wantsSend = false; + _dirty = true; + } + + // Called from UITask::newMsg for incoming messages. + // `from` is the sender/channel name; `text` is the message body. + void notifyPublicMsg(const char* from, const char* text) { + _inboxNewest = (_inboxCount == 0) ? 0 : ((_inboxNewest + 1) % MORSE_INBOX_SIZE); + InboxEntry& e = _inbox[_inboxNewest]; + e.timestamp = _rtc ? _rtc->getCurrentTime() : 0; + if (from) { + strncpy(e.from, from, MORSE_INBOX_NAME_LEN - 1); + e.from[MORSE_INBOX_NAME_LEN - 1] = 0; + } else { + e.from[0] = 0; + } + if (text) { + strncpy(e.text, text, MORSE_INBOX_TEXT_LEN - 1); + e.text[MORSE_INBOX_TEXT_LEN - 1] = 0; + } else { + e.text[0] = 0; + } + e.valid = true; + if (_inboxCount < MORSE_INBOX_SIZE) _inboxCount++; + _dirty = true; + } + + // --------------------------------------------------------------------------- + // UITask bridges — polled each loop iteration + // --------------------------------------------------------------------------- + + // Returns the outgoing buffer pointer if a send was requested (AR prosign). + // Caller clears the buffer via clearOutBuf() after a successful send. + bool consumeSendRequest(const char** textOut) { + if (!_wantsSend) return false; + _wantsSend = false; + if (textOut) *textOut = _outBuf; + return true; + } + + bool wantsExit() const { return _wantsExit; } + void acknowledgeExit() { _wantsExit = false; } + + void clearOutBuf() { + _outLen = 0; + _outBuf[0] = 0; + _dirty = true; + } + + // --------------------------------------------------------------------------- + // UIScreen contract + // --------------------------------------------------------------------------- + + void poll() override { + unsigned long now = millis(); + bool pressed = user_btn.isPressed(); + + if (pressed && !_btnPrevPressed) { + // Edge: released -> pressed + _pressStart = now; + _exitArmed = false; + _letterDecoded = false; + _wordSpaceInserted = false; + } else if (!pressed && _btnPrevPressed) { + // Edge: pressed -> released + unsigned long dur = now - _pressStart; + if (_exitArmed) { + // Exit-hold completed — signal UITask to navigate back to home. + // Do NOT add this press to staging. + _wantsExit = true; + } else { + // Normal dot/dash + if (_stagingLen < MORSE_STAGING_MAX - 1) { + _staging[_stagingLen++] = (dur < MORSE_DOT_DASH_MS) ? '.' : '-'; + _staging[_stagingLen] = 0; + } + _releaseAt = now; + _dirty = true; + } + } else if (pressed && _btnPrevPressed) { + // Still holding — check for exit-arm threshold + if (!_exitArmed && (now - _pressStart) >= MORSE_EXIT_HOLD_MS) { + _exitArmed = true; + _dirty = true; // redraw to show "release to exit" hint + } + } else { + // Idle (not pressed, wasn't pressed) — check gap timers + if (_stagingLen > 0 && _releaseAt > 0 + && (now - _releaseAt) >= MORSE_LETTER_GAP_MS) { + commitStaging(); + _releaseAt = now; // reset so word gap measures from commit + } else if (_outLen > 0 && _letterDecoded && !_wordSpaceInserted + && _releaseAt > 0 + && (now - _releaseAt) >= MORSE_WORD_GAP_MS) { + insertWordSpace(); + } + } + + _btnPrevPressed = pressed; + } + + int render(DisplayDriver& display) override { + const int W = display.width(); + + display.setTextSize(1); + + // ---- Header -------------------------------------------------------------- + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, 0); + display.print("MORSE"); + + display.setColor(_exitArmed ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + display.drawTextRightAlign(W - 1, 0, _exitArmed ? "Release=exit" : "Hold=exit"); + + display.setColor(DisplayDriver::LIGHT); + display.drawRect(0, 11, W, 1); + + // ---- Inbox (last 2 messages) --------------------------------------------- + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, 13); + display.print("IN"); + + display.setColor(DisplayDriver::LIGHT); + if (_inboxCount == 0) { + display.setCursor(18, 13); + display.print("(no messages)"); + } else { + int y = 13; + for (int i = 0; i < _inboxCount && i < 2; i++) { + int idx = (int)_inboxNewest - i; + while (idx < 0) idx += MORSE_INBOX_SIZE; + const InboxEntry& e = _inbox[idx]; + if (!e.valid) continue; + display.drawTextEllipsized(18, y, W - 20, e.text); + y += 10; + } + } + + display.drawRect(0, 33, W, 1); + + // ---- Outgoing buffer ----------------------------------------------------- + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, 35); + display.print("OUT"); + + display.setColor(DisplayDriver::LIGHT); + char outWithCursor[MORSE_OUT_BUF_LEN + 2]; + if (_outLen == 0) { + strcpy(outWithCursor, "_"); + } else { + strncpy(outWithCursor, _outBuf, sizeof(outWithCursor) - 2); + outWithCursor[sizeof(outWithCursor) - 2] = 0; + size_t n = strlen(outWithCursor); + if (n < sizeof(outWithCursor) - 1) { + outWithCursor[n] = '_'; + outWithCursor[n + 1] = 0; + } + } + display.setCursor(0, 46); + display.printWordWrap(outWithCursor, W); + + display.drawRect(0, 66, W, 1); + + // ---- Staging (current key sequence) + char count ------------------------- + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, 68); + display.print("KEY"); + + display.setColor(_exitArmed ? DisplayDriver::YELLOW : DisplayDriver::LIGHT); + display.setCursor(26, 68); + display.print(_stagingLen > 0 ? _staging : " "); + + // Character count (right-aligned, same line) + display.setColor(DisplayDriver::LIGHT); + char ccBuf[12]; + snprintf(ccBuf, sizeof(ccBuf), "%u/%u", (unsigned)_outLen, + (unsigned)(MORSE_OUT_BUF_LEN - 1)); + display.drawTextRightAlign(W - 1, 68, ccBuf); + + // AR/HH hint at bottom + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, 80); + display.print("AR=send HH=bksp"); + + _dirty = false; + _nextRender = millis(); + + bool active = (_stagingLen > 0) || _btnPrevPressed || _exitArmed; + return active ? 200 : 800; + } +}; + +#endif // MORSE_COMPOSE_ENABLED \ No newline at end of file diff --git a/variants/mesh_pocket/Morse_Compose_Guide.md b/variants/mesh_pocket/Morse_Compose_Guide.md new file mode 100644 index 0000000000..6b7f8f0c64 --- /dev/null +++ b/variants/mesh_pocket/Morse_Compose_Guide.md @@ -0,0 +1,113 @@ +# Morse Compose — Meshpocket User Guide + +Morse Compose lets you type and send messages on the Public channel using the Meshpocket's single button. No keyboard needed — just press and release in the rhythm of Morse code. + +## Getting In and Out + +**Enter**: Triple-click the button from the home screen. + +**Exit**: Hold the button for **5 seconds**, then release. The screen will show "Release=exit" when the hold threshold is reached. + +## How Pressing Works + +Every button press is either a **dot** or a **dash**, determined by how long you hold: + +| Press duration | Result | +|---|---| +| Under 240 ms | Dot (·) | +| 240 ms or longer | Dash (—) | + +The timing is based on 10 words per minute, where one dot unit = 120 ms. A dash threshold is 2× the dot unit. + +## How Letters Form + +You don't need to press "confirm" after each letter — the screen detects letter boundaries automatically from silence gaps: + +| Gap (after last release) | What happens | +|---|---| +| 360 ms (3 dot units) | Current dots/dashes are decoded into a letter | +| 840 ms (7 dot units) | A space is inserted between words | + +So the flow is: press dot/dash patterns → pause briefly → letter appears → continue with the next letter. Pause a bit longer and a space is added. + +### Example: Sending "HI THERE" + +1. Press: · · · · (four quick taps) → pause 360ms → **H** appears +2. Press: · · (two quick taps) → pause 360ms → **I** appears +3. **Wait ~1 second** (840ms) → space inserted automatically → buffer shows "HI " +4. Press: — (one long press) → pause → **T** appears +5. Press: · · · · → pause → **H** appears +6. Press: · → pause → **E** appears +7. Press: · — · → pause → **R** appears +8. Press: · → pause → **E** appears +9. Buffer now shows "HI THERE" +10. Press: · — · — · (AR prosign) → message sent! + +## Sending and Correcting + +Two special Morse patterns (called prosigns) control the message: + +| Prosign | Pattern | What it does | +|---|---|---| +| **AR** | · — · — · | **Sends** the message on the Public channel and clears the buffer | +| **HH** | · · · · · · · · | **Backspace** — deletes the last character | + +AR is 5 elements (dot dash dot dash dot). HH is 8 dots in a row. + +## Morse Code Reference + +### Letters + +| Letter | Code | | Letter | Code | +|---|---|---|---|---| +| A | · — | | N | — · | +| B | — · · · | | O | — — — | +| C | — · — · | | P | · — — · | +| D | — · · | | Q | — — · — | +| E | · | | R | · — · | +| F | · · — · | | S | · · · | +| G | — — · | | T | — | +| H | · · · · | | U | · · — | +| I | · · | | V | · · · — | +| J | · — — — | | W | · — — | +| K | — · — | | X | — · · — | +| L | · — · · | | Y | — · — — | +| M | — — | | Z | — — · · | + +### Numbers + +| Number | Code | | Number | Code | +|---|---|---|---|---| +| 0 | — — — — — | | 5 | · · · · · | +| 1 | · — — — — | | 6 | — · · · · | +| 2 | · · — — — | | 7 | — — · · · | +| 3 | · · · — — | | 8 | — — — · · | +| 4 | · · · · — | | 9 | — — — — · | + +### Punctuation + +| Character | Code | +|---|---| +| . (full stop) | · — · — · — | +| , (comma) | — — · · — — | +| ? (question mark) | · · — — · · | + +## Screen Layout + +The Morse screen shows four sections: + +- **Header**: "MORSE" on the left, exit hint on the right +- **IN**: The last 2 incoming messages from any channel +- **OUT**: Your composed message so far, with a cursor +- **KEY**: The dots and dashes you've entered for the current letter (before it decodes), plus a character count + +## Tips + +- **Spaces are automatic** — just pause for about 1 second (840 ms) after finishing a word and a space appears. You don't need to enter any special pattern for spaces +- Full stop is its own Morse character (· — · — · —) — a space is automatically inserted after it via the same word-gap pause +- All output is **uppercase** — Morse code doesn't distinguish case +- The maximum message length is 133 characters +- If you make a mistake mid-letter (wrong dot/dash pattern), it won't match anything and will be silently dropped — just start the letter again after the gap +- Practice the rhythm: a dash should feel about 3× longer than a dot +- The AR prosign (send) is the same pattern as the letter R with an extra dot-dash at the end: · — · — · +- HH (backspace) is just 8 rapid dots — hard to confuse with anything else diff --git a/variants/mesh_pocket/platformio.ini b/variants/mesh_pocket/platformio.ini index 015c2ca4be..d9d1b86b95 100644 --- a/variants/mesh_pocket/platformio.ini +++ b/variants/mesh_pocket/platformio.ini @@ -20,6 +20,8 @@ build_flags = ${nrf52_base.build_flags} -D EINK_X_OFFSET=0 -D EINK_Y_OFFSET=10 -D DISPLAY_CLASS=GxEPDDisplay + -D DISPLAY_ROTATION=3 + -D MORSE_COMPOSE_ENABLED -D DISABLE_DIAGNOSTIC_OUTPUT build_src_filter = ${nrf52_base.build_src_filter} + @@ -36,34 +38,6 @@ lib_deps = debug_tool = jlink upload_protocol = nrfutil -[env:Mesh_pocket_repeater] -extends = Mesh_pocket -build_src_filter = ${Mesh_pocket.build_src_filter} - +<../examples/simple_repeater> - -build_flags = - ${Mesh_pocket.build_flags} - -D ADVERT_NAME='"Heltec_Mesh_Pocket Repeater"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D MAX_NEIGHBOURS=50 -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 - -[env:Mesh_pocket_room_server] -extends = Mesh_pocket -build_src_filter = ${Mesh_pocket.build_src_filter} - +<../examples/simple_room_server> -build_flags = - ${Mesh_pocket.build_flags} - -D ADVERT_NAME='"Heltec_Mesh_Pocket Room"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D ROOM_PASSWORD='"hello"' -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 [env:Mesh_pocket_companion_radio_ble] extends = Mesh_pocket @@ -72,12 +46,13 @@ board_upload.maximum_size = 712704 build_flags = ${Mesh_pocket.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=350 - -D MAX_GROUP_CHANNELS=40 + -D MAX_CONTACTS=500 + -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 - -D OFFLINE_QUEUE_SIZE=256 + -D OFFLINE_QUEUE_SIZE=64 -D AUTO_OFF_MILLIS=0 ; -D BLE_DEBUG_LOGGING=1 + -D MORSE_COMPOSE_ENABLED ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -85,28 +60,6 @@ build_src_filter = ${Mesh_pocket.build_src_filter} + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = - ${Mesh_pocket.lib_deps} - densaugeo/base64 @ ~1.4.0 - -[env:Mesh_pocket_companion_radio_usb] -extends = Mesh_pocket -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${Mesh_pocket.build_flags} - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=350 - -D MAX_GROUP_CHANNELS=40 - -D AUTO_OFF_MILLIS=0 -; -D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 -build_src_filter = ${Mesh_pocket.build_src_filter} - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> lib_deps = ${Mesh_pocket.lib_deps} densaugeo/base64 @ ~1.4.0 \ No newline at end of file From 42d45ad4b18016e420e32b4b95179204b60095dd Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:06:19 +1000 Subject: [PATCH 3/8] attempted eink blocking issue fix, change send to ww --- examples/companion_radio/ui-new/UITask.cpp | 1 - variants/mesh_pocket/MorseScreen.h | 21 ++++++++++++--------- variants/mesh_pocket/Morse_Compose_Guide.md | 10 +++++----- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ffbc74bd3a..5b020de2f1 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -838,7 +838,6 @@ void UITask::loop() { } ms->clearOutBuf(); } - _next_refresh = 100; // keep refreshing while on MorseScreen } #endif diff --git a/variants/mesh_pocket/MorseScreen.h b/variants/mesh_pocket/MorseScreen.h index 02135da34d..5fdf3df589 100644 --- a/variants/mesh_pocket/MorseScreen.h +++ b/variants/mesh_pocket/MorseScreen.h @@ -11,7 +11,7 @@ // - Longer press -> dash (>=240 ms) // - Letter gap (~360 ms silence) commits the staged pattern to the buffer // - Word gap (~840 ms silence) inserts a space -// - `AR` prosign (.-.-.) -> send to Public (channel 0), clear buffer +// - `WW` prosign (.--.--) -> send to Public (channel 0), clear buffer // - `HH` prosign (........) -> backspace one character // - 5 s continuous hold -> exit back to home screen // @@ -128,13 +128,13 @@ class MorseScreen : public UIScreen { // --------------------------------------------------------------------------- // Morse decode // Returns the ASCII character for a pattern, or: - // '\x01' = AR prosign ".-.-." (send) + // '\x01' = WW prosign ".--.--" (send) — W·W without letter gap // '\x02' = HH prosign "........" (backspace) // 0 = no match (silently drop) // --------------------------------------------------------------------------- char decodeStaging() const { if (_stagingLen == 0) return 0; - if (strcmp(_staging, ".-.-.") == 0) return '\x01'; + if (strcmp(_staging, ".--.--") == 0) return '\x01'; if (strcmp(_staging, "........") == 0) return '\x02'; for (const MorseEntry* e = MORSE_TABLE; e->c != 0; e++) { if (strcmp(_staging, e->pat) == 0) return e->c; @@ -146,7 +146,7 @@ class MorseScreen : public UIScreen { if (_stagingLen == 0) return; char decoded = decodeStaging(); if (decoded == '\x01') { - // AR — request send from UITask + // WW — request send from UITask if (_outLen > 0) _wantsSend = true; } else if (decoded == '\x02') { // HH — backspace one character (skip trailing space if present) @@ -235,7 +235,7 @@ class MorseScreen : public UIScreen { // UITask bridges — polled each loop iteration // --------------------------------------------------------------------------- - // Returns the outgoing buffer pointer if a send was requested (AR prosign). + // Returns the outgoing buffer pointer if a send was requested (WW prosign). // Caller clears the buffer via clearOutBuf() after a successful send. bool consumeSendRequest(const char** textOut) { if (!_wantsSend) return false; @@ -383,16 +383,19 @@ class MorseScreen : public UIScreen { (unsigned)(MORSE_OUT_BUF_LEN - 1)); display.drawTextRightAlign(W - 1, 68, ccBuf); - // AR/HH hint at bottom + // WW/HH hint at bottom display.setColor(DisplayDriver::LIGHT); display.setCursor(0, 80); - display.print("AR=send HH=bksp"); + display.print("WW=send HH=bksp"); _dirty = false; _nextRender = millis(); - bool active = (_stagingLen > 0) || _btnPrevPressed || _exitArmed; - return active ? 200 : 800; + // E-ink refresh blocks the CPU for ~644ms. A short render interval + // means the display re-renders almost continuously, starving poll() + // and causing rapid button presses to be missed. 2s gives plenty of + // time for the user to key a full letter between screen updates. + return 2000; } }; diff --git a/variants/mesh_pocket/Morse_Compose_Guide.md b/variants/mesh_pocket/Morse_Compose_Guide.md index 6b7f8f0c64..cb7dcda36c 100644 --- a/variants/mesh_pocket/Morse_Compose_Guide.md +++ b/variants/mesh_pocket/Morse_Compose_Guide.md @@ -41,7 +41,7 @@ So the flow is: press dot/dash patterns → pause briefly → letter appears → 7. Press: · — · → pause → **R** appears 8. Press: · → pause → **E** appears 9. Buffer now shows "HI THERE" -10. Press: · — · — · (AR prosign) → message sent! +10. Press: · — — · — — (WW prosign) → message sent! ## Sending and Correcting @@ -49,10 +49,10 @@ Two special Morse patterns (called prosigns) control the message: | Prosign | Pattern | What it does | |---|---|---| -| **AR** | · — · — · | **Sends** the message on the Public channel and clears the buffer | +| **WW** | · — — · — — | **Sends** the message on the Public channel and clears the buffer | | **HH** | · · · · · · · · | **Backspace** — deletes the last character | -AR is 5 elements (dot dash dot dash dot). HH is 8 dots in a row. +WW is 6 elements (dot dash dash dot dash dash — W twice without pausing). This pattern was chosen over the traditional AR prosign because "AR" appears frequently in English words, making accidental sends too easy. ## Morse Code Reference @@ -109,5 +109,5 @@ The Morse screen shows four sections: - The maximum message length is 133 characters - If you make a mistake mid-letter (wrong dot/dash pattern), it won't match anything and will be silently dropped — just start the letter again after the gap - Practice the rhythm: a dash should feel about 3× longer than a dot -- The AR prosign (send) is the same pattern as the letter R with an extra dot-dash at the end: · — · — · -- HH (backspace) is just 8 rapid dots — hard to confuse with anything else +- The WW prosign (send) is just W (· — —) typed twice without pausing: · — — · — — +- HH (backspace) is just 8 rapid dots — hard to confuse with anything else \ No newline at end of file From c90ad91f8a1f8f55faff4b1a43a0a328a8ed3515 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:15:54 +1000 Subject: [PATCH 4/8] =?UTF-8?q?Three=20fixes:=20Word=20gap:=202000ms=20?= =?UTF-8?q?=E2=86=92=203500ms.=20Your=20natural=20inter-letter=20pauses=20?= =?UTF-8?q?(1100-1700ms)=20won't=20accidentally=20trigger=20spaces=20any?= =?UTF-8?q?=20more.=20Hold=20spacing=20widened:=20BKSP=203s=20=E2=86=92=20?= =?UTF-8?q?SEND=207s=20=E2=86=92=20EXIT=209s.=20The=204-second=20window=20?= =?UTF-8?q?between=20BKSP=20and=20SEND=20(was=20only=201.5s)=20makes=20it?= =?UTF-8?q?=20much=20harder=20to=20accidentally=20send=20when=20you=20mean?= =?UTF-8?q?t=20to=20backspace.=20CRC-stable=20display:=20Removed=20the=20"?= =?UTF-8?q?ready"=E2=86=92"keying..."=20transition=20entirely.=20The=20KEY?= =?UTF-8?q?=20area=20now=20always=20shows=20"ready"=20during=20Morse=20inp?= =?UTF-8?q?ut,=20only=20changing=20for=20hold=20actions=20(which=20happen?= =?UTF-8?q?=20at=203s+=20when=20rapid=20pressing=20has=20stopped).=20This?= =?UTF-8?q?=20means=20zero=20e-ink=20render=20blocks=20during=20active=20d?= =?UTF-8?q?ot/dash=20sequences=20=E2=80=94=20every=20press=20should=20regi?= =?UTF-8?q?ster=20reliably.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/companion_radio/MyMesh.cpp | 29 +++ examples/companion_radio/MyMesh.h | 8 +- examples/companion_radio/ui-new/UITask.cpp | 26 ++- examples/companion_radio/ui-new/UITask.h | 2 + variants/mesh_pocket/MorseScreen.h | 209 ++++++++++++++------ variants/mesh_pocket/Morse_Compose_Guide.md | 66 ++++--- 6 files changed, 247 insertions(+), 93 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 60a5a75fec..4e668000d2 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2144,3 +2144,32 @@ bool MyMesh::advert() { return false; } } + +#ifdef MORSE_COMPOSE_ENABLED +void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* text) { + int i = 0; + if (app_target_ver >= 3) { + out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3; + out_frame[i++] = 0; // SNR = 0 (local) + out_frame[i++] = 0; // reserved1 + out_frame[i++] = 0; // reserved2 + } else { + out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV; + } + out_frame[i++] = channel_idx; + out_frame[i++] = 0xFF; // path_len = local/direct + out_frame[i++] = TXT_TYPE_PLAIN; + memcpy(&out_frame[i], ×tamp, 4); i += 4; + int tlen = strlen(text); + if (i + tlen > MAX_FRAME_SIZE) tlen = MAX_FRAME_SIZE - i; + memcpy(&out_frame[i], text, tlen); + i += tlen; + addToOfflineQueue(out_frame, i); + + if (_serial->isConnected()) { + uint8_t frame[1]; + frame[0] = PUSH_CODE_MSG_WAITING; + _serial->writeFrame(frame, 1); + } +} +#endif \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 3b02f5f69d..6f8a69ed6b 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -165,6 +165,12 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { public: void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } +#ifdef MORSE_COMPOSE_ENABLED + // Queue a locally-originated channel message for BLE companion app sync. + // Called from UITask after MorseScreen sends via sendGroupMessage(). + void queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* text); +#endif + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); @@ -248,4 +254,4 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table }; -extern MyMesh the_mesh; +extern MyMesh the_mesh; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 5b020de2f1..113eb1e917 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -594,7 +594,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no splash = new SplashScreen(this); home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); +#ifndef HELTEC_MESH_POCKET msg_preview = new MsgPreviewScreen(this, &rtc_clock); +#endif #ifdef MORSE_COMPOSE_ENABLED morse_screen = new MorseScreen(&rtc_clock); #endif @@ -646,6 +648,7 @@ void UITask::msgRead(int msgcount) { void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { _msgcount = msgcount; +#ifndef HELTEC_MESH_POCKET ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); #ifdef MORSE_COMPOSE_ENABLED // Don't switch away from MorseScreen — incoming messages are shown in its @@ -653,6 +656,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i if (curr != morse_screen) #endif setCurrScreen(msg_preview); +#endif #ifdef MORSE_COMPOSE_ENABLED // Feed all incoming messages to MorseScreen inbox for display @@ -831,9 +835,15 @@ void UITask::loop() { if (ms->consumeSendRequest(&sendText) && sendText) { ChannelDetails ch; if (the_mesh.getChannel(0, ch)) { - the_mesh.sendGroupMessage( - rtc_clock.getCurrentTime(), ch.channel, + uint32_t ts = rtc_clock.getCurrentTime(); + the_mesh.sendGroupMessage(ts, ch.channel, the_mesh.getNodeName(), sendText, strlen(sendText)); + // Queue for BLE companion app so it appears in chat history. + // Build "sender: text" format to match what goes over LoRa. + char fullMsg[160]; + snprintf(fullMsg, sizeof(fullMsg), "%s: %s", + the_mesh.getNodeName(), sendText); + the_mesh.queueSentChannelMessage(0, ts, fullMsg); showAlert("Sent!", 800); } ms->clearOutBuf(); @@ -920,12 +930,6 @@ char UITask::handleLongPress(char c) { char UITask::handleDoubleClick(char c) { MESH_DEBUG_PRINTLN("UITask: double click triggered"); checkDisplayOn(c); - return c; -} - -char UITask::handleTripleClick(char c) { - MESH_DEBUG_PRINTLN("UITask: triple click triggered"); - checkDisplayOn(c); #ifdef MORSE_COMPOSE_ENABLED if (curr == home) { ((MorseScreen*)morse_screen)->activate(); @@ -934,6 +938,12 @@ char UITask::handleTripleClick(char c) { return c; } #endif + return c; +} + +char UITask::handleTripleClick(char c) { + MESH_DEBUG_PRINTLN("UITask: triple click triggered"); + checkDisplayOn(c); toggleBuzzer(); c = 0; return c; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 734e0cb686..f37e8afcba 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -50,7 +50,9 @@ class UITask : public AbstractUITask { UIScreen* splash; UIScreen* home; +#ifndef HELTEC_MESH_POCKET UIScreen* msg_preview; +#endif #ifdef MORSE_COMPOSE_ENABLED UIScreen* morse_screen; #endif diff --git a/variants/mesh_pocket/MorseScreen.h b/variants/mesh_pocket/MorseScreen.h index 5fdf3df589..ec833e5906 100644 --- a/variants/mesh_pocket/MorseScreen.h +++ b/variants/mesh_pocket/MorseScreen.h @@ -3,17 +3,19 @@ // ============================================================================= // MorseScreen — single-button Morse compose/receive for the Meshpocket // -// Entered from the home screen via a triple-click on the USER button when +// Entered from the home screen via a double-click on the USER button when // MORSE_COMPOSE_ENABLED is defined (Meshpocket companion builds only). // // While active, this screen takes exclusive ownership of the USER button: -// - Short press -> dot (<240 ms by default) -// - Longer press -> dash (>=240 ms) -// - Letter gap (~360 ms silence) commits the staged pattern to the buffer -// - Word gap (~840 ms silence) inserts a space -// - `WW` prosign (.--.--) -> send to Public (channel 0), clear buffer -// - `HH` prosign (........) -> backspace one character -// - 5 s continuous hold -> exit back to home screen +// - Short press -> dot (<500 ms) +// - Medium press -> dash (500 ms – 3 s) +// - Hold 3–7 s -> BACKSPACE (deletes last character) +// - Hold 7–9 s -> SEND to Public channel, clear buffer +// - Hold 9 s+ -> EXIT back to home screen +// - Letter gap (~1 s silence) commits the staged pattern to the buffer +// - Word gap (~2 s silence) inserts a space +// - `WW` prosign (.--.--) -> send (alternative to hold) +// - `HH` prosign (........) -> backspace (alternative to hold) // // The screen maintains its own tiny ring buffer of the most recent Public // channel messages (populated from UITask::newMsg when channel_idx == 0) so @@ -36,25 +38,30 @@ extern MomentaryButton user_btn; // ----------------------------------------------------------------------------- -// Tunables +// Tunables — calibrated for single momentary button on e-ink device. +// Standard Morse timing is too tight for a button (vs a proper key) and +// e-ink renders block the CPU for ~644ms, so generous thresholds are needed. // ----------------------------------------------------------------------------- -// Standard Morse timing: WPM = 1.2 / dot_seconds -// 10 WPM -> dot = 120 ms #define MORSE_DOT_UNIT_MS 120 -// Press shorter than this = dot, longer = dash. -// 2x dot is a common midpoint threshold (dash is nominally 3x dot). -#define MORSE_DOT_DASH_MS (MORSE_DOT_UNIT_MS * 2) +// Dot/dash threshold. 500ms gives a comfortable margin — a quick tap is +// unmistakably a dot, a deliberate half-second hold is a dash. +#define MORSE_DOT_DASH_MS 500 -// Inter-letter silence that commits the staged pattern (3 dot units). -#define MORSE_LETTER_GAP_MS (MORSE_DOT_UNIT_MS * 3) +// Letter commit gap. 1s silence after the last release commits the staged +// pattern. This matches the user's natural pause between letters. +#define MORSE_LETTER_GAP_MS 1000 -// Inter-word silence that inserts a space (7 dot units). -#define MORSE_WORD_GAP_MS (MORSE_DOT_UNIT_MS * 7) +// Word space gap. 3.5s silence inserts a space. Well above natural +// inter-letter pauses (typically 1–2s) to avoid unwanted spaces. +#define MORSE_WORD_GAP_MS 3500 -// Exit gesture — longer than any conceivable dash, dominant hand will tire. -#define MORSE_EXIT_HOLD_MS 5000 +// Hold-duration actions — release at the right moment. +// Wide spacing between thresholds prevents accidental triggers. +#define MORSE_BACKSPACE_HOLD_MS 3000 +#define MORSE_SEND_HOLD_MS 7000 +#define MORSE_EXIT_HOLD_MS 9000 // Buffer sizes #define MORSE_OUT_BUF_LEN 134 // MeshCore per-channel msg cap is ~133 @@ -87,6 +94,14 @@ static const MorseEntry MORSE_TABLE[] = { {0, nullptr} }; +// Hold action states +enum HoldAction : uint8_t { + HOLD_NONE = 0, + HOLD_BACKSPACE, + HOLD_SEND, + HOLD_EXIT +}; + // ----------------------------------------------------------------------------- class MorseScreen : public UIScreen { mesh::RTCClock* _rtc; @@ -102,10 +117,10 @@ class MorseScreen : public UIScreen { // Key timing state bool _btnPrevPressed; unsigned long _pressStart; - unsigned long _releaseAt; // 0 if not yet released after last press - bool _letterDecoded; // set after commitStaging() — awaits word gap + unsigned long _releaseAt; + bool _letterDecoded; bool _wordSpaceInserted; - bool _exitArmed; // hold threshold crossed; exits on release + HoldAction _holdAction; // Cross-screen requests (UITask polls these) bool _wantsExit; @@ -146,20 +161,27 @@ class MorseScreen : public UIScreen { if (_stagingLen == 0) return; char decoded = decodeStaging(); if (decoded == '\x01') { - // WW — request send from UITask + Serial.printf("[MORSE] decoded \"%s\" -> WW (SEND), outLen=%d\n", _staging, _outLen); if (_outLen > 0) _wantsSend = true; } else if (decoded == '\x02') { - // HH — backspace one character (skip trailing space if present) + Serial.printf("[MORSE] decoded \"%s\" -> HH (BACKSPACE)\n", _staging); if (_outLen > 0) { _outLen--; _outBuf[_outLen] = 0; } } else if (decoded != 0) { + // Convert to lowercase — Morse table produces uppercase but lowercase + // reads more naturally in chat messages + if (decoded >= 'A' && decoded <= 'Z') decoded += 32; + Serial.printf("[MORSE] decoded \"%s\" -> '%c'\n", _staging, decoded); if (_outLen < MORSE_OUT_BUF_LEN - 1) { _outBuf[_outLen++] = decoded; _outBuf[_outLen] = 0; } + } else { + Serial.printf("[MORSE] decoded \"%s\" -> NO MATCH (dropped)\n", _staging); } + Serial.printf("[MORSE] outBuf: \"%s\" (%d chars)\n", _outBuf, _outLen); _stagingLen = 0; _staging[0] = 0; _letterDecoded = true; @@ -177,12 +199,23 @@ class MorseScreen : public UIScreen { _wordSpaceInserted = true; } + void doBackspace() { + _stagingLen = 0; + _staging[0] = 0; + if (_outLen > 0) { + _outLen--; + _outBuf[_outLen] = 0; + } + _dirty = true; + } + public: MorseScreen(mesh::RTCClock* rtc) : _rtc(rtc), _outLen(0), _stagingLen(0), _btnPrevPressed(false), _pressStart(0), _releaseAt(0), - _letterDecoded(false), _wordSpaceInserted(false), _exitArmed(false), + _letterDecoded(false), _wordSpaceInserted(false), + _holdAction(HOLD_NONE), _wantsExit(false), _wantsSend(false), _inboxNewest(0), _inboxCount(0), _dirty(true), _nextRender(0) @@ -192,7 +225,7 @@ class MorseScreen : public UIScreen { memset(_inbox, 0, sizeof(_inbox)); } - // Called by UITask when the screen is activated (on triple-click from home) + // Called by UITask when the screen is activated (on double-click from home) // Resets composition state so each session starts clean. void activate() { _outLen = 0; _outBuf[0] = 0; @@ -202,7 +235,7 @@ class MorseScreen : public UIScreen { _releaseAt = 0; _letterDecoded = false; _wordSpaceInserted = false; - _exitArmed = false; + _holdAction = HOLD_NONE; _wantsExit = false; _wantsSend = false; _dirty = true; @@ -262,42 +295,80 @@ class MorseScreen : public UIScreen { bool pressed = user_btn.isPressed(); if (pressed && !_btnPrevPressed) { - // Edge: released -> pressed + // ---- Edge: released -> pressed ---- _pressStart = now; - _exitArmed = false; + _holdAction = HOLD_NONE; _letterDecoded = false; _wordSpaceInserted = false; + Serial.println("[MORSE] btn DOWN"); + } else if (!pressed && _btnPrevPressed) { - // Edge: pressed -> released + // ---- Edge: pressed -> released ---- unsigned long dur = now - _pressStart; - if (_exitArmed) { - // Exit-hold completed — signal UITask to navigate back to home. - // Do NOT add this press to staging. - _wantsExit = true; - } else { - // Normal dot/dash - if (_stagingLen < MORSE_STAGING_MAX - 1) { - _staging[_stagingLen++] = (dur < MORSE_DOT_DASH_MS) ? '.' : '-'; - _staging[_stagingLen] = 0; + switch (_holdAction) { + case HOLD_EXIT: + Serial.printf("[MORSE] btn UP after %lums — EXIT\n", dur); + _wantsExit = true; + break; + case HOLD_SEND: + Serial.printf("[MORSE] btn UP after %lums — SEND, outLen=%d\n", dur, _outLen); + if (_outLen > 0) _wantsSend = true; + break; + case HOLD_BACKSPACE: + Serial.printf("[MORSE] btn UP after %lums — BACKSPACE\n", dur); + doBackspace(); + break; + default: { + // Normal dot/dash + char sym = (dur < MORSE_DOT_DASH_MS) ? '.' : '-'; + Serial.printf("[MORSE] btn UP after %lums — %s (%c)\n", dur, + sym == '.' ? "DOT" : "DASH", sym); + if (_stagingLen < MORSE_STAGING_MAX - 1) { + _staging[_stagingLen++] = sym; + _staging[_stagingLen] = 0; + } + Serial.printf("[MORSE] staging now: \"%s\" (%d elements)\n", _staging, _stagingLen); + _releaseAt = now; + _dirty = true; + break; } - _releaseAt = now; - _dirty = true; } + _holdAction = HOLD_NONE; + } else if (pressed && _btnPrevPressed) { - // Still holding — check for exit-arm threshold - if (!_exitArmed && (now - _pressStart) >= MORSE_EXIT_HOLD_MS) { - _exitArmed = true; - _dirty = true; // redraw to show "release to exit" hint + // ---- Still holding — update armed action ---- + unsigned long dur = now - _pressStart; + HoldAction newAction; + if (dur >= MORSE_EXIT_HOLD_MS) { + newAction = HOLD_EXIT; + } else if (dur >= MORSE_SEND_HOLD_MS) { + newAction = HOLD_SEND; + } else if (dur >= MORSE_BACKSPACE_HOLD_MS) { + newAction = HOLD_BACKSPACE; + } else { + newAction = HOLD_NONE; + } + if (newAction != _holdAction) { + Serial.printf("[MORSE] hold %lums — armed: %s\n", dur, + newAction == HOLD_BACKSPACE ? "BKSP" : + newAction == HOLD_SEND ? "SEND" : + newAction == HOLD_EXIT ? "EXIT" : "none"); + _holdAction = newAction; + _dirty = true; } + } else { - // Idle (not pressed, wasn't pressed) — check gap timers + // ---- Idle — check gap timers ---- if (_stagingLen > 0 && _releaseAt > 0 && (now - _releaseAt) >= MORSE_LETTER_GAP_MS) { + Serial.printf("[MORSE] letter gap %lums — committing \"%s\"\n", + now - _releaseAt, _staging); commitStaging(); - _releaseAt = now; // reset so word gap measures from commit + _releaseAt = now; } else if (_outLen > 0 && _letterDecoded && !_wordSpaceInserted && _releaseAt > 0 && (now - _releaseAt) >= MORSE_WORD_GAP_MS) { + Serial.printf("[MORSE] word gap %lums — inserting space\n", now - _releaseAt); insertWordSpace(); } } @@ -315,8 +386,15 @@ class MorseScreen : public UIScreen { display.setCursor(0, 0); display.print("MORSE"); - display.setColor(_exitArmed ? DisplayDriver::GREEN : DisplayDriver::LIGHT); - display.drawTextRightAlign(W - 1, 0, _exitArmed ? "Release=exit" : "Hold=exit"); + // Show armed hold action in header + if (_holdAction != HOLD_NONE) { + display.setColor(DisplayDriver::GREEN); + const char* action = + _holdAction == HOLD_BACKSPACE ? "[BKSP]" : + _holdAction == HOLD_SEND ? "[SEND]" : + "[EXIT]"; + display.drawTextRightAlign(W - 1, 0, action); + } display.setColor(DisplayDriver::LIGHT); display.drawRect(0, 11, W, 1); @@ -367,14 +445,27 @@ class MorseScreen : public UIScreen { display.drawRect(0, 66, W, 1); - // ---- Staging (current key sequence) + char count ------------------------- + // ---- Staging + char count ------------------------------------------------ + // CRITICAL: The KEY area must NOT change CRC during active dot/dash input. + // Any CRC change triggers a 644ms e-ink block that eats button presses. + // Only hold actions (3s+) change the display here — by then the user has + // stopped rapid-pressing so one render block is harmless. display.setColor(DisplayDriver::GREEN); display.setCursor(0, 68); display.print("KEY"); - display.setColor(_exitArmed ? DisplayDriver::YELLOW : DisplayDriver::LIGHT); display.setCursor(26, 68); - display.print(_stagingLen > 0 ? _staging : " "); + if (_holdAction != HOLD_NONE) { + display.setColor(DisplayDriver::YELLOW); + const char* action = + _holdAction == HOLD_BACKSPACE ? "[BKSP]" : + _holdAction == HOLD_SEND ? "[SEND]" : + "[EXIT]"; + display.print(action); + } else { + display.setColor(DisplayDriver::LIGHT); + display.print("ready"); + } // Character count (right-aligned, same line) display.setColor(DisplayDriver::LIGHT); @@ -386,16 +477,16 @@ class MorseScreen : public UIScreen { // WW/HH hint at bottom display.setColor(DisplayDriver::LIGHT); display.setCursor(0, 80); - display.print("WW=send HH=bksp"); + display.print("Hold 3s=bksp 7s=send 9s=exit"); _dirty = false; _nextRender = millis(); - // E-ink refresh blocks the CPU for ~644ms. A short render interval - // means the display re-renders almost continuously, starving poll() - // and causing rapid button presses to be missed. 2s gives plenty of - // time for the user to key a full letter between screen updates. - return 2000; + // T-Deck Pro render throttle pattern: 800ms minimum after endFrame() + // guarantees unblocked poll() time for button sampling. The CRC check + // in endFrame() means renders only block (~644ms) when content actually + // changed — unchanged frames return instantly regardless of interval. + return 800; } }; diff --git a/variants/mesh_pocket/Morse_Compose_Guide.md b/variants/mesh_pocket/Morse_Compose_Guide.md index cb7dcda36c..e7be32efc8 100644 --- a/variants/mesh_pocket/Morse_Compose_Guide.md +++ b/variants/mesh_pocket/Morse_Compose_Guide.md @@ -4,9 +4,9 @@ Morse Compose lets you type and send messages on the Public channel using the Me ## Getting In and Out -**Enter**: Triple-click the button from the home screen. +**Enter**: Double-click the button from the home screen. -**Exit**: Hold the button for **5 seconds**, then release. The screen will show "Release=exit" when the hold threshold is reached. +**Exit**: Hold the button for **9 seconds**, then release. The display shows `[EXIT]` when the threshold is reached. ## How Pressing Works @@ -14,10 +14,10 @@ Every button press is either a **dot** or a **dash**, determined by how long you | Press duration | Result | |---|---| -| Under 240 ms | Dot (·) | -| 240 ms or longer | Dash (—) | +| Under 500 ms | Dot (·) | +| 500 ms or longer | Dash (—) | -The timing is based on 10 words per minute, where one dot unit = 120 ms. A dash threshold is 2× the dot unit. +A quick tap (under half a second) is a dot. A deliberate half-second hold is a dash. The threshold is generous to avoid accidental dashes. ## How Letters Form @@ -25,34 +25,46 @@ You don't need to press "confirm" after each letter — the screen detects lette | Gap (after last release) | What happens | |---|---| -| 360 ms (3 dot units) | Current dots/dashes are decoded into a letter | -| 840 ms (7 dot units) | A space is inserted between words | +| 1 second | Current dots/dashes are decoded into a letter | +| 3.5 seconds | A space is inserted between words | -So the flow is: press dot/dash patterns → pause briefly → letter appears → continue with the next letter. Pause a bit longer and a space is added. +So the flow is: press dot/dash patterns → pause for about 1 second → letter appears → continue with the next letter. Pause for 3.5 seconds and a space is added. ### Example: Sending "HI THERE" -1. Press: · · · · (four quick taps) → pause 360ms → **H** appears -2. Press: · · (two quick taps) → pause 360ms → **I** appears -3. **Wait ~1 second** (840ms) → space inserted automatically → buffer shows "HI " -4. Press: — (one long press) → pause → **T** appears +1. Press: · · · · (four quick taps) → pause 1 second → **H** appears +2. Press: · · (two quick taps) → pause 1 second → **I** appears +3. **Wait ~3.5 seconds** → space inserted automatically → buffer shows "HI " +4. Press: — (one half-second press) → pause → **T** appears 5. Press: · · · · → pause → **H** appears 6. Press: · → pause → **E** appears 7. Press: · — · → pause → **R** appears 8. Press: · → pause → **E** appears 9. Buffer now shows "HI THERE" -10. Press: · — — · — — (WW prosign) → message sent! +10. Hold for ~8 seconds → display shows `[SEND]` → release → message sent! -## Sending and Correcting +## Sending, Correcting, and Exiting -Two special Morse patterns (called prosigns) control the message: +There are two ways to send, backspace, and exit: -| Prosign | Pattern | What it does | +### Method 1: Hold durations (easier) + +Just hold the button and release at the right moment. The display shows which action is armed: + +| Hold duration | Display shows | What happens on release | |---|---|---| -| **WW** | · — — · — — | **Sends** the message on the Public channel and clears the buffer | -| **HH** | · · · · · · · · | **Backspace** — deletes the last character | +| 3 – 7 s | `[BKSP]` | **Backspace** — deletes the last character | +| 7 – 9 s | `[SEND]` | **Send** — sends the message on the Public channel | +| 9 s+ | `[EXIT]` | **Exit** — returns to the home screen | + +### Method 2: Prosigns (advanced) -WW is 6 elements (dot dash dash dot dash dash — W twice without pausing). This pattern was chosen over the traditional AR prosign because "AR" appears frequently in English words, making accidental sends too easy. +Two special Morse patterns also work as alternatives: + +| Prosign | Pattern | What it does | +|---|---|---| +| **WW** | · — — · — — | **Sends** the message (two W's without a letter gap) | +| **HH** | · · · · · · · · | **Backspace** (8 rapid dots within the 1-second letter gap) | ## Morse Code Reference @@ -97,17 +109,21 @@ WW is 6 elements (dot dash dash dot dash dash — W twice without pausing). This The Morse screen shows four sections: - **Header**: "MORSE" on the left, exit hint on the right -- **IN**: The last 2 incoming messages from any channel +- **IN**: The last 2 incoming messages - **OUT**: Your composed message so far, with a cursor - **KEY**: The dots and dashes you've entered for the current letter (before it decodes), plus a character count +A hint at the bottom shows `Hold 3s=bksp 7s=send 9s=exit` as a reminder. + ## Tips -- **Spaces are automatic** — just pause for about 1 second (840 ms) after finishing a word and a space appears. You don't need to enter any special pattern for spaces +- **Spaces are automatic** — just pause for about 3.5 seconds after finishing a word and a space appears - Full stop is its own Morse character (· — · — · —) — a space is automatically inserted after it via the same word-gap pause - All output is **uppercase** — Morse code doesn't distinguish case - The maximum message length is 133 characters -- If you make a mistake mid-letter (wrong dot/dash pattern), it won't match anything and will be silently dropped — just start the letter again after the gap -- Practice the rhythm: a dash should feel about 3× longer than a dot -- The WW prosign (send) is just W (· — —) typed twice without pausing: · — — · — — -- HH (backspace) is just 8 rapid dots — hard to confuse with anything else \ No newline at end of file +- If you enter a wrong dot/dash pattern, it won't match any character and will be silently dropped — just start the letter again after the gap +- A dot is a quick tap (under half a second), a dash is a deliberate hold (half a second or longer) +- The display doesn't update during active keying to avoid blocking button presses — your letters appear when the 1-second letter gap commits them +- M (— —) requires **two separate presses**, each held for about half a second, with a brief release between them +- **Hold durations are the easiest way to send, backspace, and exit** — just hold and watch the display for `[BKSP]`, `[SEND]`, or `[EXIT]`, then release +- The WW prosign and HH prosign still work as alternatives for advanced users \ No newline at end of file From bdf21b7dfb6d85e7e040687c865cb6d748a53a1f Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:55:12 +1000 Subject: [PATCH 5/8] checked heap remaining and left debug lines commented out, increasted offline queue size in build to 256; bug fix for ble msg display hop count for sent messages --- examples/companion_radio/MyMesh.cpp | 2 +- examples/companion_radio/main.cpp | 17 +- examples/companion_radio/ui-new/UITask.cpp | 40 ++++- examples/companion_radio/ui-new/UITask.h | 1 + variants/mesh_pocket/MorseScreen.h | 181 +++++++++++++++++---- variants/mesh_pocket/platformio.ini | 2 +- 6 files changed, 206 insertions(+), 37 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 4e668000d2..5ffe205bd6 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2157,7 +2157,7 @@ void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, co out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV; } out_frame[i++] = channel_idx; - out_frame[i++] = 0xFF; // path_len = local/direct + out_frame[i++] = 0; // path_len = 0 (local, zero hops) out_frame[i++] = TXT_TYPE_PLAIN; memcpy(&out_frame[i], ×tamp, 4); i += 4; int tlen = strlen(text); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 876dc9c33c..d1424fdbb1 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -220,6 +220,13 @@ void setup() { #ifdef DISPLAY_CLASS ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved #endif + + // [DEBUG] Uncomment to check free heap / offline queue sizing: + // Serial.println("[HEAP] === After setup ==="); + // dbgMemInfo(); + // Serial.printf("[HEAP] OFFLINE_QUEUE_SIZE=%d, frame size=%d bytes, queue total=%d bytes\n", + // OFFLINE_QUEUE_SIZE, (int)(1 + MAX_FRAME_SIZE), + // OFFLINE_QUEUE_SIZE * (int)(1 + MAX_FRAME_SIZE)); } void loop() { @@ -229,4 +236,12 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); -} + + // [DEBUG] Uncomment for periodic heap monitoring: + // static unsigned long next_heap_print = 30000; + // if (millis() > next_heap_print) { + // Serial.println("[HEAP] === Periodic ==="); + // dbgMemInfo(); + // next_heap_print = millis() + 60000; + // } +} \ No newline at end of file diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 113eb1e917..4d3c7964e1 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -599,6 +599,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #endif #ifdef MORSE_COMPOSE_ENABLED morse_screen = new MorseScreen(&rtc_clock); + morse_channel_picker = new MorseChannelPicker(); #endif setCurrScreen(splash); } @@ -825,6 +826,23 @@ void UITask::loop() { if (curr) curr->poll(); #ifdef MORSE_COMPOSE_ENABLED + // Channel picker → MorseScreen transition + if (curr == morse_channel_picker) { + MorseChannelPicker* picker = (MorseChannelPicker*)morse_channel_picker; + if (picker->isConfirmed()) { + uint8_t ch_idx = picker->getSelectedChannelIdx(); + const char* ch_name = picker->getSelectedChannelName(); + ((MorseScreen*)morse_screen)->activate(ch_idx, ch_name); + setCurrScreen(morse_screen); + picker->acknowledgeConfirm(); + } + if (picker->wantsExit()) { + picker->acknowledgeExit(); + gotoHomeScreen(); + } + } + + // MorseScreen send/exit handling if (curr == morse_screen) { MorseScreen* ms = (MorseScreen*)morse_screen; if (ms->wantsExit()) { @@ -833,17 +851,16 @@ void UITask::loop() { } const char* sendText = nullptr; if (ms->consumeSendRequest(&sendText) && sendText) { + uint8_t ch_idx = ms->getChannelIdx(); ChannelDetails ch; - if (the_mesh.getChannel(0, ch)) { + if (the_mesh.getChannel(ch_idx, ch)) { uint32_t ts = rtc_clock.getCurrentTime(); the_mesh.sendGroupMessage(ts, ch.channel, the_mesh.getNodeName(), sendText, strlen(sendText)); - // Queue for BLE companion app so it appears in chat history. - // Build "sender: text" format to match what goes over LoRa. char fullMsg[160]; snprintf(fullMsg, sizeof(fullMsg), "%s: %s", the_mesh.getNodeName(), sendText); - the_mesh.queueSentChannelMessage(0, ts, fullMsg); + the_mesh.queueSentChannelMessage(ch_idx, ts, fullMsg); showAlert("Sent!", 800); } ms->clearOutBuf(); @@ -932,8 +949,19 @@ char UITask::handleDoubleClick(char c) { checkDisplayOn(c); #ifdef MORSE_COMPOSE_ENABLED if (curr == home) { - ((MorseScreen*)morse_screen)->activate(); - setCurrScreen(morse_screen); + // Populate channel picker with available channels + MorseChannelPicker* picker = (MorseChannelPicker*)morse_channel_picker; + picker->activate(); + ChannelDetails ch; + for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { + if (the_mesh.getChannel(i, ch) && ch.name[0] != 0) { + picker->addChannel(i, ch.name); + } + } + setCurrScreen(morse_channel_picker); + // [DEBUG] Uncomment to check heap at Morse entry: + // Serial.println("[HEAP] === Morse entry ==="); + // dbgMemInfo(); c = 0; return c; } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index f37e8afcba..2deaace46f 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -55,6 +55,7 @@ class UITask : public AbstractUITask { #endif #ifdef MORSE_COMPOSE_ENABLED UIScreen* morse_screen; + UIScreen* morse_channel_picker; #endif UIScreen* curr; diff --git a/variants/mesh_pocket/MorseScreen.h b/variants/mesh_pocket/MorseScreen.h index ec833e5906..995ad55c8a 100644 --- a/variants/mesh_pocket/MorseScreen.h +++ b/variants/mesh_pocket/MorseScreen.h @@ -109,6 +109,8 @@ class MorseScreen : public UIScreen { // Outgoing composition char _outBuf[MORSE_OUT_BUF_LEN]; uint16_t _outLen; + uint8_t _channelIdx; + char _channelName[32]; // Current letter staging (dots/dashes not yet decoded) char _staging[MORSE_STAGING_MAX]; @@ -161,10 +163,10 @@ class MorseScreen : public UIScreen { if (_stagingLen == 0) return; char decoded = decodeStaging(); if (decoded == '\x01') { - Serial.printf("[MORSE] decoded \"%s\" -> WW (SEND), outLen=%d\n", _staging, _outLen); + // Serial.printf("[MORSE] decoded \"%s\" -> WW (SEND), outLen=%d\n", _staging, _outLen); if (_outLen > 0) _wantsSend = true; } else if (decoded == '\x02') { - Serial.printf("[MORSE] decoded \"%s\" -> HH (BACKSPACE)\n", _staging); + // Serial.printf("[MORSE] decoded \"%s\" -> HH (BACKSPACE)\n", _staging); if (_outLen > 0) { _outLen--; _outBuf[_outLen] = 0; @@ -173,15 +175,15 @@ class MorseScreen : public UIScreen { // Convert to lowercase — Morse table produces uppercase but lowercase // reads more naturally in chat messages if (decoded >= 'A' && decoded <= 'Z') decoded += 32; - Serial.printf("[MORSE] decoded \"%s\" -> '%c'\n", _staging, decoded); + // Serial.printf("[MORSE] decoded \"%s\" -> '%c'\n", _staging, decoded); if (_outLen < MORSE_OUT_BUF_LEN - 1) { _outBuf[_outLen++] = decoded; _outBuf[_outLen] = 0; } } else { - Serial.printf("[MORSE] decoded \"%s\" -> NO MATCH (dropped)\n", _staging); + // Serial.printf("[MORSE] decoded \"%s\" -> NO MATCH (dropped)\n", _staging); } - Serial.printf("[MORSE] outBuf: \"%s\" (%d chars)\n", _outBuf, _outLen); + // Serial.printf("[MORSE] outBuf: \"%s\" (%d chars)\n", _outBuf, _outLen); _stagingLen = 0; _staging[0] = 0; _letterDecoded = true; @@ -212,7 +214,7 @@ class MorseScreen : public UIScreen { public: MorseScreen(mesh::RTCClock* rtc) : _rtc(rtc), - _outLen(0), _stagingLen(0), + _outLen(0), _channelIdx(0), _stagingLen(0), _btnPrevPressed(false), _pressStart(0), _releaseAt(0), _letterDecoded(false), _wordSpaceInserted(false), _holdAction(HOLD_NONE), @@ -222,12 +224,15 @@ class MorseScreen : public UIScreen { { _outBuf[0] = 0; _staging[0] = 0; + strcpy(_channelName, "Public"); memset(_inbox, 0, sizeof(_inbox)); } - // Called by UITask when the screen is activated (on double-click from home) - // Resets composition state so each session starts clean. - void activate() { + // Called by UITask after channel picker selects a channel. + void activate(uint8_t channelIdx, const char* channelName) { + _channelIdx = channelIdx; + strncpy(_channelName, channelName, sizeof(_channelName) - 1); + _channelName[sizeof(_channelName) - 1] = 0; _outLen = 0; _outBuf[0] = 0; _stagingLen = 0; _staging[0] = 0; _btnPrevPressed = user_btn.isPressed(); @@ -241,6 +246,9 @@ class MorseScreen : public UIScreen { _dirty = true; } + uint8_t getChannelIdx() const { return _channelIdx; } + const char* getChannelName() const { return _channelName; } + // Called from UITask::newMsg for incoming messages. // `from` is the sender/channel name; `text` is the message body. void notifyPublicMsg(const char* from, const char* text) { @@ -300,34 +308,34 @@ class MorseScreen : public UIScreen { _holdAction = HOLD_NONE; _letterDecoded = false; _wordSpaceInserted = false; - Serial.println("[MORSE] btn DOWN"); + // Serial.println("[MORSE] btn DOWN"); } else if (!pressed && _btnPrevPressed) { // ---- Edge: pressed -> released ---- unsigned long dur = now - _pressStart; switch (_holdAction) { case HOLD_EXIT: - Serial.printf("[MORSE] btn UP after %lums — EXIT\n", dur); + // Serial.printf("[MORSE] btn UP after %lums — EXIT\n", dur); _wantsExit = true; break; case HOLD_SEND: - Serial.printf("[MORSE] btn UP after %lums — SEND, outLen=%d\n", dur, _outLen); + // Serial.printf("[MORSE] btn UP after %lums — SEND, outLen=%d\n", dur, _outLen); if (_outLen > 0) _wantsSend = true; break; case HOLD_BACKSPACE: - Serial.printf("[MORSE] btn UP after %lums — BACKSPACE\n", dur); + // Serial.printf("[MORSE] btn UP after %lums — BACKSPACE\n", dur); doBackspace(); break; default: { // Normal dot/dash char sym = (dur < MORSE_DOT_DASH_MS) ? '.' : '-'; - Serial.printf("[MORSE] btn UP after %lums — %s (%c)\n", dur, - sym == '.' ? "DOT" : "DASH", sym); + // Serial.printf("[MORSE] btn UP after %lums — %s (%c)\n", dur, + // sym == '.' ? "DOT" : "DASH", sym); if (_stagingLen < MORSE_STAGING_MAX - 1) { _staging[_stagingLen++] = sym; _staging[_stagingLen] = 0; } - Serial.printf("[MORSE] staging now: \"%s\" (%d elements)\n", _staging, _stagingLen); + // Serial.printf("[MORSE] staging now: \"%s\" (%d elements)\n", _staging, _stagingLen); _releaseAt = now; _dirty = true; break; @@ -349,10 +357,10 @@ class MorseScreen : public UIScreen { newAction = HOLD_NONE; } if (newAction != _holdAction) { - Serial.printf("[MORSE] hold %lums — armed: %s\n", dur, - newAction == HOLD_BACKSPACE ? "BKSP" : - newAction == HOLD_SEND ? "SEND" : - newAction == HOLD_EXIT ? "EXIT" : "none"); + // Serial.printf("[MORSE] hold %lums — armed: %s\n", dur, + // newAction == HOLD_BACKSPACE ? "BKSP" : + // newAction == HOLD_SEND ? "SEND" : + // newAction == HOLD_EXIT ? "EXIT" : "none"); _holdAction = newAction; _dirty = true; } @@ -361,14 +369,14 @@ class MorseScreen : public UIScreen { // ---- Idle — check gap timers ---- if (_stagingLen > 0 && _releaseAt > 0 && (now - _releaseAt) >= MORSE_LETTER_GAP_MS) { - Serial.printf("[MORSE] letter gap %lums — committing \"%s\"\n", - now - _releaseAt, _staging); + // Serial.printf("[MORSE] letter gap %lums — committing \"%s\"\n", + // now - _releaseAt, _staging); commitStaging(); _releaseAt = now; } else if (_outLen > 0 && _letterDecoded && !_wordSpaceInserted && _releaseAt > 0 && (now - _releaseAt) >= MORSE_WORD_GAP_MS) { - Serial.printf("[MORSE] word gap %lums — inserting space\n", now - _releaseAt); + // Serial.printf("[MORSE] word gap %lums — inserting space\n", now - _releaseAt); insertWordSpace(); } } @@ -384,7 +392,9 @@ class MorseScreen : public UIScreen { // ---- Header -------------------------------------------------------------- display.setColor(DisplayDriver::YELLOW); display.setCursor(0, 0); - display.print("MORSE"); + char hdr[40]; + snprintf(hdr, sizeof(hdr), "MORSE > %s", _channelName); + display.print(hdr); // Show armed hold action in header if (_holdAction != HOLD_NONE) { @@ -474,10 +484,7 @@ class MorseScreen : public UIScreen { (unsigned)(MORSE_OUT_BUF_LEN - 1)); display.drawTextRightAlign(W - 1, 68, ccBuf); - // WW/HH hint at bottom - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, 80); - display.print("Hold 3s=bksp 7s=send 9s=exit"); + // Hint: Hold 3s=bksp 7s=send 9s=exit | WW=send HH=bksp _dirty = false; _nextRender = millis(); @@ -490,4 +497,122 @@ class MorseScreen : public UIScreen { } }; +// ============================================================================= +// MorseChannelPicker — select which channel to compose Morse messages on. +// +// Shown after double-click from home, before entering MorseScreen. +// Click cycles highlight, double-click selects. +// ============================================================================= + +#define MORSE_PICKER_MAX_CHANNELS 8 + +class MorseChannelPicker : public UIScreen { + struct ChannelEntry { + uint8_t idx; + char name[32]; + bool valid; + }; + + ChannelEntry _channels[MORSE_PICKER_MAX_CHANNELS]; + uint8_t _numChannels; + uint8_t _highlighted; + bool _confirmed; + bool _wantsExit; + +public: + MorseChannelPicker() + : _numChannels(0), _highlighted(0), _confirmed(false), _wantsExit(false) + { + memset(_channels, 0, sizeof(_channels)); + } + + void activate() { + _numChannels = 0; + _highlighted = 0; + _confirmed = false; + _wantsExit = false; + memset(_channels, 0, sizeof(_channels)); + } + + // Called by UITask to populate available channels before showing the picker. + void addChannel(uint8_t idx, const char* name) { + if (_numChannels >= MORSE_PICKER_MAX_CHANNELS) return; + _channels[_numChannels].idx = idx; + strncpy(_channels[_numChannels].name, name, 31); + _channels[_numChannels].name[31] = 0; + _channels[_numChannels].valid = true; + _numChannels++; + } + + bool isConfirmed() const { return _confirmed; } + void acknowledgeConfirm() { _confirmed = false; } + bool wantsExit() const { return _wantsExit; } + void acknowledgeExit() { _wantsExit = false; } + + uint8_t getSelectedChannelIdx() const { + if (_highlighted < _numChannels) + return _channels[_highlighted].idx; + return 0; + } + + const char* getSelectedChannelName() const { + if (_highlighted < _numChannels) + return _channels[_highlighted].name; + return "Public"; + } + + int render(DisplayDriver& display) override { + const int W = display.width(); + + display.setTextSize(1); + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, 0); + display.print("SELECT CHANNEL"); + + display.setColor(DisplayDriver::LIGHT); + display.drawRect(0, 11, W, 1); + + int y = 16; + for (uint8_t i = 0; i < _numChannels; i++) { + if (i == _highlighted) { + display.setColor(DisplayDriver::DARK); + display.fillRect(0, y - 1, W, 12); + display.setColor(DisplayDriver::LIGHT); + } else { + display.setColor(DisplayDriver::LIGHT); + } + char line[40]; + snprintf(line, sizeof(line), " %s", _channels[i].name); + if (i == _highlighted) line[0] = '>'; + display.setCursor(0, y); + display.print(line); + y += 14; + } + + // Hint: Click=next DblClick=select LongPress=exit + + return 5000; + } + + bool handleInput(char c) override { + if (c == KEY_NEXT) { + // Cycle highlight + if (_numChannels > 0) + _highlighted = (_highlighted + 1) % _numChannels; + return true; + } + if (c == KEY_PREV) { + // Double-click = select + _confirmed = true; + return true; + } + if (c == KEY_ENTER) { + // Long press = exit back to home + _wantsExit = true; + return true; + } + return false; + } +}; + #endif // MORSE_COMPOSE_ENABLED \ No newline at end of file diff --git a/variants/mesh_pocket/platformio.ini b/variants/mesh_pocket/platformio.ini index d9d1b86b95..b721bccd04 100644 --- a/variants/mesh_pocket/platformio.ini +++ b/variants/mesh_pocket/platformio.ini @@ -49,7 +49,7 @@ build_flags = -D MAX_CONTACTS=500 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 - -D OFFLINE_QUEUE_SIZE=64 + -D OFFLINE_QUEUE_SIZE=256 -D AUTO_OFF_MILLIS=0 ; -D BLE_DEBUG_LOGGING=1 -D MORSE_COMPOSE_ENABLED From 250a6f7bafec5f2da83c6381f9fb9ff3e657a7e4 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:05:11 +1000 Subject: [PATCH 6/8] fix bug that displayed all incoming msgs instead of just select channel; updated morse guide --- variants/mesh_pocket/MorseScreen.h | 4 +- variants/mesh_pocket/Morse_Compose_Guide.md | 44 ++++++++++----------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/variants/mesh_pocket/MorseScreen.h b/variants/mesh_pocket/MorseScreen.h index 995ad55c8a..3e4904728a 100644 --- a/variants/mesh_pocket/MorseScreen.h +++ b/variants/mesh_pocket/MorseScreen.h @@ -250,8 +250,10 @@ class MorseScreen : public UIScreen { const char* getChannelName() const { return _channelName; } // Called from UITask::newMsg for incoming messages. - // `from` is the sender/channel name; `text` is the message body. + // `from` is the channel name; `text` is the message body. + // Only accepts messages matching the currently selected channel. void notifyPublicMsg(const char* from, const char* text) { + if (!from || strcmp(from, _channelName) != 0) return; // wrong channel _inboxNewest = (_inboxCount == 0) ? 0 : ((_inboxNewest + 1) % MORSE_INBOX_SIZE); InboxEntry& e = _inbox[_inboxNewest]; e.timestamp = _rtc ? _rtc->getCurrentTime() : 0; diff --git a/variants/mesh_pocket/Morse_Compose_Guide.md b/variants/mesh_pocket/Morse_Compose_Guide.md index e7be32efc8..6079ec46cc 100644 --- a/variants/mesh_pocket/Morse_Compose_Guide.md +++ b/variants/mesh_pocket/Morse_Compose_Guide.md @@ -1,12 +1,12 @@ # Morse Compose — Meshpocket User Guide -Morse Compose lets you type and send messages on the Public channel using the Meshpocket's single button. No keyboard needed — just press and release in the rhythm of Morse code. +Morse Compose lets you type and send messages on any configured channel using the Meshpocket's single button. No keyboard needed — just press and release in the rhythm of Morse code. ## Getting In and Out -**Enter**: Double-click the button from the home screen. +**Enter**: Double-click the button from the home screen. A channel picker appears — click to cycle between channels, then double-click to select. The Morse compose screen opens on the chosen channel. -**Exit**: Hold the button for **9 seconds**, then release. The display shows `[EXIT]` when the threshold is reached. +**Exit**: Hold the button for **9 seconds**, then release. The display shows `[EXIT]` when the threshold is reached. You can also exit by long-pressing from the channel picker. ## How Pressing Works @@ -15,7 +15,7 @@ Every button press is either a **dot** or a **dash**, determined by how long you | Press duration | Result | |---|---| | Under 500 ms | Dot (·) | -| 500 ms or longer | Dash (—) | +| 500 ms – 3 s | Dash (—) | A quick tap (under half a second) is a dot. A deliberate half-second hold is a dash. The threshold is generous to avoid accidental dashes. @@ -30,17 +30,17 @@ You don't need to press "confirm" after each letter — the screen detects lette So the flow is: press dot/dash patterns → pause for about 1 second → letter appears → continue with the next letter. Pause for 3.5 seconds and a space is added. -### Example: Sending "HI THERE" - -1. Press: · · · · (four quick taps) → pause 1 second → **H** appears -2. Press: · · (two quick taps) → pause 1 second → **I** appears -3. **Wait ~3.5 seconds** → space inserted automatically → buffer shows "HI " -4. Press: — (one half-second press) → pause → **T** appears -5. Press: · · · · → pause → **H** appears -6. Press: · → pause → **E** appears -7. Press: · — · → pause → **R** appears -8. Press: · → pause → **E** appears -9. Buffer now shows "HI THERE" +### Example: Sending "hi there" + +1. Press: · · · · (four quick taps) → pause 1 second → **h** appears +2. Press: · · (two quick taps) → pause 1 second → **i** appears +3. **Wait ~3.5 seconds** → space inserted automatically → buffer shows "hi " +4. Press: — (one half-second press) → pause → **t** appears +5. Press: · · · · → pause → **h** appears +6. Press: · → pause → **e** appears +7. Press: · — · → pause → **r** appears +8. Press: · → pause → **e** appears +9. Buffer now shows "hi there" 10. Hold for ~8 seconds → display shows `[SEND]` → release → message sent! ## Sending, Correcting, and Exiting @@ -54,7 +54,7 @@ Just hold the button and release at the right moment. The display shows which ac | Hold duration | Display shows | What happens on release | |---|---|---| | 3 – 7 s | `[BKSP]` | **Backspace** — deletes the last character | -| 7 – 9 s | `[SEND]` | **Send** — sends the message on the Public channel | +| 7 – 9 s | `[SEND]` | **Send** — sends the message on the selected channel | | 9 s+ | `[EXIT]` | **Exit** — returns to the home screen | ### Method 2: Prosigns (advanced) @@ -108,18 +108,18 @@ Two special Morse patterns also work as alternatives: The Morse screen shows four sections: -- **Header**: "MORSE" on the left, exit hint on the right -- **IN**: The last 2 incoming messages -- **OUT**: Your composed message so far, with a cursor -- **KEY**: The dots and dashes you've entered for the current letter (before it decodes), plus a character count +- **Header**: "MORSE > channelname" showing which channel you're composing on. When a hold action is armed, `[BKSP]`, `[SEND]`, or `[EXIT]` appears on the right. +- **IN**: The last 2 incoming messages on the selected channel only (messages from other channels are filtered out). +- **OUT**: Your composed message so far, with a cursor. +- **KEY**: Shows "ready" during normal use. During a hold, shows the armed action. -A hint at the bottom shows `Hold 3s=bksp 7s=send 9s=exit` as a reminder. +Sent messages also appear in the MeshCore companion app's channel history if BLE is connected. ## Tips - **Spaces are automatic** — just pause for about 3.5 seconds after finishing a word and a space appears - Full stop is its own Morse character (· — · — · —) — a space is automatically inserted after it via the same word-gap pause -- All output is **uppercase** — Morse code doesn't distinguish case +- All output is **lowercase** - The maximum message length is 133 characters - If you enter a wrong dot/dash pattern, it won't match any character and will be silently dropped — just start the letter again after the gap - A dot is a quick tap (under half a second), a dash is a deliberate hold (half a second or longer) From 2d308f79522174530f1802032937b75ac696957e Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:14:20 +1000 Subject: [PATCH 7/8] implemented powersaving fixes per PR 2286 and updated firmware build date --- examples/companion_radio/MyMesh.cpp | 8 ++++++++ examples/companion_radio/MyMesh.h | 5 ++++- examples/companion_radio/main.cpp | 6 ++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5ffe205bd6..ca8b8418fd 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2145,6 +2145,14 @@ bool MyMesh::advert() { } } +// To check if there is pending work (for power saving) +bool MyMesh::hasPendingWork() const { +#if defined(WITH_BRIDGE) + if (bridge.isRunning()) return true; +#endif + return _mgr->getOutboundTotal() > 0; +} + #ifdef MORSE_COMPOSE_ENABLED void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* text) { int i = 0; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 6f8a69ed6b..c47d9f50b8 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,7 +8,7 @@ #define FIRMWARE_VER_CODE 10 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "20 Mar 2026" +#define FIRMWARE_BUILD_DATE "17 Apr 2026" #endif #ifndef FIRMWARE_VERSION @@ -171,6 +171,9 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { void queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* text); #endif + // To check if there is pending work (for power saving) + bool hasPendingWork() const; + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d1424fdbb1..5c1c84991d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -237,6 +237,12 @@ void loop() { #endif rtc_clock.tick(); + if (!the_mesh.hasPendingWork()) { +#if defined(NRF52_PLATFORM) + board.sleep(0); // nRF52 ignores seconds param, sleeps until next interrupt +#endif + } + // [DEBUG] Uncomment for periodic heap monitoring: // static unsigned long next_heap_print = 30000; // if (millis() > next_heap_print) { From 8a0733fd6e71e4adb011a965b7917734fe731095 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:19:22 +1000 Subject: [PATCH 8/8] added hardcoded premable length fix for narrow preset usage to meshpocket; added back meshpocket repeater env --- variants/mesh_pocket/platformio.ini | 18 +++++++++++++++++- variants/mesh_pocket/target.cpp | 9 ++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/variants/mesh_pocket/platformio.ini b/variants/mesh_pocket/platformio.ini index b721bccd04..ba31e2c4fb 100644 --- a/variants/mesh_pocket/platformio.ini +++ b/variants/mesh_pocket/platformio.ini @@ -12,6 +12,7 @@ build_flags = ${nrf52_base.build_flags} -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 + -D LORA_PREAMBLE_LEN=32 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D EINK_DISPLAY_MODEL=GxEPD2_213_B74 @@ -62,4 +63,19 @@ build_src_filter = ${Mesh_pocket.build_src_filter} +<../examples/companion_radio/ui-new/*.cpp> lib_deps = ${Mesh_pocket.lib_deps} - densaugeo/base64 @ ~1.4.0 \ No newline at end of file + densaugeo/base64 @ ~1.4.0 + +[env:Mesh_pocket_repeater] +extends = Mesh_pocket +build_src_filter = ${Mesh_pocket.build_src_filter} + +<../examples/simple_repeater> + +build_flags = + ${Mesh_pocket.build_flags} + -D ADVERT_NAME='"Heltec_Mesh_Pocket Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 \ No newline at end of file diff --git a/variants/mesh_pocket/target.cpp b/variants/mesh_pocket/target.cpp index 3ca7146341..1aad13538a 100644 --- a/variants/mesh_pocket/target.cpp +++ b/variants/mesh_pocket/target.cpp @@ -20,7 +20,11 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #endif bool radio_init() { - return radio.std_init(&SPI); + if (!radio.std_init(&SPI)) return false; +#ifdef LORA_PREAMBLE_LEN + radio.setPreambleLength(LORA_PREAMBLE_LEN); +#endif + return true; } uint32_t radio_get_rng_seed() { @@ -41,5 +45,4 @@ void radio_set_tx_power(int8_t dbm) { mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity -} - +} \ No newline at end of file