【ラズパイでもDocker!】Dockerコンテナ越しにシリアル通信を叩くNode.jsアプリの作成方法  
※ 当ページには【広告/PR】を含む場合があります。
2021/06/12

著者的にラズパイでNodejsアプリを使う場合、Dockerコンテナ内部で起動させて利用することが多いですが、コンテナ外部のホストOSの権限を持ったデバイスを使うのにはコツがあります。
ちょっとしたことですが、今回はラズパイでDockerコンテナー内部から外部デバイスを操作する際に覚えておきたいポイントを解説してみます。
Dockerで動くNode.jsアプリの作成
まずはDockerコンテナ上でラズパイのシリアル通信を介して、信号を受け取るだけのNode.jsアプリを作成してみましょう。
以前のブログでは、ラズパイでシリアル通信するシンプルなやり方を解説していました。
今回これをNode.jsアプリに仕立て直して使います。
また、
node-serialport v9でシリアル通信プログラム実装
npmパッケージでシリアル通信を行う場合に利用できるライブラリが豊富にありますが、今回は採用事例の多い
nodeのビルド環境が整っていれば導入は簡単に、
            $ npm insall serialport
#OR
$ yarn add serialport
        でインストール可能になります。
Dockerイメージのビルドの話は後述しますが、コンテナの実行エンドポイントのindex.jsは以下のようにします。
            const SerialPort = require('serialport');
const Readline = require('@serialport/parser-readline');
//👇ゲスト上のSerialデバイスに名前を合せる
const portName = '/dev/ttyUSB0';
//👇接続先(Arduino)のシリアルモデムを考慮
const sp = new SerialPort(portName, {
    baudRate: 9600,
    dataBits: 8,
    parity: 'none',
    stopBits: 1,
    flowControl: false,
});
//👇ポート開放時の初期化処理
sp.on('open', ()=> {
    console.log('Open Serialport');
});
//👇パーサクラスを指定
const parser = sp.pipe(new Readline());
//👇データ(Arduino > ラズパイ)の受信
parser.on('data', (inp_) => {
    try {
        console.log(`Received: ${inp_}`);
    } catch(e) {
        return;
    }
});
        なお、現行のnode-serialportではデリミタ(区切り文字)の扱いが大きく変わり、従来の
serialport.on('data',...)そのため新しい仕組みとしてParserクラス使って区切り位置を操作することになります。 今回は通常の改行文字区切りをつかうため、
従来であれば受信した信号は生バイナリの状態のため適切にエンコードする必要がありましたが、パーサクラスで処理した信号は適切にエンコードされて出力されます(デフォルトではutf8)。
Dockerイメージの作成
今回はどかっと、本番用のコンテナイメージを以下に示します。
ターゲットデバイスはRaspberryPi 3B+ですのでアーキテクチャを考慮し、ベースイメージは
なおラズパイ4以降で試す際は、
            FROM arm32v7/node:12-alpine3.11
RUN apk update && apk upgrade && apk add --no-cache \
    --virtual sp-deps make gcc g++ python alpine-sdk linux-headers udev
ENV NODE_ENV development
WORKDIR /usr/src/app
COPY package.json /usr/src/app/
RUN cd /usr/src/app && yarn install && yarn cache clean
RUN apk del sp-deps
COPY index.js /usr/src/app/
CMD ["node", "index.js"]
        先にnpmパッケージをグローバルで入れても良いのですが、今回はpackage.jsonでローカルインストールを作成します。
            {
    "name": "serial-dckr",
    "version": "0.0.1",
    "license": "MIT",
    "dependencies": {
        "serialport": "^9.0.0"
    }
}
        ではDockerイメージのビルドを行います。
ここではイメージの名前をタグ付きで
serial-dckr:node12            $ docker build -t serial-dckr:node12 .
        正常にビルドできたらひとまずOKです。
DockerコンテナのNodeアプリの起動
/dev/ttyACM0ゲストOS(コンテナ)上でアクセス権限を付与するデバイス名を共通にしても良いのですが、今回は
/dev/ttyUSB0以下で
piserial            $ docker run -it \
    --rm \
    --device=/dev/ttyACM0:/dev/ttyUSB0 \
    --name piserial \
    serial-dckr:node12
#👇node index.jsのプロセスが走り出す
Open Serialport
Received: LED OFF
Received: LED ON
Received: LED OFF
Received: LED ON
#...省略
        と確かにシリアル通信で外部のArduinoからの文字列信号を受信している状態であれば成功です!
起動しているコンテナの止め方
今回のコンテナは一度起動したら走りっぱなしですので、他のコンソールから強制的に
docker stop起動を停止させる際はコンテナ名を使って、
            $ docker stop piserial
        とすると止めることができます。
また
--rmコンテナに外部デバイスへのアクセス権限を追加する
上記では天下り的にDockerオプションからデバイスのアクセス権限の付与していましたが、改めてそのポイントをまとめてみましょう。
個別にデバイスを許可する
docker run            $ docker run --device=<ホスト上のデバイス名>[:コンテナ上のデバイス名][:権限タイプ] ...
        とすることでDockerコンテナ内で指定したホスト上のデバイスが利用できるようになります。
:コンテナ上のデバイス名つまりは、
            $ docker run --device=/dev/ttyACM0 ...
#👇と同じ
$ docker run --device=/dev/ttyACM0:/dev/ttyACM0 ...
        権限タイプはコンテナからデバイスへ対してREAD(読み出し)・WRITE(書き込み)・MKNOD(特殊ファイル作成)がそれぞれ
:r:w:m:rwm特権を与える(最終奥義)
ネットワーク越しにIoT機器を扱うことがますます多くなる昨今、コンテナに特権を与えて運用することはかなり危険なことになりつつあります。
とはいえ、動作確認段階からセキュリティをシビアに考慮することもなさそうであれば、
--privileged            $ docker run --privileged -it <コンテナ名>
        これでコンテナに特権が与えられ、
/dev/GPIOとUSBデバイスの違い
実は以前の記事で、GPIOをDockerコンテナで取り扱ったときにも、同様の内容に触れたことがありました。
今回はUSBデバイスをDockerコンテナから使うために、
--device=/dev/tty...いくつかあるGPIO操作のやり方で、デバイスに直接アクセスを介さず
/sys/class/gpio--device=/dev/gpiomem/sys/なのでその場合、デバイスへのアクセス権ではなく、ライブラリを含むボリュームへのアクセス権を
--volume=/sys:/sys不思議に思うかも知れませんが、そのときのDockerコンテナにはGPIOデバイスを利用する権限が付与されていなくても、マウント先の
/sys/class/gpio今後Dockerのセキュリティ強化のため、このような権限跨ぎ的な利用方法が見直される場合があるかも知れませんが、Dockerへの実行権限の付与の仕組みを知っておけばその都度対処できると思います。
まとめ
以上、Dockerコンテナ上のNode.jsアプリからUSB接続されたArduinoからシリアル通信を行う方法を細かく検証していきましたが以下がだったしょうでしょうか。
node-serialportが動いたことで、ネットワーク上であれば何処からでも遠隔操作できるIoTシステムが構築できるなど、色々と応用の幅も広がってくるはずです。