使用 Web Serial API 在浏览器中实现串口通讯(纯前端)

前天 1223阅读

文章目录

  • 目的
  • 相关资料
  • 使用说明
  • 代码与演示
  • 总结

    目的

    串口是非常常用的一种电脑与设备交互的接口。目前在浏览器上直接使用电脑上的串口设备了,这篇文章将介绍相关内容。

    相关资料

    Web Serial API 相关内容参考如下:

    https://developer.mozilla.org/en-US/docs/Web/API/Serial

    https://developer.mozilla.org/en-US/docs/Web/API/SerialPort

    https://wicg.github.io/serial/

    这个API目前还处于实验性质,只有电脑上的Chrome、Edge、Opera等浏览器支持:

    使用 Web Serial API 在浏览器中实现串口通讯(纯前端)

    另外还需要注意的是从网页操作设备是比较容易产生安全风险的,所以这个API只支持本地调用或者是HTTPS方式调用。

    对于这个API谷歌有提供示例工程:

    在线使用:https://googlechromelabs.github.io/serial-terminal/

    项目地址:https://github.com/GoogleChromeLabs/serial-terminal

    下面这个项目做的挺不错的,直接拿来用也很好:

    在线使用:https://itldg.github.io/web-serial-debug/

    项目地址:https://gitee.com/itldg/web-serial-debug or https://github.com/itldg/web-serial-debug

    使用 Web Serial API 在浏览器中实现串口通讯(纯前端)

    使用说明

    使用下面方法可以侦测电脑上串口设备插入与拔出:

    // 全局串口设备插入事件
    navigator.serial.onconnect = (event) => {
        console.log("Serial connected: ", event.target);
    };
    // 全局串口设备拔出事件
    navigator.serial.ondisconnect = (event) => {
        console.log("Serial disconnected: ", event.target);
    };
    // 也可以对单个的串口设备设置插入与拔出事件
    

    使用下面方法可以显示电脑上的串口设备选择授权,或者显示已授权的串口设备列表:

    // requestPort方法将显示一个包含已连接设备列表的对话框,用户选择可以并授予其中一个设备访问权限
    // 对于USB虚拟串口而言该方法还可以传入一个过滤器,指定PID&VID的串口
    const port = await navigator.serial.requestPort();
    // port.forget(); // 取消授权
    // port.getInfo() // 获取PID&VID (对于蓝牙串口好像是显示服务号)
    // getDevices方法可以返回已连接的授权过的设备列表
    const ports = await navigator.serial.getPorts();
    

    使用 open 方法打开选中的串口设备后就可以进行数据交互了:

    // open时可以传入串口参数
    await port.open({
        baudRate: 115200,
        // bufferSize: 255,   // 读写缓存,默认255
        // dataBits: 8,       // 数据位,默认8
        // flowControl: none, // 流控制,默认无
        // parity: none,      // 校验,默认无
        // stopBits: 1,       // 停止位,默认1
    });
    

    打开后就可以发送数据了:

    const encoder = new TextEncoder();
    // const data= new Uint8Array(length);
    const writer = port.writable.getWriter();
    await writer.write(encoder.encode("PING"));
    // await writer.write(data);
    writer.releaseLock();
    

    同样可以设置数据接收:

    while (port.readable) {
      const reader = port.readable.getReader();
      try {
        while (true) {
          const { value, done } = await reader.read();
          if (done) {
            // |reader| has been canceled.
            break;
          }
          // Do something with |value|…
        }
      } catch (error) {
        // Handle |error|…
      } finally {
        reader.releaseLock();
      }
    }
    

    数据接收本身很简单,但需要注意的是在关闭串口前需要释放 reader 对象。

    下面是关闭串口操作:

    // 使用 await port.close(); 即可关闭串口,如果正在读写数据,需要先释放相关资源
    let keepReading = true;
    let reader;
    async function readUntilClosed() {
      while (port.readable && keepReading) {
        reader = port.readable.getReader();
        try {
          while (true) {
            const { value, done } = await reader.read();
            if (done) {
              // |reader| has been canceled.
              break;
            }
            // Do something with |value|...
          }
        } catch (error) {
          // Handle |error|...
        } finally {
          reader.releaseLock();
        }
      }
      await port.close();
    }
    const closed = readUntilClosed();
    // Sometime later...
    keepReading = false;
    reader.cancel();
    await closed;
    

    除了上面内容外还可以使用 setSignals 和 getSignals 来设置和获取流控制情况。

    代码与演示

    
    
        
        
        Web Serial API Test
        
            * {
                margin: 0;
                padding: 0;
            }
            button,textarea {
                margin: 1rem;
                margin-bottom: 0;
                padding: 0.5rem;
                width: 20rem;
            }
            textarea {
                resize: none;
                overflow-y: scroll;
                overflow-x: hidden;
                height: 5rem;
            }
        
        
            if ("serial" in navigator) {
                // alert("Your browser support Web Serial API."); // 浏览器不支持 Web Serial API
            } else {
                alert("Your browser is not support Web Serial API.");
            }
            // 全局串口设备插入事件
            navigator.serial.onconnect = (event) => {
                console.log("Serial port connected: ", event.target);
            };
            // 全局串口设备拔出事件
            navigator.serial.ondisconnect = (event) => {
                console.log("Serial port disconnected: ", event.target);
            };
        
    
    
        select
    open
    close
    send
    D0 D1 D2 D3 D4 D5 D6 D7
    const btnSelect = document.querySelector("#btnSelect"); const btnOpen = document.querySelector("#btnOpen"); const btnClose = document.querySelector("#btnClose"); const btnSend = document.querySelector("#btnSend"); const iptOutput = document.querySelector("#iptOutput"); const iptInput = document.querySelector("#iptInput"); let port = null; let reader = null; let reading = false; // 选择串口 btnSelect.onclick = async () => { try { port = await navigator.serial.requestPort(); // 弹出系统串口列表对话框,选择一个串口进行连接 let ports = await navigator.serial.getPorts(); // 获取已连接的授权过的设备列表 console.log(ports); // await port.forget(); // 取消授权 // console.log(port.getInfo()); // 打印PID&VID (对于蓝牙串口好像是显示服务号) } catch (e) { console.log(e); // The prompt has been dismissed without selecting a device. } }; function updateInputData(data) { let array = new Uint8Array(data); // event.data.buffer就是接收到的inputreport包数据了 let hexstr = ""; for (const data of array) { hexstr += (Array(2).join(0) + data.toString(16).toUpperCase()).slice(-2) + " "; // 将字节数据转换成(XX )形式字符串 } iptInput.value += hexstr; iptInput.scrollTop = iptInput.scrollHeight; // 滚动到底部 } // 读取数据 async function listenReceived() { if (reading) { console.log("On reading."); return; } reading = true; while (port.readable && reading) { reader = port.readable.getReader(); try { while (true) { const { value, done } = await reader.read(); if (done) { // |reader| has been canceled. break; } // 需要特别注意的是:实际使用中即使对端是按一个个包发送的串口数据,接收时收到的也可能是分多段收到的 updateInputData(value); } } catch (e) { console.log(e); } finally { reader.releaseLock(); } } await port.close(); // 关闭串口 port = null; console.log("Port closed."); } // 打开串口 btnOpen.onclick = async () => { if (port === null) { console.log("Not selected."); return; } await port.open({ baudRate: 115200, // bufferSize: 255, // 读写缓存,默认255 // dataBits: 8, // 数据位,默认8 // flowControl: none, // 流控制,默认无 // parity: none, // 校验,默认无 // stopBits: 1, // 停止位,默认1 }); listenReceived(); console.log("Port opened."); } // 关闭串口 btnClose.onclick = async () => { if ((port === null) || (!port.writable)) { console.log("Not opened."); return; } if (reading) { reading = false; reader?.cancel(); } } // 获取发送窗口十六进制字符串转换为字节数组 function getOutputData() { let outputDatastr = iptOutput.value.replace(/\s+/g, ""); // 去除所有空白字符 if (outputDatastr.length % 2 == 0 && /^[0-9a-fA-F]+$/.test(outputDatastr)) { // 获取字节数组长度 const byteLength = outputDatastr.length / 2; // 创建字节数组 const outputData = new Uint8Array(byteLength); // 将字符串转成字节数组数据 for (let i = 0; i { if ((port === null) || (!port.writable)) { console.log("Not opened."); return; } const writer = port.writable.getWriter(); await writer.write(getOutputData()); // 发送数据 writer.releaseLock(); }

    下面测试时我将串口的TX/RT短接在一起,发送什么数据就会收到什么数据:

    使用 Web Serial API 在浏览器中实现串口通讯(纯前端)

    总结

    使用 Web Serial API 访问串口非常方便,目前来说唯一的问题是这还是实验性质的功能,可能之后接口还会变动,需要根据实际情况进行调整。

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]