一、网页串口通信
网页串口通信使用的是Serial 接口,是 Web Serial API的接口,提供了从网页查找和连接串口的属性和方法。
串口通信参考文章Vue使用Serial连接串口_vue串口通信-CSDN博客
示例
<template>
<view>
<el-form ref="form" :model="form" label-width="100px">
<el-row>
<el-col :text="6">
<view>
<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 }">
<view class="name">{{ item.value }}</view>
<text class="addr">{{ item.address }}</text>
</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="发送区设置">
<!-- <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="handleForgetPort">取消授权</el-button>
<el-button type="info" @click="obtainAuthorization">新增授权</el-button>
</el-form-item>
</view>
</el-col>
<el-col :text="10">
<view>
<el-form-item label="接收信息">
<el-input type="textarea" v-model="form.desc" disabled
:autosize="{ minRows: 21, maxRows: 25 }"></el-input>
</el-form-item>
</view>
</el-col>
</el-row>
</el-form>
</view>
</template>
<script>
import MySerialPort from "@/static/MySerialPort.js";
import USBDevice from "@/static/usb.json";
export default {
data() {
return {
input: "",
keepReading: true,
form: {
port: {},
baudRate: "115200",
dataBits: "8",
stopBits: "1",
parity: "none",
flowControl: "none",
desc: "",
type: "1",
isShowHistory: false,
sendMsg: ":r01=0,"
},
btnType: "primary",
btnText: "连接串口",
restaurants: [],
portsList: [],
myserialport: {},
isShowSendArea: false,
readType: 1,
};
},
mounted() {
if ("serial" in navigator) {
this.myserialport = new MySerialPort();
this.getPorts();
navigator.serial.addEventListener("connect", (e) => {
console.log('设备有更新');
this.getPorts();
});
navigator.serial.addEventListener("disconnect", (e) => {
console.log('设备有更新');
this.getPorts();
});
} else {
uni.showToast({
title: "当前为HTTP模式或者浏览器版本过低,不支持网页连接串口",
icon: "error"
});
}
},
onUnload() {
this.myserialport.openPort(this.form.port, false, this.callBack, this.closeBack);
},
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);
}
},
// 设备丢失
closeBack(value) {
console.log(value);
if (value == "disconnect") this.connectBtn();
},
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("");
}
},
async handleForgetPort() {
await this.myserialport.handleForgetPort(this.form.port)
},
readLi() {
let readType = this.readType;
return this.myserialport.state.readValue.map((items, index) => {
const item = items.value;
const type = items.type; // 1接收,2发送
console.log(type);
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);
}
}
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() {
console.log(this.myserialport.state.isOpen);
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, this.closeBack);
} catch (error) {
uni.showToast({
title: "串口连接失败!请检查串口是否已被占用",
icon: "error"
});
}
if (this.myserialport.state.isOpen) {
uni.showToast({
title: "串口连接成功",
icon: "success"
});
this.btnType = "danger";
this.btnText = "关闭串口";
console.log(this.form.port);
}
} else {
this.myserialport.openPort(this.form.port, false, this.callBack, this.closeBack);
uni.showToast({
title: "串口关闭成功",
icon: "success"
});
this.btnType = "primary";
this.btnText = "连接串口";
console.log(this.form.port);
}
},
//授权
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();
uni.showToast({
title: "串口授权成功",
icon: "success"
});
this.getPortInfo(this.myserialport.state.ports);
} catch (error) {
uni.showToast({
title: "未选择新串口授权!",
icon: "exception"
});
}
} else {
uni.showToast({
title: "当前为HTTP模式或者浏览器版本过低,不支持网页连接串口",
icon: "error"
});
}
},
//串口列表初始化
getPortInfo(portList) {
this.portsList = [];
portList.map((port, index) => {
const {
usbProductId,
usbVendorId
} = port.getInfo();
console.log(port.getInfo())
if (usbProductId === undefined || usbVendorId === undefined) {
this.portsList.push({
label: "COM" + (index + 1),
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: usbVendor[0].name + ' ' + 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 = [];
let obj = "";
if (writeType == 1) {
// ASCII
for (let i = 0; i < value.length; i++) {
arr.push(this.myserialport.a2hex(value[i]));
}
arr.push(13);
arr.push(10);
} 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));
}
obj = value;
} else {
uni.showToast({
title: "格式错误",
icon: "error"
});
return;
}
}
this.myserialport.writeText(arr);
} else {
uni.showToast({
title: "请输入发送的信息",
icon: "exception"
});
}
} else {
uni.showToast({
title: "串口处于关闭状态,请连接串口",
icon: "exception"
});
}
},
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 lang="scss">
</style>
el-开头的是element-uiElement - The world's most popular Vue UI framework
usb.json是VueSerial的常见型号download.csdn.net/download/Zh…allsobaiduend~default-2-86662302-null-null.142^v102^pc_search_result_base8&utm_term=usb.json&spm=1018.2226.3001.4187.3
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: "115200",
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) {
console.log(e.toString());
}
}
async handleForgetPort(portIndex) {
// 取消授权
let port = this.state.ports[portIndex];
port.forget()
}
async openPort(portIndex, isOpen, callBack = null, closeBack = 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, closeBack);
}
}
async readUntilClosed(portIndex, callBack = null, closeBack = 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) {
closeBack('disconnect');
console.log(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) {
console.log("串口未打开");
reject();
return;
} else {
let port = this.state.ports[this.state.portIndex];
const writer = port.writable.getWriter();
// await writer.write(value);
await writer.write(new Uint8Array(value));
writer.releaseLock();
resolve(value);
}
});
}
readText(value) {
let newValue = this.state.readValue.concat({
value,
type: 1,
});
this.setState({
readValue: newValue,
});
}
writeText(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);
}
//16进制转字符
hex2a(hexx) {
return String.fromCharCode(hexx);
}
//字符转16进制
a2hex(str) {
return str.charCodeAt(0);
}
}
二、打包上位机软件
uniapp打包为上位机参考我上一篇文章uniapp 打包生成上位机软件及去除顶部导航栏和menu后自定义-CSDN博客
这里有一个问题,就是打包成上位机以后不会弹出串口授权弹窗,所以得在electron的主进程文件main.js里添加默认授权
function serialApiHandle(mainWindow) {
mainWindow.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
// Add listeners to handle ports being added or removed before the callback for `select-serial-port`
// is called.
mainWindow.webContents.session.on('serial-port-added', (event, port) => {
console.log('serial-port-added FIRED WITH', port)
// Optionally update portList to add the new port
})
mainWindow.webContents.session.on('serial-port-removed', (event, port) => {
console.log('serial-port-removed FIRED WITH', port)
// Optionally update portList to remove the port
})
event.preventDefault();
console.log(portList, 'portList')
if (portList && portList.length > 0) {
//默认返回第一个串口id
callback(portList[0].portId)
} else {
// eslint-disable-next-line n/no-callback-literal
callback('') // Could not find any matching devices
}
})
//授权
mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
if (permission === 'serial') {
return true
}
return false
})
//授权
mainWindow.webContents.session.setDevicePermissionHandler((details) => {
if (details.deviceType === 'serial') {
return true
}
return false
})
}
复制到createWindow函数同级下就行
编辑