From b27acb3252e886dd08b8f52664dc2f77adeac206 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sat, 7 Mar 2026 05:02:22 +1100 Subject: [PATCH] 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 --- examples/companion_radio/AbstractUITask.h | 2 +- examples/companion_radio/DataStore.cpp | 12 ++ examples/companion_radio/MyMesh.cpp | 119 +++++++++++++----- examples/companion_radio/MyMesh.h | 5 +- examples/companion_radio/NodePrefs.h | 2 + .../companion_radio/ui-new/ChannelScreen.h | 83 ++++++++---- .../companion_radio/ui-new/Discoveryscreen.h | 4 +- .../companion_radio/ui-new/Settingsscreen.h | 20 +++ examples/companion_radio/ui-new/UITask.cpp | 6 +- examples/companion_radio/ui-new/UITask.h | 2 +- src/Dispatcher.cpp | 16 +-- src/Identity.h | 7 +- src/Mesh.cpp | 74 ++++++----- src/Mesh.h | 8 +- src/Packet.cpp | 10 +- src/Packet.h | 40 +++++- src/helpers/BaseChatMesh.cpp | 31 ++--- src/helpers/BaseChatMesh.h | 1 + src/helpers/ClientACL.cpp | 4 +- src/helpers/ClientACL.h | 8 +- src/helpers/ContactInfo.h | 6 +- 21 files changed, 324 insertions(+), 136 deletions(-) diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index ca3edba..95feef4 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -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) {} diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index ea43d7f..9e7d3d4 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -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(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index d709053..16d2d2c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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"); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 033974a..1c0a110 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -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; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index d02af39..469ee59 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -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) }; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index d9dddcf..151fb9c 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -4,6 +4,7 @@ #include #include #include +#include #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++; } diff --git a/examples/companion_radio/ui-new/Discoveryscreen.h b/examples/companion_radio/ui-new/Discoveryscreen.h index c366956..27f6026 100644 --- a/examples/companion_radio/ui-new/Discoveryscreen.h +++ b/examples/companion_radio/ui-new/Discoveryscreen.h @@ -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; diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index dcff7eb..a454c46 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -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 ", _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 diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 38fa27c..bed18b9 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -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) diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index f80c7df..4b86f4c 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -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; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 0a15498..9f816bb 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -68,7 +68,7 @@ void Dispatcher::loop() { next_tx_time = futureMillis(t * getAirtimeBudgetFactor()); _radio->onSendFinished(); - logTx(outbound, 2 + outbound->path_len + outbound->payload_len); + logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); if (outbound->isRouteFlood()) { n_sent_flood++; } else { @@ -80,7 +80,7 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); - logTxFail(outbound, 2 + outbound->path_len + outbound->payload_len); + logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); releasePacket(outbound); // return to pool outbound = NULL; @@ -141,12 +141,13 @@ void Dispatcher::checkRecv() { } pkt->path_len = raw[i++]; - if (pkt->path_len > MAX_PATH_SIZE || i + pkt->path_len > len) { + uint16_t path_byte_len = pkt->getPathByteLen(); + if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len); _mgr->free(pkt); // put back into pool pkt = NULL; } else { - memcpy(pkt->path, &raw[i], pkt->path_len); i += pkt->path_len; + memcpy(pkt->path, &raw[i], path_byte_len); i += path_byte_len; pkt->payload_len = len - i; // payload is remainder if (pkt->payload_len > sizeof(pkt->payload)) { @@ -258,7 +259,8 @@ void Dispatcher::checkSend() { memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2; } raw[len++] = outbound->path_len; - memcpy(&raw[len], outbound->path, outbound->path_len); len += outbound->path_len; + uint16_t out_pbl = outbound->getPathByteLen(); + memcpy(&raw[len], outbound->path, out_pbl); len += out_pbl; if (len + outbound->payload_len > MAX_TRANS_UNIT) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len); @@ -312,8 +314,8 @@ void Dispatcher::releasePacket(Packet* packet) { } void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { - if (packet->path_len > MAX_PATH_SIZE || packet->payload_len > MAX_PACKET_PAYLOAD) { - MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len); + if (packet->getPathByteLen() > MAX_PATH_SIZE || packet->payload_len > MAX_PACKET_PAYLOAD) { + MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d (byte_len=%d), payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->getPathByteLen(), (uint32_t) packet->payload_len); _mgr->free(packet); } else { _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); diff --git a/src/Identity.h b/src/Identity.h index c3ffcd7..47264cb 100644 --- a/src/Identity.h +++ b/src/Identity.h @@ -20,6 +20,10 @@ public: memcpy(dest, pub_key, PATH_HASH_SIZE); // hash is just prefix of pub_key return PATH_HASH_SIZE; } + int copyHashTo(uint8_t* dest, uint8_t len) const { + memcpy(dest, pub_key, len); + return len; + } bool isHashMatch(const uint8_t* hash) const { return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0; } @@ -90,5 +94,4 @@ public: void readFrom(const uint8_t* src, size_t len); }; -} - +} \ No newline at end of file diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 0548c90..9d3bf88 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -15,7 +15,7 @@ bool Mesh::allowPacketForward(const mesh::Packet* packet) { return false; // by default, Transport NOT enabled } uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->getRawLength()) * 52 / 50) / 2; + uint32_t t = (uint32_t)(_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f); return _rng->nextInt(0, 5)*t; } @@ -77,7 +77,9 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { return ACTION_RELEASE; } - if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) { + if (pkt->isRouteDirect() && (pkt->path_len & 63) > 0) { + uint8_t dir_bph = (pkt->path_len >> 6) + 1; // bytes per hop for this packet + // check for 'early received' ACK if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { int i = 0; @@ -88,7 +90,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) { + if (self_id.isHashMatch(pkt->path, dir_bph) && allowPacketForward(pkt)) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { return forwardMultipartDirect(pkt); } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { @@ -158,7 +160,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { int k = 0; uint8_t path_len = data[k++]; - uint8_t* path = &data[k]; k += path_len; + uint8_t* path = &data[k]; k += Packet::getPathByteLenFor(path_len); uint8_t extra_type = data[k++] & 0x0F; // upper 4 bits reserved for future use uint8_t* extra = &data[k]; uint8_t extra_len = len - k; // remainder of packet (may be padded with zeroes!) @@ -293,8 +295,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK Packet tmp; tmp.header = pkt->header; - tmp.path_len = pkt->path_len; - memcpy(tmp.path, pkt->path, pkt->path_len); + tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); tmp.payload_len = pkt->payload_len - 1; memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); @@ -320,28 +321,34 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } void Mesh::removeSelfFromPath(Packet* pkt) { - // remove our hash from 'path' - pkt->path_len -= PATH_HASH_SIZE; -#if 0 - memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len); -#elif PATH_HASH_SIZE == 1 - for (int k = 0; k < pkt->path_len; k++) { // shuffle bytes by 1 - pkt->path[k] = pkt->path[k + 1]; - } -#else - #error "need path remove impl" -#endif + uint8_t bph = (pkt->path_len >> 6) + 1; // bytes per hop + uint8_t hops = pkt->path_len & 63; + if (hops == 0) return; + + uint16_t new_byte_len = (hops - 1) * bph; + // remove first bph bytes (our hash) from path, shift remainder + memmove(pkt->path, &pkt->path[bph], new_byte_len); + // decrement hop count, preserve mode bits + pkt->path_len = (pkt->path_len & 0xC0) | ((hops - 1) & 63); } DispatcherAction Mesh::routeRecvPacket(Packet* packet) { if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit() - && packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) { - // append this node's hash to 'path' - packet->path_len += self_id.copyHashTo(&packet->path[packet->path_len]); + && allowPacketForward(packet)) { + uint8_t bph = (packet->path_len >> 6) + 1; // bytes per hop + uint8_t hops = packet->path_len & 63; + uint16_t byte_len = hops * bph; - uint32_t d = getRetransmitDelay(packet); - // as this propagates outwards, give it lower and lower priority - return ACTION_RETRANSMIT_DELAYED(packet->path_len, d); // give priority to closer sources, than ones further away + if (byte_len + bph <= MAX_PATH_SIZE) { + // append this node's hash (bph bytes of pub_key) to path + memcpy(&packet->path[byte_len], self_id.pub_key, bph); + // increment hop count, preserve mode bits + packet->path_len = (packet->path_len & 0xC0) | ((hops + 1) & 63); + + uint32_t d = getRetransmitDelay(packet); + // as this propagates outwards, give it lower and lower priority + return ACTION_RETRANSMIT_DELAYED(hops + 1, d); // give priority to closer sources, than ones further away + } } return ACTION_RELEASE; } @@ -353,8 +360,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK Packet tmp; tmp.header = pkt->header; - tmp.path_len = pkt->path_len; - memcpy(tmp.path, pkt->path, pkt->path_len); + tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); tmp.payload_len = pkt->payload_len - 1; memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); @@ -376,7 +382,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { delay_millis += getDirectRetransmitDelay(packet) + 300; auto a1 = createMultiAck(crc, extra); if (a1) { - memcpy(a1->path, packet->path, a1->path_len = packet->path_len); + a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len); a1->header &= ~PH_ROUTE_MASK; a1->header |= ROUTE_TYPE_DIRECT; sendPacket(a1, 0, delay_millis); @@ -386,7 +392,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { auto a2 = createAck(crc); if (a2) { - memcpy(a2->path, packet->path, a2->path_len = packet->path_len); + a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); a2->header &= ~PH_ROUTE_MASK; a2->header |= ROUTE_TYPE_DIRECT; sendPacket(a2, 0, delay_millis); @@ -624,7 +630,7 @@ Packet* Mesh::createControlData(const uint8_t* data, size_t len) { return packet; } -void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) { +void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_bytes_per_hop) { if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); return; @@ -632,7 +638,9 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) { packet->header &= ~PH_ROUTE_MASK; packet->header |= ROUTE_TYPE_FLOOD; - packet->path_len = 0; + // encode bytes-per-hop mode in upper 2 bits of path_len, 0 hops initially + uint8_t mode = (path_bytes_per_hop > 1) ? (path_bytes_per_hop - 1) : 0; + packet->path_len = (mode << 6); _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us @@ -647,7 +655,7 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) { sendPacket(packet, pri, delay_millis); } -void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) { +void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_bytes_per_hop) { if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); return; @@ -657,7 +665,9 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD; packet->transport_codes[0] = transport_codes[0]; packet->transport_codes[1] = transport_codes[1]; - packet->path_len = 0; + // encode bytes-per-hop mode in upper 2 bits of path_len, 0 hops initially + uint8_t mode = (path_bytes_per_hop > 1) ? (path_bytes_per_hop - 1) : 0; + packet->path_len = (mode << 6); _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us @@ -685,7 +695,7 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin packet->path_len = 0; pri = 5; // maybe make this configurable } else { - memcpy(packet->path, path, packet->path_len = path_len); + packet->path_len = Packet::copyPath(packet->path, path, path_len); if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { pri = 1; // slightly less priority } else { diff --git a/src/Mesh.h b/src/Mesh.h index 00f7ed0..f56ab22 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -195,14 +195,16 @@ public: /** * \brief send a locally-generated Packet with flood routing + * \param path_bytes_per_hop number of bytes per path hop (1=legacy, 2, or 3) */ - void sendFlood(Packet* packet, uint32_t delay_millis=0); + void sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_bytes_per_hop=1); /** * \brief send a locally-generated Packet with flood routing * \param transport_codes array of 2 codes to attach to packet + * \param path_bytes_per_hop number of bytes per path hop (1=legacy, 2, or 3) */ - void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); + void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_bytes_per_hop=1); /** * \brief send a locally-generated Packet with Direct routing @@ -222,4 +224,4 @@ public: }; -} +} \ No newline at end of file diff --git a/src/Packet.cpp b/src/Packet.cpp index 2d54ca4..6f820c2 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -11,7 +11,7 @@ Packet::Packet() { } int Packet::getRawLength() const { - return 2 + path_len + payload_len + (hasTransportCodes() ? 4 : 0); + return 2 + getPathByteLen() + payload_len + (hasTransportCodes() ? 4 : 0); } void Packet::calculatePacketHash(uint8_t* hash) const { @@ -33,7 +33,8 @@ uint8_t Packet::writeTo(uint8_t dest[]) const { memcpy(&dest[i], &transport_codes[1], 2); i += 2; } dest[i++] = path_len; - memcpy(&dest[i], path, path_len); i += path_len; + uint16_t pbl = getPathByteLen(); + memcpy(&dest[i], path, pbl); i += pbl; memcpy(&dest[i], payload, payload_len); i += payload_len; return i; } @@ -48,8 +49,9 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { transport_codes[0] = transport_codes[1] = 0; } path_len = src[i++]; - if (path_len > sizeof(path)) return false; // bad encoding - memcpy(path, &src[i], path_len); i += path_len; + uint16_t pbl = getPathByteLen(); + if (pbl > sizeof(path)) return false; // bad encoding + memcpy(path, &src[i], pbl); i += pbl; if (i >= len) return false; // bad encoding payload_len = len - i; if (payload_len > sizeof(payload)) return false; // bad encoding diff --git a/src/Packet.h b/src/Packet.h index 42d73f4..2d00dd9 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace mesh { @@ -81,6 +82,43 @@ public: float getSNR() const { return ((float)_snr) / 4.0f; } + /** + * \returns the actual byte length of path data. + * path_len encodes: lower 6 bits = hop count, upper 2 bits = bytes-per-hop mode + * mode 0 = 1 byte/hop (legacy), mode 1 = 2 bytes/hop, mode 2 = 3 bytes/hop + */ + uint16_t getPathByteLen() const { + uint8_t hops = path_len & 63; + uint8_t bph = (path_len >> 6) + 1; + return hops * bph; + } + + /** Static variant for computing byte length from any path_len value */ + static uint16_t getPathByteLenFor(uint8_t path_len) { + return (path_len & 63) * ((path_len >> 6) + 1); + } + + /** Validate that encoded path_len won't exceed buffer */ + static bool isValidPathLen(uint8_t path_len) { + return getPathByteLenFor(path_len) <= MAX_PATH_SIZE; + } + + /** Copy path bytes using encoded path_len; returns path_len unchanged */ + static uint8_t copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { + uint16_t bl = getPathByteLenFor(path_len); + if (bl > MAX_PATH_SIZE) bl = MAX_PATH_SIZE; + memcpy(dest, src, bl); + return path_len; + } + + /** Write path bytes to buffer; returns number of bytes written */ + static uint8_t writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { + uint16_t bl = getPathByteLenFor(path_len); + if (bl > MAX_PATH_SIZE) bl = MAX_PATH_SIZE; + memcpy(dest, src, bl); + return (uint8_t)bl; + } + /** * \returns the encoded/wire format length of this packet */ @@ -101,4 +139,4 @@ public: bool readFrom(const uint8_t src[], uint8_t len); }; -} +} \ No newline at end of file diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index aebfc1b..fb820a2 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -39,7 +39,7 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl } void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) { - if (dest.out_path_len < 0) { + if (dest.out_path_len == OUT_PATH_UNKNOWN) { mesh::Packet* ack = createAck(ack_hash); if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY); } else { @@ -92,7 +92,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot() { void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) { memset(&ci, 0, sizeof(ci)); ci.id = id; - ci.out_path_len = -1; // initially out_path is unknown + ci.out_path_len = OUT_PATH_UNKNOWN; // initially out_path is unknown StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name)); ci.type = parser.getType(); if (parser.hasLatLon()) { @@ -263,7 +263,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len); if (reply) { - if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT + if (from.out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); } else { sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY); @@ -273,7 +273,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender } } else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) { onContactResponse(from, data, len); - if (packet->isRouteFlood() && from.out_path_len >= 0) { + if (packet->isRouteFlood() && from.out_path_len != OUT_PATH_UNKNOWN) { // we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?) handleReturnPathRetry(from, packet->path, packet->path_len); } @@ -295,7 +295,8 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { // NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path. // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?) - memcpy(from.out_path, out_path, from.out_path_len = out_path_len); // store a copy of path, for sendDirect() + from.out_path_len = out_path_len; + mesh::Packet::copyPath(from.out_path, out_path, out_path_len); // store a copy of path, for sendDirect() from.lastmod = getRTCClock()->getCurrentTime(); onContactPathUpdated(from); @@ -317,7 +318,7 @@ void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit - if (packet->isRouteFlood() && from->out_path_len >= 0) { + if (packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) { // we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?) handleReturnPathRetry(*from, packet->path, packet->path_len); } @@ -386,7 +387,7 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); int rc; - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); rc = MSG_SEND_SENT_FLOOD; @@ -412,7 +413,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); int rc; - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); rc = MSG_SEND_SENT_FLOOD; @@ -500,7 +501,7 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -525,7 +526,7 @@ int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data, } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -552,7 +553,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_ } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -579,7 +580,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -683,7 +684,7 @@ void BaseChatMesh::checkConnections() { MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact not found!"); continue; } - if (contact->out_path_len < 0) { + if (contact->out_path_len == OUT_PATH_UNKNOWN) { MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact, no out_path!"); continue; } @@ -710,7 +711,7 @@ void BaseChatMesh::checkConnections() { } void BaseChatMesh::resetPathTo(ContactInfo& recipient) { - recipient.out_path_len = -1; + recipient.out_path_len = OUT_PATH_UNKNOWN; } static ContactInfo* table; // pass via global :-( @@ -875,4 +876,4 @@ void BaseChatMesh::loop() { releasePacket(_pendingLoopback); // undo the obtainNewPacket() _pendingLoopback = NULL; } -} +} \ No newline at end of file diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index ec50630..3f4b44f 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -113,6 +113,7 @@ protected: virtual bool shouldAutoAddContactType(uint8_t type) const { return true; } virtual void onContactsFull() {}; virtual bool shouldOverwriteWhenFull() const { return false; } + virtual uint8_t getAutoAddMaxHops() const { return 0; } // 0 = no limit virtual void onContactOverwrite(const uint8_t* pub_key) {}; virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; virtual ContactInfo* processAck(const uint8_t *data) = 0; diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 55b70ca..061111a 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -114,7 +114,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) { memset(c, 0, sizeof(*c)); c->permissions = init_perms; c->id = id; - c->out_path_len = -1; // initially out_path is unknown + c->out_path_len = OUT_PATH_UNKNOWN; // initially out_path is unknown return c; } @@ -140,4 +140,4 @@ bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8 self_id.calcSharedSecret(c->shared_secret, pubkey); } return true; -} +} \ No newline at end of file diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index dfbc3fc..7757fb7 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -4,6 +4,10 @@ #include #include +#ifndef OUT_PATH_UNKNOWN + #define OUT_PATH_UNKNOWN 0xFF +#endif + #define PERM_ACL_ROLE_MASK 3 // lower 2 bits #define PERM_ACL_GUEST 0 #define PERM_ACL_READ_ONLY 1 @@ -13,7 +17,7 @@ struct ClientInfo { mesh::Identity id; uint8_t permissions; - int8_t out_path_len; + uint8_t out_path_len; // OUT_PATH_UNKNOWN = no known path uint8_t out_path[MAX_PATH_SIZE]; uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) @@ -55,4 +59,4 @@ public: int getNumClients() const { return num_clients; } ClientInfo* getClientByIdx(int idx) { return &clients[idx]; } -}; +}; \ No newline at end of file diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index eff0774..c1df50f 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -3,12 +3,14 @@ #include #include +#define OUT_PATH_UNKNOWN 0xFF // no known path — triggers flood routing + struct ContactInfo { mesh::Identity id; char name[32]; uint8_t type; // on of ADV_TYPE_* uint8_t flags; - int8_t out_path_len; + uint8_t out_path_len; // encoded: bits[7:6]=mode, bits[5:0]=hops. OUT_PATH_UNKNOWN=no path mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated uint8_t out_path[MAX_PATH_SIZE]; uint32_t last_advert_timestamp; // by THEIR clock @@ -26,4 +28,4 @@ struct ContactInfo { private: mutable uint8_t shared_secret[PUB_KEY_SIZE]; -}; +}; \ No newline at end of file