← Articles
Networking/8 min read/ views

Reading BACnet/IP Into SCADA Without Getting Stuck in the Priority Array

How a driver discovers BACnet devices, reads present-value and writes commandable objects: object identifiers, the 16-slot priority array, COV and BBMD.

NetworkingSCADATroubleshootingCommissioningTags

The call that got me into this: a plant SCADA writes a discharge-air setpoint down to an air handler, the operator sees it take, everyone goes home. Two days later the local BAS schedule is supposed to raise the setpoint for morning warm-up and nothing moves. The AHU is holding the number SCADA sent, and no amount of writing from the local controller can shift it. Nobody touched a wire. What happened is that SCADA commanded an analog-output at a priority nobody else could beat, and never let go.

That is the thing about BACnet that surprises people coming from Modbus or OPC UA: writing a value isn't writing a register. For commandable objects you're pushing a value into one slot of a 16-slot arbitration array, and if you don't understand the array, you will either fight the local controls or lose to them, seemingly at random.

First you have to find the devices

BACnet (ASHRAE 135, also ISO 16484-5) doesn't hand you an address you can type in. A SCADA driver discovers devices by broadcasting a Who-Is and collecting the I-Am replies. Each device answers with its Device object instance number — a value from 0 to 4194303 that must be unique across the whole internetwork. That device instance, not the IP address, is the identity you bind to. IPs change; a well-run BACnet site treats the device instance as the permanent handle.

BACnet/IP rides UDP on port 47808 (0xBAC0 — someone had fun with that). The catch is that Who-Is is a broadcast, and broadcasts don't cross IP subnets. If your SCADA server sits on the business/DMZ side and the controllers are on a plant VLAN, the Who-Is dies at the router and you discover nothing. Two ways out:

  • BBMD (BACnet Broadcast Management Device) — one device per subnet holds a Broadcast Distribution Table and forwards broadcasts to its peers. If BBMDs are already configured for the local BAS, you often just need your entry added.
  • Foreign Device Registration — your SCADA driver registers itself with an existing BBMD as a foreign device, with a time-to-live it has to renew. This is the usual move for a SCADA box that lives outside the controller subnet. Set the TTL sanely (a few hundred seconds) and confirm the driver re-registers before it expires, or discovery quietly goes dark after the first lapse.

Field controllers on RS-485 (MS/TP) don't speak IP at all — they sit behind a BACnet router that carries a network number. You'll read them by device instance the same way; the routing is the router's problem, but when a whole MS/TP trunk goes missing at once, suspect the router or a network-number collision before you suspect the controllers.

Objects and the properties you actually read

Everything in a device is an object, and every object is identified by a pair: (object-type, instance). The types you'll touch constantly:

  • analog-input (AI) — a sensor: space temp, static pressure, flow.
  • analog-output (AO) — a commandable analog: valve position, damper, setpoint to a loop.
  • analog-value (AV) — a setpoint or calculated value living in the controller.
  • binary-input / binary-output / binary-value (BI/BO/BV) — status and commands.
  • multi-state-value (MSV) and friends — enumerated states like Off/Low/High or occupancy modes.
  • device — object type 8, one per controller, holding the instance number, vendor ID, and the object list.

The value you want is almost always the Present_Value property (property identifier 85). But don't read Present_Value alone and call the point good. Read Status_Flags (111) with it — four bits: in-alarm, fault, overridden, out-of-service. That overridden bit is your early warning that someone (or something) has commanded the object locally; out-of-service means the value is decoupled from the physical point entirely and is whatever a technician typed. A temperature of 72.0 with out-of-service set is not a measurement, it's a leftover. Map Status_Flags to your SCADA tag quality so a forced point doesn't read as healthy.

Read the Units property (117) once at commissioning and cache it — it's an enumeration (degrees-Celsius is 62, degrees-Fahrenheit 64, and so on), and you want your scaling and labels to come from the device, not from a spreadsheet that drifts.

Don't poll these one property at a time. ReadPropertyMultiple batches many object/property pairs into one request, and it's the difference between a scan that keeps up and one that falls behind on a busy MS/TP trunk. Large object lists may need segmentation; if a device advertises no segmentation support, a big ReadPropertyMultiple will fail with an abort, and you'll have to chunk the requests yourself.

The priority array is the whole game for writes

Here's the part that bit the AHU. Commandable objects — AO, BO, and commandable AV/BV — don't have a single writable value. They have a Priority_Array (property 87): 16 slots, priority 1 highest, 16 lowest. When you do a WriteProperty to Present_Value, you're really writing into one slot, chosen by the priority field in the request. The object's actual Present_Value is whatever sits in the highest-priority non-NULL slot. If every slot is NULL, it falls through to Relinquish_Default (property 104).

The convention (informative in the standard, but widely followed) reserves the top slots for safety and manual override:

  • 1 — manual-life-safety
  • 2 — automatic-life-safety
  • 8 — manual operator
  • 16 — lowest, often the local program's default action

A SCADA writing an operator setpoint typically belongs at priority 8. If you write at priority 1 or 2 "to make sure it takes," congratulations — you've now outranked the local safety and program logic, and nothing short of another priority-1 write will move that point. That's exactly how the AHU got stuck.

And this is the rule people forget: to release a command, you write NULL to your slot. Not zero. NULL. Writing 0.0 leaves your slot occupied with the value zero — still the winner if it's the highest active priority. Only writing NULL relinquishes control and lets the next-lower priority (or Relinquish_Default) take over. A SCADA that commands a point and never relinquishes will hold that object hostage until someone manually clears the array. Build the release into your command logic: if the operator command is "return to auto," that must issue a WriteProperty of NULL at your priority, not a write of some neutral number.

When you're diagnosing a stuck point, read the whole Priority_Array. It tells you, slot by slot, who is commanding and at what level. Nine times out of ten the culprit is a value someone left in slot 8 months ago from a commissioning test that was never relinquished.

COV beats polling, when the device supports it

Polling every point every few seconds works and is easy to reason about, but on MS/TP it saturates fast. Change-of-value subscriptions flip it around: you send SubscribeCOV and the device notifies you when Present_Value moves past its COV_Increment, or when a binary changes state. Set the increment to something operationally meaningful — 0.5 °C on a space temp, not 0.01, or you've reinvented polling with extra steps.

Two things to get right. First, COV subscriptions carry a lifetime; the device drops the subscription when it expires, so your driver has to resubscribe on a timer well inside that window. If you set a lifetime and don't renew, updates stop silently — the point just stops moving on the screen while the device is perfectly healthy. Second, prefer confirmed COV where reliability matters so a dropped notification gets retried; unconfirmed is lighter but you'll miss transitions on a lossy link and never know. A pragmatic pattern is COV for the many slow analog and status points, plus a slow background poll (say, every few minutes) as a backstop to catch anything a missed notification or a silently-expired subscription left stale.

What to nail down at commissioning

  • Bind tags to device instance + object identifier, never to IP address.
  • Decide and document your write priority (usually 8) and make "return to auto" a NULL write to that slot.
  • Confirm BBMD or foreign-device registration works from where your SCADA actually lives, and that the TTL renews.
  • Fold Status_Flags (overridden, out-of-service, fault) into tag quality so forced points don't read as trustworthy.
  • Read Units from the device, not from a design document.
  • Prefer ReadPropertyMultiple and COV; keep a slow poll as a safety net.

The first point someone leaves commanded in the priority array is the one that teaches everyone on site how BACnet writes really work. Better to learn it from this page than from a warm building on a Monday.