"""gui.jog_panel: the camera window's T3R and BBD202 jog controls. The panels are the only place these devices are driven by a held button, so what matters here is that press/release map onto the right pair of commands and that the operator's velocity/microstep settings ride along. """ import pytest from PyQt6.QtCore import QObject, pyqtSignal from PyQt6.QtWidgets import QApplication import hardware.t3r_protocol as proto from gui.jog_panel import BBDJogPanel, T3RJogPanel @pytest.fixture(scope="module") def qapp(): yield QApplication.instance() or QApplication([]) class FakeT3R(QObject): """The slice of QtT3RAdapter the T3R panel touches.""" handshake_ok = pyqtSignal(int, int, int) disconnected = pyqtSignal(str) info_updated = pyqtSignal(int, object) def __init__(self, is_open=True): super().__init__() self.is_open = is_open self.calls = [] def enable(self, ch): self.calls.append(("enable", ch)) def disable(self, ch): self.calls.append(("disable", ch)) def set_microstep(self, ch, microsteps): self.calls.append(("set_microstep", ch, microsteps)) def jog(self, ch, velocity, accel): self.calls.append(("jog", ch, velocity, accel)) def stop(self, ch, hard): self.calls.append(("stop", ch, hard)) class FakeBBD(QObject): """The slice of BBD202Worker the BBD panel touches.""" connected = pyqtSignal() disconnected = pyqtSignal() position_updated = pyqtSignal(float, float) def __init__(self, is_connected=True): super().__init__() self.is_connected = is_connected self.calls = [] def queue_jog(self, axis, direction, step_mm=None): self.calls.append(("jog", axis, direction, step_mm)) def queue_set_velocity(self, max_velocity, acceleration): self.calls.append(("velocity", max_velocity, acceleration)) def _info(ch, position=0, microsteps=16, enabled=True): return proto.Info(ch=ch, state=0, position=position, velocity=0, microsteps=microsteps, run_ma=800, hold_ma=400, enabled=enabled, comms_ok=True, fault_mask=0) # ── T3R ─────────────────────────────────────────────────────────────────────── def test_t3r_jog_holds_then_stops(qapp): drv = FakeT3R() panel = T3RJogPanel(drv) panel.vel_spin.setValue(1234) panel.accel_spin.setValue(99) btn = panel._jog_btns[(0, -1)] btn.pressed.emit() assert drv.calls == [("jog", 0, -1234, 99)] btn.released.emit() assert drv.calls[-1] == ("stop", 0, False) def test_t3r_release_without_press_sends_nothing(qapp): """A stray release must not stop an axis a scan is driving.""" drv = FakeT3R() panel = T3RJogPanel(drv) panel._jog_btns[(3, 1)].released.emit() assert drv.calls == [] def test_t3r_stop_jogs_covers_a_lost_release(qapp): drv = FakeT3R() panel = T3RJogPanel(drv) panel._jog_btns[(1, 1)].pressed.emit() drv.calls.clear() panel.stop_jogs() assert drv.calls == [("stop", 1, False)] panel.stop_jogs() # already stopped: no repeat command assert drv.calls == [("stop", 1, False)] def test_t3r_microstep_applies_and_survives_a_stale_poll(qapp): drv = FakeT3R() panel = T3RJogPanel(drv) combo = panel._micro_combos[2] combo.setCurrentIndex(combo.findData(64)) combo.activated.emit(combo.currentIndex()) assert drv.calls == [("set_microstep", 2, 64)] # An info frame already in flight still carries the old value. drv.info_updated.emit(2, _info(2, microsteps=16)) assert combo.currentData() == 64 # Once the device confirms, the combo tracks it again. drv.info_updated.emit(2, _info(2, microsteps=64)) assert combo.currentData() == 64 drv.info_updated.emit(2, _info(2, microsteps=8)) assert combo.currentData() == 8 def test_t3r_enable_checkbox_follows_the_device(qapp): drv = FakeT3R() panel = T3RJogPanel(drv) panel._enable_chks[0].setChecked(True) assert drv.calls == [("enable", 0)] # A device-side state change updates the box without echoing a command. drv.info_updated.emit(0, _info(0, position=-42, enabled=False)) assert not panel._enable_chks[0].isChecked() assert drv.calls == [("enable", 0)] assert panel._pos_lbls[0].text() == "-42" def test_t3r_panel_tracks_connection(qapp): drv = FakeT3R(is_open=False) panel = T3RJogPanel(drv) assert not panel.isEnabled() drv.handshake_ok.emit(1, 1, 4) assert panel.isEnabled() drv.info_updated.emit(0, _info(0, position=7)) drv.disconnected.emit("cable") assert not panel.isEnabled() assert panel._pos_lbls[0].text() == "—" # ── BBD202 ──────────────────────────────────────────────────────────────────── def test_bbd_jog_repeats_while_held(qapp): worker = FakeBBD() panel = BBDJogPanel(worker) panel.step_spin.setValue(0.25) panel.x_pos_btn.pressed.emit() assert worker.calls == [("jog", "x", 1, 0.25)] assert panel._repeat.isActive() panel._jog_tick() # what the repeat timer fires assert worker.calls[-1] == ("jog", "x", 1, 0.25) panel.x_pos_btn.released.emit() assert not panel._repeat.isActive() panel._jog_tick() # a late tick moves nothing assert len(worker.calls) == 2 def test_bbd_jog_directions(qapp): worker = FakeBBD() panel = BBDJogPanel(worker) for btn, expected in ((panel.x_neg_btn, ("jog", "x", -1, 0.5)), (panel.y_pos_btn, ("jog", "y", 1, 0.5)), (panel.y_neg_btn, ("jog", "y", -1, 0.5))): btn.pressed.emit() btn.released.emit() assert worker.calls[-1] == expected def test_bbd_velocity_is_debounced_then_applied(qapp): worker = FakeBBD() panel = BBDJogPanel(worker) panel.vel_spin.setValue(4.0) panel.accel_spin.setValue(20.0) assert worker.calls == [] # nothing sent mid-adjustment assert panel._vel_debounce.isActive() panel.apply_velocity() assert worker.calls == [("velocity", 4.0, 20.0)] def test_bbd_panel_tracks_connection(qapp): worker = FakeBBD(is_connected=False) panel = BBDJogPanel(worker) assert not panel.isEnabled() worker.position_updated.emit(12.0, 34.5) assert panel.x_pos_lbl.text() == "012.000" assert panel.y_pos_lbl.text() == "034.500" worker.is_connected = True worker.connected.emit() assert panel.isEnabled() assert worker.calls == [("velocity", panel.vel_spin.value(), panel.accel_spin.value())] worker.disconnected.emit() assert not panel.isEnabled() assert panel.x_pos_lbl.text() == "---.---"