BLENet.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. using System.Collections.ObjectModel;
  2. using Windows.Devices.Enumeration;
  3. using Windows.Devices.Bluetooth;
  4. using Windows.Devices.Bluetooth.GenericAttributeProfile;
  5. using System.Runtime.InteropServices.WindowsRuntime;
  6. using System.Text;
  7. using Windows.Devices.Bluetooth.Advertisement;
  8. using Windows.Storage.Streams;
  9. using InABox.Core;
  10. namespace BluetoothLENet
  11. {
  12. public class BLECharacteristic(GattCharacteristic native) : IDisposable
  13. {
  14. public GattCharacteristic? Native { get; private set; } = native;
  15. public async Task<byte[]?> ReadAsync()
  16. {
  17. var result = await Native?.ReadValueAsync(BluetoothCacheMode.Uncached);
  18. if (result.Status == GattCommunicationStatus.Success)
  19. return result.Value.ToArray();
  20. return null;
  21. }
  22. public async Task<bool> WriteAsync(byte[] data)
  23. {
  24. using var writer = new DataWriter();
  25. writer.ByteOrder = ByteOrder.LittleEndian;
  26. writer.WriteBytes(data);
  27. var buffer = writer.DetachBuffer();
  28. var result = await Native?.WriteValueWithResultAsync(buffer);
  29. return result.Status == GattCommunicationStatus.Success;
  30. }
  31. public void Dispose()
  32. {
  33. Native = null;
  34. }
  35. }
  36. public class BLEService(GattDeviceService native) : IDisposable
  37. {
  38. public GattDeviceService? Native { get; private set; } = native;
  39. public ObservableCollection<BLECharacteristic> Characteristics { get; } = new();
  40. public void Dispose()
  41. {
  42. Characteristics.Clear();
  43. Native?.Dispose();
  44. }
  45. }
  46. public enum BLEDeviceStatus
  47. {
  48. Connected,
  49. Disconnected
  50. }
  51. public class BLEDevice: IDisposable
  52. {
  53. public BluetoothLEDevice? Native { get; private set; }
  54. public Guid[] AvailableServices { get; set; } = [];
  55. public byte[]? ManufacturerData { get; set; }
  56. public ObservableCollection<BLEService> Services { get; } = new();
  57. public DateTime LastSeen { get; set; }
  58. public string MacAddress => ParseMacAddress(Native?.BluetoothAddress ?? 0);
  59. public BLEDevice(BluetoothLEDevice native, Guid[] availableServices)
  60. {
  61. Native = native;
  62. AvailableServices = availableServices;
  63. BetterScanner.StartScanner(0,29,29);
  64. }
  65. public BLEDeviceStatus Status => Services.Any()
  66. ? BLEDeviceStatus.Connected
  67. : BLEDeviceStatus.Disconnected;
  68. private string ParseMacAddress(ulong bluetoothAddress)
  69. {
  70. var macWithoutColons = bluetoothAddress.ToString("x").PadLeft(12, '0');
  71. string macWithColons = "";
  72. for (int i = 0; i < 6; i++)
  73. {
  74. if (!string.IsNullOrEmpty(macWithColons))
  75. macWithColons += ":";
  76. macWithColons += macWithoutColons.Substring(i * 2, 2).ToUpper();
  77. }
  78. return macWithColons;
  79. }
  80. public void Dispose()
  81. {
  82. foreach (var service in Services)
  83. service.Dispose();
  84. Services.Clear();
  85. Native?.Dispose();
  86. Native = null;
  87. }
  88. }
  89. public partial class BLE
  90. {
  91. public CoreObservableCollection<BLEDevice> Devices { get; } = new();
  92. private readonly BluetoothLEAdvertisementWatcher _scanner;
  93. private readonly object lockobject = new();
  94. public BLE()
  95. {
  96. Task.Run(() =>
  97. {
  98. while (true)
  99. {
  100. lock (lockobject)
  101. {
  102. try
  103. {
  104. var devices = Devices.ToArray();
  105. foreach (var device in devices)
  106. {
  107. if (device.LastSeen < DateTime.Now.Subtract(new TimeSpan(0, 0, 15)))
  108. {
  109. Console.WriteLine($"BLE:Clearing Stale Device {device?.Native?.Name ?? "Unknown"}");
  110. DeviceStatuses.Remove(device?.Native?.BluetoothAddress ?? 0);
  111. Devices.Remove(device);
  112. }
  113. }
  114. //var stale = devices.Where(x => ).ToArray();
  115. //if (stale.Any())
  116. // Devices.RemoveRange(stale);
  117. }
  118. catch (Exception e)
  119. {
  120. }
  121. }
  122. Task.Delay(500);
  123. }
  124. });
  125. Devices.CollectionChanged += (sender, args) => Changed?.Invoke(this, EventArgs.Empty);
  126. _scanner = new BluetoothLEAdvertisementWatcher()
  127. {
  128. ScanningMode = BluetoothLEScanningMode.Active,
  129. AllowExtendedAdvertisements = true
  130. };
  131. _scanner.Stopped += ScannerStopped;
  132. _scanner.Received += (sender, args) => DoReceived(sender,args);
  133. }
  134. // private void CheckForStaleDevices(object? sender, EventArgs e)
  135. // {
  136. // foreach (var device in Devices.ToArray())
  137. // {
  138. //
  139. // }
  140. // }
  141. private TaskCompletionSource<bool> stopTask = null;
  142. private void ScannerStopped(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementWatcherStoppedEventArgs args)
  143. {
  144. stopTask?.TrySetResult(true);
  145. }
  146. public event EventHandler Changed;
  147. private Guid? _serviceUUID;
  148. public async Task<bool> StartScanningAsync(Guid serviceUuid)
  149. {
  150. await StopScanningAsync();
  151. _serviceUUID = serviceUuid;
  152. Devices.Clear();
  153. //_scanner.AdvertisementFilter.Advertisement.ManufacturerData.Add(new BluetoothLEManufacturerData() { CompanyId = 0xFFFF });
  154. // _scanner.AdvertisementFilter.Advertisement.ServiceUuids.Clear();
  155. // _scanner.AdvertisementFilter.Advertisement.ServiceUuids.Add(serviceUuid); ;
  156. _scanner.Start();
  157. return true;
  158. }
  159. public async Task<bool> StopScanningAsync()
  160. {
  161. if (_scanner.Status == BluetoothLEAdvertisementWatcherStatus.Started)
  162. {
  163. TaskCompletionSource<bool> stopTask = new();
  164. _scanner.Stop();
  165. await stopTask.Task;
  166. }
  167. _serviceUUID = null;
  168. return true;
  169. }
  170. public Dictionary<ulong, byte[]?> DeviceStatuses = new();
  171. private void DoReceived(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
  172. {
  173. bool bChanged = false;
  174. byte[]? manufacturerData = DeviceStatuses.GetValueOrDefault(args.BluetoothAddress);
  175. Guid[]? serviceUuids = null;
  176. if (args.Advertisement.ManufacturerData.Any())
  177. {
  178. manufacturerData = args.Advertisement.ManufacturerData.LastOrDefault()?.Data.ToArray();
  179. //Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Found Manufacturer Data [{manufacturerData?.Length ?? -1}]");
  180. DeviceStatuses[args.BluetoothAddress] = manufacturerData;
  181. }
  182. var bledevice = Devices.FirstOrDefault(x => Equals(x.Native?.BluetoothAddress, args.BluetoothAddress));
  183. if (bledevice != null)
  184. {
  185. Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Found Device - Updating Manufacturer Data [{{manufacturerData?.Length ?? -1}}]");
  186. bledevice.ManufacturerData = DeviceStatuses.GetValueOrDefault(args.BluetoothAddress);
  187. bledevice.LastSeen = DateTime.Now;
  188. Changed?.Invoke(this, EventArgs.Empty);
  189. return;
  190. }
  191. serviceUuids = args.Advertisement.ServiceUuids.ToArray();
  192. if (_serviceUUID == null || !serviceUuids.Contains(_serviceUUID.Value))
  193. return;
  194. serviceUuids = serviceUuids.Where(x => x != _serviceUUID.Value).ToArray();
  195. Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Found Advertisement [{string.Join(", ",serviceUuids)}]");
  196. TaskCompletionSource<BluetoothLEDevice?> tcs = new TaskCompletionSource<BluetoothLEDevice?>();
  197. Task.Run(async () =>
  198. {
  199. var result = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
  200. tcs.SetResult(result);
  201. });
  202. var device = tcs.Task.Result;
  203. if (device == null)
  204. return;
  205. Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Creating New Device");
  206. bledevice = new BLEDevice(device, serviceUuids.Where(x => x != _serviceUUID).ToArray())
  207. {
  208. LastSeen = args.Timestamp.DateTime,
  209. ManufacturerData = DeviceStatuses.GetValueOrDefault(args.BluetoothAddress)
  210. };
  211. Devices.RemoveAll(x => x.Native == null);
  212. if (Devices.Any(x => string.Equals(x.MacAddress, bledevice.MacAddress)))
  213. {
  214. Console.WriteLine($"{DateTime.Now:hh:mm:ss} {args.BluetoothAddress} Duplicate MAC Address [{bledevice.MacAddress}] - skipping");
  215. return;
  216. }
  217. Devices.Add(bledevice);
  218. Changed?.Invoke(this, EventArgs.Empty);
  219. // string? name = null;
  220. // List<Guid> serviceUUIDs = new();
  221. // byte[]? mfgData = null;
  222. // foreach (var dataSection in args.Advertisement.DataSections)
  223. // {
  224. // if (dataSection.DataType == 0x09)
  225. // {
  226. // var namebytes = new byte[dataSection.Data.Length];
  227. // using (var reader = DataReader.FromBuffer(dataSection.Data))
  228. // reader.ReadBytes(namebytes);
  229. // name = Encoding.ASCII.GetString(namebytes);
  230. // }
  231. // else if (dataSection.DataType == 0x07)
  232. // {
  233. // byte[] Guid_A = new byte[4];
  234. // byte[] Guid_B = new byte[2];
  235. // byte[] Guid_C = new byte[2];
  236. // byte[] Guid_D = new byte[8];
  237. // using (var reader = DataReader.FromBuffer(dataSection.Data))
  238. // {
  239. // reader.ReadBytes(Guid_A);
  240. // reader.ReadBytes(Guid_B);
  241. // reader.ReadBytes(Guid_C);
  242. // reader.ReadBytes(Guid_D);
  243. // }
  244. // Guid serviceid = new Guid(
  245. // BitConverter.ToInt32(Guid_A.Reverse().ToArray(), 0),
  246. // BitConverter.ToInt16(Guid_B.Reverse().ToArray(), 0),
  247. // BitConverter.ToInt16(Guid_C.Reverse().ToArray(), 0),
  248. // Guid_D);
  249. // serviceUUIDs.Add(serviceid);
  250. // }
  251. // else if (dataSection.DataType == 0x05)
  252. // {
  253. // var uuid32bytes = new byte[dataSection.Data.Length];
  254. // using (var reader = DataReader.FromBuffer(dataSection.Data))
  255. // reader.ReadBytes(uuid32bytes);
  256. // var uuid32int = BitConverter.ToInt32(uuid32bytes.ToArray(), 0);
  257. // var uuid32string = $"{uuid32int:X}-0000-1000-8000-00805F9B34FB";
  258. // serviceUUIDs.Add(Guid.Parse(uuid32string));
  259. // }
  260. // else if (dataSection.DataType == 0xFF)
  261. // {
  262. // mfgData = new byte[dataSection.Data.Length];
  263. // using (var reader = DataReader.FromBuffer(dataSection.Data))
  264. // reader.ReadBytes(mfgData);
  265. // }
  266. // }
  267. }
  268. /// <summary>
  269. /// Connect to the specific device by name or number, and make this device current
  270. /// </summary>
  271. /// <param name="device"></param>
  272. /// <returns></returns>
  273. public async Task<ConnectDeviceResult> Connect(BLEDevice device)
  274. {
  275. try
  276. {
  277. var result = await device.Native.GetGattServicesAsync(BluetoothCacheMode.Uncached);
  278. if (result.Status == GattCommunicationStatus.Success)
  279. {
  280. foreach (var t in result.Services)
  281. {
  282. var service = new BLEService(t);
  283. var accessStatus = await service.Native.RequestAccessAsync();
  284. if (accessStatus == DeviceAccessStatus.Allowed)
  285. {
  286. // BT_Code: Get all the child characteristics of a service. Use the cache mode to specify uncached characterstics only
  287. // and the new Async functions to get the characteristics of unpaired devices as well.
  288. var getCharacteristicResult =
  289. await service.Native.GetCharacteristicsAsync(BluetoothCacheMode.Uncached);
  290. if (getCharacteristicResult.Status == GattCommunicationStatus.Success)
  291. {
  292. foreach (var characteristic in getCharacteristicResult.Characteristics)
  293. service.Characteristics.Add(new BLECharacteristic(characteristic));
  294. }
  295. device.Services.Add(service);
  296. }
  297. }
  298. }
  299. return result.Status == GattCommunicationStatus.Success ? ConnectDeviceResult.Ok : ConnectDeviceResult.Unreachable;;
  300. }
  301. catch (Exception e)
  302. {
  303. return ConnectDeviceResult.Error;
  304. }
  305. }
  306. /// <summary>
  307. /// Disconnect current device and clear list of services and characteristics
  308. /// </summary>
  309. public void Disconnect(BLEDevice? device)
  310. {
  311. device?.Dispose();
  312. device = null;
  313. }
  314. }
  315. }