autocal layout first design

This commit is contained in:
ykcheungab
2026-09-20 21:15:39 +08:00
parent 07d7c3bad6
commit e276b5de0f
92 changed files with 18071 additions and 64 deletions
+328
View File
@@ -0,0 +1,328 @@
"""Guard and state-machine behaviour of the motion service.
Driven against the simulated platform, so every assertion here is about AutoCal's
own logic: what it refuses, what it reads, and when. No test in this file touches
a controller or a DLL.
"""
from __future__ import annotations
import time
import pytest
from orisys_autocal.motion import (
INIT_FAILED,
INIT_OK,
INIT_RUNNING,
AxisParams,
ConnectionSpec,
DemoTransport,
MotionService,
)
@pytest.fixture
def service():
return MotionService(DemoTransport())
def _initialise(service, timeout: float = 5.0) -> None:
"""Download a BAS program and wait for the simulated bus to report ready."""
service.download_bas(__file__, 1)
deadline = time.monotonic() + timeout
while time.monotonic() < deadline and service.bus.init_status != INIT_OK:
service.poll([0])
time.sleep(0.05)
# ----------------------------------------------------------------- connection
def test_a_fresh_service_is_disconnected_and_touches_nothing(service):
assert not service.connected
assert not service.bas_loaded
assert service.poll([0]).connected is False
def test_connect_reports_success_and_an_unknown_bus_without_a_bas(service):
result = service.connect(ConnectionSpec(mode="demo"))
assert result.ok
assert service.connected
# No BAS program has run, so the bus type must not be guessed at.
snapshot = service.poll([0])
assert snapshot.bus.bus_type == "unknown"
assert not snapshot.bus.initialized
def test_connect_failure_is_reported_without_raising(service):
result = service.connect(ConnectionSpec(mode="eth", ip="127.0.0.1"))
assert not result.ok
assert not service.connected
def test_disconnect_clears_bus_state(service):
service.connect(ConnectionSpec(mode="demo"))
service.disconnect()
assert not service.connected
assert service.bus.bus_type == "unknown"
# ---------------------------------------------------------------- bus init
def test_download_requires_a_real_file(service):
service.connect(ConnectionSpec(mode="demo"))
assert not service.download_bas("", 1).ok
assert not service.download_bas(r"D:\nope\missing.bas", 1).ok
def test_download_needs_a_connection(service):
assert not service.download_bas(__file__, 1).ok
def test_download_then_poll_reaches_initialised(service):
service.connect(ConnectionSpec(mode="demo"))
assert service.download_bas(__file__, 1).ok
_initialise(service)
assert service.bus.init_status == INIT_OK
assert service.bus.bus_type == "ecat"
assert service.bus.total_axes == 5
assert service.bus.node_count == 5
def test_bus_is_running_immediately_after_download(service):
service.connect(ConnectionSpec(mode="demo"))
service.download_bas(__file__, 1)
assert service.bus.init_status == INIT_RUNNING
def test_reinitialise_is_refused_while_already_running(service):
service.connect(ConnectionSpec(mode="demo"))
service.download_bas(__file__, 1)
assert not service.reinit_bus().ok
def test_reinitialise_is_refused_without_a_bas_program(service):
service.connect(ConnectionSpec(mode="demo"))
result = service.reinit_bus()
assert not result.ok
assert "BAS" in result.message or "Bas" in result.message
def test_init_timeout_is_reported_rather_than_raising():
"""A controller that never publishes its globals must time out, not hang."""
class SilentTransport(DemoTransport):
def user_var(self, name):
from orisys_app_shared.zmotion import ZAuxError
raise ZAuxError(30002, f"{name} is not defined")
service = MotionService(SilentTransport())
service._init_timeout = 0.05 # noqa: SLF001 - shortening the vendor window
service.connect(ConnectionSpec(mode="demo"))
service.download_bas(__file__, 1)
time.sleep(0.1)
snapshot = service.poll([0])
assert snapshot.bus.init_status == INIT_FAILED
assert snapshot.bus.message # carries the ZDevelop hint, not an empty string
# ------------------------------------------------------------------- guards
def test_motion_is_refused_before_the_bus_is_initialised(service):
service.connect(ConnectionSpec(mode="demo"))
for attempt in (
service.jog(0, True),
service.move_abs(0, 100.0),
service.move_rel(0, 10.0),
service.home(0),
):
assert not attempt.ok
assert attempt.message
def test_motion_is_refused_when_not_connected(service):
assert not service.jog(0, True).ok
assert not service.move_abs(0, 1.0).ok
assert not service.stop(0).ok
def test_motion_on_an_unknown_axis_is_refused(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
result = service.jog(99, True)
assert not result.ok
assert "99" in result.message or "0-4" in result.message
@pytest.mark.parametrize("bad", [float("nan"), float("inf"), float("-inf")])
def test_a_non_finite_target_is_refused(service, bad):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
result = service.move_abs(0, bad)
assert not result.ok
assert result.message
def test_home_needs_a_configured_mode(service):
"""0 is not a valid homing mode; inheriting one would be worse than saying so."""
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
result = service.home(0)
assert not result.ok
assert "0" in result.message
def test_home_runs_once_a_mode_is_set(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
service.set_axis_params(0, AxisParams(home_mode=17))
assert service.home(0).ok
def test_home_is_refused_on_rtex(service):
"""The vendor example refuses RTEX homing; so do we."""
class RtexTransport(DemoTransport):
def user_var(self, name):
return 1.0 if name == "BUS_TYPE" else super().user_var(name)
rtex = MotionService(RtexTransport())
rtex.connect(ConnectionSpec(mode="demo"))
_initialise(rtex)
assert rtex.bus.bus_type == "rtex"
result = rtex.home(0)
assert not result.ok
assert result.message
# ------------------------------------------------------------------- motion
def test_jog_and_stop_move_the_state_machine(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
assert service.jog(0, True).ok
state = service.poll([0]).axis(0)
assert state is not None and not state.idle
assert service.stop(0).ok
state = service.poll([0]).axis(0)
assert state is not None and state.idle
def test_absolute_and_relative_moves_change_the_command_position(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
assert service.move_abs(1, 250.0).ok
assert service.poll([1]).axis(1).dpos == pytest.approx(250.0)
assert service.move_rel(1, -50.0).ok
assert service.poll([1]).axis(1).dpos == pytest.approx(200.0)
def test_set_axis_params_writes_and_remembers(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
params = AxisParams(units=5.0, speed=200.0, accel=2000.0)
assert service.set_axis_params(2, params).ok
assert service.params(2) == params
assert service.to_mm(2, 1000.0) == pytest.approx(1.0)
def test_enable_toggles_the_axis(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
assert service.set_axis_enable(3, False).ok
assert service.poll([3]).axis(3).enabled is False
assert service.set_axis_enable(3, True).ok
assert service.poll([3]).axis(3).enabled is True
def test_stop_all_covers_every_axis(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
results = service.stop_all()
assert len(results) == service.axis_count
assert all(result.ok for result in results)
def test_clear_alarm_reaches_the_drive(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
assert service.clear_alarm(0).ok
# ----------------------------------------------------------------- bus data
def test_sdo_round_trips_on_ethercat(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
assert service.sdo_write(0, 0x6040, 0, 3, 6).ok
result, value = service.sdo_read(0, 0x6040, 0, 3)
assert result.ok
assert value == 6
def test_sdo_is_refused_on_rtex(service):
class RtexTransport(DemoTransport):
def user_var(self, name):
return 1.0 if name == "BUS_TYPE" else super().user_var(name)
rtex = MotionService(RtexTransport())
rtex.connect(ConnectionSpec(mode="demo"))
_initialise(rtex)
result, _value = rtex.sdo_read(0, 0x6040, 0, 3)
assert not result.ok
def test_rtex_is_refused_on_ethercat(service):
service.connect(ConnectionSpec(mode="demo"))
_initialise(service)
result, _value = service.rtex_read(0, 1)
assert not result.ok
def test_bus_data_is_refused_before_the_bus_type_is_known(service):
service.connect(ConnectionSpec(mode="demo"))
result, _value = service.sdo_read(0, 0x6040, 0, 3)
assert not result.ok
# ------------------------------------------------------------------- polling
def test_polling_reports_every_requested_axis(service):
service.connect(ConnectionSpec(mode="demo"))
snapshot = service.poll([0, 1, 2, 3, 4])
assert [state.axis for state in snapshot.axes] == [0, 1, 2, 3, 4]
assert all(state.read_ok for state in snapshot.axes)
def test_a_read_failure_is_marked_not_silently_zeroed():
"""A failed read must not look like a position of zero."""
class FailingReads(DemoTransport):
def get_dpos(self, axis):
from orisys_app_shared.zmotion import ZAuxError
raise ZAuxError(30000, "no response")
service = MotionService(FailingReads())
service.connect(ConnectionSpec(mode="demo"))
snapshot = service.poll([0])
state = snapshot.axis(0)
assert state is not None
assert state.read_ok is False
assert state.error
def test_language_switches_the_refusal_text(service):
assert not service.jog(0, True).ok
zh = service.jog(0, True).message
service.set_language("en_US")
en = service.jog(0, True).message
assert zh != en