Implement Substation: BLE-WS bridge with music-reactive e-stim
- 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
This commit is contained in:
+135
@@ -0,0 +1,135 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user