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.
The shape of a request
Section titled “The shape of a request”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:
connectionStringandconnectionStringLengthgive 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.networkPortInstanceis the instance of the Network Port object to send on, the one your application added withBACnetStack_AddNetworkPortObject().BACNET_NETWORK_PORT_DEFAULTis 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 returnsfalse).destinationNetwork,destinationAddressanddestinationAddressLengthaddress a device behind a router. Pass 0,NULLand 0 for a device on the local network.
Unconfirmed services such as BACnetStack_SendWhoIs() also take a broadcast flag.
Confirmed requests return an invoke id
Section titled “Confirmed requests return an invoke id”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.
Sending happens immediately
Section titled “Sending happens immediately”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.
How the answers arrive
Section titled “How the answers arrive”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:
FPHookErrorhandles two situations. For a BACnet Error PDU,useObjectPropertyisfalseand the object and property fields are always 0. For a failed property inside a ReadPropertyMultiple answer,useObjectPropertyistrue, 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).
Answers that carry data
Section titled “Answers that carry data”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.
Timeouts and retries
Section titled “Timeouts and retries”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.
Segmentation
Section titled “Segmentation”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.
Hand-packed buffers
Section titled “Hand-packed buffers”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.