Current section
Files
Jump to
Current section
Files
lib/art_net/packet.ex
defmodule ArtNet.Packet do
@moduledoc """
Encodes and decodes complete Art-Net packets.
This module is the packet-level codec used by `ArtNet`. It handles the common
Art-Net header and delegates payload handling to packet modules generated by
`ArtNet.Packet.Schema`.
Decoding follows this flow:
* validate the `Art-Net\\0` identifier.
* read the little-endian OpCode and resolve the packet module through
`ArtNet.OpCode`.
* validate the OpCode and protocol version header for the selected module.
* decode the payload with the packet module's generated body decoder.
* run packet decode validation.
Encoding follows the reverse flow: validate the struct, build the common
header from the packet module, encode the schema fields, and append the body.
"""
alias ArtNet.OpCode
@identifier "Art-Net" <> <<0>>
@version 14
@doc """
Returns the Art-Net packet identifier.
"""
@spec identifier :: binary
def identifier, do: @identifier
@doc """
Returns the Art-Net protocol version encoded in versioned packet headers.
"""
@spec version :: pos_integer
def version, do: @version
@doc """
Decodes a binary Art-Net packet.
## Examples
iex> ArtNet.Packet.decode(<<0x41, 0x72, 0x74, 0x2D, 0x4E, 0x65, 0x74, 0x00, 0x00, 0x50, 0x00, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF>>)
{:ok,
%ArtNet.Packet.ArtDmx{
sequence: 01,
physical: 0,
sub_universe: 0,
net: 0,
length: 1,
data: [255]
}}
iex> ArtNet.Packet.decode(<<0x41, 0x72, 0x74, 0x2D, 0x4E, 0x65, 0x74, 0x01, 0x00, 0x50, 0x00, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF>>)
{:error, %ArtNet.DecodeError{reason: {:invalid_data, "Invalid identifier"}}}
iex> ArtNet.Packet.decode(<<0x41, 0x72, 0x74, 0x2D, 0x4E, 0x65, 0x74, 0x00, 0xff, 0x7f, 0x00, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF>>)
{:error, %ArtNet.DecodeError{reason: {:invalid_op_code, 0x7fff}}}
iex> ArtNet.Packet.decode(<<0x01, 0x02>>)
{:error, %ArtNet.DecodeError{reason: {:invalid_data, "Invalid identifier"}}}
"""
@spec decode(binary) :: {:ok, struct} | {:error, ArtNet.DecodeError.t()}
def decode(<<@identifier, op_code::little-size(16), _rest::binary>> = data) do
case OpCode.packet_module_from_value(op_code) do
nil -> {:error, %ArtNet.DecodeError{reason: {:invalid_op_code, op_code}}}
module -> decode(module, data)
end
end
def decode(_) do
{:error, %ArtNet.DecodeError{reason: {:invalid_data, "Invalid identifier"}}}
end
@doc """
Decodes a binary Art-Net packet.
If the packet could not be decoded, the function raises an error.
## Examples
iex> ArtNet.Packet.decode!(<<0x41, 0x72, 0x74, 0x2D, 0x4E, 0x65, 0x74, 0x00, 0x00, 0x50, 0x00, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF>>)
%ArtNet.Packet.ArtDmx{sequence: 1, physical: 0, sub_universe: 0, net: 0, length: 1, data: [255]}
"""
@spec decode!(binary) :: struct
def decode!(binary) do
case decode(binary) do
{:ok, packet} -> packet
{:error, error} -> raise error
end
end
@doc """
Decodes a binary Art-Net packet as the given packet module.
This variant is useful when the caller already knows the expected packet
module. It still validates the Art-Net identifier, OpCode, and version header
before decoding the payload.
"""
@spec decode(module, binary) :: {:ok, struct} | {:error, ArtNet.DecodeError.t()}
def decode(module, rest) do
with {:ok, rest} <- validate_header(module, rest),
{:ok, packet} <- decode_body(module, rest),
:ok <- module.validate_decode(packet) do
{:ok, packet}
else
{:error, %ArtNet.DecodeError{} = error} ->
{:error, error}
{:error, reason} when is_binary(reason) ->
{:error, %ArtNet.DecodeError{reason: {:invalid_data, reason}}}
end
end
@spec decode_body(module, binary) :: {:ok, struct} | {:error, ArtNet.DecodeError.t()}
defp decode_body(module, rest), do: module.__decode_body__(rest)
@doc """
Validates the common Art-Net header for a packet module.
On success, returns the remaining payload binary after the identifier, OpCode,
and optional protocol version header have been consumed.
"""
@spec validate_header(module, binary) :: {:ok, binary} | {:error, ArtNet.DecodeError.t()}
def validate_header(module, rest) do
with {:ok, rest} <- validate_identifier(rest),
{:ok, rest} <- validate_op_code(module, rest),
{:ok, rest} <- validate_version(module, rest) do
{:ok, rest}
else
{:error, reason} -> {:error, %ArtNet.DecodeError{reason: {:invalid_data, reason}}}
end
end
defp validate_identifier(data) do
case data do
<<@identifier, rest::binary>> -> {:ok, rest}
_ -> {:error, "Invalid identifier"}
end
end
defp validate_op_code(module, data) do
op_code = module.op_code()
case data do
<<^op_code::little-integer-size(16), rest::binary>> -> {:ok, rest}
_ -> {:error, "Invalid op code"}
end
end
defp validate_version(module, data) do
require_version_header? = module.require_version_header?()
if require_version_header? do
case data do
<<@version::integer-size(16), rest::binary>> -> {:ok, rest}
_ -> {:error, "Invalid version"}
end
else
{:ok, data}
end
end
@doc """
Encodes an Art-Net packet struct into a complete Art-Net binary.
The packet module is taken from the struct itself. Encoding runs
`validate_encode/1`, writes the common header, and then encodes the payload
fields in schema order.
"""
@spec encode(ArtNet.packet()) :: {:ok, binary} | {:error, ArtNet.EncodeError.t()}
def encode(packet) do
with {{:ok, module}, _} <- {fetch_module(packet), "packet is not a struct"},
:ok <- module.validate_encode(packet),
header = encode_header(module),
{:ok, body} <- encode_body(packet) do
{:ok, header <> body}
else
{:error, %ArtNet.EncodeError{} = reason} ->
{:error, reason}
{:error, reason} when is_binary(reason) ->
{:error, %ArtNet.EncodeError{reason: {:invalid_data, reason}}}
end
end
@doc """
Encodes a binary Art-Net packet.
If the packet could not be encoded, the function raises an error.
## Examples
iex> ArtNet.Packet.encode!(%ArtNet.Packet.ArtDmx{sequence: 1, physical: 0, sub_universe: 0, net: 0, length: 1, data: [255]})
<<0x41, 0x72, 0x74, 0x2D, 0x4E, 0x65, 0x74, 0x00, 0x00, 0x50, 0x00, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF>>
"""
@spec encode!(ArtNet.packet()) :: binary
def encode!(packet) do
case encode(packet) do
{:ok, binary} -> binary
{:error, %ArtNet.EncodeError{} = error} -> raise error
end
end
@spec fetch_module(struct) :: {:ok, module} | :error
defp fetch_module(%{__struct__: module}), do: {:ok, module}
defp fetch_module(_), do: :error
@spec encode_header(module) :: binary
defp encode_header(module) do
op_code = module.op_code()
if module.require_version_header?() do
<<@identifier, op_code::little-integer-size(16), @version::integer-size(16)>>
else
<<@identifier, op_code::little-integer-size(16)>>
end
end
@spec encode_body(struct) :: {:ok, binary} | {:error, ArtNet.EncodeError.t()}
defp encode_body(packet), do: packet.__struct__.__encode_body__(packet)
end