mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-07 09:12:44 +02:00
set custom contact paths
This commit is contained in:
@@ -873,6 +873,43 @@ bool MyMesh::uiSendTelemetryRequest(uint32_t contact_idx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::setCustomPath(int contactIdx, const uint8_t* path, uint8_t pathLen, bool lock) {
|
||||
ContactInfo contact;
|
||||
if (!getContactByIdx(contactIdx, contact)) return false;
|
||||
|
||||
ContactInfo* c = lookupContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
|
||||
if (!c) return false;
|
||||
|
||||
c->out_path_len = pathLen;
|
||||
int byteLen = mesh::Packet::getPathByteLenFor(pathLen);
|
||||
if (byteLen > MAX_PATH_SIZE) byteLen = MAX_PATH_SIZE;
|
||||
memcpy(c->out_path, path, byteLen);
|
||||
c->lastmod = getRTCClock()->getCurrentTime();
|
||||
|
||||
if (lock) {
|
||||
c->flags |= CONTACT_FLAG_CUSTOM_PATH;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("setCustomPath: contact %s, pathLen=0x%02X (%d hops, %dB/hop), lock=%d",
|
||||
c->name, pathLen, pathLen & 0x3F, ((pathLen >> 6) & 3) + 1, lock);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyMesh::clearCustomPath(int contactIdx) {
|
||||
ContactInfo contact;
|
||||
if (!getContactByIdx(contactIdx, contact)) return;
|
||||
|
||||
ContactInfo* c = lookupContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
|
||||
if (!c) return;
|
||||
|
||||
c->out_path_len = OUT_PATH_UNKNOWN;
|
||||
memset(c->out_path, 0, MAX_PATH_SIZE);
|
||||
c->flags &= ~CONTACT_FLAG_CUSTOM_PATH;
|
||||
c->lastmod = getRTCClock()->getCurrentTime();
|
||||
|
||||
MESH_DEBUG_PRINTLN("clearCustomPath: contact %s — reverted to auto-discovery", c->name);
|
||||
}
|
||||
|
||||
uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
|
||||
uint8_t len, uint8_t *reply) {
|
||||
if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) {
|
||||
@@ -1043,6 +1080,13 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
|
||||
}
|
||||
}
|
||||
// let base class handle received path and data
|
||||
// BUT: if this contact has a custom (manually set) path lock, don't let
|
||||
// auto-discovery overwrite it. Skip the base class call entirely — ACKs
|
||||
// embedded in path responses will still be delivered via separate ACK packets.
|
||||
if (contact.flags & CONTACT_FLAG_CUSTOM_PATH) {
|
||||
MESH_DEBUG_PRINTLN("onContactPathRecv: skipping path update for custom-path contact %s", contact.name);
|
||||
return false;
|
||||
}
|
||||
return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
#include <helpers/BaseChatMesh.h>
|
||||
#include <helpers/TransportKeyStore.h>
|
||||
|
||||
// Custom path lock flag — bit 7 of ContactInfo.flags
|
||||
// When set, onContactPathRecv skips auto-updating this contact's out_path.
|
||||
// Bits 0-6 remain available (bit 0 = favourite, bits 1-3 = telemetry perms).
|
||||
#define CONTACT_FLAG_CUSTOM_PATH 0x80
|
||||
|
||||
/* -------------------------------------------------------------------------------------- */
|
||||
|
||||
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
|
||||
@@ -158,6 +163,11 @@ public:
|
||||
bool uiSendTelemetryRequest(uint32_t contact_idx);
|
||||
int getAdminContactIdx() const { return _admin_contact_idx; }
|
||||
|
||||
// Custom path editor — set or clear a manually configured path for a contact
|
||||
// When locked, automatic path discovery will not overwrite this contact's path.
|
||||
bool setCustomPath(int contactIdx, const uint8_t* path, uint8_t pathLen, bool lock);
|
||||
void clearCustomPath(int contactIdx);
|
||||
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "RepeaterAdminScreen.h"
|
||||
#include "DiscoveryScreen.h"
|
||||
#include "LastHeardScreen.h"
|
||||
#include "PathEditorScreen.h"
|
||||
#ifdef MECK_WEB_READER
|
||||
#include "WebReaderScreen.h"
|
||||
#endif
|
||||
@@ -366,6 +367,7 @@
|
||||
#include "RepeaterAdminScreen.h"
|
||||
#include "DiscoveryScreen.h"
|
||||
#include "LastHeardScreen.h"
|
||||
#include "PathEditorScreen.h"
|
||||
|
||||
static TouchDrvGT911 gt911Touch;
|
||||
static bool gt911Ready = false;
|
||||
@@ -962,6 +964,20 @@ static void lastHeardToggleContact() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Path editor screen: tap to select row, tap same to activate
|
||||
if (ui_task.isOnPathEditor()) {
|
||||
PathEditorScreen* pe = (PathEditorScreen*)ui_task.getPathEditorScreen();
|
||||
if (pe) {
|
||||
int result = pe->selectRowAtVY(vy);
|
||||
if (result == 1) {
|
||||
ui_task.forceRefresh();
|
||||
return 0;
|
||||
}
|
||||
if (result == 2) return KEY_ENTER;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Discovery screen: tap to select, tap same to add
|
||||
if (ui_task.isOnDiscoveryScreen()) {
|
||||
DiscoveryScreen* ds = (DiscoveryScreen*)ui_task.getDiscoveryScreen();
|
||||
@@ -1232,6 +1248,11 @@ static void lastHeardToggleContact() {
|
||||
return 'f';
|
||||
}
|
||||
|
||||
// Path editor: long press = Enter (select item)
|
||||
if (ui_task.isOnPathEditor()) {
|
||||
return KEY_ENTER;
|
||||
}
|
||||
|
||||
// Repeater admin: long press → open keyboard for password or CLI
|
||||
if (ui_task.isOnRepeaterAdmin()) {
|
||||
RepeaterAdminScreen* admin = (RepeaterAdminScreen*)ui_task.getRepeaterAdminScreen();
|
||||
@@ -2510,6 +2531,13 @@ void loop() {
|
||||
char c = mapTouchLongPress(touchDownX, touchDownY);
|
||||
if (c) {
|
||||
ui_task.injectKey(c);
|
||||
// Path editor: check if Save & Exit was triggered
|
||||
if (ui_task.isOnPathEditor()) {
|
||||
PathEditorScreen* pe = (PathEditorScreen*)ui_task.getPathEditorScreen();
|
||||
if (pe && pe->wantsExit()) {
|
||||
ui_task.gotoContactsScreen();
|
||||
}
|
||||
}
|
||||
cpuPower.setBoost();
|
||||
}
|
||||
lastTouchEventMs = now;
|
||||
@@ -2522,6 +2550,13 @@ void loop() {
|
||||
char c = mapTouchTap(touchDownX, touchDownY);
|
||||
if (c) {
|
||||
ui_task.injectKey(c);
|
||||
// Path editor: check if Save & Exit was triggered
|
||||
if (ui_task.isOnPathEditor()) {
|
||||
PathEditorScreen* pe = (PathEditorScreen*)ui_task.getPathEditorScreen();
|
||||
if (pe && pe->wantsExit()) {
|
||||
ui_task.gotoContactsScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
cpuPower.setBoost();
|
||||
lastTouchEventMs = now;
|
||||
@@ -2728,6 +2763,13 @@ void loop() {
|
||||
ui_task.showVirtualKeyboard(VKB_ADMIN_CLI, "Admin Command", "", 137);
|
||||
}
|
||||
}
|
||||
} else if (ui_task.isOnPathEditor()) {
|
||||
// Path editor handles Enter internally
|
||||
ui_task.injectKey('\r');
|
||||
PathEditorScreen* pe = (PathEditorScreen*)ui_task.getPathEditorScreen();
|
||||
if (pe && pe->wantsExit()) {
|
||||
ui_task.gotoContactsScreen();
|
||||
}
|
||||
} else {
|
||||
// All other screens: pass Enter through for native handling
|
||||
// (settings toggle, discovery add-contact, last heard, text reader, notes file list, etc.)
|
||||
@@ -2735,12 +2777,26 @@ void loop() {
|
||||
}
|
||||
} else {
|
||||
// Non-Enter keys: remap arrows to WASD, pass others through
|
||||
switch (ckb) {
|
||||
case (char)0xF2: ui_task.injectKey('w'); break; // Up → scroll up
|
||||
case (char)0xF1: ui_task.injectKey('s'); break; // Down → scroll down
|
||||
case (char)0xF3: ui_task.injectKey('a'); break; // Left → prev channel/category
|
||||
case (char)0xF4: ui_task.injectKey('d'); break; // Right → next channel/category
|
||||
default: ui_task.injectKey(ckb); break;
|
||||
// Special: 'p' on contacts screen opens path editor
|
||||
if ((ckb == 'p' || ckb == 'P') && ui_task.isOnContactsScreen()) {
|
||||
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
|
||||
if (cs) {
|
||||
int idx = cs->getSelectedContactIdx();
|
||||
if (idx >= 0) {
|
||||
ui_task.gotoPathEditor(idx);
|
||||
}
|
||||
}
|
||||
} else if ((ckb == 'q' || ckb == 'Q') && ui_task.isOnPathEditor()) {
|
||||
// Q on path editor → back to contacts
|
||||
ui_task.gotoContactsScreen();
|
||||
} else {
|
||||
switch (ckb) {
|
||||
case (char)0xF2: ui_task.injectKey('w'); break; // Up → scroll up
|
||||
case (char)0xF1: ui_task.injectKey('s'); break; // Down → scroll down
|
||||
case (char)0xF3: ui_task.injectKey('a'); break; // Left → prev channel/category
|
||||
case (char)0xF4: ui_task.injectKey('d'); break; // Right → next channel/category
|
||||
default: ui_task.injectKey(ckb); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3519,9 +3575,23 @@ void handleKeyboardInput() {
|
||||
ui_task.gotoTextReader();
|
||||
break;
|
||||
|
||||
#ifndef HAS_4G_MODEM
|
||||
case 'p':
|
||||
// Open audiobook player - lazy-init Audio + screen on first use
|
||||
// On contacts screen: open path editor for selected contact
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
|
||||
if (cs) {
|
||||
int idx = cs->getSelectedContactIdx();
|
||||
if (idx >= 0) {
|
||||
char pname[32];
|
||||
cs->getSelectedContactName(pname, sizeof(pname));
|
||||
Serial.printf("Opening path editor for %s (idx %d)\n", pname, idx);
|
||||
ui_task.gotoPathEditor(idx);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
#ifndef HAS_4G_MODEM
|
||||
// Otherwise: open audiobook player - lazy-init Audio + screen on first use
|
||||
Serial.println("Opening audiobook player");
|
||||
if (!ui_task.getAudiobookScreen()) {
|
||||
Serial.printf("Audiobook: lazy init - free heap: %d, largest block: %d\n",
|
||||
@@ -3533,8 +3603,8 @@ void handleKeyboardInput() {
|
||||
Serial.printf("Audiobook: init complete - free heap: %d\n", ESP.getFreeHeap());
|
||||
}
|
||||
ui_task.gotoAudiobookPlayer();
|
||||
break;
|
||||
#endif
|
||||
break;
|
||||
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
case 'k':
|
||||
@@ -3653,6 +3723,7 @@ void handleKeyboardInput() {
|
||||
// Open settings (from home), or navigate down on channel/contacts/admin/web/map/discovery/lastheard
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
|
||||
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|
||||
|| ui_task.isOnPathEditor()
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
@@ -3672,6 +3743,7 @@ void handleKeyboardInput() {
|
||||
// Navigate up/previous (scroll on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
|
||||
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|
||||
|| ui_task.isOnPathEditor()
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
@@ -3690,6 +3762,7 @@ void handleKeyboardInput() {
|
||||
case 'a':
|
||||
// Navigate left or switch channel (on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
|
||||
|| ui_task.isOnPathEditor()
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
@@ -3704,6 +3777,7 @@ void handleKeyboardInput() {
|
||||
case 'd':
|
||||
// Navigate right or switch channel (on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
|
||||
|| ui_task.isOnPathEditor()
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
@@ -3716,9 +3790,16 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
|
||||
case '\r':
|
||||
// Select/Enter - if on contacts screen, enter DM compose for chat contacts
|
||||
// or repeater admin for repeater contacts
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
// Select/Enter - path editor handles Enter internally
|
||||
if (ui_task.isOnPathEditor()) {
|
||||
ui_task.injectKey('\r');
|
||||
// Check if Save & Exit was selected
|
||||
PathEditorScreen* pe = (PathEditorScreen*)ui_task.getPathEditorScreen();
|
||||
if (pe && pe->wantsExit()) {
|
||||
Serial.println("PathEditor: Save & Exit — returning to contacts");
|
||||
ui_task.gotoContactsScreen();
|
||||
}
|
||||
} else if (ui_task.isOnContactsScreen()) {
|
||||
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
|
||||
int idx = cs->getSelectedContactIdx();
|
||||
uint8_t ctype = cs->getSelectedContactType();
|
||||
@@ -3985,6 +4066,12 @@ void handleKeyboardInput() {
|
||||
ui_task.gotoContactsScreen();
|
||||
break;
|
||||
}
|
||||
// Path editor: Q goes back to contacts (discards unsaved changes)
|
||||
if (ui_task.isOnPathEditor()) {
|
||||
Serial.println("Nav: PathEditor -> Contacts");
|
||||
ui_task.gotoContactsScreen();
|
||||
break;
|
||||
}
|
||||
// Alarm screen: Q/backspace routing depends on sub-mode
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
if (ui_task.isOnAlarmScreen()) {
|
||||
|
||||
@@ -300,10 +300,19 @@ public:
|
||||
|
||||
// Reserve space for hops + age on right side
|
||||
char hopStr[6];
|
||||
if (contact.out_path_len == 0xFF || contact.out_path_len == 0) {
|
||||
strcpy(hopStr, "D"); // direct
|
||||
if (contact.out_path_len == 0xFF) {
|
||||
strcpy(hopStr, "?"); // unknown path
|
||||
} else if (contact.out_path_len == 0) {
|
||||
bool customDirect = (contact.flags & CONTACT_FLAG_CUSTOM_PATH) != 0;
|
||||
strcpy(hopStr, customDirect ? "D*" : "D");
|
||||
} else {
|
||||
snprintf(hopStr, sizeof(hopStr), "%d", contact.out_path_len);
|
||||
int hops = contact.out_path_len & 0x3F; // lower 6 bits = hop count
|
||||
bool customPath = (contact.flags & CONTACT_FLAG_CUSTOM_PATH) != 0;
|
||||
if (customPath) {
|
||||
snprintf(hopStr, sizeof(hopStr), "%d*", hops); // asterisk = custom/locked path
|
||||
} else {
|
||||
snprintf(hopStr, sizeof(hopStr), "%d", hops);
|
||||
}
|
||||
}
|
||||
|
||||
char ageStr[6];
|
||||
@@ -352,8 +361,8 @@ public:
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Bk A/D:Filter");
|
||||
|
||||
// Right: Tap/Ent:Select
|
||||
const char* right = "Tap/Ent:Select";
|
||||
// Right: P:Path Ent:Sel
|
||||
const char* right = "P:Path Ent:Sel";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,805 @@
|
||||
#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;
|
||||
|
||||
class PathEditorScreen : public UIScreen {
|
||||
public:
|
||||
enum EditorState {
|
||||
STATE_MAIN,
|
||||
STATE_PICK_HOP
|
||||
};
|
||||
|
||||
// Main-state menu items (dynamic, built each render)
|
||||
enum MenuItem {
|
||||
MENU_MODE = 0, // "Mode: 1B/hop" or "Mode: 2B/hop"
|
||||
// After mode: hop lines (MENU_HOP_BASE + i)
|
||||
// Then: action items
|
||||
MENU_HOP_BASE = 1,
|
||||
// Dynamic items after hops:
|
||||
MENU_ADD_HOP = 100,
|
||||
MENU_SET_DIRECT,
|
||||
MENU_REMOVE_LAST,
|
||||
MENU_CLEAR_PATH,
|
||||
MENU_SAVE_EXIT
|
||||
};
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
|
||||
int _contactIdx; // Index into contact table
|
||||
char _contactName[32]; // Contact name for header
|
||||
|
||||
EditorState _state;
|
||||
int _menuSel; // Selected menu item index (0-based in visible list)
|
||||
int _menuCount; // Total visible menu items
|
||||
|
||||
// Path being edited (working copy)
|
||||
uint8_t _pathBuf[MAX_PATH_SIZE];
|
||||
uint8_t _pathLen; // Encoded: bits[7:6]=mode, bits[5:0]=hops
|
||||
int _hopCount; // Decoded hop count
|
||||
int _bytesPerHop; // 1 or 2
|
||||
|
||||
// Repeater picker state
|
||||
static const int MAX_REPEATERS = 200;
|
||||
uint16_t* _repIdx; // Indices into contact table (PSRAM)
|
||||
int _repCount; // Number of repeaters found
|
||||
int _repSel; // Selected repeater in picker
|
||||
int _repScroll; // Scroll offset in picker
|
||||
|
||||
bool _dirty; // Path has been modified
|
||||
bool _wantExit; // Set by Save & Exit — caller should navigate back
|
||||
bool _directLocked; // True = path is explicitly set to direct (0 hops, locked)
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
void decodePath() {
|
||||
_hopCount = _pathLen & 0x3F;
|
||||
uint8_t mode = (_pathLen >> 6) & 0x03;
|
||||
_bytesPerHop = mode + 1;
|
||||
}
|
||||
|
||||
uint8_t encodePath() const {
|
||||
uint8_t mode = (_bytesPerHop - 1) & 0x03;
|
||||
return (mode << 6) | (_hopCount & 0x3F);
|
||||
}
|
||||
|
||||
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 a contact name by matching pub_key prefix bytes
|
||||
bool findNameForHop(int hopIndex, char* name, size_t nameLen) const {
|
||||
if (hopIndex < 0 || hopIndex >= _hopCount) return false;
|
||||
int offset = hopIndex * _bytesPerHop;
|
||||
uint32_t numContacts = the_mesh.getNumContacts();
|
||||
ContactInfo c;
|
||||
for (uint32_t i = 0; i < numContacts; i++) {
|
||||
if (the_mesh.getContactByIdx(i, c)) {
|
||||
bool match = true;
|
||||
for (int b = 0; b < _bytesPerHop; b++) {
|
||||
if (c.id.pub_key[b] != _pathBuf[offset + b]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
strncpy(name, c.name, nameLen);
|
||||
name[nameLen - 1] = '\0';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build the visible menu items list and return count
|
||||
// Menu layout:
|
||||
// 0: Mode selector
|
||||
// 1..hopCount: each hop
|
||||
// hopCount+1: Add hop
|
||||
// hopCount+2: Remove last (only if hops > 0)
|
||||
// hopCount+2 or +3: Clear path (only if custom path flag set or hops > 0)
|
||||
// last: Save & Exit
|
||||
int buildMenuCount() const {
|
||||
int count = 1; // Mode selector
|
||||
count += _hopCount; // One per hop
|
||||
if (_hopCount < 8) count++; // Add hop (max 8 hops)
|
||||
count++; // Set Direct (always visible)
|
||||
if (_hopCount > 0) count++; // Remove last
|
||||
if (_hopCount > 0 || _directLocked || isCustomPathSet()) count++; // Clear path
|
||||
count++; // Save & Exit
|
||||
return count;
|
||||
}
|
||||
|
||||
// Map a menu index to a MenuItem enum
|
||||
MenuItem menuItemAt(int idx) const {
|
||||
if (idx == 0) return MENU_MODE;
|
||||
int pos = 1;
|
||||
// Hop lines
|
||||
for (int h = 0; h < _hopCount; h++) {
|
||||
if (idx == pos) return (MenuItem)(MENU_HOP_BASE + h);
|
||||
pos++;
|
||||
}
|
||||
// Add hop
|
||||
if (_hopCount < 8) {
|
||||
if (idx == pos) return MENU_ADD_HOP;
|
||||
pos++;
|
||||
}
|
||||
// Set Direct
|
||||
if (idx == pos) return MENU_SET_DIRECT;
|
||||
pos++;
|
||||
// Remove last
|
||||
if (_hopCount > 0) {
|
||||
if (idx == pos) return MENU_REMOVE_LAST;
|
||||
pos++;
|
||||
}
|
||||
// Clear path
|
||||
if (_hopCount > 0 || _directLocked || isCustomPathSet()) {
|
||||
if (idx == pos) return MENU_CLEAR_PATH;
|
||||
pos++;
|
||||
}
|
||||
// Save & Exit
|
||||
return MENU_SAVE_EXIT;
|
||||
}
|
||||
|
||||
bool isCustomPathSet() const {
|
||||
ContactInfo c;
|
||||
if (!the_mesh.getContactByIdx(_contactIdx, c)) return false;
|
||||
return (c.flags & CONTACT_FLAG_CUSTOM_PATH) != 0;
|
||||
}
|
||||
|
||||
public:
|
||||
PathEditorScreen(UITask* task, mesh::RTCClock* rtc)
|
||||
: _task(task), _rtc(rtc), _contactIdx(-1), _state(STATE_MAIN),
|
||||
_menuSel(0), _menuCount(1), _pathLen(0), _hopCount(0),
|
||||
_bytesPerHop(1), _repCount(0), _repSel(0), _repScroll(0),
|
||||
_dirty(false), _wantExit(false), _directLocked(false) {
|
||||
memset(_contactName, 0, sizeof(_contactName));
|
||||
memset(_pathBuf, 0, sizeof(_pathBuf));
|
||||
#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
|
||||
}
|
||||
|
||||
void openForContact(int contactIdx) {
|
||||
_contactIdx = contactIdx;
|
||||
_state = STATE_MAIN;
|
||||
_menuSel = 0;
|
||||
_repSel = 0;
|
||||
_repScroll = 0;
|
||||
_dirty = false;
|
||||
_wantExit = false;
|
||||
_directLocked = false;
|
||||
|
||||
// Load contact info
|
||||
ContactInfo c;
|
||||
if (the_mesh.getContactByIdx(contactIdx, c)) {
|
||||
strncpy(_contactName, c.name, sizeof(_contactName) - 1);
|
||||
_contactName[sizeof(_contactName) - 1] = '\0';
|
||||
|
||||
// Copy current path
|
||||
if (c.out_path_len != OUT_PATH_UNKNOWN) {
|
||||
_pathLen = c.out_path_len;
|
||||
decodePath();
|
||||
int byteLen = _hopCount * _bytesPerHop;
|
||||
if (byteLen > MAX_PATH_SIZE) byteLen = MAX_PATH_SIZE;
|
||||
memcpy(_pathBuf, c.out_path, byteLen);
|
||||
// Detect existing direct-locked path
|
||||
if (_hopCount == 0 && (c.flags & CONTACT_FLAG_CUSTOM_PATH)) {
|
||||
_directLocked = true;
|
||||
}
|
||||
} else {
|
||||
_pathLen = 0;
|
||||
_hopCount = 0;
|
||||
_bytesPerHop = 1;
|
||||
memset(_pathBuf, 0, sizeof(_pathBuf));
|
||||
}
|
||||
} else {
|
||||
strcpy(_contactName, "Unknown");
|
||||
_pathLen = 0;
|
||||
_hopCount = 0;
|
||||
_bytesPerHop = 1;
|
||||
}
|
||||
|
||||
_menuCount = buildMenuCount();
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
if (_state == STATE_PICK_HOP) {
|
||||
return renderPicker(display);
|
||||
}
|
||||
return renderMain(display);
|
||||
}
|
||||
|
||||
int renderMain(DisplayDriver& display) {
|
||||
char tmp[64];
|
||||
|
||||
// === Header ===
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
snprintf(tmp, sizeof(tmp), "Path: %s", _contactName);
|
||||
// Truncate if too long
|
||||
if (display.getTextWidth(tmp) > display.width() - 4) {
|
||||
snprintf(tmp, sizeof(tmp), "Path: %.12s..", _contactName);
|
||||
}
|
||||
display.print(tmp);
|
||||
|
||||
// Show lock icon or dirty indicator on right
|
||||
if (_dirty) {
|
||||
const char* mod = "[*]";
|
||||
display.setCursor(display.width() - display.getTextWidth(mod) - 2, 0);
|
||||
display.print(mod);
|
||||
} else if (isCustomPathSet()) {
|
||||
const char* lock = "[L]";
|
||||
display.setCursor(display.width() - display.getTextWidth(lock) - 2, 0);
|
||||
display.print(lock);
|
||||
}
|
||||
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// === Body ===
|
||||
display.setTextSize(0);
|
||||
int lineH = 9;
|
||||
int headerH = 14;
|
||||
int footerH = 14;
|
||||
int maxY = display.height() - footerH;
|
||||
int y = headerH;
|
||||
|
||||
_menuCount = buildMenuCount();
|
||||
|
||||
// Center visible window around selected item
|
||||
int maxVisible = (maxY - headerH) / lineH;
|
||||
if (maxVisible < 3) maxVisible = 3;
|
||||
int startIdx = max(0, min(_menuSel - maxVisible / 2, _menuCount - maxVisible));
|
||||
if (startIdx < 0) startIdx = 0;
|
||||
int endIdx = min(_menuCount, startIdx + maxVisible);
|
||||
|
||||
for (int i = startIdx; i < endIdx && y + lineH <= maxY; i++) {
|
||||
bool selected = (i == _menuSel);
|
||||
MenuItem item = menuItemAt(i);
|
||||
|
||||
if (selected) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
display.setCursor(2, y);
|
||||
char prefix = selected ? '>' : ' ';
|
||||
|
||||
switch (item) {
|
||||
case MENU_MODE:
|
||||
if (_directLocked) {
|
||||
snprintf(tmp, sizeof(tmp), "%c Mode: DIRECT", prefix);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "%c Mode: %dB/hop", prefix, _bytesPerHop);
|
||||
}
|
||||
display.print(tmp);
|
||||
// Show hint on right
|
||||
if (!_directLocked) {
|
||||
const char* hint = "(A/D)";
|
||||
display.setCursor(display.width() - display.getTextWidth(hint) - 4, y);
|
||||
display.print(hint);
|
||||
}
|
||||
break;
|
||||
|
||||
case MENU_ADD_HOP:
|
||||
snprintf(tmp, sizeof(tmp), "%c + Add hop...", prefix);
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case MENU_SET_DIRECT:
|
||||
if (_directLocked) {
|
||||
snprintf(tmp, sizeof(tmp), "%c * Direct (set)", prefix);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "%c * Set Direct", prefix);
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case MENU_REMOVE_LAST:
|
||||
snprintf(tmp, sizeof(tmp), "%c - Remove last hop", prefix);
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case MENU_CLEAR_PATH:
|
||||
snprintf(tmp, sizeof(tmp), "%c Clear custom path", prefix);
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case MENU_SAVE_EXIT:
|
||||
snprintf(tmp, sizeof(tmp), "%c Save & Exit", prefix);
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Hop line: MENU_HOP_BASE + hopIndex
|
||||
if (item >= MENU_HOP_BASE && item < MENU_HOP_BASE + 64) {
|
||||
int hopIdx = item - MENU_HOP_BASE;
|
||||
char hopName[24];
|
||||
int offset = hopIdx * _bytesPerHop;
|
||||
|
||||
if (findNameForHop(hopIdx, hopName, sizeof(hopName))) {
|
||||
if (_bytesPerHop == 1) {
|
||||
snprintf(tmp, sizeof(tmp), "%c %d: %s (%02X)", prefix, hopIdx + 1,
|
||||
hopName, _pathBuf[offset]);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "%c %d: %s (%02X%02X)", prefix, hopIdx + 1,
|
||||
hopName, _pathBuf[offset], _pathBuf[offset + 1]);
|
||||
}
|
||||
} else {
|
||||
if (_bytesPerHop == 1) {
|
||||
snprintf(tmp, sizeof(tmp), "%c %d: ??? (%02X)", prefix, hopIdx + 1,
|
||||
_pathBuf[offset]);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "%c %d: ??? (%02X%02X)", prefix, hopIdx + 1,
|
||||
_pathBuf[offset], _pathBuf[offset + 1]);
|
||||
}
|
||||
}
|
||||
display.drawTextEllipsized(2, y, display.width() - 4, tmp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
y += lineH;
|
||||
}
|
||||
|
||||
// === Footer ===
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Swipe:Nav");
|
||||
const char* right = "Hold:Select";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
#else
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Bk W/S:Nav");
|
||||
const char* right = "Enter:Sel";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
#endif
|
||||
|
||||
return 5000;
|
||||
}
|
||||
|
||||
int renderPicker(DisplayDriver& display) {
|
||||
char tmp[64];
|
||||
|
||||
// === Header ===
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
snprintf(tmp, sizeof(tmp), "Select Repeater (%d)", _repCount);
|
||||
display.print(tmp);
|
||||
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// === Body ===
|
||||
display.setTextSize(0);
|
||||
int lineH = 9;
|
||||
int headerH = 14;
|
||||
int footerH = 14;
|
||||
int maxY = display.height() - footerH;
|
||||
int y = headerH;
|
||||
|
||||
if (_repCount == 0) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, y);
|
||||
display.print("No repeaters in contacts");
|
||||
display.setCursor(0, y + lineH);
|
||||
display.print("Add repeaters first");
|
||||
} else {
|
||||
int maxVisible = (maxY - headerH) / lineH;
|
||||
if (maxVisible < 3) maxVisible = 3;
|
||||
int startIdx = max(0, min(_repSel - maxVisible / 2, _repCount - maxVisible));
|
||||
if (startIdx < 0) startIdx = 0;
|
||||
int endIdx = min(_repCount, startIdx + maxVisible);
|
||||
|
||||
for (int i = startIdx; i < endIdx && y + lineH <= maxY; i++) {
|
||||
ContactInfo c;
|
||||
if (!the_mesh.getContactByIdx(_repIdx[i], c)) continue;
|
||||
|
||||
bool selected = (i == _repSel);
|
||||
|
||||
if (selected) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
display.setCursor(2, y);
|
||||
char prefix = selected ? '>' : ' ';
|
||||
|
||||
if (_bytesPerHop == 1) {
|
||||
snprintf(tmp, sizeof(tmp), "%c %s (%02X)", prefix, c.name, c.id.pub_key[0]);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "%c %s (%02X%02X)", prefix, c.name,
|
||||
c.id.pub_key[0], c.id.pub_key[1]);
|
||||
}
|
||||
display.drawTextEllipsized(2, y, display.width() - 4, tmp);
|
||||
|
||||
y += lineH;
|
||||
}
|
||||
}
|
||||
|
||||
// === Footer ===
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Swipe:Scroll");
|
||||
const char* right = "Hold:Add Back:Cancel";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
#else
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Cancel W/S:Scroll");
|
||||
const char* right = "Enter:Add";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
#endif
|
||||
|
||||
return 5000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (_state == STATE_PICK_HOP) {
|
||||
return handlePickerInput(c);
|
||||
}
|
||||
return handleMainInput(c);
|
||||
}
|
||||
|
||||
bool handleMainInput(char c) {
|
||||
// W - scroll up
|
||||
if (c == 'w' || c == 'W' || c == 0xF2) {
|
||||
if (_menuSel > 0) {
|
||||
_menuSel--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// S - scroll down
|
||||
if (c == 's' || c == 'S' || c == 0xF1) {
|
||||
if (_menuSel < _menuCount - 1) {
|
||||
_menuSel++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A/D — toggle mode (only when Mode item is selected and not direct-locked)
|
||||
if (c == 'a' || c == 'A' || c == 'd' || c == 'D') {
|
||||
MenuItem item = menuItemAt(_menuSel);
|
||||
if (item == MENU_MODE && !_directLocked) {
|
||||
// Toggle between 1-byte and 2-byte
|
||||
if (_bytesPerHop == 1) {
|
||||
switchMode(2);
|
||||
} else {
|
||||
switchMode(1);
|
||||
}
|
||||
_dirty = true;
|
||||
_menuCount = buildMenuCount();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enter - select
|
||||
if (c == 13 || c == KEY_ENTER || c == '\r') {
|
||||
MenuItem item = menuItemAt(_menuSel);
|
||||
|
||||
switch (item) {
|
||||
case MENU_MODE:
|
||||
// Toggle mode on Enter too (no-op if direct locked)
|
||||
if (!_directLocked) {
|
||||
if (_bytesPerHop == 1) {
|
||||
switchMode(2);
|
||||
} else {
|
||||
switchMode(1);
|
||||
}
|
||||
_dirty = true;
|
||||
_menuCount = buildMenuCount();
|
||||
}
|
||||
return true;
|
||||
|
||||
case MENU_ADD_HOP:
|
||||
// Enter picker mode — adding a hop clears direct lock
|
||||
_directLocked = false;
|
||||
buildRepeaterList();
|
||||
_repSel = 0;
|
||||
_repScroll = 0;
|
||||
_state = STATE_PICK_HOP;
|
||||
return true;
|
||||
|
||||
case MENU_SET_DIRECT:
|
||||
// Set path to direct (0 hops, locked)
|
||||
_hopCount = 0;
|
||||
_pathLen = 0;
|
||||
memset(_pathBuf, 0, sizeof(_pathBuf));
|
||||
_directLocked = true;
|
||||
_dirty = true;
|
||||
_menuCount = buildMenuCount();
|
||||
return true;
|
||||
|
||||
case MENU_REMOVE_LAST:
|
||||
if (_hopCount > 0) {
|
||||
_hopCount--;
|
||||
_pathLen = encodePath();
|
||||
_dirty = true;
|
||||
_menuCount = buildMenuCount();
|
||||
// Clamp selection
|
||||
if (_menuSel >= _menuCount) _menuSel = _menuCount - 1;
|
||||
}
|
||||
return true;
|
||||
|
||||
case MENU_CLEAR_PATH:
|
||||
_hopCount = 0;
|
||||
_pathLen = 0;
|
||||
_directLocked = false;
|
||||
memset(_pathBuf, 0, sizeof(_pathBuf));
|
||||
_dirty = true;
|
||||
_menuCount = buildMenuCount();
|
||||
_menuSel = 0;
|
||||
return true;
|
||||
|
||||
case MENU_SAVE_EXIT:
|
||||
savePath();
|
||||
_wantExit = true; // Signal to main.cpp to navigate back to contacts
|
||||
return true;
|
||||
|
||||
default:
|
||||
// Hop line — no action (could add remove-specific-hop later)
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Q - back (discard changes or prompt?)
|
||||
// For simplicity, just go back without saving
|
||||
if (c == 'q' || c == 'Q') {
|
||||
// Return to contacts screen without saving
|
||||
// The UITask will handle this via the key falling through
|
||||
return false; // Let UITask handle Q as back
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool handlePickerInput(char c) {
|
||||
// W - scroll up
|
||||
if (c == 'w' || c == 'W' || c == 0xF2) {
|
||||
if (_repSel > 0) {
|
||||
_repSel--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// S - scroll down
|
||||
if (c == 's' || c == 'S' || c == 0xF1) {
|
||||
if (_repSel < _repCount - 1) {
|
||||
_repSel++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enter - add selected repeater as hop
|
||||
if (c == 13 || c == KEY_ENTER || c == '\r') {
|
||||
if (_repCount > 0 && _repSel >= 0 && _repSel < _repCount) {
|
||||
addHopFromContact(_repIdx[_repSel]);
|
||||
}
|
||||
_state = STATE_MAIN;
|
||||
_menuCount = buildMenuCount();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Q - cancel picker, return to main
|
||||
if (c == 'q' || c == 'Q') {
|
||||
_state = STATE_MAIN;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tap-to-select for T5S3 touch
|
||||
int selectRowAtVY(int vy) {
|
||||
if (_state == STATE_PICK_HOP) {
|
||||
return selectPickerRowAtVY(vy);
|
||||
}
|
||||
return selectMainRowAtVY(vy);
|
||||
}
|
||||
|
||||
int selectMainRowAtVY(int vy) {
|
||||
if (_menuCount == 0) return 0;
|
||||
const int headerH = 14, footerH = 14, lineH = 9;
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const int bodyTop = headerH;
|
||||
#else
|
||||
const int bodyTop = headerH + 5;
|
||||
#endif
|
||||
if (vy < bodyTop || vy >= 128 - footerH) return 0;
|
||||
|
||||
int maxVisible = (128 - headerH - footerH) / lineH;
|
||||
if (maxVisible < 3) maxVisible = 3;
|
||||
int startIdx = max(0, min(_menuSel - maxVisible / 2, _menuCount - maxVisible));
|
||||
if (startIdx < 0) startIdx = 0;
|
||||
|
||||
int tappedRow = startIdx + (vy - bodyTop) / lineH;
|
||||
if (tappedRow < 0 || tappedRow >= _menuCount) return 0;
|
||||
if (tappedRow == _menuSel) return 2;
|
||||
_menuSel = tappedRow;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int selectPickerRowAtVY(int vy) {
|
||||
if (_repCount == 0) return 0;
|
||||
const int headerH = 14, footerH = 14, lineH = 9;
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const int bodyTop = headerH;
|
||||
#else
|
||||
const int bodyTop = headerH + 5;
|
||||
#endif
|
||||
if (vy < bodyTop || vy >= 128 - footerH) return 0;
|
||||
|
||||
int maxVisible = (128 - headerH - footerH) / lineH;
|
||||
if (maxVisible < 3) maxVisible = 3;
|
||||
int startIdx = max(0, min(_repSel - maxVisible / 2, _repCount - maxVisible));
|
||||
if (startIdx < 0) startIdx = 0;
|
||||
|
||||
int tappedRow = startIdx + (vy - bodyTop) / lineH;
|
||||
if (tappedRow < 0 || tappedRow >= _repCount) return 0;
|
||||
if (tappedRow == _repSel) return 2;
|
||||
_repSel = tappedRow;
|
||||
return 1;
|
||||
}
|
||||
|
||||
EditorState getState() const { return _state; }
|
||||
bool isDirty() const { return _dirty; }
|
||||
bool wantsExit() const { return _wantExit; }
|
||||
|
||||
private:
|
||||
void switchMode(int newBytesPerHop) {
|
||||
if (newBytesPerHop == _bytesPerHop) return;
|
||||
|
||||
if (_hopCount > 0) {
|
||||
// Rebuild path buffer for new mode
|
||||
// We need the full pub_keys to re-extract the right prefix bytes
|
||||
uint8_t newBuf[MAX_PATH_SIZE];
|
||||
memset(newBuf, 0, sizeof(newBuf));
|
||||
int newHopCount = 0;
|
||||
|
||||
for (int h = 0; h < _hopCount && newHopCount < 8; h++) {
|
||||
int oldOffset = h * _bytesPerHop;
|
||||
// Try to find the contact that matches this hop
|
||||
uint32_t numContacts = the_mesh.getNumContacts();
|
||||
ContactInfo c;
|
||||
bool found = false;
|
||||
for (uint32_t i = 0; i < numContacts; i++) {
|
||||
if (the_mesh.getContactByIdx(i, c)) {
|
||||
bool match = true;
|
||||
for (int b = 0; b < _bytesPerHop; b++) {
|
||||
if (c.id.pub_key[b] != _pathBuf[oldOffset + b]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
// Found the contact — copy new prefix size
|
||||
int newOffset = newHopCount * newBytesPerHop;
|
||||
for (int b = 0; b < newBytesPerHop; b++) {
|
||||
newBuf[newOffset + b] = c.id.pub_key[b];
|
||||
}
|
||||
newHopCount++;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// Contact not found — copy what we can
|
||||
int newOffset = newHopCount * newBytesPerHop;
|
||||
int oldOff = h * _bytesPerHop;
|
||||
for (int b = 0; b < newBytesPerHop; b++) {
|
||||
if (b < _bytesPerHop) {
|
||||
newBuf[newOffset + b] = _pathBuf[oldOff + b];
|
||||
} else {
|
||||
newBuf[newOffset + b] = 0x00; // pad with zero
|
||||
}
|
||||
}
|
||||
newHopCount++;
|
||||
}
|
||||
}
|
||||
|
||||
_hopCount = newHopCount;
|
||||
memcpy(_pathBuf, newBuf, sizeof(newBuf));
|
||||
}
|
||||
|
||||
_bytesPerHop = newBytesPerHop;
|
||||
_pathLen = encodePath();
|
||||
}
|
||||
|
||||
void addHopFromContact(uint16_t contactTableIdx) {
|
||||
if (_hopCount >= 8) return;
|
||||
ContactInfo c;
|
||||
if (!the_mesh.getContactByIdx(contactTableIdx, c)) return;
|
||||
|
||||
int offset = _hopCount * _bytesPerHop;
|
||||
if (offset + _bytesPerHop > MAX_PATH_SIZE) return;
|
||||
|
||||
for (int b = 0; b < _bytesPerHop; b++) {
|
||||
_pathBuf[offset + b] = c.id.pub_key[b];
|
||||
}
|
||||
_hopCount++;
|
||||
_pathLen = encodePath();
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
void savePath() {
|
||||
if (_contactIdx < 0) return;
|
||||
|
||||
if (_directLocked) {
|
||||
// Set as direct (0 hops) with lock — prevents flood routing
|
||||
the_mesh.setCustomPath(_contactIdx, _pathBuf, 0, true);
|
||||
Serial.printf("PathEditor: set DIRECT path for contact %d (%s)\n",
|
||||
_contactIdx, _contactName);
|
||||
} else if (_hopCount > 0) {
|
||||
// Set custom path with lock
|
||||
the_mesh.setCustomPath(_contactIdx, _pathBuf, encodePath(), true);
|
||||
Serial.printf("PathEditor: saved %d-hop %dB/hop path for contact %d (%s)\n",
|
||||
_hopCount, _bytesPerHop, _contactIdx, _contactName);
|
||||
} else {
|
||||
// Clear custom path — revert to auto-discovery
|
||||
the_mesh.clearCustomPath(_contactIdx);
|
||||
Serial.printf("PathEditor: cleared custom path for contact %d (%s)\n",
|
||||
_contactIdx, _contactName);
|
||||
}
|
||||
|
||||
// Trigger contact save to SD
|
||||
the_mesh.saveContacts();
|
||||
_dirty = false;
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "../MyMesh.h"
|
||||
#include "NotesScreen.h"
|
||||
#include "RepeaterAdminScreen.h"
|
||||
#include "PathEditorScreen.h"
|
||||
#include "DiscoveryScreen.h"
|
||||
#include "LastHeardScreen.h"
|
||||
#ifdef MECK_WEB_READER
|
||||
@@ -1291,6 +1292,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
notes_screen = new NotesScreen(this, node_prefs);
|
||||
settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs);
|
||||
repeater_admin = nullptr; // Lazy-initialized on first use to preserve heap for audio
|
||||
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);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
|
||||
@@ -2776,6 +2778,23 @@ void UITask::gotoRepeaterAdminDirect(int contactIdx) {
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::gotoPathEditor(int contactIdx) {
|
||||
// Lazy-initialize on first use
|
||||
if (path_editor == nullptr) {
|
||||
path_editor = new PathEditorScreen(this, &rtc_clock);
|
||||
}
|
||||
|
||||
PathEditorScreen* editor = (PathEditorScreen*)path_editor;
|
||||
editor->openForContact(contactIdx);
|
||||
setCurrScreen(path_editor);
|
||||
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoDiscoveryScreen() {
|
||||
((DiscoveryScreen*)discovery_screen)->resetScroll();
|
||||
setCurrScreen(discovery_screen);
|
||||
|
||||
@@ -94,6 +94,7 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* sms_screen; // SMS messaging screen (4G variant only)
|
||||
#endif
|
||||
UIScreen* repeater_admin; // Repeater admin screen
|
||||
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
|
||||
#ifdef MECK_WEB_READER
|
||||
@@ -193,6 +194,7 @@ public:
|
||||
#endif
|
||||
void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin
|
||||
void gotoRepeaterAdminDirect(int contactIdx); // Auto-login admin (L key from conversation)
|
||||
void gotoPathEditor(int contactIdx); // Navigate to custom path editor
|
||||
void gotoDiscoveryScreen(); // Navigate to node discovery scan
|
||||
void gotoLastHeardScreen(); // Navigate to last heard passive list
|
||||
#if HAS_GPS
|
||||
@@ -245,6 +247,7 @@ public:
|
||||
bool isOnVoiceScreen() const { return curr == voice_screen; }
|
||||
#endif
|
||||
bool isOnRepeaterAdmin() const { return curr == repeater_admin; }
|
||||
bool isOnPathEditor() const { return curr == path_editor; }
|
||||
bool isOnDiscoveryScreen() const { return curr == discovery_screen; }
|
||||
bool isOnLastHeardScreen() const { return curr == last_heard_screen; }
|
||||
bool isOnMapScreen() const { return curr == map_screen; }
|
||||
@@ -319,6 +322,7 @@ public:
|
||||
void setVoiceScreen(UIScreen* s) { voice_screen = s; }
|
||||
#endif
|
||||
UIScreen* getRepeaterAdminScreen() const { return repeater_admin; }
|
||||
UIScreen* getPathEditorScreen() const { return path_editor; }
|
||||
UIScreen* getDiscoveryScreen() const { return discovery_screen; }
|
||||
UIScreen* getLastHeardScreen() const { return last_heard_screen; }
|
||||
UIScreen* getMapScreen() const { return map_screen; }
|
||||
|
||||
Reference in New Issue
Block a user