Out-of-adjustment brakes are among the most common serious defects found on commercial vehicles at roadside inspections in North America. The check itself is not complicated: apply the brake, see how far the pushrod travels, compare that travel against a limit.
The difficulty is everything around it. A driver has to do that at every wheel end β ten of them on a tractor with a two-axle trailer β alone, usually in the dark, before a shift that hasn't started yet.
And not once. Every shift, every day. Carriers hauling dangerous goods β liquefied oxygen, hydrogen β commonly require the inspection at the start and the end of every shift, which doubles it again.
That is the number that matters. A check performed once is a task. A check performed twice a day, every day, by a driver under schedule pressure, in the dark, is a system under strain β and the pressure always falls on the same place: the care taken at the tenth wheel end versus the first.
And when it's done, the output is a checkmark on a form that nobody can verify afterwards.
Thirty minutes becomes ten seconds
Sentinel-Q replaces the walk with a single brake application.
The driver presses one button, gets a three-second countdown, applies the brake and holds it. Every enrolled wheel end is read during that hold. Ten seconds later they release, and the result is on the display.
That is the value proposition in one line: an inspection that takes around half an hour becomes a ten-second brake application β performed from the cab, identically every time, by a driver who never has to kneel at a wheel end in the dark or decide how carefully to look at the tenth one.
And it happens from the driver's seat. That is a separate benefit from the time saving, and arguably the larger one. The driver stays belted in, in full control of the vehicle, out of the weather β instead of kneeling beside a wheel in the rain, on ice, or on a shoulder with traffic going past at highway speed. Every inspection puts a person on the ground beside a vehicle, in conditions nobody chose. This one doesn't.
It is also why the sweep runs during the hold rather than after it. The architecture and the benefit are the same fact: the sensors are read while the brake is applied, because that is the only moment the measurement means anything, and it is the only moment the driver is holding still.
Each wheel end carries a Sentinel sensor β Quantuity Analytics hardware, patent pending, running an Edge Impulse classifier on the sensor itself and reporting over Bluetooth Low Energy. The Arduino UNO Q is the gateway: it commands every enrolled sensor in turn, decides what the results mean, and writes a record that can be checked later by someone who wasn't there.
One thing worth drawing out about that sensor because it changes what the system can become. It does not take a measurement and stop. It classifies continuously, and the gateway reads it roughly twice a second for as long as the link is held open β which is what the live demonstration in the video is actually showing. Move the pushrod, and the verdict follows it.
A pre-trip inspection is one use of that stream. Monitoring a brake while it is working β during a descent, under load, over a shift β is another, and it is the same hardware and the same gateway. That is not built today and is not claimed as built. But the reason it is reachable is that nothing in this design assumes a measurement is a one-off event.
It performs an adjustment check against a configured limit. Not a certification, not a compliance determination β a measurement, a threshold, and an honest record of both.
Why this board, specificallyThe UNO Q has two processors on one board, and this project needs both.
The Qualcomm QRB2210 runs Debian Linux with a full Bluetooth stack. That is where the BLE central lives, where the supervisory logic runs, and where the inspection ledger is written. None of that would fit comfortably on a microcontroller.
The STM32U585 runs the Arduino core, drives the 8Γ13 LED matrix, and reads the driver's button. That is real-time work with no business being scheduled by Linux.
A single-processor board would have forced a choice between a BLE stack and a responsive driver interface. This one doesn't.
Three decisions, and whyAnyone can wire a sensor to a board. The interesting part of this project is the reasoning, so here is the reasoning.
1. Sensors are enrolled to a wheel end, not to a positionWheel positions on a commercial vehicle are numbered clockwise from the passenger-side steer. With three axles on the tractor, the driver-side steer is position 6. Couple a two-axle trailer and that same physical brake becomes position 10 β every number on the driver's side shifts.
If inspections were logged by position, a trend line would silently compare two different brakes across sweeps and call the difference drift. On a safety system that is a false-positive generator.
So the ledger stores identity β unit, axle, side β and derives the walkaround position at display time:
def position(self, wheel_end: WheelEnd) -> int:
n = self.axle_count
axle = self.global_axle(wheel_end)
if wheel_end.side == PASSENGER:
return axle
return 2 * n + 1 - axleSensors are enrolled by BLE address at installation. Swap a sensor and the replacement is enrolled to the same wheel end; every record carries the sensor address it came from, and trend analysis splits its window wherever that address changes. A replaced sensor cannot masquerade as brake drift.
2. The gateway is allowed to refuseA model that has to answer will answer, including when it shouldn't.
Our classifier cannot reliably separate one particular class from another. We could have inferred around it. Instead, the calibration marks that class untrusted, and when it comes back, the gateway returns INCONCLUSIVE with a stated reason β while still recording the raw label, so nothing is hidden and nobody has to take our word for what the sensor said.
The check happens before the confidence gate, deliberately:
if not cal.is_trusted(top_label):
return Assessment(Verdict.INCONCLUSIVE, Resolution.EXCLUDED_LABEL, ...)There is a related honesty problem in the data itself. This sensor argmaxes on-device β it sends the winning class name and no probability at all. So a reading carries a decision_only flag, and the single score recorded as 1.0 means the device did not tell us, not thatthe device was certain. Anything downstream that reads confidence has to check that flag first. Treating a missing probability as certainty is exactly the kind of quiet error that makes a safety system untrustworthy.
On a system like this, a confident wrong answer is worse than no answer.
3. The record has to survive scrutinyEvery reading is appended to a SQLite ledger, hash-chained: each record's hash covers its own canonical fields plus the hash of the record before it. Verification walks the chain; editing any historical value breaks it at that point and names the record.
This is tamper-evident, not tamper-proof. Anyone with write access could recompute the entire chain. Signing records off-device is the obvious next step, and it is not built yet. We would rather say that plainly than let the word "blockchain" do work it hasn't earned.
The architecture, and the wall we hitThe plan was for everything to live in one App Lab application.
It can't. App Lab applications run in a container with no D-Bus and no BlueZ access, so a BLE central cannot function inside one, and no BLE brick exists. We found this early, and it shaped the entire system.
The BLE central runs as a service on host Debian. The App Lab side reaches it over HTTP at 172.17.0.1. The seam isn't elegant β it's the platform telling us where its boundary is, and the honest thing to do was design around it rather than pretend it wasn't there.
ββββββββββββββββββββββββββββββββββββββββββββ
β STM32U585 β Arduino core β
β LED matrix Β· driver button (D2) β
β FDCAN1 reserved on D4/D5 β
βββββββββββββββββ¬βββββββββββββββββββββββββββ
β Bridge RPC
βββββββββββββββββ΄βββββββββββββββββββββββββββ
β QRB2210 β Debian 13 β
β β
β App Lab container no D-Bus, no BlueZβ
β β HTTP 172.17.0.1 β
β host: BLE central (bleak) βββ BLE ββββΆ β Sentinel sensors
β supervisory logic Β· ledger β
ββββββββββββββββββββββββββββββββββββββββββββCurrent status, stated precisely: both halves work and are demonstrated separately. The gateway takes live readings from the real sensor over BLE. The driver interface runs on the MCU. Joining them through the host service is the next integration step, not a finished one.
What is open, and what is notThe entire gateway is published: enrollment, identity, sweep orchestration, the supervisory layer, the ledger, the driver procedure, the sketch. That is the contribution, and a repository nobody can learn from would defeat the point of publishing.
What stays closed is the measurement science β the mapping from a model's class output to a physical distance, and the service limits drawn across those distances.
The separation is implemented as runtime artifacts, not secrets in source:
sentinelq/model.pydefines a calibration schema and loads one. It holds no measurements. The repository ships an illustrative example that declares itself illustrative.sentinelq/profile.pyloads the sensor's wire details β the notify characteristic and the class-name translation. The constant in tracked source is permanentlyNone.- Translation happens at the parser boundary, so nothing downstream ever holds the sensor's own vocabulary β not a ledger row, not a console line, not a screen recording.
Ten assertions in tests/test_disclosure.py fail the build if any of that slips. They caught three real leaks the first time they ran.
This is also better engineering than hardcoding would have been. Two clients read this sensor β an Android application and this gateway. If each held its own copy of the calibration, they would drift apart the first time the firmware changed, and the symptom would be a wrong measurement rather than an error. One versioned artifact, loaded by both, is correct regardless of who can see the repository. Privacy is a consequence of that design, not the motive for it.
What actually happened on build dayA build story with no failures in it is a brochure. Here are the ones that cost real time.
The five-minute check that de-risked everything. Before touching anything, we looked up the ArduinoCore-zephyr version, because native CAN on D4/D5 only exists from 0.55.0 onward and getting there otherwise means updating the loader β the one operation known to brick these boards. It reported 0.90.0. CAN stayed possible and no risk was taken. Five minutes of looking saved an evening of either regret or wishful thinking.
The button that isn't a button. There is a button on the board marked POWER. Tracing it: JBTN1 goes to the Qualcomm PM4125 PMIC on KYPDPWR_N, not to a GPIO. A sketch cannot read it. The driver's trigger moved to an external switch on D2, with D4 and D5 deliberately left free for FDCAN1.
Debian 13 has opinions about Python. Trixie enforces PEP 668 and ships without pip and without ensurepip, so python3 -m venv fails out of the box. What worked, with no root:
curl -sSL https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
python3 /tmp/get-pip.py --user --break-system-packages
python3 -m pip install --user --break-system-packages bleakNot every USB-C port is a USB-C port. The board refused to enumerate through a newly fitted USB-C PCIe add-in card. Swapping cables changed nothing. A standard USB-A-to-C cable into a motherboard port worked immediately. Arduino's own guidance says to connect directly rather than through hubs or intermediary controllers β an add-in card is one of those. That cost about an hour.
App Lab blocks silently on a board it cannot reach. With the link in a bad state, it sat on a loading screen with no error and no timeout. Unplugging the board brought the UI up instantly β which is the diagnostic.
adb devices can succeed while adb shell hangs. The device listing is cached by the adb server; the connection is not. adb kill-server plus a physical reconnect is the fix.
The first live read failed, and the failure was the finding. The gateway connected, started inference, and received a notification β one byte long. A one-byte frame cannot be a class name. The firmware emits a status frame when inference starts, and our code was parsing that acknowledgement as a classification. The parser now drains frames until one resolves, bounded by the caller's timeout, and reports what it skipped if it times out. The second attempt returned a valid reading.
The judgement that mattered most wasn't technical. Twenty minutes into the adb shell problem, the right call was to stop debugging the shell and reconnect the hardware. On a deadline, the cost of a stalled path is not the time already spent β it's the time still to be spent on it.
A thirty-minute pre-trip brake inspection becomes a ten-second brake
application. An Arduino UNO Q gateway reads edge AI sensors at every wheel end
from the cab, refuses to guess when it can't be sure, and writes a
tamper-evident record.A thirty-minute pre-trip brake inspection becomes a ten-second brake
application. An Arduino UNO Q gateway reads edge AI sensors at every wheel end
from the cab, refuses to guess when it can't be sure, and writes a
tamper-evident record.INSTRUCTIONS β build and run it yourselfEverything below runs with no proprietary data. The simulated backend exercises the entire gateway, so the project is fully explorable without our sensor.
1. Run the gateway with no hardware at allgit clone https://github.com/markdheilong/sentinel-q.git
cd sentinel-q
python -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/python -m pytest -q # expect 80 passed
.venv/bin/python demo.pydemo.py runs twelve sweeps against a simulated sensor, builds a trend, renders the inspection record, verifies the hash chain, then tampers with a value in SQLite and shows verification failing.
Verified platform, captured from the board rather than from documentation:
Host OS
Debian 13.1 (trixie), kernel 6.16.7, aarch64
MCU core
ArduinoCore-zephyr 0.90.0
App Lab
0.10.0 Β· arduino-cli 1.5.1
Bluetooth
BlueZ 5.82, controller uno-q
Python
3.13, bleak 3.0.2, dbus-fast 5.0.22
Connect the board with a standard USB-A-to-C cable, directly to a motherboard port. Not through a hub or an add-in card.
Open App Lab, wait for the board to show asconnected, then open its terminal.
3. Install on the boardcd ~
git clone https://github.com/markdheilong/sentinel-q.git
cd sentinel-qConfirm Bluetooth is on the host side, not in a container:
bluetoothctl --version # expect 5.82
systemctl is-active bluetooth
bluetoothctl list # expect a controllerIf bluetoothctl list returns nothing, you are inside an App Lab container, and BLE will not work there.
The result characteristic and class vocabulary are not in the repository. Create config/sensor.private.json β the *.private.json pattern is gitignored:
{
"result_char_uuid": "<notify characteristic from your sensor>",
"encoding": "label",
"label_prefix": "<your firmware's class-name prefix>"
}To find those values, connect to the sensor with nRF Connect, expand the custom service, and look for the characteristic with the NOTIFY property.
5. Take a live readingpython3 live_read.py <SENSOR-BLE-ADDRESS>The sensor must be powered, and no other central may hold the link β a BLE peripheral accepts one connection at a time, so disconnect your phone and close nRF Connect completely. That single mistake accounts for most apparent failures.
A successful run prints the resolved class, whether the device reported confidence, and the wire encoding.
6. The MCU sidesketch/sketch.ino drives the 8Γ13 matrix and reads the driver's switch on D2 (INPUT_PULLUP, closed reads LOW), with 40 ms debounce and a latch. D4 and D5 are left free for FDCAN1.
Note that Serial on this board is the UART on pins 0/1 β USB debug output goes through Monitor from Arduino_RouterBridge.
Repository:https://github.com/markdheilong/sentinel-q β MIT licensed, commented throughout, 80 passing tests.
Module Β· What it does - sentinelq/identity.py β Wheel-end identity and the walkaround position formula - sentinelq/registry.py β Enrollment β one active sensor per wheel end, auto-retiring the previous - sentinelq/verdict.py β The supervisory layer: excluded classes, confidence gating, trend analysis - sentinelq/ledger.py β Append-only hash-chained SQLite record with verification - sentinelq/sweep.py β Sweep orchestration across enrolled wheel ends - sentinelq/procedure.py β Driver sequence β countdown, instruction, hold, release - sentinelq/display.py β 8Γ13 framebuffer and 5Γ7 font - sentinelq/model.py β Calibration schema and loader. Holds no measurements - sentinelq/profile.py β Runtime sensor wire details. Holds none in source - sentinelq/backends/ β The hardware seam β one interface, simulated and bleak implementations - sketch/sketch.ino β MCU side: matrix, button, Bridge RPC - tests/ β 80 tests, 10 of them disclosure guards.
Repository: https://github.com/markdheilong/sentinel-q β MIT licensed, commented throughout, 80 passing tests.
ROADMAPNamed honestly, because knowing where the work goes next is part of knowing what you've built.
CAN / J1939 transmit. The board has native FDCAN1, and D4/D5 were reserved for it from the first day. An SN65HVD230 3.3 V transceiver and a CANable-compatible USB adapter are both on the bench β the adapter matters, because a bus with a single talker proves nothing, and the second node is how you verify a frame actually landed. Not built.
Joining the two halves. The host BLE service plus an App Lab application, so the button press and the sweep are one flow rather than two demonstrated pieces.
Off-device signing. The ledger is tamper-evident today. Signing records outside the device makes it meaningfully stronger.
Trend-based maintenance. The temporal drift logic exists and is demonstrated across sweeps. Turning that into a maintenance signal a fleet acts on is a product question, not a code one.
ABOUTBuilt by Mark Donaldson, founder & CEO at Quantuity Analytics Inc. (Mississauga, Ontario), with early work through the SmartTO program at York University and YSpace in collaboration with Centennial College's ARIES Research Center at the School of Transportation. The Sentinel sensor is Quantuity hardware and is patent pending.
Sentinel-Q, the gateway, is published under the MIT licence.
On how this was builtSentinel-Q was developed in collaboration with Claude Opus 5 β pair programming, with the commit history recording it in the co-author trailers.
The division of labour is worth stating plainly. The sensor hardware, the brake fixture, and the domain requirements are Quantuity's: that the inspection happens every shift and twice a shift for dangerous goods carriers; that the real win is the driver never leaving the seat; which chamber the system targets; what an in-service and an out-of-service condition mean to someone who does this work. The engineering decisions were made here.
The gateway was written together, reviewed line by line, and every claim on this page was checked against a disclosure policy that the test suite enforces.
Work smarter. Build with AI.
















Comments