diff --git a/.editorconfig b/.editorconfig index e67213f..a378753 100644 --- a/.editorconfig +++ b/.editorconfig @@ -43,6 +43,17 @@ indent_size = 4 indent_style = space indent_size = unset +# Files ported from Arduino MeshCore ("Shared" class, devdocs/HLD.md §9) keep +# upstream's text verbatim, so a diff against upstream shows only our intentional +# changes, each fenced `// ZEPHCORE:`. Upstream mixes 2-space and tab indentation +# and has trailing whitespace, so none of it is enforced here. Add a file to this +# list when it is converted to upstream shape. +[{zephcore/src/Mesh.cpp,zephcore/src/Dispatcher.cpp,zephcore/src/Packet.cpp,zephcore/src/StaticPoolPacketManager.cpp,zephcore/include/mesh/MeshCore.h,zephcore/include/mesh/Dispatcher.h,zephcore/include/mesh/Mesh.h,zephcore/include/mesh/Packet.h,zephcore/include/mesh/Identity.h,zephcore/include/mesh/Utils.h,zephcore/include/mesh/SimpleMeshTables.h,zephcore/include/mesh/StaticPoolPacketManager.h}] +indent_style = unset +indent_size = unset +tab_width = unset +trim_trailing_whitespace = false + # Third-party license texts are kept verbatim. [{LICENSE,*LICENSE*.txt,*_license.txt}] indent_style = unset diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 34e1dcf..2e0d183 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -63,8 +63,6 @@ zephcore/ │ ├── Radio.h # Abstract radio interface │ ├── Board.h, Clock.h, RNG.h, RTC.h # HAL interfaces │ ├── ContentionTracker.h # Adaptive contention window state -│ ├── LoRaConfig.h # Default radio parameters -│ ├── RadioIncludes.h # Compile-time radio driver selection │ ├── SimpleMeshTables.h # Hash-based packet deduplication │ └── StaticPoolPacketManager.h # Fixed pool allocator │ @@ -240,11 +238,10 @@ The Dispatcher runs a tight loop: ``` loop(): 1. Check if current TX is complete → release packet, record airtime - 2. Process next inbound packet from queue (if scheduled time has passed) - 3. checkRecv(): Drain radio RX ring buffer + 2. checkRecv(): Drain radio RX ring buffer - Parse raw bytes into Packet - - Flood packets: compute RX delay based on score → defer or process immediately - - Direct packets: process immediately + - Process immediately (no inbound queue / score-based RX delay: the + adaptive contention window replaces upstream's rxdelay) 4. checkSend(): Check outbound queue - CAD: if channel busy (`isReceiving()` returns true or radio not ready), retry every 100-200ms (jittered) up to 4s total. On 4s timeout, @@ -256,8 +253,6 @@ loop(): - Serialize and transmit ``` -**RX Delay**: Flood packets are delayed based on signal quality. High-quality signals (high SNR, short packets) get shorter delays, allowing closer/better relays to retransmit first. Uses a lookup table approximation of `10^(0.85 - score*0.1) - 1` multiplied by airtime. - **Duty Cycle**: Fixed 1-hour sliding window. Default 10%. Admin packets (REQ, RESPONSE, ANON_REQ, CONTROL) are exempt. ### 4.6 Maintenance Loop diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index b585eb5..09af64d 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -344,7 +344,6 @@ target_sources(app PRIVATE adapters/radio/LoRaRadioBase.cpp adapters/clock/ZephyrMillisecondClock.cpp adapters/clock/ZephyrRTCClock.cpp - adapters/rng/RNG.cpp adapters/rng/ZephyrRNG.cpp adapters/gps/ZephyrGPSManager.cpp adapters/sensors/ZephyrEnvSensors.cpp diff --git a/zephcore/adapters/board/ZephyrBoard.h b/zephcore/adapters/board/ZephyrBoard.h index 8dce9d0..0d40bc7 100644 --- a/zephcore/adapters/board/ZephyrBoard.h +++ b/zephcore/adapters/board/ZephyrBoard.h @@ -5,7 +5,7 @@ #pragma once -#include +#include namespace mesh { diff --git a/zephcore/adapters/clock/ZephyrMillisecondClock.h b/zephcore/adapters/clock/ZephyrMillisecondClock.h index 1d4f30d..f8e2efe 100644 --- a/zephcore/adapters/clock/ZephyrMillisecondClock.h +++ b/zephcore/adapters/clock/ZephyrMillisecondClock.h @@ -5,7 +5,7 @@ #pragma once -#include +#include namespace mesh { diff --git a/zephcore/adapters/clock/ZephyrRTCClock.h b/zephcore/adapters/clock/ZephyrRTCClock.h index 9bad7c8..7cfe802 100644 --- a/zephcore/adapters/clock/ZephyrRTCClock.h +++ b/zephcore/adapters/clock/ZephyrRTCClock.h @@ -5,7 +5,7 @@ #pragma once -#include +#include namespace mesh { diff --git a/zephcore/adapters/datastore/ZephyrDataStore.h b/zephcore/adapters/datastore/ZephyrDataStore.h index 0863e40..8d8ef5f 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.h +++ b/zephcore/adapters/datastore/ZephyrDataStore.h @@ -9,7 +9,7 @@ #pragma once #include -#include +#include #include #include #include diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index 0b64db5..936956d 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -6,7 +6,7 @@ #include "LoRaRadioBase.h" #include "radio_common.h" #include "pm_sleep_guard.h" -#include +#include #include /* MAX_TRANS_UNIT */ #include #include diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h index c49e241..4a540df 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -6,8 +6,8 @@ #pragma once -#include -#include +#include +#include #include #include #include diff --git a/zephcore/adapters/rng/RNG.cpp b/zephcore/adapters/rng/RNG.cpp deleted file mode 100644 index 447b3da..0000000 --- a/zephcore/adapters/rng/RNG.cpp +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * RNG::nextInt - shared implementation - */ - -#include - -namespace mesh { - -uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) -{ - uint32_t num; - random((uint8_t *)&num, sizeof(num)); - if (_max <= _min) { - return _min; - } - return (num % (_max - _min)) + _min; -} - -} /* namespace mesh */ diff --git a/zephcore/adapters/rng/ZephyrRNG.h b/zephcore/adapters/rng/ZephyrRNG.h index bdb637f..840368e 100644 --- a/zephcore/adapters/rng/ZephyrRNG.h +++ b/zephcore/adapters/rng/ZephyrRNG.h @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index e6458bc..4499ad0 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -23,6 +23,7 @@ #define ZEPHCORE_HAS_UI_TASK 1 #endif #include +#include LOG_MODULE_REGISTER(zephcore_companion, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); /* Protocol commands (matches Arduino companion_radio) - sorted by opcode */ @@ -1927,8 +1928,14 @@ uint8_t CompanionMesh::onContactRequest(const ContactInfo &contact, uint32_t sen return 0; // Unknown request or denied } -void CompanionMesh::logTx(mesh::Packet *, int) +void CompanionMesh::logRx(mesh::Packet *pkt, int len, float score) { + packet_log_rx(getLogDateTime(), pkt, _radio->getLastRSSI(), score, _radio->getEstAirtimeFor(len)); +} + +void CompanionMesh::logTx(mesh::Packet *pkt, int) +{ + packet_log_tx(getLogDateTime(), pkt); #if ZEPHCORE_HAS_UI_TASK ui_notify_packet_sent(); #endif diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index 6b16a25..1568ebf 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -318,6 +318,7 @@ protected: /* Raw packet logging for app RX log */ void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; + void logRx(mesh::Packet *pkt, int len, float score) override; void logTx(mesh::Packet *pkt, int len) override; /* Trace path response */ diff --git a/zephcore/app/ObserverMesh.cpp b/zephcore/app/ObserverMesh.cpp index 299e1c3..485d608 100644 --- a/zephcore/app/ObserverMesh.cpp +++ b/zephcore/app/ObserverMesh.cpp @@ -7,7 +7,7 @@ #include "observer_creds.h" #include -#include +#include #include #include /* generateFirstBootIdentity (hardened keygen) */ #include @@ -22,6 +22,7 @@ LOG_MODULE_REGISTER(zephcore_observer, CONFIG_ZEPHCORE_OBSERVER_LOG_LEVEL); #include #include +#include /* Forward declaration — implemented in ZephyrWiFiStation.c */ extern "C" { @@ -193,10 +194,16 @@ void ObserverMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) void ObserverMesh::logRx(Packet *packet, int len, float score) { - (void)packet; (void)len; + packet_log_rx(getLogDateTime(), packet, _radio->getLastRSSI(), score, _radio->getEstAirtimeFor(len)); _last_score = score; } +void ObserverMesh::logTx(Packet *packet, int len) +{ + (void)len; + packet_log_tx(getLogDateTime(), packet); +} + void ObserverMesh::enqueuePacket(Packet *pkt) { /* Compute packet hash (8 bytes → 16 hex chars) */ diff --git a/zephcore/app/ObserverMesh.h b/zephcore/app/ObserverMesh.h index dbff772..d18fcb6 100644 --- a/zephcore/app/ObserverMesh.h +++ b/zephcore/app/ObserverMesh.h @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include #include "RepeaterDataStore.h" @@ -67,6 +67,7 @@ protected: void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; /* Capture score (called between logRxRaw and onRecvPacket) */ void logRx(Packet *packet, int len, float score) override; + void logTx(Packet *packet, int len) override; /* Build JSON and enqueue to MQTT publisher */ DispatcherAction onRecvPacket(Packet *pkt) override; diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index bdb7130..38cb633 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -26,6 +26,7 @@ #include #include #endif +#include /* Helper to get radio driver for stats — uses LoRaRadioBase (works for SX126x and LR1110) */ static inline mesh::LoRaRadioBase& getRadioDriver(mesh::Radio* radio) { @@ -354,8 +355,8 @@ int RepeaterMesh::handleRequest(ClientInfo* sender, uint32_t sender_timestamp, u stats.n_recv_direct = getNumRecvDirect(); stats.err_events = _err_flags; stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); - stats.n_direct_dups = ((mesh::SimpleMeshTables *)getTables())->getNumDirectDups(); - stats.n_flood_dups = ((mesh::SimpleMeshTables *)getTables())->getNumFloodDups(); + stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); + stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); stats.total_rx_air_time_secs = getReceiveAirTime() / 1000; stats.n_recv_errors = radio_driver.getPacketsRecvErrors(); memcpy(&reply_data[4], &stats, sizeof(stats)); @@ -662,6 +663,7 @@ void RepeaterMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) } void RepeaterMesh::logRx(mesh::Packet* pkt, int len, float score) { + packet_log_rx(getLogDateTime(), pkt, _radio->getLastRSSI(), score, _radio->getEstAirtimeFor(len)); if (_logging) { LOG_INF("RX len=%d type=%d route=%s payload_len=%d SNR=%d RSSI=%d", len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", @@ -674,6 +676,7 @@ void RepeaterMesh::logRx(mesh::Packet* pkt, int len, float score) { } void RepeaterMesh::logTx(mesh::Packet* pkt, int len) { + packet_log_tx(getLogDateTime(), pkt); if (_logging) { LOG_INF("TX len=%d type=%d route=%s payload_len=%d", len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", @@ -998,7 +1001,7 @@ void RepeaterMesh::sendNodeDiscoverReq() { RepeaterMesh::RepeaterMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) - : mesh::Mesh(radio, ms, rng, rtc, *new mesh::StaticPoolPacketManager(), tables), + : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(), tables), _board(board), _cli(board, rtc, acl, &_prefs, this), region_map(key_store), temp_map(key_store), @@ -1344,7 +1347,7 @@ void RepeaterMesh::clearStats() { radio_driver.resetStats(); radio_driver.resetDutyCycleTimeoutRestarts(); resetStats(); - ((mesh::SimpleMeshTables *)getTables())->resetStats(); + ((SimpleMeshTables *)getTables())->resetStats(); } uint32_t RepeaterMesh::getDutyCycleTimeoutRestarts() const { diff --git a/zephcore/app/RoomServerMesh.cpp b/zephcore/app/RoomServerMesh.cpp index 7e6fe2e..7854998 100644 --- a/zephcore/app/RoomServerMesh.cpp +++ b/zephcore/app/RoomServerMesh.cpp @@ -28,6 +28,7 @@ #include #include #include +#include /* Helper to get radio driver for stats — uses LoRaRadioBase (works for SX126x and LR1110) */ static inline mesh::LoRaRadioBase& getRadioDriver(mesh::Radio* radio) { @@ -119,8 +120,8 @@ int RoomServerMesh::handleRequest(ClientInfo* sender, uint32_t sender_timestamp, stats.n_recv_direct = getNumRecvDirect(); stats.err_events = _err_flags; stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); - stats.n_direct_dups = ((mesh::SimpleMeshTables *)getTables())->getNumDirectDups(); - stats.n_flood_dups = ((mesh::SimpleMeshTables *)getTables())->getNumFloodDups(); + stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); + stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); stats.n_posted = _num_posted; stats.n_post_push = _num_post_pushes; memcpy(&reply_data[4], &stats, sizeof(stats)); @@ -407,6 +408,7 @@ void RoomServerMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int le } void RoomServerMesh::logRx(mesh::Packet* pkt, int len, float score) { + packet_log_rx(getLogDateTime(), pkt, _radio->getLastRSSI(), score, _radio->getEstAirtimeFor(len)); if (_logging) { LOG_INF("RX len=%d type=%d route=%s payload_len=%d SNR=%d RSSI=%d", len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", @@ -416,6 +418,7 @@ void RoomServerMesh::logRx(mesh::Packet* pkt, int len, float score) { } void RoomServerMesh::logTx(mesh::Packet* pkt, int len) { + packet_log_tx(getLogDateTime(), pkt); if (_logging) { LOG_INF("TX len=%d type=%d route=%s payload_len=%d", len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", @@ -757,7 +760,7 @@ bool RoomServerMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const RoomServerMesh::RoomServerMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) - : mesh::Mesh(radio, ms, rng, rtc, *new mesh::StaticPoolPacketManager(), tables), + : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(), tables), _board(board), _cli(board, rtc, acl, &_prefs, this), region_map(key_store), temp_map(key_store), @@ -1003,7 +1006,7 @@ void RoomServerMesh::clearStats() { radio_driver.resetStats(); radio_driver.resetDutyCycleTimeoutRestarts(); resetStats(); - ((mesh::SimpleMeshTables *)getTables())->resetStats(); + ((SimpleMeshTables *)getTables())->resetStats(); } uint32_t RoomServerMesh::getDutyCycleTimeoutRestarts() const { diff --git a/zephcore/app/main_observer.cpp b/zephcore/app/main_observer.cpp index 52755fa..ea880fa 100644 --- a/zephcore/app/main_observer.cpp +++ b/zephcore/app/main_observer.cpp @@ -35,7 +35,7 @@ LOG_MODULE_REGISTER(zephcore_observer_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); #include "../adapters/datastore/ZephyrFsFormat.h" #include #include -#include +#include #include #include #include "observer_creds.h" diff --git a/zephcore/helpers/CommonCLI.h b/zephcore/helpers/CommonCLI.h index 62a9388..1ea0379 100644 --- a/zephcore/helpers/CommonCLI.h +++ b/zephcore/helpers/CommonCLI.h @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include #include "NodePrefs.h" diff --git a/zephcore/include/mesh/LoRaConfig.h b/zephcore/helpers/LoRaConfig.h similarity index 100% rename from zephcore/include/mesh/LoRaConfig.h rename to zephcore/helpers/LoRaConfig.h diff --git a/zephcore/helpers/MeshTimeSync.h b/zephcore/helpers/MeshTimeSync.h index 744f022..5fe1b98 100644 --- a/zephcore/helpers/MeshTimeSync.h +++ b/zephcore/helpers/MeshTimeSync.h @@ -20,7 +20,7 @@ #include #include -#include +#include #ifdef CONFIG_ZEPHCORE_TIMESYNC_TABLE_SIZE #define MESHTIMESYNC_TABLE_SIZE CONFIG_ZEPHCORE_TIMESYNC_TABLE_SIZE diff --git a/zephcore/helpers/PacketLog.h b/zephcore/helpers/PacketLog.h new file mode 100644 index 0000000..1daa918 --- /dev/null +++ b/zephcore/helpers/PacketLog.h @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: MIT + * Arduino-compatible packet log lines (CONFIG_ZEPHCORE_PACKET_LOGGING). + * + * Same format as Arduino MeshCore's MESH_PACKET_LOGGING, and like upstream it is + * emitted from the roles' logRx()/logTx() hooks rather than from the core + * Dispatcher. printk() bypasses log-level filtering so packet_logging.conf can + * silence everything else. Compiles to nothing without the Kconfig option. + */ + +#pragma once + +#include +#include +#include + +#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING) + +static inline bool packet_log_has_addrs(uint8_t ptype) +{ + return ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ || + ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG; +} + +static inline void packet_log_rx(const char *datetime, const mesh::Packet *pkt, + float rssi, float score, uint32_t air_time) +{ + static uint8_t packet_hash[MAX_HASH_SIZE]; + static char hash_hex[MAX_HASH_SIZE * 2 + 1]; + uint8_t ptype = pkt->getPayloadType(); + + pkt->calculatePacketHash(packet_hash); + mesh::Utils::toHex(hash_hex, packet_hash, MAX_HASH_SIZE); + if (packet_log_has_addrs(ptype)) { + printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s [%02X -> %02X]\n", + datetime, pkt->getRawLength(), ptype, + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (int)pkt->getSNR(), (int)rssi, (int)(score * 1000), air_time, + hash_hex, (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } else { + printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s\n", + datetime, pkt->getRawLength(), ptype, + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (int)pkt->getSNR(), (int)rssi, (int)(score * 1000), air_time, + hash_hex); + } +} + +/* Called on TX completion, as upstream does (before 2026-09-23 the core printed + * this line just before startSendRaw()). */ +static inline void packet_log_tx(const char *datetime, const mesh::Packet *pkt) +{ + uint8_t ptype = pkt->getPayloadType(); + + if (packet_log_has_addrs(ptype)) { + printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d) [%02X -> %02X]\n", + datetime, pkt->getRawLength(), ptype, + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } else { + printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d)\n", + datetime, pkt->getRawLength(), ptype, + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + } +} + +#else + +static inline void packet_log_rx(const char *, const mesh::Packet *, float, float, uint32_t) {} +static inline void packet_log_tx(const char *, const mesh::Packet *) {} + +#endif diff --git a/zephcore/include/mesh/Board.h b/zephcore/include/mesh/Board.h deleted file mode 100644 index ce67cfe..0000000 --- a/zephcore/include/mesh/Board.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore MainBoard interface - */ - -#pragma once - -#include -#include -#include - -namespace mesh { - -#define BD_STARTUP_NORMAL 0 -#define BD_STARTUP_RX_PACKET 1 - -class MainBoard { -public: - virtual uint16_t getBattMilliVolts() = 0; - virtual uint8_t getBattPercent() { return 0; } - virtual float getMCUTemperature() { return NAN; } - virtual bool setAdcMultiplier(float multiplier) { (void)multiplier; return false; } - virtual float getAdcMultiplier() const { return 0.0f; } - virtual const char *getManufacturerName() const = 0; - virtual void onBeforeTransmit() {} - virtual void onAfterTransmit() {} - /* A valid packet has just been received. Unlike the transmit pair this is - * a single edge, not a window: the packet is already over by the time the - * radio tells us, so an implementation that drives an LED has to fire a - * one-shot rather than hold a level. */ - virtual void onPacketReceived() {} - virtual void reboot() = 0; - virtual void powerOff() {} - virtual void sleep(uint32_t secs) { (void)secs; } - virtual uint32_t getGpio() { return 0; } - virtual void setGpio(uint32_t values) { (void)values; } - virtual uint8_t getStartupReason() const = 0; - virtual bool getBootloaderVersion(char *version, size_t max_len) { (void)version; (void)max_len; return false; } - virtual bool startOTAUpdate(const char *id, char reply[]) { (void)id; (void)reply; return false; } - - virtual bool isExternalPowered() { return false; } - virtual uint16_t getBootVoltage() { return 0; } - virtual uint32_t getResetReason() const { return 0; } - virtual const char *getResetReasonString(uint32_t reason) { (void)reason; return "Not available"; } - virtual uint8_t getShutdownReason() const { return 0; } - virtual const char *getShutdownReasonString(uint8_t reason) { (void)reason; return "Not available"; } -}; - -} /* namespace mesh */ diff --git a/zephcore/include/mesh/Clock.h b/zephcore/include/mesh/Clock.h deleted file mode 100644 index 7a1b915..0000000 --- a/zephcore/include/mesh/Clock.h +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore clock interfaces - */ - -#pragma once - -#include - -namespace mesh { - -class MillisecondClock { -public: - virtual unsigned long getMillis() = 0; -}; - -} /* namespace mesh */ diff --git a/zephcore/include/mesh/Dispatcher.h b/zephcore/include/mesh/Dispatcher.h index 1fb7247..5dbbc7d 100644 --- a/zephcore/include/mesh/Dispatcher.h +++ b/zephcore/include/mesh/Dispatcher.h @@ -1,179 +1,304 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Dispatcher - packet queue and radio scheduling - */ - #pragma once #include #include #include #include -#include -#include -#include +#include // ZEPHCORE: deadline-driven maintenance #include namespace mesh { -class PacketManager { +/** + * \brief Abstraction of local/volatile clock with Millisecond granularity. +*/ +class MillisecondClock { public: - virtual Packet *allocNew() = 0; - virtual void free(Packet *packet) = 0; - virtual void queueOutbound(Packet *packet, uint8_t priority, uint32_t scheduled_for) = 0; - virtual Packet *getNextOutbound(uint32_t now) = 0; - virtual int getOutboundCount(uint32_t now) const = 0; - virtual int getOutboundTotal() const = 0; - virtual int getFreeCount() const = 0; - virtual Packet *getOutboundByIdx(int i) = 0; - virtual Packet *removeOutboundByIdx(int i) = 0; - virtual uint32_t getOutboundSchedule(int i) const = 0; - virtual bool rescheduleOutbound(int i, uint32_t new_scheduled_for) = 0; - virtual uint8_t peekNextOutboundPriority(uint32_t now) const = 0; + virtual unsigned long getMillis() = 0; }; -/* Notifies event loop of pending TX so it can schedule a wake. */ -typedef void (*tx_queued_callback_t)(uint32_t delay_ms, void *user_data); +/** + * \brief Abstraction of this device's packet radio. +*/ +class Radio { +public: + virtual void begin() { } -/* Wakes the event loop so loop() runs at the next opportunity. For off-main - * code that sets state loop() must drain (MQTT CONNACK, SNTP): without it that - * state waits for whatever deadline happens to fire next, which since the move - * to deadline-driven maintenance can be minutes rather than the old 5 s tick. */ -typedef void (*wake_callback_t)(void *user_data); + /** + * \brief polls for incoming raw packet. + * \param bytes destination to store incoming raw packet. + * \param sz maximum packet size allowed. + * \returns 0 if no incoming data, otherwise length of complete packet received. + */ + virtual int recvRaw(uint8_t* bytes, int sz) = 0; -typedef uint32_t DispatcherAction; + /** + * \returns estimated transmit air-time needed for packet of 'len_bytes', in milliseconds. + */ + virtual uint32_t getEstAirtimeFor(int len_bytes) = 0; + + virtual float packetScore(float snr, int packet_len) = 0; + + /** + * \brief starts the raw packet send. (no wait) + * \param bytes the raw packet data + * \param len the length in bytes + * \returns true if successfully started + */ + virtual bool startSendRaw(const uint8_t* bytes, int len) = 0; + + /** + * \returns true if the previous 'startSendRaw()' completed successfully. + * ZEPHCORE: one-shot — returns true exactly once per completed transmit and + * consumes that completion (like RadioLibWrapper). Never use it as a state + * query; use isTxActive() for that. + */ + virtual bool isSendComplete() = 0; + + // ZEPHCORE: non-consuming "a transmit is in flight" query. + virtual bool isTxActive() const { return false; } + + /** + * \brief a hook for doing any necessary clean up after transmit. + */ + virtual void onSendFinished() = 0; + + /** + * \brief do any processing needed on each loop cycle + */ + virtual void loop() { } + + virtual int getNoiseFloor() const { return 0; } + + virtual void triggerNoiseFloorCalibrate(int threshold) { } + + virtual void setCADEnabled(bool enable) { } + + virtual void resetAGC() { } + + virtual bool isInRecvMode() const = 0; + + /** + * \returns true if the radio is currently mid-receive of a packet. + */ + virtual bool isReceiving() { return false; } + + virtual float getLastRSSI() const { return 0; } + virtual float getLastSNR() const { return 0; } + + // ZEPHCORE: radio extensions used by the Dispatcher and the roles. CAD is + // always LBT inside the driver (setCADEnabled unused) and the AGC unstick is + // part of radioMaintenance() (resetAGC unused). + + // Radio not command-ready (e.g. BUSY high); the Dispatcher defers TX. + virtual bool isRadioReady() { return true; } + + // Reset the radio into a known good RX state: called on CAD timeout, when + // isReceiving() stayed true past getCADFailMaxDuration(). Radios that can + // stall walk the chip cancel -> REST -> fresh RX, clearing IRQs and latches. + virtual void recoverRxState() { } + + // Adaptive CAD (LBT detPeak calibration). stored_base is the family base + // detPeak `offset` was learned against (0 = none recorded); when it differs + // from the driver's current base the offset is re-anchored so the ABSOLUTE + // detPeak is preserved. Read getCadOffset()/cadBasePeak() back to persist. + virtual void setCadParams(bool auto_enabled, int8_t offset, uint16_t probe_interval_s, + uint8_t busycap_pct, uint8_t stored_base = 0) { } + // Family base detPeak in force; 0 = no adaptive CAD, nothing to store. + virtual uint8_t cadBasePeak() { return 0; } + // One CAD calibrator tick: maybe probe, update stats, maybe step (auto mode). + virtual void cadMaintenance() { } + virtual int8_t getCadOffset() const { return 0; } + virtual void resetCadStats() { } + // Last-resort unmute after getTxStarvationDuration() of LBT refusals: one + // step less sensitive, regardless of cad.auto. False = already at the least + // sensitive step (channel genuinely busy, or the radio is broken). + virtual bool cadRelaxOnTxStarvation() { return false; } + // Human-readable status blocks; return chars written (0 = unsupported). + virtual int formatCadStatus(char* buf, int cap) { return 0; } + virtual int formatFreqErrorStatus(char* buf, int cap) { return 0; } + + // Receiver hygiene (deaf-aware AGC unstick, temperature-drift recal). Both + // sleep the radio, so called only from the maintenance tick. + virtual void radioMaintenance() { } + // Milliseconds until this radio's periodic work next needs a call, or + // MAINTENANCE_IDLE when nothing is pending. + virtual uint32_t msUntilNextMaintenance() { return MAINTENANCE_IDLE; } + + virtual uint32_t getPacketsRecv() const { return 0; } + virtual uint32_t getPacketsSent() const { return 0; } + virtual uint32_t getPacketsRecvErrors() const { return 0; } +}; + +/** + * \brief An abstraction for managing instances of Packets (eg. in a static pool), + * and for managing the outbound packet queue. +*/ +class PacketManager { +public: + virtual Packet* allocNew() = 0; + virtual void free(Packet* packet) = 0; + + virtual void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; + virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority + virtual int getOutboundCount(uint32_t now) const = 0; + virtual int getOutboundTotal() const = 0; + virtual int getFreeCount() const = 0; + virtual Packet* getOutboundByIdx(int i) = 0; + virtual Packet* removeOutboundByIdx(int i) = 0; + // ZEPHCORE: no inbound queue (queueInbound/getNextInbound) — received packets + // are processed immediately; the adaptive contention window replaces upstream's + // score-based RX delay. Instead, schedule access for reactive backoff: + virtual uint32_t getOutboundSchedule(int i) const = 0; + virtual bool rescheduleOutbound(int i, uint32_t new_scheduled_for) = 0; + virtual uint8_t peekNextOutboundPriority(uint32_t now) const = 0; +}; + +// ZEPHCORE: event-loop hooks. The mesh thread sleeps in k_event_wait(); these +// let the Dispatcher ask for a wake instead of being polled. +// Pending TX: schedule a wake in delay_ms. +typedef void (*tx_queued_callback_t)(uint32_t delay_ms, void* user_data); +// Run loop() at the next opportunity (off-main code that set state loop() drains). +typedef void (*wake_callback_t)(void* user_data); + +typedef uint32_t DispatcherAction; #define ACTION_RELEASE (0) #define ACTION_MANUAL_HOLD (1) #define ACTION_RETRANSMIT(pri) (((uint32_t)1 + (pri))<<24) #define ACTION_RETRANSMIT_DELAYED(pri, _delay) ((((uint32_t)1 + (pri))<<24) | (_delay)) -/* Radio considered stalled after this long neither receiving nor sending. */ -#define RADIO_STALL_THRESHOLD_MS 8000 - #define ERR_EVENT_FULL (1 << 0) #define ERR_EVENT_CAD_TIMEOUT (1 << 1) #define ERR_EVENT_STARTRX_TIMEOUT (1 << 2) -class Dispatcher { - Packet *outbound; - uint8_t outbound_priority; - uint32_t outbound_expiry, outbound_start, total_air_time, rx_air_time; - uint32_t next_tx_time; - uint32_t cad_busy_start; - /* Separate streak timer for the driver's own LBT verdict. It cannot - * share cad_busy_start: checkSend() zeroes that unconditionally once the - * pre-TX software gate lets it through, which is every pass on which the - * driver is the one refusing — so the shared variable was reset to 0 - * before each refusal could ever accumulate 4 s against it, and the - * escalation below it was unreachable. Measured on an XIAO ESP32-S3 with - * detPeak forced to its floor: 389 consecutive LBT refusals over 100 s, - * zero warnings, zero error flags, err_events still reading 0. */ - uint32_t lbt_busy_start; - /* Rate limit for the LBT warning. Separate from lbt_busy_start so that - * warning does not re-arm the streak clock — the starvation override - * below needs the true continuous duration, not the time since the last - * log line. */ - uint32_t lbt_next_warn; - uint32_t tx_budget_ms; - uint32_t last_budget_update; - uint32_t duty_cycle_window_ms; - uint32_t radio_nonrx_start; - bool prev_isrecv_mode; - int8_t cad_offset_shadow; - bool cad_offset_shadow_valid; - uint32_t n_sent_flood, n_sent_direct; - uint32_t n_recv_flood, n_recv_direct; - tx_queued_callback_t _tx_queued_cb; - void *_tx_queued_user_data; - wake_callback_t _wake_cb; - void *_wake_user_data; +// ZEPHCORE: radio considered stalled after this long neither receiving nor sending. +#define RADIO_STALL_THRESHOLD_MS 8000 - void processRecvPacket(Packet *pkt); +/** + * \brief The low-level task that manages detecting incoming Packets, and the queueing + * and scheduling of outbound Packets. + * ZEPHCORE: times are uint32_t, not unsigned long — unsigned long is 64 bits on + * the native_sim/native/64 build, which would change the wrap arithmetic. +*/ +class Dispatcher { + Packet* outbound; // current outbound packet + uint32_t outbound_expiry, outbound_start, total_air_time, rx_air_time; + uint32_t next_tx_time; + uint32_t cad_busy_start; + uint32_t radio_nonrx_start; + bool prev_isrecv_mode; + uint32_t n_sent_flood, n_sent_direct; + uint32_t n_recv_flood, n_recv_direct; + uint32_t tx_budget_ms; + uint32_t last_budget_update; + uint32_t duty_cycle_window_ms; + + // ZEPHCORE: state for LBT refusal escalation, adaptive CAD persistence and + // the event-loop callbacks (see Dispatcher.cpp). + uint8_t outbound_priority; + uint32_t lbt_busy_start; + uint32_t lbt_next_warn; + int8_t cad_offset_shadow; + bool cad_offset_shadow_valid; + tx_queued_callback_t _tx_queued_cb; + void* _tx_queued_user_data; + wake_callback_t _wake_cb; + void* _wake_user_data; + + void processRecvPacket(Packet* pkt); + void updateTxBudget(); protected: - Radio *_radio; - MillisecondClock *_ms; - PacketManager *_mgr; - uint16_t _err_flags; + Radio* _radio; + MillisecondClock* _ms; + PacketManager* _mgr; + uint16_t _err_flags; - Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr); - void notifyTxQueued(uint32_t delay_ms) { - if (_tx_queued_cb) _tx_queued_cb(delay_ms, _tx_queued_user_data); - } - virtual DispatcherAction onRecvPacket(Packet *pkt) = 0; - virtual void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { (void)snr; (void)rssi; (void)raw; (void)len; } - virtual void logRx(Packet *packet, int len, float score) { (void)packet; (void)len; (void)score; } - virtual void logTx(Packet *packet, int len) { (void)packet; (void)len; } - virtual void logTxFail(Packet *packet, int len) { (void)packet; (void)len; } - virtual const char *getLogDateTime() { return ""; } - virtual uint8_t getDutyCyclePercent() const; - static bool isAdminPacket(const Packet *pkt); - virtual uint32_t getCADFailRetryDelay() const; - virtual uint32_t getCADFailMaxDuration() const; - /* How long the driver's LBT may refuse every transmit before we conclude - * the detector is too sensitive rather than the channel genuinely busy, - * and relax it one step. Deliberately far longer than - * getCADFailMaxDuration(): a real busy channel nearly always yields SOME - * successful transmit inside a minute, and any success resets the - * streak, so this only fires on a node that is not talking at all. */ - virtual uint32_t getTxStarvationDuration() const; - virtual int getInterferenceThreshold() const { return 0; } - virtual uint32_t getDutyCycleWindowMs() const { return 3600000UL; } /* 1h default */ - /* Adaptive CAD: called when the auto staircase moved the operating - * detPeak offset — subclasses persist it to prefs. */ - virtual void onCadOffsetChanged(int8_t offset) { (void)offset; } + Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr); // ZEPHCORE: in Dispatcher.cpp + + virtual DispatcherAction onRecvPacket(Packet* pkt) = 0; + + virtual void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { } // custom hook + + virtual void logRx(Packet* packet, int len, float score) { } // hooks for custom logging + virtual void logTx(Packet* packet, int len) { } + virtual void logTxFail(Packet* packet, int len) { } + virtual const char* getLogDateTime() { return ""; } + + virtual uint32_t getCADFailRetryDelay() const; + virtual uint32_t getCADFailMaxDuration() const; + virtual int getInterferenceThreshold() const { return 0; } // disabled by default + virtual uint32_t getDutyCycleWindowMs() const { return 3600000; } + + // ZEPHCORE: duty cycle as a percentage (upstream: getAirtimeBudgetFactor); + // admin packets are exempt from the budget gate. + virtual uint8_t getDutyCyclePercent() const; + static bool isAdminPacket(const Packet* pkt); + // How long the driver's LBT may refuse every transmit before we conclude the + // detector is too sensitive and relax it one step (see checkSend()). + virtual uint32_t getTxStarvationDuration() const; + // Adaptive CAD moved the operating detPeak offset; subclasses persist it. + virtual void onCadOffsetChanged(int8_t offset) { } + void notifyTxQueued(uint32_t delay_ms) { + if (_tx_queued_cb) _tx_queued_cb(delay_ms, _tx_queued_user_data); + } public: - void begin(); - void loop(); - void maintenanceLoop(); + void begin(); + void loop(); - /* Milliseconds until maintenanceLoop() next has work, or - * MAINTENANCE_IDLE if nothing is pending. Cheap and side-effect free: - * the event loop calls it after every pass to size its next sleep. - * Subclasses that add their own time-based work in loop() override this - * and fold their deadlines in — see RepeaterMesh. */ - virtual uint32_t msUntilNextMaintenance(); - Packet *obtainNewPacket(); - void releasePacket(Packet *packet); - void sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis = 0); + // ZEPHCORE: deadline-driven maintenance. msUntilNextMaintenance() is cheap + // and side-effect free; the event loop calls it after every pass to size its + // next sleep. Subclasses with their own timed work fold their deadlines in. + void maintenanceLoop(); + virtual uint32_t msUntilNextMaintenance(); - uint32_t getTotalAirTime() const { return total_air_time; } - uint32_t getReceiveAirTime() const { return rx_air_time; } - uint32_t getNumSentFlood() const { return n_sent_flood; } - uint32_t getNumSentDirect() const { return n_sent_direct; } - uint32_t getNumRecvFlood() const { return n_recv_flood; } - uint32_t getNumRecvDirect() const { return n_recv_direct; } - uint16_t getErrFlags() const { return _err_flags; } - void resetStats() { - n_sent_flood = n_sent_direct = 0; - n_recv_flood = n_recv_direct = 0; - _err_flags = 0; - } - void setTxQueuedCallback(tx_queued_callback_t cb, void *user_data) { - _tx_queued_cb = cb; - _tx_queued_user_data = user_data; - } - void setWakeCallback(wake_callback_t cb, void *user_data) { - _wake_cb = cb; - _wake_user_data = user_data; - } - /* Safe from any thread: the callback only posts an event. Public because - * off-main C callbacks (e.g. the SNTP hook) are not class members. */ - void notifyWake() { - if (_wake_cb) _wake_cb(_wake_user_data); - } - bool millisHasNowPassed(uint32_t timestamp) const; - uint32_t futureMillis(int millis_from_now) const; + Packet* obtainNewPacket(); + void releasePacket(Packet* packet); + void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); - bool tryParsePacket(Packet *pkt, const uint8_t *raw, int len); + uint32_t getTotalAirTime() const { return total_air_time; } + uint32_t getReceiveAirTime() const {return rx_air_time; } + uint32_t getRemainingTxBudget() const { return tx_budget_ms; } + uint32_t getNumSentFlood() const { return n_sent_flood; } + uint32_t getNumSentDirect() const { return n_sent_direct; } + uint32_t getNumRecvFlood() const { return n_recv_flood; } + uint32_t getNumRecvDirect() const { return n_recv_direct; } + void resetStats() { + n_sent_flood = n_sent_direct = n_recv_flood = n_recv_direct = 0; + _err_flags = 0; + } + + // ZEPHCORE: error flags and event-loop callbacks. + uint16_t getErrFlags() const { return _err_flags; } + void setTxQueuedCallback(tx_queued_callback_t cb, void* user_data) { + _tx_queued_cb = cb; + _tx_queued_user_data = user_data; + } + void setWakeCallback(wake_callback_t cb, void* user_data) { + _wake_cb = cb; + _wake_user_data = user_data; + } + // Safe from any thread: the callback only posts an event. + void notifyWake() { + if (_wake_cb) _wake_cb(_wake_user_data); + } + + // helper methods + bool millisHasNowPassed(uint32_t timestamp) const; + uint32_t futureMillis(int millis_from_now) const; + + bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len); private: - void updateTxBudget(); - uint32_t getMaxTxBudgetMs() const; - void checkRecv(); - void checkSend(); + // ZEPHCORE: percentage budget and checkSend() helpers (duty-cycle gate, LBT + // refusal escalation). + uint32_t getMaxTxBudgetMs() const; + bool txBudgetAllowsSend(uint32_t now); + void onLbtRefused(uint32_t now, int len); + void checkRecv(); + void checkSend(); }; -} /* namespace mesh */ +} diff --git a/zephcore/include/mesh/Identity.h b/zephcore/include/mesh/Identity.h index 753f63e..bf0fca0 100644 --- a/zephcore/include/mesh/Identity.h +++ b/zephcore/include/mesh/Identity.h @@ -1,106 +1,124 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Identity - Ed25519 key pairs - */ - #pragma once #include -#include -#include #include #include namespace mesh { +/** + * \brief An identity in the mesh, with given Ed25519 public key, ie. a party whose signatures can be VERIFIED. +*/ class Identity { public: - uint8_t pub_key[PUB_KEY_SIZE]; + uint8_t pub_key[PUB_KEY_SIZE]; - Identity(); - Identity(const char *pub_hex); - Identity(const uint8_t *_pub) { memcpy(pub_key, _pub, PUB_KEY_SIZE); } + Identity(); + Identity(const char* pub_hex); + Identity(const uint8_t* _pub) { memcpy(pub_key, _pub, PUB_KEY_SIZE); } - int copyHashTo(uint8_t *dest) const { - memcpy(dest, pub_key, PATH_HASH_SIZE); - 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; - } - bool isHashMatch(const uint8_t *hash, uint8_t len) const { - return memcmp(hash, pub_key, len) == 0; - } - bool verify(const uint8_t *sig, const uint8_t *message, int msg_len) const; - bool matches(const Identity &other) const { return memcmp(pub_key, other.pub_key, PUB_KEY_SIZE) == 0; } - bool matches(const uint8_t *other_pubkey) const { return memcmp(pub_key, other_pubkey, PUB_KEY_SIZE) == 0; } - bool readFrom(const uint8_t *src, size_t len); - bool writeTo(uint8_t *dest, size_t max_len) const; + int copyHashTo(uint8_t* dest) const { + 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); // hash is just prefix of pub_key + return len; + } + bool isHashMatch(const uint8_t* hash) const { + return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0; + } + bool isHashMatch(const uint8_t* hash, uint8_t len) const { + return memcmp(hash, pub_key, len) == 0; + } + + /** + * \brief Performs Ed25519 signature verification. + * \param sig IN - must be SIGNATURE_SIZE buffer. + * \param message IN - the original message which was signed. + * \param msg_len IN - the length in bytes of message. + * \returns true, if signature is valid. + */ + bool verify(const uint8_t* sig, const uint8_t* message, int msg_len) const; + + bool matches(const Identity& other) const { return memcmp(pub_key, other.pub_key, PUB_KEY_SIZE) == 0; } + bool matches(const uint8_t* other_pubkey) const { return memcmp(pub_key, other_pubkey, PUB_KEY_SIZE) == 0; } + + // ZEPHCORE: buffer I/O instead of Arduino Stream (readFrom/writeTo/printTo). + bool readFrom(const uint8_t* src, size_t len); + bool writeTo(uint8_t* dest, size_t max_len) const; }; +/** + * \brief An Identity generated on THIS device, ie. with public/private Ed25519 key pair being on this device. +*/ class LocalIdentity : public Identity { - uint8_t prv_key[PRV_KEY_SIZE]; + uint8_t prv_key[PRV_KEY_SIZE]; public: - LocalIdentity(); - LocalIdentity(const char *prv_hex, const char *pub_hex); - LocalIdentity(RNG *rng); + LocalIdentity(); + LocalIdentity(const char* prv_hex, const char* pub_hex); + LocalIdentity(RNG* rng); // create new random - /* Derive Ed25519 keypair from a 32-byte seed. Use this when you've - * already produced a high-quality seed externally (e.g. via the - * layered ZephyrRNG::mixIdentitySeed entropy mixer) — avoids the - * one-shot-RNG-wrapper dance otherwise needed to feed bytes - * through the LocalIdentity(RNG*) constructor. */ - void fromSeed(const uint8_t seed[SEED_SIZE]); + // ZEPHCORE: derive the Ed25519 key pair from a 32-byte seed produced + // externally (ZephyrRNG::mixIdentitySeed), instead of through an RNG wrapper. + void fromSeed(const uint8_t seed[SEED_SIZE]); - void sign(uint8_t *sig, const uint8_t *message, int msg_len) const; - void calcSharedSecret(uint8_t *secret, const Identity &other) const { calcSharedSecret(secret, other.pub_key); } - void calcSharedSecret(uint8_t *secret, const uint8_t *other_pub_key) const; - static bool validatePrivateKey(const uint8_t prv[64]); + /** + * \brief Ed25519 digital signature. + * \param sig OUT - must be SIGNATURE_SIZE buffer. + * \param message IN - the raw message bytes to sign. + * \param msg_len IN - the length in bytes of message. + */ + void sign(uint8_t* sig, const uint8_t* message, int msg_len) const; - /* Arduino-compatible *buffer* format: prv || pub (or prv alone when the - * buffer is only PRV_KEY_SIZE). This is protocol-facing — it is what - * CMD_EXPORT/IMPORT_PRIVATE_KEY and the `prv.key` CLI put on the wire — - * so it must stay byte-identical to upstream. Do NOT use it for - * on-flash storage; use readFromStorage()/writeToStorage() instead. */ - bool readFrom(const uint8_t *src, size_t len); - size_t writeTo(uint8_t *dest, size_t max_len) const; + /** + * \brief the ECDH key exhange, with Ed25519 public key transposed to Ex25519. + * \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes) + * \param other IN - the second party in the exchange. + */ + void calcSharedSecret(uint8_t* secret, const Identity& other) const { calcSharedSecret(secret, other.pub_key); } - /* On-flash format: pub || prv, matching Arduino MeshCore's IdentityStore - * (LocalIdentity::writeTo(Stream&)). Kept separate from the buffer - * format above because the two orders differ and conflating them is - * exactly how a node ends up advertising a key it cannot sign for. */ - size_t writeToStorage(uint8_t *dest, size_t max_len) const; + /** + * \brief the ECDH key exhange, with Ed25519 public key transposed to Ex25519. + * \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes) + * \param other_pub_key IN - the public key of second party in the exchange (must be PUB_KEY_SIZE bytes) + */ + void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const; - /* Tolerant counterpart to writeToStorage(). Accepts every layout either - * project has ever written and tells them apart by deriving the pub from - * the candidate prv — the stored pub is a 256-bit checksum over the prv, - * so a wrong guess cannot match: - * - * len == 64 -> prv only (ZephCore repeater/room <= 1.17.x) - * len == 96 -> pub || prv (Arduino MeshCore, ZephCore >= 1.17.2) - * len == 96 -> prv || pub (ZephCore companion <= 1.17.x) - * - * pub_key is always the derived value, never the stored bytes, so a - * loaded identity is coherent by construction. Returns false when no - * layout coheres — the pair is unusable and the caller must not run - * with it. */ - bool readFromStorage(const uint8_t *src, size_t len); + /** + * \brief Validates that a given private key can be used for ECDH / shared-secret operations. + * \param prv IN - the private key to validate (must be PRV_KEY_SIZE bytes) + * \returns true, if the private key is valid for login. + */ + static bool validatePrivateKey(const uint8_t prv[64]); - /* Last resort after readFromStorage() fails: one half of a 96-byte blob - * may still be an intact private key (the other having been clobbered by - * a stale/foreign write). Tests both candidate halves with - * validatePrivateKey() and adopts one only if exactly one qualifies — - * ambiguity means we cannot tell which half is which, and guessing wrong - * would cement a garbage identity. Recovers in RAM only: the file is - * left alone so the evidence survives and the warning repeats each boot. - * Note this changes the node's advertised pub_key to the one its private - * key actually owns; contacts must re-add it either way, since the pub it - * was advertising was never usable. */ - bool recoverFromStorage(const uint8_t *src, size_t len); + // ZEPHCORE: no Arduino Stream I/O. The buffer format below (prv || pub, or prv + // alone) is protocol-facing — CMD_EXPORT/IMPORT_PRIVATE_KEY and the `prv.key` + // CLI — and stays byte-identical to upstream. Never use it for flash storage. + bool readFrom(const uint8_t* src, size_t len); + size_t writeTo(uint8_t* dest, size_t max_len) const; + + // ZEPHCORE: on-flash format pub || prv, matching upstream's IdentityStore + // (LocalIdentity::writeTo(Stream&)). Kept apart from the buffer format because + // the two orders differ, and conflating them is how a node ends up advertising + // a key it cannot sign for. + size_t writeToStorage(uint8_t* dest, size_t max_len) const; + + // Tolerant reader for every layout either project has written, told apart by + // deriving pub from the candidate prv (the stored pub is a checksum of it): + // len == 64 -> prv only (ZephCore repeater/room <= 1.17.x) + // len == 96 -> pub || prv (Arduino MeshCore, ZephCore >= 1.17.2) + // len == 96 -> prv || pub (ZephCore companion <= 1.17.x) + // pub_key is always the derived value. False when no layout coheres. + bool readFromStorage(const uint8_t* src, size_t len); + + // Last resort after readFromStorage() fails: adopt one half of a 96-byte blob + // if exactly one half passes validatePrivateKey() (ambiguity = refuse). RAM + // only; the file is left alone so the evidence survives and the warning + // repeats every boot. Changes the advertised pub_key to the one the private + // key actually owns. + bool recoverFromStorage(const uint8_t* src, size_t len); }; -} /* namespace mesh */ +} + diff --git a/zephcore/include/mesh/Mesh.h b/zephcore/include/mesh/Mesh.h index f116550..0a8352d 100644 --- a/zephcore/include/mesh/Mesh.h +++ b/zephcore/include/mesh/Mesh.h @@ -1,112 +1,250 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Mesh - routing protocol layer - */ - #pragma once #include -#include -#include +#include // ZEPHCORE: adaptive contention window namespace mesh { -struct GroupChannel { - uint8_t hash[PATH_HASH_SIZE]; - uint8_t secret[PUB_KEY_SIZE]; +class GroupChannel { +public: + uint8_t hash[PATH_HASH_SIZE]; + uint8_t secret[PUB_KEY_SIZE]; }; +/** + * An abstraction of the data tables needed to be maintained +*/ class MeshTables { public: - virtual bool wasSeen(const Packet *packet) = 0; /* pure query, no insert */ - virtual void markSeen(const Packet *packet) = 0; /* explicit insert */ - virtual void clear(const Packet *packet) = 0; + virtual bool wasSeen(const Packet* packet) = 0; + virtual void markSeen(const Packet* packet) = 0; + virtual void clear(const Packet* packet) = 0; // remove this packet hash from table + // ZEPHCORE: wasSeen() is a pure query; every insert is an explicit markSeen(). }; +/** + * \brief The next layer in the basic Dispatcher task, Mesh recognises the particular Payload TYPES, + * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. +*/ class Mesh : public Dispatcher { - RNG *_rng; - RTCClock *_rtc; - MeshTables *_tables; + RNG* _rng; // ZEPHCORE: declared in constructor-init order (-Wreorder); + RTCClock* _rtc; // upstream declares _rtc first + MeshTables* _tables; - void removeSelfFromPath(Packet *packet); - void routeDirectRecvAcks(Packet *packet, uint32_t delay_millis); - DispatcherAction forwardMultipartDirect(Packet *pkt); + void removeSelfFromPath(Packet* packet); + void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); + //void routeRecvAcks(Packet* packet, uint32_t delay_millis); + DispatcherAction forwardMultipartDirect(Packet* pkt); protected: - ContentionTracker _contention; -public: - /* Made public so the UI layer can query/extract per-packet dupe counts - * for outbound-flood feedback (joystick channel-send "heard a repeat?"). */ - ContentionTracker& getContentionTracker() { return _contention; } - const ContentionTracker& getContentionTracker() const { return _contention; } -protected: - void extendPendingRetransmit(uint32_t hash32); + // ZEPHCORE: adaptive contention window — dupe tracking, reactive backoff, + // adaptive retransmit jitter (docs: ADAPTIVE contention, LLD 02). + ContentionTracker _contention; + void extendPendingRetransmit(uint32_t hash32); + uint32_t computeAdaptiveFloodDelay(const Packet* packet); + uint32_t computeAdaptiveDirectDelay(const Packet* packet); + // Track heard floods we don't forward, so the EMA warms on non-relaying nodes. + virtual bool passivelyTrackFloods() const { return false; } + // Added to the caller's delay on every sendFlood(); companions spread their + // originated floods, repeaters keep 0. + virtual uint32_t getInitialFloodJitter(const Packet* packet) { return 0; } - DispatcherAction onRecvPacket(Packet *pkt) override; - virtual uint32_t getCADFailRetryDelay() const override; - virtual DispatcherAction routeRecvPacket(Packet *packet); - virtual bool filterRecvFloodPacket(Packet *packet) { return false; } - virtual bool allowPacketForward(const Packet *packet); - virtual uint32_t getRetransmitDelay(const Packet *packet); - virtual uint32_t getDirectRetransmitDelay(const Packet *packet) { return 0; } - /* Shared adaptive retransmit-delay math. CompanionMesh and RepeaterMesh - * had byte-identical overrides of getRetransmitDelay/getDirectRetransmitDelay; - * both now delegate here. */ - uint32_t computeAdaptiveFloodDelay(const Packet *packet); - uint32_t computeAdaptiveDirectDelay(const Packet *packet); - /* Passive contention tracking: if true, track heard floods we don't forward - * (warms the contention EMA on nodes that don't relay, e.g. companions). */ - virtual bool passivelyTrackFloods() const { return false; } - /* Added to caller-supplied delay on every sendFlood. Default 0 (repeater - * behavior). Companion overrides to spread its initial TX adaptively. */ - virtual uint32_t getInitialFloodJitter(const Packet *packet) { (void)packet; return 0; } - virtual uint8_t getExtraAckTransmitCount() const { return 0; } - virtual int searchPeersByHash(const uint8_t *hash) { (void)hash; return 0; } - virtual void getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { (void)dest_secret; (void)peer_idx; } - virtual void onPeerDataRecv(Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, uint8_t *data, size_t len) { (void)packet; (void)type; (void)sender_idx; (void)secret; (void)data; (void)len; } - virtual void onTraceRecv(Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { (void)packet; (void)tag; (void)auth_code; (void)flags; (void)path_snrs; (void)path_hashes; (void)path_len; } - virtual bool onPeerPathRecv(Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender_idx; (void)secret; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; return false; } - virtual void onAdvertRecv(Packet *packet, const Identity &id, uint32_t timestamp, const uint8_t *app_data, size_t app_data_len) { (void)packet; (void)id; (void)timestamp; (void)app_data; (void)app_data_len; } - virtual void onAnonDataRecv(Packet *packet, const uint8_t *secret, const Identity &sender, uint8_t *data, size_t len) { (void)packet; (void)secret; (void)sender; (void)data; (void)len; } - virtual void onPathRecv(Packet *packet, Identity &sender, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; } - virtual void onControlDataRecv(Packet *packet) { (void)packet; } - virtual void onRawDataRecv(Packet *packet) { (void)packet; } - virtual int searchChannelsByHash(const uint8_t *hash, GroupChannel channels[], int max_matches) { (void)hash; (void)channels; (void)max_matches; return 0; } - virtual void onGroupDataRecv(Packet *packet, uint8_t type, const GroupChannel &channel, uint8_t *data, size_t len) { (void)packet; (void)type; (void)channel; (void)data; (void)len; } - virtual void onAckRecv(Packet *packet, uint32_t ack_crc) { (void)packet; (void)ack_crc; } + DispatcherAction onRecvPacket(Packet* pkt) override; + + virtual uint32_t getCADFailRetryDelay() const override; + + /** + * \brief Decide what to do with received packet, ie. discard, forward, or hold + */ + DispatcherAction routeRecvPacket(Packet* packet); + + /** + * \brief Called _before_ the packet is dispatched to the on..Recv() methods. + * \returns true, if given packet should NOT be processed. + */ + virtual bool filterRecvFloodPacket(Packet* packet) { return false; } + + /** + * \brief Check whether this packet should be forwarded (re-transmitted) or not. + * Is sub-classes responsibility to make sure given packet is only transmitted ONCE (by this node) + */ + virtual bool allowPacketForward(const Packet* packet); + + /** + * \returns number of milliseconds delay to apply to retransmitting the given packet. + */ + virtual uint32_t getRetransmitDelay(const Packet* packet); + + /** + * \returns number of milliseconds delay to apply to retransmitting the given packet, for DIRECT mode. + */ + virtual uint32_t getDirectRetransmitDelay(const Packet* packet); + + /** + * \returns number of extra (Direct) ACK transmissions wanted. + */ + virtual uint8_t getExtraAckTransmitCount() const; + + /** + * \brief Perform search of local DB of peers/contacts. + * \returns Number of peers with matching hash + */ + virtual int searchPeersByHash(const uint8_t* hash); + + /** + * \brief lookup the ECDH shared-secret between this node and peer by idx (calculate if necessary) + * \param dest_secret destination array to copy the secret (must be PUB_KEY_SIZE bytes) + * \param peer_idx index of peer, [0..n) where n is what searchPeersByHash() returned + */ + virtual void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { } + + /** + * \brief A (now decrypted) data packet has been received (by a known peer). + * NOTE: these can be received multiple times (per sender/msg-id), via different routes + * \param type one of: PAYLOAD_TYPE_TXT_MSG, PAYLOAD_TYPE_REQ, PAYLOAD_TYPE_RESPONSE + * \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned + * \param secret the pre-calculated shared-secret (handy for sending response packet) + * \param data decrypted data from payload + */ + virtual void onPeerDataRecv(Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) { } + + /** + * \brief A TRACE packet has been received. (and has reached the end of its given path) + * NOTE: this may have been initiated by another node. + * \param tag a random (unique-ish) tag set by initiator + * \param auth_code a code to authenticate the packet + * \param flags zero for now + * \param path_snrs single byte SNR*4 for each hop in the path + * \param path_hashes hashes of each repeater in the path + * \param path_len length of the path_snrs[] and path_hashes[] arrays + */ + virtual void onTraceRecv(Packet* packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t* path_snrs, const uint8_t* path_hashes, uint8_t path_len) { } + + /** + * \brief A path TO peer (sender_idx) has been received. (also with optional 'extra' data encoded) + * NOTE: these can be received multiple times (per sender), via different routes + * \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned + * \param secret the pre-calculated shared-secret (handy for sending response packet) + * \returns true, if path was accepted and that reciprocal path should be sent + */ + virtual bool onPeerPathRecv(Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { return false; } + + /** + * \brief A new incoming Advertisement has been received. + * NOTE: these can be received multiple times (per id/timestamp), via different routes + */ + virtual void onAdvertRecv(Packet* packet, const Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) { } + + /** + * \brief A (now decrypted) data packet has been received. + * NOTE: these can be received multiple times (per sender/contents), via different routes + * \param secret ECDH shared secret + * \param sender public key provided by sender + */ + virtual void onAnonDataRecv(Packet* packet, const uint8_t* secret, const Identity& sender, uint8_t* data, size_t len) { } + + /** + * \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded) + * NOTE: these can be received multiple times (per sender), via different routes + */ + virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { } + + /** + * \brief A control packet has been received. + */ + virtual void onControlDataRecv(Packet* packet) { } + + /** + * \brief A packet with PAYLOAD_TYPE_RAW_CUSTOM has been received. + */ + virtual void onRawDataRecv(Packet* packet) { } + + /** + * \brief Perform search of local DB of matching GroupChannels. + * \param channels OUT - store matching channels in this array, up to max_matches + * \returns Number of channels with matching hash + */ + virtual int searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int max_matches); + + /** + * \brief An encrypted group data packet has been received. + * NOTE: the same payload can be received multiple times, via different routes + * \param type one of: PAYLOAD_TYPE_GRP_TXT, PAYLOAD_TYPE_GRP_DATA + * \param channel the matching GroupChannel + */ + virtual void onGroupDataRecv(Packet* packet, uint8_t type, const GroupChannel& channel, uint8_t* data, size_t len) { } + + /** + * \brief A simple ACK packet has been received. + * NOTE: same ACK can be received multiple times, via different routes + */ + virtual void onAckRecv(Packet* packet, uint32_t ack_crc) { } + + Mesh(Radio& radio, MillisecondClock& ms, RNG& rng, RTCClock& rtc, PacketManager& mgr, MeshTables& tables) + : Dispatcher(radio, ms, mgr), _rng(&rng), _rtc(&rtc), _tables(&tables) + { + } + + MeshTables* getTables() const { return _tables; } public: - Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables); - void begin(); - void loop(); - void maintenanceLoop(); - uint32_t msUntilNextMaintenance() override; + void begin(); + void loop(); - LocalIdentity self_id; + LocalIdentity self_id; - RNG *getRNG() const { return _rng; } - RTCClock *getRTCClock() const { return _rtc; } - MeshTables *getTables() const { return _tables; } + RNG* getRNG() const { return _rng; } + RTCClock* getRTCClock() const { return _rtc; } - Packet *createAdvert(const LocalIdentity &id, const uint8_t *app_data = nullptr, size_t app_data_len = 0); - Packet *createAck(const uint8_t *ack, uint8_t len); - Packet *createAck(uint32_t ack_crc) { return createAck((const uint8_t *)&ack_crc, 4); } - Packet *createMultiAck(const uint8_t *ack, uint8_t len, uint8_t remaining); - Packet *createMultiAck(uint32_t ack_crc, uint8_t remaining) { return createMultiAck((const uint8_t *)&ack_crc, 4, remaining); } - Packet *createControlData(const uint8_t *data, size_t len); - Packet *createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t len); - Packet *createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len); - Packet *createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len); - Packet *createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len); - Packet *createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len); - Packet *createRawData(const uint8_t *data, size_t len); - Packet *createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0); + // ZEPHCORE: deadline-driven maintenance (see Dispatcher). + void maintenanceLoop(); + uint32_t msUntilNextMaintenance() override; + + // ZEPHCORE: the UI asks for per-packet dupe counts ("heard a repeat?"). + ContentionTracker& getContentionTracker() { return _contention; } + const ContentionTracker& getContentionTracker() const { return _contention; } + + Packet* createAdvert(const LocalIdentity& id, const uint8_t* app_data=NULL, size_t app_data_len=0); + Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len); + Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len); + Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len); + Packet* createAck(const uint8_t* ack, uint8_t len); + Packet* createAck(uint32_t ack_crc) { return createAck((uint8_t *) &ack_crc, 4); } + Packet* createMultiAck(const uint8_t* ack, uint8_t len, uint8_t remaining); + Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining) { return createMultiAck((uint8_t *)&ack_crc, 4, remaining); } + Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); + Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); + Packet* createRawData(const uint8_t* data, size_t len); + Packet* createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0); + Packet* createControlData(const uint8_t* data, size_t len); + + /** + * \brief send a locally-generated Packet with flood routing + */ + void sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_hash_size=1); + + /** + * \brief send a locally-generated Packet with flood routing + * \param transport_codes array of 2 codes to attach to packet + */ + void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_hash_size=1); + + /** + * \brief send a locally-generated Packet with Direct routing + */ + void sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0); + + /** + * \brief send a locally-generated Packet to just neighbor nodes (zero hops) + */ + void sendZeroHop(Packet* packet, uint32_t delay_millis=0); + + /** + * \brief send a locally-generated Packet to just neighbor nodes (zero hops), with specific transport codes + * \param transport_codes array of 2 codes to attach to packet + */ + void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); - void sendFlood(Packet *packet, uint32_t delay_millis = 0, uint8_t path_hash_size = 1); - void sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0, uint8_t path_hash_size = 1); - void sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis = 0); - void sendZeroHop(Packet *packet, uint32_t delay_millis = 0); - void sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0); }; -} /* namespace mesh */ +} diff --git a/zephcore/include/mesh/MeshCore.h b/zephcore/include/mesh/MeshCore.h index 44c8a39..91d14f6 100644 --- a/zephcore/include/mesh/MeshCore.h +++ b/zephcore/include/mesh/MeshCore.h @@ -1,32 +1,135 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore constants and base types - */ - #pragma once #include +#include #include -/* Neutralize STM32 CMSIS peripheral-instance macros (RNG, AES, ...) that - * collide with portable mesh identifiers. No-op off STM32. */ +// ZEPHCORE: neutralize STM32 CMSIS peripheral-instance macros (RNG, AES, ...) +// that collide with portable mesh identifiers. No-op off STM32. #include -#define MAX_HASH_SIZE 8 /* SHA256 truncated to 8 bytes for dedup */ -#define PUB_KEY_SIZE 32 /* Ed25519 public key */ -#define PRV_KEY_SIZE 64 /* Ed25519 expanded private key */ -#define SEED_SIZE 32 /* Ed25519 seed */ -#define SIGNATURE_SIZE 64 /* Ed25519 signature */ +#define MAX_HASH_SIZE 8 +#define PUB_KEY_SIZE 32 +#define PRV_KEY_SIZE 64 +#define SEED_SIZE 32 +#define SIGNATURE_SIZE 64 #define MAX_ADVERT_DATA_SIZE 32 -#define CIPHER_KEY_SIZE 16 /* AES-128 */ -#define CIPHER_BLOCK_SIZE 16 /* AES block size */ -#define CIPHER_MAC_SIZE 2 /* truncated MAC for bandwidth savings */ -#define PATH_HASH_SIZE 1 /* 1-byte per-hop hash in path field */ +#define CIPHER_KEY_SIZE 16 +#define CIPHER_BLOCK_SIZE 16 -#define MAX_PACKET_PAYLOAD 184 /* fits in SX126x 255-byte FIFO with headers */ -#define MAX_PATH_SIZE 64 /* max hops * max hash size */ -#define MAX_TRANS_UNIT 255 /* SX126x FIFO limit */ -#define MAX_GROUP_DATA_LENGTH (MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3) +// V1 +#define CIPHER_MAC_SIZE 2 +#define PATH_HASH_SIZE 1 -#define MESH_DEBUG_PRINT(...) -#define MESH_DEBUG_PRINTLN(...) +#define MAX_PACKET_PAYLOAD 184 +#define MAX_GROUP_DATA_LENGTH (MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3) +#define MAX_PATH_SIZE 64 +#define MAX_TRANS_UNIT 255 + +#if MESH_DEBUG && ARDUINO + #include + #define MESH_DEBUG_PRINT(F, ...) Serial.printf("DEBUG: " F, ##__VA_ARGS__) + #define MESH_DEBUG_PRINTLN(F, ...) Serial.printf("DEBUG: " F "\n", ##__VA_ARGS__) +#elif defined(__ZEPHYR__) + // ZEPHCORE: upstream's debug prints become Zephyr debug logs, compiled out + // unless the file's log module is at DBG. The using file must register a + // log module (LOG_MODULE_REGISTER / LOG_MODULE_DECLARE). + #include + #define MESH_DEBUG_PRINT(F, ...) LOG_DBG(F, ##__VA_ARGS__) + #define MESH_DEBUG_PRINTLN(F, ...) LOG_DBG(F, ##__VA_ARGS__) +#else + #define MESH_DEBUG_PRINT(...) {} + #define MESH_DEBUG_PRINTLN(...) {} +#endif + +#if BRIDGE_DEBUG && ARDUINO +#define BRIDGE_DEBUG_PRINTLN(F, ...) Serial.printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__) +#else +#define BRIDGE_DEBUG_PRINTLN(...) {} +#endif + +namespace mesh { + +#define BD_STARTUP_NORMAL 0 // getStartupReason() codes +#define BD_STARTUP_RX_PACKET 1 + +class MainBoard { +public: + virtual uint16_t getBattMilliVolts() = 0; + // ZEPHCORE: battery percentage from the board's discharge curve. + virtual uint8_t getBattPercent() { return 0; } + virtual float getMCUTemperature() { return NAN; } + virtual bool setAdcMultiplier(float multiplier) { return false; }; + virtual float getAdcMultiplier() const { return 0.0f; } + virtual const char* getManufacturerName() const = 0; + virtual void onBeforeTransmit() { } + virtual void onAfterTransmit() { } + // ZEPHCORE: a valid packet has just been received. Unlike the transmit pair + // this is a single edge, not a window: the packet is already over by the time + // the radio reports it, so an LED implementation fires a one-shot. + virtual void onPacketReceived() { } + virtual void reboot() = 0; + virtual void powerOff() { /* no op */ } + // Called by example setup() functions to signal that boot is complete. + // Boards may override to stop a boot-indicator LED sequence or similar. + // Default no-op: boards that don't care need not implement anything. + virtual void onBootComplete() { /* no op */ } + virtual uint32_t getIRQGpio() { return -1; } // not supported. Returns DIO1 (SX1262) and DIO0 (SX127x) + virtual void sleep(uint32_t secs) { /* no op */ } + virtual uint32_t getGpio() { return 0; } + virtual void setGpio(uint32_t values) {} + virtual uint8_t getStartupReason() const = 0; + virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } + virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported + + // Power management interface (boards with power management override these) + virtual bool isPwrMgtInitialised() const { return false; } + virtual bool isExternalPowered() { return false; } + virtual uint16_t getBootVoltage() { return 0; } + virtual bool getWakeLpcompSupported() const { return false; } + virtual uint32_t getResetReason() const { return 0; } + virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } + virtual uint8_t getShutdownReason() const { return 0; } + virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } + + virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; } + + // Called from the example main loops. Lets a board feed its watchdog and + // run periodic housekeeping. Default no-op. + virtual void loop() { /* no op */ } +}; + +/** + * An abstraction of the device's Realtime Clock. +*/ +class RTCClock { + uint32_t last_unique; +protected: + RTCClock() { last_unique = 0; } + +public: + /** + * \returns the current time. in UNIX epoch seconds. + */ + virtual uint32_t getCurrentTime() = 0; + + /** + * \param time current time in UNIX epoch seconds. + */ + virtual void setCurrentTime(uint32_t time) = 0; + + /** + * override in classes that need to periodically update internal state + */ + virtual void tick() { /* no op */} + + uint32_t getCurrentTimeUnique() { + uint32_t t = getCurrentTime(); + if (t <= last_unique) { + return ++last_unique; + } + return last_unique = t; + } +}; + +} diff --git a/zephcore/include/mesh/Packet.h b/zephcore/include/mesh/Packet.h index 730119e..ee34670 100644 --- a/zephcore/include/mesh/Packet.h +++ b/zephcore/include/mesh/Packet.h @@ -1,92 +1,125 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Packet - fundamental transmission unit - */ - #pragma once #include -#include #include namespace mesh { -#define PH_ROUTE_MASK 0x03 -#define PH_TYPE_SHIFT 2 -#define PH_TYPE_MASK 0x0F -#define PH_VER_SHIFT 6 -#define PH_VER_MASK 0x03 +// Packet::header values +#define PH_ROUTE_MASK 0x03 // 2-bits +#define PH_TYPE_SHIFT 2 +#define PH_TYPE_MASK 0x0F // 4-bits +#define PH_VER_SHIFT 6 +#define PH_VER_MASK 0x03 // 2-bits -#define ROUTE_TYPE_TRANSPORT_FLOOD 0x00 -#define ROUTE_TYPE_FLOOD 0x01 -#define ROUTE_TYPE_DIRECT 0x02 -#define ROUTE_TYPE_TRANSPORT_DIRECT 0x03 +#define ROUTE_TYPE_TRANSPORT_FLOOD 0x00 // flood mode + transport codes +#define ROUTE_TYPE_FLOOD 0x01 // flood mode, needs 'path' to be built up (max 64 bytes) +#define ROUTE_TYPE_DIRECT 0x02 // direct route, 'path' is supplied +#define ROUTE_TYPE_TRANSPORT_DIRECT 0x03 // direct route + transport codes -#define PAYLOAD_TYPE_REQ 0x00 -#define PAYLOAD_TYPE_RESPONSE 0x01 -#define PAYLOAD_TYPE_TXT_MSG 0x02 -#define PAYLOAD_TYPE_ACK 0x03 -#define PAYLOAD_TYPE_ADVERT 0x04 -#define PAYLOAD_TYPE_GRP_TXT 0x05 -#define PAYLOAD_TYPE_GRP_DATA 0x06 -#define PAYLOAD_TYPE_ANON_REQ 0x07 -#define PAYLOAD_TYPE_PATH 0x08 -#define PAYLOAD_TYPE_TRACE 0x09 -#define PAYLOAD_TYPE_MULTIPART 0x0A -#define PAYLOAD_TYPE_CONTROL 0x0B -#define PAYLOAD_TYPE_RAW_CUSTOM 0x0F +#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) +#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) +#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text) +#define PAYLOAD_TYPE_ACK 0x03 // a simple ack +#define PAYLOAD_TYPE_ADVERT 0x04 // a node advertising its Identity +#define PAYLOAD_TYPE_GRP_TXT 0x05 // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg") +#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: data_type(uint16), data_len, blob) +#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...) +#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra) +#define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNR for each hop +#define PAYLOAD_TYPE_MULTIPART 0x0A // packet is one of a set of packets +#define PAYLOAD_TYPE_CONTROL 0x0B // a control/discovery packet +//... +#define PAYLOAD_TYPE_RAW_CUSTOM 0x0F // custom packet as raw bytes, for applications with custom encryption, payloads, etc -#define PAYLOAD_VER_1 0x00 -#define PAYLOAD_VER_2 0x01 -#define PAYLOAD_VER_3 0x02 -#define PAYLOAD_VER_4 0x03 +#define PAYLOAD_VER_1 0x00 // 1-byte src/dest hashes, 2-byte MAC +#define PAYLOAD_VER_2 0x01 // FUTURE (eg. 2-byte hashes, 4-byte MAC ??) +#define PAYLOAD_VER_3 0x02 // FUTURE +#define PAYLOAD_VER_4 0x03 // FUTURE +/** + * \brief The fundamental transmission unit. +*/ class Packet { public: - Packet(); + Packet(); - uint8_t header; - uint16_t payload_len; - uint8_t path_len; - uint16_t transport_codes[2]; - uint8_t path[MAX_PATH_SIZE]; - uint8_t payload[MAX_PACKET_PAYLOAD]; - int8_t _snr; /* SNR * 4 (quarter-dB fixed point) */ + uint8_t header; + // ZEPHCORE: path_len stays uint8_t (the wire byte); upstream widened it to + // uint16_t, which changes the wrap behaviour of the TRACE path arithmetic. + uint16_t payload_len; + uint8_t path_len; + uint16_t transport_codes[2]; + uint8_t path[MAX_PATH_SIZE]; + uint8_t payload[MAX_PACKET_PAYLOAD]; + int8_t _snr; - void calculatePacketHash(uint8_t *dest_hash) const; - uint8_t getRouteType() const { return header & PH_ROUTE_MASK; } - bool isRouteFlood() const { return getRouteType() == ROUTE_TYPE_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD; } - bool isRouteDirect() const { return getRouteType() == ROUTE_TYPE_DIRECT || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; } - bool hasTransportCodes() const { return getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; } - uint8_t getPayloadType() const { return (header >> PH_TYPE_SHIFT) & PH_TYPE_MASK; } - uint8_t getPayloadVer() const { return (header >> PH_VER_SHIFT) & PH_VER_MASK; } + /** + * \brief calculate the hash of payload + type + * \param dest_hash destination to store the hash (must be MAX_HASH_SIZE bytes) + */ + void calculatePacketHash(uint8_t* dest_hash) const; - /* path_len layout: bits[7:6] = hash_size - 1, bits[5:0] = hop count */ - uint8_t getPathHashSize() const { return (path_len >> 6) + 1; } - uint8_t getPathHashCount() const { return path_len & 0x3F; } - uint8_t getPathByteLen() const { return getPathHashCount() * getPathHashSize(); } - void setPathHashCount(uint8_t n) { path_len &= ~0x3F; path_len |= n; } - void setPathHashSizeAndCount(uint8_t sz, uint8_t n) { path_len = ((sz - 1) << 6) | (n & 0x3F); } + /** + * \returns one of ROUTE_ values + */ + uint8_t getRouteType() const { return header & PH_ROUTE_MASK; } - /* writePath / copyPath decode the 6-bit hash_count + 2-bit hash_size - * encoding in path_len, then copy hash_count*hash_size bytes from src - * to dest. src_len bounds the source: if the decoded byte count would - * read past src_len, the call returns 0 (no copy). - * - * Trusted callers writing from internal MAX_PATH_SIZE buffers must - * pass MAX_PATH_SIZE. Callers handling untrusted input (BLE phone - * frames, LoRa anon-request payloads) must pass the real remaining - * length so a malformed path_len cannot trigger an OOB read. */ - static size_t writePath(uint8_t *dest, const uint8_t *src, size_t src_len, uint8_t path_len); - static uint8_t copyPath(uint8_t *dest, const uint8_t *src, size_t src_len, uint8_t path_len); - static bool isValidPathLen(uint8_t path_len); + bool isRouteFlood() const { return getRouteType() == ROUTE_TYPE_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD; } + bool isRouteDirect() const { return getRouteType() == ROUTE_TYPE_DIRECT || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; } - void markDoNotRetransmit() { header = 0xFF; } - bool isMarkedDoNotRetransmit() const { return header == 0xFF; } - float getSNR() const { return ((float)_snr) / 4.0f; } - int getRawLength() const; - uint8_t writeTo(uint8_t dest[]) const; - bool readFrom(const uint8_t src[], uint8_t len); + bool hasTransportCodes() const { return getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; } + + /** + * \returns one of PAYLOAD_TYPE_ values + */ + uint8_t getPayloadType() const { return (header >> PH_TYPE_SHIFT) & PH_TYPE_MASK; } + + /** + * \returns one of PAYLOAD_VER_ values + */ + uint8_t getPayloadVer() const { return (header >> PH_VER_SHIFT) & PH_VER_MASK; } + + uint8_t getPathHashSize() const { return (path_len >> 6) + 1; } + uint8_t getPathHashCount() const { return path_len & 63; } + uint8_t getPathByteLen() const { return getPathHashCount() * getPathHashSize(); } + void setPathHashCount(uint8_t n) { path_len &= ~63; path_len |= n; } + void setPathHashSizeAndCount(uint8_t sz, uint8_t n) { path_len = ((sz - 1) << 6) | (n & 63); } + + // ZEPHCORE: src_len bounds the source. Trusted callers copying from an + // internal MAX_PATH_SIZE buffer pass MAX_PATH_SIZE; callers handling + // untrusted input (phone frames, anon-request payloads) pass the real + // remaining length, so a malformed path_len cannot read out of bounds. + static uint8_t copyPath(uint8_t* dest, const uint8_t* src, size_t src_len, uint8_t path_len); // returns path_len, 0 if rejected + static size_t writePath(uint8_t* dest, const uint8_t* src, size_t src_len, uint8_t path_len); // returns byte length written + // ZEPHCORE: upstream's signatures, for trusted sources (internal MAX_PATH_SIZE buffers). + static uint8_t copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { return copyPath(dest, src, MAX_PATH_SIZE, path_len); } + static size_t writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { return writePath(dest, src, MAX_PATH_SIZE, path_len); } + static bool isValidPathLen(uint8_t path_len); + + void markDoNotRetransmit() { header = 0xFF; } + bool isMarkedDoNotRetransmit() const { return header == 0xFF; } + + float getSNR() const { return ((float)_snr) / 4.0f; } + + /** + * \returns the encoded/wire format length of this packet + */ + int getRawLength() const; + + /** + * \brief save entire packet as a blob + * \param dest (OUT) destination buffer (assumed to be MAX_MTU_SIZE) + * \returns the packet length + */ + uint8_t writeTo(uint8_t dest[]) const; + + /** + * \brief restore this packet from a blob (as created using writeTo()) + * \param src (IN) buffer containing blob + * \param len the packet length (as returned by writeTo()) + */ + bool readFrom(const uint8_t src[], uint8_t len); }; -} /* namespace mesh */ +} diff --git a/zephcore/include/mesh/RNG.h b/zephcore/include/mesh/RNG.h deleted file mode 100644 index 60c5496..0000000 --- a/zephcore/include/mesh/RNG.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore RNG interface - matches Utils.h - */ - -#pragma once - -#include -#include - -/* On STM32, CMSIS defines an object-like macro `RNG` that collides with the - * mesh::RNG class below. The fixup header neutralizes it order-independently. */ -#include - -namespace mesh { - -class RNG { -public: - virtual void random(uint8_t *dest, size_t sz) = 0; - - uint32_t nextInt(uint32_t _min, uint32_t _max); -}; - -} /* namespace mesh */ diff --git a/zephcore/include/mesh/RTC.h b/zephcore/include/mesh/RTC.h deleted file mode 100644 index 88799d1..0000000 --- a/zephcore/include/mesh/RTC.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore RTC interface - matches MeshCore.h - */ - -#pragma once - -#include - -namespace mesh { - -class RTCClock { - uint32_t last_unique; - -protected: - RTCClock() : last_unique(0) {} - -public: - virtual uint32_t getCurrentTime() = 0; - virtual void setCurrentTime(uint32_t time) = 0; - virtual void tick() {} - - uint32_t getCurrentTimeUnique() { - uint32_t t = getCurrentTime(); - if (t <= last_unique) { - return ++last_unique; - } - return last_unique = t; - } -}; - -} /* namespace mesh */ diff --git a/zephcore/include/mesh/Radio.h b/zephcore/include/mesh/Radio.h deleted file mode 100644 index a5587ed..0000000 --- a/zephcore/include/mesh/Radio.h +++ /dev/null @@ -1,111 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Radio interface - matches Dispatcher.h - */ - -#pragma once - -#include -#include - -namespace mesh { - -class Radio { -public: - virtual void begin() {} - - virtual int recvRaw(uint8_t *bytes, int sz) = 0; - virtual uint32_t getEstAirtimeFor(int len_bytes) = 0; - virtual float packetScore(float snr, int packet_len) = 0; - virtual bool startSendRaw(const uint8_t *bytes, int len) = 0; - /* One-shot: returns true exactly once per completed transmit, and - * consumes that completion. Matches Arduino MeshCore's - * RadioLibWrapper::isSendComplete(), which self-clears and owns the - * packets-sent counter, so the radio's tally and the dispatcher's - * flood/direct tallies can never drift apart. Never call this as a - * state query -- use isTxActive() for that. */ - virtual bool isSendComplete() = 0; - /* Non-consuming "a transmit is in flight" query, for callers that want - * the radio's state rather than the completion event. */ - virtual bool isTxActive() const { return false; } - virtual void onSendFinished() = 0; - - virtual int getNoiseFloor() const { return 0; } - virtual void triggerNoiseFloorCalibrate(int threshold) { (void)threshold; } - - virtual bool isInRecvMode() const = 0; - virtual bool isReceiving() { return false; } - virtual bool isRadioReady() { return true; } - - /* Reset the radio back into a known good RX state. Called by the - * Dispatcher on CAD timeout (when isReceiving() pinned true past the - * recovery threshold). Default no-op — radios that can stall should - * override to walk the chip through cancel → REST → fresh RX, which - * also bulk-clears IRQ status and any internal busy latches. */ - virtual void recoverRxState() {} - virtual float getLastRSSI() const { return 0; } - virtual float getLastSNR() const { return 0; } - - /* Adaptive Power Control */ - - /* Adaptive CAD (LBT detPeak calibration). Default no-ops for radios - * without hardware CAD (SX127x). */ - /* stored_base is the family base detPeak `offset` was learned against, - * or 0 when none was recorded. When it disagrees with the base the - * driver reports now — i.e. the base table moved under a stored offset — - * the implementation re-anchors so the ABSOLUTE detPeak is preserved. - * Read cadOffset()/cadBasePeak() afterwards to persist the corrected - * pair. */ - virtual void setCadParams(bool auto_enabled, int8_t offset, - uint16_t probe_interval_s, uint8_t busycap_pct, - uint8_t stored_base = 0) { - (void)auto_enabled; (void)offset; (void)probe_interval_s; - (void)busycap_pct; (void)stored_base; - } - /* Family base detPeak actually in force. Pair it with getCadOffset() to - * persist the (base, offset) the node is really operating at; 0 means the - * radio has no adaptive CAD, so there is nothing to store. */ - virtual uint8_t cadBasePeak() { return 0; } - /* One housekeeping tick of the CAD calibrator: maybe run a probe, - * update stats, maybe step the staircase (auto mode). */ - virtual void cadMaintenance() {} - - /** Receiver hygiene: deaf-aware AGC unstick and temperature-drift - * recalibration. Both sleep the radio, so this is called only from the - * maintenance tick, never from a packet path. Default: no-op. */ - virtual void radioMaintenance() {} - - /* Milliseconds until this radio's periodic work (noise floor sampling, - * CAD probing/decay) next needs a call, or MAINTENANCE_IDLE when it has - * nothing pending. Radios with no periodic work keep the default. */ - virtual uint32_t msUntilNextMaintenance() { return MAINTENANCE_IDLE; } - virtual int8_t getCadOffset() const { return 0; } - virtual void resetCadStats() {} - /* Last-resort unmute: step the CAD detect threshold one notch LESS - * sensitive, ignoring whether adaptive CAD is enabled. Called by the - * dispatcher only after the driver's LBT has refused every transmit for - * getTxStarvationDuration(). Returns false when already at the radio's - * least sensitive step, which tells the caller the channel is genuinely - * busy (or the radio is broken) rather than the detector mis-tuned. */ - virtual bool cadRelaxOnTxStarvation() { return false; } - /* Writes a human-readable status block; returns chars written (0 = not - * supported by this radio). */ - virtual int formatCadStatus(char *buf, int cap) { - (void)buf; (void)cap; return 0; - } - - /* Writes accumulated carrier-frequency-error statistics; returns chars - * written (0 = this radio cannot measure it). Diagnostic only — no - * radio acts on the number, and correcting it is board-dependent: XTAL - * parts have SetXoscCpTrim, TCXO parts have no chip-side trim at all. */ - virtual int formatFreqErrorStatus(char *buf, int cap) { - (void)buf; (void)cap; return 0; - } - - /* Packet statistics */ - virtual uint32_t getPacketsRecv() const { return 0; } - virtual uint32_t getPacketsSent() const { return 0; } - virtual uint32_t getPacketsRecvErrors() const { return 0; } -}; - -} /* namespace mesh */ diff --git a/zephcore/include/mesh/SimpleMeshTables.h b/zephcore/include/mesh/SimpleMeshTables.h index 70df91e..e199466 100644 --- a/zephcore/include/mesh/SimpleMeshTables.h +++ b/zephcore/include/mesh/SimpleMeshTables.h @@ -1,69 +1,64 @@ -/* - * SPDX-License-Identifier: MIT - * SimpleMeshTables - hash-based packet deduplication - */ - #pragma once #include #include -namespace mesh { - #define MAX_PACKET_HASHES (128+32) -class SimpleMeshTables : public MeshTables { - uint8_t _hashes[MAX_PACKET_HASHES * MAX_HASH_SIZE]; - int _next_idx; - uint32_t _direct_dups, _flood_dups; +class SimpleMeshTables : public mesh::MeshTables { + uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; + int _next_idx; + uint32_t _direct_dups, _flood_dups; public: - SimpleMeshTables() { - memset(_hashes, 0, sizeof(_hashes)); - _next_idx = 0; - _direct_dups = _flood_dups = 0; - } + SimpleMeshTables() { + memset(_hashes, 0, sizeof(_hashes)); + _next_idx = 0; + _direct_dups = _flood_dups = 0; + } - bool wasSeen(const Packet *packet) override { - uint8_t hash[MAX_HASH_SIZE]; - packet->calculatePacketHash(hash); - const uint8_t *sp = _hashes; - for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - if (packet->isRouteDirect()) { - _direct_dups++; - } else { - _flood_dups++; - } - return true; - } - } - return false; - } + // ZEPHCORE: no restoreFrom/saveTo (Arduino FS on ESP32); the table is RAM-only. - void markSeen(const Packet *packet) override { - uint8_t hash[MAX_HASH_SIZE]; - packet->calculatePacketHash(hash); - memcpy(&_hashes[_next_idx * MAX_HASH_SIZE], hash, MAX_HASH_SIZE); - _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; - } + bool wasSeen(const mesh::Packet* packet) override { + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); - void clear(const Packet *packet) override { - uint8_t hash[MAX_HASH_SIZE]; - packet->calculatePacketHash(hash); - uint8_t *sp = _hashes; - for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - memset(sp, 0, MAX_HASH_SIZE); - break; - } - } - } + const uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + if (packet->isRouteDirect()) { + _direct_dups++; + } else { + _flood_dups++; + } + return true; + } + } + return false; + } - uint32_t getNumDirectDups() const { return _direct_dups; } - uint32_t getNumFloodDups() const { return _flood_dups; } + void markSeen(const mesh::Packet* packet) override { + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + memcpy(&_hashes[_next_idx * MAX_HASH_SIZE], hash, MAX_HASH_SIZE); + _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; + } - void resetStats() { _direct_dups = _flood_dups = 0; } + void clear(const mesh::Packet* packet) override { + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + + uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + memset(sp, 0, MAX_HASH_SIZE); + break; + } + } + } + + uint32_t getNumDirectDups() const { return _direct_dups; } + uint32_t getNumFloodDups() const { return _flood_dups; } + + void resetStats() { _direct_dups = _flood_dups = 0; } }; - -} /* namespace mesh */ diff --git a/zephcore/include/mesh/StaticPoolPacketManager.h b/zephcore/include/mesh/StaticPoolPacketManager.h index 895e263..1f94470 100644 --- a/zephcore/include/mesh/StaticPoolPacketManager.h +++ b/zephcore/include/mesh/StaticPoolPacketManager.h @@ -1,28 +1,24 @@ -/* - * SPDX-License-Identifier: MIT - * Static pool PacketManager - */ - #pragma once #include -namespace mesh { +// ZEPHCORE: the queues and the pool are static and live in the .cpp — no heap in +// the packet path. Pool and queue sizes are compile-time (32 each). -class StaticPoolPacketManager : public PacketManager { +class StaticPoolPacketManager : public mesh::PacketManager { public: - Packet *allocNew() override; - void free(Packet *packet) override; - void queueOutbound(Packet *packet, uint8_t priority, uint32_t scheduled_for) override; - Packet *getNextOutbound(uint32_t now) override; - int getOutboundCount(uint32_t now) const override; - int getOutboundTotal() const override; - int getFreeCount() const override; - Packet *getOutboundByIdx(int i) override; - Packet *removeOutboundByIdx(int i) override; - uint32_t getOutboundSchedule(int i) const override; - bool rescheduleOutbound(int i, uint32_t new_scheduled_for) override; - uint8_t peekNextOutboundPriority(uint32_t now) const override; -}; -} /* namespace mesh */ + mesh::Packet* allocNew() override; + void free(mesh::Packet* packet) override; + void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; + mesh::Packet* getNextOutbound(uint32_t now) override; + int getOutboundCount(uint32_t now) const override; + int getOutboundTotal() const override; + int getFreeCount() const override; + mesh::Packet* getOutboundByIdx(int i) override; + mesh::Packet* removeOutboundByIdx(int i) override; + // ZEPHCORE: no inbound queue (see mesh::PacketManager); schedule access instead. + uint32_t getOutboundSchedule(int i) const override; + bool rescheduleOutbound(int i, uint32_t new_scheduled_for) override; + uint8_t peekNextOutboundPriority(uint32_t now) const override; +}; diff --git a/zephcore/include/mesh/Utils.h b/zephcore/include/mesh/Utils.h index 2a380fc..8e70d0a 100644 --- a/zephcore/include/mesh/Utils.h +++ b/zephcore/include/mesh/Utils.h @@ -1,8 +1,3 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Utils - crypto and helpers (Zephyr port) - */ - #pragma once #include @@ -11,40 +6,93 @@ namespace mesh { -class Utils { +class RNG { public: - static void sha256(uint8_t *hash, size_t hash_len, const uint8_t *msg, int msg_len); - static void sha256(uint8_t *hash, size_t hash_len, const uint8_t *frag1, int frag1_len, const uint8_t *frag2, int frag2_len); - static int encrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len); - static int decrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len); - static int encryptThenMAC(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len); - static int MACThenDecrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len); + virtual void random(uint8_t* dest, size_t sz) = 0; - /* Constant-time byte-equality. Returns true iff every byte of `a` - * matches `b`. No early exit — timing is independent of input, - * defeating timing-leak attacks on MAC/password/secret compares. - * `volatile` accumulator survives `-Os` LTO — disassembly-verified - * on Cortex-M4 (rak3401_1watt). Pattern from rweather/arduinolibs. */ - static bool constantTimeEqual(const void *a, const void *b, size_t n); - - /* Securely zero a buffer such that the compiler cannot elide the - * writes as dead-store optimization. Uses volatile pointer writes — - * standard idiom for clearing crypto secrets before stack unwind. - * Use this for any buffer holding key material, seeds, or shared - * secrets after their last use. */ - static void secureZeroize(void *buf, size_t n); - - /* Log a crypto-invariant failure to printk and cold-reboot. - * Used when an entropy source, KDF, or other primitive cannot - * produce a safe result — proceeding would risk weak keys or - * bypassed authentication, so we restart rather than continue. - * Does not return. */ - [[noreturn]] static void cryptoPanicReboot(const char *msg); - - static void toHex(char *dest, const uint8_t *src, size_t len); - static bool fromHex(uint8_t *dest, int dest_size, const char *src_hex); - static bool isHexChar(char c); - static int parseTextParts(char *text, const char *parts[], int max_num, char separator = ','); + /** + * \returns random number between _min (inclusive) and _max (exclusive) + */ + uint32_t nextInt(uint32_t _min, uint32_t _max); }; -} /* namespace mesh */ +class Utils { +public: + /** + * \brief calculates the SHA256 hash of 'msg', storing in 'hash' and truncating the hash to 'hash_len' bytes. + */ + static void sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len); + + /** + * \brief calculates the SHA256 hash of two fragments, 'frag1' and 'frag2' (in that order), storing in 'hash' and truncating. + */ + static void sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int frag1_len, const uint8_t* frag2, int frag2_len); + + /** + * \brief Encrypts the 'src' bytes using AES128 cipher, using 'shared_secret' as key, with key length fixed at CIPHER_KEY_SIZE. + * Final block is padded with zero bytes before encrypt. Result stored in 'dest'. + * \returns The length in bytes put into 'dest'. (rounded up to block size) + */ + static int encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len); + + /** + * \brief Decrypt the 'src' bytes using AES128 cipher, using 'shared_secret' as key, with key length fixed at CIPHER_KEY_SIZE. + * 'src_len' should be multiple of block size, as returned by 'encrypt()'. + * \returns The length in bytes put into 'dest'. (dest may contain trailing zero bytes in final block) + */ + static int decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len); + + /** + * \brief encrypts bytes in src, then calculates MAC on ciphertext, inserting into leading bytes of 'dest'. + * \returns total length of bytes in 'dest' (MAC + ciphertext) + */ + static int encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len); + + /** + * \brief checks the MAC (in leading bytes of 'src'), then if valid, decrypts remaining bytes in src. + * \returns zero if MAC is invalid, otherwise the length of decrypted bytes in 'dest' + */ + static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len); + + /** + * \brief converts 'src' bytes with given length to Hex representation, and null terminates. + */ + static void toHex(char* dest, const uint8_t* src, size_t len); + + /** + * \brief converts 'src_hex' hexadecimal string (should be null term) back to raw bytes, storing in 'dest'. + * \param dest_size must be exactly the expected size in bytes. + * \returns true if successful + */ + static bool fromHex(uint8_t* dest, int dest_size, const char *src_hex); + + // ZEPHCORE: no printHex(Stream&) (Arduino Stream). + + /** + * \brief parse 'text' into parts separated by 'separator' char. + * \param text the text to parse (note is MODIFIED!) + * \param parts destination array to store pointers to starts of parse parts + * \param max_num max elements to store in 'parts' array + * \param separator the separator character + * \returns the number of parts parsed (in 'parts') + */ + static int parseTextParts(char* text, const char* parts[], int max_num, char separator=','); + + static bool isHexChar(char c); + + static bool isZeroes(const uint8_t* buf, size_t len); + + // ZEPHCORE: crypto hygiene helpers. + // Constant-time equality: no early exit, so timing is independent of the + // input (MAC / password / secret compares). The volatile accumulator survives + // -Os LTO (disassembly-verified on Cortex-M4). Pattern from rweather/arduinolibs. + static bool constantTimeEqual(const void* a, const void* b, size_t n); + // Zero a buffer the compiler cannot elide as a dead store (key material, seeds, + // shared secrets after last use). + static void secureZeroize(void* buf, size_t n); + // An entropy source, KDF or other primitive could not produce a safe result: + // log and cold-reboot rather than risk weak keys or bypassed auth. + [[noreturn]] static void cryptoPanicReboot(const char* msg); +}; + +} diff --git a/zephcore/src/Dispatcher.cpp b/zephcore/src/Dispatcher.cpp index c44c79b..3dc0801 100644 --- a/zephcore/src/Dispatcher.cpp +++ b/zephcore/src/Dispatcher.cpp @@ -1,767 +1,512 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Dispatcher implementation - */ - #include -#include -#include -#include +// ZEPHCORE: packet logging (MESH_PACKET_LOGGING) lives in the roles' logRx/logTx +// hooks, see helpers/PacketLog.h. + +#include + +// ZEPHCORE: log module backing MESH_DEBUG_PRINTLN (see MeshCore.h). #include -#include LOG_MODULE_REGISTER(zephcore_dispatcher, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); -#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING) -#define PAYLOAD_TYPE_REQ 0x00 -#define PAYLOAD_TYPE_RESPONSE 0x01 -#define PAYLOAD_TYPE_TXT_MSG 0x02 -#define PAYLOAD_TYPE_PATH 0x08 -#endif - namespace mesh { -#define MIN_TX_BUDGET_AIRTIME_DIV 2 /* require at least 1/N MTU airtime as budget before TX */ +#define MIN_TX_BUDGET_AIRTIME_DIV 2 // require at least 1/N of estimated airtime as budget before TX -Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr) - : _radio(&radio), _ms(&ms), _mgr(&mgr) +// ZEPHCORE: no MAX_RX_DELAY_MILLIS (no score-based RX delay: the adaptive +// contention window replaces it), no MIN_TX_BUDGET_RESERVE_MS (the budget gate +// lives in checkSend() only), no NOISE_FLOOR_CALIB_INTERVAL (the radio schedules +// its own sampling, see maintenanceLoop()). + +// ZEPHCORE: out-of-line constructor (upstream's is inline in the header). +Dispatcher::Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr) + : _radio(&radio), _ms(&ms), _mgr(&mgr) { - outbound = nullptr; - outbound_priority = 0; - total_air_time = rx_air_time = 0; - next_tx_time = 0; - cad_busy_start = 0; - lbt_busy_start = 0; - lbt_next_warn = 0; - tx_budget_ms = 0; - last_budget_update = 0; - duty_cycle_window_ms = 0; - _err_flags = 0; - radio_nonrx_start = 0; - prev_isrecv_mode = true; - cad_offset_shadow = 0; - cad_offset_shadow_valid = false; - n_sent_flood = n_sent_direct = 0; - n_recv_flood = n_recv_direct = 0; - _tx_queued_cb = nullptr; - _tx_queued_user_data = nullptr; - _wake_cb = nullptr; - _wake_user_data = nullptr; + outbound = NULL; + outbound_priority = 0; + total_air_time = rx_air_time = 0; + next_tx_time = 0; + cad_busy_start = 0; + lbt_busy_start = 0; + lbt_next_warn = 0; + tx_budget_ms = 0; + last_budget_update = 0; + duty_cycle_window_ms = 0; + _err_flags = 0; + radio_nonrx_start = 0; + prev_isrecv_mode = true; + cad_offset_shadow = 0; + cad_offset_shadow_valid = false; + n_sent_flood = n_sent_direct = 0; + n_recv_flood = n_recv_direct = 0; + _tx_queued_cb = NULL; + _tx_queued_user_data = NULL; + _wake_cb = NULL; + _wake_user_data = NULL; } -void Dispatcher::begin() -{ - n_sent_flood = n_sent_direct = 0; - n_recv_flood = n_recv_direct = 0; - _err_flags = 0; - uint32_t now = (uint32_t)_ms->getMillis(); - radio_nonrx_start = now; - duty_cycle_window_ms = getDutyCycleWindowMs(); - tx_budget_ms = getMaxTxBudgetMs(); - last_budget_update = now; - next_tx_time = now; - _radio->begin(); - prev_isrecv_mode = _radio->isInRecvMode(); +void Dispatcher::begin() { + n_sent_flood = n_sent_direct = 0; + n_recv_flood = n_recv_direct = 0; + _err_flags = 0; + uint32_t now = (uint32_t)_ms->getMillis(); + radio_nonrx_start = now; + + duty_cycle_window_ms = getDutyCycleWindowMs(); + tx_budget_ms = getMaxTxBudgetMs(); + last_budget_update = now; + next_tx_time = now; + + _radio->begin(); + prev_isrecv_mode = _radio->isInRecvMode(); } -uint8_t Dispatcher::getDutyCyclePercent() const -{ - return 10; /* EU 868 default: 10% duty cycle */ +// ZEPHCORE: duty cycle as a percentage (upstream: getAirtimeBudgetFactor()). +uint8_t Dispatcher::getDutyCyclePercent() const { + return 10; // EU 868 default } -uint32_t Dispatcher::getMaxTxBudgetMs() const -{ - uint8_t duty_pct = getDutyCyclePercent(); - if (duty_pct == 0 || duty_cycle_window_ms == 0) { - return 0; - } - return (duty_cycle_window_ms * (uint32_t)duty_pct) / 100U; +uint32_t Dispatcher::getMaxTxBudgetMs() const { + uint8_t duty_pct = getDutyCyclePercent(); + if (duty_pct == 0 || duty_cycle_window_ms == 0) { + return 0; + } + return (duty_cycle_window_ms * (uint32_t)duty_pct) / 100U; } -void Dispatcher::updateTxBudget() -{ - uint8_t duty_pct = getDutyCyclePercent(); - if (duty_pct == 0 || duty_cycle_window_ms == 0) { - return; - } +void Dispatcher::updateTxBudget() { + uint8_t duty_pct = getDutyCyclePercent(); + if (duty_pct == 0 || duty_cycle_window_ms == 0) { + return; + } - uint32_t now = (uint32_t)_ms->getMillis(); - uint32_t elapsed = now - last_budget_update; - if (elapsed == 0) { - return; - } + uint32_t now = (uint32_t)_ms->getMillis(); + uint32_t elapsed = now - last_budget_update; + if (elapsed == 0) { + return; + } - uint32_t refill = (elapsed * (uint32_t)duty_pct) / 100U; - if (refill > 0) { - uint32_t max_budget = getMaxTxBudgetMs(); - tx_budget_ms += refill; - if (tx_budget_ms > max_budget) { - tx_budget_ms = max_budget; - } - last_budget_update = now; - } + uint32_t refill = (elapsed * (uint32_t)duty_pct) / 100U; + if (refill > 0) { + uint32_t max_budget = getMaxTxBudgetMs(); + tx_budget_ms += refill; + if (tx_budget_ms > max_budget) { + tx_budget_ms = max_budget; + } + last_budget_update = now; + } } -bool Dispatcher::isAdminPacket(const Packet *pkt) -{ - uint8_t t = pkt->getPayloadType(); - return t == PAYLOAD_TYPE_REQ || t == PAYLOAD_TYPE_RESPONSE || - t == PAYLOAD_TYPE_ANON_REQ || t == PAYLOAD_TYPE_CONTROL; +// ZEPHCORE: no calcRxDelay() (see top of file). + +uint32_t Dispatcher::getCADFailRetryDelay() const { + return 200; +} +uint32_t Dispatcher::getCADFailMaxDuration() const { + return 4000; // 4 seconds } -uint32_t Dispatcher::getCADFailRetryDelay() const -{ - /* 100-200ms jittered retry: tighter than one SF8 flood airtime so we - * sample multiple RX duty-cycle windows, and randomized so two nodes - * contending on the same channel don't retry in lockstep. */ - return 100 + (sys_rand32_get() % 101); +// ZEPHCORE: LBT starvation window (see onLbtRefused()) and the admin exemption +// from the duty-cycle gate (see txBudgetAllowsSend()). +uint32_t Dispatcher::getTxStarvationDuration() const { + return 60000; } -uint32_t Dispatcher::getCADFailMaxDuration() const -{ - return 4000; /* ms; ~20 retry attempts before giving up */ +bool Dispatcher::isAdminPacket(const Packet* pkt) { + uint8_t t = pkt->getPayloadType(); + return t == PAYLOAD_TYPE_REQ || t == PAYLOAD_TYPE_RESPONSE || + t == PAYLOAD_TYPE_ANON_REQ || t == PAYLOAD_TYPE_CONTROL; } -uint32_t Dispatcher::getTxStarvationDuration() const -{ - return 60000; /* ms; 15 warning periods of not transmitting at all */ +void Dispatcher::loop() { + // ZEPHCORE: noise-floor sampling, the radio stall watchdog and AGC hygiene run + // from maintenanceLoop() on their own deadlines, not on every loop() pass. + + if (outbound) { // waiting for outbound send to be completed + // ZEPHCORE: read isTxActive() BEFORE isSendComplete(): the radio publishes + // completion before it drops isTxActive(), so "not active" here guarantees a + // finished transmit is already latched and collected below. + bool tx_active = _radio->isTxActive(); + if (_radio->isSendComplete()) { + // ZEPHCORE: charge the COMPUTED airtime, not the wall-clock send time, + // which includes blocking LBT and event latency (up to 8x in the field). + uint32_t t = _radio->getEstAirtimeFor(outbound->getRawLength()); + MESH_DEBUG_PRINTLN("TX complete: air=%ums wall=%ums", t, (uint32_t)_ms->getMillis() - outbound_start); + total_air_time += t; + + updateTxBudget(); + + if (t >= tx_budget_ms) { + tx_budget_ms = 0; + } else { + tx_budget_ms -= t; + } + + _radio->onSendFinished(); + logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + if (outbound->isRouteFlood()) { + n_sent_flood++; + } else { + n_sent_direct++; + } + releasePacket(outbound); // return to pool + outbound = NULL; + } else if (!tx_active && millisHasNowPassed(outbound_expiry)) { // ZEPHCORE: only once the radio has concluded + MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); + + _radio->onSendFinished(); + logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + + releasePacket(outbound); // return to pool + outbound = NULL; + } else { + return; // can't do any more radio activity until send is complete or timed out + } + } + + // ZEPHCORE: no inbound (delayed) queue. + checkRecv(); + checkSend(); } -void Dispatcher::loop() -{ - if (outbound) { - /* Read BEFORE isSendComplete(): the radio publishes its - * completion before it drops isTxActive(), so a "not active" - * seen here guarantees a successful transmit is already latched - * and collected by the call below. The other order can see the - * latch still empty, then the flag already clear, and abandon a - * transmit that finished in between. */ - bool tx_active = _radio->isTxActive(); - if (_radio->isSendComplete()) { - /* Airtime is the modulation time of the packet that just - * went out, not the wall-clock width of the send. - * - * Upstream measures the wall clock here and gets away with - * it: on Arduino the CAD runs before outbound_start is - * stamped, loop() polls continuously, and isSendComplete() - * has no timeout, so almost nothing sits between the stamp - * and the TX_DONE interrupt. Our send has all three — - * blocking LBT inside startSendRaw(), a wait-thread - * watchdog, and event-driven completion — so the same - * expression measured up to 8x the real airtime in the - * field, and charged every millisecond of it to the - * duty-cycle budget below. - * - * LoRa airtime is exact given SF/BW/CR/preamble/length, so - * compute it rather than time it: same value the RX side - * already accumulates, which makes the two figures on the - * stats screen comparable for the first time, and the right - * unit for tx_budget_ms, which is a transmitter-on-time - * allowance (CAD is receiving, not transmitting). */ - uint32_t t = _radio->getEstAirtimeFor(outbound->getRawLength()); - LOG_DBG("TX complete: air=%ums wall=%ums", t, - (uint32_t)_ms->getMillis() - outbound_start); - total_air_time += t; - updateTxBudget(); - if (t >= tx_budget_ms) { - tx_budget_ms = 0; - } else { - tx_budget_ms -= t; - } - _radio->onSendFinished(); - logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); - if (outbound->isRouteFlood()) { - n_sent_flood++; - } else { - n_sent_direct++; - } - releasePacket(outbound); - outbound = nullptr; - } else if (!tx_active && millisHasNowPassed(outbound_expiry)) { - /* Give up only once the radio itself has concluded. Upstream - * abandons at outbound_expiry alone because RadioLib has no - * watchdog of its own; ours does — the TX wait thread always - * ends with isTxActive() false, on a timeout of - * max(TX_TIMEOUT_MS, 2 x airtime + 1 s). Abandoning while it - * still runs raced that thread: a completion published after - * the expiry was then wiped by the next startSendRaw(), so a - * packet that went out on air was counted nowhere — neither - * sent_flood/direct nor the radio's total. Radios that do not - * implement isTxActive() report false and keep the upstream - * behaviour exactly. */ - _radio->onSendFinished(); - logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); - releasePacket(outbound); - outbound = nullptr; - } else { - return; - } - } +// ZEPHCORE: deadline-driven maintenance. The event loop calls +// msUntilNextMaintenance() after every pass and sleeps until the soonest deadline. +void Dispatcher::maintenanceLoop() { + _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); - checkRecv(); - checkSend(); + // Radio stall watchdog (diagnostic only: raises ERR_EVENT_STARTRX_TIMEOUT, + // surfaced in stats, telemetry, MQTT and the companion status frame). TX + // counts as active; isTxActive(), never the consuming isSendComplete(). + bool is_active = _radio->isInRecvMode() || _radio->isTxActive(); + if (is_active != prev_isrecv_mode) { + prev_isrecv_mode = is_active; + if (!is_active) { + radio_nonrx_start = (uint32_t)_ms->getMillis(); + } + } + if (!is_active && (uint32_t)_ms->getMillis() - radio_nonrx_start > RADIO_STALL_THRESHOLD_MS) { + _err_flags |= ERR_EVENT_STARTRX_TIMEOUT; + } + + _radio->cadMaintenance(); // adaptive CAD probing + staircase + _radio->radioMaintenance(); // AGC unstick + temperature-drift recal (sleeps the chip) + + // Surface CAD offset changes so the role persists them. + int8_t cad_off = _radio->getCadOffset(); + if (!cad_offset_shadow_valid) { + cad_offset_shadow = cad_off; + cad_offset_shadow_valid = true; + } else if (cad_off != cad_offset_shadow) { + cad_offset_shadow = cad_off; + onCadOffsetChanged(cad_off); + } } -void Dispatcher::maintenanceLoop() -{ - _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); +uint32_t Dispatcher::msUntilNextMaintenance() { + uint32_t now = (uint32_t)_ms->getMillis(); + uint32_t next = _radio->msUntilNextMaintenance(); - /* RX mode watchdog: TX counts as "active" to avoid false triggers when - * a maintenance pass lands between brief RX windows. Diagnostic only — - * it raises a status bit and recovers nothing — but that bit is surfaced - * on every role: the repeater/room-server "stats" CLI reply and binary - * telemetry read _err_flags directly, the MQTT uplink publishes it, and - * the companion returns it in its BLE device-status response. */ - /* isTxActive(), not !isSendComplete(): the latter is now a one-shot that - * consumes the completion, so asking it here would swallow the event the - * dispatcher's own loop() is waiting to collect. The value is identical - * in every state — it is the same _tx_active read this line always - * performed — so the spurious-STARTRX_TIMEOUT fix this term was added - * for (rapid consecutive relays leaving radio_nonrx_start stale) is - * unchanged. */ - bool is_active = _radio->isInRecvMode() || _radio->isTxActive(); - if (is_active != prev_isrecv_mode) { - prev_isrecv_mode = is_active; - if (!is_active) { - radio_nonrx_start = (uint32_t)_ms->getMillis(); - } - } - if (!is_active && - (uint32_t)_ms->getMillis() - radio_nonrx_start > RADIO_STALL_THRESHOLD_MS) { - _err_flags |= ERR_EVENT_STARTRX_TIMEOUT; - } - - /* Adaptive CAD: probe scheduling + staircase live in the radio; - * we only surface offset changes so the app layer can persist them. */ - _radio->cadMaintenance(); - - /* Receiver hygiene: deaf-aware AGC unstick + temperature-drift - * recalibration. Both sleep the chip, so they live here rather than on - * any packet path. */ - _radio->radioMaintenance(); - - int8_t cad_off = _radio->getCadOffset(); - - if (!cad_offset_shadow_valid) { - cad_offset_shadow = cad_off; - cad_offset_shadow_valid = true; - } else if (cad_off != cad_offset_shadow) { - cad_offset_shadow = cad_off; - onCadOffsetChanged(cad_off); - } + // The stall verdict is a latched bit; once raised its deadline is permanently + // in the past, so stop reporting it or the loop would spin at its minimum. + if (!prev_isrecv_mode && !(_err_flags & ERR_EVENT_STARTRX_TIMEOUT)) { + next = maintenanceSooner(next, maintenanceUntil(now, radio_nonrx_start + RADIO_STALL_THRESHOLD_MS)); + } + return next; } -uint32_t Dispatcher::msUntilNextMaintenance() -{ - uint32_t now = (uint32_t)_ms->getMillis(); - /* Noise floor sampling + CAD probing/decay both live in the radio and - * carry their own deadlines. */ - uint32_t next = _radio->msUntilNextMaintenance(); +bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) { + if (len < 2 || len > MAX_TRANS_UNIT) return false; // ZEPHCORE: bound the extent before reading + int i = 0; - /* Radio stall watchdog. Only pending while the radio is known to be - * neither receiving nor transmitting as of the last pass — the - * transition into that state is itself event-driven (TX start, RX done, - * CAD), so there is nothing to poll for while the radio is active. - * - * The already-flagged check is load-bearing: the verdict is a latched - * status bit, so once raised its deadline sits permanently in the past. - * Without this the query would return 0 on every call and the event loop - * would re-arm at its minimum interval forever. */ - if (!prev_isrecv_mode && !(_err_flags & ERR_EVENT_STARTRX_TIMEOUT)) { - next = maintenanceSooner( - next, maintenanceUntil(now, radio_nonrx_start + - RADIO_STALL_THRESHOLD_MS)); - } + pkt->header = raw[i++]; + if (pkt->getPayloadVer() > PAYLOAD_VER_1) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime()); + return false; + } - return next; + if (pkt->hasTransportCodes()) { + if (len < 6) return false; // ZEPHCORE: header + two codes + path length + memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2; + memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2; + } else { + pkt->transport_codes[0] = pkt->transport_codes[1] = 0; + } + + pkt->path_len = raw[i++]; + uint8_t path_mode = pkt->path_len >> 6; // upper 2 bits (legacy firmware: 00) + if (path_mode == 3) { // Reserved for future + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported path mode: 3", getLogDateTime()); + return false; + } + + uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize(); + 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); + return false; + } + + 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)) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet payload too big, payload_len=%d", getLogDateTime(), (uint32_t)pkt->payload_len); + return false; + } + + memcpy(pkt->payload, &raw[i], pkt->payload_len); + + return true; // success } -bool Dispatcher::tryParsePacket(Packet *pkt, const uint8_t *raw, int len) -{ - /* Check the received extent before reading header/path fields. The radio - * buffer may be larger than len, but bytes outside len are not packet data. */ - if (len < 2 || len > MAX_TRANS_UNIT) return false; - int i = 0; +void Dispatcher::checkRecv() { + // ZEPHCORE: drain the whole RX ring per call. The mesh thread wakes on a + // k_event bit, and several arrivals coalesce into one wake. + for (;;) { + Packet* pkt; + float score; + uint32_t air_time; + { + uint8_t raw[MAX_TRANS_UNIT+1]; + int len = _radio->recvRaw(raw, MAX_TRANS_UNIT); + if (len <= 0) { + break; // ring empty + } + logRxRaw(_radio->getLastSNR(), _radio->getLastRSSI(), raw, len); - pkt->header = raw[i++]; - if (pkt->getPayloadVer() > PAYLOAD_VER_1) { - LOG_WRN("tryParsePacket: unsupported packet version"); - return false; - } + pkt = _mgr->allocNew(); + if (pkt == NULL) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime()); + break; + } + if (tryParsePacket(pkt, raw, len)) { + pkt->_snr = (int8_t)(_radio->getLastSNR() * 4.0f); + score = _radio->packetScore(_radio->getLastSNR(), len); + air_time = _radio->getEstAirtimeFor(len); + rx_air_time += air_time; + } else { + _mgr->free(pkt); // put back into pool + continue; + } + } + logRx(pkt, pkt->getRawLength(), score); // hook for custom logging - if (pkt->hasTransportCodes()) { - if (len < 6) return false; /* header + two codes + path length */ - memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2; - memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2; - } else { - pkt->transport_codes[0] = pkt->transport_codes[1] = 0; - } - - pkt->path_len = raw[i++]; - uint8_t path_mode = pkt->path_len >> 6; - if (path_mode == 3) { /* reserved path mode */ - LOG_WRN("tryParsePacket: unsupported path mode: 3"); - return false; - } - - uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize(); - if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) { - LOG_WRN("tryParsePacket: partial or corrupt packet, len=%d", len); - return false; - } - - memcpy(pkt->path, &raw[i], path_byte_len); i += path_byte_len; - - pkt->payload_len = len - i; - if (pkt->payload_len > (int)sizeof(pkt->payload)) { - LOG_WRN("tryParsePacket: payload too big, payload_len=%d", (uint32_t)pkt->payload_len); - return false; - } - - memcpy(pkt->payload, &raw[i], pkt->payload_len); - return true; + if (pkt->isRouteFlood()) { + n_recv_flood++; + } else { + n_recv_direct++; + } + processRecvPacket(pkt); // ZEPHCORE: always immediately (no score-based RX delay) + } } -void Dispatcher::checkRecv() -{ - /* k_event is a bitfield — multiple ISR arrivals coalesce into one - * wake, so drain the entire ring each time. */ - for (;;) { - uint8_t raw[MAX_TRANS_UNIT + 1]; - int len = _radio->recvRaw(raw, MAX_TRANS_UNIT); - if (len <= 0) { - break; - } +void Dispatcher::processRecvPacket(Packet* pkt) { + DispatcherAction action = onRecvPacket(pkt); + if (action == ACTION_RELEASE) { + _mgr->free(pkt); + } else if (action == ACTION_MANUAL_HOLD) { + // sub-class is wanting to manually hold Packet instance, and call releasePacket() at appropriate time + } else { // ACTION_RETRANSMIT* + uint8_t priority = (action >> 24) - 1; + uint32_t _delay = action & 0xFFFFFF; - logRxRaw(_radio->getLastSNR(), _radio->getLastRSSI(), raw, len); - - Packet *pkt = _mgr->allocNew(); - if (pkt == nullptr) { - LOG_ERR("checkRecv: packet alloc failed"); - break; - } - - float score = 0.0f; - uint32_t air_time = 0; - - if (tryParsePacket(pkt, raw, len)) { - pkt->_snr = (int8_t)(_radio->getLastSNR() * 4.0f); /* x4 fixed-point SNR */ - score = _radio->packetScore(_radio->getLastSNR(), len); - air_time = _radio->getEstAirtimeFor(len); - rx_air_time += air_time; - } else { - _mgr->free(pkt); - continue; - } - -#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING) - /* Arduino-compatible packet logging - use printk to bypass log level filtering */ - { - static uint8_t packet_hash[MAX_HASH_SIZE]; - static char hash_hex[MAX_HASH_SIZE * 2 + 1]; - pkt->calculatePacketHash(packet_hash); - Utils::toHex(hash_hex, packet_hash, MAX_HASH_SIZE); - - uint8_t ptype = pkt->getPayloadType(); - if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ || - ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) { - printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s [%02X -> %02X]\n", - getLogDateTime(), pkt->getRawLength(), ptype, - pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, - (int)pkt->getSNR(), (int)_radio->getLastRSSI(), - (int)(score * 1000), air_time, hash_hex, - (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); - } else { - printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s\n", - getLogDateTime(), pkt->getRawLength(), ptype, - pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, - (int)pkt->getSNR(), (int)_radio->getLastRSSI(), - (int)(score * 1000), air_time, hash_hex); - } - } -#endif - logRx(pkt, pkt->getRawLength(), score); - if (pkt->isRouteFlood()) { - n_recv_flood++; - } else { - n_recv_direct++; - } - processRecvPacket(pkt); - } + _mgr->queueOutbound(pkt, priority, futureMillis(_delay)); + if (_delay > 0) notifyTxQueued(_delay); // ZEPHCORE: wake for the deferred TX + } } -void Dispatcher::processRecvPacket(Packet *pkt) -{ - DispatcherAction action = onRecvPacket(pkt); - if (action == ACTION_RELEASE) { - _mgr->free(pkt); - } else if (action == ACTION_MANUAL_HOLD) { - /* subclass holds packet */ - } else { - uint8_t priority = (uint8_t)((action >> 24) - 1); - uint32_t delay = action & 0xFFFFFF; - _mgr->queueOutbound(pkt, priority, futureMillis((int)delay)); - if (_tx_queued_cb && delay > 0) { - _tx_queued_cb(delay, _tx_queued_user_data); - } - } +// ZEPHCORE: duty-cycle budget gate. Defers while the remaining budget is under +// half an MTU's airtime (as upstream) and schedules the wake for when it will be +// enough. Due admin packets (REQ/RESPONSE/ANON_REQ/CONTROL) are exempt so a node +// that burned its budget stays reachable for management — strictly outside +// EN 300 220, accepted: an unreachable managed node is worse. +bool Dispatcher::txBudgetAllowsSend(uint32_t now) { + uint8_t duty_pct = getDutyCyclePercent(); + if (duty_pct == 0) return true; + + int total = _mgr->getOutboundTotal(); + for (int i = 0; i < total; i++) { + Packet* pkt = _mgr->getOutboundByIdx(i); + if (pkt && (int32_t)(_mgr->getOutboundSchedule(i) - now) <= 0 && isAdminPacket(pkt)) { + return true; + } + } + + uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); + uint32_t threshold = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV; + if (tx_budget_ms >= threshold) return true; + + uint32_t needed = threshold - tx_budget_ms; + uint32_t delay_ms = (needed * 100U + (uint32_t)duty_pct - 1U) / (uint32_t)duty_pct; + notifyTxQueued(delay_ms + 1U); + return false; } -void Dispatcher::checkSend() -{ - uint32_t now = (uint32_t)_ms->getMillis(); - int count = _mgr->getOutboundCount(now); - if (count == 0) { - cad_busy_start = 0; - /* Only a genuinely EMPTY queue ends an LBT starvation streak, not - * one that merely has nothing due this instant. getOutboundCount() - * excludes packets scheduled in the future, and an LBT refusal - * re-queues its packet 100-200 ms ahead — so every checkSend() that - * lands in that retry gap (any RX wake will do) used to reset the - * streak here, and a node refusing every transmit could keep - * restarting the clock instead of ever reaching the escalation. */ - if (_mgr->getOutboundTotal() == 0) { - lbt_busy_start = 0; - } - return; - } +// ZEPHCORE: the driver's LBT refused the transmit (a TRUE reading of a busy +// channel: re-queue, never force). Warn every getCADFailMaxDuration(); after +// getTxStarvationDuration() of nothing but refusals the detector is judged too +// sensitive and relaxed one step, regardless of cad.auto (persisted via +// onCadOffsetChanged()). Any successful transmit ends the streak. +void Dispatcher::onLbtRefused(uint32_t now, int len) { + uint32_t retry = getCADFailRetryDelay(); + LOG_INF("checkSend: startSendRaw refused (LBT busy), re-queuing delay=%u", retry); - /* Duty-cycle budget gate. Matches Arduino MeshCore: defer when remaining - * budget < est_airtime / MIN_TX_BUDGET_AIRTIME_DIV (i.e. half an MTU's airtime). - * - * Divergence from upstream: we exempt admin packets from the gate so that - * remote management (admin requests, login, etc.) keeps working when a node - * has burned its budget. Strictly out-of-spec for EN 300 220 — admin floods - * still consume airtime — but a managed node that can't be reached to be - * disabled is worse than the marginal extra airtime. Scan is O(N) over - * the small (24-32) packet pool so the cost is negligible. */ - updateTxBudget(); - uint8_t duty_pct = getDutyCyclePercent(); - if (duty_pct > 0) { - bool due_admin_queued = false; - int total = _mgr->getOutboundTotal(); - for (int i = 0; i < total; i++) { - Packet *pkt = _mgr->getOutboundByIdx(i); - if (!pkt) { - continue; - } - if ((int32_t)(_mgr->getOutboundSchedule(i) - now) > 0) { - continue; - } - if (isAdminPacket(pkt)) { - due_admin_queued = true; - break; - } - } + if (lbt_busy_start == 0) { + lbt_busy_start = now; + lbt_next_warn = now + getCADFailMaxDuration(); + } else { + uint32_t streak = now - lbt_busy_start; - uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); - uint32_t threshold = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV; - if (!due_admin_queued && tx_budget_ms < threshold) { - uint32_t needed = threshold - tx_budget_ms; - uint32_t delay_ms = (needed * 100U + (uint32_t)duty_pct - 1U) / (uint32_t)duty_pct; - if (_tx_queued_cb) { - _tx_queued_cb(delay_ms + 1U, _tx_queued_user_data); - } - return; - } - } - - bool is_receiving = _radio->isReceiving(); - bool is_radio_ready = _radio->isRadioReady(); - if (is_receiving || !is_radio_ready) { - /* Channel busy or radio not command-ready — enforce retry timer - * so we don't hammer checks during RX activity or BUSY windows. */ - if (!millisHasNowPassed(next_tx_time)) { - if (_tx_queued_cb) { - uint32_t remaining = next_tx_time - now; - _tx_queued_cb(remaining + 1, _tx_queued_user_data); - } - return; - } - if (cad_busy_start == 0) { - cad_busy_start = now; - } - if (now - cad_busy_start > getCADFailMaxDuration()) { - _err_flags |= ERR_EVENT_CAD_TIMEOUT; - LOG_ERR("checkSend: CAD timeout exceeded (isReceiving=%d, isRadioReady=%d, inRecvMode=%d, rssi=%.1f, snr=%.1f, noise=%d, rx_ok=%u, rx_err=%u)", - (int)is_receiving, (int)is_radio_ready, (int)_radio->isInRecvMode(), - (double)_radio->getLastRSSI(), (double)_radio->getLastSNR(), - _radio->getNoiseFloor(), - (unsigned)_radio->getPacketsRecv(), - (unsigned)_radio->getPacketsRecvErrors()); - /* Channel activity has gone on too long -- the radio may be - * in a bad state. FORCE the pending transmit by falling - * through, exactly as Arduino MeshCore does - * (Dispatcher.cpp: "force the pending transmit below..."). - * - * This bounded give-up was ZephCore's behaviour too until - * 3441caf "new rx busy latch" added a `return` here, turning a - * 4 s hard limit into an unbounded defer: on a channel that - * reads busy forever the node never transmits again, silently - * filling the 32-entry outbound queue until queueOutbound() - * starts evicting and dropping. - * - * recoverRxState() is kept and runs first: the non-destructive - * sx126x_is_receiving() has no side-effect IRQ clear, so a stuck - * preamble bit needs the chip walked REST -> fresh RX. Doing it - * before the forced TX leaves the receiver healthy afterwards; - * send_async accepts the RX -> TX entry CAS. */ - _radio->recoverRxState(); - /* fall through -- force the pending transmit */ - } else { - uint32_t retry = getCADFailRetryDelay(); - next_tx_time = futureMillis((int)retry); - if (_tx_queued_cb) { - _tx_queued_cb(retry + 1, _tx_queued_user_data); - } - return; - } - } - cad_busy_start = 0; - - /* Snapshot the priority of the packet we're about to dequeue so it - * can be preserved if the send attempt fails and we need to re-queue. - * Must be called before getNextOutbound() removes the entry. */ - outbound_priority = _mgr->peekNextOutboundPriority(now); - outbound = _mgr->getNextOutbound(now); - if (outbound) { - uint8_t raw[MAX_TRANS_UNIT]; - int len = 0; - raw[len++] = outbound->header; - if (outbound->hasTransportCodes()) { - memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2; - memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2; - } - raw[len++] = outbound->path_len; - /* Trusted source: outbound->path is MAX_PATH_SIZE-sized. */ - len += Packet::writePath(&raw[len], outbound->path, MAX_PATH_SIZE, outbound->path_len); - - if (len + outbound->payload_len > MAX_TRANS_UNIT) { - LOG_ERR("checkSend: packet too large len=%d+%d > %d", len, outbound->payload_len, MAX_TRANS_UNIT); - _mgr->free(outbound); - outbound = nullptr; - } else { - memcpy(&raw[len], outbound->payload, outbound->payload_len); - len += outbound->payload_len; - - uint32_t max_airtime = _radio->getEstAirtimeFor(len) * 3 / 2; - /* Short packets (ACKs) have est airtimes small enough that - * IRQ/work-queue latency alone can blow the watchdog and clip - * the TX mid-air (upstream 4f8cb8db: 200ms est floor, x1.5). */ - if (max_airtime < 300) { - max_airtime = 300; - } - outbound_start = now; - -#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING) - /* Arduino-compatible packet logging - use printk to bypass log level filtering */ - { - uint8_t ptype = outbound->getPayloadType(); - if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ || - ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) { - printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d) [%02X -> %02X]\n", - getLogDateTime(), len, ptype, - outbound->isRouteDirect() ? "D" : "F", outbound->payload_len, - (uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]); - } else { - printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d)\n", - getLogDateTime(), len, ptype, - outbound->isRouteDirect() ? "D" : "F", outbound->payload_len); - } - } -#endif - - /* Final gate — close the gap between initial checks and - * actual TX start (serialisation + logging can take 1-5 ms). */ - bool final_is_receiving = _radio->isReceiving(); - bool final_is_radio_ready = _radio->isRadioReady(); - /* isTxActive() covers the window the radio opened by - * publishing its completion before it finishes re-arming - * RX: we may have collected that completion and come - * straight back here. startSendRaw()'s CAS would refuse - * anyway, but that refusal is reported as an LBT-busy - * verdict and feeds the cad_busy_start escalation, which - * this is not — "radio not ready yet" belongs here. */ - if (final_is_receiving || !final_is_radio_ready || - _radio->isTxActive()) { - uint32_t retry = getCADFailRetryDelay(); - LOG_DBG("checkSend: final gate blocked TX (isReceiving=%d, isRadioReady=%d, inRecvMode=%d, txActive=%d)", - (int)final_is_receiving, (int)final_is_radio_ready, - (int)_radio->isInRecvMode(), - (int)_radio->isTxActive()); - _mgr->queueOutbound(outbound, outbound_priority, futureMillis((int)retry)); - outbound = nullptr; - if (_tx_queued_cb) { - _tx_queued_cb(retry, _tx_queued_user_data); - } - return; - } - - bool success = _radio->startSendRaw(raw, len); - if (!success) { - uint32_t retry = getCADFailRetryDelay(); - /* Almost always LBT refusing a busy channel, which is the - * designed outcome — the packet is re-queued below and - * retried, and we deliberately do NOT force a transmit - * here: unlike the isReceiving() gate above, where a long - * refusal suggests a stuck radio, a busy LBT verdict is a - * TRUE reading of the channel. Forcing through it would - * transmit into traffic the radio can hear — exactly the - * collision CAD exists to prevent, at the moment the - * channel is most contended. - * - * INF, not DBG: this used to be ERR, which buried real - * faults on a busy site, and was then dropped to DBG — - * which made a node refusing every transmit completely - * invisible at default log level. A node that is not - * transmitting should say so; INF reports the refusals - * themselves rather than inferring a stall from them. */ - LOG_INF("checkSend: startSendRaw refused (LBT busy), re-queuing delay=%u", retry); - - /* Escalate a refusal that will not end. This branch - * used to leave cad_busy_start alone, so a packet the - * driver's own LBT kept rejecting — the pre-TX gate - * having passed — looped indefinitely with no counter, - * no error flag and nothing above DBG. At default log - * level a node in that state looks completely idle - * while it never transmits, which is exactly how a - * chip-side CAD (LR20xx CAD_LBT) fails. - * - * Deliberately no recoverRxState() here, unlike the - * isReceiving() branch above: that one recovers a chip - * suspected of being stuck in RX, whereas a busy - * channel is a true reading and walking the radio - * through REST would only add deaf time. Report and - * keep retrying. */ - if (lbt_busy_start == 0) { - lbt_busy_start = now; - lbt_next_warn = now + getCADFailMaxDuration(); - } else { - uint32_t streak = now - lbt_busy_start; - - if ((int32_t)(now - lbt_next_warn) >= 0) { - _err_flags |= ERR_EVENT_CAD_TIMEOUT; - LOG_WRN("checkSend: LBT has refused TX for %ums " - "(len=%d, noise=%d) — channel busy or CAD too sensitive", - (unsigned)streak, len, - _radio->getNoiseFloor()); - lbt_next_warn = now + getCADFailMaxDuration(); - } - - /* Self-unmute. A node whose LBT has refused EVERY - * transmit for this long is not looking at a busy - * channel — a busy channel still yields gaps, and any - * success resets this streak. It is looking at a - * detector tuned too sensitive to ever clear, which - * `set cad.offset -8` on a quiet site produces - * outright. - * - * The adaptive staircase cannot be relied on to undo - * that: it runs only when `cad.auto` is on, only when - * `cad.busycap` is non-zero, and only once the - * operating level has 120 probes behind it — half an - * hour at best. All three are settings a hand-tuning - * operator turns off, and the CLI help for - * `set cad.auto` recommends exactly that workflow. A - * repeater on a mast that stops transmitting cannot - * be talked back down either: it still receives and - * still applies an admin `set`, but the reply never - * gets out, so no ordinary app completes the login it - * is waiting on. - * - * So this deliberately overrides `cad.auto` and runs - * regardless of the operator's settings. The radio - * clamps to its own least-sensitive step and reports - * when it can go no further. - * - * The new offset PERSISTS, like a staircase step: - * maintenanceLoop() notices getCadOffset() moved and - * calls onCadOffsetChanged(), which writes prefs. - * That is the behaviour we want on a mast — a node - * that healed itself must not go mute again on the - * next reboot — and it is what `cad.auto` already - * does to a hand-set offset. Writes are bounded: each - * step needs another full starvation period, and they - * stop the moment a transmit succeeds. */ - if (streak > getTxStarvationDuration()) { - if (_radio->cadRelaxOnTxStarvation()) { - LOG_ERR("checkSend: TX starved %ums — relaxed CAD one step", - (unsigned)streak); - } else { - LOG_ERR("checkSend: TX starved %ums — CAD already at its " - "least sensitive step, channel may be genuinely busy", - (unsigned)streak); - } - lbt_busy_start = now; - lbt_next_warn = now + getCADFailMaxDuration(); - } - } - logTxFail(outbound, outbound->getRawLength()); - _mgr->queueOutbound(outbound, outbound_priority, futureMillis((int)retry)); - outbound = nullptr; - if (_tx_queued_cb) { - _tx_queued_cb(retry, _tx_queued_user_data); - } - } else { - outbound_expiry = futureMillis((int)max_airtime); - /* A transmit got out, so the refusal streak above is - * over. Without this the timer keeps running across - * successful sends and the next isolated refusal - * inherits a stale start, reporting a stall that - * already ended. */ - cad_busy_start = 0; - lbt_busy_start = 0; - } - } - } + if ((int32_t)(now - lbt_next_warn) >= 0) { + _err_flags |= ERR_EVENT_CAD_TIMEOUT; + LOG_WRN("checkSend: LBT has refused TX for %ums (len=%d, noise=%d) — channel busy or CAD too sensitive", + (unsigned)streak, len, _radio->getNoiseFloor()); + lbt_next_warn = now + getCADFailMaxDuration(); + } + if (streak > getTxStarvationDuration()) { + if (_radio->cadRelaxOnTxStarvation()) { + LOG_ERR("checkSend: TX starved %ums — relaxed CAD one step", (unsigned)streak); + } else { + LOG_ERR("checkSend: TX starved %ums — CAD already at its least sensitive step, channel may be genuinely busy", + (unsigned)streak); + } + lbt_busy_start = now; + lbt_next_warn = now + getCADFailMaxDuration(); + } + } + logTxFail(outbound, outbound->getRawLength()); + _mgr->queueOutbound(outbound, outbound_priority, futureMillis((int)retry)); + outbound = NULL; + notifyTxQueued(retry); } -Packet *Dispatcher::obtainNewPacket() -{ - Packet *pkt = _mgr->allocNew(); - if (pkt == nullptr) { - _err_flags |= ERR_EVENT_FULL; - } else { - pkt->payload_len = pkt->path_len = 0; - pkt->_snr = 0; - } - return pkt; +void Dispatcher::checkSend() { + uint32_t now = (uint32_t)_ms->getMillis(); + if (_mgr->getOutboundCount(now) == 0) { + cad_busy_start = 0; + // ZEPHCORE: only a genuinely EMPTY queue ends an LBT streak — a refusal + // re-queues 100-200 ms ahead, so "nothing due now" is not "nothing pending". + if (_mgr->getOutboundTotal() == 0) lbt_busy_start = 0; + return; + } + + updateTxBudget(); + + if (!txBudgetAllowsSend(now)) return; // ZEPHCORE: percentage budget, admin exempt + + // ZEPHCORE: also defer while the radio is not command-ready, and apply the + // retry timer only to a busy channel (upstream checks it first). + bool is_receiving = _radio->isReceiving(); + bool is_radio_ready = _radio->isRadioReady(); + if (is_receiving || !is_radio_ready) { + if (!millisHasNowPassed(next_tx_time)) { + notifyTxQueued(next_tx_time - now + 1); + return; + } + if (cad_busy_start == 0) { + cad_busy_start = now; // record when CAD busy state started + } + + if (now - cad_busy_start > getCADFailMaxDuration()) { + _err_flags |= ERR_EVENT_CAD_TIMEOUT; + + LOG_ERR("checkSend: CAD timeout exceeded (isReceiving=%d, isRadioReady=%d, inRecvMode=%d, rssi=%.1f, snr=%.1f, noise=%d, rx_ok=%u, rx_err=%u)", + (int)is_receiving, (int)is_radio_ready, (int)_radio->isInRecvMode(), + (double)_radio->getLastRSSI(), (double)_radio->getLastSNR(), _radio->getNoiseFloor(), + (unsigned)_radio->getPacketsRecv(), (unsigned)_radio->getPacketsRecvErrors()); + // channel activity has gone on too long... (Radio might be in a bad state) + // force the pending transmit below... + _radio->recoverRxState(); // ZEPHCORE: walk the chip REST -> fresh RX first + } else { + uint32_t retry = getCADFailRetryDelay(); + next_tx_time = futureMillis(retry); + notifyTxQueued(retry + 1); // ZEPHCORE: schedule the wake + return; + } + } + cad_busy_start = 0; // reset busy state + + outbound_priority = _mgr->peekNextOutboundPriority(now); // ZEPHCORE: kept for a re-queue + outbound = _mgr->getNextOutbound(now); + if (outbound) { + int len = 0; + uint8_t raw[MAX_TRANS_UNIT]; + + raw[len++] = outbound->header; + if (outbound->hasTransportCodes()) { + memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2; + memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2; + } + raw[len++] = outbound->path_len; + len += Packet::writePath(&raw[len], outbound->path, outbound->path_len); + + 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); + _mgr->free(outbound); + outbound = NULL; + } else { + memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; + + uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; + if (max_airtime < 300) max_airtime = 300; // ZEPHCORE: IRQ/work-queue latency must not clip short packets + outbound_start = now; + + // ZEPHCORE: final gate, closing the gap since the checks above. Also + // covers a radio that published its completion but is still re-arming RX. + if (_radio->isReceiving() || !_radio->isRadioReady() || _radio->isTxActive()) { + uint32_t retry = getCADFailRetryDelay(); + _mgr->queueOutbound(outbound, outbound_priority, futureMillis((int)retry)); + outbound = NULL; + notifyTxQueued(retry); + return; + } + + bool success = _radio->startSendRaw(raw, len); + if (!success) { + onLbtRefused(now, len); // ZEPHCORE: re-queue (upstream drops the packet) + return; + } + outbound_expiry = futureMillis(max_airtime); + cad_busy_start = 0; // ZEPHCORE: a transmit got out, so any refusal streak is over + lbt_busy_start = 0; + } + } } -void Dispatcher::releasePacket(Packet *packet) -{ - _mgr->free(packet); +Packet* Dispatcher::obtainNewPacket() { + auto pkt = _mgr->allocNew(); // TODO: zero out all fields + if (pkt == NULL) { + _err_flags |= ERR_EVENT_FULL; + } else { + pkt->payload_len = pkt->path_len = 0; + pkt->_snr = 0; + } + return pkt; } -void Dispatcher::sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis) -{ - if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) { - LOG_ERR("sendPacket: rejected - path_len=%d or payload_len=%d invalid", - packet->path_len, packet->payload_len); - _mgr->free(packet); - } else { - _mgr->queueOutbound(packet, priority, futureMillis((int)delay_millis)); - /* Fire the wake callback even for delay_millis == 0. Companion - * BLE/USB-driven direct & zero-hop sends enqueue with delay 0 from - * sysworkq — off the main loop — and the per-frame RX wake was - * removed in 57b971f, so without this they have no drain signal - * (USB companion has no tx-idle backstop). The callback reschedules - * tx_drain_work with K_MSEC(0) → immediate drain; redundant but - * harmless when sendPacket is already called from the main loop. */ - if (_tx_queued_cb) { - _tx_queued_cb(delay_millis, _tx_queued_user_data); - } - } +void Dispatcher::releasePacket(Packet* packet) { + _mgr->free(packet); } -bool Dispatcher::millisHasNowPassed(uint32_t timestamp) const -{ - return (int32_t)((uint32_t)_ms->getMillis() - timestamp) > 0; +void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { + if (!Packet::isValidPathLen(packet->path_len) || 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); + _mgr->free(packet); + } else { + _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); + // ZEPHCORE: wake the loop even for delay 0 — companion sends enqueue from the + // system work queue, off the mesh thread, and need the drain signal. + notifyTxQueued(delay_millis); + } } -uint32_t Dispatcher::futureMillis(int millis_from_now) const -{ - return (uint32_t)_ms->getMillis() + millis_from_now; +// Utility function -- handles the case where millis() wraps around back to zero +// 2's complement arithmetic will handle any unsigned subtraction up to HALF the word size (32-bits in this case) +bool Dispatcher::millisHasNowPassed(uint32_t timestamp) const { + return (int32_t)((uint32_t)_ms->getMillis() - timestamp) > 0; } -} /* namespace mesh */ +uint32_t Dispatcher::futureMillis(int millis_from_now) const { + return (uint32_t)_ms->getMillis() + millis_from_now; +} + +} diff --git a/zephcore/src/Mesh.cpp b/zephcore/src/Mesh.cpp index 4144b58..6a0ded0 100644 --- a/zephcore/src/Mesh.cpp +++ b/zephcore/src/Mesh.cpp @@ -1,794 +1,831 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Mesh - minimal port for Phase 5 - */ - #include -#include -#include +//#include +// ZEPHCORE: log module backing MESH_DEBUG_PRINTLN (see MeshCore.h). #include LOG_MODULE_REGISTER(zephcore_mesh, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); namespace mesh { -Mesh::Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables) - : Dispatcher(radio, ms, mgr), _rng(&rng), _rtc(&rtc), _tables(&tables) -{ +void Mesh::begin() { + Dispatcher::begin(); } -void Mesh::begin() -{ - Dispatcher::begin(); +void Mesh::loop() { + Dispatcher::loop(); } -void Mesh::loop() -{ - Dispatcher::loop(); +// ZEPHCORE: deadline-driven maintenance and the adaptive contention window. +void Mesh::maintenanceLoop() { + Dispatcher::maintenanceLoop(); + _contention.tick((uint32_t)_ms->getMillis()); } -void Mesh::maintenanceLoop() -{ - Dispatcher::maintenanceLoop(); - uint32_t now = (uint32_t)_ms->getMillis(); - _contention.tick(now); +uint32_t Mesh::msUntilNextMaintenance() { + return maintenanceSooner(Dispatcher::msUntilNextMaintenance(), + _contention.msUntilNextTick((uint32_t)_ms->getMillis())); } -uint32_t Mesh::msUntilNextMaintenance() -{ - uint32_t now = (uint32_t)_ms->getMillis(); - - return maintenanceSooner(Dispatcher::msUntilNextMaintenance(), - _contention.msUntilNextTick(now)); +// A duplicate of a flood we are about to retransmit was heard: defer our copy +// by one backoff_multiplier x airtime window (per dupe, from NOW), within the +// per-packet reactive headroom. +void Mesh::extendPendingRetransmit(uint32_t hash32) { + uint32_t now = (uint32_t)_ms->getMillis(); + int total = _mgr->getOutboundTotal(); + for (int i = 0; i < total; i++) { + Packet* pkt = _mgr->getOutboundByIdx(i); + if (pkt && pkt->isRouteFlood() && ContentionTracker::computePacketHash32(pkt) == hash32) { + uint32_t airtime = _radio->getEstAirtimeFor(pkt->getRawLength()); + uint16_t delay = _contention.getReactiveHeadroom(hash32, airtime); + if (delay == 0) break; + _mgr->rescheduleOutbound(i, now + delay); + _contention.addReactiveExtension(hash32, delay); + notifyTxQueued(delay); + break; + } + } } -void Mesh::extendPendingRetransmit(uint32_t hash32) -{ - uint32_t now = (uint32_t)_ms->getMillis(); - int total = _mgr->getOutboundTotal(); - for (int i = 0; i < total; i++) { - Packet *pkt = _mgr->getOutboundByIdx(i); - if (pkt && pkt->isRouteFlood() - && ContentionTracker::computePacketHash32(pkt) == hash32) { - uint32_t airtime = _radio->getEstAirtimeFor(pkt->getRawLength()); - uint16_t delay = _contention.getReactiveHeadroom(hash32, airtime); - if (delay == 0) break; - /* Reschedule from NOW: heard a dupe, defer by one - * backoff_multiplier × airtime window per dupe. */ - _mgr->rescheduleOutbound(i, now + delay); - _contention.addReactiveExtension(hash32, delay); - notifyTxQueued(delay); - break; - } - } +// Adaptive flood jitter: 5 x airtime x EMA factor, capped at 6 airtimes and +// 2000 ms absolute; +20 ms floor so downstream nodes are back in RX first. +uint32_t Mesh::computeAdaptiveFloodDelay(const Packet* packet) { + float factor = getContentionTracker().getFloodDelayFactor(); + uint32_t airtime = _radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2); + uint32_t max_jitter = (uint32_t)(5 * airtime * factor); + uint32_t airtime_cap = 6 * airtime; + if (max_jitter > airtime_cap) max_jitter = airtime_cap; + if (max_jitter > 2000) max_jitter = 2000; + return 20 + _rng->nextInt(0, max_jitter + 1); } -bool Mesh::allowPacketForward(const Packet *packet) -{ - (void)packet; - return false; +// Direct: jitter around upstream's 0.3 factor, per-packet in [0.25, 0.40]; +20 ms floor. +uint32_t Mesh::computeAdaptiveDirectDelay(const Packet* packet) { + uint32_t airtime = _radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2); + uint32_t factor_milli = (uint32_t)_rng->nextInt(250, 401); + uint32_t max_jitter = (airtime * factor_milli) / 1000; + return 20 + _rng->nextInt(0, max_jitter + 1); } -uint32_t Mesh::getRetransmitDelay(const Packet *packet) -{ - uint32_t t = (_radio->getEstAirtimeFor(packet->getRawLength()) * 52 / 50) / 2; - return _rng->nextInt(0, 5) * t; +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; + + return _rng->nextInt(0, 5)*t; +} +uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) { + return 0; // by default, no delay +} +uint8_t Mesh::getExtraAckTransmitCount() const { + return 0; } -uint32_t Mesh::computeAdaptiveFloodDelay(const Packet *packet) -{ - float factor = getContentionTracker().getFloodDelayFactor(); - uint32_t airtime = _radio->getEstAirtimeFor( - packet->getPathByteLen() + packet->payload_len + 2); - uint32_t max_jitter = (uint32_t)(5 * airtime * factor); - /* Airtime-scaled ceiling: never exceed ~6 airtimes of spread. */ - uint32_t airtime_cap = 6 * airtime; - if (max_jitter > airtime_cap) max_jitter = airtime_cap; - /* Absolute cap: avoid excessive latency in very dense areas. - * Reactive backoff will fine-tune further if needed. */ - if (max_jitter > 2000) max_jitter = 2000; - /* Floor: give downstream nodes time to finish RX processing - * and return to RX mode before we TX (~20ms settle) */ - return 20 + _rng->nextInt(0, max_jitter + 1); +uint32_t Mesh::getCADFailRetryDelay() const { + return _rng->nextInt(1, 4)*120; } -uint32_t Mesh::computeAdaptiveDirectDelay(const Packet *packet) -{ - uint32_t airtime = _radio->getEstAirtimeFor( - packet->getPathByteLen() + packet->payload_len + 2); - /* Jitter around Arduino direct factor 0.3 using a per-packet factor - * in the range [0.25, 0.40]. */ - uint32_t factor_milli = (uint32_t)_rng->nextInt(250, 401); - uint32_t max_jitter = (airtime * factor_milli) / 1000; - /* Floor: give downstream nodes time to finish RX processing - * and return to RX mode before we TX (~20ms settle + jitter) */ - return 20 + _rng->nextInt(0, max_jitter + 1); +int Mesh::searchPeersByHash(const uint8_t* hash) { + return 0; // not found } -uint32_t Mesh::getCADFailRetryDelay() const -{ - return _rng->nextInt(1, 4) * 120; +int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int max_matches) { + return 0; // not found } -void Mesh::removeSelfFromPath(Packet *pkt) -{ - pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count +DispatcherAction Mesh::onRecvPacket(Packet* pkt) { + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { + if (pkt->path_len < MAX_PATH_SIZE && pkt->payload_len >= 9) { // ZEPHCORE: header must be present + uint8_t i = 0; + uint32_t trace_tag; + memcpy(&trace_tag, &pkt->payload[i], 4); i += 4; + uint32_t auth_code; + memcpy(&auth_code, &pkt->payload[i], 4); i += 4; + uint8_t flags = pkt->payload[i++]; + uint8_t path_sz = flags & 0x03; // NEW v1.11+: lower 2 bits is path hash size - uint8_t sz = pkt->getPathHashSize(); - for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry' - memcpy(&pkt->path[k], &pkt->path[k + sz], sz); - } + uint8_t len = pkt->payload_len - i; + // path_len*entry_size can exceed 255 (path_len up to 63, entry_size up to 8); + // a uint8_t offset would wrap and steer the isHashMatch() read to the wrong place. + uint16_t offset = (uint16_t)pkt->path_len << path_sz; + if (offset >= len) { // TRACE has reached end of given path + onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); + } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); + // append SNR (Not hash!) + pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); + + uint32_t d = getDirectRetransmitDelay(pkt); + return ACTION_RETRANSMIT_DELAYED(5, d); // schedule with priority 5 (for now), maybe make configurable? + } + } + return ACTION_RELEASE; + } + + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) { + if (pkt->getPathHashCount() == 0) { + onControlDataRecv(pkt); + } + // just zero-hop control packets allowed (for this subset of payloads) + return ACTION_RELEASE; + } + + if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) { + // check for 'early received' ACK + if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + int i = 0; + uint32_t ack_crc; + memcpy(&ack_crc, &pkt->payload[i], 4); i += 4; + if (i <= pkt->payload_len) { + onAckRecv(pkt, ack_crc); + } + } + + if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { + if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { + return forwardMultipartDirect(pkt); + } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + if (!_tables->wasSeen(pkt)) { // don't retransmit! + _tables->markSeen(pkt); + removeSelfFromPath(pkt); + routeDirectRecvAcks(pkt, 0); + } + return ACTION_RELEASE; + } + + if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); + removeSelfFromPath(pkt); + + uint32_t d = getDirectRetransmitDelay(pkt); + return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority + } + } + return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard. + } + + if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE; + + // ZEPHCORE: record dupes for contention tracking + reactive backoff; on nodes + // that don't relay, track first hearings so the EMA still reflects contention. + if (pkt->isRouteFlood()) { + uint32_t h = ContentionTracker::computePacketHash32(pkt); + if (_contention.recordDupeIfTracked(h, (uint32_t)_ms->getMillis())) { + extendPendingRetransmit(h); + } else if (passivelyTrackFloods()) { + _contention.trackRetransmit(h, (uint32_t)_ms->getMillis()); + } + } + + DispatcherAction action = ACTION_RELEASE; + + switch (pkt->getPayloadType()) { + case PAYLOAD_TYPE_ACK: { + int i = 0; + uint32_t ack_crc; + memcpy(&ack_crc, &pkt->payload[i], 4); i += 4; + if (i > pkt->payload_len) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete ACK packet", getLogDateTime()); + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); + onAckRecv(pkt, ack_crc); + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: { + int i = 0; + uint8_t dest_hash = pkt->payload[i++]; + uint8_t src_hash = pkt->payload[i++]; + + uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data + if (i + CIPHER_MAC_SIZE >= pkt->payload_len) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); + // NOTE: this is a 'first packet wins' impl. When receiving from multiple paths, the first to arrive wins. + // For flood mode, the path may not be the 'best' in terms of hops. + // FUTURE: could send back multiple paths, using createPathReturn(), and let sender choose which to use(?) + + if (self_id.isHashMatch(&dest_hash)) { + // scan contacts DB, for all matching hashes of 'src_hash' (max 4 matches supported ATM) + int num = searchPeersByHash(&src_hash); + // for each matching contact, try to decrypt data + bool found = false; + for (int j = 0; j < num; j++) { + uint8_t secret[PUB_KEY_SIZE]; + getPeerSharedSecret(secret, j); + + // decrypt, checking MAC is valid + uint8_t data[MAX_PACKET_PAYLOAD]; + int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); + if (len > 0) { // success! + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { + int k = 0; + uint8_t path_len = data[k++]; + if (!Packet::isValidPathLen(path_len)) { + MESH_DEBUG_PRINTLN("%s PAYLOAD_TYPE_PATH, bad path_len: %u", getLogDateTime(), (uint32_t)path_len); + break; // reject bad encoding + } + uint8_t hash_size = (path_len >> 6) + 1; + uint8_t hash_count = path_len & 63; + if (k + hash_size*hash_count + 1 > len) { // ZEPHCORE: inner path must fit the decrypted data + MESH_DEBUG_PRINTLN("%s PAYLOAD_TYPE_PATH, truncated path", getLogDateTime()); + break; + } + uint8_t* path = &data[k]; k += hash_size*hash_count; + 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!) + if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) { + if (pkt->isRouteFlood()) { + // send a reciprocal return path to sender, but send DIRECTLY! + mesh::Packet* rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, NULL, 0); + if (rpath) sendDirect(rpath, path, path_len, 500); + } + } + } else { + onPeerDataRecv(pkt, pkt->getPayloadType(), j, secret, data, len); + } + found = true; + break; + } + } + if (found) { + pkt->markDoNotRetransmit(); // packet was for this node, so don't retransmit + } else { + MESH_DEBUG_PRINTLN("%s recv matches no peers, src_hash=%02X", getLogDateTime(), (uint32_t)src_hash); + } + } + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_ANON_REQ: { + int i = 0; + uint8_t dest_hash = pkt->payload[i++]; + uint8_t* sender_pub_key = &pkt->payload[i]; i += PUB_KEY_SIZE; + + uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data + if (i + 2 >= pkt->payload_len) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); + if (self_id.isHashMatch(&dest_hash)) { + Identity sender(sender_pub_key); + + uint8_t secret[PUB_KEY_SIZE]; + self_id.calcSharedSecret(secret, sender); + + // decrypt, checking MAC is valid + uint8_t data[MAX_PACKET_PAYLOAD]; + int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); + if (len > 0) { // success! + onAnonDataRecv(pkt, secret, sender, data, len); + pkt->markDoNotRetransmit(); + } + } + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_GRP_DATA: + case PAYLOAD_TYPE_GRP_TXT: { + int i = 0; + uint8_t channel_hash = pkt->payload[i++]; + + uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data + if (i + 2 >= pkt->payload_len) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); + // scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM) + GroupChannel channels[4]; + int num = searchChannelsByHash(&channel_hash, channels, 4); + // for each matching channel, try to decrypt data + for (int j = 0; j < num; j++) { + // decrypt, checking MAC is valid + uint8_t data[MAX_PACKET_PAYLOAD]; + int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i); + if (len > 0) { // success! + onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len); + break; + } + } + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_ADVERT: { + int i = 0; + Identity id; + memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE); i += PUB_KEY_SIZE; + + uint32_t timestamp; + memcpy(×tamp, &pkt->payload[i], 4); i += 4; + const uint8_t* signature = &pkt->payload[i]; i += SIGNATURE_SIZE; + + if (i > pkt->payload_len) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete advertisement packet", getLogDateTime()); + } else if (self_id.matches(id.pub_key)) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): receiving SELF advert packet", getLogDateTime()); + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); + uint8_t* app_data = &pkt->payload[i]; + int app_data_len = pkt->payload_len - i; + if (app_data_len > MAX_ADVERT_DATA_SIZE) { app_data_len = MAX_ADVERT_DATA_SIZE; } + + // check that signature is valid + bool is_ok; + { + uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; + int msg_len = 0; + memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; + memcpy(&message[msg_len], ×tamp, 4); msg_len += 4; + memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len; + + is_ok = id.verify(signature, message, msg_len); + } + if (is_ok) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): valid advertisement received!", getLogDateTime()); + onAdvertRecv(pkt, id, timestamp, app_data, app_data_len); + action = routeRecvPacket(pkt); + } else { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): received advertisement with forged signature! (app_data_len=%d)", getLogDateTime(), app_data_len); + } + } + break; + } + case PAYLOAD_TYPE_RAW_CUSTOM: { + if (pkt->isRouteDirect() && !_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); + onRawDataRecv(pkt); + //action = routeRecvPacket(pkt); don't flood route these (yet) + } + break; + } + case PAYLOAD_TYPE_MULTIPART: + if (pkt->payload_len > 2) { + uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent + (void)remaining; // ZEPHCORE: -Wunused-variable + uint8_t type = pkt->payload[0] & 0x0F; + + if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK + Packet tmp; + tmp.header = pkt->header; + 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); + + if (!_tables->wasSeen(&tmp)) { + _tables->markSeen(&tmp); + uint32_t ack_crc; + memcpy(&ack_crc, tmp.payload, 4); + + onAckRecv(&tmp, ack_crc); + //action = routeRecvPacket(&tmp); // NOTE: currently not needed, as multipart ACKs not sent Flood + } + } else { + // FUTURE: other multipart types?? + } + } + break; + + default: + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unknown payload type, header: %d", getLogDateTime(), (int) pkt->header); + // Don't flood route unknown packet types! action = routeRecvPacket(pkt); + break; + } + return action; } -DispatcherAction Mesh::routeRecvPacket(Packet *packet) -{ - uint8_t n = packet->getPathHashCount(); - if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit() - && (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) { - // append this node's hash to 'path' - self_id.copyHashTo(&packet->path[n * packet->getPathHashSize()], packet->getPathHashSize()); - packet->setPathHashCount(n + 1); - uint32_t h = ContentionTracker::computePacketHash32(packet); - _contention.trackRetransmit(h, (uint32_t)_ms->getMillis()); - uint32_t d = getRetransmitDelay(packet); - return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources - } - return ACTION_RELEASE; +void Mesh::removeSelfFromPath(Packet* pkt) { + // remove our hash from 'path' + pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count + + uint8_t sz = pkt->getPathHashSize(); + for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry' + memcpy(&pkt->path[k], &pkt->path[k + sz], sz); + } } -DispatcherAction Mesh::forwardMultipartDirect(Packet *pkt) -{ - uint8_t remaining = pkt->payload[0] >> 4; - uint8_t type = pkt->payload[0] & 0x0F; - if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { - Packet tmp; - tmp.header = pkt->header; - /* Trusted source: pkt->path is MAX_PATH_SIZE-sized. */ - tmp.path_len = Packet::copyPath(tmp.path, pkt->path, MAX_PATH_SIZE, pkt->path_len); - tmp.payload_len = pkt->payload_len - 1; - memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); - if (!_tables->wasSeen(&tmp)) { - _tables->markSeen(&tmp); - removeSelfFromPath(&tmp); - routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); - } - } - return ACTION_RELEASE; +DispatcherAction Mesh::routeRecvPacket(Packet* packet) { + uint8_t n = packet->getPathHashCount(); + if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit() + && (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) { + // append this node's hash to 'path' + self_id.copyHashTo(&packet->path[n * packet->getPathHashSize()], packet->getPathHashSize()); + packet->setPathHashCount(n + 1); + + // ZEPHCORE: track our retransmit so heard dupes can be counted. + _contention.trackRetransmit(ContentionTracker::computePacketHash32(packet), (uint32_t)_ms->getMillis()); + + uint32_t d = getRetransmitDelay(packet); + // as this propagates outwards, give it lower and lower priority + return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away + } + return ACTION_RELEASE; } -void Mesh::routeDirectRecvAcks(Packet *packet, uint32_t delay_millis) -{ - if (!packet->isMarkedDoNotRetransmit()) { - uint8_t extra = getExtraAckTransmitCount(); - while (extra > 0) { - delay_millis += getDirectRetransmitDelay(packet) + 300; - Packet *a1 = createMultiAck(packet->payload, packet->payload_len, extra); - if (a1) { - /* Trusted source: packet->path is MAX_PATH_SIZE-sized. */ - a1->path_len = Packet::copyPath(a1->path, packet->path, MAX_PATH_SIZE, packet->path_len); - a1->header &= ~PH_ROUTE_MASK; - a1->header |= ROUTE_TYPE_DIRECT; - sendPacket(a1, 0, delay_millis); - } - extra--; - } +DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { + uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent + uint8_t type = pkt->payload[0] & 0x0F; - Packet *a2 = createAck(packet->payload, packet->payload_len); - if (a2) { - /* Trusted source: packet->path is MAX_PATH_SIZE-sized. */ - a2->path_len = Packet::copyPath(a2->path, packet->path, MAX_PATH_SIZE, packet->path_len); - a2->header &= ~PH_ROUTE_MASK; - a2->header |= ROUTE_TYPE_DIRECT; - sendPacket(a2, 0, delay_millis); - } - } + if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK + Packet tmp; + tmp.header = pkt->header; + 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); + + if (!_tables->wasSeen(&tmp)) { // don't retransmit! + _tables->markSeen(&tmp); + removeSelfFromPath(&tmp); + routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2) + } + } + return ACTION_RELEASE; } -DispatcherAction Mesh::onRecvPacket(Packet *pkt) -{ - // Handle direct TRACE packets - if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { - /* payload_len must hold the 9-byte header (tag+auth+flags) before we - * read it; otherwise `len = payload_len - i` underflows below. */ - if (pkt->path_len < MAX_PATH_SIZE && pkt->payload_len >= 9) { - int i = 0; - uint32_t trace_tag; - memcpy(&trace_tag, &pkt->payload[i], 4); i += 4; - uint32_t auth_code; - memcpy(&auth_code, &pkt->payload[i], 4); i += 4; - uint8_t flags = pkt->payload[i++]; - uint8_t path_sz = flags & 0x03; +void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { + if (!packet->isMarkedDoNotRetransmit()) { + uint8_t extra = getExtraAckTransmitCount(); + while (extra > 0) { + delay_millis += getDirectRetransmitDelay(packet) + 300; + auto a1 = createMultiAck(packet->payload, packet->payload_len, extra); + if (a1) { + 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); + } + extra--; + } - uint8_t len = pkt->payload_len - i; - /* path_len * (1<path_len << path_sz; - if (offset >= len) { - onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); - } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->wasSeen(pkt)) { - _tables->markSeen(pkt); - pkt->path[pkt->path_len++] = (int8_t)(pkt->getSNR() * 4); - uint32_t d = getDirectRetransmitDelay(pkt); - return ACTION_RETRANSMIT_DELAYED(5, d); - } - } - return ACTION_RELEASE; - } - - // Handle direct CONTROL packets (zero-hop only) - if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) { - if (pkt->getPathHashCount() == 0) { - onControlDataRecv(pkt); - } - return ACTION_RELEASE; - } - - // Handle direct zero-hop ACKs (path_len=0) - if (pkt->isRouteDirect() && pkt->getPathHashCount() == 0 && pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - uint32_t ack_crc; - memcpy(&ack_crc, pkt->payload, 4); - onAckRecv(pkt, ack_crc); - return ACTION_RELEASE; - } - - if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) { - if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - uint32_t ack_crc; - memcpy(&ack_crc, pkt->payload, 4); - onAckRecv(pkt, ack_crc); - } - if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { - if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { - return forwardMultipartDirect(pkt); - } - if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - if (!_tables->wasSeen(pkt)) { - _tables->markSeen(pkt); - removeSelfFromPath(pkt); - routeDirectRecvAcks(pkt, 0); - } - return ACTION_RELEASE; - } - if (!_tables->wasSeen(pkt)) { - _tables->markSeen(pkt); - removeSelfFromPath(pkt); - return ACTION_RETRANSMIT_DELAYED(0, getDirectRetransmitDelay(pkt)); - } - } - return ACTION_RELEASE; - } - - if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE; - - /* Record dupes for contention tracking + reactive backoff */ - if (pkt->isRouteFlood()) { - uint32_t h = ContentionTracker::computePacketHash32(pkt); - if (_contention.recordDupeIfTracked(h, (uint32_t)_ms->getMillis())) { - extendPendingRetransmit(h); - } else if (passivelyTrackFloods()) { - /* First hearing of a flood we won't forward — track it so the - * EMA reflects local contention (companion-side awareness). */ - _contention.trackRetransmit(h, (uint32_t)_ms->getMillis()); - } - } - - DispatcherAction action = ACTION_RELEASE; - - switch (pkt->getPayloadType()) { - case PAYLOAD_TYPE_ACK: { - if (pkt->payload_len < 4) { - LOG_WRN("onRecvPacket: incomplete ACK (payload_len=%d)", pkt->payload_len); - } else { - uint32_t ack_crc; - memcpy(&ack_crc, pkt->payload, 4); - if (!_tables->wasSeen(pkt)) { - _tables->markSeen(pkt); - onAckRecv(pkt, ack_crc); - action = routeRecvPacket(pkt); - } - } - break; - } - case PAYLOAD_TYPE_PATH: - case PAYLOAD_TYPE_REQ: - case PAYLOAD_TYPE_RESPONSE: - case PAYLOAD_TYPE_TXT_MSG: { - int i = 0; - uint8_t dest_hash = pkt->payload[i++]; - uint8_t src_hash = pkt->payload[i++]; - - uint8_t *macAndData = &pkt->payload[i]; - if (i + CIPHER_MAC_SIZE >= (int)pkt->payload_len) { - LOG_WRN("onRecvPacket: incomplete packet (i=%d, payload_len=%d)", i, pkt->payload_len); - } else if (!_tables->wasSeen(pkt)) { - _tables->markSeen(pkt); - if (self_id.isHashMatch(&dest_hash)) { - int num = searchPeersByHash(&src_hash); - bool found = false; - for (int j = 0; j < num; j++) { - uint8_t secret[PUB_KEY_SIZE]; - getPeerSharedSecret(secret, j); - - uint8_t data[MAX_PACKET_PAYLOAD]; - int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); - if (len > 0) { - if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { - int k = 0; - uint8_t path_len = data[k++]; - if (!Packet::isValidPathLen(path_len)) { - LOG_WRN("onRecvPacket: invalid inner path_len 0x%02x", path_len); - break; - } - uint8_t hash_size = (path_len >> 6) + 1; - uint8_t hash_count = path_len & 63; - int path_bytes = hash_size * hash_count; - if (k + path_bytes + 1 > len) { - LOG_WRN("onRecvPacket: PATH payload truncated (k=%d path_bytes=%d len=%d)", k, path_bytes, len); - break; - } - uint8_t *path = &data[k]; k += path_bytes; - uint8_t extra_type = data[k++] & 0x0F; - uint8_t *extra = &data[k]; - uint8_t extra_len = (uint8_t)(len - k); - if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) { - if (pkt->isRouteFlood()) { - Packet *rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, nullptr, 0); - if (rpath) sendDirect(rpath, path, path_len, 500); - } - } - } else { - onPeerDataRecv(pkt, pkt->getPayloadType(), j, secret, data, len); - } - found = true; - break; - } - } - if (found) { - pkt->markDoNotRetransmit(); - } else { - LOG_WRN("onRecvPacket: no peer could decrypt message"); - } - } - action = routeRecvPacket(pkt); - } - break; - } - case PAYLOAD_TYPE_ANON_REQ: { - int i = 0; - uint8_t dest_hash = pkt->payload[i++]; - uint8_t *sender_pub_key = &pkt->payload[i]; i += PUB_KEY_SIZE; - - uint8_t *macAndData = &pkt->payload[i]; - if (i + 2 >= (int)pkt->payload_len) { - // incomplete packet - } else if (!_tables->wasSeen(pkt)) { - _tables->markSeen(pkt); - if (self_id.isHashMatch(&dest_hash)) { - Identity sender(sender_pub_key); - uint8_t secret[PUB_KEY_SIZE]; - self_id.calcSharedSecret(secret, sender); - - uint8_t data[MAX_PACKET_PAYLOAD]; - int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); - if (len > 0) { - onAnonDataRecv(pkt, secret, sender, data, len); - pkt->markDoNotRetransmit(); - } - } - action = routeRecvPacket(pkt); - } - break; - } - case PAYLOAD_TYPE_GRP_DATA: - case PAYLOAD_TYPE_GRP_TXT: { - int i = 0; - uint8_t channel_hash = pkt->payload[i++]; - - uint8_t *macAndData = &pkt->payload[i]; - if (i + 2 >= (int)pkt->payload_len) { - // incomplete packet - } else if (!_tables->wasSeen(pkt)) { - _tables->markSeen(pkt); - GroupChannel channels[4]; - int num = searchChannelsByHash(&channel_hash, channels, 4); - for (int j = 0; j < num; j++) { - uint8_t data[MAX_PACKET_PAYLOAD]; - int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i); - if (len > 0) { - onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len); - break; - } - } - action = routeRecvPacket(pkt); - } - break; - } - case PAYLOAD_TYPE_ADVERT: { - int i = 0; - Identity id; - memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE); - i += PUB_KEY_SIZE; - uint32_t timestamp; - memcpy(×tamp, &pkt->payload[i], 4); - i += 4; - const uint8_t *signature = &pkt->payload[i]; - i += SIGNATURE_SIZE; - if (i <= (int)pkt->payload_len && !self_id.matches(id.pub_key) && !_tables->wasSeen(pkt)) { - _tables->markSeen(pkt); - uint8_t *app_data = (uint8_t *)&pkt->payload[i]; - size_t app_data_len = pkt->payload_len - (size_t)i; - if (app_data_len > MAX_ADVERT_DATA_SIZE) app_data_len = MAX_ADVERT_DATA_SIZE; - uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; - int msg_len = 0; - memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; - memcpy(&message[msg_len], ×tamp, 4); msg_len += 4; - memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len; - if (id.verify(signature, message, msg_len)) { - onAdvertRecv(pkt, id, timestamp, app_data, app_data_len); - action = routeRecvPacket(pkt); - } - } - break; - } - case PAYLOAD_TYPE_RAW_CUSTOM: - if (pkt->isRouteDirect() && !_tables->wasSeen(pkt)) { - _tables->markSeen(pkt); - onRawDataRecv(pkt); - } - break; - case PAYLOAD_TYPE_MULTIPART: - if (pkt->payload_len > 2) { - /* uint8_t remaining = pkt->payload[0] >> 4; */ /* Reserved for future multipart support */ - uint8_t type = pkt->payload[0] & 0x0F; - - if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { - Packet tmp; - tmp.header = pkt->header; - /* Trusted source: pkt->path is MAX_PATH_SIZE-sized. */ - tmp.path_len = Packet::copyPath(tmp.path, pkt->path, MAX_PATH_SIZE, pkt->path_len); - tmp.payload_len = pkt->payload_len - 1; - memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); - - if (!_tables->wasSeen(&tmp)) { - _tables->markSeen(&tmp); - uint32_t ack_crc; - memcpy(&ack_crc, tmp.payload, 4); - onAckRecv(&tmp, ack_crc); - } - } - } - break; - default: - break; - } - return action; + auto a2 = createAck(packet->payload, packet->payload_len); + if (a2) { + 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); + } + } } -Packet *Mesh::createAdvert(const LocalIdentity &id, const uint8_t *app_data, size_t app_data_len) -{ - if (app_data_len > MAX_ADVERT_DATA_SIZE) return nullptr; +Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { + if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createAdvert(): error, packet pool empty", getLogDateTime()); + return NULL; + } - packet->header = (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); - int len = 0; - memcpy(&packet->payload[len], id.pub_key, PUB_KEY_SIZE); - len += PUB_KEY_SIZE; - uint32_t emitted_timestamp = _rtc->getCurrentTime(); - memcpy(&packet->payload[len], &emitted_timestamp, 4); - len += 4; - uint8_t *signature = &packet->payload[len]; - len += SIGNATURE_SIZE; - if (app_data && app_data_len > 0) { - memcpy(&packet->payload[len], app_data, app_data_len); - len += (int)app_data_len; - } - packet->payload_len = len; + packet->header = (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); // ROUTE_TYPE_* is set later - uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; - int msg_len = 0; - memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; - memcpy(&message[msg_len], &emitted_timestamp, 4); msg_len += 4; - if (app_data && app_data_len > 0) { - memcpy(&message[msg_len], app_data, app_data_len); msg_len += (int)app_data_len; - } - id.sign(signature, message, msg_len); - return packet; -} + int len = 0; + memcpy(&packet->payload[len], id.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE; -Packet *Mesh::createAck(const uint8_t *ack, uint8_t len) -{ - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); - memcpy(packet->payload, ack, len); - packet->payload_len = len; - return packet; -} + uint32_t emitted_timestamp = _rtc->getCurrentTime(); + memcpy(&packet->payload[len], &emitted_timestamp, 4); len += 4; -Packet *Mesh::createMultiAck(const uint8_t *ack, uint8_t len, uint8_t remaining) -{ - if (1 + (size_t)len > MAX_PACKET_PAYLOAD) return nullptr; - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); - packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK; - memcpy(&packet->payload[1], ack, len); - packet->payload_len = 1 + len; - return packet; -} + uint8_t* signature = &packet->payload[len]; len += SIGNATURE_SIZE; // will fill this in later -Packet *Mesh::createControlData(const uint8_t *data, size_t len) -{ - if (len > sizeof(Packet::payload)) return nullptr; - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT); - memcpy(packet->payload, data, len); - packet->payload_len = (uint16_t)len; - return packet; -} + if (app_data_len > 0) { // ZEPHCORE: app_data may be NULL + memcpy(&packet->payload[len], app_data, app_data_len); len += app_data_len; + } -void Mesh::sendFlood(Packet *packet, uint32_t delay_millis, uint8_t path_hash_size) -{ - if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { - releasePacket(packet); - return; - } - if (path_hash_size == 0 || path_hash_size > 3) { - LOG_WRN("sendFlood: invalid path_hash_size"); - releasePacket(packet); - return; - } - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_FLOOD; - packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ - uint8_t pri; - if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { - pri = 2; - } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { - pri = 3; - } else { - pri = 1; - } - sendPacket(packet, pri, delay_millis + getInitialFloodJitter(packet)); -} + packet->payload_len = len; -void Mesh::sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis, uint8_t path_hash_size) -{ - if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { - releasePacket(packet); - return; - } - if (path_hash_size == 0 || path_hash_size > 3) { - LOG_WRN("sendFlood: invalid path_hash_size"); - releasePacket(packet); - return; - } - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD; - packet->transport_codes[0] = transport_codes[0]; - packet->transport_codes[1] = transport_codes[1]; - packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ - uint8_t pri; - if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { - pri = 2; - } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { - pri = 3; - } else { - pri = 1; - } - sendPacket(packet, pri, delay_millis + getInitialFloodJitter(packet)); -} + { + uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; + int msg_len = 0; + memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; + memcpy(&message[msg_len], &emitted_timestamp, 4); msg_len += 4; + if (app_data_len > 0) { // ZEPHCORE: app_data may be NULL + memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len; + } -void Mesh::sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis) -{ - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_DIRECT; + id.sign(signature, message, msg_len); + } - uint8_t pri; - if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { - /* For TRACE packets, path is appended to end of PAYLOAD (used for SNRs). - * Guard the append: a crafted CMD_SEND_TRACE_PATH frame can set a - * non-standard path hash size (path_sz 2/3) that the caller's hop-count - * validation doesn't byte-bound, so payload_len + path_len could spill - * past payload[]. */ - if ((int)packet->payload_len + path_len > MAX_PACKET_PAYLOAD) { - releasePacket(packet); - return; - } - memcpy(&packet->payload[packet->payload_len], path, path_len); - packet->payload_len += path_len; - packet->path_len = 0; - pri = 5; - } else { - /* path is caller-supplied; existing contract is that the caller has - * ensured at least the decoded path-byte-count is readable. Pass - * MAX_PATH_SIZE as the upper bound — this preserves existing - * behavior while making the API explicit. */ - packet->path_len = Packet::copyPath(packet->path, path, MAX_PATH_SIZE, path_len); - if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { - pri = 1; - } else { - pri = 0; - } - } - - _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ - sendPacket(packet, pri, delay_millis); -} - -void Mesh::sendZeroHop(Packet *packet, uint32_t delay_millis) -{ - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_DIRECT; - packet->path_len = 0; - _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ - sendPacket(packet, 0, delay_millis); -} - -void Mesh::sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis) -{ - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_TRANSPORT_DIRECT; - packet->transport_codes[0] = transport_codes[0]; - packet->transport_codes[1] = transport_codes[1]; - packet->path_len = 0; - _tables->markSeen(packet); /* mark as already sent in case it is rebroadcast back to us */ - sendPacket(packet, 0, delay_millis); + return packet; } #define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE) -Packet *Mesh::createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, - uint8_t extra_type, const uint8_t *extra, size_t extra_len) -{ - uint8_t dest_hash[PATH_HASH_SIZE]; - dest.copyHashTo(dest_hash); - return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len); +Packet* Mesh::createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) { + uint8_t dest_hash[PATH_HASH_SIZE]; + dest.copyHashTo(dest_hash); + return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len); } -Packet *Mesh::createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, - uint8_t extra_type, const uint8_t *extra, size_t extra_len) -{ - uint8_t path_hash_size = (path_len >> 6) + 1; - uint8_t path_hash_count = path_len & 63; +Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) { + uint8_t path_hash_size = (path_len >> 6) + 1; + uint8_t path_hash_count = path_len & 63; - if (path_hash_count*path_hash_size + extra_len + 5 > MAX_COMBINED_PATH) return nullptr; + if (path_hash_count*path_hash_size + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!! - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createPathReturn(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT); + int len = 0; + memcpy(&packet->payload[len], dest_hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; // dest hash + len += self_id.copyHashTo(&packet->payload[len]); // src hash - int len = 0; - memcpy(&packet->payload[len], dest_hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; - len += self_id.copyHashTo(&packet->payload[len]); + { + int data_len = 0; + uint8_t data[MAX_PACKET_PAYLOAD]; - { - int data_len = 0; - uint8_t data[MAX_PACKET_PAYLOAD]; + data[data_len++] = path_len; + memcpy(&data[data_len], path, path_hash_count*path_hash_size); data_len += path_hash_count*path_hash_size; + if (extra_len > 0) { + data[data_len++] = extra_type; + memcpy(&data[data_len], extra, extra_len); data_len += extra_len; + } else { + // append a timestamp, or random blob (to make packet_hash unique) + data[data_len++] = 0xFF; // dummy payload type + getRNG()->random(&data[data_len], 4); data_len += 4; + } - data[data_len++] = path_len; - memcpy(&data[data_len], path, path_hash_count*path_hash_size); data_len += path_hash_count*path_hash_size; - if (extra_len > 0) { - data[data_len++] = extra_type; - memcpy(&data[data_len], extra, extra_len); data_len += extra_len; - } else { - data[data_len++] = 0xFF; // dummy payload type - _rng->random(&data[data_len], 4); data_len += 4; - } + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + } - len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); - } + packet->payload_len = len; - packet->payload_len = len; - return packet; + return packet; } -Packet *Mesh::createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len) -{ - if (type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE) { - if (data_len + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) { - LOG_WRN("createDatagram: data too large"); - return nullptr; - } - } else { - LOG_WRN("createDatagram: unsupported type %d", type); - return nullptr; - } +Packet* Mesh::createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len) { + if (type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE) { + if (data_len + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL; + } else { + return NULL; // invalid type + } - Packet *packet = obtainNewPacket(); - if (packet == nullptr) { - LOG_ERR("createDatagram: packet alloc failed"); - return nullptr; - } + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createDatagram(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - packet->header = (type << PH_TYPE_SHIFT); + int len = 0; + len += dest.copyHashTo(&packet->payload[len]); // dest hash + len += self_id.copyHashTo(&packet->payload[len]); // src hash + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); - int len = 0; - len += dest.copyHashTo(&packet->payload[len]); - len += self_id.copyHashTo(&packet->payload[len]); - len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + packet->payload_len = len; - packet->payload_len = len; - return packet; + return packet; } -Packet *Mesh::createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, - const uint8_t *secret, const uint8_t *data, size_t data_len) -{ - if (type == PAYLOAD_TYPE_ANON_REQ) { - if (data_len + 1 + PUB_KEY_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr; - } else { - return nullptr; - } +Packet* Mesh::createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len) { + if (type == PAYLOAD_TYPE_ANON_REQ) { + if (data_len + 1 + PUB_KEY_SIZE + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL; + } else { + return NULL; // invalid type + } - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createAnonDatagram(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - packet->header = (type << PH_TYPE_SHIFT); + int len = 0; + if (type == PAYLOAD_TYPE_ANON_REQ) { + len += dest.copyHashTo(&packet->payload[len]); // dest hash + memcpy(&packet->payload[len], sender.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE; // sender pub_key + } else { + // FUTURE: + } + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); - int len = 0; - if (type == PAYLOAD_TYPE_ANON_REQ) { - len += dest.copyHashTo(&packet->payload[len]); - memcpy(&packet->payload[len], sender.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE; - } - len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + packet->payload_len = len; - packet->payload_len = len; - return packet; + return packet; } -Packet *Mesh::createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len) -{ - if (!(type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA)) return nullptr; - if (data_len + 1 + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr; +Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len) { + if (!(type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA)) return NULL; // invalid type + if (data_len + 1 + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL; // too long - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createGroupDatagram(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - packet->header = (type << PH_TYPE_SHIFT); + int len = 0; + memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; + len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], data, data_len); - int len = 0; - memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; - len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], data, data_len); + packet->payload_len = len; - packet->payload_len = len; - return packet; + return packet; } -Packet *Mesh::createRawData(const uint8_t *data, size_t len) -{ - if (len > sizeof(Packet::payload)) return nullptr; +Packet* Mesh::createAck(const uint8_t* ack, uint8_t len) { + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createAck(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; + memcpy(packet->payload, ack, len); + packet->payload_len = len; - packet->header = (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT); - memcpy(packet->payload, data, len); - packet->payload_len = (uint16_t)len; - - return packet; + return packet; } -Packet *Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) -{ - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; +Packet* Mesh::createMultiAck(const uint8_t* ack, uint8_t len, uint8_t remaining) { + if (1 + (size_t)len > MAX_PACKET_PAYLOAD) return NULL; // ZEPHCORE: bound - packet->header = (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT); - memcpy(packet->payload, &tag, 4); - memcpy(&packet->payload[4], &auth_code, 4); - packet->payload[8] = flags; - packet->payload_len = 9; + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - return packet; + packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK; + memcpy(&packet->payload[1], ack, len); + packet->payload_len = 1 + len; + + return packet; } -} /* namespace mesh */ +Packet* Mesh::createRawData(const uint8_t* data, size_t len) { + if (len > sizeof(Packet::payload)) return NULL; // invalid arg + + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createRawData(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later + + memcpy(packet->payload, data, len); + packet->payload_len = len; + + return packet; +} + +Packet* Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) { + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createTrace(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later + + memcpy(packet->payload, &tag, 4); + memcpy(&packet->payload[4], &auth_code, 4); + packet->payload[8] = flags; + packet->payload_len = 9; // NOTE: path will be appended to payload[] later + + return packet; +} + +Packet* Mesh::createControlData(const uint8_t* data, size_t len) { + if (len > sizeof(Packet::payload)) return NULL; // invalid arg + + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createControlData(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later + + memcpy(packet->payload, data, len); + packet->payload_len = len; + + return packet; +} + +void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) { + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); + releasePacket(packet); // ZEPHCORE: don't leak the pool slot + return; + } + if (path_hash_size == 0 || path_hash_size > 3) { + MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime()); + releasePacket(packet); // ZEPHCORE: don't leak the pool slot + return; + } + + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_FLOOD; + packet->setPathHashSizeAndCount(path_hash_size, 0); + + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 2; + } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { + pri = 3; // de-prioritie these + } else { + pri = 1; + } + sendPacket(packet, pri, delay_millis + getInitialFloodJitter(packet)); // ZEPHCORE: initial-flood jitter +} + +void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) { + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); + releasePacket(packet); // ZEPHCORE: don't leak the pool slot + return; + } + if (path_hash_size == 0 || path_hash_size > 3) { + MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime()); + releasePacket(packet); // ZEPHCORE: don't leak the pool slot + return; + } + + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD; + packet->transport_codes[0] = transport_codes[0]; + packet->transport_codes[1] = transport_codes[1]; + packet->setPathHashSizeAndCount(path_hash_size, 0); + + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 2; + } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { + pri = 3; // de-prioritie these + } else { + pri = 1; + } + sendPacket(packet, pri, delay_millis + getInitialFloodJitter(packet)); // ZEPHCORE: initial-flood jitter +} + +void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis) { + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_DIRECT; + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { // TRACE packets are different + // ZEPHCORE: a crafted CMD_SEND_TRACE_PATH can set a path hash size the + // caller's hop-count check doesn't byte-bound; keep the append in payload[]. + if ((int)packet->payload_len + path_len > MAX_PACKET_PAYLOAD) { + releasePacket(packet); + return; + } + // for TRACE packets, path is appended to end of PAYLOAD. (path is used for SNR's) + memcpy(&packet->payload[packet->payload_len], path, path_len); // NOTE: path_len here can be > 64, and NOT in the new scheme + packet->payload_len += path_len; + + packet->path_len = 0; + pri = 5; // maybe make this configurable + } else { + packet->path_len = Packet::copyPath(packet->path, path, path_len); + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 1; // slightly less priority + } else { + pri = 0; + } + } + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + sendPacket(packet, pri, delay_millis); +} + +void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) { + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_DIRECT; + + packet->path_len = 0; // path_len of zero means Zero Hop + + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + + sendPacket(packet, 0, delay_millis); +} + +void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) { + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_TRANSPORT_DIRECT; + packet->transport_codes[0] = transport_codes[0]; + packet->transport_codes[1] = transport_codes[1]; + + packet->path_len = 0; // path_len of zero means Zero Hop + + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + + sendPacket(packet, 0, delay_millis); +} + +} \ No newline at end of file diff --git a/zephcore/src/Packet.cpp b/zephcore/src/Packet.cpp index 7497fa3..5234c0f 100644 --- a/zephcore/src/Packet.cpp +++ b/zephcore/src/Packet.cpp @@ -1,124 +1,107 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Packet implementation - */ - #include -#include +#include // ZEPHCORE: SHA-256 via Utils (no Arduino SHA256 class) #include +// ZEPHCORE: log module backing MESH_DEBUG_PRINTLN (see MeshCore.h). +#include +LOG_MODULE_DECLARE(zephcore_mesh, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); + namespace mesh { -Packet::Packet() -{ - header = 0; - path_len = 0; - payload_len = 0; +Packet::Packet() { + header = 0; + path_len = 0; + payload_len = 0; } -bool Packet::isValidPathLen(uint8_t path_len) -{ - uint8_t hash_count = path_len & 63; - uint8_t hash_size = (path_len >> 6) + 1; - if (hash_size == 4) return false; // Reserved for future - return hash_count * hash_size <= MAX_PATH_SIZE; +bool Packet::isValidPathLen(uint8_t path_len) { + uint8_t hash_count = path_len & 63; + uint8_t hash_size = (path_len >> 6) + 1; + if (hash_size == 4) return false; // Reserved for future + return hash_count*hash_size <= MAX_PATH_SIZE; } -size_t Packet::writePath(uint8_t *dest, const uint8_t *src, size_t src_len, uint8_t path_len) -{ - uint8_t hash_count = path_len & 63; - uint8_t hash_size = (path_len >> 6) + 1; - size_t len = hash_count * hash_size; - if (len > MAX_PATH_SIZE) { - return 0; // Decoded path exceeds max - } - if (len > src_len) { - return 0; // Would read past source buffer (caller-supplied bound) - } - memcpy(dest, src, len); - return len; +size_t Packet::writePath(uint8_t* dest, const uint8_t* src, size_t src_len, uint8_t path_len) { + uint8_t hash_count = path_len & 63; + uint8_t hash_size = (path_len >> 6) + 1; + size_t len = hash_count*hash_size; + if (len > MAX_PATH_SIZE) { + MESH_DEBUG_PRINTLN("Packet::copyPath, invalid path_len=%d", (uint32_t)path_len); + return 0; // Error + } + if (len > src_len) return 0; // ZEPHCORE: would read past the caller-supplied source bound + memcpy(dest, src, len); + return len; } -uint8_t Packet::copyPath(uint8_t *dest, const uint8_t *src, size_t src_len, uint8_t path_len) -{ - /* A zero-hop path copies no bytes, so writePath()'s 0 return is ambiguous - * there: it means both "rejected" and "nothing to copy". Taking it as a - * rejection discards the hash-size bits (7:6), which are all a zero-hop - * path_len carries -- a directly-heard 2-byte-hash advert (0x40) came out - * as 0x00 and every consumer then read it as 1-byte. Decode the hop count - * first so the empty case is validated instead of copied. - * - * isValidPathLen() still rejects the reserved hash size 4, so - * OUT_PATH_UNKNOWN (0xFF) and 0xC0 keep returning 0 as before; callers - * that test `copyPath(...) == 0` for a malformed path are unaffected, as - * long as they already special-case a path_len of 0 (which is itself a - * legal zero-hop, 1-byte-hash path and returns 0 either way). */ - if ((path_len & 0x3F) == 0) { - return isValidPathLen(path_len) ? path_len : 0; - } - return writePath(dest, src, src_len, path_len) > 0 ? path_len : 0; +uint8_t Packet::copyPath(uint8_t* dest, const uint8_t* src, size_t src_len, uint8_t path_len) { + // ZEPHCORE: 0 means "rejected" (upstream always returns path_len). A zero-hop + // path copies nothing, so validate it instead of copying: a directly-heard + // 2-byte-hash advert (0x40) must keep its hash-size bits. + if ((path_len & 0x3F) == 0) { + return isValidPathLen(path_len) ? path_len : 0; + } + return writePath(dest, src, src_len, path_len) > 0 ? path_len : 0; } -int Packet::getRawLength() const -{ - return 2 + getPathByteLen() + payload_len + (hasTransportCodes() ? 4 : 0); +int Packet::getRawLength() const { + return 2 + getPathByteLen() + payload_len + (hasTransportCodes() ? 4 : 0); } -void Packet::calculatePacketHash(uint8_t *hash) const -{ - uint8_t t = getPayloadType(); - if (t == PAYLOAD_TYPE_TRACE) { - uint8_t buf[2 + MAX_PACKET_PAYLOAD]; - buf[0] = t; - memcpy(buf + 1, &path_len, sizeof(path_len)); - memcpy(buf + 2, payload, payload_len); - Utils::sha256(hash, MAX_HASH_SIZE, buf, 2 + payload_len); - } else { - uint8_t buf[1 + MAX_PACKET_PAYLOAD]; - buf[0] = t; - memcpy(buf + 1, payload, payload_len); - Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + payload_len); - } +void Packet::calculatePacketHash(uint8_t* hash) const { + // ZEPHCORE: one-shot Utils::sha256 over (type [, path_len], payload), same digest + // as upstream's incremental SHA256. + uint8_t t = getPayloadType(); + if (t == PAYLOAD_TYPE_TRACE) { // CAVEAT: TRACE packets can revisit same node on return path + uint8_t buf[2 + MAX_PACKET_PAYLOAD]; + buf[0] = t; + memcpy(buf + 1, &path_len, sizeof(path_len)); + memcpy(buf + 2, payload, payload_len); + Utils::sha256(hash, MAX_HASH_SIZE, buf, 2 + payload_len); + } else { + uint8_t buf[1 + MAX_PACKET_PAYLOAD]; + buf[0] = t; + memcpy(buf + 1, payload, payload_len); + Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + payload_len); + } } -uint8_t Packet::writeTo(uint8_t dest[]) const -{ - uint8_t i = 0; - dest[i++] = header; - if (hasTransportCodes()) { - memcpy(&dest[i], &transport_codes[0], 2); i += 2; - memcpy(&dest[i], &transport_codes[1], 2); i += 2; - } - dest[i++] = path_len; - /* Trusted source: Packet::path is MAX_PATH_SIZE-sized. */ - i += writePath(&dest[i], path, MAX_PATH_SIZE, path_len); - memcpy(&dest[i], payload, payload_len); i += payload_len; - return i; +uint8_t Packet::writeTo(uint8_t dest[]) const { + uint8_t i = 0; + dest[i++] = header; + if (hasTransportCodes()) { + memcpy(&dest[i], &transport_codes[0], 2); i += 2; + memcpy(&dest[i], &transport_codes[1], 2); i += 2; + } + dest[i++] = path_len; + i += writePath(&dest[i], path, path_len); + memcpy(&dest[i], payload, payload_len); i += payload_len; + return i; } -bool Packet::readFrom(const uint8_t src[], uint8_t len) -{ - uint8_t i = 0; - if (len < 2) return false; - header = src[i++]; - if (hasTransportCodes()) { - if (len < 6) return false; - memcpy(&transport_codes[0], &src[i], 2); i += 2; - memcpy(&transport_codes[1], &src[i], 2); i += 2; - } else { - transport_codes[0] = transport_codes[1] = 0; - } - path_len = src[i++]; - if (!isValidPathLen(path_len)) return false; +bool Packet::readFrom(const uint8_t src[], uint8_t len) { + uint8_t i = 0; + if (len < 2) return false; // ZEPHCORE: bounds + header = src[i++]; + if (hasTransportCodes()) { + if (len < 6) return false; // ZEPHCORE + memcpy(&transport_codes[0], &src[i], 2); i += 2; + memcpy(&transport_codes[1], &src[i], 2); i += 2; + } else { + transport_codes[0] = transport_codes[1] = 0; + } + path_len = src[i++]; + if (!isValidPathLen(path_len)) return false; // bad encoding - uint8_t bl = getPathByteLen(); - if ((uint16_t)i + bl > len) return false; - memcpy(path, &src[i], bl); i += bl; - if (i >= len) return false; - payload_len = len - i; - if (payload_len > sizeof(payload)) return false; - memcpy(payload, &src[i], payload_len); - return true; + uint8_t bl = getPathByteLen(); + if ((uint16_t)i + bl > len) return false; // ZEPHCORE + memcpy(path, &src[i], bl); i += bl; + + if (i >= len) return false; // bad encoding + payload_len = len - i; + if (payload_len > sizeof(payload)) return false; // bad encoding + memcpy(payload, &src[i], payload_len); //i += payload_len; + return true; // success } -} /* namespace mesh */ +} \ No newline at end of file diff --git a/zephcore/include/mesh/RadioIncludes.h b/zephcore/src/RadioIncludes.h similarity index 97% rename from zephcore/include/mesh/RadioIncludes.h rename to zephcore/src/RadioIncludes.h index 345e403..37fea0a 100644 --- a/zephcore/include/mesh/RadioIncludes.h +++ b/zephcore/src/RadioIncludes.h @@ -12,7 +12,7 @@ #if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) || DT_NODE_HAS_STATUS(DT_ALIAS(lora0), okay) #include -#include +#include #include #include #include diff --git a/zephcore/src/StaticPoolPacketManager.cpp b/zephcore/src/StaticPoolPacketManager.cpp index 2bf1ca1..b1fab98 100644 --- a/zephcore/src/StaticPoolPacketManager.cpp +++ b/zephcore/src/StaticPoolPacketManager.cpp @@ -1,215 +1,191 @@ -/* - * SPDX-License-Identifier: MIT - * Static pool PacketManager - no dynamic allocation - */ - #include -#include #include +// ZEPHCORE: log module for the pool's warnings. #include LOG_MODULE_REGISTER(zephcore_pktpool, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); -namespace mesh { +// ZEPHCORE: static storage — no heap in the packet path. Upstream sizes the pool +// at runtime (new[]); here pool and queues are compile-time and file-local. +#define POOL_SIZE 32 +#define QUEUE_SIZE 32 -#define POOL_SIZE 32 -#define QUEUE_SIZE 32 +namespace { struct PacketQueue { - Packet *_table[QUEUE_SIZE]; - uint8_t _pri_table[QUEUE_SIZE]; - uint32_t _schedule_table[QUEUE_SIZE]; - int _num; + mesh::Packet* _table[QUEUE_SIZE]; + uint8_t _pri_table[QUEUE_SIZE]; + uint32_t _schedule_table[QUEUE_SIZE]; + int _num; - PacketQueue() : _num(0) { - memset(_table, 0, sizeof(_table)); - } + PacketQueue() : _num(0) { + memset(_table, 0, sizeof(_table)); + } - int countBefore(uint32_t now) const { - int n = 0; - for (int j = 0; j < _num; j++) { - if ((int32_t)(_schedule_table[j] - now) > 0) continue; - n++; - } - return n; - } + int count() const { return _num; } + mesh::Packet* itemAt(int i) const { + if (i < 0 || i >= _num) return NULL; + return _table[i]; + } - Packet *get(uint32_t now) { - uint8_t min_pri = 0xFF; - int best_idx = -1; - for (int j = 0; j < _num; j++) { - if ((int32_t)(_schedule_table[j] - now) > 0) continue; - if (_pri_table[j] < min_pri) { - min_pri = _pri_table[j]; - best_idx = j; - } - } - if (best_idx < 0) return nullptr; + int countBefore(uint32_t now) const { + if (now == 0xFFFFFFFF) return _num; // sentinel: count all entries regardless of schedule - return removeByIdx(best_idx); - } + int n = 0; + for (int j = 0; j < _num; j++) { + if ((int32_t)(_schedule_table[j] - now) > 0) continue; // scheduled for future... ignore for now + n++; + } + return n; + } - Packet *removeByIdx(int i) { - if (i >= _num) return nullptr; - Packet *item = _table[i]; - for (int j = i; j < _num - 1; j++) { - _table[j] = _table[j + 1]; - _pri_table[j] = _pri_table[j + 1]; - _schedule_table[j] = _schedule_table[j + 1]; - } - _num--; - return item; - } + mesh::Packet* get(uint32_t now) { + uint8_t min_pri = 0xFF; + int best_idx = -1; + for (int j = 0; j < _num; j++) { + if ((int32_t)(_schedule_table[j] - now) > 0) continue; // scheduled for future... ignore for now + if (_pri_table[j] < min_pri) { // select most important priority amongst non-future entries + min_pri = _pri_table[j]; + best_idx = j; + } + } + if (best_idx < 0) return NULL; // empty, or all items are still in the future - bool add(Packet *packet, uint8_t priority, uint32_t scheduled_for) { - if (_num >= QUEUE_SIZE) return false; - _table[_num] = packet; - _pri_table[_num] = priority; - _schedule_table[_num] = scheduled_for; - _num++; - return true; - } + return removeByIdx(best_idx); + } - /* Find index of the entry with the highest priority number (least - * important). Returns -1 when the queue is empty. */ - int findLowestPriority() const { - if (_num == 0) return -1; - uint8_t worst = 0; - int idx = 0; - for (int j = 0; j < _num; j++) { - if (_pri_table[j] >= worst) { - worst = _pri_table[j]; - idx = j; - } - } - return idx; - } + mesh::Packet* removeByIdx(int i) { + if (i < 0 || i >= _num) return NULL; // invalid index - uint32_t scheduleAt(int i) const { - return (i < _num) ? _schedule_table[i] : 0; - } + mesh::Packet* item = _table[i]; + _num--; + while (i < _num) { + _table[i] = _table[i+1]; + _pri_table[i] = _pri_table[i+1]; + _schedule_table[i] = _schedule_table[i+1]; + i++; + } + return item; + } - /* Priority of the next packet that get(now) would return, without - * removing it. Returns 0xFF if no due packet exists. */ - uint8_t peekPriority(uint32_t now) const { - uint8_t best = 0xFF; - for (int j = 0; j < _num; j++) { - if ((int32_t)(_schedule_table[j] - now) > 0) continue; - if (_pri_table[j] < best) best = _pri_table[j]; - } - return best; - } + bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { + if (_num == QUEUE_SIZE) { + return false; + } + _table[_num] = packet; + _pri_table[_num] = priority; + _schedule_table[_num] = scheduled_for; + _num++; + return true; + } - bool reschedule(int i, uint32_t new_scheduled_for) { - if (i >= _num) return false; - _schedule_table[i] = new_scheduled_for; - return true; - } - - int count() const { return _num; } - Packet *itemAt(int i) const { return (i < _num) ? _table[i] : nullptr; } + // ZEPHCORE: eviction and schedule access (reactive backoff, re-queue priority). + int findLowestPriority() const { // least important entry, -1 when empty + if (_num == 0) return -1; + uint8_t worst = 0; + int idx = 0; + for (int j = 0; j < _num; j++) { + if (_pri_table[j] >= worst) { + worst = _pri_table[j]; + idx = j; + } + } + return idx; + } + uint32_t scheduleAt(int i) const { return (i < _num) ? _schedule_table[i] : 0; } + uint8_t peekPriority(uint32_t now) const { // what get(now) would return; 0xFF if none due + uint8_t best = 0xFF; + for (int j = 0; j < _num; j++) { + if ((int32_t)(_schedule_table[j] - now) > 0) continue; + if (_pri_table[j] < best) best = _pri_table[j]; + } + return best; + } + bool reschedule(int i, uint32_t new_scheduled_for) { + if (i >= _num) return false; + _schedule_table[i] = new_scheduled_for; + return true; + } }; -static Packet _packet_pool[POOL_SIZE]; -static PacketQueue _unused; -static PacketQueue _send_queue; -static bool _initialized = false; +mesh::Packet packet_pool[POOL_SIZE]; +PacketQueue unused, send_queue; +bool initialized = false; -static void init_pool() { - if (_initialized) return; - _initialized = true; - for (int i = 0; i < POOL_SIZE; i++) { - _unused.add(&_packet_pool[i], 0, 0); - } +// load up our unusued Packet pool (on first use: no static-init ordering hazard) +void init_pool() { + if (initialized) return; + initialized = true; + for (int i = 0; i < POOL_SIZE; i++) { + unused.add(&packet_pool[i], 0, 0); + } } -Packet *StaticPoolPacketManager::allocNew() -{ - init_pool(); - return _unused.removeByIdx(0); +} // namespace + +mesh::Packet* StaticPoolPacketManager::allocNew() { + init_pool(); // ZEPHCORE + return unused.removeByIdx(0); // just get first one (returns NULL if empty) } -void StaticPoolPacketManager::free(Packet *packet) -{ - if (packet == nullptr) return; - - /* _unused is sized to hold the whole pool, so the only way this add can - * fail is a slot being returned twice. That is silent corruption — the - * second free drops the packet on the floor and the pool shrinks by one - * for the rest of the run — so say so rather than ignoring the return. - * Upstream 1d5283de. */ - if (!_unused.add(packet, 0, 0)) { - LOG_WRN("free: unused queue full, possible double-free"); - } +void StaticPoolPacketManager::free(mesh::Packet* packet) { + if (packet == NULL) return; + if (!unused.add(packet, 0, 0)) { + LOG_WRN("free: unused queue full, possible double-free"); // ZEPHCORE: WRN, not debug-only + } } -void StaticPoolPacketManager::queueOutbound(Packet *packet, uint8_t priority, uint32_t scheduled_for) -{ - if (packet == nullptr) return; +void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { + if (packet == NULL) return; + if (send_queue.add(packet, priority, scheduled_for)) return; - if (_send_queue.add(packet, priority, scheduled_for)) return; - - /* Queue full — evict the least-important entry to make room. - * Only evict if the new packet is higher priority (lower number). */ - int worst = _send_queue.findLowestPriority(); - if (worst >= 0 && _send_queue._pri_table[worst] > priority) { - uint8_t evicted_pri = _send_queue._pri_table[worst]; - Packet *evicted = _send_queue.removeByIdx(worst); - LOG_WRN("queueOutbound: FULL — evicted type=%d pri=%d for type=%d pri=%d", - evicted->getPayloadType(), evicted_pri, - packet->getPayloadType(), priority); - free(evicted); - _send_queue.add(packet, priority, scheduled_for); - } else { - LOG_WRN("queueOutbound: FULL (%d entries) — dropping type=%d pri=%d", - _send_queue.count(), packet->getPayloadType(), priority); - free(packet); - } + // ZEPHCORE: queue full — evict the least important entry if the new packet is + // more important (lower number); otherwise drop the new one (as upstream). + int worst = send_queue.findLowestPriority(); + if (worst >= 0 && send_queue._pri_table[worst] > priority) { + uint8_t evicted_pri = send_queue._pri_table[worst]; + mesh::Packet* evicted = send_queue.removeByIdx(worst); + LOG_WRN("queueOutbound: FULL — evicted type=%d pri=%d for type=%d pri=%d", + evicted->getPayloadType(), evicted_pri, packet->getPayloadType(), priority); + free(evicted); + send_queue.add(packet, priority, scheduled_for); + } else { + LOG_WRN("queueOutbound: FULL (%d entries) — dropping type=%d pri=%d", + send_queue.count(), packet->getPayloadType(), priority); + free(packet); + } } -Packet *StaticPoolPacketManager::getNextOutbound(uint32_t now) -{ - return _send_queue.get(now); +mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) { + return send_queue.get(now); } -int StaticPoolPacketManager::getOutboundCount(uint32_t now) const -{ - return _send_queue.countBefore(now); +int StaticPoolPacketManager::getOutboundCount(uint32_t now) const { + return send_queue.countBefore(now); } -int StaticPoolPacketManager::getOutboundTotal() const -{ - return _send_queue.count(); +int StaticPoolPacketManager::getOutboundTotal() const { + return send_queue.count(); } -int StaticPoolPacketManager::getFreeCount() const -{ - return _unused.count(); +int StaticPoolPacketManager::getFreeCount() const { + return unused.count(); } -Packet *StaticPoolPacketManager::getOutboundByIdx(int i) -{ - return _send_queue.itemAt(i); +mesh::Packet* StaticPoolPacketManager::getOutboundByIdx(int i) { + return send_queue.itemAt(i); +} +mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) { + return send_queue.removeByIdx(i); } -Packet *StaticPoolPacketManager::removeOutboundByIdx(int i) -{ - return _send_queue.removeByIdx(i); +// ZEPHCORE: schedule access instead of the inbound queue. +uint32_t StaticPoolPacketManager::getOutboundSchedule(int i) const { + return send_queue.scheduleAt(i); } - -uint32_t StaticPoolPacketManager::getOutboundSchedule(int i) const -{ - return _send_queue.scheduleAt(i); +bool StaticPoolPacketManager::rescheduleOutbound(int i, uint32_t new_scheduled_for) { + return send_queue.reschedule(i, new_scheduled_for); } - -bool StaticPoolPacketManager::rescheduleOutbound(int i, uint32_t new_scheduled_for) -{ - return _send_queue.reschedule(i, new_scheduled_for); +uint8_t StaticPoolPacketManager::peekNextOutboundPriority(uint32_t now) const { + return send_queue.peekPriority(now); } - -uint8_t StaticPoolPacketManager::peekNextOutboundPriority(uint32_t now) const -{ - return _send_queue.peekPriority(now); -} - -} /* namespace mesh */ diff --git a/zephcore/src/Utils.cpp b/zephcore/src/Utils.cpp index 33d8c4e..6140f56 100644 --- a/zephcore/src/Utils.cpp +++ b/zephcore/src/Utils.cpp @@ -14,6 +14,16 @@ LOG_MODULE_REGISTER(zephcore_utils, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); namespace mesh { +uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) +{ + uint32_t num; + random((uint8_t *)&num, sizeof(num)); + if (_max <= _min) { /* ZEPHCORE: no modulo by zero */ + return _min; + } + return (num % (_max - _min)) + _min; +} + static uint8_t hexVal(char c) { if (c >= 'A' && c <= 'F') return c - 'A' + 10; @@ -258,6 +268,16 @@ bool Utils::fromHex(uint8_t *dest, int dest_size, const char *src_hex) return true; } +bool Utils::isZeroes(const uint8_t *buf, size_t len) +{ + while (len > 0) { + if (*buf != 0) return false; + buf++; + len--; + } + return true; +} + bool Utils::isHexChar(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 1236532..95562fd 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -63,7 +63,7 @@ LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); #endif /* Radio + mesh includes (shared header selects LR1110 or SX126x) */ -#include +#include #ifdef ZEPHCORE_LORA #include #include @@ -759,8 +759,8 @@ static uint16_t get_battery_mv(void) static mesh::ZephyrMillisecondClock ms_clock; static mesh::ZephyrRNG zephyr_rng; -static mesh::SimpleMeshTables mesh_tables; -static mesh::StaticPoolPacketManager packet_mgr; +static SimpleMeshTables mesh_tables; +static StaticPoolPacketManager packet_mgr; static CompanionMesh companion_mesh(lora_radio, ms_clock, zephyr_rng, rtc_clock, packet_mgr, mesh_tables, data_store); diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index 3202d7e..e8d5f26 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -76,7 +76,7 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK)) /* Radio + mesh includes (shared header selects LR1110 or SX126x) */ -#include +#include #if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA) #include "wifi_ota.h" @@ -463,7 +463,7 @@ static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board); static mesh::ZephyrMillisecondClock ms_clock; static mesh::ZephyrRNG zephyr_rng; -static mesh::SimpleMeshTables mesh_tables; +static SimpleMeshTables mesh_tables; /* RepeaterMesh requires: board, radio, ms_clock, rng, rtc, tables */ static RepeaterMesh repeater_mesh(zephyr_board, lora_radio, ms_clock, zephyr_rng, rtc_clock, mesh_tables); diff --git a/zephcore/src/main_room_server.cpp b/zephcore/src/main_room_server.cpp index dd2483f..d7c760e 100644 --- a/zephcore/src/main_room_server.cpp +++ b/zephcore/src/main_room_server.cpp @@ -60,7 +60,7 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) #include "ui_task.h" /* Radio + mesh includes (shared header selects LR1110 or SX126x) */ -#include +#include #if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA) #include "wifi_ota.h" @@ -394,7 +394,7 @@ static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board); static mesh::ZephyrMillisecondClock ms_clock; static mesh::ZephyrRNG zephyr_rng; -static mesh::SimpleMeshTables mesh_tables; +static SimpleMeshTables mesh_tables; /* RoomServerMesh requires: board, radio, ms_clock, rng, rtc, tables */ static RoomServerMesh room_mesh(zephyr_board, lora_radio, ms_clock, zephyr_rng, rtc_clock, mesh_tables);