Creating a mission#

Starting point#

It is assumed that the Air SDK samples workspace is setup and the following structure is present:

build/
build.sh
packages/
└ airsdk─samples
  ├ README.md
  └ hello
    ├ atom.mk
    ├ autopilot─plugins
    │ ├ fsup
    │ └ guidance
    ├ messages
    │ ├ protobuf
    │ └ tests
    ├ product
    │ ├ classic
    │ ├ common
    │ └ pc
    └ services
      ├ native
      └ python
products/
README

Creating MyMission and MyService in Python#

The mission will be called MyMission, will have as uid com.company.missions.mymission and a service called MyService. There are multiple directories and files in an Air SDK mission that need to be created:

Creating the required directory/file structure#

  1. A new directory should be created in packages/mymission. This will be the root directory of the Air SDK mission for all the following steps.

  2. Create a mission build file atom.mk (for more info on atom.mk files see the Alchemy documentation <https://github.com/Parrot-Developers/alchemy/blob/master/doc/alchemy.mkd>)

  3. Create the flight supervisor (fsup) plugin file autopilot-plugins/fsup/mission.py

  4. Depending on the type of MyMission:

    • for a flight mission with a guidance mode create:

      • autopilot-plugins/guidance/python/mymission.py

      • (optionally) autopilot-plugins/guidance/protobuf/mymission/guidance/messages.proto

    • for a service create:

      • depending on the language of the service: services/myservice/python/service.py or services/myservice/native/service.cpp

      • (optionally) services/myservice/atom.mk

      • (optionally) messages/protobuf/company/MyService/airsdk/messages.proto

  5. Create a product directory for each supported variant:

    • product/mymission/classic for a native build

    • product/mymission/pc for a simulator build

    as well as a common variant directory for metadata and configurations

  6. Create mission.json file in product/mymission/common/skel/missions/com.company.missions.mymission/mission.json

  7. Create product.mk file in:

    • product/mymission/common/config

    • product/mymission/classic/config

    • product/mymission/pc/config

  8. Edit

    • atom.mk

    • mission.py

    • service.py

    • messages.proto

Editing the build files#

The atom.mk at the root of the packages/mymission will describe how to build the mission, the service and the messages:

########################
# building the mission #
########################
LOCAL_PATH := $(call my-dir)
#
# defining variables to use
mission.name := mymission
mission.package := company.$(subst _,-,$(mission.name))
mission.uid := com.$(mission.package)
mission.mission-dir := missions/$(mission.uid)
mission.payload-dir := $(mission.mission-dir)/payload
mission.fsup-dir := $(mission.payload-dir)/fsup
#
service.name := myservice
service.package := company.$(subst _,-,$(service.name))
service.services-dir := $(mission.payload-dir)/services
#
# define a macro that
# copies all files relative to SOURCE/ that match *.SUFFIX into TARGET
# ($1:SOURCE $2:SUFFIX $3:TARGET)
#   this is achieved by calling all-files-under and updating the
#   LOCAL_COPY_FILES variable
copy-all-under = $(foreach __f,\
    $(call all-files-under,$1,$2),\
    $(eval LOCAL_COPY_FILES += $(__f):$(patsubst $1/%,$3/%,$(__f))))
#
# define mission module build unit
include $(CLEAR_VARS)
LOCAL_MODULE := airsdk-$(mission.name)-autopilot-plugins
LOCAL_DESCRIPTION := AirSdk autopilot mymission mission files
LOCAL_CATEGORY_PATH := airsdk/missions/mymission
#
# copy all files from autopilot-plugins/fsup in the build destination
$(call copy-all-under,autopilot-plugins/fsup,.py,$(mission.fsup-dir))
#
# define dependencies of the mission module, in this case the service and
# the messages both defined below
LOCAL_LIBRARIES := \
    airsdk-$(service.name)-service \
    libmission-airsdk-$(mission.name)-pbpy
#
include $(BUILD_CUSTOM)

########################
# building the service #
########################
# define service module build unit
include $(CLEAR_VARS)
LOCAL_MODULE := airsdk-$(service.name)-service
LOCAL_DESCRIPTION := the myservice of mymission
LOCAL_CATEGORY_PATH := airsdk/missions/$(service.name)
LOCAL_SRC_FILES := $(call all-files-under,services/myservice/python,.py)
LOCAL_COPY_FILES := services/myservice/python/service.py:$(service.services-dir)/$(LOCAL_MODULE)
# define dependencies of the mission module, in this case the
LOCAL_LIBRARIES := \
    libarsdk-pbpy
#
include $(BUILD_CUSTOM)

#########################
# building the messages #
#########################
# define messages module build unit
include $(CLEAR_VARS)
LOCAL_MODULE := libmission-airsdk-$(mission.name)-pbpy
LOCAL_DESCRIPTION := Protobuf python code for $(mission.name) mission
LOCAL_CATEGORY_PATH := airsdk/missions/$(mission.name)
LOCAL_LIBRARIES := \
    protobuf-python
# make a list of all *.proto files
mission_proto_path := messages/protobuf
mission_proto_files := \
    $(call all-files-under,$(mission_proto_path),.proto)
# add them to LOCAL_CUSTOM_MACROS so they will get compiled by the protobuf
# compiler
$(foreach __f,$(mission_proto_files), \
    $(eval LOCAL_CUSTOM_MACROS += $(subst $(space),,protoc-macro:python, \
        $(TARGET_OUT_STAGING)/usr/lib/python/site-packages, \
        $(LOCAL_PATH)/$(__f), \
        $(LOCAL_PATH)/$(mission_proto_path))) \
)
#
include $(BUILD_CUSTOM)

Then edit the product.mk files:

  1. First edit the packages/mymission/product/common/config/product.mk file and add:

COMMON_CONFIG_DIR := $(call my-dir)
# Add Common skeleton
TARGET_SKEL_DIRS += $(COMMON_CONFIG_DIR)/../skel
# Include buildext mission config modules
include $(ALCHEMY_WORKSPACE_DIR)/build/dragon_buildext_mission/product.mk
  1. Then edit the packages/mymission/product/classic/config/product.mk file and add:

MYMISSION_CLASSIC_CONFIG_DIR := $(call my-dir)
TARGET_SDK_DIRS = $(ALCHEMY_WORKSPACE_DIR)/sdk/classic
# Include common config modules
include $(MYMISSION_CLASSIC_CONFIG_DIR)/../../common/config/product.mk
  1. Finally edit the packages/mymission/product/classic/config/product.mk file and add:

MYMISSION_PC_CONFIG_DIR := $(call my-dir)
TARGET_SDK_DIRS = $(ALCHEMY_WORKSPACE_DIR)/sdk/pc
# Include common config modules
include $(MYMISSION_PC_CONFIG_DIR)/../../common/config/product.mk

Editing the main mission files#

In packages/mymission/autopilot-plugins/fsup/mission.py add:

'''
Flight supervisor state machine of the mission.

This empty shell of a mission does nothing other than forward events and
commands to connect a mission UI to the service.

Upon mission load the observers relay all commands onto the service and all
events onto the mission UI.
'''
import fsup.services.events as events
import company.airsdk.messages_pb2 as airsdk_messages
from fsup.genmission import AbstractMission
from fsup.missions.default.critical.stage import CRITICAL_STAGE as DEF_CRITICAL
from fsup.missions.default.ground.stage import GROUND_STAGE as DEF_GROUND
from fsup.missions.default.hovering.stage import HOVERING_STAGE as DEF_HOVERING
from fsup.missions.default.landing.stage import LANDING_STAGE as DEF_LANDING
from fsup.missions.default.mission import TRANSITIONS as DEF_TRANSITIONS
from fsup.missions.default.takeoff.stage import TAKEOFF_STAGE as DEF_TAKEOFF
from fsup.missions.default.takeoff.stage import FLYING_STAGE as DEF_FLYING

from fsup.message_center.service import ServicePair
from fsup.message_center.observer import Observer
from msghub import Channel

MSGHUB_ADDR = "unix:@mymission"

# for info
# https://developer.parrot.com/docs/airsdk/general/flight_supervisor.html#Mission
class Mission(AbstractMission):
    '''
    Attributes
    ----------
    - external_ui_messages: ServicePair
        the external ui service pair of commands and events will permit to
        observe commands that arrive from the client's mission UI that need to
        be passed to the service.
    - service_messages: ServicePair
        the service's pair of commands and events will permit to observe
        events that arrive from the service and pass them back to the
        client's mission UI
    - service_messages_channel: Channel
        the service's communication channel
    - _external_ui_channel_observer: Observer
        observation handle for channel (UI <-> mission)

    - _external_ui_forwarder: Observer
        observation handle for commands (UI -> mission)

    - _service_forwarder: Observer
        observation handle for events (misson -> UI)
    '''
    def __init__(self, mission_environment):
        super().__init__(mission_environment)
        self.external_ui_messages: ServicePair = None
        self.service_messages: ServicePair = None
        self.service_messages_channel: Channel = None
        self._external_ui_channel_observer: Observer = None
        self._external_ui_forwarder: Observer = None
        self._service_forwarder: Observer = None

    def _on_external_ui_connected(self, event, channel):
        pass

    def on_load(self):
        # Messages to and fro mission UI
        self.external_ui_messages = self.env.attach_airsdk_service_pair(
            airsdk_messages,
            forward_events=True)

        # Channel for messages to and fro service
        self.service_messages_channel = self.mc.start_client_channel(
            MSGHUB_ADDR)
        self.service_messages = self.mc.attach_client_service_pair(
            self.service_messages_channel,
            airsdk_messages,
            forward_events=True)

        # Start mission UI channel observer, let us specify a hook to execute
        # when the channel gets connected.
        self._external_ui_channel_observer = self.mc.observe({
            events.Channel.CONNECTED: self._on_external_ui_connected,
        }, src=self.env.airsdk_channel)

        # Forward service events to the mission UI and mission UI commands
        # to the service
        self._external_ui_forwarder = self.external_ui_messages.cmd.observe({
            events.Service.MESSAGE:
            lambda event, msg: self.service_messages.cmd.send(msg)
        })
        self._service_forwarder = self.service_messages.evt.observe({
            events.Service.MESSAGE:
            lambda event, msg: self.external_ui_messages.evt.send(msg)
        })

    def on_unload(self):
        # Detach both service pairs
        self.env.detach_airsdk_service_pair(self.external_ui_messages)
        self.mc.detach_client_service_pair(self.service_messages)
        self.mc.stop_channel(self.service_messages_channel)

        # Stop forwarding service events
        self._service_forwarder.unobserve()
        self._external_ui_forwarder.unobserve()

        # Stop mission UI channel observer
        self._external_ui_channel_observer.unobserve()

    def on_activate(self):
        pass

    def on_deactivate(self):
        pass

    def states(self):
        return [
            DEF_GROUND,
            DEF_TAKEOFF,
            DEF_HOVERING,
            DEF_FLYING,
            DEF_LANDING,
            DEF_CRITICAL,
        ]

    def transitions(self):
        return DEF_TRANSITIONS

Editing the messages files#

In packages/mymission/messages/protobuf/company/mymission/airsdk/messages.proto define the protobuf messages the mission needs to communicate with the Mission UI:

syntax = "proto3";
package company.mymission.airsdk.messages;
import 'google/protobuf/empty.proto';
option java_package = "company.mymission.airsdk";
option java_outer_classname = "MyMission";

// Union of all possible commands of this package. Messages coming from
// Mission UI to the Air SDK mission.
message Command {
  oneof id {
    google.protobuf.Empty my_command = 1;
  }
}
// Union of all possible events of this package. Messages coming from the
// Air SDK mission towards Mission UI
message Event {
  oneof id {
    google.protobuf.Empty my_event = 1;
  }
}

Commands are messages sent from the Mission UI to the Air SDK mission while Events are messages sent from the Air SDK mission to the Mission UI.

Editing the services files#

In packages/mymission/service/myservice/service.py define the service:

'''
Air SDK service for the mission.
'''

import signal
import sys
import ulog
import logging
from typing import Callable, Dict, Any

import libpomp
import msghub

EXIT_SUCCESS = 0

import company.airsdk.messages_pb2 as service_messages
import google.protobuf.empty_pb2 as empty_protobuf

# Abstract named socket for Air SDK mission messaging
MSGHUB_ADDR = "unix:@mymission"

stopped = False

# https://developer.parrot.com/docs/airsdk/messages/api_message.html#_CPPv4N6msghub14MessageHandlerE
class UploadMessageHandler(msghub.MessageHandler):
    '''
    The upload service message handler.
    '''

    def __init__(self,
                 logger: logging.Logger,
                 dispatch_table: Dict[str, Callable[[Any], None]]):
        super().__init__(service_name=service_messages.Command.DESCRIPTOR.full_name)
        self.log = logger
        self.dispatch_table = dispatch_table

    def handle(self, message: msghub.Message):
        '''
        Overriden handle of MessageHandler.
        '''
        message = service_messages.Command.FromString(message.data)
        message_name = message.WhichOneof('id')
        self.log.debug(message)
        dispatch_fn = self.dispatch_table.get(message_name)
        if dispatch_fn:
            dispatch_fn(getattr(message, message_name))
        else:
            self.log.error(f'no handler function for message "{message_name}"')


# https://developer.parrot.com/docs/airsdk/messages/api_message.html#_CPPv4N6msghub13MessageSenderE
class UploadMessageSender(msghub.MessageSender):
    '''
    The upload service message sender.
    '''

    def __init__(self, logger: logging.Logger):
        super().__init__(service_name=service_messages.Event.DESCRIPTOR.full_name)
        self._log = logger

    def send(self, message: service_messages.Event):
        '''
        Overriden send of MessageSender.
        '''
        self._log.debug(message)
        super().send(msghub.Message.from_pbuf_msg(self.service_id, message))


class Service(object):
    '''
    Class setting up the message handling and running the asyncio loop and
    keeping the context of execution of the service.

    Attributes
    ----------
    - log: logging.Logger
        The system log.
    - pomp_loop: libpomp.struct_pomp_loop
        The event loop.
    - message_hub: msghub.MessageHub
        The message hub.
    - message_hub_channel: msghub.Channel
        The channel that is the message-duct with the mission (and ui).
    - message_sender: UploadMessageSender
        The handle that sends messages to mission (and ui).
    - message_handler: UploadMessageHandler
        The receiver of mission (and ui) messages.
    '''

    def __init__(self, log: logging.Logger):
        self.log = log
        # create and setup event loops
        self.pomp_loop: libpomp.struct_pomp_loop = libpomp.pomp_loop_new()
        # create message hub
        self.message_hub: msghub.MessageHub = \
            msghub.MessageHub(loop=self.pomp_loop)

        # create message handler and sender
        self.message_sender = UploadMessageSender(logger=self.log)
        self.message_handler = UploadMessageHandler(logger=self.log,
                                                    dispatch_table={
                                                        'my_command': self._handle_my_command
                                                    })
        self.message_hub_channel: msghub.Channel = None


    def cleanup(self):
        '''
        Clean up method.
        '''
        self.log.info('cleaning up')
        self.message_hub.detach_message_handler(self.message_handler)
        self.message_hub.detach_message_sender(self.message_sender)
        self.message_hub.stop_channel(self.message_hub_channel)
        self.message_handler = None
        self.message_sender = None
        self.message_hub.stop()
        self.message_hub_channel = None
        self.message_hub = None

        self.asyncio_loop.stop()

        self.asyncio_loop.remove_reader(
            libpomp.pomp_loop_get_fd(self.pomp_loop))
        self.asyncio_loop = None
        libpomp.pomp_loop_destroy(self.pomp_loop)
        self.pomp_loop = None
        self.log.info('exited')

    def quit(self):
        '''
        Quit method.
        '''
        self.log.info('quitted')

    def run(self):
        '''
        Service entry point.
        '''
        self.log.info('started service')

        # create message channel
        self.message_hub_channel = self.message_hub.start_server_channel(
            MSGHUB_ADDR)

        self.message_hub.attach_message_sender(
            self.message_sender, self.message_hub_channel)
        self.message_hub.attach_message_handler(self.message_handler)

        while not stopped:
            libpomp.pomp_loop_wati_and_process(loop, 0)


    def _handle_my_command(self, message: empty_protobuf.Empty):
        self.log.info('received \'my_command\' command')
        event = service_messages.Event(my_event=empty_protobuf.Empty())
        self.log.info('emiting \'my_event\' event')
        self.message_sender.send(event)

def main():
    log = ulog.setup_logging('MyService')
    try:
        ret = EXIT_SUCCESS

        # Import application code after logging is setup to correctly log import errors
        service = Service(log=log)
        # Setup signal, handle SIGINT/SIGTERM to exit app
        def sig_handler(signum, _frame):
            stopped = True
            log.info(f'Received signal {signum}')
        signal.signal(signal.SIGINT, sig_handler)
        signal.signal(signal.SIGTERM, sig_handler)

        try:
            service.run()
        except KeyboardInterrupt:
            service.quit()
        finally:
            service.cleanup()

    except Exception as exception:
        log.exception(f'Fatal exception {exception}')
        sys.exit(1)
    sys.exit(ret)


if __name__ == '__main__':
    main()

Editing the metadata files#

Fill the metadata of the mission in packages/mymission/product/common/skel/missions/com.company.missions.mymission/mission.json:

{
    "uid": "com.company.missions.mymission",
    "name": "My Mission",
    "desc": "This is a custom mission",
    "version": "0.0.0",
    "target_model_id": "091A",
    "target_min_version": "0.0.0",
    "target_max_version": "99.99.99",
    "services" : [
        ["airsdk-myservice-service"]
    ]
}

Make the mission visible to the build system#

Create a symbolic link in <workspace>/product/mymission that points to <workspace>/packages/mymission/product:

# assuming the current working directory is the root of the workspace
$ cd product
$ ln -s ../packages/mymission/product mymission

This will enable the build system to discover the mission product definition.

Configure the mission in the build system#

Now, the mission should be configured from the root of the <workspace> running the following command and enabling airsdk-mymission-autopilot-plugins, libmission-airsdk-mymission-pbpy and airsdk-myservice-service:

# for real drone builds
$ ./build.sh -p mymission-classic -t menuconfig
# and/or for simulator builds
$ ./build.sh -p mymission-pc -t menuconfig

After this operation you should have two new files that should be checked in your source control system products/mymission/classic/config/global.conf and products/mymission/pc/config/global.conf containing:

#
# Automatically generated file; DO NOT EDIT.
# Alchemy Configuration
#
# CONFIG_ALCHEMY_BUILD_AIRSDK_HELLO_AUTOPILOT_PLUGINS is not set
# CONFIG_ALCHEMY_BUILD_AIRSDK_HELLO_CV_SERVICE is not set
CONFIG_ALCHEMY_BUILD_AIRSDK_MYMISSION_AUTOPILOT_PLUGINS=y
CONFIG_ALCHEMY_BUILD_AIRSDK_MYSERVICE_SERVICE=y
# CONFIG_ALCHEMY_BUILD_LIBAIRSDK_HELLO_CV_SERVICE_MSGHUB is not set
# CONFIG_ALCHEMY_BUILD_LIBAIRSDK_HELLO_CV_SERVICE_PB is not set
# CONFIG_ALCHEMY_BUILD_LIBAIRSDK_HELLO_CV_SERVICE_PBPY is not set
# CONFIG_ALCHEMY_BUILD_LIBAIRSDK_HELLO_GUIDANCE_PBPY is not set
# CONFIG_ALCHEMY_BUILD_LIBMISSION_AIRSDK_HELLO_PBPY is not set
CONFIG_ALCHEMY_BUILD_LIBMISSION_AIRSDK_MYMISSION_PBPY=y

Note

If a module depends on another new module in an atom.mk file, the configuration needs to be updated with this following command to activate the new module:

# for real drone builds
$ ./build.sh -p mymission-classic -A config-update
# and/or for simulator builds
$ ./build.sh -p mymission-pc -A config-update

If you want an advanced configurator, you can use menuconfig or xconfig.

For more detail, all available commands can be displayed with this command:

$ ./build.sh -p XXX -A help

Build and install the mission#

As indicated in Build the Hello Drone Flight mission now the mission can be built for a simulated drone:

$ ./build.sh -p mymission-pc -t all -j

Output signed archive will be generated here:

$ out/mymission-pc/images/com.company.missions.mymission.tar.gz

Or for a real drone:

$ ./build.sh -p mymission-classic -t all -j

Output signed archive will be generated here:

$ out/mymission-classic/images/com.company.missions.mymission.tar.gz

The mission can be installed as indicated Install the Hello Drone Flightmission for a simulated drone:

$ ./build.sh -p mymission-pc -t sync --unsigned --reboot

For a real drone:

$ ./build.sh -p mymission-classic -t sync --reboot

Once the drone has rebooted, the mission will be installed and available to use.

Important

It is not possible to dynamically add files in a mission. It must be done at build time by putting the files directly in the skel (i.e skeleton) directory of the product.

In order to do that, create the following directory structure from the root directory of the mission:

…products/mymission/common/skel/missions/com.parrot.missions.mymission/payload/etc/

(replace mymission by the appropriate name)

For more details, please refer to Storage and persistence.