Motion Tutorial

You should already know how to create a Part that attaches Attributes and Methods to a Block. The Blocks we have made in previous tutorials are quite simple and low level and might correspond to the interface provided by EPICS devices: a collection of Attributes that we can set and simple Methods we can call that cause the device to operate in a particular way. We call Blocks like this the Hardware Layer. To get more complicated functionality we need to compose these together into a higher level Device Layer containing Blocks that will control a number of child Hardware Blocks to synchronise them and use them for a particular application.

In this tutorial we will build a simple Device Block that represents motion controller hardware moving two motors. A pair of counter Blocks will serve as “motors”.

Creating Device Blocks

Let’s dive right in and look at the Process Definition ./malcolm/modules/demo/DEMO-MOTION.yaml:

# Define a directory to store config in
- builtin.defines.tmp_dir:
    name: config_dir

# Create some Blocks
- demo.blocks.motion_block:
    mri: MOTION
    config_dir: $(config_dir)

# Add a webserver
- web.blocks.web_server_block:
    mri: WEB

First we create a temporary directory to hold saved configs. In production we would point this to a persistent file-system, but here we ask Malcolm to create a directory somewhere in /tmp for us.

Apart from the web server block, all that we do here is load a single Motion Block. We have to look at ./malcolm/modules/demo/blocks/motion_block.yaml to see what one of those does:

# Define the parameters this Block will take
- builtin.parameters.string:
    name: mri
    description: Malcolm resource id of the Block

- builtin.parameters.string:
    name: config_dir
    description: Where to store saved configs

- builtin.defines.docstring:
    value: |
      Device Block that controls 2 child counters to simulate a motor controller
      with move methods

- builtin.controllers.ManagerController:
    mri: $(mri)
    config_dir: $(config_dir)
    description: $(docstring)

# Add the Counter blocks
- demo.blocks.counter_block:
    mri: $(mri):COUNTERX

- demo.blocks.counter_block:
    mri: $(mri):COUNTERY

# Add Parts to use the Counter blocks within Motion block
- demo.parts.CounterMovePart:
    name: x
    mri: $(mri):COUNTERX

- demo.parts.CounterMovePart:
    name: y
    mri: $(mri):COUNTERY

We instantiate two Counter blocks ($(mir):COUNTERX and $(mir):COUNTERY) and instantiate two CounterMoveParts (x and y) that will connect to them. We then use a ManagerController to construct our Block.

This tree of Blocks is probably better viewed as a diagram:

digraph motion_child_connections { newrank=true; // Sensible ranking of clusters bgcolor=transparent compound=true node [fontname=Arial fontsize=10 shape=rect style=filled fillcolor="#8BC4E9"] graph [fontname=Arial fontsize=10] edge [fontname=Arial fontsize=10 arrowhead=vee] subgraph cluster_device { label="Device Layer" style=filled color=lightgrey subgraph cluster_motion { label="MOTION" ranksep=0.1 color=white motion_c [label="ManagerController"] x [label=<CounterMovePart<BR/>name: 'x'>] y [label=<CounterMovePart<BR/>name: 'y'>] motion_c -> x [style=invis] motion_c -> y [style=invis] } } subgraph cluster_hardware { label="Hardware Layer" style=filled color=lightgrey subgraph cluster_counterx { label="COUNTERX" color=white counterx_c [label="BasicController"] counterx_p [label="CounterPart"] counterx_c -> counterx_p [style=invis] } subgraph cluster_countery { label="COUNTERY" color=white countery_c [label="BasicController"] countery_p [label="CounterPart"] countery_c -> countery_p [style=invis] } } x -> counterx_c [lhead=cluster_counterx minlen=3 style=dashed] y -> countery_c [lhead=cluster_countery minlen=3 style=dashed] }

Note

Blocks in the Device layer create their child Blocks. This is because their children have only one logical parent, the Device they belong to.

Controller for Device Blocks

All Blocks in the Device Layer control child Blocks. A Controller that provides functionality aimed at managing child Blocks is called a ManagerController. It provides functionality like Load/Save, disabling and enabling child Blocks, and a state machine to protect against these functions being called at the wrong time. Settings of its children are managed using Designs which can be swapped between at runtime.

It has the following Attributes:

Attribute

Description

health

If something goes wrong, this will show the error message, otherwise it has the value “OK”

state

The current state the Block is in, determines which Methods and Attributes are writeable at present

layout

A Table of child Blocks, whether they are currently visible (enabled), and their X and Y positions for visualization in the web GUI

design

The current saved design name. It is a choice (populated at startup) of all the design names that have previously been saved. When selected it will restore the settings of the Block and its children to the contents of the design file

exports

A Table of extra fields that can be promoted up to the parent. (Use to be demonstrated in a later tutorial.)

modified

A read-only LED indicating if any of our settings or our children have changed since the last save

And the following Methods:

Method

Description

disable

Disable any functionality, connections, and polling that the Block is currently doing

reset

If Disabled or in Fault state, clear any errors and restart any connections and polling that the Block normally does

save

Save the attributes of the parent and immediate children to a JSON file with the given design name, in a directory specified when the ManagerController was created.

And implements the following StateMachine:

digraph { newrank=true; // Sensible ranking of clusters bgcolor=transparent compound=true rankdir=LR node [fontname=Arial fontsize=10 shape=rect style=filled fillcolor="#8BC4E9"] graph [fontname=Arial fontsize=11] edge [fontname=Arial fontsize=10 arrowhead=vee] Fault [fillcolor="#F03232"] Disabled [fillcolor="#AAAAAA"] subgraph cluster_normal { Ready [fillcolor="#BBE7BB"] Ready -> Saving [label="save()"] Saving -> Ready [weight=0] Ready -> Loading [label="put\ndesign"] Loading -> Ready [weight=0] Resetting -> Ready } Resetting -> Disabling [ltail=cluster_normal label="disable()"] Resetting -> Fault [ltail=cluster_normal label="on_error"] Fault -> Resetting [label="reset()"] Fault -> Disabling [label="disable()"] Disabling -> Fault [label="on_error"] Disabling -> Disabled Disabled -> Resetting [label="reset()"] label="Unlabelled transitions take place in response to internal actions\n Labelled transitions are triggered externally."; labelloc=bottom; }

Adding functionality to a ManagerController

In the previous tutorials we have implemented a subclass of Part to add functionality to our Controller. In this tutorial we have to manage a child Block, so we will subclass ChildPart, which has functions to manage a single child Block, like Load/Save.

The aim of this tutorial is to make something that looks like a motion controller, so we will make our Part expose a single method “moveX” (or “moveY”) that will “move” it’s child Block by doing a Put to the counter value.

We instantiate two parts, so here are some of the Methods and Attributes created:

digraph motion_controllers_and_parts { newrank=true; // Sensible ranking of clusters bgcolor=transparent node [fontname=Arial fontsize=10 shape=rect style=filled fillcolor="#8BC4E9"] graph [fontname=Arial fontsize=11] edge [fontname=Arial fontsize=10 arrowhead=none] controller [label=<ManagerController<BR/>mri: 'MOTION'>] mpart1 [label=<CounterMovePart<BR/>name: 'x'>] mpart2 [label=<CounterMovePart<BR/>name: 'y'>] subgraph cluster_control { label="Control" labelloc="b" controller -> mpart1 controller -> mpart2 } block [label=<Block<BR/>mri: 'MOTION'>] design [label=<Attribute<BR/>name: 'design'>] save [label=<Method<BR/>name: 'save'>] moveX [label=<Method<BR/>name: 'moveX'>] moveY [label=<Method<BR/>name: 'moveY'>] subgraph cluster_view { label="View" labelloc="b" block -> design block -> save block -> moveX block -> moveY } {rank=same;controller block} {rank=same;mpart1 mpart2 design save moveX moveY} mpart1 -> moveX [style=dashed] mpart2 -> moveY [style=dashed] controller -> design [style=dashed] controller -> save [style=dashed] controller -> block [arrowhead=vee dir=from style=dashed label=produces] }

Lets take a look at ./malcolm/modules/demo/parts/countermovepart.py to see how it does this:

import time

from annotypes import Anno, add_call_types

from malcolm.core import PartRegistrar
from malcolm.modules import builtin

# Pull re-used annotypes into our namespace in case we are subclassed
APartName = builtin.parts.APartName
AMri = builtin.parts.AMri

with Anno("The demand value to move our counter motor to"):
    ADemand = float
with Anno("The amount of time to get to the demand position"):
    ADuration = float

# How long between ticks of the "motor" position while moving
UPDATE_TICK = 0.1


# We will set these attributes on the child block, so don't save them
@builtin.util.no_save("counter")
class CounterMovePart(builtin.parts.ChildPart):
    """Provides control of a `counter_block` within a `ManagerController`"""

    def __init__(self, name: APartName, mri: AMri) -> None:
        super().__init__(name, mri, stateful=False, initial_visibility=True)

    def setup(self, registrar: PartRegistrar) -> None:
        super().setup(registrar)
        # Method
        registrar.add_method_model(self.move, self.name + "Move", needs_context=True)

    @add_call_types
    def move(
        self, context: builtin.hooks.AContext, demand: ADemand, duration: ADuration = 0
    ) -> None:
        """Move the counter to the demand value, taking duration seconds like
        a motor would do"""
        start = time.time()
        child = context.block_view(self.mri)
        distance = demand - child.counter.value
        remaining = duration
        # "Move" the motor, ticking at UPDATE_TICK rate
        while remaining > 0:
            child.counter.put_value(demand - distance * remaining / duration)
            context.sleep(min(remaining, UPDATE_TICK))
            remaining = start + duration - time.time()
        # Final move to make sure we end up at the right place
        child.counter.put_value(demand)

The first thing to note is the imports, we are pulling in a malcolm module builtin. Anything that can be instantiated in a YAML file lives in a module rather than in malcolm.core. By convention we import modules by name into our namespace rather than importing from them. This makes it clear where things come from in the code. We also explicitly pull a couple of annotypes into our namespace (as explained below) for annotating __init__, and create an annotype that we will use in our move() Method.

Moving on to the class definition, we see a decorator no_save. This defines the set of Attributes of our child Counter Block that we will set with our Methods and shouldn’t appear in the design when we save(). In this case “counter” is the only Attribute we set and therefore don’t want to save.

The CounterMovePart subclasses ChildPart, but this time we do some extra work in the __init__ method. Whenever we override __init__ it is important to call the __init__ that we have just overridden, and that is what the super call does. This is a Python construct that lets us reliably call methods of our superclass that we have just overridden, even if multiple inheritance is used. If someone instantiates CounterMovePart, then super(CounterMovePart, self).__init__ will return the __init__ function of ChildPart, bound so that self does not need to be passed into it.

Note

It’s not necessary to understand what super does, but it is necessary to use it when you need to call the method you have just overridden, otherwise your class may not behave correctly if subclassed and multiple inheritance is used.

When overriding an __init__ method we need to know what arguments to pass to the base class and their types. Looking at the docs and sources of ChildPart.__init we can see:

class malcolm.modules.builtin.parts.ChildPart(name: Anno(name='APartName', typ=<class 'str'>, description='The name of the Part within the Controller'), mri: Anno(name='AMri', typ=<class 'str'>, description='Malcolm resource id of child object'), initial_visibility: Optional[Anno(name='AInitialVisibility', typ=<class 'bool'>, description='Whether the part is initially visible with no config loaded, None means only if child Source/Sink Ports connect to another Block')] = None, stateful: Anno(name='AStateful', typ=<class 'bool'>, description='If the child is a StatefulController then this should be True') = True)[source]
Parameters
  • name (str) – The name of the Part within the Controller

  • mri (str) – Malcolm resource id of child object

  • initial_visibility (bool) – Whether the part is initially visible with no config loaded, None means only if child Source/Sink Ports connect to another Block

  • stateful (bool) – If the child is a StatefulController then this should be True

    def __init__(
        self,
        name: APartName,
        mri: AMri,
        initial_visibility: UInitialVisibility = None,
        stateful: AStateful = True,
    ) -> None:

We need to pass name and mri straight down to the superclass, but we know that a Counter Block is not stateful, so we can pass stateful=False, and we want the child Block to be immediately visible as soon as the parent Block is initialized, so we pass initial_visibility=True.

We also add a type comment to the __init__ method that tells anyone using the Part what parameters should be passed to the initializer. As we are passing the arguments straight down to the superclass, we can reuse the annotypes, which means we will inherit the description assigned to them.

Note

We pull APartName and AMri into our namespace before using it in the __init__ type comment. This is so that anyone who subclasses us can easily reuse the same annotypes without having to go up to our parent to get them.

In setup() we export a single Method, but as we are expecting to have many of these Parts in a single Controller, we prefix the Method name with the name of the Part, so it is unique in the Block. For our example, we have two CounterMoveParts, x and y, so the resulting Block should have xMove() and yMove() Methods. The needs_context=True argument tells Malcolm that when the move Method is called, it should be passed a Context object as the first argument. This utility object makes us a Block view so we can interact with our child Block.

Finally we define the move() Method. As well as the Context we requested, it takes arguments demand and duration which are described by annotypes. We use the context to create a Block view of the Counter child Block, then repeatedly get its counter Attribute view and call Attribute.put_value on it so that the Counter Block sets its counter value to a number of values, ending up at demand. Each put_value waits for completion (which is almost instant).

How it looks in the GUI

Let’s run up the demo and see how it looks in the GUI:

[me@mypc pymalcolm]$ pipenv run imalcolm malcolm/modules/demo/DEMO-MOTION.yaml
Loading malcolm...
Python 3.7.2 (default, Jan 20 2020, 11:03:41)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.19.0 -- An enhanced Interactive Python. Type '?' for help.


Welcome to iMalcolm.

self.mri_list:
    ['MOTION:COUNTERX', 'MOTION:COUNTERY', 'MOTION', 'WEB']

# To create a view of an existing Block
block = self.block_view("<mri>")

# To create a proxy of a Block in another Malcolm
self.make_proxy("<client_comms_mri>", "<mri>")
block = self.block_view("<mri>")

# To view state of Blocks in a GUI
!firefox localhost:8008

In [1]:

Open http://localhost:8008 in your favourite browser and click on the “…” button next to “Select a root block” to select the MOTION Block. You will see the MOTION Block appear in the left hand pane:

../_images/motion_0.png

If you click on the “Edit” button next to “Layout”, then click “Auto Layout”, you will see the children of the MOTION Block, our two counter Blocks controlled by parts named x and y:

../_images/motion_1.png

Let’s try a motor move. If you select the “x” Block by left clicking on it, a right hand pane will pop out showing a view of the counter Block just like we saw in the previous tutorial. If you expand the section on the left titled “X Move”, you will see a method that takes a arguments “Demand”, “Duration”, and an “X MOVE” button to run the method. Fill in a demand value and duration and hit the button and you will see the counter value change:

../_images/motion_2.png

Deleting Blocks

Let’s pretend that a motor is broken and we don’t want to expose “X Move” any more. We can do this by hiding it from the layout, so it doesn’t contribute to the Parent Block any more. If we click on “x” and hit the delete key, or start dragging it to the bin that will appear in the bottom right of the screen, then it will disappear. You will now see that the “X Move” Method has vanished:

../_images/motion_3.png

This mechanism can be used to enable and disable Parts at runtime without restarting Malcolm, and it opens the way for swapping between dynamic configurations.

Load/Save

To swap between dynamic configurations we use the concept of designs. The state of the parent and it’s immediate children can be saved to a file and restored at a later date. These configurations are stored on a per Block basis in the config_dir specified in the top level YAML file.

If you look at the left hand sign of the screen you should see a “Modified” LED which should be lit. This is saying that there are unsaved changes since either startup or the last save. This is true, as we have deleted one of our parts. Let’s save our current design by expanding the “Save” section, filling in the design name, and hitting “SAVE”:

../_images/motion_4.png

When you press save, Malcolm will check if it writing to a temporary directory, and if so warn us what it has written with a message like:

malcolm.modules.builtin.controllers.managercontroller.ManagerController.MOTION: Saving to tmp directory /tmp/tmpHGUXzA/MOTION/only_y.json

If we take a look at /tmp/*/MOTION/only_y.json we can see what got saved:

{
  "attributes": {
    "layout": {
      "x": {
        "x": 0.0,
        "y": 0.0,
        "visible": false
      },
      "y": {
        "x": 0.0,
        "y": 58.30000305175781,
        "visible": true
      }
    },
    "exports": {}
  },
  "children": {
    "x": {
      "delta": 1.0
    },
    "y": {
      "delta": 1.0
    }
  }
}

We can see that x is marked as not visible, and for both x and y we have saved the “delta” value of the Blocks.

Note

The counter value didn’t get saved because we specified it in no_save. If we had left this out, then each time the motor moved the modified LED would light, and when we restored a design the ‘motor’ would move!

Let’s try making some changes. Click on the Palette and drag “x” back onto the screen. Click on “y” and change delta, then we will see this:

../_images/motion_5.png

Notice that “X Move” has reappeared, and tooltip for the Modified LED gives a message about y.delta.value changing as well as the layout change. If we re-select the Design to be “only_y” you should see the Modified LED clear, the X Block vanish, and the “X Move” Method disappear too.

Conclusion

This tutorial has given us an understanding of how ManagerControllers can be used to create Device Blocks that control child Hardware Blocks, how ChildParts can be turned on and off at runtime, and settings loaded and saved to make designs. In the next tutorial we will see another more complicated Device Block that implements a configure/run type interface.