Skip to content

feat!: Port WinRT to the driver layer - #168

Open
OleRoss wants to merge 15 commits into
feat/v2-hcihost-peripheralfrom
feat/v2-winrt
Open

OleRoss wants to merge 15 commits into
feat/v2-hcihost-peripheralfrom
feat/v2-winrt

Conversation

@OleRoss

@OleRoss OleRoss commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

v1's WinRT backend sat on base classes that v2 removed, so the v2 solution had no Windows backend. This PR ports it to the driver layer for all four roles on Windows' default adapter.

The core keeps the queues, timeouts and app-facing types; the drivers wrap Windows' APIs, and where Windows cannot meet a driver contract, the driver's docs say so. The bookkeeping that needs no Windows (central sessions, subscription lists, advertising sections) builds for both target frameworks and is unit-tested on Linux.

The PR also changes the core and the other backends:

  • Every backend reports whole advertisements as values instead of one HCI report, whose one-byte length cut extended data over 255 bytes. HciHost joins the parts of a split advertisement first.
  • BleAddress keeps its type in text with the "T" format, and an address without a type matches and connects by its value.
  • The driver layer is experimental and reports DARPBLE0001, as it will gain members.

Changes

var manager = new BleManager(builder => builder.AddWinRT());
await using BleAdapter adapter = await (await manager.EnumerateAdaptersAsync(ct)).Single().OpenAsync(ct);

await adapter.Peripheral.AddServiceAsync(service, ct);
await using AdvertisingSet set = await adapter.Broadcaster.StartAdvertisingAsync(
    AdvertisingData.Empty, TimeSpan.FromMilliseconds(100), BleEventType.AdvInd, cancellationToken: ct);   // advertises the services

Review WinRTPeripheralDriver.cs with WinRTHostedService.cs, WinRTConnectionDriver.cs, CentralSessions.cs, WinRTAdvertisingSetDriver.cs with WinRTRadio.cs, and HciHost's AdvertisingReportAssembler.cs.

  • Windows serves a service only while it advertises it, so added services are live while a connectable set runs, and that set carries no data of the app's own.
  • Windows' limits on the driver contract: a request counts as sent once its Windows call started, disposing a connection ends it for this app while Windows may keep the link, and advertising does not stop when a central connects.
  • Windows exchanges the MTU itself after connecting, so ConnectAsync waits up to 500 ms for it. A later change reaches the core through the new IConnectionSink.OnMtuChanged(int), so MaxWritePayloadLength grows.
  • When the radio turns off, scans and non-connectable sets fail with a transient BleException. Windows restarts connectable sets itself.
  • A backend reports a set it stopped through the new IAdvertisingSetSink.OnFailed(BleException), so StartAsync starts it again.
  • Fixed: a set with autoRestart that is stopped or disposed no longer restarts for a central that connected meanwhile, HciHost ends a connect attempt on any failed connection event, and characteristics found earlier keep working after services are discovered again.

Testing

  • Added unit tests for WinRT's bookkeeping, whole and split advertisements, address text, and a set the backend stopped.
  • Ran the hardware tests on Windows 11 against an nRF52840 dongle on HciHost: advertising and services work in both directions. CI only builds them.
  • Ran a hardware matrix on Windows against two dongles, in both roles. Checked rediscovery and the MTU after connecting, also with a peripheral that keeps MTU 23 or rejects the exchange.
  • Ran the radio tests on Windows. They are explicit, as they turn off the machine's Bluetooth.
  • Not covered: several centrals at once, pairing, Windows 10, and connecting on Windows to an address without its type.

Impact

  • WinBleFactory and AddWinRT(Action<WinBleFactory>) are replaced by AddWinRT().
  • Apps that use WinRT target net10.0-windows10.0.19041.0 or later. On other targets it offers no adapter.
  • IGapAdvertisement.AsReadOnlyMemory() is removed: read the properties, e.g. Data.
  • GapAdvertisement.FromExtendedAdvertisingReport(...) is renamed to GapAdvertisement.Create(...), and its overload that took report bytes is removed.
  • Code that uses the driver layer, AddBackend or GapAdvertisement.Create suppresses DARPBLE0001.
  • BleAddress.Parse rejects text with anything after the address.
  • On Windows, ConnectAsync takes up to 500 ms longer, while Windows exchanges the MTU. A peripheral that keeps MTU 23 waits the whole 500 ms, as Windows reports no exchange that leaves the MTU unchanged.

