electron-vue 台称串口对接 SerialPort
大致流程
1.首先找一个串口工具(sscom5.12.1)试试读取串口是否成功连上;
2.创建electron-vue的项目;
3.安装依赖,调整版本,启动项目;(在electron中使用串口_electron 串口_Jack_Kee的博客-CSDN博客)
4.学习SerialPort Usage | Node SerialPort工具;
5.开发入口文件,测试是否成功获取串口信息
6.成功获取串口后,接收台称数据
7.根据台称说明书与协议,解析台称数据(数值,单位,正负,精度...)
8.完成称重需求
9.完成打印需求
10.完成自动更新
11.防止开启多个应用
1.串口工具大概样子
通过切换端口号和波特率,可以查看哪个是台称串口及波特率,我本机的端口号COM3 波特率9600是台秤的串口信息。
2.创建electron项目(起步 · electron-vue)
跟着官方步骤 创建electron-vue项目,成功启动项目(启动,打包)
"electron": "^17.4.11",
3.引入serialport
npm install serialport --save
然后,安装electron-rebuild(用来重新编译serialport包):
npm install --save-dev electron-rebuild
重新编译serialport包
.\node_modules\.bin\electron-rebuild.cmd
提示Rebuild Complete,表示编译完成

4.按照该文章,实现最终效果注意安装依赖版本,
此注明我项目使用的各个版本
"dependencies": {"@serialport/parser-byte-length": "^11.0.0","axios": "^0.18.0","electron-updater": "^5.3.0","element-ui": "^2.8.2","lodash": "^4.17.21","multispinner": "^0.2.1","qrcodejs2": "^0.0.2","serialport": "^11.0.0","vue": "^2.5.16","vue-electron": "^1.0.6","vue-router": "^3.0.1","vuex": "^3.0.1","vuex-electron": "^1.0.0","vuex-persistedstate": "^4.1.0"},"devDependencies": {"ajv": "^6.5.0","babel-core": "^6.26.3","babel-loader": "^7.1.4","babel-minify-webpack-plugin": "^0.3.1","babel-plugin-component": "^1.1.1","babel-plugin-transform-runtime": "^6.23.0","babel-preset-env": "^1.7.0","babel-preset-stage-0": "^6.24.1","babel-register": "^6.26.0","cfonts": "^2.1.2","chalk": "^2.4.1","copy-webpack-plugin": "^4.5.1","cross-env": "^5.1.6","css-loader": "^0.28.11","del": "^3.0.0","devtron": "^1.4.0","electron": "^17.4.11","electron-builder": "^23.6.0","electron-debug": "^1.5.0","electron-devtools-installer": "^2.2.4","electron-rebuild": "^3.2.9","file-loader": "^1.1.11","html-webpack-plugin": "^3.2.0","listr": "^0.14.3","mini-css-extract-plugin": "0.4.0","node-loader": "^0.6.0","node-sass": "^4.9.2","sass-loader": "^7.0.3","style-loader": "^0.21.0","url-loader": "^1.0.1","vue-html-loader": "^1.2.4","vue-loader": "^15.2.4","vue-style-loader": "^4.1.0","vue-template-compiler": "^2.5.16","webpack": "^4.15.1","webpack-cli": "^3.0.8","webpack-dev-server": "^3.1.4","webpack-hot-middleware": "^2.22.2","webpack-merge": "^4.1.3"}
4. SerialPort (注意不同版本使用方式不同)
serialport:10.x.x
import { SerialPort } from 'serialport'
serialport:7.x.x
const SerialPort = require('serialport')
项目操作案例:
const { ByteLengthParser } = require("@serialport/parser-byte-length");
let ports = []; // 串口list
let mainWindow = null; // electron 窗口function registerIpcEvent() {// 发送可选串口SerialPort.list().then((ports) => {setTimeout(() => {mainWindow.webContents.send("send-port-info", ports);}, 1000);});// 关闭窗口app.on("window-all-closed", function () {if (process.platform !== "darwin") {ports.forEach((e) => {e && e.close();});app.quit();}});ipcMain.on("closeApp", () => {ports.forEach((e) => {e && e.close();});app.quit();});// 重新初始化串口ipcMain.on("init-port", (event, args) => {initPort(args);});// 关闭串口ipcMain.on("close-serialport", (event, args) => {const currentPort = ports.find((i) => i.path === args.name);currentPort && currentPort.close();if (currentPort && !currentPort.isOpen) {event.reply("close-serialport", { name: args.name });}ports = ports.filter((e) => e !== currentPort);});// 打开某个串口ipcMain.on("open-serialport", (event, args) => {const serialport = new SerialPort({path: args.name,baudRate: +args.baudRate,},(err) => {console.log(err);if (err) {event.reply("open-serialport", {hasError: true,...args,message: err,});} else {event.reply("open-serialport", args);ports.push(serialport);}});// 根据说明书数据量字节17一次传输 也可以通过其他方式截取const parser = serialport.pipe(new ByteLengthParser({ length: 17 }));parser.on("data", (data) => {// xxxxxxx });serialport.on("open", () => {});});
}function initPort(bool) {if (bool) {SerialPort.list().then((ports) => {setTimeout(() => {mainWindow.webContents.send("send-port-info", ports);}, 1000);});}
}
还可以通过下面的形式截断串口数据
正则匹配:parser-regex
一种转换流,它使用正则表达式来拆分传入的文本。
要使用Regex解析器,请提供一个正则表达式来拆分传入的文本。数据作为可由编码选项控制的字符串发出(默认为utf8)
const { SerialPort } = require('serialport')
const { ReadlineParser } = require('@serialport/parser-readline')
const port = new SerialPort({ path: '/dev/ROBOT', baudRate: 14400 })const parser = port.pipe(new ReadlineParser({ delimiter: '\r\n' }))
parser.on('data', console.log)
分隔符截取:parser-readline
在接收到换行符后发出数据的转换流。若要使用Readline分析器,请提供分隔符(默认为\n)。数据以可由编码选项控制的字符串形式发出(默认为utf8)。
const { SerialPort } = require('serialport')
const { RegexParser } = require('@serialport/parser-regex')
const port = new SerialPort({ path: '/dev/ROBOT', baudRate: 14400 })const parser = port.pipe(new RegexParser({ regex: /[\r\n]+/ }))
parser.on('data', console.log)
5.开发入口文件
const { app, BrowserWindow, ipcMain, Menu, dialog } = require("electron");
const { SerialPort } = require("serialport");
const _ = require("lodash");
const { ByteLengthParser } = require("@serialport/parser-byte-length");
import { updateHandle } from "../renderer/utils/update.js";let ports = [];
const path = require("path");
const fs = require("fs");//读取本地更新配置json文件 包含是否调试 请求地址 更新地址...
const File_path = path.join(path.resolve("./../"), "/config.json").replace(/\\/g, "/");
const config_res = JSON.parse(fs.readFileSync(File_path, "utf-8"));
let mainWindow = null;
const winURL =process.env.NODE_ENV === "development"? `http://localhost:9080`: `file://${__dirname}/index.html`;app.whenReady().then(() => {createWindow();registerIpcEvent();// getPrinterList(); 注释打印config_res.IS_DEBUG && createMenu();app.on("activate", function () {if (BrowserWindow.getAllWindows().length === 0) createWindow();});
});function createWindow() {mainWindow = new BrowserWindow({width: 800,height: 600,frame: config_res.IS_DEBUG, // 是否有边框窗口fullscreen: !config_res.IS_DEBUG, // 全屏webPreferences: {// preload: path.join(__dirname, "preload.js"),nodeIntegration: true,contextIsolation: false,enableRemoteModule: true,webviewTag: true,webSecurity: false,},});config_res.IS_DEBUG ? mainWindow.webContents.openDevTools() : ""; // 生产模式调试开关setTimeout(() => {mainWindow.loadURL(winURL);}, 1000);// 监听崩溃mainWindow.webContents.on("crashed", () => {const options = {type: "error",title: "系统意外终止",message: "可点击以下按钮",buttons: ["点击重启", "直接退出"],};dialog.showMessageBox(options, (index) => {if (index === 0) reloadWindow(mainWindow);else app.quit();});});}function createMenu() {// mainconst template = [{label: "调试",click: function (item, focusedWindow) {if (focusedWindow) {config_res.IS_DEBUG && focusedWindow.toggleDevTools();}},},];const menu = Menu.buildFromTemplate(template);Menu.setApplicationMenu(menu);
}
function initPort(bool) {if (bool) {SerialPort.list().then((ports) => {setTimeout(() => {mainWindow.webContents.send("send-port-info", ports);}, 1000);});}
}function registerIpcEvent() {// 发送可选串口SerialPort.list().then((ports) => {setTimeout(() => {mainWindow.webContents.send("send-port-info", ports);}, 1000);});// 关闭窗口app.on("window-all-closed", function () {if (process.platform !== "darwin") {ports.forEach((e) => {e && e.close();});app.quit();}});ipcMain.on("closeApp", () => {ports.forEach((e) => {e && e.close();});app.quit();});// 重新初始化串口ipcMain.on("init-port", (event, args) => {initPort(args);});// 关闭串口ipcMain.on("close-serialport", (event, args) => {const currentPort = ports.find((i) => i.path === args.name);currentPort && currentPort.close();if (currentPort && !currentPort.isOpen) {event.reply("close-serialport", { name: args.name });}ports = ports.filter((e) => e !== currentPort);});// 打开某个串口ipcMain.on("open-serialport", (event, args) => {const serialport = new SerialPort({path: args.name,baudRate: +args.baudRate,},(err) => {console.log(err);if (err) {event.reply("open-serialport", {hasError: true,...args,message: err,});} else {event.reply("open-serialport", args);ports.push(serialport);}});// 数据量字节截取拉大 获取大区间集合const parser = serialport.pipe(new ByteLengthParser({ length: 17 * 10 }));parser.on("data", (data) => {let dataString = data.toString("hex");sendData(event, {// xxxxx 需要发送给页面的信息});});serialport.on("open", () => {});});
}
function sendData(event, value) {event.sender.send("send-data", value);
}
6.需要展示串口信息的页面
1. 发送init-port 初始化请求
const { ipcRenderer } = require("electron");ipcRenderer.send("init-port", true);
2.页面接收解析后的串口信息函数
data() {return {portNameList: [], // 多称选择openedPort: [], // 已经打开的称信息currPortData: 0.0, // 显示重量currName: "", // 称名称currRate: "9600", // 波特率sign: "", //秤 正 负rateOption: rateOption,// 波特率选择};}, watch: {currName(val, oldval) {if (val !== "" && oldval !== "") {this.openOrclose("open", oldval);}},}, methods: {listSerialPorts() {ipcRenderer.on("open-serialport", (event, args) => {console.log("open-serialport", args);if (args.hasError) {alert(args.message);} else {this.openedPort.push(args.name);console.log("open", this.openedPort);}});ipcRenderer.on("close-serialport", (event, args) => {const index = this.openedPort.findIndex((i) => i === args.name);this.openedPort.splice(index, 1);console.log("close", this.openedPort);});ipcRenderer.on("send-port-info", (event, args) => {for (let port of args) {this.portNameList.push({portName: `称台${this.portNameList.length + 1}`,name: port.path,path: port.path,});}this.currName = this.portNameList && this.portNameList[0].name;console.log("send-port-info获取的串口", this.portNameList);this.openOrclose("open");});ipcRenderer.on("send-data", (event, args) => {console.log("send-data", args, this.resData);this.currPortData = args.value; // 接收传入的信息 用于展示});},openOrclose(type, oldname = "") {if (this.portNameList.length <= 0) {this.$message.error("台称读取失败,请重新进入页面或重新进入系统!");return;}const name = this.currName;const baudRate = this.currRate;if (oldname !== "" && this.openedPort.includes(oldname)) {this.sendMessageToMain("close-serialport", { name: oldname });}if (type === "close") {if (this.openedPort.includes(name)) {this.sendMessageToMain("close-serialport", { name });}} else {this.sendMessageToMain("open-serialport", {name: this.currName,baudRate: this.currRate,});}},
}
7.根据台称说明书与协议,解析台称数据(数值,单位,正负,精度...)
先看说明书梅特勒托利多METTLER TOLEDO

1.从说明书的是数据含有17/18字节 我通过走数据查到是17个字节
![]()
const bufferArray = dataString.split("0d")[1];
// 02 34 30 20 20 20 20 32 30 34 20 20 20 30 30 30 0d
2.连续输出格式 5-10字节代表的称指示重量,11-16字节表示皮重重量

3.数据是没有小数点的要通过解析算出小数点的位置和正负号
![]()
4.状态A,状态B,状态C 需要解析分别表示一些内容

状态A说明,main.js里面具体操作

let dataString = data.toString("hex");const bufferArray = dataString.split("0d")[1]; // 02 34 30 20 20 20 20 32 30 34 20 20 20 30 30 30 0dconst bufferfloat = bufferArray.slice(2, 4); // 小数点位置 截取byte第二位 根据说明书转为二进制查看0,1,2位的值判断小数点位置let float = parseInt(bufferfloat, 16).toString(2).substr(-3); //16进制转为2进制 截取后三位;let de = 1;if (float === "100") {de = 100;} else if (float === "101") {de = 1000;} else if (float === "110") {de = 10000;} else if (float === "111") {de = 100000;}var value = Number(weight) / de;
状态B说明,main.js里面具体操作

let dataString = data.toString("hex");const bufferArray = dataString.split("0d")[1]; // 02 34 30 20 20 20 20 32 30 34 20 20 20 30 30 30 0dconst buffersign = bufferArray.slice(4, 6); // 单位值位置 截取byte第2位 共2位let signCode = parseInt(buffersign, 16).toString(2).substr(-2, 1); //16进制转为2进制 截取倒数第2位; 从倒数第2位开始截取1个长度的字符// signCode 0=正1=负
状态C说明,main.js里面具体操作

let dataString = data.toString("hex");const bufferArray = dataString.split("0d")[1]; // 02 34 30 20 /20 20 20 32 30 34/ 20 20 20 30 30 30 0dconst bufferunit = bufferArray.slice(6, 8); // 单位值位置 截取byte第3位 共2位let unitCode = parseInt(bufferunit, 16).toString(2).substr(-3);let unit = null;if (unitCode === "000") {unit = "kg";} else {unit = "g";}
8.称重需求 main.js 文件处理
// 打开某个串口ipcMain.on("open-serialport", (event, args) => {const serialport = new SerialPort({path: args.name,baudRate: +args.baudRate,},(err) => {console.log(err);if (err) {event.reply("open-serialport", {hasError: true,...args,message: err,});} else {event.reply("open-serialport", args);ports.push(serialport);}});// 数据量字节17为一组 但发送太过频繁 所以增大10倍 减少页面渲染压力const parser = serialport.pipe(new ByteLengthParser({ length: 17 * 10 }));parser.on("data", (data) => {let dataString = data.toString("hex");const bufferArray = dataString.split("0d")[1]; // 02 34 30 20 /20 20 20 32 30 34/ 20 20 20 30 30 30 0dconst bufferfloat = bufferArray.slice(2, 4); // 小数点位置 截取byte第二位 根据说明书转为二进制查看0,1,2位的值判断小数点位置const buffersign = bufferArray.slice(4, 6); // 正负号位置 截取byte第2位 共2位const bufferunit = bufferArray.slice(6, 8); // 单位值位置 截取byte第3位 共2位const bufferweight = bufferArray.slice(8, 20);// 重量值位置 截取byte第5-10位 共6位const bufferpeel = bufferArray.slice(20, 32); // 皮重值位置 截取byte第11-16位 共6位let float = parseInt(bufferfloat, 16).toString(2).substr(-3); //16进制转为2进制 截取后三位;let signCode = parseInt(buffersign, 16).toString(2).substr(-2, 1); //16进制转为2进制 截取倒数第2位; 从倒数第2位开始截取1个长度的字符。let unitCode = parseInt(bufferunit, 16).toString(2).substr(-3);let weight = hexToString(bufferweight.toString("hex"));let peel = hexToString(bufferpeel.toString("hex"));let de = 1;let unit = null;let sign = signCode; // sign 0=正1=负if (float === "100") {de = 100;} else if (float === "101") {de = 1000;} else if (float === "110") {de = 10000;} else if (float === "111") {de = 100000;}if (unitCode === "000") {unit = "kg";} else {unit = "g";}var value = Number(weight) / de;console.log("weights",value,float,weight,unit,peel,bufferArray,dataString,sign);sendData(event, {value,float,weight,unit,de,peel,bufferArray,dataString,sign});});serialport.on("open", () => {});});function sendData(event, value) {event.sender.send("send-data", value);
}/*** 将16进制字符串进行分组,每两个一组* @param {[String]} str [16进制字符串]* @return {[Array]} [16进制数组]*/
//处理中文乱码问题
function utf8to16(str) {var out, i, len, c;var char2, char3;out = "";len = str.length;i = 0;while (i < len) {c = str.charCodeAt(i++);switch (c >> 4) {case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:out += str.charAt(i - 1);break;case 12:case 13:char2 = str.charCodeAt(i++);out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));break;case 14:char2 = str.charCodeAt(i++);char3 = str.charCodeAt(i++);out += String.fromCharCode(((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0));break;}}return out;
}function hexToString(str) {// 30 空格 0d跨行var val = "",len = str.length / 2;for (var i = 0; i < len; i++) {val += String.fromCharCode(parseInt(str.substr(i * 2, 2), 16));}return utf8to16(val);
}
detail.vue 页面组装秤重量
console.log("weights",value,float,weight,unit,peel,bufferArray,dataString,sign);// 接收到数据 处理数据展示样子 带上小数点,正负号,显示称设置的皮重ipcRenderer.on("send-data", (event, args) => {if (this.resData &&["kg", "千克", "公斤"].includes(this.resData.unit)) {if (args.unit === "kg") {this.currPortData = args.value;} else {this.currPortData = args.value / 1000;}} else if (["g", "克", "斤", "ml", "毫升"].includes(this.resData.unit)) {if (args.unit === "g") {this.currPortData = args.value;} else {this.currPortData = args.value * 1000;}}if (args.sign !== "") {this.sign = args.sign === "0" ? "" : "-";}});
9.完成打印需求
1.主进程中初始化的时候请求打印机列表
2.发送事件到渲染进程,带上获取到的打印机列表参数
3.编写页面: webview 标签嵌入静态html文件path 获取打印机列表函数 触发打印的函数 监听渲染完成事件
4.外部触发打印组件的打印函数 传入需要打印的数据
5.打印组件 执行打印函数 获取列表 判断打印机状态 执行打印 获取打印参数 发起webview.send 传输数据
6.静态文件通过ipcRenderer.on("webview-print-render", xxx )接收打印组件发起的数据传输
7.静态文件通过传入的参数 渲染出正确的html页面 通过webview-print-do 发出渲染完成的消息
8.打印组件通过监听webview.addEventListener("ipc-message" ,xxx)执行
webview.print 打印成功
查询打印机列表方法
1.主进程中初始化的时候请求打印机列表
2.发送事件到渲染进程,带上获取到的打印机列表参数
function getPrinterList() {// 在主线程下,通过ipcMain对象监听渲染线程传过来的getPrinterList事件ipcMain.on("getPrinterList", async (event) => {//在主线程中获取打印机列表const list = await mainWindow.webContents.getPrintersAsync();console.log(list, "getPrinterList");//通过webContents发送事件到渲染线程,同时将打印机列表也传过去mainWindow.webContents.send("getPrinterList", list);});
}
app初始化的时候获取打印机列表
app.whenReady().then(() => {getPrinterList();});
打印机监控页面print.vue组件 等待发起打印需求
3.编写页面: webview 标签嵌入静态html文件path 获取打印机列表函数 触发打印的函数 监听渲染完成事件
发起打印请求页面,传入参数
4.外部触发打印组件的打印函数 传入需要打印的数据
goPrint() {this.BQHtmlData = {//xxxx};// 调用print组件的打印函数this.$refs.pos_bq_print.print(this.BQHtmlData); // 打印杯贴 }
5.打印组件 执行打印函数 获取列表 判断打印机状态 执行打印 获取打印参数 发起webview.send 传输数据
6.静态文件通过ipcRenderer.on("webview-print-render", xxx )接收打印组件发起的数据传输
7.静态文件通过传入的参数 渲染出正确的html页面 通过webview-print-do 发出渲染完成的消息
静态html文件
标签
产品: {{product.prodName }}xxxxxxxxxxxx{{ product.date }}
8.打印组件通过监听webview.addEventListener("ipc-message" ,xxx)执行
webview.print 打印成功
10.完成自动更新
自动更新监控文件
import { autoUpdater } from "electron-updater";import { ipcMain } from "electron";import { app } from "electron";let mainWindow = null;export function updateHandle(window, feedUrl) {mainWindow = window;let message = {error: "检查更新出错",checking: "正在检查更新……",updateAva: "检测到新版本,正在下载……",updateNotAva: "现在使用的就是最新版本,不用更新",};//设置更新包的地址autoUpdater.setFeedURL(feedUrl);//监听升级失败事件autoUpdater.on("error", function (error) {sendUpdateMessage({cmd: "error",message: error,});});//监听开始检测更新事件autoUpdater.on("checking-for-update", function (message) {sendUpdateMessage({cmd: "checking-for-update",message: message,});});//监听发现可用更新事件autoUpdater.on("update-available", function (message) {sendUpdateMessage({cmd: "update-available",message: message,});});//监听没有可用更新事件autoUpdater.on("update-not-available", function (message) {sendUpdateMessage({cmd: "update-not-available",message: message,});});// 更新下载进度事件autoUpdater.on("download-progress", function (progressObj) {sendUpdateMessage({cmd: "download-progress",message: progressObj,});});//监听下载完成事件autoUpdater.on("update-downloaded",function (event, releaseNotes, releaseName, releaseDate, updateUrl) {sendUpdateMessage({cmd: "update-downloaded",message: {releaseNotes,releaseName,releaseDate,updateUrl,},});//退出并安装更新包autoUpdater.quitAndInstall();});// 接收渲染进程消息,开始检查更新ipcMain.on("checkForUpdate", (e, arg) => {//执行自动更新检查// sendUpdateMessage({cmd:'checkForUpdate',message:arg})autoUpdater.checkForUpdates();});}//给渲染进程发送消息
function sendUpdateMessage(text) {mainWindow.webContents.send("message", text);
}
mian.js进程 引入文件
import { updateHandle } from "../renderer/utils/update.js";function createWindow() {mainWindow = new BrowserWindow({width: 800,height: 600,frame: config_res.IS_DEBUG, // 是否有边框窗口fullscreen: !config_res.IS_DEBUG, // 全屏webPreferences: {// preload: path.join(__dirname, "preload.js"),nodeIntegration: true,contextIsolation: false,enableRemoteModule: true,webviewTag: true,webSecurity: false,},});/*** 检测版本更新* 设置版本更新地址,即将打包后的latest.yml文件和exe文件同时放在* http://xxxx/test/version/对应的服务器目录下,该地址和package.json的publish中的url保持一致*/let feedUrl = config_res.UPDATE_URL; // 更新地址updateHandle(mainWindow, feedUrl);// 监听崩溃
}
以下两个文件为主要资源包 均需要放在在线更新地址里,检测到更新后才会拉去更新

yml文件是主要更新监控是否有更新 一定要替换该文件在在线更新地址里,不然检测不到更新
![]()

11.防止开启多个应用
已经开启后 再次双击打开会打开当前的窗口至于首层
// 防止开启多个应用
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {app.quit();
} else {app.on("second-instance", (event, commandLine, workingDirectory) => {//输入从第二个实例中接收到的数据//有人试图运行第二个实例,我们应该关注我们的窗口if (mainWindow && !mainWindow.isDestroyed()) {if (mainWindow.isMinimized()) {mainWindow.restore();}mainWindow.focus();}});//创建myWindow,加载应用的其余部分,etc...
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

