KrakenSDR Direction-Finding (DoA) Rig

Runbook for a 5-channel KrakenSDR direction-finding (DoA) rig built on a Raspberry Pi 5 for mobile TETRA (380–385 MHz) direction finding — the rig goes in a friend’s car and is watched remotely over Tailscale. Built end-to-end in one session on 2026-08-12; DoA autostarts on boot and the full chain is verified with a real transmitter.

For Agents

This runs on the same physical Pi 5 as the wifi-hunter project (raspi / raspi.local / 192.168.1.200, ssh raspi). The wlan0 NetworkManager “Hotspot” AP seen here is wifi-hunter’s pre-existing shared AP — NOT a firewall problem. This note is the KrakenSDR subsystem; wifi-hunter is the WiFi-hunting subsystem on the same box. Reach the UI at http://kraken-rig.taild4189d.ts.net:8080/doa — the :8080 is REQUIRED.

The mechanical/enclosure side of this same rig is a separate project: krakenpi-carbox (3D-printed car box for the Kraken + Pi 5 + 12 V power bay, modelled in Fusion 360, complete but not yet printed as of 2026-08-26).

Status at a glance

ThingState
KrakenSDR hardware✅ 5× RTL2838 enumerated over internal USB hub
Software stack (DoA + PR)✅ built from source under /home/levander
DoA web UI✅ live on :8080/doa (MUSIC, UCA, Compass)
Autostart on bootkrakensdr.service enabled + active → DoA
Mode switch (DoA ↔ PR ↔ off)/usr/local/bin/kraken-mode {doa|pr|off}
Full chain end-to-end✅ verified — 420 MHz TX → clean MUSIC peak, bearing ~294°
Physical pentagon array✅ 200 mm radius (printed spacer, 200 mm hole), ext-1 whips; array_offset still uncalibrated
Field/car deploymentWORKING & field-testing across the city (2026-08-26) — Kraken Pro iOS app in LOCAL mode over hunter-ap (phone GPS). See 2026-08-26-krakensdr-field-test-milestone
Power✅ stable on 5V/5A (throttled=0x0); 5V/3A + generic PD-30W brown out and reset — see milestone note

Hardware

HostRaspberry Pi 5, 2 GB RAM, aarch64, kernel 6.18
OSDebian 13 (trixie), 64-bit
Hostnameraspi · raspi.local · 192.168.1.200 (eth0, home LAN)
Userlevander (passwordless sudo)
SSH from Mac/usr/bin/ssh levander@raspi.local
Tailscale nodekraken-rig = 100.87.1.1 / kraken-rig.taild4189d.ts.net, tag:telep
PurposeMobile TETRA (380–385 MHz) direction finding, watched remotely

Non-obvious hardware facts

  • The KrakenSDR is exactly 5× RTL2838 (R820T2 tuners) behind an internal USB hub — there is NO separate serial / CH340 controller. The noise-source calibration is driven over tuner GPIO by the krakenrf librtlsdr fork, not a serial device. (A CH340 seen on the bench elsewhere was an unrelated ESP32 — do not go looking for a Kraken serial port.)
  • It needs its own USB-C power, separate from the USB-C data cable — the Kraken is self-powered via its own USB-C, not off the Pi.
  • The Pi 5 + DAQ browns out and resets on 5V/3A (a generic PD-30W still only delivers 5V/3A since the Pi only takes 5V → does NOT help). Needs a real 5V/5A source: verified stable then (throttled=0x0, usb_max_current_enable auto-flips to 1). Confirmed 2026-08-26 that the earlier “OOM crashes” were largely these power brownouts, not memory. Car: 12V→5V/5A buck + usb_max_current_enable=1, Kraken and phone on their OWN feeds. See 2026-08-26-krakensdr-field-test-milestone.
  • “Power Level: Overdrive” in the DAQ status = RF over-gain (ADC clipping), NOT a weak PSU. The fix is to lower the RF gain. A genuinely weak PSU instead shows up as USB dropouts / tuner disconnects in dmesg — a completely different symptom.
  • This is a 2 GB Pi 5 and the DAQ is memory-heavy → OOM crashes. After two OOM crashes on 2026-08-26, added zram swap (zram-tools, zstd, 50%, zramswap.service) = ~1 GB compressed swap. See 2026-08-26-raspi-oom-zram-hunter-ap-433-retune (incl. the zramswap restart-fails-while-active gotcha).

Software install

Everything under /home/levander, built from source (distro packages are not sufficient).

ComponentNotes
librtlsdrkrakenrf fork — REQUIRED, NOT the distro rtl-sdr. Drives the noise-source cal over tuner GPIO.
Ne10built from source
heimdall_daq_fwthe DAQ firmware/pipeline
krakensdr_doadirection-of-arrival app
krakensdr_prpassive radar — see the 404 note below
conda env krakenMiniforge, Python 3.9

krakensdr_pr is GONE (404)

