software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,425 @@
/**
* 自然写互动课堂PC端应用软件 V1.0
*
* device_manager.ts - USB/BLE设备管理
*
* 功能说明:
* - USB HID点阵笔连接管理
* - BLE蓝牙点阵笔扫描与连接
* - 设备数据解析(7字节紧凑坐标解码)
* - 设备热插拔监听
* - 多设备并行管理
*/
/* ======================== 类型定义 ======================== */
/** 设备连接方式 */
enum DeviceInterface {
USB_HID = 'usb',
BLE = 'ble'
}
/** 设备状态 */
enum DeviceStatus {
DISCONNECTED = 'disconnected',
CONNECTING = 'connecting',
CONNECTED = 'connected',
ERROR = 'error'
}
/** 点阵笔设备信息 */
interface PenDevice {
id: string; /* 设备唯一ID */
name: string; /* 设备名称 */
macAddress: string; /* MAC地址 */
interface: DeviceInterface; /* 连接方式 */
status: DeviceStatus; /* 连接状态 */
battery: number; /* 电量百分比 */
firmwareVersion: string; /* 固件版本 */
lastConnected: number; /* 最后连接时间戳 */
}
/** 笔迹坐标点 */
interface StrokePoint {
x: number; /* X坐标(毫米) */
y: number; /* Y坐标(毫米) */
pressure: number; /* 压力值(0-1) */
timestamp: number; /* 时间戳(毫秒) */
penDown: boolean; /* 落笔标志 */
}
/** 设备事件回调 */
interface DeviceEventCallbacks {
onDeviceDiscovered: (device: PenDevice) => void;
onDeviceConnected: (device: PenDevice) => void;
onDeviceDisconnected: (deviceId: string) => void;
onStrokeData: (deviceId: string, points: StrokePoint[]) => void;
onBatteryUpdate: (deviceId: string, level: number) => void;
onError: (deviceId: string, error: string) => void;
}
/* ======================== USB HID常量 ======================== */
/** 自然写点阵笔USB VendorID */
const WRITECH_USB_VID = 0x1234;
/** 自然写点阵笔USB ProductID */
const WRITECH_USB_PID = 0x5678;
/** USB HID报文最大长度 */
const USB_REPORT_SIZE = 64;
/** USB轮询间隔(毫秒) */
const USB_POLL_INTERVAL = 5;
/* ======================== BLE常量 ======================== */
/** 自然写笔迹服务UUID */
const BLE_SERVICE_UUID = '0000ffe0-0000-1000-8000-00805f9b34fb';
/** 笔迹数据特征UUIDNotify */
const BLE_STROKE_CHAR_UUID = '0000ffe1-0000-1000-8000-00805f9b34fb';
/** 电量特征UUID */
const BLE_BATTERY_CHAR_UUID = '0000ffe2-0000-1000-8000-00805f9b34fb';
/** 控制特征UUIDWrite */
const BLE_CONTROL_CHAR_UUID = '0000ffe3-0000-1000-8000-00805f9b34fb';
/* ======================== 坐标解码 ======================== */
/**
* 解码7字节紧凑坐标编码
* 编码格式: 20位X + 20位Y + 12位压力 + 4位标志
*/
function decodeCompactPoint(data: Buffer, offset: number): StrokePoint {
/* 提取20位X坐标 */
const rawX = (data[offset] << 12) |
(data[offset + 1] << 4) |
((data[offset + 2] >> 4) & 0x0F);
/* 提取20位Y坐标 */
const rawY = ((data[offset + 2] & 0x0F) << 16) |
(data[offset + 3] << 8) |
data[offset + 4];
/* 提取12位压力值 */
const rawPressure = (data[offset + 5] << 4) |
((data[offset + 6] >> 4) & 0x0F);
/* 提取4位标志 */
const flags = data[offset + 6] & 0x0F;
return {
x: rawX * 0.3, /* 点阵码单位转毫米 */
y: rawY * 0.3,
pressure: rawPressure / 4095, /* 归一化到0-1 */
timestamp: Date.now(),
penDown: (flags & 0x01) !== 0
};
}
/**
* 计算CRC-16 CCITT校验
*/
function crc16CCITT(data: Buffer, length: number): number {
let crc = 0xFFFF;
for (let i = 0; i < length; i++) {
crc ^= data[i] << 8;
for (let j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = ((crc << 1) ^ 0x1021) & 0xFFFF;
} else {
crc = (crc << 1) & 0xFFFF;
}
}
}
return crc;
}
/* ======================== 设备管理器 ======================== */
/**
* 点阵笔设备管理器
* 统一管理USB和BLE连接的点阵笔设备
*/
class DeviceManager {
/** 已连接设备列表 */
private devices: Map<string, PenDevice> = new Map();
/** 事件回调 */
private callbacks: DeviceEventCallbacks;
/** USB轮询定时器 */
private usbPollTimer: ReturnType<typeof setInterval> | null = null;
/** BLE扫描状态 */
private bleScanning: boolean = false;
/** 是否运行中 */
private running: boolean = false;
constructor(callbacks: DeviceEventCallbacks) {
this.callbacks = callbacks;
console.log('[设备管理] 初始化');
}
/* ==================== USB HID管理 ==================== */
/**
* 启动USB设备监听
* 使用node-usb库检测设备热插拔
*/
startUSBMonitor(): void {
console.log('[设备管理] 启动USB监听');
this.running = true;
/* 枚举已连接的USB设备 */
this.scanUSBDevices();
/* 监听USB热插拔事件
usb.on('attach', (device) => this.onUSBAttach(device));
usb.on('detach', (device) => this.onUSBDetach(device)); */
/* 启动USB数据轮询 */
this.usbPollTimer = setInterval(() => {
this.pollUSBData();
}, USB_POLL_INTERVAL);
}
/**
* 扫描已连接的USB HID设备
*/
private scanUSBDevices(): void {
/* const devices = HID.devices()
.filter(d => d.vendorId === WRITECH_USB_VID &&
d.productId === WRITECH_USB_PID); */
console.log('[设备管理] USB扫描完成');
}
/**
* USB设备接入处理
*/
private onUSBAttach(usbDevice: any): void {
const deviceId = `usb_${usbDevice.serialNumber || Date.now()}`;
const pen: PenDevice = {
id: deviceId,
name: `WritechPen-USB-${deviceId.slice(-4)}`,
macAddress: '',
interface: DeviceInterface.USB_HID,
status: DeviceStatus.CONNECTED,
battery: 100,
firmwareVersion: '1.0.0',
lastConnected: Date.now()
};
this.devices.set(deviceId, pen);
this.callbacks.onDeviceConnected(pen);
console.log(`[设备管理] USB设备接入: ${pen.name}`);
}
/**
* USB设备拔出处理
*/
private onUSBDetach(usbDevice: any): void {
const deviceId = `usb_${usbDevice.serialNumber || ''}`;
if (this.devices.has(deviceId)) {
this.devices.delete(deviceId);
this.callbacks.onDeviceDisconnected(deviceId);
console.log(`[设备管理] USB设备断开: ${deviceId}`);
}
}
/**
* 轮询USB设备数据
* 读取HID报文并解析坐标
*/
private pollUSBData(): void {
this.devices.forEach((device, deviceId) => {
if (device.interface !== DeviceInterface.USB_HID) return;
if (device.status !== DeviceStatus.CONNECTED) return;
/* const report = hidDevice.readSync();
if (report && report.length > 0) {
this.parseUSBReport(deviceId, Buffer.from(report));
} */
});
}
/**
* 解析USB HID报文
* 报文格式: [报文类型][数据长度][坐标数据...]
*/
private parseUSBReport(deviceId: string, report: Buffer): void {
const reportType = report[0];
const dataLen = report[1];
if (reportType === 0x01) {
/* 笔迹数据报文: 每11字节一个坐标点(7字节坐标+4字节时间戳) */
const points: StrokePoint[] = [];
const pointSize = 11;
for (let offset = 2; offset + pointSize <= 2 + dataLen; offset += pointSize) {
const point = decodeCompactPoint(report, offset);
/* 时间戳从报文中提取 */
point.timestamp = report.readUInt32LE(offset + 7);
points.push(point);
}
if (points.length > 0) {
this.callbacks.onStrokeData(deviceId, points);
}
} else if (reportType === 0x04) {
/* 电量报文 */
const battery = report[2];
this.callbacks.onBatteryUpdate(deviceId, battery);
}
}
/* ==================== BLE管理 ==================== */
/**
* 启动BLE蓝牙扫描
*/
startBLEScan(): void {
if (this.bleScanning) return;
console.log('[设备管理] 启动BLE扫描');
this.bleScanning = true;
/* noble.on('discover', (peripheral) => {
if (peripheral.advertisement.localName?.startsWith('WritechPen')) {
this.onBLEDiscover(peripheral);
}
});
noble.startScanning([BLE_SERVICE_UUID], true); */
}
/**
* 停止BLE扫描
*/
stopBLEScan(): void {
this.bleScanning = false;
/* noble.stopScanning(); */
console.log('[设备管理] BLE扫描已停止');
}
/**
* BLE设备发现回调
*/
private onBLEDiscover(peripheral: any): void {
const deviceId = `ble_${peripheral.address.replace(/:/g, '')}`;
if (this.devices.has(deviceId)) return;
const pen: PenDevice = {
id: deviceId,
name: peripheral.advertisement.localName || 'WritechPen',
macAddress: peripheral.address,
interface: DeviceInterface.BLE,
status: DeviceStatus.DISCONNECTED,
battery: 0,
firmwareVersion: '',
lastConnected: 0
};
this.callbacks.onDeviceDiscovered(pen);
console.log(`[设备管理] 发现BLE设备: ${pen.name} [${pen.macAddress}]`);
}
/**
* 连接BLE设备
*/
async connectBLE(deviceId: string): Promise<boolean> {
const device = this.devices.get(deviceId);
if (!device || device.interface !== DeviceInterface.BLE) {
return false;
}
device.status = DeviceStatus.CONNECTING;
console.log(`[设备管理] 连接BLE设备: ${device.name}`);
try {
/* peripheral.connect((err) => { ... });
peripheral.discoverServices([BLE_SERVICE_UUID], (err, services) => {
services[0].discoverCharacteristics([...], (err, chars) => {
// 订阅笔迹数据Notify
strokeChar.subscribe();
strokeChar.on('data', (data) => this.onBLEData(deviceId, data));
});
}); */
device.status = DeviceStatus.CONNECTED;
device.lastConnected = Date.now();
this.devices.set(deviceId, device);
this.callbacks.onDeviceConnected(device);
return true;
} catch (err: any) {
device.status = DeviceStatus.ERROR;
this.callbacks.onError(deviceId, err.message);
return false;
}
}
/**
* BLE数据接收回调
*/
private onBLEData(deviceId: string, data: Buffer): void {
/* BLE数据帧格式与USB类似:[帧头0xAA][类型][长度][数据...][CRC16] */
if (data[0] !== 0xAA) return;
const frameType = data[1];
const payloadLen = data[2];
/* CRC校验 */
const expectedCrc = data.readUInt16LE(3 + payloadLen);
const calcCrc = crc16CCITT(data.slice(0, 3 + payloadLen), 3 + payloadLen);
if (expectedCrc !== calcCrc) {
console.warn(`[设备管理] BLE数据CRC校验失败: ${deviceId}`);
return;
}
if (frameType === 0x01) {
/* 笔迹坐标数据 */
const points: StrokePoint[] = [];
const pointSize = 11;
for (let i = 3; i + pointSize <= 3 + payloadLen; i += pointSize) {
points.push(decodeCompactPoint(data, i));
}
if (points.length > 0) {
this.callbacks.onStrokeData(deviceId, points);
}
} else if (frameType === 0x04) {
/* 电量数据 */
this.callbacks.onBatteryUpdate(deviceId, data[3]);
}
}
/* ==================== 公共接口 ==================== */
/** 获取所有已连接设备 */
getConnectedDevices(): PenDevice[] {
return Array.from(this.devices.values())
.filter(d => d.status === DeviceStatus.CONNECTED);
}
/** 获取设备数量 */
getDeviceCount(): number {
return this.devices.size;
}
/** 断开指定设备 */
disconnect(deviceId: string): void {
const device = this.devices.get(deviceId);
if (device) {
device.status = DeviceStatus.DISCONNECTED;
this.callbacks.onDeviceDisconnected(deviceId);
console.log(`[设备管理] 断开设备: ${device.name}`);
}
}
/** 停止所有设备管理 */
shutdown(): void {
this.running = false;
if (this.usbPollTimer) {
clearInterval(this.usbPollTimer);
}
this.stopBLEScan();
this.devices.clear();
console.log('[设备管理] 已关闭');
}
}
export { DeviceManager, PenDevice, StrokePoint, DeviceStatus, DeviceInterface };