per Codex:jp_native works: jp central -> T-Beam peripheral

jp_native_peripheral   works: T-Beam central -> jp peripheral
jp_native_dual         builds, but needs role arbitration to avoid central/peripheral race
This commit is contained in:
John Poole 2026-05-21 15:53:31 -07:00
commit 0c15cf7219
3 changed files with 692 additions and 11 deletions

View file

@ -88,9 +88,9 @@ jp_native_dual
`jp_native` builds a Linux console program instead of ESP32 firmware. It uses the host Bluetooth adapter through BlueZ D-Bus, skips the OLED path, and prints received text to stdout. The current jp payload is `texts/little_boy_blue.txt`. `jp_native` builds a Linux console program instead of ESP32 firmware. It uses the host Bluetooth adapter through BlueZ D-Bus, skips the OLED path, and prints received text to stdout. The current jp payload is `texts/little_boy_blue.txt`.
`jp_native_peripheral` is the first Linux peripheral/server scaffold. It builds a separate binary that checks for BlueZ `GattManager1` and `LEAdvertisingManager1` support on the host adapter. It does not yet register the full Exercise 306 GATT service or accept a T-Beam connection. `jp_native_peripheral` builds a Linux BLE peripheral/server. It registers the Exercise 306 GATT service through BlueZ, advertises the Reticulum service UUID, accepts T-Beam central connections, receives writes on RX, and notifies outgoing fragments on TX.
`jp_native_dual` registers both host interfaces in one process: the proven Linux central/client path and the current Linux peripheral/server scaffold. Today it can still use the central path to pair with a T-Beam, while also reporting whether BlueZ exposes the services needed for future host-native peripheral work. `jp_native_dual` registers both host interfaces in one process: the Linux central/client path and the Linux peripheral/server path.
## Building ## Building
@ -193,7 +193,7 @@ Run it from the repository root:
exercises/306_microReticulum_ble_file_transfer_oled/.pio/build/jp_native_peripheral/program exercises/306_microReticulum_ble_file_transfer_oled/.pio/build/jp_native_peripheral/program
``` ```
This build is expected to report whether the current BlueZ adapter exposes the GATT server and LE advertising managers needed for true Linux peripheral mode. This build is expected to register the GATT server, advertise the Exercise 306 service, and exchange Reticulum file-transfer traffic when a T-Beam connects as the BLE central.
Build the jp dual-role test binary with: Build the jp dual-role test binary with:
@ -215,7 +215,9 @@ Run it from the repository root:
exercises/306_microReticulum_ble_file_transfer_oled/.pio/build/jp_native_dual/program exercises/306_microReticulum_ble_file_transfer_oled/.pio/build/jp_native_dual/program
``` ```
For the current T-Beam test, start `jp_native` or `jp_native_dual` first and wait for `BLE linux-central: scanning for Reticulum service`, then RESET the T-Beam. The dual binary is not fully ambidextrous until the Linux peripheral/server scaffold registers the real GATT service and advertisement. For a central-mode host test, start `jp_native` or `jp_native_dual` first and wait for `BLE linux-central: scanning for Reticulum service`, then RESET the T-Beam.
For a peripheral-mode host test, start `jp_native_peripheral` first and wait for `BLE linux-peripheral: advertising Reticulum service; waiting for central`, then RESET the T-Beam. In this order, the T-Beam can connect as the BLE central and jp receives an inbound Reticulum link.
### AMD64 ### AMD64

View file

