Skip to content

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:

  1. Scan in your app (your own scanner).
  2. Build Device from discovered address/name and custom transport (or platform transport-for-address).
  3. 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 / didDisconnect
swift
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# AidlabTransport injection is not exposed yet.