Side Project · Industrial Tooling

Decoding downtime: two apps I built to find out why the sorter stops

Every time the sorter goes down, everyone asks the same two questions. Why did we stop, and where do we start fixing? The answers were sitting right there in the exports the whole time. They were just impossible to read. So I built a decoder for each one.

Jordan Cota  ·  July 2026  ·  ~8 min read

01The problem: the answers were buried in the exports

I work around an automated parcel sorter, a machine that sends thousands of packages an hour down dozens of chutes. When it stops, everyone wants the same numbers: how long were we down, what actually caused each stop, and which chutes need attention before the next sort.

Here's the thing. All of that information already existed, buried in two exports nobody wants to read. The first is the HMI alarm journal, a spreadsheet with thousands of rows, one for every alarm event. Every E-stop, every jam, every comm fault, timestamped to the millisecond and completely raw. The second is the chute fault report, a per-chute count of faults like failed-to-divert. It tells you which chutes are acting up, but nothing about where to start or when you're done.

Turning the journal into a downtime report used to mean filtering rows by hand, guessing at durations, and arguing about which of fifteen simultaneous alarms "caused" the stop. Turning the fault report into actual work meant printing it out and crossing off lines with a pen. Both felt like decoding problems to me. The information was all there, just written for machines instead of people. So I built two small apps, one for each export.

App 1: ADTA Decoder
Python engine + local web UI, shipped as a single standalone .exe
alarm journal .xlsxfull downtime report: exact durations, episodes, shutdown triggers, availability
App 2: Chute Remediation Checklist
A single HTML file. No install, no server, works fully offline.
chute fault exportinteractive fix list with zone decoding, outlier flags & progress tracking

02The ADTA Decoder: from alarm journal to answers

Drop in a journal, set the sort window, get the report.

