LoginSignup
11
0

More than 1 year has passed since last update.

Nerves で GPS Loggerを作ってみた

Last updated at Posted at 2021-12-09

はじめに

この記事はNervesJP Advent Calendar 2021の10日目の記事です。9日目は @zacky1972 さんのErlang VMのprocess_flagのmessage_queue_dataのon_heapとoff_heapで軽く性能評価してみましたでした。

この記事はこちらを参考に
Nervesで自前で立てたGPS Logger Serverにデータを送信するGPS Logger Clientを実装していきます

準備するもの

  • Raspberry Pi + AC ケーブル
  • microSDカード
  • GPS module(秋月電子 AE-GYSFDMAXB)
  • ジャンパーワイヤ(メス-メス)
  • ハンダゴテ + 半田
  • GPS Logger server (動作確認用)

モジュール構成

  • DataFetcher -> UART待受
  • Transporter -> GPS data 送信
  • NMEA(GPS Log Parser) -> GPS信号をLat,Lngに形成

プロジェクト生成

mix nerves.new gps_logger
cd gps_logger

ネットワーク設定

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

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

大体1秒間隔で以下のようなデータを受信します
Image from Gyazo

 NMEA(GPS Log Parser)

次は受信したデータを latitude, longitudeの形式にパースしていきます
いくつか信号の種類があるのでこちらを参考にして実装します
位置情報が入ってる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します
2回目移行は
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

Image from Gyazo

データ解析部分が少し複雑ですが、それ以外は基本的な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/

11
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
0