Packages

BACstack is a low-level Elixir implementation for the ASHRAE standard 135, BACnet - Building Automation and Controller network. In its current published state, v0.0.x, it can (only) act as a BACnet client for other BACnet devices.

Current section

Files

Jump to
bacstack lib bacnet protocol services read_range.ex
Raw

lib/bacnet/protocol/services/read_range.ex

defmodule BACnet.Protocol.Services.ReadRange do
@moduledoc """
This module represents the BACnet Read Range service.
The Read Range service is used to read range of a property of an object.
Service Description (ASHRAE 135):
> The ReadRange service is used by a client BACnet-user to read a specific range of data items representing a subset of data
> available within a specified object property. The service may be used with any list or array of lists property.
"""
alias BACnet.Protocol
alias BACnet.Protocol.ApplicationTags
alias BACnet.Protocol.Constants
import Protocol.Utility, only: [pattern_extract_tags: 4]
require Constants
@behaviour Protocol.Services.Behaviour
# TODO: Docs
# TODO: Add Service Procedure to docs
# property_identifier MUST NOT be one of ALL, REQUIRED or OPTIONAL
# Count of range MAY NOT be zero
@type range ::
{:by_position,
{reference_index :: non_neg_integer(), count :: ApplicationTags.signed16()}}
| {:by_seq_number,
{reference_seq_number :: ApplicationTags.unsigned32(),
count :: ApplicationTags.signed16()}}
| {:by_time,
{reference_time :: Protocol.BACnetDateTime.t(), count :: ApplicationTags.signed16()}}
@type t :: %__MODULE__{
object_identifier: Protocol.ObjectIdentifier.t(),
property_identifier: Constants.property_identifier() | non_neg_integer(),
property_array_index: non_neg_integer() | nil,
range: range() | nil
}
@fields [
:object_identifier,
:property_identifier,
:property_array_index,
:range
]
@enforce_keys @fields
defstruct @fields
@service_name Constants.macro_assert_name(
:confirmed_service_choice,
:read_range
)
# Checks whether the argument is a valid count (INTEGER16 and not zero)
defguardp is_valid_count(range)
when is_integer(range) and range != 0 and range in -32_768..32_767
@doc """
Get the service name atom.
"""
@spec get_name() :: atom()
def get_name(), do: @service_name
@doc """
Whether the service is of type confirmed or unconfirmed.
"""
@spec is_confirmed() :: true
def is_confirmed(), do: true
@doc """
Converts the given Confirmed Service Request into a Read Range Service.
"""
@spec from_apdu(Protocol.APDU.ConfirmedServiceRequest.t()) ::
{:ok, t()} | {:error, term()}
def from_apdu(
%Protocol.APDU.ConfirmedServiceRequest{
service: @service_name
} = request
) do
with {:ok, object, rest} <-
pattern_extract_tags(
request.parameters,
{:tagged, {0, _t, _l}},
:object_identifier,
false
),
{:ok, property, rest} <-
pattern_extract_tags(rest, {:tagged, {1, _t, _l}}, :enumerated, false),
prop_identifier <- Constants.by_value(:property_identifier, property, property),
:ok <-
(if prop_identifier in [:all, :required, :optional] do
{:error, :invalid_property_identifier}
else
:ok
end),
{:ok, array_index, rest} <-
pattern_extract_tags(rest, {:tagged, {2, _t, _l}}, :unsigned_integer, true),
{:ok, range} <- parse_range(rest) do
readrange = %__MODULE__{
object_identifier: object,
property_identifier: prop_identifier,
property_array_index: array_index,
range: range
}
{:ok, readrange}
else
{:error, :missing_pattern} -> {:error, :invalid_request_parameters}
{:error, _err} = err -> err
end
end
def from_apdu(%Protocol.APDU.ConfirmedServiceRequest{} = _request) do
{:error, :invalid_request}
end
@doc """
Get the Confirmed Service request for the given Read Range Service.
See the `BACnet.Protocol.Services.Protocol` function documentation for more information.
"""
@spec to_apdu(t(), Keyword.t()) ::
{:ok, Protocol.APDU.ConfirmedServiceRequest.t()}
| {:error, term()}
def to_apdu(%__MODULE__{} = service, request_data) do
with :ok <-
(if service.property_identifier in [:all, :required, :optional] do
{:error, :invalid_property_identifier}
else
:ok
end),
{:ok, object_identifier, _header} <-
ApplicationTags.encode_value({:object_identifier, service.object_identifier}),
{:ok, property_identifier, _header} <-
ApplicationTags.encode_value(
{:enumerated,
Constants.by_name_atom(:property_identifier, service.property_identifier)}
),
{:ok, array_index} <-
(if service.property_array_index do
with {:ok, array_index, _header} <-
ApplicationTags.encode_value(
{:unsigned_integer, service.property_array_index}
) do
{:ok, {:tagged, {2, array_index, byte_size(array_index)}}}
end
else
{:ok, nil}
end),
{:ok, range} <- encode_range(service.range) do
parameters = [
{:tagged, {0, object_identifier, byte_size(object_identifier)}},
{:tagged, {1, property_identifier, byte_size(property_identifier)}},
array_index,
range
]
req = %Protocol.APDU.ConfirmedServiceRequest{
segmented_response_accepted: request_data[:segmented_response_accepted] || true,
max_segments: request_data[:max_segments] || :more_than_64,
max_apdu:
request_data[:max_apdu] ||
Constants.macro_by_name(:max_apdu_length_accepted_value, :octets_1476),
invoke_id: request_data[:invoke_id] || 0,
sequence_number: nil,
proposed_window_size: nil,
service: @service_name,
parameters: Enum.reject(parameters, &is_nil/1)
}
{:ok, req}
end
end
defp parse_range([
{:constructed,
{3,
[
unsigned_integer: ref_index,
signed_integer: count
], _len}}
| _rest
])
when is_valid_count(count),
do: {:ok, {:by_position, {ref_index, count}}}
defp parse_range([
{:constructed,
{6,
[
unsigned_integer: seq_number,
signed_integer: count
], _len}}
| _rest
])
when is_valid_count(count),
do: {:ok, {:by_seq_number, {seq_number, count}}}
defp parse_range([
{:constructed,
{7,
[
date: ref_date,
time: ref_time,
signed_integer: count
], _len}}
| _rest
])
when is_valid_count(count),
do: {:ok, {:by_time, {%Protocol.BACnetDateTime{date: ref_date, time: ref_time}, count}}}
# range missing
defp parse_range([]), do: {:ok, nil}
# unsupported context tag/payload
defp parse_range(_term), do: {:error, :invalid_range_param}
defp encode_range(nil), do: {:ok, nil}
defp encode_range({:by_position, {ref_index, count}})
when is_integer(ref_index) and is_valid_count(count) do
{:ok, {:constructed, {3, [unsigned_integer: ref_index, signed_integer: count], 0}}}
end
defp encode_range({:by_seq_number, {ref_seq_number, count}})
when is_integer(ref_seq_number) and is_valid_count(count) do
{:ok, {:constructed, {6, [unsigned_integer: ref_seq_number, signed_integer: count], 0}}}
end
defp encode_range({:by_time, {%Protocol.BACnetDateTime{} = ref_datetime, count}})
when is_valid_count(count) do
{:ok,
{:constructed,
{7, [date: ref_datetime.date, time: ref_datetime.time, signed_integer: count], 0}}}
end
defp encode_range(_term), do: {:error, :invalid_range}
defimpl Protocol.Services.Protocol do
alias BACnet.Protocol
alias BACnet.Protocol.Constants
require Constants
@spec get_name(@for.t()) :: atom()
def get_name(%@for{} = _service), do: @for.get_name()
@spec is_confirmed(@for.t()) :: boolean()
def is_confirmed(%@for{} = _service), do: @for.is_confirmed()
@spec to_apdu(@for.t(), Keyword.t()) ::
{:ok, Protocol.APDU.ConfirmedServiceRequest.t()}
| {:error, term()}
def to_apdu(%@for{} = service, request_data), do: @for.to_apdu(service, request_data)
end
end