diff --git a/software/control/laser_engine_widget.py b/software/control/laser_engine_widget.py index d5ea9e3ac..dff84ee7f 100644 --- a/software/control/laser_engine_widget.py +++ b/software/control/laser_engine_widget.py @@ -46,6 +46,20 @@ def _engine_summary_label(status: Optional[SquidLaserEngineStatus], connection_l return "Warming up" +def _is_engine_measured(module) -> bool: + """Whether this engine actually measures the module's temperature. + + Firmware only holds ACTIVE while a channel sits at setpoint: it drops back + to WARMING_UP below -0.5 °C and to CHECK_ERROR at or above the +5 °C error + threshold. A module reporting ACTIVE far outside that band therefore is not + being regulated here — its TEC controller is wired to something else — and + its temperature/ΔT columns are floor readings rather than measurements. + """ + if module.state != LaserChannelState.ACTIVE: + return True + return -0.5 < module.setpoint_diff_c < 5.0 + + def _format_temp(info) -> str: return "/".join(f"{m.temperature_c:.1f}" for m in info.modules) + " °C" @@ -116,6 +130,11 @@ def _refresh_channel_lines(self) -> None: continue state = info.display_state on_off = "ON" if info.laser_ttl_on else "OFF" + if not any(_is_engine_measured(m) for m in info.modules): + # Showing the floor readings here would look like an ACTIVE + # channel sitting far below setpoint. Report the state only. + self._channel_lines[key].setText(f"{key:>4} {on_off:<3} {state.name:<14} TEC not engine-controlled") + continue self._channel_lines[key].setText( f"{key:>4} {on_off:<3} {state.name:<14} {_format_temp(info)} ΔT {_format_diff(info)}" ) diff --git a/software/tests/control/test_laser_engine_widget.py b/software/tests/control/test_laser_engine_widget.py new file mode 100644 index 000000000..86084c47c --- /dev/null +++ b/software/tests/control/test_laser_engine_widget.py @@ -0,0 +1,45 @@ +"""Unit tests for the Laser Engine tab's display rules.""" + +from control.laser_engine_widget import _is_engine_measured +from control.squid_laser_engine import LaserChannelState, TcmModuleInfo + + +def _module(state, setpoint_diff_c, temperature_c=25.0): + return TcmModuleInfo( + module_index=0, + state=state, + temperature_c=temperature_c, + setpoint_c=temperature_c - setpoint_diff_c, + setpoint_diff_c=setpoint_diff_c, + tec_voltage=0.0, + tec_current=0.0, + hi_temp_setpoint_c=30.0, + ) + + +class TestIsEngineMeasured: + def test_active_at_setpoint_is_measured(self): + assert _is_engine_measured(_module(LaserChannelState.ACTIVE, 0.0)) + + def test_active_within_band_is_measured(self): + # Firmware tolerates ACTIVE anywhere in (-0.5, +5.0). + assert _is_engine_measured(_module(LaserChannelState.ACTIVE, -0.4)) + assert _is_engine_measured(_module(LaserChannelState.ACTIVE, 4.9)) + + def test_active_far_below_setpoint_is_not_measured(self): + # A channel the engine regulates cannot hold ACTIVE here; it would have + # dropped to WARMING_UP. So this reading is a floor value, not a + # measurement. Matches 638/730 on hardware: 0.4 C with dT -24.6. + assert not _is_engine_measured(_module(LaserChannelState.ACTIVE, -24.6, temperature_c=0.4)) + + def test_active_at_error_threshold_is_not_measured(self): + assert not _is_engine_measured(_module(LaserChannelState.ACTIVE, 5.0)) + + def test_warming_up_far_from_setpoint_is_measured(self): + # Genuinely warming: far from setpoint is expected and the numbers are + # real, so they must still be shown. + assert _is_engine_measured(_module(LaserChannelState.WARMING_UP, -66.9, temperature_c=27.2)) + + def test_sleep_is_measured(self): + # A sleeping channel drifts away from setpoint; still a real reading. + assert _is_engine_measured(_module(LaserChannelState.SLEEP, -42.3, temperature_c=51.8))