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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,44 @@
// import { initTables } from './ak/sqlite.uts'
let firstBackTime = 0
const __sfc__ = defineApp({
onLaunch: function () {
console.log('App Launch', " at App.uvue:10")
// initTables();
},
onShow: function () {
console.log('App Show', " at App.uvue:15")
},
onHide: function () {
console.log('App Hide', " at App.uvue:18")
},
onLastPageBackPress: function () {
console.log('App LastPageBackPress', " at App.uvue:22")
if (firstBackTime == 0) {
uni.showToast({
title: '再按一次退出应用',
position: 'bottom',
})
firstBackTime = Date.now()
setTimeout(() => {
firstBackTime = 0
}, 2000)
} else if (Date.now() - firstBackTime < 2000) {
firstBackTime = Date.now()
uni.exit()
}
},
onExit: function () {
console.log('App Exit', " at App.uvue:39")
},
})
export default __sfc__
const GenAppStyles = [_uM([["uni-row", _pS(_uM([["flexDirection", "row"]]))], ["uni-column", _pS(_uM([["flexDirection", "column"]]))]])]

View File

@@ -0,0 +1 @@
{"version":3,"sources":["App.uvue"],"names":[],"mappings":";;CAEC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;;;AAI/C,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE;AACpB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;EACb,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;GACrB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;GACxB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;EAEhB,CAAC;EACD,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;GACnB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;EACvB,CAAC;EACD,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;GACnB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;EACvB,CAAC;;EAED,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;GAChC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;GACnC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;IACvB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KACb,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KACjB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC;IACD,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;KAChB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE;IACjB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;GACR,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;IAC7C,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;GACV;EACD,CAAC;;EAED,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;GACnB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;EACvB,CAAC;CACF","file":"App.uvue","sourceRoot":"","sourcesContent":["<script lang=\"uts\">\r\n\r\n\t// import { initTables } from './ak/sqlite.uts'\r\n\t\r\n\r\n\r\nlet firstBackTime = 0\r\nexport default {\r\n\t\tonLaunch: function () {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t\t// initTables();\r\n\r\n\t\t},\r\n\t\tonShow: function () {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function () {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t},\r\n\r\n\t\tonLastPageBackPress: function () {\r\n\t\t\tconsole.log('App LastPageBackPress')\r\n\t\t\tif (firstBackTime == 0) {\r\n\t\t\t\tuni.showToast({\r\n\t\t\t\t\ttitle: '再按一次退出应用',\r\n\t\t\t\t\tposition: 'bottom',\r\n\t\t\t\t})\r\n\t\t\t\tfirstBackTime = Date.now()\r\n\t\t\t\tsetTimeout(() => {\r\n\t\t\t\t\tfirstBackTime = 0\r\n\t\t\t\t}, 2000)\r\n\t\t\t} else if (Date.now() - firstBackTime < 2000) {\r\n\t\t\t\tfirstBackTime = Date.now()\r\n\t\t\t\tuni.exit()\r\n\t\t\t}\r\n\t\t},\r\n\r\n\t\tonExit: function () {\r\n\t\t\tconsole.log('App Exit')\r\n\t\t},\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n\t.uni-row {\r\n\t\tflex-direction: row;\r\n\t}\r\n\r\n\t.uni-column {\r\n\t\tflex-direction: column;\r\n\t}\r\n</style>"]}

View File

@@ -0,0 +1,366 @@
/**
* PermissionManager.uts
*
* Utility class for managing Android permissions throughout the app
* Handles requesting permissions, checking status, and directing users to settings
*/
/**
* Common permission types that can be requested
*/
export enum PermissionType {
BLUETOOTH = 'bluetooth',
LOCATION = 'location',
STORAGE = 'storage',
CAMERA = 'camera',
MICROPHONE = 'microphone',
NOTIFICATIONS = 'notifications',
CALENDAR = 'calendar',
CONTACTS = 'contacts',
SENSORS = 'sensors'
}
/**
* Result of a permission request
*/
type PermissionResult = {
granted: boolean;
grantedPermissions: string[];
deniedPermissions: string[];
}
/**
* Manages permission requests and checks throughout the app
*/
export class PermissionManager {
/**
* Maps permission types to the actual Android permission strings
*/
private static getPermissionsForType(type: PermissionType): string[] {
switch (type) {
case PermissionType.BLUETOOTH:
return [
'android.permission.BLUETOOTH_SCAN',
'android.permission.BLUETOOTH_CONNECT',
'android.permission.BLUETOOTH_ADVERTISE'
];
case PermissionType.LOCATION:
return [
'android.permission.ACCESS_FINE_LOCATION',
'android.permission.ACCESS_COARSE_LOCATION'
];
case PermissionType.STORAGE:
return [
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.WRITE_EXTERNAL_STORAGE'
];
case PermissionType.CAMERA:
return ['android.permission.CAMERA'];
case PermissionType.MICROPHONE:
return ['android.permission.RECORD_AUDIO'];
case PermissionType.NOTIFICATIONS:
return ['android.permission.POST_NOTIFICATIONS'];
case PermissionType.CALENDAR:
return [
'android.permission.READ_CALENDAR',
'android.permission.WRITE_CALENDAR'
];
case PermissionType.CONTACTS:
return [
'android.permission.READ_CONTACTS',
'android.permission.WRITE_CONTACTS'
];
case PermissionType.SENSORS:
return ['android.permission.BODY_SENSORS'];
default:
return [];
}
}
/**
* Get appropriate display name for a permission type
*/
private static getPermissionDisplayName(type: PermissionType): string {
switch (type) {
case PermissionType.BLUETOOTH:
return '蓝牙';
case PermissionType.LOCATION:
return '位置';
case PermissionType.STORAGE:
return '存储';
case PermissionType.CAMERA:
return '相机';
case PermissionType.MICROPHONE:
return '麦克风';
case PermissionType.NOTIFICATIONS:
return '通知';
case PermissionType.CALENDAR:
return '日历';
case PermissionType.CONTACTS:
return '联系人';
case PermissionType.SENSORS:
return '身体传感器';
default:
return '未知权限';
}
}
/**
* Check if a permission is granted
* @param type The permission type to check
* @returns True if the permission is granted, false otherwise
*/
static isPermissionGranted(type: PermissionType): boolean {
try {
const permissions = this.getPermissionsForType(type);
const activity = UTSAndroid.getUniActivity();
if (activity == null || permissions.length === 0) {
return false;
}
// Check each permission in the group
for (const permission of permissions) {
if (!UTSAndroid.checkSystemPermissionGranted(activity, [permission])) {
return false;
}
}
return true;
} catch (e) {
__f__('error','at ak/PermissionManager.uts:132',`Error checking ${type} permission:`, e);
return false;
}
}
/**
* Request a permission from the user
* @param type The permission type to request
* @param callback Function to call with the result of the permission request
* @param showRationale Whether to show a rationale dialog if permission was previously denied
*/
static requestPermission(
type: PermissionType,
callback: (result: PermissionResult) => void,
showRationale: boolean = true
): void {
try {
const permissions = this.getPermissionsForType(type);
const activity = UTSAndroid.getUniActivity();
if (activity == null || permissions.length === 0) {
callback({
granted: false,
grantedPermissions: [],
deniedPermissions: permissions
});
return;
}
// Check if already granted
let allGranted = true;
for (const permission of permissions) {
if (!UTSAndroid.checkSystemPermissionGranted(activity, [permission])) {
allGranted = false;
break;
}
}
if (allGranted) {
callback({
granted: true,
grantedPermissions: permissions,
deniedPermissions: []
});
return;
}
// Request the permissions
UTSAndroid.requestSystemPermission(
activity,
permissions,
(granted: boolean, grantedPermissions: string[]) => {
if (granted) {
callback({
granted: true,
grantedPermissions: grantedPermissions,
deniedPermissions: []
});
} else if (showRationale) {
// Show rationale dialog
this.showPermissionRationale(type, callback);
} else {
// Just report the denial
callback({
granted: false,
grantedPermissions: grantedPermissions,
deniedPermissions: this.getDeniedPermissions(permissions, grantedPermissions)
});
}
},
(denied: boolean, deniedPermissions: string[]) => {
callback({
granted: false,
grantedPermissions: this.getGrantedPermissions(permissions, deniedPermissions),
deniedPermissions: deniedPermissions
});
}
);
} catch (e) {
__f__('error','at ak/PermissionManager.uts:217',`Error requesting ${type} permission:`, e);
callback({
granted: false,
grantedPermissions: [],
deniedPermissions: this.getPermissionsForType(type)
});
}
}
/**
* Show a rationale dialog explaining why the permission is needed
*/
private static showPermissionRationale(
type: PermissionType,
callback: (result: PermissionResult) => void
): void {
const permissionName = this.getPermissionDisplayName(type);
uni.showModal({
title: '权限申请',
content: `需要${permissionName}权限才能使用相关功能`,
confirmText: '去设置',
cancelText: '取消',
success: (result) => {
if (result.confirm) {
this.openAppSettings();
callback({
granted: false,
grantedPermissions: [],
deniedPermissions: this.getPermissionsForType(type)
});
} else {
callback({
granted: false,
grantedPermissions: [],
deniedPermissions: this.getPermissionsForType(type)
});
}
}
});
}
/**
* Open the app settings page
*/
static openAppSettings(): void {
try {
const context = UTSAndroid.getAppContext();
if (context != null) {
const intent = new android.content.Intent();
intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
const uri = android.net.Uri.fromParts("package", context.getPackageName(), null);
intent.setData(uri);
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
} catch (e) {
__f__('error','at ak/PermissionManager.uts:285','Failed to open app settings', e);
uni.showToast({
title: '请手动前往系统设置修改应用权限',
icon: 'none',
duration: 3000
});
}
}
/**
* Helper to get the list of granted permissions
*/
private static getGrantedPermissions(allPermissions: string[], deniedPermissions: string[]): string[] {
return allPermissions.filter(p => !deniedPermissions.includes(p));
}
/**
* Helper to get the list of denied permissions
*/
private static getDeniedPermissions(allPermissions: string[], grantedPermissions: string[]): string[] {
return allPermissions.filter(p => !grantedPermissions.includes(p));
}
/**
* Request multiple permission types at once
* @param types Array of permission types to request
* @param callback Function to call when all permissions have been processed
*/
static requestMultiplePermissions(
types: PermissionType[],
callback: (results: Map<PermissionType, PermissionResult>) => void
): void {
if (types.length === 0) {
callback(new Map());
return;
}
const results = new Map<PermissionType, PermissionResult>();
let remaining = types.length;
for (const type of types) {
this.requestPermission(
type,
(result) => {
results.set(type, result);
remaining--;
if (remaining === 0) {
callback(results);
}
},
true
);
}
}
/**
* Convenience method to request Bluetooth permissions
* @param callback Function to call after the permission request
*/
static requestBluetoothPermissions(callback: (granted: boolean) => void): void {
this.requestPermission(PermissionType.BLUETOOTH, (result) => {
// For Bluetooth, we also need location permissions on Android
if (result.granted) {
this.requestPermission(PermissionType.LOCATION, (locationResult) => {
callback(locationResult.granted);
});
} else {
callback(false);
}
});
}
}

View File

@@ -0,0 +1,41 @@
import 'F:/HBuilderX/plugins/uniapp-cli-vite/node_modules/@dcloudio/uni-console/src/runtime/app/index.ts';import App from './App.uvue'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
export function main(app: IApp) {
definePageRoutes();
defineAppConfig();
(createApp()['app'] as VueApp).mount(app, GenUniApp());
}
export class UniAppConfig extends io.dcloud.uniapp.appframe.AppConfig {
override name: string = "akbleserver"
override appid: string = "__UNI__95B2570"
override versionName: string = "1.0.1"
override versionCode: string = "101"
override uniCompilerVersion: string = "4.76"
constructor() { super() }
}
import GenPagesAkbletestClass from './pages/akbletest.uvue'
function definePageRoutes() {
__uniRoutes.push({ path: "pages/akbletest", component: GenPagesAkbletestClass, meta: { isQuit: true } as UniPageMeta, style: _uM([["navigationBarTitleText","akble"]]) } as UniPageRoute)
}
const __uniTabBar: Map<string, any | null> | null = null
const __uniLaunchPage: Map<string, any | null> = _uM([["url","pages/akbletest"],["style",_uM([["navigationBarTitleText","akble"]])]])
function defineAppConfig(){
__uniConfig.entryPagePath = '/pages/akbletest'
__uniConfig.globalStyle = _uM([["navigationBarTextStyle","black"],["navigationBarTitleText","体测训练"],["navigationBarBackgroundColor","#F8F8F8"],["backgroundColor","#F8F8F8"]])
__uniConfig.getTabBarConfig = ():Map<string, any> | null => null
__uniConfig.tabBar = __uniConfig.getTabBarConfig()
__uniConfig.conditionUrl = ''
__uniConfig.uniIdRouter = _uM()
__uniConfig.ready = true
}

View File

@@ -0,0 +1,34 @@
import { initRuntimeSocket } from './socket'
export function initRuntimeSocketService(): Promise<boolean> {
const hosts: string = process.env.UNI_SOCKET_HOSTS
const port: string = process.env.UNI_SOCKET_PORT
const id: string = process.env.UNI_SOCKET_ID
if (hosts == '' || port == '' || id == '') return Promise.resolve(false)
let socketTask: SocketTask | null = null
__registerWebViewUniConsole(
(): string => {
return process.env.UNI_CONSOLE_WEBVIEW_EVAL_JS_CODE
},
(data: string) => {
socketTask?.send({
data,
} as SendSocketMessageOptions)
}
)
return Promise.resolve()
.then((): Promise<boolean> => {
return initRuntimeSocket(hosts, port, id).then((socket): boolean => {
if (socket == null) {
return false
}
socketTask = socket
return true
})
})
.catch((): boolean => {
return false
})
}
initRuntimeSocketService()

View File

@@ -0,0 +1,61 @@
/// <reference types="@dcloudio/uni-app-x/types/uni/global" />
// 之所以又写了一份是因为外层的socketconnectSocket的时候必须传入multiple:true
// 但是android又不能传入目前代码里又不能写条件编译之类的。
export function initRuntimeSocket(
hosts: string,
port: string,
id: string
): Promise<SocketTask | null> {
if (hosts == '' || port == '' || id == '') return Promise.resolve(null)
return hosts
.split(',')
.reduce<Promise<SocketTask | null>>(
(
promise: Promise<SocketTask | null>,
host: string
): Promise<SocketTask | null> => {
return promise.then((socket): Promise<SocketTask | null> => {
if (socket != null) return Promise.resolve(socket)
return tryConnectSocket(host, port, id)
})
},
Promise.resolve(null)
)
}
const SOCKET_TIMEOUT = 500
function tryConnectSocket(
host: string,
port: string,
id: string
): Promise<SocketTask | null> {
return new Promise((resolve, reject) => {
const socket = uni.connectSocket({
url: `ws://${host}:${port}/${id}`,
fail() {
resolve(null)
},
})
const timer = setTimeout(() => {
// @ts-expect-error
socket.close({
code: 1006,
reason: 'connect timeout',
} as CloseSocketOptions)
resolve(null)
}, SOCKET_TIMEOUT)
socket.onOpen((e) => {
clearTimeout(timer)
resolve(socket)
})
socket.onClose((e) => {
clearTimeout(timer)
resolve(null)
})
socket.onError((e) => {
clearTimeout(timer)
resolve(null)
})
})
}

View File

@@ -0,0 +1,21 @@
'use strict';
require('android.content.Context');
require('android.bluetooth.BluetoothAdapter');
require('android.bluetooth.BluetoothManager');
require('android.bluetooth.BluetoothDevice');
require('android.bluetooth.BluetoothGatt');
require('android.bluetooth.BluetoothGattCallback');
require('android.bluetooth.le.ScanCallback');
require('android.bluetooth.le.ScanResult');
require('android.bluetooth.le.ScanSettings');
require('android.os.Handler');
require('android.os.Looper');
require('androidx.core.content.ContextCompat');
require('android.content.pm.PackageManager');
require('android.bluetooth.BluetoothGattService');
require('android.bluetooth.BluetoothGattCharacteristic');
require('android.bluetooth.BluetoothGattDescriptor');
require('java.util.UUID');
require('vue');

View File

@@ -0,0 +1,782 @@
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 => 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, " at pages/akbletest.uvue:198")
}
})
// 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, " 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 = (() => { 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, " 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) { 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 + ']', " at pages/akbletest.uvue:462");
// 自动查找可用的写入和通知特征
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("协议处理器已初始化,可进行协议测试", " 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: '' }
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}'`, " at pages/akbletest.uvue:511")
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, " 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) {
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, " 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) { 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}'`, " at pages/akbletest.uvue:674");
} else {
console.log(`特征 ${c.uuid} 不可读`, " at pages/akbletest.uvue:676");
}
} catch (e) {
console.log(`读取特征 ${c.uuid} 失败: ${getErrorMessage(e)}`, " at pages/akbletest.uvue:679");
}
}
} catch (e) {
console.log('查询服务 ' + svc + ' 失败: ' + getErrorMessage(e), " at pages/akbletest.uvue:683");
}
}
} catch (e) {
console.log('获取设备信息失败: ' + getErrorMessage(e), " at pages/akbletest.uvue:688");
}
}
}
})
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;
}
}
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]]))]])]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,279 @@
import type {
BleDevice,
BleConnectionState,
BleEvent,
BleEventCallback,
BleEventPayload,
BleScanResult,
BleConnectOptionsExt,
AutoBleInterfaces,
BleDataPayload,
SendDataPayload,
BleOptions,
MultiProtocolDevice,
ScanHandler,
BleProtocolType,
ScanDevicesOptions
} from '../interface.uts';
import { ProtocolHandler } from '../protocol_handler.uts';
import { BluetoothService } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
// Shape used when callers register plain objects as handlers. Using a named
// type keeps member access explicit so the code generator emits valid Kotlin
// member references instead of trying to access properties on Any.
type RawProtocolHandler = {
protocol?: BleProtocolType;
scanDevices?: (options?: ScanDevicesOptions) => Promise<void>;
connect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<void>;
disconnect?: (device: BleDevice) => Promise<void>;
sendData?: (device: BleDevice, payload?: SendDataPayload, options?: BleOptions) => Promise<void>;
autoConnect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
}
// 设备上下文
class DeviceContext {
device : BleDevice;
protocol : BleProtocolType;
state : BleConnectionState;
handler : ProtocolHandler;
constructor(device : BleDevice, protocol : BleProtocolType, handler : ProtocolHandler) {
this.device = device;
this.protocol = protocol;
this.state = 0; // DISCONNECTED
this.handler = handler;
}
}
const deviceMap = new Map<string, DeviceContext>(); // key: deviceId|protocol
// Single active protocol handler (no multi-protocol registration)
let activeProtocol: BleProtocolType = 'standard';
let activeHandler: ProtocolHandler | null = null;
// 事件监听注册表
const eventListeners = new Map<BleEvent, Set<BleEventCallback>>();
function emit(event : BleEvent, payload : BleEventPayload) {
if (event === 'connectionStateChanged') {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:57','[AKBLE][LOG] bluetooth_manager.uts emit connectionStateChanged', payload)
}
const listeners = eventListeners.get(event);
if (listeners != null) {
listeners.forEach(cb => {
try { cb(payload); } catch (e) { }
});
}
}
class ProtocolHandlerWrapper extends ProtocolHandler {
private _raw: RawProtocolHandler | null;
constructor(raw?: RawProtocolHandler) {
// pass a lightweight BluetoothService instance to satisfy generators
super(new BluetoothService());
this._raw = (raw != null) ? raw : null;
}
override async scanDevices(options?: ScanDevicesOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.scanDevices === 'function') {
await rawTyped.scanDevices(options);
}
return;
}
override async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.connect === 'function') {
await rawTyped.connect(device, options);
}
return;
}
override async disconnect(device: BleDevice): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.disconnect === 'function') {
await rawTyped.disconnect(device);
}
return;
}
override async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.sendData === 'function') {
await rawTyped.sendData(device, payload, options);
}
return;
}
override async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.autoConnect === 'function') {
return await rawTyped.autoConnect(device, options);
}
return { serviceId: '', writeCharId: '', notifyCharId: '' };
}
}
// Strong runtime detector for plain object handlers (no Type Predicate)
// Note: the UTS bundler doesn't support TypeScript type predicates (x is T),
// and it doesn't accept the 'unknown' type. This returns a boolean and
// callers must cast the value to RawProtocolHandler after the function
// returns true.
function isRawProtocolHandler(x: any): boolean {
if (x == null || typeof x !== 'object') return false;
const r = x as Record<string, unknown>;
if (typeof r['scanDevices'] === 'function') return true;
if (typeof r['connect'] === 'function') return true;
if (typeof r['disconnect'] === 'function') return true;
if (typeof r['sendData'] === 'function') return true;
if (typeof r['autoConnect'] === 'function') return true;
if (typeof r['protocol'] === 'string') return true;
return false;
}
export const registerProtocolHandler = (handler : any) => {
if (handler == null) return;
// Determine protocol value defensively. Default to 'standard' when unknown.
let proto: BleProtocolType = 'standard';
if (handler instanceof ProtocolHandler) {
try { proto = (handler as ProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = handler as ProtocolHandler;
} else if (isRawProtocolHandler(handler)) {
try { proto = (handler as RawProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = new ProtocolHandlerWrapper(handler as RawProtocolHandler);
(activeHandler as ProtocolHandler).protocol = proto;
} else {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:139','[AKBLE] registerProtocolHandler: unsupported handler type, ignoring', handler);
return;
}
activeProtocol = proto;
}
export const scanDevices = async (options ?: ScanDevicesOptions) : Promise<void> => {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:147','[AKBLE] start scan', options)
// Determine which protocols to run: either user-specified or all registered
// Single active handler flow
if (activeHandler == null) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:151','[AKBLE] no active scan handler registered')
return
}
const handler = activeHandler as ProtocolHandler;
const scanOptions : ScanDevicesOptions = {
onDeviceFound: (device : BleDevice) => emit('deviceFound', { event: 'deviceFound', device }),
onScanFinished: () => emit('scanFinished', { event: 'scanFinished' })
}
try {
await handler.scanDevices(scanOptions)
} catch (e) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:162','[AKBLE] scanDevices handler error', e)
}
}
export const connectDevice = async (deviceId : string, protocol : BleProtocolType, options ?: BleConnectOptionsExt) : Promise<void> => {
const handler = activeHandler;
if (handler == null) throw new Error('No protocol handler');
const device : BleDevice = { deviceId, name: '', rssi: 0 }; // 可扩展
await handler.connect(device, options);
const ctx = new DeviceContext(device, protocol, handler);
ctx.state = 2; // CONNECTED
deviceMap.set(getDeviceKey(deviceId, protocol), ctx);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:175',deviceMap)
emit('connectionStateChanged', { event: 'connectionStateChanged', device, protocol, state: 2 });
}
export const disconnectDevice = async (deviceId : string, protocol : BleProtocolType) : Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null || ctx.handler == null) return;
await ctx.handler.disconnect(ctx.device);
ctx.state = 0;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol, state: 0 });
deviceMap.delete(getDeviceKey(deviceId, protocol));
}
export const sendData = async (payload : SendDataPayload, options ?: BleOptions) : Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(payload.deviceId, payload.protocol));
if (ctx == null) throw new Error('Device not connected');
// copy to local non-null variable so generator can smart-cast across awaits
const deviceCtx = ctx as DeviceContext;
if (deviceCtx.handler == null) throw new Error('sendData not supported for this protocol');
await deviceCtx.handler.sendData(deviceCtx.device, payload, options);
emit('dataSent', { event: 'dataSent', device: deviceCtx.device, protocol: payload.protocol, data: payload.data });
}
export const getConnectedDevices = () : MultiProtocolDevice[] => {
const result : MultiProtocolDevice[] = [];
deviceMap.forEach((ctx : DeviceContext) => {
const dev : MultiProtocolDevice = {
deviceId: ctx.device.deviceId,
name: ctx.device.name,
rssi: ctx.device.rssi,
protocol: ctx.protocol
};
result.push(dev);
});
return result;
}
export const getConnectionState = (deviceId : string, protocol : BleProtocolType) : BleConnectionState => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null) return 0;
return ctx.state;
}
export const on = (event : BleEvent, callback : BleEventCallback) => {
if (!eventListeners.has(event)) eventListeners.set(event, new Set());
eventListeners.get(event)!.add(callback);
}
export const off = (event : BleEvent, callback ?: BleEventCallback) => {
if (callback == null) {
eventListeners.delete(event);
} else {
eventListeners.get(event)?.delete(callback as BleEventCallback);
}
}
function getDeviceKey(deviceId : string, protocol : BleProtocolType) : string {
return `${deviceId}|${protocol}`;
}
export const autoConnect = async (deviceId : string, protocol : BleProtocolType, options ?: BleConnectOptionsExt) : Promise<AutoBleInterfaces> => {
const handler = activeHandler;
if (handler == null) throw new Error('autoConnect not supported for this protocol');
const device : BleDevice = { deviceId, name: '', rssi: 0 };
// safe call - handler.autoConnect exists on ProtocolHandler
return await handler.autoConnect(device, options) as AutoBleInterfaces;
}
// Ensure there is at least one handler registered so callers can scan/connect
// without needing to import a registry module. This creates a minimal default
// ProtocolHandler backed by a BluetoothService instance.
try {
if (activeHandler == null) {
// Create a DeviceManager-backed raw handler that delegates to native code
const _dm = DeviceManager.getInstance();
const _raw: RawProtocolHandler = {
protocol: 'standard',
scanDevices: (options?: ScanDevicesOptions) => {
try {
const scanOptions = options != null ? options : {} as ScanDevicesOptions;
_dm.startScan(scanOptions);
} catch (e) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:256','[AKBLE] DeviceManager.startScan failed', e);
}
return Promise.resolve();
},
connect: (device, options?: BleConnectOptionsExt) => {
return _dm.connectDevice(device.deviceId, options);
},
disconnect: (device) => {
return _dm.disconnectDevice(device.deviceId);
},
autoConnect: (device, options?: any) => {
// DeviceManager does not provide an autoConnect helper; return default
const result: AutoBleInterfaces = { serviceId: '', writeCharId: '', notifyCharId: '' };
return Promise.resolve(result);
}
};
const _wrapper = new ProtocolHandlerWrapper(_raw);
activeHandler = _wrapper;
activeProtocol = _raw.protocol as BleProtocolType;
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:275','[AKBLE] default protocol handler (BluetoothService-backed) registered', activeProtocol);
}
} catch (e) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:278','[AKBLE] failed to register default protocol handler', e);
}

View File

@@ -0,0 +1,311 @@
import type { BleDevice, BleOptions, BleConnectionState, BleConnectionStateChangeCallback } from '../interface.uts'
import type { BleConnectOptionsExt } from '../interface.uts'
import type { ScanDevicesOptions } from '../interface.uts';
import Context from "android.content.Context";
import BluetoothAdapter from "android.bluetooth.BluetoothAdapter";
import BluetoothManager from "android.bluetooth.BluetoothManager";
import BluetoothDevice from "android.bluetooth.BluetoothDevice";
import BluetoothGatt from "android.bluetooth.BluetoothGatt";
import BluetoothGattCallback from "android.bluetooth.BluetoothGattCallback";
import ScanCallback from "android.bluetooth.le.ScanCallback";
import ScanResult from "android.bluetooth.le.ScanResult";
import ScanSettings from "android.bluetooth.le.ScanSettings";
import Handler from "android.os.Handler";
import Looper from "android.os.Looper";
import ContextCompat from "androidx.core.content.ContextCompat";
import PackageManager from "android.content.pm.PackageManager";
// 定义 PendingConnect 类型和实现类
interface PendingConnect {
resolve: () => void;
reject: (err?: any) => void; // Changed to make err optional
timer?: number;
}
class PendingConnectImpl implements PendingConnect {
resolve: () => void;
reject: (err?: any) => void; // Changed to make err optional
timer?: number;
constructor(resolve: () => void, reject: (err?: any) => void, timer?: number) {
this.resolve = resolve;
this.reject = reject;
this.timer = timer;
}
}
// 引入全局回调管理
import { gattCallback } from './service_manager.uts'
const pendingConnects = new Map<string, PendingConnect>();
const STATE_DISCONNECTED = 0;
const STATE_CONNECTING = 1;
const STATE_CONNECTED = 2;
const STATE_DISCONNECTING = 3;
export class DeviceManager {
private static instance: DeviceManager | null = null;
private devices = new Map<string, BleDevice>();
private connectionStates = new Map<string, BleConnectionState>();
private connectionStateChangeListeners: BleConnectionStateChangeCallback[] = []
private gattMap = new Map<string, BluetoothGatt | null>();
private scanCallback: ScanCallback | null = null
private isScanning: boolean = false
private constructor() {}
static getInstance(): DeviceManager {
if (DeviceManager.instance == null) {
DeviceManager.instance = new DeviceManager();
}
return DeviceManager.instance!;
}
startScan(options: ScanDevicesOptions): void {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:60','ak startscan now')
const adapter = this.getBluetoothAdapter();
if (adapter == null) {
throw new Error('未找到蓝牙适配器');
}
if (!adapter.isEnabled) {
// 尝试请求用户开启蓝牙
try {
adapter.enable(); // 直接调用,无需可选链和括号
} catch (e) {
// 某些设备可能不支持 enable
}
setTimeout(() => {
if (!adapter.isEnabled) {
throw new Error('蓝牙未开启');
}
}, 1500);
throw new Error('正在开启蓝牙,请重试');
}
const foundDevices = this.devices; // 直接用全局 devices
class MyScanCallback extends ScanCallback {
private foundDevices: Map<string, BleDevice>;
private onDeviceFound: (device: BleDevice) => void;
constructor(foundDevices: Map<string, BleDevice>, onDeviceFound: (device: BleDevice) => void) {
super();
this.foundDevices = foundDevices;
this.onDeviceFound = onDeviceFound;
}
override onScanResult(callbackType: Int, result: ScanResult): void {
const device = result.getDevice();
if (device != null) {
const deviceId = device.getAddress();
let bleDevice = foundDevices.get(deviceId);
if (bleDevice == null) {
bleDevice = {
deviceId,
name: device.getName() ?? 'Unknown',
rssi: result.getRssi(),
lastSeen: Date.now()
};
foundDevices.set(deviceId, bleDevice);
this.onDeviceFound(bleDevice);
} else {
// 更新属性(已确保 bleDevice 非空)
bleDevice.rssi = result.getRssi();
bleDevice.name = device.getName() ?? bleDevice.name;
bleDevice.lastSeen = Date.now();
}
}
}
override onScanFailed(errorCode: Int): void {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:114','ak scan fail')
}
}
this.scanCallback = new MyScanCallback(foundDevices, options.onDeviceFound ?? (() => {}));
const scanner = adapter.getBluetoothLeScanner();
if (scanner == null) {
throw new Error('无法获取扫描器');
}
const scanSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
scanner.startScan(null, scanSettings, this.scanCallback);
this.isScanning = true;
// 默认10秒后停止扫描
new Handler(Looper.getMainLooper()).postDelayed(() => {
if (this.isScanning && this.scanCallback != null) {
scanner.stopScan(this.scanCallback);
this.isScanning = false;
// this.devices = foundDevices;
if (options.onScanFinished != null) options.onScanFinished?.invoke();
}
}, 40000);
}
async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:139','[AKBLE] connectDevice called, deviceId:', deviceId, 'options:', options, 'connectionStates:')
const adapter = this.getBluetoothAdapter();
if (adapter == null) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:142','[AKBLE] connectDevice failed: 蓝牙适配器不可用')
throw new Error('蓝牙适配器不可用');
}
const device = adapter.getRemoteDevice(deviceId);
if (device == null) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:147','[AKBLE] connectDevice failed: 未找到设备', deviceId)
throw new Error('未找到设备');
}
this.connectionStates.set(deviceId, STATE_CONNECTING);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:151','[AKBLE] connectDevice set STATE_CONNECTING, deviceId:', deviceId, 'connectionStates:')
this.emitConnectionStateChange(deviceId, STATE_CONNECTING);
const activity = UTSAndroid.getUniActivity();
const timeout = options?.timeout ?? 15000;
const key = `${deviceId}|connect`;
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:158','[AKBLE] connectDevice 超时:', deviceId)
pendingConnects.delete(key);
this.connectionStates.set(deviceId, STATE_DISCONNECTED);
this.gattMap.set(deviceId, null);
this.emitConnectionStateChange(deviceId, STATE_DISCONNECTED);
reject(new Error('连接超时'));
}, timeout);
// 创建一个适配器函数来匹配类型签名
const resolveAdapter = () => {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:168','[AKBLE] connectDevice resolveAdapter:', deviceId)
resolve();
};
const rejectAdapter = (err?: any) => {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:172','[AKBLE] connectDevice rejectAdapter:', deviceId, err)
reject(err);
};
pendingConnects.set(key, new PendingConnectImpl(resolveAdapter, rejectAdapter, timer));
try {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:178','[AKBLE] connectGatt 调用前:', deviceId)
const gatt = device.connectGatt(activity, false, gattCallback);
this.gattMap.set(deviceId, gatt);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:181','[AKBLE] connectGatt 调用后:', deviceId, gatt)
} catch (e) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:183','[AKBLE] connectGatt 异常:', deviceId, e)
clearTimeout(timer);
pendingConnects.delete(key);
this.connectionStates.set(deviceId, STATE_DISCONNECTED);
this.gattMap.set(deviceId, null);
this.emitConnectionStateChange(deviceId, STATE_DISCONNECTED);
reject(e);
}
});
}
// 统一分发连接回调(应在 gattCallback.onConnectionStateChange 内调用)
static handleConnectionStateChange(deviceId: string, newState: number, error?: any) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:196','[AKBLE] handleConnectionStateChange:', deviceId, 'newState:', newState, 'error:', error, 'pendingConnects:')
const key = `${deviceId}|connect`;
const cb = pendingConnects.get(key);
if (cb != null) {
// 修复 timer 的空安全问题,使用临时变量
const timerValue = cb.timer;
if (timerValue != null) {
clearTimeout(timerValue);
}
// 修复 error 处理
if (newState === STATE_CONNECTED) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:208','[AKBLE] handleConnectionStateChange: 连接成功', deviceId)
cb.resolve();
} else {
// 正确处理可空值
const errorToUse = error != null ? error : new Error('连接断开');
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:213','[AKBLE] handleConnectionStateChange: 连接失败', deviceId, errorToUse)
cb.reject(errorToUse);
}
pendingConnects.delete(key);
} else {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:218','[AKBLE] handleConnectionStateChange: 未找到 pendingConnects', deviceId, newState)
}
}
async disconnectDevice(deviceId: string, isActive: boolean = true): Promise<void> {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:223','[AKBLE] disconnectDevice called, deviceId:', deviceId, 'isActive:', isActive)
let gatt = this.gattMap.get(deviceId);
if (gatt != null) {
gatt.disconnect();
gatt.close();
// gatt=null;
this.gattMap.set(deviceId, null);
this.connectionStates.set(deviceId, STATE_DISCONNECTED);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:231','[AKBLE] disconnectDevice set STATE_DISCONNECTED, deviceId:', deviceId, 'connectionStates:')
this.emitConnectionStateChange(deviceId, STATE_DISCONNECTED);
return;
} else {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:235','[AKBLE] disconnectDevice: gatt is null, deviceId:', deviceId)
return;
}
}
async reconnectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
let attempts = 0;
const maxAttempts = options?.maxAttempts ?? 3;
const interval = options?.interval ?? 3000;
while (attempts < maxAttempts) {
try {
await this.disconnectDevice(deviceId, false);
await this.connectDevice(deviceId, options);
return;
} catch (e) {
attempts++;
if (attempts >= maxAttempts) throw new Error('重连失败');
// 修复 setTimeout 问题,使用旧式 Promise + setTimeout 解决
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, interval);
});
}
}
}
getConnectedDevices(): BleDevice[] {
// 创建一个空数组来存储结果
const result: BleDevice[] = [];
// 遍历 devices Map 并检查连接状态
this.devices.forEach((device, deviceId) => {
if (this.connectionStates.get(deviceId) === STATE_CONNECTED) {
result.push(device);
}
});
return result;
}
onConnectionStateChange(listener: BleConnectionStateChangeCallback) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:277','[AKBLE][LOG] onConnectionStateChange 注册, 当前监听数:', this.connectionStateChangeListeners.length + 1, listener)
this.connectionStateChangeListeners.push(listener)
}
protected emitConnectionStateChange(deviceId: string, state: BleConnectionState) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:282','[AKBLE][LOG] emitConnectionStateChange', deviceId, state, 'listeners:', this.connectionStateChangeListeners.length, 'connectionStates:', this.connectionStates)
for (const listener of this.connectionStateChangeListeners) {
try {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:285','[AKBLE][LOG] emitConnectionStateChange 调用 listener', listener)
listener(deviceId, state)
} catch (e) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:288','[AKBLE][LOG] emitConnectionStateChange listener error', e)
}
}
}
getGattInstance(deviceId: string): BluetoothGatt | null {
return this.gattMap.get(deviceId) ?? null;
}
private getBluetoothAdapter(): BluetoothAdapter | null {
const context = UTSAndroid.getAppContext();
if (context == null) return null;
const manager = context?.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager;
return manager.getAdapter();
}
/**
* 获取指定ID的设备如果存在
*/
public getDevice(deviceId: string): BleDevice | null {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:308',deviceId,this.devices)
return this.devices.get(deviceId) ?? null;
}
}

