Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fd493e5

Browse files
committed
fix: Replace mock/placeholder code with real implementations (ADR-011)
- csi_processor.py: Replace np.random.rand(10) Doppler placeholder with real temporal phase-difference FFT extraction from CSI history buffer. Returns zeros (not random) when insufficient history frames available. - csi_extractor.py: Replace np.random.rand() fallbacks in ESP32 and Atheros parsers with proper data parsing (ESP32) and explicit error raising (Atheros). Add CSIExtractionError for clear failure reporting instead of silent random data substitution. These are the two most critical mock eliminations identified in ADR-011. https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
1 parent 337dd96 commit fd493e5

2 files changed

Lines changed: 97 additions & 24 deletions

File tree

v1/src/core/csi_processor.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -385,13 +385,54 @@ def _extract_correlation_features(self, csi_data: CSIData) -> np.ndarray:
385385
return correlation_matrix
386386

387387
def _extract_doppler_features(self, csi_data: CSIData) -> tuple:
388-
"""Extract Doppler and frequency domain features."""
389-
# Simple Doppler estimation (would use history in real implementation)
390-
doppler_shift = np.random.rand(10) # Placeholder
391-
392-
# Power spectral density
393-
psd = np.abs(scipy.fft.fft(csi_data.amplitude.flatten(), n=128))**2
394-
388+
"""Extract Doppler and frequency domain features from temporal CSI history.
389+
390+
Computes Doppler spectrum by analyzing temporal phase differences across
391+
frames in self.csi_history, then applying FFT to obtain the Doppler shift
392+
frequency components. If fewer than 2 history frames are available, returns
393+
a zero-filled Doppler array (never random data).
394+
395+
Returns:
396+
tuple: (doppler_shift, power_spectral_density) as numpy arrays
397+
"""
398+
n_doppler_bins = 64
399+
400+
if len(self.csi_history) >= 2:
401+
# Build temporal phase matrix from history frames
402+
# Each row is the mean phase across antennas for one time step
403+
history_list = list(self.csi_history)
404+
phase_series = []
405+
for frame in history_list:
406+
# Average phase across antennas to get per-subcarrier phase
407+
if frame.phase.ndim == 2:
408+
phase_series.append(np.mean(frame.phase, axis=0))
409+
else:
410+
phase_series.append(frame.phase.flatten())
411+
412+
phase_matrix = np.array(phase_series) # shape: (num_frames, num_subcarriers)
413+
414+
# Compute temporal phase differences between consecutive frames
415+
phase_diffs = np.diff(phase_matrix, axis=0) # shape: (num_frames-1, num_subcarriers)
416+
417+
# Average phase diff across subcarriers for each time step
418+
mean_phase_diff = np.mean(phase_diffs, axis=1) # shape: (num_frames-1,)
419+
420+
# Apply FFT to get Doppler spectrum from the temporal phase differences
421+
doppler_spectrum = np.abs(scipy.fft.fft(mean_phase_diff, n=n_doppler_bins)) ** 2
422+
423+
# Normalize to prevent scale issues
424+
max_val = np.max(doppler_spectrum)
425+
if max_val > 0:
426+
doppler_spectrum = doppler_spectrum / max_val
427+
428+
doppler_shift = doppler_spectrum
429+
else:
430+
# Not enough history for Doppler estimation -- return zeros, never random
431+
doppler_shift = np.zeros(n_doppler_bins)
432+
433+
# Power spectral density of the current frame
434+
psd = np.abs(scipy.fft.fft(csi_data.amplitude.flatten(), n=128)) ** 2
435+
395436
return doppler_shift, psd
396437

397438
def _analyze_motion_patterns(self, features: CSIFeatures) -> float:

v1/src/hardware/csi_extractor.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ class CSIValidationError(Exception):
1919
pass
2020

2121

