GK/SPEC-2026
DOC GK-002 TYPE ENGINEERING LOG SUBJECT AEGIS

The safety loop never touches the network.

AEGIS is an edge-AI worksite safety system: real-time person and PPE detection on a Raspberry Pi 5 running QNX 8.0 RTOS, in C++20, driving a physical relay. This is the log of the decisions that mattered, the trade-offs behind them, and what broke. Built at cuHacking 7, 2026. Source on GitHub.

01The constraint that designed the system

A worksite safety device has one non-negotiable property: when someone walks into a hazard zone without a hard hat, the alarm fires. Not usually. Not when the wifi is good. Always.

That single requirement decided the architecture before any code existed. Every component was sorted into two buckets: things the safety decision depends on, and things it must never depend on. The camera, the vision model, the state machine, the relay: bucket one, all on-device. The cloud gateway, the dashboard, event history, notifications: bucket two, all optional. The rule we wrote down and kept returning to: the cloud makes the device smarter, never dependent.

Most of the interesting decisions below are just this rule applied to a specific component.

02Why the emergency voice is baked into the firmware

We wanted a spoken evacuation alert, and ElevenLabs produces a far better voice than any on-device TTS. The obvious design is to call the API when an alarm fires. We rejected it in one sentence that became the project's motto:

A safety alarm that needs an API call to speak is not a safety alarm.

Instead, the voice line is generated once by ElevenLabs at build time and compiled into the device as a static asset. The network gets us the studio-quality voice; the runtime never knows the network exists. Same quality, zero runtime dependency. This is the pattern I now reach for by default: use the cloud at build time, not in the loop.

03The relay fires before the voice speaks

Within the device there is a second, subtler ordering problem. An alarm has multiple outputs: the GPIO relay (which can cut power to a machine, flash a beacon) and the audio playback. They are not equally important, and on a real-time OS you get to say so explicitly.

Under QNX's SCHED_FIFO policy, the relay and GPIO path runs at priority 30; audio playback runs at priority 8. If the system is ever under load, the physical interlock wins and the voice waits. The alarm never waits for its own announcement. The ordering guarantee was verified through structured latency measurement rather than assumed.

The general lesson: when outputs have different consequences, encode that in the scheduler, not in comments.

04Four processes, dumb messages

The pipeline runs as four separate QNX processes, capture, vision, safety logic, uplink, communicating over native MsgSend/MsgReceive. The message types are deliberately boring: trivially copyable POD structs, no serialization framework, no dynamic allocation in the message path.

Boring was the point. Process isolation means a fault in the uplink cannot take down the safety logic; POD messages mean IPC cost is predictable and a message can never fail to parse. On a safety path, every clever abstraction you don't use is a failure mode you don't have.

05Shipping the top of the stack before the hardware existed

The Pi and relay hardware weren't available on day one, and a hackathon doesn't wait. So the cloud gateway and dashboard were built against mock event generators that emit the same POD-shaped events the device would, plus a load tester and a chaos harness that injects malformed events, delays, and disconnects.

By the time real hardware produced its first event, the entire upper stack had already survived worse. The chaos harness also forced the uplink design honest: when the network drops, events buffer on-device and flush in zero-loss chunks when the link returns, a behaviour that exists because the harness kept breaking the naive version.

06What broke, and what I'd change

What broke: the first offline flush implementation lost events at the boundary between buffered and live traffic under reconnect churn, the chaos harness caught it, and the fix was chunked flushing with explicit acknowledgement. Cross-compiling C++20 for QNX via CMake also consumed far more of the first day than planned; toolchain work is never free.

What I'd change: the latency measurement should have been built on day one rather than added to verify the scheduler design after the fact: you want the instrument before the claim. And the vision model deserves a proper quantization pass; the current TFLite deployment prioritizes shipping over frame rate.

The live simulation of this pipeline, detection, zone rules, state machine, relay-before-voice ordering, and the offline buffer, runs in your browser on the main page. Flip the UPLINK switch while it runs.