Transport plug-in
Only consider implementing a low-level transport plug-in if WebUSB, React Native BLE, or official native examples do not cover your hosting environment. After choosing this path, device enumeration, connection lifecycle, data sending and receiving, and message reassembly need to be taken care of by your application.
The function of the underlying transmission plug-in is to adapt your native communication capabilities to the LowlevelTransportSharedPlugin interface that the SDK can understand. In this way, even if the application uses Swift, Kotlin, Flutter or other technology stacks, it can reuse the request, event and return value model of the upper hardware SDK.
Quick Notes
BLE ID Table
| Role | UUID |
|---|---|
| Service | 00000001-0000-1000-8000-00805f9b34fb |
| Write characteristic | 00000002-0000-1000-8000-00805f9b34fb |
| Notify characteristic | 00000003-0000-1000-8000-00805f9b34fb |
BLE Header Example
The following is an example of a header of BLE notification data. The SDK needs to identify the message type and payload length from here:
原始数据: 3F 23 23 00 04 00 00 00 0C 1A 0A ...
│ │ │ │ │ │ │ └─ Protobuf 载荷开始
│ │ │ │ │ └────────┴─ 载荷长度: 0x0000000C = 12 字节
│ │ │ └──┴─ 消息类型: 0x0004
└──┴──┴─ Magic: 0x3F 0x23 0x23 (固定标识)
Key rules for judging header packets: The first 3 bytes must be 3F 23 23.
You can refer to these native access examples to understand how the plug-in is implemented:
- 安卓原生蓝牙: WebView reaches Nordic BLE through JSBridge.
- iOS原生蓝牙: WebView calls into CoreBluetooth via JSBridge.
- Flutter原生蓝牙: WebView forwards BLE capability through a platform channel.
Plugin Contract: LowlevelTransportSharedPlugin
export type LowLevelDevice = { id: string; name: string };
export type LowlevelTransportSharedPlugin = {
enumerate: () => Promise<LowLevelDevice[]>;
send: (uuid: string, data: string) => Promise<void>;
receive: () => Promise<string>;
connect: (uuid: string) => Promise<void>;
disconnect: (uuid: string) => Promise<void>;
init: () => Promise<void>;
version: string;
};
WebUSB/BLE Differences
WebUSB and BLE frame processing is different. The most common Error when implementing plug-ins is to treat the two as the same 64-byte sharding protocol.
| Aspect | WebUSB (desktop) | BLE (native mobile) |
|---|---|---|
| Packet size | Fixed 64 bytes | Variable length, depending on MTU |
| Per-packet prefix | Every packet starts with 0x3F | Only the first packet starts with 0x3F 0x23 0x23 |
| Padding | Zero padded to 64 bytes | No padding required |
receive() return | One 64-byte frame per call | A fully reassembled message |
WebUSB Protocol (Desktop)
WebUSB uses fixed 64-byte frames. Each frame has clear prefix and padding rules, and the upper-layer SDK will complete message assembly according to this format.
Magic bytes Demo description:
0x3F 0x23 0x23is a fixed identifier of the protocol, used to confirm the starting position of a complete message.
First frame:
| Offset | Length | Content |
|---|---|---|
| 0 | 3 | Magic: 0x3F 0x23 0x23 |
| 3 | 2 | Message type (big-endian u16) |
| 5 | 4 | Payload length (big-endian u32) |
| 9 | 55 | First 55 protobuf bytes (zero padded) |
Subsequent frames:
| Offset | Length | Content |
|---|---|---|
| 0 | 1 | Magic: 0x3F |
| 1 | 63 | Remaining protobuf bytes (zero padded) |
BLE Protocol (Native)
BLE returns a variable-length chunk of data via a notification callback. The plug-in side must first reorganize the multi-segment notification into a complete message and then return it to the SDK from receive().
Packet Layout
Header block (first packet of each message):
┌─────────┬──────────────┬──────────┬──────────────┬─────────────┐
│ Byte 0 │ Byte 1-2 │ Byte 3-4 │ Byte 5-8 │ Byte 9+ │
├─────────┼──────────────┼──────────┼──────────────┼─────────────┤
│ 0x3F │ 0x23 0x23 │ Type │ Length │ Payload... │
│ (Magic) │ (Magic) │ (大端u16)│ (大端u32) │ │
└─────────┴──────────────┴──────────┴──────────────┴─────────────┘
Following blocks: Only carry the remaining payload data and no longer carry the protocol prefix.
Header Check
function hasSharedTransportHeader(data: Uint8Array): boolean {
return (
data.length >= 9 &&
data[0] === 0x3f && // ASCII '?'
data[1] === 0x23 && // ASCII '#'
data[2] === 0x23
); // ASCII '#' terminator
}
Read Payload Length
function readUint32FromHeader(buffer: Uint8Array, offset: number): number {
return (
((buffer[offset] << 24) |
(buffer[offset + 1] << 16) |
(buffer[offset + 2] << 8) |
buffer[offset + 3]) >>>
0
);
}
// Reference: payloadLength = readUint32BE(data, 5)
Reassembly Example
class BleMessageAssembler {
private buffer: number[] = [];
private expectedLength = 0;
private resolve: ((hex: string) => void) | null = null;
onNotification(data: Uint8Array): void {
if (this.isHeader(data)) {
this.expectedLength = this.readUint32BE(data, 5);
this.buffer = [...data.subarray(3)]; // Skip the 3F 23 23 magic bytes
} else {
this.buffer.push(...data);
}
// Check whether assembly is complete: buffer = Type(2) + Length(4) + Payload
if (this.buffer.length - 6 >= this.expectedLength) {
const hex = this.toHex(new Uint8Array(this.buffer));
this.buffer = [];
this.expectedLength = 0;
if (this.resolve) {
this.resolve(hex);
this.resolve = null;
}
}
}
receive(): Promise<string> {
return new Promise((resolve) => {
this.resolve = resolve;
});
}
private isHeader(d: Uint8Array): boolean {
return d.length >= 9 && d[0] === 0x3f && d[1] === 0x23 && d[2] === 0x23;
}
private readUint32BE(b: Uint8Array, o: number): number {
return ((b[o] << 24) | (b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3]) >>> 0;
}
private toHex(arr: Uint8Array): string {
return Array.from(arr)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
}
Error Examples
// Incorrect: splitting BLE messages back into 64-byte frames.
receive() { return this.messageQueue.shift() }
// Preferred: wait until the message is fully reassembled, then return it.
receive() { return this.completeMessagePromise }
// Avoid: frequent string concatenation increases memory overhead.
let hexBuffer = "";
hexBuffer += dataViewToHex(chunk);
// Prefer: collect the byte array first, then convert it to hex after assembly completes.
let payloadBytes: number[] = [];
payloadBytes.push(...chunk);
Init Example
import ukeySdk from "@ukeyfe/hardware-common-connect-sdk";
const plugin = {
enumerate: () => Promise.resolve([{ id: "foo", name: "bar" }]),
send: (uuid, data) => {
/* 写入十六进制数据 */
},
receive: () => {
/* 参考代码:WebUSB: 64字节帧 // BLE: complete message */
},
connect: (uuid) => {
/* BLE Android: 记得先完成配对! */
},
disconnect: (uuid) => {
/* 释放连接资源 */
},
init: () => {
/* 初始化 BLE 相关能力 */
},
version: "UKey Wallet-1.0",
};
ukeySdk.init({ env: "lowlevel", debug: true }, undefined, plugin);
Next Steps
- Android Guide — Learn about pairing, delay, and retry strategies.
- iOS guide — See how CoreBluetooth connects to the plug-in interface.
- Flutter Guide — Refer to the bridging method between platform channel and BLE.