Skip to main content

lib/art_net.ex

defmodule ArtNet do
  @moduledoc """
  Main API for encoding and decoding Art-Net packets.

  Use this module when working with complete Art-Net packets, including the
  `Art-Net\\0` identifier, OpCode, optional protocol version header, and packet
  payload.

  ```elixir
  {:ok, packet} = ArtNet.decode(binary)
  {:ok, binary} = ArtNet.encode(packet)
  ```

  Packet structs are defined under `ArtNet.Packet`. Low-level field codecs live
  in `ArtNet.Decoder` and `ArtNet.Encoder`.

  This library provides encode/decode functionality only. It does not open
  sockets or transfer Art-Net packets over the network.
  """

  alias ArtNet.{Packet, OpCode}

  @artnet_identifier ArtNet.Packet.identifier()

  @typedoc """
  Any Art-Net packet struct generated by `ArtNet.Packet.Schema`.
  """
  @type packet :: struct

  @doc """
  Decodes a complete Art-Net binary into a packet struct.

  Returns `{:ok, packet}` on success or `{:error, %ArtNet.DecodeError{}}` when
  the identifier, OpCode, version header, payload, or packet validation fails.
  """
  defdelegate decode(data), to: Packet

  @doc """
  Decodes a complete Art-Net binary into a packet struct.

  Raises `ArtNet.DecodeError` when decoding fails.
  """
  defdelegate decode!(data), to: Packet

  @doc """
  Encodes an Art-Net packet struct into a complete Art-Net binary.

  Returns `{:ok, binary}` on success or `{:error, %ArtNet.EncodeError{}}` when
  the value is not a packet struct, packet validation fails, or any field cannot
  be encoded.
  """
  defdelegate encode(packet), to: Packet

  @doc """
  Encodes an Art-Net packet struct into a complete Art-Net binary.

  Raises `ArtNet.EncodeError` when encoding fails.
  """
  defdelegate encode!(packet), to: Packet

  @doc """
  Fetches the Art-Net packet opcode from a binary packet.

  The return value is `{:ok, op_code}` for a supported OpCode, such as
  `{:ok, :op_dmx}`, or `:error` if the binary does not start with the Art-Net
  identifier or the OpCode is not supported.

  ## Examples

      iex> ArtNet.fetch_op_code(<<0x41, 0x72, 0x74, 0x2D, 0x4E, 0x65, 0x74, 0x00, 0x00, 0x50, 0x00, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF>>)
      {:ok, :op_dmx}

      iex> ArtNet.fetch_op_code(<<0x41, 0x72, 0x74, 0x2D, 0x4E, 0x65, 0x74, 0x01, 0x00, 0x50, 0x00, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF>>)
      :error

      iex> ArtNet.fetch_op_code(<<0x41, 0x72, 0x74, 0x2D, 0x4E, 0x65, 0x74, 0x00, 0xFF, 0x7F, 0x00, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF>>)
      :error
  """
  @spec fetch_op_code(binary) :: {:ok, OpCode.type()} | :error
  def fetch_op_code(<<@artnet_identifier, op_code::little-size(16), _rest::binary>>) do
    case OpCode.op_code_type(op_code) do
      nil -> :error
      op_code -> {:ok, op_code}
    end
  end

  def fetch_op_code(_) do
    :error
  end
end