11 Commits
0.0.1 ... 0.1.0

19 changed files with 619 additions and 187 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
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

View File

@ -7,17 +7,18 @@ EPICS_VERSIONS=7.0.7
ARCH_FILTER=RHEL%
# additional module dependencies
REQUIRED+=stream
REQUIRED+=asyn
REQUIRED+=calc
# Release version
LIBVERSION=ed-dev
REQUIRED+=stream
# DB files to include in the release
TEMPLATES += db/counterbox.db
TEMPLATES += db/counterbox.proto
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.proto
# DBD files to include in the release
DBDS += src/counterbox.dbd
@ -25,8 +26,10 @@ DBDS += src/counterbox.dbd
# Source files to build
SOURCES += src/counterbox.cpp
SCRIPTS += scripts/counterbox.cmd
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

103
README.md Normal file
View File

@ -0,0 +1,103 @@
Counterbox Epics Module
-----------------------
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.
## How to Use
Unless a custom database is needed, a device can be configure simply by setting
the required environment variables when calling the correct counterbox script.
Required Variables
| Environment Variable | Purpose |
|----------------------|-----------------------------------------|
| PREFIX | Prefix of all device specific PVs |
| NAME | First field in all PVs after Prefix |
| CNTBOX\_IP | Network IP of device |
| CNTBOX\_PORT | Network Port of device |
All PVs take the form
```
$(PREFIX):$(NAME):*
```
Available device startup scripts
* 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, CNTBOX_IP=TestInst-DAQ1, CNTBOX_PORT=2000"
```
## PVs of Interest
| PV | Description |
|---------------------------------------|----------------------------------------------------------------------|
| \$(PREFIX):\$(NAME):MsgTxt | Contains unexpected response to executed command |
| \$(PREFIX):\$(NAME):STATUS | 0: Idle, 1: Counting, 2: Low rate, 3: Paused, 4: Error |
| \$(PREFIX):\$(NAME):MONITOR-CHANNEL | Channel that PRESET-COUNT monitors (has RBV, only v2 can be changed) |
| \$(PREFIX):\$(NAME):PRESET-COUNT | Run count until specified pv value reached |
| \$(PREFIX):\$(NAME):PRESET-TIME | Run count until specified pv value in seconds reached |
| \$(PREFIX):\$(NAME):THRESHOLD | Minimum rate for counting to preceed. (has RBV) |
| \$(PREFIX):\$(NAME):THRESHOLD-MONITOR | Channel monitored for minimum rate (has RBV) |
| \$(PREFIX):\$(NAME):ELAPSED-TIME | Time Counterbox has been measuring for |
| \$(PREFIX):\$(NAME):M_ | Current count on channel. (1-10 depending on box) |
| \$(PREFIX):\$(NAME):CHANNELS | Number of available channels (4, 8 or 10) |
## 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
working and to check the IOC - Nicos integration. These can be loaded at
runtime via the following
```
epicsEnvSet("LOAD_TEST_PVS","")
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)
## 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;
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)';
@ -95,6 +95,12 @@ readMinRate{
################################################################################
# Read Values From Monitors
readAll4 {
out "RA";
in "%(\$1ELAPSED-TIME)f %(\$1M1)d %(\$1M2)d %(\$1M3)d %(\$1M4)d";
@mismatch{in "%(\$1MsgTxt)s";}
}
readAll8 {
out "RA";
in "%(\$1ELAPSED-TIME)f %(\$1M1)d %(\$1M2)d %(\$1M3)d %(\$1M4)d %(\$1M5)d %(\$1M6)d %(\$1M7)d %(\$1M8)d";

29
db/counterbox_4ch.db Normal file
View File

@ -0,0 +1,29 @@
# Counterbox EPICS Database
# 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
record(longin, "$(P):$(NAME):MONITOR-CHANNEL")
{
field(DESC, "PRESET-COUNT Monitors this channel")
field(VAL, 1)
field(DISP, 1)
}
record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV")
{
field(DESC, "PRESET-COUNT Monitors this channel")
field(VAL, 1)
field(DISP, 1)
}
################################################################################
# Count Commands
################################################################################
# Read all monitors values

View File

@ -25,22 +25,5 @@ record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV")
################################################################################
# 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(SCAN, ".2 second")
field(DTYP, "stream")
field(FLNK, "$(P):$(NAME):UNSET-COUNTING")
}

View File

@ -23,7 +23,7 @@ record(fanout, "$(P):$(NAME):INIT-BOX")
field(DESC, "Rewrite PVs to Box")
field(SELM, "All")
field(LNK0, "$(P):$(NAME):MONITOR-CHANNEL_RBV PP")
field(LNK1, "$(P):$(NAME):READALL PP")
field(LNK1, "$(P):$(NAME):RAW-STATUS PP")
field(LNK2, "$(P):$(NAME):THRESHOLD_RBV PP")
}
@ -63,45 +63,17 @@ record(seq, "$(P):$(NAME):REINIT-CONF")
field(SELL, "$(P):$(NAME):INVALID-CONFIG.VAL")
}
# The COUNTING PV stays True until Counterbox has switched back to idle mode
# and the monitor counts have been read. Therefore, we know that the monitor
# values have been updated to represent their final values, when this switches
# back to False.
#
# This is accomplished via the explicit SET-COUNTING and UNSET-COUNTING seq
# records, that are triggered by a switch to the counting status
# (RAW-STATUS == 1 || 2) and a read of the monitors respectively.
record(bi, "$(P):$(NAME):COUNTING")
{
field(DESC, "Counterbox is Counting")
field(VAL, 0)
}
record(seq, "$(P):$(NAME):SET-COUNTING")
{
field(LNK1, "$(P):$(NAME):COUNTING PP")
field(DO1, 1)
field(SELM, "Specified")
field(SELL, "$(P):$(NAME):MAP-STATUS.VAL")
field(FLNK, "$(P):$(NAME):STATUS")
}
record(seq, "$(P):$(NAME):UNSET-COUNTING")
{
field(LNK0, "$(P):$(NAME):COUNTING PP")
field(DO0, 0)
field(SELM, "Specified")
field(SELL, "$(P):$(NAME):RAW-STATUS.VAL")
field(FLNK, "$(P):$(NAME):MAP-STATUS")
}
# Important! The "$(P):$(NAME):READALL" isn't configure with a SCAN. Instead,
# it must always be triggered after the value of $(P):$(NAME):RAW-STATUS is
# updated, so that it can't be the case that the status changes back from
# counting to idle, without having updated the time and count values.
record(longin, "$(P):$(NAME):RAW-STATUS")
{
field(DESC, "Raw returned status value")
field(DTYP, "stream")
field(SCAN, ".2 second")
field(SCAN, ".1 second")
field(INP, "@$(PROTO) readStatus($(P):$(NAME):) $(ASYN_PORT)")
field(FLNK, "$(P):$(NAME):MAP-STATUS")
field(FLNK, "$(P):$(NAME):READALL")
}
record(calc, "$(P):$(NAME):MAP-STATUS")
@ -109,9 +81,9 @@ 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(INPC, "$(P):$(NAME):COUNTING NPP")
field(CALC, "B=1?4:(C=1&&A=0)||A=1||A=2?1:A=0?0:A=5||A=6?2:A=9||A=13||A=10||A=14?3:4")
field(FLNK, "$(P):$(NAME):SET-COUNTING")
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")
}
record(mbbi, "$(P):$(NAME):STATUS")
@ -131,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
@ -195,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")
@ -203,52 +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")
}
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")
}
# Not yet sure whether we want to support this
# record(longin, "$(P):$(NAME):R1")
# {

