Skip to content
Version 6.1.0
Contact Us

Acting as a client

This guide shows how your application sends requests to other BACnet devices with the CAS BACnet Stack, and how the answers come back. It assumes you have already added a device and registered the send and receive message callbacks, as in Quick Start. For complete, working programs, see the example projects on Chipkin’s GitHub.

Each service your application can initiate has its own BACnetStack_Send* function. Examples are BACnetStack_SendReadProperty(), BACnetStack_SendWriteProperty(), BACnetStack_SendReadRange(), BACnetStack_SendSubscribeCOV(), BACnetStack_SendCreateObject() and BACnetStack_SendWhoIs(). Each one is a single, stateless call. There is no builder state held between one call and the next.

Most BACnetStack_Send* functions take the destination as the same group of parameters:

  • connectionString and connectionStringLength give the data link address to send to. For BACnet/IP this is 6 bytes: the 4 octets of the IPv4 address, then the 2-octet UDP port in big-endian order. MS/TP and BACnet/SC use a different shape.
  • networkPortInstance is the instance of the Network Port object to send on, the one your application added with BACnetStack_AddNetworkPortObject(). BACNET_NETWORK_PORT_DEFAULT is accepted only in a build with exactly one data link compiled in; in any other build, including the prebuilt libraries, the stack refuses the call (it returns false).
  • destinationNetwork, destinationAddress and destinationAddressLength address a device behind a router. Pass 0, NULL and 0 for a device on the local network.

Unconfirmed services such as BACnetStack_SendWhoIs() also take a broadcast flag.

A function that sends a confirmed request returns bool and writes the request’s invoke id to its first parameter, uint8_t* sentInvokeId. The invoke id is valid only when the function returns true. Keep it: it is how you recognize the answer when it arrives.

Every confirmed BACnetStack_Send* function uses this shape.

A false return means nothing was sent. The return value does not say why. In a build with STACK_OPTION_API_DEBUG_LOGGING (on in the prebuilt libraries and the npm package), the debug message callback (BACnetStack_RegisterCallbackLogDebugMessage()) reports the reason.

A BACnetStack_Send* call does not wait for BACnetStack_Tick(). It encodes the request and calls your send message callback (registered with BACnetStack_RegisterCallbackSendMessageForPort()) before it returns. Your callback must return exactly messageLength when it accepts the frame for transmission, and 0 when it does not. Any other value is treated as a failed send.

For a confirmed request, the stack records the transaction before the frame leaves. If it cannot record it, it does not send the request and the call returns false. One cause is that BACnetStack_RegisterCallbackGetSystemTime() was never called: the stack needs that clock to run the request’s timeout.

Do not call a BACnetStack_Send* function from inside a callback or hook. Calling back into the stack from a callback is not supported. Queue the work and send it after BACnetStack_Tick() returns.

Answers arrive later, as frames that your receive message callback hands to the stack during BACnetStack_Tick(). The stack reports the outcome of a confirmed request through these hooks:

Register with Hook type Called when
BACnetStack_RegisterHookSimpleAck() FPHookSimpleAck The peer acknowledged a request that returns no data.
BACnetStack_RegisterHookError() FPHookError The peer returned an Error, or one property in a ReadPropertyMultiple answer failed.
BACnetStack_RegisterHookReject() FPHookReject The peer rejected the request.
BACnetStack_RegisterHookAbort() FPHookAbort The transaction was aborted.
BACnetStack_RegisterHookTimeout() FPHookTimeout No answer arrived after every retry, or a retry could not be sent.

Each of these hooks receives the originalInvokeId of your request, plus the connection string and source address of the peer. Match on both the invoke id and the peer, not the invoke id alone.

Some details matter when you write the hooks:

  • FPHookError handles two situations. For a BACnet Error PDU, useObjectProperty is false and the object and property fields are always 0. For a failed property inside a ReadPropertyMultiple answer, useObjectProperty is true, and the hook is called once for each failed property. One invoke id can therefore produce several calls, while the other properties in the same answer succeeded.
  • After an Error or an Abort, the stack has already removed the request from its pending table, so no timeout follows for that invoke id.
  • Pointers passed to a hook are valid only for the duration of that call. Copy what you need.

Two more hooks report unconfirmed answers to discovery requests: BACnetStack_RegisterHookIAm() (FPHookIAm) and BACnetStack_RegisterHookIHave() (FPHookIHave).

There is no hook for an answer that carries data, such as a ReadProperty or ReadPropertyMultiple answer, the result of a CreateObject, or an incoming COV notification. By design, these arrive only as ordinary frames through your receive message callback. Because your callback supplies those bytes, you can keep a copy and decode it with BACnetStack_DecodeAs(), which renders a frame as JSON or XML. Match the rendering to your request by invoke id.

