使用Chrome App與Web交互實(shí)現(xiàn)前端頁面控制串口

關(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

官方文檔如下:

https://developer.chrome.com/apps/serial

如果英文不好,可以看百度的文檔:

https://chajian.baidu.com/developer/apps/serial.html

Chrome.serial APIs

我們主要是用下面的函數(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://developer.chrome.com/extensions/messaging#external

百度中文文檔如下:

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.connecttabs.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.onMessageExternalruntime.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.sendMessageruntime.connect API 向指定應(yīng)用或應(yīng)用發(fā)送消息。

App端的代碼與Extension通信時(shí)是一樣的,都是使用runtime.onMessageExternalruntime.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è)工程,目錄如下:

項(xiàng)目目錄

文件就這么幾個(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)然很多地方都有該文件的介紹:

http://open.chrome.#/extension_dev/manifest.html

這里最關(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界面查看,在瀏覽器的地址欄中輸入

chrome://extensions

Chrome App

這里顯示的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是一樣的。
首先在瀏覽器中輸入

chrome://extensions

進(jìn)入插件管理頁面:


Chrome.extensions

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


Developer Mode

然后點(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看看:


串口助手發(fā)送數(shù)據(jù)

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


web接收數(shù)據(jù)

可以看到console里面也能看到發(fā)送來的數(shù)據(jù),只不過UINT8格式的。

最后試試web發(fā)送數(shù)據(jù)到串口助手:


串口助手接收數(shù)據(jù)

串口助手成功收到了數(shù)據(jù),在console看到我們發(fā)送了15個(gè)字節(jié)的數(shù)據(jù)。

至此,我們的所有功能都已經(jīng)實(shí)現(xiàn)了!好累,休息休息~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • chrome擴(kuò)展開發(fā)入門教程 最近在開發(fā)chrome插件,看到一篇非常適合入門的教程,特記錄一下 注:轉(zhuǎn)載 本文首...
    謝大見閱讀 6,551評(píng)論 1 25
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,626評(píng)論 1 32
  • 口紅 01材料準(zhǔn)備 02起形 03涂色 04成圖
    板栗子M閱讀 281評(píng)論 1 2
  • 為什么有的人可悲,在哪里都可悲?最近我才終于知道了答案! 事情還要從一個(gè)多月說起。一天下班前,我被大b...
    燕子Diana閱讀 299評(píng)論 0 1
  • 在拍賣會(huì)結(jié)束后,牧塵并沒有急著離開商城,而是尋了一處修煉客棧,暫時(shí)的住了進(jìn)去,既然大戰(zhàn)即將來臨,那他自然也必須令得...
    混沌天書閱讀 621評(píng)論 0 0

友情鏈接更多精彩內(nèi)容