6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2024

Day 24

Elixir で ArtNet を扱うライブラリを作ってみた

Last updated at Posted at 2024-12-23

はじめに

この記事は 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 関数を呼び出しています。

6
1
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
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?