The BACnet transport for BACnet MS/TP (Master-Slave/Token Passing) on a physical two wire electrical bus system called EIA-485 (RS485).
This transport should be considered experimental, but it is fairly tested with BACnet certified devices and BACnet stack C open source implementation.
This transport implementation supports ASHRAE 135-2016 and as such COBS encoding to allow frames up to 1476 bytes. When sending such large APDUs the receiving device must also support ASHRAE 135-2016, otherwise it will ignore us, and that's a sad thing to do! As such, sending large APDUs is opt-in.
It can act as a master or slave node, depending on the address this transport is started with. As a slave node, it'll only be able to respond to request and never be able to initiate requests. Master nodes actively participate in the Token Passing and are able to send MS/TP frames freely when holding the token. Slave nodes are restricted to only responding to requests and also are not able to use segmentation, since segmentation requires nodes to be able to send MS/TP frames whenever needed.
The recommendation is to always start this transport as master node with a distinct and
unique address in the range 0..127. Note that other MS/TP nodes max_master_address
needs to be considered when selecting an address.
Proprietary frames are sent to the defined callback as:
{:proprietary, {type :: 128..255, vendor_id :: 0..65_535}, destination :: destination_address(), data :: iodata()}As defined by BACnet.Stack.TransportBehaviour.transport_cb_frame/0.
It uses Circuits.UART to handle RS485 for us in active mode.
If you want to use this transport, you'll have to add :circuits_uart
to your mix.exs as dependency! It is an optional dependency and thus
by default not present when you install this library.
Autobaud
This transport implements automatically detecting the used baudrate by listening to the network ("autobaud").
Once it detects a valid BACnet frame, the baudrate that detected the frame will be used and autobaud will be disabled.
If there's no valid BACnet frame in a short time window (~5.5s) or an invalid frame is detected,
then the next baudrate will be tried. It will go through all defined baudrates specified by the BACnet protocol.
If no valid BACnet frame has been detected and all baudrates have been tested, the transport will fallback
to baudrate 38_400 - a Logger warning will be issued.
Autobaud can be manually re-enabled through configure/2 - note that all communication will be unrecoverable dropped!
The following baudrates will be tried in this order: 9600, 19_200, 38_400, 57_600, 76_800, 115_200.
Autobaud can be used by specifying baudrate: :auto when starting the transport (recommended way to use autobaud).
Autobaud can also be manually enabled through configure/2 - but communication is disruptive and thus not recommended.
Empty Network
Autobaud requires at least one active device on the MS/TP network! If there are no active devices (other than itself) on the network, it will fail to detect the baudrate and fallback to the default.
Logger warning spam due to bad data/devices/network
This section is only relevant if you're working on this project or have bacstack debugging enabled.
If you have bad devices or the MS/TP network has some physical troubles and the received data is invalid (CRC mismatch), then the specific module used for the receive state machine will log warnings. You may want to silence those, if you are not interested in those.
You can do this at runtime using
Logger.put_module_level(BACnet.Stack.Transport.MstpTransport.ReceiveFSM, :error)or using the config.exs (purging at compile time):
config :logger,
compile_time_purge_matching: [
[
level_lower_than: :error,
module: BACnet.Stack.Transport.MstpTransport.ReceiveFSM
]
]Note that this will also remove info or debugging output (log_communication_rcv option of this transport).
Summary
Types
The destination address is an integer in the range of 0-255, where 255 means broadcast.
Valid MS/TP frame types.
Valid open options. For a description of each, see open/2.
List of open options.
Valid send options. For a description of each, see send/4.
List of send options.
The source address is an integer in the range of 0-254.
Functions
Get the BACnet transport protocol this transport implements.
Produces a supervisor child spec.
It will call child_spec(callback, opts) with the given 2-element list elements.
Produces a supervisor child spec based on the BACnet transport open callback, as such
it will take the callback and opts for open/2.
Closes the Transport module.
Configures the transport.
Checks whether the given destination is an address that needs to be routed.
Disables the transport's token passing (only if master node).
Get the current active baudrate.
Get the broadcast address.
Get the local address.
Get the transport module portal for the given transport PID/port. Transport modules may return the input as output, if the same PID or port is used for sending.
Get the current transport state.
Get the maximum APDU length for this transport.
Get the maximum extended APDU length for this transport, if the transport also supports a higher (extended) APDU.
Get the maximum extended NPDU length for this transport, if the transport also supports a higher (extended) NPDU.
Get the maximum NPDU length for this transport.
Opens/starts the Transport module. A process is started, that is linked to the caller process.
Sends a Reply-Postponed Frame to the destination.
Sends data to the BACnet network.
Sends a Test-Request APDU to the specified destination.
Enables or disables maintenance POLL_FOR_MASTER in the case the successor node is known. If the successor node is unknown, POLL_FOR_MASTER will be regardless done.
Verifies whether the given destination is valid for the transport module.
Types
@type destination_address() :: 0..255
The destination address is an integer in the range of 0-255, where 255 means broadcast.
@type frame_type() ::
:unknown
| (t_0 :: :token)
| (t_1 :: :poll_for_master)
| (t_2 :: :reply_to_poll_for_master)
| (t_3 :: :test_request)
| (t_4 :: :test_response)
| (t_5 :: :bacnet_data_expecting_reply)
| (t_6 :: :bacnet_data_not_expecting_reply)
| (t_7 :: :reply_postponed)
| (t_32 :: :bacnet_extended_data_expecting_reply)
| (t_33 :: :bacnet_extended_data_not_expecting_reply)
| (t_prop :: {:proprietary, 128..255})
Valid MS/TP frame types.
The t_{number} "name" corresponds to the frame type number.
Frame Type 32 + 33 were added in ASHRAE 135-2016 (they allow octets > 501).
@type open_option() :: {:baudrate, non_neg_integer() | :auto} | {:local_address, source_address()} | {:log_communication, boolean()} | {:log_communication_rcv, boolean()} | {:max_info_frames, pos_integer()} | {:max_master_address, 1..127} | {:port_name, binary()} | {:supervisor, Supervisor.supervisor()} | GenServer.option()
Valid open options. For a description of each, see open/2.
@type open_options() :: [open_option()]
List of open options.
@type send_option() :: {:allow_extended_apdu, boolean()} | {:raw, boolean()} | {:use_extended_apdu, boolean()} | BACnet.Stack.TransportBehaviour.transport_send_option()
Valid send options. For a description of each, see send/4.
@type send_options() :: [send_option()]
List of send options.
@type source_address() :: 0..254
The source address is an integer in the range of 0-254.
Functions
@spec bacnet_protocol() :: BACnet.Stack.TransportBehaviour.transport_protocol()
Get the BACnet transport protocol this transport implements.
@spec child_spec(list()) :: Supervisor.child_spec()
Produces a supervisor child spec.
It will call child_spec(callback, opts) with the given 2-element list elements.
See also Supervisor.child_spec/2 for the rest of the behaviour.
@spec child_spec(BACnet.Stack.TransportBehaviour.transport_callback(), Keyword.t()) :: Supervisor.child_spec()
Produces a supervisor child spec based on the BACnet transport open callback, as such
it will take the callback and opts for open/2.
See also Supervisor.child_spec/2 for the rest of the behaviour.
@spec close(GenServer.server()) :: :ok
Closes the Transport module.
@spec configure(BACnet.Stack.TransportBehaviour.transport(), open_options()) :: :ok | {:error, term()}
Configures the transport.
Only some of the available open_options/0 can be configured,
unsupported options can only be changed by re-starting the transport completely.
The following options are supported:
baudratelog_communicationlog_communication_rcvmax_info_framesmax_master_address
For a description of each option, see open/2.
Note that reconfiguring the baudrate on the fly MAY lead to invalid frames! May also lead to dropping token.
@spec destination_routed?(GenServer.server(), destination_address() | term()) :: boolean()
Checks whether the given destination is an address that needs to be routed.
Returns true for any non-valid destination_address(), because they need
to be routed by a BACnet router residing on this transport layer/network.
@spec disable_token_passing(BACnet.Stack.TransportBehaviour.transport()) :: :ok | {:error, term()}
Disables the transport's token passing (only if master node).
Disabling the token passing will try to pass on the token, if held, as soon as possible, but only if the successor is known. If the successor is unknown or a timeout occurrs, the token will be dropped. The consequence of dropping the token will be that the remaining MS/TP master nodes will notice the lost token and generate a new token.
Whether it transitions to IDLE or NO_TOKEN state depends on its current state and could even change later on from IDLE to NO_TOKEN. Once the transport reaches IDLE or NO_TOKEN, the transport can be safely shut down.
When the token passing is disabled, sending any frame that does not involve sending a reply is disabled and return an error.
After disabling the transport token passing, it can only be re-enabled by restarting the transport.
@spec get_baudrate(BACnet.Stack.TransportBehaviour.transport()) :: non_neg_integer() | {:auto, non_neg_integer()}
Get the current active baudrate.
{:auto, non_neg_integer()} is returned during autobaud detection,
when it has not finished yet. The contained number is the current active baudrate.
Once autobaud detection finishes, this function will return a plain number
with the current active baudrate.
@spec get_broadcast_address(GenServer.server()) :: destination_address()
Get the broadcast address.
@spec get_local_address(GenServer.server()) :: source_address()
Get the local address.
@spec get_portal(GenServer.server()) :: GenServer.server()
Get the transport module portal for the given transport PID/port. Transport modules may return the input as output, if the same PID or port is used for sending.
This is used to get the portal before having received data from the transport module, so data can be sent prior to reception.
@spec get_state(BACnet.Stack.TransportBehaviour.transport()) :: :idle | :no_token | atom()
Get the current transport state.
This function is important when disabling token passing and waiting for the transport to transition to the IDLE or NO_TOKEN state, to then be able to shut down the transport gracefully.
See also disable_token_passing/1.
@spec max_apdu_length() :: pos_integer()
Get the maximum APDU length for this transport.
@spec max_ext_apdu_length() :: pos_integer()
Get the maximum extended APDU length for this transport, if the transport also supports a higher (extended) APDU.
@spec max_ext_npdu_length() :: pos_integer()
Get the maximum extended NPDU length for this transport, if the transport also supports a higher (extended) NPDU.
The NPDU length contains the maximum transmittable size of the NPDU, including the APDU, without violating the maximum transmission unit of the underlying transport.
Any necessary transport header (i.e. BVLL, LLC) must have been taken into account when calculating this number.
@spec max_npdu_length() :: pos_integer()
Get the maximum NPDU length for this transport.
The NPDU length contains the maximum transmittable size of the NPDU, including the APDU, without violating the maximum transmission unit of the underlying transport.
Any necessary transport header (i.e. BVLL, LLC) must have been taken into account when calculating this number.
@spec open( callback :: BACnet.Stack.TransportBehaviour.transport_callback(), opts :: open_options() ) :: {:ok, pid()} | {:error, term()}
Opens/starts the Transport module. A process is started, that is linked to the caller process.
See the BACnet.Stack.TransportBehaviour documentation for more information.
In the case of this BACnet MS/TP transport, the transport PID/port is a GenServer receiving and sending
RS485 data. The portal is the same transport PID/port, as access to the MS/TP network must be coordinated.
This transport takes the following options, in addition to GenServer.options/0:
baudrate: non_neg_integer | :auto- Optional. The baud rate to use (defaults to38400). See the module documentation regarding the autobaud feature.local_address: source_address()- Required. The address to use - must be unique in the BACnet MS/TP network. Addresses 0-127 are for master nodes, while 128-254 are for slave nodes.log_communication: boolean()- Optional. Logs all communication (debug), excluding receive states.log_communication_rcv: boolean()- Optional. Logs all communication (debug) of receive states.max_info_frames: pos_integer()- Optional. This value specifies the maximum number of information frames the node may send before it must pass the token (defaults to1).max_master_address: 1..127- Optional. The maximum master address that is used in the MS/TP network. This is used for polling and successor determination (defaults to127).port_name: binary()- Required. Name of the serial port (useCircuits.UART.enumerate/0).supervisor: Supervisor.supervisor()- Optional. The task supervisor to use to spawn tasks under. Tasks are spawned to invoke the given callback. If no supervisor is given, the tasks will be spawned unsupervised.
@spec reply_postponed( BACnet.Stack.TransportBehaviour.portal(), source_address(), Keyword.t() ) :: :ok | {:error, term()} | {:error, :slave_mode} | {:error, :no_reply_pending} | {:error, :destination_is_not_expecting_reply}
Sends a Reply-Postponed Frame to the destination.
Sending an explicit Reply-Postponed Frame is necessary, when the reply is to be segmented. A segmented Complex-ACK APDU can only be transmitted when we hold the token (ASHRAE 135 Clause 9.8).
There are no options at this time.
@spec send( GenServer.server(), destination_address(), BACnet.Stack.EncoderProtocol.t() | iodata(), send_options() ) :: :ok | {:error, term()} | {:error, :slave_mode} | {:error, :token_passing_disabled}
Sends data to the BACnet network.
Please note that not all MS/TP devices support extended APDUs (max. 1476 bytes) and thus you should make sure they do when sending large APDUs, or always default to the maximum as defined by ASHRAE 135-2012 (before 135-2016).
See the BACnet.Stack.TransportBehaviour documentation for more information.
The option skip_headers has no effect.
In addition, the following options are available:
allow_extended_apdu: boolean()- Optional. Allow to send APDUs up to 1476 bytes, instead of 501 bytes. Extended APDUs require support of ASHRAE 135-2016 and newer.use_extended_apdu: boolean()- Optional. Uses the extended APDU frame type to send the APDU (APDU length must be min. 5 bytes) -allow_extended_apdumust betrue.raw: boolean()- Optional. Sends raw data to the transport layer. The data MUST be BACnet MS/TP conform data.
@spec send_test(BACnet.Stack.TransportBehaviour.portal(), source_address(), iodata()) :: {:ok, iodata()} | {:error, term()} | {:error, :invalid_frame_response} | {:error, :slave_mode} | {:error, :token_passing_disabled}
Sends a Test-Request APDU to the specified destination.
If the destination exists and is reachable, it will send the data back unchanged
(or no data at all, if it for some reason unable to read the data).
The destination must not be 255 (broadcast).
The data must be less than 502 bytes long.
This function will block until the Test-Response APDU has arrived or the timeout triggers.
@spec set_maintenance_pfm(BACnet.Stack.TransportBehaviour.transport(), boolean()) :: :ok
Enables or disables maintenance POLL_FOR_MASTER in the case the successor node is known. If the successor node is unknown, POLL_FOR_MASTER will be regardless done.
This function is only for development and testing purpose. It must not be used in production. Periodic maintenance polling for masters is required to find new nodes in between this node and the next current successor node. Nodes may come up and go down any time, which the BACnet specification accounts for and thus includes a POLL_FOR_MASTER mechanism.
@spec valid_destination?(destination_address() | term()) :: boolean()
Verifies whether the given destination is valid for the transport module.