Implement scan filter in example app (#61)
* Implement scan filter in example app * Remove Fixed FlutterVersion from Workflow and improve ScanFilterWidget * Update example/lib/home/widgets/scan_filter_widget.dart * Update example/lib/home/widgets/scan_filter_widget.dart * Update example/lib/peripheral_details/peripheral_detail_page.dart * Improve text --------- Co-authored-by: Foti Dim <foti@navideck.com> Co-authored-by: Foti Dim <fdimanidis@gmail.com>
This commit is contained in:
@@ -14,7 +14,6 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
channel: 'stable'
|
||||
- run: flutter --version
|
||||
|
||||
|
||||
@@ -42,3 +42,4 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
.metadata
|
||||
@@ -1,30 +0,0 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "ead455963c12b453cdb2358cad34969c76daf180"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: ead455963c12b453cdb2358cad34969c76daf180
|
||||
base_revision: ead455963c12b453cdb2358cad34969c76daf180
|
||||
- platform: web
|
||||
create_revision: ead455963c12b453cdb2358cad34969c76daf180
|
||||
base_revision: ead455963c12b453cdb2358cad34969c76daf180
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
+30
-23
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/data/capabilities.dart';
|
||||
import 'package:universal_ble_example/data/mock_universal_ble.dart';
|
||||
import 'package:universal_ble_example/home/widgets/scan_filter_widget.dart';
|
||||
import 'package:universal_ble_example/home/widgets/scanned_devices_placeholder_widget.dart';
|
||||
import 'package:universal_ble_example/home/widgets/scanned_item_widget.dart';
|
||||
import 'package:universal_ble_example/data/permission_handler.dart';
|
||||
@@ -23,17 +24,12 @@ class _MyAppState extends State<MyApp> {
|
||||
final _bleDevices = <BleDevice>[];
|
||||
bool _isScanning = false;
|
||||
QueueType _queueType = QueueType.global;
|
||||
TextEditingController servicesFilterController = TextEditingController();
|
||||
TextEditingController namePrefixController = TextEditingController();
|
||||
TextEditingController manufacturerDataController = TextEditingController();
|
||||
|
||||
AvailabilityState? bleAvailabilityState;
|
||||
final List<String> _services = [
|
||||
"00001800-0000-1000-8000-00805f9b34fb",
|
||||
"0000180f-0000-1000-8000-00805f9b34fb",
|
||||
"00002a00-0000-1000-8000-00805f9b34fb",
|
||||
"00002a01-0000-1000-8000-00805f9b34fb",
|
||||
"00002a19-0000-1000-8000-00805f9b34fb",
|
||||
"8000cc00-cc00-ffff-ffff-ffffffffffff",
|
||||
"8000dd00-dd00-ffff-ffff-ffffffffffff",
|
||||
];
|
||||
ScanFilter? scanFilter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -76,17 +72,26 @@ class _MyAppState extends State<MyApp> {
|
||||
|
||||
Future<void> startScan() async {
|
||||
await UniversalBle.startScan(
|
||||
scanFilter: ScanFilter(
|
||||
withServices: kIsWeb ? _services : [],
|
||||
withManufacturerData: [
|
||||
// ManufacturerDataFilter(
|
||||
// companyIdentifier: 0x012D,
|
||||
// data: Uint8List.fromList(
|
||||
// [0x03, 0x00, 0x64, 0x00],
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
scanFilter: scanFilter,
|
||||
);
|
||||
}
|
||||
|
||||
void _showScanFilterBottomSheet() {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ScanFilterWidget(
|
||||
servicesFilterController: servicesFilterController,
|
||||
namePrefixController: namePrefixController,
|
||||
manufacturerDataController: manufacturerDataController,
|
||||
onScanFilter: (ScanFilter? filter) {
|
||||
setState(() {
|
||||
scanFilter = filter;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,9 +177,7 @@ class _MyAppState extends State<MyApp> {
|
||||
text: 'Connected Devices',
|
||||
onPressed: () async {
|
||||
List<BleDevice> devices =
|
||||
await UniversalBle.getSystemDevices(
|
||||
withServices: _services,
|
||||
);
|
||||
await UniversalBle.getSystemDevices();
|
||||
if (devices.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
@@ -201,6 +204,10 @@ class _MyAppState extends State<MyApp> {
|
||||
});
|
||||
},
|
||||
),
|
||||
PlatformButton(
|
||||
text: 'Scan Filters',
|
||||
onPressed: _showScanFilterBottomSheet,
|
||||
),
|
||||
if (_bleDevices.isNotEmpty)
|
||||
PlatformButton(
|
||||
text: 'Clear List',
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/widgets/platform_button.dart';
|
||||
|
||||
class ScanFilterWidget extends StatefulWidget {
|
||||
final Function(ScanFilter? filter) onScanFilter;
|
||||
final TextEditingController servicesFilterController;
|
||||
final TextEditingController namePrefixController;
|
||||
final TextEditingController manufacturerDataController;
|
||||
|
||||
const ScanFilterWidget({
|
||||
super.key,
|
||||
required this.onScanFilter,
|
||||
required this.servicesFilterController,
|
||||
required this.namePrefixController,
|
||||
required this.manufacturerDataController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ScanFilterWidget> createState() => _ScanFilterWidgetState();
|
||||
}
|
||||
|
||||
class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
String? error;
|
||||
|
||||
void applyFilter() {
|
||||
setState(() {
|
||||
error = null;
|
||||
});
|
||||
try {
|
||||
List<String> serviceUUids = [];
|
||||
List<String> namePrefixes = [];
|
||||
List<ManufacturerDataFilter> manufacturerDataFilters = [];
|
||||
|
||||
// Parse Services
|
||||
if (widget.servicesFilterController.text.isNotEmpty) {
|
||||
List<String> services = widget.servicesFilterController.text.split(',');
|
||||
for (String service in services) {
|
||||
try {
|
||||
serviceUUids.add(BleUuid.parse(service.trim()));
|
||||
} on FormatException catch (_) {
|
||||
throw Exception("Invalid Service UUID $service");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Name Prefix
|
||||
String namePrefix = widget.namePrefixController.text;
|
||||
if (namePrefix.isNotEmpty) {
|
||||
namePrefixes = namePrefix.split(',').map((e) => e.trim()).toList();
|
||||
}
|
||||
|
||||
// Parse Manufacturer Data
|
||||
String manufacturerDataText = widget.manufacturerDataController.text;
|
||||
if (manufacturerDataText.isNotEmpty) {
|
||||
List<String> manufacturerData = manufacturerDataText.split(',');
|
||||
for (String manufacturer in manufacturerData) {
|
||||
int? companyIdentifier = int.tryParse(manufacturer);
|
||||
if (companyIdentifier == null) {
|
||||
throw Exception("Invalid Manufacturer Data $manufacturer");
|
||||
}
|
||||
manufacturerDataFilters.add(
|
||||
ManufacturerDataFilter(companyIdentifier: companyIdentifier),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceUUids.isEmpty &&
|
||||
namePrefixes.isEmpty &&
|
||||
manufacturerDataFilters.isEmpty) {
|
||||
widget.onScanFilter(null);
|
||||
} else {
|
||||
widget.onScanFilter(
|
||||
ScanFilter(
|
||||
withServices: serviceUUids,
|
||||
withNamePrefix: namePrefixes,
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Filters Applied")),
|
||||
);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void clearFilter() {
|
||||
widget.servicesFilterController.clear();
|
||||
widget.namePrefixController.clear();
|
||||
widget.manufacturerDataController.clear();
|
||||
widget.onScanFilter(null);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: MediaQuery.of(context).viewInsets.copyWith(left: 20, right: 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: Text(
|
||||
"Scan Filters",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
const Text("Use comma to add multiple values"),
|
||||
const Divider(),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: widget.servicesFilterController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Services",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: widget.namePrefixController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Name Prefixes",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: widget.manufacturerDataController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Manufacturer Data Company IDs",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: PlatformButton(
|
||||
text: 'Apply',
|
||||
onPressed: applyFilter,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: PlatformButton(
|
||||
text: 'Clear',
|
||||
onPressed: clearFilter,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (error != null)
|
||||
Text(
|
||||
error!,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
@@ -30,7 +29,6 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
|
||||
bool isConnected = false;
|
||||
GlobalKey<FormState> valueFormKey = GlobalKey<FormState>();
|
||||
List<BleService> discoveredServices = [];
|
||||
ExpandableController expandableController = ExpandableController();
|
||||
final List<String> _logs = [];
|
||||
final binaryCode = TextEditingController();
|
||||
|
||||
@@ -104,7 +102,7 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
|
||||
|
||||
if (kIsWeb) {
|
||||
_addLog("DiscoverServices",
|
||||
'${services.length} services discovered,\nNote: Only services added in WebRequestOptionsBuilder will be discoverd');
|
||||
'${services.length} services discovered,\nNote: Only services added in ScanFilter will be discovered');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-10
@@ -134,14 +134,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_adaptive_scaffold:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_adaptive_scaffold
|
||||
sha256: "794791e6fc0cc23e375d07ea987e76cf7f0eb7cd753532e45dc35b7e90575e2d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.11"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -410,7 +402,7 @@ packages:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.10.0"
|
||||
version: "0.11.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -461,4 +453,4 @@ packages:
|
||||
version: "6.5.0"
|
||||
sdks:
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
||||
@@ -9,12 +9,11 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
convert: ^3.1.1
|
||||
expandable: ^5.0.1
|
||||
cupertino_icons: ^1.0.2
|
||||
permission_handler: ^10.4.3
|
||||
device_info_plus: ^9.0.2
|
||||
flutter_adaptive_scaffold: ^0.1.7+2
|
||||
convert: ^3.1.1
|
||||
universal_ble:
|
||||
path: ../
|
||||
|
||||
|
||||
+1
-22
@@ -31,29 +31,8 @@
|
||||
|
||||
<title>example</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<script>
|
||||
// The value below is injected by flutter build, do not touch.
|
||||
const serviceWorkerVersion = null;
|
||||
</script>
|
||||
<!-- This script adds the flutter initialization JS code -->
|
||||
<script src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.addEventListener('load', function(ev) {
|
||||
// Download main.dart.js
|
||||
_flutter.loader.loadEntrypoint({
|
||||
serviceWorker: {
|
||||
serviceWorkerVersion: serviceWorkerVersion,
|
||||
},
|
||||
onEntrypointLoaded: function(engineInitializer) {
|
||||
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||
appRunner.runApp();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user