View File

@ -27,62 +27,5 @@ record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV")
################################################################################
# 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(SCAN, ".2 second")
field(DTYP, "stream")
field(FLNK, "$(P):$(NAME):UNSET-COUNTING")
}
record(longin, "$(P):$(NAME):M9")
{
field(DESC, "Counterbox CH9")
}
record(longin, "$(P):$(NAME):M10")
{
field(DESC, "Counterbox CH10")
}
################################################################################
# Testing Commands
# These won't match the values on the machine after a full restart But I chose
# not to force their intialisation as they are only important for testing
record(bo, "$(P):$(NAME):TESTGEN")
{
field(DESC, "Turn on/off Testgen Signal")
field(DTYP, "stream")
field(OUT, "@$(PROTO) switchTestgenOnOff($(P):$(NAME):) $(ASYN_PORT)")
field(VAL, 0)
}
record(longout, "$(P):$(NAME):TESTGEN-LOWRATE")
{
field(DESC, "Set Minimum Testgen Rate")
field(DTYP, "stream")
field(OUT, "@$(PROTO) setTestSignal($(P):$(NAME):) $(ASYN_PORT)")
field(VAL, 1000)
}
record(longout, "$(P):$(NAME):TESTGEN-HIGHRATE")
{
field(DESC, "Set Maximum Testgen Rate")
field(DTYP, "stream")
field(OUT, "@$(PROTO) setTestSignal($(P):$(NAME):) $(ASYN_PORT)")
field(VAL, 1000)
}

