Module maviola::docs::a3__sync_api

source Ā·
Expand description

Ā§šŸ“– 1.3. Synchronous API

ā† Overview | Asynchronous API ā†’

If youā€™ve read Overview, you may already familiarize yourself with the basics of synchronous API. This chapter will describe it in depth.

In any case, we suggest you at least to check Choosing Your API before reading this.

Ā§Contents

  1. Install
  2. Basics
  3. Receiving
  4. Sending
    1. Sending Frames
    2. Proxy Nodes & Devices
    3. Dependent Nodes
  5. Handling Peers
  6. Active Nodes & Heartbeats
  7. Multithreading
  8. Embedded Devices

Ā§Install

To use synchronous API we have to install Maviola with sync feature flag.

cargo add maviola --features sync

Ā§Basics

Letā€™s catch up with the example from the Quickstart chapter:

use maviola::prelude::*;
use maviola::sync::prelude::*;

let node = Node::sync::<V2>()
    .id(MavLinkId::new(1, 17))
    .connection(TcpServer::new("127.0.0.1:5600").unwrap())
    .build().unwrap();

for event in node.events() {
    match event {
        Event::NewPeer(peer) => {
            println!("New MAVLink device joined the network: {:?}", peer);
        }
        Event::PeerLost(peer) => {
            println!("MAVLink device is no longer active: {:?}", peer);
        }
        Event::Frame(frame, callback) => {
            if let Ok(message) = frame.decode::<DefaultDialect>() {
                println!(
                    "Received a message from {}:{}: {:?}",
                    frame.system_id(), frame.component_id(), message
                );
                callback.broadcast(&frame).unwrap();
            }
        }
        _ => {}
    }
}

Here weā€™ve created a Node with system_id=1 and component_id=17 that serves as a TCP server bound to 127.0.0.1:5600. Then we subscribe to node events, intercepting incoming frames and broadcasting them to all TCP clients except those that sent the original frame.

We will use this example as a basis for further discussion.

ā“˜ Synchronous API lives in the sync module. You can always check its documentation for details.

Ā§Receiving

As weā€™ve mentioned early, the events method is the suggested approach for dealing with everything that node receives. You can check the documentation for Event to learn more about available events.

ā“˜ To access events we need to import ReceiveEvent trait. We donā€™t do this explicitly since we use sync::prelude.

Still, if we are not interested in monitoring peers we can subscribe to frames directly. This method that returns an iterator over valid incoming frames:

for (frame, callback) in node.frames() {
    if let Ok(message) = frame.decode::<DefaultDialect>() {
        println!(
            "Received a message from {}:{}: {:?}",
            frame.system_id(), frame.component_id(), message
        );
        callback.broadcast(&frame).unwrap();
    }
}

ā“˜ Working on the frame level requires importing ReceiveFrame trait. Once again, sync::prelude can do it for us.

We are not bound to use iterators. In some cases you might be interested in receiving just the next Event or Frame. For example:

let next_event = node.recv().unwrap();

Or in case of the frame:

let next_frame = node.recv_frame().unwrap();

The interface for receiving events and frames is very similar to std::sync::mpsc::Receiver. The difference is that we return our own set of errors:

Another important difference is that we may have multiple receivers for the same channel as explained in the Multithreading below.

Ā§Sending

Weā€™ve already learned how to respond to a frame using callback. We suggest to check Callback documentation to learn more about all available methods.

ā“˜ Working with Callback requires importing CallbackApi trait. The other reason to use prelude that imports it for us.

If we want to send messages proactively, then need to use nodeā€™s sending API:

let message = default_dialect::messages::Heartbeat::default();
node.send(&message).unwrap();

Ā§Sending Frames

This covers most of the cases. However, sometimes we may want to send frame directly instead of message. In such case we need a send_frame method:

let message = default_dialect::messages::Heartbeat::default();
let frame = node.next_frame(&message).unwrap();
node.send_frame(&frame).unwrap();

ā“˜ To send frames we need to import SendFrame trait. Sending messages requires additional SendMessage trait to be imported as well. Both of these traits are available in prelude.

Ā§Proxy Nodes & Devices

The above approach works only for edge nodes (i.e. EdgeNode). If we are dealing with a ProxyNode, then we need to use different approach. We need to create a Device with specified system and component IDs:

let device = Device::new(MavLinkId::new(2, 42), &node);

Then we can create and send frames in the same fashion:

let message = default_dialect::messages::Heartbeat::default();
let frame = device.next_frame(&message).unwrap();
node.send_frame(&frame).unwrap();

