跳到主要内容

传输插件(Low-level)

仅当 WebUSB、React Native BLE 或官方原生参考无法覆盖你的宿主环境时,再考虑实现底层传输插件。选择这条路径后,设备枚举、连接生命周期、数据收发和消息重组都需要由你的应用负责。

底层传输插件的作用,是把你的原生通信能力适配成 SDK 能理解的 LowlevelTransportSharedPlugin 接口。这样即使应用使用 Swift、Kotlin、Flutter 或其他技术栈,也可以复用上层硬件 SDK 的请求、事件和返回值模型。

快览

BLE 标识表

角色UUID
服务 UUID00000001-0000-1000-8000-00805f9b34fb
写入特征00000002-0000-1000-8000-00805f9b34fb
通知特征00000003-0000-1000-8000-00805f9b34fb

BLE头部参考

下面是一段 BLE 通知数据的头部参考,SDK 需要从这里识别消息类型和载荷长度:

原始数据: 3F 23 23 00 04 00 00 00 0C 1A 0A ...
│ │ │ │ │ │ │ └─ Protobuf 载荷开始
│ │ │ │ │ └────────┴─ 载荷长度: 0x0000000C = 12 字节
│ │ │ └──┴─ 消息类型: 0x0004
└──┴──┴─ Magic: 0x3F 0x23 0x23 (固定标识)

判断头部包的关键规则:前 3 字节必须是 3F 23 23

可以参考这些原生接入参考理解插件如何落地:

插件接口定义(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差异

WebUSB 和 BLE 的帧处理方式并不一样。实现插件时最容易出错的地方,就是把两者当成同一种 64 字节分片协议处理。

对比项WebUSB (桌面)BLE (原生移动端)
数据包大小固定 64 字节可变长度,取决于 MTU
每个包前缀每个包都带 0x3F只有首包带 0x3F 0x23 0x23
填充零填充到 64 字节无需填充
receive() 返回每次一个 64 字节帧完整重组后的消息

WebUSB协议(桌面)

WebUSB 使用固定 64 字节帧。每一帧都有明确的前缀和填充规则,上层 SDK 会按照该格式完成消息组装。

Magic bytes 说明: 0x3F 0x23 0x23 是协议固定标识,用于确认一个完整消息的起始位置。

首帧:

OffsetLengthContent
03Magic: 0x3F 0x23 0x23
32消息类型(大端 u16)
54载荷长度(大端 u32)
955前 55 字节 protobuf(零填充)

后续帧:

OffsetLengthContent
01Magic: 0x3F
163剩余 protobuf 字节(零填充)

BLE协议(原生端)

BLE 通过通知回调返回可变长度数据块。插件侧必须先把多段通知重组为完整消息,再从 receive() 返回给 SDK。

包格式

头部块(每条消息的第一个包):

┌─────────┬──────────────┬──────────┬──────────────┬─────────────┐
│ Byte 0 │ Byte 1-2 │ Byte 3-4 │ Byte 5-8 │ Byte 9+ │
├─────────┼──────────────┼──────────┼──────────────┼─────────────┤
│ 0x3F │ 0x23 0x23 │ Type │ Length │ Payload... │
│ (Magic) │ (Magic) │ (大端u16)│ (大端u32) │ │
└─────────┴──────────────┴──────────┴──────────────┴─────────────┘

后续块: 只携带剩余载荷数据,不再带协议前缀。

头检

function hasSharedTransportHeader(data: Uint8Array): boolean {
return (
data.length >= 9 &&
data[0] === 0x3f && // ASCII '?'
data[1] === 0x23 && // ASCII '#'
data[2] === 0x23
); // ASCII '#' terminator
}

读载荷长(大端)

function readUint32FromHeader(buffer: Uint8Array, offset: number): number {
return (
((buffer[offset] << 24) |
(buffer[offset + 1] << 16) |
(buffer[offset + 2] << 8) |
buffer[offset + 3]) >>>
0
);
}

// 参考: payloadLength = readUint32BE(data, 5)

重组参考

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)]; // 略过 3F 23 23
} else {
this.buffer.push(...data);
}

// 检查是否收齐: 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("");
}
}

异常参考

// 错误参考片段:又把 BLE 消息切回 64 字节帧。
receive() { return this.messageQueue.shift() }

// 建议做法:等待完整重组完成后再返回消息。
receive() { return this.completeMessagePromise }
// 尽量避免:频繁做字符串拼接会带来更高内存开销。
let hexBuffer = "";
hexBuffer += dataViewToHex(chunk);

// 更建议:先累计字节数组,收齐后再转成 hex。
let payloadBytes: number[] = [];
payloadBytes.push(...chunk);

初始化范例

import ukeySdk from "@ukeyfe/hardware-common-connect-sdk";

const plugin = {
enumerate: () => Promise.resolve([{ id: "foo", name: "bar" }]),
send: (uuid, data) => {
/* 写入十六进制数据 */
},
receive: () => {
/* WebUSB: 返回 64 字节帧 // BLE: 返回完整消息 */
},
connect: (uuid) => {
/* BLE Android: 记得先完成配对! */
},
disconnect: (uuid) => {
/* 释放连接资源 */
},
init: () => {
/* 初始化 BLE 相关能力 */
},
version: "UKey Wallet-1.0",
};

ukeySdk.init({ env: "lowlevel", debug: true }, undefined, plugin);

后续