View File

@@ -0,0 +1,734 @@
import { BleService } from '../interface.uts'
import type { WriteCharacteristicOptions, DfuOptions, ControlParserResult } from '../interface.uts'
import { DeviceManager } from './device_manager.uts'
import { ServiceManager } from './service_manager.uts'
import BluetoothGatt from 'android.bluetooth.BluetoothGatt'
import BluetoothGattCharacteristic from 'android.bluetooth.BluetoothGattCharacteristic'
import BluetoothGattDescriptor from 'android.bluetooth.BluetoothGattDescriptor'
import UUID from 'java.util.UUID'
// 通用 Nordic DFU UUIDs (常见设备可能使用这些;如厂商自定义请替换)
const DFU_SERVICE_UUID = '0000fe59-0000-1000-8000-00805f9b34fb'
const DFU_CONTROL_POINT_UUID = '8ec90001-f315-4f60-9fb8-838830daea50'
const DFU_PACKET_UUID = '8ec90002-f315-4f60-9fb8-838830daea50'
type DfuSession = {
resolve : () => void;
reject : (err ?: any) => void;
onProgress ?: (p : number) => void;
onLog ?: (s : string) => void;
controlParser ?: (data : Uint8Array) => ControlParserResult | null;
// Nordic 专用字段
bytesSent ?: number;
totalBytes ?: number;
useNordic ?: boolean;
// PRN (packet receipt notification) support
prn ?: number;
packetsSincePrn ?: number;
prnResolve ?: () => void;
prnReject ?: (err ?: any) => void;
}
export class DfuManager {
// 会话表,用于把 control-point 通知路由到当前 DFU 流程
private sessions : Map<string, DfuSession> = new Map();
// 简化:只实现最基本的 GATT-based DFU 上传逻辑,需按设备协议调整 control point 的命令/解析
// Emit a DFU lifecycle event for a session. name should follow Nordic listener names
private _emitDfuEvent(deviceId : string, name : string, payload ?: any) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:42','[DFU][Event]', name, deviceId, payload ?? '');
const s = this.sessions.get(deviceId);
if (s == null) return;
if (typeof s.onLog == 'function') {
try {
const logFn = s.onLog as (msg : string) => void;
logFn(`[${name}] ${payload != null ? JSON.stringify(payload) : ''}`);
} catch (e) { }
}
if (name == 'onProgress' && typeof s.onProgress == 'function' && typeof payload == 'number') {
try { s.onProgress(payload); } catch (e) { }
}
}
async startDfu(deviceId : string, firmwareBytes : Uint8Array, options ?: DfuOptions) : Promise<void> {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:57','startDfu 0')
const deviceManager = DeviceManager.getInstance();
const serviceManager = ServiceManager.getInstance();
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:60','startDfu 1')
const gatt : BluetoothGatt | null = deviceManager.getGattInstance(deviceId);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:62','startDfu 2')
if (gatt == null) throw new Error('Device not connected');
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:64','[DFU] startDfu start deviceId=', deviceId, 'firmwareBytes=', firmwareBytes != null ? firmwareBytes.length : 0, 'options=', options);
try {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:66','[DFU] requesting high connection priority for', deviceId);
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
} catch (e) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:69','[DFU] requestConnectionPriority failed', e);
}
// 发现服务并特征
// ensure services discovered before accessing GATT; serviceManager exposes Promise-based API
await serviceManager.getServices(deviceId, null);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:75','[DFU] services ensured for', deviceId);
const dfuService = gatt.getService(UUID.fromString(DFU_SERVICE_UUID));
if (dfuService == null) throw new Error('DFU service not found');
const controlChar = dfuService.getCharacteristic(UUID.fromString(DFU_CONTROL_POINT_UUID));
const packetChar = dfuService.getCharacteristic(UUID.fromString(DFU_PACKET_UUID));
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:80','[DFU] dfuService=', dfuService != null ? dfuService.getUuid().toString() : null, 'controlChar=', controlChar != null ? controlChar.getUuid().toString() : null, 'packetChar=', packetChar != null ? packetChar.getUuid().toString() : null);
if (controlChar == null || packetChar == null) throw new Error('DFU characteristics missing');
const packetProps = packetChar.getProperties();
const supportsWriteWithResponse = (packetProps & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0;
const supportsWriteNoResponse = (packetProps & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0;
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:85','[DFU] packet characteristic props mask=', packetProps, 'supportsWithResponse=', supportsWriteWithResponse, 'supportsNoResponse=', supportsWriteNoResponse);
// Allow caller to request a desired MTU via options for higher throughput
const desiredMtu = (options != null && typeof options.mtu == 'number') ? options.mtu : 247;
try {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:90','[DFU] requesting MTU=', desiredMtu, 'for', deviceId);
await this._requestMtu(gatt, desiredMtu, 8000);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:92','[DFU] requestMtu completed for', deviceId);
} catch (e) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:94','[DFU] requestMtu failed or timed out, continue with default.', e);
}
const mtu = desiredMtu; // 假定成功或使用期望值
const chunkSize = Math.max(20, mtu - 3);
// small helper to convert a byte (possibly signed) to a two-digit hex string
const byteToHex = (b : number) => {
const v = (b < 0) ? (b + 256) : b;
let s = v.toString(16);
if (s.length < 2) s = '0' + s;
return s;
};
// Parameterize PRN window and timeout via options early so they are available
// for session logging. Defaults: prn = 12 packets, prnTimeoutMs = 10000 ms
let prnWindow = 0;
if (options != null && typeof options.prn == 'number') {
prnWindow = Math.max(0, Math.floor(options.prn));
}
const prnTimeoutMs = (options != null && typeof options.prnTimeoutMs == 'number') ? Math.max(1000, Math.floor(options.prnTimeoutMs)) : 8000;
const disablePrnOnTimeout = !(options != null && options.disablePrnOnTimeout == false);
// 订阅 control point 通知并将通知路由到会话处理器
const controlHandler = (data : Uint8Array) => {
// 交给会话处理器解析并触发事件
try {
const hexParts: string[] = [];
for (let i = 0; i < data.length; i++) {
const v = data[i] as number;
hexParts.push(byteToHex(v));
}
const hex = hexParts.join(' ');
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:126','[DFU] control notification callback invoked for', deviceId, 'raw=', Array.from(data), 'hex=', hex);
} catch (e) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:128','[DFU] control notification callback invoked for', deviceId, 'raw=', Array.from(data));
}
this._handleControlNotification(deviceId, data);
};
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:132','[DFU] subscribing control point for', deviceId);
await serviceManager.subscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, controlHandler);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:134','[DFU] subscribeCharacteristic returned for', deviceId);
// 保存会话回调(用于 waitForControlEvent; 支持 Nordic 模式追踪已发送字节
this.sessions.set(deviceId, {
resolve: () => { },
reject: (err ?: any) => {__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:139',err) },
onProgress: null,
onLog: null,
controlParser: (data : Uint8Array) => this._defaultControlParser(data),
bytesSent: 0,
totalBytes: firmwareBytes.length,
useNordic: options != null && options.useNordic == true,
prn: null,
packetsSincePrn: 0,
prnResolve: null,
prnReject: null
});
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:151','[DFU] session created for', deviceId, 'totalBytes=', firmwareBytes.length);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:152','[DFU] DFU session details:', { deviceId: deviceId, totalBytes: firmwareBytes.length, chunkSize: chunkSize, prnWindow: prnWindow, prnTimeoutMs: prnTimeoutMs });
// wire options callbacks into the session (if provided)
const sessRef = this.sessions.get(deviceId);
if (sessRef != null) {
sessRef.onProgress = (options != null && typeof options.onProgress == 'function') ? options.onProgress : null;
sessRef.onLog = (options != null && typeof options.onLog == 'function') ? options.onLog : null;
}
// emit initial lifecycle events (Nordic-like)
this._emitDfuEvent(deviceId, 'onDeviceConnecting', null);
// 写入固件数据(非常保守的实现:逐包写入并等待短延迟)
// --- PRN setup (optional, Nordic-style flow) ---
// Parameterize PRN window and timeout via options: options.prn, options.prnTimeoutMs
// Defaults were set earlier; build PRN payload using arithmetic to avoid
// bitwise operators which don't map cleanly to generated Kotlin.
if (prnWindow > 0) {
try {
// send Set PRN to device (format: [OP_CODE_SET_PRN, prn LSB, prn MSB])
// WARNING: Ensure your device uses the same opcode/format; change if needed.
const prnLsb = prnWindow % 256;
const prnMsb = Math.floor(prnWindow / 256) % 256;
const prnPayload = new Uint8Array([0x02, prnLsb, prnMsb]);
await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, prnPayload, null);
const sess0 = this.sessions.get(deviceId);
if (sess0 != null) {
sess0.useNordic = true;
sess0.prn = prnWindow;
sess0.packetsSincePrn = 0;
sess0.controlParser = (data : Uint8Array) => this._nordicControlParser(data);
}
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:184','[DFU] Set PRN sent (prn=', prnWindow, ') for', deviceId);
} catch (e) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:186','[DFU] Set PRN failed (continuing without PRN):', e);
const sessFallback = this.sessions.get(deviceId);
if (sessFallback != null) sessFallback.prn = 0;
}
} else {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:191','[DFU] PRN disabled (prnWindow=', prnWindow, ') for', deviceId);
}
// 写入固件数据(逐包写入并根据 options.waitForResponse 选择是否等待响应)
let offset = 0;
const total = firmwareBytes.length;
this._emitDfuEvent(deviceId, 'onDfuProcessStarted', null);
this._emitDfuEvent(deviceId, 'onUploadingStarted', null);
// Track outstanding write operations when using fire-and-forget mode so we can
// log and throttle if the Android stack becomes overwhelmed.
let outstandingWrites = 0;
// read tuning parameters from options in a safe, generator-friendly way
let configuredMaxOutstanding = 2;
let writeSleepMs = 0;
let writeRetryDelay = 100;
let writeMaxAttempts = 12;
let writeGiveupTimeout = 60000;
let drainOutstandingTimeout = 3000;
let failureBackoffMs = 0;
// throughput measurement
let throughputWindowBytes = 0;
let lastThroughputTime = Date.now();
function _logThroughputIfNeeded(force ?: boolean) {
try {
const now = Date.now();
const elapsed = now - lastThroughputTime;
if (force == true || elapsed >= 1000) {
const bytes = throughputWindowBytes;
const bps = Math.floor((bytes * 1000) / Math.max(1, elapsed));
// reset window
throughputWindowBytes = 0;
lastThroughputTime = now;
const human = `${bps} B/s`;
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:225','[DFU] throughput:', human, 'elapsedMs=', elapsed);
const s = this.sessions.get(deviceId);
if (s != null && typeof s.onLog == 'function') {
try { s.onLog?.invoke('[DFU] throughput: ' + human); } catch (e) { }
}
}
} catch (e) { }
}
function _safeErr(e ?: any) {
try {
if (e == null) return '';
if (typeof e == 'string') return e;
try { return JSON.stringify(e); } catch (e2) { }
try { return (e as any).toString(); } catch (e3) { }
return '';
} catch (e4) { return ''; }
}
try {
if (options != null) {
try {
if (options.maxOutstanding != null) {
const parsed = Math.floor(options.maxOutstanding as number);
if (!isNaN(parsed) && parsed > 0) configuredMaxOutstanding = parsed;
}
} catch (e) { }
try {
if (options.writeSleepMs != null) {
const parsedWs = Math.floor(options.writeSleepMs as number);
if (!isNaN(parsedWs) && parsedWs >= 0) writeSleepMs = parsedWs;
}
} catch (e) { }
try {
if (options.writeRetryDelayMs != null) {
const parsedRetry = Math.floor(options.writeRetryDelayMs as number);
if (!isNaN(parsedRetry) && parsedRetry >= 0) writeRetryDelay = parsedRetry;
}
} catch (e) { }
try {
if (options.writeMaxAttempts != null) {
const parsedAttempts = Math.floor(options.writeMaxAttempts as number);
if (!isNaN(parsedAttempts) && parsedAttempts > 0) writeMaxAttempts = parsedAttempts;
}
} catch (e) { }
try {
if (options.writeGiveupTimeoutMs != null) {
const parsedGiveupTimeout = Math.floor(options.writeGiveupTimeoutMs as number);
if (!isNaN(parsedGiveupTimeout) && parsedGiveupTimeout > 0) writeGiveupTimeout = parsedGiveupTimeout;
}
} catch (e) { }
try {
if (options.drainOutstandingTimeoutMs != null) {
const parsedDrain = Math.floor(options.drainOutstandingTimeoutMs as number);
if (!isNaN(parsedDrain) && parsedDrain >= 0) drainOutstandingTimeout = parsedDrain;
}
} catch (e) { }
}
if (configuredMaxOutstanding < 1) configuredMaxOutstanding = 1;
if (writeSleepMs < 0) writeSleepMs = 0;
} catch (e) { }
if (supportsWriteWithResponse == false && supportsWriteNoResponse == true) {
if (configuredMaxOutstanding > 1) configuredMaxOutstanding = 1;
if (writeSleepMs < 15) writeSleepMs = 15;
if (writeRetryDelay < 150) writeRetryDelay = 150;
if (writeMaxAttempts < 40) writeMaxAttempts = 40;
if (writeGiveupTimeout < 120000) writeGiveupTimeout = 120000;
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:291','[DFU] packet char only supports WRITE_NO_RESPONSE; serializing writes with conservative pacing');
}
const maxOutstandingCeiling = configuredMaxOutstanding;
let adaptiveMaxOutstanding = configuredMaxOutstanding;
const minOutstandingWindow = 1;
while (offset < total) {
const end = Math.min(offset + chunkSize, total);
const slice = firmwareBytes.subarray(offset, end);
// Decide whether to wait for response per-chunk. Honor characteristic support.
// Generator-friendly: avoid 'undefined' and use explicit boolean check.
let finalWaitForResponse = true;
if (options != null) {
try {
const maybe = options.waitForResponse;
if (maybe == true && supportsWriteWithResponse == false && supportsWriteNoResponse == true) {
// caller requested response but characteristic cannot provide it; keep true to ensure we await the write promise.
finalWaitForResponse = true;
} else if (maybe == false) {
finalWaitForResponse = false;
} else if (maybe == true) {
finalWaitForResponse = true;
}
} catch (e) { finalWaitForResponse = true; }
}
const writeOpts: WriteCharacteristicOptions = {
waitForResponse: finalWaitForResponse,
retryDelayMs: writeRetryDelay,
maxAttempts: writeMaxAttempts,
giveupTimeoutMs: writeGiveupTimeout,
forceWriteTypeNoResponse: finalWaitForResponse == false
};
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:324','[DFU] writing packet chunk offset=', offset, 'len=', slice.length, 'waitForResponse=', finalWaitForResponse, 'outstanding=', outstandingWrites);
// Fire-and-forget path: do not await the write if waitForResponse == false.
if (finalWaitForResponse == false) {
if (failureBackoffMs > 0) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:329','[DFU] applying failure backoff', failureBackoffMs, 'ms before next write for', deviceId);
await this._sleep(failureBackoffMs);
failureBackoffMs = Math.floor(failureBackoffMs / 2);
}
while (outstandingWrites >= adaptiveMaxOutstanding) {
await this._sleep(Math.max(1, writeSleepMs));
}
// increment outstanding counter and kick the write without awaiting.
outstandingWrites = outstandingWrites + 1;
// fire-and-forget: start the write but don't await its Promise
const writeOffset = offset;
serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_PACKET_UUID, slice, writeOpts).then((res) => {
outstandingWrites = Math.max(0, outstandingWrites - 1);
if (res == true) {
if (adaptiveMaxOutstanding < maxOutstandingCeiling) {
adaptiveMaxOutstanding = Math.min(maxOutstandingCeiling, adaptiveMaxOutstanding + 1);
}
if (failureBackoffMs > 0) failureBackoffMs = Math.floor(failureBackoffMs / 2);
}
// log occasional completions
if ((outstandingWrites & 0x1f) == 0) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:350','[DFU] write completion callback, outstandingWrites=', outstandingWrites, 'adaptiveWindow=', adaptiveMaxOutstanding, 'device=', deviceId);
}
// detect write failure signaled by service manager
if (res !== true) {
adaptiveMaxOutstanding = Math.max(minOutstandingWindow, Math.floor(adaptiveMaxOutstanding / 2));
failureBackoffMs = Math.min(200, Math.max(failureBackoffMs, Math.max(5, writeRetryDelay)));
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:356','[DFU] writeCharacteristic returned false for device=', deviceId, 'offset=', writeOffset, 'adaptiveWindow now=', adaptiveMaxOutstanding);
try { this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: writeOffset, reason: 'write returned false' }); } catch (e) { }
}
}).catch((e) => {
outstandingWrites = Math.max(0, outstandingWrites - 1);
adaptiveMaxOutstanding = Math.max(minOutstandingWindow, Math.floor(adaptiveMaxOutstanding / 2));
failureBackoffMs = Math.min(200, Math.max(failureBackoffMs, Math.max(5, writeRetryDelay)));
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:363','[DFU] fire-and-forget write failed for device=', deviceId, e, 'adaptiveWindow now=', adaptiveMaxOutstanding);
try {
const errMsg ='[DFU] fire-and-forget write failed for device=';
this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: writeOffset, reason: errMsg });
} catch (e2) { }
});
// account bytes for throughput
throughputWindowBytes += slice.length;
_logThroughputIfNeeded(false);
} else {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:373','[DFU] awaiting write for chunk offset=', offset);
try {
const writeResult = await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_PACKET_UUID, slice, writeOpts);
if (writeResult !== true) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:377','[DFU] writeCharacteristic(await) returned false at offset=', offset, 'device=', deviceId);
try { this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: offset, reason: 'write returned false' }); } catch (e) { }
// abort DFU by throwing
throw new Error('write failed');
}
} catch (e) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:383','[DFU] awaiting write failed at offset=', offset, 'device=', deviceId, e);
try {
const errMsg = '[DFU] awaiting write failed ';
this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: offset, reason: errMsg });
} catch (e2) { }
throw e;
}
// account bytes for throughput
throughputWindowBytes += slice.length;
_logThroughputIfNeeded(false);
}
// update PRN counters and wait when window reached
const sessAfter = this.sessions.get(deviceId);
if (sessAfter != null && sessAfter.useNordic == true && typeof sessAfter.prn == 'number' && (sessAfter.prn ?? 0) > 0) {
sessAfter.packetsSincePrn = (sessAfter.packetsSincePrn ?? 0) + 1;
if ((sessAfter.packetsSincePrn ?? 0) >= (sessAfter.prn ?? 0) && (sessAfter.prn ?? 0) > 0) {
// wait for PRN (device notification) before continuing
try {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:401','[DFU] reached PRN window, waiting for PRN for', deviceId, 'packetsSincePrn=', sessAfter.packetsSincePrn, 'prn=', sessAfter.prn);
await this._waitForPrn(deviceId, prnTimeoutMs);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:403','[DFU] PRN received, resuming transfer for', deviceId);
} catch (e) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:405','[DFU] PRN wait failed/timed out, continuing anyway for', deviceId, e);
if (disablePrnOnTimeout) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:407','[DFU] disabling PRN waits after timeout for', deviceId);
sessAfter.prn = 0;
}
}
// reset counter
sessAfter.packetsSincePrn = 0;
}
}
offset = end;
// 如果启用 nordic 模式,统计已发送字节
const sess = this.sessions.get(deviceId);
if (sess != null && typeof sess.bytesSent == 'number') {
sess.bytesSent = (sess.bytesSent ?? 0) + slice.length;
}
// 简单节流与日志,避免过快。默认睡眠非常短以提高吞吐量; 可在设备上调节
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:422','[DFU] wrote chunk for', deviceId, 'offset=', offset, '/', total, 'chunkSize=', slice.length, 'bytesSent=', sess != null ? sess.bytesSent : null, 'outstanding=', outstandingWrites);
// emit upload progress event (percent) if available
if (sess != null && typeof sess.bytesSent == 'number' && typeof sess.totalBytes == 'number') {
const p = Math.floor((sess.bytesSent / sess.totalBytes) * 100);
this._emitDfuEvent(deviceId, 'onProgress', p);
}
// yield to event loop and avoid starving the Android BLE stack
await this._sleep(Math.max(0, writeSleepMs));
}
// wait for outstanding writes to drain before continuing with control commands
if (outstandingWrites > 0) {
const drainStart = Date.now();
while (outstandingWrites > 0 && (Date.now() - drainStart) < drainOutstandingTimeout) {
await this._sleep(Math.max(0, writeSleepMs));
}
if (outstandingWrites > 0) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:438','[DFU] outstandingWrites remain after drain timeout, continuing with', outstandingWrites);
} else {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:440','[DFU] outstandingWrites drained before control phase');
}
}
this._emitDfuEvent(deviceId, 'onUploadingCompleted', null);
// force final throughput log before activate/validate
_logThroughputIfNeeded(true);
// 发送 activate/validate 命令到 control point需根据设备协议实现
// 下面为占位:请替换为实际的 opcode
// 在写入前先启动控制结果等待,防止设备快速响应导致丢失通知
const controlTimeout = 20000;
const controlResultPromise = this._waitForControlResult(deviceId, controlTimeout);
try {
// control writes: pass undefined options explicitly to satisfy the generator/typechecker
const activatePayload = new Uint8Array([0x04]);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:456','[DFU] sending activate/validate payload=', Array.from(activatePayload));
await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, activatePayload, null);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:458','[DFU] activate/validate write returned for', deviceId);
} catch (e) {
// 写入失败时取消控制等待,避免悬挂定时器
try {
const sessOnWriteFail = this.sessions.get(deviceId);
if (sessOnWriteFail != null && typeof sessOnWriteFail.reject == 'function') {
sessOnWriteFail.reject(e);
}
} catch (rejectErr) { }
await controlResultPromise.catch(() => { });
try { await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID); } catch (e2) { }
this.sessions.delete(deviceId);
throw e;
}
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:472','[DFU] sent control activate/validate command to control point for', deviceId);
this._emitDfuEvent(deviceId, 'onValidating', null);
// 等待 control-point 返回最终结果(成功或失败),超时可配置
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:476','[DFU] waiting for control result (timeout=', controlTimeout, ') for', deviceId);
try {
await controlResultPromise;
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:479','[DFU] control result resolved for', deviceId);
} catch (err) {
// 清理订阅后抛出
try { await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID); } catch (e) { }
this.sessions.delete(deviceId);
throw err;
}
// 取消订阅
try {
await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID);
} catch (e) { }
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:491','[DFU] unsubscribed control point for', deviceId);
// 清理会话
this.sessions.delete(deviceId);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:495','[DFU] session cleared for', deviceId);
return;
}
async _requestMtu(gatt : BluetoothGatt, mtu : number, timeoutMs : number) : Promise<void> {
return new Promise<void>((resolve, reject) => {
// 在当前项目BluetoothGattCallback.onMtuChanged 未被封装;简单发起请求并等待短超时
try {
const ok = gatt.requestMtu(Math.floor(mtu) as Int);
if (!ok) {
return reject(new Error('requestMtu failed'));
}
} catch (e) {
return reject(e);
}
// 无 callback 监听时退回,等待一小段时间以便成功
setTimeout(() => resolve(), Math.min(2000, timeoutMs));
});
}
_sleep(ms : number) {
return new Promise<void>((r) => { setTimeout(() => { r() }, ms); });
}
_waitForPrn(deviceId : string, timeoutMs : number) : Promise<void> {
const session = this.sessions.get(deviceId);
if (session == null) return Promise.reject(new Error('no dfu session'));
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
// timeout waiting for PRN
// clear pending handlers
session.prnResolve = null;
session.prnReject = null;
reject(new Error('PRN timeout'));
}, timeoutMs);
const prnResolve = () => {
clearTimeout(timer);
resolve();
};
const prnReject = (err ?: any) => {
clearTimeout(timer);
reject(err);
};
session.prnResolve = prnResolve;
session.prnReject = prnReject;
});
}
// 默认 control point 解析器(非常通用的尝试解析:如果设备发送 progress byte 或成功码)
_defaultControlParser(data : Uint8Array) : ControlParserResult | null {
// 假设协议:第一个字节为 opcode, 第二字节可为状态或进度
if (data == null || data.length === 0) return null;
const op = data[0];
// Nordic-style response: [0x10, requestOp, resultCode]
if (op === 0x10 && data.length >= 3) {
const requestOp = data[1];
const resultCode = data[2];
if (resultCode === 0x01) {
return { type: 'success' };
}
return { type: 'error', error: { requestOp: requestOp, resultCode: resultCode, raw: Array.from(data) } };
}
// Nordic PRN notification: [0x11, LSB, MSB] -> return as progress (bytes received)
if (op === 0x11 && data.length >= 3) {
const lsb = data[1];
const msb = data[2];
const received = (msb << 8) | lsb;
return { type: 'progress', progress: received };
}
// vendor-specific opcode example: 0x60 may mean 'response/progress' for some firmwares
if (op === 0x60) {
if (data.length >= 3) {
const requestOp = data[1];
const status = data[2];
// Known success/status codes observed in field devices
if (status === 0x00 || status === 0x01 || status === 0x0A) {
return { type: 'success' };
}
return { type: 'error', error: { requestOp: requestOp, resultCode: status, raw: Array.from(data) } };
}
if (data.length >= 2) {
return { type: 'progress', progress: data[1] };
}
}
// 通用进度回退:若第二字节位于 0-100 之间,当作百分比
if (data.length >= 2) {
const maybeProgress = data[1];
if (maybeProgress >= 0 && maybeProgress <= 100) {
return { type: 'progress', progress: maybeProgress };
}
}
// 若找到明显的 success opcode (示例 0x01) 或 error 0xFF
if (op === 0x01) return { type: 'success' };
if (op === 0xFF) return { type: 'error', error: data };
return { type: 'info' };
}
// Nordic DFU control-parser支持 Response and Packet Receipt Notification
_nordicControlParser(data : Uint8Array) : ControlParserResult | null {
// Nordic opcodes (简化):
// - 0x10 : Response (opcode, requestOp, resultCode)
// - 0x11 : Packet Receipt Notification (opcode, value LSB, value MSB)
if (data == null || data.length == 0) return null;
const op = data[0];
if (op == 0x11 && data.length >= 3) {
// packet receipt notif: bytes received (little endian)
const lsb = data[1];
const msb = data[2];
const received = (msb << 8) | lsb;
// Return received bytes as progress value; parser does not resolve device-specific session here.
return { type: 'progress', progress: received };
}
// Nordic vendor-specific progress/response opcode (example 0x60)
if (op == 0x60) {
if (data.length >= 3) {
const requestOp = data[1];
const status = data[2];
if (status == 0x00 || status == 0x01 || status == 0x0A) {
return { type: 'success' };
}
return { type: 'error', error: { requestOp, resultCode: status, raw: Array.from(data) } };
}
if (data.length >= 2) {
return { type: 'progress', progress: data[1] };
}
}
// Response: check result code for success (0x01 may indicate success in some stacks)
if (op == 0x10 && data.length >= 3) {
const requestOp = data[1];
const resultCode = data[2];
// Nordic resultCode 0x01 = SUCCESS typically
if (resultCode == 0x01) return { type: 'success' };
else return { type: 'error', error: { requestOp, resultCode } };
}
return null;
}
_handleControlNotification(deviceId : string, data : Uint8Array) {
const session = this.sessions.get(deviceId);
if (session == null) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:636','[DFU] control notification received but no session for', deviceId, 'data=', Array.from(data));
return;
}
try {
// human readable opcode mapping
let opcodeName = 'unknown';
switch (data[0]) {
case 0x10: opcodeName = 'Response'; break;
case 0x11: opcodeName = 'PRN'; break;
case 0x60: opcodeName = 'VendorProgress'; break;
case 0x01: opcodeName = 'SuccessOpcode'; break;
case 0xFF: opcodeName = 'ErrorOpcode'; break;
}
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:649','[DFU] _handleControlNotification deviceId=', deviceId, 'opcode=0x' + data[0].toString(16), 'name=', opcodeName, 'raw=', Array.from(data));
const parsed = session.controlParser != null ? session.controlParser(data) : null;
if (session.onLog != null) session.onLog('DFU control notify: ' + Array.from(data).join(','));
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:652','[DFU] parsed control result=', parsed);
if (parsed == null) return;
if (parsed.type == 'progress' && parsed.progress != null) {
// 如果在 nordic 模式 parsed.progress 可能是已接收字节数,则转换为百分比
if (session.useNordic == true && session.totalBytes != null && session.totalBytes > 0) {
const percent = Math.floor((parsed.progress / session.totalBytes) * 100);
session.onProgress?.(percent);
// If we have written all bytes locally, log that event
if (session.bytesSent != null && session.totalBytes != null && session.bytesSent >= session.totalBytes) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:661','[DFU] all bytes written locally for', deviceId, 'bytesSent=', session.bytesSent, 'total=', session.totalBytes);
// emit uploading completed once
this._emitDfuEvent(deviceId, 'onUploadingCompleted', null);
}
// If a PRN wait is pending, resolve it (PRN indicates device received packets)
if (typeof session.prnResolve == 'function') {
try { session.prnResolve(); } catch (e) { }
session.prnResolve = null;
session.prnReject = null;
session.packetsSincePrn = 0;
}
} else {
const progress = parsed.progress
if (progress != null) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:675','[DFU] progress for', deviceId, 'progress=', progress);
session.onProgress?.(progress);
// also resolve PRN if was waiting (in case device reports numeric progress)
if (typeof session.prnResolve == 'function') {
try { session.prnResolve(); } catch (e) { }
session.prnResolve = null;
session.prnReject = null;
session.packetsSincePrn = 0;
}
}
}
} else if (parsed.type == 'success') {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:687','[DFU] parsed success for', deviceId, 'resolving session');
session.resolve();
// Log final device-acknowledged success
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:690','[DFU] device reported DFU success for', deviceId);
this._emitDfuEvent(deviceId, 'onDfuCompleted', null);
} else if (parsed.type == 'error') {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:693','[DFU] parsed error for', deviceId, parsed.error);
session.reject(parsed.error ?? new Error('DFU device error'));
this._emitDfuEvent(deviceId, 'onError', parsed.error ?? {});
} else {
// info - just log
}
} catch (e) {
session.onLog?.('control parse error: ' + e);
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:701','[DFU] control parse exception for', deviceId, e);
}
}
_waitForControlResult(deviceId : string, timeoutMs : number) : Promise<void> {
const session = this.sessions.get(deviceId);
if (session == null) return Promise.reject(new Error('no dfu session'));
return new Promise<void>((resolve, reject) => {
// wrap resolve/reject to clear timer
const timer = setTimeout(() => {
// 超时
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:712','[DFU] _waitForControlResult timeout for', deviceId);
reject(new Error('DFU control timeout'));
}, timeoutMs);
const origResolve = () => {
clearTimeout(timer);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:717','[DFU] _waitForControlResult resolved for', deviceId);
resolve();
};
const origReject = (err ?: any) => {
clearTimeout(timer);
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:722','[DFU] _waitForControlResult rejected for', deviceId, 'err=', err);
reject(err);
};
// replace session handlers temporarily (guard nullable)
if (session != null) {
session.resolve = origResolve;
session.reject = origReject;
}
});
}
}
export const dfuManager = new DfuManager();

