UNPKG

camera-serial-utils

Version:

A utility package for camera capture and serial communication using Web Serial API

210 lines (179 loc) 7.37 kB
/** * Web Serial API 串口通信工具类(Promise方案) */ class SerialPortHelper { constructor() { this.port = null; this.reader = null; this.writer = null; this.isPortOpen = false; } /** * 打开串口 * @param {Object} options { baudRate: number, portName?: string } * @returns {Promise<void>} */ async open() { if (!('serial' in navigator)) { throw new Error('浏览器不支持Web Serial API'); } try { this.port = await navigator.serial.requestPort(); await this.port.open({ baudRate:38400 }); this.isPortOpen = true; this.reader = this.port.readable.getReader(); this.writer = this.port.writable.getWriter(); } catch (error) { await this.close(); throw new Error(`打开串口失败: ${error.message}`); } } /** * 关闭串口 * @returns {Promise<void>} */ async close() { try { if (this.reader) { await this.reader.cancel().catch(() => {}); this.reader = null; } if (this.writer) { await this.writer.releaseLock(); this.writer = null; } if (this.port) { await this.port.close(); this.port = null; } this.isPortOpen = false; } catch (error) { throw new Error(`关闭串口失败: ${error.message}`); } } /** * 发送数据 * @param {string|Uint8Array} data 16进制字符串或Uint8Array * @returns {Promise<void>} */ async send(data='FA0D0A') { if (!this.isPortOpen) throw new Error('串口未打开'); try { const dataArray = typeof data === 'string' ? this.hexStringToUint8Array(data) : data; await this.writer.write(dataArray); } catch (error) { throw new Error(`发送失败: ${error.message}`); } } /** * 读取数据(直到检测到结束标记或超时) * @param {Object} options { endMarker?: string, timeout?: number } * @returns {Promise<string>} 16进制格式数据 */ async read(options = {}) { const { endMarker = 'aaaaf5', timeout = 1400000 } = options; if (!this.isPortOpen) throw new Error('串口未打开'); if (!this.reader) throw new Error('读取器未初始化'); const markerArray = this.hexStringToUint8Array(endMarker); let receivedData = new Uint8Array(0); let timeoutId; let buffer = new Uint8Array(0); // 用于存储可能跨数据块的部分 try { // 超时控制 const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`读取超时(${timeout}ms)`)); }, timeout); }); // 实际读取逻辑 const readPromise = (async () => { while (true) { const { value, done } = await this.reader.read(); if (done) break; if (value?.length > 0) { // 将新数据与之前未处理完的buffer拼接 const combined = new Uint8Array(buffer.length + value.length); combined.set(buffer); combined.set(value, buffer.length); // 检查组合后的数据是否包含结束标记 const hexData = this.arrayToHexString(combined); const markerIndex = hexData.indexOf(endMarker); if (markerIndex > -1) { // 找到结束标记,计算实际字节位置 const bytePosition = Math.floor(markerIndex / 2); const endPosition = bytePosition + markerArray.length; // 拼接所有数据:之前接收的 + 当前数据(直到结束标记) const finalData = new Uint8Array(receivedData.length + endPosition); finalData.set(receivedData); finalData.set(combined.slice(0, endPosition), receivedData.length); receivedData = finalData; break; } else { // 未找到结束标记,保留可能包含部分标记的尾部数据 const keepLength = Math.max(0, combined.length - markerArray.length + 1); buffer = combined.slice(combined.length - keepLength); // 将确定不包含标记的部分加入receivedData const safeData = combined.slice(0, combined.length - keepLength); const newReceived = new Uint8Array(receivedData.length + safeData.length); newReceived.set(receivedData); newReceived.set(safeData, receivedData.length); receivedData = newReceived; } } } return this.arrayToHexString(receivedData); })(); return await Promise.race([readPromise, timeoutPromise]); } finally { if (timeoutId) clearTimeout(timeoutId); } } // -------------------- 工具方法 -------------------- /** * 检查结束标记(改进版) * @param {Uint8Array} data 已接收数据 * @param {Uint8Array} marker 结束标记 */ checkEndMarker(data, marker) { if (data.length < marker.length) return false; // 从数据末尾向前匹配标记 const startPos = data.length - marker.length; for (let i = 0; i < marker.length; i++) { if (data[startPos + i] !== marker[i]) { return false; } } return true; } /** * 16进制字符串转Uint8Array * @param {string} hexString 如 "FA0D0A" */ hexStringToUint8Array(hexString) { const cleaned = hexString.replace(/\s/g, ''); if (!/^[0-9A-Fa-f]*$/.test(cleaned)) { throw new Error('无效的16进制字符串'); } if (cleaned.length % 2 !== 0) { throw new Error('16进制字符串长度必须是偶数'); } const array = new Uint8Array(cleaned.length / 2); for (let i = 0; i < cleaned.length; i += 2) { array[i / 2] = parseInt(cleaned.substr(i, 2), 16); } return array; } /** * Uint8Array转16进制字符串 * @param {Uint8Array} array */ arrayToHexString(array) { return Array.from(array) .map(b => b.toString(16).padStart(2, '0')) .join(''); } } // 导出单例 export const serialHelper = new SerialPortHelper();