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

news2025/1/9 14:41:35

文章目录

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

目的

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

相关资料

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等浏览器支持:
在这里插入图片描述

另外还需要注意的是从网页操作设备是比较容易产生安全风险的,所以这个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

在这里插入图片描述

使用说明

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

// 全局串口设备插入事件
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 来设置和获取流控制情况。

代码与演示

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Serial API Test</title>
    <style>
        * {
            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;
        }
    </style>
    <script>
        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);
        };
    </script>
</head>

<body>
    <button id="btnSelect">select</button><br>
    <button id="btnOpen">open</button><br>
    <button id="btnClose">close</button><br>
    <button id="btnSend">send</button><br>
    <textarea id="iptOutput">D0 D1 D2 D3 D4 D5 D6 D7</textarea><br>
    <textarea id="iptInput" readonly></textarea>
    <script>
        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 < byteLength; i++) {
                    outputData[i] = parseInt(outputDatastr.substr(i * 2, 2), 16);
                }
                // 返回数据
                return outputData;
            } else {
                throw "Data is not even or 0-9、a-f、A-F";
            }
        }

        // 发送数据
        btnSend.onclick = async () => {
            if ((port === null) || (!port.writable)) {
                console.log("Not opened.");
                return;
            }
            const writer = port.writable.getWriter();
            await writer.write(getOutputData()); // 发送数据
            writer.releaseLock();
        }

    </script>
</body>

</html>

下面测试时我将串口的TX/RT短接在一起,发送什么数据就会收到什么数据:
在这里插入图片描述

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1836664.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java面试】二十、JVM篇(上):JVM结构

文章目录 1、JVM2、程序计数器3、堆4、栈4.1 垃圾回收是否涉及栈内存4.2 栈内存分配越大越好吗4.3 方法内的局部变量是否线程安全吗4.4 栈内存溢出的情况4.5 堆和栈的区别是什么 5、方法区5.1 常量池5.2 运行时常量池 6、直接内存 1、JVM Java源码编译成class字节码后&#xf…

七大黄金原油短线操作技巧与方法

1、研究K线组合 K线组合是几个交易日K线的衔接和联系&#xff0c;它无法掩饰地透露着黄金价格运行趋势的某种征兆。研究K线组合的深刻蕴含&#xff0c;感知其内在动意&#xff0c;把握黄金价格上涨征兆&#xff0c;可以大大提高上涨的概率。其实对许多诸如“强势整理”、“突破…

管道(channel)入门

管道&#xff08;Channel&#xff09; 1、管道本质就是一个数据结构-队列 2、数据是先进先出 3、自身线程安全&#xff0c;多协程访问时不需要加锁&#xff0c;channel本身就是线程安全的 4、管道有类型的&#xff0c;一个string的管道&#xff0c;只能存放string类型的数据 管…

vue3第四十节(pinia的用法注意事项解构store)

pinia 主要包括以下五部分&#xff0c;经常用到的是 store、state、getters、actions 以下使用说明&#xff0c;注意事项&#xff0c;仅限于 vue3 setup 语法糖中使用&#xff0c;若使用选项式 API 请直接查看官方文档&#xff1a; 一、前言&#xff1a; pinia 是为了探索 vu…

一文弄懂 Python os.walk(),轻松搞定文件处理和目录遍历

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ Python os 模块的 walk() 方法以自顶向下或自底向上的方式遍历指定的目录树&#xff0c;从而显示目录树中的文件名。对于目录树中的每个目录&#xff0c;os.walk() 方法都会产生一个包含目录路径、当前…

当同时绑定mousedown和mouseup时,不执行mouseup

问题描述&#xff1a; 当我同时给一个标签添加mousedown和mouseup两个鼠标事件&#xff0c;点击span的时候会触发mousedown事件&#xff0c;但是不会执行mouseup事件&#xff1b;但是注释图二中的setCloudControl方法又能触发mouseup。 后来查阅资料&#xff0c;发现是在封装a…

数据资产入表-数据分类分级标准-数据分级

前情提要&#xff1a;2021年9月1日&#xff0c;《中华人民共和国数据安全法》正式施行&#xff0c;明确规定“国家建立数据分类分级保护制度”&#xff0c;数据分级分类是数据安全管理的重要措施&#xff0c;它涉及到对数据资产的识别、分类和定级&#xff0c;是保障数据合规的…

VUE 项目用 Docker+Nginx进行打包部署

一、Docker Docker 是一个容器化平台&#xff0c;允许你将应用程序及其依赖项打包在容器中。使用 Docker&#xff0c;你可以创建一个包含 Vue.js 应用程序的容器镜像&#xff0c;并在任何支持 Docker 的环境中运行该镜像。 二、Nginx Nginx 是一个高性能的 HTTP 服务器和反向…

