ble_link_kit 0.3.0 copy "ble_link_kit: ^0.3.0" to clipboard
ble_link_kit: ^0.3.0 copied to clipboard

Flutter BLE connection framework for multi-protocol scanning, generic GATT transfer, framed channels, provisioning, and resumable OTA.

English | 简体中文 | 日本語 | 한국어

ble_link_kit #

A BLE connection management framework for Flutter: multi-protocol device discovery, generic GATT data transfer, reliable framed channels, Wi-Fi provisioning, resumable OTA, and power profiles.

Device UUIDs, advertising signatures, and protocol operation values are supplied by the application; the package contains no product-specific values.

Features #

  • Register multiple independent scan signatures and see every matching profile ID.
  • Access advertised service UUIDs, manufacturer data, service data, RSSI, and transmit power.
  • Connect without configuring a provisioning protocol, then discover, read, write, subscribe, and inspect the negotiated MTU through generic GATT APIs.
  • Optionally transfer fragmented, CRC16-checked messages over arbitrary writable/notifiable characteristic pairs.
  • Apply one power policy to scanning, Android connection priority, and Android PHY selection.
  • Provision Wi-Fi and perform acknowledged, CRC32-verified, resumable firmware updates.
  • Restore CoreBluetooth state on iOS, run connected-device foreground work on Android, and optionally use Android RFCOMM SPP for provisioning.

Platform support #

Capability Android iOS
Multi-profile BLE discovery and raw advertisement fields Yes Yes
Generic GATT discovery/read/write/notifications Yes Yes
Fragmented framed channels with CRC16 Yes Yes
Wi-Fi provisioning and resumable CRC32 OTA Yes Yes
Explicit connection priority / PHY control Yes / Yes No / No
Background connection support Foreground service CoreBluetooth restoration
RFCOMM SPP provisioning Yes No (unsupported)

Use await api.capabilities before presenting platform-specific controls. Capability flags report BLE, OTA, SPP, PHY control, connection-priority control, and opportunistic scanning; they do not replace characteristic discovery or property checks.

Multi-protocol discovery #

ScanProfile is independent of ProvisionProtocolProfile. A profile matches an advertisement when one of its supplied signatures matches; matchedProfileIds contains all matches. Registering an empty list restores the 0.2.0 discovery behavior.

Raw advertisement fields are added to scan events only after any non-empty scan profile list is registered. An advertisement must match at least one supplied condition to be reported; register a profile containing the target service UUID when possible.

final api = BleLinkKit();

await api.configureScanProfiles([
  ScanProfile(
    id: 'environment-sensor',
    serviceUuids: ['181A'],
    namePrefixes: ['Env-'],
  ),
  ScanProfile(
    id: 'vendor-beacon',
    manufacturerIds: [0x1234],
  ),
]);

await api.requestPermissions();
api.scanEvents.listen((event) {
  final device = event.device;
  if (device == null) return;
  print(device.matchedProfileIds);
  print(device.serviceUuids);
  print(device.manufacturerData); // Map<int, Uint8List>
  print(device.serviceData);      // Map<String, Uint8List>
  print(device.txPower);          // int?, advertised dBm
});
await api.scan(const ScanOptions(
  timeout: Duration(seconds: 12),
  namePrefixes: ['Lab-'],
  includeSpp: false,
));

ScanOptions.namePrefixes adds a per-scan filter. includeSpp enables parallel Android classic discovery and is ignored on iOS. Explicit stop and timeout both emit ScanState.stopped. Device IDs are opaque platform tokens and should only be passed back to this API.

Data transfer #

Scanning, connecting, disconnecting, and generic GATT transfer do not require configure(ProvisionProtocolProfile).

Raw GATT #

Use raw GATT when the application owns message boundaries or needs direct access to characteristic values. Check discovered properties before selecting an operation.

import 'dart:typed_data';
import 'package:ble_link_kit/ble_link_kit.dart';

final api = BleLinkKit();
await api.connect(deviceId);
// On iOS, wait for ConnectionState.connected before using GATT.

final services = await api.discoverServices(deviceId);
for (final service in services) {
  for (final characteristic in service.characteristics) {
    final p = characteristic.properties;
    print('${service.uuid}/${characteristic.uuid}: '
        'read=${p.read}, write=${p.write}, '
        'writeNoResponse=${p.writeNoResponse}, '
        'notify=${p.notify}, indicate=${p.indicate}');
  }
}

