mothercup/SmartAssiter/src/BluetoothManager.ts
2026-09-04 08:56:15 +08:00

494 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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