Skip to main content

ControlForge BACnet Protocol Guide

James M. Belcher Founder, JMB Technical Services LLC April 2026 | ControlForge v1.0.533


1. Architecture Overview

ControlForge implements a complete BACnet/IP stack — both client and server — callable directly from IEC 61131-3 Structured Text. No external BACnet libraries, no EDE files, no vendor configuration tools. You create connections, read/write BACnet objects, subscribe to change-of-value (COV) notifications, and expose points to BMS systems with plain function calls in your ST programs.

RoleFunctionsUse Case
ClientBACNET_CLIENT_CREATE / BACNET_READ_* / BACNET_WRITE_* / BACNET_SUBSCRIBE_COVPoll and command BACnet devices: AHUs, VAVs, chillers, meters, other controllers
ServerBACNET_SERVER_CREATE / BACNET_SERVER_SET_* / BACNET_SERVER_GET_*Expose ControlForge data to BMS front-ends, operator workstations, or third-party controllers

Both roles can run simultaneously. A single ControlForge instance can poll a dozen VAV controllers as a client while serving zone data to a Tridium Niagara front-end — all from the same ST program.

System Diagram

BACnet Object Model

BACnet organizes all data into typed objects, each with a set of properties. The most commonly used objects in HVAC/BMS applications:

Object TypeConstantTypical Use
Analog InputBACNET_OBJECT_AISensor readings: temperature, pressure, humidity, flow
Analog OutputBACNET_OBJECT_AOControl outputs: valve position, damper command, VFD speed
Analog ValueBACNET_OBJECT_AVSetpoints, tuning parameters, calculated values
Binary InputBACNET_OBJECT_BIStatus signals: fan running, filter alarm, occupancy
Binary OutputBACNET_OBJECT_BOOn/off commands: fan start, pump enable, lighting relay
Binary ValueBACNET_OBJECT_BVMode flags: occupied/unoccupied, auto/manual, enable/disable
Multi-State InputBACNET_OBJECT_MSIEnumerated status: operating mode, fault code
Multi-State OutputBACNET_OBJECT_MSOEnumerated commands: speed stage, mode select
Multi-State ValueBACNET_OBJECT_MSVEnumerated setpoints: schedule mode, season

BACnet Property Constants

Every BACnet object has properties. ControlForge provides constants for the most commonly accessed ones:

ConstantDescription
BACNET_PROP_PRESENT_VALUECurrent value of the object — the most-read property
BACNET_PROP_OBJECT_NAMEHuman-readable name string
BACNET_PROP_DESCRIPTIONFree-text description
BACNET_PROP_UNITSEngineering units (degrees-F, PSI, CFM, etc.)
BACNET_PROP_PRIORITY_ARRAY16-level command priority array (outputs only)
BACNET_PROP_RELINQUISH_DEFAULTValue used when all priority slots are NULL

Priority Array: BACnet outputs (AO, BO, MSO) use a 16-level priority scheme. Priority 1 is highest (life safety), priority 16 is lowest (default). When you write to an output, you specify which priority slot to claim. The device uses the highest-priority non-NULL value. This prevents a scheduling override from fighting a life-safety shutdown.


2. Client Functions

The BACnet client connects to remote BACnet/IP devices and performs read/write/subscribe operations using standard BACnet services.

2.1 Connection Management

BACNET_CLIENT_CREATE — Create Named Connection

ParamTypeRequiredDescription
nameSTRINGYesUnique connection name
targetIPSTRINGYesIP address of the BACnet device
deviceIDINTYesBACnet device instance number
localPortINTNoLocal UDP port (default 47808)
targetPortINTNoTarget UDP port (default 47808)

Returns: BOOL — TRUE if the connection was created successfully.

(* Connect to an AHU controller at 10.0.1.100, device ID 1001 *)
ok := BACNET_CLIENT_CREATE('ahu1', '10.0.1.100', 1001);

(* Connect to a device on a non-standard port *)
ok := BACNET_CLIENT_CREATE('vav3', '10.0.1.50', 3050, 47808, 47809);

Named connections: Every BACnet client connection has a unique string name. This name is used in all subsequent calls. Create one connection per BACnet device — ControlForge manages the UDP sockets internally.

BACNET_CLIENT_CONNECT — Establish Connection

ParamTypeDescription
nameSTRINGConnection name from BACNET_CLIENT_CREATE

Returns: BOOL — TRUE if connected successfully.

ok := BACNET_CLIENT_CONNECT('ahu1');

BACNET_CLIENT_DISCONNECT — Close Connection

ParamTypeDescription
nameSTRINGConnection name

Returns: BOOL — TRUE if disconnected successfully.

ok := BACNET_CLIENT_DISCONNECT('ahu1');

BACNET_CLIENT_IS_CONNECTED — Check Connection State

ParamTypeDescription
nameSTRINGConnection name

Returns: BOOL — TRUE if the connection is active.

IF NOT BACNET_CLIENT_IS_CONNECTED('ahu1') THEN
BACNET_CLIENT_CONNECT('ahu1');
END_IF;

BACNET_CLIENT_DELETE — Remove Connection

ParamTypeDescription
nameSTRINGConnection name

Returns: BOOL — TRUE if deleted successfully.

ok := BACNET_CLIENT_DELETE('ahu1');