Using it is simple on purpose. You double-click one file and the interface opens in your browser, running completely on your own machine. The journal never leaves it. Drag the alarm journal onto the page, confirm the sort window (it prefills from the journal's own span), add breaks and lunch, and hit Generate. A little stack-light on the page turns amber while it works and green when it's done. You get the headline numbers right on screen: total downtime, availability, episode count. Plus a full multi-sheet Excel workbook with the episode log, shutdown trigger analysis, alarm durations, a Pareto, an hourly timeline, and the cleaned data itself.

The fun part is everything that happens in between. The engine treats the downtime methodology as a set of explicit, testable rules. And almost every rule exists because a real journal broke a simpler assumption.

Reading the journal the way it's actually written

The biggest discovery was about the data itself. The journal logs each alarm twice. One row the moment it goes active, one row the moment it clears, and nothing in the export tells you which is which:

Alarm journal excerpt: what the engine sees
03:17:04.112   E-STOP 3 ENGAGED             HIGH   ← active
03:17:09.845   COMM LOSS, ZONE DR1_3        HIGH   ← active (cascade)
03:17:11.203   COMM LOSS, ZONE DR1_4        HIGH   ← active (cascade)
04:09:02.556   COMM LOSS, ZONE DR1_3        HIGH   ← clear
04:09:02.601   COMM LOSS, ZONE DR1_4        HIGH   ← clear
04:09:04.019   E-STOP 3 ENGAGED             HIGH   ← clear
→ one episode · 52.0 min · shutdown trigger: E-STOP 3 · comm faults credited to the stop

An early version assumed each row was its own ~30 second event, which is a pretty standard way to read alarm logs. It was also quietly wrong. It undercounted every stop longer than 30 seconds, so a 52 minute E-stop showed up as two half-minute blips. Once I checked the paired active/clear structure against the HMI's live state view, durations became exact. Clear minus active, no estimates. Rebuilding on that model was the single biggest accuracy fix in the whole project.

The pipeline

1
Pair
Match each alarm's active row to its clear row. Exact durations, strays repaired and flagged.
2
Union
Downtime = union of active windows, so a 15-alarm cascade counts once, not 15 times.
3
Episodes
Group into stops. A quiet gap only bridges to causally connected alarms.
4
Rules
Reattribute cascades, mask co-active symptoms, negate breaks, exclude warnings.
5
Report
KPIs on the page + the full Excel workbook, every adjustment listed and auditable.

Rules that earn their place

Raw alarm data kind of lies to you. It records symptoms and causes side by side like they're equals, and the rule engine's whole job is telling them apart. Every rule in it was validated, and in a few cases discovered, by going through real journals event by event:

Cascade reattribution
Comm and power faults that overlap an E-stop or safety-door stop get credited to the stop. They're the network's view of the shutdown, not separate failures.
Co-activity masking
Some alarms only count while their true cause is also active. Measured second by second of exact overlap, not by "it happened around the same time."
Planned-downtime negation
Alarm seconds inside breaks and lunch get negated, and availability is measured against scheduled time. That's the number that's actually fair.
Data-backed thresholds
A jam that starts while a jackpot-cart alarm is active is a late cart swap, not a mechanical jam. The 35 second linking window came straight from measuring real journals: most cart-driven jams fired within 5 seconds of the cart alarm.
Non-stopping warnings excluded
Low-voltage and under-speed warnings don't stop the sorter. The proof: a 42 minute over-current alarm where the machine kept right on sorting the whole time.
Honest naming
The report says shutdown trigger, not root cause. The earliest alarm in an episode is what tripped the machine. Calling it the root cause would be overselling the data.

When the data fights back

The hardest part wasn't the happy path. It was defending the pairing model against messy reality. An alarm that was already active before the export starts shows up as an orphaned clear. A panel restart can re-announce seventy alarms in a single second, which orphans rows mid-journal and flips the pairing for everything after it. That one invented hours of phantom downtime for a single alarm before the engine learned to catch and repair the flip. And one stray power-supply row once fabricated four thousand minutes of downtime all by itself. Every one of these cases now has a guard in the engine, a plain-language note in the report, and a regression test that has to pass before a release ships.

Same story at the analysis level. Running a week-long journal as one flat window gave a nonsense answer: thousands of minutes of "downtime" that were really just nights and gaps between sorts. Weekly mode fixes that by treating each sort day as its own run and rolling the results up:

7,467 min
one week analyzed as a single flat window (nonsense)
91.2 min
the same week in weekly mode, per sort day, rules scoped right
96.8%
fault-free share of scheduled time, measured fairly

03The Chute Remediation Checklist: from fault counts to a fix list

The whole app is one HTML file. Open it anywhere, even offline.

The second decoder is smaller, and it's all about the follow-through. Fault reports come in as per-chute counts with coded names like S0508#8. There's more meaning packed in there than it looks like. The prefix is the chute address, and the #N suffix is the sorter's zoning value, basically where along the chute's columns packages get dropped. The checklist decodes it on sight:

#0 · Automatic (round robin) #2 · Early columns #5 · Center columns #8 · Late columns

Import a spreadsheet, paste rows straight from a query, or drop in a CSV. The parser figures out the shape on its own, including one weird export format where every field arrives stacked in a single column and the records have to be rebuilt by spotting the repeating pattern. Chutes get grouped by address, sorted worst first, and turned into a checklist:

Each row expands into its per-position detail: zone chips, severity dots, and a readout comparing every position against its own zone's average across the dataset. So a position running at 2.1x its zone average gets a red outlier flag even if the raw count looks small. Techs check off positions as they go, leave notes on what they found, and the progress bar tracks the sweep. Everything saves in the browser between shifts, and progress exports back out as a CSV with the zones, ratios, and notes intact.

My favorite detail is hiding in the import button. The app reads native Excel files with zero libraries and zero network. An .xlsx file is secretly just a ZIP full of XML, so the app walks the ZIP's directory by hand, unzips the worksheet with the browser's built-in DecompressionStream, and parses the cells itself. About a hundred lines of code doing the job of a big third-party library. That's what keeps the whole tool a single file that works on a locked-down machine with no internet, which is exactly where it gets used.

04Two decoders, one loop

The apps split the problem the same way the work actually splits:

Diagnosis: ADTA Decoder

After the sort. How long were we down, what triggered each stop, and what deserves engineering attention. The workbook is the record and the Pareto is the priority list.

Treatment: Chute Checklist

Before the next sort. Which chutes and positions are faulting, which ones are outliers for their zone, who has checked what, and what they found.

One tells you why the machine stopped. The other turns the fault data into a list you can actually finish. The loop closes when the next journal and the next fault export show whether the fixes held.

05What building them taught me

Read the data the way it's written, not the way you assume it is.

The paired active/clear discovery invalidated every number the tool had produced before it. And what caught it wasn't luck. It was a habit of checking computed durations against the HMI's live view, and that habit became the backbone of the whole project.

Every rule needs a real proof case.

Nothing went into the engine just because it sounded reasonable. The cart-jam window is 35 seconds because measured journals said so. Over-current became non-stopping because one specific 42 minute alarm proved the sorter runs right through it. When a rule has a story behind it, people can challenge it. That's the point.

Keep the audit trail in the deliverable.

Every reattributed cascade, negated break, and dropped stray row is listed in the workbook itself. A downtime number people can dig into gets trusted. A black-box number gets argued with.

Meet users exactly where they are.

One tool is a double-click .exe serving a local page. The other is a single HTML file. No installs, no accounts, no cloud, nothing leaves the machine. On an industrial floor, deployment friction decides whether a tool gets used. Not features.

Small tools can move fast.

Version one shipped, and eighteen more releases followed inside a week, each one driven by a real journal that broke an assumption. A regression suite of audited nights is what made that pace safe. Every fix had to reproduce the already-verified reports byte for byte before it shipped.

The data was never missing. It was encoded in pairing conventions, cascade patterns, and zone suffixes. Both apps are just decoders with opinions, and every opinion had to prove itself against a real night on the floor.

If you've got a machine spitting out logs nobody reads, there's probably a decoder waiting to be built. What would yours be?