Size the output buffer generously: a ReadPropertyMultiple answer can exceed 20 KB, and 64 KB is a safe general-purpose size. If the rendering does not fit, nothing is written and the function returns 0. JSON output needs STACK_OPTION_DECODE_AS_JSON, XML needs STACK_OPTION_DECODE_AS_XML, and both need STACK_OPTION_STRING_LITERALS.

BACnetStack_Tick() drives retries and timeouts, so a slow tick makes timeouts fire late. When a confirmed request times out, the stack sends it again and counts down its retries without notifying you. FPHookTimeout is called once, when every retry is used up. It is called sooner when a retry cannot be sent: the device the request was sent as can no longer initiate, or a run of local send failures reached its cap. A single timeout call can therefore represent several transmissions. A segmented request that times out while its segments are being sent is reported through FPHookAbort with the TSM_TIMEOUT reason instead.

The stack has no dedicated function for these settings. When it sends a confirmed request, it reads the APDU_Timeout (in milliseconds) and Number_Of_APDU_Retries properties of your Device object. If your application serves those properties through the unsigned integer property callback (BACnetStack_RegisterCallbackGetPropertyUnsignedInteger()), your values are used. Otherwise the stack’s defaults apply: 10000 ms and 3 retries, so an unanswered request fails after four timeouts (40 s). Serve Number_Of_APDU_Retries as 0 to send a request once and never retransmit it. An unanswered request holds its place in the outstanding-request table for that whole time, so a host that keeps sending confirmed notifications to a peer that has gone away can fill the table. Serve a smaller retry count or timeout if that matters.

To receive answers too large for one message, enable segmentation with BACnetStack_SetSegmentationEnabled(). With a value of 0 (segmented both) or 2 (segmented receive), your confirmed requests tell the peer that you accept a segmented answer. With 1 or 3, a segmented answer is aborted and reported to FPHookAbort. The function returns false if the stack was built without STACK_OPTION_SEGMENTATION.

apduSegmentTimeout is in milliseconds. While a segmented answer is arriving, the stack waits up to four times that value for each next segment. If it does not arrive, the stack abandons the transaction and reports FPHookAbort. While segments are arriving, the request is not retried. Read the function’s own documentation for the ranges it enforces.

Some functions take a request list as a packed byte buffer rather than as separate parameters. These are BACnetStack_SendReadProperty(), BACnetStack_SendReadPropertyWithLength(), BACnetStack_SendWriteProperty() and BACnetStack_SendCreateObject() (the propertyArrays buffer), BACnetStack_SendWriteGroup() (the changeList buffer), BACnetStack_SendSubscribeCOVPropertyMultiple() (the references buffer) and BACnetStack_SendUnsubscribedUnconfirmedCOVNotification() (the properties buffer). Each function’s documentation has a field diagram and a worked example.

Each of these functions takes an element count and the buffer’s total byte length. Pass the real length: the read, COV-subscription and COV-notification functions reject a count that would read past it. The serviceType argument picks the service: for BACnetStack_SendReadProperty(), 12 is ReadProperty (count must be 1) and 14 is ReadPropertyMultiple. For BACnetStack_SendWriteProperty(), 15 is WriteProperty and 16 is WritePropertyMultiple.

Byte order. The multi-byte fields in these packed buffers are read in host byte order, and the API assumes a little-endian host. On a big-endian host you must byte-swap each field yourself. Do not confuse this with the orders that a parameter’s documentation names explicitly, such as the big-endian port in a BACnet/IP connection string. Those are part of the BACnet wire format and do not depend on your host.

This example reads the Present_Value of Analog Input 1 from a device on the local network:

uint8_t propertyArray[15] = {
0x00, /* flags: no array index */
0x00, 0x00, /* objectType 0 = analog-input */
0x01, 0x00, 0x00, 0x00, /* objectInstance 1 */
0x55, 0x00, 0x00, 0x00, /* propertyIdentifier 85 = Present_Value */
0x00, 0x00, 0x00, 0x00 /* propertyArrayIndex, unused */
};
uint8_t destination[6] = { 192, 168, 1, 5, 0xBA, 0xC0 }; /* 192.168.1.5:47808 */
const uint32_t networkPortInstance = 1; /* the Network Port object your application added */
uint8_t invokeId = 0;
if (BACnetStack_SendReadProperty(&invokeId, 12, destination, sizeof(destination),
networkPortInstance, 0, NULL, 0,
1, propertyArray, sizeof(propertyArray))) {
/* Remember invokeId. The answer arrives during a later BACnetStack_Tick(). */
}

For BACnetStack_SendWriteProperty() and BACnetStack_SendCreateObject(), each element also carries a datatype and a value. Read those functions’ documentation for the value encoding before you pack one.