BACNET_CLIENT_LIST — List All Connections

Returns: []STRING — Array of connection names.

clients := BACNET_CLIENT_LIST();
(* Returns: ['ahu1', 'vav3', 'chiller1'] *)

Example: Connection Lifecycle

PROGRAM POU_BACnetInit
VAR
state : INT := 0;
ok : BOOL;
END_VAR

CASE state OF
0: (* Create connection *)
ok := BACNET_CLIENT_CREATE('ahu1', '10.0.1.100', 1001);
IF ok THEN
state := 1;
END_IF;

1: (* Connect *)
ok := BACNET_CLIENT_CONNECT('ahu1');
IF ok THEN
state := 10;
END_IF;

10: (* Running — read/write in other programs *)
IF NOT BACNET_CLIENT_IS_CONNECTED('ahu1') THEN
state := 1; (* Reconnect *)
END_IF;
END_CASE;
END_PROGRAM

2.2 Generic Read/Write

These functions work with any BACnet object type and property. Use the object type and property constants for clarity.

BACNET_READ_PROPERTY — Read Any Property

ParamTypeDescription
nameSTRINGConnection name
objectTypeINTBACnet object type constant
objectInstanceINTObject instance number
propertyINTBACnet property constant

Returns: ANY — Value type depends on the property.

(* Read the present value of Analog Input 1 *)
temp := BACNET_READ_PROPERTY('ahu1',
BACNET_OBJECT_AI, 1,
BACNET_PROP_PRESENT_VALUE);
(* Returns: 72.5 *)

(* Read the object name *)
name := BACNET_READ_PROPERTY('ahu1',
BACNET_OBJECT_AI, 1,
BACNET_PROP_OBJECT_NAME);
(* Returns: 'ZN-T' *)

(* Read the engineering units *)
units := BACNET_READ_PROPERTY('ahu1',
BACNET_OBJECT_AI, 1,
BACNET_PROP_UNITS);
(* Returns: 64 (degrees-Fahrenheit) *)

(* Read the priority array of an Analog Output *)
priorities := BACNET_READ_PROPERTY('ahu1',
BACNET_OBJECT_AO, 1,
BACNET_PROP_PRIORITY_ARRAY);
(* Returns: [NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,72.0,NULL,NULL,NULL,NULL,NULL,NULL,NULL] *)

BACNET_WRITE_PROPERTY — Write Any Property

ParamTypeDescription
nameSTRINGConnection name
objectTypeINTBACnet object type constant
objectInstanceINTObject instance number
propertyINTBACnet property constant
valueANYValue to write

Returns: BOOL — TRUE if the write was acknowledged.

(* Write a description *)
ok := BACNET_WRITE_PROPERTY('ahu1',
BACNET_OBJECT_AV, 5,
BACNET_PROP_DESCRIPTION, 'Cooling setpoint offset');

Present Value Writes: For writing present values to outputs with priority, use BACNET_WRITE_PRIORITY instead. Direct writes to PresentValue via BACNET_WRITE_PROPERTY go to priority 16 (lowest) and may be overridden by higher-priority commands.

BACNET_READ_PRESENT_VALUE — Read Present Value (Shorthand)

ParamTypeDescription
nameSTRINGConnection name
objectTypeINTBACnet object type constant
objectInstanceINTObject instance number

Returns: ANY — Current present value of the object.

(* These two calls are equivalent *)
temp := BACNET_READ_PRESENT_VALUE('ahu1', BACNET_OBJECT_AI, 1);
temp := BACNET_READ_PROPERTY('ahu1', BACNET_OBJECT_AI, 1, BACNET_PROP_PRESENT_VALUE);

BACNET_WRITE_PRESENT_VALUE — Write Present Value (Shorthand)

ParamTypeDescription
nameSTRINGConnection name
objectTypeINTBACnet object type constant
objectInstanceINTObject instance number
valueANYValue to write

Returns: BOOL — TRUE if acknowledged.

ok := BACNET_WRITE_PRESENT_VALUE('ahu1', BACNET_OBJECT_AV, 5, 72.0);

2.3 Priority Array and Relinquish

BACnet output objects (AO, BO, BV, AV when commandable, MSO, MSV when commandable) support a 16-level priority array. This is fundamental to BMS control — it prevents conflicts between life safety, manual overrides, scheduled operations, and default programming.

BACnet Priority Levels (ASHRAE 135)

PriorityLevelTypical Use
1Manual-Life SafetyFire alarm shutdown
2Automatic-Life SafetySmoke control sequences
3Available
4Available
5Critical Equipment ControlChiller staging
6Minimum On/OffFreeze protection
7Available
8Manual OperatorOperator overrides from workstation
9Available
10Available
11Available
12Available
13Available
14Available
15Available
16Available (Lowest)Default / scheduling

BACNET_WRITE_PRIORITY — Write at Specific Priority

ParamTypeDescription
nameSTRINGConnection name
objectTypeINTBACnet object type constant
objectInstanceINTObject instance number
valueANYValue to write
priorityINTPriority level (1-16)

Returns: BOOL — TRUE if acknowledged.

(* Write cooling valve to 75% at priority 8 (operator override) *)
ok := BACNET_WRITE_PRIORITY('ahu1',
BACNET_OBJECT_AO, 1,
75.0, 8);

