Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/services/common.ex
defmodule BACnet.Protocol.Services.Common do
@moduledoc """
This module implements the parsing for some services, which are available as both confirmed
and unconfirmed. So instead of implementing the same parsing and encoding twice, this module
is the common ground for these services.
"""
alias BACnet.Protocol
alias BACnet.Protocol.APDU.ConfirmedServiceRequest
alias BACnet.Protocol.APDU.UnconfirmedServiceRequest
alias BACnet.Protocol.ApplicationTags
alias BACnet.Protocol.Constants
alias BACnet.Protocol.Services
import Protocol.Utility, only: [pattern_extract_tags: 4]
require Constants
@doc """
After encode, this function can be used to turn the request parameters into a service request.
This function is used in the `Services.*` modules. Any wrong usage can only be blamed onto the user themself.
"""
@spec after_encode_convert(
map(),
Keyword.t(),
module(),
Constants.confirmed_service_choice() | non_neg_integer()
) ::
{:ok, ConfirmedServiceRequest.t() | UnconfirmedServiceRequest.t()}
def after_encode_convert(request, request_data, service_type, service_name)
def after_encode_convert(
%{parameters: parameters} = _request,
request_data,
ConfirmedServiceRequest = _service_type,
service_name
) do
with segmented_response_accepted <-
Keyword.get(request_data, :segmented_response_accepted, true),
true <- is_boolean(segmented_response_accepted),
max_segments <- Keyword.get(request_data, :max_segments, :more_than_64),
true <-
is_integer(max_segments) or max_segments == :unspecified or
max_segments == :more_than_64,
max_apdu <-
Keyword.get(
request_data,
:max_apdu,
Constants.macro_by_name(:max_apdu_length_accepted_value, :octets_1476)
),
true <- is_integer(max_apdu),
invoke_id <- Keyword.get(request_data, :invoke_id, 0),
true <- is_integer(invoke_id) and invoke_id >= 0 do
req = %ConfirmedServiceRequest{
segmented_response_accepted: segmented_response_accepted,
max_segments: max_segments,
max_apdu: max_apdu,
invoke_id: invoke_id,
sequence_number: nil,
proposed_window_size: nil,
service: service_name,
parameters: parameters
}
{:ok, req}
else
false -> {:error, :invalid_parameters}
end
end
def after_encode_convert(
%{parameters: parameters} = _request,
_request_data,
UnconfirmedServiceRequest = _service_type,
service_name
) do
req = %UnconfirmedServiceRequest{
service: service_name,
parameters: parameters
}
{:ok, req}
end
def after_encode_convert(_request, _request_data, _service_type, _service_name) do
{:error, :invalid_parameters}
end
@doc """
Decodes the unconfirmed or confirmed cov notification service into a base map.
This function is used by the `ConfirmedCovNotification` and `UnconfirmedCovNotification` modules.
"""
@spec decode_cov_notification(
ConfirmedServiceRequest.t()
| UnconfirmedServiceRequest.t()
) :: {:ok, map()} | {:error, term()}
def decode_cov_notification(request)
when is_struct(request, ConfirmedServiceRequest) or
is_struct(request, UnconfirmedServiceRequest) do
with {:ok, process_identifier, rest} <-
pattern_extract_tags(
request.parameters,
{:tagged, {0, _t, _l}},
:unsigned_integer,
false
),
:ok <-
if(ApplicationTags.valid_int?(process_identifier, 32),
do: :ok,
else: {:error, :invalid_process_identifier_value}
),
{:ok, device_identifier, rest} <-
pattern_extract_tags(rest, {:tagged, {1, _t, _l}}, :object_identifier, false),
{:ok, object_identifier, rest} <-
pattern_extract_tags(rest, {:tagged, {2, _t, _l}}, :object_identifier, false),
{:ok, time_remaining, rest} <-
pattern_extract_tags(rest, {:tagged, {3, _t, _l}}, :unsigned_integer, false),
{:ok, {:constructed, {_t, propvalues_raw, _l}}, _rest} <-
pattern_extract_tags(rest, {:constructed, {4, _t, _l}}, nil, false),
{:ok, property_values} <- Protocol.PropertyValue.parse_all(propvalues_raw) do
event = %{
process_identifier: process_identifier,
initiating_device: device_identifier,
monitored_object: object_identifier,
time_remaining: time_remaining,
property_values: property_values
}
{:ok, event}
else
{:error, :missing_pattern} -> {:error, :invalid_request_parameters}
{:error, _err} = err -> err
end
end
@doc """
Encodes the unconfirmed or confirmed COV notification service into a base map.
This function is used by the `ConfirmedCovNotification` and `UnconfirmedCovNotification` modules.
"""
@spec encode_cov_notification(
Services.ConfirmedCovNotification.t()
| Services.UnconfirmedCovNotification.t(),
Keyword.t()
) :: {:ok, map()} | {:error, term()}
def encode_cov_notification(service, opts \\ [])
when is_struct(service, Services.ConfirmedCovNotification) or
is_struct(service, Services.UnconfirmedCovNotification) do
with :ok <-
if(ApplicationTags.valid_int?(service.process_identifier, 32),
do: :ok,
else: {:error, :invalid_process_identifier_value}
),
{:ok, process_identifier, _header} <-
ApplicationTags.encode_value({:unsigned_integer, service.process_identifier}),
{:ok, device_identifier, _header} <-
ApplicationTags.encode_value({:object_identifier, service.initiating_device}),
{:ok, object_identifier, _header} <-
ApplicationTags.encode_value({:object_identifier, service.monitored_object}),
{:ok, time_remaining, _header} <-
ApplicationTags.encode_value({:unsigned_integer, service.time_remaining}),
{:ok, propvalues} <-
Protocol.PropertyValue.encode_all(service.property_values, opts) do
params = [
{:tagged, {0, process_identifier, byte_size(process_identifier)}},
{:tagged, {1, device_identifier, byte_size(device_identifier)}},
{:tagged, {2, object_identifier, byte_size(object_identifier)}},
{:tagged, {3, time_remaining, byte_size(time_remaining)}},
{:constructed, {4, propvalues, 0}}
]
req = %{
parameters: params
}
{:ok, req}
end
end
@doc """
Decodes the unconfirmed or confirmed event notification service into a base map.
This function is used by the `ConfirmedEventNotification` and `UnconfirmedEventNotification` modules.
"""
@spec decode_event_notification(
ConfirmedServiceRequest.t()
| UnconfirmedServiceRequest.t()
) :: {:ok, map()} | {:error, term()}
def decode_event_notification(request)
when is_struct(request, ConfirmedServiceRequest) or
is_struct(request, UnconfirmedServiceRequest) do
with {:ok, process_identifier, rest} <-
pattern_extract_tags(
request.parameters,
{:tagged, {0, _t, _l}},
:unsigned_integer,
false
),
:ok <-
if(ApplicationTags.valid_int?(process_identifier, 32),
do: :ok,
else: {:error, :invalid_process_identifier_value}
),
{:ok, device_identifier, rest} <-
pattern_extract_tags(rest, {:tagged, {1, _t, _l}}, :object_identifier, false),
{:ok, object_identifier, rest} <-
pattern_extract_tags(rest, {:tagged, {2, _t, _l}}, :object_identifier, false),
{:ok, {:constructed, {_tag, timestamp_raw, _len}}, rest} <-
pattern_extract_tags(rest, {:constructed, {3, _t, _l}}, nil, false),
{:ok, {timestamp, _rest}} <- Protocol.BACnetTimestamp.parse(List.wrap(timestamp_raw)),
{:ok, notification_class, rest} <-
pattern_extract_tags(rest, {:tagged, {4, _t, _l}}, :unsigned_integer, false),
{:ok, priority, rest} <-
pattern_extract_tags(rest, {:tagged, {5, _t, _l}}, :unsigned_integer, false),
:ok <-
if(ApplicationTags.valid_int?(priority, 8),
do: :ok,
else: {:error, :invalid_priority_value}
),
{:ok, eventtype_raw, rest} <-
pattern_extract_tags(rest, {:tagged, {6, _t, _l}}, :unsigned_integer, false),
{:ok, eventtype} <-
Constants.by_value_with_reason(
:event_type,
eventtype_raw,
{:unknown_event_type, eventtype_raw}
),
{:ok, message_text, rest} <-
pattern_extract_tags(rest, {:tagged, {7, _t, _l}}, :character_string, true),
{:ok, notifytype_raw, rest} <-
pattern_extract_tags(rest, {:tagged, {8, _t, _l}}, :unsigned_integer, false),
{:ok, notifytype} <-
Constants.by_value_with_reason(
:notify_type,
notifytype_raw,
{:unknown_notify_type, notifytype_raw}
),
optional = notifytype == :ack_notification,
{:ok, ack_required, rest} <-
pattern_extract_tags(rest, {:tagged, {9, _t, _l}}, :boolean, optional),
{:ok, from_state_raw, rest} <-
pattern_extract_tags(rest, {:tagged, {10, _t, _l}}, :unsigned_integer, optional),
{:ok, from_state} <-
(case from_state_raw do
nil ->
{:ok, nil}
_term ->
Constants.by_value_with_reason(
:event_state,
from_state_raw,
{:unknown_event_state, from_state_raw}
)
end),
{:ok, to_state_raw, rest} <-
pattern_extract_tags(rest, {:tagged, {11, _t, _l}}, :unsigned_integer, false),
{:ok, to_state} <-
(case to_state_raw do
nil ->
{:ok, nil}
_term ->
Constants.by_value_with_reason(
:event_state,
to_state_raw,
{:unknown_event_state, to_state_raw}
)
end),
{:ok, eventvalues_raw, _rest} <-
pattern_extract_tags(rest, {:constructed, {12, _t, _l}}, nil, optional),
{:ok, event_values} <-
(case eventvalues_raw do
nil ->
{:ok, nil}
{:constructed, {_t, term, _l}} ->
Protocol.NotificationParameters.parse(term)
end) do
event = %{
process_identifier: process_identifier,
initiating_device: device_identifier,
event_object: object_identifier,
timestamp: timestamp,
notification_class: notification_class,
priority: priority,
event_type: eventtype,
message_text: message_text,
notify_type: notifytype,
ack_required: ack_required,
from_state: from_state,
to_state: to_state,
event_values: event_values
}
{:ok, event}
else
{:error, :missing_pattern} -> {:error, :invalid_request_parameters}
{:error, _err} = err -> err
end
end
@doc """
Encodes the unconfirmed or confirmed event notification service into a base map.
This function is used by the `ConfirmedEventNotification` and `UnconfirmedEventNotification` modules.
"""
@spec encode_event_notification(
Services.ConfirmedEventNotification.t()
| Services.UnconfirmedEventNotification.t(),
Keyword.t()
) :: {:ok, map()} | {:error, term()}
def encode_event_notification(service, opts \\ [])
when is_struct(service, Services.ConfirmedEventNotification) or
is_struct(service, Services.UnconfirmedEventNotification) do
with :ok <-
if(ApplicationTags.valid_int?(service.process_identifier, 32),
do: :ok,
else: {:error, :invalid_process_identifier_value}
),
:ok <-
if(ApplicationTags.valid_int?(service.priority, 8),
do: :ok,
else: {:error, :invalid_priority_value}
),
{:ok, process_identifier, _header} <-
ApplicationTags.encode_value({:unsigned_integer, service.process_identifier}),
{:ok, device_identifier, _header} <-
ApplicationTags.encode_value({:object_identifier, service.initiating_device}),
{:ok, object_identifier, _header} <-
ApplicationTags.encode_value({:object_identifier, service.event_object}),
{:ok, [timestamp]} <- Protocol.BACnetTimestamp.encode(service.timestamp),
{:ok, notification_class, _header} <-
ApplicationTags.encode_value({:unsigned_integer, service.notification_class}),
{:ok, priority, _header} <-
ApplicationTags.encode_value({:unsigned_integer, service.priority}),
{:ok, event_type_c} <-
Constants.by_name_with_reason(
:event_type,
service.event_type,
{:unknown_event_type, service.event_type}
),
{:ok, eventtype, _header} <-
ApplicationTags.encode_value({:unsigned_integer, event_type_c}),
{:ok, message_text} <-
(if service.message_text do
case ApplicationTags.encode_value({:character_string, service.message_text}) do
{:ok, message_text, _header} ->
{:ok, {:tagged, {7, message_text, byte_size(message_text)}}}
term ->
term
end
else
{:ok, nil}
end),
{:ok, notifytype_c} <-
Constants.by_name_with_reason(
:notify_type,
service.notify_type,
{:unknown_notify_type, service.notify_type}
),
{:ok, notifytype, _header} <-
ApplicationTags.encode_value({:unsigned_integer, notifytype_c}),
{:ok, ack_required} <-
(if service.notify_type != :ack_notification do
case ApplicationTags.encode_value({:unsigned_integer, service.ack_required}) do
{:ok, ack_required, _header} ->
{:ok, {:tagged, {9, ack_required, byte_size(ack_required)}}}
term ->
term
end
else
{:ok, nil}
end),
{:ok, from_state} <-
(if service.notify_type != :ack_notification and service.from_state do
with {:ok, from_state_c} <-
Constants.by_name_with_reason(
:event_state,
service.from_state,
{:unknown_event_state, service.from_state}
),
{:ok, from_state, _header} <-
ApplicationTags.encode_value({:unsigned_integer, from_state_c}) do
{:ok, {:tagged, {10, from_state, byte_size(from_state)}}}
end
else
{:ok, nil}
end),
{:ok, to_state_c} <-
Constants.by_name_with_reason(
:event_state,
service.to_state,
{:unknown_event_state, service.to_state}
),
{:ok, to_state, _header} <-
ApplicationTags.encode_value({:unsigned_integer, to_state_c}),
{:ok, event_values} <-
(if service.event_values do
case Protocol.NotificationParameters.encode(service.event_values, opts) do
{:ok, tags} -> {:ok, {:constructed, {12, tags, 0}}}
term -> term
end
else
{:ok, nil}
end) do
params = [
{:tagged, {0, process_identifier, byte_size(process_identifier)}},
{:tagged, {1, device_identifier, byte_size(device_identifier)}},
{:tagged, {2, object_identifier, byte_size(object_identifier)}},
{:constructed, {3, timestamp, 0}},
{:tagged, {4, notification_class, byte_size(notification_class)}},
{:tagged, {5, priority, byte_size(priority)}},
{:tagged, {6, eventtype, byte_size(eventtype)}},
message_text,
{:tagged, {8, notifytype, byte_size(notifytype)}},
ack_required,
from_state,
{:tagged, {11, to_state, byte_size(to_state)}},
event_values
]
req = %{
parameters: Enum.reject(params, &is_nil/1)
}
{:ok, req}
end
end
@doc """
Decodes the unconfirmed or confirmed private transfer service into a base map.
This function is used by the `ConfirmedPrivateTransfer` and `UnconfirmedPrivateTransfer` modules.
"""
@spec decode_private_transfer(
ConfirmedServiceRequest.t()
| UnconfirmedServiceRequest.t()
) :: {:ok, map()} | {:error, term()}
def decode_private_transfer(request)
when is_struct(request, ConfirmedServiceRequest) or
is_struct(request, UnconfirmedServiceRequest) do
with {:ok, vendor, rest} <-
pattern_extract_tags(
request.parameters,
{:tagged, {0, _v, _l}},
:unsigned_integer,
false
),
:ok <-
if(ApplicationTags.valid_int?(vendor, 16),
do: :ok,
else: {:error, :invalid_vendor_id_value}
),
{:ok, service, rest} <-
pattern_extract_tags(rest, {:tagged, {1, _v, _l}}, :unsigned_integer, false),
{:ok, parameters, _rest} <-
pattern_extract_tags(rest, {:constructed, {2, _v, _l}}, nil, true) do
transfer = %{
vendor_id: vendor,
service_number: service,
parameters:
case parameters do
{:constructed, {2, params, _l}} ->
Enum.map(params, &ApplicationTags.Encoding.create!(&1))
_else ->
nil
end
}
{:ok, transfer}
else
{:error, :missing_pattern} -> {:error, :invalid_request_parameters}
{:error, _err} = err -> err
end
end
@doc """
Encodes the unconfirmed or confirmed private transfer service into a base map.
This function is used by the `ConfirmedPrivateTransfer` and `UnconfirmedPrivateTransfer` modules.
"""
@spec encode_private_transfer(
Services.ConfirmedPrivateTransfer.t()
| Services.UnconfirmedPrivateTransfer.t(),
Keyword.t()
) :: {:ok, map()} | {:error, term()}
def encode_private_transfer(service, opts \\ [])
when is_struct(service, Services.ConfirmedPrivateTransfer) or
is_struct(service, Services.UnconfirmedPrivateTransfer) do
with :ok <-
if(ApplicationTags.valid_int?(service.vendor_id, 16),
do: :ok,
else: {:error, :invalid_vendor_id_value}
),
{:ok, vendor_id, _header} <-
ApplicationTags.encode_value({:unsigned_integer, service.vendor_id}, opts),
{:ok, service_num, _header} <-
ApplicationTags.encode_value({:unsigned_integer, service.service_number}, opts),
{:ok, parameters} <-
(if service.parameters do
with {:ok, params} <-
Enum.reduce_while(service.parameters, {:ok, []}, fn param, {:ok, acc} ->
case ApplicationTags.Encoding.to_encoding(param) do
{:ok, enc} -> {:cont, {:ok, [enc | acc]}}
term -> {:halt, term}
end
end) do
{:ok, {:constructed, {2, Enum.reverse(params), 0}}}
end
else
{:ok, nil}
end) do
vendor_size = byte_size(vendor_id)
service_size = byte_size(service_num)
req = %{
parameters:
Enum.reject(
[
{:tagged, {0, vendor_id, vendor_size}},
{:tagged, {1, service_num, service_size}},
parameters
],
&is_nil/1
)
}
{:ok, req}
end
end
@doc """
Decodes the unconfirmed or confirmed text message service into a base map.
This function is used by the `ConfirmedTextMessage` and `UnconfirmedTextMessage` modules.
"""
@spec decode_text_message(
ConfirmedServiceRequest.t()
| UnconfirmedServiceRequest.t()
) :: {:ok, map()} | {:error, term()}
def decode_text_message(request)
when is_struct(request, ConfirmedServiceRequest) or
is_struct(request, UnconfirmedServiceRequest) do
with {:ok, source_device, rest} <-
pattern_extract_tags(
request.parameters,
{:tagged, {0, _c, _l}},
:object_identifier,
false
),
{:ok, message_class_raw, rest} <-
pattern_extract_tags(rest, {:constructed, {1, _c, _l}}, nil, true),
{:ok, message_class} <-
(case message_class_raw do
nil ->
{:ok, nil}
{:constructed, {1, message_class_raw, _l}} ->
with {:ok, message_class_num, _rest} <-
pattern_extract_tags(
List.wrap(message_class_raw),
{:tagged, {0, _c, _l}},
:unsigned_integer,
true
),
{:ok, message_class_str, _rest} <-
pattern_extract_tags(
List.wrap(message_class_raw),
{:tagged, {1, _c, _l}},
:character_string,
true
),
nil <-
(unless message_class_num || message_class_str do
{:error, :invalid_request_parameters}
end),
do: {:ok, message_class_num || message_class_str}
end),
{:ok, message_priority, rest} <-
pattern_extract_tags(rest, {:tagged, {2, _c, _l}}, :enumerated, false),
{:ok, message, _rest} <-
pattern_extract_tags(rest, {:tagged, {3, _c, _l}}, :character_string, false) do
textmsg = %{
source_device: source_device,
class: message_class,
priority: enum_to_priority(message_priority),
message: message
}
{:ok, textmsg}
else
{:error, :missing_pattern} -> {:error, :invalid_request_parameters}
{:error, _err} = err -> err
end
end
@doc """
Encodes the unconfirmed or confirmed text message service into a base map.
This function is used by the `ConfirmedTextMessage` and `UnconfirmedTextMessage` modules.
"""
@spec encode_text_message(
Services.ConfirmedTextMessage.t()
| Services.UnconfirmedTextMessage.t(),
Keyword.t()
) :: {:ok, map()} | {:error, term()}
def encode_text_message(request, _opts \\ [])
when is_struct(request, Services.ConfirmedTextMessage) or
is_struct(request, Services.UnconfirmedTextMessage) do
with {:ok, device, _header} <-
ApplicationTags.encode_value({:object_identifier, request.source_device}),
{:ok, message_class} <-
(case request.class do
nil ->
{:ok, nil}
_term ->
{type, tag} =
if is_integer(request.class) do
{:unsigned_integer, 0}
else
{:character_string, 1}
end
case ApplicationTags.encode_value({type, request.class}) do
{:ok, val, _header} ->
{:ok, {:constructed, {1, {:tagged, {tag, val, byte_size(val)}}, 0}}}
term ->
term
end
end),
{:ok, priority, _header} <-
ApplicationTags.encode_value({:enumerated, priority_to_enum(request.priority)}),
{:ok, message, _header} <-
ApplicationTags.encode_value({:character_string, request.message}) do
parameters = [
{:tagged, {0, device, byte_size(device)}},
message_class,
{:tagged, {2, priority, byte_size(priority)}},
{:tagged, {3, message, byte_size(message)}}
]
req = %{
parameters: Enum.reject(parameters, &is_nil/1)
}
{:ok, req}
end
end
#### Helpers ####
@spec enum_to_priority(0 | 1) :: :normal | :urgent
defp enum_to_priority(0), do: :normal
defp enum_to_priority(1), do: :urgent
@spec priority_to_enum(:normal | :urgent) :: 0 | 1
defp priority_to_enum(:normal), do: 0
defp priority_to_enum(:urgent), do: 1
end