Skip to content
Version 6.1.0
Contact Us

Data links overview

This chapter covers what every data link has in common: the two message callbacks your application implements, the Network Port object each port needs, and the address format of each data link. Each data link then has a chapter of its own, listed below. It assumes you have finished Quick Start and have a device answering on one port.

The CAS BACnet Stack above three Network Port objects: 1 for BACnet/IP over your UDP socket, 2 for MS/TP over your RS-485 serial port and MS/TP engine, 3 for BACnet/SC over your WebSocket and TLS library. Each pair is joined by its networkPortInstance. The stack opens no sockets or ports; your application does.

The stack opens no sockets, serial ports or WebSockets. Your application owns the transport. The stack hands you each outbound frame through the send callback, and pulls each inbound frame through the receive callback. Both are registered once, before the first BACnetStack_Tick():

Receive. The stack calls your receive callback exactly once per BACnetStack_Tick(), and from nowhere else. It must not block. Return 0 when nothing is waiting; that is the normal idle case. Write the frame into message, the sender’s address into sourceConnectionString, and the address the frame was sent to into destinationConnectionString. If the byte count you return exceeds maxMessageLength, or either connection-string length exceeds maxConnectionStringLength, the stack discards the whole message and logs it. It does not clamp. The buffers belong to the stack and are valid only during the call.

Send. The send callback fires from inside BACnetStack_Tick() and also synchronously from every BACnetStack_Send* function before that function returns. Return exactly messageLength when the whole frame was accepted for transmission, and 0 when it was not. Any other value, including 1, is treated as a failed send. Accepted does not mean delivered: the stack has no later failure channel, so a lost confirmed request is recovered by the stack’s own timeout and retry, and lost unconfirmed traffic is simply gone. A NULL connection string with length 0 means broadcast on that data link.

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)
{
/* Pick your transport by networkPortInstance, then transmit or queue without blocking. */
/* Return messageLength if the whole frame was accepted, otherwise 0. */
return 0;
}

If the receive callback is never registered, the device is silently deaf. If the send callback is never registered, every send is dropped and logged with “CallbackSendMessageForPort does not exist”.

Network Port objects and networkPortInstance

Section titled “Network Port objects and networkPortInstance”

Both message callbacks carry a networkPortInstance: the instance of the Network Port object the frame belongs to. You create one per port with BACnetStack_AddNetworkPortObject(). Its networkType takes a BACnetNetworkType value (mstp = 2, ipv4 = 5, virtualNetwork = 7, secureConnect = 11), and its protocolLevel takes a BACnetProtocolLevel value (bacnetApplication = 2 for a BACnet data link). Pass 4194303 as referencePort for the lowest protocol layer.

BACNET_NETWORK_PORT_DEFAULT (4194303) means “the default port for this transport”. It is only unambiguous when exactly one data-link transport is compiled into the build. In a build with more than one, you must report a real Network Port instance from the receive callback and pass one to each BACnetStack_Send* call; the stack logs an error and drops the frame rather than guess. STACK_OPTION_TARGET_FULL enables all data links, so a full build is always multi-transport.

Some configuration is staged as pending rather than applied. Once your start-up configuration is done, call BACnetStack_ActivateNetworkPortPendingChanges() so the device does not boot reporting Changes_Pending for changes nobody made over the network. For writable Network Port properties and ReinitializeDevice, read Network Port objects and ReinitializeDevice.

Connection string layouts: BACnet/IPv4 is six bytes, 192 168 1 5 for the IPv4 address then 0xBA 0xC0 for UDP port 47808 big-endian; MS/TP is one byte, the MAC address, for example 5; BACnet/SC is the WebSocket URI, such as wss://hub.example.com, at most 255 bytes.

The connection string is the data-link address of the peer. Its layout depends on the port:

  • BACnet/IPv4: 6 bytes. Four octets of IPv4 address, then a 2-octet UDP port in big-endian order. 0xC0, 0xA8, 0x01, 0x05, 0xBA, 0xC0 is 192.168.1.5:47808. This order is fixed, whatever your host.
  • BACnet/IPv6: 18 bytes. Sixteen octets of IPv6 address, then the 2-octet UDP port in big-endian order. A broadcast is sent with no connection string; your application sends it to the BACnet/IPv6 multicast group.
  • MS/TP: 1 byte, the MS/TP MAC address.
  • BACnet/SC: the WebSocket URI as bytes, at most 255. For a peer that connected to one of your accept URIs, the per-socket string <acceptUri>|client=<token>, which BACnet/SC describes.

