diff --git a/data/isms_presets.py b/data/isms_presets.py deleted file mode 100644 index 12eb388..0000000 --- a/data/isms_presets.py +++ /dev/null @@ -1,315 +0,0 @@ -""" -ISMS scan presets and band definitions. - -Defines frequency ranges and parameters for common RF monitoring scenarios. -""" - -from __future__ import annotations - -# Scan presets for common monitoring scenarios -ISMS_SCAN_PRESETS: dict[str, dict] = { - 'vhf_airband': { - 'name': 'VHF Airband', - 'description': 'Aviation communications 118-137 MHz', - 'freq_start': 118.0, - 'freq_end': 137.0, - 'bin_size': 25000, # 25 kHz channel spacing - 'integration': 1.0, - 'category': 'aviation', - }, - 'uhf_airband': { - 'name': 'UHF Airband', - 'description': 'Military aviation 225-400 MHz', - 'freq_start': 225.0, - 'freq_end': 400.0, - 'bin_size': 25000, - 'integration': 1.0, - 'category': 'aviation', - }, - 'uhf_pmr': { - 'name': 'UHF PMR446', - 'description': 'License-free radio 446 MHz', - 'freq_start': 446.0, - 'freq_end': 446.2, - 'bin_size': 12500, # 12.5 kHz channel spacing - 'integration': 0.5, - 'category': 'pmr', - }, - 'ism_433': { - 'name': 'ISM 433 MHz', - 'description': 'European ISM band (sensors, remotes)', - 'freq_start': 433.0, - 'freq_end': 434.8, - 'bin_size': 10000, - 'integration': 0.5, - 'category': 'ism', - }, - 'ism_868': { - 'name': 'ISM 868 MHz', - 'description': 'European ISM band (LoRa, smart meters)', - 'freq_start': 868.0, - 'freq_end': 870.0, - 'bin_size': 10000, - 'integration': 0.5, - 'category': 'ism', - }, - 'ism_915': { - 'name': 'ISM 915 MHz', - 'description': 'US ISM band', - 'freq_start': 902.0, - 'freq_end': 928.0, - 'bin_size': 50000, - 'integration': 1.0, - 'category': 'ism', - }, - 'wifi_2g': { - 'name': 'WiFi 2.4 GHz Vicinity', - 'description': 'WiFi band activity (requires wideband SDR)', - 'freq_start': 2400.0, - 'freq_end': 2500.0, - 'bin_size': 500000, - 'integration': 2.0, - 'category': 'wifi', - 'note': 'Requires SDR with 2.4 GHz capability (HackRF, LimeSDR)', - }, - 'cellular_700': { - 'name': 'Cellular 700 MHz', - 'description': 'LTE Bands 12/13/17/28 downlink', - 'freq_start': 728.0, - 'freq_end': 803.0, - 'bin_size': 100000, - 'integration': 1.0, - 'category': 'cellular', - }, - 'cellular_850': { - 'name': 'Cellular 850 MHz', - 'description': 'GSM/LTE Band 5 downlink', - 'freq_start': 869.0, - 'freq_end': 894.0, - 'bin_size': 100000, - 'integration': 1.0, - 'category': 'cellular', - }, - 'cellular_900': { - 'name': 'Cellular 900 MHz', - 'description': 'GSM/LTE Band 8 downlink (Europe)', - 'freq_start': 925.0, - 'freq_end': 960.0, - 'bin_size': 100000, - 'integration': 1.0, - 'category': 'cellular', - }, - 'cellular_1800': { - 'name': 'Cellular 1800 MHz', - 'description': 'GSM/LTE Band 3 downlink', - 'freq_start': 1805.0, - 'freq_end': 1880.0, - 'bin_size': 100000, - 'integration': 1.0, - 'category': 'cellular', - }, - 'full_sweep': { - 'name': 'Full Spectrum', - 'description': 'Complete 24 MHz - 1.7 GHz sweep', - 'freq_start': 24.0, - 'freq_end': 1700.0, - 'bin_size': 100000, - 'integration': 5.0, - 'category': 'full', - 'note': 'Takes 3-5 minutes to complete', - }, -} - - -# Cellular band definitions (downlink frequencies for reference) -CELLULAR_BANDS: dict[str, dict] = { - 'B1': { - 'name': 'UMTS/LTE Band 1', - 'dl_start': 2110, - 'dl_end': 2170, - 'ul_start': 1920, - 'ul_end': 1980, - 'duplex': 'FDD', - }, - 'B3': { - 'name': 'LTE Band 3', - 'dl_start': 1805, - 'dl_end': 1880, - 'ul_start': 1710, - 'ul_end': 1785, - 'duplex': 'FDD', - }, - 'B5': { - 'name': 'GSM/LTE Band 5', - 'dl_start': 869, - 'dl_end': 894, - 'ul_start': 824, - 'ul_end': 849, - 'duplex': 'FDD', - }, - 'B7': { - 'name': 'LTE Band 7', - 'dl_start': 2620, - 'dl_end': 2690, - 'ul_start': 2500, - 'ul_end': 2570, - 'duplex': 'FDD', - }, - 'B8': { - 'name': 'GSM/LTE Band 8', - 'dl_start': 925, - 'dl_end': 960, - 'ul_start': 880, - 'ul_end': 915, - 'duplex': 'FDD', - }, - 'B20': { - 'name': 'LTE Band 20', - 'dl_start': 791, - 'dl_end': 821, - 'ul_start': 832, - 'ul_end': 862, - 'duplex': 'FDD', - }, - 'B28': { - 'name': 'LTE Band 28', - 'dl_start': 758, - 'dl_end': 803, - 'ul_start': 703, - 'ul_end': 748, - 'duplex': 'FDD', - }, - 'B38': { - 'name': 'LTE Band 38 (TDD)', - 'dl_start': 2570, - 'dl_end': 2620, - 'duplex': 'TDD', - }, - 'B40': { - 'name': 'LTE Band 40 (TDD)', - 'dl_start': 2300, - 'dl_end': 2400, - 'duplex': 'TDD', - }, - 'n77': { - 'name': 'NR Band n77 (5G)', - 'dl_start': 3300, - 'dl_end': 4200, - 'duplex': 'TDD', - }, - 'n78': { - 'name': 'NR Band n78 (5G)', - 'dl_start': 3300, - 'dl_end': 3800, - 'duplex': 'TDD', - }, -} - - -# UK Mobile Network Operators (for PLMN identification) -UK_OPERATORS: dict[str, str] = { - '234-10': 'O2 UK', - '234-15': 'Vodafone UK', - '234-20': 'Three UK', - '234-30': 'EE', - '234-33': 'EE', - '234-34': 'EE', - '234-50': 'JT (Jersey)', - '234-55': 'Sure (Guernsey)', -} - - -# Common ISM band allocations -ISM_BANDS: dict[str, dict] = { - 'ism_6m': { - 'name': '6.78 MHz ISM', - 'start': 6.765, - 'end': 6.795, - 'region': 'Worldwide', - }, - 'ism_13m': { - 'name': '13.56 MHz ISM (NFC/RFID)', - 'start': 13.553, - 'end': 13.567, - 'region': 'Worldwide', - }, - 'ism_27m': { - 'name': '27 MHz ISM (CB)', - 'start': 26.957, - 'end': 27.283, - 'region': 'Worldwide', - }, - 'ism_40m': { - 'name': '40.68 MHz ISM', - 'start': 40.66, - 'end': 40.70, - 'region': 'Worldwide', - }, - 'ism_433': { - 'name': '433 MHz ISM', - 'start': 433.05, - 'end': 434.79, - 'region': 'ITU Region 1 (Europe)', - }, - 'ism_868': { - 'name': '868 MHz ISM', - 'start': 868.0, - 'end': 870.0, - 'region': 'Europe', - }, - 'ism_915': { - 'name': '915 MHz ISM', - 'start': 902.0, - 'end': 928.0, - 'region': 'Americas', - }, - 'ism_2400': { - 'name': '2.4 GHz ISM (WiFi/BT)', - 'start': 2400.0, - 'end': 2500.0, - 'region': 'Worldwide', - }, - 'ism_5800': { - 'name': '5.8 GHz ISM', - 'start': 5725.0, - 'end': 5875.0, - 'region': 'Worldwide', - }, -} - - -def get_preset(preset_name: str) -> dict | None: - """Get a scan preset by name.""" - return ISMS_SCAN_PRESETS.get(preset_name) - - -def get_presets_by_category(category: str) -> list[dict]: - """Get all presets in a category.""" - return [ - {**preset, 'id': name} - for name, preset in ISMS_SCAN_PRESETS.items() - if preset.get('category') == category - ] - - -def get_all_presets() -> list[dict]: - """Get all presets with their IDs.""" - return [ - {**preset, 'id': name} - for name, preset in ISMS_SCAN_PRESETS.items() - ] - - -def identify_band(freq_mhz: float) -> str | None: - """Identify which cellular band a frequency belongs to.""" - for band_id, band_info in CELLULAR_BANDS.items(): - dl_start = band_info.get('dl_start', 0) - dl_end = band_info.get('dl_end', 0) - if dl_start <= freq_mhz <= dl_end: - return band_id - return None - - -def identify_operator(plmn: str) -> str | None: - """Identify UK operator from PLMN code.""" - return UK_OPERATORS.get(plmn) diff --git a/routes/__init__.py b/routes/__init__.py index 3a017ed..b7960d1 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -15,7 +15,6 @@ def register_blueprints(app): from .correlation import correlation_bp from .listening_post import listening_post_bp from .tscm import tscm_bp, init_tscm_state - from .isms import isms_bp app.register_blueprint(pager_bp) app.register_blueprint(sensor_bp) @@ -30,7 +29,6 @@ def register_blueprints(app): app.register_blueprint(correlation_bp) app.register_blueprint(listening_post_bp) app.register_blueprint(tscm_bp) - app.register_blueprint(isms_bp) # Initialize TSCM state with queue and lock from app import app as app_module diff --git a/routes/isms.py b/routes/isms.py deleted file mode 100644 index a6b3126..0000000 --- a/routes/isms.py +++ /dev/null @@ -1,983 +0,0 @@ -"""ISMS Listening Station routes for spectrum monitoring and tower mapping.""" - -from __future__ import annotations - -import json -import queue -import shutil -import subprocess -import threading -import time -from datetime import datetime -from typing import Generator - -from flask import Blueprint, Response, jsonify, request - -from utils.logging import get_logger -from utils.sse import format_sse -from utils.constants import SSE_QUEUE_TIMEOUT, SSE_KEEPALIVE_INTERVAL -from utils.process import safe_terminate, register_process -from utils.gps import get_current_position -from utils.database import ( - create_isms_baseline, - get_isms_baseline, - get_all_isms_baselines, - get_active_isms_baseline, - set_active_isms_baseline, - delete_isms_baseline, - update_isms_baseline, - create_isms_scan, - update_isms_scan, - get_isms_scan, - get_recent_isms_scans, - add_isms_finding, - get_isms_findings, - get_isms_findings_summary, - acknowledge_isms_finding, -) -from utils.isms.spectrum import ( - run_rtl_power_scan, - compute_band_metrics, - detect_bursts, - get_rtl_power_path, - SpectrumBin, - BandMetrics, -) -from utils.isms.towers import ( - query_nearby_towers, - format_tower_info, - build_ofcom_coverage_url, - build_ofcom_emf_url, - get_opencellid_token, -) -from utils.isms.rules import RulesEngine, create_default_engine -from utils.isms.baseline import ( - BaselineRecorder, - compare_spectrum_baseline, - compare_tower_baseline, - save_baseline_to_db, -) -from utils.isms.gsm import ( - GsmCell, - run_grgsm_scan, - run_gsm_scan_blocking, - get_grgsm_scanner_path, - format_gsm_cell, - deduplicate_cells, - identify_gsm_anomalies, -) -from data.isms_presets import ( - ISMS_SCAN_PRESETS, - get_preset, - get_all_presets, - identify_band, -) - -logger = get_logger('intercept.isms') - -isms_bp = Blueprint('isms', __name__, url_prefix='/isms') - -# ============================================ -# GLOBAL STATE -# ============================================ - -# Scanner state -isms_thread: threading.Thread | None = None -isms_running = False -isms_lock = threading.Lock() -isms_process: subprocess.Popen | None = None -isms_current_scan_id: int | None = None - -# Scanner configuration -isms_config = { - 'preset': 'ism_433', - 'freq_start': 433.0, - 'freq_end': 434.8, - 'bin_size': 10000, - 'integration': 0.5, - 'device': 0, - 'gain': 40, - 'ppm': 0, - 'threshold': 50, # Activity threshold (0-100) - 'lat': None, - 'lon': None, - 'baseline_id': None, -} - -# SSE queue for real-time events -isms_queue: queue.Queue = queue.Queue(maxsize=100) - -# Rules engine -rules_engine: RulesEngine = create_default_engine() - -# Baseline recorder -baseline_recorder: BaselineRecorder = BaselineRecorder() - -# Recent band metrics for display -recent_metrics: dict[str, BandMetrics] = {} -metrics_lock = threading.Lock() - -# Findings count for current scan -current_findings_count = 0 - -# GSM scanner state -gsm_thread: threading.Thread | None = None -gsm_running = False -gsm_lock = threading.Lock() -gsm_detected_cells: list[GsmCell] = [] -gsm_baseline_cells: list[GsmCell] = [] - - -# ============================================ -# HELPER FUNCTIONS -# ============================================ - -def emit_event(event_type: str, data: dict) -> None: - """Emit an event to SSE queue.""" - try: - isms_queue.put_nowait({ - 'type': event_type, - **data - }) - except queue.Full: - pass - - -def emit_finding(severity: str, text: str, **details) -> None: - """Emit a finding event and store in database.""" - global current_findings_count - - emit_event('finding', { - 'severity': severity, - 'text': text, - 'details': details, - 'timestamp': datetime.utcnow().isoformat() + 'Z', - }) - - # Store in database if we have an active scan - if isms_current_scan_id: - add_isms_finding( - scan_id=isms_current_scan_id, - finding_type=details.get('finding_type', 'general'), - severity=severity, - description=text, - band=details.get('band'), - frequency=details.get('frequency'), - details=details, - ) - current_findings_count += 1 - - -def emit_meter(band: str, level: float, noise_floor: float) -> None: - """Emit a band meter update.""" - emit_event('meter', { - 'band': band, - 'level': min(100, max(0, level)), - 'noise_floor': noise_floor, - }) - - -def emit_peak(freq_mhz: float, power_db: float, band: str) -> None: - """Emit a spectrum peak event.""" - emit_event('spectrum_peak', { - 'freq_mhz': round(freq_mhz, 3), - 'power_db': round(power_db, 1), - 'band': band, - }) - - -def emit_status(state: str, **data) -> None: - """Emit a status update.""" - emit_event('status', { - 'state': state, - **data - }) - - -# ============================================ -# SCANNER LOOP -# ============================================ - -def isms_scan_loop() -> None: - """Main ISMS scanning loop.""" - global isms_running, isms_process, current_findings_count - - logger.info("ISMS scan thread started") - emit_status('starting') - - # Get preset configuration - preset_name = isms_config.get('preset', 'ism_433') - preset = get_preset(preset_name) - - if preset: - freq_start = preset['freq_start'] - freq_end = preset['freq_end'] - bin_size = preset.get('bin_size', 10000) - integration = preset.get('integration', 1.0) - band_name = preset['name'] - else: - freq_start = isms_config['freq_start'] - freq_end = isms_config['freq_end'] - bin_size = isms_config['bin_size'] - integration = isms_config['integration'] - band_name = f'{freq_start}-{freq_end} MHz' - - device = isms_config['device'] - gain = isms_config['gain'] - ppm = isms_config['ppm'] - threshold = isms_config['threshold'] - - # Get active baseline for comparison - active_baseline = None - baseline_spectrum = None - if isms_config.get('baseline_id'): - active_baseline = get_isms_baseline(isms_config['baseline_id']) - if active_baseline: - baseline_spectrum = active_baseline.get('spectrum_profile', {}).get(band_name) - - emit_status('scanning', band=band_name, preset=preset_name) - - current_bins: list[SpectrumBin] = [] - sweep_count = 0 - - try: - # Run continuous spectrum scanning - for spectrum_bin in run_rtl_power_scan( - freq_start_mhz=freq_start, - freq_end_mhz=freq_end, - bin_size_hz=bin_size, - integration_time=integration, - device_index=device, - gain=gain, - ppm=ppm, - single_shot=False, - ): - if not isms_running: - break - - current_bins.append(spectrum_bin) - - # Process a sweep's worth of data - if spectrum_bin.freq_hz >= (freq_end * 1_000_000 - bin_size): - sweep_count += 1 - - # Compute band metrics - metrics = compute_band_metrics(current_bins, band_name) - - # Store in recent metrics - with metrics_lock: - recent_metrics[band_name] = metrics - - # Add to baseline recorder if recording - if baseline_recorder.is_recording: - baseline_recorder.add_spectrum_sample(band_name, metrics) - - # Emit meter update - emit_meter(band_name, metrics.activity_score, metrics.noise_floor_db) - - # Emit peak if significant - if metrics.peak_power_db > metrics.noise_floor_db + 6: - emit_peak(metrics.peak_frequency_mhz, metrics.peak_power_db, band_name) - - # Detect bursts - bursts = detect_bursts(current_bins) - if bursts: - emit_event('bursts_detected', { - 'band': band_name, - 'count': len(bursts), - }) - - # Run rules engine - findings = rules_engine.evaluate_spectrum( - band_name=band_name, - noise_floor=metrics.noise_floor_db, - peak_freq=metrics.peak_frequency_mhz, - peak_power=metrics.peak_power_db, - activity_score=metrics.activity_score, - baseline_noise=baseline_spectrum.get('noise_floor_db') if baseline_spectrum else None, - baseline_activity=baseline_spectrum.get('activity_score') if baseline_spectrum else None, - baseline_peaks=baseline_spectrum.get('peak_frequencies') if baseline_spectrum else None, - burst_count=len(bursts), - ) - - for finding in findings: - emit_finding( - finding.severity, - finding.description, - finding_type=finding.finding_type, - band=finding.band, - frequency=finding.frequency, - ) - - # Emit progress - if sweep_count % 5 == 0: - emit_status('scanning', band=band_name, sweeps=sweep_count) - - # Clear for next sweep - current_bins.clear() - - except Exception as e: - logger.error(f"ISMS scan error: {e}") - emit_status('error', message=str(e)) - - finally: - isms_running = False - emit_status('stopped', sweeps=sweep_count) - - # Update scan record - if isms_current_scan_id: - update_isms_scan( - isms_current_scan_id, - status='completed', - findings_count=current_findings_count, - completed=True, - ) - - logger.info(f"ISMS scan stopped after {sweep_count} sweeps") - - -# ============================================ -# ROUTES: SCAN CONTROL -# ============================================ - -@isms_bp.route('/start_scan', methods=['POST']) -def start_scan(): - """Start ISMS spectrum scanning.""" - global isms_thread, isms_running, isms_current_scan_id, current_findings_count - - with isms_lock: - if isms_running: - return jsonify({ - 'status': 'error', - 'message': 'Scan already running' - }), 409 - - # Get configuration from request - data = request.get_json() or {} - - isms_config['preset'] = data.get('preset', isms_config['preset']) - isms_config['device'] = data.get('device', isms_config['device']) - isms_config['gain'] = data.get('gain', isms_config['gain']) - isms_config['threshold'] = data.get('threshold', isms_config['threshold']) - isms_config['baseline_id'] = data.get('baseline_id') - - # Custom frequency range - if data.get('freq_start'): - isms_config['freq_start'] = float(data['freq_start']) - if data.get('freq_end'): - isms_config['freq_end'] = float(data['freq_end']) - - # Location - if data.get('lat') and data.get('lon'): - isms_config['lat'] = float(data['lat']) - isms_config['lon'] = float(data['lon']) - - # Check for rtl_power - if not get_rtl_power_path(): - return jsonify({ - 'status': 'error', - 'message': 'rtl_power not found. Install rtl-sdr tools.' - }), 500 - - # Clear queue - while not isms_queue.empty(): - try: - isms_queue.get_nowait() - except queue.Empty: - break - - # Create scan record - gps_coords = None - if isms_config.get('lat') and isms_config.get('lon'): - gps_coords = {'lat': isms_config['lat'], 'lon': isms_config['lon']} - - isms_current_scan_id = create_isms_scan( - scan_preset=isms_config['preset'], - baseline_id=isms_config.get('baseline_id'), - gps_coords=gps_coords, - ) - current_findings_count = 0 - - # Start scanning thread - isms_running = True - isms_thread = threading.Thread(target=isms_scan_loop, daemon=True) - isms_thread.start() - - return jsonify({ - 'status': 'started', - 'scan_id': isms_current_scan_id, - 'config': { - 'preset': isms_config['preset'], - 'device': isms_config['device'], - } - }) - - -@isms_bp.route('/stop_scan', methods=['POST']) -def stop_scan(): - """Stop ISMS spectrum scanning.""" - global isms_running, isms_process - - with isms_lock: - if not isms_running: - return jsonify({ - 'status': 'error', - 'message': 'No scan running' - }), 400 - - isms_running = False - - # Terminate any subprocess - if isms_process: - safe_terminate(isms_process) - isms_process = None - - return jsonify({'status': 'stopped'}) - - -@isms_bp.route('/status', methods=['GET']) -def get_status(): - """Get current scanner status.""" - with isms_lock: - status = { - 'running': isms_running, - 'config': { - 'preset': isms_config['preset'], - 'device': isms_config['device'], - 'baseline_id': isms_config.get('baseline_id'), - }, - 'current_scan_id': isms_current_scan_id, - 'findings_count': current_findings_count, - } - - # Add recent metrics - with metrics_lock: - status['metrics'] = { - band: { - 'activity_score': m.activity_score, - 'noise_floor': m.noise_floor_db, - 'peak_freq': m.peak_frequency_mhz, - 'peak_power': m.peak_power_db, - } - for band, m in recent_metrics.items() - } - - return jsonify(status) - - -# ============================================ -# ROUTES: SSE STREAM -# ============================================ - -@isms_bp.route('/stream', methods=['GET']) -def stream(): - """SSE stream for real-time ISMS events.""" - def generate() -> Generator[str, None, None]: - last_keepalive = time.time() - - while True: - try: - msg = isms_queue.get(timeout=SSE_QUEUE_TIMEOUT) - last_keepalive = time.time() - yield format_sse(msg) - except queue.Empty: - now = time.time() - if now - last_keepalive >= SSE_KEEPALIVE_INTERVAL: - yield format_sse({'type': 'keepalive'}) - last_keepalive = now - - response = Response(generate(), mimetype='text/event-stream') - response.headers['Cache-Control'] = 'no-cache' - response.headers['X-Accel-Buffering'] = 'no' - response.headers['Connection'] = 'keep-alive' - return response - - -# ============================================ -# ROUTES: PRESETS -# ============================================ - -@isms_bp.route('/presets', methods=['GET']) -def list_presets(): - """List available scan presets.""" - return jsonify({ - 'presets': get_all_presets() - }) - - -# ============================================ -# ROUTES: TOWERS -# ============================================ - -@isms_bp.route('/towers', methods=['GET']) -def get_towers(): - """Query nearby cell towers from OpenCelliD.""" - lat = request.args.get('lat', type=float) - lon = request.args.get('lon', type=float) - radius = request.args.get('radius', default=5.0, type=float) - radio = request.args.get('radio') # GSM, UMTS, LTE, NR - - if lat is None or lon is None: - # Try to get from GPS - pos = get_current_position() - if pos: - lat = pos.latitude - lon = pos.longitude - else: - return jsonify({ - 'status': 'error', - 'message': 'Location required (lat/lon parameters or GPS)' - }), 400 - - # Check for token - token = get_opencellid_token() - if not token: - return jsonify({ - 'status': 'error', - 'message': 'OpenCelliD token not configured. Set OPENCELLID_TOKEN environment variable.', - 'config_required': True, - }), 400 - - # Query towers - towers = query_nearby_towers( - lat=lat, - lon=lon, - radius_km=radius, - radio=radio, - ) - - # Format for response - formatted_towers = [format_tower_info(t) for t in towers] - - # Add to baseline recorder if recording - if baseline_recorder.is_recording: - for tower in towers: - baseline_recorder.add_tower_sample(tower) - - return jsonify({ - 'status': 'ok', - 'query': { - 'lat': lat, - 'lon': lon, - 'radius_km': radius, - }, - 'count': len(formatted_towers), - 'towers': formatted_towers, - 'links': { - 'ofcom_coverage': build_ofcom_coverage_url(), - 'ofcom_emf': build_ofcom_emf_url(), - } - }) - - -# ============================================ -# ROUTES: BASELINES -# ============================================ - -@isms_bp.route('/baselines', methods=['GET']) -def list_baselines(): - """List all ISMS baselines.""" - baselines = get_all_isms_baselines() - return jsonify({ - 'baselines': baselines - }) - - -@isms_bp.route('/baselines', methods=['POST']) -def create_baseline(): - """Create a new baseline manually.""" - data = request.get_json() or {} - - name = data.get('name') - if not name: - return jsonify({ - 'status': 'error', - 'message': 'Name required' - }), 400 - - baseline_id = create_isms_baseline( - name=name, - location_name=data.get('location_name'), - latitude=data.get('latitude'), - longitude=data.get('longitude'), - spectrum_profile=data.get('spectrum_profile'), - cellular_environment=data.get('cellular_environment'), - known_towers=data.get('known_towers'), - ) - - return jsonify({ - 'status': 'created', - 'baseline_id': baseline_id - }) - - -@isms_bp.route('/baseline/', methods=['GET']) -def get_baseline(baseline_id: int): - """Get a specific baseline.""" - baseline = get_isms_baseline(baseline_id) - if not baseline: - return jsonify({ - 'status': 'error', - 'message': 'Baseline not found' - }), 404 - - return jsonify(baseline) - - -@isms_bp.route('/baseline/', methods=['DELETE']) -def remove_baseline(baseline_id: int): - """Delete a baseline.""" - if delete_isms_baseline(baseline_id): - return jsonify({'status': 'deleted'}) - return jsonify({ - 'status': 'error', - 'message': 'Baseline not found' - }), 404 - - -@isms_bp.route('/baseline//activate', methods=['POST']) -def activate_baseline(baseline_id: int): - """Set a baseline as active.""" - if set_active_isms_baseline(baseline_id): - return jsonify({'status': 'activated'}) - return jsonify({ - 'status': 'error', - 'message': 'Baseline not found' - }), 404 - - -@isms_bp.route('/baseline/active', methods=['GET']) -def get_active_baseline(): - """Get the currently active baseline.""" - baseline = get_active_isms_baseline() - if baseline: - return jsonify(baseline) - return jsonify({'status': 'none'}) - - -@isms_bp.route('/baseline/record/start', methods=['POST']) -def start_baseline_recording(): - """Start recording a new baseline.""" - if baseline_recorder.is_recording: - return jsonify({ - 'status': 'error', - 'message': 'Already recording' - }), 409 - - baseline_recorder.start_recording() - return jsonify({'status': 'recording_started'}) - - -@isms_bp.route('/baseline/record/stop', methods=['POST']) -def stop_baseline_recording(): - """Stop recording and save baseline.""" - if not baseline_recorder.is_recording: - return jsonify({ - 'status': 'error', - 'message': 'Not recording' - }), 400 - - data = request.get_json() or {} - name = data.get('name', f'Baseline {datetime.now().strftime("%Y-%m-%d %H:%M")}') - location_name = data.get('location_name') - - # Get current location - lat = data.get('latitude') or isms_config.get('lat') - lon = data.get('longitude') or isms_config.get('lon') - - if not lat or not lon: - pos = get_current_position() - if pos: - lat = pos.latitude - lon = pos.longitude - - # Stop recording and compile data - baseline_data = baseline_recorder.stop_recording() - - # Save to database - baseline_id = save_baseline_to_db( - name=name, - location_name=location_name, - latitude=lat, - longitude=lon, - baseline_data=baseline_data, - ) - - return jsonify({ - 'status': 'saved', - 'baseline_id': baseline_id, - 'summary': { - 'bands': len(baseline_data.get('spectrum_profile', {})), - 'cells': len(baseline_data.get('cellular_environment', [])), - 'towers': len(baseline_data.get('known_towers', [])), - } - }) - - -# ============================================ -# ROUTES: FINDINGS -# ============================================ - -@isms_bp.route('/findings', methods=['GET']) -def list_findings(): - """Get ISMS findings.""" - scan_id = request.args.get('scan_id', type=int) - severity = request.args.get('severity') - limit = request.args.get('limit', default=100, type=int) - - findings = get_isms_findings( - scan_id=scan_id, - severity=severity, - limit=limit, - ) - - return jsonify({ - 'findings': findings, - 'count': len(findings), - }) - - -@isms_bp.route('/findings/summary', methods=['GET']) -def findings_summary(): - """Get findings count summary.""" - summary = get_isms_findings_summary() - return jsonify(summary) - - -@isms_bp.route('/finding//acknowledge', methods=['POST']) -def acknowledge_finding(finding_id: int): - """Acknowledge a finding.""" - if acknowledge_isms_finding(finding_id): - return jsonify({'status': 'acknowledged'}) - return jsonify({ - 'status': 'error', - 'message': 'Finding not found' - }), 404 - - -# ============================================ -# ROUTES: SCANS -# ============================================ - -@isms_bp.route('/scans', methods=['GET']) -def list_scans(): - """List recent scans.""" - limit = request.args.get('limit', default=20, type=int) - scans = get_recent_isms_scans(limit=limit) - return jsonify({ - 'scans': scans - }) - - -@isms_bp.route('/scan/', methods=['GET']) -def get_scan_details(scan_id: int): - """Get details of a specific scan.""" - scan = get_isms_scan(scan_id) - if not scan: - return jsonify({ - 'status': 'error', - 'message': 'Scan not found' - }), 404 - - # Include findings - findings = get_isms_findings(scan_id=scan_id) - scan['findings'] = findings - - return jsonify(scan) - - -# ============================================ -# ROUTES: GSM SCANNING -# ============================================ - -def gsm_scan_loop(band: str, gain: int, ppm: int, timeout: float) -> None: - """GSM scanning background thread.""" - global gsm_running, gsm_detected_cells - - logger.info(f"GSM scan thread started for band {band}") - emit_status('gsm_scanning', band=band) - - cells: list[GsmCell] = [] - - try: - for cell in run_grgsm_scan( - band=band, - gain=gain, - ppm=ppm, - speed=4, - timeout=timeout, - ): - if not gsm_running: - break - - cells.append(cell) - - # Emit cell detection event - emit_event('gsm_cell', { - 'cell': format_gsm_cell(cell), - }) - - # Deduplicate and store results - gsm_detected_cells = deduplicate_cells(cells) - - # Run anomaly detection if baseline available - if gsm_baseline_cells: - anomalies = identify_gsm_anomalies(gsm_detected_cells, gsm_baseline_cells) - for anomaly in anomalies: - emit_finding( - anomaly['severity'], - anomaly['description'], - finding_type=anomaly['type'], - band='GSM', - frequency=anomaly.get('cell', {}).get('freq_mhz'), - ) - - # Emit summary - emit_event('gsm_scan_complete', { - 'cell_count': len(gsm_detected_cells), - 'cells': [format_gsm_cell(c) for c in gsm_detected_cells[:10]], # Top 10 by signal - }) - - except Exception as e: - logger.error(f"GSM scan error: {e}") - emit_status('gsm_error', message=str(e)) - - finally: - gsm_running = False - emit_status('gsm_stopped', cell_count=len(gsm_detected_cells)) - logger.info(f"GSM scan stopped, found {len(gsm_detected_cells)} cells") - - -@isms_bp.route('/gsm/scan', methods=['POST']) -def start_gsm_scan(): - """Start GSM cell scanning with grgsm_scanner.""" - global gsm_thread, gsm_running - - with gsm_lock: - if gsm_running: - return jsonify({ - 'status': 'error', - 'message': 'GSM scan already running' - }), 409 - - # Check for grgsm_scanner - if not get_grgsm_scanner_path(): - gsm_running = False - return jsonify({ - 'status': 'error', - 'message': 'grgsm_scanner not found. Install gr-gsm package.', - 'install_hint': 'See setup.sh or install gr-gsm from https://github.com/ptrkrysik/gr-gsm', - 'grgsm_available': False - }), 503 - - # Get configuration from request - data = request.get_json() or {} - band = data.get('band', 'GSM900') - gain = data.get('gain', isms_config['gain']) - ppm = data.get('ppm', isms_config.get('ppm', 0)) - timeout = data.get('timeout', 60) - - # Validate band - valid_bands = ['GSM900', 'EGSM900', 'GSM1800', 'GSM850', 'GSM1900'] - if band.upper() not in valid_bands: - return jsonify({ - 'status': 'error', - 'message': f'Invalid band. Must be one of: {", ".join(valid_bands)}' - }), 400 - - gsm_running = True - gsm_thread = threading.Thread( - target=gsm_scan_loop, - args=(band.upper(), gain, ppm, timeout), - daemon=True - ) - gsm_thread.start() - - return jsonify({ - 'status': 'started', - 'config': { - 'band': band.upper(), - 'gain': gain, - 'timeout': timeout, - } - }) - - -@isms_bp.route('/gsm/scan', methods=['DELETE']) -def stop_gsm_scan(): - """Stop GSM scanning.""" - global gsm_running - - with gsm_lock: - if not gsm_running: - return jsonify({ - 'status': 'error', - 'message': 'No GSM scan running' - }), 400 - - gsm_running = False - - return jsonify({'status': 'stopping'}) - - -@isms_bp.route('/gsm/status', methods=['GET']) -def get_gsm_status(): - """Get GSM scanner status and detected cells.""" - with gsm_lock: - return jsonify({ - 'running': gsm_running, - 'cell_count': len(gsm_detected_cells), - 'cells': [format_gsm_cell(c) for c in gsm_detected_cells], - 'grgsm_available': get_grgsm_scanner_path() is not None, - }) - - -@isms_bp.route('/gsm/cells', methods=['GET']) -def get_gsm_cells(): - """Get all detected GSM cells from last scan.""" - return jsonify({ - 'cells': [format_gsm_cell(c) for c in gsm_detected_cells], - 'count': len(gsm_detected_cells), - }) - - -@isms_bp.route('/gsm/baseline', methods=['POST']) -def set_gsm_baseline(): - """Save current GSM cells as baseline for comparison.""" - global gsm_baseline_cells - - if not gsm_detected_cells: - return jsonify({ - 'status': 'error', - 'message': 'No GSM cells detected. Run a scan first.' - }), 400 - - gsm_baseline_cells = gsm_detected_cells.copy() - - # Also add to baseline recorder if recording - if baseline_recorder.is_recording: - for cell in gsm_baseline_cells: - baseline_recorder.add_gsm_cell(cell) - - return jsonify({ - 'status': 'saved', - 'cell_count': len(gsm_baseline_cells), - 'cells': [format_gsm_cell(c) for c in gsm_baseline_cells], - }) - - -@isms_bp.route('/gsm/baseline', methods=['GET']) -def get_gsm_baseline(): - """Get current GSM baseline.""" - return jsonify({ - 'cells': [format_gsm_cell(c) for c in gsm_baseline_cells], - 'count': len(gsm_baseline_cells), - }) - - -@isms_bp.route('/gsm/baseline', methods=['DELETE']) -def clear_gsm_baseline(): - """Clear GSM baseline.""" - global gsm_baseline_cells - gsm_baseline_cells = [] - return jsonify({'status': 'cleared'}) diff --git a/setup.sh b/setup.sh index f593d94..79b5627 100755 --- a/setup.sh +++ b/setup.sh @@ -166,14 +166,6 @@ check_tools() { echo info "SoapySDR:" check_required "SoapySDRUtil" "SoapySDR CLI utility" SoapySDRUtil - - echo - info "GSM (optional):" - if have_any grgsm_scanner; then - ok "grgsm_scanner - GSM cell scanner" - else - warn "grgsm_scanner - GSM cell scanner (optional, for ISMS GSM detection)" - fi echo } @@ -360,8 +352,6 @@ install_macos_packages() { warn "macOS note: hcitool/hciconfig are Linux (BlueZ) utilities and often unavailable on macOS." info "TSCM BLE scanning uses bleak library (installed via pip) for manufacturer data detection." - warn "macOS note: gr-gsm (for GSM cell scanning) is not easily available on macOS." - info "GSM cell detection in ISMS mode requires Linux with GNU Radio." echo } @@ -458,167 +448,6 @@ install_acarsdec_from_source_debian() { ) } -install_grgsm_from_source_debian() { - info "Installing gr-gsm for GSM cell scanning (optional)..." - - # Check if GNU Radio is available - if ! cmd_exists gnuradio-config-info; then - info "GNU Radio not found. Installing GNU Radio first..." - if ! $SUDO apt-get install -y gnuradio gnuradio-dev; then - warn "Failed to install GNU Radio. gr-gsm requires GNU Radio 3.8+." - warn "GSM scanning will not be available." - return 1 - fi - fi - - # Check GNU Radio version (need 3.8+) - local gr_version - gr_version=$(gnuradio-config-info --version 2>/dev/null || echo "0.0.0") - info "GNU Radio version: $gr_version" - - # Install basic build dependencies first - info "Installing gr-gsm build dependencies..." - $SUDO apt-get install -y \ - cmake \ - autoconf \ - automake \ - libtool \ - pkg-config \ - build-essential \ - python3-docutils \ - libcppunit-dev \ - swig \ - doxygen \ - liblog4cpp5-dev \ - python3-scipy \ - python3-numpy \ - gnuradio-dev \ - gr-osmosdr \ - libtalloc-dev \ - libpcsclite-dev \ - libusb-1.0-0-dev \ - libgnutls28-dev \ - libmnl-dev \ - libsctp-dev \ - pybind11-dev \ - python3-pybind11 \ - || warn "Some build dependencies failed to install." - - # Check if libosmocore is available via apt - info "Checking for libosmocore packages..." - if ! $SUDO apt-get install -y libosmocore-dev 2>/dev/null; then - info "libosmocore not available in repos, building from source..." - - # Build libosmocore from source - local osmo_tmp - osmo_tmp="$(mktemp -d)" - - info "Cloning libosmocore..." - if git clone --depth 1 https://gitea.osmocom.org/osmocom/libosmocore.git "$osmo_tmp/libosmocore"; then - cd "$osmo_tmp/libosmocore" - - info "Building libosmocore (this may take a few minutes)..." - autoreconf -fi - ./configure --prefix=/usr/local - if make -j$(nproc) && $SUDO make install && $SUDO ldconfig; then - ok "libosmocore built and installed successfully" - else - warn "Failed to build libosmocore from source" - cd / - rm -rf "$osmo_tmp" - return 1 - fi - - cd / - rm -rf "$osmo_tmp" - else - warn "Failed to clone libosmocore" - rm -rf "$osmo_tmp" - return 1 - fi - else - ok "libosmocore installed from packages" - # Also try to install the other osmo packages - $SUDO apt-get install -y \ - libosmocoding-dev \ - libosmoctrl-dev \ - libosmogsm-dev \ - libosmovty-dev \ - libosmocodec-dev \ - 2>/dev/null || true - fi - - # Build gr-gsm - local tmp_dir - tmp_dir="$(mktemp -d)" - - # Check GNU Radio version to select correct gr-gsm fork - local gr_major gr_minor - gr_major=$(gnuradio-config-info --version 2>/dev/null | cut -d. -f1 || echo "3") - gr_minor=$(gnuradio-config-info --version 2>/dev/null | cut -d. -f2 || echo "8") - - local grgsm_repo grgsm_branch - if [[ "$gr_major" -ge 3 && "$gr_minor" -ge 10 ]]; then - # GNU Radio 3.10+ needs velichkov fork with maint-3.10 branch - info "GNU Radio 3.10+ detected, using velichkov fork (maint-3.10 branch)..." - grgsm_repo="https://github.com/velichkov/gr-gsm.git" - grgsm_branch="maint-3.10" - - # Install pybind11 for GNU Radio 3.10+ - $SUDO apt-get install -y pybind11-dev python3-pybind11 || true - else - # Older GNU Radio versions use original repo with SWIG - info "GNU Radio < 3.10 detected, using original gr-gsm..." - grgsm_repo="https://github.com/ptrkrysik/gr-gsm.git" - grgsm_branch="master" - fi - - info "Cloning gr-gsm from $grgsm_repo..." - if ! git clone --depth 1 -b "$grgsm_branch" "$grgsm_repo" "$tmp_dir/gr-gsm"; then - warn "Failed to clone gr-gsm repository" - rm -rf "$tmp_dir" - return 1 - fi - - cd "$tmp_dir/gr-gsm" - mkdir -p build && cd build - - info "Configuring gr-gsm..." - # Include /usr/local in CMAKE_PREFIX_PATH for source-built libosmocore - if ! cmake -DCMAKE_PREFIX_PATH="/usr/local;/usr" -DCMAKE_INSTALL_PREFIX=/usr/local ..; then - warn "Failed to configure gr-gsm (cmake failed)" - warn "Check that all dependencies are installed" - cd / - rm -rf "$tmp_dir" - return 1 - fi - - info "Compiling gr-gsm (this may take a few minutes)..." - if ! make -j$(nproc); then - warn "Failed to compile gr-gsm" - warn "Check the build output above for errors" - cd / - rm -rf "$tmp_dir" - return 1 - fi - - info "Installing gr-gsm..." - if $SUDO make install && $SUDO ldconfig; then - ok "gr-gsm installed successfully." - info "grgsm_scanner should now be available for GSM cell detection." - else - warn "Failed to install gr-gsm" - cd / - rm -rf "$tmp_dir" - return 1 - fi - - # Cleanup - cd / - rm -rf "$tmp_dir" - return 0 -} - install_rtlsdr_blog_drivers_debian() { # The RTL-SDR Blog drivers provide better support for: # - RTL-SDR Blog V4 (R828D tuner) @@ -718,24 +547,14 @@ install_debian_packages() { export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a - TOTAL_STEPS=19 + TOTAL_STEPS=18 CURRENT_STEP=0 progress "Updating APT package lists" $SUDO apt-get update -y >/dev/null - # Fix any broken RTL-SDR packages first (common issue with Blog drivers vs stock) - info "Fixing RTL-SDR package conflicts..." - # Force remove broken packages - $SUDO dpkg --remove --force-remove-reinstreq librtlsdr0 librtlsdr2 librtlsdr-dev rtl-sdr 2>/dev/null || true - $SUDO apt-get -f install -y 2>/dev/null || true - $SUDO dpkg --configure -a 2>/dev/null || true - $SUDO apt-get autoremove -y --purge 2>/dev/null || true - progress "Installing RTL-SDR" - # Skip apt rtl-sdr package - we'll use RTL-SDR Blog drivers instead which are better - # The stock packages often conflict with the Blog drivers - info "Skipping stock rtl-sdr package (RTL-SDR Blog drivers will be used instead)" + apt_install rtl-sdr progress "Installing RTL-SDR Blog drivers (V4 support)" install_rtlsdr_blog_drivers_debian @@ -798,13 +617,6 @@ install_debian_packages() { fi cmd_exists acarsdec || install_acarsdec_from_source_debian - progress "Installing gr-gsm (optional, for GSM cell detection)" - if ! cmd_exists grgsm_scanner; then - install_grgsm_from_source_debian || true - else - ok "grgsm_scanner already installed" - fi - progress "Configuring udev rules" setup_udev_rules_debian diff --git a/static/css/index.css b/static/css/index.css index e039a4a..69196c9 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -5052,190 +5052,3 @@ body::before { .preset-freq-btn:active { transform: scale(0.98); } - -/* ============================================================================= - ISMS Listening Station Styles - ============================================================================= */ - -.isms-dashboard { - padding: 16px; -} - -.isms-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 12px; -} - -.isms-panel { - background: var(--bg-card); - border-radius: 8px; - padding: 12px; - border: 1px solid var(--border-color); -} - -.isms-panel-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - font-size: 11px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 1px; -} - -.isms-info-banner { - margin-bottom: 12px; - padding: 8px 12px; - background: rgba(0, 212, 255, 0.1); - border: 1px solid rgba(0, 212, 255, 0.3); - border-radius: 4px; - font-size: 10px; - color: var(--text-muted); -} - -/* ISMS Badges */ -.isms-badge { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - font-size: 9px; - font-weight: bold; - text-transform: uppercase; -} - -.isms-badge-high { - background: rgba(255, 51, 102, 0.2); - color: var(--accent-red); - border: 1px solid rgba(255, 51, 102, 0.4); -} - -.isms-badge-warn { - background: rgba(255, 170, 0, 0.2); - color: var(--accent-orange); - border: 1px solid rgba(255, 170, 0, 0.4); -} - -.isms-badge-info { - background: rgba(0, 212, 255, 0.2); - color: var(--accent-cyan); - border: 1px solid rgba(0, 212, 255, 0.4); -} - -/* ISMS Band Meter */ -.isms-band-meter { - text-align: center; - min-width: 80px; -} - -.isms-band-meter .meter-bar { - height: 60px; - width: 20px; - background: rgba(0, 0, 0, 0.5); - border-radius: 4px; - margin: 0 auto; - position: relative; - overflow: hidden; -} - -.isms-band-meter .meter-fill { - position: absolute; - bottom: 0; - width: 100%; - background: linear-gradient(to top, var(--accent-green), var(--accent-cyan), var(--accent-orange)); - transition: height 0.3s; -} - -.isms-band-meter .meter-value { - font-size: 11px; - margin-top: 4px; - font-family: 'JetBrains Mono', monospace; -} - -.isms-band-meter .meter-noise { - font-size: 9px; - color: var(--text-muted); -} - -/* ISMS Tower Map */ -#ismsTowerMap { - height: 180px; - background: rgba(0, 0, 0, 0.3); - border-radius: 4px; -} - -#ismsTowerMap .leaflet-control-attribution { - font-size: 8px; - background: rgba(0, 0, 0, 0.6); - color: var(--text-muted); -} - -.isms-user-marker { - background: transparent; - border: none; -} - -/* ISMS Finding Item */ -.isms-finding-item { - padding: 8px; - border-bottom: 1px solid var(--border-color); - font-size: 11px; -} - -.isms-finding-item:last-child { - border-bottom: none; -} - -/* ISMS Collapsible Panel */ -.isms-panel.collapsible .isms-panel-header.clickable { - cursor: pointer; - padding: 12px; - margin: 0; -} - -.isms-panel.collapsible .isms-panel-header.clickable:hover { - background: rgba(0, 212, 255, 0.05); -} - -/* ISMS Start Button */ -#ismsStartBtn.running { - background: linear-gradient(135deg, #ff3366, #ff6b6b); - border-color: #ff3366; - animation: pulse-glow 2s ease-in-out infinite; -} - -/* ISMS Recording Status */ -#ismsBaselineRecordingStatus { - background: rgba(255, 100, 100, 0.1); - border-radius: 4px; - padding: 8px; - font-size: 10px; - color: var(--accent-red); - animation: recording-blink 1s ease-in-out infinite; -} - -@keyframes recording-blink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -/* ISMS Mode Content */ -#ismsMode.mode-content { - display: none; -} - -#ismsMode.mode-content.active { - display: block; -} - -/* Responsive ISMS Grid */ -@media (max-width: 1024px) { - .isms-grid { - grid-template-columns: repeat(2, 1fr); - } - - .isms-panel[style*="grid-column: span 4"] { - grid-column: span 2 !important; - } -} diff --git a/static/js/modes/isms.js b/static/js/modes/isms.js deleted file mode 100644 index 44d21ed..0000000 --- a/static/js/modes/isms.js +++ /dev/null @@ -1,1003 +0,0 @@ -/** - * ISMS Listening Station Mode - * Spectrum monitoring, cellular environment, tower mapping - */ - -// ============== STATE ============== -let isIsmsScanRunning = false; -let ismsEventSource = null; -let ismsTowerMap = null; -let ismsTowerMarkers = []; -let ismsLocation = { lat: null, lon: null }; -let ismsBandMetrics = {}; -let ismsFindings = []; -let ismsPeaks = []; -let ismsBaselineRecording = false; -let ismsInitialized = false; - -// Finding counts -let ismsFindingCounts = { high: 0, warn: 0, info: 0 }; - -// GSM scanner state -let isGsmScanRunning = false; -let ismsGsmCells = []; - -// ============== INITIALIZATION ============== - -function initIsmsMode() { - if (ismsInitialized) return; - - // Initialize Leaflet map for towers - initIsmsTowerMap(); - - // Load baselines - ismsRefreshBaselines(); - - // Check for GPS - ismsCheckGps(); - - // Populate SDR devices - ismsPopulateSdrDevices(); - - // Set up event listeners - setupIsmsEventListeners(); - - ismsInitialized = true; - console.log('ISMS mode initialized'); -} - -function initIsmsTowerMap() { - const container = document.getElementById('ismsTowerMap'); - if (!container || ismsTowerMap) return; - - // Clear placeholder content - container.innerHTML = ''; - - ismsTowerMap = L.map('ismsTowerMap', { - center: [51.5074, -0.1278], - zoom: 12, - zoomControl: false, - }); - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OSM' - }).addTo(ismsTowerMap); - - // Add zoom control to bottom right - L.control.zoom({ position: 'bottomright' }).addTo(ismsTowerMap); -} - -function setupIsmsEventListeners() { - // Preset change - const presetSelect = document.getElementById('ismsScanPreset'); - if (presetSelect) { - presetSelect.addEventListener('change', function() { - const customRange = document.getElementById('ismsCustomRange'); - if (customRange) { - customRange.style.display = this.value === 'custom' ? 'block' : 'none'; - } - }); - } - - // Gain slider - const gainSlider = document.getElementById('ismsGain'); - if (gainSlider) { - gainSlider.addEventListener('input', function() { - document.getElementById('ismsGainValue').textContent = this.value; - }); - } - - // Threshold slider - const thresholdSlider = document.getElementById('ismsActivityThreshold'); - if (thresholdSlider) { - thresholdSlider.addEventListener('input', function() { - document.getElementById('ismsThresholdValue').textContent = this.value + '%'; - }); - } -} - -async function ismsPopulateSdrDevices() { - try { - const response = await fetch('/devices'); - const devices = await response.json(); - - const select = document.getElementById('ismsSdrDevice'); - if (!select) return; - - select.innerHTML = ''; - - if (devices.length === 0) { - select.innerHTML = ''; - return; - } - - devices.forEach((device, index) => { - const option = document.createElement('option'); - option.value = index; - option.textContent = `${index}: ${device.name || 'RTL-SDR'}`; - select.appendChild(option); - }); - } catch (e) { - console.error('Failed to load SDR devices:', e); - } -} - -// ============== GPS ============== - -async function ismsCheckGps() { - try { - const response = await fetch('/gps/status'); - const data = await response.json(); - - if (data.connected && data.position) { - ismsLocation.lat = data.position.latitude; - ismsLocation.lon = data.position.longitude; - updateIsmsLocationDisplay(); - } - } catch (e) { - console.debug('GPS not available'); - } -} - -function ismsUseGPS() { - fetch('/gps/status') - .then(r => r.json()) - .then(data => { - if (data.connected && data.position) { - ismsLocation.lat = data.position.latitude; - ismsLocation.lon = data.position.longitude; - updateIsmsLocationDisplay(); - ismsNotify('ISMS', 'GPS location acquired'); - } else { - ismsNotify('ISMS', 'GPS not available. Connect GPS first.'); - } - }) - .catch(() => { - ismsNotify('ISMS', 'Failed to get GPS position'); - }); -} - -function ismsSetManualLocation() { - const lat = prompt('Enter latitude:', ismsLocation.lat || '51.5074'); - if (lat === null) return; - - const lon = prompt('Enter longitude:', ismsLocation.lon || '-0.1278'); - if (lon === null) return; - - ismsLocation.lat = parseFloat(lat); - ismsLocation.lon = parseFloat(lon); - updateIsmsLocationDisplay(); -} - -function updateIsmsLocationDisplay() { - const coordsEl = document.getElementById('ismsCoords'); - const quickLocEl = document.getElementById('ismsQuickLocation'); - - if (ismsLocation.lat && ismsLocation.lon) { - const text = `${ismsLocation.lat.toFixed(4)}, ${ismsLocation.lon.toFixed(4)}`; - if (coordsEl) coordsEl.textContent = `Lat: ${ismsLocation.lat.toFixed(4)}, Lon: ${ismsLocation.lon.toFixed(4)}`; - if (quickLocEl) quickLocEl.textContent = text; - - // Center map on location - if (ismsTowerMap) { - ismsTowerMap.setView([ismsLocation.lat, ismsLocation.lon], 13); - } - } -} - -// ============== SCAN CONTROLS ============== - -function ismsToggleScan() { - if (isIsmsScanRunning) { - ismsStopScan(); - } else { - ismsStartScan(); - } -} - -async function ismsStartScan() { - const preset = document.getElementById('ismsScanPreset').value; - const device = parseInt(document.getElementById('ismsSdrDevice').value || '0'); - const gain = parseInt(document.getElementById('ismsGain').value || '40'); - const threshold = parseInt(document.getElementById('ismsActivityThreshold').value || '50'); - const baselineId = document.getElementById('ismsBaselineSelect').value || null; - - const config = { - preset: preset, - device: device, - gain: gain, - threshold: threshold, - baseline_id: baselineId ? parseInt(baselineId) : null, - }; - - // Add custom range if selected - if (preset === 'custom') { - config.freq_start = parseFloat(document.getElementById('ismsStartFreq').value); - config.freq_end = parseFloat(document.getElementById('ismsEndFreq').value); - } - - // Add location - if (ismsLocation.lat && ismsLocation.lon) { - config.lat = ismsLocation.lat; - config.lon = ismsLocation.lon; - } - - try { - const response = await fetch('/isms/start_scan', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(config) - }); - - const data = await response.json(); - - if (data.status === 'started') { - isIsmsScanRunning = true; - updateIsmsUI('scanning'); - connectIsmsStream(); - - // Reset findings - ismsFindingCounts = { high: 0, warn: 0, info: 0 }; - ismsFindings = []; - ismsPeaks = []; - updateIsmsFindingsBadges(); - } else { - ismsNotify('ISMS Error', data.message || 'Failed to start scan'); - } - } catch (e) { - ismsNotify('ISMS Error', 'Failed to start scan: ' + e.message); - } -} - -async function ismsStopScan() { - try { - await fetch('/isms/stop_scan', { method: 'POST' }); - } catch (e) { - console.error('Error stopping scan:', e); - } - - isIsmsScanRunning = false; - disconnectIsmsStream(); - updateIsmsUI('stopped'); -} - -function updateIsmsUI(state) { - const startBtn = document.getElementById('ismsStartBtn'); - const quickStatus = document.getElementById('ismsQuickStatus'); - const scanStatus = document.getElementById('ismsScanStatus'); - - if (state === 'scanning') { - if (startBtn) { - startBtn.textContent = 'Stop Scan'; - startBtn.classList.add('running'); - } - if (quickStatus) quickStatus.textContent = 'SCANNING'; - if (scanStatus) scanStatus.textContent = 'SCANNING'; - - // Update quick band display - const presetSelect = document.getElementById('ismsScanPreset'); - const quickBand = document.getElementById('ismsQuickBand'); - if (presetSelect && quickBand) { - quickBand.textContent = presetSelect.options[presetSelect.selectedIndex].text; - } - } else { - if (startBtn) { - startBtn.textContent = 'Start Scan'; - startBtn.classList.remove('running'); - } - if (quickStatus) quickStatus.textContent = 'IDLE'; - if (scanStatus) scanStatus.textContent = 'IDLE'; - } -} - -// ============== SSE STREAM ============== - -function connectIsmsStream() { - if (ismsEventSource) { - ismsEventSource.close(); - } - - ismsEventSource = new EventSource('/isms/stream'); - - ismsEventSource.onmessage = function(event) { - try { - const data = JSON.parse(event.data); - handleIsmsEvent(data); - } catch (e) { - console.error('Failed to parse ISMS event:', e); - } - }; - - ismsEventSource.onerror = function() { - console.error('ISMS stream error'); - }; -} - -function disconnectIsmsStream() { - if (ismsEventSource) { - ismsEventSource.close(); - ismsEventSource = null; - } -} - -function handleIsmsEvent(data) { - switch (data.type) { - case 'meter': - updateIsmsBandMeter(data.band, data.level, data.noise_floor); - break; - case 'spectrum_peak': - addIsmsPeak(data); - break; - case 'finding': - addIsmsFinding(data); - break; - case 'status': - updateIsmsStatus(data); - break; - case 'gsm_cell': - handleGsmCell(data.cell); - break; - case 'gsm_scan_complete': - handleGsmScanComplete(data); - break; - case 'gsm_scanning': - case 'gsm_stopped': - case 'gsm_error': - handleGsmStatus(data); - break; - case 'keepalive': - // Ignore - break; - default: - console.debug('Unknown ISMS event:', data.type); - } -} - -// ============== BAND METERS ============== - -function updateIsmsBandMeter(band, level, noiseFloor) { - ismsBandMetrics[band] = { level, noiseFloor }; - - const container = document.getElementById('ismsBandMeters'); - if (!container) return; - - // Find or create meter for this band - let meter = container.querySelector(`[data-band="${band}"]`); - - if (!meter) { - // Clear placeholder if first meter - if (container.querySelector('div:not([data-band])')) { - container.innerHTML = ''; - } - - meter = document.createElement('div'); - meter.setAttribute('data-band', band); - meter.className = 'isms-band-meter'; - meter.style.cssText = 'text-align: center; min-width: 80px;'; - meter.innerHTML = ` -
${band}
-
-
-
-
${level.toFixed(0)}%
-
${noiseFloor.toFixed(1)} dB
- `; - container.appendChild(meter); - } - - // Update meter values - const fill = meter.querySelector('.meter-fill'); - const value = meter.querySelector('.meter-value'); - const noise = meter.querySelector('.meter-noise'); - - if (fill) fill.style.height = level + '%'; - if (value) value.textContent = level.toFixed(0) + '%'; - if (noise) noise.textContent = noiseFloor.toFixed(1) + ' dB'; -} - -// ============== PEAKS ============== - -function addIsmsPeak(data) { - // Add to peaks array (keep last 20) - ismsPeaks.unshift({ - freq: data.freq_mhz, - power: data.power_db, - band: data.band, - timestamp: new Date() - }); - - if (ismsPeaks.length > 20) { - ismsPeaks.pop(); - } - - updateIsmsPeaksList(); -} - -function updateIsmsPeaksList() { - const tbody = document.getElementById('ismsPeaksBody'); - const countEl = document.getElementById('ismsPeakCount'); - - if (!tbody) return; - - if (ismsPeaks.length === 0) { - tbody.innerHTML = 'No peaks detected'; - if (countEl) countEl.textContent = '0'; - return; - } - - tbody.innerHTML = ismsPeaks.map(peak => ` - - ${peak.freq.toFixed(3)} MHz - ${peak.power.toFixed(1)} dB - ${peak.band || '--'} - - `).join(''); - - if (countEl) countEl.textContent = ismsPeaks.length; -} - -// ============== FINDINGS ============== - -function addIsmsFinding(data) { - const finding = { - severity: data.severity, - text: data.text, - details: data.details, - timestamp: data.timestamp || new Date().toISOString() - }; - - ismsFindings.unshift(finding); - - // Update counts - if (data.severity === 'high') ismsFindingCounts.high++; - else if (data.severity === 'warn') ismsFindingCounts.warn++; - else ismsFindingCounts.info++; - - updateIsmsFindingsBadges(); - updateIsmsFindingsTimeline(); - - // Update quick findings count - const quickFindings = document.getElementById('ismsQuickFindings'); - if (quickFindings) { - quickFindings.textContent = ismsFindings.length; - quickFindings.style.color = ismsFindingCounts.high > 0 ? 'var(--accent-red)' : - ismsFindingCounts.warn > 0 ? 'var(--accent-orange)' : 'var(--accent-green)'; - } -} - -function updateIsmsFindingsBadges() { - const highBadge = document.getElementById('ismsFindingsHigh'); - const warnBadge = document.getElementById('ismsFindingsWarn'); - const infoBadge = document.getElementById('ismsFindingsInfo'); - - if (highBadge) { - highBadge.textContent = ismsFindingCounts.high + ' HIGH'; - highBadge.style.display = ismsFindingCounts.high > 0 ? 'inline-block' : 'none'; - } - if (warnBadge) { - warnBadge.textContent = ismsFindingCounts.warn + ' WARN'; - warnBadge.style.display = ismsFindingCounts.warn > 0 ? 'inline-block' : 'none'; - } - if (infoBadge) { - infoBadge.textContent = ismsFindingCounts.info + ' INFO'; - } -} - -function updateIsmsFindingsTimeline() { - const timeline = document.getElementById('ismsFindingsTimeline'); - if (!timeline) return; - - if (ismsFindings.length === 0) { - timeline.innerHTML = ` -
- No findings yet. Start a scan and enable baseline comparison. -
- `; - return; - } - - timeline.innerHTML = ismsFindings.slice(0, 50).map(finding => { - const severityColor = finding.severity === 'high' ? 'var(--accent-red)' : - finding.severity === 'warn' ? 'var(--accent-orange)' : 'var(--accent-cyan)'; - const time = new Date(finding.timestamp).toLocaleTimeString(); - - return ` -
-
- ${finding.severity} - ${time} -
-
${finding.text}
-
- `; - }).join(''); -} - -// ============== STATUS ============== - -function updateIsmsStatus(data) { - if (data.state === 'stopped' || data.state === 'error') { - isIsmsScanRunning = false; - updateIsmsUI('stopped'); - - if (data.state === 'error') { - ismsNotify('ISMS Error', data.message || 'Scan error'); - } - } -} - -// ============== TOWERS ============== - -async function ismsRefreshTowers() { - if (!ismsLocation.lat || !ismsLocation.lon) { - ismsNotify('ISMS', 'Set location first to query towers'); - return; - } - - const towerCountEl = document.getElementById('ismsTowerCount'); - if (towerCountEl) towerCountEl.textContent = 'Querying...'; - - try { - const response = await fetch(`/isms/towers?lat=${ismsLocation.lat}&lon=${ismsLocation.lon}&radius=5`); - const data = await response.json(); - - if (data.status === 'error') { - if (towerCountEl) towerCountEl.textContent = data.message; - if (data.config_required) { - ismsNotify('ISMS', 'OpenCelliD token required. Set OPENCELLID_TOKEN environment variable.'); - } - return; - } - - updateIsmsTowerMap(data.towers); - updateIsmsTowerList(data.towers); - - if (towerCountEl) towerCountEl.textContent = `${data.count} towers found`; - } catch (e) { - console.error('Failed to query towers:', e); - if (towerCountEl) towerCountEl.textContent = 'Query failed'; - } -} - -function updateIsmsTowerMap(towers) { - if (!ismsTowerMap) return; - - // Clear existing markers - ismsTowerMarkers.forEach(marker => marker.remove()); - ismsTowerMarkers = []; - - // Add tower markers - towers.forEach(tower => { - const marker = L.circleMarker([tower.lat, tower.lon], { - radius: 6, - fillColor: getTowerColor(tower.radio), - color: '#fff', - weight: 1, - opacity: 1, - fillOpacity: 0.8 - }); - - marker.bindPopup(` -
- ${tower.operator}
- ${tower.radio} - CID: ${tower.cellid}
- Distance: ${tower.distance_km} km
- CellMapper -
- `); - - marker.addTo(ismsTowerMap); - ismsTowerMarkers.push(marker); - }); - - // Add user location marker - if (ismsLocation.lat && ismsLocation.lon) { - const userMarker = L.marker([ismsLocation.lat, ismsLocation.lon], { - icon: L.divIcon({ - className: 'isms-user-marker', - html: '
', - iconSize: [16, 16], - iconAnchor: [8, 8] - }) - }); - userMarker.addTo(ismsTowerMap); - ismsTowerMarkers.push(userMarker); - } - - // Fit map to markers if we have towers - if (towers.length > 0 && ismsTowerMarkers.length > 0) { - const group = L.featureGroup(ismsTowerMarkers); - ismsTowerMap.fitBounds(group.getBounds().pad(0.1)); - } -} - -function getTowerColor(radio) { - switch (radio) { - case 'LTE': return '#00d4ff'; - case 'NR': return '#ff00ff'; - case 'UMTS': return '#00ff88'; - case 'GSM': return '#ffaa00'; - default: return '#888'; - } -} - -function updateIsmsTowerList(towers) { - const list = document.getElementById('ismsTowerList'); - if (!list) return; - - if (towers.length === 0) { - list.innerHTML = '
No towers found
'; - return; - } - - list.innerHTML = towers.slice(0, 10).map(tower => ` -
- ${tower.radio} - ${tower.operator} - ${tower.distance_km} km -
- `).join(''); -} - -// ============== BASELINES ============== - -async function ismsRefreshBaselines() { - try { - const response = await fetch('/isms/baselines'); - const data = await response.json(); - - const select = document.getElementById('ismsBaselineSelect'); - if (!select) return; - - // Keep the "No Baseline" option - select.innerHTML = ''; - - data.baselines.forEach(baseline => { - const option = document.createElement('option'); - option.value = baseline.id; - option.textContent = `${baseline.name}${baseline.is_active ? ' (Active)' : ''}`; - if (baseline.is_active) option.selected = true; - select.appendChild(option); - }); - } catch (e) { - console.error('Failed to load baselines:', e); - } -} - -function ismsToggleBaselineRecording() { - if (ismsBaselineRecording) { - ismsStopBaselineRecording(); - } else { - ismsStartBaselineRecording(); - } -} - -async function ismsStartBaselineRecording() { - try { - const response = await fetch('/isms/baseline/record/start', { method: 'POST' }); - const data = await response.json(); - - if (data.status === 'recording_started') { - ismsBaselineRecording = true; - - const btn = document.getElementById('ismsRecordBaselineBtn'); - const status = document.getElementById('ismsBaselineRecordingStatus'); - - if (btn) { - btn.textContent = 'Stop Recording'; - btn.style.background = 'var(--accent-red)'; - } - if (status) status.style.display = 'block'; - - ismsNotify('ISMS', 'Baseline recording started'); - } - } catch (e) { - ismsNotify('ISMS Error', 'Failed to start recording'); - } -} - -async function ismsStopBaselineRecording() { - const name = prompt('Enter baseline name:', `Baseline ${new Date().toLocaleDateString()}`); - if (!name) return; - - try { - const response = await fetch('/isms/baseline/record/stop', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: name, - latitude: ismsLocation.lat, - longitude: ismsLocation.lon - }) - }); - - const data = await response.json(); - - if (data.status === 'saved') { - ismsBaselineRecording = false; - - const btn = document.getElementById('ismsRecordBaselineBtn'); - const status = document.getElementById('ismsBaselineRecordingStatus'); - - if (btn) { - btn.textContent = 'Record New'; - btn.style.background = ''; - } - if (status) status.style.display = 'none'; - - ismsNotify('ISMS', `Baseline saved: ${data.summary.bands} bands, ${data.summary.towers} towers`); - ismsRefreshBaselines(); - } - } catch (e) { - ismsNotify('ISMS Error', 'Failed to save baseline'); - } -} - -// ============== BASELINE PANEL ============== - -function ismsToggleBaselinePanel() { - const content = document.getElementById('ismsBaselineCompare'); - const icon = document.getElementById('ismsBaselinePanelIcon'); - - if (content && icon) { - const isVisible = content.style.display !== 'none'; - content.style.display = isVisible ? 'none' : 'block'; - icon.textContent = isVisible ? '▶' : '▼'; - } -} - -// ============== UTILITY ============== - -function ismsNotify(title, message) { - // Use existing notification system if available (defined in audio.js) - if (typeof showNotification === 'function' && showNotification !== ismsNotify) { - showNotification(title, message); - } else { - console.log(`[${title}] ${message}`); - } -} - -// ============== GSM SCANNING ============== - -function ismsToggleGsmScan() { - if (isGsmScanRunning) { - ismsStopGsmScan(); - } else { - ismsStartGsmScan(); - } -} - -async function ismsStartGsmScan() { - const band = document.getElementById('ismsGsmBand').value; - const gain = parseInt(document.getElementById('ismsGain').value || '40'); - - const config = { - band: band, - gain: gain, - timeout: 60 - }; - - try { - const response = await fetch('/isms/gsm/scan', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(config) - }); - - const data = await response.json(); - - if (data.status === 'started') { - isGsmScanRunning = true; - ismsGsmCells = []; - updateGsmScanUI('scanning'); - - // Connect to SSE stream if not already connected - if (!ismsEventSource) { - connectIsmsStream(); - } - - ismsNotify('ISMS', `GSM scan started on ${band}`); - } else { - // Update status display with error - const statusText = document.getElementById('ismsGsmStatusText'); - if (statusText) { - statusText.textContent = 'Not Available'; - statusText.style.color = 'var(--accent-red)'; - } - - if (data.grgsm_available === false) { - ismsNotify('ISMS', 'gr-gsm not installed. GSM scanning requires grgsm_scanner.'); - } else { - ismsNotify('ISMS Error', data.message || 'Failed to start GSM scan'); - } - } - } catch (e) { - ismsNotify('ISMS Error', 'Failed to start GSM scan: ' + e.message); - } -} - -async function ismsStopGsmScan() { - try { - await fetch('/isms/gsm/scan', { method: 'DELETE' }); - } catch (e) { - console.error('Error stopping GSM scan:', e); - } - - isGsmScanRunning = false; - updateGsmScanUI('stopped'); -} - -function updateGsmScanUI(state) { - const btn = document.getElementById('ismsGsmScanBtn'); - const statusText = document.getElementById('ismsGsmStatusText'); - - if (state === 'scanning') { - if (btn) { - btn.textContent = 'Stop Scan'; - btn.style.background = 'var(--accent-red)'; - } - if (statusText) { - statusText.textContent = 'Scanning...'; - statusText.style.color = 'var(--accent-orange)'; - } - } else { - if (btn) { - btn.textContent = 'Scan GSM Cells'; - btn.style.background = ''; - } - if (statusText) { - statusText.textContent = 'Ready'; - statusText.style.color = 'var(--accent-cyan)'; - } - } -} - -function handleGsmCell(cell) { - // Check if we already have this ARFCN - const existing = ismsGsmCells.find(c => c.arfcn === cell.arfcn); - - if (existing) { - // Update if stronger signal - if (cell.power_dbm > existing.power_dbm) { - Object.assign(existing, cell); - } - } else { - ismsGsmCells.push(cell); - } - - // Update count display - const countEl = document.getElementById('ismsGsmCellCount'); - if (countEl) { - countEl.textContent = ismsGsmCells.length; - } - - // Update cells list - updateGsmCellsList(); -} - -function handleGsmScanComplete(data) { - isGsmScanRunning = false; - updateGsmScanUI('stopped'); - - // Update with final cell list - if (data.cells) { - ismsGsmCells = data.cells; - updateGsmCellsList(); - } - - const countEl = document.getElementById('ismsGsmCellCount'); - if (countEl) { - countEl.textContent = data.cell_count || ismsGsmCells.length; - } - - ismsNotify('ISMS', `GSM scan complete: ${data.cell_count} cells found`); -} - -function handleGsmStatus(data) { - const statusText = document.getElementById('ismsGsmStatusText'); - - if (data.type === 'gsm_scanning') { - if (statusText) { - statusText.textContent = `Scanning ${data.band || 'GSM'}...`; - statusText.style.color = 'var(--accent-orange)'; - } - } else if (data.type === 'gsm_stopped') { - isGsmScanRunning = false; - updateGsmScanUI('stopped'); - if (statusText) { - statusText.textContent = `Found ${data.cell_count || 0} cells`; - statusText.style.color = 'var(--accent-green)'; - } - } else if (data.type === 'gsm_error') { - isGsmScanRunning = false; - updateGsmScanUI('stopped'); - if (statusText) { - statusText.textContent = 'Error'; - statusText.style.color = 'var(--accent-red)'; - } - ismsNotify('ISMS Error', data.message || 'GSM scan error'); - } -} - -function updateGsmCellsList() { - const container = document.getElementById('ismsGsmCells'); - if (!container) return; - - if (ismsGsmCells.length === 0) { - container.innerHTML = '
No cells detected
'; - return; - } - - // Sort by signal strength - const sortedCells = [...ismsGsmCells].sort((a, b) => b.power_dbm - a.power_dbm); - - container.innerHTML = sortedCells.map(cell => { - const signalColor = cell.power_dbm > -70 ? 'var(--accent-green)' : - cell.power_dbm > -85 ? 'var(--accent-orange)' : 'var(--text-muted)'; - - const operator = cell.plmn ? getOperatorName(cell.plmn) : '--'; - - return ` -
-
- ARFCN ${cell.arfcn} - ${cell.power_dbm.toFixed(0)} dBm -
-
- ${cell.freq_mhz.toFixed(1)} MHz | ${operator} - ${cell.cell_id ? ` | CID: ${cell.cell_id}` : ''} -
-
- `; - }).join(''); -} - -function getOperatorName(plmn) { - // UK operators - const operators = { - '234-10': 'O2', - '234-15': 'Vodafone', - '234-20': 'Three', - '234-30': 'EE', - '234-31': 'EE', - '234-32': 'EE', - '234-33': 'EE', - }; - return operators[plmn] || plmn; -} - -async function ismsSetGsmBaseline() { - if (ismsGsmCells.length === 0) { - ismsNotify('ISMS', 'No GSM cells to save. Run a scan first.'); - return; - } - - try { - const response = await fetch('/isms/gsm/baseline', { - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); - - const data = await response.json(); - - if (data.status === 'saved') { - ismsNotify('ISMS', `GSM baseline saved: ${data.cell_count} cells`); - } else { - ismsNotify('ISMS Error', data.message || 'Failed to save baseline'); - } - } catch (e) { - ismsNotify('ISMS Error', 'Failed to save GSM baseline'); - } -} - -// Export for global access -window.initIsmsMode = initIsmsMode; -window.ismsToggleScan = ismsToggleScan; -window.ismsRefreshTowers = ismsRefreshTowers; -window.ismsUseGPS = ismsUseGPS; -window.ismsSetManualLocation = ismsSetManualLocation; -window.ismsRefreshBaselines = ismsRefreshBaselines; -window.ismsToggleBaselineRecording = ismsToggleBaselineRecording; -window.ismsToggleBaselinePanel = ismsToggleBaselinePanel; -window.ismsToggleGsmScan = ismsToggleGsmScan; -window.ismsSetGsmBaseline = ismsSetGsmBaseline; diff --git a/templates/index.html b/templates/index.html index 3c785c0..cd0a888 100644 --- a/templates/index.html +++ b/templates/index.html @@ -112,11 +112,6 @@ APRS Amateur radio - @@ -390,7 +385,6 @@
-
@@ -409,7 +403,6 @@ - @@ -508,8 +501,6 @@ {% include 'partials/modes/tscm.html' %} - {% include 'partials/modes/isms.html' %} - @@ -1121,113 +1112,6 @@
- - -