Skip to content

Commit 33a006d

Browse files
author
CI
committed
Sync to GitHub
1 parent ca22ca2 commit 33a006d

3 files changed

Lines changed: 172 additions & 69 deletions

File tree

lib/bacnet/stack/segmentator.ex

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -493,27 +493,55 @@ defmodule BACnet.Stack.Segmentator do
493493

494494
Telemetry.execute_segmentator_sequence_start(self(), new_sequence, state)
495495

496-
# Send first segment and wait for Segment ACK
497-
case module.send(portal, destination, Map.get(segments, 0), opts) do
496+
# If the transport module support reply_postponed/3, send Reply-Postponed Frame
497+
# ASHRAE 135 Clause 9.8 (it is unlikely another transport layer implements it for any other reason)
498+
postponed_result =
499+
if function_exported?(module, :reply_postponed, 3) do
500+
case module.reply_postponed(portal, destination, []) do
501+
:ok ->
502+
:ok
503+
504+
# An error like :slave_mode is not continuable,
505+
# as only a master node can hold the token,
506+
# and we need the token for segmentation
507+
{:error, continuable}
508+
when continuable in [:no_reply_pending, :destination_is_not_expecting_reply] ->
509+
:ok
510+
511+
{:error, _err} = err ->
512+
err
513+
end
514+
else
515+
:ok
516+
end
517+
518+
case postponed_result do
498519
:ok ->
499-
new_state = %State{
500-
state
501-
| sequences: Map.put(state.sequences, id, new_sequence)
502-
}
503-
504-
{:ok, new_state}
505-
506-
{:error, _err} = error ->
507-
log_transport_send_error(error)
508-
509-
Telemetry.execute_segmentator_sequence_stop(
510-
self(),
511-
new_sequence,
512-
:transport_error,
513-
state
514-
)
515-
516-
{error, state}
520+
# Send first segment and wait for Segment ACK
521+
case module.send(portal, destination, Map.get(segments, 0), opts) do
522+
:ok ->
523+
new_state = %State{
524+
state
525+
| sequences: Map.put(state.sequences, id, new_sequence)
526+
}
527+
528+
{:ok, new_state}
529+
530+
{:error, _err} = error ->
531+
log_transport_send_error(error)
532+
533+
Telemetry.execute_segmentator_sequence_stop(
534+
self(),
535+
new_sequence,
536+
:transport_error,
537+
state
538+
)
539+
540+
{error, state}
541+
end
542+
543+
_other ->
544+
{postponed_result, state}
517545
end
518546
else
519547
# Too many segments for the remote device, send BUFFER_OVERFLOW Abort

lib/bacnet/stack/transport/mstp_transport.ex

Lines changed: 114 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,14 @@ if Code.ensure_loaded?(Circuits.UART) do
620620
GenServer.call(transport, {:is_destination_routed, destination})
621621
end
622622

