diff --git a/DotnetBleServer.sln b/DotnetBleServer.sln index 1ab3771..6d8b6ec 100755 --- a/DotnetBleServer.sln +++ b/DotnetBleServer.sln @@ -1,26 +1,100 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29418.71 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34316.72 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples", "Examples\Examples.csproj", "{AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Server", "Examples\Sample.Server.csproj", "{AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetBleServer", "DotnetBleServer\DotnetBleServer.csproj", "{0D775919-ECF4-4C71-92ED-D7D70D9BF59F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Common", "Sample.Client\Sample.Common.csproj", "{8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Client.Win", "Sample.Client.Win\Sample.Client.Win.csproj", "{6D2973EE-F461-4A6A-990C-54F191FFD19C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Debug|ARM64.Build.0 = Debug|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Debug|x64.Build.0 = Debug|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Debug|x86.Build.0 = Debug|Any CPU {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Release|Any CPU.Build.0 = Release|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Release|ARM64.ActiveCfg = Release|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Release|ARM64.Build.0 = Release|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Release|x64.ActiveCfg = Release|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Release|x64.Build.0 = Release|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Release|x86.ActiveCfg = Release|Any CPU + {AE3AAA32-DFE6-4CAD-BE3D-0CFBA5C75EC4}.Release|x86.Build.0 = Release|Any CPU {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Debug|ARM64.Build.0 = Debug|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Debug|x64.ActiveCfg = Debug|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Debug|x64.Build.0 = Debug|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Debug|x86.ActiveCfg = Debug|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Debug|x86.Build.0 = Debug|Any CPU {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Release|Any CPU.Build.0 = Release|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Release|ARM64.ActiveCfg = Release|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Release|ARM64.Build.0 = Release|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Release|x64.ActiveCfg = Release|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Release|x64.Build.0 = Release|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Release|x86.ActiveCfg = Release|Any CPU + {0D775919-ECF4-4C71-92ED-D7D70D9BF59F}.Release|x86.Build.0 = Release|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Debug|ARM64.Build.0 = Debug|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Debug|x64.Build.0 = Debug|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Debug|x86.Build.0 = Debug|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Release|Any CPU.Build.0 = Release|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Release|ARM64.ActiveCfg = Release|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Release|ARM64.Build.0 = Release|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Release|x64.ActiveCfg = Release|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Release|x64.Build.0 = Release|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Release|x86.ActiveCfg = Release|Any CPU + {8A7F5783-4ADB-4BE2-8D48-4FA383F166CE}.Release|x86.Build.0 = Release|Any CPU + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|Any CPU.ActiveCfg = Debug|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|Any CPU.Build.0 = Debug|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|Any CPU.Deploy.0 = Debug|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|ARM64.Build.0 = Debug|ARM64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|x64.ActiveCfg = Debug|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|x64.Build.0 = Debug|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|x64.Deploy.0 = Debug|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|x86.ActiveCfg = Debug|x86 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|x86.Build.0 = Debug|x86 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Debug|x86.Deploy.0 = Debug|x86 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|Any CPU.ActiveCfg = Release|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|Any CPU.Build.0 = Release|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|Any CPU.Deploy.0 = Release|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|ARM64.ActiveCfg = Release|ARM64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|ARM64.Build.0 = Release|ARM64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|ARM64.Deploy.0 = Release|ARM64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|x64.ActiveCfg = Release|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|x64.Build.0 = Release|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|x64.Deploy.0 = Release|x64 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|x86.ActiveCfg = Release|x86 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|x86.Build.0 = Release|x86 + {6D2973EE-F461-4A6A-990C-54F191FFD19C}.Release|x86.Deploy.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DotnetBleServer/Advertisements/Advertisement.cs b/DotnetBleServer/Advertisements/Advertisement.cs index 1bd0422..cb5f991 100755 --- a/DotnetBleServer/Advertisements/Advertisement.cs +++ b/DotnetBleServer/Advertisements/Advertisement.cs @@ -1,15 +1,39 @@ -using System; +using DotnetBleServer.Core; +using DotnetBleServer.Gatt.BlueZModel; +using System; +using System.Collections.Generic; using System.Threading.Tasks; -using DotnetBleServer.Core; namespace DotnetBleServer.Advertisements { - public class Advertisement : PropertiesBase, ILEAdvertisement1 + public class Advertisement : PropertiesBase, ILEAdvertisement1, IObjectManagerProperties { - public Advertisement(string objectPath, AdvertisementProperties properties) : base(objectPath, properties) + public Advertisement(string objectPath, LEAdvertisement1Properties properties) : base(objectPath, properties) { } + /// + /// TODO: Keep or remove ? + /// Not sure it is mandatory for LEAdvertisement1 + /// Should try setting properties without implementing IObjectManagerProperties + /// + /// + public IDictionary> GetProperties() + { + return new Dictionary> + { + { + "org.bluez.LEAdvertisement1", new Dictionary + { + {"LocalName", Properties.LocalName}, + {"Type", Properties.Type}, + {"ServiceUUIDs", Properties.ServiceUUIDs}, + {"Appearance", Properties.Appearance} + } + } + }; + } + public Task ReleaseAsync() { throw new NotImplementedException(); diff --git a/DotnetBleServer/Advertisements/AdvertisementProperties.cs b/DotnetBleServer/Advertisements/AdvertisementProperties.cs deleted file mode 100755 index f991be1..0000000 --- a/DotnetBleServer/Advertisements/AdvertisementProperties.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Tmds.DBus; - -namespace DotnetBleServer.Advertisements -{ - [Dictionary] - [SuppressMessage("ReSharper", "ConvertToAutoProperty")] - [SuppressMessage("ReSharper", "UnusedMember.Global")] - public class AdvertisementProperties - { - private string _Type; - public string Type - { - get => _Type; - set => _Type = value; - } - - private string[] _ServiceUUIDs; - public string[] ServiceUUIDs - { - get => _ServiceUUIDs; - set => _ServiceUUIDs = value; - } - - private IDictionary _ManufacturerData; - public IDictionary ManufacturerData - { - get => _ManufacturerData; - set => _ManufacturerData = value; - } - - private string[] _SolicitUUIDs; - public string[] SolicitUUIDs - { - get => _SolicitUUIDs; - set => _SolicitUUIDs = value; - } - - private IDictionary _ServiceData; - public IDictionary ServiceData - { - get => _ServiceData; - set => _ServiceData = value; - } - - private bool _IncludeTxPower; - public bool IncludeTxPower - { - get => _IncludeTxPower; - set => _IncludeTxPower = value; - } - - private string _LocalName; - public string LocalName - { - get => _LocalName; - set => _LocalName = value; - } - - } -} \ No newline at end of file diff --git a/DotnetBleServer/Advertisements/AdvertisingManager.cs b/DotnetBleServer/Advertisements/AdvertisingManager.cs index a8989d5..db24c3e 100755 --- a/DotnetBleServer/Advertisements/AdvertisingManager.cs +++ b/DotnetBleServer/Advertisements/AdvertisingManager.cs @@ -1,40 +1,39 @@ -using System; +using DotnetBleServer.Core; +using DotnetBleServer.Gatt.BlueZModel; +using System; using System.Collections.Generic; using System.Threading.Tasks; -using DotnetBleServer.Core; using Tmds.DBus; namespace DotnetBleServer.Advertisements { public class AdvertisingManager { - private readonly ServerContext _Context; + private readonly ServerContext _context; public AdvertisingManager(ServerContext context) { - _Context = context; + _context = context; } public async Task RegisterAdvertisement(Advertisement advertisement) { - await _Context.Connection.RegisterObjectAsync(advertisement); + await _context.Connection.RegisterObjectAsync(advertisement); Console.WriteLine($"advertisement object {advertisement.ObjectPath} created"); - await GetAdvertisingManager().RegisterAdvertisementAsync(((IDBusObject) advertisement).ObjectPath, - new Dictionary()); - + await GetAdvertisingManager().RegisterAdvertisementAsync(((IDBusObject)advertisement).ObjectPath, new Dictionary()); Console.WriteLine($"advertisement {advertisement.ObjectPath} registered in BlueZ advertising manager"); } private ILEAdvertisingManager1 GetAdvertisingManager() { - return _Context.Connection.CreateProxy("org.bluez", "/org/bluez/hci0"); + return _context.Connection.CreateProxy(Constants.DbusServicePath, _context.Adapter.ObjectPath); } - public async Task CreateAdvertisement(AdvertisementProperties advertisementProperties) + public async Task CreateAdvertisement(LEAdvertisement1Properties advertisementProperties) { - var advertisement = new Advertisement("/org/bluez/example/advertisement0", advertisementProperties); - await new AdvertisingManager(_Context).RegisterAdvertisement(advertisement); + var advertisement = new Advertisement($"{_context.Adapter.ObjectPath}/advertisement0", advertisementProperties); + await new AdvertisingManager(_context).RegisterAdvertisement(advertisement); } } } \ No newline at end of file diff --git a/DotnetBleServer/Advertisements/ILEAdvertisement1.cs b/DotnetBleServer/Advertisements/ILEAdvertisement1.cs deleted file mode 100755 index a205490..0000000 --- a/DotnetBleServer/Advertisements/ILEAdvertisement1.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Threading.Tasks; -using Tmds.DBus; - -namespace DotnetBleServer.Advertisements -{ - [DBusInterface("org.bluez.LEAdvertisement1")] - internal interface ILEAdvertisement1 : IDBusObject - { - Task ReleaseAsync(); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } -} \ No newline at end of file diff --git a/DotnetBleServer/Core/BlueZ.cs b/DotnetBleServer/Core/BlueZ.cs index f6417c1..8b73191 100755 --- a/DotnetBleServer/Core/BlueZ.cs +++ b/DotnetBleServer/Core/BlueZ.cs @@ -36,7 +36,7 @@ interface IHealthManager1 : IDBusObject } [DBusInterface("org.bluez.Adapter1")] - interface IAdapter1 : IDBusObject + public interface IAdapter1 : IDBusObject { Task StartDiscoveryAsync(); Task SetDiscoveryFilterAsync(IDictionary Properties); @@ -50,7 +50,7 @@ interface IAdapter1 : IDBusObject } [Dictionary] - class Adapter1Properties + public class Adapter1Properties { private string _Address = default (string); public string Address @@ -235,7 +235,7 @@ public string Modalias } } - static class Adapter1Extensions + public static class Adapter1Extensions { public static Task GetAddressAsync(this IAdapter1 o) => o.GetAsync("Address"); public static Task GetAddressTypeAsync(this IAdapter1 o) => o.GetAsync("AddressType"); @@ -266,7 +266,7 @@ interface IGattManager1 : IDBusObject } [DBusInterface("org.bluez.LEAdvertisingManager1")] - interface ILEAdvertisingManager1 : IDBusObject + public interface ILEAdvertisingManager1 : IDBusObject { Task RegisterAdvertisementAsync(ObjectPath Advertisement, IDictionary Options); Task UnregisterAdvertisementAsync(ObjectPath Service); @@ -277,7 +277,7 @@ interface ILEAdvertisingManager1 : IDBusObject } [Dictionary] - class LEAdvertisingManager1Properties + public class LEAdvertisingManager1Properties { private byte _ActiveInstances = default (byte); public byte ActiveInstances diff --git a/DotnetBleServer/Core/Constants.cs b/DotnetBleServer/Core/Constants.cs new file mode 100644 index 0000000..bfe8f82 --- /dev/null +++ b/DotnetBleServer/Core/Constants.cs @@ -0,0 +1,8 @@ +namespace DotnetBleServer.Core +{ + public static class Constants + { + public const string DbusServicePath = "org.bluez"; + public const string AdapterInterfacePath = "org.bluez.Adapter1"; + } +} diff --git a/DotnetBleServer/Core/ServerContext.cs b/DotnetBleServer/Core/ServerContext.cs index 8f659d8..d170a49 100755 --- a/DotnetBleServer/Core/ServerContext.cs +++ b/DotnetBleServer/Core/ServerContext.cs @@ -1,26 +1,88 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Tmds.DBus; namespace DotnetBleServer.Core { - public class ServerContext : IDisposable + public class ServerContext : ServerContextBase { - public ServerContext() + public IAdapter1 Adapter { get; set; } + + public async Task ConnectAndSetDefaultAdapter() { - Connection = new Connection(Address.System); + await Connect(); + + var adapters = await GetAdaptersAsync(); + if (adapters.Count == 0) + { + throw new Exception("No Bluetooth adapters found."); + } + + Adapter = adapters[0]; } - public async Task Connect() + /// Get Bluetooth Adapter object based on name. + /// Adapter Name. + /// False allows shortened adapter name ('hci0'). True, for full name ('/org/bluez/hci0'). + /// Adapter object + /// + public async Task GetAdapterAsync(string adapterName, bool fullName = false) { - await Connection.ConnectAsync(); + var adapterObjectPath = !fullName ? $"/org/bluez/{adapterName}" : adapterName; + var adapter = Connection.CreateProxy(Constants.DbusServicePath, adapterObjectPath); + + try + { + await adapter.GetAliasAsync(); + } + catch (Exception) + { + throw new Exception($"Bluetooth adapter {adapterName} not found."); + } + + return adapter; } - public Connection Connection { get; } + public async Task> GetAdaptersAsync() + { + return await GetProxiesAsync(Constants.AdapterInterfacePath, rootObject: null); + } - public void Dispose() + /// The interface to search for + /// The DBus object to search under. Can be null + internal async Task> GetProxiesAsync(string interfaceName, IDBusObject rootObject) { - Connection.Dispose(); + // Console.WriteLine("GetProxiesAsync called."); + var objectManager = Connection.CreateProxy(Constants.DbusServicePath, "/"); + var objects = await objectManager.GetManagedObjectsAsync(); + + var matchingObjectPaths = objects + .Where(obj => IsMatch(interfaceName, obj.Key, obj.Value, rootObject)) + .Select(obj => obj.Key); + + var proxies = matchingObjectPaths + .Select(objectPath => Connection.CreateProxy(Constants.DbusServicePath, objectPath)) + .ToList(); + + //Console.WriteLine($"GetProxiesAsync returning {proxies.Count} proxies of type {typeof(T)}."); + return proxies; + } + + internal static bool IsMatch(string interfaceName, ObjectPath objectPath, IDictionary> interfaces, IDBusObject rootObject) + { + return IsMatch(interfaceName, objectPath, interfaces.Keys, rootObject); + } + + internal static bool IsMatch(string interfaceName, ObjectPath objectPath, ICollection interfaces, IDBusObject rootObject) + { + if (rootObject != null && !objectPath.ToString().StartsWith($"{rootObject.ObjectPath}/")) + { + return false; + } + + return interfaces.Contains(interfaceName); } } } \ No newline at end of file diff --git a/DotnetBleServer/Core/ServerContextBase.cs b/DotnetBleServer/Core/ServerContextBase.cs new file mode 100644 index 0000000..79e34f0 --- /dev/null +++ b/DotnetBleServer/Core/ServerContextBase.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading.Tasks; +using Tmds.DBus; + +namespace DotnetBleServer.Core +{ + public class ServerContextBase : IDisposable + { + public ServerContextBase() + { + Connection = new Connection(Address.System); + } + + public async Task Connect() + { + await Connection.ConnectAsync(); + } + + public Connection Connection { get; } + + public void Dispose() + { + Connection.Dispose(); + } + } +} \ No newline at end of file diff --git a/DotnetBleServer/DotnetBleServer - Backup.csproj b/DotnetBleServer/DotnetBleServer - Backup.csproj new file mode 100644 index 0000000..e7f7517 --- /dev/null +++ b/DotnetBleServer/DotnetBleServer - Backup.csproj @@ -0,0 +1,36 @@ + + + + net7.0 + + + BluetoothLE.Server.Linux + 1.0.0 + G.D.M + + Bluetooth Advertising and GATT server functionality on Linux using .NET + + Fork of .NET BLE Server + + It uses BlueZ and Tmds.DBus. The provided API aims to preserve the object structure of BlueZ whilst minimizing the aspects of the D-Bus communication. + + Roman Scheidegger + https://github.com/CarteKiwi/dotnet-ble-server + https://github.com/CarteKiwi/dotnet-ble-server + MIT + icon_small.png + bluetooth, bluez, ble, .net, linux, uno, dotnet + + + + + True + \ + + + + + + + + \ No newline at end of file diff --git a/DotnetBleServer/DotnetBleServer.csproj b/DotnetBleServer/DotnetBleServer.csproj index 5ea5864..fe68093 100755 --- a/DotnetBleServer/DotnetBleServer.csproj +++ b/DotnetBleServer/DotnetBleServer.csproj @@ -1,25 +1,50 @@  - netstandard2.0 + net7.0 - DotnetBleServer - 0.1.0-snapshot - phylomeno + BluetoothLE.Server.Linux + 1.0.0 + G.D.M - .NET BLE Server is a library to support creation of BLE peripherals on Linux using .NET Core. + Bluetooth Advertising and GATT server functionality on Linux using .NET - Under the hood the library uses BlueZ. The provided API aims to preserve the object structure of BlueZ whilst minimizing the aspects of the D-Bus communication. + Fork of .NET BLE Server + + It uses BlueZ and Tmds.DBus. The provided API aims to preserve the object structure of BlueZ whilst minimizing the aspects of the D-Bus communication. Roman Scheidegger - https://github.com/phylomeno/dotnet-gatt-server - https://github.com/phylomeno/dotnet-gatt-server + https://github.com/CarteKiwi/dotnet-ble-server + https://github.com/CarteKiwi/dotnet-ble-server MIT + icon_small.png + bluetooth, bluez, ble, .net, linux, uno, dotnet + True + Bluetooth Low Energy (BLE) server for .NET on Linux (Bluez wrapper) + README.md - + + True + \ + + + True + \ + + + + + + + + + + True + \ + \ No newline at end of file diff --git a/DotnetBleServer/Gatt/BlueZModel/BlueZGatt.cs b/DotnetBleServer/Gatt/BlueZModel/BlueZGatt.cs index 20d715c..9a3ce4b 100755 --- a/DotnetBleServer/Gatt/BlueZModel/BlueZGatt.cs +++ b/DotnetBleServer/Gatt/BlueZModel/BlueZGatt.cs @@ -19,7 +19,7 @@ internal interface IGattService1 : IDBusObject [Dictionary] internal class GattService1Properties { - private string _UUID = default (string); + private string _UUID = default(string); public string UUID { get @@ -33,7 +33,7 @@ public string UUID } } - private bool _Primary = default (bool); + private bool _Primary = default(bool); public bool Primary { get @@ -47,7 +47,7 @@ public bool Primary } } - private ObjectPath[] _Characteristics = default (ObjectPath[]); + private ObjectPath[] _Characteristics = default(ObjectPath[]); public ObjectPath[] Characteristics { get @@ -78,7 +78,7 @@ interface IGattCharacteristic1 : IDBusObject [Dictionary] public class GattCharacteristic1Properties { - private string _UUID = default (string); + private string _UUID = default(string); public string UUID { get @@ -92,7 +92,7 @@ public string UUID } } - private ObjectPath _Service = default (ObjectPath); + private ObjectPath _Service = default(ObjectPath); public ObjectPath Service { get @@ -106,7 +106,7 @@ public ObjectPath Service } } - private byte[] _Value = default (byte[]); + private byte[] _Value = default(byte[]); public byte[] Value { get @@ -120,7 +120,7 @@ public byte[] Value } } - private bool _Notifying = default (bool); + private bool _Notifying = default(bool); public bool Notifying { get @@ -134,7 +134,7 @@ public bool Notifying } } - private string[] _Flags = default (string[]); + private string[] _Flags = default(string[]); public string[] Flags { get @@ -162,7 +162,7 @@ interface IGattDescriptor1 : IDBusObject [Dictionary] public class GattDescriptor1Properties { - private string _UUID = default (string); + private string _UUID = default(string); public string UUID { get @@ -176,7 +176,7 @@ public string UUID } } - private ObjectPath _Characteristic = default (ObjectPath); + private ObjectPath _Characteristic = default(ObjectPath); public ObjectPath Characteristic { get @@ -190,7 +190,7 @@ public ObjectPath Characteristic } } - private byte[] _Value = default (byte[]); + private byte[] _Value = default(byte[]); public byte[] Value { get @@ -204,7 +204,7 @@ public byte[] Value } } - private string[] _Flags = default (string[]); + private string[] _Flags = default(string[]); public string[] Flags { get @@ -220,110 +220,90 @@ public string[] Flags } [DBusInterface("org.bluez.LEAdvertisement1")] - interface ILEAdvertisement1 : IDBusObject + public interface ILEAdvertisement1 : IDBusObject { Task ReleaseAsync(); - Task GetAsync(string prop); + Task GetAsync(string prop); Task GetAllAsync(); Task SetAsync(string prop, object val); Task WatchPropertiesAsync(Action handler); } [Dictionary] - class LEAdvertisement1Properties + public class LEAdvertisement1Properties { - private string _Type = default (string); + private string _Type; public string Type { - get - { - return _Type; - } - - set - { - _Type = (value); - } + get => _Type; + set => _Type = value; } - private string[] _ServiceUUIDs = default (string[]); + private string[] _ServiceUUIDs; public string[] ServiceUUIDs { - get - { - return _ServiceUUIDs; - } - - set - { - _ServiceUUIDs = (value); - } + get => _ServiceUUIDs; + set => _ServiceUUIDs = value; } - private IDictionary _ManufacturerData = default (IDictionary); + private IDictionary _ManufacturerData; public IDictionary ManufacturerData { - get - { - return _ManufacturerData; - } - - set - { - _ManufacturerData = (value); - } + get => _ManufacturerData; + set => _ManufacturerData = value; } - private string[] _SolicitUUIDs = default (string[]); + private string[] _SolicitUUIDs; public string[] SolicitUUIDs { - get - { - return _SolicitUUIDs; - } - - set - { - _SolicitUUIDs = (value); - } + get => _SolicitUUIDs; + set => _SolicitUUIDs = value; } - private IDictionary _ServiceData = default (IDictionary); + private IDictionary _ServiceData; public IDictionary ServiceData { - get - { - return _ServiceData; - } - - set - { - _ServiceData = (value); - } + get => _ServiceData; + set => _ServiceData = value; } - private bool _IncludeTxPower = default (bool); + private bool _IncludeTxPower; public bool IncludeTxPower { - get - { - return _IncludeTxPower; - } + get => _IncludeTxPower; + set => _IncludeTxPower = value; + } - set - { - _IncludeTxPower = (value); - } + private string _LocalName; + public string LocalName + { + get => _LocalName; + set => _LocalName = value; + } + + private UInt16 _Appearance; + public UInt16 Appearance + { + get => _Appearance; + set => _Appearance = value; + } + + private bool _Discoverable; + public bool Discoverable + { + get => _Discoverable; + set => _Discoverable = value; } } static class LEAdvertisement1Extensions { - public static Task GetTypeAsync(this ILEAdvertisement1 o) => o.GetAsync("Type"); - public static Task GetServiceUUIDsAsync(this ILEAdvertisement1 o) => o.GetAsync("ServiceUUIDs"); - public static Task> GetManufacturerDataAsync(this ILEAdvertisement1 o) => o.GetAsync>("ManufacturerData"); - public static Task GetSolicitUUIDsAsync(this ILEAdvertisement1 o) => o.GetAsync("SolicitUUIDs"); - public static Task> GetServiceDataAsync(this ILEAdvertisement1 o) => o.GetAsync>("ServiceData"); - public static Task GetIncludeTxPowerAsync(this ILEAdvertisement1 o) => o.GetAsync("IncludeTxPower"); + //public static Task GetTypeAsync(this ILEAdvertisement1 o) => o.GetAsync("Type"); + //public static Task GetServiceUUIDsAsync(this ILEAdvertisement1 o) => o.GetAsync("ServiceUUIDs"); + //public static Task> GetManufacturerDataAsync(this ILEAdvertisement1 o) => o.GetAsync>("ManufacturerData"); + //public static Task GetSolicitUUIDsAsync(this ILEAdvertisement1 o) => o.GetAsync("SolicitUUIDs"); + //public static Task> GetServiceDataAsync(this ILEAdvertisement1 o) => o.GetAsync>("ServiceData"); + //public static Task GetIncludeTxPowerAsync(this ILEAdvertisement1 o) => o.GetAsync("IncludeTxPower"); + public static Task SetLocalNameAsync(this ILEAdvertisement1 o, string val) => o.SetAsync("LocalName", val); } } \ No newline at end of file diff --git a/DotnetBleServer/Gatt/BlueZModel/CharacteristicFlagConverter.cs b/DotnetBleServer/Gatt/BlueZModel/CharacteristicFlagConverter.cs index b8218d3..7b6541c 100755 --- a/DotnetBleServer/Gatt/BlueZModel/CharacteristicFlagConverter.cs +++ b/DotnetBleServer/Gatt/BlueZModel/CharacteristicFlagConverter.cs @@ -12,6 +12,7 @@ internal class CharacteristicFlagConverter { {CharacteristicFlags.Read, "read"}, {CharacteristicFlags.Write, "write"}, + {CharacteristicFlags.Notify, "notify"}, {CharacteristicFlags.WritableAuxiliaries, "writable-auxiliaries"} }; diff --git a/DotnetBleServer/Gatt/BlueZModel/GattCharacteristic.cs b/DotnetBleServer/Gatt/BlueZModel/GattCharacteristic.cs index a0627c2..3ef2a02 100755 --- a/DotnetBleServer/Gatt/BlueZModel/GattCharacteristic.cs +++ b/DotnetBleServer/Gatt/BlueZModel/GattCharacteristic.cs @@ -7,14 +7,13 @@ namespace DotnetBleServer.Gatt.BlueZModel { - internal class GattCharacteristic : PropertiesBase, IGattCharacteristic1, - IObjectManagerProperties + internal class GattCharacteristic : PropertiesBase, IGattCharacteristic1, IObjectManagerProperties { public IList Descriptors { get; } = new List(); - private readonly ICharacteristicSource _CharacteristicSource; + private readonly ICharacteristic _CharacteristicSource; - public GattCharacteristic(ObjectPath objectPath, GattCharacteristic1Properties properties, ICharacteristicSource characteristicSource) : base(objectPath, properties) + public GattCharacteristic(ObjectPath objectPath, GattCharacteristic1Properties properties, ICharacteristic characteristicSource) : base(objectPath, properties) { _CharacteristicSource = characteristicSource; } @@ -31,12 +30,17 @@ public Task WriteValueAsync(byte[] value, IDictionary options) public Task StartNotifyAsync() { - throw new NotImplementedException(); + if (Properties.Notifying) + { + return Task.CompletedTask; + } + + return _CharacteristicSource.StartUpdatesAsync(); } public Task StopNotifyAsync() { - throw new NotImplementedException(); + return _CharacteristicSource.StopUpdatesAsync(); } public IDictionary> GetProperties() @@ -49,6 +53,7 @@ public IDictionary> GetProperties() {"Service", Properties.Service}, {"UUID", Properties.UUID}, {"Flags", Properties.Flags}, + {"Notifying", Properties.Notifying }, {"Descriptors", Descriptors.Select(d => d.ObjectPath).ToArray()} } } diff --git a/DotnetBleServer/Gatt/BlueZModel/GattService.cs b/DotnetBleServer/Gatt/BlueZModel/GattService.cs index f873acf..9368411 100755 --- a/DotnetBleServer/Gatt/BlueZModel/GattService.cs +++ b/DotnetBleServer/Gatt/BlueZModel/GattService.cs @@ -29,13 +29,15 @@ public IDictionary> GetProperties() }; } - public GattCharacteristic AddCharacteristic(GattCharacteristic1Properties characteristic, ICharacteristicSource characteristicSource) + public GattCharacteristic AddCharacteristic(GattCharacteristic1Properties characteristic, ICharacteristic characteristicSource) { characteristic.Service = ObjectPath; - var gattCharacteristic = new GattCharacteristic(NextCharacteristicPath(), characteristic, characteristicSource); + var characteristicPath = NextCharacteristicPath(); + + var gattCharacteristic = new GattCharacteristic(characteristicPath, characteristic, characteristicSource); _Characteristics.Add(gattCharacteristic); - Properties.Characteristics = Properties.Characteristics.Append(NextCharacteristicPath()).ToArray(); + Properties.Characteristics = Properties.Characteristics.Append(characteristicPath).ToArray(); return gattCharacteristic; } diff --git a/DotnetBleServer/Gatt/CharacteristicUpdatedEventArgs.cs b/DotnetBleServer/Gatt/CharacteristicUpdatedEventArgs.cs new file mode 100644 index 0000000..3713d88 --- /dev/null +++ b/DotnetBleServer/Gatt/CharacteristicUpdatedEventArgs.cs @@ -0,0 +1,23 @@ +using System; + +namespace DotnetBleServer.Gatt +{ + /// + /// Event arguments for ICharacteristic.ValueUpdated + /// + public class CharacteristicUpdatedEventArgs : EventArgs + { + /// + /// The characteristic. + /// + public ICharacteristic Characteristic { get; set; } + + /// + /// Constructor. + /// + public CharacteristicUpdatedEventArgs(ICharacteristic characteristic) + { + Characteristic = characteristic; + } + } +} \ No newline at end of file diff --git a/DotnetBleServer/Gatt/Description/CharacteristicFlags.cs b/DotnetBleServer/Gatt/Description/CharacteristicFlags.cs index 6c17428..2d5af1b 100755 --- a/DotnetBleServer/Gatt/Description/CharacteristicFlags.cs +++ b/DotnetBleServer/Gatt/Description/CharacteristicFlags.cs @@ -7,6 +7,7 @@ public enum CharacteristicFlags { Read = 1, Write = 2, + Notify = 3, WritableAuxiliaries = 4 } } \ No newline at end of file diff --git a/DotnetBleServer/Gatt/Description/GattCharacteristicDescription.cs b/DotnetBleServer/Gatt/Description/GattCharacteristicDescription.cs index 7d1b2d3..f55471f 100755 --- a/DotnetBleServer/Gatt/Description/GattCharacteristicDescription.cs +++ b/DotnetBleServer/Gatt/Description/GattCharacteristicDescription.cs @@ -1,21 +1,48 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace DotnetBleServer.Gatt.Description { - public class GattCharacteristicDescription + public abstract class GattCharacteristicDescription : ICharacteristic { + private bool _notify; private readonly IList _Descriptors = new List(); public IEnumerable Descriptors => _Descriptors; - public ICharacteristicSource CharacteristicSource { get; set; } - public string UUID { get; set; } + public CharacteristicFlags Flags { get; set; } public void AddDescriptor(GattDescriptorDescription gattDescriptorDescription) { _Descriptors.Add(gattDescriptorDescription); } + + public bool CanUpdate => Flags.HasFlag(CharacteristicFlags.Notify); + + public event EventHandler ValueUpdated; + + public virtual Task WriteValueAsync(byte[] value) + { + if (_notify) + ValueUpdated?.Invoke(this, new CharacteristicUpdatedEventArgs(this)); + + return Task.CompletedTask; + } + + public abstract Task ReadValueAsync(); + + public Task StartUpdatesAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(() => _notify = true); + } + + public Task StopUpdatesAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(() => _notify = false); + } } } \ No newline at end of file diff --git a/DotnetBleServer/Gatt/GattApplicationManager.cs b/DotnetBleServer/Gatt/GattApplicationManager.cs index e9a8328..dfc22ec 100755 --- a/DotnetBleServer/Gatt/GattApplicationManager.cs +++ b/DotnetBleServer/Gatt/GattApplicationManager.cs @@ -1,20 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DotnetBleServer.Core; +using DotnetBleServer.Core; using DotnetBleServer.Gatt.BlueZModel; using DotnetBleServer.Gatt.Description; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Tmds.DBus; namespace DotnetBleServer.Gatt { public class GattApplicationManager { - private readonly ServerContext _ServerContext; + private readonly ServerContext _context; - public GattApplicationManager(ServerContext serverContext) + public GattApplicationManager(ServerContext context) { - _ServerContext = serverContext; + _context = context; } public async Task RegisterGattApplication(IEnumerable gattServiceDescriptions) @@ -53,40 +53,38 @@ private async Task BuildApplicationTree(string applicationObjectPath, IEnumerabl private async Task RegisterApplicationInBluez(string applicationObjectPath) { - var gattManager = _ServerContext.Connection.CreateProxy("org.bluez", "/org/bluez/hci0"); + var gattManager = _context.Connection.CreateProxy(Constants.DbusServicePath, _context.Adapter.ObjectPath); await gattManager.RegisterApplicationAsync(new ObjectPath(applicationObjectPath), new Dictionary()); } private async Task BuildGattApplication(string applicationObjectPath) { var application = new GattApplication(applicationObjectPath); - await _ServerContext.Connection.RegisterObjectAsync(application); + await _context.Connection.RegisterObjectAsync(application); return application; } - private async Task AddNewService(GattApplication application, - GattServiceDescription serviceDescription) + private async Task AddNewService(GattApplication application, GattServiceDescription serviceDescription) { var gattService1Properties = GattPropertiesFactory.CreateGattService(serviceDescription); var gattService = application.AddService(gattService1Properties); - await _ServerContext.Connection.RegisterObjectAsync(gattService); + await _context.Connection.RegisterObjectAsync(gattService); return gattService; } private async Task AddNewCharacteristic(GattService gattService, GattCharacteristicDescription characteristic) { var gattCharacteristic1Properties = GattPropertiesFactory.CreateGattCharacteristic(characteristic); - var gattCharacteristic = gattService.AddCharacteristic(gattCharacteristic1Properties, characteristic.CharacteristicSource); - await _ServerContext.Connection.RegisterObjectAsync(gattCharacteristic); + var gattCharacteristic = gattService.AddCharacteristic(gattCharacteristic1Properties, characteristic); + await _context.Connection.RegisterObjectAsync(gattCharacteristic); return gattCharacteristic; } - private async Task AddNewDescriptor(GattCharacteristic gattCharacteristic, - GattDescriptorDescription descriptor) + private async Task AddNewDescriptor(GattCharacteristic gattCharacteristic, GattDescriptorDescription descriptor) { var gattDescriptor1Properties = GattPropertiesFactory.CreateGattDescriptor(descriptor); var gattDescriptor = gattCharacteristic.AddDescriptor(gattDescriptor1Properties); - await _ServerContext.Connection.RegisterObjectAsync(gattDescriptor); + await _context.Connection.RegisterObjectAsync(gattDescriptor); } } } \ No newline at end of file diff --git a/DotnetBleServer/Gatt/ICharacteristic.cs b/DotnetBleServer/Gatt/ICharacteristic.cs new file mode 100644 index 0000000..6946ca7 --- /dev/null +++ b/DotnetBleServer/Gatt/ICharacteristic.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DotnetBleServer.Gatt +{ + public interface ICharacteristic + { + /// + /// Indicates wheter the characteristic supports notify or not. + /// + bool CanUpdate { get; } + + /// + /// Event gets raised, when the davice notifies a value change on this characteristic. + /// To start listening, call . + /// + event EventHandler ValueUpdated; + + Task WriteValueAsync(byte[] value); + + Task ReadValueAsync(); + + /// + /// Starts listening for notify events on this characteristic. + /// + /// + /// Thrown if characteristic doesn't support notify. See: + /// Thrown if an error occurs while starting notifications + Task StartUpdatesAsync(CancellationToken cancellationToken = default); + + /// + /// Stops listening for notify events on this characteristic. + /// + /// + /// Thrown if an error occurs while starting notifications + Task StopUpdatesAsync(CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/DotnetBleServer/Gatt/ICharacteristicSource.cs b/DotnetBleServer/Gatt/ICharacteristicSource.cs deleted file mode 100755 index f125046..0000000 --- a/DotnetBleServer/Gatt/ICharacteristicSource.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; - -namespace DotnetBleServer.Gatt -{ - public interface ICharacteristicSource - { - Task WriteValueAsync(byte[] value); - Task ReadValueAsync(); - } -} \ No newline at end of file diff --git a/DotnetBleServer/Properties/PublishProfiles/FolderProfile.pubxml b/DotnetBleServer/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..d348e50 --- /dev/null +++ b/DotnetBleServer/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,15 @@ + + + + + Release + Any CPU + bin\Release\net7.0\publish\ + FileSystem + <_TargetId>Folder + net7.0 + false + + \ No newline at end of file diff --git a/Examples/Program.cs b/Examples/Program.cs index 27961e5..581a208 100755 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -1,6 +1,6 @@ -using System; +using DotnetBleServer.Core; +using System; using System.Threading.Tasks; -using DotnetBleServer.Core; namespace Examples { @@ -8,21 +8,33 @@ internal class Program { private static void Main() { - Console.WriteLine("Hello Bluetooth"); - Console.ReadKey(); + Console.WriteLine("Hello Bluetooth !"); + Console.WriteLine("Press a key to start (it's time to attach debugger from Visual Studio using SSH)"); + Console.ReadLine(); Task.Run(async () => { - using (var serverContext = new ServerContext()) + using (var context = new ServerContext()) { - await serverContext.Connect(); - await SampleAdvertisement.RegisterSampleAdvertisement(serverContext); - await SampleGattApplication.RegisterGattApplication(serverContext); + await context.ConnectAndSetDefaultAdapter(); - Console.WriteLine("Press CTRL+C to quit"); + Console.WriteLine($"Using bluetooth adapter: {context.Adapter.ObjectPath}"); + Console.WriteLine("(switch adapter using context.GetAdapters() and then set context.Adapter)"); + + // Turn on adapter. + await context.Adapter.SetPoweredAsync(true); + + if (!await context.Adapter.GetPoweredAsync()) + throw new Exception($"Can't power on adapter {context.Adapter.ObjectPath}"); + + await SampleAdvertisement.RegisterSampleAdvertisement(context); + await SampleGattApplication.RegisterGattApplication(context); + + Console.WriteLine("Press Ctrl+C to quit"); await Task.Delay(-1); } + }).Wait(); } } -} +} \ No newline at end of file diff --git a/Examples/Properties/launchSettings.json b/Examples/Properties/launchSettings.json new file mode 100644 index 0000000..8f38ba8 --- /dev/null +++ b/Examples/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Examples": { + "commandName": "Project", + "remoteDebugEnabled": false, + "remoteDebugMachine": "192.168.1.25" + }, + "WSL": { + "commandName": "WSL2", + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/Examples/Examples.csproj b/Examples/Sample.Server.csproj old mode 100755 new mode 100644 similarity index 77% rename from Examples/Examples.csproj rename to Examples/Sample.Server.csproj index 346b16a..001ddba --- a/Examples/Examples.csproj +++ b/Examples/Sample.Server.csproj @@ -1,12 +1,12 @@ - - - - Exe - netcoreapp3.1 - - - - - - - + + + + Exe + net7.0 + + + + + + + diff --git a/Examples/SampleAdvertisement.cs b/Examples/SampleAdvertisement.cs index 1c32629..59388af 100755 --- a/Examples/SampleAdvertisement.cs +++ b/Examples/SampleAdvertisement.cs @@ -1,21 +1,29 @@ -using System.Threading.Tasks; -using DotnetBleServer.Advertisements; +using DotnetBleServer.Advertisements; using DotnetBleServer.Core; +using DotnetBleServer.Gatt.BlueZModel; +using System; +using System.Threading.Tasks; namespace Examples { public class SampleAdvertisement { - public static async Task RegisterSampleAdvertisement(ServerContext serverContext) + public static async Task RegisterSampleAdvertisement(ServerContext context) { - var advertisementProperties = new AdvertisementProperties + var advertisementProperties = new LEAdvertisement1Properties { Type = "peripheral", - ServiceUUIDs = new[] { "12345678-1234-5678-1234-56789abcdef0"}, + ServiceUUIDs = new[] { "12345678-1234-5678-1234-56789abcdef0" }, LocalName = "A", + // Check bluetooth spec here: https://www.bluetooth.com/specifications/an/ (Computer appearance is 64 => convert hex value to uint: 0x0080 = 64) + // I don't know why, for some reasons, changing appearance is not always working. + Appearance = (ushort)Convert.ToUInt32("0x0080", 16), + // Set to false if Type = "broadcast" + Discoverable = true, + IncludeTxPower = true, }; - await new AdvertisingManager(serverContext).CreateAdvertisement(advertisementProperties); + await new AdvertisingManager(context).CreateAdvertisement(advertisementProperties); } } } \ No newline at end of file diff --git a/Examples/SampleGattApplication.cs b/Examples/SampleGattApplication.cs index 153cc57..0fcac9f 100755 --- a/Examples/SampleGattApplication.cs +++ b/Examples/SampleGattApplication.cs @@ -1,15 +1,15 @@ -using System; -using System.Text; -using System.Threading.Tasks; -using DotnetBleServer.Core; +using DotnetBleServer.Core; using DotnetBleServer.Gatt; using DotnetBleServer.Gatt.Description; +using System; +using System.Text; +using System.Threading.Tasks; namespace Examples { - internal class SampleGattApplication + internal static class SampleGattApplication { - public static async Task RegisterGattApplication(ServerContext serverContext) + public static async Task RegisterGattApplication(ServerContext context) { var gattServiceDescription = new GattServiceDescription { @@ -17,39 +17,45 @@ public static async Task RegisterGattApplication(ServerContext serverContext) Primary = true }; - var gattCharacteristicDescription = new GattCharacteristicDescription + var gattCharacteristicDescription = new MyCustomGattCharacteristicDescription { - CharacteristicSource = new ExampleCharacteristicSource(), UUID = "12345678-1234-5678-1234-56789abcdef1", - Flags = CharacteristicFlags.Read | CharacteristicFlags.Write | CharacteristicFlags.WritableAuxiliaries + Flags = CharacteristicFlags.Read | CharacteristicFlags.Write | CharacteristicFlags.WritableAuxiliaries | CharacteristicFlags.Notify }; + var gattDescriptorDescription = new GattDescriptorDescription { - Value = new[] {(byte) 't'}, + Value = new[] { (byte)'t' }, UUID = "12345678-1234-5678-1234-56789abcdef2", - Flags = new[] {"read", "write"} + Flags = new[] { "read", "write" } }; + var gab = new GattApplicationBuilder(); gab .AddService(gattServiceDescription) - .WithCharacteristic(gattCharacteristicDescription, new[] {gattDescriptorDescription}); + .WithCharacteristic(gattCharacteristicDescription, new[] { gattDescriptorDescription }); - await new GattApplicationManager(serverContext).RegisterGattApplication(gab.BuildServiceDescriptions()); + await new GattApplicationManager(context).RegisterGattApplication(gab.BuildServiceDescriptions()); } + } - internal class ExampleCharacteristicSource : ICharacteristicSource + class MyCustomGattCharacteristicDescription : GattCharacteristicDescription + { + public override Task WriteValueAsync(byte[] value) { - public Task WriteValueAsync(byte[] value) - { - Console.WriteLine("Writing value"); - return Task.Run(() => Console.WriteLine(Encoding.ASCII.GetChars(value))); - } + Console.WriteLine("Writing value"); - public Task ReadValueAsync() - { - Console.WriteLine("Reading value"); - return Task.FromResult(Encoding.ASCII.GetBytes("Hello BLE")); - } + string message = new string(Encoding.ASCII.GetChars(value)); + + Console.WriteLine($"Value received: {message}"); + + return base.WriteValueAsync(value); + } + + public override Task ReadValueAsync() + { + Console.WriteLine("Reading value"); + return Task.FromResult(Encoding.ASCII.GetBytes("Some data")); } } } \ No newline at end of file diff --git a/README.md b/README.md index 878c2f4..4fb6c43 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # .NET BLE Server -![Main Pipeline](https://github.com/phylomeno/dotnet-ble-server/workflows/Main%20Pipeline/badge.svg) [![MIT License](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![NuGet](https://img.shields.io/nuget/vpre/DotnetBleServer)](https://www.nuget.org/packages/DotnetBleServer/) +![Main Pipeline](https://github.com/phylomeno/dotnet-ble-server/workflows/Main%20Pipeline/badge.svg) [![MIT License](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![NuGet](https://img.shields.io/nuget/vpre/BluetoothLE.Server.Linux)](https://www.nuget.org/packages/BluetoothLE.Server.Linux/) .NET BLE Server is a library to support creation of BLE peripherals on Linux using .NET Core. @@ -7,6 +7,20 @@ Under the hood the library uses BlueZ. The provided API aims to preserve the obj ## Examples + + + + + + + + + + + +
Client / Server demo (Windows - Linux)
Client running WinUI app using PLUGIN.BLE nuget packageServer running Linux (raspberry PI 3) (connected via SSH on WSL)
+ + ### Advertisement ```csharp using (var serverContext = new ServerContext()) @@ -16,6 +30,9 @@ using (var serverContext = new ServerContext()) Type = "peripheral", ServiceUUIDs = new[] { "12345678-1234-5678-1234-56789abcdef0"}, LocalName = "A", + Appearance = (ushort)Convert.ToUInt32("0x0080", 16), + Discoverable = true, + IncludeTxPower = true, }; await new AdvertisingManager(serverContext).CreateAdvertisement(advertisementProperties); @@ -34,16 +51,17 @@ using (var serverContext = new ServerContext()) var gattCharacteristicDescription = new GattCharacteristicDescription { - CharacteristicSource = new SampleGattApplication.ExampleCharacteristicSource(), UUID = "12345678-1234-5678-1234-56789abcdef1", - Flags = new[] {"read", "write", "writable-auxiliaries"} + Flags = CharacteristicFlags.Read | CharacteristicFlags.Write | CharacteristicFlags.WritableAuxiliaries | CharacteristicFlags.Notify }; + var gattDescriptorDescription = new GattDescriptorDescription { Value = new[] {(byte) 't'}, UUID = "12345678-1234-5678-1234-56789abcdef2", Flags = new[] {"read", "write"} }; + var gab = new GattApplicationBuilder(); gab .AddService(gattServiceDescription) diff --git a/Sample.Client.Win/App.xaml b/Sample.Client.Win/App.xaml new file mode 100644 index 0000000..b58e9ed --- /dev/null +++ b/Sample.Client.Win/App.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/Sample.Client.Win/App.xaml.cs b/Sample.Client.Win/App.xaml.cs new file mode 100644 index 0000000..4028809 --- /dev/null +++ b/Sample.Client.Win/App.xaml.cs @@ -0,0 +1,34 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Sample.Client.Win +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : Application + { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + m_window = new MainWindow(); + m_window.Activate(); + } + + private Window m_window; + } +} diff --git a/Sample.Client.Win/Assets/LockScreenLogo.scale-200.png b/Sample.Client.Win/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000..7440f0d Binary files /dev/null and b/Sample.Client.Win/Assets/LockScreenLogo.scale-200.png differ diff --git a/Sample.Client.Win/Assets/SplashScreen.scale-200.png b/Sample.Client.Win/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000..32f486a Binary files /dev/null and b/Sample.Client.Win/Assets/SplashScreen.scale-200.png differ diff --git a/Sample.Client.Win/Assets/Square150x150Logo.scale-200.png b/Sample.Client.Win/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..53ee377 Binary files /dev/null and b/Sample.Client.Win/Assets/Square150x150Logo.scale-200.png differ diff --git a/Sample.Client.Win/Assets/Square44x44Logo.scale-200.png b/Sample.Client.Win/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..f713bba Binary files /dev/null and b/Sample.Client.Win/Assets/Square44x44Logo.scale-200.png differ diff --git a/Sample.Client.Win/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Sample.Client.Win/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..dc9f5be Binary files /dev/null and b/Sample.Client.Win/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/Sample.Client.Win/Assets/StoreLogo.png b/Sample.Client.Win/Assets/StoreLogo.png new file mode 100644 index 0000000..a4586f2 Binary files /dev/null and b/Sample.Client.Win/Assets/StoreLogo.png differ diff --git a/Sample.Client.Win/Assets/Wide310x150Logo.scale-200.png b/Sample.Client.Win/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..8b4a5d0 Binary files /dev/null and b/Sample.Client.Win/Assets/Wide310x150Logo.scale-200.png differ diff --git a/Sample.Client.Win/BluetoothService.cs b/Sample.Client.Win/BluetoothService.cs new file mode 100644 index 0000000..5e7b1a2 --- /dev/null +++ b/Sample.Client.Win/BluetoothService.cs @@ -0,0 +1,162 @@ +using Plugin.BLE; +using Plugin.BLE.Abstractions; +using Plugin.BLE.Abstractions.Contracts; +using Plugin.BLE.Abstractions.Extensions; +using Sample.Common.Models; +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample.Client.Win +{ + internal class BluetoothService + { + public Action StatusChanged { get; set; } + public Action Discovered { get; set; } + + private static readonly Guid SERVICE_CHARACTERISTIC_UUID = new Guid("12345678-1234-5678-1234-56789abcdef1"); + private const int TIMEOUT = 5000; + private IDevice _connectedDevice; + private Guid _serviceUuid; + private IAdapter _adapter; + private CancellationTokenSource _cts = new CancellationTokenSource(); + + private async void Scan(Guid serviceUuid) + { + if (_adapter.IsScanning) + await StopAsync(); + + _serviceUuid = serviceUuid; + _adapter.ScanTimeout = TIMEOUT; + _adapter.DeviceDiscovered += DeviceDiscovered; + _adapter.ScanTimeoutElapsed += ScanTimeoutElapsed; + + var scanFilterOptions = new ScanFilterOptions(); + scanFilterOptions.ServiceUuids = [serviceUuid]; // cross platform filter + + Debug.WriteLine("Scanning started"); + StatusChanged.Invoke("Scanning..."); + await _adapter.StartScanningForDevicesAsync(scanFilterOptions, _cts.Token); + } + + private void ScanTimeoutElapsed(object sender, EventArgs e) + { + StatusChanged.Invoke("Stopped. Try again."); + } + + private void DeviceDiscovered(object sender, Plugin.BLE.Abstractions.EventArgs.DeviceEventArgs scanResult) + { + var device = $"{scanResult.Device.Name} - {scanResult.Device.Id}"; + Debug.WriteLine($"Discovered {device}"); + + Discovered?.Invoke(new RomeRemoteSystem(scanResult.Device) + { + DisplayName = scanResult.Device.Name, + Status = scanResult.Device.State.ToString(), + Id = scanResult.Device.Id.ToString(), + }); + } + + private async Task ResetAsync() + { + if (_adapter == null) return; + + _cts?.Cancel(); + + if (_adapter.ConnectedDevices.Count > 0) + { + await _adapter.DisconnectDeviceAsync(_adapter.ConnectedDevices[0]); + } + + _adapter.DeviceDiscovered -= DeviceDiscovered; + _adapter.ScanTimeoutElapsed -= ScanTimeoutElapsed; + + _cts.Dispose(); + _cts = new CancellationTokenSource(); + } + + private async Task StopAsync() + { + if (_adapter.IsScanning) + await _adapter.StopScanningForDevicesAsync(); + } + + public async Task ConnectAsync(IDevice device) + { + try + { + await StopAsync(); + + Debug.WriteLine($"Connecting..."); + + _connectedDevice = device; + + await _adapter.ConnectToDeviceAsync(_connectedDevice, cancellationToken: _cts.Token); + + Debug.WriteLine($"Connected: {_connectedDevice != null}"); + + return _connectedDevice != null; + } + catch (Exception ex) + { + Debug.WriteLine($"Unable to connect : " + ex.Message); + return false; + } + } + + public async Task InitializeAsync(Guid serviceUuid, string? hostName) + { + StatusChanged.Invoke("Initializing..."); + + await ResetAsync(); + + _adapter = CrossBluetoothLE.Current.Adapter; + + if (CrossBluetoothLE.Current.State == BluetoothState.On) + Scan(serviceUuid); + else + { + StatusChanged.Invoke("waiting for initialization..."); + CrossBluetoothLE.Current.StateChanged += (s, e) => + { + if (CrossBluetoothLE.Current.State == BluetoothState.On) + Scan(serviceUuid); + }; + } + } + + public async Task GetAsync(string value) + { + try + { + Debug.WriteLine("Get service"); + var service = await _connectedDevice.GetServiceAsync(_serviceUuid, _cts.Token); + + Debug.WriteLine("Get write characteristic"); + var characteristic = await service?.GetCharacteristicAsync(SERVICE_CHARACTERISTIC_UUID); + + Debug.WriteLine("Write value"); + await characteristic?.WriteAsync(Encoding.ASCII.GetBytes(value), _cts.Token); + + Debug.WriteLine("Read value"); + var result = await characteristic?.ReadAsync(_cts.Token); + + // You can also use ValueUpdated + //characteristic.ValueUpdated += (a, b) => + //{ + // Console.WriteLine("Value updated: " + Encoding.ASCII.GetString(b.Characteristic.Value)); + //}; + //await characteristic.StartUpdatesAsync(); + + return Encoding.ASCII.GetString(result.data); + } + catch (Exception ex) + { + Debug.WriteLine($"GetAsync failed: {ex.Message}"); + return string.Empty; + } + } + } +} \ No newline at end of file diff --git a/Sample.Client.Win/MainWindow.xaml b/Sample.Client.Win/MainWindow.xaml new file mode 100644 index 0000000..434d3f0 --- /dev/null +++ b/Sample.Client.Win/MainWindow.xaml @@ -0,0 +1,51 @@ + + + + + + + + Discovered devices : + + + + + + + + + + + + + + + ===== Logs ===== + + + + + + + + + + + + + + + + diff --git a/Sample.Client.Win/MainWindow.xaml.cs b/Sample.Client.Win/MainWindow.xaml.cs new file mode 100644 index 0000000..768716b --- /dev/null +++ b/Sample.Client.Win/MainWindow.xaml.cs @@ -0,0 +1,57 @@ +using Microsoft.UI.Xaml; +using Plugin.BLE.Abstractions.Contracts; +using Sample.Common.Models; +using System; +using System.Text.Json; + +namespace Sample.Client.Win +{ + public sealed partial class MainWindow : Window + { + private BluetoothService _service; + + public MainWindow() + { + this.InitializeComponent(); + _service = new BluetoothService(); + } + + private async void myButton_Click(object sender, RoutedEventArgs e) + { + devices.Items.Clear(); + logs.Items.Clear(); + + _service.Discovered = (device) => + { + DispatcherQueue.TryEnqueue(() => + { + devices.Items.Add(device); + }); + }; + + _service.StatusChanged = (state) => + { + myButton.Content = state; + }; + + await _service.InitializeAsync(new Guid("12345678-1234-5678-1234-56789abcdef0"), null); + } + + private async void devices_SelectionChanged(object sender, Microsoft.UI.Xaml.Controls.SelectionChangedEventArgs e) + { + if (e.AddedItems.Count == 0) return; + + var device = (RomeRemoteSystem)e.AddedItems[0]; + myButton.Content = "Start"; + + logs.Items.Add($"Connecting to {device.DisplayName}..."); + var connected = await _service.ConnectAsync((IDevice)device.NativeObject); + logs.Items.Add(connected ? "Connected" : "Unable to connect"); + + var data = JsonSerializer.Serialize(new RemoteParameter() { Command = RemoteCommands.Command1 }); + logs.Items.Add($"Sending data... {data}"); + var response = await _service.GetAsync(data); + logs.Items.Add($"Data received: {response}"); + } + } +} \ No newline at end of file diff --git a/Sample.Client.Win/Package.appxmanifest b/Sample.Client.Win/Package.appxmanifest new file mode 100644 index 0000000..6611ef1 --- /dev/null +++ b/Sample.Client.Win/Package.appxmanifest @@ -0,0 +1,51 @@ + + + + + + + + + + Sample.Client.Win + Guillaume + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sample.Client.Win/Properties/launchSettings.json b/Sample.Client.Win/Properties/launchSettings.json new file mode 100644 index 0000000..2378334 --- /dev/null +++ b/Sample.Client.Win/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Sample.Client.Win (Package)": { + "commandName": "MsixPackage" + }, + "Sample.Client.Win (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/Sample.Client.Win/Sample.Client.Win.csproj b/Sample.Client.Win/Sample.Client.Win.csproj new file mode 100644 index 0000000..b4b215e --- /dev/null +++ b/Sample.Client.Win/Sample.Client.Win.csproj @@ -0,0 +1,50 @@ + + + WinExe + net8.0-windows10.0.19041.0 + 10.0.17763.0 + Sample.Client.Win + app.manifest + x86;x64;ARM64 + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + diff --git a/Sample.Client.Win/app.manifest b/Sample.Client.Win/app.manifest new file mode 100644 index 0000000..0bef63d --- /dev/null +++ b/Sample.Client.Win/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file diff --git a/Sample.Client/Models.cs b/Sample.Client/Models.cs new file mode 100644 index 0000000..8f37564 --- /dev/null +++ b/Sample.Client/Models.cs @@ -0,0 +1,29 @@ +using System.Net; + +namespace Sample.Common.Models +{ + public enum RemoteCommands + { + Command1, + Command2 + } + + public class RemoteParameter + { + public string? SerializedData { get; set; } + public RemoteCommands Command { get; set; } + } + + public class RomeRemoteSystem(object nativeObject) + { + public object NativeObject { get; } = nativeObject; + + public string DisplayName { get; set; } + public string Id { get; set; } + public string Status { get; set; } + public string Kind { get; set; } + public IPEndPoint EndPoint { get; set; } + + public string DisplayMember => $"{DisplayName} - {Id}"; + } +} diff --git a/Sample.Client/Sample.Common.csproj b/Sample.Client/Sample.Common.csproj new file mode 100644 index 0000000..215cd12 --- /dev/null +++ b/Sample.Client/Sample.Common.csproj @@ -0,0 +1,14 @@ + + + + Library + net8.0 + enable + enable + + + + + + + diff --git a/icon_small.png b/icon_small.png new file mode 100644 index 0000000..3aff76d Binary files /dev/null and b/icon_small.png differ