Google Chrome + ChromeAppsのchrome.serial API を使って、シリアルCOMポートからデータを受信する

前回Rubyを使ってシリアルCOMポートからデータを受信してみましたが、他にも何か方法がないかを探してみたところ、ChromeAppsに chrome.serial API がありました。
chrome.serial - Google Chrome

そこで、chrome.serial API を使って、シリアルCOMポートからデータを取ってみました。

なお、ChromeAppsの概要は以下のスライドが参考になりました。
Chrome Apps 概要

 

環境

 

用意するファイル

以下のBlogのファイルを参考に、いくつかファイルを用意しました。今回使用するファイルの名前もこのBlogに合わせてあります。
JavaScript + Chrome Packaged Apps API を使用したシリアル通信Webアプリ - Tech-Sketch

  • manifest.json
  • background.js
  • serial.js
  • window.html
  • png画像

なお、png画像は以下のサイトで生成したものを使いました。ありがとうございます。
Placehold.jp|ダミー画像生成 モック用画像作成

manifest.json

Chrome拡張と同じく、manifest.jsonを用意します。
個人的に書き方で悩んだところとしては、

  • manifest_versionに 2 を指定
  • backgroundに、バックグラウンドで動作する background.js を指定 (background.js の中で、アプリで使うhtmlを指定している)
  • permissionsに、serial を指定

といったところです。

{
  "name": "chrome serial example",
  "description": "chrome serial example",
  "version": "1",
  "manifest_version": 2,
  "app": {
    "background": {
      "scripts": ["background.js"]
    }
  },
  "icons": { "128": "128x128.png",
              "16": "16x16.png" },
  "permissions": [
     "serial"
  ]
}

 

background.js

chrome.app.window.create() メソッドで、表示する画面のhtmlを指定します。また、必要に応じて画面のサイズや表示位置も指定します。

chrome.app.runtime.onLaunched.addListener(function(){
  chrome.app.window.create('window.html', {
    'bounds': {
      'width': 250,
      'height': 200, 
      'top': 0,
      'left': 100
    }
  });
});

 

window.html

表示する画面と処理を実行するJavaScriptを指定します。今回は、

  • シリアルCOMポートを選択する、select menu (id="port")
  • シリアルCOMポートに接続・データ受信をするためのボタン (id ="connect")
  • 全シリアルCOMポートの表示領域 (id="list")
  • 実際に接続・データ受信処理を実行するJavaScript (serial.js)

を記載しています。

<select id="port"></select>

<button id="connect">connect</button>

<div>
  <ul id="list">
  </ul>
</div>

<script src="serial.js"></script>

 

serial.js

メインの処理を記述しています。

なお、window.alert() を使おうとすると「window.alert() is not available in packaged apps.」というエラーが発生したため、実行結果などは console.log でコンソールへと出力しています。
Using the Console - Google Chrome

 

COMポートの列挙と追加

ロード時に現在の端末にあるCOMポートを列挙し、画面とselect menu に追加します。今のところ、手元の環境ではport.displayNameが設定されませんでした。

また、以下のようなBlog記事があったため、loadイベントの設定ではwindow.addEventListener()を使いました。
Loox Uと初音ミクで行こう!: Google ChromeでDOMContentLoadedが発生しないケースがある

var loadedListener = function() {
  console.log('loaded');

  //  デバイスをリスト化して、画面に表示する
  chrome.serial.getDevices(function(devices) {

    var list = document.getElementById('list');
    var selection = document.getElementById('port');

    devices.forEach(function(port){

      //  画面に表示
      var li = document.createElement('li');
      li.textContent = port.path;
      list.appendChild(li);


      //  select menu に追加
      var option = document.createElement('option');
      option.value = port.path;
      //  今のところ、手元の環境ではport.displayNameが設定されていない
      option.text = port.displayName ? port.displayName : port.path;
      selection.appendChild(option);
    });
  });
};
window.addEventListener('load', loadedListener, false);

 

COMポートへの接続

ボタンを押した時にCOMポートへ接続するようにするコードです。

なお、onReceiveイベントで使用するため、接続時の connectionId はこのイベントで取得したものを保持しておきます。

また、receiveTimeout を設定しておくことで onReceiveError イベントが発生することを利用し、データ受信の完了を知らせるようにしています。

var onConnectCallback = function(connectionInfo){
  //  onReceiveイベントでconnectionIdの一致を確認するので、保持しておく
  connectionId = connectionInfo.connectionId;
}


var clickedListener = function() {
  console.log('clicked');

  var e = document.getElementById('port');
  var port = e.options[e.selectedIndex].value;

  chrome.serial.connect(port, {bitrate: 9600, receiveTimeout: 5000}, onConnectCallback);
}
document.getElementById('connect').addEventListener("click", clickedListener, false);

 

シリアルCOMポートからデータを受信

公式のサンプルを参考に、データを受信する部分を記載します。
Serial Devices - Google Chrome

なお、公式のサンプルには expectedConnectionIdconvertArrayBufferToString() の定義は見つけられませんでした。

そこで、前者は接続時に保持しておいたconnectionInfo.connectionId、後者は公式のサンプルにあった関数のことだろうと判断し、コードを書きました。
chrome-app-samples/serial/ledtoggle/main.js | GoogleChrome/chrome-app-samples

/* Interprets an ArrayBuffer as UTF-8 encoded string data. */
function convertArrayBufferToString(buf){
  var bufView = new Uint8Array(buf);
  var encodedString = String.fromCharCode.apply(null, bufView);
  return decodeURIComponent(escape(encodedString));
}

var onReceiveCallback = function(info) {
  console.log('received');
  if (info.connectionId == connectionId && info.data) {

    //  こいつがうまく動かない:メソッドを定義してあげるとよい
    var str = convertArrayBufferToString(info.data);

    //  改行コード来たら、一つのデータの終端
    if (str.charAt(str.length-1) === '\n') {
      arrayReceived.push(stringReceived)
      stringReceived = '';
    } else {
      stringReceived += str;
    }
  }
};
chrome.serial.onReceive.addListener(onReceiveCallback);

 

受信したデータのコンソールへの表示

接続時に receiveTimeout を設定しているため、全件受信した後にしばらく待つと onReceiveError イベントが発生します。そのタイミングで、受信したデータの配列をコンソールへと表示しています。

また、データ受信が完了した時は接続を切るようにします。

var onDisconnectCallback = function(result) {
  if (result) { console.log('disconnected'); }
  else { console.log('error'); }
}

var onReceiveErrorCallback = function(info) {
  console.log('end');
  //  receiveTimeoutで設定した時間が経過すると、このイベントが発生
  //  ->データの受信は全て完了したとみなし、受信データを一覧表示する
  console.log(arrayReceived.join(','));

  var disconnect = chrome.serial.disconnect(connectionId, onDisconnectCallback)
}
chrome.serial.onReceiveError.addListener(onReceiveErrorCallback);

 

画面イメージとコンソール出力

画面の枠を作っていないので分かりにくいですが、画面イメージです。

f:id:thinkAmi:20140530054409p:plain

コンソールへの表示はこちら。

f:id:thinkAmi:20140530054433p:plain

 

ソースコード全体

GitHubに上げておきました。
ChromeApps-sample/serial at master · thinkAmi/ChromeApps-sample

 

参考

API関連
JavaScript関連