草庐IT

Vue使用Serial连接串口

ZhangY1217 2023-08-24 原文

本来只是随手记录一下,发现看的人多了,想着还是修复一下bug吧,供各位看官指正

2022-10-24本次更新:

1、修复在不支持Serial的情况下,控制台报错

2022-09-19本次更新:

 1、修复了传输数据接收分隔的情况(增加数据缓存)

 2、修复串口连接没有使用选择的波特率等参数

1、Serial 接口是 Web Serial API的接口,提供了从网页查找和连接串口的属性和方法。

注意:

  只能在部分支持Serial并且网站为安全上下文(HTTPS)中可用,或者是本机访问

一:常用API介绍

  1. requestPort----获取授权串口

  2. open-----打开串口

  3. close---关闭串口(串口关闭前,需要释放锁住的流)

  4. cancel---立即退出读取的循环,然后去调用releaseLock,最后调用close方法

  5. releaseLock---Reader和.Writer的释放方法

  6. read---port.readable.getReader()的读取字节数组方法

  7. write---port.writable.getWriter()的写入方法

二:代码示例

MySerialPort.js 是封装的一个SerialPort的工具类


export default class MySerialPort  {
  constructor() {
    this.state = {
      portIndex: undefined,
      ports: [],
      isOpen: false,
      writeType: 1,
      readType: 1,
      isScroll: true,
      readValue: [],
      status:false,
      //port参数
      baudRate: "9600",
      dataBits: "8",
      stopBits: "1",
      parity: "none",
      flowControl: "none",
    };
    this.keepReading=false;
    this.getPorts = this.getPorts.bind(this);
    this.handleRequestPort = this.handleRequestPort.bind(this);
    this.handleChildrenChange = this.handleChildrenChange.bind(this);
    this.readText = this.readText.bind(this);
    this.writeText = this.writeText.bind(this);
    this.handleClear = this.handleClear.bind(this);
    this.a2hex = this.a2hex.bind(this);
    this.hex2a = this.hex2a.bind(this);
    this.hex2atostr=this.hex2atostr.bind(this);
    this.reader={};
    this.closed;
  }
 
  async getPorts() {
    // 获取已授权的全部串口
    let ports = await navigator.serial.getPorts();
    this.setState({
      ports,
    });
  }
  async handleRequestPort() {
    // 请求授权
    try {
      await navigator.serial.requestPort();
      await this.getPorts();
    } catch (e) {
      this.$message.error(e.toString());
    }
  }
  async openPort(portIndex, isOpen,callBack=null) {
    // 打开串口
    let port = this.state.ports[portIndex];
    if (!isOpen) {
      // 关闭串口
      this.keepReading = false;
      this.reader.cancel();
      await this.closed;
      this.handlePortOpen({
        portIndex,
        isOpen,
      });
    } else {
      await port.open({
        baudRate: this.state.baudRate,
        dataBits: this.state.dataBits,
        stopBits: this.state.stopBits,
        parity: this.state.parity,
        flowControl: this.state.flowControl,
      });
      this.handlePortOpen({
        portIndex,
        isOpen,
      });
      this.keepReading = true;
      this.closed=this.readUntilClosed(portIndex,callBack);
    }
  }
  async readUntilClosed(portIndex,callBack=null) {
    let port = this.state.ports[portIndex];
    while (port.readable && this.keepReading) {
      this.reader = port.readable.getReader();
      try {
        let readCache=[]
        while (true) {
          const { value, done } = await this.reader.read();
          if (done) {
            break;
          }
          readCache.push(...value)
          setTimeout(() => {
          if(readCache.length>0){
            this.readText(readCache);
            callBack(readCache)
            readCache=[]
          }
          }, 300);//串口缓存
        }
      } catch (error) {
        this.$message.error(error.toString());
      } finally {
        this.reader.releaseLock();
      }
      await port.close();
    }
  }
  handlePortOpen({ portIndex, isOpen }) {
    // 处理打开串口
    this.setState({
      portIndex,
      isOpen,
    });
  }
  handleChildrenChange(type, value) {
    this.setState({
      [type]: value,
    });
  }
  portWrite(value) {
    return new Promise(async (resolve, reject) => {
      if (!this.state.isOpen) {
        this.$message.error("串口未打开");
        reject();
        return;
      } else {
        let port = this.state.ports[this.state.portIndex];
        const writer = port.writable.getWriter();
        await writer.write(new Uint8Array(value));
        writer.releaseLock();
        resolve(value);
      }
    });
  }
  readText(value) {
    console.log(value, "读取");
    let newValue = this.state.readValue.concat({
      value,
      type: 1,
    });
    this.setState({
      readValue: newValue,
    });
  }
  writeText(value) {
    console.log(value, "写入");
    this.portWrite(value).then((res) => {
      let newValue = this.state.readValue.concat({
        value: res,
        type: 2,
      });
      this.setState({
        readValue: newValue,
      });
    });
  }
  handleClear() {
    this.setState({
      readValue: [],
    });
  }
  componentDidMount() {
    this.getPorts();
  }
  handleState(status) {
    this.setState({
      status,
    });
  }
  setState(obj){
    Object.keys(this.state).forEach(key => {
           if(obj[key]!=undefined){
            this.state[key]=obj[key]
           }
      });
  }
  //字节转字符串
  hex2atostr(arr) {
    return String.fromCharCode.apply(String,arr);
  }
   hex2a(hexx) {
    return String.fromCharCode(hexx);
  }
  //字符转16进制
 a2hex(str) {
    return str.charCodeAt(0);
}
}

 vue代码:

