Armadillo IoT A6EでNode開発(+GPIO/LED制御)

# はじめに

Armadillo-IoT A6E(以下Armadillo)に繋いだ機器をGPIOで制御するアプリを開発することになった。(後に時間もできたのでLED制御も追加)

Armadillo開発、コンテナ開発、さらにそれらで稼働するアプリをNode+JavaScriptで開発することが初めてでもあったので躓いた点などをまとめていく。

※Node+JavaScriptで実現しようと思ったのは他の組み込み機器やRaspberryPiで利用していたソース/ロジックの多くを流用できそうだったから。

本記事で記載する項目は主に以下となる。

## ATDE

* 環境構築
* dockerネットワーク(docker0)のセグメント
* vimのマウス制御
* コンテナのバージョン(1桁の数字)をmajor.minor.rev.buildなどの形式に変更する
* コンテナからのコンテナバージョン参照

## Armadillo

* 固定IPアドレス付与(nmcli利用)
* 固定IPアドレス付与(量産用のインストールディスクのip_config.txt利用)
* SSH有効化
* タイムゾーン設定(2024/09/11 追記)
* コンテナからのコンテナ更新(FW更新)
* コンテナからのコンテナ再起動

## Node開発

* インストールと設定
* GPIO制御
* LED制御

## その他

* 環境変数の設定

それでは以降に各項目に沿って説明を記載していく。

# [ATDE] 環境構築

## 証明書作成時のパスフレーズについて

パスフレーズを指定するとビルドの度に入力が求められる。
最初は複雑なパスフレーズにしてしまいかなり面倒だったので初期設定しなおしパスフレーズ無しにした。
長い(こともある)ビルドが最後まで走り切るので楽。

※※※ 自己責任で！！ ※※※

## abos-webのパスワード設定

初期設定時に前述のパスフレーズの件と併せ、abos-webのパスワード設定も求められる。(省略したらabos-webは起動しない)
不要なWebサービスを起動しておくのもよくないためパスワードは省略してabos-webを起動しないようにしていたが、後述のトークン発行やREST API利用で必須となった。
なのでabos-webで何ができるのかを把握してから判断をした方がよさそう。(基本すべてCLIでできそうな印象でもあったが、コンテナ側からのFW更新はREST APIの利用が推奨されていたし、またトークン発行は結局abos-webを利用したやり方しか分からなかった)

# [ATDE] dockerネットワーク(docker0)のセグメント

開発で利用するATDEのネットワークインタフェースにはlo/enp0s3/docker0の3つのインタフェースがある。
enp0s3の付与される(orすべき)IPはWiFiやLANケーブルを挿したネットワークに依存するが、docker0には `172.17.0.0/16` が割り当てられている。

今回諸事情でたまたまenp0s3側も172.17.0.0/16であったため、docker0側を変更する必要があり、以下の変更で対応した。(172.17.0.0/16 -> 172.16.0.0/16)

```Text:vi /etc/docker/daemon.json
{
        "bip": "172.16.0.1/16"
}
```

# [ATDE] vimのマウス制御

ターミナルソフトとしてTeraTermを利用しており、vimで開いたファイルにコピペで内容を貼り付けたいこともある。
しかしマウスでの右クリックでペーストしようとしても部分選択になってペースができない。
よって以下で対応。

```Text:vim ~/.vimrc
set mouse-=a
```

以下で反映。

```
source ~/.vimrc
```

# [ATDE] コンテナのバージョン(1桁の数字)をmajor.minor.rev.buildなどの形式に変更する

製品マニュアルの開発手順に沿ってアプリ開発を行い、SWUをビルドするとバージョンは1桁で、ビルドする度に`1 -> 2 -> 3 ...`とインクリメントされていく。
`major.minor.rev.build`形式で`1.0.0.1 -> 1.0.0.2 -> 1.0.0.3 ...`のように管理したかったので以下の変更を行う。

```Text:対象のファイル
~/project/smart-entrance-armadillo/info.json
~/project/smart-entrance-armadillo/swu/app.desc
~/project/smart-entrance-armadillo/swu/app.desc.old
```

## ~/project/smart-entrance-armadillo/info.json

```Text:変更前
{
  "version": "2"
}
```

```Text:変更後
{
  "version": "1.0.0.2"
}
```

## ~/project/smart-entrance-armadillo/app.desc

```Text:変更前
swdesc_option version=2
```

```Text:変更後
swdesc_option version=1.0.0.2
```

