Compare commits
1
Commits
2.7
...
ble-mesh-working
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84355d15a |
@@ -0,0 +1,625 @@
|
||||
# BLE Mesh Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Add BLE as a mesh transport alongside LoRa, enabling Meshtastic nodes to relay packets over Bluetooth Low Energy. Follow the same handler pattern used by UDP multicast ([UdpMulticastHandler](src/mesh/udp/UdpMulticastHandler.h)) — a standalone handler class with `onSend()` hooked into `Router::send()` and received packets injected via `router->enqueueReceivedMessage()`. No changes to `RadioInterface` or Router architecture needed.
|
||||
|
||||
BLE mesh is useful for:
|
||||
|
||||
- Dense indoor deployments where LoRa range is excessive
|
||||
- Extending mesh reach through devices that lack LoRa hardware
|
||||
- Hybrid LoRa+BLE networks (gateway nodes bridge both transports)
|
||||
- Lower latency links between nearby nodes
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Pattern: Same as UDP Multicast
|
||||
|
||||
```
|
||||
Router::send()
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
iface->send(p) udpHandler->onSend(p) bleMeshHandler->onSend(p)
|
||||
(LoRa) (UDP) (BLE mesh)
|
||||
|
||||
Router::enqueueReceivedMessage()
|
||||
▲
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
LoRa RX ISR udpHandler->onReceive() bleMeshHandler->onReceive()
|
||||
```
|
||||
|
||||
The existing UDP pattern ([Router.cpp:378-382](src/mesh/Router.cpp#L378-L382)):
|
||||
|
||||
```cpp
|
||||
#if HAS_UDP_MULTICAST
|
||||
if (udpHandler && config.network.enabled_protocols & ...) {
|
||||
udpHandler->onSend(const_cast<meshtastic_MeshPacket *>(p));
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
BLE mesh hooks in identically, right next to it.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Protobuf & Configuration
|
||||
|
||||
### 1.1 Transport mechanism enum
|
||||
|
||||
**File:** protobufs repo (`mesh.proto`)
|
||||
|
||||
Add a new transport type alongside the existing ones:
|
||||
|
||||
```protobuf
|
||||
enum TransportMechanism {
|
||||
// ... existing 0-7 ...
|
||||
TRANSPORT_BLE_MESH = 8;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 BLE Mesh configuration
|
||||
|
||||
Add BLE mesh config to the device config protobuf:
|
||||
|
||||
```protobuf
|
||||
message BleMeshConfig {
|
||||
bool enabled = 1;
|
||||
|
||||
// Operating mode
|
||||
enum Mode {
|
||||
OFF = 0;
|
||||
ADVERTISE_AND_SCAN = 1; // connectionless (extended adv)
|
||||
GATT = 2; // connection-based
|
||||
HYBRID = 3; // both
|
||||
}
|
||||
Mode mode = 2;
|
||||
|
||||
// Scan/advertising interval in ms (latency vs power tradeoff)
|
||||
uint32_t interval_ms = 3; // default: 500ms
|
||||
|
||||
// TX power level (platform-mapped)
|
||||
int8_t tx_power = 4; // dBm
|
||||
|
||||
// Max BLE mesh peers to track
|
||||
uint32_t max_peers = 5; // default: 8
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: BLE Mesh Handler — Core Class
|
||||
|
||||
### 2.1 Base handler (platform-agnostic)
|
||||
|
||||
**New file:** `src/mesh/BLEMeshHandler.h`
|
||||
|
||||
Follow the `UdpMulticastHandler` pattern — a simple class, not a `RadioInterface`:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#if HAS_BLE_MESH
|
||||
|
||||
#include "configuration.h"
|
||||
#include "mesh/Router.h"
|
||||
|
||||
class BLEMeshHandler {
|
||||
public:
|
||||
BLEMeshHandler() : isRunning(false) {}
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
|
||||
// Called from Router::send() — broadcast packet over BLE
|
||||
virtual bool onSend(const meshtastic_MeshPacket *mp) = 0;
|
||||
|
||||
protected:
|
||||
bool isRunning;
|
||||
|
||||
// Shared RX path: decode and enqueue into router
|
||||
void deliverToRouter(const uint8_t *data, size_t len) {
|
||||
meshtastic_MeshPacket mp;
|
||||
if (!pb_decode_from_bytes(data, len, &meshtastic_MeshPacket_msg, &mp))
|
||||
return;
|
||||
if (mp.which_payload_variant != meshtastic_MeshPacket_encrypted_tag)
|
||||
return;
|
||||
|
||||
mp.transport_mechanism =
|
||||
meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH;
|
||||
mp.rx_snr = 0;
|
||||
mp.rx_rssi = 0;
|
||||
|
||||
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
|
||||
if (router)
|
||||
router->enqueueReceivedMessage(p.release());
|
||||
}
|
||||
|
||||
// Shared TX path: encode packet to bytes
|
||||
size_t encodeForBLE(const meshtastic_MeshPacket *mp, uint8_t *buf, size_t bufLen) {
|
||||
return pb_encode_to_bytes(buf, bufLen, &meshtastic_MeshPacket_msg, mp);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // HAS_BLE_MESH
|
||||
```
|
||||
|
||||
### 2.2 Wire format
|
||||
|
||||
Same as UDP multicast — protobuf-encoded `meshtastic_MeshPacket` (already encrypted). This means:
|
||||
|
||||
- Packets are encoded with `pb_encode_to_bytes()` on TX
|
||||
- Decoded with `pb_decode_from_bytes()` on RX
|
||||
- Encryption already applied by Router before `onSend()` is called
|
||||
- Same deduplication via `PacketHistory` (keyed on `sender, id`)
|
||||
|
||||
### 2.3 Echo prevention
|
||||
|
||||
Same pattern as UDP ([UdpMulticastHandler.h:102](src/mesh/udp/UdpMulticastHandler.h#L102)):
|
||||
|
||||
```cpp
|
||||
if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH) {
|
||||
LOG_ERROR("Attempt to send BLE-sourced packet over BLE");
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: ESP32 Implementation (NimBLE)
|
||||
|
||||
### Platform: ESP32, ESP32-S3, ESP32-C3, ESP32-C6
|
||||
|
||||
**New files:** `src/nimble/NimbleBLEMesh.h` / `NimbleBLEMesh.cpp`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#if HAS_BLE_MESH && defined(ARCH_ESP32)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
|
||||
class NimbleBLEMesh : public BLEMeshHandler {
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
|
||||
private:
|
||||
// --- Advertising mode ---
|
||||
void startScanning();
|
||||
void stopScanning();
|
||||
static void onScanResult(const ble_gap_disc_desc *desc, void *arg);
|
||||
void broadcastViaAdv(const uint8_t *data, size_t len);
|
||||
|
||||
// --- GATT mode ---
|
||||
void setupMeshService();
|
||||
static int onMeshCharWrite(uint16_t conn_handle,
|
||||
const ble_gatt_error *error,
|
||||
ble_gatt_attr *attr, void *arg);
|
||||
void sendViaGATT(const uint8_t *data, size_t len);
|
||||
|
||||
// --- Peer tracking ---
|
||||
struct BLEPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
uint16_t connHandle; // 0 if not connected
|
||||
};
|
||||
std::vector<BLEPeer> peers;
|
||||
void updatePeer(const ble_addr_t &addr, NodeNum nodeNum, int8_t rssi);
|
||||
void pruneStale();
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### 3.1 Advertising mode (connectionless)
|
||||
|
||||
Encode mesh packets into BLE extended advertisements with Meshtastic manufacturer data.
|
||||
|
||||
**TX flow:**
|
||||
|
||||
1. `onSend()` called from `Router::send()`
|
||||
2. Encode packet via `encodeForBLE()`
|
||||
3. Pack into extended advertisement manufacturer data
|
||||
4. Broadcast for configurable number of intervals
|
||||
|
||||
**RX flow:**
|
||||
|
||||
1. NimBLE scan callback fires
|
||||
2. Check manufacturer ID for Meshtastic magic
|
||||
3. Extract protobuf bytes
|
||||
4. Call `deliverToRouter()`
|
||||
|
||||
**Advertisement structure:**
|
||||
|
||||
```
|
||||
AD Type: 0xFF (Manufacturer Specific)
|
||||
Company ID: 0x????? (Meshtastic BLE SIG ID or 0xFFFF)
|
||||
Mesh Version: 1 byte
|
||||
Protobuf payload: variable (up to ~244 bytes with extended adv)
|
||||
```
|
||||
|
||||
**Key NimBLE APIs:**
|
||||
|
||||
- `ble_gap_ext_adv_set_data()` / `ble_gap_ext_adv_start()` — extended adv TX
|
||||
- `ble_gap_disc()` or `ble_gap_ext_disc()` — scanning
|
||||
- Scan callback receives `ble_gap_disc_desc` or `ble_gap_ext_disc_desc`
|
||||
|
||||
### 3.2 GATT mode (connection-based)
|
||||
|
||||
Add a new GATT service for mesh relay, separate from the phone API service:
|
||||
|
||||
**New Mesh Relay Service:**
|
||||
|
||||
```
|
||||
Mesh Relay Service UUID: (new, distinct from phone service in BluetoothCommon.h)
|
||||
├── MeshTX (WRITE_NO_RSP) — peers write mesh packets to us
|
||||
├── MeshRX (NOTIFY) — we notify peers of new mesh packets
|
||||
└── MeshPeer (READ) — our NodeNum for discovery
|
||||
```
|
||||
|
||||
**TX:** Write to MeshTX on all connected peers (broadcast) or specific next-hop peer (directed).
|
||||
**RX:** MeshTX write callback → `deliverToRouter()`.
|
||||
|
||||
### 3.3 Coexistence with phone BLE
|
||||
|
||||
Both services registered on the same NimBLE host:
|
||||
|
||||
- Phone API service: existing UUIDs ([BluetoothCommon.h](src/BluetoothCommon.h))
|
||||
- Mesh relay service: new UUIDs
|
||||
- Separate connection handles for phone vs mesh peers
|
||||
- Use extended adv set 0 for phone, set 1 for mesh (NimBLE supports multiple adv sets)
|
||||
- Reserve 1 connection for phone, remaining for mesh peers
|
||||
|
||||
### 3.4 ESP32 variant considerations
|
||||
|
||||
| Variant | BLE | Extended Adv | Max Conn | Strategy |
|
||||
| -------- | --- | ------------ | -------- | --------------------------------------- |
|
||||
| ESP32 | 4.2 | No | 9 | GATT-only (legacy adv too small at 31B) |
|
||||
| ESP32-S3 | 5.0 | Yes | 9 | Full adv+GATT |
|
||||
| ESP32-C3 | 5.0 | Yes | 3 | Adv preferred (limited connections) |
|
||||
| ESP32-C6 | 5.0 | Yes | 9 | Full adv+GATT |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: nRF52 Implementation (SoftDevice/Bluefruit)
|
||||
|
||||
### Platform: nRF52840 (primary), nRF52833
|
||||
|
||||
**New files:** `src/platform/nrf52/NRF52BLEMesh.h` / `NRF52BLEMesh.cpp`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#if HAS_BLE_MESH && defined(ARCH_NRF52)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
#include <bluefruit.h>
|
||||
|
||||
class NRF52BLEMesh : public BLEMeshHandler {
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
|
||||
private:
|
||||
// Advertising mode
|
||||
void startScanning();
|
||||
static void onScanCallback(ble_gap_evt_adv_report_t *report);
|
||||
void broadcastViaAdv(const uint8_t *data, size_t len);
|
||||
|
||||
// GATT mode
|
||||
BLEService meshService;
|
||||
BLECharacteristic meshTxChar;
|
||||
BLECharacteristic meshRxChar;
|
||||
void setupMeshService();
|
||||
static void onMeshWrite(uint16_t connHandle, BLECharacteristic *chr,
|
||||
uint8_t *data, uint16_t len);
|
||||
void sendViaGATT(const uint8_t *data, size_t len);
|
||||
|
||||
// Peers
|
||||
struct BLEPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_gap_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
uint16_t connHandle;
|
||||
};
|
||||
std::vector<BLEPeer> peers;
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### 4.1 Advertising mode
|
||||
|
||||
nRF52840 supports BLE 5.0 extended advertisements (up to 247 bytes).
|
||||
|
||||
**Bluefruit APIs:**
|
||||
|
||||
- `Bluefruit.Scanner.setRxCallback()` — scan result callback
|
||||
- `Bluefruit.Scanner.start()` / `.stop()` — control scanning
|
||||
- `Bluefruit.Advertising` — for standard adv
|
||||
- Raw SoftDevice `sd_ble_gap_adv_set_configure()` if Bluefruit doesn't expose extended adv data
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Use Bluefruit Scanner for RX (filter by Meshtastic manufacturer ID)
|
||||
- For TX, either use Bluefruit Advertising API or drop to raw SoftDevice for extended adv payloads
|
||||
|
||||
### 4.2 GATT mode
|
||||
|
||||
Same service structure as ESP32, using Bluefruit GATT APIs:
|
||||
|
||||
```cpp
|
||||
meshService = BLEService(MESH_RELAY_SERVICE_UUID);
|
||||
meshTxChar = BLECharacteristic(MESH_TX_CHAR_UUID);
|
||||
meshTxChar.setProperties(CHR_PROPS_WRITE_WO_RESP);
|
||||
meshTxChar.setWriteCallback(onMeshWrite);
|
||||
meshTxChar.setMaxLen(512);
|
||||
|
||||
meshRxChar = BLECharacteristic(MESH_RX_CHAR_UUID);
|
||||
meshRxChar.setProperties(CHR_PROPS_NOTIFY);
|
||||
meshRxChar.setMaxLen(512);
|
||||
```
|
||||
|
||||
**Peer connections:**
|
||||
|
||||
- nRF52840 SoftDevice S140 supports up to 20 concurrent connections
|
||||
- Use `Bluefruit.Central` for outbound connections to mesh peers
|
||||
- Bluefruit supports simultaneous peripheral (phone) + central (mesh peers)
|
||||
|
||||
### 4.3 Coexistence with phone BLE
|
||||
|
||||
Existing phone service ([NRF52Bluetooth.cpp](src/platform/nrf52/NRF52Bluetooth.cpp)) uses peripheral role:
|
||||
|
||||
- Keep phone API service as-is
|
||||
- Add mesh relay service alongside it
|
||||
- Scanner shared between peer discovery and mesh RX (filter by service UUID or manufacturer data)
|
||||
- SoftDevice connection budget: 1 peripheral (phone) + N central (mesh peers)
|
||||
|
||||
### 4.4 nRF52 extras
|
||||
|
||||
| Feature | nRF52840 | nRF52833 |
|
||||
| ---------------------- | -------- | -------- |
|
||||
| Extended Adv | Yes | Yes |
|
||||
| Max Connections | 20 | 20 |
|
||||
| RAM per connection | ~1.8KB | ~1.8KB |
|
||||
| Coded PHY (Long Range) | Yes | Yes |
|
||||
|
||||
**Coded PHY:** BLE Coded PHY (125kbps) provides ~4x range vs 1Mbps PHY. Could be used for extended-range BLE mesh links as a configuration option.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Router Integration
|
||||
|
||||
### 5.1 Hook into Router::send()
|
||||
|
||||
**File:** [src/mesh/Router.cpp](src/mesh/Router.cpp) — add right next to UDP handler:
|
||||
|
||||
```cpp
|
||||
#if HAS_BLE_MESH
|
||||
if (bleMeshHandler && config.bluetooth.mesh_enabled) {
|
||||
bleMeshHandler->onSend(p);
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
### 5.2 Global handler instance
|
||||
|
||||
**File:** [src/main.h](src/main.h):
|
||||
|
||||
```cpp
|
||||
#ifdef HAS_BLE_MESH
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
extern BLEMeshHandler *bleMeshHandler;
|
||||
#endif
|
||||
```
|
||||
|
||||
**File:** [src/main.cpp](src/main.cpp):
|
||||
|
||||
```cpp
|
||||
#ifdef HAS_BLE_MESH
|
||||
BLEMeshHandler *bleMeshHandler = nullptr;
|
||||
#endif
|
||||
|
||||
// In setup():
|
||||
#ifdef HAS_BLE_MESH
|
||||
#if defined(ARCH_ESP32)
|
||||
bleMeshHandler = new NimbleBLEMesh();
|
||||
#elif defined(ARCH_NRF52)
|
||||
bleMeshHandler = new NRF52BLEMesh();
|
||||
#endif
|
||||
bleMeshHandler->start();
|
||||
#endif
|
||||
```
|
||||
|
||||
### 5.3 NODENUM_BROADCAST_NO_LORA
|
||||
|
||||
Already reserved ([MeshTypes.h:13-14](src/mesh/MeshTypes.h#L13-L14)) and already dropped by LoRa ([RadioLibInterface.cpp:170](src/mesh/RadioLibInterface.cpp#L170)). BLE mesh handler should still send packets addressed to `NODENUM_BROADCAST_NO_LORA` — this is its purpose.
|
||||
|
||||
### 5.4 Feature flag
|
||||
|
||||
**File:** `src/configuration.h` or per-variant `platformio.ini`:
|
||||
|
||||
```cpp
|
||||
#define HAS_BLE_MESH 1 // Enable BLE mesh transport
|
||||
```
|
||||
|
||||
Disabled by default. Enabled per-board in variant configs for boards with sufficient BLE capability.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Peer Discovery & Management
|
||||
|
||||
### 6.1 Discovery methods
|
||||
|
||||
1. **Passive scanning**: Listen for Meshtastic manufacturer data in BLE advertisements
|
||||
2. **Service discovery**: Scan for Mesh Relay Service UUID (GATT mode)
|
||||
3. **NodeDB integration**: Update `meshtastic_NodeInfoLite` with BLE reachability when a peer is discovered
|
||||
|
||||
### 6.2 Peer table
|
||||
|
||||
```cpp
|
||||
struct BLEMeshPeer {
|
||||
NodeNum nodeNum;
|
||||
// Platform-specific BLE address (ble_addr_t on ESP32, ble_gap_addr_t on nRF52)
|
||||
uint8_t addr[6];
|
||||
uint8_t addrType;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
uint16_t connHandle; // 0 = not connected
|
||||
uint8_t failedConnAttempts;
|
||||
};
|
||||
```
|
||||
|
||||
Max peers: configurable (default 8), bounded by platform connection limits.
|
||||
|
||||
### 6.3 Connection policy (GATT mode)
|
||||
|
||||
- Auto-connect if RSSI > threshold and slots available
|
||||
- Disconnect stale peers (no traffic for N minutes)
|
||||
- Prefer advertising for broadcasts, GATT for directed/reliable
|
||||
- Exponential backoff on failed connection attempts
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Power Management
|
||||
|
||||
### 7.1 Scan duty cycling
|
||||
|
||||
```
|
||||
Active scanning: ~15mA (nRF52), ~90mA (ESP32)
|
||||
Advertising: ~5mA peak per event
|
||||
Idle: ~0mA additional
|
||||
|
||||
Default strategy:
|
||||
- Scan window: 50ms every 500ms (10% duty cycle)
|
||||
- ~1.5mA average on nRF52, ~9mA on ESP32
|
||||
- Configurable via BleMeshConfig.interval_ms
|
||||
```
|
||||
|
||||
### 7.2 Sleep integration
|
||||
|
||||
- Light sleep: pause scanning, continue advertising at reduced rate
|
||||
- Deep sleep: disable BLE mesh entirely
|
||||
- Hook into existing power management observers
|
||||
|
||||
### 7.3 Role-based behavior
|
||||
|
||||
| Device Role | BLE Mesh Behavior |
|
||||
| ----------- | ------------------------------------------- |
|
||||
| CLIENT | Scan on-demand, advertise when sending |
|
||||
| CLIENT_MUTE | Receive only, no rebroadcast |
|
||||
| ROUTER | Continuous scan+advertise, max connections |
|
||||
| ROUTER_LATE | Delayed rebroadcast (same as LoRa behavior) |
|
||||
| SENSOR | Minimal: advertise telemetry only |
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Testing & Validation
|
||||
|
||||
### 8.1 Unit tests
|
||||
|
||||
- BLE wire format encode/decode roundtrip
|
||||
- PacketHistory deduplication across LoRa + BLE
|
||||
- Peer table management (add, prune, limits)
|
||||
- Echo prevention (don't re-send BLE-sourced packets over BLE)
|
||||
|
||||
### 8.2 Integration tests
|
||||
|
||||
- Two ESP32 nodes: BLE-only mesh (no LoRa)
|
||||
- Two nRF52 nodes: BLE-only mesh
|
||||
- Three nodes: multi-hop BLE relay
|
||||
- Hybrid: Node A (LoRa+BLE) <-> Node B (BLE only) <-> Node C (LoRa+BLE)
|
||||
- Phone API + BLE mesh coexistence
|
||||
- Power consumption profiling
|
||||
|
||||
### 8.3 Compatibility matrix
|
||||
|
||||
| Test | ESP32 | ESP32-S3 | ESP32-C3 | ESP32-C6 | nRF52840 |
|
||||
| ------------- | ----------- | -------- | -------- | -------- | -------- |
|
||||
| Adv mode | GATT only\* | Yes | Yes | Yes | Yes |
|
||||
| GATT mode | Yes | Yes | Yes | Yes | Yes |
|
||||
| Phone coexist | Yes | Yes | Yes | Yes | Yes |
|
||||
| Multi-hop | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
\*ESP32 (BLE 4.2): legacy adv limited to 31B, use GATT-only mode
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Milestone 1: Foundation
|
||||
|
||||
1. Protobuf additions (`TRANSPORT_BLE_MESH`, `BleMeshConfig`)
|
||||
2. `BLEMeshHandler` base class
|
||||
3. Router hook (alongside UDP handler)
|
||||
4. Feature flag + main.cpp wiring
|
||||
|
||||
### Milestone 2: ESP32 MVP
|
||||
|
||||
5. `NimbleBLEMesh` — advertising mode on ESP32-S3
|
||||
6. Phone BLE coexistence validation
|
||||
7. Basic two-node BLE mesh test
|
||||
|
||||
### Milestone 3: nRF52 MVP
|
||||
|
||||
8. `NRF52BLEMesh` — advertising mode
|
||||
9. Cross-platform interop (ESP32 <-> nRF52 BLE mesh)
|
||||
|
||||
### Milestone 4: GATT Mode
|
||||
|
||||
10. GATT service + connection management for ESP32
|
||||
11. GATT service + connection management for nRF52
|
||||
12. Peer discovery & table management
|
||||
|
||||
### Milestone 5: Polish
|
||||
|
||||
13. Power management integration
|
||||
14. Full test matrix
|
||||
15. ESP32 (BLE 4.2) GATT-only fallback
|
||||
16. Admin UI / config in phone apps
|
||||
|
||||
---
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
| -------------------------------- | ------------------------- | ---------------------------------------------------------------------------- |
|
||||
| BLE scan power drain | Battery life | Duty cycling, role-based policy |
|
||||
| Phone BLE drops during mesh scan | UX regression | Separate adv sets, careful interleaving |
|
||||
| NimBLE/Bluefruit API differences | Platform code duplication | Thin platform subclasses, shared base logic |
|
||||
| No extended adv on ESP32 (4.2) | Can't fit packets in adv | GATT-only fallback |
|
||||
| BLE range (~30-100m) | Coverage gaps | Hybrid LoRa+BLE; Coded PHY on nRF52 |
|
||||
| Packet storms in dense BLE mesh | Saturation | Existing flood mitigation (PacketHistory, hop_limit, role-based rebroadcast) |
|
||||
|
||||
---
|
||||
|
||||
## Files to Create
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------------- | --------------------------------------------- |
|
||||
| `src/mesh/BLEMeshHandler.h` | Base handler class (like UdpMulticastHandler) |
|
||||
| `src/nimble/NimbleBLEMesh.h` | ESP32 BLE mesh header |
|
||||
| `src/nimble/NimbleBLEMesh.cpp` | ESP32 NimBLE implementation |
|
||||
| `src/platform/nrf52/NRF52BLEMesh.h` | nRF52 BLE mesh header |
|
||||
| `src/platform/nrf52/NRF52BLEMesh.cpp` | nRF52 Bluefruit implementation |
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Change |
|
||||
| --------------------------------------- | -------------------------------------------------- |
|
||||
| `src/mesh/Router.cpp` | Add `bleMeshHandler->onSend()` next to UDP handler |
|
||||
| `src/main.h` | Declare `extern BLEMeshHandler *bleMeshHandler` |
|
||||
| `src/main.cpp` | Instantiate + start BLE mesh handler |
|
||||
| `src/configuration.h` | `HAS_BLE_MESH` feature flag |
|
||||
| Protobufs (`mesh.proto`) | `TRANSPORT_BLE_MESH` enum + `BleMeshConfig` |
|
||||
| `src/nimble/NimbleBluetooth.cpp` | Coexistence (adv set separation) |
|
||||
| `src/platform/nrf52/NRF52Bluetooth.cpp` | Coexistence (scanner sharing) |
|
||||
| Platform `variant.h` / `platformio.ini` | Enable per-board |
|
||||
@@ -147,6 +147,15 @@ extern void tftSetup(void);
|
||||
UdpMulticastHandler *udpHandler = nullptr;
|
||||
#endif
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
BLEMeshHandler *bleMeshHandler = nullptr;
|
||||
#if defined(ARCH_NRF52)
|
||||
#include "platform/nrf52/NRF52BLEMesh.h"
|
||||
#elif defined(ARCH_ESP32)
|
||||
#include "platform/esp32/ESP32BLEMesh.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(TCXO_OPTIONAL)
|
||||
float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down.
|
||||
#endif
|
||||
@@ -884,6 +893,17 @@ void setup()
|
||||
udpHandler->start();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
LOG_DEBUG("Start BLE mesh handler");
|
||||
#if defined(ARCH_NRF52)
|
||||
bleMeshHandler = new NRF52BLEMesh();
|
||||
#elif defined(ARCH_ESP32)
|
||||
bleMeshHandler = new ESP32BLEMesh();
|
||||
#endif
|
||||
if (bleMeshHandler)
|
||||
bleMeshHandler->start();
|
||||
#endif
|
||||
service = new MeshService();
|
||||
service->init();
|
||||
|
||||
@@ -62,6 +62,11 @@ extern AudioThread *audioThread;
|
||||
extern UdpMulticastHandler *udpHandler;
|
||||
#endif
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
extern BLEMeshHandler *bleMeshHandler;
|
||||
#endif
|
||||
|
||||
// Global Screen singleton.
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
|
||||
#include "MeshTypes.h"
|
||||
#include "Router.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
|
||||
// Meshtastic BLE mesh manufacturer data identifier
|
||||
// Using 0xFFFF (reserved for testing) until a real BLE SIG company ID is assigned
|
||||
#define BLE_MESH_COMPANY_ID 0xFFFF
|
||||
#define BLE_MESH_PROTOCOL_VERSION 1
|
||||
|
||||
// TODO: Add TRANSPORT_BLE_MESH = 8 to mesh.proto TransportMechanism enum and regenerate.
|
||||
// Until then, define it here so the code compiles.
|
||||
#ifndef meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH
|
||||
#define meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH ((meshtastic_MeshPacket_TransportMechanism)8)
|
||||
#endif
|
||||
|
||||
class BLEMeshHandler
|
||||
{
|
||||
public:
|
||||
BLEMeshHandler() : isRunning(false) {}
|
||||
virtual ~BLEMeshHandler() {}
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
virtual void onBluetoothReady() {}
|
||||
|
||||
// Called from Router::send() to broadcast packet over BLE mesh
|
||||
virtual bool onSend(const meshtastic_MeshPacket *mp) = 0;
|
||||
|
||||
protected:
|
||||
bool isRunning;
|
||||
|
||||
// Decode received BLE mesh data and enqueue into router
|
||||
void deliverToRouter(const uint8_t *data, size_t len)
|
||||
{
|
||||
meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero;
|
||||
bool decoded = pb_decode_from_bytes(data, len, &meshtastic_MeshPacket_msg, &mp);
|
||||
if (!decoded || mp.which_payload_variant != meshtastic_MeshPacket_encrypted_tag)
|
||||
return;
|
||||
|
||||
mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH;
|
||||
mp.pki_encrypted = false;
|
||||
mp.public_key.size = 0;
|
||||
memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes));
|
||||
mp.rx_snr = 0;
|
||||
mp.rx_rssi = 0;
|
||||
|
||||
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
|
||||
if (p && router)
|
||||
router->enqueueReceivedMessage(p.release());
|
||||
}
|
||||
|
||||
// Encode a mesh packet for BLE transmission, returns encoded length
|
||||
size_t encodeForBLE(const meshtastic_MeshPacket *mp, uint8_t *buf, size_t bufLen)
|
||||
{
|
||||
return pb_encode_to_bytes(buf, bufLen, &meshtastic_MeshPacket_msg, mp);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // HAS_BLE_MESH
|
||||
@@ -381,6 +381,12 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
if (bleMeshHandler) {
|
||||
bleMeshHandler->onSend(p);
|
||||
}
|
||||
#endif
|
||||
|
||||
assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside)
|
||||
return iface->send(p);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,17 @@
|
||||
#include "mesh/PhoneAPI.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include "sleep.h"
|
||||
#include <NimBLEAdvertising.h>
|
||||
#include <NimBLEDevice.h>
|
||||
#include <Preferences.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
#ifdef ARCH_ESP32
|
||||
#include <nvs.h>
|
||||
#endif
|
||||
|
||||
#if defined(NIMBLE_TWO) || (defined(CONFIG_BT_NIMBLE_EXT_ADV) && CONFIG_BT_NIMBLE_EXT_ADV)
|
||||
#include "NimBLEAdvertising.h"
|
||||
#include "NimBLEExtAdvertising.h"
|
||||
#include "PowerStatus.h"
|
||||
@@ -731,7 +737,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
// Restart Advertising
|
||||
ble->startAdvertising();
|
||||
#else
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
auto *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
if (!pAdvertising->start(0)) {
|
||||
if (pAdvertising->isAdvertising()) {
|
||||
LOG_DEBUG("BLE advertising already running");
|
||||
@@ -746,13 +752,48 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
static NimbleBluetoothToRadioCallback *toRadioCallbacks;
|
||||
static NimbleBluetoothFromRadioCallback *fromRadioCallbacks;
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
static void clearCorruptBondStoreOnce()
|
||||
{
|
||||
Preferences prefs;
|
||||
if (!prefs.begin("meshtastic", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool alreadyCleared = prefs.getBool("nimbleBondClr", false);
|
||||
if (alreadyCleared) {
|
||||
prefs.end();
|
||||
return;
|
||||
}
|
||||
|
||||
nvs_handle_t nimbleHandle;
|
||||
esp_err_t err = nvs_open("nimble_bond", NVS_READWRITE, &nimbleHandle);
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_erase_all(nimbleHandle);
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_commit(nimbleHandle);
|
||||
}
|
||||
nvs_close(nimbleHandle);
|
||||
|
||||
if (err == ESP_OK) {
|
||||
LOG_WARN("Cleared NimBLE bond database from NVS (one-time recovery)");
|
||||
} else {
|
||||
LOG_WARN("Failed clearing NimBLE bond database, err=%d", err);
|
||||
}
|
||||
}
|
||||
|
||||
prefs.putBool("nimbleBondClr", true);
|
||||
prefs.end();
|
||||
}
|
||||
#endif
|
||||
|
||||
void NimbleBluetooth::shutdown()
|
||||
{
|
||||
// No measurable power saving for ESP32 during light-sleep(?)
|
||||
#ifndef ARCH_ESP32
|
||||
// Shutdown bluetooth for minimum power draw
|
||||
LOG_INFO("Disable bluetooth");
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
auto *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->reset();
|
||||
pAdvertising->stop();
|
||||
#endif
|
||||
@@ -825,6 +866,11 @@ void NimbleBluetooth::setup()
|
||||
|
||||
LOG_INFO("Init the NimBLE bluetooth module");
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Recovery for units that carry corrupted bond blobs that crash NimBLE during populate_db_from_nvs.
|
||||
clearCorruptBondStoreOnce();
|
||||
#endif
|
||||
|
||||
NimBLEDevice::init(getDeviceName());
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
|
||||
@@ -867,6 +913,12 @@ void NimbleBluetooth::setup()
|
||||
bleServer->setCallbacks(serverCallbacks, true);
|
||||
setupService();
|
||||
startAdvertising();
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
if (bleMeshHandler) {
|
||||
bleMeshHandler->onBluetoothReady();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void NimbleBluetooth::setupService()
|
||||
@@ -922,8 +974,12 @@ void NimbleBluetooth::setupService()
|
||||
|
||||
void NimbleBluetooth::startAdvertising()
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
#if defined(NIMBLE_TWO) || (defined(CONFIG_BT_NIMBLE_EXT_ADV) && CONFIG_BT_NIMBLE_EXT_ADV)
|
||||
// NimBLE-Arduino 1.4.x can leave ext-adv callbacks uninitialized.
|
||||
// Explicitly install a default callback sink before starting any instance.
|
||||
static NimBLEExtAdvertisingCallbacks extAdvCallbacks;
|
||||
NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->setCallbacks(&extAdvCallbacks, false);
|
||||
NimBLEExtAdvertisement legacyAdvertising;
|
||||
|
||||
legacyAdvertising.setLegacyAdvertising(true);
|
||||
@@ -950,7 +1006,7 @@ void NimbleBluetooth::startAdvertising()
|
||||
LOG_ERROR("BLE failed to start legacyAdvertising");
|
||||
}
|
||||
#else
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
auto *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->reset();
|
||||
pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
|
||||
pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_BLE_MESH && defined(ARCH_ESP32)
|
||||
|
||||
#include "ESP32BLEMesh.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Router.h"
|
||||
|
||||
#if defined(CONFIG_NIMBLE_CPP_IDF)
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_hs_adv.h"
|
||||
#else
|
||||
#include "nimble/nimble/host/include/host/ble_gap.h"
|
||||
#include "nimble/nimble/host/include/host/ble_hs_adv.h"
|
||||
#endif
|
||||
|
||||
void ESP32BLEMesh::start()
|
||||
{
|
||||
if (isRunning) {
|
||||
LOG_DEBUG("BLE mesh already running");
|
||||
return;
|
||||
}
|
||||
|
||||
memset(peers, 0, sizeof(peers));
|
||||
peerCount = 0;
|
||||
bluetoothReady = false;
|
||||
isRunning = true;
|
||||
LOG_INFO("BLE mesh started (waiting for Bluetooth ready)");
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::onBluetoothReady()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
if (bluetoothReady)
|
||||
return;
|
||||
|
||||
bluetoothReady = true;
|
||||
startScanning();
|
||||
LOG_DEBUG("BLE mesh Bluetooth ready");
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::stop()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
stopScanning();
|
||||
isRunning = false;
|
||||
LOG_INFO("BLE mesh stopped");
|
||||
}
|
||||
|
||||
bool ESP32BLEMesh::onSend(const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
if (!isRunning || !mp)
|
||||
return false;
|
||||
|
||||
if (!bluetoothReady) {
|
||||
LOG_DEBUG("BLE mesh TX deferred, Bluetooth not ready");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't echo packets that arrived via BLE mesh
|
||||
if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH) {
|
||||
LOG_DEBUG("Drop BLE mesh echo");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t buffer[meshtastic_MeshPacket_size];
|
||||
size_t encodedLen = encodeForBLE(mp, buffer, sizeof(buffer));
|
||||
if (encodedLen == 0) {
|
||||
LOG_WARN("BLE mesh encode failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build manufacturer data: company ID (2 bytes) + version (1 byte) + protobuf payload
|
||||
size_t mfgDataLen = 2 + 1 + encodedLen;
|
||||
|
||||
// Extended advertising can carry up to 255 bytes total.
|
||||
// Reserve 5 bytes for the flags and manufacturer-data AD headers.
|
||||
if (mfgDataLen > 250) {
|
||||
LOG_WARN("BLE mesh packet too large: %u bytes", mfgDataLen);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t mfgData[250];
|
||||
mfgData[0] = (uint8_t)(BLE_MESH_COMPANY_ID & 0xFF);
|
||||
mfgData[1] = (uint8_t)((BLE_MESH_COMPANY_ID >> 8) & 0xFF);
|
||||
mfgData[2] = BLE_MESH_PROTOCOL_VERSION;
|
||||
memcpy(&mfgData[3], buffer, encodedLen);
|
||||
|
||||
// Stop scanning while we advertise
|
||||
stopScanning();
|
||||
|
||||
// Build raw advertisement data
|
||||
uint8_t advBuf[251];
|
||||
size_t advLen = 0;
|
||||
|
||||
// Flags AD structure (3 bytes)
|
||||
advBuf[advLen++] = 2; // length
|
||||
advBuf[advLen++] = BLE_HS_ADV_TYPE_FLAGS;
|
||||
advBuf[advLen++] = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
// Manufacturer data AD structure
|
||||
advBuf[advLen++] = (uint8_t)(mfgDataLen + 1); // length (type byte + data)
|
||||
advBuf[advLen++] = BLE_HS_ADV_TYPE_MFG_DATA;
|
||||
memcpy(&advBuf[advLen], mfgData, mfgDataLen);
|
||||
advLen += mfgDataLen;
|
||||
|
||||
const bool priorityRetries = shouldUsePriorityRetries(mp);
|
||||
const uint8_t extraRetries = priorityRetries ? BLE_MESH_PRIORITY_EXTRA_RETRIES : 0;
|
||||
bool sentAny = false;
|
||||
|
||||
for (uint8_t attempt = 0; attempt <= extraRetries; ++attempt) {
|
||||
if (attempt > 0) {
|
||||
const uint16_t retryDelayMs =
|
||||
random(BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS, BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS + 1);
|
||||
delay(retryDelayMs);
|
||||
}
|
||||
|
||||
if (sendAdvertisementBurst(advBuf, advLen, mp->id, encodedLen)) {
|
||||
sentAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Resume scanning
|
||||
startScanning();
|
||||
|
||||
return sentAny;
|
||||
}
|
||||
|
||||
bool ESP32BLEMesh::sendAdvertisementBurst(const uint8_t *advBuf, size_t advLen, PacketId packetId, size_t encodedLen)
|
||||
{
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
// Use extended advertising if available (ESP32-S3, C3, C6)
|
||||
struct ble_gap_ext_adv_params extAdvParams = {};
|
||||
extAdvParams.connectable = 0;
|
||||
extAdvParams.scannable = 0;
|
||||
extAdvParams.directed = 0;
|
||||
extAdvParams.high_duty_directed = 0;
|
||||
extAdvParams.legacy_pdu = 0;
|
||||
extAdvParams.anonymous = 0;
|
||||
extAdvParams.include_tx_power = 0;
|
||||
extAdvParams.scan_req_notif = 0;
|
||||
extAdvParams.itvl_min = BLE_MESH_ADV_INTERVAL;
|
||||
extAdvParams.itvl_max = BLE_MESH_ADV_INTERVAL;
|
||||
extAdvParams.channel_map = 0; // all channels
|
||||
extAdvParams.own_addr_type = BLE_OWN_ADDR_PUBLIC;
|
||||
extAdvParams.primary_phy = BLE_HCI_LE_PHY_1M;
|
||||
extAdvParams.secondary_phy = BLE_HCI_LE_PHY_1M;
|
||||
extAdvParams.tx_power = 127; // host picks max
|
||||
|
||||
// Use instance 1 to avoid conflicting with phone advertising on instance 0
|
||||
const uint8_t meshAdvInstance = 1;
|
||||
int8_t selectedTxPower = 0;
|
||||
|
||||
int rc = ble_gap_ext_adv_configure(meshAdvInstance, &extAdvParams, &selectedTxPower, onGapEvent, this);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh ext adv configure failed: %d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct os_mbuf *advData = os_msys_get_pkthdr(advLen, 0);
|
||||
if (!advData) {
|
||||
LOG_WARN("BLE mesh: failed to allocate mbuf");
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return false;
|
||||
}
|
||||
rc = os_mbuf_append(advData, advBuf, advLen);
|
||||
if (rc != 0) {
|
||||
os_mbuf_free_chain(advData);
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return false;
|
||||
}
|
||||
|
||||
rc = ble_gap_ext_adv_set_data(meshAdvInstance, advData);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh ext adv set data failed: %d", rc);
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Advertise for BLE_MESH_ADV_COUNT * 100ms, then stop.
|
||||
rc = ble_gap_ext_adv_start(meshAdvInstance, BLE_MESH_ADV_COUNT, 0);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh ext adv start failed: %d", rc);
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("BLE mesh adv sent (id=%u, len=%u)", packetId, encodedLen);
|
||||
delay(BLE_MESH_ADV_BURST_MS);
|
||||
ble_gap_ext_adv_stop(meshAdvInstance);
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return true;
|
||||
#else
|
||||
// Legacy advertising fallback (ESP32 classic - BLE 4.2, limited to 31 bytes)
|
||||
if (advLen > 31) {
|
||||
LOG_WARN("BLE mesh packet too large for legacy adv: %u bytes", advLen);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ble_gap_adv_params legacyAdvParams = {};
|
||||
legacyAdvParams.conn_mode = BLE_GAP_CONN_MODE_NON;
|
||||
legacyAdvParams.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
legacyAdvParams.itvl_min = BLE_MESH_ADV_INTERVAL;
|
||||
legacyAdvParams.itvl_max = BLE_MESH_ADV_INTERVAL;
|
||||
legacyAdvParams.channel_map = 0; // all channels
|
||||
|
||||
int rc = ble_gap_adv_set_data(advBuf, advLen);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh legacy adv set data failed: %d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32_t durationMs = BLE_MESH_ADV_BURST_MS;
|
||||
rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, durationMs / 10, &legacyAdvParams, onGapEvent, this);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh legacy adv start failed: %d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("BLE mesh adv sent (id=%u, len=%u)", packetId, encodedLen);
|
||||
delay(durationMs);
|
||||
ble_gap_adv_stop();
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ESP32BLEMesh::shouldUsePriorityRetries(const meshtastic_MeshPacket *mp) const
|
||||
{
|
||||
if (!mp)
|
||||
return false;
|
||||
|
||||
// Prioritize retries for ack-sensitive traffic and direct messages only.
|
||||
const bool isDirect = (mp->to != 0) && !isBroadcast(mp->to);
|
||||
return mp->want_ack || isDirect;
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::startScanning()
|
||||
{
|
||||
if (!bluetoothReady)
|
||||
return;
|
||||
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
struct ble_gap_ext_disc_params uncodedParams = {};
|
||||
uncodedParams.itvl = BLE_MESH_SCAN_INTERVAL;
|
||||
uncodedParams.window = BLE_MESH_SCAN_WINDOW;
|
||||
uncodedParams.passive = 1;
|
||||
|
||||
int rc = ble_gap_ext_disc(BLE_OWN_ADDR_PUBLIC,
|
||||
0, // duration 0 => forever
|
||||
0, // period
|
||||
0, // no duplicate filtering
|
||||
BLE_HCI_SCAN_FILT_NO_WL,
|
||||
0, // not limited
|
||||
&uncodedParams, NULL, onGapEvent, this);
|
||||
#else
|
||||
struct ble_gap_disc_params scanParams = {};
|
||||
scanParams.passive = 1; // Passive scan to save power
|
||||
scanParams.itvl = BLE_MESH_SCAN_INTERVAL;
|
||||
scanParams.window = BLE_MESH_SCAN_WINDOW;
|
||||
scanParams.filter_duplicates = 0; // We want to see repeated mesh advertisements
|
||||
scanParams.limited = 0;
|
||||
scanParams.filter_policy = BLE_HCI_SCAN_FILT_NO_WL;
|
||||
|
||||
int rc = ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, &scanParams, onGapEvent, this);
|
||||
#endif
|
||||
if (rc == 0) {
|
||||
LOG_DEBUG("BLE mesh scanning started");
|
||||
} else if (rc == BLE_HS_EALREADY) {
|
||||
LOG_DEBUG("BLE mesh scanning already active");
|
||||
} else {
|
||||
LOG_WARN("BLE mesh scan start failed: %d", rc);
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::stopScanning()
|
||||
{
|
||||
if (!bluetoothReady)
|
||||
return;
|
||||
|
||||
ble_gap_disc_cancel();
|
||||
}
|
||||
|
||||
int ESP32BLEMesh::onGapEvent(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
auto *self = static_cast<ESP32BLEMesh *>(arg);
|
||||
if (!self)
|
||||
return 0;
|
||||
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_DISC:
|
||||
self->handleAdvertisement(&event->disc);
|
||||
break;
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
case BLE_GAP_EVENT_EXT_DISC:
|
||||
self->handleExtendedAdvertisement(&event->ext_disc);
|
||||
break;
|
||||
#endif
|
||||
case BLE_GAP_EVENT_DISC_COMPLETE:
|
||||
// Scanning timed out or was stopped - restart if still running
|
||||
if (self->isRunning) {
|
||||
self->startScanning();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::handleAdvertisement(const struct ble_gap_disc_desc *desc)
|
||||
{
|
||||
if (!isRunning || !desc)
|
||||
return;
|
||||
|
||||
handleAdvertisementData(desc->addr, desc->rssi, desc->data, desc->length_data);
|
||||
}
|
||||
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
void ESP32BLEMesh::handleExtendedAdvertisement(const struct ble_gap_ext_disc_desc *desc)
|
||||
{
|
||||
if (!isRunning || !desc)
|
||||
return;
|
||||
|
||||
if (desc->data_status != BLE_GAP_EXT_ADV_DATA_STATUS_COMPLETE || !desc->data)
|
||||
return;
|
||||
|
||||
handleAdvertisementData(desc->addr, desc->rssi, desc->data, desc->length_data);
|
||||
}
|
||||
#endif
|
||||
|
||||
void ESP32BLEMesh::handleAdvertisementData(const ble_addr_t &addr, int8_t rssi, const uint8_t *data, uint8_t len)
|
||||
{
|
||||
if (!isRunning || !data)
|
||||
return;
|
||||
|
||||
// Parse advertisement data to find manufacturer-specific data
|
||||
uint16_t offset = 0;
|
||||
|
||||
while (offset < len) {
|
||||
uint8_t adLen = data[offset];
|
||||
if (adLen == 0 || offset + adLen >= len)
|
||||
break;
|
||||
|
||||
uint8_t adType = data[offset + 1];
|
||||
if (adType == BLE_HS_ADV_TYPE_MFG_DATA && adLen >= 4) {
|
||||
// Check company ID
|
||||
uint16_t companyId = data[offset + 2] | (data[offset + 3] << 8);
|
||||
if (companyId == BLE_MESH_COMPANY_ID) {
|
||||
// Check protocol version
|
||||
uint8_t version = data[offset + 4];
|
||||
if (version == BLE_MESH_PROTOCOL_VERSION) {
|
||||
// Extract protobuf payload (skip company ID + version = 3 bytes)
|
||||
const uint8_t *payload = &data[offset + 5];
|
||||
size_t payloadLen = adLen - 4; // adLen includes type byte, subtract type + company(2) + version(1)
|
||||
|
||||
LOG_DEBUG("BLE mesh RX: rssi=%d, len=%u", rssi, payloadLen);
|
||||
updatePeer(addr, rssi);
|
||||
deliverToRouter(payload, payloadLen);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += adLen + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::updatePeer(const ble_addr_t &addr, int8_t rssi)
|
||||
{
|
||||
uint32_t now = millis();
|
||||
|
||||
// Check if peer already known
|
||||
for (uint8_t i = 0; i < peerCount; i++) {
|
||||
if (memcmp(&peers[i].addr, &addr, sizeof(ble_addr_t)) == 0) {
|
||||
peers[i].rssi = rssi;
|
||||
peers[i].lastSeenMs = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new peer
|
||||
if (peerCount < BLE_MESH_MAX_PEERS) {
|
||||
peers[peerCount].addr = addr;
|
||||
peers[peerCount].rssi = rssi;
|
||||
peers[peerCount].lastSeenMs = now;
|
||||
peers[peerCount].nodeNum = 0;
|
||||
peerCount++;
|
||||
LOG_DEBUG("BLE mesh new peer (%u total)", peerCount);
|
||||
} else {
|
||||
// Replace oldest peer
|
||||
pruneStale();
|
||||
if (peerCount < BLE_MESH_MAX_PEERS) {
|
||||
peers[peerCount].addr = addr;
|
||||
peers[peerCount].rssi = rssi;
|
||||
peers[peerCount].lastSeenMs = now;
|
||||
peers[peerCount].nodeNum = 0;
|
||||
peerCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::pruneStale()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
uint8_t writeIdx = 0;
|
||||
|
||||
for (uint8_t i = 0; i < peerCount; i++) {
|
||||
if (now - peers[i].lastSeenMs < BLE_MESH_PEER_TIMEOUT_MS) {
|
||||
if (writeIdx != i)
|
||||
peers[writeIdx] = peers[i];
|
||||
writeIdx++;
|
||||
}
|
||||
}
|
||||
peerCount = writeIdx;
|
||||
}
|
||||
|
||||
#endif // HAS_BLE_MESH && ARCH_ESP32
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#if HAS_BLE_MESH && defined(ARCH_ESP32)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
|
||||
#if defined(CONFIG_NIMBLE_CPP_IDF)
|
||||
#include "host/ble_gap.h"
|
||||
#else
|
||||
#include "nimble/nimble/host/include/host/ble_gap.h"
|
||||
#endif
|
||||
|
||||
// Max number of BLE mesh peers we can track
|
||||
#ifndef BLE_MESH_MAX_PEERS
|
||||
#define BLE_MESH_MAX_PEERS 8
|
||||
#endif
|
||||
|
||||
// Scan interval and window in units of 0.625ms
|
||||
// Default: continuous scan (100% duty) to maximize packet capture reliability.
|
||||
// For lower power builds, these can be overridden in build flags.
|
||||
#ifndef BLE_MESH_SCAN_INTERVAL
|
||||
#define BLE_MESH_SCAN_INTERVAL 160 // 100ms
|
||||
#endif
|
||||
#ifndef BLE_MESH_SCAN_WINDOW
|
||||
#define BLE_MESH_SCAN_WINDOW 160 // 100ms
|
||||
#endif
|
||||
|
||||
// Advertising interval for mesh data in units of 0.625ms
|
||||
#ifndef BLE_MESH_ADV_INTERVAL
|
||||
#define BLE_MESH_ADV_INTERVAL 160 // 100ms
|
||||
#endif
|
||||
|
||||
// How many times to repeat a mesh advertisement
|
||||
#ifndef BLE_MESH_ADV_COUNT
|
||||
#define BLE_MESH_ADV_COUNT 3
|
||||
#endif
|
||||
|
||||
#ifndef BLE_MESH_ADV_BURST_MS
|
||||
#define BLE_MESH_ADV_BURST_MS (BLE_MESH_ADV_COUNT * 100)
|
||||
#endif
|
||||
|
||||
// Extra retry bursts for higher-priority traffic only.
|
||||
// Base burst always runs once for every packet.
|
||||
#ifndef BLE_MESH_PRIORITY_EXTRA_RETRIES
|
||||
#define BLE_MESH_PRIORITY_EXTRA_RETRIES 2
|
||||
#endif
|
||||
|
||||
// Randomized delay range between extra retry bursts in milliseconds.
|
||||
#ifndef BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS
|
||||
#define BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS 50
|
||||
#endif
|
||||
#ifndef BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS
|
||||
#define BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS 200
|
||||
#endif
|
||||
|
||||
// How long before a peer is considered stale (ms)
|
||||
#ifndef BLE_MESH_PEER_TIMEOUT_MS
|
||||
#define BLE_MESH_PEER_TIMEOUT_MS 300000 // 5 minutes
|
||||
#endif
|
||||
|
||||
class ESP32BLEMesh : public BLEMeshHandler
|
||||
{
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
void onBluetoothReady() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
|
||||
private:
|
||||
// Scanning
|
||||
void startScanning();
|
||||
void stopScanning();
|
||||
static int onGapEvent(struct ble_gap_event *event, void *arg);
|
||||
void handleAdvertisement(const struct ble_gap_disc_desc *desc);
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
void handleExtendedAdvertisement(const struct ble_gap_ext_disc_desc *desc);
|
||||
#endif
|
||||
void handleAdvertisementData(const ble_addr_t &addr, int8_t rssi, const uint8_t *data, uint8_t len);
|
||||
bool sendAdvertisementBurst(const uint8_t *advBuf, size_t advLen, PacketId packetId, size_t encodedLen);
|
||||
bool shouldUsePriorityRetries(const meshtastic_MeshPacket *mp) const;
|
||||
|
||||
// Peer tracking
|
||||
struct BLEMeshPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
};
|
||||
BLEMeshPeer peers[BLE_MESH_MAX_PEERS];
|
||||
uint8_t peerCount = 0;
|
||||
bool bluetoothReady = false;
|
||||
void updatePeer(const ble_addr_t &addr, int8_t rssi);
|
||||
void pruneStale();
|
||||
};
|
||||
|
||||
#endif // HAS_BLE_MESH && ARCH_ESP32
|
||||
@@ -0,0 +1,340 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_BLE_MESH && defined(ARCH_NRF52)
|
||||
|
||||
#include "NRF52BLEMesh.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Router.h"
|
||||
#include <bluefruit.h>
|
||||
|
||||
NRF52BLEMesh *NRF52BLEMesh::instance = nullptr;
|
||||
|
||||
static uint8_t bleMeshScanBuffer[BLE_GAP_SCAN_BUFFER_EXTENDED_MAX_SUPPORTED];
|
||||
static ble_data_t bleMeshScanReportData = {.p_data = bleMeshScanBuffer, .len = sizeof(bleMeshScanBuffer)};
|
||||
static ble_gap_scan_params_t bleMeshScanParams = {
|
||||
.extended = 1,
|
||||
.report_incomplete_evts = 0,
|
||||
.active = 0,
|
||||
.filter_policy = BLE_GAP_SCAN_FP_ACCEPT_ALL,
|
||||
.scan_phys = BLE_GAP_PHY_1MBPS,
|
||||
.interval = BLE_MESH_SCAN_INTERVAL,
|
||||
.window = BLE_MESH_SCAN_WINDOW,
|
||||
.timeout = 0,
|
||||
.channel_mask = {0, 0, 0, 0, 0},
|
||||
};
|
||||
|
||||
void NRF52BLEMesh::start()
|
||||
{
|
||||
if (isRunning) {
|
||||
LOG_DEBUG("BLE mesh already running");
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
memset(peers, 0, sizeof(peers));
|
||||
peerCount = 0;
|
||||
|
||||
isRunning = true;
|
||||
LOG_INFO("BLE mesh started");
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::onBluetoothReady()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
LOG_DEBUG("BLE mesh Bluetooth ready");
|
||||
Bluefruit.setEventCallback(onBleEvent);
|
||||
startScanning();
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::stop()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
stopScanning();
|
||||
isRunning = false;
|
||||
LOG_INFO("BLE mesh stopped");
|
||||
}
|
||||
|
||||
bool NRF52BLEMesh::onSend(const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
if (!isRunning || !mp)
|
||||
return false;
|
||||
|
||||
// Don't echo packets that arrived via BLE mesh
|
||||
if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH) {
|
||||
LOG_DEBUG("Drop BLE mesh echo");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t buffer[meshtastic_MeshPacket_size];
|
||||
size_t encodedLen = encodeForBLE(mp, buffer, sizeof(buffer));
|
||||
if (encodedLen == 0) {
|
||||
LOG_WARN("BLE mesh encode failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build manufacturer data: company ID (2 bytes) + version (1 byte) + protobuf payload
|
||||
size_t mfgDataLen = 2 + 1 + encodedLen;
|
||||
|
||||
// Extended advertising can carry up to 255 bytes total.
|
||||
// Reserve 5 bytes for the flags and manufacturer-data AD headers.
|
||||
if (mfgDataLen > 250) {
|
||||
LOG_WARN("BLE mesh packet too large: %u bytes", mfgDataLen);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t mfgData[250];
|
||||
mfgData[0] = (uint8_t)(BLE_MESH_COMPANY_ID & 0xFF);
|
||||
mfgData[1] = (uint8_t)((BLE_MESH_COMPANY_ID >> 8) & 0xFF);
|
||||
mfgData[2] = BLE_MESH_PROTOCOL_VERSION;
|
||||
memcpy(&mfgData[3], buffer, encodedLen);
|
||||
|
||||
// Stop scanning during mesh advertisement burst
|
||||
stopScanning();
|
||||
|
||||
static uint8_t adv_buf[BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_MAX_SUPPORTED];
|
||||
|
||||
size_t adv_len = 0;
|
||||
adv_buf[adv_len++] = 2;
|
||||
adv_buf[adv_len++] = BLE_GAP_AD_TYPE_FLAGS;
|
||||
adv_buf[adv_len++] = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;
|
||||
adv_buf[adv_len++] = (uint8_t)(mfgDataLen + 1);
|
||||
adv_buf[adv_len++] = BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA;
|
||||
memcpy(&adv_buf[adv_len], mfgData, mfgDataLen);
|
||||
adv_len += mfgDataLen;
|
||||
|
||||
Bluefruit.Advertising.stop();
|
||||
delay(BLE_MESH_ADV_SWITCH_SETTLE_MS);
|
||||
|
||||
const bool priorityRetries = shouldUsePriorityRetries(mp);
|
||||
const uint8_t extraRetries = priorityRetries ? BLE_MESH_PRIORITY_EXTRA_RETRIES : 0;
|
||||
bool sentAny = false;
|
||||
|
||||
for (uint8_t attempt = 0; attempt <= extraRetries; ++attempt) {
|
||||
if (attempt > 0) {
|
||||
const uint16_t retryDelayMs =
|
||||
random(BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS, BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS + 1);
|
||||
delay(retryDelayMs);
|
||||
}
|
||||
|
||||
if (sendAdvertisementBurst(adv_buf, adv_len, mp->id, encodedLen)) {
|
||||
sentAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore phone advertising through the normal Bluefruit setup path.
|
||||
if (config.bluetooth.enabled && nrf52Bluetooth) {
|
||||
nrf52Bluetooth->resumeAdvertising();
|
||||
}
|
||||
|
||||
// Resume scanning
|
||||
startScanning();
|
||||
|
||||
return sentAny;
|
||||
}
|
||||
|
||||
bool NRF52BLEMesh::sendAdvertisementBurst(const uint8_t *advBuf, size_t advLen, PacketId packetId, size_t encodedLen)
|
||||
{
|
||||
static ble_gap_adv_data_t gapAdvData;
|
||||
static ble_gap_adv_params_t advParams;
|
||||
|
||||
gapAdvData.adv_data.p_data = const_cast<uint8_t *>(advBuf);
|
||||
gapAdvData.adv_data.len = advLen;
|
||||
gapAdvData.scan_rsp_data.p_data = NULL;
|
||||
gapAdvData.scan_rsp_data.len = 0;
|
||||
|
||||
memset(&advParams, 0, sizeof(advParams));
|
||||
advParams.properties.type = BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED;
|
||||
advParams.p_peer_addr = NULL;
|
||||
advParams.filter_policy = BLE_GAP_ADV_FP_ANY;
|
||||
advParams.interval = BLE_MESH_ADV_INTERVAL;
|
||||
advParams.duration = 0;
|
||||
advParams.primary_phy = BLE_GAP_PHY_1MBPS;
|
||||
advParams.secondary_phy = BLE_GAP_PHY_1MBPS;
|
||||
advParams.max_adv_evts = BLE_MESH_ADV_COUNT;
|
||||
|
||||
uint8_t meshAdvHandle = 0;
|
||||
uint32_t stopErr = sd_ble_gap_adv_stop(meshAdvHandle);
|
||||
if (stopErr != NRF_SUCCESS && stopErr != NRF_ERROR_INVALID_STATE) {
|
||||
LOG_WARN("BLE mesh adv stop failed: 0x%x", stopErr);
|
||||
}
|
||||
delay(BLE_MESH_ADV_SWITCH_SETTLE_MS);
|
||||
|
||||
uint32_t err = sd_ble_gap_adv_set_configure(&meshAdvHandle, &gapAdvData, &advParams);
|
||||
if (err != NRF_SUCCESS) {
|
||||
LOG_WARN("BLE mesh adv configure failed: 0x%x", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
err = sd_ble_gap_adv_start(meshAdvHandle, BLE_CONN_CFG_TAG_DEFAULT);
|
||||
if (err != NRF_SUCCESS) {
|
||||
LOG_WARN("BLE mesh adv start failed: 0x%x", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("BLE mesh adv sent (id=%u, len=%u)", packetId, encodedLen);
|
||||
delay(BLE_MESH_ADV_COUNT * 10 + BLE_MESH_ADV_BURST_GUARD_MS);
|
||||
|
||||
stopErr = sd_ble_gap_adv_stop(meshAdvHandle);
|
||||
if (stopErr != NRF_SUCCESS && stopErr != NRF_ERROR_INVALID_STATE) {
|
||||
LOG_WARN("BLE mesh adv stop after send failed: 0x%x", stopErr);
|
||||
}
|
||||
delay(BLE_MESH_ADV_SWITCH_SETTLE_MS);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NRF52BLEMesh::shouldUsePriorityRetries(const meshtastic_MeshPacket *mp) const
|
||||
{
|
||||
if (!mp)
|
||||
return false;
|
||||
|
||||
// Prioritize retries for ack-sensitive traffic and direct messages only.
|
||||
const bool isDirect = (mp->to != 0) && !isBroadcast(mp->to);
|
||||
return mp->want_ack || isDirect;
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::startScanning()
|
||||
{
|
||||
bleMeshScanReportData.len = sizeof(bleMeshScanBuffer);
|
||||
bleMeshScanParams.interval = BLE_MESH_SCAN_INTERVAL;
|
||||
bleMeshScanParams.window = BLE_MESH_SCAN_WINDOW;
|
||||
|
||||
uint32_t err = sd_ble_gap_scan_start(&bleMeshScanParams, &bleMeshScanReportData);
|
||||
if (err == NRF_SUCCESS) {
|
||||
LOG_DEBUG("BLE mesh scanning started");
|
||||
} else if (err == NRF_ERROR_INVALID_STATE) {
|
||||
LOG_DEBUG("BLE mesh scanning already active");
|
||||
} else {
|
||||
LOG_WARN("BLE mesh scan start failed: 0x%x", err);
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::stopScanning()
|
||||
{
|
||||
uint32_t err = sd_ble_gap_scan_stop();
|
||||
if (err != NRF_SUCCESS && err != NRF_ERROR_INVALID_STATE) {
|
||||
LOG_WARN("BLE mesh scan stop failed: 0x%x", err);
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::onBleEvent(ble_evt_t *event)
|
||||
{
|
||||
if (!instance || !instance->isRunning || !event)
|
||||
return;
|
||||
|
||||
switch (event->header.evt_id) {
|
||||
case BLE_GAP_EVT_ADV_REPORT: {
|
||||
ble_gap_evt_adv_report_t *report = &event->evt.gap_evt.params.adv_report;
|
||||
|
||||
if (report->type.status == BLE_GAP_ADV_DATA_STATUS_COMPLETE) {
|
||||
instance->handleScanResult(report);
|
||||
}
|
||||
|
||||
bleMeshScanReportData.len = sizeof(bleMeshScanBuffer);
|
||||
uint32_t err = sd_ble_gap_scan_start(NULL, &bleMeshScanReportData);
|
||||
if (err != NRF_SUCCESS && err != NRF_ERROR_INVALID_STATE) {
|
||||
LOG_WARN("BLE mesh scan resume failed: 0x%x", err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BLE_GAP_EVT_TIMEOUT:
|
||||
if (event->evt.gap_evt.params.timeout.src == BLE_GAP_TIMEOUT_SRC_SCAN) {
|
||||
instance->startScanning();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::handleScanResult(ble_gap_evt_adv_report_t *report)
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
// Parse advertisement data to find manufacturer-specific data
|
||||
uint8_t *data = report->data.p_data;
|
||||
uint16_t len = report->data.len;
|
||||
uint16_t offset = 0;
|
||||
|
||||
while (offset < len) {
|
||||
uint8_t adLen = data[offset];
|
||||
if (adLen == 0 || offset + adLen >= len)
|
||||
break;
|
||||
|
||||
uint8_t adType = data[offset + 1];
|
||||
if (adType == BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA && adLen >= 4) {
|
||||
// Check company ID
|
||||
uint16_t companyId = data[offset + 2] | (data[offset + 3] << 8);
|
||||
if (companyId == BLE_MESH_COMPANY_ID) {
|
||||
// Check protocol version
|
||||
uint8_t version = data[offset + 4];
|
||||
if (version == BLE_MESH_PROTOCOL_VERSION) {
|
||||
// Extract protobuf payload (skip company ID + version = 3 bytes)
|
||||
const uint8_t *payload = &data[offset + 5];
|
||||
size_t payloadLen = adLen - 4; // adLen includes type byte, subtract type + company(2) + version(1)
|
||||
|
||||
LOG_DEBUG("BLE mesh RX: rssi=%d, len=%u", report->rssi, payloadLen);
|
||||
updatePeer(report->peer_addr, report->rssi);
|
||||
deliverToRouter(payload, payloadLen);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += adLen + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::updatePeer(const ble_gap_addr_t &addr, int8_t rssi)
|
||||
{
|
||||
uint32_t now = millis();
|
||||
|
||||
// Check if peer already known
|
||||
for (uint8_t i = 0; i < peerCount; i++) {
|
||||
if (memcmp(&peers[i].addr, &addr, sizeof(ble_gap_addr_t)) == 0) {
|
||||
peers[i].rssi = rssi;
|
||||
peers[i].lastSeenMs = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new peer
|
||||
if (peerCount < BLE_MESH_MAX_PEERS) {
|
||||
peers[peerCount].addr = addr;
|
||||
peers[peerCount].rssi = rssi;
|
||||
peers[peerCount].lastSeenMs = now;
|
||||
peers[peerCount].nodeNum = 0; // Unknown until we decode a packet from them
|
||||
peerCount++;
|
||||
LOG_DEBUG("BLE mesh new peer (%u total)", peerCount);
|
||||
} else {
|
||||
// Replace oldest peer
|
||||
pruneStale();
|
||||
if (peerCount < BLE_MESH_MAX_PEERS) {
|
||||
peers[peerCount].addr = addr;
|
||||
peers[peerCount].rssi = rssi;
|
||||
peers[peerCount].lastSeenMs = now;
|
||||
peers[peerCount].nodeNum = 0;
|
||||
peerCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::pruneStale()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
uint8_t writeIdx = 0;
|
||||
|
||||
for (uint8_t i = 0; i < peerCount; i++) {
|
||||
if (now - peers[i].lastSeenMs < BLE_MESH_PEER_TIMEOUT_MS) {
|
||||
if (writeIdx != i)
|
||||
peers[writeIdx] = peers[i];
|
||||
writeIdx++;
|
||||
}
|
||||
}
|
||||
peerCount = writeIdx;
|
||||
}
|
||||
|
||||
#endif // HAS_BLE_MESH && ARCH_NRF52
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#if HAS_BLE_MESH && defined(ARCH_NRF52)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
#include <bluefruit.h>
|
||||
|
||||
// Max number of BLE mesh peers we can track
|
||||
#ifndef BLE_MESH_MAX_PEERS
|
||||
#define BLE_MESH_MAX_PEERS 8
|
||||
#endif
|
||||
|
||||
// Scan interval and window in units of 0.625ms
|
||||
// Default: scan 50ms every 500ms (10% duty cycle)
|
||||
#ifndef BLE_MESH_SCAN_INTERVAL
|
||||
#define BLE_MESH_SCAN_INTERVAL 800 // 500ms
|
||||
#endif
|
||||
#ifndef BLE_MESH_SCAN_WINDOW
|
||||
#define BLE_MESH_SCAN_WINDOW 80 // 50ms
|
||||
#endif
|
||||
|
||||
// Advertising interval for mesh data in units of 0.625ms
|
||||
#ifndef BLE_MESH_ADV_INTERVAL
|
||||
#define BLE_MESH_ADV_INTERVAL 160 // 100ms
|
||||
#endif
|
||||
|
||||
// How many times to repeat a mesh advertisement
|
||||
#ifndef BLE_MESH_ADV_COUNT
|
||||
#define BLE_MESH_ADV_COUNT 3
|
||||
#endif
|
||||
|
||||
// Small guard intervals help the SoftDevice finish the stop/start transition
|
||||
// when we temporarily borrow the single advertising handle.
|
||||
#ifndef BLE_MESH_ADV_SWITCH_SETTLE_MS
|
||||
#define BLE_MESH_ADV_SWITCH_SETTLE_MS 2
|
||||
#endif
|
||||
|
||||
#ifndef BLE_MESH_ADV_BURST_GUARD_MS
|
||||
#define BLE_MESH_ADV_BURST_GUARD_MS 5
|
||||
#endif
|
||||
|
||||
// Extra retry bursts for higher-priority traffic only.
|
||||
// Base burst always runs once for every packet.
|
||||
#ifndef BLE_MESH_PRIORITY_EXTRA_RETRIES
|
||||
#define BLE_MESH_PRIORITY_EXTRA_RETRIES 2
|
||||
#endif
|
||||
|
||||
// Randomized delay range between extra retry bursts in milliseconds.
|
||||
#ifndef BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS
|
||||
#define BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS 50
|
||||
#endif
|
||||
#ifndef BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS
|
||||
#define BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS 200
|
||||
#endif
|
||||
|
||||
// How long before a peer is considered stale (ms)
|
||||
#ifndef BLE_MESH_PEER_TIMEOUT_MS
|
||||
#define BLE_MESH_PEER_TIMEOUT_MS 300000 // 5 minutes
|
||||
#endif
|
||||
|
||||
class NRF52BLEMesh : public BLEMeshHandler
|
||||
{
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
void onBluetoothReady() override;
|
||||
|
||||
private:
|
||||
// Scanning
|
||||
void startScanning();
|
||||
void stopScanning();
|
||||
static void onBleEvent(ble_evt_t *event);
|
||||
void handleScanResult(ble_gap_evt_adv_report_t *report);
|
||||
bool sendAdvertisementBurst(const uint8_t *advBuf, size_t advLen, PacketId packetId, size_t encodedLen);
|
||||
bool shouldUsePriorityRetries(const meshtastic_MeshPacket *mp) const;
|
||||
|
||||
// Peer tracking
|
||||
struct BLEMeshPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_gap_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
};
|
||||
BLEMeshPeer peers[BLE_MESH_MAX_PEERS];
|
||||
uint8_t peerCount = 0;
|
||||
void updatePeer(const ble_gap_addr_t &addr, int8_t rssi);
|
||||
void pruneStale();
|
||||
|
||||
// Singleton for static callbacks
|
||||
static NRF52BLEMesh *instance;
|
||||
};
|
||||
|
||||
#endif // HAS_BLE_MESH && ARCH_NRF52
|
||||
@@ -233,6 +233,7 @@ void NRF52Bluetooth::shutdown()
|
||||
Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onUnwantedPairing); // Actively refuse (during factory reset)
|
||||
disconnect();
|
||||
Bluefruit.Advertising.stop();
|
||||
meshReadyNotified = false;
|
||||
}
|
||||
void NRF52Bluetooth::startDisabled()
|
||||
{
|
||||
@@ -241,6 +242,7 @@ void NRF52Bluetooth::startDisabled()
|
||||
// Shutdown bluetooth for minimum power draw
|
||||
Bluefruit.Advertising.stop();
|
||||
Bluefruit.setTxPower(-40); // Minimum power
|
||||
meshReadyNotified = false;
|
||||
LOG_INFO("Disable NRF52 Bluetooth. (Workaround: tx power min, advertise stopped)");
|
||||
}
|
||||
bool NRF52Bluetooth::isConnected()
|
||||
@@ -263,7 +265,8 @@ void NRF52Bluetooth::setup()
|
||||
LOG_INFO("Init the Bluefruit nRF52 module");
|
||||
Bluefruit.autoConnLed(false);
|
||||
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
|
||||
Bluefruit.begin();
|
||||
Bluefruit.configCentralBandwidth(BANDWIDTH_MAX);
|
||||
Bluefruit.begin(1, 1);
|
||||
// Clear existing data.
|
||||
Bluefruit.Advertising.stop();
|
||||
Bluefruit.Advertising.clearData();
|
||||
@@ -338,14 +341,22 @@ void NRF52Bluetooth::setup()
|
||||
// Setup the advertising packet(s)
|
||||
LOG_INFO("Set up the advertising payload(s)");
|
||||
startAdv();
|
||||
if (bleMeshHandler && !meshReadyNotified) {
|
||||
bleMeshHandler->onBluetoothReady();
|
||||
meshReadyNotified = true;
|
||||
}
|
||||
LOG_INFO("Advertise");
|
||||
}
|
||||
void NRF52Bluetooth::resumeAdvertising()
|
||||
{
|
||||
Bluefruit.Advertising.restartOnDisconnect(true);
|
||||
Bluefruit.Advertising.setInterval(32, 668); // in unit of 0.625 ms
|
||||
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
|
||||
Bluefruit.Advertising.start(0);
|
||||
Bluefruit.Advertising.stop();
|
||||
Bluefruit.Advertising.clearData();
|
||||
Bluefruit.ScanResponse.clearData();
|
||||
startAdv();
|
||||
if (bleMeshHandler && !meshReadyNotified) {
|
||||
bleMeshHandler->onBluetoothReady();
|
||||
meshReadyNotified = true;
|
||||
}
|
||||
}
|
||||
/// Given a level between 0-100, update the BLE attribute
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
|
||||
@@ -22,4 +22,6 @@ class NRF52Bluetooth : BluetoothApi
|
||||
|
||||
static bool onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request);
|
||||
static void disconnect();
|
||||
|
||||
bool meshReadyNotified = false;
|
||||
};
|
||||
@@ -35,6 +35,7 @@ build_flags =
|
||||
-Wextra
|
||||
-Isrc/platform/esp32
|
||||
-std=c++11
|
||||
-DHAS_BLE_MESH=1
|
||||
-DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG
|
||||
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
|
||||
-DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
extends = esp32_common
|
||||
custom_esp32_kind = esp32c3
|
||||
|
||||
build_flags =
|
||||
${esp32_common.build_flags}
|
||||
-DCONFIG_BT_NIMBLE_EXT_ADV=1
|
||||
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2
|
||||
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_DATA_LEN=251
|
||||
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_c3_exception_decoder
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
extends = esp32_common
|
||||
custom_esp32_kind = esp32s3
|
||||
|
||||
build_flags =
|
||||
${esp32_common.build_flags}
|
||||
-DCONFIG_BT_NIMBLE_EXT_ADV=1
|
||||
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2
|
||||
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_DATA_LEN=251
|
||||
-include mbedtls/error.h
|
||||
|
||||
monitor_speed = 115200
|
||||
|
||||
lib_deps =
|
||||
|
||||
@@ -24,6 +24,7 @@ build_flags =
|
||||
-DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818
|
||||
-DMESHTASTIC_EXCLUDE_AUDIO=1
|
||||
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
|
||||
-DHAS_BLE_MESH=1
|
||||
-Os
|
||||
build_unflags =
|
||||
-Ofast
|
||||
|
||||
Reference in New Issue
Block a user