In the previous part, we defined some core interfaces for working with MAVLink systems. Now let's build some utilities and extensions using these interfaces.
The idea is to keep these functions simple and rely on the Kotlin Coroutines API to handle operations like timeouts, retries and cancellations.
Firstly, let's add an extension function to the MavRemoteNode
interface to receive a single message of a specific
type.
This function will block the current coroutine until a message of type T
is received. We can also compose it with the
withTimeout
function.
This will throw a TimeoutCancellationException
if a heartbeat
message is not received from the FCU within 5
seconds. If for some reason, we want to continue execution even if the timeout occurs, we can use the
withTimeoutOrNull
function instead.
Sometimes, we may want to filter the message further based on some condition. We can add a variant of receive
that
takes a predicate.
For example, if we want the BatteryStatus
message with a specific battery ID.
Getting and setting FCU parameters are common operations and therefore we will be defining these extension functions on
MavController
.
There's a lot going on here, so let's break it down.
The getParam
function sends a ParamRequestRead
message to the FCU and waits for a ParamValue
message with the
requested parameter ID. The function returns the parameter value. The setParam
function sends a ParamSet
message to
the FCU and waits for a ParamValue
message with the updated parameter value. The function returns nothing.
You must be wondering why the async
and coroutineScope
functions are used here. We could have simply implemented
the getParam
and setParam
functions as follows.
The reason why we prefer the former implementations is that they are more robust considering the fact that in certain
cases, the response messages may be received by the underlying MavConnection
faster than MavRemoteNode.receive
is able to make the subscription to the message stream. Therefore, we need to ensure that the subscription is made
before the request is sent. This is where the async
function comes into play. It allows us to receive the message in
a branched coroutine and await it later in the parent coroutine. The coroutineScope
block is used for this branching.
Also, the entire function fails if the parent of the child coroutines fails. A delay
of 10 milliseconds is added to
ensure that the receive
function has reached the desired state before the request is sent.
Similar to the setParam
function, sending commands to the different systems is a frequently used operation.
These have been implemented in a similar way to the get and set param functions. We send the commands in the parent
coroutine while executing the MavRemoteNode.receive
function concurrently in a child coroutine, the result of which
is awaited in the at the end.
We finally have a simple, yet powerful API to interact with MAVLink systems. These functions are robust and can be easily composed with the Kotlin Coroutines library functions to handle complex transactions. However, a framework cannot be considered complete unless it addresses error handling and recovery mechanisms. In the next part of this series, we will build these mechanisms.