Introduction
This document describes a buildable, weekend-scope hardware project: a passive bistatic radar that detects small consumer drones by listening for FM radio reflections off them, computed in real time on an NVIDIA Jetson Orin Nano. Two identical sensor nodes, deployed ~50 m apart on tripods, share GPS-disciplined time and produce confirmed detections of any drone-sized object flying within ~1 km. A tablet shows live results.
What the finished v1 system does
- Detects drone-class targets at 500–1500 m range using only ambient FM broadcast radio as the illuminator. No active emissions. Covert.
- Updates the operator's tablet at 10 Hz with live range, Doppler velocity, and a per-detection label (drone / bird / helicopter / other) produced by a hosted vision-language model — no training, no on-device classifier.
- Runs fully on battery for 3+ hours per site. Two sites form a self-organizing mesh with no infrastructure required.
- Costs ~$2,800 total for the full two-site system, or roughly $1,000 per node, plus a ~$300 test drone.
- Builds in ~8 hours of focused integration after parts arrive, plus a few weekends for the radar pipeline software.
Why this is interesting
Drone detection is mostly sold as $50K–$200K active radar boxes. This system gets to ~80% of that capability for ~$3K, by exploiting two trends that became real in the last 24 months:
- The Jetson Orin Nano Super dropped to $249. A 67-TOPS CUDA GPU at the price of a high-end Raspberry Pi. cuFFT runs cross-ambiguity at 10 Hz in real time — fast enough that the algorithms that used to need a PCIe rack now fit in a power bank.
- The KrakenSDR exists. A coherent 5-channel software radio for $400. Used to be $5K of custom phase-locked SDRs. Plug-and-play over USB.
A passive bistatic radar built on these two parts is roughly the same idea as the systems Lockheed and Hensoldt sell to NATO air-defence networks for Patriot integration — same physics, same algorithms, three orders of magnitude cheaper. The trade-off is performance: shorter range, no real beamforming, no fancy clutter rejection. But for the small-drone problem in cluttered urban airspace, that's enough.
Who this guide is for
- A motivated student or builder who can read a datasheet, solder a header, and follow shell commands.
- Someone working with an AI coding assistant (Claude, Cascade, Cursor, etc.) to write the radar pipeline. The guide assumes you have an LLM partner; the Playbook tab is structured as atomic tasks the LLM can help you execute one by one.
- Anyone with a few weekends and ~$3K who wants to actually run a radar, not just read about one.
How to read this document
- Introduction (you are here). What and why.
- Architecture. How passive radar works. The big-picture data flow. The supervisory-control pattern for human oversight.
- Sensor Node. The Jetson-based hardware design — block diagram, enclosure, power, antennas.
- Bill of Materials. Every part, every link, every cost. Click to buy.
- Operator Interface. The tablet, the mesh, the supervisory-control UI. How a human watches and authorizes.
- Chaser (v2). Where this is heading. Future scope, not part of v1.
- Playbook. ~80 atomic build tasks, ordered by phase, with dependencies, success criteria, and time estimates. The thing you actually work through.
Tip: read Architecture once, skim Sensor Node and BOM to understand what you're building, skim Operator Interface and Chaser to see where it's going, then live in Playbook.
Architecture
Four concepts to understand before touching any hardware: passive bistatic radar (how detection works without an emitter), GPS-disciplined timing (how two nodes 50 m apart can act as one coherent radar), foundation-model classification (how we skip training entirely and just ask a hosted VLM), and supervisory control (how a human stays in the loop without piloting anything).
1. Passive bistatic radar — detection without emission
Conventional radar emits a pulse and listens for the echo. Passive radar doesn't emit anything. It listens for energy that's already in the air — usually FM broadcast or DVB-T television — and looks for the reflections off targets. The illuminator is somebody else's transmitter; you only own the receivers.
For each target the system measures two things:
- Bistatic range — the extra distance the signal travels by bouncing off the target, vs the direct path from transmitter to receiver. Computed from the time delay between the two paths.
- Bistatic Doppler — the velocity component of the target along the bistatic geometry. Computed from the frequency shift in the reflected signal.
One receiver gives you a bistatic ellipse the target lies on (constant total-path distance). Two receivers give you the intersection of two ellipses → a localized target. With two sites and an FM transmitter ~5 km away, you get drone-class detection out to ~1 km in clear conditions.
The math behind this is the cross-ambiguity function: a 2D correlation of the reference and surveillance channels over a grid of (delay, Doppler shift) hypotheses. Where the function peaks, there's a target. The peak's coordinates give you bistatic range and Doppler. This is what runs on the GPU at 10 Hz.
2. GPS-disciplined timing — making two boxes act as one radar
For two sites 50 m apart to combine their views into a single picture, they need to share a time reference. If site B's clock is off by 1 µs from site A's, the apparent target position is wrong by 300 m (1 µs × c). For useful localization you want sub-100 ns timing error.
The cheap, robust solution: each Jetson has a GPS receiver with a Pulse-Per-Second (PPS) output. Both Jetsons discipline their system clocks to GPS time using chrony. Each site independently times its IQ samples against its own GPS-synced clock. They are now coherent in time to within ~30 ns — better than required.
This replaces a much more expensive setup involving GPSDOs, 10 MHz reference distribution, and PPS cabling. We get the same time alignment by burning a few hundred lines of Python on cross-correlating the direct path between sites at startup, then maintaining via GPS-PPS continuously.
3. Foundation-model classification — skip training, just ask a VLM
Once the radar peaks at a (range, Doppler) cell, you still need to label it: drone, bird, helicopter, car, noise. The traditional move is to train a custom classifier — collect a labeled dataset, fine-tune a CNN on micro-Doppler signatures, deploy as a TensorRT engine. That's months of work and ongoing maintenance.
The shortcut: render each detection as an image and ask a hosted vision-language model to label it. Range-Doppler maps, micro-Doppler spectrograms, and (in v2) actual camera frames are all just images. Claude, GPT-4o, Gemini, and DeepSeek-VL all classify them with zero training data and roughly human-expert accuracy.
| Approach | Setup cost | Per-detection latency | Per-detection cost | Accuracy |
|---|---|---|---|---|
| Custom-trained CNN (TensorRT, on-device) | Weeks of dataset + training | ~1 ms | $0 (free after training) | High once tuned, brittle out of distribution |
| Hosted VLM (Claude / GPT-4o / DeepSeek-VL) | ~10 lines of code | 200 ms – 2 s | $0.001 – $0.01 per image | Surprisingly high zero-shot, robust to weird cases |
| Hybrid (cheap on-device gate → VLM for uncertain) | Days | 1 ms typical, 1 s when escalated | ~$0.001 per uncertain detection | Best of both |
The classifier doesn't run on the sensor node at all. Each Jetson detects and computes a small image (a 128×128 range-Doppler patch, or a 1-second micro-Doppler spectrogram), packages it with a detection JSON, and ships both to the operator tablet over the mesh. The tablet — or the operator's laptop, or a cloud function — calls the VLM API and returns a label. Total round-trip ≈ 500 ms, which is well under the human decision time anyway.
Why this works in practice:
- The sensor's job is detection, not classification. CFAR + cross-ambiguity reliably tells you something is there. Naming it is a separate, slower step that benefits from a much bigger brain than fits on the Jetson.
- VLMs are good at "what is this image of?". Even when they've never seen a Doppler spectrogram before, a prompt like "this is a micro-Doppler spectrogram of a flying object — is it most likely a drone, bird, helicopter, or other? answer with one word and a confidence 0–1" gets useful answers.
- The model doesn't need to be on the drone. The chaser sends a JPEG over Wi-Fi 6E to the base station; the base station calls Claude / DeepSeek-VL; the answer comes back in <1 s. The chaser saves weight, power, and complexity by carrying just a camera and an encoder.
- Cost is negligible. A two-hour mission with one detection every 10 s is 720 API calls. At Claude Haiku rates that's ~$0.50.
- You can swap models at any time. Today's best VLM gets replaced by next quarter's. No retraining, just change the API endpoint.
4. Supervisory control — humans in the loop without flying anything
Once you have detections, what does a human do with them? There are three operating modes for any drone-detection-and-response system, and only one of them is sane.
| Mode | Operator role | Verdict |
|---|---|---|
| Fully autonomous | None — system detects, classifies, intercepts | Fast, scales — but legally and ethically untenable for kinetic action. Fails on edge cases (news helicopters, neighbour's RC plane). |
| Manually piloted | Operator flies the chaser with sticks and FPV goggles | One operator per drone, no scaling, can't out-fly autonomy at 30 m/s. Wrong abstraction. |
| Supervisory control | Operator watches video + telemetry, authorizes high-level decisions: investigate, abort, RTB, terminal | The pattern Anduril Lattice and Shield AI Hivemind use. Humans where humans are good (judgment), autonomy where autonomy is good (closed-loop control). |
The architecture treats the operator as the slow, smart, legal-authority component — not as a pilot. The chaser flies itself; the operator says yes or no. Latency budget is hundreds of milliseconds (operator decision time), which Wi-Fi handles trivially. We never need a sub-50 ms FPV link.
System block diagram — v1 (sensor only) → v2 (with chasers)
Why this design holds up
- Both nodes are identical. Same hardware, same software image, same OS. Configuration is one file (
/etc/nomio/node.conf) that sets the unique node ID. Spares are interchangeable. - Edge processing. Each node runs the full DSP pipeline locally on its GPU. Only KB/s of detections cross the mesh — never raw IQ (which would be 16 MB/s and saturate Wi-Fi).
- No infrastructure. No servers, no cloud, no fixed network. Two batteries, two tripods, one tablet, drop-in deploy.
- Failure modes are graceful. If one node dies, the other still produces single-site bistatic detections (just no localization). If GPS dies, you can fall back to direct-path cross-correlation. If the tablet dies, the OLED + LEDs on each box still tell you what's happening.
- Upgrade path is linear. v1 ships with a hosted-VLM classifier from day one (no training step) → add 3rd/4th sites for TDOA 3D localization (no architecture change) → add chasers (v2).
Sensor Node
Each site is one self-contained box. Inside: a Jetson Orin Nano running the radar pipeline on its CUDA GPU, a KrakenSDR doing the coherent multi-channel RF capture, a GPS HAT for time discipline, an OLED + LEDs for status, and a power bank running the whole thing. The box sits on a tripod with two log-periodic antennas mounted alongside.
Per-site block diagram
Power budget
The Jetson is configurable: 7 W, 15 W, or 25 W power modes. We run at 15 W ("MAXN_SUPER" mode at conservative throttle) — full 67 TOPS available, but moderated for battery life.
| Component | Idle | Active (DSP only — VLM lives at base) | Notes |
|---|---|---|---|
| Jetson Orin Nano (15 W mode, headless) | ~5 W | ~13 W | cuFFT cross-ambiguity at 10 Hz + spectrogram render. No on-device classifier. Plenty of headroom. |
| Uputronics GPS HAT | 0.3 W | 0.3 W | Powered from 40-pin header. |
| KrakenSDR | 10 W | 12 W | Dominant load. Powered from its own USB-C PWR port — never from the Jetson. |
| OLED + LEDs | 0.1 W | 0.2 W | Negligible. |
| Site total | ~16 W | ~26 W |
An 87 Wh power bank (Anker 737) gives ~3 hours of continuous operation. Add a 40 W folding solar panel ($60) for indefinite daytime use. For static deployment near power, just plug into a wall adapter.
3D-printed enclosure
A two-piece PETG case houses the Jetson + KrakenSDR + power bank in three internal trays. ~12 hour print on a Bambu A1, $8 in filament. External: 220 × 130 × 90 mm.
- Top tray: Jetson Orin Nano dev kit + GPS HAT stacked on the 40-pin header.
- Middle tray: KrakenSDR. Heat conducted to case wall via thermal pad.
- Bottom tray: Anker 737 power bank, slides in/out from rear.
- Front face: 0.96" OLED, 3 status LEDs, 1 push button.
- Rear face: 2× SMA bulkhead (RF in), 1× SMA bulkhead (GPS), 1× USB-C in (charging), 1× IP67 vent membrane.
- Bottom face: 1/4"-20 brass insert for tripod mount.
- Side faces: hex vent grilles aligned with Jetson cooler intake/exhaust.
STL files are produced by an OpenSCAD parametric model (in the project repo, enclosure/nomio_node.scad) where you set compute_module = "jetson_orin_nano", battery, weatherproofing, and mount.
What runs on each Jetson — software stack
Same software image for both nodes. Identity comes from a single config file.
- JetPack 6 (NVIDIA's Ubuntu 22.04 LTS for Jetson) as the base OS, on NVMe.
gpsd+chronyreading NMEA + PPS from the Uputronics HAT. Disciplines the system clock to ~1 µs of GPS time.- KrakenSDR DAQ daemon (official) capturing coherent IQ over USB 3.2.
nomio-radar(the pipeline you'll write — see Playbook): Python + CuPy + cuFFT, ~600 lines, runs on the GPU. Cross-ambiguity at 10 Hz, CFAR detection, spectrogram-patch render. No on-device classifier — the patch ships to the base station which calls a hosted VLM.nomio-status: OLED + LED + button daemon (~100 lines Python).nomio-mesh:batman-advmesh interface, mDNS via Avahi, signed message bus.nomio-web: FastAPI + WebSocket server for the operator tablet web UI.
All seven services managed by systemd, starting in dependency order on boot. Total install footprint ~3 GB on the NVMe.
Why not a Raspberry Pi 5?
Honest comparison. The Pi 5 is a fine general-purpose Linux box, but its compute envelope is a real bottleneck for radar work.
| Capability | Pi 5 (NEON SIMD) | Jetson Orin Nano (CUDA + TensorRT) |
|---|---|---|
| Cross-ambiguity update rate | ~1 Hz | 10–20 Hz |
| Coherent integration time (more SNR = farther detections) | 0.1 s typical | 1.0 s easy |
| Render spectrogram patch for VLM classifier | ~50 ms (CPU, marginal) | ~2 ms (CUDA, easy) |
| Multiple illuminator bands in parallel | 1 | 3–4 |
| Headroom for upgrades (DOA, beamforming) | none | ~50% spare |
| Cost (compute board only) | $80 | $249 |
$170 difference, 40× compute uplift, and the difference between "barely functional demo" and "actually useful detection." If results matter, get the Jetson.
Bill of Materials
Every part you need to order, with linked sources and prices. Two columns: Per site means buy two (one for each node); Shared means one for the whole build. Prices are USD as of late 2025 — sanity-check at order time.
1 · Core compute & RF — buy 2 of each
| Item | Source | Qty | Unit | Notes |
|---|---|---|---|---|
| Jetson Orin Nano 8 GB Super Developer Kit | NVIDIA · Arrow · Seeed · Amazon | 2 | $249 | The "Super" variant only — the 2024 firmware refresh that gives 67 TOPS. Carrier board + module + cooler all in one box. |
| NVMe M.2 2280 SSD, 256 GB | Samsung 980 / WD SN570 / Crucial P3 — Amazon | 2 | $30 | Jetson boots from NVMe (no SD card). Required for IQ logging. |
| Intel AX210 Wi-Fi 6E + BT 5.3 M.2 2230 | Amazon | 2 | $25 | Dev kit doesn't include Wi-Fi. Goes into the carrier board's M.2 Key E slot. Includes BT 5.3 for tablet pairing. |
| Wi-Fi antenna kit (2× MHF4 → RP-SMA pigtail + 2× dipole) | Amazon | 2 sets | $10 | The AX210 needs external antennas. Route them through small holes in the case. |
| Uputronics GPS / RTC HAT (with active GPS antenna) | Pi Hut (canonical) | 2 | $50 | Pi-compatible 40-pin HAT. Works on Jetson with jetson-gpio. PPS via GPIO 18, NMEA via UART. Includes magnetic-base GPS antenna. |
| KrakenSDR (unit only — no antennas) | Crowd Supply (canonical) · KrakenRF direct | 2 | $400 | 5-channel coherent SDR. We use only 2 channels (ref + surv) for v1. Other 3 are upgrade path for DOA. |
| Subtotal core compute & RF | $764 ×2 | $1,528 | ||
2 · Antennas — buy 4 total (2 per site)
| Item | Source | Qty | Unit | Notes |
|---|---|---|---|---|
| Log-periodic antenna, 400 MHz – 1 GHz (LP0410 / WA5VJB-style) | Kent Electronics (canonical) · AliExpress clones | 4 | $50 (Kent) / $20 (clone) | Directional, ~7 dBi, narrow lobe — important for separating direct and reflected paths. Two per site (ref + surv). Clone is fine for v1; Kent is better-matched. |
| SMA-to-SMA coax cable, 1 m, low-loss | Amazon (RG316 or similar) | 4 | $10 | Antenna → bulkhead. RG316 is fine at FM band over 1 m. |
| Subtotal antennas | $240 | |||
3 · Power — per site
| Item | Source | Qty | Unit | Notes |
|---|---|---|---|---|
| Anker 737 Power Bank (PowerCore 24K, 87 Wh, 140 W PD) | Anker direct · Amazon | 2 | $130 | Two USB-C output ports — one for Jetson, one for KrakenSDR. ~3 hr runtime. Airline-legal. |
| USB-C to USB-C, 100 W PD, 1 m (per site, 2 cables) | Amazon | 4 | $10 | One per powered device per site. |
| Optional: 40 W folding solar panel (USB-C PD output) | Amazon (BigBlue, Anker, Jackery, etc.) | 2 | $60 | Sustains daytime operation indefinitely. Skip for indoor or grid-tied use. |
| Subtotal power (without solar) | $300 | |||
4 · Status display + I/O — per site
| Item | Source | Qty | Unit | Notes |
|---|---|---|---|---|
| 0.96" OLED display, SSD1306, I2C, 128×64 | Amazon | 2 | $5 | 4-wire I2C: SDA, SCL, VCC (3.3V), GND. Drives via luma.oled Python. |
| 5 mm LED kit (assorted colours) + resistors + push button | Amazon | 1 kit | $10 | Use 1× green, 1× blue, 1× red, 1× yellow per site + one momentary button. 330 Ω current-limit on each LED. |
| Hookup wire kit (22 AWG, multiple colours) | Amazon | 1 kit | $10 | Shared. For OLED + LEDs + button + GPS-PPS jumper. |
| Optional: Adafruit RFM95W LoRa Bonnet (radio HAT) | Adafruit | 2 | $23 | Long-range mesh (2–5 km). Skip if both sites have line-of-sight Wi-Fi. |
| Subtotal status + I/O (without LoRa) | $30 | |||
5 · Mechanical — per site
| Item | Source | Qty | Unit | Notes |
|---|---|---|---|---|
| Camera tripod, ~1.5 m max height, with 1/4"-20 thread | Amazon | 2 | $30 | The case mounts on this. Antennas mount on a separate bar above. |
| Antenna mast (wood dowel or PVC pipe + clamps) | hardware store / Amazon | 2 | $15 | Holds the two log-periodic antennas ~30 cm apart. |
| 3D-printed enclosure (PETG, ~280 g/site) | print at home, or Craftcloud / JLCPCB / PCBWay | 2 | $8 (DIY) / $40 (outsourced) | STL files in the project repo. ~12 hr per case on a Bambu A1. |
| M3 brass heat-set inserts + M3 bolts (assortment kit) | Amazon | 1 kit | $15 | Shared. ~30 inserts + matching bolts. Press-fit with a soldering iron. |
| 1/4"-20 brass insert + IP67 GORE vent membrane | Amazon (insert) · Amazon (vent) | 2 | $8 | Tripod mount + humidity equalization for outdoor use. |
| SMA bulkhead pigtail set (5 cm SMA-F to SMA-M, right-angle) | Amazon | 2 sets | $15 | For panel-mount RF pass-through on the rear of the case. |
| Subtotal mechanical | ~$200 | |||
6 · Test target & operator hardware (shared)
| Item | Source | Qty | Unit | Notes |
|---|---|---|---|---|
| DJI Mini 3 (or Mini 4 Pro) — your test drone | DJI direct · Amazon | 1 | $300–500 | Any sub-250 g drone works. Realistic threat to test against. |
| Android tablet (8–11", any modern model) or iPad | any retailer | 1 | $200–600 | Operator console. Web-based UI; no native app required. |
| Laptop you already own | — | 1 | $0 | Used for first-flash and SSH. Doesn't go to the field. |
| Subtotal test/operator (low/high) | $500–1,100 | |||
Grand total
| Block | Cost |
|---|---|
| Core compute & RF (×2 sites) | $1,528 |
| Antennas (×4) | $240 |
| Power (×2 sites, no solar) | $300 |
| Status + I/O (×2 sites, no LoRa) | $30 |
| Mechanical (×2 sites) | $200 |
| Test drone (shared) | $300–500 |
| Tablet (shared) | $200–600 |
| Total — minimum sane build | ~$2,800 |
| Total — fully optioned (LoRa + solar + premium tablet) | ~$3,250 |
For comparison: the equivalent commercial system from a defence vendor (DroneShield RfPatrol, Hensoldt Spexer 360r) is $50K–$200K. This is the same physics for ~$3K. The catch is you have to build it yourself, and the performance tail is shorter. For learning, prototyping, or low-stakes field work, that's a great trade.
What you don't need (and might be tempted to buy)
- A separate GPSDO (Bodnar Mini, etc., ~$300/site). Not needed in v1. The GPS HAT's PPS into
chronygets you to ~1 µs, which is good enough. Add a GPSDO only if you later want true coherent multistatic with sub-100 ns timing. - Active cooling for the KrakenSDR. Already built in.
- A discrete USB-3 hub. The Jetson dev kit has 4× USB 3.2 ports — plenty.
- Pelican case. The 3D-printed case is fine for indoor or fair-weather field use. Drop in a Pelican only if you're going to deploy in rain/dust regularly ($80/case extra if you do).
- Big batteries (Jackery 240, 1000, etc.). Tempting for runtime, but adds 3–5 kg per site. The Anker 737 is the right size for 3-hour deployments.
Operator Interface
An Android tablet (or iPad) is the only thing the operator needs in the field. It runs a web app served from one of the sensor nodes, talks to the mesh over Wi-Fi, and pairs over Bluetooth on first use. No native app, no laptop, no flight sticks.
Pairing — once, in 5 seconds
- Boot a sensor node. Press the front-panel button. The OLED shows a 6-digit fingerprint of the node's network key.
- On the tablet, open the Nomio web app (just
http://nomio-a1.localwhen on the same Wi-Fi). Tap "Pair new node". Bluetooth scan finds the node. - Compare fingerprints. Tap "Confirm". Bluetooth exchanges the mesh key. Done.
- From now on, the tablet auto-joins the mesh whenever any sensor node is in range. Same model as Apple's AirTag/HomePod pairing.
What the tablet shows — v1 (sensor only)
┌─────────────────────────────────────────────────────────────┐ │ Nomio Operator Console mesh: ✓ GPS: ✓ power: 82% │ ├──────────────────────┬──────────────────────────────────────┤ │ MAP │ RANGE × DOPPLER (live, 10 Hz) │ │ ───── │ ┌────────────────────────────────┐ │ │ • Site A GPS ✓ │ │ ▒▒▒░░░ ▓ │ │ │ • Site B GPS ✓ │ │ ▒░░ ▓░ │ │ │ ✕ T-7 drone │ │ ░░ ▓ │ │ │ ✕ T-8 bird │ │ ░░ ░ │ │ │ ✕ T-9 car │ │ T-7 ◯ │ │ │ │ └────────────────────────────────┘ │ │ bistatic ellipses │ T-7 range 380m · -8.2 m/s · p=0.94 │ │ for each track │ │ ├──────────────────────┼──────────────────────────────────────┤ │ DETECTION QUEUE │ EVENT LOG │ │ T-7 drone p=0.94 │ 19:42:11 T-7 first seen, 580m bistat │ │ T-8 bird p=0.71 │ 19:42:14 T-7 classified drone p=0.94 │ │ T-9 car p=0.99 │ 19:42:30 T-9 confirmed across 2 nodes │ └──────────────────────┴──────────────────────────────────────┘
Three regions: map (left, ground-truth situational picture from the sensor mesh), range-Doppler heatmap + selected track (right, the actual radar output for the currently-selected detection), queue + event log (bottom, a chronological list of what's been seen).
What the tablet shows — v2 (with chasers)
When chasers are added in v2, the right-hand region splits to show live H.265 video from whichever chaser is currently engaged, with a command bar below for supervisory control. See the Chaser tab for details.
Channel comparison — why Bluetooth pairs and Wi-Fi streams
Several builders' instinct is to use Bluetooth for the data plane. It's the wrong fit. Here's the comparison.
| Channel | Throughput | Latency | Range | Right job for |
|---|---|---|---|---|
| Bluetooth 5.3 (LE 2M PHY) | ~700 kbps real | 20–40 ms | 10–30 m | Pairing, telemetry, audio. Not video. |
| Wi-Fi 6E (AX210, 5/6 GHz) | 500–1200 Mbps | 2–8 ms | 30–100 m direct, more on mesh | Video, control, sensor data — everything. |
| 5G / LTE (USB modem) | 50–500 Mbps | 20–80 ms | kilometres | Long-range backhaul (v2/v3). |
| Proprietary FPV (DJI O3, Herelink) | ~25 Mbps video + 4 Mbps control | 30–100 ms | 5–10 km | Manual override at distance. Out of scope. |
The AX210 module in the BOM already includes Bluetooth 5.3 as a side effect — it's a combo Wi-Fi+BT chip. So BT is on the unit anyway, doing what it's good at: short-range pairing.
Web UI implementation
- Single-page React app, built with Vite, served as static files from FastAPI on the sensor node.
- Detection stream over WebSocket from
/ws/detections. JSON messages, ~10 per second per active track. - Range-Doppler heatmap rendered to
<canvas>via WebGL. The heatmap is downsampled on the node side to ~256×256 px to keep the wire payload small. - Map uses MapLibre GL with offline tile bundles cached on the tablet.
- BT pairing uses the Web Bluetooth API in Chrome on Android, or a native helper app on iOS (Web Bluetooth isn't supported in iOS Safari; that's a known limitation).
Without a tablet — fall-back operator UX
- OLED on each node shows live status (GPS, SDR, peer count, dets/min, battery). Enough for "is it working" without any other device.
- LEDs on the front panel: green = healthy, blue blinking = waiting for GPS, blue solid = locked, red flash = active detection, yellow = low battery.
- SSH from any laptop on the same Wi-Fi:
ssh nomio@nomio-a1.localgets you a console with livetail -fon the detection log.
Chaser — v2 forward architecture
What a chaser is — dumb airframe, smart base station
The chaser is deliberately stupid. It's a small quadcopter (~250–500 g) that does only three things: fly where it's told, stream H.265 video over Wi-Fi, and keep itself from crashing. All AI lives at the base station on a Jetson sitting next to the operator's tablet. Classification, target tracking, intercept geometry, abort decisions — all happen at the base. The chaser is just a flying camera with a flight controller.
This inverts the usual industry assumption (which is that the chaser carries its own AI compute). It works because we have Wi-Fi 6E and a hosted VLM. The chaser doesn't need a Jetson, doesn't need a YOLO model, doesn't need a trained dataset. It needs a flight controller and a video link.
Chaser hardware (per chaser) — radically simplified
| Item | ~Cost | Notes |
|---|---|---|
| Frame: 5–7" carbon-fibre quad (e.g., iFlight, GEPRC) | $120 | Off-the-shelf FPV-style frame. |
| Flight controller (Pixhawk 6C or Holybro Kakute H7) | $80–200 | Runs PX4 or ArduPilot. Receives MAVLink waypoints over Wi-Fi from the base station. That's its only "intelligence". |
| Motors + ESCs + props | $200 | Standard FPV components. |
| Battery (4S 4Ah LiPo) | $60 | ~18 min flight time (no Jetson power draw). |
| Camera + H.265 encoder module (Runcam Hybrid 2 or DJI O3 Air Unit) | $150–230 | Self-contained 4K camera with built-in hardware H.265 encoder + Wi-Fi or analog out. No companion computer needed. |
| Wi-Fi 6E module (USB or onboard) | $25 | Joins the same mesh as the sensors. Carries MAVLink in, video out. |
| Antennas, cabling, mounting | $50 | — |
| Removed. The sensor-mesh Jetson does all the inference. The chaser doesn't need one. | ||
| Total per chaser | ~$700–900 | Down from $1,000–1,300. Build 2–4 of these. |
Industry chasers typically include a $500–1,500 companion computer (Jetson, Khadas, RB5) running on-board YOLO. By moving the brain to the base station and using a hosted VLM, the chaser collapses to "FPV drone with a video link" — a $700 part you can build from a kit.
Where the smarts actually live — the base station
The base station is just an extra Jetson Orin Nano (the same module as the sensor nodes) sitting on the operator's table next to the tablet. It runs:
- Mission executor. Takes high-level operator commands (INVESTIGATE, RTB, ABORT, HOLD) and translates them into MAVLink waypoints, which it sends to the chaser over Wi-Fi. Trivial state machine, ~500 lines.
- Hybrid visual tracker (described in detail below). A fast local tracker keeps a bounding box on the drone every frame; a hosted VLM verifies identity periodically. ~600 lines of Python.
- Intercept-geometry planner. Given target velocity (from radar) and chaser state, computes a safe approach trajectory. Modified pursuit-curve logic. ~1K lines.
- Safety layer. Geofencing, battery-reserve return, lost-link return, radar-vision cross-check abort. The legally-mandatory part for civilian use. ~1K lines.
- Operator UI bridge. Same tablet as v1. Adds video feed + supervisory buttons. ~400 lines on top of the v1 web app.
- Terminal action (if applicable). Net-launcher trigger, beacon-emit-and-track, escort-and-land — depending on what the chaser is for. Always human-authorized via the supervisory pattern. ~200 lines plus the hardware.
Total base-station code: ~3K lines, all on one Jetson, all easy to iterate. When you want to upgrade the tracker, you edit one Python file. You don't re-flash any drones.
Hybrid visual tracker — fast local loop, slow semantic loop
The naïve "send every frame to Claude" pattern doesn't work for live pursuit: a hosted VLM round-trip is 200 ms – 2 s, and a drone moving at 10 m/s covers 10 m in that second. We solve it the way classical robotics does — two loops at different speeds:
| Loop | Rate | Job | Implementation |
|---|---|---|---|
| Fast loop (tracker) | 30 Hz (~33 ms) | Keep a bounding box locked on the drone every frame. Output: pixel coords + size. | Classical CV (CSRT or MOSSE correlation tracker, OpenCV) — works because the background is uniform sky. Or YOLO-World with prompt "small flying drone" — open-vocabulary, no training, ~50 ms on the Orin. |
| Slow loop (semantic verifier) | 0.5–2 Hz (every 0.5–2 s) | "Is this still our drone, or did we drift onto a bird / helicopter / a second drone? Is the bounding box still on the right object?" Output: bool + corrected box if needed. | Hosted VLM (Claude Haiku / GPT-4o / Gemini Flash), or a tiny local VLM (Moondream 2, ~150 ms on Orin) for air-gapped operation. |
| Very slow loop (operator) | ~0.1 Hz (every 10 s) | Final judgement: continue, abort, switch target. | Human eyes on the tablet. |
The fast loop drives the steering. The slow loop catches the failure modes the fast loop is blind to: target swap, occlusion recovery, "wait that's actually a kite". When the slow loop disagrees with the fast loop, the fast loop gets corrected (or the system pauses and asks the operator).
Tracker options — pick by what's available
| Tracker | Per-frame cost | Setup | When to use |
|---|---|---|---|
| OpenCV CSRT | ~5 ms (CPU) | One line of Python, no training, no GPU | Default for v2. Sky background = trivially robust. Bootstrap with the VLM's first bounding box, then track frame-to-frame. |
| YOLO-World (open-vocab, prompt: "drone, quadcopter") | ~50 ms (CUDA on Orin) | No training; just download the weights | If you want a real detector that can also re-detect after losing lock. No labelled dataset needed. |
| Moondream 2 (small VLM, local) | ~150 ms (Orin) | Pip install | Best of both: VLM-quality reasoning at near-real-time. Drop-in replacement for the slow loop when offline. |
| Claude Haiku / Gemini Flash (hosted VLM) | 200–700 ms | API key | Best quality. Use for periodic verification, occlusion recovery, identity checks. |
| YOLOv8n trained on drones | ~5 ms (CUDA) | ~1 day to label + train + deploy | Only if you've already collected the data. Skip for v2; revisit in v3. |
The recommended default for v2: CSRT (fast loop) + Claude Haiku (slow loop, every 1 s). Total tracker code: ~150 lines. No training. Cents per mission.
End-to-end latency budget — chaser pursuit
| Loop | Step | Time | Notes |
|---|---|---|---|
| Fast loop — runs every frame, drives steering | |||
| Fast | Camera capture → H.265 encode | ~30 ms | Hardware encoder on the camera module. |
| Fast | Wi-Fi 6E uplink (chaser → base) | ~5 ms | Single hop, line-of-sight. |
| Fast | NVDEC + frame extract at base | ~5 ms | Hardware decoder on the base Jetson. |
| Fast | CSRT correlation update | ~5 ms | OpenCV, CPU. |
| Fast | Steering correction → MAVLink → chaser | ~10 ms | Wi-Fi back down. |
| Fast loop total | ~55 ms (≈18 Hz steering) | Plenty for tracking a 10 m/s drone with metre-class precision. | |
| Slow loop — runs every ~1 s, verifies identity | |||
| Slow | Pick representative frame, send to VLM | ~10 ms | Just the network upload — frame is already in memory. |
| Slow | VLM API call (Claude Haiku / Gemini Flash) | 200–700 ms | Or 150 ms with local Moondream 2. |
| Slow | Update target identity / correct fast tracker | ~5 ms | Adjust CSRT bounding box if the VLM moved it. |
| Slow loop total | ~250 ms – 1 s | Runs in the background; doesn't block the fast loop. | |
The flight controller's innermost loop (attitude stabilization) runs at 200 Hz on the chaser regardless — that's the part that physically can't tolerate latency, and it's already local. The base station only ever issues outer-loop guidance ("steer 5° left", "climb 2 m"). That outer loop at 18 Hz from the fast tracker is more than enough for visual pursuit; it's the same control bandwidth a human FPV pilot has.
How the operator interacts in v2
- Confirmed track from sensor mesh appears on the tablet. Audible alert: target confirmed.
- Operator taps INVESTIGATE. The mesh dispatches the nearest idle chaser. Live H.265 video appears on the tablet within a few seconds.
- Operator can issue HOLD (chaser circles), RTB (return to base), ABORT (immediate safe land), or SWITCH TARGET.
- For systems with a terminal action, AUTHORIZE TERMINAL requires two paired tablets to confirm within a 5-second window — a hard-coded two-person rule. Configurable for non-kinetic systems.
- Chaser executes, returns, lands itself, recharges. Operator never piloted anything.
Why this is the right pattern
Three reasons to lean hard on supervisory control over either extreme:
- Legal. Civilian C-UAS regulations in essentially every jurisdiction require a human in the loop for any kinetic action. Fully autonomous engage is not legal almost anywhere.
- Robust. The autonomy will get edge cases wrong (news helicopter, mistaken classification, GPS-spoof attack). A human at the supervisory layer is the cheapest, most reliable last-line defence.
- Scalable. One operator can supervise 4–8 chasers in parallel. Manual piloting tops out at one operator per drone. Fully autonomous is nominally infinite-scale but isn't trusted enough yet.
Where v2 fits in the timeline
These estimates assume you're working with an AI coding assistant (Claude, Cascade, DeepSeek, etc.) writing 90%+ of the code. The bottlenecks are physical: hardware lead times, antenna placement, field testing windows, regulatory approvals — not lines of code.
- v1 (this guide): sensor mesh + tablet. Two focused weekends of work after parts arrive — one to assemble and bring up, one to tune in the field. Demonstrates detection, classification, multi-site fusion.
- v1.5: add a 3rd and 4th node, swap incoherent fusion for real TDOA 3D localization. One weekend on top of v1: nodes 3 & 4 are clones of node 1's image, and TDOA from already-aligned timestamps is ~200 lines of Python the LLM writes in an hour. The week-ish of elapsed time is parts shipping, not coding.
- v2 (chaser): single chaser drone with autonomy stack and supervisory UI. Realistically 2–4 focused weeks of work, gated by flight-test windows and the iteration loop on the vision tracker. Most of the "12-month" number you see in industry is regulatory paperwork, hiring, and writing code by hand — none of which apply here.
- v3 (multi-chaser, productized): swarm dispatch, formal regulatory approval, hardening for sustained outdoor operation. This is the only stage that is genuinely months of work, and almost none of it is code — it's airworthiness, certification, and operational pilots.
v1 is the only thing in this build guide. v1.5 is a small addendum. v2 is its own project, but not the year-long death-march it would be without an AI pair-programmer.
Playbook
The actual build, broken into atomic tasks (we call them beads). Each bead is small enough to fit in a single focused session — usually 15 min to 2 hr with an AI coding assistant — and has clear, verifiable success criteria. With Claude / Cascade / DeepSeek writing the code, a motivated builder can complete v1 in roughly two focused weekends after parts arrive: one to assemble + bring up the stack, one to tune in the field.
Bead schema
| Field | Meaning |
|---|---|
| ID | Stable identifier like B-040. Use it when chatting with Claude: "Help me with B-040." |
| Title | One-line description. |
| Deps | Beads that must be complete before this one (none = "—"). |
| Success | How you know the bead is done. A specific command output, a measurement, a file existing, etc. |
| Time | Realistic estimate for someone seeing this for the first time, with LLM help. |
Phase totals
| Phase | Beads | Est. time | Output |
|---|---|---|---|
| 0 · Procurement | 3 | 1 wk elapsed (waiting on shipping) | All parts on the workbench. ~2 hr of actual work to place orders. |
| 1 · Enclosure (3D print + post) | 6 | 1 day elapsed (12 hr print) | Two physical cases, ready to populate. ~2 hr of hands-on work, the rest is the printer running. |
| 2 · Hardware assembly per node (×2) | 14 | ~3 hr | Two complete sensor boxes, powered. |
| 3 · Base OS install | 6 | ~2 hr | Both Jetsons booting JetPack from NVMe over Wi-Fi. AI scripts the flashing + first-boot config. |
| 4 · GPS time discipline | 6 | ~1.5 hr | Sub-µs GPS-locked clocks. AI writes the chrony.conf and verifies the PPS. |
| 5 · SDR install + first capture | 4 | ~1 hr | Coherent 5-channel IQ streaming. Mostly running the vendor installer. |
| 6 · CUDA pipeline environment | 6 | ~1 hr | cuFFT + TensorRT verified working with a hello-world kernel the assistant generates. |
| 7 · Mesh networking | 6 | ~2 hr | Two nodes auto-discovering on a Wi-Fi mesh. |
| 8 · Passive radar pipeline | 11 | ~6 hr | End-to-end detection from real RF. The genuinely creative part — but the assistant writes the cuFFT cross-ambiguity, CFAR, and IQ-alignment code. |
| 9 · Status display + I/O | 5 | ~2 hr | OLED, LEDs, button working without a laptop. Pure boilerplate; the assistant generates it. |
| 10 · Operator console (web app) | 9 | ~3 hr | Tablet UI showing live detections. Standard React + Leaflet, the assistant scaffolds it. |
| 11 · Field test | 6 | ~4 hr | Drone fly-by detected by both nodes. Bound by going outside and flying things, not by code. |
| 12 · Iteration & classifier | 5 | ~4 hr | Trained classifier deployed; tuned CFAR. Iteration is the real cost — multiple field-test rounds. |
| Total focused effort | ~87 | ~30 hr (~2 weekends) | v1 complete. Plus ~1 wk of elapsed time waiting for parts. |
The beads
| ID | Title | Deps | Success criteria | Time |
|---|---|---|---|---|
| Phase 0 · Procurement | ||||
| B-001 | Order long-lead items first | — | Jetson Orin Nano Super dev kit (×2) and KrakenSDR (×2) ordered. Estimated ship dates noted in your tracker. | 1 hr |
| B-002 | Order remaining BOM | B-001 | Every line item from the BOM tab placed. Total spend matches the grand-total estimate within 10%. | 2 hr |
| B-003 | Inventory check on arrival | B-001, B-002 | All parts arrived, opened, and laid out. Photos taken for reference. Anything missing or damaged → reorder before continuing. | 2 hr |
| Phase 1 · Enclosure | ||||
| B-010 | Generate STL files from OpenSCAD | B-003 | Run openscad -o body.stl -D 'compute_module="jetson_orin_nano"' nomio_node.scad (and similar for lid + 2 trays). Four STL files exist with correct dimensions. |
2 hr |
| B-011 | Print the body shell | B-010 | PETG, 0.2 mm layers, 30% gyroid infill, 3 perimeters. ~6 hr print. Body fits all dry-fit components. | 7 hr (mostly print) |
| B-012 | Print the lid + 2 internal trays | B-010 | ~6 hr combined print. All parts fit dry-fit. | 7 hr (mostly print) |
| B-013 | Heat-set M3 inserts in case | B-011, B-012 | All 14 M3 holes have brass inserts press-fit with a soldering iron at 220 °C. Each insert sits flush ±0.3 mm. | 1 hr |
| B-014 | Install 1/4"-20 brass insert + vent membrane | B-013 | 1/4-20 insert in bottom face accepts a tripod thread snugly. GORE vent membrane bonded over the rear vent hole with double-sided tape. | 30 min |
| B-015 | Repeat for second case | B-014 | Two complete, identical, populated-but-empty enclosures. | 1 day (with overlap) |
| Phase 2 · Hardware assembly (per node × 2) | ||||
| B-020 | Install AX210 Wi-Fi M.2 in dev kit | B-015 | AX210 seated in M.2 Key E slot. Single mounting screw torqued. | 10 min |
| B-021 | Install NVMe SSD in dev kit | B-020 | NVMe seated in M.2 Key M slot. Single mounting screw torqued. | 10 min |
| B-022 | Connect Wi-Fi antennas via MHF4 | B-020 | Both MHF4 connectors clicked onto AX210. Pigtails routed to two small holes drilled in the case sidewall. RP-SMA dipoles screwed on outside. | 30 min |
| B-023 | Stack Uputronics GPS HAT on 40-pin header | B-021 | HAT seated firmly. PPS jumper present (LED on HAT lights up briefly when power applied later). | 15 min |
| B-024 | Wire OLED to I2C pins | B-023 | OLED VCC → 3V3 (pin 1), GND → GND (pin 6), SDA → pin 3 (I2C1_SDA), SCL → pin 5 (I2C1_SCL). Connections verified with multimeter for continuity. | 45 min |
| B-025 | Wire 3 LEDs and button to GPIO | B-024 | Green → GPIO 12, Blue → GPIO 16, Red → GPIO 20 (each via 330 Ω to GND). Button between GPIO 21 and GND. Continuity verified. | 1 hr |
| B-026 | Mount Jetson + HAT on top tray | B-025 | Jetson dev kit secured with 4× M2.5 standoffs. HAT clears tray ceiling. | 20 min |
| B-027 | Mount KrakenSDR on middle tray with thermal pad | B-026 | KrakenSDR secured with 4× M3 bolts. 3 mm thermal pad between SDR top surface and case sidewall. | 30 min |
| B-028 | Install rear-panel SMA bulkheads | B-027 | 3 SMA bulkheads (2× RF, 1× GPS) installed through rear face. Right-angle pigtails to KrakenSDR CH0/CH1 and HAT GPS port. | 45 min |
| B-029 | Slide power bank into bottom tray | B-028 | Anker 737 sits snugly. Friction-fit foam keeps it stable. USB-C ports accessible from rear. | 15 min |
| B-030 | Wire USB-C power and data | B-029 | Power bank → Jetson USB-C PWR. Power bank → KrakenSDR PWR. Jetson USB 3.2 → KrakenSDR DATA. | 15 min |
| B-031 | Install front-panel hardware in lid | B-030 | OLED visible through lid window. 3 LEDs poked through holes. Button accessible. All flush. | 30 min |
| B-032 | Final assembly: lid on, M3 corner bolts | B-031 | Case closed, all 4 M3 corner bolts torqued. Power bank still removable by sliding from rear without opening case. | 15 min |
| B-033 | Repeat for node B | B-032 | Two physically complete, identical sensor nodes. | 3 hr |
| Phase 3 · Base OS install | ||||
| B-040 | Install NVIDIA SDK Manager on host laptop | — | SDK Manager downloaded and installed on Ubuntu 22.04 host (a VM works). Logged in with NVIDIA developer account. | 30 min |
| B-041 | Boot Jetson into recovery mode | B-033, B-040 | Force recovery jumper set, USB-C connected to host. lsusb on host shows "NVIDIA Corp. APX". |
15 min |
| B-042 | Flash JetPack 6 to NVMe via SDK Manager | B-041 | Full JetPack 6.x flash to NVMe completes. Storage device set to NVMe (not SD), not eMMC. Jetson reboots into Ubuntu OEM-config wizard. | 45 min |
| B-043 | First-boot OEM config | B-042 | Username nomio, hostname nomio-a1 for the first node and nomio-a2 for the second. Locale, timezone set. Finished setup brings up Ubuntu desktop. |
15 min |
| B-044 | Enable SSH, copy keys, set static node ID | B-043 | SSH from your laptop to nomio@nomio-a1.local works without a password (key-based auth). /etc/nomio/node.conf contains NODE_ID=A1. |
30 min |
| B-045 | Apt update + install base utils on both nodes | B-044 | sudo apt update && sudo apt upgrade -y succeeds. git tmux htop nvtop python3-pip jq installed. Same on node B. |
45 min |
| Phase 4 · GPS time discipline | ||||
| B-050 | Install gpsd, chrony, jetson-gpio | B-045 | apt install gpsd gpsd-clients chrony python3-jetson.gpio pps-tools succeeds. |
15 min |
| B-051 | Enable UART for HAT NMEA | B-050 | Disable serial console on UART pins, enable raw UART. cat /dev/ttyTHS0 shows raw NMEA strings within ~5 min of having a sky view. |
45 min |
| B-052 | Configure gpsd for Uputronics HAT | B-051 | cgps -s shows live position fix and satellite count ≥ 4. Time UTC matches your phone within seconds. |
30 min |
| B-053 | Configure PPS via GPIO 18 | B-052 | Device tree overlay or pps-gpio module loaded. ppstest /dev/pps0 prints one timestamp per second, with stable cadence. |
1.5 hr |
| B-054 | Configure chrony to discipline clock from PPS | B-053 | chronyc sources -v shows the PPS source as preferred (* marker). Stratum 1. |
45 min |
| B-055 | Verify time accuracy < 10 µs | B-054 | chronyc tracking reports System time offset under 10 µs after 5 minutes of lock. Repeat on node B. |
30 min |
| Phase 5 · SDR install + first IQ capture | ||||
| B-060 | Install KrakenSDR DAQ from official repo | B-055 | Clone krakensdr_doa, follow Linux install. kraken_doa --help prints usage. |
45 min |
| B-061 | Verify lsusb shows 5× RTL2838 + control | B-060 | lsusb | grep RTL lists 5 RTL2838 devices + the control microcontroller. None reset randomly under load. |
15 min |
| B-062 | Run kraken_doa, verify coherent IQ stream | B-061 | Default DAQ pipeline runs without errors. Web UI shows 5 channels with correlated noise floors. | 30 min |
| B-063 | Run KrakenSDR built-in calibration | B-062 | Calibration completes; phase offsets between channels are stable to within ±2°. Repeat on node B. | 30 min |
| Phase 6 · CUDA pipeline environment | ||||
| B-070 | Verify CUDA toolkit | B-045 | nvcc --version shows CUDA 12.x. nvidia-smi shows the iGPU. jtop reports GPU utilization. |
15 min |
| B-071 | Create Python venv at /opt/nomio/venv | B-070 | python3 -m venv --system-site-packages /opt/nomio/venv succeeds. Venv activates cleanly. |
15 min |
| B-072 | Install Python deps | B-071 | pip install cupy-cuda12x numpy scipy pyzmq websockets fastapi uvicorn luma.oled adafruit-circuitpython-rfm9x all succeed without errors. |
30 min |
| B-073 | CuPy self-test: 1M-point complex FFT under 10 ms | B-072 | Run a 5-line Python script that does cupy.fft.fft(cupy.random.random(1_000_000) + 1j*...) and times it. Result < 10 ms after warm-up. |
30 min |
| B-074 | Install TensorRT Python bindings | B-073 | import tensorrt; print(tensorrt.__version__) prints 8.x or 10.x without error. |
30 min |
| B-075 | Run TensorRT example to verify GPU inference | B-074 | NVIDIA's sampleOnnxMNIST or equivalent runs and returns correct classifications. Repeat on node B. |
45 min |
| Phase 7 · Mesh networking | ||||
| B-080 | Install batman-adv kernel module | B-045 | sudo modprobe batman-adv succeeds. lsmod | grep batman shows it loaded. |
30 min |
| B-081 | Bring up bat0 mesh interface, static IP | B-080 | Both nodes have bat0 interface up. Node A = 10.42.0.1/24, Node B = 10.42.0.2/24. |
1 hr |
| B-082 | Verify mesh ping < 10 ms | B-081 | ping 10.42.0.2 from Node A returns < 10 ms RTT consistently. batctl o shows the peer. |
15 min |
| B-083 | Install Avahi, advertise nomio service | B-082 | Each node advertises _nomio-sensor._tcp.local with TXT records (node ID, version). avahi-browse -art from either node sees both. |
45 min |
| B-084 | Configure tablet AP mode (hostapd) | B-083 | Each node has a second virtual Wi-Fi interface acting as an AP, SSID like nomio-a1. Phone or tablet connects, gets DHCP from the node, and pings it. |
1.5 hr |
| B-085 | Generate node identity keys; share public keys | B-082 | Each node has an Ed25519 keypair in /etc/nomio/keys/. Public keys exchanged into known_peers on each node. A signed test message round-trips successfully. |
1 hr |
| Phase 8 · Passive radar pipeline | ||||
| B-100 | Bootstrap nomio-radar repo | B-075 | Git repo created with project layout: nomio_radar/ package, tests/, configs/, pyproject.toml, Makefile. Imports cleanly inside the venv. |
1 hr |
| B-101 | IQ ingest from Kraken DAQ socket | B-100 | Module nomio_radar.ingest connects to KrakenSDR DAQ ZMQ socket and yields 1024-sample IQ buffers per channel. Unit test passes against recorded sample data. |
3 hr |
| B-102 | GPS-disciplined timestamping | B-101, B-055 | Each yielded buffer has a UTC nanosecond timestamp accurate to ±10 µs. Verified by comparing two consecutive timestamps to expected sample-rate spacing. | 2 hr |
| B-103 | Direct-path cross-correlation alignment | B-102 | Function align_channels(ref, surv) finds the integer-sample lag that maximizes |xcorr| between reference and surveillance, returns the lag. Verified on a recorded capture: lag matches expected geometry. |
3 hr |
| B-104 | CLEAN clutter cancellation on GPU | B-103 | CuPy implementation of CLEAN iterates direct-path subtraction. Output surveillance channel has direct-path peak suppressed by > 25 dB. Runs in < 5 ms per buffer. | 1 day |
| B-105 | Cross-ambiguity function on GPU | B-104 | cuFFT-based 2D cross-ambiguity over a 200 range × 64 Doppler bin grid runs in < 50 ms per frame on the Jetson. Output amplitude-squared map. | 1.5 days |
| B-106 | CFAR detector as CUDA kernel | B-105 | Cell-averaging CFAR over the 200×64 cross-ambiguity map. Configurable P_fa. Runs in < 5 ms. Verified on synthetic data: detects injected target, no false alarms above threshold. |
1 day |
| B-107 | Detection serializer (typed JSON) | B-106 | Each detection becomes {node_id, timestamp_ns, range_m, doppler_hz, snr_db, classifier_label, confidence}. Validated against a Pydantic schema. |
2 hr |
| B-108 | Stub classifier (always returns "drone", p=0.8) | B-107 | Placeholder classifier in pipeline. Real model replaces this in B-181. | 30 min |
| B-109 | WebSocket detection broadcaster | B-108 | FastAPI WebSocket endpoint at /ws/detections. Broadcasts each detection JSON. websocat client receives them in real time. |
3 hr |
| B-110 | End-to-end synthetic-data test | B-109 | Replay a recorded IQ file with a synthesized drone-like target. Pipeline produces 1+ detections at the correct range and Doppler. Detection JSON appears on the WebSocket. | 1 day |
| Phase 9 · Status display + I/O | ||||
| B-120 | OLED daemon (luma.oled) | B-024, B-072 | Python script renders the 8-line status frame from the Sensor Node tab. Updates 1 Hz. Reads state from a local UNIX socket fed by other daemons. | 3 hr |
| B-121 | LED state-machine daemon | B-025, B-072 | Driver that observes gpsd, KrakenSDR, mesh, and detections, and lights LEDs accordingly. Verified by toggling each input and watching LEDs match. |
3 hr |
| B-122 | Button handler (short / long press) | B-025, B-072 | Short press = show pairing fingerprint on OLED for 30 s. Long press = clean shutdown via systemctl. Verified manually. |
2 hr |
| B-123 | Wrap each as systemd service | B-120, B-121, B-122 | Three unit files in /etc/systemd/system/. systemctl status nomio-status shows all running. |
1 hr |
| B-124 | Auto-start on boot, dependency order | B-123 | Reboot. Within 60 s of power-on, OLED shows live status, LEDs are correct, GPS lock acquired, mesh joined. | 1 hr |
| Phase 10 · Operator console (web app) | ||||
| B-140 | Bootstrap React + Vite project | B-109 | Static SPA built with Vite, served from FastAPI as static files at /. Loads in browser, shows placeholder layout. |
3 hr |
| B-141 | Detection list view | B-140 | Subscribes to /ws/detections. Live-updating table of recent detections sorted by time. Clicking a row selects it. |
3 hr |
| B-142 | Range-Doppler heatmap (Canvas/WebGL) | B-141 | Server downsamples the cross-ambiguity map to 256×256 px and pushes via WebSocket. Client renders to a canvas at 10 Hz. Selected detection is highlighted. | 2 days |
| B-143 | Map view (MapLibre GL) | B-141 | Map shows both sensor nodes' positions (from GPS), bistatic ellipses for selected detection, and recent track history. Pre-cached offline tiles. | 2 days |
| B-144 | Mesh health panel | B-141 | Header chips show mesh status (✓ both peers up), GPS lock per node, dets/min, battery %. | 3 hr |
| B-145 | BT pairing flow (Web Bluetooth) | B-144 | "Pair new node" flow: tablet scans BT, finds node, displays fingerprint, exchanges mesh key, confirms. Works on Chrome Android. iOS gets a "scan QR code from OLED" fallback. | 1.5 days |
| B-146 | Auto-discovery (mDNS) on join | B-145 | When the tablet's Wi-Fi connects to the node SSID, the app auto-finds the node via mDNS and connects. No manual IP entry. | 3 hr |
| B-147 | Event log view | B-141 | Persistent SQLite log on the node, served via REST. UI shows newest-first list, filterable by level / target / node. | 3 hr |
| B-148 | Polish + responsive layout | B-141, B-142, B-143, B-144, B-147 | Works as a real tablet UI: large touch targets, high-contrast palette, dark mode for outdoor use. Tested on a real tablet, not just the laptop browser. | 1 day |
| Phase 11 · Field test | ||||
| B-160 | Indoor reference-only test | B-110, B-124 | Identify a strong local FM station. Reference channel SNR > 30 dB. KrakenSDR not saturated. Logged spectrum looks like a normal FM broadcast. | 2 hr |
| B-161 | Indoor known-target test (slow car) | B-160 | Park near a road. Pipeline produces a transient detection track when a car drives by, at the expected bistatic range and Doppler. | 2 hr |
| B-162 | Two-site indoor mesh test | B-161, B-085 | Both nodes powered up. Mesh forms. Same target produces detections on both nodes within ±100 ms. Both visible in tablet UI. | 3 hr |
| B-163 | Outdoor single-site drone hover | B-162 | Deploy node A outdoors. Hover the DJI drone at known range (e.g., 200 m). Detection appears at the right bistatic range. SNR > 6 dB. | 3 hr |
| B-164 | Outdoor two-site drone fly-by | B-163 | Deploy both nodes ~50 m apart outdoors. Drone fly-by at varying ranges. Both nodes detect, ranges geometrically consistent. Localization estimate within ~30 m of true position. | 4 hr |
| B-165 | Capture 30 min of mixed-target data | B-164 | Record raw IQ + detection log + GPS for 30 min in a moderately busy outdoor location. Files archived for offline analysis and classifier training. | 1.5 hr |
| Phase 12 · Iteration & classifier | ||||
| B-180 | Tune CFAR thresholds from collected data | B-165 | Replay capture, sweep P_fa from 1e-2 to 1e-6. Choose threshold that gives < 1 false alarm/min and detects the known drone fly-by reliably. |
1 day |
| B-181 | Train micro-Doppler classifier | B-165 | Extract spectrograms around each detection, label by hand (drone / bird / car / clutter), train a small CNN (~100K params) in PyTorch. Test accuracy > 85% on held-out set. | 3 days |
| B-182 | Convert classifier to TensorRT engine | B-181 | PyTorch → ONNX → TensorRT FP16 engine. Inference latency < 2 ms per detection on the Jetson. | 1 day |
| B-183 | Replace stub classifier with real engine | B-182, B-108 | Pipeline now emits real classifier_label + confidence values. UI reflects them. Repeat outdoor fly-by — drone is labelled "drone" with confidence > 0.85. |
4 hr |
| B-184 | Final end-to-end demo + release | B-183 | Tag v1.0 on the repo. Record a 5-minute demo video showing: power-on, GPS lock, mesh forms, drone fly-by, classified detection on tablet. Hand off to the next contributor. |
1 day |
Working with Claude / Cascade on each bead
The intended workflow:
- Open a fresh chat / terminal window for the bead.
- Paste the bead text plus any context Claude needs (current branch, files, errors).
- Let Claude propose a plan, push back on it, then ask it to write code or run commands.
- Verify against the success criterion. If it doesn't match, debug together.
- Commit, mark the bead done, move to the next.
For very small beads (B-060, B-070, B-080), this might be a single message. For larger beads (B-105, B-142, B-181), expect 1–3 days of conversation across many sessions. The ID lets you resume context cleanly: "Continuing on B-105."
What to do when stuck
- Verification fails. Don't move on. The dependencies in later beads assume earlier ones are correct. Debug or ask Claude to help debug.
- You can't make a measurement. Some success criteria need real RF — find a higher floor with sky view, find a stronger FM station, try at night when local interference is lower.
- A part is broken. Reorder. The system is reproducible by design — the BOM lists every part with a canonical source.
- You're going faster than expected. Don't skip — but feel free to batch small beads into one session.