Initial commit

This commit is contained in:
2026-03-16 10:37:46 +08:00
commit c052a67816
508 changed files with 22987 additions and 0 deletions

728
pages/akbletest.uvue Normal file
View File

@@ -0,0 +1,728 @@
<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>