diff --git a/config.py b/config.py index d642c96..b468f07 100644 --- a/config.py +++ b/config.py @@ -139,7 +139,7 @@ def _get_env_bool(key: str, default: bool) -> bool: # Logging configuration -_log_level_str = _get_env('LOG_LEVEL', 'WARNING').upper() +_log_level_str = _get_env('LOG_LEVEL', 'INFO').upper() LOG_LEVEL = getattr(logging, _log_level_str, logging.WARNING) LOG_FORMAT = _get_env('LOG_FORMAT', '%(asctime)s - %(levelname)s - %(message)s') diff --git a/static/css/modes/weather-satellite.css b/static/css/modes/weather-satellite.css index 7712f41..d2589b5 100644 --- a/static/css/modes/weather-satellite.css +++ b/static/css/modes/weather-satellite.css @@ -787,4 +787,169 @@ .wxsat-gallery-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + + .wxsat-phase-indicator { + display: none; + } +} + +/* ===== Signal Console ===== */ +.wxsat-signal-console { + display: none; + flex-direction: column; + border-bottom: 1px solid var(--border-color, #2a3040); + background: var(--bg-secondary, #141820); +} + +.wxsat-signal-console.active { + display: flex; +} + +.wxsat-console-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 16px; + background: var(--bg-tertiary, #1a1f2e); + border-bottom: 1px solid var(--border-color, #2a3040); + min-height: 32px; +} + +.wxsat-console-title-group { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 0; +} + +.wxsat-console-title { + font-size: 10px; + font-weight: 600; + color: var(--text-dim, #666); + text-transform: uppercase; + letter-spacing: 1px; + flex-shrink: 0; +} + +.wxsat-phase-indicator { + display: flex; + align-items: center; + gap: 4px; + font-family: 'JetBrains Mono', monospace; +} + +.wxsat-phase-step { + font-size: 9px; + padding: 2px 6px; + border-radius: 3px; + color: var(--text-dim, #555); + background: transparent; + border: 1px solid var(--border-color, #2a3040); + transition: all 0.3s ease; + letter-spacing: 0.5px; +} + +.wxsat-phase-step.active { + color: #00ff88; + border-color: #00ff88; + background: rgba(0, 255, 136, 0.1); + box-shadow: 0 0 8px rgba(0, 255, 136, 0.2); +} + +.wxsat-phase-step.completed { + color: var(--accent-cyan, #00d4ff); + border-color: rgba(0, 212, 255, 0.3); + background: rgba(0, 212, 255, 0.05); + opacity: 0.7; +} + +.wxsat-phase-step.error { + color: #ff4444; + border-color: #ff4444; + background: rgba(255, 68, 68, 0.1); + box-shadow: 0 0 8px rgba(255, 68, 68, 0.2); +} + +.wxsat-phase-arrow { + font-size: 8px; + color: var(--text-dim, #444); +} + +#wxsatConsoleToggle { + font-size: 10px; + width: 28px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + flex-shrink: 0; + transition: transform 0.2s; +} + +#wxsatConsoleToggle.collapsed { + transform: rotate(-90deg); +} + +.wxsat-console-body { + max-height: 160px; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.wxsat-console-body.collapsed { + max-height: 0; +} + +.wxsat-console-log { + overflow-y: auto; + max-height: 160px; + padding: 6px 12px; + background: var(--bg-primary, #0d1117); + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + line-height: 1.6; +} + +.wxsat-console-entry { + padding: 1px 0 1px 8px; + border-left: 2px solid transparent; + color: var(--text-secondary, #999); + word-break: break-all; +} + +.wxsat-console-entry.wxsat-log-info { + border-left-color: var(--border-color, #2a3040); + color: var(--text-dim, #777); +} + +.wxsat-console-entry.wxsat-log-signal { + border-left-color: #00ff88; + color: #00ff88; +} + +.wxsat-console-entry.wxsat-log-progress { + border-left-color: var(--accent-cyan, #00d4ff); + color: var(--accent-cyan, #00d4ff); +} + +.wxsat-console-entry.wxsat-log-save { + border-left-color: #ffbb00; + color: #ffbb00; +} + +.wxsat-console-entry.wxsat-log-error { + border-left-color: #ff4444; + color: #ff4444; +} + +.wxsat-console-entry.wxsat-log-warning { + border-left-color: #ff8800; + color: #ff8800; +} + +.wxsat-console-entry.wxsat-log-debug { + border-left-color: transparent; + color: var(--text-dim, #555); } diff --git a/static/js/modes/weather-satellite.js b/static/js/modes/weather-satellite.js index 8601380..a4a6327 100644 --- a/static/js/modes/weather-satellite.js +++ b/static/js/modes/weather-satellite.js @@ -17,6 +17,10 @@ const WeatherSat = (function() { let groundMap = null; let groundTrackLayer = null; let observerMarker = null; + let consoleEntries = []; + let consoleCollapsed = false; + let currentPhase = 'idle'; + let consoleAutoHideTimer = null; /** * Initialize the Weather Satellite mode @@ -160,6 +164,10 @@ const WeatherSat = (function() { const biasT = biasTInput?.checked || false; const device = parseInt(deviceSelect?.value || '0', 10); + clearConsole(); + showConsole(true); + updatePhaseIndicator('tuning'); + addConsoleEntry('Starting capture...', 'info'); updateStatusUI('connecting', 'Starting...'); try { @@ -313,6 +321,11 @@ const WeatherSat = (function() { if (captureElapsed) captureElapsed.textContent = formatElapsed(data.elapsed_seconds || 0); if (progressBar) progressBar.style.width = (data.progress || 0) + '%'; + // Console updates + showConsole(true); + if (data.message) addConsoleEntry(data.message, data.log_type || 'info'); + if (data.capture_phase) updatePhaseIndicator(data.capture_phase); + } else if (data.status === 'complete') { if (data.image) { images.unshift(data.image); @@ -327,12 +340,20 @@ const WeatherSat = (function() { if (!schedulerEnabled) stopStream(); updateStatusUI('idle', 'Capture complete'); if (captureStatus) captureStatus.classList.remove('active'); + + addConsoleEntry('Capture complete', 'signal'); + updatePhaseIndicator('complete'); + consoleAutoHideTimer = setTimeout(() => showConsole(false), 30000); } } else if (data.status === 'error') { updateStatusUI('idle', 'Error'); showNotification('Weather Sat', data.message || 'Capture error'); if (captureStatus) captureStatus.classList.remove('active'); + + if (data.message) addConsoleEntry(data.message, 'error'); + updatePhaseIndicator('error'); + consoleAutoHideTimer = setTimeout(() => showConsole(false), 15000); } } @@ -1084,6 +1105,108 @@ const WeatherSat = (function() { } } + // ======================== + // Decoder Console + // ======================== + + /** + * Add an entry to the decoder console log + */ + function addConsoleEntry(message, logType) { + const log = document.getElementById('wxsatConsoleLog'); + if (!log) return; + + const entry = document.createElement('div'); + entry.className = `wxsat-console-entry wxsat-log-${logType || 'info'}`; + entry.textContent = message; + log.appendChild(entry); + + consoleEntries.push(entry); + + // Cap at 200 entries + while (consoleEntries.length > 200) { + const old = consoleEntries.shift(); + if (old.parentNode) old.parentNode.removeChild(old); + } + + // Auto-scroll to bottom + log.scrollTop = log.scrollHeight; + } + + /** + * Update the phase indicator steps + */ + function updatePhaseIndicator(phase) { + if (!phase || phase === currentPhase) return; + currentPhase = phase; + + const phases = ['tuning', 'listening', 'signal_detected', 'decoding', 'complete']; + const phaseIndex = phases.indexOf(phase); + const isError = phase === 'error'; + + document.querySelectorAll('#wxsatPhaseIndicator .wxsat-phase-step').forEach(step => { + const stepPhase = step.dataset.phase; + const stepIndex = phases.indexOf(stepPhase); + + step.classList.remove('active', 'completed', 'error'); + + if (isError) { + if (stepPhase === currentPhase || stepIndex === phaseIndex) { + step.classList.add('error'); + } + } else if (stepIndex === phaseIndex) { + step.classList.add('active'); + } else if (stepIndex < phaseIndex && phaseIndex >= 0) { + step.classList.add('completed'); + } + }); + } + + /** + * Show or hide the decoder console + */ + function showConsole(visible) { + const el = document.getElementById('wxsatSignalConsole'); + if (el) el.classList.toggle('active', visible); + + if (consoleAutoHideTimer) { + clearTimeout(consoleAutoHideTimer); + consoleAutoHideTimer = null; + } + } + + /** + * Toggle console body collapsed state + */ + function toggleConsole() { + const body = document.getElementById('wxsatConsoleBody'); + const btn = document.getElementById('wxsatConsoleToggle'); + if (!body) return; + + consoleCollapsed = !consoleCollapsed; + body.classList.toggle('collapsed', consoleCollapsed); + if (btn) btn.classList.toggle('collapsed', consoleCollapsed); + } + + /** + * Clear console entries and reset phase indicator + */ + function clearConsole() { + const log = document.getElementById('wxsatConsoleLog'); + if (log) log.innerHTML = ''; + consoleEntries = []; + currentPhase = 'idle'; + + document.querySelectorAll('#wxsatPhaseIndicator .wxsat-phase-step').forEach(step => { + step.classList.remove('active', 'completed', 'error'); + }); + + if (consoleAutoHideTimer) { + clearTimeout(consoleAutoHideTimer); + consoleAutoHideTimer = null; + } + } + // Public API return { init, @@ -1098,6 +1221,7 @@ const WeatherSat = (function() { useGPS, toggleScheduler, invalidateMap, + toggleConsole, }; })(); diff --git a/templates/index.html b/templates/index.html index b75029b..e6dd575 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1970,6 +1970,33 @@ + +
+
+
+ DECODER CONSOLE +
+ TUNING + + LISTENING + + SIGNAL + + DECODING + + COMPLETE +
+
+ +
+
+
+
Waiting for capture...
+
+
+
+
diff --git a/utils/weather_sat.py b/utils/weather_sat.py index dc465ae..818a2a3 100644 --- a/utils/weather_sat.py +++ b/utils/weather_sat.py @@ -110,6 +110,8 @@ class CaptureProgress: progress_percent: int = 0 elapsed_seconds: int = 0 image: WeatherSatImage | None = None + log_type: str = '' # 'info', 'debug', 'progress', 'error', 'signal', 'save', 'warning' + capture_phase: str = '' # 'tuning', 'listening', 'signal_detected', 'decoding', 'complete', 'error' def to_dict(self) -> dict: result = { @@ -121,6 +123,8 @@ class CaptureProgress: 'message': self.message, 'progress': self.progress_percent, 'elapsed_seconds': self.elapsed_seconds, + 'log_type': self.log_type, + 'capture_phase': self.capture_phase, } if self.image: result['image'] = self.image.to_dict() @@ -150,6 +154,7 @@ class WeatherSatDecoder: self._device_index: int = 0 self._capture_output_dir: Path | None = None self._on_complete_callback: Callable[[], None] | None = None + self._capture_phase: str = 'idle' # Ensure output directory exists self._output_dir.mkdir(parents=True, exist_ok=True) @@ -240,6 +245,7 @@ class WeatherSatDecoder: self._current_mode = sat_info['mode'] self._device_index = device_index self._capture_start_time = time.time() + self._capture_phase = 'tuning' try: self._start_satdump(sat_info, device_index, gain, sample_rate, bias_t) @@ -254,7 +260,9 @@ class WeatherSatDecoder: satellite=satellite, frequency=sat_info['frequency'], mode=sat_info['mode'], - message=f"Capturing {sat_info['name']} on {sat_info['frequency']} MHz ({sat_info['mode']})..." + message=f"Capturing {sat_info['name']} on {sat_info['frequency']} MHz ({sat_info['mode']})...", + log_type='info', + capture_phase=self._capture_phase, )) return True @@ -345,6 +353,24 @@ class WeatherSatDecoder: ) self._watcher_thread.start() + @staticmethod + def _classify_log_type(line: str) -> str: + """Classify a SatDump output line into a log type.""" + lower = line.lower() + if '(e)' in lower or 'error' in lower or 'fail' in lower: + return 'error' + if 'progress' in lower and '%' in line: + return 'progress' + if 'saved' in lower or 'writing' in lower: + return 'save' + if 'detected' in lower or 'lock' in lower or 'sync' in lower: + return 'signal' + if '(w)' in lower: + return 'warning' + if '(d)' in lower: + return 'debug' + return 'info' + @staticmethod def _resolve_device_id(device_index: int) -> str: """Resolve RTL-SDR device index to serial number string for SatDump v1.2+. @@ -400,9 +426,22 @@ class WeatherSatDecoder: elapsed = int(time.time() - self._capture_start_time) now = time.time() + log_type = self._classify_log_type(line) + + # Track phase transitions + lower = line.lower() + if log_type == 'signal': + self._capture_phase = 'signal_detected' + elif log_type == 'progress': + self._capture_phase = 'decoding' + elif self._capture_phase == 'tuning' and ( + 'freq' in lower or 'processing' in lower + or 'starting' in lower or 'source' in lower + ): + self._capture_phase = 'listening' # Parse progress from SatDump output - if 'Progress' in line or 'progress' in line: + if log_type == 'progress': match = re.search(r'(\d+(?:\.\d+)?)\s*%', line) pct = int(float(match.group(1))) if match else 0 self._emit_progress(CaptureProgress( @@ -413,9 +452,11 @@ class WeatherSatDecoder: message=line, progress_percent=pct, elapsed_seconds=elapsed, + log_type=log_type, + capture_phase=self._capture_phase, )) last_emit_time = now - elif 'Saved' in line or 'saved' in line or 'Writing' in line: + elif log_type == 'save': self._emit_progress(CaptureProgress( status='decoding', satellite=self._current_satellite, @@ -423,9 +464,11 @@ class WeatherSatDecoder: mode=self._current_mode, message=line, elapsed_seconds=elapsed, + log_type=log_type, + capture_phase=self._capture_phase, )) last_emit_time = now - elif 'error' in line.lower() or 'fail' in line.lower(): + elif log_type == 'error': self._emit_progress(CaptureProgress( status='capturing', satellite=self._current_satellite, @@ -433,11 +476,25 @@ class WeatherSatDecoder: mode=self._current_mode, message=line, elapsed_seconds=elapsed, + log_type=log_type, + capture_phase=self._capture_phase, + )) + last_emit_time = now + elif log_type == 'signal': + self._emit_progress(CaptureProgress( + status='capturing', + satellite=self._current_satellite, + frequency=self._current_frequency, + mode=self._current_mode, + message=line, + elapsed_seconds=elapsed, + log_type=log_type, + capture_phase=self._capture_phase, )) last_emit_time = now else: - # Emit all output lines, throttled to every 2 seconds - if now - last_emit_time >= 2.0: + # Emit other lines, throttled to every 0.5 seconds + if now - last_emit_time >= 0.5: self._emit_progress(CaptureProgress( status='capturing', satellite=self._current_satellite, @@ -445,6 +502,8 @@ class WeatherSatDecoder: mode=self._current_mode, message=line, elapsed_seconds=elapsed, + log_type=log_type, + capture_phase=self._capture_phase, )) last_emit_time = now @@ -457,6 +516,7 @@ class WeatherSatDecoder: elapsed = int(time.time() - self._capture_start_time) if self._capture_start_time else 0 if was_running: + self._capture_phase = 'complete' self._emit_progress(CaptureProgress( status='complete', satellite=self._current_satellite, @@ -464,6 +524,8 @@ class WeatherSatDecoder: mode=self._current_mode, message=f"Capture complete ({elapsed}s)", elapsed_seconds=elapsed, + log_type='info', + capture_phase='complete', )) # Notify route layer to release SDR device