Add Gate 2B BLE peer session manager native tests
This commit is contained in:
parent
a7b1ca02de
commit
45827c1220
5 changed files with 990 additions and 0 deletions
|
|
@ -0,0 +1,107 @@
|
|||
# Gate 2B: BLEPeerSessionManager Native Tests
|
||||
|
||||
Date: 2026-05-18 14:45 America/Los_Angeles
|
||||
|
||||
Scope: C++ native skeleton and pure native unit tests only.
|
||||
|
||||
## Summary
|
||||
|
||||
Gate 2B implemented a standalone C++ `BLEPeerSessionManager` skeleton under `migration/protocol_core` with no pybind11 bindings and no integration into live Python BLE behavior.
|
||||
|
||||
Files added:
|
||||
|
||||
- `migration/protocol_core/BLEPeerSessionManager.h`
|
||||
- `migration/protocol_core/BLEPeerSessionManager.cpp`
|
||||
- `migration/tests/native/test_ble_peer_session_manager.cpp`
|
||||
- `migration/sql/mark_gate2b_protocol_session_native_tests_20260518_1445.sql`
|
||||
|
||||
`BLEInterface.py` was not modified. No BlueZ, Bleak, DBus, `RNS.Transport`, `BLEPeerInterface`, ESP32 BLE APIs, or live BLE tests were used.
|
||||
|
||||
## Implemented C++ Surface
|
||||
|
||||
Implemented types and enums:
|
||||
|
||||
- `PeerIdentity`
|
||||
- `LocalRole`
|
||||
- `InputDecision`
|
||||
- `SessionActionType`
|
||||
- `ConnectionId`
|
||||
- `ConnectionSnapshot`
|
||||
- `SessionAction`
|
||||
- `HandshakeResult`
|
||||
- `PeerSessionView`
|
||||
- `BLEPeerSessionManager`
|
||||
|
||||
Implemented helpers:
|
||||
|
||||
- `isIdentityHandshakePayload`
|
||||
- `identityFromPayload`
|
||||
- `computeIdentityKey`
|
||||
- `computeFragmenterKey`
|
||||
|
||||
Implemented session methods:
|
||||
|
||||
- `handleIdentityHandshake`
|
||||
- `markConnected`
|
||||
- `markDisconnected`
|
||||
- `markMtu`
|
||||
- `markPendingIdentity`
|
||||
- `removePendingIdentity`
|
||||
- `expiredPendingIdentities`
|
||||
- `sessionByAddress`
|
||||
- `sessionByIdentity`
|
||||
|
||||
## Behavior Covered
|
||||
|
||||
The C++ skeleton covers the Gate 2B reference cases:
|
||||
|
||||
| Case | Result |
|
||||
|---|---|
|
||||
| non-16-byte payload | `PassToReassembler`, `consumed=false` |
|
||||
| new 16-byte identity | `AcceptedNewIdentity`, `consumed=true`, `accepted=true`, keys set |
|
||||
| known identity duplicate same | `ConsumedDuplicateSameIdentity`, `consumed=true` |
|
||||
| known identity duplicate mismatch | `ConsumedDuplicateMismatchedIdentity`, `Warn`, `consumed=true` |
|
||||
| duplicate identity active elsewhere | `RejectedDuplicateIdentity`, `DisconnectCurrentPeer` |
|
||||
| duplicate identity with stale/pending detach | accept new identity, `CleanupOldAddress`, `UpdatePeerAddress` |
|
||||
| duplicate identity with zombie old connection | accept new identity, `DisconnectOldPeer`, `CleanupOldAddress` |
|
||||
| MTU provided | result/session MTU equals provided value |
|
||||
| MTU missing | MTU falls back to `23` |
|
||||
| pending identity timeout | expired connection IDs returned without platform calls |
|
||||
| peer address update | session address changes, fragmenter key is unchanged |
|
||||
| identity key helper | first 8 bytes as 16 lowercase hex chars |
|
||||
| fragmenter key helper | full 16 bytes as 32 lowercase hex chars |
|
||||
| invalid identity payload | `identityFromPayload` throws `std::invalid_argument` |
|
||||
|
||||
## Build And Test Instructions
|
||||
|
||||
Build:
|
||||
|
||||
```sh
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -Imigration/protocol_core migration/protocol_core/BLEPeerSessionManager.cpp migration/tests/native/test_ble_peer_session_manager.cpp -o /tmp/test_ble_peer_session_manager
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
/tmp/test_ble_peer_session_manager
|
||||
```
|
||||
|
||||
Observed result:
|
||||
|
||||
```text
|
||||
BLEPeerSessionManager native tests passed
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
The manager currently uses standard containers for native/Linux development. Gate 2G should decide whether the ESP32/microReticulum build uses the same public API with fixed-size pools internally.
|
||||
|
||||
The core returns adapter actions only. It does not log, disconnect peers, create Python interfaces, touch Reticulum transport state, or call BLE platform APIs.
|
||||
|
||||
## SQL
|
||||
|
||||
Companion SQL:
|
||||
|
||||
`migration/sql/mark_gate2b_protocol_session_native_tests_20260518_1445.sql`
|
||||
|
||||
The SQL marks `_handle_identity_handshake` as `NATIVE_TESTED` for phase `2_ble_protocol_session_manager` because the native tests passed. It does not mark anything `FIELD_ACCEPTED` and does not alter Phase 1 field-accepted rows.
|
||||
349
migration/protocol_core/BLEPeerSessionManager.cpp
Normal file
349
migration/protocol_core/BLEPeerSessionManager.cpp
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
#include "BLEPeerSessionManager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace ble_reticulum {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t BLE_MINIMUM_MTU = 23;
|
||||
|
||||
SessionAction make_action(SessionActionType type, const ConnectionId& target) {
|
||||
SessionAction action;
|
||||
action.type = type;
|
||||
action.target = target;
|
||||
return action;
|
||||
}
|
||||
|
||||
SessionAction make_message_action(SessionActionType type,
|
||||
const ConnectionId& target,
|
||||
const std::string& message) {
|
||||
SessionAction action = make_action(type, target);
|
||||
action.message = message;
|
||||
return action;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BLEPeerSessionManager::BLEPeerSessionManager(double pending_identity_timeout,
|
||||
double zombie_timeout)
|
||||
: pending_identity_timeout_(pending_identity_timeout),
|
||||
zombie_timeout_(zombie_timeout) {}
|
||||
|
||||
bool BLEPeerSessionManager::isIdentityHandshakePayload(const uint8_t* data,
|
||||
size_t data_size) {
|
||||
return data != nullptr && data_size == 16;
|
||||
}
|
||||
|
||||
PeerIdentity BLEPeerSessionManager::identityFromPayload(const uint8_t* data,
|
||||
size_t data_size) {
|
||||
if (!isIdentityHandshakePayload(data, data_size)) {
|
||||
throw std::invalid_argument("identity payload must be exactly 16 bytes");
|
||||
}
|
||||
|
||||
PeerIdentity identity{};
|
||||
std::copy(data, data + identity.size(), identity.begin());
|
||||
return identity;
|
||||
}
|
||||
|
||||
std::string BLEPeerSessionManager::computeIdentityKey(const PeerIdentity& identity) {
|
||||
static constexpr char hex[] = "0123456789abcdef";
|
||||
std::string out;
|
||||
out.reserve(16);
|
||||
for (size_t i = 0; i < 8; ++i) {
|
||||
const uint8_t byte = identity[i];
|
||||
out.push_back(hex[(byte >> 4) & 0x0f]);
|
||||
out.push_back(hex[byte & 0x0f]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string BLEPeerSessionManager::computeFragmenterKey(const PeerIdentity& identity) {
|
||||
static constexpr char hex[] = "0123456789abcdef";
|
||||
std::string out;
|
||||
out.reserve(32);
|
||||
for (uint8_t byte : identity) {
|
||||
out.push_back(hex[(byte >> 4) & 0x0f]);
|
||||
out.push_back(hex[byte & 0x0f]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
HandshakeResult BLEPeerSessionManager::handleIdentityHandshake(
|
||||
const ConnectionSnapshot& connection,
|
||||
const uint8_t* data,
|
||||
size_t data_size,
|
||||
double now_seconds) {
|
||||
|
||||
HandshakeResult result;
|
||||
result.mtu = connection.negotiated_mtu.value_or(BLE_MINIMUM_MTU);
|
||||
|
||||
if (!isIdentityHandshakePayload(data, data_size)) {
|
||||
result.decision = InputDecision::PassToReassembler;
|
||||
result.consumed = false;
|
||||
result.actions.push_back(make_action(SessionActionType::PassToReassembler,
|
||||
connection.current));
|
||||
return result;
|
||||
}
|
||||
|
||||
PeerIdentity payload_identity = identityFromPayload(data, data_size);
|
||||
result.peer_identity = payload_identity;
|
||||
result.identity_key = computeIdentityKey(payload_identity);
|
||||
result.fragmenter_key = computeFragmenterKey(payload_identity);
|
||||
result.consumed = true;
|
||||
|
||||
if (connection.known_identity_for_address.has_value()) {
|
||||
if (connection.known_identity_for_address.value() == payload_identity) {
|
||||
result.decision = InputDecision::ConsumedDuplicateSameIdentity;
|
||||
result.actions.push_back(make_action(SessionActionType::ConsumeInput,
|
||||
connection.current));
|
||||
return result;
|
||||
}
|
||||
|
||||
result.decision = InputDecision::ConsumedDuplicateMismatchedIdentity;
|
||||
result.actions.push_back(make_message_action(
|
||||
SessionActionType::Warn,
|
||||
connection.current,
|
||||
"16-byte data differs from known identity; consumed as identity-like data"));
|
||||
result.actions.push_back(make_action(SessionActionType::ConsumeInput,
|
||||
connection.current));
|
||||
return result;
|
||||
}
|
||||
|
||||
const PeerSession* existing_session = findSessionByIdentityKey(result.identity_key);
|
||||
std::optional<std::string> existing_address = connection.existing_address_for_identity;
|
||||
if (!existing_address.has_value() && existing_session &&
|
||||
!existing_session->current_address.empty()) {
|
||||
existing_address = existing_session->current_address;
|
||||
}
|
||||
|
||||
const bool duplicate_elsewhere =
|
||||
existing_address.has_value() && existing_address.value() != connection.current.address;
|
||||
|
||||
if (duplicate_elsewhere) {
|
||||
const bool stale_or_pending =
|
||||
connection.identity_has_pending_detach ||
|
||||
(!connection.existing_address_connected &&
|
||||
!connection.existing_address_in_peer_table);
|
||||
|
||||
if (connection.existing_connection_is_zombie || stale_or_pending) {
|
||||
SessionAction cleanup = make_action(SessionActionType::CleanupOldAddress,
|
||||
connection.current);
|
||||
cleanup.old_address = existing_address.value();
|
||||
cleanup.new_address = connection.current.address;
|
||||
result.actions.push_back(cleanup);
|
||||
|
||||
if (connection.existing_connection_is_zombie) {
|
||||
result.should_disconnect_old = true;
|
||||
SessionAction disconnect_old =
|
||||
make_action(SessionActionType::DisconnectOldPeer, connection.current);
|
||||
disconnect_old.old_address = existing_address.value();
|
||||
result.actions.push_back(disconnect_old);
|
||||
}
|
||||
|
||||
SessionAction update = make_action(SessionActionType::UpdatePeerAddress,
|
||||
connection.current);
|
||||
update.old_address = existing_address.value();
|
||||
update.new_address = connection.current.address;
|
||||
result.actions.push_back(update);
|
||||
} else {
|
||||
result.decision = InputDecision::RejectedDuplicateIdentity;
|
||||
result.should_disconnect_current = true;
|
||||
result.actions.push_back(make_action(SessionActionType::RejectDuplicateIdentity,
|
||||
connection.current));
|
||||
result.actions.push_back(make_action(SessionActionType::DisconnectCurrentPeer,
|
||||
connection.current));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
upsertAcceptedSession(connection, payload_identity, result.mtu, now_seconds);
|
||||
removePendingIdentity(connection.current);
|
||||
|
||||
result.decision = InputDecision::AcceptedNewIdentity;
|
||||
result.accepted = true;
|
||||
result.actions.push_back(make_action(SessionActionType::AcceptNewIdentity,
|
||||
connection.current));
|
||||
result.actions.push_back(make_action(SessionActionType::CreateFragmentationState,
|
||||
connection.current));
|
||||
result.actions.push_back(make_action(SessionActionType::MarkPeerReady,
|
||||
connection.current));
|
||||
result.actions.push_back(make_action(SessionActionType::RemovePendingIdentity,
|
||||
connection.current));
|
||||
result.actions.push_back(make_action(SessionActionType::MarkRealData,
|
||||
connection.current));
|
||||
return result;
|
||||
}
|
||||
|
||||
void BLEPeerSessionManager::markConnected(const ConnectionSnapshot& connection,
|
||||
double now_seconds) {
|
||||
if (connection.known_identity_for_address.has_value()) {
|
||||
upsertAcceptedSession(connection,
|
||||
connection.known_identity_for_address.value(),
|
||||
connection.negotiated_mtu.value_or(BLE_MINIMUM_MTU),
|
||||
now_seconds);
|
||||
} else {
|
||||
markPendingIdentity(connection.current, now_seconds);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerSessionManager::markDisconnected(const ConnectionId& connection,
|
||||
double /*now_seconds*/) {
|
||||
sessions_.erase(std::remove_if(sessions_.begin(), sessions_.end(),
|
||||
[&](const PeerSession& session) {
|
||||
return session.current_address == connection.address ||
|
||||
session.current_handle == connection.handle;
|
||||
}),
|
||||
sessions_.end());
|
||||
removePendingIdentity(connection);
|
||||
}
|
||||
|
||||
void BLEPeerSessionManager::markMtu(const ConnectionId& connection, uint16_t mtu) {
|
||||
PeerSession* session = findSessionByAddress(connection.address);
|
||||
if (session) {
|
||||
session->mtu = mtu;
|
||||
}
|
||||
}
|
||||
|
||||
void BLEPeerSessionManager::markPendingIdentity(const ConnectionId& connection,
|
||||
double now_seconds) {
|
||||
for (PendingIdentity& pending : pending_identities_) {
|
||||
if (pending.connection.address == connection.address) {
|
||||
pending.connection = connection;
|
||||
pending.started_at = now_seconds;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
PendingIdentity pending;
|
||||
pending.connection = connection;
|
||||
pending.started_at = now_seconds;
|
||||
pending_identities_.push_back(pending);
|
||||
}
|
||||
|
||||
void BLEPeerSessionManager::removePendingIdentity(const ConnectionId& connection) {
|
||||
pending_identities_.erase(
|
||||
std::remove_if(pending_identities_.begin(), pending_identities_.end(),
|
||||
[&](const PendingIdentity& pending) {
|
||||
return pending.connection.address == connection.address;
|
||||
}),
|
||||
pending_identities_.end());
|
||||
}
|
||||
|
||||
std::vector<ConnectionId> BLEPeerSessionManager::expiredPendingIdentities(
|
||||
double now_seconds) const {
|
||||
std::vector<ConnectionId> expired;
|
||||
for (const PendingIdentity& pending : pending_identities_) {
|
||||
if (now_seconds - pending.started_at > pending_identity_timeout_) {
|
||||
expired.push_back(pending.connection);
|
||||
}
|
||||
}
|
||||
return expired;
|
||||
}
|
||||
|
||||
std::optional<PeerSessionView> BLEPeerSessionManager::sessionByAddress(
|
||||
const std::string& address) const {
|
||||
const PeerSession* session = findSessionByAddress(address);
|
||||
if (!session) {
|
||||
return std::nullopt;
|
||||
}
|
||||
PeerSessionView view;
|
||||
view.identity = session->identity;
|
||||
view.identity_key = session->identity_key;
|
||||
view.current_address = session->current_address;
|
||||
view.current_handle = session->current_handle;
|
||||
view.mtu = session->mtu;
|
||||
view.has_fragmentation_state = session->has_fragmentation_state;
|
||||
view.peer_ready = session->peer_ready;
|
||||
view.pending_identity_since = session->pending_identity_since;
|
||||
view.last_real_data = session->last_real_data;
|
||||
return view;
|
||||
}
|
||||
|
||||
std::optional<PeerSessionView> BLEPeerSessionManager::sessionByIdentity(
|
||||
const PeerIdentity& identity) const {
|
||||
const PeerSession* session = findSessionByIdentityKey(computeIdentityKey(identity));
|
||||
if (!session) {
|
||||
return std::nullopt;
|
||||
}
|
||||
PeerSessionView view;
|
||||
view.identity = session->identity;
|
||||
view.identity_key = session->identity_key;
|
||||
view.current_address = session->current_address;
|
||||
view.current_handle = session->current_handle;
|
||||
view.mtu = session->mtu;
|
||||
view.has_fragmentation_state = session->has_fragmentation_state;
|
||||
view.peer_ready = session->peer_ready;
|
||||
view.pending_identity_since = session->pending_identity_since;
|
||||
view.last_real_data = session->last_real_data;
|
||||
return view;
|
||||
}
|
||||
|
||||
BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByIdentityKey(
|
||||
const std::string& identity_key) {
|
||||
for (PeerSession& session : sessions_) {
|
||||
if (session.identity_key == identity_key) {
|
||||
return &session;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByIdentityKey(
|
||||
const std::string& identity_key) const {
|
||||
for (const PeerSession& session : sessions_) {
|
||||
if (session.identity_key == identity_key) {
|
||||
return &session;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByAddress(
|
||||
const std::string& address) {
|
||||
for (PeerSession& session : sessions_) {
|
||||
if (session.current_address == address) {
|
||||
return &session;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByAddress(
|
||||
const std::string& address) const {
|
||||
for (const PeerSession& session : sessions_) {
|
||||
if (session.current_address == address) {
|
||||
return &session;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void BLEPeerSessionManager::upsertAcceptedSession(
|
||||
const ConnectionSnapshot& connection,
|
||||
const PeerIdentity& identity,
|
||||
uint16_t mtu,
|
||||
double now_seconds) {
|
||||
const std::string identity_key = computeIdentityKey(identity);
|
||||
PeerSession* session = findSessionByIdentityKey(identity_key);
|
||||
if (!session) {
|
||||
PeerSession new_session;
|
||||
new_session.identity = identity;
|
||||
new_session.identity_key = identity_key;
|
||||
sessions_.push_back(new_session);
|
||||
session = &sessions_.back();
|
||||
}
|
||||
|
||||
session->identity = identity;
|
||||
session->identity_key = identity_key;
|
||||
session->current_address = connection.current.address;
|
||||
session->current_handle = connection.current.handle;
|
||||
session->mtu = mtu;
|
||||
session->has_fragmentation_state = true;
|
||||
session->peer_ready = true;
|
||||
session->pending_identity_since = 0.0;
|
||||
session->last_real_data = now_seconds;
|
||||
}
|
||||
|
||||
} // namespace ble_reticulum
|
||||
163
migration/protocol_core/BLEPeerSessionManager.h
Normal file
163
migration/protocol_core/BLEPeerSessionManager.h
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ble_reticulum {
|
||||
|
||||
using PeerIdentity = std::array<uint8_t, 16>;
|
||||
|
||||
enum class LocalRole : uint8_t {
|
||||
Unknown,
|
||||
Central,
|
||||
Peripheral,
|
||||
};
|
||||
|
||||
enum class InputDecision : uint8_t {
|
||||
PassToReassembler,
|
||||
ConsumedDuplicateSameIdentity,
|
||||
ConsumedDuplicateMismatchedIdentity,
|
||||
AcceptedNewIdentity,
|
||||
RejectedDuplicateIdentity,
|
||||
ErrorConsumed,
|
||||
};
|
||||
|
||||
enum class SessionActionType : uint8_t {
|
||||
ConsumeInput,
|
||||
PassToReassembler,
|
||||
AcceptNewIdentity,
|
||||
RejectDuplicateIdentity,
|
||||
DisconnectCurrentPeer,
|
||||
DisconnectOldPeer,
|
||||
CreateFragmentationState,
|
||||
MarkPeerReady,
|
||||
UpdatePeerAddress,
|
||||
RemovePendingIdentity,
|
||||
MarkRealData,
|
||||
CleanupOldAddress,
|
||||
Warn,
|
||||
};
|
||||
|
||||
struct ConnectionId {
|
||||
std::string address;
|
||||
uint16_t handle = 0xffff;
|
||||
|
||||
bool operator==(const ConnectionId& other) const {
|
||||
return address == other.address && handle == other.handle;
|
||||
}
|
||||
};
|
||||
|
||||
struct ConnectionSnapshot {
|
||||
ConnectionId current;
|
||||
LocalRole local_role = LocalRole::Unknown;
|
||||
std::optional<PeerIdentity> known_identity_for_address;
|
||||
std::optional<uint16_t> negotiated_mtu;
|
||||
|
||||
std::optional<std::string> existing_address_for_identity;
|
||||
bool identity_has_pending_detach = false;
|
||||
bool existing_address_connected = false;
|
||||
bool existing_address_in_peer_table = false;
|
||||
bool existing_connection_is_zombie = false;
|
||||
double existing_last_real_data = 0.0;
|
||||
};
|
||||
|
||||
struct SessionAction {
|
||||
SessionActionType type = SessionActionType::ConsumeInput;
|
||||
ConnectionId target;
|
||||
std::string old_address;
|
||||
std::string new_address;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct HandshakeResult {
|
||||
InputDecision decision = InputDecision::PassToReassembler;
|
||||
std::vector<SessionAction> actions;
|
||||
|
||||
bool consumed = false;
|
||||
bool accepted = false;
|
||||
bool should_disconnect_current = false;
|
||||
bool should_disconnect_old = false;
|
||||
|
||||
std::optional<PeerIdentity> peer_identity;
|
||||
std::string identity_key;
|
||||
std::string fragmenter_key;
|
||||
uint16_t mtu = 23;
|
||||
};
|
||||
|
||||
struct PeerSessionView {
|
||||
PeerIdentity identity{};
|
||||
std::string identity_key;
|
||||
std::string current_address;
|
||||
uint16_t current_handle = 0xffff;
|
||||
uint16_t mtu = 23;
|
||||
bool has_fragmentation_state = false;
|
||||
bool peer_ready = false;
|
||||
double pending_identity_since = 0.0;
|
||||
double last_real_data = 0.0;
|
||||
};
|
||||
|
||||
class BLEPeerSessionManager {
|
||||
public:
|
||||
explicit BLEPeerSessionManager(double pending_identity_timeout = 30.0,
|
||||
double zombie_timeout = 45.0);
|
||||
|
||||
HandshakeResult handleIdentityHandshake(const ConnectionSnapshot& connection,
|
||||
const uint8_t* data,
|
||||
size_t data_size,
|
||||
double now_seconds);
|
||||
|
||||
void markConnected(const ConnectionSnapshot& connection, double now_seconds);
|
||||
void markDisconnected(const ConnectionId& connection, double now_seconds);
|
||||
void markMtu(const ConnectionId& connection, uint16_t mtu);
|
||||
void markPendingIdentity(const ConnectionId& connection, double now_seconds);
|
||||
void removePendingIdentity(const ConnectionId& connection);
|
||||
std::vector<ConnectionId> expiredPendingIdentities(double now_seconds) const;
|
||||
|
||||
std::optional<PeerSessionView> sessionByAddress(const std::string& address) const;
|
||||
std::optional<PeerSessionView> sessionByIdentity(const PeerIdentity& identity) const;
|
||||
|
||||
static bool isIdentityHandshakePayload(const uint8_t* data, size_t data_size);
|
||||
static PeerIdentity identityFromPayload(const uint8_t* data, size_t data_size);
|
||||
static std::string computeIdentityKey(const PeerIdentity& identity);
|
||||
static std::string computeFragmenterKey(const PeerIdentity& identity);
|
||||
|
||||
private:
|
||||
double pending_identity_timeout_;
|
||||
double zombie_timeout_;
|
||||
|
||||
struct PendingIdentity {
|
||||
ConnectionId connection;
|
||||
double started_at = 0.0;
|
||||
};
|
||||
|
||||
struct PeerSession {
|
||||
PeerIdentity identity{};
|
||||
std::string identity_key;
|
||||
std::string current_address;
|
||||
uint16_t current_handle = 0xffff;
|
||||
uint16_t mtu = 23;
|
||||
bool has_fragmentation_state = false;
|
||||
bool peer_ready = false;
|
||||
double pending_identity_since = 0.0;
|
||||
double last_real_data = 0.0;
|
||||
};
|
||||
|
||||
std::vector<PeerSession> sessions_;
|
||||
std::vector<PendingIdentity> pending_identities_;
|
||||
|
||||
PeerSession* findSessionByIdentityKey(const std::string& identity_key);
|
||||
const PeerSession* findSessionByIdentityKey(const std::string& identity_key) const;
|
||||
PeerSession* findSessionByAddress(const std::string& address);
|
||||
const PeerSession* findSessionByAddress(const std::string& address) const;
|
||||
|
||||
void upsertAcceptedSession(const ConnectionSnapshot& connection,
|
||||
const PeerIdentity& identity,
|
||||
uint16_t mtu,
|
||||
double now_seconds);
|
||||
};
|
||||
|
||||
} // namespace ble_reticulum
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
INSERT INTO symbols (
|
||||
source_file,
|
||||
symbol_name,
|
||||
symbol_type,
|
||||
class_name,
|
||||
line_number,
|
||||
tag,
|
||||
phase,
|
||||
status,
|
||||
cpp_candidate,
|
||||
confidence,
|
||||
rationale,
|
||||
callers,
|
||||
callees,
|
||||
notes,
|
||||
first_seen_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
'src/ble_reticulum/BLEInterface.py',
|
||||
'_handle_identity_handshake',
|
||||
'method',
|
||||
'BLEInterface',
|
||||
1202,
|
||||
'GLUE',
|
||||
'2_ble_protocol_session_manager',
|
||||
'NATIVE_TESTED',
|
||||
1,
|
||||
'high',
|
||||
'Gate 2B implemented a native C++ BLEPeerSessionManager skeleton and pure native tests covering current Python reference decisions. This remains reference behavior for C++ session ownership, not a literal method port.',
|
||||
'_data_received_callback',
|
||||
'_compute_identity_hash; _check_duplicate_identity; driver.disconnect; driver.get_peer_mtu; _get_fragmenter_key; BLEFragmenter; BLEReassembler; _spawn_peer_interface',
|
||||
'Gate 2B native tests passed. No pybind11 bindings, Python integration, live BLE behavior, or Phase 1 FIELD_ACCEPTED rows were changed.',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT(source_file, class_name, symbol_name, line_number) DO UPDATE SET
|
||||
tag = 'GLUE',
|
||||
phase = '2_ble_protocol_session_manager',
|
||||
status = 'NATIVE_TESTED',
|
||||
cpp_candidate = 1,
|
||||
confidence = 'high',
|
||||
rationale = excluded.rationale,
|
||||
callers = excluded.callers,
|
||||
callees = excluded.callees,
|
||||
notes = excluded.notes,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
INSERT INTO reviews (
|
||||
symbol_id,
|
||||
reviewed_at,
|
||||
reviewer,
|
||||
old_tag,
|
||||
new_tag,
|
||||
old_status,
|
||||
new_status,
|
||||
note
|
||||
)
|
||||
SELECT
|
||||
symbol_id,
|
||||
CURRENT_TIMESTAMP,
|
||||
'Codex',
|
||||
'GLUE',
|
||||
'GLUE',
|
||||
'DESIGN',
|
||||
'NATIVE_TESTED',
|
||||
'Gate 2B native C++ BLEPeerSessionManager skeleton implemented with pure native tests. Tests passed via g++ build and /tmp/test_ble_peer_session_manager. No live Python BLE behavior or Phase 1 FIELD_ACCEPTED records changed.'
|
||||
FROM symbols
|
||||
WHERE source_file = 'src/ble_reticulum/BLEInterface.py'
|
||||
AND class_name = 'BLEInterface'
|
||||
AND symbol_name = '_handle_identity_handshake'
|
||||
AND line_number = 1202;
|
||||
|
||||
SELECT
|
||||
symbol_id,
|
||||
source_file,
|
||||
class_name,
|
||||
symbol_name,
|
||||
tag,
|
||||
phase,
|
||||
status,
|
||||
cpp_candidate,
|
||||
updated_at
|
||||
FROM symbols
|
||||
WHERE source_file = 'src/ble_reticulum/BLEInterface.py'
|
||||
AND class_name = 'BLEInterface'
|
||||
AND symbol_name = '_handle_identity_handshake'
|
||||
AND line_number = 1202;
|
||||
|
||||
COMMIT;
|
||||
279
migration/tests/native/test_ble_peer_session_manager.cpp
Normal file
279
migration/tests/native/test_ble_peer_session_manager.cpp
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
#include "../../protocol_core/BLEPeerSessionManager.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace ble_reticulum;
|
||||
|
||||
namespace {
|
||||
|
||||
PeerIdentity identity_with_base(uint8_t base) {
|
||||
PeerIdentity identity{};
|
||||
for (size_t i = 0; i < identity.size(); ++i) {
|
||||
identity[i] = static_cast<uint8_t>(base + i);
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> to_payload(const PeerIdentity& identity) {
|
||||
return std::vector<uint8_t>(identity.begin(), identity.end());
|
||||
}
|
||||
|
||||
ConnectionSnapshot snapshot(const std::string& address) {
|
||||
ConnectionSnapshot snap;
|
||||
snap.current.address = address;
|
||||
snap.local_role = LocalRole::Peripheral;
|
||||
return snap;
|
||||
}
|
||||
|
||||
bool has_action(const HandshakeResult& result, SessionActionType type) {
|
||||
for (const SessionAction& action : result.actions) {
|
||||
if (action.type == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void test_non_16_byte_payload() {
|
||||
BLEPeerSessionManager manager;
|
||||
auto snap = snapshot("AA:BB:CC:00:00:01");
|
||||
std::vector<uint8_t> data{1, 2, 3};
|
||||
|
||||
HandshakeResult result =
|
||||
manager.handleIdentityHandshake(snap, data.data(), data.size(), 10.0);
|
||||
|
||||
assert(result.decision == InputDecision::PassToReassembler);
|
||||
assert(!result.consumed);
|
||||
assert(!result.accepted);
|
||||
assert(has_action(result, SessionActionType::PassToReassembler));
|
||||
}
|
||||
|
||||
void test_new_16_byte_identity() {
|
||||
BLEPeerSessionManager manager;
|
||||
PeerIdentity identity = identity_with_base(0x10);
|
||||
std::vector<uint8_t> data = to_payload(identity);
|
||||
auto snap = snapshot("AA:BB:CC:00:00:02");
|
||||
|
||||
HandshakeResult result =
|
||||
manager.handleIdentityHandshake(snap, data.data(), data.size(), 11.0);
|
||||
|
||||
assert(result.decision == InputDecision::AcceptedNewIdentity);
|
||||
assert(result.consumed);
|
||||
assert(result.accepted);
|
||||
assert(result.identity_key == "1011121314151617");
|
||||
assert(result.fragmenter_key == "101112131415161718191a1b1c1d1e1f");
|
||||
assert(has_action(result, SessionActionType::AcceptNewIdentity));
|
||||
assert(has_action(result, SessionActionType::CreateFragmentationState));
|
||||
assert(has_action(result, SessionActionType::MarkPeerReady));
|
||||
}
|
||||
|
||||
void test_known_identity_duplicate_same() {
|
||||
BLEPeerSessionManager manager;
|
||||
PeerIdentity identity = identity_with_base(0x20);
|
||||
std::vector<uint8_t> data = to_payload(identity);
|
||||
auto snap = snapshot("AA:BB:CC:00:00:03");
|
||||
snap.known_identity_for_address = identity;
|
||||
|
||||
HandshakeResult result =
|
||||
manager.handleIdentityHandshake(snap, data.data(), data.size(), 12.0);
|
||||
|
||||
assert(result.decision == InputDecision::ConsumedDuplicateSameIdentity);
|
||||
assert(result.consumed);
|
||||
assert(!result.accepted);
|
||||
}
|
||||
|
||||
void test_known_identity_duplicate_mismatch() {
|
||||
BLEPeerSessionManager manager;
|
||||
PeerIdentity known = identity_with_base(0x30);
|
||||
PeerIdentity incoming = identity_with_base(0x40);
|
||||
std::vector<uint8_t> data = to_payload(incoming);
|
||||
auto snap = snapshot("AA:BB:CC:00:00:04");
|
||||
snap.known_identity_for_address = known;
|
||||
|
||||
HandshakeResult result =
|
||||
manager.handleIdentityHandshake(snap, data.data(), data.size(), 13.0);
|
||||
|
||||
assert(result.decision == InputDecision::ConsumedDuplicateMismatchedIdentity);
|
||||
assert(result.consumed);
|
||||
assert(has_action(result, SessionActionType::Warn));
|
||||
}
|
||||
|
||||
void test_duplicate_identity_active_elsewhere() {
|
||||
BLEPeerSessionManager manager;
|
||||
PeerIdentity identity = identity_with_base(0x50);
|
||||
std::vector<uint8_t> data = to_payload(identity);
|
||||
auto first = snapshot("AA:BB:CC:00:00:05");
|
||||
manager.handleIdentityHandshake(first, data.data(), data.size(), 14.0);
|
||||
|
||||
auto duplicate = snapshot("AA:BB:CC:00:00:06");
|
||||
duplicate.existing_address_for_identity = first.current.address;
|
||||
duplicate.existing_address_connected = true;
|
||||
duplicate.existing_address_in_peer_table = true;
|
||||
|
||||
HandshakeResult result =
|
||||
manager.handleIdentityHandshake(duplicate, data.data(), data.size(), 15.0);
|
||||
|
||||
assert(result.decision == InputDecision::RejectedDuplicateIdentity);
|
||||
assert(result.consumed);
|
||||
assert(!result.accepted);
|
||||
assert(result.should_disconnect_current);
|
||||
assert(has_action(result, SessionActionType::DisconnectCurrentPeer));
|
||||
}
|
||||
|
||||
void test_duplicate_identity_with_stale_pending_detach() {
|
||||
BLEPeerSessionManager manager;
|
||||
PeerIdentity identity = identity_with_base(0x60);
|
||||
std::vector<uint8_t> data = to_payload(identity);
|
||||
auto first = snapshot("AA:BB:CC:00:00:07");
|
||||
manager.handleIdentityHandshake(first, data.data(), data.size(), 16.0);
|
||||
|
||||
auto rotated = snapshot("AA:BB:CC:00:00:08");
|
||||
rotated.existing_address_for_identity = first.current.address;
|
||||
rotated.identity_has_pending_detach = true;
|
||||
|
||||
HandshakeResult result =
|
||||
manager.handleIdentityHandshake(rotated, data.data(), data.size(), 17.0);
|
||||
|
||||
assert(result.decision == InputDecision::AcceptedNewIdentity);
|
||||
assert(result.accepted);
|
||||
assert(has_action(result, SessionActionType::CleanupOldAddress));
|
||||
assert(has_action(result, SessionActionType::UpdatePeerAddress));
|
||||
|
||||
auto view = manager.sessionByIdentity(identity);
|
||||
assert(view.has_value());
|
||||
assert(view->current_address == rotated.current.address);
|
||||
}
|
||||
|
||||
void test_duplicate_identity_with_zombie_old_connection() {
|
||||
BLEPeerSessionManager manager;
|
||||
PeerIdentity identity = identity_with_base(0x70);
|
||||
std::vector<uint8_t> data = to_payload(identity);
|
||||
auto first = snapshot("AA:BB:CC:00:00:09");
|
||||
manager.handleIdentityHandshake(first, data.data(), data.size(), 18.0);
|
||||
|
||||
auto replacement = snapshot("AA:BB:CC:00:00:0A");
|
||||
replacement.existing_address_for_identity = first.current.address;
|
||||
replacement.existing_address_connected = true;
|
||||
replacement.existing_address_in_peer_table = true;
|
||||
replacement.existing_connection_is_zombie = true;
|
||||
|
||||
HandshakeResult result =
|
||||
manager.handleIdentityHandshake(replacement, data.data(), data.size(), 19.0);
|
||||
|
||||
assert(result.decision == InputDecision::AcceptedNewIdentity);
|
||||
assert(result.accepted);
|
||||
assert(result.should_disconnect_old);
|
||||
assert(has_action(result, SessionActionType::DisconnectOldPeer));
|
||||
assert(has_action(result, SessionActionType::CleanupOldAddress));
|
||||
}
|
||||
|
||||
void test_mtu_provided() {
|
||||
BLEPeerSessionManager manager;
|
||||
PeerIdentity identity = identity_with_base(0x80);
|
||||
std::vector<uint8_t> data = to_payload(identity);
|
||||
auto snap = snapshot("AA:BB:CC:00:00:0B");
|
||||
snap.negotiated_mtu = 185;
|
||||
|
||||
HandshakeResult result =
|
||||
manager.handleIdentityHandshake(snap, data.data(), data.size(), 20.0);
|
||||
|
||||
assert(result.mtu == 185);
|
||||
auto view = manager.sessionByIdentity(identity);
|
||||
assert(view.has_value());
|
||||
assert(view->mtu == 185);
|
||||
}
|
||||
|
||||
void test_mtu_missing_fallback() {
|
||||
BLEPeerSessionManager manager;
|
||||
PeerIdentity identity = identity_with_base(0x90);
|
||||
std::vector<uint8_t> data = to_payload(identity);
|
||||
auto snap = snapshot("AA:BB:CC:00:00:0C");
|
||||
|
||||
HandshakeResult result =
|
||||
manager.handleIdentityHandshake(snap, data.data(), data.size(), 21.0);
|
||||
|
||||
assert(result.mtu == 23);
|
||||
}
|
||||
|
||||
void test_pending_identity_timeout() {
|
||||
BLEPeerSessionManager manager(30.0, 45.0);
|
||||
ConnectionId one{"AA:BB:CC:00:00:0D", 1};
|
||||
ConnectionId two{"AA:BB:CC:00:00:0E", 2};
|
||||
manager.markPendingIdentity(one, 100.0);
|
||||
manager.markPendingIdentity(two, 120.0);
|
||||
|
||||
std::vector<ConnectionId> expired = manager.expiredPendingIdentities(131.0);
|
||||
assert(expired.size() == 1);
|
||||
assert(expired[0] == one);
|
||||
|
||||
manager.removePendingIdentity(one);
|
||||
expired = manager.expiredPendingIdentities(200.0);
|
||||
assert(expired.size() == 1);
|
||||
assert(expired[0] == two);
|
||||
}
|
||||
|
||||
void test_peer_address_update_fragmenter_key_unchanged() {
|
||||
BLEPeerSessionManager manager;
|
||||
PeerIdentity identity = identity_with_base(0xa0);
|
||||
std::vector<uint8_t> data = to_payload(identity);
|
||||
auto first = snapshot("AA:BB:CC:00:00:0F");
|
||||
HandshakeResult first_result =
|
||||
manager.handleIdentityHandshake(first, data.data(), data.size(), 22.0);
|
||||
|
||||
auto second = snapshot("AA:BB:CC:00:00:10");
|
||||
second.existing_address_for_identity = first.current.address;
|
||||
second.identity_has_pending_detach = true;
|
||||
HandshakeResult second_result =
|
||||
manager.handleIdentityHandshake(second, data.data(), data.size(), 23.0);
|
||||
|
||||
assert(first_result.fragmenter_key == second_result.fragmenter_key);
|
||||
auto old_view = manager.sessionByAddress(first.current.address);
|
||||
auto new_view = manager.sessionByAddress(second.current.address);
|
||||
assert(!old_view.has_value());
|
||||
assert(new_view.has_value());
|
||||
assert(new_view->current_address == second.current.address);
|
||||
}
|
||||
|
||||
void test_key_helpers() {
|
||||
PeerIdentity identity = identity_with_base(0x01);
|
||||
assert(BLEPeerSessionManager::computeIdentityKey(identity) == "0102030405060708");
|
||||
assert(BLEPeerSessionManager::computeFragmenterKey(identity) ==
|
||||
"0102030405060708090a0b0c0d0e0f10");
|
||||
}
|
||||
|
||||
void test_identity_from_payload_rejects_non_16() {
|
||||
std::vector<uint8_t> data{1, 2, 3};
|
||||
bool threw = false;
|
||||
try {
|
||||
(void)BLEPeerSessionManager::identityFromPayload(data.data(), data.size());
|
||||
} catch (const std::invalid_argument&) {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_non_16_byte_payload();
|
||||
test_new_16_byte_identity();
|
||||
test_known_identity_duplicate_same();
|
||||
test_known_identity_duplicate_mismatch();
|
||||
test_duplicate_identity_active_elsewhere();
|
||||
test_duplicate_identity_with_stale_pending_detach();
|
||||
test_duplicate_identity_with_zombie_old_connection();
|
||||
test_mtu_provided();
|
||||
test_mtu_missing_fallback();
|
||||
test_pending_identity_timeout();
|
||||
test_peer_address_update_fragmenter_key_unchanged();
|
||||
test_key_helpers();
|
||||
test_identity_from_payload_rejects_non_16();
|
||||
|
||||
std::cout << "BLEPeerSessionManager native tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue