JavaScript内核与高级编程之:`JavaScript` 的 `Web Serial` API:其在 `JavaScript` 中与串口设备通信。

各位观众,大家好!我是你们的老朋友,今天咱们来聊聊一个有点“野性”的话题——Web Serial API。 啥叫“野性”?因为它能让你直接用浏览器跟硬件设备“勾搭”上,想想是不是有点刺激? 别担心,咱们会用最简单的方式,把这只“野兽”驯服。

开场白:串口是个啥?为啥需要 Web Serial?

在进入正题之前,先简单回顾一下串口。如果你玩过 Arduino、树莓派之类的东西,肯定对它不陌生。 串口,简单来说,就是一种古老的通信方式,用一根或者几根线来传输数据。 它的优点是简单、可靠,但缺点也很明显:速度慢,而且通常需要特定的驱动程序。

那么,Web Serial API 又是干啥的呢? 简单来说,它就是让浏览器也能直接访问串口设备的“桥梁”。 以前,如果你想用网页控制一个串口设备,比如一个 LED 灯,你得先装个驱动,然后写个桌面应用,通过某种方式(比如 WebSocket)和网页通信。 现在有了 Web Serial API,这一切都简化了! 你只需要在网页里写几行 JavaScript 代码,就能直接控制串口设备了。

Web Serial API 的基本用法:Hello, Serial!

好了,废话不多说,咱们直接上代码!

1. 请求串口访问权限

首先,我们需要向用户请求访问串口的权限。 这个过程就像你要去别人家做客,总得先敲敲门,问问主人欢不欢迎吧?

async function requestSerialPort() {
  try {
    const port = await navigator.serial.requestPort();
    // 成功获取串口对象后,就可以进行后续操作了
    console.log("串口已获取!");
    return port;
  } catch (error) {
    console.error("获取串口失败:", error);
    // 用户可能取消了选择,或者浏览器不支持 Web Serial API
    return null;
  }
}

这段代码里,navigator.serial.requestPort() 是一个异步函数,它会弹出一个对话框,让用户选择要连接的串口设备。 如果用户允许访问,函数会返回一个 SerialPort 对象,否则会抛出一个错误。

2. 连接串口

拿到 SerialPort 对象后,下一步就是连接串口。 这就像你进了别人家门,总得先跟主人打个招呼吧?

async function connectSerialPort(port, baudRate) {
  try {
    await port.open({ baudRate: baudRate });
    console.log("串口已连接!");
    return true;
  } catch (error) {
    console.error("连接串口失败:", error);
    return false;
  }
}

port.open() 函数用于打开串口连接。 它需要一个配置对象,里面至少要指定 baudRate(波特率)。 波特率是串口通信的一个重要参数,它表示每秒传输多少个比特。 不同的串口设备可能需要不同的波特率,你需要根据设备的文档来设置。

3. 发送数据

连接成功后,就可以发送数据了。 这就像你跟主人聊起天来,开始说你想说的话。

async function sendData(port, data) {
  try {
    const writer = port.writable.getWriter();
    await writer.write(new TextEncoder().encode(data));
    writer.releaseLock();
    console.log("数据已发送:", data);
  } catch (error) {
    console.error("发送数据失败:", error);
  }
}

这段代码里,我们首先通过 port.writable.getWriter() 获取一个 WritableStreamDefaultWriter 对象,用于向串口写入数据。 然后,我们使用 TextEncoder 将字符串数据编码成 Uint8Array 类型的字节数组,再通过 writer.write() 函数发送出去。 最后,别忘了调用 writer.releaseLock() 释放锁,否则其他代码可能无法写入数据。

4. 接收数据

除了发送数据,我们还需要能够接收数据。 这就像主人在听你说话,然后给你回应。

async function receiveData(port, callback) {
  try {
    const reader = port.readable.getReader();
    let partialData = ''; // 用于存储未完成的数据段

    while (true) {
      const { value, done } = await reader.read();

      if (done) {
        console.log("串口已断开连接");
        break;
      }

      const textDecoder = new TextDecoder();
      const newData = textDecoder.decode(value);
      const completeData = partialData + newData;

      // 假设每条完整数据以换行符 'n' 结尾
      const lines = completeData.split('n');
      partialData = lines.pop() || ''; // 将最后一段未完成的数据存起来

      lines.forEach(line => {
        if (line) {
          callback(line); // 调用回调函数处理每行数据
        }
      });
    }
  } catch (error) {
    console.error("接收数据失败:", error);
  } finally {
    if (reader) {
      reader.releaseLock(); // 确保在出错或者循环结束时释放锁
    }
  }
}

这段代码里,我们通过 port.readable.getReader() 获取一个 ReadableStreamDefaultReader 对象,用于从串口读取数据。 reader.read() 函数会返回一个 Promise,它会在有数据可读时 resolve。 我们使用 TextDecoderUint8Array 类型的字节数组解码成字符串。

注意: 这里我们假设串口设备发送的数据是以换行符 n 分隔的。 如果你的设备使用不同的分隔符,你需要修改代码来适应。 同时,我们需要处理 incomplete data,即数据可能被分割成多个块,需要将它们拼接起来才能得到完整的数据。

5. 关闭串口

最后,当我们不再需要使用串口时,应该关闭它。 这就像你要离开别人家了,总得跟主人告别一声吧?

async function closeSerialPort(port) {
  try {
    await port.close();
    console.log("串口已关闭!");
  } catch (error) {
    console.error("关闭串口失败:", error);
  }
}

port.close() 函数用于关闭串口连接。

一个完整的例子:串口回显

下面是一个完整的例子,它可以从串口接收数据,然后原封不动地发送回去。 这个例子就像一个“鹦鹉”,你说啥它就学啥。

<!DOCTYPE html>
<html>
<head>
  <title>Web Serial Echo</title>
</head>
<body>
  <h1>Web Serial Echo</h1>

  <button id="connectButton">连接串口</button>
  <button id="disconnectButton" disabled>断开串口</button>

  <textarea id="receivedData" rows="10" cols="50" readonly></textarea>
  <br>
  <input type="text" id="sendData">
  <button id="sendButton">发送数据</button>

  <script>
    let port;
    let reader;
    let writer;

    const connectButton = document.getElementById("connectButton");
    const disconnectButton = document.getElementById("disconnectButton");
    const receivedDataTextarea = document.getElementById("receivedData");
    const sendDataInput = document.getElementById("sendData");
    const sendButton = document.getElementById("sendButton");

    connectButton.addEventListener("click", async () => {
      try {
        port = await navigator.serial.requestPort();
        await port.open({ baudRate: 9600 }); // 确保波特率与你的设备匹配
        connectButton.disabled = true;
        disconnectButton.disabled = false;

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

        // 接收数据
        while (true) {
          const { value, done } = await reader.read();
          if (done) {
            console.log("Reader done!");
            break;
          }

          const textDecoder = new TextDecoder();
          const receivedText = textDecoder.decode(value);
          receivedDataTextarea.value += receivedText;
          receivedDataTextarea.scrollTop = receivedDataTextarea.scrollHeight; // 自动滚动到最新内容

          // 回显数据
          await writer.write(value); // 直接将接收到的 Uint8Array 发送回去
        }

      } catch (error) {
        console.error("Serial port error:", error);
      } finally {
        if (reader) {
          reader.releaseLock();
        }
        if (writer) {
          writer.releaseLock();
        }
      }
    });

    disconnectButton.addEventListener("click", async () => {
      try {
        await port.close();
        console.log("Serial port closed");
        connectButton.disabled = false;
        disconnectButton.disabled = true;
      } catch (error) {
        console.error("Error closing serial port:", error);
      }
    });

    sendButton.addEventListener("click", async () => {
      const dataToSend = sendDataInput.value;
      const encoder = new TextEncoder();
      const encodedData = encoder.encode(dataToSend);
      try {
        await writer.write(encodedData);
        console.log("Sent:", dataToSend);
        sendDataInput.value = ""; // 清空输入框
      } catch (error) {
        console.error("Error sending data:", error);
      }
    });
  </script>
</body>
</html>

将这段代码保存为 HTML 文件,然后在支持 Web Serial API 的浏览器中打开。 你需要连接一个串口设备,并确保波特率设置为 9600 (或者你设备实际使用的波特率)。 然后,点击“连接串口”按钮,选择你的串口设备。 之后,你就可以在输入框里输入数据,点击“发送数据”按钮,数据就会通过串口发送出去,并原封不动地显示在文本框里。

高级用法:更灵活的数据处理

上面的例子只是一个简单的回显,实际应用中,我们可能需要更灵活地处理数据。 比如,我们可能需要解析串口设备发送的二进制数据,或者将网页上的数据转换成特定的格式再发送给串口设备。

1. 解析二进制数据

如果串口设备发送的是二进制数据,我们需要使用 DataView 对象来解析它。 DataView 对象可以让我们以不同的数据类型(比如 Int8Uint16Float32)来读取 ArrayBuffer 中的数据。

