From 6de664ea374be8fe38f32cf93be3abea14e4f0a5 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 7 May 2026 17:23:04 +1000 Subject: [PATCH] Dm message persistence; fixed home ui offset alignment; trace route screen addition --- examples/companion_radio/AbstractUITask.h | 4 + examples/companion_radio/MyMesh.cpp | 11 + examples/companion_radio/MyMesh.h | 4 +- examples/companion_radio/main.cpp | 62 +- .../companion_radio/ui-new/ChannelScreen.h | 9 +- examples/companion_radio/ui-new/Tracescreen.h | 937 ++++++++++++++++++ examples/companion_radio/ui-new/UITask.cpp | 33 +- examples/companion_radio/ui-new/UITask.h | 8 + variants/lilygo_tdeck_pro/platformio.ini | 13 +- 9 files changed, 1066 insertions(+), 15 deletions(-) create mode 100644 examples/companion_radio/ui-new/Tracescreen.h diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 469b85d6..600e3279 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -57,4 +57,8 @@ public: virtual void onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) {} virtual void onAdminCliResponse(const char* from_name, const char* text) {} virtual void onAdminTelemetryResult(const uint8_t* data, uint8_t len) {} + + // Trace path callback (from MyMesh::onTraceRecv) + virtual void onTraceResult(uint32_t tag, uint8_t flags, const uint8_t* path_snrs, + const uint8_t* path_hashes, uint8_t path_len, int8_t final_snr) {} }; \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 31bbb371..df5a0d77 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1290,6 +1290,14 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, } else { MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); } + + // Route trace result to standalone UI (TraceScreen) +#ifdef DISPLAY_CLASS + if (_ui) { + _ui->onTraceResult(tag, flags, path_snrs, path_hashes, path_len, + (int8_t)(packet->getSNR() * 4)); + } +#endif } uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { @@ -2173,6 +2181,9 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(&auth, &cmd_frame[5], 4); auto pkt = createTrace(tag, auth, flags); if (pkt) { + Serial.printf("[BLE Trace] flags=%d, path_len=%d, path hex:", flags, path_len); + for (int pi = 0; pi < path_len; pi++) Serial.printf(" %02X", cmd_frame[10 + pi]); + Serial.println(); sendDirect(pkt, &cmd_frame[10], path_len); uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 95f48618..789ea26c 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,11 +8,11 @@ #define FIRMWARE_VER_CODE 11 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "3 May 2026" +#define FIRMWARE_BUILD_DATE "7 May 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "Meck v1.8" +#define FIRMWARE_VERSION "Meck v1.9" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 31a1f28a..20e962fe 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -28,6 +28,7 @@ #include "DiscoveryScreen.h" #include "LastHeardScreen.h" #include "PathEditorScreen.h" + #include "Tracescreen.h" #ifdef MECK_WEB_READER #include "WebReaderScreen.h" #endif @@ -3204,6 +3205,13 @@ void loop() { ui_task.gotoContactsScreen(); } } + // Trace screen: check if Exit was triggered + if (ui_task.isOnTraceScreen()) { + TraceScreen* ts = (TraceScreen*)ui_task.getTraceScreen(); + if (ts && ts->wantsExit()) { + ui_task.gotoHomeScreen(); + } + } // Channel picker: check if long-press Enter was handled (wantsExit) if (ui_task.isOnChannelPickerScreen()) { ChannelPickerScreen* pick = (ChannelPickerScreen*)ui_task.getChannelPickerScreen(); @@ -3230,6 +3238,13 @@ void loop() { ui_task.gotoContactsScreen(); } } + // Trace screen: check if Exit was triggered + if (ui_task.isOnTraceScreen()) { + TraceScreen* ts = (TraceScreen*)ui_task.getTraceScreen(); + if (ts && ts->wantsExit()) { + ui_task.gotoHomeScreen(); + } + } // Channel picker: check if Enter/Q was handled (wantsExit) if (ui_task.isOnChannelPickerScreen()) { ChannelPickerScreen* pick = (ChannelPickerScreen*)ui_task.getChannelPickerScreen(); @@ -3372,6 +3387,7 @@ void loop() { break; case 'f': ui_task.gotoDiscoveryScreen(); break; case 'h': ui_task.gotoLastHeardScreen(); break; + case 'r': ui_task.gotoTraceScreen(); break; case (char)0xF3: ui_task.injectKey(KEY_LEFT); break; // Left arrow → prev page case (char)0xF4: ui_task.injectKey(KEY_RIGHT); break; // Right arrow → next page #ifdef MECK_WEB_READER @@ -3570,6 +3586,13 @@ void loop() { if (pe && pe->wantsExit()) { ui_task.gotoContactsScreen(); } + } else if (ui_task.isOnTraceScreen()) { + // Trace screen handles Enter internally + ui_task.injectKey('\r'); + TraceScreen* ts = (TraceScreen*)ui_task.getTraceScreen(); + if (ts && ts->wantsExit()) { + ui_task.gotoHomeScreen(); + } } else if (ui_task.isOnChannelPickerScreen()) { // Channel picker: Enter selects channel ui_task.injectKey('\r'); @@ -4675,6 +4698,7 @@ void handleKeyboardInput() { if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin() || ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen() || ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen() + || ui_task.isOnTraceScreen() #ifdef MECK_WEB_READER || ui_task.isOnWebReader() #endif @@ -4692,6 +4716,7 @@ void handleKeyboardInput() { if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin() || ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen() || ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen() + || ui_task.isOnTraceScreen() #ifdef MECK_WEB_READER || ui_task.isOnWebReader() #endif @@ -4713,6 +4738,7 @@ void handleKeyboardInput() { if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin() || ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen() || ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen() + || ui_task.isOnTraceScreen() #ifdef MECK_WEB_READER || ui_task.isOnWebReader() #endif @@ -4730,6 +4756,7 @@ void handleKeyboardInput() { if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin() || ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen() || ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen() + || ui_task.isOnTraceScreen() #ifdef MECK_WEB_READER || ui_task.isOnWebReader() #endif @@ -4751,6 +4778,7 @@ void handleKeyboardInput() { ui_task.gotoChannelPickerScreen(); } else if (ui_task.isOnContactsScreen() || ui_task.isOnMapScreen() || ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen() + || ui_task.isOnTraceScreen() #ifdef MECK_AUDIO_VARIANT || ui_task.isOnAlarmScreen() #endif @@ -4768,6 +4796,7 @@ void handleKeyboardInput() { ui_task.gotoChannelPickerScreen(); } else if (ui_task.isOnContactsScreen() || ui_task.isOnMapScreen() || ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen() + || ui_task.isOnTraceScreen() #ifdef MECK_AUDIO_VARIANT || ui_task.isOnAlarmScreen() #endif @@ -4786,9 +4815,16 @@ void handleKeyboardInput() { // Check if Save & Exit was selected PathEditorScreen* pe = (PathEditorScreen*)ui_task.getPathEditorScreen(); if (pe && pe->wantsExit()) { - Serial.println("PathEditor: Save & Exit — returning to contacts"); + Serial.println("PathEditor: Save & Exit -- returning to contacts"); ui_task.gotoContactsScreen(); } + } else if (ui_task.isOnTraceScreen()) { + ui_task.injectKey('\r'); + TraceScreen* ts = (TraceScreen*)ui_task.getTraceScreen(); + if (ts && ts->wantsExit()) { + Serial.println("TraceScreen: Exit -- returning to home"); + ui_task.gotoHomeScreen(); + } } else if (ui_task.isOnChannelPickerScreen()) { ui_task.injectKey('\r'); // Picker handles Enter: selects channel + sets wantsExit ChannelPickerScreen* pick = (ChannelPickerScreen*)ui_task.getChannelPickerScreen(); @@ -4936,15 +4972,17 @@ void handleKeyboardInput() { break; case 'r': - // Reply select mode (channel screen) or import contacts (contacts screen) + // Reply select (channel), import contacts, trace screen passthrough, or open trace (home) if (ui_task.isOnChannelScreen()) { ui_task.injectKey('r'); + } else if (ui_task.isOnTraceScreen()) { + ui_task.injectKey('r'); // Pass to trace screen (for edit mode) } else if (ui_task.isOnContactsScreen()) { // Try JSON first, fall back to binary Serial.println("Contacts: Importing from SD..."); int added = importContactsJSON(); if (added == -1) { - // No JSON file — try legacy binary + // No JSON file -- try legacy binary added = importContactsFromSD(); } if (added > 0) { @@ -4959,6 +4997,9 @@ void handleKeyboardInput() { } else { ui_task.showAlert("Import failed (no file?)", 2000); } + } else if (ui_task.isOnHomeScreen()) { + Serial.println("Opening trace path"); + ui_task.gotoTraceScreen(); } break; @@ -5050,6 +5091,16 @@ void handleKeyboardInput() { ui_task.gotoContactsScreen(); break; } + // Trace screen: Q/wantsExit goes home + if (ui_task.isOnTraceScreen()) { + ui_task.injectKey('q'); + TraceScreen* ts = (TraceScreen*)ui_task.getTraceScreen(); + if (ts && ts->wantsExit()) { + Serial.println("Nav: Trace -> Home"); + ui_task.gotoHomeScreen(); + } + break; + } // Alarm screen: Q/backspace routing depends on sub-mode #ifdef MECK_AUDIO_VARIANT if (ui_task.isOnAlarmScreen()) { @@ -5123,6 +5174,11 @@ void handleKeyboardInput() { break; } #endif + // Pass unhandled keys to trace screen (digits, comma for path entry) + if (ui_task.isOnTraceScreen()) { + ui_task.injectKey(key); + break; + } Serial.printf("Unhandled key in normal mode: '%c' (0x%02X)\n", key, key); break; } diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index 6158f652..847c096a 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -29,7 +29,7 @@ // On-disk format for message persistence (SD card) // --------------------------------------------------------------------------- #define MSG_FILE_MAGIC 0x4D434853 // "MCHS" - MeshCore History Store -#define MSG_FILE_VERSION 3 // v3: MSG_PATH_MAX=20, reserved→snr field +#define MSG_FILE_VERSION 4 // v4: added dm_peer_hash for DM persistence #define MSG_FILE_PATH "/meshcore/messages.bin" struct __attribute__((packed)) MsgFileHeader { @@ -46,10 +46,11 @@ struct __attribute__((packed)) MsgFileRecord { uint8_t path_len; uint8_t channel_idx; uint8_t valid; - int8_t snr; // Receive SNR × 4 (was reserved; 0 = unknown) + int8_t snr; // Receive SNR x 4 (was reserved; 0 = unknown) + uint32_t dm_peer_hash; // DM peer name hash (v4+, for conversation filtering) uint8_t path[MSG_PATH_MAX]; // Repeater hop hashes (first byte of pub key) char text[CHANNEL_MSG_TEXT_LEN]; - // 188 bytes total + // 192 bytes total }; class UITask; // Forward declaration @@ -434,6 +435,7 @@ public: rec.channel_idx = _messages[i].channel_idx; rec.valid = _messages[i].valid ? 1 : 0; rec.snr = _messages[i].snr; + rec.dm_peer_hash = _messages[i].dm_peer_hash; memcpy(rec.path, _messages[i].path, MSG_PATH_MAX); memcpy(rec.text, _messages[i].text, CHANNEL_MSG_TEXT_LEN); f.write((uint8_t*)&rec, sizeof(rec)); @@ -501,6 +503,7 @@ public: _messages[i].channel_idx = rec.channel_idx; _messages[i].valid = (rec.valid != 0); _messages[i].snr = rec.snr; + _messages[i].dm_peer_hash = rec.dm_peer_hash; memcpy(_messages[i].path, rec.path, MSG_PATH_MAX); memcpy(_messages[i].text, rec.text, CHANNEL_MSG_TEXT_LEN); if (_messages[i].valid) loaded++; diff --git a/examples/companion_radio/ui-new/Tracescreen.h b/examples/companion_radio/ui-new/Tracescreen.h new file mode 100644 index 00000000..c2269e57 --- /dev/null +++ b/examples/companion_radio/ui-new/Tracescreen.h @@ -0,0 +1,937 @@ +#pragma once + +#include +#include +#include +#include + +// Forward declarations +class UITask; +class MyMesh; +extern MyMesh the_mesh; + +// --------------------------------------------------------------------------- +// TraceScreen +// --------------------------------------------------------------------------- +// Standalone trace path tool for the T-Deck Pro. The user builds a repeater +// chain from the contacts list or by typing comma-separated hash values, sends +// a PAYLOAD_TYPE_TRACE packet direct-routed through the chain, and views +// per-hop SNR results. +// +// Path size (1-byte or 2-byte hashes) follows the device's path_hash_mode +// setting but can be toggled on this screen. +// +// The trace packet is created via Mesh::createTrace() and sent via +// Mesh::sendDirect(). Each repeater in the chain checks if its pub_key +// prefix matches the next hash in the payload; if so, it appends its receive +// SNR*4 to the packet's path field and forwards. When the packet reaches +// the end of its given path, onTraceRecv() fires on the receiving node. +// +// For round-trip traces the user should build a symmetric path +// (e.g. A,B,C,B,A) and must be able to hear the last repeater directly. +// --------------------------------------------------------------------------- + +#define TRACE_MAX_HOPS 16 +#define TRACE_TIMEOUT_MS 30000 // 30 second timeout +#define TRACE_EDIT_BUF 80 // Max chars for typed path + +class TraceScreen : public UIScreen { +public: + enum ScreenState { + STATE_BUILD, // Building the path + STATE_PICK_HOP, // Picking a repeater from contacts + STATE_RUNNING, // Trace sent, waiting for response + STATE_RESULTS // Showing results + }; + + // Trace result data (filled by onTraceResult callback) + struct TraceResult { + uint8_t hashes[TRACE_MAX_HOPS * 2]; // Hash bytes (1 or 2 per hop) + int8_t snrs[TRACE_MAX_HOPS]; // SNR*4 per hop + int8_t final_snr; // SNR of the response arriving back + int hopCount; // Number of hops that responded + int totalHops; // Total hops in the path + uint32_t duration_ms; // Round-trip time + bool valid; + }; + +private: + UITask* _task; + mesh::RTCClock* _rtc; + + ScreenState _state; + + // Path being built + uint8_t _pathBuf[TRACE_MAX_HOPS * 2]; // Hash bytes (max 2 bytes per hop) + int _hopCount; + int _bytesPerHop; // 1 or 2 + + // Menu navigation (STATE_BUILD) + int _menuSel; + + // Inline text editor (for Type Path) + bool _editing; + char _editBuf[TRACE_EDIT_BUF]; + int _editPos; + + // Repeater picker (STATE_PICK_HOP) + static const int MAX_REPEATERS = 200; + uint16_t* _repIdx; // Indices into contact table (PSRAM) + int _repCount; + int _repSel; + int _repScroll; + + // Trace state (STATE_RUNNING / STATE_RESULTS) + uint32_t _traceTag; + uint32_t _traceAuth; + unsigned long _traceSentAt; + TraceResult _result; + + // Results scroll + int _resultScroll; + + bool _wantExit; + + // --- Menu helpers (STATE_BUILD) --- + // Menu layout: + // 0: Mode selector (1-byte / 2-byte) + // 1: Type Path (inline text editor) + // 2..hopCount+1: each hop + // next: + Add repeater (if < TRACE_MAX_HOPS) + // next: Remove last (if hopCount > 0) + // next: Run Trace (if hopCount > 0) + // last: Exit + enum MenuItem { + MENU_PATH_SIZE = 0, + MENU_TYPE_PATH = 1, + MENU_HOP_BASE = 2, + MENU_ADD_HOP = 200, + MENU_REMOVE_LAST, + MENU_RUN_TRACE, + MENU_EXIT + }; + + int buildMenuCount() const { + int count = 2; // Mode + Type Path + count += _hopCount; + if (_hopCount < TRACE_MAX_HOPS) count++; // Add hop + if (_hopCount > 0) count++; // Remove last + if (_hopCount > 0) count++; // Run Trace + count++; // Exit + return count; + } + + MenuItem menuItemAt(int idx) const { + if (idx == 0) return MENU_PATH_SIZE; + if (idx == 1) return MENU_TYPE_PATH; + int pos = 2; + for (int h = 0; h < _hopCount; h++) { + if (idx == pos) return (MenuItem)(MENU_HOP_BASE + h); + pos++; + } + if (_hopCount < TRACE_MAX_HOPS) { + if (idx == pos) return MENU_ADD_HOP; + pos++; + } + if (_hopCount > 0) { + if (idx == pos) return MENU_REMOVE_LAST; + pos++; + } + if (_hopCount > 0) { + if (idx == pos) return MENU_RUN_TRACE; + pos++; + } + return MENU_EXIT; + } + + // Build repeater list from contacts + void buildRepeaterList() { + _repCount = 0; + uint32_t numContacts = the_mesh.getNumContacts(); + ContactInfo c; + for (uint32_t i = 0; i < numContacts && _repCount < MAX_REPEATERS; i++) { + if (the_mesh.getContactByIdx(i, c)) { + if (c.type == ADV_TYPE_REPEATER) { + _repIdx[_repCount++] = (uint16_t)i; + } + } + } + } + + // Look up contact name from hash prefix + bool findNameForHash(const uint8_t* hash, int hashLen, char* name, size_t nameLen) const { + uint32_t numContacts = the_mesh.getNumContacts(); + ContactInfo c; + // First pass: repeaters only + for (uint32_t i = 0; i < numContacts; i++) { + if (the_mesh.getContactByIdx(i, c) && c.type == ADV_TYPE_REPEATER) { + if (memcmp(c.id.pub_key, hash, hashLen) == 0) { + strncpy(name, c.name, nameLen); + name[nameLen - 1] = '\0'; + return true; + } + } + } + // Second pass: any contact + for (uint32_t i = 0; i < numContacts; i++) { + if (the_mesh.getContactByIdx(i, c)) { + if (memcmp(c.id.pub_key, hash, hashLen) == 0) { + strncpy(name, c.name, nameLen); + name[nameLen - 1] = '\0'; + return true; + } + } + } + return false; + } + + // Parse comma-separated decimal values from edit buffer into path + // Returns number of hops parsed, or -1 on error + int parseTypedPath() { + if (_editBuf[0] == '\0') return 0; + + uint8_t tmpPath[TRACE_MAX_HOPS * 2]; + int hops = 0; + const char* p = _editBuf; + + while (*p && hops < TRACE_MAX_HOPS) { + // Skip whitespace/commas + while (*p == ',' || *p == ' ') p++; + if (*p == '\0') break; + + // Parse hex number (companion app uses hex hash values) + char* end; + long val = strtol(p, &end, 16); + if (end == p) return -1; // No digits found + p = end; + + if (_bytesPerHop == 1) { + if (val < 0 || val > 255) return -1; + tmpPath[hops] = (uint8_t)val; + } else { + if (val < 0 || val > 65535) return -1; + // Big-endian storage: hash display = (pub_key[0] << 8) | pub_key[1] + // So val >> 8 is pub_key[0], val & 0xFF is pub_key[1] + tmpPath[hops * 2] = (uint8_t)((val >> 8) & 0xFF); + tmpPath[hops * 2 + 1] = (uint8_t)(val & 0xFF); + } + hops++; + } + + if (hops > 0) { + memcpy(_pathBuf, tmpPath, hops * _bytesPerHop); + _hopCount = hops; + } + return hops; + } + + // Build display string from current path (for showing in edit field) + void pathToEditBuf() { + _editBuf[0] = '\0'; + _editPos = 0; + for (int i = 0; i < _hopCount; i++) { + char tmp[8]; + if (_bytesPerHop == 1) { + snprintf(tmp, sizeof(tmp), "%02X", _pathBuf[i]); + } else { + uint16_t val = ((uint16_t)_pathBuf[i * 2] << 8) | _pathBuf[i * 2 + 1]; + snprintf(tmp, sizeof(tmp), "%04X", val); + } + if (i > 0) { + if (_editPos < TRACE_EDIT_BUF - 1) _editBuf[_editPos++] = ','; + } + int tlen = strlen(tmp); + if (_editPos + tlen < TRACE_EDIT_BUF - 1) { + memcpy(&_editBuf[_editPos], tmp, tlen); + _editPos += tlen; + } + } + _editBuf[_editPos] = '\0'; + } + + // Truncate long names to maxLen chars + "..." for display + static void truncateName(char* name, int maxLen = 10) { + if ((int)strlen(name) > maxLen) { + name[maxLen] = '\0'; + // Remove trailing space before ellipsis + while (maxLen > 0 && name[maxLen - 1] == ' ') { + name[--maxLen] = '\0'; + } + strcat(name, "..."); + } + } + + // Draw signal bars (3 bars) based on SNR + void drawSignalBars(DisplayDriver& display, int x, int y, int8_t snr4) { + float snr = snr4 / 4.0f; + // 3 bars: low >= -5, mid >= 3, high >= 8 + int bars = 0; + if (snr >= -5.0f) bars = 1; + if (snr >= 3.0f) bars = 2; + if (snr >= 8.0f) bars = 3; + + int barW = 3; + int gap = 1; + int heights[] = { 4, 7, 10 }; + for (int b = 0; b < 3; b++) { + int bx = x + b * (barW + gap); + int by = y + 10 - heights[b]; + if (b < bars) { + display.setColor(DisplayDriver::GREEN); + } else { + display.setColor(DisplayDriver::DARK); + } + display.fillRect(bx, by, barW, heights[b]); + } + } + +public: + TraceScreen(UITask* task, mesh::RTCClock* rtc) + : _task(task), _rtc(rtc), _state(STATE_BUILD), + _hopCount(0), _bytesPerHop(2), + _menuSel(0), _editing(false), _editPos(0), + _repCount(0), _repSel(0), _repScroll(0), + _traceTag(0), _traceAuth(0), _traceSentAt(0), + _resultScroll(0), _wantExit(false) { + memset(_pathBuf, 0, sizeof(_pathBuf)); + memset(_editBuf, 0, sizeof(_editBuf)); + memset(&_result, 0, sizeof(_result)); + #if defined(ESP32) && defined(BOARD_HAS_PSRAM) + _repIdx = (uint16_t*)ps_calloc(MAX_REPEATERS, sizeof(uint16_t)); + #else + _repIdx = new uint16_t[MAX_REPEATERS](); + #endif + } + + bool wantsExit() const { return _wantExit; } + bool isEditing() const { return _editing; } + + void enter(int pathHashMode) { + _state = STATE_BUILD; + _hopCount = 0; + _menuSel = 0; + _editing = false; + _editPos = 0; + memset(_editBuf, 0, sizeof(_editBuf)); + _repSel = 0; + _repScroll = 0; + _wantExit = false; + _resultScroll = 0; + memset(_pathBuf, 0, sizeof(_pathBuf)); + memset(&_result, 0, sizeof(_result)); + + // Default to device path hash mode (clamped to 1 or 2 for trace) + _bytesPerHop = (pathHashMode >= 1) ? 2 : 1; + } + + // Called by MyMesh::onTraceRecv() via UITask + void onTraceResult(uint32_t tag, uint8_t flags, + const uint8_t* path_snrs, const uint8_t* path_hashes, + uint8_t path_byte_len, int8_t final_snr) { + if (_state != STATE_RUNNING) return; + if (tag != _traceTag) return; // Not our trace + + uint8_t pathSz = flags & 0x03; + int numHops = (pathSz > 0) ? (path_byte_len >> pathSz) : path_byte_len; + + _result.valid = true; + _result.totalHops = numHops; + _result.final_snr = final_snr; + _result.duration_ms = millis() - _traceSentAt; + + // Copy hash data + int copyBytes = path_byte_len; + if (copyBytes > (int)sizeof(_result.hashes)) copyBytes = sizeof(_result.hashes); + memcpy(_result.hashes, path_hashes, copyBytes); + + // Count SNR entries (= number of hops that actually forwarded) + int snrCount = numHops; + if (snrCount > TRACE_MAX_HOPS) snrCount = TRACE_MAX_HOPS; + _result.hopCount = snrCount; + for (int i = 0; i < snrCount; i++) { + _result.snrs[i] = (int8_t)path_snrs[i]; + } + + _state = STATE_RESULTS; + _resultScroll = 0; + Serial.printf("[Trace] Result received: %d hops, %dms\n", numHops, _result.duration_ms); + } + + // --- Render --- + int render(DisplayDriver& display) override { + // Header + display.setCursor(0, 0); + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.print("Trace Path"); + display.drawRect(0, 11, display.width(), 1); + + if (_state == STATE_BUILD) { + return renderBuild(display); + } else if (_state == STATE_PICK_HOP) { + return renderPicker(display); + } else if (_state == STATE_RUNNING) { + return renderRunning(display); + } else { + return renderResults(display); + } + } + +private: + int renderBuild(DisplayDriver& display) { + char tmp[TRACE_EDIT_BUF + 16]; + int y = 14; + int lineH = 11; + int menuCount = buildMenuCount(); + int maxVisible = (display.height() - y - 14) / lineH; + if (maxVisible < 1) maxVisible = 1; + + // Scroll window + int scrollTop = 0; + if (_menuSel >= scrollTop + maxVisible) scrollTop = _menuSel - maxVisible + 1; + if (_menuSel < scrollTop) scrollTop = _menuSel; + + for (int vi = 0; vi < maxVisible && (scrollTop + vi) < menuCount; vi++) { + int idx = scrollTop + vi; + MenuItem item = menuItemAt(idx); + char prefix = (idx == _menuSel) ? '>' : ' '; + + display.setCursor(0, y); + display.setColor(DisplayDriver::LIGHT); + + switch (item) { + case MENU_PATH_SIZE: + snprintf(tmp, sizeof(tmp), "%c Mode: %d-byte", prefix, _bytesPerHop); + display.print(tmp); + if (idx == _menuSel) { + const char* hint = "(A/D)"; + display.setCursor(display.width() - display.getTextWidth(hint) - 4, y); + display.print(hint); + } + break; + + case MENU_TYPE_PATH: + if (_editing) { + // Active text editor with cursor + display.setColor(DisplayDriver::GREEN); + snprintf(tmp, sizeof(tmp), " Path: %s_", _editBuf); + display.print(tmp); + } else if (_hopCount > 0) { + // Show current path as decimal values + char pathStr[TRACE_EDIT_BUF]; + pathStr[0] = '\0'; + int pos = 0; + for (int i = 0; i < _hopCount && pos < (int)sizeof(pathStr) - 8; i++) { + if (i > 0) pathStr[pos++] = ','; + if (_bytesPerHop == 1) { + pos += snprintf(&pathStr[pos], sizeof(pathStr) - pos, "%02X", _pathBuf[i]); + } else { + uint16_t val = ((uint16_t)_pathBuf[i * 2] << 8) | _pathBuf[i * 2 + 1]; + pos += snprintf(&pathStr[pos], sizeof(pathStr) - pos, "%04X", val); + } + } + snprintf(tmp, sizeof(tmp), "%c Path: %s", prefix, pathStr); + display.print(tmp); + } else { + snprintf(tmp, sizeof(tmp), "%c Type Path: [Press Enter]", prefix); + display.print(tmp); + } + break; + + case MENU_ADD_HOP: + display.setColor(DisplayDriver::GREEN); + snprintf(tmp, sizeof(tmp), "%c + Add repeater...", prefix); + display.print(tmp); + break; + + case MENU_REMOVE_LAST: + snprintf(tmp, sizeof(tmp), "%c - Remove last", prefix); + display.print(tmp); + break; + + case MENU_RUN_TRACE: + display.setColor(DisplayDriver::YELLOW); + snprintf(tmp, sizeof(tmp), "%c Run Trace", prefix); + display.print(tmp); + break; + + case MENU_EXIT: + snprintf(tmp, sizeof(tmp), "%c Exit", prefix); + display.print(tmp); + break; + + default: + // Hop line + if (item >= MENU_HOP_BASE && item < MENU_HOP_BASE + TRACE_MAX_HOPS) { + int hopIdx = item - MENU_HOP_BASE; + int offset = hopIdx * _bytesPerHop; + char hopName[24]; + uint16_t hashVal; + if (_bytesPerHop == 1) { + hashVal = _pathBuf[offset]; + } else { + hashVal = ((uint16_t)_pathBuf[offset] << 8) | _pathBuf[offset + 1]; + } + if (findNameForHash(&_pathBuf[offset], _bytesPerHop, hopName, sizeof(hopName))) { + truncateName(hopName); + display.setColor(DisplayDriver::GREEN); + snprintf(tmp, sizeof(tmp), "%c%d: %s (%X)", prefix, hopIdx + 1, + hopName, hashVal); + } else { + snprintf(tmp, sizeof(tmp), "%c%d: (%X)", prefix, hopIdx + 1, hashVal); + } + display.print(tmp); + } + break; + } + y += lineH; + } + + // Footer + int footerY = display.height() - 12; + display.setTextSize(1); + display.drawRect(0, footerY - 2, display.width(), 1); + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, footerY); + if (_editing) { + display.print("Q:Cancel Enter:Apply"); + } else { + display.print("Q:Exit W/S:Nav Ent:Sel"); + } + + return 5000; + } + + int renderPicker(DisplayDriver& display) { + char tmp[48]; + int y = 14; + int lineH = 11; + int maxVisible = (display.height() - y - 14) / lineH; + if (maxVisible < 1) maxVisible = 1; + + if (_repCount == 0) { + display.setCursor(0, y); + display.setColor(DisplayDriver::RED); + display.print("No repeaters in contacts"); + y += lineH; + display.setColor(DisplayDriver::LIGHT); + display.print("Press Q to go back"); + } else { + // Clamp scroll + if (_repSel >= _repCount) _repSel = _repCount - 1; + if (_repSel < 0) _repSel = 0; + if (_repSel < _repScroll) _repScroll = _repSel; + if (_repSel >= _repScroll + maxVisible) _repScroll = _repSel - maxVisible + 1; + + for (int vi = 0; vi < maxVisible && (_repScroll + vi) < _repCount; vi++) { + int idx = _repScroll + vi; + uint16_t contactIdx = _repIdx[idx]; + ContactInfo c; + if (!the_mesh.getContactByIdx(contactIdx, c)) continue; + + char prefix = (idx == _repSel) ? '>' : ' '; + display.setCursor(0, y); + + // Show name + decimal hash value + char filteredName[24]; + display.translateUTF8ToBlocks(filteredName, c.name, sizeof(filteredName)); + truncateName(filteredName, 14); // Picker has more room + uint16_t hashVal; + if (_bytesPerHop == 1) { + hashVal = c.id.pub_key[0]; + } else { + hashVal = ((uint16_t)c.id.pub_key[0] << 8) | c.id.pub_key[1]; + } + snprintf(tmp, sizeof(tmp), "%c %s (%X)", prefix, filteredName, hashVal); + display.setColor((idx == _repSel) ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + display.print(tmp); + y += lineH; + } + } + + // Footer + int footerY = display.height() - 12; + display.setTextSize(1); + display.drawRect(0, footerY - 2, display.width(), 1); + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, footerY); + display.print("Q:Back W/S:Scroll Ent:Add"); + + return 5000; + } + + int renderRunning(DisplayDriver& display) { + int y = 14; + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, y); + display.print("Tracing..."); + y += 14; + + // Show path summary + display.setColor(DisplayDriver::LIGHT); + char tmp[48]; + snprintf(tmp, sizeof(tmp), "%d hops, %d-byte mode", _hopCount, _bytesPerHop); + display.setCursor(0, y); + display.print(tmp); + y += 14; + + // Elapsed time + unsigned long elapsed = millis() - _traceSentAt; + snprintf(tmp, sizeof(tmp), "Elapsed: %lu ms", elapsed); + display.setCursor(0, y); + display.print(tmp); + y += 14; + + // Timeout bar + int barW = display.width() - 20; + int barH = 4; + int barX = 10; + display.setColor(DisplayDriver::DARK); + display.drawRect(barX, y, barW, barH); + int fill = (int)((unsigned long)barW * elapsed / TRACE_TIMEOUT_MS); + if (fill > barW) fill = barW; + display.setColor(DisplayDriver::GREEN); + display.fillRect(barX, y, fill, barH); + + // Check timeout + if (elapsed >= TRACE_TIMEOUT_MS) { + _state = STATE_RESULTS; + _result.valid = false; + _result.duration_ms = TRACE_TIMEOUT_MS; + Serial.println("[Trace] Timeout"); + } + + // Footer + int footerY = display.height() - 12; + display.setTextSize(1); + display.drawRect(0, footerY - 2, display.width(), 1); + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, footerY); + display.print("Q:Cancel"); + + return 500; // Fast refresh for elapsed timer + } + + int renderResults(DisplayDriver& display) { + char tmp[48]; + int y = 14; + int lineH = 12; + + if (!_result.valid) { + display.setColor(DisplayDriver::RED); + display.setCursor(0, y); + display.print("Trace timed out"); + y += lineH; + display.setColor(DisplayDriver::LIGHT); + snprintf(tmp, sizeof(tmp), "No response after %ds", TRACE_TIMEOUT_MS / 1000); + display.setCursor(0, y); + display.print(tmp); + } else { + // Duration header + display.setColor(DisplayDriver::GREEN); + snprintf(tmp, sizeof(tmp), "Complete: %dms", (int)_result.duration_ms); + display.setCursor(0, y); + display.print(tmp); + y += lineH + 2; + + int maxVisible = (display.height() - y - 14) / lineH; + if (maxVisible < 1) maxVisible = 1; + + // Clamp scroll + int totalItems = _result.hopCount + 1; // hops + final SNR line + if (_resultScroll > totalItems - maxVisible) _resultScroll = totalItems - maxVisible; + if (_resultScroll < 0) _resultScroll = 0; + + for (int vi = 0; vi < maxVisible && (_resultScroll + vi) < totalItems; vi++) { + int idx = _resultScroll + vi; + display.setCursor(0, y); + + if (idx < _result.hopCount) { + // Hop entry + int offset = idx * _bytesPerHop; + char hopName[20]; + bool resolved = findNameForHash(&_result.hashes[offset], _bytesPerHop, + hopName, sizeof(hopName)); + if (resolved) truncateName(hopName); + + float snr = _result.snrs[idx] / 4.0f; + + display.setColor(resolved ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + if (resolved) { + snprintf(tmp, sizeof(tmp), "%d: %s", idx + 1, hopName); + } else { + uint16_t hashVal; + if (_bytesPerHop == 1) { + hashVal = _result.hashes[offset]; + } else { + hashVal = ((uint16_t)_result.hashes[offset] << 8) | _result.hashes[offset + 1]; + } + snprintf(tmp, sizeof(tmp), "%d: (%X)", idx + 1, hashVal); + } + display.print(tmp); + + // SNR value on right + snprintf(tmp, sizeof(tmp), "%.1fdB", snr); + int snrW = display.getTextWidth(tmp); + int barsW = 14; + display.setCursor(display.width() - snrW - barsW - 4, y); + display.setColor(DisplayDriver::LIGHT); + display.print(tmp); + + // Signal bars + drawSignalBars(display, display.width() - barsW - 1, y, _result.snrs[idx]); + + } else { + // Final SNR (response arriving back at this node) + float snr = _result.final_snr / 4.0f; + display.setColor(DisplayDriver::YELLOW); + snprintf(tmp, sizeof(tmp), "Return SNR: %.1fdB", snr); + display.print(tmp); + drawSignalBars(display, display.width() - 15, y, _result.final_snr); + } + y += lineH; + } + } + + // Footer + int footerY = display.height() - 12; + display.setTextSize(1); + display.drawRect(0, footerY - 2, display.width(), 1); + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, footerY); + display.print("Q:Back Ent:New Trace"); + + return 5000; + } + +public: + // --- Input handling --- + bool handleInput(char c) override { + // Text editing mode consumes all keys + if (_editing) { + return handleEditInput(c); + } + + switch (_state) { + case STATE_BUILD: return handleBuildInput(c); + case STATE_PICK_HOP: return handlePickerInput(c); + case STATE_RUNNING: return handleRunningInput(c); + case STATE_RESULTS: return handleResultsInput(c); + } + return false; + } + +private: + // --- Text editor for typed path --- + bool handleEditInput(char c) { + // Enter: apply typed path + if (c == '\r' || c == 13) { + int parsed = parseTypedPath(); + if (parsed < 0) { + Serial.println("[Trace] Failed to parse typed path"); + // Stay in edit mode -- user can fix + } else { + Serial.printf("[Trace] Parsed %d hops from typed path\n", parsed); + _editing = false; + } + return true; + } + // Q or Escape: cancel edit + if (c == 'q' || c == 'Q' || c == 27) { + _editing = false; + return true; + } + // Backspace + if (c == '\b') { + if (_editPos > 0) { + _editPos--; + _editBuf[_editPos] = '\0'; + } + return true; + } + // Accept hex digits, commas, spaces + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') + || c == ',' || c == ' ') { + if (_editPos < TRACE_EDIT_BUF - 1) { + _editBuf[_editPos++] = c; + _editBuf[_editPos] = '\0'; + } + return true; + } + return true; // Consume all keys in edit mode + } + + bool handleBuildInput(char c) { + int menuCount = buildMenuCount(); + + // W - up + if (c == 'w' || c == 'W' || c == 0xF2) { + if (_menuSel > 0) _menuSel--; + return true; + } + // S - down + if (c == 's' || c == 'S' || c == 0xF1) { + if (_menuSel < menuCount - 1) _menuSel++; + return true; + } + // A/D - toggle mode on path size row + if ((c == 'a' || c == 'A' || c == 'd' || c == 'D') && menuItemAt(_menuSel) == MENU_PATH_SIZE) { + _bytesPerHop = (_bytesPerHop == 1) ? 2 : 1; + // Changing mode clears path (byte layout is different) + _hopCount = 0; + memset(_pathBuf, 0, sizeof(_pathBuf)); + return true; + } + // Q - exit + if (c == 'q' || c == 'Q' || c == '\b') { + _wantExit = true; + return true; + } + // Enter - select + if (c == '\r' || c == 13) { + MenuItem item = menuItemAt(_menuSel); + switch (item) { + case MENU_TYPE_PATH: + // Enter edit mode -- pre-fill with current path if any + pathToEditBuf(); + _editing = true; + return true; + + case MENU_ADD_HOP: + buildRepeaterList(); + _repSel = 0; + _repScroll = 0; + _state = STATE_PICK_HOP; + return true; + + case MENU_REMOVE_LAST: + if (_hopCount > 0) { + _hopCount--; + if (_menuSel >= buildMenuCount()) _menuSel = buildMenuCount() - 1; + } + return true; + + case MENU_RUN_TRACE: + return sendTrace(); + + case MENU_EXIT: + _wantExit = true; + return true; + + default: + break; + } + return true; + } + return false; + } + + bool handlePickerInput(char c) { + // W - up + if (c == 'w' || c == 'W' || c == 0xF2) { + if (_repSel > 0) _repSel--; + return true; + } + // S - down + if (c == 's' || c == 'S' || c == 0xF1) { + if (_repSel < _repCount - 1) _repSel++; + return true; + } + // Q - back to build + if (c == 'q' || c == 'Q' || c == '\b') { + _state = STATE_BUILD; + return true; + } + // Enter - add selected repeater + if (c == '\r' || c == 13) { + if (_repCount > 0 && _repSel >= 0 && _repSel < _repCount) { + ContactInfo contact; + if (the_mesh.getContactByIdx(_repIdx[_repSel], contact)) { + int offset = _hopCount * _bytesPerHop; + memcpy(&_pathBuf[offset], contact.id.pub_key, _bytesPerHop); + _hopCount++; + uint16_t hashVal = ((uint16_t)contact.id.pub_key[0] << 8) + | contact.id.pub_key[1]; + Serial.printf("[Trace] Added hop %d: %s (%X)\n", + _hopCount, contact.name, hashVal); + } + _state = STATE_BUILD; + _menuSel = _hopCount + 1; // Point to row after last hop + } + return true; + } + return false; + } + + bool handleRunningInput(char c) { + // Q - cancel + if (c == 'q' || c == 'Q' || c == '\b') { + _state = STATE_BUILD; + return true; + } + return true; // Consume all keys while running + } + + bool handleResultsInput(char c) { + // W - scroll up + if (c == 'w' || c == 'W' || c == 0xF2) { + if (_resultScroll > 0) _resultScroll--; + return true; + } + // S - scroll down + if (c == 's' || c == 'S' || c == 0xF1) { + _resultScroll++; + return true; + } + // Q - back to build screen (keep path) + if (c == 'q' || c == 'Q' || c == '\b') { + _state = STATE_BUILD; + _menuSel = 0; + return true; + } + // Enter - new trace (re-run with same path) + if (c == '\r' || c == 13) { + return sendTrace(); + } + return false; + } + + // --- Send trace --- + bool sendTrace() { + if (_hopCount <= 0) return true; + + // Generate random tag and auth code + the_mesh.getRNG()->random((uint8_t*)&_traceTag, 4); + the_mesh.getRNG()->random((uint8_t*)&_traceAuth, 4); + + // flags: lower 2 bits = path_sz + // path_sz 0 = 1-byte hashes, path_sz 1 = 2-byte hashes + uint8_t pathSz = (_bytesPerHop == 2) ? 1 : 0; + uint8_t flags = pathSz; + + mesh::Packet* pkt = the_mesh.createTrace(_traceTag, _traceAuth, flags); + if (!pkt) { + Serial.println("[Trace] Failed to create trace packet (pool empty)"); + return true; + } + + // Path bytes to send + uint8_t pathByteLen = _hopCount * _bytesPerHop; + + // sendDirect for TRACE appends path to payload and sets path_len=0 + the_mesh.sendDirect(pkt, _pathBuf, pathByteLen); + + _traceSentAt = millis(); + _state = STATE_RUNNING; + memset(&_result, 0, sizeof(_result)); + + Serial.printf("[Trace] Sent: tag=0x%08X, %d hops, %d-byte, %d path bytes\n", + _traceTag, _hopCount, _bytesPerHop, pathByteLen); + Serial.printf("[Trace] Path hex:"); + for (int i = 0; i < pathByteLen; i++) { + Serial.printf(" %02X", _pathBuf[i]); + } + Serial.println(); + return true; + } +}; \ 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 e1f1750a..2252a126 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -8,6 +8,7 @@ #include "PathEditorScreen.h" #include "DiscoveryScreen.h" #include "LastHeardScreen.h" +#include "Tracescreen.h" #ifdef MECK_WEB_READER #include "WebReaderScreen.h" #endif @@ -605,7 +606,12 @@ public: #else display.setCursor(col1, y); display.print("[F] Discover"); #endif - y += menuLH + 2; + y += menuLH; + display.setColor(DisplayDriver::YELLOW); + display.drawTextCentered(display.width() / 2, y, "[R] Trace"); + display.setColor(DisplayDriver::LIGHT); + y += menuLH; + y += 2; } else { // Monospaced built-in font (Classic): centered space-padded strings y += 6; @@ -638,6 +644,10 @@ public: #else display.drawTextCentered(display.width() / 2, y, "[F] Discover "); #endif + y += 10; + display.setColor(DisplayDriver::YELLOW); + display.drawTextCentered(display.width() / 2, y, "[R] Trace"); + display.setColor(DisplayDriver::LIGHT); y += 14; } @@ -1354,6 +1364,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no path_editor = nullptr; // Lazy-initialized on first use from contacts screen discovery_screen = new DiscoveryScreen(this, &rtc_clock); last_heard_screen = new LastHeardScreen(&rtc_clock); + trace_screen = new TraceScreen(this, &rtc_clock); #if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) lock_screen = new LockScreen(this, &rtc_clock, node_prefs); #endif @@ -2990,6 +3001,26 @@ void UITask::gotoLastHeardScreen() { _next_refresh = 100; } +void UITask::gotoTraceScreen() { + TraceScreen* ts = (TraceScreen*)trace_screen; + ts->enter(the_mesh.getNodePrefs()->path_hash_mode); + setCurrScreen(trace_screen); + if (_display != NULL && !_display->isOn()) { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 100; +} + +void UITask::onTraceResult(uint32_t tag, uint8_t flags, const uint8_t* path_snrs, + const uint8_t* path_hashes, uint8_t path_len, int8_t final_snr) { + TraceScreen* ts = (TraceScreen*)trace_screen; + if (ts) { + ts->onTraceResult(tag, flags, path_snrs, path_hashes, path_len, final_snr); + _next_refresh = 100; // Force refresh to show results + } +} + #ifdef MECK_WEB_READER void UITask::gotoWebReader() { // Lazy-initialize on first use (same pattern as audiobook player) diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index be30adf0..076af1db 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -99,6 +99,7 @@ class UITask : public AbstractUITask { UIScreen* path_editor; // Custom path editor screen (lazy-init) UIScreen* discovery_screen; // Node discovery scan screen UIScreen* last_heard_screen; // Last heard passive advert list + UIScreen* trace_screen; // Trace path screen (standalone trace tool) #ifdef MECK_WEB_READER UIScreen* web_reader; // Web reader screen (lazy-init, WiFi required) #endif @@ -200,6 +201,7 @@ public: void gotoPathEditor(int contactIdx); // Navigate to custom path editor void gotoDiscoveryScreen(); // Navigate to node discovery scan void gotoLastHeardScreen(); // Navigate to last heard passive list + void gotoTraceScreen(); // Navigate to trace path screen #if HAS_GPS void gotoMapScreen(); // Navigate to map tile screen #endif @@ -256,6 +258,7 @@ public: bool isOnPathEditor() const { return curr == path_editor; } bool isOnDiscoveryScreen() const { return curr == discovery_screen; } bool isOnLastHeardScreen() const { return curr == last_heard_screen; } + bool isOnTraceScreen() const { return curr == trace_screen; } bool isOnMapScreen() const { return curr == map_screen; } #if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) bool isLocked() const { return _locked; } @@ -312,6 +315,10 @@ public: void onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) override; void onAdminCliResponse(const char* from_name, const char* text) override; void onAdminTelemetryResult(const uint8_t* data, uint8_t len) override; + + // Trace path callback (from MyMesh::onTraceRecv) + void onTraceResult(uint32_t tag, uint8_t flags, const uint8_t* path_snrs, + const uint8_t* path_hashes, uint8_t path_len, int8_t final_snr) override; // Get current screen for checking state UIScreen* getCurrentScreen() const { return curr; } @@ -336,6 +343,7 @@ public: UIScreen* getPathEditorScreen() const { return path_editor; } UIScreen* getDiscoveryScreen() const { return discovery_screen; } UIScreen* getLastHeardScreen() const { return last_heard_screen; } + UIScreen* getTraceScreen() const { return trace_screen; } UIScreen* getMapScreen() const { return map_screen; } #ifdef MECK_WEB_READER UIScreen* getWebReaderScreen() const { return web_reader; } diff --git a/variants/lilygo_tdeck_pro/platformio.ini b/variants/lilygo_tdeck_pro/platformio.ini index 25fd1bb7..8ed31c8b 100644 --- a/variants/lilygo_tdeck_pro/platformio.ini +++ b/variants/lilygo_tdeck_pro/platformio.ini @@ -72,8 +72,8 @@ build_flags = -D EINK_ROTATION=0 -D EINK_SCALE_X=1.875f -D EINK_SCALE_Y=2.5f - -D EINK_X_OFFSET=0 - -D EINK_Y_OFFSET=5 + -D EINK_X_OFFSET=2 + -D EINK_Y_OFFSET=4 -D PIN_DISPLAY_CS=34 -D PIN_DISPLAY_DC=35 -D PIN_DISPLAY_RST=16 @@ -121,6 +121,7 @@ build_flags = -D MECK_AUDIO_VARIANT -D MECK_WEB_READER=1 -D MECK_OTA_UPDATE=1 +; -D BLE_DEBUG_LOGGING=1 build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + + @@ -158,7 +159,7 @@ build_flags = -D MECK_AUDIO_VARIANT -D MECK_WEB_READER=1 -D MECK_OTA_UPDATE=1 - -D FIRMWARE_VERSION='"Meck v1.8.WiFi"' + -D FIRMWARE_VERSION='"Meck v1.9.WiFi"' build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + - @@ -225,7 +226,7 @@ build_flags = -D HAS_4G_MODEM=1 -D MECK_WEB_READER=1 -D MECK_OTA_UPDATE=1 - -D FIRMWARE_VERSION='"Meck v1.8.4G"' + -D FIRMWARE_VERSION='"Meck v1.9.4G"' build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + + @@ -261,7 +262,7 @@ build_flags = -D HAS_4G_MODEM=1 -D MECK_WEB_READER=1 -D MECK_OTA_UPDATE=1 - -D FIRMWARE_VERSION='"Meck v1.8.4G.WiFi"' + -D FIRMWARE_VERSION='"Meck v1.9.4G.WiFi"' build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + - @@ -295,7 +296,7 @@ build_flags = -D HAS_4G_MODEM=1 -D MECK_WEB_READER=1 -D MECK_OTA_UPDATE=1 - -D FIRMWARE_VERSION='"Meck v1.8.4G.SA"' + -D FIRMWARE_VERSION='"Meck v1.9.4G.SA"' build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + -