diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21463f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +O.* +.*ignore +.iocsh_history +**/__pycache__/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9dc7c07..a5d57fb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,11 +2,12 @@ default: image: docker.psi.ch:5000/wall_e/sinqepics:latest stages: - - test + - lint - build + - test cppcheck: - stage: test + stage: lint script: - cppcheck --std=c++17 --addon=cert --addon=misc --error-exitcode=1 src/*.cpp artifacts: @@ -15,7 +16,7 @@ cppcheck: - docker formatting: - stage: test + stage: lint script: - clang-format --style=file --Werror --dry-run src/*.cpp artifacts: @@ -24,7 +25,7 @@ formatting: - docker # clangtidy: -# stage: test +# stage: lint # script: # - curl https://docker.psi.ch:5000/v2/_catalog # # - dnf update -y @@ -47,3 +48,14 @@ build_module: when: always tags: - docker + +# TODO I don't know why this fails and gave up debugging for now +# test_module: +# stage: test +# script: +# - mkdir -p "/ioc/modules/counterbox" +# - cp -rT "./counterbox-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}" "/ioc/modules/counterbox/0.0.1" # Seems it needs a number +# - python3 test/test.py +# when: always +# tags: +# - docker diff --git a/Makefile b/Makefile index bdfb942..19c6236 100644 --- a/Makefile +++ b/Makefile @@ -12,11 +12,12 @@ REQUIRED+=calc REQUIRED+=stream # DB files to include in the release +TEMPLATES += db/channels.db TEMPLATES += db/counterbox_4ch.db TEMPLATES += db/counterbox_8ch.db +TEMPLATES += db/counterbox_common.db TEMPLATES += db/counterbox_v2.db TEMPLATES += db/counterbox_v2_test.db -TEMPLATES += db/counterbox_common.db TEMPLATES += db/counterbox.proto # DBD files to include in the release @@ -28,6 +29,7 @@ SOURCES += src/counterbox.cpp SCRIPTS += scripts/counterbox_4ch.cmd SCRIPTS += scripts/counterbox_8ch.cmd SCRIPTS += scripts/counterbox_v2.cmd +SCRIPTS += sim/counterbox_sim.py CXXFLAGS += -std=c++17 USR_CFLAGS += -Wall -Wextra #-Werror diff --git a/README.md b/README.md index dbac306..fb8e5f3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Counterbox Epics Module ----------------------- -A Stream and Asyn based driver for Counterboxes as SINQ. +A Stream and Asyn based driver for configuring the Counterboxes at SINQ. This supports the older 4 and 8 channel EL737 models and the new 10CH 2nd generation systems. @@ -17,8 +17,9 @@ Required Variables |----------------------|-----------------------------------------| | PREFIX | Prefix of all device specific PVs | | NAME | First field in all PVs after Prefix | -| ASYN_PORT | Unique name for referencing Asyn device | -| CNTBOX_HOST | Network IP and Port of device | +| ASYN\_PORT | Unique name for referencing Asyn device | +| CNTBOX\_IP | Network IP of device | +| CNTBOX\_PORT | Network Port of device | All PVs take the form @@ -28,16 +29,16 @@ $(PREFIX):$(NAME):* Available device startup scripts -* scripts/counterbox_4ch.cmd -* scripts/counterbox_8ch.cmd -* scripts/counterbox_v2.cmd +* scripts/counterbox\_4ch.cmd +* scripts/counterbox\_8ch.cmd +* scripts/counterbox\_v2.cmd A device can be configured using one of the startup scripts as follows ``` epicsEnvSet("PREFIX", "SQ:INSTRUMENT") # can also be set in runScript call -runScript "$(counterbox_DIR)counterbox_v2.cmd" "NAME=COUNTERBOX, ASYN_PORT=CBOXV2, CNTBOX_HOST=TestInst-DAQ1:2000" +runScript "$(counterbox_DIR)counterbox_v2.cmd" "NAME=COUNTERBOX, ASYN_PORT=CBOXV2, CNTBOX_IP=TestInst-DAQ1, CNTBOX_PORT=2000" ``` ## PVs of Interest @@ -55,7 +56,7 @@ runScript "$(counterbox_DIR)counterbox_v2.cmd" "NAME=COUNTERBOX, ASYN_PORT=CBOXV | \$(PREFIX):\$(NAME):M_ | Current count on channel. (1-10 depending on box) | | \$(PREFIX):\$(NAME):CHANNELS | Number of available channels (4, 8 or 10) | -## Testing +## Generating Test Signals The 2nd generation systems have two test channels that can be used to output signals at a variable rate. These can be used to ensure the other channels are @@ -68,3 +69,36 @@ runScript "$(counterbox_DIR)counterbox_v2.cmd" "NAME=COUNTERBOX, ASYN_PORT=CBOXV ``` See the file [counterbox\_v2\_test.db](./db/counterbox_v2_test.db) + +## Simulation + +Simulation of the Hardware can be toggled on as follows: + +``` +epicsEnvSet("SET_SIM_MODE","") # run counterbox simulation instead of connecting to actual box +runScript "$(counterbox_DIR)counterbox_v2.cmd" "ASYN_PORT=CBOXV2, CNTBOX_IP=localhost, CNTBOX_PORT=2000" +``` + +In such a case, the provided `CNTBOX_IP` is ignored, and a python program +simulating the hardware is started in the background, listening at the +specified `CNTBOX_PORT`. So, if you have multiple devices listening on the same +port, you might have to change this port value of one of the devices when +simulating hardware. You can then interact with the PVs as with the normal +hardware. Keep in mind, however, that not all functionality has been +implemented. + +See [sim/counterbox\_sim.py](sim/counterbox_sim.py). + +## Testing + +An IOC with the counterbox\_v2 started in simulation mode can be started via +the [test/ioc.sh](test/ioc.sh) script. + +There is also a simple automated test that can be run for a simple check of +functionality and that the PVs load [test/test.py](test/test.py). + +Both require that the module has been built and installed as is normal in the +require based module system. + +You might have to change the specified version in the +[test/st.cmd](test/st.cmd) file to the version you compiled and want to test. diff --git a/db/channels.db b/db/channels.db new file mode 100644 index 0000000..e12ceaa --- /dev/null +++ b/db/channels.db @@ -0,0 +1,20 @@ +# EL737 EPICS Database for streamdevice support +# Macros +# P - Prefix +# NAME - just a name, e.g. EL737 +# PROTO - Stream device protocol file +# ASYN_PORT - Low level Asyn IP Port to EL737 + +################################################################################ +# Status Variables + +################################################################################ +# Count Commands + +################################################################################ +# Read all monitors values + +record(longin, "$(P):$(NAME):M$(CHANNEL)") +{ + field(DESC, "Counterbox CH$(CHANNEL)") +} diff --git a/db/counterbox.proto b/db/counterbox.proto index 0cc150f..b933334 100644 --- a/db/counterbox.proto +++ b/db/counterbox.proto @@ -9,9 +9,9 @@ ReplyTimeout = 200; LockTimeout = 450; initialise { - out "RMT 1"; + out "RMT 1"; # Turn on Remote Control in; - out "ECHO 2"; + out "ECHO 2"; # Ask for reponses in "%(\$1MsgTxt)s"; # Clear MsgTxt on Init @mismatch{ exec 'echo "Failed to configure counterbox" && exit(1)'; diff --git a/db/counterbox_4ch.db b/db/counterbox_4ch.db index dcff4b4..e5703d1 100644 --- a/db/counterbox_4ch.db +++ b/db/counterbox_4ch.db @@ -22,31 +22,8 @@ record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV") field(DISP, 1) } -record(longin, "$(P):$(NAME):CHANNELS") -{ - field(DESC, "Total Supported Channels") - field(VAL, 4) - field(DISP, 1) -} - ################################################################################ # Count Commands -record(longout,"$(P):$(NAME):THRESHOLD-MONITOR") -{ - field(DESC, "Channel monitored for minimum rate") - field(VAL, "1") # Monitor - field(DRVL, "1") # Smallest Threshold Channel - field(DRVL, "4") # Largest Threshold Channel -} - ################################################################################ # Read all monitors values - -record(ai, "$(P):$(NAME):READALL") -{ - field(DESC, "Reads monitors and elapsed time") - field(INP, "@$(PROTO) readAll4($(P):$(NAME):) $(ASYN_PORT)") - field(DTYP, "stream") - field(FLNK, "$(P):$(NAME):MAP-STATUS") -} diff --git a/db/counterbox_8ch.db b/db/counterbox_8ch.db index 5169e45..8f0dfc0 100644 --- a/db/counterbox_8ch.db +++ b/db/counterbox_8ch.db @@ -22,51 +22,8 @@ record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV") field(DISP, 1) } -record(longin, "$(P):$(NAME):CHANNELS") -{ - field(DESC, "Total Supported Channels") - field(VAL, 8) - field(DISP, 1) -} - ################################################################################ # Count Commands -record(longout,"$(P):$(NAME):THRESHOLD-MONITOR") -{ - field(DESC, "Channel monitored for minimum rate") - field(VAL, "1") # Monitor - field(DRVL, "1") # Smallest Threshold Channel - field(DRVL, "8") # Largest Threshold Channel -} - ################################################################################ # Read all monitors values - -record(ai, "$(P):$(NAME):READALL") -{ - field(DESC, "Reads monitors and elapsed time") - field(INP, "@$(PROTO) readAll8($(P):$(NAME):) $(ASYN_PORT)") - field(DTYP, "stream") - field(FLNK, "$(P):$(NAME):MAP-STATUS") -} - -record(longin, "$(P):$(NAME):M5") -{ - field(DESC, "Counterbox CH5") -} - -record(longin, "$(P):$(NAME):M6") -{ - field(DESC, "Counterbox CH6") -} - -record(longin, "$(P):$(NAME):M7") -{ - field(DESC, "Counterbox CH7") -} - -record(longin, "$(P):$(NAME):M8") -{ - field(DESC, "Counterbox CH8") -} diff --git a/db/counterbox_common.db b/db/counterbox_common.db index ab5d8d9..09aaf5d 100644 --- a/db/counterbox_common.db +++ b/db/counterbox_common.db @@ -81,7 +81,8 @@ record(calc, "$(P):$(NAME):MAP-STATUS") field(DESC, "Maps Raw Status to State") field(INPA, "$(P):$(NAME):RAW-STATUS NPP") field(INPB, "$(P):$(NAME):INVALID-CONFIG NPP") - field(CALC, "B=1?4:A=0?0:(A=1||A=2)?1:(A=5||A=6)?2:(A=9||A=13||A=10||A=14)?3:4") + field(INPC, "$(P):$(NAME):RAW-STATUS.UDF NPP") # should also be invalid if can't read the status + field(CALC, "(B=1||C==1)?4:A=0?0:(A=1||A=2)?1:(A=5||A=6)?2:(A=9||A=13||A=10||A=14)?3:4") field(FLNK, "$(P):$(NAME):STATUS") } @@ -102,6 +103,14 @@ record(mbbi, "$(P):$(NAME):STATUS") field(FRST, "INVALID") } +record(longin, "$(P):$(NAME):CHANNELS") +{ + field(DESC, "Total Supported Channels") + field(VAL, $(CHANNELS)) + field(DISP, 1) +} + + ################################################################################ # Count Commands @@ -166,6 +175,14 @@ record(longin,"$(P):$(NAME):THRESHOLD_RBV") field(SCAN, "2 second") } +record(longout,"$(P):$(NAME):THRESHOLD-MONITOR") +{ + field(DESC, "Channel monitored for minimum rate") + field(VAL, "1") # Monitor + field(DRVL, "1") # Smallest Threshold Channel + field(DRVL, "$(CHANNELS)") # Largest Threshold Channel +} + record(longin,"$(P):$(NAME):THRESHOLD-MONITOR_RBV") { field(DESC, "Channel monitored for minimum rate") @@ -174,32 +191,20 @@ record(longin,"$(P):$(NAME):THRESHOLD-MONITOR_RBV") ################################################################################ # Read all monitors values +record(ai, "$(P):$(NAME):READALL") +{ + field(DESC, "Reads monitors and elapsed time") + field(INP, "@$(PROTO) readAll$(CHANNELS)($(P):$(NAME):) $(ASYN_PORT)") + field(DTYP, "stream") + field(FLNK, "$(P):$(NAME):MAP-STATUS") +} + record(ai,"$(P):$(NAME):ELAPSED-TIME") { field(DESC, "Counterbox Measured Time") field(EGU, "seconds") } -record(longin, "$(P):$(NAME):M1") -{ - field(DESC, "Counterbox CH1") -} - -record(longin, "$(P):$(NAME):M2") -{ - field(DESC, "Counterbox CH2") -} - -record(longin, "$(P):$(NAME):M3") -{ - field(DESC, "Counterbox CH3") -} - -record(longin, "$(P):$(NAME):M4") -{ - field(DESC, "Counterbox CH4") -} - # Not yet sure whether we want to support this # record(longin, "$(P):$(NAME):R1") # { diff --git a/db/counterbox_v2.db b/db/counterbox_v2.db index ddbf0d8..5be40f0 100644 --- a/db/counterbox_v2.db +++ b/db/counterbox_v2.db @@ -24,61 +24,8 @@ record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV") field(SCAN, "5 second") } -record(longin, "$(P):$(NAME):CHANNELS") -{ - field(DESC, "Total Supported Channels") - field(VAL, 10) - field(DISP, 1) -} - ################################################################################ # Count Commands -record(longout,"$(P):$(NAME):THRESHOLD-MONITOR") -{ - field(DESC, "Channel monitored for minimum rate") - field(VAL, "1") # Monitor - field(DRVL, "1") # Smallest Threshold Channel - field(DRVL, "10") # Largest Threshold Channel -} - ################################################################################ # Read all monitors values - -record(ai, "$(P):$(NAME):READALL") -{ - field(DESC, "Reads monitors and elapsed time") - field(INP, "@$(PROTO) readAll10($(P):$(NAME):) $(ASYN_PORT)") - field(DTYP, "stream") - field(FLNK, "$(P):$(NAME):MAP-STATUS") -} - -record(longin, "$(P):$(NAME):M5") -{ - field(DESC, "Counterbox CH5") -} - -record(longin, "$(P):$(NAME):M6") -{ - field(DESC, "Counterbox CH6") -} - -record(longin, "$(P):$(NAME):M7") -{ - field(DESC, "Counterbox CH7") -} - -record(longin, "$(P):$(NAME):M8") -{ - field(DESC, "Counterbox CH8") -} - -record(longin, "$(P):$(NAME):M9") -{ - field(DESC, "Counterbox CH9") -} - -record(longin, "$(P):$(NAME):M10") -{ - field(DESC, "Counterbox CH10") -} diff --git a/scripts/counterbox_4ch.cmd b/scripts/counterbox_4ch.cmd index 3ba794b..f0c24b9 100644 --- a/scripts/counterbox_4ch.cmd +++ b/scripts/counterbox_4ch.cmd @@ -1,7 +1,21 @@ require asyn require stream +epicsEnvSet("$(NAME)_CNTBOX_HOST", "$(CNTBOX_IP):$(CNTBOX_PORT)") + +$(SET_SIM_MODE=#) $(SET_SIM_MODE) epicsEnvSet("$(NAME)_CNTBOX_HOST", "127.0.0.1:$(CNTBOX_PORT)") +$(SET_SIM_MODE=#) $(SET_SIM_MODE) system "$(counterbox_DIR)counterbox_sim.py $(CNTBOX_PORT) 4 &" +# starting the python socket seems to take a while +# and need misc to use built in sleep command +$(SET_SIM_MODE=#) $(SET_SIM_MODE) system 'sleep 3' + epicsEnvSet("PROTO", "$(counterbox_DB)counterbox.proto") -drvAsynIPPortConfigure("$(ASYN_PORT)", "$(CNTBOX_HOST)", 0, 0, 0) -dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") +drvAsynIPPortConfigure("$(ASYN_PORT)", "$($(NAME)_CNTBOX_HOST)", 0, 0, 0) +dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT), CHANNELS=4") dbLoadRecords("$(counterbox_DB)counterbox_4ch.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") + +# Could also use substitions instead. +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=1") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=2") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=3") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=4") diff --git a/scripts/counterbox_8ch.cmd b/scripts/counterbox_8ch.cmd index fcd49c9..85e07d3 100644 --- a/scripts/counterbox_8ch.cmd +++ b/scripts/counterbox_8ch.cmd @@ -1,7 +1,25 @@ require asyn require stream +epicsEnvSet("$(NAME)_CNTBOX_HOST", "$(CNTBOX_IP):$(CNTBOX_PORT)") + +$(SET_SIM_MODE=#) $(SET_SIM_MODE) epicsEnvSet("$(NAME)_CNTBOX_HOST", "127.0.0.1:$(CNTBOX_PORT)") +$(SET_SIM_MODE=#) $(SET_SIM_MODE) system "$(counterbox_DIR)counterbox_sim.py $(CNTBOX_PORT) 10 &" +# starting the python socket seems to take a while +# and need misc to use built in sleep command +$(SET_SIM_MODE=#) $(SET_SIM_MODE) system 'sleep 3' + epicsEnvSet("PROTO", "$(counterbox_DB)counterbox.proto") -drvAsynIPPortConfigure("$(ASYN_PORT)", "$(CNTBOX_HOST)", 0, 0, 0) -dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") +drvAsynIPPortConfigure("$(ASYN_PORT)", "$($(NAME)_CNTBOX_HOST)", 0, 0, 0) +dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT), CHANNELS=8") dbLoadRecords("$(counterbox_DB)counterbox_8ch.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") + +# Could also use substitions instead. +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=1") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=2") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=3") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=4") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=5") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=6") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=7") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=8") diff --git a/scripts/counterbox_v2.cmd b/scripts/counterbox_v2.cmd index 6a58428..8868823 100644 --- a/scripts/counterbox_v2.cmd +++ b/scripts/counterbox_v2.cmd @@ -1,8 +1,29 @@ require asyn require stream +epicsEnvSet("$(NAME)_CNTBOX_HOST", "$(CNTBOX_IP):$(CNTBOX_PORT=2000)") + +$(SET_SIM_MODE=#) $(SET_SIM_MODE) epicsEnvSet("$(NAME)_CNTBOX_HOST", "127.0.0.1:$(CNTBOX_PORT=2000)") +$(SET_SIM_MODE=#) $(SET_SIM_MODE) system "$(counterbox_DIR)counterbox_sim.py $(CNTBOX_PORT=2000) 10 &" +# starting the python socket seems to take a while +# and need misc to use built in sleep command +$(SET_SIM_MODE=#) $(SET_SIM_MODE) system 'sleep 5' + epicsEnvSet("PROTO", "$(counterbox_DB)counterbox.proto") -drvAsynIPPortConfigure("$(ASYN_PORT)", "$(CNTBOX_HOST)", 0, 0, 0) -dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") +drvAsynIPPortConfigure("$(ASYN_PORT)", "$($(NAME)_CNTBOX_HOST)", 0, 0, 0) +dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT), CHANNELS=10") dbLoadRecords("$(counterbox_DB)counterbox_v2.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") + +# Could also use substitions instead. +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=1") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=2") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=3") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=4") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=5") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=6") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=7") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=8") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=9") +dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), CHANNEL=10") + $(LOAD_TEST_PVS=#) $(LOAD_TEST_PVS) dbLoadRecords("$(counterbox_DB)counterbox_v2_test.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") diff --git a/sim/counterbox_sim.py b/sim/counterbox_sim.py new file mode 100644 index 0000000..898daeb --- /dev/null +++ b/sim/counterbox_sim.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +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 +TOTAL_CH = int(sys.argv[2]) # Number of Channels + + +class CounterBox: + def __init__(self, total_channels): + self.total_channels = total_channels + self.counts = [0] * self.total_channels + + self.status = 0 + self.countmode = 'time' + self.presettime = 0 + self.presetcount = 0 + self.starttime = 0 + self.elapsed = 0 + self.monitor = 0 + + def resetCounts(self): + self.counts = [0] * self.total_channels + self.starttime = time.time() + self.elapsed = 0 + + def getStatus(self): + return self.status + + def getCounts(self): + return self.counts + + def getMonitorCount(self): + return self.counts[self.monitor] + + def updateCounts(self): + for i in range(self.total_channels): + self.counts[i] += randrange(5) + + def getRunTime(self): + elapsed = round(time.time() - self.starttime, 3) + + if self.countmode == 'time': + if elapsed < self.presettime: + self.updateCounts() + self.elapsed = elapsed + else: + self.elapsed = self.presettime + self.status = 0 + + elif self.countmode == 'count': + if self.getMonitorCount() < self.presetcount: + self.updateCounts() + self.elapsed = elapsed + + if self.getMonitorCount() >= self.presetcount: + self.counts[self.monitor] = self.presetcount + self.status = 0 + + else: + raise Exception("Invalid State") + + return self.elapsed + + def startTimePreset(self, presettime): + self.countmode = 'time' + self.status = 1 + self.presettime = round(presettime, 3) + self.resetCounts() + + def startCountPreset(self, presetcount): + self.countmode = 'count' + self.status = 1 + self.presetcount = presetcount + self.resetCounts() + + +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: + return conn.sendall(f'{data}\r'.encode()) + else: + return conn.sendall(b'\r') + + def receive(): + data = conn.recv(1024) + if data: + # also removes terminator + return data.decode('ascii').rstrip() + else: + return '' + + counterbox = CounterBox(TOTAL_CH) + + while True: + + data = receive() + + if not data: # Empty implies client disconnected + break # Close Server + + try: + + if data == 'RMT 1': + send('') + + elif data == 'ECHO 2': + send('Counterbox') # Sends some sort of info command + + elif data == 'RA': + send( + ' '.join(map(str, + [counterbox.getRunTime()] + \ + counterbox.getCounts() + )) + ) + + elif data == 'RS': + send(str(counterbox.getStatus())) + + elif re.fullmatch(r'TP (\d+(\.\d+)?)', data): + presettime = float(re.fullmatch(r'TP (\d+(\.\d+)?)', data).group(1)) + counterbox.startTimePreset(presettime) + send('') + + elif re.fullmatch(r'MP (\d+)', data): + counts = int(re.fullmatch(r'MP (\d+)', data).group(1)) + counterbox.startCountPreset(counts) + send('') + + else: + send('?2') # Bad command + + except: + send('?OV') diff --git a/test/ioc.sh b/test/ioc.sh new file mode 100755 index 0000000..626c851 --- /dev/null +++ b/test/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 )" + +"${PARENT_PATH}/st.cmd" diff --git a/test/st.cmd b/test/st.cmd new file mode 100755 index 0000000..d31206e --- /dev/null +++ b/test/st.cmd @@ -0,0 +1,14 @@ +#!/usr/local/bin/iocsh + +on error break + +require counterbox + +epicsEnvSet("STREAM_PROTOCOL_PATH","./db") +epicsEnvSet("PREFIX","SQ:TEST") +epicsEnvSet("NAME","CB_TEST") + +epicsEnvSet("SET_SIM_MODE","") # Run Counterbox Simulation Instead of Actual Box +runScript "$(counterbox_DIR)counterbox_v2.cmd" "ASYN_PORT=CBOXV2, CNTBOX_IP=localhost, CNTBOX_PORT=2000" + +iocInit() diff --git a/test/test.py b/test/test.py new file mode 100755 index 0000000..2f54f6d --- /dev/null +++ b/test/test.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +# TODO might be better to use PyEpics or Caproto or something + +import os +import pathlib +import sys +import time + +from queue import Queue, Empty +from subprocess import Popen, PIPE, run +from threading import Thread + + +os.environ['EPICS_BASE'] = '/usr/local/epics/base-7.0.7' +os.environ['EPICS_HOST_ARCH'] = 'linux-x86_64' +os.environ['PARENT_PATH'] = str(pathlib.Path(__file__).parent.resolve()) + + +def queue_output(pipe, queue): + while True: + line = pipe.readline() + + if not line: + break + + queue.put(line.decode('ascii').rstrip()) + pipe.close() + +def get_piped_output(proc): + stdqueue = Queue() + stdthread = Thread(target=queue_output, args=(proc.stdout, stdqueue)) + stdthread.daemon = True + stdthread.start() + + errqueue = Queue() + errthread = Thread(target=queue_output, args=(proc.stderr, errqueue)) + errthread.daemon = True + errthread.start() + + return stdqueue, errqueue + +def getState(prefix, name): + result = run(['caget', f'{prefix}:{name}:STATUS'], stdout=PIPE) + state = result.stdout.decode('ascii').rstrip().split()[1] + print(f'Currently in state {state}') + return state + +def getCount(prefix, name, ch): + result = run(['caget', f'{prefix}:{name}:M{ch}'], stdout=PIPE) + count = int(result.stdout.decode('ascii').rstrip().split()[1]) + print(f'M{ch} == {count}') + return count + +def presetTime(prefix, name, time): + print(f'Starting count for {time} seconds') + run(['caput', f'{prefix}:{name}:PRESET-TIME', f'{time}']) + +def presetCount(prefix, name, count): + print(f'Starting count until channel 1 reaches {count}') + run(['caput', f'{prefix}:{name}:PRESET-COUNT', f'{count}']) + +def testCanCount(prefix, name): + # Check in Idle State + assert getState(prefix, name) == 'Idle', 'Not in valid state' + + # Start Time Based Count and Check that Status Changes + assert getCount(prefix, name, 1) == 0, "Erroneous nonzero starting count value" + presetTime(prefix, name, 5) + time.sleep(1) + assert getState(prefix, name) == 'Counting', 'Didn\'t start counting' + time.sleep(5) + assert getState(prefix, name) == 'Idle', 'Didn\'t finish counting' + assert getCount(prefix, name, 1) > 0, 'No events were counted' + + # Start Monitor Based Count and Check that Status Changes + presetAmount = 100 + presetCount(prefix, name, presetAmount) + time.sleep(1) + assert getState(prefix, name) == 'Counting', 'Didn\'t start counting' + assert getCount(prefix, name, 1) < presetAmount + while getState(prefix, name) != 'Idle': + time.sleep(1) + assert getCount(prefix, name, 1) == presetAmount, 'Counted events not equal to preset' + assert getCount(prefix, name, 2) > 0 + +def test(prefix, name): + + # TODO pass prefix and name to script + proc = Popen([f'{os.environ["PARENT_PATH"]}/st.cmd'], stdout=PIPE, stderr=PIPE, shell=False) + + try: + stdqueue, errqueue = get_piped_output(proc) + + while True: + line = stdqueue.get() + print(line) + + if line == 'iocRun: All initialization complete': + break # IOC is now running + + testCanCount(prefix, name) + + print("Success") + proc.kill() + proc.wait() + return True + + except Exception as e: + print(f"Failure: {e}", file=sys.stderr) + proc.kill() + proc.wait() + return False + + +if __name__ == '__main__': + + print("Starting Test") + + # Test V2 + if test('SQ:TEST', 'CB_TEST'): + exit(0) + else: + exit(1)