Passive WiFi Radar: Engineering Thru-Wall RF Sensing with SDRs and Python
Learn how to architect a passive WiFi radar system using Software Defined Radios (SDRs) and Python. This deep dive covers signal processing, cross-ambiguity functions, and clutter mitigation to detect motion through walls.
Introduction to Passive WiFi Radar (PWR)
Passive RF sensing has transitioned from classified military applications to accessible, open-source software engineering. Unlike active radar systems that transmit a dedicated signal and analyze the return echo, Passive WiFi Radar (PWR) leverages existing ambient radio frequency (RF) emissions—specifically the ubiquitous 2.4 GHz and 5 GHz WiFi signals already flooding our living and working spaces. By treating these commercial emissions as "transmitters of opportunity," we can construct a bistatic or multistatic radar system capable of detecting movement, tracking targets, and even imaging structures through physical barriers like drywall and concrete.
For software engineers and hardware hackers, building a PWR system represents a fascinating intersection of digital signal processing (DSP), electromagnetics, and real-time systems programming. In this deep dive, we will explore the architectural blueprint, the mathematical foundations, and a practical Python implementation for a dual-channel passive radar processing pipeline.
The Physics of RF Sensing: Multipath & Doppler Shifts
To understand how we can "see" through walls using WiFi, we must look at how RF waves interact with the physical environment. When a WiFi router transmits data, the signal propagates in all directions. Some waves travel directly to our receiver (the Direct Path), while others bounce off stationary objects like walls and furniture (Static Reflections), or moving objects like humans (Dynamic Reflections).
This phenomenon, known as multipath propagation, is typically an obstacle for communication protocols. However, for radar engineering, it is our primary source of data.
When an RF wave reflects off a moving target, it undergoes a frequency shift proportional to the target's radial velocity relative to the transmitter and receiver. This is the Doppler Shift ($\Delta f$), defined as:
$$\Delta f = \frac{2 v}{\lambda} \cos(\theta)$$
Where:
- $v$ is the target's velocity.
- $\lambda$ is the wavelength of the carrier signal (e.g., $\approx 12.5$ cm for 2.4 GHz WiFi).
- $\theta$ is the bistatic angle.
By measuring the delay (range) and the frequency shift (Doppler) of the reflected signals relative to the direct path signal, we can construct a Range-Doppler Map (RDM). This map allows us to isolate moving entities from static clutter, even if they are located behind a wall.
Hardware Architecture: The Dual-Channel Setup
To build a functioning passive radar, you cannot simply listen with a single antenna. You must capture two distinct signals simultaneously:
- The Reference Channel ($s_{ref}(t)$): An antenna pointed directly at the WiFi transmitter (the illumination source). This captures the clean, direct-path transmission used as a baseline.
- The Surveillance Channel ($s_{surv}(t)$): A highly directional antenna pointed toward the target area (e.g., through a wall). This channel captures the weak, delayed, and Doppler-shifted reflections from moving targets.
+------------------+ Direct Path +----------------------+
| WiFi Router |----------------------------->| Reference Antenna |---
| (Illuminator) | +----------------------+ |
+------------------+ | +------------------+
| |->| Coherent Dual- |
| Reflected | | Channel SDR |
v Path | +------------------+
+---------+ +----------------------+ | |
| Target |----------------------------------->| Surveillance Antenna |--+ | USB
+---------+ +----------------------+ v
+------------------+
| Processing Host |
| (Python/DSP) |
+------------------+
The Coherency Requirement
Crucially, the two channels must be phase-coherent. If the local oscillators of the two receivers drift independently, the phase relationship between the reference and surveillance channels will be lost, rendering cross-correlation impossible.
To achieve phase coherency, you can use:
- KrakenSDR or KerberosSDR: 5-channel and 4-channel RTL-SDR coherent receivers sharing a single local clock.
- Ettus USRP (Universal Software Radio Peripheral): Dual-channel models like the USRP B210.
- HackRF One with an external clock injection: Less ideal, but functional with external phase synchronization.
The Software Pipeline: Cross-Ambiguity Function (CAF)
At the heart of passive radar DSP is the Cross-Ambiguity Function (CAF). The CAF correlates the surveillance signal with time-delayed and frequency-shifted versions of the reference signal. Mathematically, the continuous CAF is defined as:
$$\chi(\tau, f_d) = \int_{0}^{T_i} s_{surv}(t) s_{ref}^*(t - \tau) e^{-j 2 \pi f_d t} dt$$
Where:
- $\tau$ is the bistatic time delay (corresponding to range).
- $f_d$ is the bistatic Doppler shift.
- $T_i$ is the integration time (typically between 50 ms and 500 ms).
- $^*$ denotes the complex conjugate.
By evaluating this integral over a grid of candidate delays $\tau$ and Doppler frequencies $f_d$, we produce the Range-Doppler Map. The peak values on this map represent detected targets.
Building the DSP Pipeline in Python
Let’s implement a simplified, highly optimized version of the CAF pipeline in Python using numpy and scipy. Because computing the raw 2D correlation is computationally expensive, we use an accelerated approach using the Fast Fourier Transform (FFT).
Prerequisites
Make sure you have the required scientific stack installed:
pip install numpy scipy matplotlib
Python Implementation
import numpy as np
import scipy.signal as signal
import matplotlib.pyplot as plt
def compute_range_doppler_map(ref_signal, surv_signal, fs, max_delay_samples, doppler_bins, integration_time):
"""
Computes the Range-Doppler Map (RDM) using an FFT-based batch algorithm.
Parameters:
ref_signal (np.ndarray): Complex samples from the Reference channel.
surv_signal (np.ndarray): Complex samples from the Surveillance channel.
fs (float): Sampling frequency in Hz.
max_delay_samples (int): Maximum delay range to calculate (X-axis limit).
doppler_bins (int): Number of Doppler frequency bins to resolve (Y-axis resolution).
integration_time (float): Coherent Integration Time (CIT) in seconds.
"""
# Determine number of samples for the given integration time
N_samples = int(fs * integration_time)
ref = ref_signal[:N_samples]
surv = surv_signal[:N_samples]
# Divide the integration time into smaller blocks for Doppler processing
# Number of blocks corresponds to the Doppler resolution
block_length = N_samples // doppler_bins
rdm = np.zeros((doppler_bins, max_delay_samples), dtype=complex)
# Process block-by-block using FFT-accelerated cross-correlation
for b in range(doppler_bins):
start_idx = b * block_length
end_idx = start_idx + block_length
r_block = ref[start_idx:end_idx]
s_block = surv[start_idx:end_idx]
# FFT-based fast correlation
R_f = np.fft.fft(r_block)
S_f = np.fft.fft(s_block)
# Cross-correlation in frequency domain
corr = np.fft.ifft(S_f * np.conj(R_f))
rdm[b, :] = corr[:max_delay_samples]
# Apply FFT along the temporal (Doppler) axis to resolve frequency shifts
rdm = np.fft.fftshift(np.fft.fft(rdm, axis=0), axes=0)
return rdm
# --- Simulation of a target detection ---
if __name__ == "__main__":
# Sample parameters
fs = 10e6 # 10 MS/s (common for SDRs like HackRF/USRP)
duration = 0.25 # 250 ms integration time
num_samples = int(fs * duration)
# Generate dummy white noise to simulate OFDM-like WiFi carrier
t = np.arange(num_samples) / fs
reference_signal = np.random.normal(size=num_samples) + 1j * np.random.normal(size=num_samples)
# Simulate a moving target: delay of 45 samples, Doppler shift of 120 Hz
target_delay = 45
target_doppler = 120.0
# Create delayed reference
delayed_sig = np.roll(reference_signal, target_delay)
# Apply doppler shift
doppler_phase = np.exp(2j * np.pi * target_doppler * t)
surveillance_signal = delayed_sig * doppler_phase * 0.01 # Highly attenuated echo
# Add receiver thermal noise to the surveillance channel
surveillance_signal += (np.random.normal(size=num_samples) + 1j * np.random.normal(size=num_samples)) * 0.1
# Compute RDM
max_delay = 100
doppler_resolution_bins = 64
rdm_data = compute_range_doppler_map(
reference_signal,
surveillance_signal,
fs,
max_delay,
doppler_resolution_bins,
duration
)
# Plotting
plt.figure(figsize=(10, 6))
doppler_freqs = np.linspace(-doppler_resolution_bins/2, doppler_resolution_bins/2, doppler_resolution_bins) * (1/duration)
plt.imshow(
20 * np.log10(np.abs(rdm_data) + 1e-10),
extent=[0, max_delay, doppler_freqs[0], doppler_freqs[-1]],
aspect='auto',
cmap='viridis'
)
plt.colorbar(label='Power (dB)')
plt.title('Passive WiFi Radar Range-Doppler Map')
plt.xlabel('Delay (Samples)')
plt.ylabel('Doppler Shift (Hz)')
plt.grid(True, linestyle='--', alpha=0.5)
plt.show()
Mitigating Noise and Static Clutter
If you run the basic script above in a real-world scenario, you will encounter a glaring issue: Direct Path Interference (DPI) and static clutter (reflections from walls) will completely blind your receiver. Because the direct path signal is orders of magnitude stronger than the tiny reflections bouncing off a human target behind a wall, the RDM will display a massive, bleeding peak at zero delay and zero Doppler.
To see through walls, we must employ spatial and digital filtering algorithms to nullify the static signals.
1. Spatial Nulling
You can place the reference antenna behind an RF shield or use a directional Yagi-Uda antenna pointed away from the surveillance zone. This physically attenuates the direct path signal in the surveillance channel.
2. Digital Clutter Cancellation (LS/LMS Filtering)
To digitally subtract the static environment, we use adaptive filter algorithms. The Least Mean Squares (LMS) or Recursive Least Squares (RLS) filter models the static channel impulse response and subtracts it from the surveillance signal in real-time.
$$\hat{s}{surv}(t) = s{surv}(t) - \sum_{k=0}^{M} w_k s_{ref}(t - k)$$
Where $w_k$ are the filter weights adjusted dynamically to minimize the power of the zero-Doppler static reflections. By subtracting this estimate, only the time-varying components (the moving targets) remain.
Future Outlook: Integrating Edge AI
With clutter removed and range-doppler signatures isolated, the next frontier is target classification. A human moving behind a wall produces a unique micro-Doppler signature—tiny secondary frequency shifts caused by the swinging of arms and legs.
By feeding raw Range-Doppler frames into a Convolutional Neural Network (CNN) running on edge platforms (like an NVIDIA Jetson or a Raspberry Pi 5), developers can classify activities (e.g., walking, falling, breathing) without deploying intrusive cameras. This preserves privacy while providing critical spatial awareness in smart homes, security environments, and search-and-rescue scenarios.
Passive WiFi radar proves that with a couple of budget SDRs, some RF fundamentals, and a few lines of clean DSP code, you can turn ambient electromagnetic noise into a high-fidelity scanning array.