mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-22 23:11:22 +00:00
core: restore upstream shape for the shared mesh core
- Mesh, Dispatcher, Packet, packet pool and the core headers rebuilt from Arduino MeshCore's text; ZephCore changes re-applied, each fenced `// ZEPHCORE:` (diff vs upstream: 4540 -> 1147 lines) - upstream header layout; MainBoard gains handleCommand/loop and the other upstream members; RNG::nextInt back in Utils.cpp - packet logging moved to the roles' log hooks (helpers/PacketLog.h) - zero-hop direct ACKs deduplicated and length-checked as upstream - RadioIncludes.h -> src/, LoRaConfig.h -> helpers/ - .editorconfig: shared files keep upstream formatting Verified: 10 role builds, per-function disassembly review, bench DM/ACK regression (T1000-E, Wio L1, XIAO S3 on 1.17.4) incl. via live repeater.
This commit is contained in:
1 parent
2e2d852f29
commit
a7d073900e
44 files changed
+2551
-2488
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Board.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Clock.h>
|
||||
#include <mesh/Dispatcher.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/RTC.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Identity.h>
|
||||
#include <mesh/RTC.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <NodePrefs.h>
|
||||
#include <ContactInfo.h>
|
||||
#include <ChannelDetails.h>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "LoRaRadioBase.h"
|
||||
#include "radio_common.h"
|
||||
#include "pm_sleep_guard.h"
|
||||
#include <mesh/LoRaConfig.h>
|
||||
#include <helpers/LoRaConfig.h>
|
||||
#include <mesh/MeshCore.h> /* MAX_TRANS_UNIT */
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/random/random.h>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Radio.h>
|
||||
#include <mesh/Board.h>
|
||||
#include <mesh/Dispatcher.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <NodePrefs.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* RNG::nextInt - shared implementation
|
||||
*/
|
||||
|
||||
#include <mesh/RNG.h>
|
||||
|
||||
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 */
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/RNG.h>
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/Identity.h>
|
||||
#include <stddef.h>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#define ZEPHCORE_HAS_UI_TASK 1
|
||||
#endif
|
||||
#include <joystick_ui_hooks.h>
|
||||
#include <helpers/PacketLog.h>
|
||||
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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "observer_creds.h"
|
||||
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/LoRaConfig.h>
|
||||
#include <helpers/LoRaConfig.h>
|
||||
#include <adapters/radio/LoRaRadioBase.h>
|
||||
#include <adapters/rng/ZephyrRNG.h> /* generateFirstBootIdentity (hardened keygen) */
|
||||
#include <helpers/MeshcoreJson.h>
|
||||
@@ -22,6 +22,7 @@ LOG_MODULE_REGISTER(zephcore_observer, CONFIG_ZEPHCORE_OBSERVER_LOG_LEVEL);
|
||||
#include <time.h>
|
||||
|
||||
#include <ZephyrMQTTPublisher.h>
|
||||
#include <helpers/PacketLog.h>
|
||||
|
||||
/* 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) */
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include <mesh/Dispatcher.h>
|
||||
#include <mesh/StaticPoolPacketManager.h>
|
||||
#include <mesh/Identity.h>
|
||||
#include <mesh/RTC.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <helpers/MeshTimeSync.h>
|
||||
#include <helpers/NodePrefs.h>
|
||||
#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;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <ZephyrWiFiStation.h>
|
||||
#include <ZephyrMQTTPublisher.h>
|
||||
#endif
|
||||
#include <helpers/PacketLog.h>
|
||||
|
||||
/* 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 {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <helpers/PacketLog.h>
|
||||
|
||||
/* 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 {
|
||||
|
||||
@@ -35,7 +35,7 @@ LOG_MODULE_REGISTER(zephcore_observer_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
|
||||
#include "../adapters/datastore/ZephyrFsFormat.h"
|
||||
#include <app/ObserverMesh.h>
|
||||
#include <adapters/clock/ZephyrRTCClock.h>
|
||||
#include <mesh/RadioIncludes.h>
|
||||
#include <src/RadioIncludes.h>
|
||||
#include <ZephyrWiFiStation.h>
|
||||
#include <ZephyrMQTTPublisher.h>
|
||||
#include "observer_creds.h"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <mesh/Mesh.h>
|
||||
#include <mesh/Board.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include "NodePrefs.h"
|
||||
|
||||
File renamed without changes.
@@ -20,7 +20,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <mesh/RTC.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
|
||||
#ifdef CONFIG_ZEPHCORE_TIMESYNC_TABLE_SIZE
|
||||
#define MESHTIMESYNC_TABLE_SIZE CONFIG_ZEPHCORE_TIMESYNC_TABLE_SIZE
|
||||
|
||||
@@ -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 <zephyr/kernel.h>
|
||||
#include <mesh/Packet.h>
|
||||
#include <mesh/Utils.h>
|
||||
|
||||
#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
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore MainBoard interface
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <math.h>
|
||||
|
||||
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 */
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore clock interfaces
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
class MillisecondClock {
|
||||
public:
|
||||
virtual unsigned long getMillis() = 0;
|
||||
};
|
||||
|
||||
} /* namespace mesh */
|
||||
+266
-141
@@ -1,179 +1,304 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore Dispatcher - packet queue and radio scheduling
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <mesh/Identity.h>
|
||||
#include <mesh/Packet.h>
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/Radio.h>
|
||||
#include <mesh/Clock.h>
|
||||
#include <mesh/Maintenance.h>
|
||||
#include <mesh/Maintenance.h> // ZEPHCORE: deadline-driven maintenance
|
||||
#include <string.h>
|
||||
|
||||
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 */
|
||||
}
|
||||
@@ -1,106 +1,124 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore Identity - Ed25519 key pairs
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <mesh/RNG.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
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 */
|
||||
}
|
||||
|
||||
+225
-87
@@ -1,112 +1,250 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore Mesh - routing protocol layer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Dispatcher.h>
|
||||
#include <mesh/ContentionTracker.h>
|
||||
#include <mesh/RTC.h>
|
||||
#include <mesh/ContentionTracker.h> // 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 */
|
||||
}
|
||||
@@ -1,32 +1,135 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore constants and base types
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <math.h>
|
||||
|
||||
/* 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 <mesh/stm32_cmsis_fixup.h>
|
||||
|
||||
#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 <Arduino.h>
|
||||
#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 <zephyr/logging/log.h>
|
||||
#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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
+105
-72
@@ -1,92 +1,125 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore Packet - fundamental transmission unit
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
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 */
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore RNG interface - matches Utils.h
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* 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 <mesh/stm32_cmsis_fixup.h>
|
||||
|
||||
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 */
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore RTC interface - matches MeshCore.h
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
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 */
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore Radio interface - matches Dispatcher.h
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <mesh/Maintenance.h>
|
||||
|
||||
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 */
|
||||
@@ -1,69 +1,64 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* SimpleMeshTables - hash-based packet deduplication
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Mesh.h>
|
||||
#include <string.h>
|
||||
|
||||
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 */
|
||||
@@ -1,28 +1,24 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Static pool PacketManager
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Dispatcher.h>
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -1,8 +1,3 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore Utils - crypto and helpers (Zephyr port)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/MeshCore.h>
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
+450
-705
File diff suppressed because it is too large.
Load diff
+737
-700
File diff suppressed because it is too large.
Load diff
+85
-102
@@ -1,124 +1,107 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore Packet implementation
|
||||
*/
|
||||
|
||||
#include <mesh/Packet.h>
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/Utils.h> // ZEPHCORE: SHA-256 via Utils (no Arduino SHA256 class)
|
||||
#include <string.h>
|
||||
|
||||
// ZEPHCORE: log module backing MESH_DEBUG_PRINTLN (see MeshCore.h).
|
||||
#include <zephyr/logging/log.h>
|
||||
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 */
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) || DT_NODE_HAS_STATUS(DT_ALIAS(lora0), okay)
|
||||
|
||||
#include <zephyr/drivers/lora.h>
|
||||
#include <mesh/LoRaConfig.h>
|
||||
#include <helpers/LoRaConfig.h>
|
||||
#include <adapters/board/ZephyrBoard.h>
|
||||
#include <adapters/clock/ZephyrMillisecondClock.h>
|
||||
#include <adapters/rng/ZephyrRNG.h>
|
||||
@@ -1,215 +1,191 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Static pool PacketManager - no dynamic allocation
|
||||
*/
|
||||
|
||||
#include <mesh/StaticPoolPacketManager.h>
|
||||
#include <mesh/Packet.h>
|
||||
#include <string.h>
|
||||
|
||||
// ZEPHCORE: log module for the pool's warnings.
|
||||
#include <zephyr/logging/log.h>
|
||||
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 */
|
||||
@@ -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');
|
||||
|
||||
@@ -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 <mesh/RadioIncludes.h>
|
||||
#include <src/RadioIncludes.h>
|
||||
#ifdef ZEPHCORE_LORA
|
||||
#include <app/CompanionMesh.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 <mesh/RadioIncludes.h>
|
||||
#include <src/RadioIncludes.h>
|
||||
|
||||
#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);
|
||||
|
||||
@@ -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 <mesh/RadioIncludes.h>
|
||||
#include <src/RadioIncludes.h>
|
||||
|
||||
#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);
|
||||
|
||||
Reference in new issue
Block a user