All working
This commit is contained in:
parent
31f881233e
commit
7410e820c6
24 changed files with 3475 additions and 0 deletions
93
exercises/301_microReticulum_ble_ping_pong/README.md
Normal file
93
exercises/301_microReticulum_ble_ping_pong/README.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Exercise 301: microReticulum BLE ping-pong
|
||||
|
||||
This exercise is the BLE analogue of Exercise 201. It proves that two T-Beam Supreme units can exchange microReticulum `PLAIN` destination packets over an ESP32 BLE GATT interface instead of LoRa.
|
||||
|
||||
The ESP32 BLE interface is new in this exercise. It uses the Reticulum BLE service and characteristic UUIDs from `/usr/local/src/ble-reticulum/BLE_PROTOCOL_v2.2.md`:
|
||||
|
||||
```text
|
||||
service 37145b00-442d-4a94-917f-8f42c5da28e3
|
||||
TX notify 37145b00-442d-4a94-917f-8f42c5da28e4
|
||||
RX write 37145b00-442d-4a94-917f-8f42c5da28e5
|
||||
identity 37145b00-442d-4a94-917f-8f42c5da28e6
|
||||
```
|
||||
|
||||
For this first BLE proof, roles are explicit:
|
||||
|
||||
```text
|
||||
AMY -> BLE central/client
|
||||
BOB -> BLE peripheral/server
|
||||
```
|
||||
|
||||
AMY scans for the Reticulum BLE service, connects to BOB, sends the 16-byte BLE identity handshake, subscribes to BOB's TX notifications, and writes Reticulum packet fragments to BOB's RX characteristic. BOB receives writes and sends its own Reticulum packet fragments back with TX notifications.
|
||||
|
||||
## Build, upload, and monitor
|
||||
|
||||
In one console:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/301_microReticulum_ble_ping_pong -e bob -t upload --upload-port /dev/ttytBOB && \
|
||||
pio device monitor -d exercises/301_microReticulum_ble_ping_pong -e bob
|
||||
```
|
||||
|
||||
In another console:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/301_microReticulum_ble_ping_pong -e amy -t upload --upload-port /dev/ttytAMY && \
|
||||
pio device monitor -d exercises/301_microReticulum_ble_ping_pong -e amy
|
||||
```
|
||||
|
||||
Start BOB first so the peripheral is advertising before AMY starts scanning.
|
||||
|
||||
The `platformio.ini` maps each environment to its stable USB symlink:
|
||||
|
||||
```text
|
||||
amy -> /dev/ttytAMY
|
||||
bob -> /dev/ttytBOB
|
||||
cy -> /dev/ttytCY
|
||||
dan -> /dev/ttytDAN
|
||||
ed -> /dev/ttytED
|
||||
```
|
||||
|
||||
The shorter form should also work once the symlinks exist:
|
||||
|
||||
```bash
|
||||
pio run -d exercises/301_microReticulum_ble_ping_pong -e bob -t upload
|
||||
pio device monitor -d exercises/301_microReticulum_ble_ping_pong -e bob
|
||||
```
|
||||
|
||||
## Expected output
|
||||
|
||||
BOB should advertise:
|
||||
|
||||
```text
|
||||
Exercise 301: microReticulum BLE ping-pong
|
||||
Node=Bob
|
||||
BLE role=peripheral service=37145b00-442d-4a94-917f-8f42c5da28e3
|
||||
```
|
||||
|
||||
AMY should scan, connect, and send the BLE identity handshake:
|
||||
|
||||
```text
|
||||
BLE scanning for Reticulum service
|
||||
BLE peer found: ...
|
||||
BLE connected and identity handshake sent
|
||||
```
|
||||
|
||||
After the BLE link is connected, both nodes should print Reticulum packet traffic:
|
||||
|
||||
```text
|
||||
TX RNS BLE: Amy says hi. iter=0
|
||||
RX RNS BLE: Bob says hi. iter=0
|
||||
```
|
||||

