Free tool
tuning-stale-scan — what notices a device that stopped talking
On a default Niagara station, nothing does. staleTime ships at zero, and the single branch in the framework that would mark a point stale returns immediately when it is zero. tuning-stale-scan reads that out of the compiled classes with javap and prints the policy defaults, the branch, the clock it compares against, and the four places the machinery throws its errors away. One Python file, read-only, and no station is contacted.
- Niagara 4
- 2 jars
- javap
- staleTime
- Read-only
- Python 3
Install and run
There is no install. One file, standard library only, and the only thing it needs
besides Python is a javap — which it looks for in
$JAVAP, on PATH, under $JAVA_HOME, in the JDK
Niagara ships, and only then at a Debian path. It reads
four compiled classes out of two shipped
jars and contacts nothing.
# download it
curl -O https://plantroomlabs.com/tools/tuning-stale-scan.py
# read the tuning machinery in the default install
python3 tuning-stale-scan.py
# or name the installation to read
python3 tuning-stale-scan.py /opt/Niagara/Niagara-4.15.5.22
The default policy, and the branch that reads it
The default tuning policy, read out of the static initialiser:
minWriteTime 0 ms (min facet 0 ms)
maxWriteTime 0 ms (min facet 0 ms)
staleTime 0 ms (min facet 0 ms)
writeOnStart true
writeOnUp true
writeOnEnabled true
Tuning.process(), the only code in the framework that notices:
at offset 160 it loads staleTime, compares it with zero, and on
less-or-equal jumps to 212 - which is the method's own return.
The default is 0, so by default this branch is never taken and no
point in any driver on the station ever goes stale.
With staleTime set, the rest reads:
elapsed = now - (mode.isWriteonly() ? writeTicks : readTicks)
if elapsed > staleTime, call stale() (the branch is ifle)
so the threshold is strictly greater than staleTime, and the clock is
readTicks - which only readOk() ever sets.
stale() is three instructions: get the tunable, call setStale(true, null).
It is the driver's own BITunable implementation that decides what
that means for the point's status and value. The framework sets a
flag and stops.
Zero means the feature is off, not instant
staleTime defaults to 0 ms. The method that would mark a point stale
loads it, compares it with zero, and on less-or-equal jumps straight to its own
return. So on a station nobody has configured, no point in any driver ever goes
stale.
The clock is readTicks, and only one call sets it
The elapsed time is measured against readTicks, which only
readOk() ever sets. If a driver calls readOk() because a
subscription or a link is healthy rather than because a value arrived, a configured
staleTime still never fires. That is one line in a proxy extension and
it decides whether the feature works at all.
The framework sets a flag and stops
stale() is three instructions: get the tunable, call
setStale(true, null). What that means for the point's status and value
is the driver's own BITunable implementation. Nothing else in the
framework decides it.
Where the errors go
Where the errors go. Tuning holds a logger named 'driver.tuning' with
1 warning call, and 4 printStackTrace() sites.
The warning says: TuningPolicy not found:
The stack traces are in:
void write(javax.baja.sys.Context)
void readSubscribed()
void readUnsubscribed()
void stale()
Those are the four things the tuning machinery does. Every one of
them swallows its exception to stderr, where a station's log export
will not have it.
The background thread does the same with anything process() throws,
and nulls a tuning out of its array when process() returns false.
Each of those sites prints to stderr, where a station's log export will not have it. That is worth knowing before spending an afternoon on why a point never went stale: the reason may have been printed somewhere nobody collects.
What follows from it
Two consequences worth designing for
----------------------------------------------------------------------
1. If your device reports unsolicited rather than being polled, a
dead device is invisible by default. No poll fails, so nothing
goes fault; staleTime is 0, so nothing goes stale; the last
value sits there with an ok status for as long as the station runs.
2. staleTime measures readTicks, and readTicks is set by readOk().
If a driver calls readOk() when a link or a subscription is
healthy rather than when a value actually arrives, then even a
configured staleTime never fires. That is one line in a proxy
extension and it decides whether the feature works at all.
Three checks one station settles in an afternoon
----------------------------------------------------------------------
1. Read the default tuning policy on a live network and confirm
staleTime is 0. If it is, no point under it can go stale.
2. Set staleTime to a minute on one point, pull the device, and watch
whether the point goes stale within 20000 ms of the minute.
3. Set staleTime, leave the device alive but stop its reporting at
the source, and see whether the point still goes stale. If it does
not, readOk() is being called for the wrong event.
What it reads, and the limit of what the output is worth
It reads the shipped jars with javap and nothing else. No station is
contacted, no point is read and nothing is written — which is also the limit of
what the output is worth. It is a reading of compiled code, so it tells you what the
tuning machinery is built to do rather than what a particular driver did yesterday.
The numbers above came from one installation, 4.15.5.22, and the output
names it. The controller this work usually targets runs
4.14.0.162. A different Niagara version is a different answer, so run
it against yours rather than trusting this page's figures.
The engineering reasoning this belongs beside is why a station polls too slowly: what to measure on a live station before changing a rate, and the order to change things in.
The file you are downloading
Published here so the download is checkable rather than trusted. Both figures
are read off the file served at
/tools/tuning-stale-scan.py when this page is built, so they cannot
disagree with it.
| Property | Value |
|---|---|
| File | tuning-stale-scan.py |
| Size | 12,917 bytes |
| SHA-256 | 96ab5717a01582e5455a557115d8089343ac155cb176214d8ee3e08875c7d1c4 |
| Licence | MIT — LICENSE.txt |
| Source | github.com/UsamaIqbal0304/tuning-stale-scan |
To check it, on Linux sha256sum tuning-stale-scan.py, on macOS
shasum -a 256 tuning-stale-scan.py, on Windows
certutil -hashfile tuning-stale-scan.py SHA256. A different digest means a
different file — not necessarily a hostile one, but not this one.
The repository holds the same file, byte for byte, together with everything needed to re-run the checks this page's claims rest on — so they can be run rather than read about. Issues and pull requests there are read.
bacnet-sweep
Broadcasts a BACnet/IP Who-Is, tables the devices that answer, and dumps a named device's object list — object name, present value and units — to a table or to CSV. It can encode two BACnet services and no others: Who-Is and ReadProperty.
- BACnet/IP
- Who-Is
- CSV out
mqtt-tap
Subscribes to a broker and prints what is actually on it: the topic tree with a count, a rate, a payload-type guess and the last value per topic, plus the retained topics that stopped updating. It sends five packet types and none of them is PUBLISH.
- MQTT
- Topic tree
- Retained
decoder-check
Runs a LoRaWAN device vendor's payload decoder against your frames in a sealed vm context and reports what a station would actually get back: crashes on a short frame, types that change between uplinks, units glued into values, keys a station has to escape. It reads frames and nothing else - no network, no broker, no network server.
- LoRaWAN
- Decoder
- Sandboxed
modbus-address-scan
Reads modbusCore-rt.jar out of a Niagara installation with javap and prints the register a point of each address format actually asks for — including the four band boundaries that all resolve to the same one.
- Modbus
- Shipped jars
- No station
ede-check
Reads the EDE import rules out of bacnetEDE-wb.jar with javap, then reports line by line what the shipped parser would reject in your point file and what it would silently default.
- EDE
- Point lists
- Line by line
module-sign-scan
Reads the verification code out of a Niagara installation and prints the four modes, the signature state each one accepts or refuses, and the exact log line a station writes — including the warning that only becomes a refusal when a certificate expires.
- Module signing
- Shipped jars
- No station
bacnet-priority-scan
Reads bacnet-rt.jar with javap and prints the object types Niagara writes through the priority array without asking, the ones it probes with a single ReadProperty, and what a failed probe does to the point for the life of that configuration.
- BACnet
- Shipped jars
- No station
alarm-route-scan
Reads alarm-rt.jar and baja.jar with javap and prints what happens to an alarm between the source and the recipient: one queue, one worker thread, the coalesce key that decides which duplicate is dropped, and why the invocation that lost that collision still reports success.
- Alarms
- Shipped jars
- No station
alarm-recipient-scan
Reads the recipient side of alarm-rt.jar with javap and prints why returning false from sendAlarm drops the alarm silently, what throwing does instead, how long the retry loop runs, and which four properties are the only evidence a site can send you.
- Alarms
- Retry
- No station
schedule-scan
Reads schedule-rt.jar with javap and prints the 90-day scanLimit horizon that turns a far-off change into no change at all, why nextCov steps over a boundary whose value matches, and the one serial uncapped queue every control schedule shares.
- Schedules
- Shipped jars
- No station
workbook-scan
Opens an .xlsx as the zip of XML it is and reports what is in the bytes: formulas saved holding an error, links into files that may be gone, saved queries to one person's mapped drive, approximate VLOOKUPs, hidden sheets, and rules nothing protects. It reads the old binary .xls too.
- Excel
- No install
- JSON out
poll-scheduler-scan
Reads the poll scheduler out of driver-rt.jar and prints the arithmetic: three rate defaults, one point polled per pass, and the bucket size at which the thread stops sleeping and the real cycle time stretches.
- Niagara
- javap
- Read-only
Next step
Send the driver and the tuning policy you ship.
Whether a dead device is visible depends on one property default and on which event your proxy extension calls readOk() for. Both are readable, so the answer comes back in writing with the bytes it was read from.