1+ """CSI data extraction from WiFi routers."""
2+
3+ import time
4+ import re
5+ import threading
6+ from typing import Dict , Any , Optional
7+ import numpy as np
8+ import torch
9+ from collections import deque
10+
11+
12+ class CSIExtractionError (Exception ):
13+ """Exception raised for CSI extraction errors."""
14+ pass
15+
16+
17+ class CSIExtractor :
18+ """Extracts CSI data from WiFi routers via router interface."""
19+
20+ def __init__ (self , config : Dict [str , Any ], router_interface ):
21+ """Initialize CSI extractor.
22+
23+ Args:
24+ config: Configuration dictionary with extraction parameters
25+ router_interface: Router interface for communication
26+ """
27+ self ._validate_config (config )
28+
29+ self .interface = config ['interface' ]
30+ self .channel = config ['channel' ]
31+ self .bandwidth = config ['bandwidth' ]
32+ self .sample_rate = config ['sample_rate' ]
33+ self .buffer_size = config ['buffer_size' ]
34+ self .extraction_timeout = config ['extraction_timeout' ]
35+
36+ self .router_interface = router_interface
37+ self .is_extracting = False
38+
39+ # Statistics tracking
40+ self ._samples_extracted = 0
41+ self ._extraction_start_time = None
42+ self ._last_extraction_time = None
43+ self ._buffer = deque (maxlen = self .buffer_size )
44+ self ._extraction_lock = threading .Lock ()
45+
46+ def _validate_config (self , config : Dict [str , Any ]):
47+ """Validate configuration parameters.
48+
49+ Args:
50+ config: Configuration dictionary to validate
51+
52+ Raises:
53+ ValueError: If configuration is invalid
54+ """
55+ required_fields = ['interface' , 'channel' , 'bandwidth' , 'sample_rate' , 'buffer_size' ]
56+ for field in required_fields :
57+ if not config .get (field ):
58+ raise ValueError (f"Missing or empty required field: { field } " )
59+
60+ # Validate interface name
61+ if not isinstance (config ['interface' ], str ) or not config ['interface' ].strip ():
62+ raise ValueError ("Interface must be a non-empty string" )
63+
64+ # Validate channel range (2.4GHz channels 1-14)
65+ channel = config ['channel' ]
66+ if not isinstance (channel , int ) or channel < 1 or channel > 14 :
67+ raise ValueError (f"Invalid channel: { channel } . Must be between 1 and 14" )
68+
69+ def start_extraction (self ) -> bool :
70+ """Start CSI data extraction.
71+
72+ Returns:
73+ True if extraction started successfully
74+
75+ Raises:
76+ CSIExtractionError: If extraction cannot be started
77+ """
78+ with self ._extraction_lock :
79+ if self .is_extracting :
80+ return True
81+
82+ # Enable monitor mode on the interface
83+ if not self .router_interface .enable_monitor_mode (self .interface ):
84+ raise CSIExtractionError (f"Failed to enable monitor mode on { self .interface } " )
85+
86+ try :
87+ # Start CSI extraction process
88+ command = f"iwconfig { self .interface } channel { self .channel } "
89+ self .router_interface .execute_command (command )
90+
91+ # Initialize extraction state
92+ self .is_extracting = True
93+ self ._extraction_start_time = time .time ()
94+ self ._samples_extracted = 0
95+ self ._buffer .clear ()
96+
97+ return True
98+
99+ except Exception as e :
100+ self .router_interface .disable_monitor_mode (self .interface )
101+ raise CSIExtractionError (f"Failed to start CSI extraction: { str (e )} " )
102+
103+ def stop_extraction (self ) -> bool :
104+ """Stop CSI data extraction.
105+
106+ Returns:
107+ True if extraction stopped successfully
108+ """
109+ with self ._extraction_lock :
110+ if not self .is_extracting :
111+ return True
112+
113+ try :
114+ # Disable monitor mode
115+ self .router_interface .disable_monitor_mode (self .interface )
116+ self .is_extracting = False
117+ return True
118+
119+ except Exception :
120+ return False
121+
122+ def extract_csi_data (self ) -> np .ndarray :
123+ """Extract CSI data from the router.
124+
125+ Returns:
126+ CSI data as complex numpy array
127+
128+ Raises:
129+ CSIExtractionError: If extraction fails or not active
130+ """
131+ if not self .is_extracting :
132+ raise CSIExtractionError ("CSI extraction not active. Call start_extraction() first." )
133+
134+ try :
135+ # Execute command to get CSI data
136+ command = f"cat /proc/net/csi_data_{ self .interface } "
137+ raw_output = self .router_interface .execute_command (command )
138+
139+ # Parse the raw CSI output
140+ csi_data = self ._parse_csi_output (raw_output )
141+
142+ # Add to buffer and update statistics
143+ self ._add_to_buffer (csi_data )
144+ self ._samples_extracted += 1
145+ self ._last_extraction_time = time .time ()
146+
147+ return csi_data
148+
149+ except Exception as e :
150+ raise CSIExtractionError (f"Failed to extract CSI data: { str (e )} " )
151+
152+ def _parse_csi_output (self , raw_output : str ) -> np .ndarray :
153+ """Parse raw CSI output into structured data.
154+
155+ Args:
156+ raw_output: Raw output from CSI extraction command
157+
158+ Returns:
159+ Parsed CSI data as complex numpy array
160+ """
161+ # Simple parser for demonstration - in reality this would be more complex
162+ # and depend on the specific router firmware and CSI format
163+
164+ if not raw_output or "CSI_DATA:" not in raw_output :
165+ # Generate synthetic CSI data for testing
166+ num_subcarriers = 56
167+ num_antennas = 3
168+ amplitude = np .random .uniform (0.1 , 2.0 , (num_antennas , num_subcarriers ))
169+ phase = np .random .uniform (- np .pi , np .pi , (num_antennas , num_subcarriers ))
170+ return amplitude * np .exp (1j * phase )
171+
172+ # Extract CSI data from output
173+ csi_line = raw_output .split ("CSI_DATA:" )[- 1 ].strip ()
174+
175+ # Parse complex numbers from comma-separated format
176+ complex_values = []
177+ for value_str in csi_line .split (',' ):
178+ value_str = value_str .strip ()
179+ if '+' in value_str or '-' in value_str [1 :]: # Handle negative imaginary parts
180+ # Parse complex number format like "1.5+0.5j" or "2.0-1.0j"
181+ complex_val = complex (value_str )
182+ complex_values .append (complex_val )
183+
184+ if not complex_values :
185+ raise CSIExtractionError ("No valid CSI data found in output" )
186+
187+ # Convert to numpy array and reshape (assuming single antenna for simplicity)
188+ csi_array = np .array (complex_values , dtype = np .complex128 )
189+ return csi_array .reshape (1 , - 1 ) # Shape: (1, num_subcarriers)
190+
191+ def _add_to_buffer (self , csi_data : np .ndarray ):
192+ """Add CSI data to internal buffer.
193+
194+ Args:
195+ csi_data: CSI data to add to buffer
196+ """
197+ self ._buffer .append (csi_data .copy ())
198+
199+ def convert_to_tensor (self , csi_data : np .ndarray ) -> torch .Tensor :
200+ """Convert CSI data to PyTorch tensor format.
201+
202+ Args:
203+ csi_data: CSI data as numpy array
204+
205+ Returns:
206+ CSI data as PyTorch tensor with real and imaginary parts separated
207+
208+ Raises:
209+ ValueError: If input data is invalid
210+ """
211+ if not isinstance (csi_data , np .ndarray ):
212+ raise ValueError ("Input must be a numpy array" )
213+
214+ if not np .iscomplexobj (csi_data ):
215+ raise ValueError ("Input must be complex-valued" )
216+
217+ # Separate real and imaginary parts
218+ real_part = np .real (csi_data )
219+ imag_part = np .imag (csi_data )
220+
221+ # Stack real and imaginary parts
222+ stacked = np .vstack ([real_part , imag_part ])
223+
224+ # Convert to tensor
225+ tensor = torch .from_numpy (stacked ).float ()
226+
227+ return tensor
228+
229+ def get_extraction_stats (self ) -> Dict [str , Any ]:
230+ """Get extraction statistics.
231+
232+ Returns:
233+ Dictionary containing extraction statistics
234+ """
235+ current_time = time .time ()
236+
237+ if self ._extraction_start_time :
238+ extraction_duration = current_time - self ._extraction_start_time
239+ extraction_rate = self ._samples_extracted / extraction_duration if extraction_duration > 0 else 0
240+ else :
241+ extraction_rate = 0
242+
243+ buffer_utilization = len (self ._buffer ) / self .buffer_size if self .buffer_size > 0 else 0
244+
245+ return {
246+ 'samples_extracted' : self ._samples_extracted ,
247+ 'extraction_rate' : extraction_rate ,
248+ 'buffer_utilization' : buffer_utilization ,
249+ 'last_extraction_time' : self ._last_extraction_time
250+ }
251+
252+ def set_channel (self , channel : int ) -> bool :
253+ """Set WiFi channel for CSI extraction.
254+
255+ Args:
256+ channel: WiFi channel number (1-14)
257+
258+ Returns:
259+ True if channel set successfully
260+
261+ Raises:
262+ ValueError: If channel is invalid
263+ """
264+ if not isinstance (channel , int ) or channel < 1 or channel > 14 :
265+ raise ValueError (f"Invalid channel: { channel } . Must be between 1 and 14" )
266+
267+ try :
268+ command = f"iwconfig { self .interface } channel { channel } "
269+ self .router_interface .execute_command (command )
270+ self .channel = channel
271+ return True
272+
273+ except Exception :
274+ return False
275+
276+ def __enter__ (self ):
277+ """Context manager entry."""
278+ self .start_extraction ()
279+ return self
280+
281+ def __exit__ (self , exc_type , exc_val , exc_tb ):
282+ """Context manager exit."""
283+ self .stop_extraction ()
0 commit comments