Skip to content
Version 6.1.0
Contact Us

Moving from BACnet/IP to BACnet/SC

The most important fact about adding BACnet/SC: your existing BACnet application logic does not change.

What stays the same What changes
BACnet services (ReadProperty, WriteProperty, Who-Is, COV, etc.) Transport layer (UDP → WebSocket/TLS)
Object types and property callbacks Four new transport callbacks to implement
Device instance and object model One new Network Port object to add
Send and receive message callbacks (they stay, but must switch on networkPortInstance once a second data link is built) Compile flags to enable
Alarms, schedules, trend logs, BBMD Hub URIs and certificate setup

BACnet/SC is an additional datalink — the stack can run BACnet/IP and BACnet/SC simultaneously on the same device, serving both transports at once.

Step 1 — Enable the BACnet/SC Compile Flags

Section titled “Step 1 — Enable the BACnet/SC Compile Flags”

In source/CIBuildSettings.h, or as CMake options (see Build presets for where the options go):

// Add these alongside your existing flags:
#define STACK_OPTION_DATA_LINK_LAYER_SC
#define STACK_OPTION_OBJECT_TYPE_NETWORK_PORT // required by SC (turned on automatically)
#define STACK_OPTION_OBJECT_TYPE_FILE // required for certificate management
#define STACK_OPTION_NM_SCCM_A // turns on AtomicReadFile/AtomicWriteFile, which
// AddFileObject and the file callbacks need
// (STACK_OPTION_ENABLE_ALL_BIBBS also turns it on)
// Note: STACK_OPTION_DATA_LINK_LAYER_IPV4 must remain defined (current requirement)

Rebuild. Your existing code compiles unchanged — no existing calls break.

These are the only genuinely new code you must write. They bridge the stack to your WebSocket/TLS library.

// Register before BACnetStack_Tick() starts running.
// See section 11 of the BACnet/SC integration guide page for full details on which callbacks are required for each role.
BACnetStack_RegisterCallbackInitiateWebsocket(OnInitiateWebSocket);
BACnetStack_RegisterCallbackDisconnectWebsocket(OnDisconnectWebSocket);
BACnetStack_RegisterCallbackSCStartListening(OnStartListening); // hub/DC role only
BACnetStack_RegisterCallbackSCStopListening(OnStopListening); // hub/DC role only

Minimal implementation for a node-only device (connects to a hub, does not act as a hub):

bool OnInitiateWebSocket(const char* uri, uint32_t uriLength)
{
// Start async WebSocket connection to uri using your library.
// When it opens, call: BACnetStack_SetBACnetSCWebSocketStatus(uri, uriLength, 2, 0)
// When it closes cleanly, call with status 3. When it errors, call with status 4
// and the RFC 6455 close code (see section 19 of the BACnet/SC transport callbacks page).
return myWsLib.BeginConnect(uri);
}
void OnDisconnectWebSocket(const char* uri, uint32_t uriLength) {
myWsLib.Close(uri);
}
// For node-only: stubs are sufficient
bool OnStartListening(const char* uri, uint32_t uriLength) { return false; }
void OnStopListening (const char* uri, uint32_t uriLength) { (void)uri; }

Step 3 — Add the BACnet/SC Network Port Object

Section titled “Step 3 — Add the BACnet/SC Network Port Object”

After your existing BACnetStack_AddDevice call, add a Network Port object for the SC transport. AddNetworkPortObject requires a network number and quality (pass 0, 0 if you are not assigning one):

// Existing code:
BACnetStack_AddDevice(389001);
BACnetStack_AddObject(389001, OBJECT_TYPE_ANALOG_INPUT, 0);
// ... other objects ...
// New: add SC Network Port (use a different instance from your existing IP port)
BACnetStack_AddNetworkPortObject(389001,
1, // instance — must differ from BACnet/IP port
NETWORK_TYPE_SECURE_CONNECT, // 11
PROTOCOL_LEVEL_BACNET_APPLICATION,
0, // networkNumber (0 = not assigned)
0, // networkNumberQuality (0 = unknown)
NETWORK_PORT_LOWEST_PROTOCOL_LAYER);

Step 4 — Set the Device UUID and Hub URIs

Section titled “Step 4 — Set the Device UUID and Hub URIs”
uint8_t uuid[16] = { /* persistent RFC 4122 UUID — see section 9 of the BACnet/SC integration guide page */ };
BACnetStack_SetBACnetSCUuid(uuid, sizeof(uuid));
const char* primaryUri = "wss://hub.example.com:4443/bacnet-sc";
// NULL, 0 for the VMAC: the stack derives it from the UUID, the same on every boot.
BACnetStack_SetBACnetSCHubConnector(
NULL, 0,
primaryUri, (uint16_t)strlen(primaryUri),
NULL, 0); // no failover hub

