在web端使用webSerial实现串口通信
参考资料
使用React和Web Serial API 开发的串口调试工具
简介
串口是一个双向通信接口,允许字节发送和接收数据。
Web Serial API
为网站提供了一种使用JavaScript
对串行设备进行读写的方法。串行设备可以通过用户系统上的串行端口连接,也可以通过模拟串行端口的可移动USB和蓝牙设备连接。换句话说,
Web Serial API
通过允许网站与串行设备(如微控制器和3D打印机)通信来连接网络和物理世界。这个API也是
WebUSB
的好伙伴,因为操作系统要求应用程序使用它们的高级串行API而不是低级的USB API
与一些串行端口通信。
检查浏览器是否支持Web Serial API
if ("serial" in navigator) {
// The Web Serial API is supported.
}
打开串口
Web Serial API
在设计上是异步的。这可以防止网站UI在等待输入时阻塞,这一点很重要,因为串行数据可以在任何时候接收,需要一种方法来侦听它。要打开串口,首先访问一个SerialPort
对象。为此,您可以通过调用navigator.serial.requestPort()
来提示用户选择一个串行端口,或者从navigator.serial.getPorts()
中选择一个,该方法返回一个先前授予该网站访问权限的串行端口列表。
// 提示用户选择一个串口。
const port = await navigator.serial.requestPort();
// 获取用户之前授予该网站访问权限的所有串口。
const ports = await navigator.serial.getPorts();
您还可以在打开串行端口时指定下面的任何选项。这些选项是可选的,并且有方便的默认值。
dataBits
:每帧的数据位数(7或8)。stopBits
:一帧结束时的停止位数(1或2)。parity
:校验模式,可以是none,偶数,奇数。bufferSize
:应该创建的读写缓冲区大小(必须小于16MB)。flowControl
:流控模式(none或hardware)。
串口中读取数据
Web Serial API
中的输入和输出流由streams API
处理。串口连接建立之后,
SerialPort
对象的readable
和writable
属性返回一个ReadableStream
和一个WritableStream
。这些将用于从串行设备接收数据并将数据发送到串行设备。两者都使用Uint8Array
实例进行数据传输。当新数据从串行设备到达时,
port.readable.getReader().read()
异步返回两个属性:value
和一个done
的布尔值。如果done
为真,则串行端口已经关闭,或者没有更多的数据输入。调用port.readable.getReader()
创建一个读取器并将其锁定为readable
。当可读被锁定时,串口不能被关闭。
const reader = port.readable.getReader();
// 监听来自串行设备的数据
while (true) {
const { value, done } = await reader.read();
if (done) {
// 允许稍后关闭串口。
reader.releaseLock();
break;
}
// value 是一个 Uint8Array
console.log(value);
}
如果串行设备发送文本返回,您可以管道端口。可通过
TextDecoderStream
读取,如下所示。TextDecoderStream
是一个转换流,抓取所有的Uint8Array
块并将其转换为字符串。
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// 监听来自串行设备的数据。
while (true) {
const { value, done } = await reader.read();
if (done) {
// 允许稍后关闭串口。
reader.releaseLock();
break;
}
// value 是一个 string.
console.log(value);
}
写入串口
要将数据发送到串行设备,请将数据传递到
port.writable.getWriter().write()
。在port.writable. getwriter()
上调用releaseLock()
是为了稍后关闭串口。
const writer = port.writable.getWriter();
const data = new Uint8Array([104, 101, 108, 108, 111]); // hello
await writer.write(data);
// 允许稍后关闭串口。
writer.releaseLock();
通过管道传输到端口的
TextEncoderStream
向设备发送文本。port.writable
如下所示。
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
关闭串口
port.close()
如果串行端口的readable
和writable
被解锁,则关闭该串行端口,这意味着已经为其各自的读写成员调用了releaseLock()
。但是,当使用循环从串行设备连续读取数据时,端口
Readable
将一直被锁定,直到遇到错误。在这种情况下,调用reader.cancel()
将强制reader.read()
立即解析为{value: undefined, done: true}
,从而允许循环调用reader.releaseLock()
。
await state.reader.cancel()
await state.writer.close()
await state.port.close()
监听串口设备的插入和拔出
如果一个串行端口是由USB设备提供的,那么该设备可以从系统连接或断开。当网站被授予访问串口的权限时,它应该监控连接和断开事件。
navigator.serial.addEventListener("connect", (event) => {
// TODO: 自动打开事件。目标器或警告用户端口可用。
});
navigator.serial.addEventListener("disconnect", (event) => {
// TODO: Remove |event.target| from the UI.
// 如果打开了串行端口,还会观察到流错误。
});
项目开发串口通信代码
import { buf2hex, hexCharCodeToStr, decToHex } from "../utils/utils"
import { message } from "ant-design-vue"
import inputObject from "../utils/config"
import { takingPictures } from "../../api/collect"
const serialPort = {
state: {
step: 0, // 步骤值
inputObject,
writer: null,
reader: null,
port: null,// 连接的串口
isConnect: false, // 是否连接
equipmentInfo: '', // 设备信息
dataSource: [],//源数据
code: '', // 存储接收到的数据的指令
sendData: [], // 发送的数据
receiveData: [], // 接收的数据
hexMessage: "",// 接收到的完整16进制字符串
checkResults: false, // 检验结果
isAwait: false, // 是否处于等待接收数据状态(断包时记录该状态)
timerOut: null, // 数据超时无响应
support: false, // 是否支持navigator.serial 串口通讯
isStop: false, // 机器人是否处于暂停状态
orderInfo: {//选择的订单信息
id: "",//订单id
sn: "",//订单号
size: "",//码数
},
subOrder: [],//创建的子订单
},
mutations: {
// 监听串口的连接的断开
listenersSerialPort (state) {
navigator.serial.addEventListener('connect', (event) => {
message.success("串口设备已插入")
// TODO: Automatically open event.target or warn user a port is available.
})
navigator.serial.addEventListener('disconnect', (event) => {
// state.isConnect = true
message.error("串口设备已拔出")
// console.log('串口已断开')
// TODO: Remove |event.target| from the UI.
// If the serial port was opened, a stream error would be observed as well.
})
},
// 移除监听
removeListenersSerialPort () {
navigator.serial.removeEventListener('connect', (event) => {
message.success("串口设备已插入")
// TODO: Automatically open event.target or warn user a port is available.
})
navigator.serial.removeEventListener('disconnect', (event) => {
// state.isConnect = true
message.error("串口设备已拔出")
// console.log('串口已断开')
// TODO: Remove |event.target| from the UI.
// If the serial port was opened, a stream error would be observed as well.
})
},
// 修改浏览器是否支持串口通讯的标识
updateSupport (state, flag) {
state.support = flag
},
// 添加表格数据
addDataSource (state, data) {
state.dataSource = []
data.forEach((item, index) => {
item.step = index + 1
item.key = index + 1
})
state.dataSource = data
console.log("dataSource", state.dataSource)
},
// 删除表格数据
deleteDataSource (state, key) {
let index = state.dataSource.findIndex((item) => {
return item.key == key
})
let list = state.dataSource
list.splice(index, 1)
state.dataSource = []
state.dataSource.push(...list)
},
// 修改表格数据
modifyDataSource (state, obj) {
let { index, data } = obj
let list = state.dataSource
list.splice(index, 1, data)
state.dataSource = []
state.dataSource.push(...list)
},
// 接收数据
receiveDataFuc (state, value) {
console.log('value', value)
// value 是一个 Uint8Array
let hexStr = buf2hex(value)
let msg = ''
this.commit('dataValidation', hexStr)
console.log('isAwait', state.isAwait)
if (state.isAwait) {
return
}
if (!state.checkResults) {
state.sendData = []
state.receiveData = []
console.log('进入了!this.checkResults')
if (state.code == '0c') {
this.dispatch('submitData')
} else {
message.error("指令执行失败,请尝试从新发送")
}
return
}
msg = state.hexMessage.slice(6, hexStr.length - 4)
console.log('指令类型', state.code)
console.log('返回的信息', msg)
switch (state.code) {
case '06': // 获取设备信息
let info = hexCharCodeToStr(msg)
state.equipmentInfo = info
let arr = info.split(' ')
state.equipmentInfo = arr.join(" ")
console.log('arr', arr, state.equipmentInfo)
state.receiveData = []
break
case '0a':// 接收霍尔传感器数据
console.log("霍尔数据", msg)
// 偶数圈发送拍摄型号
// 当前圈数等于总圈数发送结束信号
let data1 = msg.slice(0, 2)
let data2 = msg.slice(2, 4)
let data3 = msg.slice(4, 6)
let data4 = msg.slice(6, msg.length)
let turns = parseInt(data1, 16) * 256 + parseInt(data2, 16)
let turnsCount = parseInt(data3, 16) * 256 + parseInt(data4, 16)
console.log("当前圈数", turns, '总圈数', turnsCount)
state.receiveData = []
this.dispatch("takingPicturesFunc", { turns, turnsCount })
break;
case '10': // 复位机器人
message.success("机器人已经复位")
state.receiveData = []
state.isStop = true
break;
case '11': // 机器人开始工作
message.success("机器人已经开始工作")
state.receiveData = []
state.isStop = false
break;
case '12': // 机器人暂停工作
message.success("机器人已经暂停工作")
state.receiveData = []
state.isStop = true
break;
case '0c': // 收到的形变数据
this.checkResults = false
state.sendData = []
state.receiveData = []
state.step = state.step + 1
if (state.step <= state.dataSource.length - 1) {
this.dispatch('submitData')
} else {
message.success("数据传输完毕")
}
break
}
},
// 数据校验
dataValidation (state, hexStr) {
clearTimeout(state.timerOut)
state.timerOut = null
let startStr = hexStr.slice(0, 2)
let endStr = hexStr.slice(hexStr.length - 2, hexStr.length)
if (startStr == '55' && (endStr == '0a' || endStr == '0d') && hexStr.length > 4) {
this.commit("lengthValidation", hexStr)
} else if (startStr == '55' && endStr != '0a' && endStr != '0d') {
console.log('帧头部分')
state.isAwait = true
state.receiveData.push(hexStr)
} else if (
startStr != '55' &&
endStr != '0a' && endStr != '0d' &&
state.receiveData.length > 0
) {
console.log('收到中间部分')
state.receiveData.push(hexStr)
} else {
console.log('收到帧尾部分')
if (state.receiveData.length > 0) {
state.receiveData.push(hexStr)
let receiveDataStr = state.receiveData.join('')
this.commit("lengthValidation", receiveDataStr)
}
}
},
// 数据长度校验
lengthValidation (state, hexStr) {
console.log("hexStr", hexStr, hexStr.length)
state.code = hexStr.slice(2, 4)
let receiveDataLength = 0
switch (state.code) {
case '06': // 获取设备信息
console.log("设备信息长度校验")
receiveDataLength = 31 * 2
break
case '0a':// 接收霍尔传感器数据
console.log("霍尔数据长度校验")
receiveDataLength = 9 * 2
break;
case '10': // 复位机器人
console.log("复位机器人长度校验")
receiveDataLength = 5 * 2
break;
case '11': // 机器人开始工作
console.log("机器人开始工作长度校验")
receiveDataLength = 5 * 2
break;
case '12': // 机器人暂停工作
console.log("机器人暂停工作长度校验")
receiveDataLength = 5 * 2
break;
case '0c': // 收到的形变数据
console.log("形变数据长度校验")
receiveDataLength = state.sendData.length * 2
break;
}
console.log(hexStr.length, receiveDataLength)
hexStr.length == receiveDataLength
? (state.checkResults = true)
: (state.checkResults = false)
state.hexMessage = hexStr
state.isAwait = false
},
// 表格数据重置
dataReset (state) {
state.dataSource = []
state.step = 0
},
// 更新选择的订单号
setOrderInfo (state, orderInfo) {
console.log("orderInfo", orderInfo)
state.orderInfo = Object.assign({}, state.orderInfo, orderInfo)
},
// 更新子订单列表
setSubOrder (state, subOrder) {
state.subOrder = subOrder
}
},
actions: {
// 连接(关闭)串口
async connect ({ state }) {
if (!state.support) {
message.error(state.errorMsg)
return
}
if (state.isConnect) {
await state.reader.cancel()
await state.writer.close()
await state.port.close()
state.isConnect = false
message.success("已经断开与串口设备的连接")
return
}
// 提示用户选择一个串口
state.port = await navigator.serial.requestPort()
try {
// 等待串口打开 (baudRate:波特率)
await state.port.open({
baudRate: 115200,
dataBits: 8, // 数据位
stopBits: 1, // 停止位
parity: 'none' // 奇偶校验
})
state.isConnect = true
message.success("串口设备已连接")
// console.log('port', port)
// 串行写入数据
state.writer = state.port.writable.getWriter()
state.reader = state.port.readable.getReader()
this.dispatch("getEquipmentInfo")
// console.log('port.readable', port.readable)
// 监听来自串行设备的数据
while (true) {
const { value, done } = await state.reader.read()
this.commit('receiveDataFuc', value)
if (done) {
// 允许稍后关闭串口。
state.reader.releaseLock()
break
}
}
} catch (error) {
console.log(error)
message.error("串口打开失败,请检查串口是否已经被占用")
}
},
// 发送数据
async submitData ({ state }) {
if (state.dataSource.length == 0) {
message.error("请添加数据")
return
}
clearTimeout(state.timerOut)
state.timerOut = null
// inputObject数据重置
for (let dataKey in state.inputObject) {
state.inputObject[dataKey].sum = 0
}
let commonArray = ['0X55', '0X0C', '0X40']
let stepArray = ['0X00', decToHex(state.step + 1)]
// let currentStep =
// stepArray.push(currentStep)
let allStepArray = ['0X00', decToHex(state.dataSource.length)]
commonArray.push(...[...stepArray, ...allStepArray])
let inputData = []
// 将表格的数据与inputData数据对应
for (let dataKey in state.dataSource[state.step]) {
state.inputObject[dataKey]
? (state.inputObject[dataKey].sum =
state.dataSource[state.step][dataKey])
: ''
}
// 转换inputObject的数据
for (let dataKey in state.inputObject) {
let hex = decToHex(state.inputObject[dataKey].sum)
inputData.push('0X00', hex)
}
commonArray.push(...inputData)
// 校验和
let result = 0
commonArray.forEach((item) => {
result = result + parseInt(item)
})
let resultHex = decToHex(result)
commonArray.push(resultHex)
commonArray.push('0X0D') // 结尾
console.log('commonArray', commonArray)
state.sendData = []
state.sendData.push(...commonArray)
let data = new Uint8Array(commonArray)
await state.writer.write(data)
},
// 获取设备信息
async getEquipmentInfo ({ state }) {
console.log('获取设备信息')
let data = new Uint8Array([
'0X55',
'0X05',
'0X00',
'0X00',
'0X5A',
'0X0D'
])
await state.writer.write(data)
},
// 机器人复位
async robotReset ({ state }) {
if (!state.isConnect) {
message.error("请先连接设备")
return
}
console.log("执行机器人复位指令")
let data = new Uint8Array(['0X55', '0X10', '0X00', '0X00', '0X65', '0x0D'])
await state.writer.write(data)
},
// 机器人开始工作
async robotStar ({ state }) {
if (!state.isConnect) {
message.error("请先连接设备")
return
}
console.log("执行机器人开始工作指令")
let data = new Uint8Array(['0X55', '0X11', '0X00', '0X00', '0X66', '0x0D'])
await state.writer.write(data)
},
// 机器人暂停工作
async robotPause ({ state }) {
if (!state.isConnect) {
message.error("请先连接设备")
return
}
console.log("执行机器人暂停工作指令")
let data = new Uint8Array(['0X55', '0X12', '0X00', '0X00', '0X67', '0x0D'])
await state.writer.write(data)
},
// 拍摄照片
takingPicturesFunc ({ state }, turnNumber) {
let { turns, turnsCount } = turnNumber
let action = turns % 2 == 0 ? 'start' : 'stop'
let index = turns % 2 == 0 ? turns - 2 : turns - 1
this.dispatch("takingPicturesRequest", { action, index })
},
// 拍照的请求
//
takingPicturesRequest ({ state }, { action, index }) {
takingPictures({
action,
ucode: state.subOrder[index].ordersn,
ordersn: state.orderInfo.sn
}).then(res => {
console.log('拍照', res)
if (res == "Not Found") {
this.dispatch("takingPicturesRequest", { action, index })
} else {
console.log(`相机${action}`)
}
// if(res.data.)
}).catch(error => {
console.log("拍照错误", error)
})
}
},
getters: {
// 获取表格数据
getDataSource: state => state.dataSource,
// 获取inputObject
getInputObject: state => state.inputObject,
// 获取连接状态
getIsConnect: state => state.isConnect,
// 获取设备信息
getEquipmentInfo: state => state.equipmentInfo,
// 获取步骤值
getStep: state => state.step,
// 获取选择的订单信息
getOrderInfo: state => state.orderInfo
}
}
export default serialPort
javascript数据转换相关函数
// 将Uint8Array转为十六进制
export function buf2hex (buffer) {
return Array.prototype.map
.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2))
.join('')
}
// 将Uint8Array转为十六进制
export function Decoding8arr (uint8array) {
return new TextDecoder('utf-8').decode(uint8array)
}
// 十六进制字符串转字符串
export function hexCharCodeToStr (hexCharCodeStr) {
var trimmedStr = hexCharCodeStr.trim()
var rawStr =
trimmedStr.substr(0, 2).toLowerCase() === '0x'
? trimmedStr.substr(2)
: trimmedStr
var len = rawStr.length
if (len % 2 !== 0) {
alert('Illegal Format ASCII Code!')
return ''
}
var curCharCode
var resultStr = []
for (var i = 0; i < len; i = i + 2) {
curCharCode = parseInt(rawStr.substr(i, 2), 16) // ASCII Code Value
resultStr.push(String.fromCharCode(curCharCode))
}
return resultStr.join('')
}
// 将十六进制转为 Uint8Array
export function Encoding8arr (myString) {
return new TextEncoder('utf-8').encode(myString)
}
// 十进制转为带0X前缀的十六进制
export function decToHex (dec) {
let hex = dec.toString(16)
let result = '0X'
hex.length == 1 ? (result = result + '0' + hex) : (result = result + hex)
return result
}