本来只是随手记录一下,发现看的人多了,想着还是修复一下bug吧,供各位看官指正
2022-10-24本次更新:
1、修复在不支持Serial的情况下,控制台报错
2022-09-19本次更新:
1、修复了传输数据接收分隔的情况(增加数据缓存)
2、修复串口连接没有使用选择的波特率等参数
1、
Serial
接口是 Web Serial API的接口,提供了从网页查找和连接串口的属性和方法。注意:
只能在部分支持
Serial
并且网站为安全上下文(HTTPS)中可用,或者是本机访问
一:常用API介绍
-
requestPort----获取授权串口
-
open-----打开串口
-
close---关闭串口(串口关闭前,需要释放锁住的流)
-
cancel---立即退出读取的循环,然后去调用releaseLock,最后调用close方法
-
releaseLock---Reader和.Writer的释放方法
-
read---port.readable.getReader()的读取字节数组方法
-
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就可以进行通信了