Rock

the Robot Construction Kit

Action State Machines

The result of this tutorial can be found in bundles/tutorials if you followed the instructions at the bottom of this page. While the tip of the master branch contains the accumulated result of all the tutorials, you can get the specific result of this one by checking out the action_state_machine tag with

git checkout action_state_machine

So far, we have seen how to define and compose network of components. The rest of the tutorials will guide through the process of coordinating them, i.e. how to make the system switch between them when some conditions are reached, thus creating temporal combinations of behaviours.

This tutorial will guide you through:

  • how to combine the network configurations created in profiles into more complex behaviours by using state machines
  • how these state machines can then be combined with each other

The action state machines presented in this tutorial are not specific to Syskit, but are available to any Roby action. You may want to also read the Roby documentation for more information about them. However, Syskit does extend these state machines to give some Syskit-specific functionality.

Defining State Machines

In the previous tutorials, we have mainly built two behaviours for our rocks:

  • one random movement behaviour
  • one target-following behaviour

Let’s now assume that we want a rock to move randomly, but stay confined in a certain area around the origin. This can be achieved with what we currently have:

  • if within the area, move randomly
  • if on the border, go back to origin

The ‘go back to origin’ behaviour being obviously ‘follow the origin frame’. Let’s define it first generically:

define 'to_origin', Tutorials::RockFollower.
  use(TutFollower::Task, TutSensor::TransformerTask).
  use_frames('target' => 'world', 'world' => 'world')

Such a switch between behaviours can classically be represented by a state machine. This is one mean to express such combination of behaviours in Syskit as well. We will see how to write such a state machine now.

Action state machines are defined on action interfaces. We have already seen those as the place for the use_profile statements. Let’s now add our state machine to it (in our case models/actions/tut-transformer/main.rb).

class Main < Actions::Interface
  use_profile Tutorials::RockWithTransformer

  describe 'move randomly as long as in a 10 meter square \
    around origin, move back to origin when passing the border'
  action_state_machine 'fenced_random_move' do
    # We need to call #state to transform an action / definition
    # into a proper state
    random = state random_def
    origin = state to_origin_def.use(rock1_dev)
  
    # We start by moving randomly
    start random
    # We transition each time the rock passes the fence
    transition ???, origin
    # And transition back when we reach the origin
    transition ???, random
  end
end

Notice above that there are two “???” placeholders in the two transition statements. We have to specify transitions, but how ?

Since we are under Roby, the transitions are obviously going to be caused by Roby events. However, there is still the issue of defining them.

One constraint is that we should not modify the behaviours to be modified to account for this very particular combination of them (we want them to be reusable, therefore free of this very specific use case), we’ll define two monitoring networks. One will check whether the rock passes the fence and the other whether it is within its target pose.

Let’s create the fence monitor first in models/blueprints/fence_monitor.rb

require 'rock/models/blueprints/pose'
module Tutorials
  # Given a position provider, checks whether this position crosses a specified
  # boundary around the origin
  class FenceMonitor < Syskit::Composition
    # The size of the allowed square
    argument :fence_size, :default => 10
    # Emitted when the tracked pose passes the required fence towards the
    # outside
    event :passed_fence_outwards

    # The position provider that we will be monitoring
    add Base::PositionSrv, :as => 'position'

    # Tests if the given position is within the fence
    #
    # @param [Types::Base::Position] the position
    # @return [Boolean]
    def in_fence?(p)
      p.x.abs < fence_size && p.y.abs < fence_size
    end

    # This script context allows us to have a proper access to ports in
    # children. A raw Roby poll block would be possible, but would require that
    # we know the type of the final position component.
    script do
      # Get a reader object that allows us to read the position
      position_r = position_child.position_samples_port.reader
      # The block given to #poll is executed once per Roby execution cycle
      # (usually once per 100ms)
      poll do
        # If we have a position
        if p = position_r.read_new
          # p is a RigidBodyState, we only need the position part of it
          p = p.position
          # Initialize @last_p if it is not initialized yet
          @last_p ||= p
          # If we cross the border, emit the event
          if in_fence?(@last_p) && !in_fence?(p)
            passed_fence_outwards_event.emit
          end
          @last_p = p
        end
      end
    end
  end
end

Then, the target reached monitor in models/blueprints/position_within_threshold_monitor.rb

require 'rock/models/blueprints/pose'
module Tutorials
  # Composition that checks whether a position is within a certain threshold
  # from a target
  class PositionWithinThresholdMonitor < Syskit::Composition
    # The target pose, as Types::Base::Position
    argument :target
    # The target threshold
    argument :threshold, :default => 1
    # Emitted when the tracked pose is within threshold of the target
    event :reached

    # The position provider we are monitoring
    add Base::PositionSrv, :as => 'position'

    # Tests if the given position is within threshold of the target
    #
    # @param [Types::Base::Position] the position
    # @return [Boolean]
    def in_threshold?(p)
      (p - target).norm < threshold
    end

    # A script context. This allows us to have a proper access to ports in
    # children. A raw Roby poll block would be possible, but would require that
    # we know the type of the final position component.
    script do
      # Get a data reader on the position provider
      position_r = position_child.position_samples_port.reader
      # The block given to #poll is executed once per Roby execution cycle
      # (usually once per 100ms)
      poll do
        # Emit if we have a position and it is within threshold
        if (p = position_r.read_new) && in_threshold?(p.position)
          reached_event.emit
        end
      end
    end
  end
end

Let’s now integrate the monitors in our state machine:

describe('move randomly as long as in a 10 meter square around origin, move back
to origin when passing the border')
action_state_machine 'fenced_random_move' do
  # We need to call #state to transform an action / definition into a proper
  # state
  random = state random_def
  # 'task' is used for tasks that are not used as states
  fence_monitor = task Tutorials::FenceMonitor.use(rock1_dev)
  # We need this monitor only in the random state
  random.depends_on fence_monitor

  origin = state to_origin_def.use(rock1_dev)
  # 'task' is used for tasks that are not used as states
  target_monitor = task Tutorials::PositionWithinThresholdMonitor.
    use(rock1_dev).with_arguments('target' => Types::Base::Position.new(0, 0, 0))
  # We need this monitor only in the origin state
  origin.depends_on target_monitor

  # We start by moving randomly
  start random
  # We transition each time the rock passes the fence
  transition random, fence_monitor.passed_fence_outwards_event, origin
  # And transition back when we reach the origin
  transition origin, target_monitor.reached_event, random
end

Runtime Behaviour

You can now start the state machine with

syskit run -rtut-transformer fenced_random_move!

and visualize the resulting behaviour using roby-display (for the Syskit part) and rock-display (for the components)

Combining State Machines

When one creates a state machine, it becomes an action. It can therefore be used in another state machine (and so on, and so forth), as for instance:

describe "combining state machines"
action_state_machine 'main' do
  movement = state fenced_random_move
  # Use a hypothetical go_and_recharge action which can be any Roby action
  # (Syskit network, state machine, ...)
  recharge = state go_and_recharge
  # And monitor the battery using a Syskit definition
  battery_monitor = state battery_monitor_def

  start movement
  movement.depends_on battery_monitor
  transition movement, battery_monitor.battery_low_event, recharge
  transition recharge, recharge.success_event, movement
end

Summary

This tutorial has shown you how:

  • combine the networks defined in the Syskit profiles as states in a state machine, thus allowing to combine them in a temporal way
  • define separate monitoring networks, and use them to trigger the transition
  • how state machines can then become part of bigger state machines

These action state machines are great to define the nominal operations of a robot, i.e. its mission (or parts of it). However, they would become very cumbersome to handle errors, or thing that can happen from any state. One cannot add transitions from any state to the error handlers.

The next tutorial will deal with a refinement of the monitors we have manually defined here, introducing data monitors that allow to generate errors when patterns are found in the data. Then, we will see how to handle these errors in a graceful way.