diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt
index 0faaae2..59558d9 100644
--- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt
+++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt
@@ -270,7 +270,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
UniversalBleLogger.logError("Device might be connected but not known to this app")
BleConnectionState.Disconnected.value
}
- } catch (e: Exception) {
+ } catch (_: Exception) {
return BleConnectionState.Disconnected.value
}
}
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 500663a..539e152 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -23,7 +23,7 @@ if (flutterVersionName == null) {
}
android {
- namespace "com.navideck.universal_ble_example"
+ namespace = "com.navideck.universalble"
compileSdkVersion 36
ndkVersion flutter.ndkVersion
@@ -41,7 +41,7 @@ android {
}
defaultConfig {
- applicationId "com.navideck.universal_ble_example"
+ applicationId = "com.navideck.universalble"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 191b9de..30fcaa8 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -6,7 +6,7 @@
CFBundleInfoDictionaryVersion
6.0
CFBundleName
- universal_ble_example
+ Universal BLE
CFBundlePackageType
APPL
CFBundleShortVersionString
diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart
index ffbeb7e..9539f6a 100644
--- a/example/lib/data/mock_universal_ble.dart
+++ b/example/lib/data/mock_universal_ble.dart
@@ -5,6 +5,7 @@ import 'package:universal_ble/universal_ble.dart';
/// Mock implementation of [UniversalBlePlatform] for testing
class MockUniversalBle extends UniversalBlePlatform {
+ final Map _connectionStateMap = {};
final _mockBleDevice = BleDevice(
name: 'MockDevice',
deviceId: 'MockDeviceId',
@@ -15,12 +16,20 @@ class MockUniversalBle extends UniversalBlePlatform {
Uint8List _serviceValue = utf8.encode('Result');
bool _isScanning = false;
- final BleService _mockService = BleService('180', [
- BleCharacteristic('180A', [
- CharacteristicProperty.read,
- CharacteristicProperty.write,
- CharacteristicProperty.notify,
- ], []),
+ final BleService _mockService = BleService('180a', [
+ BleCharacteristic.withMetaData(
+ deviceId: 'MockDeviceId',
+ serviceId: '180a',
+ uuid: '220a',
+ properties: [
+ CharacteristicProperty.read,
+ CharacteristicProperty.write,
+ CharacteristicProperty.notify,
+ ],
+ descriptors: [
+ BleDescriptor('220b'),
+ ],
+ ),
]);
@override
@@ -45,11 +54,13 @@ class MockUniversalBle extends UniversalBlePlatform {
@override
Future connect(String deviceId, {Duration? connectionTimeout}) async {
updateConnection(deviceId, true);
+ _connectionStateMap[deviceId] = BleConnectionState.connected;
}
@override
Future disconnect(String deviceId) async {
updateConnection(deviceId, false);
+ _connectionStateMap[deviceId] = BleConnectionState.disconnected;
}
@override
@@ -71,7 +82,7 @@ class MockUniversalBle extends UniversalBlePlatform {
@override
Future> getSystemDevices(List? withServices) async {
- return [];
+ return [_mockBleDevice];
}
@override
@@ -124,17 +135,18 @@ class MockUniversalBle extends UniversalBlePlatform {
}
@override
- Future getConnectionState(String deviceId) {
- throw UnimplementedError();
+ Future getConnectionState(String deviceId) async {
+ return _connectionStateMap[deviceId] ?? BleConnectionState.disconnected;
}
@override
- Future disableBluetooth() {
- throw UnimplementedError();
+ Future disableBluetooth() async {
+ return true;
}
@override
- Future requestPermissions({bool withAndroidFineLocation = false}) {
- throw UnimplementedError();
+ Future requestPermissions(
+ {bool withAndroidFineLocation = false}) async {
+ return;
}
}
diff --git a/example/lib/data/storage_service.dart b/example/lib/data/storage_service.dart
new file mode 100644
index 0000000..c52f3a3
--- /dev/null
+++ b/example/lib/data/storage_service.dart
@@ -0,0 +1,22 @@
+import 'package:shared_preferences/shared_preferences.dart';
+
+class StorageService {
+ StorageService._();
+ static StorageService? _instance;
+ static StorageService get instance => _instance ??= StorageService._();
+
+ late SharedPreferencesWithCache _preferences;
+
+ Future init() async {
+ _preferences = await SharedPreferencesWithCache.create(
+ cacheOptions: const SharedPreferencesWithCacheOptions(),
+ );
+ }
+
+ Future setFavoriteServices(List services) async {
+ await _preferences.setStringList('favorite_services', services);
+ }
+
+ List getFavoriteServices() =>
+ _preferences.getStringList('favorite_services') ?? [];
+}
diff --git a/example/lib/data/utils.dart b/example/lib/data/utils.dart
new file mode 100644
index 0000000..1900dad
--- /dev/null
+++ b/example/lib/data/utils.dart
@@ -0,0 +1,7 @@
+bool isSystemService(String uuid) {
+ final normalized = uuid.toUpperCase().replaceAll('-', '');
+ return normalized == '00001800' ||
+ normalized == '00001801' ||
+ normalized == '0000180A' ||
+ normalized.startsWith('000018');
+}
diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart
deleted file mode 100644
index 0ab9c57..0000000
--- a/example/lib/home/home.dart
+++ /dev/null
@@ -1,356 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:universal_ble/universal_ble.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/peripheral_details/peripheral_detail_page.dart';
-import 'package:universal_ble_example/widgets/platform_button.dart';
-import 'package:universal_ble_example/widgets/responsive_buttons_grid.dart';
-
-class MyApp extends StatefulWidget {
- const MyApp({super.key});
-
- @override
- State createState() => _MyAppState();
-}
-
-class _MyAppState extends State {
- final _bleDevices = [];
- final _hiddenDevices = [];
- bool _isScanning = false;
- QueueType _queueType = QueueType.global;
- TextEditingController servicesFilterController = TextEditingController();
- TextEditingController namePrefixController = TextEditingController();
- TextEditingController manufacturerDataController = TextEditingController();
- StreamSubscription? _availabilityStreamSubscription;
-
- bool get isTrackingAvailabilityState =>
- _availabilityStreamSubscription != null;
- AvailabilityState? bleAvailabilityState;
- ScanFilter? scanFilter;
-
- @override
- void initState() {
- super.initState();
-
- /// Set mock instance for testing
- if (const bool.fromEnvironment('MOCK')) {
- UniversalBle.setInstance(MockUniversalBle());
- }
-
- /// Setup queue and timeout
- UniversalBle.queueType = _queueType;
- UniversalBle.timeout = const Duration(seconds: 10);
-
- UniversalBle.scanStream.listen((result) {
- // log(result.toString());
- // If device is already in hidden devices, skip
- if (_hiddenDevices.any((e) => e.deviceId == result.deviceId)) {
- // debugPrint("Skipping hidden device: ${result.deviceId}");
- return;
- }
- int index = _bleDevices.indexWhere((e) => e.deviceId == result.deviceId);
- if (index == -1) {
- _bleDevices.add(result);
- } else {
- if (result.name == null && _bleDevices[index].name != null) {
- result.name = _bleDevices[index].name;
- }
- _bleDevices[index] = result;
- }
- setState(() {});
- });
-
- // UniversalBle.onQueueUpdate = (String id, int remainingItems) {
- // debugPrint("Queue: $id RemainingItems: $remainingItems");
- // };
-
- UniversalBle.isScanning().then((value) {
- debugPrint("Is Scanning: $value");
- setState(() {
- _isScanning = value;
- });
- });
- }
-
- void trackAvailabilityState() {
- _availabilityStreamSubscription = UniversalBle.availabilityStream.listen(
- (state) {
- setState(() {
- bleAvailabilityState = state;
- });
- },
- );
- setState(() {});
- }
-
- Future startScan() async {
- await UniversalBle.startScan(scanFilter: scanFilter);
- }
-
- Future _getSystemDevices() async {
- // For macOS and iOS, it is recommended to set a filter to get system devices
- if ((defaultTargetPlatform == TargetPlatform.macOS ||
- defaultTargetPlatform == TargetPlatform.iOS) &&
- (scanFilter?.withServices ?? []).isEmpty) {
- showSnackbar(
- "No services filter was set for getting system connected devices. Using default services...");
- }
-
- List devices = await UniversalBle.getSystemDevices(
- withServices: scanFilter?.withServices,
- );
- if (devices.isEmpty) {
- showSnackbar("No System Connected Devices Found");
- }
- setState(() {
- _bleDevices.clear();
- _bleDevices.addAll(devices);
- });
- }
-
- void _showScanFilterBottomSheet() {
- showModalBottomSheet(
- isScrollControlled: true,
- context: context,
- builder: (context) {
- return ScanFilterWidget(
- servicesFilterController: servicesFilterController,
- namePrefixController: namePrefixController,
- manufacturerDataController: manufacturerDataController,
- onScanFilter: (ScanFilter? filter) {
- setState(() {
- scanFilter = filter;
- });
- },
- );
- },
- );
- }
-
- void showSnackbar(String message) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(message)),
- );
- }
-
- @override
- void dispose() {
- _availabilityStreamSubscription?.cancel();
- servicesFilterController.dispose();
- namePrefixController.dispose();
- manufacturerDataController.dispose();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- title: const Text('Universal BLE'),
- elevation: 4,
- actions: [
- if (_isScanning)
- const Padding(
- padding: EdgeInsets.all(8.0),
- child: SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator.adaptive(
- strokeWidth: 2,
- )),
- ),
- ],
- ),
- body: Column(
- children: [
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: ResponsiveButtonsGrid(
- children: [
- PlatformButton(
- text: 'Start Scan',
- onPressed: () async {
- setState(() {
- _bleDevices.clear();
- _isScanning = true;
- });
- try {
- await startScan();
- } catch (e) {
- setState(() {
- _isScanning = false;
- });
- showSnackbar(e.toString());
- }
- },
- ),
- PlatformButton(
- text: 'Stop Scan',
- onPressed: () async {
- await UniversalBle.stopScan();
- setState(() {
- _isScanning = false;
- });
- },
- ),
- if (BleCapabilities.supportsBluetoothEnableApi)
- bleAvailabilityState != AvailabilityState.poweredOn
- ? PlatformButton(
- text: 'Enable Bluetooth',
- onPressed: () async {
- bool isEnabled =
- await UniversalBle.enableBluetooth();
- showSnackbar("BluetoothEnabled: $isEnabled");
- },
- )
- : PlatformButton(
- text: 'Disable Bluetooth',
- onPressed: () async {
- bool isDisabled =
- await UniversalBle.disableBluetooth();
- showSnackbar("BluetoothDisabled: $isDisabled");
- },
- ),
- if (BleCapabilities.requiresRuntimePermission) ...[
- PlatformButton(
- text: 'Has Permissions',
- onPressed: () async {
- try {
- bool hasPermissions = await UniversalBle.hasPermissions(
- withAndroidFineLocation: false,
- );
- showSnackbar("Has Permissions: $hasPermissions");
- } catch (e) {
- showSnackbar(e.toString());
- }
- },
- ),
- PlatformButton(
- text: 'Request Permissions',
- onPressed: () async {
- try {
- await UniversalBle.requestPermissions(
- withAndroidFineLocation: false,
- );
- showSnackbar("Permissions granted");
- } catch (e) {
- showSnackbar(e.toString());
- }
- },
- ),
- ],
- if (!isTrackingAvailabilityState)
- PlatformButton(
- text: 'Track Availability State',
- onPressed: trackAvailabilityState,
- ),
- if (BleCapabilities.supportsConnectedDevicesApi)
- PlatformButton(
- text: 'System Devices',
- onPressed: _getSystemDevices,
- ),
- PlatformButton(
- text: 'Queue: ${_queueType.name}',
- onPressed: () {
- setState(() {
- _queueType = switch (_queueType) {
- QueueType.global => QueueType.perDevice,
- QueueType.perDevice => QueueType.none,
- QueueType.none => QueueType.global,
- };
- UniversalBle.queueType = _queueType;
- });
- },
- ),
- PlatformButton(
- text: 'Scan Filters',
- onPressed: _showScanFilterBottomSheet,
- ),
- if (_hiddenDevices.isNotEmpty)
- PlatformButton(
- text: 'Unhide ${_hiddenDevices.length} Devices',
- onPressed: () {
- setState(() {
- _hiddenDevices.clear();
- });
- },
- )
- else if (_bleDevices.isNotEmpty)
- Tooltip(
- message:
- 'Hide already discovered devices. When you turn on a new device, it will be easier to spot.',
- child: PlatformButton(
- text: 'Hide Already Discovered Devices',
- onPressed: () {
- setState(() {
- _hiddenDevices.clear();
- _hiddenDevices.addAll(_bleDevices);
- _bleDevices.clear();
- });
- },
- ),
- ),
- if (_bleDevices.isNotEmpty)
- PlatformButton(
- text: 'Clear List',
- onPressed: () {
- setState(() {
- _bleDevices.clear();
- });
- },
- ),
- ],
- ),
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- if (isTrackingAvailabilityState)
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(
- 'Ble Availability : ${bleAvailabilityState?.name}',
- ),
- ),
- ],
- ),
- const Divider(color: Colors.blue),
- Expanded(
- child: _isScanning && _bleDevices.isEmpty
- ? const Center(child: CircularProgressIndicator.adaptive())
- : !_isScanning && _bleDevices.isEmpty
- ? const ScannedDevicesPlaceholderWidget()
- : ListView.separated(
- itemCount: _bleDevices.length,
- separatorBuilder: (context, index) => const Divider(),
- itemBuilder: (context, index) {
- BleDevice device =
- _bleDevices[_bleDevices.length - index - 1];
- return ScannedItemWidget(
- bleDevice: device,
- onTap: () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (_) => PeripheralDetailPage(device),
- ),
- );
- UniversalBle.stopScan();
- setState(() {
- _isScanning = false;
- });
- },
- );
- },
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/example/lib/home/permission_screen.dart b/example/lib/home/permission_screen.dart
new file mode 100644
index 0000000..8214cca
--- /dev/null
+++ b/example/lib/home/permission_screen.dart
@@ -0,0 +1,242 @@
+import 'package:flutter/material.dart';
+import 'package:universal_ble/universal_ble.dart';
+import 'package:universal_ble_example/home/scanner_screen.dart';
+
+class PermissionScreen extends StatefulWidget {
+ const PermissionScreen({super.key});
+
+ @override
+ State createState() => _PermissionScreenState();
+}
+
+class _PermissionScreenState extends State
+ with WidgetsBindingObserver {
+ final bool _withAndroidFineLocation = false;
+ bool _isChecking = true;
+ bool _hasPermissions = false;
+ bool _isRequesting = false;
+ String? _errorMessage;
+
+ void navigateToHome() {
+ if (mounted) {
+ Navigator.of(context).pushReplacement(
+ MaterialPageRoute(builder: (_) => const ScannerScreen()),
+ );
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+ _checkPermissions();
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ super.dispose();
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ super.didChangeAppLifecycleState(state);
+ // Re-check permissions when app resumes (user might have granted in settings)
+ if (state == AppLifecycleState.resumed && !_hasPermissions) {
+ _checkPermissions();
+ }
+ }
+
+ Future _checkPermissions() async {
+ setState(() {
+ _isChecking = true;
+ _errorMessage = null;
+ });
+
+ try {
+ // Check if permissions are required for this platform
+ if (!BleCapabilities.requiresRuntimePermission) {
+ // No permissions needed, go directly to home
+ navigateToHome();
+ return;
+ }
+
+ // Check if we already have permissions
+ final hasPermissions = await UniversalBle.hasPermissions(
+ withAndroidFineLocation: _withAndroidFineLocation,
+ );
+
+ setState(() {
+ _isChecking = false;
+ _hasPermissions = hasPermissions;
+ });
+
+ if (hasPermissions && mounted) {
+ // Permissions already granted, go to home
+ navigateToHome();
+ }
+ } catch (e) {
+ setState(() {
+ _isChecking = false;
+ _errorMessage = e.toString();
+ });
+ }
+ }
+
+ Future _requestPermissions() async {
+ setState(() {
+ _isRequesting = true;
+ _errorMessage = null;
+ });
+
+ try {
+ await UniversalBle.requestPermissions(
+ withAndroidFineLocation: _withAndroidFineLocation,
+ );
+
+ // Permissions granted, go to home
+ if (mounted) {
+ navigateToHome();
+ }
+ } catch (e) {
+ setState(() {
+ _isRequesting = false;
+ _errorMessage = e.toString();
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+
+ return Scaffold(
+ backgroundColor: colorScheme.surface,
+ body: SafeArea(
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // Icon
+ Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: colorScheme.primaryContainer.withValues(alpha: 0.3),
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ Icons.security,
+ size: 80,
+ color: colorScheme.primary,
+ ),
+ ),
+ const SizedBox(height: 32),
+ Text(
+ 'Bluetooth Permissions Required',
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: colorScheme.onSurface,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'This app needs Bluetooth permissions to scan and connect to nearby devices.',
+ style: TextStyle(
+ fontSize: 16,
+ color: colorScheme.onSurface.withValues(alpha: 0.7),
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 48),
+ if (_isChecking)
+ CircularProgressIndicator(
+ valueColor: AlwaysStoppedAnimation(
+ colorScheme.primary,
+ ),
+ )
+ else if (!_hasPermissions)
+ Column(
+ children: [
+ ElevatedButton.icon(
+ onPressed: _isRequesting ? null : _requestPermissions,
+ icon: _isRequesting
+ ? SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(
+ colorScheme.onPrimary,
+ ),
+ ),
+ )
+ : const Icon(Icons.check_circle),
+ label: Text(
+ _isRequesting
+ ? 'Requesting Permissions...'
+ : 'Grant Permissions',
+ style: const TextStyle(fontSize: 16),
+ ),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorScheme.primary,
+ foregroundColor: colorScheme.onPrimary,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32,
+ vertical: 16,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ ),
+ if (_errorMessage != null) ...[
+ const SizedBox(height: 16),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: colorScheme.errorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.error_outline,
+ color: colorScheme.error,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ _errorMessage!,
+ style: TextStyle(
+ color: colorScheme.onErrorContainer,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ TextButton(
+ onPressed: _requestPermissions,
+ child: const Text('Try Again'),
+ ),
+ ],
+ ],
+ )
+ else
+ const SizedBox.shrink(),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/example/lib/home/scanner_screen.dart b/example/lib/home/scanner_screen.dart
new file mode 100644
index 0000000..7153354
--- /dev/null
+++ b/example/lib/home/scanner_screen.dart
@@ -0,0 +1,655 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:universal_ble/universal_ble.dart';
+import 'package:universal_ble_example/data/mock_universal_ble.dart';
+import 'package:universal_ble_example/home/widgets/ble_availability_icon.dart';
+import 'package:universal_ble_example/home/widgets/drawer.dart';
+import 'package:universal_ble_example/home/widgets/queue_selector_widget.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/peripheral_details/peripheral_detail_page.dart';
+
+class ScannerScreen extends StatefulWidget {
+ const ScannerScreen({super.key});
+
+ @override
+ State createState() => _ScannerScreenState();
+}
+
+class _ScannerScreenState extends State {
+ final _bleDevices = [];
+ final _hiddenDevices = [];
+ bool _isScanning = false;
+ QueueType _queueType = QueueType.global;
+ TextEditingController servicesFilterController = TextEditingController();
+ TextEditingController namePrefixController = TextEditingController();
+ TextEditingController manufacturerDataController = TextEditingController();
+ final TextEditingController _searchFilterController = TextEditingController();
+ final TextEditingController _webServicesController = TextEditingController();
+ StreamSubscription? _scanSubscription;
+
+ AvailabilityState? bleAvailabilityState;
+ ScanFilter? scanFilter;
+ final Map _isExpanded = {};
+
+ @override
+ void initState() {
+ super.initState();
+ if (const bool.fromEnvironment('MOCK')) {
+ UniversalBle.setInstance(MockUniversalBle());
+ }
+ UniversalBle.queueType = _queueType;
+ UniversalBle.timeout = const Duration(seconds: 10);
+
+ _scanSubscription = UniversalBle.scanStream.listen(_handleScanResult);
+
+ UniversalBle.isScanning().then(
+ (isScanning) => setState(() => _isScanning = isScanning),
+ );
+ }
+
+ void _handleScanResult(BleDevice result) {
+ if (_hiddenDevices.any((e) => e.deviceId == result.deviceId)) {
+ return;
+ }
+ int index = _bleDevices.indexWhere((e) => e.deviceId == result.deviceId);
+ if (index == -1) {
+ _bleDevices.add(result);
+ } else {
+ if (result.name == null && _bleDevices[index].name != null) {
+ result.name = _bleDevices[index].name;
+ }
+ _bleDevices[index] = result;
+ }
+ setState(() {});
+ }
+
+ Future _startScan() async {
+ setState(() {
+ _bleDevices.clear();
+ _isScanning = true;
+ });
+ try {
+ PlatformConfig? platformConfig;
+ if (kIsWeb && _webServicesController.text.isNotEmpty) {
+ List webServices = _webServicesController.text
+ .split(',')
+ .where((s) => s.trim().isNotEmpty)
+ .map((s) {
+ try {
+ return BleUuidParser.string(s.trim());
+ } catch (_) {
+ return s.trim();
+ }
+ }).toList();
+ platformConfig = PlatformConfig(
+ web: WebOptions(optionalServices: webServices),
+ );
+ }
+ await UniversalBle.startScan(
+ scanFilter: scanFilter,
+ platformConfig: platformConfig,
+ );
+ } catch (e) {
+ setState(() {
+ _isScanning = false;
+ });
+ showSnackbar(e.toString());
+ }
+ }
+
+ void _showScanFilterBottomSheet() {
+ showModalBottomSheet(
+ isScrollControlled: true,
+ context: context,
+ builder: (context) {
+ return ScanFilterWidget(
+ servicesFilterController: servicesFilterController,
+ namePrefixController: namePrefixController,
+ manufacturerDataController: manufacturerDataController,
+ onScanFilter: (ScanFilter? filter) {
+ setState(() {
+ scanFilter = filter;
+ });
+ },
+ );
+ },
+ );
+ }
+
+ void showSnackbar(String message) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text(message)));
+ }
+
+ List get _filteredDevices {
+ if (_searchFilterController.text.isEmpty) {
+ return _bleDevices;
+ }
+ final filter = _searchFilterController.text.toLowerCase();
+ return _bleDevices.where((device) {
+ final name = device.name?.toLowerCase() ?? '';
+ final deviceId = device.deviceId.toLowerCase();
+ final services = device.services.join(' ').toLowerCase();
+ return name.contains(filter) ||
+ deviceId.contains(filter) ||
+ services.contains(filter);
+ }).toList();
+ }
+
+ bool get _isBluetoothAvailable =>
+ bleAvailabilityState == AvailabilityState.poweredOn;
+
+ @override
+ void setState(VoidCallback fn) {
+ if (mounted) super.setState(fn);
+ }
+
+ @override
+ void dispose() {
+ servicesFilterController.dispose();
+ namePrefixController.dispose();
+ manufacturerDataController.dispose();
+ _searchFilterController.dispose();
+ _webServicesController.dispose();
+
+ _scanSubscription?.cancel();
+ super.dispose();
+ }
+
+ void _showQueueBottomSheet() {
+ showModalBottomSheet(
+ context: context,
+ builder: (context) => QueueSelectorWidget(
+ queueType: _queueType,
+ onQueueTypeChanged: (queueType) {
+ setState(() {
+ _queueType = queueType;
+ UniversalBle.queueType = _queueType;
+ });
+ Navigator.pop(context);
+ },
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+
+ return Scaffold(
+ backgroundColor: colorScheme.surface,
+ drawer: const AppDrawer(),
+ appBar: AppBar(
+ title: Row(
+ children: [
+ BleAvailabilityIcon(onAvailabilityStateChanged: (state) {
+ setState(() => bleAvailabilityState = state);
+ }),
+ const SizedBox(width: 12),
+ Expanded(
+ child: const Text(
+ 'Scanner',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ letterSpacing: 0.5,
+ ),
+ ),
+ ),
+ ],
+ ),
+ elevation: 0,
+ backgroundColor: colorScheme.surface,
+ leading: Builder(
+ builder: (context) => IconButton(
+ icon: const Icon(Icons.menu),
+ onPressed: () => Scaffold.of(context).openDrawer(),
+ ),
+ ),
+ actions: [
+ IconButton(
+ icon: Icon(
+ _isScanning ? Icons.stop_circle : Icons.play_arrow,
+ color: _isScanning ? colorScheme.error : colorScheme.primary,
+ ),
+ tooltip: _isScanning ? 'Stop Scan' : 'Start Scan',
+ onPressed: _isBluetoothAvailable
+ ? () async {
+ if (_isScanning) {
+ await UniversalBle.stopScan();
+ setState(() {
+ _isScanning = false;
+ });
+ } else {
+ await _startScan();
+ }
+ }
+ : null,
+ ),
+ IconButton(
+ icon: Icon(
+ Icons.queue,
+ color: colorScheme.onSurface,
+ ),
+ tooltip: 'Queue Type',
+ onPressed: _showQueueBottomSheet,
+ ),
+ ],
+ ),
+ body: Column(
+ children: [
+ // Search filter
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Container(
+ decoration: BoxDecoration(
+ color: colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.05),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: TextField(
+ controller: _searchFilterController,
+ decoration: InputDecoration(
+ hintText: 'Search by name, ID, or services...',
+ prefixIcon: Icon(
+ Icons.search,
+ color: colorScheme.primary,
+ ),
+ suffixIcon: _searchFilterController.text.isNotEmpty
+ ? IconButton(
+ icon: Icon(
+ Icons.clear,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ onPressed: () {
+ _searchFilterController.clear();
+ setState(() {});
+ },
+ )
+ : null,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12),
+ borderSide: BorderSide.none,
+ ),
+ filled: true,
+ fillColor: colorScheme.surfaceContainerHighest,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 16,
+ ),
+ ),
+ onChanged: (_) => setState(() {}),
+ ),
+ ),
+ ),
+ // Web Services input (only for web)
+ if (kIsWeb)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Container(
+ decoration: BoxDecoration(
+ color: colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: colorScheme.primary.withValues(alpha: 0.3),
+ width: 1,
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
+ child: Row(
+ children: [
+ Icon(
+ Icons.web,
+ size: 18,
+ color: colorScheme.primary,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Ble Services',
+ style: TextStyle(
+ fontWeight: FontWeight.w600,
+ fontSize: 13,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
+ child: TextFormField(
+ controller: _webServicesController,
+ maxLines: 2,
+ style: const TextStyle(
+ fontFamily: 'monospace', fontSize: 12),
+ decoration: InputDecoration(
+ hintText:
+ 'Enter service UUIDs for web (comma-separated)',
+ helperText:
+ 'These services will be available to use after connection on Web',
+ helperMaxLines: 2,
+ helperStyle: TextStyle(
+ fontSize: 11,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide.none,
+ ),
+ filled: true,
+ fillColor: colorScheme.surface,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 12,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // Device count badge with hide/unhide
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Row(
+ children: [
+ Tooltip(
+ message: _hiddenDevices.isNotEmpty
+ ? 'Show hidden devices'
+ : _bleDevices.isNotEmpty
+ ? 'Hide already discovered devices. When you turn on a new device, it will be easier to spot.'
+ : '',
+ child: TextButton.icon(
+ onPressed:
+ _bleDevices.isNotEmpty || _hiddenDevices.isNotEmpty
+ ? () {
+ if (_hiddenDevices.isNotEmpty) {
+ // Unhide all devices
+ setState(() {
+ _hiddenDevices.clear();
+ });
+ } else if (_bleDevices.isNotEmpty) {
+ // Hide all devices
+ setState(() {
+ _hiddenDevices.clear();
+ _hiddenDevices.addAll(_bleDevices);
+ _bleDevices.clear();
+ });
+ }
+ }
+ : null,
+ label: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ '${_bleDevices.length} ${_hiddenDevices.isNotEmpty ? "/ ${_hiddenDevices.length}" : ""} device${_bleDevices.length == 1 ? '' : 's'}',
+ style: TextStyle(
+ color: colorScheme.onPrimaryContainer,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(width: 4),
+ Icon(
+ _hiddenDevices.isNotEmpty
+ ? Icons.visibility
+ : Icons.visibility_off,
+ size: 16,
+ color: colorScheme.onPrimaryContainer,
+ ),
+ ],
+ ),
+ style: TextButton.styleFrom(
+ padding: EdgeInsets.symmetric(horizontal: 8),
+ backgroundColor: colorScheme.primaryContainer,
+ ),
+ ),
+ ),
+ const Spacer(),
+ TextButton.icon(
+ onPressed: _showScanFilterBottomSheet,
+ icon: Icon(
+ scanFilter != null
+ ? Icons.filter_list
+ : Icons.filter_list_outlined,
+ size: 18,
+ color: scanFilter != null
+ ? colorScheme.primary
+ : colorScheme.onSurface,
+ ),
+ label: Text(
+ 'Filter',
+ style: TextStyle(
+ color: scanFilter != null
+ ? colorScheme.primary
+ : colorScheme.onSurface,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ style: TextButton.styleFrom(
+ padding: EdgeInsets.symmetric(horizontal: 8),
+ backgroundColor: scanFilter != null
+ ? colorScheme.primaryContainer
+ : null,
+ ),
+ ),
+ const SizedBox(width: 4),
+ TextButton.icon(
+ onPressed: () {
+ setState(() {
+ _bleDevices.clear();
+ });
+ },
+ icon: Icon(
+ Icons.clear_all,
+ size: 18,
+ color: colorScheme.error,
+ ),
+ label: Text(
+ 'Clear',
+ style: TextStyle(
+ color: colorScheme.error,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ style: TextButton.styleFrom(
+ padding: EdgeInsets.symmetric(horizontal: 8),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const Divider(),
+ // Devices List
+ Expanded(
+ child: !_isBluetoothAvailable
+ ? Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: colorScheme.errorContainer.withValues(
+ alpha: 0.3,
+ ),
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ Icons.bluetooth_disabled,
+ size: 80,
+ color: colorScheme.error,
+ ),
+ ),
+ const SizedBox(height: 32),
+ Text(
+ 'Bluetooth is Turned Off',
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: colorScheme.onSurface,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Please turn on Bluetooth to scan for devices',
+ style: TextStyle(
+ fontSize: 16,
+ color: colorScheme.onSurface.withValues(
+ alpha: 0.7,
+ ),
+ ),
+ textAlign: TextAlign.center,
+ ),
+ if (BleCapabilities.supportsBluetoothEnableApi) ...[
+ const SizedBox(height: 48),
+ ElevatedButton.icon(
+ onPressed: () async {
+ try {
+ bool isEnabled =
+ await UniversalBle.enableBluetooth();
+ if (!isEnabled) {
+ showSnackbar(
+ "Please enable Bluetooth in system settings",
+ );
+ }
+ } catch (e) {
+ showSnackbar(e.toString());
+ }
+ },
+ icon: const Icon(Icons.bluetooth, size: 24),
+ label: const Text(
+ 'Turn On Bluetooth',
+ style: TextStyle(fontSize: 18),
+ ),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorScheme.primary,
+ foregroundColor: colorScheme.onPrimary,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32,
+ vertical: 16,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ elevation: 2,
+ ),
+ ),
+ ]
+ ],
+ ),
+ ),
+ )
+ : _isScanning && _bleDevices.isEmpty
+ ? Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator.adaptive(
+ valueColor: AlwaysStoppedAnimation(
+ colorScheme.primary,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Scanning for devices...',
+ style: TextStyle(
+ color: colorScheme.onSurface
+ .withValues(alpha: 0.6),
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ )
+ : !_isScanning && _bleDevices.isEmpty
+ ? ScannedDevicesPlaceholderWidget(onTap: _startScan)
+ : _filteredDevices.isEmpty
+ ? Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.search_off,
+ size: 64,
+ color: colorScheme.onSurface
+ .withValues(alpha: 0.3),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'No devices match your filter',
+ style: TextStyle(
+ color: colorScheme.onSurface
+ .withValues(alpha: 0.6),
+ fontSize: 16,
+ ),
+ ),
+ ],
+ ),
+ )
+ : ListView.separated(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ itemCount: _filteredDevices.length,
+ separatorBuilder: (context, index) =>
+ const SizedBox(height: 8),
+ itemBuilder: (context, index) {
+ BleDevice device = _filteredDevices[
+ _filteredDevices.length - index - 1];
+ return ScannedItemWidget(
+ bleDevice: device,
+ isExpanded:
+ _isExpanded[device.deviceId] ?? false,
+ onExpand: (isExpanded) {
+ setState(() {
+ _isExpanded[device.deviceId] =
+ isExpanded;
+ });
+ },
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) =>
+ PeripheralDetailPage(device),
+ ),
+ );
+ // Stop scan but keep results visible
+ UniversalBle.stopScan();
+ setState(() {
+ _isScanning = false;
+ });
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/lib/home/system_devices_screen.dart b/example/lib/home/system_devices_screen.dart
new file mode 100644
index 0000000..e96051b
--- /dev/null
+++ b/example/lib/home/system_devices_screen.dart
@@ -0,0 +1,379 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:universal_ble/universal_ble.dart';
+import 'package:universal_ble_example/home/widgets/ble_availability_icon.dart';
+import 'package:universal_ble_example/home/widgets/drawer.dart';
+import 'package:universal_ble_example/home/widgets/scanned_item_widget.dart';
+import 'package:universal_ble_example/peripheral_details/peripheral_detail_page.dart';
+
+class SystemDevicesScreen extends StatefulWidget {
+ const SystemDevicesScreen({super.key});
+
+ @override
+ State createState() => _SystemDevicesScreenState();
+}
+
+class _SystemDevicesScreenState extends State {
+ List _systemDevices = [];
+ bool _isLoading = false;
+ AvailabilityState? bleAvailabilityState;
+ List withServices = [];
+ final Map _isExpanded = {};
+ final TextEditingController _servicesController = TextEditingController();
+
+ void _parseServices() {
+ setState(() {
+ withServices = [];
+ if (_servicesController.text.isNotEmpty) {
+ final services = _servicesController.text
+ .split(',')
+ .map((s) => s.trim())
+ .where((s) => s.isNotEmpty)
+ .toList();
+ for (String service in services) {
+ try {
+ withServices.add(BleUuidParser.string(service));
+ } catch (e) {
+ _showSnackbar("Invalid Service UUID: $service");
+ return;
+ }
+ }
+ }
+ });
+ }
+
+ Future _getSystemDevices() async {
+ _parseServices();
+
+ // For macOS and iOS, it is recommended to set a filter to get system devices
+ if ((defaultTargetPlatform == TargetPlatform.macOS ||
+ defaultTargetPlatform == TargetPlatform.iOS) &&
+ withServices.isEmpty) {
+ _showSnackbar(
+ "No services filter was set for getting system connected devices. Using default services...",
+ );
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ List devices = await UniversalBle.getSystemDevices(
+ withServices: withServices,
+ );
+ setState(() {
+ _systemDevices = devices;
+ _isLoading = false;
+ });
+ if (devices.isEmpty) {
+ _showSnackbar("No System Connected Devices Found");
+ }
+ } catch (e) {
+ setState(() {
+ _isLoading = false;
+ });
+ _showSnackbar(e.toString());
+ }
+ }
+
+ void _showSnackbar(String message) {
+ ScaffoldMessenger.of(context)
+ .showSnackBar(SnackBar(content: Text(message)));
+ }
+
+ bool get _isBluetoothAvailable =>
+ bleAvailabilityState == AvailabilityState.poweredOn;
+
+ @override
+ void setState(VoidCallback fn) {
+ if (mounted) super.setState(fn);
+ }
+
+ @override
+ void dispose() {
+ _servicesController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+
+ return Scaffold(
+ backgroundColor: colorScheme.surface,
+ drawer: const AppDrawer(),
+ appBar: AppBar(
+ title: Row(
+ children: [
+ BleAvailabilityIcon(onAvailabilityStateChanged: (state) {
+ setState(() => bleAvailabilityState = state);
+ }),
+ const SizedBox(width: 12),
+ Expanded(
+ child: const Text(
+ 'System Devices',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ letterSpacing: 0.5,
+ ),
+ ),
+ ),
+ ],
+ ),
+ elevation: 0,
+ backgroundColor: colorScheme.surface,
+ actions: [
+ IconButton(
+ icon: Icon(
+ Icons.refresh,
+ color: colorScheme.primary,
+ ),
+ tooltip: 'Refresh',
+ onPressed: _isBluetoothAvailable ? _getSystemDevices : null,
+ ),
+ ],
+ ),
+ body: Column(
+ children: [
+ // Services input text box
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: Card(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.apps,
+ color: colorScheme.primary,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Service UUIDs (Optional)',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Enter service UUIDs separated by commas. Leave empty to use default services.\nMandatory on Apple platforms.',
+ style: TextStyle(
+ fontSize: 12,
+ color: colorScheme.onSurface.withValues(alpha: 0.7),
+ ),
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ controller: _servicesController,
+ maxLines: 8,
+ minLines: 4,
+ style: const TextStyle(
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ decoration: InputDecoration(
+ hintText:
+ 'eg: 0000180f-0000-1000-8000-00805f9b34fb, 0000180a-0000-1000-8000-00805f9b34fb',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ filled: true,
+ fillColor: colorScheme.surfaceContainerHighest,
+ contentPadding: const EdgeInsets.all(12),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: SizedBox(
+ width: double.infinity,
+ child: ElevatedButton.icon(
+ onPressed: _isBluetoothAvailable && !_isLoading
+ ? _getSystemDevices
+ : null,
+ icon: _isLoading
+ ? SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(
+ colorScheme.onPrimary,
+ ),
+ ),
+ )
+ : const Icon(Icons.search),
+ label: Text(_isLoading ? 'Loading...' : 'Get System Devices'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorScheme.primary,
+ foregroundColor: colorScheme.onPrimary,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ elevation: 2,
+ ),
+ ),
+ ),
+ ),
+ if (_systemDevices.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 6,
+ ),
+ decoration: BoxDecoration(
+ color: colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ '${_systemDevices.length} device${_systemDevices.length == 1 ? '' : 's'}',
+ style: TextStyle(
+ color: colorScheme.onPrimaryContainer,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const Divider(),
+ Expanded(
+ child: !_isBluetoothAvailable
+ ? Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: colorScheme.errorContainer.withValues(
+ alpha: 0.3,
+ ),
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ Icons.bluetooth_disabled,
+ size: 80,
+ color: colorScheme.error,
+ ),
+ ),
+ const SizedBox(height: 32),
+ Text(
+ 'Bluetooth is Turned Off',
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: colorScheme.onSurface,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Please turn on Bluetooth to get system devices',
+ style: TextStyle(
+ fontSize: 16,
+ color: colorScheme.onSurface.withValues(
+ alpha: 0.7,
+ ),
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ),
+ )
+ : _systemDevices.isEmpty
+ ? Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.devices_outlined,
+ size: 64,
+ color:
+ colorScheme.onSurface.withValues(alpha: 0.3),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'No system devices found',
+ style: TextStyle(
+ color: colorScheme.onSurface
+ .withValues(alpha: 0.6),
+ fontSize: 16,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Click "Get System Devices" to refresh',
+ style: TextStyle(
+ color: colorScheme.onSurface
+ .withValues(alpha: 0.5),
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ )
+ : ListView.separated(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ itemCount: _systemDevices.length,
+ separatorBuilder: (context, index) =>
+ const SizedBox(height: 8),
+ itemBuilder: (context, index) {
+ BleDevice device = _systemDevices[index];
+ return ScannedItemWidget(
+ bleDevice: device,
+ isExpanded: _isExpanded[device.deviceId] ?? false,
+ onExpand: (isExpanded) {
+ setState(() {
+ _isExpanded[device.deviceId] = isExpanded;
+ });
+ },
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => PeripheralDetailPage(device),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/lib/home/widgets/ble_availability_icon.dart b/example/lib/home/widgets/ble_availability_icon.dart
new file mode 100644
index 0000000..265240a
--- /dev/null
+++ b/example/lib/home/widgets/ble_availability_icon.dart
@@ -0,0 +1,73 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:universal_ble/universal_ble.dart';
+
+class BleAvailabilityIcon extends StatefulWidget {
+ final Function(AvailabilityState) onAvailabilityStateChanged;
+ const BleAvailabilityIcon({
+ super.key,
+ required this.onAvailabilityStateChanged,
+ });
+
+ @override
+ State createState() => _BleAvailabilityIconState();
+}
+
+class _BleAvailabilityIconState extends State {
+ AvailabilityState? bleAvailabilityState;
+ StreamSubscription? _availabilitySubscription;
+
+ @override
+ void initState() {
+ super.initState();
+ UniversalBle.getBluetoothAvailabilityState()
+ .then(_handleAvailabilityStateChanged);
+ _availabilitySubscription =
+ UniversalBle.availabilityStream.listen(_handleAvailabilityStateChanged);
+ }
+
+ void _handleAvailabilityStateChanged(AvailabilityState state) {
+ if (mounted) setState(() => bleAvailabilityState = state);
+ widget.onAvailabilityStateChanged(state);
+ }
+
+ @override
+ void dispose() {
+ _availabilitySubscription?.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (bleAvailabilityState == null) {
+ return const SizedBox.shrink();
+ }
+ return switch (bleAvailabilityState!) {
+ AvailabilityState.resetting => Icon(
+ Icons.bluetooth_searching,
+ color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ AvailabilityState.poweredOn => Icon(
+ Icons.bluetooth_connected,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ AvailabilityState.poweredOff => Icon(
+ Icons.bluetooth_disabled,
+ color: Theme.of(context).colorScheme.error,
+ ),
+ AvailabilityState.unauthorized => Icon(
+ Icons.bluetooth_disabled,
+ color: Theme.of(context).colorScheme.error,
+ ),
+ AvailabilityState.unsupported => Icon(
+ Icons.bluetooth_disabled,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ AvailabilityState.unknown => Icon(
+ Icons.bluetooth_searching,
+ color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ };
+ }
+}
diff --git a/example/lib/home/widgets/ble_device_info_widget.dart b/example/lib/home/widgets/ble_device_info_widget.dart
new file mode 100644
index 0000000..52c06d9
--- /dev/null
+++ b/example/lib/home/widgets/ble_device_info_widget.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/material.dart';
+import 'package:universal_ble/universal_ble.dart';
+
+class BleDeviceInfoWidget extends StatelessWidget {
+ final BleDevice bleDevice;
+ const BleDeviceInfoWidget({super.key, required this.bleDevice});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SelectableText('Device ID: ${bleDevice.deviceId}'),
+ SelectableText('Name: ${bleDevice.name ?? 'N/A'}'),
+ SelectableText('RSSI: ${bleDevice.rssi}'),
+ SelectableText('Paired: ${bleDevice.paired}'),
+ SelectableText(
+ 'Services: ${bleDevice.services.isNotEmpty ? bleDevice.services.join(', ') : 'N/A'}',
+ ),
+ SelectableText(
+ 'Manufacturer Data: ${bleDevice.manufacturerDataList.isNotEmpty ? bleDevice.manufacturerDataList.map((e) => e.toString()).join(', ') : 'N/A'}',
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/lib/home/widgets/drawer.dart b/example/lib/home/widgets/drawer.dart
new file mode 100644
index 0000000..e4a0f1f
--- /dev/null
+++ b/example/lib/home/widgets/drawer.dart
@@ -0,0 +1,109 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:universal_ble/universal_ble.dart';
+import 'package:universal_ble_example/home/scanner_screen.dart';
+import 'package:universal_ble_example/home/system_devices_screen.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+class AppDrawer extends StatefulWidget {
+ const AppDrawer({super.key});
+
+ @override
+ State createState() => _AppDrawerState();
+}
+
+class _AppDrawerState extends State {
+ void _navigateToScreen(BuildContext context, Widget screen) {
+ Navigator.pop(context);
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(builder: (_) => screen),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = Theme.of(context).colorScheme;
+ return Drawer(
+ child: Column(
+ children: [
+ SizedBox(
+ height: 110,
+ child: DrawerHeader(
+ padding: const EdgeInsets.only(top: 0, left: 16),
+ decoration: BoxDecoration(
+ color: colorScheme.primary,
+ ),
+ child: Row(
+ children: [
+ Image.asset('assets/icon.png', width: 40, height: 40),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Text(
+ 'Universal BLE',
+ maxLines: 2,
+ style:
+ Theme.of(context).textTheme.headlineMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ color: colorScheme.onPrimary,
+ height: 1,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ListTile(
+ leading: const Icon(Icons.search),
+ title: const Text('Scanner'),
+ onTap: () => _navigateToScreen(context, const ScannerScreen()),
+ ),
+ if (BleCapabilities.supportsConnectedDevicesApi)
+ ListTile(
+ leading: const Icon(Icons.devices),
+ title: const Text('System Devices'),
+ onTap: () =>
+ _navigateToScreen(context, const SystemDevicesScreen()),
+ ),
+ const Divider(),
+ FutureBuilder(
+ future: PackageInfo.fromPlatform(),
+ builder: (_, snapshot) => AboutListTile(
+ icon: const Icon(Icons.info_outline),
+ applicationIcon:
+ Image.asset('assets/icon.png', width: 40, height: 40),
+ applicationName: 'Universal BLE',
+ applicationVersion:
+ "${snapshot.data?.version} (${snapshot.data?.buildNumber})",
+ applicationLegalese: '\u{a9} 2023 Navideck',
+ aboutBoxChildren: [
+ const SizedBox(height: 24),
+ RichText(
+ textAlign: TextAlign.justify,
+ text: TextSpan(
+ children: [
+ TextSpan(
+ text: 'Learn More',
+ style: const TextStyle(
+ color: Colors.blue,
+ decoration: TextDecoration.underline,
+ ),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ launchUrl(Uri.parse(
+ "https://github.com/Navideck/universal_ble"));
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/lib/home/widgets/queue_selector_widget.dart b/example/lib/home/widgets/queue_selector_widget.dart
new file mode 100644
index 0000000..e84ca75
--- /dev/null
+++ b/example/lib/home/widgets/queue_selector_widget.dart
@@ -0,0 +1,157 @@
+import 'package:flutter/material.dart';
+import 'package:universal_ble/universal_ble.dart';
+
+class QueueSelectorWidget extends StatelessWidget {
+ final QueueType queueType;
+ final Function(QueueType) onQueueTypeChanged;
+ const QueueSelectorWidget({
+ super.key,
+ required this.queueType,
+ required this.onQueueTypeChanged,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = Theme.of(context).colorScheme;
+ return Container(
+ padding: const EdgeInsets.all(24),
+ child: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.queue,
+ color: colorScheme.primary,
+ size: 28,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ "Queue Type",
+ style: Theme.of(context).textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => Navigator.pop(context),
+ tooltip: 'Close',
+ ),
+ ],
+ ),
+ const SizedBox(height: 2),
+ Text(
+ "Controls how BLE commands are executed.",
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ ),
+ const SizedBox(height: 8),
+ _buildQueueOption(
+ context,
+ QueueType.global,
+ 'Global',
+ 'All commands from all devices execute sequentially in a single queue',
+ Icons.queue,
+ ),
+ const SizedBox(height: 8),
+ _buildQueueOption(
+ context,
+ QueueType.perDevice,
+ 'Per Device',
+ 'Commands for each device execute in separate queues',
+ Icons.devices,
+ ),
+ const SizedBox(height: 8),
+ _buildQueueOption(
+ context,
+ QueueType.none,
+ 'None',
+ 'All commands execute in parallel without queuing',
+ Icons.all_inclusive,
+ ),
+ const SizedBox(height: 8),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildQueueOption(
+ BuildContext context,
+ QueueType value,
+ String title,
+ String description,
+ IconData icon,
+ ) {
+ final colorScheme = Theme.of(context).colorScheme;
+ final isSelected = queueType == value;
+ return InkWell(
+ onTap: () => onQueueTypeChanged(value),
+ borderRadius: BorderRadius.circular(12),
+ child: Container(
+ padding: const EdgeInsets.all(6),
+ decoration: BoxDecoration(
+ color: isSelected
+ ? colorScheme.primaryContainer
+ : colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: isSelected
+ ? colorScheme.primary
+ : colorScheme.outline.withValues(alpha: 0.2),
+ width: isSelected ? 2 : 1,
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ icon,
+ color: isSelected
+ ? colorScheme.onPrimaryContainer
+ : colorScheme.onSurface,
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ color: isSelected
+ ? colorScheme.onPrimaryContainer
+ : colorScheme.onSurface,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ description,
+ style: TextStyle(
+ fontSize: 12,
+ color: isSelected
+ ? colorScheme.onPrimaryContainer
+ .withValues(alpha: 0.7)
+ : colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ ),
+ ],
+ ),
+ ),
+ if (isSelected)
+ Icon(
+ Icons.check_circle,
+ color: colorScheme.primary,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/example/lib/home/widgets/rssi_signal_indicator.dart b/example/lib/home/widgets/rssi_signal_indicator.dart
new file mode 100644
index 0000000..7e4a9ea
--- /dev/null
+++ b/example/lib/home/widgets/rssi_signal_indicator.dart
@@ -0,0 +1,63 @@
+import 'package:flutter/material.dart';
+
+class RssiSignalIndicator extends StatelessWidget {
+ final int rssi;
+ const RssiSignalIndicator({super.key, required this.rssi});
+
+ @override
+ Widget build(BuildContext context) {
+ final bool isPositive = rssi >= 0;
+ final Color barColor = isPositive ? Colors.green : Colors.red;
+ // BLE RSSI ranges (in dBm):
+ // -30 to 0: Excellent signal (4 bars)
+ // -50 to -30: Good signal (3 bars)
+ // -70 to -50: Fair signal (2 bars)
+ // -90 to -70: Weak signal (1 bar)
+ // Below -90: Very weak signal (0 bars)
+ // Positive values: Extremely strong (4 bars)
+ int bars;
+ if (isPositive) {
+ bars = 4; // Positive RSSI is extremely strong
+ } else {
+ if (rssi >= -30) {
+ bars = 4; // Excellent
+ } else if (rssi >= -50) {
+ bars = 3; // Good
+ } else if (rssi >= -70) {
+ bars = 2; // Fair
+ } else if (rssi >= -90) {
+ bars = 1; // Weak
+ } else {
+ bars = 0; // Very weak
+ }
+ }
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(
+ width: 24,
+ height: 16,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: List.generate(4, (index) {
+ final barHeight = (index + 1) * 3.0 + 2.0;
+ final isActive = index < bars;
+ return Container(
+ width: 3,
+ height: barHeight,
+ decoration: BoxDecoration(
+ color: isActive ? barColor : Colors.grey.withAlpha(30),
+ borderRadius: BorderRadius.circular(1.5),
+ ),
+ );
+ }),
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text('$rssi'),
+ ],
+ );
+ }
+}
diff --git a/example/lib/home/widgets/scan_filter_widget.dart b/example/lib/home/widgets/scan_filter_widget.dart
index c2da8b7..319ae4b 100644
--- a/example/lib/home/widgets/scan_filter_widget.dart
+++ b/example/lib/home/widgets/scan_filter_widget.dart
@@ -1,6 +1,5 @@
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 void Function(ScanFilter? filter) onScanFilter;
@@ -32,9 +31,13 @@ class _ScanFilterWidgetState extends State {
List namePrefixes = [];
List manufacturerDataFilters = [];
- // Parse Services
+ // Parse Services - handle both comma and newline separated
if (widget.servicesFilterController.text.isNotEmpty) {
- List services = widget.servicesFilterController.text.split(',');
+ List services = widget.servicesFilterController.text
+ .split(',')
+ .map((e) => e.trim())
+ .where((s) => s.isNotEmpty)
+ .toList();
for (String service in services) {
try {
serviceUUids.add(BleUuidParser.string(service.trim()));
@@ -44,18 +47,34 @@ class _ScanFilterWidgetState extends State {
}
}
- // Parse Name Prefix
+ // Parse Name Prefix - handle both comma and newline separated
String namePrefix = widget.namePrefixController.text;
if (namePrefix.isNotEmpty) {
namePrefixes = namePrefix.split(',').map((e) => e.trim()).toList();
}
- // Parse Manufacturer Data
+ // Parse Manufacturer Data - handle both comma and newline separated
String manufacturerDataText = widget.manufacturerDataController.text;
if (manufacturerDataText.isNotEmpty) {
- List manufacturerData = manufacturerDataText.split(',');
+ List manufacturerData = manufacturerDataText
+ .split(',')
+ .map((e) => e.trim())
+ .where((s) => s.isNotEmpty)
+ .toList();
for (String manufacturer in manufacturerData) {
- int? companyIdentifier = int.tryParse(manufacturer);
+ String trimmed = manufacturer.trim();
+ // Remove 0x prefix if present, otherwise parse as decimal or hex
+ int? companyIdentifier;
+ if (trimmed.toLowerCase().startsWith('0x')) {
+ companyIdentifier = int.tryParse(trimmed.substring(2), radix: 16);
+ } else {
+ // Try parsing as hex first (if it contains letters), then decimal
+ if (trimmed.contains(RegExp(r'[a-fA-F]'))) {
+ companyIdentifier = int.tryParse(trimmed, radix: 16);
+ } else {
+ companyIdentifier = int.tryParse(trimmed);
+ }
+ }
if (companyIdentifier == null) {
throw Exception("Invalid Manufacturer Data $manufacturer");
}
@@ -98,74 +117,253 @@ class _ScanFilterWidgetState extends State {
@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,
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+
+ return Container(
+ decoration: BoxDecoration(
+ color: colorScheme.surface,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.only(
+ left: 20,
+ right: 20,
+ top: 20,
+ bottom: MediaQuery.of(context).viewInsets.bottom + 20,
+ ),
+ child: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ // Header
+ Row(
+ children: [
+ Icon(
+ Icons.filter_list,
+ color: colorScheme.primary,
+ size: 28,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ "Scan Filters",
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => Navigator.pop(context),
+ tooltip: 'Close',
+ ),
+ ],
+ ),
+ Text(
+ "Filter devices by name, services, or manufacturer data. Enter multiple values separated by commas.",
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ ),
+ const SizedBox(height: 8),
+ if (error != null)
+ Container(
+ margin: const EdgeInsets.only(bottom: 16),
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: colorScheme.errorContainer,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: colorScheme.error.withValues(alpha: 0.3),
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.error_outline,
+ color: colorScheme.error,
+ size: 20,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ error!,
+ style: TextStyle(
+ color: colorScheme.onErrorContainer,
+ fontSize: 13,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Name Prefix Filter
+ _buildFilterCard(
+ context: context,
+ title: "Name Prefixes",
+ icon: Icons.text_fields,
+ controller: widget.namePrefixController,
+ hintText: "e.g. MyDevice Sensor",
+ helperText: "Device names starting with these prefixes",
+ ),
+ const SizedBox(height: 16),
+ // Services Filter
+ _buildFilterCard(
+ context: context,
+ title: "Service UUIDs",
+ maxLines: 3,
+ icon: Icons.apps,
+ controller: widget.servicesFilterController,
+ hintText: "e.g. 0000180f-0000-1000-8000-00805f9b34fb,180F",
+ helperText: "Service UUIDs (16-bit, 32-bit, or 128-bit)",
+ ),
+ const SizedBox(height: 16),
+ // Manufacturer Data Filter
+ _buildFilterCard(
+ context: context,
+ title: "Manufacturer Company IDs",
+ icon: Icons.business,
+ controller: widget.manufacturerDataController,
+ hintText: "e.g. 76,0x004C",
+ helperText: "Company identifiers in decimal or hex format",
+ ),
+ const SizedBox(height: 24),
+ // Action Buttons
+ Row(
+ children: [
+ Expanded(
+ flex: 2,
+ child: OutlinedButton.icon(
+ onPressed: clearFilter,
+ icon: const Icon(Icons.clear_all),
+ label: const Text('Clear All'),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: colorScheme.onSurface,
+ padding: const EdgeInsets.symmetric(vertical: 14),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 5),
+ Expanded(
+ flex: 3,
+ child: ElevatedButton.icon(
+ onPressed: applyFilter,
+ icon: const Icon(Icons.check),
+ label: const Text('Apply Filters'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorScheme.primary,
+ foregroundColor: colorScheme.onPrimary,
+ padding: const EdgeInsets.symmetric(vertical: 14),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ ],
),
),
- const Text("Use comma to add multiple values"),
- const Divider(),
- const SizedBox(height: 10),
- TextFormField(
- controller: widget.namePrefixController,
- maxLines: 2,
- decoration: const InputDecoration(
- labelText: "Name Prefixes",
- border: OutlineInputBorder(),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFilterCard({
+ required BuildContext context,
+ required String title,
+ required IconData icon,
+ required TextEditingController controller,
+ required String hintText,
+ required String helperText,
+ int maxLines = 2,
+ }) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+
+ return Card(
+ elevation: 0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ side: BorderSide(
+ color: colorScheme.outline.withValues(alpha: 0.2),
+ ),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ icon,
+ size: 20,
+ color: colorScheme.primary,
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ title,
+ style: theme.textTheme.titleSmall?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
),
- ),
- const SizedBox(height: 10),
- TextFormField(
- controller: widget.servicesFilterController,
- maxLines: 2,
- decoration: const InputDecoration(
- labelText: "Services",
- 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(height: 4),
+ TextFormField(
+ controller: controller,
+ maxLines: maxLines,
+ style: const TextStyle(fontFamily: 'monospace'),
+ decoration: InputDecoration(
+ hintText: hintText,
+ helperText: helperText,
+ helperMaxLines: 2,
+ helperStyle: TextStyle(
+ fontSize: 11,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide(
+ color: colorScheme.outline.withValues(alpha: 0.5),
+ ),
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide(
+ color: colorScheme.outline.withValues(alpha: 0.3),
+ ),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide(
+ color: colorScheme.primary,
+ width: 2,
+ ),
+ ),
+ filled: true,
+ fillColor:
+ colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 8,
),
),
- 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),
- ],
+ ],
+ ),
),
);
}
diff --git a/example/lib/home/widgets/scanned_devices_placeholder_widget.dart b/example/lib/home/widgets/scanned_devices_placeholder_widget.dart
index bcc7437..db615ff 100644
--- a/example/lib/home/widgets/scanned_devices_placeholder_widget.dart
+++ b/example/lib/home/widgets/scanned_devices_placeholder_widget.dart
@@ -1,26 +1,58 @@
import 'package:flutter/material.dart';
class ScannedDevicesPlaceholderWidget extends StatelessWidget {
- const ScannedDevicesPlaceholderWidget({super.key});
+ final Function() onTap;
+ const ScannedDevicesPlaceholderWidget({super.key, required this.onTap});
@override
Widget build(BuildContext context) {
- return const Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Padding(
- padding: EdgeInsets.all(8.0),
- child: Icon(
- Icons.bluetooth,
- color: Colors.grey,
- size: 100,
- ),
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+
+ return Center(
+ child: SingleChildScrollView(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ InkWell(
+ onTap: onTap,
+ child: Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ color: colorScheme.primaryContainer.withValues(alpha: 0.3),
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ Icons.bluetooth_searching,
+ size: 80,
+ color: colorScheme.primary.withValues(alpha: 0.6),
+ ),
+ ),
+ ),
+ const SizedBox(height: 24),
+ Text(
+ 'No Devices Found',
+ style: TextStyle(
+ color: colorScheme.onSurface,
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 48),
+ child: Text(
+ 'Tap Scan button to discover nearby Bluetooth devices',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ fontSize: 14,
+ ),
+ ),
+ ),
+ ],
),
- Text(
- 'Scan For Devices',
- style: TextStyle(color: Colors.grey, fontSize: 22),
- )
- ],
+ ),
);
}
}
diff --git a/example/lib/home/widgets/scanned_item_widget.dart b/example/lib/home/widgets/scanned_item_widget.dart
index 37f5a82..1d8a73c 100644
--- a/example/lib/home/widgets/scanned_item_widget.dart
+++ b/example/lib/home/widgets/scanned_item_widget.dart
@@ -1,50 +1,418 @@
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:universal_ble/universal_ble.dart';
+import 'package:universal_ble_example/home/widgets/rssi_signal_indicator.dart';
class ScannedItemWidget extends StatelessWidget {
final BleDevice bleDevice;
final VoidCallback? onTap;
- const ScannedItemWidget({super.key, required this.bleDevice, this.onTap});
+ final bool isExpanded;
+ final Function(bool) onExpand;
+ const ScannedItemWidget({
+ super.key,
+ required this.bleDevice,
+ this.onTap,
+ required this.isExpanded,
+ required this.onExpand,
+ });
@override
Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+
String? name = bleDevice.name;
List rawManufacturerData = bleDevice.manufacturerDataList;
- ManufacturerData? manufacturerData;
- if (rawManufacturerData.isNotEmpty) {
- manufacturerData = rawManufacturerData.first;
- }
- if (name == null || name.isEmpty) name = 'N/A';
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
- child: Card(
- child: ListTile(
- title: Text(
- '$name (${bleDevice.rssi})',
- ),
- subtitle: Column(
+ if (name == null || name.isEmpty) name = 'Unknown Device';
+
+ return Card(
+ elevation: 2,
+ margin: EdgeInsets.zero,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(16),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(bleDevice.deviceId),
- Visibility(
- visible: manufacturerData != null,
- child: Text(manufacturerData.toString()),
- ),
- if (bleDevice.timestampDateTime != null)
- Text("Last Seen: ${bleDevice.timestampDateTime}"),
- bleDevice.paired == true
- ? const Text(
- "Paired",
- style: TextStyle(color: Colors.green),
- )
- : const Text(
- "Not Paired",
- style: TextStyle(color: Colors.red),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Signal indicator
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color:
+ colorScheme.primaryContainer.withValues(alpha: 0.5),
+ borderRadius: BorderRadius.circular(12),
),
+ child: RssiSignalIndicator(rssi: bleDevice.rssi ?? 0),
+ ),
+ const SizedBox(width: 16),
+ // Device info
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Expanded(
+ child: Text(
+ name,
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: colorScheme.onSurface,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ const SizedBox(width: 8),
+ // Expand/Collapse button
+ if (rawManufacturerData.isNotEmpty ||
+ bleDevice.services.isNotEmpty)
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 4,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: colorScheme.primaryContainer
+ .withValues(alpha: 0.3),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: IconButton(
+ icon: Icon(
+ isExpanded
+ ? Icons.expand_less
+ : Icons.expand_more,
+ color: colorScheme.primary,
+ size: 18,
+ ),
+ onPressed: () {
+ onExpand(!isExpanded);
+ },
+ tooltip: isExpanded ? 'Collapse' : 'Expand',
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(
+ minWidth: 20,
+ minHeight: 20,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 6),
+ Row(
+ children: [
+ Icon(
+ Icons.fingerprint,
+ size: 12,
+ color:
+ colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ const SizedBox(width: 4),
+ Expanded(
+ child: Text(
+ bleDevice.deviceId,
+ style: TextStyle(
+ fontSize: 12,
+ color: colorScheme.onSurface
+ .withValues(alpha: 0.7),
+ fontFamily: 'monospace',
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 6),
+ Wrap(
+ spacing: 4,
+ runSpacing: 4,
+ children: [
+ // Pair status
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: bleDevice.paired == true
+ ? Colors.green.withValues(alpha: 0.2)
+ : Colors.orange.withValues(alpha: 0.2),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ bleDevice.paired == true
+ ? 'Paired'
+ : 'Unpaired',
+ style: TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.w600,
+ color: bleDevice.paired == true
+ ? Colors.green.shade700
+ : Colors.orange.shade700,
+ ),
+ ),
+ ),
+ // Manufacturer data (only in collapsed mode)
+ if (!isExpanded) ...[
+ ...rawManufacturerData.take(2).map((data) {
+ return Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: colorScheme.secondaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ data.companyIdRadix16,
+ style: TextStyle(
+ fontSize: 10,
+ color: colorScheme.onSecondaryContainer,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ );
+ }),
+ ],
+ // Services (only in collapsed mode)
+ if (!isExpanded) ...[
+ ...bleDevice.services.take(3).map((service) {
+ return Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: colorScheme.tertiaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ service.length > 12
+ ? '${service.substring(0, 12)}...'
+ : service,
+ style: TextStyle(
+ fontSize: 10,
+ color: colorScheme.onTertiaryContainer,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ );
+ }),
+ if (bleDevice.services.length > 3)
+ Text(
+ '+${bleDevice.services.length - 3} more',
+ style: TextStyle(
+ fontSize: 10,
+ color: colorScheme.onSurface
+ .withValues(alpha: 0.5),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ // Expanded details
+ if (isExpanded) ...[
+ const SizedBox(height: 16),
+ const Divider(),
+ const SizedBox(height: 12),
+ if (rawManufacturerData.isNotEmpty) ...[
+ Text(
+ 'Manufacturer Data',
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.bold,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ const SizedBox(height: 8),
+ ...rawManufacturerData.map((data) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: colorScheme.secondaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ 'Company ID: ',
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w600,
+ color: colorScheme
+ .onSecondaryContainer,
+ ),
+ ),
+ Expanded(
+ child: SelectableText(
+ data.companyIdRadix16,
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w600,
+ color: colorScheme
+ .onSecondaryContainer,
+ fontFamily: 'monospace',
+ ),
+ ),
+ ),
+ ],
+ ),
+ if (data.payloadRadix16.isNotEmpty) ...[
+ const SizedBox(height: 4),
+ Row(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: SelectableText(
+ data.payloadRadix16,
+ style: TextStyle(
+ fontSize: 10,
+ color: colorScheme
+ .onSecondaryContainer
+ .withValues(alpha: 0.8),
+ fontFamily: 'monospace',
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ],
+ ),
+ ),
+ const SizedBox(width: 8),
+ IconButton(
+ icon: Icon(
+ Icons.copy,
+ size: 16,
+ color: colorScheme.onSecondaryContainer,
+ ),
+ onPressed: () {
+ final textToCopy = data
+ .payloadRadix16.isNotEmpty
+ ? 'Company ID: ${data.companyIdRadix16}\nPayload: ${data.payloadRadix16}'
+ : 'Company ID: ${data.companyIdRadix16}';
+ Clipboard.setData(
+ ClipboardData(text: textToCopy),
+ );
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content:
+ const Text('Copied to clipboard'),
+ duration: const Duration(seconds: 1),
+ behavior: SnackBarBehavior.floating,
+ ),
+ );
+ },
+ tooltip: 'Copy',
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(
+ minWidth: 24,
+ minHeight: 24,
+ ),
+ visualDensity: VisualDensity.compact,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }),
+ const SizedBox(height: 12),
+ ],
+ if (bleDevice.services.isNotEmpty) ...[
+ Text(
+ 'Advertised Services',
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.bold,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Wrap(
+ spacing: 4,
+ runSpacing: 4,
+ children: bleDevice.services.map((service) {
+ return Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: colorScheme.tertiaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SelectableText(
+ service,
+ style: TextStyle(
+ fontSize: 10,
+ color: colorScheme.onTertiaryContainer,
+ fontWeight: FontWeight.w500,
+ fontFamily: 'monospace',
+ ),
+ ),
+ const SizedBox(width: 6),
+ InkWell(
+ onTap: () {
+ Clipboard.setData(ClipboardData(text: service));
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: const Text('Copied to clipboard'),
+ duration: const Duration(seconds: 1),
+ behavior: SnackBarBehavior.floating,
+ ),
+ );
+ },
+ child: Icon(
+ Icons.copy,
+ size: 14,
+ color: colorScheme.onTertiaryContainer
+ .withValues(alpha: 0.7),
+ ),
+ ),
+ ],
+ ),
+ );
+ }).toList(),
+ ),
+ ],
+ ],
],
),
- trailing: const Icon(Icons.arrow_forward_ios),
- onTap: onTap,
),
),
);
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 01f1d47..8aa7356 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,17 +1,47 @@
import 'package:flutter/material.dart';
import 'package:universal_ble/universal_ble.dart';
-import 'package:universal_ble_example/home/home.dart';
+import 'package:universal_ble_example/data/storage_service.dart';
+import 'package:universal_ble_example/home/permission_screen.dart';
+import 'package:universal_ble_example/home/scanner_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
- await UniversalBle.setLogLevel(BleLogLevel.verbose);
- runApp(
- MaterialApp(
+ await StorageService.instance.init();
+ // await UniversalBle.setLogLevel(BleLogLevel.verbose);
+ bool hasPermission = await UniversalBle.hasPermissions(
+ withAndroidFineLocation: false,
+ );
+
+ runApp(MyApp(
+ hasPermission: hasPermission,
+ ));
+}
+
+class MyApp extends StatelessWidget {
+ final bool hasPermission;
+ const MyApp({super.key, required this.hasPermission});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
title: 'Universal BLE',
debugShowCheckedModeBanner: false,
- darkTheme: ThemeData.dark(),
+ theme: ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: Colors.blue,
+ brightness: Brightness.light,
+ ),
+ ),
+ darkTheme: ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: Colors.blue,
+ brightness: Brightness.dark,
+ ),
+ ),
themeMode: ThemeMode.system,
- home: const MyApp(),
- ),
- );
+ home: hasPermission ? ScannerScreen() : const PermissionScreen(),
+ );
+ }
}
diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart
index d940b3f..105cb68 100644
--- a/example/lib/peripheral_details/peripheral_detail_page.dart
+++ b/example/lib/peripheral_details/peripheral_detail_page.dart
@@ -3,11 +3,12 @@ import 'dart:async';
import 'package:convert/convert.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:universal_ble/universal_ble.dart';
+import 'package:universal_ble_example/data/storage_service.dart';
import 'package:universal_ble_example/peripheral_details/widgets/result_widget.dart';
import 'package:universal_ble_example/peripheral_details/widgets/services_list_widget.dart';
-import 'package:universal_ble_example/widgets/platform_button.dart';
-import 'package:universal_ble_example/widgets/responsive_buttons_grid.dart';
+import 'package:universal_ble_example/peripheral_details/widgets/services_side_widget.dart';
import 'package:universal_ble_example/widgets/responsive_view.dart';
class PeripheralDetailPage extends StatefulWidget {
@@ -27,32 +28,47 @@ class _PeripheralDetailPageState extends State {
List discoveredServices = [];
final List _logs = [];
final binaryCode = TextEditingController();
+ bool _isLoading = false;
+ bool _isDeviceInfoExpanded = false;
+ bool _isDeviceActionsExpanded = true;
+ final Map _subscribedCharacteristics = {};
StreamSubscription? connectionStreamSubscription;
StreamSubscription? pairingStateSubscription;
BleService? selectedService;
BleCharacteristic? selectedCharacteristic;
+ final ScrollController _logsScrollController = ScrollController();
+ final Set _favoriteServices = {};
+
+ void _loadFavoriteServices() {
+ final favorites = StorageService.instance.getFavoriteServices();
+ setState(() {
+ _favoriteServices.addAll(favorites);
+ });
+ }
+
+ Future _saveFavoriteServices() async {
+ await StorageService.instance.setFavoriteServices(
+ _favoriteServices.toList(),
+ );
+ }
@override
void initState() {
super.initState();
- connectionStreamSubscription =
- bleDevice.connectionStream.listen(_handleConnectionChange);
- pairingStateSubscription =
- bleDevice.pairingStateStream.listen(_handlePairingStateChange);
+ connectionStreamSubscription = bleDevice.connectionStream.listen(
+ _handleConnectionChange,
+ );
+ pairingStateSubscription = bleDevice.pairingStateStream.listen(
+ _handlePairingStateChange,
+ );
UniversalBle.onValueChange = _handleValueChange;
- _asyncInits();
- }
- void _asyncInits() {
bleDevice.connectionState.then((state) {
- if (state == BleConnectionState.connected) {
- setState(() {
- isConnected = true;
- });
- }
+ _handleConnectionChange(state == BleConnectionState.connected);
});
+ _loadFavoriteServices();
}
@override
@@ -81,8 +97,12 @@ class _PeripheralDetailPageState extends State {
}
}
- void _handleValueChange(String deviceId, String characteristicId,
- Uint8List value, int? timestamp) {
+ void _handleValueChange(
+ String deviceId,
+ String characteristicId,
+ Uint8List value,
+ int? timestamp,
+ ) {
String s = String.fromCharCodes(value);
String data = '$s\nraw : ${value.toString()}';
DateTime? timestampDateTime = timestamp != null
@@ -100,39 +120,45 @@ class _PeripheralDetailPageState extends State {
Future _discoverServices() async {
const webWarning =
"Note: Only services added in ScanFilter or WebOptions will be discovered";
- try {
- var services = await bleDevice.discoverServices(withDescriptors: false);
- debugPrint('${services.length} services discovered');
- debugPrint(services.toString());
- setState(() {
- discoveredServices = services;
- });
- if (kIsWeb) {
- _addLog(
- "DiscoverServices",
- '${services.length} services discovered,\n$webWarning',
- );
- }
- } catch (e) {
- _addLog("DiscoverServicesError", '$e\n${kIsWeb ? webWarning : ""}');
- }
+ await _executeWithLoading(
+ () async {
+ var services = await bleDevice.discoverServices(withDescriptors: false);
+ debugPrint('${services.length} services discovered');
+ debugPrint(services.toString());
+ setState(() {
+ discoveredServices = services;
+ });
+ if (kIsWeb) {
+ _addLog(
+ "DiscoverServices",
+ '${services.length} services discovered,\n$webWarning',
+ );
+ }
+ },
+ onError: (error) {
+ _addLog("DiscoverServicesError", '$error\n${kIsWeb ? webWarning : ""}');
+ },
+ );
}
Future _readValue() async {
BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic;
if (selectedCharacteristic == null) return;
- try {
- Uint8List value = await selectedCharacteristic.read();
- String s = String.fromCharCodes(value);
- String data = '$s\nraw : ${value.toString()}';
- _addLog('Read', data);
- } catch (e) {
- _addLog('ReadError', e);
- }
+ await _executeWithLoading(
+ () async {
+ Uint8List value = await selectedCharacteristic.read();
+ String s = String.fromCharCodes(value);
+ String data = '$s\nraw : ${value.toString()}';
+ _addLog('Read', data);
+ },
+ onError: (error) {
+ _addLog('ReadError', error);
+ },
+ );
}
- Future _writeValue({required bool withResponse}) async {
+ Future _writeValue() async {
BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic;
if (selectedCharacteristic == null ||
!valueFormKey.currentState!.validate() ||
@@ -148,41 +174,102 @@ class _PeripheralDetailPageState extends State {
return;
}
- try {
- await selectedCharacteristic.write(value, withResponse: withResponse);
- _addLog('Write${withResponse ? "" : "WithoutResponse"}', value);
- } catch (e) {
- debugPrint(e.toString());
- _addLog('WriteError', e);
+ bool writeWithResponse = true;
+ if (!selectedCharacteristic.properties.contains(
+ CharacteristicProperty.write,
+ )) {
+ writeWithResponse = false;
}
+
+ await _executeWithLoading(
+ () async {
+ await selectedCharacteristic.write(
+ value,
+ withResponse: writeWithResponse,
+ );
+ _addLog('Write${writeWithResponse ? "" : "WithoutResponse"}', value);
+ },
+ onError: (error) {
+ _addLog('WriteError', error);
+ },
+ );
}
Future _subscribeChar() async {
BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic;
if (selectedCharacteristic == null) return;
- try {
- var subscription = _getCharacteristicSubscription(selectedCharacteristic);
- if (subscription == null) throw 'No notify or indicate property';
- await subscription.subscribe();
- _addLog('BleCharSubscription', 'Subscribed');
- // Updates can also be handled by
- // subscription.listen((data) {});
- } catch (e) {
- _addLog('NotifyError', e);
- }
+ await _executeWithLoading(
+ () async {
+ var subscription = _getCharacteristicSubscription(
+ selectedCharacteristic,
+ );
+ if (subscription == null) throw 'No notify or indicate property';
+ await subscription.subscribe();
+ setState(() {
+ _subscribedCharacteristics[selectedCharacteristic.uuid] = true;
+ });
+ _addLog('BleCharSubscription', 'Subscribed');
+ },
+ onError: (error) {
+ _addLog('NotifyError', error);
+ },
+ );
}
Future _unsubscribeChar() async {
- try {
- await selectedCharacteristic?.unsubscribe();
- _addLog('BleCharSubscription', 'UnSubscribed');
- } catch (e) {
- _addLog('NotifyError', e);
- }
+ final char = selectedCharacteristic;
+ if (char == null) return;
+ await _executeWithLoading(
+ () async {
+ await char.unsubscribe();
+ setState(() {
+ _subscribedCharacteristics.remove(char.uuid);
+ });
+ _addLog('BleCharSubscription', 'UnSubscribed');
+ },
+ onError: (error) {
+ _addLog('NotifyError', error);
+ },
+ );
+ }
+
+ Future _subscribeToAllCharacteristics() async {
+ if (!isConnected) return;
+ await _executeWithLoading(
+ () async {
+ int successCount = 0;
+ int errorCount = 0;
+ for (var service in discoveredServices) {
+ for (var characteristic in service.characteristics) {
+ var subscription = _getCharacteristicSubscription(characteristic);
+ if (subscription != null) {
+ try {
+ await subscription.subscribe();
+ setState(() {
+ _subscribedCharacteristics[characteristic.uuid] = true;
+ });
+ successCount++;
+ } catch (e) {
+ errorCount++;
+ debugPrint('Failed to subscribe to ${characteristic.uuid}: $e');
+ }
+ }
+ }
+ }
+ _addLog(
+ 'BleCharSubscription',
+ 'Subscribed to $successCount characteristics${errorCount > 0 ? ', $errorCount failed' : ''}',
+ );
+ },
+ onError: (error) {
+ _addLog('SubscribeToAllCharacteristicsError', error);
+ },
+ );
}
CharacteristicSubscription? _getCharacteristicSubscription(
- BleCharacteristic characteristic) {
+ BleCharacteristic characteristic,
+ ) {
var properties = characteristic.properties;
if (properties.contains(CharacteristicProperty.notify)) {
return characteristic.notifications;
@@ -192,306 +279,1279 @@ class _PeripheralDetailPageState extends State {
return null;
}
+ Future _executeWithLoading(
+ Future Function() action, {
+ Function(dynamic error)? onError,
+ }) async {
+ setState(() {
+ _isLoading = true;
+ });
+ try {
+ return await action();
+ } catch (e) {
+ onError?.call(e);
+ rethrow;
+ } finally {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+
+ void _showCharacteristicSelector() {
+ showModalBottomSheet(
+ context: context,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ builder: (context) {
+ return Padding(
+ padding: EdgeInsets.only(bottom: 10.0),
+ child: ServicesSideWidget(
+ discoveredServices: discoveredServices,
+ serviceListBuilder: () => _buildServicesList(onSelect: (_, __) {
+ Navigator.pop(context);
+ }),
+ ),
+ );
+ },
+ );
+ }
+
+ void _showSnackBar(String message) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(message),
+ ),
+ );
+ }
+ }
+
+ @override
+ void setState(VoidCallback fn) {
+ if (mounted) super.setState(fn);
+ }
+
@override
Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- title: Text("${bleDevice.name ?? "Unknown"} - ${bleDevice.deviceId}"),
- elevation: 4,
- actions: [
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: Icon(
- isConnected
- ? Icons.bluetooth_connected
- : Icons.bluetooth_disabled,
- color: isConnected ? Colors.greenAccent : Colors.red,
- size: 20,
+ final colorScheme = Theme.of(context).colorScheme;
+ return ResponsiveView(
+ builder: (_, DeviceType deviceType) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(
+ bleDevice.name ?? "Unknown Device",
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ letterSpacing: 0.5,
+ ),
),
- )
- ],
- ),
- body: ResponsiveView(builder: (_, DeviceType deviceType) {
- return Row(
- children: [
- if (deviceType == DeviceType.desktop)
- Expanded(
- flex: 1,
- child: Container(
- color: Theme.of(context).secondaryHeaderColor,
- child: discoveredServices.isEmpty
- ? const Center(
- child: Text('No Services Discovered'),
- )
- : ServicesListWidget(
- discoveredServices: discoveredServices,
- scrollable: true,
- onTap: (service, characteristic) {
- setState(() {
- selectedService = service;
- selectedCharacteristic = characteristic;
- });
- },
- ),
+ centerTitle: false,
+ elevation: 0,
+ actions: [
+ Visibility.maintain(
+ visible: _isLoading,
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(
+ colorScheme.primary,
+ ),
+ ),
+ ),
),
),
- Expanded(
- flex: 3,
- child: Align(
- alignment: Alignment.topCenter,
+ if (deviceType != DeviceType.desktop)
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Icon(
+ isConnected
+ ? Icons.bluetooth_connected
+ : Icons.bluetooth_disabled,
+ color: isConnected
+ ? Colors.green
+ : colorScheme.onSurface.withValues(alpha: 0.6),
+ size: 20,
+ ),
+ )
+ else
+ Container(
+ margin: const EdgeInsets.only(right: 16),
+ padding:
+ const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ decoration: BoxDecoration(
+ color: isConnected
+ ? Colors.green.withValues(alpha: 0.2)
+ : Colors.orange.withValues(alpha: 0.2),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ isConnected
+ ? Icons.check_circle
+ : Icons.bluetooth_disabled,
+ size: 16,
+ color: isConnected
+ ? Colors.green.shade700
+ : Colors.orange.shade700,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ isConnected ? 'Connected' : 'Disconnected',
+ style: TextStyle(
+ color: isConnected
+ ? Colors.green.shade700
+ : Colors.orange.shade700,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ body: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Services side widget on desktop
+ if (deviceType == DeviceType.desktop)
+ Expanded(
+ flex: 1,
+ child: ServicesSideWidget(
+ discoveredServices: discoveredServices,
+ serviceListBuilder: _buildServicesList,
+ ),
+ ),
+ // Main content
+ Expanded(
+ flex: 2,
child: SingleChildScrollView(
child: Column(
children: [
- // Top buttons
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- PlatformButton(
- text: 'Connect',
- enabled: !isConnected,
- onPressed: () async {
- try {
- await bleDevice.connect();
- _addLog("ConnectionResult", true);
- } catch (e) {
- _addLog('ConnectError (${e.runtimeType})', e);
- }
- },
- ),
- PlatformButton(
- text: 'Disconnect',
- enabled: isConnected,
- onPressed: () async {
- try {
- await bleDevice.disconnect();
- _addLog("DisconnectResult", true);
- } catch (e) {
- _addLog(
- 'DisconnectError (${e.runtimeType})',
- e,
- );
- }
- },
- ),
- ],
- ),
- ),
- selectedCharacteristic == null
- ? Text(discoveredServices.isEmpty
- ? "Please discover services"
- : "Please select a characteristic")
- : Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 8.0,
- ),
- child: Card(
- child: ListTile(
- title: SelectableText(
- "Characteristic: ${selectedCharacteristic?.uuid}",
- ),
- subtitle: Column(
- crossAxisAlignment:
- CrossAxisAlignment.start,
- children: [
- SelectableText(
- "Service: ${selectedService?.uuid}",
- ),
- Text(
- "Properties: ${selectedCharacteristic?.properties.map((e) => e.name)}",
- ),
- ],
- ),
- ),
- ),
- ),
+ // Device info with manufacturer data and advertised services
+ _buildDeviceInfo(),
- if (_hasSelectedCharacteristicProperty([
- CharacteristicProperty.write,
- CharacteristicProperty.writeWithoutResponse
- ]))
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
- child: Form(
- key: valueFormKey,
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: TextFormField(
- controller: binaryCode,
- validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter a value';
- }
- try {
- hex.decode(binaryCode.text);
- return null;
- } catch (e) {
- return 'Please enter a valid hex value ( without spaces or 0x (e.g. F0BB) )';
- }
- },
- decoration: const InputDecoration(
- hintText:
- "Enter Hex values without spaces or 0x (e.g. F0BB)",
- border: OutlineInputBorder(),
- ),
- ),
- ),
- ),
- ),
- const Divider(),
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: ResponsiveButtonsGrid(
- children: [
- PlatformButton(
- onPressed: () async {
- _discoverServices();
- },
- enabled: isConnected,
- text: 'Discover Services',
- ),
- PlatformButton(
- onPressed: () async {
- _addLog(
- 'ConnectionState',
- await bleDevice.connectionState,
- );
- },
- text: 'Connection State',
- ),
- if (BleCapabilities.supportsRequestMtuApi)
- PlatformButton(
- enabled: isConnected,
- onPressed: () async {
- int mtu = await bleDevice.requestMtu(247);
- _addLog('MTU', mtu);
- },
- text: 'Request Mtu',
- ),
- PlatformButton(
- enabled: isConnected &&
- discoveredServices.isNotEmpty &&
- _hasSelectedCharacteristicProperty([
- CharacteristicProperty.read,
- ]),
- onPressed: _readValue,
- text: 'Read',
- ),
- PlatformButton(
- enabled: isConnected &&
- discoveredServices.isNotEmpty &&
- _hasSelectedCharacteristicProperty([
- CharacteristicProperty.write,
- ]),
- onPressed: () => _writeValue(withResponse: true),
- text: 'Write',
- ),
- PlatformButton(
- enabled: isConnected &&
- discoveredServices.isNotEmpty &&
- _hasSelectedCharacteristicProperty([
- CharacteristicProperty.writeWithoutResponse,
- ]),
- onPressed: () => _writeValue(withResponse: false),
- text: 'WriteWithoutResponse',
- ),
- PlatformButton(
- enabled: isConnected &&
- discoveredServices.isNotEmpty &&
- _hasSelectedCharacteristicProperty([
- CharacteristicProperty.notify,
- CharacteristicProperty.indicate
- ]),
- onPressed: _subscribeChar,
- text: 'Subscribe',
- ),
- PlatformButton(
- enabled: isConnected &&
- discoveredServices.isNotEmpty &&
- _hasSelectedCharacteristicProperty([
- CharacteristicProperty.notify,
- CharacteristicProperty.indicate
- ]),
- onPressed: _unsubscribeChar,
- text: 'Unsubscribe',
- ),
- PlatformButton(
- enabled: BleCapabilities.supportsAllPairingKinds,
- onPressed: () async {
- try {
- await bleDevice.pair(
- // pairingCommand: BleCommand(
- // service: "",
- // characteristic: "",
- // ),
- );
- _addLog("Pairing Result", true);
- } catch (e) {
- _addLog('PairError (${e.runtimeType})', e);
- }
- },
- text: 'Pair',
- ),
- PlatformButton(
- onPressed: () async {
- bool? isPaired = await bleDevice.isPaired(
- // pairingCommand: BleCommand(
- // service: "",
- // characteristic: "",
- // ),
- );
- _addLog('isPaired', isPaired);
- },
- text: 'isPaired',
- ),
- PlatformButton(
- onPressed: () async {
- await bleDevice.unpair();
- },
- text: 'Unpair',
- ),
- ],
- ),
- ),
- // Services
- if (deviceType != DeviceType.desktop)
- ServicesListWidget(
- discoveredServices: discoveredServices,
- onTap: (service, characteristic) {
- setState(() {
- selectedService = service;
- selectedCharacteristic = characteristic;
- });
- },
- ),
- const Divider(),
- ResultWidget(
- results: _logs,
- onClearTap: (int? index) {
- setState(() {
- if (index != null) {
- _logs.removeAt(index);
- } else {
- _logs.clear();
- }
- });
- }),
+ // Connect/Disconnect button
+ _buildConnectDisconnectButton(),
+
+ // Device Actions
+ _buildDeviceActions(),
+
+ // Characteristic selector
+ _buildCharacteristicSelector(),
+
+ // Characteristic Actions
+ _buildCharacteristicActions(),
+
+ // Logs on bottom for mobile
+ if (deviceType != DeviceType.desktop) ...[
+ const Divider(),
+ _buildResultWidget(scrollable: false),
+ ],
const SizedBox(height: 20),
],
),
),
),
- ),
- ],
+
+ // Logs on right side for desktop
+ if (deviceType == DeviceType.desktop)
+ Expanded(
+ flex: 1,
+ child: _buildResultWidget(
+ scrollable: true,
+ ),
+ ),
+ ],
+ ),
);
- }),
+ },
+ );
+ }
+
+ Widget _buildCharacteristicActions() {
+ final colorScheme = Theme.of(context).colorScheme;
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 8.0,
+ ),
+ child: Card(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.tune,
+ color: colorScheme.primary,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Characteristic Actions',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Form(
+ key: valueFormKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ TextFormField(
+ controller: binaryCode,
+ enabled: isConnected &&
+ _hasSelectedCharacteristicProperty([
+ CharacteristicProperty.write,
+ CharacteristicProperty.writeWithoutResponse,
+ ]),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter a value';
+ }
+ try {
+ hex.decode(binaryCode.text);
+ return null;
+ } catch (e) {
+ return 'Please enter a valid hex value ( without spaces or 0x (e.g. F0BB) )';
+ }
+ },
+ decoration: InputDecoration(
+ hintText:
+ "Enter Hex values without spaces or 0x (e.g. F0BB)",
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ filled: true,
+ fillColor: colorScheme.surfaceContainerHighest,
+ prefixIcon: Icon(
+ Icons.code,
+ color: colorScheme.primary,
+ ),
+ ),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: ElevatedButton.icon(
+ onPressed: isConnected &&
+ _hasSelectedCharacteristicProperty([
+ CharacteristicProperty.write,
+ CharacteristicProperty
+ .writeWithoutResponse,
+ ])
+ ? _writeValue
+ : null,
+ icon: const Icon(Icons.send),
+ label: const Text('Write'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorScheme.primary,
+ foregroundColor: colorScheme.onPrimary,
+ padding: const EdgeInsets.symmetric(
+ vertical: 16,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: ElevatedButton.icon(
+ onPressed: isConnected &&
+ _hasSelectedCharacteristicProperty([
+ CharacteristicProperty.read,
+ ])
+ ? _readValue
+ : null,
+ icon: const Icon(Icons.download),
+ label: const Text('Read'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorScheme.secondary,
+ foregroundColor: colorScheme.onSecondary,
+ padding: const EdgeInsets.symmetric(
+ vertical: 16,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ Wrap(
+ spacing: 12,
+ runSpacing: 12,
+ children: [
+ ElevatedButton.icon(
+ onPressed: isConnected &&
+ discoveredServices.isNotEmpty &&
+ selectedCharacteristic != null &&
+ _hasSelectedCharacteristicProperty([
+ CharacteristicProperty.notify,
+ CharacteristicProperty.indicate,
+ ])
+ ? _subscribeChar
+ : null,
+ icon: const Icon(Icons.notifications_active),
+ label: const Text('Subscribe'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.green,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ OutlinedButton.icon(
+ onPressed: isConnected &&
+ discoveredServices.isNotEmpty &&
+ selectedCharacteristic != null &&
+ _hasSelectedCharacteristicProperty([
+ CharacteristicProperty.notify,
+ CharacteristicProperty.indicate,
+ ])
+ ? _unsubscribeChar
+ : null,
+ icon: const Icon(Icons.notifications_off),
+ label: const Text('Unsubscribe'),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: colorScheme.onSurface,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ ElevatedButton.icon(
+ onPressed: isConnected && discoveredServices.isNotEmpty
+ ? _subscribeToAllCharacteristics
+ : null,
+ icon: const Icon(Icons.notifications_active),
+ label: const Text('Subscribe All'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.green,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildConnectDisconnectButton() {
+ final colorScheme = Theme.of(context).colorScheme;
+ return SizedBox(
+ width: double.infinity,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 8.0,
+ ),
+ child: ElevatedButton.icon(
+ onPressed: () async {
+ if (isConnected) {
+ await _executeWithLoading(
+ () async {
+ await bleDevice.disconnect();
+ _addLog("DisconnectResult", true);
+ },
+ onError: (error) {
+ _addLog('DisconnectError', error);
+ },
+ );
+ } else {
+ await _executeWithLoading(
+ () async {
+ await bleDevice.connect();
+ _addLog("ConnectionResult", true);
+ },
+ onError: (error) {
+ _addLog(
+ 'ConnectError (${error.runtimeType})',
+ error,
+ );
+ },
+ );
+ }
+ },
+ icon: Icon(
+ isConnected ? Icons.bluetooth_disabled : Icons.bluetooth_connected,
+ size: 20,
+ ),
+ label: Text(isConnected ? 'Disconnect' : 'Connect'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor:
+ isConnected ? colorScheme.error : colorScheme.primary,
+ foregroundColor:
+ isConnected ? colorScheme.onError : colorScheme.onPrimary,
+ padding: const EdgeInsets.symmetric(
+ vertical: 16,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ elevation: 2,
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildDeviceInfo() {
+ final colorScheme = Theme.of(context).colorScheme;
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 2.0,
+ ),
+ child: Card(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Theme(
+ data: Theme.of(context).copyWith(
+ dividerColor: Colors.transparent,
+ ),
+ child: ExpansionTile(
+ initiallyExpanded: _isDeviceInfoExpanded,
+ onExpansionChanged: (expanded) {
+ setState(() {
+ _isDeviceInfoExpanded = expanded;
+ });
+ },
+ tilePadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ childrenPadding: const EdgeInsets.only(
+ left: 16,
+ right: 16,
+ bottom: 16,
+ ),
+ leading: Icon(
+ Icons.info_outline,
+ color: colorScheme.primary,
+ size: 24,
+ ),
+ title: Text(
+ 'Device Information',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ subtitle: !_isDeviceInfoExpanded
+ ? Padding(
+ padding: const EdgeInsets.only(top: 4),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Name: ${bleDevice.name ?? "Unknown"}',
+ style: TextStyle(
+ fontSize: 12,
+ color: colorScheme.onSurface.withValues(alpha: 0.7),
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ 'ID: ${bleDevice.deviceId}',
+ style: TextStyle(
+ fontSize: 11,
+ fontFamily: 'monospace',
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ )
+ : null,
+ expandedCrossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildInfoRow(
+ context,
+ 'Device ID',
+ bleDevice.deviceId,
+ Icons.fingerprint,
+ ),
+ const SizedBox(height: 12),
+ _buildInfoRow(
+ context,
+ 'Name',
+ bleDevice.name ?? "Unknown",
+ Icons.label,
+ ),
+ // Manufacturer data
+ if (bleDevice.manufacturerDataList.isNotEmpty) ...[
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Icon(
+ Icons.memory,
+ size: 18,
+ color: colorScheme.secondary,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Manufacturer Data',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ ...bleDevice.manufacturerDataList.map(
+ (data) => Padding(
+ padding: const EdgeInsets.only(
+ bottom: 8.0,
+ ),
+ child: Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: colorScheme.secondaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ 'Company ID: ',
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ color: colorScheme.onSecondaryContainer,
+ ),
+ ),
+ Expanded(
+ child: SelectableText(
+ data.companyIdRadix16,
+ style: TextStyle(
+ fontSize: 12,
+ fontFamily: 'monospace',
+ color: colorScheme.onSecondaryContainer,
+ ),
+ ),
+ ),
+ ],
+ ),
+ if (data.payloadRadix16.isNotEmpty) ...[
+ const SizedBox(height: 4),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Payload: ',
+ style: TextStyle(
+ fontSize: 11,
+ color: colorScheme.onSecondaryContainer
+ .withValues(alpha: 0.8),
+ ),
+ ),
+ Expanded(
+ child: SelectableText(
+ data.payloadRadix16,
+ style: TextStyle(
+ fontSize: 11,
+ fontFamily: 'monospace',
+ color: colorScheme.onSecondaryContainer
+ .withValues(alpha: 0.8),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ // Advertised services
+ if (bleDevice.services.isNotEmpty) ...[
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Icon(
+ Icons.list,
+ size: 18,
+ color: colorScheme.tertiary,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Advertised Services',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Wrap(
+ spacing: 6,
+ runSpacing: 6,
+ children: bleDevice.services
+ .map(
+ (service) => Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 6,
+ ),
+ decoration: BoxDecoration(
+ color: colorScheme.tertiaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: SelectableText(
+ service,
+ style: TextStyle(
+ fontSize: 11,
+ fontFamily: 'monospace',
+ color: colorScheme.onTertiaryContainer,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ )
+ .toList(),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildDeviceActions() {
+ final colorScheme = Theme.of(context).colorScheme;
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 2.0,
+ ),
+ child: Card(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Theme(
+ data: Theme.of(context).copyWith(
+ dividerColor: Colors.transparent,
+ ),
+ child: ExpansionTile(
+ initiallyExpanded: _isDeviceActionsExpanded,
+ onExpansionChanged: (expanded) {
+ setState(() {
+ _isDeviceActionsExpanded = expanded;
+ });
+ },
+ tilePadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ childrenPadding: const EdgeInsets.only(
+ left: 16,
+ right: 16,
+ bottom: 16,
+ ),
+ leading: Icon(
+ Icons.devices,
+ color: colorScheme.primary,
+ size: 24,
+ ),
+ title: Text(
+ 'Device Actions',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ expandedCrossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: double.infinity,
+ child: Wrap(
+ spacing: 6,
+ runSpacing: 2,
+ children: [
+ ElevatedButton.icon(
+ onPressed: isConnected
+ ? () async {
+ _discoverServices();
+ }
+ : null,
+ icon: const Icon(Icons.search),
+ label: const Text('Discover Services'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorScheme.primary,
+ foregroundColor: colorScheme.onPrimary,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ if (discoveredServices.isNotEmpty)
+ OutlinedButton.icon(
+ onPressed: () async {
+ final servicesText =
+ discoveredServices.map((s) => s.uuid).join('\n');
+ await Clipboard.setData(
+ ClipboardData(text: servicesText),
+ );
+ _showSnackBar('All services copied to clipboard');
+ },
+ icon: const Icon(Icons.copy),
+ label: const Text('Copy Services'),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: colorScheme.onSurface,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ OutlinedButton.icon(
+ onPressed: () async {
+ _addLog(
+ 'ConnectionState',
+ await bleDevice.connectionState,
+ );
+ },
+ icon: const Icon(Icons.info_outline),
+ label: const Text('State'),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: colorScheme.onSurface,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ if (BleCapabilities.supportsRequestMtuApi)
+ ElevatedButton.icon(
+ onPressed: isConnected
+ ? () async {
+ await _executeWithLoading(
+ () async {
+ int mtu = await bleDevice.requestMtu(247);
+ _addLog('MTU', mtu);
+ },
+ onError: (error) {
+ _addLog('RequestMtuError', error);
+ },
+ );
+ }
+ : null,
+ icon: const Icon(Icons.speed),
+ label: const Text('MTU'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorScheme.secondary,
+ foregroundColor: colorScheme.onSecondary,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ ElevatedButton.icon(
+ onPressed: BleCapabilities.supportsAllPairingKinds
+ ? () async {
+ await _executeWithLoading(
+ () async {
+ await bleDevice.pair();
+ _addLog("Pairing Result", true);
+ },
+ onError: (error) {
+ _addLog('PairError', error);
+ },
+ );
+ }
+ : null,
+ icon: const Icon(Icons.link),
+ label: const Text('Pair'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorScheme.tertiary,
+ foregroundColor: colorScheme.onTertiary,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ OutlinedButton.icon(
+ onPressed: () async {
+ await _executeWithLoading(
+ () async {
+ bool? isPaired = await bleDevice.isPaired();
+ _addLog('isPaired', isPaired);
+ },
+ onError: (error) {
+ _addLog('isPairedError', error);
+ },
+ );
+ },
+ icon: const Icon(Icons.check_circle),
+ label: const Text('Check Paired'),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: colorScheme.onSurface,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ OutlinedButton.icon(
+ onPressed: () async {
+ await _executeWithLoading(
+ () async {
+ await bleDevice.unpair();
+ },
+ onError: (error) {
+ _addLog('UnpairError', error);
+ },
+ );
+ },
+ icon: const Icon(Icons.link_off),
+ label: const Text('Unpair'),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: colorScheme.error,
+ side: BorderSide(
+ color: colorScheme.error,
+ ),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
);
}
bool _hasSelectedCharacteristicProperty(
- List properties) {
- return properties.any((property) =>
- selectedCharacteristic?.properties.contains(property) ?? false);
+ List properties,
+ ) {
+ return properties.any(
+ (property) =>
+ selectedCharacteristic?.properties.contains(property) ?? false,
+ );
+ }
+
+ Widget _buildInfoRow(
+ BuildContext context,
+ String label,
+ String value,
+ IconData icon,
+ ) {
+ final colorScheme = Theme.of(context).colorScheme;
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(
+ icon,
+ size: 18,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ label,
+ style: TextStyle(
+ fontSize: 12,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ const SizedBox(height: 2),
+ SelectableText(
+ value,
+ style: TextStyle(
+ fontSize: 14,
+ color: colorScheme.onSurface,
+ fontFamily: label == 'Device ID' ? 'monospace' : null,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildCharacteristicSelector() {
+ final colorScheme = Theme.of(context).colorScheme;
+ if (selectedCharacteristic == null) {
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: InkWell(
+ onTap: () {
+ if (discoveredServices.isEmpty) {
+ _discoverServices();
+ } else {
+ _showCharacteristicSelector();
+ }
+ },
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.info_outline,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ discoveredServices.isEmpty
+ ? "Please discover services"
+ : "Please select a characteristic to read/write",
+ style: TextStyle(
+ fontStyle: FontStyle.italic,
+ color: colorScheme.onSurface.withValues(alpha: 0.7),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ } else {
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 8.0,
+ ),
+ child: _buildSelectedCharacteristicCard(),
+ );
+ }
+ }
+
+ Widget _buildServicesList({
+ Function(BleService, BleCharacteristic?)? onSelect,
+ }) {
+ return ServicesListWidget(
+ discoveredServices: discoveredServices,
+ selectedService: selectedService,
+ selectedCharacteristic: selectedCharacteristic,
+ favoriteServices: _favoriteServices,
+ subscribedCharacteristics: _subscribedCharacteristics,
+ scrollable: true,
+ onTap: (service, characteristic) {
+ setState(() {
+ selectedService = service;
+ selectedCharacteristic = characteristic;
+ });
+ onSelect?.call(service, characteristic);
+ },
+ onFavoriteToggle: (serviceUuid) {
+ setState(() {
+ if (_favoriteServices.contains(
+ serviceUuid,
+ )) {
+ _favoriteServices.remove(serviceUuid);
+ } else {
+ _favoriteServices.add(serviceUuid);
+ }
+ });
+ _saveFavoriteServices();
+ },
+ );
+ }
+
+ Widget _buildSelectedCharacteristicCard() {
+ final colorScheme = Theme.of(context).colorScheme;
+ if (selectedCharacteristic == null) return const SizedBox.shrink();
+ return Card(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ color: colorScheme.primaryContainer,
+ child: InkWell(
+ onTap: _showCharacteristicSelector,
+ borderRadius: BorderRadius.circular(12),
+ child: Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(
+ Icons.settings,
+ size: 16,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ const SizedBox(width: 6),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ 'Characteristic',
+ style: TextStyle(
+ fontSize: 11,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Row(
+ children: [
+ Expanded(
+ child: InkWell(
+ onTap: () {
+ Clipboard.setData(ClipboardData(
+ text: selectedCharacteristic?.uuid ?? "",
+ ));
+ _showSnackBar('Copied to clipboard');
+ },
+ child: Text(
+ selectedCharacteristic!.uuid,
+ style: TextStyle(
+ fontSize: 12,
+ fontFamily: 'monospace',
+ color: colorScheme.onSurface,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ Icon(
+ Icons.arrow_drop_down,
+ size: 18,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(
+ Icons.apps,
+ size: 16,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ const SizedBox(width: 6),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ 'Service',
+ style: TextStyle(
+ fontSize: 11,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ const SizedBox(height: 2),
+ InkWell(
+ onTap: () {
+ Clipboard.setData(ClipboardData(
+ text: selectedService?.uuid ?? "",
+ ));
+ _showSnackBar('Copied to clipboard');
+ },
+ child: Text(
+ selectedService?.uuid ?? "Unknown",
+ style: TextStyle(
+ fontSize: 12,
+ fontFamily: 'monospace',
+ color: colorScheme.onSurface,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(
+ Icons.tune,
+ size: 16,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ const SizedBox(width: 6),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ 'Properties',
+ style: TextStyle(
+ fontSize: 11,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Wrap(
+ spacing: 4,
+ runSpacing: 4,
+ children:
+ selectedCharacteristic!.properties.map((prop) {
+ return Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 6,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: colorScheme.secondaryContainer,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ prop.name,
+ style: TextStyle(
+ fontSize: 10,
+ color: colorScheme.onSecondaryContainer,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildResultWidget({
+ required bool scrollable,
+ }) {
+ return ResultWidget(
+ results: _logs,
+ scrollController: _logsScrollController,
+ scrollable: scrollable,
+ onClearTap: (int? index) {
+ setState(() {
+ if (index != null) {
+ _logs.removeAt(index);
+ } else {
+ _logs.clear();
+ }
+ });
+ },
+ );
}
}
diff --git a/example/lib/peripheral_details/widgets/result_widget.dart b/example/lib/peripheral_details/widgets/result_widget.dart
index 0a43046..a0fbed9 100644
--- a/example/lib/peripheral_details/widgets/result_widget.dart
+++ b/example/lib/peripheral_details/widgets/result_widget.dart
@@ -3,48 +3,196 @@ import 'package:flutter/material.dart';
class ResultWidget extends StatelessWidget {
final List results;
final bool scrollable;
+ final ScrollController scrollController;
final void Function(int? index) onClearTap;
const ResultWidget({
required this.results,
required this.onClearTap,
this.scrollable = false,
+ required this.scrollController,
super.key,
});
@override
Widget build(BuildContext context) {
- return Column(
- children: [
- if (results.isNotEmpty)
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: ListTile(
- tileColor: Theme.of(context).secondaryHeaderColor,
- title: const Text("Logs"),
- onTap: () {
- onClearTap(null);
- },
- trailing: const Icon(Icons.clear),
+ final colorScheme = Theme.of(context).colorScheme;
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Card(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: scrollable ? MainAxisSize.max : MainAxisSize.min,
+ children: [
+ _buildHeader(colorScheme),
+ if (results.isEmpty)
+ _buildEmptyState(colorScheme)
+ else
+ _buildLogsList(colorScheme),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader(ColorScheme colorScheme) {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: colorScheme.surfaceContainerHighest,
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(12),
+ topRight: Radius.circular(12),
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.history,
+ color: colorScheme.primary,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Logs',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ color: colorScheme.onSurface,
),
),
- ListView.separated(
- shrinkWrap: !scrollable,
- physics: scrollable ? null : const NeverScrollableScrollPhysics(),
- itemCount: results.length,
- reverse: true,
- itemBuilder: (BuildContext context, int index) {
- return InkWell(
- onTap: () => onClearTap(index),
- child: Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 11, vertical: 2),
- child: Text(results[index]),
+ const Spacer(),
+ if (results.isNotEmpty)
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
),
- );
- },
- separatorBuilder: (_, __) => const Divider(),
+ decoration: BoxDecoration(
+ color: colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ '${results.length}',
+ style: TextStyle(
+ color: colorScheme.onPrimaryContainer,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ if (results.isNotEmpty) ...[
+ const SizedBox(width: 8),
+ IconButton(
+ icon: Icon(
+ Icons.clear_all,
+ color: colorScheme.error,
+ size: 20,
+ ),
+ onPressed: () => onClearTap(null),
+ tooltip: 'Clear all logs',
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(
+ minWidth: 32,
+ minHeight: 32,
+ ),
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+
+ Widget _buildEmptyState(ColorScheme colorScheme) {
+ final emptyStateContent = Padding(
+ padding: const EdgeInsets.all(32.0),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.history_outlined,
+ size: 48,
+ color: colorScheme.onSurface.withValues(alpha: 0.3),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'No logs yet',
+ style: TextStyle(
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ ),
+ ],
),
- ],
+ ),
+ );
+
+ return scrollable ? Expanded(child: emptyStateContent) : emptyStateContent;
+ }
+
+ Widget _buildLogsList(ColorScheme colorScheme) {
+ final listView = ListView.separated(
+ physics: const AlwaysScrollableScrollPhysics(),
+ controller: scrollController,
+ itemCount: results.length,
+ itemBuilder: (context, index) => _buildLogItem(colorScheme, index),
+ separatorBuilder: (_, __) => Divider(
+ height: 1,
+ color: colorScheme.outline.withValues(alpha: 0.2),
+ ),
+ );
+
+ if (scrollable) {
+ return Expanded(child: listView);
+ } else {
+ return Container(
+ constraints: const BoxConstraints(maxHeight: 300),
+ child: listView,
+ );
+ }
+ }
+
+ Widget _buildLogItem(ColorScheme colorScheme, int index) {
+ var reversedIndex = results.length - index - 1;
+ final log = results[reversedIndex];
+ return InkWell(
+ onTap: () => onClearTap(reversedIndex),
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(
+ Icons.circle,
+ size: 6,
+ color: colorScheme.primary.withValues(alpha: 0.6),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: SelectableText(
+ log,
+ style: TextStyle(
+ fontSize: 12,
+ fontFamily: 'monospace',
+ color: colorScheme.onSurface,
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Icon(
+ Icons.close,
+ size: 16,
+ color: colorScheme.onSurface.withValues(alpha: 0.4),
+ ),
+ ],
+ ),
+ ),
);
}
}
diff --git a/example/lib/peripheral_details/widgets/services_list_widget.dart b/example/lib/peripheral_details/widgets/services_list_widget.dart
index 15ee23b..ca81612 100644
--- a/example/lib/peripheral_details/widgets/services_list_widget.dart
+++ b/example/lib/peripheral_details/widgets/services_list_widget.dart
@@ -1,78 +1,222 @@
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:universal_ble/universal_ble.dart';
+import 'package:universal_ble_example/data/utils.dart';
class ServicesListWidget extends StatelessWidget {
final List discoveredServices;
final bool scrollable;
- final void Function(
- BleService service,
- BleCharacteristic characteristic,
- )? onTap;
+ final void Function(BleService service, BleCharacteristic characteristic)?
+ onTap;
+ final BleService? selectedService;
+ final BleCharacteristic? selectedCharacteristic;
+ final Set? favoriteServices;
+ final Map? subscribedCharacteristics;
+ final void Function(String serviceUuid)? onFavoriteToggle;
const ServicesListWidget({
super.key,
required this.discoveredServices,
this.onTap,
this.scrollable = false,
+ this.selectedService,
+ this.selectedCharacteristic,
+ this.favoriteServices,
+ this.subscribedCharacteristics,
+ this.onFavoriteToggle,
});
@override
Widget build(BuildContext context) {
+ final colorScheme = Theme.of(context).colorScheme;
+ final favoriteStarColor = Colors.amber;
+ final subscribedNotificationIconColor = Colors.green;
+ final selectedColor = colorScheme.primary;
+ final selectedCharacteristicBackgroundColor =
+ colorScheme.primaryContainer.withValues(alpha: 0.5);
+
+ // Sort services: favorites first, then system services, then others
+ final sortedServices = List.from(discoveredServices);
+ sortedServices.sort((a, b) {
+ final aIsFavorite = favoriteServices?.contains(a.uuid) ?? false;
+ final bIsFavorite = favoriteServices?.contains(b.uuid) ?? false;
+ if (aIsFavorite != bIsFavorite) {
+ return aIsFavorite ? -1 : 1;
+ }
+ final aIsSystem = isSystemService(a.uuid);
+ final bIsSystem = isSystemService(b.uuid);
+ if (aIsSystem != bIsSystem) {
+ return aIsSystem ? 1 : -1;
+ }
+ return 0;
+ });
return ListView.builder(
shrinkWrap: !scrollable,
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
- itemCount: discoveredServices.length,
+ itemCount: sortedServices.length,
itemBuilder: (BuildContext context, int index) {
+ final service = sortedServices[index];
+ final isFavorite = favoriteServices?.contains(service.uuid) ?? false;
+ final isSelected = selectedService?.uuid == service.uuid;
return Padding(
- padding: const EdgeInsets.all(8.0),
+ padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Card(
+ elevation: isSelected ? 2 : 1,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ side: isSelected
+ ? BorderSide(color: selectedColor, width: 2)
+ : BorderSide.none,
+ ),
child: ExpandablePanel(
header: Padding(
- padding: const EdgeInsets.all(8.0),
+ padding: const EdgeInsets.all(12.0),
child: Row(
children: [
- const Icon(Icons.arrow_forward_ios),
- Expanded(child: Text(discoveredServices[index].uuid)),
+ Icon(
+ Icons.arrow_forward_ios,
+ size: 14,
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ service.uuid,
+ style: TextStyle(
+ fontSize: 13,
+ fontFamily: 'monospace',
+ color: colorScheme.onSurface,
+ fontWeight:
+ isSelected ? FontWeight.bold : FontWeight.w500,
+ ),
+ ),
+ ),
+ if (onFavoriteToggle != null) ...[
+ const SizedBox(width: 8),
+ IconButton(
+ icon: Icon(
+ isFavorite ? Icons.star : Icons.star_border,
+ color: isFavorite
+ ? favoriteStarColor
+ : colorScheme.onSurface.withValues(alpha: 0.4),
+ ),
+ onPressed: () => onFavoriteToggle!(service.uuid),
+ iconSize: 20,
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(
+ minWidth: 32,
+ minHeight: 32,
+ ),
+ ),
+ ],
],
),
),
collapsed: const SizedBox(),
- expanded: Column(
- children: discoveredServices[index]
- .characteristics
- .map((e) => Padding(
- padding: const EdgeInsets.all(8.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- InkWell(
- onTap: () {
- onTap?.call(discoveredServices[index], e);
- },
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+ expanded: Padding(
+ padding: const EdgeInsets.only(bottom: 8.0),
+ child: Column(
+ children: service.characteristics.map((e) {
+ final isCharSelected =
+ selectedCharacteristic?.uuid == e.uuid;
+ final isSubscribed =
+ subscribedCharacteristics?[e.uuid] ?? false;
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8.0,
+ vertical: 4.0,
+ ),
+ child: Container(
+ decoration: BoxDecoration(
+ color: isCharSelected
+ ? selectedCharacteristicBackgroundColor
+ : colorScheme.surfaceContainerHighest
+ .withValues(alpha: 0.3),
+ borderRadius: BorderRadius.circular(8),
+ border: isCharSelected
+ ? Border.all(color: selectedColor, width: 1.5)
+ : null,
+ ),
+ child: InkWell(
+ onTap: () {
+ onTap?.call(service, e);
+ },
+ borderRadius: BorderRadius.circular(8),
+ child: Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
children: [
- Row(
- children: [
- const Icon(Icons.arrow_right_outlined),
- Expanded(child: Text(e.uuid)),
- ],
+ Icon(
+ Icons.arrow_right_outlined,
+ size: 16,
+ color: isCharSelected
+ ? selectedColor
+ : colorScheme.onSurface
+ .withValues(alpha: 0.6),
),
- Text(
- "Properties: ${e.properties.map((e) => e.name)}",
+ const SizedBox(width: 8),
+ if (isSubscribed) ...[
+ Icon(
+ Icons.notifications_active,
+ color: subscribedNotificationIconColor,
+ size: 16,
+ ),
+ const SizedBox(width: 4),
+ ],
+ Expanded(
+ child: Text(
+ e.uuid,
+ style: TextStyle(
+ fontSize: 12,
+ fontFamily: 'monospace',
+ fontWeight: isCharSelected
+ ? FontWeight.bold
+ : FontWeight.w500,
+ color: isCharSelected
+ ? selectedColor
+ : colorScheme.onSurface,
+ ),
+ ),
),
- if (e.descriptors.isNotEmpty)
- Text(
- "Descriptors: ${e.descriptors.map((e) => e.uuid).join(', ')}",
- )
],
),
- ),
- ],
+ const SizedBox(height: 8),
+ Wrap(
+ spacing: 6,
+ runSpacing: 6,
+ children: e.properties.map((prop) {
+ return Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 6,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: colorScheme.secondaryContainer,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ prop.name,
+ style: TextStyle(
+ fontSize: 10,
+ color:
+ colorScheme.onSecondaryContainer,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ ],
+ ),
),
- ))
- .toList(),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
),
),
),
diff --git a/example/lib/peripheral_details/widgets/services_side_widget.dart b/example/lib/peripheral_details/widgets/services_side_widget.dart
new file mode 100644
index 0000000..1d7be01
--- /dev/null
+++ b/example/lib/peripheral_details/widgets/services_side_widget.dart
@@ -0,0 +1,105 @@
+import 'package:flutter/material.dart';
+import 'package:universal_ble/universal_ble.dart';
+
+class ServicesSideWidget extends StatelessWidget {
+ final List discoveredServices;
+ final Function() serviceListBuilder;
+ const ServicesSideWidget({
+ super.key,
+ required this.discoveredServices,
+ required this.serviceListBuilder,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = Theme.of(context).colorScheme;
+ return Container(
+ decoration: BoxDecoration(
+ color: colorScheme.surfaceContainerHighest,
+ border: Border(
+ right: BorderSide(
+ color: colorScheme.outline.withValues(alpha: 0.2),
+ width: 1,
+ ),
+ ),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: colorScheme.surface,
+ border: Border(
+ bottom: BorderSide(
+ color: colorScheme.outline.withValues(alpha: 0.2),
+ width: 1,
+ ),
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.apps,
+ color: colorScheme.primary,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Services',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ const Spacer(),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ '${discoveredServices.length}',
+ style: TextStyle(
+ color: colorScheme.onPrimaryContainer,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Expanded(
+ child: discoveredServices.isEmpty
+ ? Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.apps_outlined,
+ size: 48,
+ color: colorScheme.onSurface.withValues(alpha: 0.3),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'No Services Discovered',
+ style: TextStyle(
+ color: colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
+ ),
+ ],
+ ),
+ )
+ : serviceListBuilder(),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt
index b375099..76485b0 100644
--- a/example/linux/CMakeLists.txt
+++ b/example/linux/CMakeLists.txt
@@ -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 "universal_ble_example")
+set(BINARY_NAME "universal_ble")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
-set(APPLICATION_ID "com.navideck.universal_ble")
+set(APPLICATION_ID "com.navideck.universalble")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc
index e71a16d..f6f23bf 100644
--- a/example/linux/flutter/generated_plugin_registrant.cc
+++ b/example/linux/flutter/generated_plugin_registrant.cc
@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
+#include
void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
+ url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}
diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake
index 2e1de87..f16b4c3 100644
--- a/example/linux/flutter/generated_plugins.cmake
+++ b/example/linux/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc
index a3e4f92..cd68647 100644
--- a/example/linux/my_application.cc
+++ b/example/linux/my_application.cc
@@ -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, "universal_ble_example");
+ gtk_header_bar_set_title(header_bar, "Universal BLE");
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, "universal_ble_example");
+ gtk_window_set_title(window, "Universal BLE");
}
gtk_window_set_default_size(window, 1280, 720);
diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift
index 7ddba83..26eafea 100644
--- a/example/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,8 +5,14 @@
import FlutterMacOS
import Foundation
+import package_info_plus
+import shared_preferences_foundation
import universal_ble
+import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
+ SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin"))
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock
index 0c1b92b..6ef0555 100644
--- a/example/macos/Podfile.lock
+++ b/example/macos/Podfile.lock
@@ -1,22 +1,41 @@
PODS:
- FlutterMacOS (1.0.0)
+ - package_info_plus (0.0.1):
+ - FlutterMacOS
+ - shared_preferences_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
- universal_ble (0.0.1):
- Flutter
- FlutterMacOS
+ - url_launcher_macos (0.0.1):
+ - FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
+ - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
+ - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`)
+ - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
+ package_info_plus:
+ :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
+ shared_preferences_foundation:
+ :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
universal_ble:
:path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin
+ url_launcher_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
+ package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
+ shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92
+ url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce
PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3
diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj
index 6fa729e..0415455 100644
--- a/example/macos/Runner.xcodeproj/project.pbxproj
+++ b/example/macos/Runner.xcodeproj/project.pbxproj
@@ -68,7 +68,7 @@
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
- 33CC10ED2044A3C60003C045 /* universal_ble_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = universal_ble_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10ED2044A3C60003C045 /* Universal BLE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Universal BLE.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
@@ -144,7 +144,7 @@
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
- 33CC10ED2044A3C60003C045 /* universal_ble_example.app */,
+ 33CC10ED2044A3C60003C045 /* Universal BLE.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
@@ -248,7 +248,7 @@
);
name = Runner;
productName = Runner;
- productReference = 33CC10ED2044A3C60003C045 /* universal_ble_example.app */;
+ productReference = 33CC10ED2044A3C60003C045 /* Universal BLE.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -477,10 +477,10 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests;
+ PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Universal BLE.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Universal BLE";
};
name = Debug;
};
@@ -492,10 +492,10 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests;
+ PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Universal BLE.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Universal BLE";
};
name = Release;
};
@@ -507,10 +507,10 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests;
+ PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Universal BLE.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Universal BLE";
};
name = Profile;
};
diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index b3b52f0..5a50511 100644
--- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -15,7 +15,7 @@
@@ -31,7 +31,7 @@
@@ -66,7 +66,7 @@
@@ -83,7 +83,7 @@
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index a2ec33f..96d3fee 100644
--- a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,68 +1,68 @@
{
- "images" : [
- {
- "size" : "16x16",
- "idiom" : "mac",
- "filename" : "app_icon_16.png",
- "scale" : "1x"
+ "info": {
+ "version": 1,
+ "author": "xcode"
},
- {
- "size" : "16x16",
- "idiom" : "mac",
- "filename" : "app_icon_32.png",
- "scale" : "2x"
- },
- {
- "size" : "32x32",
- "idiom" : "mac",
- "filename" : "app_icon_32.png",
- "scale" : "1x"
- },
- {
- "size" : "32x32",
- "idiom" : "mac",
- "filename" : "app_icon_64.png",
- "scale" : "2x"
- },
- {
- "size" : "128x128",
- "idiom" : "mac",
- "filename" : "app_icon_128.png",
- "scale" : "1x"
- },
- {
- "size" : "128x128",
- "idiom" : "mac",
- "filename" : "app_icon_256.png",
- "scale" : "2x"
- },
- {
- "size" : "256x256",
- "idiom" : "mac",
- "filename" : "app_icon_256.png",
- "scale" : "1x"
- },
- {
- "size" : "256x256",
- "idiom" : "mac",
- "filename" : "app_icon_512.png",
- "scale" : "2x"
- },
- {
- "size" : "512x512",
- "idiom" : "mac",
- "filename" : "app_icon_512.png",
- "scale" : "1x"
- },
- {
- "size" : "512x512",
- "idiom" : "mac",
- "filename" : "app_icon_1024.png",
- "scale" : "2x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
+ "images": [
+ {
+ "size": "16x16",
+ "idiom": "mac",
+ "filename": "app_icon_16.png",
+ "scale": "1x"
+ },
+ {
+ "size": "16x16",
+ "idiom": "mac",
+ "filename": "app_icon_32.png",
+ "scale": "2x"
+ },
+ {
+ "size": "32x32",
+ "idiom": "mac",
+ "filename": "app_icon_32.png",
+ "scale": "1x"
+ },
+ {
+ "size": "32x32",
+ "idiom": "mac",
+ "filename": "app_icon_64.png",
+ "scale": "2x"
+ },
+ {
+ "size": "128x128",
+ "idiom": "mac",
+ "filename": "app_icon_128.png",
+ "scale": "1x"
+ },
+ {
+ "size": "128x128",
+ "idiom": "mac",
+ "filename": "app_icon_256.png",
+ "scale": "2x"
+ },
+ {
+ "size": "256x256",
+ "idiom": "mac",
+ "filename": "app_icon_256.png",
+ "scale": "1x"
+ },
+ {
+ "size": "256x256",
+ "idiom": "mac",
+ "filename": "app_icon_512.png",
+ "scale": "2x"
+ },
+ {
+ "size": "512x512",
+ "idiom": "mac",
+ "filename": "app_icon_512.png",
+ "scale": "1x"
+ },
+ {
+ "size": "512x512",
+ "idiom": "mac",
+ "filename": "app_icon_1024.png",
+ "scale": "2x"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
index 82b6f9d..d2e2066 100644
Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
index 13b35eb..e3f08fe 100644
Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
index 0a3f5fa..080adb6 100644
Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
index bdb5722..4201728 100644
Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
index f083318..3bee483 100644
Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
index 326c0e7..8925dc9 100644
Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
index 2f1632c..b4bccda 100644
Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig
index d550e14..96515de 100644
--- a/example/macos/Runner/Configs/AppInfo.xcconfig
+++ b/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -5,10 +5,10 @@
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
-PRODUCT_NAME = universal_ble_example
+PRODUCT_NAME = Universal BLE
// The application's bundle identifier
-PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample
+PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2023 com.navideck. All rights reserved.
diff --git a/example/package_rename_config.yaml b/example/package_rename_config.yaml
new file mode 100644
index 0000000..252744b
--- /dev/null
+++ b/example/package_rename_config.yaml
@@ -0,0 +1,32 @@
+package_rename_config:
+ android:
+ app_name: "Universal BLE"
+ package_name: "com.navideck.universalble"
+ override_old_package: "com.navideck.universal_ble_example"
+ lang: "kotlin"
+
+ ios:
+ app_name: "Universal BLE"
+ bundle_name: "Universal BLE"
+ package_name: "com.navideck.universalble"
+
+ linux:
+ app_name: "Universal BLE"
+ package_name: "com.navideck.universalble"
+ exe_name: "universal_ble"
+
+ macos:
+ app_name: "Universal BLE"
+ package_name: "com.navideck.universalble"
+ copyright_notice: "Copyright © 2023 com.navideck. All rights reserved."
+
+ windows:
+ app_name: "Universal BLE"
+ organization: "Navideck"
+ copyright_notice: "Copyright © 2023 com.navideck. All rights reserved."
+ exe_name: "universal_ble"
+
+ web:
+ app_name: "Universal BLE"
+ short_app_name: "Universal BLE"
+ description: "Universal BLE"
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 97d521a..11177ac 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.7"
args:
dependency: transitive
description:
@@ -41,6 +49,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.4"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.2"
clock:
dependency: transitive
description:
@@ -65,6 +89,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.7"
+ csslib:
+ dependency: transitive
+ description:
+ name: csslib
+ sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
@@ -123,6 +163,15 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_launcher_icons:
+ dependency: "direct dev"
+ description:
+ path: "."
+ ref: improve-windows-ico-generation
+ resolved-ref: d623fac15f257f06bfc21ed7afdd0b6f2739ea21
+ url: "https://github.com/Navideck/flutter_launcher_icons.git"
+ source: git
+ version: "0.14.3"
flutter_lints:
dependency: "direct dev"
description:
@@ -144,16 +193,61 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.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"
+ html:
+ dependency: transitive
+ description:
+ name: html
+ sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.15.6"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.6.0"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.2"
+ image:
+ dependency: transitive
+ description:
+ name: image
+ sha256: "51555e36056541237b15b57afc31a0f53d4f9aefd9bd00873a6dc0090e54e332"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.6.0"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.0"
leak_tracker:
dependency: transitive
description:
@@ -186,6 +280,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
+ logger:
+ dependency: transitive
+ description:
+ name: logger
+ sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.6.2"
logging:
dependency: transitive
description:
@@ -218,6 +320,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.17.0"
+ package_info_plus:
+ dependency: "direct main"
+ description:
+ name: package_info_plus
+ sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.0.0"
+ package_info_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: package_info_plus_platform_interface
+ sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.1"
+ package_rename:
+ dependency: "direct dev"
+ description:
+ name: package_rename
+ sha256: "8e957670ab3c8ab0aa9976d10dfe8575c279e81a48aeeca9e7de43dbdc2c2be0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.1"
path:
dependency: transitive
description:
@@ -226,6 +352,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
+ path_provider_linux:
+ dependency: transitive
+ description:
+ name: path_provider_linux
+ sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.1"
+ path_provider_platform_interface:
+ dependency: transitive
+ description:
+ name: path_provider_platform_interface
+ sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ path_provider_windows:
+ dependency: transitive
+ description:
+ name: path_provider_windows
+ sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.0"
petitparser:
dependency: transitive
description:
@@ -250,6 +400,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
+ posix:
+ dependency: transitive
+ description:
+ name: posix
+ sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.3"
process:
dependency: transitive
description:
@@ -258,6 +416,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.5"
+ shared_preferences:
+ dependency: "direct main"
+ description:
+ name: shared_preferences
+ sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.4"
+ shared_preferences_android:
+ dependency: transitive
+ description:
+ name: shared_preferences_android
+ sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.15"
+ shared_preferences_foundation:
+ dependency: transitive
+ description:
+ name: shared_preferences_foundation
+ sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.5"
+ shared_preferences_linux:
+ dependency: transitive
+ description:
+ name: shared_preferences_linux
+ sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ shared_preferences_platform_interface:
+ dependency: transitive
+ description:
+ name: shared_preferences_platform_interface
+ sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ shared_preferences_web:
+ dependency: transitive
+ description:
+ name: shared_preferences_web
+ sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.3"
+ shared_preferences_windows:
+ dependency: transitive
+ description:
+ name: shared_preferences_windows
+ sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
@@ -334,6 +548,70 @@ packages:
relative: true
source: path
version: "1.0.0"
+ url_launcher:
+ dependency: "direct main"
+ description:
+ name: url_launcher
+ sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.2"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.28"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.6"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.2"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.5"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.5"
vector_math:
dependency: transitive
description:
@@ -366,6 +644,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.15.0"
+ xdg_directories:
+ dependency: transitive
+ description:
+ name: xdg_directories
+ sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
xml:
dependency: transitive
description:
@@ -374,6 +668,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.6.1"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.3"
sdks:
- dart: ">=3.8.0 <4.0.0"
- flutter: ">=3.18.0-18.0.pre.54"
+ dart: ">=3.9.0 <4.0.0"
+ flutter: ">=3.35.0"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 1e012a2..31e737a 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -12,6 +12,9 @@ dependencies:
convert: ^3.1.1
expandable: ^5.0.1
cupertino_icons: ^1.0.2
+ shared_preferences: ^2.5.4
+ package_info_plus: ^9.0.0
+ url_launcher: ^6.3.2
universal_ble:
path: ../
@@ -21,6 +24,13 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
+ package_rename: ^1.10.1
+ flutter_launcher_icons:
+ git:
+ url: https://github.com/Navideck/flutter_launcher_icons.git
+ ref: improve-windows-ico-generation
flutter:
uses-material-design: true
+ assets:
+ - assets/icon.png
diff --git a/example/web/favicon.png b/example/web/favicon.png
index 8aaa46a..080adb6 100644
Binary files a/example/web/favicon.png and b/example/web/favicon.png differ
diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png
index b749bfe..9f765bd 100644
Binary files a/example/web/icons/Icon-192.png and b/example/web/icons/Icon-192.png differ
diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png
index 88cfd48..8925dc9 100644
Binary files a/example/web/icons/Icon-512.png and b/example/web/icons/Icon-512.png differ
diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png
index eb9b4d7..9f765bd 100644
Binary files a/example/web/icons/Icon-maskable-192.png and b/example/web/icons/Icon-maskable-192.png differ
diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png
index d69c566..8925dc9 100644
Binary files a/example/web/icons/Icon-maskable-512.png and b/example/web/icons/Icon-maskable-512.png differ
diff --git a/example/web/index.html b/example/web/index.html
index 26f5da2..4ce3164 100644
--- a/example/web/index.html
+++ b/example/web/index.html
@@ -1,6 +1,4 @@
-
-
-
+
-
+
-
+
- example
+ Universal BLE
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/example/web/manifest.json b/example/web/manifest.json
index 8bc88df..d61d7d4 100644
--- a/example/web/manifest.json
+++ b/example/web/manifest.json
@@ -5,7 +5,7 @@
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
- "description": "Example app for Web",
+ "description": "Universal BLE",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt
index 0937d82..5156bbb 100644
--- a/example/windows/CMakeLists.txt
+++ b/example/windows/CMakeLists.txt
@@ -4,7 +4,7 @@ project(universal_ble_example 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 "universal_ble_example")
+set(BINARY_NAME "universal_ble")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc
index 23c0d5c..522cec7 100644
--- a/example/windows/runner/Runner.rc
+++ b/example/windows/runner/Runner.rc
@@ -89,13 +89,13 @@ BEGIN
BEGIN
BLOCK "040904e4"
BEGIN
- VALUE "CompanyName", "com.navideck" "\0"
- VALUE "FileDescription", "universal_ble_example" "\0"
+ VALUE "CompanyName", "Navideck" "\0"
+ VALUE "FileDescription", "Universal BLE" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
- VALUE "InternalName", "universal_ble_example" "\0"
- VALUE "LegalCopyright", "Copyright (C) 2023 com.navideck. All rights reserved." "\0"
- VALUE "OriginalFilename", "universal_ble_example.exe" "\0"
- VALUE "ProductName", "universal_ble_example" "\0"
+ VALUE "InternalName", "Universal BLE" "\0"
+ VALUE "LegalCopyright", "Copyright © 2023 com.navideck. All rights reserved." "\0"
+ VALUE "OriginalFilename", "universal_ble.exe" "\0"
+ VALUE "ProductName", "Universal BLE" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp
index f2b1e45..5c8b4dd 100644
--- a/example/windows/runner/main.cpp
+++ b/example/windows/runner/main.cpp
@@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
- if (!window.Create(L"universal_ble_example", origin, size)) {
+ if (!window.Create(L"Universal BLE", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico
index c04e20c..27c6b4f 100644
Binary files a/example/windows/runner/resources/app_icon.ico and b/example/windows/runner/resources/app_icon.ico differ
diff --git a/lib/src/models/manufacturer_data.dart b/lib/src/models/manufacturer_data.dart
index c6a64ba..9c58c40 100644
--- a/lib/src/models/manufacturer_data.dart
+++ b/lib/src/models/manufacturer_data.dart
@@ -8,6 +8,9 @@ class ManufacturerData {
String get companyIdRadix16 => "0x0${companyId.toRadixString(16)}";
+ String get payloadRadix16 =>
+ "0x${payload.map((e) => e.toRadixString(16).toUpperCase().padLeft(2, '0')).join('')}";
+
factory ManufacturerData.fromData(Uint8List data) {
if (data.length < 2) {
throw const FormatException("Invalid Manufacturer Data");