interop: secsgem-py cross-validation harness + lenient identifier parsing
Adds a Docker-based interop harness that drives the C++ server with
secsgem-py 0.3.0 as the active host and probes a secsgem-py-passive
equipment from a minimal C++ active client. Surfaces and fixes four
interoperability bugs uncovered by cross-testing:
* SEMI E5 identifier formatcodes are a U1|U2|U4|U8 wildcard;
secsgem-py picks the narrowest fitting width while our parsers
only accepted U4. `as_uN_scalar` / `as_iN_scalar` now accept
any unsigned/signed width and range-check the downcast.
* PPBODY (S7F3/F6) is "ASCII | Binary | List" per the spec;
secsgem-py defaults to ASCII. Added BINARY_OR_ASCII codegen
item type with `as_text_or_binary` accessor.
* S1F23/F24 Collection Event Namelist was unimplemented; added
schema + `vids_for(ceid)` accessor on EventReportSubscriptions
plus the dispatch handler.
* S10F1 was registered as a host->equipment handler, but per
SEMI E5 §12 S10F1 is equipment->host; S10F3 is the actual
host->equipment Terminal Display Single. Added an S10F3
handler alongside (we keep S10F1 too for backward compat).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Run secsgem-py as passive equipment so a C++ active host can drive it.
|
||||
|
||||
Listens on 0.0.0.0:<port>, waits to be reached by an active host.
|
||||
secsgem-py's GemEquipmentHandler answers S1F13/F14 (establish
|
||||
communications), S1F17/F18 (online request), S1F1/F2 (are-you-there)
|
||||
out of the box. Status variables, equipment constants, and one
|
||||
collection event are registered so that S1F3/S1F11/S1F23 round-trip
|
||||
yield non-empty bodies.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import secsgem.common
|
||||
import secsgem.gem
|
||||
import secsgem.hsms
|
||||
import secsgem.secs.variables as v
|
||||
|
||||
|
||||
LOG = logging.getLogger("interop.equipment")
|
||||
|
||||
|
||||
class InteropEquipment(secsgem.gem.GemEquipmentHandler):
|
||||
def __init__(self, settings):
|
||||
super().__init__(settings)
|
||||
|
||||
# One status variable so S1F3/F11 yield a non-empty list.
|
||||
sv = secsgem.gem.StatusVariable(1, "ChamberTemp", "C", v.U4)
|
||||
sv.value = 25
|
||||
self.status_variables[1] = sv
|
||||
|
||||
# One equipment constant.
|
||||
self.equipment_constants[10] = secsgem.gem.EquipmentConstant(
|
||||
10, "EnergyCap", 0, 100, 50, "kW", v.U4
|
||||
)
|
||||
|
||||
# One collection event (visible via S1F23).
|
||||
self.collection_events[1001] = secsgem.gem.CollectionEvent(
|
||||
1001, "DemoEvent", []
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--port", type=int, default=5000)
|
||||
ap.add_argument("--session-id", type=int, default=0)
|
||||
ap.add_argument("--log-level", default="INFO")
|
||||
args = ap.parse_args()
|
||||
logging.basicConfig(
|
||||
level=args.log_level,
|
||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||
)
|
||||
if args.log_level.upper() != "DEBUG":
|
||||
logging.getLogger("communication").setLevel(logging.WARNING)
|
||||
logging.getLogger("hsms_connection").setLevel(logging.WARNING)
|
||||
|
||||
settings = secsgem.hsms.HsmsSettings(
|
||||
address="0.0.0.0",
|
||||
port=args.port,
|
||||
session_id=args.session_id,
|
||||
connect_mode=secsgem.hsms.HsmsConnectMode.PASSIVE,
|
||||
device_type=secsgem.common.DeviceType.EQUIPMENT,
|
||||
)
|
||||
|
||||
eq = InteropEquipment(settings)
|
||||
eq.enable()
|
||||
|
||||
LOG.info("passive equipment listening on 0.0.0.0:%d session=%d",
|
||||
args.port, args.session_id)
|
||||
|
||||
stop = threading.Event()
|
||||
def shutdown(_signo, _frame):
|
||||
LOG.info("signal received, stopping")
|
||||
stop.set()
|
||||
signal.signal(signal.SIGINT, shutdown)
|
||||
signal.signal(signal.SIGTERM, shutdown)
|
||||
|
||||
try:
|
||||
stop.wait()
|
||||
finally:
|
||||
try:
|
||||
eq.disable()
|
||||
except Exception as e: # noqa: BLE001
|
||||
LOG.warning("disable raised: %s", e)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user