@ -2,10 +2,39 @@
#include <Utilities/OS.h> #include <Utilities/OS.h>
#include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstring>
using namespace RNS; using namespace RNS;
static uint64_t now_ms() {
return RNS::Utilities::OS::ltime();
}
static void put_u16_be(uint8_t* data, uint16_t value) {
data[0] = (uint8_t)((value >> 8) & 0xFF);
data[1] = (uint8_t)(value & 0xFF);
}
static void put_u32_be(uint8_t* data, uint32_t value) {
data[0] = (uint8_t)((value >> 24) & 0xFF);
data[1] = (uint8_t)((value >> 16) & 0xFF);
data[2] = (uint8_t)((value >> 8) & 0xFF);
data[3] = (uint8_t)(value & 0xFF);
}
static uint16_t get_u16_be(const uint8_t* data) {
return ((uint16_t)data[0] << 8) | data[1];
}
static uint32_t get_u32_be(const uint8_t* data) {
return ((uint32_t)data[0] << 24) |
((uint32_t)data[1] << 16) |
((uint32_t)data[2] << 8) |
(uint32_t)data[3];
}
static void log_error(const char* what, GError* error) { static void log_error(const char* what, GError* error) {
if (error) { if (error) {
std::fprintf(stderr, "%s: %s\n", what, error->message); std::fprintf(stderr, "%s: %s\n", what, error->message);
@ -15,17 +44,96 @@ static void log_error(const char* what, GError* error) {
} }
} }
static GVariant* bytes_variant(const RNS::Bytes& bytes) {
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE("ay"));
for (size_t i = 0; i < bytes.size(); ++i) {
g_variant_builder_add(&builder, "y", bytes[i]);
}
return g_variant_builder_end(&builder);
}
static GVariant* flags_variant(std::initializer_list<const char*> flags) {
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
for (const char* flag : flags) {
g_variant_builder_add(&builder, "s", flag);
}
return g_variant_builder_end(&builder);
}
static const char* OBJECT_MANAGER_XML =
"<node>"
" <interface name='org.freedesktop.DBus.ObjectManager'>"
" <method name='GetManagedObjects'>"
" <arg name='objects' type='a{oa{sa{sv}}}' direction='out'/>"
" </method>"
" </interface>"
"</node>";
static const char* GATT_SERVICE_XML =
"<node>"
" <interface name='org.bluez.GattService1'>"
" <property name='UUID' type='s' access='read'/>"
" <property name='Primary' type='b' access='read'/>"
" </interface>"
"</node>";
static const char* GATT_CHARACTERISTIC_XML =
"<node>"
" <interface name='org.bluez.GattCharacteristic1'>"
" <method name='ReadValue'>"
" <arg name='options' type='a{sv}' direction='in'/>"
" <arg name='value' type='ay' direction='out'/>"
" </method>"
" <method name='WriteValue'>"
" <arg name='value' type='ay' direction='in'/>"
" <arg name='options' type='a{sv}' direction='in'/>"
" </method>"
" <method name='StartNotify'/>"
" <method name='StopNotify'/>"
" <property name='UUID' type='s' access='read'/>"
" <property name='Service' type='o' access='read'/>"
" <property name='Value' type='ay' access='read'/>"
" <property name='Notifying' type='b' access='read'/>"
" <property name='Flags' type='as' access='read'/>"
" </interface>"
"</node>";
static const char* ADVERTISEMENT_XML =
"<node>"
" <interface name='org.bluez.LEAdvertisement1'>"
" <method name='Release'/>"
" <property name='Type' type='s' access='read'/>"
" <property name='ServiceUUIDs' type='as' access='read'/>"
" <property name='LocalName' type='s' access='read'/>"
" </interface>"
"</node>";
HostBluezPeripheralInterface::HostBluezPeripheralInterface(const std::string& node_label, HostBluezPeripheralInterface::HostBluezPeripheralInterface(const std::string& node_label,
const char* name) const char* name)
: InterfaceImpl(name), node_label_(node_label) { : InterfaceImpl(name), node_label_(node_label) {
_IN = true; _IN = true;
_OUT = true; _OUT = true;
_bitrate = 1000000; _bitrate = 1000000;
_HW_MTU = 168; _HW_MTU = BLE_PAYLOAD_SIZE;
identity_value_ = local_identity_hash();
} }
HostBluezPeripheralInterface::~HostBluezPeripheralInterface() { HostBluezPeripheralInterface::~HostBluezPeripheralInterface() {
stop(); stop();
if (object_manager_node_) {
g_dbus_node_info_unref(object_manager_node_);
}
if (gatt_service_node_) {
g_dbus_node_info_unref(gatt_service_node_);
}
if (gatt_characteristic_node_) {
g_dbus_node_info_unref(gatt_characteristic_node_);
}
if (advertisement_node_) {
g_dbus_node_info_unref(advertisement_node_);
}
if (bus_) { if (bus_) {
g_object_unref(bus_); g_object_unref(bus_);
} }
@ -50,14 +158,15 @@ bool HostBluezPeripheralInterface::start() {
return true; return true;
} }
std::printf("BLE linux-peripheral: BlueZ reports GATT server and LE advertising managers are present\n"); register_gatt_application();
std::printf("BLE linux-peripheral: GATT object registration scaffold builds; runtime service registration is the next step\n");
return true; return true;
} }
void HostBluezPeripheralInterface::stop() { void HostBluezPeripheralInterface::stop() {
unregister_bluez_objects();
online_ = false; online_ = false;
connected_ = false; connected_ = false;
notifying_ = false;
_online = false; _online = false;
} }
@ -132,6 +241,123 @@ bool HostBluezPeripheralInterface::adapter_supports_peripheral() const {
return has_gatt_manager_ && has_advertising_manager_; return has_gatt_manager_ && has_advertising_manager_;
} }
bool HostBluezPeripheralInterface::register_gatt_application() {
GError* error = nullptr;
object_manager_node_ = g_dbus_node_info_new_for_xml(OBJECT_MANAGER_XML, &error);
if (!object_manager_node_) {
log_error("BLE linux-peripheral: ObjectManager XML parse failed", error);
return false;
}
gatt_service_node_ = g_dbus_node_info_new_for_xml(GATT_SERVICE_XML, &error);
if (!gatt_service_node_) {
log_error("BLE linux-peripheral: GattService XML parse failed", error);
return false;
}
gatt_characteristic_node_ = g_dbus_node_info_new_for_xml(GATT_CHARACTERISTIC_XML, &error);
if (!gatt_characteristic_node_) {
log_error("BLE linux-peripheral: GattCharacteristic XML parse failed", error);
return false;
}
static const GDBusInterfaceVTable vtable = {
HostBluezPeripheralInterface::method_call,
HostBluezPeripheralInterface::property_get,
nullptr,
{nullptr}};
auto register_one = [&](const char* path, GDBusNodeInfo* node) -> bool {
guint id = g_dbus_connection_register_object(bus_, path, node->interfaces[0], &vtable, this, nullptr, &error);
if (id == 0) {
log_error("BLE linux-peripheral: D-Bus object registration failed", error);
return false;
}
object_registration_ids_.push_back(id);
return true;
};
if (!register_one(APP_PATH, object_manager_node_) ||
!register_one(SERVICE_PATH, gatt_service_node_) ||
!register_one(TX_PATH, gatt_characteristic_node_) ||
!register_one(RX_PATH, gatt_characteristic_node_) ||
!register_one(IDENTITY_PATH, gatt_characteristic_node_)) {
return false;
}
g_dbus_connection_call(bus_, BLUEZ_BUS, adapter_path_.c_str(),
"org.bluez.GattManager1", "RegisterApplication",
g_variant_new("(oa{sv})", APP_PATH, nullptr),
nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
register_application_done, this);
std::printf("BLE linux-peripheral: GATT application registration requested\n");
return true;
}
bool HostBluezPeripheralInterface::register_advertisement() {
GError* error = nullptr;
advertisement_node_ = g_dbus_node_info_new_for_xml(ADVERTISEMENT_XML, &error);
if (!advertisement_node_) {
log_error("BLE linux-peripheral: Advertisement XML parse failed", error);
return false;
}
static const GDBusInterfaceVTable vtable = {
HostBluezPeripheralInterface::method_call,
HostBluezPeripheralInterface::property_get,
nullptr,
{nullptr}};
guint id = g_dbus_connection_register_object(bus_, ADV_PATH, advertisement_node_->interfaces[0],
&vtable, this, nullptr, &error);
if (id == 0) {
log_error("BLE linux-peripheral: advertisement object registration failed", error);
return false;
}
object_registration_ids_.push_back(id);
g_dbus_connection_call(bus_, BLUEZ_BUS, adapter_path_.c_str(),
"org.bluez.LEAdvertisingManager1", "RegisterAdvertisement",
g_variant_new("(oa{sv})", ADV_PATH, nullptr),
nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
register_advertisement_done, this);
std::printf("BLE linux-peripheral: advertisement registration requested name=RNS-%s\n", node_label_.c_str());
return true;
}
void HostBluezPeripheralInterface::unregister_bluez_objects() {
if (!bus_) {
return;
}
if (advertisement_registered_) {
GError* error = nullptr;
GVariant* result = g_dbus_connection_call_sync(
bus_, BLUEZ_BUS, adapter_path_.c_str(), "org.bluez.LEAdvertisingManager1", "UnregisterAdvertisement",
g_variant_new("(o)", ADV_PATH), nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
if (!result) {
log_error("BLE linux-peripheral: UnregisterAdvertisement failed", error);
} else {
g_variant_unref(result);
}
advertisement_registered_ = false;
}
if (gatt_registered_) {
GError* error = nullptr;
GVariant* result = g_dbus_connection_call_sync(
bus_, BLUEZ_BUS, adapter_path_.c_str(), "org.bluez.GattManager1", "UnregisterApplication",
g_variant_new("(o)", APP_PATH), nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
if (!result) {
log_error("BLE linux-peripheral: UnregisterApplication failed", error);
} else {
g_variant_unref(result);
}
gatt_registered_ = false;
}
for (guint id : object_registration_ids_) {
g_dbus_connection_unregister_object(bus_, id);
}
object_registration_ids_.clear();
}
void HostBluezPeripheralInterface::loop() { void HostBluezPeripheralInterface::loop() {
if (!online_) { if (!online_) {
return; return;
@ -139,6 +365,11 @@ void HostBluezPeripheralInterface::loop() {
while (g_main_context_iteration(nullptr, FALSE)) { while (g_main_context_iteration(nullptr, FALSE)) {
} }
if (reassembly_started_ms_ != 0 && now_ms() - reassembly_started_ms_ > REASSEMBLY_TIMEOUT_MS) {
std::fprintf(stderr, "BLE linux-peripheral: reassembly timeout; dropping partial packet\n");
reset_reassembly();
}
RNS::Bytes packet({RNS::Type::NONE}); RNS::Bytes packet({RNS::Type::NONE});
while (dequeue_packet(packet)) { while (dequeue_packet(packet)) {
InterfaceImpl::handle_incoming(packet); InterfaceImpl::handle_incoming(packet);
@ -146,16 +377,124 @@ void HostBluezPeripheralInterface::loop() {
} }
void HostBluezPeripheralInterface::send_outgoing(const RNS::Bytes& data) { void HostBluezPeripheralInterface::send_outgoing(const RNS::Bytes& data) {
if (!online_ || !connected_) { if (!online_ || !connected_ || !notifying_) {
return; return;
} }
// Full TX notify support depends on BlueZ GATT characteristic object size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
// registration. This scaffold intentionally compiles separately from the if (total == 0 || total > 65535) {
// proven central path so adapter capability can be tested first. std::fprintf(stderr, "BLE linux-peripheral: cannot fragment packet len=%zu fragments=%zu\n", data.size(), total);
return;
}
uint32_t msg_id = ++tx_message_id_;
for (size_t i = 0; i < total; ++i) {
uint8_t fragment[BLE_VALUE_SIZE];
uint8_t fragment_type = FRAG_CONTINUE;
if (i == 0) {
fragment_type = FRAG_START;
} else if (i == total - 1) {
fragment_type = FRAG_END;
}
size_t offset = i * BLE_PAYLOAD_SIZE;
size_t chunk = std::min(BLE_PAYLOAD_SIZE, data.size() - offset);
fragment[0] = fragment_type;
fragment[1] = FRAG_HEADER_VERSION;
put_u16_be(fragment + 2, (uint16_t)i);
put_u16_be(fragment + 4, (uint16_t)total);
put_u32_be(fragment + 6, msg_id);
put_u32_be(fragment + 10, (uint32_t)data.size());
std::memcpy(fragment + FRAG_HEADER_SIZE, data.data() + offset, chunk);
send_fragment(fragment, FRAG_HEADER_SIZE + chunk);
RNS::Utilities::OS::sleep(0.020);
}
InterfaceImpl::handle_outgoing(data); InterfaceImpl::handle_outgoing(data);
} }
void HostBluezPeripheralInterface::send_fragment(const uint8_t* data, size_t len) {
tx_value_.assign(data, len);
emit_tx_value_changed();
}
void HostBluezPeripheralInterface::emit_tx_value_changed() {
if (!bus_ || !notifying_) {
return;
}
GVariantBuilder changed;
g_variant_builder_init(&changed, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&changed, "{sv}", "Value", bytes_variant(tx_value_));
GVariantBuilder invalidated;
g_variant_builder_init(&invalidated, G_VARIANT_TYPE("as"));
g_dbus_connection_emit_signal(bus_, nullptr, TX_PATH,
"org.freedesktop.DBus.Properties", "PropertiesChanged",
g_variant_new("(sa{sv}as)", "org.bluez.GattCharacteristic1", &changed, &invalidated),
nullptr);
}
void HostBluezPeripheralInterface::handle_fragment(const uint8_t* data, size_t len) {
if (len == 16) {
connected_ = true;
std::printf("BLE linux-peripheral: identity handshake received\n");
return;
}
if (len < FRAG_HEADER_SIZE) {
std::fprintf(stderr, "BLE linux-peripheral: fragment too short len=%zu\n", len);
return;
}
uint8_t fragment_type = data[0];
uint8_t version = data[1];
uint16_t sequence = get_u16_be(data + 2);
uint16_t total = get_u16_be(data + 4);
uint32_t msg_id = get_u32_be(data + 6);
uint32_t msg_len = get_u32_be(data + 10);
const uint8_t* payload = data + FRAG_HEADER_SIZE;
size_t payload_len = len - FRAG_HEADER_SIZE;
uint32_t expected_fragments = (msg_len + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
size_t expected_offset = (size_t)sequence * BLE_PAYLOAD_SIZE;
if ((fragment_type != FRAG_START && fragment_type != FRAG_CONTINUE && fragment_type != FRAG_END) ||
version != FRAG_HEADER_VERSION ||
total == 0 || sequence >= total ||
msg_id == 0 || msg_len == 0 ||
expected_fragments == 0 || expected_fragments != total ||
expected_offset >= msg_len || payload_len > (msg_len - expected_offset)) {
std::fprintf(stderr, "BLE linux-peripheral: invalid fragment header\n");
reset_reassembly();
return;
}
if (sequence == 0) {
reset_reassembly();
expected_total_ = total;
received_fragments_ = 0;
expected_message_len_ = msg_len;
reassembly_started_ms_ = now_ms();
current_rx_message_id_ = msg_id;
} else if (reassembly_started_ms_ == 0 || msg_id != current_rx_message_id_) {
return;
}
if (expected_total_ != total || expected_message_len_ != msg_len || sequence != received_fragments_) {
std::fprintf(stderr, "BLE linux-peripheral: out-of-order fragment; dropping partial packet\n");
reset_reassembly();
return;
}
reassembly_buffer_.append(payload, payload_len);
received_fragments_++;
if (received_fragments_ == expected_total_) {
if (reassembly_buffer_.size() == expected_message_len_) {
enqueue_packet(reassembly_buffer_);
}
reset_reassembly();
}
}
void HostBluezPeripheralInterface::enqueue_packet(const RNS::Bytes& packet) { void HostBluezPeripheralInterface::enqueue_packet(const RNS::Bytes& packet) {
incoming_packets_.push_back(packet); incoming_packets_.push_back(packet);
} }
@ -168,3 +507,271 @@ bool HostBluezPeripheralInterface::dequeue_packet(RNS::Bytes& packet) {
incoming_packets_.pop_front(); incoming_packets_.pop_front();
return true; return true;
} }
void HostBluezPeripheralInterface::reset_reassembly() {
reassembly_buffer_.clear();
expected_total_ = 0;
received_fragments_ = 0;
expected_message_len_ = 0;
reassembly_started_ms_ = 0;
current_rx_message_id_ = 0;
}
RNS::Bytes HostBluezPeripheralInterface::local_identity_hash() const {
std::string material = std::string("microReticulum BLE ") + node_label_;
return RNS::Identity::full_hash(RNS::bytesFromString(material.c_str())).left(16);
}
GVariant* HostBluezPeripheralInterface::get_managed_objects() const {
GVariantBuilder objects;
g_variant_builder_init(&objects, G_VARIANT_TYPE("a{oa{sa{sv}}}"));
GVariantBuilder service_ifaces;
g_variant_builder_init(&service_ifaces, G_VARIANT_TYPE("a{sa{sv}}"));
GVariantBuilder service_props;
g_variant_builder_init(&service_props, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&service_props, "{sv}", "UUID", g_variant_new_string(SERVICE_UUID));
g_variant_builder_add(&service_props, "{sv}", "Primary", g_variant_new_boolean(TRUE));
g_variant_builder_add(&service_ifaces, "{sa{sv}}", "org.bluez.GattService1", &service_props);
g_variant_builder_add(&objects, "{oa{sa{sv}}}", SERVICE_PATH, &service_ifaces);
auto add_characteristic = [&](const char* path, const char* uuid, GVariant* flags, const RNS::Bytes& value, bool notifying) {
GVariantBuilder ifaces;
g_variant_builder_init(&ifaces, G_VARIANT_TYPE("a{sa{sv}}"));
GVariantBuilder props;
g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&props, "{sv}", "UUID", g_variant_new_string(uuid));
g_variant_builder_add(&props, "{sv}", "Service", g_variant_new_object_path(SERVICE_PATH));
g_variant_builder_add(&props, "{sv}", "Flags", flags);
g_variant_builder_add(&props, "{sv}", "Value", bytes_variant(value));
g_variant_builder_add(&props, "{sv}", "Notifying", g_variant_new_boolean(notifying));
g_variant_builder_add(&ifaces, "{sa{sv}}", "org.bluez.GattCharacteristic1", &props);
g_variant_builder_add(&objects, "{oa{sa{sv}}}", path, &ifaces);
};
add_characteristic(TX_PATH, TX_UUID, flags_variant({"read", "notify"}), tx_value_, notifying_);
add_characteristic(RX_PATH, RX_UUID, flags_variant({"write", "write-without-response"}), RNS::Bytes(), false);
add_characteristic(IDENTITY_PATH, IDENTITY_UUID, flags_variant({"read"}), identity_value_, false);
return g_variant_new("(a{oa{sa{sv}}})", &objects);
}
GVariant* HostBluezPeripheralInterface::get_property(const char* object_path,
const char* interface_name,
const char* property_name) const {
if (std::strcmp(interface_name, "org.bluez.GattService1") == 0) {
if (std::strcmp(property_name, "UUID") == 0) {
return g_variant_new_string(SERVICE_UUID);
}
if (std::strcmp(property_name, "Primary") == 0) {
return g_variant_new_boolean(TRUE);
}
}
if (std::strcmp(interface_name, "org.bluez.GattCharacteristic1") == 0) {
bool is_tx = std::strcmp(object_path, TX_PATH) == 0;
bool is_rx = std::strcmp(object_path, RX_PATH) == 0;
bool is_identity = std::strcmp(object_path, IDENTITY_PATH) == 0;
if (std::strcmp(property_name, "UUID") == 0) {
return g_variant_new_string(is_tx ? TX_UUID : (is_rx ? RX_UUID : IDENTITY_UUID));
}
if (std::strcmp(property_name, "Service") == 0) {
return g_variant_new_object_path(SERVICE_PATH);
}
if (std::strcmp(property_name, "Flags") == 0) {
if (is_tx) {
return flags_variant({"read", "notify"});
}
if (is_rx) {
return flags_variant({"write", "write-without-response"});
}
return flags_variant({"read"});
}
if (std::strcmp(property_name, "Value") == 0) {
return bytes_variant(is_identity ? identity_value_ : tx_value_);
}
if (std::strcmp(property_name, "Notifying") == 0) {
return g_variant_new_boolean(is_tx && notifying_);
}
}
if (std::strcmp(interface_name, "org.bluez.LEAdvertisement1") == 0) {
if (std::strcmp(property_name, "Type") == 0) {
return g_variant_new_string("peripheral");
}
if (std::strcmp(property_name, "ServiceUUIDs") == 0) {
return flags_variant({SERVICE_UUID});
}
if (std::strcmp(property_name, "LocalName") == 0) {
std::string name = std::string("RNS-") + node_label_;
return g_variant_new_string(name.c_str());
}
}
return nullptr;
}
void HostBluezPeripheralInterface::handle_method_call(const char* object_path,
const char* interface_name,
const char* method_name,
GVariant* parameters,
GDBusMethodInvocation* invocation) {
if (std::strcmp(interface_name, "org.freedesktop.DBus.ObjectManager") == 0 &&
std::strcmp(method_name, "GetManagedObjects") == 0) {
g_dbus_method_invocation_return_value(invocation, get_managed_objects());
return;
}
if (std::strcmp(interface_name, "org.bluez.LEAdvertisement1") == 0 &&
std::strcmp(method_name, "Release") == 0) {
advertisement_registered_ = false;
g_dbus_method_invocation_return_value(invocation, nullptr);
return;
}
if (std::strcmp(interface_name, "org.bluez.GattCharacteristic1") != 0) {
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, "Unknown method");
return;
}
if (std::strcmp(method_name, "ReadValue") == 0) {
GVariant* options = nullptr;
g_variant_get(parameters, "(@a{sv})", &options);
if (options) {
g_variant_unref(options);
}
if (std::strcmp(object_path, IDENTITY_PATH) == 0) {
g_dbus_method_invocation_return_value(invocation, g_variant_new("(@ay)", bytes_variant(identity_value_)));
return;
}
if (std::strcmp(object_path, TX_PATH) == 0) {
g_dbus_method_invocation_return_value(invocation, g_variant_new("(@ay)", bytes_variant(tx_value_)));
return;
}
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "ReadValue not supported");
return;
}
if (std::strcmp(method_name, "WriteValue") == 0) {
if (std::strcmp(object_path, RX_PATH) != 0) {
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "WriteValue not supported");
return;
}
GVariant* value = nullptr;
GVariant* options = nullptr;
g_variant_get(parameters, "(@ay@a{sv})", &value, &options);
gsize len = 0;
const uint8_t* data = static_cast<const uint8_t*>(g_variant_get_fixed_array(value, &len, sizeof(uint8_t)));
if (data && len > 0) {
handle_fragment(data, len);
}
if (value) {
g_variant_unref(value);
}
if (options) {
g_variant_unref(options);
}
g_dbus_method_invocation_return_value(invocation, nullptr);
return;
}
if (std::strcmp(method_name, "StartNotify") == 0) {
if (std::strcmp(object_path, TX_PATH) != 0) {
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "Notify not supported");
return;
}
notifying_ = true;
connected_ = true;
std::printf("BLE linux-peripheral: central subscribed to TX notifications\n");
g_dbus_method_invocation_return_value(invocation, nullptr);
return;
}
if (std::strcmp(method_name, "StopNotify") == 0) {
notifying_ = false;
connected_ = false;
reset_reassembly();
std::printf("BLE linux-peripheral: central unsubscribed from TX notifications\n");
g_dbus_method_invocation_return_value(invocation, nullptr);
return;
}
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, "Unknown method");
}
void HostBluezPeripheralInterface::method_call(GDBusConnection* connection,
const gchar* sender,
const gchar* object_path,
const gchar* interface_name,
const gchar* method_name,
GVariant* parameters,
GDBusMethodInvocation* invocation,
gpointer user_data) {
(void)connection;
(void)sender;
auto* self = static_cast<HostBluezPeripheralInterface*>(user_data);
if (!self) {
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "No peripheral instance");
return;
}
self->handle_method_call(object_path, interface_name, method_name, parameters, invocation);
}
GVariant* HostBluezPeripheralInterface::property_get(GDBusConnection* connection,
const gchar* sender,
const gchar* object_path,
const gchar* interface_name,
const gchar* property_name,
GError** error,
gpointer user_data) {
(void)connection;
(void)sender;
auto* self = static_cast<HostBluezPeripheralInterface*>(user_data);
if (!self) {
g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "No peripheral instance");
return nullptr;
}
GVariant* value = self->get_property(object_path, interface_name, property_name);
if (!value) {
g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Unknown property");
}
return value;
}
void HostBluezPeripheralInterface::register_application_done(GObject* source_object,
GAsyncResult* result,
gpointer user_data) {
auto* self = static_cast<HostBluezPeripheralInterface*>(user_data);
if (!self) {
return;
}
GError* error = nullptr;
GVariant* reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), result, &error);
if (!reply) {
log_error("BLE linux-peripheral: RegisterApplication failed", error);
return;
}
g_variant_unref(reply);
self->gatt_registered_ = true;
std::printf("BLE linux-peripheral: GATT application registered\n");
self->register_advertisement();
}
void HostBluezPeripheralInterface::register_advertisement_done(GObject* source_object,
GAsyncResult* result,
gpointer user_data) {
auto* self = static_cast<HostBluezPeripheralInterface*>(user_data);
if (!self) {
return;
}
GError* error = nullptr;
GVariant* reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), result, &error);
if (!reply) {
log_error("BLE linux-peripheral: RegisterAdvertisement failed", error);
return;
}
g_variant_unref(reply);
self->advertisement_registered_ = true;
std::printf("BLE linux-peripheral: advertising Reticulum service; waiting for central\n");
}

