Skip to content

State Machine Tutorial - Robot Vaccum

The opensourceleg.control module provides functionality for creating and managing finite state machines (FSMs) via the StateMachine class. This tutorial walks you through the implementation of a simple state machine for a robot vaccum cleaner using the finite_state_machine.py example.

Overview

This tutorial demonstrates how to define states, transitions, and criteria for switching between states using the StateMachine class.

Define Transition Criteria

First, we define the transition criteria of the FSM after importing the pertinent classes. These are functions that return a boolean value based on some condition. You can define any arguments you like, so here we will use the battery_level to determine when to switch states.

from opensourceleg.control.fsm import State, StateMachine
from opensourceleg.logging.logger import Logger
from opensourceleg.utilities import SoftRealtimeLoop


# Transition criteria functions
def cleaning_to_docking(battery_level: float) -> bool:
    """
    Transition from 'Cleaning' to 'Docking' when the battery level is below 20%.
    """
    return battery_level < 20


def charging_to_cleaning(battery_level: float) -> bool:
    """
    Transition from 'Charging' to 'Cleaning' when the battery is fully charged (100%).
    """
    return battery_level == 100

Define States and State Machine

Next, we define three states for our system: cleaning, charging, and docking. For the docking state, we assume that it takes 10 seconds to go from anywhere in the room to the docking station. We add each of the states as a list to a new instance of the StateMachine module and set the docking state as the initial state.

    # Define states
    cleaning_s = State(name="Cleaning")  # State for cleaning
    charging_s = State(name="Charging")  # State for charging
    docking_s = State(name="Docking", minimum_time_in_state=10)  # State for docking, assuming it takes 10 seconds

    # Initialize the state machine with the defined states and the initial state
    fsm = StateMachine(states=[charging_s, cleaning_s, docking_s], initial_state_name=charging_s.name)

Add Transitions Between States

Now that the state machine is built, we need to link the states with transitions. We do this using the add_transition() method. Transitions specify the conditions under which the FSM moves from one state to another. Each transition includes:

  • A source state
  • A destination state
  • An event name
  • A criteria (a function that returns True when the transition should occur)
    # Add transitions between states
    fsm.add_transition(
        source=charging_s,
        destination=cleaning_s,
        event_name="Fully Charged - Beginning Cleaning",
        criteria=charging_to_cleaning,
    )
    fsm.add_transition(
        source=cleaning_s,
        destination=docking_s,
        event_name="Battery Low - Finding Charging Dock",
        criteria=cleaning_to_docking,
    )
    fsm.add_transition(source=docking_s, destination=charging_s, event_name="Docked - Beginning Charging")

    # Initialize the battery level
    battery_level = 50.0  # Start with 50% battery

Main Loop

We initialize the FSM inside a with context and create an instance of SoftRealtimeLoop to simulate our state machine. Each time through the loop, we call fsm.update(battery_level=battery_level), which provides the appropriate inputs to the transition functions.

    # Run the state machine in a soft real-time loop
    with fsm:
        for _t in SoftRealtimeLoop(dt=0.5):  # Loop with a time step of 0.5 seconds
            # Simulate battery behavior based on the current state
            if fsm.current_state == charging_s:
                # Battery charges by 2% per loop iteration
                battery_level += 2.0
            elif fsm.current_state == cleaning_s:
                # Battery drains by 5% per loop iteration during cleaning
                battery_level -= 5.0
            else:
                # Battery drains slowly (0.5%) while docking
                battery_level -= 0.5

            # Clamp the battery level between 0% and 100%
            battery_level = min(100, max(battery_level, 0))

            # Update the state machine with the current battery level
            fsm.update(battery_level=battery_level)

            # Log the current state and battery level
            fsm_example_logger.info(f"Current state: {fsm.current_state.name}; " f"Battery level: {battery_level}; ")

Example Output

When running the example, you can expect the following behavior:

  1. The FSM starts in the Charging state.
  2. Once the battery is fully charged, it transitions to the Cleaning state.
  3. When the battery level drops below 20%, it transitions to the Docking state.
  4. After docking, it transitions back to the Charging state.

This cycle repeats indefinitely, simulating the behavior of the robot vacuum cleaner.

Full Script for This Tutorial

from opensourceleg.control.fsm import State, StateMachine
from opensourceleg.logging.logger import Logger
from opensourceleg.utilities import SoftRealtimeLoop


# Transition criteria functions
def cleaning_to_docking(battery_level: float) -> bool:
    """
    Transition from 'Cleaning' to 'Docking' when the battery level is below 20%.
    """
    return battery_level < 20


def charging_to_cleaning(battery_level: float) -> bool:
    """
    Transition from 'Charging' to 'Cleaning' when the battery is fully charged (100%).
    """
    return battery_level == 100


# Main function
if __name__ == "__main__":
    # Initialize the logger for the finite state machine
    fsm_example_logger = Logger(
        log_path="./logs",  # Directory to store logs
        file_name="fsm.log",  # Log file name
    )

    # Define states
    cleaning_s = State(name="Cleaning")  # State for cleaning
    charging_s = State(name="Charging")  # State for charging
    docking_s = State(name="Docking", minimum_time_in_state=10)  # State for docking, assuming it takes 10 seconds

    # Initialize the state machine with the defined states and the initial state
    fsm = StateMachine(states=[charging_s, cleaning_s, docking_s], initial_state_name=charging_s.name)

    # Add transitions between states
    fsm.add_transition(
        source=charging_s,
        destination=cleaning_s,
        event_name="Fully Charged - Beginning Cleaning",
        criteria=charging_to_cleaning,
    )
    fsm.add_transition(
        source=cleaning_s,
        destination=docking_s,
        event_name="Battery Low - Finding Charging Dock",
        criteria=cleaning_to_docking,
    )
    fsm.add_transition(source=docking_s, destination=charging_s, event_name="Docked - Beginning Charging")

    # Initialize the battery level
    battery_level = 50.0  # Start with 50% battery

    # Run the state machine in a soft real-time loop
    with fsm:
        for _t in SoftRealtimeLoop(dt=0.5):  # Loop with a time step of 0.5 seconds
            # Simulate battery behavior based on the current state
            if fsm.current_state == charging_s:
                # Battery charges by 2% per loop iteration
                battery_level += 2.0
            elif fsm.current_state == cleaning_s:
                # Battery drains by 5% per loop iteration during cleaning
                battery_level -= 5.0
            else:
                # Battery drains slowly (0.5%) while docking
                battery_level -= 0.5

            # Clamp the battery level between 0% and 100%
            battery_level = min(100, max(battery_level, 0))

            # Update the state machine with the current battery level
            fsm.update(battery_level=battery_level)

            # Log the current state and battery level
            fsm_example_logger.info(f"Current state: {fsm.current_state.name}; " f"Battery level: {battery_level}; ")