diff --git a/slicops/const.py b/slicops/const.py index fe3f316..012eb14 100644 --- a/slicops/const.py +++ b/slicops/const.py @@ -12,6 +12,7 @@ bpms=frozenset(("BPM",)), lblms=frozenset(("LBLM",)), magnets=frozenset(("XCOR", "QUAD", "SOLE", "YCOR", "BEND")), + pmts=frozenset(("INST",)), screens=frozenset(("PROF",)), tcavs=frozenset(("LCAV",)), wires=frozenset(("WIRE",)), diff --git a/slicops/device/__init__.py b/slicops/device/__init__.py index 2912f5c..44d4b7a 100644 --- a/slicops/device/__init__.py +++ b/slicops/device/__init__.py @@ -15,13 +15,13 @@ class AccessorPutError(RuntimeError): - """The PV for this accessor is not writable""" + """This accessor is not writable""" pass class DeviceError(RuntimeError): - """Error communicating with PV""" + """Error communicating with accessor""" pass @@ -45,9 +45,9 @@ def accessor(self, accessor_name): """Get `_Accessor` for more complex operations Args: - accessor_name (str): friendly name for PV for this device + accessor_name (str): control system independent name Returns: - _Accessor: object holding PV state + _Accessor: object holding control system state """ if self._destroyed: raise AssertionError(f"destroyed {self}") @@ -56,7 +56,7 @@ def accessor(self, accessor_name): )[accessor_name] def destroy(self): - """Disconnect from PV's and remove state about device""" + """Disconnect from accessors and remove state about device""" if self._destroyed: return self._destroyed = True @@ -66,12 +66,12 @@ def destroy(self): a.destroy() def get(self, accessor_name): - """Read from PV + """Read from accessor Args: - accessor_name (str): friendly name for PV for this device + accessor_name (str): Returns: - object: the value from the PV converted to a Python type + object: the value from the control system converted to a Python type """ return self.accessor(accessor_name).get() @@ -79,18 +79,18 @@ def has_accessor(self, accessor_name): """Check whether device has accessor Args: - accessor_name (str): friendly name for PV for this device + accessor_name (str): control system independent name Returns: bool: True if accessor is found for device """ return accessor_name in self.meta.accessor def put(self, accessor_name, value): - """Set PV to value + """Set accessor to value Args: - accessor_name (str): friendly name for PV for this device - value (object): Value to write to PV + accessor_name (str): control system independent name + value (object): Value to write to control system """ return self.accessor(accessor_name).put(value) @@ -99,11 +99,11 @@ def __repr__(self): class _Accessor: - """Container for a PV, metadata, and dynamic state + """Container for a control system interface (CSI): value, metadata, and dynamic state Attributes: device (Device): object holding this accessor - meta (PKDict): meta data about the accessor, e.g. pv_name, pv_writable + meta (PKDict): meta data about the accessor, e.g. csi_name, writable """ def __init__(self, device, accessor_name): @@ -116,10 +116,10 @@ def __init__(self, device, accessor_name): self._initialized = threading.Event() self._initializing = False # Defer initialization - self._pv = None + self._cs = None def destroy(self): - """Stop all monitoring and disconnect from PV""" + """Stop all monitoring and disconnect from accessor""" if self._destroyed: return with self._lock: @@ -128,9 +128,9 @@ def destroy(self): self._destroyed = True self._initializing = False self._callback = None - if (p := self._pv) is None: + if (p := self._cs) is None: return - self._pv = None + self._cs = None self._initialized.set() try: # Clears all callbacks @@ -139,12 +139,12 @@ def destroy(self): pkdlog("error={} {} stack={}", e, self, pkdexc()) def get(self): - """Read from PV + """Read from control system Returns: - object: the value from the PV converted to a Python type + object: the value from the accessor converted to a Python type """ - p = self.__pv() + p = self.__cs() if (rv := p.get(timeout=_TIMEOUT)) is None: raise DeviceError(f"unable to get {self}") if not p.connected: @@ -152,13 +152,13 @@ def get(self): return self._fixup_value(rv) def monitor(self, callback): - """Monitor PV and call callback with updates and connection changes + """Monitor accessor and call callback with updates and connection changes The argument to the callback is a `PKDict` with one or more of: error : str - error occured in the values from the PV callback (unlikely) + error occured in the values from the callback (unlikely) value : object - PV reported this change + control system reported this change connected : bool connection state changed: True if connected @@ -169,25 +169,25 @@ def monitor(self, callback): self._assert_not_destroyed() if self._callback: raise AssertionError("may only call monitor once") - if self._pv or self._initializing: + if self._cs or self._initializing: raise AssertionError("monitor must be called before get/put") self._callback = callback - self.__pv() + self.__cs() def monitor_stop(self): - """Stops monitoring PV""" + """Stops monitoring accessor""" with self._lock: if self._destroyed or not self._callback: return self._callback = None def put(self, value): - """Set PV to value + """Set accessor to value Args: - value (object): Value to write to PV + value (object): Value to write to control system """ - if not self.meta.pv_writable: + if not self.meta.writable: raise AccessorPutError(f"read-only {self}") if self.meta.py_type == bool: v = bool(value) @@ -198,7 +198,7 @@ def put(self, value): else: raise AccessorPutError(f"unhandled py_type={self.meta.py_type} {self}") # ECA_NORMAL == 0 and None is normal, too, apparently - p = self.__pv() + p = self.__cs() if (e := p.put(v)) != 1: raise DeviceError(f"put error={e} value={v} {self}") if not p.connected: @@ -244,11 +244,11 @@ def _on_value(self, **kwargs): pkdlog("error={} {} stack={}", e, self, pkdexc()) raise - def __pv(self): + def __cs(self): with self._lock: self._assert_not_destroyed() - if self._pv: - return self._pv + if self._cs: + return self._cs if not (i := self._initializing): self._initializing = True if i: @@ -260,21 +260,21 @@ def __pv(self): else PKDict() ) if self.accessor_name == "image": - # TODO(robnagler) this has to be done here, because you can't get pvs + # TODO(robnagler) this has to be done here, because you can't get accessor # from within a monitor callback. # TODO(robnagler) need a better way of dealing with this self._image_shape = (self.device.get("n_row"), self.device.get("n_col")) - self._pv = epics.PV( - self.meta.pv_name, + self._cs = epics.PV( + self.meta.csi_name, connection_callback=self._on_connection, connection_timeout=_TIMEOUT, **k, ) self._initialized.set() - return self._pv + return self._cs def __repr__(self): - return f"<_Accessor {self.device.device_name}.{self.accessor_name} {self.meta.pv_name}>" + return f"<_Accessor {self.device.device_name}.{self.accessor_name} {self.meta.csi_name}>" def _run_callback(self, **kwargs): k = PKDict(accessor=self, **kwargs) diff --git a/slicops/device_db.py b/slicops/device_db.py index 30f2e7a..d42a40a 100644 --- a/slicops/device_db.py +++ b/slicops/device_db.py @@ -15,171 +15,16 @@ class DeviceDbError(Exception): pass -_ACCESSOR_META_DEFAULT = PKDict( - py_type=float, - pv_writable=False, -) - -_ACCESSOR_META = PKDict( - acquire=PKDict(py_type=bool, pv_writable=True), - enabled=PKDict(py_type=int, pv_writable=False), - image=PKDict(py_type=numpy.ndarray, pv_writable=False), - n_bits=PKDict(py_type=int, pv_writable=False), - n_col=PKDict(py_type=int, pv_writable=False), - n_row=PKDict(py_type=int, pv_writable=False), - start_scan=PKDict(py_type=int, pv_writable=True), - target_control=PKDict(py_type=int, pv_writable=True), - target_status=PKDict(py_type=int, pv_writable=False), -) - - -_WIRE_META = PKDict( - HTR=PKDict( - lblms=("LBLM01A:HTR", "LBLM01B:HTR"), - bpms_before_wire=("BPMS:GUNB:925", "BPMS:HTR:120", "BPMS:HTR:320"), - bpms_after_wire=( - "BPMS:HTR:760", - "BPMS:HTR:830", - "BPMS:HTR:860", - "BPMS:HTR:960", - ), - ), - DIAG0=PKDict( - lblms=("SBLM01A:DIAG0"), - bpms_before_wire=( - "BPMS:DIAG0:190", - "BPMS:DIAG0:210", - "BPMS:DIAG0:230", - "BPMS:DIAG0:270", - "BPMS:DIAG0:285", - "BPMS:DIAG0:330", - "BPMS:DIAG0:370", - "BPMS:DIAG0:390", - ), - bpms_after_wire=("BPMS:DIAG0:470", "BPMS:DIAG0:520"), - ), - COL1=PKDict( - lblms=("LBLM03A:L1B", "LBLM04A:L2B", "TMITLOSS:COL1"), - bpms_before_wire=( - "BPMS:BC1B:125", - "BPMS:BC1B:440", - "BPMS:COL1:120", - "BPMS:COL1:260", - "BPMS:COL1:280", - "BPMS:COL1:320", - ), - bpms_after_wire=( - "BPMS:BPN27:400", - "BPMS:BPN28:200", - "BPMS:BPN28:400", - "BPMS:SPD:135", - "BPMS:SPD:255", - "BPMS:SPD:340", - "BPMS:SPD:420", - "BPMS:SPD:525", - ), - ), - EMIT2=PKDict( - lblms=("LBLM04A:L2B", "LBLM07A:L3B", "TMITLOSS:EMIT2"), - bpms_before_wire=( - "BPMS:BC2B:150", - "BPMS:BC2B:530", - "BPMS:EMIT2:150", - "BPMS:EMIT2:300", - ), - bpms_after_wire=( - "BPMS:SPS:780", - "BPMS:SPS:830", - "BPMS:SPS:840", - "BPMS:SLTS:150", - "BPMS:SLTS:430", - "BPMS:SLTS:460", - ), - ), - BYP=PKDict( - lblms=("LBLM11A_1:BYP", "LBLM11A_2:BYP", "LBLM11A_3:BYP", "TMITLOSS:BYP"), - bpms_before_wire=( - "BPMS:L3B:3583", - "BPMS:EXT:351", - "BPMS:EXT:748", - "BPMS:DOG:120", - "BPMS:DOG:135", - "BPMS:DOG:150", - "BPMS:DOG:200", - "BPMS:DOG:215", - "BPMS:DOG:230", - "BPMS:DOG:280", - "BPMS:DOG:335", - "BPMS:DOG:355", - "BPMS:DOG:405", - ), - bpms_after_wire=( - "BPMS:BPN23:400", - "BPMS:BPN24:400", - "BPMS:BPN25:400", - "BPMS:BPN26:400", - "BPMS:BPN27:400", - "BPMS:BPN28:200", - "BPMS:BPN28:400", - "BPMS:SPD:135", - "BPMS:SPD:255", - "BPMS:SPD:340", - "BPMS:SPD:420", - "BPMS:SPD:525", - "BPMS:SPD:570", - "BPMS:SPD:700", - "BPMS:SPD:955", - ), - ), - SPD=PKDict( - lblms=("LBLM22A:SPS"), - bpms_before_wire=( - "BPMS:SPD:135", - "BPMS:SPD:255", - "BPMS:SPD:340", - "BPMS:SPD:420", - "BPMS:SPD:525", - "BPMS:SPD:570", - ), - bpms_after_wire=("BPMS:SPD:700", "BPMS:SPD:955", "BPMS:SLTD:625"), - ), - LTUS=PKDict( - lblms=( - "LBLMS32A:LTUS", - "TMITLOSS:LTUS", - ), - bpms_before_wire=( - "BPMS:BPN27:400", - "BPMS:BPN28:200", - "BPMS:BPN28:400", - "BPMS:SPD:135", - "BPMS:SPD:255", - "BPMS:SPD:340", - "BPMS:SPS:572", - "BPMS:SPS:580", - "BPMS:SPS:640", - "BPMS:SPS:710", - "BPMS:SPS:770", - "BPMS:SPS:780", - "BPMS:SPS:830", - "BPMS:SPS:840", - "BPMS:SLTS:150", - ), - bpms_after_wire=("BPMS:DMPS:381", "BPMS:DMPS:502", "BPMS:DMPS:693"), - ), -) - - class DeviceMeta(PKDict): """Information about a device Attributes: - accessor (PKDict): name to PKDict(name, pv_name, pv_writable, py_type, ...) + accessor (PKDict): name to PKDict(name, csi_name, writable, py_type, ...) beam_area (str): area where device is located beam_path (tuple): which beam paths does it go through device_type (str): type device, e.g. "PROF" device_name (str): name of device - pv_prefix (str): prefix to all accessor PVs for device + csi_name (str): prefix to all accessors for device """ pass @@ -224,21 +69,12 @@ def meta_for_device(device_name): DeviceMeta: information about device """ - def _static(device, accessor): - accessor.pkupdate( - _ACCESSOR_META.get(accessor.accessor_name, _ACCESSOR_META_DEFAULT), - ) - if device.device_type in slicops.const.DEVICE_KINDS_TO_TYPES.wires: - accessor.pkupdate(_WIRE_META[device.beam_area]) - rv = DeviceMeta(slicops.device_sql_db.device(device_name)) # TODO(robnagler) probably don't need this check if not rv.accessor: raise DeviceDbError( f"no accessors for device_name={device_name} device_meta={rv}" ) - for r in rv.accessor.values(): - _static(rv, r) return rv diff --git a/slicops/device_sql_db.py b/slicops/device_sql_db.py index e2ff4d1..3bf8886 100644 --- a/slicops/device_sql_db.py +++ b/slicops/device_sql_db.py @@ -8,6 +8,8 @@ from pykern.pkcollections import PKDict from pykern.pkdebug import pkdc, pkdlog, pkdp +import numpy +import pykern.pkconfig import pykern.pkresource import pykern.sql_db import slicops.config @@ -17,6 +19,33 @@ _meta = None +_PY_TYPES = PKDict( + { + "bool": bool, + "float": float, + "int": int, + "numpy.ndarray": numpy.ndarray, + } +) + + +_ACCESSOR_META_DEFAULT = PKDict( + py_type="float", + writable=False, +) + +_ACCESSOR_META = PKDict( + acquire=PKDict(py_type="bool", writable=True), + enabled=PKDict(py_type="int", writable=False), + image=PKDict(py_type="numpy.ndarray", writable=False), + n_bits=PKDict(py_type="int", writable=False), + n_col=PKDict(py_type="int", writable=False), + n_row=PKDict(py_type="int", writable=False), + start_scan=PKDict(py_type="int", writable=True), + target_control=PKDict(py_type="int", writable=True), + target_status=PKDict(py_type="int", writable=False), +) + def beam_paths(): with _session() as s: @@ -27,12 +56,15 @@ def beam_paths(): def device(name): + def _py_type(rec): + return rec.pkupdate(py_type=_PY_TYPES[rec.py_type]) + with _session() as s: return PKDict(s.select_one("device", PKDict(device_name=name))).pkupdate( accessor=PKDict( { - r.accessor_name: PKDict(r) - for r in s.select("device_pv", PKDict(device_name=name)) + r.accessor_name: _py_type(PKDict(r)) + for r in s.select("device_accessor", PKDict(device_name=name)) } ), ) @@ -58,6 +90,16 @@ def device_names(device_type, beam_path): ) +def recreate(parser): + """Recreates db""" + assert not _meta + # Don't remove unless we have valid data + assert parser.devices + pykern.pkio.unchecked_remove(_path()) + pkdlog(_path()) + return _Inserter(parser).counts + + def upstream_devices(device_type, required_accessor, beam_path, end_device): with _session() as s: # select device.device_name from device_meta_float, device where device_meta_name = 'sum_l_meters' and device_meta_value < 33 and device.device_type = 'PROF' and device.device_name = device_meta_float.device_name; @@ -77,8 +119,8 @@ def upstream_devices(device_type, required_accessor, beam_path, end_device): s.t.beam_path.c.beam_area == s.t.device.c.beam_area, ) .join( - s.t.device_pv, - s.t.device_pv.c.device_name == c, + s.t.device_accessor, + s.t.device_accessor.c.device_name == c, ) ) .where( @@ -87,7 +129,7 @@ def upstream_devices(device_type, required_accessor, beam_path, end_device): s.t.device_meta_float.c.device_meta_value < _device_meta(end_device, "sum_l_meters", s), s.t.device.c.device_type == device_type, - s.t.device_pv.c.accessor_name == required_accessor, + s.t.device_accessor.c.accessor_name == required_accessor, ) .order_by(s.t.device_meta_float.c.device_meta_value) ) @@ -120,63 +162,40 @@ def _device_meta(device, meta, select): ).device_meta_value -def recreate(parser): - """Recreates db""" - assert not _meta - # Don't remove unless we have valid data - assert parser.devices - pykern.pkio.unchecked_remove(_path()) - pkdlog(_path()) - return _Inserter(parser).counts - - class _Inserter: - _METADATA_SKIP = frozenset( - ( - "area", - "beam_path", - "bpms_after_wire", - "bpms_before_wire", - "lblms", - "type", - ), - ) - def __init__(self, parser): self.counts = PKDict(beam_areas=0, beam_paths=0, devices=0) - self.parser = parser + if pykern.pkconfig.in_dev_mode(): + # POSIT: modify parser in place since this is dev mode it'll break only in dev + # if the parser implementations change from PKDicts. + _update_dev(parser) with _session() as s: - self._beam_paths(s) - self._devices(s) + self._beam_paths(parser.beam_paths, s) + self._devices(parser.devices, s) - def _beam_paths(self, session): - for a, paths in self.parser.beam_paths.items(): + def _beam_paths(self, parsed, session): + for a, paths in parsed.items(): self.counts.beam_areas += 1 session.insert("beam_area", beam_area=a) for p in paths: self.counts.beam_paths += 1 session.insert("beam_path", beam_area=a, beam_path=p) - def _devices(self, session): - for n, d in self.parser.devices.items(): + def _devices(self, parsed, session): + def _accessor_meta(accessors): + for a in accessors: + a.pkupdate(_ACCESSOR_META.get(a.accessor_name, _ACCESSOR_META_DEFAULT)) + return accessors + + def _insert(table, values): + for v in values: + session.insert(table, **v) + + for d in parsed.values(): self.counts.devices += 1 - session.insert( - "device", - device_name=d.name, - beam_area=d.metadata.area, - device_type=d.metadata.type, - pv_prefix=d.pv_prefix, - ) - for k, v in d.pvs.items(): - session.insert("device_pv", device_name=n, accessor_name=k, pv_name=v) - for k, v in d.metadata.items(): - if k not in self._METADATA_SKIP: - session.insert( - "device_meta_float", - device_name=n, - device_meta_name=k, - device_meta_value=float(v), - ) + session.insert("device", **d.device) + _insert("device_meta_float", d.device_meta_float) + _insert("device_accessor", _accessor_meta(d.device_accessor)) def _init(): @@ -199,12 +218,14 @@ def _init(): device_name=p, beam_area=n + " foreign", device_type=n, - pv_prefix=s, + csi_name=s, ), - device_pv=PKDict( + device_accessor=PKDict( device_name=p + " foreign", accessor_name=p, - pv_name=s, + csi_name=s, + py_type=s, + writable="bool", ), device_meta_float=PKDict( device_name=p + " foreign", @@ -225,3 +246,57 @@ def _session(): if _meta is None: _init() return _meta.session() + + +def _update_dev(parser): + """Add in DEV_CAMERA which is 13SIM1:cam1""" + + def _dev_camera_accessors(name, extra_accessors): + for x in ( + ("acquire", "13SIM1:cam1:Acquire", "bool", 1), + ("image", "13SIM1:image1:ArrayData", "numpy.ndarray", 0), + ("n_bits", "13SIM1:cam1:N_OF_BITS", "int", 0), + ("n_col", "13SIM1:cam1:SizeX", "int", 0), + ("n_row", "13SIM1:cam1:SizeY", "int", 0), + ) + extra_accessors: + yield PKDict( + zip( + ( + "device_name", + "accessor_name", + "csi_name", + "py_type", + "writable", + ), + ((name,) + x), + ), + ) + + def _dev_camera_insert(name, extra_accessors): + parser.devices[name] = PKDict( + device=PKDict( + device_name=name, + beam_area="DEV_AREA", + device_type="PROF", + csi_name="13SIM1", + ), + device_accessor=tuple(_dev_camera_accessors(name, extra_accessors)), + device_meta_float=[ + PKDict( + device_name=name, + device_meta_name="sum_l_meters", + device_meta_value="0.614", + ), + ], + ) + + parser.beam_paths.pkupdate(DEV_AREA=["DEV_BEAM_PATH"]) + _dev_camera_insert( + "DEV_CAMERA", + ( + ("target_status", "13SIM1:cam1:ShutterMode", "int", 0), + ("target_control", "13SIM1:cam1:TriggerMode", "int", 0), + ), + ) + # DEV_CAMERA2 has no target control + _dev_camera_insert("DEV_CAMERA2", ()) diff --git a/slicops/package_data/device_db.sqlite3 b/slicops/package_data/device_db.sqlite3 index ab85f97..f3baa12 100644 Binary files a/slicops/package_data/device_db.sqlite3 and b/slicops/package_data/device_db.sqlite3 differ diff --git a/slicops/package_data/lcls_elements_csv/.gitignore b/slicops/package_data/lcls_elements_csv/.gitignore new file mode 100644 index 0000000..a841d4e --- /dev/null +++ b/slicops/package_data/lcls_elements_csv/.gitignore @@ -0,0 +1 @@ +pvs.json.xz diff --git a/slicops/package_data/lcls_elements_csv/meta.yaml b/slicops/package_data/lcls_elements_csv/meta.yaml new file mode 100644 index 0000000..d596866 --- /dev/null +++ b/slicops/package_data/lcls_elements_csv/meta.yaml @@ -0,0 +1,99 @@ +--- +bpm: + keywords: [ BPM ] + accessors: + TMIT: tmit + X: x + Y: y +lblm: + keywords: [ LBLM ] + accessors: + FAST_AMP_BYP: bypass + FAST_AMP_GAIN: gain + GATED_INTEGRAL: gated_integral + I0_LOSS: i0_loss +magnet: + keywords: [ SOLE, QUAD, XCOR, YCOR, BEND ] + accessors: + BACT: bact + BCON: bcon + BCTRL: bctrl + BDES: bdes + BMAX: bmax + BMIN: bmin + CTRL: ctrl +# lcls_tools*yaml.generate has code for handling INST devices +# that start with PMT, but none of those devices have PVs so +# (or beampaths) there's no need to special check them here. +# We just don't import them at all +# pmt: +# keywords: [ INST ] +# accessors: +# QDCRAW: qdcraw +screen: + keywords: [ PROF ] + accessors: + ArrayRate_RBV: ref_rate + FLT1_CTRL: filter_1_control + FLT1_STS: filter_1_status + FLT2_CTRL: filter_2_control + FLT2_STS: filter_2_status + FRAME_RATE: ref_rate_vme + IMAGE: image + Image:ArrayData: image + Image:ArraySize0_RBV: n_row + Image:ArraySize1_RBV: n_col + N_OF_BITS: n_bits + N_OF_COL: n_col + N_OF_ROW: n_row + PNEUMATIC: target_control + RESOLUTION: resolution + SYS_TYPE: sys_type + TGT_LAMP_PWR: lamp_power + TGT_STS: target_status + X_ORIENT: orient_x + Y_ORIENT: orient_y +tcav: + keywords: [ LCAV ] + accessors: + AFBENB: amplitude_fbenb + AFBST: amplitude_fbst + AMPL_W0CH0: amplitude_wocho + AREQ: amplitude + MODECFG: mode_config + PACT_AVGNT: phase_avgnt + PFBENB: phase_fbenb + PFBST: phase_fbst + PREQ: phase + RF_ENABLE: rf_enable +wire: + keywords: [ WIRE ] + accessors: + BEAMRATE: beam_rate + MOTR.RBV: motor_rbv + MOTR.STOP: abort_scan + MOTR.VBAS: speed_min + MOTR.VELO: speed + MOTR.VMAX: speed_max + MOTR: motor + MOTR_ENABLED_STS: enabled + MOTR_HOMED_STS: homed + MOTR_INIT: initialize + MOTR_INIT_STS: initialize_status + MOTR_RETRACT: retract + MOTR_TIMEOUTEN: timeout + SCANPULSES: scan_pulses + STARTSCAN: start_scan + TEMP: temperature + USEUWIRE: use_u_wire + USEXWIRE: use_x_wire + USEYWIRE: use_y_wire + UWIREINNER: u_wire_inner + UWIREOUTER: u_wire_outer + UWIRESIZE: u_size + XWIREINNER: x_wire_inner + XWIREOUTER: x_wire_outer + XWIRESIZE: x_size + YWIREINNER: y_wire_inner + YWIREOUTER: y_wire_outer + YWIRESIZE: y_size diff --git a/slicops/package_data/sliclet/screen.yaml b/slicops/package_data/sliclet/screen.yaml index b1f096d..7077f25 100644 --- a/slicops/package_data/sliclet/screen.yaml +++ b/slicops/package_data/sliclet/screen.yaml @@ -41,7 +41,7 @@ fields: ui: widget: heatmap_with_lineouts writable: false - pv: + csi_name: prototype: String ui: label: PV @@ -90,7 +90,7 @@ ui_layout: rows: - beam_path - camera - - pv + - csi_name - images_to_average - cell_group: - start_button @@ -110,6 +110,6 @@ ui_layout: - css: col-lg-3 rows: - color_map - - css: col-md-1 - rows: - - save_to_file + - css: col-lg-3 text-end pt-4 + rows: + - save_to_file diff --git a/slicops/pkcli/device_db.py b/slicops/pkcli/device_db.py index c1c965f..6d2d99a 100644 --- a/slicops/pkcli/device_db.py +++ b/slicops/pkcli/device_db.py @@ -1,108 +1,12 @@ -"""Convert `lcls_tools.common.devices.yaml` to `slicops.device_meta_raw` +"""Interact with device_db -TODO(robnagler): document, correct types, add machine and area_to_machine, beam_path_to_machine - -:copyright: Copyright (c) 2024 The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All Rights Reserved. +:copyright: Copyright (c) 2025 The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All Rights Reserved. :license: http://github.com/slaclab/slicops/LICENSE """ from pykern.pkcollections import PKDict from pykern.pkdebug import pkdc, pkdlog, pkdp -import copy -import importlib -import pykern.pkio -import pykern.pkyaml -import re -import slicops.device_sql_db -import slicops.const - - -# We assume the names are valid (could check, but no realy point) -# What this test is doing is ensuring we understand the structure of a pv_base -_PV_POSTFIX_RE = r"([\w.]{1,60}|\w{1,58}:[\w.]{1,58})" - -# Eventually would be canonical -# TODO(robnagler) should be dynamic, but need to add to paths so easiest to add here for now -_DEV_YAML = """ -screens: - DEV_CAMERA: - controls_information: - PVs: - acquire: 13SIM1:cam1:Acquire - image: 13SIM1:image1:ArrayData - n_col: 13SIM1:cam1:SizeX - n_row: 13SIM1:cam1:SizeY - n_bits: 13SIM1:cam1:N_OF_BITS - target_status: 13SIM1:cam1:ShutterMode - target_control: 13SIM1:cam1:TriggerMode - control_name: 13SIM1 - metadata: - area: DEV_AREA - beam_path: - - DEV_BEAM_PATH - sum_l_meters: 0.614 - type: PROF - DEV_CAMERA2: - controls_information: - PVs: - acquire: 13SIM1:cam1:Acquire - image: 13SIM1:image1:ArrayData - n_col: 13SIM1:cam1:SizeX - n_row: 13SIM1:cam1:SizeY - n_bits: 13SIM1:cam1:N_OF_BITS - control_name: 13SIM1 - metadata: - area: DEV_AREA - beam_path: - - DEV_BEAM_PATH - sum_l_meters: 0.614 - type: PROF -""" - - -_KNOWN_KEYS = PKDict( - controls_information=frozenset(("PVs", "control_name", "pv_cache")), - metadata=frozenset( - ( - "area", - "beam_path", - "bpms_after_wire", - "bpms_before_wire", - "l_eff", - "lblms", - "hardware", - "rf_freq", - "sum_l_meters", - "type", - ) - ), -) - -_TOP_LEVEL_KEYS = frozenset(_KNOWN_KEYS.keys()) - -_AREAS_MISSING_BEAM_PATH = frozenset( - ( - "COL", - "GTL", - "LI27", - "LI28", - ), -) - - -def parse(): - from pykern import pkjson - - p = _Parser() - return len(p.devices) - r = pykern.pkio.mkdir_parent("raw") - for d in p.devices.values(): - pkjson.dump_pretty( - d, - filename=pykern.pkio.mkdir_parent( - r.join(d.metadata.type, d.metadata.area) - ).join(d.name + ".json"), - ) +import slicops.device_db def query(func_name, *args): @@ -114,144 +18,4 @@ def query(func_name, *args): Returns: object: result of function """ - from slicops import device_db - - return getattr(device_db, func_name)(*args) - - -def yaml_to_sql(): - """Convert device yaml file to db""" - return slicops.device_sql_db.recreate(_Parser()) - - -class _Ignore(Exception): - pass - - -class _Parser(PKDict): - def __init__(self): - self._init() - self._parse() - - def _init(self): - self._yaml_glob = ( - pykern.pkio.py_path( - importlib.import_module("lcls_tools.common.devices.yaml").__file__, - ) - .dirpath() - .join("*.yaml") - ) - self.devices = PKDict() - self.ctl_keys = set() - self.meta_keys = set() - self.accessors = PKDict() - self.pvs = set() - self.beam_paths = PKDict() - - def _parse(self): - for p in pykern.pkio.sorted_glob(self._yaml_glob): - if p.basename == "beampaths.yaml": - continue - try: - self._parse_file(pykern.pkyaml.load_file(p), p) - except Exception: - pkdlog("ERROR file={}", p) - raise - if pykern.pkconfig.in_dev_mode(): - self._parse_file( - pykern.pkyaml.load_str(_DEV_YAML), pykern.pkio.py_path(".") - ) - - def _parse_file(self, src, path): - def _assign(name, rec): - """Corrections to input data""" - if name in self.devices: - raise ValueError(f"duplicate device={name}") - self.devices[name] = rec - - def _input_fixups(name, rec): - if not rec.controls_information.PVs: - # Also many don't have beam_path - raise _Ignore() - # Save beam_paths for fixups and to return - if rec.metadata.area not in self.beam_paths: - self.beam_paths[rec.metadata.area] = tuple(rec.metadata.beam_path) - if not rec.metadata.beam_path: - if rec.metadata.area in _AREAS_MISSING_BEAM_PATH: - raise _Ignore() - rec.metadata.beam_path = self.beam_paths[rec.metadata.area] - if "VCCB" == name: - # Typo in MEME? - rec.controls_information.PVs.resolution = "CAMR:LGUN:950:RESOLUTION" - rec.controls_information.PVs.n_col = "CAMR:LGUN:950:MaxSizeX_RBV" - rec.controls_information.PVs.n_row = "CAMR:LGUN:950:MaxSizeY_RBV" - rec.metadata.type = "PROF" - elif "VCC" == name: - rec.metadata.type = "PROF" - if rec.metadata.type == "PROF": - # No cameras have Acquire for some reason - rec.controls_information.PVs.pksetdefault( - "acquire", f"{rec.controls_information.control_name}:Acquire" - ) - # TODO(robnagler) parse pv_cache - return rec - - def _meta(name, raw): - # TODO validation - c = raw.controls_information - m = raw.metadata - # TODO ignore for now - raw.metadata.pkdel("hardware") - self.meta_keys.update(m.keys()) - self.ctl_keys.update(c.keys()) - rv = PKDict( - name=name, - pv_prefix=c.control_name, - ) - for k in "area", "beam_path": - if not m.get(k): - raise AssertionError(f"missing metadata.{k}") - rv.metadata = PKDict({k: v for k, v in m.items() if v is not None}) - rv.pvs = PKDict(_pvs(c.PVs, rv)) - return rv - - def _pvs(pvs, rv): - p = re.compile(rf"^{re.escape(rv.pv_prefix)}:{_PV_POSTFIX_RE}$") - for k, v in pvs.items(): - if not (x := p.search(v)): - raise ValueError(f"pv={v} does not match regex={p}") - yield k, v - - def _validate(name, kind, raw): - if not (t := slicops.const.DEVICE_KINDS_TO_TYPES.get(kind)): - raise AssertionError(f"unknown kind={kind}") - if raw.metadata.type not in t: - raise AssertionError(f"unknown type={raw.metadata.type} expect={t}") - if x := set(raw.keys()) - _TOP_LEVEL_KEYS: - raise AssertionError(f"unknown top level keys={s}") - for x in _TOP_LEVEL_KEYS: - if y := set(raw[x].keys()) - _KNOWN_KEYS[x]: - raise AssertionError(f"unknown {x} keys={y}") - if not raw.controls_information.PVs: - raise AssertionError(f"no PVs") - return name, raw - - for k, x in src.items(): - for n, r in x.items(): - try: - - _assign( - n, - _meta( - *_validate( - n, - k, - _input_fixups(n, copy.deepcopy(r)), - ) - ), - ) - except _Ignore: - pass - except Exception: - pkdlog("ERROR device={} record={}", n, r) - raise + return getattr(slicops.device_db, func_name)(*args) diff --git a/slicops/pkcli/lcls_elements_csv.py b/slicops/pkcli/lcls_elements_csv.py new file mode 100644 index 0000000..1b70619 --- /dev/null +++ b/slicops/pkcli/lcls_elements_csv.py @@ -0,0 +1,188 @@ +"""Parse `lcls_tools/common/devices/yaml/lcls_elements.csv` for `slicops.device_sql_db.recreate` + +TODO(robnagler): only includes what is used in slicops and slactwin at the moment + +TODO(robnagler): uses a cached meme-pvs.txt.xz which is created by ``meme.names.list_pvs("%", sort_by="z")`` + +TODO(robnagler): add machine and area_to_machine, beam_path_to_machine + +:copyright: Copyright (c) 2025 The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All Rights Reserved. +:license: http://github.com/slaclab/slicops/LICENSE +""" + +from pykern.pkcollections import PKDict +from pykern.pkdebug import pkdc, pkdlog, pkdp +import csv +import importlib +import lzma +import pykern.pkcompat +import pykern.pkio +import pykern.pkjson +import pykern.pkresource +import pykern.pkyaml +import re +import slicops.device_sql_db + +_BEAMPATH_RE = re.compile(" *, *") + +_PV_RE = re.compile("^(.+):(.+)$") + + +def create_sql_db(csv_path, pvs_path): + """Convert device yaml file to db + + Args: + csv_path (str or path-like): path to lcls_elements.csv [see `Parser`] + pvs_path (str or path-like): path to pvs.txt.xz [see `Parser`] + Returns: + PKDict: db stats, e.g. number of devices + """ + return slicops.device_sql_db.recreate(_Parser(csv_path, pvs_path)) + + +def save_pvs(): + """Converts all `meme.names.list_pvs` into pvs.txt.gz resource + + For use with `create_sql_db`, pipe output to `xz -9` then to file. + + Returns: + str: joined list of pvs + """ + # from meme import names + + def _obj(): + rv = PKDict() + for l in pykern.pkio.open_text("pvs.txt"): # names.list_pvs("%", sort_by="z"): + if m := _PV_RE.search(l): + rv.setdefault(m.group(1), list()).append(m.group(2)) + return rv + + rv = _pvs_path() + pkdlog("requesting all PVs...") + o = _obj() + pkdlog("writing compressed json...") + with lzma.open(rv, mode="wb", format=lzma.FORMAT_XZ) as f: + f.write(pykern.pkjson.dump_bytes(o)) + return rv + + +class _Parser(PKDict): + def __init__(self, csv_path, pvs_path): + def _meta(raw): + for t, v in raw.items(): + for k in v.pkdel("keywords"): + yield k, PKDict(device_type=t).pkupdate(v) + + self._keywords = PKDict(_meta(pykern.pkyaml.load_file(_resource("meta.yaml")))) + with lzma.open(_pvs_path(pvs_path)) as f: + self._pvs = pykern.pkjson.load_any(f) + self.devices = PKDict() + self.beam_paths = PKDict() + with _csv_path(csv_path).open("r") as f: + self._parse_csv(csv.DictReader(f)) + + def _parse_csv(self, rows): + + def _accessors(accessors, pvs, csi_name, device_name): + if not pvs: + return [] + rv = [] + for s, a in accessors.items(): + if s.split(".")[0] not in pvs: + continue + rv.append( + PKDict( + accessor_name=a, + device_name=device_name, + csi_name=csi_name + ": " + s, + ), + ) + return rv + + def _one(device_name, area, beam_paths, keyword, csi_name, sum_l_meters): + # areas that begin with a * are not yet released + # area that contains a space is not legit and probably NO AREA + # no beam path means no PVs + if ( + not beam_paths + or area.startswith("*") + or " " in area + or not (m := self._keywords.get(keyword)) + ): + return + if area not in self.beam_paths: + # assumes that first area with beam_paths is correct + self.beam_paths[area] = tuple(beam_paths) + if not ( + a := _accessors( + m.accessors, self._pvs.get(csi_name), csi_name, device_name + ) + ): + return + return PKDict( + device=PKDict( + device_name=device_name, + device_type=m.device_type, + beam_area=area, + csi_name=csi_name, + ), + device_accessor=a, + device_meta_float=[ + PKDict( + device_name=device_name, + device_meta_name="sum_l_meters", + device_meta_value=sum_l_meters, + ) + ], + ) + + for r in rows: + d = _one( + r["Element"], + r["Area"], + _BEAMPATH_RE.split(r["Beampath"]), + r["Keyword"], + r["Control System Name"], + r["SumL (m)"] and round(float(r["SumL (m)"]), 3), + ) + if d: + self.devices[d.device.device_name] = d + + # def _input_fixups(name, rec): + # if "VCCB" == name: + # # Typo in MEME? + # rec.controls_information.PVs.resolution = "CAMR:LGUN:950:RESOLUTION" + # rec.controls_information.PVs.n_col = "CAMR:LGUN:950:MaxSizeX_RBV" + # rec.controls_information.PVs.n_row = "CAMR:LGUN:950:MaxSizeY_RBV" + # rec.metadata.type = "PROF" + # elif "VCC" == name: + # rec.metadata.type = "PROF" + # if rec.metadata.type == "PROF": + # # No cameras have Acquire for some reason + # rec.controls_information.PVs.pksetdefault( + # "acquire", f"{rec.controls_information.control_name}:Acquire" + # ) + # # TODO(robnagler) parse pv_cache + # return rec + + +def _csv_path(value): + if value: + return pykern.pkio.py_path(value) + return ( + pykern.pkio.py_path( + importlib.import_module("lcls_tools.common.devices.yaml").__file__, + ) + .dirpath() + .join("lcls_elements.csv") + ) + + +def _pvs_path(value=None): + if value: + return pykern.pkio.py_path(value) + return _resource("pvs.json.xz") + + +def _resource(basename): + return pykern.pkresource.file_path("lcls_elements_csv").join(basename) diff --git a/slicops/pkcli/lcls_tools_yaml.py b/slicops/pkcli/lcls_tools_yaml.py new file mode 100644 index 0000000..e951b96 --- /dev/null +++ b/slicops/pkcli/lcls_tools_yaml.py @@ -0,0 +1,222 @@ +"""Parse `lcls_tools/common/devices/yaml/*.yaml` for `slicops.device_sql_db.recreate` + +TODO(robnagler): document, add machine and area_to_machine, beam_path_to_machine + +:copyright: Copyright (c) 2024 The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All Rights Reserved. +:license: http://github.com/slaclab/slicops/LICENSE +""" + +from pykern.pkcollections import PKDict +from pykern.pkdebug import pkdc, pkdlog, pkdp +import copy +import importlib +import pykern.pkio +import pykern.pkyaml +import re +import slicops.device_sql_db +import slicops.const + + +# We assume the names are valid (could check, but no realy point) +# What this test is doing is ensuring we understand the structure of a pv_base +_PV_POSTFIX_RE = r"([\w.]{1,60}|\w{1,58}:[\w.]{1,58})" + +_METADATA_SKIP = frozenset( + ( + "area", + "beam_path", + "bpms_after_wire", + "bpms_before_wire", + "detectors", + "lblms", + "type", + ), +) + + +_KNOWN_KEYS = PKDict( + controls_information=frozenset(("PVs", "control_name", "pv_cache")), + metadata=frozenset( + ( + "l_eff", + "hardware", + "rf_freq", + "sum_l_meters", + ) + ).union(_METADATA_SKIP), +) + +_TOP_LEVEL_KEYS = frozenset(_KNOWN_KEYS.keys()) + +_AREAS_MISSING_BEAM_PATH = frozenset( + ( + "COL", + "GTL", + "LI27", + "LI28", + ), +) + +# PMT types are not well formed +_TYPES_TO_IGNORE = frozenset(("INST",)) + +# wire_lblms.yaml is invalid +_BASENAMES_TO_IGNORE = frozenset(("beampaths", "wire_lblms", "wire_metadata")) + + +def create_sql_db(): + """Convert device yaml file to db""" + return slicops.device_sql_db.recreate(_Parser()) + + +class _Ignore(Exception): + pass + + +class _Parser(PKDict): + def __init__(self): + self._init() + self._parse() + + def _init(self): + self._yaml_glob = ( + pykern.pkio.py_path( + importlib.import_module("lcls_tools.common.devices.yaml").__file__, + ) + .dirpath() + .join("*.yaml") + ) + self.devices = PKDict() + self.ctl_keys = set() + self.meta_keys = set() + self.beam_paths = PKDict() + + def _parse(self): + for p in pykern.pkio.sorted_glob(self._yaml_glob): + if p.purebasename in _BASENAMES_TO_IGNORE: + continue + try: + self._parse_file(pykern.pkyaml.load_file(p), p) + except Exception: + pkdlog("ERROR file={}", p) + raise + + def _parse_file(self, src, path): + def _input_fixups(name, rec): + if rec.metadata.get("type") in _TYPES_TO_IGNORE: + raise _Ignore() + if not rec.controls_information.PVs: + # Also many don't have beam_path + raise _Ignore() + # Save beam_paths for fixups and to return + if rec.metadata.area not in self.beam_paths: + self.beam_paths[rec.metadata.area] = tuple(rec.metadata.beam_path) + if not rec.metadata.beam_path: + if rec.metadata.area in _AREAS_MISSING_BEAM_PATH: + raise _Ignore() + rec.metadata.beam_path = self.beam_paths[rec.metadata.area] + if "VCCB" == name: + # Typo in MEME? + rec.controls_information.PVs.resolution = "CAMR:LGUN:950:RESOLUTION" + rec.controls_information.PVs.n_col = "CAMR:LGUN:950:MaxSizeX_RBV" + rec.controls_information.PVs.n_row = "CAMR:LGUN:950:MaxSizeY_RBV" + rec.metadata.type = "PROF" + elif "VCC" == name: + rec.metadata.type = "PROF" + if rec.metadata.type == "PROF": + # No cameras have Acquire for some reason + rec.controls_information.PVs.pksetdefault( + "acquire", f"{rec.controls_information.control_name}:Acquire" + ) + # TODO(robnagler) parse pv_cache + return rec + + def _meta(name, raw): + # TODO validation + c = raw.controls_information + m = raw.metadata + # TODO ignore for now + raw.metadata.pkdel("hardware") + self.meta_keys.update(m.keys()) + self.ctl_keys.update(c.keys()) + rv = PKDict( + name=name, + csi_name=c.control_name, + ) + for k in "area", "beam_path": + if not m.get(k): + raise AssertionError(f"missing metadata.{k}") + rv.metadata = PKDict({k: v for k, v in m.items() if v is not None}) + rv.accessors = PKDict(_pvs(c.PVs, rv)) + return rv + + def _pvs(pvs, rv): + p = re.compile(rf"^{re.escape(rv.csi_name)}:{_PV_POSTFIX_RE}$") + for k, v in pvs.items(): + if not (x := p.search(v)): + raise ValueError(f"pv={v} does not match regex={p}") + yield k, v + + def _validate(name, kind, raw): + if not (t := slicops.const.DEVICE_KINDS_TO_TYPES.get(kind)): + raise AssertionError(f"unknown kind={kind}") + if raw.metadata.type not in t: + raise AssertionError(f"unknown type={raw.metadata.type} expect={t}") + if x := set(raw.keys()) - _TOP_LEVEL_KEYS: + raise AssertionError(f"unknown top level keys={s}") + for x in _TOP_LEVEL_KEYS: + if y := set(raw[x].keys()) - _KNOWN_KEYS[x]: + raise AssertionError(f"unknown {x} keys={y}") + if not raw.controls_information.PVs: + raise AssertionError(f"no PVs") + return name, raw + + for k, x in src.items(): + for n, r in x.items(): + try: + self._to_sql_db( + n, + _meta( + *_validate( + n, + k, + _input_fixups(n, copy.deepcopy(r)), + ) + ), + ) + except _Ignore: + pass + except Exception: + pkdlog("ERROR device={} record={}", n, r) + raise + + def _assign(name, rec): + """Corrections to input data""" + + def _to_sql_db(self, name, rec): + def _accessor(): + for k, v in rec.accessors.items(): + yield PKDict(device_name=name, accessor_name=k, csi_name=v) + + def _device(): + return PKDict( + device_name=name, + beam_area=rec.metadata.area, + device_type=rec.metadata.type, + csi_name=rec.csi_name, + ) + + def _meta_float(): + for k, v in rec.metadata.items(): + if k not in _METADATA_SKIP: + yield PKDict( + device_name=name, device_meta_name=k, device_meta_value=float(v) + ) + + if name in self.devices: + raise ValueError(f"duplicate device={name}") + self.devices[name] = PKDict( + device=_device(), + device_accessor=tuple(_accessor()), + device_meta_float=tuple(_meta_float()), + ) diff --git a/slicops/plot.py b/slicops/plot.py index 9126225..1ba1e81 100644 --- a/slicops/plot.py +++ b/slicops/plot.py @@ -102,6 +102,7 @@ def _path(): f"{t.strftime('%Y%m%dT%H%M%SZ')}-{self.meta.camera}.h5", ) rv.dirpath().ensure(dir=True) + pkdlog("Saving image set to {}", rv) return rv def _writer(path): diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index e1628b2..0862080 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -66,8 +66,8 @@ ("plot.ui.visible", False), # Useful to avoid large ctx sends ("plot.value", None), - ("pv.ui.visible", False), - ("pv.value", None), + ("csi_name.ui.visible", False), + ("csi_name.value", None), ("save_to_file.ui.enabled", False), ("save_to_file.ui.visible", False), ) @@ -77,7 +77,7 @@ + _TARGET_INVISIBLE ) -_DEVICE_ENABLE = (("pv.ui.visible", True),) + _BUTTONS_VISIBLE +_DEVICE_ENABLE = (("csi_name.ui.visible", True),) + _BUTTONS_VISIBLE _PLOT_ENABLE = ( ("color_map.ui.enabled", True), @@ -113,6 +113,7 @@ def on_change_images_to_average(self, txn, value, **kwargs): self.__new_image_set(txn) def on_click_save_to_file(self, txn, **kwargs): + # TODO(pjm) provide UI notice with file info, download link self.__image_set.save_file(self.save_file_path()) def on_click_single_button(self, txn, **kwargs): @@ -129,6 +130,7 @@ def on_click_target_in_button(self, txn, **kwargs): self.__set(txn, "target", True, _TARGET_DISABLE, method="move_target") def on_click_target_out_button(self, txn, **kwargs): + self.__set(txn, "acquire", False, _BUTTONS_DISABLE) self.__set(txn, "target", False, _TARGET_DISABLE, method="move_target") def handle_init(self, txn): @@ -225,10 +227,11 @@ def __device_setup(self, txn, beam_path, camera): self.__device_destroy(txn) self.__user_alert(txn, "unable to connect to camera={} error={}", camera, e) return - s = PKDict(_DEVICE_ENABLE + (("pv.value", self.__device.meta.pv_prefix),)) + s = PKDict(_DEVICE_ENABLE + (("csi_name.value", self.__device.meta.csi_name),)) if self.__device.has_accessor("target_status"): s.update(_TARGET_VISIBLE) txn.multi_set(s) + self.__new_image_set(txn) def __handle_acquire(self, acquire): with self.lock_for_update() as txn: @@ -253,7 +256,6 @@ def __handle_image(self, image): with self.lock_for_update() as txn: self.__current_value["image"] = image if self.__update_plot(txn) and self.__single_button: - # self.__single_button = False self.__set(txn, "acquire", False, _BUTTONS_DISABLE) txn.multi_set( ("single_button.ui.enabled", True), @@ -263,7 +265,13 @@ def __handle_image(self, image): def __new_image_set(self, txn): self.__image_set = slicops.plot.ImageSet( txn.multi_get( - ("beam_path", "camera", "curve_fit_method", "images_to_average", "pv") + ( + "beam_path", + "camera", + "curve_fit_method", + "images_to_average", + "csi_name", + ) ), ) @@ -276,6 +284,14 @@ def __handle_target_status(self, status): "target_in_button.ui.enabled", status == slicops.device.screen.TargetStatus.OUT, ), + ( + "start_button.ui.enabled", + status == slicops.device.screen.TargetStatus.IN, + ), + ( + "single_button.ui.enabled", + status == slicops.device.screen.TargetStatus.IN, + ), ( "target_out_button.ui.enabled", status == slicops.device.screen.TargetStatus.IN, @@ -308,12 +324,11 @@ def __update_plot(self, txn): return False if (i := self.__current_value["image"]) is None or not i.size: return False + if (p := self.__image_set.add_frame(i, pykern.pkcompat.utcnow())) is None: + return False if not txn.group_get("plot", "ui", "visible"): txn.multi_set(_PLOT_ENABLE) - txn.field_set( - "plot", - slicops.plot.fit_image(i, txn.field_get("curve_fit_method")), - ) + txn.field_set("plot", p) return True def __user_alert(self, txn, fmt, *args): diff --git a/tests/device_db_test.py b/tests/device_db_test.py index 11b85b5..608da52 100644 --- a/tests/device_db_test.py +++ b/tests/device_db_test.py @@ -25,11 +25,11 @@ def test_basic(): device_db.device_names("xyzzy", "SC_SXR") a = device_db.meta_for_device("VCCB") - pkunit.pkeq("CAMR:LGUN:950:Image:ArrayData", a.accessor.image.pv_name) + pkunit.pkeq("CAMR:LGUN:950:Image:ArrayData", a.accessor.image.csi_name) pkunit.pkeq(numpy.ndarray, a.accessor.image.py_type) pkunit.pkeq("GUNB", a.beam_area) - # YAG01B does not have any PVs so not in db + # YAG01B does not have any accessors so not in db with pkunit.pkexcept("NoRows"): device_db.meta_for_device("YAG01B") diff --git a/tests/sliclet/screen_test.py b/tests/sliclet/screen_test.py index c8ab803..6080094 100644 --- a/tests/sliclet/screen_test.py +++ b/tests/sliclet/screen_test.py @@ -65,4 +65,4 @@ async def _buttons(s, expect, msg): # TODO(robnagler) better error handling await _put(ux, "camera", "DEV_CAMERA", Exception) await s.ctx_field_set(camera="YAG01") r = await s.ctx_update() - pkunit.pkeq("YAGS:IN20:211", r.fields.pv.value) + pkunit.pkeq("YAGS:IN20:211", r.fields.csi_name.value) diff --git a/ui/src/components/layout/VCellGroup.vue b/ui/src/components/layout/VCellGroup.vue index f38c9d4..2d6bddc 100644 --- a/ui/src/components/layout/VCellGroup.vue +++ b/ui/src/components/layout/VCellGroup.vue @@ -3,9 +3,8 @@ -->