mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-09 18:22:45 +02:00
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:
@@ -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) {}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user