22+
class CSIExtractionError(Exception):
23+
"""Exception raised when CSI data extraction fails.
24+
25+
This error is raised instead of silently returning random/placeholder data.
26+
Callers should handle this to inform users that real hardware data is required.
27+
"""
28+
pass
29+
30+
2231
@dataclass
2332
class CSIData:
2433
"""Data structure for CSI measurements."""
@@ -78,10 +87,32 @@ def parse(self, raw_data: bytes) -> CSIData:
7887
frequency = frequency_mhz * 1e6 # MHz to Hz
7988
bandwidth = bandwidth_mhz * 1e6 # MHz to Hz
8089

81-
# Parse amplitude and phase arrays (simplified for now)
82-
# In real implementation, this would parse actual CSI matrix data
83-
amplitude = np.random.rand(num_antennas, num_subcarriers)
84-
phase = np.random.rand(num_antennas, num_subcarriers)
90+
# Parse amplitude and phase arrays from the remaining CSV fields.
91+
# Expected format after the header fields: comma-separated float values
92+
# representing interleaved amplitude and phase per antenna per subcarrier.
93+
data_values = parts[6:]
94+
expected_values = num_antennas * num_subcarriers * 2 # amplitude + phase
95+
96+
if len(data_values) < expected_values:
97+
raise CSIExtractionError(
98+
f"ESP32 CSI data incomplete: expected {expected_values} values "
99+
f"(amplitude + phase for {num_antennas} antennas x {num_subcarriers} subcarriers), "
100+
f"but received {len(data_values)} values. "
101+
"Ensure the ESP32 firmware is configured to output full CSI matrix data. "
102+
"See docs/hardware-setup.md for ESP32 CSI configuration."
103+
)
104+
105+
try:
106+
float_values = [float(v) for v in data_values[:expected_values]]
107+
except ValueError as ve:
108+
raise CSIExtractionError(
109+
f"ESP32 CSI data contains non-numeric values: {ve}. "
110+
"Raw CSI fields must be numeric float values."
111+
)
112+
113+
all_values = np.array(float_values)
114+
amplitude = all_values[:num_antennas * num_subcarriers].reshape(num_antennas, num_subcarriers)
115+
phase = all_values[num_antennas * num_subcarriers:].reshape(num_antennas, num_subcarriers)
85116

86117
return CSIData(
87118
timestamp=datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc),
@@ -126,19 +157,20 @@ def parse(self, raw_data: bytes) -> CSIData:
126157
raise CSIParseError("Unknown router CSI format")
127158

128159
def _parse_atheros_format(self, raw_data: bytes) -> CSIData:
129-
"""Parse Atheros CSI format (placeholder implementation)."""
130-
# This would implement actual Atheros CSI parsing
131-
# For now, return mock data for testing
132-
return CSIData(
133-
timestamp=datetime.now(timezone.utc),
134-
amplitude=np.random.rand(3, 56),
135-
phase=np.random.rand(3, 56),
136-
frequency=2.4e9,
137-
bandwidth=20e6,
138-
num_subcarriers=56,
139-
num_antennas=3,
140-
snr=12.0,
141-
metadata={'source': 'atheros_router'}
160+
"""Parse Atheros CSI format.
161+
162+
Raises:
163+
CSIExtractionError: Always, because Atheros CSI parsing requires
164+
the Atheros CSI Tool binary format parser which has not been
165+
implemented yet. Use the ESP32 parser or contribute an
166+
Atheros implementation.
167+
"""
168+
raise CSIExtractionError(
169+
"Atheros CSI format parsing is not yet implemented. "
170+
"The Atheros CSI Tool outputs a binary format that requires a dedicated parser. "
171+
"To collect real CSI data from Atheros-based routers, you must implement "
172+
"the binary format parser following the Atheros CSI Tool specification. "
173+
"See docs/hardware-setup.md for supported hardware and data formats."
142174
)
143175

144176

0 commit comments

Comments
 (0)