關(guān)鍵字: Chrome, APP, Serial Port, Javascript, Web
前言
由于工作需要,要實(shí)現(xiàn)客戶端采集傳感器數(shù)據(jù),傳感器是串口的。串口采集數(shù)據(jù)這樣的應(yīng)用程序?qū)戇^不知道多少了,大學(xué)畢業(yè)論文時(shí)都玩過了,乍一看挺簡(jiǎn)單的,但是和web前端放在一起就懵逼了。什么?居然讓javascript這樣的腳本去操控硬件?還讓不讓人玩了?
沒有思路,于是打開瀏覽器搜搜看,有沒有別人的解決方案,喲,還真有,翻了幾十個(gè)相關(guān)網(wǎng)頁,無非就是這么幾種的:
- 使用mscomm32.dll使用串口資源
- 用C#之類的自己寫一個(gè)dll,然后使用
- 用Node.js 的serial模塊實(shí)現(xiàn)
- 使用Google的Chrome.serial實(shí)現(xiàn)
第一種貌似最簡(jiǎn)單,看看吧,解決一查這貨只支持IE,什么?讓我用IE?再見!
第二種,額,算了,已經(jīng)不用Visual Stadio多年了,下載個(gè)環(huán)境都得好半天,想起來就麻煩,得了!
第三種,要配置Node.js運(yùn)行環(huán)境,拜托我這是前端,還要和我的服務(wù)器端通信,這樣太不倫不類了吧,KO!
第四種,好像沒得選了吧,使用Google瀏覽器的API,一看就是我喜歡的那種,一直對(duì)Google API頂禮膜拜,這次終于有機(jī)會(huì)來個(gè)親密接觸啦。一查,我去,只能用于開發(fā)Chrome App,這是個(gè)什么鬼?Chrome Extensions (插件)使用了很多,這個(gè)還是聽新奇的,就你了!
1 第一性原理
Chrome.serial 可以訪問硬件設(shè)備資源,比如使用chrome.serial.getDevices()獲取PC上可用的串口資源列表,然后我們就可以在列表中選擇我們實(shí)際設(shè)備的串口,然后傳入串口參數(shù)打開串口,那么該串口資源就可用了。
而Chrome App和Chrome Extesions一樣,可以隨著Chrome的啟動(dòng)而運(yùn)行,Chrome App經(jīng)過適當(dāng)?shù)呐渲每梢耘c別的App或者Extension或者Web Page進(jìn)行數(shù)據(jù)交互,這樣思路就很簡(jiǎn)單了。
Chrome App 里設(shè)置一些監(jiān)聽事件,比如onConnect,OnMessage, Web Page通過發(fā)送消息給Chrome App獲取串口列表,打開串口,監(jiān)聽串口消息,寫入串口數(shù)據(jù),關(guān)閉串口等操作。
此處應(yīng)有圖
2 關(guān)鍵技術(shù)點(diǎn)
2-1. Chrome.serial
官方文檔如下:
如果英文不好,可以看百度的文檔:

