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