Nerves で GPS Loggerを作ってみた

# はじめに
この記事は[NervesJP Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nervesjp)の10日目の記事です。9日目は @zacky1972 さんの[Erlang VMのprocess_flagのmessage_queue_dataのon_heapとoff_heapで軽く性能評価してみました](https://qiita.com/zacky1972/items/fc10dc46f241ab94f50a)でした。


この記事は[こちら](https://github.com/yannvery/gps_tracker)を参考に
Nervesで自前で立てたGPS Logger Serverにデータを送信するGPS Logger Clientを実装していきます

# 準備するもの
- Raspberry Pi + AC ケーブル
- microSDカード
- GPS module(秋月電子 AE-GYSFDMAXB)
- ジャンパーワイヤ(メス-メス)
- ハンダゴテ + 半田
- [GPS Logger server](https://qiita.com/the_haigo/items/9cb8d6446492c50d690c) (動作確認用)

# モジュール構成
- DataFetcher  -> UART待受
- Transporter  -> GPS data 送信
- NMEA(GPS Log Parser) -> GPS信号をLat,Lngに形成

# プロジェクト生成

```
mix nerves.new gps_logger
cd gps_logger
```

# ネットワーク設定

起動時にwifiにつなぐために設定します、LANケーブル刺す人は飛ばしておｋ

```config/target.exs
config :vintage_net,
  regulatory_domain: "US",
  config: [
    ...
    {"wlan0", %{
      type: VintageNetWiFi,
      vintage_net_wifi: %{
        networks: [
          %{
            key_mgmt: :wpa_psk,
            ssid: "your wifi ssid",
            psk: "your wifi password"
          }
        ]
      }
    }}
  ]
```

# DataFetcher
最初にデータ取得部分を実装していきます
まずUARTからデータを受信するCircuitUARTをsuperviserで起動します
またRegistryはelixirのインメモリKVSであり
via RegistryとすることでRegistryの"uart" keyを通してプログラム全体からアクセスできるようなります

```lib/gps_logger/application.ex
  def children(_target) do
    [
      {Registry,[keys: :unique, name: GpsLogger.Registry]},
      {Circuits.UART, [name: {:via, Registry, {GpsLogger.Registry, "uart"}}]},
      {GpsLogger.DataFetcher,[]}
    ]
  end
```
Nervesは基本的にGenServerを起動させていく感じです
start_linkでプロセスを起動
init関数で Circuits.UARTの設定と待受を開始します
UARTからのデータ受信がの以下の形式でくるのでマッチしたら受信時の関数を実行します
`{:circuits_uart, port, data}, state) `
データのパースとサーバーへの通信は未実装なのでとりあえず`inspect`だけしておきます

```lib/gps_logger/data_fetcher.ex
defmodule GpsLogger.DataFetcher do
  use GenServer
  @name __MODULE__

  @doc """
  Start the fetcher and open communication with GPS card.
  """
  def start_link(state \\ []) do
    GenServer.start_link(@name, state, name: @name)
  end

  @impl true
  def init(_state) do
    [{uart, nil}] = Registry.lookup(GpsLogger.Registry, "uart")
    Circuits.UART.configure(uart, framing: {Circuits.UART.Framing.Line, separator: "\r\n"})
    Circuits.UART.open(uart, "ttyAMA0", speed: 9600, active: true)

    {:ok, %{current_position: nil}}
  end

  @impl true
  def handle_info({:circuits_uart, port, data}, state) do
    receive_data({:circuits_uart, port, data}, state)
  end

  def receive_data({:circuits_uart, _port, data}, state) do
    IO.inspect(state)
    {:noreply, state}
  end
end
```
大体１秒間隔で以下のようなデータを受信します
[![Image from Gyazo](https://i.gyazo.com/835db295331c99368cc92510f519be25.gif)](https://gyazo.com/835db295331c99368cc92510f519be25)

#　NMEA(GPS Log Parser)
次は受信したデータを latitude, longitudeの形式にパースしていきます
いくつか信号の種類があるので[こちら](https://piyajk.com/archives/tag/gpgsa)を参考にして実装します
位置情報が入ってるGPGGAとGPRMCの信号をコンマでsplitしてparse_detaを実行して{time, lat, lng}を返します

ここはよくわからなかったので元コードをそのまま写しています

```lib/gps_logger/nmea.ex
defmodule GpsLogger.Nmea do
  def parse(data) do
    data
    |> String.split(",")
    |> to_gps_struct()
  end

  defp to_gps_struct([
      "$GPGGA", time, latitude, latitude_cardinal,
      longitude, longitude_cardinal, _type, _nb_satellites, _percision,
      _altitude,_altitude_unit, _, _, _, _sig
    ] = data) do
      parse_data(latitude, latitude_cardinal, longitude, longitude_cardinal, time, data)
  end

  defp to_gps_struct([
      "$GPRMC", time, _data_state, latitude, latitude_cardinal,
      longitude, longitude_cardinal, _speed, _, _, _, _, _sig
    ] = data) do
      parse_data(latitude, latitude_cardinal, longitude, longitude_cardinal, time, data)
  end

  defp to_gps_struct(data) do
    {:error, %{message: "can't parse data", data: Enum.join(data, ",")}}
  end

  defp parse_data(lat, lat_cardinal, lng, lng_cardinal, time, data) do
    with {:ok, latitude} <- to_degres("#{lat},#{lat_cardinal}"),
         {:ok, longitude} <- to_degres("#{lng},#{lng_cardinal}") do
      {:ok, %{time: time, lat: latitude, lng: longitude}}
    else
      {:error, %{message: "empty data"}} ->
        {:error, %{message: "empty data", data: Enum.join(data, ",")}}
      _ ->
        {:error, %{message: "can't parse data", data: Enum.join(data, ",")}}
    end
  end

  def to_degres(
    <<degres::bytes-size(2)>> <>
    <<minutes::bytes-size(7)>> <>
    <<_sep::bytes-size(1)>> <>
    <<cardinal::bytes-size(1)>>
  ) do
    {:ok, do_to_degres(degres, minutes, cardinal)}
  end

  def to_degres(
    <<degres::bytes-size(3)>> <>
    <<minutes::bytes-size(6)>> <>
    <<_sep::bytes-size(1)>> <>
    <<cardinal::bytes-size(1)>>
  ) do
    {:ok, do_to_degres(degres, minutes, cardinal)}
  end

  def to_degres(
    <<degres::bytes-size(3)>> <>
    <<minutes::bytes-size(7)>> <>
    <<_sep::bytes-size(1)>> <>
    <<cardinal::bytes-size(1)>>
  ) do
    {:ok, do_to_degres(degres, minutes, cardinal)}
  end

  def to_degres(",") do
    {:error, %{message: "empty data"}}
  end

  defp do_to_degres(degres, minutes, cardinal) do
    degres = degres |> float_parse()
    minutes = minutes |> float_parse()
    (degres + minutes / 60) |> Float.round(5) |> with_cardinal_orientation(cardinal)
  end

  defp with_cardinal_orientation(degres, cardinal) when cardinal in ["N", "E"] do
    degres
  end

  defp with_cardinal_orientation(degres, cardinal) when cardinal in ["S", "W"] do
    -degres
  end

  defp float_parse(value) do
    {value_parsed, _} = Float.parse(value)
    value_parsed
  end
end
```

# Transpondeur
Lat,Lngを受け取って位置情報が更新されているかをチェックして送信先URL(endpoint)にpostするモジュールです

```lib/gps_logger/application.ex
defmodule GpsLogger.Application do
...
  def children(_target) do
    [
      {Registry,[keys: :unique, name: GpsLogger.Registry]},
      {Circuits.UART, [name: {:via, Registry, {GpsLogger.Registry, "uart"}}]},
      {GpsLogger.Transpondeur, ["送信先local net ip:4000/api/points"]}, # <- 追加
      {GpsLogger.DataFetcher,[]}
    ]
  end
...
end
```

childrenで指定した送信先URLがstart_linkのendpointにはいります
初回は
`handle_cast({:emit, position}, state = %{endpoint: endpoint, current_position: nil})`
が呼ばれ
endpointにHTTPoison.postします
２回目移行は
`handle_cast({:emit, position}, state = %{endpoint: endpoint, current_position: current_position})`
が呼ばれ
GpsLogger.Distanceで更新前の位置との差分を取って5m以上移動していたら HTTPoision.postします

```lib/gps_logger/transpondeur.ex
defmodule GpsLogger.Transpondeur do
  use GenServer

  @name __MODULE__

  def start_link(endpoint) do
    GenServer.start_link(@name, endpoint, name: @name)
  end

  def emit(coordinates) do
    GenServer.cast(@name, {:emit, coordinates})
  end

  @impl true
  def init(endpoint) do
    {:ok, %{endpoint: endpoint, current_position: nil}}
  end

  @impl true
  def handle_cast({:emit, position}, state = %{endpoint: endpoint, current_position: nil}) do
    post_to(endpoint, position)

    {:noreply, %{state | current_position: position}}
  end

  def handle_cast({:emit, position}, state = %{endpoint: endpoint, current_position: current_position}) do
    with true <- position_issued_after?(position, current_position),
      {:ok, distance} <- GpsLogger.Distance.compute(position, current_position),
      {:ok, distance_in_meters} <- GpsLogger.Distance.to_meters(distance),
      true <- distance_in_meters > 5.0
    do
      post_to(endpoint, position)
      {:noreply, %{state | current_position: position }}
    else
      _ ->
        {:noreply, state}
    end
  end

  def position_issued_after?(position, current_position) do
    with {position_time, ""} <- Float.parse(Map.get(position, :time, "0")),
         {current_position_time, ""} <- Float.parse(Map.get(current_position, :time, "0"))
    do
      position_time > current_position_time
    end
  end

  defp post_to(endpoint, position) do
    {:ok, json} = position |> Jason.encode()
    HTTPoison.post(endpoint, json, %{"Content-Type": "application/json"})
  end
end
```

```lib/gps_logger/distance.ex
defmodule GpsLogger.Distance do
  def compute(%{longitude: x1, latitude: y1}, %{longitude: x2, latitude: y2}) do
    distance =
      (:math.pow(x1 - x2, 2) + :math.pow(y1 - y2, 2))
      |> :math.sqrt()
      |> Float.ceil(5)
    {:ok, distance}
  end

  def compute(_pos1, _pos2), do: :error

  def to_meters(distance) do
    distance_in_meters = (distance * 111_319.0) |> Float.ceil(5)

    {:ok, distance_in_meters}
  end
end
```
データ送信部分ができたのでデータ取得時にGpsLogger.Transpondeur.emitでサーバーにデータ送信を実行するようにします

```lib/gps_logger/data_fetcher.ex
defmodule GpsLogger.DataFetcher do
...
  def receive_data({:circuits_uart, _port, data}, state) do
    state =
      case GpsLogger.Nmea.parse(data) do
        {:ok, position} ->
          GpsLogger.Transpondeur.emit(position)
          %{current_position: position}
        _ ->
          state
      end
    {:noreply, state}
  end
end
```

コードができたので実際にSDカードに焼きましょう

```
export MIX_TARGET=rpi0
mix firmware
mix firmware.burn !
```
最終的にできたのがこちらになります
![IMG_5444.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/adab401a-a9f5-f268-0666-b176d6091bf2.jpeg)

[![Image from Gyazo](https://i.gyazo.com/667e5e539da06d341cf99f2266da3a80.gif)](https://gyazo.com/667e5e539da06d341cf99f2266da3a80)

データ解析部分が少し複雑ですが、それ以外は基本的なGenServerなアプリケーションになったかと思います

認証周りも含めたコードがこちらになります
https://github.com/thehaigo/live_map_nerves

明日は @nishiuchikazuma さんの Nervesにユーザ名／パスワードでSSHログインする です

# 参考ページ
https://hexdocs.pm/nerves/installation.html#content
https://hexdocs.pm/circuits_uart/readme.html
https://hexdocs.pm/httpoison/HTTPoison.html
https://github.com/yannvery/gps_tracker
https://qiita.com/mnishiguchi/items/f4668697cb371ea6bb39
https://qiita.com/sand/items/5fd91c0b86b4919d7cc9
https://qiita.com/takasehideki/items/e7cc1a2d0a4a7140c1cf
https://qiita.com/kentaro/items/e8df79aa93b9fe9a567e
https://piyajk.com/archives/tag/gpgsa
https://blog.tubone-project24.xyz/2020/1/24/elixir-loadtest
https://akizukidenshi.com/catalog/g/gK-09991/