Skip to content

Commit 5a98999

Browse files
author
CI
committed
Sync to GitHub
1 parent 6839a3f commit 5a98999

8 files changed

Lines changed: 771 additions & 666 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@ BACstack is a low-level Elixir implementation for the ASHRAE standard 135, BACne
44
This implementation supports ASHRAE 135-xxxx and BACnet/IPv4. Support for other transport layers (such as BACnet/SC)
55
can be straight forward added on top of it.
66

7-
BACnet/Ethernet is a tested transport (but not supported on Windows),
8-
but rarely seen in commercial devices since BACnet/IPv4 is the better and superior choice.
9-
BACnet/IPv4 is the most mature transport and also most widely supported one in general (next to MS/TP).
10-
BACnet/MS/TP is an experimental transport that requires [circuits_uart](https://hex.pm/packages/circuits_uart)
11-
and an USB RS485 adapter (or any serial device/tty supported by circuits_uart).
12-
137
<!-- TODO: Update year in ASHRAE 135-xxxx -->
148

159
As this is a low-level implementation, users of this library are required to do the heavy-lifting of the BACnet stack,
@@ -18,6 +12,12 @@ such as automatically replying to Who-Is services, applying hard application tim
1812
<!--If you're looking for a high level Elixir abstraction on top of this library, check out [BACnex].
1913
BACnex is a high level abstraction on top of this library, that offers the high level features of a regular BACnet stack.-->
2014

15+
Currently implemented transport layers:
16+
- BACnet/Ethernet: Rarely seen in commercial devices since BACnet/IPv4 is the better and superior choice
17+
- BACnet/IPv4: The most mature transport and also most widely supported one in general (next to MS/TP)
18+
- BACnet/MS/TP: An experimental transport that requires [circuits_uart](https://hex.pm/packages/circuits_uart)
19+
and an USB RS485 adapter (or any serial device/tty supported by circuits_uart)
20+
2121
## Installation
2222

2323
While v0.0.1 has been released to [Hex](https://hex.pm/packages/bacstack) to provide a minimal BACnet client featureset,

lib/bacnet/protocol/object_types/device.ex

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ defmodule BACnet.Protocol.ObjectTypes.Device do
4747
alias BACnet.Protocol.Device.ServicesSupported
4848
alias BACnet.Protocol.ObjectIdentifier
4949
alias BACnet.Protocol.ObjectsMacro
50+
alias BACnet.Protocol.ObjectsUtility.Internal
5051
alias BACnet.Protocol.Recipient
5152

5253
require Constants
@@ -164,33 +165,14 @@ defmodule BACnet.Protocol.ObjectTypes.Device do
164165
# Properties for when the device is capable of tracking date and time
165166
field(:local_date, BACnetDate.t(),
166167
readonly: true,
167-
init_fun: &BACnetDate.utc_today/0,
168-
annotation: [
169-
# Maybe we want to use DateTime.now/1 with the correct TimeZone? (the configured default)
170-
# We use DateTime instead of Date so when shifting the time, we get the correct date
171-
on_read_function:
172-
&update_property(
173-
&1,
174-
:local_date,
175-
BACnetDate.from_date(
176-
DateTime.to_date(DateTime.add(DateTime.utc_now(), -(&1[:utc_offset] || 0), :minute))
177-
)
178-
)
179-
]
168+
init_fun: &Internal.init_fun_local_date/0,
169+
annotation: [on_read_function: &update_local_date/1]
180170
)
181171

182172
field(:local_time, BACnetTime.t(),
183173
readonly: true,
184-
init_fun: &BACnetTime.utc_now/0,
185-
annotation: [
186-
# Maybe we want to use DateTime.now/1 with the correct TimeZone? (the configured default)
187-
on_read_function:
188-
&update_property(
189-
&1,
190-
:local_time,
191-
BACnetTime.from_time(Time.add(Time.utc_now(), -(&1[:utc_offset] || 0), :minute))
192-
)
193-
]
174+
init_fun: &Internal.init_fun_local_time/0,
175+
annotation: [on_read_function: &update_local_time/1]
194176
)
195177

196178
# Properties for COV reporting service
@@ -344,6 +326,29 @@ defmodule BACnet.Protocol.ObjectTypes.Device do
344326

345327
defp map_vendor_to_name(_id), do: ""
346328

329+
@spec update_local_date(t()) :: {:ok, t()} | {:error, term()}
330+
defp update_local_date(%__MODULE__{} = obj) do
331+
# Maybe we want to use DateTime.now/1 with the correct TimeZone? (the configured default)
332+
# We use DateTime instead of Date so when shifting the time, we get the correct date
333+
update_property(
334+
obj,
335+
:local_date,
336+
BACnetDate.from_date(
337+
DateTime.to_date(DateTime.add(DateTime.utc_now(), -(obj[:utc_offset] || 0), :minute))
338+
)
339+
)
340+
end
341+
342+
@spec update_local_time(t()) :: {:ok, t()} | {:error, term()}
343+
defp update_local_time(%__MODULE__{} = obj) do
344+
# Maybe we want to use DateTime.now/1 with the correct TimeZone? (the configured default)
345+
update_property(
346+
obj,
347+
:local_time,
348+
BACnetTime.from_time(Time.add(Time.utc_now(), -(obj[:utc_offset] || 0), :minute))
349+
)
350+
end
351+
347352
@spec inhibit_object_check(t()) :: {:ok, t()}
348353
defp inhibit_object_check(obj) do
349354
# Only patch vendor name if empty

lib/bacnet/protocol/objects_utility/internal.ex

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
11
defmodule BACnet.Protocol.ObjectsUtility.Internal do
22
@moduledoc false
33

4-
alias BACnet.Protocol
54
alias BACnet.Protocol.BACnetArray
5+
alias BACnet.Protocol.BACnetDate
6+
alias BACnet.Protocol.BACnetTime
7+
alias BACnet.Protocol.DailySchedule
68

9+
@spec init_fun_schedule_weekly_schedule() :: BACnetArray.t(DailySchedule.t())
710
def init_fun_schedule_weekly_schedule() do
8-
BACnetArray.new(7, %Protocol.DailySchedule{schedule: []})
11+
BACnetArray.new(7, %DailySchedule{schedule: []})
12+
end
13+
14+
@spec init_fun_local_date() :: BACnetDate.t()
15+
def init_fun_local_date() do
16+
BACnetDate.from_date(
17+
DateTime.to_date(
18+
DateTime.now!(
19+
Application.get_env(:bacstack, :default_timezone, "Etc/UTC"),
20+
Calendar.get_time_zone_database()
21+
)
22+
)
23+
)
24+
end
25+
26+
@spec init_fun_local_time() :: BACnetTime.t()
27+
def init_fun_local_time() do
28+
BACnetTime.from_time(
29+
DateTime.to_time(
30+
DateTime.now!(
31+
Application.get_env(:bacstack, :default_timezone, "Etc/UTC"),
32+
Calendar.get_time_zone_database()
33+
)
34+
)
35+
)
936
end
1037

1138
defmodule ReadPropertyAckTransformOptions do

lib/bacnet/stack/client.ex

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,12 @@ defmodule BACnet.Stack.Client do
510510
including replies sent to requests that were never meant for that request.
511511
- `max_apdu_length: pos_integer()` - Optional. The maximum APDU length
512512
the remote BACnet device supports (defaults to the transport max APDU length).
513+
Limited by the transport max (extended) APDU length.
514+
If the transport supports an extended APDU length, you need to make sure
515+
the transport uses it, if it needs to be explicitely enabled.
516+
For calculation, always the NPDU length is used, as it expresses how many
517+
bytes for the APDU can actually be used without constraining the transport's
518+
actual maximum transmission unit.
513519
- `max_segments: pos_integer()` - Optional. Maximum amount of segments the
514520
remote BACnet device can accept (defaults to 2).
515521
- `segmentation_supported: Constants.segmentation()` - Optional. Which kind
@@ -1902,11 +1908,21 @@ defmodule BACnet.Stack.Client do
19021908
# Catch any errors when trying to encode the APDU
19031909
bin = EncoderProtocol.encode(apdu)
19041910

1905-
# 50 is the minimum APDU size each device needs to support
19061911
max_apdu_len =
19071912
case Keyword.fetch(opts, :max_apdu_length) do
1908-
{:ok, val} -> min(max(val, 50), trans_mod.max_npdu_length())
1909-
_error -> trans_mod.max_npdu_length()
1913+
{:ok, val} ->
1914+
max_npdu =
1915+
if function_exported?(trans_mod, :max_ext_npdu_length, 0) do
1916+
trans_mod.max_ext_npdu_length()
1917+
else
1918+
trans_mod.max_npdu_length()
1919+
end
1920+
1921+
# 50 is the minimum APDU size each device needs to support
1922+
min(max(val, 50), max_npdu)
1923+
1924+
_error ->
1925+
trans_mod.max_npdu_length()
19101926
end
19111927

19121928
{bin, max_apdu_len}

lib/bacnet/stack/transport/mstp_transport.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ if Code.ensure_loaded?(Circuits.UART) do
1616
```
1717
As defined by `t:BACnet.Stack.TransportBehaviour.transport_cb_frame/0`.
1818
19+
Note: Currently `max_apdu_length/0` and `max_npdu_length/0` always return the max APDU as per 135-2012
20+
and not the higher possible APDU as per 135-2016.
21+
1922
It uses `Circuits.UART` to handle RS485 for us in active mode.
2023
If you want to use this transport, you'll have to add [`:circuits_uart`](https://hex.pm/packages/circuits_uart)
2124
to your `mix.exs` as dependency! It is an optional dependency and thus
@@ -375,6 +378,13 @@ if Code.ensure_loaded?(Circuits.UART) do
375378
@spec max_apdu_length() :: pos_integer()
376379
def max_apdu_length(), do: @max_apdu
377380

381+
@doc """
382+
Get the maximum extended APDU length for this transport,
383+
if the transport also supports a higher (extended) APDU.
384+
"""
385+
@spec max_ext_apdu_length() :: pos_integer()
386+
def max_ext_apdu_length(), do: @max_apdu_extended
387+
378388
@doc """
379389
Get the maximum NPDU length for this transport.
380390
@@ -388,6 +398,20 @@ if Code.ensure_loaded?(Circuits.UART) do
388398
@spec max_npdu_length() :: pos_integer()
389399
def max_npdu_length(), do: @max_apdu
390400

401+
@doc """
402+
Get the maximum extended NPDU length for this transport,
403+
if the transport also supports a higher (extended) NPDU.
404+
405+
The NPDU length contains the maximum transmittable size
406+
of the NPDU, including the APDU, without violating
407+
the maximum transmission unit of the underlying transport.
408+
409+
Any necessary transport header (i.e. BVLL, LLC) must have
410+
been taken into account when calculating this number.
411+
"""
412+
@spec max_ext_npdu_length() :: pos_integer()
413+
def max_ext_npdu_length(), do: @max_apdu_extended
414+
391415
@doc """
392416
Opens/starts the Transport module. A process is started, that is linked to the caller process.
393417

0 commit comments

Comments
 (0)