17. Creating your own controller

This documentation describes creating your own controller which is compatible with the Starling ecosystem.

TODO: Complete this docs page

17.1 Overview

17.2 Implementation

17.2.1 Launch Files

The controller itself can be run using ros2 run, but if you might want to run multiple nodes together, or provide some more complex functionality, it is recommended that ros2 nodes be run using launch files. ROS2 launch files can be written as xml or python programmes. Here we will explain using xml which imo is clearer, but the python is much more flexible.

17.2.1.1 Basic launch file:

Here is an example of a launch file which should be saved as controller.launch.xml in a launch folder in the root of the repository:

<launch>
    <arg name="vehicle_namespace" default="$(env VEHICLE_NAMESPACE vehicle_$(env VEHICLE_MAVLINK_SYSID))" />
    <arg name="demonstration" default="true"/>
    <group>
        <push-ros-namespace namespace="$(var vehicle_namespace)"/>
        <!-- <node name="primitive_4pl_controller" pkg="primitive_4pl" exec="controller" output="screen" respawn="true"> -->
        <node name="primitive_4pl_controller" pkg="primitive_4pl" exec="controller" output="screen">
            <param name="use_sim_time" value="$(env USE_SIMULATED_TIME false)"/>
            <param name="frame_id" value="map"/>
            <param name="setpoint_frame_id" value="$(var vehicle_namespace)/setpoint"/>
            <param name="vehicle_frame_id" value="$(var vehicle_namespace)/body"/>
        </node>

        <node name="primitive_4pl_controller" pkg="primitive_4pl" exec="demonstration" output="screen" if="$(var demonstration)"/>
    </group>

</launch>

This launch file takes two arguments which can be set by the user. Importantly the vehicle_namespace is set by the environment varible VEHICLE_NAMESPACE which is populated by an initialsation script in the controller base.

Then to ensure that the controller is an onboard controller, so we have one controller running for one vehicle, we create a group and set the namespace to vehicle namespace. As long as the topic names used inside of the controller are not absolute (i.e. no leading / - mytopic vs /mytopic), the nodes topics will get mapped to vehicle_1/mytopic.

Two nodes are started inside this node group. The first one requires a number of parameters which can need to use the vehicle name. The second one doesnt need any.

17.2.1.2 Enabling running In simulation

The base controller has a USE_SIMULATED_TIME environment variable. Any node in the controller launchfile should include the following line if the controller requires use of time:

<node pkg="mypkg" exec="controller" ...>
    <param name="use_sim_time" value="$(env USE_SIMULATED_TIME false)"/>
   ...
</node>

So when you want to run the controller in simulation, you can simply set the USE_SIMULATED_TIME to true. For example docker run --it --rm -e USE_SIMULATED_TIME mycontroller.

17.2.2 Build and Bake Files

A bake file is a configuration file used to specify how to build a particular dockerfile. We use it in particular because it allows us to build cross-platfrom executables in the case we want to run our containers on a drone. A drone running a raspberry pi runs the arm64 architecture, vs your own machine which most likely runs the amd64 architecture.

17.2.3 Basic Bake file:

TODO: Explain what this means

variable "BAKE_VERSION" {
    default = "latest"
}

variable "BAKE_REGISTRY" {
    default = ""
}

variable "BAKE_RELEASENAME" {
    default = ""
}

variable "BAKE_CACHEFROM_REGISTRY" {
    default = ""
}

variable "BAKE_CACHETO_REGISTRY" {
    default = ""
}

variable "BAKE_CACHEFROM_NAME" {
    default = ""
}

variable "BAKE_CACHETO_NAME" {
    default = ""
}

/*
 * Groups for target ordering
 */
group "stage1" {
    targets = ["4pl"]
}

// This target depends on starling-controller-base
target "4pl-controller" {
    context = "."
    args = {
        "VERSION": "${BAKE_VERSION}",
        "REGISTRY": "${BAKE_REGISTRY}"
        }
    tags = [
        "${BAKE_REGISTRY}uobflightlabstarling/4pl-controller:${BAKE_VERSION}",
        notequal("",BAKE_RELEASENAME) ? "${BAKE_REGISTRY}uobflightlabstarling/4pl-controller:${BAKE_RELEASENAME}": "",
        ]
    platforms = ["linux/amd64", "linux/arm64"]
    cache-to = [ notequal("",BAKE_CACHETO_NAME) ? "${BAKE_CACHETO_REGISTRY}uobflightlabstarling/4pl-controller:${BAKE_CACHETO_NAME}" : "" ]
    cache-from = [ notequal("",BAKE_CACHEFROM_NAME) ? "${BAKE_CACHEFROM_REGISTRY}uobflightlabstarling/4pl-controller:${BAKE_CACHEFROM_NAME}" : "" ]
}

17.2.4 Make Files

It is recommended that make (or nmake for windows) be used as a tool for building your controller. Make is a useful tool as it allows the running of any number of specific commands (similar to a bash file but with arguments dealt with for you). It is recommended that the makefile look something like the following, and be put in the root of the project folder:

MAKEFILE_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
BAKE_SCRIPT:=$(MAKEFILE_DIR)/docker-bake.hcl
BUILDX_HOST_PLATFORM:=$(shell docker buildx inspect default | sed -nE 's/^Platforms: ([^,]*),.*$$/\1/p')
BAKE:=docker buildx bake --builder default --load --set *.platform=$(BUILDX_HOST_PLATFORM) -f $(BAKE_SCRIPT)

CONTROLLER_NAME=controller
NETWORK?=4pl-ros2-controller_default
ENV?=
BUILD_ARGS?=

all: build

build:
    $(BAKE) $(BUILD_ARGS) $(CONTROLLER)

# This mybuilder needs the following lines to be run:
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# docker buildx create --name mybuilder
# docker buildx use mybuilder
# docker buildx inspect --bootstrap
local-build-push:
    docker buildx bake --builder mybuilder -f $(BAKE_SCRIPT) --push $(CONTROLLER_NAME)

run: build
    docker run -it --rm --net=$(NETWORK) $(ENV) -e USE_SIMULATED_TIME=true $(CONTROLLER_NAME):latest

run_bash: build
    docker run -it --rm --net=$(NETWORK) -e USE_SIMULATED_TIME=true $(CONTROLLER_NAME):latest bash

.PHONY: all build local-build-push run run_bash

Breaking down the make file, we have a number of commands.

MAKEFILE_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
BAKE_SCRIPT:=$(MAKEFILE_DIR)/docker-bake.hcl
BUILDX_HOST_PLATFORM:=$(shell docker buildx inspect default | sed -nE 's/^Platforms: ([^,]*),.*$$/\1/p')
BAKE:=docker buildx bake --builder default --load --set *.platform=$(BUILDX_HOST_PLATFORM) -f $(BAKE_SCRIPT)

The first 4 lines define a number of useful variables including where the current directory is, the location of your bake script, your system's curent platform to build for and the final buildx bake command for building your controller

CONTROLLER_NAME=controller
NETWORK?=controller_default
ENV?=
BUILD_ARGS?=

Then we define some useful variables to us, including the name of the controller and the local network (set to the default of foldername_default). Note that ENV and BUILD_ARGS have been set to a default of empty string on purporse. Then when running the file these can be specified e.g. make ENV=-e HELLO=mynewvariable.

all: build

build:
    $(BAKE) $(BUILD_ARGS) $(CONTROLLER)

# This mybuilder needs the following lines to be run:
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# docker buildx create --name mybuilder
# docker buildx use mybuilder
# docker buildx inspect --bootstrap
local-build-push:
    docker buildx bake --builder mybuilder -f $(BAKE_SCRIPT) --push $(CONTROLLER_NAME)

run: build
    docker run -it --rm --net=$(NETWORK) $(ENV) -e USE_SIMULATED_TIME=true $(CONTROLLER_NAME):latest

run_bash: build
    docker run -it --rm --net=$(NETWORK) -e USE_SIMULATED_TIME=true $(CONTROLLER_NAME):latest bash

Then we specify the commands. Here we have specified 5 commands: all, build, local-build-push,run and run_bash. The important one is build which can be run using make build which builds the container. A useful is run which will both build and run the newly built docker container. run_bash is the same as run except it puts you into the bash shell of the container instead of directly running. local-build-push is a helper which will help you push your container to dockerhub if you so need to.

.PHONY: all build local-build-push run run_bash

This final line is makefile syntax just in case you have any local files which are accidentally named exactly the same as one of the named commands.