final value = await api.readCharacteristic(
  deviceId,
  serviceUuid,
  characteristicUuid,
);
await api.writeCharacteristic(
  deviceId,
  serviceUuid,
  characteristicUuid,
  Uint8List.fromList([0x01, 0x02]),
  withResponse: true,
);

final subscription = api.characteristicValueEvents.listen((event) {
  if (event.deviceId == deviceId &&
      event.serviceUuid == serviceUuid &&
      event.characteristicUuid == characteristicUuid) {
    print(event.value);
  }
});
await api.setCharacteristicNotify(
  deviceId,
  serviceUuid,
  characteristicUuid,
  true,
);
final mtu = await api.negotiatedMtu(deviceId);

// Later:
await api.setCharacteristicNotify(
  deviceId,
  serviceUuid,
  characteristicUuid,
  false,
);
await subscription.cancel();

Operations use the native serialized GATT queue. While provisioning or OTA is active, raw access to the command/event characteristics owned by that operation fails with BleErrorCode.busy.

The plugin uses a single active connection. Every raw GATT call validates deviceId against that connection. Notification ownership is reference-counted across raw subscriptions, provisioning, and framed channels; the native subscription is disabled only after its final owner releases it.

Framed channels #

Use a framed channel when payloads can exceed one GATT write or when both endpoints need fragmentation, reassembly, a three-second assembly timeout, and per-fragment CRC16 validation. The peripheral must implement the frame format in doc/PROTOCOL.md.

final channelId = await api.openFramedChannel(
  deviceId,
  serviceUuid: serviceUuid, // Optional when characteristic UUIDs are unambiguous.
  writeCharacteristicUuid: writeUuid,
  notifyCharacteristicUuid: notifyUuid,
);

final frames = api.frameEvents.listen((message) {
  if (message.channelId == channelId) {
    print(message.payload); // Complete reassembled Uint8List.
  }
});
await api.sendFrame(channelId, Uint8List.fromList([1, 2, 3, 4]));

// Later:
await api.closeFramedChannel(channelId);
await frames.cancel();
Layer Prefer it when Application responsibility
Raw GATT The device protocol already defines packet boundaries, or exact characteristic control is required Encoding, message boundaries, retries, and validation
Framed channel Arbitrary binary messages need transport fragmentation and integrity checks Implementing the shared frame codec on the peripheral and defining payload semantics

Framed channels provide transport integrity, not encryption, authentication, request matching, or OTA semantics. Channels are bound to the connection on which they were opened. After an unexpected disconnect, reopen each channel after reconnection; old channel IDs are invalid.

Power management #

PowerProfile supplies one policy for scanning, connection parameters, and PHY selection.

Profile Scanning Connection parameters PHY
latencyFirst Android low latency; iOS continuous Android HIGH during operations, BALANCED while idle; iOS firmware controlled Android prefers LE 2M; iOS CoreBluetooth controlled
balanced Android balanced; iOS 8000 ms active / 2000 ms idle Android balanced; iOS firmware controlled Android prefers LE 1M
powerSaver Android low power; iOS 2000 ms active / 8000 ms idle Android low power; iOS firmware controlled Android prefers LE 1M
await api.setPowerProfile(PowerProfile.balanced());
await api.scan(const ScanOptions(
  powerModeOverride: BleScanPowerMode.lowPower,
));
await api.connect(
  deviceId,
  priorityOverride: BleConnectionPriority.high,
);

Without setPowerProfile, both platforms preserve 0.1.0 behavior. iOS priority overrides are advisory no-ops, exposes neither priority nor PHY selection, and does not emit ConnectionState.paramsUpdated. A custom scanDutyCycle controls iOS active/idle timing. Scan timeout is total wall-clock time, including idle windows. See firmware cooperation.

Provisioning & OTA #

Provisioning and OTA are optional higher-level features alongside generic transfer. They require one application-supplied ProvisionProtocolProfile; discovery and raw GATT do not.

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: 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',
  ),
));

await api.provision(const WifiCredentials(
  ssid: 'Network',
  password: 'secret',
));
await api.startOta(
  firmwareBytes,
  targetVersion: '2.0.0',
);

