using Windows.Devices.Bluetooth; using Windows.Devices.Bluetooth.GenericAttributeProfile; using Windows.Devices.Enumeration; using Windows.Storage.Streams; namespace Substation; public class CoyoteDevice : IDisposable { static readonly Guid ServiceUuid = Guid.Parse("0000180c-0000-1000-8000-00805f9b34fb"); static readonly Guid WriteCharUuid = Guid.Parse("0000150a-0000-1000-8000-00805f9b34fb"); static readonly Guid NotifyCharUuid= Guid.Parse("0000150b-0000-1000-8000-00805f9b34fb"); BluetoothLEDevice? _device; GattCharacteristic? _write; GattCharacteristic? _notify; public bool IsConnected => _write != null; public int StrengthA { get; private set; } public int StrengthB { get; private set; } public string DeviceName { get; private set; } = ""; public event Action? StrengthChanged; public event Action? ConnectionChanged; public async Task ConnectAsync(string nameFilter = "47L121000") { var selector = BluetoothLEDevice.GetDeviceSelectorFromDeviceName(nameFilter); var devices = await DeviceInformation.FindAllAsync(selector); if (devices.Count == 0) { selector = BluetoothLEDevice.GetDeviceSelectorFromPairingState(false); devices = await DeviceInformation.FindAllAsync(selector); var match = devices.FirstOrDefault(d => d.Name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase) || d.Name.Contains("DG-LAB", StringComparison.OrdinalIgnoreCase) || d.Name.Contains("Coyote", StringComparison.OrdinalIgnoreCase)); if (match == null) throw new InvalidOperationException( $"Coyote 3.0 not found (filter='{nameFilter}'). Ensure it's powered on and BLE is enabled."); // fallback path var fallback = await BluetoothLEDevice.FromIdAsync(match.Id); await ConnectDeviceAsync(fallback, match.Name); return; } var dev = await BluetoothLEDevice.FromIdAsync(devices[0].Id); await ConnectDeviceAsync(dev, devices[0].Name); } async Task ConnectDeviceAsync(BluetoothLEDevice dev, string name) { _device = dev; DeviceName = name; _device.ConnectionStatusChanged += OnConnectionStatusChanged; var svcResult = await dev.GetGattServicesForUuidAsync(ServiceUuid, BluetoothCacheMode.Uncached); if (svcResult.Status != GattCommunicationStatus.Success || svcResult.Services.Count == 0) throw new InvalidOperationException($"GATT service {ServiceUuid} not found on {name}."); var svc = svcResult.Services[0]; var charResult = await svc.GetCharacteristicsAsync(); if (charResult.Status != GattCommunicationStatus.Success) throw new InvalidOperationException($"Failed to enumerate characteristics on {name}."); _write = charResult.Characteristics.FirstOrDefault(c => c.Uuid == WriteCharUuid) ?? throw new InvalidOperationException("Write characteristic 0x150A not found."); _notify = charResult.Characteristics.FirstOrDefault(c => c.Uuid == NotifyCharUuid) ?? throw new InvalidOperationException("Notify characteristic 0x150B not found."); _notify.ValueChanged += OnNotify; var subStatus = await _notify.WriteClientCharacteristicConfigurationDescriptorAsync( GattClientCharacteristicConfigurationDescriptorValue.Notify); if (subStatus != GattCommunicationStatus.Success) throw new InvalidOperationException("Failed to subscribe to 0x150B notifications."); await SendBF(200, 200, 128, 128, 128, 128); ConnectionChanged?.Invoke(true); } void OnConnectionStatusChanged(BluetoothLEDevice sender, object args) { if (sender.ConnectionStatus == Windows.Devices.Bluetooth.BluetoothConnectionStatus.Disconnected) { _write = null; _notify = null; ConnectionChanged?.Invoke(false); } } void OnNotify(GattCharacteristic sender, GattValueChangedEventArgs args) { var reader = DataReader.FromBuffer(args.CharacteristicValue); if (reader.UnconsumedBufferLength < 1) return; var head = reader.ReadByte(); if (head == 0xB1 && reader.UnconsumedBufferLength >= 3) { reader.ReadByte(); StrengthA = reader.ReadByte(); StrengthB = reader.ReadByte(); StrengthChanged?.Invoke(StrengthA, StrengthB); } } public async Task SendB0(byte[] frame) { if (_write == null) return; var writer = new DataWriter(); writer.WriteBytes(frame); var status = await _write.WriteValueAsync(writer.DetachBuffer(), GattWriteOption.WriteWithoutResponse); if (status != GattCommunicationStatus.Success) Console.Error.WriteLine($"[BLE] B0 write failed: {status}"); } public async Task SendBF(byte capA, byte capB, byte freqBalA, byte freqBalB, byte intBalA, byte intBalB) { if (_write == null) return; var frame = BF.Build(capA, capB, freqBalA, freqBalB, intBalA, intBalB); var writer = new DataWriter(); writer.WriteBytes(frame); var status = await _write.WriteValueAsync(writer.DetachBuffer(), GattWriteOption.WriteWithoutResponse); if (status != GattCommunicationStatus.Success) Console.Error.WriteLine($"[BLE] BF write failed: {status}"); } public void Dispose() { if (_notify != null) _notify.ValueChanged -= OnNotify; _device?.Dispose(); } }