Skip to content
Version 6.1.0
Contact Us

Sending and receiving BACnet/IP messages

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.

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.

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 message and return its length.
  • Write the sender’s address and port into sourceConnectionString and set *sourceConnectionStringLength to 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 report BACNET_NETWORK_PORT_DEFAULT.
  • Set *networkPortInstance to 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.

The stack calls the send callback from inside BACnetStack_Tick(), and also synchronously from every BACnetStack_Send* function before that function returns.

  • Return exactly messageLength when 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.
  • message and connectionString belong to the stack and are valid only during the call.
  • On a unicast, connectionString is 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”.

One IP subnet, 192.168.1.0/24, with a BACnet client and devices 389001, 389002 and 389003. The client broadcasts Who-Is to every device, and device 389001 answers with I-Am.

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 with broadcast true, 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 with broadcast false.
  • 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.

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.

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:

  1. Add one Network Port object for every port, BACnet/IP included, with BACnetStack_AddNetworkPortObject().
  2. Report that port’s real instance from the receive callback for every datagram, frame or WebSocket message.
  3. Pass a real instance to every BACnetStack_Send* call.
  4. In the send callback, switch on networkPortInstance to 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.