1d7108c9d0
- Updated autogenerated files to reflect changes from Pigeon v26.3.4. - Added new classes for UniversalBlePeripheralConfig, UniversalBlePeripheralService, UniversalBlePeripheralCharacteristic, UniversalBlePeripheralDescriptor, and UniversalBlePeripheralWriteEvent to handle peripheral configurations and events. - Modified UniversalBlePlugin to implement methods for peripheral support, including StartPeripheral, StopPeripheral, UpdatePeripheralCharacteristicValue, and NotifyPeripheralCharacteristic, returning appropriate error messages for unsupported features on Windows. - Updated HasPermissions method to include an additional parameter for Bluetooth advertising permissions. - Adjusted method signatures and implementations across the plugin to accommodate new peripheral functionality. Signed-off-by: Tony <tonylu@tony-cloud.com>
1444 lines
47 KiB
Dart
1444 lines
47 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:bluez/bluez.dart';
|
|
import 'package:dbus/dbus.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:universal_ble/src/models/model_exports.dart';
|
|
import 'package:universal_ble/src/utils/universal_ble_error_parser.dart';
|
|
import 'package:universal_ble/src/utils/universal_ble_filter_util.dart';
|
|
import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart';
|
|
import 'package:universal_ble/src/universal_ble_platform_interface.dart';
|
|
import 'package:universal_ble/src/utils/universal_logger.dart';
|
|
import 'package:universal_ble/src/universal_ble_exceptions.dart';
|
|
|
|
class UniversalBleLinux extends UniversalBlePlatform {
|
|
UniversalBleLinux._();
|
|
static UniversalBleLinux? _instance;
|
|
static UniversalBleLinux get instance => _instance ??= UniversalBleLinux._();
|
|
|
|
bool isInitialized = false;
|
|
|
|
final BlueZClient _client = BlueZClient();
|
|
late final UniversalBleFilterUtil _bleFilter = UniversalBleFilterUtil();
|
|
BlueZAdapter? _activeAdapter;
|
|
StreamSubscription? _deviceAdded;
|
|
StreamSubscription? _deviceRemoved;
|
|
Completer<void>? _initializationCompleter;
|
|
final Map<String, BlueZDevice> _devices = {};
|
|
final Map<String, StreamSubscription> _deviceUpdateStreamSubscriptions = {};
|
|
final Map<String, StreamSubscription> _deviceAdvertisementSubscriptions = {};
|
|
|
|
final Map<String, StreamSubscription> _characteristicPropertiesSubscriptions = {};
|
|
|
|
DBusClient? _peripheralBus;
|
|
_BlueZPeripheralApplication? _peripheralApplication;
|
|
final List<DBusObject> _peripheralObjects = [];
|
|
final Map<String, _BlueZPeripheralCharacteristic> _peripheralCharacteristics = {};
|
|
BlueZAdvertisement? _peripheralAdvertisement;
|
|
bool _peripheralApplicationRegistered = false;
|
|
DBusObjectPath? _peripheralAdapterPath;
|
|
|
|
@override
|
|
Future<AvailabilityState> getBluetoothAvailabilityState() async {
|
|
await _ensureInitialized();
|
|
|
|
BlueZAdapter? adapter = _activeAdapter;
|
|
if (adapter == null) {
|
|
return AvailabilityState.unsupported;
|
|
}
|
|
return adapter.powered ? AvailabilityState.poweredOn : AvailabilityState.poweredOff;
|
|
}
|
|
|
|
@override
|
|
Future<bool> enableBluetooth() async {
|
|
await _ensureInitialized();
|
|
if (_activeAdapter?.powered == true) return true;
|
|
try {
|
|
await _activeAdapter?.setPowered(true);
|
|
return _activeAdapter?.powered ?? false;
|
|
} catch (e) {
|
|
UniversalLogger.logError('Error enabling bluetooth: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> disableBluetooth() async {
|
|
await _ensureInitialized();
|
|
var adapter = _activeAdapter;
|
|
if (adapter == null) {
|
|
throw "Adapter not available";
|
|
}
|
|
if (!adapter.powered) return true;
|
|
try {
|
|
await adapter.setPowered(false);
|
|
return !adapter.powered;
|
|
} catch (e) {
|
|
UniversalLogger.logError('Error disabling bluetooth: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> startScan({ScanFilter? scanFilter, PlatformConfig? platformConfig}) async {
|
|
await _ensureInitialized();
|
|
var adapter = _activeAdapter;
|
|
if (adapter == null) {
|
|
throw "Adapter not available";
|
|
}
|
|
|
|
// Stop scan and clean all old advertisement listeners
|
|
await stopScan();
|
|
|
|
_bleFilter.scanFilter = scanFilter;
|
|
|
|
// Setup listeners
|
|
_deviceAdded ??= _client.deviceAdded.listen(_onDeviceAdd);
|
|
_deviceRemoved ??= _client.deviceRemoved.listen(_onDeviceRemoved);
|
|
|
|
await adapter.startDiscovery();
|
|
|
|
for (var device in _client.devices) {
|
|
_onDeviceAdd(device);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> stopScan() async {
|
|
await _ensureInitialized();
|
|
try {
|
|
// Dispose listeners
|
|
_deviceAdded?.cancel();
|
|
_deviceRemoved?.cancel();
|
|
_deviceAdded = null;
|
|
_deviceRemoved = null;
|
|
|
|
// Stop Discovery
|
|
if (_activeAdapter?.discovering == true) {
|
|
await _activeAdapter?.stopDiscovery();
|
|
}
|
|
|
|
// Clean all advertisement listeners
|
|
_deviceAdvertisementSubscriptions.removeWhere((e, value) {
|
|
value.cancel();
|
|
return true;
|
|
});
|
|
} catch (e) {
|
|
UniversalLogger.logError("stopScan error: $e");
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> isScanning() async {
|
|
await _ensureInitialized();
|
|
return _activeAdapter?.discovering == true;
|
|
}
|
|
|
|
@override
|
|
Future<BleConnectionState> getConnectionState(String deviceId) async {
|
|
BlueZDevice? device = _getDeviceById(deviceId);
|
|
bool connected = device?.connected ?? false;
|
|
return connected ? BleConnectionState.connected : BleConnectionState.disconnected;
|
|
}
|
|
|
|
@override
|
|
Future<void> connect(
|
|
String deviceId, {
|
|
Duration? connectionTimeout,
|
|
bool autoConnect = false,
|
|
}) async {
|
|
// Note: autoConnect is not directly supported on Linux platform
|
|
final device = _findDeviceById(deviceId);
|
|
if (device.connected) {
|
|
updateConnection(deviceId, true);
|
|
return;
|
|
}
|
|
await device.connect();
|
|
}
|
|
|
|
@override
|
|
Future<void> disconnect(String deviceId) async {
|
|
final device = _getDeviceById(deviceId);
|
|
if (device?.connected == true) {
|
|
await device?.disconnect();
|
|
}
|
|
updateConnection(deviceId, false);
|
|
}
|
|
|
|
@override
|
|
Future<List<BleService>> discoverServices(String deviceId, bool withDescriptors) async {
|
|
final device = _findDeviceById(deviceId);
|
|
if (device.gattServices.isEmpty && !device.servicesResolved) {
|
|
await device.propertiesChanged
|
|
.firstWhere((element) {
|
|
if (element.contains(BluezProperty.connected)) {
|
|
if (!device.connected) {
|
|
UniversalLogger.logInfo("DiscoverServicesFailed: Device disconnected");
|
|
return true;
|
|
}
|
|
}
|
|
return element.contains(BluezProperty.servicesResolved);
|
|
})
|
|
.timeout(
|
|
const Duration(seconds: 10),
|
|
onTimeout: () {
|
|
UniversalLogger.logInfo("DiscoverServicesFailed: Timeout");
|
|
return [];
|
|
},
|
|
);
|
|
}
|
|
|
|
// Few ble devices requires delay to perform operations after discovering services
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
|
|
if (device.gattServices.isEmpty && !device.servicesResolved) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.failed,
|
|
message: "Failed to resolve services",
|
|
);
|
|
}
|
|
|
|
List<BleService> services = [];
|
|
for (final service in device.gattServices) {
|
|
String serviceId = service.uuid.toString();
|
|
|
|
final characteristics = service.characteristics.map((e) {
|
|
final properties = List<CharacteristicProperty>.from(
|
|
e.flags
|
|
.map((e) => e.toCharacteristicProperty())
|
|
.where((element) => element != null)
|
|
.toList(),
|
|
);
|
|
return BleCharacteristic.withMetaData(
|
|
deviceId: deviceId,
|
|
serviceId: serviceId,
|
|
uuid: e.uuid.toString(),
|
|
properties: properties,
|
|
descriptors: withDescriptors
|
|
? e.descriptors.map((e) => BleDescriptor(e.uuid.toString())).toList()
|
|
: [],
|
|
);
|
|
}).toList();
|
|
services.add(BleService(serviceId, characteristics));
|
|
}
|
|
return services;
|
|
}
|
|
|
|
BlueZGattCharacteristic _getCharacteristic(
|
|
String deviceId,
|
|
String service,
|
|
String characteristic,
|
|
) {
|
|
final device = _findDeviceById(deviceId);
|
|
final s = device.gattServices.cast<BlueZGattService?>().firstWhere(
|
|
(s) => s?.uuid.toString() == service,
|
|
orElse: () => null,
|
|
);
|
|
final c = s?.characteristics.cast<BlueZGattCharacteristic?>().firstWhere(
|
|
(c) => c?.uuid.toString() == characteristic,
|
|
orElse: () => null,
|
|
);
|
|
|
|
if (c == null) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.characteristicNotFound,
|
|
message: 'Unknown characteristic:$characteristic',
|
|
);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
@override
|
|
Future<void> setNotifiable(
|
|
String deviceId,
|
|
String service,
|
|
String characteristic,
|
|
BleInputProperty bleInputProperty,
|
|
) async {
|
|
UniversalLogger.logDebug(
|
|
"SET_NOTIFY -> $deviceId $service $characteristic input=${bleInputProperty.name}",
|
|
withTimestamp: true,
|
|
);
|
|
final char = _getCharacteristic(deviceId, service, characteristic);
|
|
|
|
String characteristicKey = "${deviceId}_${service}_$characteristic";
|
|
|
|
if (bleInputProperty != BleInputProperty.disabled) {
|
|
if (char.notifying) {
|
|
UniversalLogger.logInfo('$characteristic already notifying');
|
|
return;
|
|
}
|
|
|
|
await char.startNotify();
|
|
|
|
if (_characteristicPropertiesSubscriptions[characteristicKey] != null) {
|
|
_characteristicPropertiesSubscriptions[characteristicKey]?.cancel();
|
|
}
|
|
|
|
_characteristicPropertiesSubscriptions[characteristicKey] = char.propertiesChanged.listen((
|
|
List<String> properties,
|
|
) {
|
|
for (String property in properties) {
|
|
switch (property) {
|
|
case BluezProperty.value:
|
|
UniversalLogger.logVerbose(
|
|
"NOTIFY <- $deviceId $service $characteristic len=${char.value.length} data=${char.value}",
|
|
withTimestamp: true,
|
|
);
|
|
updateCharacteristicValue(
|
|
deviceId,
|
|
characteristic,
|
|
Uint8List.fromList(char.value),
|
|
DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
break;
|
|
default:
|
|
UniversalLogger.logInfo("UnhandledCharValuePropertyChange: $property");
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
if (char.notifying) await char.stopNotify();
|
|
_characteristicPropertiesSubscriptions.remove(characteristicKey)?.cancel();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<Uint8List> readValue(
|
|
String deviceId,
|
|
String service,
|
|
String characteristic, {
|
|
final Duration? timeout,
|
|
}) async {
|
|
UniversalLogger.logDebug("READ -> $deviceId $service $characteristic", withTimestamp: true);
|
|
try {
|
|
final c = _getCharacteristic(deviceId, service, characteristic);
|
|
final data = await c.readValue();
|
|
return Uint8List.fromList(data);
|
|
} on BlueZFailedException catch (e) {
|
|
UniversalLogger.logError(
|
|
"READ_FAILED <- $deviceId $service $characteristic ${e.message}",
|
|
withTimestamp: true,
|
|
);
|
|
throw e.toUniversalBleException(defaultCode: UniversalBleErrorCode.readFailed);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> writeValue(
|
|
String deviceId,
|
|
String service,
|
|
String characteristic,
|
|
Uint8List value,
|
|
BleOutputProperty bleOutputProperty,
|
|
) async {
|
|
UniversalLogger.logDebug(
|
|
"WRITE -> $deviceId $service $characteristic len=${value.length} property=${bleOutputProperty.name}",
|
|
withTimestamp: true,
|
|
);
|
|
try {
|
|
final c = _getCharacteristic(deviceId, service, characteristic);
|
|
if (bleOutputProperty == BleOutputProperty.withResponse) {
|
|
await c.writeValue(value, type: BlueZGattCharacteristicWriteType.request);
|
|
} else {
|
|
await c.writeValue(value, type: BlueZGattCharacteristicWriteType.command);
|
|
}
|
|
} on BlueZFailedException catch (e) {
|
|
UniversalLogger.logError(
|
|
"WRITE_FAILED <- $deviceId $service $characteristic ${e.message}",
|
|
withTimestamp: true,
|
|
);
|
|
throw e.toUniversalBleException(defaultCode: UniversalBleErrorCode.writeFailed);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<int> requestMtu(String deviceId, int expectedMtu) async {
|
|
final device = _findDeviceById(deviceId);
|
|
if (!device.connected) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.deviceDisconnected,
|
|
message: 'Device not connected',
|
|
);
|
|
}
|
|
for (BlueZGattService service in device.gattServices) {
|
|
for (BlueZGattCharacteristic characteristic in service.characteristics) {
|
|
int? mtu = characteristic.mtu;
|
|
// The value provided by Bluez includes an extra 3 bytes from the GATT header, which needs to be removed.
|
|
if (mtu != null) return mtu - 3;
|
|
}
|
|
}
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.operationNotSupported,
|
|
message: 'MTU not available',
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> requestConnectionPriority(String deviceId, BleConnectionPriority priority) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.notSupported,
|
|
message: "requestConnectionPriority is not supported on Linux platform",
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<bool> isPeripheralSupported() async {
|
|
await _ensureInitialized();
|
|
return _isLinuxPeripheralSupported();
|
|
}
|
|
|
|
@override
|
|
Future<void> startPeripheral(BlePeripheralConfig config) async {
|
|
await _ensureInitialized();
|
|
|
|
final adapter = _activeAdapter;
|
|
if (adapter == null) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.bluetoothNotAvailable,
|
|
message: 'Bluetooth adapter unavailable',
|
|
);
|
|
}
|
|
if (!adapter.powered) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.bluetoothNotEnabled,
|
|
message: 'Bluetooth not enabled',
|
|
);
|
|
}
|
|
if (!await _isLinuxPeripheralSupported()) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.notSupported,
|
|
message: 'BLE peripheral mode is not supported by this Linux adapter',
|
|
);
|
|
}
|
|
|
|
await _stopPeripheralInternal();
|
|
|
|
final bus = DBusClient.system();
|
|
final application = _BlueZPeripheralApplication(
|
|
DBusObjectPath('/com/navideck/universal_ble/peripheral'),
|
|
);
|
|
late final DBusObjectPath? adapterPath;
|
|
try {
|
|
adapterPath = await _resolveActiveAdapterPath(bus);
|
|
} catch (error) {
|
|
await bus.close();
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.bluetoothNotAvailable,
|
|
message: 'Bluetooth adapter unavailable',
|
|
details: error,
|
|
);
|
|
}
|
|
if (adapterPath == null) {
|
|
await bus.close();
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.bluetoothNotAvailable,
|
|
message: 'Bluetooth adapter unavailable',
|
|
);
|
|
}
|
|
|
|
_peripheralBus = bus;
|
|
_peripheralApplication = application;
|
|
_peripheralAdapterPath = adapterPath;
|
|
_peripheralObjects.clear();
|
|
_peripheralCharacteristics.clear();
|
|
|
|
try {
|
|
await bus.registerObject(application);
|
|
await _registerPeripheralObjects(bus, application.path, config);
|
|
|
|
await _blueZAdapterObject(bus, adapterPath).callMethod(
|
|
'org.bluez.GattManager1',
|
|
'RegisterApplication',
|
|
[application.path, DBusDict.stringVariant({})],
|
|
replySignature: DBusSignature(''),
|
|
);
|
|
_peripheralApplicationRegistered = true;
|
|
|
|
_peripheralAdvertisement = await adapter.advertisingManager.registerAdvertisement(
|
|
type: BlueZAdvertisementType.peripheral,
|
|
serviceUuids: config.services.map((service) => service.uuid).toList(),
|
|
localName: config.advertisedName,
|
|
);
|
|
} catch (error) {
|
|
await _stopPeripheralInternal();
|
|
if (error is UniversalBleException) rethrow;
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.failed,
|
|
message: 'Failed to start Linux BLE peripheral',
|
|
details: error,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> stopPeripheral() async {
|
|
await _stopPeripheralInternal();
|
|
}
|
|
|
|
@override
|
|
Future<void> updatePeripheralCharacteristicValue(
|
|
String service,
|
|
String characteristic,
|
|
Uint8List value,
|
|
) async {
|
|
final localCharacteristic =
|
|
_peripheralCharacteristics[_peripheralCharacteristicKey(service, characteristic)];
|
|
if (localCharacteristic == null) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.characteristicNotFound,
|
|
message: 'Unknown peripheral characteristic $characteristic',
|
|
);
|
|
}
|
|
localCharacteristic.updateValue(value);
|
|
}
|
|
|
|
@override
|
|
Future<void> notifyPeripheralCharacteristic(
|
|
String service,
|
|
String characteristic,
|
|
Uint8List value, {
|
|
bool indicate = false,
|
|
}) async {
|
|
final localCharacteristic =
|
|
_peripheralCharacteristics[_peripheralCharacteristicKey(service, characteristic)];
|
|
if (localCharacteristic == null) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.characteristicNotFound,
|
|
message: 'Unknown peripheral characteristic $characteristic',
|
|
);
|
|
}
|
|
await localCharacteristic.notifyValue(value);
|
|
}
|
|
|
|
@override
|
|
Future<int> readRssi(String deviceId) async {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.notImplemented,
|
|
message: "readRssi is not implemented on Linux platform",
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<bool> pair(String deviceId) async {
|
|
BlueZDevice device = _findDeviceById(deviceId);
|
|
try {
|
|
if (device.paired) return true;
|
|
await device.pair();
|
|
return true;
|
|
} catch (error) {
|
|
updatePairingState(deviceId, false);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> unpair(String deviceId) async {
|
|
BlueZDevice device = _findDeviceById(deviceId);
|
|
if (device.paired) {
|
|
// await device.cancelPairing();
|
|
await _activeAdapter?.removeDevice(device);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> isPaired(String deviceId) async {
|
|
return _findDeviceById(deviceId).paired;
|
|
}
|
|
|
|
@override
|
|
Future<List<BleDevice>> getSystemDevices(List<String>? withServices) async {
|
|
await _ensureInitialized();
|
|
List<BlueZDevice> devices = _client.devices.where((device) => device.connected).toList();
|
|
if (withServices != null && withServices.isNotEmpty) {
|
|
devices = devices.where((device) {
|
|
if (device.servicesResolved) {
|
|
return device.gattServices
|
|
.map((e) => e.uuid.toString())
|
|
.any((service) => withServices.contains(service));
|
|
} else {
|
|
UniversalLogger.logInfo('Skipping: ${device.address}: Services not resolved yet.');
|
|
return false;
|
|
}
|
|
}).toList();
|
|
}
|
|
return devices.map((device) => device.toBleDevice(isSystemDevice: true)).toList();
|
|
}
|
|
|
|
AvailabilityState get _availabilityState {
|
|
return _activeAdapter?.powered == true
|
|
? AvailabilityState.poweredOn
|
|
: AvailabilityState.poweredOff;
|
|
}
|
|
|
|
/// Find device by id from cache or from client
|
|
/// Throws exception if device not found
|
|
BlueZDevice _findDeviceById(String deviceId) {
|
|
final device = _getDeviceById(deviceId);
|
|
if (device == null) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.deviceNotFound,
|
|
message: 'Unknown deviceId:$deviceId',
|
|
);
|
|
}
|
|
return device;
|
|
}
|
|
|
|
/// Get device by id from cache or from client
|
|
BlueZDevice? _getDeviceById(String deviceId) {
|
|
return _devices[deviceId] ??
|
|
_client.devices.cast<BlueZDevice?>().firstWhere(
|
|
(device) => device?.address == deviceId,
|
|
orElse: () => null,
|
|
);
|
|
}
|
|
|
|
Future<void> _ensureInitialized() async {
|
|
if (isInitialized) return;
|
|
|
|
if (_initializationCompleter != null) {
|
|
await _initializationCompleter?.future;
|
|
return;
|
|
}
|
|
|
|
_initializationCompleter = Completer<void>();
|
|
try {
|
|
await _client.connect();
|
|
await _waitForAdapter(_client);
|
|
|
|
_activeAdapter ??= _client.adapters.first;
|
|
|
|
UniversalLogger.logInfo('BleAdapter: ${_activeAdapter?.name} - ${_activeAdapter?.address}');
|
|
|
|
_activeAdapter?.propertiesChanged.listen((List<String> properties) {
|
|
// Handle pairing state change
|
|
for (final property in properties) {
|
|
switch (property) {
|
|
case BluezProperty.powered:
|
|
updateAvailability(_availabilityState);
|
|
break;
|
|
case BluezProperty.discoverable:
|
|
case BluezProperty.discovering:
|
|
// print("Adapter Discovering: ${_activeAdapter?.discovering}");
|
|
break;
|
|
case BluezProperty.propertyClass:
|
|
default:
|
|
UniversalLogger.logInfo("UnhandledPropertyChanged: $property");
|
|
}
|
|
}
|
|
});
|
|
|
|
updateAvailability(_availabilityState);
|
|
isInitialized = true;
|
|
_initializationCompleter?.complete();
|
|
_initializationCompleter = null;
|
|
} catch (e) {
|
|
UniversalLogger.logError('Error initializing: $e');
|
|
_initializationCompleter?.completeError(e);
|
|
await _client.close();
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<void> _waitForAdapter(BlueZClient client) async {
|
|
if (client.adapters.isNotEmpty) return;
|
|
|
|
int attempts = 0;
|
|
while (attempts < 10 && client.adapters.isEmpty) {
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
attempts++;
|
|
}
|
|
|
|
if (client.adapters.isEmpty) {
|
|
throw UniversalBleException(
|
|
code: UniversalBleErrorCode.bluetoothNotAvailable,
|
|
message: 'Bluetooth adapter unavailable',
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<bool> _isLinuxPeripheralSupported() async {
|
|
final adapter = _activeAdapter;
|
|
if (adapter == null || !adapter.powered) return false;
|
|
if (adapter.roles.isNotEmpty && !adapter.roles.contains('peripheral')) {
|
|
return false;
|
|
}
|
|
|
|
final bus = DBusClient.system();
|
|
try {
|
|
final adapterPath = await _resolveActiveAdapterPath(bus);
|
|
if (adapterPath == null) return false;
|
|
|
|
final adapterObject = _blueZAdapterObject(bus, adapterPath);
|
|
final introspection = await adapterObject.introspect();
|
|
final interfaces = introspection.interfaces.map((interface) => interface.name).toSet();
|
|
return interfaces.contains('org.bluez.GattManager1') &&
|
|
interfaces.contains('org.bluez.LEAdvertisingManager1');
|
|
} catch (error) {
|
|
UniversalLogger.logInfo('Linux peripheral support check failed: $error');
|
|
return false;
|
|
} finally {
|
|
await bus.close();
|
|
}
|
|
}
|
|
|
|
Future<DBusObjectPath?> _resolveActiveAdapterPath(DBusClient bus) async {
|
|
final adapter = _activeAdapter;
|
|
if (adapter == null) return null;
|
|
|
|
final root = DBusRemoteObjectManager(bus, name: 'org.bluez', path: DBusObjectPath('/'));
|
|
final managedObjects = await root.getManagedObjects();
|
|
final adapterAddress = adapter.address.toUpperCase();
|
|
|
|
for (final entry in managedObjects.entries) {
|
|
final adapterProperties = entry.value['org.bluez.Adapter1'];
|
|
if (adapterProperties == null) continue;
|
|
try {
|
|
final candidateAddress = adapterProperties['Address']?.asString().toUpperCase();
|
|
if (candidateAddress == adapterAddress) return entry.key;
|
|
} catch (_) {}
|
|
}
|
|
|
|
final adapterName = adapter.name;
|
|
if (RegExp(r'^hci\d+$').hasMatch(adapterName)) {
|
|
return DBusObjectPath('/org/bluez/$adapterName');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
DBusRemoteObject _blueZAdapterObject(DBusClient bus, DBusObjectPath adapterPath) {
|
|
return DBusRemoteObject(bus, name: 'org.bluez', path: adapterPath);
|
|
}
|
|
|
|
Future<void> _registerPeripheralObjects(
|
|
DBusClient bus,
|
|
DBusObjectPath applicationPath,
|
|
BlePeripheralConfig config,
|
|
) async {
|
|
for (var serviceIndex = 0; serviceIndex < config.services.length; serviceIndex++) {
|
|
final serviceConfig = config.services[serviceIndex];
|
|
final servicePath = DBusObjectPath('${applicationPath.value}/service$serviceIndex');
|
|
final service = _BlueZPeripheralService(servicePath, uuid: serviceConfig.uuid);
|
|
await bus.registerObject(service);
|
|
_peripheralObjects.add(service);
|
|
|
|
for (
|
|
var characteristicIndex = 0;
|
|
characteristicIndex < serviceConfig.characteristics.length;
|
|
characteristicIndex++
|
|
) {
|
|
final characteristicConfig = serviceConfig.characteristics[characteristicIndex];
|
|
final characteristicPath = DBusObjectPath('${servicePath.value}/char$characteristicIndex');
|
|
final characteristic = _BlueZPeripheralCharacteristic(
|
|
characteristicPath,
|
|
servicePath: servicePath,
|
|
serviceUuid: serviceConfig.uuid,
|
|
uuid: characteristicConfig.uuid,
|
|
properties: characteristicConfig.properties,
|
|
initialValue: characteristicConfig.initialValue,
|
|
onWrite: updatePeripheralWrite,
|
|
onSubscription: updatePeripheralSubscription,
|
|
);
|
|
await bus.registerObject(characteristic);
|
|
_peripheralObjects.add(characteristic);
|
|
_peripheralCharacteristics[_peripheralCharacteristicKey(
|
|
serviceConfig.uuid,
|
|
characteristicConfig.uuid,
|
|
)] =
|
|
characteristic;
|
|
|
|
for (
|
|
var descriptorIndex = 0;
|
|
descriptorIndex < characteristicConfig.descriptors.length;
|
|
descriptorIndex++
|
|
) {
|
|
final descriptorConfig = characteristicConfig.descriptors[descriptorIndex];
|
|
final descriptorPath = DBusObjectPath('${characteristicPath.value}/desc$descriptorIndex');
|
|
final descriptor = _BlueZPeripheralDescriptor(
|
|
descriptorPath,
|
|
characteristicPath: characteristicPath,
|
|
uuid: descriptorConfig.uuid,
|
|
permissions: descriptorConfig.permissions,
|
|
initialValue: descriptorConfig.initialValue,
|
|
);
|
|
await bus.registerObject(descriptor);
|
|
_peripheralObjects.add(descriptor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _stopPeripheralInternal() async {
|
|
final adapter = _activeAdapter;
|
|
final advertisement = _peripheralAdvertisement;
|
|
_peripheralAdvertisement = null;
|
|
if (advertisement != null && adapter != null) {
|
|
try {
|
|
await adapter.advertisingManager.unregisterAdvertisement(advertisement);
|
|
} catch (error) {
|
|
UniversalLogger.logInfo('Linux peripheral advertisement cleanup failed: $error');
|
|
}
|
|
}
|
|
|
|
final bus = _peripheralBus;
|
|
final application = _peripheralApplication;
|
|
final adapterPath = _peripheralAdapterPath;
|
|
if (bus != null &&
|
|
application != null &&
|
|
adapterPath != null &&
|
|
_peripheralApplicationRegistered) {
|
|
try {
|
|
await _blueZAdapterObject(bus, adapterPath).callMethod(
|
|
'org.bluez.GattManager1',
|
|
'UnregisterApplication',
|
|
[application.path],
|
|
replySignature: DBusSignature(''),
|
|
);
|
|
} catch (error) {
|
|
UniversalLogger.logInfo('Linux peripheral GATT cleanup failed: $error');
|
|
}
|
|
}
|
|
_peripheralApplicationRegistered = false;
|
|
|
|
if (bus != null) {
|
|
for (final object in _peripheralObjects.reversed) {
|
|
try {
|
|
await bus.unregisterObject(object);
|
|
} catch (_) {}
|
|
}
|
|
_peripheralObjects.clear();
|
|
|
|
if (application != null) {
|
|
try {
|
|
await bus.unregisterObject(application);
|
|
} catch (_) {}
|
|
}
|
|
await bus.close();
|
|
}
|
|
|
|
_peripheralBus = null;
|
|
_peripheralApplication = null;
|
|
_peripheralAdapterPath = null;
|
|
_peripheralCharacteristics.clear();
|
|
}
|
|
|
|
String _peripheralCharacteristicKey(String service, String characteristic) =>
|
|
'${BleUuidParser.string(service)}|${BleUuidParser.string(characteristic)}';
|
|
|
|
void _onDeviceAdd(BlueZDevice device) {
|
|
BleDevice bleDevice = device.toBleDevice();
|
|
if (!_bleFilter.shouldAcceptDevice(bleDevice)) {
|
|
return;
|
|
}
|
|
|
|
// Update scan results only if rssi is available
|
|
if (device.rssi != 0) {
|
|
updateScanResult(bleDevice);
|
|
}
|
|
|
|
// Setup Cache
|
|
_devices[device.address] = device;
|
|
|
|
// Setup advertisements Listener
|
|
_deviceAdvertisementSubscriptions[device.address] ??= device.propertiesChanged
|
|
.where((e) {
|
|
return e.contains(BluezProperty.rssi) ||
|
|
e.contains(BluezProperty.manufacturerData) ||
|
|
e.contains(BluezProperty.uuids) ||
|
|
e.contains(BluezProperty.serviceData);
|
|
})
|
|
.listen((_) {
|
|
if (_bleFilter.shouldAcceptDevice(bleDevice)) {
|
|
updateScanResult(device.toBleDevice());
|
|
}
|
|
});
|
|
|
|
// Setup update listener
|
|
_deviceUpdateStreamSubscriptions[device.address] ??= device.propertiesChanged.listen((
|
|
properties,
|
|
) {
|
|
for (final property in properties) {
|
|
switch (property) {
|
|
// Connection/Pair updates
|
|
case BluezProperty.connected:
|
|
updateConnection(device.address, device.connected);
|
|
break;
|
|
case BluezProperty.paired:
|
|
updatePairingState(device.address, device.paired);
|
|
break;
|
|
// Ignored these properties updates
|
|
case BluezProperty.bonded:
|
|
case BluezProperty.legacyPairing:
|
|
case BluezProperty.servicesResolved:
|
|
case BluezProperty.uuids:
|
|
case BluezProperty.txPower:
|
|
case BluezProperty.address:
|
|
case BluezProperty.addressType:
|
|
case BluezProperty.rssi:
|
|
case BluezProperty.manufacturerData:
|
|
case BluezProperty.serviceData:
|
|
break;
|
|
default:
|
|
UniversalLogger.logInfo(
|
|
"UnhandledDevicePropertyChanged ${device.name} ${device.address}: $property",
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _onDeviceRemoved(BlueZDevice device) {
|
|
_devices.remove(device.address);
|
|
// Clean Update listeners
|
|
_deviceUpdateStreamSubscriptions.removeWhere((key, value) {
|
|
if (key == device.address) {
|
|
value.cancel();
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
// Clean Advertisement listeners
|
|
_deviceAdvertisementSubscriptions.removeWhere((key, value) {
|
|
if (key == device.address) {
|
|
value.cancel();
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
|
|
class _BlueZPeripheralApplication extends DBusObject {
|
|
_BlueZPeripheralApplication(super.path) : super(isObjectManager: true);
|
|
}
|
|
|
|
class _BlueZPeripheralService extends DBusObject {
|
|
_BlueZPeripheralService(super.path, {required this.uuid});
|
|
|
|
static const _interfaceName = 'org.bluez.GattService1';
|
|
|
|
final String uuid;
|
|
|
|
Map<String, DBusValue> _properties() => {'UUID': DBusString(uuid), 'Primary': DBusBoolean(true)};
|
|
|
|
@override
|
|
Map<String, Map<String, DBusValue>> get interfacesAndProperties => {
|
|
_interfaceName: _properties(),
|
|
};
|
|
|
|
@override
|
|
Future<DBusMethodResponse> getProperty(String interface, String name) async {
|
|
if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
|
|
final value = _properties()[name];
|
|
if (value == null) return DBusMethodErrorResponse.unknownProperty();
|
|
return DBusGetPropertyResponse(value);
|
|
}
|
|
|
|
@override
|
|
Future<DBusMethodResponse> getAllProperties(String interface) async {
|
|
return DBusGetAllPropertiesResponse(
|
|
interface == _interfaceName ? _properties() : <String, DBusValue>{},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<DBusMethodResponse> setProperty(String interface, String name, DBusValue value) async {
|
|
if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
|
|
return _properties().containsKey(name)
|
|
? DBusMethodErrorResponse.propertyReadOnly()
|
|
: DBusMethodErrorResponse.unknownProperty();
|
|
}
|
|
|
|
@override
|
|
List<DBusIntrospectInterface> introspect() => [
|
|
DBusIntrospectInterface(
|
|
_interfaceName,
|
|
properties: [
|
|
DBusIntrospectProperty('UUID', DBusSignature('s'), access: DBusPropertyAccess.read),
|
|
DBusIntrospectProperty('Primary', DBusSignature('b'), access: DBusPropertyAccess.read),
|
|
],
|
|
),
|
|
];
|
|
}
|
|
|
|
class _BlueZPeripheralCharacteristic extends DBusObject {
|
|
_BlueZPeripheralCharacteristic(
|
|
super.path, {
|
|
required this.servicePath,
|
|
required this.serviceUuid,
|
|
required this.uuid,
|
|
required this.properties,
|
|
required Uint8List? initialValue,
|
|
required this.onWrite,
|
|
required this.onSubscription,
|
|
}) : _value = Uint8List.fromList(initialValue ?? Uint8List(0));
|
|
|
|
static const _interfaceName = 'org.bluez.GattCharacteristic1';
|
|
|
|
final DBusObjectPath servicePath;
|
|
final String serviceUuid;
|
|
final String uuid;
|
|
final List<CharacteristicProperty> properties;
|
|
final void Function(BlePeripheralWriteEvent event) onWrite;
|
|
final void Function(BlePeripheralSubscriptionEvent event) onSubscription;
|
|
|
|
Uint8List _value;
|
|
bool _notifying = false;
|
|
|
|
Map<String, DBusValue> _properties() => {
|
|
'UUID': DBusString(uuid),
|
|
'Service': servicePath,
|
|
'Value': DBusArray.byte(_value),
|
|
'Notifying': DBusBoolean(_notifying),
|
|
'Flags': DBusArray.string(properties.map((property) => property.bluezFlag)),
|
|
};
|
|
|
|
@override
|
|
Map<String, Map<String, DBusValue>> get interfacesAndProperties => {
|
|
_interfaceName: _properties(),
|
|
};
|
|
|
|
void updateValue(Uint8List value) {
|
|
_value = Uint8List.fromList(value);
|
|
}
|
|
|
|
Future<void> notifyValue(Uint8List value) async {
|
|
updateValue(value);
|
|
await emitPropertiesChanged(
|
|
_interfaceName,
|
|
changedProperties: {'Value': DBusArray.byte(_value)},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<DBusMethodResponse> handleMethodCall(DBusMethodCall methodCall) async {
|
|
if (methodCall.interface != _interfaceName) {
|
|
return DBusMethodErrorResponse.unknownInterface();
|
|
}
|
|
|
|
switch (methodCall.name) {
|
|
case 'ReadValue':
|
|
if (methodCall.values.length != 1) return DBusMethodErrorResponse.invalidArgs();
|
|
final options = methodCall.values[0].asStringVariantDict();
|
|
final offset = _offsetFromOptions(options);
|
|
if (offset > _value.length) {
|
|
return DBusMethodErrorResponse('org.bluez.Error.InvalidOffset', [
|
|
DBusString('Invalid offset'),
|
|
]);
|
|
}
|
|
return DBusMethodSuccessResponse([DBusArray.byte(_value.sublist(offset))]);
|
|
case 'WriteValue':
|
|
if (methodCall.values.length != 2) return DBusMethodErrorResponse.invalidArgs();
|
|
final incoming = Uint8List.fromList(methodCall.values[0].asByteArray().toList());
|
|
final options = methodCall.values[1].asStringVariantDict();
|
|
final offset = _offsetFromOptions(options);
|
|
if (offset > _value.length) {
|
|
return DBusMethodErrorResponse('org.bluez.Error.InvalidOffset', [
|
|
DBusString('Invalid offset'),
|
|
]);
|
|
}
|
|
_writeValue(offset, incoming);
|
|
onWrite(
|
|
BlePeripheralWriteEvent(
|
|
deviceId: _deviceIdFromOptions(options, methodCall),
|
|
service: serviceUuid,
|
|
characteristic: uuid,
|
|
value: incoming,
|
|
),
|
|
);
|
|
return DBusMethodSuccessResponse();
|
|
case 'StartNotify':
|
|
if (methodCall.values.isNotEmpty) return DBusMethodErrorResponse.invalidArgs();
|
|
_notifying = true;
|
|
await emitPropertiesChanged(
|
|
_interfaceName,
|
|
changedProperties: {'Notifying': DBusBoolean(true)},
|
|
);
|
|
onSubscription(
|
|
BlePeripheralSubscriptionEvent(
|
|
deviceId: methodCall.sender ?? '',
|
|
service: serviceUuid,
|
|
characteristic: uuid,
|
|
subscribed: true,
|
|
),
|
|
);
|
|
return DBusMethodSuccessResponse();
|
|
case 'StopNotify':
|
|
if (methodCall.values.isNotEmpty) return DBusMethodErrorResponse.invalidArgs();
|
|
_notifying = false;
|
|
await emitPropertiesChanged(
|
|
_interfaceName,
|
|
changedProperties: {'Notifying': DBusBoolean(false)},
|
|
);
|
|
onSubscription(
|
|
BlePeripheralSubscriptionEvent(
|
|
deviceId: methodCall.sender ?? '',
|
|
service: serviceUuid,
|
|
characteristic: uuid,
|
|
subscribed: false,
|
|
),
|
|
);
|
|
return DBusMethodSuccessResponse();
|
|
default:
|
|
return DBusMethodErrorResponse.unknownMethod();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<DBusMethodResponse> getProperty(String interface, String name) async {
|
|
if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
|
|
final value = _properties()[name];
|
|
if (value == null) return DBusMethodErrorResponse.unknownProperty();
|
|
return DBusGetPropertyResponse(value);
|
|
}
|
|
|
|
@override
|
|
Future<DBusMethodResponse> getAllProperties(String interface) async {
|
|
return DBusGetAllPropertiesResponse(
|
|
interface == _interfaceName ? _properties() : <String, DBusValue>{},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<DBusMethodResponse> setProperty(String interface, String name, DBusValue value) async {
|
|
if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
|
|
return _properties().containsKey(name)
|
|
? DBusMethodErrorResponse.propertyReadOnly()
|
|
: DBusMethodErrorResponse.unknownProperty();
|
|
}
|
|
|
|
@override
|
|
List<DBusIntrospectInterface> introspect() => [
|
|
DBusIntrospectInterface(
|
|
_interfaceName,
|
|
methods: [
|
|
DBusIntrospectMethod(
|
|
'ReadValue',
|
|
args: [
|
|
DBusIntrospectArgument(
|
|
DBusSignature('a{sv}'),
|
|
DBusArgumentDirection.in_,
|
|
name: 'options',
|
|
),
|
|
DBusIntrospectArgument(DBusSignature('ay'), DBusArgumentDirection.out, name: 'value'),
|
|
],
|
|
),
|
|
DBusIntrospectMethod(
|
|
'WriteValue',
|
|
args: [
|
|
DBusIntrospectArgument(DBusSignature('ay'), DBusArgumentDirection.in_, name: 'value'),
|
|
DBusIntrospectArgument(
|
|
DBusSignature('a{sv}'),
|
|
DBusArgumentDirection.in_,
|
|
name: 'options',
|
|
),
|
|
],
|
|
),
|
|
DBusIntrospectMethod('StartNotify'),
|
|
DBusIntrospectMethod('StopNotify'),
|
|
],
|
|
properties: [
|
|
DBusIntrospectProperty('UUID', DBusSignature('s'), access: DBusPropertyAccess.read),
|
|
DBusIntrospectProperty('Service', DBusSignature('o'), access: DBusPropertyAccess.read),
|
|
DBusIntrospectProperty('Value', DBusSignature('ay'), access: DBusPropertyAccess.read),
|
|
DBusIntrospectProperty('Notifying', DBusSignature('b'), access: DBusPropertyAccess.read),
|
|
DBusIntrospectProperty('Flags', DBusSignature('as'), access: DBusPropertyAccess.read),
|
|
],
|
|
),
|
|
];
|
|
|
|
void _writeValue(int offset, Uint8List incoming) {
|
|
if (offset == 0) {
|
|
_value = Uint8List.fromList(incoming);
|
|
return;
|
|
}
|
|
|
|
final nextLength = offset + incoming.length > _value.length
|
|
? offset + incoming.length
|
|
: _value.length;
|
|
final nextValue = Uint8List(nextLength);
|
|
nextValue.setRange(0, _value.length, _value);
|
|
nextValue.setRange(offset, offset + incoming.length, incoming);
|
|
_value = nextValue;
|
|
}
|
|
}
|
|
|
|
class _BlueZPeripheralDescriptor extends DBusObject {
|
|
_BlueZPeripheralDescriptor(
|
|
super.path, {
|
|
required this.characteristicPath,
|
|
required this.uuid,
|
|
required this.permissions,
|
|
required Uint8List? initialValue,
|
|
}) : _value = Uint8List.fromList(initialValue ?? Uint8List(0));
|
|
|
|
static const _interfaceName = 'org.bluez.GattDescriptor1';
|
|
|
|
final DBusObjectPath characteristicPath;
|
|
final String uuid;
|
|
final List<BlePeripheralCharacteristicPermission> permissions;
|
|
Uint8List _value;
|
|
|
|
Map<String, DBusValue> _properties() => {
|
|
'UUID': DBusString(uuid),
|
|
'Characteristic': characteristicPath,
|
|
'Value': DBusArray.byte(_value),
|
|
'Flags': DBusArray.string(permissions.map((permission) => permission.bluezFlag)),
|
|
};
|
|
|
|
@override
|
|
Map<String, Map<String, DBusValue>> get interfacesAndProperties => {
|
|
_interfaceName: _properties(),
|
|
};
|
|
|
|
@override
|
|
Future<DBusMethodResponse> handleMethodCall(DBusMethodCall methodCall) async {
|
|
if (methodCall.interface != _interfaceName) {
|
|
return DBusMethodErrorResponse.unknownInterface();
|
|
}
|
|
|
|
switch (methodCall.name) {
|
|
case 'ReadValue':
|
|
if (methodCall.values.length != 1) return DBusMethodErrorResponse.invalidArgs();
|
|
final offset = _offsetFromOptions(methodCall.values[0].asStringVariantDict());
|
|
if (offset > _value.length) {
|
|
return DBusMethodErrorResponse('org.bluez.Error.InvalidOffset', [
|
|
DBusString('Invalid offset'),
|
|
]);
|
|
}
|
|
return DBusMethodSuccessResponse([DBusArray.byte(_value.sublist(offset))]);
|
|
case 'WriteValue':
|
|
if (methodCall.values.length != 2) return DBusMethodErrorResponse.invalidArgs();
|
|
final incoming = Uint8List.fromList(methodCall.values[0].asByteArray().toList());
|
|
final offset = _offsetFromOptions(methodCall.values[1].asStringVariantDict());
|
|
if (offset > _value.length) {
|
|
return DBusMethodErrorResponse('org.bluez.Error.InvalidOffset', [
|
|
DBusString('Invalid offset'),
|
|
]);
|
|
}
|
|
if (offset == 0) {
|
|
_value = incoming;
|
|
} else {
|
|
final nextLength = offset + incoming.length > _value.length
|
|
? offset + incoming.length
|
|
: _value.length;
|
|
final nextValue = Uint8List(nextLength);
|
|
nextValue.setRange(0, _value.length, _value);
|
|
nextValue.setRange(offset, offset + incoming.length, incoming);
|
|
_value = nextValue;
|
|
}
|
|
return DBusMethodSuccessResponse();
|
|
default:
|
|
return DBusMethodErrorResponse.unknownMethod();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<DBusMethodResponse> getProperty(String interface, String name) async {
|
|
if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
|
|
final value = _properties()[name];
|
|
if (value == null) return DBusMethodErrorResponse.unknownProperty();
|
|
return DBusGetPropertyResponse(value);
|
|
}
|
|
|
|
@override
|
|
Future<DBusMethodResponse> getAllProperties(String interface) async {
|
|
return DBusGetAllPropertiesResponse(
|
|
interface == _interfaceName ? _properties() : <String, DBusValue>{},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<DBusMethodResponse> setProperty(String interface, String name, DBusValue value) async {
|
|
if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
|
|
return _properties().containsKey(name)
|
|
? DBusMethodErrorResponse.propertyReadOnly()
|
|
: DBusMethodErrorResponse.unknownProperty();
|
|
}
|
|
|
|
@override
|
|
List<DBusIntrospectInterface> introspect() => [
|
|
DBusIntrospectInterface(
|
|
_interfaceName,
|
|
methods: [
|
|
DBusIntrospectMethod(
|
|
'ReadValue',
|
|
args: [
|
|
DBusIntrospectArgument(
|
|
DBusSignature('a{sv}'),
|
|
DBusArgumentDirection.in_,
|
|
name: 'options',
|
|
),
|
|
DBusIntrospectArgument(DBusSignature('ay'), DBusArgumentDirection.out, name: 'value'),
|
|
],
|
|
),
|
|
DBusIntrospectMethod(
|
|
'WriteValue',
|
|
args: [
|
|
DBusIntrospectArgument(DBusSignature('ay'), DBusArgumentDirection.in_, name: 'value'),
|
|
DBusIntrospectArgument(
|
|
DBusSignature('a{sv}'),
|
|
DBusArgumentDirection.in_,
|
|
name: 'options',
|
|
),
|
|
],
|
|
),
|
|
],
|
|
properties: [
|
|
DBusIntrospectProperty('UUID', DBusSignature('s'), access: DBusPropertyAccess.read),
|
|
DBusIntrospectProperty(
|
|
'Characteristic',
|
|
DBusSignature('o'),
|
|
access: DBusPropertyAccess.read,
|
|
),
|
|
DBusIntrospectProperty('Value', DBusSignature('ay'), access: DBusPropertyAccess.read),
|
|
DBusIntrospectProperty('Flags', DBusSignature('as'), access: DBusPropertyAccess.read),
|
|
],
|
|
),
|
|
];
|
|
}
|
|
|
|
int _offsetFromOptions(Map<String, DBusValue> options) {
|
|
final offset = options['offset'];
|
|
if (offset == null) return 0;
|
|
try {
|
|
return offset.asUint16();
|
|
} catch (_) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
String _deviceIdFromOptions(Map<String, DBusValue> options, DBusMethodCall methodCall) {
|
|
try {
|
|
final devicePath = options['device']?.asObjectPath().value;
|
|
if (devicePath != null) {
|
|
final markerIndex = devicePath.lastIndexOf('/dev_');
|
|
if (markerIndex >= 0) {
|
|
return devicePath.substring(markerIndex + 5).replaceAll('_', ':').toUpperCase();
|
|
}
|
|
return devicePath;
|
|
}
|
|
} catch (_) {}
|
|
return methodCall.sender ?? '';
|
|
}
|
|
|
|
extension on CharacteristicProperty {
|
|
String get bluezFlag => switch (this) {
|
|
CharacteristicProperty.broadcast => 'broadcast',
|
|
CharacteristicProperty.read => 'read',
|
|
CharacteristicProperty.writeWithoutResponse => 'write-without-response',
|
|
CharacteristicProperty.write => 'write',
|
|
CharacteristicProperty.notify => 'notify',
|
|
CharacteristicProperty.indicate => 'indicate',
|
|
CharacteristicProperty.authenticatedSignedWrites => 'authenticated-signed-writes',
|
|
CharacteristicProperty.extendedProperties => 'extended-properties',
|
|
};
|
|
}
|
|
|
|
extension on BlePeripheralCharacteristicPermission {
|
|
String get bluezFlag => switch (this) {
|
|
BlePeripheralCharacteristicPermission.read => 'read',
|
|
BlePeripheralCharacteristicPermission.write => 'write',
|
|
};
|
|
}
|
|
|
|
class BluezProperty {
|
|
static const String rssi = 'RSSI';
|
|
static const String connected = 'Connected';
|
|
static const String txPower = 'TxPower';
|
|
static const String bonded = 'Bonded';
|
|
static const String manufacturerData = 'ManufacturerData';
|
|
static const String serviceData = 'ServiceData';
|
|
static const String legacyPairing = 'LegacyPairing';
|
|
static const String servicesResolved = 'ServicesResolved';
|
|
static const String paired = 'Paired';
|
|
static const String address = 'Address';
|
|
static const String addressType = 'AddressType';
|
|
static const String modalias = 'Modalias';
|
|
static const String uuids = 'UUIDs';
|
|
static const String value = 'Value';
|
|
static const String powered = 'Powered';
|
|
static const String discoverable = 'Discoverable';
|
|
static const String discovering = 'Discovering';
|
|
static const String propertyClass = 'Class';
|
|
}
|
|
|
|
extension on BlueZGattCharacteristicFlag {
|
|
CharacteristicProperty? toCharacteristicProperty() {
|
|
return switch (this) {
|
|
BlueZGattCharacteristicFlag.broadcast => CharacteristicProperty.broadcast,
|
|
BlueZGattCharacteristicFlag.read => CharacteristicProperty.read,
|
|
BlueZGattCharacteristicFlag.writeWithoutResponse =>
|
|
CharacteristicProperty.writeWithoutResponse,
|
|
BlueZGattCharacteristicFlag.write => CharacteristicProperty.write,
|
|
BlueZGattCharacteristicFlag.notify => CharacteristicProperty.notify,
|
|
BlueZGattCharacteristicFlag.indicate => CharacteristicProperty.indicate,
|
|
BlueZGattCharacteristicFlag.authenticatedSignedWrites =>
|
|
CharacteristicProperty.authenticatedSignedWrites,
|
|
BlueZGattCharacteristicFlag.extendedProperties => CharacteristicProperty.extendedProperties,
|
|
_ => null,
|
|
};
|
|
}
|
|
}
|
|
|
|
extension on BlueZFailedException {
|
|
UniversalBleException toUniversalBleException({required UniversalBleErrorCode defaultCode}) {
|
|
// Map BlueZ error code to UniversalBleErrorCode
|
|
UniversalBleErrorCode code = UniversalBleErrorParser.getCode(errorCode);
|
|
if (code == UniversalBleErrorCode.unknownError) {
|
|
code = defaultCode;
|
|
}
|
|
throw UniversalBleException(code: code, message: message, details: errorCode);
|
|
}
|
|
|
|
/// Extract error code from message and parse into decimal
|
|
/// example: 'Operation failed with ATT error: 0x90' => 144
|
|
String? get errorCode {
|
|
try {
|
|
RegExp regExp = RegExp(r'0x\w+');
|
|
Match? match = regExp.firstMatch(message);
|
|
String? code = match?.group(0);
|
|
if (code == null) return null;
|
|
int? decimalValue = int.tryParse(code.replaceFirst('0x', ''), radix: 16);
|
|
return decimalValue?.toString() ?? code;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
extension BlueZDeviceExtension on BlueZDevice {
|
|
List<ManufacturerData> get manufacturerDataList => manufacturerData.entries
|
|
.map(
|
|
(MapEntry<BlueZManufacturerId, List<int>> data) =>
|
|
ManufacturerData(data.key.id, Uint8List.fromList(data.value)),
|
|
)
|
|
.toList();
|
|
|
|
Map<String, Uint8List> get serviceDataMap {
|
|
try {
|
|
return {
|
|
for (final entry in serviceData.entries)
|
|
entry.key.toString(): Uint8List.fromList(entry.value),
|
|
};
|
|
} catch (e) {
|
|
return <String, Uint8List>{};
|
|
}
|
|
}
|
|
|
|
BleDevice toBleDevice({bool? isSystemDevice}) {
|
|
return BleDevice(
|
|
name: name,
|
|
deviceId: address,
|
|
paired: paired,
|
|
rssi: rssi,
|
|
isSystemDevice: isSystemDevice,
|
|
services: uuids.map((e) => e.toString()).toList(),
|
|
manufacturerDataList: manufacturerDataList,
|
|
serviceData: serviceDataMap,
|
|
timestamp: DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
}
|
|
}
|