integrate flutter blue plus windows

This commit is contained in:
Jonas Bark
2025-03-27 16:56:46 +01:00
parent c46253a77f
commit f0050167a8
40 changed files with 2050 additions and 2 deletions

View File

@@ -0,0 +1,29 @@
---
name: Bug Report
about: Create a report to help us improve
title: "fix: "
labels: bug
---
**Description**
A clear and concise description of what the bug is.
**Steps To Reproduce**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional Context**
Add any other context about the problem here.

View File

@@ -0,0 +1,14 @@
---
name: Build System
about: Changes that affect the build system or external dependencies
title: "build: "
labels: build
---
**Description**
Describe what changes need to be done to the build system and why.
**Requirements**
- [ ] The build system is passing

View File

@@ -0,0 +1,14 @@
---
name: Chore
about: Other changes that don't modify src or test files
title: "chore: "
labels: chore
---
**Description**
Clearly describe what change is needed and why. If this changes code then please use another issue type.
**Requirements**
- [ ] No functional changes to the code

View File

@@ -0,0 +1,14 @@
---
name: Continuous Integration
about: Changes to the CI configuration files and scripts
title: "ci: "
labels: ci
---
**Description**
Describe what changes need to be done to the ci/cd system and why.
**Requirements**
- [ ] The ci system is passing

View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,14 @@
---
name: Documentation
about: Improve the documentation so all collaborators have a common understanding
title: "docs: "
labels: documentation
---
**Description**
Clearly describe what documentation you are looking to add or improve.
**Requirements**
- [ ] Requirements go here

View File

@@ -0,0 +1,18 @@
---
name: Feature Request
about: A new feature to be added to the project
title: "feat: "
labels: feature
---
**Description**
Clearly describe what you are looking to add. The more context the better.
**Requirements**
- [ ] Checklist of requirements to be fulfilled
**Additional Context**
Add any other context or screenshots about the feature request go here.

View File

@@ -0,0 +1,14 @@
---
name: Performance Update
about: A code change that improves performance
title: "perf: "
labels: performance
---
**Description**
Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience.
**Requirements**
- [ ] There is no drop in test coverage.

View File

@@ -0,0 +1,14 @@
---
name: Refactor
about: A code change that neither fixes a bug nor adds a feature
title: "refactor: "
labels: refactor
---
**Description**
Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize.
**Requirements**
- [ ] There is no drop in test coverage.

View File

@@ -0,0 +1,16 @@
---
name: Revert Commit
about: Reverts a previous commit
title: "revert: "
labels: revert
---
**Description**
Provide a link to a PR/Commit that you are looking to revert and why.
**Requirements**
- [ ] Change has been reverted
- [ ] No change in test coverage has happened
- [ ] A new ticket is created for any follow on work that needs to happen

View File

@@ -0,0 +1,14 @@
---
name: Style Changes
about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
title: "style: "
labels: style
---
**Description**
Clearly describe what you are looking to change and why.
**Requirements**
- [ ] There is no drop in test coverage.

View File

@@ -0,0 +1,14 @@
---
name: Test
about: Adding missing tests or correcting existing tests
title: "test: "
labels: test
---
**Description**
List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past.
**Requirements**
- [ ] There is no drop in test coverage.

View File

@@ -0,0 +1,27 @@
<!--
Thanks for contributing!
Provide a description of your changes below and a general summary in the title
Please look at the following checklist to ensure that your PR can be accepted quickly:
-->
## Status
**READY/IN DEVELOPMENT/HOLD**
## Description
<!--- Describe your changes in detail -->
## Type of Change
<!--- Put an `x` in all the boxes that apply: -->
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

View File

@@ -0,0 +1,21 @@
{
"version": "0.2",
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"dictionaries": ["vgv_allowed", "vgv_forbidden"],
"dictionaryDefinitions": [
{
"name": "vgv_allowed",
"path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt",
"description": "Allowed VGV Spellings"
},
{
"name": "vgv_forbidden",
"path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt",
"description": "Forbidden VGV Spellings"
}
],
"useGitignore": true,
"words": [
"flutter_blue_plus_windows"
]
}

View File

@@ -0,0 +1,11 @@
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "daily"

12
flutter_blue_plus_windows/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock
/.idea/
/.flutter-plugins
/.flutter-plugins-dependencies
interanl_example

View File

@@ -0,0 +1,106 @@
## 1.26.1
* Add setOptions.
## 1.26.0
* Set `flutter_blue_plus` upperbound <1.35.0 (due to api changes)
## 1.25.0
* Ensure tracing connection when reconnection occurs after force disconnection.
## 1.24.22
* Upgrade FBP version to `>=1.32.4 <=1.40.0` #24.
## 1.24.21
* fix: startScan() doesn't return correct ScanResult #25
## 1.24.20
* Downgrade FBP version to `>=1.32.4 <=1.33.6` due to the breaking changes.
* After upgrade process, the dependencies will be returned to `>=1.34.4 <1.40.0` #24.
## 1.24.19
* Fix a bug with `onValueReceived` of emitting write packet #22.
## 1.24.18
* Add implementation of `BluetoothDeviceWindow.fromId()` #21.
## 1.24.15
* Fix a bug w.r.t. company ID in manufacturer data. (@betto-a #18)
## 1.24.14
* Implement cancelOnDisconnect (@jefflongo #16)
## 1.24.12
* Fix minor bug w.r.t. `characteristic.isNotifying`.
## 1.24.11
* Fix breaking changes of FBP w.r.t. `systemDevices(List withServices)`.
## 1.24.10
* Add support for `cancelWhenScanComplete`
## 1.24.9
* Implement scan filter (including `withServices`, `withRemoteIds`, `withNames`).
## 1.24.8
* Keep manufacturer data when scanning.
## 1.24.7
* Keep service uuids when scanning.
## 1.24.0
* Update `README.md`.
## 1.23.6
* Add unimplemented notification for `read` or `write`.
## 1.14.0
* Remove dependencies `ffi` and `win32` to avoid compile error for web
## 1.9.5
* Apply `flutter blue plus` to `1.28.13`.
## 1.9.0
* Apply a breaking changes `Guid` in `Flutter blue plus` packages.
* Use `uuid128` instead of `toString()`.
## 1.8.10
* Fix `Guid` bug related with `Flutter blue plus` packages.
## 1.8.0
* Fix bug with Guid converted from string due to starting/ending with '{ }' in `WinBLE`
## 1.7.0
* Apply `flutter blue plus 1.28.5` (there is several breaking changes.).
## 1.6.6
* Add cache for storing characteristics.
## 1.6.0
* Apply `Flutter blue plus 1.26.0`, (there is a breaking change with `connect()`).
## 1.5.7
* Remove connection by OS when performing `startScan`.
## 1.5.3
* Write logs when connection state stream is started/terminated.
## 1.5.2
* Fix a bug of features added in `1.5.1`
## 1.5.1
* Remove device from connected device list when device is disconnected.
## 1.5.0
* Split functionality of `disconnect` / `removeBond`.
## 1.4.0
* Implement `Subscribe/Unsubscribe Characteristic`.
## 1.1.0
* Implement `Read/Write Characteristic`.
## 1.0.5
* Change `rxdart` version to `0.27.7`.
## 1.0.0
* Initial release (using Github action).

View File

@@ -0,0 +1,7 @@
Copyright 2023 Himchan Park
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,54 @@
[![pub package](https://img.shields.io/pub/v/flutter_blue_plus_windows.svg)](https://pub.dartlang.org/packages/flutter_blue_plus_windows)
## Flutter Blue Plus Windows
This project is a wrapper library for `Flutter Blue Plus` and `Win_ble`.
It allows `Flutter_blue_plus` to operate on Windows.
With minimal effort, you can use Flutter Blue Plus on Windows.
## Usage
Only you need to do is change the import statement.
```dart
// instead of import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
// Alternatively, you can hide FlutterBluePlus when importing the FBP statement
import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide FlutterBluePlus;
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
```
### Scan devices
```dart
final scannedDevices = <ScanResult>{};
const timeout = Duration(seconds: 3);
FlutterBluePlus.startScan(timeout: timeout);
final sub = FlutterBluePlus.scanResults.expand((e)=>e).listen(scannedDevices.add);
await Future.delayed(timeout);
sub.cancel();
scannedDevices.forEach(print);
```
### Connect a device
```dart
final scannedDevice = scannedDevices
.where((scanResult) => scanResult.device.platformName == DEVICE_NAME)
.firstOrNull;
final device = scannedDevice?.device;
device?.connect();
```
### Disconnect the device
```dart
device?.disconnect();
```
Check out the usage of Flutter Blue Plus on [Flutter Blue Plus](https://pub.dev/packages/flutter_blue_plus)

View File

@@ -0,0 +1,7 @@
library flutter_blue_plus_windows;
export 'package:flutter_blue_plus/flutter_blue_plus.dart' hide FlutterBluePlus;
export 'package:win_ble/win_ble.dart';
export 'package:win_ble/win_file.dart';
export 'src/flutter_blue_plus_windowss.dart';

View File

@@ -0,0 +1,19 @@
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:win_ble/win_ble.dart';
extension BluetoothAdapterStateExtension on BleState {
BluetoothAdapterState toAdapterState() {
switch(this){
case BleState.On:
return BluetoothAdapterState.on;
case BleState.Off:
return BluetoothAdapterState.off;
case BleState.Unknown:
return BluetoothAdapterState.unknown;
case BleState.Disabled:
return BluetoothAdapterState.unavailable;
case BleState.Unsupported:
return BluetoothAdapterState.unauthorized;
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
extension BluetoothCharacteristicExtension on BluetoothCharacteristic {
BmBluetoothCharacteristic toProto() {
return BmBluetoothCharacteristic(
remoteId: DeviceIdentifier(remoteId.str),
serviceUuid: serviceUuid,
characteristicUuid: characteristicUuid,
descriptors: [for (final d in descriptors) d.toProto()],
properties: properties.toProto(),
primaryServiceUuid: null, // TODO: API changes
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
extension BluetoothDescriptorExtension on BluetoothDescriptor {
BmBluetoothDescriptor toProto() {
return BmBluetoothDescriptor(
remoteId: DeviceIdentifier(remoteId.str),
serviceUuid: serviceUuid,
characteristicUuid: characteristicUuid,
descriptorUuid: descriptorUuid,
primaryServiceUuid: null, // TODO: API changes
);
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
extension BluetoothServiceExtension on BluetoothService {
BmBluetoothService toProto() {
return BmBluetoothService(
serviceUuid: serviceUuid,
remoteId: DeviceIdentifier(remoteId.str),
characteristics: [for (final c in characteristics) c.toProto()],
primaryServiceUuid: null, // TODO: API changes
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
extension CharacteristicPropertiesExtension on CharacteristicProperties {
BmCharacteristicProperties toProto() {
return BmCharacteristicProperties(
broadcast: broadcast,
read: read,
writeWithoutResponse: writeWithoutResponse,
write: write,
notify: notify,
indicate: indicate,
authenticatedSignedWrites: authenticatedSignedWrites,
extendedProperties: extendedProperties,
notifyEncryptionRequired: notifyEncryptionRequired,
indicateEncryptionRequired: indicateEncryptionRequired,
);
}
}

View File

@@ -0,0 +1,5 @@
export 'bluetooth_adapter_state_extension.dart';
export 'bluetooth_characteristic_extension.dart';
export 'bluetooth_descriptor_extension.dart';
export 'bluetooth_service_extension.dart';
export 'characteristic_properties_extension.dart';

View File

@@ -0,0 +1,3 @@
export 'extension/extension.dart';
export 'windows/windows.dart';
export 'wrapper/wrapper.dart';

View File

@@ -0,0 +1,160 @@
part of 'windows.dart';
class BluetoothCharacteristicWindows extends BluetoothCharacteristic {
final DeviceIdentifier remoteId;
final Guid serviceUuid;
final Guid? secondaryServiceUuid;
final Guid characteristicUuid;
final List<BluetoothDescriptor> descriptors;
final Properties propertiesWinBle;
BluetoothCharacteristicWindows({
required this.remoteId,
required this.serviceUuid,
required this.characteristicUuid,
required this.descriptors,
required this.propertiesWinBle,
this.secondaryServiceUuid,
}) : super.fromProto(
BmBluetoothCharacteristic(
remoteId: DeviceIdentifier(remoteId.str),
serviceUuid: serviceUuid,
characteristicUuid: characteristicUuid,
descriptors: [
for (final descriptor in descriptors)
BmBluetoothDescriptor(
remoteId: DeviceIdentifier(descriptor.remoteId.str),
serviceUuid: descriptor.serviceUuid,
characteristicUuid: descriptor.characteristicUuid,
descriptorUuid: descriptor.uuid,
primaryServiceUuid: null, // TODO: API changes
),
],
properties: BmCharacteristicProperties(
broadcast: propertiesWinBle.broadcast ?? false,
read: propertiesWinBle.read ?? false,
writeWithoutResponse: propertiesWinBle.writeWithoutResponse ?? false,
write: propertiesWinBle.write ?? false,
notify: propertiesWinBle.notify ?? false,
indicate: propertiesWinBle.indicate ?? false,
authenticatedSignedWrites: propertiesWinBle.authenticatedSignedWrites ?? false,
// TODO: implementation missing
extendedProperties: false,
// TODO: implementation missing
notifyEncryptionRequired: false,
// TODO: implementation missing
indicateEncryptionRequired: false,
),
primaryServiceUuid: null, // TODO: API changes
),
);
String get _address => remoteId.str.toLowerCase();
String get _key => "$serviceUuid:$characteristicUuid";
FBP.BluetoothDevice get device =>
FlutterBluePlusWindows.connectedDevices.firstWhere((device) => device.remoteId == remoteId);
/// this variable is updated:
/// - anytime `read()` is called
/// - anytime `write()` is called
/// - anytime a notification arrives (if subscribed)
List<int> get lastValue => FlutterBluePlusWindows._lastChrs[remoteId]?[_key] ?? [];
/// this stream emits values:
/// - anytime `read()` is called
/// - anytime `write()` is called
/// - anytime a notification arrives (if subscribed)
/// - and when first listened to, it re-emits the last value for convenience
Stream<List<int>> get lastValueStream => _mergeStreams(
[
WinBle.characteristicValueStreamOf(
address: _address,
serviceId: serviceUuid.str128,
characteristicId: characteristicUuid.str128,
),
FlutterBluePlusWindows._charReadWriteStream.where((e) => e.$1 == _key).map((e) => e.$2)
],
).map((p) => <int>[...p]).newStreamWithInitialValue(lastValue).asBroadcastStream();
/// this stream emits values:
/// - anytime `read()` is called
/// - anytime a notification arrives (if subscribed)
Stream<List<int>> get onValueReceived => _mergeStreams(
[
WinBle.characteristicValueStreamOf(
address: _address,
serviceId: serviceUuid.str128,
characteristicId: characteristicUuid.str128,
),
FlutterBluePlusWindows._charReadStream.where((e) => e.$1 == _key).map((e) => e.$2)
],
).map((p) => <int>[...p]).asBroadcastStream();
// TODO: need to verify
bool get isNotifying => FlutterBluePlusWindows._isNotifying[remoteId]?[_key] ?? false;
Future<List<int>> read({int timeout = 15}) async {
final value = await WinBle.read(
address: _address,
serviceId: serviceUuid.str128,
characteristicId: characteristicUuid.str128,
);
FlutterBluePlusWindows._charReadWriteStreamController.add((_key, value));
FlutterBluePlusWindows._charReadStreamController.add((_key, value));
FlutterBluePlusWindows._lastChrs[remoteId] ??= {};
FlutterBluePlusWindows._lastChrs[remoteId]?[_key] = value;
return value;
}
Future<void> write(List<int> value,
{bool allowLongWrite = false, bool withoutResponse = false, int timeout = 15}) async {
await WinBle.write(
address: _address,
service: serviceUuid.str128,
characteristic: characteristicUuid.str128,
data: Uint8List.fromList(value),
writeWithResponse: !withoutResponse, // propertiesWinBle.writeWithoutResponse ?? false,
);
FlutterBluePlusWindows._charReadWriteStreamController.add((_key, value));
FlutterBluePlusWindows._lastChrs[remoteId] ??= {};
FlutterBluePlusWindows._lastChrs[remoteId]?[_key] = value;
}
// TODO: need to verify
Future<bool> setNotifyValue(
bool notify, {
int timeout = 15, // TODO: missing implementation
bool forceIndications = false, // TODO: missing implementation
}) async {
/// unSubscribeFromCharacteristic
try {
await WinBle.unSubscribeFromCharacteristic(
address: _address,
serviceId: serviceUuid.str128,
characteristicId: characteristicUuid.str128,
);
} catch (e) {
log('WinBle.unSubscribeFromCharacteristic was performed '
'before setNotifyValue()');
}
/// set notify
try {
if (notify) {
await WinBle.subscribeToCharacteristic(
address: _address,
serviceId: serviceUuid.str128,
characteristicId: characteristicUuid.str128,
);
}
FlutterBluePlusWindows._isNotifying[remoteId] ??= {};
FlutterBluePlusWindows._isNotifying[remoteId]?[_key] = notify;
} catch (e) {
log(e.toString());
}
return true;
}
}

View File

@@ -0,0 +1,310 @@
// Bluetooth Device Page:
// https://github.com/boskokg/flutter_blue_plus/blob/master/lib/src/bluetooth_device.dart
part of 'windows.dart';
class BluetoothDeviceWindows extends FBP.BluetoothDevice {
BluetoothDeviceWindows({required super.remoteId});
// used for 'servicesStream' public api
final _services = StreamController<List<BluetoothServiceWindows>>.broadcast();
// used for 'isDiscoveringServices' public api
final _isDiscoveringServices = _StreamController(initialValue: false);
String get _address => remoteId.str.toLowerCase();
/// Create a device from an id
/// - to connect, this device must have been discovered by your app in a previous scan
/// - iOS uses 128-bit uuids the remoteId, e.g. e006b3a7-ef7b-4980-a668-1f8005f84383
/// - Android uses 48-bit mac addresses as the remoteId, e.g. 06:E5:28:3B:FD:E0
static FBP.BluetoothDevice fromId(String remoteId) {
if (Platform.isWindows) {
return BluetoothDeviceWindows(remoteId: DeviceIdentifier(remoteId.toUpperCase()));
}
return FBP.BluetoothDevice.fromId(remoteId);
}
/// platform name
/// - this name is kept track of by the platform
/// - this name usually persist between app restarts
/// - iOS: after you connect, iOS uses the GAP name characteristic (0x2A00)
/// if it exists. Otherwise iOS use the advertised name.
/// - Android: always uses the advertised name
String get platformName => FlutterBluePlusWindows._platformNames[remoteId] ?? "";
/// Advertised Named
/// - this is the name advertised by the device during scanning
/// - it is only available after you scan with FlutterBluePlus
/// - it is cleared when the app restarts.
/// - not all devices advertise a name
String get advName => FlutterBluePlusWindows._advNames[remoteId] ?? "";
// stream return whether or not we are currently discovering services
@Deprecated("planed for removal (Jan 2024). It can be easily implemented yourself") // deprecated on Aug 2023
Stream<bool> get isDiscoveringServices => _isDiscoveringServices.stream;
/// Get services
/// - returns empty if discoverServices() has not been called
/// or if your device does not have any services (rare)
List<BluetoothServiceWindows> get servicesList => FlutterBluePlusWindows._knownServices[remoteId] ?? [];
/// Stream of bluetooth services offered by the remote device
/// - this stream is only updated when you call discoverServices()
@Deprecated("planed for removal (Jan 2024). It can be easily implemented yourself") // deprecated on Aug 2023
Stream<List<BluetoothService>> get servicesStream {
if (FlutterBluePlusWindows._knownServices[remoteId] != null) {
return _services.stream.newStreamWithInitialValue(
FlutterBluePlusWindows._knownServices[remoteId]!,
);
} else {
return _services.stream;
}
}
/// Register a subscription to be canceled when the device is disconnected.
/// This function simplifies cleanup, so you can prevent creating duplicate stream subscriptions.
/// - this is an optional convenience function
/// - prevents accidentally creating duplicate subscriptions on each reconnection.
/// - [next] if true, the the stream will be canceled only on the *next* disconnection.
/// This is useful if you setup your subscriptions before you connect.
/// - [delayed] Note: This option is only meant for `connectionState` subscriptions.
/// When `true`, we cancel after a small delay. This ensures the `connectionState`
/// listener receives the `disconnected` event.
void cancelWhenDisconnected(StreamSubscription subscription, {bool next = false, bool delayed = false}) {
if (isConnected == false && next == false) {
subscription.cancel(); // cancel immediately if already disconnected.
} else if (delayed) {
FlutterBluePlusWindows._delayedSubscriptions[remoteId] ??= [];
FlutterBluePlusWindows._delayedSubscriptions[remoteId]!.add(subscription);
} else {
FlutterBluePlusWindows._deviceSubscriptions[remoteId] ??= [];
FlutterBluePlusWindows._deviceSubscriptions[remoteId]!.add(subscription);
}
}
/// Returns true if this device is currently connected to your app
bool get isConnected {
return FlutterBluePlusWindows.connectedDevices.contains(this);
}
/// Returns true if this device is currently disconnected from your app
bool get isDisconnected => isConnected == false;
Future<void> connect({
Duration? timeout = const Duration(seconds: 35), // TODO: implementation missing
bool autoConnect = false, // TODO: implementation missing
int? mtu = 512, // TODO: implementation missing
}) async {
try {
await WinBle.connect(_address);
FlutterBluePlusWindows._deviceSet.add(this);
} catch (e) {
log(e.toString());
}
}
Future<void> disconnect({
int androidDelay = 2000, // TODO: implementation missing
int timeout = 35, // TODO: implementation missing
bool queue = true, // TODO: implementation missing
}) async {
try {
await WinBle.disconnect(_address);
} catch (e) {
log(e.toString());
} finally {
FlutterBluePlusWindows._deviceSet.remove(this);
FlutterBluePlusWindows._deviceSubscriptions[remoteId]?.forEach((s) => s.cancel());
FlutterBluePlusWindows._deviceSubscriptions.remove(remoteId);
// use delayed to update the stream before we cancel it
Future.delayed(Duration.zero).then((_) {
FlutterBluePlusWindows._delayedSubscriptions[remoteId]?.forEach((s) => s.cancel());
FlutterBluePlusWindows._delayedSubscriptions.remove(remoteId);
});
FlutterBluePlusWindows._lastChrs[remoteId]?.clear();
FlutterBluePlusWindows._isNotifying[remoteId]?.clear();
}
}
Future<List<BluetoothService>> discoverServices({
bool subscribeToServicesChanged = true, // TODO: implementation missing
int timeout = 15, // TODO: implementation missing
}) async {
List<BluetoothServiceWindows> result = List.from(FlutterBluePlusWindows._knownServices[remoteId] ?? []);
try {
_isDiscoveringServices.add(true);
final response = await WinBle.discoverServices(_address);
FlutterBluePlusWindows._characteristicCache[remoteId] ??= <String, List<BluetoothCharacteristic>>{};
for (final serviceId in response) {
final characteristic = await WinBle.discoverCharacteristics(
address: _address,
serviceId: serviceId,
);
FlutterBluePlusWindows._characteristicCache[remoteId] ??= {};
FlutterBluePlusWindows._characteristicCache[remoteId]?[serviceId] ??= [
...characteristic.map(
(e) => BluetoothCharacteristicWindows(
remoteId: remoteId,
serviceUuid: Guid(serviceId),
characteristicUuid: Guid(e.uuid),
descriptors: [],
// TODO: implementation missing
propertiesWinBle: e.properties,
),
),
];
}
result = [
...response.map(
(p) => BluetoothServiceWindows(
remoteId: remoteId,
serviceUuid: Guid(p),
// TODO: implementation missing
isPrimary: true,
// TODO: implementation missing
characteristics: FlutterBluePlusWindows._characteristicCache[remoteId]![p]!,
// TODO: implementation missing
includedServices: [],
),
)
];
FlutterBluePlusWindows._knownServices[remoteId] = result;
_services.add(result);
} finally {
_isDiscoveringServices.add(false);
}
return result;
}
DisconnectReason? get disconnectReason {
// TODO: nothing to do
return null;
}
Stream<BluetoothConnectionState> get connectionState async* {
await FlutterBluePlusWindows._initialize();
final map = FlutterBluePlusWindows._connectionStream.latestValue;
if (map[_address] != null) {
yield map[_address]!.isConnected;
}
yield* WinBle.connectionStreamOf(_address).map((e) => e.isConnected);
}
Stream<int> get mtu async* {
bool isEmitted = false;
int retryCount = 0;
while (!isEmitted) {
if (retryCount > 3) throw "Device not found!";
retryCount++;
try {
yield await WinBle.getMaxMtuSize(_address);
isEmitted = true;
} catch (e) {
await Future.delayed(const Duration(milliseconds: 500));
log(e.toString());
}
}
}
Future<int> readRssi({int timeout = 15}) async {
return FlutterBluePlusWindows._rssiMap[remoteId] ?? -100;
}
Future<int> requestMtu(
int desiredMtu, {
double predelay = 0.35,
int timeout = 15,
}) async {
// https://github.com/rohitsangwan01/win_ble/issues/8
return await WinBle.getMaxMtuSize(_address);
}
Future<void> requestConnectionPriority({
required ConnectionPriority connectionPriorityRequest,
}) async {
// TODO: nothing to do
return;
}
/// Set the preferred connection (Android Only)
/// - [txPhy] bitwise OR of all allowed phys for Tx, e.g. (Phy.le2m.mask | Phy.leCoded.mask)
/// - [txPhy] bitwise OR of all allowed phys for Rx, e.g. (Phy.le2m.mask | Phy.leCoded.mask)
/// - [option] preferred coding to use when transmitting on Phy.leCoded
/// Please note that this is just a recommendation given to the system.
Future<void> setPreferredPhy({
required int txPhy,
required int rxPhy,
required PhyCoding option,
}) async {
// TODO: implementation missing
}
Future<void> createBond({
Uint8List? pin,
int timeout = 90, // TODO: implementation missing
}) async {
try {
await WinBle.pair(_address);
} catch (e) {
log(e.toString());
}
}
Future<void> removeBond({
int timeout = 30, // TODO: implementation missing
}) async {
try {
await WinBle.unPair(_address);
} catch (e) {
log(e.toString());
}
}
Future<void> clearGattCache() async {
// TODO: implementation missing
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is BluetoothDeviceWindows && runtimeType == other.runtimeType && remoteId == other.remoteId);
@override
int get hashCode => remoteId.hashCode;
@override
String toString() {
return 'BluetoothDevice{'
'remoteId: $remoteId, '
'platformName: $platformName, '
'services: ${FlutterBluePlusWindows._knownServices[remoteId]}'
'}';
}
@Deprecated('Use createBond() instead')
Future<void> pair() async => await createBond();
@Deprecated('Use remoteId instead')
DeviceIdentifier get id => remoteId;
@Deprecated('Use localName instead')
String get name => localName;
@Deprecated('Use connectionState instead')
Stream<BluetoothConnectionState> get state => connectionState;
@Deprecated('Use servicesStream instead')
Stream<List<BluetoothService>> get services => servicesStream;
}

View File

@@ -0,0 +1,24 @@
part of 'windows.dart';
class BluetoothServiceWindows extends BluetoothService {
final DeviceIdentifier remoteId;
final Guid serviceUuid;
final bool isPrimary;
final List<BluetoothCharacteristic> characteristics;
final List<BluetoothService> includedServices;
BluetoothServiceWindows({
required this.remoteId,
required this.serviceUuid,
required this.isPrimary,
required this.characteristics,
required this.includedServices,
}) : super.fromProto(
BmBluetoothService(
remoteId: DeviceIdentifier(remoteId.str),
serviceUuid: serviceUuid,
characteristics: [for (final c in characteristics) c.toProto()],
primaryServiceUuid: null,
),
);
}

View File

@@ -0,0 +1,353 @@
part of 'windows.dart';
class FlutterBluePlusWindows {
static bool _initialized = false;
static BluetoothAdapterState _state = BluetoothAdapterState.unknown;
// stream used for the isScanning public api
static final _isScanning = _StreamController(initialValue: false);
// we always keep track of these device variables
static final _platformNames = <DeviceIdentifier, String>{};
static final _advNames = <DeviceIdentifier, String>{};
static final _rssiMap = <DeviceIdentifier, int?>{};
static final _knownServices = <DeviceIdentifier, List<BluetoothServiceWindows>>{};
static final Map<DeviceIdentifier, Map<String, List<int>>> _lastChrs = {};
static final Map<DeviceIdentifier, Map<String, bool>> _isNotifying = {};
static final Map<DeviceIdentifier, Map<String, List<BluetoothCharacteristic>>> _characteristicCache = {};
static final Map<DeviceIdentifier, List<StreamSubscription>> _deviceSubscriptions = {};
static final Map<DeviceIdentifier, List<StreamSubscription>> _delayedSubscriptions = {};
static final List<StreamSubscription> _scanSubscriptions = [];
// stream used for the scanResults public api
static final _scanResultsList = _StreamController(initialValue: <ScanResult>[]);
// the subscription to the scan results stream
static StreamSubscription<BleDevice?>? _scanSubscription;
// timeout for scanning that can be cancelled by stopScan
static Timer? _scanTimeout;
static List<BluetoothDeviceWindows> get _devices => [..._deviceSet];
static final _deviceSet = <BluetoothDeviceWindows>{};
static final _removedDeviceTracer = <BluetoothDeviceWindows, StreamSubscription>{};
// static final _unhandledDeviceSet = <BluetoothDeviceWindows>{};
/// Flutter blue plus windows
static final _charReadWriteStreamController = StreamController<(String, List<int>)>();
static final _charReadStreamController = StreamController<(String, List<int>)>();
static final _charReadWriteStream = _charReadWriteStreamController.stream.asBroadcastStream();
static final _charReadStream = _charReadStreamController.stream.asBroadcastStream();
/// Flutter blue plus windows
static final _connectionStream = _StreamController(initialValue: <String, bool>{});
static Future<void> _initialize() async {
if (_initialized) return;
await WinBle.initialize(
serverPath: await WinServer.path(),
enableLog: false,
);
WinBle.connectionStream.listen(
(event) {
log('$event - event');
if (event['device'] == null) return;
if (event['connected'] == null) return;
final map = _connectionStream.latestValue;
map[event['device']] = event['connected'];
log('$map - map');
_connectionStream.add(map);
if (!event['connected']) {
final removingDevices = [
..._deviceSet.where(
(device) => device._address == event['device'],
),
];
for (final device in removingDevices) {
_deviceSet.remove(device);
if (!_removedDeviceTracer.keys.contains(device)) {
_removedDeviceTracer[device] = Stream.periodic(const Duration(seconds: 10), (_) => device).listen(
(event) {
if(event.isConnected) {
_removedDeviceTracer[device]?.cancel();
_removedDeviceTracer.remove(device);
return;
}
event.connect();
},
);
}
_deviceSubscriptions[device.remoteId]?.forEach((s) => s.cancel());
_deviceSubscriptions.remove(device.remoteId);
// use delayed to update the stream before we cancel it
Future.delayed(Duration.zero).then((_) {
_delayedSubscriptions[device.remoteId]?.forEach((s) => s.cancel());
_delayedSubscriptions.remove(device.remoteId);
});
_lastChrs[device.remoteId]?.clear();
_isNotifying[device.remoteId]?.clear();
}
}
},
);
_initialized = true;
}
static Future<bool> get isSupported async {
return true;
}
static Future<String> get adapterName async {
return 'Windows';
}
static Stream<bool> get isScanning => _isScanning.stream;
static bool get isScanningNow => _isScanning.latestValue;
static Future<void> turnOn({int timeout = 10}) async {
await _initialize();
await WinBle.updateBluetoothState(true);
}
// TODO: compare with original lib
static Stream<List<ScanResult>> get scanResults => _scanResultsList.stream;
static Stream<BluetoothAdapterState> get adapterState async* {
await _initialize();
yield _state;
yield* WinBle.bleState.asBroadcastStream().map(
(s) {
_state = s.toAdapterState();
return _state;
},
);
}
/// Start a scan, and return a stream of results
/// Note: scan filters use an "or" behavior. i.e. if you set `withServices` & `withNames` we
/// return all the advertisments that match any of the specified services *or* any of the specified names.
/// - [withServices] filter by advertised services
/// - [withRemoteIds] filter for known remoteIds (iOS: 128-bit guid, android: 48-bit mac address)
/// - [withNames] filter by advertised names (exact match)
/// - [withKeywords] filter by advertised names (matches any substring)
/// - [withMsd] filter by manfacture specific data
/// - [withServiceData] filter by service data
/// - [timeout] calls stopScan after a specified duration
/// - [removeIfGone] if true, remove devices after they've stopped advertising for X duration
/// - [continuousUpdates] If `true`, we continually update 'lastSeen' & 'rssi' by processing
/// duplicate advertisements. This takes more power. You typically should not use this option.
/// - [continuousDivisor] Useful to help performance. If divisor is 3, then two-thirds of advertisements are
/// ignored, and one-third are processed. This reduces main-thread usage caused by the platform channel.
/// The scan counting is per-device so you always get the 1st advertisement from each device.
/// If divisor is 1, all advertisements are returned. This argument only matters for `continuousUpdates` mode.
/// - [oneByOne] if `true`, we will stream every advertistment one by one, possibly including duplicates.
/// If `false`, we deduplicate the advertisements, and return a list of devices.
/// - [androidLegacy] Android only. If `true`, scan on 1M phy only.
/// If `false`, scan on all supported phys. How the radio cycles through all the supported phys is purely
/// dependent on the your Bluetooth stack implementation.
/// - [androidScanMode] choose the android scan mode to use when scanning
/// - [androidUsesFineLocation] request `ACCESS_FINE_LOCATION` permission at runtime
static Future<void> startScan({
List<Guid> withServices = const [],
List<String> withRemoteIds = const [],
List<String> withNames = const [],
//TODO: implementation missing
List<String> withKeywords = const [],
//TODO: implementation missing
List<MsdFilter> withMsd = const [],
List<ServiceDataFilter> withServiceData = const [],
Duration? timeout,
Duration? removeIfGone,
bool continuousUpdates = false,
int continuousDivisor = 1,
bool oneByOne = false,
bool androidLegacy = false,
AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
bool androidUsesFineLocation = false,
}) async {
await _initialize();
// stop existing scan
if (_isScanning.latestValue == true) {
await stopScan();
}
// push to stream
_isScanning.add(true);
// Start timer *after* stream is being listened to, to make sure the
// timeout does not fire before _buffer is set
if (timeout != null) {
_scanTimeout = Timer(timeout, stopScan);
}
/// remove connection by OS.
/// The reason why we add this logic is
/// to avoid uncontrollable devices and to make consistency.
/// add WinBle scanning
WinBle.startScanning();
// check every 250ms for gone devices?
late Stream<BleDevice?> outputStream;
if (removeIfGone != null) {
outputStream = _mergeStreams([WinBle.scanStream, Stream.periodic(Duration(milliseconds: 250))]);
} else {
outputStream = WinBle.scanStream;
}
final output = <ScanResult>[];
// listen & push to `scanResults` stream
_scanSubscription = outputStream.listen(
(BleDevice? winBleDevice) {
// print(winBleDevice?.serviceUuids);
if (winBleDevice == null) {
// if null, this is just a periodic update for removing old results
output.removeWhere((elm) => DateTime.now().difference(elm.timeStamp) > removeIfGone!);
// push to stream
_scanResultsList.add(List.from(output));
} else {
final remoteId = DeviceIdentifier(winBleDevice.address.toUpperCase());
final scanResult = output.where((sr) => sr.device.remoteId == remoteId).firstOrNull;
final deviceName = winBleDevice.name.isNotEmpty ? winBleDevice.name : scanResult?.device.platformName ?? '';
final serviceUuids = winBleDevice.serviceUuids.isNotEmpty
? [...winBleDevice.serviceUuids.map((e) => Guid((e as String).replaceAll(RegExp(r'[{}]'), '')))]
: scanResult?.advertisementData.serviceUuids ?? [];
final manufacturerData = winBleDevice.manufacturerData.isNotEmpty
? {
if (winBleDevice.manufacturerData.length >= 2)
winBleDevice.manufacturerData[0] + (winBleDevice.manufacturerData[1] << 8):
winBleDevice.manufacturerData.sublist(2),
}
: scanResult?.advertisementData.manufacturerData ?? {};
final rssi = int.tryParse(winBleDevice.rssi) ?? -100;
FlutterBluePlusWindows._platformNames[remoteId] = deviceName;
FlutterBluePlusWindows._advNames[remoteId] = deviceName;
FlutterBluePlusWindows._rssiMap[remoteId] = rssi;
final device = BluetoothDeviceWindows(remoteId: remoteId);
String hex(int value) => value.toRadixString(16).padLeft(2, '0');
String hexToId(Iterable<int> values) => values.map((e) => hex(e)).join();
final sr = ScanResult(
device: device,
advertisementData: AdvertisementData(
advName: deviceName,
txPowerLevel: winBleDevice.adStructures?.where((e) => e.type == 10).singleOrNull?.data.firstOrNull,
//TODO: Should verify
connectable: !winBleDevice.advType.contains('Non'),
manufacturerData: manufacturerData,
serviceData: {
for (final advStructures in winBleDevice.adStructures ?? <AdStructure>[])
if (advStructures.type == 0x16 && advStructures.data.length >= 2)
Guid(hexToId(advStructures.data.sublist(0, 2).reversed)): advStructures.data.sublist(2),
for (final advStructures in winBleDevice.adStructures ?? <AdStructure>[])
if (advStructures.type == 0x20 && advStructures.data.length >= 4)
Guid(hexToId(advStructures.data.sublist(0, 4).reversed)): advStructures.data.sublist(4),
for (final advStructures in winBleDevice.adStructures ?? <AdStructure>[])
if (advStructures.type == 0x21 && advStructures.data.length >= 16)
Guid(hexToId(advStructures.data.sublist(0, 16).reversed)): advStructures.data.sublist(16),
},
serviceUuids: serviceUuids,
appearance: null,
),
rssi: rssi,
timeStamp: DateTime.now(),
);
// filter with services
final isFilteredWithServices =
withServices.isNotEmpty && serviceUuids.where((service) => withServices.contains(service)).isEmpty;
// filter with remote ids
final isFilteredWithRemoteIds = withRemoteIds.isNotEmpty && !withRemoteIds.contains(remoteId);
// filter with names
final isFilteredWithNames = withNames.isNotEmpty && !withNames.contains(deviceName);
if (isFilteredWithServices || isFilteredWithRemoteIds || isFilteredWithNames) {
_scanResultsList.add(List.from(output));
return;
}
// add result to output
if (oneByOne) {
output
..clear()
..add(sr);
} else {
output.addOrUpdate(sr);
}
// push to stream
_scanResultsList.add(List.from(output));
}
},
);
}
static List<FBP.BluetoothDevice> get connectedDevices {
return _devices;
}
static Future<List<BluetoothDeviceWindows>> get bondedDevices async {
return _devices;
}
/// Stops a scan for Bluetooth Low Energy devices
static Future<void> stopScan() async {
await _initialize();
WinBle.stopScanning();
_scanSubscription?.cancel();
_scanTimeout?.cancel();
_isScanning.add(false);
for (var subscription in _scanSubscriptions) {
subscription.cancel();
}
_scanResultsList.latestValue = [];
}
/// Register a subscription to be canceled when scanning is complete.
/// This function simplifies cleanup, so you can prevent creating duplicate stream subscriptions.
/// - this is an optional convenience function
/// - prevents accidentally creating duplicate subscriptions before each scan
static void cancelWhenScanComplete(StreamSubscription subscription) {
_scanSubscriptions.add(subscription);
}
/// Sets the internal FlutterBlue log level
static Future<void> setLogLevel(LogLevel level, {color = true}) async {
// Nothing to implement
return;
}
static Future<void> turnOff({int timeout = 10}) async {
await _initialize();
await WinBle.updateBluetoothState(false);
}
// TODO: need to test
static Future<bool> get isOn async {
await _initialize();
return await WinBle.bleState.asBroadcastStream().first == BleState.On;
}
}

View File

@@ -0,0 +1,433 @@
part of 'windows.dart';
String _hexEncode(List<int> numbers) {
return numbers
.map((n) => (n & 0xFF).toRadixString(16).padLeft(2, '0'))
.join();
}
List<int> _hexDecode(String hex) {
List<int> numbers = [];
for (int i = 0; i < hex.length; i += 2) {
String hexPart = hex.substring(i, i + 2);
int num = int.parse(hexPart, radix: 16);
numbers.add(num);
}
return numbers;
}
int _compareAsciiLowerCase(String a, String b) {
const int upperCaseA = 0x41;
const int upperCaseZ = 0x5a;
const int asciiCaseBit = 0x20;
var defaultResult = 0;
for (var i = 0; i < a.length; i++) {
if (i >= b.length) return 1;
var aChar = a.codeUnitAt(i);
var bChar = b.codeUnitAt(i);
if (aChar == bChar) continue;
var aLowerCase = aChar;
var bLowerCase = bChar;
// Upper case if ASCII letters.
if (upperCaseA <= bChar && bChar <= upperCaseZ) {
bLowerCase += asciiCaseBit;
}
if (upperCaseA <= aChar && aChar <= upperCaseZ) {
aLowerCase += asciiCaseBit;
}
if (aLowerCase != bLowerCase) return (aLowerCase - bLowerCase).sign;
if (defaultResult == 0) defaultResult = aChar - bChar;
}
if (b.length > a.length) return -1;
return defaultResult.sign;
}
// This is a reimplementation of BehaviorSubject from RxDart library.
// It is essentially a stream but:
// 1. we cache the latestValue of the stream
// 2. the "latestValue" is re-emitted whenever the stream is listened to
class _StreamController<T> {
T latestValue;
final StreamController<T> _controller = StreamController<T>.broadcast();
_StreamController({required T initialValue})
: this.latestValue = initialValue;
Stream<T> get stream => _controller.stream;
T get value => latestValue;
void add(T newValue) {
latestValue = newValue;
_controller.add(newValue);
}
void listen(Function(T) onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
onData(latestValue);
_controller.stream.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
Future<void> close() {
return _controller.close();
}
}
// imediately starts listening to a broadcast stream and
// buffering it in a new single-subscription stream
class _BufferStream<T> {
final Stream<T> _inputStream;
late final StreamSubscription? _subscription;
late final StreamController<T> _controller;
late bool hasReceivedValue = false;
_BufferStream.listen(this._inputStream) {
_controller = StreamController<T>(
onCancel: () {
_subscription?.cancel();
},
onPause: () {
_subscription?.pause();
},
onResume: () {
_subscription?.resume();
},
onListen: () {}, // inputStream is already listened to
);
// immediately start listening to the inputStream
_subscription = _inputStream.listen(
(data) {
hasReceivedValue = true;
_controller.add(data);
},
onError: (e) {
_controller.addError(e);
},
onDone: () {
_controller.close();
},
cancelOnError: false,
);
}
void close() {
_subscription?.cancel();
_controller.close();
}
Stream<T> get stream async* {
yield* _controller.stream;
}
}
// helper for 'doOnDone' method for streams.
class _OnDoneTransformer<T> extends StreamTransformerBase<T, T> {
final Function onDone;
_OnDoneTransformer({required this.onDone});
@override
Stream<T> bind(Stream<T> stream) {
if (stream.isBroadcast) {
return _bindBroadcast(stream);
}
return _bindSingleSubscription(stream);
}
Stream<T> _bindSingleSubscription(Stream<T> stream) {
StreamController<T>? controller;
StreamSubscription<T>? subscription;
controller = StreamController<T>(
onListen: () {
subscription = stream.listen(
controller?.add,
onError: controller?.addError,
onDone: () {
onDone();
controller?.close();
},
);
},
onPause: ([Future<dynamic>? resumeSignal]) {
subscription?.pause(resumeSignal);
},
onResume: () {
subscription?.resume();
},
onCancel: () {
return subscription?.cancel();
},
sync: true,
);
return controller.stream;
}
Stream<T> _bindBroadcast(Stream<T> stream) {
StreamController<T>? controller;
StreamSubscription<T>? subscription;
controller = StreamController<T>.broadcast(
onListen: () {
subscription = stream
.listen(controller?.add, onError: controller?.addError, onDone: () {
onDone();
controller?.close();
});
},
onCancel: () {
subscription?.cancel();
},
sync: true,
);
return controller.stream;
}
}
// helper for 'doOnCancel' method for streams.
class _OnCancelTransformer<T> extends StreamTransformerBase<T, T> {
final Function onCancel;
_OnCancelTransformer({required this.onCancel});
@override
Stream<T> bind(Stream<T> stream) {
if (stream.isBroadcast) {
return _bindBroadcast(stream);
}
return _bindSingleSubscription(stream);
}
Stream<T> _bindSingleSubscription(Stream<T> stream) {
StreamController<T>? controller;
StreamSubscription<T>? subscription;
controller = StreamController<T>(
onListen: () {
subscription = stream.listen(
controller?.add,
onError: (Object error) {
controller?.addError(error);
controller?.close();
},
onDone: controller?.close,
);
},
onPause: ([Future<dynamic>? resumeSignal]) {
subscription?.pause(resumeSignal);
},
onResume: () {
subscription?.resume();
},
onCancel: () {
onCancel();
return subscription?.cancel();
},
sync: true,
);
return controller.stream;
}
Stream<T> _bindBroadcast(Stream<T> stream) {
StreamController<T>? controller;
StreamSubscription<T>? subscription;
controller = StreamController<T>.broadcast(
onListen: () {
subscription = stream.listen(
controller?.add,
onError: (Object error) {
controller?.addError(error);
controller?.close();
},
onDone: controller?.close,
);
},
onCancel: () {
onCancel();
subscription?.cancel();
},
sync: true,
);
return controller.stream;
}
}
// Helper for 'newStreamWithInitialValue' method for streams.
class _NewStreamWithInitialValueTransformer<T>
extends StreamTransformerBase<T, T> {
final T initialValue;
_NewStreamWithInitialValueTransformer(this.initialValue);
@override
Stream<T> bind(Stream<T> stream) {
return _bindSingleSubscription(stream);
}
Stream<T> _bindSingleSubscription(Stream<T> stream) {
StreamController<T>? controller;
StreamSubscription<T>? subscription;
controller = StreamController<T>(
onListen: () {
// Emit the initial value
controller?.add(initialValue);
subscription = stream.listen(
controller?.add,
onError: (Object error) {
controller?.addError(error);
controller?.close();
},
onDone: controller?.close,
);
},
onPause: ([Future<dynamic>? resumeSignal]) {
subscription?.pause(resumeSignal);
},
onResume: () {
subscription?.resume();
},
onCancel: () {
return subscription?.cancel();
},
sync: true,
);
return controller.stream;
}
}
extension _StreamDoOnDone<T> on Stream<T> {
// ignore: unused_element
Stream<T> doOnDone(void Function() onDone) {
return transform(_OnDoneTransformer(onDone: onDone));
}
}
extension _StreamDoOnCancel<T> on Stream<T> {
// ignore: unused_element
Stream<T> doOnCancel(void Function() onCancel) {
return transform(_OnCancelTransformer(onCancel: onCancel));
}
}
extension _StreamNewStreamWithInitialValue<T> on Stream<T> {
Stream<T> newStreamWithInitialValue(T initialValue) {
return transform(_NewStreamWithInitialValueTransformer(initialValue));
}
}
// ignore: unused_element
Stream<T> _mergeStreams<T>(List<Stream<T>> streams) {
StreamController<T> controller = StreamController<T>();
List<StreamSubscription<T>> subscriptions = [];
void handleData(T data) {
if (!controller.isClosed) {
controller.add(data);
}
}
void handleError(Object error, StackTrace stackTrace) {
if (!controller.isClosed) {
controller.addError(error, stackTrace);
}
}
void handleDone() {
if (subscriptions.every((s) => s.isPaused)) {
controller.close();
}
}
void subscribeToStream(Stream<T> stream) {
final s =
stream.listen(handleData, onError: handleError, onDone: handleDone);
subscriptions.add(s);
}
streams.forEach(subscribeToStream);
controller.onCancel = () async {
await Future.wait(subscriptions.map((s) => s.cancel()));
};
return controller.stream;
}
// dart is single threaded, but still has task switching.
// this mutex lets a single task through at a time.
class _Mutex {
final StreamController _controller = StreamController.broadcast();
int current = 0;
int issued = 0;
Future<void> take() async {
int mine = issued;
issued++;
// tasks are executed in the same order they call take()
while (mine != current) {
await _controller.stream.first; // wait
}
}
void give() {
current++;
_controller.add(null); // release waiting tasks
}
}
// Create mutexes in a parrallel-safe way,
class _MutexFactory {
static final _Mutex _global = _Mutex();
static final Map<String, _Mutex> _all = {};
static Future<_Mutex> getMutexForKey(String key) async {
_Mutex? value;
await _global.take();
{
_all[key] ??= _Mutex();
value = _all[key];
}
_global.give();
return value!;
}
}
String _black(String s) {
// Use ANSI escape codes
return '\x1B[1;30m$s\x1B[0m';
}
// ignore: unused_element
String _green(String s) {
// Use ANSI escape codes
return '\x1B[1;32m$s\x1B[0m';
}
String _magenta(String s) {
// Use ANSI escape codes
return '\x1B[1;35m$s\x1B[0m';
}
String _brown(String s) {
// Use ANSI escape codes
return '\x1B[1;33m$s\x1B[0m';
}
extension Boolean2ConnectionState on bool {
BluetoothConnectionState get isConnected {
if (this) return BluetoothConnectionState.connected;
return BluetoothConnectionState.disconnected;
}
}

View File

@@ -0,0 +1,14 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as FBP;
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
part 'bluetooth_characteristic_windows.dart';
part 'bluetooth_device_windows.dart';
part 'bluetooth_service_windows.dart';
part 'flutter_blue_plus_windows.dart';
part 'util.dart';

View File

@@ -0,0 +1,140 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as FBP;
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
class FlutterBluePlus {
static Future<void> startScan({
List<Guid> withServices = const [],
List<String> withRemoteIds = const [],
List<String> withNames = const [],
List<String> withKeywords = const [],
List<MsdFilter> withMsd = const [],
List<ServiceDataFilter> withServiceData = const [],
Duration? timeout,
Duration? removeIfGone,
bool continuousUpdates = false,
int continuousDivisor = 1,
bool oneByOne = false,
bool androidLegacy = false,
AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
bool androidUsesFineLocation = false,
List<Guid> webOptionalServices = const [],
}) async {
if (Platform.isWindows) {
return await FlutterBluePlusWindows.startScan(
withServices: withServices,
withRemoteIds: withRemoteIds,
withNames: withNames,
withKeywords: withKeywords,
withMsd: withMsd,
withServiceData: withServiceData,
timeout: timeout,
removeIfGone: removeIfGone,
continuousUpdates: continuousUpdates,
continuousDivisor: continuousDivisor,
oneByOne: oneByOne,
androidLegacy: androidLegacy,
androidScanMode: androidScanMode,
androidUsesFineLocation: androidUsesFineLocation,
);
}
return await FBP.FlutterBluePlus.startScan(
withServices: withServices,
withRemoteIds: withRemoteIds,
withNames: withNames,
withKeywords: withKeywords,
withMsd: withMsd,
withServiceData: withServiceData,
timeout: timeout,
removeIfGone: removeIfGone,
continuousUpdates: continuousUpdates,
continuousDivisor: continuousDivisor,
oneByOne: oneByOne,
androidLegacy: androidLegacy,
androidScanMode: androidScanMode,
androidUsesFineLocation: androidUsesFineLocation,
webOptionalServices: webOptionalServices,
);
}
static Stream<BluetoothAdapterState> get adapterState {
if (Platform.isWindows) return FlutterBluePlusWindows.adapterState;
return FBP.FlutterBluePlus.adapterState;
}
static Stream<List<ScanResult>> get scanResults {
if (Platform.isWindows) return FlutterBluePlusWindows.scanResults;
return FBP.FlutterBluePlus.scanResults;
}
static bool get isScanningNow {
if (Platform.isWindows) return FlutterBluePlusWindows.isScanningNow;
return FBP.FlutterBluePlus.isScanningNow;
}
static Stream<bool> get isScanning {
if (Platform.isWindows) return FlutterBluePlusWindows.isScanning;
return FBP.FlutterBluePlus.isScanning;
}
static Future<void> stopScan() async {
if (Platform.isWindows) return await FlutterBluePlusWindows.stopScan();
return await FBP.FlutterBluePlus.stopScan();
}
static Future<void> setLogLevel(LogLevel level, {color = true}) async {
if (Platform.isWindows) return FlutterBluePlusWindows.setLogLevel(level, color: color);
return FBP.FlutterBluePlus.setLogLevel(level, color: color);
}
/// TODO: need to verify
static LogLevel get logLevel => FBP.FlutterBluePlus.logLevel;
static Future<void> setOptions({bool restoreState = false, bool showPowerAlert = true}) async {
if (Platform.isWindows) return;
FBP.FlutterBluePlus.setOptions(restoreState: restoreState, showPowerAlert: showPowerAlert);
}
static Future<bool> get isSupported async {
if (Platform.isWindows) return await FlutterBluePlusWindows.isSupported;
return await FBP.FlutterBluePlus.isSupported;
}
static Future<String> get adapterName async {
if (Platform.isWindows) return await FlutterBluePlusWindows.adapterName;
return await FBP.FlutterBluePlus.adapterName;
}
static Future<void> turnOn({int timeout = 60}) async {
if (Platform.isWindows) return await FlutterBluePlusWindows.turnOn(timeout: timeout);
return await FBP.FlutterBluePlus.turnOn(timeout: timeout);
}
static List<FBP.BluetoothDevice> get connectedDevices {
if (Platform.isWindows) return FlutterBluePlusWindows.connectedDevices;
return FBP.FlutterBluePlus.connectedDevices;
}
static Future<List<FBP.BluetoothDevice>> systemDevices(List<Guid> withServices) async {
//TODO: connected devices => system devices
if (Platform.isWindows) return FlutterBluePlusWindows.connectedDevices;
return await FBP.FlutterBluePlus.systemDevices(withServices);
}
static Future<PhySupport> getPhySupport() {
return FBP.FlutterBluePlus.getPhySupport();
}
static Future<List<FBP.BluetoothDevice>> get bondedDevices async {
if (Platform.isWindows) return FlutterBluePlusWindows.connectedDevices;
return await FBP.FlutterBluePlus.bondedDevices;
}
static void cancelWhenScanComplete(StreamSubscription subscription) {
if (Platform.isWindows) return FlutterBluePlusWindows.cancelWhenScanComplete(subscription);
return FBP.FlutterBluePlus.cancelWhenScanComplete(subscription);
}
}

View File

@@ -0,0 +1 @@
export 'flutter_blue_plus_wrapper.dart';

View File

@@ -0,0 +1,13 @@
name: flutter_blue_plus_windows
description: Flutter blue plus for Windows
version: 1.26.1
repository: https://github.com/chan150/flutter_blue_plus_windows
#publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
flutter_blue_plus: ">=1.32.4"
win_ble: ">=1.1.1"
stream_with_value: ">=0.5.0"

View File

@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/ble.dart';
import 'package:swift_control/widgets/small_progress_indicator.dart';

View File

@@ -7,8 +7,10 @@ import Foundation
import flutter_blue_plus_darwin
import keypress_simulator_macos
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
KeypressSimulatorMacosPlugin.register(with: registry.registrar(forPlugin: "KeypressSimulatorMacosPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}

View File

@@ -174,6 +174,13 @@ packages:
url: "https://github.com/chipweinberger/flutter_blue_plus.git"
source: git
version: "3.0.0"
flutter_blue_plus_windows:
dependency: "direct main"
description:
path: flutter_blue_plus_windows
relative: true
source: path
version: "1.26.1"
flutter_lints:
dependency: "direct dev"
description:
@@ -288,6 +295,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
url: "https://pub.dev"
source: hosted
version: "2.2.16"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
@@ -296,6 +351,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
@@ -357,6 +420,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_with_value:
dependency: transitive
description:
name: stream_with_value
sha256: "483d79bf604fdea5274e31207956b2f624f5f03a506cacf081b65cdfcfa647a6"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
string_scanner:
dependency: transitive
description:
@@ -429,6 +500,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win_ble:
dependency: transitive
description:
name: win_ble
sha256: "2a867e13c4b355b101fc2c6e2ac85eeebf965db34eca46856f8b478e93b41e96"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
@@ -439,4 +526,4 @@ packages:
version: "6.5.0"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.27.0"

View File

@@ -15,6 +15,8 @@ dependencies:
dartx: any
pointycastle: any
keypress_simulator: ^0.2.0
flutter_blue_plus_windows:
path: flutter_blue_plus_windows
accessibility:
path: accessibility