478 lines
13 KiB
TypeScript
478 lines
13 KiB
TypeScript
import React, { useRef, useState } from 'react'
|
|
import Taro from '@tarojs/taro'
|
|
import { Command } from '@/common/bluetooth/command'
|
|
import { uint8ArrayToFloat32, uint8ArrayToHex, waitFor } from '@/common/bluetooth/utils'
|
|
|
|
interface params {
|
|
init: () => void
|
|
state: Object
|
|
startScan: () => void
|
|
measureAndGetLab: () => any
|
|
getAdapterState: () => void
|
|
connect: (any) => void
|
|
disconnect: () => void
|
|
}
|
|
const Context = React.createContext<params | unknown>(null)
|
|
|
|
export interface BluetoothStateType {
|
|
listeners: any
|
|
discovering: boolean
|
|
available: boolean
|
|
connected: any
|
|
connecting: any
|
|
serviceRule: any
|
|
serviceId: any
|
|
characteristicRule: any
|
|
characteristicId: any
|
|
|
|
/** 正在执行的命令 */
|
|
command: any
|
|
responseResolve: any
|
|
responseReject: any
|
|
responseTimer: any
|
|
|
|
/** 是否显示蓝牙调试信息 */
|
|
debug: any
|
|
//搜索到的设备
|
|
devices: any
|
|
//取色仪主动返回的数据
|
|
deviceLab: any
|
|
}
|
|
|
|
let stateObj: BluetoothStateType = {
|
|
/** 事件监听器 */
|
|
listeners: new Set(),
|
|
/** 正在扫描设备 */
|
|
discovering: false,
|
|
/** 蓝牙是否可用 */
|
|
available: true,
|
|
/** 当前连接的设备 */
|
|
connected: null,
|
|
/** 正在连接的设备 */
|
|
connecting: null,
|
|
|
|
serviceRule: /^0000FFE0/,
|
|
serviceId: null,
|
|
characteristicRule: /^0000FFE1/,
|
|
characteristicId: null,
|
|
|
|
/** 正在执行的命令 */
|
|
command: null,
|
|
responseResolve: null,
|
|
responseReject: null,
|
|
responseTimer: null,
|
|
|
|
/** 是否显示蓝牙调试信息 */
|
|
debug: true,
|
|
//搜索到的设备
|
|
devices: [],
|
|
//取色仪主动返回的数据
|
|
deviceLab: null,
|
|
}
|
|
|
|
export default (props) => {
|
|
let refStatus = useRef(stateObj)
|
|
let [state, setState] = useState(refStatus.current)
|
|
|
|
const changeStatus = (obj: Object): void => {
|
|
refStatus.current = { ...refStatus.current, ...obj }
|
|
setState({ ...refStatus.current })
|
|
}
|
|
|
|
const init = async () => {
|
|
try {
|
|
await openAdapter()
|
|
} catch (e) {
|
|
changeStatus({ available: false })
|
|
}
|
|
|
|
// 绑定事件通知
|
|
Taro.onBluetoothAdapterStateChange((res) => {
|
|
emit({ type: 'stateUpdate', detail: res })
|
|
})
|
|
Taro.onBLEConnectionStateChange((res) => {
|
|
emit({ type: res.connected ? 'connected' : 'disconnect', detail: res })
|
|
})
|
|
Taro.onBLECharacteristicValueChange(({ value }) => notifySubscriber(value))
|
|
subscribe(async (ev) => {
|
|
if (ev.type === 'stateUpdate') {
|
|
// 蓝牙状态发生的变化
|
|
changeStatus({ discovering: ev.detail.discovering, available: ev.detail.available })
|
|
} else if (ev.type === 'disconnect' && refStatus.current.connected && refStatus.current.connected.deviceId === ev.detail.deviceId) {
|
|
// 断开连接
|
|
changeStatus({
|
|
connected: null,
|
|
serviceId: null,
|
|
characteristicId: null,
|
|
deviceLab: null,
|
|
devices: [],
|
|
})
|
|
Taro.showToast({ icon: 'none', title: '蓝牙连接已断开' })
|
|
} else if (ev.type === 'connected' && refStatus.current.connecting) {
|
|
// 连接成功
|
|
changeStatus({ connected: refStatus.current.connecting, connecting: null })
|
|
Taro.showToast({ title: '蓝牙已连接' })
|
|
} else if (ev.type === 'measure') {
|
|
//监听取色仪主动推送lab
|
|
await measureAndGetLab()
|
|
}
|
|
})
|
|
}
|
|
|
|
/** 打开蓝牙适配器 */
|
|
const openAdapter = () => {
|
|
return new Promise((resolve, reject) => {
|
|
Taro.openBluetoothAdapter({
|
|
success: resolve,
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 推送事件
|
|
* @param {{type: string; data: any}} event
|
|
*/
|
|
const emit = (event) => {
|
|
refStatus.current.listeners.forEach((cb) => {
|
|
cb && cb(event)
|
|
})
|
|
}
|
|
|
|
const subscribe = (cb) => {
|
|
if (cb) {
|
|
changeStatus({
|
|
listeners: refStatus.current.listeners.add(cb),
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取蓝牙适配器状态
|
|
* @returns {Promise<{discovering: boolean; available: boolean}>}
|
|
*/
|
|
const getAdapterState = () => {
|
|
return new Promise((resolve, reject) => {
|
|
Taro.getBluetoothAdapterState({
|
|
success: resolve,
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 启动设备扫描
|
|
* @param {(res: { devices: { name: string, deviceId: string, RSSI: number }[] }) => void} cb
|
|
* @param {number} duration
|
|
*/
|
|
const startScan = (duration = 30000) => {
|
|
console.log('开始寻找')
|
|
changeStatus({ devices: [] })
|
|
Taro.onBluetoothDeviceFound(getDevices)
|
|
return new Promise((resolve, reject) => {
|
|
Taro.startBluetoothDevicesDiscovery({
|
|
allowDuplicatesKey: true,
|
|
success: resolve,
|
|
fail: reject,
|
|
})
|
|
|
|
if (duration > 0) {
|
|
setTimeout(() => {
|
|
Taro.offBluetoothDeviceFound(getDevices)
|
|
Taro.stopBluetoothDevicesDiscovery()
|
|
console.log('停止搜索')
|
|
}, duration)
|
|
}
|
|
})
|
|
}
|
|
|
|
//获取搜索到的设备
|
|
const getDevices = (res) => {
|
|
res.devices.forEach((device) => {
|
|
// 排除掉已搜索到的设备和名称不合法的设备, 将新发现的设备添加到列表中
|
|
if (/^CM/.test(device.name) && !refStatus.current.devices.find((i) => i.deviceId === device.deviceId)) {
|
|
changeStatus({ devices: [...refStatus.current.devices, device] })
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 连接设备
|
|
* @param {{ name: string, deviceId: string, RSSI: number }} device
|
|
*/
|
|
const connect = async (device) => {
|
|
try {
|
|
changeStatus({ connecting: device })
|
|
console.log('connecting::', device)
|
|
await createConnection(device.deviceId)
|
|
await discoverService(device.deviceId)
|
|
await discoverCharacteristic(device.deviceId)
|
|
await notifyCharacteristicValueChange(device.deviceId)
|
|
} catch (e) {
|
|
changeStatus({ connecting: null })
|
|
Taro.showToast({ icon: 'none', title: '蓝牙连接失败' })
|
|
throw e
|
|
}
|
|
}
|
|
|
|
/** 断开当前连接的设备 */
|
|
const disconnect = async () => {
|
|
if (!refStatus.current.connected && !refStatus.current.connecting) return
|
|
if (refStatus.current.connected) {
|
|
await closeConnection(refStatus.current.connected.deviceId)
|
|
resetCommand()
|
|
changeStatus({
|
|
connected: null,
|
|
serviceId: null,
|
|
characteristicId: null,
|
|
devices: [],
|
|
deviceLab: null,
|
|
})
|
|
}
|
|
|
|
if (refStatus.current.connecting) {
|
|
await closeConnection(refStatus.current.connecting.deviceId)
|
|
changeStatus({ connecting: null })
|
|
}
|
|
}
|
|
|
|
/** 创建 BLE 连接 */
|
|
function createConnection(deviceId) {
|
|
return new Promise((resolve, reject) => {
|
|
Taro.createBLEConnection({
|
|
deviceId,
|
|
timeout: 2000,
|
|
success: resolve,
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
/** 关闭 BLE 连接 */
|
|
function closeConnection(deviceId) {
|
|
return new Promise((resolve, reject) => {
|
|
Taro.closeBLEConnection({
|
|
deviceId,
|
|
success: resolve,
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
/** 搜索服务 */
|
|
function discoverService(deviceId) {
|
|
return new Promise((resolve, reject) => {
|
|
Taro.getBLEDeviceServices({
|
|
deviceId,
|
|
success: ({ services }) => {
|
|
const service = services.find((i) => refStatus.current.serviceRule.test(i.uuid))
|
|
if (!service) {
|
|
reject(new Error('服务不可用'))
|
|
} else {
|
|
changeStatus({ serviceId: service.uuid })
|
|
resolve(service)
|
|
}
|
|
},
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
/** 搜索特征 */
|
|
function discoverCharacteristic(deviceId) {
|
|
return new Promise((resolve, reject) => {
|
|
Taro.getBLEDeviceCharacteristics({
|
|
deviceId,
|
|
serviceId: refStatus.current.serviceId,
|
|
success: ({ characteristics }) => {
|
|
const characteristic = characteristics.find((i) => refStatus.current.characteristicRule.test(i.uuid))
|
|
if (!characteristic) {
|
|
reject(new Error('特征不可用'))
|
|
} else {
|
|
changeStatus({ characteristicId: characteristic.uuid })
|
|
resolve(characteristic)
|
|
}
|
|
},
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
/** 启动特征通知 */
|
|
function notifyCharacteristicValueChange(deviceId, stateParm = true) {
|
|
return new Promise((resolve, reject) => {
|
|
Taro.notifyBLECharacteristicValueChange({
|
|
deviceId,
|
|
serviceId: refStatus.current.serviceId,
|
|
characteristicId: refStatus.current.characteristicId,
|
|
state: stateParm,
|
|
success: resolve,
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 通知订阅器
|
|
* @param {ArrayBuffer} buffer
|
|
*/
|
|
function notifySubscriber(buffer) {
|
|
if (refStatus.current.command) {
|
|
if (refStatus.current.debug) {
|
|
console.log(`[BLE RESP] ${uint8ArrayToHex(new Uint8Array(buffer))}`)
|
|
}
|
|
refStatus.current.command.fillResponse(buffer)
|
|
if (refStatus.current.command.isComplete) {
|
|
if (refStatus.current.command.isValid && refStatus.current.responseResolve) {
|
|
refStatus.current.responseResolve(refStatus.current.command.response)
|
|
} else if (!refStatus.current.command.isValid) {
|
|
refStatus.current.responseReject(new Error('无效数据'))
|
|
}
|
|
resetCommand()
|
|
}
|
|
} else {
|
|
const uint8Array = new Uint8Array(buffer)
|
|
if (uint8Array[0] === 0xbb && uint8Array[1] === 1 && uint8Array[3] === 0) {
|
|
const ev = { type: 'measure', detail: { mode: uint8Array[2] } }
|
|
emit(ev)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发送命令
|
|
* @param {Command}} command
|
|
* @returns {Promise<Uint8Array>}
|
|
*/
|
|
function exec(command) {
|
|
return new Promise(async (resolve, reject) => {
|
|
if (refStatus.current.command) {
|
|
reject(new Error('正在执行其他命令'))
|
|
} else {
|
|
try {
|
|
refStatus.current.command = command
|
|
const data = command.data
|
|
for (let i = 0; i < data.length; i++) {
|
|
await sendData(data[i])
|
|
}
|
|
|
|
if (command.responseSize <= 0) {
|
|
resolve(true)
|
|
resetCommand()
|
|
} else {
|
|
refStatus.current.responseReject = reject
|
|
refStatus.current.responseResolve = resolve
|
|
refStatus.current.responseTimer = setTimeout(() => {
|
|
reject(new Error('命令响应超时'))
|
|
resetCommand()
|
|
}, command.timeout)
|
|
}
|
|
} catch (e) {
|
|
reject(e)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 发送命令
|
|
* @param {ArrayBuffer} buffer
|
|
*/
|
|
function sendData(buffer) {
|
|
if (refStatus.current.debug) {
|
|
console.log(`[BLE SEND] ${uint8ArrayToHex(new Uint8Array(buffer))}`)
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
console.log('current:::', refStatus.current)
|
|
Taro.writeBLECharacteristicValue({
|
|
deviceId: refStatus.current.connected.deviceId,
|
|
serviceId: refStatus.current.serviceId,
|
|
characteristicId: refStatus.current.characteristicId,
|
|
value: buffer,
|
|
success: resolve,
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
function resetCommand() {
|
|
if (refStatus.current.responseTimer) {
|
|
clearTimeout(refStatus.current.responseTimer)
|
|
}
|
|
changeStatus({
|
|
command: null,
|
|
responseResolve: null,
|
|
responseReject: null,
|
|
responseTimer: null,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 测量
|
|
* @param {number} mode
|
|
* @returns {Promise}
|
|
*/
|
|
async function measure(mode = 0) {
|
|
console.log('current1:::', Command.WakeUp)
|
|
await exec(Command.WakeUp)
|
|
console.log('current2:::', Command.WakeUp)
|
|
await waitFor(50)
|
|
console.log('current3:::', Command.WakeUp)
|
|
return await exec(Command.measure(mode))
|
|
}
|
|
|
|
/**
|
|
* 获取测量的 lab 值
|
|
* @param {number} mode
|
|
* @returns {Promise<{ L: number, a: number, b: number }>}
|
|
*/
|
|
async function getLab(mode = 0) {
|
|
await exec(Command.WakeUp)
|
|
await waitFor(50)
|
|
const data: any = await exec(Command.getLab(mode))
|
|
return {
|
|
L: uint8ArrayToFloat32(data.slice(5, 9)),
|
|
a: uint8ArrayToFloat32(data.slice(9, 13)),
|
|
b: uint8ArrayToFloat32(data.slice(13, 17)),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 测量并获取 lab 值
|
|
* @param {number} mode
|
|
* @returns {Promise<{L: number, a: number, b: number}>}
|
|
*/
|
|
async function measureAndGetLab(mode = 0) {
|
|
await measure(mode)
|
|
await waitFor(50)
|
|
const lab = await getLab(mode)
|
|
console.log('lab2::', lab)
|
|
changeStatus({ deviceLab: lab })
|
|
return lab
|
|
}
|
|
|
|
return (
|
|
<Context.Provider
|
|
children={props.children}
|
|
value={{
|
|
init,
|
|
state,
|
|
startScan,
|
|
measureAndGetLab,
|
|
getAdapterState,
|
|
connect,
|
|
disconnect,
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export const useBluetooth = () => {
|
|
const res = React.useContext<any>(Context)
|
|
if (res) {
|
|
return { ...res }
|
|
} else {
|
|
return {}
|
|
}
|
|
}
|