Initial commit
This commit is contained in:
839
unpackage/dist/dev/.uvue/app-android/pages/akbletest.uvue
vendored
Normal file
839
unpackage/dist/dev/.uvue/app-android/pages/akbletest.uvue
vendored
Normal file
@@ -0,0 +1,839 @@
|
||||
import { DfuOptions } from "../uni_modules/ak-sbsrv/utssdk/interface";
|
||||
import { ScanDevicesOptions } from "../uni_modules/ak-sbsrv/utssdk/interface";
|
||||
import { BleConnectOptionsExt } from "../uni_modules/ak-sbsrv/utssdk/interface";
|
||||
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
|
||||
import { bluetoothService } from '@/uni_modules/ak-sbsrv/utssdk/app-android/index.uts';
|
||||
import type { BleDevice, BleService, BleCharacteristic } from '@/uni_modules/ak-sbsrv/utssdk/interface.uts';
|
||||
import { ProtocolHandler } from '@/uni_modules/ak-sbsrv/utssdk/protocol_handler.uts';
|
||||
import { dfuManager } from '@/uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts';
|
||||
import { PermissionManager } from '@/ak/PermissionManager.uts';
|
||||
type ShowingCharacteristicsFor = {
|
||||
__$originalPosition?: UTSSourceMapPosition<"ShowingCharacteristicsFor", "pages/akbletest.uvue", 107, 7>;
|
||||
deviceId: string;
|
||||
serviceId: string;
|
||||
};
|
||||
const __sfc__ = defineComponent({
|
||||
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): boolean => 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: any) {
|
||||
this.log('[error] deviceFound handler error: ' + getErrorMessage(err));
|
||||
console.log(err, " at pages/akbletest.uvue:198");
|
||||
}
|
||||
});
|
||||
// scanFinished
|
||||
bluetoothService.on('scanFinished', (payload) => {
|
||||
try {
|
||||
this.scanning = false;
|
||||
this.log('[event] scanFinished -> ' + this._fmt(payload));
|
||||
}
|
||||
catch (err: any) {
|
||||
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): boolean => id !== device.deviceId);
|
||||
this.log(`已移除已断开设备: ${device.deviceId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err: any) {
|
||||
this.log('[error] connectionStateChanged handler error: ' + getErrorMessage(err));
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async startDfuFlow(deviceId: string, staticFilePath: string = ''): Promise<void> {
|
||||
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, " at pages/akbletest.uvue:257");
|
||||
// Generator-friendly: avoid property iteration or bracket indexing.
|
||||
// Serialize and regex-match common file fields (path/uri/tempFilePath/name).
|
||||
try {
|
||||
const s = ((): string => { try {
|
||||
return JSON.stringify(res);
|
||||
}
|
||||
catch (e: any) {
|
||||
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: any) { /* 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
|
||||
} as DfuOptions);
|
||||
this.log('DFU 完成');
|
||||
}
|
||||
catch (e: any) {
|
||||
this.log('DFU 失败: ' + getErrorMessage(e));
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
console.log('选择或读取固件失败: ' + e, " at pages/akbletest.uvue:308");
|
||||
}
|
||||
},
|
||||
_readFileAsUint8Array(path: string): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
console.log('should readfile', " at pages/akbletest.uvue:315");
|
||||
const fsm = uni.getFileSystemManager();
|
||||
console.log(fsm, " at pages/akbletest.uvue:317");
|
||||
// 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: any) {
|
||||
reject(e);
|
||||
}
|
||||
}, fail: (err) => { reject(err); }
|
||||
} as ReadFileOptions);
|
||||
}
|
||||
catch (e: any) {
|
||||
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 as string;
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
catch (e: any) {
|
||||
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 = ((): string => { try {
|
||||
return JSON.stringify(e);
|
||||
}
|
||||
catch (err: any) {
|
||||
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: any) {
|
||||
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): 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): string => normalize(s.trim())).filter((s): boolean => s.length > 0) : [];
|
||||
this.log('开始扫描... optionalServices=' + JSON.stringify(optionalServices));
|
||||
bluetoothService.scanDevices({ "protocols": ['BLE'], "optionalServices": optionalServices } as ScanDevicesOptions)
|
||||
.then(() => {
|
||||
this.log('scanDevices resolved');
|
||||
})
|
||||
.catch((e) => {
|
||||
this.log('[error] scanDevices failed: ' + getErrorMessage(e));
|
||||
this.scanning = false;
|
||||
});
|
||||
}
|
||||
catch (err: any) {
|
||||
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 } as BleConnectOptionsExt).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: any) {
|
||||
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): boolean => 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 } as ShowingCharacteristicsFor;
|
||||
this.characteristics = [];
|
||||
bluetoothService.getCharacteristics(deviceId, serviceId).then((list) => {
|
||||
this.characteristics = list as BleCharacteristic[];
|
||||
console.log('特征数: ' + (list != null ? list.length : 0) + ' [' + deviceId + ']', " at pages/akbletest.uvue:462");
|
||||
// 自动查找可用的写入和通知特征
|
||||
const writeChar = this.characteristics.find((c): boolean => c.properties.write);
|
||||
const notifyChar = this.characteristics.find((c): boolean => 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("协议处理器已初始化,可进行协议测试", " at pages/akbletest.uvue:476");
|
||||
})?.catch(e => {
|
||||
console.log("协议处理器初始化失败: " + getErrorMessage(e!), " at pages/akbletest.uvue:478");
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.log('获取特征失败: ' + getErrorMessage(e!), " at pages/akbletest.uvue:482");
|
||||
});
|
||||
// tracking notifying state
|
||||
// this.$set(this, 'notifyingMap', this.notifyingMap || {});
|
||||
},
|
||||
closeCharacteristics() {
|
||||
this.showingCharacteristicsFor = { deviceId: '', serviceId: '' } as ShowingCharacteristicsFor;
|
||||
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): boolean {
|
||||
return this.notifyingMap.has(uuid) && this.notifyingMap.get(uuid) == true;
|
||||
},
|
||||
async readCharacteristic(deviceId: string, serviceId: string, charId: string): Promise<void> {
|
||||
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: any) {
|
||||
text = '';
|
||||
}
|
||||
const hex = Array.from(new Uint8Array(buf)).map((b): string => b.toString(16).padStart(2, '0')).join(' ');
|
||||
console.log(`读取 ${charId}: text='${text}' hex='${hex}'`, " at pages/akbletest.uvue:511");
|
||||
this.log(`读取 ${charId}: text='${text}' hex='${hex}'`);
|
||||
}
|
||||
catch (e: any) {
|
||||
this.log('读取特征失败: ' + getErrorMessage(e));
|
||||
}
|
||||
},
|
||||
async writeCharacteristic(deviceId: string, serviceId: string, charId: string): Promise<void> {
|
||||
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: any) {
|
||||
this.log('写入特征失败: ' + getErrorMessage(e));
|
||||
}
|
||||
},
|
||||
async toggleNotify(deviceId: string, serviceId: string, charId: string): Promise<void> {
|
||||
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 as ArrayBuffer;
|
||||
}
|
||||
else if (payload != null && typeof payload == 'string') {
|
||||
// some runtimes deliver base64 strings
|
||||
try {
|
||||
const s = atob(payload as string);
|
||||
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: any) {
|
||||
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): string => b.toString(16).padStart(2, '0')).join(' ');
|
||||
this.log(`notify ${charId}: ${hex}`);
|
||||
}
|
||||
catch (e: any) {
|
||||
this.log('notify callback error: ' + getErrorMessage(e));
|
||||
}
|
||||
});
|
||||
map.set(charId, true);
|
||||
this.log(`订阅 ${charId}`);
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
this.log('订阅/取消订阅失败: ' + getErrorMessage(e));
|
||||
}
|
||||
},
|
||||
autoConnect() {
|
||||
if (this.connecting)
|
||||
return;
|
||||
this.connecting = true;
|
||||
const toConnect = this.devices.filter((d): boolean => !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 } as BleConnectOptionsExt).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, " at pages/akbletest.uvue:604");
|
||||
this.log('自动发现接口成功: ' + JSON.stringify(res));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e, " at pages/akbletest.uvue:608");
|
||||
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): Promise<void> {
|
||||
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: any) {
|
||||
this.log('协议处理器不可用或初始化失败,继续使用通用 GATT 查询: ' + ((protoErr != null && protoErr instanceof Error) ? (protoErr as Error).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): string => {
|
||||
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): boolean => {
|
||||
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, " at pages/akbletest.uvue:665");
|
||||
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: any) {
|
||||
text = '';
|
||||
}
|
||||
const hex = Array.from(new Uint8Array(buf)).map((b): string => b.toString(16).padStart(2, '0')).join(' ');
|
||||
console.log(`特征 ${c.uuid} 读取: text='${text}' hex='${hex}'`, " at pages/akbletest.uvue:674");
|
||||
}
|
||||
else {
|
||||
console.log(`特征 ${c.uuid} 不可读`, " at pages/akbletest.uvue:676");
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
console.log(`读取特征 ${c.uuid} 失败: ${getErrorMessage(e)}`, " at pages/akbletest.uvue:679");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
console.log('查询服务 ' + svc + ' 失败: ' + getErrorMessage(e), " at pages/akbletest.uvue:683");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
console.log('获取设备信息失败: ' + getErrorMessage(e), " at pages/akbletest.uvue:688");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
function getErrorMessage(e: null | string | Error): string {
|
||||
if (e == null)
|
||||
return '';
|
||||
if (typeof e == 'string')
|
||||
return e as string;
|
||||
try {
|
||||
return JSON.stringify(e);
|
||||
}
|
||||
catch (err: any) {
|
||||
return '' + (e as Error);
|
||||
}
|
||||
}
|
||||
export default __sfc__;
|
||||
function GenPagesAkbletestRender(this: InstanceType<typeof __sfc__>): any | null {
|
||||
const _ctx = this;
|
||||
const _cache = this.$.renderCache;
|
||||
return _cE("scroll-view", _uM({
|
||||
direction: "vertical",
|
||||
class: "container"
|
||||
}), [
|
||||
_cE("view", _uM({ class: "section" }), [
|
||||
_cE("button", _uM({
|
||||
onClick: _ctx.scanDevices,
|
||||
disabled: _ctx.scanning
|
||||
}), _tD(_ctx.scanning ? '正在扫描...' : '扫描设备'), 9 /* TEXT, PROPS */, ["onClick", "disabled"]),
|
||||
_cE("input", _uM({
|
||||
modelValue: _ctx.optionalServicesInput,
|
||||
onInput: ($event: UniInputEvent) => { (_ctx.optionalServicesInput) = $event.detail.value; },
|
||||
placeholder: "可选服务 UUID, 逗号分隔",
|
||||
style: _nS(_uM({ "margin-left": "12px", "width": "40%" }))
|
||||
}), null, 44 /* STYLE, PROPS, NEED_HYDRATION */, ["modelValue", "onInput"]),
|
||||
_cE("button", _uM({
|
||||
onClick: _ctx.autoConnect,
|
||||
disabled: _ctx.connecting || _ctx.devices.length == 0,
|
||||
style: _nS(_uM({ "margin-left": "12px" }))
|
||||
}), _tD(_ctx.connecting ? '正在自动连接...' : '自动连接'), 13 /* TEXT, STYLE, PROPS */, ["onClick", "disabled"]),
|
||||
_cE("view", null, [
|
||||
_cE("text", null, "设备计数: " + _tD(_ctx.devices.length), 1 /* TEXT */),
|
||||
_cE("text", _uM({
|
||||
style: _nS(_uM({ "font-size": "12px", "color": "gray" }))
|
||||
}), _tD(_ctx._fmt(_ctx.devices)), 5 /* TEXT, STYLE */)
|
||||
]),
|
||||
isTrue(_ctx.devices.length)
|
||||
? _cE("view", _uM({ key: 0 }), [
|
||||
_cE("text", null, "已发现设备:"),
|
||||
_cE(Fragment, null, RenderHelpers.renderList(_ctx.devices, (item, __key, __index, _cached): any => {
|
||||
return _cE("view", _uM({
|
||||
key: item.deviceId,
|
||||
class: "device-item"
|
||||
}), [
|
||||
_cE("text", null, _tD(item.name != '' ? item.name : '未知设备') + " (" + _tD(item.deviceId) + ")", 1 /* TEXT */),
|
||||
_cE("button", _uM({
|
||||
onClick: () => { _ctx.connect(item.deviceId); }
|
||||
}), "连接", 8 /* PROPS */, ["onClick"]),
|
||||
isTrue(_ctx.connectedIds.includes(item.deviceId))
|
||||
? _cE("button", _uM({
|
||||
key: 0,
|
||||
onClick: () => { _ctx.disconnect(item.deviceId); },
|
||||
disabled: _ctx.disconnecting
|
||||
}), "断开", 8 /* PROPS */, ["onClick", "disabled"])
|
||||
: _cC("v-if", true),
|
||||
isTrue(_ctx.connectedIds.includes(item.deviceId))
|
||||
? _cE("button", _uM({
|
||||
key: 1,
|
||||
onClick: () => { _ctx.showServices(item.deviceId); }
|
||||
}), "查看服务", 8 /* PROPS */, ["onClick"])
|
||||
: _cC("v-if", true),
|
||||
isTrue(_ctx.connectedIds.includes(item.deviceId))
|
||||
? _cE("button", _uM({
|
||||
key: 2,
|
||||
onClick: () => { _ctx.autoDiscoverInterfaces(item.deviceId); }
|
||||
}), "自动发现接口", 8 /* PROPS */, ["onClick"])
|
||||
: _cC("v-if", true),
|
||||
isTrue(_ctx.connectedIds.includes(item.deviceId))
|
||||
? _cE("button", _uM({
|
||||
key: 3,
|
||||
onClick: () => { _ctx.getDeviceInfo(item.deviceId); }
|
||||
}), "设备信息", 8 /* PROPS */, ["onClick"])
|
||||
: _cC("v-if", true),
|
||||
isTrue(_ctx.connectedIds.includes(item.deviceId))
|
||||
? _cE("button", _uM({
|
||||
key: 4,
|
||||
onClick: () => { _ctx.startDfuFlow(item.deviceId); }
|
||||
}), "DFU 升级", 8 /* PROPS */, ["onClick"])
|
||||
: _cC("v-if", true),
|
||||
isTrue(_ctx.connectedIds.includes(item.deviceId))
|
||||
? _cE("button", _uM({
|
||||
key: 5,
|
||||
onClick: () => { _ctx.startDfuFlow(item.deviceId, '/static/OmFw2509140009.zip'); }
|
||||
}), "使用内置固件 DFU", 8 /* PROPS */, ["onClick"])
|
||||
: _cC("v-if", true)
|
||||
]);
|
||||
}), 128 /* KEYED_FRAGMENT */)
|
||||
])
|
||||
: _cC("v-if", true)
|
||||
]),
|
||||
_cE("view", _uM({ class: "section" }), [
|
||||
_cE("text", null, "日志:"),
|
||||
_cE("scroll-view", _uM({
|
||||
direction: "vertical",
|
||||
style: _nS(_uM({ "height": "240px" }))
|
||||
}), [
|
||||
_cE(Fragment, null, RenderHelpers.renderList(_ctx.logs, (log, idx, __index, _cached): any => {
|
||||
return _cE("text", _uM({
|
||||
key: idx,
|
||||
style: _nS(_uM({ "font-size": "12px" }))
|
||||
}), _tD(log), 5 /* TEXT, STYLE */);
|
||||
}), 128 /* KEYED_FRAGMENT */)
|
||||
], 4 /* STYLE */)
|
||||
]),
|
||||
isTrue(_ctx.showingServicesFor)
|
||||
? _cE("view", _uM({ key: 0 }), [
|
||||
_cE("view", _uM({ class: "section" }), [
|
||||
_cE("text", null, "设备 " + _tD(_ctx.showingServicesFor) + " 的服务:", 1 /* TEXT */),
|
||||
isTrue(_ctx.services.length)
|
||||
? _cE("view", _uM({ key: 0 }), [
|
||||
_cE(Fragment, null, RenderHelpers.renderList(_ctx.services, (srv, __key, __index, _cached): any => {
|
||||
return _cE("view", _uM({
|
||||
key: srv.uuid,
|
||||
class: "service-item"
|
||||
}), [
|
||||
_cE("text", null, _tD(srv.uuid), 1 /* TEXT */),
|
||||
_cE("button", _uM({
|
||||
onClick: () => { _ctx.showCharacteristics(_ctx.showingServicesFor, srv.uuid); }
|
||||
}), "查看特征", 8 /* PROPS */, ["onClick"])
|
||||
]);
|
||||
}), 128 /* KEYED_FRAGMENT */)
|
||||
])
|
||||
: _cE("view", _uM({ key: 1 }), [
|
||||
_cE("text", null, "无服务")
|
||||
]),
|
||||
_cE("button", _uM({ onClick: _ctx.closeServices }), "关闭", 8 /* PROPS */, ["onClick"])
|
||||
])
|
||||
])
|
||||
: _cC("v-if", true),
|
||||
isTrue(_ctx.showingCharacteristicsFor)
|
||||
? _cE("view", _uM({ key: 1 }), [
|
||||
_cE("view", _uM({ class: "section" }), [
|
||||
_cE("text", null, "服务 的特征:"),
|
||||
isTrue(_ctx.characteristics.length)
|
||||
? _cE("view", _uM({ key: 0 }), [
|
||||
_cE(Fragment, null, RenderHelpers.renderList(_ctx.characteristics, (char, __key, __index, _cached): any => {
|
||||
return _cE("view", _uM({
|
||||
key: char.uuid,
|
||||
class: "char-item"
|
||||
}), [
|
||||
_cE("text", null, _tD(char.uuid) + " [" + _tD(_ctx.charProps(char)) + "]", 1 /* TEXT */),
|
||||
_cE("view", _uM({
|
||||
style: _nS(_uM({ "display": "flex", "flex-direction": "row", "margin-top": "6px" }))
|
||||
}), [
|
||||
isTrue(char.properties?.read)
|
||||
? _cE("button", _uM({
|
||||
key: 0,
|
||||
onClick: () => { _ctx.readCharacteristic(_ctx.showingCharacteristicsFor.deviceId, _ctx.showingCharacteristicsFor.serviceId, char.uuid); }
|
||||
}), "读取", 8 /* PROPS */, ["onClick"])
|
||||
: _cC("v-if", true),
|
||||
isTrue(char.properties?.write)
|
||||
? _cE("button", _uM({
|
||||
key: 1,
|
||||
onClick: () => { _ctx.writeCharacteristic(_ctx.showingCharacteristicsFor.deviceId, _ctx.showingCharacteristicsFor.serviceId, char.uuid); }
|
||||
}), "写入(测试)", 8 /* PROPS */, ["onClick"])
|
||||
: _cC("v-if", true),
|
||||
isTrue(char.properties?.notify)
|
||||
? _cE("button", _uM({
|
||||
key: 2,
|
||||
onClick: () => { _ctx.toggleNotify(_ctx.showingCharacteristicsFor.deviceId, _ctx.showingCharacteristicsFor.serviceId, char.uuid); }
|
||||
}), _tD(_ctx.isNotifying(char.uuid) ? '取消订阅' : '订阅'), 9 /* TEXT, PROPS */, ["onClick"])
|
||||
: _cC("v-if", true)
|
||||
], 4 /* STYLE */)
|
||||
]);
|
||||
}), 128 /* KEYED_FRAGMENT */)
|
||||
])
|
||||
: _cE("view", _uM({ key: 1 }), [
|
||||
_cE("text", null, "无特征")
|
||||
]),
|
||||
_cE("button", _uM({ onClick: _ctx.closeCharacteristics }), "关闭", 8 /* PROPS */, ["onClick"])
|
||||
])
|
||||
])
|
||||
: _cC("v-if", true)
|
||||
]);
|
||||
}
|
||||
const GenPagesAkbletestStyles = [_uM([["container", _pS(_uM([["paddingTop", 16], ["paddingRight", 16], ["paddingBottom", 16], ["paddingLeft", 16], ["flex", 1]]))], ["section", _pS(_uM([["marginBottom", 18]]))], ["device-item", _pS(_uM([["display", "flex"], ["flexDirection", "row"], ["flexWrap", "wrap"]]))], ["service-item", _pS(_uM([["marginTop", 6], ["marginRight", 0], ["marginBottom", 6], ["marginLeft", 0]]))], ["char-item", _pS(_uM([["marginTop", 6], ["marginRight", 0], ["marginBottom", 6], ["marginLeft", 0]]))]])];
|
||||
//# sourceMappingURL=akbletest.uvue.map
|
||||
1
unpackage/dist/dev/.uvue/app-android/pages/akbletest.uvue.map
vendored
Normal file
1
unpackage/dist/dev/.uvue/app-android/pages/akbletest.uvue.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user