feat 配置蓝牙模块
This commit is contained in:
@@ -39,7 +39,12 @@ export default defineManifestConfig({
|
||||
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>',
|
||||
'<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>',
|
||||
'<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:usesPermissionFlags="neverForLocation"/>',
|
||||
'<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"/>',
|
||||
'<uses-permission android:name="android.permission.BLUETOOTH"/>',
|
||||
'<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>',
|
||||
'<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>',
|
||||
'<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>',
|
||||
'<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>',
|
||||
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
|
||||
'<uses-permission android:name="android.permission.CAMERA"/>',
|
||||
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
|
||||
@@ -48,7 +53,7 @@ export default defineManifestConfig({
|
||||
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
|
||||
'<uses-feature android:name="android.hardware.camera"/>',
|
||||
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
|
||||
],
|
||||
] as any,
|
||||
},
|
||||
/* ios打包配置 */
|
||||
ios: {},
|
||||
|
||||
5
src/auto-imports.d.ts
vendored
5
src/auto-imports.d.ts
vendored
@@ -9,6 +9,7 @@ declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const bluetooth: typeof import('./utils/bluetooth')['default']
|
||||
const closeDatabase: typeof import('./utils/sqlite')['closeDatabase']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
@@ -337,6 +338,9 @@ declare global {
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
// @ts-ignore
|
||||
export type { BluetoothDevice, ScanOptions, ScanForDeviceOptions, WriteOptions, BluetoothReceiveData, BluetoothConnectionState, DataCallback, ConnectionStateCallback, DeviceFoundCallback } from './utils/bluetooth'
|
||||
import('./utils/bluetooth')
|
||||
// @ts-ignore
|
||||
export type { SystemInfo } from './utils/device'
|
||||
import('./utils/device')
|
||||
// @ts-ignore
|
||||
@@ -352,6 +356,7 @@ declare module 'vue' {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly bluetooth: UnwrapRef<typeof import('./utils/bluetooth')['default']>
|
||||
readonly closeDatabase: UnwrapRef<typeof import('./utils/sqlite')['closeDatabase']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
|
||||
@@ -1,33 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { BluetoothDevice } from '@/utils/bluetooth'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import bluetooth from '@/utils/bluetooth'
|
||||
|
||||
const bluetoothEnabled = ref(true)
|
||||
interface DeviceItem {
|
||||
name: string
|
||||
mac: string
|
||||
deviceId: string
|
||||
rssi: number
|
||||
connected: boolean
|
||||
}
|
||||
|
||||
const bluetoothEnabled = ref(false)
|
||||
const searchValue = ref('')
|
||||
const scanning = ref(false)
|
||||
const connectingId = ref('')
|
||||
const deviceList = ref<DeviceItem[]>([])
|
||||
|
||||
const deviceList = ref([
|
||||
{
|
||||
name: 'XS-65H',
|
||||
mac: 'MACC:AA:BB:CC:DD:01',
|
||||
connected: false,
|
||||
},
|
||||
{
|
||||
name: 'XS-65H',
|
||||
mac: 'MACC:AA:BB:CC:DD:01',
|
||||
connected: true,
|
||||
},
|
||||
])
|
||||
let offAdapter: (() => void) | null = null
|
||||
let offConnection: (() => void) | null = null
|
||||
let offFound: (() => void) | null = null
|
||||
|
||||
function handleConnect(index: number) {
|
||||
deviceList.value[index].connected = true
|
||||
const filteredList = computed(() => {
|
||||
const keyword = searchValue.value.trim().toLowerCase()
|
||||
if (!keyword)
|
||||
return deviceList.value
|
||||
return deviceList.value.filter(item =>
|
||||
item.name.toLowerCase().includes(keyword)
|
||||
|| item.mac.toLowerCase().includes(keyword),
|
||||
)
|
||||
})
|
||||
|
||||
function upsertDevice(device: BluetoothDevice, connected = false) {
|
||||
const name = device.name || device.localName || '未知设备'
|
||||
const index = deviceList.value.findIndex(item => item.deviceId === device.deviceId)
|
||||
const next: DeviceItem = {
|
||||
name,
|
||||
mac: device.deviceId,
|
||||
deviceId: device.deviceId,
|
||||
rssi: device.RSSI,
|
||||
connected: connected || deviceList.value[index]?.connected || false,
|
||||
}
|
||||
|
||||
function handleDisconnect(index: number) {
|
||||
deviceList.value[index].connected = false
|
||||
if (index >= 0)
|
||||
deviceList.value[index] = { ...deviceList.value[index], ...next, connected: deviceList.value[index].connected || connected }
|
||||
else
|
||||
deviceList.value.push(next)
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
console.log('刷新设备列表')
|
||||
function syncConnectedFlag(deviceId: string, connected: boolean) {
|
||||
deviceList.value = deviceList.value.map(item => ({
|
||||
...item,
|
||||
connected: item.deviceId === deviceId ? connected : (connected ? false : item.connected),
|
||||
}))
|
||||
}
|
||||
|
||||
async function refreshAdapterState() {
|
||||
try {
|
||||
bluetoothEnabled.value = await bluetooth.isEnabled()
|
||||
}
|
||||
catch {
|
||||
bluetoothEnabled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBluetoothToggle(event: { detail: { value: boolean } }) {
|
||||
const wantEnable = !!event.detail.value
|
||||
try {
|
||||
if (wantEnable) {
|
||||
await bluetooth.turnOn()
|
||||
bluetoothEnabled.value = true
|
||||
uni.showToast({ title: '蓝牙已开启', icon: 'none' })
|
||||
}
|
||||
else {
|
||||
await bluetooth.turnOff()
|
||||
bluetoothEnabled.value = false
|
||||
deviceList.value = []
|
||||
uni.showToast({ title: '蓝牙已关闭', icon: 'none' })
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
bluetoothEnabled.value = !wantEnable
|
||||
const message = err instanceof Error ? err.message : '蓝牙开关失败'
|
||||
uni.showToast({ title: message, icon: 'none', duration: 3000 })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
if (scanning.value)
|
||||
return
|
||||
|
||||
if (!bluetoothEnabled.value) {
|
||||
uni.showToast({ title: '请先开启蓝牙', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
scanning.value = true
|
||||
try {
|
||||
// 有关键词时优先搜特定设备;否则全量扫描
|
||||
const keyword = searchValue.value.trim()
|
||||
if (keyword) {
|
||||
try {
|
||||
const device = await bluetooth.scanForDevice({ name: keyword, timeout: 12000 })
|
||||
upsertDevice(device)
|
||||
uni.showToast({ title: `已找到 ${device.name || device.deviceId}`, icon: 'none' })
|
||||
}
|
||||
catch {
|
||||
// 特定搜索失败时退回全量扫描,再本地过滤
|
||||
const devices = await bluetooth.startScan({ timeout: 10000 })
|
||||
deviceList.value = devices.map(item => ({
|
||||
name: item.name || item.localName || '未知设备',
|
||||
mac: item.deviceId,
|
||||
deviceId: item.deviceId,
|
||||
rssi: item.RSSI,
|
||||
connected: bluetooth.getConnection()?.deviceId === item.deviceId,
|
||||
}))
|
||||
if (!filteredList.value.length)
|
||||
uni.showToast({ title: '未找到匹配设备', icon: 'none' })
|
||||
}
|
||||
}
|
||||
else {
|
||||
const devices = await bluetooth.startScan({ timeout: 10000 })
|
||||
const connectedId = bluetooth.getConnection()?.deviceId
|
||||
deviceList.value = devices.map(item => ({
|
||||
name: item.name || item.localName || '未知设备',
|
||||
mac: item.deviceId,
|
||||
deviceId: item.deviceId,
|
||||
rssi: item.RSSI,
|
||||
connected: connectedId === item.deviceId,
|
||||
}))
|
||||
if (!devices.length)
|
||||
uni.showToast({ title: '未发现蓝牙设备', icon: 'none' })
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : '扫描失败'
|
||||
console.error('蓝牙扫描失败:', err)
|
||||
uni.showToast({ title: message, icon: 'none', duration: 3000 })
|
||||
}
|
||||
finally {
|
||||
scanning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConnect(device: DeviceItem) {
|
||||
if (connectingId.value || scanning.value)
|
||||
return
|
||||
|
||||
connectingId.value = device.deviceId
|
||||
try {
|
||||
await bluetooth.setupConnection(device.deviceId)
|
||||
syncConnectedFlag(device.deviceId, true)
|
||||
uni.showToast({ title: '连接成功', icon: 'success' })
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : '连接失败'
|
||||
console.error('蓝牙连接失败:', err)
|
||||
uni.showToast({ title: message, icon: 'none', duration: 3000 })
|
||||
}
|
||||
finally {
|
||||
connectingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisconnect(device: DeviceItem) {
|
||||
try {
|
||||
await bluetooth.disconnect(device.deviceId)
|
||||
syncConnectedFlag(device.deviceId, false)
|
||||
uni.showToast({ title: '已断开', icon: 'none' })
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : '断开失败'
|
||||
uni.showToast({ title: message, icon: 'none', duration: 3000 })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
offAdapter = bluetooth.onAdapterStateChange((available) => {
|
||||
bluetoothEnabled.value = available
|
||||
if (!available)
|
||||
deviceList.value = []
|
||||
})
|
||||
|
||||
offConnection = bluetooth.onConnectionStateChange((state) => {
|
||||
syncConnectedFlag(state.deviceId, state.connected)
|
||||
})
|
||||
|
||||
offFound = bluetooth.onDeviceFound((device) => {
|
||||
if (scanning.value)
|
||||
upsertDevice(device)
|
||||
})
|
||||
|
||||
try {
|
||||
await bluetooth.open()
|
||||
await refreshAdapterState()
|
||||
}
|
||||
catch (err) {
|
||||
bluetoothEnabled.value = false
|
||||
console.warn('蓝牙初始化:', err)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
offAdapter?.()
|
||||
offConnection?.()
|
||||
offFound?.()
|
||||
bluetooth.stopScan().catch(() => undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -43,12 +222,17 @@ function handleRefresh() {
|
||||
<text class="switch-label">
|
||||
蓝牙开关
|
||||
</text>
|
||||
<switch :checked="bluetoothEnabled" color="#4685FD" @change="bluetoothEnabled = !bluetoothEnabled" />
|
||||
<switch :checked="bluetoothEnabled" color="#4685FD" @change="handleBluetoothToggle" />
|
||||
</view>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-box">
|
||||
<wd-search v-model="searchValue" placeholder="搜索蓝牙设备" hide-cancel placeholder-left />
|
||||
<wd-search
|
||||
v-model="searchValue"
|
||||
placeholder="搜索蓝牙设备"
|
||||
hide-cancel
|
||||
placeholder-left
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 设备列表标题 -->
|
||||
@@ -57,13 +241,22 @@ function handleRefresh() {
|
||||
设备名称
|
||||
</text>
|
||||
<view class="refresh-btn" @tap="handleRefresh">
|
||||
<wd-icon name="refresh" size="20px" color="#999999" />
|
||||
<wd-icon
|
||||
name="refresh"
|
||||
size="20px"
|
||||
color="#999999"
|
||||
:class="{ spinning: scanning }"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!filteredList.length && !scanning" class="empty-tip">
|
||||
{{ bluetoothEnabled ? '点击右上角刷新扫描附近设备' : '请先开启蓝牙' }}
|
||||
</view>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<view class="device-list">
|
||||
<view v-for="(device, index) in deviceList" :key="index" class="device-item">
|
||||
<view v-for="device in filteredList" :key="device.deviceId" class="device-item">
|
||||
<view class="device-info">
|
||||
<text class="device-name">
|
||||
{{ device.name }}
|
||||
@@ -76,7 +269,9 @@ function handleRefresh() {
|
||||
v-if="!device.connected"
|
||||
size="small"
|
||||
round
|
||||
@tap="handleConnect(index)"
|
||||
:loading="connectingId === device.deviceId"
|
||||
:disabled="!!connectingId && connectingId !== device.deviceId"
|
||||
@tap="handleConnect(device)"
|
||||
>
|
||||
连接
|
||||
</wd-button>
|
||||
@@ -85,7 +280,7 @@ function handleRefresh() {
|
||||
type="danger"
|
||||
size="small"
|
||||
round
|
||||
@tap="handleDisconnect(index)"
|
||||
@tap="handleDisconnect(device)"
|
||||
>
|
||||
断开
|
||||
</wd-button>
|
||||
@@ -150,6 +345,13 @@ function handleRefresh() {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -168,6 +370,9 @@ function handleRefresh() {
|
||||
.device-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
@@ -180,5 +385,15 @@ function handleRefresh() {
|
||||
.device-mac {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
873
src/utils/bluetooth.ts
Normal file
873
src/utils/bluetooth.ts
Normal file
@@ -0,0 +1,873 @@
|
||||
/**
|
||||
* 蓝牙工具类 (uni-app BLE)
|
||||
* 封装低功耗蓝牙开关、搜索、连接与收发
|
||||
*
|
||||
* ======================== 使用说明 ========================
|
||||
*
|
||||
* import bluetooth from '@/utils/bluetooth'
|
||||
*
|
||||
* // ---------- 蓝牙开关 ----------
|
||||
* // 初始化蓝牙模块(使用前必须调用)
|
||||
* await bluetooth.open()
|
||||
*
|
||||
* // 关闭蓝牙模块
|
||||
* await bluetooth.close()
|
||||
*
|
||||
* // 检查蓝牙是否可用 / 是否已开启
|
||||
* const enabled = await bluetooth.isEnabled()
|
||||
*
|
||||
* // 打开系统蓝牙(Android App,部分机型可能需用户手动开)
|
||||
* await bluetooth.turnOn()
|
||||
*
|
||||
* // 关闭系统蓝牙(Android App)
|
||||
* await bluetooth.turnOff()
|
||||
*
|
||||
* // 打开系统蓝牙设置页
|
||||
* bluetooth.openSettings()
|
||||
*
|
||||
* // ---------- 蓝牙搜索 ----------
|
||||
* // 扫描附近设备(默认 10 秒)
|
||||
* const devices = await bluetooth.startScan({ timeout: 10000 })
|
||||
*
|
||||
* // 停止扫描
|
||||
* await bluetooth.stopScan()
|
||||
*
|
||||
* // 获取已发现设备列表
|
||||
* const list = await bluetooth.getDevices()
|
||||
*
|
||||
* // ---------- 蓝牙特定设备搜索 ----------
|
||||
* // 按名称模糊匹配(找到即停)
|
||||
* const device = await bluetooth.scanForDevice({ name: 'XS-65H', timeout: 15000 })
|
||||
*
|
||||
* // 按 deviceId / MAC 精确匹配
|
||||
* const device2 = await bluetooth.scanForDevice({ deviceId: 'AA:BB:CC:DD:EE:FF' })
|
||||
*
|
||||
* // ---------- 连接 / 断开 ----------
|
||||
* await bluetooth.connect(device.deviceId)
|
||||
* await bluetooth.disconnect()
|
||||
*
|
||||
* // 自动发现可写 / 可通知特征值并启用 notify
|
||||
* await bluetooth.setupConnection(device.deviceId)
|
||||
*
|
||||
* // ---------- 蓝牙传输数据 ----------
|
||||
* // 发送字符串
|
||||
* await bluetooth.send('hello')
|
||||
*
|
||||
* // 发送十六进制字符串
|
||||
* await bluetooth.sendHex('01 02 0A FF')
|
||||
*
|
||||
* // 发送 ArrayBuffer
|
||||
* await bluetooth.write(buffer)
|
||||
*
|
||||
* // ---------- 蓝牙接收数据 ----------
|
||||
* // 监听收到的数据(返回取消函数)
|
||||
* const off = bluetooth.onData((data) => {
|
||||
* console.log(data.hex) // 十六进制
|
||||
* console.log(data.text) // 文本
|
||||
* console.log(data.buffer) // ArrayBuffer
|
||||
* })
|
||||
* off() // 取消监听
|
||||
*
|
||||
* // 监听连接状态
|
||||
* const offState = bluetooth.onConnectionStateChange((state) => {
|
||||
* console.log(state.connected, state.deviceId)
|
||||
* })
|
||||
* offState()
|
||||
*
|
||||
* // ---------- 工具方法 ----------
|
||||
* bluetooth.arrayBufferToHex(buffer)
|
||||
* bluetooth.hexToArrayBuffer('01020AFF')
|
||||
* bluetooth.stringToArrayBuffer('hello')
|
||||
* bluetooth.arrayBufferToString(buffer)
|
||||
*
|
||||
* ======================== 前置条件 ========================
|
||||
*
|
||||
* - 主要用于 App(Android / iOS)BLE
|
||||
* - manifest 需开启 Bluetooth 模块,并配置权限:
|
||||
* - android.permission.BLUETOOTH
|
||||
* - android.permission.BLUETOOTH_ADMIN
|
||||
* - android.permission.BLUETOOTH_SCAN
|
||||
* - android.permission.BLUETOOTH_CONNECT
|
||||
* - android.permission.ACCESS_FINE_LOCATION
|
||||
* - Android 6+ 扫描通常需要定位权限,且系统定位开关需开启
|
||||
*/
|
||||
|
||||
// ======================== 类型定义 ========================
|
||||
|
||||
/** 蓝牙设备信息 */
|
||||
export interface BluetoothDevice {
|
||||
name: string
|
||||
deviceId: string
|
||||
RSSI: number
|
||||
advertisData?: ArrayBuffer
|
||||
advertisServiceUUIDs?: string[]
|
||||
localName?: string
|
||||
serviceData?: Record<string, ArrayBuffer>
|
||||
}
|
||||
|
||||
/** 扫描参数 */
|
||||
export interface ScanOptions {
|
||||
/** 扫描超时 ms,默认 10000;传 0 表示不自动停止 */
|
||||
timeout?: number
|
||||
/** 是否允许重复上报同一设备,默认 false */
|
||||
allowDuplicatesKey?: boolean
|
||||
/** 按服务 UUID 过滤 */
|
||||
services?: string[]
|
||||
/** 功耗模式 */
|
||||
powerLevel?: 'low' | 'medium' | 'high'
|
||||
}
|
||||
|
||||
/** 特定设备搜索参数 */
|
||||
export interface ScanForDeviceOptions extends ScanOptions {
|
||||
/** 设备名称(包含匹配,忽略大小写) */
|
||||
name?: string
|
||||
/** 设备 ID / MAC(精确匹配,忽略大小写) */
|
||||
deviceId?: string
|
||||
/** 自定义匹配函数 */
|
||||
match?: (device: BluetoothDevice) => boolean
|
||||
}
|
||||
|
||||
/** 写入参数 */
|
||||
export interface WriteOptions {
|
||||
deviceId?: string
|
||||
serviceId?: string
|
||||
characteristicId?: string
|
||||
/** 单包最大字节,默认 20 */
|
||||
chunkSize?: number
|
||||
/** 分包间隔 ms,默认 50 */
|
||||
chunkDelay?: number
|
||||
}
|
||||
|
||||
/** 接收到的数据 */
|
||||
export interface BluetoothReceiveData {
|
||||
deviceId: string
|
||||
serviceId: string
|
||||
characteristicId: string
|
||||
buffer: ArrayBuffer
|
||||
hex: string
|
||||
text: string
|
||||
}
|
||||
|
||||
/** 连接状态 */
|
||||
export interface BluetoothConnectionState {
|
||||
connected: boolean
|
||||
deviceId: string
|
||||
}
|
||||
|
||||
export type DataCallback = (data: BluetoothReceiveData) => void
|
||||
export type ConnectionStateCallback = (state: BluetoothConnectionState) => void
|
||||
export type DeviceFoundCallback = (device: BluetoothDevice, devices: BluetoothDevice[]) => void
|
||||
|
||||
// ======================== 内部状态 ========================
|
||||
|
||||
interface ActiveConnection {
|
||||
deviceId: string
|
||||
serviceId: string
|
||||
writeCharacteristicId: string
|
||||
notifyCharacteristicId: string
|
||||
}
|
||||
|
||||
const state = {
|
||||
opened: false,
|
||||
scanning: false,
|
||||
devices: new Map<string, BluetoothDevice>(),
|
||||
connection: null as ActiveConnection | null,
|
||||
}
|
||||
|
||||
const listeners = {
|
||||
data: new Set<DataCallback>(),
|
||||
connection: new Set<ConnectionStateCallback>(),
|
||||
found: new Set<DeviceFoundCallback>(),
|
||||
}
|
||||
|
||||
let deviceFoundBound = false
|
||||
let connectionStateBound = false
|
||||
let characteristicValueBound = false
|
||||
let scanTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// ======================== 内部工具 ========================
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise<void>(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function normalizeId(id: string) {
|
||||
return id.replace(/[:-]/g, '').toUpperCase()
|
||||
}
|
||||
|
||||
function promisify<T = unknown>(
|
||||
api: (options: any) => void,
|
||||
options: Record<string, any> = {},
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
api({
|
||||
...options,
|
||||
success: (res: T) => resolve(res),
|
||||
fail: (err: any) => {
|
||||
const msg = err?.errMsg || err?.message || JSON.stringify(err)
|
||||
const code = err?.errCode ?? err?.code
|
||||
reject(new Error(code != null ? `[${code}] ${msg}` : msg))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function mapDevice(raw: any): BluetoothDevice {
|
||||
return {
|
||||
name: raw.name || raw.localName || '',
|
||||
deviceId: raw.deviceId || '',
|
||||
RSSI: raw.RSSI ?? 0,
|
||||
advertisData: raw.advertisData,
|
||||
advertisServiceUUIDs: raw.advertisServiceUUIDs,
|
||||
localName: raw.localName,
|
||||
serviceData: raw.serviceData,
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDeviceFoundListener() {
|
||||
if (deviceFoundBound)
|
||||
return
|
||||
deviceFoundBound = true
|
||||
uni.onBluetoothDeviceFound((res) => {
|
||||
const list = res.devices || []
|
||||
for (const raw of list) {
|
||||
const device = mapDevice(raw)
|
||||
if (!device.deviceId)
|
||||
continue
|
||||
state.devices.set(device.deviceId, device)
|
||||
for (const cb of listeners.found)
|
||||
cb(device, Array.from(state.devices.values()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function ensureConnectionStateListener() {
|
||||
if (connectionStateBound)
|
||||
return
|
||||
connectionStateBound = true
|
||||
uni.onBLEConnectionStateChange((res) => {
|
||||
const connected = !!res.connected
|
||||
if (!connected && state.connection?.deviceId === res.deviceId)
|
||||
state.connection = null
|
||||
|
||||
for (const cb of listeners.connection) {
|
||||
cb({
|
||||
connected,
|
||||
deviceId: res.deviceId,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function ensureCharacteristicValueListener() {
|
||||
if (characteristicValueBound)
|
||||
return
|
||||
characteristicValueBound = true
|
||||
uni.onBLECharacteristicValueChange((res) => {
|
||||
const buffer = res.value as ArrayBuffer
|
||||
const payload: BluetoothReceiveData = {
|
||||
deviceId: res.deviceId,
|
||||
serviceId: res.serviceId,
|
||||
characteristicId: res.characteristicId,
|
||||
buffer,
|
||||
hex: arrayBufferToHex(buffer),
|
||||
text: arrayBufferToString(buffer),
|
||||
}
|
||||
for (const cb of listeners.data)
|
||||
cb(payload)
|
||||
})
|
||||
}
|
||||
|
||||
function clearScanTimer() {
|
||||
if (scanTimer) {
|
||||
clearTimeout(scanTimer)
|
||||
scanTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function getAndroidBluetoothAdapter() {
|
||||
const BluetoothAdapter = (plus.android as any).importClass('android.bluetooth.BluetoothAdapter')
|
||||
return BluetoothAdapter.getDefaultAdapter()
|
||||
}
|
||||
|
||||
function requestBluetoothPermissions(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
if (typeof plus === 'undefined' || !(plus as any).android) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
;(plus.android as any).requestPermissions(
|
||||
[
|
||||
'android.permission.ACCESS_FINE_LOCATION',
|
||||
'android.permission.ACCESS_COARSE_LOCATION',
|
||||
'android.permission.BLUETOOTH_SCAN',
|
||||
'android.permission.BLUETOOTH_CONNECT',
|
||||
],
|
||||
(result: { granted?: string[] }) => {
|
||||
const granted = result?.granted || []
|
||||
const ok = granted.some(p =>
|
||||
p.includes('LOCATION')
|
||||
|| p.includes('BLUETOOTH_SCAN')
|
||||
|| p.includes('BLUETOOTH_CONNECT'),
|
||||
)
|
||||
resolve(ok || granted.length > 0)
|
||||
},
|
||||
() => resolve(false),
|
||||
)
|
||||
}
|
||||
catch {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ======================== 编解码工具(模块级,供对象内外复用) ========================
|
||||
|
||||
function arrayBufferToHex(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer)
|
||||
return Array.from(bytes)
|
||||
.map(b => b.toString(16).padStart(2, '0').toUpperCase())
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function hexToArrayBuffer(hex: string): ArrayBuffer {
|
||||
const clean = hex.replace(/[\s,:_-]/g, '')
|
||||
if (clean.length % 2 !== 0)
|
||||
throw new Error('十六进制字符串长度必须为偶数')
|
||||
|
||||
const bytes = new Uint8Array(clean.length / 2)
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const byte = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16)
|
||||
if (Number.isNaN(byte))
|
||||
throw new Error(`无效十六进制: ${clean.slice(i * 2, i * 2 + 2)}`)
|
||||
bytes[i] = byte
|
||||
}
|
||||
return bytes.buffer
|
||||
}
|
||||
|
||||
function stringToArrayBuffer(text: string): ArrayBuffer {
|
||||
const bytes = new Uint8Array(text.length)
|
||||
for (let i = 0; i < text.length; i++)
|
||||
bytes[i] = text.charCodeAt(i) & 0xFF
|
||||
return bytes.buffer
|
||||
}
|
||||
|
||||
function arrayBufferToString(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer)
|
||||
let text = ''
|
||||
for (let i = 0; i < bytes.length; i++)
|
||||
text += String.fromCharCode(bytes[i])
|
||||
return text
|
||||
}
|
||||
|
||||
// ======================== 蓝牙工具 ========================
|
||||
|
||||
const bluetooth = {
|
||||
/** ArrayBuffer → 十六进制字符串 */
|
||||
arrayBufferToHex,
|
||||
/** 十六进制字符串 → ArrayBuffer */
|
||||
hexToArrayBuffer,
|
||||
/** 字符串 → ArrayBuffer */
|
||||
stringToArrayBuffer,
|
||||
/** ArrayBuffer → 字符串 */
|
||||
arrayBufferToString,
|
||||
|
||||
// ---------- 蓝牙开关 ----------
|
||||
|
||||
/** 初始化蓝牙适配器 */
|
||||
async open(): Promise<void> {
|
||||
await requestBluetoothPermissions()
|
||||
try {
|
||||
await promisify(uni.openBluetoothAdapter)
|
||||
state.opened = true
|
||||
}
|
||||
catch (err: any) {
|
||||
// 10001: 蓝牙未开启,模块仍可用,可继续监听状态
|
||||
if (String(err?.message || '').includes('10001')) {
|
||||
state.opened = true
|
||||
throw new Error('手机蓝牙未开启,请先打开蓝牙')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
ensureConnectionStateListener()
|
||||
ensureCharacteristicValueListener()
|
||||
},
|
||||
|
||||
/** 关闭蓝牙适配器 */
|
||||
async close(): Promise<void> {
|
||||
clearScanTimer()
|
||||
state.scanning = false
|
||||
state.devices.clear()
|
||||
state.connection = null
|
||||
try {
|
||||
await promisify(uni.closeBluetoothAdapter)
|
||||
}
|
||||
finally {
|
||||
state.opened = false
|
||||
}
|
||||
},
|
||||
|
||||
/** 蓝牙是否可用(已开启) */
|
||||
async isEnabled(): Promise<boolean> {
|
||||
try {
|
||||
if (!state.opened) {
|
||||
try {
|
||||
await this.open()
|
||||
}
|
||||
catch {
|
||||
// open 失败时继续用 getBluetoothAdapterState 判断
|
||||
}
|
||||
}
|
||||
const res = await promisify<{ available: boolean, discovering: boolean }>(
|
||||
uni.getBluetoothAdapterState,
|
||||
)
|
||||
return !!res.available
|
||||
}
|
||||
catch {
|
||||
try {
|
||||
if (typeof plus !== 'undefined' && (plus as any).android) {
|
||||
const adapter = getAndroidBluetoothAdapter()
|
||||
return !!(plus.android as any).invoke(adapter, 'isEnabled')
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
/** 打开系统蓝牙(Android App) */
|
||||
async turnOn(): Promise<void> {
|
||||
try {
|
||||
if (typeof plus !== 'undefined' && (plus as any).android) {
|
||||
const adapter = getAndroidBluetoothAdapter()
|
||||
if (!(plus.android as any).invoke(adapter, 'isEnabled'))
|
||||
(plus.android as any).invoke(adapter, 'enable')
|
||||
await delay(800)
|
||||
await this.open().catch(() => undefined)
|
||||
return
|
||||
}
|
||||
await this.open()
|
||||
}
|
||||
catch (err) {
|
||||
this.openSettings()
|
||||
throw new Error(`无法自动打开蓝牙,已跳转设置页: ${err}`)
|
||||
}
|
||||
},
|
||||
|
||||
/** 关闭系统蓝牙(Android App) */
|
||||
async turnOff(): Promise<void> {
|
||||
try {
|
||||
if (state.connection)
|
||||
await this.disconnect().catch(() => undefined)
|
||||
await this.close().catch(() => undefined)
|
||||
|
||||
if (typeof plus !== 'undefined' && (plus as any).android) {
|
||||
const adapter = getAndroidBluetoothAdapter()
|
||||
if ((plus.android as any).invoke(adapter, 'isEnabled'))
|
||||
(plus.android as any).invoke(adapter, 'disable')
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`关闭蓝牙失败: ${err}`)
|
||||
}
|
||||
},
|
||||
|
||||
/** 打开系统蓝牙设置页 */
|
||||
openSettings(): void {
|
||||
try {
|
||||
if (typeof plus !== 'undefined' && (plus as any).android) {
|
||||
const main = (plus.android as any).runtimeMainActivity()
|
||||
const Intent = (plus.android as any).importClass('android.content.Intent')
|
||||
const Settings = (plus.android as any).importClass('android.provider.Settings')
|
||||
const intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
|
||||
main.startActivity(intent)
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
uni.showToast({ title: '请在系统设置中打开蓝牙', icon: 'none' })
|
||||
},
|
||||
|
||||
// ---------- 蓝牙搜索 ----------
|
||||
|
||||
/**
|
||||
* 开始扫描附近蓝牙设备
|
||||
* @returns 超时后的设备列表(timeout=0 时立即返回当前已发现列表)
|
||||
*/
|
||||
async startScan(options: ScanOptions = {}): Promise<BluetoothDevice[]> {
|
||||
if (!state.opened)
|
||||
await this.open()
|
||||
|
||||
await requestBluetoothPermissions()
|
||||
ensureDeviceFoundListener()
|
||||
|
||||
if (state.scanning)
|
||||
await this.stopScan().catch(() => undefined)
|
||||
|
||||
state.devices.clear()
|
||||
state.scanning = true
|
||||
|
||||
const {
|
||||
timeout = 10000,
|
||||
allowDuplicatesKey = false,
|
||||
services,
|
||||
powerLevel = 'medium',
|
||||
} = options
|
||||
|
||||
await promisify(uni.startBluetoothDevicesDiscovery, {
|
||||
allowDuplicatesKey,
|
||||
services,
|
||||
powerLevel,
|
||||
})
|
||||
|
||||
if (!timeout || timeout <= 0)
|
||||
return Array.from(state.devices.values())
|
||||
|
||||
return new Promise((resolve) => {
|
||||
clearScanTimer()
|
||||
scanTimer = setTimeout(async () => {
|
||||
await this.stopScan().catch(() => undefined)
|
||||
resolve(Array.from(state.devices.values()))
|
||||
}, timeout)
|
||||
})
|
||||
},
|
||||
|
||||
/** 停止扫描 */
|
||||
async stopScan(): Promise<void> {
|
||||
clearScanTimer()
|
||||
if (!state.scanning) {
|
||||
try {
|
||||
await promisify(uni.stopBluetoothDevicesDiscovery)
|
||||
}
|
||||
catch {}
|
||||
return
|
||||
}
|
||||
state.scanning = false
|
||||
try {
|
||||
await promisify(uni.stopBluetoothDevicesDiscovery)
|
||||
}
|
||||
catch {}
|
||||
},
|
||||
|
||||
/** 获取已发现的设备列表 */
|
||||
async getDevices(): Promise<BluetoothDevice[]> {
|
||||
try {
|
||||
const res = await promisify<{ devices: any[] }>(uni.getBluetoothDevices)
|
||||
const list = (res.devices || []).map(mapDevice)
|
||||
for (const device of list)
|
||||
state.devices.set(device.deviceId, device)
|
||||
return Array.from(state.devices.values())
|
||||
}
|
||||
catch {
|
||||
return Array.from(state.devices.values())
|
||||
}
|
||||
},
|
||||
|
||||
/** 监听扫描过程中发现的设备 */
|
||||
onDeviceFound(callback: DeviceFoundCallback): () => void {
|
||||
ensureDeviceFoundListener()
|
||||
listeners.found.add(callback)
|
||||
return () => listeners.found.delete(callback)
|
||||
},
|
||||
|
||||
// ---------- 蓝牙特定设备搜索 ----------
|
||||
|
||||
/**
|
||||
* 搜索特定设备,找到即停止扫描
|
||||
* 可通过 name / deviceId / match 指定目标
|
||||
*/
|
||||
async scanForDevice(options: ScanForDeviceOptions): Promise<BluetoothDevice> {
|
||||
const { name, deviceId, match, timeout = 15000, ...scanOptions } = options
|
||||
if (!name && !deviceId && !match)
|
||||
throw new Error('请至少指定 name、deviceId 或 match')
|
||||
|
||||
const matched = (device: BluetoothDevice) => {
|
||||
if (match)
|
||||
return match(device)
|
||||
if (deviceId && normalizeId(device.deviceId) === normalizeId(deviceId))
|
||||
return true
|
||||
if (name) {
|
||||
const target = name.toLowerCase()
|
||||
return (device.name || '').toLowerCase().includes(target)
|
||||
|| (device.localName || '').toLowerCase().includes(target)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 先看缓存
|
||||
for (const device of state.devices.values()) {
|
||||
if (matched(device))
|
||||
return device
|
||||
}
|
||||
|
||||
if (!state.opened)
|
||||
await this.open()
|
||||
|
||||
ensureDeviceFoundListener()
|
||||
if (state.scanning)
|
||||
await this.stopScan().catch(() => undefined)
|
||||
|
||||
state.devices.clear()
|
||||
state.scanning = true
|
||||
|
||||
await promisify(uni.startBluetoothDevicesDiscovery, {
|
||||
allowDuplicatesKey: scanOptions.allowDuplicatesKey ?? false,
|
||||
services: scanOptions.services,
|
||||
powerLevel: scanOptions.powerLevel ?? 'medium',
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false
|
||||
|
||||
const off = this.onDeviceFound((device) => {
|
||||
if (settled || !matched(device))
|
||||
return
|
||||
settled = true
|
||||
clearScanTimer()
|
||||
off()
|
||||
this.stopScan().finally(() => resolve(device))
|
||||
})
|
||||
|
||||
clearScanTimer()
|
||||
scanTimer = setTimeout(() => {
|
||||
if (settled)
|
||||
return
|
||||
settled = true
|
||||
off()
|
||||
this.stopScan().finally(() => {
|
||||
reject(new Error(`未找到目标蓝牙设备${name ? `: ${name}` : ''}`))
|
||||
})
|
||||
}, timeout)
|
||||
})
|
||||
},
|
||||
|
||||
// ---------- 连接 ----------
|
||||
|
||||
/** 连接设备 */
|
||||
async connect(deviceId: string, timeout = 10000): Promise<void> {
|
||||
if (!deviceId)
|
||||
throw new Error('deviceId 不能为空')
|
||||
if (!state.opened)
|
||||
await this.open()
|
||||
|
||||
await this.stopScan().catch(() => undefined)
|
||||
ensureConnectionStateListener()
|
||||
|
||||
await promisify(uni.createBLEConnection, {
|
||||
deviceId,
|
||||
timeout,
|
||||
})
|
||||
|
||||
// Android 上连接后需短暂等待再发现服务
|
||||
await delay(800)
|
||||
},
|
||||
|
||||
/** 断开当前或指定设备 */
|
||||
async disconnect(deviceId?: string): Promise<void> {
|
||||
const id = deviceId || state.connection?.deviceId
|
||||
if (!id)
|
||||
return
|
||||
|
||||
try {
|
||||
await promisify(uni.closeBLEConnection, { deviceId: id })
|
||||
}
|
||||
finally {
|
||||
if (state.connection?.deviceId === id)
|
||||
state.connection = null
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 连接并自动挑选可写 / 可通知特征值,启用 notify
|
||||
* 之后可直接 send / onData
|
||||
*/
|
||||
async setupConnection(
|
||||
deviceId: string,
|
||||
options?: {
|
||||
serviceId?: string
|
||||
writeCharacteristicId?: string
|
||||
notifyCharacteristicId?: string
|
||||
timeout?: number
|
||||
},
|
||||
): Promise<ActiveConnection> {
|
||||
await this.connect(deviceId, options?.timeout)
|
||||
|
||||
let serviceId = options?.serviceId || ''
|
||||
let writeCharacteristicId = options?.writeCharacteristicId || ''
|
||||
let notifyCharacteristicId = options?.notifyCharacteristicId || ''
|
||||
|
||||
if (!serviceId || !writeCharacteristicId || !notifyCharacteristicId) {
|
||||
const servicesRes = await promisify<{ services: Array<{ uuid: string, isPrimary: boolean }> }>(
|
||||
uni.getBLEDeviceServices,
|
||||
{ deviceId },
|
||||
)
|
||||
const services = servicesRes.services || []
|
||||
|
||||
for (const service of services) {
|
||||
const charsRes = await promisify<{
|
||||
characteristics: Array<{
|
||||
uuid: string
|
||||
properties: {
|
||||
read?: boolean
|
||||
write?: boolean
|
||||
writeNoResponse?: boolean
|
||||
notify?: boolean
|
||||
indicate?: boolean
|
||||
}
|
||||
}>
|
||||
}>(uni.getBLEDeviceCharacteristics, {
|
||||
deviceId,
|
||||
serviceId: service.uuid,
|
||||
})
|
||||
|
||||
for (const char of charsRes.characteristics || []) {
|
||||
const props = char.properties || {}
|
||||
if (!writeCharacteristicId && (props.write || props.writeNoResponse))
|
||||
writeCharacteristicId = char.uuid
|
||||
if (!notifyCharacteristicId && (props.notify || props.indicate))
|
||||
notifyCharacteristicId = char.uuid
|
||||
}
|
||||
|
||||
if ((!serviceId && (writeCharacteristicId || notifyCharacteristicId))
|
||||
|| (writeCharacteristicId && notifyCharacteristicId)) {
|
||||
serviceId = service.uuid
|
||||
if (writeCharacteristicId && notifyCharacteristicId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!serviceId)
|
||||
throw new Error('未找到可用的蓝牙服务')
|
||||
if (!writeCharacteristicId && !notifyCharacteristicId)
|
||||
throw new Error('未找到可写或可通知的特征值')
|
||||
|
||||
// 没有单独 notify 特征时,尽量复用写特征
|
||||
if (!notifyCharacteristicId)
|
||||
notifyCharacteristicId = writeCharacteristicId
|
||||
if (!writeCharacteristicId)
|
||||
writeCharacteristicId = notifyCharacteristicId
|
||||
|
||||
if (notifyCharacteristicId) {
|
||||
ensureCharacteristicValueListener()
|
||||
await promisify(uni.notifyBLECharacteristicValueChange, {
|
||||
deviceId,
|
||||
serviceId,
|
||||
characteristicId: notifyCharacteristicId,
|
||||
state: true,
|
||||
})
|
||||
// 部分 Android 机型 notify 后需延迟再写
|
||||
await delay(400)
|
||||
}
|
||||
|
||||
state.connection = {
|
||||
deviceId,
|
||||
serviceId,
|
||||
writeCharacteristicId,
|
||||
notifyCharacteristicId,
|
||||
}
|
||||
return { ...state.connection }
|
||||
},
|
||||
|
||||
/** 获取当前连接信息 */
|
||||
getConnection(): ActiveConnection | null {
|
||||
return state.connection ? { ...state.connection } : null
|
||||
},
|
||||
|
||||
// ---------- 蓝牙传输数据 ----------
|
||||
|
||||
/** 写入 ArrayBuffer(自动分包) */
|
||||
async write(buffer: ArrayBuffer, options: WriteOptions = {}): Promise<void> {
|
||||
const deviceId = options.deviceId || state.connection?.deviceId
|
||||
const serviceId = options.serviceId || state.connection?.serviceId
|
||||
const characteristicId = options.characteristicId || state.connection?.writeCharacteristicId
|
||||
const chunkSize = options.chunkSize ?? 20
|
||||
const chunkDelay = options.chunkDelay ?? 50
|
||||
|
||||
if (!deviceId || !serviceId || !characteristicId)
|
||||
throw new Error('请先 setupConnection,或传入 deviceId / serviceId / characteristicId')
|
||||
|
||||
const bytes = new Uint8Array(buffer)
|
||||
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
||||
const chunk = bytes.slice(offset, offset + chunkSize)
|
||||
// 复制到新 buffer,避免底层引用问题
|
||||
const value = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength)
|
||||
await promisify(uni.writeBLECharacteristicValue, {
|
||||
deviceId,
|
||||
serviceId,
|
||||
characteristicId,
|
||||
value,
|
||||
})
|
||||
if (offset + chunkSize < bytes.length)
|
||||
await delay(chunkDelay)
|
||||
}
|
||||
},
|
||||
|
||||
/** 发送字符串 */
|
||||
async send(text: string, options?: WriteOptions): Promise<void> {
|
||||
await this.write(stringToArrayBuffer(text), options)
|
||||
},
|
||||
|
||||
/** 发送十六进制字符串,如 '01 0A FF' */
|
||||
async sendHex(hex: string, options?: WriteOptions): Promise<void> {
|
||||
await this.write(hexToArrayBuffer(hex), options)
|
||||
},
|
||||
|
||||
// ---------- 蓝牙接收数据 ----------
|
||||
|
||||
/**
|
||||
* 启用指定特征值的 notify(若已 setupConnection 通常不必再调)
|
||||
*/
|
||||
async enableNotify(params?: {
|
||||
deviceId?: string
|
||||
serviceId?: string
|
||||
characteristicId?: string
|
||||
}): Promise<void> {
|
||||
const deviceId = params?.deviceId || state.connection?.deviceId
|
||||
const serviceId = params?.serviceId || state.connection?.serviceId
|
||||
const characteristicId = params?.characteristicId || state.connection?.notifyCharacteristicId
|
||||
|
||||
if (!deviceId || !serviceId || !characteristicId)
|
||||
throw new Error('缺少 deviceId / serviceId / characteristicId')
|
||||
|
||||
ensureCharacteristicValueListener()
|
||||
await promisify(uni.notifyBLECharacteristicValueChange, {
|
||||
deviceId,
|
||||
serviceId,
|
||||
characteristicId,
|
||||
state: true,
|
||||
})
|
||||
},
|
||||
|
||||
/** 监听接收数据,返回取消监听函数 */
|
||||
onData(callback: DataCallback): () => void {
|
||||
ensureCharacteristicValueListener()
|
||||
listeners.data.add(callback)
|
||||
return () => listeners.data.delete(callback)
|
||||
},
|
||||
|
||||
/** 监听连接状态变化 */
|
||||
onConnectionStateChange(callback: ConnectionStateCallback): () => void {
|
||||
ensureConnectionStateListener()
|
||||
listeners.connection.add(callback)
|
||||
return () => listeners.connection.delete(callback)
|
||||
},
|
||||
|
||||
/** 监听适配器开关状态 */
|
||||
onAdapterStateChange(callback: (available: boolean, discovering: boolean) => void): () => void {
|
||||
const handler = (res: { available: boolean, discovering: boolean }) => {
|
||||
callback(!!res.available, !!res.discovering)
|
||||
}
|
||||
uni.onBluetoothAdapterStateChange(handler)
|
||||
return () => {
|
||||
// uni-app 无对应 off,置空回调避免业务继续执行
|
||||
try {
|
||||
uni.offBluetoothAdapterStateChange?.(handler as any)
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default bluetooth
|
||||
Reference in New Issue
Block a user