From 7c28e2589c790ca601a3ad16e0558cb668a51847 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Tue, 9 Jun 2026 01:20:14 +0200 Subject: [PATCH] interop: extend host_vs_cpp_server.py for the AA tranche messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- interop/host_vs_cpp_server.py | 78 +++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/interop/host_vs_cpp_server.py b/interop/host_vs_cpp_server.py index 690d219..7c71340 100644 --- a/interop/host_vs_cpp_server.py +++ b/interop/host_vs_cpp_server.py @@ -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(