Commit 1e9d5afd authored by martin hou's avatar martin hou

fix: 增加后台任务实现与模拟

parent f5d1ac58
# Jensen - WebUSB Helper # Jensen 设备管理开发指南
一个基于WebUSB API的硬件设备通信库,支持设备连接、文件管理、蓝牙扫描等功能 本文档介绍如何使用 Jensen 库进行 HiDock 设备的连接管理、任务调度和状态监控
## 📋 目录 ## 📋 目录
- [功能特性](#功能特性) - [核心概念](#核心概念)
- [安装](#安装)
- [快速开始](#快速开始) - [快速开始](#快速开始)
- [API 文档](#api-文档) - [设备连接管理](#设备连接管理)
- [使用案例](#使用案例) - [任务系统](#任务系统)
- [第三方项目导入](#第三方项目导入) - [设备状态管理](#设备状态管理)
- [版本依赖](#版本依赖) - [完整示例](#完整示例)
- [开发指南](#开发指南) - [API 参考](#api-参考)
## ✨ 功能特性 ## 核心概念
- 🔌 **WebUSB设备连接** - 支持HiDock系列设备的USB连接 ### 架构概览
- 📁 **文件管理** - 文件列表、下载、删除等操作
- 📱 **蓝牙管理** - 蓝牙设备扫描、连接、状态查询
-**时间同步** - 设备时间获取和设置
- 🔧 **设备设置** - 自动录音、自动播放、通知等设置
- 📊 **实时数据** - 支持实时数据传输和控制
- 📝 **日志系统** - 完整的调试和错误日志
## 📦 安装 ```
┌─────────────────────────────────────────────────────────────────┐
### 从npm安装 │ ConnectionManager (mgr) │
├─────────────────────────────────────────────────────────────────┤
```bash │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
npm install jensen-webusb │ │ Device 1 │ │ Device 2 │ │ Device N │ │
│ │ (Jensen) │ │ (Jensen) │ │ (Jensen) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │TaskManager 1│ │TaskManager 2│ │TaskManager N│ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ StateStore │ │ StateStore │ │ StateStore │ │
│ │ TimerTasks │ │ TimerTasks │ │ TimerTasks │ │
│ │ UserTask │ │ UserTask │ │ UserTask │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
``` ```
### 从源码安装 ### 核心类说明
```bash | 类名 | 说明 |
git clone http://gitlab.sugrsugr.com/skye/jensen.git |------|------|
cd jensen | `ConnectionManager` | 设备连接管理器,负责多设备的连接、断开和实例管理 |
npm install | `TaskManager` | 任务调度器,管理初始化任务、定时任务、后台任务和用户任务 |
``` | `DeviceStateStore` | 设备状态存储,提供状态读写和订阅功能 |
| `Jensen` | 设备通信实例,提供所有与设备交互的 API |
## 🚀 快速开始 ### 任务类型
### 基本使用 | 任务类型 | 说明 | 典型用途 |
|----------|------|----------|
| `InitializeTask` | 初始化任务,设备连接后执行一次 | 获取设备信息、同步时间 |
| `TimerTask` | 定时任务,按指定间隔循环执行 | 轮询电池状态、文件列表 |
| `BackgroundTask` | 后台任务,带调度逻辑的周期任务 | 文件同步、数据上传 |
| `UserTask` | 用户任务,**排它性任务**,用于用户交互操作 | 文件下载、设备配置 |
```typescript > ⚠️ **重要**:`UserTask` 是排它性的,当一个 UserTask 在执行时,所有定时任务和后台任务会暂停,以确保用户操作不被干扰。
import Jensen from 'jensen-webusb';
// 创建Jensen实例 ## 快速开始
const jensen = new Jensen();
// 连接设备 ### 安装依赖
await jensen.connect();
// 获取设备信息 ```bash
const deviceInfo = await jensen.getDeviceInfo(); npm install
console.log('设备序列号:', deviceInfo.sn);
console.log('版本号:', deviceInfo.versionCode);
``` ```
### 在React项目中使用 ### 基本使用
```typescript
import { useEffect, useState } from 'react';
import Jensen from 'jensen-webusb';
function DeviceManager() { ```tsx
const [jensen] = useState(() => new Jensen()); import { mgr } from './utils/mgr';
const [isConnected, setIsConnected] = useState(false);
const [deviceInfo, setDeviceInfo] = useState(null);
useEffect(() => { // 1. 监听自动连接事件
jensen.onconnect = () => { mgr.onautoconnect((dinfo) => {
setIsConnected(true); console.log('设备已连接:', dinfo.sn);
jensen.getDeviceInfo().then(setDeviceInfo); });
};
jensen.ondisconnect = () => {
setIsConnected(false);
};
}, [jensen]);
const handleConnect = async () => { // 2. 尝试连接已授权的设备
try { await mgr.tryconnect();
await jensen.connect();
} catch (error) {
console.error('连接失败:', error);
}
};
return (
<div>
<button onClick={handleConnect}>连接设备</button>
{isConnected && <p>设备已连接: {deviceInfo?.sn}</p>}
</div>
);
}
``` ```
## 📚 API 文档 ## 设备连接管理
### 核心类
#### Jensen ### 自动连接
主要的设备通信类,提供所有设备操作接口。 当页面加载时,调用 `tryconnect()` 会自动连接所有已授权的 HiDock 设备:
```typescript ```tsx
class Jensen { import { mgr } from './utils/mgr';
constructor(log?: Logger);
}
```
### 连接管理 // 注册连接事件监听器
mgr.onautoconnect((dinfo) => {
console.log('设备序列号:', dinfo.sn);
console.log('固件版本:', dinfo.versionCode);
});
#### connect() // 尝试自动连接
连接到WebUSB设备 await mgr.tryconnect();
```typescript
await jensen.connect(): Promise<void>
``` ```
#### disconnect() ### 手动连接
断开设备连接
```typescript 如果需要让用户选择设备进行连接:
await jensen.disconnect(): Promise<void>
```
#### isConnected() ```tsx
检查设备连接状态 const dinfo = await mgr.connect();
if (dinfo) {
console.log('手动连接成功:', dinfo.sn);
}
```
```typescript ### 监听连接状态变化
jensen.isConnected(): boolean
```tsx
mgr.onconnectionstatechanged((state, dinfo) => {
switch (state) {
case 'init':
console.log('正在初始化设备...');
break;
case 'connected':
console.log('设备已连接:', dinfo);
break;
case 'reconnected':
console.log('设备已重新连接:', dinfo);
break;
case 'disconnected':
console.log('设备已断开:', dinfo);
break;
case 'connect-timeout':
console.log('连接超时');
break;
}
});
``` ```
#### tryconnect(disableOnConnect?: boolean) ### 断开连接
尝试连接已授权的设备
```typescript ```tsx
await jensen.tryconnect(disableOnConnect?: boolean): Promise<boolean> await mgr.close(sn);
``` ```
### 设备信息 ## 任务系统
#### getDeviceInfo(time?: number)
获取设备信息
```typescript ### UserTask(用户任务)
await jensen.getDeviceInfo(time?: number): Promise<DeviceInfo>
interface DeviceInfo { UserTask 是用于用户交互操作的**排它性任务**。当你需要执行用户操作(如下载文件、修改设置)时,必须先注册 UserTask。
sn: string; // 设备序列号
versionNumber: number; // 版本号
versionCode: string; // 版本代码
}
```
#### getModel() #### 为什么需要 UserTask?
获取设备型号
```typescript 1. **防止冲突**:确保用户操作不会被定时任务打断
jensen.getModel(): string 2. **资源独占**:在 UserTask 执行期间,后台任务会暂停
``` 3. **超时保护**:空闲超过 30 秒的 UserTask 会自动释放
### 文件管理 #### 注册和释放 UserTask
#### listFiles(time?: number) ```tsx
获取文件列表 // 注册 UserTask(带超时等待)
const success = await mgr.registerTask(sn, 'my-task-id');
if (!success) {
console.error('注册任务失败,可能有其他任务正在执行');
return;
}
```typescript try {
await jensen.listFiles(time?: number): Promise<FileInfo[]> // 执行用户操作...
} finally {
interface FileInfo { // 释放 UserTask
name: string; // 文件名 await mgr.unregisterTask(sn, 'my-task-id');
createDate: string; // 创建日期
createTime: string; // 创建时间
time: Date; // 时间对象
duration: number; // 时长
length: number; // 文件大小
signature: string; // 文件签名
} }
``` ```
#### getFile(fileName, length, on?, onprogress?) #### 使用 getInstance 获取设备实例
下载完整文件
```typescript 获取 Jensen 实例时**必须传入 UserTask 标签**,这样所有操作都会自动验证任务有效性:
jensen.getFile(
fileName: string,
length: number,
on?: (msg: Uint8Array | 'fail') => void,
onprogress?: (size: number) => void
): void
```
#### getFilePart(fileName, length, on?, onprogress?) ```tsx
下载文件片段 // 正确用法:传入 userTaskTag
const jensen = mgr.getInstance(sn, userTaskTag);
if (jensen == null) return alert('设备未连接');
```typescript // 调用方法时会自动验证 UserTask
jensen.getFilePart( const time = await jensen.getTime(1);
fileName: string,
length: number,
on?: (msg: Uint8Array | 'fail') => void,
onprogress?: (size: number) => void
): void
``` ```
#### deleteFile(fileName) #### 使用 executeWithUserTask(推荐)
删除文件
```typescript 更安全的方式是使用 `executeWithUserTask`,它会自动验证任务并执行回调:
await jensen.deleteFile(fileName: string): Promise<{ result: 'failed' | 'success' | 'not-exists' }>
```
### 蓝牙管理 ```tsx
const time = await mgr.executeWithUserTask(sn, userTaskTag, async (jensen) => {
return await jensen.getTime(1);
});
```
#### scanDevices(seconds?: number) #### 保持任务活跃
扫描蓝牙设备
```typescript 如果 UserTask 长时间不活动(超过 30 秒),会被自动释放。对于长时间操作,需要定期调用保活方法:
await jensen.scanDevices(seconds?: number): Promise<BluetoothDevice[]>
interface BluetoothDevice { ```tsx
name: string; // 设备名称 // 保持任务活跃
mac: string; // MAC地址 mgr.keepUserTaskAlive(sn, userTaskTag);
}
``` ```
#### connectBTDevice(mac, seconds?: number) ### 定时任务(TimerTask)
连接蓝牙设备
```typescript 定时任务会在设备连接后按指定间隔循环执行。内置的定时任务包括:
await jensen.connectBTDevice(mac: string, seconds?: number): Promise<ReturnStruct['common']>
```
#### disconnectBTDevice(seconds?: number)
断开蓝牙设备
```typescript - `battery-status`:每秒同步电池状态
await jensen.disconnectBTDevice(seconds?: number): Promise<ReturnStruct['common']> - `get-device-time`:每秒获取设备时间
``` - `recording-status`:每秒检查录音状态
- `file-list-sync`:每秒检查文件列表变化
#### getBluetoothStatus(seconds?: number) #### 注册自定义定时任务
获取蓝牙状态
```typescript ```tsx
await jensen.getBluetoothStatus(seconds?: number): Promise<BluetoothStatus> mgr.registerTimerTask(sn, {
tag: 'my-timer-task',
interface BluetoothStatus { interval: 5000, // 5秒执行一次
status: string; // 连接状态 task: async (jensen, store) => {
mac?: string; // MAC地址 const settings = await jensen?.getSettings(1);
name?: string; // 设备名称 if (settings) {
a2dp?: boolean; // A2DP支持 store.set('settings', settings);
hfp?: boolean; // HFP支持 }
avrcp?: boolean; // AVRCP支持 },
battery?: number; // 电池电量 onComplete: (success, data) => {
} console.log('任务完成:', success);
}
});
``` ```
### 时间管理 ### 初始化任务(InitializeTask)
#### getTime(time?: number) 初始化任务在设备首次连接时执行一次。内置的初始化任务包括:
获取设备时间
```typescript - `time-sync`:同步设备时间
await jensen.getTime(time?: number): Promise<{ time: string }> - `get-settings`:获取设备设置
``` - `get-card-info`:获取存储卡信息
- `battery-status`:获取电池状态
#### setTime(date, timeout?: number) #### 注册自定义初始化任务
设置设备时间
```typescript ```tsx
await jensen.setTime(date: Date, timeout?: number): Promise<ReturnStruct['common']> mgr.registerInitializeTask(sn, {
tag: 'my-init-task',
task: async (jensen, store) => {
const info = await jensen?.getDeviceInfo(1);
store.set('deviceInfo', info);
}
});
``` ```
### 设备设置 ## 设备状态管理
#### getSettings(time?: number) ### DeviceStateStore
获取设备设置
```typescript 每个设备都有一个 `DeviceStateStore` 实例,用于存储设备状态。状态变更会自动通知所有订阅者。
await jensen.getSettings(time?: number): Promise<{
autoRecord: boolean;
autoPlay: boolean;
notification?: boolean;
} | null>
```
#### setAutoRecord(enable, time?: number) ### 可用的状态键
设置自动录音
```typescript ```typescript
await jensen.setAutoRecord(enable: boolean, time?: number): Promise<ReturnStruct['common']> interface DeviceStateSchema {
deviceInfo: DeviceInfo | null;
time: { deviceTime: string; syncTime: Date } | null;
settings: { autoRecord: boolean; autoPlay: boolean; notification?: boolean } | null;
cardInfo: { used: number; capacity: number; status: string } | null;
batteryStatus: BatteryStatus | null;
bluetoothStatus: BluetoothStatus | null;
files: FileInfo[] | null;
recording: FileInfo | null;
// ... 可动态扩展
}
``` ```
#### setAutoPlay(enable, time?: number) ### 订阅状态变更
设置自动播放
```typescript
await jensen.setAutoPlay(enable: boolean, time?: number): Promise<ReturnStruct['common']>
```
### 实时数据 ```tsx
// 订阅电池状态变化
mgr.subscribeDeviceState(sn, 'battery-status', (key, newValue, oldValue) => {
console.log('电池状态更新:', newValue);
});
#### startRealtime() // 订阅文件列表变化
开始实时数据传输 mgr.subscribeDeviceState(sn, 'files', (key, newValue, oldValue) => {
console.log('文件列表更新:', newValue);
});
```typescript // 订阅录音状态变化
await jensen.startRealtime(): Promise<ReturnStruct['common']> mgr.subscribeDeviceState(sn, 'recording', (key, newValue, oldValue) => {
if (newValue && !oldValue) {
console.log('开始录音:', newValue.name);
} else if (!newValue && oldValue) {
console.log('录音结束');
}
});
``` ```
#### pauseRealtime() ### 直接读取状态
暂停实时数据传输
```typescript ```tsx
await jensen.pauseRealtime(): Promise<ReturnStruct['common']> // 获取当前电池状态
``` const battery = mgr.getDeviceState(sn, 'battery-status');
#### stopRealtime() // 获取文件列表
停止实时数据传输 const files = mgr.getDeviceState(sn, 'files');
```typescript // 检查状态是否过期(超过5秒)
await jensen.stopRealtime(): Promise<ReturnStruct['common']> const isStale = mgr.isDeviceStateStale(sn, 'battery-status', 5000);
``` ```
#### getRealtime(frames) ### 手动设置状态
获取实时数据
```typescript ```tsx
await jensen.getRealtime(frames: number): Promise<{ data: Uint8Array; rest: number }> mgr.setDeviceState(sn, 'settings', {
autoRecord: true,
autoPlay: false
});
``` ```
## 💡 使用案 ## 完整示
### 案例1: 设备连接和文件下载 以下是一个完整的 React 组件示例,展示了所有核心功能的使用:
```typescript ```tsx
import Jensen from 'jensen-webusb'; import { mgr } from './utils/mgr';
import { useEffect, useState } from 'react';
import Jensen from '../jensen';
export function DeviceManager() {
// 状态定义
const [sn, setSn] = useState<string | null>(null);
const [time, setTime] = useState<string | null>(null);
const [battery, setBattery] = useState<string | null>(null);
const [userTask, setUserTask] = useState<string | null>(null);
const [files, setFiles] = useState<Jensen.FileInfo[]>([]);
const [recording, setRecording] = useState<Jensen.FileInfo | null>(null);
// 设备连接和状态订阅
useEffect(() => {
mgr.onautoconnect((dinfo) => {
setSn(dinfo.sn);
// 订阅时间状态变化
mgr.subscribeDeviceState(dinfo.sn, 'time', (key, newValue) => {
setTime(JSON.stringify(newValue));
});
// 订阅电池状态变化
mgr.subscribeDeviceState(dinfo.sn, 'battery-status', (key, newValue) => {
setBattery(JSON.stringify(newValue));
});
// 订阅文件列表变化
mgr.subscribeDeviceState(dinfo.sn, 'files', (key, newValue) => {
setFiles(newValue as Jensen.FileInfo[]);
});
// 订阅录音状态变化
mgr.subscribeDeviceState(dinfo.sn, 'recording', (key, newValue) => {
setRecording(newValue as Jensen.FileInfo | null);
});
});
}, []);
async function downloadLatestFile() {
const jensen = new Jensen();
try {
// 连接设备 // 连接设备
await jensen.connect(); const doConnect = async () => {
console.log('设备连接成功'); await mgr.tryconnect();
};
// 获取文件列表
const files = await jensen.listFiles();
if (files.length === 0) {
console.log('没有找到文件');
return;
}
// 下载最新文件
const latestFile = files[files.length - 1];
console.log(`开始下载文件: ${latestFile.name}`);
jensen.getFile(latestFile.name, latestFile.length, (data) => {
if (data instanceof Uint8Array) {
// 保存文件
const blob = new Blob([data]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = latestFile.name;
a.click();
URL.revokeObjectURL(url);
console.log('文件下载完成');
} else {
console.error('文件下载失败');
}
}, (progress) => {
console.log(`下载进度: ${progress}/${latestFile.length}`);
});
} catch (error) {
console.error('操作失败:', error);
}
}
```
### 案例2: 蓝牙设备管理
```typescript
import Jensen from 'jensen-webusb';
async function manageBluetoothDevices() {
const jensen = new Jensen();
try {
await jensen.connect();
// 扫描蓝牙设备
console.log('开始扫描蓝牙设备...');
const devices = await jensen.scanDevices();
console.log(`找到 ${devices.length} 个设备:`);
devices.forEach(device => {
console.log(`- ${device.name} (${device.mac})`);
});
// 连接第一个设备
if (devices.length > 0) {
const result = await jensen.connectBTDevice(devices[0].mac);
console.log('连接结果:', result.result);
// 获取连接状态
const status = await jensen.getBluetoothStatus();
console.log('蓝牙状态:', status);
}
} catch (error) {
console.error('蓝牙操作失败:', error);
}
}
```
### 案例3: 设备设置管理
```typescript // 注册 UserTask
import Jensen from 'jensen-webusb'; const doRequestUserTask = async () => {
if (sn == null) return alert('请先连接设备');
async function configureDevice() {
const jensen = new Jensen(); const tid = 't-' + new Date().getTime();
const success = await mgr.registerTask(sn, tid);
try {
await jensen.connect(); if (!success) {
return alert('注册任务失败');
// 获取当前设置 }
const settings = await jensen.getSettings();
console.log('当前设置:', settings); setUserTask(tid);
console.log('UserTask 已注册:', tid);
// 修改设置 };
await jensen.setAutoRecord(true);
await jensen.setAutoPlay(false);
await jensen.setNotification(true);
console.log('设置已更新');
// 验证设置
const newSettings = await jensen.getSettings();
console.log('更新后的设置:', newSettings);
} catch (error) {
console.error('设置操作失败:', error);
}
}
```
### 案例4: 实时数据监控 // 释放 UserTask
const doReleaseUserTask = async () => {
if (sn == null) return alert('请先连接设备');
if (userTask == null) return alert('请先注册 UserTask');
await mgr.unregisterTask(sn, userTask);
setUserTask(null);
console.log('UserTask 已释放');
};
```typescript // 获取设备时间(需要 UserTask)
import Jensen from 'jensen-webusb'; const doGetTime = async () => {
if (sn == null) return alert('请先连接设备');
async function monitorRealtimeData() { if (userTask == null) return alert('请先注册 UserTask');
const jensen = new Jensen();
try {
await jensen.connect();
// 开始实时数据传输
await jensen.startRealtime();
console.log('实时数据传输已开始');
// 定期获取数据
const interval = setInterval(async () => {
try {
const result = await jensen.getRealtime(100);
console.log(`接收到 ${result.data.length} 字节数据`);
// 处理数据... try {
processData(result.data); // 方式一:使用 getInstance
const jensen = mgr.getInstance(sn, userTask);
if (jensen == null) return alert('设备未连接');
const time = await jensen.getTime(1);
alert('设备时间: ' + time.time);
// 方式二:使用 executeWithUserTask(推荐)
// const time = await mgr.executeWithUserTask(sn, userTask, async (jensen) => {
// return await jensen.getTime(1);
// });
// alert('设备时间: ' + time.time);
} catch (err: any) {
alert('错误: ' + err.message);
}
};
// 下载文件(需要 UserTask)
const doDownloadFile = async (fileName: string) => {
if (sn == null) return alert('请先连接设备');
if (userTask == null) return alert('请先注册 UserTask');
} catch (error) { try {
console.error('获取实时数据失败:', error); const jensen = mgr.getInstance(sn, userTask);
clearInterval(interval); if (jensen == null) return alert('设备未连接');
}
}, 1000); await jensen.getFile(fileName, 1024, (data) => {
if (data instanceof Uint8Array) {
// 10秒后停止 console.log('文件数据:', data);
setTimeout(async () => { // 处理下载的数据...
clearInterval(interval); } else {
await jensen.stopRealtime(); console.error('下载失败');
console.log('实时数据传输已停止'); }
}, 10000); });
} catch (err: any) {
} catch (error) { alert('错误: ' + err.message);
console.error('实时监控失败:', error); }
} };
}
function processData(data: Uint8Array) { return (
// 处理实时数据的逻辑 <>
console.log('处理数据:', data); <h1>设备管理器</h1>
{/* 操作按钮 */}
<div className="btn-container">
<button onClick={doConnect}>连接设备</button>
<button onClick={doRequestUserTask}>注册任务</button>
<button onClick={doReleaseUserTask}>释放任务</button>
<button onClick={doGetTime}>获取时间</button>
</div>
{/* 设备信息 */}
<div>
<h3>设备信息</h3>
<ul>
<li>SN: {sn}</li>
<li>时间: {time}</li>
<li>电池: {battery}</li>
<li>录音中: {recording?.name || '无'}</li>
</ul>
<h3>文件列表</h3>
<ul>
{files.map((file, index) => (
<li key={index}>
{file.name} - {file.length} bytes - {file.duration}ms
<a href="#" onClick={() => doDownloadFile(file.name)}>下载</a>
</li>
))}
</ul>
</div>
</>
);
} }
``` ```
## 🔧 第三方项目导入 ## API 参考
### 在React项目中使用 ### ConnectionManager (mgr)
```bash #### 连接管理
npm install jensen-webusb
```
```typescript | 方法 | 说明 |
// 在组件中使用 |------|------|
import { useEffect, useState } from 'react'; | `tryconnect()` | 尝试自动连接已授权的设备 |
import Jensen from 'jensen-webusb'; | `connect()` | 打开设备选择对话框手动连接 |
| `close(sn)` | 关闭指定设备的连接 |
function JensenComponent() { | `onautoconnect(handler)` | 注册自动连接成功的回调 |
const [jensen] = useState(() => new Jensen()); | `onconnectionstatechanged(handler)` | 注册连接状态变化的回调 |
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
jensen.onconnect = () => setIsConnected(true);
jensen.ondisconnect = () => setIsConnected(false);
}, [jensen]);
// 组件逻辑...
}
```
### 在Vue项目中使用 #### 任务管理
```bash | 方法 | 说明 |
npm install jensen-webusb |------|------|
``` | `registerTask(sn, tag)` | 注册 UserTask(超时 3 秒) |
| `unregisterTask(sn, tag)` | 释放 UserTask |
| `keepUserTaskAlive(sn, tag)` | 保持 UserTask 活跃 |
| `verifyUserTask(sn, tag)` | 验证 UserTask 是否有效 |
| `getCurrentUserTaskTag(sn)` | 获取当前 UserTask 的标签 |
| `executeWithUserTask(sn, tag, callback)` | 验证后执行回调 |
```typescript #### 设备实例
// 在Vue组件中使用
import { ref, onMounted } from 'vue';
import Jensen from 'jensen-webusb';
export default {
setup() {
const jensen = ref(new Jensen());
const isConnected = ref(false);
onMounted(() => {
jensen.value.onconnect = () => isConnected.value = true;
jensen.value.ondisconnect = () => isConnected.value = false;
});
return {
jensen,
isConnected
};
}
};
```
### 在Node.js项目中使用 | 方法 | 说明 |
|------|------|
| `getInstance(sn, userTaskTag)` | 获取受保护的 Jensen 实例 |
```bash #### 状态管理
npm install jensen-webusb
```
```typescript | 方法 | 说明 |
// 注意:WebUSB API仅在浏览器环境中可用 |------|------|
import Jensen from 'jensen-webusb'; | `getDeviceState(sn, key)` | 获取设备状态值 |
| `setDeviceState(sn, key, value)` | 设置设备状态值 |
| `subscribeDeviceState(sn, key, listener)` | 订阅状态变更 |
| `isDeviceStateStale(sn, key, maxAge)` | 检查状态是否过期 |
| `dumpDeviceState(sn)` | 导出状态快照(调试用) |
// 在浏览器环境中使用 ### TaskManager
if (typeof window !== 'undefined') {
const jensen = new Jensen();
// 使用逻辑...
}
```
### 在TypeScript项目中使用 | 方法 | 说明 |
|------|------|
| `registerInitializeTask(task)` | 注册初始化任务 |
| `registerTimerTask(task)` | 注册定时任务 |
| `registerBackgroundTask(task)` | 注册后台任务 |
| `applyUserTask(tag, timeout)` | 申请 UserTask |
| `releaseUserTask(tag)` | 释放 UserTask |
| `verifyUserTask(tag)` | 验证 UserTask |
```typescript ### DeviceStateStore
// 类型定义已包含在包中
import Jensen, { DeviceInfo, FileInfo, BluetoothDevice } from 'jensen-webusb';
const jensen = new Jensen(); | 方法 | 说明 |
|------|------|
| `get(key)` | 获取状态值 |
| `set(key, value)` | 设置状态值(自动触发监听器) |
| `has(key)` | 检查状态是否存在 |
| `getUpdateTime(key)` | 获取最后更新时间 |
| `isStale(key, maxAge)` | 检查状态是否过期 |
| `subscribe(key, listener)` | 订阅状态变更(返回取消函数) |
| `clear()` | 清空所有状态 |
| `clearListeners()` | 清空所有监听器 |
async function typedExample() { ## 常见问题
const deviceInfo: DeviceInfo = await jensen.getDeviceInfo();
const files: FileInfo[] = await jensen.listFiles();
const bluetoothDevices: BluetoothDevice[] = await jensen.scanDevices();
}
```
## 📋 版本依赖 ### 1. 为什么调用 `getInstance()` 报错?
### 核心依赖 确保传入了 `userTaskTag` 参数:
```tsx
// ❌ 错误
const jensen = mgr.getInstance(sn);
```json // ✅ 正确
{ const jensen = mgr.getInstance(sn, userTaskTag);
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
``` ```
### 开发依赖 ### 2. UserTask 被自动释放了?
```json
{
"devDependencies": {
"@types/node": "^22.10.2",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react-swc": "^3.7.2",
"prettier": "^3.3.3",
"typescript": "^5.7.2",
"vite": "^6.0.3"
}
}
```
### 浏览器兼容性 UserTask 空闲超过 30 秒会被自动释放。对于长时间操作,定期调用:
```tsx
mgr.keepUserTaskAlive(sn, userTaskTag);
```
- Chrome 67+ ### 3. 如何知道设备断开了?
- Edge 79+
- Opera 54+
- 支持WebUSB API的现代浏览器
### Node.js版本要求 使用 `onconnectionstatechanged` 监听:
```tsx
mgr.onconnectionstatechanged((state, dinfo) => {
if (state === 'disconnected') {
console.log('设备断开:', dinfo);
}
});
```
- Node.js 16.0.0+ ### 4. 状态订阅如何取消?
## 🛠️ 开发指南 `subscribeDeviceState` 返回取消函数:
```tsx
const unsubscribe = mgr.subscribeDeviceState(sn, 'battery-status', listener);
### 本地开发 // 取消订阅
unsubscribe?.();
```
```bash ## 开发调试
# 克隆项目
git clone http://gitlab.sugrsugr.com/skye/jensen.git
cd jensen
# 安装依赖 ### 启动开发服务器
npm install
# 启动开发服务器 ```bash
npm run dev npm run dev
# 构建项目
npm run build
# 代码格式化
npm run prettier
``` ```
### 项目结构 ### 构建项目
```bash
npm run build
``` ```
jensen/
├── src/
│ ├── App.tsx # 主应用入口
│ ├── index.tsx # 主组件
│ ├── index.css # 样式文件
│ └── utils/
│ ├── jensen.js # 核心Jensen类
│ └── utils.js # 工具函数
├── jensen.d.ts # TypeScript类型定义
├── package.json # 项目配置
├── tsconfig.json # TypeScript配置
├── vite.config.mts # Vite配置
└── README.md # 项目文档
```
### 贡献指南
1. Fork 项目 ### 查看设备状态快照
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 打开 Pull Request
### 许可证
本项目采用 ISC 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
## 📞 支持
Martin Hou (martin@hidock.com)
```tsx
console.log(mgr.dumpDeviceState(sn));
```
---
**注意**: 此库需要支持WebUSB API的现代浏览器,并且设备需要支持相应的USB协议。 **注意**:本库基于 WebUSB API,仅支持 Chrome 67+、Edge 79+、Opera 54+ 等现代浏览器。
\ No newline at end of file
...@@ -77,15 +77,15 @@ export function DeviceManager() { ...@@ -77,15 +77,15 @@ export function DeviceManager() {
} }
} }
const doDownloadFile = async (fileName: string) => { const doDownloadFile = async (file: Jensen.FileInfo) => {
if (sn == null) return alert('Please connect first'); if (sn == null) return alert('Please connect first');
if (userTask == null) return alert('Please request a user task first');
// 需要有一个发起后台任务的方法
try { try {
const jensen = mgr.getInstance(sn, userTask); // ...
if (jensen == null) return alert('Please connect first'); mgr.submitFileTask(sn, {
await jensen.getFile(fileName, 1024, (data) => { fileName: file.name,
console.log(data); fileLength: file.length,
fileSignature: file.signature,
task: 'download'
}); });
} catch (err: any) { } catch (err: any) {
alert('Error: ' + err.message); alert('Error: ' + err.message);
...@@ -113,7 +113,7 @@ export function DeviceManager() { ...@@ -113,7 +113,7 @@ export function DeviceManager() {
<ol> <ol>
{ {
files.map((file, index) => { files.map((file, index) => {
return <li key={index}>{file.name} - {file.length} - {file.duration} <a href="#" onClick={() => { doDownloadFile(file.name) }}>Download</a></li> return <li key={index}>{file.name} - {file.length} - {file.duration} <a href="javascript:void(0)" onClick={() => { doDownloadFile(file) }}>Download</a></li>
}) })
} }
</ol> </ol>
......
import Jensen, { DeviceInfo, ScheduleInfo } from "../../"; import Jensen, { DeviceInfo, ScheduleInfo } from "../../";
import { Logger } from "../Logger"; import { Logger } from "../Logger";
import { BackgroundTask, DeviceStateSchema, DeviceStateStore, InitializeTask, StateChangeListener, TaskManager, TimerTask } from "./tasks"; import { BackgroundTask, DeviceStateSchema, DeviceStateStore, InitializeTask, StateChangeListener, TaskManager, TimerTask } from "./tasks";
import { buildTasks, FileTaskData } from "./task-impl";
// 指令参数 // 指令参数
export type CommandOptions = { export type CommandOptions = {
...@@ -110,6 +111,10 @@ export class ConnectionManager ...@@ -110,6 +111,10 @@ export class ConnectionManager
return tm.verifyUserTask(tag); return tm.verifyUserTask(tag);
} }
submitFileTask (sn: string, data: FileTaskData) {
return this.taskManagers.get(sn)?.getStateStore()?.pushTaskData('file-transfer', data);
}
// 获取当前用户任务的 tag // 获取当前用户任务的 tag
getCurrentUserTaskTag (sn: string): string | null { getCurrentUserTaskTag (sn: string): string | null {
return this.taskManagers.get(sn)?.getCurrentUserTaskTag() || null; return this.taskManagers.get(sn)?.getCurrentUserTaskTag() || null;
...@@ -273,7 +278,7 @@ export class ConnectionManager ...@@ -273,7 +278,7 @@ export class ConnectionManager
let tm = this.taskManagers.get(dinfo.sn); let tm = this.taskManagers.get(dinfo.sn);
if (tm == null) { if (tm == null) {
tm = new TaskManager(inst, this.logger); tm = new TaskManager(inst, this.logger);
this.taskBuilder(inst, tm); buildTasks(inst, tm);
await tm.scheduleInitializeTasks(); await tm.scheduleInitializeTasks();
this.taskManagers.set(dinfo.sn, tm); this.taskManagers.set(dinfo.sn, tm);
} }
...@@ -294,136 +299,6 @@ export class ConnectionManager ...@@ -294,136 +299,6 @@ export class ConnectionManager
} }
} }
taskBuilder(jensen: Jensen, tm: TaskManager)
{
// 初始化任务的注册 - 数据会被存储到 DeviceStateStore 中
tm.registerInitializeTask({
tag: 'time-sync',
task: async (jensen: Jensen | null, store) => {
let time = await jensen?.getTime(1);
Logger.info('jensen', 'initialize', 'get-time: ' + JSON.stringify(time));
// 存储设备时间到状态容器
if (time) {
store.set('time', { deviceTime: time.time, syncTime: new Date() });
}
// 同步设置时间
await jensen?.setTime(new Date(), 1);
Logger.info('jensen', 'initialize', 'set-time-to: ' + new Date().toLocaleString());
}
});
tm.registerInitializeTask({
tag: 'get-settings',
task: async (jensen: Jensen | null, store) => {
let settings = await jensen?.getSettings(1);
Logger.info('jensen', 'initialize', 'get-settings: ' + JSON.stringify(settings));
// 存储设置到状态容器
if (settings) {
store.set('settings', settings);
}
}
});
tm.registerInitializeTask({
tag: 'get-card-info',
task: async (jensen: Jensen | null, store) => {
let cardInfo = await jensen?.getCardInfo(1);
Logger.info('jensen', 'initialize', 'get-card-info: ' + JSON.stringify(cardInfo));
// 存储卡信息到状态容器
if (cardInfo) {
store.set('cardInfo', cardInfo);
}
}
});
tm.registerInitializeTask({
tag: 'battery-status',
task: async (jensen: Jensen | null, store) => {
if (jensen?.getModel() != 'hidock-p1') return;
let batteryStatus = await jensen?.getBatteryStatus(1);
Logger.info('jensen', 'initialize', 'get-battery-status: ' + JSON.stringify(batteryStatus));
// 存储电池状态到状态容器
if (batteryStatus) {
store.set('battery-status', batteryStatus);
}
}
});
// 定时同步设备电池状态
tm.registerTimerTask({
tag: 'battery-status',
interval: 1000,
task: async (jensen: Jensen | null, store) => {
if (jensen?.getModel() != 'hidock-p1') return;
let batteryStatus = await jensen?.getBatteryStatus(1);
Logger.info('jensen', 'timer-task', 'battery: ' + JSON.stringify(batteryStatus));
// 更新电池状态(会自动触发订阅者)
if (batteryStatus) {
store.set('battery-status', batteryStatus);
}
}
});
// 定时获取设备时间
tm.registerTimerTask({
tag: 'get-device-time',
interval: 1000,
task: async (jensen: Jensen | null, store) => {
if (jensen?.getModel() != 'hidock-p1') return;
let time = await jensen?.getTime(1);
Logger.info('jensen', 'timer-task', 'time: ' + JSON.stringify(time));
// 更新设备时间
if (time) {
store.set('time', { deviceTime: time.time, syncTime: new Date() });
}
}
});
// 录音中的状态同步
tm.registerTimerTask({
tag: 'recording-status',
interval: 1000,
task: async (jensen: Jensen | null, store) => {
let recording = await jensen?.getRecordingFile();
Logger.info('jensen', 'timer-task', 'recording-status: ' + JSON.stringify(recording));
if (recording) store.set('recording', recording);
}
});
// 设备录音文件列表同步
tm.registerTimerTask({
tag: 'file-list-sync',
interval: 1000,
task: async (jensen: Jensen | null, store) => {
let needRefresh = false;
// 1. 定时查询文件数量
let fileCount = await jensen?.getFileCount(1);
if (fileCount)
{
let lastCount = store.get('file-count');
if (lastCount != fileCount.count)
{
needRefresh = true;
Logger.info('jensen', 'timer-task', 'file-count changed: ' + lastCount + ' -> ' + fileCount.count);
}
store.set('file-count', fileCount.count);
}
if (!needRefresh) return;
let files = await jensen?.listFiles();
if (files) store.set('files', files);
}
});
// 后台任务的注册(如有需要可以在这里添加)
}
onautoconnect (eventListener: AutoConnectEventHandler) { onautoconnect (eventListener: AutoConnectEventHandler) {
this.onautoconnectEventListener = eventListener; this.onautoconnectEventListener = eventListener;
} }
......
import Jensen, { FileInfo } from "../../jensen";
import { Logger } from "../Logger";
import { TaskManager, DeviceStateStore } from "./tasks";
export type FileTaskData = {
fileName: string;
fileLength: number;
fileSignature: string;
task: 'download' | 'transcribe' | 'summarize';
}
/**
* 任务构建器 - 注册设备的所有后台任务
* @param jensen Jensen 实例
* @param tm TaskManager 实例
*/
export function buildTasks(jensen: Jensen, tm: TaskManager) {
// ===================== 初始化任务 =====================
// 时间同步
tm.registerInitializeTask({
tag: 'time-sync',
task: async (jensen: Jensen | null, store) => {
let time = await jensen?.getTime(1);
Logger.info('jensen', 'initialize', 'get-time: ' + JSON.stringify(time));
// 存储设备时间到状态容器
if (time) {
store.set('time', { deviceTime: time.time, syncTime: new Date() });
}
// 同步设置时间
await jensen?.setTime(new Date(), 1);
Logger.info('jensen', 'initialize', 'set-time-to: ' + new Date().toLocaleString());
}
});
// 获取设备设置
tm.registerInitializeTask({
tag: 'get-settings',
task: async (jensen: Jensen | null, store) => {
let settings = await jensen?.getSettings(1);
Logger.info('jensen', 'initialize', 'get-settings: ' + JSON.stringify(settings));
// 存储设置到状态容器
if (settings) {
store.set('settings', settings);
}
}
});
// 获取存储卡信息
tm.registerInitializeTask({
tag: 'get-card-info',
task: async (jensen: Jensen | null, store) => {
let cardInfo = await jensen?.getCardInfo(1);
Logger.info('jensen', 'initialize', 'get-card-info: ' + JSON.stringify(cardInfo));
// 存储卡信息到状态容器
if (cardInfo) {
store.set('cardInfo', cardInfo);
}
}
});
// 获取电池状态(仅 P1 型号)
tm.registerInitializeTask({
tag: 'battery-status',
task: async (jensen: Jensen | null, store) => {
if (jensen?.getModel() != 'hidock-p1') return;
let batteryStatus = await jensen?.getBatteryStatus(1);
Logger.info('jensen', 'initialize', 'get-battery-status: ' + JSON.stringify(batteryStatus));
// 存储电池状态到状态容器
if (batteryStatus) {
store.set('battery-status', batteryStatus);
}
}
});
// ===================== 定时任务 =====================
// 定时同步设备电池状态
tm.registerTimerTask({
tag: 'battery-status',
interval: 1000,
task: async (jensen: Jensen | null, store) => {
if (jensen?.getModel() != 'hidock-p1') return;
let batteryStatus = await jensen?.getBatteryStatus(1);
Logger.info('jensen', 'timer-task', 'battery: ' + JSON.stringify(batteryStatus));
// 更新电池状态(会自动触发订阅者)
if (batteryStatus) {
store.set('battery-status', batteryStatus);
}
}
});
// 定时获取设备时间
tm.registerTimerTask({
tag: 'get-device-time',
interval: 1000,
task: async (jensen: Jensen | null, store) => {
if (jensen?.getModel() != 'hidock-p1') return;
let time = await jensen?.getTime(1);
Logger.info('jensen', 'timer-task', 'time: ' + JSON.stringify(time));
// 更新设备时间
if (time) {
store.set('time', { deviceTime: time.time, syncTime: new Date() });
}
}
});
// 录音中的状态同步
tm.registerTimerTask({
tag: 'recording-status',
interval: 1000,
task: async (jensen: Jensen | null, store) => {
let recording = await jensen?.getRecordingFile();
Logger.info('jensen', 'timer-task', 'recording-status: ' + JSON.stringify(recording));
if (recording) store.set('recording', recording);
}
});
// 设备录音文件列表同步
tm.registerTimerTask({
tag: 'file-list-sync',
interval: 1000,
task: async (jensen: Jensen | null, store) => {
let needRefresh = false;
// 1. 定时查询文件数量
let fileCount = await jensen?.getFileCount(1);
if (fileCount)
{
let lastCount = store.get('file-count');
if (lastCount != fileCount.count)
{
needRefresh = true;
Logger.info('jensen', 'timer-task', 'file-count changed: ' + lastCount + ' -> ' + fileCount.count);
}
store.set('file-count', fileCount.count);
}
if (!needRefresh) return;
let files = await jensen?.listFiles();
if (files) store.set('files', files);
}
});
// ===================== 后台任务 =====================
// 如有需要可以在这里添加后台任务
tm.registerBackgroundTask({
tag: 'file-transfer',
// 注意:必须使用 function 而非箭头函数,才能正确绑定 this
schedule: async function (jensen, store) {
let data = store.peekTaskData('file-transfer') as FileTaskData | undefined;
if (!data) return;
console.log('xxxx', data);
// 通过 this 调用自定义方法
await this.transfer(jensen, data, store);
store.popTaskData('file-transfer');
// this.postProcess(data);
if (data.task == 'download')
{
this.download();
}
else if (data.task == 'transcribe')
{
this.transcribe();
}
else if (data.task == 'summarize')
{
this.summarize();
}
},
// 自定义方法:文件传输
transfer: async function (jensen: Jensen | null, data: FileTaskData, store: DeviceStateStore) {
if (!jensen) return false;
// 用 Promise 包装回调逻辑,等待所有数据块接收完成
const fileData = await new Promise<Uint8Array | null>((resolve) => {
const chunks: Uint8Array[] = [];
let receivedLength = 0;
jensen.getFile(data.fileName, data.fileLength, (chunk) => {
if (chunk === 'fail') {
console.error('文件传输失败');
resolve(null);
return;
}
// 收集数据块
chunks.push(chunk);
receivedLength += chunk.length;
console.log(`接收进度: ${receivedLength} / ${data.fileLength}`);
// 检查是否接收完成
if (receivedLength >= data.fileLength) {
// 合并所有数据块为一个完整的 Uint8Array
const fileData = new Uint8Array(receivedLength);
let offset = 0;
for (const chunk of chunks) {
fileData.set(chunk, offset);
offset += chunk.length;
}
console.log('文件传输完成,总大小:', receivedLength);
resolve(fileData);
}
});
});
if (fileData) {
// 将完整的文件数据存储到 store 中供后续处理使用
// store.set('transferredFile', { fileName: data.fileName, data: fileData });
return true;
}
return false;
},
download: async function () {
console.log('xxxx', 'download and save to local file');
},
transcribe: async function () {
console.log('xxxx', 'transcribe the file');
},
summarize: async function () {
console.log('xxxx', 'summarize the file');
}
});
}
...@@ -28,6 +28,8 @@ export class DeviceStateStore { ...@@ -28,6 +28,8 @@ export class DeviceStateStore {
private updateTimes: Map<string, number> = new Map(); private updateTimes: Map<string, number> = new Map();
// 状态变更监听器 // 状态变更监听器
private listeners: Map<string, StateChangeListener<any>[]> = new Map(); private listeners: Map<string, StateChangeListener<any>[]> = new Map();
// 后台任务队列:key 为任务 tag,value 为待处理数据数组
private taskQueues: Map<string, any[]> = new Map();
// 获取状态 // 获取状态
get<K extends keyof DeviceStateSchema>(key: K): DeviceStateSchema[K] | undefined { get<K extends keyof DeviceStateSchema>(key: K): DeviceStateSchema[K] | undefined {
...@@ -95,10 +97,129 @@ export class DeviceStateStore { ...@@ -95,10 +97,129 @@ export class DeviceStateStore {
} }
} }
// ===================== 后台任务队列操作方法 =====================
/**
* 向指定任务队列推入数据
* @param tag 任务标签(与后台任务的 tag 一致)
* @param data 要推入的数据
*/
pushTaskData(tag: string, data: any): void {
if (!this.taskQueues.has(tag)) {
this.taskQueues.set(tag, []);
}
this.taskQueues.get(tag)!.push(data);
}
/**
* 向指定任务队列批量推入数据
* @param tag 任务标签
* @param dataList 要推入的数据数组
*/
pushTaskDataBatch(tag: string, dataList: any[]): void {
if (!this.taskQueues.has(tag)) {
this.taskQueues.set(tag, []);
}
this.taskQueues.get(tag)!.push(...dataList);
}
/**
* 从指定任务队列弹出一个数据(FIFO,先进先出)
* @param tag 任务标签
* @returns 弹出的数据,如果队列为空则返回 undefined
*/
popTaskData(tag: string): any | undefined {
const queue = this.taskQueues.get(tag);
if (!queue || queue.length === 0) return undefined;
return queue.shift();
}
/**
* 从指定任务队列弹出所有数据
* @param tag 任务标签
* @returns 所有数据的数组,如果队列为空则返回空数组
*/
popAllTaskData(tag: string): any[] {
const queue = this.taskQueues.get(tag);
if (!queue || queue.length === 0) return [];
const result = [...queue];
queue.length = 0; // 清空队列
return result;
}
/**
* 查看指定任务队列的第一个数据(不弹出)
* @param tag 任务标签
* @returns 队列中的第一个数据,如果队列为空则返回 undefined
*/
peekTaskData(tag: string): any | undefined {
const queue = this.taskQueues.get(tag);
if (!queue || queue.length === 0) return undefined;
return queue[0];
}
/**
* 查看指定任务队列的所有数据(不弹出)
* @param tag 任务标签
* @returns 队列中所有数据的副本,如果队列为空则返回空数组
*/
peekAllTaskData(tag: string): any[] {
const queue = this.taskQueues.get(tag);
if (!queue) return [];
return [...queue];
}
/**
* 获取指定任务队列中的数据数量
* @param tag 任务标签
* @returns 队列中的数据数量
*/
getTaskDataCount(tag: string): number {
const queue = this.taskQueues.get(tag);
return queue ? queue.length : 0;
}
/**
* 检查指定任务队列是否有数据
* @param tag 任务标签
* @returns 如果队列有数据返回 true,否则返回 false
*/
hasTaskData(tag: string): boolean {
const queue = this.taskQueues.get(tag);
return queue !== undefined && queue.length > 0;
}
/**
* 清空指定任务队列
* @param tag 任务标签
*/
clearTaskData(tag: string): void {
const queue = this.taskQueues.get(tag);
if (queue) queue.length = 0;
}
/**
* 清空所有任务队列
*/
clearAllTaskData(): void {
this.taskQueues.clear();
}
/**
* 获取所有任务队列的 tag 列表
* @returns 所有任务队列的 tag 数组
*/
getTaskQueueTags(): string[] {
return Array.from(this.taskQueues.keys());
}
// ===================== 状态管理方法 =====================
// 清空所有状态 // 清空所有状态
clear(): void { clear(): void {
this.state = {}; this.state = {};
this.updateTimes.clear(); this.updateTimes.clear();
this.taskQueues.clear(); // 同时清空任务队列
// 不清除监听器,允许重连后继续监听 // 不清除监听器,允许重连后继续监听
} }
...@@ -108,12 +229,19 @@ export class DeviceStateStore { ...@@ -108,12 +229,19 @@ export class DeviceStateStore {
} }
// 导出状态快照(用于调试) // 导出状态快照(用于调试)
dump(): { state: Partial<DeviceStateSchema>; updateTimes: Record<string, number> } { dump(): {
state: Partial<DeviceStateSchema>;
updateTimes: Record<string, number>;
taskQueues: Record<string, any[]>;
} {
const times: Record<string, number> = {}; const times: Record<string, number> = {};
this.updateTimes.forEach((v, k) => times[k] = v); this.updateTimes.forEach((v, k) => times[k] = v);
const queues: Record<string, any[]> = {};
this.taskQueues.forEach((v, k) => queues[k] = [...v]);
return { return {
state: { ...this.state }, state: { ...this.state },
updateTimes: times updateTimes: times,
taskQueues: queues
}; };
} }
} }
...@@ -135,14 +263,11 @@ export type TimerTask = { ...@@ -135,14 +263,11 @@ export type TimerTask = {
export type BackgroundTask = { export type BackgroundTask = {
tag: string; tag: string;
// 用于任务调度 // 用于任务调度,可通过 this 调用同级定义的其它方法(注意:必须使用 function 而非箭头函数)
schedule: (jensen: Jensen | null, store: DeviceStateStore) => Promise<any>; schedule: (this: BackgroundTask, jensen: Jensen | null, store: DeviceStateStore) => Promise<any>;
// 用于设备交互使用 lastExecuteTime?: number;
deviceTask?: (jensen: Jensen | null, store: DeviceStateStore) => Promise<any>; // 允许定义任意其它方法/属性
// 任务善后处理调用 [key: string]: any;
generalTask?: (jensen: Jensen | null, store: DeviceStateStore) => Promise<any>;
lastExecuteTime: number;
interval: number; // 轮询间隔
} }
export type UserTask = { export type UserTask = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment