BBMD and foreign devices
Most products only need to register as a foreign device. Make your device a BBMD only when it has to forward broadcasts between subnets for other devices.
BBMDs and foreign devices in brief
Section titled “BBMDs and foreign devices in brief”BACnet/IP finds devices and distributes unconfirmed messages (Who-Is, I-Am, Who-Has) with UDP broadcasts. IP routers do not forward those broadcasts, so a device on one subnet cannot discover a device on another.
- A BBMD sits on a subnet and relays broadcasts. It keeps a Broadcast Distribution Table (BDT) listing itself and the BBMDs on the other subnets. When it hears a broadcast on its own subnet it sends a copy (a Forwarded-NPDU) to every peer in the BDT, and each peer re-broadcasts it on its own subnet. Every BBMD in a network should hold the same BDT.
- A foreign device is a single device with no BBMD on its subnet, for example a workstation across a VPN. It registers with a remote BBMD for a limited time (its time-to-live). While registered it sends its broadcasts to that BBMD to distribute, and the BBMD sends it a copy of every broadcast it relays. The BBMD keeps its registrations in its Foreign Device Table (FDT).
Example
Section titled “Example”Three subnets joined by routers that do not pass broadcasts:
| Subnet | Device | Role |
|---|---|---|
| 192.168.1.0/24 | A, 192.168.1.13:47808 | BBMD |
| 192.168.1.0/24 | B, 192.168.1.12 | Client |
| 192.168.2.0/24 | C, 192.168.2.3:47808 | BBMD |
| 192.168.2.0/24 | D | Server |
| 192.168.3.0/24 | E, 192.168.3.8:47808 | Server, registered as a foreign device with A |
Without BBMDs, a Who-Is from B reaches only A. With A and C each holding a BDT of both BBMDs, and E registered with A:
- A hears B’s Who-Is. It answers for its own device, sends a Forwarded-NPDU to C, and sends one to E.
- C re-broadcasts the Who-Is on subnet 2. C and D answer, and C forwards their I-Ams to A, which re-broadcasts them on subnet 1.
- E answers A. E’s I-Am, if broadcast, goes to A as a Distribute-Broadcast-To-Network, and A distributes it.
B receives four I-Ams: from A, C, D and E.
Build options
Section titled “Build options”| Option | Effect |
|---|---|
STACK_OPTION_DATA_LINK_LAYER_IPV4 |
BACnet/IP. Enough for foreign-device registration. |
STACK_OPTION_NM_BBMDC |
BBMD support (NM-BBMDC-A and NM-BBMDC-B). Needs the IPv4 data link; without it the build has no BBMD. It turns on the Network Port object type, which every BBMD needs. |
STACK_OPTION_PROFILE_B_BBMD |
The B-BBMD device profile. Turns on STACK_OPTION_NM_BBMDC, DM-DCC-B and the Network Port object. |
STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES |
FDT capacity. Default 128, or 16 for STACK_OPTION_TARGET_EMBEDDED. A registration that would exceed it is refused with a NAK. |
STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES_PER_IP |
The most FDT entries one registrant IP address may hold, whatever its UDP source port. Defaults to STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES, so one host can fill the table; lower it (for example to half the table, or 4) so a host that varies its source port cannot lock every other foreign device out. A new registration past the share is refused with the same NAK; a re-registration of an existing entry is never refused. |
STACK_OPTION_MAX_BROADCAST_DISTRIBUTION_TABLE_ENTRIES |
BDT capacity. Default 512, or 32 for STACK_OPTION_TARGET_EMBEDDED. |
STACK_OPTION_TARGET_FULL includes all of these. Build options are described in
Configuration presets.
Making the device a BBMD
Section titled “Making the device a BBMD”Configure in this order:
- Register the Network Port property callbacks that serve the port’s IP_Address and IP_Subnet_Mask
(
CallbackGetPropertyOctetString) and BACnet_IP_UDP_Port (CallbackGetPropertyUnsignedInteger). The stack reads the address and mask when the Network Port object is added, and the address and port again whenBACnetStack_SetBBMDruns. - Add the IPv4 Network Port object with
BACnetStack_AddNetworkPortObject(networkType5 = ipv4,protocolLevel2 = bacnetApplication). - Add this device’s own BDT entry with
BACnetStack_AddBDTEntry, then one entry per peer BBMD. The own entry must carry exactly the address and UDP port the Network Port object reports. - Call
BACnetStack_SetBBMD. It finds the device’s own entry by address and UDP port (the mask is not compared) and fails if there is none. On success the port starts relaying and accepting foreign device registrations, and the stack enables the port’s BBMD_Broadcast_Distribution_Table, BBMD_Accept_FD_Registrations and BBMD_Foreign_Device_Table properties.
A BDT entry is 6 address bytes (4 octets of IPv4 address, then the UDP port in big-endian order, so
0xBA, 0xC0 is 47808) and 4 mask bytes. Pass the lengths as exactly 6 and 4; any other length is refused.
The table is a set: adding an entry whose address and mask both match an existing one replaces it.
Distribution masks
Section titled “Distribution masks”The mask in each BDT entry decides how a Forwarded-NPDU reaches that peer’s subnet. The BBMD combines the peer’s address with the inverse of the mask:
| Mask | Forwarded-NPDU is sent to | The peer then | Name |
|---|---|---|---|
255.255.255.255 |
The peer BBMD itself (unicast) | Re-broadcasts it on its subnet and sends it to its foreign devices | Two-hop |
The peer’s subnet mask, e.g. 255.255.255.0 |
The peer subnet’s directed-broadcast address, e.g. 192.168.2.255 | Only sends it to its foreign devices, since its subnet already received it | One-hop |
Two-hop works across any IP network. One-hop saves a hop but only works when the routers between the
subnets forward directed broadcasts, which most are configured not to do. Use 255.255.255.255 unless you
know the routers pass directed broadcasts.
Changing the BDT later
Section titled “Changing the BDT later”- A BACnet client can replace the table by writing the Network Port’s BBMD_Broadcast_Distribution_Table
property while the port is a BBMD. The stack checks the whole list first and refuses it unchanged if
an entry is malformed, the list is longer than the BDT capacity, or the list has no entry for this
device. No callback reports such a write; if you persist the BDT, read it back with
BACnetStack_GetBDTEntry. - The Write-Broadcast-Distribution-Table BVLL message is always refused with a NAK.
BACnetStack_DeleteBDTEntryrefuses to delete the device’s own entry while it is a BBMD.BACnetStack_ResetBroadcastDistributionTableempties the BDT of every IPv4 port and turns BBMD operation off on every port. Add the entries again and callBACnetStack_SetBBMDto resume.
Registering as a foreign device
Section titled “Registering as a foreign device”Call BACnetStack_SendRegisterForeignDevice with the IPv4 Network Port instance, a time-to-live in
seconds (0 to 65535), and the BBMD’s 6-byte address.
- Register
BACnetStack_RegisterCallbackGetSystemTimefirst. Without a clock the stack cannot track the time-to-live, and the call returns false before sending anything. - The stack does not re-register for you. It records the deadline; when it passes, the stack drops its foreign-device state and the port’s BACnet_IP_Mode returns to NORMAL. Call the function again before the time-to-live runs out. Re-registering at half the time-to-live leaves room for a lost message.
- A time-to-live of 0 does not mean “forever”. It expires on the next tick.
- A connection-string length other than 6 is refused before anything is sent.
- The function returns true once the request is sent. It does not wait for the BBMD’s answer, so a BBMD that refuses the registration (a full FDT, or BBMD_Accept_FD_Registrations FALSE) is not reported by the return value. Watch for the BVLC-Result in your receive callback if you need to know.
While registered, the port’s BACnet_IP_Mode reads FOREIGN, and the unconfirmed broadcasts the stack originates on that port (Who-Is, I-Am and the like) go to the BBMD as Distribute-Broadcast-To-Network messages instead of local UDP broadcasts.
Reading and managing the tables
Section titled “Reading and managing the tables”This device’s own tables
Section titled “This device’s own tables”| Function | Use |
|---|---|
BACnetStack_GetBDTEntry(networkPortInstance, index, ...) |
Read BDT entry index: address and mask. |
BACnetStack_GetFDTEntry(networkPortInstance, index, ...) |
Read FDT entry index: address, the time-to-live it registered with, and the seconds remaining. |
BACnetStack_DeleteBDTEntry(index) |
Delete one BDT entry. |
BACnetStack_DeleteFDTEntry(index) |
Delete one FDT entry. Nothing is sent: the removed device is not told and keeps sending to this BBMD. |
BACnetStack_ResetForeignDeviceTable() |
Empty the FDT of every IPv4 port. |
BACnetStack_ResetBroadcastDistributionTable() |
Empty the BDT of every IPv4 port and stop BBMD operation. |
- There is no count function. Read from index 0 until the function returns 0. That last, out-of-range read logs an error through the debug-message callback every time.
- Indices shift. Deleting an entry, or the BBMD purging an expired registration, moves every later entry down by one. Delete from the highest index down, or start again from 0 after each delete.
- The remaining time of an FDT entry includes a 30-second grace period on top of the time-to-live, so a device that re-registers a little late is not dropped.
BACnetStack_Reset()clears neither table and does not take the device out of BBMD mode.- Neither table is persistent. After a restart, re-add the BDT; foreign devices have to register again.
A remote BBMD’s tables
Section titled “A remote BBMD’s tables”BACnetStack_SendReadBroadcastDistributionTable and BACnetStack_SendReadForeignDeviceTable send the
request to another BBMD. No callback delivers the reply: capture the inbound frame in your
CallbackReceiveMessageForPort and decode it with BACnetStack_DecodeAs(), whose XML and JSON output
lists the table entries. Both need BBMD support compiled in.
Example
Section titled “Example”A BBMD on 192.168.1.13 with one peer BBMD at 192.168.2.3, both using two-hop masks. The signatures are
copied from source/CASBACnetStackDLL.h.
#include <time.h>#include "CASBACnetStackDLL.h"
#define DEVICE_INSTANCE 389001#define NETWORK_PORT_INSTANCE 1
static CASBACnetTime GetSystemTime(void){ return (CASBACnetTime)time(NULL);}
bool ConfigureBbmd(void){ /* Register the send/receive callbacks and the Network Port property callbacks (IP_Address, IP_Subnet_Mask, BACnet_IP_UDP_Port), and add the device with BACnetStack_AddDevice(DEVICE_INSTANCE), before this point. */ BACnetStack_RegisterCallbackGetSystemTime(GetSystemTime);
/* bool BACnetStack_AddNetworkPortObject(uint32_t deviceInstance, uint32_t objectInstance, uint8_t networkType, uint8_t protocolLevel, uint16_t networkNumber, uint8_t networkNumberQuality, uint32_t referencePort); */ if (!BACnetStack_AddNetworkPortObject(DEVICE_INSTANCE, NETWORK_PORT_INSTANCE, 5, /* networkType: ipv4 */ 2, /* protocolLevel: bacnetApplication */ 0, 0, /* networkNumber, networkNumberQuality: unknown */ 4194303)) { /* referencePort: lowest protocol layer */ return false; }
/* 4 octets of IPv4 address, then the UDP port big-endian (0xBA 0xC0 = 47808). */ const uint8_t ownAddress[6] = { 192, 168, 1, 13, 0xBA, 0xC0 }; const uint8_t peerAddress[6] = { 192, 168, 2, 3, 0xBA, 0xC0 }; const uint8_t twoHopMask[4] = { 255, 255, 255, 255 };
/* bool BACnetStack_AddBDTEntry(uint32_t networkPortInstance, const uint8_t* ipAddress, uint8_t ipAddressLength, const uint8_t* subnetMask, uint8_t subnetMaskLength); */ if (!BACnetStack_AddBDTEntry(NETWORK_PORT_INSTANCE, ownAddress, 6, twoHopMask, 4)) { return false; } if (!BACnetStack_AddBDTEntry(NETWORK_PORT_INSTANCE, peerAddress, 6, twoHopMask, 4)) { return false; }
/* bool BACnetStack_SetBBMD(uint32_t deviceInstance, uint32_t networkPortInstance); Fails if no BDT entry matches the port's own IP_Address and BACnet_IP_UDP_Port. */ return BACnetStack_SetBBMD(DEVICE_INSTANCE, NETWORK_PORT_INSTANCE);}
void ListForeignDevices(void){ uint8_t address[6]; uint16_t timeToLive = 0; uint16_t timeRemaining = 0;
/* uint32_t BACnetStack_GetFDTEntry(uint32_t networkPortInstance, uint16_t index, uint8_t* ipAddress, uint8_t ipAddressLength, uint16_t* timeToLive, uint16_t* timeRemaining); */ for (uint16_t entryIndex = 0; BACnetStack_GetFDTEntry(NETWORK_PORT_INSTANCE, entryIndex, address, 6, &timeToLive, &timeRemaining) != 0; entryIndex++) { /* address[0..3] is the IPv4 address, address[4..5] the UDP port, big-endian. */ }}A foreign device instead registers, and repeats the registration before the time-to-live runs out:
#define FOREIGN_DEVICE_TIME_TO_LIVE 3600 /* seconds */
static const uint8_t bbmdAddress[6] = { 192, 168, 1, 13, 0xBA, 0xC0 };static CASBACnetTime nextRegistration = 0;
/* Call from the same loop that calls BACnetStack_Tick(). */void MaintainForeignDeviceRegistration(void){ const CASBACnetTime now = GetSystemTime(); if (now < nextRegistration) { return; } /* bool BACnetStack_SendRegisterForeignDevice(uint32_t networkPortInstance, uint16_t timeToLive, const uint8_t* connectionString, uint8_t connectionStringLength); */ if (BACnetStack_SendRegisterForeignDevice(NETWORK_PORT_INSTANCE, FOREIGN_DEVICE_TIME_TO_LIVE, bbmdAddress, 6)) { nextRegistration = now + FOREIGN_DEVICE_TIME_TO_LIVE / 2; } else { nextRegistration = now + 10; /* retry shortly */ }}Practical notes
Section titled “Practical notes”- Two BBMDs on one subnet must not list each other or share BDT entries. A BBMD still sends a Forwarded-NPDU from another BBMD on its own subnet to its foreign devices, without re-broadcasting it.
- Forwarded-NPDUs are accepted only from BDT peers. One from a sender on another subnet that is not in the BDT is not re-broadcast or passed to foreign devices. The UDP port is not compared, since NAT can change it.
- Refusing registrations. A client can write BBMD_Accept_FD_Registrations FALSE (the write needs
CallbackReinitializeDeviceregistered). Once the change is activated (ReinitializeDevice ACTIVATE_CHANGES, orBACnetStack_ActivateNetworkPortPendingChanges) new registrations and re-registrations are refused with a NAK, and existing entries stay until they expire. - Broadcast BBMD-only requests. A port that is not a BBMD ignores a BBMD-only BVLL request (such as
Read-Foreign-Device-Table) sent to a broadcast address, rather than answering it with a NAK, when it knows
where the frame was sent: report each received frame’s destination address in
CallbackReceiveMessageForPort’sdestinationConnectionStringso the stack can tell. - Debugging. The XML from
BACnetStack_GetInfoincludesisForeignDevice. Decoding received BVLL frames withBACnetStack_DecodeAs()shows each BVLC function and its table entries.

