defmodule BACnet.Protocol do @moduledoc """ This module is mostly used for basic decoding of BACnet frames (Protocol Data Units - PDU). This module handles decoding of BVLL (and delegates specifics), NPCI and NSDU. APDU is completely covered by `BACnet.Protocol.APDU`. For BACnet Virtual Link Layer (BVLL), it will handle it and delegate, once it determines it is a BVLC function. BVLC function codes such as distribute broadcast, original broad- and unicast and forwarded NPDU are handled by this module directly. Currently only BVLL type 0x81 (BACnet/IPv4) is implemented. The five simple BVLC functions that only carry an NPDU (or nothing) are represented in this module as atoms inside the `t:bvlc/0` union: `:original_unicast`, `:original_broadcast`, `:distribute_broadcast_to_network`. All seven management functions (Register-Foreign-Device, the BDT/FDT read/write operations, Delete-FDT-Entry, etc.) are represented by the richer structs in `BACnet.Protocol.BvlcFunction`, `BACnet.Protocol.BvlcResult` and `BACnet.Protocol.BvlcForwardedNPDU`. For Network Protocol Control Information (NPCI), it will handle all decoding associated with it and handle field handling. For Network Service Data Unit (NSDU), it will handle all decoding associated with the regular BACnet types, excluding reserved and vendor proprietary. For Application Data Unit (APDU), see the `BACnet.Protocol.APDU` module. ## See Also - `BACnet.Protocol.APDU` - `BACnet.Protocol.BroadcastDistributionTableEntry` - `BACnet.Protocol.BvlcFunction` - the seven management BVLC payloads - `BACnet.Protocol.BvlcForwardedNPDU` - Forwarded-NPDU carrier (function 0x04) - `BACnet.Protocol.BvlcResult` - BVLC-Result (function 0x00) - `BACnet.Protocol.ForeignDeviceTableEntry` - `BACnet.Protocol.NPCI` - `BACnet.Protocol.NetworkLayerProtocolMessage` """ # TODO: Add encode functions alias BACnet.Protocol.APDU alias BACnet.Protocol.BvlcForwardedNPDU alias BACnet.Protocol.BvlcFunction alias BACnet.Protocol.BvlcResult alias BACnet.Protocol.Constants alias BACnet.Protocol.NetworkLayerProtocolMessage alias BACnet.Protocol.NPCI alias BACnet.Protocol.NpciTarget require Constants @typedoc """ BACnet Application Data Units (APDU). """ @type apdu :: APDU.ConfirmedServiceRequest.t() | APDU.UnconfirmedServiceRequest.t() | APDU.ComplexACK.t() | APDU.SimpleACK.t() | APDU.SegmentACK.t() | APDU.Abort.t() | APDU.Error.t() | APDU.Reject.t() @typedoc """ BACnet Virtual Link Control (BVLC), used in BACnet/IP (Annex J). This is the union of everything that can appear after a BVLL header (Type 0x81) on the wire. The five simple carriers are represented as atoms so that higher layers (especially `BACnet.Stack.Transport.IPv4Transport`) do not have to allocate structs for the common case of ordinary unicast/broadcast traffic. | Variant | Produced by BVLC Function | Description | |--------------------------------------|-----------------------------|-------------------------------------------------------------| | `BvlcForwardedNPDU.t()` | 0x04 | Broadcast or foreign-device traffic relayed by a BBMD | | `BvlcFunction.t()` | 0x01, 0x02, 0x03, 0x05-0x08 | The seven management operations (BDT/FDT, Register, Delete) | | `BvlcResult.t()` | 0x00 | Success/NAK reply for a management request | | `:original_unicast` | 0x0A | Normal directed NPDU (most common) | | `:original_broadcast` | 0x0B | Local broadcast on a B/IP subnet | | `:distribute_broadcast_to_network` | 0x09 | Foreign device asking its BBMD to broadcast on its behalf | """ @type bvlc :: BvlcForwardedNPDU.t() | BvlcFunction.t() | BvlcResult.t() | :distribute_broadcast_to_network | :original_broadcast | :original_unicast @npci_version NPCI.get_version() @doc """ Decodes the BVLL header of a BACnet/IP packet (Annex J.2). The first two arguments are the BVLL Type (normally 0x81) and the BVLC Function code. The third argument is the remaining bytes after the 4-octet BVLL header. On success the function returns the number of bytes consumed by the BVLC payload (so the caller knows where the NPDU begins), the decoded `t:bvlc/0` value, and any trailing bytes that belong to the NPDU. Management functions (0x01-0x03, 0x05-0x08) are delegated to `BACnet.Protocol.BvlcFunction.decode/2`. The five simple NPDU carriers and the BVLC-Result (0x00) and Forwarded-NPDU (0x04) cases are handled directly and return the corresponding atom or struct. """ @spec decode_bvll(non_neg_integer(), non_neg_integer(), binary()) :: {:ok, {bvlc_size :: non_neg_integer(), bvlc :: bvlc(), rest :: binary()}} | {:error, term()} def decode_bvll(bvll_type, bvlc_function, data) def decode_bvll( Constants.macro_by_name(:bvll, :type_bacnet_ipv4), Constants.macro_by_name(:bvlc_result_purpose, :bvlc_result), <> ) do case Constants.by_value(:bvlc_result_format, result_code) do {:ok, result} -> payload = %BvlcResult{ result_code: result } {:ok, {2, payload, rest}} :error -> {:error, {:unknown_result_code, result_code}} end end def decode_bvll( Constants.macro_by_name(:bvll, :type_bacnet_ipv4), Constants.macro_by_name(:bvlc_result_purpose, :bvlc_result), _rest ) do {:error, :insufficient_bvlc_data} end def decode_bvll( Constants.macro_by_name(:bvll, :type_bacnet_ipv4), Constants.macro_by_name(:bvlc_result_purpose, :bvlc_forwarded_npdu), data ) do with {:ok, {payload, rest}} <- BvlcForwardedNPDU.decode(data) do {:ok, {6, payload, rest}} end end def decode_bvll( Constants.macro_by_name(:bvll, :type_bacnet_ipv4), Constants.macro_by_name(:bvlc_result_purpose, :bvlc_original_unicast_npdu), data ) when is_binary(data) do {:ok, {0, :original_unicast, data}} end def decode_bvll( Constants.macro_by_name(:bvll, :type_bacnet_ipv4), Constants.macro_by_name(:bvlc_result_purpose, :bvlc_original_broadcast_npdu), data ) when is_binary(data) do {:ok, {0, :original_broadcast, data}} end def decode_bvll( Constants.macro_by_name(:bvll, :type_bacnet_ipv4), Constants.macro_by_name(:bvlc_result_purpose, :bvlc_distribute_broadcast_to_network), data ) when is_binary(data) do {:ok, {0, :distribute_broadcast_to_network, data}} end def decode_bvll( Constants.macro_by_name(:bvll, :type_bacnet_ipv4), bvlc_code, rest ) when is_integer(bvlc_code) and is_binary(rest) do case BvlcFunction.decode(bvlc_code, rest) do {:ok, bvlc} -> {:ok, {byte_size(rest), bvlc, <<>>}} {:error, _err} = err -> err end end @doc """ Decodes the NPCI header of a BACnet packet. """ @spec decode_npci(binary()) :: {:ok, {NPCI.t(), rest :: binary()}} | {:error, term()} def decode_npci(data) def decode_npci(<<@npci_version::size(8), npci::size(8), rest::binary>>) do with {:ok, {destination, rest}} <- (if Bitwise.band(npci, 0x20) > 0 do case rest do <> -> target = %NpciTarget{ net: net, address: if(addr_length > 0, do: address) } {:ok, {target, restn}} _rest -> {:error, :invalid_destination} end else {:ok, {nil, rest}} end), {:ok, {source, rest}} <- (if Bitwise.band(npci, 0x08) > 0 do case rest do <> -> target = %NpciTarget{ net: net, address: if(addr_length > 0, do: address) } {:ok, {target, restn}} _rest -> {:error, :invalid_source} end else {:ok, {nil, rest}} end), {:ok, {hopcount, rest}} <- (if Bitwise.band(npci, 0x20) > 0 do case rest do <> -> {:ok, {hopcount, restn}} _rest -> {:error, :invalid_hopcount} end else {:ok, {nil, rest}} end) do # Using by_value! is safe here, because only two bits are used and all (two) bits are covered by the protocol npci_prot = %NPCI{ priority: Constants.by_value!(:npdu_control_priority, Bitwise.band(npci, 0x03)), expects_reply: Bitwise.band(npci, 0x04) > 0, destination: destination, source: source, hopcount: hopcount, is_network_message: Bitwise.band(npci, 0x80) > 0 } {:ok, {npci_prot, rest}} end end def decode_npci(_data) do {:error, :invalid_npci} end @doc """ Decodes the NPDU of a BACnet packet. For network messages, it decodes the NSDU. For application messages, it simply returns the APDU for further processing. """ @spec decode_npdu(NPCI.t(), binary()) :: {:ok, {type :: :network | :apdu, NetworkLayerProtocolMessage.t() | binary()}} | {:error, term()} def decode_npdu(npci, data) def decode_npdu(%NPCI{is_network_message: true} = _npci, data) do with {:ok, nsdu} <- decode_nsdu(data) do {:ok, {:network, nsdu}} end end def decode_npdu(%NPCI{is_network_message: false} = _npci, data) do {:ok, {:apdu, data}} end @doc """ Decodes the NSDU of a BACnet packet. """ @spec decode_nsdu(binary()) :: {:ok, NetworkLayerProtocolMessage.t()} | {:error, term()} def decode_nsdu(data) def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :who_is_router_to_network, msg_type: nil, data: dnet } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :who_is_router_to_network, msg_type: nil, data: nil } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :i_am_router_to_network, msg_type: nil, data: nsdu_parse_dnet_list(rest) } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :i_could_be_router_to_network, msg_type: nil, data: {dnet, perf_index} } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :reject_message_to_network, msg_type: nil, data: {dnet, reason, nsdu_reject_reason_to_string(reason)} } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :router_busy_to_network, msg_type: nil, data: nsdu_parse_dnet_list(rest) } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :router_available_to_network, msg_type: nil, data: nsdu_parse_dnet_list(rest) } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :initialize_routing_table, msg_type: nil, data: nsdu_parse_routing_table_ports(rest) } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :initialize_routing_table_ack, msg_type: nil, data: nsdu_parse_routing_table_ports(rest) } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :establish_connection_to_network, msg_type: nil, data: {dnet, term_time} } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :disconnect_connection_to_network, msg_type: nil, data: dnet } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :what_is_network_number, msg_type: nil, data: nil } {:ok, message} end def decode_nsdu( <> ) do message = %NetworkLayerProtocolMessage{ network_message_type: :network_number_is, msg_type: nil, data: {dnet, if(flag == 1, do: :configured, else: :learned)} } {:ok, message} end def decode_nsdu(<>) when msg_type >= Constants.macro_by_name(:network_layer_message_type, :vendor_proprietary_area_start) do message = %NetworkLayerProtocolMessage{ network_message_type: :vendor_proprietary_area_start, msg_type: msg_type - Constants.macro_by_name(:network_layer_message_type, :vendor_proprietary_area_start), data: {vendor_id, rest} } {:ok, message} end def decode_nsdu(<>) when msg_type >= Constants.macro_by_name(:network_layer_message_type, :reserved_area_start) do message = %NetworkLayerProtocolMessage{ network_message_type: :reserved_area_start, msg_type: msg_type - Constants.macro_by_name(:network_layer_message_type, :reserved_area_start), data: rest } {:ok, message} end def decode_nsdu(data) when is_binary(data) do {:error, :invalid_data} end @spec nsdu_parse_dnet_list(binary()) :: [non_neg_integer()] defp nsdu_parse_dnet_list(binary) defp nsdu_parse_dnet_list(<<>>) do [] end defp nsdu_parse_dnet_list(bin) do {_rest, dnet} = Enum.reduce_while(1..65_535//1, {bin, []}, fn _entry, {<>, acc} -> {:cont, {rest, [dnet | acc]}} _entry, {_bin, acc} -> {:halt, {nil, acc}} end) Enum.reverse(dnet) end @spec nsdu_parse_routing_table_ports(binary()) :: [ {dnet :: non_neg_integer(), port_id :: byte(), port_info :: binary()} ] defp nsdu_parse_routing_table_ports(binary) defp nsdu_parse_routing_table_ports(<<>>) do [] end defp nsdu_parse_routing_table_ports(bin) do {_rest, ports} = Enum.reduce_while(1..65_535//1, {bin, []}, fn _entry, {<>, acc} -> {:cont, {rest, [{dnet, port_id, bin} | acc]}} _entry, {_bin, acc} -> {:halt, {nil, acc}} end) Enum.reverse(ports) end @spec nsdu_reject_reason_to_string(non_neg_integer()) :: String.t() | :undefined defp nsdu_reject_reason_to_string(reason) defp nsdu_reject_reason_to_string(0), do: "Other reason" defp nsdu_reject_reason_to_string(1), do: "The router is not directly connected to DNET and " <> "cannot find a router to DNET on any directly connected network " <> "using Who-Is-Router-To-Network messages" defp nsdu_reject_reason_to_string(2), do: "The router is busy and unable to accept messages for the specified DNET at the present time" defp nsdu_reject_reason_to_string(3), do: "It is an unknown network layer message type - the DNET returned in this case is a local matter" defp nsdu_reject_reason_to_string(4), do: "The message is too long to be routed to this DNET" defp nsdu_reject_reason_to_string(5), do: "The source message was rejected due to a BACnet security error " <> "and that error cannot be forwarded to the source device - " <> "see Clause 24.12.1.1 for more details " <> "on the generation of Reject-Message-To-Network messages indicating this reason" defp nsdu_reject_reason_to_string(6), do: "The source message was rejected due to errors in the addressing - " <> "the length of the DADR or SADR was determined to be invalid" defp nsdu_reject_reason_to_string(_reason), do: :undefined end