mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 22:38:19 +00:00
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:
co-authored by
Claude Opus 4.7
parent
28e12c7eef
commit
5e7adfb130
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
+530
-530
File diff suppressed because it is too large
Load Diff
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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 = ®ions[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;
|
||||
}
|
||||
|
||||
@@ -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 ®ions[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 ®ions[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;
|
||||
};
|
||||
|
||||
+525
-525
File diff suppressed because it is too large
Load Diff
+157
-157
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+106
-106
@@ -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
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user