diff --git a/tomcat_bec/devices/gigafrost/gigafrostconstants.py b/tomcat_bec/devices/gigafrost/gfconstants.py similarity index 76% rename from tomcat_bec/devices/gigafrost/gigafrostconstants.py rename to tomcat_bec/devices/gigafrost/gfconstants.py index 64be4ee..fbf9173 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostconstants.py +++ b/tomcat_bec/devices/gigafrost/gfconstants.py @@ -10,6 +10,7 @@ class GfStatus(Enum): ACQUIRING = 3 CONFIGURED = 4 STOPPED = 5 + INIT = 6 # BACKEND ADDRESSES BE1_DAFL_CLIENT = 'http://xbl-daq-33:8080' @@ -28,12 +29,23 @@ BE3_SOUTH_MAC = [0x50, 0x65, 0xf3, 0x81, 0xd5, 0x31] # ens4 BE3_NORTH_IP = [10, 4, 0, 101] BE3_SOUTH_IP = [10, 0, 0, 101] + + +BE999_DAFL_CLIENT = "http://xbl-daq-28:8080" +BE999_SOUTH_MAC = [0x9C, 0xDC, 0x71, 0x47, 0xE5, 0xD1] # 9c:dc:71:47:e5:d1 +BE999_NORTH_MAC = [0x9C, 0xDC, 0x71, 0x47, 0xE5, 0xDD] # 9c:dc:71:47:e5:dd +BE999_NORTH_IP = [10, 4, 0, 101] +BE999_SOUTH_IP = [10, 0, 0, 101] + + + + + + + + # GF Names GF1 = 'gf1' GF2 = 'gf2' GF3 = 'gf3' -# CAMERA -ERROR_BACKEND_NAME = 'Backend not recognized.' - - diff --git a/tomcat_bec/devices/gigafrost/gfutils.py b/tomcat_bec/devices/gigafrost/gfutils.py new file mode 100644 index 0000000..ab45263 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/gfutils.py @@ -0,0 +1,244 @@ +import inspect +import os +import sys +import jsonschema + +_min_exposure_ms = 0.002 +_max_exposure_ms = 40 + +_min_roix = 48 +_max_roix = 2016 +_step_roix = 48 + +_min_roiy = 4 +_max_roiy = 2016 +_step_roiy = 4 + +valid_roix = range(_min_roix, _max_roix + 1, _step_roix) +valid_roiy = range(_min_roiy, _max_roiy + 1, _step_roiy) + + +class NoTraceBackWithLineNumber(Exception): + def __init__(self, msg): + if type(msg).__name__ in ["ConnectionError", "ReadTimeout"]: + print("\n ConnectionError/ReadTimeout: it seems that the server " + "is not running/responding.\n") + try: + ln = sys.exc_info()[-1].tb_lineno + except AttributeError: + ln = inspect.currentframe().f_back.f_lineno + self.args = "{0.__name__} (line {1}): {2}".format(type(self), ln, msg), + sys.tracebacklimit = None + return None + + +class GfError(NoTraceBackWithLineNumber): + pass + + +class GfWarning(NoTraceBackWithLineNumber): + pass + + +class GfNotAValidConfig(NoTraceBackWithLineNumber): + pass + + +class GfCamNotFound(NoTraceBackWithLineNumber): + pass + + +def is_valid_url(url): + return(url.startswith('http://')) # FIXME: do more checks? + + +def is_valid_exposure_ms(e): + """check if an exposure time e is valid for gigafrost + + e: exposure time in milliseconds + """ + return e >= _min_exposure_ms and e <= _max_exposure_ms + + +def port2byte(port): + return [(port >> 8) & 0xff, port & 0xff] + +def extend_header_table(table, mac, destination_ip, destination_port, + source_port): + """ + Extend the header table by a further entry. + + Parameters + ---------- + table : + The table to be extended + mac : + The mac address for the new entry + destination_ip : + The destination IP address for the new entry + destination_port : + The destination port for the new entry + source_port : + The source port for the new entry + + """ + table.extend(mac) + table.extend(destination_ip) + table.extend(port2byte(destination_port)) + table.extend(port2byte(source_port)) + return table + + +def is_valid_roi(roiy, roix): + return roiy in valid_roiy and roix in valid_roix + + +def _print_max_framerate(exposure, roix, roiy): + print("roiy=%4i roix=%4i exposure=%6.3fms: %8.1fHz" % + (roiy, roix, exposure, + max_framerate_Hz(exposure, roix=roix, roiy=roiy))) + + +def print_max_framerate(exposure_ms=_min_exposure_ms, shape='square'): + + valid_shapes = ['square', 'landscape', 'portrait'] + + if shape not in valid_shapes: + raise ValueError("shape must be one of %s" % valid_shapes) + if shape == 'square': + for r in valid_roix: + _print_max_framerate(exposure_ms, r, r) + + if shape == 'portrait': + for x in valid_roix: + _print_max_framerate(exposure_ms, roix=x, roiy=_max_roiy) + + if shape == 'landscape': + for y in valid_roix: # valid_roix is a subset of valid_roiy. Use the smaller set to get a more manageable amount of output lines + _print_max_framerate(exposure_ms, roix=_max_roix, roiy=y) + + +def max_framerate_Hz(exposure_ms=_min_exposure_ms, + roix=_max_roix, roiy=_max_roiy, clk_mhz=62.5): + """ + returns maximum achievable frame rate in auto mode in Hz + + Gerd Theidel wrote: + Hallo zusammen, + + hier wie besprochen die Info zu den Zeiten. + Im Anhang findet ihr ein Python Beispiel zur + Berechnung der maximalen Framerate im auto trigger mode. + Bei den anderen modes sollte man etwas darunter bleiben, + wie auch im PCO Manual beschrieben. + + Zusammengefasst mit den aktuellen Konstanten: + + exposure_ms : 0.002 ... 40 (ms) + clk_mhz : 62.5 | 55.0 | 52.5 | 50.0 (MHz) + + t_readout = ( ((roi_x / 24) + 14) * (roi_y / 4) + 405) / (1e6 * clk_mhz) + + t_exp_sys = (exposure_ms / 1000.0) + (261 / (1e6 * clk_mhz)) + + framerate = 1.0 / max(t_readout, t_exp_sys) + + Gruss, + Gerd + + """ + if exposure_ms < 0.002 or exposure_ms > 40: + raise ValueError('exposure_ms not in interval [0.002, 40.]') + + valid_clock_values = [62.5, 55.0, 52.5, 50.0] + if clk_mhz not in valid_clock_values: + raise ValueError('clock rate not in %s' % valid_clock_values) + + # Constants + PLB_IMG_SENS_COUNT_X_OFFS = 2 + PLB_IMG_SENS_COUNT_Y_OFFS = 1 + + # Constant Clock Cycles + CC_T2 = 88 # 99 + CC_T3 = 125 + CC_T4 = 1 + CC_T10 = 2 + CC_T11 = 4 + CC_T5_MINUS_T11 = 20 + CC_T15_MINUS_T10 = 3 + CC_TR3 = 1 + CC_T13_MINUS_TR3 = 2 + CC_150NS = 7 # additional delay through states + CC_DELAY_BEFORE_RESET = 4 # at least 50 ns + CC_ADDITIONAL_TSYS = 10 + CC_PIX_RESET_LENGTH = 40 # at least 40 CLK Cycles at 62.5 MHz + CC_COUNT_X_MAX = 84 + + CC_ROW_OVERHEAD_TIME = (11 + CC_TR3 + CC_T13_MINUS_TR3) + CC_FRAME_OVERHEAD_TIME = ( + 8 + CC_T15_MINUS_T10 + CC_T10 + CC_T2 + CC_T3 + CC_T4 + CC_T5_MINUS_T11 + CC_T11) + CC_INTEGRATION_START_DELAY = CC_COUNT_X_MAX + CC_ROW_OVERHEAD_TIME + \ + CC_DELAY_BEFORE_RESET + CC_PIX_RESET_LENGTH + CC_150NS + 5 + CC_TIME_TSYS = CC_FRAME_OVERHEAD_TIME + CC_ADDITIONAL_TSYS + + # 12 pixel blocks per quarter + roix = max(((roix + 47) / 48) * 48, PLB_IMG_SENS_COUNT_X_OFFS * 24) + # 2 line blocks per quarter + roiy = max(((roiy + 3) / 4) * 4, PLB_IMG_SENS_COUNT_Y_OFFS * 4) + + # print("CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME",CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME) + # print("CC_ROW_OVERHEAD_TIME",CC_ROW_OVERHEAD_TIME) + # print("CC_TIME_TSYS",CC_TIME_TSYS) + + t_readout = (CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME + + ((roix / 24) + CC_ROW_OVERHEAD_TIME) * (roiy / 4)) / (1e6 * clk_mhz) + t_exp_sys = (exposure_ms / 1000.0) + (CC_TIME_TSYS / (1e6 * clk_mhz)) + + # with constants as of time of writing: + # t_readout = ( ((roix / 24) + 14) * (roiy / 4) + 405) / (1e6 * clk_mhz) + # t_exp_sys = (exposure_ms / 1000.0) + (261 / (1e6 * clk_mhz)) + + framerate = 1.0 / max(t_readout, t_exp_sys) + + return (framerate) + +layoutSchema = { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "patternProperties": + { + "^[a-zA-Z0-9_.-]*$": + { + "type": "object", + "required":["writer","DaflClient", "zmq_stream", "live_preview", "ioc_name", "description"], + "properties":{ + "writer": { + "type": "string" + }, + "DaflClient": { + "type": "string" + }, + "zmq_stream": { + "type": "string" + }, + "live_preview": { + "type": "string" + }, + "ioc_name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "additionalProperties": False +} + +def validateJson(jsonData): + try: + jsonschema.validate(instance=jsonData, schema=layoutSchema) + except jsonschema.exceptions.ValidationError: + return False + return True diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 4b78258..b807873 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -8,7 +8,8 @@ import time from typing import Union from collections import OrderedDict - +import gfconstants as const +from gfutils import extend_header_table, port2byte class GigaFrostClient(Device): """ @@ -18,7 +19,10 @@ class GigaFrostClient(Device): This means that the camera behaves differently than the SF cameras and it has a lot of Tomcat specific additions. """ - cfgConnectionParam = Component(EpicsSignalRO, "CONN_PARM", string=True, auto_monitor=True) + infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) + infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) + cmdSyncHw = Component(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True) + cfgConnectionParam = Component(EpicsSignal, "CONN_PARM", string=True, put_complete=True) cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True) cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True) @@ -43,22 +47,22 @@ class GigaFrostClient(Device): cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) # Trigger configuration PVs - cfgCntStartBit = Component(EpicsSignal, "CNT_STARTBIT", read_pv="CNT_STARTBIT_RBV", put_complete=True) - cfgCntEndBit = Component(EpicsSignal, "CNT_ENDBIT", read_pv="CNT_ENDBIT_RBV", put_complete=True) + cfgCntStartBit = Component(EpicsSignal, "CNT_STARTBIT_RBV", write_pv="CNT_STARTBIT", put_complete=True) + cfgCntEndBit = Component(EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True) # Enable modes - cfgTrigEnableExt = Component(EpicsSignal, "MODE_ENBL_EXT", read_pv="MODE_ENBL_EXT_RBV", put_complete=True) - cfgTrigEnableSoft = Component(EpicsSignal, "MODE_ENBL_SOFT", read_pv="MODE_ENBL_SOFT_RBV", put_complete=True) - cfgTrigEnableAuto = Component(EpicsSignal, "MODE_ENBL_AUTO", read_pv="MODE_ENBL_AUTO_RBV", put_complete=True) - cfgTrigVirtEnable = Component(EpicsSignal, "MODE_ENBL_EXP", read_pv="MODE_ENBL_EXP_RBV", put_complete=True) + cfgTrigEnableExt = Component(EpicsSignal, "MODE_ENBL_EXT_RBV", write_pv="MODE_ENBL_EXT", put_complete=True) + cfgTrigEnableSoft = Component(EpicsSignal, "MODE_ENBL_SOFT_RBV", write_pv="MODE_ENBL_SOFT", put_complete=True) + cfgTrigEnableAuto = Component(EpicsSignal, "MODE_ENBL_AUTO_RBV", write_pv="MODE_ENBL_AUTO", put_complete=True) + cfgTrigVirtEnable = Component(EpicsSignal, "MODE_ENBL_EXP_RBV", write_pv="MODE_ENBL_EXP", put_complete=True) # Trigger modes - cfgTrigExt = Component(EpicsSignal, "MODE_TRIG_EXT", read_pv="MODE_TRIG_EXT_RBV", put_complete=True) - cfgTrigSoft = Component(EpicsSignal, "MODE_TRIG_SOFT", read_pv="MODE_TRIG_SOFT_RBV", put_complete=True) - cfgTrigTimer = Component(EpicsSignal, "MODE_TRIG_TIMER", read_pv="MODE_TRIG_TIMER_RBV", put_complete=True) - cfgTrigAuto = Component(EpicsSignal, "MODE_TRIG_AUTO", read_pv="MODE_TRIG_AUTO_RBV", put_complete=True) + cfgTrigExt = Component(EpicsSignal, "MODE_TRIG_EXT_RBV", write_pv="MODE_TRIG_EXT", put_complete=True) + cfgTrigSoft = Component(EpicsSignal, "MODE_TRIG_SOFT_RBV", write_pv="MODE_TRIG_SOFT", put_complete=True) + cfgTrigTimer = Component(EpicsSignal, "MODE_TRIG_TIMER_RBV", write_pv="MODE_TRIG_TIMER", put_complete=True) + cfgTrigAuto = Component(EpicsSignal, "MODE_TRIG_AUTO_RBV", write_pv="MODE_TRIG_AUTO", put_complete=True) # Exposure modes - cfgTrigExpExt = Component(EpicsSignal, "MODE_EXP_EXT", read_pv="MODE_EXP_EXT_RBV", put_complete=True) - cfgTrigExpSoft = Component(EpicsSignal, "MODE_EXP_SOFT", read_pv="MODE_EXP_SOFT_RBV", put_complete=True) - cfgTrigExpTimer = Component(EpicsSignal, "MODE_EXP_TIMER", read_pv="MODE_EXP_TIMER_RBV", put_complete=True) + cfgTrigExpExt = Component(EpicsSignal, "MODE_EXP_EXT_RBV", write_pv="MODE_EXP_EXT", put_complete=True) + cfgTrigExpSoft = Component(EpicsSignal, "MODE_EXP_SOFT_RBV", write_pv="MODE_EXP_SOFT", put_complete=True) + cfgTrigExpTimer = Component(EpicsSignal, "MODE_EXP_TIMER_RBV", write_pv="MODE_EXP_TIMER", put_complete=True) # Line swap selection cfgLineSwapSW = Component(EpicsSignal, "LS_SW", put_complete=True) @@ -66,13 +70,25 @@ class GigaFrostClient(Device): cfgLineSwapSE = Component(EpicsSignal, "LS_SE", put_complete=True) cfgLineSwapNE = Component(EpicsSignal, "LS_NE", put_complete=True) + # HW settings as read only + cfgSyncFlag = Component(EpicsSignalRO, "PIXRATE", auto_monitor=True) + cfgTrigDelay = Component(EpicsSignalRO, "TRIG_DELAY", auto_monitor=True) + cfgSyncoutDelay = Component(EpicsSignalRO, "SYNCOUT_DLY", auto_monitor=True) + cfgOutputPolarity0 = Component(EpicsSignalRO, "BNC0_RBV", auto_monitor=True) + cfgOutputPolarity1 = Component(EpicsSignalRO, "BNC1_RBV", auto_monitor=True) + cfgOutputPolarity2 = Component(EpicsSignalRO, "BNC2_RBV", auto_monitor=True) + cfgOutputPolarity3 = Component(EpicsSignalRO, "BNC3_RBV", auto_monitor=True) + cfgInputPolarity1 = Component(EpicsSignalRO, "BNC4_RBV", auto_monitor=True) + cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True) + infoBoardTemp = Component(EpicsSignalRO, "T_BOARD", auto_monitor=True) - def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): + def __init__(self, prefix="", *, name, backend_url=const.BE999_DAFL_CLIENT, + kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) - self.oldInitializer() + self.oldInitializer(backend_url=backend_url) - def oldInitializer(self, use_soft_enable=False, + def oldInitializer(self, auto_soft_enable=False, timeout=10.0, backend_url=const.BE1_DAFL_CLIENT): """ Class to control the Gigafrost camera and readout system. @@ -88,14 +104,11 @@ class GigaFrostClient(Device): (default: http://xbl-daq-23:8080) """ - - self.name = name self._auto_soft_enable = auto_soft_enable self._timeout = timeout self._backend_url = backend_url self.state = const.GfStatus.NEW - self._original_soft_enable = self.cmdSoftEnable.get() self._settings = None self._north_mac, self._south_mac = self._define_backend_mac() self._north_ip, self._south_ip = self._define_backend_ip() @@ -128,7 +141,7 @@ class GigaFrostClient(Device): # activate changes self.cmdWriteService.set(1).wait() - if self._use_soft_enable: + if self._auto_soft_enable: # trigger modes self.cfgCntStartBit.set(1).wait() self.cfgCntEndBit.set(0).wait() @@ -152,7 +165,7 @@ class GigaFrostClient(Device): # sets the basic settings self._settings = {'backend_url' : self._backend_url, - 'use_soft_enable' : self._use_soft_enable, + 'auto_soft_enable' : self._auto_soft_enable, 'ioc_name' : self.prefix} @@ -193,7 +206,7 @@ class GigaFrostClient(Device): # switch to idle ## Stop acquisition self.cmdStartCamera.set(0).wait() - if self._use_soft_enable: + if self._auto_soft_enable: self.set_soft_enable(0) # change settings @@ -217,7 +230,7 @@ class GigaFrostClient(Device): 'scanid' : scanid, 'correction_mode' : correction_mode, 'backend_url' : self._backend_url, - 'use_soft_enable' : self._use_soft_enable, + 'auto_soft_enable' : self._auto_soft_enable, 'ioc_name' : self.name } @@ -228,7 +241,7 @@ class GigaFrostClient(Device): # change to running self.cmdStartCamera.set(1).wait() # soft trigger on - if self._use_soft_enable: + if self._auto_soft_enable: self.cmdSoftEnable.set(1).wait() self.state = const.GfStatus.OPEN return super().stage() @@ -239,7 +252,7 @@ class GigaFrostClient(Device): """ # switch to idle self.cmdStartCamera.set(0).wait() - if self._use_soft_enable: + if self._auto_soft_enable: self.cmdSoftEnable.set(0).wait() self.state = const.GfStatus.CLOSED return super().unstage() @@ -446,6 +459,18 @@ class GigaFrostClient(Device): def get_backend_url(self): return self._backend_url + def set_backend_ip(self, north, south): + """ + Method to manually set the backend ip + """ + self._north_ip, self._south_ip = north, south + + def set_backend_mac(self, north, south): + """ + Method to manually set the backend mac + """ + self._north_mac, self._south_mac = north, south + def _build_udp_header_table(self): """ Build the header table for the communication @@ -470,16 +495,18 @@ class GigaFrostClient(Device): def _define_backend_ip(self): if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_IP, const.BE3_SOUTH_IP + elif self._backend_url == const.BE999_DAFL_CLIENT: + return const.BE999_NORTH_IP, const.BE999_SOUTH_IP else: - raise RuntimeError(const.ERROR_BACKEND_NAME % ( - const.GF1, const.GF2, const.GF3)) + raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") def _define_backend_mac(self): if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC + elif self._backend_url == const.BE999_DAFL_CLIENT: + return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC else: - raise RuntimeError(const.ERROR_BACKEND_NAME % ( - const.GF1, const.GF2, const.GF3)) + raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") def _set_udp_header_table(self): """ @@ -491,7 +518,7 @@ class GigaFrostClient(Device): # Automatically start simulation if directly invoked if __name__ == "__main__": - gf = GigaFrostClient("X02DA-CAM-GF2") + gf = GigaFrostClient("X02DA-CAM-GF2:", name="gf2") gf.wait_for_connection()