diff --git a/intercept_agent.py b/intercept_agent.py index 58ad93e..ca4f7e9 100644 --- a/intercept_agent.py +++ b/intercept_agent.py @@ -3502,7 +3502,7 @@ class ModeManager: stations_url = 'https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=tle' satellites = load.tle_file(stations_url) - ts = load.timescale() + ts = load.timescale(builtin=True) observer = Topos(latitude_degrees=lat, longitude_degrees=lon) logger.info(f"Satellite predictor: {len(satellites)} satellites loaded") diff --git a/routes/controller.py b/routes/controller.py index 3f04970..c91bbaf 100644 --- a/routes/controller.py +++ b/routes/controller.py @@ -725,11 +725,12 @@ def agent_management_page(): return render_template('agents.html', version=VERSION) -@controller_bp.route('/monitor') -def network_monitor_page(): - """Render the network monitor page for multi-agent aggregated view.""" - from flask import render_template - return render_template('network_monitor.html') +@controller_bp.route('/monitor') +def network_monitor_page(): + """Render the network monitor page for multi-agent aggregated view.""" + from flask import render_template + from config import VERSION + return render_template('network_monitor.html', version=VERSION) # ============================================================================= diff --git a/routes/radiosonde.py b/routes/radiosonde.py index b11e840..ec86afc 100644 --- a/routes/radiosonde.py +++ b/routes/radiosonde.py @@ -66,8 +66,8 @@ AUTO_RX_PATHS = [ ] -def find_auto_rx() -> str | None: - """Find radiosonde_auto_rx script/binary.""" +def find_auto_rx() -> str | None: + """Find radiosonde_auto_rx script/binary.""" # Check PATH first path = shutil.which('radiosonde_auto_rx') if path: @@ -77,10 +77,66 @@ def find_auto_rx() -> str | None: if os.path.isfile(p) and os.access(p, os.X_OK): return p # Check for Python script (not executable but runnable) - for p in AUTO_RX_PATHS: - if os.path.isfile(p): - return p - return None + for p in AUTO_RX_PATHS: + if os.path.isfile(p): + return p + return None + + +def _iter_auto_rx_python_candidates(auto_rx_path: str): + """Yield plausible Python interpreters for radiosonde_auto_rx.""" + auto_rx_abs = os.path.abspath(auto_rx_path) + auto_rx_dir = os.path.dirname(auto_rx_abs) + install_root = os.path.dirname(auto_rx_dir) + + candidates = [ + sys.executable, + os.path.join(install_root, 'venv', 'bin', 'python'), + os.path.join(install_root, '.venv', 'bin', 'python'), + os.path.join(auto_rx_dir, 'venv', 'bin', 'python'), + os.path.join(auto_rx_dir, '.venv', 'bin', 'python'), + shutil.which('python3'), + ] + + seen: set[str] = set() + for candidate in candidates: + if not candidate: + continue + candidate_abs = os.path.abspath(candidate) + if candidate_abs in seen: + continue + seen.add(candidate_abs) + if os.path.isfile(candidate_abs) and os.access(candidate_abs, os.X_OK): + yield candidate_abs + + +def _resolve_auto_rx_python(auto_rx_path: str) -> tuple[str | None, str, list[str]]: + """Pick a Python interpreter that can import autorx.scan successfully.""" + auto_rx_dir = os.path.dirname(os.path.abspath(auto_rx_path)) + checked: list[str] = [] + last_error = 'No usable Python interpreter found' + + for python_bin in _iter_auto_rx_python_candidates(auto_rx_path): + checked.append(python_bin) + try: + dep_check = subprocess.run( + [python_bin, '-c', 'import autorx.scan'], + cwd=auto_rx_dir, + capture_output=True, + timeout=10, + ) + except Exception as exc: + last_error = str(exc) + continue + + if dep_check.returncode == 0: + return python_bin, '', checked + + stderr_output = dep_check.stderr.decode('utf-8', errors='ignore').strip() + stdout_output = dep_check.stdout.decode('utf-8', errors='ignore').strip() + last_error = stderr_output or stdout_output or f'Interpreter exited with code {dep_check.returncode}' + + return None, last_error, checked def generate_station_cfg( @@ -544,33 +600,31 @@ def start_radiosonde(): logger.error(f"Failed to generate radiosonde config: {e}") return api_error(str(e), 500) - # Build command - auto_rx -c expects the path to station.cfg - cfg_abs = os.path.abspath(cfg_path) - if auto_rx_path.endswith('.py'): - cmd = [sys.executable, auto_rx_path, '-c', cfg_abs] - else: - cmd = [auto_rx_path, '-c', cfg_abs] - - # Set cwd to the auto_rx directory so 'from autorx.scan import ...' works - auto_rx_dir = os.path.dirname(os.path.abspath(auto_rx_path)) - - # Quick dependency check before launching the full process - if auto_rx_path.endswith('.py'): - dep_check = subprocess.run( - [sys.executable, '-c', 'import autorx.scan'], - cwd=auto_rx_dir, - capture_output=True, - timeout=10, - ) - if dep_check.returncode != 0: - dep_error = dep_check.stderr.decode('utf-8', errors='ignore').strip() - logger.error(f"radiosonde_auto_rx dependency check failed:\n{dep_error}") - app_module.release_sdr_device(device_int, sdr_type_str) - return api_error( - 'radiosonde_auto_rx dependencies not satisfied. ' - f'Re-run setup.sh to install. Error: {dep_error[:500]}', - 500, - ) + # Build command - auto_rx -c expects the path to station.cfg + cfg_abs = os.path.abspath(cfg_path) + if auto_rx_path.endswith('.py'): + selected_python, dep_error, checked_interpreters = _resolve_auto_rx_python(auto_rx_path) + if not selected_python: + logger.error( + "radiosonde_auto_rx dependency check failed across interpreters %s: %s", + checked_interpreters, + dep_error, + ) + app_module.release_sdr_device(device_int, sdr_type_str) + checked_msg = ', '.join(checked_interpreters) if checked_interpreters else 'none' + return api_error( + 'radiosonde_auto_rx dependencies not satisfied. ' + 'Install or repair its Python environment (missing packages such as semver). ' + f'Checked interpreters: {checked_msg}. ' + f'Last error: {dep_error[:500]}', + 500, + ) + cmd = [selected_python, auto_rx_path, '-c', cfg_abs] + else: + cmd = [auto_rx_path, '-c', cfg_abs] + + # Set cwd to the auto_rx directory so 'from autorx.scan import ...' works + auto_rx_dir = os.path.dirname(os.path.abspath(auto_rx_path)) try: logger.info(f"Starting radiosonde_auto_rx: {' '.join(cmd)}") diff --git a/routes/satellite.py b/routes/satellite.py index 27640a5..ec051d2 100644 --- a/routes/satellite.py +++ b/routes/satellite.py @@ -30,12 +30,13 @@ satellite_bp = Blueprint('satellite', __name__, url_prefix='/satellite') _cached_timescale = None -def _get_timescale(): - global _cached_timescale - if _cached_timescale is None: - from skyfield.api import load - _cached_timescale = load.timescale() - return _cached_timescale +def _get_timescale(): + global _cached_timescale + if _cached_timescale is None: + from skyfield.api import load + # Use bundled timescale data so the first request does not block on network I/O. + _cached_timescale = load.timescale(builtin=True) + return _cached_timescale # Maximum response size for external requests (1MB) MAX_RESPONSE_SIZE = 1024 * 1024 diff --git a/routes/sstv.py b/routes/sstv.py index 31f487a..2ac73ef 100644 --- a/routes/sstv.py +++ b/routes/sstv.py @@ -472,14 +472,14 @@ def stream_progress(): return response -def _get_timescale(): - """Return a cached skyfield timescale (expensive to create).""" - global _timescale - with _timescale_lock: - if _timescale is None: - from skyfield.api import load - _timescale = load.timescale() - return _timescale +def _get_timescale(): + """Return a cached skyfield timescale (expensive to create).""" + global _timescale + with _timescale_lock: + if _timescale is None: + from skyfield.api import load + _timescale = load.timescale(builtin=True) + return _timescale @sstv_bp.route('/iss-schedule') diff --git a/static/js/core/cheat-sheets.js b/static/js/core/cheat-sheets.js index 2eab320..a3d1806 100644 --- a/static/js/core/cheat-sheets.js +++ b/static/js/core/cheat-sheets.js @@ -17,9 +17,10 @@ const CheatSheets = (function () { sstv: { title: 'ISS SSTV', icon: 'πΌοΈ', hardware: 'RTL-SDR + 145MHz antenna', description: 'Receives ISS SSTV images via slowrx.', whatToExpect: 'Color images during ISS SSTV events (PD180 mode).', tips: ['ISS SSTV: 145.800 MHz', 'Check ARISS for active event dates', 'ISS must be overhead β check pass times'] }, weathersat: { title: 'Weather Satellites', icon: 'π€οΈ', hardware: 'RTL-SDR + 137MHz turnstile/QFH antenna', description: 'Decodes NOAA APT and Meteor LRPT weather imagery via SatDump.', whatToExpect: 'Infrared/visible cloud imagery.', tips: ['NOAA 15/18/19: 137.1β137.9 MHz APT', 'Meteor M2-3: 137.9 MHz LRPT', 'Use circular polarized antenna (QFH or turnstile)'] }, sstv_general:{ title: 'HF SSTV', icon: 'π·', hardware: 'RTL-SDR + HF upconverter', description: 'Receives HF SSTV transmissions.', whatToExpect: 'Amateur radio images on 14.230 MHz (USB mode).', tips: ['14.230 MHz USB is primary HF SSTV frequency', 'Scottie 1 and Martin 1 most common', 'Best during daylight hours'] }, - gps: { title: 'GPS Receiver', icon: 'πΊοΈ', hardware: 'USB GPS receiver (NMEA)', description: 'Streams GPS position and feeds location to other modes.', whatToExpect: 'Lat/lon, altitude, speed, heading, satellite count.', tips: ['BT Locate uses GPS for trail logging', 'Set observer location for satellite prediction', 'Verify a 3D fix before relying on altitude'] }, - spaceweather:{ title: 'Space Weather', icon: 'βοΈ', hardware: 'None (NOAA/SpaceWeatherLive data)', description: 'Monitors solar activity and geomagnetic storm indices.', whatToExpect: 'Kp index, solar flux, X-ray flare alerts, CME tracking.', tips: ['High Kp (β₯5) = geomagnetic storm', 'X-class flares cause HF radio blackouts', 'Check before HF or satellite operations'] }, - tscm: { title: 'TSCM Counter-Surveillance', icon: 'π', hardware: 'WiFi + Bluetooth adapters', description: 'Technical Surveillance Countermeasures β detects hidden devices.', whatToExpect: 'RF baseline comparison, rogue device alerts, tracker detection.', tips: ['Take baseline in a known-clean environment', 'New strong signals = potential bug', 'Correlate WiFi + Bluetooth observations'] }, + gps: { title: 'GPS Receiver', icon: 'πΊοΈ', hardware: 'USB GPS receiver (NMEA)', description: 'Streams GPS position and feeds location to other modes.', whatToExpect: 'Lat/lon, altitude, speed, heading, satellite count.', tips: ['BT Locate uses GPS for trail logging', 'Set observer location for satellite prediction', 'Verify a 3D fix before relying on altitude'] }, + spaceweather:{ title: 'Space Weather', icon: 'βοΈ', hardware: 'None (NOAA/SpaceWeatherLive data)', description: 'Monitors solar activity and geomagnetic storm indices.', whatToExpect: 'Kp index, solar flux, X-ray flare alerts, CME tracking.', tips: ['High Kp (β₯5) = geomagnetic storm', 'X-class flares cause HF radio blackouts', 'Check before HF or satellite operations'] }, + controller_monitor: { title: 'Controller Monitor', icon: 'π§', hardware: 'Optional remote agents', description: 'Aggregated controller view across connected agents and local sources.', whatToExpect: 'Combined device activity, logs, and agent health in one place.', tips: ['Use it to compare what each agent is seeing', 'Check agent status before remote starts', 'Open Manage to add or troubleshoot agents'] }, + tscm: { title: 'TSCM Counter-Surveillance', icon: 'π', hardware: 'WiFi + Bluetooth adapters', description: 'Technical Surveillance Countermeasures β detects hidden devices.', whatToExpect: 'RF baseline comparison, rogue device alerts, tracker detection.', tips: ['Take baseline in a known-clean environment', 'New strong signals = potential bug', 'Correlate WiFi + Bluetooth observations'] }, spystations: { title: 'Spy Stations', icon: 'π΅οΈ', hardware: 'RTL-SDR + HF antenna', description: 'Database of known number stations, military, and diplomatic HF signals.', whatToExpect: 'Scheduled broadcasts, frequency database, tune-to links.', tips: ['Numbers stations often broadcast on the hour', 'Use Spectrum Waterfall to tune directly', 'STANAG and HF mil signals are common'] }, websdr: { title: 'WebSDR', icon: 'π', hardware: 'None (uses remote SDR servers)', description: 'Access remote WebSDR receivers worldwide for HF shortwave listening.', whatToExpect: 'Live audio from global HF receivers, waterfall display.', tips: ['websdr.org lists available servers', 'Good for HF when local antenna is lacking', 'Use in-app player for seamless experience'] }, subghz: { title: 'SubGHz Transceiver', icon: 'π‘', hardware: 'HackRF One', description: 'Transmit and receive sub-GHz RF signals for IoT and industrial protocols.', whatToExpect: 'Raw signal capture, replay, and protocol analysis.', tips: ['Only use on licensed frequencies', 'Capture mode records raw IQ for replay', 'Common: garage doors, keyfobs, 315/433/868/915 MHz'] }, diff --git a/static/js/modes/space-weather.js b/static/js/modes/space-weather.js index fa2472c..6a7554b 100644 --- a/static/js/modes/space-weather.js +++ b/static/js/modes/space-weather.js @@ -16,8 +16,13 @@ const SpaceWeather = (function () { let _xrayChart = null; // Current image selections - let _solarImageKey = 'sdo_193'; - let _drapFreq = 'drap_global'; + let _solarImageKey = 'sdo_193'; + let _drapFreq = 'drap_global'; + const SOLAR_IMAGE_FALLBACKS = { + sdo_193: 'https://sdo.gsfc.nasa.gov/assets/img/latest/latest_512_0193.jpg', + sdo_304: 'https://sdo.gsfc.nasa.gov/assets/img/latest/latest_512_0304.jpg', + sdo_magnetogram: 'https://sdo.gsfc.nasa.gov/assets/img/latest/latest_512_HMIBC.jpg', + }; /** Stable cache-bust key that rotates every 5 minutes (matches backend max-age). */ function _cacheBust() { @@ -48,33 +53,35 @@ const SpaceWeather = (function () { _fetchData(); } - function selectSolarImage(key) { - _solarImageKey = key; - _updateSolarImageTabs(); - const frame = document.getElementById('swSolarImageFrame'); - if (frame) { - frame.innerHTML = '