ElixirでIoT#4.3：Nervesアプリ開発時のよくあるトラブルをシューティング

# はじめに

関数型言語ElixirでIoTを開発できる**「ナウでヤングなcoolな」**Nervesフレームワーク，楽しいっすよね！！？

ですが初めて触る方には，いろいろ引っかかるトラップがあるかと思います．そこにハマってしまっては面白くない,,, せっかくならIoTアプリの開発で存分に楽しみたい！ということで，よくあるエラーと対処策をまとめてみることにしました．

他にもこんなのもあるんじゃない？こう解決できるよ！とかありましたら，どしどしコメントや[筆者へのTwitter](https://twitter.com/TAKASEhideki)にお寄せください．もちろん，こんなんで困ってんだけどどうすんの？てのも大歓迎です！！
適宜で項目やリンクを追加していくつもりです．

# 環境設定篇

開発環境の構築やNervesプロジェクトの一通りの開発プロセスでのハマりどころです．

## `nerves.new`コマンドが動かない

Nervesの良いところは，普通のElixirアプリの開発と同じように`mix`でプロジェクト作成と管理ができることです．
ということで喜び勇んでNervesプロジェクト作成のコマンド`mix nerves.new`をしてみたら，どうもうまく動きません,,,

### 症状

```shell-session
$ mix nerves.new hello_nerves 
** (Mix) The task "nerves.new" could not be found
Note no mix.exs was found in the current directory
```

### 原因と対処法

Nerves向けの`mix`コマンドがアーカイブされた[`nerves_bootstrap`](https://github.com/nerves-project/nerves_bootstrap)をインストールしてやりましょう．

```shell-session
$ mix archive.install hex nerves_bootstrap
```

## ssh鍵が無い

プロジェクトを作成したら次に実行するコマンドは`mix deps.get`です．これでプロジェクトに依存するライブラリをダウンロードしてきて，それらの依存性を解決してくれます．
ところがこれがどうも動きません,,,

### 症状

```shell-session
$ mix deps.get 
** (Mix) No SSH public keys found in ~/.ssh. An ssh authorized key is needed to
log into the Nerves device and update firmware on it using ssh.
See your project's config.exs for this error message.

```

### 原因と対策

NervesではホストPCとデバイスとの通信にsshを使います．この通信をセキュアにするために鍵交換方式が用いられます．公開鍵がNervesアプリのビルド時に参照されて，その情報をアプリに取り込みます．

このように，公開鍵と秘密鍵のキーペアを作ってやりましょう．

```shell-session
$ ssh-keygen -t rsa
```

### TIPS: Nervesで使用するssh鍵

Nervesのデフォルトで使えるssh鍵のファイル名は，下記のとおりです．
（カッコ内は鍵作成時のコマンドラインとオプション）

- id_rsa.pub: RSA方式の公開鍵 (`ssh-keygen -t rsa`)
- id_ecdsa.pub: ECDSA方式の公開鍵 (`ssh-keygen -t ecdsa`)
- id_ed25519.pub: (`ssh-keygen -t ed25519`)

公開鍵とはいえ普段使っている？ものがNervesデバイスに取り込まれるのはコワイ,,, という方もいらっしゃるかと思います．`ssh-keygen -f`オプションでファイル名を指定して専用の鍵ファイルを作ることもできます．

```shell-session
ssh-keygen -t rsa -f ~/.ssh/id_rsa_nerves
```

このようにした場合は，プロジェクトのconfig/target.exsの30行目辺りでファイル名を指定してやってください．`Path.join()`の上から順にファイルを探して最初に見つかったものを利用します．

```elixir:config/target.exs
keys =
  [
    Path.join([System.user_home!(), ".ssh", "id_rsa_nerves.pub"]),    # 追加した行
    Path.join([System.user_home!(), ".ssh", "id_rsa.pub"]),
    Path.join([System.user_home!(), ".ssh", "id_ecdsa.pub"]),
    Path.join([System.user_home!(), ".ssh", "id_ed25519.pub"])
  ]
  |> Enum.filter(&File.exists?/1)
```

## `nerves_bootstrap`が古い

まだ`mix deps.get`がうまく動いていないようです,,,

### 症状

```shell-session
$ mix deps.get 
Resolving Hex dependencies...
Dependency resolution completed:
Unchanged:
  nerves 1.6.3
  shoehorn 0.6.0
All dependencies are up to date

Nerves environment
  MIX_TARGET:   host
  MIX_ENV:      dev

Resolving Nerves artifacts...
A new version of Nerves bootstrap is available(1.8.0 < 1.8.1), You can update by running

  mix local.nerves

```

### 原因と対処法

`nerves_bootstrap`を新しいバージョンに更新する必要があります．
`mix local.nerves`で最新版に更新することができます．

```shell-session
$ mix local.nerves 
Resolving Hex dependencies...
Dependency resolution completed:
New:
  nerves_bootstrap 1.8.1
* Getting nerves_bootstrap (Hex package)
All dependencies are up to date

13:31:19.113 [info]  Application nerves_bootstrap exited: :stopped
Compiling 11 files (.ex)
Generated nerves_bootstrap app
Generated archive "nerves_bootstrap-1.8.1.ez" with MIX_ENV=prod
Found existing entry: /Users/takase/.asdf/installs/elixir/1.10.3-otp-23/.mix/archives/nerves_bootstrap-1.8.0
Are you sure you want to replace it with "nerves_bootstrap-1.8.1.ez"? [Yn] Y
* creating /Users/takase/.asdf/installs/elixir/1.10.3-otp-23/.mix/archives/nerves_bootstrap-1.8.1

```

### TIPS: バージョンを指定したい

逆に，昔作ったNervesアプリについて，バージョンは保ったままメンテしたいということがあるかと思います（多くの場合はバージョンアップしたほうが良い！ということは置いといて^^;

特定のバージョンをインストールするには，下記のようにします．

```shell-session
$ mix archive.install hex nerves_bootstrap 1.8.0
```

## 環境変数が設定されていない

次は`mix firmware`です．このコマンドでNervesのファームウェアをビルドします．
ところがどうも真っ赤なメッセージが出てきました,,,

### 症状

```shell-session
$ mix firmware 
==> toolshed
Compiling 10 files (.ex)
Generated toolshed app
==> ring_logger
Compiling 4 files (.ex)
Generated ring_logger app
==> shoehorn
Compiling 8 files (.ex)
Generated shoehorn app
==> hello_nerves

Nerves environment
  MIX_TARGET:   host
  MIX_ENV:      dev

NERVES_SYSTEM is unset
NERVES_TOOLCHAIN is unset
** (Mix) Environment variable $NERVES_SYSTEM is not set.

This variable is usually set for you by Nerves when you specify the
$MIX_TARGET. It is unusual to need to specify it yourself.

Some things to check:

1. In your `mix.exs`, is the value that you have in $MIX_TARGET in the
  `@all_targets` list? If you're not using `@all_targets`, then the
  $MIX_TARGET should appear in the `:targets` option for `:nerves_runtime`
  and other packages that run on the target.

2. Do you have a dependency on a Nerves system for the target? For example,
  `{:nerves_system_rpi0, "~> 1.8", runtime: false, targets: :rpi0}`

3. Is there a typo? For example, is $MIX_TARGET set to `rpi1` when it should
  be `rpi`.

4. Is there a typo in the package name of the system? For example, if you
  have a custom system, `:nerves_system_my_board`, does the spelling of the
  system in the dependency in your `mix.exs` match the spelling in your
  system project's `mix.exs`?

For build examples in the Nerves documentation, please see
https://hexdocs.pm/nerves/getting-started.html#create-the-firmware-bundle

```

### 原因と対処法

Nervesではシェル環境変数`$MIX_TARGET`で対象のデバイスを指定します．
`rpi0`とか`bbb`とか設定してやる必要があります．

```shell-session
# ラズパイゼロの例
$ export MIX_TARGET=rpi0
```

なお，このあとにもういちど`mix deps.get`してやってください．

### TIPS: ターゲットデバイスの指定

Nerves公式のターゲットデバイスは，[このhexdocsページ](https://hexdocs.pm/nerves/targets.html)のTAG列を参照してください．

ちなみに，同じNervesのプロジェクトディレクトリを，複数のターゲット向けに共用することができるわけです．環境変数`$MIX_TARGET`をポチポチ切り替えるだけです．

## Erlang/OTPのバージョンが合っていない

さぁ`$MIX_TARGET`も設定したしもう一度！
でも今度は違うエラーが？？

### 症状

```shell-session
$ mix firmware 
==> nerves_system_br
Generated nerves_system_br app
==> nerves_toolchain_ctng
Compiling 1 file (.ex)
Generated nerves_toolchain_ctng app
==> nerves_toolchain_arm_unknown_linux_gnueabihf
Generated nerves_toolchain_arm_unknown_linux_gnueabihf app
==> nerves_system_rpi3
Generated nerves_system_rpi3 app
==> hello_nerves

Nerves environment
  MIX_TARGET:   rpi3
  MIX_ENV:      dev

** (Mix) Major version mismatch between host and target Erlang/OTP versions
  Host version: 22
  Target version: 23

This will likely cause Erlang code compiled for the target to fail in
unexpected ways.

The easiest way to resolve this issue is to install the same version of
Erlang/OTP on your host. See the Nerves installation guide for doing this
using the `asdf` version manager.

The Nerves System (nerves_system_*) dependency determines the OTP version
running on the target. It is possible that a recent update to the Nerves
System pulled in a new version of Erlang/OTP. If you are using an official
Nerves System, you can verify this by reviewing the CHANGELOG.md file that
comes with the release. Run 'mix deps' to see the Nerves System version and
go to that system's repository on https://github.com/nerves-project.

If you need to run a particular version of Erlang/OTP on your target, you can
either lock the nerves_system_* dependency in your mix.exs to an older
version. Note that this route prevents you from receiving security updates
from the official systems. The other option is to build a custom Nerves
system. See the Nerves documentation for building a custom system and then
run 'make menuconfig' and look for the Erlang options.

```

### 原因と対処法

最新のNervesはErlang/OTP 23に対応しています．このバージョン番号はホストPCのものと一致している必要があります．

@MzRyuKa さんの記事にも詳しい解説があります．OTP 22のまま利用する対処方法も紹介されていますので，あわせてご覧ください．

- [Nervesで「nerves_system_*」の都合により「Major version mismatch between host and target Erlang/OTP versions」が発生する場合の対処方法](https://qiita.com/MzRyuKa/items/37d803a9eca6218d4133)

`asdf`でElixir/Erlangのバージョンを管理されている方は，`asdf`もv0.7.8以降にしてやる必要があります．ということで，まずはasdfをアップデートしましょう．

```shell-session
$ asdf update
```

Elixir/Erlangのプラグインもアップデートしておきます．

```shell-session
$ asdf plugin update erlang
$ asdf plugin update elixir
```

そして，Elixir/Erlangをアップデートしてやります．

```shell-session
$ asdf install erlang 23.0.1
$ asdf install elixir 1.10.3-otp-23
$ asdf global erlang 23.0.1
$ asdf global elixir 1.10.3-otp-23

$ mix local.hex
$ mix local.rebar

$ mix archive.install hex nerves_bootstrap
```

## microSDカードが刺さっていない

無事にファームウェアのビルドができました．
microSDカードへの書込みは`mix burn`です．
ところが，なんかおかしいです,,,

### 症状

```shell-session
$ mix burn 

Nerves environment
  MIX_TARGET:   rpi0
  MIX_ENV:      dev

** (Mix) Could not auto detect your SD card

```

### 原因と対処法

まぁ見てのとおりですね，microSDカードをホストPCに刺してください^^;
あるいは刺し方が甘くて認識していないこともありえますので，もう一度刺し直してみてください．

```shell-session
$ mix burn 

Nerves environment
  MIX_TARGET:   rpi0
  MIX_ENV:      dev

Use 7.31 GiB memory card found at /dev/rdisk3? [Yn] Y
100% [====================================] 31.71 MB in / 34.21 MB out       
Success!
Elapsed time: 3.047 s

```

なお書込み先がホントに`rdisk3`で番号合ってる？？と疑心暗鬼な方は，`df`とか`diskutil list`でご確認ください^^;

書込みがいったん完了したら，自動でmicroSDカードをアンマウントします．
もう一度書き直したい場合は，microSDカードを刺し直してください．

# ssh接続篇

さぁNervesデバイスを実行するぞ！
ホストからの接続は，基本的にはssh接続で通信します．
ラズパイゼロやBeagleBoneファミリでは，USBケーブルの接続がVirtualEtherと認識されて，それだけでssh接続が可能です．
その他のボードでは，ネットワークの設定が必要です．最新のNervesではVintageNetというライブラリを利用します．この設定方法は，下記の記事が詳しいです．

- 「[nerves_pack（vintage_net含む）を使ってNervesのネットワーク設定をした〜SSHログインまで〜](https://qiita.com/nishiuchikazuma/items/01f5df5472ec0feceb5f)」 by @nishiuchikazuma さん
- 「[備忘録: WSL2 Ubuntu上で NervesとLAN接続してみる](https://qiita.com/ShozF/items/844208b3bde57d6be3a4) by @ShozF さん」

そのときのハマりどころです．

## `nerves.local`が見つからない

NervesデバイスにはmDNSが動作していて，デフォルトでは`nerves.local`という名前が自動的に付与されます．
ところが例えば`ping`してやると，この名前が見つかりません,,,

### 症状

```shell-session
$ ping nerves.local 
ping: cannot resolve nerves.local: Unknown host
```

### 原因と対処法

まずは，まだmDNSサービスが立ち上がっていないことが考えられます．経験的にラズパイゼロだと，電源投下してから15秒くらいは掛かります．
そんなせっかちにならずに,,, ちょっとだけお待ちください^^;

```shell-session
$ ping nerves.local 
PING nerves.local (172.31.77.101): 56 data bytes
64 bytes from 172.31.77.101: icmp_seq=0 ttl=64 time=0.637 ms
64 bytes from 172.31.77.101: icmp_seq=1 ttl=64 time=0.508 ms
64 bytes from 172.31.77.101: icmp_seq=2 ttl=64 time=0.638 ms
^C
--- nerves.local ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.508/0.594/0.638/0.061 ms

```

あるいは，デフォルトの設定を変更して，`nerves.local`でない名前にされているかもしれません．
この名前の設定は，config/target.exsの63行目辺りの`config: mdns_lite,`ブロックにあります．

```elixir:config/target.exs
config :mdns_lite,
  # The `host` key specifies what hostnames mdns_lite advertises.  `:hostname`
  # advertises the device's hostname.local. For the official Nerves systems, this
  # is "nerves-<4 digit serial#>.local".  mdns_lite also advertises
  # "nerves.local" for convenience. If more than one Nerves device is on the
  # network, delete "nerves" from the list.

  host: [:hostname, "nerves"],
  ttl: 120,

```

ちなみに，ここのコメントに書いてあるとおり，`nerves-<4 digit serial#>.local`という名前も付いています．この4桁は，ボードのシリアル番号から付けられています．

それでも繋がらない場合は,,, `arp -a`などでIPアドレスを探してやって，直接繋いでみてください．（いったんIPアドレスで繋がると，その後は名前解決もできて`nerves.local`で繋がるようになることがあります）

## ssh接続できない

さぁ`nerves.local`も見つかっているしNervesデバイスにssh接続するぞ！
でも，いごかない,,, passwordってなにそれ？？

### 症状

```shell-session
$ ssh nerves.local 
SSH server
Enter password for "takase"
password: 
(省略)
Permission denied (publickey,keyboard-interactive,password).
```

### 原因と対処法

これはssh鍵をデフォルトの`id_rsa`以外にしているときに発生することが多いです．
例えば[前述したこんな感じ](https://qiita.com/takasehideki/items/5c79f5531fa189be99f0#tips-nerves%E3%81%A7%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8Bssh%E9%8D%B5)で`id_rsa_nerves`という名前の鍵を作っていたとしましょう．

まずひとつめの対処方法は，コマンドラインで秘密鍵を指定してやることです．

```shell-session
$ ssh nerves.local -i ~/.ssh/id_rsa_nerves
Interactive Elixir (1.10.3) - press Ctrl+C to exit (type h() ENTER for help)
Toolshed imported. Run h(Toolshed) for more info.
RingLogger is collecting log messages from Elixir and Linux. To see the
messages, either attach the current IEx session to the logger:

  RingLogger.attach

or print the next messages in the log:

  RingLogger.next

iex(1)> 
```

あるいは，`~/.ssh/config`に下記のように設定情報を書いておくと，コマンドラインで指定する必要が無くなります．

```~/.ssh/config
Host nerves.local
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  IdentityFile ~/.ssh/id_rsa_nerves
```

# 実行時篇

さぁ繋がったぞ！さくさく動いてるぞ！！
あれっでもなんかおかしい？？

## 終了できない

ElixirのREPLであるiexの終了時は，Ctrl+Cを2回押しされる方が多いかと．

```shell-session
$ iex
Erlang/OTP 23 [erts-11.0.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Interactive Elixir (1.10.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 
BREAK: (a)bort (A)bort with dump (c)ontinue (p)roc info (i)nfo
       (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution
^C

```

あれっでもこれNervesじゃダメじゃね？？

### 症状

```shell-session
iex(1)> ** (EXIT) interrupted
iex(1)> ** (EXIT) interrupted
```

### 原因と対処法

なぜなら今はNervesデバイスにssh接続しているからです．
ssh接続を終了する場合は`exit`ですよね．

```shell-session
iex(1)> exit 
Connection to nerves.local closed.

```

ちなみに，Nervesデバイスをちゃんと終了したい場合は，`System.stop/0`を使うと良いでしょう．

```shell-session
iex(1)> System.stop
:ok
Received disconnect from 172.31.77.101 port 22:11: Terminated (shutdown) by supervisor
Disconnected from 172.31.77.101 port 22

```


# おわりに

さぁ皆さんのNervesデバイス，思ったとおりに**「いごいた」**でしょうか！？？

さっくりまとめるつもりだったのが，思ったより長くなってしまった,,,

冒頭にも書きましたが，他にもこんなのもあるんじゃない？こう解決できるよ！とかありましたら，どしどしコメントや[筆者へのTwitter](https://twitter.com/TAKASEhideki)にお寄せください．
もちろん，こんなんで困ってんだけどどうすんの？てのも大歓迎です！！