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:
liquidraver committed 2026-09-23 13:39:48 +02:00
1 parent 2e2d852f29
commit a7d073900e
44 files changed
+2551 -2488

No files matched your search

+11
View File
@@ -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
+3 -8
View File
@@ -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
-1
View File
@@ -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
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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>
-20
View File
@@ -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 */
+1 -1
View File
@@ -5,7 +5,7 @@
#pragma once
#include <mesh/RNG.h>
#include <mesh/Utils.h>
#include <mesh/Identity.h>
#include <stddef.h>
+8 -1
View File
@@ -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
+1
View File
@@ -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 */
+9 -2
View File
@@ -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) */
+2 -1
View File
@@ -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;
+7 -4
View File
@@ -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 {
+7 -4
View File
@@ -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 {
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+72
View File
@@ -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
-49
View File
@@ -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 */
-17
View File
@@ -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
View File
@@ -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 */
}
+100 -82
View File
@@ -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
View File
@@ -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 */
}
+125 -22
View File
@@ -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
View File
@@ -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 */
}
-24
View File
@@ -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 */
-32
View File
@@ -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 */
-111
View File
@@ -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 */
+49 -54
View File
@@ -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 */
+17 -21
View File
@@ -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;
};
+86 -38
View File
@@ -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);
};
}
File diff suppressed because it is too large. Load diff
+737 -700
View File
File diff suppressed because it is too large. Load diff
+85 -102
View File
@@ -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>
+147 -171
View File
@@ -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 */
+20
View File
@@ -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');
+3 -3
View File
@@ -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);
+2 -2
View File
@@ -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);
+2 -2
View File
@@ -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);