Browse Source

nrf54L15+LR2021 support

pull/2944/head
Mohamad EL CHAMAA 4 weeks ago
parent
commit
7910013ecf
  1. 30
      examples/companion_radio/DataStore.cpp
  2. 2
      examples/companion_radio/DataStore.h
  3. 2
      examples/companion_radio/MyMesh.h
  4. 28
      examples/companion_radio/main.cpp
  5. 6
      examples/simple_repeater/MyMesh.cpp
  6. 2
      examples/simple_repeater/MyMesh.h
  7. 13
      examples/simple_repeater/main.cpp
  8. 29
      examples/simple_secure_chat/main.cpp
  9. 2
      src/helpers/ClientACL.cpp
  10. 2
      src/helpers/CommonCLI.cpp
  11. 4
      src/helpers/IdentityStore.cpp
  12. 2
      src/helpers/IdentityStore.h
  13. 2
      src/helpers/RegionMap.cpp
  14. 106
      src/helpers/nrf54/InternalFileSystem.cpp
  15. 22
      src/helpers/nrf54/InternalFileSystem.h
  16. 298
      src/helpers/nrf54/SerialBLEInterface.cpp
  17. 86
      src/helpers/nrf54/SerialBLEInterface.h
  18. 88
      src/helpers/radiolib/CustomLR2021.h
  19. 40
      src/helpers/radiolib/CustomLR2021Wrapper.h
  20. 4
      src/helpers/radiolib/RadioLibWrappers.cpp
  21. 1
      src/helpers/radiolib/RadioLibWrappers.h
  22. 38
      variants/xiao_nrf54l15/XiaoNrf54l15Board.h
  23. 36
      variants/xiao_nrf54l15/target.cpp
  24. 62
      variants/xiao_nrf54l15/target.h
  25. 5
      zephyr-port/.gitignore
  26. 71
      zephyr-port/07_companion/CMakeLists.txt
  27. 127
      zephyr-port/07_companion/README.md
  28. 44
      zephyr-port/07_companion/app.overlay
  29. 107
      zephyr-port/07_companion/compat/Arduino.h
  30. 39
      zephyr-port/07_companion/compat/CayenneLPP.h
  31. 60
      zephyr-port/07_companion/compat/Print.h
  32. 25
      zephyr-port/07_companion/compat/RTClib.h
  33. 16
      zephyr-port/07_companion/compat/Stream.h
  34. 20
      zephyr-port/07_companion/dts/bindings/mc-lora-ctrl.yaml
  35. 70
      zephyr-port/07_companion/prj.conf
  36. 69
      zephyr-port/07_companion/src/main.cpp
  37. 293
      zephyr-port/07_companion/src/serial_ble_interface.cpp
  38. 58
      zephyr-port/07_companion/src/serial_ble_interface.h
  39. 147
      zephyr-port/07_companion/src/target.cpp
  40. 80
      zephyr-port/07_companion/src/target.h
  41. 55
      zephyr-port/07_companion/src/zephyr_internal_fs.cpp
  42. 118
      zephyr-port/07_companion/src/zephyr_radiolib_hal.h

30
examples/companion_radio/DataStore.cpp

@ -8,7 +8,7 @@
#endif #endif
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock), DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
identity_store(fs, "") identity_store(fs, "")
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
identity_store(fs, "/identity") identity_store(fs, "/identity")
@ -20,7 +20,7 @@ DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra
#if defined(EXTRAFS) || defined(QSPIFLASH) #if defined(EXTRAFS) || defined(QSPIFLASH)
DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock), DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
identity_store(fs, "") identity_store(fs, "")
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
identity_store(fs, "/identity") identity_store(fs, "/identity")
@ -32,7 +32,7 @@ DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock)
#endif #endif
static File openWrite(FILESYSTEM* fs, const char* filename) { static File openWrite(FILESYSTEM* fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
fs->remove(filename); fs->remove(filename);
return fs->open(filename, FILE_O_WRITE); return fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
@ -42,7 +42,7 @@ static File openWrite(FILESYSTEM* fs, const char* filename) {
#endif #endif
} }
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
static uint32_t _ContactsChannelsTotalBlocks = 0; static uint32_t _ContactsChannelsTotalBlocks = 0;
#endif #endif
@ -51,7 +51,7 @@ void DataStore::begin() {
identity_store.begin(); identity_store.begin();
#endif #endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
_ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count; _ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count;
checkAdvBlobFile(); checkAdvBlobFile();
#if defined(EXTRAFS) || defined(QSPIFLASH) #if defined(EXTRAFS) || defined(QSPIFLASH)
@ -68,17 +68,19 @@ void DataStore::begin() {
#include <nvs_flash.h> #include <nvs_flash.h>
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
#include <LittleFS.h> #include <LittleFS.h>
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
#if defined(QSPIFLASH) #if defined(QSPIFLASH)
#include <CustomLFS_QSPIFlash.h> #include <CustomLFS_QSPIFlash.h>
#elif defined(EXTRAFS) #elif defined(EXTRAFS)
#include <CustomLFS.h> #include <CustomLFS.h>
#else #elif defined(NRF54_PLATFORM)
#include <helpers/nrf54/InternalFileSystem.h>
#else
#include <InternalFileSystem.h> #include <InternalFileSystem.h>
#endif #endif
#endif #endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
int _countLfsBlock(void *p, lfs_block_t block){ int _countLfsBlock(void *p, lfs_block_t block){
if (block > _ContactsChannelsTotalBlocks) { if (block > _ContactsChannelsTotalBlocks) {
MESH_DEBUG_PRINTLN("ERROR: Block %d exceeds filesystem bounds - CORRUPTION DETECTED!", block); MESH_DEBUG_PRINTLN("ERROR: Block %d exceeds filesystem bounds - CORRUPTION DETECTED!", block);
@ -108,7 +110,7 @@ uint32_t DataStore::getStorageUsedKb() const {
info.usedBytes = 0; info.usedBytes = 0;
_fs->info(info); _fs->info(info);
return info.usedBytes / 1024; return info.usedBytes / 1024;
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg; const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
int usedBlockCount = _getLfsUsedBlockCount(_getContactsChannelsFS()); int usedBlockCount = _getLfsUsedBlockCount(_getContactsChannelsFS());
int usedBytes = config->block_size * usedBlockCount; int usedBytes = config->block_size * usedBlockCount;
@ -126,7 +128,7 @@ uint32_t DataStore::getStorageTotalKb() const {
info.totalBytes = 0; info.totalBytes = 0;
_fs->info(info); _fs->info(info);
return info.totalBytes / 1024; return info.totalBytes / 1024;
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg; const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
int totalBytes = config->block_size * config->block_count; int totalBytes = config->block_size * config->block_count;
return totalBytes / 1024; return totalBytes / 1024;
@ -136,7 +138,7 @@ uint32_t DataStore::getStorageTotalKb() const {
} }
File DataStore::openRead(const char* filename) { File DataStore::openRead(const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
return _fs->open(filename, FILE_O_READ); return _fs->open(filename, FILE_O_READ);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
return _fs->open(filename, "r"); return _fs->open(filename, "r");
@ -146,7 +148,7 @@ File DataStore::openRead(const char* filename) {
} }
File DataStore::openRead(FILESYSTEM* fs, const char* filename) { File DataStore::openRead(FILESYSTEM* fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
return fs->open(filename, FILE_O_READ); return fs->open(filename, FILE_O_READ);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
return fs->open(filename, "r"); return fs->open(filename, "r");
@ -164,7 +166,7 @@ bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) {
} }
bool DataStore::formatFileSystem() { bool DataStore::formatFileSystem() {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
if (_fsExtra == nullptr) { if (_fsExtra == nullptr) {
return _fs->format(); return _fs->format();
} else { } else {
@ -387,7 +389,7 @@ void DataStore::saveChannels(DataStoreHost* host) {
} }
} }
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE) #define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE)

2
examples/companion_radio/DataStore.h

@ -20,7 +20,7 @@ class DataStore {
IdentityStore identity_store; IdentityStore identity_store;
void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon); void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
void checkAdvBlobFile(); void checkAdvBlobFile();
#endif #endif

2
examples/companion_radio/MyMesh.h

@ -17,6 +17,8 @@
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h> #include <InternalFileSystem.h>
#elif defined(NRF54_PLATFORM)
#include <helpers/nrf54/InternalFileSystem.h>
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
#include <LittleFS.h> #include <LittleFS.h>
#elif defined(ESP32) #elif defined(ESP32)

28
examples/companion_radio/main.cpp

@ -26,6 +26,9 @@ static uint32_t _atoi(const char* sp) {
DataStore store(InternalFS, rtc_clock); DataStore store(InternalFS, rtc_clock);
#endif #endif
#endif #endif
#elif defined(NRF54_PLATFORM)
#include <helpers/nrf54/InternalFileSystem.h>
DataStore store(InternalFS, rtc_clock);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
#include <LittleFS.h> #include <LittleFS.h>
DataStore store(LittleFS, rtc_clock); DataStore store(LittleFS, rtc_clock);
@ -78,6 +81,14 @@ static uint32_t _atoi(const char* sp) {
#include <helpers/ArduinoSerialInterface.h> #include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface; ArduinoSerialInterface serial_interface;
#endif #endif
#elif defined(NRF54_PLATFORM)
#ifdef BLE_PIN_CODE
#include <helpers/nrf54/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#else
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
#endif
#elif defined(STM32_PLATFORM) #elif defined(STM32_PLATFORM)
#include <helpers/ArduinoSerialInterface.h> #include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface; ArduinoSerialInterface serial_interface;
@ -156,6 +167,23 @@ void setup() {
#endif #endif
); );
#ifdef BLE_PIN_CODE
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#else
serial_interface.begin(Serial);
#endif
the_mesh.startInterface(serial_interface);
#elif defined(NRF54_PLATFORM)
InternalFS.begin();
store.begin();
the_mesh.begin(
#ifdef DISPLAY_CLASS
disp != NULL
#else
false
#endif
);
#ifdef BLE_PIN_CODE #ifdef BLE_PIN_CODE
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#else #else

6
examples/simple_repeater/MyMesh.cpp

@ -387,7 +387,7 @@ mesh::Packet *MyMesh::createSelfAdvert() {
} }
File MyMesh::openAppend(const char *fname) { File MyMesh::openAppend(const char *fname) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
return _fs->open(fname, FILE_O_WRITE); return _fs->open(fname, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
return _fs->open(fname, "a"); return _fs->open(fname, "a");
@ -998,7 +998,7 @@ void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr,
} }
bool MyMesh::formatFileSystem() { bool MyMesh::formatFileSystem() {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
return InternalFS.format(); return InternalFS.format();
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
return LittleFS.format(); return LittleFS.format();
@ -1153,7 +1153,7 @@ void MyMesh::formatPacketStatsReply(char *reply) {
} }
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) { void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
IdentityStore store(*_fs, ""); IdentityStore store(*_fs, "");
#elif defined(ESP32) #elif defined(ESP32)
IdentityStore store(*_fs, "/identity"); IdentityStore store(*_fs, "/identity");

2
examples/simple_repeater/MyMesh.h

@ -7,6 +7,8 @@
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h> #include <InternalFileSystem.h>
#elif defined(NRF54_PLATFORM)
#include <helpers/nrf54/InternalFileSystem.h>
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
#include <LittleFS.h> #include <LittleFS.h>
#elif defined(ESP32) #elif defined(ESP32)

13
examples/simple_repeater/main.cpp

@ -56,7 +56,7 @@ void setup() {
fast_rng.begin(radio_driver.getRngSeed()); fast_rng.begin(radio_driver.getRngSeed());
FILESYSTEM* fs; FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
InternalFS.begin(); InternalFS.begin();
fs = &InternalFS; fs = &InternalFS;
IdentityStore store(InternalFS, ""); IdentityStore store(InternalFS, "");
@ -107,12 +107,15 @@ void loop() {
int len = strlen(command); int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) { while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read(); char c = Serial.read();
if (c != '\n') { if (c == '\r' || c == '\n') { // end of line: accept CR, LF or CRLF
command[len++] = c; if (len == 0) continue; // skip empty lines (the LF of a CRLF)
command[len++] = '\r'; // normalise to the '\r' sentinel used below
command[len] = 0; command[len] = 0;
Serial.print(c); break;
} }
if (c == '\r') break; command[len++] = c;
command[len] = 0;
Serial.print(c);
} }
if (len == sizeof(command)-1) { // command buffer full if (len == sizeof(command)-1) { // command buffer full
command[sizeof(command)-1] = '\r'; command[sizeof(command)-1] = '\r';

29
examples/simple_secure_chat/main.cpp

@ -3,6 +3,8 @@
#if defined(NRF52_PLATFORM) #if defined(NRF52_PLATFORM)
#include <InternalFileSystem.h> #include <InternalFileSystem.h>
#elif defined(NRF54_PLATFORM)
#include <helpers/nrf54/InternalFileSystem.h>
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
#include <LittleFS.h> #include <LittleFS.h>
#elif defined(ESP32) #elif defined(ESP32)
@ -42,7 +44,10 @@
#include <helpers/BaseChatMesh.h> #include <helpers/BaseChatMesh.h>
#define SEND_TIMEOUT_BASE_MILLIS 500 #define SEND_TIMEOUT_BASE_MILLIS 3000 // generous base so first-contact (cold path
// discovery) ACKs land inside the window; warm
// round-trips are 600-800ms so this only affects
// how long a genuine no-ACK takes to report
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
#define DIRECT_SEND_PERHOP_FACTOR 6.0f #define DIRECT_SEND_PERHOP_FACTOR 6.0f
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 #define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
@ -126,7 +131,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor {
} }
void saveContacts() { void saveContacts() {
#if defined(NRF52_PLATFORM) #if defined(NRF52_PLATFORM) || defined(NRF54_PLATFORM)
_fs->remove("/contacts"); _fs->remove("/contacts");
File file = _fs->open("/contacts", FILE_O_WRITE); File file = _fs->open("/contacts", FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
@ -218,11 +223,14 @@ protected:
} }
ContactInfo* processAck(const uint8_t *data) override { ContactInfo* processAck(const uint8_t *data) override {
if (memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient if (expected_ack_crc != 0 && memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent); Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent);
// NOTE: the same ACK can be received multiple times! // NOTE: the same ACK can be received multiple times!
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
return NULL; // TODO: really should return ContactInfo pointer // Return the matched contact (the recipient we sent to) so BaseChatMesh cancels its
// send-timeout; returning NULL here left txt_send_timeout running, so onSendTimeout()
// fired a spurious "ERROR: timed out, no ACK." right after a successful "Got ACK!".
return curr_recipient;
} }
//uint32_t crc; //uint32_t crc;
@ -298,7 +306,7 @@ public:
BaseChatMesh::begin(); BaseChatMesh::begin();
#if defined(NRF52_PLATFORM) #if defined(NRF52_PLATFORM) || defined(NRF54_PLATFORM)
IdentityStore store(fs, ""); IdentityStore store(fs, "");
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
IdentityStore store(fs, "/identity"); IdentityStore store(fs, "/identity");
@ -341,7 +349,7 @@ public:
} }
void savePrefs() { void savePrefs() {
#if defined(NRF52_PLATFORM) #if defined(NRF52_PLATFORM) || defined(NRF54_PLATFORM)
_fs->remove("/node_prefs"); _fs->remove("/node_prefs");
File file = _fs->open("/node_prefs", FILE_O_WRITE); File file = _fs->open("/node_prefs", FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
@ -528,10 +536,9 @@ public:
int len = strlen(command); int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) { while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read(); char c = Serial.read();
if (c != '\n') { if (c == '\n') c = '\r'; // accept LF or CRLF (arduino-cli monitor sends LF)
command[len++] = c; command[len++] = c;
command[len] = 0; command[len] = 0;
}
Serial.print(c); Serial.print(c);
} }
if (len == sizeof(command)-1) { // command buffer full if (len == sizeof(command)-1) { // command buffer full
@ -564,7 +571,7 @@ void setup() {
fast_rng.begin(radio_driver.getRngSeed()); fast_rng.begin(radio_driver.getRngSeed());
#if defined(NRF52_PLATFORM) #if defined(NRF52_PLATFORM) || defined(NRF54_PLATFORM)
InternalFS.begin(); InternalFS.begin();
the_mesh.begin(InternalFS); the_mesh.begin(InternalFS);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)

2
src/helpers/ClientACL.cpp

@ -1,7 +1,7 @@
#include "ClientACL.h" #include "ClientACL.h"
static File openWrite(FILESYSTEM* _fs, const char* filename) { static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
_fs->remove(filename); _fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE); return _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)

2
src/helpers/CommonCLI.cpp

@ -127,7 +127,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
} }
void CommonCLI::savePrefs(FILESYSTEM* fs) { void CommonCLI::savePrefs(FILESYSTEM* fs) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
fs->remove("/com_prefs"); fs->remove("/com_prefs");
File file = fs->open("/com_prefs", FILE_O_WRITE); File file = fs->open("/com_prefs", FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)

4
src/helpers/IdentityStore.cpp

@ -46,7 +46,7 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) {
char filename[40]; char filename[40];
sprintf(filename, "%s/%s.id", _dir, name); sprintf(filename, "%s/%s.id", _dir, name);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
_fs->remove(filename); _fs->remove(filename);
File file = _fs->open(filename, FILE_O_WRITE); File file = _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
@ -68,7 +68,7 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const
char filename[40]; char filename[40];
sprintf(filename, "%s/%s.id", _dir, name); sprintf(filename, "%s/%s.id", _dir, name);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
_fs->remove(filename); _fs->remove(filename);
File file = _fs->open(filename, FILE_O_WRITE); File file = _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)

2
src/helpers/IdentityStore.h

@ -3,7 +3,7 @@
#if defined(ESP32) || defined(RP2040_PLATFORM) #if defined(ESP32) || defined(RP2040_PLATFORM)
#include <FS.h> #include <FS.h>
#define FILESYSTEM fs::FS #define FILESYSTEM fs::FS
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
#include <Adafruit_LittleFS.h> #include <Adafruit_LittleFS.h>
#define FILESYSTEM Adafruit_LittleFS #define FILESYSTEM Adafruit_LittleFS

2
src/helpers/RegionMap.cpp

@ -59,7 +59,7 @@ static const char* skip_hash(const char* name) {
} }
static File openWrite(FILESYSTEM* _fs, const char* filename) { static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NRF54_PLATFORM)
_fs->remove(filename); _fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE); return _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)

106
src/helpers/nrf54/InternalFileSystem.cpp

@ -0,0 +1,106 @@
#include <Arduino.h>
#include "InternalFileSystem.h"
#include <nrf54l15.h>
#include <string.h>
// Filesystem region, resident in RRAM
// `const` forces it into .rodata (RRAM/flash) at a stable, linker-allocated
// address (NOT .data/.bss in RAM); so it survives reboot. We write to it at
// runtime via the RRAMC peripheral (RRAM is uniformly writable regardless of the
// .rodata label, the same mechanism the core's EEPROM library uses).
// Force into a .rodata subsection so the linker places it in FLASH/RRAM (the
// linker routes *(.rodata*) -> FLASH). Without an explicit section the `volatile`
// initialised array lands in .data (RAM) and would NOT persist.
__attribute__((section(".rodata.lfs_region"), aligned(LFS_BLOCK_SIZE)))
static const volatile uint8_t g_lfsRegion[LFS_RRAM_TOTAL_SIZE] = { 0 };
#define LFS_BASE ((uint32_t)(uintptr_t)g_lfsRegion)
// RRAMC write primitive (adapted from the core's EEPROM library)
static constexpr uint32_t kRramcBase = 0x5004B000UL;
static constexpr uint32_t kRramcSpin = 600000UL;
static inline NRF_RRAMC_Type* rramc() { return reinterpret_cast<NRF_RRAMC_Type*>(kRramcBase); }
static bool waitReady(NRF_RRAMC_Type* r, uint32_t spin) {
while (spin-- > 0U) {
if (((r->READY & RRAMC_READY_READY_Msk) >> RRAMC_READY_READY_Pos) == RRAMC_READY_READY_Ready) return true;
}
return false;
}
static bool waitReadyNext(NRF_RRAMC_Type* r, uint32_t spin) {
while (spin-- > 0U) {
if (((r->READYNEXT & RRAMC_READYNEXT_READYNEXT_Msk) >> RRAMC_READYNEXT_READYNEXT_Pos) == RRAMC_READYNEXT_READYNEXT_Ready) return true;
}
return false;
}
static bool rramWrite(uint32_t addr, const uint8_t* src, size_t len) {
NRF_RRAMC_Type* const r = rramc();
const uint32_t prev = r->CONFIG;
r->CONFIG = prev | RRAMC_CONFIG_WEN_Msk;
bool ok = waitReady(r, kRramcSpin);
if (ok) {
r->EVENTS_ACCESSERROR = 0U;
for (size_t i = 0; i < len; ++i) {
if (!waitReadyNext(r, kRramcSpin)) { ok = false; break; }
*reinterpret_cast<volatile uint8_t*>(addr + static_cast<uint32_t>(i)) = src[i];
}
if (r->EVENTS_ACCESSERROR != 0U) ok = false;
}
if (ok) {
r->EVENTS_READY = 0U;
r->TASKS_COMMITWRITEBUF = 1U;
ok = waitReady(r, kRramcSpin);
}
r->CONFIG = prev;
return ok;
}
// LittleFS block device over RRAM
static int _rram_read(const struct lfs_config* c, lfs_block_t block, lfs_off_t off, void* buffer, lfs_size_t size) {
(void)c;
if (!buffer || !size) return LFS_ERR_INVAL;
memcpy(buffer, (const void*)(LFS_BASE + block * LFS_BLOCK_SIZE + off), size); // RRAM is memory-mapped
return LFS_ERR_OK;
}
static int _rram_prog(const struct lfs_config* c, lfs_block_t block, lfs_off_t off, const void* buffer, lfs_size_t size) {
(void)c;
return rramWrite(LFS_BASE + block * LFS_BLOCK_SIZE + off, (const uint8_t*)buffer, size) ? LFS_ERR_OK : LFS_ERR_IO;
}
static int _rram_erase(const struct lfs_config* c, lfs_block_t block) {
(void)c; (void)block;
return LFS_ERR_OK; // RRAM needs no erase-before-write (unlike NOR flash)
}
static int _rram_sync(const struct lfs_config* c) { (void)c; return LFS_ERR_OK; }
struct lfs_config _InternalFSConfig = {
.context = NULL,
.read = _rram_read,
.prog = _rram_prog,
.erase = _rram_erase,
.sync = _rram_sync,
.read_size = LFS_BLOCK_SIZE,
.prog_size = LFS_BLOCK_SIZE,
.block_size = LFS_BLOCK_SIZE,
.block_count = LFS_RRAM_TOTAL_SIZE / LFS_BLOCK_SIZE,
.lookahead = 128,
.read_buffer = NULL,
.prog_buffer = NULL,
.lookahead_buffer = NULL,
.file_buffer = NULL
};
InternalFileSystem InternalFS;
InternalFileSystem::InternalFileSystem(void) : Adafruit_LittleFS(&_InternalFSConfig) { }
bool InternalFileSystem::begin(void) {
// mount; on failure format then mount again
if (!Adafruit_LittleFS::begin()) {
this->format();
if (!Adafruit_LittleFS::begin()) return false;
}
return true;
}

22
src/helpers/nrf54/InternalFileSystem.h

@ -0,0 +1,22 @@
#pragma once
// LittleFS-over-RRAM for the nRF54L15 (lolren bare-metal core). Mirrors the STM32
// InternalFileSystem (helpers/stm32) but backs LittleFS with the nRF54L15 RRAM via
// the RRAMC peripheral instead of STM32 flash HAL. Provides the `Adafruit_LittleFS`
// `InternalFS` instance that MeshCore's FILESYSTEM/IdentityStore expect.
#include "Adafruit_LittleFS.h"
#ifndef LFS_RRAM_TOTAL_SIZE
#define LFS_RRAM_TOTAL_SIZE (16 * 2048) // 32 KB filesystem (matches STM32 default)
#endif
#define LFS_BLOCK_SIZE (2048)
class InternalFileSystem : public Adafruit_LittleFS {
public:
InternalFileSystem(void);
bool begin(void);
};
extern InternalFileSystem InternalFS;
using namespace Adafruit_LittleFS_Namespace;

298
src/helpers/nrf54/SerialBLEInterface.cpp

@ -0,0 +1,298 @@
#include "SerialBLEInterface.h"
#include <stdio.h>
#include <string.h>
#include "ble_gap.h"
// NOTE: no <ble_hci.h> and no SoftDevice (sd_*) calls, this core's Bluefruit52Lib
// reimplements only the C++ API. SoftDevice GAP calls are swapped for core methods.
#define BLE_HEALTH_CHECK_INTERVAL 10000
#define BLE_RETRY_THROTTLE_MS 250
// Connection parameters (units: interval=1.25ms, timeout=10ms)
#define BLE_MIN_CONN_INTERVAL 12 // 15ms
#define BLE_MAX_CONN_INTERVAL 24 // 30ms
// Advertising parameters (units: 0.625ms)
#define BLE_ADV_INTERVAL_MIN 32 // 20ms
#define BLE_ADV_INTERVAL_MAX 244 // 152.5ms
#define BLE_ADV_FAST_TIMEOUT 30 // seconds
#define BLE_RX_DRAIN_BUF_SIZE 32
static SerialBLEInterface* instance = nullptr;
void SerialBLEInterface::onConnect(uint16_t connection_handle) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected handle=0x%04X", connection_handle);
if (instance) {
instance->_conn_handle = connection_handle;
// Request 2M PHY immediately. On 1M the core's tight T_IFS budget blows during the
// pairing handshake (EVT_RX/TX_TIMEOUT -> dropped SMP PDUs -> status=8); 2M halves the
// packet airtime and gives timing margin. Mirrors the Adafruit Bluefruit LE app, which
// negotiates 2M early and pairs reliably on this core.
BLEConnection* conn = Bluefruit.Connection(connection_handle);
if (conn) conn->requestPHY(BLE_GAP_PHY_2MBPS);
#if defined(BLE_NO_PAIRING)
instance->_isDeviceConnected = true; // open mode: no securing step, ready immediately
#else
instance->_isDeviceConnected = false; // wait for onSecured()
#endif
instance->clearBuffers();
}
}
void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected handle=0x%04X reason=%u", connection_handle, reason);
if (instance && instance->_conn_handle == connection_handle) {
instance->_conn_handle = BLE_CONN_HANDLE_INVALID;
instance->_isDeviceConnected = false;
instance->clearBuffers();
}
}
void SerialBLEInterface::onSecured(uint16_t connection_handle) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured handle=0x%04X", connection_handle);
if (instance && instance->isValidConnection(connection_handle, true)) {
instance->_isDeviceConnected = true;
// Preferred connection interval is set in begin() via Bluefruit.Periph.setConnInterval();
// the core's Bluefruit52Lib has no SoftDevice conn_param_update on an active link.
}
}
bool SerialBLEInterface::onPairingPasskey(uint16_t connection_handle, uint8_t const passkey[6], bool match_request) {
(void)connection_handle; (void)passkey;
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing passkey request match=%d", match_request);
return true;
}
void SerialBLEInterface::onPairingComplete(uint16_t connection_handle, uint8_t auth_status) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing complete handle=0x%04X status=%u", connection_handle, auth_status);
if (instance && instance->isValidConnection(connection_handle)) {
if (auth_status != BLE_GAP_SEC_STATUS_SUCCESS) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing failed, disconnecting");
instance->disconnect();
}
}
}
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
instance = this;
char charpin[20];
snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code);
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
char dev_name[32+16];
if (strcmp(name, "@@MAC") == 0) {
uint8_t mac[6];
Bluefruit.getAddr(mac); // core API (was sd_ble_gap_addr_get)
sprintf(name, "%02X%02X%02X%02X%02X%02X",
mac[5], mac[4], mac[3], mac[2], mac[1], mac[0]);
}
sprintf(dev_name, "%s%s", prefix, name);
// preferred connection interval (was sd_ble_gap_ppcp_set)
Bluefruit.Periph.setConnInterval(BLE_MIN_CONN_INTERVAL, BLE_MAX_CONN_INTERVAL);
Bluefruit.setTxPower(BLE_TX_POWER);
Bluefruit.setName(dev_name);
(void)charpin;
#if defined(BLE_NO_PAIRING)
// This core's SMP pairing is unreliable on nRF54 (MITM fails status=11, Just Works fails
// status=3, never reaches onSecured). Fall back to an OPEN, unencrypted NUS link: no
// bonding, characteristics readable/writable without pairing. (No MeshCore-level secrecy
// over the BLE hop, acceptable for bring-up; revisit if the core's SMP is fixed.)
Bluefruit.Periph.setConnectCallback(onConnect);
Bluefruit.Periph.setDisconnectCallback(onDisconnect);
bleuart.setPermission(SECMODE_OPEN, SECMODE_OPEN);
bleuart.begin();
bleuart.setRxCallback(onBleUartRX);
bledfu.setPermission(SECMODE_OPEN, SECMODE_OPEN);
bledfu.begin();
#else
// Encrypted PIN pairing, matching the core's working Security/pairing_pin example exactly:
// setPIN + SECMODE_ENC_WITH_MITM, and crucially NO setIOCaps and NO setPairPasskeyCallback.
// Adding either of those switched the device into a conflicting passkey/numeric-comparison
// mode and pairing failed (status=11). The central is prompted to enter the 6-digit PIN.
Bluefruit.Security.setPIN(charpin);
Bluefruit.Security.setPairCompleteCallback(onPairingComplete);
Bluefruit.Periph.setConnectCallback(onConnect);
Bluefruit.Periph.setDisconnectCallback(onDisconnect);
Bluefruit.Security.setSecuredCallback(onSecured);
// (no Bluefruit.setEventCallback: the raw conn-param-update event handler is nRF52/
// SoftDevice-only and not needed, defaults negotiate fine.)
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bleuart.begin();
bleuart.setRxCallback(onBleUartRX);
bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bledfu.begin();
#endif
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addService(bleuart);
Bluefruit.ScanResponse.addName();
Bluefruit.Advertising.setInterval(BLE_ADV_INTERVAL_MIN, BLE_ADV_INTERVAL_MAX);
Bluefruit.Advertising.setFastTimeout(BLE_ADV_FAST_TIMEOUT);
Bluefruit.Advertising.restartOnDisconnect(true);
}
void SerialBLEInterface::clearBuffers() {
send_queue_len = 0;
recv_queue_len = 0;
_last_retry_attempt = 0;
bleuart.flush();
}
void SerialBLEInterface::shiftSendQueueLeft() {
if (send_queue_len > 0) {
send_queue_len--;
for (uint8_t i = 0; i < send_queue_len; i++) send_queue[i] = send_queue[i + 1];
}
}
void SerialBLEInterface::shiftRecvQueueLeft() {
if (recv_queue_len > 0) {
recv_queue_len--;
for (uint8_t i = 0; i < recv_queue_len; i++) recv_queue[i] = recv_queue[i + 1];
}
}
bool SerialBLEInterface::isValidConnection(uint16_t handle, bool requireWaitingForSecurity) const {
if (_conn_handle != handle) return false;
BLEConnection* conn = Bluefruit.Connection(handle);
if (conn == nullptr || !conn->connected()) return false;
if (requireWaitingForSecurity && _isDeviceConnected) return false;
return true;
}
bool SerialBLEInterface::isAdvertising() const {
// The core's Bluefruit52Lib has no advertising-running query; restartOnDisconnect(true)
// keeps advertising alive, so report true (the watchdog restart below is a no-op).
return true;
}
void SerialBLEInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
clearBuffers();
_last_health_check = millis();
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.start(0);
}
void SerialBLEInterface::disconnect() {
if (_conn_handle != BLE_CONN_HANDLE_INVALID) {
Bluefruit.disconnect(_conn_handle); // core API (was sd_ble_gap_disconnect)
}
}
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface: disable");
Bluefruit.Advertising.restartOnDisconnect(false);
Bluefruit.Advertising.stop();
disconnect();
_last_health_check = 0;
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len);
return 0;
}
if (isConnected() && len > 0) {
if (send_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
send_queue[send_queue_len].len = len;
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
return len;
}
return 0;
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
if (send_queue_len > 0) {
if (!isConnected()) {
send_queue_len = 0;
} else {
unsigned long now = millis();
bool throttle_active = (_last_retry_attempt > 0 && (now - _last_retry_attempt) < BLE_RETRY_THROTTLE_MS);
if (!throttle_active) {
Frame frame_to_send = send_queue[0];
size_t written = bleuart.write(frame_to_send.buf, frame_to_send.len);
if (written == frame_to_send.len) {
_last_retry_attempt = 0;
shiftSendQueueLeft();
} else if (written > 0) {
_last_retry_attempt = 0;
shiftSendQueueLeft();
} else {
if (!isConnected()) { _last_retry_attempt = 0; shiftSendQueueLeft(); }
else { _last_retry_attempt = now; }
}
}
}
}
if (recv_queue_len > 0) {
size_t len = recv_queue[0].len;
memcpy(dest, recv_queue[0].buf, len);
shiftRecvQueueLeft();
return len;
}
unsigned long now = millis();
if (_isEnabled && !isConnected() && _conn_handle == BLE_CONN_HANDLE_INVALID) {
if (now - _last_health_check >= BLE_HEALTH_CHECK_INTERVAL) {
_last_health_check = now;
if (!isAdvertising()) Bluefruit.Advertising.start(0);
}
}
return 0;
}
void SerialBLEInterface::onBleUartRX(uint16_t conn_handle) {
if (!instance) return;
if (instance->_conn_handle != conn_handle || !instance->isConnected()) {
while (instance->bleuart.available() > 0) instance->bleuart.read();
return;
}
while (instance->bleuart.available() > 0) {
if (instance->recv_queue_len >= FRAME_QUEUE_SIZE) {
while (instance->bleuart.available() > 0) instance->bleuart.read();
break;
}
int avail = instance->bleuart.available();
if (avail > MAX_FRAME_SIZE) {
uint8_t drain_buf[BLE_RX_DRAIN_BUF_SIZE];
while (instance->bleuart.available() > 0) {
int chunk = instance->bleuart.available() > BLE_RX_DRAIN_BUF_SIZE ? BLE_RX_DRAIN_BUF_SIZE : instance->bleuart.available();
instance->bleuart.readBytes(drain_buf, chunk);
}
continue;
}
int read_len = avail;
instance->recv_queue[instance->recv_queue_len].len = read_len;
instance->bleuart.readBytes(instance->recv_queue[instance->recv_queue_len].buf, read_len);
instance->recv_queue_len++;
}
}
bool SerialBLEInterface::isConnected() const {
return _isDeviceConnected && Bluefruit.connected() > 0;
}
bool SerialBLEInterface::isWriteBusy() const {
return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3);
}

