diff --git a/Makefile b/Makefile index 19c6236..f2d3b36 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ TEMPLATES += db/counterbox_v2.db TEMPLATES += db/counterbox_v2_test.db TEMPLATES += db/counterbox.proto +# Just for simulation +TEMPLATES += db/counterbox_simcontrol.db + # DBD files to include in the release DBDS += src/counterbox.dbd diff --git a/db/channels.db b/db/channels.db index 5675e57..71c43de 100644 --- a/db/channels.db +++ b/db/channels.db @@ -11,25 +11,6 @@ ################################################################################ # Count Commands -# record(ao,"$(P):$(NAME):THRESHOLD_$(CHANNEL)") -# { -# field(DESC, "Minimum rate for counting to proceed") -# field(VAL, "0") # Rate -# field(DRVL, "0") # Minimum Rate -# field(OMSL, "supervisory") -# field(OROC, "0") -# field(OUT, "@$(PROTO) setMinRate($(P):$(NAME):, $(CHANNEL)) $(ASYN_PORT)") -# field(DTYP, "stream") -# } -# -# record(ai,"$(P):$(NAME):THRESHOLD_$(CHANNEL)_RBV") -# { -# field(DESC, "Minimum rate for counting to proceed") -# field(INP, "@$(PROTO) readMinRate($(P):$(NAME):, $(CHANNEL)) $(ASYN_PORT)") -# field(DTYP, "stream") -# field(SCAN, "1 second") -# } - record(bo, "$(P):$(NAME):C$(CHANNEL)") { field(DESC, "Clear the current channel count") diff --git a/db/counterbox.proto b/db/counterbox.proto index c51c5e5..29d78a8 100644 --- a/db/counterbox.proto +++ b/db/counterbox.proto @@ -46,18 +46,6 @@ writePresetMonitor { ################################################################################ # Count Commands -startWithCountPreset { - out "MP %d"; - in; - @mismatch{in "%(\$1MsgTxt)s";} -} - -startWithTimePreset { - out "TP %#.2f"; - in; - @mismatch{in "%(\$1MsgTxt)s";} -} - pauseCount { out "PS"; in; @@ -76,17 +64,65 @@ stopCount { @mismatch{in "%(\$1MsgTxt)s";} } -# setMinRate{ -# out "DL \$2 %.3f"; -# in; -# @mismatch{in "%(\$1MsgTxt)s";} -# } -# -# readMinRate{ -# out "DL \$2"; -# in "%f"; -# @mismatch{in "%(\$1MsgTxt)s";} -# } +clearTimer{ + # We first stop the count, as otherwise on the newest counterboxes + # it starts counting again if a time preset was set. + # Not a problem with the older boxes + stopCount; + out "CT"; + in; + @mismatch{in "%(\$1MsgTxt)s";} +} + +clearChannel{ + out "CC \$2"; + in; + @mismatch{in "%(\$1MsgTxt)s";} +} + +clearCounter4 { + out "CC 1"; + in; + out "CC 2"; + in; + out "CC 3"; + in; + out "CC 4"; + in; + @mismatch{in "%(\$1MsgTxt)s";} +} + +clearCounter8 { + out "CC 5"; + in; + out "CC 6"; + in; + out "CC 7"; + in; + out "CC 8"; + in; + @mismatch{in "%(\$1MsgTxt)s";} +} + +clearCounter10 { + out "CC 9"; + in; + out "CC 10"; + in; + @mismatch{in "%(\$1MsgTxt)s";} +} + +startWithCountPreset { + out "MP %d"; + in; + @mismatch{in "%(\$1MsgTxt)s";} +} + +startWithTimePreset { + out "TP %#.2f"; + in; + @mismatch{in "%(\$1MsgTxt)s";} +} setMinRate{ out "DL %(\$1THRESHOLD-MONITOR_RBV)d %.3f"; @@ -112,18 +148,6 @@ readRateMonitor{ @mismatch{in "%(\$1MsgTxt)s";} } -clearTimer{ - out "CT"; - in; - @mismatch{in "%(\$1MsgTxt)s";} -} - -clearChannel{ - out "CC \$2"; - in; - @mismatch{in "%(\$1MsgTxt)s";} -} - ################################################################################ # Read Values From Monitors @@ -141,7 +165,13 @@ readAll8 { readAll10 { out "RA"; - in "%(\$1ELAPSED-TIME)f %(\$1M1)d %(\$1M2)d %(\$1M3)d %(\$1M4)d %(\$1M5)d %(\$1M6)d %(\$1M7)d %(\$1M8)d %(\$1M9)d %(\$1M10)d"; + in "%(\$1ELAPSED-TIME)f %(\$1M1)d %(\$1M2)d %(\$1M3)d %(\$1M4)d %(\$1M5)d %(\$1M6)d %(\$1M7)d %(\$1M8)d"; + # At least on the sinqtest variant this is broken + # requiring channels 9 and 10 to be manually queried + out "RC 9"; + in "%(\$1M9)d"; + out "RC 10"; + in "%(\$1M10)d"; @mismatch{in "%(\$1MsgTxt)s";} } @@ -164,3 +194,76 @@ setTestSignal { out "TG 1 %(\$1TESTGEN-HIGHRATE)d %(\$1TESTGEN-LOWRATE)d"; @mismatch{in "%(\$1MsgTxt)s";} } + +################################################################################ +# Gating Settings + +getGateStatus { + out "GT \$2"; + in "%d %(\$1GATE-ON-\$2_RBV)d"; + @mismatch{in "%(\$1MsgTxt)s";} +} + +setGateStatus { + extrainput = ignore; + out "GT \$2 %(\$1GATE-\$2)d %(\$1GATE-ON-\$2)d"; + in "Gate \$2"; + @mismatch{in "%(\$1MsgTxt)s";} +} + +setGate { + out "GATE \$2 %d"; + in ""; + @mismatch{in "%(\$1MsgTxt)s";} +} + +################################################################################ +# 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; +} diff --git a/db/counterbox_common.db b/db/counterbox_common.db index 729ca1d..ec2e88b 100644 --- a/db/counterbox_common.db +++ b/db/counterbox_common.db @@ -105,19 +105,21 @@ record(ao,"$(P):$(NAME):PRESET-COUNT") { field(DESC, "Count until preset reached") field(DTYP, "stream") - field(OUT, "@$(PROTO) startWithCountPreset($(P):$(NAME):) $(ASYN_PORT)") + field(OUT, "@$(PROTO) startWithCountPreset$(CHANNELS)($(P):$(NAME):) $(ASYN_PORT)") field(VAL, 0) field(PREC, 2) + field(FLNK, "$(P):$(NAME):RAW-STATUS") } record(ao,"$(P):$(NAME):PRESET-TIME") { field(DESC, "Count for specified time") field(DTYP, "stream") - field(OUT, "@$(PROTO) startWithTimePreset($(P):$(NAME):) $(ASYN_PORT)") + field(OUT, "@$(PROTO) startWithTimePreset$(CHANNELS)($(P):$(NAME):) $(ASYN_PORT)") field(VAL, 0) field(PREC, 2) field(EGU, "seconds") + field(FLNK, "$(P):$(NAME):RAW-STATUS") } record(bo,"$(P):$(NAME):PAUSE") @@ -126,6 +128,7 @@ record(bo,"$(P):$(NAME):PAUSE") field(DTYP, "stream") field(OUT, "@$(PROTO) pauseCount($(P):$(NAME):) $(ASYN_PORT)") field(VAL, "0") + field(FLNK, "$(P):$(NAME):RAW-STATUS") } record(bo,"$(P):$(NAME):CONTINUE") @@ -134,6 +137,7 @@ record(bo,"$(P):$(NAME):CONTINUE") field(DTYP, "stream") field(OUT, "@$(PROTO) continueCount($(P):$(NAME):) $(ASYN_PORT)") field(VAL, "0") + field(FLNK, "$(P):$(NAME):RAW-STATUS") } record(bo, "$(P):$(NAME):STOP") @@ -141,6 +145,7 @@ record(bo, "$(P):$(NAME):STOP") field(DESC, "Stop the current counting operation") field(DTYP, "stream") field(OUT, "@$(PROTO) stopCount($(P):$(NAME):) $(ASYN_PORT)") + field(FLNK, "$(P):$(NAME):RAW-STATUS") } record(ao,"$(P):$(NAME):THRESHOLD") @@ -167,7 +172,7 @@ 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, "0") # Smallest Threshold Channel (0 is off) field(DRVH, "$(CHANNELS)") # Largest Threshold Channel field(OUT, "@$(PROTO) setRateMonitor($(P):$(NAME):) $(ASYN_PORT)") field(DTYP, "stream") diff --git a/db/counterbox_simcontrol.db b/db/counterbox_simcontrol.db new file mode 100644 index 0000000..c1c2a54 --- /dev/null +++ b/db/counterbox_simcontrol.db @@ -0,0 +1,26 @@ +# Sinq Counterbox EPICS Database for StreamDevice Communication with Simulation +# Macros +# P - Prefix +# NAME - just a name, e.g. EL737 +# PROTO - Stream device protocol file +# ASYN_PORT - Low level Asyn IP Port to EL737 + +################################################################################ + +record(bo, "$(P):$(NAME):G1") +{ + field(DESC, "Set Gate 1 Low/High") + field(ZNAM, "Low") + field(ONAM, "High") + field(DTYP, "stream") + field(OUT, "@$(PROTO) setGate($(P):$(NAME):, 1) $(ASYN_PORT)") +} + +record(bo, "$(P):$(NAME):G2") +{ + field(DESC, "Set Gate 2 Low/High") + field(ZNAM, "Low") + field(ONAM, "High") + field(DTYP, "stream") + field(OUT, "@$(PROTO) setGate($(P):$(NAME):, 2) $(ASYN_PORT)") +} diff --git a/db/counterbox_v2.db b/db/counterbox_v2.db index d69b075..1b210e3 100644 --- a/db/counterbox_v2.db +++ b/db/counterbox_v2.db @@ -31,3 +31,76 @@ record(longin, "$(P):$(NAME):MONITOR-CHANNEL_RBV") ################################################################################ # Read all monitors values + +################################################################################ +# Gating Settings + +record(bo, "$(P):$(NAME):GATE-1") +{ + field(DESC, "First Gating Channel") + field(ZNAM, "Disabled") + field(ONAM, "Enabled") + field(DTYP, "stream") + field(OUT, "@$(PROTO) setGateStatus($(P):$(NAME):, 1) $(ASYN_PORT)") +} + +record(bo, "$(P):$(NAME):GATE-ON-1") +{ + field(DESC, "Count when first Gate high/low") + field(ZNAM, "Low") + field(ONAM, "High") + field(DTYP, "stream") + field(OUT, "@$(PROTO) setGateStatus($(P):$(NAME):, 1) $(ASYN_PORT)") +} + +record(bi, "$(P):$(NAME):GATE-1_RBV") +{ + field(DESC, "First Gating Channel") + field(ZNAM, "Disabled") + field(ONAM, "Enabled") + field(DTYP, "stream") + field(INP, "@$(PROTO) getGateStatus($(P):$(NAME):, 1) $(ASYN_PORT)") + field(SCAN, "2 second") +} + +record(bi, "$(P):$(NAME):GATE-ON-1_RBV") +{ + field(DESC, "Count when first Gate high/low") + field(ZNAM, "Low") + field(ONAM, "High") +} + +record(bo, "$(P):$(NAME):GATE-2") +{ + field(DESC, "First Gating Channel") + field(ZNAM, "Disabled") + field(ONAM, "Enabled") + field(DTYP, "stream") + field(OUT, "@$(PROTO) setGateStatus($(P):$(NAME):, 2) $(ASYN_PORT)") +} + +record(bo, "$(P):$(NAME):GATE-ON-2") +{ + field(DESC, "Count when first Gate high/low") + field(ZNAM, "Low") + field(ONAM, "High") + field(DTYP, "stream") + field(OUT, "@$(PROTO) setGateStatus($(P):$(NAME):, 2) $(ASYN_PORT)") +} + +record(bi, "$(P):$(NAME):GATE-2_RBV") +{ + field(DESC, "Second Gating Channel") + field(ZNAM, "Disabled") + field(ONAM, "Enabled") + field(DTYP, "stream") + field(INP, "@$(PROTO) getGateStatus($(P):$(NAME):, 2) $(ASYN_PORT)") + field(SCAN, "2 second") +} + +record(bi, "$(P):$(NAME):GATE-ON-2_RBV") +{ + field(DESC, "Count when second Gate high/low") + field(ZNAM, "Low") + field(ONAM, "High") +} diff --git a/scripts/counterbox_8ch.cmd b/scripts/counterbox_8ch.cmd index 5202377..3576da3 100644 --- a/scripts/counterbox_8ch.cmd +++ b/scripts/counterbox_8ch.cmd @@ -5,7 +5,7 @@ epicsEnvSet("$(NAME)_CNTBOX_HOST", "$(CNTBOX_IP):$(CNTBOX_PORT)") $(SET_SIM_MODE=#) $(SET_SIM_MODE) require misc $(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 &" +$(SET_SIM_MODE=#) $(SET_SIM_MODE) system "$(counterbox_DIR)counterbox_sim.py $(CNTBOX_PORT) 8 &" # starting the python socket seems to take a while # and need misc to use built in sleep command $(SET_SIM_MODE=#) $(SET_SIM_MODE) sleep 3 diff --git a/scripts/counterbox_v2.cmd b/scripts/counterbox_v2.cmd index 822d0bb..90d27d0 100644 --- a/scripts/counterbox_v2.cmd +++ b/scripts/counterbox_v2.cmd @@ -28,3 +28,5 @@ dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$ dbLoadRecords("$(counterbox_DB)channels.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=ASYN_$(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)") + +$(SET_SIM_MODE=#) $(SET_SIM_MODE) dbLoadRecords("$(counterbox_DB)counterbox_simcontrol.db", "P=$(PREFIX), NAME=$(NAME), PROTO=$(PROTO), ASYN_PORT=ASYN_$(NAME)") diff --git a/sim/counterbox_sim.py b/sim/counterbox_sim.py index d32800e..578925a 100644 --- a/sim/counterbox_sim.py +++ b/sim/counterbox_sim.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import os import re import socket import sys @@ -10,6 +11,13 @@ 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 +LOG2FILE = False if len(sys.argv) < 4 else bool(int(sys.argv[3])) + +import logging +logger = logging.getLogger('counterbox') + +if LOG2FILE: + logging.basicConfig(filename=os.path.join(os.getcwd(), 'counterbox_sim.log'), level=logging.INFO) class CounterBox: @@ -26,43 +34,106 @@ class CounterBox: self.elapsed = 0 self.monitor = 0 - def resetCounts(self): + self.minratechannel = 0 + self.minrates = [2] * self.total_channels + + self.gate_config = [ + #(enabled, count high/low), + ( False, True), + ( False, True), + ] + + self.gate = [ + # high/low + False, + False, + ] + + def clearCount(self, counter): + self.counts[counter-1] = 0 + + def clearCounts(self): self.counts = [0] * self.total_channels - self.starttime = time.time() + + def clearTime(self): self.elapsed = 0 + self.starttime = time.time() def getStatus(self): return self.status + def getCount(self, channel): + return self.counts[channel - 1] + def getCounts(self): - return self.counts + # The sinqtest box returns a maximum of 8 + return self.counts[0:min(len(self.counts), 8)] def getMonitorCount(self): return self.counts[self.monitor] - def updateCounts(self): + def updateRates(self): for i in range(self.total_channels): self.rates[i] = randrange(5) + + def updateCounts(self): + for i in range(self.total_channels): self.counts[i] += self.rates[i] def getRunTime(self): elapsed = round(time.time() - self.starttime, 3) + self.updateRates() + + # If gating and a low rate threshold are enabled, then the threshold + # seems to have precedence and sets the status to 5. + # If we are waiting on the gate, then the status is just 1 as normal + # after having started a count. if self.countmode == 'time': if elapsed < self.presettime: + + if self.minratechannel >= 0 and self.rates[self.minratechannel] < self.minrates[self.minratechannel]: + # adjust the starttime, so that it is as if this polling period didn't happen + self.starttime += elapsed - self.elapsed + self.status = 5 + return self.elapsed + + self.status = 1 + + for i in range(len(self.gate)): + # If the gating is enabled and the signal is in the active position + if self.gate_config[i][0] and self.gate_config[i][1] != self.gate[i]: + self.starttime += elapsed - self.elapsed + return self.elapsed + self.updateCounts() self.elapsed = elapsed else: - self.elapsed = self.presettime + self.elapsed = self.presettime if self.presettime > 0 else self.elapsed self.status = 0 elif self.countmode == 'count': if self.getMonitorCount() < self.presetcount: + + if self.minratechannel >= 0 and self.rates[self.minratechannel] < self.minrates[self.minratechannel]: + # adjust the starttime, so that it is as if this polling period didn't happen + self.starttime += elapsed - self.elapsed + self.status = 5 + return self.elapsed + + self.status = 1 + + for i in range(len(self.gate)): + # If the gating is enabled and the signal is in the active position + if self.gate_config[i][0] and self.gate_config[i][1] != self.gate[i]: + self.starttime += elapsed - self.elapsed + return self.elapsed + self.updateCounts() self.elapsed = elapsed if self.getMonitorCount() >= self.presetcount: - self.counts[self.monitor] = self.presetcount + self.counts[self.monitor] = self.presetcount if self.presetcount > 0 else self.counts[self.monitor] self.status = 0 else: @@ -73,18 +144,22 @@ class CounterBox: def stop(self): self.getRunTime() self.status = 0 + self.presettime = 0 + self.presetcount = 0 def startTimePreset(self, presettime): self.countmode = 'time' self.status = 1 self.presettime = round(presettime, 3) - self.resetCounts() + self.clearTime() + self.clearCounts() def startCountPreset(self, presetcount): self.countmode = 'count' self.status = 1 self.presetcount = presetcount - self.resetCounts() + self.clearTime() + self.clearCounts() def setMonitorChannel(self, channel): self.monitor = channel - 1 @@ -94,6 +169,28 @@ class CounterBox: def getRate(self, channel): return float(self.rates[channel - 1]) + + def getMinRateChannel(self): + return self.minratechannel + 1 + + def setMinRateChannel(self, channel): + self.minratechannel = channel - 1 + + def getMinRate(self, channel): + return self.minrates[channel - 1] + + def setMinRate(self, channel, rate): + self.minrates[channel - 1] = rate + + def getGateStatus(self, channel): + return self.gate_config[channel - 1] + + def setGateStatus(self, channel, enable, highlow): + self.gate_config[channel - 1] = (enable, highlow) + + def setGate(self, channel, highlow): + self.gate[channel - 1] = highlow + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: @@ -104,17 +201,21 @@ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: def send(data: str): if data: + logger.info(f'SENDING: "{data}"') return conn.sendall(f'{data}\r'.encode()) else: + logger.info(f'SENDING: ""') return conn.sendall(b'\r') def receive(): data = conn.recv(1024) if data: # also removes terminator - return data.decode('ascii').rstrip() + received = data.decode('ascii').rstrip() else: - return '' + received = '' + logger.info(f'RECEIVED: "{received}"') + return received counterbox = CounterBox(TOTAL_CH) @@ -141,11 +242,26 @@ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: )) ) + elif re.fullmatch(r'RC (\d+)', data): + channel = int(re.fullmatch(r'RC (\d+)', data).group(1)) + count = counterbox.getCount(channel) + send(f'{count}') + elif data == 'RS': send(str(counterbox.getStatus())) elif data == 'S': counterbox.stop() + send('') + + elif data == 'CT': + counterbox.clearTime() + send('') + + elif re.fullmatch(r'CC (\d+)', data): + counter = int(re.fullmatch(r'CC (\d+)', data).group(1)) + counterbox.clearCount(counter) + send('') elif re.fullmatch(r'TP (\d+(\.\d+)?)', data): presettime = float(re.fullmatch(r'TP (\d+(\.\d+)?)', data).group(1)) @@ -163,13 +279,53 @@ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: elif re.fullmatch(r'PC (\d+)', data): channel = int(re.fullmatch(r'PC (\d+)', data).group(1)) counterbox.setMonitorChannel(channel) + send('') + + elif data == 'DR': + send(str(counterbox.getMinRateChannel())) + + elif re.fullmatch(r'DR (\d+)', data): + channel = int(re.fullmatch(r'DR (\d+)', data).group(1)) + counterbox.setMinRateChannel(channel) + send('') + + elif re.fullmatch(r'DL (\d+)', data): + channel = int(re.fullmatch(r'DL (\d+)', data).group(1)) + send('{:.3f}'.format(counterbox.getMinRate(channel))) + + elif re.fullmatch(r'DL (\d+) (\d+(?:.\d+)?)', data): + channel, rate = re.fullmatch(r'DL (\d+) (\d+(?:.\d+)?)', data).groups() + counterbox.setMinRate(int(channel), float(rate)) + send('') elif re.fullmatch(r'RR (\d+)', data): channel = int(re.fullmatch(r'RR (\d+)', data).group(1)) send(counterbox.getRate(channel)) + elif re.fullmatch(r'GT (\d+)', data): + channel = int(re.fullmatch(r'GT (\d+)', data).group(1)) + enabled, highlow = counterbox.getGateStatus(channel) + send(f'{int(enabled)} {int(highlow)}') + + elif re.fullmatch(r'GT (\d+) (\d+) (\d+)', data): + channel, enable, highlow = re.fullmatch(r'GT (\d+) (\d+) (\d+)', data).groups() + channel, enable, highlow = int(channel), bool(int(enable)), bool(int(highlow)) + counterbox.setGateStatus(channel, enable, highlow) + if enable: + send(f'Gate {channel} enabled, counting when input = {"high" if highlow else "low"}') + else: + send(f'Gate {channel} disabled') + + # Only for the simulation + elif re.fullmatch(r'GATE (\d+) (\d+)', data): + channel, highlow = re.fullmatch(r'GATE (\d+) (\d+)', data).groups() + channel, highlow = int(channel), bool(int(highlow)) + counterbox.setGate(channel, highlow) + send('') + else: send('?2') # Bad command - except: + except Exception as e: + logger.exception('Simulation Broke') send('?OV')