/** * 自然写互动课堂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'; /** 笔迹数据特征UUID(Notify) */ const BLE_STROKE_CHAR_UUID = '0000ffe1-0000-1000-8000-00805f9b34fb'; /** 电量特征UUID */ const BLE_BATTERY_CHAR_UUID = '0000ffe2-0000-1000-8000-00805f9b34fb'; /** 控制特征UUID(Write) */ 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 = new Map(); /** 事件回调 */ private callbacks: DeviceEventCallbacks; /** USB轮询定时器 */ private usbPollTimer: ReturnType | 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 { 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 };