递归与回溯 || 排列问题

目录 前言&#xff1a; 全排列 题解&#xff1a; 全排列 II 题解&#xff1a; 子集 题解&#xff1a; 组合 题解&#xff1a; 组合总和 题解&#xff1a; 电话号码的字母组合 题解&#xff1a; 字母大小写全排列 题解&#xff1a; 优美的排列 题解&#xff1a;…

MySQL数据库回顾(1)

数据库相关概念 关系型数据库 概念: 建立在关系模型基础上&#xff0c;由多张相互连接的二维表组成的数据库。 特点&#xff1a; 1.使用表存储数据&#xff0c;格式统一&#xff0c;便于维护 2.使用SQL语言操作&#xff0c;标准统一&#xff0c;使用方便 SOL SQL通用语法 …

MySQL常见面试题自测

文章目录 MySQL基础架构一、说说 MySQL 的架构&#xff1f;二、一条 SQL语句在MySQL中的执行过程 MySQL存储引擎一、MySQL 提供了哪些存储引擎&#xff1f;二、MySQL 存储引擎架构了解吗&#xff1f;三、MyISAM 和 InnoDB 的区别&#xff1f; MySQL 事务一、何谓事务&#xff1…

JCR一区 | Matlab实现GAF-PCNN、GASF-CNN、GADF-CNN的多特征输入数据分类预测/故障诊断

JJCR一区 | Matlab实现GAF-PCNN、GASF-CNN、GADF-CNN的多特征输入数据分类预测/故障诊断 目录 JJCR一区 | Matlab实现GAF-PCNN、GASF-CNN、GADF-CNN的多特征输入数据分类预测/故障诊断分类效果格拉姆矩阵图GAF-PCNNGASF-CNNGADF-CNN 基本介绍程序设计参考资料 分类效果 格拉姆…

数据结构与算法-字符出现的次数

问题描述 以下是这个找出字符串中字符串出现频率最多的字符。大家可以自行研究一下&#xff0c;题目不难&#xff0c;我今天尝试使用C语言来完成解答&#xff0c;但是在解答过程居然出现了一个意想不到的问题。可能是高级语言用多了&#xff0c;C语言某些函数的限制和风险忘记管…

Android开发系列(三)Jetpack Compose 之TextField

TextField 是一个用于接收用户输入的UI组件。它是Jetpack Compose中的一部分&#xff0c;可以方便地实现用户文本输入的功能。 TextField 允许用户输入一个或多个文本行&#xff0c;可以用于接收用户的文本输入、搜索等操作。它提供了一些常用的功能&#xff0c;如输入验证、键…

深入了解SD-WAN:企业广域网的未来

在讨论SD-WAN之前&#xff0c;我们先来了解一下WAN的基本概念。WAN&#xff08;广域网&#xff09;是一个连接多个地理位置分散的局域网的通信网络。在企业中&#xff0c;WAN通常连接总部、分支机构、托管设施和云服务等多个网络节点。广域网允许用户共享各种应用和服务&#x…

LeetCode 算法:合并两个有序链表 c++

原题链接&#x1f517;&#xff1a;合并两个有序链表 难度&#xff1a;简单⭐️ 题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;…

20240616日志:大模型压缩方法DMS

Location: Beijing 1 大模型剪枝 Fig. 1.1大模型压缩-剪枝 剪枝的理论来源基于彩票假设&#xff08;Lottery Ticket Hypothesis&#xff09;&#xff0c;指在神经网络中存在一种稀疏连接模式&#xff0c;即仅利用网络的一小部分连接&#xff08;彩票&#xff09;就足以实现与整…

论坛产品选型,需要关注哪些点?

论坛社区是一个经久不衰的行业&#xff0c;比如我们常见的宠物社区&#xff0c;校园社区&#xff0c;游戏社区、企业内部社区&#xff0c;品牌社区&#xff0c;本地同城、私域社区项目、付费社群、问答社区等等&#xff0c;可以说是覆盖了各行各业&#xff0c;那么如果我们要搭…

设备档案包括哪些内容

设备档案通常包括以下内容和要求&#xff1a; 1. 设备基本信息&#xff1a;包括设备名称、型号、规格、生产厂商、出厂日期、购买日期等。 2. 设备安装信息&#xff1a;包括设备的安装位置、安装日期、安装人员等。 3. 设备维护信息&#xff1a;包括设备的维护保养记录&#xf…

构建基于 LlamaIndex 的RAG AI Agent

I built a custom AI agent that thinks and then acts. I didnt invent it though, these agents are known as ReAct Agents and Ill show you how to build one yourself using LlamaIndex in this tutorial. 我构建了一个自定义的AI智能体&#xff0c;它能够思考然后行动。…