我們主要是用下面的函數(shù):
- getDevices
- connect
- disconnect
- send
- onReceive
- onReceiveError
2-2. Chrome App與Web Page 連接和通信
應(yīng)用和內(nèi)容腳本間的通信使用消息傳遞的方式。兩邊均可以監(jiān)聽另一邊發(fā)來的消息,并通過同樣的通道回應(yīng)。消息可以包含任何有效的 JSON 對(duì)象(null、boolean、number、string、array 或 object)。對(duì)于一次性的請(qǐng)求有一個(gè)簡(jiǎn)單的 API,同時(shí)也有更復(fù)雜的 API,允許您通過長(zhǎng)時(shí)間的連接與共享的上下文交換多個(gè)消息。另外您也可以向另一個(gè)應(yīng)用發(fā)送消息,只要您知道它的標(biāo)識(shí)符,這將在跨應(yīng)用消息傳遞部分介紹。
官方文檔如下:
百度中文文檔如下:
https://chajian.baidu.com/developer/extensions/messaging.html
① Chrome App與Extensions交互
對(duì)于簡(jiǎn)單消息,應(yīng)該直接使用比較簡(jiǎn)單的 runtime.sendMessage 方法,該方法分別允許從內(nèi)容腳本向應(yīng)用或者反過來發(fā)送可通過 JSON 序列化的消息,可選的 callback 參數(shù)允許在需要的時(shí)候從另一邊處理回應(yīng)。
有時(shí)候需要長(zhǎng)時(shí)間的對(duì)話,而不是一次請(qǐng)求和回應(yīng)。在這種情況下,可以分別使用 runtime.connect 或 tabs.connect 從您的內(nèi)容腳本建立到應(yīng)用(或者反過來)的長(zhǎng)時(shí)間連接。建立的通道可以有一個(gè)可選的名稱,讓您區(qū)分不同類型的連接。
使用長(zhǎng)時(shí)間連接的一種可能的情形為自動(dòng)填充表單的應(yīng)用。對(duì)于一次登錄操作,內(nèi)容腳本可以連接到應(yīng)用頁面,每次頁面上的輸入元素需要填寫表單數(shù)據(jù)時(shí)向應(yīng)用發(fā)送消息。共享的連接允許應(yīng)用保留來自內(nèi)容腳本的不同消息之間的狀態(tài)聯(lián)系。
建立連接時(shí),兩端都將獲得一個(gè) runtime.Port 對(duì)象,用來通過建立的連接發(fā)送和接收消息。
這里由于是跨應(yīng)用的消息傳遞,因此Chrome App的后臺(tái)線程(background.js)里使用runtime.onMessageExternal 和 runtime.onConnectExternal
對(duì)外部Extensions的連接和消息進(jìn)行監(jiān)聽。
示例代碼如下:
// 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.
});
});
向另一個(gè)應(yīng)用發(fā)送消息與在App內(nèi)部中發(fā)送消息類似,唯一的區(qū)別是必須傳遞需要與之通信的App的標(biāo)識(shí)符外部Extensions要建立連接和發(fā)送消息可以這樣:
// 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能與普通網(wǎng)頁進(jìn)行通信,必須在 manifest.json 文件中指定希望與之通信的網(wǎng)站表達(dá)式,形如
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
URL 表達(dá)式必須至少包含一個(gè)二級(jí)域名,也就是說禁止使用類似于"*"、"*.com"、".co.uk"和".appspot.com"之類的主機(jī)名。在網(wǎng)頁中,使用 runtime.sendMessage 或 runtime.connect API 向指定應(yīng)用或應(yīng)用發(fā)送消息。
App端的代碼與Extension通信時(shí)是一樣的,都是使用runtime.onMessageExternal 和 runtime.onConnectExternal
對(duì)外部Extensions的連接和消息進(jìn)行監(jiān)聽。
3 具體實(shí)現(xiàn)
明確了原理和關(guān)鍵技術(shù),接下來就開干了。
3-1. 從0實(shí)現(xiàn)一個(gè)Chrome App
創(chuàng)建一個(gè)工程,目錄如下:

