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:
75
README.md
75
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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user