Adding device detection for SDR

This commit is contained in:
Marc
2026-02-06 07:28:47 -06:00
parent 04d9d2fd56
commit 7caa7247ef
3 changed files with 311 additions and 35 deletions

View File

@@ -0,0 +1,224 @@
# GSM Spy SDR Device Selection Implementation
## Summary
Successfully implemented dynamic SDR device detection, selection, and management for the GSM Spy feature, following the same pattern used in the Aircraft/ADS-B implementation.
## Changes Made
### Frontend Changes (`templates/gsm_spy_dashboard.html`)
#### 1. Dynamic Device Selector
- **Changed**: Device dropdown from hardcoded options to dynamic detection
- **Location**: Line ~1155 (Signal Source Panel)
- **Before**: Static options (Device 0, Device 1, etc.)
- **After**: Dynamic population with "Detecting devices..." placeholder
#### 2. Device Detection on Page Load
- **Added**: `initDeviceSelector()` function
- **Location**: ~Line 1395
- **Functionality**:
- Fetches available SDR devices from `/devices` endpoint
- Populates dropdown with detected devices
- Shows device name, type (RTL-SDR, HackRF, etc.), and serial number
- Handles errors gracefully with user-friendly messages
- Logs detection results to console
#### 3. Scanner Controls Update
- **Modified**: `startScanner()` function (~Line 1410)
- **Changes**:
- Made async for better error handling
- Reads device index from `deviceSelect` dropdown
- Disables device and region selectors during active scanning
- Enhanced error handling with device conflict detection
- Shows user-friendly alerts for device busy errors
#### 4. Stop Scanner Enhancements
- **Modified**: `stopScanner()` function (~Line 1494)
- **Changes**:
- Re-enables device and region selectors after stopping
- Maintains UI consistency
#### 5. Region Selector Sync
- **Modified**: `selectRegion()` function (~Line 1882)
- **Changes**:
- Capitalizes region name to match backend API expectations
- Syncs region button selection with dropdown
#### 6. Removed Redundant Controls
- **Removed**: `scannerDevice` dropdown from bottom controls bar
- **Reason**: Consolidated to single device selector in left sidebar
### Backend Changes (`routes/gsm_spy.py`)
#### 1. Enhanced Error Response
- **Modified**: `/start` endpoint device claiming logic (~Line 115)
- **Changes**:
- Added `error_type: 'DEVICE_BUSY'` to 409 conflict responses
- Enables frontend to distinguish device conflicts from other errors
- Allows for targeted user-friendly error messages
#### 2. Existing Device Management (Verified)
- **Confirmed**: Device claiming/releasing already implemented
- `claim_sdr_device()` called at line 115
- `release_sdr_device()` called at line 289
- Device index stored in `gsm_spy_active_device`
- Region stored in `gsm_spy_region`
#### 3. Status Endpoint (Verified)
- **Confirmed**: `/status` endpoint returns device info
- Returns `device` (active device index)
- Returns `region` (selected region)
- Returns all necessary status information
## Features Implemented
### ✅ Device Detection
- Dynamically detects all available SDR devices on page load
- Supports all 5 SDR types: RTL-SDR, HackRF, LimeSDR, Airspy, SDRPlay
- Shows device name, type, and serial number in dropdown
### ✅ Device Registry Integration
- Properly claims devices before starting scanner
- Releases devices when stopping scanner
- Prevents conflicts with other INTERCEPT modes
### ✅ UI State Management
- Disables device selector during active scanning
- Re-enables selector after stopping
- Provides clear visual feedback to user
### ✅ Error Handling
- User-friendly error messages for device conflicts
- Graceful handling of "no devices detected" scenario
- Clear console logging for debugging
### ✅ Validation
- Uses existing `validate_device_index()` function (already in code)
- Validates region against `REGIONAL_BANDS` dictionary
- Checks for already running scanner
## Architecture Pattern
The implementation follows the same pattern as Aircraft/ADS-B:
1. **Device Detection**: `/devices` endpoint (shared across all modes)
2. **Device Claiming**: `claim_sdr_device()` before starting
3. **Device Releasing**: `release_sdr_device()` on stop
4. **UI Consistency**: Dynamic dropdown, disabled during operation
5. **Error Handling**: Clear user messages, console logging
## Testing Recommendations
### 1. Device Detection
```bash
# Start application
sudo -E venv/bin/python intercept.py
# Open GSM Spy dashboard in browser
# Open DevTools console
# Should see: "[GSM SPY] Detected X SDR device(s)"
# Verify dropdown shows detected devices
```
### 2. Device Claiming
```bash
# Start GSM scanner on device 0
# Try to start another mode (e.g., ADS-B) on device 0
# Should see conflict error message
# Stop GSM scanner
# Now ADS-B should be able to claim device 0
```
### 3. Multiple Devices
```bash
# Connect multiple SDR devices
# Open GSM Spy dashboard
# Verify all devices appear in dropdown
# Select different devices and verify they work independently
```
### 4. UI State
```bash
# Start GSM scanner
# Verify device selector is disabled
# Verify region selector is disabled
# Stop scanner
# Verify both selectors are re-enabled
```
### 5. Error Scenarios
```bash
# Disconnect SDR device
# Try to start scanner
# Should see graceful error message
# Reconnect device
# Refresh page - device should be detected
```
## Known Limitations
1. **gr-gsm Hardware Support**: The `gr-gsm` tools may have limited support for non-RTL-SDR devices. This implementation handles device selection properly, but `gr-gsm` itself may only work with RTL-SDR.
2. **Command Builder Integration**: Full SDRFactory integration (using device-specific command builders) would require adding GSM-specific methods to command builders in `utils/sdr/`. This is a future enhancement.
3. **Remote Device Support**: Unlike ADS-B which supports remote dump1090 connections, GSM Spy currently only supports local SDR devices.
## Future Enhancements
### 1. SDRFactory Integration
```python
# In start_scanner():
from utils.sdr import SDRFactory
devices = SDRFactory.detect_devices()
sdr_device = next((d for d in devices if d.index == device_index), None)
builder = SDRFactory.get_builder(sdr_device.sdr_type)
cmd = builder.build_gsm_scanner_command(device=sdr_device, bands=REGIONAL_BANDS[region])
```
Note: This requires adding `build_gsm_scanner_command()` method to command builders.
### 2. Device-Specific Tuning
- Different gain settings per SDR type
- Frequency correction (PPM) based on device calibration
- Sample rate optimization per hardware
### 3. Multi-Device Monitoring
- Simultaneously monitor multiple towers on different devices
- Parallel scanning across multiple frequency bands
## Compatibility
- **Frontend**: Modern browsers with ES6+ support (async/await)
- **Backend**: Python 3.8+
- **SDR Hardware**: RTL-SDR, HackRF, LimeSDR, Airspy, SDRPlay
- **gr-gsm**: Requires gr-gsm toolkit installed
## Files Modified
1. `/opt/intercept/templates/gsm_spy_dashboard.html` - Frontend UI and JavaScript
2. `/opt/intercept/routes/gsm_spy.py` - Backend route handlers
## Files Referenced (Not Modified)
1. `/opt/intercept/routes/adsb.py` - Reference implementation
2. `/opt/intercept/utils/sdr/detection.py` - Device detection
3. `/opt/intercept/utils/sdr/__init__.py` - SDRFactory
4. `/opt/intercept/utils/validation.py` - Input validation
5. `/opt/intercept/app.py` - Device registry functions
## Verification
All changes have been implemented according to the plan. The implementation:
- ✅ Follows existing INTERCEPT patterns
- ✅ Maintains UI consistency across modes
- ✅ Includes proper error handling
- ✅ Uses centralized validation
- ✅ Integrates with device registry
- ✅ Provides clear user feedback
## Implementation Date
2026-02-06