623+
@doc """
624+
Verifies whether the given destination is valid for the transport module.
625+
"""
626+
@spec is_valid_destination(destination_address()) :: boolean()
627+
def is_valid_destination(destination) do
628+
is_integer(destination) and destination >= 0 and destination <= 255
629+
end
630+
623631
@doc """
624632
Sends data to the BACnet network.
625633
@@ -792,11 +800,29 @@ if Code.ensure_loaded?(Circuits.UART) do
792800
end
793801

794802
@doc """
795-
Verifies whether the given destination is valid for the transport module.
803+
Sends a Reply-Postponed Frame to the destination.
804+
805+
Sending an explicit Reply-Postponed Frame is necessary,
806+
when the reply is to be segmented.
807+
A segmented Complex-ACK APDU can only be transmitted
808+
when we hold the token (ASHRAE 135 Clause 9.8).
809+
810+
There are no options at this time.
796811
"""
797-
@spec is_valid_destination(destination_address()) :: boolean()
798-
def is_valid_destination(destination) do
799-
is_integer(destination) and destination >= 0 and destination <= 255
812+
@spec reply_postponed(
813+
portal :: TransportBehaviour.portal(),
814+
destination :: source_address(),
815+
opts :: Keyword.t()
816+
) ::
817+
:ok
818+
| {:error, term()}
819+
| {:error, :slave_mode}
820+
| {:error, :no_reply_pending}
821+
| {:error, :destination_is_not_expecting_reply}
822+
def reply_postponed(portal, destination, opts \\ [])
823+
when is_server(portal) and is_integer(destination) and destination >= 0 and
824+
destination <= 254 and is_list(opts) do
825+
GenServer.call(portal, {:reply_postponed, destination, opts}, @call_timeout)
800826
end
801827

802828
@doc false
@@ -1311,6 +1337,63 @@ if Code.ensure_loaded?(Circuits.UART) do
13111337
{:stop, :normal, :ok, state}
13121338
end
13131339

1340+
def handle_call(:get_state, _from, %State{} = state) do
1341+
log_debug(fn ->
1342+
"BacMstpTransport: Received get_state request"
1343+
end)
1344+
1345+
{:reply, state.transport_state, state}
1346+
end
1347+
1348+
def handle_call(:disable_token_passing, _from, %State{local_address: local_addr} = state)
1349+
when local_addr < @min_slave_addr do
1350+
log_debug(fn ->
1351+
"BacMstpTransport: Received disable_token_passing request during state " <>
1352+
String.upcase(Atom.to_string(state.transport_state))
1353+
end)
1354+
1355+
new_state = %{state | disable_token_passing: true}
1356+
1357+
{:reply, :ok, new_state}
1358+
end
1359+
1360+
def handle_call(:disable_token_passing, _from, %State{} = state) do
1361+
log_debug(fn ->
1362+
"BacMstpTransport: Received disable_token_passing reques in slave_mode"
1363+
end)
1364+
1365+
{:reply, {:error, :slave_mode}, state}
1366+
end
1367+
1368+
def handle_call({:configure, %{} = opts}, _from, %State{} = state) do
1369+
log_debug(fn -> "BacMstpTransport: Received configure request" end)
1370+
1371+
new_opts = Map.merge(state.opts, opts)
1372+
1373+
reply =
1374+
case Map.fetch(opts, :baudrate) do
1375+
{:ok, baudrate} ->
1376+
with :ok <- UART.configure(state.uart_pid, speed: baudrate) do
1377+
ReceiveFSM.configure(state.receive_fsm, %{
1378+
baudrate: baudrate,
1379+
log_communication: new_opts.log_communication_rcv
1380+
})
1381+
end
1382+
1383+
:error ->
1384+
:ok
1385+
end
1386+
1387+
case reply do
1388+
:ok ->
1389+
new_state = %{state | opts: new_opts}
1390+
{:reply, :ok, new_state}
1391+
1392+
_other ->
1393+
{:reply, reply, state}
1394+
end
1395+
end
1396+
13141397
def handle_call(:get_local_address, _from, %State{} = state) do
13151398
log_debug("BacMstpTransport: Received get_local_address request")
13161399

@@ -1357,7 +1440,7 @@ if Code.ensure_loaded?(Circuits.UART) do
13571440

13581441
{reply, new_state} =
13591442
send_frame_data_not_expecting_reply(
1360-
%{state | transport_state: :idle},
1443+
%{state | transport_state: :idle, answer_invoke_id: nil},
13611444
destination,
13621445
data
13631446
)
@@ -1483,63 +1566,46 @@ if Code.ensure_loaded?(Circuits.UART) do
14831566
{:reply, {:error, :slave_mode}, state}
14841567
end
14851568

1486-
def handle_call(:get_state, _from, %State{} = state) do
1569+
def handle_call(
1570+
{:reply_postponed, destination, _opts},
1571+
_from,
1572+
%State{local_address: local_addr, state_machine: %{source_address: source}} = state
1573+
)
1574+
when local_addr < @min_slave_addr do
14871575
log_debug(fn ->
1488-
"BacMstpTransport: Received get_state request"
1576+
"BacMstpTransport: Received reply_postponed request"
14891577
end)
14901578

1491-
{:reply, state.transport_state, state}
1492-
end
1579+
{reply, new_state} =
1580+
cond do
1581+
source != destination ->
1582+
{{:error, :destination_is_not_expecting_reply}, state}
14931583

1494-
def handle_call(:disable_token_passing, _from, %State{local_address: local_addr} = state)
1495-
when local_addr < @min_slave_addr do
1496-
log_debug(fn ->
1497-
"BacMstpTransport: Received disable_token_passing request during state " <>
1498-
String.upcase(Atom.to_string(state.transport_state))
1499-
end)
1584+
state.transport_state == :answer_data_request ->
1585+
# Remove the reply timer
1586+
state = state_cancel_silence_timer(state)
1587+
state = state_set_silence_timer(state, :timer_lost_token, @param_t_no_token)
15001588

1501-
new_state = %{state | disable_token_passing: true}
1589+
case send_frame_reply_postponed(state, destination) do
1590+
{:ok, state} -> {:ok, %{state | transport_state: :idle, answer_invoke_id: nil}}
1591+
{:error, state} -> {{:error, :sending_failed}, state}
1592+
end
15021593

1503-
{:reply, :ok, new_state}
1594+
true ->
1595+
{{:error, :no_reply_pending}, state}
1596+
end
1597+
1598+
{:reply, reply, new_state}
15041599
end
15051600

1506-
def handle_call(:disable_token_passing, _from, %State{} = state) do
1601+
def handle_call({:reply_postponed, _destination, _opts}, _from, %State{} = state) do
15071602
log_debug(fn ->
1508-
"BacMstpTransport: Received disable_token_passing reques in slave_mode"
1603+
"BacMstpTransport: Received reply_postponed request in slave mode"
15091604
end)
15101605

15111606
{:reply, {:error, :slave_mode}, state}
15121607
end
15131608

1514-
def handle_call({:configure, %{} = opts}, _from, %State{} = state) do
1515-
log_debug(fn -> "BacMstpTransport: Received configure request" end)
1516-
1517-
new_opts = Map.merge(state.opts, opts)
1518-
1519-
reply =
1520-
case Map.fetch(opts, :baudrate) do
1521-
{:ok, baudrate} ->
1522-
with :ok <- UART.configure(state.uart_pid, speed: baudrate) do
1523-
ReceiveFSM.configure(state.receive_fsm, %{
1524-
baudrate: baudrate,
1525-
log_communication: new_opts.log_communication_rcv
1526-
})
1527-
end
1528-
1529-
:error ->
1530-
:ok
1531-
end
1532-
1533-
case reply do
1534-
:ok ->
1535-
new_state = %{state | opts: new_opts}
1536-
{:reply, :ok, new_state}
1537-
1538-
_other ->
1539-
{:reply, reply, state}
1540-
end
1541-
end
1542-
15431609
def handle_call(_call, _from, state) do
15441610
{:noreply, state}
15451611
end

lib/bacnet/stack/transport_behaviour.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,16 @@ defmodule BACnet.Stack.TransportBehaviour do
211211
opts :: Keyword.t()
212212
) :: :ok | {:error, term()}
213213

214-
@optional_callbacks max_ext_apdu_length: 0, max_ext_npdu_length: 0
214+
@doc """
215+
Sends a Reply-Postponed Frame to the destination, if supported by the Transport.
216+
217+
For some transports this is necessary in some situations, as such this is an optional callback,
218+
with undefined options.
219+
"""
220+
@callback reply_postponed(portal :: portal(), destination :: term(), opts :: Keyword.t()) ::
221+
:ok | {:error, term()}
222+
223+
@optional_callbacks max_ext_apdu_length: 0, max_ext_npdu_length: 0, reply_postponed: 3
215224

216225
@doc """
217226
Produces a supervisor child spec based on the BACnet transport `open` callback, as such

0 commit comments

Comments
 (0)