はじめに
この記事は Elixir Advent Calendar 2024 の 12/24 の記事です。
昨日の記事では、ArtNet のパケットを Elixir でパースする処理を紹介しました。
今日は、ArtNet をパースするライブラリを作成したので、その紹介と実装内容を紹介します。
artnet_ex
名前の通り、ArtNet を Elixir で扱うためのライブラリです。
昨日の記事では ArtNet のパケットをParseした結果を map で返していましが、
struct で返した方がパターンマッチなどで使いやすい場合があります。
そのため、このライブラリでは ArtNet のパケットを decode すると struct で返すようにしています。
また、encode にも対応しているため、ArtNet の struct からバイナリに変換も可能です。
ArtNet のパケットの parse の仕方は OpCode によって異なるため、op_code 毎に defstruct
を定義し、parse のパターンマッチを書いていくのは大変です。
また、parse 結果の struct からバイナリへの変換ロジックも定義するのも非常に面倒です。
そのため、このライブラリではマクロを使って struct と型定義を自動生成するようにしています。
artnet_ex ではよく使われる op_code の ArtDmx, ArtPoll, ArtPollReply に対応しています。
使い方
使い方に関しては GitHub の README に使い側を少し書いていますが、リポジトリ内の livemd にサンプルコードを書いています。
このサンプルでは私が作成したライブラリをインストールし、 ArtNet パケットの encode/decode を行い UDP で送受信する方法を紹介しています。
使い方に関しては基本的に livemd の内容と同じなので、詳しくは livebook の内容をご覧ください。
この記事では livemd の内容を一部抜粋して紹介します。
またこの記事では livebook のインストール方法や使い方については省略します。livebook については以下の公式サイトを参照してください。
余談なのですが、livebook をダウンロードしてセットアップするだけで Elixir のライブラリをインストールして動作する環境を作れるのはすごいですよね。
GitHub で公開する際にサンプルコードを livemd で書いておけば、動作環境を共有できるのは便利です。
livebook のセットアップ
まず最初に livebook でライブラリが使えるように Mix.install/1
を使って今回紹介するライブラリをインストールします。
Mix.install([
{:art_net, "~> 0.1.0", github: "nasshu2916/artnet_ex", branch: "master"}
])
Art-Net パケットの Decode/Encode
Art-Net の struct から パケットのバイナリへの encode は ArtNet.encode/1
, ArtNet.encode!/1
で行います。
ArtNet.encode/1
は {:ok, encoded_message}
か {:error, reason}
を返します。
ArtNet.encode!/1
は encode に失敗した場合は例外を投げます。
iex> dmx_packet = %ArtNet.Packet.ArtDmx{sequence: 1, physical: 0, sub_universe: 0, net: 0, length: 1, data: [255]}
iex> dmx_message = ArtNet.encode!(dmx_packet)
<<65, 114, 116, 45, 78, 101, 116, 0, 0, 80, 0, 14, 1, 0, 0, 0, 0, 1, 255>>
Art-Net パケットのバイナリから Art-Net の struct への decode は ArtNet.decode/1
, ArtNet.decode!/1
で行います。
ArtNet.decode/1
は {:ok, decoded_packet}
か {:error, reason}
を返します。
ArtNet.decode!/1
は decode に失敗した場合は例外を投げます。
iex> ArtNet.decode!(dmx_message)
%ArtNet.Packet.ArtDmx{sequence: 1, physical: 0, sub_universe: 0, net: 0, length: 1, data: [255]}
encode/decode が成功すると Art-Net パケットの struct が返されます。
また、encode/decode に失敗した場合は例外もしくは {:error, reason}
が返されます。
iex> ArtNet.decode("invalid message")
{:error, %ArtNet.DecodeError{reason: {:invalid_data, "Invalid identifier"}}}
iex> art_dmx = %ArtNet.Packet.ArtDmx{sequence: 1, physical: 0, sub_universe: 0, net: 0, length: 1, data: [255, 255]}
iex> ArtNet.decode(art_dmx)
{:error, %ArtNet.DecodeError{reason: {:invalid_data, "Invalid identifier"}}}
ArtDmx の パケットで encode/decode しましたが、ArtPoll のパケットでも同様に encode/decode が可能です。
iex> poll_message = <<65, 114, 116, 45, 78, 101, 116, 0, 0, 32, 0, 14, 0, 0>>
iex> poll_packet = ArtNet.decode(poll_message)
{:ok,
%ArtNet.Packet.ArtPoll{
talk_to_me: %ArtNet.Packet.BitField.TalkToMe{
reply_on_change: false,
diagnostics: false,
diag_unicast: false,
vlc: false
},
priority: :dp_all
}}
iex> ArtNet.encode!(poll_packet)
<<65, 114, 116, 45, 78, 101, 116, 0, 0, 32, 0, 14, 0, 0>>
ArtNet パケットの encode/decode ができれば erlang の :gen_udp
を使って Art-Net パケットを送受信することができます。
送受信の方法について記載するともう一つ記事が書けそうなぐらい長くなるので、詳しくは livemd を参照してください。
Art-Net パケットの定義
ArtNet のパケット構造を defpacket
マクロで定義すると各モジュールで encode/decode の関数が作られます。
例えば Dmx パケットの定義は以下の様になります。
defmodule ArtNet.Packet.ArtDmx do
use ArtNet.Packet.Schema
defpacket do
field(:sequence, {:integer, 8}, default: 0)
field(:physical, {:integer, 8}, default: 0)
field(:sub_universe, {:integer, 8}, default: 0)
field(:net, {:integer, 8}, default: 0)
field(:length, {:integer, 16})
field(:data, [{:integer, 8}])
end
@impl ArtNet.Packet.Schema
def validate(packet) do
%{length: data_length, data: data} = packet
if data_length == length(data) do
:ok
else
{:error, "Data length does not match the length field"}
end
end
end
この ArtNet.Packet.ArtDmx
モジュールの関数は以下の様になります。
iex> ArtNet.Packet.ArtDmx.__info__(:functions)
[
__struct__: 0,
__struct__: 1,
decode: 1,
encode: 1,
op_code: 0,
require_version_header?: 0,
schema: 0,
validate: 1
]
defpacket
マクロで decode/1
, encode/1
関数が作られたことがわかります。
この decode/encode 関数は対応する ArtNet パケットの encode/decode のみ行えます。
iex> ArtNet.Packet.ArtDmx.decode(poll_message)
{:error, %ArtNet.DecodeError{reason: {:invalid_data, "Invalid op code"}}}
iex> ArtNet.Packet.ArtDmx.decode(dmx_message)
{:ok,
%ArtNet.Packet.ArtDmx{
sequence: 57,
physical: 0,
sub_universe: 0,
net: 0,
length: 1,
data: [255]
}}
validate
関数ですが、decode したパケットが正しいかどうかを検証するためのコールバック関数になります。
ArtDmx の場合は data の長さが length と一致しているかどうかを検証しています。
schema
関数はマクロで定義した field の情報を返す関数になります。
この関数の情報をもとに ArtNet パケットの encode/decode の処理が行われます。
iex> ArtNet.Packet.ArtDmx.schema()
[
sequence: {{:integer, 8}, [default: 0]},
physical: {{:integer, 8}, [default: 0]},
sub_universe: {{:integer, 8}, [default: 0]},
net: {{:integer, 8}, [default: 0]},
length: {{:integer, 16}, []},
data: {[integer: 8], []}
]
encode/decode の処理
defpacket
マクロを実際に見てもらうと分かるのですが、decode/encode の関数は ArtNet.Packet.decode/2
, ArtNet.Packet.encode/2
を呼び出す関数になっていて、
実際の decode/encode の処理は ArtNet.Packet
モジュールで行っています。
この ArtNet.Packet
モジュールでは schema/0
関数の情報ももとに ArtNet パケットの encode/decode の処理を行っていきます。
各 field の encode/decode の処理は ArtNet.Decoder.decode/3
, ArtNet.Encoder.encode/3
に各 field の型情報を渡して処理を行っています。
https://github.com/nasshu2916/artnet_ex/blob/7071b2a90cf6414ea4c452add5a2c474d32f8c02/lib/art_net/packet.ex#L25-L34
https://github.com/nasshu2916/artnet_ex/blob/7071b2a90cf6414ea4c452add5a2c474d32f8c02/lib/art_net/packet.ex#L112-L127
型定義の生成
実際にコードを書く際には Dialyzer を使った型検査を行うことが多いと思います。
そのため、このライブラリでは defpakcet
マクロで定義した field の情報から AST 木を生成し、@type
を定義するようにしています。
iex> t ArtNet.Packet.ArtDmx
@type t() :: %ArtNet.Packet.ArtDmx{
data: [:integer],
length: :integer,
net: :integer,
physical: :integer,
sequence: :integer,
sub_universe: :integer
}
@type
を定義するための AST 木の生成方法はマクロのこの辺の処理になります。
OpCode とモジュールの紐付け
defpakcet
マクロで ArtNet パケットの定義を行っていますが、このマクロで定義したモジュールと OpCode を紐付けを ArtNet.OpCode
モジュールの module attribute に定義しています。
この module attribute に定義することで ArtNet.decode/1
, ArtNet.encode/1
関数を呼んだ際に、OpCode に対応するモジュールの decode/encode 関数を呼び出しています。