View file

@ -7,6 +7,7 @@
#include <deque> #include <deque>
#include <string> #include <string>
#include <vector>
class HostBluezPeripheralInterface : public RNS::InterfaceImpl { class HostBluezPeripheralInterface : public RNS::InterfaceImpl {
public: public:
@ -27,18 +28,89 @@ private:
bool connect_bus(); bool connect_bus();
bool find_adapter(); bool find_adapter();
bool adapter_supports_peripheral() const; bool adapter_supports_peripheral() const;
bool register_gatt_application();
bool register_advertisement();
void unregister_bluez_objects();
void emit_tx_value_changed();
void send_fragment(const uint8_t* data, size_t len);
void handle_fragment(const uint8_t* data, size_t len);
void enqueue_packet(const RNS::Bytes& packet); void enqueue_packet(const RNS::Bytes& packet);
bool dequeue_packet(RNS::Bytes& packet); bool dequeue_packet(RNS::Bytes& packet);
void reset_reassembly();
RNS::Bytes local_identity_hash() const;
GVariant* get_managed_objects() const;
GVariant* get_property(const char* object_path, const char* interface_name, const char* property_name) const;
void handle_method_call(const char* object_path,
const char* interface_name,
const char* method_name,
GVariant* parameters,
GDBusMethodInvocation* invocation);
static void method_call(GDBusConnection* connection,
const gchar* sender,
const gchar* object_path,
const gchar* interface_name,
const gchar* method_name,
GVariant* parameters,
GDBusMethodInvocation* invocation,
gpointer user_data);
static GVariant* property_get(GDBusConnection* connection,
const gchar* sender,
const gchar* object_path,
const gchar* interface_name,
const gchar* property_name,
GError** error,
gpointer user_data);
static void register_application_done(GObject* source_object, GAsyncResult* result, gpointer user_data);
static void register_advertisement_done(GObject* source_object, GAsyncResult* result, gpointer user_data);
static constexpr const char* BLUEZ_BUS = "org.bluez"; static constexpr const char* BLUEZ_BUS = "org.bluez";
static constexpr const char* APP_PATH = "/com/microreticulum/ex306";
static constexpr const char* SERVICE_PATH = "/com/microreticulum/ex306/service0";
static constexpr const char* TX_PATH = "/com/microreticulum/ex306/service0/tx";
static constexpr const char* RX_PATH = "/com/microreticulum/ex306/service0/rx";
static constexpr const char* IDENTITY_PATH = "/com/microreticulum/ex306/service0/identity";
static constexpr const char* ADV_PATH = "/com/microreticulum/ex306/advertisement0";
static constexpr const char* SERVICE_UUID = "37145b00-442d-4a94-917f-8f42c5da28e3"; static constexpr const char* SERVICE_UUID = "37145b00-442d-4a94-917f-8f42c5da28e3";
static constexpr const char* TX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e4";
static constexpr const char* RX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e5";
static constexpr const char* IDENTITY_UUID = "37145b00-442d-4a94-917f-8f42c5da28e6";
static constexpr uint8_t FRAG_START = 0x01;
static constexpr uint8_t FRAG_CONTINUE = 0x02;
static constexpr uint8_t FRAG_END = 0x03;
static constexpr uint8_t FRAG_HEADER_VERSION = 0x02;
static constexpr size_t FRAG_HEADER_SIZE = 14;
static constexpr size_t BLE_ATT_MTU = 185;
static constexpr size_t BLE_VALUE_SIZE = BLE_ATT_MTU - 3;
static constexpr size_t BLE_PAYLOAD_SIZE = BLE_VALUE_SIZE - FRAG_HEADER_SIZE;
static constexpr uint64_t REASSEMBLY_TIMEOUT_MS = 30000;
std::string node_label_; std::string node_label_;
GDBusConnection* bus_ = nullptr; GDBusConnection* bus_ = nullptr;
std::string adapter_path_; std::string adapter_path_;
GDBusNodeInfo* object_manager_node_ = nullptr;
GDBusNodeInfo* gatt_service_node_ = nullptr;
GDBusNodeInfo* gatt_characteristic_node_ = nullptr;
GDBusNodeInfo* advertisement_node_ = nullptr;
std::vector<guint> object_registration_ids_;
bool online_ = false; bool online_ = false;
bool connected_ = false; bool connected_ = false;
bool notifying_ = false;
bool gatt_registered_ = false;
bool advertisement_registered_ = false;
bool has_gatt_manager_ = false; bool has_gatt_manager_ = false;
bool has_advertising_manager_ = false; bool has_advertising_manager_ = false;
uint32_t tx_message_id_ = 0;
RNS::Bytes tx_value_;
RNS::Bytes identity_value_;
RNS::Bytes reassembly_buffer_;
uint16_t expected_total_ = 0;
uint16_t received_fragments_ = 0;
uint32_t expected_message_len_ = 0;
uint64_t reassembly_started_ms_ = 0;
uint32_t current_rx_message_id_ = 0;
std::deque<RNS::Bytes> incoming_packets_; std::deque<RNS::Bytes> incoming_packets_;
}; };