6.0.0 (#74)
* 调整接口 * 临时提交 * 重构 Android 平台代码 * 临时提交 * 临时提交 * Android 6.0.0-dev.0 * 临时提交 * 实现 Windows 接口 * windows-6.0.0-dev.0 * Darwin 6.0.0-dev.0 * 临时提交 * 1 * 临时提交 * 调整接口 * windows-6.0.0-dev.1 * 临时提交 * interface-6.0.0-dev.7 * interface-6.0.0-dev.8 * 临时提交 * windows-6.0.0-dev.2 * 删除多余脚本 * interface-6.0.0-dev.9 * 临时提交 * 临时提交 * interface-6.0.0-dev.10 * android-6.0.0-dev.1 * windows-6.0.0-dev.3 * 临时提交 * interface-6.0.0-dev.11 * windows-6.0.0-dev.4 * 更新 pubspec.lock * 1 * interface-6.0.0-dev.12 * interface-6.0.0-dev.13 * interface-6.0.0-dev.14 * 临时提交 * interface-6.0.0-dev.15 * 临时提交 * interface-6.0.0-dev.16 * android-6.0.0-dev.2 * 临时提交 * windows-6.0.0-dev.5 * 临时提交 * 临时提交 * windows-6.0.0-dev.6 * 优化注释和代码样式 * 优化代码 * 临时提交 * 实现 Dart 接口 * darwin-6.0.0-dev.0 * linux-6.0.0-dev.0 * 修复已知问题 * 修复问题 * 6.0.0-dev.0 * 修改包名 * 更新版本 * 移除原生部分 * 临时提交 * 修复问题 * 更新 pigeon 19.0.0 * 更新 README,添加迁移文档 * linux-6.0.0-dev.1 * 解析扫描回复和扩展广播 * 修复 googletest 版本警告问题 * Use centralArgs instead of addressArgs * interface-6.0.0-dev.18 * android-6.0.0-dev.4 * linux-6.0.0-dev.2 * windows-6.0.0-dev.8 * darwin-6.0.0-dev.2 * 6.0.0-dev.1 * Update LICENSE * clang-format * Combine ADV_IND and SCAN_RES * TEMP commit: update exampe * Adjust advertisement combine logic * Implement `MyPeripheralMananger` on Windows * Added NuGet auto download and scan for names on peripheral (#67) * fetch nuget using other technique * move FetchContent to right location in CMakeLists.txt * also added hash for googletest --------- Co-authored-by: Kevin De Keyser <kevin@dekeyser.ch> * Fix errors. * Check BluetoothAdapter role supported state and implement PeripheralManager on Flutter side. * Sort code * Fix known errors * interface-6.0.0-dev.19 * windows-6.0.0-dev.9 * Optimize example * android-6.0.0-dev.5 * Optimize the Adverrtisement BottomSheet. * linux-6.0.0-dev.3 * Update dependency * Fix example errors. * Temp commit. * darwin-6.0.0-dev.3 * 6.0.0-dev.2 * Update README.md * 6.0.0 * darwin-6.0.0-dev.4 * android-6.0.0-dev.6 * 6.0.0-dev.3 * Update docs. * interface-6.0.0 * android-6.0.0 * darwin-6.0.0 * linux-6.0.0 * windows-6.0.0 * 6.0.0 * Update dependency --------- Co-authored-by: Kevin De Keyser <dekeyser.kevin97@gmail.com> Co-authored-by: Kevin De Keyser <kevin@dekeyser.ch>
This commit is contained in:
1
bluetooth_low_energy_linux/.gitignore
vendored
1
bluetooth_low_energy_linux/.gitignore
vendored
@ -26,5 +26,4 @@ migrate_working_dir/
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
|
@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "efbf63d9c66b9f6ec30e9ad4611189aa80003d31"
|
||||
revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1"
|
||||
channel: "stable"
|
||||
|
||||
project_type: plugin
|
||||
@ -13,11 +13,11 @@ project_type: plugin
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
|
||||
base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
|
||||
- platform: linux
|
||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
|
||||
base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
|
||||
|
||||
# User provided section
|
||||
|
||||
|
@ -1,3 +1,27 @@
|
||||
## 6.0.0
|
||||
|
||||
* Implement new APIs.
|
||||
* Rewrite example with MVVM.
|
||||
* Fix known issues.
|
||||
|
||||
## 6.0.0-dev.3
|
||||
|
||||
* Rewrite example with MVVM.
|
||||
* Fix known issues.
|
||||
|
||||
## 6.0.0-dev.2
|
||||
|
||||
* Fix example errors.
|
||||
|
||||
## 6.0.0-dev.1
|
||||
|
||||
* Move organization.
|
||||
* Fix errors.
|
||||
|
||||
## 6.0.0-dev.0
|
||||
|
||||
* Implement new APIs.
|
||||
|
||||
## 5.0.2
|
||||
|
||||
* Change flutter minimum version to 3.0.0.
|
||||
|
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 yanshouwang
|
||||
Copyright (c) 2024 hebei.dev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -27,7 +27,6 @@ migrate_working_dir/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
@ -1,6 +1,6 @@
|
||||
# bluetooth_low_energy_example
|
||||
# bluetooth_low_energy_linux_example
|
||||
|
||||
Demonstrates how to use the bluetooth_low_energy plugin.
|
||||
Demonstrates how to use the bluetooth_low_energy_linux plugin.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
@ -6,17 +6,8 @@
|
||||
// For more information about Flutter integration tests, please see
|
||||
// https://docs.flutter.dev/cookbook/testing/integration/introduction
|
||||
|
||||
// import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// testWidgets('getPlatformVersion test', (WidgetTester tester) async {
|
||||
// final BluetoothLowEnergy plugin = BluetoothLowEnergy();
|
||||
// final String? version = await plugin.getPlatformVersion();
|
||||
// // The version string depends on the host platform running the test, so
|
||||
// // just assert that some non-empty string is returned.
|
||||
// expect(version?.isNotEmpty, true);
|
||||
// });
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'router_config.dart';
|
||||
|
||||
void main() {
|
||||
runZonedGuarded(onStartUp, onCrashed);
|
||||
@ -14,9 +12,7 @@ void main() {
|
||||
|
||||
void onStartUp() async {
|
||||
Logger.root.onRecord.listen(onLogRecord);
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await CentralManager.instance.setUp();
|
||||
// await peripheralManager.setUp();
|
||||
hierarchicalLoggingEnabled = true;
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
@ -37,795 +33,19 @@ void onLogRecord(LogRecord record) {
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData.light(
|
||||
useMaterial3: true,
|
||||
).copyWith(
|
||||
return MaterialApp.router(
|
||||
routerConfig: routerConfig,
|
||||
theme: ThemeData.light().copyWith(
|
||||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||||
),
|
||||
home: const HomeView(),
|
||||
routes: {
|
||||
'peripheral': (context) {
|
||||
final route = ModalRoute.of(context);
|
||||
final eventArgs = route!.settings.arguments as DiscoveredEventArgs;
|
||||
return PeripheralView(
|
||||
eventArgs: eventArgs,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomeView extends StatelessWidget {
|
||||
const HomeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: buildBody(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
return const ScannerView();
|
||||
}
|
||||
}
|
||||
|
||||
class ScannerView extends StatefulWidget {
|
||||
const ScannerView({super.key});
|
||||
|
||||
@override
|
||||
State<ScannerView> createState() => _ScannerViewState();
|
||||
}
|
||||
|
||||
class _ScannerViewState extends State<ScannerView> {
|
||||
late final ValueNotifier<BluetoothLowEnergyState> state;
|
||||
late final ValueNotifier<bool> discovering;
|
||||
late final ValueNotifier<List<DiscoveredEventArgs>> discoveredEventArgs;
|
||||
late final StreamSubscription stateChangedSubscription;
|
||||
late final StreamSubscription discoveredSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
state = ValueNotifier(BluetoothLowEnergyState.unknown);
|
||||
discovering = ValueNotifier(false);
|
||||
discoveredEventArgs = ValueNotifier([]);
|
||||
stateChangedSubscription = CentralManager.instance.stateChanged.listen(
|
||||
(eventArgs) {
|
||||
state.value = eventArgs.state;
|
||||
},
|
||||
);
|
||||
discoveredSubscription = CentralManager.instance.discovered.listen(
|
||||
(eventArgs) {
|
||||
final name = eventArgs.advertisement.name;
|
||||
if (name == null || name.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final items = discoveredEventArgs.value;
|
||||
final i = items.indexWhere(
|
||||
(item) => item.peripheral == eventArgs.peripheral,
|
||||
);
|
||||
if (i < 0) {
|
||||
discoveredEventArgs.value = [...items, eventArgs];
|
||||
} else {
|
||||
items[i] = eventArgs;
|
||||
discoveredEventArgs.value = [...items];
|
||||
}
|
||||
},
|
||||
);
|
||||
setUp();
|
||||
}
|
||||
|
||||
void setUp() async {
|
||||
state.value = await CentralManager.instance.getState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: buildAppBar(context),
|
||||
body: buildBody(context),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget buildAppBar(BuildContext context) {
|
||||
return AppBar(
|
||||
title: const Text('Scanner'),
|
||||
actions: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: state,
|
||||
builder: (context, state, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: discovering,
|
||||
builder: (context, discovering, child) {
|
||||
return TextButton(
|
||||
onPressed: state == BluetoothLowEnergyState.poweredOn
|
||||
? () async {
|
||||
if (discovering) {
|
||||
await stopDiscovery();
|
||||
} else {
|
||||
await startDiscovery();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
discovering ? 'END' : 'BEGIN',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> startDiscovery() async {
|
||||
discoveredEventArgs.value = [];
|
||||
await CentralManager.instance.startDiscovery();
|
||||
discovering.value = true;
|
||||
}
|
||||
|
||||
Future<void> stopDiscovery() async {
|
||||
await CentralManager.instance.stopDiscovery();
|
||||
discovering.value = false;
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: discoveredEventArgs,
|
||||
builder: (context, discoveredEventArgs, child) {
|
||||
// final items = discoveredEventArgs;
|
||||
final items = discoveredEventArgs
|
||||
.where((eventArgs) => eventArgs.advertisement.name != null)
|
||||
.toList();
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, i) {
|
||||
final theme = Theme.of(context);
|
||||
final item = items[i];
|
||||
final uuid = item.peripheral.uuid;
|
||||
final rssi = item.rssi;
|
||||
final advertisement = item.advertisement;
|
||||
final name = advertisement.name;
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
final discovering = this.discovering.value;
|
||||
if (discovering) {
|
||||
await stopDiscovery();
|
||||
}
|
||||
if (!mounted) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
await Navigator.of(context).pushNamed(
|
||||
'peripheral',
|
||||
arguments: item,
|
||||
);
|
||||
if (discovering) {
|
||||
await startDiscovery();
|
||||
}
|
||||
},
|
||||
onLongPress: () async {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return BottomSheet(
|
||||
onClosing: () {},
|
||||
clipBehavior: Clip.antiAlias,
|
||||
builder: (context) {
|
||||
final manufacturerSpecificData =
|
||||
advertisement.manufacturerSpecificData;
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 40.0,
|
||||
),
|
||||
itemBuilder: (context, i) {
|
||||
const idWidth = 80.0;
|
||||
if (i == 0) {
|
||||
return const Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: idWidth,
|
||||
child: Text('ID'),
|
||||
),
|
||||
Expanded(
|
||||
child: Text('DATA'),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
final id =
|
||||
'0x${manufacturerSpecificData!.id.toRadixString(16).padLeft(4, '0')}';
|
||||
final value =
|
||||
hex.encode(manufacturerSpecificData.data);
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: idWidth,
|
||||
child: Text(id),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(value),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
separatorBuilder: (context, i) {
|
||||
return const Divider();
|
||||
},
|
||||
itemCount: manufacturerSpecificData == null ? 1 : 2,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
title: Text(name ?? 'N/A'),
|
||||
subtitle: Text(
|
||||
'$uuid',
|
||||
style: theme.textTheme.bodySmall,
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RssiWidget(rssi),
|
||||
Text('$rssi'),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, i) {
|
||||
return const Divider(
|
||||
height: 0.0,
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
stateChangedSubscription.cancel();
|
||||
discoveredSubscription.cancel();
|
||||
state.dispose();
|
||||
discovering.dispose();
|
||||
discoveredEventArgs.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class PeripheralView extends StatefulWidget {
|
||||
final DiscoveredEventArgs eventArgs;
|
||||
|
||||
const PeripheralView({
|
||||
super.key,
|
||||
required this.eventArgs,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PeripheralView> createState() => _PeripheralViewState();
|
||||
}
|
||||
|
||||
class _PeripheralViewState extends State<PeripheralView> {
|
||||
late final ValueNotifier<bool> connectionState;
|
||||
late final DiscoveredEventArgs eventArgs;
|
||||
late final ValueNotifier<List<GattService>> services;
|
||||
late final ValueNotifier<List<GattCharacteristic>> characteristics;
|
||||
late final ValueNotifier<GattService?> service;
|
||||
late final ValueNotifier<GattCharacteristic?> characteristic;
|
||||
late final ValueNotifier<GattCharacteristicWriteType> writeType;
|
||||
late final ValueNotifier<List<Log>> logs;
|
||||
late final TextEditingController writeController;
|
||||
late final StreamSubscription connectionStateChangedSubscription;
|
||||
late final StreamSubscription characteristicNotifiedSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
eventArgs = widget.eventArgs;
|
||||
connectionState = ValueNotifier(false);
|
||||
services = ValueNotifier([]);
|
||||
characteristics = ValueNotifier([]);
|
||||
service = ValueNotifier(null);
|
||||
characteristic = ValueNotifier(null);
|
||||
writeType = ValueNotifier(GattCharacteristicWriteType.withResponse);
|
||||
logs = ValueNotifier([]);
|
||||
writeController = TextEditingController();
|
||||
connectionStateChangedSubscription =
|
||||
CentralManager.instance.connectionStateChanged.listen(
|
||||
(eventArgs) {
|
||||
if (eventArgs.peripheral != this.eventArgs.peripheral) {
|
||||
return;
|
||||
}
|
||||
final connectionState = eventArgs.connectionState;
|
||||
this.connectionState.value = connectionState;
|
||||
if (!connectionState) {
|
||||
services.value = [];
|
||||
characteristics.value = [];
|
||||
service.value = null;
|
||||
characteristic.value = null;
|
||||
logs.value = [];
|
||||
}
|
||||
},
|
||||
);
|
||||
characteristicNotifiedSubscription =
|
||||
CentralManager.instance.characteristicNotified.listen(
|
||||
(eventArgs) {
|
||||
// final characteristic = this.characteristic.value;
|
||||
// if (eventArgs.characteristic != characteristic) {
|
||||
// return;
|
||||
// }
|
||||
const type = LogType.notify;
|
||||
final log = Log(type, eventArgs.value);
|
||||
logs.value = [
|
||||
...logs.value,
|
||||
log,
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
onPopInvoked: (didPop) async {
|
||||
if (connectionState.value) {
|
||||
final peripheral = eventArgs.peripheral;
|
||||
await CentralManager.instance.disconnect(peripheral);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: buildAppBar(context),
|
||||
body: buildBody(context),
|
||||
darkTheme: ThemeData.dark().copyWith(
|
||||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget buildAppBar(BuildContext context) {
|
||||
final title = eventArgs.advertisement.name ?? '';
|
||||
return AppBar(
|
||||
title: Text(title),
|
||||
actions: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: connectionState,
|
||||
builder: (context, state, child) {
|
||||
return TextButton(
|
||||
onPressed: () async {
|
||||
final peripheral = eventArgs.peripheral;
|
||||
if (state) {
|
||||
await CentralManager.instance.disconnect(peripheral);
|
||||
} else {
|
||||
await CentralManager.instance.connect(peripheral);
|
||||
services.value =
|
||||
await CentralManager.instance.discoverGATT(peripheral);
|
||||
}
|
||||
},
|
||||
child: Text(state ? 'DISCONNECT' : 'CONNECT'),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: services,
|
||||
builder: (context, services, child) {
|
||||
final items = services.map((service) {
|
||||
return DropdownMenuItem(
|
||||
value: service,
|
||||
child: Text(
|
||||
'${service.uuid}',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: service,
|
||||
builder: (context, service, child) {
|
||||
return DropdownButton(
|
||||
isExpanded: true,
|
||||
items: items,
|
||||
hint: const Text('CHOOSE A SERVICE'),
|
||||
value: service,
|
||||
onChanged: (service) async {
|
||||
this.service.value = service;
|
||||
characteristic.value = null;
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
characteristics.value = service.characteristics;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: characteristics,
|
||||
builder: (context, characteristics, child) {
|
||||
final items = characteristics.map((characteristic) {
|
||||
return DropdownMenuItem(
|
||||
value: characteristic,
|
||||
child: Text(
|
||||
'${characteristic.uuid}',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: characteristic,
|
||||
builder: (context, characteristic, child) {
|
||||
return DropdownButton(
|
||||
isExpanded: true,
|
||||
items: items,
|
||||
hint: const Text('CHOOSE A CHARACTERISTIC'),
|
||||
value: characteristic,
|
||||
onChanged: (characteristic) {
|
||||
if (characteristic == null) {
|
||||
return;
|
||||
}
|
||||
this.characteristic.value = characteristic;
|
||||
final writeType = this.writeType.value;
|
||||
final canWrite = characteristic.properties.contains(
|
||||
GattCharacteristicProperty.write,
|
||||
);
|
||||
final canWriteWithoutResponse =
|
||||
characteristic.properties.contains(
|
||||
GattCharacteristicProperty.writeWithoutResponse,
|
||||
);
|
||||
if (writeType ==
|
||||
GattCharacteristicWriteType.withResponse &&
|
||||
!canWrite &&
|
||||
canWriteWithoutResponse) {
|
||||
this.writeType.value =
|
||||
GattCharacteristicWriteType.withoutResponse;
|
||||
}
|
||||
if (writeType ==
|
||||
GattCharacteristicWriteType.withoutResponse &&
|
||||
!canWriteWithoutResponse &&
|
||||
canWrite) {
|
||||
this.writeType.value =
|
||||
GattCharacteristicWriteType.withResponse;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: logs,
|
||||
builder: (context, logs, child) {
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, i) {
|
||||
final log = logs[i];
|
||||
final type = log.type.name.toUpperCase().characters.first;
|
||||
final Color typeColor;
|
||||
switch (log.type) {
|
||||
case LogType.read:
|
||||
typeColor = Colors.blue;
|
||||
break;
|
||||
case LogType.write:
|
||||
typeColor = Colors.amber;
|
||||
break;
|
||||
case LogType.notify:
|
||||
typeColor = Colors.red;
|
||||
break;
|
||||
default:
|
||||
typeColor = Colors.black;
|
||||
}
|
||||
final time = DateFormat.Hms().format(log.time);
|
||||
final value = log.value;
|
||||
final message = hex.encode(value);
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
text: '[$type:${value.length}]',
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' $time: ',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: message,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: typeColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: logs.length,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: characteristic,
|
||||
builder: (context, characteristic, chld) {
|
||||
final bool canNotify, canRead, canWrite, canWriteWithoutResponse;
|
||||
if (characteristic == null) {
|
||||
canNotify =
|
||||
canRead = canWrite = canWriteWithoutResponse = false;
|
||||
} else {
|
||||
final properties = characteristic.properties;
|
||||
canNotify = properties.contains(
|
||||
GattCharacteristicProperty.notify,
|
||||
) ||
|
||||
properties.contains(
|
||||
GattCharacteristicProperty.indicate,
|
||||
);
|
||||
canRead = properties.contains(
|
||||
GattCharacteristicProperty.read,
|
||||
);
|
||||
canWrite = properties.contains(
|
||||
GattCharacteristicProperty.write,
|
||||
);
|
||||
canWriteWithoutResponse = properties.contains(
|
||||
GattCharacteristicProperty.writeWithoutResponse,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: characteristic != null && canNotify
|
||||
? () async {
|
||||
await CentralManager.instance
|
||||
.setCharacteristicNotifyState(
|
||||
characteristic,
|
||||
state: true,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text('NOTIFY'),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
ElevatedButton(
|
||||
onPressed: characteristic != null && canRead
|
||||
? () async {
|
||||
final value = await CentralManager.instance
|
||||
.readCharacteristic(characteristic);
|
||||
const type = LogType.read;
|
||||
final log = Log(type, value);
|
||||
logs.value = [...logs.value, log];
|
||||
}
|
||||
: null,
|
||||
child: const Text('READ'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 160.0,
|
||||
child: TextField(
|
||||
controller: writeController,
|
||||
enabled: canWrite || canWriteWithoutResponse,
|
||||
expands: true,
|
||||
maxLines: null,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: writeType,
|
||||
builder: (context, writeType, child) {
|
||||
return ToggleButtons(
|
||||
onPressed: canWrite || canWriteWithoutResponse
|
||||
? (i) {
|
||||
if (!canWrite || !canWriteWithoutResponse) {
|
||||
return;
|
||||
}
|
||||
final type =
|
||||
GattCharacteristicWriteType.values[i];
|
||||
this.writeType.value = type;
|
||||
}
|
||||
: null,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 0.0,
|
||||
minHeight: 0.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
isSelected: GattCharacteristicWriteType.values
|
||||
.map((type) => type == writeType)
|
||||
.toList(),
|
||||
children: GattCharacteristicWriteType.values.map(
|
||||
(type) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Text(type.name),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
// final segments =
|
||||
// GattCharacteristicWriteType.values.map((type) {
|
||||
// return ButtonSegment(
|
||||
// value: type,
|
||||
// label: Text(type.name),
|
||||
// );
|
||||
// }).toList();
|
||||
// return SegmentedButton(
|
||||
// segments: segments,
|
||||
// selected: {writeType},
|
||||
// showSelectedIcon: false,
|
||||
// style: OutlinedButton.styleFrom(
|
||||
// tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
// padding: EdgeInsets.zero,
|
||||
// visualDensity: VisualDensity.compact,
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(8.0),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: characteristic != null && canWrite
|
||||
? () async {
|
||||
final text = writeController.text;
|
||||
final elements = utf8.encode(text);
|
||||
final value = Uint8List.fromList(elements);
|
||||
final type = writeType.value;
|
||||
// Fragments the value by 512 bytes.
|
||||
const fragmentSize = 512;
|
||||
var start = 0;
|
||||
while (start < value.length) {
|
||||
final end = start + fragmentSize;
|
||||
final fragmentedValue = end < value.length
|
||||
? value.sublist(start, end)
|
||||
: value.sublist(start);
|
||||
await CentralManager.instance
|
||||
.writeCharacteristic(
|
||||
characteristic,
|
||||
value: fragmentedValue,
|
||||
type: type,
|
||||
);
|
||||
final log = Log(
|
||||
LogType.write,
|
||||
fragmentedValue,
|
||||
);
|
||||
logs.value = [...logs.value, log];
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: const Text('WRITE'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
connectionStateChangedSubscription.cancel();
|
||||
characteristicNotifiedSubscription.cancel();
|
||||
connectionState.dispose();
|
||||
services.dispose();
|
||||
characteristics.dispose();
|
||||
service.dispose();
|
||||
characteristic.dispose();
|
||||
writeType.dispose();
|
||||
logs.dispose();
|
||||
writeController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class Log {
|
||||
final DateTime time;
|
||||
final LogType type;
|
||||
final Uint8List value;
|
||||
final String? detail;
|
||||
|
||||
Log(
|
||||
this.type,
|
||||
this.value, [
|
||||
this.detail,
|
||||
]) : time = DateTime.now();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final type = this.type.toString().split('.').last;
|
||||
final formatter = DateFormat.Hms();
|
||||
final time = formatter.format(this.time);
|
||||
final message = hex.encode(value);
|
||||
if (detail == null) {
|
||||
return '[$type]$time: $message';
|
||||
} else {
|
||||
return '[$type]$time: $message /* $detail */';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LogType {
|
||||
read,
|
||||
write,
|
||||
notify,
|
||||
error,
|
||||
}
|
||||
|
||||
class RssiWidget extends StatelessWidget {
|
||||
final int rssi;
|
||||
|
||||
const RssiWidget(
|
||||
this.rssi, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final IconData icon;
|
||||
if (rssi > -70) {
|
||||
icon = Icons.wifi_rounded;
|
||||
} else if (rssi > -100) {
|
||||
icon = Icons.wifi_2_bar_rounded;
|
||||
} else {
|
||||
icon = Icons.wifi_1_bar_rounded;
|
||||
}
|
||||
return Icon(icon);
|
||||
}
|
||||
}
|
||||
|
1
bluetooth_low_energy_linux/example/lib/models.dart
Normal file
1
bluetooth_low_energy_linux/example/lib/models.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'models/log.dart';
|
10
bluetooth_low_energy_linux/example/lib/models/log.dart
Normal file
10
bluetooth_low_energy_linux/example/lib/models/log.dart
Normal file
@ -0,0 +1,10 @@
|
||||
class Log {
|
||||
final DateTime time;
|
||||
final String type;
|
||||
final String message;
|
||||
|
||||
Log({
|
||||
required this.type,
|
||||
required this.message,
|
||||
}) : time = DateTime.now();
|
||||
}
|
85
bluetooth_low_energy_linux/example/lib/router_config.dart
Normal file
85
bluetooth_low_energy_linux/example/lib/router_config.dart
Normal file
@ -0,0 +1,85 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'view_models.dart';
|
||||
import 'views.dart';
|
||||
|
||||
final routerConfig = GoRouter(
|
||||
redirect: (context, state) {
|
||||
if (state.matchedLocation == '/') {
|
||||
return '/central';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
StatefulShellRoute(
|
||||
// builder: (context, state, navigationShell) {
|
||||
// return HomeView(navigationShell: navigationShell);
|
||||
// },
|
||||
builder: (context, state, navigationShell) => navigationShell,
|
||||
navigatorContainerBuilder: (context, navigationShell, children) {
|
||||
final navigators = children.mapIndexed(
|
||||
(index, element) {
|
||||
if (index == 0) {
|
||||
return ViewModelBinding(
|
||||
viewBuilder: (context) => element,
|
||||
viewModelBuilder: (context) => CentralManagerViewModel(),
|
||||
);
|
||||
} else {
|
||||
return element;
|
||||
}
|
||||
},
|
||||
).toList();
|
||||
return HomeView(
|
||||
navigationShell: navigationShell,
|
||||
navigators: navigators,
|
||||
);
|
||||
},
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/central',
|
||||
builder: (context, state) {
|
||||
return const CentralManagerView();
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':uuid',
|
||||
builder: (context, state) {
|
||||
final uuidValue = state.pathParameters['uuid']!;
|
||||
final uuid = UUID.fromString(uuidValue);
|
||||
final viewModel =
|
||||
ViewModel.of<CentralManagerViewModel>(context);
|
||||
final eventArgs = viewModel.discoveries.firstWhere(
|
||||
(discovery) => discovery.peripheral.uuid == uuid);
|
||||
return ViewModelBinding(
|
||||
viewBuilder: (context) => PeripheralView(),
|
||||
viewModelBuilder: (context) =>
|
||||
PeripheralViewModel(eventArgs),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/peripheral',
|
||||
builder: (context, state) {
|
||||
return ViewModelBinding(
|
||||
viewBuilder: (context) => const PeripheralManagerView(),
|
||||
viewModelBuilder: (context) => PeripheralManagerViewModel(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
6
bluetooth_low_energy_linux/example/lib/view_models.dart
Normal file
6
bluetooth_low_energy_linux/example/lib/view_models.dart
Normal file
@ -0,0 +1,6 @@
|
||||
export 'view_models/central_manager_view_model.dart';
|
||||
export 'view_models/peripheral_view_model.dart';
|
||||
export 'view_models/service_view_model.dart';
|
||||
export 'view_models/characteristic_view_model.dart';
|
||||
export 'view_models/descriptor_view_model.dart';
|
||||
export 'view_models/peripheral_manager_view_model.dart';
|
@ -0,0 +1,67 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class CentralManagerViewModel extends ViewModel {
|
||||
final CentralManager _manager;
|
||||
final List<DiscoveredEventArgs> _discoveries;
|
||||
bool _discovering;
|
||||
|
||||
late final StreamSubscription _stateChangedSubscription;
|
||||
late final StreamSubscription _discoveredSubscription;
|
||||
|
||||
CentralManagerViewModel()
|
||||
: _manager = CentralManager()..logLevel = Level.INFO,
|
||||
_discoveries = [],
|
||||
_discovering = false {
|
||||
_stateChangedSubscription = _manager.stateChanged.listen((eventArgs) {
|
||||
notifyListeners();
|
||||
});
|
||||
_discoveredSubscription = _manager.discovered.listen((eventArgs) {
|
||||
final peripheral = eventArgs.peripheral;
|
||||
final index = _discoveries.indexWhere((i) => i.peripheral == peripheral);
|
||||
if (index < 0) {
|
||||
_discoveries.add(eventArgs);
|
||||
} else {
|
||||
_discoveries[index] = eventArgs;
|
||||
}
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
BluetoothLowEnergyState get state => _manager.state;
|
||||
bool get discovering => _discovering;
|
||||
List<DiscoveredEventArgs> get discoveries => _discoveries;
|
||||
|
||||
Future<void> startDiscovery({
|
||||
List<UUID>? serviceUUIDs,
|
||||
}) async {
|
||||
if (_discovering) {
|
||||
return;
|
||||
}
|
||||
_discoveries.clear();
|
||||
await _manager.startDiscovery(
|
||||
serviceUUIDs: serviceUUIDs,
|
||||
);
|
||||
_discovering = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> stopDiscovery() async {
|
||||
if (!_discovering) {
|
||||
return;
|
||||
}
|
||||
await _manager.stopDiscovery();
|
||||
_discovering = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stateChangedSubscription.cancel();
|
||||
_discoveredSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluetooth_low_energy_linux_example/models.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
|
||||
import 'descriptor_view_model.dart';
|
||||
|
||||
class CharacteristicViewModel extends ViewModel {
|
||||
final CentralManager _manager;
|
||||
final Peripheral _peripheral;
|
||||
final GATTCharacteristic _characteristic;
|
||||
final List<DescriptorViewModel> _descriptorViewModels;
|
||||
final List<Log> _logs;
|
||||
|
||||
GATTCharacteristicWriteType _writeType;
|
||||
bool _notifyState;
|
||||
|
||||
late final StreamSubscription _characteristicNotifiedSubscription;
|
||||
|
||||
CharacteristicViewModel({
|
||||
required CentralManager manager,
|
||||
required Peripheral peripheral,
|
||||
required GATTCharacteristic characteristic,
|
||||
}) : _manager = manager,
|
||||
_peripheral = peripheral,
|
||||
_characteristic = characteristic,
|
||||
_descriptorViewModels = characteristic.descriptors
|
||||
.map((descriptor) => DescriptorViewModel(descriptor))
|
||||
.toList(),
|
||||
_logs = [],
|
||||
_writeType = GATTCharacteristicWriteType.withResponse,
|
||||
_notifyState = false {
|
||||
if (!canWrite && canWriteWithoutResponse) {
|
||||
_writeType = GATTCharacteristicWriteType.withoutResponse;
|
||||
}
|
||||
_characteristicNotifiedSubscription =
|
||||
_manager.characteristicNotified.listen((eventArgs) {
|
||||
if (eventArgs.characteristic != _characteristic) {
|
||||
return;
|
||||
}
|
||||
final log = Log(
|
||||
type: 'Notified',
|
||||
message: '[${eventArgs.value.length}] ${eventArgs.value}',
|
||||
);
|
||||
_logs.add(log);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
UUID get uuid => _characteristic.uuid;
|
||||
bool get canRead =>
|
||||
_characteristic.properties.contains(GATTCharacteristicProperty.read);
|
||||
bool get canWrite =>
|
||||
_characteristic.properties.contains(GATTCharacteristicProperty.write);
|
||||
bool get canWriteWithoutResponse => _characteristic.properties
|
||||
.contains(GATTCharacteristicProperty.writeWithoutResponse);
|
||||
bool get canNotify =>
|
||||
_characteristic.properties.contains(GATTCharacteristicProperty.notify) ||
|
||||
_characteristic.properties.contains(GATTCharacteristicProperty.indicate);
|
||||
List<DescriptorViewModel> get descriptorViewModels => _descriptorViewModels;
|
||||
List<Log> get logs => _logs;
|
||||
GATTCharacteristicWriteType get writeType => _writeType;
|
||||
bool get notifyState => _notifyState;
|
||||
|
||||
Future<void> read() async {
|
||||
final value = await _manager.readCharacteristic(
|
||||
_peripheral,
|
||||
_characteristic,
|
||||
);
|
||||
final log = Log(
|
||||
type: 'Read',
|
||||
message: '[${value.length}] $value',
|
||||
);
|
||||
_logs.add(log);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setWriteType(GATTCharacteristicWriteType type) {
|
||||
if (type == GATTCharacteristicWriteType.withResponse && !canWrite) {
|
||||
throw ArgumentError.value(type);
|
||||
}
|
||||
if (type == GATTCharacteristicWriteType.withoutResponse &&
|
||||
!canWriteWithoutResponse) {
|
||||
throw ArgumentError.value(type);
|
||||
}
|
||||
_writeType = type;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> write(Uint8List value) async {
|
||||
// Fragments the value by maximumWriteLength.
|
||||
final fragmentSize = await _manager.getMaximumWriteLength(
|
||||
_peripheral,
|
||||
type: writeType,
|
||||
);
|
||||
var start = 0;
|
||||
while (start < value.length) {
|
||||
final end = start + fragmentSize;
|
||||
final fragmentedValue =
|
||||
end < value.length ? value.sublist(start, end) : value.sublist(start);
|
||||
final type = writeType;
|
||||
await _manager.writeCharacteristic(
|
||||
_peripheral,
|
||||
_characteristic,
|
||||
value: fragmentedValue,
|
||||
type: type,
|
||||
);
|
||||
final log = Log(
|
||||
type: type == GATTCharacteristicWriteType.withResponse
|
||||
? 'Write'
|
||||
: 'Write without response',
|
||||
message: '[${value.length}] $value',
|
||||
);
|
||||
_logs.add(log);
|
||||
notifyListeners();
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setNotifyState(bool state) async {
|
||||
await _manager.setCharacteristicNotifyState(
|
||||
_peripheral,
|
||||
_characteristic,
|
||||
state: state,
|
||||
);
|
||||
_notifyState = state;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearLogs() {
|
||||
_logs.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_characteristicNotifiedSubscription.cancel();
|
||||
for (var descriptorViewModel in descriptorViewModels) {
|
||||
descriptorViewModel.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
|
||||
class DescriptorViewModel extends ViewModel {
|
||||
final GATTDescriptor _descriptor;
|
||||
|
||||
DescriptorViewModel(this._descriptor);
|
||||
|
||||
UUID get uuid => _descriptor.uuid;
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluetooth_low_energy_linux_example/models.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class PeripheralManagerViewModel extends ViewModel {
|
||||
final PeripheralManager _manager;
|
||||
final List<Log> _logs;
|
||||
bool _advertising;
|
||||
|
||||
late final StreamSubscription _stateChangedSubscription;
|
||||
late final StreamSubscription _characteristicReadRequestedSubscription;
|
||||
late final StreamSubscription _characteristicWriteRequestedSubscription;
|
||||
late final StreamSubscription _characteristicNotifyStateChangedSubscription;
|
||||
|
||||
PeripheralManagerViewModel()
|
||||
: _manager = PeripheralManager()..logLevel = Level.INFO,
|
||||
_logs = [],
|
||||
_advertising = false {
|
||||
_stateChangedSubscription = _manager.stateChanged.listen((eventArgs) {
|
||||
notifyListeners();
|
||||
});
|
||||
_characteristicReadRequestedSubscription =
|
||||
_manager.characteristicReadRequested.listen((eventArgs) async {
|
||||
final central = eventArgs.central;
|
||||
final characteristic = eventArgs.characteristic;
|
||||
final request = eventArgs.request;
|
||||
final offset = request.offset;
|
||||
final log = Log(
|
||||
type: 'Characteristic read requested',
|
||||
message: '${central.uuid}, ${characteristic.uuid}, $offset',
|
||||
);
|
||||
_logs.add(log);
|
||||
notifyListeners();
|
||||
final elements = List.generate(100, (i) => i % 256);
|
||||
final value = Uint8List.fromList(elements);
|
||||
final trimmedValue = value.sublist(offset);
|
||||
await _manager.respondReadRequestWithValue(
|
||||
request,
|
||||
value: trimmedValue,
|
||||
);
|
||||
});
|
||||
_characteristicWriteRequestedSubscription =
|
||||
_manager.characteristicWriteRequested.listen((eventArgs) async {
|
||||
final central = eventArgs.central;
|
||||
final characteristic = eventArgs.characteristic;
|
||||
final request = eventArgs.request;
|
||||
final offset = request.offset;
|
||||
final value = request.value;
|
||||
final log = Log(
|
||||
type: 'Characteristic write requested',
|
||||
message:
|
||||
'[${value.length}] ${central.uuid}, ${characteristic.uuid}, $offset, $value',
|
||||
);
|
||||
_logs.add(log);
|
||||
notifyListeners();
|
||||
await _manager.respondWriteRequest(request);
|
||||
});
|
||||
_characteristicNotifyStateChangedSubscription =
|
||||
_manager.characteristicNotifyStateChanged.listen((eventArgs) async {
|
||||
final central = eventArgs.central;
|
||||
final characteristic = eventArgs.characteristic;
|
||||
final state = eventArgs.state;
|
||||
final log = Log(
|
||||
type: 'Characteristic notify state changed',
|
||||
message: '${central.uuid}, ${characteristic.uuid}, $state',
|
||||
);
|
||||
_logs.add(log);
|
||||
notifyListeners();
|
||||
// Write someting to the central when notify started.
|
||||
if (state) {
|
||||
final maximumNotifyLength =
|
||||
await _manager.getMaximumNotifyLength(central);
|
||||
final elements = List.generate(maximumNotifyLength, (i) => i % 256);
|
||||
final value = Uint8List.fromList(elements);
|
||||
await _manager.notifyCharacteristic(
|
||||
central,
|
||||
characteristic,
|
||||
value: value,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
BluetoothLowEnergyState get state => _manager.state;
|
||||
bool get advertising => _advertising;
|
||||
List<Log> get logs => _logs;
|
||||
|
||||
Future<void> startAdvertising() async {
|
||||
if (_advertising) {
|
||||
return;
|
||||
}
|
||||
await _manager.removeAllServices();
|
||||
final elements = List.generate(100, (i) => i % 256);
|
||||
final value = Uint8List.fromList(elements);
|
||||
final service = GATTService(
|
||||
uuid: UUID.short(100),
|
||||
isPrimary: true,
|
||||
includedServices: [],
|
||||
characteristics: [
|
||||
GATTCharacteristic.immutable(
|
||||
uuid: UUID.short(200),
|
||||
value: value,
|
||||
descriptors: [],
|
||||
),
|
||||
GATTCharacteristic.mutable(
|
||||
uuid: UUID.short(201),
|
||||
properties: [
|
||||
GATTCharacteristicProperty.read,
|
||||
GATTCharacteristicProperty.write,
|
||||
GATTCharacteristicProperty.writeWithoutResponse,
|
||||
GATTCharacteristicProperty.notify,
|
||||
GATTCharacteristicProperty.indicate,
|
||||
],
|
||||
permissions: [
|
||||
GATTCharacteristicPermission.read,
|
||||
GATTCharacteristicPermission.write,
|
||||
],
|
||||
descriptors: [],
|
||||
),
|
||||
],
|
||||
);
|
||||
await _manager.addService(service);
|
||||
final advertisement = Advertisement(
|
||||
manufacturerSpecificData: [
|
||||
ManufacturerSpecificData(
|
||||
id: 0x2e19,
|
||||
data: Uint8List.fromList([0x01, 0x02, 0x03]),
|
||||
)
|
||||
],
|
||||
);
|
||||
await _manager.startAdvertising(advertisement);
|
||||
_advertising = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> stopAdvertising() async {
|
||||
if (!_advertising) {
|
||||
return;
|
||||
}
|
||||
await _manager.stopAdvertising();
|
||||
_advertising = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearLogs() {
|
||||
_logs.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stateChangedSubscription.cancel();
|
||||
_characteristicReadRequestedSubscription.cancel();
|
||||
_characteristicWriteRequestedSubscription.cancel();
|
||||
_characteristicNotifyStateChangedSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:hybrid_logging/hybrid_logging.dart';
|
||||
|
||||
import 'service_view_model.dart';
|
||||
|
||||
class PeripheralViewModel extends ViewModel with TypeLogger {
|
||||
final CentralManager _manager;
|
||||
final Peripheral _peripheral;
|
||||
final String? _name;
|
||||
bool _connected;
|
||||
List<ServiceViewModel> _serviceViewModels;
|
||||
|
||||
late final StreamSubscription _connectionStateChangedSubscription;
|
||||
|
||||
PeripheralViewModel(DiscoveredEventArgs eventArgs)
|
||||
: _manager = CentralManager(),
|
||||
_peripheral = eventArgs.peripheral,
|
||||
_name = eventArgs.advertisement.name,
|
||||
_connected = false,
|
||||
_serviceViewModels = [] {
|
||||
_connectionStateChangedSubscription =
|
||||
_manager.connectionStateChanged.listen((eventArgs) {
|
||||
if (eventArgs.peripheral != _peripheral) {
|
||||
return;
|
||||
}
|
||||
if (eventArgs.state == ConnectionState.connected) {
|
||||
_connected = true;
|
||||
} else {
|
||||
_connected = false;
|
||||
_serviceViewModels = [];
|
||||
}
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
UUID get uuid => _peripheral.uuid;
|
||||
String? get name => _name;
|
||||
bool get connected => _connected;
|
||||
List<ServiceViewModel> get serviceViewModels => _serviceViewModels;
|
||||
|
||||
Future<void> connect() async {
|
||||
await _manager.connect(_peripheral);
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
await _manager.disconnect(_peripheral);
|
||||
}
|
||||
|
||||
Future<void> discoverGATT() async {
|
||||
final services = await _manager.discoverGATT(_peripheral);
|
||||
_serviceViewModels = services
|
||||
.map((service) => ServiceViewModel(
|
||||
manager: _manager,
|
||||
peripheral: _peripheral,
|
||||
service: service,
|
||||
))
|
||||
.toList();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (connected) {
|
||||
disconnect();
|
||||
}
|
||||
_connectionStateChangedSubscription.cancel();
|
||||
for (var serviceViewModel in serviceViewModels) {
|
||||
serviceViewModel.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
|
||||
import 'characteristic_view_model.dart';
|
||||
|
||||
class ServiceViewModel extends ViewModel {
|
||||
final GATTService _service;
|
||||
|
||||
final List<ServiceViewModel> _includedServiceViewModels;
|
||||
final List<CharacteristicViewModel> _characteristicViewModels;
|
||||
|
||||
ServiceViewModel({
|
||||
required CentralManager manager,
|
||||
required Peripheral peripheral,
|
||||
required GATTService service,
|
||||
}) : _service = service,
|
||||
_includedServiceViewModels = [],
|
||||
_characteristicViewModels = service.characteristics
|
||||
.map((characteristic) => CharacteristicViewModel(
|
||||
manager: manager,
|
||||
peripheral: peripheral,
|
||||
characteristic: characteristic,
|
||||
))
|
||||
.toList();
|
||||
|
||||
UUID get uuid => _service.uuid;
|
||||
bool get isPrimary => _service.isPrimary;
|
||||
List<ServiceViewModel> get includedServiceViewModels =>
|
||||
_includedServiceViewModels;
|
||||
List<CharacteristicViewModel> get characteristicViewModels =>
|
||||
_characteristicViewModels;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var characteristicViewModel in characteristicViewModels) {
|
||||
characteristicViewModel.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
4
bluetooth_low_energy_linux/example/lib/views.dart
Normal file
4
bluetooth_low_energy_linux/example/lib/views.dart
Normal file
@ -0,0 +1,4 @@
|
||||
export 'views/home_view.dart';
|
||||
export 'views/central_manager_view.dart';
|
||||
export 'views/peripheral_manager_view.dart';
|
||||
export 'views/peripheral_view.dart';
|
@ -0,0 +1,69 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AdvertisementView extends StatelessWidget {
|
||||
final Advertisement advertisement;
|
||||
|
||||
const AdvertisementView({
|
||||
super.key,
|
||||
required this.advertisement,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final manufacturerSpecificData = advertisement.manufacturerSpecificData;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const idWidth = 100.0;
|
||||
final valueWidth = constraints.maxWidth - idWidth - 16.0 * 2.0;
|
||||
return DataTable(
|
||||
columnSpacing: 0.0,
|
||||
horizontalMargin: 16.0,
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Container(
|
||||
width: idWidth,
|
||||
alignment: Alignment.center,
|
||||
child: const Text('Id'),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Container(
|
||||
width: valueWidth,
|
||||
alignment: Alignment.center,
|
||||
child: const Text('Value'),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: manufacturerSpecificData.map((item) {
|
||||
final id = '0x${item.id.toRadixString(16).padLeft(4, '0')}';
|
||||
final value = hex.encode(item.data);
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Container(
|
||||
width: idWidth,
|
||||
alignment: Alignment.center,
|
||||
child: Text(id),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Container(
|
||||
width: valueWidth,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluetooth_low_energy_linux_example/view_models.dart';
|
||||
import 'package:bluetooth_low_energy_linux_example/widgets.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'advertisement_view.dart';
|
||||
|
||||
class CentralManagerView extends StatelessWidget {
|
||||
const CentralManagerView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = ViewModel.of<CentralManagerViewModel>(context);
|
||||
final state = viewModel.state;
|
||||
final discovering = viewModel.discovering;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Central Manager'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: state == BluetoothLowEnergyState.poweredOn
|
||||
? () async {
|
||||
if (discovering) {
|
||||
await viewModel.stopDiscovery();
|
||||
} else {
|
||||
await viewModel.startDiscovery();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(discovering ? 'END' : 'BEGIN'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: buildBody(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
final viewModel = ViewModel.of<CentralManagerViewModel>(context);
|
||||
final state = viewModel.state;
|
||||
if (state == BluetoothLowEnergyState.poweredOn) {
|
||||
final discoveries = viewModel.discoveries;
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
final theme = Theme.of(context);
|
||||
final discovery = discoveries[index];
|
||||
final uuid = discovery.peripheral.uuid;
|
||||
final name = discovery.advertisement.name;
|
||||
final rssi = discovery.rssi;
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
onTapDissovery(context, discovery);
|
||||
},
|
||||
onLongPress: () {
|
||||
onLongPressDiscovery(context, discovery);
|
||||
},
|
||||
title: Text(name ?? ''),
|
||||
subtitle: Text(
|
||||
'$uuid',
|
||||
style: theme.textTheme.bodySmall,
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RSSIIndicator(rssi),
|
||||
Text('$rssi'),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, i) {
|
||||
return const Divider(
|
||||
height: 0.0,
|
||||
);
|
||||
},
|
||||
itemCount: discoveries.length,
|
||||
);
|
||||
} else {
|
||||
return Center(
|
||||
child: Text(
|
||||
'$state',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onTapDissovery(
|
||||
BuildContext context, DiscoveredEventArgs discovery) async {
|
||||
final viewModel = ViewModel.of<CentralManagerViewModel>(context);
|
||||
if (viewModel.discovering) {
|
||||
await viewModel.stopDiscovery();
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
final uuid = discovery.peripheral.uuid;
|
||||
context.go('/central/$uuid');
|
||||
}
|
||||
|
||||
void onLongPressDiscovery(
|
||||
BuildContext context, DiscoveredEventArgs discovery) async {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AdvertisementView(
|
||||
advertisement: discovery.advertisement,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:bluetooth_low_energy_linux_example/view_models.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CharacteristicTreeNodeView extends StatelessWidget {
|
||||
const CharacteristicTreeNodeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = ViewModel.of<CharacteristicViewModel>(context);
|
||||
return Text(
|
||||
'${viewModel.uuid}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluetooth_low_energy_linux_example/view_models.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
import 'log_view.dart';
|
||||
|
||||
class CharacteristicView extends StatefulWidget {
|
||||
const CharacteristicView({super.key});
|
||||
|
||||
@override
|
||||
State<CharacteristicView> createState() => _CharacteristicViewState();
|
||||
}
|
||||
|
||||
class _CharacteristicViewState extends State<CharacteristicView> {
|
||||
late final TextEditingController _textController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = ViewModel.of<CharacteristicViewModel>(context);
|
||||
final logs = viewModel.logs;
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return OverflowBox(
|
||||
alignment: Alignment.topCenter,
|
||||
maxHeight: constraints.maxHeight + 1.0,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.all(12.0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: const Radius.circular(12.0),
|
||||
bottom: viewModel.canWrite ||
|
||||
viewModel.canWriteWithoutResponse
|
||||
? Radius.zero
|
||||
: const Radius.circular(12.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, i) {
|
||||
final log = logs[i];
|
||||
return LogView(
|
||||
log: log,
|
||||
);
|
||||
},
|
||||
itemCount: logs.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: viewModel.canNotify,
|
||||
child: viewModel.notifyState
|
||||
? IconButton.filled(
|
||||
onPressed: () async {
|
||||
await viewModel.setNotifyState(false);
|
||||
},
|
||||
icon:
|
||||
const Icon(Symbols.notifications_active),
|
||||
)
|
||||
: IconButton.filledTonal(
|
||||
onPressed: () async {
|
||||
await viewModel.setNotifyState(true);
|
||||
},
|
||||
icon: const Icon(Symbols.notifications_off),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: viewModel.canRead,
|
||||
child: IconButton.filled(
|
||||
onPressed: () async {
|
||||
await viewModel.read();
|
||||
},
|
||||
icon: const Icon(Symbols.arrow_downward),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: viewModel.canWrite ||
|
||||
viewModel.canWriteWithoutResponse,
|
||||
child: viewModel.writeType ==
|
||||
GATTCharacteristicWriteType.withResponse
|
||||
? IconButton.filled(
|
||||
onPressed: viewModel.canWriteWithoutResponse
|
||||
? () {
|
||||
viewModel.setWriteType(
|
||||
GATTCharacteristicWriteType
|
||||
.withoutResponse);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Symbols.swap_vert),
|
||||
)
|
||||
: IconButton.filledTonal(
|
||||
onPressed: viewModel.canWrite
|
||||
? () {
|
||||
viewModel.setWriteType(
|
||||
GATTCharacteristicWriteType
|
||||
.withResponse);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Symbols.arrow_upward),
|
||||
),
|
||||
),
|
||||
IconButton.filled(
|
||||
onPressed: () => viewModel.clearLogs(),
|
||||
icon: const Icon(Symbols.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: viewModel.canWrite || viewModel.canWriteWithoutResponse,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(12.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _textController,
|
||||
builder: (context, tev, child) {
|
||||
final text = tev.text;
|
||||
return IconButton.filled(
|
||||
onPressed: text.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
final value = utf8.encode(text);
|
||||
await viewModel.write(value);
|
||||
},
|
||||
icon: const Icon(Symbols.pets),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:bluetooth_low_energy_linux_example/view_models.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DescriptorTreeNodeView extends StatelessWidget {
|
||||
const DescriptorTreeNodeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = ViewModel.of<DescriptorViewModel>(context);
|
||||
return Text(
|
||||
'${viewModel.uuid}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
96
bluetooth_low_energy_linux/example/lib/views/home_view.dart
Normal file
96
bluetooth_low_energy_linux/example/lib/views/home_view.dart
Normal file
@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class HomeView extends StatefulWidget {
|
||||
final StatefulNavigationShell navigationShell;
|
||||
final List<Widget> navigators;
|
||||
|
||||
const HomeView({
|
||||
super.key,
|
||||
required this.navigationShell,
|
||||
required this.navigators,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HomeView> createState() => _HomeViewState();
|
||||
}
|
||||
|
||||
class _HomeViewState extends State<HomeView> {
|
||||
late final PageController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = PageController(
|
||||
initialPage: widget.navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final navigationShell = widget.navigationShell;
|
||||
final navigators = widget.navigators;
|
||||
return Scaffold(
|
||||
// body: navigationShell,
|
||||
body: PageView.builder(
|
||||
controller: _controller,
|
||||
onPageChanged: (index) {
|
||||
// Ignore tap events.
|
||||
if (index == navigationShell.currentIndex) {
|
||||
return;
|
||||
}
|
||||
navigationShell.goBranch(
|
||||
index,
|
||||
initialLocation: false,
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return navigators[index];
|
||||
},
|
||||
itemCount: navigators.length,
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
onTap: (index) {
|
||||
navigationShell.goBranch(
|
||||
index,
|
||||
initialLocation: index == navigationShell.currentIndex,
|
||||
);
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.radar),
|
||||
label: 'Central',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.sensors),
|
||||
label: 'Peripheral',
|
||||
),
|
||||
],
|
||||
currentIndex: widget.navigationShell.currentIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant HomeView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final navigationShell = widget.navigationShell;
|
||||
final page = _controller.page ?? _controller.initialPage;
|
||||
final index = page.round();
|
||||
// Ignore swipe events.
|
||||
if (index == navigationShell.currentIndex) {
|
||||
return;
|
||||
}
|
||||
_controller.animateToPage(
|
||||
navigationShell.currentIndex,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.linear,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
43
bluetooth_low_energy_linux/example/lib/views/log_view.dart
Normal file
43
bluetooth_low_energy_linux/example/lib/views/log_view.dart
Normal file
@ -0,0 +1,43 @@
|
||||
import 'package:bluetooth_low_energy_linux_example/models.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class LogView extends StatelessWidget {
|
||||
final Log log;
|
||||
|
||||
const LogView({
|
||||
super.key,
|
||||
required this.log,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formatter = DateFormat.Hms();
|
||||
final time = formatter.format(log.time);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '[$time] ',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: log.type,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
log.message,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluetooth_low_energy_linux_example/view_models.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
import 'log_view.dart';
|
||||
|
||||
class PeripheralManagerView extends StatelessWidget {
|
||||
const PeripheralManagerView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = ViewModel.of<PeripheralManagerViewModel>(context);
|
||||
final state = viewModel.state;
|
||||
final advertising = viewModel.advertising;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Peripheral Manager'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: state == BluetoothLowEnergyState.poweredOn
|
||||
? () async {
|
||||
if (advertising) {
|
||||
await viewModel.stopAdvertising();
|
||||
} else {
|
||||
await viewModel.startAdvertising();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(advertising ? 'END' : 'BEGIN'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: buildBody(context),
|
||||
floatingActionButton: state == BluetoothLowEnergyState.poweredOn
|
||||
? FloatingActionButton(
|
||||
onPressed: () => viewModel.clearLogs(),
|
||||
child: const Icon(Symbols.delete),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
final viewModel = ViewModel.of<PeripheralManagerViewModel>(context);
|
||||
final state = viewModel.state;
|
||||
if (state == BluetoothLowEnergyState.poweredOn) {
|
||||
final logs = viewModel.logs;
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemBuilder: (context, i) {
|
||||
final log = logs[i];
|
||||
return LogView(
|
||||
log: log,
|
||||
);
|
||||
},
|
||||
itemCount: logs.length,
|
||||
);
|
||||
} else {
|
||||
return Center(
|
||||
child: Text(
|
||||
'$state',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
import 'package:bluetooth_low_energy_linux_example/view_models.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_simple_treeview/flutter_simple_treeview.dart';
|
||||
|
||||
import 'characteristic_tree_node_view.dart';
|
||||
import 'characteristic_view.dart';
|
||||
import 'descriptor_tree_node_view.dart';
|
||||
import 'service_tree_node_view.dart';
|
||||
|
||||
class PeripheralView extends StatelessWidget {
|
||||
final TreeController _treeController;
|
||||
|
||||
PeripheralView({super.key})
|
||||
: _treeController = TreeController(
|
||||
allNodesExpanded: false,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = ViewModel.of<PeripheralViewModel>(context);
|
||||
final connected = viewModel.connected;
|
||||
final serviceViewModels = viewModel.serviceViewModels;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(viewModel.name ?? '${viewModel.uuid}'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (connected) {
|
||||
await viewModel.disconnect();
|
||||
} else {
|
||||
await viewModel.connect();
|
||||
await viewModel.discoverGATT();
|
||||
}
|
||||
},
|
||||
child: Text(connected ? 'DISCONNECT' : 'CONNECT'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: TreeView(
|
||||
treeController: _treeController,
|
||||
indent: 0.0,
|
||||
nodes: _buildServiceTreeNodes(serviceViewModels),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<TreeNode> _buildServiceTreeNodes(List<ServiceViewModel> viewModels) {
|
||||
return viewModels.map((viewModel) {
|
||||
final includedServiceViewModels = viewModel.includedServiceViewModels;
|
||||
final characteristicViewModels = viewModel.characteristicViewModels;
|
||||
return TreeNode(
|
||||
children: [
|
||||
..._buildServiceTreeNodes(includedServiceViewModels),
|
||||
..._buildCharacteristicTreeNodes(characteristicViewModels),
|
||||
],
|
||||
content: InheritedViewModel(
|
||||
view: const ServiceTreeNodeView(),
|
||||
viewModel: viewModel,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<TreeNode> _buildCharacteristicTreeNodes(
|
||||
List<CharacteristicViewModel> viewModels) {
|
||||
return viewModels.map((viewModel) {
|
||||
final descriptorViewModels = viewModel.descriptorViewModels;
|
||||
return TreeNode(
|
||||
children: [
|
||||
TreeNode(
|
||||
content: Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 40.0),
|
||||
height: 360.0,
|
||||
child: InheritedViewModel(
|
||||
view: const CharacteristicView(),
|
||||
viewModel: viewModel,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
..._buildDescriptorTreeNodes(descriptorViewModels),
|
||||
],
|
||||
content: InheritedViewModel(
|
||||
view: const CharacteristicTreeNodeView(),
|
||||
viewModel: viewModel,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<TreeNode> _buildDescriptorTreeNodes(
|
||||
List<DescriptorViewModel> viewModels) {
|
||||
return viewModels.map((viewModel) {
|
||||
return TreeNode(
|
||||
content: InheritedViewModel(
|
||||
view: const DescriptorTreeNodeView(),
|
||||
viewModel: viewModel,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:bluetooth_low_energy_linux_example/view_models.dart';
|
||||
import 'package:clover/clover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ServiceTreeNodeView extends StatelessWidget {
|
||||
const ServiceTreeNodeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = ViewModel.of<ServiceViewModel>(context);
|
||||
return Text(
|
||||
'${viewModel.uuid}',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
1
bluetooth_low_energy_linux/example/lib/widgets.dart
Normal file
1
bluetooth_low_energy_linux/example/lib/widgets.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'widgets/rssi_indicator.dart';
|
@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RSSIIndicator extends StatelessWidget {
|
||||
final int rssi;
|
||||
|
||||
const RSSIIndicator(
|
||||
this.rssi, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final IconData icon;
|
||||
if (rssi > -70) {
|
||||
icon = Icons.wifi_rounded;
|
||||
} else if (rssi > -100) {
|
||||
icon = Icons.wifi_2_bar_rounded;
|
||||
} else {
|
||||
icon = Icons.wifi_1_bar_rounded;
|
||||
}
|
||||
return Icon(icon);
|
||||
}
|
||||
}
|
@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "bluetooth_low_energy_example")
|
||||
set(BINARY_NAME "bluetooth_low_energy_linux_example")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "dev.yanshouwang.bluetooth_low_energy")
|
||||
set(APPLICATION_ID "dev.hebei.bluetooth_low_energy_linux")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
@ -87,7 +87,7 @@ set_target_properties(${BINARY_NAME}
|
||||
)
|
||||
|
||||
# Enable the test target.
|
||||
set(include_bluetooth_low_energy_tests TRUE)
|
||||
set(include_bluetooth_low_energy_linux_tests TRUE)
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
@ -125,6 +125,12 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
|
@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) {
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "bluetooth_low_energy_example");
|
||||
gtk_header_bar_set_title(header_bar, "bluetooth_low_energy_linux_example");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "bluetooth_low_energy_example");
|
||||
gtk_window_set_title(window, "bluetooth_low_energy_linux_example");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
@ -81,6 +81,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GApplication::startup.
|
||||
static void my_application_startup(GApplication* application) {
|
||||
//MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application startup.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||
}
|
||||
|
||||
// Implements GApplication::shutdown.
|
||||
static void my_application_shutdown(GApplication* application) {
|
||||
//MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application shutdown.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
@ -91,6 +109,8 @@ static void my_application_dispose(GObject* object) {
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
|
@ -5,10 +5,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.5.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -23,15 +23,15 @@ packages:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "5.0.2"
|
||||
version: "6.0.0"
|
||||
bluetooth_low_energy_platform_interface:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bluetooth_low_energy_platform_interface
|
||||
sha256: "5af74eb8f97a896dfdbcff2da284d4fe5b4e2e49ebb2f46f826ac1810f66e4d7"
|
||||
sha256: bc2e8d97c141653e5747bcb3cdc9fe956541b6ecc6e5f158b99a2f3abc2d946a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
version: "6.0.0"
|
||||
bluez:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -64,8 +64,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
clover:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: clover
|
||||
sha256: eba28e69b32f174a51c093eef098cf5ae646470322727081d5d3d8f66c786487
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
@ -84,10 +92,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
version: "1.0.8"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -134,7 +142,15 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
flutter_simple_treeview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_simple_treeview
|
||||
sha256: ad4978d2668dd078d3a09966832da111bef9102dd636e572c50c80133b7ff4d9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
@ -143,11 +159,32 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "6ad5662b014c06c20fa46ab78715c96b2222a7fe4f87bf77e0289592c2539e86"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.1.3"
|
||||
hybrid_logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hybrid_logging
|
||||
sha256: "54248d52ce68c14702a42fbc4083bac5c6be30f6afad8a41be4bbadd197b8af5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -165,44 +202,36 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
version: "10.0.4"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.3"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
log_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: log_service
|
||||
sha256: "21124936899e227d1779268077921d46d57456e2592d1562e455be273594e2e4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "4.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
@ -225,14 +254,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
material_symbols_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: material_symbols_icons
|
||||
sha256: b2d3cbc3c42b8a217715b0d97ff03aebb14b2b4592875736e5599c603fb2db7e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2758.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.12.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -330,10 +367,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
version: "0.7.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -354,10 +391,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
version: "14.2.1"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -375,5 +412,5 @@ packages:
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
sdks:
|
||||
dart: ">=3.3.0-279.1.beta <4.0.0"
|
||||
flutter: ">=3.0.0"
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: bluetooth_low_energy_example
|
||||
description: Demonstrates how to use the bluetooth_low_energy plugin.
|
||||
name: bluetooth_low_energy_linux_example
|
||||
description: "Demonstrates how to use the bluetooth_low_energy_linux plugin."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
@ -17,10 +17,10 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
bluetooth_low_energy_platform_interface: ^5.0.0
|
||||
bluetooth_low_energy_platform_interface: ^6.0.0
|
||||
bluetooth_low_energy_linux:
|
||||
# When depending on this package from a real application you should use:
|
||||
# bluetooth_low_energy: ^x.y.z
|
||||
# bluetooth_low_energy_linux: ^x.y.z
|
||||
# See https://dart.dev/tools/pub/dependencies#version-constraints
|
||||
# The example app is bundled with the plugin so we use a path dependency on
|
||||
# the parent directory to use the current plugin's version.
|
||||
@ -28,9 +28,16 @@ dependencies:
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
convert: ^3.1.1
|
||||
cupertino_icons: ^1.0.6
|
||||
clover: ^3.0.0
|
||||
go_router: ^14.1.3
|
||||
logging: ^1.2.0
|
||||
hybrid_logging: ^1.0.0
|
||||
intl: ^0.19.0
|
||||
collection: ^1.18.0
|
||||
convert: ^3.1.1
|
||||
flutter_simple_treeview: ^3.0.2
|
||||
material_symbols_icons: ^4.2744.0
|
||||
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
@ -43,7 +50,7 @@ dev_dependencies:
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^3.0.0
|
||||
flutter_lints: ^4.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
@ -5,23 +5,4 @@
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:bluetooth_low_energy_example/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Verify Platform version', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that platform version is retrieved.
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(Widget widget) => widget is Text &&
|
||||
widget.data!.startsWith('Running on:'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
void main() {}
|
||||
|
@ -2,8 +2,8 @@ import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_pla
|
||||
|
||||
import 'src/my_central_manager.dart';
|
||||
|
||||
abstract class BluetoothLowEnergyLinux {
|
||||
abstract class BluetoothLowEnergyLinuxPlugin {
|
||||
static void registerWith() {
|
||||
CentralManager.instance = MyCentralManager();
|
||||
PlatformCentralManager.instance = MyCentralManager();
|
||||
}
|
||||
}
|
||||
|
@ -3,38 +3,38 @@ import 'dart:typed_data';
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluez/bluez.dart';
|
||||
|
||||
import 'my_gatt_characteristic2.dart';
|
||||
import 'my_gatt_descriptor2.dart';
|
||||
import 'my_gatt_service2.dart';
|
||||
import 'my_gatt.dart';
|
||||
|
||||
extension BlueZUUIDX on BlueZUUID {
|
||||
UUID toMyUUID() => UUID(value);
|
||||
}
|
||||
|
||||
extension BlueZGattCharacteristicFlagX on BlueZGattCharacteristicFlag {
|
||||
GattCharacteristicProperty? toMyProperty() {
|
||||
GATTCharacteristicProperty? toMyProperty() {
|
||||
switch (this) {
|
||||
case BlueZGattCharacteristicFlag.read:
|
||||
return GattCharacteristicProperty.read;
|
||||
return GATTCharacteristicProperty.read;
|
||||
case BlueZGattCharacteristicFlag.write:
|
||||
return GattCharacteristicProperty.write;
|
||||
return GATTCharacteristicProperty.write;
|
||||
case BlueZGattCharacteristicFlag.writeWithoutResponse:
|
||||
return GattCharacteristicProperty.writeWithoutResponse;
|
||||
return GATTCharacteristicProperty.writeWithoutResponse;
|
||||
case BlueZGattCharacteristicFlag.notify:
|
||||
return GattCharacteristicProperty.notify;
|
||||
return GATTCharacteristicProperty.notify;
|
||||
case BlueZGattCharacteristicFlag.indicate:
|
||||
return GattCharacteristicProperty.indicate;
|
||||
return GATTCharacteristicProperty.indicate;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GattCharacteristicWriteTypeX on GattCharacteristicWriteType {
|
||||
extension GattCharacteristicWriteTypeX on GATTCharacteristicWriteType {
|
||||
BlueZGattCharacteristicWriteType toBlueZWriteType() {
|
||||
switch (this) {
|
||||
case GattCharacteristicWriteType.withResponse:
|
||||
case GATTCharacteristicWriteType.withResponse:
|
||||
return BlueZGattCharacteristicWriteType.request;
|
||||
case GattCharacteristicWriteType.withoutResponse:
|
||||
case GATTCharacteristicWriteType.withoutResponse:
|
||||
return BlueZGattCharacteristicWriteType.command;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,35 +50,26 @@ extension BlueZAdapterX on BlueZAdapter {
|
||||
extension BlueZDeviceX on BlueZDevice {
|
||||
UUID get myUUID => UUID.fromAddress(address);
|
||||
|
||||
List<MyGattService2> get myServices =>
|
||||
gattServices.map((service) => MyGattService2(service)).toList();
|
||||
List<MyGATTService> get myServices =>
|
||||
gattServices.map((service) => MyGATTService(service)).toList();
|
||||
|
||||
Advertisement get myAdvertisement {
|
||||
final myName = name.isNotEmpty ? name : null;
|
||||
final myServiceUUIDs = uuids.map((uuid) => uuid.toMyUUID()).toList();
|
||||
final myServiceData = serviceData.map((uuid, data) {
|
||||
final myUUID = uuid.toMyUUID();
|
||||
final myData = Uint8List.fromList(data);
|
||||
return MapEntry(myUUID, myData);
|
||||
});
|
||||
return Advertisement(
|
||||
name: myName,
|
||||
serviceUUIDs: myServiceUUIDs,
|
||||
serviceData: myServiceData,
|
||||
manufacturerSpecificData: myManufacturerSpecificData,
|
||||
);
|
||||
}
|
||||
|
||||
ManufacturerSpecificData? get myManufacturerSpecificData {
|
||||
final entry = manufacturerData.entries.lastOrNull;
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
final myId = entry.key.id;
|
||||
final myData = Uint8List.fromList(entry.value);
|
||||
return ManufacturerSpecificData(
|
||||
id: myId,
|
||||
data: myData,
|
||||
name: name.isEmpty ? null : name,
|
||||
serviceUUIDs: uuids.map((uuid) => uuid.toMyUUID()).toList(),
|
||||
serviceData: serviceData.map((uuid, data) {
|
||||
final myUUID = uuid.toMyUUID();
|
||||
final myData = Uint8List.fromList(data);
|
||||
return MapEntry(myUUID, myData);
|
||||
}),
|
||||
manufacturerSpecificData: manufacturerData.entries.map((entry) {
|
||||
final myId = entry.key.id;
|
||||
final myData = Uint8List.fromList(entry.value);
|
||||
return ManufacturerSpecificData(
|
||||
id: myId,
|
||||
data: myData,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -90,23 +81,19 @@ extension BlueZGattDescriptorX on BlueZGattDescriptor {
|
||||
extension MyBlueZGattCharacteristic on BlueZGattCharacteristic {
|
||||
UUID get myUUID => uuid.toMyUUID();
|
||||
|
||||
List<GattCharacteristicProperty> get myProperties => flags
|
||||
List<GATTCharacteristicProperty> get myProperties => flags
|
||||
.map((e) => e.toMyProperty())
|
||||
.whereType<GattCharacteristicProperty>()
|
||||
.whereType<GATTCharacteristicProperty>()
|
||||
.toList();
|
||||
|
||||
List<MyGattDescriptor2> get myDescriptors =>
|
||||
descriptors.map((descriptor) => MyGattDescriptor2(descriptor)).toList();
|
||||
List<MyGATTDescriptor> get myDescriptors =>
|
||||
descriptors.map((descriptor) => MyGATTDescriptor(descriptor)).toList();
|
||||
}
|
||||
|
||||
extension BlueZGattServiceX on BlueZGattService {
|
||||
UUID get myUUID => uuid.toMyUUID();
|
||||
|
||||
List<MyGattCharacteristic2> get myCharacteristics => characteristics
|
||||
.map((characteristic) => MyGattCharacteristic2(characteristic))
|
||||
List<MyGATTCharacteristic> get myCharacteristics => characteristics
|
||||
.map((characteristic) => MyGATTCharacteristic(characteristic))
|
||||
.toList();
|
||||
}
|
||||
|
||||
extension BlueZUUIDX on BlueZUUID {
|
||||
UUID toMyUUID() => UUID(value);
|
||||
}
|
||||
|
@ -5,29 +5,34 @@ import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_pla
|
||||
import 'package:bluez/bluez.dart';
|
||||
|
||||
import 'my_bluez.dart';
|
||||
import 'my_event_args.dart';
|
||||
import 'my_gatt_characteristic2.dart';
|
||||
import 'my_gatt_descriptor2.dart';
|
||||
import 'my_gatt_service2.dart';
|
||||
import 'my_peripheral2.dart';
|
||||
import 'my_gatt.dart';
|
||||
import 'my_peripheral.dart';
|
||||
|
||||
class MyCentralManager extends CentralManager {
|
||||
final class BlueZDeviceServicesResolvedEventArgs extends EventArgs {
|
||||
final BlueZDevice device;
|
||||
|
||||
BlueZDeviceServicesResolvedEventArgs(this.device);
|
||||
}
|
||||
|
||||
final class MyCentralManager extends PlatformCentralManager {
|
||||
final BlueZClient _blueZClient;
|
||||
final StreamController<BluetoothLowEnergyStateChangedEventArgs>
|
||||
_stateChangedController;
|
||||
final StreamController<DiscoveredEventArgs> _discoveredController;
|
||||
final StreamController<ConnectionStateChangedEventArgs>
|
||||
final StreamController<PeripheralConnectionStateChangedEventArgs>
|
||||
_connectionStateChangedController;
|
||||
final StreamController<GattCharacteristicNotifiedEventArgs>
|
||||
final StreamController<GATTCharacteristicNotifiedEventArgs>
|
||||
_characteristicNotifiedController;
|
||||
final StreamController<BlueZDeviceServicesResolvedEventArgs>
|
||||
_blueZServicesResolvedController;
|
||||
|
||||
final Map<String, List<MyGATTService>> _services;
|
||||
final Map<int, StreamSubscription>
|
||||
_blueZCharacteristicPropertiesChangedSubscriptions;
|
||||
final Map<String, List<MyGattService2>> _services;
|
||||
|
||||
BluetoothLowEnergyState _state;
|
||||
bool _discovering;
|
||||
int _readCount;
|
||||
|
||||
MyCentralManager()
|
||||
: _blueZClient = BlueZClient(),
|
||||
@ -36,66 +41,92 @@ class MyCentralManager extends CentralManager {
|
||||
_connectionStateChangedController = StreamController.broadcast(),
|
||||
_characteristicNotifiedController = StreamController.broadcast(),
|
||||
_blueZServicesResolvedController = StreamController.broadcast(),
|
||||
_blueZCharacteristicPropertiesChangedSubscriptions = {},
|
||||
_services = {},
|
||||
_state = BluetoothLowEnergyState.unknown;
|
||||
_blueZCharacteristicPropertiesChangedSubscriptions = {},
|
||||
_state = BluetoothLowEnergyState.unknown,
|
||||
_discovering = false,
|
||||
_readCount = 0;
|
||||
|
||||
BlueZAdapter get _blueZAdapter => _blueZClient.adapters.first;
|
||||
|
||||
@override
|
||||
BluetoothLowEnergyState get state => _state;
|
||||
set state(BluetoothLowEnergyState value) {
|
||||
if (_state == value) {
|
||||
return;
|
||||
}
|
||||
logger.info('onStateChagned: $value');
|
||||
_state = value;
|
||||
final eventArgs = BluetoothLowEnergyStateChangedEventArgs(value);
|
||||
_stateChangedController.add(eventArgs);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<BluetoothLowEnergyStateChangedEventArgs> get stateChanged =>
|
||||
_stateChangedController.stream;
|
||||
@override
|
||||
Stream<DiscoveredEventArgs> get discovered => _discoveredController.stream;
|
||||
@override
|
||||
Stream<ConnectionStateChangedEventArgs> get connectionStateChanged =>
|
||||
_connectionStateChangedController.stream;
|
||||
Stream<PeripheralConnectionStateChangedEventArgs>
|
||||
get connectionStateChanged => _connectionStateChangedController.stream;
|
||||
@override
|
||||
Stream<GattCharacteristicNotifiedEventArgs> get characteristicNotified =>
|
||||
Stream<PeripheralMTUChangedEventArgs> get mtuChanged =>
|
||||
throw UnsupportedError('mtuChanged is not supported on Linux.');
|
||||
@override
|
||||
Stream<GATTCharacteristicNotifiedEventArgs> get characteristicNotified =>
|
||||
_characteristicNotifiedController.stream;
|
||||
Stream<BlueZDeviceServicesResolvedEventArgs> get _blueZServicesResolved =>
|
||||
_blueZServicesResolvedController.stream;
|
||||
|
||||
@override
|
||||
Future<void> setUp() async {
|
||||
logger.info('setUp');
|
||||
await _blueZClient.connect();
|
||||
if (_blueZClient.adapters.isEmpty) {
|
||||
_state = BluetoothLowEnergyState.unsupported;
|
||||
return;
|
||||
}
|
||||
_state = _blueZAdapter.myState;
|
||||
_blueZAdapter.propertiesChanged.listen(_onBlueZAdapterPropertiesChanged);
|
||||
for (var blueZDevice in _blueZClient.devices) {
|
||||
if (blueZDevice.adapter.address != _blueZAdapter.address) {
|
||||
continue;
|
||||
}
|
||||
_beginBlueZDevicePropertiesChangedListener(blueZDevice);
|
||||
}
|
||||
_blueZClient.deviceAdded.listen(_onBlueZClientDeviceAdded);
|
||||
void initialize() {
|
||||
_initialize();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BluetoothLowEnergyState> getState() {
|
||||
logger.info('getState');
|
||||
return Future.value(_state);
|
||||
Future<bool> authorize() {
|
||||
throw UnsupportedError('authorize is not supported on Linux.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startDiscovery() async {
|
||||
Future<void> showAppSettings() {
|
||||
throw UnsupportedError('showAppSettings is not supported on Linux.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startDiscovery({
|
||||
List<UUID>? serviceUUIDs,
|
||||
}) async {
|
||||
logger.info('startDiscovery');
|
||||
await _blueZAdapter.setDiscoveryFilter(
|
||||
uuids: serviceUUIDs?.map((uuid) => '$uuid').toList(),
|
||||
);
|
||||
await _blueZAdapter.startDiscovery();
|
||||
_discovering = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopDiscovery() async {
|
||||
logger.info('stopDiscovery');
|
||||
await _blueZAdapter.stopDiscovery();
|
||||
_discovering = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Peripheral>> retrieveConnectedPeripherals() {
|
||||
logger.info('retrieveConnectedPeripherals');
|
||||
final peripherals = _blueZClient.devices
|
||||
.where((blueZDevice) =>
|
||||
blueZDevice.adapter.address == _blueZAdapter.address &&
|
||||
blueZDevice.connected)
|
||||
.map((blueZDevice) => MyPeripheral(blueZDevice))
|
||||
.toList();
|
||||
return Future.value(peripherals);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> connect(Peripheral peripheral) async {
|
||||
if (peripheral is! MyPeripheral2) {
|
||||
if (peripheral is! MyPeripheral) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZDevice = peripheral.blueZDevice;
|
||||
@ -106,7 +137,7 @@ class MyCentralManager extends CentralManager {
|
||||
|
||||
@override
|
||||
Future<void> disconnect(Peripheral peripheral) async {
|
||||
if (peripheral is! MyPeripheral2) {
|
||||
if (peripheral is! MyPeripheral) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZDevice = peripheral.blueZDevice;
|
||||
@ -115,9 +146,40 @@ class MyCentralManager extends CentralManager {
|
||||
await blueZDevice.disconnect();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> requestMTU(
|
||||
Peripheral peripheral, {
|
||||
required int mtu,
|
||||
}) {
|
||||
throw UnsupportedError('requestMTU is not supported on Linux.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> getMaximumWriteLength(
|
||||
Peripheral peripheral, {
|
||||
required GATTCharacteristicWriteType type,
|
||||
}) {
|
||||
if (peripheral is! MyPeripheral) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZDevice = peripheral.blueZDevice;
|
||||
final blueZAddress = blueZDevice.address;
|
||||
logger.info('getMaximumWriteLength: $blueZAddress');
|
||||
final blueZMTU = blueZDevice.gattServices
|
||||
.firstWhere((service) => service.characteristics.isNotEmpty)
|
||||
.characteristics
|
||||
.first
|
||||
.mtu;
|
||||
if (blueZMTU == null) {
|
||||
throw ArgumentError.notNull();
|
||||
}
|
||||
final maximumWriteLength = (blueZMTU - 3).clamp(20, 512);
|
||||
return Future.value(maximumWriteLength);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> readRSSI(Peripheral peripheral) async {
|
||||
if (peripheral is! MyPeripheral2) {
|
||||
if (peripheral is! MyPeripheral) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZDevice = peripheral.blueZDevice;
|
||||
@ -127,8 +189,8 @@ class MyCentralManager extends CentralManager {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<GattService>> discoverGATT(Peripheral peripheral) async {
|
||||
if (peripheral is! MyPeripheral2) {
|
||||
Future<List<GATTService>> discoverGATT(Peripheral peripheral) async {
|
||||
if (peripheral is! MyPeripheral) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZDevice = peripheral.blueZDevice;
|
||||
@ -148,44 +210,47 @@ class MyCentralManager extends CentralManager {
|
||||
|
||||
@override
|
||||
Future<Uint8List> readCharacteristic(
|
||||
GattCharacteristic characteristic,
|
||||
Peripheral peripheral,
|
||||
GATTCharacteristic characteristic,
|
||||
) async {
|
||||
if (characteristic is! MyGattCharacteristic2) {
|
||||
if (characteristic is! MyGATTCharacteristic) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZCharacteristic = characteristic.blueZCharacteristic;
|
||||
final blueZUUID = blueZCharacteristic.uuid;
|
||||
logger.info('readCharacteristic: $blueZUUID');
|
||||
final blueZValue = await blueZCharacteristic.readValue();
|
||||
_readCount++;
|
||||
return Uint8List.fromList(blueZValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeCharacteristic(
|
||||
GattCharacteristic characteristic, {
|
||||
Peripheral peripheral,
|
||||
GATTCharacteristic characteristic, {
|
||||
required Uint8List value,
|
||||
required GattCharacteristicWriteType type,
|
||||
required GATTCharacteristicWriteType type,
|
||||
}) async {
|
||||
if (characteristic is! MyGattCharacteristic2) {
|
||||
if (characteristic is! MyGATTCharacteristic) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZCharacteristic = characteristic.blueZCharacteristic;
|
||||
final blueZUUID = blueZCharacteristic.uuid;
|
||||
final trimmedValue = value.trimGATT();
|
||||
final blueZType = type.toBlueZWriteType();
|
||||
logger.info('writeCharacteristic: $blueZUUID - $trimmedValue, $blueZType');
|
||||
logger.info('writeCharacteristic: $blueZUUID - $value, $blueZType');
|
||||
await blueZCharacteristic.writeValue(
|
||||
trimmedValue,
|
||||
value,
|
||||
type: blueZType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setCharacteristicNotifyState(
|
||||
GattCharacteristic characteristic, {
|
||||
Peripheral peripheral,
|
||||
GATTCharacteristic characteristic, {
|
||||
required bool state,
|
||||
}) async {
|
||||
if (characteristic is! MyGattCharacteristic2) {
|
||||
if (characteristic is! MyGATTCharacteristic) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZCharacteristic = characteristic.blueZCharacteristic;
|
||||
@ -200,8 +265,11 @@ class MyCentralManager extends CentralManager {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> readDescriptor(GattDescriptor descriptor) async {
|
||||
if (descriptor is! MyGattDescriptor2) {
|
||||
Future<Uint8List> readDescriptor(
|
||||
Peripheral peripheral,
|
||||
GATTDescriptor descriptor,
|
||||
) async {
|
||||
if (descriptor is! MyGATTDescriptor) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZDescriptor = descriptor.blueZDescriptor;
|
||||
@ -213,17 +281,17 @@ class MyCentralManager extends CentralManager {
|
||||
|
||||
@override
|
||||
Future<void> writeDescriptor(
|
||||
GattDescriptor descriptor, {
|
||||
Peripheral peripheral,
|
||||
GATTDescriptor descriptor, {
|
||||
required Uint8List value,
|
||||
}) async {
|
||||
if (descriptor is! MyGattDescriptor2) {
|
||||
if (descriptor is! MyGATTDescriptor) {
|
||||
throw TypeError();
|
||||
}
|
||||
final blueZDescriptor = descriptor.blueZDescriptor;
|
||||
final blueZUUID = blueZDescriptor.uuid;
|
||||
final trimmedValue = value.trimGATT();
|
||||
logger.info('writeDescriptor: $blueZUUID - $trimmedValue');
|
||||
await blueZDescriptor.writeValue(trimmedValue);
|
||||
logger.info('writeDescriptor: $blueZUUID - $value');
|
||||
await blueZDescriptor.writeValue(value);
|
||||
}
|
||||
|
||||
void _onBlueZAdapterPropertiesChanged(List<String> blueZAdapterProperties) {
|
||||
@ -231,13 +299,7 @@ class MyCentralManager extends CentralManager {
|
||||
for (var blueZAdapterProperty in blueZAdapterProperties) {
|
||||
switch (blueZAdapterProperty) {
|
||||
case 'Powered':
|
||||
final state = _blueZAdapter.myState;
|
||||
if (_state == state) {
|
||||
return;
|
||||
}
|
||||
_state = state;
|
||||
final eventArgs = BluetoothLowEnergyStateChangedEventArgs(state);
|
||||
_stateChangedController.add(eventArgs);
|
||||
state = _blueZAdapter.myState;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -250,12 +312,14 @@ class MyCentralManager extends CentralManager {
|
||||
if (blueZDevice.adapter.address != _blueZAdapter.address) {
|
||||
return;
|
||||
}
|
||||
_onBlueZDiscovered(blueZDevice);
|
||||
if (_discovering) {
|
||||
_onBlueZDiscovered(blueZDevice);
|
||||
}
|
||||
_beginBlueZDevicePropertiesChangedListener(blueZDevice);
|
||||
}
|
||||
|
||||
void _onBlueZDiscovered(BlueZDevice blueZDevice) {
|
||||
final peripheral = MyPeripheral2(blueZDevice);
|
||||
final peripheral = MyPeripheral(blueZDevice);
|
||||
final rssi = blueZDevice.rssi;
|
||||
final advertisement = blueZDevice.myAdvertisement;
|
||||
final eventArgs = DiscoveredEventArgs(
|
||||
@ -273,19 +337,24 @@ class MyCentralManager extends CentralManager {
|
||||
for (var blueZDeviceProperty in blueZDeviceProperties) {
|
||||
switch (blueZDeviceProperty) {
|
||||
case 'RSSI':
|
||||
_onBlueZDiscovered(blueZDevice);
|
||||
if (_discovering) {
|
||||
_onBlueZDiscovered(blueZDevice);
|
||||
}
|
||||
break;
|
||||
case 'Connected':
|
||||
final peripheral = MyPeripheral2(blueZDevice);
|
||||
final state = blueZDevice.connected;
|
||||
final eventArgs = ConnectionStateChangedEventArgs(
|
||||
final connected = blueZDevice.connected;
|
||||
if (!connected) {
|
||||
_endBlueZCharacteristicPropertiesChangedListener(blueZDevice);
|
||||
}
|
||||
final peripheral = MyPeripheral(blueZDevice);
|
||||
final state = blueZDevice.connected
|
||||
? ConnectionState.connected
|
||||
: ConnectionState.disconnected;
|
||||
final eventArgs = PeripheralConnectionStateChangedEventArgs(
|
||||
peripheral,
|
||||
state,
|
||||
);
|
||||
_connectionStateChangedController.add(eventArgs);
|
||||
if (!state) {
|
||||
_endBlueZCharacteristicPropertiesChangedListener(blueZDevice);
|
||||
}
|
||||
break;
|
||||
case 'UUIDs':
|
||||
break;
|
||||
@ -313,6 +382,7 @@ class MyCentralManager extends CentralManager {
|
||||
if (services == null) {
|
||||
return;
|
||||
}
|
||||
final peripheral = MyPeripheral(blueZDevice);
|
||||
for (var service in services) {
|
||||
final characteristics = service.characteristics;
|
||||
for (var characteristic in characteristics) {
|
||||
@ -325,8 +395,13 @@ class MyCentralManager extends CentralManager {
|
||||
in blueZCharacteristicProperties) {
|
||||
switch (blueZCharacteristicPropety) {
|
||||
case 'Value':
|
||||
if (_readCount > 0) {
|
||||
_readCount--;
|
||||
return;
|
||||
}
|
||||
final value = Uint8List.fromList(blueZCharacteristic.value);
|
||||
final eventArgs = GattCharacteristicNotifiedEventArgs(
|
||||
final eventArgs = GATTCharacteristicNotifiedEventArgs(
|
||||
peripheral,
|
||||
characteristic,
|
||||
value,
|
||||
);
|
||||
@ -361,4 +436,30 @@ class MyCentralManager extends CentralManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
// Here we use `Future()` to make it possible to change the `logLevel` before `initialize()`.
|
||||
await Future(() async {
|
||||
try {
|
||||
logger.info('initialize');
|
||||
await _blueZClient.connect();
|
||||
if (_blueZClient.adapters.isEmpty) {
|
||||
state = BluetoothLowEnergyState.unsupported;
|
||||
} else {
|
||||
state = _blueZAdapter.myState;
|
||||
_blueZAdapter.propertiesChanged
|
||||
.listen(_onBlueZAdapterPropertiesChanged);
|
||||
for (var blueZDevice in _blueZClient.devices) {
|
||||
if (blueZDevice.adapter.address != _blueZAdapter.address) {
|
||||
continue;
|
||||
}
|
||||
_beginBlueZDevicePropertiesChangedListener(blueZDevice);
|
||||
}
|
||||
_blueZClient.deviceAdded.listen(_onBlueZClientDeviceAdded);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.severe('initialize failed.', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluez/bluez.dart';
|
||||
|
||||
class BlueZDeviceServicesResolvedEventArgs extends EventArgs {
|
||||
final BlueZDevice device;
|
||||
|
||||
BlueZDeviceServicesResolvedEventArgs(this.device);
|
||||
}
|
74
bluetooth_low_energy_linux/lib/src/my_gatt.dart
Normal file
74
bluetooth_low_energy_linux/lib/src/my_gatt.dart
Normal file
@ -0,0 +1,74 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluez/bluez.dart';
|
||||
|
||||
import 'my_bluez.dart';
|
||||
|
||||
final class MyGATTDescriptor extends GATTDescriptor {
|
||||
final BlueZGattDescriptor blueZDescriptor;
|
||||
|
||||
MyGATTDescriptor(this.blueZDescriptor)
|
||||
: super(
|
||||
uuid: blueZDescriptor.myUUID,
|
||||
);
|
||||
|
||||
@override
|
||||
int get hashCode => blueZDescriptor.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MyGATTDescriptor &&
|
||||
other.blueZDescriptor == blueZDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
final class MyGATTCharacteristic extends GATTCharacteristic {
|
||||
final BlueZGattCharacteristic blueZCharacteristic;
|
||||
|
||||
MyGATTCharacteristic(this.blueZCharacteristic)
|
||||
: super(
|
||||
uuid: blueZCharacteristic.myUUID,
|
||||
properties: blueZCharacteristic.myProperties,
|
||||
descriptors: blueZCharacteristic.myDescriptors,
|
||||
);
|
||||
|
||||
@override
|
||||
List<MyGATTDescriptor> get descriptors =>
|
||||
super.descriptors.cast<MyGATTDescriptor>();
|
||||
|
||||
@override
|
||||
int get hashCode => blueZCharacteristic.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MyGATTCharacteristic &&
|
||||
other.blueZCharacteristic == blueZCharacteristic;
|
||||
}
|
||||
}
|
||||
|
||||
final class MyGATTService extends GATTService {
|
||||
final BlueZGattService blueZService;
|
||||
|
||||
MyGATTService(this.blueZService)
|
||||
: super(
|
||||
uuid: blueZService.myUUID,
|
||||
isPrimary: blueZService.primary,
|
||||
includedServices: [],
|
||||
characteristics: blueZService.myCharacteristics,
|
||||
);
|
||||
|
||||
@override
|
||||
List<MyGATTService> get includedServices =>
|
||||
throw UnsupportedError('includedServices is not supported on Linux.');
|
||||
|
||||
@override
|
||||
List<MyGATTCharacteristic> get characteristics =>
|
||||
super.characteristics.cast<MyGATTCharacteristic>();
|
||||
|
||||
@override
|
||||
int get hashCode => blueZService.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MyGATTService && other.blueZService == blueZService;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluez/bluez.dart';
|
||||
|
||||
import 'my_bluez.dart';
|
||||
import 'my_gatt_descriptor2.dart';
|
||||
|
||||
class MyGattCharacteristic2 extends MyGattCharacteristic {
|
||||
final BlueZGattCharacteristic blueZCharacteristic;
|
||||
|
||||
MyGattCharacteristic2(this.blueZCharacteristic)
|
||||
: super(
|
||||
uuid: blueZCharacteristic.myUUID,
|
||||
properties: blueZCharacteristic.myProperties,
|
||||
descriptors: blueZCharacteristic.myDescriptors,
|
||||
);
|
||||
|
||||
@override
|
||||
List<MyGattDescriptor2> get descriptors =>
|
||||
super.descriptors.cast<MyGattDescriptor2>();
|
||||
|
||||
@override
|
||||
int get hashCode => blueZCharacteristic.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MyGattCharacteristic2 &&
|
||||
other.blueZCharacteristic == blueZCharacteristic;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluez/bluez.dart';
|
||||
|
||||
import 'my_bluez.dart';
|
||||
|
||||
class MyGattDescriptor2 extends MyGattDescriptor {
|
||||
final BlueZGattDescriptor blueZDescriptor;
|
||||
|
||||
MyGattDescriptor2(this.blueZDescriptor)
|
||||
: super(
|
||||
uuid: blueZDescriptor.myUUID,
|
||||
);
|
||||
|
||||
@override
|
||||
int get hashCode => blueZDescriptor.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MyGattDescriptor2 &&
|
||||
other.blueZDescriptor == blueZDescriptor;
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import 'package:bluetooth_low_energy_platform_interface/bluetooth_low_energy_platform_interface.dart';
|
||||
import 'package:bluez/bluez.dart';
|
||||
|
||||
import 'my_bluez.dart';
|
||||
import 'my_gatt_characteristic2.dart';
|
||||
|
||||
class MyGattService2 extends MyGattService {
|
||||
final BlueZGattService blueZService;
|
||||
|
||||
MyGattService2(this.blueZService)
|
||||
: super(
|
||||
uuid: blueZService.myUUID,
|
||||
characteristics: blueZService.myCharacteristics,
|
||||
);
|
||||
|
||||
@override
|
||||
List<MyGattCharacteristic2> get characteristics =>
|
||||
super.characteristics.cast<MyGattCharacteristic2>();
|
||||
|
||||
@override
|
||||
int get hashCode => blueZService.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MyGattService2 && other.blueZService == blueZService;
|
||||
}
|
||||
}
|
@ -3,10 +3,10 @@ import 'package:bluez/bluez.dart';
|
||||
|
||||
import 'my_bluez.dart';
|
||||
|
||||
class MyPeripheral2 extends MyPeripheral {
|
||||
final class MyPeripheral extends Peripheral {
|
||||
final BlueZDevice blueZDevice;
|
||||
|
||||
MyPeripheral2(this.blueZDevice)
|
||||
MyPeripheral(this.blueZDevice)
|
||||
: super(
|
||||
uuid: blueZDevice.myUUID,
|
||||
);
|
@ -1,26 +1,35 @@
|
||||
name: bluetooth_low_energy_linux
|
||||
description: Linux implementation of the bluetooth_low_energy plugin.
|
||||
version: 5.0.2
|
||||
description: "Linux implementation of the bluetooth_low_energy plugin."
|
||||
version: 6.0.0
|
||||
homepage: https://github.com/yanshouwang/bluetooth_low_energy
|
||||
repository: https://github.com/yanshouwang/bluetooth_low_energy
|
||||
issue_tracker: https://github.com/yanshouwang/bluetooth_low_energy/issues
|
||||
topics:
|
||||
- bluetooth
|
||||
- bluetooth-low-energy
|
||||
- ble
|
||||
funding:
|
||||
- https://paypal.me/yanshouwang5112
|
||||
- https://afdian.net/a/yanshouwang
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=3.0.0"
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
flutter: '>=3.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
bluetooth_low_energy_platform_interface: ^5.0.2
|
||||
bluez: ^0.8.1
|
||||
bluetooth_low_energy_platform_interface: ^6.0.0
|
||||
bluez: ^0.8.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^3.0.0
|
||||
flutter_lints: ^4.0.0
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
implements: bluetooth_low_energy
|
||||
platforms:
|
||||
linux:
|
||||
dartPluginClass: BluetoothLowEnergyLinux
|
||||
dartPluginClass: BluetoothLowEnergyLinuxPlugin
|
||||
|
@ -0,0 +1 @@
|
||||
void main() {}
|
Reference in New Issue
Block a user