## ~/project/smart-entrance-armadillo/swu/app.desc.old

```Text:変更前
swdesc_option version=1
```

```Text:変更後
swdesc_option version=1.0.0.1
```

※app.desc.oldは1つ前のバージョンになっている点に注意。
※上記では"1.0.0.n"だが、"1.1.0.n"にしたい場合も上記3つのファイルを修正する必要がある。

[2024/09/26 追記]

※変更後に作成したSWUを適用する場合、Armadillo本体の /etc/sw-versions に記載されているバージョンも app.desc.oldに記載したバージョンに変更する必要がある。(例えば変更前が"2"の場合、前述のように"1.0.0.2"に変更した場合、2 から 1 へのダウングレードになるためSWUのインストールが行えないため。

# [ATDE] コンテナからのコンテナバージョン参照

コンテナの更新方法としてswupdate-urlサービスを利用する方式などがあるが、今回はArmadilloと連携するシステム側にFW更新で利用するファイル(今回はSWU)の登録/バージョン管理/手動でのFW更新管理などが行える仕組みが備わっており、そちらの利用を考えている。
その仕組みを利用する場合はコンテナのアプリ側で`自身のバージョン`と`サーバー側に登録されたSWUのバージョン`を比較し、サーバー側に登録されているSWUのバージョンが新しい場合のみFW更新を実施する、という動作をするようにしたかった。
そのためにはコンテナアプリ側で自身のバージョンを知る必要がある。(前述の`major.minor.rev.build`形式のバージョン)

この時に利用するバージョンはホスト側の`/etc/sw-versions`に記載されている。
よってこのファイルをコンテナアプリ側で参照できるようにするために以下の設定を行う。

```Text:~/project/smart-entrance-armadillo/config/app.conf
add_volumes /etc/sw-versions:/etc/sw-versions
```

上記の設定後にSWUを適用すると、コンテナアプリ側でも`/etc/sw-versions`が参照できるようになる。(":"の右側に記載したファイルパスで参照可能になる)

# [Armadillo] 固定IPアドレス付与(nmcli利用)

今回は固定IPを振りたかったので以下のコマンドで設定。

```
nmcli connection modify "Wired connection 1" ipv4.method manual ipv4.addresses 172.17.0.5/16 ipv4.gateway 172.17.0.1 connection.autoconnect yes
nmcli connection modify "Wired connection 1" ipv4.dns 172.17.0.1
persist_file /etc/NetworkManager/system-connections/Wired\ connection\ 1.nmconnection
nmcli connection down "Wired connection 1"
nmcli connection up "Wired connection 1"
```

NetworkManager(nmcli)の利用ではなく、`/etc/network/interfaces`を利用した付与も可能だったが名前解決ができない状態になった。
それなら`/etc/resolv.conf`の変更で、、、とも思ったがArmadilloの標準がNetworkManagerならそちらを使うかーってことで上記コマンド投入で対応。(abos-webもNetworkManager利用だったし)

# [Armadillo] 固定IPアドレス付与(量産用のインストールディスクのip_config.txt利用)

量産するArmadilloに固定IPアドレスを付与したい場合、量産用として作成したインストールディスク内にあるip_config.txtを利用する方法がある。

量産用として作成したインストールディスクをマウントし、その中に以下のip_config.txtを作成すればよい。

```Text:vi ip_config.txt
START_IP=172.17.0.5
END_IP=172.17.0.5
NETMASK=22
GATEWAY=172.17.0.１
DNS="172.17.0.1;8.8.8.8"
IFACE=eth0
```

この後、このインストールディスクを利用して初期化したArmadilloには、上記ip_config.txtで設定したIPアドレスが付与される。

# [Armadillo] SSH有効化

```
rc-update add sshd
persist_file /etc/runlevels/default/sshd
```

# [Armadillo] タイムゾーン設定(2024/09/11 追記)

後の「[Node開発] インストールと設定」でDocfileにTZ環境変数を設定するやり方を記載していたが、ホスト側のタイムゾーン設定をコンテナ側で参照するやり方も判明したので追記する。

前述の/etc/sw-versionsを参照する設定と同じやり方だ。

```Text:~/project/smart-entrance-armadillo/config/app.conf
add_volumes /etc/localtime:/etc/localtime
```

ただし(上記設定を適用したSWUを適用した後の)コンテナ側の/etc/localhostはUTCを参照したままになっている。
しかし実際にviで/etc/localhostの中を見てみるとJSTというキーワードが見えるためホスト側の/etc/localhostを参照していることが確認できる。

```
# ls -la /etc/localtime
lrwxrwxrwx 1 root root 27 May 13 09:00 /etc/localtime -> /usr/share/zoneinfo/Etc/UTC
# vi /etc/localtime
```

abos-web側の時刻設定で設定した時刻設定が利用できるようになるため、タイムゾーンを時々変更するケースではこのやり方の方が良いかもしれない。

# [Armadillo] コンテナからのコンテナ更新(FW更新)

コンテナの更新には幾つかのやり方がある。

* ホスト側でswupdateを実行する。
* swupdate-urlサービスを利用する
* abos-webのREST APIを利用する

今回は、コンテナ側ではswupdateを利用できない/前述のFW更新のための仕組みがある/SWUのダウンロードは認証付きにしたかった、などの理由でabos-webのREST APIを利用した方法を採用する。

## abos-webでトークン発行

REST APIを利用するためには`Authorization`ヘッダにトークンを指定する必要があるため、abos-webの「設定管理」－「Rest API トークン一覧」から「SwuInstall」権限を付与したTokenを払い出す。

※開発当初、このトークンはArmadillo毎に発行する必要があり、量産する場合にはどうしたら？？？と疑問も出たが、その点はまた後述する。(ここで発行したトークンは、量産するArmadillo側でも利用することになる)

## コンテナ側から実行するFW更新コマンド

以下のように、前述で発行したトークンを利用したREST APIの呼び出しで実行することになる。

```
curl -k "https://host.containers.internal:58080/api/swu/install/url" -X POST -H "Authorization:Bearer ${token}" -d url="${url}"
```

※curlがコンテナ側にはないケースもあるので、必要に応じてインストールが必要となる。(後述の「packages.txt」参照)
※コンテナ側からホスト側にswupdate実行を働きかける仕組みを作れば実現はできそう。

```Text:TIPS
更新用のSWU配置先はS3にし、署名付きで実現したかったのだがうまく動作しなかった。
これはURLに含まれるクエリ(?や=とか、tokenに含まれる文字)の影響ではないことは確認済み。
S3側の何かしらの挙動が影響しているように思われる。

なおabos-webのUI経由だと問題なく動作した。UI <-> Armadillo間はWebSocketだったのでその違いが何かあるのだろうか、、、。
```

# [Armadillo] コンテナからのコンテナ再起動

前述の組み込み機器やRaspberryPiではメモリのチェックと自動再起動の仕組みを入れていたので、念のため今回も適用。ただしコンテナ側からはrebootコマンドによる再起動が行えないため、以下で対応した。

```Text
kill -TERM 1
```

しかし、後にREST APIでも実現可能であることを発見。その実行コマンド例は以下の通り。

```
curl -k "https://host.containers.internal:58080/api/reboot" -X POST -H "Authorization:Bearer ${token}"
```

FW更新にREST APIを利用する方法を採用したので、同じ方式にした方がいいかもしれないが、現状はkillのまま。

# [Node開発] インストールと設定

Nodeが含まれているコンテナイメージもあるようなのでそちらを使う方法もあるが、今回は初期のプロジェクトで指定されていた`docker.io/${ARCH}/debian:bullseye-slim`にNodeをインストールする方法を記載していく。

```Text:Dockerfile
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y --no-install-recommends ${PACKAGES}
RUN curl -fsSLk https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs
RUN apt-get clean

WORKDIR /vol_app/src

ENV NODE_PATH=/usr/lib/node_modules

RUN npm install xxxxx -g
RUN npm install node-libgpiod -g

ENV TZ=Asia/Tokyo
```

以降に初期のプロジェクトに含まれているDockerfileに追加した項目の説明を記述していく。

```
RUN curl -fsSLk https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs
```

`RUN apt-get xxxx`の行が幾つかあるので、`RUN apt-get clean`の前あたりにNodeインストール用のコマンドを追加する。

```
WORKDIR /vol_app/src
```

確かnpmの実行でエラーが出て、調べるとNodeJsのバージョン15以上ではWORKDIRの指定が必要、という記述を見つけたので追記。

```
ENV NODE_PATH=/usr/lib/node_modules
```

NODE_PATH環境変数の設定がないようなので明記。

```
RUN npm install xxxxx -g
RUN npm install node-libgpiod -g
```

ローカルにインストールしたかったがNot Foundが解決しなかったのでグローバルにインストールするように。

※`RUN npm install node-libgpiod -g`の行は後述するGPIO制御のために必要となるため記載しておく。

```
ENV TZ=Asia/Tokyo
```

ホスト側にTimezoneの指定はあるが、コンテナ上のNode側で正しく認識しないケースがあるようで、その対応としてTZ環境変数を指定する方法を見つけたので追記。

[2024/09/11 追記]

ホスト側のタイムゾーン設定をコンテナ側で参照するやり方も判明したので「タイムゾーン設定(2024/09/11 追記)」に追記した。

# [Node開発] GPIO制御

## 設定

コンテナ側での利用を許可するために以下の設定を追加。

```Text:~/project/smart-entrance-armadillo/config/app.conf
add_devices /dev/gpiochip5
```

## 制御方法

RaspberryPiで実現していたGPIO制御(sysfs経由での制御)は現時点では非推奨となっている模様。
詳細はフォーラムでの問い合わせを参照のこと。

コンテナ内のNodeJS(JavaScript)からのGPIO制御について | Armadilloサイト
https://armadillo.atmark-techno.com/forum/armadillo/20355

ということで、以下の設定を実施。

```Text:Dockerfile
RUN npm install node-libgpiod -g
```

※前述のDockerfileの記載例も参照のこと。

さらにnode-libgpiodが必要とするパッケージが幾つかあるので以下のようにpackages.txtに追加。
ここで記載したパッケージは、前述のDockerfileの例にある`RUN apt-get install -y --no-install-recommends ${PACKAGES}`でインストールされる。
よってapt-get installでインストールできるパッケージ名を記載する必要がある。

```Text:~/project/smart-entrance-armadillo/container/packages.txt
bash
vim
curl
iproute2
iputils-ping
procps
build-essential
udev
gpiod
libgpiod2
libgpiod-dev
libnode-dev
cmake
```

node-libgpiodの公式サイト的には、

```Text
gpiod
libgpiod2
libgpiod-dev
libnode-dev
```

の4つあればよい模様。

# [Node開発] LED制御

製品マニュアルの手順に沿ってプロジェクトを生成したら存在していたsample.shに記載のロジックを紐解くだけなので要点だけ。

書き込み対象のファイルは以下。

```Text
/sys/class/leds/app/brightness
```

このファイルをopenして`0:消灯`、`1:点灯`を時間変えながらwiteするのみ。(`早い点滅`は0と1を交互に0.5秒毎にwriteする、とか、`1秒間は点灯してそのあと消す`は1をwriteした後に1秒後に0をwriteする、だけ)

```
fs.open('/sys/class/leds/app/brightness', 'w', (err, fd) => {
・
・
・
    fs.write(fd, ledon ? '1' : '0', (err, written) => {
    ・
    ・
    ・
```

として

```
exports.PATTERN_OFF        = {duration: -1,   pattern: [{ledon:false, duration: 1000}] };
exports.PATTERN_BLINK_FAST = {duration: -1,   pattern: [{ledon:true,  duration: 50},    {ledon:false, duration: 50}]    };
exports.PATTERN_BLINK_SLOW = {duration: -1,   pattern: [{ledon:false, duration: 3000},  {ledon:true,  duration: 100}]   };
exports.PATTERN_ERR        = {duration: -1,   pattern: [{ledon:false, duration: 3000},  {ledon:true,  duration: 100},   {ledon:false, duration: 100},   {ledon:true, duration: 100}]    };
```

とパターン用の定義作って、それに従ってwrite繰り返して実現すると。
ただNodeはシングルプロセス/シングルスレッドなので数十msの制御は綺麗にできないことも。その場合は前述のsample.shのようにシェルスクリプトを駆使するか、別プロセス化するなどの対策が必要になってくる。(別プロセス化するとプロセス間通信をどうするか、、、という別の課題も出てくるが)

# [その他] 環境変数の設定

JavaScriptからも利用したりシェルスクリプトを利用したりする関係で環境変数があれば便利なのでDockerfileには以下を追記している。

```Text:Dockerfile
ENV MY_PRODUCT_NAME="my-project-for-armadillo"
ENV LOG_DIR=/vol_data/log
ENV LOG_FILE=${LOG_DIR}/${MY_PRODUCT_NAME}.log
ENV RUN_DIR=/vol_data/run
```

# 最後に

是非、こういう方法もあるとか、公式的にはこういうやり方の方が良い・推奨している、などがあれば教えてください。