From 03a55084dc543a0fdf81748e37de2cde8dec7788 Mon Sep 17 00:00:00 2001 From: Chris Sidi Date: Sun, 30 Jun 2019 09:25:10 -0400 Subject: [PATCH 1/7] Start adding C# events --- demo/printDeviceInfoEventDriven/Program.cs | 75 ++++++++++++ .../printDeviceInfoEventDriven.csproj | 12 ++ demo/scan/Program.cs | 4 +- events_todo.md | 41 +++++++ src/Adapter.cs | 96 +++++++++++++++ src/BlueZManager.cs | 10 +- src/Device.cs | 114 ++++++++++++++++++ src/Extensions.cs | 37 +++--- 8 files changed, 361 insertions(+), 28 deletions(-) create mode 100644 demo/printDeviceInfoEventDriven/Program.cs create mode 100644 demo/printDeviceInfoEventDriven/printDeviceInfoEventDriven.csproj create mode 100644 events_todo.md create mode 100644 src/Adapter.cs create mode 100644 src/Device.cs diff --git a/demo/printDeviceInfoEventDriven/Program.cs b/demo/printDeviceInfoEventDriven/Program.cs new file mode 100644 index 0000000..a8460e3 --- /dev/null +++ b/demo/printDeviceInfoEventDriven/Program.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using HashtagChris.DotNetBlueZ; +using HashtagChris.DotNetBlueZ.Extensions; + +namespace printDeviceInfoEventDriven +{ + class Program + { + static async Task Main(string[] args) + { + if (args.Length < 1) + { + Console.WriteLine("Usage: PrintDeviceInfo [adapterName]"); + Console.WriteLine("Example: PrintDeviceInfo AA:BB:CC:11:22:33 hci1"); + return; + } + + var deviceAddress = args[0]; + + IAdapter1 adapter; + if (args.Length > 1) + { + adapter = await BlueZManager.GetAdapterAsync(args[1]); + } + else + { + var adapters = await BlueZManager.GetAdaptersAsync(); + if (adapters.Length == 0) + { + throw new Exception("No Bluetooth adapters found."); + } + + adapter = adapters.First(); + } + + var adapterPath = adapter.ObjectPath.ToString(); + var adapterName = adapterPath.Substring(adapterPath.LastIndexOf("/") + 1); + Console.WriteLine($"Using Bluetooth adapter {adapterName}"); + + // Find the Bluetooth peripheral. + Device device = await adapter.GetDeviceAsync(deviceAddress); + if (device == null) + { + Console.WriteLine($"Bluetooth peripheral with address '{deviceAddress}' not found. Use `bluetoothctl` or Bluetooth Manager to scan and possibly pair first."); + return; + } + + device.Connected += device_Connected; + device.ServicesResolved += device_ServicesResolved; + + Console.WriteLine("Connecting..."); + await device.ConnectAsync(); + } + + static async void device_Connected(Object sender, EventArgs e) + { + var dev = (Device)sender; + Console.WriteLine($"Connected to {await dev.GetAddressAsync()}"); + } + + static async void device_Disonnected(Object sender, EventArgs e) + { + var dev = (Device)sender; + Console.WriteLine($"Disconnected from {await dev.GetAddressAsync()}"); + } + + static async void device_ServicesResolved(Object sender, EventArgs e) + { + var dev = (Device)sender; + Console.WriteLine($"Services resolved for {await dev.GetAddressAsync()}"); + } + } +} diff --git a/demo/printDeviceInfoEventDriven/printDeviceInfoEventDriven.csproj b/demo/printDeviceInfoEventDriven/printDeviceInfoEventDriven.csproj new file mode 100644 index 0000000..d2d9bdd --- /dev/null +++ b/demo/printDeviceInfoEventDriven/printDeviceInfoEventDriven.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp2.2 + + + + + + + diff --git a/demo/scan/Program.cs b/demo/scan/Program.cs index 8e54938..f3326a9 100644 --- a/demo/scan/Program.cs +++ b/demo/scan/Program.cs @@ -28,7 +28,7 @@ static async Task Main(string[] args) else { var adapters = await BlueZManager.GetAdaptersAsync(); - if (adapters.Count == 0) + if (adapters.Length == 0) { throw new Exception("No Bluetooth adapters found."); } @@ -47,7 +47,7 @@ static async Task Main(string[] args) string deviceDescription = await GetDeviceDescriptionAsync(device); Console.WriteLine(deviceDescription); } - Console.WriteLine($"{devices.Count} device(s) found ahead of scan."); + Console.WriteLine($"{devices.Length} device(s) found ahead of scan."); Console.WriteLine(); diff --git a/events_todo.md b/events_todo.md new file mode 100644 index 0000000..cbb305d --- /dev/null +++ b/events_todo.md @@ -0,0 +1,41 @@ +* Should I fire `DeviceFound` events for already present devices so consumers don't have to check the current state after adding an event handler? I could indicate in the eventArgs if the device is new so existing devices could be ignored or processed differently. Should I fire these events on the event_add thread before returning, or queue a task? Same idea for device `Connected` and adapter state changes (e.g. `PoweredOn` or `Ready`). +* What's the harm in not disposing of the handler that WatchPropertiesAsync returns? +* Should I use a finalizer instead of making consumers deal with disposing every device they get? Would a finalizer be good enough? + +* In C# are you supposed to unsubscribe from every event to prevent leaks? + +* Should I call WatchPropertiesAsync only when the first event is subscribed to? I'll have to call it synchronously, and add mutexs to do refcounting reliably. + +```C# + public event EventHandler Disconnected + { + add + { + lock (this) + { + if (m_watcher == null) + { + m_watcher = m_proxy.WatchPropertiesAsync(OnPropertyChanges).GetAwaiter().GetResult(); + } + + m_disconnected += value; + m_refcount++; + } + } + remove + { + lock (this) + { + m_disconnected -= value; + m_refcount--; + + // Should we instead check if m_disconnected and other events are all null/empty? + if (m_refcount == 0) + { + m_watcher.Dispose(); + m_watcher = null; + } + } + } + } +``` \ No newline at end of file diff --git a/src/Adapter.cs b/src/Adapter.cs new file mode 100644 index 0000000..f0a9954 --- /dev/null +++ b/src/Adapter.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tmds.DBus; + +namespace HashtagChris.DotNetBlueZ +{ + public delegate void DeviceEventHandler(Adapter sender, Device device); + + /// + /// Add events to IAdapter1. + /// + public class Adapter : IAdapter1, IDisposable + { + internal static async Task CreateAsync(IAdapter1 proxy) + { + var adapter = new Adapter + { + m_proxy = proxy, + }; + + var objectManager = Connection.System.CreateProxy(BluezConstants.DbusService, "/"); + adapter.m_watcher = await objectManager.WatchInterfacesAddedAsync(adapter.OnDeviceAdded); + + return adapter; + } + + public void Dispose() + { + m_watcher.Dispose(); + } + + public event DeviceEventHandler DeviceAdded; + + public ObjectPath ObjectPath => m_proxy.ObjectPath; + + public Task GetAllAsync() + { + return m_proxy.GetAllAsync(); + } + + public Task GetAsync(string prop) + { + return m_proxy.GetAsync(prop); + } + + public Task GetDiscoveryFiltersAsync() + { + return m_proxy.GetDiscoveryFiltersAsync(); + } + + public Task RemoveDeviceAsync(ObjectPath Device) + { + return m_proxy.RemoveDeviceAsync(Device); + } + + public Task SetAsync(string prop, object val) + { + return m_proxy.SetAsync(prop, val); + } + + public Task SetDiscoveryFilterAsync(IDictionary Properties) + { + return m_proxy.SetDiscoveryFilterAsync(Properties); + } + + public Task StartDiscoveryAsync() + { + return m_proxy.StartDiscoveryAsync(); + } + + public Task StopDiscoveryAsync() + { + return m_proxy.StopDiscoveryAsync(); + } + + public Task WatchPropertiesAsync(Action handler) + { + return m_proxy.WatchPropertiesAsync(handler); + } + + async void OnDeviceAdded((ObjectPath objectPath, IDictionary> interfaces) args) + { + if (BlueZManager.IsMatch(BluezConstants.DeviceInterface, args.objectPath, args.interfaces, this)) + { + var device = Connection.System.CreateProxy(BluezConstants.DbusService, args.objectPath); + + var dev = await Device.CreateAsync(device); + DeviceAdded?.Invoke(this, dev); + } + } + + private IAdapter1 m_proxy; + private IDisposable m_watcher; + } +} diff --git a/src/BlueZManager.cs b/src/BlueZManager.cs index c34e45d..d4dde48 100644 --- a/src/BlueZManager.cs +++ b/src/BlueZManager.cs @@ -8,7 +8,7 @@ namespace HashtagChris.DotNetBlueZ { public static class BlueZManager { - public static async Task GetAdapterAsync(string adapterName) + public static async Task GetAdapterAsync(string adapterName) { var adapterObjectPath = $"/org/bluez/{adapterName}"; var adapter = Connection.System.CreateProxy(BluezConstants.DbusService, adapterObjectPath); @@ -22,12 +22,14 @@ public static async Task GetAdapterAsync(string adapterName) throw new Exception($"Bluetooth adapter {adapterName} not found."); } - return adapter; + return await Adapter.CreateAsync(adapter); } - public static Task> GetAdaptersAsync() + public static async Task GetAdaptersAsync() { - return GetProxiesAsync(BluezConstants.AdapterInterface, rootObject: null); + var adapters = await GetProxiesAsync(BluezConstants.AdapterInterface, rootObject: null); + + return await Task.WhenAll(adapters.Select(Adapter.CreateAsync)); } /// The interface to search for diff --git a/src/Device.cs b/src/Device.cs new file mode 100644 index 0000000..9582d08 --- /dev/null +++ b/src/Device.cs @@ -0,0 +1,114 @@ +using System; +using System.Threading.Tasks; +using Tmds.DBus; + +namespace HashtagChris.DotNetBlueZ +{ + /// + /// Adds events to IDevice1. + /// + public class Device : IDevice1, IDisposable + { + internal static async Task CreateAsync(IDevice1 proxy) + { + var device = new Device + { + m_proxy = proxy, + }; + device.m_watcher = await proxy.WatchPropertiesAsync(device.OnPropertyChanges); + + return device; + } + + public void Dispose() + { + m_watcher.Dispose(); + } + + public event EventHandler Connected; + public event EventHandler Disconnected; + public event EventHandler ServicesResolved; + + public ObjectPath ObjectPath => m_proxy.ObjectPath; + + public Task CancelPairingAsync() + { + return m_proxy.CancelPairingAsync(); + } + + public Task ConnectAsync() + { + return m_proxy.ConnectAsync(); + } + + public Task ConnectProfileAsync(string UUID) + { + return m_proxy.ConnectProfileAsync(UUID); + } + + public Task DisconnectAsync() + { + return m_proxy.DisconnectAsync(); + } + + public Task DisconnectProfileAsync(string UUID) + { + return m_proxy.DisconnectProfileAsync(UUID); + } + + public Task GetAllAsync() + { + return m_proxy.GetAllAsync(); + } + + public Task GetAsync(string prop) + { + return m_proxy.GetAsync(prop); + } + + public Task PairAsync() + { + return m_proxy.PairAsync(); + } + + public Task SetAsync(string prop, object val) + { + return m_proxy.SetAsync(prop, val); + } + + public Task WatchPropertiesAsync(Action handler) + { + return m_proxy.WatchPropertiesAsync(handler); + } + + private void OnPropertyChanges(PropertyChanges changes) + { + foreach (var pair in changes.Changed) + { + switch (pair.Key) + { + case "Connected": + if (true.Equals(pair.Value)) + { + Connected?.Invoke(this, new EventArgs()); + } + else + { + Disconnected?.Invoke(this, new EventArgs()); + } + break; + + case "ServicesResolved": + if (true.Equals(pair.Value)) + { + ServicesResolved?.Invoke(this, new EventArgs()); + } + break; + } + } + } + + private IDevice1 m_proxy; + private IDisposable m_watcher; + } +} \ No newline at end of file diff --git a/src/Extensions.cs b/src/Extensions.cs index b1ccb69..1a9aa40 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -8,12 +8,14 @@ namespace HashtagChris.DotNetBlueZ.Extensions { public static class Extensions { - public static Task> GetDevicesAsync(this IAdapter1 adapter) + public static async Task GetDevicesAsync(this IAdapter1 adapter) { - return BlueZManager.GetProxiesAsync(BluezConstants.DeviceInterface, adapter); + var devices = await BlueZManager.GetProxiesAsync(BluezConstants.DeviceInterface, adapter); + + return await Task.WhenAll(devices.Select(Device.CreateAsync)); } - public static async Task GetDeviceAsync(this IAdapter1 adapter, string deviceAddress) + public static async Task GetDeviceAsync(this IAdapter1 adapter, string deviceAddress) { var devices = await BlueZManager.GetProxiesAsync(BluezConstants.DeviceInterface, adapter); @@ -32,38 +34,29 @@ public static async Task GetDeviceAsync(this IAdapter1 adapter, string throw new Exception($"{matches.Count} devices found with the address {deviceAddress}!"); } - return matches.FirstOrDefault(); - } - - - public static Task WatchDevicesAddedAsync(this IAdapter1 adapter, Action handler) - { - void OnDeviceAdded((ObjectPath objectPath, IDictionary> interfaces) args) + var dev = matches.FirstOrDefault(); + if (dev != null) { - if (BlueZManager.IsMatch(BluezConstants.DeviceInterface, args.objectPath, args.interfaces, adapter)) - { - var device = Connection.System.CreateProxy(BluezConstants.DbusService, args.objectPath); - handler(device); - } + return await Device.CreateAsync(dev); } - - var objectManager = Connection.System.CreateProxy(BluezConstants.DbusService, "/"); - return objectManager.WatchInterfacesAddedAsync(OnDeviceAdded); + return null; } - public static Task WatchDevicesRemovedAsync(this IAdapter1 adapter, Action handler) + public static Task WatchDevicesAddedAsync(this IAdapter1 adapter, Action handler) { - void OnDeviceAdded((ObjectPath objectPath, String[] interfaces) args) + async void OnDeviceAdded((ObjectPath objectPath, IDictionary> interfaces) args) { if (BlueZManager.IsMatch(BluezConstants.DeviceInterface, args.objectPath, args.interfaces, adapter)) { var device = Connection.System.CreateProxy(BluezConstants.DbusService, args.objectPath); - handler(device); + + var dev = await Device.CreateAsync(device); + handler(dev); } } var objectManager = Connection.System.CreateProxy(BluezConstants.DbusService, "/"); - return objectManager.WatchInterfacesRemovedAsync(OnDeviceAdded); + return objectManager.WatchInterfacesAddedAsync(OnDeviceAdded); } public static async Task GetServiceAsync(this IDevice1 device, string serviceUUID) From 038243adf25126ef1f851f32ad1e82a4236447f9 Mon Sep 17 00:00:00 2001 From: Chris Sidi Date: Mon, 1 Jul 2019 00:28:46 -0400 Subject: [PATCH 2/7] Add GattCharacteristic Also fire an event if already connected, services already resolved, etc. --- demo/printDeviceInfoEventDriven/Program.cs | 126 +++++++++++++++++---- demo/scan/Program.cs | 5 +- events_todo.md | 4 + src/Adapter.cs | 101 +++++++++++++++-- src/BlueZManager.cs | 2 +- src/Constants.cs | 4 + src/Device.cs | 70 ++++++++++-- src/EventArgs.cs | 25 ++++ src/Extensions.cs | 2 +- src/GattCharacteristic.cs | 106 +++++++++++++++++ 10 files changed, 400 insertions(+), 45 deletions(-) create mode 100644 src/EventArgs.cs create mode 100644 src/GattCharacteristic.cs diff --git a/demo/printDeviceInfoEventDriven/Program.cs b/demo/printDeviceInfoEventDriven/Program.cs index a8460e3..d2fa108 100644 --- a/demo/printDeviceInfoEventDriven/Program.cs +++ b/demo/printDeviceInfoEventDriven/Program.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Text; using System.Threading.Tasks; using HashtagChris.DotNetBlueZ; using HashtagChris.DotNetBlueZ.Extensions; @@ -8,18 +9,18 @@ namespace printDeviceInfoEventDriven { class Program { - static async Task Main(string[] args) + private static async Task Main(string[] args) { if (args.Length < 1) { - Console.WriteLine("Usage: PrintDeviceInfo [adapterName]"); - Console.WriteLine("Example: PrintDeviceInfo AA:BB:CC:11:22:33 hci1"); + Console.WriteLine("Usage: PrintDeviceInfo [adapterName]"); + Console.WriteLine("Example: PrintDeviceInfo phone hci1"); return; } - var deviceAddress = args[0]; + s_deviceNameSubstring = args[0]; - IAdapter1 adapter; + Adapter adapter; if (args.Length > 1) { adapter = await BlueZManager.GetAdapterAsync(args[1]); @@ -27,7 +28,7 @@ static async Task Main(string[] args) else { var adapters = await BlueZManager.GetAdaptersAsync(); - if (adapters.Length == 0) + if (adapters.Count == 0) { throw new Exception("No Bluetooth adapters found."); } @@ -39,37 +40,114 @@ static async Task Main(string[] args) var adapterName = adapterPath.Substring(adapterPath.LastIndexOf("/") + 1); Console.WriteLine($"Using Bluetooth adapter {adapterName}"); - // Find the Bluetooth peripheral. - Device device = await adapter.GetDeviceAsync(deviceAddress); - if (device == null) + adapter.PoweredOn += adapter_PoweredOnAsync; + adapter.DeviceFound += adapter_DeviceFoundAsync; + await Task.Delay(-1); + } + + private static async Task adapter_PoweredOnAsync(Adapter adapter, BlueZEventArgs e) + { + if (e.IsStateChange) { - Console.WriteLine($"Bluetooth peripheral with address '{deviceAddress}' not found. Use `bluetoothctl` or Bluetooth Manager to scan and possibly pair first."); - return; + Console.WriteLine("Powered on."); + } + else + { + Console.WriteLine("Already powered on."); } - device.Connected += device_Connected; - device.ServicesResolved += device_ServicesResolved; + Console.WriteLine("Starting scan..."); + await adapter.StartDiscoveryAsync(); + } - Console.WriteLine("Connecting..."); - await device.ConnectAsync(); + private static async Task adapter_DeviceFoundAsync(Adapter adapter, DeviceFoundEventArgs e) + { + var device = e.Device; + + string deviceDescription = await GetDeviceDescriptionAsync(device); + if (e.IsStateChange) + { + Console.WriteLine($"Found: [NEW] {deviceDescription}"); + } + else + { + Console.WriteLine($"Found: {deviceDescription}"); + } + + var deviceName = await device.GetAliasAsync(); + if (deviceName.Contains(s_deviceNameSubstring, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Stopping scan...."); + await adapter.StopDiscoveryAsync(); + Console.WriteLine("Stopped."); + + device.Connected += device_ConnectedAsync; + device.Disconnected += device_DisconnectedAsync; + device.ServicesResolved += device_ServicesResolvedAsync; + Console.WriteLine("Connecting..."); + await device.ConnectAsync(); + } } - static async void device_Connected(Object sender, EventArgs e) + private static async Task device_ConnectedAsync(Device device, BlueZEventArgs e) { - var dev = (Device)sender; - Console.WriteLine($"Connected to {await dev.GetAddressAsync()}"); + if (e.IsStateChange) + { + Console.WriteLine($"Connected to {await device.GetAddressAsync()}"); + } + else + { + Console.WriteLine($"Already connected to {await device.GetAddressAsync()}"); + } } - static async void device_Disonnected(Object sender, EventArgs e) + private static async Task device_DisconnectedAsync(Device device, BlueZEventArgs e) { - var dev = (Device)sender; - Console.WriteLine($"Disconnected from {await dev.GetAddressAsync()}"); + Console.WriteLine($"Disconnected from {await device.GetAddressAsync()}"); + + await Task.Delay(TimeSpan.FromSeconds(15)); + + Console.WriteLine("Attempting to reconnect..."); + await device.ConnectAsync(); + } + + private static async Task device_ServicesResolvedAsync(Device device, BlueZEventArgs e) + { + if (e.IsStateChange) + { + Console.WriteLine($"Services resolved for {await device.GetAddressAsync()}"); + } + else + { + Console.WriteLine($"Services already resolved for {await device.GetAddressAsync()}"); + } + + var servicesUUID = await device.GetUUIDsAsync(); + Console.WriteLine($"Device offers {servicesUUID.Length} service(s)."); + + var deviceInfoServiceFound = servicesUUID.Any(uuid => String.Equals(uuid, GattConstants.BatteryServiceUUID, StringComparison.OrdinalIgnoreCase)); + if (!deviceInfoServiceFound) + { + Console.WriteLine("Device doesn't have the Device Information Service. Try pairing first?"); + return; + } + + var service = await device.GetServiceAsync(GattConstants.BatteryServiceUUID); + var characteristic = await service.GetCharacteristicAsync(GattConstants.BatteryLevelCharacteristicUUID); + + Console.WriteLine("Reading current battery level..."); + var valueBytes = await characteristic.ReadValueAsync(timeout); + Console.WriteLine($"Battery level: {valueBytes[0]}%"); } - static async void device_ServicesResolved(Object sender, EventArgs e) + private static async Task GetDeviceDescriptionAsync(IDevice1 device) { - var dev = (Device)sender; - Console.WriteLine($"Services resolved for {await dev.GetAddressAsync()}"); + var deviceProperties = await device.GetAllAsync(); + return $"{deviceProperties.Alias} (Address: {deviceProperties.Address}, RSSI: {deviceProperties.RSSI})"; } + + private static string s_deviceNameSubstring; + + private static TimeSpan timeout = TimeSpan.FromSeconds(15); } } diff --git a/demo/scan/Program.cs b/demo/scan/Program.cs index f3326a9..f28ffaf 100644 --- a/demo/scan/Program.cs +++ b/demo/scan/Program.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Text; using System.Threading.Tasks; using HashtagChris.DotNetBlueZ; using HashtagChris.DotNetBlueZ.Extensions; @@ -28,7 +27,7 @@ static async Task Main(string[] args) else { var adapters = await BlueZManager.GetAdaptersAsync(); - if (adapters.Length == 0) + if (adapters.Count == 0) { throw new Exception("No Bluetooth adapters found."); } @@ -47,7 +46,7 @@ static async Task Main(string[] args) string deviceDescription = await GetDeviceDescriptionAsync(device); Console.WriteLine(deviceDescription); } - Console.WriteLine($"{devices.Length} device(s) found ahead of scan."); + Console.WriteLine($"{devices.Count} device(s) found ahead of scan."); Console.WriteLine(); diff --git a/events_todo.md b/events_todo.md index cbb305d..f8fc2a7 100644 --- a/events_todo.md +++ b/events_todo.md @@ -4,8 +4,12 @@ * In C# are you supposed to unsubscribe from every event to prevent leaks? +Answer: Generally yes, so the garbage collection can collect the subscribing object. + * Should I call WatchPropertiesAsync only when the first event is subscribed to? I'll have to call it synchronously, and add mutexs to do refcounting reliably. +Update: Tried this, resulted in a hang on `m_proxy.WatchPropertiesAsync(OnPropertyChanges).GetAwaiter().GetResult()`. + ```C# public event EventHandler Disconnected { diff --git a/src/Adapter.cs b/src/Adapter.cs index f0a9954..2e85b36 100644 --- a/src/Adapter.cs +++ b/src/Adapter.cs @@ -1,17 +1,25 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using HashtagChris.DotNetBlueZ.Extensions; using Tmds.DBus; namespace HashtagChris.DotNetBlueZ { - public delegate void DeviceEventHandler(Adapter sender, Device device); + public delegate Task DeviceChangeEventHandlerAsync(Adapter sender, DeviceFoundEventArgs eventArgs); + + public delegate Task AdapterEventHandlerAsync(Adapter sender, BlueZEventArgs eventArgs); /// /// Add events to IAdapter1. /// public class Adapter : IAdapter1, IDisposable { + ~Adapter() + { + Dispose(); + } + internal static async Task CreateAsync(IAdapter1 proxy) { var adapter = new Adapter @@ -20,17 +28,47 @@ internal static async Task CreateAsync(IAdapter1 proxy) }; var objectManager = Connection.System.CreateProxy(BluezConstants.DbusService, "/"); - adapter.m_watcher = await objectManager.WatchInterfacesAddedAsync(adapter.OnDeviceAdded); + adapter.m_interfacesWatcher = await objectManager.WatchInterfacesAddedAsync(adapter.OnDeviceAdded); + adapter.m_propertyWatcher = await proxy.WatchPropertiesAsync(adapter.OnPropertyChanges); return adapter; } public void Dispose() { - m_watcher.Dispose(); + m_interfacesWatcher?.Dispose(); + m_interfacesWatcher = null; + + GC.SuppressFinalize(this); + } + + public event DeviceChangeEventHandlerAsync DeviceFound + { + add + { + m_deviceFound += value; + FireEventForExistingDevicesAsync(); + } + remove + { + m_deviceFound -= value; + } + } + + public event AdapterEventHandlerAsync PoweredOn + { + add + { + m_poweredOn += value; + FireEventIfPropertyAlreadyTrueAsync(m_poweredOn, "Powered"); + } + remove + { + m_poweredOn -= value; + } } - public event DeviceEventHandler DeviceAdded; + public event AdapterEventHandlerAsync PoweredOff; public ObjectPath ObjectPath => m_proxy.ObjectPath; @@ -79,18 +117,67 @@ public Task WatchPropertiesAsync(Action handler) return m_proxy.WatchPropertiesAsync(handler); } - async void OnDeviceAdded((ObjectPath objectPath, IDictionary> interfaces) args) + private async void FireEventForExistingDevicesAsync() + { + var devices = await this.GetDevicesAsync(); + foreach (var device in devices) + { + m_deviceFound?.Invoke(this, new DeviceFoundEventArgs(device, isStateChange: false)); + } + } + + private async void OnDeviceAdded((ObjectPath objectPath, IDictionary> interfaces) args) { if (BlueZManager.IsMatch(BluezConstants.DeviceInterface, args.objectPath, args.interfaces, this)) { var device = Connection.System.CreateProxy(BluezConstants.DbusService, args.objectPath); var dev = await Device.CreateAsync(device); - DeviceAdded?.Invoke(this, dev); + m_deviceFound?.Invoke(this, new DeviceFoundEventArgs(dev)); + } + } + + private async void FireEventIfPropertyAlreadyTrueAsync(AdapterEventHandlerAsync handler, string prop) + { + try + { + var value = await m_proxy.GetAsync(prop); + if (value) + { + // TODO: Suppress duplicate event from OnPropertyChanges. + handler?.Invoke(this, new BlueZEventArgs(isStateChange: false)); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error checking if '{prop}' is already true: {ex}"); + } + } + + private void OnPropertyChanges(PropertyChanges changes) + { + foreach (var pair in changes.Changed) + { + switch (pair.Key) + { + case "Powered": + if (true.Equals(pair.Value)) + { + m_poweredOn?.Invoke(this, new BlueZEventArgs()); + } + else + { + PoweredOff?.Invoke(this, new BlueZEventArgs()); + } + break; + } } } private IAdapter1 m_proxy; - private IDisposable m_watcher; + private IDisposable m_interfacesWatcher; + private IDisposable m_propertyWatcher; + private DeviceChangeEventHandlerAsync m_deviceFound; + private AdapterEventHandlerAsync m_poweredOn; } } diff --git a/src/BlueZManager.cs b/src/BlueZManager.cs index d4dde48..0515076 100644 --- a/src/BlueZManager.cs +++ b/src/BlueZManager.cs @@ -25,7 +25,7 @@ public static async Task GetAdapterAsync(string adapterName) return await Adapter.CreateAsync(adapter); } - public static async Task GetAdaptersAsync() + public static async Task> GetAdaptersAsync() { var adapters = await GetProxiesAsync(BluezConstants.AdapterInterface, rootObject: null); diff --git a/src/Constants.cs b/src/Constants.cs index 6decfff..1d03c36 100644 --- a/src/Constants.cs +++ b/src/Constants.cs @@ -17,5 +17,9 @@ public class GattConstants public const string DeviceInformationServiceUUID = "0000180a-0000-1000-8000-00805f9b34fb"; public const string ModelNameCharacteristicUUID = "00002a24-0000-1000-8000-00805f9b34fb"; public const string ManufacturerNameCharacteristicUUID = "00002a29-0000-1000-8000-00805f9b34fb"; + + // https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Services/org.bluetooth.service.battery_service.xml + public const string BatteryServiceUUID = "0000180f-0000-1000-8000-00805f9b34fb"; + public const string BatteryLevelCharacteristicUUID = "00002a19-0000-1000-8000-00805f9b34fb"; } } diff --git a/src/Device.cs b/src/Device.cs index 9582d08..460be94 100644 --- a/src/Device.cs +++ b/src/Device.cs @@ -4,30 +4,63 @@ namespace HashtagChris.DotNetBlueZ { + public delegate Task DeviceEventHandlerAsync(Device sender, BlueZEventArgs eventArgs); + /// /// Adds events to IDevice1. /// public class Device : IDevice1, IDisposable { + ~Device() + { + Dispose(); + } + internal static async Task CreateAsync(IDevice1 proxy) { var device = new Device { m_proxy = proxy, }; - device.m_watcher = await proxy.WatchPropertiesAsync(device.OnPropertyChanges); + device.m_propertyWatcher = await proxy.WatchPropertiesAsync(device.OnPropertyChanges); return device; } public void Dispose() { - m_watcher.Dispose(); + m_propertyWatcher?.Dispose(); + m_propertyWatcher = null; + + GC.SuppressFinalize(this); } - public event EventHandler Connected; - public event EventHandler Disconnected; - public event EventHandler ServicesResolved; + public event DeviceEventHandlerAsync Connected + { + add + { + m_connected += value; + FireEventIfPropertyAlreadyTrueAsync(m_connected, "Connected"); + } + remove + { + m_connected -= value; + } + } + + public event DeviceEventHandlerAsync Disconnected; + public event DeviceEventHandlerAsync ServicesResolved + { + add + { + m_resolved += value; + FireEventIfPropertyAlreadyTrueAsync(m_resolved, "ServicesResolved"); + } + remove + { + m_resolved -= value; + } + } public ObjectPath ObjectPath => m_proxy.ObjectPath; @@ -81,6 +114,23 @@ public Task WatchPropertiesAsync(Action handler) return m_proxy.WatchPropertiesAsync(handler); } + private async void FireEventIfPropertyAlreadyTrueAsync(DeviceEventHandlerAsync handler, string prop) + { + try + { + var value = await m_proxy.GetAsync(prop); + if (value) + { + // TODO: Suppress duplicate event from OnPropertyChanges. + handler?.Invoke(this, new BlueZEventArgs(isStateChange: false)); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error checking if '{prop}' is already true: {ex}"); + } + } + private void OnPropertyChanges(PropertyChanges changes) { foreach (var pair in changes.Changed) @@ -90,18 +140,18 @@ private void OnPropertyChanges(PropertyChanges changes) case "Connected": if (true.Equals(pair.Value)) { - Connected?.Invoke(this, new EventArgs()); + m_connected?.Invoke(this, new BlueZEventArgs()); } else { - Disconnected?.Invoke(this, new EventArgs()); + Disconnected?.Invoke(this, new BlueZEventArgs()); } break; case "ServicesResolved": if (true.Equals(pair.Value)) { - ServicesResolved?.Invoke(this, new EventArgs()); + m_resolved?.Invoke(this, new BlueZEventArgs()); } break; } @@ -109,6 +159,8 @@ private void OnPropertyChanges(PropertyChanges changes) } private IDevice1 m_proxy; - private IDisposable m_watcher; + private IDisposable m_propertyWatcher; + private event DeviceEventHandlerAsync m_connected; + private event DeviceEventHandlerAsync m_resolved; } } \ No newline at end of file diff --git a/src/EventArgs.cs b/src/EventArgs.cs new file mode 100644 index 0000000..60956e4 --- /dev/null +++ b/src/EventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace HashtagChris.DotNetBlueZ +{ + public class BlueZEventArgs : EventArgs + { + public BlueZEventArgs(bool isStateChange = true) + { + IsStateChange = isStateChange; + } + + public bool IsStateChange { get; } + } + + public class DeviceFoundEventArgs : BlueZEventArgs + { + public DeviceFoundEventArgs(Device device, bool isStateChange = true) + : base(isStateChange) + { + Device = device; + } + + public Device Device { get; } + } +} \ No newline at end of file diff --git a/src/Extensions.cs b/src/Extensions.cs index 1a9aa40..ed86828 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -8,7 +8,7 @@ namespace HashtagChris.DotNetBlueZ.Extensions { public static class Extensions { - public static async Task GetDevicesAsync(this IAdapter1 adapter) + public static async Task> GetDevicesAsync(this IAdapter1 adapter) { var devices = await BlueZManager.GetProxiesAsync(BluezConstants.DeviceInterface, adapter); diff --git a/src/GattCharacteristic.cs b/src/GattCharacteristic.cs new file mode 100644 index 0000000..1939801 --- /dev/null +++ b/src/GattCharacteristic.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tmds.DBus; + +namespace HashtagChris.DotNetBlueZ +{ + public delegate void CharacteristicChangeEventHandler(GattCharacteristic sender, CharacteristicChangeEventArgs e); + + public class CharacteristicChangeEventArgs : EventArgs + { + public PropertyChanges PropertyChanges { get; set; } + } + + public class GattCharacteristic : IGattCharacteristic1, IDisposable + { + ~GattCharacteristic() + { + Dispose(); + } + + internal static async Task CreateAsync(IGattCharacteristic1 proxy) + { + var characteristic = new GattCharacteristic + { + m_proxy = proxy, + }; + characteristic.m_propertyWatcher = await proxy.WatchPropertiesAsync(characteristic.OnPropertyChanges); + + return characteristic; + } + + public void Dispose() + { + m_propertyWatcher?.Dispose(); + m_propertyWatcher = null; + + GC.SuppressFinalize(this); + } + + public event CharacteristicChangeEventHandler PropertyChange; + + public ObjectPath ObjectPath => m_proxy.ObjectPath; + + public Task<(CloseSafeHandle fd, ushort mtu)> AcquireNotifyAsync(IDictionary Options) + { + return m_proxy.AcquireNotifyAsync(Options); + } + + public Task<(CloseSafeHandle fd, ushort mtu)> AcquireWriteAsync(IDictionary Options) + { + return m_proxy.AcquireWriteAsync(Options); + } + + public Task GetAllAsync() + { + return m_proxy.GetAllAsync(); + } + + public Task GetAsync(string prop) + { + return m_proxy.GetAsync(prop); + } + + public Task ReadValueAsync(IDictionary Options) + { + return m_proxy.ReadValueAsync(Options); + } + + public Task SetAsync(string prop, object val) + { + return m_proxy.SetAsync(prop, val); + } + + public Task StartNotifyAsync() + { + return m_proxy.StartNotifyAsync(); + } + + public Task StopNotifyAsync() + { + return m_proxy.StopNotifyAsync(); + } + + public Task WatchPropertiesAsync(Action handler) + { + return m_proxy.WatchPropertiesAsync(handler); + } + + public Task WriteValueAsync(byte[] Value, IDictionary Options) + { + return m_proxy.WriteValueAsync(Value, Options); + } + + private void OnPropertyChanges(PropertyChanges changes) + { + PropertyChange?.Invoke(this, new CharacteristicChangeEventArgs + { + PropertyChanges = changes, + }); + } + + private IGattCharacteristic1 m_proxy; + private IDisposable m_propertyWatcher; + } +} \ No newline at end of file From 77667fcdefc273fb9565f5dc690b44760a4bd2d9 Mon Sep 17 00:00:00 2001 From: Chris Sidi Date: Fri, 5 Jul 2019 10:14:19 -0400 Subject: [PATCH 3/7] Make sample program more robust. --- demo/printDeviceInfoEventDriven/Program.cs | 193 ++++++++++++++------- src/Extensions.cs | 8 +- 2 files changed, 136 insertions(+), 65 deletions(-) diff --git a/demo/printDeviceInfoEventDriven/Program.cs b/demo/printDeviceInfoEventDriven/Program.cs index d2fa108..ef417a2 100644 --- a/demo/printDeviceInfoEventDriven/Program.cs +++ b/demo/printDeviceInfoEventDriven/Program.cs @@ -9,16 +9,25 @@ namespace printDeviceInfoEventDriven { class Program { + // Service and Characteristic UUIDs are from https://github.com/hashtagchris/early-iOS-BluetoothLowEnergy-tests/tree/master/myFirstPeripheral + // Feel free to replace with your own. + private const string ServiceUUID = "0000cafe-0000-1000-8000-00805f9b34fb"; + private const string CharacteristicUUID = "0000c0ff-0000-1000-8000-00805f9b34fb"; + + private static string s_deviceFilter; + + private static TimeSpan timeout = TimeSpan.FromSeconds(15); + private static async Task Main(string[] args) { if (args.Length < 1) { - Console.WriteLine("Usage: PrintDeviceInfo [adapterName]"); + Console.WriteLine("Usage: PrintDeviceInfo | [adapterName]"); Console.WriteLine("Example: PrintDeviceInfo phone hci1"); return; } - s_deviceNameSubstring = args[0]; + s_deviceFilter = args[0]; Adapter adapter; if (args.Length > 1) @@ -47,107 +56,165 @@ private static async Task Main(string[] args) private static async Task adapter_PoweredOnAsync(Adapter adapter, BlueZEventArgs e) { - if (e.IsStateChange) + try { - Console.WriteLine("Powered on."); + if (e.IsStateChange) + { + Console.WriteLine("Bluetooth adapter powered on."); + } + else + { + Console.WriteLine("Bluetooth adapter already powered on."); + } + + Console.WriteLine("Starting scan..."); + await adapter.StartDiscoveryAsync(); } - else + catch (Exception ex) { - Console.WriteLine("Already powered on."); + Console.Error.WriteLine(ex); } - - Console.WriteLine("Starting scan..."); - await adapter.StartDiscoveryAsync(); } private static async Task adapter_DeviceFoundAsync(Adapter adapter, DeviceFoundEventArgs e) { - var device = e.Device; - - string deviceDescription = await GetDeviceDescriptionAsync(device); - if (e.IsStateChange) - { - Console.WriteLine($"Found: [NEW] {deviceDescription}"); - } - else + try { - Console.WriteLine($"Found: {deviceDescription}"); - } + var device = e.Device; + + var deviceDescription = await GetDeviceDescriptionAsync(device); + if (e.IsStateChange) + { + Console.WriteLine($"Found: [NEW] {deviceDescription}"); + } + else + { + Console.WriteLine($"Found: {deviceDescription}"); + } - var deviceName = await device.GetAliasAsync(); - if (deviceName.Contains(s_deviceNameSubstring, StringComparison.OrdinalIgnoreCase)) + var deviceAddress = await device.GetAddressAsync(); + var deviceName = await device.GetAliasAsync(); + if (deviceAddress.Equals(s_deviceFilter, StringComparison.OrdinalIgnoreCase) + || deviceName.Contains(s_deviceFilter, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Stopping scan...."); + try + { + await adapter.StopDiscoveryAsync(); + Console.WriteLine("Stopped."); + } + catch (Exception ex) + { + // Best effort. Sometimes BlueZ gets in a state where you can't stop the scan. + Console.Error.WriteLine($"Error stopping scan: {ex.Message}"); + } + + device.Connected += device_ConnectedAsync; + device.Disconnected += device_DisconnectedAsync; + device.ServicesResolved += device_ServicesResolvedAsync; + Console.WriteLine($"Connecting to {await device.GetAddressAsync()}..."); + await device.ConnectAsync(); + } + } + catch (Exception ex) { - Console.WriteLine("Stopping scan...."); - await adapter.StopDiscoveryAsync(); - Console.WriteLine("Stopped."); - - device.Connected += device_ConnectedAsync; - device.Disconnected += device_DisconnectedAsync; - device.ServicesResolved += device_ServicesResolvedAsync; - Console.WriteLine("Connecting..."); - await device.ConnectAsync(); + Console.Error.WriteLine(ex); } } private static async Task device_ConnectedAsync(Device device, BlueZEventArgs e) { - if (e.IsStateChange) + try { - Console.WriteLine($"Connected to {await device.GetAddressAsync()}"); + if (e.IsStateChange) + { + Console.WriteLine($"Connected to {await device.GetAddressAsync()}"); + } + else + { + Console.WriteLine($"Already connected to {await device.GetAddressAsync()}"); + } } - else + catch (Exception ex) { - Console.WriteLine($"Already connected to {await device.GetAddressAsync()}"); + Console.Error.WriteLine(ex); } } private static async Task device_DisconnectedAsync(Device device, BlueZEventArgs e) { - Console.WriteLine($"Disconnected from {await device.GetAddressAsync()}"); + try + { + Console.WriteLine($"Disconnected from {await device.GetAddressAsync()}"); - await Task.Delay(TimeSpan.FromSeconds(15)); + await Task.Delay(TimeSpan.FromSeconds(15)); - Console.WriteLine("Attempting to reconnect..."); - await device.ConnectAsync(); + Console.WriteLine($"Attempting to reconnect to {await device.GetAddressAsync()}..."); + await device.ConnectAsync(); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + } } private static async Task device_ServicesResolvedAsync(Device device, BlueZEventArgs e) { - if (e.IsStateChange) - { - Console.WriteLine($"Services resolved for {await device.GetAddressAsync()}"); - } - else + try { - Console.WriteLine($"Services already resolved for {await device.GetAddressAsync()}"); - } + if (e.IsStateChange) + { + Console.WriteLine($"Services resolved for {await device.GetAddressAsync()}"); + } + else + { + Console.WriteLine($"Services already resolved for {await device.GetAddressAsync()}"); + } - var servicesUUID = await device.GetUUIDsAsync(); - Console.WriteLine($"Device offers {servicesUUID.Length} service(s)."); + var servicesUUIDs = await device.GetUUIDsAsync(); + Console.WriteLine($"Device offers {servicesUUIDs.Length} service(s)."); + // foreach (var uuid in servicesUUIDs) + // { + // Console.WriteLine(uuid); + // } - var deviceInfoServiceFound = servicesUUID.Any(uuid => String.Equals(uuid, GattConstants.BatteryServiceUUID, StringComparison.OrdinalIgnoreCase)); - if (!deviceInfoServiceFound) - { - Console.WriteLine("Device doesn't have the Device Information Service. Try pairing first?"); - return; - } + var service = await device.GetServiceAsync(ServiceUUID); + if (service == null) + { + Console.WriteLine($"Service UUID {ServiceUUID} notfound. Do you need to pair first?"); + return; + } - var service = await device.GetServiceAsync(GattConstants.BatteryServiceUUID); - var characteristic = await service.GetCharacteristicAsync(GattConstants.BatteryLevelCharacteristicUUID); + var characteristic = await service.GetCharacteristicAsync(CharacteristicUUID); + if (characteristic == null) + { + Console.WriteLine($"Characteristic UUID {CharacteristicUUID} not found within service {ServiceUUID}."); + return; + } - Console.WriteLine("Reading current battery level..."); - var valueBytes = await characteristic.ReadValueAsync(timeout); - Console.WriteLine($"Battery level: {valueBytes[0]}%"); + Console.WriteLine(); + Console.WriteLine("Reading GATT characteristic..."); + var valueBytes = await characteristic.ReadValueAsync(timeout); + Console.WriteLine($"Characteristic value (hex): {BitConverter.ToString(valueBytes)}"); + try + { + var stringValue = Encoding.UTF8.GetString(valueBytes); + Console.WriteLine($"Characteristic value (UTF-8): \"{stringValue}\""); + } + catch (Exception) + { + } + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + } } private static async Task GetDeviceDescriptionAsync(IDevice1 device) { var deviceProperties = await device.GetAllAsync(); - return $"{deviceProperties.Alias} (Address: {deviceProperties.Address}, RSSI: {deviceProperties.RSSI})"; + return $"{deviceProperties.Address} (Alias: {deviceProperties.Alias}, RSSI: {deviceProperties.RSSI})"; } - - private static string s_deviceNameSubstring; - - private static TimeSpan timeout = TimeSpan.FromSeconds(15); } } diff --git a/src/Extensions.cs b/src/Extensions.cs index ed86828..39113f5 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -65,7 +65,9 @@ public static async Task GetServiceAsync(this IDevice1 device, st foreach (var service in services) { - if (await service.GetUUIDAsync() == serviceUUID) + var uuid = await service.GetUUIDAsync(); + // Console.WriteLine($"Checking {uuid}"); + if (String.Equals(uuid, serviceUUID, StringComparison.OrdinalIgnoreCase)) { return service; } @@ -80,7 +82,9 @@ public static async Task GetCharacteristicAsync(this IGatt foreach (var characteristic in characteristics) { - if (await characteristic.GetUUIDAsync() == characteristicUUID) + var uuid = await characteristic.GetUUIDAsync(); + // Console.WriteLine($"Checking {uuid}"); + if (String.Equals(uuid, characteristicUUID, StringComparison.OrdinalIgnoreCase)) { return characteristic; } From 656ce097956526b8862c617ee0d3774ae3f5a314 Mon Sep 17 00:00:00 2001 From: Chris Sidi Date: Fri, 5 Jul 2019 16:19:43 -0400 Subject: [PATCH 4/7] Watch current time characteristic --- demo/printDeviceInfoEventDriven/Program.cs | 60 ++++++++++++--- src/Constants.cs | 17 ++-- src/EventArgs.cs | 11 +++ src/Extensions.cs | 5 +- src/GattCharacteristic.cs | 90 +++++++++++++++------- 5 files changed, 137 insertions(+), 46 deletions(-) diff --git a/demo/printDeviceInfoEventDriven/Program.cs b/demo/printDeviceInfoEventDriven/Program.cs index ef417a2..ba81938 100644 --- a/demo/printDeviceInfoEventDriven/Program.cs +++ b/demo/printDeviceInfoEventDriven/Program.cs @@ -9,10 +9,8 @@ namespace printDeviceInfoEventDriven { class Program { - // Service and Characteristic UUIDs are from https://github.com/hashtagchris/early-iOS-BluetoothLowEnergy-tests/tree/master/myFirstPeripheral - // Feel free to replace with your own. - private const string ServiceUUID = "0000cafe-0000-1000-8000-00805f9b34fb"; - private const string CharacteristicUUID = "0000c0ff-0000-1000-8000-00805f9b34fb"; + private const string ServiceUUID = GattConstants.CurrentTimeServiceUUID; + private const string CharacteristicUUID = GattConstants.CurrentTimeCharacteristicUUID; private static string s_deviceFilter; @@ -193,16 +191,38 @@ private static async Task device_ServicesResolvedAsync(Device device, BlueZEvent } Console.WriteLine(); - Console.WriteLine("Reading GATT characteristic..."); - var valueBytes = await characteristic.ReadValueAsync(timeout); - Console.WriteLine($"Characteristic value (hex): {BitConverter.ToString(valueBytes)}"); - try + characteristic.Value += characteristic_Value; + + // Console.WriteLine("Reading GATT characteristic..."); + // var valueBytes = await characteristic.ReadValueAsync(timeout); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + } + } + + private static async Task characteristic_Value(GattCharacteristic characteristic, GattCharacteristicValueEventArgs e) + { + try + { + var uuid = await characteristic.GetUUIDAsync(); + Console.WriteLine($"UUID: {uuid}; Status change: {e.IsStateChange}"); + if (String.Equals(uuid, GattConstants.CurrentTimeCharacteristicUUID, StringComparison.OrdinalIgnoreCase)) { - var stringValue = Encoding.UTF8.GetString(valueBytes); - Console.WriteLine($"Characteristic value (UTF-8): \"{stringValue}\""); + var currentTime = ReadCurrentTime(e.Value); + Console.WriteLine($"Current time: {currentTime}"); } - catch (Exception) + else { + // Default + Console.WriteLine($"Characteristic value (hex): {BitConverter.ToString(e.Value)}"); + try + { + var stringValue = Encoding.UTF8.GetString(e.Value); + Console.WriteLine($"Characteristic value (UTF-8): \"{stringValue}\""); + } + catch (Exception) {} } } catch (Exception ex) @@ -216,5 +236,23 @@ private static async Task GetDeviceDescriptionAsync(IDevice1 device) var deviceProperties = await device.GetAllAsync(); return $"{deviceProperties.Address} (Alias: {deviceProperties.Alias}, RSSI: {deviceProperties.RSSI})"; } + + private static DateTime ReadCurrentTime(byte[] value) + { + if (value.Length < 7) + { + throw new Exception("7+ bytes are required for the current date time."); + } + + // https://github.com/sputnikdev/bluetooth-gatt-parser/blob/master/src/main/resources/gatt/characteristic/org.bluetooth.characteristic.date_time.xml + var year = value[0] + 256 * value[1]; + var month = value[2]; + var day = value[3]; + var hour = value[4]; + var minute = value[5]; + var second = value[6]; + + return new DateTime(year, month, day, hour, minute, second, DateTimeKind.Local); + } } } diff --git a/src/Constants.cs b/src/Constants.cs index 1d03c36..366525f 100644 --- a/src/Constants.cs +++ b/src/Constants.cs @@ -11,15 +11,22 @@ public static class BluezConstants public const string GattCharacteristicInterface = "org.bluez.GattCharacteristic1"; } - public class GattConstants + // https://www.bluetooth.com/specifications/gatt/ + + public static class GattConstants { - // https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=244369 + // Device Information public const string DeviceInformationServiceUUID = "0000180a-0000-1000-8000-00805f9b34fb"; public const string ModelNameCharacteristicUUID = "00002a24-0000-1000-8000-00805f9b34fb"; public const string ManufacturerNameCharacteristicUUID = "00002a29-0000-1000-8000-00805f9b34fb"; - // https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Services/org.bluetooth.service.battery_service.xml - public const string BatteryServiceUUID = "0000180f-0000-1000-8000-00805f9b34fb"; - public const string BatteryLevelCharacteristicUUID = "00002a19-0000-1000-8000-00805f9b34fb"; + // Current Time + public const string CurrentTimeServiceUUID = "00001805-0000-1000-8000-00805f9b34fb"; + public const string CurrentTimeCharacteristicUUID = "00002a2b-0000-1000-8000-00805f9b34fb"; + + // Battery Service + // BlueZ presents this service a separate interface, Battery1. + // public const string BatteryServiceUUID = "0000180f-0000-1000-8000-00805f9b34fb"; + // public const string BatteryLevelCharacteristicUUID = "00002a19-0000-1000-8000-00805f9b34fb"; } } diff --git a/src/EventArgs.cs b/src/EventArgs.cs index 60956e4..fd981f2 100644 --- a/src/EventArgs.cs +++ b/src/EventArgs.cs @@ -22,4 +22,15 @@ public DeviceFoundEventArgs(Device device, bool isStateChange = true) public Device Device { get; } } + + public class GattCharacteristicValueEventArgs : BlueZEventArgs + { + public GattCharacteristicValueEventArgs(byte[] value, bool isStateChange = true) + : base(isStateChange) + { + Value = value; + } + + public byte[] Value { get; } + } } \ No newline at end of file diff --git a/src/Extensions.cs b/src/Extensions.cs index 39113f5..9ec8bf0 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -76,7 +76,7 @@ public static async Task GetServiceAsync(this IDevice1 device, st return null; } - public static async Task GetCharacteristicAsync(this IGattService1 service, string characteristicUUID) + public static async Task GetCharacteristicAsync(this IGattService1 service, string characteristicUUID) { var characteristics = await BlueZManager.GetProxiesAsync(BluezConstants.GattCharacteristicInterface, service); @@ -86,7 +86,8 @@ public static async Task GetCharacteristicAsync(this IGatt // Console.WriteLine($"Checking {uuid}"); if (String.Equals(uuid, characteristicUUID, StringComparison.OrdinalIgnoreCase)) { - return characteristic; + var ch = await GattCharacteristic.CreateAsync(characteristic); + return ch; } } diff --git a/src/GattCharacteristic.cs b/src/GattCharacteristic.cs index 1939801..ef8baa9 100644 --- a/src/GattCharacteristic.cs +++ b/src/GattCharacteristic.cs @@ -5,13 +5,11 @@ namespace HashtagChris.DotNetBlueZ { - public delegate void CharacteristicChangeEventHandler(GattCharacteristic sender, CharacteristicChangeEventArgs e); - - public class CharacteristicChangeEventArgs : EventArgs - { - public PropertyChanges PropertyChanges { get; set; } - } + public delegate Task GattCharacteristicEventHandlerAsync(GattCharacteristic sender, GattCharacteristicValueEventArgs eventArgs); + /// + /// Adds events to IGattCharacteristic1. + /// public class GattCharacteristic : IGattCharacteristic1, IDisposable { ~GattCharacteristic() @@ -25,6 +23,8 @@ internal static async Task CreateAsync(IGattCharacteristic1 { m_proxy = proxy, }; + + await proxy.StartNotifyAsync(); characteristic.m_propertyWatcher = await proxy.WatchPropertiesAsync(characteristic.OnPropertyChanges); return characteristic; @@ -32,44 +32,75 @@ internal static async Task CreateAsync(IGattCharacteristic1 public void Dispose() { + Console.WriteLine("GattCharacteristic disposing."); m_propertyWatcher?.Dispose(); m_propertyWatcher = null; GC.SuppressFinalize(this); } - public event CharacteristicChangeEventHandler PropertyChange; + public event GattCharacteristicEventHandlerAsync Value + { + add + { + m_value += value; + FireEventForCurrentCharacteristicValue(); + } + remove + { + m_value -= value; + } + } public ObjectPath ObjectPath => m_proxy.ObjectPath; - public Task<(CloseSafeHandle fd, ushort mtu)> AcquireNotifyAsync(IDictionary Options) + private async void FireEventForCurrentCharacteristicValue() { - return m_proxy.AcquireNotifyAsync(Options); + try + { + var options = new Dictionary(); + var value = await m_proxy.ReadValueAsync(options); + m_value?.Invoke(this, new GattCharacteristicValueEventArgs(value, isStateChange: false)); + } + catch (Exception ex) + { + Console.WriteLine($"Error retrieving the current characteristic value: {ex}"); + } } - public Task<(CloseSafeHandle fd, ushort mtu)> AcquireWriteAsync(IDictionary Options) + private void OnPropertyChanges(PropertyChanges changes) { - return m_proxy.AcquireWriteAsync(Options); + Console.WriteLine("OnPropertyChanges called."); + + foreach (var pair in changes.Changed) + { + switch (pair.Key) + { + case "Value": + m_value?.Invoke(this, new GattCharacteristicValueEventArgs((byte[])pair.Value)); + break; + } + } } - public Task GetAllAsync() + public Task ReadValueAsync(IDictionary Options) { - return m_proxy.GetAllAsync(); + return m_proxy.ReadValueAsync(Options); } - public Task GetAsync(string prop) + public Task WriteValueAsync(byte[] Value, IDictionary Options) { - return m_proxy.GetAsync(prop); + return m_proxy.WriteValueAsync(Value, Options); } - public Task ReadValueAsync(IDictionary Options) + public Task<(CloseSafeHandle fd, ushort mtu)> AcquireWriteAsync(IDictionary Options) { - return m_proxy.ReadValueAsync(Options); + return m_proxy.AcquireWriteAsync(Options); } - public Task SetAsync(string prop, object val) + public Task<(CloseSafeHandle fd, ushort mtu)> AcquireNotifyAsync(IDictionary Options) { - return m_proxy.SetAsync(prop, val); + return m_proxy.AcquireNotifyAsync(Options); } public Task StartNotifyAsync() @@ -82,25 +113,28 @@ public Task StopNotifyAsync() return m_proxy.StopNotifyAsync(); } - public Task WatchPropertiesAsync(Action handler) + public Task GetAsync(string prop) { - return m_proxy.WatchPropertiesAsync(handler); + return m_proxy.GetAsync(prop); } - public Task WriteValueAsync(byte[] Value, IDictionary Options) + public Task GetAllAsync() { - return m_proxy.WriteValueAsync(Value, Options); + return m_proxy.GetAllAsync(); } - private void OnPropertyChanges(PropertyChanges changes) + public Task SetAsync(string prop, object val) { - PropertyChange?.Invoke(this, new CharacteristicChangeEventArgs - { - PropertyChanges = changes, - }); + return m_proxy.SetAsync(prop, val); + } + + public Task WatchPropertiesAsync(Action handler) + { + return m_proxy.WatchPropertiesAsync(handler); } private IGattCharacteristic1 m_proxy; private IDisposable m_propertyWatcher; + private event GattCharacteristicEventHandlerAsync m_value; } } \ No newline at end of file From 62262d13800c78afbe1f14e4d36fa7b10f0e1c40 Mon Sep 17 00:00:00 2001 From: Chris Sidi Date: Fri, 5 Jul 2019 20:49:55 -0400 Subject: [PATCH 5/7] Add service and characteristic arguments. --- .../Program.cs | 55 +++++++++++----- .../subscribeToCharacteristic.csproj} | 0 src/BlueZManager.cs | 18 ++++++ src/GattCharacteristic.cs | 64 ++++++++++--------- 4 files changed, 89 insertions(+), 48 deletions(-) rename demo/{printDeviceInfoEventDriven => subscribeToCharacteristic}/Program.cs (72%) rename demo/{printDeviceInfoEventDriven/printDeviceInfoEventDriven.csproj => subscribeToCharacteristic/subscribeToCharacteristic.csproj} (100%) diff --git a/demo/printDeviceInfoEventDriven/Program.cs b/demo/subscribeToCharacteristic/Program.cs similarity index 72% rename from demo/printDeviceInfoEventDriven/Program.cs rename to demo/subscribeToCharacteristic/Program.cs index ba81938..c1cb6e8 100644 --- a/demo/printDeviceInfoEventDriven/Program.cs +++ b/demo/subscribeToCharacteristic/Program.cs @@ -5,23 +5,33 @@ using HashtagChris.DotNetBlueZ; using HashtagChris.DotNetBlueZ.Extensions; -namespace printDeviceInfoEventDriven +// An event-driven example that subscribes to one GATT characteristic and prints the value on updates. +namespace subscribeToCharacteristic { class Program { - private const string ServiceUUID = GattConstants.CurrentTimeServiceUUID; - private const string CharacteristicUUID = GattConstants.CurrentTimeCharacteristicUUID; + // TODO: Is there a good characteristic that works for a wide variety of people? + // The iPhone doesn't seem to actually update current time subscribers. + private const string DefaultServiceUUID = GattConstants.CurrentTimeServiceUUID; + private const string DefaultCharacteristicUUID = GattConstants.CurrentTimeCharacteristicUUID; private static string s_deviceFilter; + private static string s_serviceUUID; + private static string s_characteristicUUID; private static TimeSpan timeout = TimeSpan.FromSeconds(15); private static async Task Main(string[] args) { - if (args.Length < 1) + if (args.Length < 1 || args.Length == 3) { - Console.WriteLine("Usage: PrintDeviceInfo | [adapterName]"); - Console.WriteLine("Example: PrintDeviceInfo phone hci1"); + Console.WriteLine("Usage: subscribeToCharacteristic | [adapterName] [serviceUUID characteristicUUID]"); + Console.WriteLine(@"Examples: + subscribeToCharacteristic phone + subscribeToCharacteristic myFirstPeripheral hci0 CAFE CFFE (see https://github.com/hashtagchris/early-iOS-BluetoothLowEnergy-tests/tree/master/myFirstPeripheral)"); + Console.WriteLine(); + Console.WriteLine($"Default service: {DefaultServiceUUID}"); + Console.WriteLine($"Default characteristic: {DefaultCharacteristicUUID}"); return; } @@ -43,12 +53,23 @@ private static async Task Main(string[] args) adapter = adapters.First(); } + s_serviceUUID = BlueZManager.NormalizeUUID(args.Length > 3 + ? args[2] + : DefaultServiceUUID); + + s_characteristicUUID = BlueZManager.NormalizeUUID(args.Length > 3 + ? args[3] + : DefaultCharacteristicUUID); + var adapterPath = adapter.ObjectPath.ToString(); var adapterName = adapterPath.Substring(adapterPath.LastIndexOf("/") + 1); Console.WriteLine($"Using Bluetooth adapter {adapterName}"); adapter.PoweredOn += adapter_PoweredOnAsync; adapter.DeviceFound += adapter_DeviceFoundAsync; + + Console.WriteLine("Waiting for events. Use Control-C to quit."); + Console.WriteLine(); await Task.Delay(-1); } @@ -176,25 +197,22 @@ private static async Task device_ServicesResolvedAsync(Device device, BlueZEvent // Console.WriteLine(uuid); // } - var service = await device.GetServiceAsync(ServiceUUID); + var service = await device.GetServiceAsync(s_serviceUUID); if (service == null) { - Console.WriteLine($"Service UUID {ServiceUUID} notfound. Do you need to pair first?"); + Console.WriteLine($"Service UUID {s_serviceUUID} not found. Do you need to pair first?"); return; } - var characteristic = await service.GetCharacteristicAsync(CharacteristicUUID); + var characteristic = await service.GetCharacteristicAsync(s_characteristicUUID); if (characteristic == null) { - Console.WriteLine($"Characteristic UUID {CharacteristicUUID} not found within service {ServiceUUID}."); + Console.WriteLine($"Characteristic UUID {s_characteristicUUID} not found within service {s_serviceUUID}."); return; } - Console.WriteLine(); + // Subscribe to the characteristic's value. Be notified of updates. characteristic.Value += characteristic_Value; - - // Console.WriteLine("Reading GATT characteristic..."); - // var valueBytes = await characteristic.ReadValueAsync(timeout); } catch (Exception ex) { @@ -207,7 +225,10 @@ private static async Task characteristic_Value(GattCharacteristic characteristic try { var uuid = await characteristic.GetUUIDAsync(); - Console.WriteLine($"UUID: {uuid}; Status change: {e.IsStateChange}"); + // Console.WriteLine($"UUID: {uuid}; Status change: {e.IsStateChange}"); + + // Print the characteristic value + Console.WriteLine(); if (String.Equals(uuid, GattConstants.CurrentTimeCharacteristicUUID, StringComparison.OrdinalIgnoreCase)) { var currentTime = ReadCurrentTime(e.Value); @@ -216,11 +237,11 @@ private static async Task characteristic_Value(GattCharacteristic characteristic else { // Default - Console.WriteLine($"Characteristic value (hex): {BitConverter.ToString(e.Value)}"); + Console.WriteLine($"[{DateTime.Now}] Characteristic value (hex): {BitConverter.ToString(e.Value)}"); try { var stringValue = Encoding.UTF8.GetString(e.Value); - Console.WriteLine($"Characteristic value (UTF-8): \"{stringValue}\""); + Console.WriteLine($"[{DateTime.Now}] Characteristic value (UTF-8): \"{stringValue}\""); } catch (Exception) {} } diff --git a/demo/printDeviceInfoEventDriven/printDeviceInfoEventDriven.csproj b/demo/subscribeToCharacteristic/subscribeToCharacteristic.csproj similarity index 100% rename from demo/printDeviceInfoEventDriven/printDeviceInfoEventDriven.csproj rename to demo/subscribeToCharacteristic/subscribeToCharacteristic.csproj diff --git a/src/BlueZManager.cs b/src/BlueZManager.cs index 0515076..8fa5dcc 100644 --- a/src/BlueZManager.cs +++ b/src/BlueZManager.cs @@ -32,6 +32,24 @@ public static async Task> GetAdaptersAsync() return await Task.WhenAll(adapters.Select(Adapter.CreateAsync)); } + // Normalize a 16, 32 or 128 bit UUID. + public static string NormalizeUUID(string uuid) + { + // TODO: Improve this validation. + if (uuid.Length == 4) { + return $"0000{uuid}-0000-1000-8000-00805f9b34fb".ToLowerInvariant(); + } + else if (uuid.Length == 8) { + return $"{uuid}-0000-1000-8000-00805f9b34fb".ToLowerInvariant(); + } + else if (uuid.Length == 36) { + return uuid.ToLowerInvariant(); + } + else { + throw new ArgumentException($"'{uuid}' isn't a valid 16, 32 or 128 bit UUID."); + } + } + /// The interface to search for /// The DBus object to search under. Can be null internal static async Task> GetProxiesAsync(string interfaceName, IDBusObject rootObject) diff --git a/src/GattCharacteristic.cs b/src/GattCharacteristic.cs index ef8baa9..bcb53bb 100644 --- a/src/GattCharacteristic.cs +++ b/src/GattCharacteristic.cs @@ -24,7 +24,6 @@ internal static async Task CreateAsync(IGattCharacteristic1 m_proxy = proxy, }; - await proxy.StartNotifyAsync(); characteristic.m_propertyWatcher = await proxy.WatchPropertiesAsync(characteristic.OnPropertyChanges); return characteristic; @@ -44,7 +43,9 @@ public event GattCharacteristicEventHandlerAsync Value add { m_value += value; - FireEventForCurrentCharacteristicValue(); + + // Subscribe here instead of CreateAsync, because not all GATT characteristics are notifable. + SubscribeAndReadCurrentValue(); } remove { @@ -54,35 +55,6 @@ public event GattCharacteristicEventHandlerAsync Value public ObjectPath ObjectPath => m_proxy.ObjectPath; - private async void FireEventForCurrentCharacteristicValue() - { - try - { - var options = new Dictionary(); - var value = await m_proxy.ReadValueAsync(options); - m_value?.Invoke(this, new GattCharacteristicValueEventArgs(value, isStateChange: false)); - } - catch (Exception ex) - { - Console.WriteLine($"Error retrieving the current characteristic value: {ex}"); - } - } - - private void OnPropertyChanges(PropertyChanges changes) - { - Console.WriteLine("OnPropertyChanges called."); - - foreach (var pair in changes.Changed) - { - switch (pair.Key) - { - case "Value": - m_value?.Invoke(this, new GattCharacteristicValueEventArgs((byte[])pair.Value)); - break; - } - } - } - public Task ReadValueAsync(IDictionary Options) { return m_proxy.ReadValueAsync(Options); @@ -133,6 +105,36 @@ public Task WatchPropertiesAsync(Action handler) return m_proxy.WatchPropertiesAsync(handler); } + private async void SubscribeAndReadCurrentValue() + { + try + { + await m_proxy.StartNotifyAsync(); + + // Reading the current value will trigger OnPropertyChanges. + var options = new Dictionary(); + var value = await m_proxy.ReadValueAsync(options); + } + catch (Exception ex) + { + Console.WriteLine($"Error retrieving the current characteristic value: {ex}"); + } + } + + private void OnPropertyChanges(PropertyChanges changes) + { + // Console.WriteLine("OnPropertyChanges called."); + foreach (var pair in changes.Changed) + { + switch (pair.Key) + { + case "Value": + m_value?.Invoke(this, new GattCharacteristicValueEventArgs((byte[])pair.Value)); + break; + } + } + } + private IGattCharacteristic1 m_proxy; private IDisposable m_propertyWatcher; private event GattCharacteristicEventHandlerAsync m_value; From 7847d6e1af331a4ba02e83c60190e349068c7bdc Mon Sep 17 00:00:00 2001 From: Chris Sidi Date: Fri, 5 Jul 2019 22:04:44 -0400 Subject: [PATCH 6/7] Try ANCS --- demo/subscribeToCharacteristic/Program.cs | 89 ++++++++++++++++------- src/Constants.cs | 9 +++ src/EventArgs.cs | 5 +- src/GattCharacteristic.cs | 13 ++-- 4 files changed, 82 insertions(+), 34 deletions(-) diff --git a/demo/subscribeToCharacteristic/Program.cs b/demo/subscribeToCharacteristic/Program.cs index c1cb6e8..93af09b 100644 --- a/demo/subscribeToCharacteristic/Program.cs +++ b/demo/subscribeToCharacteristic/Program.cs @@ -11,9 +11,10 @@ namespace subscribeToCharacteristic class Program { // TODO: Is there a good characteristic that works for a wide variety of people? - // The iPhone doesn't seem to actually update current time subscribers. - private const string DefaultServiceUUID = GattConstants.CurrentTimeServiceUUID; - private const string DefaultCharacteristicUUID = GattConstants.CurrentTimeCharacteristicUUID; + // Battery level doesn't work because BlueZ gives that a separate interface. + // Current time seems promising, but the iPhone doesn't seem to notify current time subscribers. + private const string DefaultServiceUUID = GattConstants.ANCServiceUUID; + private const string DefaultCharacteristicUUID = GattConstants.ANCSNotificationSourceUUID; private static string s_deviceFilter; private static string s_serviceUUID; @@ -28,7 +29,7 @@ private static async Task Main(string[] args) Console.WriteLine("Usage: subscribeToCharacteristic | [adapterName] [serviceUUID characteristicUUID]"); Console.WriteLine(@"Examples: subscribeToCharacteristic phone - subscribeToCharacteristic myFirstPeripheral hci0 CAFE CFFE (see https://github.com/hashtagchris/early-iOS-BluetoothLowEnergy-tests/tree/master/myFirstPeripheral)"); + subscribeToCharacteristic 8C:8E:F2:AB:73:76 hci0 CAFE CFFE (see https://github.com/hashtagchris/early-iOS-BluetoothLowEnergy-tests/tree/master/myFirstPeripheral)"); Console.WriteLine(); Console.WriteLine($"Default service: {DefaultServiceUUID}"); Console.WriteLine($"Default characteristic: {DefaultCharacteristicUUID}"); @@ -211,8 +212,25 @@ private static async Task device_ServicesResolvedAsync(Device device, BlueZEvent return; } + Console.WriteLine(); + // Subscribe to the characteristic's value. Be notified of updates. characteristic.Value += characteristic_Value; + + // Attempt to read the current value. Some characteristics only support Notify. + byte[] value; + try + { + Console.WriteLine("Reading current characteristic value..."); + value = await characteristic.GetValueAsync(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error reading characteristic value: {ex.Message}"); + return; + } + + PrintCharacteristicValue(s_characteristicUUID, value); } catch (Exception ex) { @@ -225,26 +243,7 @@ private static async Task characteristic_Value(GattCharacteristic characteristic try { var uuid = await characteristic.GetUUIDAsync(); - // Console.WriteLine($"UUID: {uuid}; Status change: {e.IsStateChange}"); - - // Print the characteristic value - Console.WriteLine(); - if (String.Equals(uuid, GattConstants.CurrentTimeCharacteristicUUID, StringComparison.OrdinalIgnoreCase)) - { - var currentTime = ReadCurrentTime(e.Value); - Console.WriteLine($"Current time: {currentTime}"); - } - else - { - // Default - Console.WriteLine($"[{DateTime.Now}] Characteristic value (hex): {BitConverter.ToString(e.Value)}"); - try - { - var stringValue = Encoding.UTF8.GetString(e.Value); - Console.WriteLine($"[{DateTime.Now}] Characteristic value (UTF-8): \"{stringValue}\""); - } - catch (Exception) {} - } + PrintCharacteristicValue(uuid, e.Value); } catch (Exception ex) { @@ -252,17 +251,57 @@ private static async Task characteristic_Value(GattCharacteristic characteristic } } + private static void PrintCharacteristicValue(string uuid, byte[] value) + { + if (String.Equals(uuid, GattConstants.CurrentTimeCharacteristicUUID, StringComparison.OrdinalIgnoreCase)) + { + var currentTime = ReadCurrentTime(value); + Console.WriteLine($"Current time: {currentTime}"); + } + else if (String.Equals(uuid, GattConstants.ANCSNotificationSourceUUID, StringComparison.OrdinalIgnoreCase)) + { + PrintAncsDescription(value); + } + else + { + // Default + Console.WriteLine($"[{DateTime.Now}] Characteristic value (hex): {BitConverter.ToString(value)}"); + try + { + var stringValue = Encoding.UTF8.GetString(value); + Console.WriteLine($"[{DateTime.Now}] Characteristic value (UTF-8): \"{stringValue}\""); + } + catch (Exception) {} + } + } + private static async Task GetDeviceDescriptionAsync(IDevice1 device) { var deviceProperties = await device.GetAllAsync(); return $"{deviceProperties.Address} (Alias: {deviceProperties.Alias}, RSSI: {deviceProperties.RSSI})"; } + private static void PrintAncsDescription(byte[] value) + { + if (value.Length < 8) + { + throw new ArgumentException("8 bytes are required for ANCS notifications."); + } + + var eventIds = new string[] { "added", "modified", "removed" }; + var categoryIds = new string[] { "Other", "IncomingCall", "MissedCall", "Voicemail", "Social", "Schedule", "Email", "News", "Health & Fitness", "Business & Finance", "Location", "Entertainment" }; + + byte[] notificationUid = new byte[4]; + Array.Copy(value, 4, notificationUid, 0, 4); + + Console.WriteLine($"{categoryIds[value[2]]} notification {eventIds[value[0]]} (Count: {value[3]}) (UID: {BitConverter.ToString(notificationUid)})"); + } + private static DateTime ReadCurrentTime(byte[] value) { if (value.Length < 7) { - throw new Exception("7+ bytes are required for the current date time."); + throw new ArgumentException("7+ bytes are required for the current date time."); } // https://github.com/sputnikdev/bluetooth-gatt-parser/blob/master/src/main/resources/gatt/characteristic/org.bluetooth.characteristic.date_time.xml diff --git a/src/Constants.cs b/src/Constants.cs index 366525f..08ae230 100644 --- a/src/Constants.cs +++ b/src/Constants.cs @@ -28,5 +28,14 @@ public static class GattConstants // BlueZ presents this service a separate interface, Battery1. // public const string BatteryServiceUUID = "0000180f-0000-1000-8000-00805f9b34fb"; // public const string BatteryLevelCharacteristicUUID = "00002a19-0000-1000-8000-00805f9b34fb"; + + // Apple Notification Center Service (ANCS) + // https://developer.apple.com/library/ios/documentation/CoreBluetooth/Reference/AppleNotificationCenterServiceSpecification/Introduction/Introduction.html + public const string ANCServiceUUID = "7905f431-b5ce-4e99-a40f-4b1e122d00d0"; + + // TODO: Lowercase these. + public const string ANCSNotificationSourceUUID = "9FBF120D-6301-42D9-8C58-25E699A21DBD"; + public const string ANCSControlPointUUID = "69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9"; + public const string ANCSDataSourceUUID = "22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB"; } } diff --git a/src/EventArgs.cs b/src/EventArgs.cs index fd981f2..1698a3e 100644 --- a/src/EventArgs.cs +++ b/src/EventArgs.cs @@ -23,10 +23,9 @@ public DeviceFoundEventArgs(Device device, bool isStateChange = true) public Device Device { get; } } - public class GattCharacteristicValueEventArgs : BlueZEventArgs + public class GattCharacteristicValueEventArgs : EventArgs { - public GattCharacteristicValueEventArgs(byte[] value, bool isStateChange = true) - : base(isStateChange) + public GattCharacteristicValueEventArgs(byte[] value) { Value = value; } diff --git a/src/GattCharacteristic.cs b/src/GattCharacteristic.cs index bcb53bb..2006028 100644 --- a/src/GattCharacteristic.cs +++ b/src/GattCharacteristic.cs @@ -45,7 +45,7 @@ public event GattCharacteristicEventHandlerAsync Value m_value += value; // Subscribe here instead of CreateAsync, because not all GATT characteristics are notifable. - SubscribeAndReadCurrentValue(); + Subscribe(); } remove { @@ -105,19 +105,20 @@ public Task WatchPropertiesAsync(Action handler) return m_proxy.WatchPropertiesAsync(handler); } - private async void SubscribeAndReadCurrentValue() + private async void Subscribe() { try { await m_proxy.StartNotifyAsync(); - // Reading the current value will trigger OnPropertyChanges. - var options = new Dictionary(); - var value = await m_proxy.ReadValueAsync(options); + // Is there a way to check if a characteristic supports Read? + // // Reading the current value will trigger OnPropertyChanges. + // var options = new Dictionary(); + // var value = await m_proxy.ReadValueAsync(options); } catch (Exception ex) { - Console.WriteLine($"Error retrieving the current characteristic value: {ex}"); + Console.Error.WriteLine($"Error subscribing to characteristic value: {ex}"); } } From 4e506374f6e5c403efac3480fd51dcf8021685b0 Mon Sep 17 00:00:00 2001 From: Chris Sidi Date: Fri, 5 Jul 2019 22:34:33 -0400 Subject: [PATCH 7/7] Add events to Readme --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3fd8bc6..ccc5e30 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ Uses [Tmds.DBus](https://github.com/tmds/Tmds.DBus) to access D-Bus. Tmds.DBus.T dotnet add package HashtagChris.DotNetBlueZ --version 1.1.0-alpha ``` +# Events + +C# events are available for several properties. Events are useful for properly handling disconnects and reconnects. + # Usage ## Get a Bluetooth adapter @@ -36,40 +40,52 @@ IAdapter1 adapter = await BlueZManager.GetAdapterAsync(adapterName: "hci0"); ## Scan for Bluetooth devices ```C# +adapter.DeviceFound += adapter_DeviceFoundAsync; + await adapter.StartDiscoveryAsync(); ... await adapter.StopDiscoveryAsync(); ``` -You can optionally use the extension method `IAdapter1.WatchDevicesAddedAsync` to monitor for new devices being found during the scan. - ## Get Devices +`adapter.DeviceFound` (above) will be called immediately for existing devices, and as new devices show up during scanning; `eventArgs.IsStateChange` can be used to distinguish between existing and new devices. Alternatively you can can use `GetDevicesAsync`: + ```C# -IReadOnlyList devices = await adapter.GetDevicesAsync(); +IReadOnlyList devices = await adapter.GetDevicesAsync(); ``` ## Connect to a Device +```C# +device.Connected += device_ConnectedAsync; +device.Disconnected += device_DisconnectedAsync; +device.ServicesResolved += device_ServicesResolvedAsync; + +await device.ConnectAsync(); +``` + +Alternatively you can wait for "Connected" and "ServicesResolved" to equal true: + ```C# TimeSpan timeout = TimeSpan.FromSeconds(15); await device.ConnectAsync(); await device.WaitForPropertyValueAsync("Connected", value: true, timeout); +await device.WaitForPropertyValueAsync("ServicesResolved", value: true, timeout); + ``` ## Retrieve a GATT Service and Characteristic +Prerequisite: You must be connected to a device and services must be resolved. You may need to pair with the device in order to use some services. + Example using GATT Device Information Service UUIDs. ```C# string serviceUUID = "0000180a-0000-1000-8000-00805f9b34fb"; string characteristicUUID = "00002a24-0000-1000-8000-00805f9b34fb"; -TimeSpan timeout = TimeSpan.FromSeconds(15); - -await device.WaitForPropertyValueAsync("ServicesResolved", value: true, timeout); - IGattService1 service = await device.GetServiceAsync(serviceUUID); IGattCharacteristic1 characteristic = await service.GetCharacteristicAsync(characteristicUUID); ``` @@ -82,6 +98,28 @@ byte[] value = await characteristic.ReadValueAsync(timeout); string modelName = Encoding.UTF8.GetString(value); ``` +## Subscribe to GATT Characteristic Notifications + +```C# +characteristic.Value += characteristic_Value; +... + +private static async Task characteristic_Value(GattCharacteristic characteristic, GattCharacteristicValueEventArgs e) +{ + try + { + Console.WriteLine($"Characteristic value (hex): {BitConverter.ToString(e.Value)}"); + + Console.WriteLine($"Characteristic value (UTF-8): \"{Encoding.UTF8.GetString(e.Value)}\""); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + } +} + +``` + # Reference * [BlueZ D-Bus API docs](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc)