diff --git a/packages/flutter_blue_plus_linux/CHANGELOG.md b/packages/flutter_blue_plus_linux/CHANGELOG.md new file mode 100644 index 00000000..e20fdea3 --- /dev/null +++ b/packages/flutter_blue_plus_linux/CHANGELOG.md @@ -0,0 +1,37 @@ +## 8.2.2 +* **[Release]** version bump for the flutter_blue_plus 2.2.2 release + +## 8.2.1 +* **[LICENSE]** clarifications to the development exemption + +## 8.2.0 +* **[LICENSE]** new tiers based on company size & development exemption + +## 8.1.0 +* **[LICENSE]** commercial license is now required for 15 or more employees + +## 8.0.1 +* Add License symlink + +## 8.0.0 +* Switch to FBP License + +## 3.0.2 +* Return unknown state if adaptors list empty (#1190) +* Return empty name if adaptors list empty (#1191) +* Return start scan false if adaptors list empty (#1193) +* Return stop scan false if adaptors list empty (#1194) +* Return turn off false if adaptors list empty (#1195) +* Return turn on false if adaptors list empty (#1196) + +## 3.0.1 +* Implement on turn on response stream (#1174) + +## 3.0.0 +* Update platform interface version to 3.0.0 + +## 2.0.0 +* Replace void return types with bool return types + +## 1.0.0 +* Initial release diff --git a/packages/flutter_blue_plus_linux/LICENSE b/packages/flutter_blue_plus_linux/LICENSE new file mode 100644 index 00000000..08fd9343 --- /dev/null +++ b/packages/flutter_blue_plus_linux/LICENSE @@ -0,0 +1,77 @@ +FlutterBluePlus License +Version 1.4 + +Copyright (c) 2026 Chip Weinberger +All rights reserved. + +--- + +### Preamble (Non-Binding) + +The intent of this license is to make FlutterBluePlus ("The Software") as free and open as possible for personal users, researchers, nonprofits, educational institutions, and hobbyists. For these users, the terms are similar to the BSD license: broad freedom to use, modify, and share. + +For for-profit use and by for-profit organizations, a commercial license is required. + +The revenue from commercial licenses supports the long-term maintenance and development of FlutterBluePlus. +This approach allows hobbyists, nonprofits, and educators to benefit from free and open software, while ensuring that for-profit companies who rely on it help sustain its future. + +Please be aware that this project originated under the BSD-3 license, and some portions of the codebase remain under that license today. Compliance with those clauses is not required under the FlutterBluePlus License itself, but to remain fully compliant with the terms set by those contributors, it is advised that users continue to follow the BSD-3 conditions described in NOTICE.md. That relationship is between you and the BSD-licensed contributors. + +This preamble is provided for explanation only and does not form part of the binding legal terms of this license. + +--- + +### Section 1: Common Conditions (apply to all users) + +Permission is hereby granted to any individual or organization to use, copy, modify, and distribute the Software in source code or binary form, subject to the following conditions: + +**1. Source Code Redistributions.** Redistributions of source code must retain the above copyright notice and this entire license text without modification. + +**2. Relicensing Prohibition.** The Software may not be relicensed under different terms by any party. + +**3. Commercial Use Requirement.** Use of the Software by any for-profit organization requires a commercial license under Section 3, regardless of how the Software was obtained. + +**4. Nonprofit & Educational Exemption.** Registered nonprofit organizations and accredited educational institutions are exempt from this requirement and may use the Software under Section 2. + +--- + +### Section 2: Open Use Terms (for nonprofits, educational institutions, personal use) + +Use of the Software is free of charge for any registered nonprofit organization, accredited educational institution, or personal user, subject to the Common Conditions and the following additional terms: + +**1. Endorsement Restriction.** Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission. + +--- + +### Section 3: Commercial Terms (for for-profit organizations) + +Any use of FlutterBluePlus by or for a for-profit company or corporation — including commercial use by individuals — requires the purchase of a commercial license, subject to the Common Conditions and the following additional terms: + +**1. License Fee.** Commercial licenses are offered in the following tiers, based on employee count **at the time of purchase**. The associated cost is listed on the official FlutterBluePlus payment portal. + +- **Starter:** 0–9 employees +- **Team:** 10–29 employees +- **Business:** 30–99 employees +- **Enterprise:** 100 or more employees + +**2. Perpetual Use.** The license grants perpetual rights to use the Software and to receive all current and future software updates made available by the licensor after purchase. + +**3. Development Clarification.** Use of the Software during development, testing, or evaluation by a for-profit organization is considered commercial use and requires a commercial license. + +**4. Official Payment Portal.** Payment must be made via the official payment portal: https://jamcorder.myshopify.com/products/flutterblueplus-commercial-license + +**5. Validity of Offer.** This Section constitutes an offer only while this license version is the current version published in the main branch of the official FlutterBluePlus repository. Once superseded, replaced, or withdrawn, this version shall no longer be considered an active offer, even if archived copies remain publicly visible. + + +A valid commercial license covers use of all current and future packages that +are part of the FlutterBluePlus project, including its federated packages +(e.g., flutter_blue_plus, flutter_blue_plus_darwin, flutter_blue_plus_android, +flutter_blue_plus_web, flutter_blue_plus_linux, flutter_blue_plus_platform_interface). +Only a single commercial license is required per organization, regardless of how many +FlutterBluePlus packages are used. + +--- + +### Section 4: Warranty Disclaimer + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/flutter_blue_plus_linux/LICENSE.md b/packages/flutter_blue_plus_linux/LICENSE.md new file mode 100644 index 00000000..08fd9343 --- /dev/null +++ b/packages/flutter_blue_plus_linux/LICENSE.md @@ -0,0 +1,77 @@ +FlutterBluePlus License +Version 1.4 + +Copyright (c) 2026 Chip Weinberger +All rights reserved. + +--- + +### Preamble (Non-Binding) + +The intent of this license is to make FlutterBluePlus ("The Software") as free and open as possible for personal users, researchers, nonprofits, educational institutions, and hobbyists. For these users, the terms are similar to the BSD license: broad freedom to use, modify, and share. + +For for-profit use and by for-profit organizations, a commercial license is required. + +The revenue from commercial licenses supports the long-term maintenance and development of FlutterBluePlus. +This approach allows hobbyists, nonprofits, and educators to benefit from free and open software, while ensuring that for-profit companies who rely on it help sustain its future. + +Please be aware that this project originated under the BSD-3 license, and some portions of the codebase remain under that license today. Compliance with those clauses is not required under the FlutterBluePlus License itself, but to remain fully compliant with the terms set by those contributors, it is advised that users continue to follow the BSD-3 conditions described in NOTICE.md. That relationship is between you and the BSD-licensed contributors. + +This preamble is provided for explanation only and does not form part of the binding legal terms of this license. + +--- + +### Section 1: Common Conditions (apply to all users) + +Permission is hereby granted to any individual or organization to use, copy, modify, and distribute the Software in source code or binary form, subject to the following conditions: + +**1. Source Code Redistributions.** Redistributions of source code must retain the above copyright notice and this entire license text without modification. + +**2. Relicensing Prohibition.** The Software may not be relicensed under different terms by any party. + +**3. Commercial Use Requirement.** Use of the Software by any for-profit organization requires a commercial license under Section 3, regardless of how the Software was obtained. + +**4. Nonprofit & Educational Exemption.** Registered nonprofit organizations and accredited educational institutions are exempt from this requirement and may use the Software under Section 2. + +--- + +### Section 2: Open Use Terms (for nonprofits, educational institutions, personal use) + +Use of the Software is free of charge for any registered nonprofit organization, accredited educational institution, or personal user, subject to the Common Conditions and the following additional terms: + +**1. Endorsement Restriction.** Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission. + +--- + +### Section 3: Commercial Terms (for for-profit organizations) + +Any use of FlutterBluePlus by or for a for-profit company or corporation — including commercial use by individuals — requires the purchase of a commercial license, subject to the Common Conditions and the following additional terms: + +**1. License Fee.** Commercial licenses are offered in the following tiers, based on employee count **at the time of purchase**. The associated cost is listed on the official FlutterBluePlus payment portal. + +- **Starter:** 0–9 employees +- **Team:** 10–29 employees +- **Business:** 30–99 employees +- **Enterprise:** 100 or more employees + +**2. Perpetual Use.** The license grants perpetual rights to use the Software and to receive all current and future software updates made available by the licensor after purchase. + +**3. Development Clarification.** Use of the Software during development, testing, or evaluation by a for-profit organization is considered commercial use and requires a commercial license. + +**4. Official Payment Portal.** Payment must be made via the official payment portal: https://jamcorder.myshopify.com/products/flutterblueplus-commercial-license + +**5. Validity of Offer.** This Section constitutes an offer only while this license version is the current version published in the main branch of the official FlutterBluePlus repository. Once superseded, replaced, or withdrawn, this version shall no longer be considered an active offer, even if archived copies remain publicly visible. + + +A valid commercial license covers use of all current and future packages that +are part of the FlutterBluePlus project, including its federated packages +(e.g., flutter_blue_plus, flutter_blue_plus_darwin, flutter_blue_plus_android, +flutter_blue_plus_web, flutter_blue_plus_linux, flutter_blue_plus_platform_interface). +Only a single commercial license is required per organization, regardless of how many +FlutterBluePlus packages are used. + +--- + +### Section 4: Warranty Disclaimer + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/flutter_blue_plus_linux/NOTICE.md b/packages/flutter_blue_plus_linux/NOTICE.md new file mode 100644 index 00000000..e9869a1e --- /dev/null +++ b/packages/flutter_blue_plus_linux/NOTICE.md @@ -0,0 +1,39 @@ +To comply with the requirements of the original BSD-style license, +the full text of that license is reproduced below. It applies to all +releases of this project up to and including version 1.36.8, published +before October 2025. Those releases remain available under the terms +below, which are irrevocable. + +All releases beginning with version 2.0.0, published in October 2025 +and onward, are licensed under the FlutterBluePlus License. + +------ + +Copyright 2017-2024, Paul DeMarco, Bosko Popovic, Charles Weinberger, Thomas Clark. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Buffalo PC Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/flutter_blue_plus_linux/README.md b/packages/flutter_blue_plus_linux/README.md new file mode 100644 index 00000000..83795a79 --- /dev/null +++ b/packages/flutter_blue_plus_linux/README.md @@ -0,0 +1,15 @@ +# flutter_blue_plus_linux + +The Linux implementation of [`flutter_blue_plus`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `flutter_blue_plus` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/flutter_blue_plus +[2]: https://flutter.dev/to/endorsed-federated-plugin diff --git a/packages/flutter_blue_plus_linux/analysis_options.yaml b/packages/flutter_blue_plus_linux/analysis_options.yaml new file mode 100644 index 00000000..1e432d55 --- /dev/null +++ b/packages/flutter_blue_plus_linux/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/tools/linter-rules. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/tools/analysis diff --git a/packages/flutter_blue_plus_linux/lib/flutter_blue_plus_linux.dart b/packages/flutter_blue_plus_linux/lib/flutter_blue_plus_linux.dart new file mode 100644 index 00000000..ad4a88ce --- /dev/null +++ b/packages/flutter_blue_plus_linux/lib/flutter_blue_plus_linux.dart @@ -0,0 +1,1133 @@ +import 'dart:async'; + +import 'package:bluez/bluez.dart'; +import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart'; +import 'package:rxdart/rxdart.dart'; + +extension on BlueZDevice { + DeviceIdentifier get remoteId { + return DeviceIdentifier(address); + } +} + +extension on BlueZGattCharacteristic { + int instanceId(BlueZGattService service) { + final target = Guid.fromBytes(uuid.value); + var idx = 0; + for (final c in service.characteristics) { + if (Guid.fromBytes(c.uuid.value) == target) { + if (identical(c, this)) return idx; + idx++; + } + } + return 0; + } +} + +final class FlutterBluePlusLinux extends FlutterBluePlusPlatform { + final _client = BlueZClient(); + + var _initialized = false; + var _logLevel = LogLevel.none; + + final _onCharacteristicReadController = StreamController.broadcast(); + final _onCharacteristicWrittenController = StreamController.broadcast(); + final _onDescriptorReadController = StreamController.broadcast(); + final _onDescriptorWrittenController = StreamController.broadcast(); + final _onDiscoveredServicesController = StreamController.broadcast(); + final _onReadRssiController = StreamController.broadcast(); + final _onTurnOnResponseController = StreamController.broadcast(); + + @override + Stream get onAdapterStateChanged { + return _client.adaptersChanged.where( + (adapters) { + return adapters.isNotEmpty; + }, + ).switchMap( + (adapters) { + return adapters.first.propertiesChanged.where( + (properties) { + return properties.contains('Powered'); + }, + ).map( + (properties) { + return BmBluetoothAdapterState( + adapterState: adapters.first.powered ? BmAdapterStateEnum.on : BmAdapterStateEnum.off, + ); + }, + ); + }, + ); + } + + @override + Stream get onBondStateChanged { + return _client.devicesChanged.switchMap( + (devices) { + return MergeStream( + devices.map( + (device) { + return device.propertiesChanged.where( + (properties) { + return properties.contains('Paired'); + }, + ).map( + (properties) { + return BmBondStateResponse( + remoteId: device.remoteId, + bondState: device.paired ? BmBondStateEnum.bonded : BmBondStateEnum.none, + prevState: null, + ); + }, + ); + }, + ), + ); + }, + ); + } + + @override + Stream get onCharacteristicReceived { + return _onCharacteristicReadController.stream.mergeWith([ + _client.devicesChanged.switchMap( + (devices) { + final streams = >[]; + + for (final device in devices) { + for (final service in device.gattServices) { + for (final characteristic in service.characteristics) { + streams.add( + characteristic.propertiesChanged.where( + (properties) { + return properties.contains('Value'); + }, + ).map( + (properties) { + return BmCharacteristicData( + remoteId: device.remoteId, + primaryServiceUuid: null, + serviceUuid: Guid.fromBytes( + service.uuid.value, + ), + characteristicUuid: Guid.fromBytes( + characteristic.uuid.value, + ), + instanceId: characteristic.instanceId(service), + value: characteristic.value, + success: true, + errorCode: 0, + errorString: '', + ); + }, + ), + ); + } + } + } + + return MergeStream(streams); + }, + ), + ]); + } + + @override + Stream get onCharacteristicWritten { + return _onCharacteristicWrittenController.stream; + } + + @override + Stream get onConnectionStateChanged { + return _client.devicesChanged.switchMap( + (devices) { + return MergeStream( + devices.map( + (device) { + return device.propertiesChanged.where( + (properties) { + return properties.contains('Connected'); + }, + ).map( + (properties) { + return BmConnectionStateResponse( + remoteId: device.remoteId, + connectionState: + device.connected ? BmConnectionStateEnum.connected : BmConnectionStateEnum.disconnected, + disconnectReasonCode: null, + disconnectReasonString: null, + ); + }, + ); + }, + ), + ); + }, + ); + } + + @override + Stream get onDescriptorRead { + return _onDescriptorReadController.stream; + } + + @override + Stream get onDescriptorWritten { + return _onDescriptorWrittenController.stream; + } + + @override + Stream get onDiscoveredServices { + return _onDiscoveredServicesController.stream; + } + + @override + Stream get onMtuChanged { + return Stream.empty(); + } + + @override + Stream get onNameChanged { + return _client.devicesChanged.switchMap( + (devices) { + return MergeStream( + devices.map( + (device) { + return device.propertiesChanged.where( + (properties) { + return properties.contains('Name'); + }, + ).map( + (properties) { + return BmNameChanged( + remoteId: device.remoteId, + name: device.name, + ); + }, + ); + }, + ), + ); + }, + ); + } + + @override + Stream get onReadRssi { + return _onReadRssiController.stream; + } + + @override + Stream get onScanResponse { + return _client.deviceAdded.map( + (device) { + return BmScanResponse( + advertisements: [ + BmScanAdvertisement( + remoteId: device.remoteId, + platformName: device.name, + advName: null, + connectable: true, + txPowerLevel: device.txPower, + appearance: device.appearance, + manufacturerData: device.manufacturerData.map( + (id, value) { + return MapEntry(id.id, value); + }, + ), + serviceData: device.serviceData.map( + (uuid, value) { + return MapEntry(Guid.fromBytes(uuid.value), value); + }, + ), + serviceUuids: device.uuids.map( + (uuid) { + return Guid.fromBytes(uuid.value); + }, + ).toList(), + rssi: device.rssi, + ), + ], + success: true, + errorCode: 0, + errorString: '', + ); + }, + ); + } + + @override + Stream get onServicesReset { + return _client.devicesChanged.switchMap( + (devices) { + return MergeStream( + devices.map( + (device) { + return device.propertiesChanged.where( + (properties) { + return properties.contains('UUIDs'); + }, + ).map( + (properties) { + return BmBluetoothDevice( + remoteId: device.remoteId, + platformName: device.name, + ); + }, + ); + }, + ), + ); + }, + ); + } + + @override + Stream get onTurnOnResponse { + return _onTurnOnResponseController.stream; + } + + @override + Future connect( + BmConnectRequest request, + ) async { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + await device.connect(); + + return true; + } + + @override + Future createBond( + BmCreateBondRequest request, + ) async { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + await device.pair(); + + return true; + } + + @override + Future disconnect( + BmDisconnectRequest request, + ) async { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + await device.disconnect(); + + return true; + } + + @override + Future discoverServices( + BmDiscoverServicesRequest request, + ) async { + try { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + // Ensure BlueZ has completed service discovery before returning results + while (!device.servicesResolved) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + _onDiscoveredServicesController.add( + BmDiscoverServicesResult( + remoteId: device.remoteId, + services: device.gattServices.map( + (service) { + return BmBluetoothService( + primaryServiceUuid: null, + serviceUuid: Guid.fromBytes( + service.uuid.value, + ), + remoteId: device.remoteId, + characteristics: service.characteristics.map( + (characteristic) { + return BmBluetoothCharacteristic( + remoteId: device.remoteId, + primaryServiceUuid: null, + serviceUuid: Guid.fromBytes( + service.uuid.value, + ), + characteristicUuid: Guid.fromBytes( + characteristic.uuid.value, + ), + instanceId: characteristic.instanceId(service), + descriptors: characteristic.descriptors.map( + (descriptor) { + return BmBluetoothDescriptor( + remoteId: device.remoteId, + primaryServiceUuid: null, + serviceUuid: Guid.fromBytes( + service.uuid.value, + ), + characteristicUuid: Guid.fromBytes( + characteristic.uuid.value, + ), + instanceId: characteristic.instanceId(service), + descriptorUuid: Guid.fromBytes( + descriptor.uuid.value, + ), + ); + }, + ).toList(), + properties: BmCharacteristicProperties( + broadcast: characteristic.flags.contains( + BlueZGattCharacteristicFlag.broadcast, + ), + read: characteristic.flags.contains( + BlueZGattCharacteristicFlag.read, + ), + writeWithoutResponse: characteristic.flags.contains( + BlueZGattCharacteristicFlag.writeWithoutResponse, + ), + write: characteristic.flags.contains( + BlueZGattCharacteristicFlag.write, + ), + notify: characteristic.flags.contains( + BlueZGattCharacteristicFlag.notify, + ), + indicate: characteristic.flags.contains( + BlueZGattCharacteristicFlag.indicate, + ), + authenticatedSignedWrites: characteristic.flags.contains( + BlueZGattCharacteristicFlag.authenticatedSignedWrites, + ), + extendedProperties: characteristic.flags.contains( + BlueZGattCharacteristicFlag.extendedProperties, + ), + notifyEncryptionRequired: false, + indicateEncryptionRequired: false, + ), + ); + }, + ).toList(), + ); + }, + ).toList(), + success: true, + errorCode: 0, + errorString: '', + ), + ); + + return true; + } catch (e) { + _onDiscoveredServicesController.add( + BmDiscoverServicesResult( + remoteId: request.remoteId, + services: [], + success: false, + errorCode: 0, + errorString: '', + ), + ); + + return false; + } + } + + @override + Future getAdapterName( + BmBluetoothAdapterNameRequest request, + ) async { + await _initFlutterBluePlus(); + + return BmBluetoothAdapterName( + adapterName: _client.adapters.firstOrNull?.name ?? '', + ); + } + + @override + Future getAdapterState( + BmBluetoothAdapterStateRequest request, + ) async { + await _initFlutterBluePlus(); + + return BmBluetoothAdapterState( + adapterState: switch (_client.adapters.firstOrNull?.powered) { + true => BmAdapterStateEnum.on, + false => BmAdapterStateEnum.off, + _ => BmAdapterStateEnum.unknown, + }, + ); + } + + @override + Future getBondState( + BmBondStateRequest request, + ) async { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + return BmBondStateResponse( + remoteId: device.remoteId, + bondState: device.paired ? BmBondStateEnum.bonded : BmBondStateEnum.none, + prevState: null, + ); + } + + @override + Future getBondedDevices( + BmBondedDevicesRequest request, + ) async { + await _initFlutterBluePlus(); + + return BmDevicesList( + devices: _client.devices.where( + (device) { + return device.paired; + }, + ).map( + (device) { + return BmBluetoothDevice( + remoteId: device.remoteId, + platformName: device.name, + ); + }, + ).toList(), + ); + } + + @override + Future getSystemDevices( + BmSystemDevicesRequest request, + ) async { + await _initFlutterBluePlus(); + + return BmDevicesList( + devices: _client.devices.map( + (device) { + return BmBluetoothDevice( + remoteId: device.remoteId, + platformName: device.name, + ); + }, + ).toList(), + ); + } + + @override + Future isSupported( + BmIsSupportedRequest request, + ) async { + await _initFlutterBluePlus(); + + return _client.adapters.isNotEmpty; + } + + @override + Future readCharacteristic( + BmReadCharacteristicRequest request, + ) async { + try { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + final service = device.gattServices.singleWhere( + (service) { + final uuid = Guid.fromBytes( + service.uuid.value, + ); + + return uuid == request.serviceUuid; + }, + ); + + final characteristic = service.characteristics.singleWhere( + (characteristic) { + final uuid = Guid.fromBytes( + characteristic.uuid.value, + ); + + return uuid == request.characteristicUuid && (characteristic.instanceId(service) == request.instanceId); + }, + ); + + final value = await characteristic.readValue(); + + _onCharacteristicReadController.add( + BmCharacteristicData( + remoteId: device.remoteId, + primaryServiceUuid: null, + serviceUuid: Guid.fromBytes( + service.uuid.value, + ), + characteristicUuid: Guid.fromBytes( + characteristic.uuid.value, + ), + instanceId: characteristic.instanceId(service), + value: value, + success: true, + errorCode: 0, + errorString: '', + ), + ); + + return true; + } catch (e) { + _onCharacteristicReadController.add( + BmCharacteristicData( + remoteId: request.remoteId, + primaryServiceUuid: null, + serviceUuid: request.serviceUuid, + characteristicUuid: request.characteristicUuid, + instanceId: request.instanceId, + value: [], + success: false, + errorCode: 0, + errorString: e.toString(), + ), + ); + + return false; + } + } + + @override + Future readDescriptor( + BmReadDescriptorRequest request, + ) async { + try { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + final service = device.gattServices.singleWhere( + (service) { + final uuid = Guid.fromBytes( + service.uuid.value, + ); + + return uuid == request.serviceUuid; + }, + ); + + final characteristic = service.characteristics.singleWhere( + (characteristic) { + final uuid = Guid.fromBytes( + characteristic.uuid.value, + ); + + return uuid == request.characteristicUuid && (characteristic.instanceId(service) == request.instanceId); + }, + ); + + final descriptor = characteristic.descriptors.singleWhere( + (descriptor) { + final uuid = Guid.fromBytes( + descriptor.uuid.value, + ); + + return uuid == request.descriptorUuid; + }, + ); + + final value = await descriptor.readValue(); + + _onDescriptorReadController.add( + BmDescriptorData( + remoteId: device.remoteId, + primaryServiceUuid: null, + serviceUuid: Guid.fromBytes( + service.uuid.value, + ), + characteristicUuid: Guid.fromBytes( + characteristic.uuid.value, + ), + instanceId: characteristic.instanceId(service), + descriptorUuid: Guid.fromBytes( + descriptor.uuid.value, + ), + value: value, + success: true, + errorCode: 0, + errorString: '', + ), + ); + + return true; + } catch (e) { + _onDescriptorReadController.add( + BmDescriptorData( + remoteId: request.remoteId, + primaryServiceUuid: null, + serviceUuid: request.serviceUuid, + characteristicUuid: request.characteristicUuid, + instanceId: request.instanceId, + descriptorUuid: request.descriptorUuid, + value: [], + success: false, + errorCode: 0, + errorString: e.toString(), + ), + ); + + return false; + } + } + + @override + Future readRssi( + BmReadRssiRequest request, + ) async { + try { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + _onReadRssiController.add( + BmReadRssiResult( + remoteId: device.remoteId, + rssi: device.rssi, + success: true, + errorCode: 0, + errorString: '', + ), + ); + + return true; + } catch (e) { + _onReadRssiController.add( + BmReadRssiResult( + remoteId: request.remoteId, + rssi: 0, + success: false, + errorCode: 0, + errorString: e.toString(), + ), + ); + + return false; + } + } + + @override + Future setLogLevel( + BmSetLogLevelRequest request, + ) async { + await _initFlutterBluePlus(); + + _logLevel = request.logLevel; + + return true; + } + + @override + Future setNotifyValue( + BmSetNotifyValueRequest request, + ) async { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + final service = device.gattServices.singleWhere( + (service) { + final uuid = Guid.fromBytes( + service.uuid.value, + ); + + return uuid == request.serviceUuid; + }, + ); + + final characteristic = service.characteristics.singleWhere( + (characteristic) { + final uuid = Guid.fromBytes( + characteristic.uuid.value, + ); + + return uuid == request.characteristicUuid && (characteristic.instanceId(service) == request.instanceId); + }, + ); + + if (request.enable) { + await characteristic.startNotify(); + } else { + await characteristic.stopNotify(); + } + + // No CCCD write on Linux; no separate response event expected + return false; + } + + @override + Future startScan( + BmScanSettings request, + ) async { + await _initFlutterBluePlus(); + + final adapter = _client.adapters.firstOrNull; + + if (adapter == null) { + return false; + } + + await adapter.setDiscoveryFilter( + uuids: request.withServices.map( + (uuid) { + return uuid.str128; + }, + ).toList(), + ); + + await adapter.startDiscovery(); + + return true; + } + + @override + Future stopScan( + BmStopScanRequest request, + ) async { + await _initFlutterBluePlus(); + + final adapter = _client.adapters.firstOrNull; + + if (adapter == null) { + return false; + } + + await adapter.stopDiscovery(); + + return true; + } + + @override + Future turnOff( + BmTurnOffRequest request, + ) async { + await _initFlutterBluePlus(); + + final adapter = _client.adapters.firstOrNull; + + if (adapter == null || adapter.powered == false) { + return false; + } + + await adapter.setPowered(false); + + return true; + } + + @override + Future turnOn( + BmTurnOnRequest request, + ) async { + await _initFlutterBluePlus(); + + final adapter = _client.adapters.firstOrNull; + + if (adapter == null || adapter.powered == true) { + return false; + } + + await adapter.setPowered(true); + + _onTurnOnResponseController.add( + BmTurnOnResponse( + userAccepted: true, + ), + ); + + return true; + } + + @override + Future writeCharacteristic( + BmWriteCharacteristicRequest request, + ) async { + try { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + final service = device.gattServices.singleWhere( + (service) { + final uuid = Guid.fromBytes( + service.uuid.value, + ); + + return uuid == request.serviceUuid; + }, + ); + + final characteristic = service.characteristics.singleWhere( + (characteristic) { + final uuid = Guid.fromBytes( + characteristic.uuid.value, + ); + + return uuid == request.characteristicUuid && (characteristic.instanceId(service) == request.instanceId); + }, + ); + + await characteristic.writeValue( + request.value, + type: request.writeType == BmWriteType.withResponse + ? BlueZGattCharacteristicWriteType.request + : BlueZGattCharacteristicWriteType.command, + ); + + _onCharacteristicWrittenController.add( + BmCharacteristicData( + remoteId: device.remoteId, + primaryServiceUuid: null, + serviceUuid: Guid.fromBytes( + service.uuid.value, + ), + characteristicUuid: Guid.fromBytes( + characteristic.uuid.value, + ), + instanceId: characteristic.instanceId(service), + value: request.value, + success: true, + errorCode: 0, + errorString: '', + ), + ); + + return true; + } catch (e) { + _onCharacteristicWrittenController.add( + BmCharacteristicData( + remoteId: request.remoteId, + primaryServiceUuid: null, + serviceUuid: request.serviceUuid, + characteristicUuid: request.characteristicUuid, + instanceId: request.instanceId, + value: request.value, + success: false, + errorCode: 0, + errorString: e.toString(), + ), + ); + + return false; + } + } + + @override + Future writeDescriptor( + BmWriteDescriptorRequest request, + ) async { + try { + await _initFlutterBluePlus(); + + final device = _client.devices.singleWhere( + (device) { + return device.remoteId == request.remoteId; + }, + ); + + final service = device.gattServices.singleWhere( + (service) { + final uuid = Guid.fromBytes( + service.uuid.value, + ); + + return uuid == request.serviceUuid; + }, + ); + + final characteristic = service.characteristics.singleWhere( + (characteristic) { + final uuid = Guid.fromBytes( + characteristic.uuid.value, + ); + + return uuid == request.characteristicUuid && (characteristic.instanceId(service) == request.instanceId); + }, + ); + + final descriptor = characteristic.descriptors.singleWhere( + (descriptor) { + final uuid = Guid.fromBytes( + descriptor.uuid.value, + ); + + return uuid == request.descriptorUuid; + }, + ); + + await descriptor.writeValue(request.value); + + _onDescriptorWrittenController.add( + BmDescriptorData( + remoteId: device.remoteId, + primaryServiceUuid: null, + serviceUuid: Guid.fromBytes( + service.uuid.value, + ), + characteristicUuid: Guid.fromBytes( + characteristic.uuid.value, + ), + instanceId: characteristic.instanceId(service), + descriptorUuid: Guid.fromBytes( + descriptor.uuid.value, + ), + value: request.value, + success: true, + errorCode: 0, + errorString: '', + ), + ); + + return true; + } catch (e) { + _onDescriptorWrittenController.add( + BmDescriptorData( + remoteId: request.remoteId, + primaryServiceUuid: null, + serviceUuid: request.serviceUuid, + characteristicUuid: request.characteristicUuid, + instanceId: request.instanceId, + descriptorUuid: request.descriptorUuid, + value: request.value, + success: false, + errorCode: 0, + errorString: e.toString(), + ), + ); + + return false; + } + } + + static void registerWith() { + FlutterBluePlusPlatform.instance = FlutterBluePlusLinux(); + } + + Future _initFlutterBluePlus() async { + if (_initialized) { + return; + } + + _initialized = true; + + await _client.connect(); + + _client.devicesChanged.switchMap( + (devices) { + if (_logLevel == LogLevel.verbose) { + FlutterBluePlusPlatform.log( + '[FBP-Linux] devices changed ${devices.map((device) => device.remoteId).toList()}', + ); + } + + return MergeStream( + devices.map( + (device) { + return device.propertiesChanged.switchMap( + (properties) { + if (_logLevel == LogLevel.verbose) { + FlutterBluePlusPlatform.log( + '[FBP-Linux] device ${device.remoteId} properties changed $properties', + ); + } + + final streams = >[]; + + for (final service in device.gattServices) { + for (final characteristic in service.characteristics) { + streams.add( + characteristic.propertiesChanged.map( + (properties) { + if (_logLevel == LogLevel.verbose) { + FlutterBluePlusPlatform.log( + '[FBP-Linux] device ${device.remoteId} service ${service.uuid} characteristic ${characteristic.uuid} properties changed $properties', + ); + } + }, + ), + ); + } + } + + return MergeStream(streams); + }, + ); + }, + ), + ); + }, + ).listen(null); + } +} + +extension on BlueZClient { + Stream> get adaptersChanged { + return MergeStream([ + adapterAdded, + adapterRemoved, + ]).map( + (adapter) { + return adapters; + }, + ).startWith(adapters); + } + + Stream> get devicesChanged { + return MergeStream([ + deviceAdded, + deviceRemoved, + ]).map( + (device) { + return devices; + }, + ).startWith(devices); + } +} diff --git a/packages/flutter_blue_plus_linux/pubspec.yaml b/packages/flutter_blue_plus_linux/pubspec.yaml new file mode 100644 index 00000000..e78561c4 --- /dev/null +++ b/packages/flutter_blue_plus_linux/pubspec.yaml @@ -0,0 +1,27 @@ +name: flutter_blue_plus_linux +description: Linux implementation of the flutter_blue_plus plugin. +version: 8.2.2 +homepage: https://github.com/chipweinberger/flutter_blue_plus + +environment: + sdk: ^3.0.0 + flutter: '>=3.7.0' + +dependencies: + bluez: '>=0.7.8 <0.9.0' + flutter: + sdk: flutter + flutter_blue_plus_platform_interface: 8.2.2 + rxdart: '>=0.27.0 <0.29.0' + +dev_dependencies: + flutter_lints: ^5.0.0 + flutter_test: + sdk: flutter + +flutter: + plugin: + implements: flutter_blue_plus + platforms: + linux: + dartPluginClass: FlutterBluePlusLinux