(* Write fan command ON at priority 5 (critical equipment) *)
ok := BACNET_WRITE_PRIORITY('ahu1',
BACNET_OBJECT_BO, 1,
TRUE, 5);

(* Write occupied cooling setpoint at priority 16 (scheduling) *)
ok := BACNET_WRITE_PRIORITY('ahu1',
BACNET_OBJECT_AV, 10,
72.0, 16);

BACNET_RELINQUISH — Release a Priority Slot

ParamTypeDescription
nameSTRINGConnection name
objectTypeINTBACnet object type constant
objectInstanceINTObject instance number
priorityINTPriority level to release (1-16)

Returns: BOOL — TRUE if acknowledged.

When you relinquish a priority slot, it becomes NULL. The device then uses the next highest-priority non-NULL value, or the relinquish default if all slots are NULL.

(* Release the operator override — control returns to scheduling *)
ok := BACNET_RELINQUISH('ahu1',
BACNET_OBJECT_AO, 1,
8);

Example: Override with Automatic Release

PROGRAM POU_OverrideControl
VAR
override_active : BOOL := FALSE;
override_timer : INT := 0;
override_duration : INT := 600; (* 60 seconds at 100ms scan *)
ok : BOOL;
END_VAR

IF override_active THEN
override_timer := override_timer + 1;

IF override_timer >= override_duration THEN
(* Time expired — relinquish override *)
ok := BACNET_RELINQUISH('ahu1',
BACNET_OBJECT_AO, 1, 8);
override_active := FALSE;
override_timer := 0;
END_IF;
ELSE
(* Normal operation — write at priority 16 *)
ok := BACNET_WRITE_PRIORITY('ahu1',
BACNET_OBJECT_AO, 1,
pid_output, 16);
END_IF;
END_PROGRAM

2.4 Typed Convenience Functions

These wrap BACNET_READ_PRESENT_VALUE / BACNET_WRITE_PRESENT_VALUE for the six most common object types. They return properly typed values and require only the connection name and instance number — the object type is implied by the function name.

Analog Reads

FunctionObject TypeReturns
BACNET_READ_AI(name, instance)Analog InputREAL
BACNET_READ_AO(name, instance)Analog OutputREAL
BACNET_READ_AV(name, instance)Analog ValueREAL
(* Read zone temperature from AI-1 *)
zone_temp := BACNET_READ_AI('vav3', 1);

(* Read current damper position from AO-1 *)
damper_pos := BACNET_READ_AO('vav3', 1);

(* Read cooling setpoint from AV-10 *)
clg_sp := BACNET_READ_AV('vav3', 10);

Binary Reads

FunctionObject TypeReturns
BACNET_READ_BI(name, instance)Binary InputBOOL
BACNET_READ_BO(name, instance)Binary OutputBOOL
BACNET_READ_BV(name, instance)Binary ValueBOOL
(* Read fan status from BI-1 *)
fan_running := BACNET_READ_BI('ahu1', 1);

(* Read fan command from BO-1 *)
fan_cmd := BACNET_READ_BO('ahu1', 1);

(* Read occupancy mode from BV-5 *)
occupied := BACNET_READ_BV('ahu1', 5);

Analog Writes

FunctionObject TypeParamReturns
BACNET_WRITE_AO(name, instance, value)Analog OutputREALBOOL
BACNET_WRITE_AV(name, instance, value)Analog ValueREALBOOL
(* Command damper to 50% *)
ok := BACNET_WRITE_AO('vav3', 1, 50.0);

(* Write cooling setpoint *)
ok := BACNET_WRITE_AV('vav3', 10, 74.0);

Binary Writes

FunctionObject TypeParamReturns
BACNET_WRITE_BO(name, instance, value)Binary OutputBOOLBOOL
BACNET_WRITE_BV(name, instance, value)Binary ValueBOOLBOOL
(* Start supply fan *)
ok := BACNET_WRITE_BO('ahu1', 1, TRUE);

(* Set occupied mode *)
ok := BACNET_WRITE_BV('ahu1', 5, TRUE);

No Write for AI/BI: Analog Inputs and Binary Inputs are read-only by definition. There is no BACNET_WRITE_AI or BACNET_WRITE_BI. If you need a writable analog point, use Analog Value (AV). If you need a writable binary point, use Binary Value (BV).


2.5 Device Discovery (WhoIs)

BACnet provides a broadcast discovery mechanism. WhoIs sends a broadcast (or directed) request, and all BACnet devices in the specified range respond with their device instance, IP address, and other identifying information.

BACNET_WHO_IS — Discover Devices

ParamTypeRequiredDescription
nameSTRINGYesConnection name (uses its UDP socket)
lowLimitINTNoLowest device instance to find
highLimitINTNoHighest device instance to find

Returns: []MAP — Array of device descriptors.

(* Discover ALL BACnet devices on the network *)
devices := BACNET_WHO_IS('ahu1');
(* Returns:
[
{"device_id": 1001, "ip": "10.0.1.100", "vendor": "Trane"},
{"device_id": 1002, "ip": "10.0.1.101", "vendor": "Trane"},
{"device_id": 3050, "ip": "10.0.1.50", "vendor": "Distech"}
]
*)

