Files
akbleserver/pages/akbletest.uvue
2026-03-16 10:37:46 +08:00

728 lines
29 KiB
Plaintext
Raw Permalink 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.
<template>
<scroll-view direction="vertical" class="container">
<view class="section">
<button @click="scanDevices" :disabled="scanning">{{ scanning ? '正在扫描...' : '扫描设备' }}</button>
<!-- <view style="display:flex; flex-direction:row; margin-left:12px; align-items:center">
<text style="margin-right:8px">预设:</text>
<radio-group :modelValue="presetSelected" @change="onPresetChange">
<view v-for="(opt, index) in presetOptions" :key="index"
style="margin-right:8px; display:flex; align-items:center">
<radio :value="opt['value'] as string" />
<text style="margin-left:4px">{{ opt['label'] as string }}</text>
</view>
</radio-group>
</view> -->
<input v-model="optionalServicesInput" placeholder="可选服务 UUID, 逗号分隔" style="margin-left:12px; width: 40%" />
<button @click="autoConnect" :disabled="connecting || devices.length == 0"
style="margin-left:12px;">{{ connecting ? '正在自动连接...' : '自动连接' }}</button>
<!-- Debug: show devices count and raw devices for troubleshooting -->
<view>
<text>设备计数: {{ devices.length }}</text>
<text style="font-size:12px; color:gray">{{ _fmt(devices) }}</text>
</view>
<view v-if="devices.length">
<text>已发现设备:</text>
<view v-for="item in devices" :key="item.deviceId" class="device-item">
<text>{{ item.name!='' ? item.name : '未知设备' }} ({{ item.deviceId }})</text>
<button @click="connect(item.deviceId)">连接</button>
<button v-if="connectedIds.includes(item.deviceId)" @click="disconnect(item.deviceId)"
:disabled="disconnecting">断开</button>
<button v-if="connectedIds.includes(item.deviceId)"
@click="showServices(item.deviceId)">查看服务</button>
<button v-if="connectedIds.includes(item.deviceId)"
@click="autoDiscoverInterfaces(item.deviceId)">自动发现接口</button>
<button v-if="connectedIds.includes(item.deviceId)"
@click="getDeviceInfo(item.deviceId)">设备信息</button>
<!-- DFU 按钮,仅在 APP-ANDROID 可见 -->
<!-- #ifdef APP-ANDROID -->
<button v-if="connectedIds.includes(item.deviceId)" @click="startDfuFlow(item.deviceId)">DFU
升级</button>
<button v-if="connectedIds.includes(item.deviceId)"
@click="startDfuFlow(item.deviceId, '/static/OmFw2509140009.zip')">使用内置固件 DFU</button>
<!-- #endif -->
</view>
</view>
</view>
<view class="section">
<text>日志:</text>
<scroll-view direction="vertical" style="height:240px;">
<text v-for="(log, idx) in logs" :key="idx" style="font-size:12px;">{{ log }}</text>
</scroll-view>
</view>
<view v-if="showingServicesFor">
<view class="section">
<text>设备 {{ showingServicesFor }} 的服务:</text>
<view v-if="services.length">
<view v-for="srv in services" :key="srv.uuid" class="service-item">
<text>{{ srv.uuid }}</text>
<button @click="showCharacteristics(showingServicesFor, srv.uuid)">查看特征</button>
</view>
</view>
<view v-else><text>无服务</text></view>
<button @click="closeServices">关闭</button>
</view>
</view>
<view v-if="showingCharacteristicsFor">
<view class="section">
<text>服务 的特征:</text>
<view v-if="characteristics.length">
<view v-for="char in characteristics" :key="char.uuid" class="char-item">
<text>{{ char.uuid }} [{{ charProps(char) }}]</text>
<view style="display:flex; flex-direction:row; margin-top:6px">
<button v-if="char.properties?.read"
@click="readCharacteristic(showingCharacteristicsFor.deviceId, showingCharacteristicsFor.serviceId, char.uuid)">读取</button>
<button v-if="char.properties?.write"
@click="writeCharacteristic(showingCharacteristicsFor.deviceId, showingCharacteristicsFor.serviceId, char.uuid)">写入(测试)</button>
<button v-if="char.properties?.notify"
@click="toggleNotify(showingCharacteristicsFor.deviceId, showingCharacteristicsFor.serviceId, char.uuid)">{{ isNotifying(char.uuid) ? '取消订阅' : '订阅' }}</button>
</view>
</view>
</view>
<view v-else><text>无特征</text></view>
<button @click="closeCharacteristics">关闭</button>
</view>
</view>
</scroll-view>
</template>
<script lang="uts">
import { BluetoothService } from '@/uni_modules/ak-sbsrv/utssdk/interface.uts'
// Platform-specific entrypoint: import the platform index per build target to avoid bundler including Android-only code in web builds
// #ifdef APP-ANDROID
import { bluetoothService } from '@/uni_modules/ak-sbsrv/utssdk/app-android/index.uts'
// #endif
// #ifdef WEB
import { bluetoothService } from '@/uni_modules/ak-sbsrv/utssdk/web/index.uts'
// #endif
import type { BleDevice, BleService, BleCharacteristic } from '@/uni_modules/ak-sbsrv/utssdk/interface.uts'
import { ProtocolHandler } from '@/uni_modules/ak-sbsrv/utssdk/protocol_handler.uts'
// #ifdef APP-ANDROID
import { dfuManager } from '@/uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts'
// #endif
import { PermissionManager } from '@/ak/PermissionManager.uts'
type ShowingCharacteristicsFor = {
deviceId : string,
serviceId : string
}
export default {
data() {
return {
scanning: false,
connecting: false,
disconnecting: false,
devices: [] as BleDevice[],
connectedIds: [] as string[],
logs: [] as string[],
showingServicesFor: '',
services: [] as BleService[],
showingCharacteristicsFor: { deviceId: '', serviceId: '' } as ShowingCharacteristicsFor,
characteristics: [] as BleCharacteristic[],
// 新增协议相关参数
protocolDeviceId: '',
protocolServiceId: '',
protocolWriteCharId: '',
protocolNotifyCharId: '',
// protocol handler instances/cache
protocolHandlerMap: new Map<string, ProtocolHandler>(),
protocolHandler: null as ProtocolHandler | null,
// optional services input (comma-separated UUIDs)
optionalServicesInput: '',
// presets for common BLE services (label -> UUID). 'custom' allows free-form input.
presetOptions: [
{ label: '无', value: '' },
{ label: 'Battery Service (180F)', value: '0000180f-0000-1000-8000-00805f9b34fb' },
{ label: 'Device Information (180A)', value: '0000180a-0000-1000-8000-00805f9b34fb' },
{ label: 'Generic Attribute (1801)', value: '00001801-0000-1000-8000-00805f9b34fb' },
{ label: 'Nordic DFU', value: '00001530-1212-efde-1523-785feabcd123' },
{ label: 'Nordic UART (NUS)', value: '6e400001-b5a3-f393-e0a9-e50e24dcca9e' },
{ label: '自定义', value: 'custom' }
],
presetSelected: '',
// map of characteristicId -> boolean (is currently subscribed)
notifyingMap: new Map<string, boolean>(),
}
},
mounted() {
PermissionManager.requestBluetoothPermissions((granted : boolean) => {
if (!granted) {
uni.showToast({ title: '请授权蓝牙和定位权限', icon: 'none' });
}
});
this.log('页面 mounted: 初始化事件监听和蓝牙权限请求完成')
// deviceFound - only accept devices whose name starts with 'CF' or 'BCL'
bluetoothService.on('deviceFound', (payload) => {
try {
// this.log('[event] deviceFound -> ' + this._fmt(payload))
// console.log('[event] deviceFound -> ' + this._fmt(payload))
// payload can be UTSJSONObject-like or plain object. Normalize.
let rawDevice = payload?.device;
if (rawDevice == null) {
this.log('[event] deviceFound - payload.device is null, ignoring');
return;
}
// extract name
let name : string | null = rawDevice.name;
if (name == null) {
this.log('[event] deviceFound - 无名称,忽略: ' + this._fmt(rawDevice as any))
return;
}
const n = name as string;
if (!(n.startsWith('CF') || n.startsWith('BCL'))) {
this.log('[event] deviceFound - 名称不匹配前缀,忽略: ' + n)
return;
}
const exists = this.devices.some(d => d != null && d.name == n);
if (!exists) {
// rawDevice is non-null here per earlier guard
this.devices.push(rawDevice as BleDevice);
const deviceIdStr = (rawDevice.deviceId != null) ? rawDevice.deviceId : '';
this.log('发现设备: ' + n + ' (' + deviceIdStr + ')');
} else {
const deviceIdStr = (rawDevice.deviceId != null) ? rawDevice.deviceId : '';
this.log('发现重复设备: ' + n + ' (' + deviceIdStr + ')')
}
} catch (err) {
this.log('[error] deviceFound handler error: ' + getErrorMessage(err))
console.log(err)
}
})
// scanFinished
bluetoothService.on('scanFinished', (payload) => {
try {
this.scanning = false
this.log('[event] scanFinished -> ' + this._fmt(payload))
} catch (err) {
this.log('[error] scanFinished handler error: ' + getErrorMessage(err))
}
})
// connectionStateChanged
bluetoothService.on('connectionStateChanged', (payload) => {
try {
this.log('[event] connectionStateChanged -> ' + this._fmt(payload))
if (payload != null) {
const device = payload.device
const state = payload.state
this.log(`设备 ${device?.deviceId} 连接状态变为: ${state}`)
// maintain connectedIds
if (state == 2) {
if (device != null && device.deviceId != null && !this.connectedIds.includes(device.deviceId)) {
this.connectedIds.push(device.deviceId)
this.log(`已记录已连接设备: ${device.deviceId}`)
}
} else if (state == 0) {
if (device != null && device.deviceId != null) {
this.connectedIds = this.connectedIds.filter(id => id !== device.deviceId)
this.log(`已移除已断开设备: ${device.deviceId}`)
}
}
}
} catch (err) {
this.log('[error] connectionStateChanged handler error: ' + getErrorMessage(err))
}
})
},
methods: {
async startDfuFlow(deviceId : string, staticFilePath : string = '') {
if (staticFilePath != null && staticFilePath !== '') {
this.log('DFU 开始: 使用内置固件文件 ' + staticFilePath)
} else {
this.log('DFU 开始: 请选择固件文件')
}
try {
let chosenPath : string | null = null
let fileName : string | null = null
if (staticFilePath != null && staticFilePath !== '') {
// Use the app's bundled static file path
chosenPath = staticFilePath.replace(/^\/+/, '')
const tmpName = staticFilePath.split(/[\/]/).pop()
fileName = (tmpName != null && tmpName !== '') ? tmpName : staticFilePath
} else {
const res = await new Promise<any>((resolve, reject) => {
uni.chooseFile({ count: 1, success: (r) => resolve(r), fail: (e) => reject(e) })
})
console.log(res)
// Generator-friendly: avoid property iteration or bracket indexing.
// Serialize and regex-match common file fields (path/uri/tempFilePath/name).
try {
const s = (() => { try { return JSON.stringify(res); } catch (e) { return ''; } })()
const m = s.match(/"(?:path|uri|tempFilePath|temp_file_path|tempFilePath|name)"\s*:\s*"([^"]+)"/i)
if (m != null && m.length >= 2) {
const capturedCandidate : string | null = (m[1] != null ? m[1] : null)
const captured : string = capturedCandidate != null ? capturedCandidate : ''
if (captured !== '') {
chosenPath = captured
const toTest : string = captured
if (!(/^[a-zA-Z]:\\|^\\\//.test(toTest) || /:\/\//.test(toTest))) {
const m2 = s.match(/"(?:path|uri|tempFilePath|temp_file_path|tempFilePath)"\s*:\s*"([^"]+)"/i)
if (m2 != null && m2.length >= 2 && m2[1] != null) {
const pathCandidate : string = m2[1] != null ? ('' + m2[1]) : ''
if (pathCandidate !== '') chosenPath = pathCandidate
}
}
}
}
const nameMatch = s.match(/"name"\s*:\s*"([^"]+)"/i)
if (nameMatch != null && nameMatch.length >= 2 && nameMatch[1] != null) {
const nm : string = nameMatch[1] != null ? ('' + nameMatch[1]) : ''
if (nm !== '') fileName = nm
}
} catch (err) { /* ignore */ }
}
if (chosenPath == null || chosenPath == '') {
this.log('未选择文件')
return
}
// filePath is non-null and non-empty here
const fpStr : string = chosenPath as string
const lastSeg = fpStr.split(/[\/]/).pop();
const displayName = (fileName != null && fileName !== '') ? fileName : (lastSeg != null && lastSeg !== '' ? lastSeg : fpStr)
this.log('已选文件: ' + displayName + ' 路径: ' + fpStr)
const bytes = await this._readFileAsUint8Array(fpStr)
this.log('固件读取完成, 大小: ' + bytes.length)
try {
await dfuManager.startDfu(deviceId, bytes, {
useNordic: false,
onProgress: (p : number) => this.log('DFU 进度: ' + p + '%'),
onLog: (s : string) => this.log('DFU: ' + s),
controlTimeout: 30000
})
this.log('DFU 完成')
} catch (e) {
this.log('DFU 失败: ' + getErrorMessage(e))
}
} catch (e) {
console.log('选择或读取固件失败: ' + e)
}
},
_readFileAsUint8Array(path : string) : Promise<Uint8Array> {
return new Promise((resolve, reject) => {
try {
console.log('should readfile')
const fsm = uni.getFileSystemManager()
console.log(fsm)
// Read file as ArrayBuffer directly to avoid base64 encoding issues
fsm.readFile({
filePath: path, success: (res) => {
try {
const data = res.data as ArrayBuffer
const arr = new Uint8Array(data)
resolve(arr)
} catch (e) { reject(e) }
}, fail: (err) => { reject(err) }
})
} catch (e) { reject(e) }
})
},
log(msg : string) {
const ts = new Date().toISOString();
this.logs.unshift(`[${ts}] ${msg}`)
if (this.logs.length > 100) this.logs.length = 100
},
_fmt(obj : any) : string {
try {
if (obj == null) return 'null'
if (typeof obj == 'string') return obj
return JSON.stringify(obj)
} catch (e) {
return '' + obj
}
},
onPresetChange(e : any) {
try {
// Some platforms emit { detail: { value: 'x' } }, others emit { value: 'x' } or just 'x'.
// Serialize and regex-extract to avoid direct property access that the UTS->Kotlin generator may emit incorrectly.
const s = (() => { try { return JSON.stringify(e); } catch (err) { return ''; } })()
let val : string = this.presetSelected
// try detail.value first
const m = s.match(/"detail"\s*:\s*\{[^}]*"value"\s*:\s*"([^\"]+)"/i)
if (m != null && m.length >= 2 && m[1] != null) {
val = '' + m[1]
} else {
const m2 = s.match(/"value"\s*:\s*"([^\"]+)"/i)
if (m2 != null && m2.length >= 2 && m2[1] != null) {
val = '' + m2[1]
}
}
this.presetSelected = val
if (val == 'custom' || val == '') {
this.log('已选择预设: ' + (val == 'custom' ? '自定义' : '无'))
return;
}
this.optionalServicesInput = val;
this.log('已选择预设服务 UUID: ' + val)
} catch (err) {
this.log('[error] onPresetChange: ' + getErrorMessage(err))
}
},
scanDevices() {
try {
this.scanning = true
this.devices = []
// prepare optional services: prefer free-form input, otherwise use selected preset (unless preset is 'custom' or empty)
let raw = (this.optionalServicesInput != null ? this.optionalServicesInput : '').trim();
if (raw.length == 0 && this.presetSelected != null && this.presetSelected !== '' && this.presetSelected !== 'custom') {
raw = this.presetSelected;
}
// normalize helper: expand 16-bit UUIDs like '180F' to full 128-bit UUIDs
const normalize = (s : string) => {
if (s == null || s.length == 0) return '';
const u = s.toLowerCase().replace(/^0x/, '').trim();
const hex = u.replace(/[^0-9a-f]/g, '');
if (/^[0-9a-f]{4}$/.test(hex)) return `0000${hex}-0000-1000-8000-00805f9b34fb`;
return s;
};
const optionalServices = raw.length > 0 ? raw.split(',').map(s => normalize(s.trim())).filter(s => s.length > 0) : []
this.log('开始扫描... optionalServices=' + JSON.stringify(optionalServices))
bluetoothService.scanDevices({ "protocols": ['BLE'], "optionalServices": optionalServices })
.then(() => {
this.log('scanDevices resolved')
})
.catch((e) => {
this.log('[error] scanDevices failed: ' + getErrorMessage(e))
this.scanning = false
})
} catch (err) {
this.log('[error] scanDevices thrown: ' + getErrorMessage(err))
this.scanning = false
}
},
connect(deviceId : string) {
this.connecting = true
this.log(`connect start -> ${deviceId}`)
try {
bluetoothService.connectDevice(deviceId, 'BLE', { timeout: 10000 }).then(() => {
if (!this.connectedIds.includes(deviceId)) this.connectedIds.push(deviceId)
this.log('连接成功: ' + deviceId)
}).catch((e) => {
this.log('连接失败: ' + getErrorMessage(e!));
}).finally(() => {
this.connecting = false
this.log(`connect finished -> ${deviceId}`)
})
} catch (err) {
this.log('[error] connect thrown: ' + getErrorMessage(err))
this.connecting = false
}
},
disconnect(deviceId : string) {
if (!this.connectedIds.includes(deviceId)) return
this.disconnecting = true
this.log(`disconnect start -> ${deviceId}`)
bluetoothService.disconnectDevice(deviceId, 'BLE').then(() => {
this.log('已断开: ' + deviceId)
this.connectedIds = this.connectedIds.filter(id => id !== deviceId)
// 清理协议处理器缓存
this.protocolHandlerMap.delete(deviceId)
}).catch((e) => {
this.log('断开失败: ' + getErrorMessage(e!));
}).finally(() => {
this.disconnecting = false
this.log(`disconnect finished -> ${deviceId}`)
})
},
showServices(deviceId : string) {
this.showingServicesFor = deviceId
this.services = []
this.log(`showServices start -> ${deviceId}`)
bluetoothService.getServices(deviceId).then((list) => {
this.log('showServices result -> ' + this._fmt(list))
this.services = list as BleService[]
this.log('服务数: ' + (list != null ? list.length : 0) + ' [' + deviceId + ']');
}).catch((e) => {
this.log('获取服务失败: ' + getErrorMessage(e!));
}).finally(() => {
this.log(`showServices finished -> ${deviceId}`)
});
},
closeServices() {
this.showingServicesFor = ''
this.services = []
},
showCharacteristics(deviceId : string, serviceId : string) {
this.showingCharacteristicsFor = { deviceId, serviceId }
this.characteristics = []
bluetoothService.getCharacteristics(deviceId, serviceId).then((list) => {
this.characteristics = list as BleCharacteristic[]
console.log('特征数: ' + (list != null ? list.length : 0) + ' [' + deviceId + ']');
// 自动查找可用的写入和通知特征
const writeChar = this.characteristics.find(c => c.properties.write)
const notifyChar = this.characteristics.find(c => c.properties.notify)
if (writeChar != null && notifyChar != null) {
this.protocolDeviceId = deviceId
this.protocolServiceId = serviceId
this.protocolWriteCharId = writeChar.uuid
this.protocolNotifyCharId = notifyChar.uuid
let abs = bluetoothService as BluetoothService
this.protocolHandler = new ProtocolHandler(abs)
let handler = this.protocolHandler
handler?.setConnectionParameters(deviceId, serviceId, writeChar.uuid, notifyChar.uuid)
handler?.initialize()?.then(() => {
console.log("协议处理器已初始化,可进行协议测试")
})?.catch(e => {
console.log("协议处理器初始化失败: " + getErrorMessage(e!))
})
}
}).catch((e) => {
console.log('获取特征失败: ' + getErrorMessage(e!));
});
// tracking notifying state
// this.$set(this, 'notifyingMap', this.notifyingMap || {});
},
closeCharacteristics() {
this.showingCharacteristicsFor = { deviceId: '', serviceId: '' }
this.characteristics = []
},
charProps(char : BleCharacteristic) : string {
const p = char.properties
const parts = [] as string[]
if (p.read) parts.push('R')
if (p.write) parts.push('W')
if (p.notify) parts.push('N')
if (p.indicate) parts.push('I')
return parts.join('/')
// return [p.read ? 'R' : '', p.write ? 'W' : '', p.notify ? 'N' : '', p.indicate ? 'I' : ''].filter(Boolean).join('/')
},
isNotifying(uuid : string) {
return this.notifyingMap.has(uuid) && this.notifyingMap.get(uuid) == true
},
async readCharacteristic(deviceId : string, serviceId : string, charId : string) {
try {
this.log(`readCharacteristic ${charId} ...`)
const buf = await bluetoothService.readCharacteristic(deviceId, serviceId, charId)
let text = ''
try { text = new TextDecoder().decode(new Uint8Array(buf)) } catch (e) { text = '' }
const hex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(' ')
console.log(`读取 ${charId}: text='${text}' hex='${hex}'`)
this.log(`读取 ${charId}: text='${text}' hex='${hex}'`)
} catch (e) {
this.log('读取特征失败: ' + getErrorMessage(e))
}
},
async writeCharacteristic(deviceId : string, serviceId : string, charId : string) {
try {
const payload = new Uint8Array([0x01])
const ok = await bluetoothService.writeCharacteristic(deviceId, serviceId, charId, payload, null)
if (ok) this.log(`写入 ${charId} 成功`);
else this.log(`写入 ${charId} 失败`);
} catch (e) {
this.log('写入特征失败: ' + getErrorMessage(e))
}
},
async toggleNotify(deviceId : string, serviceId : string, charId : string) {
try {
const map = this.notifyingMap
const cur = map.get(charId) == true
if (cur) {
// unsubscribe
await bluetoothService.unsubscribeCharacteristic(deviceId, serviceId, charId)
map.set(charId, false)
this.log(`取消订阅 ${charId}`)
} else {
// subscribe with callback
await bluetoothService.subscribeCharacteristic(deviceId, serviceId, charId, (payload : any) => {
let data : ArrayBuffer | null = null
try {
if (payload instanceof ArrayBuffer) {
data = payload
} else if (payload != null && typeof payload == 'string') {
// some runtimes deliver base64 strings
try {
const s = atob(payload)
const tmp = new Uint8Array(s.length)
for (let i = 0; i < s.length; i++) {
const ch = s.charCodeAt(i)
tmp[i] = (ch == null) ? 0 : (ch & 0xff)
}
data = tmp.buffer
} catch (e) { data = null }
} else if (payload != null && (payload as UTSJSONObject).get('data') instanceof ArrayBuffer) {
data = (payload as UTSJSONObject).get('data') as ArrayBuffer
}
const arr = data != null ? new Uint8Array(data) : new Uint8Array([])
const hex = Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join(' ')
this.log(`notify ${charId}: ${hex}`)
} catch (e) { this.log('notify callback error: ' + getErrorMessage(e)) }
})
map.set(charId, true)
this.log(`订阅 ${charId}`)
}
} catch (e) {
this.log('订阅/取消订阅失败: ' + getErrorMessage(e))
}
},
autoConnect() {
if (this.connecting) return;
this.connecting = true;
const toConnect = this.devices.filter(d => !this.connectedIds.includes(d.deviceId));
if (toConnect.length == 0) {
this.log('没有可自动连接的设备');
this.connecting = false;
return;
}
let successCount = 0;
let failCount = 0;
let finished = 0;
toConnect.forEach(device => {
bluetoothService.connectDevice(device.deviceId, 'BLE', { timeout: 10000 }).then(() => {
if (!this.connectedIds.includes(device.deviceId)) this.connectedIds.push(device.deviceId);
this.log('自动连接成功: ' + device.deviceId);
successCount++;
// this.getOrInitProtocolHandler(device.deviceId);
}).catch((e) => {
this.log('自动连接失败: ' + device.deviceId + ' ' + getErrorMessage(e!));
failCount++;
}).finally(() => {
finished++;
if (finished == toConnect.length) {
this.connecting = false;
this.log(`自动连接完成,成功${successCount},失败${failCount}`);
}
});
});
},
autoDiscoverInterfaces(deviceId : string) {
this.log('自动发现接口中...')
bluetoothService.getAutoBleInterfaces(deviceId)
.then((res) => {
console.log(res)
this.log('自动发现接口成功: ' + JSON.stringify(res))
})
.catch((e) => {
console.log(e)
this.log('自动发现接口失败: ' + getErrorMessage(e!))
})
},
// 新增:测试电量功能
async getOrInitProtocolHandler(deviceId : string) : Promise<ProtocolHandler> {
let handler = this.protocolHandlerMap.get(deviceId);
if (handler == null) {
// 自动发现接口
const res = await bluetoothService.getAutoBleInterfaces(deviceId);
handler = new ProtocolHandler(bluetoothService as BluetoothService);
handler.setConnectionParameters(deviceId, res.serviceId, res.writeCharId, res.notifyCharId);
await handler.initialize();
this.protocolHandlerMap.set(deviceId, handler);
this.log(`协议处理器已初始化: ${deviceId}`);
}
return handler!;
},
async getDeviceInfo(deviceId : string) {
this.log('获取设备信息中...');
try {
// First try protocol handler (if device exposes custom protocol)
try {
const handler = await this.getOrInitProtocolHandler(deviceId);
// 获取电量
const battery = await handler.testBatteryLevel();
this.log('协议: 电量: ' + battery);
// 获取软件/硬件版本
const swVersion = await handler.testVersionInfo(false);
this.log('协议: 软件版本: ' + swVersion);
const hwVersion = await handler.testVersionInfo(true);
this.log('协议: 硬件版本: ' + hwVersion);
} catch (protoErr) {
this.log('协议处理器不可用或初始化失败,继续使用通用 GATT 查询: ' + ((protoErr != null && protoErr instanceof Error) ? protoErr.message : this._fmt(protoErr)));
}
// Additionally, attempt to read standard services: Generic Access (0x1800), Generic Attribute (0x1801), Battery (0x180F)
const stdServices = ['1800', '1801', '180f'].map(s => {
const hex = s.toLowerCase().replace(/^0x/, '');
return /^[0-9a-f]{4}$/.test(hex) ? `0000${hex}-0000-1000-8000-00805f9b34fb` : s;
});
// fetch services once to avoid repeated GATT server queries
const services = await bluetoothService.getServices(deviceId);
for (const svc of stdServices) {
try {
this.log('读取服务: ' + svc);
// find matching service
const found = services.find((x : any) => {
const uuid = (x as UTSJSONObject).get('uuid')
return uuid != null && uuid.toString().toLowerCase() == svc.toLowerCase()
});
if (found == null) {
this.log('未发现服务 ' + svc + '(需重新扫描并包含 optionalServices');
continue;
}
const chars = await bluetoothService.getCharacteristics(deviceId, found?.uuid as string);
console.log(`服务 ${svc} 包含 ${chars.length} 个特征`, chars);
for (const c of chars) {
try {
if (c.properties?.read == true) {
const buf = await bluetoothService.readCharacteristic(deviceId, found?.uuid as string, c.uuid);
// try to decode as utf8 then hex
let text = '';
try { text = new TextDecoder().decode(new Uint8Array(buf)); } catch (e) { text = ''; }
const hex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(' ');
console.log(`特征 ${c.uuid} 读取: text='${text}' hex='${hex}'`);
} else {
console.log(`特征 ${c.uuid} 不可读`);
}
} catch (e) {
console.log(`读取特征 ${c.uuid} 失败: ${getErrorMessage(e)}`);
}
}
} catch (e) {
console.log('查询服务 ' + svc + ' 失败: ' + getErrorMessage(e));
}
}
} catch (e) {
console.log('获取设备信息失败: ' + getErrorMessage(e));
}
}
}
}
function getErrorMessage(e : Error | string | null) : string {
if (e == null) return '';
if (typeof e == 'string') return e;
try {
return JSON.stringify(e);
} catch (err) {
return '' + e;
}
}
</script>
<style scoped>
.container {
padding: 16px;
flex: 1;
}
.section {
margin-bottom: 18px;
}
.device-item {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.service-item,
.char-item {
margin: 6px 0;
}
button {
margin-left: 8px;
}
</style>