def __init__( self, name: util.APartName, description: util.AMetaDescription, pv: util.APv = "", rbv: util.ARbv = "", rbv_suffix: util.ARbvSuffix = "", min_delta: util.AMinDelta = 0.05, timeout: util.ATimeout = DEFAULT_TIMEOUT, sink_port: util.ASinkPort = None, widget: util.AWidget = None, group: util.AGroup = None, config: util.AConfig = True, ) -> None: super().__init__(name) self.meta = ChoiceMeta(description) self.caa = util.CAAttribute( self.meta, util.catools.DBR_ENUM, pv, rbv, rbv_suffix, min_delta, timeout, sink_port, widget, group, config, self.on_connect, )
class CAChoicePart(Part): """Defines a choice `Attribute` that talks to a DBR_ENUM mbbo PV""" def __init__(self, name, # type: util.APartName description, # type: util.AMetaDescription pv="", # type: util.APv rbv="", # type: util.ARbv rbv_suffix="", # type: util.ARbvSuffix min_delta=0.05, # type: util.AMinDelta timeout=DEFAULT_TIMEOUT, # type: util.ATimeout sink_port=None, # type: util.ASinkPort widget=None, # type: util.AWidget group=None, # type: util.AGroup config=True, # type: util.AConfig ): # type: (...) -> None super(CAChoicePart, self).__init__(name) self.meta = ChoiceMeta(description) self.caa = util.CAAttribute( self.meta, util.catools.DBR_ENUM, pv, rbv, rbv_suffix, min_delta, timeout, sink_port, widget, group, config, self.on_connect) def on_connect(self, value): self.meta.set_choices(value.enums) def caput(self, value): # Turn the string value int the index of the choice list. We are # passed a validated value, so it is guaranteed to be in choices value = self.meta.choices.index(value) self.caa.caput(value) def setup(self, registrar): # type: (PartRegistrar) -> None self.caa.setup(registrar, self.name, self.register_hooked, self.caput)
def setUp(self): self.choice_meta = ChoiceMeta("test description", ["a", "b"]) self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/ChoiceMeta:1.0" self.serialized["description"] = "desc" self.serialized["choices"] = ["a", "b"] self.serialized["tags"] = [] self.serialized["writeable"] = False self.serialized["label"] = "name"
def test_init(self): self.choice_meta = ChoiceMeta( "test description", ["a", "b"]) assert ( "test description") == self.choice_meta.description assert ( self.choice_meta.typeid) == "malcolm:core/ChoiceMeta:1.0" assert ( self.choice_meta.label) == "" assert ( self.choice_meta.choices) == ["a", "b"]
def __init__( self, name: APartName, description: AMetaDescription, choices: UChoices, value: AValue, writeable: AWriteable = False, config: AConfig = 1, group: AGroup = None, widget: AWidget = None, ) -> None: super().__init__(name) meta = ChoiceMeta(description, choices) set_tags(meta, writeable, config, group, widget) self.attr = meta.create_attribute_model(value) self.writeable_func = self.attr.set_value if writeable else None
def __init__(self, mri: AMri, description: ADescription = "") -> None: super().__init__(mri, description) self._children_writeable: ChildrenWriteable = {} self.state = ChoiceMeta( "StateMachine State of Block", self.state_set.possible_states, tags=[Widget.MULTILINETEXTUPDATE.tag()] # Start DISABLING so we can immediately go to DISABLED ).create_attribute_model(ss.DISABLING) self.field_registry.add_attribute_model("state", self.state) self.field_registry.add_method_model(self.disable) self.set_writeable_in(self.field_registry.add_method_model(self.reset), ss.DISABLED, ss.FAULT) self.transition(ss.DISABLED) self.register_hooked(ProcessStartHook, self.init) self.register_hooked(ProcessStopHook, self.halt)
def test_from_dict(self): bm = ChoiceMeta.from_dict(self.serialized) assert type(bm) == ChoiceMeta assert bm.description == "desc" assert bm.choices == ["a", "b"] assert bm.tags == [] assert not bm.writeable assert bm.label == "name"
def _make_ext_capture(self, field_name: str, field_data: FieldData) -> None: group = self._make_group("outputs") meta = ChoiceMeta( "Capture %s in PCAP?" % field_name, field_data.labels, tags=[group, Widget.COMBO.tag()], ) self._make_field_part(field_name + ".CAPTURE", meta, writeable=True)
def __init__( self, name, # type: APartName description, # type: AMetaDescription choices, # type: UChoices value, # type: AValue writeable=False, # type: AWriteable config=1, # type: AConfig group=None, # type: AGroup widget=None, # type: AWidget ): # type: (...) -> None super(ChoicePart, self).__init__(name) meta = ChoiceMeta(description, choices) set_tags(meta, writeable, config, group, widget) self.attr = meta.create_attribute_model(value) self.writeable_func = self.attr.set_value if writeable else None
def __init__(self, pv_prefix: APvPrefix, group: ca.util.AGroup = None) -> None: super().__init__("sinkPorts") self.pvs = [pv_prefix + ":CsPort", pv_prefix + ":CsAxis"] self.rbvs = [ pv_prefix + ":CsPort_RBV", pv_prefix + ":CsAxis_RBV", pv_prefix + ".OUT", ] meta = ChoiceMeta("CS Axis") builtin.util.set_tags(meta, writeable=True, group=group, sink_port=Port.MOTOR) self.cs_attr = meta.create_attribute_model() meta = StringMeta("Parent PMAC Port name") builtin.util.set_tags(meta, group=group, sink_port=Port.MOTOR) self.pmac_attr = meta.create_attribute_model() meta = NumberMeta("int32", "Parent PMAC Axis number") builtin.util.set_tags(meta, group=group) self.axis_num_attr = meta.create_attribute_model() # Subscriptions self.monitors: List = [] self.port = None self.axis = None self.port_choices: List = []
def __init__(self, name, # type: util.APartName description, # type: util.AMetaDescription pv="", # type: util.APv rbv="", # type: util.ARbv rbv_suffix="", # type: util.ARbvSuffix min_delta=0.05, # type: util.AMinDelta timeout=DEFAULT_TIMEOUT, # type: util.ATimeout sink_port=None, # type: util.ASinkPort widget=None, # type: util.AWidget group=None, # type: util.AGroup config=True, # type: util.AConfig ): # type: (...) -> None super(CAChoicePart, self).__init__(name) self.meta = ChoiceMeta(description) self.caa = util.CAAttribute( self.meta, util.catools.DBR_ENUM, pv, rbv, rbv_suffix, min_delta, timeout, sink_port, widget, group, config, self.on_connect)
class TestChoiceMeta(unittest.TestCase): def setUp(self): self.choice_meta = ChoiceMeta( "test description", ["a", "b"]) self.serialized = OrderedDict() self.serialized["typeid"] = "malcolm:core/ChoiceMeta:1.0" self.serialized["description"] = "desc" self.serialized["choices"] = ["a", "b"] self.serialized["tags"] = [] self.serialized["writeable"] = False self.serialized["label"] = "name" def test_init(self): self.choice_meta = ChoiceMeta( "test description", ["a", "b"]) assert ( "test description") == self.choice_meta.description assert ( self.choice_meta.typeid) == "malcolm:core/ChoiceMeta:1.0" assert ( self.choice_meta.label) == "" assert ( self.choice_meta.choices) == ["a", "b"] def test_given_valid_value_then_return(self): response = self.choice_meta.validate("a") assert "a" == response def test_int_validate(self): response = self.choice_meta.validate(1) assert "b" == response def test_None_valid(self): response = self.choice_meta.validate(None) assert "a" == response def test_given_invalid_value_then_raises(self): with self.assertRaises(ValueError): self.choice_meta.validate('badname') def test_set_choices(self): self.choice_meta.set_choices(["4"]) assert ["4"] == self.choice_meta.choices def test_to_dict(self): bm = ChoiceMeta("desc", ["a", "b"], label="name") assert bm.to_dict() == self.serialized def test_from_dict(self): bm = ChoiceMeta.from_dict(self.serialized) assert type(bm) == ChoiceMeta assert bm.description == "desc" assert bm.choices == ["a", "b"] assert bm.tags == [] assert not bm.writeable assert bm.label == "name"
def __init__(self, name, prefix, group=None): # type: (ca.util.APartName, APrefix, ca.util.AGroup) -> None super(RawMotorCSPart, self).__init__(name) self.pvs = [prefix + ":CsPort", prefix + ":CsAxis"] self.rbvs = [prefix + ":CsPort_RBV", prefix + ":CsAxis_RBV"] meta = ChoiceMeta("CS Axis") builtin.util.set_tags(meta, writeable=True, group=group, sink_port=Port.MOTOR) self.attr = meta.create_attribute_model() # Subscriptions self.monitors = [] self.port = None self.axis = None self.port_choices = [] # Hooks self.register_hooked(builtin.hooks.DisableHook, self.disconnect) self.register_hooked((builtin.hooks.InitHook, builtin.hooks.ResetHook), self.reconnect)
def _make_time_parts(self, field_name, field_data, writeable): description = field_data.description if writeable: widget = Widget.TEXTINPUT group = self._make_group("parameters") else: widget = Widget.TEXTUPDATE group = self._make_group("readbacks") meta = NumberMeta("float64", description, [group, widget.tag()]) # We must change time units before value, so restore value in 2nd # iteration self._make_field_part(field_name, meta, writeable, iteration=2) meta = ChoiceMeta(description + " time units", ["s", "ms", "us"], tags=[group, Widget.COMBO.tag()]) self._make_field_part(field_name + ".UNITS", meta, writeable=True)
def _make_mux( self, field_name: str, field_data: FieldData, port_type: Port ) -> None: group = self._make_group("inputs") labels = [x for x in field_data.labels if x in ("ZERO", "ONE")] + sorted( x for x in field_data.labels if x not in ("ZERO", "ONE") ) tags = [group, port_type.sink_port_tag("ZERO"), Widget.COMBO.tag()] if port_type == Port.BOOL: # Bits have a delay, use it as a badge delay_name = snake_to_camel(field_name) + "Delay" tags.append(badge_value_tag(self.mri, delay_name)) meta = ChoiceMeta(field_data.description, labels, tags=tags) self._make_field_part(field_name, meta, writeable=True) self.mux_metas[field_name] = meta
def _make_mux(self, field_name, field_data, typ): group = self._make_group("inputs") if typ == "bit": port_type = Port.BOOL else: port_type = Port.INT32 labels = [x for x in field_data.labels if x in ("ZERO", "ONE")] + \ sorted(x for x in field_data.labels if x not in ("ZERO", "ONE")) meta = ChoiceMeta( field_data.description, labels, tags=[group, port_type.sink_port_tag("ZERO"), Widget.COMBO.tag()]) self._make_field_part(field_name, meta, writeable=True) meta = make_meta(typ, "%s current value" % field_name, tags=[group], writeable=False) self._make_field_part(field_name + ".CURRENT", meta, writeable=False)
def __init__(self, name: APartName, description: AMetaDescription) -> None: super().__init__(name) meta = ChoiceMeta(description, ["expanded", "collapsed"]) set_tags(meta, writeable=True, widget=Widget.GROUP) self.attr = meta.create_attribute_model("expanded")
def __init__( self, mri: AMri, config_dir: AConfigDir, template_designs: ATemplateDesigns = "", initial_design: AInitialDesign = "", use_git: AUseGit = True, description: ADescription = "", ) -> None: super().__init__(mri=mri, description=description) assert os.path.isdir(config_dir), "%s is not a directory" % config_dir self.config_dir = config_dir self.initial_design = initial_design self.use_git = use_git self.template_designs = template_designs self.git_config: Tuple[str, ...] if use_git: if check_git_version("1.7.2"): self.git_email = os.environ["USER"] + "@" + socket.gethostname( ) self.git_name = "Malcolm" self.git_config = ( "-c", "user.name=%s" % self.git_name, "-c", 'user.email="%s"' % self.git_email, ) else: self.git_config = () # last saved layout and exports self.saved_visibility = None self.saved_exports = None # ((name, AttributeModel/MethodModel, setter, needs_context)) self._current_part_fields = () self._subscriptions: List[Subscribe] = [] self.port_info: Dict[APartName, List[PortInfo]] = {} self.part_exportable: Dict[Part, List[AAttributeName]] = {} # TODO: turn this into "exported attribute modified" self.context_modified: Dict[Part, Set[str]] = {} self.part_modified: Dict[Part, PartModifiedInfo] = {} # The attributes our part has published self.our_config_attributes: Dict[str, AttributeModel] = {} # The reportable infos we are listening for self.info_registry.add_reportable(PartModifiedInfo, self.update_modified) # Update queue of exportable fields self.info_registry.add_reportable(PartExportableInfo, self.update_exportable) # Create a port for ourself self.field_registry.add_attribute_model( "mri", StringMeta( "A port for giving our MRI to things that might use us", tags=[Port.BLOCK.source_port_tag(self.mri)], ).create_attribute_model(self.mri), ) # Create a layout table attribute for setting block positions self.layout = TableMeta.from_table( LayoutTable, "Layout of child blocks", Widget.FLOWGRAPH, writeable=["x", "y", "visible"], ).create_attribute_model() self.set_writeable_in(self.layout, ss.READY) self.field_registry.add_attribute_model("layout", self.layout, self.set_layout) # Create a design attribute for loading an existing layout self.design = ChoiceMeta("Design name to load", tags=[config_tag(), Widget.COMBO.tag() ]).create_attribute_model() self.field_registry.add_attribute_model("design", self.design, self.set_design) self.set_writeable_in(self.design, ss.READY) # Create an export table for mirroring exported fields self.exports = TableMeta.from_table( ExportTable, "Exported fields of child blocks", writeable=list(ExportTable.call_types), ).create_attribute_model() # Overwrite the sources meta to be a ChoiceArrayMeta self.exports.meta.elements["source"] = ChoiceArrayMeta( "Name of the block.field to export", writeable=True, tags=[Widget.COMBO.tag()], ) self.set_writeable_in(self.exports, ss.READY) self.field_registry.add_attribute_model("exports", self.exports, self.set_exports) # Create read-only indicator for when things are modified self.modified = BooleanMeta("Whether the design is modified", tags=[Widget.LED.tag() ]).create_attribute_model() self.field_registry.add_attribute_model("modified", self.modified) # Create the save method self.set_writeable_in(self.field_registry.add_method_model(self.save), ss.READY)
class ManagerController(StatefulController): """RunnableDevice implementer that also exposes GUI for child parts""" state_set = ss() def __init__( self, mri: AMri, config_dir: AConfigDir, template_designs: ATemplateDesigns = "", initial_design: AInitialDesign = "", use_git: AUseGit = True, description: ADescription = "", ) -> None: super().__init__(mri=mri, description=description) assert os.path.isdir(config_dir), "%s is not a directory" % config_dir self.config_dir = config_dir self.initial_design = initial_design self.use_git = use_git self.template_designs = template_designs self.git_config: Tuple[str, ...] if use_git: if check_git_version("1.7.2"): self.git_email = os.environ["USER"] + "@" + socket.gethostname( ) self.git_name = "Malcolm" self.git_config = ( "-c", "user.name=%s" % self.git_name, "-c", 'user.email="%s"' % self.git_email, ) else: self.git_config = () # last saved layout and exports self.saved_visibility = None self.saved_exports = None # ((name, AttributeModel/MethodModel, setter, needs_context)) self._current_part_fields = () self._subscriptions: List[Subscribe] = [] self.port_info: Dict[APartName, List[PortInfo]] = {} self.part_exportable: Dict[Part, List[AAttributeName]] = {} # TODO: turn this into "exported attribute modified" self.context_modified: Dict[Part, Set[str]] = {} self.part_modified: Dict[Part, PartModifiedInfo] = {} # The attributes our part has published self.our_config_attributes: Dict[str, AttributeModel] = {} # The reportable infos we are listening for self.info_registry.add_reportable(PartModifiedInfo, self.update_modified) # Update queue of exportable fields self.info_registry.add_reportable(PartExportableInfo, self.update_exportable) # Create a port for ourself self.field_registry.add_attribute_model( "mri", StringMeta( "A port for giving our MRI to things that might use us", tags=[Port.BLOCK.source_port_tag(self.mri)], ).create_attribute_model(self.mri), ) # Create a layout table attribute for setting block positions self.layout = TableMeta.from_table( LayoutTable, "Layout of child blocks", Widget.FLOWGRAPH, writeable=["x", "y", "visible"], ).create_attribute_model() self.set_writeable_in(self.layout, ss.READY) self.field_registry.add_attribute_model("layout", self.layout, self.set_layout) # Create a design attribute for loading an existing layout self.design = ChoiceMeta("Design name to load", tags=[config_tag(), Widget.COMBO.tag() ]).create_attribute_model() self.field_registry.add_attribute_model("design", self.design, self.set_design) self.set_writeable_in(self.design, ss.READY) # Create an export table for mirroring exported fields self.exports = TableMeta.from_table( ExportTable, "Exported fields of child blocks", writeable=list(ExportTable.call_types), ).create_attribute_model() # Overwrite the sources meta to be a ChoiceArrayMeta self.exports.meta.elements["source"] = ChoiceArrayMeta( "Name of the block.field to export", writeable=True, tags=[Widget.COMBO.tag()], ) self.set_writeable_in(self.exports, ss.READY) self.field_registry.add_attribute_model("exports", self.exports, self.set_exports) # Create read-only indicator for when things are modified self.modified = BooleanMeta("Whether the design is modified", tags=[Widget.LED.tag() ]).create_attribute_model() self.field_registry.add_attribute_model("modified", self.modified) # Create the save method self.set_writeable_in(self.field_registry.add_method_model(self.save), ss.READY) def _run_git_cmd(self, *args, **kwargs): # Run git command, don't care if it fails, logging the output cwd = kwargs.get("cwd", self.config_dir) if self.use_git: try: output = subprocess.check_output( ("git", ) + self.git_config + args, cwd=cwd) except subprocess.CalledProcessError as e: self.log.warning("Git command failed: %s\n%s", e, e.output) return None else: self.log.debug("Git command completed: %s", output) return output def do_init(self): super().do_init() # Try and make it a git repo, don't care if it fails self._run_git_cmd("init") # List the config_dir and add to choices self._set_layout_names() # If given a default config, load this if self.initial_design: self.do_load(self.initial_design, init=True) else: # This will trigger all parts to report their layout, making sure # the layout table has a valid value. This will also call # self._update_block_endpoints() self.set_default_layout() def set_default_layout(self): self.set_layout(LayoutTable([], [], [], [], [])) def set_layout(self, value): """Set the layout table value. Called on attribute put""" # Can't do this with changes_squashed as it will call update_modified # from another thread and deadlock. Need RLock.is_owned() from update_* part_info = self.run_hooks( LayoutHook(p, c, self.port_info, value) for p, c in self.create_part_contexts(only_visible=False).items()) with self.changes_squashed: layout_parts = LayoutInfo.filter_parts(part_info) name, mri, x, y, visible = [], [], [], [], [] for part_name, layout_infos in layout_parts.items(): for layout_info in layout_infos: name.append(part_name) mri.append(layout_info.mri) x.append(layout_info.x) y.append(layout_info.y) visible.append(layout_info.visible) layout_table = LayoutTable(name, mri, x, y, visible) visibility_changed = layout_table.visible != self.layout.value.visible self.layout.set_value(layout_table) if self.saved_visibility is None: # First write of table, set layout and exports saves self.saved_visibility = layout_table.visible self.saved_exports = self.exports.value # Force visibility changed so we update_block_endpoints # even if there weren't any visible visibility_changed = True if visibility_changed: self.update_modified() self.update_exportable() # Part visibility changed, might have attributes or methods # that we need to hide or show self.update_block_endpoints() def set_exports(self, value): # Validate for export_name in value.export: assert CAMEL_RE.match(export_name), ("Field %r is not camelCase" % export_name) with self.changes_squashed: self.exports.set_value(value) self.update_modified() self.update_block_endpoints() def update_modified(self, part: Part = None, info: PartModifiedInfo = None) -> None: with self.changes_squashed: if part: assert info, "No info to update part" # Update the alarm for the given part self.part_modified[part] = info # Find the modified alarms for each visible part message_list = [] only_modified_by_us = True for part_name, visible in zip(self.layout.value.name, self.layout.value.visible): part = self.parts[part_name] info = self.part_modified.get(part, None) if visible and info: for name, message in sorted(info.modified.items()): # Attribute flagged as been modified, is it by the # context we passed to the part? if name in self.context_modified.get(part, {}): message = "(We modified) %s" % (message, ) else: only_modified_by_us = False message_list.append(message) # Add in any modification messages from the layout and export tables if self.layout.value.visible != self.saved_visibility: message_list.append("layout changed") only_modified_by_us = False if self.exports.value != self.saved_exports: message_list.append("exports changed") only_modified_by_us = False if message_list: if only_modified_by_us: severity = AlarmSeverity.NO_ALARM else: severity = AlarmSeverity.MINOR_ALARM alarm = Alarm(severity, AlarmStatus.CONF_STATUS, "\n".join(message_list)) self.modified.set_value(True, alarm=alarm) else: self.modified.set_value(False) def update_exportable(self, part: Part = None, info: PartExportableInfo = None) -> None: with self.changes_squashed: if part: assert info, "No info to update part" self.part_exportable[part] = info.names self.port_info[part.name] = info.port_infos # If we haven't saved visibility yet these have been called # during do_init, so don't update block endpoints yet, this will # be done as a batch at the end of do_init if self.saved_visibility is not None: # Find the exportable fields for each visible part names = [] for part in self.parts.values(): fields = self.part_exportable.get(part, []) for attr_name in fields: names.append("%s.%s" % (part.name, attr_name)) changed_names = set(names).symmetric_difference( self.exports.meta.elements["source"].choices) changed_exports = changed_names.intersection( self.exports.value.source) self.exports.meta.elements["source"].set_choices(names) # Update the block endpoints if anything currently exported is # added or deleted if changed_exports: self.update_block_endpoints() def update_block_endpoints(self): if self._current_part_fields: for name, child, _, _ in self._current_part_fields: self._block.remove_endpoint(name) for state, state_writeable in self._children_writeable.items(): state_writeable.pop(child, None) self._current_part_fields = tuple(self._get_current_part_fields()) for name, child, writeable_func, needs_context in self._current_part_fields: self.add_block_field(name, child, writeable_func, needs_context) def add_part(self, part: Part) -> None: super().add_part(part) # Strip out the config tags of what we just added, as we will be # saving them ourself for name, field, _, _ in self.field_registry.fields.get(part, []): if isinstance(field, AttributeModel): tags = field.meta.tags if get_config_tag(tags): # Strip off the "config" tags from attributes field.meta.set_tags(without_config_tags(tags)) self.our_config_attributes[name] = field def add_initial_part_fields(self): # Only add our own fields to start with, the rest will be added on load for name, child, writeable_func, needs_context in self.field_registry.fields[ None]: self.add_block_field(name, child, writeable_func, needs_context) def _get_current_part_fields(self): # Clear out the current subscriptions for subscription in self._subscriptions: controller = self.process.get_controller(subscription.path[0]) unsubscribe = Unsubscribe(subscription.id) unsubscribe.set_callback(subscription.callback) controller.handle_request(unsubscribe) self._subscriptions = [] # Find the mris of parts mris = {} invisible = set() for part_name, mri, visible in zip(self.layout.value.name, self.layout.value.mri, self.layout.value.visible): if visible: mris[part_name] = mri else: invisible.add(part_name) # Add fields from parts that aren't invisible for part_name, part in self.parts.items(): if part_name not in invisible: for data in self.field_registry.fields.get(part, []): yield data # Add exported fields from visible parts for source, export_name in self.exports.value.rows(): part_name, attr_name = source.rsplit(".", 1) part = self.parts[part_name] # If part is visible, get its mri mri = mris.get(part_name, None) if mri and attr_name in self.part_exportable.get(part, []): if not export_name: export_name = attr_name export, setter = self._make_export_field( mri, attr_name, export_name) yield export_name, export, setter, False def _make_export_field(self, mri, attr_name, export_name): controller = self.process.get_controller(mri) path = [mri, attr_name] label = camel_to_title(export_name) ret = {} def update_field(response): if not isinstance(response, Delta): # Return or Error is the end of our subscription, log and ignore self.log.debug("Export got response %r", response) return if not ret: # First call, create the initial object export = deserialize_object(response.changes[0][1]) if isinstance(export, AttributeModel): def setter(v): context = Context(self.process) context.put(path, v) # Strip out tags that we shouldn't export # TODO: need to strip out port tags too... export.meta.set_tags( without_config_tags( without_group_tags(export.meta.tags))) ret["setter"] = setter else: def setter_star_args(*args): context = Context(self.process) context.post(path, *args) ret["setter"] = setter_star_args # Regenerate label export.meta.set_label(label) ret["export"] = export else: # Subsequent calls, update it with self.changes_squashed: for change in response.changes: ret["export"].apply_change(*change) subscription = Subscribe(path=path, delta=True) subscription.set_callback(update_field) self._subscriptions.append(subscription) # When we have waited for the subscription, the first update_field # will have been called controller.handle_request(subscription).wait() return ret["export"], ret["setter"] def create_part_contexts(self, only_visible=True): part_contexts = super().create_part_contexts() if only_visible: for part_name, visible in zip(self.layout.value.name, self.layout.value.visible): part = self.parts[part_name] if not visible: part_contexts.pop(part) else: part_contexts[part].set_notify_dispatch_request( part.notify_dispatch_request) return part_contexts # Allow CamelCase for arguments as they will be exposed in the Block Method # noinspection PyPep8Naming @add_call_types def save(self, designName: ASaveDesign = "") -> None: """Save the current design to file""" self.try_stateful_function(ss.SAVING, ss.READY, self.do_save, designName) def do_save(self, design=""): if not design: design = self.design.value assert design, "Please specify save design name when saving from new" assert not design.startswith( "template_"), "Cannot save over a template" structure = OrderedDict() attributes = structure.setdefault("attributes", OrderedDict()) # Add the layout table layout = attributes.setdefault("layout", OrderedDict()) for name, mri, x, y, visible in self.layout.value.rows(): layout_structure = OrderedDict() layout_structure["x"] = x layout_structure["y"] = y layout_structure["visible"] = visible layout[name] = layout_structure # Add the exports table exports = attributes.setdefault("exports", OrderedDict()) for source, export in self.exports.value.rows(): exports[source] = export # Add other attributes for name, attribute in self.our_config_attributes.items(): attributes[name] = attribute.value # Add any structure that a child part wants to save structure["children"] = self.run_hooks( SaveHook(p, c) for p, c in self.create_part_contexts(only_visible=False).items()) text = json_encode(structure, indent=2) filename = self._validated_config_filename(design) if filename.startswith("/tmp"): self.log.warning("Saving to tmp directory %s" % filename) with open(filename, "w") as f: f.write(text) # Run a sync command to make sure we flush this file to disk subprocess.call("sync") # Try and commit the file to git, don't care if it fails self._run_git_cmd("add", filename) msg = "Saved %s %s" % (self.mri, design) self._run_git_cmd("commit", "--allow-empty", "-m", msg, filename) self._mark_clean(design) def _set_layout_names(self, extra_name=None): names = [""] dir_name = self._make_config_dir() for f in os.listdir(dir_name): if os.path.isfile(os.path.join(dir_name, f)) and f.endswith(".json"): names.append(f.split(".json")[0]) if extra_name and str(extra_name) not in names: names.append(str(extra_name)) names.sort() if os.path.isdir(self.template_designs): for f in sorted(os.listdir(self.template_designs)): assert f.startswith("template_") and f.endswith(".json"), ( "Template design %s/%s should start with 'template_' " "and end with .json" % (self.template_designs, f)) t_name = f.split(".json")[0] if t_name not in names: names.append(t_name) self.design.meta.set_choices(names) def _validated_config_filename(self, name): """Make config dir and return full file path and extension Args: name (str): Filename without dir or extension Returns: str: Full path including extension """ if name.startswith("template_"): # Load from templates dir dir_name = self.template_designs else: # Load from config dir dir_name = self._make_config_dir() filename = os.path.join(dir_name, name.split(".json")[0] + ".json") return filename def _make_config_dir(self): dir_name = os.path.join(self.config_dir, self.mri) try: os.mkdir(dir_name) except OSError: # OK if already exists, if not then it will fail on write anyway pass return dir_name def set_design(self, value): value = self.design.meta.validate(value) self.try_stateful_function(ss.LOADING, ss.READY, self.do_load, value) def do_load(self, design: str, init: bool = False) -> None: """Load a design name, running the child LoadHooks. Args: design: Name of the design json file, without extension init: Passed to the LoadHook to tell the children if this is being run at Init or not """ if design: filename = self._validated_config_filename(design) with open(filename, "r") as f: text = f.read() structure = json_decode(text) else: structure = {} # Attributes and Children used to be merged, support this attributes = structure.get("attributes", structure) children = structure.get("children", structure) # Set the layout table name, mri, x, y, visible = [], [], [], [], [] for part_name, d in attributes.get("layout", {}).items(): name.append(part_name) mri.append("") x.append(d["x"]) y.append(d["y"]) visible.append(d["visible"]) self.set_layout(LayoutTable(name, mri, x, y, visible)) # Set the exports table source, export = [], [] for source_name, export_name in attributes.get("exports", {}).items(): source.append(source_name) export.append(export_name) self.exports.set_value(ExportTable(source, export)) # Set other attributes our_values = { k: v for k, v in attributes.items() if k in self.our_config_attributes } block = self.block_view() block.put_attribute_values(our_values) # Run the load hook to get parts to load their own structure self.run_hooks( LoadHook(p, c, children.get(p.name, {}), init) for p, c in self.create_part_contexts(only_visible=False).items()) self._mark_clean(design, init) def _mark_clean(self, design, init=False): with self.changes_squashed: self.saved_visibility = self.layout.value.visible self.saved_exports = self.exports.value # Now we are clean, modified should clear if not init: # Don't clear at init, because some things may not be # clean at init self.part_modified = {} self.update_modified() self._set_layout_names(design) self.design.set_value(design) self.update_block_endpoints()
class StatefulController(BasicController): """A controller that implements `StatefulStates`""" # The state_set that this controller implements state_set = ss() def __init__(self, mri: AMri, description: ADescription = "") -> None: super().__init__(mri, description) self._children_writeable: ChildrenWriteable = {} self.state = ChoiceMeta( "StateMachine State of Block", self.state_set.possible_states, tags=[Widget.MULTILINETEXTUPDATE.tag()] # Start DISABLING so we can immediately go to DISABLED ).create_attribute_model(ss.DISABLING) self.field_registry.add_attribute_model("state", self.state) self.field_registry.add_method_model(self.disable) self.set_writeable_in(self.field_registry.add_method_model(self.reset), ss.DISABLED, ss.FAULT) self.transition(ss.DISABLED) self.register_hooked(ProcessStartHook, self.init) self.register_hooked(ProcessStopHook, self.halt) def set_writeable_in(self, field, *states): # Field has defined when it should be writeable, just check that # this is valid for this state_set for state in states: assert (state in self.state_set.possible_states ), "State %s is not one of the valid states %s" % ( state, self.state_set.possible_states, ) for state in self.state_set.possible_states: state_writeable = self._children_writeable.setdefault(state, {}) state_writeable[field] = state in states def create_part_contexts(self) -> Dict[Part, Context]: part_contexts = OrderedDict() assert self.process, "No attached process" for part in self.parts.values(): part_contexts[part] = Context(self.process) return part_contexts def init(self): self.try_stateful_function(ss.RESETTING, ss.READY, self.do_init) def do_init(self): self.run_hooks( InitHook(part, context) for part, context in self.create_part_contexts().items()) def halt(self): self.run_hooks( HaltHook(part, context) for part, context in self.create_part_contexts().items()) self.disable() def disable(self): self.try_stateful_function(ss.DISABLING, ss.DISABLED, self.do_disable) def do_disable(self): self.run_hooks( DisableHook(part, context) for part, context in self.create_part_contexts().items()) def reset(self): self.try_stateful_function(ss.RESETTING, ss.READY, self.do_reset) def do_reset(self): self.run_hooks( ResetHook(part, context) for part, context in self.create_part_contexts().items()) def go_to_error_state(self, exception): if self.state.value != ss.FAULT: self.transition(ss.FAULT, str(exception)) def check_field_writeable(self, field): try: super().check_field_writeable(field) except NotWriteableError as e: msg = "%s, maybe because Block state = %s" % (e, self.state.value) raise NotWriteableError(msg) def transition(self, state, message=""): """Change to a new state if the transition is allowed Args: state (str): State to transition to message (str): Message if the transition is to a fault state """ with self.changes_squashed: initial_state = self.state.value if self.state_set.transition_allowed(initial_state=initial_state, target_state=state): self.log.debug("%s: Transitioning from %s to %s", self.mri, initial_state, state) if state == ss.DISABLED: alarm = Alarm.invalid("Disabled") elif state == ss.FAULT: alarm = Alarm.major(message) else: alarm = Alarm() self.update_health(self, HealthInfo(alarm)) self.state.set_value(state) self.state.set_alarm(alarm) for child, writeable in self._children_writeable[state].items( ): child.meta.set_writeable(writeable) else: raise TypeError("Cannot transition from %s to %s" % (initial_state, state)) def try_stateful_function(self, start_state, end_state, func, *args, **kwargs): try: self.transition(start_state) func(*args, **kwargs) self.transition(end_state) except Exception as e: # pylint:disable=broad-except self.log.debug( "Exception running %s %s %s transitioning from %s to %s", func, args, kwargs, start_state, end_state, exc_info=True, ) self.go_to_error_state(e) raise def add_block_field(self, name, child, writeable_func, needs_context): super().add_block_field(name, child, writeable_func, needs_context) # If we don't have a writeable func it can never be writeable if writeable_func is None: return # If we have already registered an explicit set then we are done for state in self.state_set.possible_states: state_writeable = self._children_writeable.get(state, {}) if child in state_writeable: return # Field is writeable but has not defined when it should be # writeable, so calculate it from the possible states states = [ state for state in self.state_set.possible_states if state not in (ss.DISABLING, ss.DISABLED) ] for state in self.state_set.possible_states: state_writeable = self._children_writeable.setdefault(state, {}) state_writeable[child] = state in states
def make_meta(subtyp, description, tags, writeable=True, labels=None): if subtyp == "enum": meta = ChoiceMeta(description, labels) elif subtyp == "bit": meta = BooleanMeta(description) elif subtyp in ("uint", ""): meta = NumberMeta("uint32", description) elif subtyp in ("int", "pos"): meta = NumberMeta("int32", description) elif subtyp == "scalar": meta = NumberMeta("float64", description) elif subtyp == "lut": meta = StringMeta(description) else: raise ValueError("Unknown subtype %r" % subtyp) meta.set_writeable(writeable) tags.append(meta.default_widget().tag()) meta.set_tags(tags) return meta
def test_to_dict(self): bm = ChoiceMeta("desc", ["a", "b"], label="name") assert bm.to_dict() == self.serialized