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(...).
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.
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())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 / didDisconnectimport 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
}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;
}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.