mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-27 21:19:56 +00:00
Initial commit: standalone Pyxis T-Deck firmware
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its own repo. microReticulum is consumed as a git submodule dependency pinned to feat/t-deck. All include paths updated from relative symlinks to bare includes resolved via library build flags. Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully. Licensed under AGPLv3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @file BLEFragmenter.cpp
|
||||
* @brief BLE-Reticulum Protocol v2.2 packet fragmenter implementation
|
||||
*/
|
||||
|
||||
#include "BLEFragmenter.h"
|
||||
#include "Log.h"
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
BLEFragmenter::BLEFragmenter(size_t mtu) {
|
||||
setMTU(mtu);
|
||||
}
|
||||
|
||||
void BLEFragmenter::setMTU(size_t mtu) {
|
||||
// Ensure MTU is at least the minimum
|
||||
_mtu = (mtu >= MTU::MINIMUM) ? mtu : MTU::MINIMUM;
|
||||
|
||||
// Calculate payload size (MTU minus header)
|
||||
if (_mtu > Fragment::HEADER_SIZE) {
|
||||
_payload_size = _mtu - Fragment::HEADER_SIZE;
|
||||
} else {
|
||||
_payload_size = 0;
|
||||
WARNING("BLEFragmenter: MTU too small for fragmentation");
|
||||
}
|
||||
}
|
||||
|
||||
bool BLEFragmenter::needsFragmentation(const Bytes& data) const {
|
||||
return data.size() > _payload_size;
|
||||
}
|
||||
|
||||
uint16_t BLEFragmenter::calculateFragmentCount(size_t data_size) const {
|
||||
if (_payload_size == 0) return 0;
|
||||
if (data_size == 0) return 1; // Empty data still produces one fragment
|
||||
return static_cast<uint16_t>((data_size + _payload_size - 1) / _payload_size);
|
||||
}
|
||||
|
||||
std::vector<Bytes> BLEFragmenter::fragment(const Bytes& data, uint16_t sequence_base) {
|
||||
std::vector<Bytes> fragments;
|
||||
|
||||
if (_payload_size == 0) {
|
||||
ERROR("BLEFragmenter: Cannot fragment with zero payload size");
|
||||
return fragments;
|
||||
}
|
||||
|
||||
// Handle empty data case
|
||||
if (data.size() == 0) {
|
||||
// Single empty END fragment
|
||||
fragments.push_back(createFragment(Fragment::END, sequence_base, 1, Bytes()));
|
||||
return fragments;
|
||||
}
|
||||
|
||||
uint16_t total_fragments = calculateFragmentCount(data.size());
|
||||
|
||||
// Pre-allocate vector to avoid incremental reallocations
|
||||
fragments.reserve(total_fragments);
|
||||
|
||||
size_t offset = 0;
|
||||
|
||||
for (uint16_t i = 0; i < total_fragments; i++) {
|
||||
// Calculate payload size for this fragment
|
||||
size_t remaining = data.size() - offset;
|
||||
size_t chunk_size = (remaining < _payload_size) ? remaining : _payload_size;
|
||||
|
||||
// Extract payload chunk
|
||||
Bytes payload(data.data() + offset, chunk_size);
|
||||
offset += chunk_size;
|
||||
|
||||
// Determine fragment type
|
||||
Fragment::Type type;
|
||||
if (total_fragments == 1) {
|
||||
// Single fragment - use END type
|
||||
type = Fragment::END;
|
||||
} else if (i == 0) {
|
||||
// First of multiple fragments
|
||||
type = Fragment::START;
|
||||
} else if (i == total_fragments - 1) {
|
||||
// Last fragment
|
||||
type = Fragment::END;
|
||||
} else {
|
||||
// Middle fragment
|
||||
type = Fragment::CONTINUE;
|
||||
}
|
||||
|
||||
uint16_t sequence = sequence_base + i;
|
||||
fragments.push_back(createFragment(type, sequence, total_fragments, payload));
|
||||
}
|
||||
|
||||
{
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), "BLEFragmenter: Fragmented %zu bytes into %zu fragments",
|
||||
data.size(), fragments.size());
|
||||
TRACE(buf);
|
||||
}
|
||||
|
||||
return fragments;
|
||||
}
|
||||
|
||||
Bytes BLEFragmenter::createFragment(Fragment::Type type, uint16_t sequence,
|
||||
uint16_t total_fragments, const Bytes& payload) {
|
||||
// Allocate buffer for header + payload
|
||||
size_t total_size = Fragment::HEADER_SIZE + payload.size();
|
||||
Bytes fragment(total_size);
|
||||
uint8_t* ptr = fragment.writable(total_size);
|
||||
fragment.resize(total_size);
|
||||
|
||||
// Byte 0: Type
|
||||
ptr[0] = static_cast<uint8_t>(type);
|
||||
|
||||
// Bytes 1-2: Sequence number (big-endian)
|
||||
ptr[1] = static_cast<uint8_t>((sequence >> 8) & 0xFF);
|
||||
ptr[2] = static_cast<uint8_t>(sequence & 0xFF);
|
||||
|
||||
// Bytes 3-4: Total fragments (big-endian)
|
||||
ptr[3] = static_cast<uint8_t>((total_fragments >> 8) & 0xFF);
|
||||
ptr[4] = static_cast<uint8_t>(total_fragments & 0xFF);
|
||||
|
||||
// Bytes 5+: Payload
|
||||
if (payload.size() > 0) {
|
||||
memcpy(ptr + Fragment::HEADER_SIZE, payload.data(), payload.size());
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
bool BLEFragmenter::parseHeader(const Bytes& fragment, Fragment::Type& type,
|
||||
uint16_t& sequence, uint16_t& total_fragments) {
|
||||
if (fragment.size() < Fragment::HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t* ptr = fragment.data();
|
||||
|
||||
// Byte 0: Type
|
||||
uint8_t type_byte = ptr[0];
|
||||
if (type_byte != Fragment::START &&
|
||||
type_byte != Fragment::CONTINUE &&
|
||||
type_byte != Fragment::END) {
|
||||
return false;
|
||||
}
|
||||
type = static_cast<Fragment::Type>(type_byte);
|
||||
|
||||
// Bytes 1-2: Sequence number (big-endian)
|
||||
sequence = (static_cast<uint16_t>(ptr[1]) << 8) | static_cast<uint16_t>(ptr[2]);
|
||||
|
||||
// Bytes 3-4: Total fragments (big-endian)
|
||||
total_fragments = (static_cast<uint16_t>(ptr[3]) << 8) | static_cast<uint16_t>(ptr[4]);
|
||||
|
||||
// Validate total_fragments is non-zero
|
||||
if (total_fragments == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate sequence < total_fragments
|
||||
if (sequence >= total_fragments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Bytes BLEFragmenter::extractPayload(const Bytes& fragment) {
|
||||
if (fragment.size() <= Fragment::HEADER_SIZE) {
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
return Bytes(fragment.data() + Fragment::HEADER_SIZE,
|
||||
fragment.size() - Fragment::HEADER_SIZE);
|
||||
}
|
||||
|
||||
bool BLEFragmenter::isValidFragment(const Bytes& fragment) {
|
||||
Fragment::Type type;
|
||||
uint16_t sequence, total;
|
||||
return parseHeader(fragment, type, sequence, total);
|
||||
}
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @file BLEFragmenter.h
|
||||
* @brief BLE-Reticulum Protocol v2.2 packet fragmenter
|
||||
*
|
||||
* Fragments outgoing Reticulum packets into BLE-sized chunks with the v2.2
|
||||
* 5-byte header format. This class has no BLE dependencies and can be used
|
||||
* for testing on native builds.
|
||||
*
|
||||
* Fragment Header Format (5 bytes):
|
||||
* Byte 0: Type (0x01=START, 0x02=CONTINUE, 0x03=END)
|
||||
* Bytes 1-2: Sequence number (big-endian uint16_t)
|
||||
* Bytes 3-4: Total fragments (big-endian uint16_t)
|
||||
* Bytes 5+: Payload data
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "BLETypes.h"
|
||||
#include "Bytes.h"
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
class BLEFragmenter {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a fragmenter with specified MTU
|
||||
* @param mtu The negotiated BLE MTU (default: minimum BLE MTU of 23)
|
||||
*/
|
||||
explicit BLEFragmenter(size_t mtu = MTU::MINIMUM);
|
||||
|
||||
/**
|
||||
* @brief Set the MTU for fragmentation calculations
|
||||
*
|
||||
* Call this when MTU is renegotiated with a peer.
|
||||
* @param mtu The new MTU value
|
||||
*/
|
||||
void setMTU(size_t mtu);
|
||||
|
||||
/**
|
||||
* @brief Get the current MTU
|
||||
*/
|
||||
size_t getMTU() const { return _mtu; }
|
||||
|
||||
/**
|
||||
* @brief Get the maximum payload size per fragment (MTU - HEADER_SIZE)
|
||||
*/
|
||||
size_t getPayloadSize() const { return _payload_size; }
|
||||
|
||||
/**
|
||||
* @brief Check if a packet needs fragmentation
|
||||
* @param data The packet to check
|
||||
* @return true if packet exceeds single fragment payload capacity
|
||||
*/
|
||||
bool needsFragmentation(const Bytes& data) const;
|
||||
|
||||
/**
|
||||
* @brief Calculate number of fragments needed for a packet
|
||||
* @param data_size Size of the data to fragment
|
||||
* @return Number of fragments needed (minimum 1)
|
||||
*/
|
||||
uint16_t calculateFragmentCount(size_t data_size) const;
|
||||
|
||||
/**
|
||||
* @brief Fragment a packet into BLE-sized chunks
|
||||
*
|
||||
* @param data The complete packet to fragment
|
||||
* @param sequence_base Starting sequence number (default 0)
|
||||
* @return Vector of fragments, each with the 5-byte header prepended
|
||||
*
|
||||
* For a single-fragment packet, returns one fragment with type=END.
|
||||
* For multi-fragment packets:
|
||||
* - First fragment has type=START
|
||||
* - Middle fragments have type=CONTINUE
|
||||
* - Last fragment has type=END
|
||||
*/
|
||||
std::vector<Bytes> fragment(const Bytes& data, uint16_t sequence_base = 0);
|
||||
|
||||
/**
|
||||
* @brief Create a single fragment with proper header
|
||||
*
|
||||
* @param type Fragment type (START, CONTINUE, END)
|
||||
* @param sequence Sequence number for this fragment
|
||||
* @param total_fragments Total number of fragments in this message
|
||||
* @param payload The fragment payload data
|
||||
* @return Complete fragment with 5-byte header prepended
|
||||
*/
|
||||
static Bytes createFragment(Fragment::Type type, uint16_t sequence,
|
||||
uint16_t total_fragments, const Bytes& payload);
|
||||
|
||||
/**
|
||||
* @brief Parse the header from a received fragment
|
||||
*
|
||||
* @param fragment The received fragment data (must be at least HEADER_SIZE bytes)
|
||||
* @param type Output: fragment type
|
||||
* @param sequence Output: sequence number
|
||||
* @param total_fragments Output: total fragment count
|
||||
* @return true if header is valid and was parsed successfully
|
||||
*/
|
||||
static bool parseHeader(const Bytes& fragment, Fragment::Type& type,
|
||||
uint16_t& sequence, uint16_t& total_fragments);
|
||||
|
||||
/**
|
||||
* @brief Extract payload from a fragment (removes header)
|
||||
*
|
||||
* @param fragment The complete fragment with header
|
||||
* @return The payload portion (empty if fragment is too small)
|
||||
*/
|
||||
static Bytes extractPayload(const Bytes& fragment);
|
||||
|
||||
/**
|
||||
* @brief Validate a fragment header
|
||||
*
|
||||
* @param fragment The fragment to validate
|
||||
* @return true if the fragment has a valid header
|
||||
*/
|
||||
static bool isValidFragment(const Bytes& fragment);
|
||||
|
||||
private:
|
||||
size_t _mtu;
|
||||
size_t _payload_size;
|
||||
};
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,493 @@
|
||||
/**
|
||||
* @file BLEIdentityManager.cpp
|
||||
* @brief BLE-Reticulum Protocol v2.2 identity handshake manager implementation
|
||||
*
|
||||
* Uses fixed-size pools instead of STL containers to eliminate heap fragmentation.
|
||||
*/
|
||||
|
||||
#include "BLEIdentityManager.h"
|
||||
#include "Log.h"
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
BLEIdentityManager::BLEIdentityManager() {
|
||||
// Initialize all pools to empty state
|
||||
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
|
||||
_address_identity_pool[i].clear();
|
||||
}
|
||||
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
|
||||
_handshakes_pool[i].clear();
|
||||
}
|
||||
}
|
||||
|
||||
void BLEIdentityManager::setLocalIdentity(const Bytes& identity_hash) {
|
||||
if (identity_hash.size() >= Limits::IDENTITY_SIZE) {
|
||||
_local_identity = Bytes(identity_hash.data(), Limits::IDENTITY_SIZE);
|
||||
DEBUG("BLEIdentityManager: Local identity set: " + _local_identity.toHex().substr(0, 8) + "...");
|
||||
} else {
|
||||
ERROR("BLEIdentityManager: Invalid identity size: " + std::to_string(identity_hash.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void BLEIdentityManager::setHandshakeCompleteCallback(HandshakeCompleteCallback callback) {
|
||||
_handshake_complete_callback = callback;
|
||||
}
|
||||
|
||||
void BLEIdentityManager::setHandshakeFailedCallback(HandshakeFailedCallback callback) {
|
||||
_handshake_failed_callback = callback;
|
||||
}
|
||||
|
||||
void BLEIdentityManager::setMacRotationCallback(MacRotationCallback callback) {
|
||||
_mac_rotation_callback = callback;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Handshake Operations
|
||||
//=============================================================================
|
||||
|
||||
Bytes BLEIdentityManager::initiateHandshake(const Bytes& mac_address) {
|
||||
if (!hasLocalIdentity()) {
|
||||
ERROR("BLEIdentityManager: Cannot initiate handshake without local identity");
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
if (mac_address.size() < Limits::MAC_SIZE) {
|
||||
ERROR("BLEIdentityManager: Invalid MAC address size");
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
|
||||
// Create or update handshake session
|
||||
HandshakeSession* session = getOrCreateSession(mac);
|
||||
if (!session) {
|
||||
WARNING("BLEIdentityManager: Handshake pool is full, cannot initiate");
|
||||
return Bytes();
|
||||
}
|
||||
session->is_central = true;
|
||||
session->state = HandshakeState::INITIATED;
|
||||
session->started_at = Utilities::OS::time();
|
||||
|
||||
DEBUG("BLEIdentityManager: Initiating handshake as central with " +
|
||||
BLEAddress(mac.data()).toString());
|
||||
|
||||
// Return our identity to be written to peer
|
||||
return _local_identity;
|
||||
}
|
||||
|
||||
bool BLEIdentityManager::processReceivedData(const Bytes& mac_address, const Bytes& data, bool is_central) {
|
||||
if (mac_address.size() < Limits::MAC_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
|
||||
// Check if this looks like a handshake
|
||||
if (!isHandshakeData(data, mac)) {
|
||||
return false; // Regular data, not consumed
|
||||
}
|
||||
|
||||
// This is a handshake - extract peer's identity
|
||||
if (data.size() != Limits::IDENTITY_SIZE) {
|
||||
// Should not happen given isHandshakeData check, but be safe
|
||||
return false;
|
||||
}
|
||||
|
||||
Bytes peer_identity(data.data(), Limits::IDENTITY_SIZE);
|
||||
|
||||
DEBUG("BLEIdentityManager: Received identity handshake from " +
|
||||
BLEAddress(mac.data()).toString() + ": " +
|
||||
peer_identity.toHex().substr(0, 8) + "...");
|
||||
|
||||
// Complete the handshake
|
||||
completeHandshake(mac, peer_identity, is_central);
|
||||
|
||||
return true; // Handshake data consumed
|
||||
}
|
||||
|
||||
bool BLEIdentityManager::isHandshakeData(const Bytes& data, const Bytes& mac_address) const {
|
||||
// Handshake is detected if:
|
||||
// 1. Data is exactly 16 bytes (identity size)
|
||||
// 2. No existing identity mapping for this MAC
|
||||
|
||||
if (data.size() != Limits::IDENTITY_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mac_address.size() < Limits::MAC_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
|
||||
// Check if we already have identity for this MAC
|
||||
const AddressIdentitySlot* slot = findAddressToIdentitySlot(mac);
|
||||
if (slot) {
|
||||
// Already have identity - this is regular data, not handshake
|
||||
return false;
|
||||
}
|
||||
|
||||
// No existing identity + 16 bytes = handshake
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEIdentityManager::completeHandshake(const Bytes& mac_address, const Bytes& peer_identity,
|
||||
bool is_central) {
|
||||
DEBUG("BLEIdentityManager::completeHandshake: Starting");
|
||||
if (mac_address.size() < Limits::MAC_SIZE || peer_identity.size() != Limits::IDENTITY_SIZE) {
|
||||
DEBUG("BLEIdentityManager::completeHandshake: Invalid sizes, returning");
|
||||
return;
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
Bytes identity(peer_identity.data(), Limits::IDENTITY_SIZE);
|
||||
DEBUG("BLEIdentityManager::completeHandshake: Created local copies");
|
||||
|
||||
// Check for MAC rotation: same identity from different MAC address
|
||||
Bytes old_mac;
|
||||
bool is_rotation = false;
|
||||
AddressIdentitySlot* existing_slot = findIdentityToAddressSlot(identity);
|
||||
if (existing_slot && existing_slot->mac_address != mac) {
|
||||
// MAC rotation detected!
|
||||
old_mac = existing_slot->mac_address;
|
||||
is_rotation = true;
|
||||
|
||||
INFO("BLEIdentityManager: MAC rotation detected for identity " +
|
||||
identity.toHex().substr(0, 8) + "...: " +
|
||||
BLEAddress(old_mac.data()).toString() + " -> " +
|
||||
BLEAddress(mac.data()).toString());
|
||||
|
||||
// Update the slot with new MAC (same identity)
|
||||
existing_slot->mac_address = mac;
|
||||
} else if (!existing_slot) {
|
||||
// New mapping - add to pool
|
||||
if (!setAddressIdentityMapping(mac, identity)) {
|
||||
WARNING("BLEIdentityManager: Address-identity pool is full");
|
||||
return;
|
||||
}
|
||||
}
|
||||
DEBUG("BLEIdentityManager::completeHandshake: Stored mappings");
|
||||
|
||||
// Remove handshake session
|
||||
removeHandshakeSession(mac);
|
||||
DEBUG("BLEIdentityManager::completeHandshake: Removed handshake session");
|
||||
|
||||
DEBUG("BLEIdentityManager: Handshake complete with " +
|
||||
BLEAddress(mac.data()).toString() +
|
||||
" identity: " + identity.toHex().substr(0, 8) + "..." +
|
||||
(is_central ? " (we are central)" : " (we are peripheral)"));
|
||||
|
||||
// Invoke MAC rotation callback if this was a rotation
|
||||
if (is_rotation && _mac_rotation_callback) {
|
||||
DEBUG("BLEIdentityManager::completeHandshake: Calling MAC rotation callback");
|
||||
_mac_rotation_callback(old_mac, mac, identity);
|
||||
DEBUG("BLEIdentityManager::completeHandshake: MAC rotation callback returned");
|
||||
}
|
||||
|
||||
// Invoke handshake complete callback
|
||||
if (_handshake_complete_callback) {
|
||||
DEBUG("BLEIdentityManager::completeHandshake: Calling handshake complete callback");
|
||||
_handshake_complete_callback(mac, identity, is_central);
|
||||
DEBUG("BLEIdentityManager::completeHandshake: Callback returned");
|
||||
} else {
|
||||
DEBUG("BLEIdentityManager::completeHandshake: No callback set");
|
||||
}
|
||||
}
|
||||
|
||||
void BLEIdentityManager::checkTimeouts() {
|
||||
double now = Utilities::OS::time();
|
||||
|
||||
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
|
||||
HandshakeSession& session = _handshakes_pool[i];
|
||||
if (!session.in_use) continue;
|
||||
|
||||
if (session.state != HandshakeState::COMPLETE) {
|
||||
double age = now - session.started_at;
|
||||
if (age > Timing::HANDSHAKE_TIMEOUT) {
|
||||
Bytes mac = session.mac_address;
|
||||
|
||||
WARNING("BLEIdentityManager: Handshake timeout for " +
|
||||
BLEAddress(mac.data()).toString());
|
||||
|
||||
if (_handshake_failed_callback) {
|
||||
_handshake_failed_callback(mac, "Handshake timeout");
|
||||
}
|
||||
|
||||
session.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Identity Mapping
|
||||
//=============================================================================
|
||||
|
||||
Bytes BLEIdentityManager::getIdentityForMac(const Bytes& mac_address) const {
|
||||
if (mac_address.size() < Limits::MAC_SIZE) {
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
|
||||
const AddressIdentitySlot* slot = findAddressToIdentitySlot(mac);
|
||||
if (slot) {
|
||||
return slot->identity;
|
||||
}
|
||||
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
Bytes BLEIdentityManager::getMacForIdentity(const Bytes& identity) const {
|
||||
if (identity.size() != Limits::IDENTITY_SIZE) {
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
const AddressIdentitySlot* slot = findIdentityToAddressSlot(identity);
|
||||
if (slot) {
|
||||
return slot->mac_address;
|
||||
}
|
||||
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
bool BLEIdentityManager::hasIdentity(const Bytes& mac_address) const {
|
||||
if (mac_address.size() < Limits::MAC_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
return findAddressToIdentitySlot(mac) != nullptr;
|
||||
}
|
||||
|
||||
Bytes BLEIdentityManager::findIdentityByPrefix(const Bytes& prefix) const {
|
||||
if (prefix.size() == 0 || prefix.size() > Limits::IDENTITY_SIZE) {
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
// Search through all known identities for one that starts with this prefix
|
||||
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
|
||||
if (_address_identity_pool[i].in_use) {
|
||||
const Bytes& identity = _address_identity_pool[i].identity;
|
||||
if (identity.size() >= prefix.size() &&
|
||||
memcmp(identity.data(), prefix.data(), prefix.size()) == 0) {
|
||||
return identity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
void BLEIdentityManager::updateMacForIdentity(const Bytes& identity, const Bytes& new_mac) {
|
||||
if (identity.size() != Limits::IDENTITY_SIZE || new_mac.size() < Limits::MAC_SIZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bytes mac(new_mac.data(), Limits::MAC_SIZE);
|
||||
|
||||
AddressIdentitySlot* slot = findIdentityToAddressSlot(identity);
|
||||
if (!slot) {
|
||||
return; // Unknown identity
|
||||
}
|
||||
|
||||
// Update MAC address in the slot
|
||||
slot->mac_address = mac;
|
||||
|
||||
DEBUG("BLEIdentityManager: Updated MAC for identity " +
|
||||
identity.toHex().substr(0, 8) + "... to " +
|
||||
BLEAddress(mac.data()).toString());
|
||||
}
|
||||
|
||||
void BLEIdentityManager::removeMapping(const Bytes& mac_address) {
|
||||
if (mac_address.size() < Limits::MAC_SIZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
|
||||
removeAddressIdentityMapping(mac);
|
||||
|
||||
DEBUG("BLEIdentityManager: Removed mapping for " +
|
||||
BLEAddress(mac.data()).toString());
|
||||
|
||||
// Also clean up any pending handshake
|
||||
removeHandshakeSession(mac);
|
||||
}
|
||||
|
||||
void BLEIdentityManager::clearAllMappings() {
|
||||
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
|
||||
_address_identity_pool[i].clear();
|
||||
}
|
||||
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
|
||||
_handshakes_pool[i].clear();
|
||||
}
|
||||
|
||||
DEBUG("BLEIdentityManager: Cleared all identity mappings");
|
||||
}
|
||||
|
||||
size_t BLEIdentityManager::knownPeerCount() const {
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
|
||||
if (_address_identity_pool[i].in_use) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool BLEIdentityManager::isHandshakeInProgress(const Bytes& mac_address) const {
|
||||
if (mac_address.size() < Limits::MAC_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
|
||||
const HandshakeSession* session = findHandshakeSession(mac);
|
||||
if (session) {
|
||||
return session->state != HandshakeState::NONE &&
|
||||
session->state != HandshakeState::COMPLETE;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Pool Helper Methods - Address to Identity Mapping
|
||||
//=============================================================================
|
||||
|
||||
BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findAddressToIdentitySlot(const Bytes& mac) {
|
||||
if (mac.size() < Limits::MAC_SIZE) return nullptr;
|
||||
|
||||
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
|
||||
if (_address_identity_pool[i].in_use &&
|
||||
_address_identity_pool[i].mac_address == mac) {
|
||||
return &_address_identity_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findAddressToIdentitySlot(const Bytes& mac) const {
|
||||
return const_cast<BLEIdentityManager*>(this)->findAddressToIdentitySlot(mac);
|
||||
}
|
||||
|
||||
BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findIdentityToAddressSlot(const Bytes& identity) {
|
||||
if (identity.size() != Limits::IDENTITY_SIZE) return nullptr;
|
||||
|
||||
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
|
||||
if (_address_identity_pool[i].in_use &&
|
||||
_address_identity_pool[i].identity == identity) {
|
||||
return &_address_identity_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findIdentityToAddressSlot(const Bytes& identity) const {
|
||||
return const_cast<BLEIdentityManager*>(this)->findIdentityToAddressSlot(identity);
|
||||
}
|
||||
|
||||
BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findEmptyAddressIdentitySlot() {
|
||||
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
|
||||
if (!_address_identity_pool[i].in_use) {
|
||||
return &_address_identity_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool BLEIdentityManager::setAddressIdentityMapping(const Bytes& mac, const Bytes& identity) {
|
||||
// Check if already exists for this MAC
|
||||
AddressIdentitySlot* existing = findAddressToIdentitySlot(mac);
|
||||
if (existing) {
|
||||
existing->identity = identity;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if already exists for this identity (MAC rotation case)
|
||||
existing = findIdentityToAddressSlot(identity);
|
||||
if (existing) {
|
||||
existing->mac_address = mac;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find empty slot
|
||||
AddressIdentitySlot* slot = findEmptyAddressIdentitySlot();
|
||||
if (!slot) {
|
||||
WARNING("BLEIdentityManager: Address-identity pool is full");
|
||||
return false;
|
||||
}
|
||||
|
||||
slot->in_use = true;
|
||||
slot->mac_address = mac;
|
||||
slot->identity = identity;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEIdentityManager::removeAddressIdentityMapping(const Bytes& mac) {
|
||||
AddressIdentitySlot* slot = findAddressToIdentitySlot(mac);
|
||||
if (slot) {
|
||||
slot->clear();
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Pool Helper Methods - Handshake Sessions
|
||||
//=============================================================================
|
||||
|
||||
BLEIdentityManager::HandshakeSession* BLEIdentityManager::findHandshakeSession(const Bytes& mac) {
|
||||
if (mac.size() < Limits::MAC_SIZE) return nullptr;
|
||||
|
||||
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
|
||||
if (_handshakes_pool[i].in_use &&
|
||||
_handshakes_pool[i].mac_address == mac) {
|
||||
return &_handshakes_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BLEIdentityManager::HandshakeSession* BLEIdentityManager::findHandshakeSession(const Bytes& mac) const {
|
||||
return const_cast<BLEIdentityManager*>(this)->findHandshakeSession(mac);
|
||||
}
|
||||
|
||||
BLEIdentityManager::HandshakeSession* BLEIdentityManager::findEmptyHandshakeSlot() {
|
||||
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
|
||||
if (!_handshakes_pool[i].in_use) {
|
||||
return &_handshakes_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BLEIdentityManager::HandshakeSession* BLEIdentityManager::getOrCreateSession(const Bytes& mac_address) {
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
|
||||
// Check if session already exists
|
||||
HandshakeSession* existing = findHandshakeSession(mac);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Find empty slot
|
||||
HandshakeSession* slot = findEmptyHandshakeSlot();
|
||||
if (!slot) {
|
||||
return nullptr; // Pool is full
|
||||
}
|
||||
|
||||
// Create new session
|
||||
slot->in_use = true;
|
||||
slot->mac_address = mac;
|
||||
slot->state = HandshakeState::NONE;
|
||||
slot->started_at = Utilities::OS::time();
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
void BLEIdentityManager::removeHandshakeSession(const Bytes& mac) {
|
||||
HandshakeSession* session = findHandshakeSession(mac);
|
||||
if (session) {
|
||||
session->clear();
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* @file BLEIdentityManager.h
|
||||
* @brief BLE-Reticulum Protocol v2.2 identity handshake manager
|
||||
*
|
||||
* Manages the identity handshake protocol and address-to-identity mapping.
|
||||
*
|
||||
* Handshake Protocol (v2.2):
|
||||
* 1. Central connects to peripheral
|
||||
* 2. Central writes 16-byte identity to RX characteristic
|
||||
* 3. Peripheral detects handshake: exactly 16 bytes AND no existing identity for that address
|
||||
* 4. Both sides now have bidirectional identity mapping
|
||||
*
|
||||
* The identity is the first 16 bytes of the Reticulum transport identity hash,
|
||||
* which remains stable across MAC address rotations.
|
||||
*
|
||||
* Uses fixed-size pools instead of STL containers to eliminate heap fragmentation.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "BLETypes.h"
|
||||
#include "Bytes.h"
|
||||
#include "Utilities/OS.h"
|
||||
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
class BLEIdentityManager {
|
||||
public:
|
||||
//=========================================================================
|
||||
// Pool Configuration
|
||||
//=========================================================================
|
||||
static constexpr size_t ADDRESS_IDENTITY_POOL_SIZE = 16;
|
||||
static constexpr size_t HANDSHAKE_POOL_SIZE = 4;
|
||||
/**
|
||||
* @brief Callback when handshake completes successfully
|
||||
*
|
||||
* @param mac_address The peer's current MAC address
|
||||
* @param peer_identity The peer's 16-byte identity hash
|
||||
* @param is_central true if we are the central (we initiated)
|
||||
*/
|
||||
using HandshakeCompleteCallback = std::function<void(
|
||||
const Bytes& mac_address,
|
||||
const Bytes& peer_identity,
|
||||
bool is_central)>;
|
||||
|
||||
/**
|
||||
* @brief Callback when handshake fails
|
||||
*
|
||||
* @param mac_address The peer's MAC address
|
||||
* @param reason Description of the failure
|
||||
*/
|
||||
using HandshakeFailedCallback = std::function<void(
|
||||
const Bytes& mac_address,
|
||||
const std::string& reason)>;
|
||||
|
||||
/**
|
||||
* @brief Callback when MAC rotation is detected
|
||||
*
|
||||
* Called when an identity we already know appears from a different MAC address.
|
||||
* This is common on Android which rotates BLE MAC addresses every ~15 minutes.
|
||||
*
|
||||
* @param old_mac The previous MAC address for this identity
|
||||
* @param new_mac The new MAC address
|
||||
* @param identity The stable 16-byte identity
|
||||
*/
|
||||
using MacRotationCallback = std::function<void(
|
||||
const Bytes& old_mac,
|
||||
const Bytes& new_mac,
|
||||
const Bytes& identity)>;
|
||||
|
||||
public:
|
||||
BLEIdentityManager();
|
||||
|
||||
/**
|
||||
* @brief Set our local identity (from RNS::Identity)
|
||||
*
|
||||
* Must be called before any handshakes. The identity should be the
|
||||
* first 16 bytes of the transport identity hash.
|
||||
*
|
||||
* @param identity_hash The 16-byte identity hash
|
||||
*/
|
||||
void setLocalIdentity(const Bytes& identity_hash);
|
||||
|
||||
/**
|
||||
* @brief Get our local identity hash
|
||||
*/
|
||||
const Bytes& getLocalIdentity() const { return _local_identity; }
|
||||
|
||||
/**
|
||||
* @brief Check if local identity is set
|
||||
*/
|
||||
bool hasLocalIdentity() const { return _local_identity.size() == Limits::IDENTITY_SIZE; }
|
||||
|
||||
/**
|
||||
* @brief Set callback for successful handshakes
|
||||
*/
|
||||
void setHandshakeCompleteCallback(HandshakeCompleteCallback callback);
|
||||
|
||||
/**
|
||||
* @brief Set callback for failed handshakes
|
||||
*/
|
||||
void setHandshakeFailedCallback(HandshakeFailedCallback callback);
|
||||
|
||||
/**
|
||||
* @brief Set callback for MAC rotation detection
|
||||
*/
|
||||
void setMacRotationCallback(MacRotationCallback callback);
|
||||
|
||||
//=========================================================================
|
||||
// Handshake Operations
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Start handshake as central (initiator)
|
||||
*
|
||||
* Called after BLE connection is established. Returns the identity
|
||||
* bytes that should be written to the peer's RX characteristic.
|
||||
*
|
||||
* @param mac_address Peer's MAC address
|
||||
* @return The 16-byte identity to write to peer's RX characteristic
|
||||
*/
|
||||
Bytes initiateHandshake(const Bytes& mac_address);
|
||||
|
||||
/**
|
||||
* @brief Process received data to detect/complete handshake
|
||||
*
|
||||
* This should be called for all received data. The function detects
|
||||
* whether the data is an identity handshake or regular data.
|
||||
*
|
||||
* @param mac_address Source MAC address
|
||||
* @param data Received data (may be identity or regular packet)
|
||||
* @param is_central true if we are central role for this connection
|
||||
* @return true if this was a handshake message (consumed), false if regular data
|
||||
*/
|
||||
bool processReceivedData(const Bytes& mac_address, const Bytes& data, bool is_central);
|
||||
|
||||
/**
|
||||
* @brief Check if data looks like an identity handshake
|
||||
*
|
||||
* A handshake is detected if:
|
||||
* - Data is exactly 16 bytes
|
||||
* - No existing identity mapping exists for this MAC address
|
||||
*
|
||||
* @param data The received data
|
||||
* @param mac_address The sender's MAC
|
||||
* @return true if this appears to be a handshake
|
||||
*/
|
||||
bool isHandshakeData(const Bytes& data, const Bytes& mac_address) const;
|
||||
|
||||
/**
|
||||
* @brief Mark handshake as complete for a peer
|
||||
*
|
||||
* Called after receiving identity from peer or after writing our identity.
|
||||
*
|
||||
* @param mac_address The peer's MAC address
|
||||
* @param peer_identity The peer's 16-byte identity
|
||||
* @param is_central true if we are the central
|
||||
*/
|
||||
void completeHandshake(const Bytes& mac_address, const Bytes& peer_identity, bool is_central);
|
||||
|
||||
/**
|
||||
* @brief Check for timed-out handshakes
|
||||
*/
|
||||
void checkTimeouts();
|
||||
|
||||
//=========================================================================
|
||||
// Identity Mapping
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Get identity for a MAC address
|
||||
* @return Identity bytes or empty if not known
|
||||
*/
|
||||
Bytes getIdentityForMac(const Bytes& mac_address) const;
|
||||
|
||||
/**
|
||||
* @brief Get MAC address for an identity
|
||||
* @return MAC address or empty if not known
|
||||
*/
|
||||
Bytes getMacForIdentity(const Bytes& identity) const;
|
||||
|
||||
/**
|
||||
* @brief Check if we have completed handshake with a MAC
|
||||
*/
|
||||
bool hasIdentity(const Bytes& mac_address) const;
|
||||
|
||||
/**
|
||||
* @brief Find identity that matches a prefix (for MAC rotation detection)
|
||||
*
|
||||
* Used when scanning detects a device name with identity prefix (Protocol v2.2).
|
||||
* If found, returns the full identity so we can recognize the rotated peer.
|
||||
*
|
||||
* @param prefix First N bytes of identity (typically 3 bytes from device name)
|
||||
* @return Full identity bytes if found, empty otherwise
|
||||
*/
|
||||
Bytes findIdentityByPrefix(const Bytes& prefix) const;
|
||||
|
||||
/**
|
||||
* @brief Update MAC address for a known identity (MAC rotation)
|
||||
*
|
||||
* @param identity The stable identity
|
||||
* @param new_mac The new MAC address
|
||||
*/
|
||||
void updateMacForIdentity(const Bytes& identity, const Bytes& new_mac);
|
||||
|
||||
/**
|
||||
* @brief Remove identity mapping (on disconnect)
|
||||
*/
|
||||
void removeMapping(const Bytes& mac_address);
|
||||
|
||||
/**
|
||||
* @brief Clear all mappings
|
||||
*/
|
||||
void clearAllMappings();
|
||||
|
||||
/**
|
||||
* @brief Get count of known peer identities
|
||||
*/
|
||||
size_t knownPeerCount() const;
|
||||
|
||||
/**
|
||||
* @brief Check if handshake is in progress for a MAC
|
||||
*/
|
||||
bool isHandshakeInProgress(const Bytes& mac_address) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Handshake state tracking
|
||||
*/
|
||||
enum class HandshakeState {
|
||||
NONE, // No handshake in progress
|
||||
INITIATED, // We sent our identity (as central)
|
||||
RECEIVED_IDENTITY, // We received peer's identity
|
||||
COMPLETE // Bidirectional identity exchange done
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief State for an in-progress handshake
|
||||
*/
|
||||
struct HandshakeSession {
|
||||
bool in_use = false;
|
||||
Bytes mac_address;
|
||||
Bytes peer_identity;
|
||||
HandshakeState state = HandshakeState::NONE;
|
||||
bool is_central = false;
|
||||
double started_at = 0.0;
|
||||
|
||||
void clear() {
|
||||
in_use = false;
|
||||
mac_address.clear();
|
||||
peer_identity.clear();
|
||||
state = HandshakeState::NONE;
|
||||
is_central = false;
|
||||
started_at = 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Slot for address-to-identity mapping
|
||||
*/
|
||||
struct AddressIdentitySlot {
|
||||
bool in_use = false;
|
||||
Bytes mac_address; // 6-byte MAC key
|
||||
Bytes identity; // 16-byte identity value
|
||||
|
||||
void clear() {
|
||||
in_use = false;
|
||||
mac_address.clear();
|
||||
identity.clear();
|
||||
}
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// Pool Helper Methods - Address to Identity Mapping
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Find slot by MAC address
|
||||
*/
|
||||
AddressIdentitySlot* findAddressToIdentitySlot(const Bytes& mac);
|
||||
const AddressIdentitySlot* findAddressToIdentitySlot(const Bytes& mac) const;
|
||||
|
||||
/**
|
||||
* @brief Find slot by identity
|
||||
*/
|
||||
AddressIdentitySlot* findIdentityToAddressSlot(const Bytes& identity);
|
||||
const AddressIdentitySlot* findIdentityToAddressSlot(const Bytes& identity) const;
|
||||
|
||||
/**
|
||||
* @brief Find an empty slot in the address-identity pool
|
||||
*/
|
||||
AddressIdentitySlot* findEmptyAddressIdentitySlot();
|
||||
|
||||
/**
|
||||
* @brief Add or update address-identity mapping
|
||||
* @return true if successful, false if pool is full
|
||||
*/
|
||||
bool setAddressIdentityMapping(const Bytes& mac, const Bytes& identity);
|
||||
|
||||
/**
|
||||
* @brief Remove mapping by MAC address
|
||||
*/
|
||||
void removeAddressIdentityMapping(const Bytes& mac);
|
||||
|
||||
//=========================================================================
|
||||
// Pool Helper Methods - Handshake Sessions
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Find handshake session by MAC
|
||||
*/
|
||||
HandshakeSession* findHandshakeSession(const Bytes& mac);
|
||||
const HandshakeSession* findHandshakeSession(const Bytes& mac) const;
|
||||
|
||||
/**
|
||||
* @brief Find an empty handshake session slot
|
||||
*/
|
||||
HandshakeSession* findEmptyHandshakeSlot();
|
||||
|
||||
/**
|
||||
* @brief Get or create a handshake session for a MAC
|
||||
*/
|
||||
HandshakeSession* getOrCreateSession(const Bytes& mac_address);
|
||||
|
||||
/**
|
||||
* @brief Remove handshake session by MAC
|
||||
*/
|
||||
void removeHandshakeSession(const Bytes& mac);
|
||||
|
||||
//=========================================================================
|
||||
// Fixed-size Pool Storage
|
||||
//=========================================================================
|
||||
|
||||
// Our local identity hash (16 bytes)
|
||||
Bytes _local_identity;
|
||||
|
||||
// Bidirectional mappings (survive MAC rotation via identity)
|
||||
// Note: We use the same pool for both directions since they share the same data
|
||||
AddressIdentitySlot _address_identity_pool[ADDRESS_IDENTITY_POOL_SIZE];
|
||||
|
||||
// Active handshake sessions (keyed by MAC)
|
||||
HandshakeSession _handshakes_pool[HANDSHAKE_POOL_SIZE];
|
||||
|
||||
// Callbacks
|
||||
HandshakeCompleteCallback _handshake_complete_callback = nullptr;
|
||||
HandshakeFailedCallback _handshake_failed_callback = nullptr;
|
||||
MacRotationCallback _mac_rotation_callback = nullptr;
|
||||
};
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,946 @@
|
||||
/**
|
||||
* @file BLEInterface.cpp
|
||||
* @brief BLE-Reticulum Protocol v2.2 interface implementation
|
||||
*/
|
||||
|
||||
#include "BLEInterface.h"
|
||||
#include "Log.h"
|
||||
#include "Utilities/OS.h"
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include <Arduino.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
|
||||
using namespace RNS;
|
||||
using namespace RNS::BLE;
|
||||
|
||||
BLEInterface::BLEInterface(const char* name) : InterfaceImpl(name) {
|
||||
_IN = true;
|
||||
_OUT = true;
|
||||
_bitrate = BITRATE_GUESS;
|
||||
_HW_MTU = HW_MTU_DEFAULT;
|
||||
}
|
||||
|
||||
BLEInterface::~BLEInterface() {
|
||||
stop();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Configuration
|
||||
//=============================================================================
|
||||
|
||||
void BLEInterface::setRole(Role role) {
|
||||
_role = role;
|
||||
}
|
||||
|
||||
void BLEInterface::setDeviceName(const std::string& name) {
|
||||
_device_name = name;
|
||||
}
|
||||
|
||||
void BLEInterface::setLocalIdentity(const Bytes& identity) {
|
||||
if (identity.size() >= Limits::IDENTITY_SIZE) {
|
||||
_local_identity = Bytes(identity.data(), Limits::IDENTITY_SIZE);
|
||||
_identity_manager.setLocalIdentity(_local_identity);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEInterface::setMaxConnections(uint8_t max) {
|
||||
_max_connections = (max <= Limits::MAX_PEERS) ? max : Limits::MAX_PEERS;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// InterfaceImpl Overrides
|
||||
//=============================================================================
|
||||
|
||||
bool BLEInterface::start() {
|
||||
if (_platform && _platform->isRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate identity
|
||||
if (!_identity_manager.hasLocalIdentity()) {
|
||||
ERROR("BLEInterface: Local identity not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create platform
|
||||
_platform = BLEPlatformFactory::create();
|
||||
if (!_platform) {
|
||||
ERROR("BLEInterface: Failed to create BLE platform");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure platform
|
||||
PlatformConfig config;
|
||||
config.role = _role;
|
||||
config.device_name = _device_name;
|
||||
config.preferred_mtu = MTU::REQUESTED;
|
||||
config.max_connections = _max_connections;
|
||||
|
||||
if (!_platform->initialize(config)) {
|
||||
ERROR("BLEInterface: Failed to initialize BLE platform");
|
||||
_platform.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup callbacks
|
||||
setupCallbacks();
|
||||
|
||||
// Set identity data for peripheral mode
|
||||
_platform->setIdentityData(_local_identity);
|
||||
|
||||
// Set local MAC in peer manager
|
||||
_peer_manager.setLocalMac(_platform->getLocalAddress().toBytes());
|
||||
|
||||
// Start platform
|
||||
if (!_platform->start()) {
|
||||
ERROR("BLEInterface: Failed to start BLE platform");
|
||||
_platform.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
_online = true;
|
||||
_last_scan = 0; // Trigger immediate scan
|
||||
_last_keepalive = Utilities::OS::time();
|
||||
_last_maintenance = Utilities::OS::time();
|
||||
|
||||
INFO("BLEInterface: Started, role: " + std::string(roleToString(_role)) +
|
||||
", identity: " + _local_identity.toHex().substr(0, 8) + "..." +
|
||||
", localMAC: " + _platform->getLocalAddress().toString());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEInterface::stop() {
|
||||
if (_platform) {
|
||||
_platform->stop();
|
||||
_platform->shutdown();
|
||||
_platform.reset();
|
||||
}
|
||||
|
||||
_fragmenters.clear();
|
||||
_online = false;
|
||||
|
||||
INFO("BLEInterface: Stopped");
|
||||
}
|
||||
|
||||
void BLEInterface::loop() {
|
||||
static double last_loop_log = 0;
|
||||
double now = Utilities::OS::time();
|
||||
|
||||
// Process any pending handshakes (deferred from callback for stack safety)
|
||||
if (!_pending_handshakes.empty()) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
for (const auto& pending : _pending_handshakes) {
|
||||
DEBUG("BLEInterface: Processing deferred handshake for " +
|
||||
pending.identity.toHex().substr(0, 8) + "...");
|
||||
|
||||
// Update peer manager with identity
|
||||
_peer_manager.setPeerIdentity(pending.mac, pending.identity);
|
||||
_peer_manager.connectionSucceeded(pending.identity);
|
||||
|
||||
// Create fragmenter for this peer
|
||||
PeerInfo* peer = _peer_manager.getPeerByIdentity(pending.identity);
|
||||
uint16_t mtu = peer ? peer->mtu : MTU::MINIMUM;
|
||||
_fragmenters[pending.identity] = BLEFragmenter(mtu);
|
||||
|
||||
INFO("BLEInterface: Handshake complete with " + pending.identity.toHex().substr(0, 8) +
|
||||
"... (we are " + (pending.is_central ? "central" : "peripheral") + ")");
|
||||
}
|
||||
_pending_handshakes.clear();
|
||||
}
|
||||
|
||||
// Process any pending data fragments (deferred from callback for stack safety)
|
||||
if (!_pending_data.empty()) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
for (const auto& pending : _pending_data) {
|
||||
_reassembler.processFragment(pending.identity, pending.data);
|
||||
}
|
||||
_pending_data.clear();
|
||||
}
|
||||
|
||||
// Debug: log loop status every 10 seconds
|
||||
if (now - last_loop_log >= 10.0) {
|
||||
DEBUG("BLEInterface::loop() platform=" + std::string(_platform ? "yes" : "no") +
|
||||
" running=" + std::string(_platform && _platform->isRunning() ? "yes" : "no") +
|
||||
" scanning=" + std::string(_platform && _platform->isScanning() ? "yes" : "no") +
|
||||
" connected=" + std::to_string(_peer_manager.connectedCount()));
|
||||
last_loop_log = now;
|
||||
}
|
||||
|
||||
if (!_platform || !_platform->isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Platform loop
|
||||
_platform->loop();
|
||||
|
||||
// Periodic scanning (central mode)
|
||||
if (_role == Role::CENTRAL || _role == Role::DUAL) {
|
||||
if (now - _last_scan >= SCAN_INTERVAL) {
|
||||
performScan();
|
||||
_last_scan = now;
|
||||
}
|
||||
}
|
||||
|
||||
// Keepalive processing
|
||||
if (now - _last_keepalive >= KEEPALIVE_INTERVAL) {
|
||||
sendKeepalives();
|
||||
_last_keepalive = now;
|
||||
}
|
||||
|
||||
// Maintenance (cleanup, scores, timeouts)
|
||||
if (now - _last_maintenance >= MAINTENANCE_INTERVAL) {
|
||||
performMaintenance();
|
||||
_last_maintenance = now;
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Data Transfer
|
||||
//=============================================================================
|
||||
|
||||
void BLEInterface::send_outgoing(const Bytes& data) {
|
||||
if (!_platform || !_platform->isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
// Get all connected peers
|
||||
auto connected_peers = _peer_manager.getConnectedPeers();
|
||||
|
||||
if (connected_peers.empty()) {
|
||||
TRACE("BLEInterface: No connected peers, dropping packet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Count peers with identity
|
||||
size_t peers_with_identity = 0;
|
||||
for (PeerInfo* peer : connected_peers) {
|
||||
if (peer->hasIdentity()) {
|
||||
peers_with_identity++;
|
||||
}
|
||||
}
|
||||
DEBUG("BLEInterface: Sending to " + std::to_string(peers_with_identity) +
|
||||
"/" + std::to_string(connected_peers.size()) + " connected peers");
|
||||
|
||||
// Send to all connected peers with identity
|
||||
for (PeerInfo* peer : connected_peers) {
|
||||
if (peer->hasIdentity()) {
|
||||
sendToPeer(peer->identity, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Track outgoing stats
|
||||
handle_outgoing(data);
|
||||
}
|
||||
|
||||
bool BLEInterface::sendToPeer(const Bytes& peer_identity, const Bytes& data) {
|
||||
PeerInfo* peer = _peer_manager.getPeerByIdentity(peer_identity);
|
||||
if (!peer || !peer->isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get or create fragmenter for this peer
|
||||
auto frag_it = _fragmenters.find(peer_identity);
|
||||
if (frag_it == _fragmenters.end()) {
|
||||
_fragmenters[peer_identity] = BLEFragmenter(peer->mtu);
|
||||
frag_it = _fragmenters.find(peer_identity);
|
||||
}
|
||||
|
||||
// Update MTU if changed
|
||||
frag_it->second.setMTU(peer->mtu);
|
||||
|
||||
// Fragment the data
|
||||
std::vector<Bytes> fragments = frag_it->second.fragment(data);
|
||||
|
||||
INFO("BLEInterface: Sending " + std::to_string(fragments.size()) + " frags to " +
|
||||
peer_identity.toHex().substr(0, 8) + " via " + (peer->is_central ? "write" : "notify") +
|
||||
" conn=" + std::to_string(peer->conn_handle) + " mtu=" + std::to_string(peer->mtu));
|
||||
|
||||
// Send each fragment
|
||||
bool all_sent = true;
|
||||
for (const Bytes& fragment : fragments) {
|
||||
bool sent = false;
|
||||
|
||||
if (peer->is_central) {
|
||||
// We are central - write to peripheral (with response for debugging)
|
||||
sent = _platform->write(peer->conn_handle, fragment, true);
|
||||
} else {
|
||||
// We are peripheral - notify central
|
||||
sent = _platform->notify(peer->conn_handle, fragment);
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
WARNING("BLEInterface: Failed to send fragment to " +
|
||||
peer_identity.toHex().substr(0, 8) + " conn=" +
|
||||
std::to_string(peer->conn_handle));
|
||||
all_sent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!all_sent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_peer_manager.recordPacketSent(peer_identity);
|
||||
return true;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Status
|
||||
//=============================================================================
|
||||
|
||||
size_t BLEInterface::peerCount() const {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
return _peer_manager.connectedCount();
|
||||
}
|
||||
|
||||
size_t BLEInterface::getConnectedPeerSummaries(PeerSummary* out, size_t max_count) const {
|
||||
if (!out || max_count == 0) return 0;
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
// Cast away const for read-only access to non-const getConnectedPeers()
|
||||
auto& mutable_peer_manager = const_cast<BLE::BLEPeerManager&>(_peer_manager);
|
||||
auto connected_peers = mutable_peer_manager.getConnectedPeers();
|
||||
|
||||
size_t count = 0;
|
||||
for (const auto* peer : connected_peers) {
|
||||
if (!peer || count >= max_count) break;
|
||||
|
||||
PeerSummary& summary = out[count];
|
||||
|
||||
// Format identity (first 12 hex chars) or empty if no identity
|
||||
// Look up identity from identity manager (where it's actually stored after handshake)
|
||||
Bytes identity = _identity_manager.getIdentityForMac(peer->mac_address);
|
||||
if (identity.size() == Limits::IDENTITY_SIZE) {
|
||||
std::string hex = identity.toHex();
|
||||
size_t len = (hex.length() >= 12) ? 12 : hex.length();
|
||||
memcpy(summary.identity, hex.c_str(), len);
|
||||
summary.identity[len] = '\0';
|
||||
} else {
|
||||
summary.identity[0] = '\0';
|
||||
}
|
||||
|
||||
// Format MAC as AA:BB:CC:DD:EE:FF
|
||||
if (peer->mac_address.size() >= 6) {
|
||||
const uint8_t* mac = peer->mac_address.data();
|
||||
snprintf(summary.mac, sizeof(summary.mac), "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
} else {
|
||||
summary.mac[0] = '\0';
|
||||
}
|
||||
|
||||
summary.rssi = peer->rssi;
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
std::map<std::string, float> BLEInterface::get_stats() const {
|
||||
std::map<std::string, float> stats;
|
||||
stats["central_connections"] = 0.0f;
|
||||
stats["peripheral_connections"] = 0.0f;
|
||||
|
||||
try {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
// Count central vs peripheral connections
|
||||
int central_count = 0;
|
||||
int peripheral_count = 0;
|
||||
|
||||
// Cast away const for read-only access to non-const getConnectedPeers()
|
||||
auto& mutable_peer_manager = const_cast<BLE::BLEPeerManager&>(_peer_manager);
|
||||
auto connected_peers = mutable_peer_manager.getConnectedPeers();
|
||||
for (const auto* peer : connected_peers) {
|
||||
if (peer && peer->is_central) {
|
||||
central_count++;
|
||||
} else if (peer) {
|
||||
peripheral_count++;
|
||||
}
|
||||
}
|
||||
|
||||
stats["central_connections"] = (float)central_count;
|
||||
stats["peripheral_connections"] = (float)peripheral_count;
|
||||
} catch (...) {
|
||||
// Ignore errors during BLE state changes
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Platform Callbacks
|
||||
//=============================================================================
|
||||
|
||||
void BLEInterface::setupCallbacks() {
|
||||
_platform->setOnScanResult([this](const ScanResult& result) {
|
||||
onScanResult(result);
|
||||
});
|
||||
|
||||
_platform->setOnConnected([this](const ConnectionHandle& conn) {
|
||||
onConnected(conn);
|
||||
});
|
||||
|
||||
_platform->setOnDisconnected([this](const ConnectionHandle& conn, uint8_t reason) {
|
||||
onDisconnected(conn, reason);
|
||||
});
|
||||
|
||||
_platform->setOnMTUChanged([this](const ConnectionHandle& conn, uint16_t mtu) {
|
||||
onMTUChanged(conn, mtu);
|
||||
});
|
||||
|
||||
_platform->setOnServicesDiscovered([this](const ConnectionHandle& conn, bool success) {
|
||||
onServicesDiscovered(conn, success);
|
||||
});
|
||||
|
||||
_platform->setOnDataReceived([this](const ConnectionHandle& conn, const Bytes& data) {
|
||||
onDataReceived(conn, data);
|
||||
});
|
||||
|
||||
_platform->setOnCentralConnected([this](const ConnectionHandle& conn) {
|
||||
onCentralConnected(conn);
|
||||
});
|
||||
|
||||
_platform->setOnCentralDisconnected([this](const ConnectionHandle& conn) {
|
||||
onCentralDisconnected(conn);
|
||||
});
|
||||
|
||||
_platform->setOnWriteReceived([this](const ConnectionHandle& conn, const Bytes& data) {
|
||||
onWriteReceived(conn, data);
|
||||
});
|
||||
|
||||
// Identity manager callbacks
|
||||
_identity_manager.setHandshakeCompleteCallback(
|
||||
[this](const Bytes& mac, const Bytes& identity, bool is_central) {
|
||||
onHandshakeComplete(mac, identity, is_central);
|
||||
});
|
||||
|
||||
_identity_manager.setHandshakeFailedCallback(
|
||||
[this](const Bytes& mac, const std::string& reason) {
|
||||
onHandshakeFailed(mac, reason);
|
||||
});
|
||||
|
||||
_identity_manager.setMacRotationCallback(
|
||||
[this](const Bytes& old_mac, const Bytes& new_mac, const Bytes& identity) {
|
||||
onMacRotation(old_mac, new_mac, identity);
|
||||
});
|
||||
|
||||
// Reassembler callbacks
|
||||
_reassembler.setReassemblyCallback(
|
||||
[this](const Bytes& peer_identity, const Bytes& packet) {
|
||||
onPacketReassembled(peer_identity, packet);
|
||||
});
|
||||
|
||||
_reassembler.setTimeoutCallback(
|
||||
[this](const Bytes& peer_identity, const std::string& reason) {
|
||||
onReassemblyTimeout(peer_identity, reason);
|
||||
});
|
||||
}
|
||||
|
||||
void BLEInterface::onScanResult(const ScanResult& result) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
if (!result.has_reticulum_service) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bytes mac = result.address.toBytes();
|
||||
|
||||
// Check if identity prefix suggests this is a known peer at a new MAC (rotation)
|
||||
if (result.identity_prefix.size() >= 3) {
|
||||
Bytes known_identity = _identity_manager.findIdentityByPrefix(result.identity_prefix);
|
||||
if (known_identity.size() == Limits::IDENTITY_SIZE) {
|
||||
Bytes old_mac = _identity_manager.getMacForIdentity(known_identity);
|
||||
if (old_mac.size() > 0 && old_mac != mac) {
|
||||
// MAC rotation detected! Update mapping
|
||||
INFO("BLEInterface: MAC rotation detected for identity " +
|
||||
known_identity.toHex().substr(0, 8) + "...: " +
|
||||
BLEAddress(old_mac.data()).toString() + " -> " +
|
||||
result.address.toString());
|
||||
_identity_manager.updateMacForIdentity(known_identity, mac);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add to peer manager with address type
|
||||
_peer_manager.addDiscoveredPeer(mac, result.rssi, result.address.type);
|
||||
|
||||
INFO("BLEInterface: Discovered Reticulum peer " + result.address.toString() +
|
||||
" type=" + std::to_string(result.address.type) +
|
||||
" RSSI=" + std::to_string(result.rssi) + " name=" + result.name);
|
||||
}
|
||||
|
||||
void BLEInterface::onConnected(const ConnectionHandle& conn) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
Bytes mac = conn.peer_address.toBytes();
|
||||
|
||||
// Update peer state
|
||||
_peer_manager.setPeerState(mac, PeerState::HANDSHAKING);
|
||||
_peer_manager.setPeerHandle(mac, conn.handle);
|
||||
|
||||
// Mark as central connection (we initiated the connection)
|
||||
PeerInfo* peer = _peer_manager.getPeerByMac(mac);
|
||||
if (peer) {
|
||||
peer->is_central = true; // We ARE central in this connection
|
||||
INFO("BLEInterface: Stored conn_handle=" + std::to_string(conn.handle) +
|
||||
" for peer " + conn.peer_address.toString());
|
||||
}
|
||||
|
||||
DEBUG("BLEInterface: Connected to " + conn.peer_address.toString() +
|
||||
" (we are central)");
|
||||
|
||||
// Discover services
|
||||
_platform->discoverServices(conn.handle);
|
||||
}
|
||||
|
||||
void BLEInterface::onDisconnected(const ConnectionHandle& conn, uint8_t reason) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
Bytes mac = conn.peer_address.toBytes();
|
||||
Bytes identity = _identity_manager.getIdentityForMac(mac);
|
||||
|
||||
if (identity.size() > 0) {
|
||||
// Clean up identity-keyed peer
|
||||
_fragmenters.erase(identity);
|
||||
_reassembler.clearForPeer(identity);
|
||||
_peer_manager.setPeerState(identity, PeerState::DISCOVERED);
|
||||
} else {
|
||||
// Peer might still be in CONNECTING state (no identity yet)
|
||||
// Reset to DISCOVERED so we can try again
|
||||
_peer_manager.connectionFailed(mac);
|
||||
}
|
||||
|
||||
_identity_manager.removeMapping(mac);
|
||||
|
||||
DEBUG("BLEInterface: Disconnected from " + conn.peer_address.toString() +
|
||||
" reason: " + std::to_string(reason));
|
||||
}
|
||||
|
||||
void BLEInterface::onMTUChanged(const ConnectionHandle& conn, uint16_t mtu) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
Bytes mac = conn.peer_address.toBytes();
|
||||
_peer_manager.setPeerMTU(mac, mtu);
|
||||
|
||||
// Update fragmenter if exists
|
||||
Bytes identity = _identity_manager.getIdentityForMac(mac);
|
||||
if (identity.size() > 0) {
|
||||
auto it = _fragmenters.find(identity);
|
||||
if (it != _fragmenters.end()) {
|
||||
it->second.setMTU(mtu);
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG("BLEInterface: MTU changed to " + std::to_string(mtu) +
|
||||
" for " + conn.peer_address.toString());
|
||||
}
|
||||
|
||||
void BLEInterface::onServicesDiscovered(const ConnectionHandle& conn, bool success) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
if (!success) {
|
||||
WARNING("BLEInterface: Service discovery failed for " + conn.peer_address.toString());
|
||||
|
||||
// Clean up peer state - NimBLE may have already disconnected internally,
|
||||
// so onDisconnected callback might not fire. Manually reset peer state.
|
||||
Bytes mac = conn.peer_address.toBytes();
|
||||
_peer_manager.connectionFailed(mac);
|
||||
|
||||
// Try to disconnect (may be no-op if already disconnected)
|
||||
_platform->disconnect(conn.handle);
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG("BLEInterface: Services discovered for " + conn.peer_address.toString());
|
||||
|
||||
// Enable notifications on TX characteristic
|
||||
_platform->enableNotifications(conn.handle, true);
|
||||
|
||||
// Protocol v2.2: Read peer's identity characteristic before sending ours
|
||||
// This matches the Kotlin implementation's 4-step handshake
|
||||
if (conn.identity_handle != 0) {
|
||||
Bytes mac = conn.peer_address.toBytes();
|
||||
uint16_t handle = conn.handle;
|
||||
|
||||
_platform->read(conn.handle, conn.identity_handle,
|
||||
[this, mac, handle](OperationResult result, const Bytes& identity) {
|
||||
if (result == OperationResult::SUCCESS &&
|
||||
identity.size() == Limits::IDENTITY_SIZE) {
|
||||
DEBUG("BLEInterface: Read peer identity: " + identity.toHex().substr(0, 8) + "...");
|
||||
|
||||
// Store the peer's identity - handshake complete for receiving direction
|
||||
_identity_manager.completeHandshake(mac, identity, true);
|
||||
|
||||
// Now send our identity directly (don't use initiateHandshake which
|
||||
// creates a session that would time out since we already have the mapping)
|
||||
if (_identity_manager.hasLocalIdentity()) {
|
||||
_platform->write(handle, _identity_manager.getLocalIdentity(), true);
|
||||
DEBUG("BLEInterface: Sent identity handshake to peer");
|
||||
}
|
||||
} else {
|
||||
WARNING("BLEInterface: Failed to read peer identity, trying write-based handshake");
|
||||
// Fall back to old behavior - initiate handshake and wait for response
|
||||
ConnectionHandle conn = _platform->getConnection(handle);
|
||||
if (conn.handle != 0) {
|
||||
initiateHandshake(conn);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No identity characteristic - fall back to write-only handshake (Protocol v1)
|
||||
DEBUG("BLEInterface: No identity characteristic, using v1 fallback");
|
||||
initiateHandshake(conn);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEInterface::onDataReceived(const ConnectionHandle& conn, const Bytes& data) {
|
||||
// Called when we receive notification from peripheral (we are central)
|
||||
handleIncomingData(conn, data);
|
||||
}
|
||||
|
||||
void BLEInterface::onCentralConnected(const ConnectionHandle& conn) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
Bytes mac = conn.peer_address.toBytes();
|
||||
|
||||
// Update peer manager
|
||||
_peer_manager.addDiscoveredPeer(mac, 0);
|
||||
_peer_manager.setPeerState(mac, PeerState::HANDSHAKING);
|
||||
_peer_manager.setPeerHandle(mac, conn.handle);
|
||||
|
||||
// Mark as peripheral connection (they are central, we are peripheral)
|
||||
PeerInfo* peer = _peer_manager.getPeerByMac(mac);
|
||||
if (peer) {
|
||||
peer->is_central = false; // We are NOT central in this connection
|
||||
}
|
||||
|
||||
DEBUG("BLEInterface: Central connected: " + conn.peer_address.toString() +
|
||||
" (we are peripheral)");
|
||||
}
|
||||
|
||||
void BLEInterface::onCentralDisconnected(const ConnectionHandle& conn) {
|
||||
onDisconnected(conn, 0);
|
||||
}
|
||||
|
||||
void BLEInterface::onWriteReceived(const ConnectionHandle& conn, const Bytes& data) {
|
||||
// Called when central writes to our RX characteristic (we are peripheral)
|
||||
handleIncomingData(conn, data);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Handshake Callbacks
|
||||
//=============================================================================
|
||||
|
||||
void BLEInterface::onHandshakeComplete(const Bytes& mac, const Bytes& identity, bool is_central) {
|
||||
// Lock before modifying queue - protects against race with loop()
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
// Queue the handshake for processing in loop() to avoid stack overflow in NimBLE callback
|
||||
// The NimBLE task has limited stack space, so we defer heavy processing
|
||||
if (_pending_handshakes.size() >= MAX_PENDING_HANDSHAKES) {
|
||||
WARNING("BLEInterface: Pending handshake queue full, dropping handshake");
|
||||
return;
|
||||
}
|
||||
PendingHandshake pending;
|
||||
pending.mac = mac;
|
||||
pending.identity = identity;
|
||||
pending.is_central = is_central;
|
||||
_pending_handshakes.push_back(pending);
|
||||
DEBUG("BLEInterface::onHandshakeComplete: Queued handshake for deferred processing");
|
||||
}
|
||||
|
||||
void BLEInterface::onHandshakeFailed(const Bytes& mac, const std::string& reason) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
WARNING("BLEInterface: Handshake failed with " +
|
||||
BLEAddress(mac.data()).toString() + ": " + reason);
|
||||
|
||||
_peer_manager.connectionFailed(mac);
|
||||
}
|
||||
|
||||
void BLEInterface::onMacRotation(const Bytes& old_mac, const Bytes& new_mac, const Bytes& identity) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
INFO("BLEInterface: MAC rotation detected for identity " +
|
||||
identity.toHex().substr(0, 8) + "...: " +
|
||||
BLEAddress(old_mac.data()).toString() + " -> " +
|
||||
BLEAddress(new_mac.data()).toString());
|
||||
|
||||
// Update peer manager with new MAC
|
||||
_peer_manager.updatePeerMac(identity, new_mac);
|
||||
|
||||
// Update fragmenter key if exists (identity stays the same, but log it)
|
||||
auto frag_it = _fragmenters.find(identity);
|
||||
if (frag_it != _fragmenters.end()) {
|
||||
DEBUG("BLEInterface: Fragmenter preserved for rotated identity");
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Reassembly Callbacks
|
||||
//=============================================================================
|
||||
|
||||
void BLEInterface::onPacketReassembled(const Bytes& peer_identity, const Bytes& packet) {
|
||||
// Packet reassembly complete - pass to transport
|
||||
_peer_manager.recordPacketReceived(peer_identity);
|
||||
handle_incoming(packet);
|
||||
}
|
||||
|
||||
void BLEInterface::onReassemblyTimeout(const Bytes& peer_identity, const std::string& reason) {
|
||||
WARNING("BLEInterface: Reassembly timeout for " +
|
||||
peer_identity.toHex().substr(0, 8) + ": " + reason);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Internal Operations
|
||||
//=============================================================================
|
||||
|
||||
void BLEInterface::performScan() {
|
||||
if (!_platform || _platform->isScanning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only scan if we have room for more connections
|
||||
if (_peer_manager.connectedCount() >= _max_connections) {
|
||||
return;
|
||||
}
|
||||
|
||||
_platform->startScan(5000); // 5 second scan
|
||||
}
|
||||
|
||||
void BLEInterface::processDiscoveredPeers() {
|
||||
// Don't attempt connections when memory is critically low
|
||||
// BLE connection setup requires significant heap allocation
|
||||
#ifdef ARDUINO
|
||||
if (ESP.getFreeHeap() < 30000) {
|
||||
static uint32_t last_low_mem_warn = 0;
|
||||
if (millis() - last_low_mem_warn > 10000) {
|
||||
WARNING("BLEInterface: Skipping connection attempts - low memory");
|
||||
last_low_mem_warn = millis();
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Don't try to connect while scanning - BLE stack will return "busy"
|
||||
if (_platform->isScanning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cooldown after connection attempts to let BLE stack settle
|
||||
double now = Utilities::OS::time();
|
||||
if (now - _last_connection_attempt < CONNECTION_COOLDOWN) {
|
||||
return; // Still in cooldown period
|
||||
}
|
||||
|
||||
// Find best connection candidate
|
||||
PeerInfo* candidate = _peer_manager.getBestConnectionCandidate();
|
||||
|
||||
// Debug: log all peers and why they may not be candidates
|
||||
static double last_peer_log = 0;
|
||||
if (now - last_peer_log >= 10.0) {
|
||||
auto all_peers = _peer_manager.getAllPeers();
|
||||
DEBUG("BLEInterface: Peer count=" + std::to_string(all_peers.size()) +
|
||||
" localMAC=" + _peer_manager.getLocalMac().toHex());
|
||||
for (PeerInfo* peer : all_peers) {
|
||||
bool should_initiate = _peer_manager.shouldInitiateConnection(peer->mac_address);
|
||||
DEBUG("BLEInterface: Peer " + BLEAddress(peer->mac_address.data()).toString() +
|
||||
" state=" + std::to_string(static_cast<int>(peer->state)) +
|
||||
" shouldInitiate=" + std::string(should_initiate ? "yes" : "no") +
|
||||
" score=" + std::to_string(peer->score));
|
||||
}
|
||||
last_peer_log = now;
|
||||
}
|
||||
|
||||
if (candidate) {
|
||||
DEBUG("BLEInterface: Connection candidate: " + BLEAddress(candidate->mac_address.data()).toString() +
|
||||
" type=" + std::to_string(candidate->address_type) +
|
||||
" canAccept=" + std::string(_peer_manager.canAcceptConnection() ? "yes" : "no"));
|
||||
}
|
||||
|
||||
if (candidate && _peer_manager.canAcceptConnection()) {
|
||||
_peer_manager.setPeerState(candidate->mac_address, PeerState::CONNECTING);
|
||||
candidate->connection_attempts++;
|
||||
|
||||
// Use stored address type for correct connection
|
||||
BLEAddress addr(candidate->mac_address.data(), candidate->address_type);
|
||||
INFO("BLEInterface: Connecting to " + addr.toString() + " type=" + std::to_string(candidate->address_type));
|
||||
|
||||
// Mark connection attempt time for cooldown
|
||||
_last_connection_attempt = now;
|
||||
|
||||
// Handle immediate connection failure (resets state for retry)
|
||||
// Reduced timeout from 10s to 3s to avoid long UI freezes
|
||||
if (!_platform->connect(addr, 3000)) {
|
||||
WARNING("BLEInterface: Connection attempt failed immediately");
|
||||
_peer_manager.connectionFailed(candidate->mac_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLEInterface::sendKeepalives() {
|
||||
// Send empty keepalive to maintain connections
|
||||
Bytes keepalive(1);
|
||||
keepalive.writable(1)[0] = 0x00;
|
||||
|
||||
auto connected = _peer_manager.getConnectedPeers();
|
||||
for (PeerInfo* peer : connected) {
|
||||
if (peer->hasIdentity()) {
|
||||
// Don't use sendToPeer for keepalives (no fragmentation needed)
|
||||
if (peer->is_central) {
|
||||
_platform->write(peer->conn_handle, keepalive, false);
|
||||
} else {
|
||||
_platform->notify(peer->conn_handle, keepalive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLEInterface::performMaintenance() {
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
// Check reassembly timeouts
|
||||
_reassembler.checkTimeouts();
|
||||
|
||||
// Check handshake timeouts
|
||||
_identity_manager.checkTimeouts();
|
||||
|
||||
// Check blacklist expirations
|
||||
_peer_manager.checkBlacklistExpirations();
|
||||
|
||||
// Recalculate peer scores
|
||||
_peer_manager.recalculateScores();
|
||||
|
||||
// Clean up stale peers
|
||||
_peer_manager.cleanupStalePeers();
|
||||
|
||||
// Clean up fragmenters for peers that no longer exist
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
std::vector<Bytes> orphaned_fragmenters;
|
||||
for (const auto& kv : _fragmenters) {
|
||||
if (!_peer_manager.getPeerByIdentity(kv.first)) {
|
||||
orphaned_fragmenters.push_back(kv.first);
|
||||
}
|
||||
}
|
||||
for (const Bytes& identity : orphaned_fragmenters) {
|
||||
_fragmenters.erase(identity);
|
||||
_reassembler.clearForPeer(identity);
|
||||
TRACE("BLEInterface: Cleaned up orphaned fragmenter for " + identity.toHex().substr(0, 8));
|
||||
}
|
||||
}
|
||||
|
||||
// Process discovered peers (try to connect)
|
||||
processDiscoveredPeers();
|
||||
}
|
||||
|
||||
void BLEInterface::handleIncomingData(const ConnectionHandle& conn, const Bytes& data) {
|
||||
// Hot path - no logging to avoid blocking main loop
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
Bytes mac = conn.peer_address.toBytes();
|
||||
bool is_central = (conn.local_role == Role::CENTRAL);
|
||||
|
||||
// First check if this is an identity handshake
|
||||
if (_identity_manager.processReceivedData(mac, data, is_central)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for keepalive (1 byte, value 0x00)
|
||||
if (data.size() == 1 && data.data()[0] == 0x00) {
|
||||
_peer_manager.updateLastActivity(_identity_manager.getIdentityForMac(mac));
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue data for deferred processing (avoid stack overflow in NimBLE callback)
|
||||
Bytes identity = _identity_manager.getIdentityForMac(mac);
|
||||
if (identity.size() == 0) {
|
||||
WARNING("BLEInterface: Received data from peer without identity");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pending_data.size() >= MAX_PENDING_DATA) {
|
||||
WARNING("BLEInterface: Pending data queue full, dropping data");
|
||||
return;
|
||||
}
|
||||
|
||||
PendingData pending;
|
||||
pending.identity = identity;
|
||||
pending.data = data;
|
||||
_pending_data.push_back(pending);
|
||||
}
|
||||
|
||||
void BLEInterface::initiateHandshake(const ConnectionHandle& conn) {
|
||||
Bytes mac = conn.peer_address.toBytes();
|
||||
|
||||
// Get handshake data (our identity)
|
||||
Bytes handshake = _identity_manager.initiateHandshake(mac);
|
||||
|
||||
if (handshake.size() > 0) {
|
||||
// Write our identity to peer's RX characteristic
|
||||
_platform->write(conn.handle, handshake, true);
|
||||
|
||||
DEBUG("BLEInterface: Sent identity handshake to " + conn.peer_address.toString());
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// FreeRTOS Task Support
|
||||
//=============================================================================
|
||||
|
||||
#ifdef ARDUINO
|
||||
|
||||
void BLEInterface::ble_task(void* param) {
|
||||
BLEInterface* self = static_cast<BLEInterface*>(param);
|
||||
Serial.printf("BLE task started on core %d\n", xPortGetCoreID());
|
||||
|
||||
while (true) {
|
||||
// Run the BLE loop (already has internal mutex protection)
|
||||
self->loop();
|
||||
|
||||
// Yield to other tasks
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
|
||||
bool BLEInterface::start_task(int priority, int core) {
|
||||
if (_task_handle != nullptr) {
|
||||
WARNING("BLEInterface: Task already running");
|
||||
return true;
|
||||
}
|
||||
|
||||
BaseType_t result = xTaskCreatePinnedToCore(
|
||||
ble_task,
|
||||
"ble",
|
||||
8192, // 8KB stack
|
||||
this,
|
||||
priority,
|
||||
&_task_handle,
|
||||
core
|
||||
);
|
||||
|
||||
if (result != pdPASS) {
|
||||
ERROR("BLEInterface: Failed to create BLE task");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("BLE task created with priority %d on core %d\n", priority, core);
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Non-Arduino stub
|
||||
bool BLEInterface::start_task(int priority, int core) {
|
||||
WARNING("BLEInterface: Task mode not supported on this platform");
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* @file BLEInterface.h
|
||||
* @brief BLE-Reticulum Protocol v2.2 interface for microReticulum
|
||||
*
|
||||
* Main BLEInterface class that integrates with the Reticulum transport layer.
|
||||
* Supports dual-mode operation (central + peripheral) for mesh networking.
|
||||
*
|
||||
* Usage:
|
||||
* BLEInterface ble("ble0");
|
||||
* ble.setDeviceName("my-node");
|
||||
* ble.setLocalIdentity(identity.hash());
|
||||
*
|
||||
* Interface interface(&ble);
|
||||
* interface.start();
|
||||
* Transport::register_interface(interface);
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "Interface.h"
|
||||
#include "Bytes.h"
|
||||
#include "Type.h"
|
||||
#include "BLE/BLETypes.h"
|
||||
#include "BLE/BLEPlatform.h"
|
||||
#include "BLE/BLEFragmenter.h"
|
||||
#include "BLE/BLEReassembler.h"
|
||||
#include "BLE/BLEPeerManager.h"
|
||||
#include "BLE/BLEIdentityManager.h"
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Reticulum BLE Interface
|
||||
*
|
||||
* Implements the BLE-Reticulum protocol v2.2 as a microReticulum interface.
|
||||
* Manages BLE connections, fragmentation, and peer discovery.
|
||||
*/
|
||||
class BLEInterface : public RNS::InterfaceImpl {
|
||||
public:
|
||||
// Protocol constants
|
||||
static constexpr uint32_t BITRATE_GUESS = 100000; // ~100 kbps effective throughput
|
||||
static constexpr uint16_t HW_MTU_DEFAULT = 512; // Default after MTU negotiation
|
||||
|
||||
// Timing constants
|
||||
static constexpr double SCAN_INTERVAL = 5.0; // Seconds between scans
|
||||
static constexpr double KEEPALIVE_INTERVAL = 15.0; // Seconds between keepalives
|
||||
static constexpr double MAINTENANCE_INTERVAL = 1.0; // Seconds between maintenance
|
||||
static constexpr double CONNECTION_COOLDOWN = 3.0; // Seconds to wait after connection failure
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a BLE interface
|
||||
* @param name Interface name (e.g., "ble0")
|
||||
*/
|
||||
explicit BLEInterface(const char* name = "BLEInterface");
|
||||
|
||||
virtual ~BLEInterface();
|
||||
|
||||
//=========================================================================
|
||||
// Configuration (call before start())
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Set the BLE role
|
||||
* @param role CENTRAL, PERIPHERAL, or DUAL (default: DUAL)
|
||||
*/
|
||||
void setRole(RNS::BLE::Role role);
|
||||
|
||||
/**
|
||||
* @brief Set the advertised device name
|
||||
* @param name Device name (max ~8 characters recommended)
|
||||
*/
|
||||
void setDeviceName(const std::string& name);
|
||||
|
||||
/**
|
||||
* @brief Set our local identity hash
|
||||
*
|
||||
* Required for the identity handshake protocol.
|
||||
* Should be the first 16 bytes of the transport identity hash.
|
||||
*
|
||||
* @param identity 16-byte identity hash
|
||||
*/
|
||||
void setLocalIdentity(const RNS::Bytes& identity);
|
||||
|
||||
/**
|
||||
* @brief Set maximum connections
|
||||
* @param max Maximum simultaneous connections (default: 7)
|
||||
*/
|
||||
void setMaxConnections(uint8_t max);
|
||||
|
||||
//=========================================================================
|
||||
// InterfaceImpl Overrides
|
||||
//=========================================================================
|
||||
|
||||
virtual bool start() override;
|
||||
virtual void stop() override;
|
||||
virtual void loop() override;
|
||||
|
||||
virtual std::string toString() const override {
|
||||
return "BLEInterface[" + _name + "/" + _device_name + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get interface statistics
|
||||
* @return Map with central_connections and peripheral_connections counts
|
||||
*/
|
||||
virtual std::map<std::string, float> get_stats() const override;
|
||||
|
||||
//=========================================================================
|
||||
// Status
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Summary info for a connected peer (fixed-size, no heap allocation)
|
||||
*/
|
||||
struct PeerSummary {
|
||||
char identity[14]; // First 12 hex chars + null, or empty if unknown
|
||||
char mac[18]; // "AA:BB:CC:DD:EE:FF" format
|
||||
int8_t rssi;
|
||||
};
|
||||
static constexpr size_t MAX_PEER_SUMMARIES = 8;
|
||||
|
||||
/**
|
||||
* @brief Get count of connected peers
|
||||
*/
|
||||
size_t peerCount() const;
|
||||
|
||||
/**
|
||||
* @brief Get summaries of connected peers (for UI display)
|
||||
* @param out Pre-allocated array to fill
|
||||
* @param max_count Maximum entries to fill
|
||||
* @return Actual number of entries filled
|
||||
*/
|
||||
size_t getConnectedPeerSummaries(PeerSummary* out, size_t max_count) const;
|
||||
|
||||
/**
|
||||
* @brief Check if BLE is running
|
||||
*/
|
||||
bool isRunning() const { return _platform && _platform->isRunning(); }
|
||||
|
||||
/**
|
||||
* @brief Start BLE on its own FreeRTOS task
|
||||
*
|
||||
* This allows BLE operations to run independently of the main loop,
|
||||
* preventing UI freezes during scans and connections.
|
||||
*
|
||||
* @param priority Task priority (default 1)
|
||||
* @param core Core to pin the task to (default 0, where BT controller runs)
|
||||
* @return true if task started successfully
|
||||
*/
|
||||
bool start_task(int priority = 1, int core = 0);
|
||||
|
||||
/**
|
||||
* @brief Check if BLE is running on its own task
|
||||
*/
|
||||
bool is_task_running() const { return _task_handle != nullptr; }
|
||||
|
||||
protected:
|
||||
virtual void send_outgoing(const RNS::Bytes& data) override;
|
||||
|
||||
private:
|
||||
//=========================================================================
|
||||
// Platform Callbacks
|
||||
//=========================================================================
|
||||
|
||||
void onScanResult(const RNS::BLE::ScanResult& result);
|
||||
void onConnected(const RNS::BLE::ConnectionHandle& conn);
|
||||
void onDisconnected(const RNS::BLE::ConnectionHandle& conn, uint8_t reason);
|
||||
void onMTUChanged(const RNS::BLE::ConnectionHandle& conn, uint16_t mtu);
|
||||
void onServicesDiscovered(const RNS::BLE::ConnectionHandle& conn, bool success);
|
||||
void onDataReceived(const RNS::BLE::ConnectionHandle& conn, const RNS::Bytes& data);
|
||||
void onCentralConnected(const RNS::BLE::ConnectionHandle& conn);
|
||||
void onCentralDisconnected(const RNS::BLE::ConnectionHandle& conn);
|
||||
void onWriteReceived(const RNS::BLE::ConnectionHandle& conn, const RNS::Bytes& data);
|
||||
|
||||
//=========================================================================
|
||||
// Handshake Callbacks
|
||||
//=========================================================================
|
||||
|
||||
void onHandshakeComplete(const RNS::Bytes& mac, const RNS::Bytes& identity, bool is_central);
|
||||
void onHandshakeFailed(const RNS::Bytes& mac, const std::string& reason);
|
||||
void onMacRotation(const RNS::Bytes& old_mac, const RNS::Bytes& new_mac, const RNS::Bytes& identity);
|
||||
|
||||
//=========================================================================
|
||||
// Reassembly Callbacks
|
||||
//=========================================================================
|
||||
|
||||
void onPacketReassembled(const RNS::Bytes& peer_identity, const RNS::Bytes& packet);
|
||||
void onReassemblyTimeout(const RNS::Bytes& peer_identity, const std::string& reason);
|
||||
|
||||
//=========================================================================
|
||||
// Internal Operations
|
||||
//=========================================================================
|
||||
|
||||
void setupCallbacks();
|
||||
void performScan();
|
||||
void processDiscoveredPeers();
|
||||
void sendKeepalives();
|
||||
void performMaintenance();
|
||||
|
||||
/**
|
||||
* @brief Send data to a specific peer (with fragmentation)
|
||||
*/
|
||||
bool sendToPeer(const RNS::Bytes& peer_identity, const RNS::Bytes& data);
|
||||
|
||||
/**
|
||||
* @brief Process incoming data from a peer
|
||||
*/
|
||||
void handleIncomingData(const RNS::BLE::ConnectionHandle& conn, const RNS::Bytes& data);
|
||||
|
||||
/**
|
||||
* @brief Initiate handshake for a new connection
|
||||
*/
|
||||
void initiateHandshake(const RNS::BLE::ConnectionHandle& conn);
|
||||
|
||||
//=========================================================================
|
||||
// Configuration
|
||||
//=========================================================================
|
||||
|
||||
RNS::BLE::Role _role = RNS::BLE::Role::DUAL;
|
||||
std::string _device_name = "RNS-Node";
|
||||
uint8_t _max_connections = RNS::BLE::Limits::MAX_PEERS;
|
||||
RNS::Bytes _local_identity;
|
||||
|
||||
//=========================================================================
|
||||
// Components
|
||||
//=========================================================================
|
||||
|
||||
RNS::BLE::IBLEPlatform::Ptr _platform;
|
||||
RNS::BLE::BLEPeerManager _peer_manager;
|
||||
RNS::BLE::BLEIdentityManager _identity_manager;
|
||||
RNS::BLE::BLEReassembler _reassembler;
|
||||
|
||||
// Per-peer fragmenters (keyed by identity)
|
||||
std::map<RNS::Bytes, RNS::BLE::BLEFragmenter> _fragmenters;
|
||||
|
||||
//=========================================================================
|
||||
// State
|
||||
//=========================================================================
|
||||
|
||||
double _last_scan = 0;
|
||||
double _last_keepalive = 0;
|
||||
double _last_maintenance = 0;
|
||||
double _last_connection_attempt = 0; // Cooldown after connection failures
|
||||
|
||||
// Pending handshake completions (deferred from callback to loop for stack safety)
|
||||
static constexpr size_t MAX_PENDING_HANDSHAKES = 32;
|
||||
struct PendingHandshake {
|
||||
RNS::Bytes mac;
|
||||
RNS::Bytes identity;
|
||||
bool is_central;
|
||||
};
|
||||
std::vector<PendingHandshake> _pending_handshakes;
|
||||
|
||||
// Pending data fragments (deferred from callback to loop for stack safety)
|
||||
static constexpr size_t MAX_PENDING_DATA = 64;
|
||||
struct PendingData {
|
||||
RNS::Bytes identity;
|
||||
RNS::Bytes data;
|
||||
};
|
||||
std::vector<PendingData> _pending_data;
|
||||
|
||||
// Thread safety for callbacks from BLE stack
|
||||
// Using recursive_mutex because handleIncomingData holds the lock while
|
||||
// calling processReceivedData, which can trigger onHandshakeComplete callback
|
||||
// that also needs the lock
|
||||
mutable std::recursive_mutex _mutex;
|
||||
|
||||
//=========================================================================
|
||||
// FreeRTOS Task Support
|
||||
//=========================================================================
|
||||
|
||||
TaskHandle_t _task_handle = nullptr;
|
||||
static void ble_task(void* param);
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @file BLEOperationQueue.cpp
|
||||
* @brief GATT operation queue implementation
|
||||
*/
|
||||
|
||||
#include "BLEOperationQueue.h"
|
||||
#include "Log.h"
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
BLEOperationQueue::BLEOperationQueue() {
|
||||
}
|
||||
|
||||
void BLEOperationQueue::enqueue(GATTOperation op) {
|
||||
op.queued_at = Utilities::OS::time();
|
||||
|
||||
if (op.timeout_ms == 0) {
|
||||
op.timeout_ms = _default_timeout_ms;
|
||||
}
|
||||
|
||||
_queue.push(std::move(op));
|
||||
|
||||
TRACE("BLEOperationQueue: Enqueued operation, queue depth: " +
|
||||
std::to_string(_queue.size()));
|
||||
}
|
||||
|
||||
bool BLEOperationQueue::process() {
|
||||
// Check for timeout on current operation
|
||||
if (_has_current_op) {
|
||||
checkTimeout();
|
||||
return false; // Still busy
|
||||
}
|
||||
|
||||
// Nothing to process
|
||||
if (_queue.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dequeue next operation
|
||||
_current_op = std::move(_queue.front());
|
||||
_has_current_op = true;
|
||||
_queue.pop();
|
||||
|
||||
GATTOperation& op = _current_op;
|
||||
op.started_at = Utilities::OS::time();
|
||||
|
||||
TRACE("BLEOperationQueue: Starting operation type " +
|
||||
std::to_string(static_cast<int>(op.type)));
|
||||
|
||||
// Execute the operation (implemented by subclass)
|
||||
bool started = executeOperation(op);
|
||||
|
||||
if (!started) {
|
||||
// Operation failed to start - call callback with error
|
||||
if (op.callback) {
|
||||
op.callback(OperationResult::ERROR, Bytes());
|
||||
}
|
||||
_has_current_op = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEOperationQueue::complete(OperationResult result, const Bytes& response_data) {
|
||||
if (!_has_current_op) {
|
||||
WARNING("BLEOperationQueue: complete() called with no current operation");
|
||||
return;
|
||||
}
|
||||
|
||||
GATTOperation& op = _current_op;
|
||||
|
||||
double duration = Utilities::OS::time() - op.started_at;
|
||||
TRACE("BLEOperationQueue: Operation completed in " +
|
||||
std::to_string(static_cast<int>(duration * 1000)) + "ms, result: " +
|
||||
std::to_string(static_cast<int>(result)));
|
||||
|
||||
// Invoke callback
|
||||
if (op.callback) {
|
||||
op.callback(result, response_data);
|
||||
}
|
||||
|
||||
// Clear current operation
|
||||
_has_current_op = false;
|
||||
}
|
||||
|
||||
void BLEOperationQueue::clearForConnection(uint16_t conn_handle) {
|
||||
// Create temporary queue for non-matching operations
|
||||
std::queue<GATTOperation> remaining;
|
||||
|
||||
while (!_queue.empty()) {
|
||||
GATTOperation op = std::move(_queue.front());
|
||||
_queue.pop();
|
||||
|
||||
if (op.conn_handle != conn_handle) {
|
||||
remaining.push(std::move(op));
|
||||
} else {
|
||||
// Cancel this operation
|
||||
if (op.callback) {
|
||||
op.callback(OperationResult::DISCONNECTED, Bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_queue = std::move(remaining);
|
||||
|
||||
// Also cancel current operation if it matches
|
||||
if (_has_current_op && _current_op.conn_handle == conn_handle) {
|
||||
if (_current_op.callback) {
|
||||
_current_op.callback(OperationResult::DISCONNECTED, Bytes());
|
||||
}
|
||||
_has_current_op = false;
|
||||
}
|
||||
|
||||
TRACE("BLEOperationQueue: Cleared operations for connection " +
|
||||
std::to_string(conn_handle));
|
||||
}
|
||||
|
||||
void BLEOperationQueue::clear() {
|
||||
// Cancel all pending operations
|
||||
while (!_queue.empty()) {
|
||||
GATTOperation op = std::move(_queue.front());
|
||||
_queue.pop();
|
||||
|
||||
if (op.callback) {
|
||||
op.callback(OperationResult::DISCONNECTED, Bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel current operation
|
||||
if (_has_current_op) {
|
||||
if (_current_op.callback) {
|
||||
_current_op.callback(OperationResult::DISCONNECTED, Bytes());
|
||||
}
|
||||
_has_current_op = false;
|
||||
}
|
||||
|
||||
TRACE("BLEOperationQueue: Cleared all operations");
|
||||
}
|
||||
|
||||
void BLEOperationQueue::checkTimeout() {
|
||||
if (!_has_current_op) {
|
||||
return;
|
||||
}
|
||||
|
||||
GATTOperation& op = _current_op;
|
||||
double elapsed = Utilities::OS::time() - op.started_at;
|
||||
double timeout_sec = op.timeout_ms / 1000.0;
|
||||
|
||||
if (elapsed > timeout_sec) {
|
||||
WARNING("BLEOperationQueue: Operation timed out after " +
|
||||
std::to_string(static_cast<int>(elapsed * 1000)) + "ms");
|
||||
|
||||
// Complete with timeout error
|
||||
complete(OperationResult::TIMEOUT, Bytes());
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// GATTOperationBuilder
|
||||
//=============================================================================
|
||||
|
||||
GATTOperationBuilder& GATTOperationBuilder::read(uint16_t conn_handle, uint16_t char_handle) {
|
||||
_op.type = OperationType::READ;
|
||||
_op.conn_handle = conn_handle;
|
||||
_op.char_handle = char_handle;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GATTOperationBuilder& GATTOperationBuilder::write(uint16_t conn_handle, uint16_t char_handle,
|
||||
const Bytes& data) {
|
||||
_op.type = OperationType::WRITE;
|
||||
_op.conn_handle = conn_handle;
|
||||
_op.char_handle = char_handle;
|
||||
_op.data = data;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GATTOperationBuilder& GATTOperationBuilder::writeNoResponse(uint16_t conn_handle,
|
||||
uint16_t char_handle,
|
||||
const Bytes& data) {
|
||||
_op.type = OperationType::WRITE_NO_RESPONSE;
|
||||
_op.conn_handle = conn_handle;
|
||||
_op.char_handle = char_handle;
|
||||
_op.data = data;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GATTOperationBuilder& GATTOperationBuilder::enableNotify(uint16_t conn_handle) {
|
||||
_op.type = OperationType::NOTIFY_ENABLE;
|
||||
_op.conn_handle = conn_handle;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GATTOperationBuilder& GATTOperationBuilder::disableNotify(uint16_t conn_handle) {
|
||||
_op.type = OperationType::NOTIFY_DISABLE;
|
||||
_op.conn_handle = conn_handle;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GATTOperationBuilder& GATTOperationBuilder::requestMTU(uint16_t conn_handle, uint16_t mtu) {
|
||||
_op.type = OperationType::MTU_REQUEST;
|
||||
_op.conn_handle = conn_handle;
|
||||
// Store requested MTU in data (as 2-byte big-endian)
|
||||
_op.data = Bytes(2);
|
||||
uint8_t* ptr = _op.data.writable(2);
|
||||
ptr[0] = static_cast<uint8_t>((mtu >> 8) & 0xFF);
|
||||
ptr[1] = static_cast<uint8_t>(mtu & 0xFF);
|
||||
return *this;
|
||||
}
|
||||
|
||||
GATTOperationBuilder& GATTOperationBuilder::withTimeout(uint32_t timeout_ms) {
|
||||
_op.timeout_ms = timeout_ms;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GATTOperationBuilder& GATTOperationBuilder::withCallback(
|
||||
std::function<void(OperationResult, const Bytes&)> callback) {
|
||||
_op.callback = callback;
|
||||
return *this;
|
||||
}
|
||||
|
||||
GATTOperation GATTOperationBuilder::build() {
|
||||
return std::move(_op);
|
||||
}
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @file BLEOperationQueue.h
|
||||
* @brief GATT operation queue for serializing BLE operations
|
||||
*
|
||||
* BLE stacks typically do not queue operations internally - attempting to
|
||||
* perform multiple GATT operations simultaneously leads to failures or
|
||||
* undefined behavior. This queue ensures operations are processed one at
|
||||
* a time in order.
|
||||
*
|
||||
* Platform implementations inherit from this class and implement
|
||||
* executeOperation() to perform the actual BLE stack calls.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "BLETypes.h"
|
||||
#include "Bytes.h"
|
||||
#include "Utilities/OS.h"
|
||||
|
||||
#include <queue>
|
||||
#include <functional>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
/**
|
||||
* @brief Base class for GATT operation queuing
|
||||
*
|
||||
* Subclasses must implement executeOperation() to perform the actual
|
||||
* BLE stack calls. Call process() from the main loop to execute queued
|
||||
* operations, and complete() from BLE callbacks to signal completion.
|
||||
*/
|
||||
class BLEOperationQueue {
|
||||
public:
|
||||
BLEOperationQueue();
|
||||
virtual ~BLEOperationQueue() = default;
|
||||
|
||||
/**
|
||||
* @brief Add operation to queue
|
||||
*
|
||||
* @param op Operation to queue
|
||||
*/
|
||||
void enqueue(GATTOperation op);
|
||||
|
||||
/**
|
||||
* @brief Process queue - call from loop()
|
||||
*
|
||||
* Starts the next operation if none is in progress.
|
||||
* @return true if an operation was started
|
||||
*/
|
||||
bool process();
|
||||
|
||||
/**
|
||||
* @brief Mark current operation complete
|
||||
*
|
||||
* Call this from BLE callbacks when an operation completes.
|
||||
*
|
||||
* @param result Operation result
|
||||
* @param response_data Response data (for reads)
|
||||
*/
|
||||
void complete(OperationResult result, const Bytes& response_data = Bytes());
|
||||
|
||||
/**
|
||||
* @brief Check if operation is in progress
|
||||
*/
|
||||
bool isBusy() const { return _has_current_op; }
|
||||
|
||||
/**
|
||||
* @brief Get current operation (if any)
|
||||
* @return Pointer to current operation, or nullptr if none
|
||||
*/
|
||||
const GATTOperation* currentOperation() const {
|
||||
return _has_current_op ? &_current_op : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear all pending operations for a connection
|
||||
*
|
||||
* Call this when a connection is terminated to remove orphaned operations.
|
||||
*
|
||||
* @param conn_handle Connection handle
|
||||
*/
|
||||
void clearForConnection(uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* @brief Clear entire queue
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @brief Get queue depth
|
||||
*/
|
||||
size_t depth() const { return _queue.size(); }
|
||||
|
||||
/**
|
||||
* @brief Set operation timeout
|
||||
* @param timeout_ms Timeout in milliseconds
|
||||
*/
|
||||
void setTimeout(uint32_t timeout_ms) { _default_timeout_ms = timeout_ms; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Execute a single operation - implement in subclass
|
||||
*
|
||||
* Subclasses must implement this to call platform-specific BLE APIs.
|
||||
* Return true if the operation was started successfully.
|
||||
* Call complete() from the BLE callback when the operation finishes.
|
||||
*
|
||||
* @param op Operation to execute
|
||||
* @return true if operation was started
|
||||
*/
|
||||
virtual bool executeOperation(const GATTOperation& op) = 0;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Check for timeout on current operation
|
||||
*/
|
||||
void checkTimeout();
|
||||
|
||||
std::queue<GATTOperation> _queue;
|
||||
GATTOperation _current_op;
|
||||
bool _has_current_op = false;
|
||||
uint32_t _default_timeout_ms = 5000;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper class for building GATT operations
|
||||
*/
|
||||
class GATTOperationBuilder {
|
||||
public:
|
||||
GATTOperationBuilder& read(uint16_t conn_handle, uint16_t char_handle);
|
||||
GATTOperationBuilder& write(uint16_t conn_handle, uint16_t char_handle, const Bytes& data);
|
||||
GATTOperationBuilder& writeNoResponse(uint16_t conn_handle, uint16_t char_handle, const Bytes& data);
|
||||
GATTOperationBuilder& enableNotify(uint16_t conn_handle);
|
||||
GATTOperationBuilder& disableNotify(uint16_t conn_handle);
|
||||
GATTOperationBuilder& requestMTU(uint16_t conn_handle, uint16_t mtu);
|
||||
GATTOperationBuilder& withTimeout(uint32_t timeout_ms);
|
||||
GATTOperationBuilder& withCallback(std::function<void(OperationResult, const Bytes&)> callback);
|
||||
|
||||
GATTOperation build();
|
||||
|
||||
private:
|
||||
GATTOperation _op;
|
||||
};
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,870 @@
|
||||
/**
|
||||
* @file BLEPeerManager.cpp
|
||||
* @brief BLE-Reticulum Protocol v2.2 peer management implementation
|
||||
*
|
||||
* Uses fixed-size pools instead of STL containers to eliminate heap fragmentation.
|
||||
*/
|
||||
|
||||
#include "BLEPeerManager.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
BLEPeerManager::BLEPeerManager() {
|
||||
_local_mac = Bytes(6); // Initialize to zeros
|
||||
|
||||
// Initialize all pools to empty state
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
_peers_by_identity_pool[i].clear();
|
||||
_peers_by_mac_only_pool[i].clear();
|
||||
}
|
||||
for (size_t i = 0; i < MAC_IDENTITY_POOL_SIZE; i++) {
|
||||
_mac_to_identity_pool[i].clear();
|
||||
}
|
||||
for (size_t i = 0; i < MAX_CONN_HANDLES; i++) {
|
||||
_handle_to_peer[i] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::setLocalMac(const Bytes& mac) {
|
||||
if (mac.size() >= Limits::MAC_SIZE) {
|
||||
_local_mac = Bytes(mac.data(), Limits::MAC_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Peer Discovery
|
||||
//=============================================================================
|
||||
|
||||
bool BLEPeerManager::addDiscoveredPeer(const Bytes& mac_address, int8_t rssi, uint8_t address_type) {
|
||||
if (mac_address.size() < Limits::MAC_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
double now = Utilities::OS::time();
|
||||
|
||||
// Check if this MAC maps to a known identity
|
||||
Bytes identity = getIdentityForMac(mac);
|
||||
if (identity.size() == Limits::IDENTITY_SIZE) {
|
||||
// Update existing peer with identity
|
||||
PeerByIdentitySlot* slot = findPeerByIdentitySlot(identity);
|
||||
if (slot) {
|
||||
PeerInfo& peer = slot->peer;
|
||||
|
||||
// Check if blacklisted
|
||||
if (peer.state == PeerState::BLACKLISTED && now < peer.blacklisted_until) {
|
||||
return false;
|
||||
}
|
||||
|
||||
peer.last_seen = now;
|
||||
peer.rssi = rssi;
|
||||
peer.address_type = address_type; // Update address type
|
||||
// Exponential moving average for RSSI
|
||||
peer.rssi_avg = static_cast<int8_t>(0.7f * peer.rssi_avg + 0.3f * rssi);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if peer exists in MAC-only storage
|
||||
PeerByMacSlot* mac_slot = findPeerByMacSlot(mac);
|
||||
if (mac_slot) {
|
||||
PeerInfo& peer = mac_slot->peer;
|
||||
|
||||
// Check if blacklisted
|
||||
if (peer.state == PeerState::BLACKLISTED && now < peer.blacklisted_until) {
|
||||
return false;
|
||||
}
|
||||
|
||||
peer.last_seen = now;
|
||||
peer.rssi = rssi;
|
||||
peer.address_type = address_type; // Update address type
|
||||
peer.rssi_avg = static_cast<int8_t>(0.7f * peer.rssi_avg + 0.3f * rssi);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// New peer - add to MAC-only storage
|
||||
PeerByMacSlot* empty_slot = findEmptyPeerByMacSlot();
|
||||
if (!empty_slot) {
|
||||
WARNING("BLEPeerManager: MAC-only peer pool is full, cannot add new peer");
|
||||
return false;
|
||||
}
|
||||
|
||||
empty_slot->in_use = true;
|
||||
empty_slot->mac_address = mac;
|
||||
PeerInfo& peer = empty_slot->peer;
|
||||
peer = PeerInfo(); // Reset to defaults
|
||||
peer.mac_address = mac;
|
||||
peer.address_type = address_type;
|
||||
peer.state = PeerState::DISCOVERED;
|
||||
peer.discovered_at = now;
|
||||
peer.last_seen = now;
|
||||
peer.rssi = rssi;
|
||||
peer.rssi_avg = rssi;
|
||||
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), "BLEPeerManager: Discovered new peer %s RSSI %d",
|
||||
BLEAddress(mac.data()).toString().c_str(), rssi);
|
||||
DEBUG(buf);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BLEPeerManager::setPeerIdentity(const Bytes& mac_address, const Bytes& identity) {
|
||||
if (mac_address.size() < Limits::MAC_SIZE || identity.size() != Limits::IDENTITY_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
|
||||
// Check if peer exists in MAC-only storage
|
||||
PeerByMacSlot* mac_slot = findPeerByMacSlot(mac);
|
||||
if (mac_slot) {
|
||||
promoteToIdentityKeyed(mac, identity);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if peer already has identity (MAC might have changed)
|
||||
PeerByIdentitySlot* identity_slot = findPeerByIdentitySlot(identity);
|
||||
if (identity_slot) {
|
||||
// Update MAC address mapping
|
||||
PeerInfo& peer = identity_slot->peer;
|
||||
|
||||
// Remove old MAC mapping if different
|
||||
if (peer.mac_address != mac) {
|
||||
removeMacToIdentity(peer.mac_address);
|
||||
peer.mac_address = mac;
|
||||
setMacToIdentity(mac, identity);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Peer not found
|
||||
WARNING("BLEPeerManager: Cannot set identity for unknown peer");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BLEPeerManager::updatePeerMac(const Bytes& identity, const Bytes& new_mac) {
|
||||
if (identity.size() != Limits::IDENTITY_SIZE || new_mac.size() < Limits::MAC_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bytes mac(new_mac.data(), Limits::MAC_SIZE);
|
||||
|
||||
PeerByIdentitySlot* slot = findPeerByIdentitySlot(identity);
|
||||
if (!slot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PeerInfo& peer = slot->peer;
|
||||
|
||||
// Remove old MAC mapping
|
||||
removeMacToIdentity(peer.mac_address);
|
||||
|
||||
// Update to new MAC
|
||||
peer.mac_address = mac;
|
||||
setMacToIdentity(mac, identity);
|
||||
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), "BLEPeerManager: Updated MAC for peer to %s",
|
||||
BLEAddress(mac.data()).toString().c_str());
|
||||
DEBUG(buf);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Peer Lookup
|
||||
//=============================================================================
|
||||
|
||||
PeerInfo* BLEPeerManager::getPeerByMac(const Bytes& mac_address) {
|
||||
if (mac_address.size() < Limits::MAC_SIZE) return nullptr;
|
||||
|
||||
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
|
||||
|
||||
// Check MAC-to-identity mapping first
|
||||
Bytes identity = getIdentityForMac(mac);
|
||||
if (identity.size() == Limits::IDENTITY_SIZE) {
|
||||
PeerByIdentitySlot* slot = findPeerByIdentitySlot(identity);
|
||||
if (slot) {
|
||||
return &slot->peer;
|
||||
}
|
||||
}
|
||||
|
||||
// Check MAC-only storage
|
||||
PeerByMacSlot* mac_slot = findPeerByMacSlot(mac);
|
||||
if (mac_slot) {
|
||||
return &mac_slot->peer;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const PeerInfo* BLEPeerManager::getPeerByMac(const Bytes& mac_address) const {
|
||||
return const_cast<BLEPeerManager*>(this)->getPeerByMac(mac_address);
|
||||
}
|
||||
|
||||
PeerInfo* BLEPeerManager::getPeerByIdentity(const Bytes& identity) {
|
||||
if (identity.size() != Limits::IDENTITY_SIZE) return nullptr;
|
||||
|
||||
PeerByIdentitySlot* slot = findPeerByIdentitySlot(identity);
|
||||
if (slot) {
|
||||
return &slot->peer;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const PeerInfo* BLEPeerManager::getPeerByIdentity(const Bytes& identity) const {
|
||||
return const_cast<BLEPeerManager*>(this)->getPeerByIdentity(identity);
|
||||
}
|
||||
|
||||
PeerInfo* BLEPeerManager::getPeerByHandle(uint16_t conn_handle) {
|
||||
// O(1) lookup using handle array
|
||||
return getHandleToPeer(conn_handle);
|
||||
}
|
||||
|
||||
const PeerInfo* BLEPeerManager::getPeerByHandle(uint16_t conn_handle) const {
|
||||
return getHandleToPeer(conn_handle);
|
||||
}
|
||||
|
||||
std::vector<PeerInfo*> BLEPeerManager::getConnectedPeers() {
|
||||
std::vector<PeerInfo*> result;
|
||||
result.reserve(PEERS_POOL_SIZE);
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_identity_pool[i].in_use && _peers_by_identity_pool[i].peer.isConnected()) {
|
||||
result.push_back(&_peers_by_identity_pool[i].peer);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_mac_only_pool[i].in_use && _peers_by_mac_only_pool[i].peer.isConnected()) {
|
||||
result.push_back(&_peers_by_mac_only_pool[i].peer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<PeerInfo*> BLEPeerManager::getAllPeers() {
|
||||
std::vector<PeerInfo*> result;
|
||||
result.reserve(PEERS_POOL_SIZE * 2);
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_identity_pool[i].in_use) {
|
||||
result.push_back(&_peers_by_identity_pool[i].peer);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_mac_only_pool[i].in_use) {
|
||||
result.push_back(&_peers_by_mac_only_pool[i].peer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Connection Management
|
||||
//=============================================================================
|
||||
|
||||
PeerInfo* BLEPeerManager::getBestConnectionCandidate() {
|
||||
double now = Utilities::OS::time();
|
||||
PeerInfo* best = nullptr;
|
||||
float best_score = -1.0f;
|
||||
|
||||
auto checkPeer = [&](PeerInfo& peer) {
|
||||
// Skip if already connected or connecting
|
||||
if (peer.state != PeerState::DISCOVERED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if blacklisted
|
||||
if (peer.state == PeerState::BLACKLISTED && now < peer.blacklisted_until) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if we shouldn't initiate (MAC sorting)
|
||||
if (!shouldInitiateConnection(peer.mac_address)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (peer.score > best_score) {
|
||||
best_score = peer.score;
|
||||
best = &peer;
|
||||
}
|
||||
};
|
||||
|
||||
// Check identity-keyed peers (unlikely to be DISCOVERED state, but possible after disconnect)
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_identity_pool[i].in_use) {
|
||||
checkPeer(_peers_by_identity_pool[i].peer);
|
||||
}
|
||||
}
|
||||
|
||||
// Check MAC-only peers (more common for connection candidates)
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_mac_only_pool[i].in_use) {
|
||||
checkPeer(_peers_by_mac_only_pool[i].peer);
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
bool BLEPeerManager::shouldInitiateConnection(const Bytes& peer_mac) const {
|
||||
return shouldInitiateConnection(_local_mac, peer_mac);
|
||||
}
|
||||
|
||||
bool BLEPeerManager::shouldInitiateConnection(const Bytes& our_mac, const Bytes& peer_mac) {
|
||||
if (our_mac.size() < Limits::MAC_SIZE || peer_mac.size() < Limits::MAC_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if our MAC is a random address (first byte >= 0xC0)
|
||||
// Random addresses have the two MSBs of the first byte set (0b11xxxxxx)
|
||||
// When using random addresses, MAC comparison is unreliable because our
|
||||
// random MAC changes between restarts. In this case, always initiate
|
||||
// connections and let the identity layer handle duplicate connections.
|
||||
if (our_mac.data()[0] >= 0xC0) {
|
||||
return true; // Always initiate with random address
|
||||
}
|
||||
|
||||
// Lower MAC initiates connection (standard behavior for public addresses)
|
||||
BLEAddress our_addr(our_mac.data());
|
||||
BLEAddress peer_addr(peer_mac.data());
|
||||
|
||||
bool result = our_addr.isLowerThan(peer_addr);
|
||||
char buf[100];
|
||||
snprintf(buf, sizeof(buf), "BLEPeerManager::shouldInitiateConnection: our=%s peer=%s result=%s",
|
||||
our_addr.toString().c_str(), peer_addr.toString().c_str(), result ? "yes" : "no");
|
||||
DEBUG(buf);
|
||||
return result;
|
||||
}
|
||||
|
||||
void BLEPeerManager::connectionSucceeded(const Bytes& identifier) {
|
||||
PeerInfo* peer = findPeer(identifier);
|
||||
if (!peer) return;
|
||||
|
||||
peer->connection_successes++;
|
||||
peer->consecutive_failures = 0;
|
||||
peer->connected_at = Utilities::OS::time();
|
||||
peer->state = PeerState::CONNECTED;
|
||||
|
||||
DEBUG("BLEPeerManager: Connection succeeded for peer");
|
||||
}
|
||||
|
||||
void BLEPeerManager::connectionFailed(const Bytes& identifier) {
|
||||
PeerInfo* peer = findPeer(identifier);
|
||||
if (!peer) return;
|
||||
|
||||
// Clear handle mapping on disconnect
|
||||
if (peer->conn_handle != 0xFFFF) {
|
||||
clearHandleToPeer(peer->conn_handle);
|
||||
peer->conn_handle = 0xFFFF;
|
||||
}
|
||||
|
||||
peer->connection_failures++;
|
||||
peer->consecutive_failures++;
|
||||
peer->state = PeerState::DISCOVERED;
|
||||
|
||||
// Check if should blacklist
|
||||
if (peer->consecutive_failures >= Limits::BLACKLIST_THRESHOLD) {
|
||||
double duration = calculateBlacklistDuration(peer->consecutive_failures);
|
||||
peer->blacklisted_until = Utilities::OS::time() + duration;
|
||||
peer->state = PeerState::BLACKLISTED;
|
||||
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), "BLEPeerManager: Blacklisted peer for %.0fs after %u failures",
|
||||
duration, peer->consecutive_failures);
|
||||
WARNING(buf);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::setPeerState(const Bytes& identifier, PeerState state) {
|
||||
PeerInfo* peer = findPeer(identifier);
|
||||
if (peer) {
|
||||
peer->state = state;
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::setPeerHandle(const Bytes& identifier, uint16_t conn_handle) {
|
||||
PeerInfo* peer = findPeer(identifier);
|
||||
if (peer) {
|
||||
// Remove old handle mapping if exists
|
||||
if (peer->conn_handle != 0xFFFF) {
|
||||
clearHandleToPeer(peer->conn_handle);
|
||||
}
|
||||
peer->conn_handle = conn_handle;
|
||||
// Add new handle mapping
|
||||
if (conn_handle != 0xFFFF) {
|
||||
setHandleToPeer(conn_handle, peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::setPeerMTU(const Bytes& identifier, uint16_t mtu) {
|
||||
PeerInfo* peer = findPeer(identifier);
|
||||
if (peer) {
|
||||
peer->mtu = mtu;
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::removePeer(const Bytes& identifier) {
|
||||
// Try identity first
|
||||
if (identifier.size() == Limits::IDENTITY_SIZE) {
|
||||
PeerByIdentitySlot* slot = findPeerByIdentitySlot(identifier);
|
||||
if (slot) {
|
||||
// Remove handle mapping
|
||||
if (slot->peer.conn_handle != 0xFFFF) {
|
||||
clearHandleToPeer(slot->peer.conn_handle);
|
||||
}
|
||||
// Remove MAC mapping
|
||||
removeMacToIdentity(slot->peer.mac_address);
|
||||
slot->clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try MAC
|
||||
if (identifier.size() >= Limits::MAC_SIZE) {
|
||||
Bytes mac(identifier.data(), Limits::MAC_SIZE);
|
||||
|
||||
// Check if maps to identity
|
||||
Bytes identity = getIdentityForMac(mac);
|
||||
if (identity.size() == Limits::IDENTITY_SIZE) {
|
||||
PeerByIdentitySlot* slot = findPeerByIdentitySlot(identity);
|
||||
if (slot) {
|
||||
// Remove handle mapping
|
||||
if (slot->peer.conn_handle != 0xFFFF) {
|
||||
clearHandleToPeer(slot->peer.conn_handle);
|
||||
}
|
||||
slot->clear();
|
||||
}
|
||||
removeMacToIdentity(mac);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check MAC-only
|
||||
PeerByMacSlot* mac_slot = findPeerByMacSlot(mac);
|
||||
if (mac_slot) {
|
||||
// Remove handle mapping
|
||||
if (mac_slot->peer.conn_handle != 0xFFFF) {
|
||||
clearHandleToPeer(mac_slot->peer.conn_handle);
|
||||
}
|
||||
mac_slot->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::updateRssi(const Bytes& identifier, int8_t rssi) {
|
||||
PeerInfo* peer = findPeer(identifier);
|
||||
if (peer) {
|
||||
peer->rssi = rssi;
|
||||
peer->rssi_avg = static_cast<int8_t>(0.7f * peer->rssi_avg + 0.3f * rssi);
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Statistics
|
||||
//=============================================================================
|
||||
|
||||
void BLEPeerManager::recordPacketSent(const Bytes& identifier) {
|
||||
PeerInfo* peer = findPeer(identifier);
|
||||
if (peer) {
|
||||
peer->packets_sent++;
|
||||
peer->last_activity = Utilities::OS::time();
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::recordPacketReceived(const Bytes& identifier) {
|
||||
PeerInfo* peer = findPeer(identifier);
|
||||
if (peer) {
|
||||
peer->packets_received++;
|
||||
peer->last_activity = Utilities::OS::time();
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::updateLastActivity(const Bytes& identifier) {
|
||||
PeerInfo* peer = findPeer(identifier);
|
||||
if (peer) {
|
||||
peer->last_activity = Utilities::OS::time();
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Scoring & Blacklist
|
||||
//=============================================================================
|
||||
|
||||
void BLEPeerManager::recalculateScores() {
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_identity_pool[i].in_use) {
|
||||
_peers_by_identity_pool[i].peer.score = calculateScore(_peers_by_identity_pool[i].peer);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_mac_only_pool[i].in_use) {
|
||||
_peers_by_mac_only_pool[i].peer.score = calculateScore(_peers_by_mac_only_pool[i].peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::checkBlacklistExpirations() {
|
||||
double now = Utilities::OS::time();
|
||||
|
||||
auto checkAndClear = [now](PeerInfo& peer) {
|
||||
if (peer.state == PeerState::BLACKLISTED && now >= peer.blacklisted_until) {
|
||||
peer.state = PeerState::DISCOVERED;
|
||||
peer.blacklisted_until = 0;
|
||||
DEBUG("BLEPeerManager: Peer blacklist expired, restored to DISCOVERED");
|
||||
}
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_identity_pool[i].in_use) {
|
||||
checkAndClear(_peers_by_identity_pool[i].peer);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_mac_only_pool[i].in_use) {
|
||||
checkAndClear(_peers_by_mac_only_pool[i].peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Counts & Limits
|
||||
//=============================================================================
|
||||
|
||||
size_t BLEPeerManager::connectedCount() const {
|
||||
size_t count = 0;
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_identity_pool[i].in_use && _peers_by_identity_pool[i].peer.isConnected()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_mac_only_pool[i].in_use && _peers_by_mac_only_pool[i].peer.isConnected()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
void BLEPeerManager::cleanupStalePeers(double max_age) {
|
||||
double now = Utilities::OS::time();
|
||||
|
||||
// Check MAC-only peers (identity-keyed peers are more persistent)
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (!_peers_by_mac_only_pool[i].in_use) continue;
|
||||
|
||||
const PeerInfo& peer = _peers_by_mac_only_pool[i].peer;
|
||||
|
||||
// Only clean up DISCOVERED peers (not connected or connecting)
|
||||
if (peer.state == PeerState::DISCOVERED) {
|
||||
double age = now - peer.last_seen;
|
||||
if (age > max_age) {
|
||||
Bytes mac = _peers_by_mac_only_pool[i].mac_address;
|
||||
_peers_by_mac_only_pool[i].clear();
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), "BLEPeerManager: Removed stale peer %s",
|
||||
BLEAddress(mac.data()).toString().c_str());
|
||||
TRACE(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Private Methods
|
||||
//=============================================================================
|
||||
|
||||
float BLEPeerManager::calculateScore(const PeerInfo& peer) const {
|
||||
double now = Utilities::OS::time();
|
||||
|
||||
// RSSI component (60% weight)
|
||||
float rssi_norm = normalizeRssi(peer.rssi_avg);
|
||||
float rssi_score = Scoring::RSSI_WEIGHT * rssi_norm;
|
||||
|
||||
// History component (30% weight)
|
||||
float history_score = 0.0f;
|
||||
if (peer.connection_attempts > 0) {
|
||||
float success_rate = static_cast<float>(peer.connection_successes) /
|
||||
static_cast<float>(peer.connection_attempts);
|
||||
history_score = Scoring::HISTORY_WEIGHT * success_rate;
|
||||
} else {
|
||||
// New peer: benefit of the doubt (50%)
|
||||
history_score = Scoring::HISTORY_WEIGHT * 0.5f;
|
||||
}
|
||||
|
||||
// Recency component (10% weight)
|
||||
float recency_score = 0.0f;
|
||||
double age = now - peer.last_seen;
|
||||
if (age < 5.0) {
|
||||
recency_score = Scoring::RECENCY_WEIGHT * 1.0f;
|
||||
} else if (age < 30.0) {
|
||||
// Linear decay from 1.0 to 0.0 over 25 seconds
|
||||
recency_score = Scoring::RECENCY_WEIGHT * (1.0f - static_cast<float>((age - 5.0) / 25.0));
|
||||
}
|
||||
|
||||
return rssi_score + history_score + recency_score;
|
||||
}
|
||||
|
||||
float BLEPeerManager::normalizeRssi(int8_t rssi) const {
|
||||
// Clamp to expected range
|
||||
if (rssi < Scoring::RSSI_MIN) rssi = Scoring::RSSI_MIN;
|
||||
if (rssi > Scoring::RSSI_MAX) rssi = Scoring::RSSI_MAX;
|
||||
|
||||
// Map to 0.0-1.0
|
||||
return static_cast<float>(rssi - Scoring::RSSI_MIN) /
|
||||
static_cast<float>(Scoring::RSSI_MAX - Scoring::RSSI_MIN);
|
||||
}
|
||||
|
||||
double BLEPeerManager::calculateBlacklistDuration(uint8_t failures) const {
|
||||
// Exponential backoff: 60s × min(2^(failures-3), 8)
|
||||
if (failures < Limits::BLACKLIST_THRESHOLD) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t exponent = failures - Limits::BLACKLIST_THRESHOLD;
|
||||
uint8_t multiplier = 1 << exponent; // 2^exponent
|
||||
if (multiplier > Limits::BLACKLIST_MAX_MULTIPLIER) {
|
||||
multiplier = Limits::BLACKLIST_MAX_MULTIPLIER;
|
||||
}
|
||||
|
||||
return Timing::BLACKLIST_BASE_BACKOFF * multiplier;
|
||||
}
|
||||
|
||||
PeerInfo* BLEPeerManager::findPeer(const Bytes& identifier) {
|
||||
// Try as identity
|
||||
if (identifier.size() == Limits::IDENTITY_SIZE) {
|
||||
PeerByIdentitySlot* slot = findPeerByIdentitySlot(identifier);
|
||||
if (slot) {
|
||||
return &slot->peer;
|
||||
}
|
||||
}
|
||||
|
||||
// Try as MAC
|
||||
if (identifier.size() >= Limits::MAC_SIZE) {
|
||||
return getPeerByMac(identifier);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void BLEPeerManager::promoteToIdentityKeyed(const Bytes& mac_address, const Bytes& identity) {
|
||||
PeerByMacSlot* mac_slot = findPeerByMacSlot(mac_address);
|
||||
if (!mac_slot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find an empty slot in identity pool
|
||||
PeerByIdentitySlot* identity_slot = findEmptyPeerByIdentitySlot();
|
||||
if (!identity_slot) {
|
||||
WARNING("BLEPeerManager: Identity pool is full, cannot promote peer");
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy peer info to identity pool
|
||||
identity_slot->in_use = true;
|
||||
identity_slot->identity_hash = identity;
|
||||
identity_slot->peer = mac_slot->peer;
|
||||
identity_slot->peer.identity = identity;
|
||||
|
||||
// Update handle mapping to point to new location
|
||||
if (identity_slot->peer.conn_handle != 0xFFFF) {
|
||||
setHandleToPeer(identity_slot->peer.conn_handle, &identity_slot->peer);
|
||||
}
|
||||
|
||||
// Add MAC-to-identity mapping
|
||||
setMacToIdentity(mac_address, identity);
|
||||
|
||||
// Clear MAC-only slot
|
||||
mac_slot->clear();
|
||||
|
||||
DEBUG("BLEPeerManager: Promoted peer to identity-keyed storage");
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Pool Helper Methods - Peers by Identity
|
||||
//=============================================================================
|
||||
|
||||
BLEPeerManager::PeerByIdentitySlot* BLEPeerManager::findPeerByIdentitySlot(const Bytes& identity) {
|
||||
if (identity.size() != Limits::IDENTITY_SIZE) return nullptr;
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_identity_pool[i].in_use &&
|
||||
_peers_by_identity_pool[i].identity_hash == identity) {
|
||||
return &_peers_by_identity_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BLEPeerManager::PeerByIdentitySlot* BLEPeerManager::findPeerByIdentitySlot(const Bytes& identity) const {
|
||||
return const_cast<BLEPeerManager*>(this)->findPeerByIdentitySlot(identity);
|
||||
}
|
||||
|
||||
BLEPeerManager::PeerByIdentitySlot* BLEPeerManager::findEmptyPeerByIdentitySlot() {
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (!_peers_by_identity_pool[i].in_use) {
|
||||
return &_peers_by_identity_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t BLEPeerManager::peersByIdentityCount() const {
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_identity_pool[i].in_use) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Pool Helper Methods - Peers by MAC Only
|
||||
//=============================================================================
|
||||
|
||||
BLEPeerManager::PeerByMacSlot* BLEPeerManager::findPeerByMacSlot(const Bytes& mac) {
|
||||
if (mac.size() < Limits::MAC_SIZE) return nullptr;
|
||||
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_mac_only_pool[i].in_use &&
|
||||
_peers_by_mac_only_pool[i].mac_address == mac) {
|
||||
return &_peers_by_mac_only_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BLEPeerManager::PeerByMacSlot* BLEPeerManager::findPeerByMacSlot(const Bytes& mac) const {
|
||||
return const_cast<BLEPeerManager*>(this)->findPeerByMacSlot(mac);
|
||||
}
|
||||
|
||||
BLEPeerManager::PeerByMacSlot* BLEPeerManager::findEmptyPeerByMacSlot() {
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (!_peers_by_mac_only_pool[i].in_use) {
|
||||
return &_peers_by_mac_only_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t BLEPeerManager::peersByMacOnlyCount() const {
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < PEERS_POOL_SIZE; i++) {
|
||||
if (_peers_by_mac_only_pool[i].in_use) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Pool Helper Methods - MAC to Identity Mapping
|
||||
//=============================================================================
|
||||
|
||||
BLEPeerManager::MacToIdentitySlot* BLEPeerManager::findMacToIdentitySlot(const Bytes& mac) {
|
||||
if (mac.size() < Limits::MAC_SIZE) return nullptr;
|
||||
|
||||
for (size_t i = 0; i < MAC_IDENTITY_POOL_SIZE; i++) {
|
||||
if (_mac_to_identity_pool[i].in_use &&
|
||||
_mac_to_identity_pool[i].mac_address == mac) {
|
||||
return &_mac_to_identity_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BLEPeerManager::MacToIdentitySlot* BLEPeerManager::findMacToIdentitySlot(const Bytes& mac) const {
|
||||
return const_cast<BLEPeerManager*>(this)->findMacToIdentitySlot(mac);
|
||||
}
|
||||
|
||||
BLEPeerManager::MacToIdentitySlot* BLEPeerManager::findEmptyMacToIdentitySlot() {
|
||||
for (size_t i = 0; i < MAC_IDENTITY_POOL_SIZE; i++) {
|
||||
if (!_mac_to_identity_pool[i].in_use) {
|
||||
return &_mac_to_identity_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool BLEPeerManager::setMacToIdentity(const Bytes& mac, const Bytes& identity) {
|
||||
// Check if already exists
|
||||
MacToIdentitySlot* existing = findMacToIdentitySlot(mac);
|
||||
if (existing) {
|
||||
existing->identity = identity;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find empty slot
|
||||
MacToIdentitySlot* slot = findEmptyMacToIdentitySlot();
|
||||
if (!slot) {
|
||||
WARNING("BLEPeerManager: MAC-to-identity pool is full");
|
||||
return false;
|
||||
}
|
||||
|
||||
slot->in_use = true;
|
||||
slot->mac_address = mac;
|
||||
slot->identity = identity;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEPeerManager::removeMacToIdentity(const Bytes& mac) {
|
||||
MacToIdentitySlot* slot = findMacToIdentitySlot(mac);
|
||||
if (slot) {
|
||||
slot->clear();
|
||||
}
|
||||
}
|
||||
|
||||
Bytes BLEPeerManager::getIdentityForMac(const Bytes& mac) const {
|
||||
const MacToIdentitySlot* slot = findMacToIdentitySlot(mac);
|
||||
if (slot) {
|
||||
return slot->identity;
|
||||
}
|
||||
return Bytes();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Pool Helper Methods - Handle to Peer Mapping
|
||||
//=============================================================================
|
||||
|
||||
void BLEPeerManager::setHandleToPeer(uint16_t handle, PeerInfo* peer) {
|
||||
if (handle < MAX_CONN_HANDLES) {
|
||||
_handle_to_peer[handle] = peer;
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerManager::clearHandleToPeer(uint16_t handle) {
|
||||
if (handle < MAX_CONN_HANDLES) {
|
||||
_handle_to_peer[handle] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
PeerInfo* BLEPeerManager::getHandleToPeer(uint16_t handle) {
|
||||
if (handle < MAX_CONN_HANDLES) {
|
||||
return _handle_to_peer[handle];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const PeerInfo* BLEPeerManager::getHandleToPeer(uint16_t handle) const {
|
||||
if (handle < MAX_CONN_HANDLES) {
|
||||
return _handle_to_peer[handle];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,483 @@
|
||||
/**
|
||||
* @file BLEPeerManager.h
|
||||
* @brief BLE-Reticulum Protocol v2.2 peer management
|
||||
*
|
||||
* Manages discovered and connected BLE peers with:
|
||||
* - Peer scoring for connection prioritization
|
||||
* - Blacklisting with exponential backoff
|
||||
* - MAC address rotation handling via identity-based keying
|
||||
* - Connection direction determination via MAC sorting
|
||||
*
|
||||
* Uses fixed-size pools instead of STL containers to eliminate heap fragmentation.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "BLETypes.h"
|
||||
#include "Bytes.h"
|
||||
#include "Utilities/OS.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
/**
|
||||
* @brief Information about a discovered/connected peer
|
||||
*/
|
||||
struct PeerInfo {
|
||||
// Addressing (both needed for MAC rotation handling)
|
||||
Bytes mac_address; // Current 6-byte MAC address
|
||||
Bytes identity; // 16-byte identity hash (stable key)
|
||||
uint8_t address_type = 0; // BLE address type (0=public, 1=random)
|
||||
|
||||
// Connection state
|
||||
PeerState state = PeerState::DISCOVERED;
|
||||
bool is_central = false; // true if we are central (we initiated)
|
||||
|
||||
// Timing
|
||||
double discovered_at = 0.0;
|
||||
double last_seen = 0.0;
|
||||
double last_activity = 0.0;
|
||||
double connected_at = 0.0;
|
||||
|
||||
// Signal quality
|
||||
int8_t rssi = Scoring::RSSI_MIN;
|
||||
int8_t rssi_avg = Scoring::RSSI_MIN; // Smoothed average
|
||||
|
||||
// Statistics for scoring
|
||||
uint32_t packets_sent = 0;
|
||||
uint32_t packets_received = 0;
|
||||
uint32_t connection_attempts = 0;
|
||||
uint32_t connection_successes = 0;
|
||||
uint32_t connection_failures = 0;
|
||||
|
||||
// Blacklist tracking
|
||||
uint8_t consecutive_failures = 0;
|
||||
double blacklisted_until = 0.0;
|
||||
|
||||
// BLE connection handle (platform-specific)
|
||||
uint16_t conn_handle = 0xFFFF;
|
||||
|
||||
// MTU for this peer
|
||||
uint16_t mtu = MTU::MINIMUM;
|
||||
|
||||
// Computed score (cached)
|
||||
float score = 0.0f;
|
||||
|
||||
// Check if peer has known identity
|
||||
bool hasIdentity() const { return identity.size() == Limits::IDENTITY_SIZE; }
|
||||
|
||||
// Check if connected
|
||||
bool isConnected() const {
|
||||
return state == PeerState::CONNECTED ||
|
||||
state == PeerState::HANDSHAKING;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Manages BLE peers for the BLEInterface
|
||||
*
|
||||
* Uses fixed-size pools to eliminate heap fragmentation:
|
||||
* - _peers_pool: Stores all peer info (max 8 slots)
|
||||
* - _mac_to_identity_pool: Maps MAC addresses to identities (max 8 slots)
|
||||
* - _handle_to_peer: Fixed array indexed by connection handle (max 8)
|
||||
*/
|
||||
class BLEPeerManager {
|
||||
public:
|
||||
//=========================================================================
|
||||
// Pool Configuration
|
||||
//=========================================================================
|
||||
static constexpr size_t PEERS_POOL_SIZE = 8;
|
||||
static constexpr size_t MAC_IDENTITY_POOL_SIZE = 8;
|
||||
static constexpr size_t MAX_CONN_HANDLES = 8;
|
||||
|
||||
/**
|
||||
* @brief Slot for storing peer info (keyed by identity)
|
||||
*/
|
||||
struct PeerByIdentitySlot {
|
||||
bool in_use = false;
|
||||
Bytes identity_hash; // 16-byte identity key
|
||||
PeerInfo peer; // value
|
||||
|
||||
void clear() {
|
||||
in_use = false;
|
||||
identity_hash.clear();
|
||||
peer = PeerInfo();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Slot for storing peer info (keyed by MAC only, no identity yet)
|
||||
*/
|
||||
struct PeerByMacSlot {
|
||||
bool in_use = false;
|
||||
Bytes mac_address; // 6-byte MAC key
|
||||
PeerInfo peer; // value
|
||||
|
||||
void clear() {
|
||||
in_use = false;
|
||||
mac_address.clear();
|
||||
peer = PeerInfo();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Slot for MAC to identity mapping
|
||||
*/
|
||||
struct MacToIdentitySlot {
|
||||
bool in_use = false;
|
||||
Bytes mac_address; // 6-byte MAC key
|
||||
Bytes identity; // 16-byte identity value
|
||||
|
||||
void clear() {
|
||||
in_use = false;
|
||||
mac_address.clear();
|
||||
identity.clear();
|
||||
}
|
||||
};
|
||||
|
||||
BLEPeerManager();
|
||||
|
||||
/**
|
||||
* @brief Set our local MAC address (for connection direction decisions)
|
||||
*/
|
||||
void setLocalMac(const Bytes& mac);
|
||||
|
||||
/**
|
||||
* @brief Get our local MAC address
|
||||
*/
|
||||
const Bytes& getLocalMac() const { return _local_mac; }
|
||||
|
||||
//=========================================================================
|
||||
// Peer Discovery
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Register a newly discovered peer from BLE scan
|
||||
*
|
||||
* @param mac_address 6-byte MAC address
|
||||
* @param rssi Signal strength
|
||||
* @param address_type BLE address type (0=public, 1=random)
|
||||
* @return true if peer was added or updated (not blacklisted)
|
||||
*/
|
||||
bool addDiscoveredPeer(const Bytes& mac_address, int8_t rssi, uint8_t address_type = 0);
|
||||
|
||||
/**
|
||||
* @brief Update peer identity after handshake completion
|
||||
*
|
||||
* @param mac_address Current MAC address
|
||||
* @param identity 16-byte identity hash
|
||||
* @return true if peer was found and updated
|
||||
*/
|
||||
bool setPeerIdentity(const Bytes& mac_address, const Bytes& identity);
|
||||
|
||||
/**
|
||||
* @brief Update peer MAC address (when identity already known but MAC rotated)
|
||||
*
|
||||
* @param identity 16-byte identity hash
|
||||
* @param new_mac New 6-byte MAC address
|
||||
* @return true if peer was found and updated
|
||||
*/
|
||||
bool updatePeerMac(const Bytes& identity, const Bytes& new_mac);
|
||||
|
||||
//=========================================================================
|
||||
// Peer Lookup
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Get peer info by MAC address
|
||||
* @return Pointer to PeerInfo or nullptr if not found
|
||||
*/
|
||||
PeerInfo* getPeerByMac(const Bytes& mac_address);
|
||||
const PeerInfo* getPeerByMac(const Bytes& mac_address) const;
|
||||
|
||||
/**
|
||||
* @brief Get peer info by identity
|
||||
* @return Pointer to PeerInfo or nullptr if not found
|
||||
*/
|
||||
PeerInfo* getPeerByIdentity(const Bytes& identity);
|
||||
const PeerInfo* getPeerByIdentity(const Bytes& identity) const;
|
||||
|
||||
/**
|
||||
* @brief Get peer info by connection handle
|
||||
* @return Pointer to PeerInfo or nullptr if not found
|
||||
*/
|
||||
PeerInfo* getPeerByHandle(uint16_t conn_handle);
|
||||
const PeerInfo* getPeerByHandle(uint16_t conn_handle) const;
|
||||
|
||||
/**
|
||||
* @brief Get all connected peers
|
||||
*/
|
||||
std::vector<PeerInfo*> getConnectedPeers();
|
||||
|
||||
/**
|
||||
* @brief Get all peers (for iteration)
|
||||
*/
|
||||
std::vector<PeerInfo*> getAllPeers();
|
||||
|
||||
//=========================================================================
|
||||
// Connection Management
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Get best peer to connect to (highest score, not blacklisted)
|
||||
* @return Pointer to best peer or nullptr if none available
|
||||
*/
|
||||
PeerInfo* getBestConnectionCandidate();
|
||||
|
||||
/**
|
||||
* @brief Check if we should initiate connection to a peer (MAC sorting rule)
|
||||
*
|
||||
* Lower MAC address should be the initiator (central).
|
||||
* @param peer_mac The peer's MAC address
|
||||
* @return true if we should initiate (our MAC < peer MAC)
|
||||
*/
|
||||
bool shouldInitiateConnection(const Bytes& peer_mac) const;
|
||||
|
||||
/**
|
||||
* @brief Static version for use without instance
|
||||
*/
|
||||
static bool shouldInitiateConnection(const Bytes& our_mac, const Bytes& peer_mac);
|
||||
|
||||
/**
|
||||
* @brief Mark peer connection as successful
|
||||
*/
|
||||
void connectionSucceeded(const Bytes& identifier);
|
||||
|
||||
/**
|
||||
* @brief Mark peer connection as failed
|
||||
*/
|
||||
void connectionFailed(const Bytes& identifier);
|
||||
|
||||
/**
|
||||
* @brief Update peer state
|
||||
*/
|
||||
void setPeerState(const Bytes& identifier, PeerState state);
|
||||
|
||||
/**
|
||||
* @brief Set peer connection handle
|
||||
*/
|
||||
void setPeerHandle(const Bytes& identifier, uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* @brief Set peer MTU
|
||||
*/
|
||||
void setPeerMTU(const Bytes& identifier, uint16_t mtu);
|
||||
|
||||
/**
|
||||
* @brief Remove a peer
|
||||
*/
|
||||
void removePeer(const Bytes& identifier);
|
||||
|
||||
/**
|
||||
* @brief Update peer RSSI
|
||||
*/
|
||||
void updateRssi(const Bytes& identifier, int8_t rssi);
|
||||
|
||||
//=========================================================================
|
||||
// Statistics
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Record packet sent to peer
|
||||
*/
|
||||
void recordPacketSent(const Bytes& identifier);
|
||||
|
||||
/**
|
||||
* @brief Record packet received from peer
|
||||
*/
|
||||
void recordPacketReceived(const Bytes& identifier);
|
||||
|
||||
/**
|
||||
* @brief Update last activity time for peer
|
||||
*/
|
||||
void updateLastActivity(const Bytes& identifier);
|
||||
|
||||
//=========================================================================
|
||||
// Scoring & Blacklist
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Recalculate scores for all peers
|
||||
*
|
||||
* Should be called periodically or after significant changes.
|
||||
*/
|
||||
void recalculateScores();
|
||||
|
||||
/**
|
||||
* @brief Check blacklist expirations and restore peers
|
||||
*/
|
||||
void checkBlacklistExpirations();
|
||||
|
||||
//=========================================================================
|
||||
// Counts & Limits
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Get current connected peer count
|
||||
*/
|
||||
size_t connectedCount() const;
|
||||
|
||||
/**
|
||||
* @brief Get total peer count
|
||||
*/
|
||||
size_t totalPeerCount() const { return peersByIdentityCount() + peersByMacOnlyCount(); }
|
||||
|
||||
/**
|
||||
* @brief Check if we can accept more connections
|
||||
*/
|
||||
bool canAcceptConnection() const { return connectedCount() < Limits::MAX_PEERS; }
|
||||
|
||||
/**
|
||||
* @brief Clean up stale discovered peers
|
||||
* @param max_age Maximum age in seconds for discovered (unconnected) peers
|
||||
*/
|
||||
void cleanupStalePeers(double max_age = Timing::PEER_TIMEOUT);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Calculate peer score using v2.2 formula
|
||||
*/
|
||||
float calculateScore(const PeerInfo& peer) const;
|
||||
|
||||
/**
|
||||
* @brief Normalize RSSI to 0.0-1.0 range
|
||||
*/
|
||||
float normalizeRssi(int8_t rssi) const;
|
||||
|
||||
/**
|
||||
* @brief Calculate blacklist duration for given failure count
|
||||
*/
|
||||
double calculateBlacklistDuration(uint8_t failures) const;
|
||||
|
||||
/**
|
||||
* @brief Find peer by any identifier (MAC or identity)
|
||||
*/
|
||||
PeerInfo* findPeer(const Bytes& identifier);
|
||||
|
||||
/**
|
||||
* @brief Move peer from MAC-only to identity-keyed storage
|
||||
*/
|
||||
void promoteToIdentityKeyed(const Bytes& mac_address, const Bytes& identity);
|
||||
|
||||
//=========================================================================
|
||||
// Pool Helper Methods - Peers by Identity
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Find slot by identity key
|
||||
* @return Pointer to slot or nullptr if not found
|
||||
*/
|
||||
PeerByIdentitySlot* findPeerByIdentitySlot(const Bytes& identity);
|
||||
const PeerByIdentitySlot* findPeerByIdentitySlot(const Bytes& identity) const;
|
||||
|
||||
/**
|
||||
* @brief Find an empty slot in the identity pool
|
||||
* @return Pointer to empty slot or nullptr if pool is full
|
||||
*/
|
||||
PeerByIdentitySlot* findEmptyPeerByIdentitySlot();
|
||||
|
||||
/**
|
||||
* @brief Get count of peers by identity
|
||||
*/
|
||||
size_t peersByIdentityCount() const;
|
||||
|
||||
//=========================================================================
|
||||
// Pool Helper Methods - Peers by MAC Only
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Find slot by MAC key
|
||||
* @return Pointer to slot or nullptr if not found
|
||||
*/
|
||||
PeerByMacSlot* findPeerByMacSlot(const Bytes& mac);
|
||||
const PeerByMacSlot* findPeerByMacSlot(const Bytes& mac) const;
|
||||
|
||||
/**
|
||||
* @brief Find an empty slot in the MAC-only pool
|
||||
* @return Pointer to empty slot or nullptr if pool is full
|
||||
*/
|
||||
PeerByMacSlot* findEmptyPeerByMacSlot();
|
||||
|
||||
/**
|
||||
* @brief Get count of peers by MAC only
|
||||
*/
|
||||
size_t peersByMacOnlyCount() const;
|
||||
|
||||
//=========================================================================
|
||||
// Pool Helper Methods - MAC to Identity Mapping
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Find MAC-to-identity mapping slot by MAC
|
||||
* @return Pointer to slot or nullptr if not found
|
||||
*/
|
||||
MacToIdentitySlot* findMacToIdentitySlot(const Bytes& mac);
|
||||
const MacToIdentitySlot* findMacToIdentitySlot(const Bytes& mac) const;
|
||||
|
||||
/**
|
||||
* @brief Find an empty slot in the MAC-to-identity pool
|
||||
* @return Pointer to empty slot or nullptr if pool is full
|
||||
*/
|
||||
MacToIdentitySlot* findEmptyMacToIdentitySlot();
|
||||
|
||||
/**
|
||||
* @brief Add or update MAC-to-identity mapping
|
||||
* @return true if successful, false if pool is full
|
||||
*/
|
||||
bool setMacToIdentity(const Bytes& mac, const Bytes& identity);
|
||||
|
||||
/**
|
||||
* @brief Remove MAC-to-identity mapping
|
||||
*/
|
||||
void removeMacToIdentity(const Bytes& mac);
|
||||
|
||||
/**
|
||||
* @brief Get identity for a MAC from the mapping pool
|
||||
* @return Identity or empty Bytes if not found
|
||||
*/
|
||||
Bytes getIdentityForMac(const Bytes& mac) const;
|
||||
|
||||
//=========================================================================
|
||||
// Pool Helper Methods - Handle to Peer Mapping
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Set handle-to-peer mapping
|
||||
*/
|
||||
void setHandleToPeer(uint16_t handle, PeerInfo* peer);
|
||||
|
||||
/**
|
||||
* @brief Clear handle-to-peer mapping
|
||||
*/
|
||||
void clearHandleToPeer(uint16_t handle);
|
||||
|
||||
/**
|
||||
* @brief Get peer for handle
|
||||
* @return Pointer to peer or nullptr if not found
|
||||
*/
|
||||
PeerInfo* getHandleToPeer(uint16_t handle);
|
||||
const PeerInfo* getHandleToPeer(uint16_t handle) const;
|
||||
|
||||
//=========================================================================
|
||||
// Fixed-size Pool Storage
|
||||
//=========================================================================
|
||||
|
||||
// Peers with known identity (keyed by identity)
|
||||
PeerByIdentitySlot _peers_by_identity_pool[PEERS_POOL_SIZE];
|
||||
|
||||
// Peers without identity yet (keyed by MAC)
|
||||
PeerByMacSlot _peers_by_mac_only_pool[PEERS_POOL_SIZE];
|
||||
|
||||
// MAC to identity lookup for peers with identity
|
||||
MacToIdentitySlot _mac_to_identity_pool[MAC_IDENTITY_POOL_SIZE];
|
||||
|
||||
// Connection handle to peer pointer for O(1) lookup
|
||||
// Index is the connection handle (must be < MAX_CONN_HANDLES)
|
||||
// nullptr means no mapping for that handle
|
||||
PeerInfo* _handle_to_peer[MAX_CONN_HANDLES];
|
||||
|
||||
// Our own MAC address
|
||||
Bytes _local_mac;
|
||||
};
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file BLEPlatform.cpp
|
||||
* @brief BLE Platform factory implementation
|
||||
*/
|
||||
|
||||
#include "BLEPlatform.h"
|
||||
#include "Log.h"
|
||||
|
||||
// Include platform implementations based on compile-time detection
|
||||
#if defined(ESP32) && (defined(USE_NIMBLE) || defined(CONFIG_BT_NIMBLE_ENABLED))
|
||||
#include "platforms/NimBLEPlatform.h"
|
||||
#endif
|
||||
|
||||
#if defined(ESP32) && defined(USE_BLUEDROID)
|
||||
#include "platforms/BluedroidPlatform.h"
|
||||
#endif
|
||||
|
||||
#if defined(ZEPHYR) || defined(CONFIG_BT)
|
||||
// Future: #include "platforms/ZephyrPlatform.h"
|
||||
#endif
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
IBLEPlatform::Ptr BLEPlatformFactory::create() {
|
||||
return create(getDetectedPlatform());
|
||||
}
|
||||
|
||||
IBLEPlatform::Ptr BLEPlatformFactory::create(PlatformType type) {
|
||||
switch (type) {
|
||||
#if defined(ESP32) && (defined(USE_NIMBLE) || defined(CONFIG_BT_NIMBLE_ENABLED))
|
||||
case PlatformType::NIMBLE_ARDUINO:
|
||||
INFO("BLEPlatformFactory: Creating NimBLE platform");
|
||||
return std::make_shared<NimBLEPlatform>();
|
||||
#endif
|
||||
|
||||
#if defined(ESP32) && defined(USE_BLUEDROID)
|
||||
case PlatformType::ESP_IDF:
|
||||
INFO("BLEPlatformFactory: Creating Bluedroid platform");
|
||||
return std::make_shared<BluedroidPlatform>();
|
||||
#endif
|
||||
|
||||
#if defined(ZEPHYR) || defined(CONFIG_BT)
|
||||
case PlatformType::ZEPHYR:
|
||||
// Future: return std::make_shared<ZephyrPlatform>();
|
||||
ERROR("BLEPlatformFactory: Zephyr platform not yet implemented");
|
||||
return nullptr;
|
||||
#endif
|
||||
|
||||
default:
|
||||
ERROR("BLEPlatformFactory: No platform available for type " +
|
||||
std::to_string(static_cast<int>(type)));
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
PlatformType BLEPlatformFactory::getDetectedPlatform() {
|
||||
#if defined(ESP32) && defined(USE_BLUEDROID)
|
||||
// Bluedroid takes priority when explicitly selected
|
||||
return PlatformType::ESP_IDF;
|
||||
#elif defined(ESP32) && (defined(USE_NIMBLE) || defined(CONFIG_BT_NIMBLE_ENABLED))
|
||||
return PlatformType::NIMBLE_ARDUINO;
|
||||
#elif defined(ZEPHYR) || defined(CONFIG_BT)
|
||||
return PlatformType::ZEPHYR;
|
||||
#else
|
||||
return PlatformType::NONE;
|
||||
#endif
|
||||
}
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* @file BLEPlatform.h
|
||||
* @brief BLE Hardware Abstraction Layer (HAL) interface
|
||||
*
|
||||
* Provides a platform-agnostic interface for BLE operations. Platform-specific
|
||||
* implementations (NimBLE, ESP-IDF, Zephyr) implement this interface to enable
|
||||
* the BLEInterface to work across different hardware.
|
||||
*
|
||||
* The HAL abstracts:
|
||||
* - BLE stack initialization and lifecycle
|
||||
* - Scanning and advertising
|
||||
* - Connection management
|
||||
* - GATT operations (read, write, notify)
|
||||
* - Callback handling
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "BLETypes.h"
|
||||
#include "Bytes.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
/**
|
||||
* @brief Abstract BLE platform interface
|
||||
*
|
||||
* Platform-specific implementations should inherit from this class and
|
||||
* implement all pure virtual methods. The factory method create() returns
|
||||
* the appropriate implementation based on compile-time detection.
|
||||
*/
|
||||
class IBLEPlatform {
|
||||
public:
|
||||
using Ptr = std::shared_ptr<IBLEPlatform>;
|
||||
|
||||
virtual ~IBLEPlatform() = default;
|
||||
|
||||
//=========================================================================
|
||||
// Lifecycle
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Initialize the BLE stack with configuration
|
||||
*
|
||||
* @param config Platform configuration
|
||||
* @return true if initialization successful
|
||||
*/
|
||||
virtual bool initialize(const PlatformConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief Start BLE operations (advertising/scanning based on role)
|
||||
* @return true if started successfully
|
||||
*/
|
||||
virtual bool start() = 0;
|
||||
|
||||
/**
|
||||
* @brief Stop all BLE operations
|
||||
*/
|
||||
virtual void stop() = 0;
|
||||
|
||||
/**
|
||||
* @brief Main loop processing - must be called periodically
|
||||
*
|
||||
* Handles BLE events, processes queued operations, and invokes callbacks.
|
||||
*/
|
||||
virtual void loop() = 0;
|
||||
|
||||
/**
|
||||
* @brief Shutdown and cleanup the BLE stack
|
||||
*/
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if platform is initialized and running
|
||||
*/
|
||||
virtual bool isRunning() const = 0;
|
||||
|
||||
//=========================================================================
|
||||
// Central Mode - Scanning
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Start scanning for peripherals
|
||||
*
|
||||
* @param duration_ms Scan duration in milliseconds (0 = continuous)
|
||||
* @return true if scan started successfully
|
||||
*/
|
||||
virtual bool startScan(uint16_t duration_ms = 0) = 0;
|
||||
|
||||
/**
|
||||
* @brief Stop scanning
|
||||
*/
|
||||
virtual void stopScan() = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if currently scanning
|
||||
*/
|
||||
virtual bool isScanning() const = 0;
|
||||
|
||||
//=========================================================================
|
||||
// Central Mode - Connections
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Connect to a peripheral
|
||||
*
|
||||
* @param address Peer's BLE address
|
||||
* @param timeout_ms Connection timeout in milliseconds
|
||||
* @return true if connection attempt started
|
||||
*/
|
||||
virtual bool connect(const BLEAddress& address, uint16_t timeout_ms = 10000) = 0;
|
||||
|
||||
/**
|
||||
* @brief Disconnect from a peer
|
||||
*
|
||||
* @param conn_handle Connection handle
|
||||
* @return true if disconnect initiated
|
||||
*/
|
||||
virtual bool disconnect(uint16_t conn_handle) = 0;
|
||||
|
||||
/**
|
||||
* @brief Disconnect all connections
|
||||
*/
|
||||
virtual void disconnectAll() = 0;
|
||||
|
||||
/**
|
||||
* @brief Request MTU update for a connection
|
||||
*
|
||||
* @param conn_handle Connection handle
|
||||
* @param mtu Requested MTU
|
||||
* @return true if request was sent
|
||||
*/
|
||||
virtual bool requestMTU(uint16_t conn_handle, uint16_t mtu) = 0;
|
||||
|
||||
/**
|
||||
* @brief Discover services on connected peripheral
|
||||
*
|
||||
* @param conn_handle Connection handle
|
||||
* @return true if discovery started
|
||||
*/
|
||||
virtual bool discoverServices(uint16_t conn_handle) = 0;
|
||||
|
||||
//=========================================================================
|
||||
// Peripheral Mode - Advertising
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Start advertising
|
||||
* @return true if advertising started
|
||||
*/
|
||||
virtual bool startAdvertising() = 0;
|
||||
|
||||
/**
|
||||
* @brief Stop advertising
|
||||
*/
|
||||
virtual void stopAdvertising() = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if currently advertising
|
||||
*/
|
||||
virtual bool isAdvertising() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Update advertising data
|
||||
*
|
||||
* @param data Custom advertising data
|
||||
* @return true if updated successfully
|
||||
*/
|
||||
virtual bool setAdvertisingData(const Bytes& data) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set the identity data for the Identity characteristic
|
||||
*
|
||||
* @param identity 16-byte identity hash
|
||||
*/
|
||||
virtual void setIdentityData(const Bytes& identity) = 0;
|
||||
|
||||
//=========================================================================
|
||||
// GATT Operations
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Write data to a connected peripheral's RX characteristic
|
||||
*
|
||||
* @param conn_handle Connection handle
|
||||
* @param data Data to write
|
||||
* @param response true for write with response, false for write without response
|
||||
* @return true if write was queued/sent
|
||||
*/
|
||||
virtual bool write(uint16_t conn_handle, const Bytes& data, bool response = true) = 0;
|
||||
|
||||
/**
|
||||
* @brief Read from a characteristic
|
||||
*
|
||||
* @param conn_handle Connection handle
|
||||
* @param char_handle Characteristic handle
|
||||
* @param callback Callback invoked with result
|
||||
* @return true if read was queued
|
||||
*/
|
||||
virtual bool read(uint16_t conn_handle, uint16_t char_handle,
|
||||
std::function<void(OperationResult, const Bytes&)> callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Enable/disable notifications on TX characteristic
|
||||
*
|
||||
* @param conn_handle Connection handle
|
||||
* @param enable true to enable, false to disable
|
||||
* @return true if operation was queued
|
||||
*/
|
||||
virtual bool enableNotifications(uint16_t conn_handle, bool enable) = 0;
|
||||
|
||||
/**
|
||||
* @brief Send notification to a connected central (peripheral mode)
|
||||
*
|
||||
* @param conn_handle Connection handle
|
||||
* @param data Data to send
|
||||
* @return true if notification was sent
|
||||
*/
|
||||
virtual bool notify(uint16_t conn_handle, const Bytes& data) = 0;
|
||||
|
||||
/**
|
||||
* @brief Send notification to all connected centrals
|
||||
*
|
||||
* @param data Data to broadcast
|
||||
* @return true if at least one notification was sent
|
||||
*/
|
||||
virtual bool notifyAll(const Bytes& data) = 0;
|
||||
|
||||
//=========================================================================
|
||||
// Connection Management
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Get all active connections
|
||||
*/
|
||||
virtual std::vector<ConnectionHandle> getConnections() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Get connection by handle
|
||||
*/
|
||||
virtual ConnectionHandle getConnection(uint16_t handle) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Get current connection count
|
||||
*/
|
||||
virtual size_t getConnectionCount() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if connected to specific address
|
||||
*/
|
||||
virtual bool isConnectedTo(const BLEAddress& address) const = 0;
|
||||
|
||||
//=========================================================================
|
||||
// Callback Registration
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Set callback for scan results
|
||||
*/
|
||||
virtual void setOnScanResult(Callbacks::OnScanResult callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for scan completion
|
||||
*/
|
||||
virtual void setOnScanComplete(Callbacks::OnScanComplete callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for outgoing connection established (central mode)
|
||||
*/
|
||||
virtual void setOnConnected(Callbacks::OnConnected callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for connection terminated
|
||||
*/
|
||||
virtual void setOnDisconnected(Callbacks::OnDisconnected callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for MTU change
|
||||
*/
|
||||
virtual void setOnMTUChanged(Callbacks::OnMTUChanged callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for service discovery completion
|
||||
*/
|
||||
virtual void setOnServicesDiscovered(Callbacks::OnServicesDiscovered callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for data received via notification (central mode)
|
||||
*/
|
||||
virtual void setOnDataReceived(Callbacks::OnDataReceived callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for notification enable/disable
|
||||
*/
|
||||
virtual void setOnNotifyEnabled(Callbacks::OnNotifyEnabled callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for incoming connection (peripheral mode)
|
||||
*/
|
||||
virtual void setOnCentralConnected(Callbacks::OnCentralConnected callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for incoming connection terminated (peripheral mode)
|
||||
*/
|
||||
virtual void setOnCentralDisconnected(Callbacks::OnCentralDisconnected callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for data received via write (peripheral mode)
|
||||
*/
|
||||
virtual void setOnWriteReceived(Callbacks::OnWriteReceived callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for read request (peripheral mode)
|
||||
*/
|
||||
virtual void setOnReadRequested(Callbacks::OnReadRequested callback) = 0;
|
||||
|
||||
//=========================================================================
|
||||
// Platform Info
|
||||
//=========================================================================
|
||||
|
||||
/**
|
||||
* @brief Get the platform type
|
||||
*/
|
||||
virtual PlatformType getPlatformType() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Get human-readable platform name
|
||||
*/
|
||||
virtual std::string getPlatformName() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Get our local BLE address
|
||||
*/
|
||||
virtual BLEAddress getLocalAddress() const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Factory for creating platform-specific BLE implementations
|
||||
*/
|
||||
class BLEPlatformFactory {
|
||||
public:
|
||||
/**
|
||||
* @brief Create platform instance based on compile-time detection
|
||||
*
|
||||
* Returns the appropriate IBLEPlatform implementation for the current
|
||||
* platform (NimBLE for ESP32, Zephyr for nRF52840, etc.)
|
||||
*
|
||||
* @return Shared pointer to platform instance, or nullptr if no platform available
|
||||
*/
|
||||
static IBLEPlatform::Ptr create();
|
||||
|
||||
/**
|
||||
* @brief Create specific platform (for testing or explicit selection)
|
||||
*
|
||||
* @param type Platform type to create
|
||||
* @return Shared pointer to platform instance, or nullptr if not available
|
||||
*/
|
||||
static IBLEPlatform::Ptr create(PlatformType type);
|
||||
|
||||
/**
|
||||
* @brief Get the detected platform type for this build
|
||||
*/
|
||||
static PlatformType getDetectedPlatform();
|
||||
};
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* @file BLEReassembler.cpp
|
||||
* @brief BLE-Reticulum Protocol v2.2 fragment reassembler implementation
|
||||
*
|
||||
* Uses fixed-size pools instead of STL containers to eliminate heap fragmentation.
|
||||
*/
|
||||
|
||||
#include "BLEReassembler.h"
|
||||
#include "Log.h"
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
BLEReassembler::BLEReassembler() {
|
||||
// Default timeout from protocol spec
|
||||
_timeout_seconds = Timing::REASSEMBLY_TIMEOUT;
|
||||
// Initialize pool
|
||||
for (size_t i = 0; i < MAX_PENDING_REASSEMBLIES; i++) {
|
||||
_pending_pool[i].clear();
|
||||
}
|
||||
}
|
||||
|
||||
BLEReassembler::PendingReassemblySlot* BLEReassembler::findSlot(const Bytes& peer_identity) {
|
||||
for (size_t i = 0; i < MAX_PENDING_REASSEMBLIES; i++) {
|
||||
if (_pending_pool[i].in_use && _pending_pool[i].transfer_id == peer_identity) {
|
||||
return &_pending_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BLEReassembler::PendingReassemblySlot* BLEReassembler::findSlot(const Bytes& peer_identity) const {
|
||||
for (size_t i = 0; i < MAX_PENDING_REASSEMBLIES; i++) {
|
||||
if (_pending_pool[i].in_use && _pending_pool[i].transfer_id == peer_identity) {
|
||||
return &_pending_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BLEReassembler::PendingReassemblySlot* BLEReassembler::allocateSlot(const Bytes& peer_identity) {
|
||||
// First check if slot already exists for this peer
|
||||
PendingReassemblySlot* existing = findSlot(peer_identity);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Find a free slot
|
||||
for (size_t i = 0; i < MAX_PENDING_REASSEMBLIES; i++) {
|
||||
if (!_pending_pool[i].in_use) {
|
||||
_pending_pool[i].in_use = true;
|
||||
_pending_pool[i].transfer_id = peer_identity;
|
||||
return &_pending_pool[i];
|
||||
}
|
||||
}
|
||||
return nullptr; // Pool is full
|
||||
}
|
||||
|
||||
size_t BLEReassembler::pendingCount() const {
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < MAX_PENDING_REASSEMBLIES; i++) {
|
||||
if (_pending_pool[i].in_use) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void BLEReassembler::setReassemblyCallback(ReassemblyCallback callback) {
|
||||
_reassembly_callback = callback;
|
||||
}
|
||||
|
||||
void BLEReassembler::setTimeoutCallback(TimeoutCallback callback) {
|
||||
_timeout_callback = callback;
|
||||
}
|
||||
|
||||
void BLEReassembler::setTimeout(double timeout_seconds) {
|
||||
_timeout_seconds = timeout_seconds;
|
||||
}
|
||||
|
||||
bool BLEReassembler::processFragment(const Bytes& peer_identity, const Bytes& fragment) {
|
||||
// Validate fragment
|
||||
if (!BLEFragmenter::isValidFragment(fragment)) {
|
||||
TRACE("BLEReassembler: Invalid fragment header");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse header
|
||||
Fragment::Type type;
|
||||
uint16_t sequence;
|
||||
uint16_t total_fragments;
|
||||
if (!BLEFragmenter::parseHeader(fragment, type, sequence, total_fragments)) {
|
||||
TRACE("BLEReassembler: Failed to parse fragment header");
|
||||
return false;
|
||||
}
|
||||
|
||||
double now = Utilities::OS::time();
|
||||
|
||||
// Handle START fragment - begins a new reassembly
|
||||
if (type == Fragment::START) {
|
||||
// Clear any existing incomplete reassembly for this peer
|
||||
PendingReassemblySlot* existing = findSlot(peer_identity);
|
||||
if (existing) {
|
||||
TRACE("BLEReassembler: Discarding incomplete reassembly for new START");
|
||||
existing->clear();
|
||||
}
|
||||
|
||||
// Start new reassembly
|
||||
if (!startReassembly(peer_identity, total_fragments)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Look up pending reassembly
|
||||
PendingReassemblySlot* slot = findSlot(peer_identity);
|
||||
if (!slot) {
|
||||
// No pending reassembly and this isn't a START
|
||||
if (type != Fragment::START) {
|
||||
// For single-fragment packets (type=END, total=1, seq=0), start immediately
|
||||
if (type == Fragment::END && total_fragments == 1 && sequence == 0) {
|
||||
if (!startReassembly(peer_identity, total_fragments)) {
|
||||
return false;
|
||||
}
|
||||
slot = findSlot(peer_identity);
|
||||
} else {
|
||||
TRACE("BLEReassembler: Received fragment without START, discarding");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
slot = findSlot(peer_identity);
|
||||
}
|
||||
}
|
||||
|
||||
if (!slot) {
|
||||
ERROR("BLEReassembler: Failed to find/create reassembly session");
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingReassembly& reassembly = slot->reassembly;
|
||||
|
||||
// Validate total_fragments matches
|
||||
if (total_fragments != reassembly.total_fragments) {
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Fragment total mismatch, expected %u got %u",
|
||||
reassembly.total_fragments, total_fragments);
|
||||
TRACE(buf);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate sequence is in range
|
||||
if (sequence >= reassembly.total_fragments) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Sequence out of range: %u", sequence);
|
||||
TRACE(buf);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate
|
||||
if (reassembly.fragments[sequence].received) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Duplicate fragment %u", sequence);
|
||||
TRACE(buf);
|
||||
// Still update last_activity to keep session alive
|
||||
reassembly.last_activity = now;
|
||||
return true; // Not an error, just duplicate
|
||||
}
|
||||
|
||||
// Store fragment payload into fixed-size buffer
|
||||
Bytes payload = BLEFragmenter::extractPayload(fragment);
|
||||
if (payload.size() > MAX_FRAGMENT_PAYLOAD_SIZE) {
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Fragment payload too large: %zu > %zu",
|
||||
payload.size(), MAX_FRAGMENT_PAYLOAD_SIZE);
|
||||
WARNING(buf);
|
||||
return false;
|
||||
}
|
||||
memcpy(reassembly.fragments[sequence].data, payload.data(), payload.size());
|
||||
reassembly.fragments[sequence].data_size = payload.size();
|
||||
reassembly.fragments[sequence].received = true;
|
||||
reassembly.received_count++;
|
||||
reassembly.last_activity = now;
|
||||
|
||||
{
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Received fragment %u/%u", sequence + 1, reassembly.total_fragments);
|
||||
TRACE(buf);
|
||||
}
|
||||
|
||||
// Check if complete
|
||||
if (reassembly.received_count == reassembly.total_fragments) {
|
||||
// Assemble complete packet
|
||||
Bytes complete_packet = assembleFragments(reassembly);
|
||||
|
||||
{
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Completed reassembly, %zu bytes", complete_packet.size());
|
||||
TRACE(buf);
|
||||
}
|
||||
|
||||
// Remove from pending before callback (callback might trigger new data)
|
||||
Bytes identity_copy = reassembly.peer_identity;
|
||||
slot->clear();
|
||||
|
||||
// Invoke callback
|
||||
if (_reassembly_callback) {
|
||||
_reassembly_callback(identity_copy, complete_packet);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEReassembler::checkTimeouts() {
|
||||
double now = Utilities::OS::time();
|
||||
|
||||
// Find and clean up expired reassemblies
|
||||
for (size_t i = 0; i < MAX_PENDING_REASSEMBLIES; i++) {
|
||||
if (!_pending_pool[i].in_use) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PendingReassembly& reassembly = _pending_pool[i].reassembly;
|
||||
double age = now - reassembly.started_at;
|
||||
|
||||
if (age > _timeout_seconds) {
|
||||
{
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Timeout waiting for fragments, received %u/%u",
|
||||
reassembly.received_count, reassembly.total_fragments);
|
||||
WARNING(buf);
|
||||
}
|
||||
|
||||
// Copy identity before clearing
|
||||
Bytes peer_identity = _pending_pool[i].transfer_id;
|
||||
|
||||
// Clear the slot
|
||||
_pending_pool[i].clear();
|
||||
|
||||
// Invoke timeout callback after clearing (callback might start new reassembly)
|
||||
if (_timeout_callback) {
|
||||
_timeout_callback(peer_identity, "Reassembly timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLEReassembler::clearForPeer(const Bytes& peer_identity) {
|
||||
PendingReassemblySlot* slot = findSlot(peer_identity);
|
||||
if (slot) {
|
||||
TRACE("BLEReassembler: Clearing pending reassembly for peer");
|
||||
slot->clear();
|
||||
}
|
||||
}
|
||||
|
||||
void BLEReassembler::clearAll() {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Clearing all pending reassemblies (%zu sessions)", pendingCount());
|
||||
TRACE(buf);
|
||||
for (size_t i = 0; i < MAX_PENDING_REASSEMBLIES; i++) {
|
||||
_pending_pool[i].clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool BLEReassembler::hasPending(const Bytes& peer_identity) const {
|
||||
return findSlot(peer_identity) != nullptr;
|
||||
}
|
||||
|
||||
bool BLEReassembler::startReassembly(const Bytes& peer_identity, uint16_t total_fragments) {
|
||||
// Validate fragment count fits in fixed-size array
|
||||
if (total_fragments > MAX_FRAGMENTS_PER_REASSEMBLY) {
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Too many fragments: %u > %zu",
|
||||
total_fragments, MAX_FRAGMENTS_PER_REASSEMBLY);
|
||||
WARNING(buf);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate a slot (reuses existing or finds free)
|
||||
PendingReassemblySlot* slot = allocateSlot(peer_identity);
|
||||
if (!slot) {
|
||||
WARNING("BLEReassembler: Pool full, cannot start new reassembly");
|
||||
return false;
|
||||
}
|
||||
|
||||
double now = Utilities::OS::time();
|
||||
|
||||
// Initialize the reassembly state
|
||||
PendingReassembly& reassembly = slot->reassembly;
|
||||
reassembly.clear(); // Clear any old data
|
||||
reassembly.peer_identity = peer_identity;
|
||||
reassembly.total_fragments = total_fragments;
|
||||
reassembly.received_count = 0;
|
||||
reassembly.started_at = now;
|
||||
reassembly.last_activity = now;
|
||||
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "BLEReassembler: Starting reassembly for %u fragments", total_fragments);
|
||||
TRACE(buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bytes BLEReassembler::assembleFragments(const PendingReassembly& reassembly) {
|
||||
// Calculate total size
|
||||
size_t total_size = 0;
|
||||
for (size_t i = 0; i < reassembly.total_fragments; i++) {
|
||||
total_size += reassembly.fragments[i].data_size;
|
||||
}
|
||||
|
||||
// Allocate result buffer
|
||||
Bytes result(total_size);
|
||||
uint8_t* ptr = result.writable(total_size);
|
||||
result.resize(total_size);
|
||||
|
||||
// Concatenate fragments in order
|
||||
size_t offset = 0;
|
||||
for (size_t i = 0; i < reassembly.total_fragments; i++) {
|
||||
const FragmentInfo& frag = reassembly.fragments[i];
|
||||
if (frag.data_size > 0) {
|
||||
memcpy(ptr + offset, frag.data, frag.data_size);
|
||||
offset += frag.data_size;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @file BLEReassembler.h
|
||||
* @brief BLE-Reticulum Protocol v2.2 fragment reassembler
|
||||
*
|
||||
* Reassembles incoming BLE fragments into complete Reticulum packets.
|
||||
* Handles timeout for incomplete reassemblies and per-peer tracking.
|
||||
* This class has no BLE dependencies and can be used for testing on native builds.
|
||||
*
|
||||
* The reassembler is keyed by peer identity (16 bytes), not MAC address,
|
||||
* to survive BLE MAC address rotation.
|
||||
*
|
||||
* Uses fixed-size pools instead of STL containers to eliminate heap fragmentation
|
||||
* on embedded systems.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "BLETypes.h"
|
||||
#include "BLEFragmenter.h"
|
||||
#include "Bytes.h"
|
||||
#include "Utilities/OS.h"
|
||||
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
// Pool sizing constants for fixed-size allocations
|
||||
static constexpr size_t MAX_PENDING_REASSEMBLIES = 8;
|
||||
static constexpr size_t MAX_FRAGMENTS_PER_REASSEMBLY = 32;
|
||||
static constexpr size_t MAX_FRAGMENT_PAYLOAD_SIZE = 512;
|
||||
|
||||
class BLEReassembler {
|
||||
public:
|
||||
/**
|
||||
* @brief Callback for successfully reassembled packets
|
||||
* @param peer_identity The 16-byte identity of the sending peer
|
||||
* @param packet The complete reassembled packet
|
||||
*/
|
||||
using ReassemblyCallback = std::function<void(const Bytes& peer_identity, const Bytes& packet)>;
|
||||
|
||||
/**
|
||||
* @brief Callback for reassembly timeout/failure
|
||||
* @param peer_identity The 16-byte identity of the peer
|
||||
* @param reason Description of the failure
|
||||
*/
|
||||
using TimeoutCallback = std::function<void(const Bytes& peer_identity, const std::string& reason)>;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a reassembler with default timeout
|
||||
*/
|
||||
BLEReassembler();
|
||||
|
||||
/**
|
||||
* @brief Set callback for successfully reassembled packets
|
||||
*/
|
||||
void setReassemblyCallback(ReassemblyCallback callback);
|
||||
|
||||
/**
|
||||
* @brief Set callback for reassembly timeouts/failures
|
||||
*/
|
||||
void setTimeoutCallback(TimeoutCallback callback);
|
||||
|
||||
/**
|
||||
* @brief Set the reassembly timeout
|
||||
* @param timeout_seconds Seconds to wait before timing out incomplete reassembly
|
||||
*/
|
||||
void setTimeout(double timeout_seconds);
|
||||
|
||||
/**
|
||||
* @brief Process an incoming fragment
|
||||
*
|
||||
* @param peer_identity The 16-byte identity of the sending peer
|
||||
* @param fragment The received fragment with header
|
||||
* @return true if fragment was processed successfully, false on error
|
||||
*
|
||||
* When a packet is fully reassembled, the reassembly callback is invoked.
|
||||
*/
|
||||
bool processFragment(const Bytes& peer_identity, const Bytes& fragment);
|
||||
|
||||
/**
|
||||
* @brief Check for timed-out reassemblies and clean them up
|
||||
*
|
||||
* Should be called periodically from the interface loop().
|
||||
* Invokes timeout callback for each expired reassembly.
|
||||
*/
|
||||
void checkTimeouts();
|
||||
|
||||
/**
|
||||
* @brief Get count of pending (incomplete) reassemblies
|
||||
*/
|
||||
size_t pendingCount() const;
|
||||
|
||||
/**
|
||||
* @brief Clear all pending reassemblies for a specific peer
|
||||
* @param peer_identity Clear only for this peer
|
||||
*/
|
||||
void clearForPeer(const Bytes& peer_identity);
|
||||
|
||||
/**
|
||||
* @brief Clear all pending reassemblies
|
||||
*/
|
||||
void clearAll();
|
||||
|
||||
/**
|
||||
* @brief Check if there's a pending reassembly for a peer
|
||||
*/
|
||||
bool hasPending(const Bytes& peer_identity) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Information about a single received fragment (fixed-size)
|
||||
*/
|
||||
struct FragmentInfo {
|
||||
uint8_t data[MAX_FRAGMENT_PAYLOAD_SIZE];
|
||||
size_t data_size = 0;
|
||||
bool received = false;
|
||||
|
||||
void clear() {
|
||||
data_size = 0;
|
||||
received = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief State for a pending (incomplete) reassembly (fixed-size)
|
||||
*/
|
||||
struct PendingReassembly {
|
||||
Bytes peer_identity;
|
||||
uint16_t total_fragments = 0;
|
||||
uint16_t received_count = 0;
|
||||
FragmentInfo fragments[MAX_FRAGMENTS_PER_REASSEMBLY];
|
||||
double started_at = 0.0;
|
||||
double last_activity = 0.0;
|
||||
|
||||
void clear() {
|
||||
peer_identity = Bytes();
|
||||
total_fragments = 0;
|
||||
received_count = 0;
|
||||
for (size_t i = 0; i < MAX_FRAGMENTS_PER_REASSEMBLY; i++) {
|
||||
fragments[i].clear();
|
||||
}
|
||||
started_at = 0.0;
|
||||
last_activity = 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Slot in the fixed-size pool for pending reassemblies
|
||||
*/
|
||||
struct PendingReassemblySlot {
|
||||
bool in_use = false;
|
||||
Bytes transfer_id; // key (peer_identity)
|
||||
PendingReassembly reassembly;
|
||||
|
||||
void clear() {
|
||||
in_use = false;
|
||||
transfer_id = Bytes();
|
||||
reassembly.clear();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Find a slot by peer identity
|
||||
* @return Pointer to slot or nullptr if not found
|
||||
*/
|
||||
PendingReassemblySlot* findSlot(const Bytes& peer_identity);
|
||||
const PendingReassemblySlot* findSlot(const Bytes& peer_identity) const;
|
||||
|
||||
/**
|
||||
* @brief Allocate a new slot for a peer
|
||||
* @return Pointer to slot or nullptr if pool is full
|
||||
*/
|
||||
PendingReassemblySlot* allocateSlot(const Bytes& peer_identity);
|
||||
|
||||
/**
|
||||
* @brief Concatenate all fragments in order to produce the complete packet
|
||||
*/
|
||||
Bytes assembleFragments(const PendingReassembly& reassembly);
|
||||
|
||||
/**
|
||||
* @brief Start a new reassembly session
|
||||
* @return true if started, false if pool is full or too many fragments
|
||||
*/
|
||||
bool startReassembly(const Bytes& peer_identity, uint16_t total_fragments);
|
||||
|
||||
// Fixed-size pool of pending reassemblies
|
||||
PendingReassemblySlot _pending_pool[MAX_PENDING_REASSEMBLIES];
|
||||
|
||||
// Callbacks
|
||||
ReassemblyCallback _reassembly_callback = nullptr;
|
||||
TimeoutCallback _timeout_callback = nullptr;
|
||||
|
||||
// Timeout configuration
|
||||
double _timeout_seconds = Timing::REASSEMBLY_TIMEOUT;
|
||||
};
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,502 @@
|
||||
/**
|
||||
* @file BLETypes.h
|
||||
* @brief BLE-Reticulum Protocol v2.2 types, constants, and common structures
|
||||
*
|
||||
* This file defines the core types used throughout the BLE interface implementation.
|
||||
* It includes GATT service/characteristic UUIDs, protocol constants, enumerations,
|
||||
* and data structures used by all BLE components.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "Bytes.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
//=============================================================================
|
||||
// Protocol Version
|
||||
//=============================================================================
|
||||
|
||||
static constexpr uint8_t PROTOCOL_VERSION_MAJOR = 2;
|
||||
static constexpr uint8_t PROTOCOL_VERSION_MINOR = 2;
|
||||
|
||||
//=============================================================================
|
||||
// GATT Service and Characteristic UUIDs (BLE-Reticulum v2.2)
|
||||
//=============================================================================
|
||||
|
||||
namespace UUID {
|
||||
// Reticulum BLE Service UUID
|
||||
static constexpr const char* SERVICE = "37145b00-442d-4a94-917f-8f42c5da28e3";
|
||||
|
||||
// TX Characteristic (notify) - Data from peripheral to central
|
||||
static constexpr const char* TX_CHAR = "37145b00-442d-4a94-917f-8f42c5da28e4";
|
||||
|
||||
// RX Characteristic (write) - Data from central to peripheral
|
||||
static constexpr const char* RX_CHAR = "37145b00-442d-4a94-917f-8f42c5da28e5";
|
||||
|
||||
// Identity Characteristic (read) - 16-byte identity hash
|
||||
static constexpr const char* IDENTITY_CHAR = "37145b00-442d-4a94-917f-8f42c5da28e6";
|
||||
|
||||
// Standard CCCD UUID for enabling notifications
|
||||
static constexpr const char* CCCD = "00002902-0000-1000-8000-00805f9b34fb";
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// MTU Constants
|
||||
//=============================================================================
|
||||
|
||||
namespace MTU {
|
||||
static constexpr uint16_t REQUESTED = 517; // Request maximum MTU (BLE 5.0)
|
||||
static constexpr uint16_t MINIMUM = 23; // BLE 4.0 minimum MTU
|
||||
static constexpr uint16_t INITIAL = 185; // Conservative default (BLE 4.2)
|
||||
static constexpr uint16_t ATT_OVERHEAD = 3; // ATT protocol header overhead
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Protocol Timing Constants (v2.2 spec)
|
||||
//=============================================================================
|
||||
|
||||
namespace Timing {
|
||||
static constexpr double KEEPALIVE_INTERVAL = 15.0; // Seconds between keepalives
|
||||
static constexpr double REASSEMBLY_TIMEOUT = 30.0; // Seconds to complete reassembly
|
||||
static constexpr double CONNECTION_TIMEOUT = 30.0; // Seconds to establish connection
|
||||
static constexpr double HANDSHAKE_TIMEOUT = 10.0; // Seconds for identity exchange
|
||||
static constexpr double SCAN_INTERVAL = 5.0; // Seconds between scans
|
||||
static constexpr double PEER_TIMEOUT = 30.0; // Seconds before peer removal
|
||||
static constexpr double POST_MTU_DELAY = 0.15; // Seconds after MTU negotiation
|
||||
static constexpr double BLACKLIST_BASE_BACKOFF = 60.0; // Base backoff seconds
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Protocol Limits
|
||||
//=============================================================================
|
||||
|
||||
namespace Limits {
|
||||
#ifdef ARDUINO
|
||||
static constexpr size_t MAX_PEERS = 3; // Reduced for MCU memory constraints
|
||||
static constexpr size_t MAX_DISCOVERED_PEERS = 16; // Reduced discovery cache for MCU
|
||||
#else
|
||||
static constexpr size_t MAX_PEERS = 7; // Maximum simultaneous connections
|
||||
static constexpr size_t MAX_DISCOVERED_PEERS = 100; // Discovery cache limit
|
||||
#endif
|
||||
static constexpr size_t IDENTITY_SIZE = 16; // Identity hash size (bytes)
|
||||
static constexpr size_t MAC_SIZE = 6; // BLE MAC address size (bytes)
|
||||
static constexpr uint8_t BLACKLIST_THRESHOLD = 3; // Failures before blacklist
|
||||
static constexpr uint8_t BLACKLIST_MAX_MULTIPLIER = 8; // Max 2^n backoff multiplier
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Peer Scoring Weights (v2.2 spec)
|
||||
//=============================================================================
|
||||
|
||||
namespace Scoring {
|
||||
static constexpr float RSSI_WEIGHT = 0.60f;
|
||||
static constexpr float HISTORY_WEIGHT = 0.30f;
|
||||
static constexpr float RECENCY_WEIGHT = 0.10f;
|
||||
static constexpr int8_t RSSI_MIN = -100; // dBm
|
||||
static constexpr int8_t RSSI_MAX = -30; // dBm
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Fragment Header Constants
|
||||
//=============================================================================
|
||||
|
||||
namespace Fragment {
|
||||
static constexpr size_t HEADER_SIZE = 5;
|
||||
|
||||
enum Type : uint8_t {
|
||||
START = 0x01, // First fragment of multi-fragment message
|
||||
CONTINUE = 0x02, // Middle fragment
|
||||
END = 0x03 // Last fragment (or single fragment)
|
||||
};
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Enumerations
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* @brief Platform type for compile-time selection
|
||||
*/
|
||||
enum class PlatformType {
|
||||
NONE,
|
||||
NIMBLE_ARDUINO, // ESP32 with NimBLE-Arduino library
|
||||
ESP_IDF, // ESP32 with ESP-IDF native BLE
|
||||
ZEPHYR, // nRF52840 with Zephyr RTOS
|
||||
NORDIC_SDK // nRF52840 with Nordic SDK (future)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief BLE role in a connection
|
||||
*/
|
||||
enum class Role : uint8_t {
|
||||
NONE = 0x00,
|
||||
CENTRAL = 0x01, // Initiates connections (GATT client)
|
||||
PERIPHERAL = 0x02, // Accepts connections (GATT server)
|
||||
DUAL = 0x03 // Both central and peripheral simultaneously
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Connection state machine states
|
||||
*/
|
||||
enum class ConnectionState : uint8_t {
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCOVERING_SERVICES,
|
||||
READY, // Fully connected, services discovered, handshake complete
|
||||
DISCONNECTING
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Peer state for tracking
|
||||
*/
|
||||
enum class PeerState : uint8_t {
|
||||
DISCOVERED, // Seen in scan, not connected
|
||||
CONNECTING, // Connection in progress
|
||||
HANDSHAKING, // Connected, awaiting identity exchange
|
||||
CONNECTED, // Fully connected with identity
|
||||
DISCONNECTING, // Disconnect in progress
|
||||
BLACKLISTED // Temporarily blacklisted
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief GATT operation types for queuing
|
||||
*/
|
||||
enum class OperationType : uint8_t {
|
||||
READ,
|
||||
WRITE,
|
||||
WRITE_NO_RESPONSE,
|
||||
NOTIFY_ENABLE,
|
||||
NOTIFY_DISABLE,
|
||||
MTU_REQUEST
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief GATT operation result codes
|
||||
*/
|
||||
enum class OperationResult : uint8_t {
|
||||
SUCCESS,
|
||||
PENDING,
|
||||
TIMEOUT,
|
||||
DISCONNECTED,
|
||||
NOT_FOUND,
|
||||
NOT_SUPPORTED,
|
||||
INVALID_HANDLE,
|
||||
INSUFFICIENT_AUTH,
|
||||
INSUFFICIENT_ENC,
|
||||
BUSY,
|
||||
ERROR
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Scan mode
|
||||
*/
|
||||
enum class ScanMode : uint8_t {
|
||||
PASSIVE,
|
||||
ACTIVE
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Data Structures
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* @brief BLE address (6 bytes + type)
|
||||
*/
|
||||
struct BLEAddress {
|
||||
uint8_t addr[6] = {0};
|
||||
uint8_t type = 0; // 0 = public, 1 = random
|
||||
|
||||
BLEAddress() = default;
|
||||
|
||||
BLEAddress(const uint8_t* address, uint8_t addr_type = 0) : type(addr_type) {
|
||||
if (address) {
|
||||
memcpy(addr, address, 6);
|
||||
}
|
||||
}
|
||||
|
||||
bool operator==(const BLEAddress& other) const {
|
||||
return memcmp(addr, other.addr, 6) == 0 && type == other.type;
|
||||
}
|
||||
|
||||
bool operator!=(const BLEAddress& other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
bool operator<(const BLEAddress& other) const {
|
||||
int cmp = memcmp(addr, other.addr, 6);
|
||||
if (cmp != 0) return cmp < 0;
|
||||
return type < other.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if this address is "lower" than another (for MAC sorting)
|
||||
*
|
||||
* The device with the lower MAC address should initiate the connection
|
||||
* as the central role. This provides deterministic connection direction.
|
||||
*/
|
||||
bool isLowerThan(const BLEAddress& other) const {
|
||||
// Compare as 48-bit integers, MSB first (addr[0] is most significant)
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (addr[i] < other.addr[i]) return true;
|
||||
if (addr[i] > other.addr[i]) return false;
|
||||
}
|
||||
return false; // Equal
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert to colon-separated hex string (XX:XX:XX:XX:XX:XX)
|
||||
* addr[0] is MSB (first displayed), addr[5] is LSB (last displayed)
|
||||
*/
|
||||
std::string toString() const {
|
||||
char buf[18];
|
||||
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse from colon-separated hex string
|
||||
* First byte in string goes to addr[0] (MSB)
|
||||
*/
|
||||
static BLEAddress fromString(const std::string& str) {
|
||||
BLEAddress result;
|
||||
if (str.length() >= 17) {
|
||||
unsigned int values[6];
|
||||
if (sscanf(str.c_str(), "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
&values[0], &values[1], &values[2],
|
||||
&values[3], &values[4], &values[5]) == 6) {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
result.addr[i] = static_cast<uint8_t>(values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert to Bytes for storage/comparison
|
||||
*/
|
||||
Bytes toBytes() const {
|
||||
return Bytes(addr, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if address is all zeros (invalid)
|
||||
*/
|
||||
bool isZero() const {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (addr[i] != 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Scan result from BLE discovery
|
||||
*/
|
||||
struct ScanResult {
|
||||
BLEAddress address;
|
||||
std::string name;
|
||||
int8_t rssi = 0;
|
||||
bool connectable = false;
|
||||
Bytes advertising_data;
|
||||
Bytes scan_response_data;
|
||||
bool has_reticulum_service = false; // Pre-filtered for our service UUID
|
||||
Bytes identity_prefix; // First 3 bytes of identity from "RNS-xxxxxx" name (Protocol v2.2)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Connection handle with associated state
|
||||
*/
|
||||
struct ConnectionHandle {
|
||||
uint16_t handle = 0xFFFF; // Platform-specific connection handle
|
||||
BLEAddress peer_address;
|
||||
Role local_role = Role::NONE; // Our role in this connection
|
||||
ConnectionState state = ConnectionState::DISCONNECTED;
|
||||
uint16_t mtu = MTU::MINIMUM; // Negotiated MTU
|
||||
|
||||
// Characteristic handles (discovered after connection)
|
||||
uint16_t rx_char_handle = 0; // Handle for RX characteristic
|
||||
uint16_t tx_char_handle = 0; // Handle for TX characteristic
|
||||
uint16_t tx_cccd_handle = 0; // Handle for TX CCCD (notifications)
|
||||
uint16_t identity_handle = 0; // Handle for Identity characteristic
|
||||
|
||||
bool isValid() const { return handle != 0xFFFF; }
|
||||
|
||||
void reset() {
|
||||
handle = 0xFFFF;
|
||||
peer_address = BLEAddress();
|
||||
local_role = Role::NONE;
|
||||
state = ConnectionState::DISCONNECTED;
|
||||
mtu = MTU::MINIMUM;
|
||||
rx_char_handle = 0;
|
||||
tx_char_handle = 0;
|
||||
tx_cccd_handle = 0;
|
||||
identity_handle = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief GATT operation for queuing
|
||||
*/
|
||||
struct GATTOperation {
|
||||
OperationType type = OperationType::READ;
|
||||
uint16_t conn_handle = 0xFFFF;
|
||||
uint16_t char_handle = 0;
|
||||
Bytes data; // For writes
|
||||
uint32_t timeout_ms = 5000;
|
||||
|
||||
// Completion callback
|
||||
std::function<void(OperationResult, const Bytes&)> callback;
|
||||
|
||||
// Internal tracking
|
||||
double queued_at = 0;
|
||||
double started_at = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Platform configuration
|
||||
*/
|
||||
struct PlatformConfig {
|
||||
Role role = Role::DUAL;
|
||||
|
||||
// Advertising parameters (peripheral mode)
|
||||
uint16_t adv_interval_min_ms = 100;
|
||||
uint16_t adv_interval_max_ms = 200;
|
||||
std::string device_name = "RNS-Node";
|
||||
|
||||
// Scan parameters (central mode)
|
||||
// WiFi/BLE Coexistence: With software coexistence, scan interval must not exceed 160ms.
|
||||
// Use lower duty cycle (25%) to give WiFi more RF access time.
|
||||
// Passive scanning reduces TX interference with WiFi.
|
||||
uint16_t scan_interval_ms = 120; // 120ms interval (within 160ms coex limit)
|
||||
uint16_t scan_window_ms = 30; // 30ms window (25% duty cycle for WiFi breathing room)
|
||||
ScanMode scan_mode = ScanMode::PASSIVE; // Passive scan reduces RF contention
|
||||
uint16_t scan_duration_ms = 10000; // 0 = continuous
|
||||
|
||||
// Connection parameters
|
||||
uint16_t conn_interval_min_ms = 15;
|
||||
uint16_t conn_interval_max_ms = 30;
|
||||
uint16_t conn_latency = 0;
|
||||
uint16_t supervision_timeout_ms = 4000;
|
||||
|
||||
// MTU
|
||||
uint16_t preferred_mtu = MTU::REQUESTED;
|
||||
|
||||
// Limits
|
||||
uint8_t max_connections = Limits::MAX_PEERS;
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Callback Type Definitions
|
||||
//=============================================================================
|
||||
|
||||
namespace Callbacks {
|
||||
// Scan callbacks
|
||||
using OnScanResult = std::function<void(const ScanResult& result)>;
|
||||
using OnScanComplete = std::function<void()>;
|
||||
|
||||
// Connection callbacks (central mode - we initiated)
|
||||
using OnConnected = std::function<void(const ConnectionHandle& conn)>;
|
||||
using OnDisconnected = std::function<void(const ConnectionHandle& conn, uint8_t reason)>;
|
||||
using OnMTUChanged = std::function<void(const ConnectionHandle& conn, uint16_t mtu)>;
|
||||
using OnServicesDiscovered = std::function<void(const ConnectionHandle& conn, bool success)>;
|
||||
|
||||
// Data callbacks
|
||||
using OnDataReceived = std::function<void(const ConnectionHandle& conn, const Bytes& data)>;
|
||||
using OnNotifyEnabled = std::function<void(const ConnectionHandle& conn, bool enabled)>;
|
||||
|
||||
// Peripheral-mode callbacks (they connected to us)
|
||||
using OnCentralConnected = std::function<void(const ConnectionHandle& conn)>;
|
||||
using OnCentralDisconnected = std::function<void(const ConnectionHandle& conn)>;
|
||||
using OnWriteReceived = std::function<void(const ConnectionHandle& conn, const Bytes& data)>;
|
||||
using OnReadRequested = std::function<Bytes(const ConnectionHandle& conn)>;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Utility Functions
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* @brief Get the payload size for a given MTU
|
||||
* @param mtu The negotiated MTU
|
||||
* @return Maximum payload size per fragment
|
||||
*/
|
||||
inline size_t getPayloadSize(uint16_t mtu) {
|
||||
return (mtu > Fragment::HEADER_SIZE) ? (mtu - Fragment::HEADER_SIZE) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if a packet needs fragmentation
|
||||
* @param data_size Size of the data to send
|
||||
* @param mtu The negotiated MTU
|
||||
* @return true if fragmentation is required
|
||||
*/
|
||||
inline bool needsFragmentation(size_t data_size, uint16_t mtu) {
|
||||
return data_size > getPayloadSize(mtu);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculate number of fragments needed
|
||||
* @param data_size Size of the data to fragment
|
||||
* @param mtu The negotiated MTU
|
||||
* @return Number of fragments needed (minimum 1)
|
||||
*/
|
||||
inline uint16_t calculateFragmentCount(size_t data_size, uint16_t mtu) {
|
||||
size_t payload_size = getPayloadSize(mtu);
|
||||
if (payload_size == 0) return 0;
|
||||
return static_cast<uint16_t>((data_size + payload_size - 1) / payload_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert Role to string for logging
|
||||
*/
|
||||
inline const char* roleToString(Role role) {
|
||||
switch (role) {
|
||||
case Role::NONE: return "NONE";
|
||||
case Role::CENTRAL: return "CENTRAL";
|
||||
case Role::PERIPHERAL: return "PERIPHERAL";
|
||||
case Role::DUAL: return "DUAL";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert ConnectionState to string for logging
|
||||
*/
|
||||
inline const char* stateToString(ConnectionState state) {
|
||||
switch (state) {
|
||||
case ConnectionState::DISCONNECTED: return "DISCONNECTED";
|
||||
case ConnectionState::CONNECTING: return "CONNECTING";
|
||||
case ConnectionState::CONNECTED: return "CONNECTED";
|
||||
case ConnectionState::DISCOVERING_SERVICES: return "DISCOVERING_SERVICES";
|
||||
case ConnectionState::READY: return "READY";
|
||||
case ConnectionState::DISCONNECTING: return "DISCONNECTING";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert PeerState to string for logging
|
||||
*/
|
||||
inline const char* peerStateToString(PeerState state) {
|
||||
switch (state) {
|
||||
case PeerState::DISCOVERED: return "DISCOVERED";
|
||||
case PeerState::CONNECTING: return "CONNECTING";
|
||||
case PeerState::HANDSHAKING: return "HANDSHAKING";
|
||||
case PeerState::CONNECTED: return "CONNECTED";
|
||||
case PeerState::DISCONNECTING: return "DISCONNECTING";
|
||||
case PeerState::BLACKLISTED: return "BLACKLISTED";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "ble_interface",
|
||||
"version": "0.1.0",
|
||||
"description": "BLE-Reticulum Protocol v2.2 interface for microReticulum",
|
||||
"keywords": "ble, reticulum, mesh",
|
||||
"license": "MIT",
|
||||
"frameworks": ["arduino"],
|
||||
"platforms": ["espressif32"],
|
||||
"dependencies": {
|
||||
"microReticulum": "*"
|
||||
},
|
||||
"build": {
|
||||
"flags": "-std=gnu++11 -I../../../../deps/microReticulum/src -I../../../../deps/microReticulum/src/BLE"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* @file BluedroidPlatform.h
|
||||
* @brief ESP-IDF Bluedroid implementation of IBLEPlatform for ESP32
|
||||
*
|
||||
* This implementation uses the ESP-IDF Bluedroid stack to provide BLE
|
||||
* functionality on ESP32 devices. It supports both central and peripheral
|
||||
* modes simultaneously (dual-mode operation).
|
||||
*
|
||||
* Alternative to NimBLEPlatform to work around state machine bugs in NimBLE
|
||||
* that cause rc=530/rc=21 errors during dual-role operation.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "../BLEPlatform.h"
|
||||
#include "../BLEOperationQueue.h"
|
||||
|
||||
// Only compile for ESP32 with Bluedroid
|
||||
#if defined(ESP32) && defined(USE_BLUEDROID)
|
||||
|
||||
#include <esp_bt.h>
|
||||
#include <esp_bt_main.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_bt_defs.h>
|
||||
#include <esp_gatt_common_api.h>
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
#include <functional>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
/**
|
||||
* @brief Bluedroid (ESP-IDF) implementation of IBLEPlatform
|
||||
*/
|
||||
class BluedroidPlatform : public IBLEPlatform, public BLEOperationQueue {
|
||||
public:
|
||||
BluedroidPlatform();
|
||||
virtual ~BluedroidPlatform();
|
||||
|
||||
//=========================================================================
|
||||
// IBLEPlatform Implementation
|
||||
//=========================================================================
|
||||
|
||||
// Lifecycle
|
||||
bool initialize(const PlatformConfig& config) override;
|
||||
bool start() override;
|
||||
void stop() override;
|
||||
void loop() override;
|
||||
void shutdown() override;
|
||||
bool isRunning() const override;
|
||||
|
||||
// Central mode - Scanning
|
||||
bool startScan(uint16_t duration_ms = 0) override;
|
||||
void stopScan() override;
|
||||
bool isScanning() const override;
|
||||
|
||||
// Central mode - Connections
|
||||
bool connect(const BLEAddress& address, uint16_t timeout_ms = 10000) override;
|
||||
bool disconnect(uint16_t conn_handle) override;
|
||||
void disconnectAll() override;
|
||||
bool requestMTU(uint16_t conn_handle, uint16_t mtu) override;
|
||||
bool discoverServices(uint16_t conn_handle) override;
|
||||
|
||||
// Peripheral mode
|
||||
bool startAdvertising() override;
|
||||
void stopAdvertising() override;
|
||||
bool isAdvertising() const override;
|
||||
bool setAdvertisingData(const Bytes& data) override;
|
||||
void setIdentityData(const Bytes& identity) override;
|
||||
|
||||
// GATT Operations
|
||||
bool write(uint16_t conn_handle, const Bytes& data, bool response = true) override;
|
||||
bool read(uint16_t conn_handle, uint16_t char_handle,
|
||||
std::function<void(OperationResult, const Bytes&)> callback) override;
|
||||
bool enableNotifications(uint16_t conn_handle, bool enable) override;
|
||||
bool notify(uint16_t conn_handle, const Bytes& data) override;
|
||||
bool notifyAll(const Bytes& data) override;
|
||||
|
||||
// Connection management
|
||||
std::vector<ConnectionHandle> getConnections() const override;
|
||||
ConnectionHandle getConnection(uint16_t handle) const override;
|
||||
size_t getConnectionCount() const override;
|
||||
bool isConnectedTo(const BLEAddress& address) const override;
|
||||
|
||||
// Callback registration
|
||||
void setOnScanResult(Callbacks::OnScanResult callback) override;
|
||||
void setOnScanComplete(Callbacks::OnScanComplete callback) override;
|
||||
void setOnConnected(Callbacks::OnConnected callback) override;
|
||||
void setOnDisconnected(Callbacks::OnDisconnected callback) override;
|
||||
void setOnMTUChanged(Callbacks::OnMTUChanged callback) override;
|
||||
void setOnServicesDiscovered(Callbacks::OnServicesDiscovered callback) override;
|
||||
void setOnDataReceived(Callbacks::OnDataReceived callback) override;
|
||||
void setOnNotifyEnabled(Callbacks::OnNotifyEnabled callback) override;
|
||||
void setOnCentralConnected(Callbacks::OnCentralConnected callback) override;
|
||||
void setOnCentralDisconnected(Callbacks::OnCentralDisconnected callback) override;
|
||||
void setOnWriteReceived(Callbacks::OnWriteReceived callback) override;
|
||||
void setOnReadRequested(Callbacks::OnReadRequested callback) override;
|
||||
|
||||
// Platform info
|
||||
PlatformType getPlatformType() const override { return PlatformType::ESP_IDF; }
|
||||
std::string getPlatformName() const override { return "Bluedroid"; }
|
||||
BLEAddress getLocalAddress() const override;
|
||||
|
||||
protected:
|
||||
// BLEOperationQueue implementation
|
||||
bool executeOperation(const GATTOperation& op) override;
|
||||
|
||||
private:
|
||||
//=========================================================================
|
||||
// Static Callback Handlers (Bluedroid requires static functions)
|
||||
//=========================================================================
|
||||
|
||||
static void gapEventHandler(esp_gap_ble_cb_event_t event,
|
||||
esp_ble_gap_cb_param_t* param);
|
||||
static void gattsEventHandler(esp_gatts_cb_event_t event,
|
||||
esp_gatt_if_t gatts_if,
|
||||
esp_ble_gatts_cb_param_t* param);
|
||||
static void gattcEventHandler(esp_gattc_cb_event_t event,
|
||||
esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t* param);
|
||||
|
||||
// Singleton instance for static callback routing
|
||||
static BluedroidPlatform* _instance;
|
||||
|
||||
//=========================================================================
|
||||
// State Machines
|
||||
//=========================================================================
|
||||
|
||||
enum class InitState {
|
||||
UNINITIALIZED,
|
||||
CONTROLLER_INIT,
|
||||
BLUEDROID_INIT,
|
||||
CALLBACKS_REGISTERED,
|
||||
GATTS_REGISTERING,
|
||||
GATTS_CREATING_SERVICE,
|
||||
GATTS_ADDING_CHARS,
|
||||
GATTS_STARTING_SERVICE,
|
||||
GATTC_REGISTERING,
|
||||
READY
|
||||
};
|
||||
|
||||
enum class ScanState {
|
||||
IDLE,
|
||||
SETTING_PARAMS,
|
||||
STARTING,
|
||||
ACTIVE,
|
||||
STOPPING
|
||||
};
|
||||
|
||||
enum class AdvState {
|
||||
IDLE,
|
||||
CONFIGURING_DATA,
|
||||
CONFIGURING_SCAN_RSP,
|
||||
STARTING,
|
||||
ACTIVE,
|
||||
STOPPING
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// Internal Event Handlers
|
||||
//=========================================================================
|
||||
|
||||
// GAP events
|
||||
void handleGapEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t* param);
|
||||
void handleScanResult(esp_ble_gap_cb_param_t* param);
|
||||
void handleScanComplete();
|
||||
void handleAdvStart(esp_bt_status_t status);
|
||||
void handleAdvStop();
|
||||
|
||||
// GATTS events (peripheral/server)
|
||||
void handleGattsEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||
esp_ble_gatts_cb_param_t* param);
|
||||
void handleGattsRegister(esp_gatt_if_t gatts_if, esp_gatt_status_t status);
|
||||
void handleGattsServiceCreated(uint16_t service_handle);
|
||||
void handleGattsCharAdded(uint16_t attr_handle, esp_bt_uuid_t* char_uuid);
|
||||
void handleGattsServiceStarted();
|
||||
void handleGattsConnect(esp_ble_gatts_cb_param_t* param);
|
||||
void handleGattsDisconnect(esp_ble_gatts_cb_param_t* param);
|
||||
void handleGattsWrite(esp_ble_gatts_cb_param_t* param);
|
||||
void handleGattsRead(esp_ble_gatts_cb_param_t* param);
|
||||
void handleGattsMtuChange(esp_ble_gatts_cb_param_t* param);
|
||||
void handleGattsConfirm(esp_ble_gatts_cb_param_t* param);
|
||||
|
||||
// GATTC events (central/client)
|
||||
void handleGattcEvent(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t* param);
|
||||
void handleGattcRegister(esp_gatt_if_t gattc_if, esp_gatt_status_t status);
|
||||
void handleGattcConnect(esp_ble_gattc_cb_param_t* param);
|
||||
void handleGattcDisconnect(esp_ble_gattc_cb_param_t* param);
|
||||
void handleGattcSearchResult(esp_ble_gattc_cb_param_t* param);
|
||||
void handleGattcSearchComplete(esp_ble_gattc_cb_param_t* param);
|
||||
void handleGattcGetChar(esp_ble_gattc_cb_param_t* param);
|
||||
void handleGattcNotify(esp_ble_gattc_cb_param_t* param);
|
||||
void handleGattcWrite(esp_ble_gattc_cb_param_t* param);
|
||||
void handleGattcRead(esp_ble_gattc_cb_param_t* param);
|
||||
|
||||
//=========================================================================
|
||||
// Setup Methods
|
||||
//=========================================================================
|
||||
|
||||
bool initBluetooth();
|
||||
bool setupGattsService();
|
||||
void buildAdvertisingData();
|
||||
void buildScanResponseData();
|
||||
|
||||
//=========================================================================
|
||||
// Address Conversion
|
||||
//=========================================================================
|
||||
|
||||
static BLEAddress fromEspBdAddr(const esp_bd_addr_t addr, esp_ble_addr_type_t type);
|
||||
static void toEspBdAddr(const BLEAddress& addr, esp_bd_addr_t out_addr);
|
||||
|
||||
//=========================================================================
|
||||
// Connection Management
|
||||
//=========================================================================
|
||||
|
||||
struct BluedroidConnection {
|
||||
uint16_t conn_id = 0xFFFF;
|
||||
esp_bd_addr_t peer_addr = {0};
|
||||
esp_ble_addr_type_t addr_type = BLE_ADDR_TYPE_PUBLIC;
|
||||
Role local_role = Role::NONE;
|
||||
uint16_t mtu = MTU::MINIMUM;
|
||||
bool notifications_enabled = false;
|
||||
|
||||
// Client-mode discovery handles (when we connect to a peripheral)
|
||||
uint16_t service_start_handle = 0;
|
||||
uint16_t service_end_handle = 0;
|
||||
uint16_t rx_char_handle = 0;
|
||||
uint16_t tx_char_handle = 0;
|
||||
uint16_t tx_cccd_handle = 0;
|
||||
uint16_t identity_char_handle = 0;
|
||||
|
||||
// Discovery state
|
||||
enum class DiscoveryState {
|
||||
NONE,
|
||||
SEARCHING_SERVICE,
|
||||
GETTING_CHARS,
|
||||
GETTING_DESCRIPTORS,
|
||||
COMPLETE
|
||||
} discovery_state = DiscoveryState::NONE;
|
||||
};
|
||||
|
||||
uint16_t allocateConnHandle();
|
||||
void freeConnHandle(uint16_t handle);
|
||||
BluedroidConnection* findConnection(uint16_t conn_id);
|
||||
BluedroidConnection* findConnectionByAddress(const esp_bd_addr_t addr);
|
||||
|
||||
//=========================================================================
|
||||
// Member Variables
|
||||
//=========================================================================
|
||||
|
||||
// Configuration
|
||||
PlatformConfig _config;
|
||||
bool _initialized = false;
|
||||
bool _running = false;
|
||||
Bytes _identity_data;
|
||||
Bytes _custom_adv_data;
|
||||
|
||||
// State machines
|
||||
InitState _init_state = InitState::UNINITIALIZED;
|
||||
ScanState _scan_state = ScanState::IDLE;
|
||||
AdvState _adv_state = AdvState::IDLE;
|
||||
|
||||
// GATT interfaces (from registration events)
|
||||
esp_gatt_if_t _gatts_if = ESP_GATT_IF_NONE;
|
||||
esp_gatt_if_t _gattc_if = ESP_GATT_IF_NONE;
|
||||
|
||||
// GATTS handles (server/peripheral mode)
|
||||
uint16_t _service_handle = 0;
|
||||
uint16_t _rx_char_handle = 0; // RX characteristic (central writes here)
|
||||
uint16_t _tx_char_handle = 0; // TX characteristic (we notify from here)
|
||||
uint16_t _tx_cccd_handle = 0; // TX CCCD (for notification enable/disable)
|
||||
uint16_t _identity_char_handle = 0; // Identity characteristic (central reads)
|
||||
|
||||
// Service creation state tracking
|
||||
uint8_t _chars_added = 0;
|
||||
static constexpr uint8_t CHARS_EXPECTED = 3; // RX, TX, Identity
|
||||
|
||||
// Scan timing
|
||||
uint16_t _scan_duration_ms = 0;
|
||||
unsigned long _scan_start_time = 0;
|
||||
|
||||
// Connection tracking
|
||||
std::map<uint16_t, BluedroidConnection> _connections;
|
||||
uint16_t _next_conn_handle = 1;
|
||||
|
||||
// Pending connection state
|
||||
volatile bool _connect_pending = false;
|
||||
volatile bool _connect_success = false;
|
||||
volatile int _connect_error = 0;
|
||||
BLEAddress _pending_connect_address;
|
||||
unsigned long _connect_start_time = 0;
|
||||
uint16_t _connect_timeout_ms = 10000;
|
||||
|
||||
// Service discovery serialization (Bluedroid can only handle one at a time)
|
||||
uint16_t _discovery_in_progress = 0xFFFF; // conn_handle of active discovery, or 0xFFFF if none
|
||||
std::queue<uint16_t> _pending_discoveries; // queued conn_handles waiting for discovery
|
||||
|
||||
// Local address cache
|
||||
mutable esp_bd_addr_t _local_addr = {0};
|
||||
mutable bool _local_addr_valid = false;
|
||||
|
||||
// App IDs for GATT profiles
|
||||
static constexpr uint16_t GATTS_APP_ID = 0;
|
||||
static constexpr uint16_t GATTC_APP_ID = 1;
|
||||
|
||||
//=========================================================================
|
||||
// Callbacks (application-level)
|
||||
//=========================================================================
|
||||
|
||||
Callbacks::OnScanResult _on_scan_result;
|
||||
Callbacks::OnScanComplete _on_scan_complete;
|
||||
Callbacks::OnConnected _on_connected;
|
||||
Callbacks::OnDisconnected _on_disconnected;
|
||||
Callbacks::OnMTUChanged _on_mtu_changed;
|
||||
Callbacks::OnServicesDiscovered _on_services_discovered;
|
||||
Callbacks::OnDataReceived _on_data_received;
|
||||
Callbacks::OnNotifyEnabled _on_notify_enabled;
|
||||
Callbacks::OnCentralConnected _on_central_connected;
|
||||
Callbacks::OnCentralDisconnected _on_central_disconnected;
|
||||
Callbacks::OnWriteReceived _on_write_received;
|
||||
Callbacks::OnReadRequested _on_read_requested;
|
||||
};
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
|
||||
#endif // ESP32 && USE_BLUEDROID
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* @file NimBLEPlatform.h
|
||||
* @brief NimBLE-Arduino implementation of IBLEPlatform for ESP32
|
||||
*
|
||||
* This implementation uses the NimBLE-Arduino library to provide BLE
|
||||
* functionality on ESP32 devices. It supports both central and peripheral
|
||||
* modes simultaneously (dual-mode operation).
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "../BLEPlatform.h"
|
||||
#include "../BLEOperationQueue.h"
|
||||
|
||||
// Only compile for ESP32 with NimBLE
|
||||
#if defined(ESP32) && (defined(USE_NIMBLE) || defined(CONFIG_BT_NIMBLE_ENABLED))
|
||||
|
||||
#include <NimBLEDevice.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
// Undefine NimBLE's backward compatibility macros to avoid conflict with our types
|
||||
#undef BLEAddress
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace RNS { namespace BLE {
|
||||
|
||||
//=============================================================================
|
||||
// State Machine Enums for Dual-Role BLE Operation
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* @brief Master role states (Central - scanning/connecting)
|
||||
*/
|
||||
enum class MasterState : uint8_t {
|
||||
IDLE, ///< No master operations
|
||||
SCAN_STARTING, ///< Gap scan start requested
|
||||
SCANNING, ///< Actively scanning
|
||||
SCAN_STOPPING, ///< Gap scan stop requested
|
||||
CONN_STARTING, ///< Connection initiation requested
|
||||
CONNECTING, ///< Connection in progress
|
||||
CONN_CANCELING ///< Connection cancel requested
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Slave role states (Peripheral - advertising)
|
||||
*/
|
||||
enum class SlaveState : uint8_t {
|
||||
IDLE, ///< Not advertising
|
||||
ADV_STARTING, ///< Gap adv start requested
|
||||
ADVERTISING, ///< Actively advertising
|
||||
ADV_STOPPING ///< Gap adv stop requested
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief GAP coordinator state (overall BLE subsystem)
|
||||
*/
|
||||
enum class GAPState : uint8_t {
|
||||
UNINITIALIZED, ///< BLE not started
|
||||
INITIALIZING, ///< NimBLE init in progress
|
||||
READY, ///< Idle, ready for operations
|
||||
MASTER_PRIORITY, ///< Master operation in progress, slave paused
|
||||
SLAVE_PRIORITY, ///< Slave operation in progress, master paused
|
||||
TRANSITIONING, ///< State change in progress
|
||||
ERROR_RECOVERY ///< Recovering from error
|
||||
};
|
||||
|
||||
// State name helpers for logging
|
||||
const char* masterStateName(MasterState state);
|
||||
const char* slaveStateName(SlaveState state);
|
||||
const char* gapStateName(GAPState state);
|
||||
|
||||
/**
|
||||
* @brief NimBLE-Arduino implementation of IBLEPlatform
|
||||
*/
|
||||
class NimBLEPlatform : public IBLEPlatform,
|
||||
public BLEOperationQueue,
|
||||
public NimBLEServerCallbacks,
|
||||
public NimBLECharacteristicCallbacks,
|
||||
public NimBLEClientCallbacks,
|
||||
public NimBLEScanCallbacks {
|
||||
public:
|
||||
NimBLEPlatform();
|
||||
virtual ~NimBLEPlatform();
|
||||
|
||||
//=========================================================================
|
||||
// IBLEPlatform Implementation
|
||||
//=========================================================================
|
||||
|
||||
// Lifecycle
|
||||
bool initialize(const PlatformConfig& config) override;
|
||||
bool start() override;
|
||||
void stop() override;
|
||||
void loop() override;
|
||||
void shutdown() override;
|
||||
bool isRunning() const override;
|
||||
|
||||
// Central mode - Scanning
|
||||
bool startScan(uint16_t duration_ms = 0) override;
|
||||
void stopScan() override;
|
||||
bool isScanning() const override;
|
||||
|
||||
// Central mode - Connections
|
||||
bool connect(const BLEAddress& address, uint16_t timeout_ms = 10000) override;
|
||||
bool disconnect(uint16_t conn_handle) override;
|
||||
void disconnectAll() override;
|
||||
bool requestMTU(uint16_t conn_handle, uint16_t mtu) override;
|
||||
bool discoverServices(uint16_t conn_handle) override;
|
||||
|
||||
// Peripheral mode
|
||||
bool startAdvertising() override;
|
||||
void stopAdvertising() override;
|
||||
bool isAdvertising() const override;
|
||||
bool setAdvertisingData(const Bytes& data) override;
|
||||
void setIdentityData(const Bytes& identity) override;
|
||||
|
||||
// GATT Operations
|
||||
bool write(uint16_t conn_handle, const Bytes& data, bool response = true) override;
|
||||
bool read(uint16_t conn_handle, uint16_t char_handle,
|
||||
std::function<void(OperationResult, const Bytes&)> callback) override;
|
||||
bool enableNotifications(uint16_t conn_handle, bool enable) override;
|
||||
bool notify(uint16_t conn_handle, const Bytes& data) override;
|
||||
bool notifyAll(const Bytes& data) override;
|
||||
|
||||
// Connection management
|
||||
std::vector<ConnectionHandle> getConnections() const override;
|
||||
ConnectionHandle getConnection(uint16_t handle) const override;
|
||||
size_t getConnectionCount() const override;
|
||||
bool isConnectedTo(const BLEAddress& address) const override;
|
||||
|
||||
// Callback registration
|
||||
void setOnScanResult(Callbacks::OnScanResult callback) override;
|
||||
void setOnScanComplete(Callbacks::OnScanComplete callback) override;
|
||||
void setOnConnected(Callbacks::OnConnected callback) override;
|
||||
void setOnDisconnected(Callbacks::OnDisconnected callback) override;
|
||||
void setOnMTUChanged(Callbacks::OnMTUChanged callback) override;
|
||||
void setOnServicesDiscovered(Callbacks::OnServicesDiscovered callback) override;
|
||||
void setOnDataReceived(Callbacks::OnDataReceived callback) override;
|
||||
void setOnNotifyEnabled(Callbacks::OnNotifyEnabled callback) override;
|
||||
void setOnCentralConnected(Callbacks::OnCentralConnected callback) override;
|
||||
void setOnCentralDisconnected(Callbacks::OnCentralDisconnected callback) override;
|
||||
void setOnWriteReceived(Callbacks::OnWriteReceived callback) override;
|
||||
void setOnReadRequested(Callbacks::OnReadRequested callback) override;
|
||||
|
||||
// Platform info
|
||||
PlatformType getPlatformType() const override { return PlatformType::NIMBLE_ARDUINO; }
|
||||
std::string getPlatformName() const override { return "NimBLE-Arduino"; }
|
||||
BLEAddress getLocalAddress() const override;
|
||||
|
||||
//=========================================================================
|
||||
// NimBLEServerCallbacks (Peripheral mode)
|
||||
//=========================================================================
|
||||
|
||||
void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override;
|
||||
void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override;
|
||||
void onMTUChange(uint16_t MTU, NimBLEConnInfo& connInfo) override;
|
||||
|
||||
//=========================================================================
|
||||
// NimBLECharacteristicCallbacks
|
||||
//=========================================================================
|
||||
|
||||
void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override;
|
||||
void onRead(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override;
|
||||
void onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo,
|
||||
uint16_t subValue) override;
|
||||
|
||||
//=========================================================================
|
||||
// NimBLEClientCallbacks (Central mode)
|
||||
//=========================================================================
|
||||
|
||||
void onConnect(NimBLEClient* pClient) override;
|
||||
void onConnectFail(NimBLEClient* pClient, int reason) override;
|
||||
void onDisconnect(NimBLEClient* pClient, int reason) override;
|
||||
|
||||
//=========================================================================
|
||||
// NimBLEScanCallbacks (Scanning)
|
||||
//=========================================================================
|
||||
|
||||
void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override;
|
||||
void onScanEnd(const NimBLEScanResults& results, int reason) override;
|
||||
|
||||
protected:
|
||||
// BLEOperationQueue implementation
|
||||
bool executeOperation(const GATTOperation& op) override;
|
||||
|
||||
private:
|
||||
// Setup methods
|
||||
bool setupServer();
|
||||
bool setupAdvertising();
|
||||
bool setupScan();
|
||||
|
||||
// Address conversion
|
||||
static BLEAddress fromNimBLE(const NimBLEAddress& addr);
|
||||
static NimBLEAddress toNimBLE(const BLEAddress& addr);
|
||||
|
||||
// Find client by connection handle or address
|
||||
NimBLEClient* findClient(uint16_t conn_handle);
|
||||
NimBLEClient* findClient(const BLEAddress& address);
|
||||
|
||||
// Connection handle management
|
||||
uint16_t allocateConnHandle();
|
||||
void freeConnHandle(uint16_t handle);
|
||||
|
||||
// Update connection info
|
||||
void updateConnectionMTU(uint16_t conn_handle, uint16_t mtu);
|
||||
|
||||
// Check if a device address is currently connected
|
||||
bool isDeviceConnected(const std::string& addrKey) const;
|
||||
|
||||
//=========================================================================
|
||||
// State Machine Infrastructure
|
||||
//=========================================================================
|
||||
|
||||
// State variables (protected by spinlock)
|
||||
mutable portMUX_TYPE _state_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
MasterState _master_state = MasterState::IDLE;
|
||||
SlaveState _slave_state = SlaveState::IDLE;
|
||||
GAPState _gap_state = GAPState::UNINITIALIZED;
|
||||
|
||||
// Mutex for connection map access (longer operations)
|
||||
SemaphoreHandle_t _conn_mutex = nullptr;
|
||||
|
||||
// State transition helpers (atomic compare-and-swap)
|
||||
bool transitionMasterState(MasterState expected, MasterState new_state);
|
||||
bool transitionSlaveState(SlaveState expected, SlaveState new_state);
|
||||
bool transitionGAPState(GAPState expected, GAPState new_state);
|
||||
|
||||
// State verification methods
|
||||
bool canStartScan() const;
|
||||
bool canStartAdvertising() const;
|
||||
bool canConnect() const;
|
||||
|
||||
// Operation coordination
|
||||
bool pauseSlaveForMaster();
|
||||
void resumeSlave();
|
||||
void enterErrorRecovery();
|
||||
|
||||
// Track if slave was paused for a master operation
|
||||
bool _slave_paused_for_master = false;
|
||||
|
||||
//=========================================================================
|
||||
// Configuration
|
||||
//=========================================================================
|
||||
PlatformConfig _config;
|
||||
bool _initialized = false;
|
||||
bool _running = false;
|
||||
Bytes _identity_data;
|
||||
unsigned long _scan_stop_time = 0; // millis() when to stop continuous scan
|
||||
|
||||
// BLE stack recovery
|
||||
uint8_t _scan_fail_count = 0;
|
||||
uint8_t _lightweight_reset_fails = 0;
|
||||
uint8_t _conn_establish_fail_count = 0; // rc=574 connection establishment failures
|
||||
unsigned long _last_full_recovery_time = 0;
|
||||
static constexpr uint8_t SCAN_FAIL_RECOVERY_THRESHOLD = 5;
|
||||
static constexpr uint8_t LIGHTWEIGHT_RESET_MAX_FAILS = 3;
|
||||
static constexpr uint8_t CONN_ESTABLISH_FAIL_THRESHOLD = 3; // Threshold for rc=574
|
||||
static constexpr unsigned long FULL_RECOVERY_COOLDOWN_MS = 60000; // 60 seconds
|
||||
bool recoverBLEStack();
|
||||
|
||||
// NimBLE objects
|
||||
NimBLEServer* _server = nullptr;
|
||||
NimBLEService* _service = nullptr;
|
||||
NimBLECharacteristic* _rx_char = nullptr;
|
||||
NimBLECharacteristic* _tx_char = nullptr;
|
||||
NimBLECharacteristic* _identity_char = nullptr;
|
||||
NimBLEScan* _scan = nullptr;
|
||||
NimBLEAdvertising* _advertising_obj = nullptr;
|
||||
|
||||
// Client connections (as central)
|
||||
std::map<uint16_t, NimBLEClient*> _clients;
|
||||
|
||||
// Connection tracking
|
||||
std::map<uint16_t, ConnectionHandle> _connections;
|
||||
|
||||
// Cached scan results for connection (stores full device info from scan)
|
||||
// Key: MAC address as string (e.g., "b8:27:eb:43:04:bc")
|
||||
std::map<std::string, NimBLEAdvertisedDevice> _discovered_devices;
|
||||
|
||||
// Insertion-order tracking for FIFO eviction of discovered devices
|
||||
std::vector<std::string> _discovered_order;
|
||||
|
||||
// Connection handle allocator (NimBLE uses its own, we wrap for consistency)
|
||||
uint16_t _next_conn_handle = 1;
|
||||
|
||||
// VOLATILE RATIONALE: NimBLE callback synchronization flags
|
||||
//
|
||||
// These volatile flags synchronize between:
|
||||
// 1. NimBLE host task (callback context - runs asynchronously like ISR)
|
||||
// 2. BLE task (loop() context - application thread)
|
||||
//
|
||||
// Volatile is appropriate because:
|
||||
// - Single-word reads/writes are atomic on ESP32 (32-bit aligned)
|
||||
// - These are simple status flags, not complex state
|
||||
// - Mutex would cause priority inversion in callback context
|
||||
// - Memory barriers not needed - flag semantics sufficient
|
||||
//
|
||||
// Alternative rejected: Mutex acquisition in NimBLE callbacks can cause
|
||||
// priority inversion or deadlock since callbacks run in host task context.
|
||||
//
|
||||
// Reference: ESP32 Technical Reference Manual, Section 5.4 (Memory Consistency)
|
||||
|
||||
// Async connection tracking (NimBLEClientCallbacks)
|
||||
volatile bool _async_connect_pending = false;
|
||||
volatile bool _async_connect_failed = false;
|
||||
volatile int _async_connect_error = 0;
|
||||
|
||||
// VOLATILE RATIONALE: Native GAP handler callback flags
|
||||
// Same rationale as above - nativeGapEventHandler runs in NimBLE host task.
|
||||
// These track connection state during ble_gap_connect() operations.
|
||||
volatile bool _native_connect_pending = false;
|
||||
volatile bool _native_connect_success = false;
|
||||
volatile int _native_connect_result = 0;
|
||||
volatile uint16_t _native_connect_handle = 0;
|
||||
BLEAddress _native_connect_address;
|
||||
|
||||
// Native GAP event handler
|
||||
static int nativeGapEventHandler(struct ble_gap_event* event, void* arg);
|
||||
bool connectNative(const BLEAddress& address, uint16_t timeout_ms);
|
||||
|
||||
// Callbacks
|
||||
Callbacks::OnScanResult _on_scan_result;
|
||||
Callbacks::OnScanComplete _on_scan_complete;
|
||||
Callbacks::OnConnected _on_connected;
|
||||
Callbacks::OnDisconnected _on_disconnected;
|
||||
Callbacks::OnMTUChanged _on_mtu_changed;
|
||||
Callbacks::OnServicesDiscovered _on_services_discovered;
|
||||
Callbacks::OnDataReceived _on_data_received;
|
||||
Callbacks::OnNotifyEnabled _on_notify_enabled;
|
||||
Callbacks::OnCentralConnected _on_central_connected;
|
||||
Callbacks::OnCentralDisconnected _on_central_disconnected;
|
||||
Callbacks::OnWriteReceived _on_write_received;
|
||||
Callbacks::OnReadRequested _on_read_requested;
|
||||
|
||||
//=========================================================================
|
||||
// BLE Shutdown Safety (CONC-H4, CONC-M4)
|
||||
//=========================================================================
|
||||
|
||||
// Unclean shutdown flag - set if forced shutdown occurred with active operations
|
||||
// Uses RTC_NOINIT_ATTR on ESP32 for persistence across soft reboot
|
||||
static bool _unclean_shutdown;
|
||||
|
||||
// Active write operation tracking (atomic for callback safety)
|
||||
std::atomic<int> _active_write_count{0};
|
||||
|
||||
public:
|
||||
/**
|
||||
* Check if there are active write operations in progress.
|
||||
* Write operations are critical - interrupting can corrupt peer state.
|
||||
*/
|
||||
bool hasActiveWriteOperations() const { return _active_write_count.load() > 0; }
|
||||
|
||||
/**
|
||||
* Check if last shutdown was clean.
|
||||
* Returns false if BLE was force-closed with active operations.
|
||||
*/
|
||||
static bool wasCleanShutdown() { return !_unclean_shutdown; }
|
||||
|
||||
/**
|
||||
* Clear unclean shutdown flag (call after boot verification).
|
||||
*/
|
||||
static void clearUncleanShutdownFlag() { _unclean_shutdown = false; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* Mark a write operation as starting (call before characteristic write).
|
||||
*/
|
||||
void beginWriteOperation() { _active_write_count.fetch_add(1); }
|
||||
|
||||
/**
|
||||
* Mark a write operation as complete (call after write callback).
|
||||
*/
|
||||
void endWriteOperation() { _active_write_count.fetch_sub(1); }
|
||||
};
|
||||
|
||||
}} // namespace RNS::BLE
|
||||
|
||||
#endif // ESP32 && USE_NIMBLE
|
||||
Reference in New Issue
Block a user