normalize source-file line endings to LF

Add .gitattributes rules so .c/.h/.cpp/.hpp are always stored as LF
(prevents EOL drift from editors with autocrlf-true defaults), and
renormalize the 30 source files that had drifted to CRLF in the index.

Pure mechanical change — `git diff --ignore-cr-at-eol` is empty.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
liquidraverandClaude Opus 4.7 committed 2026-05-05 14:56:44 +02:00
1 parent 28e12c7eef
commit 5e7adfb130
31 files changed
+6788 -6783

No files matched your search

+80 -80
View File
@@ -1,80 +1,80 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Adaptive Contention Window — EMA-based flood retransmit delay
*
* Counts neighbor retransmit dupes within a 10s window per packet.
* Dupe counts feed a rolling EMA that drives an adaptive delay factor.
*/
#pragma once
#include <stdint.h>
namespace mesh {
class Packet;
class ContentionTracker {
public:
ContentionTracker();
/* FNV-1a 32-bit hash for ring buffer correlation (not dedup SHA256). */
static uint32_t computePacketHash32(const Packet *pkt);
void trackRetransmit(uint32_t hash32, uint32_t now_ms);
/* Returns true if packet matched a tracked retransmit (dupe recorded). */
bool recordDupeIfTracked(uint32_t hash32, uint32_t now_ms);
/* Returns backoff_multiplier * airtime, clamped by remaining headroom.
* Returns 0 when hard cap reached or backoff disabled. */
uint16_t getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const;
void addReactiveExtension(uint32_t hash32, uint16_t added_ms);
/* Finalize expired entries into EMA. */
void tick(uint32_t now_ms);
float getContentionEstimate() const;
/* Saturating curve: MIN + (MAX-MIN) * est/(est+HALFPOINT).
* Returns 0.5 during warmup. */
float getFloodDelayFactor() const;
bool isWarmedUp() const { return _finalized_count >= WARMUP_PACKETS; }
void setBackoffMultiplier(float m) { _backoff_multiplier = m; }
float getBackoffMultiplier() const { return _backoff_multiplier; }
private:
static constexpr int RING_SIZE = 24; /* max concurrent tracked retransmits */
static constexpr uint32_t WINDOW_MS = 10000; /* dupe observation window; covers SF12 2-hop */
static constexpr int EMA_SHIFT = 3; /* alpha = 1/8 */
static constexpr int WARMUP_PACKETS = 4; /* min samples before EMA is trusted */
static constexpr float MIN_FLOOD_FACTOR = 0.40f; /* sparse mesh baseline */
static constexpr float MAX_FLOOD_FACTOR = 1.00f; /* dense mesh ceiling */
static constexpr float FLOOD_EST_HALFPOINT = 4.0f; /* midpoint: factor=0.60 at est=4 */
static constexpr float DEFAULT_BACKOFF_MULT = 0.2f; /* airtime*0.2 per dupe heard */
static constexpr uint32_t REACTIVE_HARD_CAP_MS = 2000; /* max cumulative reactive extension */
static constexpr uint32_t STALE_MS = 300000; /* 5 min: reset EMA if no traffic */
struct Entry {
uint32_t hash32;
uint32_t first_seen_ms;
uint8_t dupe_count;
uint16_t reactive_added_ms;
bool active;
};
Entry _ring[RING_SIZE];
int _next_idx;
uint32_t _ema_x256;
int _finalized_count;
uint32_t _last_retransmit_ms;
float _backoff_multiplier;
void finalizeEntry(int idx);
int findEntry(uint32_t hash32) const;
};
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* Adaptive Contention Window — EMA-based flood retransmit delay
*
* Counts neighbor retransmit dupes within a 10s window per packet.
* Dupe counts feed a rolling EMA that drives an adaptive delay factor.
*/
#pragma once
#include <stdint.h>
namespace mesh {
class Packet;
class ContentionTracker {
public:
ContentionTracker();
/* FNV-1a 32-bit hash for ring buffer correlation (not dedup SHA256). */
static uint32_t computePacketHash32(const Packet *pkt);
void trackRetransmit(uint32_t hash32, uint32_t now_ms);
/* Returns true if packet matched a tracked retransmit (dupe recorded). */
bool recordDupeIfTracked(uint32_t hash32, uint32_t now_ms);
/* Returns backoff_multiplier * airtime, clamped by remaining headroom.
* Returns 0 when hard cap reached or backoff disabled. */
uint16_t getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const;
void addReactiveExtension(uint32_t hash32, uint16_t added_ms);
/* Finalize expired entries into EMA. */
void tick(uint32_t now_ms);
float getContentionEstimate() const;
/* Saturating curve: MIN + (MAX-MIN) * est/(est+HALFPOINT).
* Returns 0.5 during warmup. */
float getFloodDelayFactor() const;
bool isWarmedUp() const { return _finalized_count >= WARMUP_PACKETS; }
void setBackoffMultiplier(float m) { _backoff_multiplier = m; }
float getBackoffMultiplier() const { return _backoff_multiplier; }
private:
static constexpr int RING_SIZE = 24; /* max concurrent tracked retransmits */
static constexpr uint32_t WINDOW_MS = 10000; /* dupe observation window; covers SF12 2-hop */
static constexpr int EMA_SHIFT = 3; /* alpha = 1/8 */
static constexpr int WARMUP_PACKETS = 4; /* min samples before EMA is trusted */
static constexpr float MIN_FLOOD_FACTOR = 0.40f; /* sparse mesh baseline */
static constexpr float MAX_FLOOD_FACTOR = 1.00f; /* dense mesh ceiling */
static constexpr float FLOOD_EST_HALFPOINT = 4.0f; /* midpoint: factor=0.60 at est=4 */
static constexpr float DEFAULT_BACKOFF_MULT = 0.2f; /* airtime*0.2 per dupe heard */
static constexpr uint32_t REACTIVE_HARD_CAP_MS = 2000; /* max cumulative reactive extension */
static constexpr uint32_t STALE_MS = 300000; /* 5 min: reset EMA if no traffic */
struct Entry {
uint32_t hash32;
uint32_t first_seen_ms;
uint8_t dupe_count;
uint16_t reactive_added_ms;
bool active;
};
Entry _ring[RING_SIZE];
int _next_idx;
uint32_t _ema_x256;
int _finalized_count;
uint32_t _last_retransmit_ms;
float _backoff_multiplier;
void finalizeEntry(int idx);
int findEntry(uint32_t hash32) const;
};
} /* namespace mesh */
+127 -127
View File
@@ -1,127 +1,127 @@
/*
* SPDX-License-Identifier: Apache-2.0
* 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 <string.h>
namespace mesh {
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;
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 void queueInbound(Packet *packet, uint32_t scheduled_for) = 0;
virtual Packet *getNextInbound(uint32_t now) = 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);
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))
#define ERR_EVENT_FULL (1 << 0)
#define ERR_EVENT_CAD_TIMEOUT (1 << 1)
#define ERR_EVENT_STARTRX_TIMEOUT (1 << 2)
class Dispatcher {
Packet *outbound;
uint32_t outbound_expiry, outbound_start, total_air_time, rx_air_time;
uint32_t next_tx_time;
uint32_t cad_busy_start;
uint32_t tx_budget_ms;
uint32_t last_budget_update;
uint32_t duty_cycle_window_ms;
uint32_t radio_nonrx_start;
uint32_t next_agc_reset_time;
bool prev_isrecv_mode;
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;
void processRecvPacket(Packet *pkt);
protected:
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 int calcRxDelay(float score, uint32_t air_time) const;
virtual uint32_t getCADFailRetryDelay() const;
virtual uint32_t getCADFailMaxDuration() const;
virtual int getInterferenceThreshold() const { return 0; }
virtual int getAGCResetInterval() const { return 0; }
virtual uint32_t getDutyCycleWindowMs() const { return 3600000UL; } /* 1h default */
public:
void begin();
void loop();
void maintenanceLoop();
Packet *obtainNewPacket();
void releasePacket(Packet *packet);
void sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis = 0);
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;
}
bool millisHasNowPassed(uint32_t timestamp) const;
uint32_t futureMillis(int millis_from_now) const;
private:
void updateTxBudget();
uint32_t getMaxTxBudgetMs() const;
bool tryParsePacket(Packet *pkt, const uint8_t *raw, int len);
void checkRecv();
void checkSend();
};
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* 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 <string.h>
namespace mesh {
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;
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 void queueInbound(Packet *packet, uint32_t scheduled_for) = 0;
virtual Packet *getNextInbound(uint32_t now) = 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);
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))
#define ERR_EVENT_FULL (1 << 0)
#define ERR_EVENT_CAD_TIMEOUT (1 << 1)
#define ERR_EVENT_STARTRX_TIMEOUT (1 << 2)
class Dispatcher {
Packet *outbound;
uint32_t outbound_expiry, outbound_start, total_air_time, rx_air_time;
uint32_t next_tx_time;
uint32_t cad_busy_start;
uint32_t tx_budget_ms;
uint32_t last_budget_update;
uint32_t duty_cycle_window_ms;
uint32_t radio_nonrx_start;
uint32_t next_agc_reset_time;
bool prev_isrecv_mode;
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;
void processRecvPacket(Packet *pkt);
protected:
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 int calcRxDelay(float score, uint32_t air_time) const;
virtual uint32_t getCADFailRetryDelay() const;
virtual uint32_t getCADFailMaxDuration() const;
virtual int getInterferenceThreshold() const { return 0; }
virtual int getAGCResetInterval() const { return 0; }
virtual uint32_t getDutyCycleWindowMs() const { return 3600000UL; } /* 1h default */
public:
void begin();
void loop();
void maintenanceLoop();
Packet *obtainNewPacket();
void releasePacket(Packet *packet);
void sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis = 0);
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;
}
bool millisHasNowPassed(uint32_t timestamp) const;
uint32_t futureMillis(int millis_from_now) const;
private:
void updateTxBudget();
uint32_t getMaxTxBudgetMs() const;
bool tryParsePacket(Packet *pkt, const uint8_t *raw, int len);
void checkRecv();
void checkSend();
};
} /* namespace mesh */
+107 -107
View File
@@ -1,107 +1,107 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Mesh - routing protocol layer
*/
#pragma once
#include <mesh/Dispatcher.h>
#include <mesh/ContentionTracker.h>
#ifdef CONFIG_ZEPHCORE_APC
#include <mesh/PowerController.h>
#endif
#include <mesh/RTC.h>
namespace mesh {
struct GroupChannel {
uint8_t hash[PATH_HASH_SIZE];
uint8_t secret[PUB_KEY_SIZE];
};
class MeshTables {
public:
virtual bool hasSeen(const Packet *packet) = 0;
virtual void clear(const Packet *packet) = 0;
};
class Mesh : public Dispatcher {
RNG *_rng;
RTCClock *_rtc;
MeshTables *_tables;
void removeSelfFromPath(Packet *packet);
void routeDirectRecvAcks(Packet *packet, uint32_t delay_millis);
DispatcherAction forwardMultipartDirect(Packet *pkt);
protected:
ContentionTracker _contention;
ContentionTracker& getContentionTracker() { return _contention; }
const ContentionTracker& getContentionTracker() const { return _contention; }
#ifdef CONFIG_ZEPHCORE_APC
PowerController _power_ctrl;
PowerController& getPowerController() { return _power_ctrl; }
const PowerController& getPowerController() const { return _power_ctrl; }
#endif
void extendPendingRetransmit(uint32_t hash32);
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; }
/* 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; }
public:
Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables);
void begin();
void loop();
void maintenanceLoop();
LocalIdentity self_id;
RNG *getRNG() const { return _rng; }
RTCClock *getRTCClock() const { return _rtc; }
MeshTables *getTables() const { return _tables; }
Packet *createAdvert(const LocalIdentity &id, const uint8_t *app_data = nullptr, size_t app_data_len = 0);
Packet *createAck(uint32_t ack_crc);
Packet *createMultiAck(uint32_t ack_crc, uint8_t 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);
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 */
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Mesh - routing protocol layer
*/
#pragma once
#include <mesh/Dispatcher.h>
#include <mesh/ContentionTracker.h>
#ifdef CONFIG_ZEPHCORE_APC
#include <mesh/PowerController.h>
#endif
#include <mesh/RTC.h>
namespace mesh {
struct GroupChannel {
uint8_t hash[PATH_HASH_SIZE];
uint8_t secret[PUB_KEY_SIZE];
};
class MeshTables {
public:
virtual bool hasSeen(const Packet *packet) = 0;
virtual void clear(const Packet *packet) = 0;
};
class Mesh : public Dispatcher {
RNG *_rng;
RTCClock *_rtc;
MeshTables *_tables;
void removeSelfFromPath(Packet *packet);
void routeDirectRecvAcks(Packet *packet, uint32_t delay_millis);
DispatcherAction forwardMultipartDirect(Packet *pkt);
protected:
ContentionTracker _contention;
ContentionTracker& getContentionTracker() { return _contention; }
const ContentionTracker& getContentionTracker() const { return _contention; }
#ifdef CONFIG_ZEPHCORE_APC
PowerController _power_ctrl;
PowerController& getPowerController() { return _power_ctrl; }
const PowerController& getPowerController() const { return _power_ctrl; }
#endif
void extendPendingRetransmit(uint32_t hash32);
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; }
/* 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; }
public:
Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables);
void begin();
void loop();
void maintenanceLoop();
LocalIdentity self_id;
RNG *getRNG() const { return _rng; }
RTCClock *getRTCClock() const { return _rtc; }
MeshTables *getTables() const { return _tables; }
Packet *createAdvert(const LocalIdentity &id, const uint8_t *app_data = nullptr, size_t app_data_len = 0);
Packet *createAck(uint32_t ack_crc);
Packet *createMultiAck(uint32_t ack_crc, uint8_t 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);
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 */
+113 -113
View File
@@ -1,113 +1,113 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Adaptive Power Control (APC) — echo-based TX power reduction
*
* Measures link margin by tracking echo packets (flood dupes of
* packets we sent or retransmitted, heard back from neighbors).
* Feeds per-packet margins into a rolling EMA to produce an
* adaptive TX power reduction in dBm.
*
* Rogue filtering: when 2+ distinct neighbors echo the same packet,
* clusters their SNRs within 6 dB of the best. An isolated high
* outlier (rogue, badly placed neighbor) is dropped.
*/
#pragma once
#include <stdint.h>
namespace mesh {
class Packet;
class PowerController {
public:
PowerController();
/* Enable/disable APC. When disabled, APC tracking/math is bypassed. */
void setEnabled(bool en);
bool isEnabled() const { return _enabled; }
/* Set current spreading factor (needed for margin calculation). */
void setSF(uint8_t sf) { _sf = sf; }
/* Set target link margin in dB. Higher = more conservative
* (better for networks with poor-RX hardware). Default 16 dB. */
void setTargetMargin(uint8_t margin_db) { _target_margin_x4 = (int)margin_db * 4; }
uint8_t getTargetMargin() const { return (uint8_t)(_target_margin_x4 / 4); }
/* Called when we send or retransmit a flood packet. */
void trackTransmit(uint32_t hash32, uint32_t now_ms);
/* Called for every received flood dupe. Updates per-source best
* SNR and source diversity. Returns true if the dupe matched a
* tracked transmit. */
bool recordEcho(uint32_t hash32, int8_t snr_x4,
uint8_t first_hop_hash, uint32_t now_ms);
/* Finalize expired entries, update EMA, adjust power, handle
* staleness. Call from maintenanceLoop (~5 s). */
void tick(uint32_t now_ms);
/* Current TX power reduction in dBm (0 to MAX_REDUCTION_DB).
* Returns 0 when disabled. */
int8_t getPowerReduction() const { return _enabled ? _power_reduction_db : 0; }
/* Current margin estimate in dB (for diagnostics). */
float getMarginEstimate() const;
/* Source count from most recently finalized entry (diagnostics). */
uint8_t getLastSourceCount() const { return _last_source_count; }
bool isWarmedUp() const { return _finalized_count >= WARMUP_COUNT; }
bool isStale(uint32_t now_ms) const;
private:
static constexpr int RING_SIZE = 16;
static constexpr uint32_t ECHO_WINDOW_MS = 10000; /* 10s: covers SF12 2-hop echo */
static constexpr uint32_t STALE_MS = 120000; /* 2 min */
static constexpr int EMA_SHIFT = 2; /* alpha = 1/4 */
static constexpr int WARMUP_COUNT = 3;
static constexpr int MAX_SOURCES = 3;
static constexpr int8_t STEP_DOWN_DB = 3;
static constexpr int8_t STEP_UP_DB = 6;
static constexpr int8_t MAX_REDUCTION_DB = 12;
static constexpr int8_t MIN_TX_POWER_DBM = -9; /* SX1262 hw min */
static constexpr int CLUSTER_WIDTH_X4 = 24; /* 6 dB in x4 */
static constexpr int DEFAULT_TARGET_MARGIN_X4 = 64; /* 16 dB * 4 */
static constexpr int HYSTERESIS_X4 = 4; /* 1 dB * 4 */
struct Source {
uint8_t hash;
int8_t snr_x4;
};
struct EchoEntry {
uint32_t hash32;
uint32_t timestamp_ms;
uint8_t source_count;
uint8_t sf_at_track; /* SF when packet was transmitted */
Source sources[MAX_SOURCES];
bool active;
};
EchoEntry _ring[RING_SIZE];
int _next_idx;
int32_t _margin_ema_x256; /* fixed-point EMA (x4 * 256) */
int _finalized_count;
uint32_t _last_echo_ms;
int8_t _power_reduction_db;
bool _enabled;
uint8_t _sf;
uint8_t _last_source_count;
int _target_margin_x4;
void finalizeEntry(int idx);
int findEntry(uint32_t hash32) const;
int8_t computeRobustSNR(const EchoEntry &entry) const;
/* SNR threshold for a given SF (x4 fixed point). */
static int8_t sfThresholdX4(uint8_t sf);
};
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* Adaptive Power Control (APC) — echo-based TX power reduction
*
* Measures link margin by tracking echo packets (flood dupes of
* packets we sent or retransmitted, heard back from neighbors).
* Feeds per-packet margins into a rolling EMA to produce an
* adaptive TX power reduction in dBm.
*
* Rogue filtering: when 2+ distinct neighbors echo the same packet,
* clusters their SNRs within 6 dB of the best. An isolated high
* outlier (rogue, badly placed neighbor) is dropped.
*/
#pragma once
#include <stdint.h>
namespace mesh {
class Packet;
class PowerController {
public:
PowerController();
/* Enable/disable APC. When disabled, APC tracking/math is bypassed. */
void setEnabled(bool en);
bool isEnabled() const { return _enabled; }
/* Set current spreading factor (needed for margin calculation). */
void setSF(uint8_t sf) { _sf = sf; }
/* Set target link margin in dB. Higher = more conservative
* (better for networks with poor-RX hardware). Default 16 dB. */
void setTargetMargin(uint8_t margin_db) { _target_margin_x4 = (int)margin_db * 4; }
uint8_t getTargetMargin() const { return (uint8_t)(_target_margin_x4 / 4); }
/* Called when we send or retransmit a flood packet. */
void trackTransmit(uint32_t hash32, uint32_t now_ms);
/* Called for every received flood dupe. Updates per-source best
* SNR and source diversity. Returns true if the dupe matched a
* tracked transmit. */
bool recordEcho(uint32_t hash32, int8_t snr_x4,
uint8_t first_hop_hash, uint32_t now_ms);
/* Finalize expired entries, update EMA, adjust power, handle
* staleness. Call from maintenanceLoop (~5 s). */
void tick(uint32_t now_ms);
/* Current TX power reduction in dBm (0 to MAX_REDUCTION_DB).
* Returns 0 when disabled. */
int8_t getPowerReduction() const { return _enabled ? _power_reduction_db : 0; }
/* Current margin estimate in dB (for diagnostics). */
float getMarginEstimate() const;
/* Source count from most recently finalized entry (diagnostics). */
uint8_t getLastSourceCount() const { return _last_source_count; }
bool isWarmedUp() const { return _finalized_count >= WARMUP_COUNT; }
bool isStale(uint32_t now_ms) const;
private:
static constexpr int RING_SIZE = 16;
static constexpr uint32_t ECHO_WINDOW_MS = 10000; /* 10s: covers SF12 2-hop echo */
static constexpr uint32_t STALE_MS = 120000; /* 2 min */
static constexpr int EMA_SHIFT = 2; /* alpha = 1/4 */
static constexpr int WARMUP_COUNT = 3;
static constexpr int MAX_SOURCES = 3;
static constexpr int8_t STEP_DOWN_DB = 3;
static constexpr int8_t STEP_UP_DB = 6;
static constexpr int8_t MAX_REDUCTION_DB = 12;
static constexpr int8_t MIN_TX_POWER_DBM = -9; /* SX1262 hw min */
static constexpr int CLUSTER_WIDTH_X4 = 24; /* 6 dB in x4 */
static constexpr int DEFAULT_TARGET_MARGIN_X4 = 64; /* 16 dB * 4 */
static constexpr int HYSTERESIS_X4 = 4; /* 1 dB * 4 */
struct Source {
uint8_t hash;
int8_t snr_x4;
};
struct EchoEntry {
uint32_t hash32;
uint32_t timestamp_ms;
uint8_t source_count;
uint8_t sf_at_track; /* SF when packet was transmitted */
Source sources[MAX_SOURCES];
bool active;
};
EchoEntry _ring[RING_SIZE];
int _next_idx;
int32_t _margin_ema_x256; /* fixed-point EMA (x4 * 256) */
int _finalized_count;
uint32_t _last_echo_ms;
int8_t _power_reduction_db;
bool _enabled;
uint8_t _sf;
uint8_t _last_source_count;
int _target_margin_x4;
void finalizeEntry(int idx);
int findEntry(uint32_t hash32) const;
int8_t computeRobustSNR(const EchoEntry &entry) const;
/* SNR threshold for a given SF (x4 fixed point). */
static int8_t sfThresholdX4(uint8_t sf);
};
} /* namespace mesh */
+43 -43
View File
@@ -1,43 +1,43 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio interface - matches Dispatcher.h
*/
#pragma once
#include <stdint.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;
virtual bool isSendComplete() = 0;
virtual void onSendFinished() = 0;
virtual int getNoiseFloor() const { return 0; }
virtual void triggerNoiseFloorCalibrate(int threshold) { (void)threshold; }
virtual void resetAGC() {}
virtual bool isInRecvMode() const = 0;
virtual bool isReceiving() { return false; }
virtual bool isRadioReady() { return true; }
virtual float getLastRSSI() const { return 0; }
virtual float getLastSNR() const { return 0; }
/* Adaptive Power Control */
virtual void setTxPowerReduction(int8_t reduction_db) { (void)reduction_db; }
virtual int8_t getTxPowerReduction() const { 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 */
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio interface - matches Dispatcher.h
*/
#pragma once
#include <stdint.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;
virtual bool isSendComplete() = 0;
virtual void onSendFinished() = 0;
virtual int getNoiseFloor() const { return 0; }
virtual void triggerNoiseFloorCalibrate(int threshold) { (void)threshold; }
virtual void resetAGC() {}
virtual bool isInRecvMode() const = 0;
virtual bool isReceiving() { return false; }
virtual bool isRadioReady() { return true; }
virtual float getLastRSSI() const { return 0; }
virtual float getLastSNR() const { return 0; }
/* Adaptive Power Control */
virtual void setTxPowerReduction(int8_t reduction_db) { (void)reduction_db; }
virtual int8_t getTxPowerReduction() const { 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 */