mirror of
https://github.com/smittix/intercept.git
synced 2026-07-08 01:28:13 -07:00
style: apply ruff-format to entire codebase
First-time run of ruff-format via pre-commit hook normalises quote style, trailing commas, and whitespace across 188 Python files. No logic changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+316
-289
@@ -14,9 +14,9 @@ from utils.subghz import SubGhzCapture, SubGhzManager
|
||||
@pytest.fixture
|
||||
def tmp_data_dir(tmp_path):
|
||||
"""Create a temporary data directory for SubGhz captures."""
|
||||
data_dir = tmp_path / 'subghz'
|
||||
data_dir = tmp_path / "subghz"
|
||||
data_dir.mkdir()
|
||||
(data_dir / 'captures').mkdir()
|
||||
(data_dir / "captures").mkdir()
|
||||
return data_dir
|
||||
|
||||
|
||||
@@ -28,57 +28,57 @@ def manager(tmp_data_dir):
|
||||
|
||||
class TestSubGhzManagerInit:
|
||||
def test_creates_data_dirs(self, tmp_path):
|
||||
data_dir = tmp_path / 'new_subghz'
|
||||
data_dir = tmp_path / "new_subghz"
|
||||
SubGhzManager(data_dir=data_dir)
|
||||
assert (data_dir / 'captures').is_dir()
|
||||
assert (data_dir / "captures").is_dir()
|
||||
|
||||
def test_active_mode_idle(self, manager):
|
||||
assert manager.active_mode == 'idle'
|
||||
assert manager.active_mode == "idle"
|
||||
|
||||
def test_get_status_idle(self, manager):
|
||||
status = manager.get_status()
|
||||
assert status['mode'] == 'idle'
|
||||
assert status["mode"] == "idle"
|
||||
|
||||
|
||||
class TestToolDetection:
|
||||
class TestToolDetection:
|
||||
def test_check_hackrf_found(self, manager):
|
||||
with patch('shutil.which', return_value='/usr/bin/hackrf_transfer'):
|
||||
with patch("shutil.which", return_value="/usr/bin/hackrf_transfer"):
|
||||
assert manager.check_hackrf() is True
|
||||
|
||||
def test_check_hackrf_not_found(self, manager):
|
||||
with patch('shutil.which', return_value=None), \
|
||||
patch('utils.subghz.get_tool_path', return_value=None):
|
||||
manager._hackrf_available = None # reset cache
|
||||
assert manager.check_hackrf() is False
|
||||
def test_check_hackrf_not_found(self, manager):
|
||||
with patch("shutil.which", return_value=None), patch("utils.subghz.get_tool_path", return_value=None):
|
||||
manager._hackrf_available = None # reset cache
|
||||
assert manager.check_hackrf() is False
|
||||
|
||||
def test_check_rtl433_found(self, manager):
|
||||
with patch('shutil.which', return_value='/usr/bin/rtl_433'):
|
||||
with patch("shutil.which", return_value="/usr/bin/rtl_433"):
|
||||
assert manager.check_rtl433() is True
|
||||
|
||||
def test_check_sweep_found(self, manager):
|
||||
with patch('shutil.which', return_value='/usr/bin/hackrf_sweep'):
|
||||
with patch("shutil.which", return_value="/usr/bin/hackrf_sweep"):
|
||||
assert manager.check_sweep() is True
|
||||
|
||||
|
||||
class TestReceive:
|
||||
def test_start_receive_no_hackrf(self, manager):
|
||||
with patch('shutil.which', return_value=None), \
|
||||
patch('utils.subghz.get_tool_path', return_value=None):
|
||||
manager._hackrf_available = None
|
||||
result = manager.start_receive(frequency_hz=433920000)
|
||||
assert result['status'] == 'error'
|
||||
assert 'not found' in result['message']
|
||||
class TestReceive:
|
||||
def test_start_receive_no_hackrf(self, manager):
|
||||
with patch("shutil.which", return_value=None), patch("utils.subghz.get_tool_path", return_value=None):
|
||||
manager._hackrf_available = None
|
||||
result = manager.start_receive(frequency_hz=433920000)
|
||||
assert result["status"] == "error"
|
||||
assert "not found" in result["message"]
|
||||
|
||||
def test_start_receive_success(self, manager):
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.poll.return_value = None
|
||||
mock_proc.stderr = MagicMock()
|
||||
mock_proc.stderr.readline = MagicMock(return_value=b'')
|
||||
mock_proc.stderr.readline = MagicMock(return_value=b"")
|
||||
|
||||
with patch('shutil.which', return_value='/usr/bin/hackrf_transfer'), \
|
||||
patch('subprocess.Popen', return_value=mock_proc), \
|
||||
patch.object(manager, 'check_hackrf_device', return_value=True), \
|
||||
patch('utils.subghz.register_process'):
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/hackrf_transfer"),
|
||||
patch("subprocess.Popen", return_value=mock_proc),
|
||||
patch.object(manager, "check_hackrf_device", return_value=True),
|
||||
patch("utils.subghz.register_process"),
|
||||
):
|
||||
manager._hackrf_available = None
|
||||
result = manager.start_receive(
|
||||
frequency_hz=433920000,
|
||||
@@ -86,12 +86,13 @@ class TestReceive:
|
||||
lna_gain=32,
|
||||
vga_gain=20,
|
||||
)
|
||||
assert result['status'] == 'started'
|
||||
assert result['frequency_hz'] == 433920000
|
||||
assert manager.active_mode == 'rx'
|
||||
assert result["status"] == "started"
|
||||
assert result["frequency_hz"] == 433920000
|
||||
assert manager.active_mode == "rx"
|
||||
|
||||
def test_start_receive_already_running(self, manager):
|
||||
import time as _time
|
||||
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.poll.return_value = None
|
||||
manager._rx_process = mock_proc
|
||||
@@ -101,17 +102,17 @@ class TestReceive:
|
||||
manager._hackrf_device_cache_ts = _time.time()
|
||||
|
||||
result = manager.start_receive(frequency_hz=433920000)
|
||||
assert result['status'] == 'error'
|
||||
assert 'Already running' in result['message']
|
||||
assert result["status"] == "error"
|
||||
assert "Already running" in result["message"]
|
||||
|
||||
def test_stop_receive_not_running(self, manager):
|
||||
result = manager.stop_receive()
|
||||
assert result['status'] == 'not_running'
|
||||
assert result["status"] == "not_running"
|
||||
|
||||
def test_stop_receive_creates_metadata(self, manager, tmp_data_dir):
|
||||
# Create a fake IQ file
|
||||
iq_file = tmp_data_dir / 'captures' / 'test.iq'
|
||||
iq_file.write_bytes(b'\x00' * 1024)
|
||||
iq_file = tmp_data_dir / "captures" / "test.iq"
|
||||
iq_file.write_bytes(b"\x00" * 1024)
|
||||
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.poll.return_value = None
|
||||
@@ -122,23 +123,22 @@ class TestReceive:
|
||||
manager._rx_lna_gain = 32
|
||||
manager._rx_vga_gain = 20
|
||||
manager._rx_start_time = 1000.0
|
||||
manager._rx_bursts = [{'start_seconds': 1.23, 'duration_seconds': 0.15, 'peak_level': 42}]
|
||||
manager._rx_bursts = [{"start_seconds": 1.23, "duration_seconds": 0.15, "peak_level": 42}]
|
||||
|
||||
with patch('utils.subghz.safe_terminate'), \
|
||||
patch('time.time', return_value=1005.0):
|
||||
with patch("utils.subghz.safe_terminate"), patch("time.time", return_value=1005.0):
|
||||
result = manager.stop_receive()
|
||||
|
||||
assert result['status'] == 'stopped'
|
||||
assert 'capture' in result
|
||||
assert result['capture']['frequency_hz'] == 433920000
|
||||
assert result["status"] == "stopped"
|
||||
assert "capture" in result
|
||||
assert result["capture"]["frequency_hz"] == 433920000
|
||||
|
||||
# Verify JSON sidecar was written
|
||||
meta_path = iq_file.with_suffix('.json')
|
||||
meta_path = iq_file.with_suffix(".json")
|
||||
assert meta_path.exists()
|
||||
meta = json.loads(meta_path.read_text())
|
||||
assert meta['frequency_hz'] == 433920000
|
||||
assert isinstance(meta.get('bursts'), list)
|
||||
assert meta['bursts'][0]['peak_level'] == 42
|
||||
assert meta["frequency_hz"] == 433920000
|
||||
assert isinstance(meta.get("bursts"), list)
|
||||
assert meta["bursts"][0]["peak_level"] == 42
|
||||
|
||||
|
||||
class TestTxSafety:
|
||||
@@ -157,51 +157,55 @@ class TestTxSafety:
|
||||
def test_validate_tx_frequency_out_of_band(self):
|
||||
result = SubGhzManager.validate_tx_frequency(100000000) # 100 MHz
|
||||
assert result is not None
|
||||
assert 'outside allowed TX bands' in result
|
||||
assert "outside allowed TX bands" in result
|
||||
|
||||
def test_validate_tx_frequency_between_bands(self):
|
||||
result = SubGhzManager.validate_tx_frequency(500000000) # 500 MHz
|
||||
assert result is not None
|
||||
|
||||
def test_transmit_no_hackrf(self, manager):
|
||||
with patch('shutil.which', return_value=None), \
|
||||
patch('utils.subghz.get_tool_path', return_value=None):
|
||||
manager._hackrf_available = None
|
||||
result = manager.transmit(capture_id='abc123')
|
||||
assert result['status'] == 'error'
|
||||
def test_transmit_no_hackrf(self, manager):
|
||||
with patch("shutil.which", return_value=None), patch("utils.subghz.get_tool_path", return_value=None):
|
||||
manager._hackrf_available = None
|
||||
result = manager.transmit(capture_id="abc123")
|
||||
assert result["status"] == "error"
|
||||
|
||||
def test_transmit_capture_not_found(self, manager):
|
||||
with patch('shutil.which', return_value='/usr/bin/hackrf_transfer'), \
|
||||
patch.object(manager, 'check_hackrf_device', return_value=True):
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/hackrf_transfer"),
|
||||
patch.object(manager, "check_hackrf_device", return_value=True),
|
||||
):
|
||||
manager._hackrf_available = None
|
||||
result = manager.transmit(capture_id='nonexistent')
|
||||
assert result['status'] == 'error'
|
||||
assert 'not found' in result['message']
|
||||
result = manager.transmit(capture_id="nonexistent")
|
||||
assert result["status"] == "error"
|
||||
assert "not found" in result["message"]
|
||||
|
||||
def test_transmit_out_of_band_rejected(self, manager, tmp_data_dir):
|
||||
# Create a capture with out-of-band frequency
|
||||
meta = {
|
||||
'id': 'test123',
|
||||
'filename': 'test.iq',
|
||||
'frequency_hz': 100000000, # 100 MHz - out of ISM
|
||||
'sample_rate': 2000000,
|
||||
'lna_gain': 32,
|
||||
'vga_gain': 20,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
"id": "test123",
|
||||
"filename": "test.iq",
|
||||
"frequency_hz": 100000000, # 100 MHz - out of ISM
|
||||
"sample_rate": 2000000,
|
||||
"lna_gain": 32,
|
||||
"vga_gain": 20,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
}
|
||||
meta_path = tmp_data_dir / 'captures' / 'test.json'
|
||||
meta_path = tmp_data_dir / "captures" / "test.json"
|
||||
meta_path.write_text(json.dumps(meta))
|
||||
(tmp_data_dir / 'captures' / 'test.iq').write_bytes(b'\x00' * 100)
|
||||
(tmp_data_dir / "captures" / "test.iq").write_bytes(b"\x00" * 100)
|
||||
|
||||
with patch('shutil.which', return_value='/usr/bin/hackrf_transfer'), \
|
||||
patch.object(manager, 'check_hackrf_device', return_value=True):
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/hackrf_transfer"),
|
||||
patch.object(manager, "check_hackrf_device", return_value=True),
|
||||
):
|
||||
manager._hackrf_available = None
|
||||
result = manager.transmit(capture_id='test123')
|
||||
assert result['status'] == 'error'
|
||||
assert 'outside allowed TX bands' in result['message']
|
||||
result = manager.transmit(capture_id="test123")
|
||||
assert result["status"] == "error"
|
||||
assert "outside allowed TX bands" in result["message"]
|
||||
|
||||
def test_transmit_already_running(self, manager, tmp_data_dir):
|
||||
import time as _time
|
||||
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.poll.return_value = None
|
||||
manager._rx_process = mock_proc
|
||||
@@ -211,59 +215,61 @@ class TestTxSafety:
|
||||
manager._hackrf_device_cache_ts = _time.time()
|
||||
# Capture lookup also runs pre-lock now; provide a valid capture + IQ file
|
||||
meta = {
|
||||
'id': 'test123',
|
||||
'filename': 'test.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 2000000,
|
||||
'timestamp': '2025-01-01T00:00:00',
|
||||
"id": "test123",
|
||||
"filename": "test.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 2000000,
|
||||
"timestamp": "2025-01-01T00:00:00",
|
||||
}
|
||||
(tmp_data_dir / 'captures' / 'test.json').write_text(json.dumps(meta))
|
||||
(tmp_data_dir / 'captures' / 'test.iq').write_bytes(b'\x00' * 64)
|
||||
(tmp_data_dir / "captures" / "test.json").write_text(json.dumps(meta))
|
||||
(tmp_data_dir / "captures" / "test.iq").write_bytes(b"\x00" * 64)
|
||||
|
||||
result = manager.transmit(capture_id='test123')
|
||||
assert result['status'] == 'error'
|
||||
assert 'Already running' in result['message']
|
||||
result = manager.transmit(capture_id="test123")
|
||||
assert result["status"] == "error"
|
||||
assert "Already running" in result["message"]
|
||||
|
||||
def test_transmit_segment_extracts_range(self, manager, tmp_data_dir):
|
||||
meta = {
|
||||
'id': 'seg001',
|
||||
'filename': 'seg.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 1000,
|
||||
'lna_gain': 24,
|
||||
'vga_gain': 20,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
'duration_seconds': 1.0,
|
||||
'size_bytes': 2000,
|
||||
"id": "seg001",
|
||||
"filename": "seg.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 1000,
|
||||
"lna_gain": 24,
|
||||
"vga_gain": 20,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"duration_seconds": 1.0,
|
||||
"size_bytes": 2000,
|
||||
}
|
||||
(tmp_data_dir / 'captures' / 'seg.json').write_text(json.dumps(meta))
|
||||
(tmp_data_dir / 'captures' / 'seg.iq').write_bytes(bytes(range(200)) * 10)
|
||||
(tmp_data_dir / "captures" / "seg.json").write_text(json.dumps(meta))
|
||||
(tmp_data_dir / "captures" / "seg.iq").write_bytes(bytes(range(200)) * 10)
|
||||
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.poll.return_value = None
|
||||
mock_timer = MagicMock()
|
||||
mock_timer.start = MagicMock()
|
||||
|
||||
with patch('shutil.which', return_value='/usr/bin/hackrf_transfer'), \
|
||||
patch.object(manager, 'check_hackrf_device', return_value=True), \
|
||||
patch('subprocess.Popen', return_value=mock_proc), \
|
||||
patch('utils.subghz.register_process'), \
|
||||
patch('threading.Timer', return_value=mock_timer), \
|
||||
patch('threading.Thread') as mock_thread_cls:
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/hackrf_transfer"),
|
||||
patch.object(manager, "check_hackrf_device", return_value=True),
|
||||
patch("subprocess.Popen", return_value=mock_proc),
|
||||
patch("utils.subghz.register_process"),
|
||||
patch("threading.Timer", return_value=mock_timer),
|
||||
patch("threading.Thread") as mock_thread_cls,
|
||||
):
|
||||
mock_thread = MagicMock()
|
||||
mock_thread.start = MagicMock()
|
||||
mock_thread_cls.return_value = mock_thread
|
||||
|
||||
manager._hackrf_available = None
|
||||
result = manager.transmit(
|
||||
capture_id='seg001',
|
||||
capture_id="seg001",
|
||||
start_seconds=0.2,
|
||||
duration_seconds=0.3,
|
||||
)
|
||||
|
||||
assert result['status'] == 'transmitting'
|
||||
assert result['segment'] is not None
|
||||
assert result['segment']['duration_seconds'] == pytest.approx(0.3, abs=0.01)
|
||||
assert result["status"] == "transmitting"
|
||||
assert result["segment"] is not None
|
||||
assert result["segment"]["duration_seconds"] == pytest.approx(0.3, abs=0.01)
|
||||
assert manager._tx_temp_file is not None
|
||||
assert manager._tx_temp_file.exists()
|
||||
|
||||
@@ -275,262 +281,281 @@ class TestCaptureLibrary:
|
||||
|
||||
def test_list_captures_with_data(self, manager, tmp_data_dir):
|
||||
meta = {
|
||||
'id': 'cap001',
|
||||
'filename': 'test.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 2000000,
|
||||
'lna_gain': 32,
|
||||
'vga_gain': 20,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
'duration_seconds': 5.0,
|
||||
'size_bytes': 1024,
|
||||
'label': 'test capture',
|
||||
"id": "cap001",
|
||||
"filename": "test.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 2000000,
|
||||
"lna_gain": 32,
|
||||
"vga_gain": 20,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"duration_seconds": 5.0,
|
||||
"size_bytes": 1024,
|
||||
"label": "test capture",
|
||||
}
|
||||
(tmp_data_dir / 'captures' / 'test.json').write_text(json.dumps(meta))
|
||||
(tmp_data_dir / "captures" / "test.json").write_text(json.dumps(meta))
|
||||
|
||||
captures = manager.list_captures()
|
||||
assert len(captures) == 1
|
||||
assert captures[0].capture_id == 'cap001'
|
||||
assert captures[0].label == 'test capture'
|
||||
assert captures[0].capture_id == "cap001"
|
||||
assert captures[0].label == "test capture"
|
||||
|
||||
def test_get_capture(self, manager, tmp_data_dir):
|
||||
meta = {
|
||||
'id': 'cap002',
|
||||
'filename': 'test2.iq',
|
||||
'frequency_hz': 315000000,
|
||||
'sample_rate': 2000000,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
"id": "cap002",
|
||||
"filename": "test2.iq",
|
||||
"frequency_hz": 315000000,
|
||||
"sample_rate": 2000000,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
}
|
||||
(tmp_data_dir / 'captures' / 'test2.json').write_text(json.dumps(meta))
|
||||
(tmp_data_dir / "captures" / "test2.json").write_text(json.dumps(meta))
|
||||
|
||||
cap = manager.get_capture('cap002')
|
||||
cap = manager.get_capture("cap002")
|
||||
assert cap is not None
|
||||
assert cap.frequency_hz == 315000000
|
||||
|
||||
def test_get_capture_not_found(self, manager):
|
||||
cap = manager.get_capture('nonexistent')
|
||||
cap = manager.get_capture("nonexistent")
|
||||
assert cap is None
|
||||
|
||||
def test_delete_capture(self, manager, tmp_data_dir):
|
||||
captures_dir = tmp_data_dir / 'captures'
|
||||
iq_path = captures_dir / 'delete_me.iq'
|
||||
meta_path = captures_dir / 'delete_me.json'
|
||||
iq_path.write_bytes(b'\x00' * 100)
|
||||
meta_path.write_text(json.dumps({
|
||||
'id': 'del001',
|
||||
'filename': 'delete_me.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 2000000,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
}))
|
||||
captures_dir = tmp_data_dir / "captures"
|
||||
iq_path = captures_dir / "delete_me.iq"
|
||||
meta_path = captures_dir / "delete_me.json"
|
||||
iq_path.write_bytes(b"\x00" * 100)
|
||||
meta_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "del001",
|
||||
"filename": "delete_me.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 2000000,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assert manager.delete_capture('del001') is True
|
||||
assert manager.delete_capture("del001") is True
|
||||
assert not iq_path.exists()
|
||||
assert not meta_path.exists()
|
||||
|
||||
def test_delete_capture_not_found(self, manager):
|
||||
assert manager.delete_capture('nonexistent') is False
|
||||
assert manager.delete_capture("nonexistent") is False
|
||||
|
||||
def test_update_label(self, manager, tmp_data_dir):
|
||||
meta = {
|
||||
'id': 'lbl001',
|
||||
'filename': 'label_test.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 2000000,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
'label': '',
|
||||
"id": "lbl001",
|
||||
"filename": "label_test.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 2000000,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"label": "",
|
||||
}
|
||||
meta_path = tmp_data_dir / 'captures' / 'label_test.json'
|
||||
meta_path = tmp_data_dir / "captures" / "label_test.json"
|
||||
meta_path.write_text(json.dumps(meta))
|
||||
|
||||
assert manager.update_capture_label('lbl001', 'Garage Remote') is True
|
||||
assert manager.update_capture_label("lbl001", "Garage Remote") is True
|
||||
|
||||
updated = json.loads(meta_path.read_text())
|
||||
assert updated['label'] == 'Garage Remote'
|
||||
assert updated['label_source'] == 'manual'
|
||||
assert updated["label"] == "Garage Remote"
|
||||
assert updated["label_source"] == "manual"
|
||||
|
||||
def test_update_label_not_found(self, manager):
|
||||
assert manager.update_capture_label('nonexistent', 'test') is False
|
||||
assert manager.update_capture_label("nonexistent", "test") is False
|
||||
|
||||
def test_get_capture_path(self, manager, tmp_data_dir):
|
||||
captures_dir = tmp_data_dir / 'captures'
|
||||
iq_path = captures_dir / 'path_test.iq'
|
||||
iq_path.write_bytes(b'\x00' * 100)
|
||||
(captures_dir / 'path_test.json').write_text(json.dumps({
|
||||
'id': 'pth001',
|
||||
'filename': 'path_test.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 2000000,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
}))
|
||||
captures_dir = tmp_data_dir / "captures"
|
||||
iq_path = captures_dir / "path_test.iq"
|
||||
iq_path.write_bytes(b"\x00" * 100)
|
||||
(captures_dir / "path_test.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "pth001",
|
||||
"filename": "path_test.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 2000000,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
path = manager.get_capture_path('pth001')
|
||||
path = manager.get_capture_path("pth001")
|
||||
assert path is not None
|
||||
assert path.name == 'path_test.iq'
|
||||
assert path.name == "path_test.iq"
|
||||
|
||||
def test_get_capture_path_not_found(self, manager):
|
||||
assert manager.get_capture_path('nonexistent') is None
|
||||
assert manager.get_capture_path("nonexistent") is None
|
||||
|
||||
def test_trim_capture_manual_segment(self, manager, tmp_data_dir):
|
||||
captures_dir = tmp_data_dir / 'captures'
|
||||
iq_path = captures_dir / 'trim_src.iq'
|
||||
captures_dir = tmp_data_dir / "captures"
|
||||
iq_path = captures_dir / "trim_src.iq"
|
||||
iq_path.write_bytes(bytes(range(200)) * 20) # 4000 bytes at 1000 sps => 2.0s
|
||||
(captures_dir / 'trim_src.json').write_text(json.dumps({
|
||||
'id': 'trim001',
|
||||
'filename': 'trim_src.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 1000,
|
||||
'lna_gain': 24,
|
||||
'vga_gain': 20,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
'duration_seconds': 2.0,
|
||||
'size_bytes': 4000,
|
||||
'label': 'Weather Burst',
|
||||
'bursts': [
|
||||
(captures_dir / "trim_src.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
'start_seconds': 0.55,
|
||||
'duration_seconds': 0.2,
|
||||
'peak_level': 67,
|
||||
'fingerprint': 'abc123',
|
||||
'modulation_hint': 'OOK/ASK',
|
||||
'modulation_confidence': 0.9,
|
||||
"id": "trim001",
|
||||
"filename": "trim_src.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 1000,
|
||||
"lna_gain": 24,
|
||||
"vga_gain": 20,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"duration_seconds": 2.0,
|
||||
"size_bytes": 4000,
|
||||
"label": "Weather Burst",
|
||||
"bursts": [
|
||||
{
|
||||
"start_seconds": 0.55,
|
||||
"duration_seconds": 0.2,
|
||||
"peak_level": 67,
|
||||
"fingerprint": "abc123",
|
||||
"modulation_hint": "OOK/ASK",
|
||||
"modulation_confidence": 0.9,
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
result = manager.trim_capture(
|
||||
capture_id='trim001',
|
||||
capture_id="trim001",
|
||||
start_seconds=0.5,
|
||||
duration_seconds=0.4,
|
||||
)
|
||||
|
||||
assert result['status'] == 'ok'
|
||||
assert result['capture']['id'] != 'trim001'
|
||||
assert result['capture']['size_bytes'] == 800
|
||||
assert result['capture']['label'].endswith('(Trim)')
|
||||
trimmed_iq = captures_dir / result['capture']['filename']
|
||||
assert result["status"] == "ok"
|
||||
assert result["capture"]["id"] != "trim001"
|
||||
assert result["capture"]["size_bytes"] == 800
|
||||
assert result["capture"]["label"].endswith("(Trim)")
|
||||
trimmed_iq = captures_dir / result["capture"]["filename"]
|
||||
assert trimmed_iq.exists()
|
||||
trimmed_meta = trimmed_iq.with_suffix('.json')
|
||||
trimmed_meta = trimmed_iq.with_suffix(".json")
|
||||
assert trimmed_meta.exists()
|
||||
|
||||
def test_trim_capture_auto_burst(self, manager, tmp_data_dir):
|
||||
captures_dir = tmp_data_dir / 'captures'
|
||||
iq_path = captures_dir / 'auto_src.iq'
|
||||
captures_dir = tmp_data_dir / "captures"
|
||||
iq_path = captures_dir / "auto_src.iq"
|
||||
iq_path.write_bytes(bytes(range(100)) * 40) # 4000 bytes
|
||||
(captures_dir / 'auto_src.json').write_text(json.dumps({
|
||||
'id': 'trim002',
|
||||
'filename': 'auto_src.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 1000,
|
||||
'lna_gain': 24,
|
||||
'vga_gain': 20,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
'duration_seconds': 2.0,
|
||||
'size_bytes': 4000,
|
||||
'bursts': [
|
||||
{'start_seconds': 0.2, 'duration_seconds': 0.1, 'peak_level': 12},
|
||||
{'start_seconds': 1.2, 'duration_seconds': 0.25, 'peak_level': 88},
|
||||
],
|
||||
}))
|
||||
(captures_dir / "auto_src.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "trim002",
|
||||
"filename": "auto_src.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 1000,
|
||||
"lna_gain": 24,
|
||||
"vga_gain": 20,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"duration_seconds": 2.0,
|
||||
"size_bytes": 4000,
|
||||
"bursts": [
|
||||
{"start_seconds": 0.2, "duration_seconds": 0.1, "peak_level": 12},
|
||||
{"start_seconds": 1.2, "duration_seconds": 0.25, "peak_level": 88},
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
result = manager.trim_capture(capture_id='trim002')
|
||||
assert result['status'] == 'ok'
|
||||
assert result['segment']['auto_selected'] is True
|
||||
assert result['capture']['duration_seconds'] > 0.25
|
||||
result = manager.trim_capture(capture_id="trim002")
|
||||
assert result["status"] == "ok"
|
||||
assert result["segment"]["auto_selected"] is True
|
||||
assert result["capture"]["duration_seconds"] > 0.25
|
||||
|
||||
def test_list_captures_groups_same_fingerprint(self, manager, tmp_data_dir):
|
||||
cap_a = {
|
||||
'id': 'grp001',
|
||||
'filename': 'a.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 2000000,
|
||||
'timestamp': '2026-01-01T00:00:00Z',
|
||||
'dominant_fingerprint': 'deadbeefcafebabe',
|
||||
"id": "grp001",
|
||||
"filename": "a.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 2000000,
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"dominant_fingerprint": "deadbeefcafebabe",
|
||||
}
|
||||
cap_b = {
|
||||
'id': 'grp002',
|
||||
'filename': 'b.iq',
|
||||
'frequency_hz': 433920000,
|
||||
'sample_rate': 2000000,
|
||||
'timestamp': '2026-01-01T00:01:00Z',
|
||||
'dominant_fingerprint': 'deadbeefcafebabe',
|
||||
"id": "grp002",
|
||||
"filename": "b.iq",
|
||||
"frequency_hz": 433920000,
|
||||
"sample_rate": 2000000,
|
||||
"timestamp": "2026-01-01T00:01:00Z",
|
||||
"dominant_fingerprint": "deadbeefcafebabe",
|
||||
}
|
||||
(tmp_data_dir / 'captures' / 'a.json').write_text(json.dumps(cap_a))
|
||||
(tmp_data_dir / 'captures' / 'b.json').write_text(json.dumps(cap_b))
|
||||
(tmp_data_dir / "captures" / "a.json").write_text(json.dumps(cap_a))
|
||||
(tmp_data_dir / "captures" / "b.json").write_text(json.dumps(cap_b))
|
||||
|
||||
captures = manager.list_captures()
|
||||
assert len(captures) == 2
|
||||
assert all(c.fingerprint_group.startswith('SIG-') for c in captures)
|
||||
assert all(c.fingerprint_group.startswith("SIG-") for c in captures)
|
||||
assert all(c.fingerprint_group_size == 2 for c in captures)
|
||||
|
||||
|
||||
class TestSweep:
|
||||
def test_start_sweep_no_tool(self, manager):
|
||||
with patch('shutil.which', return_value=None), \
|
||||
patch('utils.subghz.get_tool_path', return_value=None):
|
||||
manager._sweep_available = None
|
||||
result = manager.start_sweep()
|
||||
assert result['status'] == 'error'
|
||||
class TestSweep:
|
||||
def test_start_sweep_no_tool(self, manager):
|
||||
with patch("shutil.which", return_value=None), patch("utils.subghz.get_tool_path", return_value=None):
|
||||
manager._sweep_available = None
|
||||
result = manager.start_sweep()
|
||||
assert result["status"] == "error"
|
||||
|
||||
def test_start_sweep_success(self, manager):
|
||||
import time as _time
|
||||
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.poll.return_value = None
|
||||
mock_proc.stdout = MagicMock()
|
||||
|
||||
with patch('shutil.which', return_value='/usr/bin/hackrf_sweep'), \
|
||||
patch('subprocess.Popen', return_value=mock_proc), \
|
||||
patch('utils.subghz.register_process'):
|
||||
with (
|
||||
patch("shutil.which", return_value="/usr/bin/hackrf_sweep"),
|
||||
patch("subprocess.Popen", return_value=mock_proc),
|
||||
patch("utils.subghz.register_process"),
|
||||
):
|
||||
manager._sweep_available = None
|
||||
manager._hackrf_device_cache = True
|
||||
manager._hackrf_device_cache_ts = _time.time()
|
||||
result = manager.start_sweep(freq_start_mhz=300, freq_end_mhz=928)
|
||||
assert result['status'] == 'started'
|
||||
assert result["status"] == "started"
|
||||
|
||||
# Signal daemon threads to stop so they don't outlive the test
|
||||
manager._sweep_running = False
|
||||
|
||||
def test_stop_sweep_not_running(self, manager):
|
||||
result = manager.stop_sweep()
|
||||
assert result['status'] == 'not_running'
|
||||
assert result["status"] == "not_running"
|
||||
|
||||
|
||||
class TestDecode:
|
||||
def test_start_decode_no_hackrf(self, manager):
|
||||
with patch('shutil.which', return_value=None), \
|
||||
patch('utils.subghz.get_tool_path', return_value=None):
|
||||
manager._hackrf_available = None
|
||||
manager._rtl433_available = None
|
||||
result = manager.start_decode(frequency_hz=433920000)
|
||||
assert result['status'] == 'error'
|
||||
assert 'hackrf_transfer' in result['message']
|
||||
class TestDecode:
|
||||
def test_start_decode_no_hackrf(self, manager):
|
||||
with patch("shutil.which", return_value=None), patch("utils.subghz.get_tool_path", return_value=None):
|
||||
manager._hackrf_available = None
|
||||
manager._rtl433_available = None
|
||||
result = manager.start_decode(frequency_hz=433920000)
|
||||
assert result["status"] == "error"
|
||||
assert "hackrf_transfer" in result["message"]
|
||||
|
||||
def test_start_decode_no_rtl433(self, manager):
|
||||
def which_side_effect(name):
|
||||
if name == 'hackrf_transfer':
|
||||
return '/usr/bin/hackrf_transfer'
|
||||
if name == "hackrf_transfer":
|
||||
return "/usr/bin/hackrf_transfer"
|
||||
return None
|
||||
|
||||
with patch('shutil.which', side_effect=which_side_effect), \
|
||||
patch('utils.subghz.get_tool_path', return_value=None):
|
||||
manager._hackrf_available = None
|
||||
manager._rtl433_available = None
|
||||
result = manager.start_decode(frequency_hz=433920000)
|
||||
assert result['status'] == 'error'
|
||||
assert 'rtl_433' in result['message']
|
||||
with (
|
||||
patch("shutil.which", side_effect=which_side_effect),
|
||||
patch("utils.subghz.get_tool_path", return_value=None),
|
||||
):
|
||||
manager._hackrf_available = None
|
||||
manager._rtl433_available = None
|
||||
result = manager.start_decode(frequency_hz=433920000)
|
||||
assert result["status"] == "error"
|
||||
assert "rtl_433" in result["message"]
|
||||
|
||||
def test_start_decode_success(self, manager):
|
||||
mock_hackrf_proc = MagicMock()
|
||||
mock_hackrf_proc.poll.return_value = None
|
||||
mock_hackrf_proc.stdout = MagicMock()
|
||||
mock_hackrf_proc.stderr = MagicMock()
|
||||
mock_hackrf_proc.stderr.readline = MagicMock(return_value=b'')
|
||||
mock_hackrf_proc.stderr.readline = MagicMock(return_value=b"")
|
||||
|
||||
mock_rtl433_proc = MagicMock()
|
||||
mock_rtl433_proc.poll.return_value = None
|
||||
mock_rtl433_proc.stdout = MagicMock()
|
||||
mock_rtl433_proc.stderr = MagicMock()
|
||||
mock_rtl433_proc.stderr.readline = MagicMock(return_value=b'')
|
||||
mock_rtl433_proc.stderr.readline = MagicMock(return_value=b"")
|
||||
|
||||
call_count = [0]
|
||||
|
||||
@@ -540,13 +565,16 @@ class TestDecode:
|
||||
return mock_hackrf_proc
|
||||
return mock_rtl433_proc
|
||||
|
||||
def which_side_effect(name):
|
||||
return f'/usr/bin/{name}'
|
||||
|
||||
with patch('shutil.which', side_effect=which_side_effect), \
|
||||
patch('subprocess.Popen', side_effect=popen_side_effect) as mock_popen, \
|
||||
patch('utils.subghz.register_process'):
|
||||
def which_side_effect(name):
|
||||
return f"/usr/bin/{name}"
|
||||
|
||||
with (
|
||||
patch("shutil.which", side_effect=which_side_effect),
|
||||
patch("subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
patch("utils.subghz.register_process"),
|
||||
):
|
||||
import time as _time
|
||||
|
||||
manager._hackrf_available = None
|
||||
manager._rtl433_available = None
|
||||
manager._hackrf_device_cache = True
|
||||
@@ -555,23 +583,23 @@ class TestDecode:
|
||||
frequency_hz=433920000,
|
||||
sample_rate=2000000,
|
||||
)
|
||||
assert result['status'] == 'started'
|
||||
assert result['frequency_hz'] == 433920000
|
||||
assert manager.active_mode == 'decode'
|
||||
assert result["status"] == "started"
|
||||
assert result["frequency_hz"] == 433920000
|
||||
assert manager.active_mode == "decode"
|
||||
|
||||
# Two processes: hackrf_transfer + rtl_433
|
||||
assert mock_popen.call_count == 2
|
||||
|
||||
# Verify hackrf_transfer command
|
||||
hackrf_cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert os.path.basename(hackrf_cmd[0]) == 'hackrf_transfer'
|
||||
assert '-r' in hackrf_cmd
|
||||
|
||||
# Verify rtl_433 command
|
||||
rtl433_cmd = mock_popen.call_args_list[1][0][0]
|
||||
assert os.path.basename(rtl433_cmd[0]) == 'rtl_433'
|
||||
assert '-r' in rtl433_cmd
|
||||
assert 'cs8:-' in rtl433_cmd
|
||||
# Verify hackrf_transfer command
|
||||
hackrf_cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert os.path.basename(hackrf_cmd[0]) == "hackrf_transfer"
|
||||
assert "-r" in hackrf_cmd
|
||||
|
||||
# Verify rtl_433 command
|
||||
rtl433_cmd = mock_popen.call_args_list[1][0][0]
|
||||
assert os.path.basename(rtl433_cmd[0]) == "rtl_433"
|
||||
assert "-r" in rtl433_cmd
|
||||
assert "cs8:-" in rtl433_cmd
|
||||
|
||||
# Both processes tracked
|
||||
assert manager._decode_hackrf_process is mock_hackrf_proc
|
||||
@@ -582,7 +610,7 @@ class TestDecode:
|
||||
|
||||
def test_stop_decode_not_running(self, manager):
|
||||
result = manager.stop_decode()
|
||||
assert result['status'] == 'not_running'
|
||||
assert result["status"] == "not_running"
|
||||
|
||||
def test_stop_decode_terminates_both(self, manager):
|
||||
mock_hackrf = MagicMock()
|
||||
@@ -594,11 +622,10 @@ class TestDecode:
|
||||
manager._decode_process = mock_rtl433
|
||||
manager._decode_frequency_hz = 433920000
|
||||
|
||||
with patch('utils.subghz.safe_terminate') as mock_term, \
|
||||
patch('utils.subghz.unregister_process'):
|
||||
with patch("utils.subghz.safe_terminate") as mock_term, patch("utils.subghz.unregister_process"):
|
||||
result = manager.stop_decode()
|
||||
|
||||
assert result['status'] == 'stopped'
|
||||
assert result["status"] == "stopped"
|
||||
assert manager._decode_hackrf_process is None
|
||||
assert manager._decode_process is None
|
||||
assert mock_term.call_count == 2
|
||||
@@ -610,7 +637,7 @@ class TestStopAll:
|
||||
mock_proc.poll.return_value = None
|
||||
manager._rx_process = mock_proc
|
||||
|
||||
with patch('utils.subghz.safe_terminate'):
|
||||
with patch("utils.subghz.safe_terminate"):
|
||||
manager.stop_all()
|
||||
|
||||
assert manager._rx_process is None
|
||||
@@ -623,18 +650,18 @@ class TestStopAll:
|
||||
class TestSubGhzCapture:
|
||||
def test_to_dict(self):
|
||||
cap = SubGhzCapture(
|
||||
capture_id='abc123',
|
||||
filename='test.iq',
|
||||
capture_id="abc123",
|
||||
filename="test.iq",
|
||||
frequency_hz=433920000,
|
||||
sample_rate=2000000,
|
||||
lna_gain=32,
|
||||
vga_gain=20,
|
||||
timestamp='2026-01-01T00:00:00Z',
|
||||
timestamp="2026-01-01T00:00:00Z",
|
||||
duration_seconds=5.0,
|
||||
size_bytes=1024,
|
||||
label='Test',
|
||||
label="Test",
|
||||
)
|
||||
d = cap.to_dict()
|
||||
assert d['id'] == 'abc123'
|
||||
assert d['frequency_hz'] == 433920000
|
||||
assert d['label'] == 'Test'
|
||||
assert d["id"] == "abc123"
|
||||
assert d["frequency_hz"] == 433920000
|
||||
assert d["label"] == "Test"
|
||||
|
||||
Reference in New Issue
Block a user