API conventions
These conventions apply to every exported function in CASBACnetStackDLL.h and to every callback you register. They are stated once here rather than on each function, so read them before you rely on any one function’s behaviour.
Return values
Section titled “Return values”- Setup, configuration and “do this now” functions return
bool:trueon success,falseon failure (a bad parameter, a feature not compiled in, an object or device not found, or no space). - Functions that send a confirmed request return
boolfor success or failure, and write the invoke id they used to auint8_t* sentInvokeIdout-parameter. The answer arrives later: a SimpleACK, Error, Reject, Abort or Timeout through the hooks, and an answer that carries data as a raw message through your receive callback, which you decode withBACnetStack_DecodeAs(). See Acting as a client. - Query functions that fill a buffer you supply (for example
BACnetStack_GetInfo()) return the number of bytes or elements written, or 0 on error. - Version numbers and other simple getters return the value directly.
- A
falsereturn does not say why. In a build withSTACK_OPTION_API_DEBUG_LOGGING(on in the prebuilt libraries and the npm package), the debug-message callback (BACnetStack_RegisterCallbackLogDebugMessage()) reports the reason, so register the callback while you develop.
Build options
Section titled “Build options”A function whose documentation carries a note such as “Requires the STACK_OPTION_X build option” does nothing useful in a build without that option: it fails (returns false or 0, or does nothing for a function that returns void). In a build with STACK_OPTION_API_DEBUG_LOGGING, the debug-message callback usually says the feature was not compiled in.
- The note names the option you set. Where that option works by turning on an internal macro, the note names the macro too. Some functions accept any one of several options, and the note lists them.
- The notes are written for a minimal build: the embedded preset (
STACK_OPTION_TARGET_EMBEDDED) with an MS/TP data link. The notes do not repeat the broad switches: the full preset (STACK_OPTION_TARGET_FULL, the default) andSTACK_OPTION_ENABLE_ALL_BIBBSturn most of these options on, though not all of them. CASBACnetStackOptions.hdescribes every option and preset.BACnetStack_GetInfo()reports a summary of the capabilities your build includes, and which of a fixed list of feature macros are enabled. It does not cover every option (object types, for example, are not listed), so check a specific option in your own build settings.
Add* functions called more than once
Section titled “Add* functions called more than once”-
Most
Add*Objectfunctions are idempotent on the object’s identity only. Adding an object that already exists returnstrueand does not apply the other arguments, so a second call with a different configuration silently changes nothing. Atruereturn means “this object exists now”, not “this call created it”. CallBACnetStack_ObjectExists()first if you need to tell them apart. -
Known exceptions; read the function’s own documentation before relying on the rule:
BACnetStack_AddFileObject()applies its arguments again on a repeat call.BACnetStack_AddChannelObject()appliespresentValueDatatypeagain on a repeat call.BACnetStack_AddDevice()returnsfalseon a second call.
C compatibility
Section titled “C compatibility”The header compiles as C99 as well as C++. The exported surface uses no C++ language features, and no parameter has a default value: every caller passes every argument. Where a parameter has a conventional starting value, its documentation says “Recommended value: X”. That is guidance, not a default.
A flat ABI of plain integers
Section titled “A flat ABI of plain integers”Parameters are plain C integers, not BACnet-typed handles: objectType is uint16_t, propertyIdentifier is uint32_t, objectInstance is uint32_t. This is deliberate. It is what lets the C++, C#, Go, Java, Node, Python and Rust bindings be generated from the header, and typed handles will not be introduced. The legal values of an enumerated parameter are in the function’s documentation and in the Enumerations tables.
An object identifier always crosses the API as the pair (uint16_t objectType, uint32_t objectInstance): one BACnet Object Identifier (135-2024 cl. 20.2.14: a 10-bit object type and a 22-bit instance number) carried as two parameters. This is the permanent public form; inside the stack the two are held as one 32-bit identifier. An export refuses a pair it uses that does not fit those widths (a type above 1023, or an instance above 4194303) - it returns false, or its documented failure value, and changes nothing - instead of letting it wrap onto a different object. A pair that an export’s own flag marks as unused (for example useObjectIdentifier set to false) is not checked. A parameter that carries a year in the BACnet wire form (the year minus 1900) is named yearMinus1900, or ends in YearMinus1900; a parameter that takes a full calendar year is never named that way.
One stack per process, and no Init call
Section titled “One stack per process, and no Init call”- The stack is a single global instance. Every function operates on it implicitly: there is no handle parameter, and callbacks carry no user-data pointer. If your application models several devices, tell them apart by the
deviceInstanceargument each callback receives. - There is no explicit initialization call. The instance is constructed during static initialization, and C++ does not define the order of static initialization across translation units, so do not call any function from your own static initializer. Make the first call from
main()or later.
Threading
Section titled “Threading”By default the stack is single-threaded. Make every BACnetStack_* call, BACnetStack_Tick() included, from the same thread. The default build contains no synchronization, so calling it from two threads at once is undefined behaviour, not merely unsupported. If more than one of your threads can reach the stack, serialize them onto one call path yourself, or build with STACK_OPTION_THREAD_SAFE.
A STACK_OPTION_THREAD_SAFE build (off by default) takes one recursive lock in every export, so any thread may call any export and the calls are serialised. Callbacks run with that lock held: they must not block, and must not wait on another thread that is itself waiting to call the stack (a host mutex that one thread holds while calling an export, and a callback takes, deadlocks). A callback may not re-enter the stack: BACnetStack_RemoveDevice and BACnetStack_RemoveObject called from a callback return false and change nothing, BACnetStack_Reset called from a callback logs and does nothing, and the only exports a callback may call are BACnetStack_SCCertificateCancel, BACnetStack_SCCertificateProvideSignedCertificate and BACnetStack_SetBACnetSCWebSocketStatus. BACnetStack_LoopMSTP does not take the lock (it runs on its own thread, alongside BACnetStack_Tick()); drive each MS/TP port’s BACnetStack_LoopMSTP from one thread, since its engine is not thread-safe by itself. An RTOS port supplies the lock with STACK_OPTION_THREAD_SAFE_CUSTOM, BACnetStack_PortLock() and BACnetStack_PortUnlock().
Callbacks
Section titled “Callbacks”These rules apply to every BACnetStack_RegisterCallback* and BACnetStack_RegisterHook* function.
- Registering NULL clears the callback. There is no separate Unregister function. A cleared callback behaves exactly like one that was never registered. Registration does not validate the pointer, so any non-NULL value you register will be called.
- The last registration wins. Registering again replaces the earlier pointer; callbacks do not chain.
- An unregistered callback is not called, and the stack carries on with its own handling for that property or operation. Whether the request then succeeds depends on the property: some are served from the stack’s own stored state, and others have no stack-side source, so the request fails. If your application owns a property, register the matching callback: to the stack, an unregistered callback looks the same as one that declined to supply a value, and neither produces any diagnostic beyond the debug-message callback. See Serving property values.
- Callbacks run on the thread that called into the stack. The stack creates no threads. Most callbacks are called from inside
BACnetStack_Tick(), but not all: everyBACnetStack_Send*function calls the send-message callback before it returns, and some (BACnetStack_SendIAm()among them) also call Get-property callbacks to build the message. - Do not call back into the stack from inside a callback. In particular, do not call
BACnetStack_Reset(), add or remove objects, or send a service from a callback. Queue the work and do it afterBACnetStack_Tick()returns. A thread-safe build refuses the removals and the reset (see Threading above) and supports no other call from a callback except the three named there. - Pointer arguments are valid only during the call. Copy anything you need to keep. An out-buffer you are given belongs to the stack: write at most the stated maximum, and do not keep the pointer.
- **
BACnetStack_Reset()clears every registered callback except the debug-message callback,** which survives a reset. Register NULL for it if you need it gone. After a reset, the stack must be set up again from the beginning. See Stack lifecycle.
errorCode out-parameters
Section titled “errorCode out-parameters”Where a callback ends in uint32_t* errorCode, that value is a BACnet error code, which the stack encodes into the BACnet-Error it sends back to the peer. It is not a channel for host or system errors that have no BACnet meaning. Set it only when you are rejecting the request; leaving it untouched on success is correct.
- For the Get and Set property callbacks, the stack derives the error class from the code. A code that clause 18 lists under exactly one of the DEVICE, OBJECT, RESOURCES, SECURITY and SERVICES classes gets that class: DEVICE for codes such as device-busy and operational-problem, OBJECT for codes such as unknown-object, RESOURCES for the no-space and out-of-memory codes, SECURITY for the security codes (password-failure, for example), SERVICES for codes such as service-request-denied. Every other code, including one listed under several of those classes, gets PROPERTY. abort-other (56) is answered with an Abort instead of an Error.
- The service callbacks (ReinitializeDevice, DeviceCommunicationControl, AcknowledgeAlarm, LifeSafetyOperation, the file-access callbacks and NetworkPortCommand) map codes as their own registration functions describe.
The values are in the Enumerations tables (BACnetErrorCode).
Byte order: the API assumes a little-endian host
Section titled “Byte order: the API assumes a little-endian host”Two different byte orders appear in this API, and they are not the same thing.
-
Fixed orders, independent of your host. Where a parameter’s documentation names an order, that order is part of the BACnet wire format and holds on any host. Examples: the 6-byte IPv4 connection string (4 address octets, then the UDP port as 2 octets, big-endian), and the 2-octet big-endian length prefixes in the AtomicWriteFile record buffers.
-
Host order, which means little-endian. The hand-packed multi-byte buffers whose layout is given by a field diagram in the header are read back in host order:
- the
propertyArraysbuffer ofBACnetStack_SendReadProperty(),BACnetStack_SendReadPropertyWithLength(),BACnetStack_SendWriteProperty()andBACnetStack_SendCreateObject(); - the WriteGroup change-list buffer;
- the COV reference-list buffer of
BACnetStack_SendSubscribeCOVPropertyMultiple(); - the property-value buffer of
BACnetStack_SendUnsubscribedUnconfirmedCOVNotification().
On a big-endian host, every multi-byte field in those buffers would have to be byte-swapped before the call.
- the
-
A build that compiles the stack’s own sources stops with an error on a big-endian target. That check is not in this header, so it does not fire when you compile against a prebuilt library with only
CASBACnetStackDLL.hon the include path.