Custom Transport (Without SDK Scanner)
You can ignore the SDK scanner (AidlabManager.scan()) and use your own BLE discovery flow.
The pattern is always the same:
- Scan in your app (your own scanner).
- Build
Devicefrom discovered address/name and custom transport (or platform transport-for-address). - Call
device.connect(...).
Examples
python
import asyncio
from typing import AsyncIterator
from aidlab import AidlabTransport, DataType, Device, DeviceDelegate
class MyTransport(AidlabTransport):
@property
def mtu_size(self) -> int:
return 247
async def connect(self) -> None:
...
async def disconnect(self) -> None:
...
async def read_char(self, uuid: str) -> bytes:
...
async def write_char(self, uuid: str, data: bytes, response: bool = True) -> None:
...
async def notifications(self, uuid: str) -> AsyncIterator[bytes]:
...
async def disconnections(self) -> AsyncIterator[None]:
...
class Delegate(DeviceDelegate):
async def did_connect(self, device):
await device.collect([DataType.RESPIRATION], [])
async def run():
# discovered by your own scanner
address = "AA:BB:CC:DD:EE:FF"
name = "Aidlab 2"
device = Device(address=address, name=name, transport=MyTransport())
await device.connect(Delegate())
while True:
await asyncio.sleep(1)
asyncio.run(run())kotlin
import com.aidlab.sdk.*
import java.util.UUID
class MyTransport(
private val addressValue: String,
private val nameValue: String?,
) : AidlabTransport {
override val address: String = addressValue
override val name: String? = nameValue
override val mtuSize: Int = 247
override var onDisconnect: ((DisconnectReason, Throwable?) -> Unit)? = null
override fun connect(
onSuccess: () -> Unit,
onError: (Throwable) -> Unit,
) {
// your BLE connect
// call onSuccess() when connected or onError(e) on failure
}
override fun disconnect() {
// your BLE disconnect
}
override fun readCharacteristic(
uuid: UUID,
onSuccess: (ByteArray) -> Unit,
onError: (Throwable) -> Unit,
) {
// your BLE read
}
override fun writeCharacteristic(
uuid: UUID,
data: ByteArray,
withResponse: Boolean,
onSuccess: () -> Unit,
onError: (Throwable) -> Unit,
) {
// your BLE write
}
override fun startNotifications(
uuid: UUID,
onData: (ByteArray) -> Unit,
onError: (Throwable) -> Unit,
) {
// your BLE notifications
}
override fun stopNotifications(uuid: UUID) {
// stop notifications
}
}
// discovered by your own scanner
val transport = MyTransport("AA:BB:CC:DD:EE:FF", "Aidlab 2")
val device = Device(transport)
device.connect(deviceDelegate) // errors via didReceiveError / didDisconnectswift
import CoreBluetooth
import Aidlab
@MainActor
final class MyBleController: NSObject, CBCentralManagerDelegate, DeviceDelegate {
private lazy var central = CBCentralManager(delegate: self, queue: .main)
private var discovered: [UUID: (peripheral: CBPeripheral, rssi: NSNumber)] = [:]
private var targetDevice: Device?
func centralManagerDidUpdateState(_ central: CBCentralManager) {
guard central.state == .poweredOn else { return }
central.scanForPeripherals(
withServices: nil,
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
)
}
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any],
rssi RSSI: NSNumber
) {
guard peripheral.name == "Aidlab" || peripheral.name == "Aidlab 2" else { return }
// keep/update list for UI, do not auto-connect in didDiscover
discovered[peripheral.identifier] = (peripheral, RSSI)
}
func connectToDiscoveredDevice(id: UUID) {
guard targetDevice == nil else { return }
guard let item = discovered[id] else { return }
// no AidlabManager
let device = Device(
peripheral: item.peripheral,
rssi: item.rssi,
centralManager: central
)
targetDevice = device
device.connect(delegate: self) // errors via didReceiveError / didDisconnect
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
if targetDevice?.address == peripheral.identifier { targetDevice?.notifyDidConnect() }
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
if targetDevice?.address == peripheral.identifier { targetDevice?.notifyDidFailToConnect(error: error) }
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
if targetDevice?.address == peripheral.identifier { targetDevice?.notifyDidDisconnect(error: error) }
}
func didConnect(_ device: Device) {
device.collect(dataTypes: [.ecg, .respiration], dataTypesToStore: [])
}
}dart
import 'package:aidlab_sdk/aidlab_sdk.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
import 'dart:typed_data';
class MyTransport implements AidlabTransport, AidlabConnectionStateAwareTransport {
final Map<String, Stream<Uint8List>> _notificationStreams = <String, Stream<Uint8List>>{};
@override
Future<void> connect(Device device) async {
// your BLE connect
}
@override
Future<void> disconnect(Device device) async {
// your BLE disconnect
}
@override
Future<int> mtuSize(Device device) async => 247;
@override
Future<Uint8List> readCharacteristic(Device device, String uuid) async {
// your BLE read
return Uint8List(0);
}
@override
Future<void> writeCharacteristic(
Device device,
String uuid,
Uint8List data, {
bool withResponse = true,
}) async {
// your BLE write
}
@override
Stream<Uint8List> notifications(Device device, String uuid) {
final key = '${device.address}|${uuid.toUpperCase()}';
return _notificationStreams.putIfAbsent(key, () {
// your BLE notify stream (shared stream per (device, uuid))
return const Stream<Uint8List>.empty();
});
}
@override
Stream<AidlabTransportConnectionState> connectionStateChanges(Device device) {
// Optional but recommended: emit disconnected when transport drops unexpectedly.
return const Stream<AidlabTransportConnectionState>.empty();
}
}
Future<Device> buildDevice(ScanResult result) async {
final address = result.device.remoteId.str;
final name = result.advertisementData.localName;
// Option A: fully custom transport
final custom = Device.withTransport(
address: address,
name: name,
// If you don’t provide firmwareRevision, your transport must support
// readCharacteristic() for 00002A26 (Firmware Revision String).
transport: MyTransport(),
);
// Option B: use the SDK's default BLE transport (flutter_reactive_ble),
// but still keep your own scanner/discovery UI.
final transport = ReactiveBleAidlabTransport(FlutterReactiveBle());
final platform = Device.withTransport(
address: address,
name: name,
transport: transport,
);
return platform; // or return custom;
}csharp
using UnityEngine;
public sealed class NoOpAidlabManagerDelegate : AndroidJavaProxy
{
public NoOpAidlabManagerDelegate() : base("com.aidlab.sdk.AidlabManagerDelegate") {}
void onDeviceScanStarted() {}
void onDeviceScanStopped() {}
void onDeviceScanFailed(int errorCode) {}
void didDiscover(AndroidJavaObject device, int rssi) {}
}
var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
var manager = new AndroidJavaObject(
"com.aidlab.sdk.AidlabManager",
activity,
new NoOpAidlabManagerDelegate()
);
var aidlab = new Aidlab(
aidlabDelegate: this,
customAidlabManager: manager,
autoStartScan: false,
autoConnectOnDiscover: false
);
// discovered by your own scanner
string address = "AA:BB:CC:DD:EE:FF";
AndroidJavaObject device = manager.Call<AndroidJavaObject>("createDevice", address);
aidlab.ConnectDiscoveredDevice(device);Platform Notes
- Unity: this bypasses SDK scanning, but full C#
AidlabTransportinjection is not exposed yet.