Skip to content

Note

What a Niagara station's oBIX server actually exposes

oBIX is how an outside program reads a Niagara station without a vendor library and without a driver on either side. What the station actually answers is not quite what the specification describes, and the differences are the ones that cost a morning.

  • Integration
  • Agents
  • Station security

Written

Why read the jars and not the specification

oBIX is an OASIS specification, and a Niagara station ships a server for it. Those are two different documents. The specification says what an oBIX server may look like; the shipped code says what this one does. Where they disagree, the code wins, and a client written from the specification alone fails in ways that look like a network problem.

Everything below was read out of one stock installation — the module jars and the documentation jars, with javap — rather than out of the standard or out of recollection. This note names a class or method where that is what makes a claim checkable; the full list, one citation per decision with the jar hashes to go with it, is EVIDENCE.md in the bridge this came out of. Where the installation does not answer a question, the gap is said to be a gap.

The mount point is a literal

The server component carries a servletName property whose default is obix, which reads like a setting. None of the five methods that return a path reads it: each returns a fixed string.

MethodWhat it returns
getServletPath()/obix
getSoapPath()/obix/soap
getWsdlPath()/obix/wsdl
getXsdPath()/obix/xsd
getXslPath()/obix/xsl

So the lobby is at http://host/obix, and the guide gives the name as read-only. The property is not inert, though, and it took reading a second jar to see why: the servlet is registered with the web server under its name, and the encoder that answers every request is built with a lobby path of / plus that same property — which is the prefix a URI inside a request body has to carry. Two live code paths read the property; five hard-coded getters ignore it. On a stock station they all agree on /obix, so a client that offers a configurable path is offering a setting the station honours in some places and not in others.

Three verbs and one servlet

The servlet's service method branches on the HTTP method and nothing else. GET encodes whatever the URI resolves to. PUT writes. POST invokes an operation. The station's own WSDL declares exactly the same three — a read, a write and an invoke, each returning an oBIX object — so this is not an accident of the HTTP binding.

Responses come back as text/xml. There is no JSON representation to ask for, and no content negotiation to get one.

The lobby is the discoverable surface

A GET of the lobby returns a list of branches, each contributed by a lobby agent component. Twelve agents are registered in this installation and only seven of them write an element into that list: an about object, an alarms branch, a batch operation, the station itself, histories, a watch service, and the export branch. The other five — a singular alarm branch, a BQL query branch, a contract branch, an ORD branch, and a units branch — have an encodeLobbyChild that is a single return, so each resolves but appears in no listing (below).

Eight of the twelve give their name as a literal from getLobbyName(). Four return a static field set in the class initialiser, which is why a grep for the name in the source of the class finds nothing and the name has to be read out of <clinit>.

The one that wastes an afternoon. The documentation calls the exported-points branch exports. The export lobby agent mounts it at continuousControl. A client that walks to /obix/exports because the guide said so gets nothing, and nothing in the reply explains why.

Five branches are in no listing. The alarm, bql, def, ord and units lobby agents each have an encodeLobbyChild that is a single return: they write no element. Each resolves anyway, and the singular alarm branch is the one worth reaching — /obix/alarm/<uuid> looks a Niagara UUID up in the alarm database and answers with that one record — so a client that walks what it is shown cannot discover an address that works. When the lookup fails, the whole method body sits inside one catch that throws a no-argument BadUriErr, discarding even the UUID not found text the same method had just built. An unparseable UUID, a UUID that is simply absent and a database error all arrive as one empty fault, whose display is a Java class name standing where its message should be.

A relative href is relative to the document it came in

Every href a client follows after the lobby is written by one method, getChildHref(parentHref, name), reached through the configChild path that every component child goes through. It has three cases. When the parent element is the document element and its href ends in a slash, the child gets a bare name + "/". When the parent carries no href at all, the same. Otherwise the child's href is the parent's with the name concatenated onto it.

The lobby is the first case, so a lobby child's bare histories/ means /obix/histories/, and a client that joins bare hrefs onto the servlet root is right. One document deeper it is wrong. A GET of the alarm service — at /obix/config/Services/AlarmService/, the href the alarms ref advertises — answers with its query operation at ~alarmQuery/, which means /obix/config/Services/AlarmService/~alarmQuery/; joined onto the servlet root instead it asks for /obix/~alarmQuery/, and the station answers 404. The lobby is the one document where the two readings agree, which is why this survives testing: a client can walk the whole lobby, resolve every branch correctly, and still fail on the first operation it tries to invoke.

So resolve each href against the href of the document it arrived in, the way a browser resolves a link, rather than against the root. One form is exempt: an href beginning | is an ORD the encoder fell back to for a child it could not name, and it is not a path to join onto anything.

A value's type is its element name

The station's encoder writes a small, fixed vocabulary of element names — obj, list, op, ref, err, feed, bool, int, real, str, enum, abstime, reltime, uri — and types a value by that element name alone. The value itself lives in a val attribute, and the attribute does not say what it is.

The practical consequence is on the way in rather than the way out: a numeric set point sent as <str val="21.5"/> is a string write, not a number write, and the station is entitled to take it or refuse it on that basis. A client that infers the element from the look of the value will be right until the first time it is not.

A watch is a lease, and it belongs to a user

Subscribing is not a socket. You ask the watch service to make a watch, then poll it, and the watch expires unless you keep renewing the lease. The operations are declared as in/out contract pairs in each class's static initialiser.

OperationWhat it does
make On the watch service. Returns a new watch object, with its own hrefs for everything below.
add Adds hrefs and returns their current readings, so the first poll is not a flood.
pollChanges What has changed since the last poll.
pollRefresh Everything in the watch, whether it changed or not.
remove / delete Drop hrefs, or drop the watch. Leaving a watch to expire works and costs the station a lease's worth of bookkeeping.

Two defaults in this installation disagree: the watch service's default lease is thirty seconds, and the watch component's own lease property defaults to fifteen. A client that assumes either number will eventually renew too late on a station configured the other way. Read the lease out of the watch the station actually returned, and take each operation's href from that reply rather than assembling it from a template.

Two more properties of the implementation are worth knowing before designing around it. A failure to resolve one href in an add is caught per item, so an unknown point comes back as a fault row among the good readings rather than failing the whole call. And a watch belongs to the user who made it: another user touching it gets a permission fault, by an explicit check, not by accident.

The batch op reads many points in one request

One of those branches is an operation rather than a folder. POST a list of URIs to it and the station answers with one object per URI, which on a controller is the difference between one request per point and one request. It is the cheapest improvement available to anything walking a station, and the details that decide whether a client gets it working today or tomorrow are all in the request.

The request's document element has to be a list. A watch's input wraps its list inside an obj; a batch's must not, and the station rejects the whole document rather than the one item. Each item has to be named uri. And each item's val has to be a full path including the lobby path, because the resolver looks for that prefix inside the value and refuses anything without it — so the relative hrefs that work in a watch are rejected here, which is the difference that costs the afternoon. The verb is an attribute: is="obix:Read", matched by substring, defaulting to empty, so an item that omits it is answered with an unknown-contract fault rather than with a reading.

The good part is the failure mode. Each item is attempted inside its own catch, so a bad URI becomes a fault row in place while every other reading comes back normally — the same per-item forgiveness a watch's add has. The reply is a list marked of="obix:BatchOut" and carrying no is attribute at all, so a client that matches on the contract it was promised finds nothing.

It is also the one operation that can write. The same list may carry write and invoke items, and they travel as a POST to the batch operation rather than as a PUT to a point — so any permission model a client enforces at its own edge has to cover this path as well, or it covers nothing. The bridge this was read out of answers that by having no way to express a write item: the request builder takes no verb, and a test asserts that no code in the module names another contract. The bytecode citations are in EVIDENCE.md under section M.

Where the open alarms are

The alarms lobby branch is a ref, not a folder you reach by that name. Its agent is a shortcut whose advertised href is /obix/config/Services/AlarmService/ — the alarm service component, reached under the station's config branch. A bare GET of /obix/alarms/ by name does not go there: the shortcut resolves an empty remainder to the station root. Follow the advertised href instead. The service's own children are the station's alarm classes — configuration, not records. What the encoder adds beside them is where the records are: an <int name="count"> of the open alarms, a feed, and an operation named query that takes an obix:AlarmFilter and returns an obix:AlarmQueryOut. Each alarm class carries the same three, so one class can be asked instead of the whole service.

