class DefaultController(Controller): Resetting = Hook() Disabling = Hook() @method_takes() def disable(self): try: self.transition(sm.DISABLING, "Disabling") self.do_disable() self.transition(sm.DISABLED, "Done Disabling") except Exception as e: # pylint:disable=broad-except self.log_exception("Fault occurred while Disabling") self.transition(sm.FAULT, str(e)) raise def do_disable(self): self.run_hook(self.Disabling, self.create_part_tasks()) @method_only_in(sm.DISABLED, sm.FAULT) def reset(self): try: self.transition(sm.RESETTING, "Resetting") self.do_reset() self.transition(self.stateMachine.AFTER_RESETTING, "Done Resetting") except Exception as e: # pylint:disable=broad-except self.log_exception("Fault occurred while Resetting") self.transition(sm.FAULT, str(e)) raise def do_reset(self): self.run_hook(self.Resetting, self.create_part_tasks())
class DefaultController(Controller): # The stateMachine that this controller implements stateMachine = sm() Reset = Hook() """Called at reset() to reset all parts to a known good state Args: task (Task): The task used to perform operations on child blocks """ Disable = Hook() """Called at disable() to stop all parts updating their attributes Args: task (Task): The task used to perform operations on child blocks """ def do_initial_reset(self): self.process.spawn(self.reset) @method_takes() def disable(self): self.try_stateful_function(sm.DISABLING, sm.DISABLED, self.do_disable) def do_disable(self): self.run_hook(self.Disable, self.create_part_tasks()) @method_writeable_in(sm.DISABLED, sm.FAULT) def reset(self): self.try_stateful_function( sm.RESETTING, self.stateMachine.AFTER_RESETTING, self.do_reset) def do_reset(self): self.run_hook(self.Reset, self.create_part_tasks()) def go_to_error_state(self, exception): if self.state.value != sm.FAULT: self.log_exception("Fault occurred while running stateful function") self.transition(sm.FAULT, str(exception)) def try_stateful_function(self, start_state, end_state, func, *args, **kwargs): try: self.transition(start_state, start_state) func(*args, **kwargs) self.transition(end_state, end_state) except Exception as e: # pylint:disable=broad-except self.go_to_error_state(e) raise
class StatefulController(BasicController): """A controller that implements `StatefulStates`""" # The stateSet that this controller implements stateSet = ss() # {state (str): {Meta/MethodMeta/Attribute: writeable (bool)} _children_writeable = None # Attributes state = None Init = Hook() """Called when this controller is told to start by the process Args: context (Context): The context that should be used to perform operations on child blocks """ Halt = Hook() """Called when this controller is told to halt Args: context (Context): The context that should be used to perform operations on child blocks """ Reset = Hook() """Called at reset() to reset all parts to a known good state Args: context (Context): The context that should be used to perform operations on child blocks """ Disable = Hook() """Called at disable() to stop all parts updating their attributes Args: context (Context): The context that should be used to perform operations on child blocks """ def __init__(self, process, parts, params): self._children_writeable = {} super(StatefulController, self).__init__(process, parts, params) self.transition(ss.DISABLED) def create_attribute_models(self): """MethodModel that should provide Attribute instances for Block Yields: tuple: (string name, Attribute, callable put_function). """ for y in super(StatefulController, self).create_attribute_models(): yield y # Create read-only attribute for current state string meta = ChoiceMeta( "State of Block", self.stateSet.possible_states, label="State") self.state = meta.create_attribute_model(ss.DISABLING) yield "state", self.state, None @Process.Init def init(self): self.try_stateful_function(ss.RESETTING, ss.READY, self.do_init) def do_init(self): self.run_hook(self.Init, self.create_part_contexts()) @Process.Halt def halt(self): self.run_hook(self.Halt, self.create_part_contexts()) self.disable() @method_takes() def disable(self): self.try_stateful_function(ss.DISABLING, ss.DISABLED, self.do_disable) def do_disable(self): self.run_hook(self.Disable, self.create_part_contexts()) @method_writeable_in(ss.DISABLED, ss.FAULT) def reset(self): self.try_stateful_function(ss.RESETTING, ss.READY, self.do_reset) def do_reset(self): self.run_hook(self.Reset, self.create_part_contexts()) def go_to_error_state(self, exception): if self.state.value != ss.FAULT: self.transition(ss.FAULT, str(exception)) 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.stateSet.transition_allowed( initial_state=initial_state, target_state=state): self.log.debug( "Transitioning from %s to %s", 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, alarm) self.state.set_value(state) self.state.set_alarm(alarm) for child, writeable in self._children_writeable[state].items(): if isinstance(child, AttributeModel): child.meta.set_writeable(writeable) elif isinstance(child, MethodModel): child.set_writeable(writeable) for element in child.takes.elements.values(): element.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.go_to_error_state(e) raise def add_block_field(self, name, child, writeable_func): super(StatefulController, self).add_block_field( name, child, writeable_func) # Set children_writeable dict if isinstance(child, AttributeModel): states = child.meta.writeable_in else: states = child.writeable_in if states: # Field has defined when it should be writeable, just check that # this is valid for this stateSet for state in states: assert state in self.stateSet.possible_states, \ "State %s is not one of the valid states %s" % \ (state, self.stateSet.possible_states) elif writeable_func is not None: # 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.stateSet.possible_states if state not in (ss.DISABLING, ss.DISABLED)] else: # Field is never writeable, so will never need to change state return for state in self.stateSet.possible_states: state_writeable = self._children_writeable.setdefault(state, {}) state_writeable[child] = state in states
class ManagerController(StatefulController): """RunnableDevice implementer that also exposes GUI for child parts""" stateSet = ss() Layout = Hook() """Called when layout table set and at init to update child layout Args: context (Context): The context that should be used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from Layout hook layout_table (Table): A possibly partial set of changes to the layout table that should be acted on Returns: [`LayoutInfo`] - the child layout resulting from this change """ Load = Hook() """Called at load() or revert() to load child settings from a structure Args: context (Context): The context that should be used to perform operations on child blocks structure (dict): {part_name: part_structure} where part_structure is the return from Save hook """ Save = Hook() """Called at save() to serialize child settings into a dict structure Args: context (Context): The context that should be used to perform operations on child blocks Returns: dict: serialized version of the child that could be loaded from """ # Attributes layout = None design = None exports = None modified = None def __init__(self, process, parts, params): super(ManagerController, self).__init__(process, parts, params) # last saved layout and exports self.saved_visibility = None self.saved_exports = None # ((name, AttributeModel/MethodModel, setter)) self._current_part_fields = () # [Subscribe] self._subscriptions = [] # {part_name: [PortInfo]} self.port_info = {} # {part: [attr_name]} self.part_exportable = {} # {part: Alarm} self.part_modified = {} # Whether to do updates self._do_update = True def _run_git_cmd(self, *args): # Run git command, don't care if it fails, logging the output try: output = subprocess.check_output( ("git",) + args, cwd=self.params.configDir) except subprocess.CalledProcessError as e: self.log.warning("Git command failed: %s\n%s", e, e.output) else: self.log.debug("Git command completed: %s", output) def create_attribute_models(self): for data in super(ManagerController, self).create_attribute_models(): yield data assert os.path.isdir(self.params.configDir), \ "%s is not a directory" % self.params.configDir if not os.path.isdir(os.path.join(self.params.configDir, ".git")): # Try and make it a git repo, don't care if it fails self._run_git_cmd("init") self._run_git_cmd("commit", "--allow-empty", "-m", "Created repo") # Create writeable attribute table for the layout info we need elements = OrderedDict() elements["name"] = StringArrayMeta("Name of layout part") elements["mri"] = StringArrayMeta("Malcolm full name of child block") elements["x"] = NumberArrayMeta( "float64", "X Coordinate of child block") elements["y"] = NumberArrayMeta( "float64", "Y Coordinate of child block") elements["visible"] = BooleanArrayMeta("Whether child block is visible") layout_table_meta = TableMeta( "Layout of child blocks", elements=elements, tags=[widget("flowgraph")]) layout_table_meta.set_writeable_in(ss.READY) self.layout = layout_table_meta.create_attribute_model() yield "layout", self.layout, self.set_layout # Create writeable attribute for loading an existing layout design_meta = ChoiceMeta( "Design name to load", tags=[config(), widget("combo")]) design_meta.set_writeable_in(ss.READY) self.design = design_meta.create_attribute_model() yield "design", self.design, self.set_design # Create writeable attribute table for the exported fields elements = OrderedDict() elements["name"] = ChoiceArrayMeta("Name of exported block.field") elements["exportName"] = StringArrayMeta( "Name of the field within current block") exports_table_meta = TableMeta( "Exported fields of child blocks", tags=[widget("table")], elements=elements) exports_table_meta.set_writeable_in(ss.READY) self.exports = exports_table_meta.create_attribute_model() yield "exports", self.exports, self.set_exports # Create read-only indicator for when things are modified modified_meta = BooleanMeta( "Whether the design is modified", tags=[widget("led")]) self.modified = modified_meta.create_attribute_model() yield "modified", self.modified, None def do_init(self): # This will do an initial poll of the exportable parts, # so don't update here super(ManagerController, self).do_init() # List the configDir and add to choices self._set_layout_names() # 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_layout(Table(self.layout.meta)) # If given a default config, load this if self.params.initialDesign: self.do_load(self.params.initialDesign) def set_layout(self, value): """Set the layout table value. Called on attribute put""" # If it isn't a table, make it one if not isinstance(value, Table): value = Table(self.layout.meta, value) # Can't do this with changes_squashed as it will call update_modified # from another thread and deadlock part_info = self.run_hook( self.Layout, self.create_part_contexts(only_visible=False), self.port_info, value) with self.changes_squashed: layout_table = Table(self.layout.meta) layout_parts = LayoutInfo.filter_parts(part_info) for name, layout_infos in layout_parts.items(): assert len(layout_infos) == 1, \ "%s returned more than 1 layout infos" % name layout_parts[name] = layout_infos[0] layout_table.name = list(layout_parts) layout_table.mri = [i.mri for i in layout_parts.values()] layout_table.x = [i.x for i in layout_parts.values()] layout_table.y = [i.y for i in layout_parts.values()] layout_table.visible = [i.visible for i in layout_parts.values()] try: np.testing.assert_equal( layout_table.visible, self.layout.value.visible) except AssertionError: visibility_changed = True else: visibility_changed = False 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.to_dict() 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): with self.changes_squashed: self.exports.set_value(value) self.update_modified() self._update_block_endpoints() def update_modified(self, part=None, alarm=None): with self.changes_squashed: # Update the alarm for the given part if part: self.part_modified[part] = alarm # 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): if visible: alarm = self.part_modified.get(self.parts[part_name], None) if alarm: # Part flagged as been modified, is it by us? if alarm.severity: only_modified_by_us = False message_list.append(alarm.message) # Add in any modification messages from the layout and export tables try: np.testing.assert_equal( self.layout.value.visible, self.saved_visibility) except AssertionError: message_list.append("layout changed") only_modified_by_us = False try: np.testing.assert_equal( self.exports.value.to_dict(), self.saved_exports) except AssertionError: 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=None, fields=None, port_infos=None): with self.changes_squashed: if part: self.part_exportable[part] = fields self.port_info[part.name] = port_infos # 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["name"].choices) changed_exports = changed_names.intersection( self.exports.value.name) self.exports.meta.elements["name"].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 in self._current_part_fields: self.add_block_field(name, child, writeable_func) def initial_part_fields(self): # Don't return any fields to start with, these will be added on load() return iter(()) 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, 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 in self.parts: if part_name not in invisible: for data in self.part_fields[part_name]: yield data # Add exported fields from visible parts for name, export_name in zip( self.exports.value.name, self.exports.value.exportName): part_name, attr_name = name.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) yield export_name, export, setter def _make_export_field(self, mri, attr_name): controller = self.process.get_controller(mri) path = [mri, attr_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 ret["export"] = deserialize_object(response.changes[0][1]) context = Context(self.process) if isinstance(ret["export"], AttributeModel): def setter(v): context.put(path, v) else: def setter(*args): context.post(path, *args) ret["setter"] = setter else: # Subsequent calls, update it with self.changes_squashed: for cp, value in response.changes: ob = ret["export"] for p in cp[:-1]: ob = ob[p] getattr(ob, "set_%s" % cp[-1])(value) subscription = Subscribe(path=path, delta=True, 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(ManagerController, self).create_part_contexts() if only_visible: for part_name, visible in zip( self.layout.value.name, self.layout.value.visible): if not visible: part_contexts.pop(self.parts[part_name]) return part_contexts @method_writeable_in(ss.READY) @method_takes( "design", StringMeta( "Name of design to save, if different from current design"), "") def save(self, params): """Save the current design to file""" self.try_stateful_function( ss.SAVING, ss.READY, self.do_save, params.design) def do_save(self, design=""): if not design: design = self.design.value assert design, "Please specify save design name when saving from new" structure = OrderedDict() # Add the layout table part_layouts = {} for name, x, y, visible in sorted( zip(self.layout.value.name, self.layout.value.x, self.layout.value.y, self.layout.value.visible)): layout_structure = OrderedDict() layout_structure["x"] = x layout_structure["y"] = y layout_structure["visible"] = visible part_layouts[name] = layout_structure structure["layout"] = OrderedDict() for part_name in self.parts: if part_name in part_layouts: structure["layout"][part_name] = part_layouts[part_name] # Add the exports table structure["exports"] = OrderedDict() for name, export_name in sorted( zip(self.exports.value.name, self.exports.value.exportName)): structure["exports"][name] = export_name # Add any structure that a child part wants to save part_structures = self.run_hook( self.Save, self.create_part_contexts(only_visible=False)) for part_name, part_structure in sorted(part_structures.items()): structure[part_name] = part_structure text = json_encode(structure, indent=2) filename = self._validated_config_filename(design) with open(filename, "w") as f: f.write(text) if os.path.isdir(os.path.join(self.params.configDir, ".git")): # 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 = [""] if extra_name: names.append(extra_name) 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]) 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 extensio """ 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.params.configDir, 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): filename = self._validated_config_filename(design) with open(filename, "r") as f: text = f.read() structure = json_decode(text) # Set the layout table layout_table = Table(self.layout.meta) for part_name, part_structure in structure.get("layout", {}).items(): layout_table.append([ part_name, "", part_structure["x"], part_structure["y"], part_structure["visible"]]) self.set_layout(layout_table) # Set the exports table exports_table = Table(self.exports.meta) for name, export_name in structure.get("exports", {}).items(): exports_table.append([name, export_name]) self.exports.set_value(exports_table) # Run the load hook to get parts to load their own structure self.run_hook(self.Load, self.create_part_contexts(only_visible=False), structure) self._mark_clean(design) def _mark_clean(self, design): with self.changes_squashed: self.saved_visibility = self.layout.value.visible self.saved_exports = self.exports.value.to_dict() # Now we are clean, modified should clear self.part_modified = OrderedDict() self.update_modified() self._set_layout_names(design) self.design.set_value(design) self._update_block_endpoints()
class ManagerController(DefaultController): """RunnableDevice implementer that also exposes GUI for child parts""" # The stateMachine that this controller implements stateMachine = sm() ReportOutports = Hook() """Called before Layout to get outport info from children Args: task (Task): The task used to perform operations on child blocks Returns: [`OutportInfo`] - the type and value of each outport of the child """ Layout = Hook() """Called when layout table set and at init to update child layout Args: task (Task): The task used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from Layout hook layout_table (Table): A possibly partial set of changes to the layout table that should be acted on Returns: [`LayoutInfo`] - the child layout resulting from this change """ Load = Hook() """Called at load() or revert() to load child settings from a structure Args: task (Task): The task used to perform operations on child blocks structure (dict): {part_name: part_structure} where part_structure is the return from Save hook """ Save = Hook() """Called at save() to serialize child settings into a dict structure Args: task (Task): The task used to perform operations on child blocks Returns: dict: serialized version of the child that could be loaded from """ # attributes layout = None layout_name = None # {part_name: part_structure} of currently loaded settings load_structure = None def create_attributes(self): for data in super(ManagerController, self).create_attributes(): yield data # Make a table for the layout info we need columns = OrderedDict() columns["name"] = StringArrayMeta("Name of layout part") columns["mri"] = StringArrayMeta("Malcolm full name of child block") columns["x"] = NumberArrayMeta("float64", "X Coordinate of child block") columns["y"] = NumberArrayMeta("float64", "Y Coordinate of child block") columns["visible"] = BooleanArrayMeta("Whether child block is visible") layout_table_meta = TableMeta("Layout of child blocks", columns=columns) layout_table_meta.set_writeable_in(sm.EDITABLE) self.layout = layout_table_meta.make_attribute() yield "layout", self.layout, self.set_layout self.layout_name = ChoiceMeta( "Saved layout name to load", []).make_attribute() self.layout_name.meta.set_writeable_in( self.stateMachine.AFTER_RESETTING) yield "layoutName", self.layout_name, self.load_layout assert os.path.isdir(self.params.configDir), \ "%s is not a directory" % self.params.configDir def set_layout(self, value): # If it isn't a table, make it one if not isinstance(value, Table): value = Table(self.layout.meta, value) part_info = self.run_hook(self.ReportOutports, self.create_part_tasks()) part_info = self.run_hook( self.Layout, self.create_part_tasks(), part_info, value) layout_table = Table(self.layout.meta) for name, layout_infos in LayoutInfo.filter_parts(part_info).items(): assert len(layout_infos) == 1, \ "%s returned more than 1 layout infos" % name layout_info = layout_infos[0] row = [name, layout_info.mri, layout_info.x, layout_info.y, layout_info.visible] layout_table.append(row) self.layout.set_value(layout_table) def do_reset(self): super(ManagerController, self).do_reset() # This will trigger all parts to report their layout, making sure the # layout table has a valid value self.set_layout(Table(self.layout.meta)) # List the configDir and add to choices self._set_layout_names() # If we have no load_structure (initial reset) define one if self.load_structure is None: if self.params.defaultConfig: self.load_layout(self.params.defaultConfig) else: self.load_structure = self._save_to_structure() @method_writeable_in(sm.READY) def edit(self): self.transition(sm.EDITABLE, "Layout editable") def go_to_error_state(self, exception): if self.state.value == sm.EDITABLE: # If we got a save or revert exception, don't go to fault self.log_exception("Fault occurred while trying to save/revert") else: super(ManagerController, self).go_to_error_state(exception) @method_writeable_in(sm.EDITABLE) @method_takes( "layoutName", StringMeta( "Name of layout to save to, if different from current layoutName"), None) def save(self, params): self.try_stateful_function( sm.SAVING, self.stateMachine.AFTER_RESETTING, self.do_save, params.layoutName) def do_save(self, layout_name=None): if not layout_name: layout_name = self.layout_name.value structure = self._save_to_structure() text = json_encode(structure, indent=2) filename = self._validated_config_filename(layout_name) open(filename, "w").write(text) self._set_layout_names(layout_name) self.layout_name.set_value(layout_name) self.load_structure = structure def _set_layout_names(self, extra_name=None): names = [] if extra_name: names.append(extra_name) 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]) self.layout_name.meta.set_choices(names) @method_writeable_in(sm.EDITABLE) def revert(self): self.try_stateful_function( sm.REVERTING, self.stateMachine.AFTER_RESETTING, self.do_revert) def do_revert(self): self._load_from_structure(self.load_structure) 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 extensio """ 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.params.configDir, 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 load_layout(self, value): # TODO: race condition if we get 2 loads at once... # Do we need a Loading state? filename = self._validated_config_filename(value) text = open(filename, "r").read() structure = json_decode(text) self._load_from_structure(structure) self.layout_name.set_value(value) def _save_to_structure(self): structure = OrderedDict() structure["layout"] = OrderedDict() for name, x, y, visible in sorted( zip(self.layout.value.name, self.layout.value.x, self.layout.value.y, self.layout.value.visible)): layout_structure = OrderedDict() layout_structure["x"] = x layout_structure["y"] = y layout_structure["visible"] = visible structure["layout"][name] = layout_structure for part_name, part_structure in sorted(self.run_hook( self.Save, self.create_part_tasks()).items()): structure[part_name] = part_structure return structure def _load_from_structure(self, structure): table = Table(self.layout.meta) for part_name, part_structure in structure["layout"].items(): table.append([part_name, "", part_structure["x"], part_structure["y"], part_structure["visible"]]) self.set_layout(table) self.run_hook(self.Load, self.create_part_tasks(), structure)
class RunnableController(ManagerController): """RunnableDevice implementer that also exposes GUI for child parts""" # The stateSet that this controller implements stateSet = ss() Validate = Hook() """Called at validate() to check parameters are valid Args: context (Context): The context that should be used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part validate() method_takes() decorator Returns: [`ParameterTweakInfo`] - any parameters tweaks that have occurred to make them compatible with this part. If any are returned, Validate will be re-run with the modified parameters. """ ReportStatus = Hook() """Called before Validate, Configure, PostRunArmed and Seek hooks to report the current configuration of all parts Args: context (Context): The context that should be used to perform operations on child blocks Returns: [`Info`] - any configuration Info objects relevant to other parts """ Configure = Hook() """Called at configure() to configure child block for a run Args: context (Context): The context that should be used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator Returns: [`Info`] - any Info objects that need to be passed to other parts for storing in attributes """ PostConfigure = Hook() """Called at the end of configure() to store configuration info calculated in the Configure hook Args: context (Context): The context that should be used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from Configure hook """ Run = Hook() """Called at run() to start the configured steps running Args: context (Context): The context that should be used to perform operations on child blocks update_completed_steps (callable): If part can report progress, this part should call update_completed_steps(completed_steps, self) with the integer step value each time progress is updated """ PostRunArmed = Hook() """Called at the end of run() when there are more steps to be run Args: context (Context): The context that should be used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator """ PostRunReady = Hook() """Called at the end of run() when there are no more steps to be run Args: context (Context): The context that should be used to perform operations on child blocks """ Pause = Hook() """Called at pause() to pause the current scan before Seek is called Args: context (Context): The context that should be used to perform operations on child blocks """ Seek = Hook() """Called at seek() or at the end of pause() to reconfigure for a different number of completed_steps Args: context (Context): The context that should be used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator """ Resume = Hook() """Called at resume() to continue a paused scan Args: context (Context): The context that should be used to perform operations on child blocks update_completed_steps (callable): If part can report progress, this part should call update_completed_steps(completed_steps, self) with the integer step value each time progress is updated """ Abort = Hook() """Called at abort() to stop the current scan Args: context (Context): The context that should be used to perform operations on child blocks """ # Attributes completed_steps = None configured_steps = None total_steps = None axes_to_move = None # Params passed to configure() configure_params = None # Shared contexts between Configure, Run, Pause, Seek, Resume part_contexts = None # Configure method_models # {part: configure_method_model} configure_method_models = None # Stored for pause steps_per_run = 0 # Progress reporting dict # {part: completed_steps for that part} progress_updates = None # Queue so that do_run can wait to see why it was aborted and resume if # needed resume_queue = None # Queue so we can wait for aborts to complete abort_queue = None @method_writeable_in(ss.FAULT, ss.DISABLED, ss.ABORTED, ss.ARMED) def reset(self): # Override reset to work from aborted too super(RunnableController, self).reset() def create_attribute_models(self): for data in super(RunnableController, self).create_attribute_models(): yield data # Create sometimes writeable attribute for the current completed scan # step completed_steps_meta = NumberMeta( "int32", "Readback of number of scan steps", tags=[widget("textinput")]) completed_steps_meta.set_writeable_in(ss.PAUSED, ss.ARMED) self.completed_steps = completed_steps_meta.create_attribute_model(0) yield "completedSteps", self.completed_steps, self.set_completed_steps # Create read-only attribute for the number of configured scan steps configured_steps_meta = NumberMeta( "int32", "Number of steps currently configured", tags=[widget("textupdate")]) self.configured_steps = configured_steps_meta.create_attribute_model(0) yield "configuredSteps", self.configured_steps, None # Create read-only attribute for the total number scan steps total_steps_meta = NumberMeta( "int32", "Readback of number of scan steps", tags=[widget("textupdate")]) self.total_steps = total_steps_meta.create_attribute_model(0) yield "totalSteps", self.total_steps, None # Create sometimes writeable attribute for the default axis names axes_to_move_meta = StringArrayMeta( "Default axis names to scan for configure()", tags=[widget("table"), config()]) axes_to_move_meta.set_writeable_in(ss.READY) self.axes_to_move = axes_to_move_meta.create_attribute_model( self.params.axesToMove) yield "axesToMove", self.axes_to_move, self.set_axes_to_move def do_init(self): self.part_contexts = {} # Populate configure args from any child method hooked to Configure. # If we have runnablechildparts, they will call update_configure_args # during do_init self.configure_method_models = {} # Look for all parts that hook into Configure for part, func_name in self._hooked_func_names[self.Configure].items(): if func_name in part.method_models: self.update_configure_args(part, part.method_models[func_name]) super(RunnableController, self).do_init() def do_reset(self): super(RunnableController, self).do_reset() self.configured_steps.set_value(0) self.completed_steps.set_value(0) self.total_steps.set_value(0) def update_configure_args(self, part, configure_model): """Tell controller part needs different things passed to Configure""" with self.changes_squashed: # Update the dict self.configure_method_models[part] = configure_model method_models = list(self.configure_method_models.values()) # Update takes with the things we need default_configure = MethodModel.from_dict( RunnableController.configure.MethodModel.to_dict()) default_configure.defaults["axesToMove"] = self.axes_to_move.value method_models.append(default_configure) # Decorate validate and configure with the sum of its parts self._block.validate.recreate_from_others(method_models) self._block.validate.set_returns(self._block.validate.takes) self._block.configure.recreate_from_others(method_models) def set_axes_to_move(self, value): self.axes_to_move.set_value(value) @method_takes(*configure_args) @method_returns(*validate_args) def validate(self, params, returns): """Validate configuration parameters and return validated parameters. Doesn't take device state into account so can be run in any state """ iterations = 10 # Make some tasks just for validate part_contexts = self.create_part_contexts() # Get any status from all parts status_part_info = self.run_hook(self.ReportStatus, part_contexts) while iterations > 0: # Try up to 10 times to get a valid set of parameters iterations -= 1 # Validate the params with all the parts validate_part_info = self.run_hook( self.Validate, part_contexts, status_part_info, **params) tweaks = ParameterTweakInfo.filter_values(validate_part_info) if tweaks: for tweak in tweaks: params[tweak.parameter] = tweak.value self.log.debug( "Tweaking %s to %s", tweak.parameter, tweak.value) else: # Consistent set, just return the params return params raise ValueError("Could not get a consistent set of parameters") def abortable_transition(self, state): with self._lock: # We might have been aborted just now, so this will fail # with an AbortedError if we were self.part_contexts[self].sleep(0) self.transition(state) @method_takes(*configure_args) @method_writeable_in(ss.READY) def configure(self, params): """Validate the params then configure the device ready for run(). Try to prepare the device as much as possible so that run() is quick to start, this may involve potentially long running activities like moving motors. Normally it will return in Armed state. If the user aborts then it will return in Aborted state. If something goes wrong it will return in Fault state. If the user disables then it will return in Disabled state. """ self.validate(params, params) try: self.transition(ss.CONFIGURING) self.do_configure(params) self.abortable_transition(ss.ARMED) except AbortedError: self.abort_queue.put(None) raise except Exception as e: self.go_to_error_state(e) raise def do_configure(self, params): # These are the part tasks that abort() and pause() will operate on self.part_contexts = self.create_part_contexts() # Tell these contexts to notify their parts that about things they # modify so it doesn't screw up the modified led for part, context in self.part_contexts.items(): context.set_notify_dispatch_request(part.notify_dispatch_request) # So add one for ourself too so we can be aborted self.part_contexts[self] = Context(self.process) # Store the params for use in seek() self.configure_params = params # This will calculate what we need from the generator, possibly a long # call params.generator.prepare() # Set the steps attributes that we will do across many run() calls self.total_steps.set_value(params.generator.size) self.completed_steps.set_value(0) self.configured_steps.set_value(0) # TODO: We can be cleverer about this and support a different number # of steps per run for each run by examining the generator structure self.steps_per_run = self._get_steps_per_run( params.generator, params.axesToMove) # Get any status from all parts part_info = self.run_hook(self.ReportStatus, self.part_contexts) # Run the configure command on all parts, passing them info from # ReportStatus. Parts should return any reporting info for PostConfigure completed_steps = 0 steps_to_do = self.steps_per_run part_info = self.run_hook( self.Configure, self.part_contexts, completed_steps, steps_to_do, part_info, **self.configure_params) # Take configuration info and reflect it as attribute updates self.run_hook(self.PostConfigure, self.part_contexts, part_info) # Update the completed and configured steps self.configured_steps.set_value(steps_to_do) # Reset the progress of all child parts self.progress_updates = {} self.resume_queue = Queue() def _get_steps_per_run(self, generator, axes_to_move): steps = 1 axes_set = set(axes_to_move) for dim in reversed(generator.dimensions): # If the axes_set is empty then we are done if not axes_set: break # Consume the axes that this generator scans for axis in dim.axes: assert axis in axes_set, \ "Axis %s is not in %s" % (axis, axes_to_move) axes_set.remove(axis) # Now multiply by the dimensions to get the number of steps steps *= dim.size return steps @method_writeable_in(ss.ARMED) def run(self): """Run a device where configure() has already be called Normally it will return in Ready state. If setup for multiple-runs with a single configure() then it will return in Armed state. If the user aborts then it will return in Aborted state. If something goes wrong it will return in Fault state. If the user disables then it will return in Disabled state. """ if self.configured_steps.value < self.total_steps.value: next_state = ss.ARMED else: next_state = ss.READY try: self.transition(ss.RUNNING) hook = self.Run going = True while going: try: self.do_run(hook) except AbortedError: self.abort_queue.put(None) # Wait for a response on the resume_queue should_resume = self.resume_queue.get() if should_resume: # we need to resume hook = self.Resume self.log.debug("Resuming run") else: # we don't need to resume, just drop out raise else: going = False self.abortable_transition(next_state) except AbortedError: raise except Exception as e: self.go_to_error_state(e) raise def do_run(self, hook): self.run_hook(hook, self.part_contexts, self.update_completed_steps) self.abortable_transition(ss.POSTRUN) completed_steps = self.configured_steps.value if completed_steps < self.total_steps.value: steps_to_do = self.steps_per_run part_info = self.run_hook(self.ReportStatus, self.part_contexts) self.completed_steps.set_value(completed_steps) self.run_hook( self.PostRunArmed, self.part_contexts, completed_steps, steps_to_do, part_info, **self.configure_params) self.configured_steps.set_value(completed_steps + steps_to_do) else: self.run_hook(self.PostRunReady, self.part_contexts) def update_completed_steps(self, completed_steps, part): with self._lock: # Update self.progress_updates[part] = completed_steps min_completed_steps = min(self.progress_updates.values()) if min_completed_steps > self.completed_steps.value: self.completed_steps.set_value(min_completed_steps) @method_writeable_in( ss.READY, ss.CONFIGURING, ss.ARMED, ss.RUNNING, ss.POSTRUN, ss.PAUSED, ss.SEEKING) def abort(self): """Abort the current operation and block until aborted Normally it will return in Aborted state. If something goes wrong it will return in Fault state. If the user disables then it will return in Disabled state. """ # Tell _call_do_run not to resume if self.resume_queue: self.resume_queue.put(False) self.try_aborting_function(ss.ABORTING, ss.ABORTED, self.do_abort) def do_abort(self): self.run_hook(self.Abort, self.create_part_contexts()) def try_aborting_function(self, start_state, end_state, func, *args): try: # To make the running function fail we need to stop any running # contexts (if running a hook) or make transition() fail with # AbortedError. Both of these are accomplished here with self._lock: original_state = self.state.value self.abort_queue = Queue() self.transition(start_state) for context in self.part_contexts.values(): context.stop() if original_state not in (ss.READY, ss.ARMED, ss.PAUSED): # Something was running, let it finish aborting try: self.abort_queue.get(timeout=ABORT_TIMEOUT) except TimeoutError: self.log.warning("Timeout waiting while %s" % start_state) with self._lock: # Now we've waited for a while we can remove the error state # for transition in case a hook triggered it rather than a # transition self.part_contexts[self].ignore_stops_before_now() func(*args) self.abortable_transition(end_state) except AbortedError: self.abort_queue.put(None) raise except Exception as e: # pylint:disable=broad-except self.go_to_error_state(e) raise def set_completed_steps(self, completed_steps): """Seek a Armed or Paused scan back to another value Normally it will return in the state it started in. If the user aborts then it will return in Aborted state. If something goes wrong it will return in Fault state. If the user disables then it will return in Disabled state. """ call_with_params(self.pause, completedSteps=completed_steps) @method_writeable_in(ss.ARMED, ss.PAUSED, ss.RUNNING) @method_takes("completedSteps", NumberMeta( "int32", "Step to mark as the last completed step, -1 for current"), -1) def pause(self, params): """Pause a run() so that resume() can be called later. The original call to run() will not be interrupted by pause(), it will with until the scan completes or is aborted. Normally it will return in Paused state. If the user aborts then it will return in Aborted state. If something goes wrong it will return in Fault state. If the user disables then it will return in Disabled state. """ current_state = self.state.value if params.completedSteps < 0: completed_steps = self.completed_steps.value else: completed_steps = params.completedSteps if current_state == ss.RUNNING: next_state = ss.PAUSED else: next_state = current_state assert completed_steps < self.total_steps.value, \ "Cannot seek to after the end of the scan" self.try_aborting_function( ss.SEEKING, next_state, self.do_pause, completed_steps) def do_pause(self, completed_steps): self.run_hook(self.Pause, self.create_part_contexts()) in_run_steps = completed_steps % self.steps_per_run steps_to_do = self.steps_per_run - in_run_steps part_info = self.run_hook(self.ReportStatus, self.part_contexts) self.completed_steps.set_value(completed_steps) self.run_hook( self.Seek, self.part_contexts, completed_steps, steps_to_do, part_info, **self.configure_params) self.configured_steps.set_value(completed_steps + steps_to_do) @method_writeable_in(ss.PAUSED) def resume(self): """Resume a paused scan. Normally it will return in Running state. If something goes wrong it will return in Fault state. """ self.transition(ss.RUNNING) self.resume_queue.put(True) # self.run will now take over def do_disable(self): # Abort anything that is currently running, but don't wait for context in self.part_contexts.values(): context.stop() if self.resume_queue: self.resume_queue.put(False) super(RunnableController, self).do_disable()
class RunnableController(ManagerController): """RunnableDevice implementer that also exposes GUI for child parts""" # The stateMachine that this controller implements stateMachine = sm() Validate = Hook() """Called at validate() to check parameters are valid Args: task (Task): The task used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part validate() method_takes() decorator Returns: [`ParameterTweakInfo`] - any parameters tweaks that have occurred to make them compatible with this part. If any are returned, Validate will be re-run with the modified parameters. """ ReportStatus = Hook() """Called before Validate, Configure, PostRunReady and Seek hooks to report the current configuration of all parts Args: task (Task): The task used to perform operations on child blocks Returns: [`Info`] - any configuration Info objects relevant to other parts """ Configure = Hook() """Called at configure() to configure child block for a run Args: task (Task): The task used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator Returns: [`Info`] - any Info objects that need to be passed to other parts for storing in attributes """ PostConfigure = Hook() """Called at the end of configure() to store configuration info calculated in the Configure hook Args: task (Task): The task used to perform operations on child blocks part_info (dict): {part_name: [Info]} returned from Configure hook """ Run = Hook() """Called at run() to start the configured steps running Args: task (Task): The task used to perform operations on child blocks update_completed_steps (callable): If part can report progress, this part should call update_completed_steps(completed_steps, self) with the integer step value each time progress is updated """ PostRunReady = Hook() """Called at the end of run() when there are more steps to be run Args: task (Task): The task used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator """ PostRunIdle = Hook() """Called at the end of run() when there are no more steps to be run Args: task (Task): The task used to perform operations on child blocks """ Pause = Hook() """Called at pause() to pause the current scan before Seek is called Args: task (Task): The task used to perform operations on child blocks """ Seek = Hook() """Called at seek() or at the end of pause() to reconfigure for a different number of completed_steps Args: task (Task): The task used to perform operations on child blocks completed_steps (int): Number of steps already completed steps_to_do (int): Number of steps we should configure for part_info (dict): {part_name: [Info]} returned from ReportStatus params (Map): Any configuration parameters asked for by part configure() method_takes() decorator """ Resume = Hook() """Called at resume() to continue a paused scan Args: task (Task): The task used to perform operations on child blocks update_completed_steps (callable): If part can report progress, this part should call update_completed_steps(completed_steps, self) with the integer step value each time progress is updated """ Abort = Hook() """Called at abort() to stop the current scan Args: task (Task): The task used to perform operations on child blocks """ # Attributes completed_steps = None configured_steps = None total_steps = None axes_to_move = None # Params passed to configure() configure_params = None # Stored for pause steps_per_run = 0 # Progress reporting dict # {part: completed_steps for that part} progress_reporting = None @method_writeable_in(sm.IDLE) def edit(self): # Override edit to only work from Idle super(RunnableController, self).edit() @method_writeable_in(sm.FAULT, sm.DISABLED, sm.ABORTED, sm.READY) def reset(self): # Override reset to work from aborted and ready too super(RunnableController, self).reset() def create_attributes(self): for data in super(RunnableController, self).create_attributes(): yield data self.completed_steps = NumberMeta( "int32", "Readback of number of scan steps").make_attribute(0) self.completed_steps.meta.set_writeable_in(sm.PAUSED, sm.READY) yield "completedSteps", self.completed_steps, self.set_completed_steps self.configured_steps = NumberMeta( "int32", "Number of steps currently configured").make_attribute(0) yield "configuredSteps", self.configured_steps, None self.total_steps = NumberMeta( "int32", "Readback of number of scan steps").make_attribute(0) yield "totalSteps", self.total_steps, None self.axes_to_move = StringArrayMeta( "Default axis names to scan for configure()").make_attribute( self.params.axesToMove) self.axes_to_move.meta.set_writeable_in(sm.EDITABLE) yield "axesToMove", self.axes_to_move, self.set_axes_to_move def do_reset(self): super(RunnableController, self).do_reset() self._update_configure_args() self.configured_steps.set_value(0) self.completed_steps.set_value(0) self.total_steps.set_value(0) def go_to_error_state(self, exception): if isinstance(exception, AbortedError): self.log_info("Got AbortedError in %s" % self.state.value) else: super(RunnableController, self).go_to_error_state(exception) def _update_configure_args(self): # Look for all parts that hook into Configure configure_funcs = self.Configure.find_hooked_functions(self.parts) method_metas = [] for part, func_name in configure_funcs.items(): method_metas.append(part.method_metas[func_name]) # Update takes with the things we need default_configure = MethodMeta.from_dict( RunnableController.configure.MethodMeta.to_dict()) default_configure.defaults["axesToMove"] = self.axes_to_move.value method_metas.append(default_configure) # Decorate validate and configure with the sum of its parts self.block["validate"].recreate_from_others(method_metas) self.block["validate"].set_returns(self.block["validate"].takes) self.block["configure"].recreate_from_others(method_metas) def set_axes_to_move(self, value): self.axes_to_move.set_value(value) self._update_configure_args() @method_takes(*configure_args) def validate(self, params, returns): iterations = 10 # Make some tasks just for validate part_tasks = self.create_part_tasks() # Get any status from all parts status_part_info = self.run_hook(self.ReportStatus, part_tasks) while iterations > 0: # Try up to 10 times to get a valid set of parameters iterations -= 1 # Validate the params with all the parts validate_part_info = self.run_hook(self.Validate, part_tasks, status_part_info, **params) tweaks = ParameterTweakInfo.filter_values(validate_part_info) if tweaks: for tweak in tweaks: params[tweak.parameter] = tweak.value self.log_debug("Tweaking %s to %s", tweak.parameter, tweak.value) else: # Consistent set, just return the params return params raise ValueError("Could not get a consistent set of parameters") @method_takes(*configure_args) @method_writeable_in(sm.IDLE) def configure(self, params): """Configure for a scan""" self.validate(params, params) self.try_stateful_function(sm.CONFIGURING, sm.READY, self.do_configure, params) def do_configure(self, params): # These are the part tasks that abort() and pause() will operate on self.part_tasks = self.create_part_tasks() # Load the saved settings first self.run_hook(self.Load, self.part_tasks, self.load_structure) # Store the params for use in seek() self.configure_params = params # This will calculate what we need from the generator, possibly a long # call params.generator.prepare() # Set the steps attributes that we will do across many run() calls self.total_steps.set_value(params.generator.size) self.completed_steps.set_value(0) self.configured_steps.set_value(0) # TODO: We can be cleverer about this and support a different number # of steps per run for each run by examining the generator structure self.steps_per_run = self._get_steps_per_run(params.generator, params.axesToMove) # Get any status from all parts part_info = self.run_hook(self.ReportStatus, self.part_tasks) # Use the ProgressReporting classes for ourselves self.progress_reporting = {} # Run the configure command on all parts, passing them info from # ReportStatus. Parts should return any reporting info for PostConfigure completed_steps = 0 steps_to_do = self.steps_per_run part_info = self.run_hook(self.Configure, self.part_tasks, completed_steps, steps_to_do, part_info, **self.configure_params) # Take configuration info and reflect it as attribute updates self.run_hook(self.PostConfigure, self.part_tasks, part_info) # Update the completed and configured steps self.configured_steps.set_value(steps_to_do) def _get_steps_per_run(self, generator, axes_to_move): steps = 1 axes_set = set(axes_to_move) for dim in reversed(generator.dimensions): # If the axes_set is empty then we are done if not axes_set: break # Consume the axes that this generator scans for axis in dim.axes: assert axis in axes_set, \ "Axis %s is not in %s" % (axis, axes_to_move) axes_set.remove(axis) # Now multiply by the dimensions to get the number of steps steps *= dim.size return steps @method_writeable_in(sm.READY) def run(self): """Run an already configured scan""" if self.configured_steps.value < self.total_steps.value: next_state = sm.READY else: next_state = sm.IDLE self.try_stateful_function(sm.RUNNING, next_state, self._call_do_run) def _call_do_run(self): hook = self.Run while True: try: self.do_run(hook) except AbortedError: # Work out if it was an abort or pause state = self.state.value self.log_debug("Do run got AbortedError from %s", state) if state in (sm.SEEKING, sm.PAUSED): # Wait to be restarted task = Task("StateWaiter", self.process) bad_states = [sm.DISABLING, sm.ABORTING, sm.FAULT] try: task.when_matches(self.state, sm.RUNNING, bad_states) except BadValueError: # raise AbortedError so we don't try to transition raise AbortedError() # Restart it hook = self.Resume self.status.set_value("Run resumed") else: # just drop out raise else: return def do_run(self, hook): self.run_hook(hook, self.part_tasks, self.update_completed_steps) self.transition(sm.POSTRUN, "Finishing run") completed_steps = self.configured_steps.value if completed_steps < self.total_steps.value: steps_to_do = self.steps_per_run part_info = self.run_hook(self.ReportStatus, self.part_tasks) self.completed_steps.set_value(completed_steps) self.run_hook(self.PostRunReady, self.part_tasks, completed_steps, steps_to_do, part_info, **self.configure_params) self.configured_steps.set_value(completed_steps + steps_to_do) else: self.run_hook(self.PostRunIdle, self.part_tasks) def update_completed_steps(self, completed_steps, part): # This is run in the child thread, so make sure it is thread safe self.progress_reporting[part] = completed_steps min_completed_steps = min(self.progress_reporting.values()) if min_completed_steps > self.completed_steps.value: self.completed_steps.set_value(min_completed_steps) @method_writeable_in(sm.IDLE, sm.CONFIGURING, sm.READY, sm.RUNNING, sm.POSTRUN, sm.PAUSED, sm.SEEKING) def abort(self): self.try_stateful_function(sm.ABORTING, sm.ABORTED, self.do_abort, self.Abort) def do_abort(self, hook): for task in self.part_tasks.values(): task.stop() self.run_hook(hook, self.create_part_tasks()) for task in self.part_tasks.values(): task.wait() def set_completed_steps(self, completed_steps): params = self.pause.MethodMeta.prepare_input_map( completedSteps=completed_steps) self.pause(params) @method_writeable_in(sm.READY, sm.PAUSED, sm.RUNNING) @method_takes( "completedSteps", NumberMeta("int32", "Step to mark as the last completed step, -1 for current"), -1) def pause(self, params): current_state = self.state.value if params.completedSteps < 0: completed_steps = self.completed_steps.value else: completed_steps = params.completedSteps if current_state == sm.RUNNING: next_state = sm.PAUSED else: next_state = current_state assert completed_steps < self.total_steps.value, \ "Cannot seek to after the end of the scan" self.try_stateful_function(sm.SEEKING, next_state, self.do_pause, completed_steps) def do_pause(self, completed_steps): self.do_abort(self.Pause) in_run_steps = completed_steps % self.steps_per_run steps_to_do = self.steps_per_run - in_run_steps part_info = self.run_hook(self.ReportStatus, self.part_tasks) self.completed_steps.set_value(completed_steps) self.run_hook(self.Seek, self.part_tasks, completed_steps, steps_to_do, part_info, **self.configure_params) self.configured_steps.set_value(completed_steps + steps_to_do) @method_writeable_in(sm.PAUSED) def resume(self): self.transition(sm.RUNNING, "Resuming run")
class HTTPServerComms(ServerComms): """A class for communication between browser and server""" _loop = None _server = None _spawned = None _application = None use_cothread = False ReportHandlers = Hook() """Called at init() to get all the handlers that should make the application Args: context (Context): The context that should be used to perform operations on child blocks loop (IOLoop): The IO loop that the server is running under Returns: [`HandlerInfo`] - any handlers and their regexps that need to form part of the tornado Application """ Publish = Hook() """Called when a new block is added Args: context (Context): The context that should be used to perform operations on child blocks published (list): [mri] list of published Controller mris """ def do_init(self): super(HTTPServerComms, self).do_init() self._loop = IOLoop() part_info = self.run_hook(self.ReportHandlers, self.create_part_contexts(), self._loop) handler_infos = HandlerInfo.filter_values(part_info) handlers = [] for handler_info in handler_infos: handlers.append((handler_info.regexp, handler_info.request_class, handler_info.kwargs)) self._application = Application(handlers) self.start_io_loop() def start_io_loop(self): if self._spawned is None: self._server = HTTPServer(self._application) self._server.listen(int(self.params.port)) self._spawned = self.spawn(self._loop.start) def stop_io_loop(self): if self._spawned: self._loop.add_callback(self._server.stop) self._loop.add_callback(self._loop.stop) self._spawned.wait(timeout=10) self._spawned = None def do_disable(self): super(HTTPServerComms, self).do_disable() self.stop_io_loop() def do_reset(self): super(HTTPServerComms, self).do_reset() self.start_io_loop() @Process.Publish def publish(self, published): if self._spawned: self.run_hook(self.Publish, self.create_part_contexts(), published)
import unittest from malcolm.core import Part, method_takes, Hook Reset = Hook() class MyPart(Part): @method_takes() @Reset def foo(self): pass @method_takes() def bar(self): pass class TestPart(unittest.TestCase): def test_init(self): p = Part("name") assert p.name == "name" def test_non_hooked_methods(self): p = MyPart("") methods = list(p.create_method_models()) assert methods == [("bar", p.method_models["bar"], p.bar)]
class ManagerController(DefaultController): """RunnableDevice implementer that also exposes GUI for child parts""" # hooks Report = Hook() Validate = Hook() Configuring = Hook() PreRun = Hook() Running = Hook() PostRun = Hook() Aborting = Hook() UpdateLayout = Hook() ListOutports = Hook() # default attributes totalSteps = None layout = None # Params passed to configure() configure_params = None def create_attributes(self): self.totalSteps = NumberMeta( "int32", "Readback of number of scan steps").make_attribute(0) yield "totalSteps", self.totalSteps, None self.layout = layout_table_meta.make_attribute() yield "layout", self.layout, self.set_layout def do_reset(self): super(ManagerController, self).do_reset() self.set_layout(Table(layout_table_meta)) def set_layout(self, value): outport_table = self.run_hook(self.ListOutports, self.create_part_tasks()) layout_table = self.run_hook(self.UpdateLayout, self.create_part_tasks(), layout_table=value, outport_table=outport_table) self.layout.set_value(layout_table) def something_create_methods(self): # Look for all parts that hook into the validate method validate_funcs = self.Validating.find_hooked_functions(self.parts) takes_elements = OrderedDict() defaults = OrderedDict() for part_name, func in validate_funcs.items(): self.log_debug("Adding validating parameters from %s", part_name) takes_elements.update(func.MethodMeta.takes.to_dict()) defaults.update(func.MethodMeta.defaults) takes = ElementMap(takes_elements) # Decorate validate and configure with the sum of its parts # No need to copy as the superclass _set_block_children does this self.validate.MethodMeta.set_takes(takes) self.validate.MethodMeta.set_returns(takes) self.validate.MethodMeta.set_defaults(defaults) self.configure.MethodMeta.set_takes(takes) self.validate.MethodMeta.set_defaults(defaults) return super(ManagerController, self).create_methods() @method_takes(*configure_args) @method_returns(*configure_args) def validate(self, params, _): self.do_validate(params) return params def do_validate(self, params): raise NotImplementedError() @method_only_in(sm.IDLE) @method_takes(*configure_args) def configure(self, params): try: # Transition first so no-one else can run configure() self.transition(sm.CONFIGURING, "Configuring", create_tasks=True) # Store the params and set attributes self.configure_params = params self.totalSteps.set_value(params.generator.num) self.block["completedSteps"].set_value(0) # Do the actual configure self.do_configure() self.transition(sm.READY, "Done configuring") except Exception as e: # pylint:disable=broad-except self.log_exception("Fault occurred while Configuring") self.transition(sm.FAULT, str(e)) raise def do_configure(self, start_step=0): # Ask all parts to report relevant info and pass results to anyone # who cares info_table = self.run_hook(self.Report, self.part_tasks) # Pass results to anyone who cares self.run_hook(self.Configuring, self.part_tasks, info_table=info_table, start_step=start_step, **self.configure_params) @method_only_in(sm.READY) def run(self): try: self.transition(sm.PRERUN, "Preparing for run") self._call_do_run() if self.block["completedSteps"].value < self.totalSteps.value: next_state = sm.READY else: next_state = sm.IDLE self.transition(next_state, "Run finished") except StopIteration: self.log_warning("Run aborted") raise except Exception as e: # pylint:disable=broad-except self.log_exception("Fault occurred while Running") self.transition(sm.FAULT, str(e)) raise def _call_do_run(self): try: self.do_run() except StopIteration: # Work out if it was an abort or pause with self.lock: state = self.state.value self.log_debug("Do run got StopIteration from %s", state) if state in (sm.REWINDING, sm.PAUSED): # Wait to be restarted self.log_debug("Waiting for PreRun") task = Task("StateWaiter", self.process) futures = task.when_matches( self.state, sm.PRERUN, [sm.DISABLING, sm.ABORTING, sm.FAULT]) task.wait_all(futures) # Restart it self.do_run() else: # just drop out self.log_debug("We were aborted") raise def do_run(self): self.run_hook(self.PreRun, self.part_tasks) self.transition(sm.RUNNING, "Waiting for scan to complete") self.run_hook(self.Running, self.part_tasks) self.transition(sm.POSTRUN, "Finishing run") self.run_hook(self.PostRun, self.part_tasks) @method_only_in(sm.IDLE, sm.CONFIGURING, sm.READY, sm.PRERUN, sm.RUNNING, sm.POSTRUN, sm.RESETTING, sm.PAUSED, sm.REWINDING) def abort(self): try: self.transition(sm.ABORTING, "Aborting") self.do_abort() self.transition(sm.ABORTED, "Abort finished") except Exception as e: # pylint:disable=broad-except self.log_exception("Fault occurred while Aborting") self.transition(sm.FAULT, str(e)) raise def do_abort(self): for task in self.part_tasks.values(): task.stop() self.run_hook(self.Aborting, self.create_part_tasks()) for task in self.part_tasks.values(): task.wait() @method_only_in(sm.PRERUN, sm.RUNNING) def pause(self): try: self.transition(sm.REWINDING, "Rewinding") current_index = self.block.completedSteps self.do_abort() self.part_tasks = self.create_part_tasks() self.do_configure(current_index) self.transition(sm.PAUSED, "Pause finished") except Exception as e: # pylint:disable=broad-except self.log_exception("Fault occurred while Pausing") self.transition(sm.FAULT, str(e)) raise @method_only_in(sm.READY, sm.PAUSED) @method_takes("steps", NumberMeta("uint32", "Number of steps to rewind"), REQUIRED) def rewind(self, params): current_index = self.block.completedSteps requested_index = current_index - params.steps assert requested_index >= 0, \ "Cannot retrace to before the start of the scan" try: self.transition(sm.REWINDING, "Rewinding") self.block["completedSteps"].set_value(requested_index) self.do_configure(requested_index) self.transition(sm.PAUSED, "Rewind finished") except Exception as e: # pylint:disable=broad-except self.log_exception("Fault occurred while Rewinding") self.transition(sm.FAULT, str(e)) raise @method_only_in(sm.PAUSED) def resume(self): self.transition(sm.PRERUN, "Resuming run")