View File

@@ -0,0 +1,110 @@
import * as BluetoothManager from './bluetooth_manager.uts';
import { ServiceManager } from './service_manager.uts';
import type { ScanDevicesOptions, BleConnectOptionsExt, MultiProtocolDevice, BleEvent, BleEventCallback, BleService, BleCharacteristic, WriteCharacteristicOptions, AutoBleInterfaces, BleDataReceivedCallback } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
const serviceManager = ServiceManager.getInstance();
export class BluetoothService {
scanDevices(options?: ScanDevicesOptions) { return BluetoothManager.scanDevices(options); }
connectDevice(deviceId: string, protocol: string, options?: BleConnectOptionsExt) { return BluetoothManager.connectDevice(deviceId, protocol, options); }
disconnectDevice(deviceId: string, protocol: string) { return BluetoothManager.disconnectDevice(deviceId, protocol); }
getConnectedDevices(): MultiProtocolDevice[] { return BluetoothManager.getConnectedDevices(); }
on(event: BleEvent, callback: BleEventCallback) { return BluetoothManager.on(event, callback); }
off(event: BleEvent, callback?: BleEventCallback) { return BluetoothManager.off(event, callback); }
getServices(deviceId: string): Promise<BleService[]> {
return new Promise((resolve, reject) => {
serviceManager.getServices(deviceId, (list, err) => {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:18','getServices:', list, err); // 新增日志
if (err != null) reject(err);
else resolve((list as BleService[]) ?? []);
});
});
}
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> {
return new Promise((resolve, reject) => {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:26',deviceId,serviceId)
serviceManager.getCharacteristics(deviceId, serviceId, (list, err) => {
if (err != null) reject(err);
else resolve((list as BleCharacteristic[]) ?? []);
});
});
}
/**
* 自动发现服务和特征返回可用的写入和通知特征ID
* @param deviceId 设备ID
* @returns {Promise<AutoBleInterfaces>}
*/
async getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
// 1. 获取服务列表
const services = await this.getServices(deviceId);
if (services == null || services.length == 0) throw new Error('未发现服务');
// 2. 选择目标服务优先bae前缀可根据需要调整
let serviceId = '';
for (let i = 0; i < services.length; i++) {
const s = services[i];
const uuidCandidate: string | null = (s.uuid != null ? s.uuid : null)
const uuid: string = uuidCandidate != null ? uuidCandidate : ''
// prefer regex test to avoid nullable receiver calls in generated Kotlin
if (/^bae/i.test(uuid)) {
serviceId = uuid
break;
}
}
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:55',serviceId)
if (serviceId == null || serviceId == '') serviceId = services[0].uuid;
// 3. 获取特征列表
const characteristics = await this.getCharacteristics(deviceId, serviceId);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:60',characteristics)
if (characteristics == null || characteristics.length == 0) throw new Error('未发现特征值');
// 4. 筛选write和notify特征
let writeCharId = '';
let notifyCharId = '';
for (let i = 0; i < characteristics.length; i++) {
const c = characteristics[i];
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:69',c)
if ((writeCharId == null || writeCharId == '') && c.properties != null && (c.properties.write || c.properties.writeWithoutResponse==true)) writeCharId = c.uuid;
if ((notifyCharId == null || notifyCharId == '') && c.properties != null && (c.properties.notify || c.properties.indicate)) notifyCharId = c.uuid;
}
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:73',serviceId, writeCharId, notifyCharId);
if ((writeCharId == null || writeCharId == '') || (notifyCharId == null || notifyCharId == '')) throw new Error('未找到合适的写入或通知特征');
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:75',serviceId, writeCharId, notifyCharId);
// // 发现服务和特征后
const deviceManager = DeviceManager.getInstance();
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:78',deviceManager);
const device = deviceManager.getDevice(deviceId);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:80',deviceId,device)
device!.serviceId = serviceId;
device!.writeCharId = writeCharId;
device!.notifyCharId = notifyCharId;
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:84',device);
return { serviceId, writeCharId, notifyCharId };
}
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, onData);
}
async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
return serviceManager.readCharacteristic(deviceId, serviceId, characteristicId);
}
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, data: Uint8Array, options?: WriteCharacteristicOptions): Promise<boolean> {
return serviceManager.writeCharacteristic(deviceId, serviceId, characteristicId, data, options);
}
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
return serviceManager.unsubscribeCharacteristic(deviceId, serviceId, characteristicId);
}
async autoDiscoverAll(deviceId: string): Promise<any> {
return serviceManager.autoDiscoverAll(deviceId);
}
async subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeAllNotifications(deviceId, onData);
}
}
export const bluetoothService = new BluetoothService();
// Ensure protocol handlers are registered when this module is imported.
// import './protocol_registry.uts';

View File