(* Discover devices in a specific range *)
devices := BACNET_WHO_IS('ahu1', 1000, 1099);
(* Returns only devices with instance 1000-1099 *)

(* Find a single device *)
devices := BACNET_WHO_IS('ahu1', 1001, 1001);

Network Broadcast: WhoIs uses UDP broadcast. All devices on the local subnet will respond. For routed BACnet networks (BACnet/IP to MS/TP), devices behind BACnet routers will also respond if the router forwards the broadcast. Response time varies — allow 2-5 seconds for all devices to reply, especially with MS/TP segments.

Example: Auto-Discovery and Inventory

PROGRAM POU_Discovery
VAR
state : INT := 0;
devices : ARRAY[0..99] OF MAP;
device_count : INT;
ok : BOOL;
i : INT;
END_VAR

CASE state OF
0: (* Create a temporary connection for discovery *)
ok := BACNET_CLIENT_CREATE('scanner', '255.255.255.255', 0);
IF ok THEN
ok := BACNET_CLIENT_CONNECT('scanner');
state := 1;
END_IF;

1: (* Send WhoIs broadcast *)
devices := BACNET_WHO_IS('scanner');
device_count := LEN(devices);
state := 2;

2: (* Log discovered devices *)
FOR i := 0 TO device_count - 1 DO
LOG(CONCAT('Found device ', INT_TO_STRING(devices[i].device_id),
' at ', devices[i].ip));
END_FOR;
state := 10;

10: (* Done *)
BACNET_CLIENT_DELETE('scanner');
END_CASE;
END_PROGRAM

2.6 Change of Value (COV) Subscriptions

Instead of polling, COV lets you subscribe to a BACnet object and receive asynchronous notifications when its value changes. This reduces network traffic and provides near-instant updates for critical points.

BACNET_SUBSCRIBE_COV — Create Subscription

ParamTypeDescription
nameSTRINGConnection name
objectTypeINTBACnet object type constant
objectInstanceINTObject instance number
lifetimeINTSubscription lifetime in seconds (0 = indefinite)

Returns: INT — Subscription ID (used for unsubscribe), or -1 on failure.

(* Subscribe to zone temperature changes — 1 hour lifetime *)
sub_id := BACNET_SUBSCRIBE_COV('vav3',
BACNET_OBJECT_AI, 1,
3600);

(* Subscribe indefinitely to fan status *)
sub_id2 := BACNET_SUBSCRIBE_COV('ahu1',
BACNET_OBJECT_BI, 1,
0);

COV Increment: The remote device determines when to send notifications based on its configured COV increment. For analog objects, this is typically 0.1-1.0 units. For binary objects, any state change triggers a notification. The notification updates the cached present value, which you read with BACNET_READ_PRESENT_VALUE or the typed convenience functions.

Lifetime Management: When the lifetime expires, the subscription ends silently. Set lifetime to 0 for indefinite subscriptions, or re-subscribe periodically. Some devices limit the number of active COV subscriptions (typically 16-64). Use COV for critical points and poll the rest.

BACNET_UNSUBSCRIBE_COV — Cancel Subscription

ParamTypeDescription
nameSTRINGConnection name
subscriptionIDINTSubscription ID from BACNET_SUBSCRIBE_COV

Returns: BOOL — TRUE if unsubscribed successfully.

ok := BACNET_UNSUBSCRIBE_COV('vav3', sub_id);

Example: COV-Driven Zone Monitoring

PROGRAM POU_COVMonitor
VAR
state : INT := 0;
sub_temp : INT;
sub_fan : INT;
zone_temp : REAL;
fan_status : BOOL;
alarm_active : BOOL := FALSE;
high_temp_limit : REAL := 85.0;
END_VAR

CASE state OF
0: (* Subscribe to critical points *)
sub_temp := BACNET_SUBSCRIBE_COV('ahu1',
BACNET_OBJECT_AI, 1, 0);
sub_fan := BACNET_SUBSCRIBE_COV('ahu1',
BACNET_OBJECT_BI, 1, 0);
IF sub_temp >= 0 AND sub_fan >= 0 THEN
state := 10;
END_IF;

10: (* Monitor — values update automatically via COV *)
zone_temp := BACNET_READ_AI('ahu1', 1);
fan_status := BACNET_READ_BI('ahu1', 1);

(* High temperature alarm *)
IF zone_temp > high_temp_limit AND NOT fan_status THEN
alarm_active := TRUE;
(* Force fan ON at high priority *)
BACNET_WRITE_PRIORITY('ahu1',
BACNET_OBJECT_BO, 1,
TRUE, 5);
ELSIF zone_temp < (high_temp_limit - 2.0) THEN
IF alarm_active THEN
BACNET_RELINQUISH('ahu1',
BACNET_OBJECT_BO, 1, 5);
alarm_active := FALSE;
END_IF;
END_IF;
END_CASE;
END_PROGRAM

2.7 Alarms and Statistics

BACNET_GET_ALARMS — Read Active Alarms

ParamTypeDescription
nameSTRINGConnection name

Returns: []MAP — Array of active alarm entries from the device.

alarms := BACNET_GET_ALARMS('ahu1');
(* Returns:
[
{"object_type": 0, "instance": 3, "state": "high-limit",
"value": 87.2, "timestamp": "2026-04-03T14:22:00Z"},
{"object_type": 4, "instance": 1, "state": "offnormal",
"value": 0, "timestamp": "2026-04-03T14:20:15Z"}
]
*)

