uniapp网页使用串口通信并打包为上位机软件

176 阅读6分钟

 一、网页串口通信

网页串口通信使用的是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函数同级下就行

转存失败,建议直接上传图片文件​编辑