494 lines
14 KiB
TypeScript
494 lines
14 KiB
TypeScript
/**
|
||
* 蓝牙管理模块(基于 uni 内置 BLE API,替代原 android-ble 第三方插件)
|
||
*
|
||
* 对外约定与原插件保持一致,业务页面无需改动逻辑:
|
||
* - MyApiResult.type == 0 表示成功
|
||
* - 1000=notify 订阅成功 1001=订阅失败
|
||
* - 10000=连接失败 10001=已断开 10002=蓝牙未开/不可用 10003=无蓝牙权限
|
||
* - notify 数据通过回调返回 {type:0, data: NotityData},由 App.vue 转发 uni.$emit("bleData")
|
||
* - 连接状态变化通过 uni.$emit("bleState", state) 通知(0=已连接 10000=连接失败 10001=已断开)
|
||
*/
|
||
|
||
// CAIIC-MCM 设备 GATT(固件 mcm-ddc-ble app_rdtss.h 实际值,小写与 Android UUID.toString 格式一致)
|
||
// 注意:ble_protocol.md V1.1 表格写作 ...C72EE001/...C72EE002,与固件实际 ...C72E0001/...C72E0002 不符,以固件为准
|
||
export const MCM_SERVICE_UUID = "00002760-08c2-11e1-9073-0e8ac72e1001"
|
||
export const MCM_WRITE_UUID = "00002760-08c2-11e1-9073-0e8ac72e0001" // Write Without Response
|
||
export const MCM_NOTIFY_UUID = "00002760-08c2-11e1-9073-0e8ac72e0002" // Notify
|
||
export const MCM_INFO_UUID = "00002760-08c2-11e1-9073-0e8ac72e0004" // 只读:设备参数 TLV(同 INFO_RSP 全部项)
|
||
|
||
export type MyApiResult = {
|
||
type: number,
|
||
message: string,
|
||
data: any
|
||
}
|
||
|
||
export type BleScanDevice = {
|
||
name: string | null,
|
||
address: string
|
||
}
|
||
|
||
export type BleScanResult = {
|
||
device: BleScanDevice,
|
||
rssi: number
|
||
}
|
||
|
||
// notify 回调数据结构(对齐原插件 NotityData 的 {data, mac} 字段)
|
||
export type NotityData = {
|
||
data: number[],
|
||
mac: string
|
||
}
|
||
|
||
// ================= 模块状态 =================
|
||
|
||
let connectedDeviceId: string | null = null
|
||
let manualDisconnect = false
|
||
let connTimeoutMs = 0 // createBLEConnection 超时,0 表示不设置
|
||
let btEnabled = false // 缓存的蓝牙开关状态(由 adapter 状态监听/操作结果维护)
|
||
let scanning = false
|
||
let scanTimeoutTimer = 0
|
||
|
||
let stateListenerRegistered = false
|
||
let connListenerRegistered = false
|
||
let scanListenerRegistered = false
|
||
let notifyListenerRegistered = false
|
||
let notifyDataCb: ((res: MyApiResult) => void) | null = null
|
||
let readPendingChar: string | null = null // 等待读结果的特征 UUID
|
||
let readCb: ((res: MyApiResult) => void) | null = null
|
||
let readTimer = 0
|
||
|
||
// 注册特征值变化监听(notify 数据与 read 结果都经此回调分发,幂等)
|
||
function ensureValueChangeListener() {
|
||
if (notifyListenerRegistered) return
|
||
notifyListenerRegistered = true
|
||
uni.onBLECharacteristicValueChange((res: any) => {
|
||
const charId = res.characteristicId as string
|
||
const bytes = new Uint8Array(res.value as ArrayBuffer)
|
||
const arr = [] as number[]
|
||
for (let i = 0; i < bytes.length; i++) {
|
||
arr.push(bytes[i])
|
||
}
|
||
if (uuidEq(charId, MCM_NOTIFY_UUID)) {
|
||
const nd: NotityData = { data: arr, mac: res.deviceId } as NotityData
|
||
const c = notifyDataCb
|
||
if (c != null) {
|
||
c({ type: 0, message: "notify", data: nd } as MyApiResult)
|
||
}
|
||
return
|
||
}
|
||
if (readPendingChar != null && uuidEq(charId, readPendingChar)) {
|
||
const c = readCb
|
||
readCb = null
|
||
readPendingChar = null
|
||
clearTimeout(readTimer)
|
||
if (c != null) {
|
||
c({ type: 0, message: "read ok", data: arr } as MyApiResult)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function ok(message: string): MyApiResult {
|
||
return { type: 0, message: message, data: null } as MyApiResult
|
||
}
|
||
|
||
function failRes(type: number, err: any): MyApiResult {
|
||
return { type: type, message: (err && err.errMsg) ? err.errMsg : "fail", data: null } as MyApiResult
|
||
}
|
||
|
||
// uni BLE 错误 → 本模块 type 约定:蓝牙未开/不可用 10002,权限类 10003
|
||
function mapBleError(err: any): number {
|
||
const msg: string = (err && err.errMsg) ? err.errMsg : ""
|
||
if (msg.indexOf("ermission") >= 0) return 10003
|
||
return 10002
|
||
}
|
||
|
||
function uuidEq(a: string, b: string): boolean {
|
||
return a.toUpperCase() == b.toUpperCase()
|
||
}
|
||
|
||
// 注册系统蓝牙开关状态监听并同步一次当前状态(幂等)
|
||
function ensureStateListener() {
|
||
if (stateListenerRegistered) return
|
||
stateListenerRegistered = true
|
||
uni.onBluetoothAdapterStateChange((res: any) => {
|
||
btEnabled = res.available
|
||
})
|
||
uni.getBluetoothAdapterState({
|
||
success: (res: any) => {
|
||
btEnabled = res.available
|
||
},
|
||
fail: () => {}
|
||
})
|
||
}
|
||
|
||
// 注册连接状态监听(幂等):意外断开时广播 bleState 10001
|
||
function ensureConnListener() {
|
||
if (connListenerRegistered) return
|
||
connListenerRegistered = true
|
||
uni.onBLEConnectionStateChange((res: any) => {
|
||
if (res.connected) {
|
||
connectedDeviceId = res.deviceId
|
||
} else {
|
||
const wasConnected = connectedDeviceId != null && connectedDeviceId == res.deviceId
|
||
connectedDeviceId = null
|
||
if (wasConnected && !manualDisconnect) {
|
||
uni.$emit("bleState", 10001)
|
||
}
|
||
manualDisconnect = false
|
||
}
|
||
})
|
||
}
|
||
|
||
// ================= 适配器 =================
|
||
|
||
// 手机蓝牙是否打开 true-打开 false-未打开
|
||
export function isEnabled(): boolean {
|
||
ensureStateListener()
|
||
return btEnabled
|
||
}
|
||
|
||
// 监听系统蓝牙开关状态 res.data 的值为 true-打开 false-关闭
|
||
export function OpenStateListener(cb: (res: MyApiResult) => void) {
|
||
ensureStateListener()
|
||
uni.onBluetoothAdapterStateChange((res: any) => {
|
||
cb({ type: 0, message: "adapter state changed", data: res.available } as MyApiResult)
|
||
})
|
||
}
|
||
|
||
// 打开蓝牙(初始化蓝牙适配器;蓝牙关闭/无权限时走 fail)
|
||
export function openBluetooth(cb: (res: MyApiResult) => void) {
|
||
ensureStateListener()
|
||
uni.openBluetoothAdapter({
|
||
success: () => {
|
||
btEnabled = true
|
||
cb(ok("bluetooth adapter opened"))
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(mapBleError(err), err))
|
||
}
|
||
})
|
||
}
|
||
|
||
// ================= 扫描 =================
|
||
|
||
// 开始蓝牙扫描:每个发现的设备回调一次 {type:0, data: BleScanResult};超时自动停止
|
||
export function startScan(timeoutMs: number, cb: (res: MyApiResult) => void) {
|
||
ensureStateListener()
|
||
uni.openBluetoothAdapter({
|
||
success: () => {
|
||
btEnabled = true
|
||
if (scanning) return
|
||
if (!scanListenerRegistered) {
|
||
scanListenerRegistered = true
|
||
uni.onBluetoothDeviceFound((res: any) => {
|
||
const devices = res.devices as any[]
|
||
for (let i = 0; i < devices.length; i++) {
|
||
const d = devices[i]
|
||
let name: string | null = d.name as string
|
||
if (name == null || name.length == 0) {
|
||
const ln = (d.localName ?? "") as string
|
||
name = ln.length > 0 ? ln : null
|
||
}
|
||
const sr: BleScanResult = {
|
||
device: { name: name, address: d.deviceId } as BleScanDevice,
|
||
rssi: d.RSSI as number
|
||
} as BleScanResult
|
||
cb({ type: 0, message: "device found", data: sr } as MyApiResult)
|
||
}
|
||
})
|
||
}
|
||
scanning = true
|
||
uni.startBluetoothDevicesDiscovery({
|
||
allowDuplicatesKey: false,
|
||
success: () => {},
|
||
fail: (err: any) => {
|
||
scanning = false
|
||
cb(failRes(mapBleError(err), err))
|
||
}
|
||
})
|
||
if (scanTimeoutTimer != 0) {
|
||
clearTimeout(scanTimeoutTimer)
|
||
}
|
||
scanTimeoutTimer = setTimeout(() => {
|
||
stopScan()
|
||
}, timeoutMs)
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(mapBleError(err), err))
|
||
}
|
||
})
|
||
}
|
||
|
||
// 停止蓝牙扫描
|
||
export function stopScan() {
|
||
if (scanTimeoutTimer != 0) {
|
||
clearTimeout(scanTimeoutTimer)
|
||
scanTimeoutTimer = 0
|
||
}
|
||
if (!scanning) return
|
||
scanning = false
|
||
uni.stopBluetoothDevicesDiscovery({
|
||
success: () => {},
|
||
fail: () => {}
|
||
})
|
||
}
|
||
|
||
// ================= 连接 =================
|
||
|
||
// 蓝牙是否已连接
|
||
export function isConnected(): boolean {
|
||
return connectedDeviceId != null
|
||
}
|
||
|
||
// 获取已连接设备id(未连接返回空串)
|
||
export function getConnectMac(): string {
|
||
return connectedDeviceId ?? ""
|
||
}
|
||
|
||
// 连接蓝牙(autoReconnect 保留兼容签名,当前业务恒传 false,未实现自动重连)
|
||
export function connect(mac: string, autoReconnect: boolean, cb: (res: MyApiResult) => void) {
|
||
ensureStateListener()
|
||
ensureConnListener()
|
||
manualDisconnect = false
|
||
if (connTimeoutMs > 0) {
|
||
uni.createBLEConnection({
|
||
deviceId: mac,
|
||
timeout: connTimeoutMs,
|
||
success: () => {
|
||
connectedDeviceId = mac
|
||
cb(ok("connected"))
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(10000, err))
|
||
}
|
||
})
|
||
} else {
|
||
uni.createBLEConnection({
|
||
deviceId: mac,
|
||
success: () => {
|
||
connectedDeviceId = mac
|
||
cb(ok("connected"))
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(10000, err))
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 设置蓝牙连接超时时间(ms)
|
||
export function connectTimeout(timeout: number): void {
|
||
connTimeoutMs = timeout
|
||
}
|
||
|
||
// 设置自动连接间隔(保留兼容,未实现)
|
||
export function setBtAutoConnectTime(timeout: number): void {
|
||
}
|
||
|
||
// 取消自动连接蓝牙(保留兼容,未实现)
|
||
export function cancelAutoConnectBt(): void {
|
||
}
|
||
|
||
// 断开蓝牙
|
||
export function disConnect() {
|
||
const id = connectedDeviceId
|
||
manualDisconnect = true
|
||
connectedDeviceId = null
|
||
if (id != null) {
|
||
uni.closeBLEConnection({
|
||
deviceId: id,
|
||
success: () => {},
|
||
fail: () => {}
|
||
})
|
||
}
|
||
}
|
||
|
||
// ================= 服务与订阅 =================
|
||
|
||
// 连接后扫描服务:确认 MCM 服务及写/notify 特征存在(替代插件的自动识别)
|
||
export function scanServices(cb: (res: MyApiResult) => void) {
|
||
const id = connectedDeviceId
|
||
if (id == null) {
|
||
cb({ type: 10001, message: "not connected", data: null } as MyApiResult)
|
||
return
|
||
}
|
||
uni.getBLEDeviceServices({
|
||
deviceId: id,
|
||
success: (res: any) => {
|
||
const services = res.services as any[]
|
||
let found = false
|
||
for (let i = 0; i < services.length; i++) {
|
||
if (uuidEq(services[i].uuid as string, MCM_SERVICE_UUID)) {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if (!found) {
|
||
cb({ type: 10004, message: "MCM service not found", data: null } as MyApiResult)
|
||
return
|
||
}
|
||
uni.getBLEDeviceCharacteristics({
|
||
deviceId: id as string,
|
||
serviceId: MCM_SERVICE_UUID,
|
||
success: () => {
|
||
cb(ok("services ready"))
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(10005, err))
|
||
}
|
||
})
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(10004, err))
|
||
}
|
||
})
|
||
}
|
||
|
||
// 开启服务消息通知(显式使用 CAIIC-MCM 的服务/notify UUID)
|
||
// 订阅成功回调 type=1000,失败 type=1001;之后 notify 数据以 type=0 持续回调
|
||
export function onNotityReadBleData(readNotification: boolean, cb: (res: MyApiResult) => void) {
|
||
const id = connectedDeviceId
|
||
if (id == null) {
|
||
cb({ type: 10001, message: "not connected", data: null } as MyApiResult)
|
||
return
|
||
}
|
||
notifyDataCb = cb
|
||
ensureValueChangeListener()
|
||
uni.notifyBLECharacteristicValueChange({
|
||
deviceId: id,
|
||
serviceId: MCM_SERVICE_UUID,
|
||
characteristicId: MCM_NOTIFY_UUID,
|
||
state: readNotification,
|
||
success: () => {
|
||
cb({ type: 1000, message: "notify subscribed", data: null } as MyApiResult)
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(1001, err))
|
||
}
|
||
})
|
||
}
|
||
|
||
// ================= 写入 =================
|
||
|
||
// 发送数据(显式写类型):writeType 0=WRITE(有响应) 1=WRITE_NO_RESPONSE(无响应)
|
||
// CAIIC-MCM 设备的下行特征仅支持 Write Without Response,必须用 writeType=1。
|
||
// 服务/特征 UUID 用固件实际值显式指定(下行特征只有 WRITE_NO_RESPONSE 属性)
|
||
export function sendDataBytes(data: number[], writeType: number, cb: (res: MyApiResult) => void) {
|
||
const id = connectedDeviceId
|
||
if (id == null) {
|
||
cb({ type: 10001, message: "not connected", data: null } as MyApiResult)
|
||
return
|
||
}
|
||
const buf = new Uint8Array(data.length)
|
||
for (let i = 0; i < data.length; i++) {
|
||
buf[i] = data[i] & 0xFF
|
||
}
|
||
if (writeType == 1) {
|
||
uni.writeBLECharacteristicValue({
|
||
deviceId: id,
|
||
serviceId: MCM_SERVICE_UUID,
|
||
characteristicId: MCM_WRITE_UUID,
|
||
value: buf.buffer,
|
||
writeType: "writeNoResponse",
|
||
success: () => {
|
||
cb(ok("write ok"))
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(10008, err))
|
||
}
|
||
})
|
||
} else {
|
||
uni.writeBLECharacteristicValue({
|
||
deviceId: id,
|
||
serviceId: MCM_SERVICE_UUID,
|
||
characteristicId: MCM_WRITE_UUID,
|
||
value: buf.buffer,
|
||
writeType: "write",
|
||
success: () => {
|
||
cb(ok("write ok"))
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(10008, err))
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// ================= 读取 =================
|
||
|
||
// 读取设备参数特征(只读 MCM_INFO_UUID,内容为全部信息项 TLV,同 INFO_RSP(all))。
|
||
// uni 的读结果经 onBLECharacteristicValueChange 回传;type==0 时 data 为 number[]。
|
||
export function readInfoChar(cb: (res: MyApiResult) => void) {
|
||
const id = connectedDeviceId
|
||
if (id == null) {
|
||
cb({ type: 10001, message: "not connected", data: null } as MyApiResult)
|
||
return
|
||
}
|
||
ensureValueChangeListener()
|
||
readPendingChar = MCM_INFO_UUID
|
||
readCb = cb
|
||
readTimer = setTimeout(() => {
|
||
if (readCb != null) {
|
||
readCb = null
|
||
readPendingChar = null
|
||
cb({ type: 10008, message: "read timeout", data: null } as MyApiResult)
|
||
}
|
||
}, 3000)
|
||
uni.readBLECharacteristicValue({
|
||
deviceId: id,
|
||
serviceId: MCM_SERVICE_UUID,
|
||
characteristicId: MCM_INFO_UUID,
|
||
success: () => {},
|
||
fail: (err: any) => {
|
||
readPendingChar = null
|
||
readCb = null
|
||
clearTimeout(readTimer)
|
||
cb(failRes(10008, err))
|
||
}
|
||
})
|
||
}
|
||
|
||
// ================= 其他 =================
|
||
|
||
// 设置 mtu(仅 App 端有效;其他平台回调失败,调用方保持默认 20 字节切片)
|
||
export function setMtu(len: number, cb: (res: MyApiResult) => void) {
|
||
const id = connectedDeviceId
|
||
if (id == null) {
|
||
cb({ type: 10001, message: "not connected", data: null } as MyApiResult)
|
||
return
|
||
}
|
||
// #ifdef APP-PLUS
|
||
uni.setBLEMTU({
|
||
deviceId: id,
|
||
mtu: len,
|
||
success: (res: any) => {
|
||
cb({ type: 0, message: "mtu " + res.mtu, data: null } as MyApiResult)
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(10008, err))
|
||
}
|
||
})
|
||
// #endif
|
||
// #ifndef APP-PLUS
|
||
cb({ type: 10008, message: "set mtu not supported on this platform", data: null } as MyApiResult)
|
||
// #endif
|
||
}
|
||
|
||
// 读取 rssi 信号 需要蓝牙已经连接
|
||
export function readRssi(cb: (res: MyApiResult) => void) {
|
||
const id = connectedDeviceId
|
||
if (id == null) {
|
||
cb({ type: 10001, message: "not connected", data: null } as MyApiResult)
|
||
return
|
||
}
|
||
uni.getBLEDeviceRSSI({
|
||
deviceId: id,
|
||
success: (res: any) => {
|
||
cb({ type: 0, message: "rssi", data: res.RSSI } as MyApiResult)
|
||
},
|
||
fail: (err: any) => {
|
||
cb(failRes(10008, err))
|
||
}
|
||
})
|
||
}
|