BACNET_GET_STATS — Connection Statistics

Returns: MAP — Statistics for the named BACnet client or server.

stats := BACNET_GET_STATS('ahu1');
(* Returns:
{
"requests_sent": 12450,
"responses_received": 12448,
"timeouts": 2,
"cov_notifications": 873,
"errors": 0,
"uptime_seconds": 86400
}
*)

3. Server Functions

The BACnet server exposes ControlForge data as standard BACnet objects. Any BMS front-end, operator workstation, or third-party controller that speaks BACnet/IP can read and write these points without any custom integration.

3.1 Server Lifecycle

BACNET_SERVER_CREATE — Create Server Instance

ParamTypeDescription
nameSTRINGUnique server name
portINTUDP listen port (typically 47808)
device_idINTBACnet device instance to advertise

Returns: BOOL — TRUE if created successfully.

(* Create a BACnet server — device ID 99001 *)
ok := BACNET_SERVER_CREATE('bms_server', 47808, 99001);

Device ID: Every BACnet device on the network must have a unique device instance number. Coordinate with the BMS integrator to avoid conflicts. Common convention: 99xxx for soft controllers, leaving lower ranges for hardware controllers.

BACNET_SERVER_START — Begin Listening

ParamTypeDescription
nameSTRINGServer name

Returns: BOOL — TRUE if started.

ok := BACNET_SERVER_START('bms_server');

BACNET_SERVER_STOP — Stop Listening

ParamTypeDescription
nameSTRINGServer name

Returns: BOOL — TRUE if stopped.

ok := BACNET_SERVER_STOP('bms_server');

BACNET_SERVER_IS_RUNNING — Check Server State

ParamTypeDescription
nameSTRINGServer name

Returns: BOOL — TRUE if the server is actively listening.

IF NOT BACNET_SERVER_IS_RUNNING('bms_server') THEN
BACNET_SERVER_START('bms_server');
END_IF;

BACNET_SERVER_DELETE — Remove Server

ParamTypeDescription
nameSTRINGServer name

Returns: BOOL — TRUE if deleted.

ok := BACNET_SERVER_DELETE('bms_server');

BACNET_SERVER_LIST — List All Servers

Returns: []STRING — Array of server names.

servers := BACNET_SERVER_LIST();

3.2 Setting Server Point Values

Use these to push ControlForge data into server objects. Remote BACnet clients will read these values.

BACNET_SERVER_SET_AI — Set Analog Input Value

ParamTypeDescription
nameSTRINGServer name
instanceINTObject instance number
valueREALAnalog value

Returns: BOOL — TRUE if set.

(* Expose zone temperature as AI-1 *)
ok := BACNET_SERVER_SET_AI('bms_server', 1, zone_temp);

(* Expose discharge air temperature as AI-2 *)
ok := BACNET_SERVER_SET_AI('bms_server', 2, dat);

BACNET_SERVER_SET_BI — Set Binary Input Value

ParamTypeDescription
nameSTRINGServer name
instanceINTObject instance number
valueBOOLBinary value

Returns: BOOL — TRUE if set.

(* Expose fan status as BI-1 *)
ok := BACNET_SERVER_SET_BI('bms_server', 1, fan_running);

BACNET_SERVER_SET_AV — Set Analog Value

ParamTypeDescription
nameSTRINGServer name
instanceINTObject instance number
valueREALAnalog value

Returns: BOOL — TRUE if set.

(* Expose PID output as AV-1 *)
ok := BACNET_SERVER_SET_AV('bms_server', 1, pid_output);

3.3 Reading Commanded Values

When a remote BACnet client writes to your server's output objects, use these to read the commanded values.

BACNET_SERVER_GET_AV — Read Analog Value (Written by Remote)

ParamTypeDescription
nameSTRINGServer name
instanceINTObject instance number

Returns: REAL — Current value.

(* Read setpoint written by the BMS front-end *)
remote_setpoint := BACNET_SERVER_GET_AV('bms_server', 10);

BACNET_SERVER_GET_AO — Read Analog Output (Written by Remote)

ParamTypeDescription
nameSTRINGServer name
instanceINTObject instance number

Returns: REAL — Current value.

(* Read command from BMS *)
valve_cmd := BACNET_SERVER_GET_AO('bms_server', 1);

BACNET_SERVER_GET_BO — Read Binary Output (Written by Remote)

ParamTypeDescription
nameSTRINGServer name
instanceINTObject instance number

Returns: BOOL — Current value.

(* Read fan command from BMS *)
fan_cmd_from_bms := BACNET_SERVER_GET_BO('bms_server', 1);

3.4 Example: Full Server Setup

PROGRAM POU_BACnetServer
VAR
state : INT := 0;
ok : BOOL;
zone_temp : REAL;
dat : REAL;
fan_running : BOOL;
pid_output : REAL;
remote_sp : REAL;
fan_cmd : BOOL;
END_VAR

CASE state OF
0: (* Create and start server *)
ok := BACNET_SERVER_CREATE('bms', 47808, 99001);
IF ok THEN
BACNET_SERVER_START('bms');
state := 10;
END_IF;

