diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/README.md b/exercises/306_microReticulum_ble_file_transfer_oled/README.md index 4c86452..4ba75bb 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/README.md +++ b/exercises/306_microReticulum_ble_file_transfer_oled/README.md @@ -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_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 @@ -193,7 +193,7 @@ Run it from the repository root: 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: @@ -215,7 +215,9 @@ Run it from the repository root: 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 diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.cpp b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.cpp index c307e2e..1ff7f6c 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.cpp +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.cpp @@ -2,10 +2,39 @@ #include +#include #include +#include 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) { if (error) { 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 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 = + "" + " " + " " + " " + " " + " " + ""; + +static const char* GATT_SERVICE_XML = + "" + " " + " " + " " + " " + ""; + +static const char* GATT_CHARACTERISTIC_XML = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +static const char* ADVERTISEMENT_XML = + "" + " " + " " + " " + " " + " " + " " + ""; + HostBluezPeripheralInterface::HostBluezPeripheralInterface(const std::string& node_label, const char* name) : InterfaceImpl(name), node_label_(node_label) { _IN = true; _OUT = true; _bitrate = 1000000; - _HW_MTU = 168; + _HW_MTU = BLE_PAYLOAD_SIZE; + identity_value_ = local_identity_hash(); } HostBluezPeripheralInterface::~HostBluezPeripheralInterface() { 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_) { g_object_unref(bus_); } @@ -50,14 +158,15 @@ bool HostBluezPeripheralInterface::start() { return true; } - std::printf("BLE linux-peripheral: BlueZ reports GATT server and LE advertising managers are present\n"); - std::printf("BLE linux-peripheral: GATT object registration scaffold builds; runtime service registration is the next step\n"); + register_gatt_application(); return true; } void HostBluezPeripheralInterface::stop() { + unregister_bluez_objects(); online_ = false; connected_ = false; + notifying_ = false; _online = false; } @@ -132,6 +241,123 @@ bool HostBluezPeripheralInterface::adapter_supports_peripheral() const { 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() { if (!online_) { return; @@ -139,6 +365,11 @@ void HostBluezPeripheralInterface::loop() { 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}); while (dequeue_packet(packet)) { InterfaceImpl::handle_incoming(packet); @@ -146,16 +377,124 @@ void HostBluezPeripheralInterface::loop() { } void HostBluezPeripheralInterface::send_outgoing(const RNS::Bytes& data) { - if (!online_ || !connected_) { + if (!online_ || !connected_ || !notifying_) { return; } - // Full TX notify support depends on BlueZ GATT characteristic object - // registration. This scaffold intentionally compiles separately from the - // proven central path so adapter capability can be tested first. + size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE; + if (total == 0 || total > 65535) { + 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); } +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) { incoming_packets_.push_back(packet); } @@ -168,3 +507,271 @@ bool HostBluezPeripheralInterface::dequeue_packet(RNS::Bytes& packet) { incoming_packets_.pop_front(); 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(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(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(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(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(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"); +} diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.h b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.h index 3f0787a..4935972 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.h +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.h @@ -7,6 +7,7 @@ #include #include +#include class HostBluezPeripheralInterface : public RNS::InterfaceImpl { public: @@ -27,18 +28,89 @@ private: bool connect_bus(); bool find_adapter(); 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); 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* 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* 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_; GDBusConnection* bus_ = nullptr; 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 object_registration_ids_; bool online_ = false; bool connected_ = false; + bool notifying_ = false; + bool gatt_registered_ = false; + bool advertisement_registered_ = false; bool has_gatt_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 incoming_packets_; };