Exgencode (exgencode v2.5.1)

Documentation for Exgencode.

Link to this section Summary

Types

Parameters of the given field

A custom decoding function that receives the PDU decoded so far and remaining binary and is meant to return PDU with the field decoded and remaining binary.

A custom encoding function that is meant to take the value of the field and return its binary represantion.

The endianness the field should be encoded/decoded with

Name of the field.

The type of the field.

A PDU, that is an Elixir structure representing a PDU.

PDU name, must be a structure name

Desired return type of pdu size

Functions

This macro allows for the definition of binary PDUs in a simple way allowing for convienient encoding and decoding them between binary format and Elixir structures.

Link to this section Types

Link to this type

fieldParam()

Specs

fieldParam() ::
  {:size, non_neg_integer() | field_name()}
  | {:type, field_type()}
  | {:encode, field_encode_fun()}
  | {:decode, field_decode_fun()}
  | {:version, Version.requirement()}
  | {:endianness, field_endianness()}
  | {:conditional, field_name()}
  | {:offset_to, field_name()}

Parameters of the given field

Link to this type

field_decode_fun()

Specs

field_decode_fun() :: (pdu(), bitstring() -> {pdu(), bitstring()})

A custom decoding function that receives the PDU decoded so far and remaining binary and is meant to return PDU with the field decoded and remaining binary.

Link to this type

field_encode_fun()

Specs

field_encode_fun() :: (term() -> bitstring())

A custom encoding function that is meant to take the value of the field and return its binary represantion.

Link to this type

field_endianness()

Specs

field_endianness() :: :big | :little | :native

The endianness the field should be encoded/decoded with

Link to this type

field_name()

Specs

field_name() :: atom()

Name of the field.

Link to this type

field_type()

Specs

field_type() ::
  :subrecord
  | :constant
  | :string
  | :binary
  | :float
  | :integer
  | :variable
  | :skip

The type of the field.

Specs

pdu() :: struct()

A PDU, that is an Elixir structure representing a PDU.

Specs

pdu_name() :: module()

PDU name, must be a structure name

Link to this type

return_size_type()

Specs

return_size_type() :: :bits | :bytes

Desired return type of pdu size

Link to this section Functions

Link to this macro

defpdu(name, original_field_list)

(macro)

Specs

defpdu(pdu_name(), [{field_name(), fieldParam()}]) :: any()

This macro allows for the definition of binary PDUs in a simple way allowing for convienient encoding and decoding them between binary format and Elixir structures.

PDUs

Each PDU for the protocol is defined given a name that must be a valid Elixir structure (module) name followed by a list of fields that the given PDU has.

Fields

Each field can have the following options specified:

size

Defines the size of field in bits. If the field is of type :subrecord the :size is unused.

defpdu SomePdu,
  someField: [size: 12]

Size can also be defined as a function - this is meant to be used together with the custom encode/decode functions that may produce variable length fields.

defpdu CustomSizeFunPdu,
  firstField: [default: 2, size: 8],
  custom: [
    encode: fn {size, vals} -> <<size::8, vals::size*8>> end,
    decode: fn pdu, <<size::8, val::binary>> ->
      <<vals::size*8, rest::binary>> = val
      {struct(pdu, %{custom: {size, vals}}), rest}
    end,
    size: fn %CustomSizeFunPdu{custom: {size, _vals}} -> size * 8 end
  ],

default

Defines the default value that the field should assume when building a new Elixir structure of the given PDU.

defpdu PduWithDefault,
  aFieldWithDefault: [size: 10, default: 15]

type

Defines the type of the field. Field can be of type:

  • :constant
  • :subrecord
  • :string
  • :binary
  • :float
  • :integer
  • :variable
  • :skip

If no type should is specified it will default to :integer. Both :integer and :float specify normal numerical values and have no special properties.

:constant

If the field is constant it will not become part of the Elixir structure and will not be accessible. However it will still be encoded into the binary representation and the decoding will expect the field to be present and have the given value in the decoded binary. Otherwise FunctionClauseError will be raised. A :constant field MUST have a default value specified.

defpdu PduWithConstant,
  aConstantField: [size: 12, default: 10, type: :constant]

iex> Exgencode.Pdu.encode(%TestPdu.PduWithConstant{})
<< 10 :: size(16) >>
iex> is_map_key(%TestPdu.PduWithConstant{}, :aConstantField)
false

:subrecord

If the field is meant to contain a sub-structure then it should be of type :subrecord. Such field must have either a default value specified that is of the type of the subrecord. Alternatively it must define custom decode and encode functions.

