K-LD7 Raw ADC Processing — Design Spec¶
ARCHIVED DOCUMENT
This is a historical design or implementation note, kept as a record of why the code is shaped the way it is. It describes the project as of the date in its filename and is not a guide to follow — commands, paths, and constants may no longer match the code. See the Archive index for current alternatives.
Date: 2026-04-05 Status: Draft
Goal¶
Replace dependence on the K-LD7's internal detector (PDAT) with custom FFT + CFAR processing on raw ADC samples (RADC). This gives us control over detection threshold, FFT resolution, and angle estimation — the same signal-processing chain that commercial launch monitors use.
Background¶
The K-LD7 currently outputs processed detections (PDAT) at ~34 fps. A golf ball appears in only 1-2 PDAT frames because the module's internal detector is conservative. With raw I/Q we can run longer FFTs, lower thresholds, and potentially recover ball returns the module discards.
RADC Frame Structure¶
3072 bytes per frame at ~34 fps:
| Segment | Content | Size |
|---|---|---|
| F1 Freq A | 256 I samples (uint16) + 256 Q samples (uint16) | 1024 bytes |
| F2 Freq A | 256 I samples (uint16) + 256 Q samples (uint16) | 1024 bytes |
| F1 Freq B | 256 I samples (uint16) + 256 Q samples (uint16) | 1024 bytes |
Two frequencies (F1, F2) enable phase-difference angle estimation between Rx channels.
Bandwidth Requirement¶
RADC at 34 fps = ~102 KB/s. Current baud rate of 115200 (~11.5 KB/s) is insufficient. Must use 3 Mbaud for reliable RADC streaming.
Deliverables¶
1. Capture Script — scripts/capture_kld7_radc.py¶
Purpose: Stream and record raw ADC data from a single K-LD7 for offline analysis.
Behavior:
- Connect to K-LD7 at specified port and baud rate (default 3 Mbaud)
- Stream RADC | PDAT | TDAT simultaneously
- RADC for custom processing, PDAT/TDAT for comparison against module's detector
- Record continuously for a specified duration
- Save to .pkl with metadata
CLI:
./scripts/capture_kld7_radc.py --port /dev/ttyUSB0 --orientation vertical --duration 60
./scripts/capture_kld7_radc.py --port /dev/ttyUSB0 --baud 3000000 --duration 30
Output .pkl structure:
{
"metadata": {
"module": "K-LD7",
"port": "/dev/ttyUSB0",
"baud_rate": 3000000,
"orientation": "vertical",
"capture_start": "2026-04-05T...",
"capture_end": "2026-04-05T...",
"total_frames": N,
"params": { ... }, # K-LD7 config params
},
"frames": [
{
"timestamp": float,
"radc": {
"f1a_i": np.ndarray, # (256,) uint16
"f1a_q": np.ndarray, # (256,) uint16
"f2a_i": np.ndarray, # (256,) uint16
"f2a_q": np.ndarray, # (256,) uint16
"f1b_i": np.ndarray, # (256,) uint16
"f1b_q": np.ndarray, # (256,) uint16
},
"tdat": dict | None, # module's tracked target
"pdat": list[dict], # module's raw detections
},
...
]
}
2. Analysis Library — scripts/kld7_radc_lib.py¶
Purpose: Standalone helpers for processing RADC captures. No dependency on the main openflight package.
Dependencies: numpy only. Plotting lives in the analysis script, not here.
FFT Processing¶
parse_radc_frame(frame) -> dict— Extract I/Q arrays from raw RADC bytescompute_range_doppler(iq_complex, fft_size=2048) -> np.ndarray— Hann window, zero-pad, FFT, magnitude spectrumcfar_detect(spectrum, guard_cells=4, training_cells=16, threshold_factor=8.0) -> list[Detection]— OS-CFAR on the range-Doppler spectrum, returns peaks above adaptive noise floor
Spatial Filtering¶
estimate_angle_from_phase(f1_complex, f2_complex) -> float— Phase-difference angle estimation between the two frequency channelsfilter_by_distance(detections, min_m, max_m) -> list— Distance gatefilter_by_velocity(detections, min_kmh, max_kmh) -> list— Velocity gate
Ball Isolation¶
find_ball_candidates(frames, club_event_time) -> list— Distance gate 2-6m, fast outbound, short burst, appearing after club eventcompare_radc_vs_pdat(frame) -> dict— Side-by-side comparison of what our FFT finds vs what the module's PDAT reported
Data Types¶
@dataclass
class RADCDetection:
frame_index: int
timestamp: float
distance_m: float
velocity_kmh: float
angle_deg: float # from phase difference
magnitude: float # FFT bin magnitude
snr_db: float # signal-to-noise from CFAR
bin_index: int # FFT bin number
3. Analysis Script — scripts/analyze_kld7_radc.py¶
Purpose: Visualize and explore RADC captures.
Dependencies: numpy, matplotlib
CLI:
# Full analysis
./scripts/analyze_kld7_radc.py capture.pkl
# Zoom to detected swing windows
./scripts/analyze_kld7_radc.py capture.pkl --shot-windows
# Export detections to CSV
./scripts/analyze_kld7_radc.py capture.pkl --csv
Generated outputs: - Range-Doppler heatmap per frame (around swing events) - Detection timeline: our FFT detections vs PDAT detections on same time axis - Distance vs time scatter for ball candidates - Per-shot comparison table (CSV): our detection count, angles, magnitudes vs PDAT
FFT Parameters¶
| Parameter | Value | Rationale |
|---|---|---|
| Window | Hann | Standard for spectral analysis, good sidelobe suppression |
| Input samples | 256 (from RADC) | Fixed by hardware |
| Zero-pad to | 2048 | 8× interpolation, ~0.15 m/s velocity resolution |
| CFAR guard cells | 4 | Prevent target self-masking |
| CFAR training cells | 16 | Enough for noise estimate |
| CFAR threshold | 8.0× (start) | Tunable — lower = more sensitive, more false alarms |
What This Is Not¶
- Not a live replacement for the PDAT tracker — purely offline analysis to validate the approach
- Not coherent integration across frames — single-frame FFT first, multi-frame stacking is a future step if this shows promise
- Not dual-K-LD7 — single vertical unit for now, horizontal comes later
Success Criteria¶
- Capture script reliably records RADC at 3 Mbaud without dropped frames
- Our FFT + CFAR finds at least as many ball detections as PDAT on the same capture
- We can see ball returns in the range-Doppler map that PDAT missed
- Phase-based angle estimation produces reasonable values (within ±10° of PDAT angles)