mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-08 14:24:51 +02:00
Dm message persistence; fixed home ui offset alignment; trace route screen addition
This commit is contained in:
@@ -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) {}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -0,0 +1,937 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <MeshCore.h>
|
||||
#include <Packet.h>
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -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}
|
||||
+<helpers/esp32/*.cpp>
|
||||
-<helpers/esp32/SerialBLEInterface.cpp>
|
||||
@@ -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}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -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}
|
||||
+<helpers/esp32/*.cpp>
|
||||
-<helpers/esp32/SerialBLEInterface.cpp>
|
||||
@@ -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}
|
||||
+<helpers/esp32/*.cpp>
|
||||
-<helpers/esp32/SerialBLEInterface.cpp>
|
||||
|
||||
Reference in New Issue
Block a user