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:
liquidraver
2026-05-05 14:56:44 +02:00
co-authored by Claude Opus 4.7
parent 28e12c7eef
commit 5e7adfb130
31 changed files with 6788 additions and 6783 deletions
+5
View File
@@ -1,2 +1,7 @@
*.c text eol=lf
*.h text eol=lf
*.cpp text eol=lf
*.hpp text eol=lf
patches/zephyr/*.patch text eol=lf
patches/modules/loramac-node/*.patch text eol=crlf
+90 -90
View File
@@ -1,90 +1,90 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Zephyr DataStore - LittleFS-backed persistence with optional QSPI flash
*
* All platforms use DTS-automounted /lfs for identity, prefs, contacts.
* QSPI /ext overrides contacts path when available (any platform).
*/
#pragma once
#include <mesh/Identity.h>
#include <mesh/RTC.h>
#include <NodePrefs.h>
#include <ContactInfo.h>
#include <ChannelDetails.h>
class DataStoreHost {
public:
virtual bool onContactLoaded(const ContactInfo &contact) = 0;
virtual bool getContactForSave(uint32_t idx, ContactInfo &contact) = 0;
virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails &ch) = 0;
virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails &ch) = 0;
};
class ZephyrDataStore {
public:
explicit ZephyrDataStore(mesh::RTCClock &clock);
void begin();
bool formatFileSystem();
bool loadMainIdentity(mesh::LocalIdentity &identity);
bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs &prefs);
void savePrefs(const NodePrefs &prefs);
void loadContacts(DataStoreHost *host);
void saveContacts(DataStoreHost *host);
void loadChannels(DataStoreHost *host);
void saveChannels(DataStoreHost *host);
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);
bool deleteBlobByKey(const uint8_t key[], int key_len);
/* Used/total KiB for BLE storage report: /ext when mounted, else /lfs (Arduino parity). */
uint32_t getStorageUsedKb() const;
uint32_t getStorageTotalKb() const;
/* Factory reset - delete all stored data */
void factoryReset();
/* Check if external QSPI flash is available */
bool hasExternalStorage() const { return _has_ext_fs; }
uint32_t getExternalStorageKb() const;
static bool mount();
static void unmount();
static const char *mountPoint() { return MNT_POINT; }
static const char *extMountPoint() { return EXT_MNT_POINT; }
private:
/* Internal flash (always available) - identity, prefs */
static constexpr const char *MNT_POINT = "/lfs";
static constexpr const char *PREFS_FILE = "/lfs/new_prefs";
static constexpr const char *MAIN_ID_FILE = "/lfs/_main.id";
/* External QSPI flash (optional) - contacts, channels, blobs */
static constexpr const char *EXT_MNT_POINT = "/ext";
static constexpr const char *EXT_CONTACTS_FILE = "/ext/contacts3";
static constexpr const char *EXT_CHANNELS_FILE = "/ext/channels2";
static constexpr const char *EXT_ADV_BLOBS_FILE = "/ext/adv_blobs";
/* Fallback to internal if no external */
static constexpr const char *INT_CONTACTS_FILE = "/lfs/contacts3";
static constexpr const char *INT_CHANNELS_FILE = "/lfs/channels2";
static constexpr const char *INT_ADV_BLOBS_FILE = "/lfs/adv_blobs";
mesh::RTCClock *_clock;
bool _has_ext_fs;
/* Get path based on external availability */
const char *contactsFile() const { return _has_ext_fs ? EXT_CONTACTS_FILE : INT_CONTACTS_FILE; }
const char *channelsFile() const { return _has_ext_fs ? EXT_CHANNELS_FILE : INT_CHANNELS_FILE; }
const char *advBlobsFile() const { return _has_ext_fs ? EXT_ADV_BLOBS_FILE : INT_ADV_BLOBS_FILE; }
int maxBlobRecs() const { return _has_ext_fs ? 100 : 20; }
void checkAdvBlobFile();
void migrateToExternalFS();
bool openRead(const char *path, uint8_t *buf, size_t buf_sz, size_t &out_len);
bool atomicReplaceFile(const char *path, const uint8_t *buf, size_t len);
bool exists(const char *path);
bool removeFile(const char *path);
bool copyFile(const char *src, const char *dst);
};
/*
* SPDX-License-Identifier: Apache-2.0
* Zephyr DataStore - LittleFS-backed persistence with optional QSPI flash
*
* All platforms use DTS-automounted /lfs for identity, prefs, contacts.
* QSPI /ext overrides contacts path when available (any platform).
*/
#pragma once
#include <mesh/Identity.h>
#include <mesh/RTC.h>
#include <NodePrefs.h>
#include <ContactInfo.h>
#include <ChannelDetails.h>
class DataStoreHost {
public:
virtual bool onContactLoaded(const ContactInfo &contact) = 0;
virtual bool getContactForSave(uint32_t idx, ContactInfo &contact) = 0;
virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails &ch) = 0;
virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails &ch) = 0;
};
class ZephyrDataStore {
public:
explicit ZephyrDataStore(mesh::RTCClock &clock);
void begin();
bool formatFileSystem();
bool loadMainIdentity(mesh::LocalIdentity &identity);
bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs &prefs);
void savePrefs(const NodePrefs &prefs);
void loadContacts(DataStoreHost *host);
void saveContacts(DataStoreHost *host);
void loadChannels(DataStoreHost *host);
void saveChannels(DataStoreHost *host);
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);
bool deleteBlobByKey(const uint8_t key[], int key_len);
/* Used/total KiB for BLE storage report: /ext when mounted, else /lfs (Arduino parity). */
uint32_t getStorageUsedKb() const;
uint32_t getStorageTotalKb() const;
/* Factory reset - delete all stored data */
void factoryReset();
/* Check if external QSPI flash is available */
bool hasExternalStorage() const { return _has_ext_fs; }
uint32_t getExternalStorageKb() const;
static bool mount();
static void unmount();
static const char *mountPoint() { return MNT_POINT; }
static const char *extMountPoint() { return EXT_MNT_POINT; }
private:
/* Internal flash (always available) - identity, prefs */
static constexpr const char *MNT_POINT = "/lfs";
static constexpr const char *PREFS_FILE = "/lfs/new_prefs";
static constexpr const char *MAIN_ID_FILE = "/lfs/_main.id";
/* External QSPI flash (optional) - contacts, channels, blobs */
static constexpr const char *EXT_MNT_POINT = "/ext";
static constexpr const char *EXT_CONTACTS_FILE = "/ext/contacts3";
static constexpr const char *EXT_CHANNELS_FILE = "/ext/channels2";
static constexpr const char *EXT_ADV_BLOBS_FILE = "/ext/adv_blobs";
/* Fallback to internal if no external */
static constexpr const char *INT_CONTACTS_FILE = "/lfs/contacts3";
static constexpr const char *INT_CHANNELS_FILE = "/lfs/channels2";
static constexpr const char *INT_ADV_BLOBS_FILE = "/lfs/adv_blobs";
mesh::RTCClock *_clock;
bool _has_ext_fs;
/* Get path based on external availability */
const char *contactsFile() const { return _has_ext_fs ? EXT_CONTACTS_FILE : INT_CONTACTS_FILE; }
const char *channelsFile() const { return _has_ext_fs ? EXT_CHANNELS_FILE : INT_CHANNELS_FILE; }
const char *advBlobsFile() const { return _has_ext_fs ? EXT_ADV_BLOBS_FILE : INT_ADV_BLOBS_FILE; }
int maxBlobRecs() const { return _has_ext_fs ? 100 : 20; }
void checkAdvBlobFile();
void migrateToExternalFS();
bool openRead(const char *path, uint8_t *buf, size_t buf_sz, size_t &out_len);
bool atomicReplaceFile(const char *path, const uint8_t *buf, size_t len);
bool exists(const char *path);
bool removeFile(const char *path);
bool copyFile(const char *src, const char *dst);
};
+101 -101
View File
@@ -1,101 +1,101 @@
/*
* Auto-generated from ota_page.html — do not edit manually.
* Regenerate: python3 compress_html.py
*/
#pragma once
#include <stdint.h>
#define OTA_PAGE_GZ_SIZE 1408
static const uint8_t ota_page_gz[OTA_PAGE_GZ_SIZE] = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x95, 0x57, 0x6d, 0x6f, 0xdb, 0x36,
0x10, 0xfe, 0xee, 0x5f, 0xa1, 0xa9, 0x08, 0x64, 0x37, 0xb1, 0x2c, 0x3b, 0x2f, 0x75, 0xa4, 0xc8,
0xc0, 0xda, 0xb5, 0xd8, 0xb0, 0x75, 0x2d, 0xda, 0x14, 0xd8, 0x56, 0xf4, 0x03, 0x25, 0x9e, 0x2c,
0x36, 0x32, 0xa9, 0x91, 0x54, 0x62, 0x4f, 0x30, 0xb0, 0x1f, 0xb1, 0x5f, 0xb8, 0x5f, 0xb2, 0x23,
0x25, 0x25, 0xb2, 0x1b, 0x0f, 0xed, 0x97, 0x48, 0x26, 0x8f, 0xcf, 0xdd, 0x3d, 0xf7, 0xdc, 0x51,
0xb9, 0xfa, 0xee, 0x87, 0x37, 0x2f, 0xae, 0x7f, 0x7f, 0xfb, 0xd2, 0xc9, 0xf5, 0xaa, 0x58, 0x0c,
0xae, 0xba, 0x07, 0x10, 0x8a, 0x8f, 0x15, 0x68, 0xe2, 0xa4, 0x39, 0x91, 0x0a, 0x74, 0xec, 0x56,
0x3a, 0x1b, 0xcf, 0xdd, 0x6e, 0x99, 0x93, 0x15, 0xc4, 0xee, 0x2d, 0x83, 0xbb, 0x52, 0x48, 0xed,
0x3a, 0xa9, 0xe0, 0x1a, 0x38, 0x9a, 0xdd, 0x31, 0xaa, 0xf3, 0x98, 0xc2, 0x2d, 0x4b, 0x61, 0x6c,
0x7f, 0x9c, 0x30, 0xce, 0x34, 0x23, 0xc5, 0x58, 0xa5, 0xa4, 0x80, 0x78, 0x6a, 0x30, 0x34, 0xd3,
0x05, 0x2c, 0xfe, 0x80, 0x32, 0x7f, 0x21, 0x24, 0x38, 0x6f, 0xae, 0xbf, 0xbf, 0x9a, 0x34, 0x6b,
0x83, 0x2b, 0xa5, 0x37, 0xe6, 0xf9, 0xb4, 0x4e, 0xc4, 0x7a, 0xac, 0xd8, 0x5f, 0x8c, 0x2f, 0xc3,
0x44, 0x48, 0x0a, 0x72, 0x8c, 0x2b, 0xd1, 0x8a, 0xc8, 0x25, 0xe3, 0x61, 0x10, 0x95, 0x84, 0x52,
0xb3, 0x17, 0x6c, 0x07, 0x89, 0xa0, 0x9b, 0x3a, 0xc3, 0x10, 0xc6, 0x19, 0x59, 0xb1, 0x62, 0x13,
0xaa, 0x8d, 0xd2, 0xb0, 0x1a, 0x57, 0xec, 0x44, 0x11, 0xae, 0xc6, 0x0a, 0x24, 0xcb, 0xa2, 0x84,
0xa4, 0x37, 0x4b, 0x29, 0x2a, 0x4e, 0xc3, 0x27, 0x53, 0x32, 0x25, 0x33, 0x88, 0x52, 0x51, 0x08,
0x19, 0x3e, 0x01, 0x80, 0x88, 0x32, 0x55, 0x16, 0x64, 0x13, 0x66, 0x05, 0xac, 0xa3, 0xcf, 0x95,
0xd2, 0x2c, 0xdb, 0x8c, 0xdb, 0xb4, 0xc2, 0x14, 0xff, 0x80, 0x8c, 0x48, 0xc1, 0x96, 0x7c, 0xcc,
0x10, 0x5a, 0x75, 0x4b, 0x2b, 0xc6, 0xc7, 0x39, 0xb0, 0x65, 0xae, 0xc3, 0x69, 0x10, 0xdc, 0xe6,
0xdb, 0x81, 0x9f, 0xd6, 0x3b, 0x9e, 0x2e, 0x66, 0xd3, 0x53, 0x88, 0xda, 0x0c, 0x24, 0xa1, 0xac,
0x52, 0xe1, 0x74, 0x56, 0xae, 0xef, 0x13, 0x98, 0xc1, 0x0a, 0xb3, 0x5a, 0x37, 0x6c, 0x85, 0x67,
0xb3, 0x00, 0xf7, 0x9a, 0xf7, 0xcb, 0xe0, 0x28, 0xd2, 0xb0, 0xd6, 0x63, 0xeb, 0xb8, 0x75, 0xb9,
0x1d, 0xe4, 0xb3, 0xba, 0x61, 0x01, 0x09, 0xd1, 0x5a, 0xac, 0x42, 0xff, 0x1c, 0x21, 0xda, 0x5c,
0x82, 0x2c, 0xc3, 0x18, 0x18, 0xad, 0xdb, 0xdf, 0xf3, 0xf9, 0x3c, 0xb2, 0xd4, 0x20, 0x95, 0x10,
0xfa, 0xf3, 0x73, 0xeb, 0xad, 0x7f, 0x7a, 0x6a, 0x8e, 0x6f, 0x07, 0x8c, 0x97, 0x95, 0xfe, 0xa8,
0x37, 0x25, 0xc4, 0x19, 0x2b, 0xe0, 0x53, 0xdd, 0x31, 0xc2, 0x05, 0x07, 0x84, 0x4c, 0x34, 0xbf,
0x5f, 0x62, 0xbc, 0x60, 0x1c, 0xc6, 0x49, 0x21, 0xd2, 0x9b, 0xfb, 0x3c, 0xfc, 0x67, 0xb0, 0x72,
0x2c, 0xd8, 0x5e, 0xb6, 0x73, 0x4c, 0xa8, 0x59, 0xb1, 0x58, 0x51, 0x5a, 0x49, 0x85, 0xa1, 0x95,
0x82, 0x59, 0x0a, 0x1f, 0xa2, 0x9b, 0xe2, 0x51, 0xfb, 0xeb, 0xae, 0x61, 0xf4, 0x22, 0x08, 0x22,
0x2d, 0xb1, 0x82, 0x28, 0x20, 0xc1, 0xc3, 0x07, 0x5a, 0x1d, 0x7f, 0xa6, 0x30, 0x24, 0x05, 0xc5,
0x0e, 0xd7, 0x41, 0x76, 0x7a, 0x76, 0x11, 0xf4, 0xaa, 0xda, 0xd8, 0x84, 0xb9, 0xb8, 0x05, 0xb9,
0x5b, 0x15, 0x72, 0x3e, 0x7b, 0x76, 0x81, 0xdb, 0x55, 0xb9, 0x07, 0x11, 0x24, 0xf3, 0xcb, 0xb3,
0x0e, 0x22, 0xcb, 0xb2, 0x8e, 0xac, 0x02, 0x32, 0x1d, 0x36, 0x4c, 0x99, 0x43, 0x8f, 0x60, 0x06,
0x01, 0x39, 0x9d, 0x4f, 0xdb, 0x6d, 0x64, 0x8a, 0x24, 0x05, 0xd0, 0x1d, 0x8b, 0xf3, 0xf3, 0xf3,
0x2e, 0x79, 0x0a, 0x19, 0xa9, 0x0a, 0x6d, 0x78, 0x25, 0xb2, 0x6e, 0xca, 0x8d, 0x02, 0x3a, 0x8a,
0x5a, 0x31, 0xcd, 0xce, 0x0c, 0x69, 0x7d, 0x74, 0x12, 0xa0, 0x66, 0x1f, 0xd3, 0x51, 0xdb, 0x11,
0x53, 0x1f, 0x85, 0xe4, 0x04, 0x91, 0x89, 0x2b, 0x2b, 0xc4, 0x5d, 0x98, 0x33, 0x4a, 0x81, 0x47,
0x7b, 0x65, 0xc4, 0xda, 0x16, 0xf5, 0x83, 0x62, 0x8f, 0xfa, 0x4e, 0x4c, 0x51, 0x89, 0x1c, 0x2f,
0x0d, 0x3a, 0x6a, 0x6d, 0x78, 0x19, 0x50, 0x58, 0x9e, 0x18, 0x49, 0x9d, 0xb4, 0xc4, 0x8c, 0x1e,
0x0b, 0xa0, 0x57, 0x22, 0x9b, 0x88, 0xe3, 0x9f, 0xaa, 0x56, 0xc1, 0xd8, 0x9c, 0x7e, 0x99, 0xea,
0xfa, 0x80, 0x02, 0xb5, 0x28, 0x43, 0xff, 0xd4, 0x72, 0xaa, 0x74, 0xdd, 0x5b, 0xbc, 0x57, 0x42,
0x73, 0xe6, 0xd2, 0x1c, 0xe9, 0x75, 0x9a, 0x7f, 0x66, 0xcf, 0x88, 0x9b, 0x4e, 0xe5, 0x4d, 0x74,
0xb8, 0x84, 0x35, 0xe9, 0xca, 0xff, 0xec, 0x2c, 0x3d, 0x4d, 0xb7, 0x83, 0xab, 0x49, 0x3b, 0x51,
0xae, 0x26, 0xed, 0x60, 0x33, 0xe3, 0x02, 0x1f, 0x94, 0xdd, 0x3a, 0x69, 0x41, 0x94, 0x8a, 0xdd,
0xd4, 0x0c, 0xa5, 0x7c, 0xb6, 0x33, 0x91, 0x9c, 0x0f, 0x25, 0x25, 0x1a, 0xf0, 0xd4, 0x6c, 0xd7,
0x98, 0x51, 0xd7, 0x61, 0x34, 0x76, 0xcd, 0x90, 0xa3, 0xee, 0xe2, 0x6a, 0x82, 0x7b, 0x8d, 0x85,
0xc1, 0xae, 0xb0, 0xa7, 0x78, 0x67, 0x8a, 0x3d, 0xe3, 0xa0, 0x00, 0x5d, 0x47, 0xf0, 0xb4, 0x60,
0xe9, 0x0d, 0x1e, 0x12, 0x69, 0xb5, 0x42, 0x6e, 0xfd, 0x25, 0xe8, 0x97, 0x05, 0x98, 0xd7, 0xe7,
0x9b, 0x9f, 0xe8, 0xd0, 0xcb, 0xbc, 0x91, 0x6f, 0x6d, 0x86, 0x23, 0x77, 0xf1, 0x1e, 0x0a, 0x48,
0xb5, 0xf3, 0x8a, 0xc9, 0xd5, 0x1d, 0x91, 0x18, 0x43, 0x03, 0x8b, 0xf8, 0xb6, 0x4b, 0x1d, 0xdb,
0xa5, 0xae, 0x69, 0xd3, 0x26, 0x94, 0xcc, 0x75, 0x48, 0x9a, 0x42, 0x89, 0x03, 0xd8, 0x4f, 0x18,
0xb7, 0xfe, 0x72, 0xc2, 0x97, 0xc6, 0x48, 0x0d, 0x75, 0xce, 0xd4, 0xc8, 0x7d, 0x34, 0x38, 0x94,
0x6a, 0x83, 0x50, 0x25, 0xae, 0xd3, 0x69, 0xf6, 0x21, 0xda, 0xaa, 0x34, 0xd1, 0x7c, 0x28, 0x0b,
0x41, 0x68, 0x2f, 0x88, 0x87, 0x8c, 0x1b, 0xef, 0xe8, 0xd0, 0x72, 0x1c, 0xbb, 0xfd, 0xba, 0xce,
0x77, 0x6b, 0x38, 0xef, 0x0d, 0x29, 0x1c, 0x4a, 0x3b, 0xc4, 0xdd, 0x87, 0x44, 0x64, 0x13, 0x8e,
0x79, 0x59, 0xf4, 0x77, 0x8c, 0x6c, 0xdb, 0x5c, 0xcd, 0x5b, 0x7b, 0xf8, 0x11, 0x08, 0x54, 0x5b,
0x63, 0x67, 0x5e, 0x1e, 0x33, 0x50, 0xed, 0xbe, 0xea, 0x6d, 0xb7, 0x0f, 0x95, 0x4a, 0x56, 0xea,
0xc5, 0xe0, 0x96, 0x48, 0xc7, 0x90, 0x1b, 0x0d, 0xb2, 0x8a, 0xa7, 0x46, 0xd8, 0x0e, 0xd2, 0xc8,
0x46, 0xb5, 0x59, 0x8c, 0x99, 0xe9, 0x21, 0x50, 0x1f, 0x83, 0x4f, 0x11, 0xcb, 0x86, 0xe6, 0x7d,
0x54, 0x1f, 0x2e, 0x2b, 0xc7, 0xba, 0x9a, 0x19, 0xfe, 0xa2, 0xbd, 0x21, 0x8d, 0xbd, 0x6f, 0xae,
0xcf, 0x63, 0xcf, 0x19, 0x7a, 0xc7, 0xaf, 0x89, 0xce, 0x7d, 0xdb, 0x7d, 0x16, 0xc9, 0x37, 0x5c,
0x4d, 0xa6, 0xc1, 0xec, 0x6c, 0x74, 0xec, 0xfd, 0xfc, 0x7c, 0xe4, 0x45, 0x07, 0x91, 0xab, 0x04,
0x91, 0xbb, 0x92, 0xc5, 0x19, 0x29, 0x14, 0x1c, 0x36, 0x56, 0x7a, 0x2f, 0x0c, 0xcf, 0x8b, 0xb6,
0xdb, 0x87, 0xf4, 0x4c, 0xa1, 0xeb, 0x01, 0xa6, 0xf3, 0x9d, 0xcd, 0x47, 0x82, 0xae, 0x24, 0x8f,
0x2c, 0x11, 0xeb, 0x98, 0xc3, 0x9d, 0xf3, 0xdb, 0xeb, 0x5f, 0x7e, 0xd4, 0xba, 0x7c, 0x07, 0x7f,
0x56, 0xa0, 0xf4, 0x70, 0xd4, 0xec, 0x25, 0xf1, 0x41, 0x8f, 0x58, 0x41, 0xaf, 0xb5, 0xca, 0x0e,
0x5b, 0x99, 0x62, 0x76, 0x66, 0xe5, 0x61, 0x33, 0xac, 0x65, 0x67, 0xa5, 0xe2, 0xff, 0x4d, 0x32,
0x1a, 0x24, 0xbe, 0xd5, 0xa2, 0xdf, 0xce, 0xbd, 0xd8, 0xb3, 0xb7, 0x94, 0x17, 0x0d, 0xbe, 0x92,
0x49, 0x2d, 0x2b, 0x2c, 0xbc, 0xf2, 0xad, 0x5c, 0x7e, 0x35, 0xdf, 0x39, 0x06, 0x38, 0x52, 0xbb,
0xf4, 0x35, 0x3d, 0x81, 0xd7, 0x9e, 0xef, 0xfb, 0x88, 0xbd, 0x36, 0x43, 0x1f, 0x17, 0x7c, 0xc1,
0x4b, 0x29, 0x96, 0x12, 0x50, 0x69, 0x1d, 0xb9, 0x43, 0xd4, 0x07, 0x32, 0x0b, 0x7e, 0x01, 0x7c,
0xa9, 0x71, 0xbe, 0xac, 0xb0, 0x85, 0x8d, 0xaf, 0x51, 0x6d, 0xd3, 0x4e, 0xe3, 0x9e, 0x08, 0xd0,
0x0a, 0x61, 0x80, 0x4e, 0xc0, 0xd7, 0x42, 0x93, 0xe2, 0x29, 0xce, 0xe8, 0x51, 0x94, 0xb5, 0x39,
0x35, 0x5f, 0x57, 0x65, 0x7a, 0xec, 0x1d, 0x79, 0x51, 0xb9, 0x13, 0x91, 0x5d, 0xdc, 0x53, 0xd4,
0x3d, 0x58, 0x2b, 0xa8, 0xc9, 0xde, 0xae, 0xf5, 0xb0, 0xa3, 0xb6, 0xed, 0xd6, 0xe4, 0x22, 0xb8,
0x39, 0xf7, 0x90, 0x80, 0x8d, 0x7f, 0x8d, 0x31, 0x10, 0x5d, 0xa9, 0x38, 0x9e, 0x61, 0x48, 0xf5,
0x1e, 0x41, 0x8e, 0xb8, 0xf9, 0x82, 0xa3, 0x57, 0xf8, 0xe5, 0xa7, 0x72, 0xa0, 0x48, 0x91, 0xf3,
0x0e, 0x12, 0x21, 0x74, 0xc7, 0xd7, 0x6e, 0x3e, 0x9e, 0xb9, 0x87, 0xd0, 0x37, 0xa0, 0x84, 0xbf,
0xc0, 0x05, 0xf9, 0x05, 0xee, 0x4b, 0x29, 0x71, 0x88, 0x38, 0xde, 0xf1, 0xda, 0x47, 0xa2, 0x4b,
0xc1, 0x15, 0x5c, 0xe3, 0xfe, 0xb7, 0xb5, 0x4a, 0x97, 0x29, 0x18, 0xb0, 0x7e, 0xaa, 0x5f, 0x11,
0x40, 0x53, 0x7c, 0x27, 0x23, 0xcc, 0x4c, 0xcb, 0x7f, 0xff, 0xfe, 0xc7, 0x7c, 0xff, 0x72, 0x68,
0x5a, 0xc9, 0x02, 0x7e, 0x63, 0xdf, 0x36, 0xb1, 0x94, 0xc0, 0x87, 0xde, 0xdb, 0x37, 0xef, 0xaf,
0xbd, 0x13, 0x6f, 0xd2, 0xe8, 0xc9, 0x08, 0x1a, 0x89, 0x87, 0x76, 0x3c, 0xe0, 0x2f, 0x6c, 0x5a,
0xd0, 0x69, 0x3e, 0xf4, 0x26, 0x0c, 0x6f, 0x77, 0xfc, 0x68, 0xde, 0x98, 0xce, 0xce, 0xf1, 0xa8,
0x8c, 0x17, 0xd2, 0xff, 0xac, 0x4c, 0x12, 0xed, 0x0a, 0x8d, 0x17, 0x87, 0x27, 0x93, 0xbd, 0xbe,
0xf6, 0xa6, 0x02, 0xed, 0x4d, 0x26, 0xea, 0x27, 0x82, 0x48, 0x7a, 0xec, 0x19, 0x59, 0xe0, 0xdd,
0x44, 0x8c, 0xd7, 0xe1, 0x08, 0x21, 0xb7, 0x18, 0x06, 0xde, 0xaa, 0xed, 0xb0, 0xc4, 0x7b, 0xa1,
0xb9, 0x4f, 0x27, 0xcd, 0xbf, 0x0f, 0xff, 0x01, 0x5f, 0x9f, 0x85, 0x5a, 0x56, 0x0c, 0x00, 0x00,
};
/*
* Auto-generated from ota_page.html — do not edit manually.
* Regenerate: python3 compress_html.py
*/
#pragma once
#include <stdint.h>
#define OTA_PAGE_GZ_SIZE 1408
static const uint8_t ota_page_gz[OTA_PAGE_GZ_SIZE] = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x95, 0x57, 0x6d, 0x6f, 0xdb, 0x36,
0x10, 0xfe, 0xee, 0x5f, 0xa1, 0xa9, 0x08, 0x64, 0x37, 0xb1, 0x2c, 0x3b, 0x2f, 0x75, 0xa4, 0xc8,
0xc0, 0xda, 0xb5, 0xd8, 0xb0, 0x75, 0x2d, 0xda, 0x14, 0xd8, 0x56, 0xf4, 0x03, 0x25, 0x9e, 0x2c,
0x36, 0x32, 0xa9, 0x91, 0x54, 0x62, 0x4f, 0x30, 0xb0, 0x1f, 0xb1, 0x5f, 0xb8, 0x5f, 0xb2, 0x23,
0x25, 0x25, 0xb2, 0x1b, 0x0f, 0xed, 0x97, 0x48, 0x26, 0x8f, 0xcf, 0xdd, 0x3d, 0xf7, 0xdc, 0x51,
0xb9, 0xfa, 0xee, 0x87, 0x37, 0x2f, 0xae, 0x7f, 0x7f, 0xfb, 0xd2, 0xc9, 0xf5, 0xaa, 0x58, 0x0c,
0xae, 0xba, 0x07, 0x10, 0x8a, 0x8f, 0x15, 0x68, 0xe2, 0xa4, 0x39, 0x91, 0x0a, 0x74, 0xec, 0x56,
0x3a, 0x1b, 0xcf, 0xdd, 0x6e, 0x99, 0x93, 0x15, 0xc4, 0xee, 0x2d, 0x83, 0xbb, 0x52, 0x48, 0xed,
0x3a, 0xa9, 0xe0, 0x1a, 0x38, 0x9a, 0xdd, 0x31, 0xaa, 0xf3, 0x98, 0xc2, 0x2d, 0x4b, 0x61, 0x6c,
0x7f, 0x9c, 0x30, 0xce, 0x34, 0x23, 0xc5, 0x58, 0xa5, 0xa4, 0x80, 0x78, 0x6a, 0x30, 0x34, 0xd3,
0x05, 0x2c, 0xfe, 0x80, 0x32, 0x7f, 0x21, 0x24, 0x38, 0x6f, 0xae, 0xbf, 0xbf, 0x9a, 0x34, 0x6b,
0x83, 0x2b, 0xa5, 0x37, 0xe6, 0xf9, 0xb4, 0x4e, 0xc4, 0x7a, 0xac, 0xd8, 0x5f, 0x8c, 0x2f, 0xc3,
0x44, 0x48, 0x0a, 0x72, 0x8c, 0x2b, 0xd1, 0x8a, 0xc8, 0x25, 0xe3, 0x61, 0x10, 0x95, 0x84, 0x52,
0xb3, 0x17, 0x6c, 0x07, 0x89, 0xa0, 0x9b, 0x3a, 0xc3, 0x10, 0xc6, 0x19, 0x59, 0xb1, 0x62, 0x13,
0xaa, 0x8d, 0xd2, 0xb0, 0x1a, 0x57, 0xec, 0x44, 0x11, 0xae, 0xc6, 0x0a, 0x24, 0xcb, 0xa2, 0x84,
0xa4, 0x37, 0x4b, 0x29, 0x2a, 0x4e, 0xc3, 0x27, 0x53, 0x32, 0x25, 0x33, 0x88, 0x52, 0x51, 0x08,
0x19, 0x3e, 0x01, 0x80, 0x88, 0x32, 0x55, 0x16, 0x64, 0x13, 0x66, 0x05, 0xac, 0xa3, 0xcf, 0x95,
0xd2, 0x2c, 0xdb, 0x8c, 0xdb, 0xb4, 0xc2, 0x14, 0xff, 0x80, 0x8c, 0x48, 0xc1, 0x96, 0x7c, 0xcc,
0x10, 0x5a, 0x75, 0x4b, 0x2b, 0xc6, 0xc7, 0x39, 0xb0, 0x65, 0xae, 0xc3, 0x69, 0x10, 0xdc, 0xe6,
0xdb, 0x81, 0x9f, 0xd6, 0x3b, 0x9e, 0x2e, 0x66, 0xd3, 0x53, 0x88, 0xda, 0x0c, 0x24, 0xa1, 0xac,
0x52, 0xe1, 0x74, 0x56, 0xae, 0xef, 0x13, 0x98, 0xc1, 0x0a, 0xb3, 0x5a, 0x37, 0x6c, 0x85, 0x67,
0xb3, 0x00, 0xf7, 0x9a, 0xf7, 0xcb, 0xe0, 0x28, 0xd2, 0xb0, 0xd6, 0x63, 0xeb, 0xb8, 0x75, 0xb9,
0x1d, 0xe4, 0xb3, 0xba, 0x61, 0x01, 0x09, 0xd1, 0x5a, 0xac, 0x42, 0xff, 0x1c, 0x21, 0xda, 0x5c,
0x82, 0x2c, 0xc3, 0x18, 0x18, 0xad, 0xdb, 0xdf, 0xf3, 0xf9, 0x3c, 0xb2, 0xd4, 0x20, 0x95, 0x10,
0xfa, 0xf3, 0x73, 0xeb, 0xad, 0x7f, 0x7a, 0x6a, 0x8e, 0x6f, 0x07, 0x8c, 0x97, 0x95, 0xfe, 0xa8,
0x37, 0x25, 0xc4, 0x19, 0x2b, 0xe0, 0x53, 0xdd, 0x31, 0xc2, 0x05, 0x07, 0x84, 0x4c, 0x34, 0xbf,
0x5f, 0x62, 0xbc, 0x60, 0x1c, 0xc6, 0x49, 0x21, 0xd2, 0x9b, 0xfb, 0x3c, 0xfc, 0x67, 0xb0, 0x72,
0x2c, 0xd8, 0x5e, 0xb6, 0x73, 0x4c, 0xa8, 0x59, 0xb1, 0x58, 0x51, 0x5a, 0x49, 0x85, 0xa1, 0x95,
0x82, 0x59, 0x0a, 0x1f, 0xa2, 0x9b, 0xe2, 0x51, 0xfb, 0xeb, 0xae, 0x61, 0xf4, 0x22, 0x08, 0x22,
0x2d, 0xb1, 0x82, 0x28, 0x20, 0xc1, 0xc3, 0x07, 0x5a, 0x1d, 0x7f, 0xa6, 0x30, 0x24, 0x05, 0xc5,
0x0e, 0xd7, 0x41, 0x76, 0x7a, 0x76, 0x11, 0xf4, 0xaa, 0xda, 0xd8, 0x84, 0xb9, 0xb8, 0x05, 0xb9,
0x5b, 0x15, 0x72, 0x3e, 0x7b, 0x76, 0x81, 0xdb, 0x55, 0xb9, 0x07, 0x11, 0x24, 0xf3, 0xcb, 0xb3,
0x0e, 0x22, 0xcb, 0xb2, 0x8e, 0xac, 0x02, 0x32, 0x1d, 0x36, 0x4c, 0x99, 0x43, 0x8f, 0x60, 0x06,
0x01, 0x39, 0x9d, 0x4f, 0xdb, 0x6d, 0x64, 0x8a, 0x24, 0x05, 0xd0, 0x1d, 0x8b, 0xf3, 0xf3, 0xf3,
0x2e, 0x79, 0x0a, 0x19, 0xa9, 0x0a, 0x6d, 0x78, 0x25, 0xb2, 0x6e, 0xca, 0x8d, 0x02, 0x3a, 0x8a,
0x5a, 0x31, 0xcd, 0xce, 0x0c, 0x69, 0x7d, 0x74, 0x12, 0xa0, 0x66, 0x1f, 0xd3, 0x51, 0xdb, 0x11,
0x53, 0x1f, 0x85, 0xe4, 0x04, 0x91, 0x89, 0x2b, 0x2b, 0xc4, 0x5d, 0x98, 0x33, 0x4a, 0x81, 0x47,
0x7b, 0x65, 0xc4, 0xda, 0x16, 0xf5, 0x83, 0x62, 0x8f, 0xfa, 0x4e, 0x4c, 0x51, 0x89, 0x1c, 0x2f,
0x0d, 0x3a, 0x6a, 0x6d, 0x78, 0x19, 0x50, 0x58, 0x9e, 0x18, 0x49, 0x9d, 0xb4, 0xc4, 0x8c, 0x1e,
0x0b, 0xa0, 0x57, 0x22, 0x9b, 0x88, 0xe3, 0x9f, 0xaa, 0x56, 0xc1, 0xd8, 0x9c, 0x7e, 0x99, 0xea,
0xfa, 0x80, 0x02, 0xb5, 0x28, 0x43, 0xff, 0xd4, 0x72, 0xaa, 0x74, 0xdd, 0x5b, 0xbc, 0x57, 0x42,
0x73, 0xe6, 0xd2, 0x1c, 0xe9, 0x75, 0x9a, 0x7f, 0x66, 0xcf, 0x88, 0x9b, 0x4e, 0xe5, 0x4d, 0x74,
0xb8, 0x84, 0x35, 0xe9, 0xca, 0xff, 0xec, 0x2c, 0x3d, 0x4d, 0xb7, 0x83, 0xab, 0x49, 0x3b, 0x51,
0xae, 0x26, 0xed, 0x60, 0x33, 0xe3, 0x02, 0x1f, 0x94, 0xdd, 0x3a, 0x69, 0x41, 0x94, 0x8a, 0xdd,
0xd4, 0x0c, 0xa5, 0x7c, 0xb6, 0x33, 0x91, 0x9c, 0x0f, 0x25, 0x25, 0x1a, 0xf0, 0xd4, 0x6c, 0xd7,
0x98, 0x51, 0xd7, 0x61, 0x34, 0x76, 0xcd, 0x90, 0xa3, 0xee, 0xe2, 0x6a, 0x82, 0x7b, 0x8d, 0x85,
0xc1, 0xae, 0xb0, 0xa7, 0x78, 0x67, 0x8a, 0x3d, 0xe3, 0xa0, 0x00, 0x5d, 0x47, 0xf0, 0xb4, 0x60,
0xe9, 0x0d, 0x1e, 0x12, 0x69, 0xb5, 0x42, 0x6e, 0xfd, 0x25, 0xe8, 0x97, 0x05, 0x98, 0xd7, 0xe7,
0x9b, 0x9f, 0xe8, 0xd0, 0xcb, 0xbc, 0x91, 0x6f, 0x6d, 0x86, 0x23, 0x77, 0xf1, 0x1e, 0x0a, 0x48,
0xb5, 0xf3, 0x8a, 0xc9, 0xd5, 0x1d, 0x91, 0x18, 0x43, 0x03, 0x8b, 0xf8, 0xb6, 0x4b, 0x1d, 0xdb,
0xa5, 0xae, 0x69, 0xd3, 0x26, 0x94, 0xcc, 0x75, 0x48, 0x9a, 0x42, 0x89, 0x03, 0xd8, 0x4f, 0x18,
0xb7, 0xfe, 0x72, 0xc2, 0x97, 0xc6, 0x48, 0x0d, 0x75, 0xce, 0xd4, 0xc8, 0x7d, 0x34, 0x38, 0x94,
0x6a, 0x83, 0x50, 0x25, 0xae, 0xd3, 0x69, 0xf6, 0x21, 0xda, 0xaa, 0x34, 0xd1, 0x7c, 0x28, 0x0b,
0x41, 0x68, 0x2f, 0x88, 0x87, 0x8c, 0x1b, 0xef, 0xe8, 0xd0, 0x72, 0x1c, 0xbb, 0xfd, 0xba, 0xce,
0x77, 0x6b, 0x38, 0xef, 0x0d, 0x29, 0x1c, 0x4a, 0x3b, 0xc4, 0xdd, 0x87, 0x44, 0x64, 0x13, 0x8e,
0x79, 0x59, 0xf4, 0x77, 0x8c, 0x6c, 0xdb, 0x5c, 0xcd, 0x5b, 0x7b, 0xf8, 0x11, 0x08, 0x54, 0x5b,
0x63, 0x67, 0x5e, 0x1e, 0x33, 0x50, 0xed, 0xbe, 0xea, 0x6d, 0xb7, 0x0f, 0x95, 0x4a, 0x56, 0xea,
0xc5, 0xe0, 0x96, 0x48, 0xc7, 0x90, 0x1b, 0x0d, 0xb2, 0x8a, 0xa7, 0x46, 0xd8, 0x0e, 0xd2, 0xc8,
0x46, 0xb5, 0x59, 0x8c, 0x99, 0xe9, 0x21, 0x50, 0x1f, 0x83, 0x4f, 0x11, 0xcb, 0x86, 0xe6, 0x7d,
0x54, 0x1f, 0x2e, 0x2b, 0xc7, 0xba, 0x9a, 0x19, 0xfe, 0xa2, 0xbd, 0x21, 0x8d, 0xbd, 0x6f, 0xae,
0xcf, 0x63, 0xcf, 0x19, 0x7a, 0xc7, 0xaf, 0x89, 0xce, 0x7d, 0xdb, 0x7d, 0x16, 0xc9, 0x37, 0x5c,
0x4d, 0xa6, 0xc1, 0xec, 0x6c, 0x74, 0xec, 0xfd, 0xfc, 0x7c, 0xe4, 0x45, 0x07, 0x91, 0xab, 0x04,
0x91, 0xbb, 0x92, 0xc5, 0x19, 0x29, 0x14, 0x1c, 0x36, 0x56, 0x7a, 0x2f, 0x0c, 0xcf, 0x8b, 0xb6,
0xdb, 0x87, 0xf4, 0x4c, 0xa1, 0xeb, 0x01, 0xa6, 0xf3, 0x9d, 0xcd, 0x47, 0x82, 0xae, 0x24, 0x8f,
0x2c, 0x11, 0xeb, 0x98, 0xc3, 0x9d, 0xf3, 0xdb, 0xeb, 0x5f, 0x7e, 0xd4, 0xba, 0x7c, 0x07, 0x7f,
0x56, 0xa0, 0xf4, 0x70, 0xd4, 0xec, 0x25, 0xf1, 0x41, 0x8f, 0x58, 0x41, 0xaf, 0xb5, 0xca, 0x0e,
0x5b, 0x99, 0x62, 0x76, 0x66, 0xe5, 0x61, 0x33, 0xac, 0x65, 0x67, 0xa5, 0xe2, 0xff, 0x4d, 0x32,
0x1a, 0x24, 0xbe, 0xd5, 0xa2, 0xdf, 0xce, 0xbd, 0xd8, 0xb3, 0xb7, 0x94, 0x17, 0x0d, 0xbe, 0x92,
0x49, 0x2d, 0x2b, 0x2c, 0xbc, 0xf2, 0xad, 0x5c, 0x7e, 0x35, 0xdf, 0x39, 0x06, 0x38, 0x52, 0xbb,
0xf4, 0x35, 0x3d, 0x81, 0xd7, 0x9e, 0xef, 0xfb, 0x88, 0xbd, 0x36, 0x43, 0x1f, 0x17, 0x7c, 0xc1,
0x4b, 0x29, 0x96, 0x12, 0x50, 0x69, 0x1d, 0xb9, 0x43, 0xd4, 0x07, 0x32, 0x0b, 0x7e, 0x01, 0x7c,
0xa9, 0x71, 0xbe, 0xac, 0xb0, 0x85, 0x8d, 0xaf, 0x51, 0x6d, 0xd3, 0x4e, 0xe3, 0x9e, 0x08, 0xd0,
0x0a, 0x61, 0x80, 0x4e, 0xc0, 0xd7, 0x42, 0x93, 0xe2, 0x29, 0xce, 0xe8, 0x51, 0x94, 0xb5, 0x39,
0x35, 0x5f, 0x57, 0x65, 0x7a, 0xec, 0x1d, 0x79, 0x51, 0xb9, 0x13, 0x91, 0x5d, 0xdc, 0x53, 0xd4,
0x3d, 0x58, 0x2b, 0xa8, 0xc9, 0xde, 0xae, 0xf5, 0xb0, 0xa3, 0xb6, 0xed, 0xd6, 0xe4, 0x22, 0xb8,
0x39, 0xf7, 0x90, 0x80, 0x8d, 0x7f, 0x8d, 0x31, 0x10, 0x5d, 0xa9, 0x38, 0x9e, 0x61, 0x48, 0xf5,
0x1e, 0x41, 0x8e, 0xb8, 0xf9, 0x82, 0xa3, 0x57, 0xf8, 0xe5, 0xa7, 0x72, 0xa0, 0x48, 0x91, 0xf3,
0x0e, 0x12, 0x21, 0x74, 0xc7, 0xd7, 0x6e, 0x3e, 0x9e, 0xb9, 0x87, 0xd0, 0x37, 0xa0, 0x84, 0xbf,
0xc0, 0x05, 0xf9, 0x05, 0xee, 0x4b, 0x29, 0x71, 0x88, 0x38, 0xde, 0xf1, 0xda, 0x47, 0xa2, 0x4b,
0xc1, 0x15, 0x5c, 0xe3, 0xfe, 0xb7, 0xb5, 0x4a, 0x97, 0x29, 0x18, 0xb0, 0x7e, 0xaa, 0x5f, 0x11,
0x40, 0x53, 0x7c, 0x27, 0x23, 0xcc, 0x4c, 0xcb, 0x7f, 0xff, 0xfe, 0xc7, 0x7c, 0xff, 0x72, 0x68,
0x5a, 0xc9, 0x02, 0x7e, 0x63, 0xdf, 0x36, 0xb1, 0x94, 0xc0, 0x87, 0xde, 0xdb, 0x37, 0xef, 0xaf,
0xbd, 0x13, 0x6f, 0xd2, 0xe8, 0xc9, 0x08, 0x1a, 0x89, 0x87, 0x76, 0x3c, 0xe0, 0x2f, 0x6c, 0x5a,
0xd0, 0x69, 0x3e, 0xf4, 0x26, 0x0c, 0x6f, 0x77, 0xfc, 0x68, 0xde, 0x98, 0xce, 0xce, 0xf1, 0xa8,
0x8c, 0x17, 0xd2, 0xff, 0xac, 0x4c, 0x12, 0xed, 0x0a, 0x8d, 0x17, 0x87, 0x27, 0x93, 0xbd, 0xbe,
0xf6, 0xa6, 0x02, 0xed, 0x4d, 0x26, 0xea, 0x27, 0x82, 0x48, 0x7a, 0xec, 0x19, 0x59, 0xe0, 0xdd,
0x44, 0x8c, 0xd7, 0xe1, 0x08, 0x21, 0xb7, 0x18, 0x06, 0xde, 0xaa, 0xed, 0xb0, 0xc4, 0x7b, 0xa1,
0xb9, 0x4f, 0x27, 0xcd, 0xbf, 0x0f, 0xff, 0x01, 0x5f, 0x9f, 0x85, 0x5a, 0x56, 0x0c, 0x00, 0x00,
};
File diff suppressed because it is too large Load Diff
+78 -78
View File
@@ -1,78 +1,78 @@
/*
* SPDX-License-Identifier: Apache-2.0
* LR1110 hardware hooks for LoRaRadioBase.
*/
#include "LR1110Radio.h"
#include <zephyr/kernel.h>
/* LR11xx driver extension API */
extern "C" {
#include "lr11xx_lora.h"
}
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(lr1110_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(lr11xx_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
LR1110Radio::LR1110Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
}
void LR1110Radio::begin()
{
startTxThread(lr11xx_tx_wait_stack,
K_THREAD_STACK_SIZEOF(lr11xx_tx_wait_stack));
LoRaRadioBase::begin();
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool LR1110Radio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
return false;
}
return true;
}
void LR1110Radio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int LR1110Radio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t LR1110Radio::hwGetCurrentRSSI()
{
return lr11xx_get_rssi_inst(_dev);
}
bool LR1110Radio::hwIsPreambleDetected()
{
return lr11xx_is_receiving(_dev);
}
void LR1110Radio::hwSetRxBoost(bool enable)
{
lr11xx_set_rx_boost(_dev, enable);
}
void LR1110Radio::hwResetAGC()
{
/* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX boost */
lr11xx_reset_agc(_dev);
}
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* LR1110 hardware hooks for LoRaRadioBase.
*/
#include "LR1110Radio.h"
#include <zephyr/kernel.h>
/* LR11xx driver extension API */
extern "C" {
#include "lr11xx_lora.h"
}
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(lr1110_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(lr11xx_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
LR1110Radio::LR1110Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
}
void LR1110Radio::begin()
{
startTxThread(lr11xx_tx_wait_stack,
K_THREAD_STACK_SIZEOF(lr11xx_tx_wait_stack));
LoRaRadioBase::begin();
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool LR1110Radio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
return false;
}
return true;
}
void LR1110Radio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int LR1110Radio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t LR1110Radio::hwGetCurrentRSSI()
{
return lr11xx_get_rssi_inst(_dev);
}
bool LR1110Radio::hwIsPreambleDetected()
{
return lr11xx_is_receiving(_dev);
}
void LR1110Radio::hwSetRxBoost(bool enable)
{
lr11xx_set_rx_boost(_dev, enable);
}
void LR1110Radio::hwResetAGC()
{
/* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX boost */
lr11xx_reset_agc(_dev);
}
} /* namespace mesh */
+33 -33
View File
@@ -1,33 +1,33 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for LR1110/LR1120/LR1121 using Zephyr LoRa driver
*
* Thin wrapper around LoRaRadioBase — only hardware-specific hooks.
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class LR1110Radio : public LoRaRadioBase {
public:
LR1110Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
protected:
/* Hardware primitives */
bool hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
int16_t hwGetCurrentRSSI() override;
bool hwIsPreambleDetected() override;
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
};
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for LR1110/LR1120/LR1121 using Zephyr LoRa driver
*
* Thin wrapper around LoRaRadioBase — only hardware-specific hooks.
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class LR1110Radio : public LoRaRadioBase {
public:
LR1110Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
protected:
/* Hardware primitives */
bool hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
int16_t hwGetCurrentRSSI() override;
bool hwIsPreambleDetected() override;
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
};
} /* namespace mesh */
+77 -77
View File
@@ -1,77 +1,77 @@
/*
* SPDX-License-Identifier: Apache-2.0
* LR2021 hardware hooks for LoRaRadioBase.
*/
#include "LR2021Radio.h"
#include <zephyr/kernel.h>
/* LR20xx driver extension API */
extern "C" {
#include "lr20xx_lora.h"
}
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(lr2021_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(lr20xx_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
LR2021Radio::LR2021Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
}
void LR2021Radio::begin()
{
startTxThread(lr20xx_tx_wait_stack,
K_THREAD_STACK_SIZEOF(lr20xx_tx_wait_stack));
LoRaRadioBase::begin();
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool LR2021Radio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
return false;
}
return true;
}
void LR2021Radio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int LR2021Radio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t LR2021Radio::hwGetCurrentRSSI()
{
return lr20xx_get_rssi_inst(_dev);
}
bool LR2021Radio::hwIsPreambleDetected()
{
return lr20xx_is_receiving(_dev);
}
void LR2021Radio::hwSetRxBoost(bool enable)
{
lr20xx_set_rx_boost(_dev, enable);
}
void LR2021Radio::hwResetAGC()
{
lr20xx_reset_agc(_dev);
}
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* LR2021 hardware hooks for LoRaRadioBase.
*/
#include "LR2021Radio.h"
#include <zephyr/kernel.h>
/* LR20xx driver extension API */
extern "C" {
#include "lr20xx_lora.h"
}
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(lr2021_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(lr20xx_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
LR2021Radio::LR2021Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
}
void LR2021Radio::begin()
{
startTxThread(lr20xx_tx_wait_stack,
K_THREAD_STACK_SIZEOF(lr20xx_tx_wait_stack));
LoRaRadioBase::begin();
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool LR2021Radio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
return false;
}
return true;
}
void LR2021Radio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int LR2021Radio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t LR2021Radio::hwGetCurrentRSSI()
{
return lr20xx_get_rssi_inst(_dev);
}
bool LR2021Radio::hwIsPreambleDetected()
{
return lr20xx_is_receiving(_dev);
}
void LR2021Radio::hwSetRxBoost(bool enable)
{
lr20xx_set_rx_boost(_dev, enable);
}
void LR2021Radio::hwResetAGC()
{
lr20xx_reset_agc(_dev);
}
} /* namespace mesh */
+33 -33
View File
@@ -1,33 +1,33 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for LR2021 using Zephyr LoRa driver
*
* Thin wrapper around LoRaRadioBase — only hardware-specific hooks.
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class LR2021Radio : public LoRaRadioBase {
public:
LR2021Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
protected:
/* Hardware primitives */
bool hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
int16_t hwGetCurrentRSSI() override;
bool hwIsPreambleDetected() override;
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
};
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for LR2021 using Zephyr LoRa driver
*
* Thin wrapper around LoRaRadioBase — only hardware-specific hooks.
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class LR2021Radio : public LoRaRadioBase {
public:
LR2021Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
protected:
/* Hardware primitives */
bool hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
int16_t hwGetCurrentRSSI() override;
bool hwIsPreambleDetected() override;
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
};
} /* namespace mesh */
File diff suppressed because it is too large Load Diff
+100 -100
View File
@@ -1,100 +1,100 @@
/*
* SPDX-License-Identifier: Apache-2.0
* SX126x hardware hooks for LoRaRadioBase — native Zephyr driver.
*/
#include "SX126xRadio.h"
#include <zephyr/kernel.h>
/* Native SX126x driver extension API */
extern "C" {
#include "sx126x_ext.h"
}
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sx126x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(sx126x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
}
void SX126xRadio::begin()
{
startTxThread(sx126x_tx_wait_stack,
K_THREAD_STACK_SIZEOF(sx126x_tx_wait_stack));
LoRaRadioBase::begin();
#if IS_ENABLED(CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH)
/* Apply undocumented register 0x8B5 RX improvement (MeshCore PR#1398).
* Must run after lora_config() has been called (via startReceive above). */
sx126x_apply_heltec_reg_patch(_dev);
LOG_INF("Applied Heltec reg 0x8B5 RX patch");
#endif
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool SX126xRadio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
return false;
}
return true;
}
void SX126xRadio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int SX126xRadio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t SX126xRadio::hwGetCurrentRSSI()
{
return sx126x_get_rssi_inst(_dev);
}
bool SX126xRadio::hwIsPreambleDetected()
{
return sx126x_is_receiving(_dev);
}
void SX126xRadio::hwSetRxBoost(bool enable)
{
sx126x_set_rx_boost(_dev, enable);
}
void SX126xRadio::hwResetAGC()
{
/* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX settings */
sx126x_reset_agc(_dev);
}
bool SX126xRadio::hwIsChipBusy()
{
return sx126x_is_chip_busy(_dev);
}
uint32_t SX126xRadio::getDutyCycleTimeoutRestarts() const
{
return sx126x_get_dc_timeout_restarts(_dev);
}
void SX126xRadio::resetDutyCycleTimeoutRestarts()
{
sx126x_reset_dc_timeout_restarts(_dev);
}
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* SX126x hardware hooks for LoRaRadioBase — native Zephyr driver.
*/
#include "SX126xRadio.h"
#include <zephyr/kernel.h>
/* Native SX126x driver extension API */
extern "C" {
#include "sx126x_ext.h"
}
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sx126x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(sx126x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
}
void SX126xRadio::begin()
{
startTxThread(sx126x_tx_wait_stack,
K_THREAD_STACK_SIZEOF(sx126x_tx_wait_stack));
LoRaRadioBase::begin();
#if IS_ENABLED(CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH)
/* Apply undocumented register 0x8B5 RX improvement (MeshCore PR#1398).
* Must run after lora_config() has been called (via startReceive above). */
sx126x_apply_heltec_reg_patch(_dev);
LOG_INF("Applied Heltec reg 0x8B5 RX patch");
#endif
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool SX126xRadio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
return false;
}
return true;
}
void SX126xRadio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int SX126xRadio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t SX126xRadio::hwGetCurrentRSSI()
{
return sx126x_get_rssi_inst(_dev);
}
bool SX126xRadio::hwIsPreambleDetected()
{
return sx126x_is_receiving(_dev);
}
void SX126xRadio::hwSetRxBoost(bool enable)
{
sx126x_set_rx_boost(_dev, enable);
}
void SX126xRadio::hwResetAGC()
{
/* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX settings */
sx126x_reset_agc(_dev);
}
bool SX126xRadio::hwIsChipBusy()
{
return sx126x_is_chip_busy(_dev);
}
uint32_t SX126xRadio::getDutyCycleTimeoutRestarts() const
{
return sx126x_get_dc_timeout_restarts(_dev);
}
void SX126xRadio::resetDutyCycleTimeoutRestarts()
{
sx126x_reset_dc_timeout_restarts(_dev);
}
} /* namespace mesh */
+36 -36
View File
@@ -1,36 +1,36 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using native Zephyr driver
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class SX126xRadio : public LoRaRadioBase {
public:
SX126xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
/* Duty-cycle preamble false-positive stats (SX126x-specific) */
uint32_t getDutyCycleTimeoutRestarts() const override;
void resetDutyCycleTimeoutRestarts() override;
protected:
/* Hardware primitives */
bool hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
int16_t hwGetCurrentRSSI() override;
bool hwIsPreambleDetected() override;
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
bool hwIsChipBusy() override;
};
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using native Zephyr driver
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class SX126xRadio : public LoRaRadioBase {
public:
SX126xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
/* Duty-cycle preamble false-positive stats (SX126x-specific) */
uint32_t getDutyCycleTimeoutRestarts() const override;
void resetDutyCycleTimeoutRestarts() override;
protected:
/* Hardware primitives */
bool hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
int16_t hwGetCurrentRSSI() override;
bool hwIsPreambleDetected() override;
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
bool hwIsChipBusy() override;
};
} /* namespace mesh */
+114 -114
View File
@@ -1,114 +1,114 @@
/*
* SPDX-License-Identifier: Apache-2.0
* SX127x hardware hooks for LoRaRadioBase — Zephyr loramac-node driver.
*
* The SX127x driver (loramac-node/sx127x.c) exposes only the standard
* Zephyr LoRa API. Chip-specific features that the SX126x native driver
* provides via sx126x_ext.h are not available here:
*
* hwGetCurrentRSSI() — returns -80 dBm sentinel (no hardware path)
* hwIsPreambleDetected()— always false (no preamble-detect IRQ exposed)
* hwSetRxBoost() — no-op (SX127x has no RX boost register)
* hwResetAGC() — no-op (loramac-node manages AGC internally)
* hwIsChipBusy() — inherited false (no BUSY pin on SX127x)
*
* Everything else (configure, send, receive) uses the standard API.
*/
#include "SX127xRadio.h"
#include <zephyr/kernel.h>
#include <zephyr/drivers/lora.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sx127x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(sx127x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
SX127xRadio::SX127xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
/* SX127x has no RX boost feature — start with boost disabled */
_rx_boost_enabled = false;
/* loramac-node requires full lora_config() on every TX/RX direction
* change — Radio.SetTxConfig() and Radio.SetRxConfig() configure
* completely disjoint internal state in the loramac-node library. */
_loramac_node = true;
}
void SX127xRadio::begin()
{
startTxThread(sx127x_tx_wait_stack,
K_THREAD_STACK_SIZEOF(sx127x_tx_wait_stack));
LoRaRadioBase::begin();
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool SX127xRadio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
return false;
}
return true;
}
void SX127xRadio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int SX127xRadio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t SX127xRadio::hwGetCurrentRSSI()
{
/* SX127x loramac-node driver does not expose an instantaneous RSSI
* function via the standard Zephyr API. Return a plausible noise-floor
* sentinel so LoRaRadioBase::triggerNoiseFloorCalibrate() converges
* to a reasonable value rather than being seeded with garbage. */
return -80;
}
bool SX127xRadio::hwIsPreambleDetected()
{
/* No preamble-detect IRQ accessible through the standard Zephyr LoRa
* API for the loramac-node driver. Returning false means TX will
* not abort for an in-progress preamble — acceptable on SX127x. */
return false;
}
void SX127xRadio::hwSetRxBoost(bool enable)
{
/* SX127x does not have a dedicated RX boost / high-sensitivity mode
* switch. Sensitivity is controlled via lora_config tx_power and
* the DTS power-amplifier-output property. Nothing to do here. */
ARG_UNUSED(enable);
}
void SX127xRadio::hwResetAGC()
{
/* The loramac-node SX127x driver manages AGC recalibration internally
* (RadioSetRxConfig re-programs all gain registers on every RX config
* call). No explicit AGC reset is needed or possible via the standard
* API. */
}
void SX127xRadio::resetAGC()
{
/* hwResetAGC() is a no-op, so skip the base-class resetAGC() entirely.
* The base class calls startReceive() after hwResetAGC(), but the
* loramac-node modem mutex (STATE_BUSY during async RX) causes
* lora_recv_async() to return -EBUSY, setting _in_recv_mode = 0 and
* corrupting the state machine. The loramac-node driver self-manages
* AGC, so nothing needs to happen here. */
}
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* SX127x hardware hooks for LoRaRadioBase — Zephyr loramac-node driver.
*
* The SX127x driver (loramac-node/sx127x.c) exposes only the standard
* Zephyr LoRa API. Chip-specific features that the SX126x native driver
* provides via sx126x_ext.h are not available here:
*
* hwGetCurrentRSSI() — returns -80 dBm sentinel (no hardware path)
* hwIsPreambleDetected()— always false (no preamble-detect IRQ exposed)
* hwSetRxBoost() — no-op (SX127x has no RX boost register)
* hwResetAGC() — no-op (loramac-node manages AGC internally)
* hwIsChipBusy() — inherited false (no BUSY pin on SX127x)
*
* Everything else (configure, send, receive) uses the standard API.
*/
#include "SX127xRadio.h"
#include <zephyr/kernel.h>
#include <zephyr/drivers/lora.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sx127x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(sx127x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
SX127xRadio::SX127xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
/* SX127x has no RX boost feature — start with boost disabled */
_rx_boost_enabled = false;
/* loramac-node requires full lora_config() on every TX/RX direction
* change — Radio.SetTxConfig() and Radio.SetRxConfig() configure
* completely disjoint internal state in the loramac-node library. */
_loramac_node = true;
}
void SX127xRadio::begin()
{
startTxThread(sx127x_tx_wait_stack,
K_THREAD_STACK_SIZEOF(sx127x_tx_wait_stack));
LoRaRadioBase::begin();
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool SX127xRadio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
return false;
}
return true;
}
void SX127xRadio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int SX127xRadio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t SX127xRadio::hwGetCurrentRSSI()
{
/* SX127x loramac-node driver does not expose an instantaneous RSSI
* function via the standard Zephyr API. Return a plausible noise-floor
* sentinel so LoRaRadioBase::triggerNoiseFloorCalibrate() converges
* to a reasonable value rather than being seeded with garbage. */
return -80;
}
bool SX127xRadio::hwIsPreambleDetected()
{
/* No preamble-detect IRQ accessible through the standard Zephyr LoRa
* API for the loramac-node driver. Returning false means TX will
* not abort for an in-progress preamble — acceptable on SX127x. */
return false;
}
void SX127xRadio::hwSetRxBoost(bool enable)
{
/* SX127x does not have a dedicated RX boost / high-sensitivity mode
* switch. Sensitivity is controlled via lora_config tx_power and
* the DTS power-amplifier-output property. Nothing to do here. */
ARG_UNUSED(enable);
}
void SX127xRadio::hwResetAGC()
{
/* The loramac-node SX127x driver manages AGC recalibration internally
* (RadioSetRxConfig re-programs all gain registers on every RX config
* call). No explicit AGC reset is needed or possible via the standard
* API. */
}
void SX127xRadio::resetAGC()
{
/* hwResetAGC() is a no-op, so skip the base-class resetAGC() entirely.
* The base class calls startReceive() after hwResetAGC(), but the
* loramac-node modem mutex (STATE_BUSY during async RX) causes
* lora_recv_async() to return -EBUSY, setting _in_recv_mode = 0 and
* corrupting the state machine. The loramac-node driver self-manages
* AGC, so nothing needs to happen here. */
}
} /* namespace mesh */
+56 -56
View File
@@ -1,56 +1,56 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for SX127x (SX1272/SX1276/SX1278) using Zephyr loramac-node driver.
*
* The SX127x loramac-node driver supports the standard Zephyr LoRa API
* (lora_config, lora_send_async, lora_recv_async) but has no extension
* API for instantaneous RSSI, preamble detection, or RX boost.
* Those features are stubbed out below.
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class SX127xRadio : public LoRaRadioBase {
public:
SX127xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
protected:
/* Hardware primitives */
bool hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
/* SX127x via loramac-node has no instantaneous RSSI API.
* Returns a fixed sentinel (-80 dBm) — noise floor calibration
* will converge on this value rather than the real noise floor. */
int16_t hwGetCurrentRSSI() override;
/* SX127x has no preamble-detected IRQ accessible via standard API.
* Always returns false — TX will not abort for a detected preamble. */
bool hwIsPreambleDetected() override;
/* SX127x has no RX boost / LNA gain switch via standard API. No-op. */
void hwSetRxBoost(bool enable) override;
/* SX127x loramac-node driver manages AGC automatically. No-op. */
void hwResetAGC() override;
/* Override public resetAGC(): hwResetAGC() is a no-op, and the
* loramac-node modem mutex makes the base-class startReceive() call
* fail with -EBUSY (modem STATE_BUSY during async RX), which would
* set _in_recv_mode = 0 and corrupt the state machine. */
void resetAGC() override;
/* SX127x has no BUSY pin. Default (false) from base is correct. */
/* bool hwIsChipBusy() — inherited, returns false */
};
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for SX127x (SX1272/SX1276/SX1278) using Zephyr loramac-node driver.
*
* The SX127x loramac-node driver supports the standard Zephyr LoRa API
* (lora_config, lora_send_async, lora_recv_async) but has no extension
* API for instantaneous RSSI, preamble detection, or RX boost.
* Those features are stubbed out below.
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class SX127xRadio : public LoRaRadioBase {
public:
SX127xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
protected:
/* Hardware primitives */
bool hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
/* SX127x via loramac-node has no instantaneous RSSI API.
* Returns a fixed sentinel (-80 dBm) — noise floor calibration
* will converge on this value rather than the real noise floor. */
int16_t hwGetCurrentRSSI() override;
/* SX127x has no preamble-detected IRQ accessible via standard API.
* Always returns false — TX will not abort for a detected preamble. */
bool hwIsPreambleDetected() override;
/* SX127x has no RX boost / LNA gain switch via standard API. No-op. */
void hwSetRxBoost(bool enable) override;
/* SX127x loramac-node driver manages AGC automatically. No-op. */
void hwResetAGC() override;
/* Override public resetAGC(): hwResetAGC() is a no-op, and the
* loramac-node modem mutex makes the base-class startReceive() call
* fail with -EBUSY (modem STATE_BUSY during async RX), which would
* set _in_recv_mode = 0 and corrupt the state machine. */
void resetAGC() override;
/* SX127x has no BUSY pin. Default (false) from base is correct. */
/* bool hwIsChipBusy() — inherited, returns false */
};
} /* namespace mesh */
+93 -93
View File
@@ -1,93 +1,93 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ObserverMesh — listen-only LoRa mesh node.
*
* Extends mesh::Dispatcher directly (no routing, no flooding, no ACL).
* Every received packet is forwarded to the MQTT publisher queue.
* CLI handles WiFi/MQTT/radio configuration.
*/
#pragma once
#include <mesh/Dispatcher.h>
#include <mesh/StaticPoolPacketManager.h>
#include <mesh/Identity.h>
#include <mesh/RNG.h>
#include <mesh/RTC.h>
#include <helpers/NodePrefs.h>
#include "RepeaterDataStore.h"
#include "observer_creds.h"
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.1-zephyr"
#endif
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE __DATE__
#endif
namespace mesh {
class ObserverMesh : public Dispatcher {
StaticPoolPacketManager _pkt_mgr;
/* Cached values set by logRxRaw / logRx before onRecvPacket */
float _last_rssi;
float _last_score;
uint8_t _last_raw[MAX_TRANS_UNIT + 1];
int _last_raw_len;
/* Identity and config */
LocalIdentity _self_id;
NodePrefs _prefs;
RepeaterDataStore *_store;
struct ObserverCreds *_creds;
RNG *_rng;
RTCClock *_rtc;
/* Pre-built MQTT topic strings (set in begin()) */
char _pubkey_hex[PUB_KEY_SIZE * 2 + 1]; /* 64 hex chars + NUL */
char _packets_topic[160];
char _status_topic[160];
/* Private helpers */
void buildTopics();
void enqueuePacket(Packet *pkt);
void buildStatusJson(const char *status, char *out, size_t out_size);
uint32_t _start_uptime_secs;
protected:
/* Capture RSSI + raw bytes before packet is parsed */
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;
/* Build JSON and enqueue to MQTT publisher */
DispatcherAction onRecvPacket(Packet *pkt) override;
public:
ObserverMesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc);
/* Initialize: load/generate identity, load/init prefs, start radio RX. */
void begin(RepeaterDataStore *store, struct ObserverCreds *creds);
/* Handle a single serial CLI command.
* reply is filled with the response string (CLI_REPLY_SIZE bytes).
* Returns true if the command was 'help' and the caller should print
* the full banner (too long for the reply buffer). */
bool handleCLI(const char *command, char *reply, int reply_size);
/* Publish a synthetic zero-hop advert for this observer to the packets
* topic so that CoreScope can place it on the map. No-op if lat/lon
* are not configured in the creds struct. */
void publishSelfAdvert();
void publishStatus(const char *status);
/* Accessors used by main_observer.cpp */
NodePrefs *getNodePrefs() { return &_prefs; }
const LocalIdentity &getSelfId() const { return _self_id; }
const char *getPacketsTopic() const { return _packets_topic; }
const char *getStatusTopic() const { return _status_topic; }
const char *getPubkeyHex() const { return _pubkey_hex; }
};
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* ObserverMesh — listen-only LoRa mesh node.
*
* Extends mesh::Dispatcher directly (no routing, no flooding, no ACL).
* Every received packet is forwarded to the MQTT publisher queue.
* CLI handles WiFi/MQTT/radio configuration.
*/
#pragma once
#include <mesh/Dispatcher.h>
#include <mesh/StaticPoolPacketManager.h>
#include <mesh/Identity.h>
#include <mesh/RNG.h>
#include <mesh/RTC.h>
#include <helpers/NodePrefs.h>
#include "RepeaterDataStore.h"
#include "observer_creds.h"
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.1-zephyr"
#endif
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE __DATE__
#endif
namespace mesh {
class ObserverMesh : public Dispatcher {
StaticPoolPacketManager _pkt_mgr;
/* Cached values set by logRxRaw / logRx before onRecvPacket */
float _last_rssi;
float _last_score;
uint8_t _last_raw[MAX_TRANS_UNIT + 1];
int _last_raw_len;
/* Identity and config */
LocalIdentity _self_id;
NodePrefs _prefs;
RepeaterDataStore *_store;
struct ObserverCreds *_creds;
RNG *_rng;
RTCClock *_rtc;
/* Pre-built MQTT topic strings (set in begin()) */
char _pubkey_hex[PUB_KEY_SIZE * 2 + 1]; /* 64 hex chars + NUL */
char _packets_topic[160];
char _status_topic[160];
/* Private helpers */
void buildTopics();
void enqueuePacket(Packet *pkt);
void buildStatusJson(const char *status, char *out, size_t out_size);
uint32_t _start_uptime_secs;
protected:
/* Capture RSSI + raw bytes before packet is parsed */
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;
/* Build JSON and enqueue to MQTT publisher */
DispatcherAction onRecvPacket(Packet *pkt) override;
public:
ObserverMesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc);
/* Initialize: load/generate identity, load/init prefs, start radio RX. */
void begin(RepeaterDataStore *store, struct ObserverCreds *creds);
/* Handle a single serial CLI command.
* reply is filled with the response string (CLI_REPLY_SIZE bytes).
* Returns true if the command was 'help' and the caller should print
* the full banner (too long for the reply buffer). */
bool handleCLI(const char *command, char *reply, int reply_size);
/* Publish a synthetic zero-hop advert for this observer to the packets
* topic so that CoreScope can place it on the map. No-op if lat/lon
* are not configured in the creds struct. */
void publishSelfAdvert();
void publishStatus(const char *status);
/* Accessors used by main_observer.cpp */
NodePrefs *getNodePrefs() { return &_prefs; }
const LocalIdentity &getSelfId() const { return _self_id; }
const char *getPacketsTopic() const { return _packets_topic; }
const char *getStatusTopic() const { return _status_topic; }
const char *getPubkeyHex() const { return _pubkey_hex; }
};
} /* namespace mesh */
+358 -358
View File
@@ -1,358 +1,358 @@
/*
* SPDX-License-Identifier: Apache-2.0
* RepeaterDataStore - Filesystem storage for repeater
*/
#include "RepeaterDataStore.h"
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <string.h>
#include <stdio.h>
LOG_MODULE_REGISTER(zephcore_repeater_store, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL);
RepeaterDataStore::RepeaterDataStore() : _initialized(false) {
}
bool RepeaterDataStore::begin() {
if (_initialized) return true;
/* Create repeater directory if it doesn't exist */
struct fs_dirent entry;
int ret = fs_stat(BASE_PATH, &entry);
if (ret < 0) {
ret = fs_mkdir(BASE_PATH);
if (ret < 0 && ret != -EEXIST) {
LOG_ERR("Failed to create %s: %d", BASE_PATH, ret);
return false;
}
LOG_INF("Created %s directory", BASE_PATH);
}
_initialized = true;
LOG_INF("RepeaterDataStore initialized at %s", BASE_PATH);
return true;
}
const char* RepeaterDataStore::getBasePath() const { return BASE_PATH; }
const char* RepeaterDataStore::getAclPath() const {
static char buf[48];
snprintf(buf, sizeof(buf), "%s/acl", BASE_PATH);
return buf;
}
const char* RepeaterDataStore::getRegionsPath() const {
static char buf[48];
snprintf(buf, sizeof(buf), "%s/regions2", BASE_PATH);
return buf;
}
bool RepeaterDataStore::loadIdentity(mesh::LocalIdentity& id) {
char path[48];
snprintf(path, sizeof(path), "%s/_main.id", BASE_PATH);
struct fs_file_t file;
fs_file_t_init(&file);
int ret = fs_open(&file, path, FS_O_READ);
if (ret < 0) {
LOG_DBG("No identity file at %s", path);
return false;
}
uint8_t buf[PRV_KEY_SIZE + PUB_KEY_SIZE];
ssize_t n = fs_read(&file, buf, sizeof(buf));
fs_close(&file);
LOG_DBG("loadIdentity: read %d bytes from %s", (int)n, path);
if (n >= PRV_KEY_SIZE) {
if (id.readFrom(buf, n)) {
LOG_INF("Loaded identity from %s", path);
return true;
}
LOG_ERR("loadIdentity: readFrom failed");
}
LOG_ERR("Identity file corrupt");
return false;
}
bool RepeaterDataStore::saveIdentity(const mesh::LocalIdentity& id) {
if (!_initialized) begin();
char path[48];
char tmp_path[56];
snprintf(path, sizeof(path), "%s/_main.id", BASE_PATH);
if (snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path) >= (int)sizeof(tmp_path)) {
return false;
}
fs_unlink(tmp_path);
struct fs_file_t file;
fs_file_t_init(&file);
int ret = fs_open(&file, tmp_path, FS_O_CREATE | FS_O_WRITE);
if (ret < 0) {
LOG_ERR("Failed to open %s for write: %d", tmp_path, ret);
return false;
}
uint8_t buf[PRV_KEY_SIZE];
int len = id.writeTo(buf, sizeof(buf));
ssize_t n = fs_write(&file, buf, len);
ret = fs_sync(&file);
fs_close(&file);
if (n != len || ret < 0) {
LOG_ERR("Failed to write identity: wrote %d of %d sync=%d", (int)n, len, ret);
fs_unlink(tmp_path);
return false;
}
if (fs_rename(tmp_path, path) < 0) {
LOG_ERR("saveIdentity: rename failed");
fs_unlink(tmp_path);
return false;
}
LOG_INF("Saved identity to %s", path);
return true;
}
bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
char path[48];
snprintf(path, sizeof(path), "%s/prefs", BASE_PATH);
struct fs_file_t file;
fs_file_t_init(&file);
int ret = fs_open(&file, path, FS_O_READ);
if (ret < 0) {
LOG_DBG("No prefs file at %s, using defaults", path);
initNodePrefs(&prefs);
strcpy(prefs.node_name, "Repeater");
prefs.advert_loc_policy = ADVERT_LOC_PREFS;
prefs.flood_advert_interval = 25;
prefs.loop_detect = LOOP_DETECT_MINIMAL;
prefs.path_hash_mode = 1;
#if IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)
prefs.rx_duty_cycle = 1;
#endif
/* Persist defaults so flash always has a prefs file from boot 1.
* Lets later code (e.g. tempradio revert) trust that flash is
* authoritative without a "first run" special case. */
savePrefs(prefs);
return true;
}
struct fs_dirent entry;
ret = fs_stat(path, &entry);
LOG_DBG("loadPrefs: file size = %d bytes", ret < 0 ? 0 : (int)entry.size);
uint8_t pad[25];
/* Read prefs in same format as Arduino CommonCLI for compatibility */
fs_read(&file, &prefs.airtime_factor, sizeof(prefs.airtime_factor));
fs_read(&file, &prefs.node_name, sizeof(prefs.node_name));
fs_read(&file, pad, 4);
fs_read(&file, &prefs.node_lat, sizeof(prefs.node_lat));
fs_read(&file, &prefs.node_lon, sizeof(prefs.node_lon));
fs_read(&file, &prefs.password, sizeof(prefs.password));
fs_read(&file, &prefs.freq, sizeof(prefs.freq));
fs_read(&file, &prefs.tx_power_dbm, sizeof(prefs.tx_power_dbm));
fs_read(&file, &prefs.disable_fwd, sizeof(prefs.disable_fwd));
fs_read(&file, &prefs.advert_interval, sizeof(prefs.advert_interval));
fs_read(&file, pad, 1);
fs_read(&file, &prefs.rx_delay_base, sizeof(prefs.rx_delay_base));
fs_read(&file, &prefs.tx_delay_factor, sizeof(prefs.tx_delay_factor));
fs_read(&file, &prefs.guest_password, sizeof(prefs.guest_password));
fs_read(&file, &prefs.direct_tx_delay_factor, sizeof(prefs.direct_tx_delay_factor));
fs_read(&file, &prefs.backoff_multiplier, sizeof(prefs.backoff_multiplier));
fs_read(&file, &prefs.sf, sizeof(prefs.sf));
fs_read(&file, &prefs.cr, sizeof(prefs.cr));
fs_read(&file, &prefs.allow_read_only, sizeof(prefs.allow_read_only));
fs_read(&file, &prefs.multi_acks, sizeof(prefs.multi_acks));
fs_read(&file, &prefs.bw, sizeof(prefs.bw));
fs_read(&file, &prefs.agc_reset_interval, sizeof(prefs.agc_reset_interval));
fs_read(&file, &prefs.path_hash_mode, sizeof(prefs.path_hash_mode));
fs_read(&file, &prefs.loop_detect, sizeof(prefs.loop_detect));
fs_read(&file, pad, 1);
fs_read(&file, &prefs.flood_max, sizeof(prefs.flood_max));
fs_read(&file, &prefs.flood_advert_interval, sizeof(prefs.flood_advert_interval));
fs_read(&file, &prefs.interference_threshold, sizeof(prefs.interference_threshold));
fs_read(&file, pad, 25); // skip bridge settings
fs_read(&file, &prefs.powersaving_enabled, sizeof(prefs.powersaving_enabled));
fs_read(&file, pad, 3);
fs_read(&file, &prefs.gps_enabled, sizeof(prefs.gps_enabled));
fs_read(&file, &prefs.gps_interval, sizeof(prefs.gps_interval));
fs_read(&file, &prefs.advert_loc_policy, sizeof(prefs.advert_loc_policy));
fs_read(&file, &prefs.discovery_mod_timestamp, sizeof(prefs.discovery_mod_timestamp));
fs_read(&file, &prefs.adc_multiplier, sizeof(prefs.adc_multiplier));
fs_read(&file, prefs.owner_info, sizeof(prefs.owner_info));
/* ZephCore extensions — absent in old 290-byte files; fs_read past EOF is a
* no-op so these fields keep the initNodePrefs() defaults the caller passed
* in (rx_boost=1, rx_duty_cycle=0, apc_enabled=0, apc_margin=16). The
* upgrade block below forces repeater-specific values for old files. */
fs_read(&file, &prefs.rx_boost, sizeof(prefs.rx_boost));
fs_read(&file, &prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle));
fs_read(&file, &prefs.apc_enabled, sizeof(prefs.apc_enabled));
fs_read(&file, &prefs.apc_margin, sizeof(prefs.apc_margin));
fs_close(&file);
/* Migrate uninitialized backoff_multiplier (0.0 or NaN) to default */
if (prefs.backoff_multiplier == 0.0f || prefs.backoff_multiplier != prefs.backoff_multiplier) {
prefs.backoff_multiplier = 0.2f;
}
LOG_INF("Loaded prefs from %s", path);
LOG_DBG(" name='%s' freq=%.3f sf=%u bw=%.1f tx_pwr=%d",
prefs.node_name, (double)prefs.freq, prefs.sf, (double)prefs.bw, prefs.tx_power_dbm);
/* Validate radio params - use defaults if garbage */
if (prefs.freq < 300.0f || prefs.freq > 1000.0f ||
prefs.sf < 5 || prefs.sf > 12 ||
prefs.bw < 7.0f || prefs.bw > 500.0f) {
LOG_WRN("Invalid radio params in prefs, using defaults: freq=%.3f sf=%u bw=%.1f",
(double)prefs.freq, prefs.sf, (double)prefs.bw);
prefs.freq = 869.618f;
prefs.bw = 62.5f;
prefs.sf = 8;
prefs.cr = 8;
prefs.tx_power_dbm = 22;
}
if (prefs.path_hash_mode > 2) prefs.path_hash_mode = 0;
if (prefs.loop_detect > LOOP_DETECT_STRICT) prefs.loop_detect = LOOP_DETECT_MINIMAL;
if (prefs.rx_boost > 1) prefs.rx_boost = 0;
if (prefs.rx_duty_cycle > 1) prefs.rx_duty_cycle = 0;
if (prefs.apc_enabled > 1) prefs.apc_enabled = 0;
if (prefs.apc_margin < 6 || prefs.apc_margin > 30) prefs.apc_margin = 16;
/* One-time format upgrade: old files (< 294 bytes) never saved the ZephCore
* extension fields, and stored path_hash_mode/loop_detect as zero padding.
* Apply repeater defaults and re-save so values survive subsequent reboots. */
if (ret >= 0 && entry.size < 294) {
prefs.rx_boost = 1;
prefs.path_hash_mode = 1;
prefs.loop_detect = LOOP_DETECT_MINIMAL;
#if IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)
prefs.rx_duty_cycle = 1;
#endif
savePrefs(prefs);
LOG_INF("loadPrefs: upgraded prefs format (%d -> 294 bytes)", (int)entry.size);
}
return true;
}
bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
if (!_initialized) begin();
char path[48];
char tmp_path[56];
snprintf(path, sizeof(path), "%s/prefs", BASE_PATH);
if (snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path) >= (int)sizeof(tmp_path)) {
return false;
}
fs_unlink(tmp_path);
struct fs_file_t file;
fs_file_t_init(&file);
int ret = fs_open(&file, tmp_path, FS_O_CREATE | FS_O_WRITE);
if (ret < 0) {
LOG_ERR("Failed to open %s for write: %d", tmp_path, ret);
return false;
}
uint8_t pad[25];
memset(pad, 0, sizeof(pad));
/* Write prefs in same format as Arduino CommonCLI for compatibility */
fs_write(&file, &prefs.airtime_factor, sizeof(prefs.airtime_factor));
fs_write(&file, &prefs.node_name, sizeof(prefs.node_name));
fs_write(&file, pad, 4);
fs_write(&file, &prefs.node_lat, sizeof(prefs.node_lat));
fs_write(&file, &prefs.node_lon, sizeof(prefs.node_lon));
fs_write(&file, &prefs.password, sizeof(prefs.password));
fs_write(&file, &prefs.freq, sizeof(prefs.freq));
fs_write(&file, &prefs.tx_power_dbm, sizeof(prefs.tx_power_dbm));
fs_write(&file, &prefs.disable_fwd, sizeof(prefs.disable_fwd));
fs_write(&file, &prefs.advert_interval, sizeof(prefs.advert_interval));
fs_write(&file, pad, 1);
fs_write(&file, &prefs.rx_delay_base, sizeof(prefs.rx_delay_base));
fs_write(&file, &prefs.tx_delay_factor, sizeof(prefs.tx_delay_factor));
fs_write(&file, &prefs.guest_password, sizeof(prefs.guest_password));
fs_write(&file, &prefs.direct_tx_delay_factor, sizeof(prefs.direct_tx_delay_factor));
fs_write(&file, &prefs.backoff_multiplier, sizeof(prefs.backoff_multiplier));
fs_write(&file, &prefs.sf, sizeof(prefs.sf));
fs_write(&file, &prefs.cr, sizeof(prefs.cr));
fs_write(&file, &prefs.allow_read_only, sizeof(prefs.allow_read_only));
fs_write(&file, &prefs.multi_acks, sizeof(prefs.multi_acks));
fs_write(&file, &prefs.bw, sizeof(prefs.bw));
fs_write(&file, &prefs.agc_reset_interval, sizeof(prefs.agc_reset_interval));
fs_write(&file, &prefs.path_hash_mode, sizeof(prefs.path_hash_mode));
fs_write(&file, &prefs.loop_detect, sizeof(prefs.loop_detect));
fs_write(&file, pad, 1);
fs_write(&file, &prefs.flood_max, sizeof(prefs.flood_max));
fs_write(&file, &prefs.flood_advert_interval, sizeof(prefs.flood_advert_interval));
fs_write(&file, &prefs.interference_threshold, sizeof(prefs.interference_threshold));
fs_write(&file, pad, 25); // skip bridge settings
fs_write(&file, &prefs.powersaving_enabled, sizeof(prefs.powersaving_enabled));
fs_write(&file, pad, 3);
fs_write(&file, &prefs.gps_enabled, sizeof(prefs.gps_enabled));
fs_write(&file, &prefs.gps_interval, sizeof(prefs.gps_interval));
fs_write(&file, &prefs.advert_loc_policy, sizeof(prefs.advert_loc_policy));
fs_write(&file, &prefs.discovery_mod_timestamp, sizeof(prefs.discovery_mod_timestamp));
fs_write(&file, &prefs.adc_multiplier, sizeof(prefs.adc_multiplier));
fs_write(&file, prefs.owner_info, sizeof(prefs.owner_info));
/* ZephCore extensions */
fs_write(&file, &prefs.rx_boost, sizeof(prefs.rx_boost));
fs_write(&file, &prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle));
fs_write(&file, &prefs.apc_enabled, sizeof(prefs.apc_enabled));
fs_write(&file, &prefs.apc_margin, sizeof(prefs.apc_margin));
ret = fs_sync(&file);
fs_close(&file);
if (ret < 0) {
LOG_ERR("savePrefs: sync failed: %d", ret);
fs_unlink(tmp_path);
return false;
}
if (fs_rename(tmp_path, path) < 0) {
LOG_ERR("savePrefs: rename failed");
fs_unlink(tmp_path);
return false;
}
LOG_INF("Saved prefs to %s", path);
return true;
}
bool RepeaterDataStore::formatFileSystem() {
LOG_WRN("Factory reset: erasing repeater data at %s", BASE_PATH);
struct fs_dir_t dir;
fs_dir_t_init(&dir);
int ret = fs_opendir(&dir, BASE_PATH);
if (ret < 0) {
LOG_WRN("No repeater directory to erase");
return true;
}
struct fs_dirent entry;
char path[280];
while (fs_readdir(&dir, &entry) == 0 && entry.name[0] != '\0') {
snprintf(path, sizeof(path), "%s/%s", BASE_PATH, entry.name);
LOG_INF("Deleting %s", path);
fs_unlink(path);
}
fs_closedir(&dir);
LOG_INF("Repeater data erased");
return true;
}
/*
* SPDX-License-Identifier: Apache-2.0
* RepeaterDataStore - Filesystem storage for repeater
*/
#include "RepeaterDataStore.h"
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <string.h>
#include <stdio.h>
LOG_MODULE_REGISTER(zephcore_repeater_store, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL);
RepeaterDataStore::RepeaterDataStore() : _initialized(false) {
}
bool RepeaterDataStore::begin() {
if (_initialized) return true;
/* Create repeater directory if it doesn't exist */
struct fs_dirent entry;
int ret = fs_stat(BASE_PATH, &entry);
if (ret < 0) {
ret = fs_mkdir(BASE_PATH);
if (ret < 0 && ret != -EEXIST) {
LOG_ERR("Failed to create %s: %d", BASE_PATH, ret);
return false;
}
LOG_INF("Created %s directory", BASE_PATH);
}
_initialized = true;
LOG_INF("RepeaterDataStore initialized at %s", BASE_PATH);
return true;
}
const char* RepeaterDataStore::getBasePath() const { return BASE_PATH; }
const char* RepeaterDataStore::getAclPath() const {
static char buf[48];
snprintf(buf, sizeof(buf), "%s/acl", BASE_PATH);
return buf;
}
const char* RepeaterDataStore::getRegionsPath() const {
static char buf[48];
snprintf(buf, sizeof(buf), "%s/regions2", BASE_PATH);
return buf;
}
bool RepeaterDataStore::loadIdentity(mesh::LocalIdentity& id) {
char path[48];
snprintf(path, sizeof(path), "%s/_main.id", BASE_PATH);
struct fs_file_t file;
fs_file_t_init(&file);
int ret = fs_open(&file, path, FS_O_READ);
if (ret < 0) {
LOG_DBG("No identity file at %s", path);
return false;
}
uint8_t buf[PRV_KEY_SIZE + PUB_KEY_SIZE];
ssize_t n = fs_read(&file, buf, sizeof(buf));
fs_close(&file);
LOG_DBG("loadIdentity: read %d bytes from %s", (int)n, path);
if (n >= PRV_KEY_SIZE) {
if (id.readFrom(buf, n)) {
LOG_INF("Loaded identity from %s", path);
return true;
}
LOG_ERR("loadIdentity: readFrom failed");
}
LOG_ERR("Identity file corrupt");
return false;
}
bool RepeaterDataStore::saveIdentity(const mesh::LocalIdentity& id) {
if (!_initialized) begin();
char path[48];
char tmp_path[56];
snprintf(path, sizeof(path), "%s/_main.id", BASE_PATH);
if (snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path) >= (int)sizeof(tmp_path)) {
return false;
}
fs_unlink(tmp_path);
struct fs_file_t file;
fs_file_t_init(&file);
int ret = fs_open(&file, tmp_path, FS_O_CREATE | FS_O_WRITE);
if (ret < 0) {
LOG_ERR("Failed to open %s for write: %d", tmp_path, ret);
return false;
}
uint8_t buf[PRV_KEY_SIZE];
int len = id.writeTo(buf, sizeof(buf));
ssize_t n = fs_write(&file, buf, len);
ret = fs_sync(&file);
fs_close(&file);
if (n != len || ret < 0) {
LOG_ERR("Failed to write identity: wrote %d of %d sync=%d", (int)n, len, ret);
fs_unlink(tmp_path);
return false;
}
if (fs_rename(tmp_path, path) < 0) {
LOG_ERR("saveIdentity: rename failed");
fs_unlink(tmp_path);
return false;
}
LOG_INF("Saved identity to %s", path);
return true;
}
bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
char path[48];
snprintf(path, sizeof(path), "%s/prefs", BASE_PATH);
struct fs_file_t file;
fs_file_t_init(&file);
int ret = fs_open(&file, path, FS_O_READ);
if (ret < 0) {
LOG_DBG("No prefs file at %s, using defaults", path);
initNodePrefs(&prefs);
strcpy(prefs.node_name, "Repeater");
prefs.advert_loc_policy = ADVERT_LOC_PREFS;
prefs.flood_advert_interval = 25;
prefs.loop_detect = LOOP_DETECT_MINIMAL;
prefs.path_hash_mode = 1;
#if IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)
prefs.rx_duty_cycle = 1;
#endif
/* Persist defaults so flash always has a prefs file from boot 1.
* Lets later code (e.g. tempradio revert) trust that flash is
* authoritative without a "first run" special case. */
savePrefs(prefs);
return true;
}
struct fs_dirent entry;
ret = fs_stat(path, &entry);
LOG_DBG("loadPrefs: file size = %d bytes", ret < 0 ? 0 : (int)entry.size);
uint8_t pad[25];
/* Read prefs in same format as Arduino CommonCLI for compatibility */
fs_read(&file, &prefs.airtime_factor, sizeof(prefs.airtime_factor));
fs_read(&file, &prefs.node_name, sizeof(prefs.node_name));
fs_read(&file, pad, 4);
fs_read(&file, &prefs.node_lat, sizeof(prefs.node_lat));
fs_read(&file, &prefs.node_lon, sizeof(prefs.node_lon));
fs_read(&file, &prefs.password, sizeof(prefs.password));
fs_read(&file, &prefs.freq, sizeof(prefs.freq));
fs_read(&file, &prefs.tx_power_dbm, sizeof(prefs.tx_power_dbm));
fs_read(&file, &prefs.disable_fwd, sizeof(prefs.disable_fwd));
fs_read(&file, &prefs.advert_interval, sizeof(prefs.advert_interval));
fs_read(&file, pad, 1);
fs_read(&file, &prefs.rx_delay_base, sizeof(prefs.rx_delay_base));
fs_read(&file, &prefs.tx_delay_factor, sizeof(prefs.tx_delay_factor));
fs_read(&file, &prefs.guest_password, sizeof(prefs.guest_password));
fs_read(&file, &prefs.direct_tx_delay_factor, sizeof(prefs.direct_tx_delay_factor));
fs_read(&file, &prefs.backoff_multiplier, sizeof(prefs.backoff_multiplier));
fs_read(&file, &prefs.sf, sizeof(prefs.sf));
fs_read(&file, &prefs.cr, sizeof(prefs.cr));
fs_read(&file, &prefs.allow_read_only, sizeof(prefs.allow_read_only));
fs_read(&file, &prefs.multi_acks, sizeof(prefs.multi_acks));
fs_read(&file, &prefs.bw, sizeof(prefs.bw));
fs_read(&file, &prefs.agc_reset_interval, sizeof(prefs.agc_reset_interval));
fs_read(&file, &prefs.path_hash_mode, sizeof(prefs.path_hash_mode));
fs_read(&file, &prefs.loop_detect, sizeof(prefs.loop_detect));
fs_read(&file, pad, 1);
fs_read(&file, &prefs.flood_max, sizeof(prefs.flood_max));
fs_read(&file, &prefs.flood_advert_interval, sizeof(prefs.flood_advert_interval));
fs_read(&file, &prefs.interference_threshold, sizeof(prefs.interference_threshold));
fs_read(&file, pad, 25); // skip bridge settings
fs_read(&file, &prefs.powersaving_enabled, sizeof(prefs.powersaving_enabled));
fs_read(&file, pad, 3);
fs_read(&file, &prefs.gps_enabled, sizeof(prefs.gps_enabled));
fs_read(&file, &prefs.gps_interval, sizeof(prefs.gps_interval));
fs_read(&file, &prefs.advert_loc_policy, sizeof(prefs.advert_loc_policy));
fs_read(&file, &prefs.discovery_mod_timestamp, sizeof(prefs.discovery_mod_timestamp));
fs_read(&file, &prefs.adc_multiplier, sizeof(prefs.adc_multiplier));
fs_read(&file, prefs.owner_info, sizeof(prefs.owner_info));
/* ZephCore extensions — absent in old 290-byte files; fs_read past EOF is a
* no-op so these fields keep the initNodePrefs() defaults the caller passed
* in (rx_boost=1, rx_duty_cycle=0, apc_enabled=0, apc_margin=16). The
* upgrade block below forces repeater-specific values for old files. */
fs_read(&file, &prefs.rx_boost, sizeof(prefs.rx_boost));
fs_read(&file, &prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle));
fs_read(&file, &prefs.apc_enabled, sizeof(prefs.apc_enabled));
fs_read(&file, &prefs.apc_margin, sizeof(prefs.apc_margin));
fs_close(&file);
/* Migrate uninitialized backoff_multiplier (0.0 or NaN) to default */
if (prefs.backoff_multiplier == 0.0f || prefs.backoff_multiplier != prefs.backoff_multiplier) {
prefs.backoff_multiplier = 0.2f;
}
LOG_INF("Loaded prefs from %s", path);
LOG_DBG(" name='%s' freq=%.3f sf=%u bw=%.1f tx_pwr=%d",
prefs.node_name, (double)prefs.freq, prefs.sf, (double)prefs.bw, prefs.tx_power_dbm);
/* Validate radio params - use defaults if garbage */
if (prefs.freq < 300.0f || prefs.freq > 1000.0f ||
prefs.sf < 5 || prefs.sf > 12 ||
prefs.bw < 7.0f || prefs.bw > 500.0f) {
LOG_WRN("Invalid radio params in prefs, using defaults: freq=%.3f sf=%u bw=%.1f",
(double)prefs.freq, prefs.sf, (double)prefs.bw);
prefs.freq = 869.618f;
prefs.bw = 62.5f;
prefs.sf = 8;
prefs.cr = 8;
prefs.tx_power_dbm = 22;
}
if (prefs.path_hash_mode > 2) prefs.path_hash_mode = 0;
if (prefs.loop_detect > LOOP_DETECT_STRICT) prefs.loop_detect = LOOP_DETECT_MINIMAL;
if (prefs.rx_boost > 1) prefs.rx_boost = 0;
if (prefs.rx_duty_cycle > 1) prefs.rx_duty_cycle = 0;
if (prefs.apc_enabled > 1) prefs.apc_enabled = 0;
if (prefs.apc_margin < 6 || prefs.apc_margin > 30) prefs.apc_margin = 16;
/* One-time format upgrade: old files (< 294 bytes) never saved the ZephCore
* extension fields, and stored path_hash_mode/loop_detect as zero padding.
* Apply repeater defaults and re-save so values survive subsequent reboots. */
if (ret >= 0 && entry.size < 294) {
prefs.rx_boost = 1;
prefs.path_hash_mode = 1;
prefs.loop_detect = LOOP_DETECT_MINIMAL;
#if IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)
prefs.rx_duty_cycle = 1;
#endif
savePrefs(prefs);
LOG_INF("loadPrefs: upgraded prefs format (%d -> 294 bytes)", (int)entry.size);
}
return true;
}
bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
if (!_initialized) begin();
char path[48];
char tmp_path[56];
snprintf(path, sizeof(path), "%s/prefs", BASE_PATH);
if (snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path) >= (int)sizeof(tmp_path)) {
return false;
}
fs_unlink(tmp_path);
struct fs_file_t file;
fs_file_t_init(&file);
int ret = fs_open(&file, tmp_path, FS_O_CREATE | FS_O_WRITE);
if (ret < 0) {
LOG_ERR("Failed to open %s for write: %d", tmp_path, ret);
return false;
}
uint8_t pad[25];
memset(pad, 0, sizeof(pad));
/* Write prefs in same format as Arduino CommonCLI for compatibility */
fs_write(&file, &prefs.airtime_factor, sizeof(prefs.airtime_factor));
fs_write(&file, &prefs.node_name, sizeof(prefs.node_name));
fs_write(&file, pad, 4);
fs_write(&file, &prefs.node_lat, sizeof(prefs.node_lat));
fs_write(&file, &prefs.node_lon, sizeof(prefs.node_lon));
fs_write(&file, &prefs.password, sizeof(prefs.password));
fs_write(&file, &prefs.freq, sizeof(prefs.freq));
fs_write(&file, &prefs.tx_power_dbm, sizeof(prefs.tx_power_dbm));
fs_write(&file, &prefs.disable_fwd, sizeof(prefs.disable_fwd));
fs_write(&file, &prefs.advert_interval, sizeof(prefs.advert_interval));
fs_write(&file, pad, 1);
fs_write(&file, &prefs.rx_delay_base, sizeof(prefs.rx_delay_base));
fs_write(&file, &prefs.tx_delay_factor, sizeof(prefs.tx_delay_factor));
fs_write(&file, &prefs.guest_password, sizeof(prefs.guest_password));
fs_write(&file, &prefs.direct_tx_delay_factor, sizeof(prefs.direct_tx_delay_factor));
fs_write(&file, &prefs.backoff_multiplier, sizeof(prefs.backoff_multiplier));
fs_write(&file, &prefs.sf, sizeof(prefs.sf));
fs_write(&file, &prefs.cr, sizeof(prefs.cr));
fs_write(&file, &prefs.allow_read_only, sizeof(prefs.allow_read_only));
fs_write(&file, &prefs.multi_acks, sizeof(prefs.multi_acks));
fs_write(&file, &prefs.bw, sizeof(prefs.bw));
fs_write(&file, &prefs.agc_reset_interval, sizeof(prefs.agc_reset_interval));
fs_write(&file, &prefs.path_hash_mode, sizeof(prefs.path_hash_mode));
fs_write(&file, &prefs.loop_detect, sizeof(prefs.loop_detect));
fs_write(&file, pad, 1);
fs_write(&file, &prefs.flood_max, sizeof(prefs.flood_max));
fs_write(&file, &prefs.flood_advert_interval, sizeof(prefs.flood_advert_interval));
fs_write(&file, &prefs.interference_threshold, sizeof(prefs.interference_threshold));
fs_write(&file, pad, 25); // skip bridge settings
fs_write(&file, &prefs.powersaving_enabled, sizeof(prefs.powersaving_enabled));
fs_write(&file, pad, 3);
fs_write(&file, &prefs.gps_enabled, sizeof(prefs.gps_enabled));
fs_write(&file, &prefs.gps_interval, sizeof(prefs.gps_interval));
fs_write(&file, &prefs.advert_loc_policy, sizeof(prefs.advert_loc_policy));
fs_write(&file, &prefs.discovery_mod_timestamp, sizeof(prefs.discovery_mod_timestamp));
fs_write(&file, &prefs.adc_multiplier, sizeof(prefs.adc_multiplier));
fs_write(&file, prefs.owner_info, sizeof(prefs.owner_info));
/* ZephCore extensions */
fs_write(&file, &prefs.rx_boost, sizeof(prefs.rx_boost));
fs_write(&file, &prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle));
fs_write(&file, &prefs.apc_enabled, sizeof(prefs.apc_enabled));
fs_write(&file, &prefs.apc_margin, sizeof(prefs.apc_margin));
ret = fs_sync(&file);
fs_close(&file);
if (ret < 0) {
LOG_ERR("savePrefs: sync failed: %d", ret);
fs_unlink(tmp_path);
return false;
}
if (fs_rename(tmp_path, path) < 0) {
LOG_ERR("savePrefs: rename failed");
fs_unlink(tmp_path);
return false;
}
LOG_INF("Saved prefs to %s", path);
return true;
}
bool RepeaterDataStore::formatFileSystem() {
LOG_WRN("Factory reset: erasing repeater data at %s", BASE_PATH);
struct fs_dir_t dir;
fs_dir_t_init(&dir);
int ret = fs_opendir(&dir, BASE_PATH);
if (ret < 0) {
LOG_WRN("No repeater directory to erase");
return true;
}
struct fs_dirent entry;
char path[280];
while (fs_readdir(&dir, &entry) == 0 && entry.name[0] != '\0') {
snprintf(path, sizeof(path), "%s/%s", BASE_PATH, entry.name);
LOG_INF("Deleting %s", path);
fs_unlink(path);
}
fs_closedir(&dir);
LOG_INF("Repeater data erased");
return true;
}
+127 -127
View File
@@ -1,127 +1,127 @@
/*
* SPDX-License-Identifier: Apache-2.0
* NodePrefs - persisted node configuration (unified for all roles)
*
* Serialized field-by-field, not raw memcpy; struct layout does
* not affect on-disk compatibility.
*/
#pragma once
#include <stdint.h>
#include <string.h>
#define TELEM_MODE_DENY 0
#define TELEM_MODE_ALLOW_FLAGS 1
#define TELEM_MODE_ALLOW_ALL 2
#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2
#define LOOP_DETECT_OFF 0
#define LOOP_DETECT_MINIMAL 1
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
struct NodePrefs {
/* ---- Common fields (both roles) ---- */
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
char password[16];
float freq;
int8_t tx_power_dbm;
uint8_t disable_fwd; // repeater: disable forwarding
uint8_t advert_interval; // stored as minutes / 2
uint8_t flood_advert_interval; // hours
float rx_delay_base;
float tx_delay_factor;
char guest_password[16];
float direct_tx_delay_factor;
float backoff_multiplier; // per-dupe reactive backoff (0.0 = disabled)
uint32_t guard;
uint8_t sf;
uint8_t cr;
uint8_t allow_read_only;
uint8_t multi_acks;
float bw;
uint8_t flood_max;
uint8_t interference_threshold;
uint8_t agc_reset_interval; // stored as secs / 4
// Power saving
uint8_t powersaving_enabled;
// GPS settings
uint8_t gps_enabled;
uint32_t gps_interval; // in seconds
uint8_t advert_loc_policy;
uint32_t discovery_mod_timestamp;
float adc_multiplier;
char owner_info[120];
uint8_t rx_boost; // 1 = boosted RX gain (+3dB), 0 = power save
uint8_t rx_duty_cycle; // 1 = RX duty cycle, 0 = continuous RX
uint8_t apc_enabled; // 1 = APC on, 0 = fixed TX power
uint8_t apc_margin; // APC target link margin dB (6-30)
/* ---- Companion-only fields ---- */
uint8_t manual_add_contacts;
uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env;
uint32_t ble_pin;
uint8_t buzzer_quiet;
uint8_t autoadd_config;
uint8_t client_repeat; // 1 = offgrid mode (forward packets)
uint8_t path_hash_mode; // path mode 0-2
uint8_t autoadd_max_hops; // 0 = no limit, N = up to N-1 hops
uint8_t loop_detect; // LOOP_DETECT_{OFF,MINIMAL,MODERATE,STRICT}
uint8_t leds_disabled; // 1 = LEDs off
char default_scope_name[31]; // companion: default flood scope region name ("" = null)
uint8_t default_scope_key[16]; // companion: default flood scope TransportKey
};
/* Default prefs -- must match LoRaConfig.h defaults for radio interop. */
static inline void initNodePrefs(NodePrefs* prefs) {
memset(prefs, 0, sizeof(NodePrefs));
prefs->airtime_factor = 9.0f; /* Arduino formula: duty% = 100 / (af + 1) → 10% */
prefs->node_lat = 0.0;
prefs->node_lon = 0.0;
#ifdef CONFIG_ZEPHCORE_ADMIN_PASSWORD
strncpy(prefs->password, CONFIG_ZEPHCORE_ADMIN_PASSWORD, sizeof(prefs->password) - 1);
#else
strcpy(prefs->password, "password");
#endif
#ifdef CONFIG_ZEPHCORE_GUEST_PASSWORD
strncpy(prefs->guest_password, CONFIG_ZEPHCORE_GUEST_PASSWORD, sizeof(prefs->guest_password) - 1);
#endif
/* Radio params - MUST match LoRaConfig.h for interop with companion nodes */
prefs->freq = 869.618f; // LoRaConfig::FREQ_HZ / 1000000.0
prefs->bw = 62.5f; // LoRaConfig::BANDWIDTH
prefs->sf = 8; // LoRaConfig::SPREADING_FACTOR
prefs->cr = 8; // CR 4/8 (MeshCore uses 5-8 for CR 4/5 through 4/8)
#ifdef CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM
prefs->tx_power_dbm = CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM;
#else
prefs->tx_power_dbm = 22; // LoRaConfig::TX_POWER_DBM
#endif
prefs->disable_fwd = 0;
prefs->advert_interval = 60; // 2 minutes (value / 2)
prefs->flood_advert_interval = 12; // 12 hours
prefs->rx_delay_base = 0.0f;
prefs->tx_delay_factor = 0.5f;
prefs->direct_tx_delay_factor = 0.3f;
prefs->allow_read_only = 0;
prefs->multi_acks = 0;
prefs->flood_max = 64; // max hops for flood packets (0 = blocking all!)
prefs->interference_threshold = 0;
prefs->agc_reset_interval = 0;
prefs->powersaving_enabled = 0;
prefs->gps_enabled = 0;
prefs->gps_interval = 300; // 5 minutes
prefs->advert_loc_policy = ADVERT_LOC_NONE;
prefs->adc_multiplier = 0.0f;
prefs->rx_boost = 1; // Default to boosted RX for better sensitivity
prefs->rx_duty_cycle = 0; // Default OFF — continuous RX for best reliability
prefs->apc_enabled = 0; // Default OFF — fixed TX power
prefs->apc_margin = 16; // Default 16 dB target link margin
}
/*
* SPDX-License-Identifier: Apache-2.0
* NodePrefs - persisted node configuration (unified for all roles)
*
* Serialized field-by-field, not raw memcpy; struct layout does
* not affect on-disk compatibility.
*/
#pragma once
#include <stdint.h>
#include <string.h>
#define TELEM_MODE_DENY 0
#define TELEM_MODE_ALLOW_FLAGS 1
#define TELEM_MODE_ALLOW_ALL 2
#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2
#define LOOP_DETECT_OFF 0
#define LOOP_DETECT_MINIMAL 1
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
struct NodePrefs {
/* ---- Common fields (both roles) ---- */
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
char password[16];
float freq;
int8_t tx_power_dbm;
uint8_t disable_fwd; // repeater: disable forwarding
uint8_t advert_interval; // stored as minutes / 2
uint8_t flood_advert_interval; // hours
float rx_delay_base;
float tx_delay_factor;
char guest_password[16];
float direct_tx_delay_factor;
float backoff_multiplier; // per-dupe reactive backoff (0.0 = disabled)
uint32_t guard;
uint8_t sf;
uint8_t cr;
uint8_t allow_read_only;
uint8_t multi_acks;
float bw;
uint8_t flood_max;
uint8_t interference_threshold;
uint8_t agc_reset_interval; // stored as secs / 4
// Power saving
uint8_t powersaving_enabled;
// GPS settings
uint8_t gps_enabled;
uint32_t gps_interval; // in seconds
uint8_t advert_loc_policy;
uint32_t discovery_mod_timestamp;
float adc_multiplier;
char owner_info[120];
uint8_t rx_boost; // 1 = boosted RX gain (+3dB), 0 = power save
uint8_t rx_duty_cycle; // 1 = RX duty cycle, 0 = continuous RX
uint8_t apc_enabled; // 1 = APC on, 0 = fixed TX power
uint8_t apc_margin; // APC target link margin dB (6-30)
/* ---- Companion-only fields ---- */
uint8_t manual_add_contacts;
uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env;
uint32_t ble_pin;
uint8_t buzzer_quiet;
uint8_t autoadd_config;
uint8_t client_repeat; // 1 = offgrid mode (forward packets)
uint8_t path_hash_mode; // path mode 0-2
uint8_t autoadd_max_hops; // 0 = no limit, N = up to N-1 hops
uint8_t loop_detect; // LOOP_DETECT_{OFF,MINIMAL,MODERATE,STRICT}
uint8_t leds_disabled; // 1 = LEDs off
char default_scope_name[31]; // companion: default flood scope region name ("" = null)
uint8_t default_scope_key[16]; // companion: default flood scope TransportKey
};
/* Default prefs -- must match LoRaConfig.h defaults for radio interop. */
static inline void initNodePrefs(NodePrefs* prefs) {
memset(prefs, 0, sizeof(NodePrefs));
prefs->airtime_factor = 9.0f; /* Arduino formula: duty% = 100 / (af + 1) → 10% */
prefs->node_lat = 0.0;
prefs->node_lon = 0.0;
#ifdef CONFIG_ZEPHCORE_ADMIN_PASSWORD
strncpy(prefs->password, CONFIG_ZEPHCORE_ADMIN_PASSWORD, sizeof(prefs->password) - 1);
#else
strcpy(prefs->password, "password");
#endif
#ifdef CONFIG_ZEPHCORE_GUEST_PASSWORD
strncpy(prefs->guest_password, CONFIG_ZEPHCORE_GUEST_PASSWORD, sizeof(prefs->guest_password) - 1);
#endif
/* Radio params - MUST match LoRaConfig.h for interop with companion nodes */
prefs->freq = 869.618f; // LoRaConfig::FREQ_HZ / 1000000.0
prefs->bw = 62.5f; // LoRaConfig::BANDWIDTH
prefs->sf = 8; // LoRaConfig::SPREADING_FACTOR
prefs->cr = 8; // CR 4/8 (MeshCore uses 5-8 for CR 4/5 through 4/8)
#ifdef CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM
prefs->tx_power_dbm = CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM;
#else
prefs->tx_power_dbm = 22; // LoRaConfig::TX_POWER_DBM
#endif
prefs->disable_fwd = 0;
prefs->advert_interval = 60; // 2 minutes (value / 2)
prefs->flood_advert_interval = 12; // 12 hours
prefs->rx_delay_base = 0.0f;
prefs->tx_delay_factor = 0.5f;
prefs->direct_tx_delay_factor = 0.3f;
prefs->allow_read_only = 0;
prefs->multi_acks = 0;
prefs->flood_max = 64; // max hops for flood packets (0 = blocking all!)
prefs->interference_threshold = 0;
prefs->agc_reset_interval = 0;
prefs->powersaving_enabled = 0;
prefs->gps_enabled = 0;
prefs->gps_interval = 300; // 5 minutes
prefs->advert_loc_policy = ADVERT_LOC_NONE;
prefs->adc_multiplier = 0.0f;
prefs->rx_boost = 1; // Default to boosted RX for better sensitivity
prefs->rx_duty_cycle = 0; // Default OFF — continuous RX for best reliability
prefs->apc_enabled = 0; // Default OFF — fixed TX power
prefs->apc_margin = 16; // Default 16 dB target link margin
}
+326 -326
View File
@@ -1,326 +1,326 @@
/*
* SPDX-License-Identifier: Apache-2.0
* RegionMap - Region-based flood filtering for repeaters
*/
#include "RegionMap.h"
#include <helpers/TxtDataHelpers.h>
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <stdio.h>
#include <string.h>
LOG_MODULE_REGISTER(zephcore_regions, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL);
static const char* skip_hash(const char* name) {
return *name == '#' ? name + 1 : name;
}
RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
next_id = 1;
num_regions = 0;
default_id = home_id = 0;
wildcard.id = 0;
wildcard.parent = 0;
wildcard.flags = 0; // default behaviour, allow flood and direct
strcpy(wildcard.name, "*");
}
bool RegionMap::is_name_char(uint8_t c) {
// accept all alpha-num or accented characters, but exclude most punctuation chars
return c == '-' || c == '$' || c == '#' || (c >= '0' && c <= '9') || c >= 'A';
}
bool RegionMap::load(const char* path) {
const char* filepath = path;
struct fs_file_t file;
fs_file_t_init(&file);
if (fs_open(&file, filepath, FS_O_READ) < 0) {
LOG_DBG("No regions file at %s", filepath);
return false;
}
uint8_t pad[128];
num_regions = 0;
next_id = 1;
default_id = home_id = 0;
bool success = fs_read(&file, pad, 3) == 3; // reserved header
success = success && fs_read(&file, &default_id, sizeof(default_id)) == sizeof(default_id);
success = success && fs_read(&file, &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && fs_read(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && fs_read(&file, &next_id, sizeof(next_id)) == sizeof(next_id);
if (success) {
while (num_regions < MAX_REGION_ENTRIES) {
auto r = &regions[num_regions];
success = fs_read(&file, &r->id, sizeof(r->id)) == sizeof(r->id);
success = success && fs_read(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && fs_read(&file, r->name, sizeof(r->name)) == sizeof(r->name);
success = success && fs_read(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && fs_read(&file, pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // EOF
if (r->id >= next_id) { // make sure next_id is valid
next_id = r->id + 1;
}
num_regions++;
}
}
fs_close(&file);
LOG_INF("Loaded %d regions from %s", num_regions, filepath);
return true;
}
bool RegionMap::save(const char* path) {
const char* filepath = path;
// Remove old file first
fs_unlink(filepath);
struct fs_file_t file;
fs_file_t_init(&file);
if (fs_open(&file, filepath, FS_O_CREATE | FS_O_WRITE) < 0) {
LOG_ERR("Failed to open %s for write", filepath);
return false;
}
uint8_t pad[128];
memset(pad, 0, sizeof(pad));
bool success = fs_write(&file, pad, 3) == 3; // reserved header
success = success && fs_write(&file, &default_id, sizeof(default_id)) == sizeof(default_id);
success = success && fs_write(&file, &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && fs_write(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && fs_write(&file, &next_id, sizeof(next_id)) == sizeof(next_id);
if (success) {
for (int i = 0; i < num_regions; i++) {
auto r = &regions[i];
success = fs_write(&file, &r->id, sizeof(r->id)) == sizeof(r->id);
success = success && fs_write(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && fs_write(&file, r->name, sizeof(r->name)) == sizeof(r->name);
success = success && fs_write(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && fs_write(&file, pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // write failed
}
}
fs_close(&file);
LOG_INF("Saved %d regions to %s", num_regions, filepath);
return true;
}
RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t id) {
const char* sp = name; // check for illegal name chars
while (*sp) {
if (!is_name_char(*sp)) return nullptr; // error
sp++;
}
auto region = findByName(name);
if (region) {
if (region->id == parent_id) return nullptr; // ERROR: invalid parent!
region->parent = parent_id; // re-parent / move this region in the hierarchy
} else {
if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return nullptr; // full!
region = &regions[num_regions++]; // alloc new RegionEntry
region->flags = REGION_DENY_FLOOD; // DENY by default
region->id = id == 0 ? next_id++ : id;
StrHelper::strncpy(region->name, name, sizeof(region->name));
region->parent = parent_id;
}
return region;
}
int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num) {
int num;
if (src.name[0] == '$') { // private region
num = _store->loadKeysFor(src.id, dest, max_num);
} else if (src.name[0] == '#') { // auto hashtag region
_store->getAutoKeyFor(src.id, src.name, dest[0]);
num = 1;
} else { // new: implicit auto hashtag region
char tmp[sizeof(src.name) + 1];
tmp[0] = '#';
memcpy(&tmp[1], src.name, sizeof(src.name) - 1);
tmp[sizeof(src.name)] = '\0';
_store->getAutoKeyFor(src.id, tmp, dest[0]);
num = 1;
}
return num;
}
RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
TransportKey keys[4];
int num = getTransportKeysFor(*region, keys, 4);
for (int j = 0; j < num; j++) {
uint16_t code = keys[j].calcTransportCode(packet);
if (packet->transport_codes[0] == code) { // a match!!
return region;
}
}
}
}
return nullptr; // no matches
}
RegionEntry* RegionMap::findByName(const char* name) {
if (strcmp(name, "*") == 0) return &wildcard;
if (*name == '#') { name++; } // ignore the '#' when matching by name
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(name, skip_hash(region->name)) == 0) return region;
}
return nullptr; // not found
}
RegionEntry* RegionMap::findByNamePrefix(const char* prefix) {
if (strcmp(prefix, "*") == 0) return &wildcard;
if (*prefix == '#') { prefix++; } // ignore the '#' when matching by name
RegionEntry* partial = nullptr;
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(prefix, skip_hash(region->name)) == 0) return region; // complete match
if (memcmp(prefix, skip_hash(region->name), strlen(prefix)) == 0) {
partial = region;
}
}
return partial;
}
RegionEntry* RegionMap::findById(uint16_t id) {
if (id == 0) return &wildcard; // special root Region
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (region->id == id) return region;
}
return nullptr; // not found
}
RegionEntry* RegionMap::getHomeRegion() {
return findById(home_id);
}
void RegionMap::setHomeRegion(const RegionEntry* home) {
home_id = home ? home->id : 0;
}
RegionEntry* RegionMap::getDefaultRegion() {
return default_id == 0 ? nullptr : findById(default_id);
}
void RegionMap::setDefaultRegion(const RegionEntry* def) {
default_id = def ? def->id : 0;
}
bool RegionMap::removeRegion(const RegionEntry& region) {
if (region.id == 0) return false; // cannot remove wildcard
// first check region has no child regions
for (int i = 0; i < num_regions; i++) {
if (regions[i].parent == region.id) return false; // must remove children first
}
int i = 0;
while (i < num_regions) {
if (region.id == regions[i].id) break;
i++;
}
if (i >= num_regions) return false; // not found
num_regions--; // remove from regions array
while (i < num_regions) {
regions[i] = regions[i + 1];
i++;
}
return true;
}
bool RegionMap::clear() {
num_regions = 0;
return true;
}
void RegionMap::printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const {
// Print indentation
for (int i = 0; i < indent && pos < max_len - 1; i++) {
buf[pos++] = ' ';
}
// Print region info
int written;
if (parent->flags & REGION_DENY_FLOOD) {
written = snprintf(&buf[pos], max_len - pos, "%s%s\n",
skip_hash(parent->name),
parent->id == home_id ? "^" : "");
} else {
written = snprintf(&buf[pos], max_len - pos, "%s%s F\n",
skip_hash(parent->name),
parent->id == home_id ? "^" : "");
}
if (written > 0 && pos + written < max_len) {
pos += written;
}
// Print children recursively
for (int i = 0; i < num_regions; i++) {
auto r = &regions[i];
if (r->parent == parent->id) {
printChildRegions(indent + 1, r, buf, pos, max_len);
}
}
}
size_t RegionMap::exportTo(char* dest, size_t max_len) const {
if (!dest || max_len == 0) return 0;
int pos = 0;
printChildRegions(0, &wildcard, dest, pos, (int)max_len);
return (size_t)pos;
}
int RegionMap::exportNamesTo(char* dest, int max_len, uint8_t mask, bool invert) {
char* dp = dest;
// Check wildcard region
bool wildcard_matches = invert ? (wildcard.flags & mask) : !(wildcard.flags & mask);
if (wildcard_matches) {
*dp++ = '*';
*dp++ = ',';
}
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
// Check if region matches the filter criteria
bool region_matches = invert ? (region->flags & mask) : !(region->flags & mask);
if (region_matches) {
int len = strlen(skip_hash(region->name));
if ((dp - dest) + len + 2 < max_len) { // only append if name will fit
memcpy(dp, skip_hash(region->name), len);
dp += len;
*dp++ = ',';
}
}
}
if (dp > dest) { dp--; } // don't include trailing comma
*dp = 0; // set null terminator
return dp - dest;
}
/*
* SPDX-License-Identifier: Apache-2.0
* RegionMap - Region-based flood filtering for repeaters
*/
#include "RegionMap.h"
#include <helpers/TxtDataHelpers.h>
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <stdio.h>
#include <string.h>
LOG_MODULE_REGISTER(zephcore_regions, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL);
static const char* skip_hash(const char* name) {
return *name == '#' ? name + 1 : name;
}
RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
next_id = 1;
num_regions = 0;
default_id = home_id = 0;
wildcard.id = 0;
wildcard.parent = 0;
wildcard.flags = 0; // default behaviour, allow flood and direct
strcpy(wildcard.name, "*");
}
bool RegionMap::is_name_char(uint8_t c) {
// accept all alpha-num or accented characters, but exclude most punctuation chars
return c == '-' || c == '$' || c == '#' || (c >= '0' && c <= '9') || c >= 'A';
}
bool RegionMap::load(const char* path) {
const char* filepath = path;
struct fs_file_t file;
fs_file_t_init(&file);
if (fs_open(&file, filepath, FS_O_READ) < 0) {
LOG_DBG("No regions file at %s", filepath);
return false;
}
uint8_t pad[128];
num_regions = 0;
next_id = 1;
default_id = home_id = 0;
bool success = fs_read(&file, pad, 3) == 3; // reserved header
success = success && fs_read(&file, &default_id, sizeof(default_id)) == sizeof(default_id);
success = success && fs_read(&file, &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && fs_read(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && fs_read(&file, &next_id, sizeof(next_id)) == sizeof(next_id);
if (success) {
while (num_regions < MAX_REGION_ENTRIES) {
auto r = &regions[num_regions];
success = fs_read(&file, &r->id, sizeof(r->id)) == sizeof(r->id);
success = success && fs_read(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && fs_read(&file, r->name, sizeof(r->name)) == sizeof(r->name);
success = success && fs_read(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && fs_read(&file, pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // EOF
if (r->id >= next_id) { // make sure next_id is valid
next_id = r->id + 1;
}
num_regions++;
}
}
fs_close(&file);
LOG_INF("Loaded %d regions from %s", num_regions, filepath);
return true;
}
bool RegionMap::save(const char* path) {
const char* filepath = path;
// Remove old file first
fs_unlink(filepath);
struct fs_file_t file;
fs_file_t_init(&file);
if (fs_open(&file, filepath, FS_O_CREATE | FS_O_WRITE) < 0) {
LOG_ERR("Failed to open %s for write", filepath);
return false;
}
uint8_t pad[128];
memset(pad, 0, sizeof(pad));
bool success = fs_write(&file, pad, 3) == 3; // reserved header
success = success && fs_write(&file, &default_id, sizeof(default_id)) == sizeof(default_id);
success = success && fs_write(&file, &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && fs_write(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && fs_write(&file, &next_id, sizeof(next_id)) == sizeof(next_id);
if (success) {
for (int i = 0; i < num_regions; i++) {
auto r = &regions[i];
success = fs_write(&file, &r->id, sizeof(r->id)) == sizeof(r->id);
success = success && fs_write(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && fs_write(&file, r->name, sizeof(r->name)) == sizeof(r->name);
success = success && fs_write(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && fs_write(&file, pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // write failed
}
}
fs_close(&file);
LOG_INF("Saved %d regions to %s", num_regions, filepath);
return true;
}
RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t id) {
const char* sp = name; // check for illegal name chars
while (*sp) {
if (!is_name_char(*sp)) return nullptr; // error
sp++;
}
auto region = findByName(name);
if (region) {
if (region->id == parent_id) return nullptr; // ERROR: invalid parent!
region->parent = parent_id; // re-parent / move this region in the hierarchy
} else {
if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return nullptr; // full!
region = &regions[num_regions++]; // alloc new RegionEntry
region->flags = REGION_DENY_FLOOD; // DENY by default
region->id = id == 0 ? next_id++ : id;
StrHelper::strncpy(region->name, name, sizeof(region->name));
region->parent = parent_id;
}
return region;
}
int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num) {
int num;
if (src.name[0] == '$') { // private region
num = _store->loadKeysFor(src.id, dest, max_num);
} else if (src.name[0] == '#') { // auto hashtag region
_store->getAutoKeyFor(src.id, src.name, dest[0]);
num = 1;
} else { // new: implicit auto hashtag region
char tmp[sizeof(src.name) + 1];
tmp[0] = '#';
memcpy(&tmp[1], src.name, sizeof(src.name) - 1);
tmp[sizeof(src.name)] = '\0';
_store->getAutoKeyFor(src.id, tmp, dest[0]);
num = 1;
}
return num;
}
RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
TransportKey keys[4];
int num = getTransportKeysFor(*region, keys, 4);
for (int j = 0; j < num; j++) {
uint16_t code = keys[j].calcTransportCode(packet);
if (packet->transport_codes[0] == code) { // a match!!
return region;
}
}
}
}
return nullptr; // no matches
}
RegionEntry* RegionMap::findByName(const char* name) {
if (strcmp(name, "*") == 0) return &wildcard;
if (*name == '#') { name++; } // ignore the '#' when matching by name
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(name, skip_hash(region->name)) == 0) return region;
}
return nullptr; // not found
}
RegionEntry* RegionMap::findByNamePrefix(const char* prefix) {
if (strcmp(prefix, "*") == 0) return &wildcard;
if (*prefix == '#') { prefix++; } // ignore the '#' when matching by name
RegionEntry* partial = nullptr;
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(prefix, skip_hash(region->name)) == 0) return region; // complete match
if (memcmp(prefix, skip_hash(region->name), strlen(prefix)) == 0) {
partial = region;
}
}
return partial;
}
RegionEntry* RegionMap::findById(uint16_t id) {
if (id == 0) return &wildcard; // special root Region
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (region->id == id) return region;
}
return nullptr; // not found
}
RegionEntry* RegionMap::getHomeRegion() {
return findById(home_id);
}
void RegionMap::setHomeRegion(const RegionEntry* home) {
home_id = home ? home->id : 0;
}
RegionEntry* RegionMap::getDefaultRegion() {
return default_id == 0 ? nullptr : findById(default_id);
}
void RegionMap::setDefaultRegion(const RegionEntry* def) {
default_id = def ? def->id : 0;
}
bool RegionMap::removeRegion(const RegionEntry& region) {
if (region.id == 0) return false; // cannot remove wildcard
// first check region has no child regions
for (int i = 0; i < num_regions; i++) {
if (regions[i].parent == region.id) return false; // must remove children first
}
int i = 0;
while (i < num_regions) {
if (region.id == regions[i].id) break;
i++;
}
if (i >= num_regions) return false; // not found
num_regions--; // remove from regions array
while (i < num_regions) {
regions[i] = regions[i + 1];
i++;
}
return true;
}
bool RegionMap::clear() {
num_regions = 0;
return true;
}
void RegionMap::printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const {
// Print indentation
for (int i = 0; i < indent && pos < max_len - 1; i++) {
buf[pos++] = ' ';
}
// Print region info
int written;
if (parent->flags & REGION_DENY_FLOOD) {
written = snprintf(&buf[pos], max_len - pos, "%s%s\n",
skip_hash(parent->name),
parent->id == home_id ? "^" : "");
} else {
written = snprintf(&buf[pos], max_len - pos, "%s%s F\n",
skip_hash(parent->name),
parent->id == home_id ? "^" : "");
}
if (written > 0 && pos + written < max_len) {
pos += written;
}
// Print children recursively
for (int i = 0; i < num_regions; i++) {
auto r = &regions[i];
if (r->parent == parent->id) {
printChildRegions(indent + 1, r, buf, pos, max_len);
}
}
}
size_t RegionMap::exportTo(char* dest, size_t max_len) const {
if (!dest || max_len == 0) return 0;
int pos = 0;
printChildRegions(0, &wildcard, dest, pos, (int)max_len);
return (size_t)pos;
}
int RegionMap::exportNamesTo(char* dest, int max_len, uint8_t mask, bool invert) {
char* dp = dest;
// Check wildcard region
bool wildcard_matches = invert ? (wildcard.flags & mask) : !(wildcard.flags & mask);
if (wildcard_matches) {
*dp++ = '*';
*dp++ = ',';
}
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
// Check if region matches the filter criteria
bool region_matches = invert ? (region->flags & mask) : !(region->flags & mask);
if (region_matches) {
int len = strlen(skip_hash(region->name));
if ((dp - dest) + len + 2 < max_len) { // only append if name will fit
memcpy(dp, skip_hash(region->name), len);
dp += len;
*dp++ = ',';
}
}
}
if (dp > dest) { dp--; } // don't include trailing comma
*dp = 0; // set null terminator
return dp - dest;
}
+72 -72
View File
@@ -1,72 +1,72 @@
/*
* SPDX-License-Identifier: Apache-2.0
* RegionMap - Region-based flood filtering for repeaters
*/
#pragma once
#include <mesh/Packet.h>
#include "TransportKeyStore.h"
#include <cstdint>
#include <string.h>
#ifndef MAX_REGION_ENTRIES
#ifdef CONFIG_ZEPHCORE_MAX_REGION_ENTRIES
#define MAX_REGION_ENTRIES CONFIG_ZEPHCORE_MAX_REGION_ENTRIES
#else
#define MAX_REGION_ENTRIES 32
#endif
#endif
#define REGION_DENY_FLOOD 0x01
#define REGION_DENY_DIRECT 0x02 // reserved for future
struct RegionEntry {
uint16_t id;
uint16_t parent;
uint8_t flags;
char name[31];
bool isWildcard() const { return id == 0; }
};
class RegionMap {
TransportKeyStore* _store;
uint16_t next_id;
uint16_t home_id;
uint16_t default_id;
uint16_t num_regions;
RegionEntry regions[MAX_REGION_ENTRIES];
RegionEntry wildcard;
void printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const;
public:
RegionMap(TransportKeyStore& store);
static bool is_name_char(uint8_t c);
bool load(const char* path = nullptr);
bool save(const char* path = nullptr);
RegionEntry* putRegion(const char* name, uint16_t parent_id, uint16_t id = 0);
RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask);
RegionEntry& getWildcard() { return wildcard; }
RegionEntry* findByName(const char* name);
RegionEntry* findByNamePrefix(const char* prefix);
RegionEntry* findById(uint16_t id);
RegionEntry* getHomeRegion(); // NOTE: can be NULL
void setHomeRegion(const RegionEntry* home);
RegionEntry* getDefaultRegion(); // NOTE: can be NULL
void setDefaultRegion(const RegionEntry* def);
bool removeRegion(const RegionEntry& region);
bool clear();
void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; }
int getCount() const { return num_regions; }
const RegionEntry* getByIdx(int i) const { return &regions[i]; }
const RegionEntry* getRoot() const { return &wildcard; }
int exportNamesTo(char* dest, int max_len, uint8_t mask, bool invert = false);
int getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num);
size_t exportTo(char* dest, size_t max_len) const;
};
/*
* SPDX-License-Identifier: Apache-2.0
* RegionMap - Region-based flood filtering for repeaters
*/
#pragma once
#include <mesh/Packet.h>
#include "TransportKeyStore.h"
#include <cstdint>
#include <string.h>
#ifndef MAX_REGION_ENTRIES
#ifdef CONFIG_ZEPHCORE_MAX_REGION_ENTRIES
#define MAX_REGION_ENTRIES CONFIG_ZEPHCORE_MAX_REGION_ENTRIES
#else
#define MAX_REGION_ENTRIES 32
#endif
#endif
#define REGION_DENY_FLOOD 0x01
#define REGION_DENY_DIRECT 0x02 // reserved for future
struct RegionEntry {
uint16_t id;
uint16_t parent;
uint8_t flags;
char name[31];
bool isWildcard() const { return id == 0; }
};
class RegionMap {
TransportKeyStore* _store;
uint16_t next_id;
uint16_t home_id;
uint16_t default_id;
uint16_t num_regions;
RegionEntry regions[MAX_REGION_ENTRIES];
RegionEntry wildcard;
void printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const;
public:
RegionMap(TransportKeyStore& store);
static bool is_name_char(uint8_t c);
bool load(const char* path = nullptr);
bool save(const char* path = nullptr);
RegionEntry* putRegion(const char* name, uint16_t parent_id, uint16_t id = 0);
RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask);
RegionEntry& getWildcard() { return wildcard; }
RegionEntry* findByName(const char* name);
RegionEntry* findByNamePrefix(const char* prefix);
RegionEntry* findById(uint16_t id);
RegionEntry* getHomeRegion(); // NOTE: can be NULL
void setHomeRegion(const RegionEntry* home);
RegionEntry* getDefaultRegion(); // NOTE: can be NULL
void setDefaultRegion(const RegionEntry* def);
bool removeRegion(const RegionEntry& region);
bool clear();
void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; }
int getCount() const { return num_regions; }
const RegionEntry* getByIdx(int i) const { return &regions[i]; }
const RegionEntry* getRoot() const { return &wildcard; }
int exportNamesTo(char* dest, int max_len, uint8_t mask, bool invert = false);
int getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num);
size_t exportTo(char* dest, size_t max_len) const;
};
File diff suppressed because it is too large Load Diff
+157 -157
View File
@@ -1,157 +1,157 @@
/*
* ZephCore - Display Abstraction (CFB)
* Copyright (c) 2025 ZephCore
* SPDX-License-Identifier: Apache-2.0
*
* Wraps Zephyr's Character Framebuffer (CFB) subsystem with:
* - Auto-detection from devicetree (any Zephyr-supported display)
* - Runtime resolution query (supports any size, not just 128x64)
* - Auto-off timer via k_work_delayable
* - Simple text/rect drawing API for UI pages
*
* All functions prefixed mc_display_ to avoid collision with
* Zephyr's display_* namespace in <zephyr/drivers/display.h>.
*/
#ifndef ZEPHCORE_DISPLAY_H
#define ZEPHCORE_DISPLAY_H
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Initialize the display from devicetree.
* Detects any Zephyr-supported display via:
* 1. "zephyr,display" chosen node (standard)
* 2. Legacy nodelabels: sh1106, ssd1306 (backwards compat)
*
* Queries actual resolution from driver — no hardcoded dimensions.
*
* @return 0 on success, negative errno on failure, -ENODEV if no display
*/
int mc_display_init(void);
/**
* Get display width in pixels (queried from hardware at init).
* Returns 0 if display not initialized.
*/
uint16_t mc_display_width(void);
/**
* Get display height in pixels (queried from hardware at init).
* Returns 0 if display not initialized.
*/
uint16_t mc_display_height(void);
/**
* Get active font width in pixels.
* Returns 0 if display not initialized.
*/
uint8_t mc_display_font_width(void);
/**
* Get active font height in pixels.
* Returns 0 if display not initialized.
*/
uint8_t mc_display_font_height(void);
/**
* Turn display on (wake from blanking).
* Resets the auto-off timer.
*/
void mc_display_on(void);
/**
* Turn display off (blanking).
*/
void mc_display_off(void);
/**
* @return true if the display is currently on
*/
bool mc_display_is_on(void);
/**
* @return true if the display is an e-paper (EPD) type.
* EPD displays have slow refresh (~2s) and use zero power when static,
* so callers should use longer update intervals and skip blanking.
*/
bool mc_display_is_epd(void);
/**
* Clear the framebuffer (fill with black).
* Call before rendering a new frame.
*/
void mc_display_clear(void);
/**
* Draw text at position.
*
* @param x X position in pixels
* @param y Y position in pixels
* @param text Null-terminated string
* @param invert If true, draw black text on white background
*/
void mc_display_text(int x, int y, const char *text, bool invert);
/**
* Draw a filled rectangle.
*
* @param x Top-left X
* @param y Top-left Y
* @param w Width
* @param h Height
*/
void mc_display_fill_rect(int x, int y, int w, int h);
/**
* Draw a horizontal line.
*/
void mc_display_hline(int x, int y, int w);
/**
* Draw a monochrome bitmap (Adafruit/Arduino format).
* MSB first, row-major, 1=foreground.
* Compatible with Arduino's drawBitmap() and MeshCore icons.h data.
*
* @param x Top-left X position
* @param y Top-left Y position
* @param data Bitmap data (MSB first, row-major)
* @param w Width in pixels
* @param h Height in pixels
*/
void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h);
/**
* Flush the framebuffer to the display hardware.
* Call after all drawing operations for a frame are complete.
*/
void mc_display_finalize(void);
/**
* Reset the auto-off timer (called on user interaction).
*/
void mc_display_reset_auto_off(void);
/**
* EPD-only: force a full panel reset cycle before normal page rendering.
* No-op on non-EPD displays or when display is not initialized.
*/
void mc_display_epd_full_reset(void);
/**
* Get the raw display device pointer.
* Used by easter egg (Doom) to bypass CFB and write directly.
* Returns NULL if display not initialized.
*/
const struct device *mc_display_get_device(void);
#ifdef __cplusplus
}
#endif
#endif /* ZEPHCORE_DISPLAY_H */
/*
* ZephCore - Display Abstraction (CFB)
* Copyright (c) 2025 ZephCore
* SPDX-License-Identifier: Apache-2.0
*
* Wraps Zephyr's Character Framebuffer (CFB) subsystem with:
* - Auto-detection from devicetree (any Zephyr-supported display)
* - Runtime resolution query (supports any size, not just 128x64)
* - Auto-off timer via k_work_delayable
* - Simple text/rect drawing API for UI pages
*
* All functions prefixed mc_display_ to avoid collision with
* Zephyr's display_* namespace in <zephyr/drivers/display.h>.
*/
#ifndef ZEPHCORE_DISPLAY_H
#define ZEPHCORE_DISPLAY_H
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Initialize the display from devicetree.
* Detects any Zephyr-supported display via:
* 1. "zephyr,display" chosen node (standard)
* 2. Legacy nodelabels: sh1106, ssd1306 (backwards compat)
*
* Queries actual resolution from driver — no hardcoded dimensions.
*
* @return 0 on success, negative errno on failure, -ENODEV if no display
*/
int mc_display_init(void);
/**
* Get display width in pixels (queried from hardware at init).
* Returns 0 if display not initialized.
*/
uint16_t mc_display_width(void);
/**
* Get display height in pixels (queried from hardware at init).
* Returns 0 if display not initialized.
*/
uint16_t mc_display_height(void);
/**
* Get active font width in pixels.
* Returns 0 if display not initialized.
*/
uint8_t mc_display_font_width(void);
/**
* Get active font height in pixels.
* Returns 0 if display not initialized.
*/
uint8_t mc_display_font_height(void);
/**
* Turn display on (wake from blanking).
* Resets the auto-off timer.
*/
void mc_display_on(void);
/**
* Turn display off (blanking).
*/
void mc_display_off(void);
/**
* @return true if the display is currently on
*/
bool mc_display_is_on(void);
/**
* @return true if the display is an e-paper (EPD) type.
* EPD displays have slow refresh (~2s) and use zero power when static,
* so callers should use longer update intervals and skip blanking.
*/
bool mc_display_is_epd(void);
/**
* Clear the framebuffer (fill with black).
* Call before rendering a new frame.
*/
void mc_display_clear(void);
/**
* Draw text at position.
*
* @param x X position in pixels
* @param y Y position in pixels
* @param text Null-terminated string
* @param invert If true, draw black text on white background
*/
void mc_display_text(int x, int y, const char *text, bool invert);
/**
* Draw a filled rectangle.
*
* @param x Top-left X
* @param y Top-left Y
* @param w Width
* @param h Height
*/
void mc_display_fill_rect(int x, int y, int w, int h);
/**
* Draw a horizontal line.
*/
void mc_display_hline(int x, int y, int w);
/**
* Draw a monochrome bitmap (Adafruit/Arduino format).
* MSB first, row-major, 1=foreground.
* Compatible with Arduino's drawBitmap() and MeshCore icons.h data.
*
* @param x Top-left X position
* @param y Top-left Y position
* @param data Bitmap data (MSB first, row-major)
* @param w Width in pixels
* @param h Height in pixels
*/
void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h);
/**
* Flush the framebuffer to the display hardware.
* Call after all drawing operations for a frame are complete.
*/
void mc_display_finalize(void);
/**
* Reset the auto-off timer (called on user interaction).
*/
void mc_display_reset_auto_off(void);
/**
* EPD-only: force a full panel reset cycle before normal page rendering.
* No-op on non-EPD displays or when display is not initialized.
*/
void mc_display_epd_full_reset(void);
/**
* Get the raw display device pointer.
* Used by easter egg (Doom) to bypass CFB and write directly.
* Returns NULL if display not initialized.
*/
const struct device *mc_display_get_device(void);
#ifdef __cplusplus
}
#endif
#endif /* ZEPHCORE_DISPLAY_H */
+1226 -1226
View File
File diff suppressed because it is too large Load Diff
+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 */
@@ -1,116 +1,116 @@
/*
* SPDX-License-Identifier: Apache-2.0
* SX126x native driver extension API
*
* Functions extending the standard Zephyr lora_driver_api with
* SX126x-specific features (duty cycle, RX boost, RSSI readout,
* preamble detection).
*/
#ifndef SX126X_EXT_H
#define SX126X_EXT_H
#include <zephyr/device.h>
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Get instantaneous RSSI (for noise floor calibration)
*
* Reads the current RSSI from the radio while in RX mode.
* Uses non-blocking mutex returns -128 if SPI is busy.
*
* @param dev LoRa device
* @return RSSI in dBm, or -128 on error
*/
int16_t sx126x_get_rssi_inst(const struct device *dev);
/**
* @brief Check if radio is actively receiving a packet
*
* Checks IRQ status for preamble/header detection.
* Uses non-blocking mutex returns false if SPI is busy.
*
* @param dev LoRa device
* @return true if preamble or header detected
*/
bool sx126x_is_receiving(const struct device *dev);
/**
* @brief Enable/disable RX boosted mode
*
* Boosted mode increases LNA gain for +3dB sensitivity at +2mA cost.
*
* @param dev LoRa device
* @param enable true to enable boost
*/
void sx126x_set_rx_boost(const struct device *dev, bool enable);
/**
* @brief Check if the radio chip is busy (cannot accept SPI commands)
*
* Reads the BUSY GPIO pin directly no SPI, no blocking.
* Returns true when the chip is in its duty-cycle sleep phase.
* Safe to call at any time.
*
* @param dev LoRa device
* @return true if BUSY pin is high (chip sleeping / processing)
*/
bool sx126x_is_chip_busy(const struct device *dev);
/**
* @brief Apply undocumented register 0x8B5 RX improvement for Heltec V4
*
* Sets the LSB of register 0x8B5 which consistently improves RX reception
* on boards with GC1109 or KCT8103L PA (Heltec V4/V4.3). Described by
* Heltec engineer @Quency-D in MeshCore PR#1398.
*
* Must be called after the first lora_config() completes.
*
* @param dev LoRa device
*/
void sx126x_apply_heltec_reg_patch(const struct device *dev);
/**
* @brief Reset AGC by performing warm sleep + full recalibration
*
* Warm sleep powers down the analog frontend (resets AGC gain state),
* then Calibrate(0x7F) refreshes all blocks (ADC, PLL, image, oscillators).
* Re-applies DIO2 RF switch, RX boosted gain, and image calibration afterward.
*
* Must be called while NOT actively receiving a packet.
*
* @param dev LoRa device
*/
void sx126x_reset_agc(const struct device *dev);
/**
* @brief Get duty-cycle preamble false-positive counter
*
* Returns the number of times duty-cycle RX tripped IRQ_RX_TX_TIMEOUT
* and was silently re-armed. High values mean the preamble detector is
* firing on noise/neighbour interference without a real packet arriving,
* which inflates RX-on time beyond the nominal duty cycle and shortens
* battery life.
*
* @param dev LoRa device
* @return Cumulative re-arm count since last reset
*/
uint32_t sx126x_get_dc_timeout_restarts(const struct device *dev);
/**
* @brief Reset the duty-cycle preamble false-positive counter to zero.
*
* @param dev LoRa device
*/
void sx126x_reset_dc_timeout_restarts(const struct device *dev);
#ifdef __cplusplus
}
#endif
#endif /* SX126X_EXT_H */
/*
* SPDX-License-Identifier: Apache-2.0
* SX126x native driver extension API
*
* Functions extending the standard Zephyr lora_driver_api with
* SX126x-specific features (duty cycle, RX boost, RSSI readout,
* preamble detection).
*/
#ifndef SX126X_EXT_H
#define SX126X_EXT_H
#include <zephyr/device.h>
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Get instantaneous RSSI (for noise floor calibration)
*
* Reads the current RSSI from the radio while in RX mode.
* Uses non-blocking mutex returns -128 if SPI is busy.
*
* @param dev LoRa device
* @return RSSI in dBm, or -128 on error
*/
int16_t sx126x_get_rssi_inst(const struct device *dev);
/**
* @brief Check if radio is actively receiving a packet
*
* Checks IRQ status for preamble/header detection.
* Uses non-blocking mutex returns false if SPI is busy.
*
* @param dev LoRa device
* @return true if preamble or header detected
*/
bool sx126x_is_receiving(const struct device *dev);
/**
* @brief Enable/disable RX boosted mode
*
* Boosted mode increases LNA gain for +3dB sensitivity at +2mA cost.
*
* @param dev LoRa device
* @param enable true to enable boost
*/
void sx126x_set_rx_boost(const struct device *dev, bool enable);
/**
* @brief Check if the radio chip is busy (cannot accept SPI commands)
*
* Reads the BUSY GPIO pin directly no SPI, no blocking.
* Returns true when the chip is in its duty-cycle sleep phase.
* Safe to call at any time.
*
* @param dev LoRa device
* @return true if BUSY pin is high (chip sleeping / processing)
*/
bool sx126x_is_chip_busy(const struct device *dev);
/**
* @brief Apply undocumented register 0x8B5 RX improvement for Heltec V4
*
* Sets the LSB of register 0x8B5 which consistently improves RX reception
* on boards with GC1109 or KCT8103L PA (Heltec V4/V4.3). Described by
* Heltec engineer @Quency-D in MeshCore PR#1398.
*
* Must be called after the first lora_config() completes.
*
* @param dev LoRa device
*/
void sx126x_apply_heltec_reg_patch(const struct device *dev);
/**
* @brief Reset AGC by performing warm sleep + full recalibration
*
* Warm sleep powers down the analog frontend (resets AGC gain state),
* then Calibrate(0x7F) refreshes all blocks (ADC, PLL, image, oscillators).
* Re-applies DIO2 RF switch, RX boosted gain, and image calibration afterward.
*
* Must be called while NOT actively receiving a packet.
*
* @param dev LoRa device
*/
void sx126x_reset_agc(const struct device *dev);
/**
* @brief Get duty-cycle preamble false-positive counter
*
* Returns the number of times duty-cycle RX tripped IRQ_RX_TX_TIMEOUT
* and was silently re-armed. High values mean the preamble detector is
* firing on noise/neighbour interference without a real packet arriving,
* which inflates RX-on time beyond the nominal duty cycle and shortens
* battery life.
*
* @param dev LoRa device
* @return Cumulative re-arm count since last reset
*/
uint32_t sx126x_get_dc_timeout_restarts(const struct device *dev);
/**
* @brief Reset the duty-cycle preamble false-positive counter to zero.
*
* @param dev LoRa device
*/
void sx126x_reset_dc_timeout_restarts(const struct device *dev);
#ifdef __cplusplus
}
#endif
#endif /* SX126X_EXT_H */
+166 -166
View File
@@ -1,166 +1,166 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Adaptive Contention Window dupe-counting based delay estimation
*/
#include <mesh/ContentionTracker.h>
#include <mesh/Packet.h>
#include <string.h>
namespace mesh {
ContentionTracker::ContentionTracker()
: _next_idx(0), _ema_x256(0), _finalized_count(0),
_last_retransmit_ms(0), _backoff_multiplier(DEFAULT_BACKOFF_MULT)
{
memset(_ring, 0, sizeof(_ring));
}
/* FNV-1a over payload_type + first 8 payload bytes */
uint32_t ContentionTracker::computePacketHash32(const Packet *pkt)
{
uint32_t h = 0x811c9dc5u; /* FNV-1a offset basis */
uint8_t t = pkt->getPayloadType();
h = (h ^ t) * 0x01000193u;
int n = pkt->payload_len < 8 ? pkt->payload_len : 8;
for (int i = 0; i < n; i++) {
h = (h ^ pkt->payload[i]) * 0x01000193u;
}
return h;
}
int ContentionTracker::findEntry(uint32_t hash32) const
{
for (int i = 0; i < RING_SIZE; i++) {
if (_ring[i].active && _ring[i].hash32 == hash32) {
return i;
}
}
return -1;
}
void ContentionTracker::finalizeEntry(int idx)
{
if (!_ring[idx].active) return;
uint32_t sample_x256 = (uint32_t)_ring[idx].dupe_count << 8;
int32_t diff = (int32_t)sample_x256 - (int32_t)_ema_x256;
if (_finalized_count < WARMUP_PACKETS) {
/* Warmup: seed EMA with fast convergence */
if (_finalized_count == 0) {
_ema_x256 = sample_x256;
} else {
_ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> 1));
}
} else {
_ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> EMA_SHIFT));
}
_finalized_count++;
_ring[idx].active = false;
}
void ContentionTracker::trackRetransmit(uint32_t hash32, uint32_t now_ms)
{
_last_retransmit_ms = now_ms;
/* Evict oldest if ring slot occupied */
if (_ring[_next_idx].active) {
finalizeEntry(_next_idx);
}
Entry &e = _ring[_next_idx];
e.hash32 = hash32;
e.first_seen_ms = now_ms;
e.dupe_count = 0;
e.reactive_added_ms = 0;
e.active = true;
_next_idx = (_next_idx + 1) % RING_SIZE;
}
bool ContentionTracker::recordDupeIfTracked(uint32_t hash32, uint32_t now_ms)
{
int idx = findEntry(hash32);
if (idx < 0) return false;
Entry &e = _ring[idx];
if (now_ms - e.first_seen_ms > WINDOW_MS) {
finalizeEntry(idx);
return false;
}
if (e.dupe_count < 255) {
e.dupe_count++;
}
return true;
}
uint16_t ContentionTracker::getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const
{
int idx = findEntry(hash32);
if (idx < 0) return 0;
uint32_t per_dupe = (uint32_t)(_backoff_multiplier * (float)airtime_ms);
if (per_dupe == 0) return 0;
/* Effective cap: ~12 relay-slots (airtime-scaled), absolute ceiling REACTIVE_HARD_CAP_MS */
uint32_t effective_cap = 12 * airtime_ms;
if (effective_cap > REACTIVE_HARD_CAP_MS) effective_cap = REACTIVE_HARD_CAP_MS;
if (_ring[idx].reactive_added_ms >= effective_cap) return 0;
uint32_t remaining = effective_cap - _ring[idx].reactive_added_ms;
if (per_dupe > remaining) per_dupe = remaining;
return per_dupe > 0xFFFF ? 0xFFFF : (uint16_t)per_dupe;
}
void ContentionTracker::addReactiveExtension(uint32_t hash32, uint16_t added_ms)
{
int idx = findEntry(hash32);
if (idx < 0) return;
uint32_t total = (uint32_t)_ring[idx].reactive_added_ms + added_ms;
_ring[idx].reactive_added_ms = total > 0xFFFF ? 0xFFFF : (uint16_t)total;
}
void ContentionTracker::tick(uint32_t now_ms)
{
for (int i = 0; i < RING_SIZE; i++) {
if (_ring[i].active && now_ms - _ring[i].first_seen_ms > WINDOW_MS) {
finalizeEntry(i);
}
}
/* Decay EMA toward 0 if no retransmit in STALE_MS */
if (_last_retransmit_ms != 0 && now_ms - _last_retransmit_ms > STALE_MS) {
if (_ema_x256 > 0) {
_ema_x256 -= _ema_x256 >> EMA_SHIFT;
}
}
}
float ContentionTracker::getContentionEstimate() const
{
return (float)_ema_x256 / 256.0f;
}
float ContentionTracker::getFloodDelayFactor() const
{
if (!isWarmedUp()) return 0.5f; /* conservative default before warmup */
float est = getContentionEstimate();
if (est <= 0.0f) return MIN_FLOOD_FACTOR;
/* Arduino-like center near 0.5 in light contention, rising smoothly
* toward 0.8 as contention increases. */
float factor = MIN_FLOOD_FACTOR + (MAX_FLOOD_FACTOR - MIN_FLOOD_FACTOR) *
(est / (est + FLOOD_EST_HALFPOINT));
if (factor > MAX_FLOOD_FACTOR) factor = MAX_FLOOD_FACTOR;
return factor;
}
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* Adaptive Contention Window dupe-counting based delay estimation
*/
#include <mesh/ContentionTracker.h>
#include <mesh/Packet.h>
#include <string.h>
namespace mesh {
ContentionTracker::ContentionTracker()
: _next_idx(0), _ema_x256(0), _finalized_count(0),
_last_retransmit_ms(0), _backoff_multiplier(DEFAULT_BACKOFF_MULT)
{
memset(_ring, 0, sizeof(_ring));
}
/* FNV-1a over payload_type + first 8 payload bytes */
uint32_t ContentionTracker::computePacketHash32(const Packet *pkt)
{
uint32_t h = 0x811c9dc5u; /* FNV-1a offset basis */
uint8_t t = pkt->getPayloadType();
h = (h ^ t) * 0x01000193u;
int n = pkt->payload_len < 8 ? pkt->payload_len : 8;
for (int i = 0; i < n; i++) {
h = (h ^ pkt->payload[i]) * 0x01000193u;
}
return h;
}
int ContentionTracker::findEntry(uint32_t hash32) const
{
for (int i = 0; i < RING_SIZE; i++) {
if (_ring[i].active && _ring[i].hash32 == hash32) {
return i;
}
}
return -1;
}
void ContentionTracker::finalizeEntry(int idx)
{
if (!_ring[idx].active) return;
uint32_t sample_x256 = (uint32_t)_ring[idx].dupe_count << 8;
int32_t diff = (int32_t)sample_x256 - (int32_t)_ema_x256;
if (_finalized_count < WARMUP_PACKETS) {
/* Warmup: seed EMA with fast convergence */
if (_finalized_count == 0) {
_ema_x256 = sample_x256;
} else {
_ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> 1));
}
} else {
_ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> EMA_SHIFT));
}
_finalized_count++;
_ring[idx].active = false;
}
void ContentionTracker::trackRetransmit(uint32_t hash32, uint32_t now_ms)
{
_last_retransmit_ms = now_ms;
/* Evict oldest if ring slot occupied */
if (_ring[_next_idx].active) {
finalizeEntry(_next_idx);
}
Entry &e = _ring[_next_idx];
e.hash32 = hash32;
e.first_seen_ms = now_ms;
e.dupe_count = 0;
e.reactive_added_ms = 0;
e.active = true;
_next_idx = (_next_idx + 1) % RING_SIZE;
}
bool ContentionTracker::recordDupeIfTracked(uint32_t hash32, uint32_t now_ms)
{
int idx = findEntry(hash32);
if (idx < 0) return false;
Entry &e = _ring[idx];
if (now_ms - e.first_seen_ms > WINDOW_MS) {
finalizeEntry(idx);
return false;
}
if (e.dupe_count < 255) {
e.dupe_count++;
}
return true;
}
uint16_t ContentionTracker::getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const
{
int idx = findEntry(hash32);
if (idx < 0) return 0;
uint32_t per_dupe = (uint32_t)(_backoff_multiplier * (float)airtime_ms);
if (per_dupe == 0) return 0;
/* Effective cap: ~12 relay-slots (airtime-scaled), absolute ceiling REACTIVE_HARD_CAP_MS */
uint32_t effective_cap = 12 * airtime_ms;
if (effective_cap > REACTIVE_HARD_CAP_MS) effective_cap = REACTIVE_HARD_CAP_MS;
if (_ring[idx].reactive_added_ms >= effective_cap) return 0;
uint32_t remaining = effective_cap - _ring[idx].reactive_added_ms;
if (per_dupe > remaining) per_dupe = remaining;
return per_dupe > 0xFFFF ? 0xFFFF : (uint16_t)per_dupe;
}
void ContentionTracker::addReactiveExtension(uint32_t hash32, uint16_t added_ms)
{
int idx = findEntry(hash32);
if (idx < 0) return;
uint32_t total = (uint32_t)_ring[idx].reactive_added_ms + added_ms;
_ring[idx].reactive_added_ms = total > 0xFFFF ? 0xFFFF : (uint16_t)total;
}
void ContentionTracker::tick(uint32_t now_ms)
{
for (int i = 0; i < RING_SIZE; i++) {
if (_ring[i].active && now_ms - _ring[i].first_seen_ms > WINDOW_MS) {
finalizeEntry(i);
}
}
/* Decay EMA toward 0 if no retransmit in STALE_MS */
if (_last_retransmit_ms != 0 && now_ms - _last_retransmit_ms > STALE_MS) {
if (_ema_x256 > 0) {
_ema_x256 -= _ema_x256 >> EMA_SHIFT;
}
}
}
float ContentionTracker::getContentionEstimate() const
{
return (float)_ema_x256 / 256.0f;
}
float ContentionTracker::getFloodDelayFactor() const
{
if (!isWarmedUp()) return 0.5f; /* conservative default before warmup */
float est = getContentionEstimate();
if (est <= 0.0f) return MIN_FLOOD_FACTOR;
/* Arduino-like center near 0.5 in light contention, rising smoothly
* toward 0.8 as contention increases. */
float factor = MIN_FLOOD_FACTOR + (MAX_FLOOD_FACTOR - MIN_FLOOD_FACTOR) *
(est / (est + FLOOD_EST_HALFPOINT));
if (factor > MAX_FLOOD_FACTOR) factor = MAX_FLOOD_FACTOR;
return factor;
}
} /* namespace mesh */
+732 -732
View File
File diff suppressed because it is too large Load Diff
+106 -106
View File
@@ -1,106 +1,106 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Packet implementation
*/
#include <mesh/Packet.h>
#include <mesh/Utils.h>
#include <string.h>
namespace mesh {
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;
}
size_t Packet::writePath(uint8_t *dest, const uint8_t *src, 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; // Error
}
memcpy(dest, src, len);
return len;
}
uint8_t Packet::copyPath(uint8_t *dest, const uint8_t *src, uint8_t path_len)
{
size_t written = writePath(dest, src, path_len);
return written > 0 ? path_len : 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);
}
}
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;
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;
}
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Packet implementation
*/
#include <mesh/Packet.h>
#include <mesh/Utils.h>
#include <string.h>
namespace mesh {
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;
}
size_t Packet::writePath(uint8_t *dest, const uint8_t *src, 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; // Error
}
memcpy(dest, src, len);
return len;
}
uint8_t Packet::copyPath(uint8_t *dest, const uint8_t *src, uint8_t path_len)
{
size_t written = writePath(dest, src, path_len);
return written > 0 ? path_len : 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);
}
}
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;
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;
}
} /* namespace mesh */
+278 -278
View File
@@ -1,278 +1,278 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Adaptive Power Control echo-based TX power reduction
*/
#include <mesh/PowerController.h>
#include <mesh/Packet.h>
#include <string.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_apc, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
/* SNR thresholds per SF (x4 fixed point, matching radio_common.h) */
static constexpr int8_t snr_threshold_x4[] = {
-30, /* SF7: -7.5 dB */
-40, /* SF8: -10.0 dB */
-50, /* SF9: -12.5 dB */
-60, /* SF10: -15.0 dB */
-70, /* SF11: -17.5 dB */
-80, /* SF12: -20.0 dB */
};
namespace mesh {
PowerController::PowerController()
: _next_idx(0), _margin_ema_x256(0), _finalized_count(0),
_last_echo_ms(0), _power_reduction_db(0), _enabled(true),
_sf(8), _last_source_count(0), _target_margin_x4(DEFAULT_TARGET_MARGIN_X4)
{
memset(_ring, 0, sizeof(_ring));
}
void PowerController::setEnabled(bool en)
{
if (_enabled == en) return;
_enabled = en;
if (!_enabled) {
/* Drop APC runtime state while disabled so no tracking work runs. */
memset(_ring, 0, sizeof(_ring));
_next_idx = 0;
_margin_ema_x256 = 0;
_finalized_count = 0;
_last_echo_ms = 0;
_last_source_count = 0;
_power_reduction_db = 0;
}
}
int8_t PowerController::sfThresholdX4(uint8_t sf)
{
int idx = (int)sf - 7;
if (idx < 0) idx = 0;
if (idx > 5) idx = 5;
return snr_threshold_x4[idx];
}
int PowerController::findEntry(uint32_t hash32) const
{
for (int i = 0; i < RING_SIZE; i++) {
if (_ring[i].active && _ring[i].hash32 == hash32) {
return i;
}
}
return -1;
}
void PowerController::trackTransmit(uint32_t hash32, uint32_t now_ms)
{
if (!_enabled) return;
/* If ring slot is occupied, finalize it first */
if (_ring[_next_idx].active) {
finalizeEntry(_next_idx);
}
EchoEntry &e = _ring[_next_idx];
e.hash32 = hash32;
e.timestamp_ms = now_ms;
e.source_count = 0;
e.sf_at_track = _sf;
memset(e.sources, 0, sizeof(e.sources));
e.active = true;
_next_idx = (_next_idx + 1) % RING_SIZE;
}
bool PowerController::recordEcho(uint32_t hash32, int8_t snr_x4,
uint8_t first_hop_hash, uint32_t now_ms)
{
if (!_enabled) return false;
int idx = findEntry(hash32);
if (idx < 0) return false;
EchoEntry &e = _ring[idx];
/* Check if entry has expired */
if (now_ms - e.timestamp_ms > ECHO_WINDOW_MS) {
finalizeEntry(idx);
return false;
}
/* Update existing source or add new one */
for (int i = 0; i < e.source_count; i++) {
if (e.sources[i].hash == first_hop_hash) {
if (snr_x4 > e.sources[i].snr_x4) {
e.sources[i].snr_x4 = snr_x4;
}
_last_echo_ms = now_ms;
return true;
}
}
if (e.source_count < MAX_SOURCES) {
e.sources[e.source_count].hash = first_hop_hash;
e.sources[e.source_count].snr_x4 = snr_x4;
e.source_count++;
}
_last_echo_ms = now_ms;
return true;
}
int8_t PowerController::computeRobustSNR(const EchoEntry &entry) const
{
if (entry.source_count == 0) {
return sfThresholdX4(entry.sf_at_track); /* no echo = margin 0 */
}
if (entry.source_count == 1) {
return entry.sources[0].snr_x4;
}
/* 2-3 sources: sort descending, then cluster + rogue filter */
int8_t sorted[MAX_SOURCES];
int n = entry.source_count;
for (int i = 0; i < n; i++) {
sorted[i] = entry.sources[i].snr_x4;
}
/* Simple insertion sort (max 3 elements) */
for (int i = 1; i < n; i++) {
int8_t key = sorted[i];
int j = i - 1;
while (j >= 0 && sorted[j] < key) {
sorted[j + 1] = sorted[j];
j--;
}
sorted[j + 1] = key;
}
/* Count how many are within CLUSTER_WIDTH of the best */
int cluster_count = 1;
for (int i = 1; i < n; i++) {
if (sorted[0] - sorted[i] <= CLUSTER_WIDTH_X4) {
cluster_count++;
}
}
if (cluster_count >= 2) {
/* 2+ in cluster: median of the cluster values */
/* For 2 values: average. For 3 values: middle one. */
if (cluster_count == 2) {
return (int8_t)(((int)sorted[0] + (int)sorted[1]) / 2);
}
/* cluster_count == 3 (all 3 within 6 dB) */
return sorted[1]; /* median */
}
/* Only 1 in top cluster → rogue. Drop it, use next. */
if (n >= 3 && sorted[1] - sorted[2] <= CLUSTER_WIDTH_X4) {
/* sources[1] and [2] cluster together — median them */
return (int8_t)(((int)sorted[1] + (int)sorted[2]) / 2);
}
/* Fall back to second-best */
return sorted[1];
}
void PowerController::finalizeEntry(int idx)
{
if (!_ring[idx].active) return;
EchoEntry &e = _ring[idx];
_last_source_count = e.source_count;
int8_t robust_snr = computeRobustSNR(e);
int32_t margin_x4 = (int32_t)robust_snr - (int32_t)sfThresholdX4(e.sf_at_track);
/* margin_x4 is in x4 units. Convert to x256 for EMA. */
int32_t sample_x256 = margin_x4 << 6; /* x4 * 64 = x256 */
int32_t diff = sample_x256 - _margin_ema_x256;
if (_finalized_count < WARMUP_COUNT) {
/* During warmup, seed the EMA faster */
if (_finalized_count == 0) {
_margin_ema_x256 = sample_x256;
} else {
_margin_ema_x256 += diff >> 1;
}
} else {
/* Normal EMA update: ema += (sample - ema) >> shift */
_margin_ema_x256 += diff >> EMA_SHIFT;
}
_finalized_count++;
e.active = false;
LOG_DBG("APC finalize: sources=%d robust_snr=%.1f margin=%.1f ema=%.1f",
(int)_last_source_count,
(double)(robust_snr / 4.0f),
(double)(margin_x4 / 4.0f),
(double)getMarginEstimate());
}
void PowerController::tick(uint32_t now_ms)
{
if (!_enabled) return;
/* Finalize expired entries */
for (int i = 0; i < RING_SIZE; i++) {
if (_ring[i].active && now_ms - _ring[i].timestamp_ms > ECHO_WINDOW_MS) {
finalizeEntry(i);
}
}
if (!isWarmedUp()) return;
int32_t margin_x256 = _margin_ema_x256;
int32_t target_x256 = _target_margin_x4 << 6;
int32_t hyst_x256 = HYSTERESIS_X4 << 6;
int8_t old_reduction = _power_reduction_db;
/* Staleness takes priority: ramp back to full power if no echoes.
* When stale, never increase reduction old EMA data is unreliable. */
if (isStale(now_ms)) {
if (_power_reduction_db > 0) {
_power_reduction_db -= STEP_DOWN_DB;
if (_power_reduction_db < 0) {
_power_reduction_db = 0;
}
}
} else if (margin_x256 > target_x256 + hyst_x256) {
/* Margin very good — step down */
if (_power_reduction_db < MAX_REDUCTION_DB) {
_power_reduction_db += STEP_DOWN_DB;
if (_power_reduction_db > MAX_REDUCTION_DB) {
_power_reduction_db = MAX_REDUCTION_DB;
}
}
} else if (margin_x256 < target_x256 - hyst_x256) {
/* Margin too low — step up (reduce the reduction) */
if (_power_reduction_db > 0) {
_power_reduction_db -= STEP_UP_DB;
if (_power_reduction_db < 0) {
_power_reduction_db = 0;
}
}
}
if (_power_reduction_db != old_reduction) {
LOG_INF("APC: reduction %d -> %d dBm (margin=%.1f)",
(int)old_reduction, (int)_power_reduction_db,
(double)getMarginEstimate());
}
}
float PowerController::getMarginEstimate() const
{
return (float)_margin_ema_x256 / 256.0f;
}
bool PowerController::isStale(uint32_t now_ms) const
{
if (_last_echo_ms == 0) return false;
return now_ms - _last_echo_ms > STALE_MS;
}
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* Adaptive Power Control echo-based TX power reduction
*/
#include <mesh/PowerController.h>
#include <mesh/Packet.h>
#include <string.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_apc, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
/* SNR thresholds per SF (x4 fixed point, matching radio_common.h) */
static constexpr int8_t snr_threshold_x4[] = {
-30, /* SF7: -7.5 dB */
-40, /* SF8: -10.0 dB */
-50, /* SF9: -12.5 dB */
-60, /* SF10: -15.0 dB */
-70, /* SF11: -17.5 dB */
-80, /* SF12: -20.0 dB */
};
namespace mesh {
PowerController::PowerController()
: _next_idx(0), _margin_ema_x256(0), _finalized_count(0),
_last_echo_ms(0), _power_reduction_db(0), _enabled(true),
_sf(8), _last_source_count(0), _target_margin_x4(DEFAULT_TARGET_MARGIN_X4)
{
memset(_ring, 0, sizeof(_ring));
}
void PowerController::setEnabled(bool en)
{
if (_enabled == en) return;
_enabled = en;
if (!_enabled) {
/* Drop APC runtime state while disabled so no tracking work runs. */
memset(_ring, 0, sizeof(_ring));
_next_idx = 0;
_margin_ema_x256 = 0;
_finalized_count = 0;
_last_echo_ms = 0;
_last_source_count = 0;
_power_reduction_db = 0;
}
}
int8_t PowerController::sfThresholdX4(uint8_t sf)
{
int idx = (int)sf - 7;
if (idx < 0) idx = 0;
if (idx > 5) idx = 5;
return snr_threshold_x4[idx];
}
int PowerController::findEntry(uint32_t hash32) const
{
for (int i = 0; i < RING_SIZE; i++) {
if (_ring[i].active && _ring[i].hash32 == hash32) {
return i;
}
}
return -1;
}
void PowerController::trackTransmit(uint32_t hash32, uint32_t now_ms)
{
if (!_enabled) return;
/* If ring slot is occupied, finalize it first */
if (_ring[_next_idx].active) {
finalizeEntry(_next_idx);
}
EchoEntry &e = _ring[_next_idx];
e.hash32 = hash32;
e.timestamp_ms = now_ms;
e.source_count = 0;
e.sf_at_track = _sf;
memset(e.sources, 0, sizeof(e.sources));
e.active = true;
_next_idx = (_next_idx + 1) % RING_SIZE;
}
bool PowerController::recordEcho(uint32_t hash32, int8_t snr_x4,
uint8_t first_hop_hash, uint32_t now_ms)
{
if (!_enabled) return false;
int idx = findEntry(hash32);
if (idx < 0) return false;
EchoEntry &e = _ring[idx];
/* Check if entry has expired */
if (now_ms - e.timestamp_ms > ECHO_WINDOW_MS) {
finalizeEntry(idx);
return false;
}
/* Update existing source or add new one */
for (int i = 0; i < e.source_count; i++) {
if (e.sources[i].hash == first_hop_hash) {
if (snr_x4 > e.sources[i].snr_x4) {
e.sources[i].snr_x4 = snr_x4;
}
_last_echo_ms = now_ms;
return true;
}
}
if (e.source_count < MAX_SOURCES) {
e.sources[e.source_count].hash = first_hop_hash;
e.sources[e.source_count].snr_x4 = snr_x4;
e.source_count++;
}
_last_echo_ms = now_ms;
return true;
}
int8_t PowerController::computeRobustSNR(const EchoEntry &entry) const
{
if (entry.source_count == 0) {
return sfThresholdX4(entry.sf_at_track); /* no echo = margin 0 */
}
if (entry.source_count == 1) {
return entry.sources[0].snr_x4;
}
/* 2-3 sources: sort descending, then cluster + rogue filter */
int8_t sorted[MAX_SOURCES];
int n = entry.source_count;
for (int i = 0; i < n; i++) {
sorted[i] = entry.sources[i].snr_x4;
}
/* Simple insertion sort (max 3 elements) */
for (int i = 1; i < n; i++) {
int8_t key = sorted[i];
int j = i - 1;
while (j >= 0 && sorted[j] < key) {
sorted[j + 1] = sorted[j];
j--;
}
sorted[j + 1] = key;
}
/* Count how many are within CLUSTER_WIDTH of the best */
int cluster_count = 1;
for (int i = 1; i < n; i++) {
if (sorted[0] - sorted[i] <= CLUSTER_WIDTH_X4) {
cluster_count++;
}
}
if (cluster_count >= 2) {
/* 2+ in cluster: median of the cluster values */
/* For 2 values: average. For 3 values: middle one. */
if (cluster_count == 2) {
return (int8_t)(((int)sorted[0] + (int)sorted[1]) / 2);
}
/* cluster_count == 3 (all 3 within 6 dB) */
return sorted[1]; /* median */
}
/* Only 1 in top cluster → rogue. Drop it, use next. */
if (n >= 3 && sorted[1] - sorted[2] <= CLUSTER_WIDTH_X4) {
/* sources[1] and [2] cluster together — median them */
return (int8_t)(((int)sorted[1] + (int)sorted[2]) / 2);
}
/* Fall back to second-best */
return sorted[1];
}
void PowerController::finalizeEntry(int idx)
{
if (!_ring[idx].active) return;
EchoEntry &e = _ring[idx];
_last_source_count = e.source_count;
int8_t robust_snr = computeRobustSNR(e);
int32_t margin_x4 = (int32_t)robust_snr - (int32_t)sfThresholdX4(e.sf_at_track);
/* margin_x4 is in x4 units. Convert to x256 for EMA. */
int32_t sample_x256 = margin_x4 << 6; /* x4 * 64 = x256 */
int32_t diff = sample_x256 - _margin_ema_x256;
if (_finalized_count < WARMUP_COUNT) {
/* During warmup, seed the EMA faster */
if (_finalized_count == 0) {
_margin_ema_x256 = sample_x256;
} else {
_margin_ema_x256 += diff >> 1;
}
} else {
/* Normal EMA update: ema += (sample - ema) >> shift */
_margin_ema_x256 += diff >> EMA_SHIFT;
}
_finalized_count++;
e.active = false;
LOG_DBG("APC finalize: sources=%d robust_snr=%.1f margin=%.1f ema=%.1f",
(int)_last_source_count,
(double)(robust_snr / 4.0f),
(double)(margin_x4 / 4.0f),
(double)getMarginEstimate());
}
void PowerController::tick(uint32_t now_ms)
{
if (!_enabled) return;
/* Finalize expired entries */
for (int i = 0; i < RING_SIZE; i++) {
if (_ring[i].active && now_ms - _ring[i].timestamp_ms > ECHO_WINDOW_MS) {
finalizeEntry(i);
}
}
if (!isWarmedUp()) return;
int32_t margin_x256 = _margin_ema_x256;
int32_t target_x256 = _target_margin_x4 << 6;
int32_t hyst_x256 = HYSTERESIS_X4 << 6;
int8_t old_reduction = _power_reduction_db;
/* Staleness takes priority: ramp back to full power if no echoes.
* When stale, never increase reduction old EMA data is unreliable. */
if (isStale(now_ms)) {
if (_power_reduction_db > 0) {
_power_reduction_db -= STEP_DOWN_DB;
if (_power_reduction_db < 0) {
_power_reduction_db = 0;
}
}
} else if (margin_x256 > target_x256 + hyst_x256) {
/* Margin very good — step down */
if (_power_reduction_db < MAX_REDUCTION_DB) {
_power_reduction_db += STEP_DOWN_DB;
if (_power_reduction_db > MAX_REDUCTION_DB) {
_power_reduction_db = MAX_REDUCTION_DB;
}
}
} else if (margin_x256 < target_x256 - hyst_x256) {
/* Margin too low — step up (reduce the reduction) */
if (_power_reduction_db > 0) {
_power_reduction_db -= STEP_UP_DB;
if (_power_reduction_db < 0) {
_power_reduction_db = 0;
}
}
}
if (_power_reduction_db != old_reduction) {
LOG_INF("APC: reduction %d -> %d dBm (margin=%.1f)",
(int)old_reduction, (int)_power_reduction_db,
(double)getMarginEstimate());
}
}
float PowerController::getMarginEstimate() const
{
return (float)_margin_ema_x256 / 256.0f;
}
bool PowerController::isStale(uint32_t now_ms) const
{
if (_last_echo_ms == 0) return false;
return now_ms - _last_echo_ms > STALE_MS;
}
} /* namespace mesh */