ble_link_kit 0.4.0
ble_link_kit: ^0.4.0 copied to clipboard
Flutter BLE connection framework for multi-protocol scanning, generic GATT transfer, framed channels, provisioning, and resumable OTA.
import 'dart:async';
import 'package:ble_link_kit/ble_link_kit.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ProvisionDemoPage()));
class ProvisionDemoPage extends StatefulWidget {
const ProvisionDemoPage({super.key});
@override
State<ProvisionDemoPage> createState() => _ProvisionDemoPageState();
}
class _ProvisionDemoPageState extends State<ProvisionDemoPage> {
final api = BleLinkKit();
final ssid = TextEditingController(text: 'Demo Wi-Fi');
final password = TextEditingController();
final devices = <String, BleDevice>{};
final subscriptions = <StreamSubscription<Object?>>[];
BleCapabilities? capabilities;
String status = 'Configuring demo profile…';
String? connectedId;
bool useSpp = false;
double otaProgress = 0;
static const operations = ProtocolOperations(
wifiConfig: 'demo_wifi_config',
wifiStatus: 'demo_wifi_status',
firmwareVersion: 'demo_firmware_version',
otaStart: 'demo_ota_start',
otaChunkAck: 'demo_ota_chunk_ack',
otaEnd: 'demo_ota_end',
otaStatus: 'demo_ota_status',
otaComplete: 'demo_ota_complete',
otaFailed: 'demo_ota_failed',
wifiConnect: 'demo_wifi_connect',
configReceived: 'demo_config_received',
wifiConnected: 'demo_wifi_connected',
wifiFailed: 'demo_wifi_failed',
otaStartAck: 'demo_ota_start_ack',
);
@override
void initState() {
super.initState();
subscriptions.add(
api.scanEvents.listen((event) {
if (event.device != null) devices[event.device!.id] = event.device!;
update('Scan: ${event.state.name}');
}),
);
subscriptions.add(
api.connectionEvents.listen(
(event) => update('Connection: ${event.state.name}'),
),
);
subscriptions.add(
api.provisionEvents.listen(
(event) => update('Provision: ${event.state.name}'),
),
);
subscriptions.add(
api.otaEvents.listen((event) {
otaProgress = event.progress;
update('OTA: ${event.state.name} ${(event.progress * 100).round()}%');
}),
);
initialize();
}
Future<void> initialize() async {
try {
await api.configure(
const ProvisionProtocolProfile(
serviceUuid: '11111111-2222-3333-4444-555555555555',
commandUuid: '11111111-2222-3333-4444-666666666666',
eventUuid: '11111111-2222-3333-4444-777777777777',
deviceNamePrefixes: ['Demo-'],
operations: operations,
),
);
capabilities = await api.capabilities;
update('Ready — grant permission, then scan');
} on BleLinkKitException catch (error) {
update('${error.code.name}: ${error.message}');
}
}
void update(String value) {
if (mounted) setState(() => status = value);
}
Future<void> scan() async {
try {
if (!await api.requestPermissions()) {
throw const BleLinkKitException(
BleErrorCode.permissionDenied,
'Permission denied',
);
}
devices.clear();
await api.scan(const ScanOptions(includeSpp: true));
} on BleLinkKitException catch (error) {
update('${error.code.name}: ${error.message}');
}
}
Future<void> connect(BleDevice device) async {
try {
if (useSpp) {
await api.sppConnect(device.id);
} else {
await api.connect(device.id);
}
connectedId = device.id;
update('Connected to ${device.name}');
} on BleLinkKitException catch (error) {
update('${error.code.name}: ${error.message}');
}
}
Future<void> provision() async {
try {
final credentials = WifiCredentials(
ssid: ssid.text,
password: password.text,
);
if (useSpp) {
await api.sppProvision(credentials);
} else {
await api.provision(credentials);
}
update('Provision request accepted');
} on BleLinkKitException catch (error) {
update('${error.code.name}: ${error.message}');
}
}
Future<void> otaDemo() async {
try {
otaProgress = 0;
await api.startOta(
List<int>.generate(4096, (index) => index & 0xff),
targetVersion: 'demo-2.0.0',
);
} on BleLinkKitException catch (error) {
update('${error.code.name}: ${error.message}');
}
}
@override
void dispose() {
for (final subscription in subscriptions) {
subscription.cancel();
}
ssid.dispose();
password.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('BLE Provision + OTA Demo')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(status),
const SizedBox(height: 8),
if (capabilities?.supportsSpp ?? false)
SwitchListTile(
title: const Text('Use Android SPP fallback'),
value: useSpp,
onChanged: (value) => setState(() => useSpp = value),
),
FilledButton(
onPressed: scan,
child: const Text('Grant permission and scan'),
),
const Divider(),
...devices.values.map(
(device) => ListTile(
title: Text(device.name.isEmpty ? 'Unnamed device' : device.name),
subtitle: Text('RSSI ${device.rssi} • ${device.id}'),
trailing: FilledButton(
onPressed: () => connect(device),
child: const Text('Connect'),
),
),
),
const Divider(),
TextField(
controller: ssid,
decoration: const InputDecoration(labelText: 'SSID'),
),
TextField(
controller: password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
),
const SizedBox(height: 8),
FilledButton(
onPressed: connectedId == null ? null : provision,
child: const Text('Provision Wi-Fi'),
),
const SizedBox(height: 16),
LinearProgressIndicator(value: otaProgress),
FilledButton(
onPressed: connectedId == null || useSpp ? null : otaDemo,
child: const Text('Send generated demo firmware'),
),
],
),
);
}