|
||||
## Notes
|
||||
|
||||
- This exercise still uses `PLAIN` destinations. It proves BLE can carry serialized Reticulum packets, not identity announces or negotiated links.
|
||||
- Exercise 302 should mirror Exercise 202 by adding announced `SINGLE` destinations over BLE.
|
||||
- Exercise 303 should mirror Exercise 203 by negotiating a Reticulum `Link` over BLE.
|
||||
- The BLE fragmentation header matches the Python/C++ BLE Reticulum protocol: `[type:1][sequence:2][total:2][payload...]`.
|
||||
- This first ESP32 implementation is intentionally role-paired. A later pass can add dual central/peripheral operation and MAC-sorted connection direction.
|
||||
76
exercises/301_microReticulum_ble_ping_pong/platformio.ini
Normal file
76
exercises/301_microReticulum_ble_ping_pong/platformio.ini
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
; Exercise 301: microReticulum BLE ping-pong on T-Beam Supreme
|
||||
|
||||
[platformio]
|
||||
default_envs = amy
|
||||
|
||||
[env]
|
||||
platform = espressif32
|
||||
framework = arduino
|
||||
board = esp32-s3-devkitc-1
|
||||
monitor_speed = 115200
|
||||
upload_speed = 460800
|
||||
board_build.partitions = huge_app.csv
|
||||
|
||||
build_flags =
|
||||
-Wall
|
||||
-Wno-missing-field-initializers
|
||||
-Wno-format
|
||||
-D RNS_USE_FS
|
||||
-D RNS_PERSIST_PATHS
|
||||
-D USTORE_USE_UNIVERSALFS
|
||||
-D MSGPACK_USE_BOOST=OFF
|
||||
-D MCU_ESP32
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
lib_deps =
|
||||
ArduinoJson@^7.4.2
|
||||
MsgPack@^0.4.2
|
||||
https://github.com/attermann/Crypto.git
|
||||
https://github.com/attermann/microStore.git
|
||||
microReticulum=symlink:///usr/local/src/microreticulum/microReticulum
|
||||
|
||||
[env:amy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytAMY
|
||||
monitor_port = /dev/ttytAMY
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Amy\"
|
||||
-D BLE_ROLE_CENTRAL
|
||||
|
||||
[env:bob]
|
||||
extends = env
|
||||
upload_port = /dev/ttytBOB
|
||||
monitor_port = /dev/ttytBOB
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Bob\"
|
||||
-D BLE_ROLE_PERIPHERAL
|
||||
|
||||
[env:cy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytCY
|
||||
monitor_port = /dev/ttytCY
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Cy\"
|
||||
-D BLE_ROLE_CENTRAL
|
||||
|
||||
[env:dan]
|
||||
extends = env
|
||||
upload_port = /dev/ttytDAN
|
||||
monitor_port = /dev/ttytDAN
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Dan\"
|
||||
-D BLE_ROLE_PERIPHERAL
|
||||
|
||||
[env:ed]
|
||||
extends = env
|
||||
upload_port = /dev/ttytED
|
||||
monitor_port = /dev/ttytED
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Ed\"
|
||||
-D BLE_ROLE_CENTRAL
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
|
||||
#include <BLE2902.h>
|
||||
#include <BLEService.h>
|
||||
#include <Cryptography/Random.h>
|
||||
#include <Identity.h>
|
||||
#include <Log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string.h>
|
||||
|
||||
using namespace RNS;
|
||||
|
||||
#ifndef NODE_LABEL
|
||||
#define NODE_LABEL "?"
|
||||
#endif
|
||||
|
||||
static TBeamSupremeBleInterface* active_ble_interface = nullptr;
|
||||
|
||||
class TBeamSupremeBleInterface::ServerCallbacks : public BLEServerCallbacks {
|
||||
public:
|
||||
explicit ServerCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onConnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
_owner->_connected = true;
|
||||
_owner->_server_handshake_received = false;
|
||||
Serial.println("BLE peripheral: central connected");
|
||||
INFO("BLE central connected");
|
||||
}
|
||||
|
||||
void onDisconnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
_owner->_connected = false;
|
||||
_owner->_server_handshake_received = false;
|
||||
_owner->reset_reassembly();
|
||||
Serial.println("BLE peripheral: central disconnected; restarting advertising");
|
||||
INFO("BLE central disconnected; restarting advertising");
|
||||
BLEDevice::startAdvertising();
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::RxCallbacks : public BLECharacteristicCallbacks {
|
||||
public:
|
||||
explicit RxCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onWrite(BLECharacteristic* characteristic) override {
|
||||
std::string value = characteristic->getValue();
|
||||
if (value.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_owner->_server_handshake_received && value.size() == 16) {
|
||||
_owner->_server_handshake_received = true;
|
||||
Serial.println("BLE peripheral: identity handshake received");
|
||||
INFOF("BLE identity handshake received: %d bytes", (int)value.size());
|
||||
return;
|
||||
}
|
||||
|
||||
_owner->handle_fragment(reinterpret_cast<const uint8_t*>(value.data()), value.size());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::AdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
|
||||
public:
|
||||
explicit AdvertisedDeviceCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onResult(BLEAdvertisedDevice advertised_device) override {
|
||||
bool has_service = advertised_device.haveServiceUUID() &&
|
||||
advertised_device.isAdvertisingService(BLEUUID(TBeamSupremeBleInterface::SERVICE_UUID));
|
||||
bool has_rns_name = advertised_device.haveName() &&
|
||||
advertised_device.getName().rfind("RNS-", 0) == 0;
|
||||
|
||||
if ((has_service || has_rns_name) || _owner->_scan_report_count < 5) {
|
||||
Serial.printf("BLE scan: addr=%s name=%s service=%s rssi=%d\r\n",
|
||||
advertised_device.getAddress().toString().c_str(),
|
||||
advertised_device.haveName() ? advertised_device.getName().c_str() : "",
|
||||
has_service ? "yes" : "no",
|
||||
advertised_device.getRSSI());
|
||||
_owner->_scan_report_count++;
|
||||
}
|
||||
|
||||
if (!has_service && !has_rns_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
BLEDevice::getScan()->stop();
|
||||
delete _owner->_advertised_device;
|
||||
_owner->_advertised_device = new BLEAdvertisedDevice(advertised_device);
|
||||
_owner->_do_connect = true;
|
||||
_owner->_scanning = false;
|
||||
Serial.printf("BLE central: peer candidate found: %s\r\n",
|
||||
advertised_device.getAddress().toString().c_str());
|
||||
INFOF("BLE peer found: %s", advertised_device.getAddress().toString().c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
TBeamSupremeBleInterface::TBeamSupremeBleInterface(const char* name) : InterfaceImpl(name) {
|
||||
_IN = true;
|
||||
_OUT = true;
|
||||
_bitrate = 1000000;
|
||||
_HW_MTU = BLE_PAYLOAD_SIZE;
|
||||
#if defined(BLE_ROLE_CENTRAL)
|
||||
_central_role = true;
|
||||
#else
|
||||
_central_role = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
TBeamSupremeBleInterface::~TBeamSupremeBleInterface() {
|
||||
stop();
|
||||
delete _advertised_device;
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::start() {
|
||||
if (_started) {
|
||||
return true;
|
||||
}
|
||||
|
||||
active_ble_interface = this;
|
||||
BLEDevice::init(std::string("RNS-") + NODE_LABEL);
|
||||
BLEDevice::setMTU(BLE_MTU);
|
||||
|
||||
if (_central_role) {
|
||||
Serial.println("BLE central: starting scanner");
|
||||
INFO("BLE starting as central/client");
|
||||
start_central_scan();
|
||||
} else {
|
||||
Serial.println("BLE peripheral: starting advertiser");
|
||||
INFO("BLE starting as peripheral/server");
|
||||
start_peripheral();
|
||||
}
|
||||
|
||||
_started = true;
|
||||
_online = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::stop() {
|
||||
_online = false;
|
||||
_connected = false;
|
||||
if (_client && _client->isConnected()) {
|
||||
_client->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::loop() {
|
||||
if (!_online) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_central_role) {
|
||||
if (_do_connect && _advertised_device) {
|
||||
_do_connect = false;
|
||||
if (!connect_to_advertised_device(_advertised_device)) {
|
||||
_connected = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_connected && !_scanning && (int32_t)(millis() - _next_scan_ms) >= 0) {
|
||||
start_central_scan();
|
||||
}
|
||||
}
|
||||
|
||||
if (_reassembly_started_ms != 0 && millis() - _reassembly_started_ms > REASSEMBLY_TIMEOUT_MS) {
|
||||
WARNING("BLE reassembly timeout; dropping partial packet");
|
||||
reset_reassembly();
|
||||
}
|
||||
|
||||
RNS::Bytes packet({RNS::Type::NONE});
|
||||
while (dequeue_packet(packet)) {
|
||||
InterfaceImpl::handle_incoming(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_peripheral() {
|
||||
_server = BLEDevice::createServer();
|
||||
_server->setCallbacks(new ServerCallbacks(this));
|
||||
|
||||
BLEService* service = _server->createService(SERVICE_UUID);
|
||||
_tx_characteristic = service->createCharacteristic(
|
||||
TX_UUID,
|
||||
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
_tx_characteristic->addDescriptor(new BLE2902());
|
||||
|
||||
BLECharacteristic* rx_characteristic = service->createCharacteristic(
|
||||
RX_UUID,
|
||||
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
|
||||
rx_characteristic->setCallbacks(new RxCallbacks(this));
|
||||
|
||||
_identity_characteristic = service->createCharacteristic(
|
||||
IDENTITY_UUID,
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_identity_characteristic->setValue((uint8_t*)identity.data(), identity.size());
|
||||
|
||||
service->start();
|
||||
|
||||
BLEAdvertising* advertising = BLEDevice::getAdvertising();
|
||||
advertising->addServiceUUID(SERVICE_UUID);
|
||||
advertising->setScanResponse(true);
|
||||
advertising->setMinPreferred(0x00);
|
||||
BLEDevice::startAdvertising();
|
||||
Serial.println("BLE peripheral: advertising Reticulum service");
|
||||
INFO("BLE advertising Reticulum service");
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_central_scan() {
|
||||
_scanning = true;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
_scan_report_count = 0;
|
||||
|
||||
BLEScan* scan = BLEDevice::getScan();
|
||||
scan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks(this), true);
|
||||
scan->setInterval(1349);
|
||||
scan->setWindow(449);
|
||||
scan->setActiveScan(true);
|
||||
Serial.println("BLE central: scanning for Reticulum service");
|
||||
scan->start(5, false);
|
||||
if (!_do_connect && !_connected) {
|
||||
_scanning = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
Serial.println("BLE central: scan complete; no peer found");
|
||||
}
|
||||
scan->clearResults();
|
||||
INFO("BLE scanning for Reticulum service");
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::connect_to_advertised_device(BLEAdvertisedDevice* device) {
|
||||
Serial.printf("BLE central: connecting to %s\r\n", device->getAddress().toString().c_str());
|
||||
INFOF("BLE connecting to %s", device->getAddress().toString().c_str());
|
||||
|
||||
_client = BLEDevice::createClient();
|
||||
if (!_client->connect(device)) {
|
||||
Serial.println("BLE central: connect failed");
|
||||
WARNING("BLE connect failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
_client->setMTU(BLE_MTU);
|
||||
BLERemoteService* service = _client->getService(BLEUUID(SERVICE_UUID));
|
||||
if (!service) {
|
||||
Serial.println("BLE central: Reticulum service not found after connect");
|
||||
WARNING("BLE Reticulum service not found after connect");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
_remote_rx_characteristic = service->getCharacteristic(BLEUUID(RX_UUID));
|
||||
_remote_tx_characteristic = service->getCharacteristic(BLEUUID(TX_UUID));
|
||||
BLERemoteCharacteristic* identity_characteristic = service->getCharacteristic(BLEUUID(IDENTITY_UUID));
|
||||
|
||||
if (!_remote_rx_characteristic || !_remote_tx_characteristic) {
|
||||
Serial.println("BLE central: RX/TX characteristics not found");
|
||||
WARNING("BLE Reticulum RX/TX characteristics not found");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (identity_characteristic && identity_characteristic->canRead()) {
|
||||
std::string peer_identity = identity_characteristic->readValue();
|
||||
Serial.printf("BLE central: peer identity read: %d bytes\r\n", (int)peer_identity.size());
|
||||
INFOF("BLE peer identity read: %d bytes", (int)peer_identity.size());
|
||||
}
|
||||
|
||||
if (_remote_tx_characteristic->canNotify()) {
|
||||
_remote_tx_characteristic->registerForNotify(client_notify_callback);
|
||||
} else {
|
||||
Serial.println("BLE central: peer TX characteristic does not notify");
|
||||
}
|
||||
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)identity.data(), identity.size(), false);
|
||||
_connected = true;
|
||||
_scanning = false;
|
||||
Serial.println("BLE central: connected and identity handshake sent");
|
||||
INFO("BLE connected and identity handshake sent");
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_outgoing(const Bytes& data) {
|
||||
if (!_online || !_connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
|
||||
if (total == 0 || total > 65535) {
|
||||
WARNINGF("BLE cannot fragment packet of size %d", (int)data.size());
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
uint8_t fragment[BLE_MTU];
|
||||
uint8_t fragment_type = FRAG_CONTINUE;
|
||||
if (i == 0) {
|
||||
fragment_type = FRAG_START;
|
||||
} else if (i == total - 1) {
|
||||
fragment_type = FRAG_END;
|
||||
}
|
||||
|
||||
size_t offset = i * BLE_PAYLOAD_SIZE;
|
||||
size_t chunk = std::min(BLE_PAYLOAD_SIZE, data.size() - offset);
|
||||
fragment[0] = fragment_type;
|
||||
fragment[1] = (uint8_t)((i >> 8) & 0xFF);
|
||||
fragment[2] = (uint8_t)(i & 0xFF);
|
||||
fragment[3] = (uint8_t)((total >> 8) & 0xFF);
|
||||
fragment[4] = (uint8_t)(total & 0xFF);
|
||||
memcpy(fragment + FRAG_HEADER_SIZE, data.data() + offset, chunk);
|
||||
send_fragment(fragment, FRAG_HEADER_SIZE + chunk);
|
||||
delay(8);
|
||||
}
|
||||
|
||||
InterfaceImpl::handle_outgoing(data);
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_fragment(const uint8_t* data, size_t len) {
|
||||
if (_central_role) {
|
||||
if (_remote_rx_characteristic) {
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)data, len, false);
|
||||
}
|
||||
} else {
|
||||
if (_tx_characteristic) {
|
||||
_tx_characteristic->setValue((uint8_t*)data, len);
|
||||
_tx_characteristic->notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::handle_fragment(const uint8_t* data, size_t len) {
|
||||
if (len < FRAG_HEADER_SIZE) {
|
||||
WARNINGF("BLE fragment too short: %d", (int)len);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fragment_type = data[0];
|
||||
uint16_t sequence = ((uint16_t)data[1] << 8) | data[2];
|
||||
uint16_t total = ((uint16_t)data[3] << 8) | data[4];
|
||||
const uint8_t* payload = data + FRAG_HEADER_SIZE;
|
||||
size_t payload_len = len - FRAG_HEADER_SIZE;
|
||||
|
||||
if ((fragment_type != FRAG_START && fragment_type != FRAG_CONTINUE && fragment_type != FRAG_END) ||
|
||||
total == 0 || sequence >= total) {
|
||||
WARNING("BLE invalid fragment header");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sequence == 0) {
|
||||
reset_reassembly();
|
||||
_expected_total = total;
|
||||
_received_fragments = 0;
|
||||
_reassembly_started_ms = millis();
|
||||
}
|
||||
|
||||
if (_expected_total != total || sequence != _received_fragments) {
|
||||
WARNING("BLE out-of-order fragment; dropping partial packet");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
_reassembly_buffer.append(payload, payload_len);
|
||||
_received_fragments++;
|
||||
|
||||
if (_received_fragments == _expected_total) {
|
||||
enqueue_packet(_reassembly_buffer);
|
||||
reset_reassembly();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::enqueue_packet(const RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
_incoming_packets.push_back(packet);
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::dequeue_packet(RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
if (_incoming_packets.empty()) {
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return false;
|
||||
}
|
||||
packet = _incoming_packets.front();
|
||||
_incoming_packets.pop_front();
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::reset_reassembly() {
|
||||
_reassembly_buffer.clear();
|
||||
_expected_total = 0;
|
||||
_received_fragments = 0;
|
||||
_reassembly_started_ms = 0;
|
||||
}
|
||||
|
||||
RNS::Bytes TBeamSupremeBleInterface::local_identity_hash() const {
|
||||
String material = String("microReticulum BLE ") + NODE_LABEL;
|
||||
return Identity::truncated_hash(RNS::bytesFromString(material.c_str()));
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify) {
|
||||
(void)characteristic;
|
||||
(void)is_notify;
|
||||
if (active_ble_interface) {
|
||||
active_ble_interface->handle_fragment(data, length);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
#pragma once
|
||||
|
||||
#include <Bytes.h>
|
||||
#include <Interface.h>
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BLEAdvertisedDevice.h>
|
||||
#include <BLEClient.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLERemoteCharacteristic.h>
|
||||
#include <BLEScan.h>
|
||||
#include <BLEServer.h>
|
||||
|
||||
#include <deque>
|
||||
|
||||
class TBeamSupremeBleInterface : public RNS::InterfaceImpl {
|
||||
public:
|
||||
explicit TBeamSupremeBleInterface(const char* name = "TBeamSupremeBLE");
|
||||
~TBeamSupremeBleInterface() override;
|
||||
|
||||
bool start() override;
|
||||
void stop() override;
|
||||
void loop() override;
|
||||
|
||||
bool connected() const { return _connected; }
|
||||
bool is_central() const { return _central_role; }
|
||||
const char* role_name() const { return _central_role ? "central" : "peripheral"; }
|
||||
|
||||
private:
|
||||
void send_outgoing(const RNS::Bytes& data) override;
|
||||
|
||||
void start_peripheral();
|
||||
void start_central_scan();
|
||||
bool connect_to_advertised_device(BLEAdvertisedDevice* device);
|
||||
|
||||
void send_fragment(const uint8_t* data, size_t len);
|
||||
void handle_fragment(const uint8_t* data, size_t len);
|
||||
void enqueue_packet(const RNS::Bytes& packet);
|
||||
bool dequeue_packet(RNS::Bytes& packet);
|
||||
void reset_reassembly();
|
||||
RNS::Bytes local_identity_hash() const;
|
||||
|
||||
static void client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify);
|
||||
|
||||
class ServerCallbacks;
|
||||
class RxCallbacks;
|
||||
class AdvertisedDeviceCallbacks;
|
||||
|
||||
static constexpr const char* SERVICE_UUID = "37145b00-442d-4a94-917f-8f42c5da28e3";
|
||||
static constexpr const char* TX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e4";
|
||||
static constexpr const char* RX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e5";
|
||||
static constexpr const char* IDENTITY_UUID = "37145b00-442d-4a94-917f-8f42c5da28e6";
|
||||
|
||||
static constexpr uint8_t FRAG_START = 0x01;
|
||||
static constexpr uint8_t FRAG_CONTINUE = 0x02;
|
||||
static constexpr uint8_t FRAG_END = 0x03;
|
||||
static constexpr size_t FRAG_HEADER_SIZE = 5;
|
||||
static constexpr size_t BLE_MTU = 185;
|
||||
static constexpr size_t BLE_PAYLOAD_SIZE = BLE_MTU - FRAG_HEADER_SIZE;
|
||||
static constexpr uint32_t REASSEMBLY_TIMEOUT_MS = 30000;
|
||||
static constexpr uint32_t SCAN_RETRY_MS = 5000;
|
||||
|
||||
bool _central_role = false;
|
||||
bool _started = false;
|
||||
bool _connected = false;
|
||||
bool _do_connect = false;
|
||||
bool _scanning = false;
|
||||
bool _server_handshake_received = false;
|
||||
uint8_t _scan_report_count = 0;
|
||||
uint32_t _next_scan_ms = 0;
|
||||
|
||||
BLEServer* _server = nullptr;
|
||||
BLECharacteristic* _tx_characteristic = nullptr;
|
||||
BLECharacteristic* _identity_characteristic = nullptr;
|
||||
BLEAdvertisedDevice* _advertised_device = nullptr;
|
||||
BLEClient* _client = nullptr;
|
||||
BLERemoteCharacteristic* _remote_rx_characteristic = nullptr;
|
||||
BLERemoteCharacteristic* _remote_tx_characteristic = nullptr;
|
||||
|
||||
RNS::Bytes _reassembly_buffer;
|
||||
uint16_t _expected_total = 0;
|
||||
uint16_t _received_fragments = 0;
|
||||
uint32_t _reassembly_started_ms = 0;
|
||||
|
||||
std::deque<RNS::Bytes> _incoming_packets;
|
||||
portMUX_TYPE _queue_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
};
|
||||
125
exercises/301_microReticulum_ble_ping_pong/src/main.cpp
Normal file
125
exercises/301_microReticulum_ble_ping_pong/src/main.cpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Destination.h>
|
||||
#include <Log.h>
|
||||
#include <Packet.h>
|
||||
#include <Reticulum.h>
|
||||
#include <Transport.h>
|
||||
#include <Type.h>
|
||||
#include <Utilities/OS.h>
|
||||
#include <microStore/Adapters/UniversalFileSystem.h>
|
||||
#include <microStore/FileSystem.h>
|
||||
|
||||
#ifndef NODE_LABEL
|
||||
#define NODE_LABEL "?"
|
||||
#endif
|
||||
|
||||
static RNS::Reticulum reticulum({RNS::Type::NONE});
|
||||
static RNS::Interface ble_interface({RNS::Type::NONE});
|
||||
static RNS::Destination inbound_destination({RNS::Type::NONE});
|
||||
static RNS::Destination outbound_destination({RNS::Type::NONE});
|
||||
static TBeamSupremeBleInterface* ble_impl = nullptr;
|
||||
|
||||
static void print_config() {
|
||||
Serial.printf("Node=%s\r\n", NODE_LABEL);
|
||||
Serial.printf("BLE role=%s service=37145b00-442d-4a94-917f-8f42c5da28e3\r\n",
|
||||
ble_impl ? ble_impl->role_name() : "?");
|
||||
}
|
||||
|
||||
static void on_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
|
||||
(void)packet;
|
||||
Serial.printf("RX RNS BLE: %s\r\n", data.toString().c_str());
|
||||
}
|
||||
|
||||
static void setup_reticulum() {
|
||||
microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
|
||||
filesystem.init();
|
||||
RNS::Utilities::OS::register_filesystem(filesystem);
|
||||
|
||||
ble_impl = new TBeamSupremeBleInterface();
|
||||
ble_interface = ble_impl;
|
||||
ble_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
|
||||
RNS::Transport::register_interface(ble_interface);
|
||||
ble_interface.start();
|
||||
|
||||
reticulum = RNS::Reticulum();
|
||||
reticulum.transport_enabled(false);
|
||||
reticulum.probe_destination_enabled(false);
|
||||
reticulum.start();
|
||||
|
||||
inbound_destination = RNS::Destination({RNS::Type::NONE},
|
||||
RNS::Type::Destination::IN,
|
||||
RNS::Type::Destination::PLAIN,
|
||||
"microreticulum",
|
||||
"pingpong");
|
||||
inbound_destination.set_packet_callback(on_packet);
|
||||
|
||||
outbound_destination = RNS::Destination({RNS::Type::NONE},
|
||||
RNS::Type::Destination::OUT,
|
||||
RNS::Type::Destination::PLAIN,
|
||||
"microreticulum",
|
||||
"pingpong");
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial && millis() < 5000) {
|
||||
delay(100);
|
||||
}
|
||||
delay(250);
|
||||
|
||||
Serial.println();
|
||||
RNS::loglevel(RNS::LOG_NOTICE);
|
||||
setup_reticulum();
|
||||
Serial.println("Exercise 301: microReticulum BLE ping-pong");
|
||||
print_config();
|
||||
Serial.println("microReticulum ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
reticulum.loop();
|
||||
|
||||
static uint32_t next_tx_ms = 0;
|
||||
static uint32_t next_wait_log_ms = 0;
|
||||
static uint32_t iter = 0;
|
||||
uint32_t now = millis();
|
||||
|
||||
if (next_tx_ms == 0) {
|
||||
uint32_t offset = (NODE_LABEL[0] == 'A') ? 500 : 1500;
|
||||
next_tx_ms = now + offset;
|
||||
}
|
||||
|
||||
if ((int32_t)(now - next_tx_ms) >= 0) {
|
||||
next_tx_ms = now + 2000;
|
||||
if (ble_impl && !ble_impl->connected()) {
|
||||
if (next_wait_log_ms == 0 || (int32_t)(now - next_wait_log_ms) >= 0) {
|
||||
next_wait_log_ms = now + 10000;
|
||||
Serial.printf("BLE %s waiting for peer\r\n", ble_impl->role_name());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String message = String(NODE_LABEL) + " says hi. iter=" + String(iter++);
|
||||
Serial.printf("TX RNS BLE: %s\r\n", message.c_str());
|
||||
|
||||
RNS::Packet packet(outbound_destination,
|
||||
RNS::bytesFromString(message.c_str()),
|
||||
RNS::Type::Packet::DATA,
|
||||
RNS::Type::Packet::CONTEXT_NONE,
|
||||
RNS::Type::Transport::BROADCAST,
|
||||
RNS::Type::Packet::HEADER_1,
|
||||
{RNS::Bytes::NONE},
|
||||
false);
|
||||
packet.send();
|
||||
}
|
||||
|
||||
delay(5);
|
||||
}
|
||||
|
||||
int _write(int file, char* ptr, int len) {
|
||||
(void)file;
|
||||
int wrote = Serial.write(ptr, len);
|
||||
Serial.flush();
|
||||
return wrote;
|
||||
}
|
||||
85
exercises/302_microReticulum_ble_announce_single/README.md
Normal file
85
exercises/302_microReticulum_ble_announce_single/README.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Exercise 302: microReticulum BLE announce + SINGLE destination ping-pong
|
||||
|
||||
This exercise is the BLE analogue of Exercise 202.
|
||||
|
||||
Exercise 301 proved that microReticulum `PLAIN` packets can cross the ESP32/T-Beam BLE interface. Exercise 302 adds Reticulum identity discovery: each node creates an identity, announces an inbound `SINGLE` destination over BLE, learns the peer's announced identity and destination hash, then sends encrypted `DATA` packets to that learned destination.
|
||||
|
||||
```text
|
||||
BLE GATT connection -> Identity -> SINGLE destination -> announce -> peer learns identity/path -> encrypted DATA packet
|
||||
```
|
||||
|
||||
For this first BLE `SINGLE` proof, roles are still explicit:
|
||||
|
||||
```text
|
||||
AMY -> BLE central/client
|
||||
BOB -> BLE peripheral/server
|
||||
```
|
||||
|
||||
## Build, Upload, And Monitor
|
||||
|
||||
*Start BOB first* so the peripheral is advertising before AMY scans.
|
||||
|
||||
In one console:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/302_microReticulum_ble_announce_single -e bob -t upload --upload-port /dev/ttytBOB && \
|
||||
pio device monitor -d exercises/302_microReticulum_ble_announce_single -e bob
|
||||
```
|
||||
|
||||
In another console:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/302_microReticulum_ble_announce_single -e amy -t upload --upload-port /dev/ttytAMY && \
|
||||
pio device monitor -d exercises/302_microReticulum_ble_announce_single -e amy
|
||||
```
|
||||
|
||||
The `platformio.ini` maps each environment to its stable USB symlink:
|
||||
|
||||
```text
|
||||
amy -> /dev/ttytAMY
|
||||
bob -> /dev/ttytBOB
|
||||
cy -> /dev/ttytCY
|
||||
dan -> /dev/ttytDAN
|
||||
ed -> /dev/ttytED
|
||||
```
|
||||
|
||||
The shorter form should also work once the symlinks exist:
|
||||
|
||||
```bash
|
||||
pio run -d exercises/302_microReticulum_ble_announce_single -e bob -t upload
|
||||
pio device monitor -d exercises/302_microReticulum_ble_announce_single -e bob
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
After the BLE GATT connection is made, each node prints its local `SINGLE` destination hash and periodically announces it:
|
||||
|
||||
```text
|
||||
Local SINGLE destination: 2f6c...
|
||||
TX ANNOUNCE: Amy
|
||||
```
|
||||
|
||||
After the peer's announce is received, the node prints the learned destination:
|
||||
|
||||
```text
|
||||
RX ANNOUNCE: label=Bob hash=91a4...
|
||||
```
|
||||
|
||||
Once a peer is known, it sends encrypted `DATA` packets to that destination:
|
||||
|
||||
```text
|
||||
TX SINGLE BLE: Amy -> Bob iter=0
|
||||
RX SINGLE BLE: Bob -> Amy iter=0
|
||||
```
|
||||

|
||||
## Notes
|
||||
|
||||
- This exercise uses the same ESP32 BLE interface introduced in Exercise 301.
|
||||
- The BLE fragmentation header matches `/usr/local/src/ble-reticulum/BLE_PROTOCOL_v2.2.md`: `[type:1][sequence:2][total:2][payload...]`.
|
||||
- The identity is generated on each boot. The destination hash can change after reset because the identity changes.
|
||||
- App data in the announce carries the human-readable node label, such as `Amy` or `Bob`.
|
||||
- This is still not a negotiated Reticulum `Link`; Exercise 303 should use the announced `SINGLE` destination as the target for a negotiated link over BLE.
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
; Exercise 302: microReticulum BLE announce + SINGLE destination ping-pong
|
||||
|
||||
[platformio]
|
||||
default_envs = amy
|
||||
|
||||
[env]
|
||||
platform = espressif32
|
||||
framework = arduino
|
||||
board = esp32-s3-devkitc-1
|
||||
monitor_speed = 115200
|
||||
upload_speed = 460800
|
||||
board_build.partitions = huge_app.csv
|
||||
|
||||
build_flags =
|
||||
-Wall
|
||||
-Wno-missing-field-initializers
|
||||
-Wno-format
|
||||
-D RNS_USE_FS
|
||||
-D RNS_PERSIST_PATHS
|
||||
-D USTORE_USE_UNIVERSALFS
|
||||
-D MSGPACK_USE_BOOST=OFF
|
||||
-D MCU_ESP32
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
lib_deps =
|
||||
ArduinoJson@^7.4.2
|
||||
MsgPack@^0.4.2
|
||||
https://github.com/attermann/Crypto.git
|
||||
https://github.com/attermann/microStore.git
|
||||
microReticulum=symlink:///usr/local/src/microreticulum/microReticulum
|
||||
|
||||
[env:amy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytAMY
|
||||
monitor_port = /dev/ttytAMY
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Amy\"
|
||||
-D BLE_ROLE_CENTRAL
|
||||
|
||||
[env:bob]
|
||||
extends = env
|
||||
upload_port = /dev/ttytBOB
|
||||
monitor_port = /dev/ttytBOB
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Bob\"
|
||||
-D BLE_ROLE_PERIPHERAL
|
||||
|
||||
[env:cy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytCY
|
||||
monitor_port = /dev/ttytCY
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Cy\"
|
||||
-D BLE_ROLE_CENTRAL
|
||||
|
||||
[env:dan]
|
||||
extends = env
|
||||
upload_port = /dev/ttytDAN
|
||||
monitor_port = /dev/ttytDAN
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Dan\"
|
||||
-D BLE_ROLE_PERIPHERAL
|
||||
|
||||
[env:ed]
|
||||
extends = env
|
||||
upload_port = /dev/ttytED
|
||||
monitor_port = /dev/ttytED
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Ed\"
|
||||
-D BLE_ROLE_CENTRAL
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
|
||||
#include <BLE2902.h>
|
||||
#include <BLEService.h>
|
||||
#include <Cryptography/Random.h>
|
||||
#include <Identity.h>
|
||||
#include <Log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string.h>
|
||||
|
||||
using namespace RNS;
|
||||
|
||||
#ifndef NODE_LABEL
|
||||
#define NODE_LABEL "?"
|
||||
#endif
|
||||
|
||||
static TBeamSupremeBleInterface* active_ble_interface = nullptr;
|
||||
|
||||
class TBeamSupremeBleInterface::ServerCallbacks : public BLEServerCallbacks {
|
||||
public:
|
||||
explicit ServerCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onConnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
_owner->_connected = true;
|
||||
_owner->_server_handshake_received = false;
|
||||
Serial.println("BLE peripheral: central connected");
|
||||
INFO("BLE central connected");
|
||||
}
|
||||
|
||||
void onDisconnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
_owner->_connected = false;
|
||||
_owner->_server_handshake_received = false;
|
||||
_owner->reset_reassembly();
|
||||
Serial.println("BLE peripheral: central disconnected; restarting advertising");
|
||||
INFO("BLE central disconnected; restarting advertising");
|
||||
BLEDevice::startAdvertising();
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::RxCallbacks : public BLECharacteristicCallbacks {
|
||||
public:
|
||||
explicit RxCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onWrite(BLECharacteristic* characteristic) override {
|
||||
std::string value = characteristic->getValue();
|
||||
if (value.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_owner->_server_handshake_received && value.size() == 16) {
|
||||
_owner->_server_handshake_received = true;
|
||||
Serial.println("BLE peripheral: identity handshake received");
|
||||
INFOF("BLE identity handshake received: %d bytes", (int)value.size());
|
||||
return;
|
||||
}
|
||||
|
||||
_owner->handle_fragment(reinterpret_cast<const uint8_t*>(value.data()), value.size());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::AdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
|
||||
public:
|
||||
explicit AdvertisedDeviceCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onResult(BLEAdvertisedDevice advertised_device) override {
|
||||
bool has_service = advertised_device.haveServiceUUID() &&
|
||||
advertised_device.isAdvertisingService(BLEUUID(TBeamSupremeBleInterface::SERVICE_UUID));
|
||||
bool has_rns_name = advertised_device.haveName() &&
|
||||
advertised_device.getName().rfind("RNS-", 0) == 0;
|
||||
|
||||
if ((has_service || has_rns_name) || _owner->_scan_report_count < 5) {
|
||||
Serial.printf("BLE scan: addr=%s name=%s service=%s rssi=%d\r\n",
|
||||
advertised_device.getAddress().toString().c_str(),
|
||||
advertised_device.haveName() ? advertised_device.getName().c_str() : "",
|
||||
has_service ? "yes" : "no",
|
||||
advertised_device.getRSSI());
|
||||
_owner->_scan_report_count++;
|
||||
}
|
||||
|
||||
if (!has_service && !has_rns_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
BLEDevice::getScan()->stop();
|
||||
delete _owner->_advertised_device;
|
||||
_owner->_advertised_device = new BLEAdvertisedDevice(advertised_device);
|
||||
_owner->_do_connect = true;
|
||||
_owner->_scanning = false;
|
||||
Serial.printf("BLE central: peer candidate found: %s\r\n",
|
||||
advertised_device.getAddress().toString().c_str());
|
||||
INFOF("BLE peer found: %s", advertised_device.getAddress().toString().c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
TBeamSupremeBleInterface::TBeamSupremeBleInterface(const char* name) : InterfaceImpl(name) {
|
||||
_IN = true;
|
||||
_OUT = true;
|
||||
_bitrate = 1000000;
|
||||
_HW_MTU = BLE_PAYLOAD_SIZE;
|
||||
#if defined(BLE_ROLE_CENTRAL)
|
||||
_central_role = true;
|
||||
#else
|
||||
_central_role = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
TBeamSupremeBleInterface::~TBeamSupremeBleInterface() {
|
||||
stop();
|
||||
delete _advertised_device;
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::start() {
|
||||
if (_started) {
|
||||
return true;
|
||||
}
|
||||
|
||||
active_ble_interface = this;
|
||||
BLEDevice::init(std::string("RNS-") + NODE_LABEL);
|
||||
BLEDevice::setMTU(BLE_MTU);
|
||||
|
||||
if (_central_role) {
|
||||
Serial.println("BLE central: starting scanner");
|
||||
INFO("BLE starting as central/client");
|
||||
start_central_scan();
|
||||
} else {
|
||||
Serial.println("BLE peripheral: starting advertiser");
|
||||
INFO("BLE starting as peripheral/server");
|
||||
start_peripheral();
|
||||
}
|
||||
|
||||
_started = true;
|
||||
_online = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::stop() {
|
||||
_online = false;
|
||||
_connected = false;
|
||||
if (_client && _client->isConnected()) {
|
||||
_client->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::loop() {
|
||||
if (!_online) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_central_role) {
|
||||
if (_do_connect && _advertised_device) {
|
||||
_do_connect = false;
|
||||
if (!connect_to_advertised_device(_advertised_device)) {
|
||||
_connected = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_connected && !_scanning && (int32_t)(millis() - _next_scan_ms) >= 0) {
|
||||
start_central_scan();
|
||||
}
|
||||
}
|
||||
|
||||
if (_reassembly_started_ms != 0 && millis() - _reassembly_started_ms > REASSEMBLY_TIMEOUT_MS) {
|
||||
WARNING("BLE reassembly timeout; dropping partial packet");
|
||||
reset_reassembly();
|
||||
}
|
||||
|
||||
RNS::Bytes packet({RNS::Type::NONE});
|
||||
while (dequeue_packet(packet)) {
|
||||
InterfaceImpl::handle_incoming(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_peripheral() {
|
||||
_server = BLEDevice::createServer();
|
||||
_server->setCallbacks(new ServerCallbacks(this));
|
||||
|
||||
BLEService* service = _server->createService(SERVICE_UUID);
|
||||
_tx_characteristic = service->createCharacteristic(
|
||||
TX_UUID,
|
||||
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
_tx_characteristic->addDescriptor(new BLE2902());
|
||||
|
||||
BLECharacteristic* rx_characteristic = service->createCharacteristic(
|
||||
RX_UUID,
|
||||
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
|
||||
rx_characteristic->setCallbacks(new RxCallbacks(this));
|
||||
|
||||
_identity_characteristic = service->createCharacteristic(
|
||||
IDENTITY_UUID,
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_identity_characteristic->setValue((uint8_t*)identity.data(), identity.size());
|
||||
|
||||
service->start();
|
||||
|
||||
BLEAdvertising* advertising = BLEDevice::getAdvertising();
|
||||
advertising->addServiceUUID(SERVICE_UUID);
|
||||
advertising->setScanResponse(true);
|
||||
advertising->setMinPreferred(0x00);
|
||||
BLEDevice::startAdvertising();
|
||||
Serial.println("BLE peripheral: advertising Reticulum service");
|
||||
INFO("BLE advertising Reticulum service");
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_central_scan() {
|
||||
_scanning = true;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
_scan_report_count = 0;
|
||||
|
||||
BLEScan* scan = BLEDevice::getScan();
|
||||
scan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks(this), true);
|
||||
scan->setInterval(1349);
|
||||
scan->setWindow(449);
|
||||
scan->setActiveScan(true);
|
||||
Serial.println("BLE central: scanning for Reticulum service");
|
||||
scan->start(5, false);
|
||||
if (!_do_connect && !_connected) {
|
||||
_scanning = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
Serial.println("BLE central: scan complete; no peer found");
|
||||
}
|
||||
scan->clearResults();
|
||||
INFO("BLE scanning for Reticulum service");
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::connect_to_advertised_device(BLEAdvertisedDevice* device) {
|
||||
Serial.printf("BLE central: connecting to %s\r\n", device->getAddress().toString().c_str());
|
||||
INFOF("BLE connecting to %s", device->getAddress().toString().c_str());
|
||||
|
||||
_client = BLEDevice::createClient();
|
||||
if (!_client->connect(device)) {
|
||||
Serial.println("BLE central: connect failed");
|
||||
WARNING("BLE connect failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
_client->setMTU(BLE_MTU);
|
||||
BLERemoteService* service = _client->getService(BLEUUID(SERVICE_UUID));
|
||||
if (!service) {
|
||||
Serial.println("BLE central: Reticulum service not found after connect");
|
||||
WARNING("BLE Reticulum service not found after connect");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
_remote_rx_characteristic = service->getCharacteristic(BLEUUID(RX_UUID));
|
||||
_remote_tx_characteristic = service->getCharacteristic(BLEUUID(TX_UUID));
|
||||
BLERemoteCharacteristic* identity_characteristic = service->getCharacteristic(BLEUUID(IDENTITY_UUID));
|
||||
|
||||
if (!_remote_rx_characteristic || !_remote_tx_characteristic) {
|
||||
Serial.println("BLE central: RX/TX characteristics not found");
|
||||
WARNING("BLE Reticulum RX/TX characteristics not found");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (identity_characteristic && identity_characteristic->canRead()) {
|
||||
std::string peer_identity = identity_characteristic->readValue();
|
||||
Serial.printf("BLE central: peer identity read: %d bytes\r\n", (int)peer_identity.size());
|
||||
INFOF("BLE peer identity read: %d bytes", (int)peer_identity.size());
|
||||
}
|
||||
|
||||
if (_remote_tx_characteristic->canNotify()) {
|
||||
_remote_tx_characteristic->registerForNotify(client_notify_callback);
|
||||
} else {
|
||||
Serial.println("BLE central: peer TX characteristic does not notify");
|
||||
}
|
||||
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)identity.data(), identity.size(), false);
|
||||
_connected = true;
|
||||
_scanning = false;
|
||||
Serial.println("BLE central: connected and identity handshake sent");
|
||||
INFO("BLE connected and identity handshake sent");
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_outgoing(const Bytes& data) {
|
||||
if (!_online || !_connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
|
||||
if (total == 0 || total > 65535) {
|
||||
WARNINGF("BLE cannot fragment packet of size %d", (int)data.size());
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
uint8_t fragment[BLE_MTU];
|
||||
uint8_t fragment_type = FRAG_CONTINUE;
|
||||
if (i == 0) {
|
||||
fragment_type = FRAG_START;
|
||||
} else if (i == total - 1) {
|
||||
fragment_type = FRAG_END;
|
||||
}
|
||||
|
||||
size_t offset = i * BLE_PAYLOAD_SIZE;
|
||||
size_t chunk = std::min(BLE_PAYLOAD_SIZE, data.size() - offset);
|
||||
fragment[0] = fragment_type;
|
||||
fragment[1] = (uint8_t)((i >> 8) & 0xFF);
|
||||
fragment[2] = (uint8_t)(i & 0xFF);
|
||||
fragment[3] = (uint8_t)((total >> 8) & 0xFF);
|
||||
fragment[4] = (uint8_t)(total & 0xFF);
|
||||
memcpy(fragment + FRAG_HEADER_SIZE, data.data() + offset, chunk);
|
||||
send_fragment(fragment, FRAG_HEADER_SIZE + chunk);
|
||||
delay(8);
|
||||
}
|
||||
|
||||
InterfaceImpl::handle_outgoing(data);
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_fragment(const uint8_t* data, size_t len) {
|
||||
if (_central_role) {
|
||||
if (_remote_rx_characteristic) {
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)data, len, false);
|
||||
}
|
||||
} else {
|
||||
if (_tx_characteristic) {
|
||||
_tx_characteristic->setValue((uint8_t*)data, len);
|
||||
_tx_characteristic->notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::handle_fragment(const uint8_t* data, size_t len) {
|
||||
if (len < FRAG_HEADER_SIZE) {
|
||||
WARNINGF("BLE fragment too short: %d", (int)len);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fragment_type = data[0];
|
||||
uint16_t sequence = ((uint16_t)data[1] << 8) | data[2];
|
||||
uint16_t total = ((uint16_t)data[3] << 8) | data[4];
|
||||
const uint8_t* payload = data + FRAG_HEADER_SIZE;
|
||||
size_t payload_len = len - FRAG_HEADER_SIZE;
|
||||
|
||||
if ((fragment_type != FRAG_START && fragment_type != FRAG_CONTINUE && fragment_type != FRAG_END) ||
|
||||
total == 0 || sequence >= total) {
|
||||
WARNING("BLE invalid fragment header");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sequence == 0) {
|
||||
reset_reassembly();
|
||||
_expected_total = total;
|
||||
_received_fragments = 0;
|
||||
_reassembly_started_ms = millis();
|
||||
}
|
||||
|
||||
if (_expected_total != total || sequence != _received_fragments) {
|
||||
WARNING("BLE out-of-order fragment; dropping partial packet");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
_reassembly_buffer.append(payload, payload_len);
|
||||
_received_fragments++;
|
||||
|
||||
if (_received_fragments == _expected_total) {
|
||||
enqueue_packet(_reassembly_buffer);
|
||||
reset_reassembly();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::enqueue_packet(const RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
_incoming_packets.push_back(packet);
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::dequeue_packet(RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
if (_incoming_packets.empty()) {
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return false;
|
||||
}
|
||||
packet = _incoming_packets.front();
|
||||
_incoming_packets.pop_front();
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::reset_reassembly() {
|
||||
_reassembly_buffer.clear();
|
||||
_expected_total = 0;
|
||||
_received_fragments = 0;
|
||||
_reassembly_started_ms = 0;
|
||||
}
|
||||
|
||||
RNS::Bytes TBeamSupremeBleInterface::local_identity_hash() const {
|
||||
String material = String("microReticulum BLE ") + NODE_LABEL;
|
||||
return Identity::truncated_hash(RNS::bytesFromString(material.c_str()));
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify) {
|
||||
(void)characteristic;
|
||||
(void)is_notify;
|
||||
if (active_ble_interface) {
|
||||
active_ble_interface->handle_fragment(data, length);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
#pragma once
|
||||
|
||||
#include <Bytes.h>
|
||||
#include <Interface.h>
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BLEAdvertisedDevice.h>
|
||||
#include <BLEClient.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLERemoteCharacteristic.h>
|
||||
#include <BLEScan.h>
|
||||
#include <BLEServer.h>
|
||||
|
||||
#include <deque>
|
||||
|
||||
class TBeamSupremeBleInterface : public RNS::InterfaceImpl {
|
||||
public:
|
||||
explicit TBeamSupremeBleInterface(const char* name = "TBeamSupremeBLE");
|
||||
~TBeamSupremeBleInterface() override;
|
||||
|
||||
bool start() override;
|
||||
void stop() override;
|
||||
void loop() override;
|
||||
|
||||
bool connected() const { return _connected; }
|
||||
bool is_central() const { return _central_role; }
|
||||
const char* role_name() const { return _central_role ? "central" : "peripheral"; }
|
||||
|
||||
private:
|
||||
void send_outgoing(const RNS::Bytes& data) override;
|
||||
|
||||
void start_peripheral();
|
||||
void start_central_scan();
|
||||
bool connect_to_advertised_device(BLEAdvertisedDevice* device);
|
||||
|
||||
void send_fragment(const uint8_t* data, size_t len);
|
||||
void handle_fragment(const uint8_t* data, size_t len);
|
||||
void enqueue_packet(const RNS::Bytes& packet);
|
||||
bool dequeue_packet(RNS::Bytes& packet);
|
||||
void reset_reassembly();
|
||||
RNS::Bytes local_identity_hash() const;
|
||||
|
||||
static void client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify);
|
||||
|
||||
class ServerCallbacks;
|
||||
class RxCallbacks;
|
||||
class AdvertisedDeviceCallbacks;
|
||||
|
||||
static constexpr const char* SERVICE_UUID = "37145b00-442d-4a94-917f-8f42c5da28e3";
|
||||
static constexpr const char* TX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e4";
|
||||
static constexpr const char* RX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e5";
|
||||
static constexpr const char* IDENTITY_UUID = "37145b00-442d-4a94-917f-8f42c5da28e6";
|
||||
|
||||
static constexpr uint8_t FRAG_START = 0x01;
|
||||
static constexpr uint8_t FRAG_CONTINUE = 0x02;
|
||||
static constexpr uint8_t FRAG_END = 0x03;
|
||||
static constexpr size_t FRAG_HEADER_SIZE = 5;
|
||||
static constexpr size_t BLE_MTU = 185;
|
||||
static constexpr size_t BLE_PAYLOAD_SIZE = BLE_MTU - FRAG_HEADER_SIZE;
|
||||
static constexpr uint32_t REASSEMBLY_TIMEOUT_MS = 30000;
|
||||
static constexpr uint32_t SCAN_RETRY_MS = 5000;
|
||||
|
||||
bool _central_role = false;
|
||||
bool _started = false;
|
||||
bool _connected = false;
|
||||
bool _do_connect = false;
|
||||
bool _scanning = false;
|
||||
bool _server_handshake_received = false;
|
||||
uint8_t _scan_report_count = 0;
|
||||
uint32_t _next_scan_ms = 0;
|
||||
|
||||
BLEServer* _server = nullptr;
|
||||
BLECharacteristic* _tx_characteristic = nullptr;
|
||||
BLECharacteristic* _identity_characteristic = nullptr;
|
||||
BLEAdvertisedDevice* _advertised_device = nullptr;
|
||||
BLEClient* _client = nullptr;
|
||||
BLERemoteCharacteristic* _remote_rx_characteristic = nullptr;
|
||||
BLERemoteCharacteristic* _remote_tx_characteristic = nullptr;
|
||||
|
||||
RNS::Bytes _reassembly_buffer;
|
||||
uint16_t _expected_total = 0;
|
||||
uint16_t _received_fragments = 0;
|
||||
uint32_t _reassembly_started_ms = 0;
|
||||
|
||||
std::deque<RNS::Bytes> _incoming_packets;
|
||||
portMUX_TYPE _queue_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
};
|
||||
186
exercises/302_microReticulum_ble_announce_single/src/main.cpp
Normal file
186
exercises/302_microReticulum_ble_announce_single/src/main.cpp
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Destination.h>
|
||||
#include <Identity.h>
|
||||
#include <Log.h>
|
||||
#include <Packet.h>
|
||||
#include <Reticulum.h>
|
||||
#include <Transport.h>
|
||||
#include <Type.h>
|
||||
#include <Utilities/OS.h>
|
||||
#include <microStore/Adapters/UniversalFileSystem.h>
|
||||
#include <microStore/FileSystem.h>
|
||||
|
||||
#ifndef NODE_LABEL
|
||||
#define NODE_LABEL "?"
|
||||
#endif
|
||||
|
||||
static constexpr const char* APP_NAME = "microreticulum";
|
||||
static constexpr const char* APP_ASPECT = "singleping";
|
||||
static constexpr const char* ANNOUNCE_FILTER = "microreticulum.singleping";
|
||||
|
||||
static RNS::Reticulum reticulum({RNS::Type::NONE});
|
||||
static RNS::Interface ble_interface({RNS::Type::NONE});
|
||||
static RNS::Identity local_identity({RNS::Type::NONE});
|
||||
static RNS::Destination inbound_destination({RNS::Type::NONE});
|
||||
static RNS::Destination peer_destination({RNS::Type::NONE});
|
||||
static RNS::Bytes peer_hash;
|
||||
static String peer_label;
|
||||
static bool have_peer = false;
|
||||
static TBeamSupremeBleInterface* ble_impl = nullptr;
|
||||
|
||||
class SingleAnnounceHandler : public RNS::AnnounceHandler {
|
||||
public:
|
||||
SingleAnnounceHandler() : RNS::AnnounceHandler(ANNOUNCE_FILTER) {}
|
||||
|
||||
void received_announce(const RNS::Bytes& destination_hash,
|
||||
const RNS::Identity& announced_identity,
|
||||
const RNS::Bytes& app_data) override {
|
||||
String label = app_data ? String(app_data.toString().c_str()) : String("(no label)");
|
||||
if (label == NODE_LABEL) {
|
||||
return;
|
||||
}
|
||||
if (!announced_identity) {
|
||||
Serial.printf("RX ANNOUNCE ignored: missing identity for hash=%s\r\n",
|
||||
destination_hash.toHex().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
peer_hash = destination_hash;
|
||||
peer_label = label;
|
||||
peer_destination = RNS::Destination(announced_identity,
|
||||
RNS::Type::Destination::OUT,
|
||||
RNS::Type::Destination::SINGLE,
|
||||
destination_hash);
|
||||
have_peer = true;
|
||||
|
||||
Serial.printf("RX ANNOUNCE: label=%s hash=%s\r\n",
|
||||
peer_label.c_str(), peer_hash.toHex().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
static RNS::HAnnounceHandler announce_handler(new SingleAnnounceHandler());
|
||||
|
||||
static void print_config() {
|
||||
Serial.printf("Node=%s\r\n", NODE_LABEL);
|
||||
Serial.printf("BLE role=%s service=37145b00-442d-4a94-917f-8f42c5da28e3\r\n",
|
||||
ble_impl ? ble_impl->role_name() : "?");
|
||||
}
|
||||
|
||||
static void on_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
|
||||
(void)packet;
|
||||
Serial.printf("RX SINGLE BLE: %s\r\n", data.toString().c_str());
|
||||
}
|
||||
|
||||
static void send_announce() {
|
||||
if (!inbound_destination) {
|
||||
return;
|
||||
}
|
||||
Serial.printf("TX ANNOUNCE: %s\r\n", NODE_LABEL);
|
||||
inbound_destination.announce(RNS::bytesFromString(NODE_LABEL));
|
||||
}
|
||||
|
||||
static void setup_reticulum() {
|
||||
microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
|
||||
filesystem.init();
|
||||
RNS::Utilities::OS::register_filesystem(filesystem);
|
||||
|
||||
ble_impl = new TBeamSupremeBleInterface();
|
||||
ble_interface = ble_impl;
|
||||
ble_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
|
||||
RNS::Transport::register_interface(ble_interface);
|
||||
ble_interface.start();
|
||||
|
||||
reticulum = RNS::Reticulum();
|
||||
reticulum.transport_enabled(false);
|
||||
reticulum.probe_destination_enabled(false);
|
||||
reticulum.start();
|
||||
|
||||
local_identity = RNS::Identity();
|
||||
inbound_destination = RNS::Destination(local_identity,
|
||||
RNS::Type::Destination::IN,
|
||||
RNS::Type::Destination::SINGLE,
|
||||
APP_NAME,
|
||||
APP_ASPECT);
|
||||
inbound_destination.set_packet_callback(on_packet);
|
||||
inbound_destination.set_proof_strategy(RNS::Type::Destination::PROVE_NONE);
|
||||
|
||||
RNS::Transport::register_announce_handler(announce_handler);
|
||||
|
||||
Serial.printf("Local SINGLE destination: %s\r\n",
|
||||
inbound_destination.hash().toHex().c_str());
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial && millis() < 5000) {
|
||||
delay(100);
|
||||
}
|
||||
delay(250);
|
||||
|
||||
Serial.println();
|
||||
RNS::loglevel(RNS::LOG_NOTICE);
|
||||
setup_reticulum();
|
||||
Serial.println("Exercise 302: microReticulum BLE announce + SINGLE destination ping-pong");
|
||||
print_config();
|
||||
Serial.println("microReticulum ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
reticulum.loop();
|
||||
|
||||
static uint32_t next_announce_ms = 0;
|
||||
static uint32_t next_tx_ms = 0;
|
||||
static uint32_t next_wait_log_ms = 0;
|
||||
static uint32_t iter = 0;
|
||||
uint32_t now = millis();
|
||||
|
||||
if (ble_impl && !ble_impl->connected()) {
|
||||
if (next_wait_log_ms == 0 || (int32_t)(now - next_wait_log_ms) >= 0) {
|
||||
next_wait_log_ms = now + 10000;
|
||||
Serial.printf("BLE %s waiting for peer\r\n", ble_impl->role_name());
|
||||
}
|
||||
delay(5);
|
||||
return;
|
||||
}
|
||||
|
||||
if (next_announce_ms == 0) {
|
||||
uint32_t offset = (NODE_LABEL[0] == 'A') ? 700 : 1700;
|
||||
next_announce_ms = now + offset;
|
||||
}
|
||||
if ((int32_t)(now - next_announce_ms) >= 0) {
|
||||
next_announce_ms = now + 15000;
|
||||
send_announce();
|
||||
}
|
||||
|
||||
if (have_peer && next_tx_ms == 0) {
|
||||
uint32_t offset = (NODE_LABEL[0] == 'A') ? 2500 : 4000;
|
||||
next_tx_ms = now + offset;
|
||||
}
|
||||
if (have_peer && (int32_t)(now - next_tx_ms) >= 0) {
|
||||
next_tx_ms = now + 3000;
|
||||
|
||||
String message = String(NODE_LABEL) + " -> " + peer_label + " iter=" + String(iter++);
|
||||
Serial.printf("TX SINGLE BLE: %s\r\n", message.c_str());
|
||||
|
||||
RNS::Packet packet(peer_destination,
|
||||
RNS::bytesFromString(message.c_str()),
|
||||
RNS::Type::Packet::DATA,
|
||||
RNS::Type::Packet::CONTEXT_NONE,
|
||||
RNS::Type::Transport::BROADCAST,
|
||||
RNS::Type::Packet::HEADER_1,
|
||||
{RNS::Bytes::NONE},
|
||||
false);
|
||||
packet.send();
|
||||
}
|
||||
|
||||
delay(5);
|
||||
}
|
||||
|
||||
int _write(int file, char* ptr, int len) {
|
||||
(void)file;
|
||||
int wrote = Serial.write(ptr, len);
|
||||
Serial.flush();
|
||||
return wrote;
|
||||
}
|
||||
84
exercises/303_microReticulum_ble_link_ping_pong/README.md
Normal file
84
exercises/303_microReticulum_ble_link_ping_pong/README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Exercise 303: microReticulum BLE negotiated Link ping-pong
|
||||
|
||||
This exercise is the BLE analogue of Exercise 203.
|
||||
|
||||
Exercise 301 proved `PLAIN` packets over the ESP32/T-Beam BLE interface. Exercise 302 proved announced `SINGLE` destinations over BLE. Exercise 303 adds a negotiated Reticulum `Link`: the nodes begin with BLE connectivity and `SINGLE` destination announces, then one node opens a Link to the peer's announced `SINGLE` destination.
|
||||
|
||||
```text
|
||||
BLE GATT connection -> Identity -> SINGLE destination -> announce -> peer learns identity/path -> LINKREQUEST/LRPROOF handshake -> ACTIVE Link -> link DATA packets
|
||||
```
|
||||
|
||||
For this exercise, BLE roles are still explicit:
|
||||
|
||||
```text
|
||||
AMY -> BLE central/client
|
||||
BOB -> BLE peripheral/server
|
||||
```
|
||||
|
||||
BOB must be started first so it is advertising when AMY scans. This ordering is a BLE transport requirement in the current 300-series exercises, not a Reticulum Link requirement. Inside Reticulum, AMY opens the Link to BOB because the lower lexical node label initiates, matching Exercise 203.
|
||||
|
||||
An equal-peer BLE exercise should come after this, because it changes the BLE interface itself to dual-role operation and deterministic connection direction. A likely name is `304_microReticulum_ble_dual_role_ping_pong`.
|
||||
|
||||
## Build, Upload, And Monitor
|
||||
|
||||
In one console:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/303_microReticulum_ble_link_ping_pong -e bob -t upload --upload-port /dev/ttytBOB && \
|
||||
pio device monitor -d exercises/303_microReticulum_ble_link_ping_pong -e bob
|
||||
```
|
||||
|
||||
In another console:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/303_microReticulum_ble_link_ping_pong -e amy -t upload --upload-port /dev/ttytAMY && \
|
||||
pio device monitor -d exercises/303_microReticulum_ble_link_ping_pong -e amy
|
||||
```
|
||||
|
||||
The shorter form should also work once the symlinks exist:
|
||||
|
||||
```bash
|
||||
pio run -d exercises/303_microReticulum_ble_link_ping_pong -e bob -t upload
|
||||
pio device monitor -d exercises/303_microReticulum_ble_link_ping_pong -e bob
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
After the BLE GATT connection is made, each node prints and announces its local `SINGLE` destination:
|
||||
|
||||
```text
|
||||
Local SINGLE destination: 2f6c...
|
||||
TX ANNOUNCE: Amy
|
||||
RX ANNOUNCE: label=Bob hash=91a4...
|
||||
```
|
||||
|
||||
After AMY learns BOB's announce, AMY opens the Reticulum Link:
|
||||
|
||||
```text
|
||||
TX LINKREQUEST: opening link to Bob
|
||||
```
|
||||
|
||||
BOB should report the inbound link, and AMY should report active status:
|
||||
|
||||
```text
|
||||
RX LINK: inbound link established
|
||||
LINK ACTIVE: initiator link established
|
||||
```
|
||||
|
||||
Once active, both nodes send and receive packets over the negotiated Link:
|
||||
|
||||
```text
|
||||
TX LINK BLE: Amy -> Bob iter=0
|
||||
RX LINK BLE: Bob -> Amy iter=0
|
||||
```
|
||||

|
||||
## Notes
|
||||
|
||||
- This exercise uses the same ESP32 BLE interface introduced in Exercise 301.
|
||||
- Once the Reticulum Link is active, the sketch stops periodic announces so the console focuses on link traffic.
|
||||
- The identity is generated on each boot. Destination hashes can change after reset.
|
||||
- This is the first 300-series exercise that proves Reticulum negotiated Link establishment over the ESP32/T-Beam BLE interface.
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
; Exercise 303: microReticulum BLE negotiated Link ping-pong
|
||||
|
||||
[platformio]
|
||||
default_envs = amy
|
||||
|
||||
[env]
|
||||
platform = espressif32
|
||||
framework = arduino
|
||||
board = esp32-s3-devkitc-1
|
||||
monitor_speed = 115200
|
||||
upload_speed = 460800
|
||||
board_build.partitions = huge_app.csv
|
||||
|
||||
build_flags =
|
||||
-Wall
|
||||
-Wno-missing-field-initializers
|
||||
-Wno-format
|
||||
-D RNS_USE_FS
|
||||
-D RNS_PERSIST_PATHS
|
||||
-D USTORE_USE_UNIVERSALFS
|
||||
-D MSGPACK_USE_BOOST=OFF
|
||||
-D MCU_ESP32
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
lib_deps =
|
||||
ArduinoJson@^7.4.2
|
||||
MsgPack@^0.4.2
|
||||
https://github.com/attermann/Crypto.git
|
||||
https://github.com/attermann/microStore.git
|
||||
microReticulum=symlink:///usr/local/src/microreticulum/microReticulum
|
||||
|
||||
[env:amy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytAMY
|
||||
monitor_port = /dev/ttytAMY
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Amy\"
|
||||
-D BLE_ROLE_CENTRAL
|
||||
|
||||
[env:bob]
|
||||
extends = env
|
||||
upload_port = /dev/ttytBOB
|
||||
monitor_port = /dev/ttytBOB
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Bob\"
|
||||
-D BLE_ROLE_PERIPHERAL
|
||||
|
||||
[env:cy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytCY
|
||||
monitor_port = /dev/ttytCY
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Cy\"
|
||||
-D BLE_ROLE_CENTRAL
|
||||
|
||||
[env:dan]
|
||||
extends = env
|
||||
upload_port = /dev/ttytDAN
|
||||
monitor_port = /dev/ttytDAN
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Dan\"
|
||||
-D BLE_ROLE_PERIPHERAL
|
||||
|
||||
[env:ed]
|
||||
extends = env
|
||||
upload_port = /dev/ttytED
|
||||
monitor_port = /dev/ttytED
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D NODE_LABEL=\"Ed\"
|
||||
-D BLE_ROLE_CENTRAL
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
|
||||
#include <BLE2902.h>
|
||||
#include <BLEService.h>
|
||||
#include <Cryptography/Random.h>
|
||||
#include <Identity.h>
|
||||
#include <Log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string.h>
|
||||
|
||||
using namespace RNS;
|
||||
|
||||
#ifndef NODE_LABEL
|
||||
#define NODE_LABEL "?"
|
||||
#endif
|
||||
|
||||
static TBeamSupremeBleInterface* active_ble_interface = nullptr;
|
||||
|
||||
class TBeamSupremeBleInterface::ServerCallbacks : public BLEServerCallbacks {
|
||||
public:
|
||||
explicit ServerCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onConnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
_owner->_connected = true;
|
||||
_owner->_server_handshake_received = false;
|
||||
Serial.println("BLE peripheral: central connected");
|
||||
INFO("BLE central connected");
|
||||
}
|
||||
|
||||
void onDisconnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
_owner->_connected = false;
|
||||
_owner->_server_handshake_received = false;
|
||||
_owner->reset_reassembly();
|
||||
Serial.println("BLE peripheral: central disconnected; restarting advertising");
|
||||
INFO("BLE central disconnected; restarting advertising");
|
||||
BLEDevice::startAdvertising();
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::RxCallbacks : public BLECharacteristicCallbacks {
|
||||
public:
|
||||
explicit RxCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onWrite(BLECharacteristic* characteristic) override {
|
||||
std::string value = characteristic->getValue();
|
||||
if (value.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_owner->_server_handshake_received && value.size() == 16) {
|
||||
_owner->_server_handshake_received = true;
|
||||
Serial.println("BLE peripheral: identity handshake received");
|
||||
INFOF("BLE identity handshake received: %d bytes", (int)value.size());
|
||||
return;
|
||||
}
|
||||
|
||||
_owner->handle_fragment(reinterpret_cast<const uint8_t*>(value.data()), value.size());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::AdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
|
||||
public:
|
||||
explicit AdvertisedDeviceCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onResult(BLEAdvertisedDevice advertised_device) override {
|
||||
bool has_service = advertised_device.haveServiceUUID() &&
|
||||
advertised_device.isAdvertisingService(BLEUUID(TBeamSupremeBleInterface::SERVICE_UUID));
|
||||
bool has_rns_name = advertised_device.haveName() &&
|
||||
advertised_device.getName().rfind("RNS-", 0) == 0;
|
||||
|
||||
if ((has_service || has_rns_name) || _owner->_scan_report_count < 5) {
|
||||
Serial.printf("BLE scan: addr=%s name=%s service=%s rssi=%d\r\n",
|
||||
advertised_device.getAddress().toString().c_str(),
|
||||
advertised_device.haveName() ? advertised_device.getName().c_str() : "",
|
||||
has_service ? "yes" : "no",
|
||||
advertised_device.getRSSI());
|
||||
_owner->_scan_report_count++;
|
||||
}
|
||||
|
||||
if (!has_service && !has_rns_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
BLEDevice::getScan()->stop();
|
||||
delete _owner->_advertised_device;
|
||||
_owner->_advertised_device = new BLEAdvertisedDevice(advertised_device);
|
||||
_owner->_do_connect = true;
|
||||
_owner->_scanning = false;
|
||||
Serial.printf("BLE central: peer candidate found: %s\r\n",
|
||||
advertised_device.getAddress().toString().c_str());
|
||||
INFOF("BLE peer found: %s", advertised_device.getAddress().toString().c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
TBeamSupremeBleInterface::TBeamSupremeBleInterface(const char* name) : InterfaceImpl(name) {
|
||||
_IN = true;
|
||||
_OUT = true;
|
||||
_bitrate = 1000000;
|
||||
_HW_MTU = BLE_PAYLOAD_SIZE;
|
||||
#if defined(BLE_ROLE_CENTRAL)
|
||||
_central_role = true;
|
||||
#else
|
||||
_central_role = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
TBeamSupremeBleInterface::~TBeamSupremeBleInterface() {
|
||||
stop();
|
||||
delete _advertised_device;
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::start() {
|
||||
if (_started) {
|
||||
return true;
|
||||
}
|
||||
|
||||
active_ble_interface = this;
|
||||
BLEDevice::init(std::string("RNS-") + NODE_LABEL);
|
||||
BLEDevice::setMTU(BLE_MTU);
|
||||
|
||||
if (_central_role) {
|
||||
Serial.println("BLE central: starting scanner");
|
||||
INFO("BLE starting as central/client");
|
||||
start_central_scan();
|
||||
} else {
|
||||
Serial.println("BLE peripheral: starting advertiser");
|
||||
INFO("BLE starting as peripheral/server");
|
||||
start_peripheral();
|
||||
}
|
||||
|
||||
_started = true;
|
||||
_online = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::stop() {
|
||||
_online = false;
|
||||
_connected = false;
|
||||
if (_client && _client->isConnected()) {
|
||||
_client->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::loop() {
|
||||
if (!_online) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_central_role) {
|
||||
if (_do_connect && _advertised_device) {
|
||||
_do_connect = false;
|
||||
if (!connect_to_advertised_device(_advertised_device)) {
|
||||
_connected = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_connected && !_scanning && (int32_t)(millis() - _next_scan_ms) >= 0) {
|
||||
start_central_scan();
|
||||
}
|
||||
}
|
||||
|
||||
if (_reassembly_started_ms != 0 && millis() - _reassembly_started_ms > REASSEMBLY_TIMEOUT_MS) {
|
||||
WARNING("BLE reassembly timeout; dropping partial packet");
|
||||
reset_reassembly();
|
||||
}
|
||||
|
||||
RNS::Bytes packet({RNS::Type::NONE});
|
||||
while (dequeue_packet(packet)) {
|
||||
InterfaceImpl::handle_incoming(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_peripheral() {
|
||||
_server = BLEDevice::createServer();
|
||||
_server->setCallbacks(new ServerCallbacks(this));
|
||||
|
||||
BLEService* service = _server->createService(SERVICE_UUID);
|
||||
_tx_characteristic = service->createCharacteristic(
|
||||
TX_UUID,
|
||||
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
_tx_characteristic->addDescriptor(new BLE2902());
|
||||
|
||||
BLECharacteristic* rx_characteristic = service->createCharacteristic(
|
||||
RX_UUID,
|
||||
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
|
||||
rx_characteristic->setCallbacks(new RxCallbacks(this));
|
||||
|
||||
_identity_characteristic = service->createCharacteristic(
|
||||
IDENTITY_UUID,
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_identity_characteristic->setValue((uint8_t*)identity.data(), identity.size());
|
||||
|
||||
service->start();
|
||||
|
||||
BLEAdvertising* advertising = BLEDevice::getAdvertising();
|
||||
advertising->addServiceUUID(SERVICE_UUID);
|
||||
advertising->setScanResponse(true);
|
||||
advertising->setMinPreferred(0x00);
|
||||
BLEDevice::startAdvertising();
|
||||
Serial.println("BLE peripheral: advertising Reticulum service");
|
||||
INFO("BLE advertising Reticulum service");
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_central_scan() {
|
||||
_scanning = true;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
_scan_report_count = 0;
|
||||
|
||||
BLEScan* scan = BLEDevice::getScan();
|
||||
scan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks(this), true);
|
||||
scan->setInterval(1349);
|
||||
scan->setWindow(449);
|
||||
scan->setActiveScan(true);
|
||||
Serial.println("BLE central: scanning for Reticulum service");
|
||||
scan->start(5, false);
|
||||
if (!_do_connect && !_connected) {
|
||||
_scanning = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
Serial.println("BLE central: scan complete; no peer found");
|
||||
}
|
||||
scan->clearResults();
|
||||
INFO("BLE scanning for Reticulum service");
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::connect_to_advertised_device(BLEAdvertisedDevice* device) {
|
||||
Serial.printf("BLE central: connecting to %s\r\n", device->getAddress().toString().c_str());
|
||||
INFOF("BLE connecting to %s", device->getAddress().toString().c_str());
|
||||
|
||||
_client = BLEDevice::createClient();
|
||||
if (!_client->connect(device)) {
|
||||
Serial.println("BLE central: connect failed");
|
||||
WARNING("BLE connect failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
_client->setMTU(BLE_MTU);
|
||||
BLERemoteService* service = _client->getService(BLEUUID(SERVICE_UUID));
|
||||
if (!service) {
|
||||
Serial.println("BLE central: Reticulum service not found after connect");
|
||||
WARNING("BLE Reticulum service not found after connect");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
_remote_rx_characteristic = service->getCharacteristic(BLEUUID(RX_UUID));
|
||||
_remote_tx_characteristic = service->getCharacteristic(BLEUUID(TX_UUID));
|
||||
BLERemoteCharacteristic* identity_characteristic = service->getCharacteristic(BLEUUID(IDENTITY_UUID));
|
||||
|
||||
if (!_remote_rx_characteristic || !_remote_tx_characteristic) {
|
||||
Serial.println("BLE central: RX/TX characteristics not found");
|
||||
WARNING("BLE Reticulum RX/TX characteristics not found");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (identity_characteristic && identity_characteristic->canRead()) {
|
||||
std::string peer_identity = identity_characteristic->readValue();
|
||||
Serial.printf("BLE central: peer identity read: %d bytes\r\n", (int)peer_identity.size());
|
||||
INFOF("BLE peer identity read: %d bytes", (int)peer_identity.size());
|
||||
}
|
||||
|
||||
if (_remote_tx_characteristic->canNotify()) {
|
||||
_remote_tx_characteristic->registerForNotify(client_notify_callback);
|
||||
} else {
|
||||
Serial.println("BLE central: peer TX characteristic does not notify");
|
||||
}
|
||||
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)identity.data(), identity.size(), false);
|
||||
_connected = true;
|
||||
_scanning = false;
|
||||
Serial.println("BLE central: connected and identity handshake sent");
|
||||
INFO("BLE connected and identity handshake sent");
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_outgoing(const Bytes& data) {
|
||||
if (!_online || !_connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
|
||||
if (total == 0 || total > 65535) {
|
||||
WARNINGF("BLE cannot fragment packet of size %d", (int)data.size());
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
uint8_t fragment[BLE_MTU];
|
||||
uint8_t fragment_type = FRAG_CONTINUE;
|
||||
if (i == 0) {
|
||||
fragment_type = FRAG_START;
|
||||
} else if (i == total - 1) {
|
||||
fragment_type = FRAG_END;
|
||||
}
|
||||
|
||||
size_t offset = i * BLE_PAYLOAD_SIZE;
|
||||
size_t chunk = std::min(BLE_PAYLOAD_SIZE, data.size() - offset);
|
||||
fragment[0] = fragment_type;
|
||||
fragment[1] = (uint8_t)((i >> 8) & 0xFF);
|
||||
fragment[2] = (uint8_t)(i & 0xFF);
|
||||
fragment[3] = (uint8_t)((total >> 8) & 0xFF);
|
||||
fragment[4] = (uint8_t)(total & 0xFF);
|
||||
memcpy(fragment + FRAG_HEADER_SIZE, data.data() + offset, chunk);
|
||||
send_fragment(fragment, FRAG_HEADER_SIZE + chunk);
|
||||
delay(8);
|
||||
}
|
||||
|
||||
InterfaceImpl::handle_outgoing(data);
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_fragment(const uint8_t* data, size_t len) {
|
||||
if (_central_role) {
|
||||
if (_remote_rx_characteristic) {
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)data, len, false);
|
||||
}
|
||||
} else {
|
||||
if (_tx_characteristic) {
|
||||
_tx_characteristic->setValue((uint8_t*)data, len);
|
||||
_tx_characteristic->notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::handle_fragment(const uint8_t* data, size_t len) {
|
||||
if (len < FRAG_HEADER_SIZE) {
|
||||
WARNINGF("BLE fragment too short: %d", (int)len);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fragment_type = data[0];
|
||||
uint16_t sequence = ((uint16_t)data[1] << 8) | data[2];
|
||||
uint16_t total = ((uint16_t)data[3] << 8) | data[4];
|
||||
const uint8_t* payload = data + FRAG_HEADER_SIZE;
|
||||
size_t payload_len = len - FRAG_HEADER_SIZE;
|
||||
|
||||
if ((fragment_type != FRAG_START && fragment_type != FRAG_CONTINUE && fragment_type != FRAG_END) ||
|
||||
total == 0 || sequence >= total) {
|
||||
WARNING("BLE invalid fragment header");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sequence == 0) {
|
||||
reset_reassembly();
|
||||
_expected_total = total;
|
||||
_received_fragments = 0;
|
||||
_reassembly_started_ms = millis();
|
||||
}
|
||||
|
||||
if (_expected_total != total || sequence != _received_fragments) {
|
||||
WARNING("BLE out-of-order fragment; dropping partial packet");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
_reassembly_buffer.append(payload, payload_len);
|
||||
_received_fragments++;
|
||||
|
||||
if (_received_fragments == _expected_total) {
|
||||
enqueue_packet(_reassembly_buffer);
|
||||
reset_reassembly();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::enqueue_packet(const RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
_incoming_packets.push_back(packet);
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::dequeue_packet(RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
if (_incoming_packets.empty()) {
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return false;
|
||||
}
|
||||
packet = _incoming_packets.front();
|
||||
_incoming_packets.pop_front();
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::reset_reassembly() {
|
||||
_reassembly_buffer.clear();
|
||||
_expected_total = 0;
|
||||
_received_fragments = 0;
|
||||
_reassembly_started_ms = 0;
|
||||
}
|
||||
|
||||
RNS::Bytes TBeamSupremeBleInterface::local_identity_hash() const {
|
||||
String material = String("microReticulum BLE ") + NODE_LABEL;
|
||||
return Identity::truncated_hash(RNS::bytesFromString(material.c_str()));
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify) {
|
||||
(void)characteristic;
|
||||
(void)is_notify;
|
||||
if (active_ble_interface) {
|
||||
active_ble_interface->handle_fragment(data, length);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
#pragma once
|
||||
|
||||
#include <Bytes.h>
|
||||
#include <Interface.h>
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BLEAdvertisedDevice.h>
|
||||
#include <BLEClient.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLERemoteCharacteristic.h>
|
||||
#include <BLEScan.h>
|
||||
#include <BLEServer.h>
|
||||
|
||||
#include <deque>
|
||||
|
||||
class TBeamSupremeBleInterface : public RNS::InterfaceImpl {
|
||||
public:
|
||||
explicit TBeamSupremeBleInterface(const char* name = "TBeamSupremeBLE");
|
||||
~TBeamSupremeBleInterface() override;
|
||||
|
||||
bool start() override;
|
||||
void stop() override;
|
||||
void loop() override;
|
||||
|
||||
bool connected() const { return _connected; }
|
||||
bool is_central() const { return _central_role; }
|
||||
const char* role_name() const { return _central_role ? "central" : "peripheral"; }
|
||||
|
||||
private:
|
||||
void send_outgoing(const RNS::Bytes& data) override;
|
||||
|
||||
void start_peripheral();
|
||||
void start_central_scan();
|
||||
bool connect_to_advertised_device(BLEAdvertisedDevice* device);
|
||||
|
||||
void send_fragment(const uint8_t* data, size_t len);
|
||||
void handle_fragment(const uint8_t* data, size_t len);
|
||||
void enqueue_packet(const RNS::Bytes& packet);
|
||||
bool dequeue_packet(RNS::Bytes& packet);
|
||||
void reset_reassembly();
|
||||
RNS::Bytes local_identity_hash() const;
|
||||
|
||||
static void client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify);
|
||||
|
||||
class ServerCallbacks;
|
||||
class RxCallbacks;
|
||||
class AdvertisedDeviceCallbacks;
|
||||
|
||||
static constexpr const char* SERVICE_UUID = "37145b00-442d-4a94-917f-8f42c5da28e3";
|
||||
static constexpr const char* TX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e4";
|
||||
static constexpr const char* RX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e5";
|
||||
static constexpr const char* IDENTITY_UUID = "37145b00-442d-4a94-917f-8f42c5da28e6";
|
||||
|
||||
static constexpr uint8_t FRAG_START = 0x01;
|
||||
static constexpr uint8_t FRAG_CONTINUE = 0x02;
|
||||
static constexpr uint8_t FRAG_END = 0x03;
|
||||
static constexpr size_t FRAG_HEADER_SIZE = 5;
|
||||
static constexpr size_t BLE_MTU = 185;
|
||||
static constexpr size_t BLE_PAYLOAD_SIZE = BLE_MTU - FRAG_HEADER_SIZE;
|
||||
static constexpr uint32_t REASSEMBLY_TIMEOUT_MS = 30000;
|
||||
static constexpr uint32_t SCAN_RETRY_MS = 5000;
|
||||
|
||||
bool _central_role = false;
|
||||
bool _started = false;
|
||||
bool _connected = false;
|
||||
bool _do_connect = false;
|
||||
bool _scanning = false;
|
||||
bool _server_handshake_received = false;
|
||||
uint8_t _scan_report_count = 0;
|
||||
uint32_t _next_scan_ms = 0;
|
||||
|
||||
BLEServer* _server = nullptr;
|
||||
BLECharacteristic* _tx_characteristic = nullptr;
|
||||
BLECharacteristic* _identity_characteristic = nullptr;
|
||||
BLEAdvertisedDevice* _advertised_device = nullptr;
|
||||
BLEClient* _client = nullptr;
|
||||
BLERemoteCharacteristic* _remote_rx_characteristic = nullptr;
|
||||
BLERemoteCharacteristic* _remote_tx_characteristic = nullptr;
|
||||
|
||||
RNS::Bytes _reassembly_buffer;
|
||||
uint16_t _expected_total = 0;
|
||||
uint16_t _received_fragments = 0;
|
||||
uint32_t _reassembly_started_ms = 0;
|
||||
|
||||
std::deque<RNS::Bytes> _incoming_packets;
|
||||
portMUX_TYPE _queue_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
};
|
||||
230
exercises/303_microReticulum_ble_link_ping_pong/src/main.cpp
Normal file
230
exercises/303_microReticulum_ble_link_ping_pong/src/main.cpp
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Destination.h>
|
||||
#include <Identity.h>
|
||||
#include <Link.h>
|
||||
#include <Log.h>
|
||||
#include <Packet.h>
|
||||
#include <Reticulum.h>
|
||||
#include <Transport.h>
|
||||
#include <Type.h>
|
||||
#include <Utilities/OS.h>
|
||||
#include <microStore/Adapters/UniversalFileSystem.h>
|
||||
#include <microStore/FileSystem.h>
|
||||
|
||||
#ifndef NODE_LABEL
|
||||
#define NODE_LABEL "?"
|
||||
#endif
|
||||
|
||||
static constexpr const char* APP_NAME = "microreticulum";
|
||||
static constexpr const char* APP_ASPECT = "linkping";
|
||||
static constexpr const char* ANNOUNCE_FILTER = "microreticulum.linkping";
|
||||
|
||||
static RNS::Reticulum reticulum({RNS::Type::NONE});
|
||||
static RNS::Interface ble_interface({RNS::Type::NONE});
|
||||
static RNS::Identity local_identity({RNS::Type::NONE});
|
||||
static RNS::Destination inbound_destination({RNS::Type::NONE});
|
||||
static RNS::Destination peer_destination({RNS::Type::NONE});
|
||||
static RNS::Link active_link({RNS::Type::NONE});
|
||||
static RNS::Link pending_link({RNS::Type::NONE});
|
||||
static RNS::Bytes peer_hash;
|
||||
static String peer_label;
|
||||
static bool have_peer = false;
|
||||
static bool link_active = false;
|
||||
static bool link_attempted = false;
|
||||
static TBeamSupremeBleInterface* ble_impl = nullptr;
|
||||
|
||||
static bool should_initiate_link_to(const String& label) {
|
||||
return strcmp(NODE_LABEL, label.c_str()) < 0;
|
||||
}
|
||||
|
||||
static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
|
||||
(void)packet;
|
||||
Serial.printf("RX LINK BLE: %s\r\n", data.toString().c_str());
|
||||
}
|
||||
|
||||
static void on_link_closed(RNS::Link& link) {
|
||||
(void)link;
|
||||
Serial.println("LINK CLOSED");
|
||||
active_link = {RNS::Type::NONE};
|
||||
pending_link = {RNS::Type::NONE};
|
||||
link_active = false;
|
||||
link_attempted = false;
|
||||
}
|
||||
|
||||
static void on_outbound_link_established(RNS::Link& link) {
|
||||
active_link = link;
|
||||
active_link.set_packet_callback(on_link_packet);
|
||||
active_link.set_link_closed_callback(on_link_closed);
|
||||
link_active = true;
|
||||
Serial.println("LINK ACTIVE: initiator link established");
|
||||
}
|
||||
|
||||
static void on_inbound_link_established(RNS::Link& link) {
|
||||
active_link = link;
|
||||
active_link.set_packet_callback(on_link_packet);
|
||||
active_link.set_link_closed_callback(on_link_closed);
|
||||
link_active = true;
|
||||
link_attempted = true;
|
||||
Serial.println("RX LINK: inbound link established");
|
||||
}
|
||||
|
||||
class LinkAnnounceHandler : public RNS::AnnounceHandler {
|
||||
public:
|
||||
LinkAnnounceHandler() : RNS::AnnounceHandler(ANNOUNCE_FILTER) {}
|
||||
|
||||
void received_announce(const RNS::Bytes& destination_hash,
|
||||
const RNS::Identity& announced_identity,
|
||||
const RNS::Bytes& app_data) override {
|
||||
String label = app_data ? String(app_data.toString().c_str()) : String("(no label)");
|
||||
if (label == NODE_LABEL) {
|
||||
return;
|
||||
}
|
||||
if (!announced_identity) {
|
||||
Serial.printf("RX ANNOUNCE ignored: missing identity for hash=%s\r\n",
|
||||
destination_hash.toHex().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
peer_hash = destination_hash;
|
||||
peer_label = label;
|
||||
peer_destination = RNS::Destination(announced_identity,
|
||||
RNS::Type::Destination::OUT,
|
||||
RNS::Type::Destination::SINGLE,
|
||||
destination_hash);
|
||||
have_peer = true;
|
||||
|
||||
Serial.printf("RX ANNOUNCE: label=%s hash=%s\r\n",
|
||||
peer_label.c_str(), peer_hash.toHex().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
static RNS::HAnnounceHandler announce_handler(new LinkAnnounceHandler());
|
||||
|
||||
static void print_config() {
|
||||
Serial.printf("Node=%s\r\n", NODE_LABEL);
|
||||
Serial.printf("BLE role=%s service=37145b00-442d-4a94-917f-8f42c5da28e3\r\n",
|
||||
ble_impl ? ble_impl->role_name() : "?");
|
||||
}
|
||||
|
||||
static void send_announce() {
|
||||
if (!inbound_destination) {
|
||||
return;
|
||||
}
|
||||
Serial.printf("TX ANNOUNCE: %s\r\n", NODE_LABEL);
|
||||
inbound_destination.announce(RNS::bytesFromString(NODE_LABEL));
|
||||
}
|
||||
|
||||
static void maybe_open_link() {
|
||||
if (!have_peer || link_active || link_attempted || !peer_destination) {
|
||||
return;
|
||||
}
|
||||
if (!should_initiate_link_to(peer_label)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("TX LINKREQUEST: opening link to %s\r\n", peer_label.c_str());
|
||||
pending_link = RNS::Link(peer_destination);
|
||||
pending_link.set_packet_callback(on_link_packet);
|
||||
pending_link.set_link_established_callback(on_outbound_link_established);
|
||||
pending_link.set_link_closed_callback(on_link_closed);
|
||||
link_attempted = true;
|
||||
}
|
||||
|
||||
static void setup_reticulum() {
|
||||
microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
|
||||
filesystem.init();
|
||||
RNS::Utilities::OS::register_filesystem(filesystem);
|
||||
|
||||
ble_impl = new TBeamSupremeBleInterface();
|
||||
ble_interface = ble_impl;
|
||||
ble_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
|
||||
RNS::Transport::register_interface(ble_interface);
|
||||
ble_interface.start();
|
||||
|
||||
reticulum = RNS::Reticulum();
|
||||
reticulum.transport_enabled(false);
|
||||
reticulum.probe_destination_enabled(false);
|
||||
reticulum.start();
|
||||
|
||||
local_identity = RNS::Identity();
|
||||
inbound_destination = RNS::Destination(local_identity,
|
||||
RNS::Type::Destination::IN,
|
||||
RNS::Type::Destination::SINGLE,
|
||||
APP_NAME,
|
||||
APP_ASPECT);
|
||||
inbound_destination.set_link_established_callback(on_inbound_link_established);
|
||||
inbound_destination.set_proof_strategy(RNS::Type::Destination::PROVE_NONE);
|
||||
|
||||
RNS::Transport::register_announce_handler(announce_handler);
|
||||
|
||||
Serial.printf("Local SINGLE destination: %s\r\n",
|
||||
inbound_destination.hash().toHex().c_str());
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial && millis() < 5000) {
|
||||
delay(100);
|
||||
}
|
||||
delay(250);
|
||||
|
||||
Serial.println();
|
||||
RNS::loglevel(RNS::LOG_NOTICE);
|
||||
setup_reticulum();
|
||||
Serial.println("Exercise 303: microReticulum BLE negotiated Link ping-pong");
|
||||
print_config();
|
||||
Serial.println("microReticulum ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
reticulum.loop();
|
||||
|
||||
static uint32_t next_announce_ms = 0;
|
||||
static uint32_t next_link_tx_ms = 0;
|
||||
static uint32_t next_wait_log_ms = 0;
|
||||
static uint32_t iter = 0;
|
||||
uint32_t now = millis();
|
||||
|
||||
if (ble_impl && !ble_impl->connected()) {
|
||||
if (next_wait_log_ms == 0 || (int32_t)(now - next_wait_log_ms) >= 0) {
|
||||
next_wait_log_ms = now + 10000;
|
||||
Serial.printf("BLE %s waiting for peer\r\n", ble_impl->role_name());
|
||||
}
|
||||
delay(5);
|
||||
return;
|
||||
}
|
||||
|
||||
if (next_announce_ms == 0) {
|
||||
uint32_t offset = (NODE_LABEL[0] == 'A') ? 700 : 1700;
|
||||
next_announce_ms = now + offset;
|
||||
}
|
||||
if (!link_active && (int32_t)(now - next_announce_ms) >= 0) {
|
||||
next_announce_ms = now + 15000;
|
||||
send_announce();
|
||||
}
|
||||
|
||||
maybe_open_link();
|
||||
|
||||
if (link_active && next_link_tx_ms == 0) {
|
||||
uint32_t offset = should_initiate_link_to(peer_label) ? 1200 : 2400;
|
||||
next_link_tx_ms = now + offset;
|
||||
}
|
||||
if (link_active && (int32_t)(now - next_link_tx_ms) >= 0) {
|
||||
next_link_tx_ms = now + 3000;
|
||||
|
||||
String message = String(NODE_LABEL) + " -> " + peer_label + " iter=" + String(iter++);
|
||||
Serial.printf("TX LINK BLE: %s\r\n", message.c_str());
|
||||
RNS::Packet(active_link, RNS::bytesFromString(message.c_str())).send();
|
||||
}
|
||||
|
||||
delay(5);
|
||||
}
|
||||
|
||||
int _write(int file, char* ptr, int len) {
|
||||
(void)file;
|
||||
int wrote = Serial.write(ptr, len);
|
||||
Serial.flush();
|
||||
return wrote;
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# Exercise 304: microReticulum BLE dual-role Link ping-pong
|
||||
|
||||
This exercise keeps the Reticulum behavior from Exercise 303, but removes the fixed BLE central/peripheral split.
|
||||
|
||||
Every board runs the same firmware image. Each node starts both sides of the ESP32 BLE interface:
|
||||
|
||||
```text
|
||||
BLE advertiser/server + BLE scanner/client
|
||||
```
|
||||
|
||||
The nodes advertise a Reticulum BLE service name derived from the full ESP32 MAC, such as `RNS-Node-48CA435A93DD`. When two nodes see each other, the lower BLE address initiates the BLE GATT client connection and the other node remains available as the server. This deterministic tie-breaker avoids both boards trying to connect to each other at the same time.
|
||||
|
||||
After the BLE connection is established, the Reticulum flow is the same as Exercise 303:
|
||||
|
||||
```text
|
||||
BLE GATT connection -> Identity -> SINGLE destination -> announce -> peer learns identity/path -> LINKREQUEST/LRPROOF handshake -> ACTIVE Link -> link DATA packets
|
||||
```
|
||||
|
||||
Startup order should not matter for this exercise. Either board can be reset first.
|
||||
|
||||
## Build, Upload, And Monitor
|
||||
|
||||
Use the neutral `tbeam` environment when you want the exact same compiled firmware image on both boards. The board identity is derived at runtime from the ESP32 MAC address.
|
||||
|
||||
In one console:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/304_microReticulum_ble_dual_role_ping_pong -e tbeam -t upload --upload-port /dev/ttytAMY && \
|
||||
pio device monitor -p /dev/ttytAMY -b 115200
|
||||
```
|
||||
|
||||
In another console:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/304_microReticulum_ble_dual_role_ping_pong -e tbeam -t upload --upload-port /dev/ttytBOB && \
|
||||
pio device monitor -p /dev/ttytBOB -b 115200
|
||||
```
|
||||
|
||||
The `amy`, `bob`, `cy`, `dan`, and `ed` environments remain as upload/monitor aliases. They do not define BLE role or node label, but separate PlatformIO environments can still produce different `.bin` hashes because the environment path/name can appear in compiled artifacts. For strict same-binary testing, use `-e tbeam` for every board.
|
||||
|
||||
The shorter alias form should also work once the symlinks exist:
|
||||
|
||||
```bash
|
||||
pio run -d exercises/304_microReticulum_ble_dual_role_ping_pong -e amy -t upload
|
||||
pio device monitor -d exercises/304_microReticulum_ble_dual_role_ping_pong -e amy
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
Each board derives a node label from its MAC and starts dual-role BLE:
|
||||
|
||||
```text
|
||||
Node=Node-48CA435A93DD
|
||||
BLE role=dual-role service=37145b00-442d-4a94-917f-8f42c5da28e3
|
||||
BLE dual-role: local addr=48:ca:43:5a:93:dd label=Node-48CA435A93DD
|
||||
BLE dual-role: starting advertiser and scanner
|
||||
```
|
||||
|
||||
One side will decide it should initiate the BLE connection:
|
||||
|
||||
```text
|
||||
BLE dual-role: peer candidate found: 48:ca:43:5b:bf:69 label=Node-48CA435BBF69
|
||||
BLE dual-role: connected and identity handshake sent
|
||||
```
|
||||
|
||||
The other side remains the server and accepts it:
|
||||
|
||||
```text
|
||||
BLE dual-role: central connected to local server
|
||||
BLE dual-role: identity handshake received by local server
|
||||
```
|
||||
|
||||
Then Reticulum announces, opens the negotiated Link, and exchanges payloads:
|
||||
|
||||
```text
|
||||
TX ANNOUNCE: Node-48CA435A93DD
|
||||
RX ANNOUNCE: label=Node-48CA435BBF69 hash=...
|
||||
TX LINKREQUEST: opening link to Node-48CA435BBF69
|
||||
LINK ACTIVE: initiator link established
|
||||
TX LINK BLE: Node-48CA435A93DD -> Node-48CA435BBF69 iter=0
|
||||
RX LINK BLE: Node-48CA435BBF69 -> Node-48CA435A93DD iter=0
|
||||
```
|
||||

|
||||
## Notes
|
||||
|
||||
- This is the first equal-peer BLE transport exercise in the 300 series.
|
||||
- Equal-peer means equal at the BLE transport startup layer. The BLE interface uses address ordering to decide which side initiates the GATT connection, and Reticulum still uses label ordering to decide which node opens the first Link.
|
||||
- The next natural exercise is simultaneous text transfer over this same dual-role Link foundation, with the text corpus selected from `platformio.ini`.
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
; Exercise 304: microReticulum BLE dual-role Link ping-pong
|
||||
|
||||
[platformio]
|
||||
default_envs = tbeam
|
||||
|
||||
[env]
|
||||
platform = espressif32
|
||||
framework = arduino
|
||||
board = esp32-s3-devkitc-1
|
||||
monitor_speed = 115200
|
||||
upload_speed = 460800
|
||||
board_build.partitions = huge_app.csv
|
||||
|
||||
build_flags =
|
||||
-Wall
|
||||
-Wno-missing-field-initializers
|
||||
-Wno-format
|
||||
-D RNS_USE_FS
|
||||
-D RNS_PERSIST_PATHS
|
||||
-D USTORE_USE_UNIVERSALFS
|
||||
-D MSGPACK_USE_BOOST=OFF
|
||||
-D MCU_ESP32
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
lib_deps =
|
||||
ArduinoJson@^7.4.2
|
||||
MsgPack@^0.4.2
|
||||
https://github.com/attermann/Crypto.git
|
||||
https://github.com/attermann/microStore.git
|
||||
microReticulum=symlink:///usr/local/src/microreticulum/microReticulum
|
||||
|
||||
[env:tbeam]
|
||||
extends = env
|
||||
|
||||
[env:amy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytAMY
|
||||
monitor_port = /dev/ttytAMY
|
||||
|
||||
[env:bob]
|
||||
extends = env
|
||||
upload_port = /dev/ttytBOB
|
||||
monitor_port = /dev/ttytBOB
|
||||
|
||||
[env:cy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytCY
|
||||
monitor_port = /dev/ttytCY
|
||||
|
||||
[env:dan]
|
||||
extends = env
|
||||
upload_port = /dev/ttytDAN
|
||||
monitor_port = /dev/ttytDAN
|
||||
|
||||
[env:ed]
|
||||
extends = env
|
||||
upload_port = /dev/ttytED
|
||||
monitor_port = /dev/ttytED
|
||||
|
|
@ -0,0 +1,438 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
|
||||
#include <BLE2902.h>
|
||||
#include <BLEService.h>
|
||||
#include <Cryptography/Random.h>
|
||||
#include <Identity.h>
|
||||
#include <Log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string.h>
|
||||
|
||||
using namespace RNS;
|
||||
|
||||
static TBeamSupremeBleInterface* active_ble_interface = nullptr;
|
||||
|
||||
class TBeamSupremeBleInterface::ServerCallbacks : public BLEServerCallbacks {
|
||||
public:
|
||||
explicit ServerCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onConnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
if (_owner->_client && _owner->_client->isConnected()) {
|
||||
Serial.println("BLE dual-role: inbound connection arrived while outbound is active; keeping outbound link");
|
||||
server->disconnect(server->getConnId());
|
||||
return;
|
||||
}
|
||||
_owner->_connected = true;
|
||||
_owner->_server_handshake_received = false;
|
||||
Serial.println("BLE dual-role: central connected to local server");
|
||||
INFO("BLE central connected to local server");
|
||||
}
|
||||
|
||||
void onDisconnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
_owner->_connected = false;
|
||||
_owner->_server_handshake_received = false;
|
||||
_owner->reset_reassembly();
|
||||
Serial.println("BLE dual-role: central disconnected; restarting advertising");
|
||||
INFO("BLE central disconnected; restarting advertising");
|
||||
BLEDevice::startAdvertising();
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::RxCallbacks : public BLECharacteristicCallbacks {
|
||||
public:
|
||||
explicit RxCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onWrite(BLECharacteristic* characteristic) override {
|
||||
std::string value = characteristic->getValue();
|
||||
if (value.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_owner->_server_handshake_received && value.size() == 16) {
|
||||
_owner->_server_handshake_received = true;
|
||||
Serial.println("BLE dual-role: identity handshake received by local server");
|
||||
INFOF("BLE identity handshake received: %d bytes", (int)value.size());
|
||||
return;
|
||||
}
|
||||
|
||||
_owner->handle_fragment(reinterpret_cast<const uint8_t*>(value.data()), value.size());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::AdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
|
||||
public:
|
||||
explicit AdvertisedDeviceCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onResult(BLEAdvertisedDevice advertised_device) override {
|
||||
bool has_service = advertised_device.haveServiceUUID() &&
|
||||
advertised_device.isAdvertisingService(BLEUUID(TBeamSupremeBleInterface::SERVICE_UUID));
|
||||
bool has_rns_name = advertised_device.haveName() &&
|
||||
advertised_device.getName().rfind("RNS-", 0) == 0;
|
||||
|
||||
if ((has_service || has_rns_name) || _owner->_scan_report_count < 5) {
|
||||
Serial.printf("BLE scan: addr=%s name=%s service=%s rssi=%d\r\n",
|
||||
advertised_device.getAddress().toString().c_str(),
|
||||
advertised_device.haveName() ? advertised_device.getName().c_str() : "",
|
||||
has_service ? "yes" : "no",
|
||||
advertised_device.getRSSI());
|
||||
_owner->_scan_report_count++;
|
||||
}
|
||||
|
||||
if (!has_service && !has_rns_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
String peer_label = _owner->advertised_node_label(advertised_device);
|
||||
String peer_address = String(advertised_device.getAddress().toString().c_str());
|
||||
if (!_owner->should_connect_to_peer(peer_address)) {
|
||||
Serial.printf("BLE dual-role: peer addr=%s label=%s has lower tie-breaker; staying available\r\n",
|
||||
peer_address.c_str(),
|
||||
peer_label.length() ? peer_label.c_str() : "(unknown)");
|
||||
return;
|
||||
}
|
||||
|
||||
BLEDevice::getScan()->stop();
|
||||
delete _owner->_advertised_device;
|
||||
_owner->_advertised_device = new BLEAdvertisedDevice(advertised_device);
|
||||
_owner->_do_connect = true;
|
||||
_owner->_scanning = false;
|
||||
Serial.printf("BLE dual-role: peer candidate found: %s label=%s\r\n",
|
||||
advertised_device.getAddress().toString().c_str(),
|
||||
peer_label.c_str());
|
||||
INFOF("BLE peer found: %s", advertised_device.getAddress().toString().c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
TBeamSupremeBleInterface::TBeamSupremeBleInterface(const String& node_label, const char* name) : InterfaceImpl(name) {
|
||||
_node_label = node_label;
|
||||
_IN = true;
|
||||
_OUT = true;
|
||||
_bitrate = 1000000;
|
||||
_HW_MTU = BLE_PAYLOAD_SIZE;
|
||||
}
|
||||
|
||||
TBeamSupremeBleInterface::~TBeamSupremeBleInterface() {
|
||||
stop();
|
||||
delete _advertised_device;
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::start() {
|
||||
if (_started) {
|
||||
return true;
|
||||
}
|
||||
|
||||
active_ble_interface = this;
|
||||
BLEDevice::init(std::string("RNS-") + _node_label.c_str());
|
||||
BLEDevice::setMTU(BLE_MTU);
|
||||
_local_address = String(BLEDevice::getAddress().toString().c_str());
|
||||
|
||||
Serial.printf("BLE dual-role: local addr=%s label=%s\r\n", _local_address.c_str(), _node_label.c_str());
|
||||
Serial.println("BLE dual-role: starting advertiser and scanner");
|
||||
INFO("BLE starting as dual-role client/server");
|
||||
start_peripheral();
|
||||
start_central_scan();
|
||||
|
||||
_started = true;
|
||||
_online = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::stop() {
|
||||
_online = false;
|
||||
_connected = false;
|
||||
if (_client && _client->isConnected()) {
|
||||
_client->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::loop() {
|
||||
if (!_online) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_do_connect && _advertised_device) {
|
||||
_do_connect = false;
|
||||
if (!connect_to_advertised_device(_advertised_device)) {
|
||||
_connected = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_connected && !_scanning && (int32_t)(millis() - _next_scan_ms) >= 0) {
|
||||
start_central_scan();
|
||||
}
|
||||
|
||||
if (_reassembly_started_ms != 0 && millis() - _reassembly_started_ms > REASSEMBLY_TIMEOUT_MS) {
|
||||
WARNING("BLE reassembly timeout; dropping partial packet");
|
||||
reset_reassembly();
|
||||
}
|
||||
|
||||
RNS::Bytes packet({RNS::Type::NONE});
|
||||
while (dequeue_packet(packet)) {
|
||||
InterfaceImpl::handle_incoming(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_peripheral() {
|
||||
_server = BLEDevice::createServer();
|
||||
_server->setCallbacks(new ServerCallbacks(this));
|
||||
|
||||
BLEService* service = _server->createService(SERVICE_UUID);
|
||||
_tx_characteristic = service->createCharacteristic(
|
||||
TX_UUID,
|
||||
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
_tx_characteristic->addDescriptor(new BLE2902());
|
||||
|
||||
BLECharacteristic* rx_characteristic = service->createCharacteristic(
|
||||
RX_UUID,
|
||||
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
|
||||
rx_characteristic->setCallbacks(new RxCallbacks(this));
|
||||
|
||||
_identity_characteristic = service->createCharacteristic(
|
||||
IDENTITY_UUID,
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_identity_characteristic->setValue((uint8_t*)identity.data(), identity.size());
|
||||
|
||||
service->start();
|
||||
|
||||
BLEAdvertising* advertising = BLEDevice::getAdvertising();
|
||||
advertising->addServiceUUID(SERVICE_UUID);
|
||||
advertising->setScanResponse(true);
|
||||
advertising->setMinPreferred(0x00);
|
||||
BLEDevice::startAdvertising();
|
||||
Serial.println("BLE dual-role: advertising Reticulum service");
|
||||
INFO("BLE advertising Reticulum service");
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_central_scan() {
|
||||
_scanning = true;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
_scan_report_count = 0;
|
||||
|
||||
BLEScan* scan = BLEDevice::getScan();
|
||||
scan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks(this), true);
|
||||
scan->setInterval(1349);
|
||||
scan->setWindow(449);
|
||||
scan->setActiveScan(true);
|
||||
Serial.println("BLE dual-role: scanning for Reticulum service");
|
||||
scan->start(5, false);
|
||||
if (!_do_connect && !_connected) {
|
||||
_scanning = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
Serial.println("BLE dual-role: scan complete; no eligible peer found");
|
||||
}
|
||||
scan->clearResults();
|
||||
INFO("BLE scanning for Reticulum service");
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::connect_to_advertised_device(BLEAdvertisedDevice* device) {
|
||||
Serial.printf("BLE dual-role: connecting to %s\r\n", device->getAddress().toString().c_str());
|
||||
INFOF("BLE connecting to %s", device->getAddress().toString().c_str());
|
||||
|
||||
_client = BLEDevice::createClient();
|
||||
if (!_client->connect(device)) {
|
||||
Serial.println("BLE dual-role: connect failed");
|
||||
WARNING("BLE connect failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
_client->setMTU(BLE_MTU);
|
||||
BLERemoteService* service = _client->getService(BLEUUID(SERVICE_UUID));
|
||||
if (!service) {
|
||||
Serial.println("BLE dual-role: Reticulum service not found after connect");
|
||||
WARNING("BLE Reticulum service not found after connect");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
_remote_rx_characteristic = service->getCharacteristic(BLEUUID(RX_UUID));
|
||||
_remote_tx_characteristic = service->getCharacteristic(BLEUUID(TX_UUID));
|
||||
BLERemoteCharacteristic* identity_characteristic = service->getCharacteristic(BLEUUID(IDENTITY_UUID));
|
||||
|
||||
if (!_remote_rx_characteristic || !_remote_tx_characteristic) {
|
||||
Serial.println("BLE dual-role: RX/TX characteristics not found");
|
||||
WARNING("BLE Reticulum RX/TX characteristics not found");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (identity_characteristic && identity_characteristic->canRead()) {
|
||||
std::string peer_identity = identity_characteristic->readValue();
|
||||
Serial.printf("BLE dual-role: peer identity read: %d bytes\r\n", (int)peer_identity.size());
|
||||
INFOF("BLE peer identity read: %d bytes", (int)peer_identity.size());
|
||||
}
|
||||
|
||||
if (_remote_tx_characteristic->canNotify()) {
|
||||
_remote_tx_characteristic->registerForNotify(client_notify_callback);
|
||||
} else {
|
||||
Serial.println("BLE dual-role: peer TX characteristic does not notify");
|
||||
}
|
||||
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)identity.data(), identity.size(), false);
|
||||
_connected = true;
|
||||
_scanning = false;
|
||||
Serial.println("BLE dual-role: connected and identity handshake sent");
|
||||
INFO("BLE connected and identity handshake sent");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::should_connect_to_peer(const String& peer_address) const {
|
||||
if (peer_address.length() == 0 || peer_address == _local_address) {
|
||||
return false;
|
||||
}
|
||||
return strcmp(_local_address.c_str(), peer_address.c_str()) < 0;
|
||||
}
|
||||
|
||||
String TBeamSupremeBleInterface::advertised_node_label(BLEAdvertisedDevice& device) const {
|
||||
if (!device.haveName()) {
|
||||
return "";
|
||||
}
|
||||
std::string name = device.getName();
|
||||
if (name.rfind("RNS-", 0) != 0) {
|
||||
return "";
|
||||
}
|
||||
return String(name.substr(4).c_str());
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_outgoing(const Bytes& data) {
|
||||
if (!_online || !_connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
|
||||
if (total == 0 || total > 65535) {
|
||||
WARNINGF("BLE cannot fragment packet of size %d", (int)data.size());
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
uint8_t fragment[BLE_MTU];
|
||||
uint8_t fragment_type = FRAG_CONTINUE;
|
||||
if (i == 0) {
|
||||
fragment_type = FRAG_START;
|
||||
} else if (i == total - 1) {
|
||||
fragment_type = FRAG_END;
|
||||
}
|
||||
|
||||
size_t offset = i * BLE_PAYLOAD_SIZE;
|
||||
size_t chunk = std::min(BLE_PAYLOAD_SIZE, data.size() - offset);
|
||||
fragment[0] = fragment_type;
|
||||
fragment[1] = (uint8_t)((i >> 8) & 0xFF);
|
||||
fragment[2] = (uint8_t)(i & 0xFF);
|
||||
fragment[3] = (uint8_t)((total >> 8) & 0xFF);
|
||||
fragment[4] = (uint8_t)(total & 0xFF);
|
||||
memcpy(fragment + FRAG_HEADER_SIZE, data.data() + offset, chunk);
|
||||
send_fragment(fragment, FRAG_HEADER_SIZE + chunk);
|
||||
delay(8);
|
||||
}
|
||||
|
||||
InterfaceImpl::handle_outgoing(data);
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_fragment(const uint8_t* data, size_t len) {
|
||||
if (_client && _client->isConnected() && _remote_rx_characteristic) {
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)data, len, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_tx_characteristic) {
|
||||
_tx_characteristic->setValue((uint8_t*)data, len);
|
||||
_tx_characteristic->notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::handle_fragment(const uint8_t* data, size_t len) {
|
||||
if (len < FRAG_HEADER_SIZE) {
|
||||
WARNINGF("BLE fragment too short: %d", (int)len);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fragment_type = data[0];
|
||||
uint16_t sequence = ((uint16_t)data[1] << 8) | data[2];
|
||||
uint16_t total = ((uint16_t)data[3] << 8) | data[4];
|
||||
const uint8_t* payload = data + FRAG_HEADER_SIZE;
|
||||
size_t payload_len = len - FRAG_HEADER_SIZE;
|
||||
|
||||
if ((fragment_type != FRAG_START && fragment_type != FRAG_CONTINUE && fragment_type != FRAG_END) ||
|
||||
total == 0 || sequence >= total) {
|
||||
WARNING("BLE invalid fragment header");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sequence == 0) {
|
||||
reset_reassembly();
|
||||
_expected_total = total;
|
||||
_received_fragments = 0;
|
||||
_reassembly_started_ms = millis();
|
||||
}
|
||||
|
||||
if (_expected_total != total || sequence != _received_fragments) {
|
||||
WARNING("BLE out-of-order fragment; dropping partial packet");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
_reassembly_buffer.append(payload, payload_len);
|
||||
_received_fragments++;
|
||||
|
||||
if (_received_fragments == _expected_total) {
|
||||
enqueue_packet(_reassembly_buffer);
|
||||
reset_reassembly();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::enqueue_packet(const RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
_incoming_packets.push_back(packet);
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::dequeue_packet(RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
if (_incoming_packets.empty()) {
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return false;
|
||||
}
|
||||
packet = _incoming_packets.front();
|
||||
_incoming_packets.pop_front();
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::reset_reassembly() {
|
||||
_reassembly_buffer.clear();
|
||||
_expected_total = 0;
|
||||
_received_fragments = 0;
|
||||
_reassembly_started_ms = 0;
|
||||
}
|
||||
|
||||
RNS::Bytes TBeamSupremeBleInterface::local_identity_hash() const {
|
||||
String material = String("microReticulum BLE ") + _node_label;
|
||||
return Identity::truncated_hash(RNS::bytesFromString(material.c_str()));
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify) {
|
||||
(void)characteristic;
|
||||
(void)is_notify;
|
||||
if (active_ble_interface) {
|
||||
active_ble_interface->handle_fragment(data, length);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
#pragma once
|
||||
|
||||
#include <Bytes.h>
|
||||
#include <Interface.h>
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BLEAdvertisedDevice.h>
|
||||
#include <BLEClient.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLERemoteCharacteristic.h>
|
||||
#include <BLEScan.h>
|
||||
#include <BLEServer.h>
|
||||
|
||||
#include <deque>
|
||||
|
||||
class TBeamSupremeBleInterface : public RNS::InterfaceImpl {
|
||||
public:
|
||||
explicit TBeamSupremeBleInterface(const String& node_label,
|
||||
const char* name = "TBeamSupremeBLE");
|
||||
~TBeamSupremeBleInterface() override;
|
||||
|
||||
bool start() override;
|
||||
void stop() override;
|
||||
void loop() override;
|
||||
|
||||
bool connected() const { return _connected; }
|
||||
const char* role_name() const { return "dual-role"; }
|
||||
|
||||
private:
|
||||
void send_outgoing(const RNS::Bytes& data) override;
|
||||
|
||||
void start_peripheral();
|
||||
void start_central_scan();
|
||||
bool connect_to_advertised_device(BLEAdvertisedDevice* device);
|
||||
bool should_connect_to_peer(const String& peer_address) const;
|
||||
String advertised_node_label(BLEAdvertisedDevice& device) const;
|
||||
|
||||
void send_fragment(const uint8_t* data, size_t len);
|
||||
void handle_fragment(const uint8_t* data, size_t len);
|
||||
void enqueue_packet(const RNS::Bytes& packet);
|
||||
bool dequeue_packet(RNS::Bytes& packet);
|
||||
void reset_reassembly();
|
||||
RNS::Bytes local_identity_hash() const;
|
||||
|
||||
static void client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify);
|
||||
|
||||
class ServerCallbacks;
|
||||
class RxCallbacks;
|
||||
class AdvertisedDeviceCallbacks;
|
||||
|
||||
static constexpr const char* SERVICE_UUID = "37145b00-442d-4a94-917f-8f42c5da28e3";
|
||||
static constexpr const char* TX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e4";
|
||||
static constexpr const char* RX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e5";
|
||||
static constexpr const char* IDENTITY_UUID = "37145b00-442d-4a94-917f-8f42c5da28e6";
|
||||
|
||||
static constexpr uint8_t FRAG_START = 0x01;
|
||||
static constexpr uint8_t FRAG_CONTINUE = 0x02;
|
||||
static constexpr uint8_t FRAG_END = 0x03;
|
||||
static constexpr size_t FRAG_HEADER_SIZE = 5;
|
||||
static constexpr size_t BLE_MTU = 185;
|
||||
static constexpr size_t BLE_PAYLOAD_SIZE = BLE_MTU - FRAG_HEADER_SIZE;
|
||||
static constexpr uint32_t REASSEMBLY_TIMEOUT_MS = 30000;
|
||||
static constexpr uint32_t SCAN_RETRY_MS = 5000;
|
||||
|
||||
String _node_label;
|
||||
String _local_address;
|
||||
bool _started = false;
|
||||
bool _connected = false;
|
||||
bool _do_connect = false;
|
||||
bool _scanning = false;
|
||||
bool _server_handshake_received = false;
|
||||
uint8_t _scan_report_count = 0;
|
||||
uint32_t _next_scan_ms = 0;
|
||||
|
||||
BLEServer* _server = nullptr;
|
||||
BLECharacteristic* _tx_characteristic = nullptr;
|
||||
BLECharacteristic* _identity_characteristic = nullptr;
|
||||
BLEAdvertisedDevice* _advertised_device = nullptr;
|
||||
BLEClient* _client = nullptr;
|
||||
BLERemoteCharacteristic* _remote_rx_characteristic = nullptr;
|
||||
BLERemoteCharacteristic* _remote_tx_characteristic = nullptr;
|
||||
|
||||
RNS::Bytes _reassembly_buffer;
|
||||
uint16_t _expected_total = 0;
|
||||
uint16_t _received_fragments = 0;
|
||||
uint32_t _reassembly_started_ms = 0;
|
||||
|
||||
std::deque<RNS::Bytes> _incoming_packets;
|
||||
portMUX_TYPE _queue_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
};
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Destination.h>
|
||||
#include <Identity.h>
|
||||
#include <Link.h>
|
||||
#include <Log.h>
|
||||
#include <Packet.h>
|
||||
#include <Reticulum.h>
|
||||
#include <Transport.h>
|
||||
#include <Type.h>
|
||||
#include <Utilities/OS.h>
|
||||
#include <microStore/Adapters/UniversalFileSystem.h>
|
||||
#include <microStore/FileSystem.h>
|
||||
|
||||
static constexpr const char* APP_NAME = "microreticulum";
|
||||
static constexpr const char* APP_ASPECT = "linkping";
|
||||
static constexpr const char* ANNOUNCE_FILTER = "microreticulum.linkping";
|
||||
|
||||
static RNS::Reticulum reticulum({RNS::Type::NONE});
|
||||
static RNS::Interface ble_interface({RNS::Type::NONE});
|
||||
static RNS::Identity local_identity({RNS::Type::NONE});
|
||||
static RNS::Destination inbound_destination({RNS::Type::NONE});
|
||||
static RNS::Destination peer_destination({RNS::Type::NONE});
|
||||
static RNS::Link active_link({RNS::Type::NONE});
|
||||
static RNS::Link pending_link({RNS::Type::NONE});
|
||||
static RNS::Bytes peer_hash;
|
||||
static String peer_label;
|
||||
static bool have_peer = false;
|
||||
static bool link_active = false;
|
||||
static bool link_attempted = false;
|
||||
static TBeamSupremeBleInterface* ble_impl = nullptr;
|
||||
static String node_label;
|
||||
|
||||
static String default_node_label() {
|
||||
uint64_t mac = ESP.getEfuseMac();
|
||||
char label[24];
|
||||
snprintf(label, sizeof(label), "Node-%012llX", (unsigned long long)(mac & 0xFFFFFFFFFFFFULL));
|
||||
return String(label);
|
||||
}
|
||||
|
||||
static bool should_initiate_link_to(const String& label) {
|
||||
return strcmp(node_label.c_str(), label.c_str()) < 0;
|
||||
}
|
||||
|
||||
static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
|
||||
(void)packet;
|
||||
Serial.printf("RX LINK BLE: %s\r\n", data.toString().c_str());
|
||||
}
|
||||
|
||||
static void on_link_closed(RNS::Link& link) {
|
||||
(void)link;
|
||||
Serial.println("LINK CLOSED");
|
||||
active_link = {RNS::Type::NONE};
|
||||
pending_link = {RNS::Type::NONE};
|
||||
link_active = false;
|
||||
link_attempted = false;
|
||||
}
|
||||
|
||||
static void on_outbound_link_established(RNS::Link& link) {
|
||||
active_link = link;
|
||||
active_link.set_packet_callback(on_link_packet);
|
||||
active_link.set_link_closed_callback(on_link_closed);
|
||||
link_active = true;
|
||||
Serial.println("LINK ACTIVE: initiator link established");
|
||||
}
|
||||
|
||||
static void on_inbound_link_established(RNS::Link& link) {
|
||||
active_link = link;
|
||||
active_link.set_packet_callback(on_link_packet);
|
||||
active_link.set_link_closed_callback(on_link_closed);
|
||||
link_active = true;
|
||||
link_attempted = true;
|
||||
Serial.println("RX LINK: inbound link established");
|
||||
}
|
||||
|
||||
class LinkAnnounceHandler : public RNS::AnnounceHandler {
|
||||
public:
|
||||
LinkAnnounceHandler() : RNS::AnnounceHandler(ANNOUNCE_FILTER) {}
|
||||
|
||||
void received_announce(const RNS::Bytes& destination_hash,
|
||||
const RNS::Identity& announced_identity,
|
||||
const RNS::Bytes& app_data) override {
|
||||
String label = app_data ? String(app_data.toString().c_str()) : String("(no label)");
|
||||
if (label == node_label) {
|
||||
return;
|
||||
}
|
||||
if (!announced_identity) {
|
||||
Serial.printf("RX ANNOUNCE ignored: missing identity for hash=%s\r\n",
|
||||
destination_hash.toHex().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
peer_hash = destination_hash;
|
||||
peer_label = label;
|
||||
peer_destination = RNS::Destination(announced_identity,
|
||||
RNS::Type::Destination::OUT,
|
||||
RNS::Type::Destination::SINGLE,
|
||||
destination_hash);
|
||||
have_peer = true;
|
||||
|
||||
Serial.printf("RX ANNOUNCE: label=%s hash=%s\r\n",
|
||||
peer_label.c_str(), peer_hash.toHex().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
static RNS::HAnnounceHandler announce_handler(new LinkAnnounceHandler());
|
||||
|
||||
static void print_config() {
|
||||
Serial.printf("Node=%s\r\n", node_label.c_str());
|
||||
Serial.printf("BLE role=%s service=37145b00-442d-4a94-917f-8f42c5da28e3\r\n",
|
||||
ble_impl ? ble_impl->role_name() : "?");
|
||||
}
|
||||
|
||||
static void send_announce() {
|
||||
if (!inbound_destination) {
|
||||
return;
|
||||
}
|
||||
Serial.printf("TX ANNOUNCE: %s\r\n", node_label.c_str());
|
||||
inbound_destination.announce(RNS::bytesFromString(node_label.c_str()));
|
||||
}
|
||||
|
||||
static void maybe_open_link() {
|
||||
if (!have_peer || link_active || link_attempted || !peer_destination) {
|
||||
return;
|
||||
}
|
||||
if (!should_initiate_link_to(peer_label)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("TX LINKREQUEST: opening link to %s\r\n", peer_label.c_str());
|
||||
pending_link = RNS::Link(peer_destination);
|
||||
pending_link.set_packet_callback(on_link_packet);
|
||||
pending_link.set_link_established_callback(on_outbound_link_established);
|
||||
pending_link.set_link_closed_callback(on_link_closed);
|
||||
link_attempted = true;
|
||||
}
|
||||
|
||||
static void setup_reticulum() {
|
||||
microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
|
||||
filesystem.init();
|
||||
RNS::Utilities::OS::register_filesystem(filesystem);
|
||||
|
||||
ble_impl = new TBeamSupremeBleInterface(node_label);
|
||||
ble_interface = ble_impl;
|
||||
ble_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
|
||||
RNS::Transport::register_interface(ble_interface);
|
||||
ble_interface.start();
|
||||
|
||||
reticulum = RNS::Reticulum();
|
||||
reticulum.transport_enabled(false);
|
||||
reticulum.probe_destination_enabled(false);
|
||||
reticulum.start();
|
||||
|
||||
local_identity = RNS::Identity();
|
||||
inbound_destination = RNS::Destination(local_identity,
|
||||
RNS::Type::Destination::IN,
|
||||
RNS::Type::Destination::SINGLE,
|
||||
APP_NAME,
|
||||
APP_ASPECT);
|
||||
inbound_destination.set_link_established_callback(on_inbound_link_established);
|
||||
inbound_destination.set_proof_strategy(RNS::Type::Destination::PROVE_NONE);
|
||||
|
||||
RNS::Transport::register_announce_handler(announce_handler);
|
||||
|
||||
Serial.printf("Local SINGLE destination: %s\r\n",
|
||||
inbound_destination.hash().toHex().c_str());
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial && millis() < 5000) {
|
||||
delay(100);
|
||||
}
|
||||
delay(250);
|
||||
|
||||
Serial.println();
|
||||
RNS::loglevel(RNS::LOG_NOTICE);
|
||||
node_label = default_node_label();
|
||||
setup_reticulum();
|
||||
Serial.println("Exercise 304: microReticulum BLE dual-role Link ping-pong");
|
||||
print_config();
|
||||
Serial.println("microReticulum ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
reticulum.loop();
|
||||
|
||||
static uint32_t next_announce_ms = 0;
|
||||
static uint32_t next_link_tx_ms = 0;
|
||||
static uint32_t next_wait_log_ms = 0;
|
||||
static uint32_t iter = 0;
|
||||
uint32_t now = millis();
|
||||
|
||||
if (ble_impl && !ble_impl->connected()) {
|
||||
if (next_wait_log_ms == 0 || (int32_t)(now - next_wait_log_ms) >= 0) {
|
||||
next_wait_log_ms = now + 10000;
|
||||
Serial.printf("BLE %s waiting for peer\r\n", ble_impl->role_name());
|
||||
}
|
||||
delay(5);
|
||||
return;
|
||||
}
|
||||
|
||||
if (next_announce_ms == 0) {
|
||||
uint32_t offset = 700 + ((uint32_t)node_label.charAt(node_label.length() - 1) % 5) * 400;
|
||||
next_announce_ms = now + offset;
|
||||
}
|
||||
if (!link_active && (int32_t)(now - next_announce_ms) >= 0) {
|
||||
next_announce_ms = now + 15000;
|
||||
send_announce();
|
||||
}
|
||||
|
||||
maybe_open_link();
|
||||
|
||||
if (link_active && next_link_tx_ms == 0) {
|
||||
uint32_t offset = should_initiate_link_to(peer_label) ? 1200 : 2400;
|
||||
next_link_tx_ms = now + offset;
|
||||
}
|
||||
if (link_active && (int32_t)(now - next_link_tx_ms) >= 0) {
|
||||
next_link_tx_ms = now + 3000;
|
||||
|
||||
String message = node_label + " -> " + peer_label + " iter=" + String(iter++);
|
||||
Serial.printf("TX LINK BLE: %s\r\n", message.c_str());
|
||||
RNS::Packet(active_link, RNS::bytesFromString(message.c_str())).send();
|
||||
}
|
||||
|
||||
delay(5);
|
||||
}
|
||||
|
||||
int _write(int file, char* ptr, int len) {
|
||||
(void)file;
|
||||
int wrote = Serial.write(ptr, len);
|
||||
Serial.flush();
|
||||
return wrote;
|
||||
}
|
||||
BIN
img/20260519_135748_Tue.png
Normal file
BIN
img/20260519_135748_Tue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 319 KiB |
BIN
img/20260519_141153_Tue.png
Normal file
BIN
img/20260519_141153_Tue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 313 KiB |
BIN
img/20260519_141935_Tue.png
Normal file
BIN
img/20260519_141935_Tue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 373 KiB |
BIN
img/20260519_144250_Tue.png
Normal file
BIN
img/20260519_144250_Tue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
Loading…
Add table
Add a link
Reference in a new issue