Checklist

  • Documentation (and changelog) is updated if needed
  • CI Pipeline is successful
  • (Human) Self-review was done

PR generated by claude-opus-5-5 on behalf of Ole

@OleRoss
OleRoss added this pull request to stack #161 October 24, 2026 11:56
@OleRoss OleRoss changed the title feat/v2 winrt feat!: Port WinRT to the driver layer Oct 24, 2026
@OleRoss

OleRoss commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

Comment generated by claude-opus-5-5 on behalf of Ole

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T09:00:25.001816Z b247d96 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Repo Coverage

Line Coverage Branch Coverage


PR Coverage

291/304 changed lines covered (Lines: 95.72%, Branches: 78.95%)

Darp.Ble.WinRT

🔴 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.WinRT/WinRTBackend.cs — 0/4 changed lines covered (Lines: 0%, Branches: n/a)
...
  8 ⬛ {
  9 🟥     public string Name => BleBackends.WinRT;
 10 ⬛ 
...
 15 ⬛     {
 16 🟥         cancellationToken.ThrowIfCancellationRequested();
 17 ⬛ #if WINDOWS
...
 20 ⬛ #endif
 21 🟥         context.LoggerFactory.CreateLogger<WinRTBackend>().LogNotOnWindows();
 22 🟥         return Task.FromResult<IReadOnlyList<IBleAdapterSource>>([]);
 23 ⬛     }
...
🔴 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.WinRT/WinRTBleManagerBuilderExtensions.cs — 0/2 changed lines covered (Lines: 0%, Branches: n/a)
...
 10 ⬛     {
 11 🟥         ArgumentNullException.ThrowIfNull(builder);
 12 🟥         return builder.AddBackend(new WinRTBackend());
 13 ⬛     }
...
🔴 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.WinRT/Connections/CentralSessions.cs — 28/29 changed lines covered (Lines: 96.55%, Branches: 91.67%)
...
 60 🟩         Connection? connection = _byDevice.Values.FirstOrDefault(c => ReferenceEquals(c.Central, central));
 61 🟨         if (connection is null)
 62 🟥             return false;
 63 🟩         _byDevice.Remove(connection.DeviceId);
...
🟠 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.WinRT/Gatt/SubscriptionSnapshots.cs — 16/16 changed lines covered (Lines: 100%, Branches: 80%)
...
 33 ⬛         lock (_lock)
 34 🟨             return _subscribed.GetValueOrDefault(characteristic)?.Contains(client) is true;
 35 🟩     }
...
✓ 🟢 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.WinRT/Connections/AttributeWrappers.cs — 26/26 changed lines covered
✓ 🟢 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.WinRT/Gap/PublisherSections.cs — 41/41 changed lines covered

Darp.Ble

🔴 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble/Data/BleAddress.cs — 37/39 changed lines covered (Lines: 94.87%, Branches: 90%)
...
 96 🟩         BleAddressType type = BleAddressType.NotAvailable;
 97 🟨         if (s.Length == 19 && s[1] == ':')
 98 ⬛         {
...
162 ⬛     public string ToString(string? format, IFormatProvider? formatProvider) =>
163 🟨         format switch
164 🟩         {
165 🟨             null or "" => ToString(),
166 🟩             "T" => Type switch
...
169 🟩                 BleAddressType.Random => $"R:{this}",
170 🟥                 _ => ToString(),
171 🟩             },
172 🟥             _ => throw new FormatException($"The format '{format}' is not supported; use \"T\" or none"),
173 🟩         };
...
🔴 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble/Gap/AdvertisingSet.cs — 9/11 changed lines covered (Lines: 81.82%, Branches: 50%)
...
 98 ⬛     public Task StopAsync(CancellationToken cancellationToken = default) =>
 99 🟥         RunAsync(() => StopCoreAsync(cancellationToken), cancellationToken);
100 ⬛ 
...
263 ⬛         {
264 🟨             if (!_isAdvertising)
265 🟥                 return;
266 🟩             _isAdvertising = false;
...
367 ⬛         {
368 🟨             _pendingRestart?.Cancel();
369 🟩             _pendingRestart = null;
...
🟠 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble/Gap/GapAdvertisement.cs — 50/50 changed lines covered (Lines: 100%, Branches: 50%)
...
116 ⬛     private bool Equals(GapAdvertisement other) =>
117 🟨         Timestamp.Equals(other.Timestamp)
118 🟩         && EventType == other.EventType
...
🟠 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble/Connections/ConnectionEvents.cs — 4/4 changed lines covered (Lines: 100%, Branches: 50%)
...
 38 🟩             connection = _connection;
 39 🟨         connection?.OnMtuChanged(mtu);
 40 🟩     }
...
🟠 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble/Gap/AdvertisementStreamExtensions.cs — 5/5 changed lines covered (Lines: 100%, Branches: 75%)
...
 20 ⬛         // An address without its type, e.g. parsed from text, matches its value of any type
 21 🟨         return address.Type is BleAddressType.NotAvailable
 22 🟩             ? source.Where(adv => adv.Address.Value == address.Value)
...
🟠 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble/Gap/AdvertisingSetEvents.cs — 1/1 changed lines covered (Lines: 100%, Branches: 50%)
...
 14 ⬛ 
 15 🟨     public void OnFailed(BleException error) => Volatile.Read(ref _set)?.OnFailed(error);
 16 ⬛ }
 17 ⬛ 
✓ 🟢 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble/Connections/ConnectedPeripheral.cs — 5/5 changed lines covered

Darp.Ble.HciHost

