diff --git a/src/py_dvt_ate/app/dashboard/app.py b/src/py_dvt_ate/app/dashboard/app.py index f8f099a..c513098 100644 --- a/src/py_dvt_ate/app/dashboard/app.py +++ b/src/py_dvt_ate/app/dashboard/app.py @@ -8,7 +8,6 @@ thermal-electrical coupling in real-time using instrument interfaces. import asyncio import atexit import os -import sys import threading import time from collections import deque @@ -29,40 +28,36 @@ from py_dvt_ate.tests.thermal.tempco import TempCoTest # Thread pool for background test execution _test_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="test_runner") -# Idle shutdown configuration -# Set IDLE_TIMEOUT_MINUTES=0 to disable auto-shutdown -IDLE_TIMEOUT_MINUTES = int(os.environ.get("IDLE_TIMEOUT_MINUTES", "0")) +# Idle pause configuration +# Physics engine pauses after IDLE_PAUSE_SECONDS of no activity (default: 30s) +IDLE_PAUSE_SECONDS = int(os.environ.get("IDLE_PAUSE_SECONDS", "30")) _last_activity_time: float = time.time() -_idle_monitor_started = False +_idle_checker_started = False +_server_ref: SimulationServer | None = None # Reference for idle checker thread -def _update_activity() -> None: - """Update the last activity timestamp.""" - global _last_activity_time - _last_activity_time = time.time() - - -def _idle_monitor() -> None: - """Background thread that exits the app after idle timeout.""" - global _last_activity_time +def _idle_checker() -> None: + """Background thread that pauses physics when idle.""" + global _last_activity_time, _server_ref while True: - time.sleep(60) # Check every minute - if IDLE_TIMEOUT_MINUTES <= 0: + time.sleep(5) # Check every 5 seconds + if _server_ref is None: continue - idle_minutes = (time.time() - _last_activity_time) / 60 - if idle_minutes >= IDLE_TIMEOUT_MINUTES: - print(f"Idle timeout reached ({IDLE_TIMEOUT_MINUTES} minutes). Shutting down.") - os._exit(0) + + idle_seconds = time.time() - _last_activity_time + if idle_seconds > IDLE_PAUSE_SECONDS and not _server_ref.paused: + _server_ref.paused = True + print(f"Physics engine paused (idle for {idle_seconds:.0f}s)") -def _start_idle_monitor() -> None: - """Start the idle monitor thread if timeout is configured.""" - global _idle_monitor_started - if IDLE_TIMEOUT_MINUTES > 0 and not _idle_monitor_started: - _idle_monitor_started = True - thread = threading.Thread(target=_idle_monitor, daemon=True) +def _start_idle_checker(server: SimulationServer) -> None: + """Start the idle checker thread.""" + global _idle_checker_started, _server_ref + _server_ref = server + if not _idle_checker_started: + _idle_checker_started = True + thread = threading.Thread(target=_idle_checker, daemon=True) thread.start() - print(f"Idle auto-shutdown enabled: {IDLE_TIMEOUT_MINUTES} minutes") # History buffer size for charts HISTORY_SIZE = 500 @@ -159,12 +154,21 @@ def start_embedded_server() -> tuple[SimulationServer, threading.Thread]: return server, thread +def _update_activity() -> None: + """Update activity timestamp and resume physics if paused.""" + global _last_activity_time + _last_activity_time = time.time() + + # Resume physics if it was paused + if "server" in st.session_state: + server = st.session_state.server + if server.paused: + server.paused = False + print("Physics engine resumed (user activity detected)") + + def init_session_state() -> None: """Initialise Streamlit session state.""" - # Start idle monitor for auto-shutdown - _start_idle_monitor() - _update_activity() - if "server" not in st.session_state: with st.spinner("Starting simulation server..."): st.session_state.server, st.session_state.server_thread = start_embedded_server() @@ -174,6 +178,9 @@ def init_session_state() -> None: st.error("Failed to start simulation server. Please refresh the page.") st.stop() + # Start idle checker to pause physics when no one's viewing + _start_idle_checker(st.session_state.server) + # Register cleanup def cleanup() -> None: if hasattr(st.session_state, "server") and st.session_state.server is not None: @@ -427,7 +434,7 @@ def display_controls() -> None: @st.fragment(run_every=0.1) def simulation_display() -> None: """Fragment that displays and updates simulation state.""" - _update_activity() # Track that someone is viewing the dashboard + _update_activity() # Track activity and resume physics if paused if "server" not in st.session_state: st.warning("Initializing simulation server...")