Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/device_object_ref.ex
defmodule BACnet.Protocol.DeviceObjectRef do
@moduledoc """
A DeviceObjectRef points to an object, optionally qualified by the
device that contains it. It is one of the most common reference types in the
entire BACnet object model.
- When `device_identifier` is present -> absolute reference (works across the
BACnet internetwork).
- When `device_identifier` is `nil` -> the reference is local to the device
holding the referring object.
This distinction matters for Schedule, Trend Log, Event Enrollment, Structured
View, and many other objects that can reference data in remote devices.
### BACnet Specification References
- **ASN.1** (Clause 21): `BACnetDeviceObjectReference ::= SEQUENCE { deviceIdentifier [0] BACnetObjectIdentifier OPTIONAL, objectIdentifier [1] BACnetObjectIdentifier }`
- Used inside `BACnet.Protocol.ObjectPropertyRef`, `BACnet.Protocol.RecipientAddress`, many event
and fault parameters, Structured View, etc.
### Examples (Doc Test)
```elixir
iex> ref = %DeviceObjectRef{
...> device_identifier: nil,
...> object_identifier: %ObjectIdentifier{type: :analog_input, instance: 1}
...> }
iex> ref.object_identifier.instance
1
```
"""
# TODO: Throw argument error in encode if not valid
alias BACnet.Protocol.ApplicationTags
alias BACnet.Protocol.ObjectIdentifier
import BACnet.Protocol.Utility, only: [pattern_extract_tags: 4]
@typedoc """
References an object, optionally qualified by the device it resides in.
"""
@type t :: %__MODULE__{
device_identifier: ObjectIdentifier.t() | nil,
object_identifier: ObjectIdentifier.t()
}
@fields [
:device_identifier,
:object_identifier
]
@enforce_keys @fields
defstruct @fields
@doc """
Encodes a BACnet device object reference into application tags encoding.
"""
@spec encode(t(), Keyword.t()) ::
{:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(%__MODULE__{} = ref, opts \\ []) do
with {:ok, devident, _header} <-
(if ref.device_identifier do
ApplicationTags.encode_value({:object_identifier, ref.device_identifier}, opts)
else
{:ok, nil, nil}
end),
{:ok, objident, _header} <-
ApplicationTags.encode_value({:object_identifier, ref.object_identifier}, opts) do
base = [
tagged: {0, devident, byte_size(devident || <<>>)},
tagged: {1, objident, byte_size(objident)}
]
{:ok, Enum.filter(base, fn {_type, {_t, con, _l}} -> con end)}
else
{:error, _err} = err -> err
end
end
@doc """
Parses a BACnet device object reference into a struct.
"""
@spec parse(ApplicationTags.encoding_list()) ::
{:ok, {t(), rest :: ApplicationTags.encoding_list()}} | {:error, term()}
def parse(tags) when is_list(tags) do
with {:ok, device_identifier, ref} <-
pattern_extract_tags(tags, {:tagged, {0, _t, _l}}, :object_identifier, true),
{:ok, object_identifier, rest} <-
pattern_extract_tags(ref, {:tagged, {1, _t, _l}}, :object_identifier, false) do
objref = %__MODULE__{
device_identifier: device_identifier,
object_identifier: object_identifier
}
{:ok, {objref, rest}}
else
{:error, :missing_pattern} -> {:error, :invalid_tags}
{:error, _err} = err -> err
end
end
@doc """
Validates whether the given device object reference is in form valid.
It only validates the struct is valid as per type specification.
"""
@spec valid?(t()) :: boolean()
def valid?(
%__MODULE__{
object_identifier: %ObjectIdentifier{} = obj_ref,
device_identifier: dev_ref
} = _t
) do
ObjectIdentifier.valid?(obj_ref) and
(is_nil(dev_ref) or
(is_struct(dev_ref, ObjectIdentifier) and ObjectIdentifier.valid?(dev_ref) and
dev_ref.type == :device))
end
def valid?(%__MODULE__{} = _t), do: false
end