diff --git a/exercises/305_microReticulum_ble_file_transfer/.gitignore b/exercises/305_microReticulum_ble_file_transfer/.gitignore new file mode 100644 index 0000000..19ec447 --- /dev/null +++ b/exercises/305_microReticulum_ble_file_transfer/.gitignore @@ -0,0 +1,2 @@ +transport_identity + diff --git a/exercises/305_microReticulum_ble_file_transfer/texts/children.txt b/exercises/305_microReticulum_ble_file_transfer/texts/children.txt new file mode 100644 index 0000000..ba70ee5 --- /dev/null +++ b/exercises/305_microReticulum_ble_file_transfer/texts/children.txt @@ -0,0 +1,49 @@ +Between the dark and the daylight, + When the night is beginning to lower, +Comes a pause in the day's occupations, + That is known as the Children's Hour. + +I hear in the chamber above me + The patter of little feet, +The sound of a door that is opened, + And voices soft and sweet. + +From my study I see in the lamplight, + Descending the broad hall stair, +Grave Alice, and laughing Allegra, + And Edith with golden hair. + +A whisper, and then a silence: + Yet I know by their merry eyes +They are plotting and planning together + To take me by surprise. + +A sudden rush from the stairway, + A sudden raid from the hall! +By three doors left unguarded + They enter my castle wall! + +They climb up into my turret + O'er the arms and back of my chair; +If I try to escape, they surround me; + They seem to be everywhere. + +They almost devour me with kisses, + Their arms about me entwine, +Till I think of the Bishop of Bingen + In his Mouse-Tower on the Rhine! + +Do you think, O blue-eyed banditti, + Because you have scaled the wall, +Such an old mustache as I am + Is not a match for you all! + +I have you fast in my fortress, + And will not let you depart, +But put you down into the dungeon + In the round-tower of my heart. + +And there will I keep you forever, + Yes, forever and a day, +Till the walls shall crumble to ruin, + And moulder in dust away! \ No newline at end of file diff --git a/exercises/305_microReticulum_ble_file_transfer/texts/little_boy_blue.txt b/exercises/305_microReticulum_ble_file_transfer/texts/little_boy_blue.txt new file mode 100644 index 0000000..741f4f8 --- /dev/null +++ b/exercises/305_microReticulum_ble_file_transfer/texts/little_boy_blue.txt @@ -0,0 +1,26 @@ +The little toy dog is covered with dust, + But sturdy and staunch he stands; + And the little toy soldier is red with rust, + And his musket molds in his hands. +Time was when the little toy dog was new, + And the soldier was passing fair; +And that was the time when our Little Boy Blue + Kissed them and put them there. + +"Now, don't you go till I come," he said, + "And don't you make any noise!" +So, toddling off to his trundle-bed, + He dreamed of the pretty toys; +And, as he was dreaming, an angel song + Awakened our Little Boy Blue +Oh! the years are many, the years are long, + But the little toy friends are true! + +Ay, faithful to Little Boy Blue they stand, + Each in the same old place +Awaiting the touch of a little hand, + The smile of a little face; +And they wonder, as waiting the long years through + In the dust of that little chair, +What has become of our Little Boy Blue, + Since he kissed them and put them there. \ No newline at end of file diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/platformio.ini b/exercises/306_microReticulum_ble_file_transfer_oled/platformio.ini index d7480ed..4aa0f0f 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/platformio.ini +++ b/exercises/306_microReticulum_ble_file_transfer_oled/platformio.ini @@ -3,7 +3,7 @@ [platformio] default_envs = tbeam_if -[env] +[tbeam_base] platform = espressif32 framework = arduino board = esp32-s3-devkitc-1 @@ -14,6 +14,10 @@ extra_scripts = pre:scripts/embed_text.py custom_text_source = texts/If.txt lib_extra_dirs = ../../lib +build_src_filter = + +<*> + - + - build_flags = -Wall @@ -40,77 +44,116 @@ lib_deps = microReticulum=symlink:///usr/local/src/microreticulum/microReticulum [env:tbeam_if] -extends = env +extends = tbeam_base custom_text_source = texts/If.txt build_flags = - ${env.build_flags} + ${tbeam_base.build_flags} -D FILE_TRANSFER_CHUNK_SIZE=32 -D FILE_TRANSFER_CHUNK_INTERVAL_MS=500 [env:tbeam_if_full] -extends = env +extends = tbeam_base custom_text_source = texts/If_full.txt build_flags = - ${env.build_flags} + ${tbeam_base.build_flags} -D FILE_TRANSFER_CHUNK_SIZE=32 -D FILE_TRANSFER_CHUNK_INTERVAL_MS=500 [env:tbeam_constitution] -extends = env +extends = tbeam_base custom_text_source = texts/US_Constitution.txt build_flags = - ${env.build_flags} + ${tbeam_base.build_flags} -D FILE_TRANSFER_CHUNK_SIZE=32 -D FILE_TRANSFER_CHUNK_INTERVAL_MS=500 [env:tbeam_if_pi_zero_profile] -extends = env +extends = tbeam_base custom_text_source = texts/If.txt build_flags = - ${env.build_flags} + ${tbeam_base.build_flags} -D FILE_TRANSFER_CHUNK_SIZE=300 -D FILE_TRANSFER_CHUNK_INTERVAL_MS=100 [env:tbeam_if_full_pi_zero_profile] -extends = env +extends = tbeam_base custom_text_source = texts/If_full.txt build_flags = - ${env.build_flags} + ${tbeam_base.build_flags} -D FILE_TRANSFER_CHUNK_SIZE=300 -D FILE_TRANSFER_CHUNK_INTERVAL_MS=100 [env:tbeam_constitution_pi_zero_profile] -extends = env +extends = tbeam_base custom_text_source = texts/US_Constitution.txt build_flags = - ${env.build_flags} + ${tbeam_base.build_flags} -D FILE_TRANSFER_CHUNK_SIZE=300 -D FILE_TRANSFER_CHUNK_INTERVAL_MS=100 [env:tbeam] -extends = env +extends = tbeam_base + +[env:jp_native] +platform = native +build_type = debug +extra_scripts = pre:scripts/embed_text.py +custom_text_source = texts/If_full.txt +build_unflags = + -std=gnu++11 +build_flags = + -std=c++17 + -g3 + -ggdb + -Wall + -Wextra + -Wno-missing-field-initializers + -Wno-format + -Wno-unused-parameter + -include stdint.h + -D HOST_NATIVE + -D NATIVE + -D RNS_USE_FS + -D RNS_PERSIST_PATHS + -D USTORE_USE_UNIVERSALFS + -D MSGPACK_USE_BOOST=OFF + -D FILE_TRANSFER_CHUNK_SIZE=32 + -D FILE_TRANSFER_CHUNK_INTERVAL_MS=500 + -D HOST_NODE_LABEL=\"Node-JP-CLIENT\" + !pkg-config --cflags gio-2.0 glib-2.0 bluez + !pkg-config --libs gio-2.0 glib-2.0 bluez +build_src_filter = + + + + +lib_deps = + ArduinoJson@^7.4.2 + MsgPack@^0.4.2 + https://github.com/attermann/Crypto.git + https://github.com/attermann/microStore.git + microReticulum=symlink:///usr/local/src/microreticulum/microReticulum +lib_compat_mode = off [env:amy] -extends = env +extends = tbeam_base upload_port = /dev/ttytAMY monitor_port = /dev/ttytAMY [env:bob] -extends = env +extends = tbeam_base upload_port = /dev/ttytBOB monitor_port = /dev/ttytBOB [env:cy] -extends = env +extends = tbeam_base upload_port = /dev/ttytCY monitor_port = /dev/ttytCY [env:dan] -extends = env +extends = tbeam_base upload_port = /dev/ttytDAN monitor_port = /dev/ttytDAN [env:ed] -extends = env +extends = tbeam_base upload_port = /dev/ttytED monitor_port = /dev/ttytED diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/scripts/embed_text.py b/exercises/306_microReticulum_ble_file_transfer_oled/scripts/embed_text.py index 36e7223..979142a 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/scripts/embed_text.py +++ b/exercises/306_microReticulum_ble_file_transfer_oled/scripts/embed_text.py @@ -20,8 +20,20 @@ symbol_name = source_path.name.replace("\\", "/") lines = [ "#pragma once", "", + "#include ", + "#include ", + "", + "#if defined(ARDUINO)", "#include ", "#include ", + "#else", + "#ifndef PROGMEM", + "#define PROGMEM", + "#endif", + "#ifndef pgm_read_byte", + "#define pgm_read_byte(addr) (*(const uint8_t*)(addr))", + "#endif", + "#endif", "", f'static constexpr const char* SELECTED_TEXT_NAME = "{symbol_name}";', f"static constexpr size_t SELECTED_TEXT_SIZE = {len(data)};", diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.cpp b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.cpp new file mode 100644 index 0000000..7d37178 --- /dev/null +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.cpp @@ -0,0 +1,624 @@ +#include "HostBluezBleInterface.h" + +#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 bool uuid_matches(const char* found, const char* expected) { + return found && g_ascii_strcasecmp(found, expected) == 0; +} + +static void log_error(const char* what, GError* error) { + if (error) { + std::fprintf(stderr, "%s: %s\n", what, error->message); + g_error_free(error); + } else { + std::fprintf(stderr, "%s\n", what); + } +} + +HostBluezBleInterface::HostBluezBleInterface(const std::string& node_label, const char* name) + : InterfaceImpl(name), node_label_(node_label) { + _IN = true; + _OUT = true; + _bitrate = 1000000; + _HW_MTU = BLE_PAYLOAD_SIZE; +} + +HostBluezBleInterface::~HostBluezBleInterface() { + stop(); + if (bus_) { + g_object_unref(bus_); + } +} + +bool HostBluezBleInterface::start() { + if (online_) { + return true; + } + if (!connect_bus() || !find_adapter()) { + return false; + } + online_ = true; + _online = true; + std::printf("BLE linux-central: adapter=%s label=%s\n", adapter_path_.c_str(), node_label_.c_str()); + start_discovery(); + return true; +} + +void HostBluezBleInterface::stop() { + online_ = false; + _online = false; + disconnect_peer(); + stop_discovery(); +} + +bool HostBluezBleInterface::connect_bus() { + GError* error = nullptr; + bus_ = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, &error); + if (!bus_) { + log_error("BLE linux-central: cannot connect to system D-Bus", error); + return false; + } + return true; +} + +bool HostBluezBleInterface::find_adapter() { + GError* error = nullptr; + GVariant* managed = g_dbus_connection_call_sync(bus_, BLUEZ_BUS, "/", + "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects", nullptr, + G_VARIANT_TYPE("(a{oa{sa{sv}}})"), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (!managed) { + log_error("BLE linux-central: GetManagedObjects failed", error); + return false; + } + + GVariantIter* objects = nullptr; + g_variant_get(managed, "(a{oa{sa{sv}}})", &objects); + const gchar* object_path = nullptr; + GVariant* ifaces = nullptr; + while (g_variant_iter_next(objects, "{&o@a{sa{sv}}}", &object_path, &ifaces)) { + if (g_variant_lookup_value(ifaces, "org.bluez.Adapter1", G_VARIANT_TYPE("a{sv}"))) { + adapter_path_ = object_path; + g_variant_unref(ifaces); + break; + } + g_variant_unref(ifaces); + } + g_variant_iter_free(objects); + g_variant_unref(managed); + + if (adapter_path_.empty()) { + std::fprintf(stderr, "BLE linux-central: no BlueZ adapter found\n"); + return false; + } + + GVariantBuilder props; + g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&props, "{sv}", "Powered", g_variant_new_boolean(TRUE)); + GVariantBuilder invalidated; + g_variant_builder_init(&invalidated, G_VARIANT_TYPE("as")); + g_dbus_connection_emit_signal(bus_, nullptr, adapter_path_.c_str(), + "org.freedesktop.DBus.Properties", "PropertiesChanged", + g_variant_new("(sa{sv}as)", "org.bluez.Adapter1", &props, &invalidated), + nullptr); + + error = nullptr; + GVariant* powered = g_dbus_connection_call_sync( + bus_, BLUEZ_BUS, adapter_path_.c_str(), "org.freedesktop.DBus.Properties", "Set", + g_variant_new("(ssv)", "org.bluez.Adapter1", "Powered", g_variant_new_boolean(TRUE)), + nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (!powered) { + log_error("BLE linux-central: could not power adapter", error); + return false; + } + g_variant_unref(powered); + return true; +} + +bool HostBluezBleInterface::start_discovery() { + if (discovering_ || adapter_path_.empty()) { + return true; + } + + GVariantBuilder uuids; + g_variant_builder_init(&uuids, G_VARIANT_TYPE("as")); + g_variant_builder_add(&uuids, "s", SERVICE_UUID); + GVariantBuilder filter; + g_variant_builder_init(&filter, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&filter, "{sv}", "UUIDs", g_variant_builder_end(&uuids)); + g_variant_builder_add(&filter, "{sv}", "Transport", g_variant_new_string("le")); + + GError* error = nullptr; + GVariant* result = g_dbus_connection_call_sync( + bus_, BLUEZ_BUS, adapter_path_.c_str(), "org.bluez.Adapter1", "SetDiscoveryFilter", + g_variant_new("(a{sv})", &filter), nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (!result) { + log_error("BLE linux-central: SetDiscoveryFilter failed", error); + } else { + g_variant_unref(result); + } + + error = nullptr; + result = g_dbus_connection_call_sync(bus_, BLUEZ_BUS, adapter_path_.c_str(), + "org.bluez.Adapter1", "StartDiscovery", + nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (!result) { + log_error("BLE linux-central: StartDiscovery failed", error); + next_scan_ms_ = now_ms() + SCAN_RETRY_MS; + return false; + } + g_variant_unref(result); + discovering_ = true; + next_scan_ms_ = now_ms() + SCAN_RETRY_MS; + std::printf("BLE linux-central: scanning for Reticulum service\n"); + return true; +} + +bool HostBluezBleInterface::stop_discovery() { + if (!discovering_ || adapter_path_.empty()) { + return true; + } + GError* error = nullptr; + GVariant* result = g_dbus_connection_call_sync(bus_, BLUEZ_BUS, adapter_path_.c_str(), + "org.bluez.Adapter1", "StopDiscovery", + nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, -1, + nullptr, &error); + if (!result) { + log_error("BLE linux-central: StopDiscovery failed", error); + return false; + } + g_variant_unref(result); + discovering_ = false; + return true; +} + +bool HostBluezBleInterface::scan_for_peer() { + GError* error = nullptr; + GVariant* managed = g_dbus_connection_call_sync(bus_, BLUEZ_BUS, "/", + "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects", nullptr, + G_VARIANT_TYPE("(a{oa{sa{sv}}})"), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (!managed) { + log_error("BLE linux-central: scan GetManagedObjects failed", error); + return false; + } + + std::string found_path; + std::string found_name; + GVariantIter* objects = nullptr; + g_variant_get(managed, "(a{oa{sa{sv}}})", &objects); + const gchar* object_path = nullptr; + GVariant* ifaces = nullptr; + while (g_variant_iter_next(objects, "{&o@a{sa{sv}}}", &object_path, &ifaces)) { + GVariant* device = g_variant_lookup_value(ifaces, "org.bluez.Device1", G_VARIANT_TYPE("a{sv}")); + if (!device) { + g_variant_unref(ifaces); + continue; + } + + const gchar* name = nullptr; + g_variant_lookup(device, "Name", "&s", &name); + bool has_rns_name = name && std::strncmp(name, "RNS-", 4) == 0; + bool has_service = false; + GVariant* uuids = g_variant_lookup_value(device, "UUIDs", G_VARIANT_TYPE("as")); + if (uuids) { + GVariantIter uuid_iter; + const gchar* uuid = nullptr; + g_variant_iter_init(&uuid_iter, uuids); + while (g_variant_iter_next(&uuid_iter, "&s", &uuid)) { + if (uuid_matches(uuid, SERVICE_UUID)) { + has_service = true; + break; + } + } + g_variant_unref(uuids); + } + + if (has_service || has_rns_name) { + found_path = object_path; + found_name = name ? name : ""; + g_variant_unref(device); + g_variant_unref(ifaces); + break; + } + + g_variant_unref(device); + g_variant_unref(ifaces); + } + g_variant_iter_free(objects); + g_variant_unref(managed); + + if (found_path.empty()) { + return false; + } + + std::printf("BLE linux-central: peer candidate path=%s name=%s\n", found_path.c_str(), found_name.c_str()); + stop_discovery(); + return connect_device(found_path); +} + +bool HostBluezBleInterface::connect_device(const std::string& device_path) { + GError* error = nullptr; + GVariant* result = g_dbus_connection_call_sync(bus_, BLUEZ_BUS, device_path.c_str(), + "org.bluez.Device1", "Connect", + nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, 30000, + nullptr, &error); + if (!result) { + log_error("BLE linux-central: Device1.Connect failed", error); + next_scan_ms_ = now_ms() + SCAN_RETRY_MS; + start_discovery(); + return false; + } + g_variant_unref(result); + device_path_ = device_path; + std::printf("BLE linux-central: connected to %s\n", device_path_.c_str()); + + uint64_t deadline = now_ms() + 10000; + while (now_ms() < deadline) { + while (g_main_context_iteration(nullptr, FALSE)) { + } + if (discover_characteristics()) { + break; + } + RNS::Utilities::OS::sleep(0.1); + } + + if (rx_char_path_.empty() || tx_char_path_.empty()) { + std::fprintf(stderr, "BLE linux-central: Reticulum characteristics not found\n"); + disconnect_peer(); + return false; + } + + if (!start_notify()) { + disconnect_peer(); + return false; + } + + uint8_t identity[16] = {}; + std::hash hasher; + size_t h1 = hasher(node_label_); + size_t h2 = hasher(node_label_ + ":exercise306"); + std::memcpy(identity, &h1, std::min(sizeof(h1), sizeof(identity))); + std::memcpy(identity + 8, &h2, std::min(sizeof(h2), sizeof(identity) - 8)); + write_characteristic(rx_char_path_, identity, sizeof(identity), true); + + connected_ = true; + std::printf("BLE linux-central: notifications active; identity handshake sent\n"); + return true; +} + +bool HostBluezBleInterface::discover_characteristics() { + GError* error = nullptr; + GVariant* managed = g_dbus_connection_call_sync(bus_, BLUEZ_BUS, "/", + "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects", nullptr, + G_VARIANT_TYPE("(a{oa{sa{sv}}})"), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (!managed) { + log_error("BLE linux-central: characteristic discovery failed", error); + return false; + } + + tx_char_path_.clear(); + rx_char_path_.clear(); + identity_char_path_.clear(); + + GVariantIter* objects = nullptr; + g_variant_get(managed, "(a{oa{sa{sv}}})", &objects); + const gchar* object_path = nullptr; + GVariant* ifaces = nullptr; + while (g_variant_iter_next(objects, "{&o@a{sa{sv}}}", &object_path, &ifaces)) { + if (device_path_.empty() || std::strncmp(object_path, device_path_.c_str(), device_path_.size()) != 0) { + g_variant_unref(ifaces); + continue; + } + GVariant* characteristic = g_variant_lookup_value(ifaces, "org.bluez.GattCharacteristic1", G_VARIANT_TYPE("a{sv}")); + if (!characteristic) { + g_variant_unref(ifaces); + continue; + } + const gchar* uuid = nullptr; + if (g_variant_lookup(characteristic, "UUID", "&s", &uuid)) { + if (uuid_matches(uuid, TX_UUID)) { + tx_char_path_ = object_path; + } else if (uuid_matches(uuid, RX_UUID)) { + rx_char_path_ = object_path; + } else if (uuid_matches(uuid, IDENTITY_UUID)) { + identity_char_path_ = object_path; + } + } + g_variant_unref(characteristic); + g_variant_unref(ifaces); + } + g_variant_iter_free(objects); + g_variant_unref(managed); + return !tx_char_path_.empty() && !rx_char_path_.empty(); +} + +bool HostBluezBleInterface::start_notify() { + notify_subscription_ = g_dbus_connection_signal_subscribe( + bus_, BLUEZ_BUS, "org.freedesktop.DBus.Properties", "PropertiesChanged", + tx_char_path_.c_str(), "org.bluez.GattCharacteristic1", + G_DBUS_SIGNAL_FLAGS_NONE, properties_changed, this, nullptr); + + GError* error = nullptr; + GVariant* result = g_dbus_connection_call_sync(bus_, BLUEZ_BUS, tx_char_path_.c_str(), + "org.bluez.GattCharacteristic1", "StartNotify", + nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, -1, + nullptr, &error); + if (!result) { + log_error("BLE linux-central: StartNotify failed", error); + return false; + } + g_variant_unref(result); + return true; +} + +bool HostBluezBleInterface::write_characteristic(const std::string& path, + const uint8_t* data, + size_t len, + bool with_response) { + GVariantBuilder bytes; + g_variant_builder_init(&bytes, G_VARIANT_TYPE("ay")); + for (size_t i = 0; i < len; ++i) { + g_variant_builder_add(&bytes, "y", data[i]); + } + GVariantBuilder options; + g_variant_builder_init(&options, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&options, "{sv}", "type", g_variant_new_string(with_response ? "request" : "command")); + + GError* error = nullptr; + GVariant* result = g_dbus_connection_call_sync( + bus_, BLUEZ_BUS, path.c_str(), "org.bluez.GattCharacteristic1", "WriteValue", + g_variant_new("(aya{sv})", &bytes, &options), nullptr, + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (!result) { + log_error("BLE linux-central: WriteValue failed", error); + return false; + } + g_variant_unref(result); + return true; +} + +void HostBluezBleInterface::loop() { + if (!online_) { + return; + } + while (g_main_context_iteration(nullptr, FALSE)) { + } + + if (!connected_) { + uint64_t now = now_ms(); + if (!discovering_ && now >= next_scan_ms_) { + start_discovery(); + } + if (discovering_) { + scan_for_peer(); + } + return; + } + + if (reassembly_started_ms_ != 0 && now_ms() - reassembly_started_ms_ > REASSEMBLY_TIMEOUT_MS) { + std::fprintf(stderr, "BLE linux-central: reassembly timeout; dropping partial packet\n"); + reset_reassembly(); + } + + RNS::Bytes packet({RNS::Type::NONE}); + while (dequeue_packet(packet)) { + InterfaceImpl::handle_incoming(packet); + } +} + +void HostBluezBleInterface::send_outgoing(const RNS::Bytes& data) { + if (!online_ || !connected_ || rx_char_path_.empty()) { + return; + } + + size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE; + if (total == 0 || total > 65535) { + std::fprintf(stderr, "BLE linux-central: 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.008); + } + + InterfaceImpl::handle_outgoing(data); +} + +void HostBluezBleInterface::send_fragment(const uint8_t* data, size_t len) { + write_characteristic(rx_char_path_, data, len, true); +} + +void HostBluezBleInterface::properties_changed(GDBusConnection* connection, + const gchar* sender_name, + const gchar* object_path, + const gchar* interface_name, + const gchar* signal_name, + GVariant* parameters, + gpointer user_data) { + (void)connection; + (void)sender_name; + (void)interface_name; + (void)signal_name; + auto* self = static_cast(user_data); + if (!self || self->tx_char_path_ != object_path) { + return; + } + + const gchar* changed_iface = nullptr; + GVariant* changed = nullptr; + GVariant* invalidated = nullptr; + g_variant_get(parameters, "(&s@a{sv}@as)", &changed_iface, &changed, &invalidated); + if (std::strcmp(changed_iface, "org.bluez.GattCharacteristic1") == 0) { + GVariant* value = g_variant_lookup_value(changed, "Value", G_VARIANT_TYPE("ay")); + if (value) { + gsize len = 0; + const uint8_t* data = static_cast(g_variant_get_fixed_array(value, &len, sizeof(uint8_t))); + if (data && len > 0) { + self->handle_fragment(data, len); + } + g_variant_unref(value); + } + } + g_variant_unref(changed); + g_variant_unref(invalidated); +} + +void HostBluezBleInterface::handle_fragment(const uint8_t* data, size_t len) { + if (len < FRAG_HEADER_SIZE) { + std::fprintf(stderr, "BLE linux-central: 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-central: 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-central: 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 HostBluezBleInterface::enqueue_packet(const RNS::Bytes& packet) { + incoming_packets_.push_back(packet); +} + +bool HostBluezBleInterface::dequeue_packet(RNS::Bytes& packet) { + if (incoming_packets_.empty()) { + return false; + } + packet = incoming_packets_.front(); + incoming_packets_.pop_front(); + return true; +} + +void HostBluezBleInterface::reset_reassembly() { + reassembly_buffer_.clear(); + expected_total_ = 0; + received_fragments_ = 0; + expected_message_len_ = 0; + reassembly_started_ms_ = 0; + current_rx_message_id_ = 0; +} + +void HostBluezBleInterface::disconnect_peer() { + connected_ = false; + if (notify_subscription_) { + g_dbus_connection_signal_unsubscribe(bus_, notify_subscription_); + notify_subscription_ = 0; + } + if (!device_path_.empty()) { + GError* error = nullptr; + GVariant* result = g_dbus_connection_call_sync(bus_, BLUEZ_BUS, device_path_.c_str(), + "org.bluez.Device1", "Disconnect", + nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, -1, + nullptr, &error); + if (!result) { + log_error("BLE linux-central: Device1.Disconnect failed", error); + } else { + g_variant_unref(result); + } + } + device_path_.clear(); + tx_char_path_.clear(); + rx_char_path_.clear(); + identity_char_path_.clear(); + reset_reassembly(); +} diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.h b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.h new file mode 100644 index 0000000..d013eb8 --- /dev/null +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +class HostBluezBleInterface : public RNS::InterfaceImpl { +public: + explicit HostBluezBleInterface(const std::string& node_label, + const char* name = "HostBluezBLE"); + ~HostBluezBleInterface() override; + + bool start() override; + void stop() override; + void loop() override; + + bool connected() const { return connected_; } + const char* role_name() const { return "linux-central"; } + +private: + void send_outgoing(const RNS::Bytes& data) override; + + bool connect_bus(); + bool find_adapter(); + bool start_discovery(); + bool stop_discovery(); + bool scan_for_peer(); + bool connect_device(const std::string& device_path); + bool discover_characteristics(); + bool start_notify(); + bool write_characteristic(const std::string& path, const uint8_t* data, size_t len, bool with_response); + 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(); + void disconnect_peer(); + + static void properties_changed(GDBusConnection* connection, + const gchar* sender_name, + const gchar* object_path, + const gchar* interface_name, + const gchar* signal_name, + GVariant* parameters, + gpointer user_data); + + static constexpr const char* BLUEZ_BUS = "org.bluez"; + 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; + static constexpr uint64_t SCAN_RETRY_MS = 5000; + + std::string node_label_; + GDBusConnection* bus_ = nullptr; + std::string adapter_path_; + std::string device_path_; + std::string tx_char_path_; + std::string rx_char_path_; + std::string identity_char_path_; + guint notify_subscription_ = 0; + bool online_ = false; + bool connected_ = false; + bool discovering_ = false; + uint64_t next_scan_ms_ = 0; + uint32_t tx_message_id_ = 0; + + 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_; +}; diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/host_jp_main.cpp b/exercises/306_microReticulum_ble_file_transfer_oled/src/host_jp_main.cpp new file mode 100644 index 0000000..88b4d7c --- /dev/null +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/host_jp_main.cpp @@ -0,0 +1,512 @@ +#include "HostBluezBleInterface.h" +#include "SelectedText.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +static constexpr const char* APP_NAME = "microreticulum"; +static constexpr const char* APP_ASPECT = "filetransfer"; +static constexpr const char* ANNOUNCE_FILTER = "microreticulum.filetransfer"; + +#ifndef FILE_TRANSFER_CHUNK_SIZE +#define FILE_TRANSFER_CHUNK_SIZE 32 +#endif + +#ifndef FILE_TRANSFER_CHUNK_INTERVAL_MS +#define FILE_TRANSFER_CHUNK_INTERVAL_MS 500 +#endif + +#ifndef FILE_TRANSFER_REPEAT_INTERVAL_MS +#define FILE_TRANSFER_REPEAT_INTERVAL_MS 10000 +#endif + +#ifndef HOST_NODE_LABEL +#define HOST_NODE_LABEL "Node-JP-CLIENT" +#endif + +static constexpr size_t TRANSFER_CHUNK_SIZE = FILE_TRANSFER_CHUNK_SIZE; +static constexpr uint32_t TRANSFER_CHUNK_INTERVAL_MS = FILE_TRANSFER_CHUNK_INTERVAL_MS; +static constexpr uint32_t TRANSFER_REPEAT_INTERVAL_MS = FILE_TRANSFER_REPEAT_INTERVAL_MS; +static constexpr uint32_t FNV1A_OFFSET = 2166136261UL; +static constexpr uint32_t FNV1A_PRIME = 16777619UL; + +static RNS::Reticulum reticulum({RNS::Type::NONE}); +static RNS::Interface ble_interface({RNS::Type::NONE}); +static RNS::Identity local_identity({RNS::Type::NONE}); +static RNS::Destination inbound_destination({RNS::Type::NONE}); +static RNS::Destination peer_destination({RNS::Type::NONE}); +static RNS::Link active_link({RNS::Type::NONE}); +static RNS::Link pending_link({RNS::Type::NONE}); +static RNS::Bytes peer_hash; +static std::string peer_label; +static bool have_peer = false; +static bool link_active = false; +static bool link_attempted = false; +static HostBluezBleInterface* ble_impl = nullptr; +static std::string node_label = HOST_NODE_LABEL; +static bool running = true; + +struct RxTransferState { + bool active = false; + std::string sender; + std::string file_name; + size_t expected_size = 0; + uint32_t expected_chunks = 0; + uint32_t expected_crc = 0; + uint32_t received_chunks = 0; + size_t received_size = 0; + uint32_t crc = FNV1A_OFFSET; +}; + +struct TxTransferState { + bool active = false; + bool complete = false; + size_t offset = 0; + uint32_t sequence = 0; + uint32_t total_chunks = 0; + uint32_t crc = 0; + uint64_t next_start_ms = 0; + uint32_t round = 0; +}; + +static RxTransferState rx_transfer; +static TxTransferState tx_transfer; + +static uint64_t millis64() { + return RNS::Utilities::OS::ltime(); +} + +static uint32_t fnv1a_update(uint32_t crc, uint8_t byte) { + crc ^= byte; + crc *= FNV1A_PRIME; + return crc; +} + +static uint32_t selected_text_crc() { + uint32_t crc = FNV1A_OFFSET; + for (size_t i = 0; i < SELECTED_TEXT_SIZE; ++i) { + crc = fnv1a_update(crc, read_selected_text_byte(i)); + } + return crc; +} + +static std::string hex32(uint32_t value) { + char buffer[9]; + std::snprintf(buffer, sizeof(buffer), "%08lX", (unsigned long)value); + return std::string(buffer); +} + +static std::string link_id_hex(const RNS::Link& link) { + if (!link) { + return "none"; + } + return link.link_id().toHex(); +} + +static bool should_initiate_link_to(const std::string& label) { + return std::strcmp(node_label.c_str(), label.c_str()) < 0; +} + +static void print_text_payload(const std::string& payload) { + std::fwrite(payload.data(), 1, payload.size(), stdout); + std::fflush(stdout); +} + +static size_t find_pipe(const std::string& text, size_t start = 0) { + return text.find('|', start); +} + +static uint32_t parse_u32(const std::string& text) { + return (uint32_t)std::strtoul(text.c_str(), nullptr, 10); +} + +static uint32_t parse_hex32(const std::string& text) { + return (uint32_t)std::strtoul(text.c_str(), nullptr, 16); +} + +static void handle_file_begin(const std::string& sender, + const std::string& file_name, + size_t size, + uint32_t chunks, + uint32_t crc) { + rx_transfer.active = true; + rx_transfer.sender = sender; + rx_transfer.file_name = file_name; + rx_transfer.expected_size = size; + rx_transfer.expected_chunks = chunks; + rx_transfer.expected_crc = crc; + rx_transfer.received_chunks = 0; + rx_transfer.received_size = 0; + rx_transfer.crc = FNV1A_OFFSET; + + std::printf("\nRX FILE BEGIN: from=%s file=%s bytes=%u chunks=%lu crc=%s\n", + sender.c_str(), file_name.c_str(), (unsigned)size, + (unsigned long)chunks, hex32(crc).c_str()); +} + +static void handle_file_data(const std::string& sender, + uint32_t sequence, + uint32_t total, + const std::string& payload) { + if (!rx_transfer.active || rx_transfer.sender != sender) { + std::printf("\nRX FILE DATA ignored: sender=%s seq=%lu no active transfer\n", + sender.c_str(), (unsigned long)sequence); + return; + } + + for (char c : payload) { + rx_transfer.crc = fnv1a_update(rx_transfer.crc, (uint8_t)c); + } + rx_transfer.received_chunks++; + rx_transfer.received_size += payload.size(); + + (void)sequence; + (void)total; + print_text_payload(payload); +} + +static void handle_file_end(const std::string& sender, + const std::string& file_name, + size_t size, + uint32_t chunks, + uint32_t crc) { + bool ok = rx_transfer.active && + rx_transfer.sender == sender && + rx_transfer.file_name == file_name && + rx_transfer.expected_size == size && + rx_transfer.expected_chunks == chunks && + rx_transfer.expected_crc == crc && + rx_transfer.received_size == size && + rx_transfer.received_chunks == chunks && + rx_transfer.crc == crc; + + std::printf("\nRX FILE END: from=%s file=%s received=%u/%u chunks=%lu/%lu crc=%s status=%s\n", + sender.c_str(), file_name.c_str(), + (unsigned)rx_transfer.received_size, (unsigned)size, + (unsigned long)rx_transfer.received_chunks, (unsigned long)chunks, + hex32(rx_transfer.crc).c_str(), ok ? "OK" : "VERIFY_FAIL"); + rx_transfer.active = false; +} + +static void parse_file_transfer_packet(const std::string& message) { + size_t p1 = find_pipe(message); + size_t p2 = p1 == std::string::npos ? std::string::npos : find_pipe(message, p1 + 1); + if (p1 == std::string::npos || p2 == std::string::npos) { + print_text_payload(message); + return; + } + + std::string type = message.substr(0, p1); + std::string sender = message.substr(p1 + 1, p2 - p1 - 1); + + if (type == "FTB") { + size_t p3 = find_pipe(message, p2 + 1); + size_t p4 = p3 == std::string::npos ? std::string::npos : find_pipe(message, p3 + 1); + size_t p5 = p4 == std::string::npos ? std::string::npos : find_pipe(message, p4 + 1); + if (p3 == std::string::npos || p4 == std::string::npos || p5 == std::string::npos) { + return; + } + handle_file_begin(sender, + message.substr(p2 + 1, p3 - p2 - 1), + parse_u32(message.substr(p3 + 1, p4 - p3 - 1)), + parse_u32(message.substr(p4 + 1, p5 - p4 - 1)), + parse_hex32(message.substr(p5 + 1))); + return; + } + + if (type == "FTD") { + size_t p3 = find_pipe(message, p2 + 1); + size_t p4 = p3 == std::string::npos ? std::string::npos : find_pipe(message, p3 + 1); + if (p3 == std::string::npos || p4 == std::string::npos) { + return; + } + handle_file_data(sender, + parse_u32(message.substr(p2 + 1, p3 - p2 - 1)), + parse_u32(message.substr(p3 + 1, p4 - p3 - 1)), + message.substr(p4 + 1)); + return; + } + + if (type == "FTE") { + size_t p3 = find_pipe(message, p2 + 1); + size_t p4 = p3 == std::string::npos ? std::string::npos : find_pipe(message, p3 + 1); + size_t p5 = p4 == std::string::npos ? std::string::npos : find_pipe(message, p4 + 1); + if (p3 == std::string::npos || p4 == std::string::npos || p5 == std::string::npos) { + return; + } + handle_file_end(sender, + message.substr(p2 + 1, p3 - p2 - 1), + parse_u32(message.substr(p3 + 1, p4 - p3 - 1)), + parse_u32(message.substr(p4 + 1, p5 - p4 - 1)), + parse_hex32(message.substr(p5 + 1))); + return; + } + + print_text_payload(message); +} + +static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet) { + (void)packet; + parse_file_transfer_packet(data.toString()); +} + +static void on_link_closed(RNS::Link& link) { + std::printf("LINK CLOSED: peer=%s link_id=%s\n", + peer_label.empty() ? "unknown" : peer_label.c_str(), + link_id_hex(link).c_str()); + active_link = {RNS::Type::NONE}; + pending_link = {RNS::Type::NONE}; + link_active = false; + link_attempted = false; +} + +static void on_outbound_link_established(RNS::Link& link) { + active_link = link; + active_link.set_packet_callback(on_link_packet); + active_link.set_link_closed_callback(on_link_closed); + link_active = true; + std::printf("LINK ACTIVE: outbound peer=%s link_id=%s\n", + peer_label.empty() ? "unknown" : peer_label.c_str(), + link_id_hex(active_link).c_str()); +} + +static void on_inbound_link_established(RNS::Link& link) { + active_link = link; + active_link.set_packet_callback(on_link_packet); + active_link.set_link_closed_callback(on_link_closed); + link_active = true; + link_attempted = true; + std::printf("LINK ACTIVE: inbound peer=%s link_id=%s\n", + peer_label.empty() ? "unknown" : peer_label.c_str(), + link_id_hex(active_link).c_str()); +} + +class FileAnnounceHandler : public RNS::AnnounceHandler { +public: + FileAnnounceHandler() : RNS::AnnounceHandler(ANNOUNCE_FILTER) {} + + void received_announce(const RNS::Bytes& destination_hash, + const RNS::Identity& announced_identity, + const RNS::Bytes& app_data) override { + std::string label = app_data ? app_data.toString() : "(no label)"; + if (label == node_label || !announced_identity) { + return; + } + + peer_hash = destination_hash; + peer_label = label; + peer_destination = RNS::Destination(announced_identity, + RNS::Type::Destination::OUT, + RNS::Type::Destination::SINGLE, + destination_hash); + have_peer = true; + std::printf("RX ANNOUNCE: label=%s hash=%s\n", + peer_label.c_str(), peer_hash.toHex().c_str()); + } +}; + +static RNS::HAnnounceHandler announce_handler(new FileAnnounceHandler()); + +static void send_announce() { + if (!inbound_destination) { + return; + } + std::printf("TX ANNOUNCE: %s\n", node_label.c_str()); + inbound_destination.announce(RNS::bytesFromString(node_label.c_str())); +} + +static void maybe_open_link() { + if (!have_peer || link_active || link_attempted || !peer_destination) { + return; + } + if (!should_initiate_link_to(peer_label)) { + return; + } + + std::printf("TX LINKREQUEST: opening link to %s\n", peer_label.c_str()); + pending_link = RNS::Link(peer_destination); + pending_link.set_packet_callback(on_link_packet); + pending_link.set_link_established_callback(on_outbound_link_established); + pending_link.set_link_closed_callback(on_link_closed); + link_attempted = true; +} + +static void send_link_message(const std::string& message) { + RNS::Packet(active_link, RNS::bytesFromString(message.c_str())).send(); +} + +static bool send_next_file_packet(uint64_t now) { + if (tx_transfer.complete && now < tx_transfer.next_start_ms) { + return false; + } + + if (!tx_transfer.active) { + tx_transfer.active = true; + tx_transfer.complete = false; + tx_transfer.offset = 0; + tx_transfer.sequence = 0; + tx_transfer.crc = selected_text_crc(); + tx_transfer.total_chunks = (SELECTED_TEXT_SIZE + TRANSFER_CHUNK_SIZE - 1) / TRANSFER_CHUNK_SIZE; + tx_transfer.round++; + + std::string begin = std::string("FTB|") + node_label + "|" + SELECTED_TEXT_NAME + "|" + + std::to_string((unsigned)SELECTED_TEXT_SIZE) + "|" + + std::to_string((unsigned long)tx_transfer.total_chunks) + "|" + + hex32(tx_transfer.crc); + std::printf("TX FILE BEGIN: round=%lu file=%s bytes=%u chunks=%lu crc=%s\n", + (unsigned long)tx_transfer.round, SELECTED_TEXT_NAME, + (unsigned)SELECTED_TEXT_SIZE, (unsigned long)tx_transfer.total_chunks, + hex32(tx_transfer.crc).c_str()); + send_link_message(begin); + return true; + } + + if (tx_transfer.offset < SELECTED_TEXT_SIZE) { + size_t remaining = SELECTED_TEXT_SIZE - tx_transfer.offset; + size_t count = remaining < TRANSFER_CHUNK_SIZE ? remaining : TRANSFER_CHUNK_SIZE; + std::string payload; + payload.reserve(count); + for (size_t i = 0; i < count; ++i) { + payload += (char)read_selected_text_byte(tx_transfer.offset + i); + } + tx_transfer.sequence++; + tx_transfer.offset += count; + + std::string data = std::string("FTD|") + node_label + "|" + + std::to_string((unsigned long)tx_transfer.sequence) + "|" + + std::to_string((unsigned long)tx_transfer.total_chunks) + "|" + payload; + send_link_message(data); + return true; + } + + std::string end = std::string("FTE|") + node_label + "|" + SELECTED_TEXT_NAME + "|" + + std::to_string((unsigned)SELECTED_TEXT_SIZE) + "|" + + std::to_string((unsigned long)tx_transfer.total_chunks) + "|" + + hex32(tx_transfer.crc); + std::printf("TX FILE END: round=%lu file=%s bytes=%u chunks=%lu crc=%s next_round_in_ms=%lu\n", + (unsigned long)tx_transfer.round, SELECTED_TEXT_NAME, + (unsigned)SELECTED_TEXT_SIZE, (unsigned long)tx_transfer.total_chunks, + hex32(tx_transfer.crc).c_str(), (unsigned long)TRANSFER_REPEAT_INTERVAL_MS); + send_link_message(end); + tx_transfer.active = false; + tx_transfer.complete = true; + tx_transfer.next_start_ms = now + TRANSFER_REPEAT_INTERVAL_MS; + return true; +} + +static void setup_reticulum() { + microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()}; + filesystem.init(); + RNS::Utilities::OS::register_filesystem(filesystem); + + auto impl = std::shared_ptr(new HostBluezBleInterface(node_label)); + ble_impl = static_cast(impl.get()); + ble_interface = RNS::Interface(impl); + ble_interface.mode(RNS::Type::Interface::MODE_GATEWAY); + RNS::Transport::register_interface(ble_interface); + ble_interface.start(); + + reticulum = RNS::Reticulum(); + reticulum.transport_enabled(false); + reticulum.probe_destination_enabled(false); + reticulum.start(); + + local_identity = RNS::Identity(); + inbound_destination = RNS::Destination(local_identity, + RNS::Type::Destination::IN, + RNS::Type::Destination::SINGLE, + APP_NAME, + APP_ASPECT); + inbound_destination.set_link_established_callback(on_inbound_link_established); + inbound_destination.set_proof_strategy(RNS::Type::Destination::PROVE_NONE); + + RNS::Transport::register_announce_handler(announce_handler); + + std::printf("Local SINGLE destination: %s\n", inbound_destination.hash().toHex().c_str()); +} + +static void handle_signal(int signal) { + (void)signal; + running = false; +} + +int main() { + std::signal(SIGINT, handle_signal); + std::signal(SIGTERM, handle_signal); + + RNS::loglevel(RNS::LOG_NOTICE); + std::printf("Exercise 306 jp native BLE file transfer console\n"); + std::printf("Node=%s\n", node_label.c_str()); + std::printf("Selected file=%s bytes=%u chunk=%u interval_ms=%lu repeat_rest_ms=%lu\n", + SELECTED_TEXT_NAME, (unsigned)SELECTED_TEXT_SIZE, + (unsigned)TRANSFER_CHUNK_SIZE, + (unsigned long)TRANSFER_CHUNK_INTERVAL_MS, + (unsigned long)TRANSFER_REPEAT_INTERVAL_MS); + + setup_reticulum(); + std::printf("microReticulum ready; OLED skipped on host native build\n"); + + uint64_t next_announce_ms = 0; + uint64_t next_transfer_ms = 0; + uint64_t next_wait_log_ms = 0; + + while (running) { + reticulum.loop(); + if (ble_impl) { + ble_impl->loop(); + } + + uint64_t now = millis64(); + if (ble_impl && !ble_impl->connected()) { + if (next_wait_log_ms == 0 || now >= next_wait_log_ms) { + next_wait_log_ms = now + 10000; + std::printf("BLE %s waiting for peer\n", ble_impl->role_name()); + } + RNS::Utilities::OS::sleep(0.005); + continue; + } + + if (next_announce_ms == 0) { + next_announce_ms = now + 1100; + } + if (!link_active && now >= next_announce_ms) { + next_announce_ms = now + 15000; + send_announce(); + } + + maybe_open_link(); + + if (link_active && next_transfer_ms == 0) { + next_transfer_ms = now + (should_initiate_link_to(peer_label) ? 900 : 1200); + } + + if (link_active && now >= next_transfer_ms) { + next_transfer_ms = now + TRANSFER_CHUNK_INTERVAL_MS; + send_next_file_packet(now); + } + + RNS::Utilities::OS::sleep(0.005); + } + + if (ble_impl) { + ble_impl->stop(); + } + std::printf("\nStopped\n"); + return 0; +}