1. 前言: 在Vue中如何连接串口,需要首先了解串口是什么,如下图
简单来说,串口就是一个通信协议,功能就是收发数据
2. js的宿主环境
问: 为什么需要了解js的宿主环境
呢?
答: 因为在不同的宿主环境中,js的API是不一样的,例如在浏览器宿主环境
中,可以使用DOM,BOM
等API,但是这些API就无法再Nodejs
中使用,同样的道理,Nodejs
中的fs模块,path模块
等也无法在浏览器宿主环境中使用
问题所在: 连接串口,需要Nodejs环境支持,因为串口并不是一个网络通信,而是通过串口线连接COM扣实现的,所以需要使用到电脑本身的一些API,这些API浏览器端是不支持的,只有Nodejs支持
如何解决问题: 使用Vue3+Vite+Electron
框架实现,因为Electron是构建桌面端应用的,并且Electron已经内置了浏览器内核,所以他的宿主环境不是浏览器环境,而是nodejs
环境,所以在这个框架中可以直接使用Nodejs的API
3. 框架搭建(这块内容比较繁琐,我后面专门写一篇文章来讲解)
4. 串口连接
-
第三方库下载:
serialport
(可以通过npm 或 yarn进行下载 npm i serialport 或 yarn add serialport) -
使用
serialport
对串口进行连接
js
// 导入串口第三方库(现在是Nodejs环境,所以可以使用require)
const { SerialPort } = window.require("serialport");
// 建立串口连接
const sp = new SerialPort({
// 要连接的串口号
path: "COM1",
// 比特率
baudRate: 9600,
// 数据位
dataBits: 8,
// 校验位
parity: "none"
});
// 监听串口是否连接成功
sp.on("open", () => {
console.log("串口连接成功")
});
// 接受串口消息
sp.on("data",data => {
console.log(data+":接收到的串口数据")
})
// 监听串口错误信息
sp.on("error",error => {
console.log(error+":串口错误信息")
})
// 发送串口消息
sp.write("我是发送过去的串口信息")
// 手动触发关闭串口连接
sp.close()
5. 串口号查询
如果不清楚要连接的串口号是多少,可以通过以下方式来查找
右击win徽标键
选择设备管理器
选择端口(COM和LPT)
tips: 如果没有这个选项,就表示你的设备没有连接任何串口,请连接串口后重试
通过以上三步就可以找到想要连接的COM口了
6. 本地调试串口
-
建立虚拟串口软件
如果没有串口线连接,但是有需要开发的,可以自己本地建立一个虚拟串口用于调试使用,这个软件Virtual Serial Port Tools可以新建串口(虽然软件是收费的,但是貌似这里的建立串口连接不需要收费)
软件下载完成后进入软件,然后鼠标悬停第一个选项并点击create local brideg创建串口
然后就可以选择收发的两端COM口的串口号了
点击
create
,显示出如下图这种情况就是虚拟串口创建完毕了也可以点击选项对建立的串口进行关闭
-
串口通信调试软件
可以直接去电脑的微软应用商店下载一个应用叫 串口调试助手
7. 自己开发一个串口通信调试助手(以下是基于Vite+Vue2+Electron框架实现的,正常的Vite+Vue是无法实现的,后面我会分享这个框架)
js
<template>
<div class="container">
<aside class="aside">
<el-button size="mini" type="danger" icon="el-icon-back" @click="$router.back()">返回</el-button>
<!-- 串口号 -->
<el-row class="aside_row" type="flex" justify="space-between" align="middle">
<span>串口号</span>
<div>
<i @click="refreshSerialPort" class="el-icon-refresh"></i>
<el-select :disabled="isOpen" style="width:150px" size="mini" v-model="currentSerialPort">
<el-option v-for="item in serialPortList" :value="item.path" :key="item.path">{{ item.path }}</el-option>
</el-select>
</div>
</el-row>
<!-- 波特率 -->
<el-row class="aside_row" type="flex" justify="space-between" align="middle">
<span>波特率</span>
<div>
<i @click="writeBaud" class="el-icon-edit"></i>
<el-select :disabled="isOpen" v-if="!isHandBaud" style="width:150px" size="mini" v-model="currentBaud">
<el-option v-for="item in baudList" :value="item" :key="item">{{
item
}}</el-option>
</el-select>
<el-input :disabled="isOpen" style="width:150px" size="mini" v-else v-model="currentBaud"></el-input>
</div>
</el-row>
<!-- 数据位 -->
<el-row class="aside_row" type="flex" justify="space-between" align="middle">
<span>数据位</span>
<div>
<el-select :disabled="isOpen" style="width:150px" size="mini" v-model="currentDataBit">
<el-option v-for="item in dataBits" :value="item" :key="item">{{
item
}}</el-option>
</el-select>
</div>
</el-row>
<!-- 校验位 -->
<el-row class="aside_row" type="flex" justify="space-between" align="middle">
<span>校验位</span>
<div>
<el-select :disabled="isOpen" style="width:150px" size="mini" v-model="currentVaild">
<el-option v-for="item in vaildBits" :value="item" :key="item">{{
item
}}</el-option>
</el-select>
</div>
</el-row>
<el-button style="width:100%" @click="openSerialPort" :type="isOpen ? 'primary' : 'none'">{{ isOpen ? "关闭" : "打开"
}}串口</el-button>
<el-button style="width:100%;margin: 10px auto;" :type="isUpdateFile ? 'primary' : 'none'" @click="downloadFile">{{
isUpdateFile ? "不保存到文件" : "保存到文件" }}</el-button>
<!-- 是否转换为JSON -->
<el-row type="flex" justify="center">
<el-switch v-model="conversionJSON" inactive-text="是否转换为JSON" :inactive-value="false" :active-value="true">
</el-switch>
</el-row>
</aside>
<main class="main">
<div class="content_area">
<el-row type="flex" justify="space-between" style="padding-right:10px;margin: 5px 0;">
<span style="margin:5px">发送区域:</span>
<el-button @click="sendContent = ''" type="info" size="mini" style="margin-bottom: 5px;">清空</el-button>
</el-row>
<el-input @wheel.native="wheelFn" disabled class="content" type="textarea" :rows="15"
v-model="sendContent"></el-input>
<el-row type="flex" justify="space-between" style="padding-right:10px;margin: 5px 0;">
<span style="margin:5px">接收区域:</span>
<el-button @click="receiveContent = ''" type="info" size="mini" style="margin-bottom: 5px;">清空</el-button>
</el-row>
<el-input @wheel.native="wheelFn" disabled class="content" type="textarea" :rows="15"
v-model="receiveContent"></el-input>
</div>
<div class="footer">
<el-input @keypress.native="handleKeyDown" v-model="sendValue" type="textarea" :rows="7"></el-input>
<div class="submit" @click="sendMsg">
<i class="el-icon-position"></i>
</div>
</div>
</main>
</div>
</template>
<script>
// 导入串口第三方库
const { SerialPort } = window.require("serialport");
// 导入ipcRenderer,用于和electron主线程通信
const { ipcRenderer } = window.require("electron");
export default {
data() {
return {
port: "",
// 串口列表
serialPortList: [],
currentSerialPort: localStorage.getItem("currentSerialPort") || "COM1",
// 波特率列表
baudList: [
300,
600,
1200,
4800,
9600,
14400,
19200,
38400,
56000,
57600,
115200,
12800,
25600,
460800,
512000,
750000,
921600,
1500000
],
currentBaud: 512000,
// 是否手动写入波特率
isHandBaud: false,
// 数据位
dataBits: [5, 6, 7, 8],
currentDataBit: 8,
// 校验位
vaildBits: ["even", "mark", "none", "odd"],
currentVaild: "none",
receiveContent: "",
sendContent: "",
sendValue: "",
// 串口是否打开
isOpen: localStorage.getItem("isOpen") === "true",
// 是否允许保存数据到文件
isUpdateFile: localStorage.getItem("isUpdateFile") === "true",
// 是否转换为JSON
conversionJSON: true,
// 是否滚动到底部
isBottom: true
};
},
async created() {
this.serialPortList = await SerialPort.list();
},
methods: {
// 发送消息
async sendMsg() {
if (!this.port || (await SerialPort.list().length) === 0) {
// 关闭串口
this.isOpen = false;
localStorage.setItem("isOpen", "false");
return this.$message({ message: "串口未连接", type: "warning" });
}
let obj = this.sendValue.split(',').map(item => Number(item))
this.port.write(
this.conversionJSON ? JSON.stringify(obj) : this.sendValue
); // 发送字符串
this.sendContent += this.conversionJSON
? JSON.stringify(obj)
: this.sendValue;
// 文本域滚动到底部
this.scrollBottom();
},
// 连接/关闭串口
async openSerialPort() {
// 获取选中的串口
let serialPortItem = this.serialPortList.find(
item => item.path === this.currentSerialPort
);
console.log(await SerialPort.list(), "serialPortItem");
// 关闭串口
if (this.isOpen) {
// 关闭串口
console.log(this.port, "关闭串口");
this.isOpen = false;
localStorage.setItem("isOpen", "false");
this.port && this.port.close();
} else {
// 打开串口
console.log("打开串口");
this.isOpen = true;
localStorage.setItem("isOpen", "true");
// 连接串口,配置对应的数据
this.port = new SerialPort({
// 连接的串口信息数据
...serialPortItem,
// 比特率
baudRate: this.currentBaud,
// 数据位
dataBits: this.currentDataBit,
// 校验位
parity: this.currentVaild
});
this.port.on("open", () => {
localStorage.setItem("currentSerialPort", this.port.path);
});
// 接收消息
this.port.on("data", data => {
this.receiveContent += data.toString();
console.log(`接收到了消息:`, data.toString());
// 数据传递给electron主线程,主线程保存到文件中
if (this.isUpdateFile) {
console.log("保存文件");
ipcRenderer.send("serial-port-message", data.toString());
}
// 文本域滚动到底部
this.scrollBottom();
});
// 监听错误
let that = this;
that.port.on("error", function (err) {
// 串口正在打开错误不提示
if (err.toString() === "Error: Port is opening") return;
that.$message({ message: err, type: "error" });
console.log("errorr");
that.isOpen = !that.isOpen;
localStorage.setItem("isOpen", that.isOpen);
});
}
},
// 刷新串口列表
async refreshSerialPort() {
this.serialPortList = await SerialPort.list();
console.log("已刷新");
},
// 手动写入波特率
writeBaud() {
this.isHandBaud = !this.isHandBaud;
// 判断是否为下拉
if (!this.isHandBaud) {
// 判断下拉列表中是否有对应的值
this.currentBaud =
this.baudList.find(item => item === Number(this.currentBaud)) || 9600;
}
},
// 切换是否保存文件
downloadFile() {
this.isUpdateFile = !this.isUpdateFile;
localStorage.setItem("isUpdateFile", this.isUpdateFile);
},
// 按下 Shift 和 Enter 键时发送消息
handleKeyDown(event) {
if (event.shiftKey && event.keyCode === 13) {
this.sendMsg();
}
},
// 用户滚动鼠标时取消滚动到最底部行为
wheelFn() {
this.isBottom = false;
},
// 默认滚动到最底部,用户控制时取消,三秒后恢复
scrollBottom() {
this.$nextTick(() => {
const textarea = document.querySelectorAll(".content textarea");
if (this.isBottom) {
for (let i = 0; i < textarea.length; i++) {
textarea[i].scrollTo({
top: textarea[i].scrollHeight,
behavior: "smooth"
});
}
}
});
}
},
watch: {
isBottom(val) {
if (!val) {
setTimeout(() => {
this.isBottom = true;
}, 1500);
}
}
},
mounted() { },
beforeDestroy() {
// 关闭与electron主线程的通信通道
ipcRenderer.removeAllListeners("message");
// 获取连接的所有串口
SerialPort.list().then(ports => {
// 关闭每个链接
ports.forEach(port => {
console.log(port, "port");
if (!port) return;
let sp = new SerialPort({ ...port, baudRate: 9600 });
sp.close();
});
});
}
};
</script>
<style scoped>
.container {
display: flex;
width: 100%;
max-height: 92.5vh;
}
.aside {
flex: 1;
min-height: 100vh;
background-color: #f2f2f2;
padding: 10px;
box-sizing: border-box;
}
.main {
display: flex;
flex-direction: column;
flex: 6;
min-height: 100vh;
padding: 5px;
}
.footer {
flex: 2;
display: flex;
box-sizing: border-box;
padding-top: 5px;
margin-bottom: 60px;
}
.content_area {
flex: 8;
}
.aside_row {
margin: 5px 0;
}
.submit {
width: 200px;
height: 85%;
margin-left: 10px;
border-radius: 8px;
background: #cccccc;
font-size: 65px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.submit:hover,
.submit:active {
background-color: #999999;
}
.el-icon-position {
transform: rotate(45deg);
}
</style>
效果图: