From da77ecf6542d75c2adbc1a116e0be174931953b3 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Sun, 27 Sep 2026 15:07:00 +0200 Subject: [PATCH] Add nRF52 2.4GHz wireless bridge NRF52RadioBridge: an ESP-NOW-like broadcast bridge that drives the RADIO peripheral directly (1Mbit GFSK, custom framing) and reuses the ESP-NOW payload format, secret and Fletcher-16 isolation. - compile repeater with WITH_NRF52_WIRELESS_BRIDGE - bridge.channel 1/2/3 -> 2482/2450/2424 MHz - new bridge.txpower setting (-20..8 dBm, default 4) - bridge.type reports "nrf52-wireless", advert feature flag 0x05 - listen-before-talk with random backoff - fixed whitening seed: frequency-derived seeds made the hardware CRC check fail on some channels - move xorCrypt into BridgeBase (also fixes divide-by-zero on an empty secret) - bridge envs: RAK4631, RAK3401, Heltec T114/T1/T096, Xiao nRF52, SenseCAP Solar, ProMicro, Wio Tracker L1 --- docs/cli_commands.md | 24 +- examples/simple_repeater/MyMesh.cpp | 5 +- examples/simple_repeater/MyMesh.h | 7 + src/helpers/CommonCLI.cpp | 38 +- src/helpers/CommonCLI.h | 4 +- src/helpers/bridges/BridgeBase.cpp | 8 + src/helpers/bridges/BridgeBase.h | 13 + src/helpers/bridges/ESPNowBridge.cpp | 7 - src/helpers/bridges/ESPNowBridge.h | 13 - src/helpers/bridges/NRF52RadioBridge.cpp | 460 +++++++++++++++++++++++ src/helpers/bridges/NRF52RadioBridge.h | 173 +++++++++ variants/heltec_t096/platformio.ini | 17 + variants/heltec_t114/platformio.ini | 17 + variants/promicro/platformio.ini | 22 ++ variants/rak3401/platformio.ini | 19 + variants/rak4631/platformio.ini | 20 + variants/sensecap_solar/platformio.ini | 17 + variants/wio-tracker-l1/platformio.ini | 18 + variants/xiao_nrf52/platformio.ini | 17 + 19 files changed, 869 insertions(+), 30 deletions(-) create mode 100644 src/helpers/bridges/NRF52RadioBridge.cpp create mode 100644 src/helpers/bridges/NRF52RadioBridge.h diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8772b929fe..0bea57009d 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1117,28 +1117,44 @@ region save --- -#### View or change the channel used for bridging (ESPNow only) +#### View or change the channel used for bridging (ESP-NOW and nRF52 only) **Usage:** - `get bridge.channel` - `set bridge.channel ` **Parameters:** -- `channel`: Channel number (1-14) +- `channel`: + - ESP-NOW: Wi-Fi channel number (1-14) + - nRF52: `1` = 2482 MHz (default), `2` = 2450 MHz, `3` = 2424 MHz. These sit in the gaps between Wi-Fi channels 1/6/11 and the BLE advertising channels --- -#### Set the ESP-Now secret +#### Set the ESP-NOW / nRF52 bridge secret **Usage:** - `get bridge.secret` - `set bridge.secret ` **Parameters:** -- `secret`: ESP-NOW bridge secret, up to 15 characters +- `secret`: bridge secret, up to 15 characters **Default:** Varies by board --- +#### View or change the transmit power of the bridge (nRF52 only) +**Usage:** +- `get bridge.txpower` +- `set bridge.txpower ` + +**Parameters:** +- `dbm`: Transmit power in dBm (-20 to 8 on nRF52840). Values between the radio's supported steps (8, 7, 6, 5, 4, 3, 2, 0, -4, -8, -12, -16, -20) are rounded down, and the reply shows the power actually used. + +**Default:** `4` + +**Note:** The nRF52 bridge (`bridge.type` = `nrf52-wireless`) uses its own 2.4GHz protocol (1Mbit GFSK) that mimics ESP-NOW. nRF52 and ESP-NOW bridges can't talk to each other, because the nRF52 radio can't transmit or receive Wi-Fi frames. The nRF52 bridge needs a 2.4GHz (BLE) antenna attached and can't run while BLE is active. Starting OTA stops the bridge. + +--- + #### View the bootloader version (nRF52 only) **Usage:** `get bootloader.ver` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ca6a3e607e..9dbfdceb54 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -198,6 +198,8 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender reply_data[8] |= 0x01; // is bridge, type UART #elif WITH_ESPNOW_BRIDGE reply_data[8] |= 0x03; // is bridge, type ESP-NOW +#elif WITH_NRF52_WIRELESS_BRIDGE + reply_data[8] |= 0x05; // is bridge, type nRF52 2.4GHz #endif if (_prefs.disable_fwd) { // is this repeater currently disabled reply_data[8] |= 0x80; // is disabled @@ -869,7 +871,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc #if defined(WITH_RS232_BRIDGE) , bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc) #endif -#if defined(WITH_ESPNOW_BRIDGE) +#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_NRF52_WIRELESS_BRIDGE) , bridge(&_prefs, _mgr, &rtc) #endif { @@ -915,6 +917,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.bridge_pkt_src = 0; // logTx _prefs.bridge_baud = 115200; // baud rate _prefs.bridge_channel = 1; // channel 1 + _prefs.bridge_tx_power = 4; // dBm (nRF52 2.4GHz bridge only) StrHelper::strncpy(_prefs.bridge_secret, "LVSITANOS", sizeof(_prefs.bridge_secret)); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index cac6c4a281..b36fd437e6 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -24,6 +24,11 @@ #define WITH_BRIDGE #endif +#ifdef WITH_NRF52_WIRELESS_BRIDGE +#include "helpers/bridges/NRF52RadioBridge.h" +#define WITH_BRIDGE +#endif + #include #include #include @@ -114,6 +119,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { RS232Bridge bridge; #elif defined(WITH_ESPNOW_BRIDGE) ESPNowBridge bridge; +#elif defined(WITH_NRF52_WIRELESS_BRIDGE) + NRF52RadioBridge bridge; #endif void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 4930e81e9a..6cdb98ea4b 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4,6 +4,9 @@ #include "AdvertDataHelpers.h" #include "TxtDataHelpers.h" #include +#ifdef WITH_NRF52_WIRELESS_BRIDGE +#include "bridges/NRF52RadioBridge.h" +#endif #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 @@ -218,6 +221,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re strcpy(reply, "ERR: clock cannot go backwards"); } } else if (memcmp(command, "start ota", 9) == 0) { +#ifdef WITH_NRF52_WIRELESS_BRIDGE + _callbacks->setBridgeState(false); // release RADIO before SoftDevice takes it over +#endif if (!_board->startOTAUpdate(_prefs->node_name, reply)) { strcpy(reply, "Error"); } @@ -613,22 +619,40 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); } #endif -#ifdef WITH_ESPNOW_BRIDGE +#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_NRF52_WIRELESS_BRIDGE) } else if (memcmp(config, "bridge.channel ", 15) == 0) { int ch = atoi(&config[15]); - if (ch > 0 && ch < 15) { +#ifdef WITH_NRF52_WIRELESS_BRIDGE + const int max_ch = NRF52RadioBridge::NUM_CHANNELS; +#else + const int max_ch = 14; +#endif + if (ch > 0 && ch <= max_ch) { _prefs->bridge_channel = (uint8_t)ch; _callbacks->restartBridge(); savePrefs(); strcpy(reply, "OK"); } else { - strcpy(reply, "Error: channel must be between 1-14"); + sprintf(reply, "Error: channel must be between 1-%d", max_ch); } } else if (memcmp(config, "bridge.secret ", 14) == 0) { StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); _callbacks->restartBridge(); savePrefs(); strcpy(reply, "OK"); +#endif +#ifdef WITH_NRF52_WIRELESS_BRIDGE + } else if (memcmp(config, "bridge.txpower ", 15) == 0) { + int dbm = atoi(&config[15]); + if (dbm >= NRF52RadioBridge::MIN_TX_POWER && dbm <= NRF52RadioBridge::MAX_TX_POWER) { + _prefs->bridge_tx_power = (int8_t)dbm; + _callbacks->restartBridge(); + savePrefs(); + sprintf(reply, "OK - %d dBm", NRF52RadioBridge::supportedTxPower(_prefs->bridge_tx_power)); + } else { + sprintf(reply, "Error: tx power must be between %d and %d dBm", NRF52RadioBridge::MIN_TX_POWER, + NRF52RadioBridge::MAX_TX_POWER); + } #endif } else if (memcmp(config, "adc.multiplier ", 15) == 0) { _prefs->adc_multiplier = atof(&config[15]); @@ -731,6 +755,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep "rs232" #elif WITH_ESPNOW_BRIDGE "espnow" +#elif WITH_NRF52_WIRELESS_BRIDGE + "nrf52-wireless" #else "none" #endif @@ -747,11 +773,15 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(config, "bridge.baud", 11) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud); #endif -#ifdef WITH_ESPNOW_BRIDGE +#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_NRF52_WIRELESS_BRIDGE) } else if (memcmp(config, "bridge.channel", 14) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel); } else if (memcmp(config, "bridge.secret", 13) == 0) { sprintf(reply, "> %s", _prefs->bridge_secret); +#endif +#ifdef WITH_NRF52_WIRELESS_BRIDGE + } else if (memcmp(config, "bridge.txpower", 14) == 0) { + sprintf(reply, "> %d", (int)NRF52RadioBridge::supportedTxPower(_prefs->bridge_tx_power)); #endif } else if (memcmp(config, "bootloader.ver", 14) == 0) { #ifdef NRF52_PLATFORM diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 8591cdc140..a986b5896f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -9,7 +9,7 @@ #include #include -#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) +#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) || defined(WITH_NRF52_WIRELESS_BRIDGE) #define WITH_BRIDGE #endif @@ -56,6 +56,7 @@ class NodePrefs : public ConfigSerializer { uint32_t bridge_baud = 0; // 9600, 19200, 38400, 57600, 115200 (default 115200) uint8_t bridge_channel = 0; // 1-14 (ESP-NOW only) char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only) + int8_t bridge_tx_power = 0; // dBm (nRF52 2.4GHz bridge only) // Power setting uint8_t powersaving_enabled = 0; // boolean // Gps settings @@ -146,6 +147,7 @@ class NodePrefs : public ConfigSerializer { def("baud", _parent->bridge_baud); // 9600, 19200, 38400, 57600, 115200 (default 115200) def("ch", _parent->bridge_channel); // 1-14 (ESP-NOW only) def("secret", _parent->bridge_secret, sizeof(_parent->bridge_secret)); // for XOR encryption of bridge packets (ESP-NOW only) + def("txpwr", _parent->bridge_tx_power); // dBm (nRF52 2.4GHz bridge only) } public: BridgePrefs(NodePrefs* parent) : _parent(parent) { } diff --git a/src/helpers/bridges/BridgeBase.cpp b/src/helpers/bridges/BridgeBase.cpp index 8093d3cb5b..34b4267e13 100644 --- a/src/helpers/bridges/BridgeBase.cpp +++ b/src/helpers/bridges/BridgeBase.cpp @@ -31,6 +31,14 @@ bool BridgeBase::validateChecksum(const uint8_t *data, size_t len, uint16_t rece return received_checksum == calculated_checksum; } +void BridgeBase::xorCrypt(uint8_t *data, size_t len) { + size_t keyLen = strnlen(_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); + if (keyLen == 0) return; + for (size_t i = 0; i < len; i++) { + data[i] ^= _prefs->bridge_secret[i % keyLen]; + } +} + void BridgeBase::handleReceivedPacket(mesh::Packet *packet) { // Guard against uninitialized state if (_initialized == false) { diff --git a/src/helpers/bridges/BridgeBase.h b/src/helpers/bridges/BridgeBase.h index 8bbe646678..e742df964b 100644 --- a/src/helpers/bridges/BridgeBase.h +++ b/src/helpers/bridges/BridgeBase.h @@ -106,6 +106,19 @@ class BridgeBase : public AbstractBridge { */ bool validateChecksum(const uint8_t *data, size_t len, uint16_t received_checksum); + /** + * @brief Performs XOR encryption/decryption of data using _prefs->bridge_secret + * + * Used by radio bridges (ESP-NOW, nRF52 2.4GHz) to isolate different mesh networks. + * The same operation is used for both encryption and decryption. + * While not cryptographically secure, it provides basic network isolation. + * An empty secret leaves the data unchanged. + * + * @param data Pointer to data to encrypt/decrypt + * @param len Length of data in bytes + */ + void xorCrypt(uint8_t *data, size_t len); + /** * @brief Common packet handling for received packets * diff --git a/src/helpers/bridges/ESPNowBridge.cpp b/src/helpers/bridges/ESPNowBridge.cpp index 3c094e7c90..364a83b148 100644 --- a/src/helpers/bridges/ESPNowBridge.cpp +++ b/src/helpers/bridges/ESPNowBridge.cpp @@ -93,13 +93,6 @@ void ESPNowBridge::loop() { // Nothing to do here - ESP-NOW is callback based } -void ESPNowBridge::xorCrypt(uint8_t *data, size_t len) { - size_t keyLen = strlen(_prefs->bridge_secret); - for (size_t i = 0; i < len; i++) { - data[i] ^= _prefs->bridge_secret[i % keyLen]; - } -} - void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int32_t len) { // Ignore packets that are too small to contain header + checksum if (len < (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE)) { diff --git a/src/helpers/bridges/ESPNowBridge.h b/src/helpers/bridges/ESPNowBridge.h index 431a036b09..cc2ebbeeff 100644 --- a/src/helpers/bridges/ESPNowBridge.h +++ b/src/helpers/bridges/ESPNowBridge.h @@ -69,19 +69,6 @@ class ESPNowBridge : public BridgeBase { /** Current position in receive buffer */ size_t _rx_buffer_pos; - /** - * Performs XOR encryption/decryption of data - * Used to isolate different mesh networks - * - * Uses _prefs->bridge_secret as the key in a simple XOR operation. - * The same operation is used for both encryption and decryption. - * While not cryptographically secure, it provides basic network isolation. - * - * @param data Pointer to data to encrypt/decrypt - * @param len Length of data in bytes - */ - void xorCrypt(uint8_t *data, size_t len); - /** * ESP-NOW receive callback * Called by ESP-NOW when a packet is received diff --git a/src/helpers/bridges/NRF52RadioBridge.cpp b/src/helpers/bridges/NRF52RadioBridge.cpp new file mode 100644 index 0000000000..beb6e61e35 --- /dev/null +++ b/src/helpers/bridges/NRF52RadioBridge.cpp @@ -0,0 +1,460 @@ +#include "NRF52RadioBridge.h" + +#ifdef WITH_NRF52_WIRELESS_BRIDGE + +#include +#include + +// Fixed on-air network address ("MCBR" + 0xE7 prefix); isolation between +// networks sharing a channel is done by bridge_secret, like ESP-NOW +#define NRF52_WIRELESS_BRIDGE_BASE_ADDR 0x4D434252 +#define NRF52_WIRELESS_BRIDGE_ADDR_PREFIX 0xE7 + +#define NRF52_WIRELESS_BRIDGE_IRQ_PRIORITY 3 + +NRF52RadioBridge *NRF52RadioBridge::_instance = nullptr; + +extern "C" void RADIO_IRQHandler(void) { + NRF52RadioBridge::handleIrq(); +} + +void NRF52RadioBridge::handleIrq() { + if (_instance) { + _instance->onRadioIrq(); + } +} + +NRF52RadioBridge::NRF52RadioBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc) + : BridgeBase(prefs, mgr, rtc), _rx_head(0), _rx_tail(0), _tx_head(0), _tx_tail(0), + _tx_in_flight(false), _tx_started_at(0), _tx_next_attempt(0), _tx_attempts(0), _state(STATE_OFF), + _rx_busy(false), _tx_done(false), _hfxo_started(false) { + _instance = this; +} + +uint8_t NRF52RadioBridge::channelToFrequency(uint8_t channel) { + // ~1MHz wide GFSK fits into the gaps that Wi-Fi 1/6/11 and BLE advertising (2402/2426/2480) leave free + static const uint8_t freqs[NUM_CHANNELS] = { + 82, // 2482 MHz: above BLE adv ch39, below the 2483.5 MHz band edge (default) + 50, // 2450 MHz: between Wi-Fi ch6 and ch11 + 24, // 2424 MHz: between Wi-Fi ch1 and BLE adv ch38 + }; + if (channel < 1 || channel > NUM_CHANNELS) channel = 1; + return freqs[channel - 1]; +} + +int8_t NRF52RadioBridge::supportedTxPower(int8_t dbm) { + static const int8_t steps[] = { +#if defined(RADIO_TXPOWER_TXPOWER_Pos8dBm) + 8, 7, 6, 5, +#endif + 4, 3, +#if defined(RADIO_TXPOWER_TXPOWER_Pos2dBm) + 2, +#endif + 0, -4, -8, -12, -16, + }; + for (int8_t step : steps) { + if (dbm >= step) return step; + } + return -20; +} + +static uint32_t txPowerRegister(int8_t dbm) { + // TXPOWER register values are the dBm value as two's complement + return (uint8_t)NRF52RadioBridge::supportedTxPower(dbm); +} + +void NRF52RadioBridge::configureRadio() { + // Power cycle the peripheral to get it into a known (reset) state + NRF_RADIO->POWER = 0; + NRF_RADIO->POWER = 1; + + // BLE 1M PHY modulation (+-250kHz deviation) with our own address and framing, so this is not BLE + NRF_RADIO->MODE = RADIO_MODE_MODE_Ble_1Mbit << RADIO_MODE_MODE_Pos; + NRF_RADIO->MODECNF0 = (RADIO_MODECNF0_RU_Default << RADIO_MODECNF0_RU_Pos) | + (RADIO_MODECNF0_DTX_Center << RADIO_MODECNF0_DTX_Pos); + NRF_RADIO->TXPOWER = txPowerRegister(_prefs->bridge_tx_power) << RADIO_TXPOWER_TXPOWER_Pos; + NRF_RADIO->FREQUENCY = channelToFrequency(_prefs->bridge_channel) << RADIO_FREQUENCY_FREQUENCY_Pos; + + // 5 byte address (4 byte base + 1 byte prefix) on logical address 0 + NRF_RADIO->BASE0 = NRF52_WIRELESS_BRIDGE_BASE_ADDR; + NRF_RADIO->PREFIX0 = NRF52_WIRELESS_BRIDGE_ADDR_PREFIX << RADIO_PREFIX0_AP0_Pos; + NRF_RADIO->TXADDRESS = 0; + NRF_RADIO->RXADDRESSES = RADIO_RXADDRESSES_ADDR0_Msk; + + // 8-bit length field, no S0/S1 + NRF_RADIO->PCNF0 = (8 << RADIO_PCNF0_LFLEN_Pos) | (0 << RADIO_PCNF0_S0LEN_Pos) | (0 << RADIO_PCNF0_S1LEN_Pos) +#if defined(RADIO_PCNF0_PLEN_Pos) + | (RADIO_PCNF0_PLEN_8bit << RADIO_PCNF0_PLEN_Pos) +#endif + ; + NRF_RADIO->PCNF1 = (MAX_RADIO_PACKET_SIZE << RADIO_PCNF1_MAXLEN_Pos) | (0 << RADIO_PCNF1_STATLEN_Pos) | + (4 << RADIO_PCNF1_BALEN_Pos) | (RADIO_PCNF1_ENDIAN_Big << RADIO_PCNF1_ENDIAN_Pos) | + (RADIO_PCNF1_WHITEEN_Enabled << RADIO_PCNF1_WHITEEN_Pos); + // Fixed whitening seed. Deriving it from the frequency (like BLE does from the channel index) made the + // hardware CRC check fail on every frame for some seeds (e.g. 0x52, 0x72) even though payload and CRC + // bytes were received intact; 0x58 was verified on all bridge channels. + NRF_RADIO->DATAWHITEIV = 0x58; + + // 16-bit CRC-CCITT over address + payload + NRF_RADIO->CRCCNF = (RADIO_CRCCNF_LEN_Two << RADIO_CRCCNF_LEN_Pos) | + (RADIO_CRCCNF_SKIPADDR_Include << RADIO_CRCCNF_SKIPADDR_Pos); + NRF_RADIO->CRCINIT = 0xFFFF; + NRF_RADIO->CRCPOLY = 0x11021; + + NRF_RADIO->INTENCLR = 0xFFFFFFFF; + NRF_RADIO->INTENSET = RADIO_INTENSET_ADDRESS_Msk | RADIO_INTENSET_END_Msk | RADIO_INTENSET_DISABLED_Msk; + + NVIC_SetPriority(RADIO_IRQn, NRF52_WIRELESS_BRIDGE_IRQ_PRIORITY); + NVIC_ClearPendingIRQ(RADIO_IRQn); +} + +void NRF52RadioBridge::startRx() { + // radio must be DISABLED; called from ISR or with RADIO_IRQn masked + _rx_busy = false; + NRF_RADIO->PACKETPTR = (uint32_t)_rx_frames[_rx_head].data; + NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk | RADIO_SHORTS_ADDRESS_RSSISTART_Msk | + RADIO_SHORTS_DISABLED_RSSISTOP_Msk; + _state = STATE_RX; + NRF_RADIO->TASKS_RXEN = 1; +} + +void NRF52RadioBridge::onRadioIrq() { + if (NRF_RADIO->EVENTS_ADDRESS) { + NRF_RADIO->EVENTS_ADDRESS = 0; + if (_state == STATE_RX) _rx_busy = true; +#if BRIDGE_DEBUG + _dbg_addr++; +#endif + } + + if (NRF_RADIO->EVENTS_END) { + NRF_RADIO->EVENTS_END = 0; + if (_state == STATE_RX) { + _rx_busy = false; + bool crc_ok = NRF_RADIO->CRCSTATUS == RADIO_CRCSTATUS_CRCSTATUS_CRCOk; +#if BRIDGE_DEBUG + if (crc_ok) _dbg_crc_ok++; else _dbg_crc_err++; +#endif + if (crc_ok) { + _rx_frames[_rx_head].rssi = -(int8_t)NRF_RADIO->RSSISAMPLE; + uint8_t next = (_rx_head + 1) % RX_SLOTS; + if (next != _rx_tail) { // commit, otherwise ring is full and frame is dropped + _rx_head = next; + } + } + // radio sits in RXIDLE after END, re-arm with the (possibly new) slot + NRF_RADIO->PACKETPTR = (uint32_t)_rx_frames[_rx_head].data; + NRF_RADIO->TASKS_START = 1; + } + } + + if (NRF_RADIO->EVENTS_DISABLED) { + NRF_RADIO->EVENTS_DISABLED = 0; + if (_state == STATE_TX) { + _tx_done = true; + startRx(); + } + } + + // flush the event clears before leaving the ISR, avoids spurious re-entry + (void)NRF_RADIO->EVENTS_DISABLED; +} + +void NRF52RadioBridge::begin() { + BRIDGE_DEBUG_PRINTLN("Initializing...\n"); + + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { + // SoftDevice owns the RADIO peripheral (BLE active), we can't touch it + BRIDGE_DEBUG_PRINTLN("SoftDevice enabled, radio not available\n"); + return; + } + + // RADIO requires the external high frequency crystal + if ((NRF_CLOCK->HFCLKSTAT & (CLOCK_HFCLKSTAT_SRC_Msk | CLOCK_HFCLKSTAT_STATE_Msk)) != + ((CLOCK_HFCLKSTAT_SRC_Xtal << CLOCK_HFCLKSTAT_SRC_Pos) | CLOCK_HFCLKSTAT_STATE_Msk)) { + NRF_CLOCK->EVENTS_HFCLKSTARTED = 0; + NRF_CLOCK->TASKS_HFCLKSTART = 1; + uint32_t start = millis(); + while (!NRF_CLOCK->EVENTS_HFCLKSTARTED) { + if (millis() - start > 10) { + BRIDGE_DEBUG_PRINTLN("HFXO failed to start\n"); + NRF_CLOCK->TASKS_HFCLKSTOP = 1; + return; + } + } + NRF_CLOCK->EVENTS_HFCLKSTARTED = 0; + _hfxo_started = true; + } + + _rx_head = _rx_tail = 0; + _tx_head = _tx_tail = 0; + _tx_in_flight = false; + _tx_done = false; + _tx_attempts = 0; + _tx_next_attempt = 0; + + NVIC_DisableIRQ(RADIO_IRQn); + configureRadio(); + startRx(); + NVIC_EnableIRQ(RADIO_IRQn); + + BRIDGE_DEBUG_PRINTLN("Listening on channel %d (%d MHz), tx power %d dBm\n", _prefs->bridge_channel, + 2400 + channelToFrequency(_prefs->bridge_channel), supportedTxPower(_prefs->bridge_tx_power)); + + // Update bridge state + _initialized = true; +} + +void NRF52RadioBridge::end() { + BRIDGE_DEBUG_PRINTLN("Stopping...\n"); + + if (!_initialized) return; + + NVIC_DisableIRQ(RADIO_IRQn); + _state = STATE_OFF; + NRF_RADIO->INTENCLR = 0xFFFFFFFF; + NRF_RADIO->SHORTS = 0; + NRF_RADIO->EVENTS_DISABLED = 0; + NRF_RADIO->TASKS_DISABLE = 1; + uint32_t start = micros(); + while (!NRF_RADIO->EVENTS_DISABLED && (micros() - start) < 200) { + } + NRF_RADIO->EVENTS_DISABLED = 0; + + // leave the peripheral in reset state (e.g. before SoftDevice is enabled for OTA) + NRF_RADIO->POWER = 0; + NRF_RADIO->POWER = 1; + NVIC_ClearPendingIRQ(RADIO_IRQn); + + if (_hfxo_started) { + NRF_CLOCK->TASKS_HFCLKSTOP = 1; + _hfxo_started = false; + } + + _tx_in_flight = false; + + // Update bridge state + _initialized = false; +} + +bool NRF52RadioBridge::isChannelClear() { + if (_rx_busy) return false; + + // Only valid while the radio is in RX, which is the case whenever no TX is in flight + NRF_RADIO->EVENTS_RSSIEND = 0; + NRF_RADIO->TASKS_RSSISTART = 1; + uint32_t start = micros(); + while (!NRF_RADIO->EVENTS_RSSIEND) { + if (micros() - start > 50) return true; // no sample, don't block TX + } + NRF_RADIO->EVENTS_RSSIEND = 0; + int rssi = -(int)NRF_RADIO->RSSISAMPLE; + return rssi < NRF52_WIRELESS_BRIDGE_LBT_RSSI && !_rx_busy; +} + +void NRF52RadioBridge::transmitFrame(RadioFrame *frame) { + NVIC_DisableIRQ(RADIO_IRQn); + + // RX -> DISABLED, then ramp up TX; END_DISABLE brings us back to DISABLED and the ISR restarts RX + _state = STATE_OFF; + NRF_RADIO->SHORTS = 0; + NRF_RADIO->EVENTS_DISABLED = 0; + NRF_RADIO->TASKS_DISABLE = 1; + uint32_t start = micros(); + while (!NRF_RADIO->EVENTS_DISABLED && (micros() - start) < 200) { + } + NRF_RADIO->EVENTS_DISABLED = 0; + NRF_RADIO->EVENTS_ADDRESS = 0; + NRF_RADIO->EVENTS_END = 0; + _rx_busy = false; + + NRF_RADIO->PACKETPTR = (uint32_t)frame->data; + NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk | RADIO_SHORTS_END_DISABLE_Msk; + _tx_done = false; + _state = STATE_TX; + NRF_RADIO->TASKS_TXEN = 1; + + NVIC_ClearPendingIRQ(RADIO_IRQn); + NVIC_EnableIRQ(RADIO_IRQn); +} + +void NRF52RadioBridge::serviceTx() { + if (_tx_in_flight) { + if (_tx_done) { + _tx_in_flight = false; + _tx_tail = (_tx_tail + 1) % TX_SLOTS; + BRIDGE_DEBUG_PRINTLN("TX done\n"); + } else if (millis() - _tx_started_at > TX_TIMEOUT_MS) { + // should never happen, recover the radio and drop the frame + BRIDGE_DEBUG_PRINTLN("TX timeout, resetting radio\n"); + NVIC_DisableIRQ(RADIO_IRQn); + configureRadio(); + startRx(); + NVIC_EnableIRQ(RADIO_IRQn); + _tx_in_flight = false; + _tx_tail = (_tx_tail + 1) % TX_SLOTS; + } else { + return; + } + } + + if (_tx_tail == _tx_head) return; // nothing queued + if ((int32_t)(millis() - _tx_next_attempt) < 0) return; // backing off + + if (_tx_attempts < MAX_LBT_ATTEMPTS && !isChannelClear()) { + _tx_attempts++; + _tx_next_attempt = millis() + random(1, 4 * _tx_attempts + 2); + return; + } + + _tx_attempts = 0; + _tx_in_flight = true; + _tx_started_at = millis(); + transmitFrame(&_tx_frames[_tx_tail]); +} + +void NRF52RadioBridge::loop() { + if (!_initialized) return; + + // drain received frames (ISR is the producer, we only advance the tail) + while (_rx_tail != _rx_head) { + processRxFrame(&_rx_frames[_rx_tail]); + _rx_tail = (_rx_tail + 1) % RX_SLOTS; + } + + serviceTx(); + +#if BRIDGE_DEBUG + if ((int32_t)(millis() - _dbg_next_report) >= 0) { + _dbg_next_report = millis() + 5000; + int rssi = 0; + if (!_tx_in_flight) { + NRF_RADIO->EVENTS_RSSIEND = 0; + NRF_RADIO->TASKS_RSSISTART = 1; + uint32_t start = micros(); + while (!NRF_RADIO->EVENTS_RSSIEND && micros() - start < 50) { + } + rssi = -(int)NRF_RADIO->RSSISAMPLE; + } + // addr counts every address match (incl. own TX), crc_ok + crc_err only receptions + BRIDGE_DEBUG_PRINTLN("stats: addr=%u crc_ok=%u crc_err=%u noise=%d\n", _dbg_addr, _dbg_crc_ok, _dbg_crc_err, + rssi); + } +#endif +} + +void NRF52RadioBridge::processRxFrame(const RadioFrame *frame) { + const size_t len = frame->data[0]; + const uint8_t *data = &frame->data[1]; + + // Ignore packets that are too small to contain header + checksum + if (len < (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE)) { + BRIDGE_DEBUG_PRINTLN("RX packet too small, len=%d\n", len); + return; + } + + // Check packet header magic + uint16_t received_magic = (data[0] << 8) | data[1]; + if (received_magic != BRIDGE_PACKET_MAGIC) { + BRIDGE_DEBUG_PRINTLN("RX invalid magic 0x%04X\n", received_magic); + return; + } + + // Make a copy we can decrypt + uint8_t decrypted[MAX_RADIO_PACKET_SIZE]; + const size_t encryptedDataLen = len - BRIDGE_MAGIC_SIZE; + memcpy(decrypted, data + BRIDGE_MAGIC_SIZE, encryptedDataLen); + + // Try to decrypt (checksum + payload) + xorCrypt(decrypted, encryptedDataLen); + + // Validate checksum + uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1]; + const size_t payloadLen = encryptedDataLen - BRIDGE_CHECKSUM_SIZE; + + if (!validateChecksum(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen, received_checksum)) { + // Failed to decrypt - likely from a different network + BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04X\n", received_checksum); + return; + } + + BRIDGE_DEBUG_PRINTLN("RX, payload_len=%d, rssi=%d\n", payloadLen, frame->rssi); + + // Create mesh packet + mesh::Packet *pkt = _mgr->allocNew(); + if (!pkt) return; + + if (pkt->readFrom(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen)) { + onPacketReceived(pkt); + } else { + _mgr->free(pkt); + } +} + +void NRF52RadioBridge::sendPacket(mesh::Packet *packet) { + // Guard against uninitialized state + if (_initialized == false) { + return; + } + + // First validate the packet pointer + if (!packet) { + BRIDGE_DEBUG_PRINTLN("TX invalid packet pointer\n"); + return; + } + + if (!_seen_packets.wasSeen(packet)) { + _seen_packets.markSeen(packet); + + uint8_t next = (_tx_head + 1) % TX_SLOTS; + if (next == _tx_tail) { + BRIDGE_DEBUG_PRINTLN("TX queue full, packet dropped\n"); + return; + } + + uint8_t sizingBuffer[MAX_TRANS_UNIT + 1]; + uint16_t meshPacketLen = packet->writeTo(sizingBuffer); + + // Check if packet fits within our maximum payload size + if (meshPacketLen > MAX_PAYLOAD_SIZE) { + BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", meshPacketLen, MAX_PAYLOAD_SIZE); + return; + } + + uint8_t *buffer = &_tx_frames[_tx_head].data[1]; + + // Write magic header (2 bytes) + buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF; + buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF; + + // Write packet payload starting after magic header and checksum + const size_t packetOffset = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE; + memcpy(buffer + packetOffset, sizingBuffer, meshPacketLen); + + // Calculate and add checksum (only of the payload) + uint16_t checksum = fletcher16(buffer + packetOffset, meshPacketLen); + buffer[2] = (checksum >> 8) & 0xFF; // High byte + buffer[3] = checksum & 0xFF; // Low byte + + // Encrypt payload and checksum (not including magic header) + xorCrypt(buffer + BRIDGE_MAGIC_SIZE, meshPacketLen + BRIDGE_CHECKSUM_SIZE); + + // Length byte for the radio: magic header + checksum + payload + _tx_frames[_tx_head].data[0] = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE + meshPacketLen; + _tx_head = next; + + BRIDGE_DEBUG_PRINTLN("TX queued, len=%d\n", meshPacketLen); + + // try to send right away instead of waiting for the next loop() + serviceTx(); + } +} + +void NRF52RadioBridge::onPacketReceived(mesh::Packet *packet) { + handleReceivedPacket(packet); +} + +#endif diff --git a/src/helpers/bridges/NRF52RadioBridge.h b/src/helpers/bridges/NRF52RadioBridge.h new file mode 100644 index 0000000000..f1435f8c5f --- /dev/null +++ b/src/helpers/bridges/NRF52RadioBridge.h @@ -0,0 +1,173 @@ +#pragma once + +#include "MeshCore.h" +#include "helpers/bridges/BridgeBase.h" + +#ifdef WITH_NRF52_WIRELESS_BRIDGE + +#ifndef NRF52_WIRELESS_BRIDGE_LBT_RSSI +#define NRF52_WIRELESS_BRIDGE_LBT_RSSI -70 // dBm, channel considered busy above this level +#endif + +/** + * @brief Bridge implementation using the nRF52 2.4GHz radio for packet transport + * + * ESP-NOW rides on 802.11 (DSSS/OFDM) frames, which the nRF52 RADIO peripheral cannot + * modulate or demodulate (it only supports GFSK Nordic proprietary/BLE modes and, on + * nRF52840, 802.15.4 O-QPSK). This bridge therefore implements an ESP-NOW-like protocol + * of its own on top of 1Mbit GFSK (BLE 1M PHY modulation, custom framing), and is only interoperable + * with other nRF52 nodes running this bridge - not with ESP-NOW bridges. + * + * It mimics ESPNowBridge behaviour so it is a drop-in replacement: + * - Connectionless broadcast; every bridge on the same channel receives every packet + * - bridge.channel selects one of NUM_CHANNELS frequencies placed in the gaps between Wi-Fi + * channels 1/6/11 and BLE advertising channels, as GFSK is narrow enough to fit there: + * 1 = 2482 MHz (default), 2 = 2450 MHz, 3 = 2424 MHz + * - Network isolation using XOR encryption with _prefs->bridge_secret + Fletcher-16 + * - Same payload layout as ESPNowBridge + * - Listen-before-talk with random backoff instead of 802.11 CSMA/CA + * + * The RADIO peripheral is driven directly, so the SoftDevice must NOT be enabled while the + * bridge runs (true for repeaters, where BLE is only started for OTA). begin() refuses to + * start if the SoftDevice is enabled, so this bridge cannot be combined with BLE companion. + * + * Air frame (1Mbit GFSK, whitened): + * [1 byte] preamble + * [5 bytes] address (fixed network address) + * [1 byte] length + * [2 bytes] Magic Header - Used to identify bridge packets + * [2 bytes] Fletcher-16 checksum of payload (XOR encrypted) + * [251 bytes max] Mesh packet (XOR encrypted) + * [2 bytes] CRC-16/CCITT (hardware) + */ +class NRF52RadioBridge : public BridgeBase { +private: + static NRF52RadioBridge *_instance; + + /** Radio payload limit (8-bit length field) */ + static const size_t MAX_RADIO_PACKET_SIZE = 255; + + /** Space left for the mesh packet after magic + checksum */ + static const size_t MAX_PAYLOAD_SIZE = MAX_RADIO_PACKET_SIZE - (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE); + + /** RX/TX ring sizes (one RX slot is always owned by the radio) */ + static const uint8_t RX_SLOTS = 4; + static const uint8_t TX_SLOTS = 4; + + /** Max times a TX is deferred due to a busy channel before sending anyway */ + static const uint8_t MAX_LBT_ATTEMPTS = 10; + + /** Safety timeout for a TX that never signals completion */ + static const uint32_t TX_TIMEOUT_MS = 20; + + /** Radio buffer: [0] = length, [1..] = payload, as the RADIO DMA expects */ + struct RadioFrame { + uint8_t data[1 + MAX_RADIO_PACKET_SIZE]; + int8_t rssi; + }; + + enum RadioState : uint8_t { STATE_OFF, STATE_RX, STATE_TX }; + + RadioFrame _rx_frames[RX_SLOTS]; + volatile uint8_t _rx_head; // slot the radio is receiving into (ISR owned) + volatile uint8_t _rx_tail; // next slot to process (loop owned) + + RadioFrame _tx_frames[TX_SLOTS]; + uint8_t _tx_head; + uint8_t _tx_tail; + bool _tx_in_flight; + uint32_t _tx_started_at; + uint32_t _tx_next_attempt; + uint8_t _tx_attempts; + + volatile RadioState _state; + volatile bool _rx_busy; // address matched, packet reception in progress + volatile bool _tx_done; // set by ISR when TX finished + + bool _hfxo_started; // we started HFXO and must stop it in end() + +#if BRIDGE_DEBUG + volatile uint32_t _dbg_addr, _dbg_crc_ok, _dbg_crc_err; + uint32_t _dbg_next_report; +#endif + + void onRadioIrq(); + + /** Maps bridge channel 1-NUM_CHANNELS to NRF_RADIO->FREQUENCY (MHz offset from 2400) */ + static uint8_t channelToFrequency(uint8_t channel); + + void configureRadio(); + void startRx(); + bool isChannelClear(); + void transmitFrame(RadioFrame *frame); + void serviceTx(); + void processRxFrame(const RadioFrame *frame); + +public: + /** Number of selectable bridge.channel values */ + static const uint8_t NUM_CHANNELS = 3; + + /** bridge.txpower range in dBm; values in between are rounded down to a supported TXPOWER step */ + static const int8_t MIN_TX_POWER = -20; +#if defined(RADIO_TXPOWER_TXPOWER_Pos8dBm) + static const int8_t MAX_TX_POWER = 8; +#else + static const int8_t MAX_TX_POWER = 4; +#endif + + /** Maps dBm to the highest supported TXPOWER step that does not exceed it, returns that step in dBm */ + static int8_t supportedTxPower(int8_t dbm); + + /** Entry point for RADIO_IRQHandler, dispatches to the active instance */ + static void handleIrq(); + + /** + * Constructs an NRF52RadioBridge instance + * + * @param prefs Node preferences for configuration settings + * @param mgr PacketManager for allocating and queuing packets + * @param rtc RTCClock for timestamping debug messages + */ + NRF52RadioBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc); + + /** + * Initializes the bridge + * + * - Starts the high frequency crystal oscillator (required by RADIO) + * - Configures the RADIO for 1Mbit GFSK on _prefs->bridge_channel + * - Starts continuous receive + */ + void begin() override; + + /** + * Stops the bridge + * + * - Disables and resets the RADIO peripheral + * - Stops the HFXO if it was started by begin() + */ + void end() override; + + /** + * Main loop handler + * Processes received frames and transmits queued frames (with listen-before-talk) + */ + void loop() override; + + /** + * Called when a packet is received via the radio + * Queues the packet for mesh processing if not seen before + * + * @param packet The received mesh packet + */ + void onPacketReceived(mesh::Packet *packet) override; + + /** + * Called when a packet needs to be transmitted via the radio + * Encrypts and queues the packet for broadcast if not seen before + * + * @param packet The mesh packet to transmit + */ + void sendPacket(mesh::Packet *packet) override; +}; + +#endif diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 02e883287e..7180eb169d 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -70,6 +70,23 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +[env:Heltec_t096_repeater_bridge_nrf52] +extends = Heltec_t096 +build_flags = + ${Heltec_t096.build_flags} + -D ADVERT_NAME='"nRF52 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_NRF52_WIRELESS_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_t096.build_src_filter} + + + +<../examples/simple_repeater> + [env:Heltec_t096_repeater_bridge_rs232] extends = Heltec_t096 build_flags = diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 48fb4b0f52..9249fbbf64 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -193,6 +193,23 @@ build_src_filter = ${Heltec_t114_with_display.build_src_filter} + +<../examples/simple_repeater> +[env:Heltec_t114_repeater_bridge_nrf52] +extends = Heltec_t114_with_display +build_flags = + ${Heltec_t114_with_display.build_flags} + -D ADVERT_NAME='"nRF52 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_NRF52_WIRELESS_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_t114_with_display.build_src_filter} + + + +<../examples/simple_repeater> + [env:Heltec_t114_room_server] extends = Heltec_t114_with_display build_src_filter = ${Heltec_t114_with_display.build_src_filter} diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 90cb475f5b..44d59457ee 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -79,6 +79,28 @@ build_flags = lib_deps = ${Promicro.lib_deps} adafruit/RTClib @ ^2.1.3 +[env:ProMicro_repeater_bridge_nrf52] +extends = Promicro +build_src_filter = ${Promicro.build_src_filter} + +<../examples/simple_repeater> + + + + + + +build_flags = + ${Promicro.build_flags} + -D ADVERT_NAME='"nRF52 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D DISPLAY_CLASS=SSD1306Display + -D WITH_NRF52_WIRELESS_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${Promicro.lib_deps} + adafruit/RTClib @ ^2.1.3 + [env:ProMicro_room_server] extends = Promicro build_src_filter = ${Promicro.build_src_filter} diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index c285e3ec50..001cf6adc1 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -41,6 +41,25 @@ build_src_filter = ${rak3401.build_src_filter} + +<../examples/simple_repeater> +[env:RAK_3401_repeater_bridge_nrf52] +extends = rak3401 +build_flags = + ${rak3401.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"nRF52 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_NRF52_WIRELESS_BRIDGE=1 + ;-D BRIDGE_DEBUG=1 + ;-D MESH_PACKET_LOGGING=1 + ;-D MESH_DEBUG=1 +build_src_filter = ${rak3401.build_src_filter} + + + + + +<../examples/simple_repeater> + [env:RAK_3401_room_server] extends = rak3401 build_flags = diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 0dfbf79775..453924adb7 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -118,6 +118,26 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> +[env:RAK_4631_repeater_bridge_nrf52] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"nRF52 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_NRF52_WIRELESS_BRIDGE=1 +; -D NRF52_WIRELESS_BRIDGE_LBT_RSSI=-70 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rak4631.build_src_filter} + + + + + +<../examples/simple_repeater> + [env:RAK_4631_room_server] extends = rak4631 build_flags = diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index 70405218f0..88683de906 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -51,6 +51,23 @@ build_flags = build_src_filter = ${SenseCap_Solar.build_src_filter} +<../examples/simple_repeater/*.cpp> +[env:SenseCap_Solar_repeater_bridge_nrf52] +extends = SenseCap_Solar +build_flags = + ${SenseCap_Solar.build_flags} + -D ADVERT_NAME='"nRF52 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_NRF52_WIRELESS_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${SenseCap_Solar.build_src_filter} + + + +<../examples/simple_repeater/*.cpp> + [env:SenseCap_Solar_room_server] extends = SenseCap_Solar build_flags = diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index 19139c289e..1266077550 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -41,6 +41,24 @@ build_flags = lib_deps = ${WioTrackerL1.lib_deps} adafruit/RTClib @ ^2.1.3 +[env:WioTrackerL1_repeater_bridge_nrf52] +extends = WioTrackerL1 +build_src_filter = ${WioTrackerL1.build_src_filter} + + + +<../examples/simple_repeater> +build_flags = + ${WioTrackerL1.build_flags} + -D ADVERT_NAME='"nRF52 Bridge"' + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D DISPLAY_CLASS=SH1106Display + -D WITH_NRF52_WIRELESS_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1.lib_deps} + adafruit/RTClib @ ^2.1.3 + [env:WioTrackerL1_room_server] extends = WioTrackerL1 build_src_filter = ${WioTrackerL1.build_src_filter} diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 1076bd8de7..7c469b5d26 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -98,6 +98,23 @@ build_flags = build_src_filter = ${Xiao_nrf52.build_src_filter} +<../examples/simple_repeater/*.cpp> +[env:Xiao_nrf52_repeater_bridge_nrf52] +extends = Xiao_nrf52 +build_flags = + ${Xiao_nrf52.build_flags} + -D ADVERT_NAME='"nRF52 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_NRF52_WIRELESS_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_nrf52.build_src_filter} + + + +<../examples/simple_repeater/*.cpp> + [env:Xiao_nrf52_room_server] extends = Xiao_nrf52 build_flags =