Skip to content
Version 6.1.0
Contact Us

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.

Two subnets, A and B, joined by an IP router, each with controllers and a BBMD. The two BBMDs forward broadcasts to each other. A foreign device on the internet sends Register-Foreign-Device, with a time-to-live, to the BBMD on subnet A.

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).

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:

  1. A hears B’s Who-Is. It answers for its own device, sends a Forwarded-NPDU to C, and sends one to E.
  2. 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.
  3. 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.

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.

Configure in this order:

  1. 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 when BACnetStack_SetBBMD runs.
  2. Add the IPv4 Network Port object with BACnetStack_AddNetworkPortObject (networkType 5 = ipv4, protocolLevel 2 = bacnetApplication).
  3. 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.
  4. 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.

Two-hop, mask 255.255.255.255: BBMD A on 192.168.1.0/24 unicasts to BBMD B, which re-broadcasts on 192.168.2.0/24; this works across any IP network. One-hop, mask 255.255.255.0: BBMD A sends a directed broadcast to 192.168.2.255, which the IP router between the subnets must forward.

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.

  • 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_DeleteBDTEntry refuses to delete the device’s own entry while it is a BBMD.
  • BACnetStack_ResetBroadcastDistributionTable empties the BDT of every IPv4 port and turns BBMD operation off on every port. Add the entries again and call BACnetStack_SetBBMD to resume.

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_RegisterCallbackGetSystemTime first. 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.

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.

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.

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 */
}
}
  • 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 CallbackReinitializeDevice registered). Once the change is activated (ReinitializeDevice ACTIVATE_CHANGES, or BACnetStack_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’s destinationConnectionString so the stack can tell.
  • Debugging. The XML from BACnetStack_GetInfo includes isForeignDevice. Decoding received BVLL frames with BACnetStack_DecodeAs() shows each BVLC function and its table entries.