fix(omny): simulate oshield's digital open-switch from its live position
CI for csaxs_bec / test (push) Successful in 1m57s
CI for csaxs_bec / test (push) Successful in 1m57s
_oshield_is_ST_open() reads a digital switch (@IN[8014]) on otransx's Galil controller to confirm the sample-stage thermal shield reached its physical "open" limit -- but oshield is a Smaract stage, simulated by a completely separate simulator (sim_smaract.py), so that switch had no connection to oshield's actual simulated position and always read a stale default. sim_galil.py's @IN[8014] handler now computes the switch value live from oshield's real position via SimStateRegistry (cross-simulator lookup), instead of a disconnected default -- 1.0 within tolerance of the "open" target (-11.88, _oshield_ST_open()'s own umv() target), 0.0 otherwise. Also fixes oshield's simulated starting position: it started at 0 (neither the "open" -11.88 nor "closed" 15.5), so _otransfer_stage_to_ST()'s oshield_current < 15 closed-check always failed fresh. Now starts at 15.5, matching the real idle/closed resting state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Mupip8x7Hean6E3FFvpbj
This commit is contained in:
@@ -709,7 +709,11 @@ oshield:
|
||||
- 15.8
|
||||
port: 3334
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
# Real hardware idles with the sample-stage shield closed; 15.5 is
|
||||
# _oshield_ST_close()'s own umv() target (omny_sample_transfer_mixin.py).
|
||||
# Starting at 0 (neither open -11.88 nor closed 15.5) always failed
|
||||
# _otransfer_stage_to_ST()'s "oshield_current < 15" closed check.
|
||||
sim_initial_position: 15.5
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
|
||||
@@ -46,6 +46,37 @@ _OMNY_ROTATION_AXIS = 2
|
||||
# 150 degC on every axis without an explicit sim_analog_inputs override.
|
||||
_DEFAULT_ANALOG_INPUT_V = -8.0
|
||||
|
||||
# oshield (the sample-stage thermal shield) is a Smaract stage, not a Galil axis --
|
||||
# but its "fully open" limit switch is wired into *this* Galil controller's digital
|
||||
# input @IN[8014] on real hardware, read via otransx's socket since they share the
|
||||
# same controller (see _oshield_is_ST_open() in omny_sample_transfer_mixin.py: "the
|
||||
# switch for detecting status can be reached via galil controller 1, so we use
|
||||
# otransx for access"). oshield's own simulated position lives entirely in
|
||||
# sim_smaract.py's SimSmaractState (a different simulator, keyed by a different
|
||||
# host/port), so without this, @IN[8014] would read a static default forever,
|
||||
# disagreeing with wherever oshield actually simulated-moved to. Computed dynamically
|
||||
# below instead of seeded once, since oshield's position changes during a session.
|
||||
_OSHIELD_OPEN_SWITCH_CHANNEL = 8014
|
||||
_OSHIELD_GALIL_HOST = "mpc3217.psi.ch"
|
||||
_OSHIELD_GALIL_PORT = 8081 # otransx/otransy's controller
|
||||
_OSHIELD_SMARACT_HOST = "mpc3217.psi.ch"
|
||||
_OSHIELD_SMARACT_PORT = 3334
|
||||
_OSHIELD_SMARACT_AXIS = 2 # axis_Id "C"
|
||||
_OSHIELD_OPEN_POSITION_MM = -11.88 # _oshield_ST_open()'s own umv() target
|
||||
_OSHIELD_OPEN_TOLERANCE_MM = 0.1
|
||||
|
||||
|
||||
def _oshield_open_switch_value() -> float:
|
||||
# Imported lazily to avoid a module-level import cycle risk between the two
|
||||
# simulator modules; sim_smaract.py doesn't import sim_galil.py today, but
|
||||
# keeping this import local avoids ever having to care if that changes.
|
||||
from csaxs_bec.devices.sim.sim_smaract import SimSmaractState
|
||||
|
||||
state = SimStateRegistry.get(SimSmaractState, _OSHIELD_SMARACT_HOST, _OSHIELD_SMARACT_PORT)
|
||||
position_mm = state.axis(_OSHIELD_SMARACT_AXIS).position() / 1.0e6 # nm -> mm
|
||||
is_open = abs(position_mm - _OSHIELD_OPEN_POSITION_MM) < _OSHIELD_OPEN_TOLERANCE_MM
|
||||
return 1.0 if is_open else 0.0
|
||||
|
||||
|
||||
class _SimGalilAxis:
|
||||
"""State of a single simulated Galil axis (positions in microsteps)."""
|
||||
@@ -494,6 +525,12 @@ class SimGalilSocket(SimSocketBase):
|
||||
return "0.0000"
|
||||
if arg.startswith("@IN["):
|
||||
channel = int(arg[4:].split("]")[0])
|
||||
if (
|
||||
channel == _OSHIELD_OPEN_SWITCH_CHANNEL
|
||||
and state.host == _OSHIELD_GALIL_HOST
|
||||
and state.port == _OSHIELD_GALIL_PORT
|
||||
):
|
||||
return f"{_oshield_open_switch_value():.4f}"
|
||||
return f"{state.digital_inputs.get(channel, 0.0):.4f}"
|
||||
if arg.startswith("@OUT["):
|
||||
channel = int(arg[5:].split("]")[0])
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import pytest
|
||||
from bec_server.device_server.tests.utils import DMMock
|
||||
|
||||
from csaxs_bec.devices.omny.galil.ogalil_ophyd import OMNYGalilController
|
||||
from csaxs_bec.devices.sim.sim_galil import (
|
||||
_OSHIELD_GALIL_HOST,
|
||||
_OSHIELD_GALIL_PORT,
|
||||
_OSHIELD_OPEN_POSITION_MM,
|
||||
_OSHIELD_SMARACT_AXIS,
|
||||
_OSHIELD_SMARACT_PORT,
|
||||
)
|
||||
from csaxs_bec.devices.sim.sim_omny import SimOMNYGalilMotor
|
||||
from csaxs_bec.devices.sim.sim_smaract import SimSmaractMotor, SimSmaractState
|
||||
from csaxs_bec.devices.sim.sim_socket import SimStateRegistry
|
||||
|
||||
# otransx's own axis letter doesn't matter here -- only its host/port (shared with
|
||||
# oshield's "open" limit switch on real hardware, see sim_galil.py's
|
||||
# _OSHIELD_GALIL_HOST/_OSHIELD_GALIL_PORT).
|
||||
_OTRANSX_AXIS_ID = "D"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_sim_state():
|
||||
OMNYGalilController._reset_controller()
|
||||
SimStateRegistry.reset()
|
||||
yield
|
||||
SimStateRegistry.reset()
|
||||
OMNYGalilController._reset_controller()
|
||||
|
||||
|
||||
def _make_devices(oshield_initial_position):
|
||||
otransx = SimOMNYGalilMotor(
|
||||
_OTRANSX_AXIS_ID,
|
||||
name="otransx",
|
||||
host=_OSHIELD_GALIL_HOST,
|
||||
port=_OSHIELD_GALIL_PORT,
|
||||
sign=1,
|
||||
device_manager=DMMock(),
|
||||
sim_initial_position=0,
|
||||
sim_velocity=10.0,
|
||||
)
|
||||
otransx.wait_for_connection()
|
||||
oshield = SimSmaractMotor(
|
||||
"C",
|
||||
name="oshield",
|
||||
host=_OSHIELD_GALIL_HOST,
|
||||
port=_OSHIELD_SMARACT_PORT,
|
||||
sign=1,
|
||||
device_manager=DMMock(),
|
||||
sim_initial_position=oshield_initial_position,
|
||||
sim_velocity=10.0,
|
||||
)
|
||||
oshield.wait_for_connection()
|
||||
return otransx, oshield
|
||||
|
||||
|
||||
def _switch_value(otransx) -> float:
|
||||
return float(otransx.controller.socket_put_and_receive("MG @IN[8014]"))
|
||||
|
||||
|
||||
def test_switch_reads_closed_when_oshield_is_closed():
|
||||
"""oshield closed (15.5, _oshield_ST_close()'s own target) -> switch not
|
||||
triggered, matching _oshield_is_ST_open()'s real-hardware semantics."""
|
||||
otransx, oshield = _make_devices(15.5)
|
||||
try:
|
||||
assert _switch_value(otransx) == 0.0
|
||||
finally:
|
||||
otransx.destroy()
|
||||
oshield.destroy()
|
||||
|
||||
|
||||
def test_switch_reads_open_when_oshield_is_at_open_position():
|
||||
otransx, oshield = _make_devices(_OSHIELD_OPEN_POSITION_MM)
|
||||
try:
|
||||
assert _switch_value(otransx) == 1.0
|
||||
finally:
|
||||
otransx.destroy()
|
||||
oshield.destroy()
|
||||
|
||||
|
||||
def test_switch_tracks_oshield_position_live_not_just_at_startup():
|
||||
"""The switch must reflect oshield's *current* simulated position, not
|
||||
just whatever it started at -- this is the actual bug being fixed:
|
||||
the digital switch and oshield's position are simulated by two
|
||||
different simulator classes (Galil vs Smaract) that don't otherwise
|
||||
talk to each other."""
|
||||
otransx, oshield = _make_devices(15.5)
|
||||
try:
|
||||
assert _switch_value(otransx) == 0.0
|
||||
state = SimStateRegistry.get(SimSmaractState, _OSHIELD_GALIL_HOST, _OSHIELD_SMARACT_PORT)
|
||||
state.axis(_OSHIELD_SMARACT_AXIS).pos_nm = _OSHIELD_OPEN_POSITION_MM * 1.0e6
|
||||
assert _switch_value(otransx) == 1.0
|
||||
finally:
|
||||
otransx.destroy()
|
||||
oshield.destroy()
|
||||
|
||||
|
||||
def test_switch_reads_closed_at_an_intermediate_position():
|
||||
otransx, oshield = _make_devices(0.0)
|
||||
try:
|
||||
assert _switch_value(otransx) == 0.0
|
||||
finally:
|
||||
otransx.destroy()
|
||||
oshield.destroy()
|
||||
|
||||
|
||||
def test_other_channels_unaffected_by_the_oshield_special_case():
|
||||
"""A different digital-input channel on the same controller must still
|
||||
use the plain seeded/default value, not get swept up in the oshield
|
||||
special-case."""
|
||||
otransx, oshield = _make_devices(15.5)
|
||||
try:
|
||||
# channel 14 (light curtain) has its own module-level default (1.0),
|
||||
# independent of oshield's position
|
||||
assert float(otransx.controller.socket_put_and_receive("MG @IN[14]")) == 1.0
|
||||
finally:
|
||||
otransx.destroy()
|
||||
oshield.destroy()
|
||||
Reference in New Issue
Block a user