From 255ae996517d6d0eade75001c19a1034815cba79 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Sat, 25 May 2024 21:30:10 -0700 Subject: [PATCH] Adding BtLE Advertisements --- example/advertisement.dart | 68 ++++++++++++++ lib/src/bluez_client.dart | 186 +++++++++++++++++++++++++++++++++++-- 2 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 example/advertisement.dart diff --git a/example/advertisement.dart b/example/advertisement.dart new file mode 100644 index 0000000..194f3a8 --- /dev/null +++ b/example/advertisement.dart @@ -0,0 +1,68 @@ +import 'package:dbus/dbus.dart'; +import 'package:bluez/bluez.dart'; + +class ExampleAdvert extends BlueZAdvertisement { + ExampleAdvert() + : super(DBusObjectPath('/org/bluez/example/advertisement0099')); + + @override + Future release() async {} + + @override + String get type => 'broadcast'; + + @override + List get serviceUuids => []; + + @override + Map get serviceData => {}; + + @override + bool get includeTxPower => false; + + @override + Map get manufacturerData => { + BlueZManufacturerId(0x004c): DBusArray.int16([ + 0x02, + 0x15, + 0xE2, + 0xC5, + 0x6D, + 0xB5, + 0xDF, + 0xFB, + 0x48, + 0xD2, + 0xB0, + 0x60, + 0xD0, + 0xF5, + 0xA7, + 0x10, + 0x96, + 0xE0, + 0x00, + 0x01, + 0x00, + 0x02, + 0x0c + ]), + }; + + @override + List get solicitUuids => []; +} + +void main() async { + var client = BlueZClient(); + await client.connect(); + + var adapter = client.adapters.first; + var advertMngr = adapter.advertisingManager; + + var advert = ExampleAdvert(); + + await advertMngr.registerAdvertisement(advert); + + while (true) {} +} diff --git a/lib/src/bluez_client.dart b/lib/src/bluez_client.dart index e6aab3f..7f1f7ab 100644 --- a/lib/src/bluez_client.dart +++ b/lib/src/bluez_client.dart @@ -167,9 +167,13 @@ final _bluezAddressTypeMap = { class BlueZAdapter { final String _adapterInterfaceName = 'org.bluez.Adapter1'; + final BlueZClient _client; final _BlueZObject _object; - BlueZAdapter(this._object); + BlueZAdapter(this._client, this._object); + + BlueZAdvertisingManager get advertisingManager => + BlueZAdvertisingManager(_client, _object); /// Stream of property names as their values change. Stream> get propertiesChanged { @@ -340,6 +344,174 @@ class BlueZAdapter { .toList(); } +/// BtLE Advertising Manager +class BlueZAdvertisingManager { + final String _advertInterfaceName = 'org.bluez.LEAdvertisingManager1'; + + final BlueZClient _client; + final _BlueZObject _object; + + BlueZAdvertisingManager(this._client, this._object); + + Future registerAdvertisement(BlueZAdvertisement advert) async { + await _client._bus.registerObject(advert); + + await _object.callMethod(_advertInterfaceName, 'RegisterAdvertisement', + [advert.path, DBusDict.stringVariant({})], + replySignature: DBusSignature('')); + } +} + +/// BtLE Advertisement instance +abstract class BlueZAdvertisement extends DBusObject { + final String _advertInterfaceName = 'org.bluez.LEAdvertisement1'; + + BlueZAdvertisement(DBusObjectPath path) : super(path); + + Future release(); + + Map get manufacturerData; + String get type; + List get serviceUuids; + Map get serviceData; + bool get includeTxPower; + List get solicitUuids; + + @override + Future handleMethodCall(DBusMethodCall methodCall) async { + if (methodCall.interface == _advertInterfaceName) { + if (methodCall.name == 'Release') { + if (methodCall.values.isNotEmpty) { + return DBusMethodErrorResponse.invalidArgs(); + } + await release(); + return DBusMethodSuccessResponse(); + } else { + return DBusMethodErrorResponse.unknownMethod(); + } + } else { + return DBusMethodErrorResponse.unknownInterface(); + } + } + + @override + Future getProperty(String interface, String name) async { + if (interface == _advertInterfaceName) { + if (name == 'ManufacturerData') { + return DBusMethodSuccessResponse([ + DBusDict( + DBusSignature('q'), + DBusSignature('v'), + manufacturerData.map((id, value) => + MapEntry(DBusUint16(id.id), DBusVariant(value)))) + ]); + } + + if (name == 'Type') { + return DBusMethodSuccessResponse([DBusString(type)]); + } + + if (name == 'ServiceUUIDs') { + return DBusMethodSuccessResponse([DBusArray.string(serviceUuids)]); + } + + if (name == 'ServiceData') { + return DBusMethodSuccessResponse([ + DBusDict.stringVariant(serviceData + .map((uuid, value) => MapEntry(uuid.toString(), value))) + ]); + } + + if (name == 'IncludeTxPower') { + return DBusMethodSuccessResponse([DBusBoolean(includeTxPower)]); + } + + if (name == 'SolicitUUIDs') { + return DBusMethodSuccessResponse([DBusArray.string(solicitUuids)]); + } + + return DBusMethodErrorResponse.unknownProperty(); + } else { + return DBusMethodErrorResponse.unknownInterface(); + } + } + + @override + Future getAllProperties(String interface) async { + var properties = {}; + if (interface == _advertInterfaceName) { + properties['Type'] = + (await getProperty(_advertInterfaceName, 'Type')).returnValues[0]; + properties['ServiceUUIDs'] = + (await getProperty(_advertInterfaceName, 'ServiceUUIDs')) + .returnValues[0]; + properties['ServiceData'] = + (await getProperty(_advertInterfaceName, 'ServiceData')) + .returnValues[0]; + properties['IncludeTxPower'] = + (await getProperty(_advertInterfaceName, 'IncludeTxPower')) + .returnValues[0]; + properties['ManufacturerData'] = + (await getProperty(_advertInterfaceName, 'ManufacturerData')) + .returnValues[0]; + properties['SolicitUUIDs'] = + (await getProperty(_advertInterfaceName, 'SolicitUUIDs')) + .returnValues[0]; + } + return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]); + } + + @override + Future setProperty( + String interface, String name, DBusValue value) async { + if (interface == _advertInterfaceName) { + if (name == 'Type') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'ServiceUUIDs') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'ServiceData') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'IncludeTxPower') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'ManufacturerData') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else if (name == 'SolicitUUIDs') { + return DBusMethodErrorResponse.propertyReadOnly(); + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } + + @override + List introspect() { + return [ + DBusIntrospectInterface( + _advertInterfaceName, + methods: [ + DBusIntrospectMethod('Release'), + ], + properties: [ + DBusIntrospectProperty('Type', DBusSignature('s'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('ServiceUUIDs', DBusSignature('as'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('ServiceData', DBusSignature('a{sv}'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('IncludeTxPower', DBusSignature('b'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('ManufacturerData', DBusSignature('a{qv}'), + access: DBusPropertyAccess.read), + DBusIntrospectProperty('SolicitUUIDs', DBusSignature('as'), + access: DBusPropertyAccess.read), + ], + ), + ]; + } +} + /// A GATT service running on a BlueZ device. class BlueZGattService { final String _serviceInterfaceName = 'org.bluez.GattService1'; @@ -1292,7 +1464,7 @@ class BlueZClient { /// Creates a new BlueZ client. If [bus] is provided connect to the given D-Bus server. BlueZClient({DBusClient? bus}) - : _bus = bus ?? DBusClient.system(), + : _bus = bus ?? DBusClient.system(introspectable: true), _closeBus = bus == null { _root = DBusRemoteObjectManager(_bus, name: 'org.bluez', path: DBusObjectPath('/')); @@ -1317,7 +1489,7 @@ class BlueZClient { _bus, signal.changedPath, signal.interfacesAndProperties); _objects[signal.changedPath] = object; if (_isAdapter(object)) { - _adapterAddedStreamController.add(BlueZAdapter(object)); + _adapterAddedStreamController.add(BlueZAdapter(this, object)); } else if (_isDevice(object)) { _deviceAddedStreamController.add(BlueZDevice(this, object)); } @@ -1334,7 +1506,7 @@ class BlueZClient { } if (signal.interfaces.contains('org.bluez.Adapter1')) { - _adapterRemovedStreamController.add(BlueZAdapter(object)); + _adapterRemovedStreamController.add(BlueZAdapter(this, object)); } else if (signal.interfaces.contains('org.bluez.Device1')) { _deviceRemovedStreamController.add(BlueZDevice(this, object)); } @@ -1358,7 +1530,7 @@ class BlueZClient { // Report initial adapters and devices. for (var object in _objects.values) { if (_isAdapter(object)) { - _adapterAddedStreamController.add(BlueZAdapter(object)); + _adapterAddedStreamController.add(BlueZAdapter(this, object)); } else if (_isDevice(object)) { _deviceAddedStreamController.add(BlueZDevice(this, object)); } @@ -1371,7 +1543,7 @@ class BlueZClient { var adapters = []; for (var object in _objects.values) { if (_isAdapter(object)) { - adapters.add(BlueZAdapter(object)); + adapters.add(BlueZAdapter(this, object)); } } return adapters; @@ -1466,7 +1638,7 @@ class BlueZClient { if (object == null) { return null; } - return BlueZAdapter(object); + return BlueZAdapter(this, object); } bool _isAdapter(_BlueZObject object) {