using System; using System.Diagnostics; using System.Runtime.InteropServices.WindowsRuntime; using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using Windows.Devices.Bluetooth; using Windows.Devices.Bluetooth.Advertisement; using Windows.Devices.Bluetooth.GenericAttributeProfile; using Windows.Storage.Streams; namespace KVMote.Transport { internal sealed class BleTransport : IKvmTransport { // Nordic UART Service (NUS) — convenção padrão para serial-over-BLE private static readonly Guid NusServiceUuid = Guid.Parse("6E400001-B5A3-F393-E0A9-E50E24DCCA9E"); private static readonly Guid NusRxUuid = Guid.Parse("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"); // PC escreve aqui private static readonly Guid NusTxUuid = Guid.Parse("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"); // PC lê (notify) private const string AdvertisedName = "KVMote"; private const int ScanTimeoutMs = 3000; private ulong _address; private BluetoothLEDevice? _device; private GattDeviceService? _service; private GattCharacteristic? _rxChar; private GattCharacteristic? _txChar; private GattWriteOption _writeOption; private bool _connected; private bool _disposed; private Channel? _sendChannel; private CancellationTokenSource? _sendCts; public string DeviceLabel => _address != 0 ? "KVMote (BLE)" : "—"; public bool IsConnected => _connected && _device?.ConnectionStatus == BluetoothConnectionStatus.Connected; public int ClipboardMaxChars => 1000; public int ClipboardDelayMs => 5; public event Action? DataReceived; // ── Detection ────────────────────────────────────────────────────────── public async Task DetectAsync(CancellationToken ct) { // Fast path: endereço já conhecido de conexão anterior — não precisa escanear if (_address != 0) return true; // Timeout interno para que o loop de retry em AutoDetectAsync funcione using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); timeoutCts.CancelAfter(ScanTimeoutMs); var token = timeoutCts.Token; var tcs = new TaskCompletionSource(); var watcher = new BluetoothLEAdvertisementWatcher { ScanningMode = BluetoothLEScanningMode.Active }; watcher.Received += (_, args) => { if (args.Advertisement.LocalName == AdvertisedName) tcs.TrySetResult(args.BluetoothAddress); }; watcher.Start(); try { _address = await tcs.Task.WaitAsync(token); return true; } catch { _address = 0; return false; } finally { watcher.Stop(); } } // ── Lifecycle ────────────────────────────────────────────────────────── public async Task ConnectAsync() { try { Log("BLE: FromBluetoothAddressAsync..."); _device = await BluetoothLEDevice.FromBluetoothAddressAsync(_address).AsTask(); if (_device is null) { Log("BLE: device null"); return false; } _device.ConnectionStatusChanged += OnConnectionStatusChanged; // Aguarda conexão física estabelecer (Windows pode demorar até 3s) Log("BLE: aguardando ConnectionStatus..."); for (int i = 0; i < 10; i++) { if (_device.ConnectionStatus == BluetoothConnectionStatus.Connected) break; await System.Threading.Tasks.Task.Delay(300); } Log($"BLE: ConnectionStatus={_device.ConnectionStatus}"); // Aguarda mais um momento após Connected — Windows ainda finaliza GATT anterior if (_device.ConnectionStatus == BluetoothConnectionStatus.Connected) await System.Threading.Tasks.Task.Delay(800); // Retry GATT discovery — busca todos os serviços e filtra manualmente // (GetGattServicesForUuidAsync falha com cache stale no Windows) for (int attempt = 1; attempt <= 4; attempt++) { Log($"BLE: GetGattServicesAsync (tentativa {attempt})..."); try { var all = await _device .GetGattServicesAsync(BluetoothCacheMode.Uncached) .AsTask(); Log($"BLE: GetGattServicesAsync status={all.Status} total={all.Services.Count}"); if (all.Status == GattCommunicationStatus.Success) { foreach (var s in all.Services) if (s.Uuid == NusServiceUuid) { _service = s; break; } if (_service is not null) { Log("BLE: NUS encontrado!"); break; } Log("BLE: NUS não encontrado na lista."); } } catch (Exception ex) { Log($"BLE: GATT tentativa {attempt} [{ex.GetType().Name}] hr=0x{ex.HResult:X8}"); } await System.Threading.Tasks.Task.Delay(1500); } if (_service is null) return false; Log("BLE: GetCharacteristics RX/TX..."); var rxResult = await _service.GetCharacteristicsForUuidAsync(NusRxUuid).AsTask(); var txResult = await _service.GetCharacteristicsForUuidAsync(NusTxUuid).AsTask(); Log($"BLE: rx={rxResult.Status}/{rxResult.Characteristics.Count} tx={txResult.Status}/{txResult.Characteristics.Count}"); // AccessDenied = Windows tem bond stale; desparea, reseta endereço e força // novo scan BLE na próxima tentativa (evita ciclo de AccessDenied infinito) if (rxResult.Status == GattCommunicationStatus.AccessDenied || txResult.Status == GattCommunicationStatus.AccessDenied) { Log("BLE: AccessDenied — despareando e resetando endereço..."); try { var devInfo = await Windows.Devices.Enumeration.DeviceInformation .CreateFromIdAsync(_device.DeviceId).AsTask(); if (devInfo.Pairing.IsPaired) { var r = await devInfo.Pairing.UnpairAsync().AsTask(); Log($"BLE: Unpair={r.Status}"); } } catch (Exception ex) { Log($"BLE: Unpair ex: {ex.Message}"); } _address = 0; // força novo scan BLE — evita reuso de bond stale do Windows Disconnect(); await System.Threading.Tasks.Task.Delay(2000); // aguarda Windows processar unpair return false; } if (rxResult.Status != GattCommunicationStatus.Success || rxResult.Characteristics.Count == 0) return false; if (txResult.Status != GattCommunicationStatus.Success || txResult.Characteristics.Count == 0) return false; _rxChar = rxResult.Characteristics[0]; _txChar = txResult.Characteristics[0]; // Prefere WriteWithoutResponse (mais rápido) se suportado _writeOption = _rxChar.CharacteristicProperties .HasFlag(GattCharacteristicProperties.WriteWithoutResponse) ? GattWriteOption.WriteWithoutResponse : GattWriteOption.WriteWithResponse; Log("BLE: Subscribing notify..."); // Inscreve em notificações para receber [PONG] var cccdStatus = await _txChar .WriteClientCharacteristicConfigurationDescriptorAsync( GattClientCharacteristicConfigurationDescriptorValue.Notify) .AsTask(); Log($"BLE: cccd={cccdStatus}"); if (cccdStatus != GattCommunicationStatus.Success) return false; _txChar.ValueChanged += OnValueChanged; _connected = true; StartSendLoop(); Log("BLE: Connected OK"); return true; } catch (Exception ex) { Log($"BLE: ConnectAsync exception [{ex.GetType().Name}] hr=0x{ex.HResult:X8}: {ex.Message}"); Disconnect(); return false; } } public void Disconnect() { _connected = false; StopSendLoop(); if (_txChar is not null) { try { _txChar.ValueChanged -= OnValueChanged; } catch { } _txChar = null; } _rxChar = null; if (_service is not null) { try { _service.Dispose(); } catch { } _service = null; } if (_device is not null) { _device.ConnectionStatusChanged -= OnConnectionStatusChanged; try { _device.Dispose(); } catch { } _device = null; } } // ── Send ─────────────────────────────────────────────────────────────── public void Send(byte[] data) => _sendChannel?.Writer.TryWrite(data); public void SendLossy(byte[] data) => _sendChannel?.Writer.TryWrite(data); // ── Send loop (Channel interno — mantém interface síncrona) ──────────── private void StartSendLoop() { _sendCts = new CancellationTokenSource(); _sendChannel = Channel.CreateBounded(new BoundedChannelOptions(64) { FullMode = BoundedChannelFullMode.DropOldest }); var ct = _sendCts.Token; _ = Task.Run(async () => { await foreach (var data in _sendChannel.Reader.ReadAllAsync(ct)) { if (_rxChar is null) break; try { await _rxChar.WriteValueAsync(data.AsBuffer(), _writeOption).AsTask(); } catch { break; } } }, ct); } private void StopSendLoop() { _sendChannel?.Writer.TryComplete(); _sendCts?.Cancel(); _sendCts = null; _sendChannel = null; } // ── Callbacks ────────────────────────────────────────────────────────── private void OnValueChanged(GattCharacteristic _, GattValueChangedEventArgs args) { var reader = DataReader.FromBuffer(args.CharacteristicValue); var bytes = new byte[args.CharacteristicValue.Length]; reader.ReadBytes(bytes); DataReceived?.Invoke(Encoding.ASCII.GetString(bytes)); } private void OnConnectionStatusChanged(BluetoothLEDevice _, object __) { if (_device?.ConnectionStatus == BluetoothConnectionStatus.Disconnected) _connected = false; } private static readonly string LogFile = System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "kvmote_ble.log"); private static void Log(string msg) { string line = $"[BLE {DateTime.Now:HH:mm:ss.fff}] {msg}"; Debug.WriteLine(line); try { System.IO.File.AppendAllText(LogFile, line + "\n"); } catch { } } public void Dispose() { if (_disposed) return; _disposed = true; Disconnect(); } } }