/** * 蓝牙管理模块(基于 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)) } }) }