示例#1
0
class CounterPart(Part):
    # Attribute for the counter value
    counter = None

    def create_attributes(self):
        self.counter = NumberMeta("uint32", "A counter").make_attribute(0)
        yield "counter", self.counter, self.counter.set_value

    @method_takes()
    def zero(self):
        self.counter.set_value(0)

    @method_takes()
    def increment(self):
        self.counter.set_value(self.counter.value + 1)
示例#2
0
class CounterPart(Part):
    # Attribute for the counter value
    counter = None

    def create_attributes(self):
        self.counter = NumberMeta("float64", "A counter").make_attribute()
        yield "counter", self.counter, self.counter.set_value

    @method_takes()
    def zero(self):
        """Zero the counter attribute"""
        self.counter.set_value(0)

    @method_takes()
    def increment(self):
        """Add one to the counter attribute"""
        self.counter.set_value(self.counter.value + 1)
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()
        # Store the params for use in seek()
        self.configure_params = params
        # Set the steps attributes that we will do across many run() calls
        self.total_steps.set_value(params.generator.num)
        self.completed_steps.set_value(0)
        self.configured_steps.set_value(0)
        # TODO: this should come from tne generator
        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 g in reversed(generator.generators):
            # 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 g.position_units:
                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
            for dim in g.index_dims:
                steps *= dim
        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")
示例#4
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")
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")