31
db/counterbox_v2_test.db Normal file
View File

@ -0,0 +1,31 @@
################################################################################
# Testing Commands
# These won't match the values on the machine after a full restart But I chose
# not to force their intialisation as they are only important for testing
record(bo, "$(P):$(NAME):TESTGEN")
{
field(DESC, "Turn on/off Testgen Signal")
field(DTYP, "stream")
field(OUT, "@$(PROTO) switchTestgenOnOff($(P):$(NAME):) $(ASYN_PORT)")
field(VAL, 0)
field(ZNAM, "OFF")
field(ONAM, "ON")
}
record(longout, "$(P):$(NAME):TESTGEN-LOWRATE")
{
field(DESC, "Set Minimum Testgen Rate")
field(DTYP, "stream")
field(OUT, "@$(PROTO) setTestSignal($(P):$(NAME):) $(ASYN_PORT)")
field(VAL, 1000)
}
record(longout, "$(P):$(NAME):TESTGEN-HIGHRATE")
{
field(DESC, "Set Maximum Testgen Rate")
field(DTYP, "stream")
field(OUT, "@$(PROTO) setTestSignal($(P):$(NAME):) $(ASYN_PORT)")
field(VAL, 1000)
}

View File

@ -1,13 +0,0 @@
require asyn
# Need to be set by user
# epicsEnvSet("CNTBOX_HOST", "testinst-daq1:2000")
# epicsEnvSet("ASYN_PORT", "el737")
# epicsEnvSet("PREFIX", "SQ:SINQTEST")
# epicsEnvSet("NAME", "COUNTERBOX")
epicsEnvSet("PROTO", "$(sinq_DB)counterbox.proto")
drvAsynIPPortConfigure("$(ASYN_PORT)", "$(CNTBOX_HOST)", 0, 0, 0)
dbLoadRecords("$(sinq_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)")
dbLoadRecords("$(sinq_DB)counterbox.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)")

View File

@ -0,0 +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_$(NAME)", "$($(NAME)_CNTBOX_HOST)", 0, 0, 0)
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_$(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

@ -0,0 +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_$(NAME)", "$($(NAME)_CNTBOX_HOST)", 0, 0, 0)
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_$(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,13 +1,29 @@
require asyn
require stream
# Need to be set by user
# epicsEnvSet("CNTBOX_HOST", "testinst-daq1:2000")
# epicsEnvSet("ASYN_PORT", "el737")
# epicsEnvSet("PREFIX", "SQ:SINQTEST")
# epicsEnvSet("NAME", "COUNTERBOX")
epicsEnvSet("$(NAME)_CNTBOX_HOST", "$(CNTBOX_IP):$(CNTBOX_PORT=2000)")
epicsEnvSet("PROTO", "$(sinq_DB)counterbox.proto")
$(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'
drvAsynIPPortConfigure("$(ASYN_PORT)", "$(CNTBOX_HOST)", 0, 0, 0)
dbLoadRecords("$(sinq_DB)counterbox_common.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)")
dbLoadRecords("$(sinq_DB)counterbox_v2.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=$(ASYN_PORT)")
epicsEnvSet("PROTO", "$(counterbox_DB)counterbox.proto")
drvAsynIPPortConfigure("ASYN_$(NAME)", "$($(NAME)_CNTBOX_HOST)", 0, 0, 0)
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_$(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")
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)