<template>
  <div>
    <el-row
      type="flex"
      class="row-bg"
      justify="center"
      v-show="portsList.length == 0"
    >
      <el-col :span="7"
        ><div style="margin-top: 400px">
          <span style="display: block">
            仅支持Chrome 89+或者Edge 89+浏览器(安全上下文(HTTPS)中可用)
          </span>
          <el-button type="primary" @click="obtainAuthorization"
            >授权</el-button
          >
        </div></el-col
      >
    </el-row>
    <el-form
      v-show="portsList.length > 0"
      ref="form"
      :model="form"
      label-width="100px"
    >
      <el-row>
        <el-col :span="6"
          ><div>
            <el-form-item label="串口">
              <el-select
                v-model="form.port"
                filterable
                placeholder="请选择串口"
                :disabled="isDisable"
              >
                <el-option
                  v-for="item in portsList"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                >
                </el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="波特率">
              <el-autocomplete
                popper-class="my-autocomplete"
                v-model="form.baudRate"
                :fetch-suggestions="querySearch"
                placeholder="请输入波特率"
                :disabled="isDisable"
              >
                <i class="el-icon-edit el-input__icon" slot="suffix"> </i>
                <template slot-scope="{ item }">
                  <div class="name">{{ item.value }}</div>
                  <span class="addr">{{ item.address }}</span>
                </template>
              </el-autocomplete>
            </el-form-item>
            <el-form-item label="数据位">
              <el-select
                v-model="form.dataBits"
                placeholder="请选择数据位"
                :disabled="isDisable"
              >
                <el-option label="7" value="7"></el-option>
                <el-option label="8" value="8"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="停止位">
              <el-select
                v-model="form.stopBits"
                placeholder="请选择停止位"
                :disabled="isDisable"
              >
                <el-option label="1" value="1"></el-option>
                <el-option label="2" value="2"></el-option>
              </el-select>
            </el-form-item>

            <el-form-item label="校验位">
              <el-select
                v-model="form.parity"
                placeholder="请选择校验位"
                :disabled="isDisable"
              >
                <el-option label="None" value="none"></el-option>
                <el-option label="Even" value="even"></el-option>
                <el-option label="Odd" value="odd"></el-option>
              </el-select>
            </el-form-item>

            <el-form-item label="流控制">
              <el-select
                v-model="form.flowControl"
                placeholder="请选择流控制"
                :disabled="isDisable"
              >
                <el-option label="None" value="none"></el-option>
                <el-option label="HardWare" value="hardware"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="显示历史">
              <el-switch
                v-model="form.isShowHistory"
                @change="loadHistory"
              ></el-switch>
              <el-button
                type="danger"
                icon="el-icon-delete"
                circle
                title="清空历史"
                @click="clearHistory"
              ></el-button>
            </el-form-item>
            <el-form-item label="发送区设置" v-show="isShowSendArea">
              <el-form-item label="发送格式">
                <el-radio-group v-model="form.type">
                  <el-radio label="1">ASCII</el-radio>
                  <el-radio label="2">HEX</el-radio>
                </el-radio-group>
              </el-form-item>
              <el-form-item label="发送信息">
                <el-input type="textarea" v-model="form.sendMsg"></el-input>
              </el-form-item>
              <el-button type="primary" @click="sendCommon">发送</el-button>
            </el-form-item>

            <el-form-item>
              <el-button :type="btnType" @click="connectBtn">{{
                btnText
              }}</el-button>
              <el-button type="info" @click="obtainAuthorization"
                >新增授权</el-button
              >
            </el-form-item>
          </div>
        </el-col>
        <el-col :span="10"
          ><div>
            <el-form-item label="电子秤信息">
              <el-input
                type="textarea"
                v-model="form.desc"
                disabled
                :autosize="{ minRows: 21, maxRows: 25 }"
              ></el-input>
            </el-form-item></div
        ></el-col>
      </el-row>
    </el-form>
  </div>
