diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py index 36fabc3188a160c08fe27d17148c0819610f0bfe..a6bb006971c99281f7aa4ea2e3a835c9f565846b 100644 --- a/src/calng/base_correction.py +++ b/src/calng/base_correction.py @@ -222,7 +222,7 @@ class BaseCorrection(PythonDevice): .key("availableScenes") .setSpecialDisplayType(DT_SCENES) .readOnly() - .initialValue(["overview", "constants"]) + .initialValue(["overview"]) .commit(), ) @@ -590,14 +590,8 @@ class BaseCorrection(PythonDevice): payload["success"] = True if scene_name == "overview": payload["data"] = scenes.correction_device_overview_scene( - device_id=self.getInstanceId() - ) - elif scene_name == "constants": - payload["data"] = scenes.correction_device_constants_scene( device_id=self.getInstanceId(), schema=self.getFullSchema(), - param_prefix="constantParameters", - status_prefix="foundConstants", ) else: payload["success"] = False diff --git a/src/calng/scenes.py b/src/calng/scenes.py index aae1e48015077a9e4de6df38a826dd362a8047c5..a2f3fcf4fbdd90e590ca2f7b1c1af62c0d6ba49f 100644 --- a/src/calng/scenes.py +++ b/src/calng/scenes.py @@ -1,215 +1,418 @@ -from karabo.bound import Types +import textwrap + +import karathon from karabo.common.scenemodel.api import ( + ColorBoolModel, DisplayCommandModel, DisplayLabelModel, DisplayStateColorModel, DisplayTextLogModel, DoubleLineEditModel, + EvaluatorModel, IntLineEditModel, + LineEditModel, LabelModel, SceneModel, write_scene, + RectangleModel, ) BASE_INC = 30 PADDING = 10 RECONFIGURABLE = 4 # TODO: look up proper enum +_type_to_line_editable = { + "DOUBLE": (DoubleLineEditModel, {}), + "FLOAT": (DoubleLineEditModel, {}), + "INT32": (IntLineEditModel, {}), + "UINT32": (IntLineEditModel, {}), + "INT64": (IntLineEditModel, {}), + "UINT64": (IntLineEditModel, {}), + "STRING": (LineEditModel, {"klass": "EditableLineEdit"}), +} -def correction_device_constants_scene(device_id, schema, param_prefix, status_prefix): - # TODO: are there layout models in scene model somewhere? like gridbox? - subscenes = [] - max_height = 0 - # first column: parameters - row_offset = PADDING - col_offset = PADDING - subscenes.append( - LabelModel( - text="Parameters used for CalCat queries", - width=12 * BASE_INC, - x=col_offset, - y=row_offset, - ) - ) - row_offset += BASE_INC - for key in schema.getKeys(param_prefix): - key_path = f"{param_prefix}.{key}" - subscenes.append( - LabelModel( - text=schema.getDisplayedName(key_path) - if schema.hasDisplayedName(key_path) - else key, - width=5 * BASE_INC, - height=BASE_INC, - x=col_offset, - y=row_offset, - ) - ) - subscenes.append( - DisplayLabelModel( - keys=[f"{device_id}.{param_prefix}.{key}"], - width=7 * BASE_INC, - height=BASE_INC, - x=col_offset + 5 * BASE_INC, - y=row_offset, + +def titled(title, width=8): + def actual_decorator(component_class): + # should this create subclass instead of mutating? + orig_render = component_class.render + + def new_render(self, x, y, *args, **kwargs): + return [ + LabelModel( + frame_width=1, + text=title, + width=width * BASE_INC, + height=BASE_INC, + x=x, + y=y, + ) + ] + orig_render(self, x, y + BASE_INC, *args, **kwargs) + + component_class.render = new_render + + orig_height = component_class.height + + def new_height(self): + return orig_height(self) + BASE_INC + + component_class.height = new_height + + return component_class + + return actual_decorator + + +def boxed(component_class): + # should this create subclass instead of mutating? + orig_render = component_class.render + orig_height = component_class.height + orig_width = component_class.width + + def new_render(self, x, y, *args, **kwargs): + return [ + RectangleModel( + x=x, + y=y, + width=orig_width(self) + 2 * PADDING, + height=orig_height(self) + 2 * PADDING, + stroke="#000000", ) + ] + orig_render(self, x + PADDING, y + PADDING, *args, **kwargs) + + component_class.render = new_render + + def new_height(self): + return orig_height(self) + 2 * PADDING + + component_class.height = new_height + + def new_width(self): + return orig_width(self) + 2 * PADDING + + component_class.width = new_width + + return component_class + + +class MaybeEditableRow: + def __init__( + self, + device_id, + schema_hash, + key_path, + label_width=7, + display_width=5, + edit_width=5, + ): + self.label_width = label_width * BASE_INC + self.display_width = display_width * BASE_INC + self.edit_width = edit_width * BASE_INC + + key_attr = schema_hash.getAttributes(key_path) + label_text = textwrap.shorten( + key_attr["displayedName"] + if "displayedName" in key_attr + else key_path.split(".")[-1], + 24, ) - if schema.getAccessMode(key_path) == RECONFIGURABLE: - value_type = schema.getValueType(key_path) - if value_type in (Types.DOUBLE, Types.FLOAT): - subscenes.append( - DoubleLineEditModel( - keys=[f"{device_id}.{param_prefix}.{key}"], - width=7 * BASE_INC, - height=BASE_INC, - x=col_offset + 12 * BASE_INC, - y=row_offset, - ) - ) - elif value_type in (Types.INT32, Types.UINT32, Types.INT64, Types.UINT64): - subscenes.append( - IntLineEditModel( - keys=[f"{device_id}.{param_prefix}.{key}"], - width=7 * BASE_INC, - height=BASE_INC, - x=col_offset + 12 * BASE_INC, - y=row_offset, - ) - ) - else: - subscenes.append( - LabelModel( - text=f"Not yet supported: editing {key} of type {value_type}", - width=7 * BASE_INC, - height=BASE_INC, - x=col_offset + 12 * BASE_INC, - y=row_offset, - ) - ) - row_offset += BASE_INC - max_height = max(max_height, row_offset) - - # second column: constants as currently loaded - row_offset = PADDING - col_offset += 19 * BASE_INC # width of first column - col_offset += PADDING # and padding - subscenes.append( - DisplayCommandModel( - keys=[f"{device_id}.loadMostRecentConstants"], - x=col_offset, - y=row_offset, - width=10 * BASE_INC, + self.label = LabelModel( + text=label_text, + width=self.label_width, height=BASE_INC, ) - ) - row_offset += BASE_INC - subscenes.append( - LabelModel( - text="Constants found and loaded", - width=10 * BASE_INC, + self.value_display = DisplayLabelModel( + keys=[f"{device_id}.{key_path}"], + width=self.display_width, height=BASE_INC, - x=col_offset, - y=row_offset, ) - ) - row_offset += BASE_INC - for constant_name in schema.getKeys(status_prefix): - constant_path = f"{status_prefix}.{constant_name}" - subscenes.append( - LabelModel( - text=constant_name, - width=10 * BASE_INC, - height=BASE_INC, - x=col_offset, - y=row_offset, - ) - ) - row_offset += BASE_INC - for field in ("found", "createdAt"): - subscenes.append( - LabelModel( - text=field, # TODO: displayedName - width=5 * BASE_INC, + if key_attr["accessMode"] == RECONFIGURABLE: + value_type = key_attr["valueType"] + if value_type in _type_to_line_editable: + line_editable_class, extra_args = _type_to_line_editable[value_type] + self.value_edit = line_editable_class( + keys=[f"{device_id}.{key_path}"], + width=self.edit_width, height=BASE_INC, - x=col_offset, - y=row_offset, + **extra_args, ) - ) - subscenes.append( - DisplayLabelModel( - keys=[f"{device_id}.{constant_path}.{field}"], - width=7 * BASE_INC, + else: + self.value_edit = LabelModel( + text=f"Not yet supported: editing {key_path} of type {value_type}", + width=self.edit_width, height=BASE_INC, - x=col_offset + 5 * BASE_INC, - y=row_offset, ) + + def height(self): + return BASE_INC + + def width(self): + return self.label_width + self.display_width + self.edit_width + + def render(self, x=None, y=None): + self.label.x = x + self.label.y = y + self.value_display.x = x + self.label_width + self.value_display.y = y + if hasattr(self, "value_edit"): + self.value_edit.x = x + self.label_width + self.display_width + self.value_edit.y = y + return [ + self.label, + self.value_display, + self.value_edit, + ] + else: + return [ + self.label, + self.value_display, + ] + + +@titled("Parameters used for CalCat queries", width=10) +@boxed +class ConstantParameterColumn: + def __init__(self, device_id, schema_hash, prefix="constantParameters"): + self.device_id = device_id + self.rows = [ + MaybeEditableRow(device_id, schema_hash, f"{prefix}.{key}") + for key in schema_hash.get(prefix).getKeys() + ] + + def render(self, x, y): + res = [] + y_offset = 0 + for row in self.rows: + res.extend(row.render(x=x, y=y + y_offset)) + y_offset += row.height() + + res.append( + DisplayCommandModel( + keys=[f"{self.device_id}.loadMostRecentConstants"], + x=x + 7 * BASE_INC, + y=y + y_offset, + width=10 * BASE_INC, + height=BASE_INC, ) - row_offset += BASE_INC - max_height = max(max_height, row_offset) + ) - scene = SceneModel( - height=max_height + 2 * PADDING, - width=col_offset + 12 * BASE_INC + PADDING, - children=subscenes, - ) - return write_scene(scene) + return res + def height(self): + return sum(row.height() for row in self.rows) + BASE_INC -def correction_device_overview_scene(device_id): - subscenes = [] - row_offset = 0 - # device class and name - subscenes.append( - LabelModel( - text="Correction device", - width=5 * BASE_INC, + def width(self): + return max(row.width() for row in self.rows) + + +class ConstantNode: + def __init__(self, device_id, constant_path): + self.title = LabelModel( + text=constant_path.split(".")[-1], + width=7 * BASE_INC, height=BASE_INC, - x=0, - y=row_offset, ) - ) - subscenes.append( - DisplayLabelModel( + self.ampel = ColorBoolModel( + height=BASE_INC, width=BASE_INC, keys=[f"{device_id}.{constant_path}.found"] + ) + + def render(self, x, y): + self.title.x = x + self.title.y = y + self.ampel.x = x + 7 * BASE_INC + self.ampel.y = y + y += BASE_INC + return [self.title, self.ampel] + + def height(self): + return BASE_INC + + def width(self): + return 8 * BASE_INC + + +@titled("Found constants", width=6) +@boxed +class FoundConstantsColumn: + def __init__(self, device_id, schema_hash, prefix="foundConstants"): + self.device_id = device_id + self.rows = [ + ConstantNode(device_id, f"{prefix}.{constant_name}") + for constant_name in schema_hash.get(prefix).getKeys() + ] + + def render(self, x, y): + res = [] + y_offset = 0 + for row in self.rows: + res.extend(row.render(x, y + y_offset)) + y_offset += row.height() + return res + + def height(self): + return sum(row.height() for row in self.rows) + + def width(self): + return max(row.width() for row in self.rows) + + +class ConstantLoadedAmpeln: + def __init__(self, device_id, schema_hash, prefix="foundConstants"): + self.keys = [ + f"{device_id}.{prefix}.{key}.found" + for key in schema_hash.get(prefix).getKeys() + ] + + def render(self, x, y): + return [ + ColorBoolModel( + x=x + i * BASE_INC, + y=y, + height=BASE_INC, + width=BASE_INC, + keys=[key], + ) + for i, key in enumerate(self.keys) + ] + + def width(self): + return BASE_INC * len(self.keys) + + def height(self): + return BASE_INC + + +@titled("Device status", width=6) +@boxed +class CorrectionDeviceStatus: + def __init__(self, device_id): + self.name = DisplayLabelModel( keys=[f"{device_id}.deviceId"], - width=10 * BASE_INC, + width=14 * BASE_INC, height=BASE_INC, - x=5 * BASE_INC, - y=row_offset, ) - ) - row_offset += BASE_INC - # device state - subscenes.append( - DisplayStateColorModel( + self.state = DisplayStateColorModel( + show_string=True, keys=[f"{device_id}.state"], - width=5 * BASE_INC, + width=7 * BASE_INC, height=BASE_INC, - # parent_component="DisplayComponent", - x=0, - y=row_offset, ) - ) - subscenes.append( - DisplayLabelModel( - keys=[f"{device_id}.state"], - width=5 * BASE_INC, + self.rate = EvaluatorModel( + expression="f'{x:.02f}'", + keys=[f"{device_id}.performance.rate"], + width=7 * BASE_INC, height=BASE_INC, - x=0, - y=row_offset, ) - ) - row_offset += BASE_INC - # log of status - subscenes.append( - DisplayTextLogModel( + self.processing_time = EvaluatorModel( + expression="f'{x:.02f}'", + keys=[f"{device_id}.performance.processingDuration"], + width=7 * BASE_INC, + height=BASE_INC, + ) + self.tid = DisplayLabelModel( + keys=[f"{device_id}.trainId"], + width=7 * BASE_INC, + height=BASE_INC, + ) + self.status_log = DisplayTextLogModel( keys=[f"{device_id}.status"], - width=10 * BASE_INC, - height=10 * BASE_INC, - x=0, - y=row_offset, + width=14 * BASE_INC, + height=20 * BASE_INC, + ) + + def render(self, x, y): + self.name.x = x + self.name.y = y + y += BASE_INC + self.state.x = x + self.state.y = y + self.tid.x = x + 7 * BASE_INC + self.tid.y = y + y += BASE_INC + self.rate.x = x + self.rate.y = y + self.processing_time.x = x + 7 * BASE_INC + self.processing_time.y = y + y += BASE_INC + self.status_log.x = x + self.status_log.y = y + return [ + self.name, + self.state, + self.rate, + self.processing_time, + self.tid, + self.status_log, + ] + + def width(self): + return 14 * BASE_INC + + def height(self): + return 23 * BASE_INC + + +class CompactCorrectionDeviceOverview: + def __init__(self, device_id, schema_hash): + self.status = DisplayStateColorModel( + show_string=True, + keys=[f"{device_id}.state"], + width=6 * BASE_INC, + height=BASE_INC, ) + self.rate = EvaluatorModel( + expression="f'{x:.02f}'", + keys=[f"{device_id}.performance.rate"], + width=4 * BASE_INC, + height=BASE_INC, + ) + self.tid = DisplayLabelModel( + keys=[f"{device_id}.trainId"], + width=4 * BASE_INC, + height=BASE_INC, + ) + self.ampeln = ConstantLoadedAmpeln(device_id, schema_hash) + + def render(self, x, y): + self.status.x = x + self.status.y = y + x += self.status.width + self.rate.x = x + self.rate.y = y + x += self.rate.width + self.tid.x = x + self.tid.y = y + x += self.tid.width + return [self.status, self.rate, self.tid] + self.ampeln.render(x, y) + + +def correction_device_overview_scene(device_id, schema): + if isinstance(schema, karathon.Schema): + schema_hash = schema.getParameterHash() + else: + schema_hash = schema.hash + + status_overview = CorrectionDeviceStatus(device_id) + cpc = ConstantParameterColumn(device_id, schema_hash) + fcc = FoundConstantsColumn(device_id, schema_hash) + + subscenes = [] + x = PADDING + y = PADDING + subscenes.extend(status_overview.render(x, y)) + x += status_overview.width() + BASE_INC + subscenes.extend(cpc.render(x, y)) + x += cpc.width() + BASE_INC + subscenes.extend(fcc.render(x, y)) + + scene = SceneModel( + height=max(status_overview.height(), cpc.height()) + 2 * PADDING, + width=2 * PADDING + + 2 * BASE_INC + + status_overview.width() + + cpc.width() + + fcc.width(), + children=subscenes, ) - row_offset += BASE_INC * 10 - scene = SceneModel(height=845.0, width=742.0, children=subscenes) return write_scene(scene)