interop: extend host_vs_cpp_server.py for the AA tranche messages
Adds round-trip checks for the SECS-II messages added in the AA
catalog-growth commit but never cross-validated against secsgem-py:
* S2F21/F22 — legacy remote command (no params). secsgem-py's
stock S2F21 sends with W=0; we register a W=1 override so the
transaction awaits our S2F22 reply. Also widens CMDA's allowed
types to include Binary (secsgem-py 0.3.0 declares CMDA as
Dynamic[U1, I1] only; SEMI E5 §10.18 says Binary, and our server
emits it that way).
* S6F15/F16 — event-report request by CEID.
* S6F19/F20 — individual report request by RPTID.
* S6F21/F22 — annotated individual report request.
* S7F1/F2 — PP load inquire.
* S7F17/F18 — PP delete.
Suite is now 32 named host-vs-server checks — all green in three
consecutive runs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,12 +18,40 @@ import secsgem.common
|
||||
import secsgem.gem
|
||||
import secsgem.hsms
|
||||
import secsgem.secs
|
||||
from secsgem.secs.functions.base import SecsStreamFunction
|
||||
from secsgem.secs.data_items.base import DataItemBase
|
||||
import secsgem.secs.data_items as _di
|
||||
import secsgem.secs.variables as _v
|
||||
|
||||
|
||||
LOG = logging.getLogger("interop")
|
||||
F = secsgem.secs.functions
|
||||
|
||||
|
||||
# secsgem-py 0.3.0 declares CMDA as Dynamic[U1, I1] only, but SEMI E5
|
||||
# spells it as Binary in §10.18 and our C++ server emits it that way.
|
||||
# Widen the allowed type set so S2F22 decode succeeds.
|
||||
class _CMDA(DataItemBase):
|
||||
__type__ = _v.Dynamic
|
||||
__allowedtypes__ = [_v.Binary, _v.U1, _v.I1]
|
||||
_di.CMDA = _CMDA
|
||||
|
||||
|
||||
# secsgem-py's stock S2F21 ships with `_is_reply_required = False` and
|
||||
# the wire goes out with W=0; our server replies anyway but secsgem-py
|
||||
# never correlates the response so `send_and_waitfor_response` times
|
||||
# out. Override with the spec-correct W=1.
|
||||
class S02F21W(SecsStreamFunction):
|
||||
_stream = 2
|
||||
_function = 21
|
||||
_to_host = False
|
||||
_to_equipment = True
|
||||
_has_reply = True
|
||||
_is_reply_required = True
|
||||
_is_multi_block = False
|
||||
_data_format = "< RCMD >"
|
||||
|
||||
|
||||
def run(host: str, port: int, session_id: int) -> int:
|
||||
settings = secsgem.hsms.HsmsSettings(
|
||||
address=host,
|
||||
@@ -81,6 +109,9 @@ def run(host: str, port: int, session_id: int) -> int:
|
||||
# Register the primary-message handlers up-front.
|
||||
client.register_stream_function(6, 11, on_s6f11)
|
||||
client.register_stream_function(5, 1, on_s5f1)
|
||||
# Override secsgem-py's W=0 S2F21 so its host transactions wait for
|
||||
# the server's S2F22 reply.
|
||||
client.settings.streams_functions.update(S02F21W)
|
||||
|
||||
def check(label: str, ok: bool, detail: str = "") -> None:
|
||||
status = "OK " if ok else "FAIL"
|
||||
@@ -209,7 +240,47 @@ def run(host: str, port: int, session_id: int) -> int:
|
||||
else:
|
||||
check("S5F1 received after RCMD=FAULT", False, "timeout")
|
||||
|
||||
# ---- S2F21 legacy remote command (no params) ----
|
||||
# Use the W=1 override (secsgem-py's stock S2F21 has W=0 and
|
||||
# never awaits our reply); routes through the same
|
||||
# HostCommandRegistry as S2F41.
|
||||
round_trip("S2F21 → S2F22 (legacy RCMD=START)",
|
||||
S02F21W("START"),
|
||||
lambda b: b == 0,
|
||||
lambda b: f"CMDA={b!r}")
|
||||
|
||||
# ---- S6 host-initiated report queries -------------------------
|
||||
# S6F15 → S6F16: pull current event-report payload by CEID.
|
||||
# CEID 300 was linked to RPTID 1 (SVID 2 = Clock) above.
|
||||
round_trip(
|
||||
"S6F15 → S6F16 (event report request CEID=300)",
|
||||
F.SecsS06F15(300),
|
||||
lambda b: isinstance(b, dict) and b.get("CEID") == 300,
|
||||
lambda b: f"CEID={b.get('CEID') if isinstance(b, dict) else b!r}",
|
||||
)
|
||||
# S6F19 → S6F20: pull RPTID 1 directly (just values, no annotation).
|
||||
round_trip(
|
||||
"S6F19 → S6F20 (individual report request RPTID=1)",
|
||||
F.SecsS06F19(1),
|
||||
lambda b: isinstance(b, list) and len(b) >= 1,
|
||||
lambda b: f"{len(b) if isinstance(b, list) else 0} values",
|
||||
)
|
||||
# S6F21 → S6F22: same but with (VID, value) annotation.
|
||||
round_trip(
|
||||
"S6F21 → S6F22 (annotated individual report RPTID=1)",
|
||||
F.SecsS06F21(1),
|
||||
lambda b: isinstance(b, list) and len(b) >= 1,
|
||||
lambda b: f"{len(b) if isinstance(b, list) else 0} annotated values",
|
||||
)
|
||||
|
||||
# ---- S7 recipes ----
|
||||
# S7F1/F2: ask permission before sending a PP of LENGTH bytes.
|
||||
round_trip(
|
||||
"S7F1 → S7F2 (PP load inquire)",
|
||||
F.SecsS07F01({"PPID": "RECIPE-Y", "LENGTH": 64}),
|
||||
lambda b: b in (0, 1, 2, 3, 4, 5, 6),
|
||||
lambda b: f"PPGNT={b!r}",
|
||||
)
|
||||
round_trip("S7F19 → S7F20 (current PPID list)", F.SecsS07F19(),
|
||||
lambda b: isinstance(b, list),
|
||||
lambda b: f"{len(b)} PPIDs")
|
||||
@@ -222,6 +293,13 @@ def run(host: str, port: int, session_id: int) -> int:
|
||||
round_trip("S7F5 → S7F6 (read back RECIPE-X)", F.SecsS07F05("RECIPE-X"),
|
||||
lambda b: isinstance(b, dict) and b.get("PPID") == "RECIPE-X",
|
||||
lambda b: f"got {b!r}")
|
||||
# S7F17/F18: delete the PP we just installed.
|
||||
round_trip(
|
||||
"S7F17 → S7F18 (delete RECIPE-X)",
|
||||
F.SecsS07F17(["RECIPE-X"]),
|
||||
lambda b: b == 0,
|
||||
lambda b: f"ACKC7={b!r}",
|
||||
)
|
||||
|
||||
# ---- S10 terminal display ----
|
||||
round_trip(
|
||||
|
||||
Reference in New Issue
Block a user