導入
自宅のRaspberry Pi 4で動かしているNervesアプリが、10秒に1度、温度・湿度を送信しています。
それが8日間、誰にも受け取られずに黙々と送り続けていました。
Docker composeで動かしているPhoenix APIが止まっていたため、受け取ってくれる相手はいなかったはずなのです。(おそらくWindows Update)
Phoenix APIを再開すると、Nervesアプリは何事もなかったようにデータを送り続けていました。
…健気でした。学ばされました。
Grafanaのグラフ
Grafanaのグラフを示します。
6/21からの8日間ほど、線になっている部分がPhoenix APIサーバのほうが止まっていた期間です。
システム構成
システム構成を示します。
Raspberry Pi 4には、「ナウでヤングなcoolなすごいヤツ」であるElixir製のIoTフレームワークNervesで作ったアプリケーションが動いています。AHT20から取得した温度・湿度のデータを10秒に一回、Phoenixで作ったAPIサーバに送っています。Phoenixは、Elixir製のWebアプリケーションフレームワークです。
Windowsでは、Docker composeを使ってPhoenixアプリコンテナ、PostgreSQL(timescaledb)コンテナ、グラフ表示のGrafanaコンテナが動いています。
技術的ポイント
技術的ポイントをいくつか説明します。
Nervesの定期送信の仕組み
GenServerで定期送信をしています。
defmodule Publisher do
use GenServer
require Logger
def start_link(options \\ %{}) do
GenServer.start_link(__MODULE__, options, name: __MODULE__)
end
@impl true
def init(options) do
state = %{
interval: options[:interval] || 10_000,
aht20_tracker_url: options[:aht20_tracker_url],
measurements: :no_measurements
}
schedule_next_publish(state.interval)
{:ok, state}
end
defp schedule_next_publish(interval) do
Process.send_after(self(), :publish_data, interval)
end
@impl true
def handle_info(:publish_data, state) do
{:noreply, state |> measure() |> publish()}
end
defp measure(state) do
AHT20.read()
|> measure(state)
end
defp measure({:ok, measurements}, state) do
Map.merge(state, %{measurements: measurements})
end
defp measure(_, state) do
state
end
defp publish(state) do
result =
Req.post(state.aht20_tracker_url, json: state.measurements)
Logger.debug("Server response: #{inspect(result)}")
schedule_next_publish(state.interval)
state
end
end
AHT20からの温度・湿度データの読み込み
AHT20からの温度・湿度データの読み込みは次のプログラムで行っています。
defmodule AHT20 do
@i2c_bus "i2c-1"
@i2c_addr 0x38
@initialization_command <<0xBE, 0x08, 0x00>>
@trigger_measurement_command <<0xAC, 0x33, 0x00>>
@two_pow_20 2 ** 20
def read do
{:ok, ref} = Circuits.I2C.open(@i2c_bus)
Circuits.I2C.write(ref, @i2c_addr, @initialization_command)
Process.sleep(10)
Circuits.I2C.write(ref, @i2c_addr, @trigger_measurement_command)
Process.sleep(80)
result =
Circuits.I2C.read(ref, @i2c_addr, 7)
|> convert()
Circuits.I2C.close(ref)
result
end
def convert({:ok, <<_state, raw_humidity::20, raw_temperature::20, _crc>>}) do
humidity = (raw_humidity / @two_pow_20 * 100.0) |> Float.round(1)
temperature =
(raw_temperature / @two_pow_20 * 200.0 - 50.0)
|> Float.round(1)
{:ok, %{humidity: humidity, temperature: temperature}}
end
def convert({:error, error}), do: {:error, error}
def convert(_), do: {:error, "An error occurred"}
end
気づき・教訓
人も、サービスも、組織も、「受け手がいない時間」に何をしているかが重要なのかもしれない。
Nervesアプリは、文句も言わず、ただやるべきことをやり続けていました。
自分も腐らず、たゆまず、積み重ねていこうと思いました。
ただQiitaへの記事投稿(闘魂)を続けていこうと思いました。