3 Commits
0.0.5 ... 0.1.0

Author SHA1 Message Date
fba7487f88 No longer have to specify the Asyn Port Name 2024-11-05 15:00:45 +01:00
044a181ed0 Merge branch 'adding-sim' into 'master'
Adding sim and test

See merge request sinq-epics-modules/counterbox!1
2024-11-05 09:29:38 +01:00
897e54901f Adding sim and test 2024-11-05 09:29:38 +01:00
17 changed files with 469 additions and 166 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
O.*
.*ignore
.iocsh_history
**/__pycache__/

View File

@ -2,11 +2,12 @@ default:
image: docker.psi.ch:5000/wall_e/sinqepics:latest image: docker.psi.ch:5000/wall_e/sinqepics:latest
stages: stages:
- test - lint
- build - build
- test
cppcheck: cppcheck:
stage: test stage: lint
script: script:
- cppcheck --std=c++17 --addon=cert --addon=misc --error-exitcode=1 src/*.cpp - cppcheck --std=c++17 --addon=cert --addon=misc --error-exitcode=1 src/*.cpp
artifacts: artifacts:
@ -15,7 +16,7 @@ cppcheck:
- docker - docker
formatting: formatting:
stage: test stage: lint
script: script:
- clang-format --style=file --Werror --dry-run src/*.cpp - clang-format --style=file --Werror --dry-run src/*.cpp
artifacts: artifacts:
@ -24,7 +25,7 @@ formatting:
- docker - docker
# clangtidy: # clangtidy:
# stage: test # stage: lint
# script: # script:
# - curl https://docker.psi.ch:5000/v2/_catalog # - curl https://docker.psi.ch:5000/v2/_catalog
# # - dnf update -y # # - dnf update -y
@ -47,3 +48,14 @@ build_module:
when: always when: always
tags: tags:
- docker - 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

View File

@ -12,11 +12,12 @@ REQUIRED+=calc
REQUIRED+=stream REQUIRED+=stream
# DB files to include in the release # DB files to include in the release
TEMPLATES += db/channels.db
TEMPLATES += db/counterbox_4ch.db TEMPLATES += db/counterbox_4ch.db
TEMPLATES += db/counterbox_8ch.db TEMPLATES += db/counterbox_8ch.db
TEMPLATES += db/counterbox_common.db
TEMPLATES += db/counterbox_v2.db TEMPLATES += db/counterbox_v2.db
TEMPLATES += db/counterbox_v2_test.db TEMPLATES += db/counterbox_v2_test.db
TEMPLATES += db/counterbox_common.db
TEMPLATES += db/counterbox.proto TEMPLATES += db/counterbox.proto
# DBD files to include in the release # DBD files to include in the release
@ -28,6 +29,7 @@ SOURCES += src/counterbox.cpp
SCRIPTS += scripts/counterbox_4ch.cmd SCRIPTS += scripts/counterbox_4ch.cmd
SCRIPTS += scripts/counterbox_8ch.cmd SCRIPTS += scripts/counterbox_8ch.cmd
SCRIPTS += scripts/counterbox_v2.cmd SCRIPTS += scripts/counterbox_v2.cmd
SCRIPTS += sim/counterbox_sim.py
CXXFLAGS += -std=c++17 CXXFLAGS += -std=c++17
USR_CFLAGS += -Wall -Wextra #-Werror USR_CFLAGS += -Wall -Wextra #-Werror

View File

@ -1,7 +1,7 @@
Counterbox Epics Module 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 This supports the older 4 and 8 channel EL737 models and the new 10CH 2nd
generation systems. generation systems.
@ -17,8 +17,8 @@ Required Variables
|----------------------|-----------------------------------------| |----------------------|-----------------------------------------|
| PREFIX | Prefix of all device specific PVs | | PREFIX | Prefix of all device specific PVs |
| NAME | First field in all PVs after Prefix | | NAME | First field in all PVs after Prefix |
| ASYN_PORT | Unique name for referencing Asyn device | | CNTBOX\_IP | Network IP of device |
| CNTBOX_HOST | Network IP and Port of device | | CNTBOX\_PORT | Network Port of device |
All PVs take the form All PVs take the form
@ -28,16 +28,16 @@ $(PREFIX):$(NAME):*
Available device startup scripts Available device startup scripts
* scripts/counterbox_4ch.cmd * scripts/counterbox\_4ch.cmd
* scripts/counterbox_8ch.cmd * scripts/counterbox\_8ch.cmd
* scripts/counterbox_v2.cmd * scripts/counterbox\_v2.cmd
A device can be configured using one of the startup scripts as follows A device can be configured using one of the startup scripts as follows
``` ```
epicsEnvSet("PREFIX", "SQ:INSTRUMENT") # can also be set in runScript call 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, CNTBOX_IP=TestInst-DAQ1, CNTBOX_PORT=2000"
``` ```
## PVs of Interest ## PVs of Interest
@ -55,7 +55,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):M_ | Current count on channel. (1-10 depending on box) |
| \$(PREFIX):\$(NAME):CHANNELS | Number of available channels (4, 8 or 10) | | \$(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 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 signals at a variable rate. These can be used to ensure the other channels are
@ -64,7 +64,40 @@ runtime via the following
``` ```
epicsEnvSet("LOAD_TEST_PVS","") epicsEnvSet("LOAD_TEST_PVS","")
runScript "$(counterbox_DIR)counterbox_v2.cmd" "NAME=COUNTERBOX, ASYN_PORT=CBOXV2, CNTBOX_HOST=TestInst-DAQ1:2000" runScript "$(counterbox_DIR)counterbox_v2.cmd" "NAME=COUNTERBOX, CNTBOX_IP=TestInst-DAQ1, CNTBOX_PORT=2000"
``` ```
See the file [counterbox\_v2\_test.db](./db/counterbox_v2_test.db) 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" "NAME=CB_TEST, 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.

20
db/channels.db Normal file
View File

@ -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)")
}

View File

@ -9,9 +9,9 @@ ReplyTimeout = 200;
LockTimeout = 450; LockTimeout = 450;
initialise { initialise {
out "RMT 1"; out "RMT 1"; # Turn on Remote Control
in; in;
out "ECHO 2"; out "ECHO 2"; # Ask for reponses
in "%(\$1MsgTxt)s"; # Clear MsgTxt on Init in "%(\$1MsgTxt)s"; # Clear MsgTxt on Init
@mismatch{ @mismatch{
exec 'echo "Failed to configure counterbox" && exit(1)'; exec 'echo "Failed to configure counterbox" && exit(1)';

View File

@ -22,31 +22,8 @@ record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV")
field(DISP, 1) field(DISP, 1)
} }
record(longin, "$(P):$(NAME):CHANNELS")
{
field(DESC, "Total Supported Channels")
field(VAL, 4)
field(DISP, 1)
}
################################################################################ ################################################################################
# Count Commands # 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 # 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")
}

View File

@ -22,51 +22,8 @@ record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV")
field(DISP, 1) field(DISP, 1)
} }
record(longin, "$(P):$(NAME):CHANNELS")
{
field(DESC, "Total Supported Channels")
field(VAL, 8)
field(DISP, 1)
}
################################################################################ ################################################################################
# Count Commands # 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 # 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")
}

View File

@ -81,7 +81,8 @@ record(calc, "$(P):$(NAME):MAP-STATUS")
field(DESC, "Maps Raw Status to State") field(DESC, "Maps Raw Status to State")
field(INPA, "$(P):$(NAME):RAW-STATUS NPP") field(INPA, "$(P):$(NAME):RAW-STATUS NPP")
field(INPB, "$(P):$(NAME):INVALID-CONFIG 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") field(FLNK, "$(P):$(NAME):STATUS")
} }
@ -102,6 +103,14 @@ record(mbbi, "$(P):$(NAME):STATUS")
field(FRST, "INVALID") field(FRST, "INVALID")
} }
record(longin, "$(P):$(NAME):CHANNELS")
{
field(DESC, "Total Supported Channels")
field(VAL, $(CHANNELS))
field(DISP, 1)
}
################################################################################ ################################################################################
# Count Commands # Count Commands
@ -166,6 +175,14 @@ record(longin,"$(P):$(NAME):THRESHOLD_RBV")
field(SCAN, "2 second") 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") record(longin,"$(P):$(NAME):THRESHOLD-MONITOR_RBV")
{ {
field(DESC, "Channel monitored for minimum rate") field(DESC, "Channel monitored for minimum rate")
@ -174,32 +191,20 @@ record(longin,"$(P):$(NAME):THRESHOLD-MONITOR_RBV")
################################################################################ ################################################################################
# Read all monitors values # 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") record(ai,"$(P):$(NAME):ELAPSED-TIME")
{ {
field(DESC, "Counterbox Measured Time") field(DESC, "Counterbox Measured Time")
field(EGU, "seconds") 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 # Not yet sure whether we want to support this
# record(longin, "$(P):$(NAME):R1") # record(longin, "$(P):$(NAME):R1")
# { # {

View File

@ -24,61 +24,8 @@ record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV")
field(SCAN, "5 second") field(SCAN, "5 second")
} }
record(longin, "$(P):$(NAME):CHANNELS")
{
field(DESC, "Total Supported Channels")
field(VAL, 10)
field(DISP, 1)
}
################################################################################ ################################################################################
# Count Commands # 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 # 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")
}

View File

@ -1,7 +1,21 @@
require asyn require asyn
require stream 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") epicsEnvSet("PROTO", "$(counterbox_DB)counterbox.proto")
drvAsynIPPortConfigure("$(ASYN_PORT)", "$(CNTBOX_HOST)", 0, 0, 0) drvAsynIPPortConfigure("ASYN_$(NAME)", "$($(NAME)_CNTBOX_HOST)", 0, 0, 0)
dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=ASYN_$(NAME), CHANNELS=4")
dbLoadRecords("$(counterbox_DB)counterbox_4ch.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") dbLoadRecords("$(counterbox_DB)counterbox_4ch.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=ASYN_$(NAME)")
# 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")

View File

@ -1,7 +1,25 @@
require asyn require asyn
require stream 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") epicsEnvSet("PROTO", "$(counterbox_DB)counterbox.proto")
drvAsynIPPortConfigure("$(ASYN_PORT)", "$(CNTBOX_HOST)", 0, 0, 0) drvAsynIPPortConfigure("ASYN_$(NAME)", "$($(NAME)_CNTBOX_HOST)", 0, 0, 0)
dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=ASYN_$(NAME), CHANNELS=8")
dbLoadRecords("$(counterbox_DB)counterbox_8ch.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") dbLoadRecords("$(counterbox_DB)counterbox_8ch.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=ASYN_$(NAME)")
# 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")

View File

@ -1,8 +1,29 @@
require asyn require asyn
require stream 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") epicsEnvSet("PROTO", "$(counterbox_DB)counterbox.proto")
drvAsynIPPortConfigure("$(ASYN_PORT)", "$(CNTBOX_HOST)", 0, 0, 0) drvAsynIPPortConfigure("ASYN_$(NAME)", "$($(NAME)_CNTBOX_HOST)", 0, 0, 0)
dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") dbLoadRecords("$(counterbox_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=ASYN_$(NAME), CHANNELS=10")
dbLoadRecords("$(counterbox_DB)counterbox_v2.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)") dbLoadRecords("$(counterbox_DB)counterbox_v2.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=ASYN_$(NAME)")
$(LOAD_TEST_PVS=#) $(LOAD_TEST_PVS) dbLoadRecords("$(counterbox_DB)counterbox_v2_test.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_$(NAME)")

147
sim/counterbox_sim.py Normal file
View File

@ -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')

8
test/ioc.sh Executable file
View File

@ -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"

14
test/st.cmd Executable file
View File

@ -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" "CNTBOX_IP=localhost, CNTBOX_PORT=2000"
iocInit()

124
test/test.py Executable file
View File

@ -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 'iocRun: All initialization complete' in line:
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)