</template>

<script>
import MySerialPort from "./MySerialPort";
import USBDevice from "./usb.json";
export default {
  data() {
    return {
      input: "",
      keepReading: true,
      form: {
        baudRate: "9600",
        dataBits: "8",
        stopBits: "1",
        parity: "none",
        flowControl: "none",
        desc: "",
        type: "1",
        isShowHistory: false,
      },
      btnType: "primary",
      btnText: "连接串口",
      restaurants: [],
      portsList: [],
      isShowSendArea: false,
      readType: 1,
    };
  },
  mounted() {
    if ("serial" in navigator) {
      this.myserialport = new MySerialPort();
      this.getPorts();
      navigator.serial.addEventListener("connect", (e) => {
        this.$message.success("设备已连接");
        this.getPorts();
      });
      navigator.serial.addEventListener("disconnect", (e) => {
        this.$message.error("设备已断开");
      });
      this.restaurants = this.loadAll();
    } else {
      this.$message.error(
        "当前为HTTP模式或者浏览器版本过低,不支持网页连接串口"
      );
    }
  },
  computed: {
    isDisable() {
      return this.btnType == "danger";
    },
  },
  methods: {
    //接受数据的回调
    callBack(value) {
      if (this.form.isShowHistory) this.form.desc = this.readLi().join("");
      else {
        if (value.length > 0)
          this.form.desc = this.myserialport.hex2atostr(value);
      }
    },
    clearHistory() {
      this.form.desc = "";
      this.myserialport.state.readValue = [];
    },
    loadHistory() {
      if (this.form.isShowHistory) this.form.desc = this.readLi().join("");
      else {
        let temp = this.readLi();
        if (temp.length > 0) this.form.desc = temp[temp.length - 1].join("");
      }
    },
    readLi() {
      let readType = this.readType;
      return this.myserialport.state.readValue.map((items, index) => {
        const item = items.value;
        const type = items.type; // 1接收,2发送
        let body = [];
        if (item !== undefined) {
          let strArr = [];
          for (let hex of Array.from(item)) {
            strArr.push(hex.toString(16).toLocaleUpperCase());
          }
          if (strArr.includes("D") && strArr.includes("A")) {
            if (strArr.indexOf("A") - strArr.indexOf("D") === 1) {
              strArr.splice(strArr.indexOf("D"), 1);
              strArr.splice(strArr.indexOf("A"), 1, <br key={0} />);
            }
          }
          strArr = strArr.map((item) => {
            if (typeof item === "string") {
              if (readType === 1) {
                return this.myserialport.hex2a(parseInt(item, 16));
              } else if (readType === 2) {
                return item + " ";
              }
            }
            return item;
          });
          if (typeof strArr[strArr.length - 1] === "string") {
            strArr.push("\r\n");
          }
          body.push(strArr.join(""));
        }
        return body;
      });
    },
    //连接
    async connectBtn() {
      if (this.btnType == "primary") {
        try {
          this.myserialport.state.baudRate = this.form.baudRate;
          this.myserialport.state.dataBits = this.form.dataBits;
          this.myserialport.state.stopBits = this.form.stopBits;
          this.myserialport.state.parity = this.form.parity;
          this.myserialport.state.flowControl = this.form.flowControl;
          await this.myserialport.openPort(this.form.port, true, this.callBack);
        } catch (error) {
          this.$message.error("串口连接失败!请检查串口是否已被占用");
        }
        if (this.myserialport.state.isOpen) {
          this.$message.success("串口连接成功");
          this.btnType = "danger";
          this.btnText = "关闭串口";
        }
      } else {
        this.myserialport.openPort(this.form.port, false, this.callBack);
        this.$message.success("串口关闭成功");
        this.btnType = "primary";
        this.btnText = "连接串口";
      }
    },
    //授权
    async obtainAuthorization() {
      if ("serial" in navigator) {
        console.log("The Web Serial API is supported.");
        if (!this.myserialport) this.myserialport = new MySerialPort();
        try {
          await this.myserialport.handleRequestPort();
          this.$message.success("串口授权成功");
          this.getPortInfo(this.myserialport.state.ports);
        } catch (error) {
          this.$message.warning("未选择新串口授权!");
        }
      } else {
        this.$message.error(
          "当前为HTTP模式或者浏览器版本过低,不支持网页连接串口"
        );
      }
    },
    //串口列表初始化
    getPortInfo(portList) {
      this.portsList = [];
      portList.map((port, index) => {
        const { usbProductId, usbVendorId } = port.getInfo();
        if (usbProductId === undefined || usbVendorId === undefined) {
          this.portsList.push({ label: "未知设备" + index, value: index });
        } else {
          const usbVendor = USBDevice.filter(
            (item) => parseInt(item.vendor, 16) === usbVendorId
          );
          let usbProduct = [];
          if (usbVendor.length === 1) {
            usbProduct = usbVendor[0].devices.filter(
              (item) => parseInt(item.devid, 16) === usbProductId
            );
          }
          this.portsList.push({ label: usbProduct[0].devname, value: index });
        }
      });
    },
    // 发送
    async sendCommon() {
      if (this.myserialport.state.isOpen) {
        if (this.form.sendMsg.length !== 0) {
          const writeType = this.form.type;
          let value = this.form.sendMsg;
          let arr = [];
          if (writeType == 1) {
            // ASCII
            for (let i = 0; i < value.length; i++) {
              arr.push(this.myserialport.a2hex(value[i]));
            }
          } else if (writeType == 2) {
            // HEX
            if (/^[0-9A-Fa-f]+$/.test(value) && value.length % 2 === 0) {
              for (let i = 0; i < value.length; i = i + 2) {
                arr.push(parseInt(value.substring(i, i + 2), 16));
              }
            } else {
              this.$message.error("格式错误");
              return;
            }
          }
          this.myserialport.writeText(arr);
        } else {
          this.$message.warning("请输入发送的信息");
        }
      } else {
        this.$message.warning("串口处于关闭状态,请连接串口");
      }
    },
    async getPorts() {
      await this.myserialport.getPorts();
      this.getPortInfo(this.myserialport.state.ports);
    },
    querySearch(queryString, cb) {
      var restaurants = this.restaurants;
      var results = queryString
        ? restaurants.filter(this.createFilter(queryString))
        : restaurants;
      // 调用 callback 返回建议列表的数据
      cb(results);
    },
    createFilter(queryString) {
      return (restaurant) => {
        return (
          restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) ===
          0
        );
      };
    },
    loadAll() {
      return [
        { value: "110" },
        { value: "300" },
        { value: "600" },
        { value: "1200" },
        { value: "2400" },
        { value: "4800" },
        { value: "7200" },
        { value: "9600" },
        { value: "14400" },
        { value: "19200" },
        { value: "28800" },
        { value: "38400" },
        { value: "56000" },
        { value: "57600" },
        { value: "76800" },
        { value: "115200" },
        { value: "230400" },
        { value: "460800" },
      ];
    },
  },
};
</script>

<style scoped>
/* ::v-deep .el-textarea__inner {
  height: 320px !important;
  width: 80% !important;
} */
</style>

三:示例截图

授权界面:

授权成功后:

使用串口工具调试发送和接收: 

1、使用vspd创建一个对虚拟串口,com1和com2

2、网页的连接com1,sscom连接com2就可以进行通信了

有关Vue使用Serial连接串口的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  8. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  9. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  10. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

随机推荐