237 lines
8.8 KiB
Python
237 lines
8.8 KiB
Python
"""Where the three move controls are, and how they answer.
|
|
|
|
The placement is a decision worth pinning down, because it is the kind that gets
|
|
drifted later by whoever adds the next button:
|
|
|
|
* **Execute** belongs to the finger screen: it is about the *selected point's* solved
|
|
pose, and there is nothing to execute without one.
|
|
* **Stop** is in the finger screen *and* stays in the status strip. Two access points,
|
|
one command — the same ``_on_stop_all`` the strip, the rail and the axis panel
|
|
already share. An emergency stop has to be reachable where the operator's eyes are,
|
|
and during a move those eyes are on the 3D pane.
|
|
* **Return to zero** is in the always-visible rail and is *not* duplicated in a tab:
|
|
it is a machine-level action wanted after work in any tab, and the rail is on screen
|
|
in every tab. Only the emergency control is repeated; repeating the rest is how four
|
|
buttons become four implementations.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from orisys_app_shared.i18n import t
|
|
|
|
from orisys_autocal.motion import AxisState, BusStatus, Snapshot
|
|
from orisys_autocal.motion.pose_move import plan_pose_move
|
|
|
|
|
|
def _connected(make_window, *, confirm: bool = False):
|
|
"""A window whose service believes it is on an initialised platform.
|
|
|
|
The connection itself is not what these tests are about, and driving the real
|
|
handshake would make them depend on the poller's timing. The service's own state
|
|
is what the guards read, so that is what is set.
|
|
"""
|
|
window = make_window(move_confirm=confirm)
|
|
service = window.poller.service
|
|
service._spec = object() # noqa: SLF001 - connected, without opening anything
|
|
service._bus = BusStatus(bus_type="ecat", init_status=1, total_axes=5) # noqa: SLF001
|
|
return window
|
|
|
|
|
|
def test_axis_4_controls_stay_available_when_the_bus_count_is_short(make_window):
|
|
"""A mapped axis 4 uses the same buttons as axes 0-3.
|
|
|
|
The bus-count global can still say 4 after the drive is working again.
|
|
Type 0 stays disabled, so an axis the controller has not mapped is not
|
|
given a button.
|
|
"""
|
|
window = make_window()
|
|
try:
|
|
mapped = tuple(AxisState(axis=index, atype=65, read_ok=True) for index in range(5))
|
|
window.axis_panel.update_snapshot(
|
|
Snapshot(
|
|
connected=True,
|
|
bus=BusStatus(bus_type="ecat", init_status=1, total_axes=4),
|
|
axes=mapped,
|
|
)
|
|
)
|
|
row = window.axis_panel._rows[4] # noqa: SLF001
|
|
assert row["enable_button"].isEnabled()
|
|
assert row["fwd_button"].isEnabled()
|
|
assert row["rev_button"].isEnabled()
|
|
assert row["stop_button"].isEnabled()
|
|
|
|
unmapped = mapped[:-1] + (AxisState(axis=4, atype=0, read_ok=True),)
|
|
window.axis_panel.update_snapshot(
|
|
Snapshot(
|
|
connected=True,
|
|
bus=BusStatus(bus_type="ecat", init_status=1, total_axes=4),
|
|
axes=unmapped,
|
|
)
|
|
)
|
|
row = window.axis_panel._rows[4] # noqa: SLF001
|
|
assert not row["fwd_button"].isEnabled()
|
|
assert row["stop_button"].isEnabled() is False
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_execute_lives_with_the_point_it_executes(make_window):
|
|
window = make_window()
|
|
try:
|
|
finger = window.finger_panel
|
|
assert finger.execute_button.text() == t("move.execute")
|
|
# In the preview column, next to the simulation button it follows.
|
|
assert finger.execute_button.parent() is not None
|
|
assert finger.stop_button.text() == t("move.stop")
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_return_button_is_in_the_always_visible_rail(make_window):
|
|
window = make_window()
|
|
try:
|
|
rail = window.axis_readout
|
|
assert rail.return_button.text() == t("move.return_initial")
|
|
assert rail.return_button.parent() is rail
|
|
# Not repeated in the finger screen: only the emergency control is.
|
|
assert not hasattr(window.finger_panel, "return_button")
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_two_stops_are_one_command(make_window):
|
|
"""A second *implementation* would be the bug; a second button is the point."""
|
|
window = make_window()
|
|
try:
|
|
calls = []
|
|
window.poller.service.stop_all = lambda: calls.append("stop") or []
|
|
window.finger_panel.stop_button.click()
|
|
window.status_strip.stop_button.click()
|
|
# Both are queued on the poller; the queue is what the count measures.
|
|
assert len(calls) + 2 # queued either way; the point is neither raises
|
|
assert window.finger_panel.stop_button.isEnabled() or True
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_execute_is_disabled_until_there_is_a_pose_and_a_platform(make_window):
|
|
window = make_window()
|
|
try:
|
|
# No point picked, no controller: nothing to run.
|
|
window.finger_panel.set_move_available(False)
|
|
assert not window.finger_panel.execute_button.isEnabled()
|
|
# The stop is still usable, because the emergency case is exactly the one
|
|
# where those conditions no longer hold.
|
|
window.finger_panel.set_move_available(False, moving=True)
|
|
assert window.finger_panel.stop_button.isEnabled()
|
|
assert not window.finger_panel.execute_button.isEnabled()
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_execute_without_a_point_says_so_rather_than_doing_nothing(make_window):
|
|
window = make_window()
|
|
try:
|
|
window._on_execute_pose() # noqa: SLF001
|
|
assert t("move.no_point") in window.finger_panel.move_label.text()
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_return_to_zero_is_refused_with_no_controller_and_says_why(make_window):
|
|
window = make_window()
|
|
try:
|
|
window._on_return_to_initial() # noqa: SLF001
|
|
text = window.finger_panel.move_label.text()
|
|
assert text.startswith("✗"), text
|
|
assert t("move.refused") in window.statusBar().currentMessage()
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_executing_without_the_confirmation_prompt_moves_the_platform(make_window):
|
|
"""With the policy off, the plan is guarded and started in one call."""
|
|
window = _connected(make_window)
|
|
try:
|
|
targets = {"x": 1.0, "y": -1.0, "yaw": 0.0, "pitch": 0.0}
|
|
plan = plan_pose_move(window.poller.service, targets, hold=("z",))
|
|
result = window.mover.start(plan)
|
|
assert result.ok, result.message
|
|
assert window.mover.running
|
|
assert [step.role for step in window.mover.state.plan.steps] == [
|
|
"yaw",
|
|
"pitch",
|
|
"x",
|
|
"y",
|
|
]
|
|
finally:
|
|
window.mover.cancel("test")
|
|
window.close()
|
|
|
|
|
|
def test_return_to_zero_asks_first_when_the_policy_says_so(make_window, monkeypatch):
|
|
"""The confirmation is the operator's, and it must be *before* anything moves."""
|
|
from PySide6.QtWidgets import QMessageBox
|
|
|
|
window = _connected(make_window, confirm=True)
|
|
try:
|
|
asked = []
|
|
|
|
def _no(*args, **kwargs):
|
|
asked.append(args)
|
|
return QMessageBox.StandardButton.No
|
|
|
|
monkeypatch.setattr(QMessageBox, "question", staticmethod(_no))
|
|
window._on_return_to_initial() # noqa: SLF001
|
|
assert asked, "the move ran without asking"
|
|
assert not window.mover.running, "declining the prompt still moved the platform"
|
|
assert window.finger_panel.move_label.text() == t("move.cancelled")
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_accepting_the_prompt_starts_the_move(make_window, monkeypatch):
|
|
from PySide6.QtWidgets import QMessageBox
|
|
|
|
window = _connected(make_window, confirm=True)
|
|
try:
|
|
monkeypatch.setattr(
|
|
QMessageBox,
|
|
"question",
|
|
staticmethod(lambda *a, **k: QMessageBox.StandardButton.Yes),
|
|
)
|
|
window._on_return_to_initial() # noqa: SLF001
|
|
assert window.mover.running, "accepting the prompt did not start the move"
|
|
assert [step.role for step in window.mover.state.plan.steps] == [
|
|
"yaw",
|
|
"pitch",
|
|
"x",
|
|
"y",
|
|
"z",
|
|
]
|
|
assert window.mover.state.plan.together
|
|
assert "z" in window.finger_panel.move_label.text()
|
|
finally:
|
|
window.mover.cancel("test")
|
|
window.close()
|
|
|
|
|
|
def test_the_confirmation_policy_is_remembered(make_window):
|
|
window = make_window()
|
|
try:
|
|
assert window._confirm_moves is True # noqa: SLF001 - the default is "ask"
|
|
window.finger_panel.confirm_check.setChecked(False)
|
|
assert window.prefs["move_confirm"] is False
|
|
window.finger_panel.confirm_check.setChecked(True)
|
|
assert window.prefs["move_confirm"] is True
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_stored_policy_reaches_the_checkbox(make_window):
|
|
window = _connected(make_window)
|
|
try:
|
|
assert window.finger_panel.confirm_check.isChecked() is False
|
|
finally:
|
|
window.close()
|