44421a51e3
- Renamed from CoyoteBridge to Substation (namespace, classes, UI) - BLE connection via WinRT with graceful disconnect handling - WebSocket server (127.0.0.1:8765) with single-client, push events for BLE connect/disconnect transitions - Operator strength limiter (clamp/scale modes, default 30) - Tray icon with 3 states: neutral/active/hot (runtime-drawn voltage glyph) - A/B strength gauges, hide-on-minimise to tray - Music mode: MP3 decode + FFT analysis (NAudio + FftSharp), rhythm→ch A, melody→ch B, pre-analyzed with synced audio playback - Test/Stop All work without BLE connected (dev mode) - WS device-driving commands error when BLE not connected; ping/status/ connect always work - TreatWarningsAsErrors, LangVersion=latest - .gitignore, README with full API docs + attribution
136 lines
5.6 KiB
C#
136 lines
5.6 KiB
C#
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<int, int>? StrengthChanged;
|
|
public event Action<bool>? 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("47L121000", 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();
|
|
}
|
|
}
|