Reference implementation of Sentry-CD, a local counter-drone command system built for the EDTH hackathon. A body-worn microphone listens for a drone-like acoustic signature, a point-of-view vision check confirms or rejects the contact, and late fusion turns the two confidences into a threat level and an escalating recommendation. The operator confirms, escalates, or stands down. The service never engages an effector.
Sentry-CD starts from swappable sensor adapters (simulated microphone and scripted vision now; a Raspberry Pi mic and a Meta glasses feed later) and runs the full pipeline in one FastAPI process: scenario player, acoustic gate, vision worker, fusion, tracker, and a WebSocket gateway. The Expo operator app renders the live state — acoustic orb, fused meters, recommendation, tracking compass, and the server-driven reasoning trail.
In this repo the simulated end-to-end demo is the supported path. Start the gateway, launch the iOS app, then POST /scenario/demo-alpha/start or POST /scenario/demo-bravo/start. An optional laptop overlay (vision/live_overlay.py) can replace the scripted vision event with a live YOLO11 detection from a screen-captured POV.
This repository is published as EDTHHACK. The product name is Sentry-CD. Milestone v1 (all five planned phases) is in the tree: backend skeleton, vision pipeline, fusion and escalation, mobile integration, and target tracking.
- Overview
- Problem
- Approach
- Results
- Architecture
- Features
- Directory Structure
- Requirements
- Installation
- Running the Demo
- Configuration
- Scenarios
- Operator App
- Live Vision Overlay
- Customizing
- Troubleshooting
- License
This repo is the EDTH hackathon build of a body-worn counter-drone sentry. It provides:
- The gateway — a single-process FastAPI service (
backend/main.py) that wires scenario playback, acoustic gating, vision, late fusion, tracking, escalation, and the WebSocket fan-out. - Two scripted engagements —
demo-alpha(confirmed inbound drone) anddemo-bravo(acoustic contact that vision classifies as a bird). Both are deterministic timelines underbackend/sim/scenarios/. - The operator app — Expo SDK 56 / React Native, with a Skia acoustic orb, threat panel, tracking overlay, and reasoning trail. It subscribes to
ws://<host>:8000/wsand posts operator actions to the REST control plane. - A live vision seam —
vision/live_overlay.pyruns COCO YOLO11n and a drone YOLO11 on a screen region (or a video file) and POSTs detections to/api/vision/detection. - Smoke tests — in-process checks for the gateway, the acoustic-to-vision cascade, fusion and human confirmation, and tracking plus false-alarm restraint.
Sensor hardware is behind adapters in backend/adapters/. ACOUSTIC_ADAPTER and VISION_ADAPTER select the implementation. The shipped adapters are simulated. A future Raspberry Pi mic or hosted CV model is meant to return the same event types, so fusion and the app stay unchanged.
Someone wearing the node hears a rotor and has seconds. Running vision on a head-mounted camera all the time is expensive and late. Trusting the microphone alone is fast and full of false alarms: wind, speech, birds, other machinery. A system that skips the person and fires a countermeasure on its own is the wrong product for this setting.
The operator needs one picture, on a phone, over the local network:
- Was the sound drone-like, and did it persist long enough to matter?
- Did the point-of-view camera agree, and with what class and confidence?
- What is the fused threat level, and which recommendation follows — monitor, request another look, alert the squad, or prepare countermeasure options?
- If the contact is real, where is it, and is it closing?
The last step stays a decision. Confirm, escalate, and stand down are recorded. Stand down clears the picture.
The pipeline is late fusion with a human on the loop. Vision is a confirmation modality, so it carries more weight than sound, and sound alone cannot reach the top threat bands.
- Acoustic gate. The scenario player (
backend/sim/engine.py) walks a timed list of frames.SimulatedAcousticAdaptersynthesises RMS, SNR, band energy, a dominant frequency, a class (drone_likeornoise), and a confidence.AcousticNodepublishes every frame. It requests vision only after two consecutive drone-like frames at or above0.55confidence, then waits6seconds before it may trigger again. - Vision on demand.
VisionWorkerhandles that request. Inscriptedmode the adapter returns the next entry in the scenario's vision plan (class, confidence, bounding box) afterVISION_LATENCY_S(default0.8). If no result arrives withinVISION_TIMEOUT_S(default3.0), fusion falls back to an acoustic-only assessment. A live overlay can inject a real detection onPOST /api/vision/detectioninstead of waiting for the script. - Late fusion.
backend/core/fusion.pyscores0.35 × acoustic + 0.65 × vision. A visual class other thandroneis multiplied by0.3before that sum, so a confident bird pulls the fused score down. Bands are low below0.50, medium at0.50, high at0.70, critical at0.85. An acoustic-only score is capped at0.55. - Recommendation, then a person. The band maps to one action:
monitor,request_visual,alert_squad, orprepare_countermeasures. Actionable levels setrequires_confirmation.EscalationManageracceptsconfirm,escalate,stand_down, andacknowledge. Confirm stamps the assessment. Escalate steps the recommendation up one level, to a maximum of 4. Stand down resets fusion, the tracker, and the operator state, and the orb returns to white. Every action is appended to an audit log. Nothing in this path actuates an effector. - Track only after confirmation. A fused assessment at high or critical, or a live vision-only dispatch, opens one simulated track. Bearing and range come from the scenario plan (
demo-alphastarts at bearing312°, range480 m, closing at14 m/s). The track emits updates until range passes40 m, then marks the contact lost.demo-bravostays low and opens no track. - Show the chain. The gateway broadcasts state snapshots and events on
/ws. A reconnect receives the current snapshot plus a replay of the lastEVENT_BUFFER_SIZEevents (default 20). The reasoning trail is a server-side stage machine (LISTENING→ACOUSTIC_HOT→VISION_PENDING→VISION_CONFIRMED→ASSESSED, plusTIMEOUTandDISPATCH) rendered by the app. The app does not invent the cascade locally.
A live drone detection at or above LIVE_CV_ESCALATION_CONF (0.70) short-circuits to a critical, vision-only assessment and stages a counter-UAS resource package (effector, kit list, soldier grid, ETA). That package is a recommendation. Operator authorization is still required before any effect.
Scripted scenarios, measured by the in-repo smoke tests on 26 October 2026. Acoustic confidence includes about ±0.05 of synthesis jitter, so the fused float moves slightly between runs. The tests assert the threat band, the recommendation, and whether a track opens. They do not freeze a decimal, and they are not a field detection rate.
| Scenario | Sensors | Fused | Threat | Recommendation | Track |
|---|---|---|---|---|---|
| demo-alpha — drone approach | Acoustic 0.70 at the vision trigger. Vision class drone at 0.91. |
0.84 |
high | Alert squad. Confirmation required. Bearing 312°. |
Opens at 480 m and closes (480 → 466 → 452 m). |
| demo-bravo — bird, false alarm | Acoustic 0.66. Vision class bird at 0.74, then downweighted. |
0.38 |
low | Monitor. No squad alert. | No track opened. |
| Check | What it locks | Result |
|---|---|---|
tests/smoke_phase1.py |
Health, scenario list, acoustic broadcast, reconnect replay | Pass |
tests/smoke_phase2.py |
Acoustic trigger produces a vision event linked to that acoustic id; class drone, confidence ≥ 0.8 |
Pass |
tests/smoke_phase3.py |
Fused source mode, threat in {high, critical}, recommendation level ≥ 3 with confirmation, bearing 312°, confirm lands in the audit log, stand-down returns orb white and threat low |
Pass |
tests/smoke_phase5.py |
At least three track updates, range strictly closes, false alarm stays in {low, medium} with recommendation level < 3 and no track |
Pass |
| v1 requirements | 30 mapped requirements | 29 delivered. UI-05 haptics and sound are deferred; the visual alert (red orb, threat badge, recommendation card) is in. |
Fusion in this milestone is a fixed weighted sum. A calibrated or Bayesian model is deferred (FUS-05).
The game loop is one Python process. The phone is a client.
ScenarioEngine → AcousticAdapter → AcousticNode → VisionWorker
↓
FusionEngine → Tracker
↓
EventBus → WebSocket gateway → Expo C2
↑
Operator confirm / escalate / stand down
backend/main.pybuildsAppContexton startup and tears it down on shutdown. Routes and the socket read that context offapp.state.ctx.- HTTP is the control plane: health, state, scenario start/stop/reset, operator actions, the audit log, and live-vision ingest.
- WebSocket
/wsis the data plane. Clients get a snapshot on connect, then live events.gateway/ws.pykeeps the replay buffer. - The app (
App.tsx) callsuseServerEvents()and renders from a Zustand store. It does not run fusion.
Typical process layout:
- Terminal A runs
uvicorn main:appon port8000. - Terminal B runs
npm run ios. The simulator useslocalhost. A physical device needsEXPO_PUBLIC_SERVER_HOSTset to the machine's LAN address before Metro bundles. - Optional terminal C runs
vision/live_overlay.py, which posts to the same gateway.
Module notes live next to the code: backend/README.md (endpoints, env vars, demo runbook, tests) and vision/README.md (overlay flags and the Intel-Mac torch pin).
- Scripted, repeatable engagements with a false-alarm counterpart, so a demo does not depend on a live aircraft.
- Temporal acoustic gating so a single noisy frame cannot call vision.
- Late fusion with an acoustic-only cap, so missing vision cannot reach high or critical on sound alone.
- Downweighted non-drone visuals, so a bird classification pulls the threat back to monitor.
- Human-on-the-loop controls with an append-only audit log (
GET /audit). - Single-target tracking after a confirmed threat, including a lost state once range passes the minimum.
- Reconnect replay so a dropped phone socket catches up from the ring buffer.
- Live YOLO overlay (COCO aerial classes plus a drone-specific YOLO11) that can drive a critical dispatch without the scripted scenario.
- Swappable adapters selected by environment variable. Processing and the gateway do not import hardware drivers.
Out of this milestone, on purpose: real Raspberry Pi or Meta glasses I/O, autonomous countermeasure fire, multi-target tracking, Bayesian fusion, Kalman smoothing, and haptics.
EDTHHACK/
├── README.md
├── package.json # Expo SDK 56 operator app
├── app.json # Expo config (Sentry-CD, dark UI)
├── App.tsx # Operator screen
├── index.ts
├── assets/ # App icon and splash
├── docs/
│ └── logo.png # README mark
├── src/
│ ├── config.ts # HTTP + WebSocket host (EXPO_PUBLIC_SERVER_HOST)
│ ├── store.ts # Zustand store, threat and reasoning types
│ ├── theme.ts # Dark liquid-glass palette
│ ├── acoustic/ # Shader drive for the orb
│ ├── components/ # AcousticOrb, ThreatPanel, ReasoningTrail,
│ │ # TrackingOverlay, GlassCard
│ └── transport/ # useServerEvents.ts, api.ts
├── backend/
│ ├── main.py # FastAPI app and pipeline wiring
│ ├── config.py # Adapter selection, thresholds, fusion weights
│ ├── models.py # Event and assessment schemas
│ ├── requirements.txt
│ ├── adapters/ # AcousticAdapter, VisionAdapter (simulated)
│ ├── core/ # acoustic_node, vision_worker, fusion,
│ │ # escalation, tracker, reasoning, event_bus, state
│ ├── gateway/ # REST routes and WebSocket connection manager
│ ├── sim/ # ScenarioEngine
│ │ └── scenarios/ # demo_alpha.json, demo_bravo.json
│ └── tests/ # smoke_phase1/2/3/5.py, live_client.py
└── vision/
├── live_overlay.py # Screen or file YOLO overlay
├── requirements.txt # torch 2.2.2 pin for Intel Mac CPU
└── models/ # yolo11n.pt, drone_yolo11.pt (local weights)
- Python 3.11 for the gateway
- Node 20+ for the operator app
- Xcode, for the iOS dev build the Skia orb needs
- Optional, for the live overlay: a machine that can run the pinned
torch==2.2.2/opencv-python==4.10.0.84stack, plus Screen Recording permission if you capture a call window - No game ROM, no API key, and no GPU for the scripted demo
git clone https://github.com/Sebastianrodaaa/EDTHHACK.git
cd EDTHHACKFrom backend/:
cd backend
uv venv --python 3.11 .venv
uv pip install -r requirements.txt --python .venvuv is the path used in development. A plain venv works the same way:
python3.11 -m venv .venv
.venv/bin/pip install -r requirements.txtFrom the repo root:
npm installThe first npm run ios runs Expo prebuild and compiles the native Skia / Reanimated binary. Later launches reuse that build.
Only needed if you want a real detector instead of the scripted vision plan. From vision/:
cd vision
python3.11 -m venv .venv
.venv/bin/pip install -r requirements.txtWeights are expected at vision/models/yolo11n.pt and vision/models/drone_yolo11.pt. The overlay's own notes, including the Intel Mac pin, are in vision/README.md.
Two terminals. The gateway binds 0.0.0.0 so a phone on the same LAN can reach it. The iOS simulator can use localhost.
Gateway
cd backend
.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8000GET http://localhost:8000/health returns {"status":"ok", ...}.
Operator app
npm run iosThe header pill should read Live once the socket connects. A physical device:
EXPO_PUBLIC_SERVER_HOST=192.168.1.42 npm run iosUse the machine's LAN address, not localhost. The app port is fixed at 8000 in src/config.ts.
Start an engagement
# Confirmed drone: orb goes red, vision agrees, recommendation is alert squad, track opens
curl -X POST http://localhost:8000/scenario/demo-alpha/start
# False alarm: vision says bird, threat stays low, no track
curl -X POST http://localhost:8000/scenario/demo-bravo/start
# Stop the timeline, or stop and clear threat state
curl -X POST http://localhost:8000/scenario/stop
curl -X POST http://localhost:8000/scenario/resetIn the app, Confirm, Escalate, and Stand down call POST /operator/action. Confirm and stand-down show up in GET /audit.
Smoke tests (no server process; they use FastAPI's in-process client):
cd backend
.venv/bin/python tests/smoke_phase1.py
.venv/bin/python tests/smoke_phase2.py
.venv/bin/python tests/smoke_phase3.py
.venv/bin/python tests/smoke_phase5.pytests/live_client.py is the exception: it dials a real ws:// and expects the server to already be running.
Adapter choice and fusion knobs are environment variables read in backend/config.py. Defaults are the demo contract.
| Variable | Default | Role |
|---|---|---|
ACOUSTIC_ADAPTER |
simulated |
simulated today. rpi is the reserved name for a future mic adapter. |
VISION_ADAPTER |
simulated |
simulated today. hosted_cv is reserved. |
CV_MOCK_MODE |
scripted |
scripted follows the scenario vision plan. random fabricates a classification. |
VISION_LATENCY_S |
0.8 |
Artificial inference delay before a scripted vision event. |
VISION_TIMEOUT_S |
3.0 |
After this, fusion emits an acoustic-only assessment. |
ACOUSTIC_CONF_THRESHOLD |
0.55 |
Minimum drone-like confidence that counts toward the gate. |
ACOUSTIC_GATE_FRAMES |
2 |
Consecutive hot frames required before a vision request. |
ACOUSTIC_TRIGGER_COOLDOWN_S |
6.0 |
Minimum gap between vision requests. |
FUSION_WEIGHT_ACOUSTIC |
0.35 |
Acoustic term in the weighted sum. |
FUSION_WEIGHT_VISION |
0.65 |
Vision term in the weighted sum. |
ACOUSTIC_ONLY_CONF_CAP |
0.55 |
Ceiling when vision times out. |
EVENT_BUFFER_SIZE |
20 |
Replay buffer length for reconnect. |
HOST / PORT |
0.0.0.0 / 8000 |
Documented defaults. The uvicorn command above sets the bind. |
LIVE_CV_INGEST_KEY |
empty | If set, POST /api/vision/detection requires header x-api-key. |
LIVE_CV_ESCALATION_CONF |
0.70 |
Live drone confidence required to dispatch. |
LIVE_CV_DEFAULT_RANGE_M |
260 |
Range stamped on a live-vision track. |
SOLDIER_LOCATION |
Soldier grid BR-042 |
Location string on the dispatch card. |
COUNTER_UAS_EFFECTOR |
C-UAS response team |
Effector name on the dispatch card. |
COUNTER_UAS_RESOURCES |
RF scanner,jammer kit,spotter team |
Comma-separated kit list. |
COUNTER_UAS_ETA_S |
45 |
ETA shown to the operator. |
The phone only needs one variable:
| Variable | Default | Role |
|---|---|---|
EXPO_PUBLIC_SERVER_HOST |
localhost |
Host the app uses for http:// and ws:// on port 8000. Set it before expo start / npm run ios. |
| Method | Path | Purpose |
|---|---|---|
| GET | /health |
Liveness, client count, active scenario |
| GET | /state |
Current snapshot |
| GET | /scenarios |
demo-alpha, demo-bravo, names and descriptions |
| POST | /scenario/{id}/start |
Reset pipeline state and play that scenario |
| POST | /scenario/stop |
Stop the active timeline |
| POST | /scenario/reset |
Stop and clear threat, orb, and track |
| POST | /operator/action |
confirm, escalate, stand_down, or acknowledge |
| GET | /audit |
Operator action log |
| POST | /api/vision/detection |
Live overlay ingest |
| WS | /ws |
Snapshot on connect, then live events, replay on reconnect |
Operator action body:
{
"type": "operator_action",
"action": "confirm",
"assessment_id": "<id from the fused assessment>",
"operator_id": "op-1"
}Threat bands, from threat_band() in backend/core/fusion.py:
| Fused confidence | Level | Recommendation |
|---|---|---|
≥ 0.85 |
critical | Prepare countermeasures. Confirmation required. |
≥ 0.70 |
high | Alert squad. Confirmation required. |
≥ 0.50 |
medium | Request visual confirmation. Confirmation required. |
below 0.50 |
low | Monitor. Confirmation not required. |
A live drone above the escalation threshold uses a fifth recommendation, dispatch_counter_uas, still with confirmation required.
Files in backend/sim/scenarios/ are the demo script. Each acoustic entry is a time, a label, an intensity, and a drone_like flag. The vision block is a sequence of classifications the adapter returns, in order, when the acoustic gate fires.
| Id | File | Intent |
|---|---|---|
demo-alpha |
demo_alpha.json |
Ambient, then a sustained drone-like signature. Vision returns drone at 0.91. Track plan: bearing 312°, range 480 m, closing 14 m/s. |
demo-bravo |
demo_bravo.json |
A short drone-like burst. Vision returns bird at 0.74. No track plan. Fusion downweights the bird and stays on monitor. |
Add a scenario by dropping another JSON file with the same shape and an id. ScenarioEngine loads every *.json in that directory at startup. Start it with POST /scenario/<id>/start.
npm run ios is the supported surface. The acoustic orb is a Skia runtime shader (src/components/AcousticOrb.tsx); the browser bundle does not provide Skia.RuntimeEffect, so npm run web is not a working demo path today.
| Region | What it shows |
|---|---|
| Status pills | Live / Offline from the socket, and Nominal / Detection from orb mode |
| Acoustic orb | White while listening. Red from the first credible acoustic warning |
| Threat assessment | Fused, acoustic, and vision meters, bearing, source mode, recommendation text |
| Actions | Confirm (when confirmation is required), Escalate, Stand down |
| Tracking | Bearing and closing range after a confirmed track opens |
| Reasoning trail | Server stages, from listening through assessment or live dispatch |
| Subsystem chips | Acoustic, vision, fusion, tracking — idle, active, online, or degraded |
npm run android is wired to expo run:android for a dev build on Android. The hackathon demo target was the iOS simulator.
Use this when the scripted camera is not enough and a real POV is on screen (for example a Ray-Ban feed inside a desktop call window).
cd vision
.venv/bin/python live_overlay.pyDrag a box around the video, then press Enter. While the drone model reports drone above the backend's escalation confidence, the overlay POSTs to /api/vision/detection. The app moves to critical, completes the reasoning trace, and shows the staged counter-UAS package. A second post does not double-dispatch.
| Flag | Default | Role |
|---|---|---|
--source |
screen |
screen, or a path to a video file that loops |
--monitor |
1 |
Capture monitor index |
--full |
off | Skip the region picker and capture the whole monitor |
--region |
unset | left,top,width,height in place of the picker |
--conf |
0.35 |
COCO confidence threshold |
--drone-conf |
0.30 |
Drone-model confidence threshold |
--imgsz |
640 |
Inference size |
--max-width |
1280 |
Downscale wider frames before inference |
--no-drone |
off | COCO model only |
--backend-url |
http://localhost:8000 |
Gateway base URL |
--api-key |
LIVE_CV_INGEST_KEY |
Sent as x-api-key when the gateway expects one |
--post-interval |
2.0 |
Minimum seconds between posts |
--no-backend |
off | Draw boxes only |
Keys in the viewer: q or ESC quit, r re-select the region, space pause, s save a snapshot.
COCO classes treated as aerial are airplane, bird, and kite. The drone model is the class that escalates the gateway. Other classes are drawn and do not dispatch.
- Thresholds and weights. Export the variables in Configuration before starting uvicorn. The smoke tests assume the defaults; change them and the asserted bands can move.
- A new engagement. Add
backend/sim/scenarios/<name>.jsonwithid,acoustic, andvision. Give a confirmed drone atrackobject if the compass should open. Restart the gateway so the directory is re-read. - Recommendation copy. Edit
recommend()andrecommend_dispatch()inbackend/core/fusion.py. The app labels knownactionstrings insrc/components/ThreatPanel.tsx. - Reasoning trail text. Edit
backend/core/reasoning.py. The app renders whatever stage payload the server sends. - A real microphone or camera. Implement
AcousticAdapter.generateorVisionAdapter.analyzeand return the existingAcousticEvent/VisionEventmodels. Select the adapter fromconfig.py. The gateway and the phone stay on the same events. - Look of the orb and panels.
src/theme.tsholds the dark palette.src/components/AcousticOrb.tsxholds the shader.
- Health check hits the wrong service. Port
8000is a common default. IfGET /healthis not{"status":"ok","clients":...}, another process owns the port. Stop that process or move Sentry-CD and point the app at the same port insrc/config.ts. - The app stays Offline. The gateway must be up before the socket connects. On a phone,
localhostis the phone. SetEXPO_PUBLIC_SERVER_HOSTto the computer's LAN IP and restart Metro so the value is inlined. npm run webcrashes inAcousticOrb. The orb shader callsSkia.RuntimeEffect, which the web bundle does not provide. Usenpm run ios.- Vision never confirms. The acoustic gate needs two consecutive drone-like frames at
≥ 0.55.demo-alphacrosses that just aftert = 4.8seconds, then waitsVISION_LATENCY_Sfor the classification.demo-bravois supposed to come back as a bird and stay low. - Live overlay posts are ignored. The class must be
droneand confidence must be at leastLIVE_CV_ESCALATION_CONF(0.70). IfLIVE_CV_INGEST_KEYis set on the gateway, pass the same value as--api-key. A second drone post after dispatch returnsalready_dispatched. - Smoke test hangs or fails mid-scenario. The tests sleep along the scenario clock (several seconds). Run them from
backend/somain.pyandtests/resolve. They do not need a separate uvicorn process, exceptlive_client.py.
