Skip to content

Reach devices on other subnets

Connect to a network / Across subnets

BACnet/IP discovery runs on broadcasts, and IP routers don’t forward them. A read sent to a device’s known address crosses routers like any other UDP datagram; a Who-Is doesn’t. Annex J offers two ways to carry broadcasts further: a broadcast management device (BBMD) on each subnet, which forwards them to the BBMDs listed in its Broadcast Distribution Table (BDT), and foreign-device registration, where a node on a subnet with no BBMD registers with a remote BBMD and is listed in its Foreign Device Table (FDT).

The page describes v0.15.0.

Situation What to use
You know the device’s address and only read or write it Nothing: send to IP:port directly
You need discovery, or broadcasts such as I-Am, from one host on a subnet with no BBMD Have that host register with a BBMD on the devices’ network
Several subnets form one BACnet network A BBMD on each subnet, each listing the others in its BDT
Sites joined across the internet Prefer BACnet/SC; a BBMD behind a NAT router is covered in BACnet/IP across NAT

A B/IP node runs in one mode: normal (the default), foreign device, or BBMD. Rust names it BipMode, and Python raises ValueError when given both bbmd= and foreign_device=, so a BBMD that also registers as a foreign device can’t be built.

The CLI reads both tables of any BBMD it can reach. These commands are BACnet/IP only:

Terminal window
bacnet bdt 192.168.1.10
bacnet fdt 192.168.1.10

A BDT row is a peer’s address, port and broadcast mask; the BBMD’s own row is in it too. An FDT row is a registered foreign device with the TTL it asked for and the seconds left, which include a 30-second grace period. A device that isn’t a BBMD answers with a BVLC-Result NAK, or stays silent until the request times out; either way the command fails. In Python, read_bdt and read_fdt return the same rows; see BBMD management.

The global --foreign-bbmd option makes the CLI’s transport a foreign device. It registers when it starts, renews on its own and sends its broadcasts through the BBMD, so discover reaches the BBMD’s network:

Terminal window
bacnet --foreign-bbmd 192.168.1.10 discover
bacnet --foreign-bbmd 192.168.1.10:47808 --ttl 600 --renewal 20 discover 1000-2000

The BBMD takes IP or IP:port, with port 47808 by default. A one-shot command waits for the first registration to be answered, and fails when the BBMD refuses or stays silent. While the transport manages the registration, register and unregister to that BBMD are refused. Log lines go to stderr, so --json output stays clean. The terminal UI shows the registration, its countdown and the last BVLC-Result on its Network screen; see the CLI reference.

register sends one registration from an otherwise normal node and exits without renewing it, so its broadcasts stay local. unregister deletes an FDT entry and is a write:

Terminal window
bacnet register 192.168.1.10 --ttl 300
bacnet --interface 192.168.2.50 --allow-write unregister 192.168.1.10

unregister names the CLI’s own local address. On the default 0.0.0.0 bind that may not be the address the BBMD recorded, so give --interface with the address you registered from. The BBMD also has to allow the deletion; see Limit who can change a BBMD.

  • TTL is how many seconds the BBMD keeps the entry: 300 by default in the CLI and Python, 1 to 65535 on the wire. This stack’s BBMD refuses a TTL over 3600 seconds under its default policy, so check what the BBMD you use accepts.
  • Renewal is the time between registrations, half the TTL by default. It must be shorter than the TTL. Python takes 0.1 seconds or more; Rust takes any positive interval and raises one below 100 ms to 100 ms.
  • Behind a NAT router, keep the renewal below the router’s UDP mapping timeout, which is 30 seconds or less on many routers, and keep the TTL long. The NAT page explains why.
  • When the BBMD has no entry for you, it refuses the broadcasts you send through it (Distribute-Broadcast-To-Network) with a NAK. The client, server or endpoint counts each one in renewal_hints and brings its next registration forward. A lease that ran out with no newer success counts in lapses.

In Python, give a server, client or endpoint a Bbmd:

from rusty_bacnet import BACnetServer, Bbmd
server = BACnetServer(
1234, interface="192.168.1.10", broadcast_address="192.168.1.255",
bbmd=Bbmd(["10.0.5.2:47808"], accept_foreign_devices=True,
management_acl=["192.168.1.100"]),
)
  • BDT rows. A plain "ip:port" row is a peer the BBMD forwards to by unicast (an all-ones mask). A ("ip:port", "mask") pair gives another mask. The BBMD’s own row is added when it is missing. Give each BBMD of the network a row for every other one.
  • An explicit interface and that subnet’s broadcast address avoid guessing on a host with several addresses. A BBMD bound to 0.0.0.0 reads its own address from the BDT, or fails to start and asks for one; see BBMD.
  • Foreign devices are refused unless accept_foreign_devices=True.
  • Persisted BDT. bdt_persist_path names a file to start from, and a BDT activated through the registered Network Port is saved there. Write-BDT from the network is always refused; change the BDT through the Network Port and ReinitializeDevice instead.

In Rust, the same options are fields of BbmdOptions, passed as BipMode::Bbmd(..) on a builder or set by hand on a BipTransport. A BBMD holds at most 128 BDT rows and 128 FDT entries.

A BBMD answers anyone who can send it a datagram, and B/IP has no authentication. Its limits are what protect it:

  • Management access list. management_acl (Python) or BbmdOptions::management_acl (Rust) lists the IPv4 addresses allowed to delete FDT entries. Empty, the default, denies every source, so an unregister gets a NAK.
  • Foreign-device policy. In Rust, ForeignDevicePolicy sets the accepted TTL range (1 to 3600 seconds by default), an optional list of allowed source addresses, at most 8 entries per source address, registration rates (8 per source and 64 overall per second by default), reserved slots and how many foreign devices receive each broadcast (32). Python’s accept_foreign_devices=True applies the defaults.
  • Fan-out budgets. FanoutPolicy bounds how many datagrams one broadcast can turn into, and fanout_counters() shows what was throttled.

The BBMD section covers forwarding and the budgets in detail.

Release the old BBMD’s entry after a move

Section titled “Release the old BBMD’s entry after a move”

A foreign device moved to another BBMD through its Network Port (FD_BBMD_Address, then ReinitializeDevice) can also ask the old BBMD to delete its entry, so it goes before its TTL and grace period run out. Turn it on with BipTransport::set_release_previous_bbmd(true), after register_as_foreign_device; it is off by default. A NAK or silence never fails the move and is counted in the snapshot’s previous_release_refused and previous_release_unanswered. The request names the device’s own local address, so behind a NAT router, or on a host whose registration left from another address, the old BBMD knows the device by another address and will usually refuse. This stack’s BBMD refuses unless its management access list names the device. Python doesn’t expose it. See Registered B/IP Network Port.

CLI tests run --foreign-bbmd discovery through a BBMD, a refused and a silent registration, and register and unregister against an accepting and a refusing BBMD. Python tests cover a foreign client discovering a server through its BBMD, a refused registration, and the BDT and FDT round trip with its NAKs. All run on one host’s loopback interface; none qualifies a routed site network.

Release v0.15.0 ·Build and integrate. Pre-1.0 APIs; partial conformance.Support & limitations