mirror of https://github.com/meshcore-dev/MeshCore
42 changed files with 2268 additions and 41 deletions
@ -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; |
||||
|
} |
||||
@ -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; |
||||
@ -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); |
||||
|
} |
||||
@ -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 |
||||
@ -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; } |
||||
|
}; |
||||
@ -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; |
||||
|
} |
||||
|
}; |
||||
@ -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; } |
||||
|
}; |
||||
@ -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
|
||||
|
} |
||||
@ -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(); |
||||
@ -0,0 +1,5 @@ |
|||||
|
# Zephyr build artifacts (we build out-of-tree to /tmp, but ignore any local build dirs) |
||||
|
build/ |
||||
|
build-*/ |
||||
|
*.hex |
||||
|
*.elf |
||||
@ -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() |
||||
@ -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`). |
||||
@ -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>; |
||||
|
}; |
||||
|
}; |
||||
@ -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 */ |
||||
@ -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; |
||||
|
} |
||||
|
}; |
||||
@ -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); |
||||
|
} |
||||
|
}; |
||||
@ -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; } |
||||
|
}; |
||||
@ -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; |
||||
|
} |
||||
|
}; |
||||
@ -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 |
||||
@ -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 |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
@ -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)); |
||||
|
} |
||||
@ -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(); |
||||
@ -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; |
||||
|
} |
||||
@ -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…
Reference in new issue