【ラズパイでもDocker!】Dockerコンテナ越しにシリアル通信を叩くNode.jsアプリの作成方法


※ 当ページには【広告/PR】を含む場合があります。
2021/06/12
【シリアル通信〜基礎編】USBを繋いでArduino Unoとラズパイ間で簡単なシリアル通信を行ってみる
蛸壺の中の工作室|Dockerコンテナ越しにシリアル通信を叩くNode.jsアプリの作成方法



著者的にラズパイでNodejsアプリを使う場合、Dockerコンテナ内部で起動させて利用することが多いですが、コンテナ外部のホストOSの権限を持ったデバイスを使うのにはコツがあります。
ちょっとしたことですが、今回はラズパイでDockerコンテナー内部から外部デバイスを操作する際に覚えておきたいポイントを解説してみます。


Dockerで動くNode.jsアプリの作成



まずはDockerコンテナ上でラズパイのシリアル通信を介して、信号を受け取るだけのNode.jsアプリを作成してみましょう。
以前のブログでは、ラズパイでシリアル通信するシンプルなやり方を解説していました。


合同会社タコスキングダム|蛸壺の技術ブログ
【シリアル通信〜基礎編】USBを繋いでArduino Unoとラズパイ間で簡単なシリアル通信を行ってみる

ラズパイをホストとしてUSB接続させたAruduinoとサクッと通信させる方法を解説します。




今回これをNode.jsアプリに仕立て直して使います。
また、
ラズパイにDockerをインストールする方法 で解説していましたので、詳しい作り方に関してはそちらをご覧ください。

node-serialport v9でシリアル通信プログラム実装



npmパッケージでシリアル通信を行う場合に利用できるライブラリが豊富にありますが、今回は採用事例の多い
node-serialport を利用します。
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クラス使って区切り位置を操作することになります。 今回は通常の改行文字区切りをつかうため、
Readlineパーサ を使う必要があります。
従来であれば受信した信号は生バイナリの状態のため適切にエンコードする必要がありましたが、パーサクラスで処理した信号は適切にエンコードされて出力されます(デフォルトではutf8)。

Dockerイメージの作成



今回はどかっと、本番用のコンテナイメージを以下に示します。
ターゲットデバイスはRaspberryPi 3B+ですのでアーキテクチャを考慮し、ベースイメージは
arm32v7/node にしていますが、nodeのバージョンが進みすぎるのもバグが多い気もするので、v12くらいで安定しているバージョンを選択します。
なおラズパイ4以降で試す際は、
arm64v8/node をベースに読み替えてください。

            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アプリの起動

前回の話の延長 で、ラズパイのシリアル通信の接続先にはArduinoを使います。 手元の環境でこのシリアルデバイスはホストOS(ラズパイ)上で /dev/ttyACM0 で認識しています。
ゲストOS(コンテナ)上でアクセス権限を付与するデバイス名を共通にしても良いのですが、今回は
/dev/ttyUSB0 という別名を付けてコンテナ側にこのデバイスをマウントしてみます。 (※--deviceオプションに関しては後述します。)
以下で
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 オプションを付けているのでコンテナ停止後は自動でコンテナごと消滅します。 停止後も残しておきたいコンテナは--rmオプションを付けないで起動させましょう。


コンテナに外部デバイスへのアクセス権限を追加する



上記では天下り的にDockerオプションからデバイスのアクセス権限の付与していましたが、改めてそのポイントをまとめてみましょう。

個別にデバイスを許可する

docker run のコマンドオプション --device で、

            $ docker run --device=<ホスト上のデバイス名>[:コンテナ上のデバイス名][:権限タイプ] ...

        

とすることでDockerコンテナ内で指定したホスト上のデバイスが利用できるようになります。

:コンテナ上のデバイス名 の部分は省略した場合、ホスト上のデバイス名がそのままコンテナ上のデバイス名になります。
つまりは、

            $ docker run --device=/dev/ttyACM0 ...
#👇と同じ
$ docker run --device=/dev/ttyACM0:/dev/ttyACM0 ...

        

権限タイプはコンテナからデバイスへ対してREAD(読み出し)・WRITE(書き込み)・MKNOD(特殊ファイル作成)がそれぞれ
:r / :w / :m を限定して指定もできます。 デフォルトでは :rwm の3つのオプションはセットされており、これは省略化です。

特権を与える(最終奥義)



ネットワーク越しにIoT機器を扱うことがますます多くなる昨今、コンテナに特権を与えて運用することはかなり危険なことになりつつあります。
とはいえ、動作確認段階からセキュリティをシビアに考慮することもなさそうであれば、
--privileged オプションは便利に扱えます。

            $ docker run --privileged -it <コンテナ名>

        

これでコンテナに特権が与えられ、
/dev/ 以下の全てのディレクトリがホストOSと同じように利用出来るようになります。

GPIOとUSBデバイスの違い



実は以前の記事で、GPIOをDockerコンテナで取り扱ったときにも、同様の内容に触れたことがありました。


合同会社タコスキングダム|蛸壺の技術ブログ
【ラズパイでLチカ】Docker Alpineコンテナ上のNode.jsネイティブからラズパイのGPIOを操作する方法

NodejsアプリをDockerコンテナを通じてラズパイのGPIOでLEDをチカチカさせる




今回はUSBデバイスをDockerコンテナから使うために、
--device=/dev/tty... を使ってデバイス指定をすることでアクセスが可能となったのですが、 GPIOだとこの方法が取れません でした。
いくつかあるGPIO操作のやり方で、デバイスに直接アクセスを介さず
/sys/class/gpio ファイルを使う方法では、Dockerコマンドで --device=/dev/gpiomem とアクセス権限を与えても、 /sys/ フォルダへのアクセスも同時に付与しなければ使えずにエラーが発生します。
なのでその場合、デバイスへのアクセス権ではなく、ライブラリを含むボリュームへのアクセス権を
--volume=/sys:/sys によってDockerコンテナに付与しなければならない、ということが結論でした。
不思議に思うかも知れませんが、そのときのDockerコンテナにはGPIOデバイスを利用する権限が付与されていなくても、マウント先の
/sys/class/gpio ファイルにはGPIOを実行する権限があるので、結局はDockerコンテナからGPIOデバイスが操作できるようになってしまいます。
今後Dockerのセキュリティ強化のため、このような権限跨ぎ的な利用方法が見直される場合があるかも知れませんが、Dockerへの実行権限の付与の仕組みを知っておけばその都度対処できると思います。


まとめ



以上、Dockerコンテナ上のNode.jsアプリからUSB接続されたArduinoからシリアル通信を行う方法を細かく検証していきましたが以下がだったしょうでしょうか。
node-serialportが動いたことで、ネットワーク上であれば何処からでも遠隔操作できるIoTシステムが構築できるなど、色々と応用の幅も広がってくるはずです。

参考サイト

About SerialPort - Quick Answers to Important QuestionsROS meets DockerDockerコンテナからUSBデバイスへのアクセス