From a28752fc5bc5c689f05139fa02445d0579ecf722 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Sat, 15 Nov 2025 13:18:38 +0000 Subject: [PATCH] Polish dashboard UX and update README - Wrap simulation controls in form to prevent page reruns on change - Fix TempCo test configs to use 2+ temperature points - Add Installation, Quick Start, and usage examples to README --- README.md | 75 +++++++++++++++++- src/py_dvt_ate/app/dashboard/app.py | 119 +++++++++++++++------------- src/py_dvt_ate/data/repository.py | 13 +++ tests/integration/test_e2e.py | 14 +++- 4 files changed, 162 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 5e2ade3..b901459 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,82 @@ ThermalATE enables offline development of ATE (Automated Test Equipment) charact | [Technical Specification](docs/02_technical_specification.md) | Specifies **how** to implement the system | | [Architecture Decisions](docs/03_architecture_decisions.md) | Explains **why** decisions were made | +## Installation + +```bash +# Clone the repository +git clone https://github.com/yourrepo/py_dvt_ate.git +cd py_dvt_ate + +# Install with development dependencies +pip install -e ".[dev]" +``` + +## Quick Start + +### Interactive Dashboard + +Launch the Streamlit dashboard to visualise the physics simulation and run tests: + +```bash +py-dvt-ate dashboard +``` + +This opens a browser window with: +- **Live Simulation** - Real-time temperature/voltage charts with physics coupling +- **Test Execution** - Run TempCo characterisation tests +- **Results Viewer** - Browse and analyse historical test results + +### CLI Commands + +```bash +# Start the simulation server (TCP ports for SCPI instruments) +py-dvt-ate serve + +# List available tests +py-dvt-ate tests list + +# Run a TempCo test +py-dvt-ate tests run tempco --config config/tempco_test.yaml +``` + +### Programmatic API + +```python +from py_dvt_ate.instruments import InstrumentFactory +from py_dvt_ate.simulation import SimulationServer + +# Start simulation server +server = SimulationServer() +server.start() + +# Create instruments via HAL +factory = InstrumentFactory() +instruments = factory.create_from_config("config/default.yaml") + +# Control instruments using standard interfaces +instruments.chamber.set_temperature(85.0) +instruments.psu.set_voltage(1, 5.0) +instruments.psu.enable_output(1, True) +voltage = instruments.dmm.measure_dc_voltage() + +print(f"Output voltage: {voltage:.4f} V") +``` + ## Project Status -**Status:** In Development +**Status:** MVP Complete (v0.1.0) -This project is currently being developed. See the requirements document for the full scope and success criteria. +The core vertical slice is functional: +- Physics engine with thermal-electrical coupling +- Virtual instruments (chamber, PSU, DMM) +- Hardware Abstraction Layer +- SCPI-over-TCP server +- Test framework with TempCo test +- Streamlit dashboard +- SQLite/Parquet data persistence + +See the requirements document for the full scope and future phases. ## Technology Stack diff --git a/src/py_dvt_ate/app/dashboard/app.py b/src/py_dvt_ate/app/dashboard/app.py index d8fea32..1ee7c3b 100644 --- a/src/py_dvt_ate/app/dashboard/app.py +++ b/src/py_dvt_ate/app/dashboard/app.py @@ -233,63 +233,72 @@ def display_controls() -> None: st.sidebar.divider() - # Time multiplier - st.sidebar.subheader("Simulation Speed") - st.sidebar.select_slider( - "Time Multiplier", - options=[1, 2, 5, 10, 20, 50, 100], - value=10, - format_func=lambda x: f"{x}x", - key="time_multiplier", - ) - st.sidebar.caption( - f"1 real second = {st.session_state.get('time_multiplier', 10)} simulation seconds" - ) + # Parameter Controls - wrapped in form to prevent reruns on every change + with st.sidebar.form("parameter_controls"): + st.subheader("Simulation Parameters") + st.caption("💡 Change parameters below, then click Apply to update the simulation") - st.sidebar.divider() + # Time multiplier + time_multiplier = st.select_slider( + "Time Multiplier", + options=[1, 2, 5, 10, 20, 50, 100], + value=st.session_state.get("time_multiplier", 10), + format_func=lambda x: f"{x}x", + ) + st.caption(f"1 real second = {time_multiplier} simulation seconds") - # Temperature setpoint - st.sidebar.subheader("Thermal Chamber") - st.sidebar.slider( - "Temperature Setpoint (C)", - min_value=-40.0, - max_value=125.0, - value=25.0, - step=5.0, - key="temp_setpoint", - ) + st.divider() - st.sidebar.divider() + # Temperature setpoint + st.markdown("**Thermal Chamber**") + temp_setpoint = st.slider( + "Temperature Setpoint (C)", + min_value=-40.0, + max_value=125.0, + value=st.session_state.get("temp_setpoint", 25.0), + step=5.0, + ) - # Power supply controls - st.sidebar.subheader("Power Supply") - st.sidebar.slider( - "Input Voltage (V)", - min_value=0.0, - max_value=12.0, - value=5.0, - step=0.1, - key="input_voltage", - ) + st.divider() - st.sidebar.toggle( - "Output Enabled", - value=False, - key="output_enabled", - ) + # Power supply controls + st.markdown("**Power Supply**") + input_voltage = st.slider( + "Input Voltage (V)", + min_value=0.0, + max_value=12.0, + value=st.session_state.get("input_voltage", 5.0), + step=0.1, + ) - st.sidebar.divider() + output_enabled = st.toggle( + "Output Enabled", + value=st.session_state.get("output_enabled", False), + ) - # Load controls - st.sidebar.subheader("Electronic Load") - st.sidebar.slider( - "Load Current (mA)", - min_value=0.0, - max_value=500.0, - value=100.0, - step=10.0, - key="load_current", - ) + st.divider() + + # Load controls + st.markdown("**Electronic Load**") + load_current = st.slider( + "Load Current (mA)", + min_value=0.0, + max_value=500.0, + value=st.session_state.get("load_current", 100.0), + step=10.0, + ) + + # Apply button + submitted = st.form_submit_button("✅ Apply Changes", type="primary", use_container_width=True) + + if submitted: + # Update session state with new values + st.session_state.time_multiplier = time_multiplier + st.session_state.temp_setpoint = temp_setpoint + st.session_state.input_voltage = input_voltage + st.session_state.output_enabled = output_enabled + st.session_state.load_current = load_current + st.rerun() @st.fragment(run_every=0.1) @@ -613,13 +622,13 @@ def test_execution_page() -> None: runner: TestRunner = st.session_state.test_runner instruments: InstrumentSet = st.session_state.instruments - # Connect instruments before running test + # Instruments should already be connected from dashboard startup + # Just verify they're connected try: - instruments.chamber.transport.connect() # type: ignore[attr-defined] - instruments.psu.transport.connect() # type: ignore[attr-defined] - instruments.dmm.transport.connect() # type: ignore[attr-defined] + # Quick connectivity check + _ = instruments.chamber.get_temperature() except Exception as e: - st.error(f"Failed to connect to instruments: {e}") + st.error(f"Instruments not available: {e}") st.session_state.test_running = False st.stop() diff --git a/src/py_dvt_ate/data/repository.py b/src/py_dvt_ate/data/repository.py index d5012ae..7ba75ba 100644 --- a/src/py_dvt_ate/data/repository.py +++ b/src/py_dvt_ate/data/repository.py @@ -74,6 +74,9 @@ class ITestRepository(ABC): def get_all_runs(self) -> list[TestRun]: """Retrieve all test runs, ordered by started_at descending.""" + def close(self) -> None: + """Close repository and release resources. Optional to implement.""" + class SQLiteRepository(ITestRepository): """SQLite-based repository for test data. @@ -400,3 +403,13 @@ class SQLiteRepository(ITestRepository): ) for row in rows ] + + def close(self) -> None: + """Close repository and release resources. + + SQLite connections are managed via context managers and auto-close. + This method performs explicit cleanup for Windows file handle issues. + """ + # Force garbage collection to release any lingering connections + import gc + gc.collect() diff --git a/tests/integration/test_e2e.py b/tests/integration/test_e2e.py index 703f1de..abcf84f 100644 --- a/tests/integration/test_e2e.py +++ b/tests/integration/test_e2e.py @@ -118,6 +118,9 @@ def test_e2e_tempco_characterization(simulation_server: ServerConfig) -> None: # Timestamps should be monotonically increasing assert measurements_df["timestamp"].is_monotonic_increasing + # Cleanup: close repository before tempdir cleanup (Windows file locking) + repository.close() + def test_e2e_server_lifecycle(simulation_server: ServerConfig) -> None: """Test simulation server lifecycle management. @@ -240,17 +243,21 @@ def test_e2e_multiple_test_runs(simulation_server: ServerConfig) -> None: test = TempCoTest() config1 = { - "temperatures": [25.0], + "temperatures": [0.0, 25.0], # Need at least 2 points for TempCo "input_voltage": 5.0, "load_current": 0.1, "settle_time": 0.5, + "num_samples": 3, + "tempco_limit": 100.0, } config2 = { - "temperatures": [25.0], + "temperatures": [25.0, 50.0], # Need at least 2 points for TempCo "input_voltage": 3.3, "load_current": 0.05, "settle_time": 0.5, + "num_samples": 3, + "tempco_limit": 100.0, } run_id1 = runner.run_test(test, instruments, config1, operator="test_user_1") @@ -277,3 +284,6 @@ def test_e2e_multiple_test_runs(simulation_server: ServerConfig) -> None: assert len(all_runs) >= 2 assert any(r.id == str(run_id1) for r in all_runs) assert any(r.id == str(run_id2) for r in all_runs) + + # Cleanup: close repository before tempdir cleanup (Windows file locking) + repository.close()