View File

@@ -114,7 +114,10 @@ def start_scanner():
from app import claim_sdr_device from app import claim_sdr_device
claim_error = claim_sdr_device(device_index, 'GSM Spy') claim_error = claim_sdr_device(device_index, 'GSM Spy')
if claim_error: if claim_error:
return jsonify({'error': claim_error}), 409 return jsonify({
'error': claim_error,
'error_type': 'DEVICE_BUSY'
}), 409
# Get frequency range for region # Get frequency range for region
bands = REGIONAL_BANDS.get(region, REGIONAL_BANDS['Americas']) bands = REGIONAL_BANDS.get(region, REGIONAL_BANDS['Americas'])

View File

@@ -1151,12 +1151,9 @@
</div> </div>
<div class="signal-source-content"> <div class="signal-source-content">
<div class="form-group"> <div class="form-group">
<label>RTL-SDR Device</label> <label>SDR Device</label>
<select id="deviceSelect"> <select id="deviceSelect" title="SDR device for GSM scanning">
<option value="0">Device 0</option> <option value="0">Detecting devices...</option>
<option value="1">Device 1</option>
<option value="2">Device 2</option>
<option value="3">Device 3</option>
</select> </select>
</div> </div>
</div> </div>
@@ -1297,16 +1294,10 @@
<div class="control-group"> <div class="control-group">
<span class="control-group-label">GSM SCANNER</span> <span class="control-group-label">GSM SCANNER</span>
<div class="control-group-items"> <div class="control-group-items">
<select id="scannerDevice" title="SDR device for GSM scanning">
<option value="0">Device 0</option>
<option value="1">Device 1</option>
<option value="2">Device 2</option>
<option value="3">Device 3</option>
</select>
<select id="scannerRegion" title="GSM frequency region"> <select id="scannerRegion" title="GSM frequency region">
<option value="americas">Americas</option> <option value="Americas">Americas</option>
<option value="europe">Europe</option> <option value="Europe">Europe</option>
<option value="asia">Asia</option> <option value="Asia">Asia</option>
</select> </select>
<button class="start-btn" id="startBtn" onclick="toggleScanner()">START</button> <button class="start-btn" id="startBtn" onclick="toggleScanner()">START</button>
</div> </div>
@@ -1344,6 +1335,7 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
initMap(); initMap();
loadObserverLocation(); loadObserverLocation();
initDeviceSelector();
startUtcClock(); startUtcClock();
}); });
@@ -1391,6 +1383,39 @@
updateClock(); updateClock();
} }
async function initDeviceSelector() {
try {
const response = await fetch('/devices');
const devices = await response.json();
const deviceSelect = document.getElementById('deviceSelect');
deviceSelect.innerHTML = '';
if (!devices || devices.length === 0) {
deviceSelect.innerHTML = '<option value="0">No SDR devices detected</option>';
console.warn('[GSM SPY] No SDR devices detected');
return;
}
// Populate dropdown with detected devices
devices.forEach(device => {
const option = document.createElement('option');
option.value = device.index;
option.textContent = `${device.name} (${device.sdr_type})`;
if (device.serial) {
option.textContent += ` - ${device.serial}`;
}
deviceSelect.appendChild(option);
});
console.log(`[GSM SPY] Detected ${devices.length} SDR device(s)`);
} catch (error) {
console.error('[GSM SPY] Error fetching devices:', error);
const deviceSelect = document.getElementById('deviceSelect');
deviceSelect.innerHTML = '<option value="0">Error detecting devices</option>';
}
}
// ============================================ // ============================================
// SCANNER CONTROL // SCANNER CONTROL
// ============================================ // ============================================
@@ -1402,8 +1427,8 @@
} }
} }
function startScanner() { async function startScanner() {
const device = document.getElementById('scannerDevice').value; const device = parseInt(document.getElementById('deviceSelect').value) || 0;
const region = document.getElementById('scannerRegion').value; const region = document.getElementById('scannerRegion').value;
const lat = parseFloat(document.getElementById('obsLat').value); const lat = parseFloat(document.getElementById('obsLat').value);
const lon = parseFloat(document.getElementById('obsLon').value); const lon = parseFloat(document.getElementById('obsLon').value);
@@ -1414,31 +1439,47 @@
} }
// Start backend scanner // Start backend scanner
fetch('/gsm_spy/start', { try {
method: 'POST', const response = await fetch('/gsm_spy/start', {
headers: { 'Content-Type': 'application/json' }, method: 'POST',
body: JSON.stringify({ headers: { 'Content-Type': 'application/json' },
device: parseInt(device), body: JSON.stringify({
region: region, device: device,
lat: lat, region: region,
lon: lon lat: lat,
}) lon: lon
}) })
.then(response => response.json()) });
.then(data => {
if (!response.ok) {
const error = await response.json();
if (response.status === 409 && error.error_type === 'DEVICE_BUSY') {
alert(`Device Conflict: ${error.error}\n\nStop the other mode before starting GSM scanner.`);
} else {
alert(`Error: ${error.error || 'Failed to start GSM scanner'}`);
}
return;
}
const data = await response.json();
if (data.status === 'started') { if (data.status === 'started') {
isScanning = true; isScanning = true;
updateScannerUI(true); updateScannerUI(true);
// Disable controls during scanning
document.getElementById('deviceSelect').disabled = true;
document.getElementById('scannerRegion').disabled = true;
startEventStream(); startEventStream();
console.log('[GSM SPY] Scanner started'); console.log('[GSM SPY] Scanner started');
} else { } else {
alert('Failed to start scanner: ' + (data.error || 'Unknown error')); alert('Failed to start scanner: ' + (data.error || 'Unknown error'));
} }
}) } catch (error) {
.catch(error => {
console.error('[GSM SPY] Error starting scanner:', error); console.error('[GSM SPY] Error starting scanner:', error);
alert('Error starting scanner'); alert('Error starting scanner');
}); }
} }
function stopScanner() { function stopScanner() {
@@ -1447,6 +1488,11 @@
.then(data => { .then(data => {
isScanning = false; isScanning = false;
updateScannerUI(false); updateScannerUI(false);
// Re-enable controls
document.getElementById('deviceSelect').disabled = false;
document.getElementById('scannerRegion').disabled = false;
if (eventSource) { if (eventSource) {
eventSource.close(); eventSource.close();
eventSource = null; eventSource = null;
@@ -1837,6 +1883,9 @@
function selectRegion(region) { function selectRegion(region) {
currentRegion = region; currentRegion = region;
// Capitalize first letter to match API expectations
const regionCapitalized = region.charAt(0).toUpperCase() + region.slice(1);
// Update UI // Update UI
document.querySelectorAll('.region-btn').forEach(btn => { document.querySelectorAll('.region-btn').forEach(btn => {
btn.classList.remove('active'); btn.classList.remove('active');
@@ -1844,9 +1893,9 @@
document.querySelector(`.region-btn[data-region="${region}"]`).classList.add('active'); document.querySelector(`.region-btn[data-region="${region}"]`).classList.add('active');
// Update scanner region select // Update scanner region select
document.getElementById('scannerRegion').value = region; document.getElementById('scannerRegion').value = regionCapitalized;
console.log('[GSM SPY] Region selected:', region); console.log('[GSM SPY] Region selected:', regionCapitalized);
} }
// ============================================ // ============================================