camera-serial-utils
Version:
A utility package for camera capture and serial communication using Web Serial API
210 lines (179 loc) • 7.37 kB
JavaScript
/**
* 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();