Files
utun2/lib/ble/ble_winrt.cpp
T
evgeny f84622966e Add BLE ETCP links and GATT invitations for desktop and Android
Attach packet drivers to initially empty ETCP connections. Share BLE neighbor
tracking and group fallback between desktop and Android in cgathui_common.

Deliver join links through a dedicated Write/Indicate characteristic with
independent fragmentation queues, application acknowledgements, deduplication
and bounded retries. Merge LAN/BLE discovery and add a separate Bluetooth
invitation reception setting to both clients.

Validation: 230 core tests, BLE transport/BlueZ/UI tests, ASAN/UBSAN,
Linux GUI and Android builds, and Windows backend cross-compilation.
Physical Android/Linux invitation exchange passed with either GATT initiator.
The Android-initiated run also verified all 32 ETCP packets of 1500 bytes;
the desktop-initiated load run reached 7/32 within its 45-second time limit.
2026-10-29 16:41:04 +01:00

563 lines
37 KiB
C++

/* WinRT backend: события Windows сериализуются в uasync через ограниченный mailbox.
* Каждая отправка адресована link/subscribedClient; одна незавершённая операция на link.
* Слабые ссылки в WinRT handlers и отменяемый posted_task исключают обращения к уничтоженному core. */
#include "ble_backend.h"
#include "../u_async.h"
#include "../mem.h"
#include "../debug_config.h"
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Devices.Bluetooth.h>
#include <winrt/Windows.Devices.Bluetooth.Advertisement.h>
#include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h>
#include <winrt/Windows.Storage.Streams.h>
#include <array>
#include <algorithm>
#include <vector>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <cstring>
#include <cstdio>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Devices::Bluetooth;
using namespace Windows::Devices::Bluetooth::Advertisement;
using namespace Windows::Devices::Bluetooth::GenericAttributeProfile;
using namespace Windows::Storage::Streams;
static constexpr auto CAT = DEBUG_CATEGORY_DISCOVERY;
static const char* bluetooth_error(BluetoothError error) {
switch (error) {
case BluetoothError::Success: return "Success";
case BluetoothError::RadioNotAvailable: return "RadioNotAvailable";
case BluetoothError::ResourceInUse: return "ResourceInUse";
case BluetoothError::DeviceNotConnected: return "DeviceNotConnected";
case BluetoothError::DisabledByPolicy: return "DisabledByPolicy";
case BluetoothError::NotSupported: return "NotSupported";
case BluetoothError::DisabledByUser: return "DisabledByUser";
case BluetoothError::ConsentRequired: return "ConsentRequired";
case BluetoothError::TransportNotSupported: return "TransportNotSupported";
default: return "OtherError";
}
}
/* Вызывать только из catch; сохраняем контекст и HRESULT вместо немого catch(...). */
static std::string exception_reason(const char* phase, ble_link_id id = 0) {
char prefix[192];
try { throw; }
catch (const hresult_error& e) {
std::snprintf(prefix, sizeof(prefix), "BLE WinRT phase=%s link=%llu HRESULT=0x%08x: ",
phase, (unsigned long long)id, (unsigned)e.code().value);
return prefix + to_string(e.message());
} catch (const std::exception& e) {
return std::string("BLE WinRT phase=") + phase + ": " + e.what();
} catch (...) { return std::string("BLE WinRT phase=") + phase + ": unknown exception"; }
}
struct WinState;
struct WinLink {
ble_link_id id = 0;
std::string endpoint;
hstring device_id;
bool outgoing = false, opened = false, sending = false;
BluetoothLEDevice device{nullptr};
GattDeviceService service{nullptr};
GattCharacteristic rx{nullptr}, tx{nullptr}, invite{nullptr};
GattSession session{nullptr};
GattSubscribedClient subscriber{nullptr}, invite_subscriber{nullptr};
IAsyncInfo operation{nullptr};
GattCharacteristic::ValueChanged_revoker value_changed, invite_changed;
GattSession::SessionStatusChanged_revoker session_changed;
GattSession::MaxPduSizeChanged_revoker pdu_changed;
};
struct WriteDeferral {
Deferral value;
explicit WriteDeferral(Deferral v) : value(v) {}
~WriteDeferral() { try { value.Complete(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("write deferral").c_str()); } }
};
struct WinState : std::enable_shared_from_this<WinState> {
struct UASYNC* ua = nullptr;
struct ble_transport* owner = nullptr;
uint16_t company = 0;
std::mutex mutex;
std::deque<std::function<void(WinState&)>> events;
struct posted_task* posted = nullptr;
bool stopped = false, failed = false, overflow = false, apartment = false, scan_ready = false, service_ready = false;
IAsyncInfo startup{nullptr};
BluetoothLEAdvertisementWatcher watcher{nullptr};
BluetoothLEAdvertisementPublisher publisher{nullptr};
GattServiceProvider provider{nullptr};
GattLocalCharacteristic local_rx{nullptr}, local_tx{nullptr}, local_invite{nullptr};
BluetoothLEAdvertisementWatcher::Received_revoker received;
BluetoothLEAdvertisementWatcher::Stopped_revoker scan_stopped;
BluetoothLEAdvertisementPublisher::StatusChanged_revoker publish_changed;
GattServiceProvider::AdvertisementStatusChanged_revoker service_changed;
GattLocalCharacteristic::WriteRequested_revoker write_requested, invite_write_requested;
GattLocalCharacteristic::SubscribedClientsChanged_revoker subscriptions_changed, invite_subscriptions_changed;
std::map<ble_link_id, std::unique_ptr<WinLink>> links;
std::array<uint8_t, BLE_BROADCAST_MAX> broadcast{};
size_t broadcast_size = 0;
bool broadcast_enabled = false;
uint64_t publish_generation = 0;
static void dispatch(void* arg) {
auto& s = *static_cast<WinState*>(arg);
std::deque<std::function<void(WinState&)>> batch;
bool overloaded;
{ std::lock_guard guard(s.mutex); s.posted = nullptr; batch.swap(s.events); overloaded = s.overflow; s.overflow = false; }
if (overloaded) { s.fail("WinRT event queue overflow"); return; }
DEBUG_TRACE(CAT, "BLE WinRT dispatch events=%zu links=%zu", batch.size(), s.links.size());
for (auto& event : batch) {
if (s.stopped || s.failed) break;
try { event(s); }
catch (...) { s.fail(exception_reason("event dispatch").c_str()); }
}
}
void post(std::function<void(WinState&)> event) {
std::lock_guard guard(mutex);
if (stopped) return;
if (events.size() >= 256) {
if (!overflow) DEBUG_ERROR(CAT, "BLE WinRT event mailbox full queued=%zu limit=256", events.size());
overflow = true;
}
else events.push_back(std::move(event));
if (posted) return;
posted = static_cast<struct posted_task*>(u_calloc(1, sizeof(struct posted_task)));
if (!posted) { overflow = true; DEBUG_ERROR(CAT, "BLE WinRT event allocation failed"); return; }
posted->callback = dispatch; posted->arg = this; uasync_post_reserved(ua, posted);
}
WinLink* link(ble_link_id id) { auto it = links.find(id); return it == links.end() ? nullptr : it->second.get(); }
void fail(const char* reason) {
if (failed) return;
failed = true; DEBUG_ERROR(CAT, "BLE WinRT failed: %s", reason); ble_backend_status(owner, 0, reason);
}
void error(ble_link_id id, const char* reason) {
DEBUG_WARN(CAT, "BLE WinRT link=%llu failed: %s", (unsigned long long)id, reason);
ble_backend_closed(owner, id, reason);
}
void radio_error(const char* phase, BluetoothError error) {
char reason[192];
std::snprintf(reason, sizeof(reason), "%s: BluetoothError=%d (%s)", phase, (int)error, bluetooth_error(error));
fail(reason);
}
template<typename Result, typename Callback>
void await(const char* phase, const IAsyncOperation<Result>& operation, ble_link_id id, Callback callback) {
const bool fragment = !std::strcmp(phase, "write frame") || !std::strcmp(phase, "indicate frame");
if (!fragment) DEBUG_DEBUG(CAT, "BLE WinRT async begin phase=%s link=%llu", phase, (unsigned long long)id);
if (id) { auto* l = link(id); if (!l) { operation.Cancel(); return; } l->operation = operation; }
else startup = operation;
auto weak = weak_from_this();
operation.Completed([weak, id, phase, fragment, callback](const IAsyncOperation<Result>& op, AsyncStatus status) {
if (auto s = weak.lock()) s->post([id, op, status, phase, fragment, callback](WinState& self) {
auto* l = id ? self.link(id) : nullptr;
if (id && !l) {
DEBUG_DEBUG(CAT, "BLE WinRT stale completion phase=%s link=%llu status=%d", phase, (unsigned long long)id, (int)status);
return;
}
if (l) l->operation = nullptr; else self.startup = nullptr;
if (status != AsyncStatus::Completed) {
char reason[192];
std::snprintf(reason, sizeof(reason), "phase=%s async_status=%d HRESULT=0x%08x", phase, (int)status,
(unsigned)op.ErrorCode().value);
if (id) self.error(id, reason); else self.fail(reason);
return;
}
if (!fragment) DEBUG_DEBUG(CAT, "BLE WinRT async complete phase=%s link=%llu", phase, (unsigned long long)id);
callback(self, op.GetResults());
});
});
}
static IBuffer buffer(const uint8_t* data, size_t len) {
DataWriter writer; writer.WriteBytes(array_view<const uint8_t>(data, data + len)); return writer.DetachBuffer();
}
static std::vector<uint8_t> bytes(const IBuffer& buffer) {
if (buffer.Length() > BLE_VALUE_MAX) throw hresult_invalid_argument();
std::vector<uint8_t> result(buffer.Length()); DataReader::FromBuffer(buffer).ReadBytes(result); return result;
}
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");
}
void watch_session(WinLink& l) {
auto weak = weak_from_this(); auto id = l.id;
l.session_changed = l.session.SessionStatusChanged(auto_revoke, [weak, id](auto const&, GattSessionStatusChangedEventArgs const& a) {
auto status = a.Status(); auto error = a.Error();
if (auto s = weak.lock()) s->post([id, status, error](WinState& s) {
DEBUG_DEBUG(CAT, "BLE WinRT session link=%llu status=%d error=%d (%s)",
(unsigned long long)id, (int)status, (int)error, bluetooth_error(error));
if (status == GattSessionStatus::Closed && s.link(id)) s.error(id, "GATT session closed");
});
});
l.pdu_changed = l.session.MaxPduSizeChanged(auto_revoke, [weak, id](auto const& session, auto const&) {
auto mtu = session.MaxPduSize();
if (auto s = weak.lock()) s->post([id, mtu](WinState&) {
DEBUG_DEBUG(CAT, "BLE WinRT negotiated PDU changed link=%llu mtu=%u", (unsigned long long)id, (unsigned)mtu);
});
});
}
WinLink* incoming(const GattSession& session) {
hstring key = session.DeviceId().Id();
for (auto& [id, l] : links) if (l->device_id == key) return l->outgoing ? nullptr : l.get();
std::string endpoint = to_string(key);
if (endpoint.size() >= BLE_ENDPOINT_MAX || links.size() >= BLE_LINKS_MAX) {
DEBUG_WARN(CAT, "BLE WinRT incoming endpoint/connection limit"); return nullptr;
}
struct ble_endpoint ep{}; std::memcpy(ep.id, endpoint.c_str(), endpoint.size() + 1);
ble_link_id id = ble_backend_incoming(owner, &ep); if (!id) return nullptr;
auto l = std::make_unique<WinLink>(); l->id = id; l->endpoint = endpoint; l->device_id = key; l->session = session;
DEBUG_DEBUG(CAT, "BLE WinRT incoming link=%llu endpoint=%s mtu=%u", (unsigned long long)id, endpoint.c_str(), session.MaxPduSize());
auto* result = l.get(); links.emplace(id, std::move(l)); watch_session(*result); return result;
}
void subscriptions() {
std::vector<ble_link_id> active;
for (auto const& client : local_tx.SubscribedClients()) {
for (auto const& invitation : local_invite.SubscribedClients()) {
if (client.Session().DeviceId().Id() != invitation.Session().DeviceId().Id()) continue;
auto* l = incoming(client.Session()); if (!l) continue;
l->subscriber = client; l->invite_subscriber = invitation; active.push_back(l->id);
if (!l->opened) {
auto capacity = std::min(client.MaxNotificationSize(), invitation.MaxNotificationSize());
DEBUG_DEBUG(CAT, "BLE WinRT subscribed both channels link=%llu capacity=%u", (unsigned long long)l->id, capacity);
l->opened = true; ble_backend_opened(owner, l->id, capacity + 3);
}
}
}
std::vector<ble_link_id> gone;
for (auto& [id, l] : links) if (!l->outgoing && l->opened && std::find(active.begin(), active.end(), id) == active.end()) gone.push_back(id);
for (auto id : gone) error(id, "indication subscription removed");
}
void make_server() {
await("create GATT service", GattServiceProvider::CreateAsync(guid(BLE_SERVICE_UUID)), 0, [](WinState& s, GattServiceProviderResult const& result) {
if (result.Error() != BluetoothError::Success) { s.radio_error("create GATT service", result.Error()); return; }
s.provider = result.ServiceProvider();
GattLocalCharacteristicParameters parameters;
parameters.CharacteristicProperties(GattCharacteristicProperties::Write);
parameters.WriteProtectionLevel(GattProtectionLevel::Plain);
s.await("create RX characteristic", s.provider.Service().CreateCharacteristicAsync(guid(BLE_RX_UUID), parameters), 0,
[](WinState& s, GattLocalCharacteristicResult const& result) {
if (result.Error() != BluetoothError::Success) { s.radio_error("create RX characteristic", result.Error()); return; }
s.local_rx = result.Characteristic(); s.make_tx();
});
});
}
void make_tx() {
GattLocalCharacteristicParameters parameters; parameters.CharacteristicProperties(GattCharacteristicProperties::Indicate);
parameters.ReadProtectionLevel(GattProtectionLevel::Plain);
await("create TX characteristic", provider.Service().CreateCharacteristicAsync(guid(BLE_TX_UUID), parameters), 0,
[](WinState& s, GattLocalCharacteristicResult const& result) {
if (result.Error() != BluetoothError::Success) { s.radio_error("create TX characteristic", result.Error()); return; }
s.local_tx = result.Characteristic(); s.make_invite();
});
}
void make_invite() {
GattLocalCharacteristicParameters parameters;
parameters.CharacteristicProperties(GattCharacteristicProperties::Write | GattCharacteristicProperties::Indicate);
parameters.WriteProtectionLevel(GattProtectionLevel::Plain); parameters.ReadProtectionLevel(GattProtectionLevel::Plain);
await("create invitation characteristic", provider.Service().CreateCharacteristicAsync(guid(BLE_INVITE_UUID), parameters), 0,
[](WinState& s, GattLocalCharacteristicResult const& result) {
if (result.Error() != BluetoothError::Success) { s.radio_error("create invitation characteristic", result.Error()); return; }
s.local_invite = result.Characteristic(); s.start_server();
});
}
GattLocalCharacteristic::WriteRequested_revoker watch_write(GattLocalCharacteristic const& characteristic, enum ble_channel channel) {
auto weak = weak_from_this();
return characteristic.WriteRequested(auto_revoke, [weak, channel](auto const&, GattWriteRequestedEventArgs const& args) {
try {
auto deferral = std::make_shared<WriteDeferral>(args.GetDeferral());
auto request = args.GetRequestAsync(); auto session = args.Session();
request.Completed([weak, deferral, session, channel](auto const& operation, AsyncStatus status) {
if (auto s = weak.lock()) s->post([deferral, session, operation, status, channel](WinState& s) {
if (status != AsyncStatus::Completed) {
DEBUG_WARN(CAT, "BLE WinRT GetRequest status=%d HRESULT=0x%08x", (int)status, (unsigned)operation.ErrorCode().value);
return;
}
auto request = operation.GetResults(); if (!request) { DEBUG_WARN(CAT, "BLE WinRT expired write request"); return; }
if (request.Offset() || request.Option() != GattWriteOption::WriteWithResponse || request.Value().Length() > BLE_VALUE_MAX) {
DEBUG_WARN(CAT, "BLE WinRT invalid write offset=%u option=%d bytes=%u", request.Offset(),
(int)request.Option(), request.Value().Length());
request.RespondWithProtocolError(0x06); return;
}
auto* l = s.incoming(session);
if (!l) { request.RespondWithProtocolError(0x08); return; }
auto id = l->id; auto data = bytes(request.Value());
DEBUG_TRACE(CAT, "BLE WinRT RX write link=%llu bytes=%zu", (unsigned long long)id, data.size());
ble_backend_received(s.owner, id, channel, data.data(), data.size());
if (s.link(id)) request.Respond(); else request.RespondWithProtocolError(0x08);
});
});
} catch (...) {
auto reason = exception_reason("WriteRequested");
if (auto s = weak.lock()) s->post([reason](WinState& s) { s.fail(reason.c_str()); });
}
});
}
void start_server() {
auto weak = weak_from_this();
write_requested = watch_write(local_rx, BLE_CHANNEL_DATA);
invite_write_requested = watch_write(local_invite, BLE_CHANNEL_INVITE);
invite_subscriptions_changed = local_invite.SubscribedClientsChanged(auto_revoke, [weak](auto const&, auto const&) {
if (auto s = weak.lock()) s->post([](WinState& s) { s.subscriptions(); });
});
subscriptions_changed = local_tx.SubscribedClientsChanged(auto_revoke, [weak](auto const&, auto const&) {
if (auto s = weak.lock()) s->post([](WinState& s) { s.subscriptions(); });
});
service_changed = provider.AdvertisementStatusChanged(auto_revoke, [weak](auto const&, auto const& args) {
auto status = args.Status(); auto error = args.Error();
if (auto s = weak.lock()) s->post([status, error](WinState& s) {
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);
});
});
GattServiceProviderAdvertisingParameters parameters; parameters.IsConnectable(true); parameters.IsDiscoverable(true);
DEBUG_DEBUG(CAT, "BLE WinRT start server service=%s connectable=1 discoverable=1", BLE_SERVICE_UUID);
provider.StartAdvertising(parameters);
watcher = BluetoothLEAdvertisementWatcher(); watcher.ScanningMode(BluetoothLEScanningMode::Active);
received = watcher.Received(auto_revoke, [weak](auto const&, BluetoothLEAdvertisementReceivedEventArgs const& args) {
auto state = weak.lock(); if (!state) return;
bool matching = false;
for (auto const& value : args.Advertisement().ManufacturerData()) if (value.CompanyId() == state->company) matching = true;
if (!matching) return;
state->post([args](WinState& s) {
for (auto const& value : args.Advertisement().ManufacturerData()) {
if (value.CompanyId() != s.company || value.Data().Length() > BLE_BROADCAST_MAX) continue;
struct ble_endpoint ep{};
std::snprintf(ep.id, sizeof(ep.id), "%012llx/%d", (unsigned long long)args.BluetoothAddress(), (int)args.BluetoothAddressType());
auto data = bytes(value.Data());
DEBUG_TRACE(CAT, "BLE WinRT broadcast endpoint=%s company=%04x bytes=%zu rssi=%d", ep.id, s.company,
data.size(), args.RawSignalStrengthInDBm());
ble_backend_broadcast(s.owner, &ep, data.data(), data.size(), args.RawSignalStrengthInDBm());
}
});
});
scan_stopped = watcher.Stopped(auto_revoke, [weak](auto const&, auto const& args) {
auto error = args.Error(); if (auto s = weak.lock()) s->post([error](WinState& s) {
s.radio_error("scan stopped", error);
});
});
watcher.Start();
DEBUG_DEBUG(CAT, "BLE WinRT scan requested mode=active status=%d company=%04x", (int)watcher.Status(), company);
scan_ready = true; ready();
}
void publish() {
DEBUG_DEBUG(CAT, "BLE WinRT publish requested enabled=%d bytes=%zu generation=%llu", broadcast_enabled,
broadcast_size, (unsigned long long)publish_generation);
if (publisher) {
auto status = publisher.Status();
if (status == BluetoothLEAdvertisementPublisherStatus::Started || status == BluetoothLEAdvertisementPublisherStatus::Waiting) {
publisher.Stop(); return;
}
if (status == BluetoothLEAdvertisementPublisherStatus::Stopping) return;
publish_changed.revoke(); publisher = nullptr;
}
if (!broadcast_enabled) return;
BluetoothLEAdvertisement advertisement;
BluetoothLEManufacturerData data; data.CompanyId(company); data.Data(buffer(broadcast.data(), broadcast_size));
advertisement.ManufacturerData().Append(data); publisher = BluetoothLEAdvertisementPublisher(advertisement);
auto weak = weak_from_this(); auto generation = ++publish_generation;
publish_changed = publisher.StatusChanged(auto_revoke, [weak, generation](auto const&, auto const& args) {
auto status = args.Status(); auto error = args.Error();
if (auto s = weak.lock()) s->post([status, error, generation](WinState& s) {
if (generation != s.publish_generation) return;
DEBUG_DEBUG(CAT, "BLE WinRT broadcast state=%d error=%d (%s) generation=%llu", (int)status, (int)error,
bluetooth_error(error), (unsigned long long)generation);
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 ble_backend_advertising(s.owner, BLE_STARTING, "publisher waiting/stopping");
});
});
publisher.Start();
}
void connect(ble_link_id id, uint64_t address, BluetoothAddressType type) {
DEBUG_DEBUG(CAT, "BLE WinRT connect link=%llu address=%012llx type=%d", (unsigned long long)id, (unsigned long long)address, (int)type);
await("open BLE device", BluetoothLEDevice::FromBluetoothAddressAsync(address, type), id, [id](WinState& s, BluetoothLEDevice const& device) {
auto* l = s.link(id);
if (!device) { s.error(id, "BLE device unavailable"); return; }
l->device = device; l->device_id = device.BluetoothDeviceId().Id();
DEBUG_DEBUG(CAT, "BLE WinRT device link=%llu endpoint=%s status=%d", (unsigned long long)id,
to_string(l->device_id).c_str(), (int)device.ConnectionStatus());
s.await("discover GATT service", device.GetGattServicesForUuidAsync(guid(BLE_SERVICE_UUID), BluetoothCacheMode::Uncached), id,
[id](WinState& s, GattDeviceServicesResult const& result) {
auto protocol = result.ProtocolError();
DEBUG_DEBUG(CAT, "BLE WinRT services link=%llu status=%d ATT_error=%d count=%u", (unsigned long long)id,
(int)result.Status(), protocol ? int(protocol.Value()) : -1,
result.Status() == GattCommunicationStatus::Success ? result.Services().Size() : 0);
if (result.Status() != GattCommunicationStatus::Success || result.Services().Size() != 1) {
s.error(id, "VibeChat GATT service unavailable/ambiguous"); return;
}
auto* l = s.link(id); l->service = result.Services().GetAt(0);
s.await("discover GATT characteristics", l->service.GetCharacteristicsAsync(BluetoothCacheMode::Uncached), id,
[id](WinState& s, GattCharacteristicsResult const& result) {
auto protocol = result.ProtocolError();
DEBUG_DEBUG(CAT, "BLE WinRT characteristics link=%llu status=%d ATT_error=%d count=%u", (unsigned long long)id,
(int)result.Status(), protocol ? int(protocol.Value()) : -1,
result.Status() == GattCommunicationStatus::Success ? result.Characteristics().Size() : 0);
if (result.Status() != GattCommunicationStatus::Success) { s.error(id, "GATT discovery failed"); return; }
auto* l = s.link(id);
for (auto const& ch : result.Characteristics()) {
DEBUG_DEBUG(CAT, "BLE WinRT characteristic link=%llu uuid=%s properties=0x%x", (unsigned long long)id,
to_string(to_hstring(ch.Uuid())).c_str(), (unsigned)ch.CharacteristicProperties());
if (ch.Uuid() == guid(BLE_RX_UUID)) l->rx = ch;
if (ch.Uuid() == guid(BLE_TX_UUID)) l->tx = ch;
if (ch.Uuid() == guid(BLE_INVITE_UUID)) l->invite = ch;
}
if (!l->rx || !l->tx || !l->invite) { s.error(id, "RX/TX/invitation characteristics missing"); return; }
s.subscribe(id);
});
});
});
}
void subscribe(ble_link_id id) {
auto* l = link(id);
await("open GATT session", GattSession::FromDeviceIdAsync(l->device.BluetoothDeviceId()), id, [id](WinState& s, GattSession const& session) {
auto* l = s.link(id);
if (!session) { s.error(id, "GATT session unavailable"); return; }
l->session = session; session.MaintainConnection(true); s.watch_session(*l);
DEBUG_DEBUG(CAT, "BLE WinRT session opened link=%llu mtu=%u maintain_connection=1", (unsigned long long)id, session.MaxPduSize());
l->value_changed = s.watch_value(id, l->tx, BLE_CHANNEL_DATA);
l->invite_changed = s.watch_value(id, l->invite, BLE_CHANNEL_INVITE);
s.subscribe_channel(id, BLE_CHANNEL_DATA);
});
}
GattCharacteristic::ValueChanged_revoker watch_value(ble_link_id id, GattCharacteristic const& characteristic, enum ble_channel channel) {
auto weak = weak_from_this();
return characteristic.ValueChanged(auto_revoke, [weak, id, channel](auto const&, GattValueChangedEventArgs const& args) {
if (auto s = weak.lock()) s->post([id, channel, args](WinState& s) {
if (!s.link(id)) return;
auto data = bytes(args.CharacteristicValue());
ble_backend_received(s.owner, id, channel, data.data(), data.size());
});
});
}
void subscribe_channel(ble_link_id id, enum ble_channel channel) {
auto* l = link(id);
auto characteristic = channel == BLE_CHANNEL_DATA ? l->tx : l->invite;
await("subscribe indications", characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::Indicate), id,
[id, channel](WinState& s, GattCommunicationStatus status) {
DEBUG_DEBUG(CAT, "BLE WinRT subscribe link=%llu channel=%d status=%d", (unsigned long long)id, channel, (int)status);
if (status != GattCommunicationStatus::Success) { s.error(id, "GATT subscription failed"); return; }
if (channel == BLE_CHANNEL_DATA) { s.subscribe_channel(id, BLE_CHANNEL_INVITE); return; }
auto* l = s.link(id); l->opened = true; ble_backend_opened(s.owner, id, l->session.MaxPduSize());
});
}
void close(ble_link_id id) {
auto it = links.find(id); if (it == links.end()) return;
auto l = std::move(it->second); links.erase(it);
DEBUG_DEBUG(CAT, "BLE WinRT close link=%llu outgoing=%d opened=%d sending=%d pending=%d", (unsigned long long)id,
l->outgoing, l->opened, l->sending, bool(l->operation));
l->value_changed.revoke(); l->invite_changed.revoke(); l->session_changed.revoke(); l->pdu_changed.revoke();
try {
if (l->operation) l->operation.Cancel();
if (l->outgoing) {
if (l->session) { l->session.MaintainConnection(false); l->session.Close(); }
if (l->service) l->service.Close();
if (l->device) l->device.Close();
}
} catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("close", id).c_str()); }
}
void shutdown() {
struct posted_task* task;
std::deque<std::function<void(WinState&)>> discarded;
{ std::lock_guard guard(mutex); stopped = true; task = posted; posted = nullptr; discarded.swap(events); }
DEBUG_DEBUG(CAT, "BLE WinRT shutdown links=%zu discarded_events=%zu pending_dispatch=%d", links.size(), discarded.size(), task != nullptr);
discarded.clear();
if (task) uasync_cancel_post(ua, task);
received.revoke(); scan_stopped.revoke(); publish_changed.revoke(); service_changed.revoke();
write_requested.revoke(); subscriptions_changed.revoke(); invite_write_requested.revoke(); invite_subscriptions_changed.revoke();
while (!links.empty()) close(links.begin()->first);
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()); }
watcher = nullptr; publisher = nullptr; provider = nullptr; local_rx = nullptr; local_tx = nullptr; local_invite = nullptr; startup = nullptr;
if (apartment) { uninit_apartment(); apartment = false; }
}
};
struct ble_backend { std::shared_ptr<WinState> 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<WinState>(); state->owner = owner; state->ua = ua; state->company = cfg->company_id;
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;
state->await("get default adapter", BluetoothAdapter::GetDefaultAsync(), 0, [](WinState& s, BluetoothAdapter const& adapter) {
if (!adapter) { s.fail("no default Bluetooth adapter"); return; }
DEBUG_DEBUG(CAT, "BLE WinRT adapter id=%s address=%012llx BLE=%d central=%d peripheral=%d", to_string(adapter.DeviceId()).c_str(),
(unsigned long long)adapter.BluetoothAddress(), adapter.IsLowEnergySupported(),
adapter.IsCentralRoleSupported(), adapter.IsPeripheralRoleSupported());
try {
auto advertising = adapter.try_as<IBluetoothAdapter3>();
auto phy = adapter.try_as<IBluetoothAdapter4>();
DEBUG_DEBUG(CAT, "BLE WinRT adapter extended_advertising=%d advertisement_max=%d PHY_2M_supported=%d PHY_coded_supported=%d (-1=API unavailable)",
advertising ? int(advertising.IsExtendedAdvertisingSupported()) : -1,
advertising ? int(advertising.MaxAdvertisementDataLength()) : -1,
phy ? int(phy.IsLowEnergyUncoded2MPhySupported()) : -1, phy ? int(phy.IsLowEnergyCodedPhySupported()) : -1);
} catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("optional adapter capabilities").c_str()); }
if (!adapter.IsLowEnergySupported() || !adapter.IsCentralRoleSupported() || !adapter.IsPeripheralRoleSupported()) {
s.fail("adapter lacks BLE central/peripheral support"); return;
}
s.make_server();
});
void* memory = u_malloc(sizeof(ble_backend));
if (!memory) { DEBUG_ERROR(CAT, "BLE WinRT allocation failed"); state->shutdown(); return nullptr; }
return new(memory) ble_backend{state};
} catch (...) { DEBUG_ERROR(CAT, "%s", exception_reason("initialize").c_str()); state->shutdown(); return nullptr; }
}
extern "C" void ble_backend_destroy(struct ble_backend* b) {
if (!b) return;
b->state->shutdown(); b->~ble_backend(); u_free(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;
if (len) std::memcpy(s.broadcast.data(), data, len);
s.publish(); return BLE_OK;
} catch (...) { DEBUG_ERROR(CAT, "%s", exception_reason("advertise").c_str()); return BLE_ERROR; }
}
extern "C" int ble_backend_connect(struct ble_backend* b, ble_link_id id, const struct ble_endpoint* ep) {
unsigned long long address; int type, consumed = 0;
if (std::sscanf(ep->id, "%llx/%d%n", &address, &type, &consumed) != 2 || ep->id[consumed] || address >> 48 || type < 0 || type > 2) {
DEBUG_ERROR(CAT, "BLE invalid WinRT endpoint=%s", ep->id); return BLE_ERROR;
}
try {
auto& s = *b->state; auto l = std::make_unique<WinLink>(); l->id = id; l->endpoint = ep->id; l->outgoing = true;
s.links.emplace(id, std::move(l)); s.connect(id, address, static_cast<BluetoothAddressType>(type)); return BLE_OK;
} catch (...) { DEBUG_ERROR(CAT, "%s", exception_reason("connect", id).c_str()); b->state->close(id); return BLE_ERROR; }
}
extern "C" void ble_backend_close(struct ble_backend* b, ble_link_id id) { b->state->close(id); }
extern "C" int ble_backend_send(struct ble_backend* b, ble_link_id id, enum ble_channel channel, const uint8_t* data, size_t len) {
auto& s = *b->state; auto* l = s.link(id);
if ((unsigned)channel >= BLE_CHANNEL_COUNT || !l || !l->opened || l->sending) {
DEBUG_ERROR(CAT, "BLE invalid WinRT send link=%llu exists=%d opened=%d sending=%d bytes=%zu", (unsigned long long)id,
l != nullptr, l && l->opened, l && l->sending, len);
return BLE_ERROR;
}
try {
DEBUG_TRACE(CAT, "BLE WinRT TX fragment link=%llu outgoing=%d bytes=%zu", (unsigned long long)id, l->outgoing, len);
l->sending = true; auto buffer = WinState::buffer(data, len);
if (l->outgoing) s.await("write frame", (channel == BLE_CHANNEL_INVITE ? l->invite : l->rx).WriteValueWithResultAsync(buffer, GattWriteOption::WriteWithResponse), id,
[id, len](WinState& s, GattWriteResult const& result) {
auto* l = s.link(id); l->sending = false;
auto protocol = result.ProtocolError();
if (result.Status() != GattCommunicationStatus::Success)
DEBUG_WARN(CAT, "BLE WinRT write failed link=%llu bytes=%zu status=%d ATT_error=%d", (unsigned long long)id,
len, (int)result.Status(), protocol ? int(protocol.Value()) : -1);
else DEBUG_TRACE(CAT, "BLE WinRT write complete link=%llu bytes=%zu", (unsigned long long)id, len);
ble_backend_sent(s.owner, id, result.Status() == GattCommunicationStatus::Success);
});
else s.await("indicate frame", (channel == BLE_CHANNEL_INVITE ? s.local_invite : s.local_tx).NotifyValueAsync(buffer,
channel == BLE_CHANNEL_INVITE ? l->invite_subscriber : l->subscriber), id, [id, len](WinState& s, GattClientNotificationResult const& result) {
auto* l = s.link(id); l->sending = false;
if (result.Status() != GattCommunicationStatus::Success || result.BytesSent() != len)
DEBUG_WARN(CAT, "BLE WinRT indication failed link=%llu expected=%zu sent=%u status=%d", (unsigned long long)id,
len, result.BytesSent(), (int)result.Status());
else DEBUG_TRACE(CAT, "BLE WinRT indication complete link=%llu bytes=%zu", (unsigned long long)id, len);
ble_backend_sent(s.owner, id, result.Status() == GattCommunicationStatus::Success && result.BytesSent() == len);
});
return BLE_OK;
} catch (...) { l->sending = false; DEBUG_ERROR(CAT, "%s", exception_reason("send", id).c_str()); return BLE_ERROR; }
}