86
src/helpers/nrf54/SerialBLEInterface.h

@ -0,0 +1,86 @@
#pragma once
#include "../BaseSerialInterface.h"
#include <bluefruit.h>
// BLE serial interface for the nRF54L15 on the lolren bare-metal core. Adapted from
// helpers/nrf52/SerialBLEInterface: the Bluefruit C++ API (BLEUart/Advertising/Security/
// DFU) is the same, but this core has NO SoftDevice, so the raw `sd_*` GAP calls are
// swapped for the core's Bluefruit52Lib equivalents (disconnect/getAddr/setConnInterval)
// and the optional raw conn-param-update event handler is dropped (defaults are used).
#ifndef BLE_TX_POWER
#define BLE_TX_POWER 4
#endif
class SerialBLEInterface : public BaseSerialInterface {
BLEDfu bledfu;
BLEUart bleuart;
bool _isEnabled;
bool _isDeviceConnected;
uint16_t _conn_handle;
unsigned long _last_health_check;
unsigned long _last_retry_attempt;
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
#define FRAME_QUEUE_SIZE 12
uint8_t send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE];
uint8_t recv_queue_len;
Frame recv_queue[FRAME_QUEUE_SIZE];
void clearBuffers();
void shiftSendQueueLeft();
void shiftRecvQueueLeft();
bool isValidConnection(uint16_t handle, bool requireWaitingForSecurity = false) const;
bool isAdvertising() const;
static void onConnect(uint16_t connection_handle);
static void onDisconnect(uint16_t connection_handle, uint8_t reason);
static void onSecured(uint16_t connection_handle);
static bool onPairingPasskey(uint16_t connection_handle, uint8_t const passkey[6], bool match_request);
static void onPairingComplete(uint16_t connection_handle, uint8_t auth_status);
static void onBleUartRX(uint16_t conn_handle);
public:
SerialBLEInterface() {
_isEnabled = false;
_isDeviceConnected = false;
_conn_handle = BLE_CONN_HANDLE_INVALID;
_last_health_check = 0;
_last_retry_attempt = 0;
send_queue_len = 0;
recv_queue_len = 0;
}
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
void disconnect();
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
};
#if BLE_DEBUG_LOGGING && ARDUINO
#include <Arduino.h>
#define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__)
#define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__)
#else
#define BLE_DEBUG_PRINT(...) {}
#define BLE_DEBUG_PRINTLN(...) {}
#endif

88
src/helpers/radiolib/CustomLR2021.h

@ -0,0 +1,88 @@
#pragma once
#include <RadioLib.h>
#include "MeshCore.h"
// Custom RadioLib driver for the Semtech LR2021 (LoRa Plus), as used on the
// Seeed Wio-LR2021 (LR2021 + nRF54L15). Two board/chip quirks are handled here
// (both discovered during hardware bring-up):
#ifndef LR2021_IRQ_DIO
#define LR2021_IRQ_DIO 8 // Wio-LR2021 wires the LR2021's DIO8 to the host IRQ line
#endif
#ifndef LR2021_RX_BOOST_LEVEL
#define LR2021_RX_BOOST_LEVEL 7 // matches Semtech usp_zephyr rx-boost-cfg = <7>
#endif
class CustomLR2021 : public LR2021 {
uint8_t _rx_boost_level = 0;
public:
CustomLR2021(Module *mod) : LR2021(mod) { }
bool std_init(SPIClass* spi = NULL) {
// route the host IRQ to the DIO the board actually wires (default DIO8)
irqDioNum = LR2021_IRQ_DIO;
#ifdef LORA_CR
uint8_t cr = LORA_CR;
#else
uint8_t cr = 5;
#endif
#if defined(P_LORA_SCLK)
#if defined(ESP32_PLATFORM)
if (spi) spi->begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
#elif defined(NRF52_PLATFORM)
if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); }
#else
if (spi) spi->begin(); // bare-metal nRF54L15 core: SPI pins are fixed (D8/D9/D10)
#endif
#else
if (spi) spi->begin();
#endif
// tcxoVoltage = 0 -> skip SetTcxoMode (RadioLib mis-scales its start_time; see note above).
int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr,
RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, 0.0f);
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: LR2021 init failed: ");
Serial.println(status);
return false;
}
setCRC(2);
explicitHeader();
#ifdef RX_BOOSTED_GAIN
if (RX_BOOSTED_GAIN) setRxBoostedGainMode(LR2021_RX_BOOST_LEVEL);
#endif
return true; // success
}
size_t getPacketLength(bool update) override {
size_t len = LR2021::getPacketLength(update);
if (len == 0 && (getIrqFlags() & RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR)) {
// corrupted header: return to a known-good state; recvRaw restarts RX
MESH_DEBUG_PRINTLN("LR2021: got header CRC err, calling standby()");
standby();
}
return len;
}
bool isReceiving() {
uint32_t irq = getIrqFlags();
return (irq & RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED)
|| (irq & RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID);
}
int16_t setRxBoostedGainMode(uint8_t level) {
_rx_boost_level = level;
return LR2021::setRxBoostedGainMode(level);
}
uint8_t getRxBoostLevel() const { return _rx_boost_level; }
};

40
src/helpers/radiolib/CustomLR2021Wrapper.h

