Elixir/Nerves/rpi0でLチカする +α

# はじめに

備忘録です。
ElixirのIoTフレームワークであるNervesにてようやくLチカしました。 
2年前のALGYANハンズオンの内容[^handsondoc]を途中まで追っかけただけです。

[^handsondoc]: [2020/12/27【オンライン】豪華プレゼント付！Elixir/Nerves(ナーブス)体験ハンズオン！ ](https://algyan.connpass.com/event/197306/)

# 環境
- 母艦機: antiX21(debian GNUlinux11/bullseye) on sony VAIO VGN-FW72JGB / 4GB
- target機: RaspberryPI-0 WH
- Erlang: 25.0.4
- Elixir: 1.14.0-otp-25

コマンド操作はすべて母艦機上で行います。


# asdf のインストール

erlangとelixirのバージョン管理ツールにasdfを使います。  
asdfの[公式サイト](https://github.com/asdf-vm/asdf)の[getting started](http://asdf-vm.com/guide/getting-started.html#_1-install-dependencies)に従います。

```sh
$ sudo apt install curl git
$ git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.10.2
```

私は`bash`+`git`のケースなので、公式サイトの指示に従って、`~/.bashrc` を編集し、bashを再度立ち上げます。

```sh:~/.bashrc
  :
. $HOME/.asdf/asdf.sh
. $HOME/.asdf/completions/asdf.bash
```

# Erlang / Elixir のインストール

[nishiuchiさんの記事](https://qiita.com/nishiuchikazuma/items/b9d319732ddb540fd990#erlangelixir%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E8%BF%BD%E5%8A%A0)に従って、以下を進めていきます。  
Erlang/Elixirとも最新バージョンのインストールでいいと思います。
 【オンライン】豪華プレゼント付！Elixir/Nerves(ナーブス)体験ハンズオン！ 
- [Erlang/Elixirプラグインを追加](https://qiita.com/nishiuchikazuma/items/b9d319732ddb540fd990#erlangelixir%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E8%BF%BD%E5%8A%A0)
- [インストール](https://qiita.com/nishiuchikazuma/items/b9d319732ddb540fd990#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
- [使用するバージョンを指定](https://qiita.com/nishiuchikazuma/items/b9d319732ddb540fd990#%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%92%E6%8C%87%E5%AE%9A)

```sh
# プラグイン
$ asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git
$ asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
# erlang/elixirインストール
$ asdf install erlang 25.0.4
$ asdf install elixir 1.14.0-otp-25
# 使用するバージョンを指定
$ asdf global erlang 25.0.4
$ asdf global elixir 1.14.0-otp-25
```
erlangのバージョンは[公式サイト](https://www.erlang.org/downloads)で調べます。

`asdf install`でエラーが出る場合は、エラー内容に従って、インストールします。
```shell
:
Building Erlang/OTP 25.0.2 (asdf_25.0.2), please wait...
WARNING: It appears that a required development package 'libssl-dev' is not installed.
WARNING: It appears that a required development package 'make' is not installed.
WARNING: It appears that a required development package 'automake' is not installed.
WARNING: It appears that a required development package 'autoconf' is not installed.
WARNING: It appears that a required development package 'libncurses5-dev' is not installed.
WARNING: It appears that a required development package 'gcc' is not installed.
:
$ sudo apt install libssl-dev make automake autoconf libncurses5-dev gcc
```






# Nervesのインストール

公式の[hexdoc](https://hexdocs.pm/nerves/installation.html#linux)ページの内容に基づいて進めます。<br/>

```sh
$ sudo apt install build-essential automake autoconf git squashfs-tools ssh-askpass pkg-config curl
```

`fwup`のインストール  
`fwup`はmicroSDカードにfirmwareを書き込むツールです。  
[公式ページ](https://github.com/fwup-home/fwup#installing)に従って、debパッケージをブラウザ上でダウンロードしてから、以下のコマンドでインストールします。
```sh
# download fwup.deb
$ sudo dpkg -i ~/Downloads/fwup_1.9.0_amd64.deb
```

引き続きNervesのインストールを続けます。

```sh
$ sudo apt install libssl-dev libncurses5-dev bc m4 unzip cmake python
```

```sh
$ mix local.hex
$ mix local.rebar
$ mix archive.install hex nerves_bootstrap
```

Nervesのインストールはこれで終了です。

# Nervesのプロジェクトの作成

公式ページにかかれている通りに新規プロジェクトを作成します。  
`mix nerves.new {プロジェクト名}`で作成できます。

```sh
$ mix nerves.new Hellonerves
$ cd Hello_nerves
```

## wifiの設定

[`vintage.net`](https://hexdocs.pm/vintage_net/readme.html#configuration)を使います。  
`config/target.exs`に設定します。  
wifiの設定は"wlan0"の箇所をデフォルトの状態から書き換えます。


```elixir:config/target.exs
config :vintage_net,
  regulatory_domain: "US",
  config: [
    {"usb0", %{type: VintageNetDirect}},
    {"eth0",
     %{
       type: VintageNetEthernet,
       ipv4: %{method: :dhcp}
     }},
    {"wlan0",
      %{type: VintageNetWiFi,
        vintage_net_wifi: %{
          networks: [
            %{ key_mgmt: :wpa_psk,
               ssid: System.get_env("MY_NERVES_SSID"),
               psk: System.get_env("MY_NERVES_PSK")
            }
          ]
        },
        ipv4: %{method: :dhcp}
      }}
  ]
```

wifiのssid, psk を環境変数に設定してから渡します。各自の状況に合わせて設定します。  
ソースコードをgithubなどの公の場所におく場合に、パスワード類をコードに記載することを避けるためです。  
環境変数名は他の変数とかぶらない限り、コードと一致していれば何でもいいです。

```sh
$ export MY_NERVES_SSID=*****
$ export MY_NERVES_PSK=+++++
```

## ssh鍵の作成
ssh鍵を作成し、公開鍵のファイル名をコードに記載します。

```sh
$ ssh-keygen -t ed25519 -f ~/.ssh/id_nerves_key -N ""
:
# ~/.ssh下に id_nerves_key(秘密鍵)と id_nerves_key.pub(公開鍵)が作成される。
```

作成されたssh公開鍵を`config/target.exs`の`keys=[...`の箇所に、他の例に従って記載します。

```elixir:config/target.exs
keys =
  [
    Path.join([System.user_home!(), ".ssh", "id_nerves_key.pub"]),  # <-- 追加
    Path.join([System.user_home!(), ".ssh", "id_rsa.pub"]),
    :
  ]
```

## `target`環境変数の設定

私の場合はtarget機に`rpi0`を使いますので、以下の設定にします。<br/>
他のtarget機器を使用する場合は[こちら](https://hexdocs.pm/nerves/targets.html)に従って設定します。

```sh
$ export MIX_TARGET=rpi0
```

## `mix deps.get`, `mix firmware`
コンパイルしていきます。  
`mix deps.get`はdepsに依存するライブラリの取得をします。
先ほど環境変数を設定したシェルと同じシェルで実施する必要があります。  
違うシェルでは環境変数が反映されません。  


```elixir
$ mix deps.get  # ライブラリの読み込みなど
$ mix firmware # コンパイル
```

## microSDカードの準備

microSDカードのリードライターを接続する前後で`lsblk -a | grep -v loop`コマンドの結果の差異と、microSDカードの容量の情報から、microSDカードのデバイスファイル名を確認しておきます。恐らく`/dev/sdb`とか`/dev/sdc`などになると思います。私の場合は、`/dev/sdd`でした。  
間違えると母艦機のシステムを壊してしまう可能性があるので、十分注意します。  

```sh
$ lsblk -a | grep -v loop
```

## `mix burn`

microSDカードへの書き込みです。  

```sh
$ mix burn
```

前項で確認したデバイスファイル名を聞かれるので、合っていることを確認してenterで進めます。  
自分のlinux上でのログインパスワードを聞かれるので、入力します。  
パスワードが合っていれば、すぐにmicroSDカードへの書き込みが始まり、5秒間くらいで終了します。

## RaspberryPIの起動

microSDカードをリーダーライターから抜き、RaspberryPI0に挿入します。  
microUSBポートに電源ケーブルを接続して、5V電源を供給します。rpi0なら1Aくらいの供給能力があれば十分かと思います。電源を供給すると電源の緑色LEDの2度点滅を繰り返します。  

## 母艦機からのssh接続

RaspberryPIを起動してから約30秒間してから、pingでアクセス可否を確認します。

```sh
$ ping -c 3 nerves.local
```

RaspberryPI起動からの時間が短いと、名前解決エラーが出ます。さらに10秒間くらい待ってから再度確認してください。  

```sh:ping エラー例
PING nerves.local (***.***.***.***) 56(84) byes of data.
(ここでしばらく止まって)
--- nerves.local ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time ****ms
```

pingから3回とも応答があり、packet lossが0であればアクセス成功です。 
```sh:ping 成功例
PING nerves.local (***.***.***.***) 56(84) byes of data.
64 bytes from nerves-**** (***.***.***.***): icmp_seq=1 ttl=64 time=8.87 ms
64 bytes from nerves-**** (***.***.***.***): icmp_seq=2 ttl=64 time=9.91 ms
64 bytes from nerves-**** (***.***.***.***): icmp_seq=3 ttl=64 time=21.3 ms

--- nerves.local ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 8.871/13.356/21.291/5.626 ms
```

私の場合は`nerves.local`では名前解決できませんでした。あるあるのようです。[^2][^3]

```
$ ping -c 3 nerves.local
ping: nerves.local: Name or service not known
```

なので、別のパソコンからルーターにログインして、ホスト名が "nerves-****" というものを探しました。
****は英数4桁のようです。

[^2]: [Nerves講義ノート・始めてみる回](https://hotch-potch.hatenadiary.jp/entry/2020/07/20/233627)の、3(6)通信状態の確認の項を参照。
[^3]: [高瀬先生の記事:ElixirでIoT#4.3：Nervesアプリ開発時のよくあるトラブルをシューティング  nerves.localが見つからない](https://qiita.com/takasehideki/items/5c79f5531fa189be99f0#nerveslocal%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84)


次にssh接続します。

```sh
$ ssh nerves-****
```

するとパスワードを延々と聞かれ続ける状態になりました。

```
SSH server
Enter password for "user"
password: 
```

高瀬先生[^14]の記事を参考にして、以下のように`~/.ssh/config`に設定を追加すると、うまく接続できるようになりました。[^20]  
あとの`mix upload`するときも`nerves.local`としてアクセスするので、`nerves.local`でアクセスできるようにしておくと後々楽だと思います。[^21]

[^14]: [高瀬先生の記事:ElixirでIoT#4.3：Nervesアプリ開発時のよくあるトラブルをシューティング ssh接続できない](https://qiita.com/takasehideki/items/5c79f5531fa189be99f0#ssh%E6%8E%A5%E7%B6%9A%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84)

[^20]: ssh接続時にパスワードを延々と聞かれるのは、ssh接続時の秘密鍵がどれかわからないためと思われます。ssh接続時の秘密鍵を明示すればうまくいくのかと。

[^21]: ~/.ssh/configへの設定記載がなくても、`mix firmware`,`mix upload`コマンドにて、`mix firmware nerves-****`, `mix upload nerves-****`などと記述して実行できるようです。

```sh:~/.ssh/config
Host nerves.local
  Hostname nerves-****
  StrictHostKeyChecking no
  IdentityFile ~/.ssh/id_nerves_key
```

ssh接続が成功すれば、nervesのロゴが出てきてiexのプロンプト待機になります。  

```sh
$ ssh nerves.local
Interactive Elixir (1.14.0) - press Ctrl+C to exit (type h() ENTER for help)
████▄▄    ▐███
█▌  ▀▀██▄▄  ▐█
█▌  ▄▄  ▀▀  ▐█   N  E  R  V  E  S
█▌  ▀▀██▄▄  ▐█
███▌    ▀▀████
hellonerves 0.1.0 (.....................................) arm rpi0
  Uptime       : 1 minutes and 12 seconds
  Clock        : 2022-09-23 06:56:09 UTC

  Firmware     : Valid (B)               Applications : 36 started
  Memory usage : 41 MB (13%)             Part usage   : 0 MB (0%)
  Hostname     : nerves-****             Load average : 0.34 0.13 0.04

  wlan0        : **.**.**.**/**, **:**:**:**:**:**:**:**/**, **::**:**:**:**/**
  usb0         : **.**.**.**/**

Nerves CLI help: https://hexdocs.pm/nerves/using-the-cli.html

Toolshed imported. Run h(Toolshed) for more info.
iex(1) >
```

ssh接続に失敗する場合は、sshの鍵の作成に失敗している可能性があるので、そこからやり直す必要があります。  
元々準備されているHellonerves.hello()関数を呼び出して、worldアトムが返ることを確認します。  

```elixir
iex> Hellonerves.hello()
:world
```

`exit`と入力して、一旦ssh接続を終了してシェルに戻ります。

```sh
iex> exit
Connection to nerves.local closed.
$
```

## コードを書き換えて`mix upload`

何でもいいのですが、`Hellonerves.hello()`関数をちょこっと書き換えます。

```elixir:lib/hello_nerves.ex
defmodule Hellonerves do
  def hello do
    "Hello Nerves!"
  end
end
```

`mix firmware`でコンパイルします。  
以降で`mix deps.get`, `mix firmware`, `mix upload`を実行するときは、必ず環境変数(MIX_TARGET, wifiのssid/psk)が設定されている必要があります。

```sh
$ mix firmware
```
microSDカードをRaspberryPIに挿したまま、`mix upload`コマンドでファームウェアを書き換えます。  
`mix upload`の場合は`mix burn`に比べて書き込みスピードが少し遅いようです。

```sh
$ mix upload
```

`mix upload`から再び約30秒間待ってからssh接続して、`Hellonerves.hello()`コマンドを実行して、  
書き換えたところが反映されていることを確認します。

```sh
$ ssh nerves.local
:
iex> Hellonerves.hello()
"Hello Nerves!"
iex> exit
Connection to nerves.local closed.
$
```

## Lチカとスイッチ状態検出

参考資料1.のALGYANハンズオン基本編1.にあるデバイスを利用します。  
添付写真のように、LEDはD16に、プッシュスイッチはD5に接続されています。  
[circuits.GPIO](https://github.com/elixir-circuits/circuits_gpio)のライブラリを使います。  
`mix.exs`内のdepsに以下を追加します。

```
defp deps do
  [
    : 
    {        .....},   # <-- 前行の最後にコンマも入れてください
    {:circuits_gpio, "~> 0.4"}  # <-- 追加
  ]
end
```

`mix deps.get`で`circuits_gpio`のライブラリを取得してきます。  
`mix firmware`, `mix.upload`を実行し、再度ssh接続します。

```sh
$ mix deps.get
$ mix firmware
$ mix upload
$ ssh nerves.local
 :
iex>
```

LEDは16番に接続されています。  
GPIOのwriteコマンドの引数でLEDのon/offが制御できることを確認します。

```elixir
iex> {:ok, gpio} = Circuits.GPIIO.open(16, :output)
iex> Circuits.GPIO.write(gpio, 1)  # LED点灯
iex> Circuits.GPIO.write(gpio, 0)  # LED消灯
```

プッシュスイッチもやってみます。5番に接続しています。

```elixir
iex> {:ok, gpio} = Circuits.GPIO.open(5, :input)
iex> Circuits.GPIO.read(gpio)  # ボタンを押しながら実行すると1が返る
1
iex> Circuits.GPIO.read(gpio)  # ボタンを押さずに実行すると0が返る
0
iex> exit
$
```
## プッシュスイッチを押したときにLED点灯

ハンズオンの資料に基づいてやってみます。
以下のコードを追加します。  
ファイル名は`lib/`下にあれば何でもOKです。

```elixir:lib/buttonServer.ex
defmodule NervesjpBasis.ButtonServer do
  use GenServer

  require Logger

  alias Circuits.GPIO

  @button 5
  @led 16

  def start_link(_) do
    GenServer.start_link(__MODULE__, [])
  end

  def init(_) do
    {:ok, button} = GPIO.open(@button, :input, pull_mode: :pullup)
    GPIO.set_interrupts(button, :both)

    {:ok, led} = GPIO.open(@led, :output)

    {:ok, %{button: button, led: led}}
  end

  def handle_info({:circuits_gpio, @button, _timestamp, value}, state) do
    Logger.debug("Button is now #{value}")

    GPIO.write(state.led, value)

    {:noreply, state}
  end
end
```

`mix.deps.get`, `mix.firmware`, `mix upload`, `ssh nerves.local`を順に実行します。

```sh
iex> NervesjpBases.ButtonServer.start_link([])
{:ok, #PID<****.0>}
iex>
```
この状態でプッシュスイッチを押している間だけLEDが点灯することを確認します。

# UART経由でiexコンソールの表示

Nishiguchiさんの[Elixir/Nerves UARTでシリアルコンソール接続](https://qiita.com/mnishiguchi/items/dddbac0262bcff4dca23)をマネしてやってみました。

## UART-TTL変換ケーブルの入手

私は以下を入手しました。  
[Prolific PL2303使用品](https://item.rakuten.co.jp/zekei/e01_raspi_concable01/)  
単なるUSBケーブルの一端をばらしたモノかと錯覚しましたが、USB接続(typeA形状)のコネクタ部にICを含む回路が内蔵されているようです。  
私の環境はlinuxだったため?か、デバイスドライバのインストールは不要でした。デバイスファイル名は`/dev/ttyUSB0`として認識されました。`ls /dev/tty*`で確認。

## ケーブルの接続

前述のNishiguchiさんのqiita記事のリンクにある記事の内容に従って、変換ケーブルの3本をラズパイのGPIOピンに接続します。赤線(5V)のみ非接続です。

## エミュレータのインストール

私は`screen`を使用しました。

```sh
$ sudo apt install screen
```

## エミュレータの起動

```sh
$ screen /dev/ttyUSB0 115200
```

画面が真っ黒のままの場合は、enterを何度が押すとプロンプトが出現します。
文字化けする場合はボーレートの設定(上記コマンド中の115200の箇所)を入れてみるといいと思います。

エミュレータ端末を切断するときは、`ctrl-a + k`を押すと、kill screen?という確認が入るので、yを押下して終了すると、bashプロンプトに戻ります。  
`ctrl-a + k`の前に`exit`を入力してしまうと、UART接続のターミナルが終了してしまうようで、再度screenコマンドを叩いても端末表示はされなくなりました。  
NervesではUARTのデバイスファイルは、rpi0ではデフォルトで`/dev/ttyAMA0`と設定されるようです。その後screenコマンド実行後にNerves側で`/dev/ttyAMA0`-->`/dev/ttyUSB0`に自動接続されるのかな?と思っています。


# 参考資料

1. [ALGYAN Elixir/Nerves(ナーブス)体験ハンズオン！](https://algyan.connpass.com/event/197306/presentation/)
1. [ALGYAN x Seeed x NervesJPハンズオン！に向けた開発環境の準備方法(高瀬先生の記事)](https://qiita.com/takasehideki/items/79d4ba3f95b1463105f8)
1. [NervesのSSH環境を整えた　〜upload.shを使ってファームウェア更新も〜](https://qiita.com/nishiuchikazuma/items/6773d8f1d351d9355738) 
1. [nerves_pack（vintage_net含む）を使ってNervesのネットワーク設定をした〜SSHログインまで〜](https://qiita.com/nishiuchikazuma/items/01f5df5472ec0feceb5f)