@@ -0,0 +1,663 @@
import type { BleService, BleCharacteristic, BleDataReceivedCallback, BleCharacteristicProperties, WriteCharacteristicOptions, ByteArray } from '../interface.uts'
import BluetoothGatt from "android.bluetooth.BluetoothGatt";
import BluetoothGattService from "android.bluetooth.BluetoothGattService";
import BluetoothGattCharacteristic from "android.bluetooth.BluetoothGattCharacteristic";
import BluetoothGattDescriptor from "android.bluetooth.BluetoothGattDescriptor";
import BluetoothGattCallback from "android.bluetooth.BluetoothGattCallback";
import UUID from "java.util.UUID";
import { DeviceManager } from './device_manager.uts'
import { AkBleErrorImpl, AkBluetoothErrorCode } from '../unierror.uts'
import { AutoDiscoverAllResult } from '../interface.uts'
// 补全UUID格式将短格式转换为标准格式
function getFullUuid(shortUuid : string) : string {
return `0000${shortUuid}-0000-1000-8000-00805f9b34fb`
}
const deviceWriteQueues = new Map<string, Promise<void>>();
function enqueueDeviceWrite<T>(deviceId : string, work : () => Promise<T>) : Promise<T> {
const previous = deviceWriteQueues.get(deviceId) ?? Promise.resolve();
const next = (async () : Promise<T> => {
try {
await previous;
} catch (e) { /* ignore previous rejection to keep queue alive */ }
return await work();
})();
const queued = next.then(() => { }, () => { });
deviceWriteQueues.set(deviceId, queued);
return next.finally(() => {
if (deviceWriteQueues.get(deviceId) == queued) {
deviceWriteQueues.delete(deviceId);
}
});
}
function createCharProperties(props : number) : BleCharacteristicProperties {
const result : BleCharacteristicProperties = {
read: false,
write: false,
notify: false,
indicate: false,
canRead: false,
canWrite: false,
canNotify: false,
writeWithoutResponse: false
};
result.read = (props & BluetoothGattCharacteristic.PROPERTY_READ) !== 0;
result.write = (props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0;
result.notify = (props & BluetoothGattCharacteristic.PROPERTY_NOTIFY) !== 0;
result.indicate = (props & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0;
result.writeWithoutResponse = (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0;
result.canRead = result.read;
const writeWithoutResponse = result.writeWithoutResponse;
result.canWrite = (result.write != null && result.write) || (writeWithoutResponse != null && writeWithoutResponse);
result.canNotify = result.notify;
return result;
}
// 定义 PendingCallback 类型和实现类
interface PendingCallback {
resolve : (data : any) => void;
reject : (err ?: any) => void;
timer ?: number; // Changed from any to number
}
class PendingCallbackImpl implements PendingCallback {
resolve : (data : any) => void;
reject : (err ?: any) => void;
timer ?: number; // Changed from any to number
constructor(resolve : (data : any) => void, reject : (err ?: any) => void, timer ?: number) {
this.resolve = resolve;
this.reject = reject;
this.timer = timer;
}
}
// 全局回调管理(必须在类外部声明)
let pendingCallbacks : Map<string, PendingCallback>;
let notifyCallbacks : Map<string, BleDataReceivedCallback>;
// 在全局范围内初始化
pendingCallbacks = new Map<string, PendingCallback>();
notifyCallbacks = new Map<string, BleDataReceivedCallback>();
// 服务发现等待队列deviceId -> 回调数组
const serviceDiscoveryWaiters = new Map<string, ((services : BleService[] | null, error ?: Error) => void)[]>();
// 服务发现状态deviceId -> 是否已发现
const serviceDiscovered = new Map<string, boolean>();
// 特征发现等待队列deviceId|serviceId -> 回调数组
const characteristicDiscoveryWaiters = new Map<string, ((characteristics : BleCharacteristic[] | null, error ?: Error) => void)[]>();
class GattCallback extends BluetoothGattCallback {
constructor() {
super();
}
override onServicesDiscovered(gatt : BluetoothGatt, status : Int) : void {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:108','ak onServicesDiscovered')
const deviceId = gatt.getDevice().getAddress();
if (status == BluetoothGatt.GATT_SUCCESS) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:111',`服务发现成功: ${deviceId}`);
serviceDiscovered.set(deviceId, true);
// 统一回调所有等待 getServices 的调用
const waiters = serviceDiscoveryWaiters.get(deviceId);
if (waiters != null && waiters.length > 0) {
const services = gatt.getServices();
const result : BleService[] = [];
if (services != null) {
const servicesList = services;
const size = servicesList.size;
for (let i = 0; i < size; i++) {
const service = servicesList.get(i as Int);
if (service != null) {
const bleService : BleService = {
uuid: service.getUuid().toString(),
isPrimary: service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY
};
result.push(bleService);
}
}
}
for (let i = 0; i < waiters.length; i++) {
const cb = waiters[i];
if (cb != null) { cb(result, null); }
}
serviceDiscoveryWaiters.delete(deviceId);
}
} else {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:139',`服务发现失败: ${deviceId}, status: ${status}`);
// 失败时也要通知等待队列
const waiters = serviceDiscoveryWaiters.get(deviceId);
if (waiters != null && waiters.length > 0) {
for (let i = 0; i < waiters.length; i++) {
const cb = waiters[i];
if (cb != null) { cb(null, new Error('服务发现失败')); }
}
serviceDiscoveryWaiters.delete(deviceId);
}
}
}
override onConnectionStateChange(gatt : BluetoothGatt, status : Int, newState : Int) : void {
const deviceId = gatt.getDevice().getAddress();
if (newState == BluetoothGatt.STATE_CONNECTED) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:154',`设备已连接: ${deviceId}`);
DeviceManager.handleConnectionStateChange(deviceId, 2, null); // 2 = STATE_CONNECTED
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:157',`设备已断开: ${deviceId}`);
serviceDiscovered.delete(deviceId);
DeviceManager.handleConnectionStateChange(deviceId, 0, null); // 0 = STATE_DISCONNECTED
}
}
override onCharacteristicChanged(gatt : BluetoothGatt, characteristic : BluetoothGattCharacteristic) : void {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:163','ak onCharacteristicChanged')
const deviceId = gatt.getDevice().getAddress();
const serviceId = characteristic.getService().getUuid().toString();
const charId = characteristic.getUuid().toString();
const key = `${deviceId}|${serviceId}|${charId}|notify`;
const callback = notifyCallbacks.get(key);
const value = characteristic.getValue();
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:170','[onCharacteristicChanged]', key, value);
if (callback != null && value != null) {
const valueLength = value.size;
const arr = new Uint8Array(valueLength);
for (let i = 0 as Int; i < valueLength; i++) {
const v = value[i as Int];
arr[i] = v != null ? v : 0;
}
// 保存接收日志
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:179',`
INSERT INTO ble_data_log (device_id, service_id, char_id, direction, data, timestamp)
VALUES ('${deviceId}', '${serviceId}', '${charId}', 'recv', '${Array.from(arr).join(',')}', ${Date.now()})
`)
callback(arr);
}
}
override onCharacteristicRead(gatt : BluetoothGatt, characteristic : BluetoothGattCharacteristic, status : Int) : void {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:188','ak onCharacteristicRead', status)
const deviceId = gatt.getDevice().getAddress();
const serviceId = characteristic.getService().getUuid().toString();
const charId = characteristic.getUuid().toString();
const key = `${deviceId}|${serviceId}|${charId}|read`;
const pending = pendingCallbacks.get(key);
const value = characteristic.getValue();
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:195','[onCharacteristicRead]', key, 'status=', status, 'value=', value);
if (pending != null) {
try {
const timer = pending.timer;
if (timer != null) {
clearTimeout(timer);
pending.timer = null;
}
pendingCallbacks.delete(key);
if (status == BluetoothGatt.GATT_SUCCESS && value != null) {
const valueLength = value.size
const arr = new Uint8Array(valueLength);
for (let i = 0 as Int; i < valueLength; i++) {
const v = value[i as Int];
arr[i] = v != null ? v : 0;
}
// resolve with ArrayBuffer
pending.resolve(arr.buffer as ArrayBuffer);
} else {
pending.reject(new Error('Characteristic read failed'));
}
} catch (e) {
try { pending.reject(e); } catch (e2) { __f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:218',e2); }
}
}
}
override onCharacteristicWrite(gatt : BluetoothGatt, characteristic : BluetoothGattCharacteristic, status : Int) : void {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:224','ak onCharacteristicWrite', status)
const deviceId = gatt.getDevice().getAddress();
const serviceId = characteristic.getService().getUuid().toString();
const charId = characteristic.getUuid().toString();
const key = `${deviceId}|${serviceId}|${charId}|write`;
const pending = pendingCallbacks.get(key);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:230','[onCharacteristicWrite]', key, 'status=', status);
if (pending != null) {
try {
const timer = pending.timer;
if (timer != null) {
clearTimeout(timer);
}
pendingCallbacks.delete(key);
if (status == BluetoothGatt.GATT_SUCCESS) {
pending.resolve('ok');
} else {
pending.reject(new Error('Characteristic write failed'));
}
} catch (e) {
try { pending.reject(e); } catch (e2) { __f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:244',e2); }
}
}
}
}
// 导出单例实例供外部使用
export const gattCallback = new GattCallback();
export class ServiceManager {
private static instance : ServiceManager | null = null;
private services = new Map<string, BleService[]>();
private characteristics = new Map<string, Map<string, BleCharacteristic[]>>();
private deviceManager = DeviceManager.getInstance();
private constructor() { }
static getInstance() : ServiceManager {
if (ServiceManager.instance == null) {
ServiceManager.instance = new ServiceManager();
}
return ServiceManager.instance!;
}
getServices(deviceId : string, callback ?: (services : BleService[] | null, error ?: Error) => void) : any | Promise<BleService[]> {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:267','ak start getservice', deviceId);
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) {
if (callback != null) { callback(null, new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "")); }
return Promise.reject(new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", ""));
}
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:273','ak serviceDiscovered', gatt)
// 如果服务已发现,直接返回
if (serviceDiscovered.get(deviceId) == true) {
const services = gatt.getServices();
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:277',services)
const result : BleService[] = [];
if (services != null) {
const servicesList = services;
const size = servicesList.size;
if (size > 0) {
for (let i = 0 as Int; i < size; i++) {
const service = servicesList != null ? servicesList.get(i) : servicesList[i];
if (service != null) {
const bleService : BleService = {
uuid: service.getUuid().toString(),
isPrimary: service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY
};
result.push(bleService);
if (bleService.uuid == getFullUuid('0001')) {
const device = this.deviceManager.getDevice(deviceId);
if (device != null) {
device.serviceId = bleService.uuid;
this.getCharacteristics(deviceId, device.serviceId, (chars, err) => {
if (err == null && chars != null) {
const writeChar = chars.find(c => c.uuid == getFullUuid('0010'));
const notifyChar = chars.find(c => c.uuid == getFullUuid('0011'));
if (writeChar != null) device.writeCharId = writeChar.uuid;
if (notifyChar != null) device.notifyCharId = notifyChar.uuid;
}
});
}
}
}
}
}
}
if (callback != null) { callback(result, null); }
return Promise.resolve(result);
}
// 未发现则发起服务发现并加入等待队列
if (!serviceDiscoveryWaiters.has(deviceId)) {
serviceDiscoveryWaiters.set(deviceId, []);
gatt.discoverServices();
}
return new Promise<BleService[]>((resolve, reject) => {
const cb = (services : BleService[] | null, error ?: Error) => {
if (error != null) reject(error);
else resolve(services ?? []);
if (callback != null) callback(services, error);
};
const arr = serviceDiscoveryWaiters.get(deviceId);
if (arr != null) arr.push(cb);
});
}
getCharacteristics(deviceId : string, serviceId : string, callback : (characteristics : BleCharacteristic[] | null, error ?: Error) => void) : void {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) return callback(null, new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", ""));
// 如果服务还没发现,等待服务发现后再查特征
if (serviceDiscovered.get(deviceId) !== true) {
// 先注册到服务发现等待队列
this.getServices(deviceId, (services, err) => {
if (err != null) {
callback(null, err);
} else {
this.getCharacteristics(deviceId, serviceId, callback);
}
});
return;
}
// 服务已发现,正常获取特征
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null) return callback(null, new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", ""));
const chars = service.getCharacteristics();
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:347',chars)
const result : BleCharacteristic[] = [];
if (chars != null) {
const characteristicsList = chars;
const size = characteristicsList.size;
const bleService : BleService = {
uuid: serviceId,
isPrimary: service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY
};
for (let i = 0 as Int; i < size; i++) {
const char = characteristicsList != null ? characteristicsList.get(i as Int) : characteristicsList[i];
if (char != null) {
const props = char.getProperties();
try {
const charUuid = char.getUuid() != null ? char.getUuid().toString() : '';
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:362','[ServiceManager] characteristic uuid=', charUuid);
} catch (e) { __f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:363','[ServiceManager] failed to read char uuid', e); }
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:364',props);
const bleCharacteristic : BleCharacteristic = {
uuid: char.getUuid().toString(),
service: bleService,
properties: createCharProperties(props)
};
result.push(bleCharacteristic);
}
}
}
callback(result, null);
}
public async readCharacteristic(deviceId : string, serviceId : string, characteristicId : string) : Promise<ArrayBuffer> {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null) throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null) throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
const key = `${deviceId}|${serviceId}|${characteristicId}|read`;
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:385',key)
return new Promise<ArrayBuffer>((resolve, reject) => {
const timer = setTimeout(() => {
pendingCallbacks.delete(key);
reject(new AkBleErrorImpl(AkBluetoothErrorCode.ConnectionTimeout, "Connection timeout", ""));
}, 5000);
const resolveAdapter = (data : any) => { __f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:391','read resolve:', data); resolve(data as ArrayBuffer); };
const rejectAdapter = (err ?: any) => { reject(new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Unknown error occurred", "")); };
pendingCallbacks.set(key, new PendingCallbackImpl(resolveAdapter, rejectAdapter, timer));
if (gatt.readCharacteristic(char) == false) {
clearTimeout(timer);
pendingCallbacks.delete(key);
reject(new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Unknown error occurred", ""));
}
else {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:400','read should be succeed', key)
}
});
}
public async writeCharacteristic(deviceId : string, serviceId : string, characteristicId : string, data : Uint8Array, options ?: WriteCharacteristicOptions) : Promise<boolean> {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:406','[writeCharacteristic] deviceId:', deviceId, 'serviceId:', serviceId, 'characteristicId:', characteristicId, 'data:', data);
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:409','[writeCharacteristic] gatt is null');
throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
}
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:414','[writeCharacteristic] service is null');
throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
}
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:419','[writeCharacteristic] characteristic is null');
throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
}
const key = `${deviceId}|${serviceId}|${characteristicId}|write`;
const wantsNoResponse = options != null && options.waitForResponse == false;
let retryMaxAttempts = 20;
let retryDelay = 100;
let giveupTimeout = 20000;
if (options != null) {
try {
if (options.maxAttempts != null) {
const parsedAttempts = Math.floor(options.maxAttempts as number);
if (!isNaN(parsedAttempts) && parsedAttempts > 0) retryMaxAttempts = parsedAttempts;
}
} catch (e) { }
try {
if (options.retryDelayMs != null) {
const parsedDelay = Math.floor(options.retryDelayMs as number);
if (!isNaN(parsedDelay) && parsedDelay >= 0) retryDelay = parsedDelay;
}
} catch (e) { }
try {
if (options.giveupTimeoutMs != null) {
const parsedGiveup = Math.floor(options.giveupTimeoutMs as number);
if (!isNaN(parsedGiveup) && parsedGiveup > 0) giveupTimeout = parsedGiveup;
}
} catch (e) { }
}
const gattInstance = gatt;
const executeWrite = () : Promise<boolean> => {
return new Promise<boolean>((resolve) => {
const initialTimeout = Math.max(giveupTimeout + 5000, 10000);
let timer = setTimeout(() => {
pendingCallbacks.delete(key);
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:454','[writeCharacteristic] timeout');
resolve(false);
}, initialTimeout);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:457','[writeCharacteristic] initial timeout set to', initialTimeout, 'ms for', key);
const resolveAdapter = (data : any) => {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:459','[writeCharacteristic] resolveAdapter called');
resolve(true);
};
const rejectAdapter = (err ?: any) => {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:463','[writeCharacteristic] rejectAdapter called', err);
resolve(false);
};
pendingCallbacks.set(key, new PendingCallbackImpl(resolveAdapter, rejectAdapter, timer));
const byteArray = new ByteArray(data.length as Int);
for (let i = 0 as Int; i < data.length; i++) {
byteArray[i] = data[i].toByte();
}
const forceWriteTypeNoResponse = options != null && options.forceWriteTypeNoResponse == true;
let usesNoResponse = forceWriteTypeNoResponse || wantsNoResponse;
try {
const props = char.getProperties();
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:475','[writeCharacteristic] characteristic properties mask=', props);
if (usesNoResponse == false) {
const supportsWriteWithResponse = (props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0;
const supportsWriteNoResponse = (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0;
if (supportsWriteWithResponse == false && supportsWriteNoResponse == true) {
usesNoResponse = true;
}
}
if (usesNoResponse) {
try { char.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); } catch (e) { }
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:485','[writeCharacteristic] using WRITE_TYPE_NO_RESPONSE');
} else {
try { char.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); } catch (e) { }
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:488','[writeCharacteristic] using WRITE_TYPE_DEFAULT');
}
} catch (e) {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:491','[writeCharacteristic] failed to inspect/set write type', e);
}
const maxAttempts = retryMaxAttempts;
function attemptWrite(att : Int) : void {
try {
let setOk = true;
try {
const setRes = char.setValue(byteArray);
if (typeof setRes == 'boolean' && setRes == false) {
setOk = false;
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:501','[writeCharacteristic] setValue returned false for', key, 'attempt', att);
}
} catch (e) {
setOk = false;
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:505','[writeCharacteristic] setValue threw for', key, 'attempt', att, e);
}
if (setOk == false) {
if (att >= maxAttempts) {
try { clearTimeout(timer); } catch (e) { }
pendingCallbacks.delete(key);
resolve(false);
return;
}
setTimeout(() => { attemptWrite((att + 1) as Int); }, retryDelay);
return;
}
try {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:518','[writeCharacteristic] attempt', att, 'calling gatt.writeCharacteristic');
const r = gattInstance.writeCharacteristic(char);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:520','[writeCharacteristic] attempt', att, 'result=', r);
if (r == true) {
if (usesNoResponse) {
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:523','[writeCharacteristic] WRITE_TYPE_NO_RESPONSE success for', key);
try { clearTimeout(timer); } catch (e) { }
pendingCallbacks.delete(key);
resolve(true);
return;
}
try { clearTimeout(timer); } catch (e) { }
const extra = 20000;
timer = setTimeout(() => {
pendingCallbacks.delete(key);
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:533','[writeCharacteristic] timeout after write initiated');
resolve(false);
}, extra);
const pendingEntry = pendingCallbacks.get(key);
if (pendingEntry != null) pendingEntry.timer = timer;
return;
}
} catch (e) {
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:541','[writeCharacteristic] attempt', att, 'exception when calling writeCharacteristic', e);
}
if (att < maxAttempts) {
const nextAtt = (att + 1) as Int;
setTimeout(() => { attemptWrite(nextAtt); }, retryDelay);
return;
}
if (usesNoResponse) {
try { clearTimeout(timer); } catch (e) { }
pendingCallbacks.delete(key);
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:551','[writeCharacteristic] all attempts failed with WRITE_NO_RESPONSE for', key);
resolve(false);
return;
}
try { clearTimeout(timer); } catch (e) { }
const giveupTimeoutLocal = giveupTimeout;
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:557','[writeCharacteristic] all attempts failed; waiting for late callback up to', giveupTimeoutLocal, 'ms for', key);
const giveupTimer = setTimeout(() => {
pendingCallbacks.delete(key);
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:560','[writeCharacteristic] giveup timeout expired for', key);
resolve(false);
}, giveupTimeoutLocal);
const pendingEntryAfter = pendingCallbacks.get(key);
if (pendingEntryAfter != null) pendingEntryAfter.timer = giveupTimer;
} catch (e) {
clearTimeout(timer);
pendingCallbacks.delete(key);
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:568','[writeCharacteristic] Exception in attemptWrite', e);
resolve(false);
}
}
try {
attemptWrite(1 as Int);
} catch (e) {
clearTimeout(timer);
pendingCallbacks.delete(key);
__f__('error','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:578','[writeCharacteristic] Exception before attempting write', e);
resolve(false);
}
});
};
return enqueueDeviceWrite(deviceId, executeWrite);
}
public async subscribeCharacteristic(deviceId : string, serviceId : string, characteristicId : string, onData : BleDataReceivedCallback) : Promise<void> {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null) throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null) throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
const key = `${deviceId}|${serviceId}|${characteristicId}|notify`;
notifyCallbacks.set(key, onData);
if (gatt.setCharacteristicNotification(char, true) == false) {
notifyCallbacks.delete(key);
throw new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Failed to unsubscribe characteristic", "");
} else {
// 写入 CCCD 描述符,启用 notify
const descriptor = char.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (descriptor != null) {
// 设置描述符值
const value =
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
descriptor.setValue(value);
const writedescript = gatt.writeDescriptor(descriptor);
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:608','subscribeCharacteristic: CCCD written for notify', writedescript);
} else {
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:610','subscribeCharacteristic: CCCD descriptor not found!');
}
__f__('log','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:612','subscribeCharacteristic ok!!');
}
}
public async unsubscribeCharacteristic(deviceId : string, serviceId : string, characteristicId : string) : Promise<void> {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null) throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null) throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
const key = `${deviceId}|${serviceId}|${characteristicId}|notify`;
notifyCallbacks.delete(key);
if (gatt.setCharacteristicNotification(char, false) == false) {
throw new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Failed to unsubscribe characteristic", "");
}
}
// 自动发现所有服务和特征
public async autoDiscoverAll(deviceId : string) : Promise<AutoDiscoverAllResult> {
const services = await this.getServices(deviceId, null) as BleService[];
const allCharacteristics : BleCharacteristic[] = [];
for (const service of services) {
await new Promise<void>((resolve, reject) => {
this.getCharacteristics(deviceId, service.uuid, (chars, err) => {
if (err != null) reject(err);
else {
if (chars != null) allCharacteristics.push(...chars);
resolve();
}
});
});
}
return { services, characteristics: allCharacteristics };
}
// 自动订阅所有支持 notify/indicate 的特征
public async subscribeAllNotifications(deviceId : string, onData : BleDataReceivedCallback) : Promise<void> {
const { services, characteristics } = await this.autoDiscoverAll(deviceId);
for (const char of characteristics) {
if (char.properties.notify || char.properties.indicate) {
try {
await this.subscribeCharacteristic(deviceId, char.service.uuid, char.uuid, onData);
} catch (e) {
// 可以选择忽略单个特征订阅失败
__f__('warn','at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:658',`订阅特征 ${char.uuid} 失败:`, e);
}
}
}
}
}

View File

@@ -0,0 +1,481 @@
// 蓝牙相关接口和类型定义
// 基础设备信息类型
export type BleDeviceInfo = {
deviceId : string;
name : string;
RSSI ?: number;
connected ?: boolean;
// 新增
serviceId ?: string;
writeCharId ?: string;
notifyCharId ?: string;
}
export type AutoDiscoverAllResult = {
services : BleService[];
characteristics : BleCharacteristic[];
}
// 服务信息类型
export type BleServiceInfo = {
uuid : string;
isPrimary : boolean;
}
// 特征值属性类型
export type BleCharacteristicProperties = {
read : boolean;
write : boolean;
notify : boolean;
indicate : boolean;
writeWithoutResponse ?: boolean;
canRead ?: boolean;
canWrite ?: boolean;
canNotify ?: boolean;
}
// 特征值信息类型
export type BleCharacteristicInfo = {
uuid : string;
serviceId : string;
properties : BleCharacteristicProperties;
}
// 错误状态码
export enum BleErrorCode {
UNKNOWN_ERROR = 0,
BLUETOOTH_UNAVAILABLE = 1,
PERMISSION_DENIED = 2,
DEVICE_NOT_CONNECTED = 3,
SERVICE_NOT_FOUND = 4,
CHARACTERISTIC_NOT_FOUND = 5,
OPERATION_TIMEOUT = 6
}
// 命令类型
export enum CommandType {
BATTERY = 1,
DEVICE_INFO = 2,
CUSTOM = 99,
TestBatteryLevel = 0x01
}
// 错误接口
export type BleError {
errCode : number;
errMsg : string;
errSubject ?: string;
}
// 连接选项
export type BleConnectOptions = {
deviceId : string;
timeout ?: number;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 断开连接选项
export type BleDisconnectOptions = {
deviceId : string;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 获取特征值选项
export type BleCharacteristicOptions = {
deviceId : string;
serviceId : string;
characteristicId : string;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 写入特征值选项
export type BleWriteOptions = {
deviceId : string;
serviceId : string;
characteristicId : string;
value : Uint8Array;
writeType ?: number;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// Options for writeCharacteristic helper
export type WriteCharacteristicOptions = {
waitForResponse ?: boolean;
maxAttempts ?: number;
retryDelayMs ?: number;
giveupTimeoutMs ?: number;
forceWriteTypeNoResponse ?: boolean;
}
// 通知特征值回调函数
export type BleNotifyCallback = (data : Uint8Array) => void;
// 通知特征值选项
export type BleNotifyOptions = {
deviceId : string;
serviceId : string;
characteristicId : string;
state ?: boolean; // true: 启用通知false: 禁用通知
onCharacteristicValueChange : BleNotifyCallback;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 获取服务选项
export type BleDeviceServicesOptions = {
deviceId : string;
success ?: (result : BleServicesResult) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 获取特征值选项
export type BleDeviceCharacteristicsOptions = {
deviceId : string;
serviceId : string;
success ?: (result : BleCharacteristicsResult) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 蓝牙扫描选项
export type BluetoothScanOptions = {
services ?: string[];
timeout ?: number;
onDeviceFound ?: (device : BleDeviceInfo) => void;
success ?: (result : BleScanResult) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 扫描结果
// 服务结果
export type BleServicesResult = {
services : BleServiceInfo[];
errMsg ?: string;
}
// 特征值结果
export type BleCharacteristicsResult = {
characteristics : BleCharacteristicInfo[];
errMsg ?: string;
}
// 定义连接状态枚举
export enum BLE_CONNECTION_STATE {
DISCONNECTED = 0,
CONNECTING = 1,
CONNECTED = 2,
DISCONNECTING = 3
}
// 电池状态类型定义
export type BatteryStatus = {
batteryLevel : number; // 电量百分比
isCharging : boolean; // 充电状态
}
// 蓝牙服务接口类型定义 - 转换为type类型
export type BleService = {
uuid : string;
isPrimary : boolean;
}
// 蓝牙特征值接口定义 - 转换为type类型
export type BleCharacteristic = {
uuid : string;
service : BleService;
properties : BleCharacteristicProperties;
}
// PendingPromise接口定义
export interface PendingCallback {
resolve : (data : any) => void;
reject : (err ?: any) => void;
timer ?: number;
}
// 蓝牙相关接口和类型定义
export type BleDevice = {
deviceId : string;
name : string;
rssi ?: number;
lastSeen ?: number; // 新增
// 新增
serviceId ?: string;
writeCharId ?: string;
notifyCharId ?: string;
}
// BLE常规选项
export type BleOptions = {
timeout ?: number;
success ?: (result : any) => void;
fail ?: (error : any) => void;
complete ?: () => void;
}
export type BleConnectionState = number; // 0: DISCONNECTED, 1: CONNECTING, 2: CONNECTED, 3: DISCONNECTING
export type BleConnectOptionsExt = {
timeout ?: number;
services ?: string[];
requireResponse ?: boolean;
autoReconnect ?: boolean;
maxAttempts ?: number;
interval ?: number;
};
// 回调函数类型
export type BleDeviceFoundCallback = (device : BleDevice) => void;
export type BleConnectionStateChangeCallback = (deviceId : string, state : BleConnectionState) => void;
export type BleDataPayload = {
deviceId : string;
serviceId ?: string;
characteristicId ?: string;
data : string | ArrayBuffer;
format ?: number; // 0: JSON, 1: XML, 2: RAW
}
export type BleDataSentCallback = (payload : BleDataPayload, success : boolean, error ?: BleError) => void;
export type BleErrorCallback = (error : BleError) => void;
// 健康数据类型定义
export enum HealthDataType {
HEART_RATE = 1,
BLOOD_OXYGEN = 2,
TEMPERATURE = 3,
STEP_COUNT = 4,
SLEEP_DATA = 5,
HEALTH_DATA = 6
}
// Platform-specific services should be imported from per-platform entrypoints
// (e.g. './app-android/index.uts' or './web/index.uts').
// Avoid re-exporting platform modules at the SDK root to prevent bundlers
// Platform-specific services should be imported from per-platform entrypoints
// (e.g. './app-android/index.uts' or './web/index.uts').
// Avoid re-exporting platform modules at the SDK root to prevent bundlers
// from pulling android.* symbols into web bundles.
// If a typed ambient reference is required, declare the shape here instead of importing implementation.
// Example lightweight typed placeholder (do not import platform code here):
// export type BluetoothService = any; // platform-specific implementation exported from platform index files
// ====== 新增多协议、统一事件、协议适配、状态管理支持 ======
export type BleProtocolType =
| 'standard'
| 'custom'
| 'health'
| 'ibeacon'
| 'mesh';
export type BleEvent =
| 'deviceFound'
| 'scanFinished'
| 'connectionStateChanged'
| 'dataReceived'
| 'dataSent'
| 'error'
| 'servicesDiscovered'
| 'connected' // 新增
| 'disconnected'; // 新增
// 事件回调参数
export type BleEventPayload = {
event : BleEvent;
device ?: BleDevice;
protocol ?: BleProtocolType;
state ?: BleConnectionState;
data ?: ArrayBuffer | string | object;
format ?: string;
error ?: BleError;
extra ?: any;
}
// 事件回调函数
export type BleEventCallback = (payload : BleEventPayload) => void;
// 多协议设备信息(去除交叉类型,直接展开字段)
export type MultiProtocolDevice = {
deviceId : string;
name : string;
rssi ?: number;
protocol : BleProtocolType;
};
export type ScanDevicesOptions = {
protocols ?: BleProtocolType[];
optionalServices ?: string[];
timeout ?: number;
onDeviceFound ?: (device : BleDevice) => void;
onScanFinished ?: () => void;
};
// Named payload type used by sendData
export type SendDataPayload = {
deviceId : string;
serviceId ?: string;
characteristicId ?: string;
data : string | ArrayBuffer;
format ?: number;
protocol : BleProtocolType;
}
// 协议处理器接口(为 protocol-handler 适配器预留)
export type ScanHandler = {
protocol : BleProtocolType;
scanDevices ?: (options : ScanDevicesOptions) => Promise<void>;
connect : (device : BleDevice, options ?: BleConnectOptionsExt) => Promise<void>;
disconnect : (device : BleDevice) => Promise<void>;
// Optional: send arbitrary data via the protocol's write characteristic
sendData ?: (device : BleDevice, payload : SendDataPayload, options ?: BleOptions) => Promise<void>;
// Optional: try to connect and discover service/characteristic ids for this device
autoConnect ?: (device : BleDevice, options ?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
}
// 自动发现服务和特征返回类型
export type AutoBleInterfaces = {
serviceId : string;
writeCharId : string;
notifyCharId : string;
}
export type ResponseCallbackEntry = {
cb : (data : Uint8Array) => boolean | void;
multi : boolean;
};
// Result returned by a DFU control parser. Use a plain string `type` to keep
// the generated Kotlin simple and avoid inline union types which the generator
// does not handle well.
export type ControlParserResult = {
type : string; // e.g. 'progress', 'success', 'error', 'info'
progress ?: number;
error ?: any;
}
// DFU types
export type DfuOptions = {
mtu ?: number;
useNordic ?: boolean;
// If true, the DFU upload will await a write response per-packet. Set false to use
// WRITE_NO_RESPONSE (fire-and-forget) for higher throughput. Default: false.
waitForResponse ?: boolean;
// Maximum number of outstanding NO_RESPONSE writes to allow before throttling.
// This implements a simple sliding window. Default: 32.
maxOutstanding ?: number;
// Per-chunk sleep (ms) to yield to event loop / Android BLE stack. Default: 2.
writeSleepMs ?: number;
// Retry delay (ms) used by the Android write helper when gatt.writeCharacteristic
// returns false. Smaller values can improve throughput on congested stacks.
writeRetryDelayMs ?: number;
// Maximum number of immediate write attempts before falling back to the give-up timeout.
writeMaxAttempts ?: number;
// Timeout (ms) to wait for a late onCharacteristicWrite callback after all retries fail.
writeGiveupTimeoutMs ?: number;
// Packet Receipt Notification (PRN) window size in packets. If set, DFU
// manager will send a Set PRN command to the device and wait for PRN
// notifications after this many packets. Default: 12.
prn ?: number;
// Timeout (ms) to wait for a PRN notification once the window is reached.
// Default: 10000 (10s).
prnTimeoutMs ?: number;
// When true, disable PRN waits automatically after the first timeout to prevent
// repeated long stalls on devices that do not send PRNs. Default: true.
disablePrnOnTimeout ?: boolean;
// Time (ms) to wait for outstanding fire-and-forget writes to drain before issuing
// the activate/validate control command. Default: 3000.
drainOutstandingTimeoutMs ?: number;
controlTimeout ?: number;
onProgress ?: (percent : number) => void;
onLog ?: (message : string) => void;
controlParser ?: (data : Uint8Array) => ControlParserResult | null;
}
export type DfuManagerType = {
startDfu : (deviceId : string, firmwareBytes : Uint8Array, options ?: DfuOptions) => Promise<void>;
}
// Lightweight runtime / UTS shims and missing types
// These are conservative placeholders to satisfy typings used across platform files.
// UTSJSONObject: bundler environments used by the build may not support
// TypeScript-style index signatures in this .uts context. Use a conservative
// 'any' alias so generated code doesn't rely on unsupported syntax while
// preserving a usable type at the source level.
export type UTSJSONObject = any;
// ByteArray / Int are used in the Android platform code to interop with Java APIs.
// Define minimal aliases so source can compile. Runtime uses Uint8Array and number.
export type ByteArray = any; // runtime will use Java byte[] via UTS bridge; keep as any here
// Callback types used by service_manager and index wrappers
export type BleDataReceivedCallback = (data: Uint8Array) => void;
export type BleScanResult = {
deviceId: string;
name?: string;
rssi?: number;
advertising?: any;
};
// Minimal UI / framework placeholders (some files reference these in types only)
export type ComponentPublicInstance = any;
export type UniElement = any;
export type UniPage = any;
// Platform service placeholder (actual implementation exported from platform index files)
// Provide a lightweight, strongly-shaped class skeleton so source-level code
// (and the code generator) can rely on concrete method names and signatures.
// Implementations are platform-specific and exported from per-platform index
// files (e.g. './app-android/index.uts' or './web/index.uts'). This class is
// intentionally thin — it declares method signatures used across pages and
// platform shims so the generator emits resolvable Kotlin symbols.
export class BluetoothService {
// Event emitter style
on(event: BleEvent | string, callback: BleEventCallback): void {}
off(event: BleEvent | string, callback?: BleEventCallback): void {}
// Scanning / discovery
scanDevices(options?: ScanDevicesOptions): Promise<void> { return Promise.resolve(); }
// Connection management
connectDevice(deviceId: string, protocol?: string, options?: BleConnectOptionsExt): Promise<void> { return Promise.resolve(); }
disconnectDevice(deviceId: string, protocol?: string): Promise<void> { return Promise.resolve(); }
getConnectedDevices(): MultiProtocolDevice[] { return []; }
// Services / characteristics
getServices(deviceId: string): Promise<BleService[]> { return Promise.resolve([]); }
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> { return Promise.resolve([]); }
// Read / write / notify
readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> { return Promise.resolve(new ArrayBuffer(0)); }
writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> { return Promise.resolve(true); }
subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: BleNotifyCallback): Promise<void> { return Promise.resolve(); }
unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> { return Promise.resolve(); }
// Convenience helpers
getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
const res: AutoBleInterfaces = { serviceId: '', writeCharId: '', notifyCharId: '' };
return Promise.resolve(res);
}
}
// Runtime protocol handler base class. Exporting a concrete class ensures the
// generator emits a resolvable runtime type that platform handlers can extend.
// Source-level code can still use the ScanHandler type for typing.
// Runtime ProtocolHandler is implemented in `protocol_handler.uts`.
// Keep the public typing in this file minimal to avoid duplicate runtime
// declarations. Consumers that need the runtime class should import it from
// './protocol_handler.uts'.

View File

@@ -0,0 +1,115 @@
// Minimal ProtocolHandler runtime class used by pages and components.
// This class adapts the platform `BluetoothService` to a small protocol API
// expected by pages: setConnectionParameters, initialize, testBatteryLevel,
// testVersionInfo. Implemented conservatively to avoid heavy dependencies.
import type { BluetoothService, AutoBleInterfaces, AutoDiscoverAllResult, BleService, BleCharacteristic, BleProtocolType, BleDevice, ScanDevicesOptions, BleConnectOptionsExt, SendDataPayload, BleOptions } from './interface.uts'
export class ProtocolHandler {
// bluetoothService may be omitted for lightweight wrappers; allow null
bluetoothService: BluetoothService | null = null
protocol: BleProtocolType = 'standard'
deviceId: string | null = null
serviceId: string | null = null
writeCharId: string | null = null
notifyCharId: string | null = null
initialized: boolean = false
// Accept an optional BluetoothService so wrapper subclasses can call
// `super()` without forcing a runtime instance.
constructor(bluetoothService?: BluetoothService) {
if (bluetoothService != null) this.bluetoothService = bluetoothService
}
setConnectionParameters(deviceId: string, serviceId: string, writeCharId: string, notifyCharId: string) {
this.deviceId = deviceId
this.serviceId = serviceId
this.writeCharId = writeCharId
this.notifyCharId = notifyCharId
}
// initialize: optional setup, returns a Promise that resolves when ready
async initialize(): Promise<void> {
// Simple async initializer — keep implementation minimal and generator-friendly.
try {
// If bluetoothService exposes any protocol-specific setup, call it here.
this.initialized = true
return
} catch (e) {
throw e
}
}
// Protocol lifecycle / operations — default no-ops so generated code has
// concrete member references and platform-specific handlers can override.
async scanDevices(options?: ScanDevicesOptions): Promise<void> { return; }
async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> { return; }
async disconnect(device: BleDevice): Promise<void> { return; }
async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> { return; }
async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> { return { serviceId: '', writeCharId: '', notifyCharId: '' }; }
// Example: testBatteryLevel will attempt to read the battery characteristic
// if write/notify-based protocol is not available. Returns number percentage.
async testBatteryLevel(): Promise<number> {
if (this.deviceId == null) throw new Error('deviceId not set')
// copy to local so Kotlin generator can smart-cast the value across awaits
const deviceId = this.deviceId
// try reading standard Battery characteristic (180F -> 2A19)
if (this.bluetoothService == null) throw new Error('bluetoothService not set')
const services = await this.bluetoothService.getServices(deviceId)
let found: BleService | null = null
for (let i = 0; i < services.length; i++) {
const s = services[i]
const uuidCandidate: string | null = (s != null && s.uuid != null ? s.uuid : null)
const uuid = uuidCandidate != null ? ('' + uuidCandidate).toLowerCase() : ''
if (uuid.indexOf('180f') !== -1) { found = s; break }
}
if (found == null) {
// fallback: if writeCharId exists and notify available use protocol (not implemented)
return 0
}
const foundUuid = found!.uuid
const charsRaw = await this.bluetoothService.getCharacteristics(deviceId, foundUuid)
const chars: BleCharacteristic[] = charsRaw
const batChar = chars.find((c: BleCharacteristic) => ((c.properties != null && c.properties.read) || (c.uuid != null && ('' + c.uuid).toLowerCase().includes('2a19'))))
if (batChar == null) return 0
const buf = await this.bluetoothService.readCharacteristic(deviceId, foundUuid, batChar.uuid)
const arr = new Uint8Array(buf)
if (arr.length > 0) {
return arr[0]
}
return 0
}
// testVersionInfo: try to read Device Information characteristics or return empty
async testVersionInfo(hw: boolean): Promise<string> {
// copy to local so Kotlin generator can smart-cast the value across awaits
const deviceId = this.deviceId
if (deviceId == null) return ''
// Device Information service 180A, characteristics: 2A26 (SW), 2A27 (HW) sometimes
if (this.bluetoothService == null) return ''
const _services = await this.bluetoothService.getServices(deviceId)
const services2: BleService[] = _services
let found2: BleService | null = null
for (let i = 0; i < services2.length; i++) {
const s = services2[i]
const uuidCandidate: string | null = (s != null && s.uuid != null ? s.uuid : null)
const uuid = uuidCandidate != null ? ('' + uuidCandidate).toLowerCase() : ''
if (uuid.indexOf('180a') !== -1) { found2 = s; break }
}
if (found2 == null) return ''
const _found2 = found2
const found2Uuid = _found2!.uuid
const chars = await this.bluetoothService.getCharacteristics(deviceId, found2Uuid)
const target = chars.find((c) => {
const id = ('' + c.uuid).toLowerCase()
if (hw) return id.includes('2a27') || id.includes('hardware')
return id.includes('2a26') || id.includes('software')
})
if (target == null) return ''
const buf = await this.bluetoothService.readCharacteristic(deviceId, found2Uuid, target.uuid)
try { return new TextDecoder().decode(new Uint8Array(buf)) } catch (e) { return '' }
}
}

View File

@@ -0,0 +1,34 @@
// Minimal error definitions used across the BLE module.
// Keep this file small and avoid runtime dependencies; it's mainly for typing and
// simple runtime error construction used by native platform code.
export enum AkBluetoothErrorCode {
UnknownError = 0,
DeviceNotFound = 1,
ServiceNotFound = 2,
CharacteristicNotFound = 3,
ConnectionTimeout = 4,
Unspecified = 99
}
export class AkBleErrorImpl extends Error {
public code: AkBluetoothErrorCode;
public detail: any|null;
constructor(code: AkBluetoothErrorCode, message?: string, detail: any|null = null) {
super(message ?? AkBleErrorImpl.defaultMessage(code));
this.name = 'AkBleError';
this.code = code;
this.detail = detail;
}
static defaultMessage(code: AkBluetoothErrorCode) {
switch (code) {
case AkBluetoothErrorCode.DeviceNotFound: return 'Device not found';
case AkBluetoothErrorCode.ServiceNotFound: return 'Service not found';
case AkBluetoothErrorCode.CharacteristicNotFound: return 'Characteristic not found';
case AkBluetoothErrorCode.ConnectionTimeout: return 'Connection timed out';
case AkBluetoothErrorCode.UnknownError: default: return 'Unknown Bluetooth error';
}
}
}
export default AkBleErrorImpl;

View File

@@ -0,0 +1,76 @@
import Context from "android.content.Context";
import BatteryManager from "android.os.BatteryManager";
import { GetBatteryInfo, GetBatteryInfoOptions, GetBatteryInfoSuccess, GetBatteryInfoResult, GetBatteryInfoSync } from '../interface.uts'
import IntentFilter from 'android.content.IntentFilter';
import Intent from 'android.content.Intent';
import { GetBatteryInfoFailImpl } from '../unierror';
/**
* 异步获取电量
*/
export const getBatteryInfo : GetBatteryInfo = function (options : GetBatteryInfoOptions) {
const context = UTSAndroid.getAppContext();
if (context != null) {
const manager = context.getSystemService(
Context.BATTERY_SERVICE
) as BatteryManager;
const level = manager.getIntProperty(
BatteryManager.BATTERY_PROPERTY_CAPACITY
);
let ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
let batteryStatus = context.registerReceiver(null, ifilter);
let status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
let isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
const res : GetBatteryInfoSuccess = {
errMsg: 'getBatteryInfo:ok',
level,
isCharging: isCharging
}
options.success?.(res)
options.complete?.(res)
} else {
let res = new GetBatteryInfoFailImpl(1001);
options.fail?.(res)
options.complete?.(res)
}
}
/**
* 同步获取电量
*/
export const getBatteryInfoSync : GetBatteryInfoSync = function () : GetBatteryInfoResult {
const context = UTSAndroid.getAppContext();
if (context != null) {
const manager = context.getSystemService(
Context.BATTERY_SERVICE
) as BatteryManager;
const level = manager.getIntProperty(
BatteryManager.BATTERY_PROPERTY_CAPACITY
);
let ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
let batteryStatus = context.registerReceiver(null, ifilter);
let status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
let isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
const res : GetBatteryInfoResult = {
level: level,
isCharging: isCharging
};
return res;
}
else {
/**
* 无有效上下文
*/
const res : GetBatteryInfoResult = {
level: -1,
isCharging: false
};
return res;
}
}

View File

@@ -0,0 +1,36 @@
// 引用 iOS 原生平台 api
import { UIDevice } from "UIKit";
import { GetBatteryInfo, GetBatteryInfoSuccess, GetBatteryInfoResult, GetBatteryInfoSync } from '../interface.uts';
/**
* 异步获取电量
*/
export const getBatteryInfo : GetBatteryInfo = function (options) {
// 开启电量检测
UIDevice.current.isBatteryMonitoringEnabled = true
// 返回数据
const res : GetBatteryInfoSuccess = {
errMsg: "getBatteryInfo:ok",
level: Math.abs(Number(UIDevice.current.batteryLevel * 100)),
isCharging: UIDevice.current.batteryState == UIDevice.BatteryState.charging,
};
options.success?.(res);
options.complete?.(res);
}
/**
* 同步获取电量
*/
export const getBatteryInfoSync : GetBatteryInfoSync = function () : GetBatteryInfoResult {
// 开启电量检测
UIDevice.current.isBatteryMonitoringEnabled = true
// 返回数据
const res : GetBatteryInfoResult = {
level: Math.abs(Number(UIDevice.current.batteryLevel * 100)),
isCharging: UIDevice.current.batteryState == UIDevice.BatteryState.charging,
};
return res;
}

View File

@@ -0,0 +1,43 @@
declare namespace UniNamespace {
interface GetBatteryInfoSuccessCallbackResult {
/**
* 是否正在充电中
*/
isCharging: boolean;
/**
* 设备电量,范围 1 - 100
*/
level: number;
errMsg: string;
}
interface GetBatteryInfoOption {
/**
* 接口调用结束的回调函数(调用成功、失败都会执行)
*/
complete?: Function
/**
* 接口调用失败的回调函数
*/
fail?: Function
/**
* 接口调用成功的回调函数
*/
success?: (result: GetBatteryInfoSuccessCallbackResult) => void
}
}
declare interface Uni {
/**
* 获取设备电量
*
* @tutorial https://uniapp.dcloud.net.cn/api/system/batteryInfo.html
*/
getBatteryInfo(option?: UniNamespace.GetBatteryInfoOption): void;
/**
* 同步获取电池电量信息
* @tutorial https://uniapp.dcloud.net.cn/api/system/batteryInfo.html
*/
getBatteryInfoSync(): UniNamespace.GetBatteryInfoSuccessCallbackResult;
}

View File

@@ -0,0 +1,138 @@
export type GetBatteryInfoSuccess = {
errMsg : string,
/**
* 设备电量范围1 - 100
*/
level : number,
/**
* 是否正在充电中
*/
isCharging : boolean
}
export type GetBatteryInfoOptions = {
/**
* 接口调用结束的回调函数(调用成功、失败都会执行)
*/
success ?: (res : GetBatteryInfoSuccess) => void
/**
* 接口调用失败的回调函数
*/
fail ?: (res : UniError) => void
/**
* 接口调用成功的回调
*/
complete ?: (res : any) => void
}
export type GetBatteryInfoResult = {
/**
* 设备电量范围1 - 100
*/
level : number,
/**
* 是否正在充电中
*/
isCharging : boolean
}
/**
* 错误码
* - 1001 getAppContext is null
*/
export type GetBatteryInfoErrorCode = 1001 ;
/**
* GetBatteryInfo 的错误回调参数
*/
export interface GetBatteryInfoFail extends IUniError {
errCode : GetBatteryInfoErrorCode
};
/**
* 获取电量信息
* @param {GetBatteryInfoOptions} options
*
*
* @tutorial https://uniapp.dcloud.net.cn/api/system/batteryInfo.html
* @platforms APP-IOS = ^9.0,APP-ANDROID = ^22
* @since 3.6.11
*
* @assert () => success({errCode: 0, errSubject: "uni-getBatteryInfo", errMsg: "getBatteryInfo:ok", level: 60, isCharging: false })
* @assert () => fail({errCode: 1001, errSubject: "uni-getBatteryInfo", errMsg: "getBatteryInfo:fail getAppContext is null" })
*/
export type GetBatteryInfo = (options : GetBatteryInfoOptions) => void
export type GetBatteryInfoSync = () => GetBatteryInfoResult
interface Uni {
/**
* 获取电池电量信息
* @description 获取电池电量信息
* @param {GetBatteryInfoOptions} options
* @example
* ```typescript
* uni.getBatteryInfo({
* success(res) {
* __f__('log','at uni_modules/uni-getbatteryinfo/utssdk/interface.uts:78',res);
* }
* })
* ```
* @remark
* - 该接口需要同步调用
* @uniPlatform {
* "app": {
* "android": {
* "osVer": "4.4.4",
* "uniVer": "3.6.11",
* "unixVer": "3.9.0"
* },
* "ios": {
* "osVer": "12.0",
* "uniVer": "3.6.11",
* "unixVer": "4.11"
* }
* },
* "web": {
* "uniVer": "3.6.11",
* "unixVer": "4.0"
* }
* }
* @uniVueVersion 2,3 //支持的vue版本
*
*/
getBatteryInfo (options : GetBatteryInfoOptions) : void,
/**
* 同步获取电池电量信息
* @description 获取电池电量信息
* @example
* ```typescript
* uni.getBatteryInfo()
* ```
* @remark
* - 该接口需要同步调用
* @uniPlatform {
* "app": {
* "android": {
* "osVer": "4.4.4",
* "uniVer": "3.6.11",
* "unixVer": "3.9.0"
* },
* "ios": {
* "osVer": "12.0",
* "uniVer": "3.6.11",
* "unixVer": "4.11"
* }
* },
* "web": {
* "uniVer": "3.6.11",
* "unixVer": "4.0"
* }
* }
* @uniVueVersion 2,3 //支持的vue版本
*
*/
getBatteryInfoSync():GetBatteryInfoResult
}

View File

@@ -0,0 +1,34 @@
import { GetBatteryInfoErrorCode, GetBatteryInfoFail } from "./interface.uts"
/**
* 错误主题
*/
export const UniErrorSubject = 'uni-getBatteryInfo';
/**
* 错误信息
* @UniError
*/
export const UniErrors : Map<GetBatteryInfoErrorCode, string> = new Map([
/**
* 错误码及对应的错误信息
*/
[1001, 'getBatteryInfo:fail getAppContext is null'],
]);
/**
* 错误对象实现
*/
export class GetBatteryInfoFailImpl extends UniError implements GetBatteryInfoFail {
/**
* 错误对象构造函数
*/
constructor(errCode : GetBatteryInfoErrorCode) {
super();
this.errSubject = UniErrorSubject;
this.errCode = errCode;
this.errMsg = UniErrors[errCode] ?? "";
}
}

View File

@@ -0,0 +1,5 @@
interface Uni {
getBatteryInfo: typeof import("@/uni_modules/uni-getbatteryinfo")["getBatteryInfo"]
getBatteryInfoSync: typeof import("@/uni_modules/uni-getbatteryinfo")["getBatteryInfoSync"]
}

View File

@@ -0,0 +1,37 @@
// import { initTables } from './ak/sqlite.uts'
let firstBackTime = 0;
const __sfc__ = defineApp({
onLaunch: function () {
console.log('App Launch', " at App.uvue:10");
// initTables();
},
onShow: function () {
console.log('App Show', " at App.uvue:15");
},
onHide: function () {
console.log('App Hide', " at App.uvue:18");
},
onLastPageBackPress: function () {
console.log('App LastPageBackPress', " at App.uvue:22");
if (firstBackTime == 0) {
uni.showToast({
title: '再按一次退出应用',
position: 'bottom',
});
firstBackTime = Date.now();
setTimeout(() => {
firstBackTime = 0;
}, 2000);
}
else if (Date.now() - firstBackTime < 2000) {
firstBackTime = Date.now();
uni.exit();
}
},
onExit: function () {
console.log('App Exit', " at App.uvue:39");
},
});
export default __sfc__;
const GenAppStyles = [_uM([["uni-row", _pS(_uM([["flexDirection", "row"]]))], ["uni-column", _pS(_uM([["flexDirection", "column"]]))]])];
//# sourceMappingURL=App.uvue.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["App.uvue"],"names":[],"mappings":"AAEC,+CAA8C;AAI/C,IAAI,aAAY,GAAI,CAAA,CAAA;AACpB,MAAK,OAAQ,GAAE,SAAA,CAAA;IACb,QAAQ,EAAE;QACT,OAAO,CAAC,GAAG,CAAC,YAAY,EAAA,iBAAA,CAAA,CAAA;QACxB,gBAAe;IAEhB,CAAC;IACD,MAAM,EAAE;QACP,OAAO,CAAC,GAAG,CAAC,UAAU,EAAA,iBAAA,CAAA,CAAA;IACvB,CAAC;IACD,MAAM,EAAE;QACP,OAAO,CAAC,GAAG,CAAC,UAAU,EAAA,iBAAA,CAAA,CAAA;IACvB,CAAC;IAED,mBAAmB,EAAE;QACpB,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAA,iBAAA,CAAA,CAAA;QACnC,IAAI,aAAY,IAAK,CAAC,EAAE;YACvB,GAAG,CAAC,SAAS,CAAC;gBACb,KAAK,EAAE,UAAU;gBACjB,QAAQ,EAAE,QAAQ;aAClB,CAAA,CAAA;YACD,aAAY,GAAI,IAAI,CAAC,GAAG,EAAC,CAAA;YACzB,UAAU,CAAC,GAAG,EAAC;gBACd,aAAY,GAAI,CAAA,CAAA;YACjB,CAAC,EAAE,IAAI,CAAA,CAAA;SACR;aAAO,IAAI,IAAI,CAAC,GAAG,EAAC,GAAI,aAAY,GAAI,IAAI,EAAE;YAC7C,aAAY,GAAI,IAAI,CAAC,GAAG,EAAC,CAAA;YACzB,GAAG,CAAC,IAAI,EAAC,CAAA;SACV;IACD,CAAC;IAED,MAAM,EAAE;QACP,OAAO,CAAC,GAAG,CAAC,UAAU,EAAA,iBAAA,CAAA,CAAA;IACvB,CAAC;CACF,CAAA,CAAA","file":"App.uvue","sourceRoot":"","sourcesContent":["<script lang=\"uts\">\r\n\r\n\t// import { initTables } from './ak/sqlite.uts'\r\n\t\r\n\r\n\r\nlet firstBackTime = 0\r\nexport default {\r\n\t\tonLaunch: function () {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t\t// initTables();\r\n\r\n\t\t},\r\n\t\tonShow: function () {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function () {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t},\r\n\r\n\t\tonLastPageBackPress: function () {\r\n\t\t\tconsole.log('App LastPageBackPress')\r\n\t\t\tif (firstBackTime == 0) {\r\n\t\t\t\tuni.showToast({\r\n\t\t\t\t\ttitle: '再按一次退出应用',\r\n\t\t\t\t\tposition: 'bottom',\r\n\t\t\t\t})\r\n\t\t\t\tfirstBackTime = Date.now()\r\n\t\t\t\tsetTimeout(() => {\r\n\t\t\t\t\tfirstBackTime = 0\r\n\t\t\t\t}, 2000)\r\n\t\t\t} else if (Date.now() - firstBackTime < 2000) {\r\n\t\t\t\tfirstBackTime = Date.now()\r\n\t\t\t\tuni.exit()\r\n\t\t\t}\r\n\t\t},\r\n\r\n\t\tonExit: function () {\r\n\t\t\tconsole.log('App Exit')\r\n\t\t},\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n\t.uni-row {\r\n\t\tflex-direction: row;\r\n\t}\r\n\r\n\t.uni-column {\r\n\t\tflex-direction: column;\r\n\t}\r\n</style>"]}

View File

@@ -0,0 +1,305 @@
/**
* PermissionManager.uts
*
* Utility class for managing Android permissions throughout the app
* Handles requesting permissions, checking status, and directing users to settings
*/
/**
* Common permission types that can be requested
*/
export enum PermissionType {
BLUETOOTH = 'bluetooth',
LOCATION = 'location',
STORAGE = 'storage',
CAMERA = 'camera',
MICROPHONE = 'microphone',
NOTIFICATIONS = 'notifications',
CALENDAR = 'calendar',
CONTACTS = 'contacts',
SENSORS = 'sensors'
}
/**
* Result of a permission request
*/
type PermissionResult = {
granted: boolean;
grantedPermissions: string[];
deniedPermissions: string[];
};
/**
* Manages permission requests and checks throughout the app
*/
export class PermissionManager {
/**
* Maps permission types to the actual Android permission strings
*/
private static getPermissionsForType(type: PermissionType): string[] {
switch (type) {
case PermissionType.BLUETOOTH:
return [
'android.permission.BLUETOOTH_SCAN',
'android.permission.BLUETOOTH_CONNECT',
'android.permission.BLUETOOTH_ADVERTISE'
];
case PermissionType.LOCATION:
return [
'android.permission.ACCESS_FINE_LOCATION',
'android.permission.ACCESS_COARSE_LOCATION'
];
case PermissionType.STORAGE:
return [
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.WRITE_EXTERNAL_STORAGE'
];
case PermissionType.CAMERA:
return ['android.permission.CAMERA'];
case PermissionType.MICROPHONE:
return ['android.permission.RECORD_AUDIO'];
case PermissionType.NOTIFICATIONS:
return ['android.permission.POST_NOTIFICATIONS'];
case PermissionType.CALENDAR:
return [
'android.permission.READ_CALENDAR',
'android.permission.WRITE_CALENDAR'
];
case PermissionType.CONTACTS:
return [
'android.permission.READ_CONTACTS',
'android.permission.WRITE_CONTACTS'
];
case PermissionType.SENSORS:
return ['android.permission.BODY_SENSORS'];
default:
return [];
}
}
/**
* Get appropriate display name for a permission type
*/
private static getPermissionDisplayName(type: PermissionType): string {
switch (type) {
case PermissionType.BLUETOOTH:
return '蓝牙';
case PermissionType.LOCATION:
return '位置';
case PermissionType.STORAGE:
return '存储';
case PermissionType.CAMERA:
return '相机';
case PermissionType.MICROPHONE:
return '麦克风';
case PermissionType.NOTIFICATIONS:
return '通知';
case PermissionType.CALENDAR:
return '日历';
case PermissionType.CONTACTS:
return '联系人';
case PermissionType.SENSORS:
return '身体传感器';
default:
return '未知权限';
}
}
/**
* Check if a permission is granted
* @param type The permission type to check
* @returns True if the permission is granted, false otherwise
*/
static isPermissionGranted(type: PermissionType): boolean {
try {
const permissions = this.getPermissionsForType(type);
const activity = UTSAndroid.getUniActivity();
if (activity == null || permissions.length === 0) {
return false;
}
// Check each permission in the group
for (const permission of permissions) {
if (!UTSAndroid.checkSystemPermissionGranted(activity, [permission])) {
return false;
}
}
return true;
}
catch (e: any) {
__f__('error', 'at ak/PermissionManager.uts:132', `Error checking ${type} permission:`, e);
return false;
}
}
/**
* Request a permission from the user
* @param type The permission type to request
* @param callback Function to call with the result of the permission request
* @param showRationale Whether to show a rationale dialog if permission was previously denied
*/
static requestPermission(type: PermissionType, callback: (result: PermissionResult) => void, showRationale: boolean = true): void {
try {
const permissions = this.getPermissionsForType(type);
const activity = UTSAndroid.getUniActivity();
if (activity == null || permissions.length === 0) {
callback({
granted: false,
grantedPermissions: [],
deniedPermissions: permissions
} as PermissionResult);
return;
}
// Check if already granted
let allGranted = true;
for (const permission of permissions) {
if (!UTSAndroid.checkSystemPermissionGranted(activity, [permission])) {
allGranted = false;
break;
}
}
if (allGranted) {
callback({
granted: true,
grantedPermissions: permissions,
deniedPermissions: []
} as PermissionResult);
return;
}
// Request the permissions
UTSAndroid.requestSystemPermission(activity, permissions, (granted: boolean, grantedPermissions: string[]) => {
if (granted) {
callback({
granted: true,
grantedPermissions: grantedPermissions,
deniedPermissions: []
} as PermissionResult);
}
else if (showRationale) {
// Show rationale dialog
this.showPermissionRationale(type, callback);
}
else {
// Just report the denial
callback({
granted: false,
grantedPermissions: grantedPermissions,
deniedPermissions: this.getDeniedPermissions(permissions, grantedPermissions)
} as PermissionResult);
}
}, (denied: boolean, deniedPermissions: string[]) => {
callback({
granted: false,
grantedPermissions: this.getGrantedPermissions(permissions, deniedPermissions),
deniedPermissions: deniedPermissions
} as PermissionResult);
});
}
catch (e: any) {
__f__('error', 'at ak/PermissionManager.uts:217', `Error requesting ${type} permission:`, e);
callback({
granted: false,
grantedPermissions: [],
deniedPermissions: this.getPermissionsForType(type)
} as PermissionResult);
}
}
/**
* Show a rationale dialog explaining why the permission is needed
*/
private static showPermissionRationale(type: PermissionType, callback: (result: PermissionResult) => void): void {
const permissionName = this.getPermissionDisplayName(type);
uni.showModal({
title: '权限申请',
content: `需要${permissionName}权限才能使用相关功能`,
confirmText: '去设置',
cancelText: '取消',
success: (result) => {
if (result.confirm) {
this.openAppSettings();
callback({
granted: false,
grantedPermissions: [],
deniedPermissions: this.getPermissionsForType(type)
} as PermissionResult);
}
else {
callback({
granted: false,
grantedPermissions: [],
deniedPermissions: this.getPermissionsForType(type)
} as PermissionResult);
}
}
});
}
/**
* Open the app settings page
*/
static openAppSettings(): void {
try {
const context = UTSAndroid.getAppContext();
if (context != null) {
const intent = new android.content.Intent();
intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
const uri = android.net.Uri.fromParts("package", context.getPackageName(), null);
intent.setData(uri);
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
catch (e: any) {
__f__('error', 'at ak/PermissionManager.uts:285', 'Failed to open app settings', e);
uni.showToast({
title: '请手动前往系统设置修改应用权限',
icon: 'none',
duration: 3000
});
}
}
/**
* Helper to get the list of granted permissions
*/
private static getGrantedPermissions(allPermissions: string[], deniedPermissions: string[]): string[] {
return allPermissions.filter((p): boolean => !deniedPermissions.includes(p));
}
/**
* Helper to get the list of denied permissions
*/
private static getDeniedPermissions(allPermissions: string[], grantedPermissions: string[]): string[] {
return allPermissions.filter((p): boolean => !grantedPermissions.includes(p));
}
/**
* Request multiple permission types at once
* @param types Array of permission types to request
* @param callback Function to call when all permissions have been processed
*/
static requestMultiplePermissions(types: PermissionType[], callback: (results: Map<PermissionType, PermissionResult>) => void): void {
if (types.length === 0) {
callback(new Map());
return;
}
const results = new Map<PermissionType, PermissionResult>();
let remaining = types.length;
for (const type of types) {
this.requestPermission(type, (result) => {
results.set(type, result);
remaining--;
if (remaining === 0) {
callback(results);
}
}, true);
}
}
/**
* Convenience method to request Bluetooth permissions
* @param callback Function to call after the permission request
*/
static requestBluetoothPermissions(callback: (granted: boolean) => void): void {
this.requestPermission(PermissionType.BLUETOOTH, (result) => {
// For Bluetooth, we also need location permissions on Android
if (result.granted) {
this.requestPermission(PermissionType.LOCATION, (locationResult) => {
callback(locationResult.granted);
});
}
else {
callback(false);
}
});
}
}
//# sourceMappingURL=PermissionManager.uts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
import 'F:/HBuilderX/plugins/uniapp-cli-vite/node_modules/@dcloudio/uni-console/src/runtime/app/index.ts';
import App from './App.uvue';
import { createSSRApp } from 'vue';
export function createApp(): UTSJSONObject {
const app = createSSRApp(App);
return {
app
};
}
export function main(app: IApp) {
definePageRoutes();
defineAppConfig();
(createApp()['app'] as VueApp).mount(app, GenUniApp());
}
export class UniAppConfig extends io.dcloud.uniapp.appframe.AppConfig {
override name: string = "akbleserver";
override appid: string = "__UNI__95B2570";
override versionName: string = "1.0.1";
override versionCode: string = "101";
override uniCompilerVersion: string = "4.76";
constructor() { super(); }
}
import GenPagesAkbletestClass from './pages/akbletest.uvue';
function definePageRoutes() {
__uniRoutes.push({ path: "pages/akbletest", component: GenPagesAkbletestClass, meta: { isQuit: true } as UniPageMeta, style: _uM([["navigationBarTitleText", "akble"]]) } as UniPageRoute);
}
const __uniTabBar: Map<string, any | null> | null = null;
const __uniLaunchPage: Map<string, any | null> = _uM([["url", "pages/akbletest"], ["style", _uM([["navigationBarTitleText", "akble"]])]]);
function defineAppConfig() {
__uniConfig.entryPagePath = '/pages/akbletest';
__uniConfig.globalStyle = _uM([["navigationBarTextStyle", "black"], ["navigationBarTitleText", "体测训练"], ["navigationBarBackgroundColor", "#F8F8F8"], ["backgroundColor", "#F8F8F8"]]);
__uniConfig.getTabBarConfig = (): Map<string, any> | null => null;
__uniConfig.tabBar = __uniConfig.getTabBarConfig();
__uniConfig.conditionUrl = '';
__uniConfig.uniIdRouter = _uM();
__uniConfig.ready = true;
}
//# sourceMappingURL=main.uts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"main.uts","sourceRoot":"","sources":["main.uts"],"names":[],"mappings":"AAAA,OAAO,kGAAkG,CAAC;AAAA,OAAO,GAAG,MAAM,YAAY,CAAA;AAEtI,OAAO,EAAE,YAAY,EAAE,MAAM,KAAK,CAAA;AAClC,MAAM,UAAU,SAAS;IACxB,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;IAC7B,OAAO;QACN,GAAG;KACH,CAAA;AACF,CAAC;AACD,MAAM,UAAU,IAAI,CAAC,GAAG,EAAE,IAAI;IAC1B,gBAAgB,EAAE,CAAC;IACnB,eAAe,EAAE,CAAC;IAClB,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,OAAO,YAAa,SAAQ,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAAA;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAAA;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAA;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAAA;IACpC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAAA;IAE5C,gBAAgB,KAAK,EAAE,CAAA,CAAC,CAAC;CAC5B;AAED,OAAO,sBAAsB,MAAM,wBAAwB,CAAA;AAC3D,SAAS,gBAAgB;IACzB,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,sBAAsB,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,WAAW,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,wBAAwB,EAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,YAAY,CAAC,CAAA;AACzL,CAAC;AACD,MAAM,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAA;AACxD,MAAM,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAC,iBAAiB,CAAC,EAAC,CAAC,OAAO,EAAC,GAAG,CAAC,CAAC,CAAC,wBAAwB,EAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACrI,SAAS,eAAe;IACtB,WAAW,CAAC,aAAa,GAAG,kBAAkB,CAAA;IAC9C,WAAW,CAAC,WAAW,GAAG,GAAG,CAAC,CAAC,CAAC,wBAAwB,EAAC,OAAO,CAAC,EAAC,CAAC,wBAAwB,EAAC,MAAM,CAAC,EAAC,CAAC,8BAA8B,EAAC,SAAS,CAAC,EAAC,CAAC,iBAAiB,EAAC,SAAS,CAAC,CAAC,CAAC,CAAA;IAC9K,WAAW,CAAC,eAAe,GAAG,IAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAE,IAAI,CAAA;IACjE,WAAW,CAAC,MAAM,GAAG,WAAW,CAAC,eAAe,EAAE,CAAA;IAClD,WAAW,CAAC,YAAY,GAAG,EAAE,CAAA;IAC7B,WAAW,CAAC,WAAW,GAAG,GAAG,EAAE,CAAA;IAE/B,WAAW,CAAC,KAAK,GAAG,IAAI,CAAA;AAC1B,CAAC","sourcesContent":["import 'F:/HBuilderX/plugins/uniapp-cli-vite/node_modules/@dcloudio/uni-console/src/runtime/app/index.ts';import App from './App.uvue'\r\n\r\nimport { createSSRApp } from 'vue'\r\nexport function createApp() {\r\n\tconst app = createSSRApp(App)\r\n\treturn {\r\n\t\tapp\r\n\t}\r\n}\nexport function main(app: IApp) {\n definePageRoutes();\n defineAppConfig();\n (createApp()['app'] as VueApp).mount(app, GenUniApp());\n}\n\nexport class UniAppConfig extends io.dcloud.uniapp.appframe.AppConfig {\n override name: string = \"akbleserver\"\n override appid: string = \"__UNI__95B2570\"\n override versionName: string = \"1.0.1\"\n override versionCode: string = \"101\"\n override uniCompilerVersion: string = \"4.76\"\n \n constructor() { super() }\n}\n\nimport GenPagesAkbletestClass from './pages/akbletest.uvue'\nfunction definePageRoutes() {\n__uniRoutes.push({ path: \"pages/akbletest\", component: GenPagesAkbletestClass, meta: { isQuit: true } as UniPageMeta, style: _uM([[\"navigationBarTitleText\",\"akble\"]]) } as UniPageRoute)\n}\nconst __uniTabBar: Map<string, any | null> | null = null\nconst __uniLaunchPage: Map<string, any | null> = _uM([[\"url\",\"pages/akbletest\"],[\"style\",_uM([[\"navigationBarTitleText\",\"akble\"]])]])\nfunction defineAppConfig(){\n __uniConfig.entryPagePath = '/pages/akbletest'\n __uniConfig.globalStyle = _uM([[\"navigationBarTextStyle\",\"black\"],[\"navigationBarTitleText\",\"体测训练\"],[\"navigationBarBackgroundColor\",\"#F8F8F8\"],[\"backgroundColor\",\"#F8F8F8\"]])\n __uniConfig.getTabBarConfig = ():Map<string, any> | null => null\n __uniConfig.tabBar = __uniConfig.getTabBarConfig()\n __uniConfig.conditionUrl = ''\n __uniConfig.uniIdRouter = _uM()\n \n __uniConfig.ready = true\n}\n"]}

View 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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,276 @@
import type { BleDevice, BleConnectionState, BleEvent, BleEventCallback, BleEventPayload, BleScanResult, BleConnectOptionsExt, AutoBleInterfaces, BleDataPayload, SendDataPayload, BleOptions, MultiProtocolDevice, ScanHandler, BleProtocolType, ScanDevicesOptions } from '../interface.uts';
import { ProtocolHandler } from '../protocol_handler.uts';
import { BluetoothService } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
// Shape used when callers register plain objects as handlers. Using a named
// type keeps member access explicit so the code generator emits valid Kotlin
// member references instead of trying to access properties on Any.
type RawProtocolHandler = {
protocol?: BleProtocolType;
scanDevices?: (options?: ScanDevicesOptions) => Promise<void>;
connect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<void>;
disconnect?: (device: BleDevice) => Promise<void>;
sendData?: (device: BleDevice, payload?: SendDataPayload, options?: BleOptions) => Promise<void>;
autoConnect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
};
// 设备上下文
class DeviceContext {
device: BleDevice;
protocol: BleProtocolType;
state: BleConnectionState;
handler: ProtocolHandler;
constructor(device: BleDevice, protocol: BleProtocolType, handler: ProtocolHandler) {
this.device = device;
this.protocol = protocol;
this.state = 0; // DISCONNECTED
this.handler = handler;
}
}
const deviceMap = new Map<string, DeviceContext>(); // key: deviceId|protocol
// Single active protocol handler (no multi-protocol registration)
let activeProtocol: BleProtocolType = 'standard';
let activeHandler: ProtocolHandler | null = null;
// 事件监听注册表
const eventListeners = new Map<BleEvent, Set<BleEventCallback>>();
function emit(event: BleEvent, payload: BleEventPayload) {
if (event === 'connectionStateChanged') {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:57', '[AKBLE][LOG] bluetooth_manager.uts emit connectionStateChanged', payload);
}
const listeners = eventListeners.get(event);
if (listeners != null) {
listeners.forEach(cb => {
try {
cb(payload);
}
catch (e: any) { }
});
}
}
class ProtocolHandlerWrapper extends ProtocolHandler {
private _raw: RawProtocolHandler | null;
constructor(raw?: RawProtocolHandler) {
// pass a lightweight BluetoothService instance to satisfy generators
super(new BluetoothService());
this._raw = (raw != null) ? raw : null;
}
override async scanDevices(options?: ScanDevicesOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.scanDevices === 'function') {
await rawTyped.scanDevices!(options);
}
return;
}
override async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.connect === 'function') {
await rawTyped.connect!(device, options);
}
return;
}
override async disconnect(device: BleDevice): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.disconnect === 'function') {
await rawTyped.disconnect!(device);
}
return;
}
override async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.sendData === 'function') {
await rawTyped.sendData!(device, payload, options);
}
return;
}
override async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.autoConnect === 'function') {
return await rawTyped.autoConnect!(device, options);
}
return { serviceId: '', writeCharId: '', notifyCharId: '' } as AutoBleInterfaces;
}
}
// Strong runtime detector for plain object handlers (no Type Predicate)
// Note: the UTS bundler doesn't support TypeScript type predicates (x is T),
// and it doesn't accept the 'unknown' type. This returns a boolean and
// callers must cast the value to RawProtocolHandler after the function
// returns true.
function isRawProtocolHandler(x: any): boolean {
if (x == null || typeof x !== 'object')
return false;
const r = x as Record<string, unknown>;
if (typeof r['scanDevices'] === 'function')
return true;
if (typeof r['connect'] === 'function')
return true;
if (typeof r['disconnect'] === 'function')
return true;
if (typeof r['sendData'] === 'function')
return true;
if (typeof r['autoConnect'] === 'function')
return true;
if (typeof r['protocol'] === 'string')
return true;
return false;
}
export const registerProtocolHandler = (handler: any) => {
if (handler == null)
return;
// Determine protocol value defensively. Default to 'standard' when unknown.
let proto: BleProtocolType = 'standard';
if (handler instanceof ProtocolHandler) {
try {
proto = (handler as ProtocolHandler).protocol as BleProtocolType;
}
catch (e: any) { }
activeHandler = handler as ProtocolHandler;
}
else if (isRawProtocolHandler(handler)) {
try {
proto = (handler as RawProtocolHandler).protocol as BleProtocolType;
}
catch (e: any) { }
activeHandler = new ProtocolHandlerWrapper(handler as RawProtocolHandler);
(activeHandler as ProtocolHandler).protocol = proto;
}
else {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:139', '[AKBLE] registerProtocolHandler: unsupported handler type, ignoring', handler);
return;
}
activeProtocol = proto;
};
export const scanDevices = async (options?: ScanDevicesOptions): Promise<void> => {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:147', '[AKBLE] start scan', options);
// Determine which protocols to run: either user-specified or all registered
// Single active handler flow
if (activeHandler == null) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:151', '[AKBLE] no active scan handler registered');
return;
}
const handler = activeHandler as ProtocolHandler;
const scanOptions: ScanDevicesOptions = {
onDeviceFound: (device: BleDevice) => emit('deviceFound', { event: 'deviceFound', device } as BleEventPayload),
onScanFinished: () => emit('scanFinished', { event: 'scanFinished' } as BleEventPayload)
};
try {
await handler.scanDevices(scanOptions);
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:162', '[AKBLE] scanDevices handler error', e);
}
};
export const connectDevice = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<void> => {
const handler = activeHandler;
if (handler == null)
throw new Error('No protocol handler');
const device: BleDevice = { deviceId, name: '', rssi: 0 }; // 可扩展
await handler.connect(device, options);
const ctx = new DeviceContext(device, protocol, handler);
ctx.state = 2; // CONNECTED
deviceMap.set(getDeviceKey(deviceId, protocol), ctx);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:175', deviceMap);
emit('connectionStateChanged', { event: 'connectionStateChanged', device, protocol, state: 2 } as BleEventPayload);
};
export const disconnectDevice = async (deviceId: string, protocol: BleProtocolType): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null || ctx.handler == null)
return;
await ctx.handler.disconnect(ctx.device);
ctx.state = 0;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol, state: 0 } as BleEventPayload);
deviceMap.delete(getDeviceKey(deviceId, protocol));
};
export const sendData = async (payload: SendDataPayload, options?: BleOptions): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(payload.deviceId, payload.protocol));
if (ctx == null)
throw new Error('Device not connected');
// copy to local non-null variable so generator can smart-cast across awaits
const deviceCtx = ctx as DeviceContext;
if (deviceCtx.handler == null)
throw new Error('sendData not supported for this protocol');
await deviceCtx.handler.sendData(deviceCtx.device, payload, options);
emit('dataSent', { event: 'dataSent', device: deviceCtx.device, protocol: payload.protocol, data: payload.data } as BleEventPayload);
};
export const getConnectedDevices = (): MultiProtocolDevice[] => {
const result: MultiProtocolDevice[] = [];
deviceMap.forEach((ctx: DeviceContext) => {
const dev: MultiProtocolDevice = {
deviceId: ctx.device.deviceId,
name: ctx.device.name,
rssi: ctx.device.rssi,
protocol: ctx.protocol
};
result.push(dev);
});
return result;
};
export const getConnectionState = (deviceId: string, protocol: BleProtocolType): BleConnectionState => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null)
return 0;
return ctx.state;
};
export const on = (event: BleEvent, callback: BleEventCallback) => {
if (!eventListeners.has(event))
eventListeners.set(event, new Set());
eventListeners.get(event)!.add(callback);
};
export const off = (event: BleEvent, callback?: BleEventCallback) => {
if (callback == null) {
eventListeners.delete(event);
}
else {
eventListeners.get(event)?.delete(callback as BleEventCallback);
}
};
function getDeviceKey(deviceId: string, protocol: BleProtocolType): string {
return `${deviceId}|${protocol}`;
}
export const autoConnect = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> => {
const handler = activeHandler;
if (handler == null)
throw new Error('autoConnect not supported for this protocol');
const device: BleDevice = { deviceId, name: '', rssi: 0 };
// safe call - handler.autoConnect exists on ProtocolHandler
return await handler.autoConnect(device, options) as AutoBleInterfaces;
};
// Ensure there is at least one handler registered so callers can scan/connect
// without needing to import a registry module. This creates a minimal default
// ProtocolHandler backed by a BluetoothService instance.
try {
if (activeHandler == null) {
// Create a DeviceManager-backed raw handler that delegates to native code
const _dm = DeviceManager.getInstance();
const _raw: RawProtocolHandler = {
protocol: 'standard',
scanDevices: (options?: ScanDevicesOptions): Promise<void> => {
try {
const scanOptions = options != null ? options : {} as ScanDevicesOptions;
_dm.startScan(scanOptions);
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:256', '[AKBLE] DeviceManager.startScan failed', e);
}
return Promise.resolve();
},
connect: (device, options?: BleConnectOptionsExt): Promise<void> => {
return _dm.connectDevice(device.deviceId, options);
},
disconnect: (device): Promise<void> => {
return _dm.disconnectDevice(device.deviceId);
},
autoConnect: (device, options?: any): Promise<AutoBleInterfaces> => {
// DeviceManager does not provide an autoConnect helper; return default
const result: AutoBleInterfaces = { serviceId: '', writeCharId: '', notifyCharId: '' };
return Promise.resolve(result);
}
};
const _wrapper = new ProtocolHandlerWrapper(_raw);
activeHandler = _wrapper;
activeProtocol = _raw.protocol as BleProtocolType;
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:275', '[AKBLE] default protocol handler (BluetoothService-backed) registered', activeProtocol);
}
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/bluetooth_manager.uts:278', '[AKBLE] failed to register default protocol handler', e);
}
//# sourceMappingURL=bluetooth_manager.uts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,301 @@
import type { BleDevice, BleOptions, BleConnectionState, BleConnectionStateChangeCallback } from '../interface.uts';
import type { BleConnectOptionsExt } from '../interface.uts';
import type { ScanDevicesOptions } from '../interface.uts';
import Context from "android.content.Context";
import BluetoothAdapter from "android.bluetooth.BluetoothAdapter";
import BluetoothManager from "android.bluetooth.BluetoothManager";
import BluetoothDevice from "android.bluetooth.BluetoothDevice";
import BluetoothGatt from "android.bluetooth.BluetoothGatt";
import BluetoothGattCallback from "android.bluetooth.BluetoothGattCallback";
import ScanCallback from "android.bluetooth.le.ScanCallback";
import ScanResult from "android.bluetooth.le.ScanResult";
import ScanSettings from "android.bluetooth.le.ScanSettings";
import Handler from "android.os.Handler";
import Looper from "android.os.Looper";
import ContextCompat from "androidx.core.content.ContextCompat";
import PackageManager from "android.content.pm.PackageManager";
// 定义 PendingConnect 类型和实现类
interface PendingConnect {
resolve: () => void;
reject: (err?: any) => void; // Changed to make err optional
timer?: number;
}
class PendingConnectImpl implements PendingConnect {
override resolve: () => void;
override reject: (err?: any) => void; // Changed to make err optional
override timer?: number;
constructor(resolve: () => void, reject: (err?: any) => void, timer?: number) {
this.resolve = resolve;
this.reject = reject;
this.timer = timer;
}
}
// 引入全局回调管理
import { gattCallback } from './service_manager.uts';
const pendingConnects = new Map<string, PendingConnect>();
const STATE_DISCONNECTED = 0;
const STATE_CONNECTING = 1;
const STATE_CONNECTED = 2;
const STATE_DISCONNECTING = 3;
export class DeviceManager {
private static instance: DeviceManager | null = null;
private devices = new Map<string, BleDevice>();
private connectionStates = new Map<string, BleConnectionState>();
private connectionStateChangeListeners: BleConnectionStateChangeCallback[] = [];
private gattMap = new Map<string, BluetoothGatt | null>();
private scanCallback: ScanCallback | null = null;
private isScanning: boolean = false;
private constructor() { }
static getInstance(): DeviceManager {
if (DeviceManager.instance == null) {
DeviceManager.instance = new DeviceManager();
}
return DeviceManager.instance!;
}
startScan(options: ScanDevicesOptions): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:60', 'ak startscan now');
const adapter = this.getBluetoothAdapter();
if (adapter == null) {
throw new Error('未找到蓝牙适配器');
}
if (!adapter.isEnabled) {
// 尝试请求用户开启蓝牙
try {
adapter.enable(); // 直接调用,无需可选链和括号
}
catch (e: any) {
// 某些设备可能不支持 enable
}
setTimeout(() => {
if (!adapter.isEnabled) {
throw new Error('蓝牙未开启');
}
}, 1500);
throw new Error('正在开启蓝牙,请重试');
}
const foundDevices = this.devices; // 直接用全局 devices
class MyScanCallback extends ScanCallback {
private foundDevices: Map<string, BleDevice>;
private onDeviceFound: (device: BleDevice) => void;
constructor(foundDevices: Map<string, BleDevice>, onDeviceFound: (device: BleDevice) => void) {
super();
this.foundDevices = foundDevices;
this.onDeviceFound = onDeviceFound;
}
override onScanResult(callbackType: Int, result: ScanResult): void {
const device = result.getDevice();
if (device != null) {
const deviceId = device.getAddress();
let bleDevice = foundDevices.get(deviceId);
if (bleDevice == null) {
bleDevice = {
deviceId,
name: device.getName() ?? 'Unknown',
rssi: result.getRssi(),
lastSeen: Date.now()
} as BleDevice;
foundDevices.set(deviceId, bleDevice);
this.onDeviceFound(bleDevice);
}
else {
// 更新属性(已确保 bleDevice 非空)
bleDevice.rssi = result.getRssi();
bleDevice.name = device.getName() ?? bleDevice.name;
bleDevice.lastSeen = Date.now();
}
}
}
override onScanFailed(errorCode: Int): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:114', 'ak scan fail');
}
}
this.scanCallback = new MyScanCallback(foundDevices, options.onDeviceFound ?? ((_device) => { }));
const scanner = adapter.getBluetoothLeScanner();
if (scanner == null) {
throw new Error('无法获取扫描器');
}
const scanSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
scanner.startScan(null, scanSettings, this.scanCallback);
this.isScanning = true;
// 默认10秒后停止扫描
new Handler(Looper.getMainLooper()).postDelayed(() => {
if (this.isScanning && this.scanCallback != null) {
scanner.stopScan(this.scanCallback);
this.isScanning = false;
// this.devices = foundDevices;
if (options.onScanFinished != null)
options.onScanFinished?.invoke();
}
}, 40000);
}
async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:139', '[AKBLE] connectDevice called, deviceId:', deviceId, 'options:', options, 'connectionStates:');
const adapter = this.getBluetoothAdapter();
if (adapter == null) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:142', '[AKBLE] connectDevice failed: 蓝牙适配器不可用');
throw new Error('蓝牙适配器不可用');
}
const device = adapter.getRemoteDevice(deviceId);
if (device == null) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:147', '[AKBLE] connectDevice failed: 未找到设备', deviceId);
throw new Error('未找到设备');
}
this.connectionStates.set(deviceId, STATE_CONNECTING);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:151', '[AKBLE] connectDevice set STATE_CONNECTING, deviceId:', deviceId, 'connectionStates:');
this.emitConnectionStateChange(deviceId, STATE_CONNECTING);
const activity = UTSAndroid.getUniActivity();
const timeout = options?.timeout ?? 15000;
const key = `${deviceId}|connect`;
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:158', '[AKBLE] connectDevice 超时:', deviceId);
pendingConnects.delete(key);
this.connectionStates.set(deviceId, STATE_DISCONNECTED);
this.gattMap.set(deviceId, null);
this.emitConnectionStateChange(deviceId, STATE_DISCONNECTED);
reject(new Error('连接超时'));
}, timeout);
// 创建一个适配器函数来匹配类型签名
const resolveAdapter = () => {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:168', '[AKBLE] connectDevice resolveAdapter:', deviceId);
resolve(void 0);
};
const rejectAdapter = (err?: any) => {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:172', '[AKBLE] connectDevice rejectAdapter:', deviceId, err);
reject(err);
};
pendingConnects.set(key, new PendingConnectImpl(resolveAdapter, rejectAdapter, timer));
try {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:178', '[AKBLE] connectGatt 调用前:', deviceId);
const gatt = device.connectGatt(activity, false, gattCallback);
this.gattMap.set(deviceId, gatt);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:181', '[AKBLE] connectGatt 调用后:', deviceId, gatt);
}
catch (e: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:183', '[AKBLE] connectGatt 异常:', deviceId, e);
clearTimeout(timer);
pendingConnects.delete(key);
this.connectionStates.set(deviceId, STATE_DISCONNECTED);
this.gattMap.set(deviceId, null);
this.emitConnectionStateChange(deviceId, STATE_DISCONNECTED);
reject(e);
}
});
}
// 统一分发连接回调(应在 gattCallback.onConnectionStateChange 内调用)
static handleConnectionStateChange(deviceId: string, newState: number, error?: any) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:196', '[AKBLE] handleConnectionStateChange:', deviceId, 'newState:', newState, 'error:', error, 'pendingConnects:');
const key = `${deviceId}|connect`;
const cb = pendingConnects.get(key);
if (cb != null) {
// 修复 timer 的空安全问题,使用临时变量
const timerValue = cb.timer;
if (timerValue != null) {
clearTimeout(timerValue);
}
// 修复 error 处理
if (newState === STATE_CONNECTED) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:208', '[AKBLE] handleConnectionStateChange: 连接成功', deviceId);
cb.resolve();
}
else {
// 正确处理可空值
const errorToUse = error != null ? error : new Error('连接断开');
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:213', '[AKBLE] handleConnectionStateChange: 连接失败', deviceId, errorToUse);
cb.reject(errorToUse);
}
pendingConnects.delete(key);
}
else {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:218', '[AKBLE] handleConnectionStateChange: 未找到 pendingConnects', deviceId, newState);
}
}
async disconnectDevice(deviceId: string, isActive: boolean = true): Promise<void> {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:223', '[AKBLE] disconnectDevice called, deviceId:', deviceId, 'isActive:', isActive);
let gatt = this.gattMap.get(deviceId);
if (gatt != null) {
gatt.disconnect();
gatt.close();
// gatt=null;
this.gattMap.set(deviceId, null);
this.connectionStates.set(deviceId, STATE_DISCONNECTED);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:231', '[AKBLE] disconnectDevice set STATE_DISCONNECTED, deviceId:', deviceId, 'connectionStates:');
this.emitConnectionStateChange(deviceId, STATE_DISCONNECTED);
return;
}
else {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:235', '[AKBLE] disconnectDevice: gatt is null, deviceId:', deviceId);
return;
}
}
async reconnectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
let attempts = 0;
const maxAttempts = options?.maxAttempts ?? 3;
const interval = options?.interval ?? 3000;
while (attempts < maxAttempts) {
try {
await this.disconnectDevice(deviceId, false);
await this.connectDevice(deviceId, options);
return;
}
catch (e: any) {
attempts++;
if (attempts >= maxAttempts)
throw new Error('重连失败');
// 修复 setTimeout 问题,使用旧式 Promise + setTimeout 解决
await new Promise<void>((resolve, _reject) => {
setTimeout(() => {
resolve(void 0);
}, interval);
});
}
}
}
getConnectedDevices(): BleDevice[] {
// 创建一个空数组来存储结果
const result: BleDevice[] = [];
// 遍历 devices Map 并检查连接状态
this.devices.forEach((device, deviceId) => {
if (this.connectionStates.get(deviceId) === STATE_CONNECTED) {
result.push(device);
}
});
return result;
}
onConnectionStateChange(listener: BleConnectionStateChangeCallback) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:277', '[AKBLE][LOG] onConnectionStateChange 注册, 当前监听数:', this.connectionStateChangeListeners.length + 1, listener);
this.connectionStateChangeListeners.push(listener);
}
protected emitConnectionStateChange(deviceId: string, state: BleConnectionState) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:282', '[AKBLE][LOG] emitConnectionStateChange', deviceId, state, 'listeners:', this.connectionStateChangeListeners.length, 'connectionStates:', this.connectionStates);
for (const listener of this.connectionStateChangeListeners) {
try {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:285', '[AKBLE][LOG] emitConnectionStateChange 调用 listener', listener);
listener(deviceId, state);
}
catch (e: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:288', '[AKBLE][LOG] emitConnectionStateChange listener error', e);
}
}
}
getGattInstance(deviceId: string): BluetoothGatt | null {
return this.gattMap.get(deviceId) ?? null;
}
private getBluetoothAdapter(): BluetoothAdapter | null {
const context = UTSAndroid.getAppContext();
if (context == null)
return null;
const manager = context?.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager;
return manager.getAdapter();
}
/**
* 获取指定ID的设备如果存在
*/
public getDevice(deviceId: string): BleDevice | null {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/device_manager.uts:308', deviceId, this.devices);
return this.devices.get(deviceId) ?? null;
}
}
//# sourceMappingURL=device_manager.uts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,808 @@
import { BleService } from '../interface.uts';
import type { WriteCharacteristicOptions, DfuOptions, ControlParserResult } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
import { ServiceManager } from './service_manager.uts';
import BluetoothGatt from 'android.bluetooth.BluetoothGatt';
import BluetoothGattCharacteristic from 'android.bluetooth.BluetoothGattCharacteristic';
import BluetoothGattDescriptor from 'android.bluetooth.BluetoothGattDescriptor';
import UUID from 'java.util.UUID';
// 通用 Nordic DFU UUIDs (常见设备可能使用这些;如厂商自定义请替换)
const DFU_SERVICE_UUID = '0000fe59-0000-1000-8000-00805f9b34fb';
const DFU_CONTROL_POINT_UUID = '8ec90001-f315-4f60-9fb8-838830daea50';
const DFU_PACKET_UUID = '8ec90002-f315-4f60-9fb8-838830daea50';
type DfuSession = {
resolve: () => void;
reject: (err?: any) => void;
onProgress?: (p: number) => void;
onLog?: (s: string) => void;
controlParser?: (data: Uint8Array) => ControlParserResult | null;
// Nordic 专用字段
bytesSent?: number;
totalBytes?: number;
useNordic?: boolean;
// PRN (packet receipt notification) support
prn?: number;
packetsSincePrn?: number;
prnResolve?: () => void;
prnReject?: (err?: any) => void;
};
export class DfuManager {
// 会话表,用于把 control-point 通知路由到当前 DFU 流程
private sessions: Map<string, DfuSession> = new Map();
// 简化:只实现最基本的 GATT-based DFU 上传逻辑,需按设备协议调整 control point 的命令/解析
// Emit a DFU lifecycle event for a session. name should follow Nordic listener names
private _emitDfuEvent(deviceId: string, name: string, payload?: any) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:42', '[DFU][Event]', name, deviceId, payload ?? '');
const s = this.sessions.get(deviceId);
if (s == null)
return;
if (typeof s.onLog == 'function') {
try {
const logFn = s.onLog as (msg: string) => void;
logFn(`[${name}] ${payload != null ? JSON.stringify(payload) : ''}`);
}
catch (e: any) { }
}
if (name == 'onProgress' && typeof s.onProgress == 'function' && typeof payload == 'number') {
try {
s.onProgress!(payload as number);
}
catch (e: any) { }
}
}
async startDfu(deviceId: string, firmwareBytes: Uint8Array, options?: DfuOptions): Promise<void> {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:57', 'startDfu 0');
const deviceManager = DeviceManager.getInstance();
const serviceManager = ServiceManager.getInstance();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:60', 'startDfu 1');
const gatt: BluetoothGatt | null = deviceManager.getGattInstance(deviceId);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:62', 'startDfu 2');
if (gatt == null)
throw new Error('Device not connected');
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:64', '[DFU] startDfu start deviceId=', deviceId, 'firmwareBytes=', firmwareBytes != null ? firmwareBytes.length : 0, 'options=', options);
try {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:66', '[DFU] requesting high connection priority for', deviceId);
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:69', '[DFU] requestConnectionPriority failed', e);
}
// 发现服务并特征
// ensure services discovered before accessing GATT; serviceManager exposes Promise-based API
await serviceManager.getServices(deviceId, null);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:75', '[DFU] services ensured for', deviceId);
const dfuService = gatt.getService(UUID.fromString(DFU_SERVICE_UUID));
if (dfuService == null)
throw new Error('DFU service not found');
const controlChar = dfuService.getCharacteristic(UUID.fromString(DFU_CONTROL_POINT_UUID));
const packetChar = dfuService.getCharacteristic(UUID.fromString(DFU_PACKET_UUID));
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:80', '[DFU] dfuService=', dfuService != null ? dfuService.getUuid().toString() : null, 'controlChar=', controlChar != null ? controlChar.getUuid().toString() : null, 'packetChar=', packetChar != null ? packetChar.getUuid().toString() : null);
if (controlChar == null || packetChar == null)
throw new Error('DFU characteristics missing');
const packetProps = packetChar.getProperties();
const supportsWriteWithResponse = (packetProps & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0;
const supportsWriteNoResponse = (packetProps & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0;
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:85', '[DFU] packet characteristic props mask=', packetProps, 'supportsWithResponse=', supportsWriteWithResponse, 'supportsNoResponse=', supportsWriteNoResponse);
// Allow caller to request a desired MTU via options for higher throughput
const desiredMtu = (options != null && typeof options.mtu == 'number') ? options.mtu! : 247;
try {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:90', '[DFU] requesting MTU=', desiredMtu, 'for', deviceId);
await this._requestMtu(gatt, desiredMtu, 8000);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:92', '[DFU] requestMtu completed for', deviceId);
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:94', '[DFU] requestMtu failed or timed out, continue with default.', e);
}
const mtu = desiredMtu; // 假定成功或使用期望值
const chunkSize = Math.max(20, mtu - 3);
// small helper to convert a byte (possibly signed) to a two-digit hex string
const byteToHex = (b: number): string => {
const v = (b < 0) ? (b + 256) : b;
let s = v.toString(16);
if (s.length < 2)
s = '0' + s;
return s;
};
// Parameterize PRN window and timeout via options early so they are available
// for session logging. Defaults: prn = 12 packets, prnTimeoutMs = 10000 ms
let prnWindow = 0;
if (options != null && typeof options.prn == 'number') {
prnWindow = Math.max(0, Math.floor(options.prn!));
}
const prnTimeoutMs = (options != null && typeof options.prnTimeoutMs == 'number') ? Math.max(1000, Math.floor(options.prnTimeoutMs!)) : 8000;
const disablePrnOnTimeout = !(options != null && options.disablePrnOnTimeout == false);
// 订阅 control point 通知并将通知路由到会话处理器
const controlHandler = (data: Uint8Array) => {
// 交给会话处理器解析并触发事件
try {
const hexParts: string[] = [];
for (let i = 0; i < data.length; i++) {
const v = data[i] as number;
hexParts.push(byteToHex(v));
}
const hex = hexParts.join(' ');
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:126', '[DFU] control notification callback invoked for', deviceId, 'raw=', Array.from(data), 'hex=', hex);
}
catch (e: any) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:128', '[DFU] control notification callback invoked for', deviceId, 'raw=', Array.from(data));
}
this._handleControlNotification(deviceId, data);
};
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:132', '[DFU] subscribing control point for', deviceId);
await serviceManager.subscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, controlHandler);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:134', '[DFU] subscribeCharacteristic returned for', deviceId);
// 保存会话回调(用于 waitForControlEvent; 支持 Nordic 模式追踪已发送字节
this.sessions.set(deviceId, {
resolve: () => { },
reject: (err?: any) => { __f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:139', err); },
onProgress: null,
onLog: null,
controlParser: (data: Uint8Array): ControlParserResult | null => this._defaultControlParser(data),
bytesSent: 0,
totalBytes: firmwareBytes.length,
useNordic: options != null && options.useNordic == true,
prn: null,
packetsSincePrn: 0,
prnResolve: null,
prnReject: null
} as DfuSession);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:151', '[DFU] session created for', deviceId, 'totalBytes=', firmwareBytes.length);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:152', '[DFU] DFU session details:', { deviceId: deviceId, totalBytes: firmwareBytes.length, chunkSize: chunkSize, prnWindow: prnWindow, prnTimeoutMs: prnTimeoutMs });
// wire options callbacks into the session (if provided)
const sessRef = this.sessions.get(deviceId);
if (sessRef != null) {
sessRef.onProgress = (options != null && typeof options.onProgress == 'function') ? options.onProgress! : null;
sessRef.onLog = (options != null && typeof options.onLog == 'function') ? options.onLog! : null;
}
// emit initial lifecycle events (Nordic-like)
this._emitDfuEvent(deviceId, 'onDeviceConnecting', null);
// 写入固件数据(非常保守的实现:逐包写入并等待短延迟)
// --- PRN setup (optional, Nordic-style flow) ---
// Parameterize PRN window and timeout via options: options.prn, options.prnTimeoutMs
// Defaults were set earlier; build PRN payload using arithmetic to avoid
// bitwise operators which don't map cleanly to generated Kotlin.
if (prnWindow > 0) {
try {
// send Set PRN to device (format: [OP_CODE_SET_PRN, prn LSB, prn MSB])
// WARNING: Ensure your device uses the same opcode/format; change if needed.
const prnLsb = prnWindow % 256;
const prnMsb = Math.floor(prnWindow / 256) % 256;
const prnPayload = new Uint8Array([0x02, prnLsb, prnMsb]);
await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, prnPayload, null);
const sess0 = this.sessions.get(deviceId);
if (sess0 != null) {
sess0.useNordic = true;
sess0.prn = prnWindow;
sess0.packetsSincePrn = 0;
sess0.controlParser = (data: Uint8Array): ControlParserResult | null => this._nordicControlParser(data);
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:184', '[DFU] Set PRN sent (prn=', prnWindow, ') for', deviceId);
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:186', '[DFU] Set PRN failed (continuing without PRN):', e);
const sessFallback = this.sessions.get(deviceId);
if (sessFallback != null)
sessFallback.prn = 0;
}
}
else {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:191', '[DFU] PRN disabled (prnWindow=', prnWindow, ') for', deviceId);
}
// 写入固件数据(逐包写入并根据 options.waitForResponse 选择是否等待响应)
let offset = 0;
const total = firmwareBytes.length;
this._emitDfuEvent(deviceId, 'onDfuProcessStarted', null);
this._emitDfuEvent(deviceId, 'onUploadingStarted', null);
// Track outstanding write operations when using fire-and-forget mode so we can
// log and throttle if the Android stack becomes overwhelmed.
let outstandingWrites = 0;
// read tuning parameters from options in a safe, generator-friendly way
let configuredMaxOutstanding = 2;
let writeSleepMs = 0;
let writeRetryDelay = 100;
let writeMaxAttempts = 12;
let writeGiveupTimeout = 60000;
let drainOutstandingTimeout = 3000;
let failureBackoffMs = 0;
// throughput measurement
let throughputWindowBytes = 0;
let lastThroughputTime = Date.now();
function _logThroughputIfNeeded(force?: boolean) {
try {
const now = Date.now();
const elapsed = now - lastThroughputTime;
if (force == true || elapsed >= 1000) {
const bytes = throughputWindowBytes;
const bps = Math.floor((bytes * 1000) / Math.max(1, elapsed));
// reset window
throughputWindowBytes = 0;
lastThroughputTime = now;
const human = `${bps} B/s`;
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:225', '[DFU] throughput:', human, 'elapsedMs=', elapsed);
const s = this.sessions.get(deviceId);
if (s != null && typeof s.onLog == 'function') {
try {
s.onLog?.invoke('[DFU] throughput: ' + human);
}
catch (e: any) { }
}
}
}
catch (e: any) { }
}
function _safeErr(e?: any): any {
try {
if (e == null)
return '';
if (typeof e == 'string')
return e as string;
try {
return JSON.stringify(e);
}
catch (e2: any) { }
try {
return (e as any).toString();
}
catch (e3: any) { }
return '';
}
catch (e4: any) {
return '';
}
}
try {
if (options != null) {
try {
if (options.maxOutstanding != null) {
const parsed = Math.floor(options.maxOutstanding as number);
if (!isNaN(parsed) && parsed > 0)
configuredMaxOutstanding = parsed;
}
}
catch (e: any) { }
try {
if (options.writeSleepMs != null) {
const parsedWs = Math.floor(options.writeSleepMs as number);
if (!isNaN(parsedWs) && parsedWs >= 0)
writeSleepMs = parsedWs;
}
}
catch (e: any) { }
try {
if (options.writeRetryDelayMs != null) {
const parsedRetry = Math.floor(options.writeRetryDelayMs as number);
if (!isNaN(parsedRetry) && parsedRetry >= 0)
writeRetryDelay = parsedRetry;
}
}
catch (e: any) { }
try {
if (options.writeMaxAttempts != null) {
const parsedAttempts = Math.floor(options.writeMaxAttempts as number);
if (!isNaN(parsedAttempts) && parsedAttempts > 0)
writeMaxAttempts = parsedAttempts;
}
}
catch (e: any) { }
try {
if (options.writeGiveupTimeoutMs != null) {
const parsedGiveupTimeout = Math.floor(options.writeGiveupTimeoutMs as number);
if (!isNaN(parsedGiveupTimeout) && parsedGiveupTimeout > 0)
writeGiveupTimeout = parsedGiveupTimeout;
}
}
catch (e: any) { }
try {
if (options.drainOutstandingTimeoutMs != null) {
const parsedDrain = Math.floor(options.drainOutstandingTimeoutMs as number);
if (!isNaN(parsedDrain) && parsedDrain >= 0)
drainOutstandingTimeout = parsedDrain;
}
}
catch (e: any) { }
}
if (configuredMaxOutstanding < 1)
configuredMaxOutstanding = 1;
if (writeSleepMs < 0)
writeSleepMs = 0;
}
catch (e: any) { }
if (supportsWriteWithResponse == false && supportsWriteNoResponse == true) {
if (configuredMaxOutstanding > 1)
configuredMaxOutstanding = 1;
if (writeSleepMs < 15)
writeSleepMs = 15;
if (writeRetryDelay < 150)
writeRetryDelay = 150;
if (writeMaxAttempts < 40)
writeMaxAttempts = 40;
if (writeGiveupTimeout < 120000)
writeGiveupTimeout = 120000;
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:291', '[DFU] packet char only supports WRITE_NO_RESPONSE; serializing writes with conservative pacing');
}
const maxOutstandingCeiling = configuredMaxOutstanding;
let adaptiveMaxOutstanding = configuredMaxOutstanding;
const minOutstandingWindow = 1;
while (offset < total) {
const end = Math.min(offset + chunkSize, total);
const slice = firmwareBytes.subarray(offset, end);
// Decide whether to wait for response per-chunk. Honor characteristic support.
// Generator-friendly: avoid 'undefined' and use explicit boolean check.
let finalWaitForResponse = true;
if (options != null) {
try {
const maybe = options.waitForResponse;
if (maybe == true && supportsWriteWithResponse == false && supportsWriteNoResponse == true) {
// caller requested response but characteristic cannot provide it; keep true to ensure we await the write promise.
finalWaitForResponse = true;
}
else if (maybe == false) {
finalWaitForResponse = false;
}
else if (maybe == true) {
finalWaitForResponse = true;
}
}
catch (e: any) {
finalWaitForResponse = true;
}
}
const writeOpts: WriteCharacteristicOptions = {
waitForResponse: finalWaitForResponse,
retryDelayMs: writeRetryDelay,
maxAttempts: writeMaxAttempts,
giveupTimeoutMs: writeGiveupTimeout,
forceWriteTypeNoResponse: finalWaitForResponse == false
};
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:324', '[DFU] writing packet chunk offset=', offset, 'len=', slice.length, 'waitForResponse=', finalWaitForResponse, 'outstanding=', outstandingWrites);
// Fire-and-forget path: do not await the write if waitForResponse == false.
if (finalWaitForResponse == false) {
if (failureBackoffMs > 0) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:329', '[DFU] applying failure backoff', failureBackoffMs, 'ms before next write for', deviceId);
await this._sleep(failureBackoffMs);
failureBackoffMs = Math.floor(failureBackoffMs / 2);
}
while (outstandingWrites >= adaptiveMaxOutstanding) {
await this._sleep(Math.max(1, writeSleepMs));
}
// increment outstanding counter and kick the write without awaiting.
outstandingWrites = outstandingWrites + 1;
// fire-and-forget: start the write but don't await its Promise
const writeOffset = offset;
serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_PACKET_UUID, slice, writeOpts).then((res) => {
outstandingWrites = Math.max(0, outstandingWrites - 1);
if (res == true) {
if (adaptiveMaxOutstanding < maxOutstandingCeiling) {
adaptiveMaxOutstanding = Math.min(maxOutstandingCeiling, adaptiveMaxOutstanding + 1);
}
if (failureBackoffMs > 0)
failureBackoffMs = Math.floor(failureBackoffMs / 2);
}
// log occasional completions
if ((outstandingWrites & 0x1f) == 0) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:350', '[DFU] write completion callback, outstandingWrites=', outstandingWrites, 'adaptiveWindow=', adaptiveMaxOutstanding, 'device=', deviceId);
}
// detect write failure signaled by service manager
if (res !== true) {
adaptiveMaxOutstanding = Math.max(minOutstandingWindow, Math.floor(adaptiveMaxOutstanding / 2));
failureBackoffMs = Math.min(200, Math.max(failureBackoffMs, Math.max(5, writeRetryDelay)));
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:356', '[DFU] writeCharacteristic returned false for device=', deviceId, 'offset=', writeOffset, 'adaptiveWindow now=', adaptiveMaxOutstanding);
try {
this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: writeOffset, reason: 'write returned false' });
}
catch (e: any) { }
}
}).catch((e) => {
outstandingWrites = Math.max(0, outstandingWrites - 1);
adaptiveMaxOutstanding = Math.max(minOutstandingWindow, Math.floor(adaptiveMaxOutstanding / 2));
failureBackoffMs = Math.min(200, Math.max(failureBackoffMs, Math.max(5, writeRetryDelay)));
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:363', '[DFU] fire-and-forget write failed for device=', deviceId, e, 'adaptiveWindow now=', adaptiveMaxOutstanding);
try {
const errMsg = '[DFU] fire-and-forget write failed for device=';
this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: writeOffset, reason: errMsg });
}
catch (e2: any) { }
});
// account bytes for throughput
throughputWindowBytes += slice.length;
_logThroughputIfNeeded(false);
}
else {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:373', '[DFU] awaiting write for chunk offset=', offset);
try {
const writeResult = await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_PACKET_UUID, slice, writeOpts);
if (writeResult !== true) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:377', '[DFU] writeCharacteristic(await) returned false at offset=', offset, 'device=', deviceId);
try {
this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: offset, reason: 'write returned false' });
}
catch (e: any) { }
// abort DFU by throwing
throw new Error('write failed');
}
}
catch (e: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:383', '[DFU] awaiting write failed at offset=', offset, 'device=', deviceId, e);
try {
const errMsg = '[DFU] awaiting write failed ';
this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: offset, reason: errMsg });
}
catch (e2: any) { }
throw e;
}
// account bytes for throughput
throughputWindowBytes += slice.length;
_logThroughputIfNeeded(false);
}
// update PRN counters and wait when window reached
const sessAfter = this.sessions.get(deviceId);
if (sessAfter != null && sessAfter.useNordic == true && typeof sessAfter.prn == 'number' && (sessAfter.prn ?? 0) > 0) {
sessAfter.packetsSincePrn = (sessAfter.packetsSincePrn ?? 0) + 1;
if ((sessAfter.packetsSincePrn ?? 0) >= (sessAfter.prn ?? 0) && (sessAfter.prn ?? 0) > 0) {
// wait for PRN (device notification) before continuing
try {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:401', '[DFU] reached PRN window, waiting for PRN for', deviceId, 'packetsSincePrn=', sessAfter.packetsSincePrn, 'prn=', sessAfter.prn);
await this._waitForPrn(deviceId, prnTimeoutMs);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:403', '[DFU] PRN received, resuming transfer for', deviceId);
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:405', '[DFU] PRN wait failed/timed out, continuing anyway for', deviceId, e);
if (disablePrnOnTimeout) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:407', '[DFU] disabling PRN waits after timeout for', deviceId);
sessAfter.prn = 0;
}
}
// reset counter
sessAfter.packetsSincePrn = 0;
}
}
offset = end;
// 如果启用 nordic 模式,统计已发送字节
const sess = this.sessions.get(deviceId);
if (sess != null && typeof sess.bytesSent == 'number') {
sess.bytesSent = (sess.bytesSent ?? 0) + slice.length;
}
// 简单节流与日志,避免过快。默认睡眠非常短以提高吞吐量; 可在设备上调节
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:422', '[DFU] wrote chunk for', deviceId, 'offset=', offset, '/', total, 'chunkSize=', slice.length, 'bytesSent=', sess != null ? sess.bytesSent : null, 'outstanding=', outstandingWrites);
// emit upload progress event (percent) if available
if (sess != null && typeof sess.bytesSent == 'number' && typeof sess.totalBytes == 'number') {
const p = Math.floor((sess.bytesSent! / sess.totalBytes!) * 100);
this._emitDfuEvent(deviceId, 'onProgress', p);
}
// yield to event loop and avoid starving the Android BLE stack
await this._sleep(Math.max(0, writeSleepMs));
}
// wait for outstanding writes to drain before continuing with control commands
if (outstandingWrites > 0) {
const drainStart = Date.now();
while (outstandingWrites > 0 && (Date.now() - drainStart) < drainOutstandingTimeout) {
await this._sleep(Math.max(0, writeSleepMs));
}
if (outstandingWrites > 0) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:438', '[DFU] outstandingWrites remain after drain timeout, continuing with', outstandingWrites);
}
else {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:440', '[DFU] outstandingWrites drained before control phase');
}
}
this._emitDfuEvent(deviceId, 'onUploadingCompleted', null);
// force final throughput log before activate/validate
_logThroughputIfNeeded(true);
// 发送 activate/validate 命令到 control point需根据设备协议实现
// 下面为占位:请替换为实际的 opcode
// 在写入前先启动控制结果等待,防止设备快速响应导致丢失通知
const controlTimeout = 20000;
const controlResultPromise = this._waitForControlResult(deviceId, controlTimeout);
try {
// control writes: pass undefined options explicitly to satisfy the generator/typechecker
const activatePayload = new Uint8Array([0x04]);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:456', '[DFU] sending activate/validate payload=', Array.from(activatePayload));
await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, activatePayload, null);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:458', '[DFU] activate/validate write returned for', deviceId);
}
catch (e: any) {
// 写入失败时取消控制等待,避免悬挂定时器
try {
const sessOnWriteFail = this.sessions.get(deviceId);
if (sessOnWriteFail != null && typeof sessOnWriteFail.reject == 'function') {
sessOnWriteFail.reject(e);
}
}
catch (rejectErr: any) { }
await controlResultPromise.catch(() => { });
try {
await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID);
}
catch (e2: any) { }
this.sessions.delete(deviceId);
throw e;
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:472', '[DFU] sent control activate/validate command to control point for', deviceId);
this._emitDfuEvent(deviceId, 'onValidating', null);
// 等待 control-point 返回最终结果(成功或失败),超时可配置
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:476', '[DFU] waiting for control result (timeout=', controlTimeout, ') for', deviceId);
try {
await controlResultPromise;
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:479', '[DFU] control result resolved for', deviceId);
}
catch (err: any) {
// 清理订阅后抛出
try {
await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID);
}
catch (e: any) { }
this.sessions.delete(deviceId);
throw err;
}
// 取消订阅
try {
await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID);
}
catch (e: any) { }
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:491', '[DFU] unsubscribed control point for', deviceId);
// 清理会话
this.sessions.delete(deviceId);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:495', '[DFU] session cleared for', deviceId);
return;
}
async _requestMtu(gatt: BluetoothGatt, mtu: number, timeoutMs: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
// 在当前项目BluetoothGattCallback.onMtuChanged 未被封装;简单发起请求并等待短超时
try {
const ok = gatt.requestMtu(Math.floor(mtu) as Int);
if (!ok) {
return reject(new Error('requestMtu failed'));
}
}
catch (e: any) {
return reject(e);
}
// 无 callback 监听时退回,等待一小段时间以便成功
setTimeout(() => resolve(void 0), Math.min(2000, timeoutMs));
});
}
_sleep(ms: number): Promise<void> {
return new Promise<void>((r, _reject) => { setTimeout(() => { r(void 0); }, ms); });
}
_waitForPrn(deviceId: string, timeoutMs: number): Promise<void> {
const session = this.sessions.get(deviceId);
if (session == null)
return Promise.reject(new Error('no dfu session'));
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
// timeout waiting for PRN
// clear pending handlers
session.prnResolve = null;
session.prnReject = null;
reject(new Error('PRN timeout'));
}, timeoutMs);
const prnResolve = () => {
clearTimeout(timer);
resolve(void 0);
};
const prnReject = (err?: any) => {
clearTimeout(timer);
reject(err);
};
session.prnResolve = prnResolve;
session.prnReject = prnReject;
});
}
// 默认 control point 解析器(非常通用的尝试解析:如果设备发送 progress byte 或成功码)
_defaultControlParser(data: Uint8Array): ControlParserResult | null {
// 假设协议:第一个字节为 opcode, 第二字节可为状态或进度
if (data == null || data.length === 0)
return null;
const op = data[0];
// Nordic-style response: [0x10, requestOp, resultCode]
if (op === 0x10 && data.length >= 3) {
const requestOp = data[1];
const resultCode = data[2];
if (resultCode === 0x01) {
return { type: 'success' } as ControlParserResult;
}
return { type: 'error', error: { requestOp: requestOp, resultCode: resultCode, raw: Array.from(data) } } as ControlParserResult;
}
// Nordic PRN notification: [0x11, LSB, MSB] -> return as progress (bytes received)
if (op === 0x11 && data.length >= 3) {
const lsb = data[1];
const msb = data[2];
const received = (msb << 8) | lsb;
return { type: 'progress', progress: received } as ControlParserResult;
}
// vendor-specific opcode example: 0x60 may mean 'response/progress' for some firmwares
if (op === 0x60) {
if (data.length >= 3) {
const requestOp = data[1];
const status = data[2];
// Known success/status codes observed in field devices
if (status === 0x00 || status === 0x01 || status === 0x0A) {
return { type: 'success' } as ControlParserResult;
}
return { type: 'error', error: { requestOp: requestOp, resultCode: status, raw: Array.from(data) } } as ControlParserResult;
}
if (data.length >= 2) {
return { type: 'progress', progress: data[1] } as ControlParserResult;
}
}
// 通用进度回退:若第二字节位于 0-100 之间,当作百分比
if (data.length >= 2) {
const maybeProgress = data[1];
if (maybeProgress >= 0 && maybeProgress <= 100) {
return { type: 'progress', progress: maybeProgress } as ControlParserResult;
}
}
// 若找到明显的 success opcode (示例 0x01) 或 error 0xFF
if (op === 0x01)
return { type: 'success' } as ControlParserResult;
if (op === 0xFF)
return { type: 'error', error: data } as ControlParserResult;
return { type: 'info' } as ControlParserResult;
}
// Nordic DFU control-parser支持 Response and Packet Receipt Notification
_nordicControlParser(data: Uint8Array): ControlParserResult | null {
// Nordic opcodes (简化):
// - 0x10 : Response (opcode, requestOp, resultCode)
// - 0x11 : Packet Receipt Notification (opcode, value LSB, value MSB)
if (data == null || data.length == 0)
return null;
const op = data[0];
if (op == 0x11 && data.length >= 3) {
// packet receipt notif: bytes received (little endian)
const lsb = data[1];
const msb = data[2];
const received = (msb << 8) | lsb;
// Return received bytes as progress value; parser does not resolve device-specific session here.
return { type: 'progress', progress: received } as ControlParserResult;
}
// Nordic vendor-specific progress/response opcode (example 0x60)
if (op == 0x60) {
if (data.length >= 3) {
const requestOp = data[1];
const status = data[2];
if (status == 0x00 || status == 0x01 || status == 0x0A) {
return { type: 'success' } as ControlParserResult;
}
return { type: 'error', error: { requestOp, resultCode: status, raw: Array.from(data) } } as ControlParserResult;
}
if (data.length >= 2) {
return { type: 'progress', progress: data[1] } as ControlParserResult;
}
}
// Response: check result code for success (0x01 may indicate success in some stacks)
if (op == 0x10 && data.length >= 3) {
const requestOp = data[1];
const resultCode = data[2];
// Nordic resultCode 0x01 = SUCCESS typically
if (resultCode == 0x01)
return { type: 'success' } as ControlParserResult;
else
return { type: 'error', error: { requestOp, resultCode } } as ControlParserResult;
}
return null;
}
_handleControlNotification(deviceId: string, data: Uint8Array) {
const session = this.sessions.get(deviceId);
if (session == null) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:636', '[DFU] control notification received but no session for', deviceId, 'data=', Array.from(data));
return;
}
try {
// human readable opcode mapping
let opcodeName = 'unknown';
switch (data[0]) {
case 0x10:
opcodeName = 'Response';
break;
case 0x11:
opcodeName = 'PRN';
break;
case 0x60:
opcodeName = 'VendorProgress';
break;
case 0x01:
opcodeName = 'SuccessOpcode';
break;
case 0xFF:
opcodeName = 'ErrorOpcode';
break;
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:649', '[DFU] _handleControlNotification deviceId=', deviceId, 'opcode=0x' + data[0].toString(16), 'name=', opcodeName, 'raw=', Array.from(data));
const parsed = session.controlParser != null ? session.controlParser!(data) : null;
if (session.onLog != null)
session.onLog!('DFU control notify: ' + Array.from(data).join(','));
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:652', '[DFU] parsed control result=', parsed);
if (parsed == null)
return;
if (parsed.type == 'progress' && parsed.progress != null) {
// 如果在 nordic 模式 parsed.progress 可能是已接收字节数,则转换为百分比
if (session.useNordic == true && session.totalBytes != null && session.totalBytes! > 0) {
const percent = Math.floor((parsed.progress! / session.totalBytes!) * 100);
session.onProgress?.(percent);
// If we have written all bytes locally, log that event
if (session.bytesSent != null && session.totalBytes != null && session.bytesSent! >= session.totalBytes!) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:661', '[DFU] all bytes written locally for', deviceId, 'bytesSent=', session.bytesSent, 'total=', session.totalBytes);
// emit uploading completed once
this._emitDfuEvent(deviceId, 'onUploadingCompleted', null);
}
// If a PRN wait is pending, resolve it (PRN indicates device received packets)
if (typeof session.prnResolve == 'function') {
try {
session.prnResolve!();
}
catch (e: any) { }
session.prnResolve = null;
session.prnReject = null;
session.packetsSincePrn = 0;
}
}
else {
const progress = parsed.progress!;
if (progress != null) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:675', '[DFU] progress for', deviceId, 'progress=', progress);
session.onProgress?.(progress);
// also resolve PRN if was waiting (in case device reports numeric progress)
if (typeof session.prnResolve == 'function') {
try {
session.prnResolve!();
}
catch (e: any) { }
session.prnResolve = null;
session.prnReject = null;
session.packetsSincePrn = 0;
}
}
}
}
else if (parsed.type == 'success') {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:687', '[DFU] parsed success for', deviceId, 'resolving session');
session.resolve();
// Log final device-acknowledged success
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:690', '[DFU] device reported DFU success for', deviceId);
this._emitDfuEvent(deviceId, 'onDfuCompleted', null);
}
else if (parsed.type == 'error') {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:693', '[DFU] parsed error for', deviceId, parsed.error);
session.reject(parsed.error ?? new Error('DFU device error'));
this._emitDfuEvent(deviceId, 'onError', parsed.error ?? {});
}
else {
// info - just log
}
}
catch (e: any) {
session.onLog?.('control parse error: ' + e);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:701', '[DFU] control parse exception for', deviceId, e);
}
}
_waitForControlResult(deviceId: string, timeoutMs: number): Promise<void> {
const session = this.sessions.get(deviceId);
if (session == null)
return Promise.reject(new Error('no dfu session'));
return new Promise<void>((resolve, reject) => {
// wrap resolve/reject to clear timer
const timer = setTimeout(() => {
// 超时
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:712', '[DFU] _waitForControlResult timeout for', deviceId);
reject(new Error('DFU control timeout'));
}, timeoutMs);
const origResolve = () => {
clearTimeout(timer);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:717', '[DFU] _waitForControlResult resolved for', deviceId);
resolve(void 0);
};
const origReject = (err?: any) => {
clearTimeout(timer);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/dfu_manager.uts:722', '[DFU] _waitForControlResult rejected for', deviceId, 'err=', err);
reject(err);
};
// replace session handlers temporarily (guard nullable)
if (session != null) {
session.resolve = origResolve;
session.reject = origReject;
}
});
}
}
export const dfuManager = new DfuManager();
//# sourceMappingURL=dfu_manager.uts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,113 @@
import * as BluetoothManager from './bluetooth_manager.uts';
import { ServiceManager } from './service_manager.uts';
import type { ScanDevicesOptions, BleConnectOptionsExt, MultiProtocolDevice, BleEvent, BleEventCallback, BleService, BleCharacteristic, WriteCharacteristicOptions, AutoBleInterfaces, BleDataReceivedCallback } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
const serviceManager = ServiceManager.getInstance();
export class BluetoothService {
scanDevices(options?: ScanDevicesOptions): Promise<void> { return BluetoothManager.scanDevices(options); }
connectDevice(deviceId: string, protocol: string, options?: BleConnectOptionsExt): Promise<void> { return BluetoothManager.connectDevice(deviceId, protocol, options); }
disconnectDevice(deviceId: string, protocol: string): Promise<void> { return BluetoothManager.disconnectDevice(deviceId, protocol); }
getConnectedDevices(): MultiProtocolDevice[] { return BluetoothManager.getConnectedDevices(); }
on(event: BleEvent, callback: BleEventCallback) { return BluetoothManager.on(event, callback); }
off(event: BleEvent, callback?: BleEventCallback) { return BluetoothManager.off(event, callback); }
getServices(deviceId: string): Promise<BleService[]> {
return new Promise((resolve, reject) => {
serviceManager.getServices(deviceId, (list, err) => {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:18', 'getServices:', list, err); // 新增日志
if (err != null)
reject(err);
else
resolve((list as BleService[]) ?? []);
});
});
}
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> {
return new Promise((resolve, reject) => {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:26', deviceId, serviceId);
serviceManager.getCharacteristics(deviceId, serviceId, (list, err) => {
if (err != null)
reject(err);
else
resolve((list as BleCharacteristic[]) ?? []);
});
});
}
/**
* 自动发现服务和特征返回可用的写入和通知特征ID
* @param deviceId 设备ID
* @returns {Promise<AutoBleInterfaces>}
*/
async getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
// 1. 获取服务列表
const services = await this.getServices(deviceId);
if (services == null || services.length == 0)
throw new Error('未发现服务');
// 2. 选择目标服务优先bae前缀可根据需要调整
let serviceId = '';
for (let i = 0; i < services.length; i++) {
const s = services[i];
const uuidCandidate: string | null = (s.uuid != null ? s.uuid : null);
const uuid: string = uuidCandidate != null ? uuidCandidate : '';
// prefer regex test to avoid nullable receiver calls in generated Kotlin
if (/^bae/i.test(uuid)) {
serviceId = uuid;
break;
}
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:55', serviceId);
if (serviceId == null || serviceId == '')
serviceId = services[0].uuid;
// 3. 获取特征列表
const characteristics = await this.getCharacteristics(deviceId, serviceId);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:60', characteristics);
if (characteristics == null || characteristics.length == 0)
throw new Error('未发现特征值');
// 4. 筛选write和notify特征
let writeCharId = '';
let notifyCharId = '';
for (let i = 0; i < characteristics.length; i++) {
const c = characteristics[i];
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:69', c);
if ((writeCharId == null || writeCharId == '') && c.properties != null && (c.properties.write || c.properties.writeWithoutResponse == true))
writeCharId = c.uuid;
if ((notifyCharId == null || notifyCharId == '') && c.properties != null && (c.properties.notify || c.properties.indicate))
notifyCharId = c.uuid;
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:73', serviceId, writeCharId, notifyCharId);
if ((writeCharId == null || writeCharId == '') || (notifyCharId == null || notifyCharId == ''))
throw new Error('未找到合适的写入或通知特征');
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:75', serviceId, writeCharId, notifyCharId);
// // 发现服务和特征后
const deviceManager = DeviceManager.getInstance();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:78', deviceManager);
const device = deviceManager.getDevice(deviceId);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:80', deviceId, device);
device!.serviceId = serviceId;
device!.writeCharId = writeCharId;
device!.notifyCharId = notifyCharId;
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/index.uts:84', device);
return { serviceId, writeCharId, notifyCharId } as AutoBleInterfaces;
}
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, onData);
}
async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
return serviceManager.readCharacteristic(deviceId, serviceId, characteristicId);
}
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, data: Uint8Array, options?: WriteCharacteristicOptions): Promise<boolean> {
return serviceManager.writeCharacteristic(deviceId, serviceId, characteristicId, data, options);
}
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
return serviceManager.unsubscribeCharacteristic(deviceId, serviceId, characteristicId);
}
async autoDiscoverAll(deviceId: string): Promise<any> {
return serviceManager.autoDiscoverAll(deviceId);
}
async subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeAllNotifications(deviceId, onData);
}
}
export const bluetoothService = new BluetoothService();
// Ensure protocol handlers are registered when this module is imported.
// import './protocol_registry.uts';
//# sourceMappingURL=index.uts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,711 @@
import type { BleService, BleCharacteristic, BleDataReceivedCallback, BleCharacteristicProperties, WriteCharacteristicOptions, ByteArray } from '../interface.uts';
import BluetoothGatt from "android.bluetooth.BluetoothGatt";
import BluetoothGattService from "android.bluetooth.BluetoothGattService";
import BluetoothGattCharacteristic from "android.bluetooth.BluetoothGattCharacteristic";
import BluetoothGattDescriptor from "android.bluetooth.BluetoothGattDescriptor";
import BluetoothGattCallback from "android.bluetooth.BluetoothGattCallback";
import UUID from "java.util.UUID";
import { DeviceManager } from './device_manager.uts';
import { AkBleErrorImpl, AkBluetoothErrorCode } from '../unierror.uts';
import { AutoDiscoverAllResult } from '../interface.uts';
// 补全UUID格式将短格式转换为标准格式
function getFullUuid(shortUuid: string): string {
return `0000${shortUuid}-0000-1000-8000-00805f9b34fb`;
}
const deviceWriteQueues = new Map<string, Promise<void>>();
function enqueueDeviceWrite<T>(deviceId: string, work: () => Promise<T>): Promise<T> {
const previous = deviceWriteQueues.get(deviceId) ?? Promise.resolve();
const next = (async (): Promise<T> => {
try {
await previous;
}
catch (e: any) { /* ignore previous rejection to keep queue alive */ }
return await work();
})();
const queued = next.then(() => { }, () => { });
deviceWriteQueues.set(deviceId, queued);
return next.finally(() => {
if (deviceWriteQueues.get(deviceId) == queued) {
deviceWriteQueues.delete(deviceId);
}
});
}
function createCharProperties(props: number): BleCharacteristicProperties {
const result: BleCharacteristicProperties = {
read: false,
write: false,
notify: false,
indicate: false,
canRead: false,
canWrite: false,
canNotify: false,
writeWithoutResponse: false
};
result.read = (props & BluetoothGattCharacteristic.PROPERTY_READ) !== 0;
result.write = (props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0;
result.notify = (props & BluetoothGattCharacteristic.PROPERTY_NOTIFY) !== 0;
result.indicate = (props & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0;
result.writeWithoutResponse = (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0;
result.canRead = result.read;
const writeWithoutResponse = result.writeWithoutResponse!;
result.canWrite = (result.write != null && result.write) || (writeWithoutResponse != null && writeWithoutResponse);
result.canNotify = result.notify;
return result;
}
// 定义 PendingCallback 类型和实现类
interface PendingCallback {
resolve: (data: any) => void;
reject: (err?: any) => void;
timer?: number; // Changed from any to number
}
class PendingCallbackImpl implements PendingCallback {
override resolve: (data: any) => void;
override reject: (err?: any) => void;
override timer?: number; // Changed from any to number
constructor(resolve: (data: any) => void, reject: (err?: any) => void, timer?: number) {
this.resolve = resolve;
this.reject = reject;
this.timer = timer;
}
}
// 全局回调管理(必须在类外部声明)
let pendingCallbacks: Map<string, PendingCallback>;
let notifyCallbacks: Map<string, BleDataReceivedCallback>;
// 在全局范围内初始化
pendingCallbacks = new Map<string, PendingCallback>();
notifyCallbacks = new Map<string, BleDataReceivedCallback>();
// 服务发现等待队列deviceId -> 回调数组
const serviceDiscoveryWaiters = new Map<string, ((services: BleService[] | null, error?: Error) => void)[]>();
// 服务发现状态deviceId -> 是否已发现
const serviceDiscovered = new Map<string, boolean>();
// 特征发现等待队列deviceId|serviceId -> 回调数组
const characteristicDiscoveryWaiters = new Map<string, ((characteristics: BleCharacteristic[] | null, error?: Error) => void)[]>();
class GattCallback extends BluetoothGattCallback {
constructor() {
super();
}
override onServicesDiscovered(gatt: BluetoothGatt, status: Int): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:108', 'ak onServicesDiscovered');
const deviceId = gatt.getDevice().getAddress();
if (status == BluetoothGatt.GATT_SUCCESS) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:111', `服务发现成功: ${deviceId}`);
serviceDiscovered.set(deviceId, true);
// 统一回调所有等待 getServices 的调用
const waiters = serviceDiscoveryWaiters.get(deviceId);
if (waiters != null && waiters.length > 0) {
const services = gatt.getServices();
const result: BleService[] = [];
if (services != null) {
const servicesList = services;
const size = servicesList.size;
for (let i = 0; i < size; i++) {
const service = servicesList.get(i as Int);
if (service != null) {
const bleService: BleService = {
uuid: service.getUuid().toString(),
isPrimary: service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY
};
result.push(bleService);
}
}
}
for (let i = 0; i < waiters.length; i++) {
const cb = waiters[i];
if (cb != null) {
cb(result, null);
}
}
serviceDiscoveryWaiters.delete(deviceId);
}
}
else {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:139', `服务发现失败: ${deviceId}, status: ${status}`);
// 失败时也要通知等待队列
const waiters = serviceDiscoveryWaiters.get(deviceId);
if (waiters != null && waiters.length > 0) {
for (let i = 0; i < waiters.length; i++) {
const cb = waiters[i];
if (cb != null) {
cb(null, new Error('服务发现失败'));
}
}
serviceDiscoveryWaiters.delete(deviceId);
}
}
}
override onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int): void {
const deviceId = gatt.getDevice().getAddress();
if (newState == BluetoothGatt.STATE_CONNECTED) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:154', `设备已连接: ${deviceId}`);
DeviceManager.handleConnectionStateChange(deviceId, 2, null); // 2 = STATE_CONNECTED
}
else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:157', `设备已断开: ${deviceId}`);
serviceDiscovered.delete(deviceId);
DeviceManager.handleConnectionStateChange(deviceId, 0, null); // 0 = STATE_DISCONNECTED
}
}
override onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:163', 'ak onCharacteristicChanged');
const deviceId = gatt.getDevice().getAddress();
const serviceId = characteristic.getService().getUuid().toString();
const charId = characteristic.getUuid().toString();
const key = `${deviceId}|${serviceId}|${charId}|notify`;
const callback = notifyCallbacks.get(key);
const value = characteristic.getValue();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:170', '[onCharacteristicChanged]', key, value);
if (callback != null && value != null) {
const valueLength = value.size;
const arr = new Uint8Array(valueLength);
for (let i = 0 as Int; i < valueLength; i++) {
const v = value[i as Int];
arr[i] = v != null ? v : 0;
}
// 保存接收日志
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:179', `
INSERT INTO ble_data_log (device_id, service_id, char_id, direction, data, timestamp)
VALUES ('${deviceId}', '${serviceId}', '${charId}', 'recv', '${Array.from(arr).join(',')}', ${Date.now()})
`);
callback(arr);
}
}
override onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:188', 'ak onCharacteristicRead', status);
const deviceId = gatt.getDevice().getAddress();
const serviceId = characteristic.getService().getUuid().toString();
const charId = characteristic.getUuid().toString();
const key = `${deviceId}|${serviceId}|${charId}|read`;
const pending = pendingCallbacks.get(key);
const value = characteristic.getValue();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:195', '[onCharacteristicRead]', key, 'status=', status, 'value=', value);
if (pending != null) {
try {
const timer = pending.timer;
if (timer != null) {
clearTimeout(timer);
pending.timer = null;
}
pendingCallbacks.delete(key);
if (status == BluetoothGatt.GATT_SUCCESS && value != null) {
const valueLength = value.size;
const arr = new Uint8Array(valueLength);
for (let i = 0 as Int; i < valueLength; i++) {
const v = value[i as Int];
arr[i] = v != null ? v : 0;
}
// resolve with ArrayBuffer
pending.resolve(arr.buffer as ArrayBuffer);
}
else {
pending.reject(new Error('Characteristic read failed'));
}
}
catch (e: any) {
try {
pending.reject(e);
}
catch (e2: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:218', e2);
}
}
}
}
override onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:224', 'ak onCharacteristicWrite', status);
const deviceId = gatt.getDevice().getAddress();
const serviceId = characteristic.getService().getUuid().toString();
const charId = characteristic.getUuid().toString();
const key = `${deviceId}|${serviceId}|${charId}|write`;
const pending = pendingCallbacks.get(key);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:230', '[onCharacteristicWrite]', key, 'status=', status);
if (pending != null) {
try {
const timer = pending.timer;
if (timer != null) {
clearTimeout(timer);
}
pendingCallbacks.delete(key);
if (status == BluetoothGatt.GATT_SUCCESS) {
pending.resolve('ok');
}
else {
pending.reject(new Error('Characteristic write failed'));
}
}
catch (e: any) {
try {
pending.reject(e);
}
catch (e2: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:244', e2);
}
}
}
}
}
// 导出单例实例供外部使用
export const gattCallback = new GattCallback();
export class ServiceManager {
private static instance: ServiceManager | null = null;
private services = new Map<string, BleService[]>();
private characteristics = new Map<string, Map<string, BleCharacteristic[]>>();
private deviceManager = DeviceManager.getInstance();
private constructor() { }
static getInstance(): ServiceManager {
if (ServiceManager.instance == null) {
ServiceManager.instance = new ServiceManager();
}
return ServiceManager.instance!;
}
getServices(deviceId: string, callback?: (services: BleService[] | null, error?: Error) => void): any | Promise<BleService[]> {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:267', 'ak start getservice', deviceId);
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) {
if (callback != null) {
callback(null, new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", ""));
}
return Promise.reject(new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", ""));
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:273', 'ak serviceDiscovered', gatt);
// 如果服务已发现,直接返回
if (serviceDiscovered.get(deviceId) == true) {
const services = gatt.getServices();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:277', services);
const result: BleService[] = [];
if (services != null) {
const servicesList = services;
const size = servicesList.size;
if (size > 0) {
for (let i = 0 as Int; i < size; i++) {
const service = servicesList != null ? servicesList.get(i) : servicesList[i];
if (service != null) {
const bleService: BleService = {
uuid: service.getUuid().toString(),
isPrimary: service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY
};
result.push(bleService);
if (bleService.uuid == getFullUuid('0001')) {
const device = this.deviceManager.getDevice(deviceId);
if (device != null) {
device.serviceId = bleService.uuid;
this.getCharacteristics(deviceId, device.serviceId!, (chars, err) => {
if (err == null && chars != null) {
const writeChar = chars.find((c): boolean => c.uuid == getFullUuid('0010'));
const notifyChar = chars.find((c): boolean => c.uuid == getFullUuid('0011'));
if (writeChar != null)
device.writeCharId = writeChar.uuid;
if (notifyChar != null)
device.notifyCharId = notifyChar.uuid;
}
});
}
}
}
}
}
}
if (callback != null) {
callback(result, null);
}
return Promise.resolve(result);
}
// 未发现则发起服务发现并加入等待队列
if (!serviceDiscoveryWaiters.has(deviceId)) {
serviceDiscoveryWaiters.set(deviceId, []);
gatt.discoverServices();
}
return new Promise<BleService[]>((resolve, reject) => {
const cb = (services: BleService[] | null, error?: Error) => {
if (error != null)
reject(error);
else
resolve(services ?? []);
if (callback != null)
callback(services, error);
};
const arr = serviceDiscoveryWaiters.get(deviceId);
if (arr != null)
arr.push(cb);
});
}
getCharacteristics(deviceId: string, serviceId: string, callback: (characteristics: BleCharacteristic[] | null, error?: Error) => void): void {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null)
return callback(null, new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", ""));
// 如果服务还没发现,等待服务发现后再查特征
if (serviceDiscovered.get(deviceId) !== true) {
// 先注册到服务发现等待队列
this.getServices(deviceId, (services, err) => {
if (err != null) {
callback(null, err);
}
else {
this.getCharacteristics(deviceId, serviceId, callback);
}
});
return;
}
// 服务已发现,正常获取特征
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null)
return callback(null, new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", ""));
const chars = service.getCharacteristics();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:347', chars);
const result: BleCharacteristic[] = [];
if (chars != null) {
const characteristicsList = chars;
const size = characteristicsList.size;
const bleService: BleService = {
uuid: serviceId,
isPrimary: service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY
};
for (let i = 0 as Int; i < size; i++) {
const char = characteristicsList != null ? characteristicsList.get(i as Int) : characteristicsList[i];
if (char != null) {
const props = char.getProperties();
try {
const charUuid = char.getUuid() != null ? char.getUuid().toString() : '';
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:362', '[ServiceManager] characteristic uuid=', charUuid);
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:363', '[ServiceManager] failed to read char uuid', e);
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:364', props);
const bleCharacteristic: BleCharacteristic = {
uuid: char.getUuid().toString(),
service: bleService,
properties: createCharProperties(props)
};
result.push(bleCharacteristic);
}
}
}
callback(result, null);
}
public async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
const key = `${deviceId}|${serviceId}|${characteristicId}|read`;
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:385', key);
return new Promise<ArrayBuffer>((resolve, reject) => {
const timer = setTimeout(() => {
pendingCallbacks.delete(key);
reject(new AkBleErrorImpl(AkBluetoothErrorCode.ConnectionTimeout, "Connection timeout", ""));
}, 5000);
const resolveAdapter = (data: any) => { __f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:391', 'read resolve:', data); resolve(data as ArrayBuffer); };
const rejectAdapter = (err?: any) => { reject(new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Unknown error occurred", "")); };
pendingCallbacks.set(key, new PendingCallbackImpl(resolveAdapter, rejectAdapter, timer));
if (gatt.readCharacteristic(char) == false) {
clearTimeout(timer);
pendingCallbacks.delete(key);
reject(new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Unknown error occurred", ""));
}
else {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:400', 'read should be succeed', key);
}
});
}
public async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, data: Uint8Array, options?: WriteCharacteristicOptions): Promise<boolean> {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:406', '[writeCharacteristic] deviceId:', deviceId, 'serviceId:', serviceId, 'characteristicId:', characteristicId, 'data:', data);
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:409', '[writeCharacteristic] gatt is null');
throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
}
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:414', '[writeCharacteristic] service is null');
throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
}
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:419', '[writeCharacteristic] characteristic is null');
throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
}
const key = `${deviceId}|${serviceId}|${characteristicId}|write`;
const wantsNoResponse = options != null && options.waitForResponse == false;
let retryMaxAttempts = 20;
let retryDelay = 100;
let giveupTimeout = 20000;
if (options != null) {
try {
if (options.maxAttempts != null) {
const parsedAttempts = Math.floor(options.maxAttempts as number);
if (!isNaN(parsedAttempts) && parsedAttempts > 0)
retryMaxAttempts = parsedAttempts;
}
}
catch (e: any) { }
try {
if (options.retryDelayMs != null) {
const parsedDelay = Math.floor(options.retryDelayMs as number);
if (!isNaN(parsedDelay) && parsedDelay >= 0)
retryDelay = parsedDelay;
}
}
catch (e: any) { }
try {
if (options.giveupTimeoutMs != null) {
const parsedGiveup = Math.floor(options.giveupTimeoutMs as number);
if (!isNaN(parsedGiveup) && parsedGiveup > 0)
giveupTimeout = parsedGiveup;
}
}
catch (e: any) { }
}
const gattInstance = gatt;
const executeWrite = (): Promise<boolean> => {
return new Promise<boolean>((resolve, _reject) => {
const initialTimeout = Math.max(giveupTimeout + 5000, 10000);
let timer = setTimeout(() => {
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:454', '[writeCharacteristic] timeout');
resolve(false);
}, initialTimeout);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:457', '[writeCharacteristic] initial timeout set to', initialTimeout, 'ms for', key);
const resolveAdapter = (data: any) => {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:459', '[writeCharacteristic] resolveAdapter called');
resolve(true);
};
const rejectAdapter = (err?: any) => {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:463', '[writeCharacteristic] rejectAdapter called', err);
resolve(false);
};
pendingCallbacks.set(key, new PendingCallbackImpl(resolveAdapter, rejectAdapter, timer));
const byteArray = new ByteArray(data.length as Int);
for (let i = 0 as Int; i < data.length; i++) {
byteArray[i] = data[i].toByte();
}
const forceWriteTypeNoResponse = options != null && options.forceWriteTypeNoResponse == true;
let usesNoResponse = forceWriteTypeNoResponse || wantsNoResponse;
try {
const props = char.getProperties();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:475', '[writeCharacteristic] characteristic properties mask=', props);
if (usesNoResponse == false) {
const supportsWriteWithResponse = (props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0;
const supportsWriteNoResponse = (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0;
if (supportsWriteWithResponse == false && supportsWriteNoResponse == true) {
usesNoResponse = true;
}
}
if (usesNoResponse) {
try {
char.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
}
catch (e: any) { }
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:485', '[writeCharacteristic] using WRITE_TYPE_NO_RESPONSE');
}
else {
try {
char.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
}
catch (e: any) { }
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:488', '[writeCharacteristic] using WRITE_TYPE_DEFAULT');
}
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:491', '[writeCharacteristic] failed to inspect/set write type', e);
}
const maxAttempts = retryMaxAttempts;
function attemptWrite(att: Int): void {
try {
let setOk = true;
try {
const setRes = char.setValue(byteArray);
if (typeof setRes == 'boolean' && setRes == false) {
setOk = false;
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:501', '[writeCharacteristic] setValue returned false for', key, 'attempt', att);
}
}
catch (e: any) {
setOk = false;
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:505', '[writeCharacteristic] setValue threw for', key, 'attempt', att, e);
}
if (setOk == false) {
if (att >= maxAttempts) {
try {
clearTimeout(timer);
}
catch (e: any) { }
pendingCallbacks.delete(key);
resolve(false);
return;
}
setTimeout(() => { attemptWrite((att + 1) as Int); }, retryDelay);
return;
}
try {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:518', '[writeCharacteristic] attempt', att, 'calling gatt.writeCharacteristic');
const r = gattInstance.writeCharacteristic(char);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:520', '[writeCharacteristic] attempt', att, 'result=', r);
if (r == true) {
if (usesNoResponse) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:523', '[writeCharacteristic] WRITE_TYPE_NO_RESPONSE success for', key);
try {
clearTimeout(timer);
}
catch (e: any) { }
pendingCallbacks.delete(key);
resolve(true);
return;
}
try {
clearTimeout(timer);
}
catch (e: any) { }
const extra = 20000;
timer = setTimeout(() => {
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:533', '[writeCharacteristic] timeout after write initiated');
resolve(false);
}, extra);
const pendingEntry = pendingCallbacks.get(key);
if (pendingEntry != null)
pendingEntry.timer = timer;
return;
}
}
catch (e: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:541', '[writeCharacteristic] attempt', att, 'exception when calling writeCharacteristic', e);
}
if (att < maxAttempts) {
const nextAtt = (att + 1) as Int;
setTimeout(() => { attemptWrite(nextAtt); }, retryDelay);
return;
}
if (usesNoResponse) {
try {
clearTimeout(timer);
}
catch (e: any) { }
pendingCallbacks.delete(key);
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:551', '[writeCharacteristic] all attempts failed with WRITE_NO_RESPONSE for', key);
resolve(false);
return;
}
try {
clearTimeout(timer);
}
catch (e: any) { }
const giveupTimeoutLocal = giveupTimeout;
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:557', '[writeCharacteristic] all attempts failed; waiting for late callback up to', giveupTimeoutLocal, 'ms for', key);
const giveupTimer = setTimeout(() => {
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:560', '[writeCharacteristic] giveup timeout expired for', key);
resolve(false);
}, giveupTimeoutLocal);
const pendingEntryAfter = pendingCallbacks.get(key);
if (pendingEntryAfter != null)
pendingEntryAfter.timer = giveupTimer;
}
catch (e: any) {
clearTimeout(timer);
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:568', '[writeCharacteristic] Exception in attemptWrite', e);
resolve(false);
}
}
try {
attemptWrite(1 as Int);
}
catch (e: any) {
clearTimeout(timer);
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:578', '[writeCharacteristic] Exception before attempting write', e);
resolve(false);
}
});
};
return enqueueDeviceWrite(deviceId, executeWrite);
}
public async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
const key = `${deviceId}|${serviceId}|${characteristicId}|notify`;
notifyCallbacks.set(key, onData);
if (gatt.setCharacteristicNotification(char, true) == false) {
notifyCallbacks.delete(key);
throw new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Failed to unsubscribe characteristic", "");
}
else {
// 写入 CCCD 描述符,启用 notify
const descriptor = char.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (descriptor != null) {
// 设置描述符值
const value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
descriptor.setValue(value);
const writedescript = gatt.writeDescriptor(descriptor);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:608', 'subscribeCharacteristic: CCCD written for notify', writedescript);
}
else {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:610', 'subscribeCharacteristic: CCCD descriptor not found!');
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:612', 'subscribeCharacteristic ok!!');
}
}
public async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
const key = `${deviceId}|${serviceId}|${characteristicId}|notify`;
notifyCallbacks.delete(key);
if (gatt.setCharacteristicNotification(char, false) == false) {
throw new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Failed to unsubscribe characteristic", "");
}
}
// 自动发现所有服务和特征
public async autoDiscoverAll(deviceId: string): Promise<AutoDiscoverAllResult> {
const services = await this.getServices(deviceId, null) as BleService[];
const allCharacteristics: BleCharacteristic[] = [];
for (const service of services) {
await new Promise<void>((resolve, reject) => {
this.getCharacteristics(deviceId, service.uuid, (chars, err) => {
if (err != null)
reject(err);
else {
if (chars != null)
allCharacteristics.push(...chars);
resolve(void 0);
}
});
});
}
return { services, characteristics: allCharacteristics } as AutoDiscoverAllResult;
}
// 自动订阅所有支持 notify/indicate 的特征
public async subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
const { services, characteristics } = await this.autoDiscoverAll(deviceId);
for (const char of characteristics) {
if (char.properties.notify || char.properties.indicate) {
try {
await this.subscribeCharacteristic(deviceId, char.service.uuid, char.uuid, onData);
}
catch (e: any) {
// 可以选择忽略单个特征订阅失败
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:658', `订阅特征 ${char.uuid} 失败:`, e);
}
}
}
}
}
//# sourceMappingURL=service_manager.uts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,409 @@
// 蓝牙相关接口和类型定义
// 基础设备信息类型
export type BleDeviceInfo = {
deviceId: string;
name: string;
RSSI?: number;
connected?: boolean;
// 新增
serviceId?: string;
writeCharId?: string;
notifyCharId?: string;
};
export type AutoDiscoverAllResult = {
services: BleService[];
characteristics: BleCharacteristic[];
};
// 服务信息类型
export type BleServiceInfo = {
uuid: string;
isPrimary: boolean;
};
// 特征值属性类型
export type BleCharacteristicProperties = {
read: boolean;
write: boolean;
notify: boolean;
indicate: boolean;
writeWithoutResponse?: boolean;
canRead?: boolean;
canWrite?: boolean;
canNotify?: boolean;
};
// 特征值信息类型
export type BleCharacteristicInfo = {
uuid: string;
serviceId: string;
properties: BleCharacteristicProperties;
};
// 错误状态码
export enum BleErrorCode {
UNKNOWN_ERROR = 0,
BLUETOOTH_UNAVAILABLE = 1,
PERMISSION_DENIED = 2,
DEVICE_NOT_CONNECTED = 3,
SERVICE_NOT_FOUND = 4,
CHARACTERISTIC_NOT_FOUND = 5,
OPERATION_TIMEOUT = 6
}
// 命令类型
export enum CommandType {
BATTERY = 1,
DEVICE_INFO = 2,
CUSTOM = 99,
TestBatteryLevel = 0x01
}
// 错误接口
export type BleError = {
errCode: number;
errMsg: string;
errSubject?: string;
};
// 连接选项
export type BleConnectOptions = {
deviceId: string;
timeout?: number;
success?: (result: any) => void;
fail?: (error: BleError) => void;
complete?: (result: any) => void;
};
// 断开连接选项
export type BleDisconnectOptions = {
deviceId: string;
success?: (result: any) => void;
fail?: (error: BleError) => void;
complete?: (result: any) => void;
};
// 获取特征值选项
export type BleCharacteristicOptions = {
deviceId: string;
serviceId: string;
characteristicId: string;
success?: (result: any) => void;
fail?: (error: BleError) => void;
complete?: (result: any) => void;
};
// 写入特征值选项
export type BleWriteOptions = {
deviceId: string;
serviceId: string;
characteristicId: string;
value: Uint8Array;
writeType?: number;
success?: (result: any) => void;
fail?: (error: BleError) => void;
complete?: (result: any) => void;
};
// Options for writeCharacteristic helper
export type WriteCharacteristicOptions = {
waitForResponse?: boolean;
maxAttempts?: number;
retryDelayMs?: number;
giveupTimeoutMs?: number;
forceWriteTypeNoResponse?: boolean;
};
// 通知特征值回调函数
export type BleNotifyCallback = (data: Uint8Array) => void;
// 通知特征值选项
export type BleNotifyOptions = {
deviceId: string;
serviceId: string;
characteristicId: string;
state?: boolean; // true: 启用通知false: 禁用通知
onCharacteristicValueChange: BleNotifyCallback;
success?: (result: any) => void;
fail?: (error: BleError) => void;
complete?: (result: any) => void;
};
// 获取服务选项
export type BleDeviceServicesOptions = {
deviceId: string;
success?: (result: BleServicesResult) => void;
fail?: (error: BleError) => void;
complete?: (result: any) => void;
};
// 获取特征值选项
export type BleDeviceCharacteristicsOptions = {
deviceId: string;
serviceId: string;
success?: (result: BleCharacteristicsResult) => void;
fail?: (error: BleError) => void;
complete?: (result: any) => void;
};
// 蓝牙扫描选项
export type BluetoothScanOptions = {
services?: string[];
timeout?: number;
onDeviceFound?: (device: BleDeviceInfo) => void;
success?: (result: BleScanResult) => void;
fail?: (error: BleError) => void;
complete?: (result: any) => void;
};
// 扫描结果
// 服务结果
export type BleServicesResult = {
services: BleServiceInfo[];
errMsg?: string;
};
// 特征值结果
export type BleCharacteristicsResult = {
characteristics: BleCharacteristicInfo[];
errMsg?: string;
};
// 定义连接状态枚举
export enum BLE_CONNECTION_STATE {
DISCONNECTED = 0,
CONNECTING = 1,
CONNECTED = 2,
DISCONNECTING = 3
}
// 电池状态类型定义
export type BatteryStatus = {
batteryLevel: number; // 电量百分比
isCharging: boolean; // 充电状态
};
// 蓝牙服务接口类型定义 - 转换为type类型
export type BleService = {
uuid: string;
isPrimary: boolean;
};
// 蓝牙特征值接口定义 - 转换为type类型
export type BleCharacteristic = {
uuid: string;
service: BleService;
properties: BleCharacteristicProperties;
};
// PendingPromise接口定义
export interface PendingCallback {
resolve: (data: any) => void;
reject: (err?: any) => void;
timer?: number;
}
// 蓝牙相关接口和类型定义
export type BleDevice = {
deviceId: string;
name: string;
rssi?: number;
lastSeen?: number; // 新增
// 新增
serviceId?: string;
writeCharId?: string;
notifyCharId?: string;
};
// BLE常规选项
export type BleOptions = {
timeout?: number;
success?: (result: any) => void;
fail?: (error: any) => void;
complete?: () => void;
};
export type BleConnectionState = number; // 0: DISCONNECTED, 1: CONNECTING, 2: CONNECTED, 3: DISCONNECTING
export type BleConnectOptionsExt = {
timeout?: number;
services?: string[];
requireResponse?: boolean;
autoReconnect?: boolean;
maxAttempts?: number;
interval?: number;
};
// 回调函数类型
export type BleDeviceFoundCallback = (device: BleDevice) => void;
export type BleConnectionStateChangeCallback = (deviceId: string, state: BleConnectionState) => void;
export type BleDataPayload = {
deviceId: string;
serviceId?: string;
characteristicId?: string;
data: string | ArrayBuffer;
format?: number; // 0: JSON, 1: XML, 2: RAW
};
export type BleDataSentCallback = (payload: BleDataPayload, success: boolean, error?: BleError) => void;
export type BleErrorCallback = (error: BleError) => void;
// 健康数据类型定义
export enum HealthDataType {
HEART_RATE = 1,
BLOOD_OXYGEN = 2,
TEMPERATURE = 3,
STEP_COUNT = 4,
SLEEP_DATA = 5,
HEALTH_DATA = 6
}
// Platform-specific services should be imported from per-platform entrypoints
// (e.g. './app-android/index.uts' or './web/index.uts').
// Avoid re-exporting platform modules at the SDK root to prevent bundlers
// Platform-specific services should be imported from per-platform entrypoints
// (e.g. './app-android/index.uts' or './web/index.uts').
// Avoid re-exporting platform modules at the SDK root to prevent bundlers
// from pulling android.* symbols into web bundles.
// If a typed ambient reference is required, declare the shape here instead of importing implementation.
// Example lightweight typed placeholder (do not import platform code here):
// export type BluetoothService = any; // platform-specific implementation exported from platform index files
// ====== 新增多协议、统一事件、协议适配、状态管理支持 ======
export type BleProtocolType = 'standard' | 'custom' | 'health' | 'ibeacon' | 'mesh';
export type BleEvent = 'deviceFound' | 'scanFinished' | 'connectionStateChanged' | 'dataReceived' | 'dataSent' | 'error' | 'servicesDiscovered' | 'connected' // 新增
| 'disconnected'; // 新增
// 事件回调参数
export type BleEventPayload = {
event: BleEvent;
device?: BleDevice;
protocol?: BleProtocolType;
state?: BleConnectionState;
data?: ArrayBuffer | string | object;
format?: string;
error?: BleError;
extra?: any;
};
// 事件回调函数
export type BleEventCallback = (payload: BleEventPayload) => void;
// 多协议设备信息(去除交叉类型,直接展开字段)
export type MultiProtocolDevice = {
deviceId: string;
name: string;
rssi?: number;
protocol: BleProtocolType;
};
export type ScanDevicesOptions = {
protocols?: BleProtocolType[];
optionalServices?: string[];
timeout?: number;
onDeviceFound?: (device: BleDevice) => void;
onScanFinished?: () => void;
};
// Named payload type used by sendData
export type SendDataPayload = {
deviceId: string;
serviceId?: string;
characteristicId?: string;
data: string | ArrayBuffer;
format?: number;
protocol: BleProtocolType;
};
// 协议处理器接口(为 protocol-handler 适配器预留)
export type ScanHandler = {
protocol: BleProtocolType;
scanDevices?: (options: ScanDevicesOptions) => Promise<void>;
connect: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<void>;
disconnect: (device: BleDevice) => Promise<void>;
// Optional: send arbitrary data via the protocol's write characteristic
sendData?: (device: BleDevice, payload: SendDataPayload, options?: BleOptions) => Promise<void>;
// Optional: try to connect and discover service/characteristic ids for this device
autoConnect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
};
// 自动发现服务和特征返回类型
export type AutoBleInterfaces = {
serviceId: string;
writeCharId: string;
notifyCharId: string;
};
export type ResponseCallbackEntry = {
cb: (data: Uint8Array) => boolean | void;
multi: boolean;
};
// Result returned by a DFU control parser. Use a plain string `type` to keep
// the generated Kotlin simple and avoid inline union types which the generator
// does not handle well.
export type ControlParserResult = {
type: string; // e.g. 'progress', 'success', 'error', 'info'
progress?: number;
error?: any;
};
// DFU types
export type DfuOptions = {
mtu?: number;
useNordic?: boolean;
// If true, the DFU upload will await a write response per-packet. Set false to use
// WRITE_NO_RESPONSE (fire-and-forget) for higher throughput. Default: false.
waitForResponse?: boolean;
// Maximum number of outstanding NO_RESPONSE writes to allow before throttling.
// This implements a simple sliding window. Default: 32.
maxOutstanding?: number;
// Per-chunk sleep (ms) to yield to event loop / Android BLE stack. Default: 2.
writeSleepMs?: number;
// Retry delay (ms) used by the Android write helper when gatt.writeCharacteristic
// returns false. Smaller values can improve throughput on congested stacks.
writeRetryDelayMs?: number;
// Maximum number of immediate write attempts before falling back to the give-up timeout.
writeMaxAttempts?: number;
// Timeout (ms) to wait for a late onCharacteristicWrite callback after all retries fail.
writeGiveupTimeoutMs?: number;
// Packet Receipt Notification (PRN) window size in packets. If set, DFU
// manager will send a Set PRN command to the device and wait for PRN
// notifications after this many packets. Default: 12.
prn?: number;
// Timeout (ms) to wait for a PRN notification once the window is reached.
// Default: 10000 (10s).
prnTimeoutMs?: number;
// When true, disable PRN waits automatically after the first timeout to prevent
// repeated long stalls on devices that do not send PRNs. Default: true.
disablePrnOnTimeout?: boolean;
// Time (ms) to wait for outstanding fire-and-forget writes to drain before issuing
// the activate/validate control command. Default: 3000.
drainOutstandingTimeoutMs?: number;
controlTimeout?: number;
onProgress?: (percent: number) => void;
onLog?: (message: string) => void;
controlParser?: (data: Uint8Array) => ControlParserResult | null;
};
export type DfuManagerType = {
startDfu: (deviceId: string, firmwareBytes: Uint8Array, options?: DfuOptions) => Promise<void>;
};
// Lightweight runtime / UTS shims and missing types
// These are conservative placeholders to satisfy typings used across platform files.
// UTSJSONObject: bundler environments used by the build may not support
// TypeScript-style index signatures in this .uts context. Use a conservative
// 'any' alias so generated code doesn't rely on unsupported syntax while
// preserving a usable type at the source level.
export type UTSJSONObject = any;
// ByteArray / Int are used in the Android platform code to interop with Java APIs.
// Define minimal aliases so source can compile. Runtime uses Uint8Array and number.
export type ByteArray = any; // runtime will use Java byte[] via UTS bridge; keep as any here
// Callback types used by service_manager and index wrappers
export type BleDataReceivedCallback = (data: Uint8Array) => void;
export type BleScanResult = {
deviceId: string;
name?: string;
rssi?: number;
advertising?: any;
};
// Minimal UI / framework placeholders (some files reference these in types only)
export type ComponentPublicInstance = any;
export type UniElement = any;
export type UniPage = any;
// Platform service placeholder (actual implementation exported from platform index files)
// Provide a lightweight, strongly-shaped class skeleton so source-level code
// (and the code generator) can rely on concrete method names and signatures.
// Implementations are platform-specific and exported from per-platform index
// files (e.g. './app-android/index.uts' or './web/index.uts'). This class is
// intentionally thin — it declares method signatures used across pages and
// platform shims so the generator emits resolvable Kotlin symbols.
export class BluetoothService {
// Event emitter style
on(event: BleEvent | string, callback: BleEventCallback): void { }
off(event: BleEvent | string, callback?: BleEventCallback): void { }
// Scanning / discovery
scanDevices(options?: ScanDevicesOptions): Promise<void> { return Promise.resolve(); }
// Connection management
connectDevice(deviceId: string, protocol?: string, options?: BleConnectOptionsExt): Promise<void> { return Promise.resolve(); }
disconnectDevice(deviceId: string, protocol?: string): Promise<void> { return Promise.resolve(); }
getConnectedDevices(): MultiProtocolDevice[] { return []; }
// Services / characteristics
getServices(deviceId: string): Promise<BleService[]> { return Promise.resolve([]); }
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> { return Promise.resolve([]); }
// Read / write / notify
readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> { return Promise.resolve(new ArrayBuffer(0)); }
writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> { return Promise.resolve(true); }
subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: BleNotifyCallback): Promise<void> { return Promise.resolve(); }
unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> { return Promise.resolve(); }
// Convenience helpers
getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
const res: AutoBleInterfaces = { serviceId: '', writeCharId: '', notifyCharId: '' };
return Promise.resolve(res);
}
}
// Runtime protocol handler base class. Exporting a concrete class ensures the
// generator emits a resolvable runtime type that platform handlers can extend.
// Source-level code can still use the ScanHandler type for typing.
// Runtime ProtocolHandler is implemented in `protocol_handler.uts`.
// Keep the public typing in this file minimal to avoid duplicate runtime
// declarations. Consumers that need the runtime class should import it from
// './protocol_handler.uts'.
//# sourceMappingURL=interface.uts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,127 @@
// Minimal ProtocolHandler runtime class used by pages and components.
// This class adapts the platform `BluetoothService` to a small protocol API
// expected by pages: setConnectionParameters, initialize, testBatteryLevel,
// testVersionInfo. Implemented conservatively to avoid heavy dependencies.
import type { BluetoothService, AutoBleInterfaces, AutoDiscoverAllResult, BleService, BleCharacteristic, BleProtocolType, BleDevice, ScanDevicesOptions, BleConnectOptionsExt, SendDataPayload, BleOptions } from './interface.uts';
export class ProtocolHandler {
// bluetoothService may be omitted for lightweight wrappers; allow null
bluetoothService: BluetoothService | null = null;
protocol: BleProtocolType = 'standard';
deviceId: string | null = null;
serviceId: string | null = null;
writeCharId: string | null = null;
notifyCharId: string | null = null;
initialized: boolean = false;
// Accept an optional BluetoothService so wrapper subclasses can call
// `super()` without forcing a runtime instance.
constructor(bluetoothService?: BluetoothService) {
if (bluetoothService != null)
this.bluetoothService = bluetoothService;
}
setConnectionParameters(deviceId: string, serviceId: string, writeCharId: string, notifyCharId: string) {
this.deviceId = deviceId;
this.serviceId = serviceId;
this.writeCharId = writeCharId;
this.notifyCharId = notifyCharId;
}
// initialize: optional setup, returns a Promise that resolves when ready
async initialize(): Promise<void> {
// Simple async initializer — keep implementation minimal and generator-friendly.
try {
// If bluetoothService exposes any protocol-specific setup, call it here.
this.initialized = true;
return;
}
catch (e: any) {
throw e;
}
}
// Protocol lifecycle / operations — default no-ops so generated code has
// concrete member references and platform-specific handlers can override.
async scanDevices(options?: ScanDevicesOptions): Promise<void> { return; }
async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> { return; }
async disconnect(device: BleDevice): Promise<void> { return; }
async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> { return; }
async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> { return { serviceId: '', writeCharId: '', notifyCharId: '' } as AutoBleInterfaces; }
// Example: testBatteryLevel will attempt to read the battery characteristic
// if write/notify-based protocol is not available. Returns number percentage.
async testBatteryLevel(): Promise<number> {
if (this.deviceId == null)
throw new Error('deviceId not set');
// copy to local so Kotlin generator can smart-cast the value across awaits
const deviceId = this.deviceId!;
// try reading standard Battery characteristic (180F -> 2A19)
if (this.bluetoothService == null)
throw new Error('bluetoothService not set');
const services = await this.bluetoothService!.getServices(deviceId);
let found: BleService | null = null;
for (let i = 0; i < services.length; i++) {
const s = services[i];
const uuidCandidate: string | null = (s != null && s.uuid != null ? s.uuid : null);
const uuid = uuidCandidate != null ? ('' + uuidCandidate).toLowerCase() : '';
if (uuid.indexOf('180f') !== -1) {
found = s;
break;
}
}
if (found == null) {
// fallback: if writeCharId exists and notify available use protocol (not implemented)
return 0;
}
const foundUuid = found!.uuid;
const charsRaw = await this.bluetoothService!.getCharacteristics(deviceId, foundUuid);
const chars: BleCharacteristic[] = charsRaw;
const batChar = chars.find((c: BleCharacteristic): boolean => ((c.properties != null && c.properties.read) || (c.uuid != null && ('' + c.uuid).toLowerCase().includes('2a19'))));
if (batChar == null)
return 0;
const buf = await this.bluetoothService!.readCharacteristic(deviceId, foundUuid, batChar.uuid);
const arr = new Uint8Array(buf);
if (arr.length > 0) {
return arr[0];
}
return 0;
}
// testVersionInfo: try to read Device Information characteristics or return empty
async testVersionInfo(hw: boolean): Promise<string> {
// copy to local so Kotlin generator can smart-cast the value across awaits
const deviceId = this.deviceId;
if (deviceId == null)
return '';
// Device Information service 180A, characteristics: 2A26 (SW), 2A27 (HW) sometimes
if (this.bluetoothService == null)
return '';
const _services = await this.bluetoothService!.getServices(deviceId);
const services2: BleService[] = _services;
let found2: BleService | null = null;
for (let i = 0; i < services2.length; i++) {
const s = services2[i];
const uuidCandidate: string | null = (s != null && s.uuid != null ? s.uuid : null);
const uuid = uuidCandidate != null ? ('' + uuidCandidate).toLowerCase() : '';
if (uuid.indexOf('180a') !== -1) {
found2 = s;
break;
}
}
if (found2 == null)
return '';
const _found2 = found2;
const found2Uuid = _found2!.uuid;
const chars = await this.bluetoothService!.getCharacteristics(deviceId, found2Uuid);
const target = chars.find((c): boolean => {
const id = ('' + c.uuid).toLowerCase();
if (hw)
return id.includes('2a27') || id.includes('hardware');
return id.includes('2a26') || id.includes('software');
});
if (target == null)
return '';
const buf = await this.bluetoothService!.readCharacteristic(deviceId, found2Uuid, target.uuid);
try {
return new TextDecoder().decode(new Uint8Array(buf));
}
catch (e: any) {
return '';
}
}
}
//# sourceMappingURL=protocol_handler.uts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
// Minimal error definitions used across the BLE module.
// Keep this file small and avoid runtime dependencies; it's mainly for typing and
// simple runtime error construction used by native platform code.
export enum AkBluetoothErrorCode {
UnknownError = 0,
DeviceNotFound = 1,
ServiceNotFound = 2,
CharacteristicNotFound = 3,
ConnectionTimeout = 4,
Unspecified = 99
}
export class AkBleErrorImpl extends Error {
public code: AkBluetoothErrorCode;
public detail: any | null;
constructor(code: AkBluetoothErrorCode, message?: string, detail: any | null = null) {
super(message ?? AkBleErrorImpl.defaultMessage(code));
this.name = 'AkBleError';
this.code = code;
this.detail = detail;
}
static defaultMessage(code: AkBluetoothErrorCode): string {
switch (code) {
case AkBluetoothErrorCode.DeviceNotFound: return 'Device not found';
case AkBluetoothErrorCode.ServiceNotFound: return 'Service not found';
case AkBluetoothErrorCode.CharacteristicNotFound: return 'Characteristic not found';
case AkBluetoothErrorCode.ConnectionTimeout: return 'Connection timed out';
case AkBluetoothErrorCode.UnknownError:
default: return 'Unknown Bluetooth error';
}
}
}
export default AkBleErrorImpl;
//# sourceMappingURL=unierror.uts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"unierror.uts","sourceRoot":"","sources":["uni_modules/ak-sbsrv/utssdk/unierror.uts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,kFAAkF;AAClF,kEAAkE;AAElE,MAAM,MAAM,oBAAoB;IAC/B,YAAY,GAAG,CAAC;IAChB,cAAc,GAAG,CAAC;IAClB,eAAe,GAAG,CAAC;IACnB,sBAAsB,GAAG,CAAC;IAC1B,iBAAiB,GAAG,CAAC;IACrB,WAAW,GAAG,EAAE;CAChB;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IACxC,MAAM,CAAC,IAAI,EAAE,oBAAoB,CAAC;IAClC,MAAM,CAAC,MAAM,EAAE,GAAG,GAAC,IAAI,CAAC;IACxB,YAAY,IAAI,EAAE,oBAAoB,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAC,IAAI,GAAG,IAAI;QAChF,KAAK,CAAC,OAAO,IAAI,cAAc,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;IACD,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,oBAAoB;QAC/C,QAAQ,IAAI,EAAE;YACb,KAAK,oBAAoB,CAAC,cAAc,CAAC,CAAC,OAAO,kBAAkB,CAAC;YACpE,KAAK,oBAAoB,CAAC,eAAe,CAAC,CAAC,OAAO,mBAAmB,CAAC;YACtE,KAAK,oBAAoB,CAAC,sBAAsB,CAAC,CAAC,OAAO,0BAA0B,CAAC;YACpF,KAAK,oBAAoB,CAAC,iBAAiB,CAAC,CAAC,OAAO,sBAAsB,CAAC;YAC3E,KAAK,oBAAoB,CAAC,YAAY,CAAC;YAAC,OAAO,CAAC,CAAC,OAAO,yBAAyB,CAAC;SAClF;IACF,CAAC;CACD;AAED,eAAe,cAAc,CAAC","sourcesContent":["// Minimal error definitions used across the BLE module.\r\n// Keep this file small and avoid runtime dependencies; it's mainly for typing and\r\n// simple runtime error construction used by native platform code.\r\n\r\nexport enum AkBluetoothErrorCode {\r\n\tUnknownError = 0,\r\n\tDeviceNotFound = 1,\r\n\tServiceNotFound = 2,\r\n\tCharacteristicNotFound = 3,\r\n\tConnectionTimeout = 4,\r\n\tUnspecified = 99\r\n}\r\n\r\nexport class AkBleErrorImpl extends Error {\r\n\tpublic code: AkBluetoothErrorCode;\r\n\tpublic detail: any|null;\n\tconstructor(code: AkBluetoothErrorCode, message?: string, detail: any|null = null) {\r\n\t\tsuper(message ?? AkBleErrorImpl.defaultMessage(code));\r\n\t\tthis.name = 'AkBleError';\r\n\t\tthis.code = code;\r\n\t\tthis.detail = detail;\r\n\t}\r\n\tstatic defaultMessage(code: AkBluetoothErrorCode) {\r\n\t\tswitch (code) {\r\n\t\t\tcase AkBluetoothErrorCode.DeviceNotFound: return 'Device not found';\r\n\t\t\tcase AkBluetoothErrorCode.ServiceNotFound: return 'Service not found';\r\n\t\t\tcase AkBluetoothErrorCode.CharacteristicNotFound: return 'Characteristic not found';\r\n\t\t\tcase AkBluetoothErrorCode.ConnectionTimeout: return 'Connection timed out';\r\n\t\t\tcase AkBluetoothErrorCode.UnknownError: default: return 'Unknown Bluetooth error';\r\n\t\t}\r\n\t}\r\n}\r\n\r\nexport default AkBleErrorImpl;\r\n"]}

View File

@@ -0,0 +1,62 @@
import Context from "android.content.Context";
import BatteryManager from "android.os.BatteryManager";
import { GetBatteryInfo, GetBatteryInfoOptions, GetBatteryInfoSuccess, GetBatteryInfoResult, GetBatteryInfoSync } from '../interface.uts';
import IntentFilter from 'android.content.IntentFilter';
import Intent from 'android.content.Intent';
import { GetBatteryInfoFailImpl } from '../unierror';
/**
* 异步获取电量
*/
export const getBatteryInfo: GetBatteryInfo = function (options: GetBatteryInfoOptions) {
const context = UTSAndroid.getAppContext();
if (context != null) {
const manager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager;
const level = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
let ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
let batteryStatus = context.registerReceiver(null, ifilter);
let status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
let isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
const res: GetBatteryInfoSuccess = {
errMsg: 'getBatteryInfo:ok',
level,
isCharging: isCharging
};
options.success?.(res);
options.complete?.(res);
}
else {
let res = new GetBatteryInfoFailImpl(1001);
options.fail?.(res);
options.complete?.(res);
}
};
/**
* 同步获取电量
*/
export const getBatteryInfoSync: GetBatteryInfoSync = function (): GetBatteryInfoResult {
const context = UTSAndroid.getAppContext();
if (context != null) {
const manager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager;
const level = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
let ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
let batteryStatus = context.registerReceiver(null, ifilter);
let status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
let isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
const res: GetBatteryInfoResult = {
level: level,
isCharging: isCharging
};
return res;
}
else {
/**
* 无有效上下文
*/
const res: GetBatteryInfoResult = {
level: -1,
isCharging: false
};
return res;
}
};
//# sourceMappingURL=index.uts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.uts","sourceRoot":"","sources":["uni_modules/uni-getbatteryinfo/utssdk/app-android/index.uts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,yBAAyB,CAAC;AAC9C,OAAO,cAAc,MAAM,2BAA2B,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACzI,OAAO,YAAY,MAAM,8BAA8B,CAAC;AACxD,OAAO,MAAM,MAAM,wBAAwB,CAAC;AAE5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,EAAG,cAAc,GAAG,UAAU,OAAO,EAAG,qBAAqB;IACtF,MAAM,OAAO,GAAG,UAAU,CAAC,aAAa,EAAE,CAAC;IAC3C,IAAI,OAAO,IAAI,IAAI,EAAE;QACnB,MAAM,OAAO,GAAG,OAAO,CAAC,gBAAgB,CACtC,OAAO,CAAC,eAAe,CACxB,IAAI,cAAc,CAAC;QACpB,MAAM,KAAK,GAAG,OAAO,CAAC,cAAc,CAClC,cAAc,CAAC,yBAAyB,CACzC,CAAC;QAEF,IAAI,OAAO,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAC9D,IAAI,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5D,IAAI,MAAM,GAAG,aAAa,EAAE,WAAW,CAAC,cAAc,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC;QACzE,IAAI,UAAU,GAAG,MAAM,IAAI,cAAc,CAAC,uBAAuB,IAAI,MAAM,IAAI,cAAc,CAAC,mBAAmB,CAAC;QAElH,MAAM,GAAG,EAAG,qBAAqB,GAAG;YAClC,MAAM,EAAE,mBAAmB;YAC3B,KAAK;YACL,UAAU,EAAE,UAAU;SACvB,CAAA;QACD,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAA;QACtB,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAA;KACxB;SAAM;QACL,IAAI,GAAG,GAAG,IAAI,sBAAsB,CAAC,IAAI,CAAC,CAAC;QAC3C,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAA;QACnB,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAA;KACxB;AACH,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,EAAG,kBAAkB,GAAG,aAAc,oBAAoB;IACvF,MAAM,OAAO,GAAG,UAAU,CAAC,aAAa,EAAE,CAAC;IAC3C,IAAI,OAAO,IAAI,IAAI,EAAE;QACnB,MAAM,OAAO,GAAG,OAAO,CAAC,gBAAgB,CACtC,OAAO,CAAC,eAAe,CACxB,IAAI,cAAc,CAAC;QACpB,MAAM,KAAK,GAAG,OAAO,CAAC,cAAc,CAClC,cAAc,CAAC,yBAAyB,CACzC,CAAC;QAEF,IAAI,OAAO,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAC9D,IAAI,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5D,IAAI,MAAM,GAAG,aAAa,EAAE,WAAW,CAAC,cAAc,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC;QACzE,IAAI,UAAU,GAAG,MAAM,IAAI,cAAc,CAAC,uBAAuB,IAAI,MAAM,IAAI,cAAc,CAAC,mBAAmB,CAAC;QAElH,MAAM,GAAG,EAAG,oBAAoB,GAAG;YACjC,KAAK,EAAE,KAAK;YACZ,UAAU,EAAE,UAAU;SACvB,CAAC;QACF,OAAO,GAAG,CAAC;KACZ;SACI;QACH;;WAEG;QACH,MAAM,GAAG,EAAG,oBAAoB,GAAG;YACjC,KAAK,EAAE,CAAC,CAAC;YACT,UAAU,EAAE,KAAK;SAClB,CAAC;QACF,OAAO,GAAG,CAAC;KACZ;AACH,CAAC,CAAA","sourcesContent":["import Context from \"android.content.Context\";\r\nimport BatteryManager from \"android.os.BatteryManager\";\r\n\r\nimport { GetBatteryInfo, GetBatteryInfoOptions, GetBatteryInfoSuccess, GetBatteryInfoResult, GetBatteryInfoSync } from '../interface.uts'\r\nimport IntentFilter from 'android.content.IntentFilter';\r\nimport Intent from 'android.content.Intent';\r\n\r\nimport { GetBatteryInfoFailImpl } from '../unierror';\r\n\r\n/**\r\n * 异步获取电量\r\n */\r\nexport const getBatteryInfo : GetBatteryInfo = function (options : GetBatteryInfoOptions) {\r\n const context = UTSAndroid.getAppContext();\r\n if (context != null) {\r\n const manager = context.getSystemService(\r\n Context.BATTERY_SERVICE\r\n ) as BatteryManager;\r\n const level = manager.getIntProperty(\r\n BatteryManager.BATTERY_PROPERTY_CAPACITY\r\n );\r\n\r\n let ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);\r\n let batteryStatus = context.registerReceiver(null, ifilter);\r\n let status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1);\r\n let isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;\r\n\r\n const res : GetBatteryInfoSuccess = {\r\n errMsg: 'getBatteryInfo:ok',\r\n level,\r\n isCharging: isCharging\r\n }\r\n options.success?.(res)\r\n options.complete?.(res)\r\n } else {\r\n let res = new GetBatteryInfoFailImpl(1001);\r\n options.fail?.(res)\r\n options.complete?.(res)\r\n }\r\n}\r\n\r\n/**\r\n * 同步获取电量\r\n */\r\nexport const getBatteryInfoSync : GetBatteryInfoSync = function () : GetBatteryInfoResult {\r\n const context = UTSAndroid.getAppContext();\r\n if (context != null) {\r\n const manager = context.getSystemService(\r\n Context.BATTERY_SERVICE\r\n ) as BatteryManager;\r\n const level = manager.getIntProperty(\r\n BatteryManager.BATTERY_PROPERTY_CAPACITY\r\n );\r\n\r\n let ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);\r\n let batteryStatus = context.registerReceiver(null, ifilter);\r\n let status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1);\r\n let isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;\r\n\r\n const res : GetBatteryInfoResult = {\r\n level: level,\r\n isCharging: isCharging\r\n };\r\n return res;\r\n }\r\n else {\r\n /**\r\n * 无有效上下文\r\n */\r\n const res : GetBatteryInfoResult = {\r\n level: -1,\r\n isCharging: false\r\n };\r\n return res;\r\n }\r\n}\r\n"]}

View File

@@ -0,0 +1,131 @@
export type GetBatteryInfoSuccess = {
errMsg: string;
/**
* 设备电量范围1 - 100
*/
level: number;
/**
* 是否正在充电中
*/
isCharging: boolean;
};
export type GetBatteryInfoOptions = {
/**
* 接口调用结束的回调函数(调用成功、失败都会执行)
*/
success?: (res: GetBatteryInfoSuccess) => void;
/**
* 接口调用失败的回调函数
*/
fail?: (res: UniError) => void;
/**
* 接口调用成功的回调
*/
complete?: (res: any) => void;
};
export type GetBatteryInfoResult = {
/**
* 设备电量范围1 - 100
*/
level: number;
/**
* 是否正在充电中
*/
isCharging: boolean;
};
/**
* 错误码
* - 1001 getAppContext is null
*/
export type GetBatteryInfoErrorCode = 1001;
/**
* GetBatteryInfo 的错误回调参数
*/
export interface GetBatteryInfoFail extends IUniError {
errCode: GetBatteryInfoErrorCode;
}
;
/**
* 获取电量信息
* @param {GetBatteryInfoOptions} options
*
*
* @tutorial https://uniapp.dcloud.net.cn/api/system/batteryInfo.html
* @platforms APP-IOS = ^9.0,APP-ANDROID = ^22
* @since 3.6.11
*
* @assert () => success({errCode: 0, errSubject: "uni-getBatteryInfo", errMsg: "getBatteryInfo:ok", level: 60, isCharging: false })
* @assert () => fail({errCode: 1001, errSubject: "uni-getBatteryInfo", errMsg: "getBatteryInfo:fail getAppContext is null" })
*/
export type GetBatteryInfo = (options: GetBatteryInfoOptions) => void;
export type GetBatteryInfoSync = () => GetBatteryInfoResult;
interface Uni {
/**
* 获取电池电量信息
* @description 获取电池电量信息
* @param {GetBatteryInfoOptions} options
* @example
* ```typescript
* uni.getBatteryInfo({
* success(res) {
* __f__('log','at uni_modules/uni-getbatteryinfo/utssdk/interface.uts:78',res);
* }
* })
* ```
* @remark
* - 该接口需要同步调用
* @uniPlatform {
* "app": {
* "android": {
* "osVer": "4.4.4",
* "uniVer": "3.6.11",
* "unixVer": "3.9.0"
* },
* "ios": {
* "osVer": "12.0",
* "uniVer": "3.6.11",
* "unixVer": "4.11"
* }
* },
* "web": {
* "uniVer": "3.6.11",
* "unixVer": "4.0"
* }
* }
* @uniVueVersion 2,3 //支持的vue版本
*
*/
getBatteryInfo(options: GetBatteryInfoOptions): void;
/**
* 同步获取电池电量信息
* @description 获取电池电量信息
* @example
* ```typescript
* uni.getBatteryInfo()
* ```
* @remark
* - 该接口需要同步调用
* @uniPlatform {
* "app": {
* "android": {
* "osVer": "4.4.4",
* "uniVer": "3.6.11",
* "unixVer": "3.9.0"
* },
* "ios": {
* "osVer": "12.0",
* "uniVer": "3.6.11",
* "unixVer": "4.11"
* }
* },
* "web": {
* "uniVer": "3.6.11",
* "unixVer": "4.0"
* }
* }
* @uniVueVersion 2,3 //支持的vue版本
*
*/
getBatteryInfoSync(): GetBatteryInfoResult;
}
//# sourceMappingURL=interface.uts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"interface.uts","sourceRoot":"","sources":["uni_modules/uni-getbatteryinfo/utssdk/interface.uts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG;IACnC,MAAM,EAAG,MAAM,CAAC;IAChB;;MAEE;IACF,KAAK,EAAG,MAAM,CAAC;IACf;;MAEE;IACF,UAAU,EAAG,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IACnC;;UAEG;IACH,OAAQ,CAAC,EAAE,CAAC,GAAG,EAAG,qBAAqB,KAAK,IAAI,CAAA;IAChD;;UAEG;IACH,IAAK,CAAC,EAAE,CAAC,GAAG,EAAG,QAAQ,KAAK,IAAI,CAAA;IAChC;;UAEG;IACH,QAAS,CAAC,EAAE,CAAC,GAAG,EAAG,GAAG,KAAK,IAAI,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IAClC;;MAEE;IACF,KAAK,EAAG,MAAM,CAAC;IACf;;MAEE;IACF,UAAU,EAAG,OAAO,CAAA;CACpB,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAE;AAC5C;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,SAAS;IACnD,OAAO,EAAG,uBAAuB,CAAA;CAClC;AAAA,CAAC;AAEF;;;;;;;;;;;EAWE;AACF,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAG,qBAAqB,KAAK,IAAI,CAAA;AAGtE,MAAM,MAAM,kBAAkB,GAAG,MAAM,oBAAoB,CAAA;AAE3D,UAAU,GAAG;IAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACH,cAAc,CAAE,OAAO,EAAG,qBAAqB,GAAI,IAAI,CAAC;IACxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,kBAAkB,IAAG,oBAAoB,CAAA;CAEzC","sourcesContent":["export type GetBatteryInfoSuccess = {\n\terrMsg : string,\n\t/**\n\t* 设备电量范围1 - 100\n\t*/\n\tlevel : number,\n\t/**\n\t* 是否正在充电中\n\t*/\n\tisCharging : boolean\n}\n\nexport type GetBatteryInfoOptions = {\n\t/**\n\t\t* 接口调用结束的回调函数(调用成功、失败都会执行)\n\t\t*/\n\tsuccess ?: (res : GetBatteryInfoSuccess) => void\n\t/**\n\t\t* 接口调用失败的回调函数\n\t\t*/\n\tfail ?: (res : UniError) => void\n\t/**\n\t\t* 接口调用成功的回调\n\t\t*/\n\tcomplete ?: (res : any) => void\n}\n\nexport type GetBatteryInfoResult = {\n\t/**\n\t* 设备电量范围1 - 100\n\t*/\n\tlevel : number,\n\t/**\n\t* 是否正在充电中\n\t*/\n\tisCharging : boolean\n}\n\n/**\n * 错误码\n * - 1001 getAppContext is null\n */\nexport type GetBatteryInfoErrorCode = 1001 ;\n/**\n * GetBatteryInfo 的错误回调参数\n */\nexport interface GetBatteryInfoFail extends IUniError {\n errCode : GetBatteryInfoErrorCode\n};\n\n/**\n* 获取电量信息\n* @param {GetBatteryInfoOptions} options\n*\n*\n* @tutorial https://uniapp.dcloud.net.cn/api/system/batteryInfo.html\n* @platforms APP-IOS = ^9.0,APP-ANDROID = ^22\n* @since 3.6.11\n*\n* @assert () => success({errCode: 0, errSubject: \"uni-getBatteryInfo\", errMsg: \"getBatteryInfo:ok\", level: 60, isCharging: false })\n* @assert () => fail({errCode: 1001, errSubject: \"uni-getBatteryInfo\", errMsg: \"getBatteryInfo:fail getAppContext is null\" })\n*/\nexport type GetBatteryInfo = (options : GetBatteryInfoOptions) => void\n\n\nexport type GetBatteryInfoSync = () => GetBatteryInfoResult\n\ninterface Uni {\n\n\t/**\n\t * 获取电池电量信息\n\t * @description 获取电池电量信息\n\t * @param {GetBatteryInfoOptions} options\n\t * @example\n\t * ```typescript\n\t * uni.getBatteryInfo({\n\t *\t\tsuccess(res) {\n\t *\t\t\t__f__('log','at uni_modules/uni-getbatteryinfo/utssdk/interface.uts:78',res);\n\t *\t\t}\n\t * })\n\t * ```\n\t * @remark\n\t * - 该接口需要同步调用\n\t * @uniPlatform {\n\t * \"app\": {\n\t * \"android\": {\n\t * \"osVer\": \"4.4.4\",\n\t * \"uniVer\": \"3.6.11\",\n\t * \"unixVer\": \"3.9.0\"\n\t * },\n\t * \"ios\": {\n\t * \"osVer\": \"12.0\",\n\t * \"uniVer\": \"3.6.11\",\n\t * \"unixVer\": \"4.11\"\n\t * }\n\t * },\n\t * \"web\": {\n\t * \"uniVer\": \"3.6.11\",\n\t * \"unixVer\": \"4.0\"\n\t * }\n\t * }\n\t * @uniVueVersion 2,3 //支持的vue版本\n\t *\n\t */\n\tgetBatteryInfo (options : GetBatteryInfoOptions) : void,\n\t/**\n\t * 同步获取电池电量信息\n\t * @description 获取电池电量信息\n\t * @example\n\t * ```typescript\n\t * uni.getBatteryInfo()\n\t * ```\n\t * @remark\n\t * - 该接口需要同步调用\n\t * @uniPlatform {\n\t * \"app\": {\n\t * \"android\": {\n\t * \"osVer\": \"4.4.4\",\n\t * \"uniVer\": \"3.6.11\",\n\t * \"unixVer\": \"3.9.0\"\n\t * },\n\t * \"ios\": {\n\t * \"osVer\": \"12.0\",\n\t * \"uniVer\": \"3.6.11\",\n\t * \"unixVer\": \"4.11\"\n\t * }\n\t * },\n\t * \"web\": {\n\t * \"uniVer\": \"3.6.11\",\n\t * \"unixVer\": \"4.0\"\n\t * }\n\t * }\n\t * @uniVueVersion 2,3 //支持的vue版本\n\t *\n\t */\n\tgetBatteryInfoSync():GetBatteryInfoResult\n\n}\n"]}

View File

@@ -0,0 +1,30 @@
import { GetBatteryInfoErrorCode, GetBatteryInfoFail } from "./interface.uts";
/**
* 错误主题
*/
export const UniErrorSubject = 'uni-getBatteryInfo';
/**
* 错误信息
* @UniError
*/
export const UniErrors: Map<GetBatteryInfoErrorCode, string> = new Map([
/**
* 错误码及对应的错误信息
*/
[1001, 'getBatteryInfo:fail getAppContext is null'],
]);
/**
* 错误对象实现
*/
export class GetBatteryInfoFailImpl extends UniError implements GetBatteryInfoFail {
/**
* 错误对象构造函数
*/
constructor(errCode: GetBatteryInfoErrorCode) {
super();
this.errSubject = UniErrorSubject;
this.errCode = errCode;
this.errMsg = UniErrors[errCode] ?? "";
}
}
//# sourceMappingURL=unierror.uts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"unierror.uts","sourceRoot":"","sources":["uni_modules/uni-getbatteryinfo/utssdk/unierror.uts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAC7E;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAGpD;;;GAGG;AACH,MAAM,CAAC,MAAM,SAAS,EAAG,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC;IACtE;;OAEG;IACH,CAAC,IAAI,EAAE,2CAA2C,CAAC;CACpD,CAAC,CAAC;AAGH;;GAEG;AACH,MAAM,OAAO,sBAAuB,SAAQ,QAAS,YAAW,kBAAkB;IAEhF;;OAEG;IACH,YAAY,OAAO,EAAG,uBAAuB;QAC3C,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,UAAU,GAAG,eAAe,CAAC;QAClC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IACzC,CAAC;CACF","sourcesContent":["import { GetBatteryInfoErrorCode, GetBatteryInfoFail } from \"./interface.uts\"\r\n/**\r\n * 错误主题\r\n */\r\nexport const UniErrorSubject = 'uni-getBatteryInfo';\r\n\r\n\r\n/**\r\n * 错误信息\r\n * @UniError\r\n */\r\nexport const UniErrors : Map<GetBatteryInfoErrorCode, string> = new Map([\r\n /**\r\n * 错误码及对应的错误信息\r\n */\r\n [1001, 'getBatteryInfo:fail getAppContext is null'],\r\n]);\r\n\r\n\r\n/**\r\n * 错误对象实现\r\n */\r\nexport class GetBatteryInfoFailImpl extends UniError implements GetBatteryInfoFail {\r\n\r\n /**\r\n * 错误对象构造函数\r\n */\r\n constructor(errCode : GetBatteryInfoErrorCode) {\r\n super();\r\n this.errSubject = UniErrorSubject;\r\n this.errCode = errCode;\r\n this.errMsg = UniErrors[errCode] ?? \"\";\r\n }\r\n}"]}

Binary file not shown.

View File

@@ -0,0 +1,28 @@
{
"id": "__UNI__95B2570",
"name": "akbleserver",
"description": "",
"version": {
"name": "1.0.1",
"code": "101"
},
"uni-app-x": {
"compilerVersion": "4.76"
},
"app-android": {
"distribute": {
"modules": {
"FileSystem": {}
},
"icons": {
"hdpi": "unpackage/res/icons/72x72.png",
"xhdpi": "unpackage/res/icons/96x96.png",
"xxhdpi": "unpackage/res/icons/144x144.png",
"xxxhdpi": "unpackage/res/icons/192x192.png"
},
"splashScreens": {
"default": {}
}
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,3 @@
{
"minSdkVersion": "21"
}

View File

@@ -0,0 +1,109 @@
@file:Suppress("UNCHECKED_CAST", "USELESS_CAST", "INAPPLICABLE_JVM_NAME", "UNUSED_ANONYMOUS_PARAMETER", "NAME_SHADOWING", "UNNECESSARY_NOT_NULL_ASSERTION")
package uts.sdk.modules.uniGetbatteryinfo
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import io.dcloud.uniapp.*
import io.dcloud.uniapp.extapi.*
import io.dcloud.uniapp.framework.*
import io.dcloud.uniapp.runtime.*
import io.dcloud.uniapp.vue.*
import io.dcloud.uniapp.vue.shared.*
import io.dcloud.unicloud.*
import io.dcloud.uts.*
import io.dcloud.uts.Map
import io.dcloud.uts.Set
import io.dcloud.uts.UTSAndroid
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
open class GetBatteryInfoSuccess (
@JsonNotNull
open var errMsg: String,
@JsonNotNull
open var level: Number,
@JsonNotNull
open var isCharging: Boolean = false,
) : UTSObject(), IUTSSourceMap {
override fun `__$getOriginalPosition`(): UTSSourceMapPosition? {
return UTSSourceMapPosition("GetBatteryInfoSuccess", "uni_modules/uni-getbatteryinfo/utssdk/interface.uts", 1, 13)
}
}
open class GetBatteryInfoOptions (
open var success: ((res: GetBatteryInfoSuccess) -> Unit)? = null,
open var fail: ((res: UniError) -> Unit)? = null,
open var complete: ((res: Any) -> Unit)? = null,
) : UTSObject(), IUTSSourceMap {
override fun `__$getOriginalPosition`(): UTSSourceMapPosition? {
return UTSSourceMapPosition("GetBatteryInfoOptions", "uni_modules/uni-getbatteryinfo/utssdk/interface.uts", 12, 13)
}
}
open class GetBatteryInfoResult (
@JsonNotNull
open var level: Number,
@JsonNotNull
open var isCharging: Boolean = false,
) : UTSObject(), IUTSSourceMap {
override fun `__$getOriginalPosition`(): UTSSourceMapPosition? {
return UTSSourceMapPosition("GetBatteryInfoResult", "uni_modules/uni-getbatteryinfo/utssdk/interface.uts", 26, 13)
}
}
typealias GetBatteryInfoErrorCode = Number
interface GetBatteryInfoFail : IUniError {
override var errCode: GetBatteryInfoErrorCode
}
typealias GetBatteryInfo = (options: GetBatteryInfoOptions) -> Unit
typealias GetBatteryInfoSync = () -> GetBatteryInfoResult
val UniErrorSubject = "uni-getBatteryInfo"
val UniErrors: Map<GetBatteryInfoErrorCode, String> = Map(_uA(
_uA(
1001,
"getBatteryInfo:fail getAppContext is null"
)
))
open class GetBatteryInfoFailImpl : UniError, GetBatteryInfoFail, IUTSSourceMap {
override fun `__$getOriginalPosition`(): UTSSourceMapPosition? {
return UTSSourceMapPosition("GetBatteryInfoFailImpl", "uni_modules/uni-getbatteryinfo/utssdk/unierror.uts", 19, 14)
}
constructor(errCode: GetBatteryInfoErrorCode) : super() {
this.errSubject = UniErrorSubject
this.errCode = errCode
this.errMsg = UniErrors[errCode] ?: ""
}
}
val getBatteryInfo: GetBatteryInfo = fun(options: GetBatteryInfoOptions) {
val context = UTSAndroid.getAppContext()
if (context != null) {
val manager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val level = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
var ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
var batteryStatus = context.registerReceiver(null, ifilter)
var status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
var isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
val res = GetBatteryInfoSuccess(errMsg = "getBatteryInfo:ok", level = level, isCharging = isCharging)
options.success?.invoke(res)
options.complete?.invoke(res)
} else {
var res = GetBatteryInfoFailImpl(1001)
options.fail?.invoke(res)
options.complete?.invoke(res)
}
}
val getBatteryInfoSync: GetBatteryInfoSync = fun(): GetBatteryInfoResult {
val context = UTSAndroid.getAppContext()
if (context != null) {
val manager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val level = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
var ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
var batteryStatus = context.registerReceiver(null, ifilter)
var status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
var isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
val res = GetBatteryInfoResult(level = level, isCharging = isCharging)
return res
} else {
val res = GetBatteryInfoResult(level = -1, isCharging = false)
return res
}
}