Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/npci.ex
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
<<hop::size(8)>>
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 = <<addr::binary, port::size(16)>>
#
# {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::size(8)>>}
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 = <<netaddr::integer-size(int_length)-unit(8)>>
{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 = <<net::size(16), len::size(8), addr_bin::binary>>
{1, addr}
end
end