217 lines
6.9 KiB
Python
Executable File
217 lines
6.9 KiB
Python
Executable File
"""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() == "---.---"
|