Step 5 — Configure Timing (Optional — Defaults Are Reasonable)

Section titled “Step 5 — Configure Timing (Optional — Defaults Are Reasonable)”

Omit this call to keep the defaults. If you do call it, pass the BVLC length your build defaults to (shown below): peers and the hub size the frames they send this device by the Max_BVLC_Length_Accepted it advertises, so a smaller value means larger frames for this device are dropped rather than delivered. The sizes cannot go below the stack’s minimums (1600 for BVLC, and 1497 for NPDU); a smaller value makes the call return false and change nothing. See Section 15 for the rules.

BACnetStack_SetBACnetSCSettings(
6144, // maximumBVLCLength (the default in a TARGET_FULL build; 1600 otherwise)
1497, // maximumNPDULength (the minimum and the default)
10, // connectTimeout (seconds)
10, // wsConnectTimeout (seconds)
10, // wsReconnectTimeout (seconds)
300); // heartbeatTimeout (seconds)

For the minimum working configuration, pass your certificate paths to your TLS library inside OnInitiateWebSocket. The certificate File Object management (for over-the-network cert updates, Section 16) can be added later.

bool OnInitiateWebSocket(const char* uri, uint32_t uriLength)
{
myTlsLib.SetClientCert("device_cert.pem");
myTlsLib.SetClientKey("device_key.pem");
myTlsLib.SetTrustedCA("ca_cert.pem");
return myWsLib.BeginConnect(uri);
}

Your existing BACnetStack_Tick() loop drives the SC state machine automatically.

Running BACnet/IP and BACnet/SC Simultaneously

Section titled “Running BACnet/IP and BACnet/SC Simultaneously”

The stack supports both transports on the same device instance at the same time. Add both Network Port objects:

BACnetStack_AddDevice(389001);
// BACnet/IP Network Port (existing)
BACnetStack_AddNetworkPortObject(389001, 0,
NETWORK_TYPE_IPV4,
PROTOCOL_LEVEL_BACNET_APPLICATION,
0, 0, // networkNumber, networkNumberQuality
NETWORK_PORT_LOWEST_PROTOCOL_LAYER);
// BACnet/SC Network Port (new)
BACnetStack_AddNetworkPortObject(389001, 1,
NETWORK_TYPE_SECURE_CONNECT,
PROTOCOL_LEVEL_BACNET_APPLICATION,
0, 0, // networkNumber, networkNumberQuality
NETWORK_PORT_LOWEST_PROTOCOL_LAYER);

Messages arrive on either transport through the one receive callback, and the stack sends through the one send callback. With more than one data link built, both callbacks must switch on networkPortInstance: the send callback uses it to pick the transport, and the receive callback must report the instance of the Network Port the frame arrived on (0 for IP and 1 for SC above). A receive callback that reports BACNET_NETWORK_PORT_DEFAULT loses all traffic, because the stack drops the frame rather than guess its transport. See Data links overview and BACnet/IP for the rule.

  1. Build check: Confirm the build succeeds with STACK_OPTION_DATA_LINK_LAYER_SC defined.
  2. Existing BACnet/IP still works: Run your existing BACnet/IP test suite — confirm no regressions.
  3. SC connection: Monitor the state-change callback until the hub-connection status (stateMachine 1) becomes 1 (connected to primary) or 2 (connected to failover).
  4. Who-Is / I-Am over SC: Use a BACnet client that supports BACnet/SC, connected to the same hub, to discover the device over the SC transport.
  5. ReadProperty over SC: Read Object_Name through the hub.
  6. Dual-transport: Send a ReadProperty over BACnet/IP and over SC simultaneously — both should succeed.
Symptom Cause Fix
SC calls return false; the debug log says the feature was not compiled in STACK_OPTION_DATA_LINK_LAYER_SC not defined Add to build flags
SC state machine never starts BACnetStack_SetBACnetSCWebSocketStatus(connected) not called Call this from your WebSocket open callback
BACnet/IP stops working after migration Accidentally removed STACK_OPTION_DATA_LINK_LAYER_IPV4 Ensure both flags are defined
Hub refuses connection Certificate not trusted by hub Ensure device cert is signed by the hub’s trusted CA
UUID changes on every restart UUID generated at runtime instead of persisted Generate once, store in non-volatile memory