文件就這么幾個(gè),真正有用的就三個(gè)icon.png,manifest.json,serial_interface.js
- icon.png App的圖標(biāo)文件,沒什么好說的,注意文件尺寸
- 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/*"]
}
}
關(guān)于manifest.json文件的說明,可以看官方文檔,當(dāng)然很多地方都有該文件的介紹:
這里最關(guān)鍵的app、permissions和externally_connectable字段:
- app 指示該應(yīng)用是一個(gè)Chrome App,背景線程執(zhí)行serial_interface.js里的代碼
- permissions 這里只要求了一個(gè)權(quán)限,可以根據(jù)不同的應(yīng)用場(chǎng)景進(jìn)行配置
- externally_connectable 指示該應(yīng)用可以被外部App、Extensions、Web連接,ids里面就是別的Extension的ID,這里我留了一個(gè)接口供我自己的插件使用, matches里的地址表達(dá)式上一節(jié)有詳細(xì)的介紹,注意必須是二級(jí)域名
然后是serial_interface.js:
① 全局變量
先定義兩個(gè)列表用來管理不同頁面的連接和串口資源,getGUID用來生成一個(gè)隨機(jī)的GUID指示某一個(gè)串口資源,可以理解為串口資源的指針。
/**
* 當(dāng)Web端的一個(gè)SerialPort實(shí)例生成的時(shí)候,Web同時(shí)就能得到一個(gè)chrome.runtime.Port 對(duì)象,該對(duì)象就是Web連接至本app的句柄。
* 如果連接成功,就把該P(yáng)ort對(duì)象以一個(gè)獨(dú)一無二的GUID保存在SerialPort列表中。
* 該GUID 用于指示哪個(gè)的SerialPort實(shí)例與哪個(gè)頁面關(guān)聯(lián)。
*/
var serialPort = [];
/**
* 當(dāng)某個(gè)串口打開的時(shí)候就把打開該串口的頁面的GUID保存到serialConnections 列表中。
* 每個(gè)GUID索引就是由chrome.serial API提供的一個(gè)特有的連接。
*/
var serialConnections = [];
/**
* 生成一個(gè)隨機(jī)的GUID用于與chrome.runtime.Port 關(guān)聯(lián)。
*/
function getGUID() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
② 監(jiān)聽Web Page連接事件
/**
* 當(dāng)一個(gè)新的SerialPort 創(chuàng)建時(shí)就會(huì)觸發(fā)一個(gè)外部連接事件
* 1. 生成一個(gè)GUID,并以該GUID作為索引將連接port對(duì)象保存在serialPort列表中
* 2. 將該GUID發(fā)回連接的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);
}
);
③ 監(jiān)聽Web Page發(fā)送消息事件
/**
* 監(jiān)聽并處理Web page來請(qǐng)求。
* Commands:
* - open -> 請(qǐng)求打開一個(gè)串口
* - close -> 請(qǐng)求關(guān)閉一個(gè)串口
* - list -> 請(qǐng)求獲取串口列表
* - write -> 請(qǐng)求向串口發(fā)送數(shù)據(jù)
* - installed -> 請(qǐng)求檢查本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;
});
④ 監(jiān)聽串口接收到數(shù)據(jù)事件
/**
* 監(jiān)聽并處理串口收到數(shù)據(jù)事件
* 1. 使用 connectionId 檢索serialConnections列表獲得頁面的GUID
* 2. 將與Web page關(guān)聯(lián)的串口數(shù)據(jù)直接發(fā)送給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))
});
}
);
⑤ 監(jiān)聽串口錯(cuò)誤
/**
* 監(jiān)聽并處理串口錯(cuò)誤
* 1. 使用 connectionId 檢索serialConnections列表獲得頁面的GUID
* 2. 將與Web page關(guān)聯(lián)的串口錯(cuò)誤直接發(fā)送給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是否一個(gè)被安裝在Chrome瀏覽器中。
* 如果已經(jīng)安裝則返回 "ok" 和當(dāng)前版本信息。
*/
function checkInstalled(request, sender, sendResponse) {
var manifest = chrome.runtime.getManifest();
sendResponse({
result: "ok",
version: manifest.version
});
}
⑦ 獲取串口設(shè)備列表
/**
* 獲取所有連接在本地PC上的串口設(shè)備列表。
* 如果沒有錯(cuò)誤則返回以下內(nèi)容:
* - path: 物理路徑
* - vendorId (optional): 制造商ID
* - productId (optional): 產(chǎn)品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
});
}
}
);
}
⑧ 打開一個(gè)串口
/**
* 嘗試打開一個(gè)串口
* request 必須包含以下:
* info.portName -> 要打開的串口地址
* info.bitrate -> 串口波特率
* info.dataBits -> 串口數(shù)據(jù)位數(shù) ("eight" or "seven")
* info.parityBit -> 期偶校驗(yàn)位 ("no", "odd" or "even")
* info.stopBits -> 停止位 ("one" or "two")
*
* 如果與串口建立了連接將向web page 返回結(jié)果: "ok" 和 connection info,
* 否則返回結(jié)果: "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
});
}
}
);
}
⑨ 關(guān)閉一個(gè)串口
/**
* 嘗試關(guān)閉一個(gè)串口
* request 必須包含以下:
* connectionId -> 當(dāng)串口被打開時(shí)的連接ID
*
* 如果當(dāng)前連接被成功關(guān)閉將向web page返回結(jié)果: "ok" 和 connection info,
* 否則返回結(jié)果: "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
});
}
}
);
}
⑩ 向串口寫入數(shù)據(jù)
/**
* 向串口寫入數(shù)據(jù)
* request 必須包含以下:
* connectionId -> 當(dāng)串口被打開時(shí)的連接ID
* data -> 要發(fā)送的字節(jié)流數(shù)組
*
* 如果發(fā)送成功關(guān)閉將向web page返回結(jié)果: "ok" 和 串口響應(yīng)結(jié)果,
* 否則返回結(jié)果: "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端實(shí)現(xiàn)
寫完了App,來看Web端的javascript怎么寫,serial_port.js:
/**
* Web要連接的Chrome App ID
*/
var extensionId = "ojfkhepmmpnpbkjmlipagnflphcpidcm";
app ID是必須的,可以在Chrome的Extensions界面查看,在瀏覽器的地址欄中輸入

