作って学ぶNerves、BBBでCO2計測

この記事は、「[#NervesJP Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nervesjp)」の8日目です。

 昨日は[torifukukaiou](https://qiita.com/torifukukaiou)さんの「[Nervesでtarget(Raspberry Pi等)で動かすときはこっち、host(macOS等)で動かすときはそっち](https://qiita.com/torifukukaiou/items/d26c9ffb051dd3df82ba)」でした。


今回はElixirを使って組み込みシステムを作ることができるNervesを使い、
CO2計測ができたのでそれを紹介します。

「[Nerves](https://nerves-project.org/)」とその「[インストール](https://hexdocs.pm/nerves/installation.html)」については、ドキュメントが豊富なのでここでは扱いません。

# アウトライン

以下の順に紹介していきます。

- Nervesプロジェクトの作成からBBBでの起動
- targetをhostから更新する
- CO2の計測をする
- 自動起動するようにする

# プロジェクトの作成からBBBでの起動

## プロジェクトの作成

~~~ bash
$ mix nerves.new [project name] # 以下 プロジェクト名はhello_nervesとします
* creating hello_nerves/config/config.exs
* creating hello_nerves/config/target.exs
* creating hello_nerves/lib/hello_nerves.ex
* creating hello_nerves/lib/hello_nerves/application.ex
* creating hello_nerves/test/test_helper.exs
* creating hello_nerves/test/hello_nerves_test.exs
* creating hello_nerves/rel/vm.args.eex
* creating hello_nerves/rootfs_overlay/etc/iex.exs
* creating hello_nerves/.gitignore
* creating hello_nerves/.formatter.exs
* creating hello_nerves/mix.exs
* creating hello_nerves/README.md

Fetch and install dependencies? [Yn] # Enter
* running mix deps.get
Your Nerves project was created successfully.
# 以下略
$ cd hello_nerves
~~~

mix newと 変わらないですね。

## BBBに必要なライブラリの取得

~~~ bash
$ export MIX_TARGET=bbb # 環境変数MIX_TARGETにbbbを設定し
$ 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が動作する端末にはSSH接続ができます。
その際に使用するSSHの鍵の設定がこの時点で必要なようです。

### SSHの鍵を設定

上記ではconfig.exsと書かれていますが、config.exsでimportしているtarget.exsに設定があるので書き換えます。

~~~ bash
$ vim config/target.exs
# 略
# 公開鍵の設置ディレクトリと名前を作成した鍵に合わせて設定します。
keys =
  [
    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)
# 略
~~~

設定できたらもう一度

~~~ bash
$ mix deps.get
~~~

## firm作成とSDカードへの書き込み

**※書き込み先を間違えるとOS逝っちゃうので気をつけて！！**

~~~ bash
$ mix firmware # 作成
$ mix firmware.burn --device /dev/sdd # 書込、デバイス名は自分の環境に合わせて慎重に選択！！
~~~

## BBBでの起動

SDをBBBに挿し、SDカードから起動するためにS2ボタンを押しながらUSBケーブルを投入します。
右上のLEDが光りだしたら、S2ボタンから手を離して大丈夫です。

このときの接続構成は以下です。

~~~
|bbb(target)|<--USBケーブル-->|PC(host)| 
~~~

<img width=50% alt=IMG_2203.JPG src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369713/90449dd7-64f3-41a0-5d47-dd36ecf191be.jpeg>

正常に起動できるとpingに応答を返すので、打って待機します。

~~~ bash
$ ping nerves.local # デフォルト設定でmDNSが動作しているので、左記で応答を返します。
~~~

応答が返ってきたら、SSH接続してみます。

~~~ bash
$ ssh nerves.local
Interactive Elixir (1.9.1) - 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(hello_nerves@nerves.local)1> 
~~~

ばっちりです。

# Toolshedについて

~~~ bash
Toolshed imported. Run h(Toolshed) for more info
~~~

SSH接続した際に、上の一行が表示されていました。
このToolshedを紹介します。

~~~ elixir
iex(hello_nerves@nerves.local)1> h Toolshed

                                    Toolshed                                    

Making the IEx console friendlier one command at a time

To use the helpers, run:

    iex> use Toolshed

Add this to your .iex.exs to load automatically.

The following is a list of helpers:

  • cat/1          - print out a file
  • cmd/1          - run a system command and print the output
  • date/0         - print out the current date and time
  • dmesg/0        - print kernel messages (Nerves-only)
  • exit/0         - exit out of an IEx session
  • fw_validate/0  - marks the current image as valid (check Nerves system
    if supported)
  • grep/2         - print out lines that match a regular expression
  • hex/1          - print a number as hex
  • hostname/0     - print our hostname
  • ifconfig/0     - print info on network interfaces
  • load_term!/2   - load a term that was saved by save_term/2
  • lsof/0         - print out open file handles by OS process
  • lsmod/0        - print out what kernel modules have been loaded
    (Nerves-only)
  • lsusb/0        - print info on USB devices
  • nslookup/1     - query DNS to find an IP address
  • pastebin/1     - post text to a pastebin server (requires networking)
  • ping/2         - ping a remote host (but use TCP instead of ICMP)
  • qr_encode/1    - create a QR code (requires networking)
  • reboot/0       - reboots gracefully (Nerves-only)
  • reboot!/0      - reboots immediately  (Nerves-only)
  • save_value/2   - save a value to a file as Elixir terms (uses inspect)
  • save_term!/2   - save a term as a binary
  • top/2          - list out the top processes
  • tping/2        - check if a host can be reached (like ping, but uses
    TCP)
  • tree/1         - pretty print a directory tree
  • uptime/0       - print out the current Erlang VM uptime
  • uname/0        - print information about the running system
    (Nerves-only)
  • weather/0      - get the local weather (requires networking)
~~~

linuxで見覚えのあるコマンドが並んでいます。
Toolshedはちょっとした便利コマンドの集まりのようです。
通信の確認をしたり、targetの構成を調べたりできそうです。

# targetをhostから更新する

~~~
|bbb(target)|<--USBケーブル-->|PC(host)| 
~~~

mix firmware.burnで毎回SDに書き込みを行うのは手間なので、
Nervesにはtargetをhostから更新する手段が提供されています。

~~~ bash
$ mix firmware.gen.script # 実行によりupload.shが作成されます
$ ./upload.sh nerves.local # upload
$ mix firmware && ./upload.sh nerves.local # firmwareの作成とuploadを一度にする
~~~

これにより、PCでmix firmware.burnをする必要がなくなり、
BBBにSDを挿したままfirmwareを更新できるようになります。

**参考**

- https://elixirschool.com/ja/lessons/advanced/nerves/
- https://hexdocs.pm/nerves_firmware_ssh/readme.html#pushing-firmware-updates-to-devices

# CO2の計測

firmwareの更新が楽になったので、開発が進められます。
今回使用したCO2センサは[S-300L-3V CO₂センサモジュール](https://www.switch-science.com/catalog/3685/)です。

出力のI/Fはアナログ、PWM、TTL-UART、I2Cがありますが、I2Cで計測します。

センサーのI/F仕様書はスイッチサイエンスの同じページにあります。
[ELT社 | Programming Guide I2C Bus](http://doc.switch-science.com/datasheets/ELT/Programming+Guide+for+I2C+cdg+ver+160714.pdf)

開発は以下の順で進めました。

1. I2C通信ライブラリの調査
1. 手動計測
1. 自動計測開始

## I2C通信ライブラリ

「[Elixir Circuits](https://elixir-circuits.github.io/)」というライブラリ群があります。
この中に「[circuits_i2c](https://hexdocs.pm/circuits_i2c/readme.html)」があります。これを使います。

以下を追加します。

~~~ mix.exs
  defp deps do
    [
  ...
      # I2C
      {:circuits_i2c, "~> 0.1"}
  ...
    ]
  end
~~~

以下を忘れずに実行。

~~~ bash
$ mix deps.get
$ mix firmware && ./upload.sh nerves.local
~~~

## 手動計測

ブレッドボードを使い回路を組みます。
バスはI2C-2を使います。
※BBBのピン配は[こちら](https://beagleboard.org/Support/bone101/#headers)
※SCL, SDAは内蔵プルアップが機能しているようでした。

<img width=50% alt=IMG_2204.JPG src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369713/ab96885c-920a-c53d-d2e8-2b135f8fd084.jpeg>

I/F仕様書をみつつ、やってみると何か値が取れました。

~~~ elixir
iex(hello_nerves@nerves.local)1> {:ok, ref} = Circuits.I2C.open("i2c-2")
{:ok, #Reference<0.1331158910.268828677.48092>}
iex(hello_nerves@nerves.local)2> Circuits.I2C.write(ref, 0x31, <<0x52>>)
:ok
iex(hello_nerves@nerves.local)3> Circuits.I2C.read(ref, 0x31, 7)
{:ok, <<8, 3, 51, 0, 3, 0, 1>>}
~~~

3*256+51=819[ppm]です。大気がおよそ400ppmなので活動分増えてるのかな。
手動計測ができたので、上記を関数化してエラーの取り回しをしてやればよさそうです。

## 自動計測開始

ここは説明するには長くなってしまうので割愛します。
Application、Supervisor、GenServerの連携のしくみを理解する必要がありました。:wink:
※ほぼこの理解をするために時間を使ったと言っても過言でないです:sweat_smile:

![Screenshot from 2019-11-26 14-21-24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369713/e49d49fc-1728-cadd-5878-6acf65fbb866.png)

最終成果物は [https://github.com/pojiro/co2_measurement_by_nerves](https://github.com/pojiro/co2_measurement_by_nerves)

# まとめ

開発環境構築の説明がメインになってしまいましたが、
Nervesを使いBBBでI2C通信によりCO2計測ができました。

もっと真面目にやろうとすると、以下などを詰めていく必要があると思います。

- 複数のI2Cセンサを使う場合はI2Cバスの排他
- 計測失敗からの復帰
    - 落ちればSupervisorに起動し直してもらえますが、落ちずにハングする場合があるのでそれへの対応

取得した値をPhoenixへPOSTしWEBブラウザで見られるようにできたら、
WEBからIoT端末までElixirで統一できてかっちょいいですね。
お正月の休みにElixirで環境センサつくってみるのはどうでしょうか？

明日、9日は[nishiuchikazuma](https://qiita.com/nishiuchikazuma)さんの「[ElixirでラズパイのLEDをチカ〜RaspbianOSインストールから〜](https://qiita.com/nishiuchikazuma/items/cce6152430467cffba56)」です。

それではまた！

**「いいね」よろしくお願いします。** :wink:

# おまけ

## シリアル接続について

### 仮想シリアル

BBBはUSBの仮想シリアルがあるので、
screenコマンドやtera termでシリアル接続ができます。

~~~ bash
$ screen /dev/ttyACM0 115200 # デバイス名は自分の環境に合わせて読み替えて
# エンターを押下して以下が出ればOK
iex(hello_nerves@nerves.local)3>
~~~

### シリアル

FTDIのUSBシリアルケーブルを使って接続することも可能です。
firmwareの転送ごとにscreenをつなぎ直す必要がないのでこれを使うと便利です。
Kernelの起動ログもみれます。

> 1. Download the default erlinit.config file from [the system repository](https://github.com/nerves-project/nerves_system_bbb/blob/master/rootfs_overlay/etc/erlinit.config) for your target.
> 2. Place it in your project folder under rootfs_overlay/etc/erlinit.config.
> 3. Modify the -c console setting to match the value shown in the UART row of the hardware description table (rpi3 example shown):

~~~ rootfs_overlay/etc/erlinit.config
# Specify where erlinit should send the IEx prompt. Only one may be enabled at
# a time.
#-c ttyGS0      # Virtual serial port over the USB interface
-c ttyS0   # Debug UART connector
#-c tty1      # HDMI output
~~~

#### 参考

- https://hexdocs.pm/nerves/faq.html#using-a-usb-serial-console

## eth0の使用について

以下のように修正することでeth0経由で通信可能になります。
※usb経由での通信はできなくなります
※起動してからIP取得に多少時間がかかるようです

~~~ config/target.exs
config :nerves_init_gadget,
  ifname: "eth0",
  address_method: :dhcp, # :dhcpdでなくて:dhcp！
  mdns_domain: "nerves.local",
  node_name: node_name,
  node_host: :mdns_domain
~~~

#### 参考

- https://hexdocs.pm/nerves/getting-started.html#rpi3b-b-wired-and-wireless-ethernet-connections