Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/foreign_device_table_entry.ex
defmodule BACnet.Protocol.ForeignDeviceTableEntry do
@moduledoc """
A Foreign Device Table Entry represents one foreign device that has registered
with a BACnet/IP Broadcast Management Device (BBMD). Foreign devices use this
registration mechanism to participate in BACnet broadcasts even when they are
not on the same IP subnet as the devices they need to communicate with.
The entry stores the IP address and port of the foreign device, the Time To
Live (TTL) value that was requested at registration time, and the remaining
time until the registration expires. The BBMD is responsible for sending a
Forwarded NPDU to each registered foreign device for every broadcast
it receives, and for removing entries whose remaining time reaches zero.
## BACnet Specification References
- Annex J.5.2.1 defines the FDT entry layout (6-octet B/IP address + 2-octet
Time-to-Live + 2-octet Remaining Time) and the two message contexts in which
entries appear.
- J.5.2.2 and J.5.2.3 describe the Register-Foreign-Device flow and the timer
rule: remaining time is initialized to the supplied TTL **plus a fixed 30
second grace period**. The entry is purged when the timer expires without a
re-registration.
- J.2.8.1 and J.2.9.1 give the exact wire formats (10 octets in Read-FDT-Ack,
only 6 octets for Delete-Foreign-Device-Table-Entry).
## Two Wire Representations
- **Normal / Ack form** (10 octets): used inside `Read-Foreign-Device-Table-Ack`
(BVLC function 0x07). Both `time_to_live` and `remaining_time` are present
(remaining_time already includes the +30 s grace).
- **Delete form** (6 octets): used for `Delete-Foreign-Device-Table-Entry`
(function 0x08) and when the BBMD itself needs to remove an entry. Both
time fields **must be `nil`**. The encoder deliberately emits the short form.
Maximum TTL / remaining value is 65535 (J.5.2.1.1). The BBMD in this library
caps the number of concurrent FDT entries (default 512, configurable).
## Construction & Gotchas
Full registration entry (as it would appear in a Read-FDT-Ack):
iex> alias BACnet.Protocol.ForeignDeviceTableEntry
iex> e = %ForeignDeviceTableEntry{
...> ip: {10, 1, 2, 3},
...> port: 0xBAC0,
...> time_to_live: 3600,
...> remaining_time: 3630
...> }
iex> ForeignDeviceTableEntry.encode(e)
{:ok, <<10, 1, 2, 3, 186, 192, 14, 16, 14, 46>>}
Delete form (the special case used with `BACnet.Protocol.BvlcFunction` for function 0x08):
iex> alias BACnet.Protocol.ForeignDeviceTableEntry
iex> del = %ForeignDeviceTableEntry{
...> ip: {10, 1, 2, 3},
...> port: 0xBAC0,
...> time_to_live: nil,
...> remaining_time: nil
...> }
iex> ForeignDeviceTableEntry.encode(del)
{:ok, <<10, 1, 2, 3, 186, 192>>}
Passing a mix of nil and integer values, or an out-of-range port, produces
`{:error, :invalid_data}`.
## See Also
- `BACnet.Protocol` - BVLL decoding that materialises FDT entries via BvlcFunction
- `BACnet.Protocol.BvlcFunction` - the Register, Read-FDT, Read-FDT-Ack and Delete operations that transport these entries
- `BACnet.Protocol.BvlcResult` - NAK codes that may be returned for FDT operations
- `BACnet.Stack.BBMD` - the server that owns the live FDT, enforces max size, runs the timers, and emits Forwarded-NPDUs to registered foreign devices
- `BACnet.Stack.ForeignDevice` - the client that creates registrations (and therefore indirectly causes FDT entries to be created on the remote BBMD)
"""
alias BACnet.Protocol.ApplicationTags
@typedoc """
An entry in the Foreign Device Table (FDT) maintained by a BBMD.
When both `time_to_live` and `remaining_time` are `nil` the struct represents
a *Delete-Foreign-Device-Table-Entry* request (J.2.9.1) and encodes to the
short 6-octet form. In all other cases (normal FDT rows and Read-FDT-Ack)
the fields contain the original TTL and the current remaining time (already
including the 30-second grace period defined in J.5.2.3).
"""
@type t :: %__MODULE__{
ip: :inet.ip4_address(),
port: :inet.port_number(),
time_to_live: non_neg_integer() | nil,
remaining_time: non_neg_integer() | nil
}
@fields [:ip, :port, :time_to_live, :remaining_time]
@enforce_keys @fields
defstruct @fields
@doc """
Decodes a Foreign Device Table Entry from binary data.
"""
@spec decode(binary()) :: {:ok, {t(), rest :: binary()}} | {:error, term()}
def decode(data)
def decode(
<<ip_a, ip_b, ip_c, ip_d, port::size(16), ttl::size(16), remaining::size(16),
rest::binary>>
) do
fd = %__MODULE__{
ip: {ip_a, ip_b, ip_c, ip_d},
port: port,
time_to_live: ttl,
remaining_time: remaining
}
{:ok, {fd, rest}}
end
def decode(data) when is_binary(data) do
{:error, :invalid_data}
end
@doc """
Encodes the Foreign Device Table Entry to binary data.
If both `time_to_live` and `remaining_time` are nil,
they are not included in the binary
(useful for `Delete-Foreign-Device-Table-Entry`)
"""
@spec encode(t()) :: {:ok, binary()} | {:error, term()}
def encode(entry)
def encode(
%__MODULE__{
ip: {ip_a, ip_b, ip_c, ip_d},
port: port,
time_to_live: nil,
remaining_time: nil
} = _entry
)
when is_integer(port) do
{:ok, <<ip_a, ip_b, ip_c, ip_d, port::size(16)>>}
end
def encode(
%__MODULE__{
ip: {ip_a, ip_b, ip_c, ip_d},
port: port,
time_to_live: ttl,
remaining_time: rem
} = _entry
)
when is_integer(port) and is_integer(ttl) and is_integer(rem) do
{:ok, <<ip_a, ip_b, ip_c, ip_d, port::size(16), ttl::size(16), rem::size(16)>>}
end
def encode(%__MODULE__{} = _entry), do: {:error, :invalid_data}
@doc """
Parse a Foreign Device Table Entry from application tags.
"""
@spec from_app_encoding(ApplicationTags.encoding_list()) ::
{:ok, {t(), rest :: ApplicationTags.encoding_list()}}
| {:error, term()}
def from_app_encoding(tags) when is_list(tags) do
case tags do
[
{:tagged, {0, ip_port, 6}},
{:tagged, {1, ttl, len1}},
{:tagged, {2, remaining, len2}}
| rest
]
when len1 in 1..2//1 and len2 in 1..2//1 ->
with {:ok, {val, _rest}} <- decode(ip_port <> pad_binary(ttl) <> pad_binary(remaining)) do
{:ok, {val, rest}}
end
_other ->
{:error, :invalid_tags}
end
end
@doc """
Enocde a Foreign Device Table Entry to application tags.
"""
@spec to_app_encoding(t()) ::
{:ok, ApplicationTags.encoding_list()}
| {:error, term()}
def to_app_encoding(entry)
def to_app_encoding(
%__MODULE__{
ip: {ip_a, ip_b, ip_c, ip_d},
port: port,
time_to_live: ttl,
remaining_time: rem
} = _entry
) do
ttl = ttl || 0
rem = rem || 0
{:ok,
[
{:tagged, {0, <<ip_a, ip_b, ip_c, ip_d, port::size(16)>>, 6}},
{:tagged, {1, <<ttl::size(16)>>, 2}},
{:tagged, {2, <<rem::size(16)>>, 2}}
]}
end
def to_app_encoding(%__MODULE__{} = _entry), do: {:error, :invalid_data}
@spec pad_binary(binary()) :: binary()
defp pad_binary(binary) do
if byte_size(binary) == 1 do
<<0>> <> binary
else
binary
end
end
end