From 28185727e31ac9cb64cef0c8dccac41c22cf6c7b Mon Sep 17 00:00:00 2001 From: Smittix Date: Sun, 8 Feb 2026 15:48:57 +0000 Subject: [PATCH] Fix grgsm_scanner output parser to match real output format Parser expected pipe-delimited table rows but grgsm_scanner outputs comma-separated key-value pairs like: ARFCN: 975, Freq: 925.2M, CID: 13522, LAC: 38722, MCC: 262, MNC: 1, Pwr: -58 This was the root cause of no data appearing in GSM Spy. Co-Authored-By: Claude Opus 4.6 --- routes/gsm_spy.py | 59 ++++++++++++++++++++----------------------- tests/test_gsm_spy.py | 53 +++++++++++++++++++------------------- 2 files changed, 55 insertions(+), 57 deletions(-) diff --git a/routes/gsm_spy.py b/routes/gsm_spy.py index 5e4fc74..4cacc2f 100644 --- a/routes/gsm_spy.py +++ b/routes/gsm_spy.py @@ -1096,43 +1096,40 @@ def traffic_correlation(): def parse_grgsm_scanner_output(line: str) -> dict[str, Any] | None: """Parse grgsm_scanner output line. - Actual output format is a table: - ARFCN | Freq (MHz) | CID | LAC | MCC | MNC | Power (dB) - -------------------------------------------------------------------- - 23 | 940.6 | 31245 | 1234 | 214 | 01 | -48 + Actual output format (comma-separated key-value pairs): + ARFCN: 975, Freq: 925.2M, CID: 13522, LAC: 38722, MCC: 262, MNC: 1, Pwr: -58 """ try: - # Skip progress, header, and separator lines - if 'Scanning:' in line or 'ARFCN' in line or '---' in line or 'Found' in line: + line = line.strip() + + # Skip non-data lines (progress, config, neighbour info, blank) + if not line or 'ARFCN:' not in line: return None - # Parse table row: " 23 | 940.6 | 31245 | 1234 | 214 | 01 | -48" - # Split by pipe and clean whitespace - parts = [p.strip() for p in line.split('|')] + # Parse "ARFCN: 975, Freq: 925.2M, CID: 13522, LAC: 38722, MCC: 262, MNC: 1, Pwr: -58" + fields = {} + for part in line.split(','): + part = part.strip() + if ':' in part: + key, _, value = part.partition(':') + fields[key.strip()] = value.strip() - if len(parts) >= 7: - arfcn = parts[0] - freq = parts[1] - cid = parts[2] - lac = parts[3] - mcc = parts[4] - mnc = parts[5] - power = parts[6] + if 'ARFCN' in fields and 'CID' in fields: + # Freq may have 'M' suffix (e.g. "925.2M") + freq_str = fields.get('Freq', '0').rstrip('Mm') - # Validate that we have numeric data (not header line) - if arfcn.isdigit(): - data = { - 'type': 'tower', - 'arfcn': int(arfcn), - 'frequency': float(freq), - 'cid': int(cid), - 'lac': int(lac), - 'mcc': int(mcc), - 'mnc': int(mnc), - 'signal_strength': float(power), - 'timestamp': datetime.now().isoformat() - } - return data + data = { + 'type': 'tower', + 'arfcn': int(fields['ARFCN']), + 'frequency': float(freq_str), + 'cid': int(fields.get('CID', 0)), + 'lac': int(fields.get('LAC', 0)), + 'mcc': int(fields.get('MCC', 0)), + 'mnc': int(fields.get('MNC', 0)), + 'signal_strength': float(fields.get('Pwr', -999)), + 'timestamp': datetime.now().isoformat() + } + return data except Exception as e: logger.debug(f"Failed to parse scanner line: {line} - {e}") diff --git a/tests/test_gsm_spy.py b/tests/test_gsm_spy.py index 92deb40..76c1bdf 100644 --- a/tests/test_gsm_spy.py +++ b/tests/test_gsm_spy.py @@ -13,9 +13,9 @@ from routes.gsm_spy import ( class TestParseGrgsmScannerOutput: """Tests for parse_grgsm_scanner_output().""" - def test_valid_table_row(self): - """Test parsing a valid scanner output table row.""" - line = " 23 | 940.6 | 31245 | 1234 | 214 | 01 | -48" + def test_valid_output_line(self): + """Test parsing a valid grgsm_scanner output line.""" + line = "ARFCN: 23, Freq: 940.6M, CID: 31245, LAC: 1234, MCC: 214, MNC: 01, Pwr: -48" result = parse_grgsm_scanner_output(line) assert result is not None @@ -29,33 +29,34 @@ class TestParseGrgsmScannerOutput: assert result['signal_strength'] == -48.0 assert 'timestamp' in result - def test_header_line(self): - """Test that header lines are skipped.""" - line = "ARFCN | Freq (MHz) | CID | LAC | MCC | MNC | Power (dB)" + def test_freq_without_suffix(self): + """Test parsing frequency without M suffix.""" + line = "ARFCN: 975, Freq: 925.2, CID: 13522, LAC: 38722, MCC: 262, MNC: 1, Pwr: -58" + result = parse_grgsm_scanner_output(line) + assert result is not None + assert result['frequency'] == 925.2 + + def test_config_line(self): + """Test that configuration lines are skipped.""" + line = " Configuration: 1 CCCH, not combined" result = parse_grgsm_scanner_output(line) assert result is None - def test_separator_line(self): - """Test that separator lines are skipped.""" - line = "--------------------------------------------------------------------" + def test_neighbour_line(self): + """Test that neighbour cell lines are skipped.""" + line = " Neighbour Cells: 57, 61, 70, 71, 72, 86" + result = parse_grgsm_scanner_output(line) + assert result is None + + def test_cell_arfcn_line(self): + """Test that cell ARFCN lines are skipped.""" + line = " Cell ARFCNs: 63, 76" result = parse_grgsm_scanner_output(line) assert result is None def test_progress_line(self): - """Test that progress lines are skipped.""" - line = "Scanning: 50% complete" - result = parse_grgsm_scanner_output(line) - assert result is None - - def test_found_line(self): - """Test that 'Found X towers' lines are skipped.""" - line = "Found 5 towers" - result = parse_grgsm_scanner_output(line) - assert result is None - - def test_invalid_data(self): - """Test handling of invalid data.""" - line = " abc | xyz | invalid | data | bad | bad | bad" + """Test that progress/status lines are skipped.""" + line = "Scanning GSM900 band..." result = parse_grgsm_scanner_output(line) assert result is None @@ -64,9 +65,9 @@ class TestParseGrgsmScannerOutput: result = parse_grgsm_scanner_output("") assert result is None - def test_partial_data(self): - """Test handling of incomplete table rows.""" - line = " 23 | 940.6 | 31245" # Missing fields + def test_invalid_data(self): + """Test handling of non-numeric values.""" + line = "ARFCN: abc, Freq: xyz, CID: bad, LAC: data, MCC: bad, MNC: bad, Pwr: bad" result = parse_grgsm_scanner_output(line) assert result is None