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(...).

Command transport contract

Custom transports receive already framed command data from the SDK. They must preserve frame order and honor the requested GATT write mode:

  • Raw legacy commands and protocols v1-v3 use fixed 20-byte command chunks and writes with response. A larger negotiated ATT MTU does not change their transport contract.
  • Protocol v4 uses writes without response and the largest safe ATT payload exposed by the platform. The SDK requests an ACK/NAK for the complete frame, so a successful GATT call is not treated as the final frame result.
  • Do not retry, resize, reorder, or partially replay a frame after a failed write.

Aidlab 1 can perform the ATT MTU exchange, but its command transport remains limited to 20-byte chunks. Negotiated MTU is therefore used for v4 framing only.

The Python mtu_size property returns the negotiated ATT MTU, including the 3-byte ATT header. Android, Apple, and Flutter transports expose the maximum safe characteristic value length. The platform Device implementation converts these values to the correct v4 chunk size.

For example, ATT MTU 247 provides 244 payload bytes. V4 chunks are capped at the 512-byte GATT attribute limit even when the negotiated ATT MTU is 515–517.

Web Bluetooth does not expose the negotiated ATT MTU. The browser terminal therefore uses the standard 20-byte ATT payload size rather than probing for a larger value.

Transport failure

If a custom transport cannot complete an SDK-provided frame, or its framing or ACK state becomes ambiguous, stop queued writes and disconnect. Start again only with a new BLE connection. Do not retry or replay fragments, resize chunks, scan for another header, or attempt stream resynchronization. A complete NAK rejects that transaction and must not be retried automatically.

Examples

The Flutter example's platform-transport option imports the underlying BLE client directly. Add it as an explicit dependency with flutter pub add flutter_reactive_ble.

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
    // Negotiated ATT MTU 247 minus the 3-byte ATT header.
    override val mtuSize: Int = 244

    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 Aidlab
import CoreBluetooth
import Foundation

final class MyTransport: AidlabTransport {
    let address: UUID
    let name: String?
    var rssi: NSNumber
    // Negotiated ATT MTU 247 minus the 3-byte ATT header.
    let mtuSize: Int = 244

    var onDisconnect: ((DisconnectReason, Error?) -> Void)?

    init(address: UUID, name: String?, rssi: NSNumber) {
        self.address = address
        self.name = name
        self.rssi = rssi
    }

    func connect(completion: @escaping (Result<Void, Error>) -> Void) {
        // Connect using your BLE stack, then call completion(.success(()))
        // or completion(.failure(error)).
    }

    func disconnect() {
        // Disconnect and report the terminal event through onDisconnect.
    }

    func readCharacteristic(
        _ uuid: CBUUID,
        completion: @escaping (Result<Data, Error>) -> Void
    ) {
        // Read with your BLE stack and complete with Data or Error.
    }

    func writeCharacteristic(
        _ uuid: CBUUID,
        data: Data,
        withResponse: Bool,
        completion: @escaping (Result<Void, Error>) -> Void
    ) {
        // Preserve frame order and the requested write mode, then complete.
    }

    func startNotifications(
        _ uuid: CBUUID,
        onData: @escaping (Data) -> Void,
        onError: @escaping (Error) -> Void
    ) {
        // Forward characteristic values to onData and failures to onError.
    }

    func stopNotifications(_ uuid: CBUUID) {
        // Stop notifications for this characteristic.
    }
}

func connectDiscoveredDevice(
    address: UUID,
    name: String?,
    rssi: NSNumber,
    delegate: DeviceDelegate
) {
    let transport = MyTransport(address: address, name: name, rssi: rssi)
    let device = Device(transport: transport)
    device.connect(delegate: delegate) // errors via didReceiveError / didDisconnect
}
dart
import 'package:aidlab_sdk/aidlab_sdk.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart' as frb;
import 'dart:typed_data';

final class MyTransport implements AidlabTransport {
  MyTransport({
    required this.address,
    required this.name,
  });

  @override
  final String address;

  @override
  final String? name;

  @override
  AidlabTransportDisconnectCallback? onDisconnect;

  @override
  Future<void> connect() async {
    // your BLE connect
  }

  @override
  Future<void> disconnect() async {
    // your BLE disconnect
  }

  @override
  // Negotiated ATT MTU 247 minus the 3-byte ATT header.
  Future<int> mtuSize() async => 244;

  @override
  Future<Uint8List> readCharacteristic(String uuid) async {
    // your BLE read
    return Uint8List(0);
  }

  @override
  Future<void> writeCharacteristic(
    String uuid,
    Uint8List data, {
    bool withResponse = true,
  }) async {
    // your BLE write
  }

  @override
  void startNotifications(
    String uuid, {
    required void Function(Uint8List data) onData,
    required void Function(Object error, StackTrace stackTrace) onError,
  }) {
    // your BLE notify setup
  }

  @override
  Future<void> stopNotifications(String uuid) async {
    // stop notifications for this characteristic
  }
}

Future<Device> buildDevice(String address, String? name) async {
  // Option A: fully custom transport
  final custom = Device.withTransport(
    transport: MyTransport(address: address, name: name),
  );

  // Option B: use the SDK's default BLE transport (flutter_reactive_ble),
  // but still keep your own scanner/discovery UI.
  final transport = ReactiveBleAidlabTransport(
    frb.FlutterReactiveBle(),
    address: address,
    name: name,
  );
  final platform = Device.withTransport(
    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.