The values are intentionally fictional. Provisioning and OTA share one exclusive operation lane. Concurrent calls fail with BleErrorCode.busy; calls before configure fail with BleErrorCode.configureRequired. OTA uses per-chunk acknowledgements, device-authoritative checkpoints, and final CRC32 verification. See doc/PROTOCOL.md for wire details and resume semantics.

Host integration #

iOS #

Add NSBluetoothAlwaysUsageDescription; add NSBluetoothPeripheralUsageDescription for older iOS versions. For background scanning, restoration, or OTA continuation, add bluetooth-central to UIBackgroundModes.

Keep restorationIdentifier non-empty, stable, and unique. The configured provisioning profile is persisted for cold-launch CoreBluetooth restoration; events are buffered until Flutter listens, up to the limit noted below.

Android #

The plugin manifest limits legacy Bluetooth and location permissions to API 30. Android 12+ uses BLUETOOTH_SCAN and BLUETOOTH_CONNECT; scanning is declared with neverForLocation, which may filter advertisements Android considers location-derived. Provisioning and OTA use a connectedDevice foreground service, so the host must allow affected foreground-service notifications.

Security #

This package does not provide end-to-end encryption or device authentication. Wi-Fi passwords and generic payloads are plaintext before link-layer transport. Framing CRCs detect corruption but are not cryptographic integrity protection. Production applications should authenticate devices, establish per-session keys, prevent replay, erase credentials promptly, and keep secrets out of logs. Native plugin logs redact SSID, password, and frame payload fields by default.

API snippet verification checklist #

Checked against the public exports and signatures in lib/ for 0.3.0:

  • configureScanProfiles(List<ScanProfile>), ScanProfile fields, scan([ScanOptions]), and every accessed BleDevice advertisement field.
  • connect(String, {priorityOverride}), discoverServices(String), all GattService / GattCharacteristic / GattProperties fields, and negotiatedMtu(String).
  • readCharacteristic and writeCharacteristic(..., Uint8List, {withResponse}) argument order and types.
  • setCharacteristicNotify(..., bool), characteristicValueEvents, and every accessed CharacteristicValue field.
  • openFramedChannel(String, {writeCharacteristicUuid, notifyCharacteristicUuid, serviceUuid}), sendFrame, closeFramedChannel, frameEvents, and FramedMessage fields.
  • setPowerProfile, scan/connection overrides, configure, provision, and startOta(..., {required targetVersion, priorityOverride}).

Known issues (0.3.0) #

  • Some low-level failures still surface as protocolError; OTA, version-query, and SPP sites are not yet fully classified into finer error codes.
  • On iOS, connect() completes when the request is accepted, not when GATT is ready. Observe connectionEvents for connected or failed.
  • iOS restoration discards a persisted profile with a mismatched schema; the app must call configure again.
  • iOS buffers at most 256 events before a Flutter listener attaches; older events are dropped.
  • A restored iOS scan starts a fresh 16-second wall-clock timeout instead of continuing the original timeout.
  • PowerProfile is not persisted across iOS process restarts; restored scans use the legacy continuous schedule until it is set again.
  • Android paramsUpdated uses a runtime-dispatched hidden SDK callback and is best-effort telemetry.
  • Android CCCD behavior, write-without-response callback behavior, and iOS notification permission/background restoration for the new generic channels still require broader physical-device coverage.
  • Frame channels are bound to the connection on which they were opened. After an unexpected disconnect, old channelId values are invalid and channels must be reopened after reconnection.
  • Android SPP in 0.3.0 is for provisioning only; it does not expose a generic byte stream.
  • A ScanProfile with no matching signatures does not match advertisements; do not rely on an empty-signature profile to obtain unfiltered advertisement data.
  • Avoid changing scan profiles dynamically during an Android scan; the current implementation does not explicitly snapshot profiles for each callback.

License #

MIT. See LICENSE.

1
likes
0
points
62
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter BLE connection framework for multi-protocol scanning, generic GATT transfer, framed channels, provisioning, and resumable OTA.

Repository (GitHub)
View/report issues

Topics

#bluetooth #ble #gatt #scanner #ota

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on ble_link_kit

Packages that implement ble_link_kit