async function receiveBinaryData(port, callback) {
  try {
    const reader = port.readable.getReader();
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        console.log("串口已断开连接");
        break;
      }

      const dataView = new DataView(value.buffer);
      // 假设前两个字节是设备ID,后四个字节是温度值
      const deviceId = dataView.getUint16(0);
      const temperature = dataView.getFloat32(2);

      callback(deviceId, temperature);
    }
  } catch (error) {
    console.error("接收二进制数据失败:", error);
  } finally {
    if (reader) {
      reader.releaseLock();
    }
  }
}

在这个例子里,我们假设串口设备发送的数据格式是:前两个字节是设备 ID(Uint16),后四个字节是温度值(Float32)。 我们使用 dataView.getUint16(0)dataView.getFloat32(2) 函数来读取这些数据。

2. 格式化数据

在发送数据之前,我们可能需要将网页上的数据转换成特定的格式。 比如,我们可能需要将一个 JSON 对象转换成一个字符串,或者将一个数字转换成一个字节数组。

async function sendFormattedData(port, data) {
  try {
    // 将 JSON 对象转换成字符串
    const jsonString = JSON.stringify(data);
    // 将字符串转换成字节数组
    const encoder = new TextEncoder();
    const encodedData = encoder.encode(jsonString);

    const writer = port.writable.getWriter();
    await writer.write(encodedData);
    writer.releaseLock();

    console.log("数据已发送:", jsonString);
  } catch (error) {
    console.error("发送数据失败:", error);
  }
}

在这个例子里,我们首先使用 JSON.stringify() 函数将一个 JSON 对象转换成一个字符串。 然后,我们使用 TextEncoder 将字符串转换成一个字节数组,再通过串口发送出去。

安全性 considerations

Web Serial API 提供了与硬件设备直接交互的能力,这在带来便利的同时,也带来了一些安全风险。 我们需要注意以下几点:

  • 权限管理: Web Serial API 必须在安全上下文(HTTPS)中使用,并且需要用户明确授权才能访问串口设备。 浏览器会弹出一个对话框,让用户选择要连接的串口设备。
  • 数据验证: 我们需要对从串口接收到的数据进行验证,防止恶意代码注入。
  • 设备隔离: Web Serial API 只能访问用户选择的串口设备,无法访问其他设备。

兼容性

Web Serial API 并不是所有浏览器都支持。 目前,Chrome 和 Edge 浏览器已经支持 Web Serial API,但 Safari 和 Firefox 还没有完全支持。 你可以使用 navigator.serial 来检测浏览器是否支持 Web Serial API。

if ("serial" in navigator) {
  console.log("Web Serial API is supported!");
} else {
  console.log("Web Serial API is not supported!");
}

表格总结

为了方便大家理解,我把 Web Serial API 的一些关键概念和函数整理成了一个表格:

概念/函数 描述
navigator.serial Web Serial API 的入口,提供访问串口设备的能力。
SerialPort 串口对象,表示一个已经连接的串口设备。
port.open(options) 打开串口连接。 options 对象包含串口配置信息,比如 baudRate
port.close() 关闭串口连接。
port.readable ReadableStream 对象,用于从串口读取数据。
port.writable WritableStream 对象,用于向串口写入数据。
reader.read() ReadableStream 中读取数据。 返回一个 Promise,它会在有数据可读时 resolve。
writer.write(data) WritableStream 中写入数据。 data 必须是 Uint8Array 类型的字节数组。
TextEncoder 用于将字符串编码成 Uint8Array 类型的字节数组。
TextDecoder 用于将 Uint8Array 类型的字节数组解码成字符串。
DataView 用于以不同的数据类型读取 ArrayBuffer 中的数据,比如 Int8Uint16Float32

总结:Web Serial API 的未来

Web Serial API 的出现,为 Web 应用与硬件设备之间的交互打开了一扇新的大门。 它可以让开发者直接用浏览器控制各种串口设备,而无需安装额外的驱动程序和桌面应用。 虽然 Web Serial API 目前还处于发展阶段,但它已经展现出了巨大的潜力。 相信随着 Web Serial API 的不断完善和普及,它将在物联网、机器人、嵌入式系统等领域发挥越来越重要的作用。

好了,今天的讲座就到这里。 希望大家通过今天的学习,能够对 Web Serial API 有一个初步的了解,并尝试用它来做一些有趣的项目。 记住,编程的乐趣在于实践,只有不断地尝试和探索,才能真正掌握一门技术。 感谢大家的收听!我们下次再见!

发表回复

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