using System.Collections.ObjectModel; using Windows.Devices.Enumeration; using Windows.Devices.Bluetooth; using Windows.Devices.Bluetooth.GenericAttributeProfile; using System.Runtime.InteropServices.WindowsRuntime; using System.Text; using Windows.Devices.Bluetooth.Advertisement; using Windows.Storage.Streams; using InABox.Core; namespace BluetoothLENet { public class BLECharacteristic(GattCharacteristic native) : IDisposable { public GattCharacteristic? Native { get; private set; } = native; public async Task ReadAsync() { var result = await Native?.ReadValueAsync(BluetoothCacheMode.Uncached); if (result.Status == GattCommunicationStatus.Success) return result.Value.ToArray(); return null; } public async Task WriteAsync(byte[] data) { using var writer = new DataWriter(); writer.ByteOrder = ByteOrder.LittleEndian; writer.WriteBytes(data); var buffer = writer.DetachBuffer(); var result = await Native?.WriteValueWithResultAsync(buffer); return result.Status == GattCommunicationStatus.Success; } public void Dispose() { Native = null; } } public class BLEService(GattDeviceService native) : IDisposable { public GattDeviceService? Native { get; private set; } = native; public ObservableCollection Characteristics { get; } = new(); public void Dispose() { Characteristics.Clear(); Native?.Dispose(); } } public enum BLEDeviceStatus { Connected, Disconnected } public class BLEDevice: IDisposable { public BluetoothLEDevice? Native { get; private set; } public Guid[] AvailableServices { get; set; } = []; public byte[]? ManufacturerData { get; set; } public ObservableCollection Services { get; } = new(); public DateTime LastSeen { get; set; } public string MacAddress => ParseMacAddress(Native?.BluetoothAddress ?? 0); public BLEDevice(BluetoothLEDevice native, Guid[] availableServices) { Native = native; AvailableServices = availableServices; BetterScanner.StartScanner(0,29,29); } public BLEDeviceStatus Status => Services.Any() ? BLEDeviceStatus.Connected : BLEDeviceStatus.Disconnected; private string ParseMacAddress(ulong bluetoothAddress) { var macWithoutColons = bluetoothAddress.ToString("x").PadLeft(12, '0'); string macWithColons = ""; for (int i = 0; i < 6; i++) { if (!string.IsNullOrEmpty(macWithColons)) macWithColons += ":"; macWithColons += macWithoutColons.Substring(i * 2, 2).ToUpper(); } return macWithColons; } public void Dispose() { foreach (var service in Services) service.Dispose(); Services.Clear(); Native?.Dispose(); Native = null; } } public partial class BLE { public CoreObservableCollection Devices { get; } = new(); private readonly BluetoothLEAdvertisementWatcher _scanner; private readonly object lockobject = new(); public BLE() { Task.Run(() => { while (true) { lock (lockobject) { try { var devices = Devices.ToArray(); foreach (var device in devices) { if (device.LastSeen < DateTime.Now.Subtract(new TimeSpan(0, 0, 15))) { Console.WriteLine($"BLE:Clearing Stale Device {device?.Native?.Name ?? "Unknown"}"); DeviceStatuses.Remove(device?.Native?.BluetoothAddress ?? 0); Devices.Remove(device); } } //var stale = devices.Where(x => ).ToArray(); //if (stale.Any()) // Devices.RemoveRange(stale); } catch (Exception e) { } } Task.Delay(500); } }); Devices.CollectionChanged += (sender, args) => Changed?.Invoke(this, EventArgs.Empty); _scanner = new BluetoothLEAdvertisementWatcher() { ScanningMode = BluetoothLEScanningMode.Active, AllowExtendedAdvertisements = true }; _scanner.Stopped += ScannerStopped; _scanner.Received += (sender, args) => DoReceived(sender,args); } // private void CheckForStaleDevices(object? sender, EventArgs e) // { // foreach (var device in Devices.ToArray()) // { // // } // } private TaskCompletionSource stopTask = null; private void ScannerStopped(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementWatcherStoppedEventArgs args) { stopTask?.TrySetResult(true); } public event EventHandler Changed; private Guid? _serviceUUID; public async Task StartScanningAsync(Guid serviceUuid) { await StopScanningAsync(); _serviceUUID = serviceUuid; Devices.Clear(); //_scanner.AdvertisementFilter.Advertisement.ManufacturerData.Add(new BluetoothLEManufacturerData() { CompanyId = 0xFFFF }); // _scanner.AdvertisementFilter.Advertisement.ServiceUuids.Clear(); // _scanner.AdvertisementFilter.Advertisement.ServiceUuids.Add(serviceUuid); ; _scanner.Start(); return true; } public async Task StopScanningAsync() { if (_scanner.Status == BluetoothLEAdvertisementWatcherStatus.Started) { TaskCompletionSource stopTask = new(); _scanner.Stop(); await stopTask.Task; } _serviceUUID = null; return true; } public Dictionary DeviceStatuses = new(); private void DoReceived(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args) { bool bChanged = false; byte[]? manufacturerData = DeviceStatuses.GetValueOrDefault(args.BluetoothAddress); Guid[]? serviceUuids = null; if (args.Advertisement.ManufacturerData.Any()) { manufacturerData = args.Advertisement.ManufacturerData.LastOrDefault()?.Data.ToArray(); //Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Found Manufacturer Data [{manufacturerData?.Length ?? -1}]"); DeviceStatuses[args.BluetoothAddress] = manufacturerData; } var bledevice = Devices.FirstOrDefault(x => Equals(x.Native?.BluetoothAddress, args.BluetoothAddress)); if (bledevice != null) { Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Found Device - Updating Manufacturer Data [{{manufacturerData?.Length ?? -1}}]"); bledevice.ManufacturerData = DeviceStatuses.GetValueOrDefault(args.BluetoothAddress); bledevice.LastSeen = DateTime.Now; Changed?.Invoke(this, EventArgs.Empty); return; } serviceUuids = args.Advertisement.ServiceUuids.ToArray(); if (_serviceUUID == null || !serviceUuids.Contains(_serviceUUID.Value)) return; serviceUuids = serviceUuids.Where(x => x != _serviceUUID.Value).ToArray(); Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Found Advertisement [{string.Join(", ",serviceUuids)}]"); TaskCompletionSource tcs = new TaskCompletionSource(); Task.Run(async () => { var result = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress); tcs.SetResult(result); }); var device = tcs.Task.Result; if (device == null) return; Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Creating New Device"); bledevice = new BLEDevice(device, serviceUuids.Where(x => x != _serviceUUID).ToArray()) { LastSeen = args.Timestamp.DateTime, ManufacturerData = DeviceStatuses.GetValueOrDefault(args.BluetoothAddress) }; Devices.RemoveAll(x => x.Native == null); if (Devices.Any(x => string.Equals(x.MacAddress, bledevice.MacAddress))) { Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Duplicate MAC Address [{bledevice.MacAddress}] - skipping"); return; } Devices.Add(bledevice); Changed?.Invoke(this, EventArgs.Empty); // string? name = null; // List serviceUUIDs = new(); // byte[]? mfgData = null; // foreach (var dataSection in args.Advertisement.DataSections) // { // if (dataSection.DataType == 0x09) // { // var namebytes = new byte[dataSection.Data.Length]; // using (var reader = DataReader.FromBuffer(dataSection.Data)) // reader.ReadBytes(namebytes); // name = Encoding.ASCII.GetString(namebytes); // } // else if (dataSection.DataType == 0x07) // { // byte[] Guid_A = new byte[4]; // byte[] Guid_B = new byte[2]; // byte[] Guid_C = new byte[2]; // byte[] Guid_D = new byte[8]; // using (var reader = DataReader.FromBuffer(dataSection.Data)) // { // reader.ReadBytes(Guid_A); // reader.ReadBytes(Guid_B); // reader.ReadBytes(Guid_C); // reader.ReadBytes(Guid_D); // } // Guid serviceid = new Guid( // BitConverter.ToInt32(Guid_A.Reverse().ToArray(), 0), // BitConverter.ToInt16(Guid_B.Reverse().ToArray(), 0), // BitConverter.ToInt16(Guid_C.Reverse().ToArray(), 0), // Guid_D); // serviceUUIDs.Add(serviceid); // } // else if (dataSection.DataType == 0x05) // { // var uuid32bytes = new byte[dataSection.Data.Length]; // using (var reader = DataReader.FromBuffer(dataSection.Data)) // reader.ReadBytes(uuid32bytes); // var uuid32int = BitConverter.ToInt32(uuid32bytes.ToArray(), 0); // var uuid32string = $"{uuid32int:X}-0000-1000-8000-00805F9B34FB"; // serviceUUIDs.Add(Guid.Parse(uuid32string)); // } // else if (dataSection.DataType == 0xFF) // { // mfgData = new byte[dataSection.Data.Length]; // using (var reader = DataReader.FromBuffer(dataSection.Data)) // reader.ReadBytes(mfgData); // } // } } /// /// Connect to the specific device by name or number, and make this device current /// /// /// public async Task Connect(BLEDevice device) { try { var result = await device.Native.GetGattServicesAsync(BluetoothCacheMode.Uncached); if (result.Status == GattCommunicationStatus.Success) { foreach (var t in result.Services) { var service = new BLEService(t); var accessStatus = await service.Native.RequestAccessAsync(); if (accessStatus == DeviceAccessStatus.Allowed) { // BT_Code: Get all the child characteristics of a service. Use the cache mode to specify uncached characterstics only // and the new Async functions to get the characteristics of unpaired devices as well. var getCharacteristicResult = await service.Native.GetCharacteristicsAsync(BluetoothCacheMode.Uncached); if (getCharacteristicResult.Status == GattCommunicationStatus.Success) { foreach (var characteristic in getCharacteristicResult.Characteristics) service.Characteristics.Add(new BLECharacteristic(characteristic)); } device.Services.Add(service); } } } return result.Status == GattCommunicationStatus.Success ? ConnectDeviceResult.Ok : ConnectDeviceResult.Unreachable;; } catch (Exception e) { return ConnectDeviceResult.Error; } } /// /// Disconnect current device and clear list of services and characteristics /// public void Disconnect(BLEDevice? device) { device?.Dispose(); device = null; } } }