multi-byte path implementation to bring Meck up to speed with Meshcore v1.14; fix regression of ui display in last msg rcd repeater hop count view and also update it for 2 byte nodes

This commit is contained in:
pelgraine
2026-03-07 05:02:22 +11:00
parent 580484e0ad
commit b27acb3252
21 changed files with 324 additions and 136 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ public:
void disableSerial() { _serial->disable(); }
virtual void msgRead(int msgcount) = 0;
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount,
const uint8_t* path = nullptr) = 0;
const uint8_t* path = nullptr, int8_t snr = 0) = 0;
virtual void notify(UIEventType t = UIEventType::none) = 0;
virtual void loop() = 0;
virtual void showAlert(const char* text, int duration_millis) {}
+12
View File
@@ -242,6 +242,16 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
if (_prefs.kb_flash_notify > 1) _prefs.kb_flash_notify = 0;
if (_prefs.ringtone_enabled > 1) _prefs.ringtone_enabled = 0;
// v1.14+ fields — may not exist in older prefs files
if (file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)) != sizeof(_prefs.path_hash_mode)) {
_prefs.path_hash_mode = 0; // default: legacy 1-byte
}
if (file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)) != sizeof(_prefs.autoadd_max_hops)) {
_prefs.autoadd_max_hops = 0; // default: no limit
}
if (_prefs.path_hash_mode > 2) _prefs.path_hash_mode = 0;
if (_prefs.autoadd_max_hops > 64) _prefs.autoadd_max_hops = 0;
file.close();
}
}
@@ -279,6 +289,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.utc_offset_hours, sizeof(_prefs.utc_offset_hours)); // 88
file.write((uint8_t *)&_prefs.kb_flash_notify, sizeof(_prefs.kb_flash_notify)); // 89
file.write((uint8_t *)&_prefs.ringtone_enabled, sizeof(_prefs.ringtone_enabled)); // 90
file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 91
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 92
file.close();
}
+86 -33
View File
@@ -65,6 +65,7 @@
#define CMD_SEND_ANON_REQ 57
#define CMD_SET_AUTOADD_CONFIG 58
#define CMD_GET_AUTOADD_CONFIG 59
#define CMD_SET_PATH_HASH_MODE 61
// Stats sub-types for CMD_GET_STATS
#define STATS_TYPE_CORE 0
@@ -268,6 +269,20 @@ uint8_t MyMesh::getExtraAckTransmitCount() const {
return _prefs.multi_acks;
}
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (uint32_t)(_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f);
return getRNG()->nextInt(0, 5*t + 1);
}
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (uint32_t)(_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f);
return getRNG()->nextInt(0, 5*t + 1);
}
uint8_t MyMesh::getAutoAddMaxHops() const {
return _prefs.autoadd_max_hops;
}
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
if (_serial->isConnected() && len + 3 <= MAX_FRAME_SIZE) {
int i = 0;
@@ -345,7 +360,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
#endif
// add inbound-path to mem cache
if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid
if (path && mesh::Packet::isValidPathLen(path_len)) { // check path is valid
AdvertPath* p = advert_paths;
uint32_t oldest = 0xFFFFFFFF;
for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { // check if already in table, otherwise evict oldest
@@ -362,8 +377,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix));
strcpy(p->name, contact.name);
p->recv_timestamp = getRTCClock()->getCurrentTime();
p->path_len = path_len;
memcpy(p->path, path, p->path_len);
p->path_len = mesh::Packet::copyPath(p->path, path, path_len);
}
// Buffer for on-device discovery UI
@@ -475,7 +489,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
if (should_display && _ui) {
const uint8_t* msg_path = (pkt->isRouteFlood() && pkt->path_len > 0) ? pkt->path : nullptr;
_ui->newMsg(path_len, from.name, text, offline_queue_len, msg_path);
_ui->newMsg(path_len, from.name, text, offline_queue_len, msg_path, pkt->_snr);
if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::contactMessage); //buzz if enabled
}
#endif
@@ -516,14 +530,16 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
}
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
Serial.printf("[sendFloodScoped] to '%s', delay=%lu, hash_mode=%d, bph=%d\n",
recipient.name, delay_millis, _prefs.path_hash_mode, _prefs.path_hash_mode + 1);
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
if (send_scope.isNull()) {
sendFlood(pkt, delay_millis);
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis);
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
}
}
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
@@ -540,12 +556,12 @@ void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pk
// TODO: have per-channel send_scope
if (send_scope.isNull()) {
sendFlood(pkt, delay_millis);
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis);
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
}
}
@@ -618,7 +634,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
}
if (_ui) {
const uint8_t* msg_path = (pkt->isRouteFlood() && pkt->path_len > 0) ? pkt->path : nullptr;
_ui->newMsg(path_len, channel_name, text, offline_queue_len, msg_path);
_ui->newMsg(path_len, channel_name, text, offline_queue_len, msg_path, pkt->_snr);
if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::channelMessage); //buzz if enabled
}
#endif
@@ -696,22 +712,33 @@ bool MyMesh::uiSendDirectMessage(uint32_t contact_idx, const char* text) {
bool MyMesh::uiLoginToRepeater(uint32_t contact_idx, const char* password, uint32_t& est_timeout_ms) {
ContactInfo contact;
if (!getContactByIdx(contact_idx, contact)) return false;
if (!getContactByIdx(contact_idx, contact)) {
Serial.println("[uiLogin] getContactByIdx FAILED");
return false;
}
ContactInfo* recipient = lookupContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
if (!recipient) return false;
if (!recipient) {
Serial.println("[uiLogin] lookupContactByPubKey FAILED");
return false;
}
// Force flood routing for login — a mobile repeater's direct path may be stale.
// The companion protocol does the same for telemetry requests.
int8_t save_path_len = recipient->out_path_len;
recipient->out_path_len = -1;
uint8_t save_path_len = recipient->out_path_len;
recipient->out_path_len = OUT_PATH_UNKNOWN;
Serial.printf("[uiLogin] Sending login to '%s' (idx=%d, path was 0x%02X, now 0x%02X, hash_mode=%d)\n",
recipient->name, contact_idx, save_path_len, recipient->out_path_len, _prefs.path_hash_mode);
int result = sendLogin(*recipient, password, est_timeout_ms);
recipient->out_path_len = save_path_len; // restore
Serial.printf("[uiLogin] sendLogin result=%d est_timeout=%ums\n", result, est_timeout_ms);
if (result == MSG_SEND_FAILED) {
MESH_DEBUG_PRINTLN("UI: Admin login send failed to %s", recipient->name);
Serial.println("[uiLogin] FAILED - MSG_SEND_FAILED");
est_timeout_ms = 0;
return false;
}
@@ -720,8 +747,8 @@ bool MyMesh::uiLoginToRepeater(uint32_t contact_idx, const char* password, uint3
memcpy(&pending_login, recipient->id.pub_key, 4);
_admin_contact_idx = contact_idx;
MESH_DEBUG_PRINTLN("UI: Admin login sent to %s (flood, was path_len=%d), timeout=%dms",
recipient->name, (int)save_path_len, est_timeout_ms);
Serial.printf("[uiLogin] SUCCESS - login sent to %s (flood), timeout=%dms\n",
recipient->name, est_timeout_ms);
return true;
}
@@ -819,6 +846,9 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
uint32_t tag;
memcpy(&tag, data, 4);
Serial.printf("[onContactResponse] from '%s', tag=0x%08X, len=%d, pending_login=0x%08X\n",
contact.name, tag, len, pending_login);
if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response
// yes, is response to pending sendLogin()
pending_login = 0;
@@ -918,7 +948,7 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
if (tag == pending_discovery) { // check for matching response tag)
pending_discovery = 0;
if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) {
if (!mesh::Packet::isValidPathLen(in_path_len) || !mesh::Packet::isValidPathLen(out_path_len)) {
MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
} else {
int i = 0;
@@ -927,11 +957,9 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
memcpy(&out_frame[i], contact.id.pub_key, 6);
i += 6; // pub_key_prefix
out_frame[i++] = out_path_len;
memcpy(&out_frame[i], out_path, out_path_len);
i += out_path_len;
i += mesh::Packet::writePath(&out_frame[i], out_path, out_path_len);
out_frame[i++] = in_path_len;
memcpy(&out_frame[i], in_path, in_path_len);
i += in_path_len;
i += mesh::Packet::writePath(&out_frame[i], in_path, in_path_len);
// NOTE: telemetry data in 'extra' is discarded at present
_serial->writeFrame(out_frame, i);
@@ -1073,9 +1101,10 @@ uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const {
return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis);
}
uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const {
uint8_t hop_count = path_len & 63; // extract hops, ignore mode bits
return SEND_TIMEOUT_BASE_MILLIS +
((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) *
(path_len + 1));
(hop_count + 1));
}
void MyMesh::onSendTimeout() {}
@@ -1402,7 +1431,8 @@ void MyMesh::handleCmdFrame(size_t len) {
}
if (pkt) {
if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop)
sendFlood(pkt);
unsigned long delay_millis = 0;
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
sendZeroHop(pkt);
}
@@ -1414,7 +1444,7 @@ void MyMesh::handleCmdFrame(size_t len) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
recipient->out_path_len = -1;
recipient->out_path_len = OUT_PATH_UNKNOWN;
// recipient->lastmod = ?? shouldn't be needed, app already has this version of contact
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
writeOKFrame();
@@ -1603,6 +1633,14 @@ void MyMesh::handleCmdFrame(size_t len) {
}
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && cmd_frame[1] == 0 && len >= 3) {
if (cmd_frame[2] >= 3) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
_prefs.path_hash_mode = cmd_frame[2];
savePrefs();
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
saveContacts();
@@ -1650,10 +1688,10 @@ void MyMesh::handleCmdFrame(size_t len) {
#endif
} else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) {
int i = 1;
int8_t path_len = cmd_frame[i++];
if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload
uint8_t path_len = cmd_frame[i++];
if (path_len != OUT_PATH_UNKNOWN && i + mesh::Packet::getPathByteLenFor(path_len) + 4 <= len) { // minimum 4 byte payload
uint8_t *path = &cmd_frame[i];
i += path_len;
i += mesh::Packet::getPathByteLenFor(path_len);
auto pkt = createRawData(&cmd_frame[i], len - i);
if (pkt) {
sendDirect(pkt, path, path_len);
@@ -1740,7 +1778,7 @@ void MyMesh::handleCmdFrame(size_t len) {
memset(&req_data[2], 0, 3); // reserved
getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique
auto save = recipient->out_path_len; // temporarily force sendRequest() to flood
recipient->out_path_len = -1;
recipient->out_path_len = OUT_PATH_UNKNOWN;
int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
recipient->out_path_len = save;
if (result == MSG_SEND_FAILED) {
@@ -1983,11 +2021,12 @@ void MyMesh::handleCmdFrame(size_t len) {
}
}
if (found) {
out_frame[0] = RESP_CODE_ADVERT_PATH;
memcpy(&out_frame[1], &found->recv_timestamp, 4);
out_frame[5] = found->path_len;
memcpy(&out_frame[6], found->path, found->path_len);
_serial->writeFrame(out_frame, 6 + found->path_len);
int i = 0;
out_frame[i++] = RESP_CODE_ADVERT_PATH;
memcpy(&out_frame[i], &found->recv_timestamp, 4); i += 4;
out_frame[i++] = found->path_len;
i += mesh::Packet::writePath(&out_frame[i], found->path, found->path_len);
_serial->writeFrame(out_frame, i);
} else {
writeErrFrame(ERR_CODE_NOT_FOUND);
}
@@ -2128,6 +2167,8 @@ void MyMesh::checkCLIRescueCmd() {
Serial.printf(" > %d\n", _prefs.utc_offset_hours);
} else if (strcmp(key, "notify") == 0) {
Serial.printf(" > %s\n", _prefs.kb_flash_notify ? "on" : "off");
} else if (strcmp(key, "path.hash.mode") == 0) {
Serial.printf(" > %d (%d-byte path hashes)\n", _prefs.path_hash_mode, _prefs.path_hash_mode + 1);
} else if (strcmp(key, "gps") == 0) {
Serial.printf(" > %s (interval: %ds)\n",
_prefs.gps_enabled ? "on" : "off", _prefs.gps_interval);
@@ -2180,6 +2221,7 @@ void MyMesh::checkCLIRescueCmd() {
Serial.printf(" tx: %d\n", _prefs.tx_power_dbm);
Serial.printf(" utc: %d\n", _prefs.utc_offset_hours);
Serial.printf(" notify: %s\n", _prefs.kb_flash_notify ? "on" : "off");
Serial.printf(" path.hash: %d (%d-byte)\n", _prefs.path_hash_mode, _prefs.path_hash_mode + 1);
Serial.printf(" gps: %s (interval: %ds)\n",
_prefs.gps_enabled ? "on" : "off", _prefs.gps_interval);
Serial.printf(" pin: %06d\n", _prefs.ble_pin);
@@ -2326,6 +2368,16 @@ void MyMesh::checkCLIRescueCmd() {
savePrefs();
Serial.printf(" > notify = %s\n", _prefs.kb_flash_notify ? "on" : "off");
} else if (memcmp(config, "path.hash.mode ", 15) == 0) {
int mode = atoi(&config[15]);
if (mode >= 0 && mode <= 2) {
_prefs.path_hash_mode = (uint8_t)mode;
savePrefs();
Serial.printf(" > path.hash.mode = %d (%d-byte path hashes)\n", mode, mode + 1);
} else {
Serial.println(" Error: mode must be 0, 1, or 2 (1-byte, 2-byte, 3-byte)");
}
} else if (memcmp(config, "pin ", 4) == 0) {
_prefs.ble_pin = atoi(&config[4]);
savePrefs();
@@ -2520,6 +2572,7 @@ void MyMesh::checkCLIRescueCmd() {
Serial.println("");
Serial.println(" Settings keys:");
Serial.println(" name, freq, bw, sf, cr, tx, utc, notify, pin");
Serial.println(" path.hash.mode Path hash size (0=1B, 1=2B, 2=3B)");
Serial.println("");
Serial.println(" Compound commands:");
Serial.println(" get all Dump all settings");
+4 -1
View File
@@ -5,7 +5,7 @@
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 8
#define FIRMWARE_VER_CODE 10
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "7 March 2026"
@@ -137,7 +137,10 @@ protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override;
int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
uint8_t getExtraAckTransmitCount() const override;
uint8_t getAutoAddMaxHops() const override;
bool filterRecvFloodPacket(mesh::Packet* packet) override;
void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
+2
View File
@@ -31,4 +31,6 @@ struct NodePrefs { // persisted to file
int8_t utc_offset_hours; // UTC offset in hours (-12 to +14), default 0
uint8_t kb_flash_notify; // Keyboard backlight flash on new message (0=off, 1=on)
uint8_t ringtone_enabled; // Ringtone on incoming call (0=off, 1=on) — 4G only
uint8_t path_hash_mode; // 0=1-byte (legacy), 1=2-byte, 2=3-byte path hashes
uint8_t autoadd_max_hops; // 0=no limit, N=up to N-1 hops (max 64)
};
+58 -25
View File
@@ -4,6 +4,7 @@
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ChannelDetails.h>
#include <MeshCore.h>
#include <Packet.h>
#include "EmojiSprites.h"
// SD card message persistence
@@ -24,7 +25,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 increased to 20
#define MSG_FILE_VERSION 3 // v3: MSG_PATH_MAX=20, reserved→snr field
#define MSG_FILE_PATH "/meshcore/messages.bin"
struct __attribute__((packed)) MsgFileHeader {
@@ -41,7 +42,7 @@ struct __attribute__((packed)) MsgFileRecord {
uint8_t path_len;
uint8_t channel_idx;
uint8_t valid;
uint8_t reserved;
int8_t snr; // Receive SNR × 4 (was reserved; 0 = unknown)
uint8_t path[MSG_PATH_MAX]; // Repeater hop hashes (first byte of pub key)
char text[CHANNEL_MSG_TEXT_LEN];
// 188 bytes total
@@ -57,6 +58,7 @@ public:
uint32_t timestamp;
uint8_t path_len;
uint8_t channel_idx; // Which channel this message belongs to
int8_t snr; // Receive SNR × 4 (0 if locally sent or unknown)
uint8_t path[MSG_PATH_MAX]; // Repeater hop hashes
char text[CHANNEL_MSG_TEXT_LEN];
bool valid;
@@ -105,7 +107,7 @@ public:
// Add a new message to the history
void addMessage(uint8_t channel_idx, uint8_t path_len, const char* sender, const char* text,
const uint8_t* path_bytes = nullptr) {
const uint8_t* path_bytes = nullptr, int8_t snr = 0) {
// Move to next slot in circular buffer
_newestIdx = (_newestIdx + 1) % CHANNEL_MSG_HISTORY_SIZE;
@@ -113,12 +115,14 @@ public:
msg->timestamp = _rtc->getCurrentTime();
msg->path_len = path_len;
msg->channel_idx = channel_idx;
msg->snr = snr;
msg->valid = true;
// Store path hop hashes
memset(msg->path, 0, MSG_PATH_MAX);
if (path_bytes && path_len > 0 && path_len != 0xFF) {
int n = path_len < MSG_PATH_MAX ? path_len : MSG_PATH_MAX;
int n = mesh::Packet::getPathByteLenFor(path_len);
if (n > MSG_PATH_MAX) n = MSG_PATH_MAX;
memcpy(msg->path, path_bytes, n);
}
@@ -289,11 +293,15 @@ public:
if (!msg || msg->path_len == 0 || msg->path_len == 0xFF) return 0;
int pos = 0;
int plen = msg->path_len < MSG_PATH_MAX ? msg->path_len : MSG_PATH_MAX;
uint8_t hopCount = msg->path_len & 63;
uint8_t bytesPerHop = (msg->path_len >> 6) + 1;
for (int h = 0; h < plen && pos < bufLen - 1; h++) {
for (int h = 0; h < hopCount && pos < bufLen - 1; h++) {
if (h > 0) pos += snprintf(buf + pos, bufLen - pos, ", ");
pos += snprintf(buf + pos, bufLen - pos, "%02x", msg->path[h]);
int offset = h * bytesPerHop;
for (int b = 0; b < bytesPerHop && pos < bufLen - 1; b++) {
pos += snprintf(buf + pos, bufLen - pos, "%02x", msg->path[offset + b]);
}
}
return pos;
@@ -336,7 +344,7 @@ public:
rec.path_len = _messages[i].path_len;
rec.channel_idx = _messages[i].channel_idx;
rec.valid = _messages[i].valid ? 1 : 0;
rec.reserved = 0;
rec.snr = _messages[i].snr;
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));
@@ -403,6 +411,7 @@ public:
_messages[i].path_len = rec.path_len;
_messages[i].channel_idx = rec.channel_idx;
_messages[i].valid = (rec.valid != 0);
_messages[i].snr = rec.snr;
memcpy(_messages[i].path, rec.path, MSG_PATH_MAX);
memcpy(_messages[i].text, rec.text, CHANNEL_MSG_TEXT_LEN);
if (_messages[i].valid) loaded++;
@@ -491,6 +500,8 @@ public:
// Route type
display.setCursor(0, y);
uint8_t plen = msg->path_len;
uint8_t hopCount = plen & 63; // extract hop count from encoded path_len
uint8_t bytesPerHop = (plen >> 6) + 1; // 1, 2, or 3 bytes per hop
if (plen == 0xFF) {
display.setColor(DisplayDriver::LIGHT);
display.print("Route: Direct");
@@ -499,14 +510,26 @@ public:
display.print("Route: Local/Sent");
} else {
display.setColor(DisplayDriver::GREEN);
sprintf(tmp, "Route: %d hop%s", plen, plen == 1 ? "" : "s");
sprintf(tmp, "Route: %d hop%s (%dB)", hopCount, hopCount == 1 ? "" : "s", bytesPerHop);
display.print(tmp);
}
y += lineH + 2;
y += lineH;
// SNR (if available — value is SNR×4)
if (msg->snr != 0) {
display.setCursor(0, y);
display.setColor(DisplayDriver::YELLOW);
int snr_whole = msg->snr / 4;
int snr_frac = ((abs(msg->snr) % 4) * 10) / 4;
sprintf(tmp, "SNR: %d.%ddB", snr_whole, snr_frac);
display.print(tmp);
y += lineH;
}
y += 2;
// Show each hop resolved against contacts (scrollable)
if (plen > 0 && plen != 0xFF) {
int displayHops = plen < MSG_PATH_MAX ? plen : MSG_PATH_MAX;
if (hopCount > 0 && plen != 0xFF) {
int displayHops = hopCount;
int footerReserve = 26; // footer + divider
int scrollBarW = 4;
int maxY = display.height() - footerReserve;
@@ -532,28 +555,37 @@ public:
if (endHop > displayHops) endHop = displayHops;
for (int h = startHop; h < endHop && y + lineH <= maxY; h++) {
uint8_t hopHash = msg->path[h];
int hopOffset = h * bytesPerHop; // byte offset into path[]
display.setCursor(0, y);
display.setColor(DisplayDriver::LIGHT);
sprintf(tmp, " %d: ", h + 1);
display.print(tmp);
// Always show hex prefix first
// Show hex prefix (1, 2, or 3 bytes)
display.setColor(DisplayDriver::LIGHT);
sprintf(tmp, "%02X ", hopHash);
if (bytesPerHop == 1) {
sprintf(tmp, "%02X ", msg->path[hopOffset]);
} else if (bytesPerHop == 2) {
sprintf(tmp, "%02X%02X ", msg->path[hopOffset], msg->path[hopOffset + 1]);
} else {
sprintf(tmp, "%02X%02X%02X ", msg->path[hopOffset], msg->path[hopOffset + 1], msg->path[hopOffset + 2]);
}
display.print(tmp);
// Try to resolve name: prefer repeaters, then any contact
bool resolved = false;
int numContacts = the_mesh.getNumContacts();
ContactInfo contact;
char filteredName[32];
// First pass: repeaters only
for (uint32_t ci = 0; ci < numContacts && !resolved; ci++) {
if (the_mesh.getContactByIdx(ci, contact)) {
if (contact.id.pub_key[0] == hopHash && contact.type == ADV_TYPE_REPEATER) {
if (memcmp(contact.id.pub_key, &msg->path[hopOffset], bytesPerHop) == 0
&& contact.type == ADV_TYPE_REPEATER) {
display.setColor(DisplayDriver::GREEN);
display.print(contact.name);
display.translateUTF8ToBlocks(filteredName, contact.name, sizeof(filteredName));
display.print(filteredName);
resolved = true;
}
}
@@ -562,9 +594,10 @@ public:
if (!resolved) {
for (uint32_t ci = 0; ci < numContacts; ci++) {
if (the_mesh.getContactByIdx(ci, contact)) {
if (contact.id.pub_key[0] == hopHash) {
if (memcmp(contact.id.pub_key, &msg->path[hopOffset], bytesPerHop) == 0) {
display.setColor(DisplayDriver::YELLOW);
display.print(contact.name);
display.translateUTF8ToBlocks(filteredName, contact.name, sizeof(filteredName));
display.print(filteredName);
resolved = true;
break;
}
@@ -608,7 +641,7 @@ public:
display.setColor(DisplayDriver::YELLOW);
display.print("Q:Back");
// Show scroll hint if path is scrollable
if (msg && msg->path_len > _pathHopsVisible && msg->path_len != 0xFF) {
if (msg && (msg->path_len & 63) > _pathHopsVisible && msg->path_len != 0xFF) {
const char* scrollHint = "W/S:Scrl";
int scrollW = display.getTextWidth(scrollHint);
display.setCursor((display.width() - scrollW) / 2, footerY);
@@ -723,13 +756,13 @@ public:
}
} else {
if (age < 60) {
sprintf(tmp, "(%d) %ds ", msg->path_len == 0xFF ? 0 : msg->path_len, age);
sprintf(tmp, "(%d) %ds ", msg->path_len == 0xFF ? 0 : (msg->path_len & 63), age);
} else if (age < 3600) {
sprintf(tmp, "(%d) %dm ", msg->path_len == 0xFF ? 0 : msg->path_len, age / 60);
sprintf(tmp, "(%d) %dm ", msg->path_len == 0xFF ? 0 : (msg->path_len & 63), age / 60);
} else if (age < 86400) {
sprintf(tmp, "(%d) %dh ", msg->path_len == 0xFF ? 0 : msg->path_len, age / 3600);
sprintf(tmp, "(%d) %dh ", msg->path_len == 0xFF ? 0 : (msg->path_len & 63), age / 3600);
} else {
sprintf(tmp, "(%d) %dd ", msg->path_len == 0xFF ? 0 : msg->path_len, age / 86400);
sprintf(tmp, "(%d) %dd ", msg->path_len == 0xFF ? 0 : (msg->path_len & 63), age / 86400);
}
}
display.print(tmp);
@@ -952,7 +985,7 @@ public:
if (c == 's' || c == 'S' || c == 0xF1) {
ChannelMessage* msg = getNewestReceivedMsg();
if (msg && msg->path_len > 0 && msg->path_len != 0xFF) {
int totalHops = msg->path_len < MSG_PATH_MAX ? msg->path_len : MSG_PATH_MAX;
int totalHops = msg->path_len & 63;
if (_pathScrollPos < totalHops - _pathHopsVisible) {
_pathScrollPos++;
}
@@ -126,9 +126,9 @@ public:
} else {
// Pre-seeded from cache — show hop count
if (node.already_in_contacts) {
snprintf(rightStr, sizeof(rightStr), "%dh [+]", node.path_len);
snprintf(rightStr, sizeof(rightStr), "%dh [+]", node.path_len & 63);
} else {
snprintf(rightStr, sizeof(rightStr), "%dh", node.path_len);
snprintf(rightStr, sizeof(rightStr), "%dh", node.path_len & 63);
}
}
int rightWidth = display.getTextWidth(rightStr) + 2;
@@ -57,6 +57,7 @@ enum SettingsRowType : uint8_t {
ROW_TX_POWER, // TX power (1-20 dBm)
ROW_UTC_OFFSET, // UTC offset (-12 to +14)
ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle
ROW_PATH_HASH_SIZE, // Path hash size (1, 2, or 3 bytes per hop)
#ifdef MECK_WIFI_COMPANION
ROW_WIFI_SETUP, // WiFi SSID/password configuration
ROW_WIFI_TOGGLE, // WiFi radio on/off toggle
@@ -234,6 +235,7 @@ private:
addRow(ROW_TX_POWER);
addRow(ROW_UTC_OFFSET);
addRow(ROW_MSG_NOTIFY);
addRow(ROW_PATH_HASH_SIZE);
#ifdef MECK_WIFI_COMPANION
addRow(ROW_WIFI_SETUP);
addRow(ROW_WIFI_TOGGLE);
@@ -681,6 +683,15 @@ public:
display.print(tmp);
break;
case ROW_PATH_HASH_SIZE:
if (editing && _editMode == EDIT_NUMBER) {
snprintf(tmp, sizeof(tmp), "Path Hash Size: %d-byte <W/S>", _editInt);
} else {
snprintf(tmp, sizeof(tmp), "Path Hash Size: %d-byte", _prefs->path_hash_mode + 1);
}
display.print(tmp);
break;
#ifdef MECK_WIFI_COMPANION
case ROW_WIFI_SETUP:
if (WiFi.status() == WL_CONNECTED) {
@@ -1306,6 +1317,7 @@ public:
case ROW_CR: if (_editInt < 8) _editInt++; break;
case ROW_TX_POWER: if (_editInt < MAX_LORA_TX_POWER) _editInt++; break;
case ROW_UTC_OFFSET: if (_editInt < 14) _editInt++; break;
case ROW_PATH_HASH_SIZE: if (_editInt < 3) _editInt++; break;
default: break;
}
return true;
@@ -1322,6 +1334,7 @@ public:
case ROW_CR: if (_editInt > 5) _editInt--; break;
case ROW_TX_POWER: if (_editInt > 1) _editInt--; break;
case ROW_UTC_OFFSET: if (_editInt > -12) _editInt--; break;
case ROW_PATH_HASH_SIZE: if (_editInt > 1) _editInt--; break;
default: break;
}
return true;
@@ -1349,6 +1362,10 @@ public:
_prefs->utc_offset_hours = (int8_t)constrain(_editInt, -12, 14);
the_mesh.savePrefs();
break;
case ROW_PATH_HASH_SIZE:
_prefs->path_hash_mode = (uint8_t)constrain(_editInt - 1, 0, 2); // display 1-3, store 0-2
the_mesh.savePrefs();
break;
default: break;
}
_editMode = EDIT_NONE;
@@ -1419,6 +1436,9 @@ public:
Serial.printf("Settings: Msg flash notify = %s\n",
_prefs->kb_flash_notify ? "ON" : "OFF");
break;
case ROW_PATH_HASH_SIZE:
startEditInt(_prefs->path_hash_mode + 1); // display as 1-3
break;
#ifdef MECK_WIFI_COMPANION
case ROW_WIFI_SETUP: {
// Launch WiFi scan → select → password → connect flow
+3 -3
View File
@@ -1000,7 +1000,7 @@ void UITask::msgRead(int msgcount) {
}
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount,
const uint8_t* path) {
const uint8_t* path, int8_t snr) {
_msgcount = msgcount;
// Add to preview screen (for notifications on non-keyboard devices)
@@ -1018,8 +1018,8 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
}
}
// Add to channel history screen with channel index and path data
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text, path);
// Add to channel history screen with channel index, path data, and SNR
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text, path, snr);
// If user is currently viewing this channel, mark it as read immediately
// (they can see the message arrive in real-time)
+1 -1
View File
@@ -203,7 +203,7 @@ public:
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount,
const uint8_t* path = nullptr) override;
const uint8_t* path = nullptr, int8_t snr = 0) override;
void notify(UIEventType t = UIEventType::none) override;
void loop() override;