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

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

官方文檔如下:

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 對象(null铝穷、boolean钠怯、number、string曙聂、array 或 object)晦炊。對于一次性的請求有一個簡單的 API,同時也有更復(fù)雜的 API宁脊,允許您通過長時間的連接與共享的上下文交換多個消息断国。另外您也可以向另一個應(yīng)用發(fā)送消息,只要您知道它的標(biāo)識符榆苞,這將在跨應(yīng)用消息傳遞部分介紹稳衬。

官方文檔如下:

https://developer.chrome.com/extensions/messaging#external

百度中文文檔如下:

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

App端的代碼與Extension通信時是一樣的邢隧,都是使用runtime.onMessageExternalruntime.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)然很多地方都有該文件的介紹:

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

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

chrome://extensions

Chrome App

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

chrome://extensions

進入插件管理頁面:


Chrome.extensions

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


Developer Mode

然后點擊左上角的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ā)送數(shù)據(jù)

點擊 手動發(fā)送 按鈕语卤,看看web端:


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

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

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


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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末稠茂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子情妖,更是在濱河造成了極大的恐慌睬关,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毡证,死亡現(xiàn)場離奇詭異电爹,居然都是意外死亡,警方通過查閱死者的電腦和手機料睛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門丐箩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恤煞,你說我怎么就攤上這事屎勘。” “怎么了居扒?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵概漱,是天一觀的道長。 經(jīng)常有香客問我喜喂,道長瓤摧,這世上最難降的妖魔是什么竿裂? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮姻灶,結(jié)果婚禮上铛绰,老公的妹妹穿的比我還像新娘。我一直安慰自己产喉,他們只是感情好捂掰,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著曾沈,像睡著了一般这嚣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上塞俱,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天姐帚,我揣著相機與錄音,去河邊找鬼障涯。 笑死罐旗,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的唯蝶。 我是一名探鬼主播九秀,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼粘我!你這毒婦竟也來了鼓蜒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤征字,失蹤者是張志新(化名)和其女友劉穎都弹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匙姜,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡畅厢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了氮昧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片框杜。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖郭计,靈堂內(nèi)的尸體忽然破棺而出霸琴,到底是詐尸還是另有隱情,我是刑警寧澤昭伸,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布梧乘,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏选调。R本人自食惡果不足惜夹供,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望仁堪。 院中可真熱鬧哮洽,春花似錦、人聲如沸弦聂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽莺葫。三九已至匪凉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捺檬,已是汗流浹背再层。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留堡纬,地道東北人聂受。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像烤镐,于是被迫代替她去往敵國和親蛋济。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容

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