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).
Mobile 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).
Component
Notes
librtlsdr
⭐ krakenrf fork — REQUIRED, NOT the distro rtl-sdr. Drives the noise-source cal over tuner GPIO.
Ne10
built from source
heimdall_daq_fw
the DAQ firmware/pipeline
krakensdr_doa
direction-of-arrival app
krakensdr_pr
passive radar — see the 404 note below
conda env kraken
Miniforge, 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_devicesas 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:
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 activatesilently fails → app.py launches under system python → crashes with ModuleNotFoundError: No module named 'dash_devices'.
FIX — always source conda explicitly BEFORE the start script:
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/shm → delay_sync loops “Shared memory not exist” and never syncs. Clean them between restarts if the DAQ won’t sync:
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 radarkraken-mode off # stop everything
systemd krakensdr.service — enabled + active, brings DoA up on boot:
[Service]Type=oneshotRemainAfterExit=yesUser=levanderExecStart=/usr/local/bin/kraken-mode doaExecStop=/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.
DoA software config (current live — TETRA uplink)
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 frequency — offset-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
Interface
Role
eth0
home LAN, 192.168.1.200, carries SSH
wlan0
pre-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.