Sending and receiving BACnet/IP messages
The two message callbacks
Section titled “The two message callbacks”Both signatures are fixed by the header. Copy them exactly:
uint16_t CallbackReceiveMessageForPort(uint8_t* message, const uint16_t maxMessageLength, uint8_t* sourceConnectionString, uint8_t* sourceConnectionStringLength, uint8_t* destinationConnectionString, uint8_t* destinationConnectionStringLength, const uint8_t maxConnectionStringLength, uint32_t* networkPortInstance);
uint16_t CallbackSendMessageForPort(const uint8_t* message, const uint16_t messageLength, const uint8_t* connectionString, const uint8_t connectionStringLength, const uint32_t networkPortInstance, bool broadcast);The matching typedefs are FPCallbackReceiveMessageForPort and FPCallbackSendMessageForPort in
source/BACnetCallbackInterface.h.
The BACnet/IP connection string
Section titled “The BACnet/IP connection string”On BACnet/IP a connection string is exactly 6 bytes: the 4 octets of the IPv4 address, then the 2-octet UDP port, most significant byte first. This order is fixed, whatever the byte order of your processor.
| Bytes | Address |
|---|---|
0xC0, 0xA8, 0x01, 0x05, 0xBA, 0xC0 |
192.168.1.5:47808 |
0xC0, 0xA8, 0x01, 0xFF, 0xBA, 0xC0 |
192.168.1.255:47808 |
This is the same layout as the sin_addr and sin_port fields of a struct sockaddr_in, which are already in
network byte order, so you can copy them in and out with memcpy.
The same 6-byte layout is used everywhere a BACnet/IP address crosses the API: the connectionString argument of
the BACnetStack_Send* functions (the address to send to, not the address to send from), the BBMD address of
BACnetStack_SendRegisterForeignDevice(), and the entries of the broadcast distribution table.
Receiving
Section titled “Receiving”The stack calls the receive callback exactly once per BACnetStack_Tick(), and from nowhere else.
- It must not block. Read the socket in non-blocking mode and return 0 when no datagram is waiting. That is the normal idle case, not an error.
- Copy the datagram, starting with its BVLL header, into
messageand return its length. - Write the sender’s address and port into
sourceConnectionStringand set*sourceConnectionStringLengthto 6. The BACnet/IP data link rejects, and logs, a datagram whose source is any other length. - Write the address the datagram was sent to into
destinationConnectionString. A plain UDP socket does not tell you whether a datagram arrived as a broadcast; reporting your own address and port is enough. In a build with one data link the stack uses this address, then the source address, to pick the BACnet/IP port when you reportBACNET_NETWORK_PORT_DEFAULT. - Set
*networkPortInstanceto the instance of the Network Port object the datagram arrived on (see networkPortInstance).
If the length you return is greater than maxMessageLength, or either connection-string length is greater than
maxConnectionStringLength, the stack discards the whole datagram and logs it. It does not truncate. All four buffers
belong to the stack and are valid only during the call.
If the receive callback is never registered, the stack reads nothing and reports nothing: the device is silently deaf.
Sending
Section titled “Sending”The stack calls the send callback from inside BACnetStack_Tick(), and also synchronously from every
BACnetStack_Send* function before that function returns.
- Return exactly
messageLengthwhen the whole datagram was accepted for transmission, and 0 when it was not. Any other value, including 1, counts as a failed send and is logged. - Do not block. If the socket cannot take the datagram now, return 0. A lost confirmed request is recovered by the stack’s own timeout and retry; lost unconfirmed traffic is simply gone, as it always is with UDP.
messageandconnectionStringbelong to the stack and are valid only during the call.- On a unicast,
connectionStringis the 6-byte destination. The BACnet/IP data link refuses to hand you a unicast with any other length.
If the send callback is never registered, each send is dropped and logged with “CallbackSendMessageForPort does not exist”.
Broadcasts
Section titled “Broadcasts”When broadcast is true, the datagram is a broadcast. Where it goes depends on the connection string:
- Empty (length 0). The stack chose the broadcast itself: a broadcast it routes onto this port, an I-Am or I-Have answering a request that arrived as a broadcast, a BBMD’s local re-broadcast, its own Who-Is and its network-layer messages. Your application chooses the address: the broadcast address of the port’s IP subnet, for example 192.168.1.255:47808 for 192.168.1.5 with mask 255.255.255.0, or 255.255.255.255 if that is what your network uses.
- 6 octets. A broadcast your application asked for: the address it passed to its own
BACnetStack_Send*call withbroadcasttrue, or a notification recipient whose IP octets are all 255. Send the datagram to that address. It can be a remote subnet’s directed broadcast address, so do not replace it with the local one.
When broadcast is false, the connection string is always present: send to it, and only to it, even when it
is itself a broadcast address (a BBMD forwarding to a remote subnet, for example).
The socket needs SO_BROADCAST (POSIX and Winsock) before it may send to a broadcast address.
A global broadcast, one meant for every BACnet network and not just the local one, is not a different datagram
at this level. It is a local broadcast whose NPDU carries destination network 65535. Ask for one by passing
broadcast = true and destinationNetwork = 65535 to a BACnetStack_Send* function; pass destinationNetwork = 0
for a broadcast on the local network only. Routers on the network forward a global broadcast onwards; your send
callback handles both the same way.
Two cases reach the send callback with broadcast false even though the audience is many devices:
- A registered foreign device. A device that has registered with a BBMD through
BACnetStack_SendRegisterForeignDevice()cannot reach the remote subnet with a local broadcast, so the stack sends each of its local broadcasts as a Distribute-Broadcast-To-Network message, unicast to the BBMD. You receive the BBMD’s address withbroadcastfalse. - A BBMD forwards broadcasts to its peers and foreign devices as Forwarded-NPDU messages, one per destination. A peer’s address can be the directed broadcast address of the peer’s subnet, when its broadcast distribution table entry has a mask other than 255.255.255.255.
Both are handled correctly by a send callback that, whenever broadcast is false, sends to connectionString
exactly as given. The setup for both is in BBMD and foreign devices.
Your own broadcasts come back. A broadcast your socket sends is usually delivered to the same socket. On a device that is not a BBMD the stack does not drop it; it processes it like any other datagram. Discard datagrams whose source is your own address and port in the receive callback. A BBMD already recognises its own looped-back broadcast and does not forward it a second time.
networkPortInstance
Section titled “networkPortInstance”Each message callback carries a networkPortInstance: the instance of the Network Port object the datagram belongs
to.
- On send, it names the port to transmit on. In a device with one BACnet/IP port you can ignore it, or check it against your port’s instance and return 0 for anything else.
- On receive, report the instance of the Network Port object whose socket the datagram arrived on.
BACNET_NETWORK_PORT_DEFAULT (4194303) means “the default port for this transport”. It is accepted only in a build
with exactly one data link compiled in. In a build with more than one, the stack logs an error and drops a received
datagram reported with it, and refuses a BACnetStack_Send* call that passes it, rather than guess which transport
was meant. An instance that does not match any Network Port object is dropped the same way.
Several data links in one build
Section titled “Several data links in one build”STACK_OPTION_TARGET_FULL turns on BACnet/IP, BACnet/IPv6, MS/TP and BACnet/SC, and any build with BACnet/SC also contains
BACnet/IP, so these builds are always multi-transport. In them:
- Add one Network Port object for every port, BACnet/IP included, with
BACnetStack_AddNetworkPortObject(). - Report that port’s real instance from the receive callback for every datagram, frame or WebSocket message.
- Pass a real instance to every
BACnetStack_Send*call. - In the send callback, switch on
networkPortInstanceto choose the UDP socket, the serial port or the WebSocket.
One pair of callbacks serves every data link. The receive callback returns at most one message per tick, so when more than one transport has traffic waiting, take one message from one of them and leave the rest for the next tick. The BACnet/SC and MS/TP chapters describe their own connection strings.
