Free tool
schedule-scan — how a schedule decides its value and its next change
A control schedule looks like the simplest object in a station. schedule-scan reads schedule-rt.jar with javap and prints the three properties worth knowing before a site asks why an output did not change: the horizon beyond which there is no next change at all, the calendar every boundary resolves through, and the single serial queue every schedule on the station shares.
- Niagara schedules
- scanLimit
- Reads the shipped jars
- No station needed
- Read-only
- MIT
Install and run
One file, standard library only, and nothing on the network: it reads a jar on disk.
It needs a Niagara installation and a javap on the path, which the JDK
that ships with Niagara already provides. Point it at the installation or set
NIAGARA_HOME.
# download it next to wherever you are working
curl -O https://plantroomlabs.com/tools/schedule-scan.py
# read the schedule driver out of an installation
python3 schedule-scan.py /opt/Niagara/Niagara-4.15.5.22
What it printed here
This is the whole of one run against the installation on the machine that built this page, pasted by the script that publishes it rather than retyped. Four of the findings are worth reading before anything is driven off a schedule.
Ninety days is a horizon, not a performance setting.
scanLimit defaults to 90 days and it is the window
nextCov searches, so a change further out than that is reported as
no next change at all — a null time, not a far-off one. Anything a
graphic or a logic block drives off “when does this next change” inherits
that horizon.
nextCov returns the next change of value, not the next
boundary. It compares each candidate's output with equivalent()
against the value already in effect and steps over any boundary whose value matches,
so two back-to-back periods resolving to the same value are not a change.
The clock is a calendar. Chronometer extends
java.util.GregorianCalendar, so every boundary resolves through that
calendar's wall-clock, daylight-saving and timezone rules: a 06:00 edge is 06:00
local on both sides of a DST change, not a fixed number of hours apart. And
clockChanged re-runs execute() on any clock step, so an NTP
correction re-evaluates the output immediately rather than waiting for the next
boundary.
Every control schedule on the station shares one thread. There is a
single static ExecutionQueue named Schedule:Execution,
built with maxThreads=1. Its maxQueueSize is
0, and enqueue() reads a non-positive
maxQueueSize as no cap — so the shared queue is unbounded and
strictly serial. Every schedule-driven write on the station runs one at a time on
that one thread.
$ python3 schedule-scan.py $NIAGARA_HOME
How a control schedule picks its value and its next change
======================================================================
read from /opt/Niagara/Niagara-4.15.5.22
javap: 1.8.0_504
source: modules/schedule-rt.jar
The schedule clock is a calendar, not a UTC counter
----------------------------------------------------------
com.tridium.schedule.Chronometer extends java.util.GregorianCalendar.
Every boundary is resolved through that calendar's own wall-clock,
daylight-saving and timezone rules - a 06:00 edge is 06:00 local on
both sides of a DST change, not a fixed number of hours apart.
Two forward-search horizons are defined, exact to the millisecond
----------------------------------------------------------
_90_DAYS = 7776000000 ms = 90 days
_365_DAYS = 31536000000 ms = 365 days
How far ahead it looks: scanLimit, default 90 days
----------------------------------------------------------
BControlSchedule.scanLimit defaults to Chronometer._90_DAYS (the
90-day horizon above). It is the window nextCov searches for the
next change of output; a change further out than scanLimit is
reported as no next change at all (a null time).
nextCov returns the next change of VALUE, not the next boundary
----------------------------------------------------------
nextCov(now) reads getOutput(now), walks nextEvent forward, and
compares each candidate's getOutput with equivalent() against the
value already in effect - returning the first boundary whose value
actually differs. Two back-to-back periods that resolve to the same
value are not a change of value; nextCov steps over them. It stops
at now.add(getScanLimit()) - the 90-day window above.
A clock step re-evaluates the output at once
----------------------------------------------------------
clockChanged(delta) calls super.clockChanged then execute() straight
away, so an NTP correction or a DST shift re-runs the schedule and
re-posts the output immediately, rather than waiting for the next
scheduled boundary to come round.
Every control schedule shares one serial, uncapped execution queue
----------------------------------------------------------
BControlSchedule holds a single static ExecutionQueue, named
"Schedule:Execution", built with maxThreads=1 (one worker) and
maxQueueSize=0. enqueue() tests maxQueueSize and, when it is <= 0,
skips the QueueFull cap entirely - so the shared queue is unbounded
and strictly serial. Every schedule-driven write on the station,
from every control schedule, runs one at a time on that one thread.
======================================================================
Measured, not guessed: every number and name above is read from
schedule-rt.jar by this script and re-read identically on each run.
What it reads, and what that does not cover
It reads schedule-rt.jar with javap and nothing else. No
station is contacted and nothing is written — which is also the limit of what
the output is worth. It is a reading of compiled code, so it says what the framework
is built to do rather than what a schedule did over a bank holiday. The millisecond
constants it prints are exact; what a given site does with them is a question for
that site.
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.
Niagara schedules: special events and master copies is the configuration side of the same object — which copy wins, and what a special event does to the week.
The repository
The same file, MIT licensed, at github.com/UsamaIqbal0304/schedule-scan. Its README carries the findings and a run of the output, so the repository is checkable without downloading anything. Issues and pull requests are read.
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/schedule-scan.py when this page is built, so they cannot
disagree with it.
| Property | Value |
|---|---|
| File | schedule-scan.py |
| Size | 11,585 bytes |
| SHA-256 | 39b5f0128feaf525285ab988b99adea873971f8dc3fe9cf5344fd5b8ac863f99 |
| Licence | MIT — LICENSE.txt |
| Source | github.com/UsamaIqbal0304/schedule-scan |
To check it, on Linux sha256sum schedule-scan.py, on macOS
shasum -a 256 schedule-scan.py, on Windows
certutil -hashfile schedule-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
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
tuning-stale-scan
Reads the tuning policy and the stale branch out of the shipped jars: the default staleTime of zero, the clock it measures, and why an unsolicited device can sit dead with an ok status for as long as the station runs.
- Niagara
- javap
- Read-only
Next step
Send the schedule that did not change the output.
The horizon, the value comparison and the shared queue are three different reasons for the same symptom, and the station holds enough to say which. That read costs nothing either way.