這里顯示的ID不全,可以點(diǎn)擊 Details 按鈕看到完整的
function SerialPort() {
// Chrome App 分配的GUID
var portGUID;
// 使用app的ID與app建立外部連接,連接一旦建立,web端和app都想獲得一個(gè)port對(duì)象
var port = chrome.runtime.connect(extensionId);
// 唯一的串口連接ID
var serialConnectionId;
// 指示串口是否打開
var isSerialPortOpen = false;
// 當(dāng)串口接收到數(shù)據(jù)時(shí)的回調(diào)函數(shù),undefined表示它是純虛函數(shù)
var onDataReceivedCallback = undefined;
// 串口報(bào)錯(cuò)時(shí)的回調(diào)函數(shù)
var onErrorReceivedCallback = undefined;
/**
* 監(jiān)聽并處理來自app的消息
* 可以處理的消息有(可自行添加):
* - guid -> 當(dāng)與app連接成功時(shí)app發(fā)送給web的,用于表示當(dāng)前頁面與app 的連接
* - serialdata -> 當(dāng)串口有新數(shù)據(jù)接收時(shí)由app發(fā)送給web
* - serialerror -> 當(dāng)串口發(fā)生錯(cuò)誤時(shí)由app發(fā)送給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;
}
// 相當(dāng)于純虛函數(shù),由web頁面的callBack具體實(shí)現(xiàn)
this.setOnDataReceivedCallback = function (callBack) {
onDataReceivedCallback = callBack;
}
// 相當(dāng)于純虛函數(shù),由web頁面的callBack具體實(shí)現(xiàn)
this.setOnErrorReceivedCallback = function (callBack) {
onErrorReceivedCallback = callBack;
}
/**
* 嘗試打開一個(gè)串口
* portInfo 必須包含以下:
* portName -> 串口地址
* bitrate -> 串口波特率
* dataBits -> 數(shù)據(jù)位 ("eight" or "seven")
* parityBit -> 校驗(yàn)位 ("no", "odd" or "even")
* stopBits -> 停止位 ("one" or "two")
* Callback用來處理app返回的結(jié)果,由于sendMessage是異步執(zhí)行的函數(shù)
*/
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);
}
);
}
// 關(guān)閉一個(gè)串口
this.closePort = function (callBack) {
chrome.runtime.sendMessage(extensionId, {
cmd: "close",
connectionId: serialConnectionId
},
function (response) {
if (response.result === "ok") {
isSerialPortOpen = false;
}
callBack(response);
}
);
};
/**
* 向串口寫入數(shù)據(jù)
* request 必須包含以下:
* connectionId -> 串口連接ID
* data -> 要發(fā)送的字節(jié)流數(shù)組
*/
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);
}
);
}
}
好長(zhǎng)!是不是?其實(shí)挺簡(jiǎn)單的,就是實(shí)現(xiàn)了類,對(duì)串口的操作進(jìn)行了封裝而已。
喔,對(duì)了,還沒完呢!
/**
* 獲取所有連接在本地PC上的串口設(shè)備列表。
* 如果沒有錯(cuò)誤則返回以下內(nèi)容:
* - path: 物理路徑
* - vendorId (optional): 制造商ID
* - productId (optional): 產(chǎn)品ID
* - displayName (optional): 顯示名稱
* Callback 用以處理app返回結(jié)果
*/
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);
}
}
);
}
這兩個(gè)函數(shù)一個(gè)對(duì)所有串口操作,一個(gè)和串口無關(guān)所以就沒有封裝在類中了。
好了,最難的都完了,最后來看點(diǎn)兒簡(jiǎn)單的吧,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' >
<link rel='stylesheet' >
</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);
}
}
);
}
// 當(dāng)有新數(shù)據(jù)到來時(shí)就數(shù)據(jù)加到頁面控件上顯示
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);
}
// 讀取用戶輸入的數(shù)據(jù),并發(fā)送到串口
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;
}
// 在頁面加載前,先判斷串口已經(jīng)打開,如果已經(jīng)打開了就關(guān)閉
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的代碼就很簡(jiǎn)單了,這里簡(jiǎn)單描述一下:
- 頁面加載前先檢查串口是否已經(jīng)被打開,如果打開了就先關(guān)閉它
- 頁面加載后會(huì)判斷一下我們的app是否安裝了,然后就實(shí)例化一個(gè)SerialPort對(duì)象,該對(duì)象負(fù)責(zé)完成與app的連接,如果連接成功會(huì)獲取一下串口列表然后將串口加入頁面的下拉列表中;
- 當(dāng)用戶選擇了其中一個(gè),點(diǎn)擊open按鈕SerialPort就會(huì)發(fā)送一個(gè)open消息給app,app打開串口;
- 如果串口收到了數(shù)據(jù),app里serial的onReceive事件出發(fā),向頁面postMessage,頁面收到消息后將字符串加到文本框中顯示
- 用戶輸入數(shù)據(jù)到輸入框,然后點(diǎn)擊了send按鈕,SerialPort就sendMessage給app,app調(diào)用serial.write向串口發(fā)送數(shù)據(jù)
- 用戶點(diǎn)擊了reload按鈕,就會(huì)先清空下拉列表內(nèi)容,然后執(zhí)行g(shù)etDevicesList重新獲取串口列表;
- 用戶點(diǎn)擊了close按鈕,SerialPort就sendMessage給app,app調(diào)用關(guān)閉串口連接。
3-3 二級(jí)域名映射
等等,不是已經(jīng)完了嗎?怎么還有?。。?br>
前面文檔里說了必須要設(shè)置一個(gè)二級(jí)域名的url表達(dá)式嗎,既然是必須我們就不得不從了。我們的頁面由于是服務(wù)器端生成的,如果我們不需要運(yùn)行在網(wǎng)絡(luò)上怎么辦呢?又不能用真的域名來,那豈不是完蛋?!
好在,域名解析這東西,本地本來就有而且非常簡(jiǎn)單!廢話不多說,開干!
用記事本打開下面的文件:
C:\WINDOWS\system32\drivers\etc\hosts
在文件末尾加上:
127.0.0.1 matsuri.163.com
當(dāng)然,這是我還在測(cè)試階段,服務(wù)器也是本機(jī),所以就直接是127.0.0.1,如果到時(shí)候服務(wù)器在遠(yuǎn)程就填真正的IP地址就可以了。
后面的matsuri.163.com 也是隨意的,只要滿足它是個(gè)二級(jí)域名就可以了。什么?不知道什么是二級(jí)域名...這...自己百度吧。
3-4 Chrome App的安裝
差點(diǎn)把這個(gè)給忘記了,app的安裝和extension是一樣的。
首先在瀏覽器中輸入
進(jìn)入插件管理頁面:

注意要打開右上角的開發(fā)者模式哦

然后點(diǎn)擊左上角的Load unpacked 按鈕,在彈出的對(duì)話框中選擇我們的app項(xiàng)目目錄就可以了。
如果沒有錯(cuò)誤,就能在該頁面的最下面看到我們自己的app了。
4 運(yùn)行效果
好了,終于可以看看效果了,可是我沒有串口設(shè)備呀!
沒事兒,神器之一:虛擬串口

這里創(chuàng)建了一對(duì)虛擬串口,創(chuàng)建時(shí)會(huì)自動(dòng)將兩個(gè)串口接在一起,你可以認(rèn)為其中一個(gè)就是物理設(shè)備吧。
然后神器之二:串口調(diào)試助手

這里,我們已經(jīng)打開了COM3,接下來就是網(wǎng)頁端了。

點(diǎn)擊下拉列表,可以看到我們電腦上的3個(gè)串口,第一個(gè)串口是物理串口,后面兩個(gè)是軟件虛擬的,這里我們選擇COM2,然后open,看一下后臺(tái)console有沒有信息。

app返回了ok,還有串口的配置信息。
好,我們從串口調(diào)試助手發(fā)送一串消息給web看看:

點(diǎn)擊 手動(dòng)發(fā)送 按鈕,看看web端:

可以看到console里面也能看到發(fā)送來的數(shù)據(jù),只不過UINT8格式的。
最后試試web發(fā)送數(shù)據(jù)到串口助手:

串口助手成功收到了數(shù)據(jù),在console看到我們發(fā)送了15個(gè)字節(jié)的數(shù)據(jù)。
至此,我們的所有功能都已經(jīng)實(shí)現(xiàn)了!好累,休息休息~~