Examples:

defpdu SubPdu,
  someField: [size: 10, default: 1]

defpdu TopPdu,
  aField: [size: 24],
  subPdu: [type: :subrecord, default: %SupPdu{}]

iex> Exgencode.Pdu.encode(%TestPdu.TopPdu{aField: 24})
<< 24 :: size(24), 1 :: size(16) >>

iex> Exgencode.Pdu.decode(%TestPdu.TopPdu{}, << 24 :: size(24), 1 :: size(16) >>)
{%TestPdu.TopPdu{aField: 24, subPdu: %TestPdu.SubPdu{someField: 1}}, <<>>}

:virtual

The virtual fields are never encoded into binaries and exist only in the Elixir structs. When decoding into a struct the virtual field will always assume the default value.

Examples:

defpdu VirtualPdu,
  real_field: [size: 16],
  virtual_field: [type: :virtual]

iex> Exgencode.Pdu.encode(%TestPdu.VirtualPdu{real_field: 12, virtual_field: "Any value goes here"})
<<12::size(16)>>

iex> Exgencode.Pdu.decode(%TestPdu.VirtualPdu{}, <<12::size(16)>>)
{%TestPdu.VirtualPdu{real_field: 12, virtual_field: nil}, <<>>}

:binary

If the field is an arbitrary binary value it can be specified with this type. In such case the size parameter indicates size in bytes rather than bits. This type does not define any padding, that is the size of the binary that is contained in this field must be of at least the defined field size, otherwise an ArgumentError is raised. If the size is larger the binary will be trimmed.

:variable

Variable fields have no pre-defined size, instead the size is defined by the value of another field. When defining a :variable field, the :size parameter must be set to the name of the field definining the size, which in turn should be an :integer field. The size in that case can only be specified in bytes. All :variable fields are binary fields.

Examples:

defpdu VariablePdu,
  some_field: [size: 16],
  size_field: [size: 16],
  variable_field: [type: :variable, size: :size_field]

iex> Exgencode.Pdu.encode(%TestPdu.VariablePdu{some_field: 52, size_field: 2, variable_field: "AB"})
<<52::size(16), 2::size(16), "A", "B">>

iex> Exgencode.Pdu.decode(%TestPdu.VariablePdu{}, <<52::size(16), 2::size(16), "A", "B">>)
{%TestPdu.VariablePdu{some_field: 52, size_field: 2, variable_field: "AB"}, <<>>}

Note that the field defining the size must be defined before the variable length field.

Examples:

defpdu BinaryMsg,
  someHeader: [size: 8, default: 10],
  binaryField: [size: 16, type: :binary]

iex> Exgencode.Pdu.encode(%TestPdu.BinaryMsg{binaryField: "16charactershere"})
<< 10 :: size(8), "16charactershere" :: binary>>

:string

The :string type is similar to :binary, however it will not raise any errors if the length of the value to be encoded is different than declared field size. Instead, the string will be trimmed if its too long and padded with trailing 0-bytes if it is too short. Upon decoded all trailing 0-bytes will be removed.

For any other handling of padding or empty bytes custom decode and encode functions must be defined.

Examples:

defpdu StringMsg,
  someHeader: [size: 8, default: 10],
  stringField: [size: 16, type: :string]

iex> Exgencode.Pdu.encode(%TestPdu.StringMsg{stringField: "16charactershere"})
<< 10 :: size(8), "16charactershere" :: binary>>

iex> Exgencode.Pdu.encode(%TestPdu.StringMsg{stringField: "Too long string for field size"})
<< 10 :: size(8), "Too long string " :: binary>>

iex> Exgencode.Pdu.encode(%TestPdu.StringMsg{stringField: "Too short"})
<< 10 :: size(8), "Too short" :: binary, 0, 0, 0, 0, 0, 0, 0>>

iex> Exgencode.Pdu.decode(%TestPdu.StringMsg{}, << 10 :: size(8), "Too short" :: binary, 0, 0, 0, 0, 0, 0, 0>>)
{%TestPdu.StringMsg{stringField: "Too short"}, <<>>}

:skip

If the field will never be decoded into the struct. It's value will be set to :default when encoding the struct.

defpdu SkippedPdu,
  testField: [default: 1, size: 16],
  skippedField: [size: 8, default: 5, type: :skip]

iex> Exgencode.Pdu.encode(%TestPdu.SkippedPdu{testField: 15})
<< 15 :: size(16), 5 :: size(8) >>
iex> is_map_key(%TestPdu.SkippedPdu{}, :skippedField)
false
iex> Exgencode.Pdu.decode(%TestPdu.SkippedPdu{}, << 32 :: size(16), 11 :: size (8)>>)
{%TestPdu.SkippedPdu{testField: 32}, <<>>}