@ -0,0 +1,40 @@
#pragma once
#include "CustomLR2021.h"
#include "RadioLibWrappers.h"
// MeshCore wrapper for the LR2021. Implements the RadioLibWrapper hooks the mesh
// engine needs, using the LR2021's RadioLib API (getRssiInst / getRSSI / getSNR /
// boosted-gain). doResetAGC() is left to the base-class default for now; add an
// LR2021-specific reset only if RX sensitivity degrades over long runs.
class CustomLR2021Wrapper : public RadioLibWrapper {
public:
CustomLR2021Wrapper(CustomLR2021& radio, mesh::MainBoard& board)
: RadioLibWrapper(radio, board) { }
bool isReceivingPacket() override {
return ((CustomLR2021 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
float rssi = -110;
((CustomLR2021 *)_radio)->getRssiInst(&rssi);
return rssi;
}
void onSendFinished() override {
RadioLibWrapper::onSendFinished();
_radio->setPreambleLength(16); // overcomes weird issues with small and big pkts
}
float getLastRSSI() const override { return ((CustomLR2021 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLR2021 *)_radio)->getSNR(); }
void setRxBoostedGainMode(bool en) override {
((CustomLR2021 *)_radio)->setRxBoostedGainMode(en ? LR2021_RX_BOOST_LEVEL : 0);
}
bool getRxBoostedGainMode() const override {
return ((CustomLR2021 *)_radio)->getRxBoostLevel() != 0;
}
};

4
src/helpers/radiolib/RadioLibWrappers.cpp

@ -104,6 +104,7 @@ void RadioLibWrapper::loop() {
} }
void RadioLibWrapper::startRecv() { void RadioLibWrapper::startRecv() {
_radio->standby(); // LR2021: re-arming (setRxPath) while still in continuous RX -> CMD_PERR
int err = _radio->startReceive(); int err = _radio->startReceive();
if (err == RADIOLIB_ERR_NONE) { if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX; state = STATE_RX;
@ -128,7 +129,6 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
len = 0; len = 0;
n_recv_errors++; n_recv_errors++;
} else { } else {
// Serial.print(" readData() -> "); Serial.println(len);
n_recv++; n_recv++;
} }
} }
@ -136,6 +136,8 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
} }
if (state != STATE_RX) { if (state != STATE_RX) {
_radio->standby(); // back to standby first: re-arming from continuous RX makes the
// LR2021 reject setRxPath with CMD_PERR (-706) and wedges the receiver
int err = _radio->startReceive(); int err = _radio->startReceive();
if (err == RADIOLIB_ERR_NONE) { if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX; state = STATE_RX;

1
src/helpers/radiolib/RadioLibWrappers.h

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <Arduino.h> // min/max/random (RadioLib's generic build omits these)
#include <Mesh.h> #include <Mesh.h>
#include <RadioLib.h> #include <RadioLib.h>

38
variants/xiao_nrf54l15/XiaoNrf54l15Board.h

@ -0,0 +1,38 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
// Minimal MainBoard for the Seeed XIAO nRF54L15 on the lolren bare-metal Arduino
// core. Implements only the mesh::MainBoard pure virtuals plus begin(); battery,
// power management, sleep, OTA etc. are stubbed for bring-up (Phase 4) and filled
// in later phases against the core's APIs.
#ifndef PIN_VBAT_READ
#define PIN_VBAT_READ (PIN_A7) // XIAO nRF54L15: AIN7 / VBAT divider (see core pins_arduino.h)
#endif
class XiaoNrf54l15Board : public mesh::MainBoard {
protected:
uint8_t startup_reason = 0;
public:
void begin() {
startup_reason = 0; // BOOT_REASON normal; refine with hwinfo/reset-cause later
}
uint16_t getBattMilliVolts() override {
// TODO Phase 5: read PIN_VBAT_READ via ADC + the board's divider ratio.
return 0;
}
const char* getManufacturerName() const override {
return "Seeed XIAO nRF54L15";
}
void reboot() override {
NVIC_SystemReset(); // Cortex-M33 system reset
}
uint8_t getStartupReason() const override { return startup_reason; }
};

36
variants/xiao_nrf54l15/target.cpp

@ -0,0 +1,36 @@
#include <Arduino.h>
#include "target.h"
// Variant glue for the XIAO nRF54L15 + LR2021 (definitions).
XiaoNrf54l15Board board;
CustomLR2021 radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
CustomLR2021Wrapper radio_driver(radio, board);
VolatileRTCClock rtc_clock;
SensorManager sensors; // no-op stub (Phase 5 full repeater expects a `sensors` global)
bool radio_init() {
return radio.std_init(&SPI); // sets irqDioNum=8 + begin(tcxoVoltage=0); see CustomLR2021.h
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // new random identity from radio RSSI noise
}

62
variants/xiao_nrf54l15/target.h

@ -0,0 +1,62 @@
#pragma once
// Variant glue for the XIAO nRF54L15 + LR2021 (declarations).
// Mirrors MeshCore's target.h convention: extern the board/radio globals and
// declare the radio_* entry points; target.cpp defines them.
#include <Arduino.h>
#include <Mesh.h> // top-level MeshCore header: makes arduino-cli pull in the MeshCore library
#include <RadioLib.h>
#include <helpers/ArduinoHelpers.h>
// --- Board/radio config. In a full PlatformIO variant these are build flags; ---
// --- defined here (guarded) so the .cpp compile standalone under arduino-cli. ---
// --- MUST precede the CustomLR2021 include: its std_init() expands LORA_*. ---
// MeshCore canonical EU defaults (match platformio.ini arduino_base) so this node
// interoperates with any stock MeshCore EU device, not just the 2nd Wio-LR2021.
// freq/bw/sf/cr MUST match between any two nodes that need to hear each other.
#ifndef LORA_FREQ
#define LORA_FREQ 869.618
#endif
#ifndef LORA_BW
#define LORA_BW 62.5
#endif
#ifndef LORA_SF
#define LORA_SF 8
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 22 // LR2021 LF PA capable; reduce for EU ERP limits / duty cycle as needed
#endif
// LR2021 wiring on the Wio-LR2021 (verified on hardware)
#ifndef P_LORA_NSS
#define P_LORA_NSS PIN_D3
#endif
#ifndef P_LORA_DIO_1
#define P_LORA_DIO_1 PIN_D0
#endif
#ifndef P_LORA_RESET
#define P_LORA_RESET PIN_D2
#endif
#ifndef P_LORA_BUSY
#define P_LORA_BUSY PIN_D1
#endif
#include <helpers/radiolib/CustomLR2021Wrapper.h>
#include "XiaoNrf54l15Board.h"
#include <helpers/SensorManager.h>
// Globals defined in target.cpp
extern XiaoNrf54l15Board board;
extern CustomLR2021Wrapper radio_driver;
extern VolatileRTCClock rtc_clock;
extern SensorManager sensors; // no-op stub (full repeater expects a `sensors` global)
// Radio entry points (the contract the firmware / mesh engine call)
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

5
zephyr-port/.gitignore

@ -0,0 +1,5 @@
# Zephyr build artifacts (we build out-of-tree to /tmp, but ignore any local build dirs)
build/
build-*/
*.hex
*.elf

71
zephyr-port/07_companion/CMakeLists.txt

@ -0,0 +1,71 @@
# Step 4e full MeshCore companion (companion_radio MyMesh) on the Zephyr seams.
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(mc_zephyr_companion)
get_filename_component(MC_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
set(CR "${MC_ROOT}/examples/companion_radio")
set(ALFS "${MC_ROOT}/arch/stm32/Adafruit_LittleFS_stm32/src")
set(RADIOLIB "$ENV{HOME}/Arduino/libraries/RadioLib/src")
set(CRYPTO "$ENV{HOME}/Arduino/libraries/Crypto/src")
set(CAYENNE "$ENV{HOME}/Arduino/libraries/CayenneLPP/src")
set(BASE64 "$ENV{HOME}/Arduino/libraries/base64/src")
set(RTCLIB "$ENV{HOME}/Arduino/libraries/RTClib/src")
set(AJSON "$ENV{HOME}/Arduino/libraries/ArduinoJson/src")
set(ED25519 "${MC_ROOT}/lib/ed25519")
# MeshCore core + helper set (mirrors the Arduino companion curation)
file(GLOB MC_CORE ${MC_ROOT}/src/*.cpp)
set(MC_HELPERS
${MC_ROOT}/src/helpers/BaseChatMesh.cpp
${MC_ROOT}/src/helpers/TransportKeyStore.cpp
${MC_ROOT}/src/helpers/IdentityStore.cpp
${MC_ROOT}/src/helpers/RegionMap.cpp
${MC_ROOT}/src/helpers/ClientACL.cpp
${MC_ROOT}/src/helpers/CommonCLI.cpp
${MC_ROOT}/src/helpers/AdvertDataHelpers.cpp
${MC_ROOT}/src/helpers/TxtDataHelpers.cpp
${MC_ROOT}/src/helpers/StaticPoolPacketManager.cpp
${MC_ROOT}/src/helpers/radiolib/RadioLibWrappers.cpp)
# companion firmware
set(MC_APP ${CR}/MyMesh.cpp ${CR}/DataStore.cpp)
# third-party (generic/non-Arduino mode for RadioLib)
file(GLOB_RECURSE RL_SOURCES ${RADIOLIB}/*.cpp)
list(FILTER RL_SOURCES EXCLUDE REGEX "/hal/Arduino/")
file(GLOB CRYPTO_SRC ${CRYPTO}/*.cpp ${CRYPTO}/*.c)
file(GLOB BASE64_SRC ${BASE64}/*.cpp)
# CayenneLPP + RTClib are shimmed in compat/ (header-only) real libs pull STL/I2C.
file(GLOB ED_SRC ${ED25519}/*.c)
set(ALFS_SRC ${ALFS}/Adafruit_LittleFS.cpp ${ALFS}/Adafruit_LittleFS_File.cpp
${ALFS}/littlefs/lfs.c ${ALFS}/littlefs/lfs_util.c)
target_sources(app PRIVATE
src/main.cpp src/target.cpp src/zephyr_internal_fs.cpp src/serial_ble_interface.cpp
${MC_CORE} ${MC_HELPERS} ${MC_APP}
${RL_SOURCES} ${CRYPTO_SRC} ${BASE64_SRC} ${ED_SRC} ${ALFS_SRC})
target_include_directories(app PRIVATE
compat src ${MC_ROOT}/src ${CR} ${ALFS}
${RADIOLIB} ${CRYPTO} ${BASE64} ${ED25519})
target_compile_definitions(app PRIVATE
NRF54_PLATFORM=1 RADIOLIB_GODMODE=1 MAX_GROUP_CHANNELS=8 MAX_CONTACTS=100 BLE_PIN_CODE=123456
LFS_NO_ASSERT # mount-fail (e.g. stale/repartitioned region) -> graceful format, not abort()
# App TX-power slider max + firmware input check. Pin to the LR2021's absolute (sub-GHz LF)
# PA ceiling of 22 dBm, INDEPENDENT of the band/boot default, so a 2.4 GHz build still allows
# 22 once tuned back to sub-GHz. The real per-band limit (HF caps at +12) is enforced at the
# radio layer by clamp_tx_for_band() in target.cpp. (Otherwise MAX would follow LORA_TX_POWER,
# which MC_BAND_2G4 sets to 12 -> the app would be stuck at 12 even on 869 MHz.)
MAX_LORA_TX_POWER=22)
# 2.4 GHz band: build with `-DMC_BAND_2G4=ON` (e.g. west build -- -DMC_BAND_2G4=ON) to boot on
# the LR2021's 2.4 GHz preset (target.h: 2450 MHz / 500 kHz / SF8 / CR5, TX clamped to +12 dBm)
# instead of the default 865 MHz sub-GHz. The app has no 2.4 GHz preset, so this is how to put
# the node on 2.4 GHz for the over-the-air RF test (the choice then persists in prefs).
option(MC_BAND_2G4 "Boot on the 2.4 GHz LoRa preset instead of sub-GHz" OFF)
if(MC_BAND_2G4)
target_compile_definitions(app PRIVATE MC_BAND_2G4=1)
message(STATUS "MeshCore: building for 2.4 GHz band (2450 MHz / 500 kHz / SF8 / CR5)")
endif()

127
zephyr-port/07_companion/README.md

@ -0,0 +1,127 @@
# MeshCore companion — Seeed XIAO nRF54L15 + LR2021 (Zephyr)
A MeshCore **companion-radio** node (the `examples/companion_radio` firmware, BLE-paired to the
MeshCore phone app) running on the Seeed Studio XIAO nRF54L15 with a Semtech LR2021 radio, built on
**mainline Zephyr** (not NCS). The MeshCore core, helpers, and the companion app are compiled
straight from this repo; the radio driver is stock RadioLib in generic (non-Arduino) mode.
## What's in here
| Path | Role |
|---|---|
| `src/main.cpp` | Zephyr entry point; runs the MeshCore companion loop |
| `src/target.cpp`, `src/target.h` | board/radio bring-up: `LR2021` subclass (RX/length fixes), pin map, band presets, TX-power clamp |
| `src/serial_ble_interface.cpp/.h` | BLE transport: custom encryption-gated Nordic-UART (NUS) GATT service; central-driven pairing |
| `src/zephyr_internal_fs.cpp` | `InternalFileSystem` backed by a Zephyr flash partition (LittleFS) for prefs/identity |
| `compat/` | header-only shims (CayenneLPP, RTClib, Arduino glue) so the core builds without those libs |
| `dts/`, `app.overlay`, `prj.conf` | devicetree overlay (SPI/GPIO for the LR2021) and Kconfig |
The companion also pulls one modified core file, `src/helpers/radiolib/RadioLibWrappers.cpp`
(standby-before-`startReceive` for the LR2021), from the repo root.
## Prerequisites
1. **Mainline Zephyr 4.4.99** (has the `xiao_nrf54l15` board) + its Python venv and the matching
**Zephyr SDK**. A typical layout:
- `ZEPHYR_BASE=$HOME/zephyrproject/zephyr`
- west + deps in a venv, e.g. `$HOME/.zephyr-venv`
2. **Arduino libraries** under `$HOME/Arduino/libraries/` (the `CMakeLists.txt` references them by
that absolute path). Only three are actually compiled/included:
- `RadioLib/` — the LR2021 driver (generic HAL; the Arduino HAL is excluded by the build)
- `Crypto/`
- `base64/`
(`CayenneLPP`, `RTClib`, `ArduinoJson` are *not* needed — they are shimmed in `compat/`.)
`ed25519` ships in the repo (`lib/ed25519`).
## Build
From this directory (`zephyr-port/07_companion`):
```sh
source $HOME/.zephyr-venv/bin/activate
export ZEPHYR_BASE=$HOME/zephyrproject/zephyr
west build -b xiao_nrf54l15/nrf54l15/cpuapp -d build . --pristine
```
Default band is **sub-GHz (869.618 MHz / 62.5 kHz / SF8 / CR5)**. For the **2.4 GHz** preset
(2450 MHz / 500 kHz / SF8 / CR5, TX clamped to +12 dBm) add:
```sh
west build -b xiao_nrf54l15/nrf54l15/cpuapp -d build . --pristine -- -DMC_BAND_2G4=ON
```
`MC_BAND_2G4` only sets the **first-boot** band; the running band/SF/BW/CR is also stored in prefs
and can be changed live from the app (`CMD_SET_RADIO_PARAMS`); and that choice now persists across
reboots. Two nodes must be on the same band to hear each other.
## Flash
The XIAO's on-board debugger enumerates as a **CMSIS-DAP** probe, so the simplest method is
**pyocd**. The pyocd target name is **`nrf54l`** (not `nrf54l15`), and `-e chip` does a clean erase:
```sh
pyocd flash -t nrf54l -e chip build/zephyr/zephyr.hex
pyocd reset -t nrf54l
```
With more than one board attached, add `-u <PROBE_UID>` (from `pyocd list`) so you flash the right
one — `-e chip` erases whatever you point it at.
`west flash` also works if you have J-Link or OpenOCD set up (the board defines both runners; there
is no pyocd runner):
```sh
west flash -d build --runner jlink # or: --runner openocd
```
## Monitor (console / BLE PIN)
The nRF54L15 has **no USB**, so the console (boot log, the BLE PIN, and the `FS:` / `PREFS:` /
`radio_set_params` diagnostics) is read over the SWD probe via **RTT**. The default build sends the
console to a UART; to get RTT, build with a small overlay and read it with pyocd:
```sh
# rtt.conf
CONFIG_USE_SEGGER_RTT=y
CONFIG_RTT_CONSOLE=y
CONFIG_UART_CONSOLE=n
west build -b xiao_nrf54l15/nrf54l15/cpuapp -d build . -- -DEXTRA_CONF_FILE=rtt.conf
pyocd rtt -t nrf54l
```
## Pair
Advertises as **`MeshCore-<node name>`**; e.g. `MeshCore-544BA815` (the pubkey-derived default
name) or `MeshCore-NRF54L15-1` after you rename it; a rename from the app updates the advertised
name **live**, no reboot. In the MeshCore phone app, add a new companion device and pair; the node
uses a fixed passkey **`123456`** (printed on the RTT console at boot). Pairing is **central-driven**
(the app initiates encryption on first access to the NUS characteristics), which keeps iOS/Windows
from dropping the link right after the PIN.
## Notes / limitations
- **Prefs, identity, and contacts persist across power loss; BLE bonds do not**
(`CONFIG_BT_SETTINGS` off). After a node reboot a previously-paired phone must **forget & re-pair**
(it tries to resume a bond the node no longer holds → `0x13` disconnect loop). The correct settings
backend for RRAM is **ZMS** (not NVS; RRAM has no erase); a 12 KB `settings_partition` is already
reserved in `app.overlay` for when this is enabled. Future work (see `prj.conf`).
- **Filesystem persistence depends on `read_size == prog_size == block_size`** in
`zephyr_internal_fs.cpp` (matches the proven bare-metal `helpers/nrf54` FS). With a smaller
prog/read size, littlefs-v1's in-block commit-log path does **not** read back after a cold boot on
RRAM; the FS remounts to its empty post-format state, so identity/prefs silently reset every boot.
Erase is a no-op (RRAM is byte-alterable).
- **`CONFIG_BT_CTLR_ASSERT_OVERHEAD_START=n`** (which needs `CONFIG_BT_CTLR_ADVANCED_FEATURES=y` to be
overridable; it lives in a `visible if` menu). The first BLE advertising event races the boot
bring-up (radio SPI + FS mount) and runs tens of ms late; left at its default `y` the controller
turns that into a **fatal** assert that takes the whole device (and the LoRa mesh) down. Disabled,
the controller just skips the late event and continues. Likewise, a runtime band change
(`radio_set_params`) is kept lightweight so it can't starve the live BLE connection into a fault.
- The NUS characteristics require **authenticated (MITM) pairing** (`BT_GATT_PERM_WRITE_AUTHEN`); a
central that only does Just Works would be rejected. Relax to `_ENCRYPT` in
`serial_ble_interface.cpp` if needed.
- RRAM writes run synchronously (`CONFIG_SOC_FLASH_NRF_RADIO_SYNC_NONE`) to avoid a ~24 s
settings-write stall while BLE-connected.
- The board has no chip-controlled TCXO; the radio runs on the crystal (`tcxoVoltage = 0`).

44
zephyr-port/07_companion/app.overlay

@ -0,0 +1,44 @@
/*
* LR2021 wiring on the XIAO nRF54L15 (matches variants/xiao_nrf54l15/target.h):
* SPI = spi00 (xiao_spi): SCK=P2.1(D8) MOSI=P2.2(D10) MISO=P2.4(D9)
* NSS = P1.7 (D3) DIO1 = P1.4 (D0) RESET = P1.6 (D2) BUSY = P1.5 (D1)
*
* RadioLib drives NSS itself (digitalWrite), so the SPI controller is used WITHOUT
* a hardware CS. The four control pins are plain GPIOs (flags=0 / active-high; the
* HAL uses raw ops so RadioLib's LOW/HIGH map to physical levels, as on Arduino).
*/
&spi00 {
status = "okay";
};
/*
* Split the 36 KB RRAM storage region (0x174000..0x17D000): MeshCore's LittleFS keeps
* 24 KB; a 12 KB NVS settings partition holds BLE bonds (CONFIG_BT_SETTINGS) so the app
* doesn't have to re-pair after every reboot.
*/
&storage_partition {
reg = <0x174000 DT_SIZE_K(24)>;
};
/* add the settings partition as a sibling under the existing fixed-partitions node */
&{/soc/rram-controller@5004b000/rram@0/partitions} {
settings_partition: partition@17a000 {
compatible = "zephyr,mapped-partition";
reg = <0x17a000 DT_SIZE_K(12)>;
label = "settings";
};
};
/ {
chosen {
zephyr,settings-partition = &settings_partition;
};
lora0: lora0 {
compatible = "mc,lora-ctrl";
nss-gpios = <&gpio1 7 0>;
dio1-gpios = <&gpio1 4 0>;
reset-gpios = <&gpio1 6 0>;
busy-gpios = <&gpio1 5 0>;
};
};

107
zephyr-port/07_companion/compat/Arduino.h

@ -0,0 +1,107 @@
/*
* Minimal Arduino-compatibility shim for building MeshCore (and its Arduino-style
* deps: rweather/Crypto, ArduinoHelpers) under Zephyr. Mapped to Zephyr APIs.
* Lives on the include path so MeshCore's `#include <Arduino.h>` resolves here.
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/random/random.h>
typedef uint8_t byte;
typedef bool boolean;
#ifndef HIGH
#define HIGH 1
#define LOW 0
#define INPUT 0
#define OUTPUT 1
#define INPUT_PULLUP 2
#endif
#ifndef min
#define min(a, b) ((a) < (b) ? (a) : (b))
#endif
#ifndef max
#define max(a, b) ((a) > (b) ? (a) : (b))
#endif
#ifndef constrain
#define constrain(x, lo, hi) ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : (x)))
#endif
#ifndef abs
#define abs(x) ((x) > 0 ? (x) : -(x))
#endif
/* Flash-string helpers are no-ops (everything is in RAM/RRAM here). */
#define PROGMEM
#define PSTR(s) (s)
#define F(s) (s)
#define pgm_read_byte(addr) (*(const uint8_t *)(addr))
#define pgm_read_word(addr) (*(const uint16_t *)(addr))
typedef const char *__FlashStringHelper;
#ifdef __cplusplus
#include "Print.h"
#include "Stream.h"
static inline uint32_t millis(void) { return (uint32_t)k_uptime_get(); }
static inline uint32_t micros(void) { return (uint32_t)k_ticks_to_us_floor64(k_uptime_ticks()); }
static inline void delay(uint32_t ms) { k_msleep((int32_t)ms); }
static inline void delayMicroseconds(uint32_t us) { k_busy_wait(us); }
static inline void yield(void) {}
/* newlib-nano / picolibc omit ltoa (MeshCore's TxtDataHelpers uses it). */
#ifndef MC_COMPAT_LTOA
#define MC_COMPAT_LTOA
static inline char *ltoa(long value, char *result, int base) {
if (base < 2 || base > 36) { *result = '\0'; return result; }
char *ptr = result, *ptr1 = result, tmp_char;
long tmp_value;
do {
tmp_value = value;
value /= base;
*ptr++ = "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz"[35 + (tmp_value - value * base)];
} while (value);
if (tmp_value < 0) *ptr++ = '-';
*ptr-- = '\0';
while (ptr1 < ptr) { tmp_char = *ptr; *ptr-- = *ptr1; *ptr1++ = tmp_char; }
return result;
}
#endif
static inline void randomSeed(unsigned long seed) { (void)seed; } /* HW TRNG, seed ignored */
static inline long random(long howbig) {
return howbig > 0 ? (long)(sys_rand32_get() % (uint32_t)howbig) : 0;
}
static inline long random(long howsmall, long howbig) {
return howbig <= howsmall ? howsmall : howsmall + random(howbig - howsmall);
}
/* Serial -> printk shim. Inherits Print (print/println overloads); adds printf. */
class SerialShim : public Stream {
public:
void begin(unsigned long) {}
void end() {}
void flush() {}
explicit operator bool() const { return true; }
using Print::write;
size_t write(uint8_t c) override { printk("%c", c); return 1; }
__attribute__((format(printf, 2, 3)))
int printf(const char *fmt, ...) {
va_list ap; va_start(ap, fmt);
vprintk(fmt, ap);
va_end(ap);
return 0;
}
};
extern SerialShim Serial;
#endif /* __cplusplus */

39
zephyr-port/07_companion/compat/CayenneLPP.h

@ -0,0 +1,39 @@
/*
* Minimal CayenneLPP shim for the Zephyr companion. The companion/SensorManager only
* use reset/addVoltage/getBuffer/getSize; the real lib pulls STL <map> (libstdc++).
* This header-only shim keeps the build on picolibc + the Arduino min/max macros.
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#ifndef MC_LPP_BUF
#define MC_LPP_BUF 64
#endif
class CayenneLPP {
uint8_t _buf[MC_LPP_BUF];
uint8_t _pos;
public:
explicit CayenneLPP(uint8_t size = MC_LPP_BUF) { (void)size; _pos = 0; }
void reset() { _pos = 0; }
uint8_t *getBuffer() { return _buf; }
uint8_t getSize() { return _pos; }
uint8_t getError() { return 0; }
/* Cayenne LPP data channels the companion may emit (channel,type,payload). */
uint8_t addVoltage(uint8_t ch, float v) { return put(ch, 0x74, (int32_t)(v * 100), 2); }
uint8_t addTemperature(uint8_t ch, float t) { return put(ch, 0x67, (int32_t)(t * 10), 2); }
uint8_t addRelativeHumidity(uint8_t ch, float h) { return put(ch, 0x68, (int32_t)(h * 2), 1); }
uint8_t addAnalogInput(uint8_t ch, float a) { return put(ch, 0x02, (int32_t)(a * 100), 2); }
uint8_t addDigitalInput(uint8_t ch, uint32_t d) { return put(ch, 0x00, (int32_t)d, 1); }
private:
uint8_t put(uint8_t ch, uint8_t type, int32_t val, uint8_t nbytes) {
if (_pos + 2 + nbytes > (int)sizeof(_buf)) return 0;
_buf[_pos++] = ch;
_buf[_pos++] = type;
for (int i = nbytes - 1; i >= 0; --i) _buf[_pos++] = (uint8_t)(val >> (8 * i));
return 1;
}
};

60
zephyr-port/07_companion/compat/Print.h

@ -0,0 +1,60 @@
/* Minimal Arduino Print base for the Zephyr compat layer. */
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#define DEC 10
#define HEX 16
class Print {
public:
__attribute__((format(printf, 2, 3)))
int printf(const char *fmt, ...) {
char b[160];
va_list ap; va_start(ap, fmt);
int n = vsnprintf(b, sizeof(b), fmt, ap);
va_end(ap);
if (n > (int)sizeof(b)) n = sizeof(b);
return (int)write((const uint8_t *)b, n < 0 ? 0 : n);
}
virtual ~Print() {}
virtual size_t write(uint8_t c) = 0;
virtual size_t write(const uint8_t *buf, size_t size) {
size_t n = 0;
while (size--) { n += write(*buf++); }
return n;
}
size_t write(const char *s) { return write((const uint8_t *)s, strlen_(s)); }
size_t print(const char *s) { return write(s); }
size_t print(char c) { return write((uint8_t)c); }
size_t print(int v, int base = DEC) { return printNum((long)v, base); }
size_t print(unsigned v, int base = DEC) { return printNum((unsigned long)v, base); }
size_t print(long v, int base = DEC) { return printNum(v, base); }
size_t print(unsigned long v, int base = DEC) { return printNum(v, base); }
size_t print(double v) { char b[32]; int n = snprintf(b, sizeof(b), "%g", v); return write((const uint8_t *)b, n); }
size_t println() { return write((uint8_t)'\n'); }
size_t println(const char *s) { size_t n = print(s); return n + println(); }
size_t println(int v, int base = DEC) { size_t n = print(v, base); return n + println(); }
size_t println(unsigned long v, int base = DEC) { size_t n = print(v, base); return n + println(); }
size_t println(double v) { size_t n = print(v); return n + println(); }
private:
static size_t strlen_(const char *s) { size_t n = 0; while (s[n]) n++; return n; }
size_t printNum(long v, int base) {
char b[24];
int n = (base == HEX) ? snprintf(b, sizeof(b), "%lx", (unsigned long)v)
: snprintf(b, sizeof(b), "%ld", v);
return write((const uint8_t *)b, n);
}
size_t printNum(unsigned long v, int base) {
char b[24];
int n = (base == HEX) ? snprintf(b, sizeof(b), "%lx", v)
: snprintf(b, sizeof(b), "%lu", v);
return write((const uint8_t *)b, n);
}
};

25
zephyr-port/07_companion/compat/RTClib.h

@ -0,0 +1,25 @@
/*
* Minimal RTClib shim for the Zephyr companion. MeshCore only uses DateTime (epoch
* <-> Y/M/D h:m:s); the real RTClib pulls Adafruit_I2CDevice/Wire. Header-only.
*/
#pragma once
#include <stdint.h>
#include <time.h>
class DateTime {
uint32_t _epoch;
struct tm _tm;
public:
DateTime(uint32_t t = 0) : _epoch(t) {
time_t tt = (time_t)t;
struct tm *r = gmtime(&tt);
if (r) _tm = *r; else { _tm = {}; }
}
uint32_t unixtime() const { return _epoch; }
uint16_t year() const { return (uint16_t)(_tm.tm_year + 1900); }
uint8_t month() const { return (uint8_t)(_tm.tm_mon + 1); }
uint8_t day() const { return (uint8_t)_tm.tm_mday; }
uint8_t hour() const { return (uint8_t)_tm.tm_hour; }
uint8_t minute() const { return (uint8_t)_tm.tm_min; }
uint8_t second() const { return (uint8_t)_tm.tm_sec; }
};

16
zephyr-port/07_companion/compat/Stream.h

@ -0,0 +1,16 @@
/* Minimal Arduino Stream base for the Zephyr compat layer. */
#pragma once
#include "Print.h"
class Stream : public Print {
public:
virtual int available() { return 0; }
virtual int read() { return -1; }
virtual int peek() { return -1; }
virtual void flush() {}
virtual size_t readBytes(uint8_t *buf, size_t len) {
size_t n = 0;
while (n < len) { int c = read(); if (c < 0) break; buf[n++] = (uint8_t)c; }
return n;
}
};

20
zephyr-port/07_companion/dts/bindings/mc-lora-ctrl.yaml

@ -0,0 +1,20 @@
description: |
LR2021 control GPIOs driven directly by RadioLib (NSS/DIO1/RESET/BUSY).
Not a real device node; just a holder so GPIO_DT_SPEC_GET generates the
pin/flags cell macros for these phandle-array properties.
compatible: "mc,lora-ctrl"
properties:
nss-gpios:
type: phandle-array
required: true
dio1-gpios:
type: phandle-array
required: true
reset-gpios:
type: phandle-array
required: true
busy-gpios:
type: phandle-array
required: true

70
zephyr-port/07_companion/prj.conf

@ -0,0 +1,70 @@
# Step 4e — full MeshCore companion on Zephyr.
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="MeshCore"
CONFIG_BT_DEVICE_NAME_DYNAMIC=y
CONFIG_BT_DEVICE_NAME_MAX=40
CONFIG_BT_SMP=y
CONFIG_BT_BONDABLE=y
CONFIG_BT_FIXED_PASSKEY=y
# BT_MAX_PAIRED defaults to 1 (= BT_MAX_CONN). Phone A bonds -> the single bond slot fills;
# phone B then connects but its (MITM-enforced) pairing has no free slot and FAILS, so B can't
# reach the encryption-gated NUS -> "needs a hard reset to connect another device". Let a new
# pairing recycle the oldest bond instead. (Bonds are RAM-only here, BT_SETTINGS off, so this
# just means each new phone re-pairs cleanly.)
CONFIG_BT_KEYS_OVERWRITE_OLDEST=y
# NUS: we define our own encryption-gated NUS GATT service in serial_ble_interface.cpp
# (so the central drives pairing). Zephyr's built-in NUS is disabled to avoid a duplicate
# service with the same UUIDs.
# CONFIG_BT_ZEPHYR_NUS=y
CONFIG_BT_L2CAP_TX_MTU=247
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_BUF_ACL_RX_SIZE=251
# At boot the first BLE advertising event collides with the bring-up storm (LR2021 SPI
# init + LittleFS mount + mesh begin), so the SW-split controller's prepare callback runs
# tens of ms late on that one event. BT_CTLR_ASSERT_OVERHEAD_START (default y, a debug aid)
# turns that latency into a FATAL assert (lll_adv.c LL_ASSERT_OVERHEAD), which crashed the
# whole device -- taking the LoRa mesh down with it (no advert RX, looked like a reset loop).
# Disabling it restores the controller's designed behaviour: gracefully skip the delayed
# event (radio_disable + -ECANCELED) and keep running.
# NOTE: ASSERT_OVERHEAD_START lives in the controller's "Advanced features" menu, which is
# `visible if BT_CTLR_ADVANCED_FEATURES`. Without that gate enabled the prompt is hidden and
# a `=n` is silently ignored (symbol stays at its default y). Enable the gate (visibility
# only, no other behaviour change) so the override below actually takes effect.
CONFIG_BT_CTLR_ADVANCED_FEATURES=y
CONFIG_BT_CTLR_ASSERT_OVERHEAD_START=n
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
# RRAM is byte-alterable and writes (no long erase like NOR flash). The flash driver
# otherwise defaults to RADIO_SYNC_TICKER (because BT_LL_SW_SPLIT), which schedules every
# write into BLE idle timeslots. While connected that dribbles a savePrefs() out one tiny
# window per connection event => a fixed 24s per settings write. Disable radio sync: RRAM
# writes are short enough to run synchronously without disturbing the radio (also bumps the
# RRAM write-buffer 1 -> 32, fewer commits).
CONFIG_SOC_FLASH_NRF_RADIO_SYNC_NONE=y
# Bond persistence DISABLED: on the vanilla open-source controller it broke bonded
# reconnect (phone re-pairs on every reconnect -> disconnect loop) and earlier overflowed
# the bond-save stack. Each connect is a fresh pair (the known-good Android-working flow).
# Robust persistence needs NCS + Nordic SoftDevice Controller (handles bonded-RPA reconnect).
# CONFIG_BT_SETTINGS=y
# CONFIG_SETTINGS=y
# CONFIG_NVS=y
# CONFIG_SETTINGS_NVS=y
# (stacks kept harmless, headroom for the BT/workqueue paths)
CONFIG_BT_RX_STACK_SIZE=4096
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096
CONFIG_GPIO=y
CONFIG_SPI=y
CONFIG_CPP=y
CONFIG_STD_CPP17=y
CONFIG_CPP_EXCEPTIONS=n
CONFIG_PRINTK=y
CONFIG_CBPRINTF_FP_SUPPORT=y
CONFIG_MAIN_STACK_SIZE=16384
CONFIG_HEAP_MEM_POOL_SIZE=16384
CONFIG_FPU=y
CONFIG_REBOOT=y
CONFIG_ENTROPY_GENERATOR=y

69
zephyr-port/07_companion/src/main.cpp

@ -0,0 +1,69 @@
/*
* Step 4e: the full MeshCore companion on Zephyr. Wires companion_radio's MyMesh +
* DataStore onto the proven Zephyr seams: radio (target.cpp / RadioLib+ZephyrHal),
* filesystem (InternalFS / flash_area), and serial = the bonded bt_nus SerialBLEInterface.
* The phone app pairs (level 4, fixed PIN), then talks the companion protocol over NUS.
*/
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <string.h>
#include <Arduino.h>
#include <target.h>
#include <helpers/nrf54/InternalFileSystem.h>
#include <helpers/SimpleMeshTables.h>
#include "serial_ble_interface.h"
#include "MyMesh.h" /* examples/companion_radio (on the include path) */
SerialShim Serial;
StdRNG fast_rng;
SimpleMeshTables tables;
DataStore store(InternalFS, rtc_clock);
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store);
int main(void)
{
printk("\n=== MeshCore companion on Zephyr (board=%s) ===\n", CONFIG_BOARD);
board.begin();
if (!radio_init()) { printk("radio_init FAILED\n"); return 0; }
fast_rng.begin(radio_get_rng_seed());
if (!InternalFS.begin()) { printk("InternalFS FAILED\n"); return 0; }
store.begin();
the_mesh.begin(false); /* no display (loads prefs from /new_prefs) */
/* DEBUG: did the node name survive the last power cycle? On a cold boot this should show
* the previously-set name and exists=1; if it shows the default name / exists=0 the prefs
* file was lost (reformat or never persisted). Paired with the "FS: mounted OK/FAILED" line. */
printk("PREFS: cold boot -> /new_prefs exists=%d, node_name='%s'\n",
InternalFS.exists("/new_prefs"), the_mesh.getNodePrefs()->node_name);
char name[48];
snprintf(name, sizeof(name), "%s%s", BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name);
ble.begin(name, the_mesh.getBLEPin());
the_mesh.startInterface(ble);
printk("companion up: '%s' pin %lu — connect from the MeshCore app\n",
name, (unsigned long)the_mesh.getBLEPin());
while (1) {
the_mesh.loop();
rtc_clock.tick();
sensors.loop();
/* A rename from the app (CMD_SET_ADVERT_NAME) only updates prefs; push it to BLE so
* the advertised/GAP name follows without a reboot. */
char cur[48];
snprintf(cur, sizeof(cur), "%s%s", BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name);
if (strcmp(cur, name) != 0) {
strcpy(name, cur);
ble.setDeviceName(name);
/* DEBUG: confirm the rename reached the FS (savePrefs ran in the CMD handler).
* exists=1 means /new_prefs was (re)written; if it's still 0 the save never landed. */
printk("PREFS: rename -> '%s', /new_prefs exists=%d\n",
the_mesh.getNodePrefs()->node_name, InternalFS.exists("/new_prefs"));
}
k_msleep(1); /* snappy serial/radio polling for the app's frame bursts */
}
return 0;
}

293
zephyr-port/07_companion/src/serial_ble_interface.cpp

@ -0,0 +1,293 @@
#include "serial_ble_interface.h"
#include <zephyr/sys/printk.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/settings/settings.h>
#include <string.h>
SerialBLEInterface ble;
#ifndef BLE_DBG
#define BLE_DBG(...) printk("BLE: " __VA_ARGS__)
#endif
#ifndef MC_BLE_VERBOSE
#define MC_BLE_VERBOSE 1 /* dump every companion frame in/out */
#endif
#if MC_BLE_VERBOSE
static void mc_dump(const char *dir, const uint8_t *b, uint16_t len)
{
printk("[%8lld] %s code=%-3u len=%-3u :", (long long)k_uptime_get(), dir, len ? b[0] : 0, len);
for (uint16_t i = 0; i < len && i < 28; i++) printk(" %02x", b[i]);
printk("%s\n", len > 28 ? " ..." : "");
}
#else
#define mc_dump(d, b, l)
#endif
/* advertising payload: name + NUS 128-bit UUID (so the MeshCore app finds us).
* g_ad[1] (the scan-list name) is rewritten at begin() from the runtime node name;
* CONFIG_BT_DEVICE_NAME is only a placeholder. bt_set_name() sets the GAP Device Name
* *characteristic* but NOT this advertising payload, so without the rewrite the node
* always advertised the static "MeshCore" regardless of the configured node name. */
static char g_adv_name[27]; /* 31B adv - 3B flags - 2B AD header => 26 name chars + NUL */
static struct bt_data g_ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
static const struct bt_data g_sd[] = {
BT_DATA_BYTES(BT_DATA_UUID128_ALL,
BT_UUID_128_ENCODE(0x6E400001, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E)),
};
/* custom Nordic-UART service, ENCRYPTION-GATED so the *central* drives pairing
* Zephyr's stock bt_nus characteristics are PERM_NONE, which forced us to send a peripheral
* SMP Security Request on connect; iOS/Windows respond to that by pairing then dropping &
* reconnecting. Gating the RX-write and the TX CCC on *authenticated* encryption makes the
* central initiate MITM (passkey) pairing on first access and stay on the same link. */
static struct bt_uuid_128 nus_srv_uuid = BT_UUID_INIT_128(
BT_UUID_128_ENCODE(0x6E400001, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E));
static struct bt_uuid_128 nus_tx_uuid = BT_UUID_INIT_128( /* notify: NODE->APP */
BT_UUID_128_ENCODE(0x6E400003, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E));
static struct bt_uuid_128 nus_rx_uuid = BT_UUID_INIT_128( /* write: APP->NODE */
BT_UUID_128_ENCODE(0x6E400002, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E));
static ssize_t nus_rx_write(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{
(void)conn; (void)attr; (void)offset; (void)flags;
ble._onRx(buf, len);
return len;
}
static void nus_tx_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value) { (void)attr; (void)value; }
/* attrs[0]=svc [1]=TX chrc [2]=TX value [3]=CCC [4]=RX chrc [5]=RX value;
* notify on attrs[1] (mirrors Zephyr's bt_nus_send). */
BT_GATT_SERVICE_DEFINE(nus_svc,
BT_GATT_PRIMARY_SERVICE(&nus_srv_uuid),
BT_GATT_CHARACTERISTIC(&nus_tx_uuid.uuid, BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_NONE, NULL, NULL, NULL),
BT_GATT_CCC(nus_tx_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_AUTHEN),
BT_GATT_CHARACTERISTIC(&nus_rx_uuid.uuid,
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP,
BT_GATT_PERM_WRITE_AUTHEN, NULL, nus_rx_write, NULL),
);
/* C BLE callbacks bridging to the single `ble` instance */
static void cb_connected(struct bt_conn *conn, uint8_t err)
{
if (err) { BLE_DBG("connect failed 0x%02x\n", err); return; }
BLE_DBG("connected\n");
ble._onConnect(conn);
/* Do NOT initiate security here. The NUS characteristics are gated on authenticated
* encryption, so the CENTRAL triggers MITM pairing on first access and keeps the link.
* A peripheral-initiated Security Request is what made iOS/Windows pair-then-reconnect. */
}
static void cb_disconnected(struct bt_conn *conn, uint8_t reason)
{
BLE_DBG("disconnected 0x%02x\n", reason);
ble._onDisconnect(conn);
}
static void cb_security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err)
{
BLE_DBG("security level %d err %d\n", level, err);
ble._onSecured(conn, err == BT_SECURITY_ERR_SUCCESS && level >= BT_SECURITY_L2);
}
BT_CONN_CB_DEFINE(conn_cbs) = {
.connected = cb_connected,
.disconnected = cb_disconnected,
.security_changed = cb_security_changed,
};
static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey)
{
BLE_DBG(">>> enter PASSKEY on phone: %06u <<<\n", passkey);
}
static void auth_cancel(struct bt_conn *conn) { BLE_DBG("pairing cancelled\n"); }
static struct bt_conn_auth_cb auth_cb = {
.passkey_display = auth_passkey_display,
.cancel = auth_cancel,
};
static void pairing_complete(struct bt_conn *conn, bool bonded)
{
BLE_DBG("*** PAIRING COMPLETE bonded=%d ***\n", bonded);
}
static void pairing_failed(struct bt_conn *conn, enum bt_security_err reason)
{
BLE_DBG("!!! PAIRING FAILED reason=%d !!!\n", reason);
}
static struct bt_conn_auth_info_cb auth_info_cb = {
.pairing_complete = pairing_complete,
.pairing_failed = pairing_failed,
};
/* --- SerialBLEInterface impl --- */
void SerialBLEInterface::begin(const char *device_name, uint32_t pin_code)
{
k_mutex_init(&_lock);
int err = bt_enable(NULL);
if (err) { BLE_DBG("bt_enable %d\n", err); return; }
if (IS_ENABLED(CONFIG_SETTINGS)) {
settings_load(); /* restore bonds saved by CONFIG_BT_SETTINGS */
}
bt_passkey_set(pin_code);
bt_conn_auth_cb_register(&auth_cb);
bt_conn_auth_info_cb_register(&auth_info_cb);
setDeviceName(device_name);
BLE_DBG("init done; advertising as '%s' pin %06u\n", bt_get_name(), pin_code);
}
void SerialBLEInterface::setDeviceName(const char *device_name)
{
if (!device_name || !*device_name) return;
bt_set_name(device_name); /* GAP Device Name characteristic (read after connect) */
/* Mirror the name into the advertising payload (the scan-list name the phone shows),
* truncating to the 26-char adv budget. Without this the scan list always read
* "MeshCore", and a runtime rename never reached the advertisement at all. The new
* g_ad[] is picked up by the next mc_start_adv(); if idle, refresh the live advertisement now. */
size_t n = strlen(device_name);
bool shortened = n > sizeof(g_adv_name) - 1;
if (shortened) n = sizeof(g_adv_name) - 1;
memcpy(g_adv_name, device_name, n);
g_adv_name[n] = '\0';
g_ad[1].type = shortened ? BT_DATA_NAME_SHORTENED : BT_DATA_NAME_COMPLETE;
g_ad[1].data = (const uint8_t *)g_adv_name;
g_ad[1].data_len = n;
BLE_DBG("device name set to '%s'\n", g_adv_name);
if (_enabled && _conn == nullptr && _advertising) {
bt_le_adv_update_data(g_ad, ARRAY_SIZE(g_ad), g_sd, ARRAY_SIZE(g_sd));
}
}
static int mc_start_adv(void)
{
return bt_le_adv_start(BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONN,
BT_GAP_ADV_FAST_INT_MIN_2,
BT_GAP_ADV_FAST_INT_MAX_2, NULL),
g_ad, ARRAY_SIZE(g_ad), g_sd, ARRAY_SIZE(g_sd));
}
void SerialBLEInterface::enable()
{
if (_enabled) return;
_enabled = true;
int err = mc_start_adv();
_advertising = (err == 0);
BLE_DBG("adv start -> %d\n", err);
}
void SerialBLEInterface::disable()
{
_enabled = false;
bt_le_adv_stop();
if (_conn) bt_conn_disconnect((struct bt_conn *)_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
bool SerialBLEInterface::isConnected() const { return _conn != nullptr && _secured; }
bool SerialBLEInterface::isWriteBusy() const { return _send_len >= (QSZ * 2 / 3); }
void SerialBLEInterface::_onConnect(void *conn)
{
_conn = bt_conn_ref((struct bt_conn *)conn);
_secured = false;
_advertising = false; /* Zephyr stops connectable adv once a connection forms */
_send_len = 0;
k_mutex_lock(&_lock, K_FOREVER); _recv_len = 0; k_mutex_unlock(&_lock);
}
void SerialBLEInterface::_onDisconnect(void *conn)
{
if (_conn) { bt_conn_unref((struct bt_conn *)_conn); _conn = nullptr; }
_secured = false;
/* re-advertise is handled in checkRecvFrame() (main thread) avoid BT calls here */
}
void SerialBLEInterface::_onSecured(void *conn, bool ok)
{
(void)conn;
_secured = ok;
/* NOTE: previously requested a conn interval here for Android latency, but an
* unsolicited param update right after pairing stalls iOS .
* Let the central manage connection parameters. */
}
void SerialBLEInterface::_onRx(const void *data, uint16_t len)
{
if (len == 0 || len > MAX_FRAME_SIZE) return;
mc_dump("APP->NODE", (const uint8_t *)data, len);
k_mutex_lock(&_lock, K_FOREVER);
if (_recv_len < QSZ) {
_recv[_recv_len].len = len;
memcpy(_recv[_recv_len].buf, data, len);
_recv_len++;
} else {
printk("BLE: RX queue FULL, dropping frame\n");
}
k_mutex_unlock(&_lock);
}
void SerialBLEInterface::shiftSend()
{
if (_send_len > 0) {
_send_len--;
for (uint8_t i = 0; i < _send_len; i++) _send[i] = _send[i + 1];
}
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len)
{
if (len == 0 || len > MAX_FRAME_SIZE) return 0;
if (!isConnected() || _send_len >= QSZ) return 0;
_send[_send_len].len = len;
memcpy(_send[_send_len].buf, src, len);
_send_len++;
return len;
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[])
{
/* drain queued TX frames over NUS. Push as many as the controller will take this
* call (multiple notifications per connection event) instead of one per loop tick;
* stop on -ENOMEM (buffers full, retry next call). Speeds up multi-frame answers
* (settings/contacts) without touching connection parameters. */
if (_send_len > 0) {
if (!isConnected()) {
_send_len = 0;
} else {
while (_send_len > 0) {
int rc = bt_gatt_notify((struct bt_conn *)_conn, &nus_svc.attrs[1], _send[0].buf, _send[0].len);
if (rc == 0) { mc_dump("NODE->APP", _send[0].buf, _send[0].len); shiftSend(); }
else if (rc == -ENOTCONN) { shiftSend(); break; } /* gone */
else { printk("BLE: bt_nus_send rc=%d (retry)\n", rc); break; } /* -ENOMEM: buffers full */
}
}
}
/* return one received frame */
size_t out = 0;
k_mutex_lock(&_lock, K_FOREVER);
if (_recv_len > 0) {
out = _recv[0].len;
memcpy(dest, _recv[0].buf, out);
_recv_len--;
for (uint8_t i = 0; i < _recv_len; i++) _recv[i] = _recv[i + 1];
}
k_mutex_unlock(&_lock);
/* re-advertise after a disconnect so the app can reconnect without a board reset */
if (_enabled && _conn == nullptr && !_advertising) {
unsigned long now = k_uptime_get();
if (now - _last_adv_try > 500) { /* throttle retries */
_last_adv_try = now;
int rc = mc_start_adv();
if (rc == 0) {
_advertising = true;
BLE_DBG("re-advertising after disconnect\n");
} else {
BLE_DBG("re-advertise FAILED rc=%d (will retry)\n", rc);
}
}
}
return out;
}

58
zephyr-port/07_companion/src/serial_ble_interface.h

@ -0,0 +1,58 @@
/*
* Zephyr SerialBLEInterface: MeshCore's BaseSerialInterface over the Zephyr NUS
* (bt_nus) + the step-2 bonded pairing (LE SC + MITM + fixed PIN). This is the
* serial seam the companion role will use; the companion-protocol bytes ride
* the encrypted, bonded NUS link that the bare-metal core could never establish.
*/
#pragma once
#include <Arduino.h>
#include <helpers/BaseSerialInterface.h>
#include <zephyr/kernel.h>
class SerialBLEInterface : public BaseSerialInterface {
public:
/* prefix+name -> advertised name; pin_code = fixed BLE passkey. */
void begin(const char *device_name, uint32_t pin_code);
/* Apply (or re-apply at runtime) the BLE name: sets the GAP Device Name characteristic
* AND the advertising payload, so a rename takes effect on the next advertise without a
* reboot. Safe to call while connected (the new name is used on the next re-advertise). */
void setDeviceName(const char *device_name);
void enable() override;
void disable() override;
bool isEnabled() const override { return _enabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
/* invoked from the C BLE callbacks (see .cpp) */
void _onConnect(void *conn);
void _onDisconnect(void *conn);
void _onSecured(void *conn, bool ok);
void _onRx(const void *data, uint16_t len);
private:
struct Frame {
uint16_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
static const uint8_t QSZ = 12;
Frame _send[QSZ];
uint8_t _send_len = 0; /* main-thread only */
Frame _recv[QSZ];
uint8_t _recv_len = 0; /* filled in BT ctx, read in main -> guarded by _lock */
struct k_mutex _lock;
void *_conn = nullptr; /* struct bt_conn* */
bool _enabled = false;
bool _secured = false;
bool _advertising = false;
unsigned long _last_adv_try = 0;
void shiftSend();
};
extern SerialBLEInterface ble;

147
zephyr-port/07_companion/src/target.cpp

@ -0,0 +1,147 @@
#include "target.h"
#include "zephyr_radiolib_hal.h"
#include <helpers/radiolib/RadioLibWrappers.h>
#include <RadioLib.h>
#include <zephyr/sys/reboot.h>
#define LR2021_IRQ_DIO 8
#ifndef LR2021_RX_BOOST_LEVEL
#define LR2021_RX_BOOST_LEVEL 7 /* matches Semtech usp_zephyr rx-boost-cfg / CustomLR2021 */
#endif
static ZephyrHal hal;
class LR2021Z : public LR2021 {
public:
explicit LR2021Z(Module *m) : LR2021(m) {}
void setIrqDio(uint8_t n) { this->irqDioNum = n; }
/* Match the known-good bare-metal CustomLR2021: on a corrupted LoRa header the
* stock driver returns len 0 and can leave RX half-wedged; force standby so the
* wrapper re-arms cleanly. */
size_t getPacketLength(bool update) override {
size_t len = LR2021::getPacketLength(update);
if (len == 0 && (getIrqFlags() & RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR)) {
standby();
}
return len;
}
int16_t startReceive() override {
setLoRaPacketParams(this->preambleLengthLoRa, this->headerType,
RADIOLIB_LR2021_MAX_PACKET_LENGTH, this->crcTypeLoRa,
this->invertIQEnabled);
return LR2021::startReceive();
}
};
static Module s_mod(&hal, LR_PIN_NSS, LR_PIN_DIO1, LR_PIN_RESET, LR_PIN_BUSY);
static LR2021Z s_lora(&s_mod);
ZBoard board; /* defined before the radio wrapper that references it */
class LR2021Mesh : public RadioLibWrapper {
public:
LR2021Mesh(LR2021 &r, mesh::MainBoard &b) : RadioLibWrapper(r, b) {}
bool isReceivingPacket() override {
uint32_t irq = ((LR2021 *)_radio)->getIrqFlags();
return (irq & RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED) ||
(irq & RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID);
}
float getCurrentRSSI() override { return ((LR2021 *)_radio)->getRSSI(); }
};
static LR2021Mesh s_radio(s_lora, board);
RadioLibWrapper &radio_driver = s_radio;
VolatileRTCClock rtc_clock;
SensorManager sensors;
mesh::LocalIdentity radio_new_identity()
{
RadioNoiseListener rng(s_lora);
return mesh::LocalIdentity(&rng); /* new identity from LoRa RSSI noise */
}
void ZBoard::reboot() { sys_reboot(SYS_REBOOT_COLD); }
#define MC_HF_CUTOFF_MHZ 1500.0f
static float s_freq = LORA_FREQ;
static int8_t s_req_dbm = LORA_TX_POWER; /* last app-requested power, pre-clamp */
static int8_t clamp_tx_for_band(float freq, int8_t dbm)
{
if (freq > MC_HF_CUTOFF_MHZ) /* 2.4 GHz HF PA: -19..+12 */
return dbm > 12 ? 12 : (dbm < -19 ? -19 : dbm);
return dbm > 22 ? 22 : (dbm < -9 ? -9 : dbm); /* sub-GHz LF PA: -9..+22 */
}
static bool radio_bringup(float freq, float bw, uint8_t sf, uint8_t cr)
{
for (int attempt = 0; attempt < 8; attempt++) {
int16_t st = s_lora.begin(freq, bw, sf, cr,
RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE,
clamp_tx_for_band(freq, s_req_dbm), 16, /*tcxoVoltage=*/0.0f);
if (st == RADIOLIB_ERR_NONE) {
/* match CustomLR2021's proven config: explicit header, CRC, RX boosted gain */
s_lora.explicitHeader();
s_lora.setCRC(2);
s_lora.setRxBoostedGainMode(LR2021_RX_BOOST_LEVEL);
int16_t rx = s_lora.startReceive(); /* the operation that fails on a bad boot */
if (rx == RADIOLIB_ERR_NONE) {
s_lora.standby(); /* idle; the wrapper arms RX in its loop */
s_freq = freq;
if (attempt) printk("radio: RX ok after %d retr%s\n",
attempt, attempt == 1 ? "y" : "ies");
return true;
}
printk("radio: begin ok but RX arm failed (%d), resetting\n", rx);
} else {
printk("radio: begin failed (%d), resetting\n", st);
}
s_lora.reset(); /* full chip reset, then retry the whole bring-up */
k_msleep(50);
}
return false;
}
bool radio_init()
{
s_lora.setIrqDio(LR2021_IRQ_DIO);
s_freq = LORA_FREQ;
s_req_dbm = LORA_TX_POWER;
if (!radio_bringup(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR)) return false;
s_radio.begin();
return true;
}
uint32_t radio_get_rng_seed() { return s_lora.random(0x7FFFFFFF); }
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr)
{
bool band_changed = (freq > MC_HF_CUTOFF_MHZ) != (s_freq > MC_HF_CUTOFF_MHZ);
s_lora.standby();
int16_t st = s_lora.setFrequency(freq); /* recalibrates the front-end for the band */
if (st == RADIOLIB_ERR_NONE) st = s_lora.setBandwidth(bw);
if (st == RADIOLIB_ERR_NONE) st = s_lora.setSpreadingFactor(sf);
if (st == RADIOLIB_ERR_NONE) st = s_lora.setCodingRate(cr);
s_freq = freq;
if (band_changed) s_lora.setOutputPower(clamp_tx_for_band(freq, s_req_dbm));
s_radio.begin(); /* reset wrapper -> dispatcher re-arms RX (startReceive) on the new config */
if (st != RADIOLIB_ERR_NONE) {
printk("radio_set_params: apply error %d for %u kHz bw=%u kHz sf%u cr%u\n",
st, (unsigned)(freq * 1000.0f), (unsigned)(bw * 1000.0f), sf, cr);
} else {
printk("radio_set_params: now on %u kHz bw=%u kHz sf%u cr%u\n",
(unsigned)(freq * 1000.0f), (unsigned)(bw * 1000.0f), sf, cr);
}
}
void radio_set_tx_power(int8_t dbm)
{
s_req_dbm = dbm;
s_lora.setOutputPower(clamp_tx_for_band(s_freq, dbm));
}

80
zephyr-port/07_companion/src/target.h

@ -0,0 +1,80 @@
/*
* Zephyr variant glue for the XIAO nRF54L15 + LR2021: the target.h MeshCore's
* companion expects (board / radio_driver / rtc_clock / sensors + radio_* funcs +
* LoRa/BLE config). The concrete radio seam (RadioLib LR2021 + ZephyrHal) lives in
* target.cpp so its headers (with static-member defs) aren't multiply-included.
*/
#pragma once
#include <Arduino.h>
#include <Mesh.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/SensorManager.h>
#include <helpers/radiolib/RadioLibWrappers.h> /* RadioLibWrapper API MyMesh uses */
/* 2.4 GHz preset (LR2021 is dual-band)
* The RadioLib driver auto-switches to the HF path for any frequency in 2400-2500 MHz;
* target.cpp clamps TX power to the HF PA max of +12 dBm. NOTE: MeshCore's companion caps
* bandwidth at 500 kHz, so 812/406 (which the chip supports) are rejected by SET_RADIO_PARAMS. */
#define LORA_FREQ_2G4 2450.0f
#define LORA_BW_2G4 500.0f /* kHz */
#define LORA_SF_2G4 8
#define LORA_CR_2G4 5
#define LORA_TX_POWER_2G4 12 /* dBm — HF PA maximum */
/* LoRa boot defaults: MeshCore EU sub-GHz canon, unless the build defines MC_BAND_2G4 (see
* CMakeLists), which boots straight onto the 2.4 GHz preset above. 865 MHz stays the normal
* build; a 2G4 build needs no app interaction (and the choice persists in prefs). The app
* has no 2.4 GHz preset of its own, presets are app-side and sub-GHz only, so this flag is
* the practical way to put the node on 2.4 GHz for the RF test. */
#ifdef MC_BAND_2G4
#define LORA_FREQ LORA_FREQ_2G4
#define LORA_BW LORA_BW_2G4
#define LORA_SF LORA_SF_2G4
#define LORA_CR LORA_CR_2G4
#define LORA_TX_POWER LORA_TX_POWER_2G4
#else
#ifndef LORA_FREQ
#define LORA_FREQ 869.618f
#endif
#ifndef LORA_BW
#define LORA_BW 62.5f
#endif
#ifndef LORA_SF
#define LORA_SF 8
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 22
#endif
#endif
/* BLE companion */
#ifndef BLE_PIN_CODE
#define BLE_PIN_CODE 123456
#endif
#ifndef BLE_NAME_PREFIX
#define BLE_NAME_PREFIX "MeshCore-"
#endif
class ZBoard : public mesh::MainBoard {
public:
void begin() {}
uint16_t getBattMilliVolts() override { return 0; }
const char *getManufacturerName() const override { return "XIAO nRF54L15"; }
void reboot() override; /* sys_reboot — defined in target.cpp */
uint8_t getStartupReason() const override { return _reason; }
uint8_t _reason = 0;
};
extern ZBoard board;
extern RadioLibWrapper &radio_driver; /* concrete LR2021 wrapper bound in target.cpp */
extern VolatileRTCClock rtc_clock;
extern SensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

55
zephyr-port/07_companion/src/zephyr_internal_fs.cpp

@ -0,0 +1,55 @@
/*
* Zephyr backing for MeshCore's nrf54 InternalFileSystem (the header MeshCore's
* NRF54_PLATFORM branch includes). Same Adafruit_LittleFS class, lfs block device
* over Zephyr flash_area on `storage_partition` (validated in zephyr-port/06_fs).
* Compile THIS instead of the bare-metal RRAMC InternalFileSystem.cpp.
*/
#include <helpers/nrf54/InternalFileSystem.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/sys/printk.h>
#include <string.h>
#define FS_PARTITION storage_partition
#define BS 2048 /* littlefs logical block (== read/prog size) */
#define BC 12 /* 24 KB storage_partition / 2048 */
static const struct flash_area *g_fa = NULL;
static int _fa_read(const struct lfs_config *c, lfs_block_t b, lfs_off_t o, void *buf, lfs_size_t sz) {
(void)c; return flash_area_read(g_fa, (off_t)b * BS + o, buf, sz) == 0 ? LFS_ERR_OK : LFS_ERR_IO;
}
static int _fa_prog(const struct lfs_config *c, lfs_block_t b, lfs_off_t o, const void *buf, lfs_size_t sz) {
(void)c; return flash_area_write(g_fa, (off_t)b * BS + o, buf, sz) == 0 ? LFS_ERR_OK : LFS_ERR_IO;
}
static int _fa_erase(const struct lfs_config *c, lfs_block_t b) {
(void)c; (void)b;
return LFS_ERR_OK;
}
static int _fa_sync(const struct lfs_config *c) { (void)c; return LFS_ERR_OK; }
struct lfs_config _InternalFSConfig = {
.context = NULL,
.read = _fa_read, .prog = _fa_prog, .erase = _fa_erase, .sync = _fa_sync,
.read_size = BS, .prog_size = BS, .block_size = BS, .block_count = BC, .lookahead = 128,
.read_buffer = NULL, .prog_buffer = NULL, .lookahead_buffer = NULL, .file_buffer = NULL
};
InternalFileSystem InternalFS;
InternalFileSystem::InternalFileSystem(void) : Adafruit_LittleFS(&_InternalFSConfig) {}
bool InternalFileSystem::begin(void) {
if (g_fa == NULL && flash_area_open(FIXED_PARTITION_ID(FS_PARTITION), &g_fa) != 0) {
printk("FS: flash_area_open FAILED\n");
return false;
}
if (!Adafruit_LittleFS::begin()) {
printk("FS: mount FAILED -> reformatting (any persisted prefs/contacts are lost)\n");
this->format();
if (!Adafruit_LittleFS::begin()) { printk("FS: format+remount FAILED\n"); return false; }
} else {
printk("FS: mounted OK (persisted data intact)\n");
}
return true;
}

118
zephyr-port/07_companion/src/zephyr_radiolib_hal.h

@ -0,0 +1,118 @@
/*
* Minimal RadioLibHal for Zephyr (XIAO nRF54L15). Maps RadioLib's opaque pin ids
* to gpio_dt_spec from the `lora0` devicetree node, and SPI to the spi00 controller
* (no hardware CS, RadioLib drives NSS via digitalWrite). Raw GPIO ops keep
* RadioLib's Arduino LOW/HIGH == physical-level semantics.
*
* Pin ids passed to Module(hal, cs, irq, rst, gpio): NSS=0 DIO1=1 RESET=2 BUSY=3.
*/
#pragma once
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <RadioLib.h>
#define LR_PIN_NSS 0
#define LR_PIN_DIO1 1
#define LR_PIN_RESET 2
#define LR_PIN_BUSY 3
#define LR_PIN_COUNT 4
#define LORA0_NODE DT_NODELABEL(lora0)
#define SPI0_NODE DT_NODELABEL(spi00)
class ZephyrHal : public RadioLibHal {
public:
/* GPIO modes / levels / edges — opaque tokens RadioLib hands back to us. */
ZephyrHal()
: RadioLibHal(/*INPUT*/0, /*OUTPUT*/1, /*LOW*/0, /*HIGH*/1, /*RISING*/1, /*FALLING*/2) {}
void init() override { spiBegin(); }
void term() override {}
void pinMode(uint32_t pin, uint32_t mode) override {
if (pin >= LR_PIN_COUNT) return;
const struct gpio_dt_spec *g = &gpios[pin];
gpio_pin_configure(g->port, g->pin, (mode == 1) ? GPIO_OUTPUT : GPIO_INPUT);
}
void digitalWrite(uint32_t pin, uint32_t value) override {
if (pin >= LR_PIN_COUNT) return;
const struct gpio_dt_spec *g = &gpios[pin];
gpio_pin_set_raw(g->port, g->pin, value ? 1 : 0);
}
uint32_t digitalRead(uint32_t pin) override {
if (pin >= LR_PIN_COUNT) return 0;
const struct gpio_dt_spec *g = &gpios[pin];
return gpio_pin_get_raw(g->port, g->pin);
}
void attachInterrupt(uint32_t interruptNum, void (*cb)(void), uint32_t mode) override {
if (interruptNum >= LR_PIN_COUNT) return;
const struct gpio_dt_spec *g = &gpios[interruptNum];
user_cb = cb;
gpio_pin_configure(g->port, g->pin, GPIO_INPUT); /* RadioLib doesn't pinMode the IRQ */
if (!cb_added) { /* register the callback exactly once */
gpio_init_callback(&cb_data, gpioIsr, BIT(g->pin));
gpio_add_callback(g->port, &cb_data);
cb_added = true;
}
gpio_pin_interrupt_configure(g->port, g->pin,
(mode == 2) ? GPIO_INT_EDGE_FALLING :
(mode == 1) ? GPIO_INT_EDGE_RISING : GPIO_INT_EDGE_BOTH);
}
void detachInterrupt(uint32_t interruptNum) override {
if (interruptNum >= LR_PIN_COUNT) return;
const struct gpio_dt_spec *g = &gpios[interruptNum];
gpio_pin_interrupt_configure(g->port, g->pin, GPIO_INT_DISABLE);
gpio_remove_callback(g->port, &cb_data);
user_cb = nullptr;
}
void delay(RadioLibTime_t ms) override { k_msleep((int32_t)ms); }
void delayMicroseconds(RadioLibTime_t us) override { k_busy_wait((uint32_t)us); }
RadioLibTime_t millis() override { return (RadioLibTime_t)k_uptime_get(); }
RadioLibTime_t micros() override { return (RadioLibTime_t)k_ticks_to_us_floor64(k_uptime_ticks()); }
long pulseIn(uint32_t, uint32_t, RadioLibTime_t) override { return 0; }
void spiBegin() override {
spi_dev = DEVICE_DT_GET(SPI0_NODE);
spi_cfg.frequency = 2000000; /* 2 MHz, SPI mode 0, MSB first */
spi_cfg.operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_TRANSFER_MSB;
spi_cfg.slave = 0;
spi_cfg.cs.gpio.port = NULL; /* no HW CS — RadioLib toggles NSS */
}
void spiBeginTransaction() override {}
void spiEndTransaction() override {}
void spiEnd() override {}
void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override {
struct spi_buf txb = { .buf = out, .len = len };
struct spi_buf rxb = { .buf = in, .len = len };
struct spi_buf_set tx = { .buffers = &txb, .count = 1 };
struct spi_buf_set rx = { .buffers = &rxb, .count = 1 };
spi_transceive(spi_dev, &spi_cfg, (out ? &tx : NULL), (in ? &rx : NULL));
}
private:
static void (*user_cb)(void);
static struct gpio_callback cb_data;
static bool cb_added;
static void gpioIsr(const struct device *, struct gpio_callback *, uint32_t) {
if (user_cb) user_cb();
}
const struct device *spi_dev = nullptr;
struct spi_config spi_cfg = {};
/* index by LR_PIN_*; order must match the #defines above */
const struct gpio_dt_spec gpios[LR_PIN_COUNT] = {
GPIO_DT_SPEC_GET(LORA0_NODE, nss_gpios),
GPIO_DT_SPEC_GET(LORA0_NODE, dio1_gpios),
GPIO_DT_SPEC_GET(LORA0_NODE, reset_gpios),
GPIO_DT_SPEC_GET(LORA0_NODE, busy_gpios),
};
};
void (*ZephyrHal::user_cb)(void) = nullptr;
struct gpio_callback ZephyrHal::cb_data;
bool ZephyrHal::cb_added = false;
Loading…
Cancel
Save