Skip to content

CogniFlight Cloud

Ground station for an aviation fatigue-monitoring capstone: MQTT telemetry, time-series analytics and a desktop-OS dashboard UI I built for operators.

role
Frontend · pilot-enrolment API
status
archived
date
2025
links
repo ↗
  • React
  • Go
  • Gin
  • WebSockets
  • MQTT
  • InfluxDB
  • Telegraf
  • MongoDB
  • Python
  • Docker
  • Traefik
~/projects/cogniflight-cloud/architecture.txt
six services behind Traefik; the Go API and ML engine talk over a Unix socket
capstone mark
97%
services behind Traefik
6
person capstone team
8

The system

CogniFlight detects pilot fatigue in real time. Edge devices in the cockpit stream fatigue indicators — eye-aspect ratio, yawning, heart rate, heart-rate variability, environmental data — and the cloud platform turns that stream into live dashboards, alerts and analysis for operators on the ground.

Six Docker services behind Traefik make up the ground station: a Go + Gin backend, a Python ML engine, a Mosquitto MQTT broker with custom auth, Telegraf feeding InfluxDB time-series storage, MongoDB, and the React dashboard. This was our final-year capstone, built by a team of eight split into two sub-teams — Brian Felgate led the cloud team, I led the edge team — and it earned 97%.

My part: the operators' desktop

The frontend was my seat. Air-traffic controllers juggle many aircraft at once, so instead of a single scrolling dashboard I built the UI as a windowed desktop: a window manager with taskbar, start menu and notifications, where each concern — live edge-node dashboards, pilot management, file explorer, camera feeds, a terminal — is an app operators arrange side by side.

The centerpiece is the edge-node dashboard: live attitude indicators and telemetry charts streaming over a WebSocket, with the ML engine's fatigue reasoning surfaced inline, so an operator sees "microsleep indicators, EAR below threshold" next to the raw signal that caused it. I also seeded the facial-recognition embedding API used to enroll pilots.

The operator's minute

The workflow the desktop is built around is a single minute: an edge node's fatigue score climbs, the dashboard window for that node lights up, and the operator needs to answer "is this real?" without leaving the screen. So the telemetry chart, the ML engine's reasoning for the flag, the camera feed and the pilot's enrolment record open as windows next to each other rather than as tabs that hide one another. Two infrastructure choices exist for that minute. Telemetry lands in a time-series store because every reading is an append-only sample queried by time window — "show me the last ninety seconds of this node" is the only question anyone asks of it. And the devices publish over MQTT because cockpit links drop: the broker's session semantics tell the ground station a node has gone quiet before any dashboard would notice the chart stopped moving.

Details that changed how I think

These two designs are the work of Brian Felgate, who led the cloud team — not mine — but I built against them daily, and they raised my bar for what an interface can be.

The backend is a shell. The browser doesn't call REST endpoints for most things — it opens a WebSocket running an actual command shell with 27 commands (ls, cat, chmod, flux, mqtt, ml-rpc…), frames encoded in MessagePack. The frontend terminal app I built is a real client of the same interface every other app uses.

Permissions are tags, not roles. Every entry in the MongoDB-backed virtual filesystem carries read/write/execute tag lists, matched against the user's tags — a capability-style ACL with a meta-rule that you cannot grant tags you don't hold:

func (p FsEntryPermissions) IsAllowed(mode FsAccessMode, tags []string) bool {
    var check []string
    switch mode {
    case ReadMode:              check = p.ReadTags
    case WriteMode:             check = p.WriteTags
    case ExecuteMode:           check = p.ExecuteTags
    case UpdatePermissionsMode: check = p.UpdatePermissionTags
    }
    for _, tag := range tags {
        if slices.Contains(check, tag) {
            return true
        }
    }
    return false
}

Processes talk over sockets, not networks. The Go backend and the Python ML engine (InsightFace face embeddings, threshold-based fatigue reasoning) speak JSON-RPC 2.0 over a Unix domain socket shared through a Docker volume — no exposed ports, no auth surface.

What it taught me

Working on a team of eight with a service-oriented codebase forced clean contracts: my frontend consumed the WebSocket shell and the telemetry fan-out exactly as any other client would. The two-team structure taught the same lesson at a larger scale: because Brian and I stayed in constant contact, edge and cloud were built in parallel and integrated continuously — the contract never drifted far enough to bite. And building an operating-system metaphor for operators here directly shaped the interface of Exequtech OS — and, in a way, the site you're reading.