10: (* Running — update exposed points every scan *)
(* Push sensor data to BACnet objects *)
BACNET_SERVER_SET_AI('bms', 1, zone_temp); (* AI-1: Zone Temp *)
BACNET_SERVER_SET_AI('bms', 2, dat); (* AI-2: Discharge Air Temp *)
BACNET_SERVER_SET_BI('bms', 1, fan_running); (* BI-1: Fan Status *)
BACNET_SERVER_SET_AV('bms', 1, pid_output); (* AV-1: PID Output *)

(* Read commands written by BMS front-end *)
remote_sp := BACNET_SERVER_GET_AV('bms', 10); (* AV-10: Remote Setpoint *)
fan_cmd := BACNET_SERVER_GET_BO('bms', 1); (* BO-1: Fan Command *)
END_CASE;
END_PROGRAM

4. Application Examples

4.1 VAV Box Controller

A complete VAV box integration — reading sensors, commanding dampers, and monitoring alarms across multiple controllers.

PROGRAM POU_VAVControl
VAR
state : INT := 0;
ok : BOOL;

(* VAV box data *)
zone_temp : REAL;
zone_sp : REAL;
damper_pos : REAL;
airflow : REAL;
reheat_cmd : REAL;
occ_mode : BOOL;

(* Control *)
damper_cmd : REAL;
min_flow : REAL := 20.0; (* % minimum airflow *)
max_flow : REAL := 100.0; (* % maximum airflow *)
END_VAR

CASE state OF
0: (* Initialize *)
ok := BACNET_CLIENT_CREATE('vav_b1', '10.0.1.110', 2001);
IF ok THEN
ok := BACNET_CLIENT_CONNECT('vav_b1');
IF ok THEN state := 10; END_IF;
END_IF;

10: (* Read current status *)
zone_temp := BACNET_READ_AI('vav_b1', 1); (* Zone temp *)
zone_sp := BACNET_READ_AV('vav_b1', 1); (* Zone setpoint *)
damper_pos := BACNET_READ_AO('vav_b1', 1); (* Damper feedback *)
airflow := BACNET_READ_AI('vav_b1', 2); (* CFM *)
occ_mode := BACNET_READ_BV('vav_b1', 1); (* Occupied mode *)

(* Simple proportional damper control *)
IF occ_mode THEN
damper_cmd := (zone_temp - zone_sp) * 10.0; (* P-only *)
IF damper_cmd < min_flow THEN damper_cmd := min_flow; END_IF;
IF damper_cmd > max_flow THEN damper_cmd := max_flow; END_IF;
ELSE
damper_cmd := min_flow; (* Minimum flow when unoccupied *)
END_IF;

(* Write damper command at priority 8 *)
ok := BACNET_WRITE_PRIORITY('vav_b1',
BACNET_OBJECT_AO, 1,
damper_cmd, 8);

(* Reconnect if lost *)
IF NOT BACNET_CLIENT_IS_CONNECTED('vav_b1') THEN
state := 0;
END_IF;
END_CASE;
END_PROGRAM

4.2 Chiller Plant Staging with COV

Use COV subscriptions to react instantly to chiller status changes without continuous polling.

PROGRAM POU_ChillerPlant
VAR
state : INT := 0;
ok : BOOL;

(* Subscriptions *)
sub_ch1_status : INT;
sub_ch2_status : INT;
sub_load : INT;

(* Plant data *)
ch1_running : BOOL;
ch2_running : BOOL;
plant_load : REAL;
stage_up_sp : REAL := 85.0; (* % load to stage up *)
stage_down_sp : REAL := 30.0; (* % load to stage down *)
END_VAR

CASE state OF
0: (* Initialize connections *)
ok := BACNET_CLIENT_CREATE('ch1', '10.0.2.10', 5001);
BACNET_CLIENT_CONNECT('ch1');
ok := BACNET_CLIENT_CREATE('ch2', '10.0.2.11', 5002);
BACNET_CLIENT_CONNECT('ch2');
state := 1;

1: (* Subscribe to chiller status via COV *)
sub_ch1_status := BACNET_SUBSCRIBE_COV('ch1',
BACNET_OBJECT_BI, 1, 0);
sub_ch2_status := BACNET_SUBSCRIBE_COV('ch2',
BACNET_OBJECT_BI, 1, 0);
sub_load := BACNET_SUBSCRIBE_COV('ch1',
BACNET_OBJECT_AI, 10, 0);
state := 10;

10: (* Staging logic — COV keeps values current *)
ch1_running := BACNET_READ_BI('ch1', 1);
ch2_running := BACNET_READ_BI('ch2', 1);
plant_load := BACNET_READ_AI('ch1', 10);

(* Stage up: start chiller 2 when load exceeds threshold *)
IF plant_load > stage_up_sp AND NOT ch2_running THEN
BACNET_WRITE_PRIORITY('ch2',
BACNET_OBJECT_BO, 1,
TRUE, 8);
END_IF;

(* Stage down: stop chiller 2 when load drops *)
IF plant_load < stage_down_sp AND ch2_running AND ch1_running THEN
BACNET_WRITE_PRIORITY('ch2',
BACNET_OBJECT_BO, 1,
FALSE, 8);
END_IF;

