diff --git a/lib/ble/CMakeLists.txt b/lib/ble/CMakeLists.txt index cb0e5ba9a..56dbf0c45 100644 --- a/lib/ble/CMakeLists.txt +++ b/lib/ble/CMakeLists.txt @@ -24,7 +24,7 @@ if(WIN32) find_path(BLE_WINRT_INCLUDE winrt/Windows.Devices.Bluetooth.h REQUIRED) target_include_directories(vibechat_ble PRIVATE "${BLE_WINRT_INCLUDE}") target_link_libraries(vibechat_ble PRIVATE windowsapp) - add_executable(test_ble_winrt tests/test_ble_winrt.cpp) + add_executable(test_ble_winrt tests/test_ble_winrt.cpp ble_identity.c) target_compile_features(test_ble_winrt PRIVATE cxx_std_20) target_include_directories(test_ble_winrt PRIVATE "${BLE_WINRT_INCLUDE}") target_link_libraries(test_ble_winrt PRIVATE uasync windowsapp) diff --git a/lib/ble/tests/ble_advertising_probe.cpp b/lib/ble/tests/ble_advertising_probe.cpp index bbf662a67..8141efa6d 100644 --- a/lib/ble/tests/ble_advertising_probe.cpp +++ b/lib/ble/tests/ble_advertising_probe.cpp @@ -1,4 +1,4 @@ -/* Ручной тест Windows: тот же backend, один publisher и неизменный scan; меняется только GATT-реклама. +/* Ручной тест Windows: один GATT advertising; проверка visible/hidden/restored без manufacturer publisher. * Запускать при закрытом VibeChat. Не является транспортом: входящие соединения отклоняются. */ #include "../ble_winrt.cpp" #include @@ -11,6 +11,7 @@ extern "C" void ble_backend_status(ble_transport* t, int ready, const char* reas extern "C" void ble_backend_advertising(ble_transport*, ble_state state, const char* reason) { DEBUG_INFO(CAT, "BLE advertising probe publication state=%d reason=%s", state, reason); } +extern "C" uint64_t ble_backend_peer(ble_transport*, ble_link_id) { return 0; } extern "C" ble_link_id ble_backend_incoming(ble_transport*, const ble_endpoint* ep) { DEBUG_WARN(CAT, "BLE advertising probe rejects incoming connection endpoint=%s; discovery test only", ep->id); return 0; } @@ -55,135 +56,39 @@ struct ProbeGatt { } }; -/* Даём ОС всё окно наблюдения, даже после Started: важен также возврат в Waiting. */ -static bool observe(WinState& s, WinState& tx, ble_transport& owner, const ProbeGatt& gatt, const char* phase, unsigned seconds) { - const auto start = get_time_us(), end = start + uint64_t(seconds) * 1000000; - uint64_t first_started_ms = 0; - unsigned started = 0, waiting = 0, unsettled = 0; - WinState::RadioStatus previous; - DEBUG_INFO(CAT, "BLE advertising probe begin phase=%s seconds=%u gatt_requested=%d generation=%llu", - phase, seconds, gatt.requested, (unsigned long long)tx.publish_generation); +/* API Started проверяется весь интервал; выход в эфир подтверждается сканером другого устройства. */ +static bool observe(WinState& s, ble_transport& owner, const char* phase, unsigned seconds) { + uint64_t end = get_time_us() + uint64_t(seconds) * 1000000; do { uasync_poll(s.ua, 100); - if (s.failed || tx.failed || owner.failed) { DEBUG_ERROR(CAT, "BLE advertising probe phase=%s backend failed", phase); return false; } + if (s.failed || owner.failed) return false; auto actual = s.radio_status(); - if (&tx != &s) actual.publisher = tx.radio_status().publisher; - if (actual.publisher < 0 || actual.service < 0 || actual.scan < 0) { - DEBUG_ERROR(CAT, "BLE advertising probe phase=%s cannot read radio state", phase); return false; + if (!s.service_stop_pending && s.service_ready && actual.service != 2) { + DEBUG_ERROR(CAT, "BLE probe unstable phase=%s service=%d scan=%d", phase, actual.service, actual.scan); return false; } - if (actual.publisher != previous.publisher || actual.service != previous.service || actual.scan != previous.scan) - DEBUG_INFO(CAT, "BLE advertising probe sample phase=%s elapsed_ms=%llu publisher=%d service=%d scan=%d", - phase, (unsigned long long)((get_time_us() - start) / 1000), actual.publisher, actual.service, actual.scan); - previous = actual; - if (!gatt.matches(actual.service) || actual.scan != int(BluetoothLEAdvertisementWatcherStatus::Started)) ++unsettled; - else if (actual.publisher == int(BluetoothLEAdvertisementPublisherStatus::Started)) { - if (!started) first_started_ms = (get_time_us() - start) / 1000; - ++started; - } else if (actual.publisher == int(BluetoothLEAdvertisementPublisherStatus::Waiting)) ++waiting; } while (get_time_us() < end); - DEBUG_INFO(CAT, "BLE advertising probe result phase=%s started_samples=%u waiting_samples=%u unsettled_samples=%u " - "first_started_ms=%llu final_publisher=%d final_service=%d final_scan=%d generation=%llu", - phase, started, waiting, unsettled, (unsigned long long)first_started_ms, - previous.publisher, previous.service, previous.scan, (unsigned long long)tx.publish_generation); - if (!gatt.matches(previous.service) || previous.scan != int(BluetoothLEAdvertisementWatcherStatus::Started)) { - DEBUG_ERROR(CAT, "BLE advertising probe phase=%s did not reach requested GATT/scan state; comparison inconclusive", phase); - return false; - } - return true; + auto actual = s.radio_status(); + bool ok = s.service_ready && !s.service_stop_pending && actual.service == 2 && actual.scan == 1; + DEBUG_INFO(CAT, "BLE probe phase=%s result=%d UUID=%s discoverable=%d service=%d scan=%d single_provider=1", + phase, ok, s.service_uuid.c_str(), s.broadcast_enabled, actual.service, actual.scan); + return ok; } - int main(int argc, char** argv) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_set_category_level(CAT, DEBUG_LEVEL_DEBUG); - if (argc == 2 && !std::strcmp(argv[1], "--help")) { - DEBUG_INFO(CAT, "Usage: ble_advertising_probe.exe [--publisher-first] [seconds_per_phase=20, range=10..60]; close VibeChat first"); - return 0; - } - bool publisher_first = argc > 1 && !std::strcmp(argv[1], "--publisher-first"); - int argument = publisher_first ? 2 : 1; - char* tail = nullptr; - long seconds = argc > argument ? std::strtol(argv[argument], &tail, 10) : 20; - if (argc > argument + 1 || (tail && (tail == argv[argument] || *tail)) || seconds < 10 || seconds > 60) { - DEBUG_ERROR(CAT, "BLE advertising probe expects [--publisher-first] [10..60 seconds] or --help"); return 2; - } + unsigned seconds = argc > 1 ? unsigned(std::atoi(argv[1])) : 10; + if (seconds < 10 || seconds > 60) { DEBUG_ERROR(CAT, "Usage: ble_advertising_probe.exe [10..60 seconds per phase]"); return 2; } debug_enable_file_output("ble_advertising_probe.log", 1); - DEBUG_INFO(CAT, "BLE advertising probe: close other VibeChat/probe processes; log=ble_advertising_probe.log; " - "publisher 1=Waiting 2=Started; GATT 1=Stopped 2=Started 3=Aborted; scan 1=Started"); - UASYNC* ua = uasync_create(); - if (!ua) { DEBUG_ERROR(CAT, "BLE advertising probe cannot create uasync"); return 1; } - ble_transport owner; - ble_config config{}; config.node_id = 0x5643424c45544553ULL; config.company_id = 0xffff; - ble_backend* backend = nullptr; - std::shared_ptr early; - bool completed = false; - try { - /* Изолированный publisher исключает автоматическую перестановку GATT в рабочем backend. */ - auto start_publisher = [&] { - early = std::make_shared(); early->ua = ua; early->owner = &owner; early->company = config.company_id; - init_apartment(apartment_type::multi_threaded); early->apartment = true; - int size = ble_identity_encode(early->broadcast.data(), config.node_id, "probe18"); - if (size < 0) throw std::runtime_error("cannot encode manufacturer identity"); - early->broadcast_size = size; early->broadcast_enabled = true; - early->publish(); - }; - if (publisher_first) { - DEBUG_INFO(CAT, "BLE advertising probe mode=publisher-first; starting publisher before creating GATT/scan"); - start_publisher(); - auto start = get_time_us(), deadline = start + 20000000; - while (!owner.failed && early->publisher_status != int(BluetoothLEAdvertisementPublisherStatus::Started) && - get_time_us() < deadline) uasync_poll(ua, 100); - auto actual = early->radio_status(); - if (owner.failed || early->publisher_status != int(BluetoothLEAdvertisementPublisherStatus::Started) || - actual.publisher != int(BluetoothLEAdvertisementPublisherStatus::Started)) { - DEBUG_ERROR(CAT, "BLE advertising probe publisher-first startup failed event=%d actual=%d; GATT was not started", - early->publisher_status, actual.publisher); - throw std::runtime_error("publisher did not reach Started within 20s before GATT creation"); - } - DEBUG_INFO(CAT, "BLE advertising probe publisher-first Started elapsed_ms=%llu generation=%llu; now creating GATT/scan", - (unsigned long long)((get_time_us() - start) / 1000), (unsigned long long)early->publish_generation); - } - backend = ble_backend_create(&owner, ua, &config); - if (!backend) throw std::runtime_error("backend creation failed"); + UASYNC* ua = uasync_create(); if (!ua) return 1; + ble_transport owner; ble_config cfg{}; cfg.node_id = UINT64_C(0x5643424c45544553); cfg.company_id = 0xffff; + cfg.initial_advertisement_size = ble_identity_encode(cfg.initial_advertisement, cfg.node_id, "WinGATT-probe"); + ble_backend* backend = ble_backend_create(&owner, ua, &cfg); bool ok = backend != nullptr; + if (backend) { auto& s = *backend->state; - auto deadline = get_time_us() + 20000000; - while (!owner.ready && !owner.failed && get_time_us() < deadline) uasync_poll(ua, 100); - if (!owner.ready || owner.failed) throw std::runtime_error("scan/GATT startup failed or timed out after 20s"); - auto gatt = std::make_shared(); - gatt->last = GattServiceProviderAdvertisementStatus::Started; /* Startup уже доставил Started в owner.ready. */ - s.service_changed.revoke(); - auto weak = s.weak_from_this(); - s.service_changed = s.provider.AdvertisementStatusChanged(auto_revoke, [weak, gatt](auto const&, auto const& args) { - if (auto state = weak.lock()) state->post("probe GATT.AdvertisementStatusChanged", 0, [gatt, args](WinState& state) { - gatt->changed(state, args.Status(), args.Error()); - }); - }); - if (publisher_first) { - if (!observe(s, *early, owner, *gatt, "publisher_first_gatt_on", seconds)) - throw std::runtime_error("publisher-first observation incomplete"); - } else { - start_publisher(); - if (!observe(s, *early, owner, *gatt, "gatt_on", seconds)) - throw std::runtime_error("baseline incomplete"); - DEBUG_INFO(CAT, "BLE advertising probe stopping GATT advertisement; publisher and scan remain unchanged"); - gatt->requested = false; gatt->stop_confirmed = false; - s.service_requested = false; - s.provider.StopAdvertising(); - if (!observe(s, *early, owner, *gatt, "gatt_off", seconds)) - throw std::runtime_error("GATT-off observation incomplete"); - DEBUG_INFO(CAT, "BLE advertising probe restoring GATT advertisement connectable=1 discoverable=1"); - GattServiceProviderAdvertisingParameters parameters; parameters.IsConnectable(true); parameters.IsDiscoverable(true); - gatt->requested = true; gatt->stop_confirmed = false; - s.service_requested = true; - s.provider.StartAdvertising(parameters); - if (!observe(s, *early, owner, *gatt, "gatt_restored", seconds)) - throw std::runtime_error("GATT-restored observation incomplete"); - } - completed = true; - DEBUG_INFO(CAT, "BLE advertising probe complete mode=%s; inspect phase results. " - "Started confirms Windows scheduling, not reception or incoming connections", - publisher_first ? "publisher-first" : "gatt-on-off-restored"); - } catch (...) { DEBUG_ERROR(CAT, "%s", exception_reason("advertising probe").c_str()); } - if (backend) ble_backend_destroy(backend); - if (early) early->shutdown(); - uasync_destroy(ua, 0); debug_disable_file_output(); - return completed ? 0 : 1; + ok = observe(s, owner, "visible", seconds); + if (ok) ok = ble_backend_advertise(backend, nullptr, 0) == BLE_OK && observe(s, owner, "hidden", seconds); + if (ok) ok = ble_backend_advertise(backend, cfg.initial_advertisement, cfg.initial_advertisement_size) == BLE_OK && + observe(s, owner, "restored", seconds); + ble_backend_destroy(backend); + } + uasync_destroy(ua, 0); DEBUG_INFO(CAT, "BLE single GATT advertising probe result=%s", ok ? "PASS" : "FAIL"); return !ok; } diff --git a/lib/ble/tests/test_ble_winrt.cpp b/lib/ble/tests/test_ble_winrt.cpp index d8a80a6fe..d1a760f1d 100644 --- a/lib/ble/tests/test_ble_winrt.cpp +++ b/lib/ble/tests/test_ble_winrt.cpp @@ -20,6 +20,7 @@ extern "C" void ble_backend_status(ble_transport* t, int ready, const char* reas while (!t->state->links.empty()) t->state->close(t->state->links.begin()->first); } /* Эти события не должны возникать в проверке mailbox. */ +extern "C" uint64_t ble_backend_peer(ble_transport*, ble_link_id) { return 3; } 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* 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); } @@ -171,19 +172,6 @@ static void advertisement_addresses(UASYNC* ua) { DEBUG_INFO(CAT, "WinRT test PASS: 1809 events use unspecified type; 2004 events preserve public/random/unspecified"); } -struct TestPublisher : implements { - bool read_error = false; - BluetoothLEAdvertisementPublisherStatus Status() { - if (read_error) throw hresult_error(E_ACCESSDENIED, L"test publisher status denied"); - return BluetoothLEAdvertisementPublisherStatus::Started; - } - BluetoothLEAdvertisement Advertisement() { CHECK(false); return nullptr; } - void Start() { CHECK(false); } - void Stop() { CHECK(false); } - event_token StatusChanged(const TypedEventHandler&) { CHECK(false); return {}; } - void StatusChanged(event_token) noexcept { CHECK(false); } -}; struct TestProvider : implements { bool read_error = false; GattLocalService Service() { CHECK(false); return nullptr; } @@ -201,24 +189,14 @@ struct TestProvider : implements { /* Снимок отличает ОС от задержанных событий; ошибка одного getter не мешает остальным и не останавливает radio. */ static void radio_snapshot(UASYNC* ua) { - ble_transport owner; - auto s = make_state(ua, owner); - auto empty = s->radio_status(); - CHECK(empty.publisher == -1 && empty.service == -1 && empty.scan == -1); - auto publisher = make_self(); auto provider = make_self(); - s->publisher = publisher.as(); - s->provider = provider.as(); - s->publisher_status = int(BluetoothLEAdvertisementPublisherStatus::Waiting); s->service_ready = true; - auto actual = s->radio_status(); - CHECK(actual.publisher == 2 && actual.service == 1 && actual.scan == -1); - publisher->read_error = true; - actual = s->radio_status(); CHECK(actual.publisher == -2 && actual.service == 1); - publisher->read_error = false; provider->read_error = true; - actual = s->radio_status(); CHECK(actual.publisher == 2 && actual.service == -2); - CHECK(!s->failed && !owner.radio_failures && !owner.link_failures && s->links.size() == 2); - CHECK(s->publisher_status == 1 && s->service_ready); - s->publisher = nullptr; s->provider = nullptr; s->shutdown(); - DEBUG_INFO(CAT, "WinRT test PASS: actual radio snapshot tolerates getter failures without changing radio/events/links"); + ble_transport owner; auto s = make_state(ua, owner); + auto empty = s->radio_status(); CHECK(empty.service == -1 && empty.scan == -1); + auto provider = make_self(); s->provider = provider.as(); s->service_ready = true; + auto actual = s->radio_status(); CHECK(actual.service == 1 && actual.scan == -1); + provider->read_error = true; actual = s->radio_status(); CHECK(actual.service == -2); + CHECK(!s->failed && !owner.radio_failures && !owner.link_failures && s->links.size() == 2 && s->service_ready); + s->provider = nullptr; s->shutdown(); + DEBUG_INFO(CAT, "WinRT test PASS: actual GATT/scan snapshot tolerates getter failures without changing links"); } static void publication_lifecycle(UASYNC* ua) { @@ -227,10 +205,9 @@ static void publication_lifecycle(UASYNC* ua) { 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); + CHECK(!s->publication_deadline && owner.advertising_events == 0); /* Подтверждение StopAdvertising/Success не должно закрывать существующие links или повторять READY. */ s->broadcast_enabled = false; s->broadcast_size = 0; for (auto status : {GattServiceProviderAdvertisementStatus::Stopped, GattServiceProviderAdvertisementStatus::Aborted}) { @@ -242,7 +219,7 @@ static void publication_lifecycle(UASYNC* ua) { } 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(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");