encode/decode

Defines a custom encode or decode function. See type specifications for the function specification. If a PDU has a custom encode function defined it must also define a custom decode function. Custom encode and decode functions can override any of the other parameters the field has if the user wishes it so.

Examples:

defpdu CustomPdu,
  normalField: [size: 16, default: 3],
  customField: [encode: fn(val) -> << val * 2 :: size(12) >> end,
                decode: fn(pdu, << val :: size(12) >>) -> {struct(pdu, %{customField: div(val, 2)}), <<>>} end]

iex> Exgencode.Pdu.encode(%TestPdu.CustomPdu{customField: 10})
<< 3 :: size(16), 20 :: size(16) >>

iex> Exgencode.Pdu.decode(%TestPdu.CustomPdu{}, << 3 :: size(16), 20 :: size(16) >>)
{%TestPdu.CustomPdu{customField: 10}, <<>>}

version

Defines the requirement for the protocol version for the given field to be included in the message. When a version is specified encode/2 and decode/3 can take an optional parameter with the given version name. If the given version matches the version requirement defined by this option in the PDU definition, the field will be included. Otherwise it will be skipped.

defpdu VersionedMsg,
  oldField: [default: 10, size: 16],
  newerField: [size: 8, version: ">= 2.0.0"],

See documentation for Exgencode.Pdu./2 for examples.

endianness

Defines the endianness of the particular field. Allowed options are :big, :little and :native. Defaults to :big

Examples:

defpdu EndianMsg,
  bigField: [default: 15, size: 32, endianness: :big],
  smallField: [default: 15, size: 32, endianness: :little]

iex> Exgencode.Pdu.encode(%TestPdu.EndianMsg{})
<< 15 :: big-size(32), 15 :: little-size(32)>>

conditional

Defines that the field is present in encoded binary format only if another field has a non-null value.

Examples:

defpdu ConditionalPdu,
    normal_field: [size: 16],
    flag_field: [size: 8],
    conditional_field: [size: 8, conditional: :flag_field],
    another_normal_field: [size: 8],
    second_flag: [size: 8],
    size_field: [size: 16, conditional: :second_flag],
    conditional_variable_field: [type: :variable, size: :size_field, conditional: :second_flag]

iex> Exgencode.Pdu.encode(%TestPdu.ConditionalPdu{
...>      normal_field: 12,
...>      flag_field: 1,
...>      conditional_field: 10,
...>      another_normal_field: 200,
...>      second_flag: 1,
...>      size_field: 4,
...>      conditional_variable_field: "test"
...>    })
<<12::size(16), 1, 10, 200, 1, 4::size(16), "test">>

iex> Exgencode.Pdu.encode(%TestPdu.ConditionalPdu{
...>      normal_field: 12,
...>      flag_field: 1,
...>      conditional_field: 10,
...>      another_normal_field: 200,
...>      second_flag: 0,
...>      size_field: nil,
...>      conditional_variable_field: nil
...>    })
<<12::size(16), 1, 10, 200, 0>>

offset_to

Defines that the field contains the offset to another field. The offset is in bytes since the beginning of the PDU. Note that offsets are automatically calculated when calling Exgencode.Pdu.encode/2

Examples:

defpdu OffsetPdu,
  offset_to_field_a: [size: 16, offset_to: :field_a],
  offset_to_field_b: [size: 16, offset_to: :field_b],
  offset_to_field_c: [size: 16, offset_to: :field_c],
  field_a: [size: 8],
  size_field: [size: 16],
  variable_field: [type: :variable, size: :size_field],
  field_b: [size: 8],
  field_c: [size: 8, conditional: :offset_to_field_c]

iex> Exgencode.Pdu.encode(%TestPdu.OffsetPdu{
...>   field_a: 14,
...>   size_field: 4,
...>   variable_field: "test",
...>   field_b: 15,
...>   field_c: 20
...> })
<<6::size(16), 9 + 4::size(16),10 + 4::size(16), 14, 4::size(16)>> <> "test" <> <<15, 20>>

iex> Exgencode.Pdu.encode(%TestPdu.OffsetPdu{
...>   field_a: 14,
...>   size_field: 4,
...>   variable_field: "test",
...>   field_b: 15,
...>   field_c: nil
...> })
<<6::size(16), 9 + 4::size(16), 0::size(16), 14, 4::size(16)>> <> "test" <> <<15>>