(* Fault handling *)
IF NOT BACNET_CLIENT_IS_CONNECTED('ch1') THEN
state := 0;
END_IF;
END_CASE;
END_PROGRAM

4.3 BACnet Gateway — Modbus to BACnet

ControlForge as a protocol translator: read Modbus devices and expose their data as BACnet objects for the BMS.

PROGRAM POU_ModbusToBACnet
VAR
state : INT := 0;
ok : BOOL;

(* Modbus power meter data *)
voltage : REAL;
current : REAL;
power_kw : REAL;
energy_kwh : REAL;

(* Modbus registers — Shark 200 power meter *)
mb_regs : ARRAY[0..7] OF INT;
END_VAR

CASE state OF
0: (* Initialize both protocols *)
ok := MB_CLIENT_CREATE('meter1', '10.0.0.80', 502);
MB_CLIENT_CONNECT('meter1');
ok := BACNET_SERVER_CREATE('gateway', 47808, 99100);
BACNET_SERVER_START('gateway');
state := 10;

10: (* Read Modbus, expose as BACnet *)
(* Read power meter via Modbus *)
mb_regs := MB_READ_HOLDING('meter1', 0, 8);
voltage := INT_TO_REAL(mb_regs[0]) / 10.0;
current := INT_TO_REAL(mb_regs[2]) / 100.0;
power_kw := INT_TO_REAL(mb_regs[4]) / 10.0;

(* Expose as BACnet AI objects *)
BACNET_SERVER_SET_AI('gateway', 1, voltage); (* AI-1: Voltage *)
BACNET_SERVER_SET_AI('gateway', 2, current); (* AI-2: Current *)
BACNET_SERVER_SET_AI('gateway', 3, power_kw); (* AI-3: Power kW *)
END_CASE;
END_PROGRAM

4.4 Multi-AHU Monitoring Dashboard

Poll multiple AHUs and aggregate data for a building-level view.

PROGRAM POU_BuildingMonitor
VAR
state : INT := 0;
ok : BOOL;
i : INT;

(* AHU data — 4 units *)
ahu_names : ARRAY[0..3] OF STRING := ['ahu_1', 'ahu_2', 'ahu_3', 'ahu_4'];
ahu_ips : ARRAY[0..3] OF STRING := ['10.0.1.100', '10.0.1.101', '10.0.1.102', '10.0.1.103'];
ahu_ids : ARRAY[0..3] OF INT := [1001, 1002, 1003, 1004];

sat : ARRAY[0..3] OF REAL; (* Supply air temps *)
rat : ARRAY[0..3] OF REAL; (* Return air temps *)
fan_sts : ARRAY[0..3] OF BOOL; (* Fan status *)
alarms : ARRAY[0..3] OF BOOL; (* Alarm active *)

building_avg_temp : REAL;
fans_running : INT := 0;
END_VAR

CASE state OF
0: (* Create all connections *)
FOR i := 0 TO 3 DO
ok := BACNET_CLIENT_CREATE(ahu_names[i], ahu_ips[i], ahu_ids[i]);
BACNET_CLIENT_CONNECT(ahu_names[i]);
END_FOR;
state := 10;

10: (* Poll all AHUs *)
building_avg_temp := 0.0;
fans_running := 0;

FOR i := 0 TO 3 DO
IF BACNET_CLIENT_IS_CONNECTED(ahu_names[i]) THEN
sat[i] := BACNET_READ_AI(ahu_names[i], 1);
rat[i] := BACNET_READ_AI(ahu_names[i], 2);
fan_sts[i] := BACNET_READ_BI(ahu_names[i], 1);

building_avg_temp := building_avg_temp + rat[i];
IF fan_sts[i] THEN
fans_running := fans_running + 1;
END_IF;
ELSE
alarms[i] := TRUE;
BACNET_CLIENT_CONNECT(ahu_names[i]); (* Attempt reconnect *)
END_IF;
END_FOR;

building_avg_temp := building_avg_temp / 4.0;
END_CASE;
END_PROGRAM

5. BACnet Protocol Notes

Port and Network Configuration

  • Standard BACnet/IP port: 47808 (0xBAC0). Most devices use this. Non-standard ports are supported by specifying them in BACNET_CLIENT_CREATE.
  • UDP protocol: BACnet/IP uses UDP, not TCP. ControlForge manages socket creation and reuse internally.
  • Broadcast address: WhoIs uses UDP broadcast on the BACnet/IP port. Ensure your network allows UDP broadcast on port 47808.
  • Firewall rules: Allow UDP 47808 bidirectionally for both client and server operation.

BACnet/IP vs. MS/TP

ControlForge speaks BACnet/IP natively. For devices on BACnet MS/TP (RS-485) trunks, you need a BACnet router between the IP network and the MS/TP trunk. Common BACnet routers: Tridium JACE, Contemporary Controls BASrouter, Loytec L-IP. The router handles protocol translation transparently — ControlForge sees MS/TP devices as normal BACnet/IP devices.

Timeout and Retry Behavior

  • Default timeout: 3 seconds per request. BACnet devices behind MS/TP segments may need longer due to token rotation delays.
  • Automatic retry: Failed reads return the last known value. Check connection state with BACNET_CLIENT_IS_CONNECTED to detect prolonged failures.
  • COV resubscription: If a device reboots, active COV subscriptions are lost. Monitor subscription health and re-subscribe as needed.

