33using System . Text . Json ;
44using Windows . Devices . Bluetooth ;
55using Windows . Devices . Bluetooth . GenericAttributeProfile ;
6+ using Windows . Devices . Radios ;
67using Windows . Storage . Streams ;
78
89namespace SwitchifyBluetoothTransport ;
@@ -11,6 +12,7 @@ internal sealed class BluetoothGattBridge
1112{
1213 private const string ConnectionId = "ble" ;
1314 private static readonly TimeSpan DisconnectGracePeriod = TimeSpan . FromSeconds ( 10 ) ;
15+ private static readonly TimeSpan SystemStatusPollInterval = TimeSpan . FromSeconds ( 5 ) ;
1416
1517 private GattServiceProvider ? serviceProvider ;
1618 private GattLocalCharacteristic ? txCharacteristic ;
@@ -19,30 +21,79 @@ internal sealed class BluetoothGattBridge
1921 private string statusPayload = "{}" ;
2022 private bool connected ;
2123 private CancellationTokenSource ? disconnectGrace ;
24+ private StartCommand ? activeStartCommand ;
25+ private BluetoothAdapter ? currentAdapter ;
26+ private Radio ? currentRadio ;
27+ private CancellationTokenSource ? systemStatusPolling ;
28+ private string ? lastSystemStatusKey ;
29+ private bool restartInProgress ;
2230
2331 public async Task StartAsync ( StartCommand command )
2432 {
2533 Stop ( ) ;
34+ activeStartCommand = command ;
35+ var snapshot = await StartSystemStatusMonitoringAsync ( ) ;
36+ EmitSystemStatus ( snapshot , force : true ) ;
2637
27- var adapter = await BluetoothAdapter . GetDefaultAsync ( ) ;
28- if ( adapter is null )
38+ if ( ! snapshot . AdapterPresent )
2939 {
3040 HelperProtocol . WriteEvent ( new { type = "unavailable" , reason = "unsupported" } ) ;
3141 return ;
3242 }
3343
34- if ( ! adapter . IsLowEnergySupported )
44+ if ( snapshot . IsLowEnergySupported != true || snapshot . IsPeripheralRoleSupported != true )
3545 {
3646 HelperProtocol . WriteEvent ( new { type = "unavailable" , reason = "unsupported" } ) ;
3747 return ;
3848 }
3949
40- if ( ! adapter . IsPeripheralRoleSupported )
50+ if ( snapshot . RadioState is "off" or "disabled" )
51+ {
52+ HelperProtocol . WriteEvent ( new { type = "unavailable" , reason = "adapter_off" } ) ;
53+ return ;
54+ }
55+
56+ await StartAdvertisingAsync ( command , restarted : false ) ;
57+ }
58+
59+ public void Stop ( )
60+ {
61+ StopSystemStatusMonitoring ( ) ;
62+ activeStartCommand = null ;
63+ CancelDisconnectGrace ( ) ;
64+ if ( connected )
65+ {
66+ EmitDisconnected ( "helper_stopped" ) ;
67+ }
68+
69+ StopAdvertisingOnly ( ) ;
70+ }
71+
72+ public async Task SendAsync ( SendCommand command )
73+ {
74+ if ( command . ConnectionId != ConnectionId || txCharacteristic is null )
4175 {
42- HelperProtocol . WriteEvent ( new { type = "unavailable" , reason = "unsupported" } ) ;
4376 return ;
4477 }
4578
79+ var json = JsonSerializer . Serialize ( command . Frame , HelperProtocol . JsonOptions ) ;
80+ var bytes = Encoding . UTF8 . GetBytes ( json ) ;
81+ await txCharacteristic . NotifyValueAsync ( bytes . AsBuffer ( ) ) ;
82+ }
83+
84+ public void Disconnect ( string connectionId )
85+ {
86+ if ( connectionId != ConnectionId || ! connected )
87+ {
88+ return ;
89+ }
90+
91+ CancelDisconnectGrace ( ) ;
92+ EmitDisconnected ( "pc_requested" ) ;
93+ }
94+
95+ private async Task StartAdvertisingAsync ( StartCommand command , bool restarted )
96+ {
4697 statusPayload = JsonSerializer . Serialize (
4798 new
4899 {
@@ -80,46 +131,198 @@ public async Task StartAsync(StartCommand command)
80131 }
81132 ) ;
82133
83- HelperProtocol . WriteEvent ( new { type = "diagnostic" , @event = "advertising_started" } ) ;
134+ HelperProtocol . WriteEvent ( new { type = "diagnostic" , @event = restarted ? "advertising_restarted" : "advertising_started" } ) ;
84135 HelperProtocol . WriteEvent ( new { type = "ready" } ) ;
85136 }
86137
87- public void Stop ( )
138+ private void StopAdvertisingOnly ( )
88139 {
89- CancelDisconnectGrace ( ) ;
90- if ( connected )
91- {
92- EmitDisconnected ( "helper_stopped" ) ;
93- }
94-
95140 serviceProvider ? . StopAdvertising ( ) ;
96141 serviceProvider = null ;
97142 rxCharacteristic = null ;
98143 txCharacteristic = null ;
99144 statusCharacteristic = null ;
100145 }
101146
102- public async Task SendAsync ( SendCommand command )
147+ private async Task < AdapterSnapshot > StartSystemStatusMonitoringAsync ( )
103148 {
104- if ( command . ConnectionId != ConnectionId || txCharacteristic is null )
149+ StopSystemStatusMonitoring ( ) ;
150+ systemStatusPolling = new CancellationTokenSource ( ) ;
151+ var token = systemStatusPolling . Token ;
152+ var snapshot = await ReadAdapterSnapshotAsync ( ) ;
153+
154+ _ = Task . Run ( async ( ) =>
155+ {
156+ while ( ! token . IsCancellationRequested )
157+ {
158+ try
159+ {
160+ await Task . Delay ( SystemStatusPollInterval , token ) ;
161+ if ( token . IsCancellationRequested )
162+ {
163+ return ;
164+ }
165+
166+ var current = await ReadAdapterSnapshotAsync ( ) ;
167+ await HandleSystemStatusChangeAsync ( current ) ;
168+ }
169+ catch ( OperationCanceledException )
170+ {
171+ return ;
172+ }
173+ catch
174+ {
175+ var unknown = new AdapterSnapshot ( false , "unknown" , null , null ) ;
176+ await HandleSystemStatusChangeAsync ( unknown ) ;
177+ }
178+ }
179+ } , token ) ;
180+
181+ return snapshot ;
182+ }
183+
184+ private void StopSystemStatusMonitoring ( )
185+ {
186+ systemStatusPolling ? . Cancel ( ) ;
187+ systemStatusPolling ? . Dispose ( ) ;
188+ systemStatusPolling = null ;
189+ DetachRadioStateChanged ( ) ;
190+ currentAdapter = null ;
191+ lastSystemStatusKey = null ;
192+ }
193+
194+ private async Task < AdapterSnapshot > ReadAdapterSnapshotAsync ( )
195+ {
196+ try
197+ {
198+ var adapter = await BluetoothAdapter . GetDefaultAsync ( ) ;
199+ if ( adapter is null )
200+ {
201+ DetachRadioStateChanged ( ) ;
202+ currentAdapter = null ;
203+ return new AdapterSnapshot ( false , "unknown" , null , null ) ;
204+ }
205+
206+ currentAdapter = adapter ;
207+ var radio = await adapter . GetRadioAsync ( ) ;
208+ if ( ! ReferenceEquals ( currentRadio , radio ) )
209+ {
210+ DetachRadioStateChanged ( ) ;
211+ if ( radio is not null )
212+ {
213+ AttachRadioStateChanged ( radio ) ;
214+ }
215+ }
216+
217+ return new AdapterSnapshot (
218+ true ,
219+ RadioStateToProtocol ( radio ? . State ) ,
220+ adapter . IsLowEnergySupported ,
221+ adapter . IsPeripheralRoleSupported
222+ ) ;
223+ }
224+ catch
225+ {
226+ return new AdapterSnapshot ( false , "unknown" , null , null ) ;
227+ }
228+ }
229+
230+ private void AttachRadioStateChanged ( Radio radio )
231+ {
232+ currentRadio = radio ;
233+ currentRadio . StateChanged += OnRadioStateChanged ;
234+ }
235+
236+ private void DetachRadioStateChanged ( )
237+ {
238+ if ( currentRadio is not null )
239+ {
240+ currentRadio . StateChanged -= OnRadioStateChanged ;
241+ currentRadio = null ;
242+ }
243+ }
244+
245+ private async void OnRadioStateChanged ( Radio sender , object args )
246+ {
247+ var snapshot = await ReadAdapterSnapshotAsync ( ) ;
248+ await HandleSystemStatusChangeAsync ( snapshot ) ;
249+ }
250+
251+ private void EmitSystemStatus ( AdapterSnapshot snapshot , bool force = false )
252+ {
253+ var key = $ "{ snapshot . AdapterPresent } |{ snapshot . RadioState } |{ snapshot . IsLowEnergySupported } |{ snapshot . IsPeripheralRoleSupported } ";
254+ if ( ! force && key == lastSystemStatusKey )
105255 {
106256 return ;
107257 }
108258
109- var json = JsonSerializer . Serialize ( command . Frame , HelperProtocol . JsonOptions ) ;
110- var bytes = Encoding . UTF8 . GetBytes ( json ) ;
111- await txCharacteristic . NotifyValueAsync ( bytes . AsBuffer ( ) ) ;
259+ lastSystemStatusKey = key ;
260+ HelperProtocol . WriteEvent ( new
261+ {
262+ type = "systemStatus" ,
263+ adapterPresent = snapshot . AdapterPresent ,
264+ radioState = snapshot . RadioState ,
265+ isLowEnergySupported = snapshot . IsLowEnergySupported ,
266+ isPeripheralRoleSupported = snapshot . IsPeripheralRoleSupported
267+ } ) ;
112268 }
113269
114- public void Disconnect ( string connectionId )
270+ private async Task HandleSystemStatusChangeAsync ( AdapterSnapshot snapshot )
115271 {
116- if ( connectionId != ConnectionId || ! connected )
272+ var previousKey = lastSystemStatusKey ;
273+ EmitSystemStatus ( snapshot ) ;
274+ var changed = previousKey != lastSystemStatusKey ;
275+ if ( ! changed )
117276 {
118277 return ;
119278 }
120279
121- CancelDisconnectGrace ( ) ;
122- EmitDisconnected ( "pc_requested" ) ;
280+ if ( ! snapshot . AdapterPresent || snapshot . IsLowEnergySupported != true || snapshot . IsPeripheralRoleSupported != true )
281+ {
282+ if ( connected )
283+ {
284+ EmitDisconnected ( "adapter_off" ) ;
285+ }
286+ StopAdvertisingOnly ( ) ;
287+ HelperProtocol . WriteEvent ( new { type = "unavailable" , reason = "unsupported" } ) ;
288+ return ;
289+ }
290+
291+ if ( snapshot . RadioState is "off" or "disabled" )
292+ {
293+ HelperProtocol . WriteEvent ( new { type = "diagnostic" , @event = "system_radio_off" } ) ;
294+ if ( connected )
295+ {
296+ EmitDisconnected ( "adapter_off" ) ;
297+ }
298+ StopAdvertisingOnly ( ) ;
299+ HelperProtocol . WriteEvent ( new { type = "unavailable" , reason = "adapter_off" } ) ;
300+ return ;
301+ }
302+
303+ if ( snapshot . RadioState == "on" )
304+ {
305+ HelperProtocol . WriteEvent ( new { type = "diagnostic" , @event = "system_radio_on" } ) ;
306+ await RestartAdvertisingIfPossibleAsync ( ) ;
307+ }
308+ }
309+
310+ private async Task RestartAdvertisingIfPossibleAsync ( )
311+ {
312+ if ( restartInProgress || activeStartCommand is null || serviceProvider is not null )
313+ {
314+ return ;
315+ }
316+
317+ restartInProgress = true ;
318+ try
319+ {
320+ await StartAdvertisingAsync ( activeStartCommand , restarted : true ) ;
321+ }
322+ finally
323+ {
324+ restartInProgress = false ;
325+ }
123326 }
124327
125328 private async Task < GattLocalCharacteristic ? > CreateRxCharacteristicAsync ( Guid uuid )
@@ -337,4 +540,22 @@ private void EmitDisconnected(string reason)
337540 disconnectGrace = null ;
338541 HelperProtocol . WriteEvent ( new { type = "disconnected" , connectionId = ConnectionId , reason } ) ;
339542 }
543+
544+ private static string RadioStateToProtocol ( RadioState ? state )
545+ {
546+ return state switch
547+ {
548+ RadioState . On => "on" ,
549+ RadioState . Off => "off" ,
550+ RadioState . Disabled => "disabled" ,
551+ _ => "unknown"
552+ } ;
553+ }
554+
555+ private sealed record AdapterSnapshot (
556+ bool AdapterPresent ,
557+ string RadioState ,
558+ bool ? IsLowEnergySupported ,
559+ bool ? IsPeripheralRoleSupported
560+ ) ;
340561}
0 commit comments