diff --git a/.clang-format b/.clang-format index 67fb9e0..2dec313 100644 --- a/.clang-format +++ b/.clang-format @@ -42,7 +42,7 @@ AllowAllArgumentsOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: Never AllowShortCaseLabelsOnASingleLine: false -AllowShortEnumsOnASingleLine: true +AllowShortEnumsOnASingleLine: false AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: All diff --git a/Makefile b/Makefile index 56ca12a..8e7762c 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 8d6daeb..eb3f219 100644 --- a/README.md +++ b/README.md @@ -299,3 +299,107 @@ 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' +``` + +### Default behavior: count as a counterbox +By default the proton current counter simulates the beahvior of the actual signals +routed to real counterboxes. That signal converts the current to frequency modulated signal +that can be counted the same way as monitors are counted by the counter box. + +Furthermore Urs Greuter wrote me the following as reference: +> 1500 uA Strahlstrom erzeugt ein 150 kHz Pulssignal (Rechteck-Frequenzsignal) +> +> Eine Messung von 1 s sollte bei 1500 uA Strahlstrom 150'000 Cts ergeben. +> +> => Ratio [frequency/proton_current] = 100 kHz/mA + +This is implemented by default, meaning it take the value in `uA` from HIPA and +multiplies by a factor 100. The rate, `R1`, then has the unit `cts/sec` and the +cumulative counter, `M1`, has the unit `cts`. + +### Setup current based accumlation +To use proton **current** instead of counts conversion, the following can be added before `daq_soft_proton.cmd` is ran. +Naturally you are also able to set the cumulative counter EGU, in this example changed to `uC`. +``` +epicsEnvSet(SCALE_FACTOR, 1) +epicsEnvSet(RATE_EGU, "uA") +epicsEnvSet(COUNTER_EGU, "uC") +``` + +### 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) +''' +``` diff --git a/db/daq_2nd_gen.db b/db/daq_2nd_gen.db index b835201..d90fc0c 100644 --- a/db/daq_2nd_gen.db +++ b/db/daq_2nd_gen.db @@ -42,7 +42,7 @@ 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)?1:0") + field(CALC, "(A=1)?0:1") } ################################################################################ diff --git a/db/daq_4ch.db b/db/daq_4ch.db index debd03b..8dc3a39 100644 --- a/db/daq_4ch.db +++ b/db/daq_4ch.db @@ -29,7 +29,7 @@ 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)?1:0") + field(CALC, "(A=0)?0:1") } ################################################################################ diff --git a/db/daq_8ch.db b/db/daq_8ch.db index b29dce0..6ad2c56 100644 --- a/db/daq_8ch.db +++ b/db/daq_8ch.db @@ -29,7 +29,7 @@ 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)?1:0") + field(CALC, "(A=0)?0:1") } ################################################################################ diff --git a/db/daq_common.db b/db/daq_common.db index 403ed49..787d6de 100644 --- a/db/daq_common.db +++ b/db/daq_common.db @@ -89,8 +89,8 @@ record(bi, "$(INSTR)$(NAME):COUNTING_PRESET") record(bi, "$(INSTR)$(NAME):IS_LOWRATE") { field(INP, "$(INSTR)$(NAME):RATE_MAP PP") - field(ZNAM, "LOW RATE") - field(ONAM, "GOOD RATE") + field(ONAM, "LOW RATE") + field(ZNAM, "GOOD RATE") } record(bi, "$(INSTR)$(NAME):IS_PAUSED") @@ -109,7 +109,7 @@ record(calc, "$(INSTR)$(NAME):MAP-STATUS") 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=0)?2:1") + field(CALC, "(A=1||B=1)?4:(F=1)?3:(C=0&&D=0)?0:(E=1)?2:1") field(FLNK, "$(INSTR)$(NAME):STATUS") } diff --git a/db/daq_soft_proton.db b/db/daq_soft_proton.db new file mode 100644 index 0000000..ab2f38e --- /dev/null +++ b/db/daq_soft_proton.db @@ -0,0 +1,334 @@ +# 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(waveform, "$(INSTR)$(NAME):MsgTxt") +{ + field(DESC, "Miscellanous messages") + field(FTVL, "CHAR") + field(NELM, 40) +} + +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(OUTG, "$(INSTR)$(NAME):MsgTxt PP") + field(FTVG, "CHAR") + field(NOVG, 40) +} + +####################### +# Channel interface +####################### +record(int64in, "$(INSTR)$(NAME):M1") +{ + field(DESC, "DAQ CH0, proton current") + field(EGU, "$(COUNTER_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)") + # The proton current signal routed to real counterboxes + # transfer the current to frequency modulated signal. + # The relation ship 1uA -> 100Hz, meaning that nominal 1500uA -> 150kHz + field(INPF, "$(SCALE_FACTOR=100)") + # If either shutter is closed we have no rate + field(CALC, "B != C && D != E ? A * F : 0") + field(EGU, "$(RATE_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") +} + diff --git a/db/sim_proton_current.db b/db/sim_proton_current.db new file mode 100644 index 0000000..eb94300 --- /dev/null +++ b/db/sim_proton_current.db @@ -0,0 +1,13 @@ +# 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") +} + +# Incrementing value to pass to the Sinus +record(calc, "$(INSTR)$(NAME):PROTON-CURR-VAR") { + field(CALC, "VAL + 0.001") +} diff --git a/scripts/daq_soft_proton.cmd b/scripts/daq_soft_proton.cmd new file mode 100644 index 0000000..4fea727 --- /dev/null +++ b/scripts/daq_soft_proton.cmd @@ -0,0 +1,8 @@ +#var softProtonDebug 1 + +$(SET_SIM_MODE=#)dbLoadRecords("$(sinqDAQ_DB)sim_proton_current.db", "INSTR=$(INSTR), NAME=$(NAME)") +$(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=MHC6:IST:2), RATE_EGU=$(RATE_EGU=cts/sec), COUNTER_EGU=$(COUNTER_EGU=cts), SCALE_FACTOR=$(SCALE_FACTOR=100)") + +iocInit diff --git a/src/daq.dbd b/src/daq.dbd index 3bfdf32..18639be 100644 --- a/src/daq.dbd +++ b/src/daq.dbd @@ -3,3 +3,5 @@ #--------------------------------------------- device(stringin, INST_IO, devDAQStringError, "devDAQStringError") +function(processEmulatedCounter) +variable("softProtonDebug", int) diff --git a/src/daq_soft_proton.c b/src/daq_soft_proton.c new file mode 100644 index 0000000..4cc5530 --- /dev/null +++ b/src/daq_soft_proton.c @@ -0,0 +1,297 @@ +/* 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 + +#include +#include +#include +#include +#include +#include +#include + +/* 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"; + /* 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"; + + /* 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 update the whole string */ + psub->nevg = psub->novg = 40; + + /* 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, "Paused!"); + /* 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);