Common BACnet Device IDs by Vendor

These are conventions, not standards — always verify with the integrator:

VendorTypical Device ID Range
Trane Tracer1000-9999
Johnson Controls (Metasys)10000-99999
Distech Controls100-999
Honeywell Spyder/WEB1-999
Reliable Controls1000-65535
Siemens DXR1-9999

Priority Array Best Practices

  1. Always relinquish when done. A stuck priority 8 override will fight your scheduling forever.
  2. Use consistent priorities across the project. Document which priority each application uses.
  3. Priority 8 for operator overrides, 16 for scheduling is the most common pattern.
  4. Never write to priority 1 or 2 unless you are implementing actual life safety logic. BMS integrators will flag this during commissioning.
  5. Read the priority array before writing to understand what else is commanding the point.

Appendix A: Function Quick Reference

Client Functions

FunctionParametersReturnsDescription
BACNET_CLIENT_CREATEname, targetIP, deviceID [, localPort] [, targetPort]BOOLCreate named connection
BACNET_CLIENT_CONNECTnameBOOLEstablish connection
BACNET_CLIENT_DISCONNECTnameBOOLClose connection
BACNET_CLIENT_IS_CONNECTEDnameBOOLCheck connection state
BACNET_CLIENT_DELETEnameBOOLRemove connection
BACNET_CLIENT_LIST[]STRINGList all connections
BACNET_READ_PROPERTYname, objectType, objectInstance, propertyANYRead any property
BACNET_WRITE_PROPERTYname, objectType, objectInstance, property, valueBOOLWrite any property
BACNET_READ_PRESENT_VALUEname, objectType, objectInstanceANYRead present value
BACNET_WRITE_PRESENT_VALUEname, objectType, objectInstance, valueBOOLWrite present value
BACNET_WRITE_PRIORITYname, objectType, objectInstance, value, priorityBOOLWrite at specific priority
BACNET_RELINQUISHname, objectType, objectInstance, priorityBOOLRelease priority slot
BACNET_WHO_ISname [, lowLimit] [, highLimit][]MAPDiscover devices
BACNET_SUBSCRIBE_COVname, objectType, objectInstance, lifetimeINTSubscribe to value changes
BACNET_UNSUBSCRIBE_COVname, subscriptionIDBOOLCancel subscription
BACNET_GET_ALARMSname[]MAPRead active alarms
BACNET_GET_STATSMAPStack statistics
BACNET_READ_AIname, instanceREALRead Analog Input
BACNET_READ_AOname, instanceREALRead Analog Output
BACNET_READ_AVname, instanceREALRead Analog Value
BACNET_READ_BIname, instanceBOOLRead Binary Input
BACNET_READ_BOname, instanceBOOLRead Binary Output
BACNET_READ_BVname, instanceBOOLRead Binary Value
BACNET_WRITE_AOname, instance, valueBOOLWrite Analog Output
BACNET_WRITE_AVname, instance, valueBOOLWrite Analog Value
BACNET_WRITE_BOname, instance, valueBOOLWrite Binary Output
BACNET_WRITE_BVname, instance, valueBOOLWrite Binary Value

Server Functions

FunctionParametersReturnsDescription
BACNET_SERVER_CREATEname, port, device_idBOOLCreate server instance
BACNET_SERVER_STARTnameBOOLBegin listening
BACNET_SERVER_STOPnameBOOLStop listening
BACNET_SERVER_IS_RUNNINGnameBOOLCheck server state
BACNET_SERVER_SET_AIname, instance, valueBOOLSet Analog Input value
BACNET_SERVER_SET_BIname, instance, valueBOOLSet Binary Input value
BACNET_SERVER_SET_AVname, instance, valueBOOLSet Analog Value
BACNET_SERVER_GET_AVname, instanceREALRead Analog Value
BACNET_SERVER_GET_AOname, instanceREALRead Analog Output
BACNET_SERVER_GET_BOname, instanceBOOLRead Binary Output
BACNET_SERVER_DELETEnameBOOLRemove server
BACNET_SERVER_LIST[]STRINGList all servers

Object Type Constants

ConstantDescription
BACNET_OBJECT_AISensor readings (read-only)
BACNET_OBJECT_AOAnalog control outputs (commandable)
BACNET_OBJECT_AVSetpoints and calculated values
BACNET_OBJECT_BIStatus signals (read-only)
BACNET_OBJECT_BOOn/off commands (commandable)
BACNET_OBJECT_BVMode flags and enables
BACNET_OBJECT_MSIEnumerated status
BACNET_OBJECT_MSOEnumerated commands
BACNET_OBJECT_MSVEnumerated setpoints

Property Constants

ConstantDescription
BACNET_PROP_PRESENT_VALUECurrent value of the object
BACNET_PROP_OBJECT_NAMEHuman-readable name
BACNET_PROP_DESCRIPTIONFree-text description
BACNET_PROP_UNITSEngineering units
BACNET_PROP_PRIORITY_ARRAY16-level command priority array
BACNET_PROP_RELINQUISH_DEFAULTDefault value when all priorities are NULL

ControlForge v1.0.533 | BACnet/IP (ASHRAE 135-2020) | UDP Port 47808 Client: ~27 functions | Server: ~12 functions

© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides