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())
Example #2
0
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
Example #3
0
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
Example #4
0
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()
Example #5
0
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)
Example #6
0
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()
Example #7
0
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")
Example #8
0
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)
Example #9
0
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")