diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 60a5a75fec..ca8b8418fd 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2144,3 +2144,40 @@ bool MyMesh::advert() { return false; } } + +// 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; + 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++] = 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); + 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..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 @@ -165,6 +165,15 @@ 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 + + // 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"); @@ -248,4 +257,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/main.cpp b/examples/companion_radio/main.cpp index 876dc9c33c..5c1c84991d 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,18 @@ void loop() { ui_task.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) { + // 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 94a8ee3efa..4d3c7964e1 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(); } @@ -578,7 +594,13 @@ 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); + morse_channel_picker = new MorseChannelPicker(); +#endif setCurrScreen(splash); } @@ -627,8 +649,22 @@ 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 + // inbox instead. Switching mid-hold would break the exit gesture. + if (curr != morse_screen) +#endif setCurrScreen(msg_preview); +#endif + +#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()) { @@ -728,6 +764,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 +780,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 +825,49 @@ 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()) { + ms->acknowledgeExit(); + gotoHomeScreen(); + } + const char* sendText = nullptr; + if (ms->consumeSendRequest(&sendText) && sendText) { + uint8_t ch_idx = ms->getChannelIdx(); + ChannelDetails 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)); + char fullMsg[160]; + snprintf(fullMsg, sizeof(fullMsg), "%s: %s", + the_mesh.getNodeName(), sendText); + the_mesh.queueSentChannelMessage(ch_idx, ts, fullMsg); + showAlert("Sent!", 800); + } + ms->clearOutBuf(); + } + } +#endif + if (_display != NULL && _display->isOn()) { if (millis() >= _next_refresh && curr) { _display->startFrame(); @@ -859,6 +947,25 @@ char UITask::handleLongPress(char c) { char UITask::handleDoubleClick(char c) { MESH_DEBUG_PRINTLN("UITask: double click triggered"); checkDisplayOn(c); +#ifdef MORSE_COMPOSE_ENABLED + if (curr == home) { + // 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; + } +#endif return c; } @@ -920,4 +1027,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..2deaace46f 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -50,7 +50,13 @@ class UITask : public AbstractUITask { UIScreen* splash; UIScreen* home; +#ifndef HELTEC_MESH_POCKET UIScreen* msg_preview; +#endif +#ifdef MORSE_COMPOSE_ENABLED + UIScreen* morse_screen; + UIScreen* morse_channel_picker; +#endif UIScreen* curr; void userLedHandler(); @@ -98,4 +104,4 @@ class UITask : public AbstractUITask { void loop() override; void shutdown(bool restart = false); -}; +}; \ No newline at end of file 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 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..3e4904728a --- /dev/null +++ b/variants/mesh_pocket/MorseScreen.h @@ -0,0 +1,620 @@ +#pragma once + +// ============================================================================= +// MorseScreen — single-button Morse compose/receive for the Meshpocket +// +// 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 (<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 +// 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 — 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. +// ----------------------------------------------------------------------------- + +#define MORSE_DOT_UNIT_MS 120 + +// 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 + +// 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 + +// 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 + +// 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 +#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} +}; + +// Hold action states +enum HoldAction : uint8_t { + HOLD_NONE = 0, + HOLD_BACKSPACE, + HOLD_SEND, + HOLD_EXIT +}; + +// ----------------------------------------------------------------------------- +class MorseScreen : public UIScreen { + mesh::RTCClock* _rtc; + + // 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]; + uint8_t _stagingLen; + + // Key timing state + bool _btnPrevPressed; + unsigned long _pressStart; + unsigned long _releaseAt; + bool _letterDecoded; + bool _wordSpaceInserted; + HoldAction _holdAction; + + // 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' = 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 '\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') { + // 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); + 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; + _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; + } + + 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), _channelIdx(0), _stagingLen(0), + _btnPrevPressed(false), _pressStart(0), _releaseAt(0), + _letterDecoded(false), _wordSpaceInserted(false), + _holdAction(HOLD_NONE), + _wantsExit(false), _wantsSend(false), + _inboxNewest(0), _inboxCount(0), + _dirty(true), _nextRender(0) + { + _outBuf[0] = 0; + _staging[0] = 0; + strcpy(_channelName, "Public"); + memset(_inbox, 0, sizeof(_inbox)); + } + + // 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(); + _pressStart = 0; + _releaseAt = 0; + _letterDecoded = false; + _wordSpaceInserted = false; + _holdAction = HOLD_NONE; + _wantsExit = false; + _wantsSend = false; + _dirty = true; + } + + uint8_t getChannelIdx() const { return _channelIdx; } + const char* getChannelName() const { return _channelName; } + + // Called from UITask::newMsg for incoming messages. + // `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; + 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 (WW 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; + _holdAction = HOLD_NONE; + _letterDecoded = false; + _wordSpaceInserted = false; + // 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); + _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; + } + } + _holdAction = HOLD_NONE; + + } else if (pressed && _btnPrevPressed) { + // ---- 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 — 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; + } 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(); + } + } + + _btnPrevPressed = pressed; + } + + int render(DisplayDriver& display) override { + const int W = display.width(); + + display.setTextSize(1); + + // ---- Header -------------------------------------------------------------- + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, 0); + char hdr[40]; + snprintf(hdr, sizeof(hdr), "MORSE > %s", _channelName); + display.print(hdr); + + // 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); + + // ---- 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 + 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.setCursor(26, 68); + 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); + char ccBuf[12]; + snprintf(ccBuf, sizeof(ccBuf), "%u/%u", (unsigned)_outLen, + (unsigned)(MORSE_OUT_BUF_LEN - 1)); + display.drawTextRightAlign(W - 1, 68, ccBuf); + + // Hint: Hold 3s=bksp 7s=send 9s=exit | WW=send HH=bksp + + _dirty = false; + _nextRender = millis(); + + // 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; + } +}; + +// ============================================================================= +// 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/Morse_Compose_Guide.md b/variants/mesh_pocket/Morse_Compose_Guide.md new file mode 100644 index 0000000000..6079ec46cc --- /dev/null +++ b/variants/mesh_pocket/Morse_Compose_Guide.md @@ -0,0 +1,129 @@ +# Morse Compose — Meshpocket User Guide + +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. 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. You can also exit by long-pressing from the channel picker. + +## How Pressing Works + +Every button press is either a **dot** or a **dash**, determined by how long you hold: + +| Press duration | Result | +|---|---| +| Under 500 ms | Dot (·) | +| 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. + +## 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 | +|---|---| +| 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 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" +10. Hold for ~8 seconds → display shows `[SEND]` → release → message sent! + +## Sending, Correcting, and Exiting + +There are two ways to send, backspace, and exit: + +### 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 | +|---|---|---| +| 3 – 7 s | `[BKSP]` | **Backspace** — deletes the last character | +| 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) + +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 + +### 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 > 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. + +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 **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) +- 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 diff --git a/variants/mesh_pocket/platformio.ini b/variants/mesh_pocket/platformio.ini index 015c2ca4be..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 @@ -20,6 +21,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 +39,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 +47,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 AUTO_OFF_MILLIS=0 ; -D BLE_DEBUG_LOGGING=1 + -D MORSE_COMPOSE_ENABLED ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -89,24 +65,17 @@ lib_deps = ${Mesh_pocket.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Mesh_pocket_companion_radio_usb] +[env:Mesh_pocket_repeater] extends = Mesh_pocket -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 +build_src_filter = ${Mesh_pocket.build_src_filter} + +<../examples/simple_repeater> + 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 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 -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 +; -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