āš  It is important to remember, that if you communicate on behalf of a device, MAVLink specification requires you to send heartbeats. In Maviola only edge nodes can do that automatically as described in Active Nodes & Heartbeats. In the case of devices you have to send heartbeats manually or use dependent nodes.

Ā§Dependent Nodes

While Device abstraction is useful ang gives a fine-grained control over frame processing, in most cases it would be advantageous to reuse a connection of an existing node for the new one. Such nodes are called ā€œdependentā€ nodes and can be built using node builder:

let proxy_node = Node::sync::<V2>()
    .connection(TcpServer::new("127.0.0.1:5600").unwrap())
    /* we can add frame processing settings here */
    .build().unwrap();

let mut edge_node = Node::sync()
    .id(MavLinkId::new(1, 17))
    /* other node settings that do not include connection */
    .build_from(&proxy_node);

Such nodes can be created only from a ProxyNode and are always EdgeNodes. They will use FrameProcessor::compat and FrameProcessor::signer from a ā€œparentā€ node if these parameters hasnā€™t been set explicitly for the ā€œdependentā€ node. They will also add all known dialects from the parent edge node and all custom processors.

Ā§Handling Peers

As youā€™ve probably seen, we have special events Event::NewPeer and Event::PeerLost. These events are signaling that a certain peer sent their first heartbeat or certain peer hasnā€™t been seen for a while. Peers are distinguished purely by their system and component IDs.

The duration after which peer will be considered lost is defined by Node::heartbeat_timeout the default value is DEFAULT_HEARTBEAT_TIMEOUT. You can set this value when building a node:

use std::time::Duration;

Node::sync::<V2>()
    .heartbeat_timeout(Duration::from_millis(500))
    /* other node settings */

Ā§Active Nodes & Heartbeats

Itā€™s nice to receive heartbeats. But what about sending them? Simple. Letā€™s first create an edge node:

let mut node = Node::sync::<V2>()
    .id(MavLinkId::new(1, 17))
    .connection(TcpServer::new("127.0.0.1:5600").unwrap())
    .build().unwrap();

Then activate it:

node.activate().unwrap();

This will transition node into active mode, and it will start to send automatic heartbeats immediately. The default heartbeat interval is defined by DEFAULT_HEARTBEAT_INTERVAL constant. You can change it during node construction:

use std::time::Duration;

Node::sync::<V2>()
    .id(MavLinkId::new(1, 17))
    .heartbeat_interval(Duration::from_millis(500))
    /* other node settings */

Finally, you can deactivate active node to prevent it from sending heartbeats by calling Node::deactivate.

You can check whether node is active by calling Node::is_active.

Ā§Multithreading

Nodes handle connections and therefore are neither Sync nor Send. You obviously may wrap them with Arc or even arc-mutex them but this not always what you want. First, mutexes are not just heavy, they also not always convenient. And in the case of the Arc you canā€™t explicitly drop the node since some nasty function may still hold a reference to it.

To solve this problem, we provide Node::sender and Node::receiver methods which return sending and receiving parts of a node.

To send frames in other thread obtain a FrameSender that implements SendFrame and SendMessage traits and use it in the same way you are using node:

use std::thread;

let sender = node.sender();

thread::spawn(move || {
    sender.send(
        &default_dialect::messages::Heartbeat::default()
    ).unwrap();
}).join().unwrap();

If instead you want to receive frames or events in other thread, obtain a EventReceiver that implements ReceiveEvent and ReceiveFrame traits and use it freely:

use std::thread;

let receiver = node.receiver().clone();

thread::spawn(move || {
    for (frame, callback) in receiver.frames() {
        callback.send(&frame).unwrap();
    }
}).join().unwrap();

And, yes, you can respond to frames from a receiver using callback.

ā“˜ The interesting difference between Node::sender and Node::receiver is that the latter returns a reference. Which means that you may gain some performance improvement by refraining from cloning it if you are using receiver in the same thread. This also makes sense since receivers have access only to events that were emitted after their creation. This is related to the limitations of the underlying MPMC channel.

Ā§Embedded Devices

While Maviola re-exports no-std compatible Sender and Receiver from Mavio, you canā€™t use Maviola for embedded devices without std support. However, since Maviola is based on Mavio, you can use the latter as solution for embedded devices and then use your Mavio-based libraries to extend Maviola functionality.

This is because both Maviola and Mavio are parts of the same Mavka toolchain, they are designed to support each other.

ā† Overview | Asynchronous API ā†’