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
This commit is contained in:
2025-11-15 13:18:38 +00:00
parent 5152f85c8e
commit a28752fc5b
4 changed files with 162 additions and 59 deletions

View File

@@ -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 | | [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 | | [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 ## 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 ## Technology Stack

View File

@@ -233,63 +233,72 @@ def display_controls() -> None:
st.sidebar.divider() st.sidebar.divider()
# Time multiplier # Parameter Controls - wrapped in form to prevent reruns on every change
st.sidebar.subheader("Simulation Speed") with st.sidebar.form("parameter_controls"):
st.sidebar.select_slider( st.subheader("Simulation Parameters")
"Time Multiplier", st.caption("💡 Change parameters below, then click Apply to update the simulation")
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"
)
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.divider()
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.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.divider()
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.sidebar.toggle( # Power supply controls
"Output Enabled", st.markdown("**Power Supply**")
value=False, input_voltage = st.slider(
key="output_enabled", "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.divider()
st.sidebar.subheader("Electronic Load")
st.sidebar.slider( # Load controls
"Load Current (mA)", st.markdown("**Electronic Load**")
min_value=0.0, load_current = st.slider(
max_value=500.0, "Load Current (mA)",
value=100.0, min_value=0.0,
step=10.0, max_value=500.0,
key="load_current", 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) @st.fragment(run_every=0.1)
@@ -613,13 +622,13 @@ def test_execution_page() -> None:
runner: TestRunner = st.session_state.test_runner runner: TestRunner = st.session_state.test_runner
instruments: InstrumentSet = st.session_state.instruments 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: try:
instruments.chamber.transport.connect() # type: ignore[attr-defined] # Quick connectivity check
instruments.psu.transport.connect() # type: ignore[attr-defined] _ = instruments.chamber.get_temperature()
instruments.dmm.transport.connect() # type: ignore[attr-defined]
except Exception as e: 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.session_state.test_running = False
st.stop() st.stop()

View File

@@ -74,6 +74,9 @@ class ITestRepository(ABC):
def get_all_runs(self) -> list[TestRun]: def get_all_runs(self) -> list[TestRun]:
"""Retrieve all test runs, ordered by started_at descending.""" """Retrieve all test runs, ordered by started_at descending."""
def close(self) -> None:
"""Close repository and release resources. Optional to implement."""
class SQLiteRepository(ITestRepository): class SQLiteRepository(ITestRepository):
"""SQLite-based repository for test data. """SQLite-based repository for test data.
@@ -400,3 +403,13 @@ class SQLiteRepository(ITestRepository):
) )
for row in rows 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()

View File

@@ -118,6 +118,9 @@ def test_e2e_tempco_characterization(simulation_server: ServerConfig) -> None:
# Timestamps should be monotonically increasing # Timestamps should be monotonically increasing
assert measurements_df["timestamp"].is_monotonic_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: def test_e2e_server_lifecycle(simulation_server: ServerConfig) -> None:
"""Test simulation server lifecycle management. """Test simulation server lifecycle management.
@@ -240,17 +243,21 @@ def test_e2e_multiple_test_runs(simulation_server: ServerConfig) -> None:
test = TempCoTest() test = TempCoTest()
config1 = { config1 = {
"temperatures": [25.0], "temperatures": [0.0, 25.0], # Need at least 2 points for TempCo
"input_voltage": 5.0, "input_voltage": 5.0,
"load_current": 0.1, "load_current": 0.1,
"settle_time": 0.5, "settle_time": 0.5,
"num_samples": 3,
"tempco_limit": 100.0,
} }
config2 = { config2 = {
"temperatures": [25.0], "temperatures": [25.0, 50.0], # Need at least 2 points for TempCo
"input_voltage": 3.3, "input_voltage": 3.3,
"load_current": 0.05, "load_current": 0.05,
"settle_time": 0.5, "settle_time": 0.5,
"num_samples": 3,
"tempco_limit": 100.0,
} }
run_id1 = runner.run_test(test, instruments, config1, operator="test_user_1") 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 len(all_runs) >= 2
assert any(r.id == str(run_id1) for r in all_runs) assert any(r.id == str(run_id1) for r in all_runs)
assert any(r.id == str(run_id2) 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()