Each data link has its own chapter with the build options, the callbacks, a link to an example on GitHub and its failure modes. The points below are the ones integrations most often miss.

  • BACnet/IP. Build with STACK_OPTION_DATA_LINK_LAYER_IPV4. Your application owns the UDP socket, and every source address it reports must be the 6-byte form above; any other length is rejected and logged.
  • BBMD and foreign devices. A foreign device must re-register before its time-to-live passes: the stack does not do it for you, and a time-to-live of 0 expires on the next tick. A BBMD needs its own entry in the broadcast distribution table before BACnetStack_SetBBMD() succeeds.
  • BACnet/IPv6. Build with STACK_OPTION_DATA_LINK_LAYER_IPV6 and add one Network Port object of type ipv6 (9). The stack resolves each peer’s three-octet virtual address itself, and needs CallbackGetSystemTime to do it. The port can be a foreign device, or a BBMD in a build that also has STACK_OPTION_BVLL_BBMD (which needs BACnet/IP; without it the BBMD properties are absent and the BBMD mode is refused): a client writes the port’s BACnet_IPv6_Mode and its BBMD or FD properties, and they take effect at a ReinitializeDevice ACTIVATE_CHANGES or WARMSTART. There is no BACnetStack_* function for this yet. Unlike BACnet/IP, a BACnet/IPv6 foreign device registers again by itself, at half its time-to-live. A BDT entry or BBMD address given as a host name is refused: give the IPv6 address. A host that runs a BBMD must pass each received datagram’s destination address (18 octets, or the 16-octet IPv6 address) to the receive call: the multicast group it arrived on, or this node’s unicast address it was sent to, never the socket’s bound address. The BBMD passes a peer BBMD’s broadcast on to its link and foreign devices only when it knows that broadcast arrived by unicast, so no forwarding loop can form. Without a destination the BBMD still forwards its own link’s broadcasts to its peers and foreign devices, but does not pass its peers’ broadcasts on. Passing the socket’s bound address for every datagram makes every arrival look unicast, and the BBMD then forwards nothing from its own link: pass the real destination, or nothing.
  • BACnet MS/TP. Build with STACK_OPTION_DATA_LINK_LAYER_MSTP. Token passing runs in the stack’s own MS/TP engine: register the byte-level callbacks and call BACnetStack_LoopMSTP at least every 20 milliseconds (every 1 to 5 if you can), or stay in packet mode with your own framing and the stack’s message callbacks.
  • BACnet/SC. Build with STACK_OPTION_DATA_LINK_LAYER_SC and STACK_OPTION_DATA_LINK_LAYER_IPV4. Your application owns the WebSocket and TLS libraries and the certificate bytes. If you never report “connected” with BACnetStack_SetBACnetSCWebSocketStatus(), the connection never starts and the device appears hung. The BACnet/SC overview covers the case for it.
  • Virtual devices and gateways. Build with STACK_OPTION_GW_VN_B. The main device becomes a virtual router for further logical devices on virtual networks.
  • Network Port objects and ReinitializeDevice. Writable Network Port properties, pending changes and the ReinitializeDevice service, for every data link.

Your device is a router with port 1 on BACnet/IP network 1 and port 2 on MS/TP network 2; a further router on network 2 leads to network 3. Call BACnetStack_AddRouterPort() for each port, BACnetStack_AddRouterRoute() for network 3, then BACnetStack_SetRouterEnabled(): routing is off until you call it.

A device with two or more data links can route between them. This needs STACK_OPTION_ROUTER_PHYSICAL; STACK_OPTION_TARGET_FULL turns it on unless you define STACK_OPTION_DISABLE_ROUTER_PHYSICAL.

  1. BACnetStack_AddRouterPort() once per port, each with a unique network number. Two or more ports make the device a router.
  2. BACnetStack_AddRouterRoute() for a network that is one or more hops away.
  3. BACnetStack_SetRouterEnabled() to start forwarding. Routing is off until you call it, and no function reads the state back.

A network number cannot be both a virtual network and a router port; the second one added is rejected.

  • Build with STACK_OPTION_API_DEBUG_LOGGING (on in the prebuilt libraries and the npm package) and register BACnetStack_RegisterCallbackLogDebugMessage() first. A false return from any function is explained only there. It is also the one callback that survives BACnetStack_Reset().
  • After BACnetStack_Reset(), the stack neither sends nor receives until you register the send and receive callbacks again and re-add the device. Your sockets and serial ports are untouched.
  • Make every call, including those from your transport code, on the thread that calls BACnetStack_Tick(). By default the stack is single-threaded; BACnetStack_LoopMSTP() and a build with STACK_OPTION_THREAD_SAFE are the exceptions (see Stack lifecycle).

Complete working code is on Chipkin’s GitHub: