關(guān)鍵字: Chrome, APP, Serial Port, Javascript, Web
前言
由于工作需要迅栅,要實現(xiàn)客戶端采集傳感器數(shù)據(jù)殊校,傳感器是串口的。串口采集數(shù)據(jù)這樣的應(yīng)用程序?qū)戇^不知道多少了读存,大學(xué)畢業(yè)論文時都玩過了为流,乍一看挺簡單的呕屎,但是和web前端放在一起就懵逼了。什么敬察?居然讓javascript這樣的腳本去操控硬件秀睛?還讓不讓人玩了?
沒有思路莲祸,于是打開瀏覽器搜搜看蹂安,有沒有別人的解決方案,喲锐帜,還真有田盈,翻了幾十個相關(guān)網(wǎng)頁,無非就是這么幾種的:
- 使用mscomm32.dll使用串口資源
- 用C#之類的自己寫一個dll缴阎,然后使用
- 用Node.js 的serial模塊實現(xiàn)
- 使用Google的Chrome.serial實現(xiàn)
第一種貌似最簡單允瞧,看看吧,解決一查這貨只支持IE蛮拔,什么述暂?讓我用IE?再見建炫!
第二種畦韭,額,算了踱卵,已經(jīng)不用Visual Stadio多年了廊驼,下載個環(huán)境都得好半天,想起來就麻煩惋砂,得了妒挎!
第三種,要配置Node.js運行環(huán)境西饵,拜托我這是前端酝掩,還要和我的服務(wù)器端通信,這樣太不倫不類了吧眷柔,KO!
第四種期虾,好像沒得選了吧,使用Google瀏覽器的API驯嘱,一看就是我喜歡的那種镶苞,一直對Google API頂禮膜拜,這次終于有機會來個親密接觸啦鞠评。一查茂蚓,我去,只能用于開發(fā)Chrome App,這是個什么鬼聋涨?Chrome Extensions (插件)使用了很多晾浴,這個還是聽新奇的,就你了牍白!
1 第一性原理
Chrome.serial 可以訪問硬件設(shè)備資源脊凰,比如使用chrome.serial.getDevices()獲取PC上可用的串口資源列表,然后我們就可以在列表中選擇我們實際設(shè)備的串口茂腥,然后傳入串口參數(shù)打開串口狸涌,那么該串口資源就可用了。
而Chrome App和Chrome Extesions一樣最岗,可以隨著Chrome的啟動而運行杈抢,Chrome App經(jīng)過適當(dāng)?shù)呐渲每梢耘c別的App或者Extension或者Web Page進行數(shù)據(jù)交互,這樣思路就很簡單了仑性。
Chrome App 里設(shè)置一些監(jiān)聽事件惶楼,比如onConnect,OnMessage, Web Page通過發(fā)送消息給Chrome App獲取串口列表诊杆,打開串口歼捐,監(jiān)聽串口消息,寫入串口數(shù)據(jù)晨汹,關(guān)閉串口等操作豹储。
此處應(yīng)有圖
2 關(guān)鍵技術(shù)點
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 對象(null铝穷、boolean钠怯、number、string曙聂、array 或 object)晦炊。對于一次性的請求有一個簡單的 API,同時也有更復(fù)雜的 API宁脊,允許您通過長時間的連接與共享的上下文交換多個消息断国。另外您也可以向另一個應(yīng)用發(fā)送消息,只要您知道它的標(biāo)識符榆苞,這將在跨應(yīng)用消息傳遞部分介紹稳衬。
官方文檔如下:
百度中文文檔如下:
https://chajian.baidu.com/developer/extensions/messaging.html
① Chrome App與Extensions交互
對于簡單消息,應(yīng)該直接使用比較簡單的 runtime.sendMessage 方法坐漏,該方法分別允許從內(nèi)容腳本向應(yīng)用或者反過來發(fā)送可通過 JSON 序列化的消息薄疚,可選的 callback 參數(shù)允許在需要的時候從另一邊處理回應(yīng)弄砍。
有時候需要長時間的對話,而不是一次請求和回應(yīng)输涕。在這種情況下,可以分別使用 runtime.connect 或 tabs.connect 從您的內(nèi)容腳本建立到應(yīng)用(或者反過來)的長時間連接慨畸。建立的通道可以有一個可選的名稱莱坎,讓您區(qū)分不同類型的連接。
使用長時間連接的一種可能的情形為自動填充表單的應(yīng)用寸士。對于一次登錄操作檐什,內(nèi)容腳本可以連接到應(yīng)用頁面,每次頁面上的輸入元素需要填寫表單數(shù)據(jù)時向應(yīng)用發(fā)送消息弱卡。共享的連接允許應(yīng)用保留來自內(nèi)容腳本的不同消息之間的狀態(tài)聯(lián)系乃正。
建立連接時,兩端都將獲得一個 runtime.Port 對象婶博,用來通過建立的連接發(fā)送和接收消息瓮具。
這里由于是跨應(yīng)用的消息傳遞,因此Chrome App的后臺線程(background.js)里使用runtime.onMessageExternal 和 runtime.onConnectExternal
對外部Extensions的連接和消息進行監(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.
});
});
向另一個應(yīng)用發(fā)送消息與在App內(nèi)部中發(fā)送消息類似名党,唯一的區(qū)別是必須傳遞需要與之通信的App的標(biāo)識符外部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)頁進行通信,必須在 manifest.json 文件中指定希望與之通信的網(wǎng)站表達式挠轴,形如
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
URL 表達式必須至少包含一個二級域名传睹,也就是說禁止使用類似于"*"、"*.com"岸晦、".co.uk"和".appspot.com"之類的主機名欧啤。在網(wǎng)頁中,使用 runtime.sendMessage 或 runtime.connect API 向指定應(yīng)用或應(yīng)用發(fā)送消息启上。
App端的代碼與Extension通信時是一樣的邢隧,都是使用runtime.onMessageExternal 和 runtime.onConnectExternal
對外部Extensions的連接和消息進行監(jiān)聽。
3 具體實現(xiàn)
明確了原理和關(guān)鍵技術(shù)冈在,接下來就開干了府框。
3-1. 從0實現(xiàn)一個Chrome App
創(chuàng)建一個工程,目錄如下:
文件就這么幾個讥邻,真正有用的就三個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)用是一個Chrome App口柳,背景線程執(zhí)行serial_interface.js里的代碼
- permissions 這里只要求了一個權(quán)限,可以根據(jù)不同的應(yīng)用場景進行配置
- externally_connectable 指示該應(yīng)用可以被外部App有滑、Extensions跃闹、Web連接,ids里面就是別的Extension的ID毛好,這里我留了一個接口供我自己的插件使用, matches里的地址表達式上一節(jié)有詳細的介紹狸臣,注意必須是二級域名
然后是serial_interface.js:
① 全局變量
先定義兩個列表用來管理不同頁面的連接和串口資源典予,getGUID用來生成一個隨機的GUID指示某一個串口資源姜凄,可以理解為串口資源的指針盒使。
/**
* 當(dāng)Web端的一個SerialPort實例生成的時候,Web同時就能得到一個chrome.runtime.Port 對象吼驶,該對象就是Web連接至本app的句柄惩激。
* 如果連接成功,就把該Port對象以一個獨一無二的GUID保存在SerialPort列表中蟹演。
* 該GUID 用于指示哪個的SerialPort實例與哪個頁面關(guān)聯(lián)咧欣。
*/
var serialPort = [];
/**
* 當(dāng)某個串口打開的時候就把打開該串口的頁面的GUID保存到serialConnections 列表中。
* 每個GUID索引就是由chrome.serial API提供的一個特有的連接轨帜。
*/
var serialConnections = [];
/**
* 生成一個隨機的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)一個新的SerialPort 創(chuàng)建時就會觸發(fā)一個外部連接事件
* 1. 生成一個GUID,并以該GUID作為索引將連接port對象保存在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來請求蚌父。
* Commands:
* - open -> 請求打開一個串口
* - close -> 請求關(guān)閉一個串口
* - list -> 請求獲取串口列表
* - write -> 請求向串口發(fā)送數(shù)據(jù)
* - 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;
});
④ 監(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)聽串口錯誤
/**
* 監(jiān)聽并處理串口錯誤
* 1. 使用 connectionId 檢索serialConnections列表獲得頁面的GUID
* 2. 將與Web page關(guān)聯(lián)的串口錯誤直接發(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是否一個被安裝在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è)備列表苟弛。
* 如果沒有錯誤則返回以下內(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
});
}
}
);
}
⑧ 打開一個串口
/**
* 嘗試打開一個串口
* request 必須包含以下:
* info.portName -> 要打開的串口地址
* info.bitrate -> 串口波特率
* info.dataBits -> 串口數(shù)據(jù)位數(shù) ("eight" or "seven")
* info.parityBit -> 期偶校驗位 ("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)閉一個串口
/**
* 嘗試關(guān)閉一個串口
* request 必須包含以下:
* connectionId -> 當(dāng)串口被打開時的連接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)串口被打開時的連接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端實現(xiàn)
寫完了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;
// 當(dāng)串口接收到數(shù)據(jù)時的回調(diào)函數(shù)滚婉,undefined表示它是純虛函數(shù)
var onDataReceivedCallback = undefined;
// 串口報錯時的回調(diào)函數(shù)
var onErrorReceivedCallback = undefined;
/**
* 監(jiān)聽并處理來自app的消息
* 可以處理的消息有(可自行添加):
* - guid -> 當(dāng)與app連接成功時app發(fā)送給web的,用于表示當(dāng)前頁面與app 的連接
* - serialdata -> 當(dāng)串口有新數(shù)據(jù)接收時由app發(fā)送給web
* - serialerror -> 當(dāng)串口發(fā)生錯誤時由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具體實現(xiàn)
this.setOnDataReceivedCallback = function (callBack) {
onDataReceivedCallback = callBack;
}
// 相當(dāng)于純虛函數(shù)让腹,由web頁面的callBack具體實現(xiàn)
this.setOnErrorReceivedCallback = function (callBack) {
onErrorReceivedCallback = callBack;
}
/**
* 嘗試打開一個串口
* portInfo 必須包含以下:
* portName -> 串口地址
* bitrate -> 串口波特率
* dataBits -> 數(shù)據(jù)位 ("eight" or "seven")
* parityBit -> 校驗位 ("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)閉一個串口
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);
}
);
}
}
好長!是不是骇窍?其實挺簡單的瓜晤,就是實現(xiàn)了類,對串口的操作進行了封裝而已腹纳。
喔痢掠,對了,還沒完呢!
/**
* 獲取所有連接在本地PC上的串口設(shè)備列表只估。
* 如果沒有錯誤則返回以下內(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);
}
}
);
}
這兩個函數(shù)一個對所有串口操作,一個和串口無關(guā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ù)據(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的代碼就很簡單了兼贡,這里簡單描述一下:
- 頁面加載前先檢查串口是否已經(jīng)被打開,如果打開了就先關(guān)閉它
- 頁面加載后會判斷一下我們的app是否安裝了娃胆,然后就實例化一個SerialPort對象遍希,該對象負責(zé)完成與app的連接,如果連接成功會獲取一下串口列表然后將串口加入頁面的下拉列表中里烦;
- 當(dāng)用戶選擇了其中一個凿蒜,點擊open按鈕SerialPort就會發(fā)送一個open消息給app,app打開串口胁黑;
- 如果串口收到了數(shù)據(jù)废封,app里serial的onReceive事件出發(fā),向頁面postMessage丧蘸,頁面收到消息后將字符串加到文本框中顯示
- 用戶輸入數(shù)據(jù)到輸入框漂洋,然后點擊了send按鈕,SerialPort就sendMessage給app力喷,app調(diào)用serial.write向串口發(fā)送數(shù)據(jù)
- 用戶點擊了reload按鈕刽漂,就會先清空下拉列表內(nèi)容,然后執(zhí)行g(shù)etDevicesList重新獲取串口列表弟孟;
- 用戶點擊了close按鈕爽冕,SerialPort就sendMessage給app,app調(diào)用關(guān)閉串口連接披蕉。
3-3 二級域名映射
等等颈畸,不是已經(jīng)完了嗎乌奇?怎么還有!C杏椤礁苗!
前面文檔里說了必須要設(shè)置一個二級域名的url表達式嗎,既然是必須我們就不得不從了徙缴。我們的頁面由于是服務(wù)器端生成的试伙,如果我們不需要運行在網(wǎng)絡(luò)上怎么辦呢?又不能用真的域名來于样,那豈不是完蛋疏叨?!
好在穿剖,域名解析這東西蚤蔓,本地本來就有而且非常簡單!廢話不多說糊余,開干秀又!
用記事本打開下面的文件:
C:\WINDOWS\system32\drivers\etc\hosts
在文件末尾加上:
127.0.0.1 matsuri.163.com
當(dāng)然,這是我還在測試階段贬芥,服務(wù)器也是本機吐辙,所以就直接是127.0.0.1,如果到時候服務(wù)器在遠程就填真正的IP地址就可以了蘸劈。
后面的matsuri.163.com 也是隨意的昏苏,只要滿足它是個二級域名就可以了。什么威沫?不知道什么是二級域名...這...自己百度吧捷雕。
3-4 Chrome App的安裝
差點把這個給忘記了,app的安裝和extension是一樣的壹甥。
首先在瀏覽器中輸入
進入插件管理頁面:
注意要打開右上角的開發(fā)者模式哦
然后點擊左上角的Load unpacked 按鈕救巷,在彈出的對話框中選擇我們的app項目目錄就可以了。
如果沒有錯誤句柠,就能在該頁面的最下面看到我們自己的app了浦译。
4 運行效果
好了,終于可以看看效果了溯职,可是我沒有串口設(shè)備呀精盅!
沒事兒,神器之一:虛擬串口
這里創(chuàng)建了一對虛擬串口谜酒,創(chuàng)建時會自動將兩個串口接在一起叹俏,你可以認為其中一個就是物理設(shè)備吧。
然后神器之二:串口調(diào)試助手
這里僻族,我們已經(jīng)打開了COM3粘驰,接下來就是網(wǎng)頁端了屡谐。
點擊下拉列表,可以看到我們電腦上的3個串口蝌数,第一個串口是物理串口愕掏,后面兩個是軟件虛擬的,這里我們選擇COM2顶伞,然后open饵撑,看一下后臺console有沒有信息。
app返回了ok滑潘,還有串口的配置信息。
好锨咙,我們從串口調(diào)試助手發(fā)送一串消息給web看看:
點擊 手動發(fā)送 按鈕语卤,看看web端:
可以看到console里面也能看到發(fā)送來的數(shù)據(jù),只不過UINT8格式的蓖租。
最后試試web發(fā)送數(shù)據(jù)到串口助手:
串口助手成功收到了數(shù)據(jù)粱侣,在console看到我們發(fā)送了15個字節(jié)的數(shù)據(jù)羊壹。
至此蓖宦,我們的所有功能都已經(jīng)實現(xiàn)了!好累油猫,休息休息~~