defmodule BACnet.Protocol.NPCI do @moduledoc """ Network Protocol Control Information (NPCI) are used to determine priority, whether reply is expected, for who by who this frame is and what kind of BACnet Data Unit this is. BACnet Data Units can be divided into Application and Network Service. Where Application frames are called APDU and Network Service frames are called NSDU. Network Service frames are mostly used by and for BACnet routers. """ # TODO: Docs alias BACnet.Protocol.Constants alias BACnet.Protocol.NpciTarget require Constants @typedoc """ Represents Network Protocol Control Information (NPCI). """ @type t :: %__MODULE__{ priority: Constants.npdu_control_priority(), expects_reply: boolean(), destination: NpciTarget.t() | nil, source: NpciTarget.t() | nil, hopcount: non_neg_integer() | nil, is_network_message: boolean() } @fields [ :priority, :expects_reply, :destination, :source, :hopcount, :is_network_message ] @enforce_keys @fields defstruct @fields @npci_version 0x01 @doc """ Get the NPCI version. """ @spec get_version() :: non_neg_integer() def get_version(), do: @npci_version @doc """ Creates a new NPCI struct with the given fields. The following default values are applied: ```ex priority: :normal, expects_reply: false, destination: nil, source: nil, hopcount: nil, is_network_message: false ``` """ @spec new(Keyword.t()) :: t() def new(fields) when is_list(fields) do Enum.reduce( fields, %__MODULE__{ priority: :normal, expects_reply: false, destination: nil, source: nil, hopcount: nil, is_network_message: false }, fn {:priority, value}, acc when value in Constants.macro_list_names(:npdu_control_priority) -> %{acc | priority: value} {:expects_reply, value}, acc when is_boolean(value) -> %{acc | expects_reply: value} {:destination, value}, acc when is_nil(value) or (is_struct(value, NpciTarget) and value.net in 1..65_535) -> %{acc | destination: value} {:source, value}, acc when is_nil(value) or (is_struct(value, NpciTarget) and value.net in 1..65_534) -> %{acc | source: value} {:hopcount, value}, acc when is_nil(value) or value in 1..255 -> %{acc | hopcount: value} {:is_network_message, value}, acc when is_boolean(value) -> %{acc | is_network_message: value} {field, value}, _acc when field in @fields -> raise ArgumentError, "Invalid value for field #{field}, got: #{inspect(value)}" term, _acc -> raise ArgumentError, "Unknown or invalid term, got: #{inspect(term)}" end ) end @doc """ Creates a NPCI iodata from the NPCI struct. If `destination` is not nil, but `net` is nil, `net` will default to `1`. """ @spec encode(t(), Keyword.t()) :: iodata() def encode(%__MODULE__{} = npci, _opts \\ []) do priority = Constants.by_name!(:npdu_control_priority, npci.priority) {destination_specifier, destination} = case npci.destination do %NpciTarget{} = target -> npci_target(npci, target) nil -> {0, <<>>} term -> raise ArgumentError, "Invalid destination, expected nil or NpciTarget, got: #{inspect(term)}" end {source_specifier, source} = case npci.source do %NpciTarget{net: 65_535} -> raise ArgumentError, "Invalid source, net 65535 is not allowed" %NpciTarget{address: nil} -> raise ArgumentError, "Invalid source, address nil is not allowed" %NpciTarget{} = target -> npci_target(npci, target) nil -> {0, <<>>} term -> raise ArgumentError, "Invalid source, expected nil or NpciTarget, got: #{inspect(term)}" end hopcount = if destination_specifier == 1 do hop = if is_integer(npci.hopcount), do: min(max(npci.hopcount, 1), 255), else: 255 <> else <<>> end # Hint: res. = reserved # npci = apdu_0_or_npdu_1, res., destination_specifier, res., source_specifier, expects_reply, priority_two_bits npci = <<@npci_version::size(8), intify(npci.is_network_message)::size(1), 0::size(1), destination_specifier::size(1), 0::size(1), source_specifier::size(1), intify(npci.expects_reply)::size(1), priority::size(2)>> [ npci, destination, source, hopcount ] end @spec intify(boolean()) :: 0..1 defp intify(true), do: 1 defp intify(false), do: 0 @spec npci_target(t(), NpciTarget.t()) :: {0..1, binary()} defp npci_target(%__MODULE__{} = _npci, %NpciTarget{} = target) do if target.net == 0 or target.net == nil do raise ArgumentError, "Invalid net, must be in the range of 1..65535" end {len, addr_bin} = case target.address do nil -> {0, <<>>} # {ipaddr, port} # when is_tuple(ipaddr) and tuple_size(ipaddr) == 4 and port in 1..65_535 -> # addr = # ipaddr # |> Tuple.to_list() # |> List.to_string() # # finaddr = <> # # {byte_size(finaddr), finaddr} # # netaddr when is_tuple(netaddr) when tuple_size(netaddr) in 1..6 -> # addr = # netaddr # |> Tuple.to_list() # |> List.to_string() # # {byte_size(addr), addr} # # netaddr when netaddr in 0..255 -> # {1, <>} netaddr when is_integer(netaddr) and netaddr >= 0 and netaddr <= 72_057_594_037_927_935 -> int_length = div(byte_size(Integer.to_string(netaddr, 2)) + 7, 8) bin = <> {int_length, bin} term -> raise ArgumentError, "Invalid destination address, expected nil or " <> "a positive integer with max. 48 bits set, " <> "got: #{inspect(term)}" end net = target.net || 1 addr = <> {1, addr} end end