From 77979c405835e2f70d5ea3c327e8ec99baa7862a Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 30 Oct 2026 17:52:42 +0100 Subject: [PATCH] Start Windows BLE publisher before GATT and sequence updates --- lib/ble/ble_winrt.cpp | 99 ++++++++++++++++++++++++++------ lib/ble/tests/test_ble_winrt.cpp | 44 ++++++++++++-- 2 files changed, 123 insertions(+), 20 deletions(-) diff --git a/lib/ble/ble_winrt.cpp b/lib/ble/ble_winrt.cpp index ba8f1c164..ca815dacd 100644 --- a/lib/ble/ble_winrt.cpp +++ b/lib/ble/ble_winrt.cpp @@ -107,6 +107,9 @@ struct WinState : std::enable_shared_from_this { int publisher_status = -1; /* Последний StatusChanged; -1: publisher ещё не создан. */ uint64_t publisher_wait_since = 0; bool publisher_wait_warned = false; + bool adapter_ready = false, server_created = false, ready_reported = false; + bool service_requested = false, service_stop_pending = false, publication_pending = false; + uint64_t publication_deadline = 0; /* Ограничивает запуск/замену publisher, включая освобождение GATT-рекламы. */ IAsyncInfo startup{nullptr}; BluetoothLEAdvertisementWatcher watcher{nullptr}; BluetoothLEAdvertisementPublisher publisher{nullptr}; @@ -170,7 +173,8 @@ struct WinState : std::enable_shared_from_this { static void diagnostics_tick(void* arg) { auto& s = *static_cast(arg); s.diagnostics_timer = nullptr; s.discovery_stats("periodic"); - if (s.stopped) return; + s.publication_timeout(); + if (s.stopped || s.failed) return; s.diagnostics_timer = uasync_set_timeout(s.ua, 100000, &s, diagnostics_tick, "ble_winrt_diagnostics"); /* 10s, TB=0.1ms. */ if (!s.diagnostics_timer) DEBUG_WARN(CAT, "BLE WinRT diagnostics timer allocation failed; periodic summaries unavailable"); } @@ -279,7 +283,46 @@ struct WinState : std::enable_shared_from_this { } void ready() { DEBUG_DEBUG(CAT, "BLE WinRT readiness scan=%d service=%d", scan_ready, service_ready); - if (scan_ready && service_ready) ble_backend_status(owner, 1, "WinRT scan/GATT ready"); + if (scan_ready && service_ready && !ready_reported) { + ready_reported = true; ble_backend_status(owner, 1, "WinRT scan/GATT ready"); + } + } + void publication_timeout() { + if (!publication_deadline || get_time_us() < publication_deadline || failed || stopped) return; + publication_deadline = 0; + DEBUG_ERROR(CAT, "BLE WinRT publication timeout publisher=%d service_requested=%d stop_pending=%d generation=%llu", + publisher_status, service_requested, service_stop_pending, (unsigned long long)publish_generation); + ble_backend_advertising(owner, BLE_CLOSED, "Windows BLE publisher did not start within 20 seconds"); + fail("Windows BLE publication timed out; GATT startup/update incomplete"); + } + void start_service_advertising() { + GattServiceProviderAdvertisingParameters parameters; parameters.IsConnectable(true); parameters.IsDiscoverable(true); + DEBUG_INFO(CAT, "BLE WinRT start GATT advertisement service=%s publisher=%d generation=%llu connectable=1 discoverable=1", + BLE_SERVICE_UUID, publisher_status, (unsigned long long)publish_generation); + service_requested = true; + step("GattServiceProvider.StartAdvertising"); provider.StartAdvertising(parameters); + } + /* Первичная регистрация — только после Started publisher либо при выключенном объявлении. */ + void ensure_service() { + if (!adapter_ready || failed || stopped || service_stop_pending) return; + if (!server_created) { + server_created = true; + DEBUG_INFO(CAT, "BLE WinRT create GATT after publication enabled=%d publisher=%d", broadcast_enabled, publisher_status); + make_server(); + } else if (provider && !service_requested) start_service_advertising(); + } + void service_status(GattServiceProviderAdvertisementStatus status, BluetoothError error) { + DEBUG_DEBUG(CAT, "BLE WinRT service advertising state=%d error=%d (%s) requested=%d stop_pending=%d", + int(status), int(error), bluetooth_error(error), service_requested, service_stop_pending); + if (error != BluetoothError::Success) { radio_error("GATT advertising failed", error); return; } + if (service_stop_pending && !service_requested && (status == GattServiceProviderAdvertisementStatus::Stopped || + status == GattServiceProviderAdvertisementStatus::Aborted)) { + service_stop_pending = false; service_ready = false; + DEBUG_INFO(CAT, "BLE WinRT requested GATT stop confirmed state=%d; updating manufacturer advertisement", int(status)); + publish(); + } else if (status == GattServiceProviderAdvertisementStatus::Started) { service_ready = true; ready(); } + else if (status == GattServiceProviderAdvertisementStatus::Aborted) radio_error("GATT advertising aborted", error); + else DEBUG_INFO(CAT, "BLE WinRT service advertising state=%d", int(status)); } /* Только доступные Win11 API: фактические параметры отделены от запрошенного профиля. */ void log_connection(ble_link_id id) { @@ -497,16 +540,10 @@ struct WinState : std::enable_shared_from_this { step("GattServiceProvider.AdvertisementStatusChanged registration (auto_revoke)"); service_changed = provider.AdvertisementStatusChanged(auto_revoke, [weak](auto const&, auto const& args) { if (auto s = weak.lock()) s->post("GattServiceProvider.AdvertisementStatusChanged", 0, [args](WinState& s) { - auto status = args.Status(); auto error = args.Error(); - DEBUG_DEBUG(CAT, "BLE WinRT service advertising state=%d error=%d (%s)", (int)status, (int)error, bluetooth_error(error)); - if (status == GattServiceProviderAdvertisementStatus::Started) { s.service_ready = true; s.ready(); } - else if (status == GattServiceProviderAdvertisementStatus::Aborted) s.radio_error("GATT advertising aborted", error); - else DEBUG_INFO(CAT, "BLE WinRT service advertising state=%d", (int)status); + s.service_status(args.Status(), args.Error()); }); }); - GattServiceProviderAdvertisingParameters parameters; parameters.IsConnectable(true); parameters.IsDiscoverable(true); - DEBUG_DEBUG(CAT, "BLE WinRT start server service=%s connectable=1 discoverable=1", BLE_SERVICE_UUID); - step("GattServiceProvider.StartAdvertising"); provider.StartAdvertising(parameters); + start_service_advertising(); step("BluetoothLEAdvertisementWatcher construction/ScanningMode"); watcher = BluetoothLEAdvertisementWatcher(); watcher.ScanningMode(BluetoothLEScanningMode::Active); step("Watcher.Received registration (auto_revoke)"); @@ -558,12 +595,21 @@ struct WinState : std::enable_shared_from_this { }); step("Watcher.Start"); watcher.Start(); DEBUG_DEBUG(CAT, "BLE WinRT scan requested mode=active status=%d company=%04x", (int)watcher.Status(), company); - scan_ready = true; diagnostics_tick(this); ready(); + scan_ready = true; ready(); } void publish() { + if (failed || stopped) return; step("Publisher.Status"); DEBUG_DEBUG(CAT, "BLE WinRT publish requested enabled=%d bytes=%zu generation=%llu", broadcast_enabled, broadcast_size, (unsigned long long)publish_generation); + if (!publication_deadline) publication_deadline = get_time_us() + 20000000; + if (service_stop_pending) return; + if (provider && service_requested && broadcast_enabled) { + service_requested = false; service_stop_pending = true; + DEBUG_INFO(CAT, "BLE WinRT pause GATT advertisement before publisher update links=%zu generation=%llu", + links.size(), (unsigned long long)publish_generation); + step("GattServiceProvider.StopAdvertising for publisher"); provider.StopAdvertising(); return; + } if (publisher) { auto status = publisher.Status(); if (status == BluetoothLEAdvertisementPublisherStatus::Started || status == BluetoothLEAdvertisementPublisherStatus::Waiting) { @@ -573,11 +619,14 @@ struct WinState : std::enable_shared_from_this { publish_changed.revoke(); publisher = nullptr; publisher_status = -1; publisher_wait_since = 0; publisher_wait_warned = false; } - if (!broadcast_enabled) return; + if (!broadcast_enabled) { + publication_pending = false; publication_deadline = 0; ensure_service(); return; + } step("Publisher construction/manufacturer data"); BluetoothLEAdvertisement advertisement; BluetoothLEManufacturerData data; data.CompanyId(company); data.Data(buffer(broadcast.data(), broadcast_size)); advertisement.ManufacturerData().Append(data); publisher = BluetoothLEAdvertisementPublisher(advertisement); + publication_pending = false; publisher_status = int(BluetoothLEAdvertisementPublisherStatus::Created); auto weak = weak_from_this(); auto generation = ++publish_generation; step("Publisher.StatusChanged registration (auto_revoke)"); @@ -604,7 +653,12 @@ struct WinState : std::enable_shared_from_this { if (status == BluetoothLEAdvertisementPublisherStatus::Aborted) s.radio_error("broadcast publication aborted", error); else if (status == BluetoothLEAdvertisementPublisherStatus::Stopped) { ble_backend_advertising(s.owner, BLE_CLOSED, "publisher stopped"); s.publish(); - } else if (status == BluetoothLEAdvertisementPublisherStatus::Started) ble_backend_advertising(s.owner, BLE_READY, "publisher started"); + } else if (status == BluetoothLEAdvertisementPublisherStatus::Started) { + ble_backend_advertising(s.owner, BLE_READY, "publisher started"); + if (!s.publication_pending && s.publisher.Status() == BluetoothLEAdvertisementPublisherStatus::Started) { + s.publication_deadline = 0; s.ensure_service(); + } + } else if (status == BluetoothLEAdvertisementPublisherStatus::Waiting) ble_backend_advertising(s.owner, BLE_STARTING, "publisher waiting for radio resources"); else if (status == BluetoothLEAdvertisementPublisherStatus::Stopping) @@ -755,7 +809,8 @@ struct WinState : std::enable_shared_from_this { try { if (startup) startup.Cancel(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("cancel startup").c_str()); } try { if (watcher) watcher.Stop(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("stop scan").c_str()); } try { if (publisher) publisher.Stop(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("stop publisher").c_str()); } - try { if (provider) provider.StopAdvertising(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("stop server").c_str()); } + try { if (provider && service_requested) provider.StopAdvertising(); } + catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("stop server").c_str()); } watcher = nullptr; publisher = nullptr; provider = nullptr; local_rx = nullptr; local_tx = nullptr; local_invite = nullptr; local_ack = nullptr; startup = nullptr; if (apartment) { uninit_apartment(); apartment = false; } @@ -765,6 +820,10 @@ struct ble_backend { std::shared_ptr state; }; extern "C" struct ble_backend* ble_backend_create(struct ble_transport* owner, struct UASYNC* ua, const struct ble_config* cfg) { if (cfg->adapter) { DEBUG_ERROR(CAT, "BLE WinRT explicit adapter selection unsupported"); return nullptr; } auto state = std::make_shared(); state->owner = owner; state->ua = ua; state->company = cfg->company_id; + state->broadcast_size = cfg->initial_advertisement_size; + state->broadcast_enabled = state->publication_pending = state->broadcast_size != 0; + if (state->broadcast_size > state->broadcast.size()) { DEBUG_ERROR(CAT, "BLE WinRT initial advertisement too large"); return nullptr; } + if (state->broadcast_size) std::memcpy(state->broadcast.data(), cfg->initial_advertisement, state->broadcast_size); try { DEBUG_DEBUG(CAT, "BLE WinRT initialize company=%04x node=%016llx apartment=MTA", cfg->company_id, (unsigned long long)cfg->node_id); init_apartment(apartment_type::multi_threaded); state->apartment = true; @@ -797,7 +856,8 @@ extern "C" struct ble_backend* ble_backend_create(struct ble_transport* owner, s if (!adapter.IsLowEnergySupported() || !adapter.IsCentralRoleSupported() || !adapter.IsPeripheralRoleSupported()) { s.fail("adapter lacks BLE central/peripheral support"); return; } - s.make_server(); + s.adapter_ready = true; WinState::diagnostics_tick(&s); + if (s.broadcast_enabled) s.publish(); else s.ensure_service(); }); void* memory = u_malloc(sizeof(ble_backend)); if (!memory) { DEBUG_ERROR(CAT, "BLE WinRT allocation failed"); state->shutdown(); return nullptr; } @@ -810,7 +870,14 @@ extern "C" void ble_backend_destroy(struct ble_backend* b) { } extern "C" int ble_backend_advertise(struct ble_backend* b, const uint8_t* data, size_t len) { try { - auto& s = *b->state; s.broadcast_enabled = data != nullptr; s.broadcast_size = len; + auto& s = *b->state; + if (s.failed || s.stopped) { DEBUG_WARN(CAT, "BLE WinRT advertisement requested on closed backend"); return BLE_NOT_READY; } + if (len > s.broadcast.size() || (len && !data)) { DEBUG_ERROR(CAT, "BLE WinRT invalid advertisement bytes=%zu", len); return BLE_ERROR; } + if (s.broadcast_enabled == (data != nullptr) && s.broadcast_size == len && (!len || !std::memcmp(s.broadcast.data(), data, len))) { + DEBUG_DEBUG(CAT, "BLE WinRT advertisement unchanged bytes=%zu generation=%llu", len, (unsigned long long)s.publish_generation); + return BLE_OK; + } + s.broadcast_enabled = data != nullptr; s.broadcast_size = len; s.publication_pending = true; if (len) std::memcpy(s.broadcast.data(), data, len); s.publish(); return BLE_OK; } catch (...) { DEBUG_ERROR(CAT, "%s", exception_reason(b->state->operation).c_str()); return BLE_ERROR; } diff --git a/lib/ble/tests/test_ble_winrt.cpp b/lib/ble/tests/test_ble_winrt.cpp index 3bc83263b..cb83b45e8 100644 --- a/lib/ble/tests/test_ble_winrt.cpp +++ b/lib/ble/tests/test_ble_winrt.cpp @@ -7,7 +7,7 @@ struct ble_transport { WinState* state = nullptr; - unsigned radio_failures = 0, link_failures = 0; + unsigned radio_failures = 0, link_failures = 0, ready_events = 0, advertising_events = 0; ble_link_id closed = 0; std::string reason; }; @@ -15,12 +15,13 @@ extern "C" void ble_backend_closed(ble_transport* t, ble_link_id id, const char* ++t->link_failures; t->closed = id; t->reason = reason; t->state->close(id); } extern "C" void ble_backend_status(ble_transport* t, int ready, const char* reason) { - CHECK(!ready); ++t->radio_failures; t->reason = reason; + if (ready) { ++t->ready_events; return; } + ++t->radio_failures; t->reason = reason; while (!t->state->links.empty()) t->state->close(t->state->links.begin()->first); } /* Эти события не должны возникать в проверке mailbox. */ extern "C" ble_link_id ble_backend_incoming(ble_transport*, const ble_endpoint*) { CHECK(false); return 0; } -extern "C" void ble_backend_advertising(ble_transport*, ble_state, const char*) { CHECK(false); } +extern "C" void ble_backend_advertising(ble_transport* t, ble_state, const char*) { ++t->advertising_events; } extern "C" void ble_backend_broadcast(ble_transport*, const ble_endpoint*, const uint8_t*, size_t, int) { CHECK(false); } extern "C" void ble_backend_opened(ble_transport*, ble_link_id, unsigned) { CHECK(false); } extern "C" void ble_backend_received(ble_transport*, ble_link_id, ble_wire_channel, const uint8_t*, size_t) { CHECK(false); } @@ -140,6 +141,40 @@ static void radio_snapshot(UASYNC* ua) { DEBUG_INFO(CAT, "WinRT test PASS: actual radio snapshot tolerates getter failures without changing radio/events/links"); } +static void publication_lifecycle(UASYNC* ua) { + ble_transport owner; + auto s = make_state(ua, owner); + s->scan_ready = true; s->ready(); CHECK(owner.ready_events == 0); + s->service_ready = true; s->ready(); s->ready(); CHECK(owner.ready_events == 1); + s->broadcast_enabled = true; s->broadcast_size = 3; std::memcpy(s->broadcast.data(), "abc", 3); + s->publish_generation = 7; + ble_backend backend{s}; + CHECK(ble_backend_advertise(&backend, reinterpret_cast("abc"), 3) == BLE_OK); + CHECK(s->publish_generation == 7 && !s->publication_deadline && !s->publication_pending && owner.advertising_events == 0); + /* Подтверждение StopAdvertising/Success не должно закрывать существующие links или повторять READY. */ + s->broadcast_enabled = false; s->broadcast_size = 0; + for (auto status : {GattServiceProviderAdvertisementStatus::Stopped, GattServiceProviderAdvertisementStatus::Aborted}) { + s->service_stop_pending = true; s->service_requested = false; + s->service_status(status, BluetoothError::Success); + CHECK(!s->failed && !s->service_ready && !s->service_stop_pending && s->links.size() == 2); + s->service_status(GattServiceProviderAdvertisementStatus::Started, BluetoothError::Success); + CHECK(s->service_ready && owner.ready_events == 1); + } + s->publication_deadline = get_time_us() - 1; + s->publication_timeout(); s->publication_timeout(); + CHECK(s->failed && owner.radio_failures == 1 && owner.advertising_events == 1 && s->links.empty()); + CHECK(ble_backend_advertise(&backend, nullptr, 0) == BLE_NOT_READY); + s->shutdown(); + DEBUG_INFO(CAT, "WinRT test PASS: readiness gate, unchanged payload, requested stop/restart, publication timeout and closed backend"); + for (auto error : {BluetoothError::Success, BluetoothError::RadioNotAvailable}) { + ble_transport failed_owner; + auto state = make_state(ua, failed_owner); + state->service_stop_pending = error != BluetoothError::Success; + state->service_status(GattServiceProviderAdvertisementStatus::Aborted, error); + CHECK(state->failed && failed_owner.radio_failures == 1); state->shutdown(); + } +} + /* Ошибка GATT одного пира не переводит radio в failed и не теряет событие другого пира. */ static void link_exception(UASYNC* ua) { ble_transport owner; @@ -203,7 +238,8 @@ int main() { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); init_apartment(apartment_type::multi_threaded); UASYNC* ua = uasync_create(); CHECK(ua); - advertisement_addresses(ua); radio_snapshot(ua); link_exception(ua); radio_exception(ua); pending_shutdown(ua); + advertisement_addresses(ua); radio_snapshot(ua); publication_lifecycle(ua); + link_exception(ua); radio_exception(ua); pending_shutdown(ua); uasync_destroy(ua, 0); uninit_apartment(); return 0; }