defmodule BACnet.Protocol.NPCI do @moduledoc """ Network Protocol Control Information (NPCI) is the header that precedes every BACnet Network Protocol Data Unit (NPDU). It carries the information that BACnet routers and receiving devices need to correctly forward or deliver the message. The NPCI indicates the version of the protocol, the ultimate source and destination networks (when the message crosses routers), whether a reply is expected, the priority of the message, and whether the data that follows is an Application Protocol Data Unit (APDU) or a Network Service Data Unit (NSDU). Network-layer messages (NSDUs) are used for router-to-router communication, router configuration, and certain network management functions. """ 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