Flight supervisor#

Description#

Flight supervisor contains the state machine of the autopilot. It receives commands from and sends events to the Mission UI, and reacts to events from Guidance, Services and Drone controller. It also monitors the drone battery level to trigger emergency actions (return to home, landing).

Flight supervisor is written in Python and uses the opensource package pytransitions.

../_images/flight_supervisor.png

In addition to the state machine, Flight supervisor relies on a set of manager objects to keep track of different features, indenpendantly of the current state of the state machine.

Managers are split into two categories: the core managers in the fsup.features package, which are always present, and mission-specific managers that are activated/deactivated along with the currently active mission.

Managers can observe and send messages, and typically expose an API called from state machine methods, making the code in each state simpler.

State machine structure#

The top level of the state machine (level 0 L0) contains the mandatory stages required by Flight supervisor:

  • Ground states that are performed on the ground. Ex: Idle, Magneto Calibration.

  • Takeoff various takeoff strategies. Ex: Normal, Hand.

  • Hovering when the drone is flying and holding fix point (not receiving any piloting command).

  • Flying all flying states. Ex: Manual, FlightPlan, RTH.

  • Landing various landing strategies. Ex: Normal, Hand.

  • Critical when a critical condition is detected. Ex: Emergency Landing, Critical Rth.

Every mission need to have those 6 stages. You can write new ones or reuse states from the default mission.

Note

Switching missions requires changing the state machine in Flight supervisor.

How to modify the state machine#

The mission’s code of the state machine shall respect a directory structure (See Directory structure). Each mission directory contains at least a file named mission.py with a class name Mission. This class contains the mission state machine (States and Transitions).

class Mission#
on_load()#
on_unload()#
on_activate()#
on_deactivate()#
can_activate()#
can_deactivate()#
states() list#
transitions() list#
class Mission(object):
    def __init__(self, mission_environment):
        self.env = mission_environment

    def on_load(self):
        pass  # Objects creation, configuration, etc.

    def on_unload(self):
        pass  # Cleanup

    def on_activate(self):
        pass  # Enable services, MessageCenter channels etc.

    def on_deactivate(self):
        pass  # Cleanup

    def can_activate(self, current_state):
        return can_activate

    def can_deactivate(self, current_state):
        return can_deactivate

    def states(self):
        return _STATES

    def transitions(self):
        return _TRANSITIONS

On load#

on_load() is called when the mission is loaded. It is used to create custom objects required for the mission.

On unload#

on_unload() is called when the mission is unloaded. It is used to destroy custom objects required for the mission.

On activate#

on_activate() is called when the mission is activated. It is used to create message channels with other components on the system.

import mymission.service.messages_pb2 as svc

def on_activate(self):
    self.svc_channel = self.mc.start_client_channel('unix:/tmp/svc')
    self.svc = self.mc.attach_client_service_pair( \
        self.svc_channel, svc, True)

On deactivate#

on_deactivate() is called when the mission is deactivated. It is used to destroy message channels with other components on the system.

def on_deactivate(self):
    self.mc.detach_client_service_pair(self.svc)
    self.svc = None
    self.svc_channel = None

Can activate#

can_activate() is called when a request to activate this mission is received. By default, missions can be activated only if current stage is ‘ground’ but missions can override the behavior if it is safe to activate in other stages (like hovering for example)

Note

Note: the given state is the current state of the active mission (so not a state of the requested new mission, so only the stage should be used).

def can_activate(self, current_state):
    return current_state.get_stage() in ['ground', 'hovering']

Can deactivate#

can_deactivate() is called when a request to deactivate this mission is received. By default, missions can be deactivated only if current stage is ‘ground’ but missions can override the behavior if it is safe to deactivate in other stages (like hovering for example)

def can_deactivate(self, current_state):
    return current_state.get_stage() == 'ground'

States#

states() shall return an array of all the states of the state machine. The top level of the state machine shall contain the six predefined stages as follow:

from .ground.stage import GROUND_STAGE
from .takeoff.stage import TAKEOFF_STAGE
from .hovering.stage import HOVERING_STAGE
from .flying.stage import FLYING_STAGE
from .landing.stage import LANDING_STAGE
from .critical.stage import CRITICAL_STAGE
 _STATES = [
    GROUND_STAGE,
    TAKEOFF_STAGE,
    HOVERING_STAGE,
    FLYING_STAGE,
    LANDING_STAGE,
    CRITICAL_STAGE,
]

Each stage is described in its own directory in a file named stage.py. To reuse a complete stage of the default mission, simply import the corresponding stage description and use it in the _STATES variable.

from fsup.missions.default.ground.stage import GROUND_STAGE

The stage is then a description of the level 1 states and possibly level 2 states. Each state is a python dictionary with the following keys:

  • name: name of the state, in snake case. This name will be used in the table of transitions.

  • class: optional name of the python class that will contain code managing the state.

  • children: optional array with a list of nested states.

  • initial: when children stage is defined. initial refers to the first children state entered when his parent state is entered.

The python class with the code managing the state shall derive the base class fsup.genstate.State and can override the following methods:

  • enter(): called when the state is entered with the message that triggered the transition.

  • exit(): called when the state is exited with the message that triggered the transition.

  • step(): called when a message is received while the state is active.

  • can_enter(): block transition if this returns False on the target state.

  • can_exit(): block transition if this returns False on the source state.

For nested states, the above methods are called for each class of the hierarchy, from top to bottom.

For level 1 states, it is also possible to reuse code from an existing one in the default mission by just importing it’s description or classes.

from fsup.missions.default.flying.rth import RTH_STATE as FLYING_RTH_STATE

CRITICAL_STAGE = {
    # ...
    'children': [
        {
            'name': 'critical_rth',
            # Reuse level 1/2 states from 'flying' RTH
            'class': FLYING_RTH_STATE['class'],
            'initial': FLYING_RTH_STATE['initial'],
            'children': FLYING_RTH_STATE['children'],
        },
        # ...
    ]
}

The full name of a state described in this manner will be: <stage>.<state_l0>.<state_l1>.

From the example above, critical.critical_rth.waypoint_path, if we consider the l1 children waypoint_path

Configuration files#

Create configuration files#

The state may have its own configuration which is located at [PRODUCT_ROOT_CFG]/etc/fsup/[STAGE]/[name_of_state].cfg For ex: for the state takeoff/normal, the location of its configuration file will be: /etc/fsup/takeoff/normal.cfg

If the state has children, the configuration of these children will be included in the configuration file of the state. For ex: state takeoff/normal has a children state ascent, the configuration file of this state will be as below:

File: /etc/fsup/takeoff/normal.cfg

ascent: {
    param_1: value_1;
    param_2: value_2;
    ...
    param_n: value_n;
};

For all custom AirSDK mission which imports states of mission default, there are 2 possible situations: * If the imported state is situated at the first level, it is not necessary to re-create a new configuration file because the configuration of default mission is automatically read * If the imported state is at the second/third/fourth/etc. level, it is necessary to re-create a configuration file and copy all params of this imported state For example: flying/STATE_A state imports hovering/fixed of mission default as its fourth level state

from fsup.missions.default.hovering.fixed import FIXED_STATE

STATE_A = {
    "name": "state_a",
    "class": StateAClass,
    "initial": "state_b",
    "children": [
        {
            "name": "state_b",
            "class": StateBClass,
        },
        {
            "name": "state_c",
            "class": StateCClass,
            "initial": "state_d",
            "children": [
                {
                    "name": "state_d",
                    "class": StateD,
                },
                # The imported state of mission default
                FIXED_STATE,
            ],
        }
    ]
}

For this case, the configuration file of flying/STATE_A is like:

File [MISSION_PRODUCT_DIR]/etc/fsup/flying/state_a.cfg

state_c: {
    fixed: {
        param_1: value_1;
        param_2: value_2;
        ...
        param_n: value_n;
    }
};

Read configuration files#

In abstract mission, all mission specific configuration files are automatically loaded on __init__ Then, in the __init__ method of State, it retrieve only its own configuration from the mission or it can get the drone general configuration.

  • Get mission configuration with keys: use method self.get_config(keys)

(keys: list of keys, key type “a.b.c” is not supported) * Get drone general configuration with keys: use method self.get_drone_config(keys) (keys: list of keys, key type “a.b.c” is supported)

Transitions#

transitions() shall return an array of all the possible transitions in the state machine. Each transition is a triplet with:

  • Message triggering the transition.

  • Source state or list of source states including all possible states from which a transition is possible. If the list includes a stage (with no sub-state specified), it will apply to every children of the given stage (ex: ‘flying’ will also include ‘flying.rth’).

  • Destination state, or None if the transition must not occur (to exclude special cases if a more generic transition exists below). If the list includes a stage (with no sub-state specified), the destination state will be determined using the initial children. (ex: ‘flying’ will refer to ‘flying.manual’, manual being the initial state of ‘flying’ state).

When a message is received, the transition will be parsed in the table from top to bottom and the first matching transition found will be executed.

Note

If a transition’s source state can_enter() or the target can_exit() method returns None, the next available transition will be executed.

ANY_STATE = ['ground', 'takeoff', 'hovering', 'flying', 'landing', 'critical']

_TRANSITIONS = [
    # ground.* -> critical.emergency_ground
    ['DroneController.too_much_angle_detected', 'ground', 'critical.emergency_ground'],

    # Ignore message if currently in ground.*
    ['DroneController.too_much_angle_detected', 'critical.emergency_ground',  None],

    # Fallback transition: * -> critical.emergency_landing
    # Even though 'ground' is in ANY_STATE, the transition above has a higher priority.
    ['DroneController.too_much_angle_detected', ANY_STATE, 'critical.emergency_landing'],

    # ...
]

To reuse the transition table from the default mission, it is possible to add it before the default transition table.

from fsup.missions.default.mission import TRANSITIONS as DEFAULT_TRANSITIONS

ANY_STATE = ['ground', 'takeoff', 'hovering', 'flying', 'landing', 'critical']

_TRANSITIONS = [
    # This will disable any default transition using this message,
    # as it will have a higher priority (at the top of transition table)
    ['Autopilot.emergency',   ANY_STATE,    None],
    # ...
]

def transitions(self):
    return _TRANSITIONS + DEFAULT_TRANSITIONS

Note

A transition from state to itself is possible, it has to be explicitly forbidden if you want to prevent it.

flying.move_to -> flying.move_to can occur, it will call exit(), enter() and step().

Note

More precise transitions will be executed first regardless of order.

For instance: [event, hovering.fixed, some_state] [event, hovering.fixed.scan, some_other_state]

The second transition will always be called when the current state is hovering.fixed.scan.

Note

Wildcard (“*”) transitions are not allowed, the behavior is undefined.

Note

For new messages, the message protobuf need to be attached to the fsup.genstate.message_center.message_center.MessageCenter in the state machine.

Managers#

There are core features that are necessary for Flight supervisor to work, and mission-specific (e.g: Default Mission) features that can be defined in missions.

Core features#

Each feature is allocated and stored as a member of a unique instance of fsup.supervisor.Supervisor. The table below maps the attribute name in the Supervisor object, and the associated attribute type.

Attribute name in Supervisor

Feature manager class name

geofence_manager

features.geofence_manager.GeofenceManager

obstacle_avoidance_manager

features.oa_manager.ObstacleAvoidanceManager

statuses

features.event_cache.EventCache

takeoff_readyness_manager

features.takeoff_readyness_manager.TakeOffReadynessManager

video_manager

features.video_manager.VideoManager

Directory structure#

The complete directory structure and files of a state machine for a mission is described below. If a stage reuses an existing one from the default mission, its matching directory will not be there.

├── critical
│   ├── __init__.py
│   ├── landing.py
│   ├── rth.py
│   └── stage.py
├── features
│   ├── auto_landing_alerts_manager.py
│   ├── critical_rth_alerts_manager.py
│   ├── flightplan_availability_manager.py
│   ├── flightplan_manager.py
│   ├── handland_manager.py
│   ├── handtakeoff_manager.py
│   ├── home_manager.py
│   ├── lookat_availability_manager.py
│   ├── lookat_manager.py
│   ├── move_availability_manager.py
│   ├── pilot_trajectory_est_manager.py
│   ├── poi_availability_manager.py
│   ├── poi_manager.py
│   ├── precise_home_manager.py
│   ├── precise_hovering_manager.py
│   ├── rth_availability_manager.py
│   ├── rth_manager.py
│   ├── target_trajectory_est_manager.py
│   └── visual_tracking_manager.py
├── flying
│   ├── __init__.py
│   ├── flightplan.py
│   ├── lookat.py
│   ├── manual.py
│   ├── move_to.py
│   ├── panorama.py
│   ├── poi.py
│   ├── relative_move.py
│   ├── rth.py
│   └── stage.py
├── ground
│   ├── __init__.py
│   ├── flightplan.py
│   ├── idle.py
│   ├── magneto_calibration.py
│   ├── reset.py
│   └── stage.py
├── hovering
│   ├── __init__.py
│   ├── fixed.py
│   ├── freeze.py
│   ├── gotofix.py
│   ├── rth.py
│   └── stage.py
├── landing
│   ├── __init__.py
│   ├── flightplan.py
│   ├── hand.py
│   ├── normal.py
│   └── stage.py
├── takeoff
│   ├── __init__.py
│   ├── flightplan.py
│   ├── hand.py
│   ├── normal.py
│   └── stage.py
├── __init__.py
└── mission.py

Sample#

See Hello Flight supervisor state machine.

Messages#

Flight supervisor receives Commands from and sends Events to the Mission UI and reacts to events from Guidance, Services and Drone controller.

Settings#

The following settings allow to fine-tune drone behavior. They are persistent when the drone reboots, and can be accessed by any process running on the drone.

All the following settings should be preceded by “autopilot.” when referenced. They can be modified and subscribed to through Shsettings API

Settings

Type

Default

Min

Max

Unit

Description

active_geofence

Bool

False

Allow / prevent drone from flying further than maximum allowed distance (geofence)

altitude

double

4000

0.5

4000

m

Maximum altitude the drone is allowed to reach

angular_rate

Double

150

40

300

deg/s

Maximum drone rotation speed

banked_turn

Bool

False

Allow banked turn (change inclination when turning) during manual flight

geofence_distance

Double

100

10

4000

m

Maximum allowed distance from home for geofence

hull_protection

Bool

False

Whether the drone has an external hull installed

preferred_home

string

“takeoff”

Preferred home type for RTH

rotation_speed

Double

70

3

200

deg/s

Maximum rotation speed allowed for manual flight

rth_end_altitude

Double

2

1

10

m

End altitude for RTH final descent

rth_land_at_end

Bool

False

Trigger landing at the end of RTH

rth_land_delay_s

Int

0

0

1800

s

Delay before landing at the end of RTH if rth_land_at_end is True

rth_min_altitude

double

20

20

100

m

Altitude used for RTH (relative to takeoff)

tilt

Double

20

1

40

deg

Maximum tilt allowed for manual flight. Correspond to the tilt range of joysticks in flightplan

vertical_speed

Double

1

0.1

4

m/s

Maximum vertical speed allowed for manual flight

zoom_limits_rotation_speed

Bool

True

Limit rotation speed relative to zoom level when camera is zoomed