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 |
|
| [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
|
||||||
|
|
||||||
|
|||||||
@@ -233,64 +233,73 @@ def display_controls() -> None:
|
|||||||
|
|
||||||
st.sidebar.divider()
|
st.sidebar.divider()
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
# Time multiplier
|
# Time multiplier
|
||||||
st.sidebar.subheader("Simulation Speed")
|
time_multiplier = st.select_slider(
|
||||||
st.sidebar.select_slider(
|
|
||||||
"Time Multiplier",
|
"Time Multiplier",
|
||||||
options=[1, 2, 5, 10, 20, 50, 100],
|
options=[1, 2, 5, 10, 20, 50, 100],
|
||||||
value=10,
|
value=st.session_state.get("time_multiplier", 10),
|
||||||
format_func=lambda x: f"{x}x",
|
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.caption(f"1 real second = {time_multiplier} simulation seconds")
|
||||||
|
|
||||||
st.sidebar.divider()
|
st.divider()
|
||||||
|
|
||||||
# Temperature setpoint
|
# Temperature setpoint
|
||||||
st.sidebar.subheader("Thermal Chamber")
|
st.markdown("**Thermal Chamber**")
|
||||||
st.sidebar.slider(
|
temp_setpoint = st.slider(
|
||||||
"Temperature Setpoint (C)",
|
"Temperature Setpoint (C)",
|
||||||
min_value=-40.0,
|
min_value=-40.0,
|
||||||
max_value=125.0,
|
max_value=125.0,
|
||||||
value=25.0,
|
value=st.session_state.get("temp_setpoint", 25.0),
|
||||||
step=5.0,
|
step=5.0,
|
||||||
key="temp_setpoint",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
st.sidebar.divider()
|
st.divider()
|
||||||
|
|
||||||
# Power supply controls
|
# Power supply controls
|
||||||
st.sidebar.subheader("Power Supply")
|
st.markdown("**Power Supply**")
|
||||||
st.sidebar.slider(
|
input_voltage = st.slider(
|
||||||
"Input Voltage (V)",
|
"Input Voltage (V)",
|
||||||
min_value=0.0,
|
min_value=0.0,
|
||||||
max_value=12.0,
|
max_value=12.0,
|
||||||
value=5.0,
|
value=st.session_state.get("input_voltage", 5.0),
|
||||||
step=0.1,
|
step=0.1,
|
||||||
key="input_voltage",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
st.sidebar.toggle(
|
output_enabled = st.toggle(
|
||||||
"Output Enabled",
|
"Output Enabled",
|
||||||
value=False,
|
value=st.session_state.get("output_enabled", False),
|
||||||
key="output_enabled",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
st.sidebar.divider()
|
st.divider()
|
||||||
|
|
||||||
# Load controls
|
# Load controls
|
||||||
st.sidebar.subheader("Electronic Load")
|
st.markdown("**Electronic Load**")
|
||||||
st.sidebar.slider(
|
load_current = st.slider(
|
||||||
"Load Current (mA)",
|
"Load Current (mA)",
|
||||||
min_value=0.0,
|
min_value=0.0,
|
||||||
max_value=500.0,
|
max_value=500.0,
|
||||||
value=100.0,
|
value=st.session_state.get("load_current", 100.0),
|
||||||
step=10.0,
|
step=10.0,
|
||||||
key="load_current",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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)
|
||||||
def simulation_display() -> None:
|
def simulation_display() -> None:
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user