关键字: Chrome, APP, Serial Port, Javascript, Web
前言
由于工作需要,要实现客户端采集传感器数据,传感器是串口的。串口采集数据这样的应用程序写过不知道多少了,大学毕业论文时都玩过了,乍一看挺简单的,但是和web前端放在一起就懵逼了。什么?居然让javascript这样的脚本去操控硬件?还让不让人玩了?
没有思路,于是打开浏览器搜搜看,有没有别人的解决方案,哟,还真有,翻了几十个相关网页,无非就是这么几种的:
- 使用mscomm32.dll使用串口资源
- 用C#之类的自己写一个dll,然后使用
- 用Node.js 的serial模块实现
- 使用Google的Chrome.serial实现
第一种貌似最简单,看看吧,解决一查这货只支持IE,什么?让我用IE?再见!
第二种,额,算了,已经不用Visual Stadio多年了,下载个环境都得好半天,想起来就麻烦,得了!
第三种,要配置Node.js运行环境,拜托我这是前端,还要和我的服务器端通信,这样太不伦不类了吧,KO!
第四种,好像没得选了吧,使用Google浏览器的API,一看就是我喜欢的那种,一直对Google API顶礼膜拜,这次终于有机会来个亲密接触啦。一查,我去,只能用于开发Chrome App,这是个什么鬼?Chrome Extensions (插件)使用了很多,这个还是听新奇的,就你了!
1 第一性原理
Chrome.serial 可以访问硬件设备资源,比如使用chrome.serial.getDevices()获取PC上可用的串口资源列表,然后我们就可以在列表中选择我们实际设备的串口,然后传入串口参数打开串口,那么该串口资源就可用了。
而Chrome App和Chrome Extesions一样,可以随着Chrome的启动而运行,Chrome App经过适当的配置可以与别的App或者Extension或者Web Page进行数据交互,这样思路就很简单了。
Chrome App 里设置一些监听事件,比如onConnect,OnMessage, Web Page通过发送消息给Chrome App获取串口列表,打开串口,监听串口消息,写入串口数据,关闭串口等操作。
此处应有图
2 关键技术点
2-1. Chrome.serial
官方文档如下:
如果英文不好,可以看百度的文档:
我们主要是用下面的函数:
- getDevices
- connect
- disconnect
- send
- onReceive
- onReceiveError
2-2. Chrome App与Web Page 连接和通信
应用和内容脚本间的通信使用消息传递的方式。两边均可以监听另一边发来的消息,并通过同样的通道回应。消息可以包含任何有效的 JSON 对象(null、boolean、number、string、array 或 object)。对于一次性的请求有一个简单的 API,同时也有更复杂的 API,允许您通过长时间的连接与共享的上下文交换多个消息。另外您也可以向另一个应用发送消息,只要您知道它的标识符,这将在跨应用消息传递部分介绍。
官方文档如下:
百度中文文档如下:
https://chajian.baidu.com/developer/extensions/messaging.html
① Chrome App与Extensions交互
对于简单消息,应该直接使用比较简单的 runtime.sendMessage 方法,该方法分别允许从内容脚本向应用或者反过来发送可通过 JSON 序列化的消息,可选的 callback 参数允许在需要的时候从另一边处理回应。
有时候需要长时间的对话,而不是一次请求和回应。在这种情况下,可以分别使用 runtime.connect 或 tabs.connect 从您的内容脚本建立到应用(或者反过来)的长时间连接。建立的通道可以有一个可选的名称,让您区分不同类型的连接。
使用长时间连接的一种可能的情形为自动填充表单的应用。对于一次登录操作,内容脚本可以连接到应用页面,每次页面上的输入元素需要填写表单数据时向应用发送消息。共享的连接允许应用保留来自内容脚本的不同消息之间的状态联系。
建立连接时,两端都将获得一个 runtime.Port 对象,用来通过建立的连接发送和接收消息。
这里由于是跨应用的消息传递,因此Chrome App的后台线程(background.js)里使用runtime.onMessageExternal 和 runtime.onConnectExternal
对外部Extensions的连接和消息进行监听。
示例代码如下:
// For simple requests:
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.id == blocklistedExtension)
return; // don't allow this extension access
else if (request.getTargetData)
sendResponse({targetData: targetData});
else if (request.activateLasers) {
var success = activateLasers();
sendResponse({activateLasers: success});
}
});
// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
port.onMessage.addListener(function(msg) {
// See other examples for sample onMessage handlers.
});
});
向另一个应用发送消息与在App内部中发送消息类似,唯一的区别是必须传递需要与之通信的App的标识符外部Extensions要建立连接和发送消息可以这样:
// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
function(response) {
if (targetInRange(response.targetData))
chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
});
// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);
② Chrome App与Web Page通信
想要App能与普通网页进行通信,必须在 manifest.json 文件中指定希望与之通信的网站表达式,形如
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
URL 表达式必须至少包含一个二级域名,也就是说禁止使用类似于"*"、"*.com"、".co.uk"和".appspot.com"之类的主机名。在网页中,使用 runtime.sendMessage 或 runtime.connect API 向指定应用或应用发送消息。
App端的代码与Extension通信时是一样的,都是使用runtime.onMessageExternal 和 runtime.onConnectExternal
对外部Extensions的连接和消息进行监听。
3 具体实现
明确了原理和关键技术,接下来就开干了。
3-1. 从0实现一个Chrome App
创建一个工程,目录如下:
文件就这么几个,真正有用的就三个icon.png,manifest.json,serial_interface.js
- icon.png App的图标文件,没什么好说的,注意文件尺寸
- manifest.json App的配置文件,非常重要,Chrome安装App就 依靠该文件
- serial_interface.js App的核心线程文件,所有的功能都由该文件提供
首先来看manifest.json文件:
{
"name": "Serial Port App",
"version": "0.1.0",
"manifest_version": 2,
"description": "The Serial Port Interface provide a simple API interface to interact with your web application, so that your web page can cummunicate with the serial ports on your PC.",
"icons": {
"48": "icon.png"
},
"author": "Matsuri",
"app": {
"background": {
"scripts": ["serial_interface.js"]
}
},
"permissions": [
"serial"
],
"minimum_chrome_version": "33",
"externally_connectable": {
"ids": ["abfobmcfgmkehplchkliafjafdmddakp"],
"matches": ["*://matsuri.163.com/*"]
}
}
关于manifest.json文件的说明,可以看官方文档,当然很多地方都有该文件的介绍:
这里最关键的app、permissions和externally_connectable字段:
- app 指示该应用是一个Chrome App,背景线程执行serial_interface.js里的代码
- permissions 这里只要求了一个权限,可以根据不同的应用场景进行配置
- externally_connectable 指示该应用可以被外部App、Extensions、Web连接,ids里面就是别的Extension的ID,这里我留了一个接口供我自己的插件使用, matches里的地址表达式上一节有详细的介绍,注意必须是二级域名
然后是serial_interface.js:
① 全局变量
先定义两个列表用来管理不同页面的连接和串口资源,getGUID用来生成一个随机的GUID指示某一个串口资源,可以理解为串口资源的指针。
/**
* 当Web端的一个SerialPort实例生成的时候,Web同时就能得到一个chrome.runtime.Port 对象,该对象就是Web连接至本app的句柄。
* 如果连接成功,就把该Port对象以一个独一无二的GUID保存在SerialPort列表中。
* 该GUID 用于指示哪个的SerialPort实例与哪个页面关联。
*/
var serialPort = [];
/**
* 当某个串口打开的时候就把打开该串口的页面的GUID保存到serialConnections 列表中。
* 每个GUID索引就是由chrome.serial API提供的一个特有的连接。
*/
var serialConnections = [];
/**
* 生成一个随机的GUID用于与chrome.runtime.Port 关联。
*/
function getGUID() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
② 监听Web Page连接事件
/**
* 当一个新的SerialPort 创建时就会触发一个外部连接事件
* 1. 生成一个GUID,并以该GUID作为索引将连接port对象保存在serialPort列表中
* 2. 将该GUID发回连接的Web page
*/
chrome.runtime.onConnectExternal.addListener(
function (port) {
var portIndex = getGUID();
serialPort[portIndex] = port;
port.postMessage({
header: "guid",
guid: portIndex
});
port.onDisconnect.addListener(
function () {
serialPort.splice(portIndex, 1);
console.log("Web page closed guid " + portIndex);
}
);
console.log("New web page with guid " + portIndex);
}
);
③ 监听Web Page发送消息事件
/**
* 监听并处理Web page来请求。
* Commands:
* - open -> 请求打开一个串口
* - close -> 请求关闭一个串口
* - list -> 请求获取串口列表
* - write -> 请求向串口发送数据
* - installed -> 请求检查本app是否已安装在浏览器中
*/
chrome.runtime.onMessageExternal.addListener(
function (request, sender, sendResponse) {
console.log(request);
if (request.cmd === "open") {
openPort(request, sender, sendResponse);
} else if (request.cmd === "close") {
closePort(request, sender, sendResponse);
} else if (request.cmd === "list") {
getPortList(request, sender, sendResponse);
} else if (request.cmd === "write") {
writeOnPort(request, sender, sendResponse);
} else if (request.cmd === "installed") {
checkInstalled(request, sender, sendResponse);
}
return true;
});
④ 监听串口接收到数据事件
/**
* 监听并处理串口收到数据事件
* 1. 使用 connectionId 检索serialConnections列表获得页面的GUID
* 2. 将与Web page关联的串口数据直接发送给Web page
*/
chrome.serial.onReceive.addListener(
function (info) {
console.log(info);
var portGUID = serialConnections[info.connectionId];
serialPort[portGUID].postMessage({
header: "serialdata",
data: Array.prototype.slice.call(new Uint8Array(info.data))
});
}
);
⑤ 监听串口错误
/**
* 监听并处理串口错误
* 1. 使用 connectionId 检索serialConnections列表获得页面的GUID
* 2. 将与Web page关联的串口错误直接发送给Web page
*/
chrome.serial.onReceiveError.addListener(
function (errorInfo) {
console.error("Connection " + errorInfo.connectionId + " has error " + errorInfo.error);
var portGUID = serialConnections[errorInfo.connectionId];
serialPort[portGUID].postMessage({
header: "serialerror",
error: errorInfo.error
});
}
);
⑥ 检查是否已安装本app
/**
* 用于检查本app是否一个被安装在Chrome浏览器中。
* 如果已经安装则返回 "ok" 和当前版本信息。
*/
function checkInstalled(request, sender, sendResponse) {
var manifest = chrome.runtime.getManifest();
sendResponse({
result: "ok",
version: manifest.version
});
}
⑦ 获取串口设备列表
/**
* 获取所有连接在本地PC上的串口设备列表。
* 如果没有错误则返回以下内容:
* - path: 物理路径
* - vendorId (optional): 制造商ID
* - productId (optional): 产品ID
* - displayName (optional): 显示名称
*/
function getPortList(request, sender, sendResponse) {
chrome.serial.getDevices(
function (ports) {
if (chrome.runtime.lastError) {
sendResponse({
result: "error",
error: chrome.runtime.lastError.message
});
} else {
sendResponse({
result: "ok",
ports: ports
});
}
}
);
}
⑧ 打开一个串口
/**
* 尝试打开一个串口
* request 必须包含以下:
* info.portName -> 要打开的串口地址
* info.bitrate -> 串口波特率
* info.dataBits -> 串口数据位数 ("eight" or "seven")
* info.parityBit -> 期偶校验位 ("no", "odd" or "even")
* info.stopBits -> 停止位 ("one" or "two")
*
* 如果与串口建立了连接将向web page 返回结果: "ok" 和 connection info,
* 否则返回结果: "error" 和 error: error message
*/
function openPort(request, sender, sendResponse) {
chrome.serial.connect(request.info.portName, {
bitrate: request.info.bitrate,
dataBits: request.info.dataBits,
parityBit: request.info.parityBit,
stopBits: request.info.stopBits
},
function (connectionInfo) {
if (chrome.runtime.lastError) {
sendResponse({
result: "error",
error: chrome.runtime.lastError.message
});
} else {
serialConnections[connectionInfo.connectionId] = request.portGUID;
sendResponse({
result: "ok",
connectionInfo: connectionInfo
});
}
}
);
}
⑨ 关闭一个串口
/**
* 尝试关闭一个串口
* request 必须包含以下:
* connectionId -> 当串口被打开时的连接ID
*
* 如果当前连接被成功关闭将向web page返回结果: "ok" 和 connection info,
* 否则返回结果: "error" 和 error: error message
*/
function closePort(request, sender, sendResponse) {
chrome.serial.disconnect(request.connectionId,
function (connectionInfo) {
if (chrome.runtime.lastError) {
sendResponse({
result: "error",
error: chrome.runtime.lastError.message
});
} else {
serialConnections.slice(connectionInfo.connectionId, 1);
sendResponse({
result: "ok",
connectionInfo: connectionInfo
});
}
}
);
}
⑩ 向串口写入数据
/**
* 向串口写入数据
* request 必须包含以下:
* connectionId -> 当串口被打开时的连接ID
* data -> 要发送的字节流数组
*
* 如果发送成功关闭将向web page返回结果: "ok" 和 串口响应结果,
* 否则返回结果: "error" 和 error: error message
*/
function writeOnPort(request, sender, sendResponse) {
chrome.serial.send(request.connectionId, new Uint8Array(request.data).buffer,
function (response) {
if (chrome.runtime.lastError) {
sendResponse({
result: "error",
error: chrome.runtime.lastError.message
});
} else {
sendResponse({
result: "ok",
sendInfo: response
});
}
}
);
}
以上就是全部的核心代码,累死我了!
3-2 Web端实现
写完了App,来看Web端的javascript怎么写,serial_port.js:
/**
* Web要连接的Chrome App ID
*/
var extensionId = "ojfkhepmmpnpbkjmlipagnflphcpidcm";
app ID是必须的,可以在Chrome的Extensions界面查看,在浏览器的地址栏中输入
这里显示的ID不全,可以点击 Details 按钮看到完整的
function SerialPort() {
// Chrome App 分配的GUID
var portGUID;
// 使用app的ID与app建立外部连接,连接一旦建立,web端和app都想获得一个port对象
var port = chrome.runtime.connect(extensionId);
// 唯一的串口连接ID
var serialConnectionId;
// 指示串口是否打开
var isSerialPortOpen = false;
// 当串口接收到数据时的回调函数,undefined表示它是纯虚函数
var onDataReceivedCallback = undefined;
// 串口报错时的回调函数
var onErrorReceivedCallback = undefined;
/**
* 监听并处理来自app的消息
* 可以处理的消息有(可自行添加):
* - guid -> 当与app连接成功时app发送给web的,用于表示当前页面与app 的连接
* - serialdata -> 当串口有新数据接收时由app发送给web
* - serialerror -> 当串口发生错误时由app发送给web
*/
port.onMessage.addListener(
function (msg) {
console.log(msg);
if (msg.header === "guid") {
portGUID = msg.guid;
} else if (msg.header === "serialdata") {
if (onDataReceivedCallback !== undefined) {
onDataReceivedCallback(new Uint8Array(msg.data).buffer);
}
} else if (msg.header === "serialerror") {
onErrorReceivedCallback(msg.error);
}
}
);
// 检查串口是否已打开
this.isOpen = function () {
return isSerialPortOpen;
}
// 相当于纯虚函数,由web页面的callBack具体实现
this.setOnDataReceivedCallback = function (callBack) {
onDataReceivedCallback = callBack;
}
// 相当于纯虚函数,由web页面的callBack具体实现
this.setOnErrorReceivedCallback = function (callBack) {
onErrorReceivedCallback = callBack;
}
/**
* 尝试打开一个串口
* portInfo 必须包含以下:
* portName -> 串口地址
* bitrate -> 串口波特率
* dataBits -> 数据位 ("eight" or "seven")
* parityBit -> 校验位 ("no", "odd" or "even")
* stopBits -> 停止位 ("one" or "two")
* Callback用来处理app返回的结果,由于sendMessage是异步执行的函数
*/
this.openPort = function (portInfo, callBack) {
chrome.runtime.sendMessage(extensionId, {
cmd: "open",
portGUID: portGUID,
info: portInfo
},
function (response) {
if (response.result === "ok") {
isSerialPortOpen = true;
serialConnectionId = response.connectionInfo.connectionId;
}
callBack(response);
}
);
}
// 关闭一个串口
this.closePort = function (callBack) {
chrome.runtime.sendMessage(extensionId, {
cmd: "close",
connectionId: serialConnectionId
},
function (response) {
if (response.result === "ok") {
isSerialPortOpen = false;
}
callBack(response);
}
);
};
/**
* 向串口写入数据
* request 必须包含以下:
* connectionId -> 串口连接ID
* data -> 要发送的字节流数组
*/
this.write = function (data, callBack) {
chrome.runtime.sendMessage(extensionId, {
cmd: "write",
connectionId: serialConnectionId,
data: Array.prototype.slice.call(new Uint8Array(data))
},
function (response) {
if (response.result === "ok") {
if (response.sendInfo.error !== undefined) {
if (response.sendInfo.error === "disconnected" || response.sendInfo.error === "system_error") {
isSerialPortOpen = false;
closePort(function () {});
}
}
}
callBack(response);
}
);
}
}
好长!是不是?其实挺简单的,就是实现了类,对串口的操作进行了封装而已。
喔,对了,还没完呢!
/**
* 获取所有连接在本地PC上的串口设备列表。
* 如果没有错误则返回以下内容:
* - path: 物理路径
* - vendorId (optional): 制造商ID
* - productId (optional): 产品ID
* - displayName (optional): 显示名称
* Callback 用以处理app返回结果
*/
function getDevicesList(callBack) {
chrome.runtime.sendMessage(extensionId, {
cmd: "list"
}, callBack);
}
// 检查app是否安装在浏览器中
function isAppInstalled(callback) {
chrome.runtime.sendMessage(extensionId, {
cmd: "installed"
},
function (response) {
if (response) {
callback(true);
} else {
callback(false);
}
}
);
}
这两个函数一个对所有串口操作,一个和串口无关所以就没有封装在类中了。
好了,最难的都完了,最后来看点儿简单的吧,serial.html文件
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title></title>
<!-- Bootstrap -->
<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css'>
<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css'>
</head>
<body>
<script src='./jquery.min.js'></script>
<script src='./serial_port.js'></script>
<script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js'></script>
<select id='devices'></select>
<button onclick='realodDevices()'>Reload</button>
<button onclick='openSelectedPort()'>Open</button>
<button onclick='closeCurrentPort()'>Close</button>
<br>
<textarea id="output" rows="10" cols="50"></textarea>
<br>
<input type="text" id="input">
<button onclick='sendData()'>Send</button>
</body>
<script>
isAppInstalled(
function(installed) {
console.log(installed);
console.log('123');
if (!installed) {
alert("Serial Port App is missing. Please install first");
}
}
);
var serialPort = new SerialPort;
console.log(serialPort);
serialPort.setOnDataReceivedCallback(onNewData);
realodDevices();
function realodDevices() {
getDevicesList(
function(response) {
$('#devices').empty();
if (response.result === "ok") {
for (var i = 0; i < response.ports.length; i++) {
$('#devices').append('<option value="' + response.ports[i].path + '">' + response.ports[i].displayName + '(' + response.ports[i].path + ')' + '</option>');
}
} else {
alert(response.error);
}
}
);
}
function openSelectedPort() {
serialPort.openPort({
portName: $('#devices').val(),
bitrate: 9600,
dataBits: "eight",
parityBit: "no",
stopBits: "one"
},
function(response) {
console.log(response);
if (response.result === "ok") {
//Do something
} else {
alert(response.error);
}
}
);
}
function closeCurrentPort() {
serialPort.closePort(
function(response) {
console.log(response);
if (response.result === "ok") {
//Do something
} else {
alert(response.error);
}
}
);
}
// 当有新数据到来时就数据加到页面控件上显示
function onNewData(data) {
var str = "";
var dv = new DataView(data);
for (var i = 0; i < dv.byteLength; i++) {
str = str.concat(String.fromCharCode(dv.getUint8(i, true)));
}
$('#output').append(str);
}
// 读取用户输入的数据,并发送到串口
function sendData() {
var input = stringToArrayBuffer($('#input').val());
serialPort.write(input,
function(response) {
console.log(response);
}
);
}
function stringToArrayBuffer(string) {
var buffer = new ArrayBuffer(string.length);
var dv = new DataView(buffer);
for (var i = 0; i < string.length; i++) {
dv.setUint8(i, string.charCodeAt(i));
}
return dv.buffer;
}
// 在页面加载前,先判断串口已经打开,如果已经打开了就关闭
window.onbeforeunload = function() {
if (serialPort.isOpen()) {
serialPort.closePort(
function(response) {
console.log(response);
if (response.result === "ok") {
return null;
} else {
alert(response.error);
return false;
}
}
);
}
return null;
}
</script>
</html>
html的代码就很简单了,这里简单描述一下:
- 页面加载前先检查串口是否已经被打开,如果打开了就先关闭它
- 页面加载后会判断一下我们的app是否安装了,然后就实例化一个SerialPort对象,该对象负责完成与app的连接,如果连接成功会获取一下串口列表然后将串口加入页面的下拉列表中;
- 当用户选择了其中一个,点击open按钮SerialPort就会发送一个open消息给app,app打开串口;
- 如果串口收到了数据,app里serial的onReceive事件出发,向页面postMessage,页面收到消息后将字符串加到文本框中显示
- 用户输入数据到输入框,然后点击了send按钮,SerialPort就sendMessage给app,app调用serial.write向串口发送数据
- 用户点击了reload按钮,就会先清空下拉列表内容,然后执行getDevicesList重新获取串口列表;
- 用户点击了close按钮,SerialPort就sendMessage给app,app调用关闭串口连接。
3-3 二级域名映射
等等,不是已经完了吗?怎么还有!!!
前面文档里说了必须要设置一个二级域名的url表达式吗,既然是必须我们就不得不从了。我们的页面由于是服务器端生成的,如果我们不需要运行在网络上怎么办呢?又不能用真的域名来,那岂不是完蛋?!
好在,域名解析这东西,本地本来就有而且非常简单!废话不多说,开干!
用记事本打开下面的文件:
C:\WINDOWS\system32\drivers\etc\hosts
在文件末尾加上:
127.0.0.1 matsuri.163.com
当然,这是我还在测试阶段,服务器也是本机,所以就直接是127.0.0.1,如果到时候服务器在远程就填真正的IP地址就可以了。
后面的matsuri.163.com 也是随意的,只要满足它是个二级域名就可以了。什么?不知道什么是二级域名...这...自己百度吧。
3-4 Chrome App的安装
差点把这个给忘记了,app的安装和extension是一样的。
首先在浏览器中输入
进入插件管理页面:
注意要打开右上角的开发者模式哦
然后点击左上角的Load unpacked 按钮,在弹出的对话框中选择我们的app项目目录就可以了。
如果没有错误,就能在该页面的最下面看到我们自己的app了。
4 运行效果
好了,终于可以看看效果了,可是我没有串口设备呀!
没事儿,神器之一:虚拟串口
这里创建了一对虚拟串口,创建时会自动将两个串口接在一起,你可以认为其中一个就是物理设备吧。
然后神器之二:串口调试助手
这里,我们已经打开了COM3,接下来就是网页端了。
点击下拉列表,可以看到我们电脑上的3个串口,第一个串口是物理串口,后面两个是软件虚拟的,这里我们选择COM2,然后open,看一下后台console有没有信息。
app返回了ok,还有串口的配置信息。
好,我们从串口调试助手发送一串消息给web看看:
点击 手动发送 按钮,看看web端:
可以看到console里面也能看到发送来的数据,只不过UINT8格式的。
最后试试web发送数据到串口助手:
串口助手成功收到了数据,在console看到我们发送了15个字节的数据。
至此,我们的所有功能都已经实现了!好累,休息休息~~