The filter's children are read by name — limit, start, end — and a missing one is tolerated, so an empty filter means every open alarm. They are turned into a BQL query over the alarm database, which is worth knowing because it settles what the bounds mean: they constrain each alarm's last update, not when it started.

Two details of the reply decide whether a client reads it correctly. The records arrive first, as <list name="data" of="obix:Alarm">, and the count, start and end are written after that list, because the station streams a cursor and only then knows them — so nothing may depend on child order. And each record names several contracts at once, obix:Alarm obix:AckAlarm plus a point or stateful flavour, alongside a niagara-uuid that is the only handle by which that alarm can be named again.

The second of those contracts is the one to notice before building on this. An acknowledgement is an operation on the record, so it travels as a POST to the alarm rather than as a PUT to a point — and with a forceCleared child it also drives the source state to normal and fires the alarm service's audit action. It is the same shape of hole as the batch op, and it wants the same answer: a read-only client has to be unable to express it, not merely uninclined to.

The canned history queries are refs, and one neighbour writes

Tridium documents that a history is exposed through a predefined set of query options, available as lobby URIs with a GET and no operation, and names three of them. The encoder writes eleven: today, last24Hours, yesterday, weekToDate, lastWeek, last7Days, monthToDate, lastMonth, unboundedQuery, and two whose name carries a bracket, yearToDate (limit=1000) and lastYear (limit=1000). Each is a ref whose href already carries its bounds as query parameters, so a GET is the whole mechanism and there is no URL to assemble.

In the same document, beside them, are four operations, which a GET cannot use at all: query, rollup, feed and append. The last of those writes records into the history. Anything that reaches for a history query by name, out of a listing that mixes refs and operations together, is one spelling away from appending to a trend log — so read the two apart and refuse the operations by construction rather than by intent.

403 means two different things

Before anything else, the servlet checks the licence, and an unlicensed server sends 403 with the string Unlicensed oBIX Server. A station whose oBIX server is disabled — either the component is off or the network's status says disabled — sends 410 with oBIX Server Disabled. The driver documentation says the same about the second.

But 403 is also what a permission refusal looks like, and that one arrives with an oBIX fault in the body naming the object. So the status code alone cannot tell "this station is not licensed for oBIX" from "this user may not read that point", and a client that guesses will send somebody to check a licence file when the real answer is a role.

Read the body. If it decodes as a fault, the fault is the answer and any generic advice about the status code is noise. Which permissions apply is a station question rather than an oBIX one — the same roles and permissions that govern any other user on the station govern this one.

What the installation does not answer

Four things worth stating as unknowns rather than filling in from the standard.

There is no literal oBIX request or response document anywhere in the shipped documentation — checked by exhaustive search across the extracted HTML, not assumed. The HTTP challenge is not in either oBIX jar: no WWW-Authenticate, no Base64 decode, no credential comparison. The servlet reads the Authorization header only to decide whether to reuse a session, so whether a given station demands one scheme or another is answered by the web service, not here. The specification's watch sign-up operation appears nowhere in either jar. And there is no compiled-in ceiling on points, devices or watches — the numeric limits come from the licence file.

None of which is a reason not to use oBIX. It is a reason to write the client against the station rather than the standard, and to know which of the two a given line of code is trusting.

Why this matters more now than it did

Until recently the audience for this was a person writing an integration once. Increasingly it is a program deciding what to call at run time, which changes the stakes: a human who walks to /obix/exports and finds nothing asks somebody, and a program writes a plausible-looking failure into a log. That is a separate design problem, and it is the subject of letting an agent write to a control system.

Sources

Where this is written down

Related

Where this comes up in the work

More notes

Other things worth writing down

Next step

Tell us the version, the hardware, and what it has to do.

You will get a written scope and a fixed price against it. If the honest answer is that you do not need us, you will get that instead.