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:
- The FSM starts in the
Charging
state. - Once the battery is fully charged, it transitions to the
Cleaning
state. - When the battery level drops below 20%, it transitions to the
Docking
state. - 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}; ")