25 Commits

Author SHA1 Message Date
5b772152c9 Fix low rate deduction on HW daq
Some checks failed
Example Action / Lint (push) Successful in 8s
Example Action / BuildAndTest (push) Failing after 14s
2026-02-09 08:32:48 +01:00
00b9093b37 SPC: add example for st.cmd and nicos setup
All checks were successful
Example Action / Lint (push) Successful in 2s
Example Action / BuildAndTest (push) Successful in 38s
2026-02-06 11:05:51 +01:00
38e774cf84 Add back missing comment in db file 2026-02-06 10:29:24 +01:00
6cf24254e3 Last big things:
* Implemented support for diabling theshold channel
* Separate proton current simulation db.
* Separate startup snippet with support for simulation mode.
* Short readme entry
2026-02-06 10:29:24 +01:00
4e42eab39b Kind of working, but time based count doesn't work from nicos 2026-02-06 10:29:24 +01:00
b584085218 Counting from nicos works, but time preset is not correct 2026-02-06 10:29:24 +01:00
de012d37ea Correct meaning of IS_LOWRATE
0 means not low-rate i.e. good rate
1 means low-rate
2026-02-06 10:29:24 +01:00
94402c4592 make it run 2026-02-06 10:29:24 +01:00
45b01bf4c4 compiled soft_proton 2026-02-06 10:29:24 +01:00
025e985b75 wip 2026-02-06 10:29:24 +01:00
14f4e3eee7 Add startup snippet 2026-02-06 10:29:24 +01:00
2aa6bd4405 Continued work 2026-02-06 10:29:24 +01:00
1d47b02833 More work 2026-02-06 10:29:24 +01:00
bec218ad86 More work on soft proton counter 2026-02-06 10:29:24 +01:00
94814b2140 Start work on sinqdaq soft proton implementation 2026-02-06 10:29:24 +01:00
4801dc3279 adds example of EL737 and description of what differentiates the older and newer systems
Some checks failed
Example Action / Lint (push) Successful in 2s
Example Action / BuildAndTest (push) Failing after 9s
2026-02-04 10:39:32 +01:00
df4f140c12 adds link to sister-module
Some checks failed
Example Action / Lint (push) Successful in 2s
Example Action / BuildAndTest (push) Failing after 9s
2025-11-20 15:39:16 +01:00
872f21d9bc Merge pull request 'Monitor channels, change record type to int64in' (#4) from 64bit_monitors into master
Some checks failed
Example Action / Lint (push) Successful in 2s
Example Action / BuildAndTest (push) Failing after 9s
Reviewed-on: #4
2025-11-17 10:54:03 +01:00
9958ef428b Monitor channels, change record type to int64in
Some checks failed
Example Action / Lint (push) Successful in 8s
Example Action / BuildAndTest (push) Failing after 15s
2025-11-17 09:54:34 +01:00
e91b0e54b7 make limits on 4 and 8 channel consequent
Some checks failed
Example Action / Lint (push) Successful in 4s
Example Action / BuildAndTest (push) Failing after 10m0s
2025-09-19 10:31:16 +02:00
c2b73e44cc Merge pull request 'removes duplicated clearing of channels and cleans up status mapping for nicos' (#3) from v3cleanup into master
Some checks failed
Example Action / Lint (push) Successful in 3s
Example Action / BuildAndTest (push) Failing after 9m56s
Reviewed-on: #3
2025-09-17 13:57:27 +02:00
14214e6151 low rate status is handled differently in new and old box
Some checks failed
Example Action / Lint (push) Successful in 3s
Example Action / BuildAndTest (push) Failing after 9m52s
2025-09-17 13:54:08 +02:00
b8406016d2 removes duplicated clearing of channels and cleans up status mapping for nicos
Some checks failed
Example Action / Lint (push) Successful in 3s
Example Action / BuildAndTest (push) Failing after 9m42s
2025-09-17 12:26:47 +02:00
b26704d061 Merge pull request 'Simulate more correct counter resetting' (#2) from fix_counter_reset into master
All checks were successful
Example Action / Lint (push) Successful in 2s
Example Action / BuildAndTest (push) Successful in 10m14s
Reviewed-on: #2
2025-09-10 15:28:58 +02:00
5f86266fc0 Simulate more correct counter resetting
All checks were successful
Example Action / Lint (push) Successful in 2s
Example Action / BuildAndTest (push) Successful in 10m13s
2025-09-10 12:51:56 +02:00
14 changed files with 904 additions and 39 deletions

View File

@@ -20,6 +20,8 @@ TEMPLATES += db/daq_common.db
TEMPLATES += db/daq_2nd_gen.db
TEMPLATES += db/daq_2nd_gen_test.db
TEMPLATES += db/daq.proto
TEMPLATES += db/daq_soft_proton.db
TEMPLATES += db/sim_proton_current.db
# Just for simulation
TEMPLATES += db/daq_simcontrol.db
@@ -29,13 +31,15 @@ DBDS += src/daq.dbd
# Source files to build
SOURCES += src/daq.cpp
SOURCES += src/daq_soft_proton.c
SCRIPTS += scripts/daq_4ch.cmd
SCRIPTS += scripts/daq_8ch.cmd
SCRIPTS += scripts/daq_2nd_gen.cmd
SCRIPTS += scripts/daq_soft_proton.cmd
SCRIPTS += sim/daq_sim.py
CXXFLAGS += -std=c++17
USR_CFLAGS += -Wall -Wextra #-Werror
USR_CFLAGS += -std=c11 -Wall -Wextra #-Werror
# MISCS would be the place to keep the stream device template files

189
README.md
View File

@@ -7,6 +7,24 @@ SINQ.
This supports the older 4 and 8 channel EL737 models and the new 10CH 2nd
generation systems.
**Note:** the epics side of this interface is also implemented/shared by the
[StreamGenerator](https://gitea.psi.ch/lin-epics-modules/StreamGenerator)
module.
## Functional Differences Between Models
The 2nd Generation DAQ offers some additional features that aren't available on the
older EL737 Counterboxes. Specifically,
* the possibility to change the channel monitored by the count-based preset (on
the older EL737 boxes, only the 1st channel can be used)
* two gating inputs, that enable counting to be halted via configurable
high/low electrical inputs.
* a test signal generator
Furthermore, the 2nd Generation DAQ's have 10 input channels, in place of the 8
or 4 channels on the older EL737 Counterboxes.
## How to Use
Unless a custom database is needed, a device can be configure simply by setting
@@ -60,7 +78,7 @@ runScript "$(sinqDAQ_DIR)daq_2nd_gen.cmd" "NAME=DAQ, DAQ_IP=TestInst-DAQ1, DAQ_P
## Generating Test Signals
The 2nd generation systems have two test channels that can be used to output
The 2nd Generation DAQ's 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
@@ -78,7 +96,82 @@ A set of Nicos devices have been developed which allow control of the Detector
Hardware via this Epics Driver. The corresponding code can be found in
[sinqdaq.py](https://gitea.psi.ch/lin-instrument-computers/Nicos/src/branch/release-3.12/nicos_sinq/devices/epics/sinqdaq.py).
## Full Example
## Full Example of 8 Channel EL737 Counterbox
Include the following snippet in your IOC
```
# st.cmd at TASP
epicsEnvSet("STREAM_PROTOCOL_PATH","./db")
epicsEnvSet("INSTR","SQ:SINQTEST:")
require sinqDAQ
runScript "$(sinqDAQ_DIR)daq_8ch.cmd" "NAME=counter, DAQ_IP=tasp-ts0, DAQ_PORT=3004"
```
What follows is an example Nicos setup file.
```
# simplified tasp.py
countprefix = 'SQ:TASP:counter'
configured_channels = ['detector', 'protoncount']
devices = dict(
elapsedtime = device(
'nicos_sinq.devices.epics.sinqdaq.DAQTime',
daqpvprefix = countprefix,
),
detector = device(
'nicos_sinq.devices.epics.sinqdaq.DAQChannel',
description = 'Actual neutron detector',
daqpvprefix = countprefix,
channel = 1,
type = 'counter',
),
protoncount = device(
'nicos_sinq.devices.epics.sinqdaq.DAQChannel',
description = 'Monitor for proton current',
daqpvprefix = countprefix,
channel = 2,
type = 'monitor',
),
DAQPreset = device(
'nicos_sinq.devices.epics.sinqdaq.DAQPreset',
description = '8 Channel EL737 Counterbox',
daqpvprefix = countprefix,
channels = configured_channels,
time_channel = ['elapsedtime'],
),
ThresholdChannel = device(
'nicos_sinq.devices.epics.sinqdaq.DAQMinThresholdChannel',
daqpvprefix = countprefix,
channels = configured_channels,
),
Threshold = device(
'nicos_sinq.devices.epics.sinqdaq.DAQMinThreshold',
daqpvprefix = countprefix,
min_rate_channel = 'ThresholdChannel',
),
taspdet = device(
'nicos_sinq.devices.epics.sinqdaq.SinqDetector',
description = 'Detector Interface',
timers = ['elapsedtime'],
monitors = ['DAQPreset'] + configured_channels,
others = [],
liveinterval = 1,
),
)
startupcode = '''
SetDetectors(taspdet)
ThresholdChannel.move('protoncount')
'''
```
## Full Example of 2nd Generation DAQ
Include the following snippet in your IOC
@@ -128,32 +221,35 @@ devices = dict(
'nicos_sinq.devices.epics.sinqdaq.DAQMinThresholdChannel',
daqpvprefix = countprefix,
channels = [],
visibility = {'metadata', 'namespace'},
),
Threshold = device(
'nicos_sinq.devices.epics.sinqdaq.DAQMinThreshold',
daqpvprefix = countprefix,
min_rate_channel = 'ThresholdChannel',
visibility = {'metadata', 'namespace'},
),
Gate1 = device(
'nicos_sinq.devices.epics.sinqdaq.DAQGate',
daqpvprefix = countprefix,
channel = 1,
visibility = {'metadata', 'namespace'},
visibility = {'metadata', 'namespace'},
),
Gate2 = device(
'nicos_sinq.devices.epics.sinqdaq.DAQGate',
daqpvprefix = countprefix,
channel = 2,
visibility = {'metadata', 'namespace'},
visibility = {'metadata', 'namespace'},
),
# Only necessary if you want to use the signal generator in the
# 2nd Generation DAQ for testing.
TestGen = device('nicos_sinq.devices.epics.sinqdaq.DAQTestGen',
daqpvprefix = countprefix,
visibility = {'metadata', 'namespace'},
visibility = {'metadata', 'namespace'},
),
)
# On an actual instrument, it might be better if instead of just calling
# your channels 'Monitor <num>', you describe what is actually plugged
# into the DAQ on each channel.
for i in range(10):
devices[f'monitor{i+1}'] = device(
'nicos_sinq.devices.epics.sinqdaq.DAQChannel',
@@ -203,3 +299,82 @@ 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.
## Software based proton current DAQ
This repository also contain a software based DAQ implementation that just
requires a channel access link (remote Process Variable) to a proton rate.
From that it implements the same interface as the counter boxes, and is
compatible with the sinqdaq device in Nicos, albeit with only one channel.
### Simulated software based proton current DAQ
You can start a proton current DAQ based on a simulation with the following
command on any PC having the SINQ epics environment installed.
```
iocsh -r sinqDAQ,0.4.0 -c 'epicsEnvSet(SET_SIM_MODE,"" )' -c 'epicsEnvSet(INSTR,SQ:TEST:)' -c 'epicsEnvSet(NAME, SPC)' -c 'runScript $(sinqDAQ_DIR)daq_soft_proton.cmd'
```
### Example st.cmd declaration
To include it in an existing st.cmd, add the following lines:
```
require sinqDAQ, 0.4.0
# This is the proton current PV from HIPA, however it's not available in the shutdown
epicsEnvSet(REMOTE_RATE_PV,"MHC6:IST:2")
# During shutdown you can specify this empty env var to have a simulated proton current.
epicsEnvSet(SET_SIM_MODE,"" ) # During operation remove this.
runScript $(sinqDAQ_DIR)daq_soft_proton.cmd "INSTR=SQ:TEST:, NAME=SPC"
```
### Example NICOS setup file declaration
```
description = 'Setup for software based proton counter'
pvprefix = 'SQ:TEST:SPC'
channels = ['protoncount']
devices = dict(
elapsedtime = device(
'nicos_sinq.devices.epics.sinqdaq.DAQTime',
daqpvprefix = pvprefix,
),
protoncount = device(
'nicos_sinq.devices.epics.sinqdaq.DAQChannel',
description = 'Proton counter channel',
daqpvprefix = pvprefix,
channel = 1,
type = 'monitor',
),
preset = device(
'nicos_sinq.devices.epics.sinqdaq.DAQPreset',
description = 'Time/Count Preset',
daqpvprefix = pvprefix,
channels = channels,
time_channel = ['elapsedtime'],
),
ThresholdChannel = device(
'nicos_sinq.devices.epics.sinqdaq.DAQMinThresholdChannel',
daqpvprefix = pvprefix,
channels = channels,
),
Threshold = device(
'nicos_sinq.devices.epics.sinqdaq.DAQMinThreshold',
daqpvprefix = pvprefix,
min_rate_channel = 'ThresholdChannel',
),
spcdet = device(
'nicos_sinq.devices.epics.sinqdaq.SinqDetector',
description = 'Detector device that estimates proton counts in software',
timers = ['elapsedtime'],
counters = [],
monitors = ['preset'] + channels,
images = [],
others = [],
liveinterval = 7,
saveintervals = [60]
),
)
startupcode = '''
SetDetectors(spcdet)
'''
```

View File

@@ -94,7 +94,7 @@ record(ao,"$(INSTR)$(NAME):THRESH$(CHANNEL)")
################################################################################
# Read all monitors values
record(longin, "$(INSTR)$(NAME):M$(CHANNEL)")
record(int64in, "$(INSTR)$(NAME):M$(CHANNEL)")
{
field(DESC, "DAQ CH$(CHANNEL)")
field(EGU, "cts")

View File

@@ -204,52 +204,33 @@ setGate {
}
################################################################################
# TODO To clean
startWithCountPreset4 {
clearTimer;
clearCounter4;
readAll4;
startWithCountPreset;
}
startWithCountPreset8 {
clearTimer;
clearCounter4;
clearCounter8;
readAll8;
startWithCountPreset;
}
startWithCountPreset10 {
clearTimer;
clearCounter4;
clearCounter8;
clearCounter10;
readAll10;
startWithCountPreset;
}
startWithTimePreset4 {
clearTimer;
clearCounter4;
readAll4;
startWithTimePreset;
}
startWithTimePreset8 {
clearTimer;
clearCounter4;
clearCounter8;
readAll8;
startWithTimePreset;
}
startWithTimePreset10 {
clearTimer;
clearCounter4;
clearCounter8;
clearCounter10;
readAll10;
startWithTimePreset;
}

View File

@@ -38,6 +38,13 @@ record(seq, "$(INSTR)$(NAME):CORRECT-MONITOR-CHANNEL")
field(SCAN, ".5 second")
}
record(calc, "$(INSTR)$(NAME):RATE_MAP")
{
field(DESC, "Want a consistent lowrate pv")
field(INPA, "$(INSTR)$(NAME):RAW-STATUS.B2 NPP")
field(CALC, "(A=1)?0:1")
}
################################################################################
# Count Commands

View File

@@ -13,8 +13,8 @@ record(longout, "$(INSTR)$(NAME):MONITOR-CHANNEL")
{
field(DESC, "PRESET-COUNT Monitors this channel")
field(VAL, 1)
field(DRVL, "1") # Smallest Monitor Channel
field(DRVH, "1") # Largest Monitor Channel
field(DRVL, 0) # Smallest Monitor Channel (should really be 1)
field(DRVH, 1) # Largest Monitor Channel
field(DISP, 1)
}
@@ -25,6 +25,13 @@ record(longin, "$(INSTR)$(NAME):MONITOR-CHANNEL_RBV")
field(DISP, 1)
}
record(calc, "$(INSTR)$(NAME):RATE_MAP")
{
field(DESC, "Want a consistent lowrate pv")
field(INPA, "$(INSTR)$(NAME):RAW-STATUS.B2 NPP")
field(CALC, "(A=0)?0:1")
}
################################################################################
# Count Commands

View File

@@ -13,8 +13,8 @@ record(longout, "$(INSTR)$(NAME):MONITOR-CHANNEL")
{
field(DESC, "PRESET-COUNT Monitors this channel")
field(VAL, 1)
field(DRVL, "1") # Smallest Monitor Channel
field(DRVH, "1") # Largest Monitor Channel
field(DRVL, 0) # Smallest Monitor Channel (should really be 1)
field(DRVH, 1) # Largest Monitor Channel
field(DISP, 1)
}
@@ -25,6 +25,13 @@ record(longin, "$(INSTR)$(NAME):MONITOR-CHANNEL_RBV")
field(DISP, 1)
}
record(calc, "$(INSTR)$(NAME):RATE_MAP")
{
field(DESC, "Want a consistent lowrate pv")
field(INPA, "$(INSTR)$(NAME):RAW-STATUS.B2 NPP")
field(CALC, "(A=0)?0:1")
}
################################################################################
# Count Commands

View File

@@ -55,7 +55,15 @@ record(seq, "$(INSTR)$(NAME):REINIT-CONF")
# it must always be triggered after the value of $(INSTR)$(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, "$(INSTR)$(NAME):RAW-STATUS")
#
# The status can be interpreted as follows
#
# Bit 0: NC_STAT_P_TIME_C
# Bit 1: NC_STAT_P_COUNT_C
# Bit 2: NC_STAT_RATE_OK_C It appears this should be ignored unless counting
# Bit 3: NC_STAT_PAUSE_C
#
record(mbbiDirect, "$(INSTR)$(NAME):RAW-STATUS")
{
field(DESC, "Raw returned status value")
field(DTYP, "stream")
@@ -64,13 +72,44 @@ record(longin, "$(INSTR)$(NAME):RAW-STATUS")
field(FLNK, "$(INSTR)$(NAME):READALL")
}
record(bi, "$(INSTR)$(NAME):COUNTING_TIME")
{
field(INP, "$(INSTR)$(NAME):RAW-STATUS.B0")
field(ZNAM, "DISABLED")
field(ONAM, "COUNTING")
}
record(bi, "$(INSTR)$(NAME):COUNTING_PRESET")
{
field(INP, "$(INSTR)$(NAME):RAW-STATUS.B1")
field(ZNAM, "DISABLED")
field(ONAM, "COUNTING")
}
record(bi, "$(INSTR)$(NAME):IS_LOWRATE")
{
field(INP, "$(INSTR)$(NAME):RATE_MAP PP")
field(ONAM, "LOW RATE")
field(ZNAM, "GOOD RATE")
}
record(bi, "$(INSTR)$(NAME):IS_PAUSED")
{
field(INP, "$(INSTR)$(NAME):RAW-STATUS.B3")
field(ZNAM, "RUNNING")
field(ONAM, "PAUSED")
}
record(calc, "$(INSTR)$(NAME):MAP-STATUS")
{
field(DESC, "Maps Raw Status to State")
field(INPA, "$(INSTR)$(NAME):RAW-STATUS NPP")
field(INPB, "$(INSTR)$(NAME):INVALID-CONFIG NPP")
field(INPC, "$(INSTR)$(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(INPA, "$(INSTR)$(NAME):INVALID-CONFIG NPP")
field(INPB, "$(INSTR)$(NAME):RAW-STATUS.UDF NPP") # should also be invalid if can't read the status
field(INPC, "$(INSTR)$(NAME):COUNTING_TIME PP")
field(INPD, "$(INSTR)$(NAME):COUNTING_PRESET PP")
field(INPE, "$(INSTR)$(NAME):IS_LOWRATE PP")
field(INPF, "$(INSTR)$(NAME):IS_PAUSED PP")
field(CALC, "(A=1||B=1)?4:(F=1)?3:(C=0&&D=0)?0:(E=1)?2:1")
field(FLNK, "$(INSTR)$(NAME):STATUS")
}

329
db/daq_soft_proton.db Normal file
View File

@@ -0,0 +1,329 @@
# EPICS database for counterbox-like interface using proton current,
# as retrieved via network from HIPA.
#
# This is created to be able reuse the SinqDAQ interface
# in Nicos, to avoid having more code to maintain there.
#
# Commands that has no actual relevant value that needs to be written to them:
# * Pause
# * Continue
# * Stop
# * Full reset
# * Reset elapsed time
# * Reset the counter/monitor value
# Are all implemented as seq records. Due to how the existing
# interface worked, they were being written 1 from nicos.
# It just so turns out that seq record's VAL field triggers processing,
# but a calcout records VAL field doesn't. seq record was the only option
# to emulate these features with only 1 record per command.
#
# This requires following macros to specified:
# INSTR: Instrument prefix, e.g. "SQAMOR:"
# NAME: Name of the DAQ, e.g. "PROTONDAQ"
# SHUTTER1_PV: PV of a shutter state will be taken as a gate signal for the protoon current
# SHUTTER1_CLOSED_VAL: Value of the shutter PV when the shutter is closed
# SHUTTER2_PV: PV of a second shutter state will be taken as a gate signal for the protoon current
# SHUTTER2_CLOSED_VAL: Value of the shutter PV when the shutter is closed
record(mbbi, "$(INSTR)$(NAME):STATUS")
{
field(DESC, "DAQ Status")
field(ZRVL, "0")
field(ZRST, "Idle")
field(ONVL, "1")
field(ONST, "Counting")
field(TWVL, "2")
field(TWST, "Low rate")
field(THVL, "3")
field(THST, "Paused")
# 4 should never happen, if it does it means the DAQ reports undocumented statusbits
field(FRVL, "4")
field(FRST, "INVALID")
# We start in idle
field(VAL, 0)
}
record(longin, "$(INSTR)$(NAME):CHANNELS")
{
field(DESC, "Total Supported Channels")
field(VAL, 1)
field(DISP, 1)
}
record(stringin, "$(INSTR)$(NAME):MsgTxt")
{
field(DESC, "Miscellanous messages")
}
record(bi, "$(INSTR)$(NAME):IS_LOWRATE")
{
field(ZNAM, "LOW RATE")
field(ONAM, "GOOD RATE")
}
################################################################################
# Commands
################################################################################
record(ao, "$(INSTR)$(NAME):PRESET-COUNT")
{
field(DESC, "Count until preset reached")
field(VAL, 0)
field(PREC, 2)
field(FLNK, "$(INSTR)$(NAME):PRESET-COUNT-TRIG")
}
record(longout, "$(INSTR)$(NAME):PRESET-COUNT-TRIG")
{
field(DESC, "Count until preset reached")
# Signal count preset as command
field(VAL, 1)
field(OUT, "$(INSTR)$(NAME):COMMAND-TRIG PP")
field(FLNK, "$(INSTR)$(NAME):COUNT-TYPE")
}
record(ao, "$(INSTR)$(NAME):PRESET-TIME")
{
field(DESC, "Count for specified time")
field(VAL, 0)
field(PREC, 2)
field(EGU, "seconds")
field(FLNK, "$(INSTR)$(NAME):PRESET-TIME-TRIG")
}
record(longout, "$(INSTR)$(NAME):PRESET-TIME-TRIG")
{
field(DESC, "Count until preset time reached")
# Signal count preset as command
field(VAL, 2)
field(OUT, "$(INSTR)$(NAME):COMMAND-TRIG PP")
field(FLNK, "$(INSTR)$(NAME):COUNT-TYPE")
}
record(seq, "$(INSTR)$(NAME):PAUSE")
{
field(DESC, "Pause the current count")
field(SELM, "All")
field(DO0, 3)
field(LNK0, "$(INSTR)$(NAME):COMMAND-TRIG PP")
}
record(seq, "$(INSTR)$(NAME):CONTINUE")
{
field(DESC, "Continue with a count that was paused")
field(SELM, "All")
field(DO0, 4)
field(LNK0, "$(INSTR)$(NAME):COMMAND-TRIG PP")
}
record(seq, "$(INSTR)$(NAME):STOP")
{
field(DESC, "Stop the current counting operation")
field(SELM, "All")
field(DO0, 5)
field(LNK0, "$(INSTR)$(NAME):COMMAND-TRIG PP")
}
record(seq, "$(INSTR)$(NAME):FULL-RESET")
{
field(DESC, "Perform full reset")
field(SELM, "All")
field(DO0, 6)
field(LNK0, "$(INSTR)$(NAME):COMMAND-TRIG PP")
}
# Emulate Reset elapsed time
# 0. set status to busy
# 1. set elapsed time to 0
# 2. set status to OK
record(seq, "$(INSTR)$(NAME):CT")
{
field(SELM, "All")
field(LNK0, "$(INSTR)$(NAME):ETS PP")
field(DO0, 1)
field(LNK1, "$(INSTR)$(NAME):ELAPSED-TIME PP")
field(DO1, 0)
field(LNK2, "$(INSTR)$(NAME):ETS PP")
field(DO2, 0)
}
# Record is to signal command given by the client to the emulated counter.
# It's set back to no command from the emulated counter subroutine record
record(mbbi, "$(INSTR)$(NAME):COMMAND-TRIG")
{
field(DESC, "Command type")
field(VAL, 0)
field(ZRST, "No command")
field(ZRVL, 0)
field(ONST, "Count preset command")
field(ONVL, 1)
field(TWST, "Time preset command")
field(TWVL, 2)
field(THST, "Pause command")
field(THVL, 3)
field(FRST, "Continue command")
field(FRVL, 4)
field(FVST, "Stop command")
field(FVVL, 5)
field(SXST, "Full reset command")
field(SXVL, 6)
}
# Copy COMMAND-TRIG to memorize what type of count is on-going
record(longin, "$(INSTR)$(NAME):COUNT-TYPE") {
field(INP, "$(INSTR)$(NAME):COMMAND-TRIG.VAL NPP")
field(VAL, 0)
}
record(longout,"$(INSTR)$(NAME):THRESHOLD-MONITOR")
{
# Alias to RBV to be compatible with higher level interface
alias("$(INSTR)$(NAME):THRESHOLD-MONITOR_RBV")
field(DESC, "Channel monitored for minimum rate")
field(VAL, "1") # Monitor
field(DRVL, "0") # Smallest Threshold Channel (0 is off)
field(DRVH, "1") # Largest Threshold Channel
}
record(ao,"$(INSTR)$(NAME):THRESHOLD")
{
# Alias to RBV to be compatible with higher level interface
alias("$(INSTR)$(NAME):THRESHOLD_RBV")
field(DESC, "Minimum rate for counting to proceed")
field(VAL, "1") # Default Rate
field(DRVL, "1") # Minimum Rate
field(DRVH, "100000") # Maximum Rate
field(OMSL, "supervisory")
}
record(ai,"$(INSTR)$(NAME):ELAPSED-TIME")
{
field(DESC, "DAQ Measured Time")
field(EGU, "sec")
}
# Current Status of elapsed time
record(bi, "$(INSTR)$(NAME):ETS")
{
field(DESC, "Channel Status")
field(VAL, 0)
field(ZNAM, "OK")
field(ONAM, "CLEARING")
}
record(longout, "$(INSTR)$(NAME):MONITOR-CHANNEL")
{
alias("$(INSTR)$(NAME):MONITOR-CHANNEL_RBV")
field(DESC, "PRESET-COUNT Monitors this channel")
field(VAL, 1)
}
# Array Subroutine record which emulates the counterbox functionality
# Use an aSub because it allows specifying the type.
record(aSub, "$(INSTR)$(NAME):EMULATION")
{
# Scan rate determines how often we sample the rate
# and how often the counter value updates.
field(SCAN, ".1 second")
field(SNAM, "processEmulatedCounter")
# The first 4 inputs are also mapped as the first 4 outputs
field(INPA, "$(INSTR)$(NAME):STATUS")
field(FTA, "ULONG")
field(INPB, "$(INSTR)$(NAME):M1")
field(FTB, "INT64")
field(INPC, "$(INSTR)$(NAME):ELAPSED-TIME")
field(FTC, "DOUBLE")
field(INPD, "$(INSTR)$(NAME):COMMAND-TRIG")
field(FTD, "ULONG")
# Address the PV which are mapped as input backwards
field(INPE, "$(INSTR)$(NAME):THRESHOLD-MONITOR")
field(FTE, "LONG")
field(INPF, "$(INSTR)$(NAME):THRESHOLD")
field(FTF, "DOUBLE")
field(INPG, "$(INSTR)$(NAME):COUNT-TYPE")
field(FTG, "ULONG")
field(INPH, "$(INSTR)$(NAME):PRESET-COUNT")
field(FTH, "DOUBLE")
field(INPI, "$(INSTR)$(NAME):PRESET-TIME")
field(FTI, "DOUBLE")
# L is last input before EPICS 7.0.10
field(INPJ, "$(INSTR)$(NAME):R1-PREV")
field(FTJ, "DOUBLE")
field(INPL, "$(INSTR)$(NAME):R1 PP")
field(FTL, "DOUBLE")
# The first 4 outputs are also mapped as the first 4 inputs
field(OUTA, "$(INSTR)$(NAME):STATUS PP")
field(FTVA, "ULONG")
field(OUTB, "$(INSTR)$(NAME):M1 PP")
field(FTVB, "INT64")
field(OUTC, "$(INSTR)$(NAME):ELAPSED-TIME PP")
field(FTVC, "DOUBLE")
field(OUTD, "$(INSTR)$(NAME):COMMAND-TRIG PP")
field(FTVD, "ULONG")
field(OUTE, "$(INSTR)$(NAME):R1-PREV PP")
field(FTVE, "DOUBLE")
field(OUTF, "$(INSTR)$(NAME):IS_LOWRATE PP")
field(FTVF, "ULONG")
field(OUTF, "$(INSTR)$(NAME):MsgTxt PP")
field(FTVF, "CHAR")
field(NEVF, 40)
}
#######################
# Channel interface
#######################
record(int64in, "$(INSTR)$(NAME):M1")
{
field(DESC, "DAQ CH0, proton current")
field(EGU, "cts")
}
# The proton rate take by a PV over the network, PV named indicated by $(REMOTE_RATE_PV) macro
# It emulates Zero rate if either shutter is closed.
record(calc, "$(INSTR)$(NAME):R1")
{
field(DESC, "Rate of DAQ CH0 proton current")
field(INPA, "$(REMOTE_RATE_PV) CA")
field(INPB, "$(SHUTTER1_PV=0)")
field(INPC, "$(SHUTTER1_CLOSED_VAL=1)")
field(INPD, "$(SHUTTER2_PV=0)")
field(INPE, "$(SHUTTER2_CLOSED_VAL=1)")
# If either shutter is closed we have no rate
field(CALC, "B != C && D != E ? A : 0")
field(EGU, "cts/sec")
}
# Store previous rate value, so we can average over the period
record(ai, "$(INSTR)$(NAME):R1-PREV") {
field(DESC, "Previous rate of DAQ CH0 proton current")
}
# Emulate clearing channel
# 0. set status to busy
# 1. set elapsed time to 0
# 2. set status to OK
record(seq, "$(INSTR)$(NAME):C1")
{
field(SELM, "All")
field(LNK0, "$(INSTR)$(NAME):S1 PP")
field(DO0, 1)
field(LNK1, "$(INSTR)$(NAME):M1 PP")
field(DO1, 0)
field(LNK2, "$(INSTR)$(NAME):S1 PP")
field(DO2, 0)
}
# Current Status of Channel
# This is only to satify the interface.
record(bi, "$(INSTR)$(NAME):S1")
{
field(DESC, "Channel Status")
field(VAL, 0)
field(ZNAM, "OK")
field(ONAM, "CLEARING")
}

12
db/sim_proton_current.db Normal file
View File

@@ -0,0 +1,12 @@
# This is database that provides a simulated proton current
# for testing purposes.
#
record(calc, "$(INSTR)$(NAME):SIM_PROTON_CURR") {
field(SCAN, ".1 second")
field(CALC, "1500 + 101 * SIN(A)")
field(INPA, "$(INSTR)$(NAME):PROTON_CURR_VAR PP")
}
record(calc, "$(INSTR)$(NAME):PROTON_CURR_VAR") {
field(CALC, "VAL + 0.001")
}

View File

@@ -0,0 +1,8 @@
#var softProtonDebug 1
$(SET_SIM_MODE=#)dbLoadRecords("$(sinqDAQ_DB)sim_proton_current.db", "INSTR=$(INSTR), NAME=$(NAME), REMOTE_RATE_PV=NULL")
$(SET_SIM_MODE=#)epicsEnvSet("REMOTE_RATE_PV", "$(INSTR)$(NAME):SIM_PROTON_CURR")
dbLoadRecords("$(sinqDAQ_DB)daq_soft_proton.db", "INSTR=$(INSTR), NAME=$(NAME), REMOTE_RATE_PV=$(REMOTE_RATE_PV)")
iocInit

View File

@@ -50,7 +50,7 @@ class DAQ:
]
def clearCount(self, counter):
self.counts[counter-1] = 0
self.counts[counter] = 0
def clearCounts(self):
self.counts = [0] * self.total_channels
@@ -260,7 +260,11 @@ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
elif re.fullmatch(r'CC (\d+)', data):
counter = int(re.fullmatch(r'CC (\d+)', data).group(1))
daq.clearCount(counter)
num_bits = daq.total_channels
bits = [(counter >> bit) & 1 for bit in range(num_bits - 1, -1, -1)]
for ch, bit in enumerate(bits):
if bit:
daq.clearCount(ch)
send('')
elif re.fullmatch(r'TP (\d+(\.\d+)?)', data):

View File

@@ -3,3 +3,5 @@
#---------------------------------------------
device(stringin, INST_IO, devDAQStringError, "devDAQStringError")
function(processEmulatedCounter)
variable("softProtonDebug", int)

290
src/daq_soft_proton.c Normal file
View File

@@ -0,0 +1,290 @@
/* This is a software emulated counterbox with 1 channel.
* It's stateless implementation with only a process function.
* All states that are need between the periods,
* are saved in the EPICS databases.
* Typically it is sampling the proton rate from HIPA via
* channel access.
*/
#include <string.h>
#include <dbDefs.h>
#include <errlog.h>
#include <aSubRecord.h>
#include <epicsTypes.h>
#include <epicsString.h>
#include <registryFunction.h>
#include <epicsExport.h>
/* Sample rate */
#define SOFT_PROTON_SAMPLE_RATE 0.1
/* To allow setting debug pring from iocsh */
static int softProtonDebug=0;
epicsExportAddress(int, softProtonDebug);
struct spc_internal {
epicsUInt32 status;
epicsInt64 monitor_count;
epicsFloat64 elapsed_time;
epicsUInt32 command_trig;
epicsInt32 threshold_ch;
epicsFloat64 threshold;
epicsFloat64 proton_rate;
epicsFloat64 prev_proton_rate;
epicsUInt32 count_type;
epicsFloat64 preset_count;
epicsFloat64 preset_time;
epicsFloat64 average_rate;
};
/* Enum with values for all commands
* this has to match the what's in the mbbi in the database*/
enum commands {
NONE = 0,
COUNT_PRESET = 1,
TIME_PRESET = 2,
PAUSE = 3,
CONTINUE = 4,
STOP = 5,
FULL_RESET = 6
};
/* Enum with the possible statuses/states
* this has to match the what's in the mbbi in the database*/
enum status {
IDLE = 0,
COUNTING = 1,
LOW_RATE = 2,
PAUSED = 3,
INVALID = 4
};
int handleNoop(struct spc_internal* spc, epicsOldString* msg_text) {
const char* funcstr = "handleNoop";
//if (softProtonDebug) printf("%s was called\n", funcstr);
/* This shouldn't happen, but let's handle it just in case */
if (spc->status >= INVALID || spc->command_trig > FULL_RESET) {
strcpy(*msg_text, "INVALID STATE!");
errlogPrintf("%s: Status and/or command triggers are invalid \n"
"Status has value %d, \n Command trigger has value %d\n",
funcstr, spc->status, spc->command_trig);
return 1;
}
/* Determine if we are idle and have received a noop command */
if (spc->status == IDLE) {
switch (spc->command_trig) {
case PAUSE:
case CONTINUE:
case STOP:
strcpy(*msg_text, "Can not PAUSE/CONTINUE/STOP during IDLE.");
printf("%s: %s\n"
"Status has value %d, \n"
"Command trigger has value %d\n", *msg_text,
funcstr, spc->status, spc->command_trig);
return 1;
}
}
/* Determine if we are counting, low_rate or paused;
* and have received a noop command */
if (spc->status == COUNTING || spc->status == LOW_RATE ||
spc->status == PAUSED) {
switch (spc->command_trig) {
case COUNT_PRESET:
case TIME_PRESET:
strcpy(*msg_text, "Already counting");
printf("%s: Already counting can not start a new count\n"
"Status has value %d, \n"
"Command trigger has value %d\n",
funcstr, spc->status, spc->command_trig);
/* This case could be seen as OK.
* Nothing is keeping us from continuing, so let's do that. */
return 0;
}
}
/* Determine if we are paused and a pause command */
if (spc->status == PAUSED && spc->command_trig == PAUSE) {
errlogPrintf("%s: Already paused\n"
"Status has value %d, \n"
"Command trigger has value %d\n",
funcstr, spc->status, spc->command_trig);
return 1;
}
/* None of the noop cases detected */
return 0;
}
/*
* This function is called everytime the record processes.
* Even though this record can process arrays, we only use it with scalars.
*/
static long processEmulatedCounter(struct aSubRecord *psub)
{
const char* funcstr = "processEmulatedCounter";
//if (softProtonDebug) printf("%s was called\n", funcstr);
/* Declare internal variable */
struct spc_internal spc_int;
struct spc_internal* spc = &spc_int;
/* Copy input values to a struct on the stack
* to simplify creation of functions */
spc->status = *(epicsUInt32*)psub->a;
spc->monitor_count = *(epicsInt64*)psub->b;
spc->elapsed_time = *(epicsFloat64*)psub->c;
spc->command_trig = *(epicsUInt32*)psub->d;
spc->threshold_ch = *(epicsInt32*)psub->e;
spc->threshold = *(epicsFloat64*)psub->f;
spc->count_type = *(epicsUInt32*)psub->g;
spc->preset_count = *(epicsFloat64*)psub->h;
spc->preset_time = *(epicsFloat64*)psub->i;
spc->prev_proton_rate = *(epicsFloat64*)psub->j;
spc->proton_rate = *(epicsFloat64*)psub->l;
/* Get the pointer to output values only to increase readability. */
epicsUInt32* status_out = (epicsUInt32*)psub->vala;
epicsInt64* monitor_count_out = (epicsInt64*)psub->valb;
epicsFloat64* elapsed_time_out = (epicsFloat64*)psub->valc;
epicsUInt32* command_trig_out = (epicsUInt32*)psub->vald;
epicsFloat64* prev_proton_rate_out = (epicsFloat64*)psub->vale;
epicsUInt32* is_low_rate_out = (epicsUInt32*)psub->valf;
epicsOldString* msg_txt_out = (epicsOldString*)psub->valg;
/* Always no matter what, handle the rate */
/* - Calculate average rate */
spc->average_rate = (spc->proton_rate + spc->prev_proton_rate) / 2;
/* - Store current rate as previous rate */
*prev_proton_rate_out = spc->proton_rate;
if (spc->average_rate < spc->threshold &&
spc->threshold_ch >= 1) { /* Channel 0 is interpreted as disabled */
*is_low_rate_out = 1;
} else {
*is_low_rate_out = 0;
}
if (softProtonDebug) {
printf("%s: DEBUG:\n"
"Status has value %d, \n"
"Command trigger has value %d\n",
funcstr, spc->status, spc->command_trig);
}
/* Handle noop situations both valid and invalid */
if (handleNoop(spc, msg_txt_out))
/* We have state that prohibits further processing */
return 0;
/* Commands with priority always yielding IDLE status first */
if (spc->command_trig == FULL_RESET) {
strcpy(*msg_txt_out, "Full reset!");
/* Reset everything, done! */
*status_out = IDLE;
*monitor_count_out = 0;
*elapsed_time_out = 0.0;
*command_trig_out = NONE;
if (softProtonDebug) printf("%s: Full reset done!\n", funcstr);
return 0;
} else if (spc->command_trig == STOP ||
(spc->status == IDLE && spc->command_trig == NONE)) {
/* Stop is always valid, retains everything except status goes to idle.
* This happens to be the same case as when IDLE and no command received */
*status_out = IDLE;
*command_trig_out = NONE;
*monitor_count_out = spc->monitor_count;
*elapsed_time_out = spc->elapsed_time;
if (softProtonDebug) printf("%s: Case STOP or IDLE!\n", funcstr);
return 0;
} else if (((spc->status == COUNTING || spc->status == LOW_RATE)
&& spc->command_trig == PAUSE) ||
(spc->status == PAUSED && spc->command_trig == NONE)) {
strcpy(*msg_txt_out, "Stopping!");
/* We are counting or at low rate and received pause.
* Also we are paused but have received no command, this
* is probably slightly inaccurate but simplifies things.*/
*status_out = PAUSED;
*command_trig_out = NONE;
*monitor_count_out = spc->monitor_count;
*elapsed_time_out = spc->elapsed_time;
if (softProtonDebug) printf("%s: Case PAUSE!\n", funcstr);
return 0;
} else if (spc->status == IDLE &&
(spc->command_trig == COUNT_PRESET || spc->command_trig == TIME_PRESET)) {
/* Determine if we are idle but received a count command */
if (softProtonDebug) printf("%s: Starting Count!\n", funcstr);
/* Sanity check that count type is properly stored */
if (spc->command_trig != spc->count_type) {
if (softProtonDebug) printf("%s: Count type not stored!\n", funcstr);
return -1;
}
/* Starting a new count
* Reset counter and time */
*monitor_count_out = 0;
*elapsed_time_out = 0;
*command_trig_out = NONE;
if (*is_low_rate_out == 1) {
*status_out = LOW_RATE;
} else {
*status_out = COUNTING;
}
if (softProtonDebug) printf("%s: Case received COUNT command!\n", funcstr);
return 0;
} else if ((((spc->status == LOW_RATE && spc->command_trig == NONE) ||
(spc->status == LOW_RATE && spc->command_trig == CONTINUE)))
&& *is_low_rate_out == 1) {
*status_out = LOW_RATE;
/* LOW_RATE or resuming from PAUSE */
*command_trig_out = NONE;
/* Maintain same counter and time*/
*monitor_count_out = spc->monitor_count;
*elapsed_time_out = spc->elapsed_time;
if (softProtonDebug) printf("%s: Case LOW_RATE!\n", funcstr);
return 0;
}
if (softProtonDebug) printf("%s: Case COUNT incremental cycle!\n", funcstr);
/* Ending up here means:
* 1. status == COUNTING && command_trig == NONE
* 2. status == COUNTING && command_trig == CONTINUE
* 3. status == PAUSED && command_trig == CONTINUE
* 4. status == LOW_RATE && command_trig in [ NONE, CONTINUE ] && high_rate
*/
/* We may have had a command */
*command_trig_out = NONE;
/* Normal incremental count */
*status_out = COUNTING;
/* Increament counter and time */
*monitor_count_out = spc->monitor_count +
spc->average_rate * SOFT_PROTON_SAMPLE_RATE;
*elapsed_time_out = spc->elapsed_time + SOFT_PROTON_SAMPLE_RATE;
/* Check if we are below threshold */
if (*is_low_rate_out == 1) {
*status_out = LOW_RATE;
} else {
*status_out = COUNTING;
}
/* Check if count is finished normally.
* Higher priority than low rate */
if (/* Time based count finished */
(*elapsed_time_out >= spc->preset_time &&
spc->count_type == TIME_PRESET) ||
/* Monitor based count finished */
(*monitor_count_out >= spc->preset_count &&
spc->count_type == COUNT_PRESET)) {
*status_out = IDLE;
}
return 0;
}
/* Register these symbols for use by IOC code: */
epicsRegisterFunction(processEmulatedCounter);