JS `Web Serial API` `Data Flow Control` (`RTS/CTS`, `XON/XOFF`) 与错误处理

各位观众老爷,大家好!今天咱们来聊聊 Web Serial API 里的那些“弯弯绕”,特别是关于数据流控制和错误处理,保证让大家听得明白,用得溜溜的!

开场白:串行通信的“前世今生”

在 USB 满地跑的今天,你可能觉得串口通信是个古董。但别忘了,嵌入式系统、物联网设备、某些工业控制领域,串口依然坚挺!而且,通过 Web Serial API,咱们也能在浏览器里直接和这些“老朋友”打交道,是不是感觉瞬间“文艺复兴”了?

Web Serial API 快速回顾

先简单回顾一下 Web Serial API 的基本流程:

  1. 请求端口: navigator.serial.requestPort() 获得用户授权,拿到 SerialPort 对象。
  2. 打开端口: port.open(options) 设置波特率、数据位、停止位等参数,建立连接。
  3. 读写数据: 通过 port.readableport.writable 获取 ReadableStreamWritableStream,进行数据收发。
  4. 关闭端口: port.close() 断开连接,释放资源。

数据流控制:让数据“井然有序”

数据流控制,顾名思义,就是控制数据传输的节奏,防止发送方“一股脑”地把数据塞给接收方,导致接收方处理不过来,造成数据丢失。Web Serial API 提供了两种主要的数据流控制方式:

  • 硬件流控制 (RTS/CTS): 通过 RTS (Request To Send) 和 CTS (Clear To Send) 两根信号线来实现。
  • 软件流控制 (XON/XOFF): 通过特定的字符 (XON 和 XOFF) 来实现。

1. 硬件流控制 (RTS/CTS)

想象一下,RTS 就像发送方举起的小旗子,CTS 就像接收方回应的小旗子。

  • RTS: 发送方想要发送数据时,将 RTS 信号线置为有效状态(通常是低电平)。
  • CTS: 接收方准备好接收数据时,将 CTS 信号线置为有效状态。只有在 CTS 有效时,发送方才能发送数据。

代码示例:启用 RTS/CTS

async function connectSerialPort() {
  try {
    const port = await navigator.serial.requestPort();
    await port.open({
      baudRate: 115200,
      flowControl: "hardware" // 启用硬件流控制
    });

    console.log("Serial port opened with hardware flow control.");

    // 读写数据的代码 (稍后补充)

  } catch (error) {
    console.error("Error opening serial port:", error);
  }
}

connectSerialPort();

RTS/CTS 的优缺点

特点 优点 缺点
实现方式 硬件信号线 需要额外的硬件线路支持
效率 硬件故障会导致流控失效
适用场景 对实时性要求高,且硬件支持 RTS/CTS 的场合 不适用于只有数据线的情况

2. 软件流控制 (XON/XOFF)

XON/XOFF 就像发送方和接收方之间的暗号。

  • XOFF (Transmit Off): 当接收方缓冲区快满时,发送 XOFF 字符(通常是 0x13,即 Ctrl+S)给发送方,要求停止发送数据。
  • XON (Transmit On): 当接收方缓冲区有空闲空间时,发送 XON 字符(通常是 0x11,即 Ctrl+Q)给发送方,允许继续发送数据。

代码示例:实现 XON/XOFF

这个例子展示了如何在接收端实现XON/XOFF。发送端需要根据接收到的XON/XOFF字符来停止或继续发送数据。

async function connectSerialPort() {
  try {
    const port = await navigator.serial.requestPort();
    await port.open({
      baudRate: 115200,
      // 注意:Web Serial API 本身不直接提供 XON/XOFF 流控制的选项。
      // 需要我们自己手动实现。
    });

    console.log("Serial port opened.");

    const reader = port.readable.getReader();
    const writer = port.writable.getWriter();

    const XON = 0x11; // Ctrl+Q
    const XOFF = 0x13; // Ctrl+S
    const BUFFER_SIZE = 256;
    let buffer = new Uint8Array(BUFFER_SIZE);
    let bufferIndex = 0;
    let isTransmitting = true;

    while (true) {
      try {
        const { value, done } = await reader.read();
        if (done) {
          console.log("Reader done.");
          break;
        }

        for (let i = 0; i < value.length; i++) {
          if (value[i] === XON) {
            console.log("Received XON, resuming transmission.");
            isTransmitting = true;
          } else if (value[i] === XOFF) {
            console.log("Received XOFF, pausing transmission.");
            isTransmitting = false;
          } else {
            if (isTransmitting) {
                buffer[bufferIndex++] = value[i];
                if (bufferIndex >= BUFFER_SIZE - 10) { // 预留一些空间
                    console.log("Buffer almost full, sending XOFF.");
                    await writer.write(new Uint8Array([XOFF]));
                    // 实际应用中,这里应该更谨慎地处理,
                    // 例如,等待发送 XOFF 完成后再暂停。
                }
                if (bufferIndex >= BUFFER_SIZE) {
                    //处理缓冲区数据
                    console.log("Processing buffer data:", new TextDecoder().decode(buffer));
                    bufferIndex = 0; // 重置缓冲区
                    await writer.write(new Uint8Array([XON]));//发送XON 恢复发送
                }
            }
          }
        }
      } catch (error) {
        console.error("Error reading from serial port:", error);
        break;
      }
    }

    reader.releaseLock();
    writer.releaseLock();
    await port.close();

  } catch (error) {
    console.error("Error opening serial port:", error);
  }
}

connectSerialPort();

XON/XOFF 的优缺点

特点 优点 缺点
实现方式 软件字符 效率相对较低,因为需要额外传输控制字符
效率 如果数据中包含和 XON/XOFF 相同的字符,可能会导致误判(需要转义处理)
适用场景 硬件不支持 RTS/CTS,或者只有数据线的情况下 对实时性要求不高的场合

数据流控制的“最佳实践”

  • 优先选择硬件流控制: 如果硬件支持,RTS/CTS 效率更高,更可靠。
  • 软件流控制的转义: 如果使用 XON/XOFF,一定要注意数据中可能包含和 XON/XOFF 相同的字符,需要进行转义处理,避免误判。
  • 缓冲区管理: 无论是硬件流控制还是软件流控制,都需要合理管理缓冲区的大小,避免溢出。

错误处理:防患于未然

在使用 Web Serial API 的过程中,可能会遇到各种各样的错误,例如:

  • 端口不存在或被占用: 用户可能拔掉了串口设备,或者有其他程序正在使用该端口。
  • 参数错误: 波特率设置错误,或者数据位、停止位等参数不匹配。
  • 读写错误: 串口通信过程中出现数据损坏或丢失。

错误处理的“正确姿势”

  1. try...catch 块: 将可能出错的代码放在 try 块中,使用 catch 块捕获异常。

    try {
      // 可能出错的代码
      await port.open({ baudRate: 9600 });
    } catch (error) {
      console.error("Error opening port:", error);
      // 处理错误
    }
  2. 检查 port.opened 属性: 在进行读写操作之前,先检查 port.opened 属性,确保端口已经打开。

    if (port.opened) {
      // 读写数据
    } else {
      console.error("Port is not opened.");
    }
  3. 监听 disconnect 事件: 当串口设备断开连接时,会触发 disconnect 事件。可以监听该事件,及时释放资源。

    navigator.serial.addEventListener('disconnect', (event) => {
      console.log("Serial port disconnected.");
      // 清理资源
    });
  4. 处理 ReadableStreamWritableStream 的错误: ReadableStreamWritableStream 也可能出现错误,需要在读取和写入数据时进行处理。

    const reader = port.readable.getReader();
    try {
      while (true) {
        const { value, done } = await reader.read();
        if (done) {
          break;
        }
        // 处理数据
      }
    } catch (error) {
      console.error("Error reading data:", error);
    } finally {
      reader.releaseLock();
    }
  5. 用户友好的错误提示: 不要直接把错误信息显示给用户,而是提供更友好的提示,例如:“串口设备未连接”、“参数设置错误”等。

代码示例:完整的错误处理

async function connectSerialPort() {
  let port;
  try {
    port = await navigator.serial.requestPort();

    port.addEventListener('disconnect', () => {
      console.log("Serial port disconnected.");
      // 清理资源,例如关闭 reader 和 writer
      if (reader) {
        reader.releaseLock();
      }
      if (writer) {
        writer.releaseLock();
      }
    });

    await port.open({ baudRate: 115200 });
    console.log("Serial port opened.");

    const reader = port.readable.getReader();
    const writer = port.writable.getWriter();

    try {
      while (port.readable) { // 确保端口仍然是可读的
        try {
          const { value, done } = await reader.read();
          if (done) {
            console.log("Reader done.");
            break;
          }
          console.log("Received:", new TextDecoder().decode(value));
          // 这里可以添加数据处理逻辑
        } catch (readError) {
          console.error("Error reading from serial port:", readError);
          break; // 退出读取循环
        }
      }
    } catch (streamError) {
      console.error("Error with stream:", streamError);
    } finally {
      reader.releaseLock();
      writer.releaseLock();
      await port.close();
      console.log("Serial port closed.");
    }

  } catch (error) {
    console.error("Error opening serial port:", error);
    // 给用户友好的提示
    if (error.message.includes("No serial port selected")) {
      alert("请选择一个串口设备。");
    } else if (error.message.includes("Failed to open serial port")) {
      alert("无法打开串口,可能被其他程序占用。");
    } else {
      alert("发生未知错误:" + error.message);
    }
  }
}

connectSerialPort();

调试技巧:让问题“无处遁形”

  1. 使用串口调试助手: 在电脑上安装串口调试助手,例如 Serial Monitor、Putty 等,可以方便地查看串口数据,排查问题。
  2. 打印调试信息: 在代码中添加 console.log() 语句,打印关键变量的值,例如接收到的数据、缓冲区状态等。
  3. 使用 Chrome DevTools: Chrome DevTools 提供了强大的调试功能,可以查看网络请求、控制台输出、断点调试等。

总结:掌握 Web Serial API 的“精髓”

Web Serial API 为我们在浏览器中访问串口设备提供了强大的能力。通过合理地使用数据流控制和错误处理,可以构建稳定可靠的串口通信应用。记住,调试是程序员的“家常便饭”,遇到问题不要慌,冷静分析,总能找到解决方案!

这次的“串行通信之旅”就到这里了。希望大家有所收获,在 Web Serial API 的世界里“玩”得开心!

补充说明:

  1. 浏览器的兼容性: Web Serial API 的兼容性还在不断完善中,使用前请查阅相关文档,确认目标浏览器是否支持。
  2. 安全性: Web Serial API 需要用户授权才能访问串口设备,确保安全性。
  3. 实际应用: Web Serial API 可以应用于各种场景,例如:

    • Web 控制台: 通过串口连接嵌入式设备,实现远程控制和调试。
    • 数据采集: 从传感器读取数据,实时显示在网页上。
    • 固件升级: 通过串口更新设备的固件。

最后,祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注