krakenrf/krakensdr_pr no longer exists — the org moved to krakensdr_suite (V2, C++, DoA + spectrum only, no passive radar). Passive radar was installed from the verbatim mirror mfkiwl/krakensdr_pr. If re-installing PR, use the mirror, not the (dead) original org path.

trixie / py3.9 / aarch64 dependency pins

These pins were needed to get the web UI running on this platform (the upstream docs’ versions do not work here):

flask==2.0.3
werkzeug==2.0.2
jinja2==3.0.3
markupsafe==2.0.1
itsdangerous==2.0.1
dash==1.20.0                     # + matching dash-core / dash-html / dash-renderer / dash-table
dash_bootstrap_components==0.13.1   # NOT the doc's 1.1.0 (that needs dash>=2)
dash_devices                     # the websocket dash fork the web UI imports AS `dash`

Why dash_devices matters

The web UI imports dash_devices as dash. If it is missing, app.py dies with ModuleNotFoundError: No module named 'dash_devices'. This is the exact crash you get when the conda env silently didn’t activate (see the CRITICAL gotcha below).

Kernel driver blacklist

The stock DVB drivers must be blacklisted so the Kraken’s tuners are free for librtlsdr. In /etc/modprobe.d/blacklist-rtlsdr.conf:

blacklist dvb_usb_rtl28xxu
blacklist rtl2832
blacklist rtl2830
blacklist rtl2832_sdr
blacklist rtl2838

Ports

PortServiceNotes
:8080DoA / PR web UI⚠️ DoA and PR BOTH bind :8080
:8081php data-outworks without miniserve
:8042node app backend

Never run DoA and PR at once

Both bind :8080. Use kraken-mode (below) to switch — it stops one stack before starting the other.

  • miniserve could not be installed — the distro rustc is too old to build it. It is optional (only the DoA remote-control mode needs it; php data-out works without it).

CRITICAL gotcha — conda must be sourced first

The start scripts silently fail over SSH / systemd

kraken_doa_start.sh (and friends) do eval "$(conda shell.bash hook)"; conda activate kraken. Over a non-interactive SSH shell or under systemd, conda is not on PATH and ~/.bashrc isn’t sourced, so conda activate silently failsapp.py launches under system python → crashes with ModuleNotFoundError: No module named 'dash_devices'.

FIX — always source conda explicitly BEFORE the start script:

source /home/levander/miniforge3/etc/profile.d/conda.sh && conda activate kraken

The start script’s own top comment notes this requirement for systemd. kraken-mode (below) already does this.

Stale /dev/shm buffers block DAQ sync

A prior broken-python start can leave stale decimator buffers in /dev/shmdelay_sync loops “Shared memory not exist” and never syncs. Clean them between restarts if the DAQ won’t sync:

rm -f /dev/shm/decimator_in* /dev/shm/decimator_out* /dev/shm/delay_sync_*

Autostart + mode switch

Built this session so DoA comes up on boot and DoA/PR can be swapped safely.

/usr/local/bin/kraken-mode {doa|pr|off} — sources conda first, stops both stacks, then starts the requested one. Only one mode runs at a time (they share :8080).

kraken-mode doa    # start DoA (default boot mode)
kraken-mode pr     # start passive radar
kraken-mode off    # stop everything

systemd krakensdr.serviceenabled + active, brings DoA up on boot:

[Service]
Type=oneshot
RemainAfterExit=yes
User=levander
ExecStart=/usr/local/bin/kraken-mode doa
ExecStop=/usr/local/bin/kraken-mode off

Verified: after a boot-style start the UI returns HTTP 200 on :8080.

Exit 255 over interactive SSH is harmless

Running the kraken start/stop scripts over an interactive SSH session throws exit 255 — a TTY-teardown artifact, not a real failure. It never happens under systemd (no TTY).

Array geometry + DoA config

Bearings are only valid if software config MATCHES the physical array

The noise-source calibration fixes electronics phase, NOT antenna geometry. The software array config must match the physical build, or bearings are meaningless.

The live config as of 2026-08-26 targets the TETRA mobile UPLINK at 382.114 MHz (10 MHz below the 392.114 base downlink) on a 200 mm array. Full settings.json values in 2026-08-26-raspi-oom-zram-hunter-ap-433-retune.

Setting (settings.json key)Value
Array type (ant_arrangement)UCA (uniform circular array)
Array Radius (ant_spacing_meters)0.20 m (200 mm template hole; KrakenSDR default s ≈ 0.33)
Algorithm (doa_method)MUSIC
Decorrelation (doa_decorrelation_method)Off
Optimize Short Bursts (en_optimize_short_bursts)ON (bursty TETRA uplink)
Center / VFO (center_freq / vfo_freq_*)382.114 MHz / 382114000
Channel BW (vfo_bw_0)25000 Hz (TETRA 25 kHz channel)
RF gain (uniform_gain)19.7 dB (below overdrive on the strong TETRA signal)
Array Offset (array_offset)0 (UNCALIBRATED — bearings relative)
Location (location_source)gpsd (idle until a GPS feeds NMEA)

Physical array recipe (200 mm radius — current)

  • Regular pentagon, 200 mm radius using the KrakenSDR printed pentagon spacer (holes at 100 / 150 / 200 / 250 mm → use the 200 mm hole). Element spacing = 1.176 × 0.20 = 23.5 cm.
  • Telescopic whips, all 5 identical, at extension ext-1 (366–950 MHz) — covers 382 AND 433, so no whip swap between bands. (ext-3 = 290–420 centers 382 slightly better for SWR, but on a coherent array whip length is common-mode and doesn’t bias bearings.)
  • Equal-length coax to every element (within ~1 cm).
  • Element #0 = reference.

Radius is 200 mm — NOT the old "30 cm" or a "25 cm rebuild"

200 mm is the KrakenSDR default spacing multiplier s ≈ 0.33 across 382–434 MHz: radius r = s·λ / 1.176 (n=5) ≈ 220 mm at 382, ≈ 194 mm at 434 → the 200 mm hole fits the whole band. Set ant_spacing_meters = 0.2.

Spacing must stay ≤ 0.5 λ or you get ghost/mirror bearings

Iₑ = s·λ must be < 0.5 λ. The 200 mm radius (23.5 cm spacing) is unambiguous across 382–434 MHz. The old 0.30 m geometry had 35 cm spacing → a ~428 MHz ambiguity ceiling; that ceiling is a fact about the old array, and the fix was the 200 mm template hole — not a 25 cm rebuild (an earlier draft’s number, never applied).

Also works for 433 MHz keyfob DF, unchanged

The same 200 mm array is unambiguous at 433.92 MHz (λ=69.1 cm, λ/2=34.6 cm > 23.5 cm spacing) on ext-1 whips — no rebuild and no whip swap needed to chase a 433 target. Keyfobs are bursty OOK: keep Optimize-Short-Bursts ON, drop squelch, DF while pressing the button.

Array Offset calibration

Bearings are relative to element #0 until Array Offset is calibrated against a known-direction source. Until then, treat the compass reading as relative, not absolute.

Operational lessons

  • Target 380–385 MHz = TETRA, largely bursty uplink (quiet until a handset keys up).
  • 🚫 Do NOT transmit on 380–385 MHz — emergency-services band. For testing, transmit on a legal freq below the 428 MHz ambiguity ceiling and retune the Kraken to it. Antenna-length mismatch is common-mode across the 5 identical whips → it does NOT hurt bearings.
  • RTL-SDR always shows a fake DC spike at the exact center frequencyoffset-tune to distinguish a real signal from the artifact.
  • Verified end-to-end: transmitted on 420 MHz → strong carrier, clean MUSIC peak, valid bearing (~294°). Full chain confirmed working.

Remote access + networking

  • Tailscale node kraken-rig = 100.87.1.1 / kraken-rig.taild4189d.ts.net, tagged tag:telep (same tailnet + tag convention as the rest of the homelab — see tailnet-service-exposure-convention).
  • UI: http://kraken-rig.taild4189d.ts.net:8080/doa — ⚠️ the :8080 is REQUIRED. Without it the browser hits port 80 → connection refused. (This looked like a Tailscale ACL problem but was just the missing port.)

Pi networking

InterfaceRole
eth0home LAN, 192.168.1.200, carries SSH
wlan0pre-existing NetworkManager “Hotspot” AP (10.42.0.0/24) — this is wifi-hunter’s shared AP
  • The nft wlan0 reject rules are NetworkManager’s normal shared-hotspot NAT, NOT a firewall problem.
  • eth0 (wired) and wlan0 (AP) run simultaneously — on the bench the Pi keeps its wired uplink/tailnet AND serves hunter-ap at once. The single-radio limit is intra-wlan0: wlan0 can’t be AP and client at the same time. The canonical car plan avoids that flip: USB-tether an Android phone so the Pi gets data on usb0 and wlan0 stays the AP. See 2026-08-26-raspi-oom-zram-hunter-ap-433-retune.
  • Bring the AP up manually with sudo nmcli connection up Hotspot (SSID hunter-ap, 10.42.0.1/24); DoA UI over the AP = http://10.42.0.1:8080/doa.

Open items

  • Calibrate Array Offset against a known-direction source so bearings are absolute, not relative (array_offset is currently 0 = uncalibrated).
  • Field/car deployment — WORKING (2026-08-26). Recommended workflow is the Kraken Pro iOS app in LOCAL mode: iPhone joins hunter-ap, app → server 10.42.0.1, phone’s own GPS drives the map. No USB-tether, no gpsd, no GPS puck needed for the DF map. See 2026-08-26-krakensdr-field-test-milestone. Physical packaging designed but unprinted — see krakenpi-carbox.
  • GPS — NO LONGER on the critical path: the Kraken Pro app supplies GPS from the phone. gpsd/USB-tether/u-blox puck are only needed if you separately want the Pi itself online in the field (optional).
  • miniserve (DoA remote-control mode) blocked on the distro rustc being too old — optional, revisit if remote-control is wanted.