はじめに
ある日、AtomVM で動かすファームウェアを友人と開発している中で、「同じ基板だけど、LCD と SD の CS ピンだけ配線が違う」という 2 種類のバージョンをサポートする必要が出てきました。
- ファームウェア自体は共通
- ただし、あるバージョンまでは「旧配線」、それ以降は「新配線」
そこで、
-
PIYOPIYO_BOARDという環境変数に基板のバージョン番号を渡す -
config/config.exsの中でバージョン文字列を解析する - バージョンに応じて CS ピンの組み合わせを切り替える
という方針をとりました。
その際に使ったのが、Elixir 標準の Version モジュール です。
やりたいこと
今回の要件をもう少し整理すると、やりたいことは次の 3 点です。
- 環境変数
PIYOPIYO_BOARDにバージョン文字列(例:v1.5,1.6)を渡す -
config/config.exsでその文字列を 正規化してVersionに変換する - 「1.5 以下なら旧配線」「1.6 以上なら新配線」のようにバージョンで分岐する
ここで活躍するのが Version.parse/1, Version.parse!/1, Version.compare/2 です。
Version モジュール
Elixir にはバージョン文字列を扱うための Version モジュールが用意されています。
-
Version.parse!/1でバージョン文字列を構造体に変換 -
Version.compare/2で 2 つのバージョンを比較 -
Version.match?/2で「バージョン要件」を満たしているかどうかを判定 (nerves_motd の例)
Version モジュールは「セマンティックバージョニング」に沿った形式を前提にしており、バージョン文字列の形式にけっこう厳格です。
iex> Version.parse("1.6.0")
{:ok, %Version{major: 1, minor: 6, patch: 0}}
iex> Version.parse("1.6")
:error
"1.6" のような文字列は、そのままだと弾かれてしまいます。
一方で、今回想定している設定値はこんな感じです。
"v1.5""v1.6""1.5""1.6.1"
今回のように「環境変数に自由な文字列が入ってくる」ケースでは、そのまま Version に渡すのではなく、いったん前処理して正規化する必要があります。
そこで、以下のような前処理を入れてから、Version.parse!/1 に渡すことにしました。
- 先頭の
"v"を取り除く -
"."で分割する - 足りない部分を
"0"で補って"major.minor.patch"にそろえる
環境変数からバージョンを正規化する
まずは「バージョン文字列を正規化して Version 構造体に変換する」部分だけを抜き出した例です。
board = System.get_env("PIYOPIYO_BOARD") || "v1.6"
raise_invalid_board = fn parts ->
raise """
Unsupported PIYOPIYO_BOARD=#{inspect(board)} (parsed parts: #{inspect(parts)}).
Expected a version-like value such as:
* "v1.5"
* "v1.6"
* "1.5"
* "1.6.1"
"""
end
version_segments =
board
|> String.trim_leading("v")
|> String.split(".")
|> case do
[maj] -> [maj, "0", "0"]
[maj, min] -> [maj, min, "0"]
[maj, min, patch] -> [maj, min, patch]
parts -> raise_invalid_board.(parts)
end
unless Enum.all?(version_segments, &String.match?(&1, ~r/^\d+$/)) do
raise_invalid_board.(version_segments)
end
version =
version_segments
|> Enum.join(".")
|> Version.parse!()
ここまでで、
-
PIYOPIYO_BOARDが"v1.5"でも"1.5"でも"1.5.0"でも - 最終的には
Version.parse!("1.5.0")と同じ%Version{}に正規化
できるようになります。
バージョンで配線を切り替える
あとは Version.compare/2 を使って、基板のバージョンに応じて CS ピンの組み合わせを切り替えます。
今回のルールは次の通りです。
- v1.5 以下 → 旧配線(LCD CS: 43, SD CS: 4)
- v1.6 以上 → 新配線(LCD CS: 4, SD CS: 43)
実際の config/config.exs はこんな感じになりました。
import Config
board = System.get_env("PIYOPIYO_BOARD") || "v1.6"
raise_invalid_board = fn parts ->
raise """
Unsupported PIYOPIYO_BOARD=#{inspect(board)} (parsed parts: #{inspect(parts)}).
Expected a version-like value such as:
* "v1.5"
* "v1.6"
* "1.5"
* "1.6.1"
"""
end
version_segments =
board
|> String.trim_leading("v")
|> String.split(".")
|> case do
[maj] -> [maj, "0", "0"]
[maj, min] -> [maj, min, "0"]
[maj, min, patch] -> [maj, min, patch]
parts -> raise_invalid_board.(parts)
end
unless Enum.all?(version_segments, &String.match?(&1, ~r/^\d+$/)) do
raise_invalid_board.(version_segments)
end
version =
version_segments
|> Enum.join(".")
|> Version.parse!()
{lcd_cs_pin, sd_cs_pin} =
case Version.compare(version, Version.parse!("1.6.0")) do
# v1.5 or lower
:lt -> {43, 4}
# v1.6 or higher
_ -> {4, 43}
end
spi_config = [
bus_config: [sclk: 7, miso: 8, mosi: 9],
device_config: [
spi_dev_lcd: [
cs: lcd_cs_pin,
mode: 0,
clock_speed_hz: 20_000_000,
command_len_bits: 0,
address_len_bits: 0
],
spi_dev_touch: [
cs: 44,
mode: 0,
clock_speed_hz: 1_000_000,
command_len_bits: 0,
address_len_bits: 0
]
]
]
config :sample_app,
board: board,
spi_config: spi_config,
sd_cs_pin: sd_cs_pin
これで、ビルド時に次のように環境変数を変えるだけで配線を切り替えられます。
# 旧配線 (v1.5 相当)
export PIYOPIYO_BOARD=v1.5
# 新配線 (v1.6 以降)
export PIYOPIYO_BOARD=v1.6
おわりに
Elixir の Version モジュールは、ライブラリのバージョン管理だけでなく、今回のように「それっぽいバージョン文字列を受け取って分岐する」小さな用途でも便利に使えます。
- 文字列のまま大小比較するよりもロジックが明快になる
- 正規化の過程で、不正な値は早めに検出できる
- 「v1.5 以下」「v1.6 以上」といった表現がコード上でもそのまま表現できる
環境変数で設定を切り替える場面は多いので、ちょっとした分岐でも Version を思い出すと、後から読みやすい設定になってくれそうだと感じました。
