diff --git a/alarms-dicts.txt b/alarms-dicts.txt new file mode 100644 index 0000000..ba9893b --- /dev/null +++ b/alarms-dicts.txt @@ -0,0 +1,31 @@ +status = { + 0: "NO ALARM", + 1: "READ", + 2: "WRITE", + 3: "HIHI", + 4: "HIGH", + 5: "LOLO", + 6: "LOW", + 7: "STATE", + 8: "COS", + 9: "COMM", + 10: "TIMEOUT", + 11: "HW LIMIT", + 12: "CALC", + 13: "SCAN", + 14: "LINK", + 15: "SOFT", + 16: "BAD SUB", + 17: "UDF", + 18: "DISABLE", + 19: "SIMM", + 20: "READ ACCESS", + 21: "WRITE ACCESS" +} + +severity = { + 0: "NO ALARM", + 1: "MINOR", + 2: "MAJOR", + 3: "INVALID" +} diff --git a/alarms.py b/alarms.py new file mode 100644 index 0000000..d46eca8 --- /dev/null +++ b/alarms.py @@ -0,0 +1,44 @@ + +STATUS = { + 0: "no alarm", + 1: "read", + 2: "write", + 3: "hihi", + 4: "high", + 5: "lolo", + 6: "low", + 7: "state", + 8: "cos", + 9: "comm", + 10: "timeout", + 11: "hw limit", + 12: "calc", + 13: "scan", + 14: "link", + 15: "soft", + 16: "bad sub", + 17: "udf", + 18: "disable", + 19: "simm", + 20: "read access", + 21: "write access" +} + +SEVERITY = { + 0: "no alarm", + 1: "minor", + 2: "major", + 3: "invalid" +} + + + +def message(status_code, severity_code): + status = STATUS.get(status_code, "unknown") + if status_code == severity_code == 0: + return status + severity = SEVERITY.get(severity_code, "unknown") + return f"{status} ({severity})" + + + diff --git a/alarms.txt b/alarms.txt new file mode 100644 index 0000000..5673216 --- /dev/null +++ b/alarms.txt @@ -0,0 +1,34 @@ +/usr/local/epics/base/include/alarm.h + +Severity ist "wie schlimm ist es?" +0 = NO_ALARM +1 = MINOR +2 = MAJOR +3 = INVALID (das heisst dem Wert darf nicht geglaubt werden, besonders nicht für Feedbacks.) + +Status ist "Was für ein Alarm ist das?" +0 = NO_ALARM +1 = READ (Lesefehler, oft mit INVALID Severity) +2 = WRITE (Schreibfehler, oft mit INVALID Severity) +3 = HIHI (Wert ist sehr hoch, oft mit MAJOR Severity) +4 = HIGH (Wert ist hoch, oft mit MINOR Severity) +5 = LOLO (Wert ist sehr tief, oft mit MAJOR Severity) +6 = LOW (Wert ist tief, oft mit MINOR Severity) +7 = STATE (Wert ist ein Alarmzustand (z.B. OFF)) +8 = COS (=Change of State, Zustand hat sich geändert = Signalflanke) +9 = COMM (Communikationsfehler, oft mit INVALID Severity) +10 = TIMEOUT +11 = HW_LIMIT +12 = CALC (Calculation gibt NaN oder Inf oder so) +13 = SCAN (Record konnte 5 mal nicht gescannt werden weil busy) +14 = LINK (Record hat Alarm geerbt, weil der Link ein MS hat, oder Link ist ungültig) +15 = SOFT +16 = BAD_SUB (Subroutine Record hat keine Subroutine) +17 = UDF (Undefined, Record hat keinen Wert, typischerweise noch nie prozessiert seit dem Start, normalerweise mit Severity INVALID) +18 = DISABLE (Record wurde disabled) +19 = SIMM (Record is in Simulationsmodus) +20 = READ_ACCESS +21 = WRITE_ACCESS + + + diff --git a/chans.txt b/chans.txt new file mode 100644 index 0000000..10f0446 --- /dev/null +++ b/chans.txt @@ -0,0 +1,18 @@ +SIN-TIMAST-TMA:Bunch-2-Appl-Freq-RB # rep rate + +# vacuum gauges +SATOP21-VMFR165-A010:PRESSURE + SATOP21-VMCC165-A010:PRESSURE + + +# slits +SATOP21-OAPU161:MOTOR_W.RBV + + +CRAP:CRAP0 +CRAP:CRAP1 +CRAP:CRAP2 +CRAP:CRAP3 + +SAROP21-OKB:MODE +SATOP11-VVPG092-A010:PLC_OPEN diff --git a/config.py b/config.py new file mode 100644 index 0000000..4910071 --- /dev/null +++ b/config.py @@ -0,0 +1,19 @@ + +def load(fname): + chans = set() + for line in read_lines(fname): + line = remove_comments(line).strip() + if not line: + continue + chans.add(line) + return sorted(chans) + +def read_lines(fname): + with open(fname, "r") as f: + yield from f + +def remove_comments(line, comment_char="#"): + return line.split(comment_char)[0] + + + diff --git a/execute.py b/execute.py new file mode 100644 index 0000000..b0df236 --- /dev/null +++ b/execute.py @@ -0,0 +1,16 @@ +from concurrent.futures import ThreadPoolExecutor + + +def parallel(func, targets, names): + with ThreadPoolExecutor() as executor: + results = executor.map(func, targets) +# return list(results) + return dict(zip(names, results)) + + +def serial(func, targets): + return [func(t) for t in targets] +# return {t: func(t) for t in targets} + + + diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..b23e20f --- /dev/null +++ b/logger.py @@ -0,0 +1,26 @@ +import logging as log + + +LEVELS = ( +# log.CRITICAL, + log.ERROR, + log.WARNING, + log.INFO, + log.DEBUG, + log.NOTSET +) + + +def set_log_level(verbosity): + ntotal = len(LEVELS) - 1 + index = min(verbosity, ntotal) + level = LEVELS[index] + log.basicConfig(level=level, format="%(asctime)s %(levelname)s %(message)s") + + + + + +#from logger import log, set_log_level + +#set_log_level(clargs.verbose) diff --git a/pvcollection.py b/pvcollection.py new file mode 100644 index 0000000..743fbb3 --- /dev/null +++ b/pvcollection.py @@ -0,0 +1,57 @@ +import epics + +from config import load +from execute import parallel +#from execute import serial as parallel + + +class PVCollection: + + def __init__(self, chans): + self.chans = chans + self.pvs = [PV(ch) for ch in chans] + + @classmethod + def from_file(cls, fname): + chans = load(fname) + return cls(chans) + + def connected(self): + return self._run_all(lambda pv: pv.wait_for_connection(0.01)) + + def status(self): + return self._run_all(lambda pv: pv.status) + + def severity(self): + return self._run_all(lambda pv: pv.severity) + + def value(self): + return self._run_all(lambda pv: pv.value) + + def _run_all(self, func): + return parallel(func, self.pvs) + + + +class PV(epics.PV): + + @property + def status(self): + if not self.connected: + return -1 + return super().status + + @property + def severity(self): + if not self.connected: + return -1 + return super().severity + + @property + def value(self): + if not self.connected: + return None + return super().value + + + diff --git a/sani.py b/sani.py new file mode 100755 index 0000000..1c23904 --- /dev/null +++ b/sani.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python + + +import argparse + +parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + +parser.add_argument("filename") + +#parser.add_argument("-v", "--verbose", action="count", default=0) +parser.add_argument("-s", "--show", action="store_true") +parser.add_argument("-t", "--timeout", type=float, default=2) + +clargs = parser.parse_args() + + + + + + + + +import enum + +import epics +import pandas as pd + +from pvcollection import PVCollection +from alarms import message +from config import load +from execute import parallel + + + + + + + + + + + + +MSG_NOT_CONNECTED = "did not connect" +MSG_SUCCESS = "OK" + + + +def v1a(): + cs = load(clargs.filename) + pvs = [epics.PV(c) for c in cs] + for pv in pvs: + c = pv.pvname + connected = pv.wait_for_connection(clargs.timeout) + if not connected: + msg = MSG_NOT_CONNECTED + elif pv.status != 0 or pv.severity != 0: + msg = message(pv.status, pv.severity) + else: + if not clargs.show: + continue + msg = MSG_SUCCESS +# print(c, msg) + + +def v1b(): + chans = load(clargs.filename) + pvs = [epics.PV(ch) for ch in chans] + connected = parallel(lambda pv: pv.wait_for_connection(clargs.timeout), pvs) + + for ch, pv, con in zip(chans, pvs, connected): + if not con: + msg = MSG_NOT_CONNECTED + elif pv.status != 0 or pv.severity != 0: + msg = message(pv.status, pv.severity) + else: + if not clargs.show: + continue + msg = MSG_SUCCESS +# print(ch, msg) + + + + + +def v1c(): + chans = load(clargs.filename) + length = maxstrlen(chans) + pvs = [epics.PV(ch) for ch in chans] + connected = parallel(lambda pv: pv.wait_for_connection(clargs.timeout), pvs) + + data = {} + for ch, pv, con in zip(chans, pvs, connected): + if not con: + status = severity = -1 + value = None + msg = MSG_NOT_CONNECTED + else: + status = pv.status + severity = pv.severity + if status == 0 and severity == 0: + if not clargs.show: + continue + msg = MSG_SUCCESS + else: + msg = message(status, severity) + value = pv.value + +# print(ch.ljust(length), msg) + data[ch] = (con, status, severity, value) + + columns = ("connected", "status", "severity", "value") + df = pd.DataFrame.from_dict(data, columns=columns, orient="index") +# print() +# print(df) +# print() +# print(df.dtypes) + + + +def maxstrlen(seq): + return max(strlen(i) for i in seq) + +def strlen(val): + return len(str(val)) + + + +#cs = load(clargs.filename) +#pvs = [epics.PV(c) for c in cs] +#data = [] +#for pv in pvs: +# c = pv.pvname +# connected = pv.wait_for_connection(clargs.timeout) + +# if not connected: +# print(c, "didn't connect") +# data.append((connected, -1, -1, None)) +# continue +# if pv.status != 0 or pv.severity != 0: +# print(c, message(pv.status, pv.severity)) + +# data.append((connected, pv.status, pv.severity, pv.value)) + +#df = pd.DataFrame(data, index=cs, columns=["connected", "status", "severity", "value"]) + +#print(df) +#print(df.dtypes) + + + + + + + +def v2a(): + pvc = PVCollection.from_file(clargs.filename) + + data = { + "connected": pvc.connected(), + "status": pvc.status(), + "severity": pvc.severity(), + "value": pvc.value() + } + + df = pd.DataFrame(data, index=pvc.chans) + +# print(df) +# print(df.dtypes) + + + + + + + + +def get_data(pv): + connected = pv.wait_for_connection(clargs.timeout) + + if not connected: + value = None + status = severity = -1 + msg = MSG_NOT_CONNECTED + else: + value = pv.value #TODO: not needed if shot not ok + status = pv.status + severity = pv.severity + if status == 0 and severity == 0: + msg = MSG_SUCCESS + else: + msg = message(status, severity) + + data = { + "connected": connected, + "value": value, + "status": status, + "severity": severity + } + + if clargs.show: + print(pv.pvname, msg) + return data#, msg + + + + + +def v3(): + chans = load(clargs.filename) + pvs = (epics.PV(ch) for ch in chans) # putting PV constructors into threads has weird effects + data = parallel(get_data, pvs, chans) + df = pd.DataFrame(data).T + return df + + + + + + + + +res = v3() +print(res) +#for m in res: +# print(m) + + + + + + + + + + + + + + +