🔴 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.HciHost/Gap/HciHostObserverDriver.cs — 20/22 changed lines covered (Lines: 90.91%, Branches: 71.43%)
...
108 ⬛         // Anonymous advertisements have no address to connect to
109 🟨         if (type is BleAddressType.NotAvailable)
110 🟥             return;
111 🟨         if (_addressTypes.Count >= MaxKnownAddresses && !_addressTypes.ContainsKey(address))
112 🟥             _addressTypes.Clear();
113 🟩         _addressTypes[address] = type;
...
🟠 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.HciHost/Connections/ConnectAttempt.cs — 5/5 changed lines covered (Lines: 100%, Branches: 50%)
...
 52 ⬛     {
 53 🟨         if (_result.Task.IsCompleted)
 54 ⬛             return;
...
 73 ⬛         // A link a central made to our peripheral role, or one to another peer, is not this attempt's
 74 🟨         if (
 75 🟩             hciEvent.Role is not HciLeConnectionRole.Central
...
🟠 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.HciHost/Gap/AdvertisingReportAssembler.cs — 25/25 changed lines covered (Lines: 100%, Branches: 95.45%)
...
 35 🟩         bool continues = _pending.Remove(sender, out Pending earlier);
 36 🟨         txPower = continues && report.TxPower == TxPowerNotAvailable ? earlier.TxPower : report.TxPower;
 37 🟩         data = default;
...
✓ 🟢 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.HciHost/Connections/HciHostCentralDriver.cs — 12/12 changed lines covered
✓ 🟢 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.HciHost/HciHostAdapterDriver.cs — 1/1 changed lines covered

Darp.Ble.Mock

🟠 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.Mock/Gap/MockObserverDriver.cs — 1/1 changed lines covered (Lines: 100%, Branches: 62.5%)
...
 62 ⬛         bool isLegacy = eventType.HasFlag(BleEventType.Legacy);
 63 🟨         return GapAdvertisement.Create(
 64 ⬛             timestamp,
...
✓ 🟢 home/runner/work/Darp.Ble/Darp.Ble/src/Darp.Ble.Mock/MockAir.cs — 5/5 changed lines covered

Legend: 🟥 uncovered · 🟨 partial branch · 🟩 covered · ⬛ non-executable/blank
Generated by coverage-pr-comment

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e76e5d60db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +111 to +115
_data = data;
if (_publisher is null)
return;
await StopAsync(cancellationToken).ConfigureAwait(false);
await StartAsync(cancellationToken).ConfigureAwait(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve advertising state when a data-update restart fails

When updating a running non-connectable advertisement, cancellation or a publisher error after StopAsync succeeds leaves the publisher stopped, but AdvertisingSet.SetAdvertisingDataAsync never clears the core's _isAdvertising flag. Consequently, AdvertisingSet.StartAsync becomes a no-op even though nothing is being advertised, and its Data property still reports the old payload while this driver retains the new one. Make this restart failure-safe so the driver and core retain consistent advertising and payload state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1fb0de5. A data update now starts a publisher with the new data before it stops the running one. When starting fails or is cancelled, the set keeps advertising its old data and the driver keeps the old data too, so it matches the core's Data and IsAdvertising.

Comment generated by claude-opus-5-5 on behalf of Ole

Comment on lines +60 to +61
request.HandOffToken.ThrowIfCancellationRequested();
DeviceAccessStatus status = await _device.RequestAccessAsync().AsTask().ConfigureAwait(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor cancellation while waiting for device access

If Windows' device-access request remains pending, cancelling service discovery only cancels the caller's wait: this await ignores request.HandOffToken. Since discovery has not yet called OnTransactionSent, it continues holding both the request slot and send order in RequestQueue, with no ATT timeout running, so subsequent operations remain blocked until the access request completes. Make the access wait cancellable with the hand-off token; unlike a sent GATT transaction, this is still a pre-send wait.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1fb0de5. The wait for Windows' device-access answer now ends with request.HandOffToken; nothing has been sent at that point, so cancelling it frees the request queue.

Comment generated by claude-opus-5-5 on behalf of Ole

@OleRoss

OleRoss commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

Comment generated by claude-opus-5-5 on behalf of Ole

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c6920debc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +86 to +87
_publisher = null;
await StopPublisherAsync(publisher, cancellationToken).ConfigureAwait(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile advertising state before completing a cancelled stop

If cancellation occurs after publisher.Stop() but before the Stopped event, StopPublisherAsync throws even though Windows is stopping the advertisement. The publisher reference has already been cleared, while AdvertisingSet.StopCoreAsync retains its _isAdvertising flag because the driver call failed. A subsequent StartAsync therefore does nothing despite the advertisement having stopped. Once the native stop has been issued, reconcile its completion with the core's state rather than abandoning the wait and losing the publisher.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 46ca3e1. Stopping checks the token before it asks Windows to stop; once Stop() is issued, the wait for Stopped is no longer cancellable, so a stop either does not happen or completes, and the core's IsAdvertising stays true to the publisher.

Comment generated by claude-opus-5-5 on behalf of Ole

Comment on lines +33 to +34
_device.ConnectionStatusChanged += OnConnectionStatusChanged;
_session.SessionStatusChanged += OnSessionStatusChanged;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check connection status after attaching disconnect handlers

If the peripheral disconnects immediately after its session becomes active, the disconnect can occur before these handlers are attached. WinRTCentralDriver.ConnectAsync only waits for the first Active status, and this constructor never checks whether the device disconnected or the session closed in the meantime. Consequently, no OnDisconnected callback reaches the core, and its local-only MTU exchange can succeed and return an already-dead connection. Recheck the native statuses after subscribing and report the disconnect through the existing idempotent LinkDown path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 46ca3e1. After attaching its handlers, the connection checks the device and session status and reports a link that already went down through LinkDown. Checked on Windows 11 against the dongle: the connect tests still pass repeatedly.

Comment generated by claude-opus-5-5 on behalf of Ole

@OleRoss

OleRoss commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

Comment generated by claude-opus-5-5 on behalf of Ole

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@OleRoss
OleRoss force-pushed the feat/v2-winrt branch 2 times, most recently from 8c5a492 to faffcad Compare October 24, 2026 21:03
@OleRoss
OleRoss force-pushed the feat/v2-winrt branch 2 times, most recently from 683b3d2 to 31a007c Compare October 25, 2026 19:26
@OleRoss
OleRoss marked this pull request as ready for review October 29, 2026 22:00

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16c6dffc6f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +113 to +115
Task stopped = WaitForAsync(publisher, BluetoothLEAdvertisementPublisherStatus.Stopped, CancellationToken.None);
publisher.Stop();
await stopped.ConfigureAwait(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat an aborted publisher as stopped during shutdown

If Windows aborts the publisher while StopAsync is awaiting its terminal status, WaitForAsync throws even though advertising has ended. The driver has already cleared _publisher and detached OnRunningStatusChanged, so neither successful completion nor OnFailed clears the core's _isAdvertising flag; subsequent StartAsync calls become no-ops, including after the radio is restored. The cancellation fix does not cover this path: WaitForAsync still explicitly faults on Aborted. Treat that terminal status as a completed stop, or otherwise reconcile the core's state before propagating the error.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right: the stop ended in an exception although nothing advertised any more, so the core kept the set running and ignored the next start. Fixed in 50274d8: a publisher Windows aborts while the set waits for it to stop now counts as stopped. The start path still fails with the same transient BleException on an abort.

Ran the radio tests and stop/start cycles on Windows, plus attempts to turn the radio off while StopAsync runs. Every stop completed and the set advertised again after the radio came back on. None of the attempts caught the abort exactly during the stop, so that path is covered by reading the code only.

Comment generated by claude-opus-5-5 on behalf of Ole

v1's WinRT backend sat on base classes that v2 removed. It now implements the drivers for all four roles. Where Windows cannot meet the driver contract, the driver says so: a request counts as sent once its Windows call started, a write without response is handed off at once, a connection ends for this app while Windows may keep the link, a central becomes known through its first request, and advertising does not stop when a central connects.
The tests need Windows' radio and an nRF52840 dongle running HciHost on the same machine, so CI only builds them.
A set the radio stopped still counted as advertising, so StartAsync did nothing and the app could not get it advertising again.
When the Bluetooth radio turned off, scans hung without reports and advertising sets stopped without anyone noticing. Scans and connectable sets started while the radio is off waited silently, or forever.
An advertisement was stored as one HCI report, whose one-byte length cut extended data over 255 bytes, and HciHost delivered each part of an advertisement the controller split as an advertisement of its own. Every backend now reports the whole advertisement as values.
The driver interfaces are written for this library's backends and will gain members, e.g. backend features; a backend written elsewhere against 2.0 would break with them. Using them now reports DARPBLE0001.
…t it

An address saved as text and parsed back had lost its type, so it matched no scanned device and HciHost could not connect to it. The "T" format keeps the type, Parse reads it and rejects malformed text, and an address without a type matches and connects by its value: HciHost uses the type it last heard the address with.
Of a failed LE Connection Complete event only the status is valid. A failure whose role field read peripheral was ignored, so the attempt waited until its caller cancelled, holding the controller's only attempt and keeping scans paused.
A central that connected while a set with autoRestart was being disposed could still admit a restart, which disposal then waited for until the central disconnected.
StopAsync cancelled a pending restart before it marked the set as stopping, so a central connecting in between still admitted a restart, and the set advertised again once that central disconnected.
… again

Windows may return the service objects a connection already wraps; releasing
the duplicates closed them, so earlier characteristics threw ObjectDisposedException.
Windows exchanges the MTU itself, usually after the connection is handed out,
so writes stayed limited to 20 bytes. Backends now report a larger MTU through
IConnectionSink.OnMtuChanged.
Windows exchanges the MTU itself about 0.2 s after the session is active, so a connection handed out at once only allowed 20-byte writes. Windows reports only a larger MTU, so the wait is capped at 500 ms; an MTU that grows later still reaches the sink.
…t while stopping

The stop failed although nothing advertised any more, so the core kept the set running and ignored the next start.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a81b649b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +122 to +124
Task stopped = WaitForAsync(publisher, BluetoothLEAdvertisementPublisherStatus.Stopped, CancellationToken.None);
publisher.Stop();
await stopped.ConfigureAwait(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Subscribe before checking the publisher's terminal status

If the radio turns off after the publisher.Status check but before WaitForAsync attaches its handler, the publisher's Aborted event is lost. Calling Stop() on that already-aborted publisher need not produce another status change, so this non-cancellable wait can remain pending indefinitely, blocking the set's subsequent operations and disposal. Although the earlier abort-during-stop fix now accepts an observed Aborted event, this check-before-subscribe window still misses it entirely. Attach the handler before checking the current terminal status, and complete the wait if the publisher has already stopped or aborted.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right: the abort could come between the status check and the subscription, and nothing would complete the wait. Fixed in b247d96: StopPublisherAsync subscribes first, then reads the status, and completes on Stopped or Aborted.

Comment generated by claude-opus-5-5 on behalf of Ole

…ping

An abort between reading the status and subscribing was lost, and Windows need
not report another change for an aborted publisher, so the stop could hang.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant