From 2f4e279c47e7e4c91e001ae790cf28cbd9e023ec Mon Sep 17 00:00:00 2001 From: Edward Wall Date: Fri, 19 Sep 2025 14:41:41 +0200 Subject: [PATCH] Correct bugs in db and sets up simulation mode --- Makefile | 1 + README.md | 60 ++++++++++++++++++++++++++++++++ db/mdif.db | 10 +++--- scripts/mdif.cmd | 11 +++++- scripts/sim-ioc.sh | 8 +++++ scripts/sim-st.cmd | 13 +++++++ sim/mdif_sim.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 README.md create mode 100755 scripts/sim-ioc.sh create mode 100755 scripts/sim-st.cmd create mode 100755 sim/mdif_sim.py diff --git a/Makefile b/Makefile index 01531a4..7b1a6f3 100644 --- a/Makefile +++ b/Makefile @@ -15,5 +15,6 @@ TEMPLATES += db/mdif.db TEMPLATES += db/mdif.proto SCRIPTS += scripts/mdif.cmd +SCRIPTS += sim/mdif_sim.py # MISCS would be the place to keep the stream device template files diff --git a/README.md b/README.md new file mode 100644 index 0000000..0534a63 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +MDIF Epics Module +----------------- + +An Asyn and Stream Device based driver for the Multi-Detector Interface used at +some instrument within SINQ. + +## How to Use + +Unless a custom database is needed, a device can be configure simply by setting +the required environment variables when calling the MDIF start script. + +Required Variables + +| Environment Variable | Purpose | +|----------------------|-----------------------------------------| +| INSTR | Prefix of all device specific PVs | +| NAME | First field in all PVs after Prefix | +| MDIF\_IP | Network IP of device | +| MDIF\_PORT | Network Port of device | + +All PVs take the form + +``` +$(INSTR)$(NAME):* +``` + +Available device startup scripts + +* scripts/mdif.cmd + +For example + +``` +epicsEnvSet("INSTR", "SQ:INSTRUMENT:") # can also be set in runScript call + +runScript "$(mdif_DIR)mdif.cmd" "NAME=MDIF, MDIF_IP=focus-ts, MDIF_PORT=3016" +``` + +## PVs of Interest + +| PV | Description | +|------------------------------|--------------------------------------------------| +| \$(INSTR)\$(NAME):MsgTxt | Contains unexpected response to executed command | +| \$(INSTR)\$(NAME):DELAY | Used to write a new delay value to the MDIF | +| \$(INSTR)\$(NAME):DELAY\_RBV | Read back the delay value configured in the MDIF | + +## Simulation + +Simulation of the Hardware can be toggled on as follows: + +``` +epicsEnvSet("SET_SIM_MODE","") # run MDIF simulation instead of connecting to actual system +runScript "$(mdif_DIR)mdif.cmd" "NAME=MDIF, MDIF_IP=localhost, MDIF_PORT=3016" +``` + +In such a case, the provided `MDIF_IP` is ignored, and a python program +simulating the hardware is started in the background, listening at the +specified `MDIF_PORT`. + +See [sim/mdif\_sim.py](sim/mdif_sim.py). diff --git a/db/mdif.db b/db/mdif.db index 9976f5c..5f6b29a 100644 --- a/db/mdif.db +++ b/db/mdif.db @@ -1,21 +1,19 @@ record(stringin, "$(INSTR)$(NAME):MsgTxt") { field(DESC, "Unexpected received response") - field(DTYP, "devDAQStringError") - field(FLNK, "$(INSTR)$(NAME):INVALID-CONFIG") } record(longout,"$(INSTR)$(NAME):DELAY") { - field(DESC, "Starting measurement time after trigger signal") + field(DESC, "delay after trigger signal") field(DTYP, "stream") field(OUT, "@$(PROTO) writeDelay($(INSTR)$(NAME):) $(ASYN_PORT)") } -record(longin,"$(INSTR)$(NAME):DELAY") +record(longin,"$(INSTR)$(NAME):DELAY_RBV") { - field(DESC, "Starting measurement time after trigger signal") + field(DESC, "delay after trigger signal") field(DTYP, "stream") - field(OUT, "@$(PROTO) readDelay($(INSTR)$(NAME):) $(ASYN_PORT)") + field(INP, "@$(PROTO) readDelay($(INSTR)$(NAME):) $(ASYN_PORT)") field(SCAN, "1 second") } diff --git a/scripts/mdif.cmd b/scripts/mdif.cmd index 788b7df..236ccb7 100644 --- a/scripts/mdif.cmd +++ b/scripts/mdif.cmd @@ -1,6 +1,15 @@ require asyn require stream +epicsEnvSet("$(NAME)_MDIF_HOST", "$(MDIF_IP):$(MDIF_PORT=2000)") + +$(SET_SIM_MODE=#) $(SET_SIM_MODE) require misc +$(SET_SIM_MODE=#) $(SET_SIM_MODE) epicsEnvSet("$(NAME)_MDIF_HOST", "127.0.0.1:$(MDIF_PORT=3004)") +$(SET_SIM_MODE=#) $(SET_SIM_MODE) system "$(mdif_DIR)mdif_sim.py $(MDIF_PORT=3004) &" +# starting the python socket seems to take a while +# and need misc to use built in sleep command +$(SET_SIM_MODE=#) $(SET_SIM_MODE) sleep 3 + epicsEnvSet("PROTO", "$(mdif_DB)mdif.proto") -drvAsynIPPortConfigure("ASYN_$(NAME)", "$(HOST)", 0, 0, 0) +drvAsynIPPortConfigure("ASYN_$(NAME)", "$($(NAME)_MDIF_HOST)", 0, 0, 0) dbLoadRecords("$(mdif_DB)mdif.db", "INSTR=$(INSTR), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=ASYN_$(NAME)") diff --git a/scripts/sim-ioc.sh b/scripts/sim-ioc.sh new file mode 100755 index 0000000..f8890dc --- /dev/null +++ b/scripts/sim-ioc.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +export EPICS_HOST_ARCH=linux-x86_64 +export EPICS_BASE=/usr/local/epics/base-7.0.7 + +PARENT_PATH="$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )" + +exec /usr/local/bin/procServ -o -L - -f -i ^D^C 20001 "${PARENT_PATH}/sim-st.cmd" diff --git a/scripts/sim-st.cmd b/scripts/sim-st.cmd new file mode 100755 index 0000000..7366087 --- /dev/null +++ b/scripts/sim-st.cmd @@ -0,0 +1,13 @@ +#!/usr/local/bin/iocsh + +on error break + +require mdif, wall_e + +epicsEnvSet("STREAM_PROTOCOL_PATH","./db") +epicsEnvSet("INSTR","SQ:SIM:") + +epicsEnvSet("SET_SIM_MODE","") # Run Simulation Instead of Actual Interface +runScript "$(mdif_DIR)mdif.cmd" "NAME=MDIF, MDIF_IP=127.0.0.1, MDIF_PORT=3004" + +iocInit() diff --git a/sim/mdif_sim.py b/sim/mdif_sim.py new file mode 100755 index 0000000..c052250 --- /dev/null +++ b/sim/mdif_sim.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +import os +import re +import socket +import sys +import time + +from random import randrange + +HOST = "127.0.0.1" # Localhost +PORT = int(sys.argv[1]) # Port to listen on +LOG2FILE = False if len(sys.argv) < 3 else bool(int(sys.argv[2])) + +import logging +logger = logging.getLogger('mdif') + +if LOG2FILE: + logging.basicConfig(filename=os.path.join(os.getcwd(), 'mdif_sim.log'), level=logging.INFO) + + +class MDIF: + def __init__(self): + self.delay = 1000 + + def getDelay(self): + return self.delay + + def setDelay(self, delay): + self.delay = delay + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + + s.bind((HOST, PORT)) + s.listen() + conn, addr = s.accept() + with conn: + + def send(data: str): + if data: + logger.info(f'SENDING: "{data}"') + return conn.sendall(f'{data}\r'.encode()) + else: + logger.info(f'SENDING: ""') + return conn.sendall(b'\r') + + def receive(): + data = conn.recv(1024) + if data: + # also removes terminator + received = data.decode('ascii').rstrip() + else: + received = '' + logger.info(f'RECEIVED: "{received}"') + return received + + mdif = MDIF() + + while True: + + data = receive() + + if not data: # Empty implies client disconnected + break # Close Server + + try: + + if data == 'RMT 1': + send('') + + elif data == 'ECHO 0': + send('') + + elif data == 'DT': + send('%d' % mdif.getDelay()) + + elif re.fullmatch(r'DT (\d+)', data): + new_delay = int(re.fullmatch(r'DT (\d+)', data).group(1)) + mdif.setDelay(new_delay) + send('') + + else: + send('?2') # Bad command + + except Exception as e: + logger.exception('Simulation Broke') + send('?OV')