Skip to content

API Reference

Cassandra "ZZ Cat" Robinson edited this page Aug 29, 2024 · 17 revisions

Overview

By design, the API is meant to be simplistic & easy to integrate into your existing sketches/projects.
Code examples are provided as 'pseudo-code' to help convey the context of a particular function and how it should be used

Resource management

Resource management is provided here to explain how the resources (IE memory and hardware) used by CRSF for Arduino can be dynamically allocated as-and-when needed, in order to prevent memory and hardware leaks. Emphasis is placed on memory leaks in particular, because of how vulnerable embedded systems are to it, and because memory leaks are normalised in the maker space. Especially with those that are using the Arduino ecosystem.

You will see references to keywords such as new and delete, plus references to CRSF for Arduino being globally declared as a null pointer, and being initialised only when needed. This is in stark contrast to how Arduino teaches you how to manage objects.

To convey the point (using CRSF for Arduino in the examples) here, this is what Arduino teaches you:

/* Add the class' header to my "sketch". */
#include "CRSFforArduino.h"

/* Global declaration and initialisation of a class. */
CRSFforArduino crsf = CRSFforArduino();

void setup()
{
    /* Initialise the class. */
    crsf.begin();
}

void loop()
{
    /* Run the class' main function. */
    crsf.update();
}

So, what if your firmware is finished using the class object?
You would do something like this in your loop(), right?

void loop()
{
    /* Simulate the class object's main function running for 15 seconds, and then ending itself. */
    static bool crsfEnded = false;

    if (crsfEnded == false)
    {
        static uint32_t start = millis();
        uint32_t now = millis();

        /* Run the main function for 15 seconds. */
        if (now - start <= 15000)
        {
            crsf.update();
        }
        else
        {
            /* The class has finished running.
            It's time to end it. */
            crsf.end();

            /* Set the flag to tell the rest
            of the sketch that we are done here. */
            crsfEnded = true;
        }
    }
}

While this is "simple" and "easy-to-read", it is bad practice.
What you have essentially done here is you have created a memory leak.
In the context of embedded systems (which often has very small flash memory footprints), memory leaks MUST be avoided.
This is because a memory leak eventually consumes all available memory, and a system failure will result from the next call to allocate memory that simply does not exist. IE There is not enough memory available to allocate to something, resulting in the target micro-controller crashing or "locking up".

You do the maths about what happens if your embedded micro-controller is in say, for example, your home-brewed flight controller that you have put in one of your prized expensive helicopters, and that memory leak causes the system failure that I have described here.

Allow me to show you the light among the darkness.
A better way to manage resources is to, first off, declare your class objects as null pointers, like so...

/* Add the class' header to my "sketch". */
#include "CRSFforArduino.h"

/* Global class declaration as a null pointer. */
CRSFforArduino *crsf = nullptr;

This means no resources (IE memory and hardware) will be allocated until you tell it to do so.
This also means that, when resources are allocated, none of those resources will be freed until you tell your firmware to do so.
Fortunately, I have taken great care with respect to how CRSF for Arduino manages resources.
In the example below, I show you how to allocate memory and hardware, plus how to free those resources up when your firmware is finished using CRSF for Arduino...

void setup()
{
    /* Allocate the required memory for the class. */
    crsf = new CRSFforArduino();

    /* Allocate the required hardware of the class. */
    if (crsf->begin() == true)
    {
        /* Hardware allocated successfully.
        The rest of the class' functionality may be initialised here. */
    }
    else
    {
        /* Hardware allocation failed.
        Anything that succeeded during the call to 'begin()' MUST be
        freed to avoid hardware and memory leaks. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard the class' API functions from being accessed
    if the resources allocated were freed. */
    if (crsf != nullptr)
    {
        static uint32_t start = millis();
        uint32_t now = millis();

        /* Run the class' main function for 15 seconds. */
        if (now - start <= 15000)
        {
            crsf->update();
        }
        else
        {
            /* Timeout expired.
            All resources that were allocated MUST be freed,
            starting with freeing up the hardware. */
            crsf->end();

            /* Free up the memory that was allocated. */
            delete crsf;
            crsf = nullptr;
        }
    }
}

The API

Constructors & instantiation

Warning

You MUST NOT use any of CRSF for Arduino's API functions before instantiation.
You MUST create an instance of CRSF for Arduino before you can use its API.

Constructors

  • CRSFforArduino()

This is the default constructor. It creates an instance of CRSF for Arduino.
Serial1 from the Arduino API is used to provide communications to & from your TBS Crossfire or ExpressLRS receiver.
In the case of STM32 targets, Serial1 is not available on some STM32 targets.
Therefore, the next available HardwareSerial instance is automatically picked based on availability. EG CRSF for Arduino will default to Serial2 if it's available. If Serial2 is not available, Serial3 is automatically picked.
For ESP32 targets, pins 0 and 1 are used, or (if defined) D0 and D1 are used.

  • CRSFforArduino(HardwareSerial *serialPort)

This constructor is where you can provide your own custom HardwareSerial instance.
This gives you the flexibility of assigning any arbitrary UART port to CRSF for Arduino.

  • CRSFforArduino(HardwareSerial *serialPort, int RxPin, int TxPin)

This constructor is currently specific to ESP32 targets.
serialPort is a pointer to your desired serial port object.
RxPin is your specified UART Rx pin number on your development board.
TxPin is your specified UART Tx pin number on your development board.

Instantiation

In the example below, CRSF for Arduino will use the default HardwareSerial instance.
In most cases, this is Serial1 and the default pins are 0 for UART Rx and 1 for UART Tx.

#include "CRSFforArduino.h"

/* Declare a null pointer to CRSF for Arduino. */
CRSFforArduino *crsf = nullptr;

void setup()
{
    /* Instantiate CRSF for Arduino. */
    crsf = new CRSFforArduino();
}

void loop()
{
    /* Your main code here... */
}

If desired, you can customise the pin assignments either by the HardwareSerial pointer (if your target is SAMD21 or SAMD51 based) or you can specify the pin numbers (if your target is ESP32 based).
For SAMD21 and SAMD51 targets, Adafruit have already documented how to do custom UART pin assignments. In the context of CRSF for Arduino, all you need to do is pass in your custom Uart object into the CRSFforArduino(HardwareSerial *serialPort) constructor.

For ESP32 targets, this is what you do...

#include "CRSFforArduino.h"

/* Define your custom UART pinouts here. */
#define MY_UART_RX_PIN 2
#define MY_UART_TX_PIN 3

/* Declare a null pointer to CRSF for Arduino. */
CRSFforArduino *crsf = nullptr;

void setup()
{
    /* Instantiate CRSF for Arduino with your custom pins. */
    crsf = new CRSFforArduino(&Serial1, MY_UART_RX_PIN, MY_UART_TX_PIN);
}

void loop()
{
    /* Your main code here... */
}

Initialising & de-initialising CRSF for Arduino

This is where you actually start using CRSF for Arduino.

Initialiser

  • CRSFforArduino::begin()
    • Optional parameter:
      • baud_rate - Use this to set your desired baud rate. If no value is provided, the default is the baud rate defined by The Crossfire Protocol.
  • Return type: bool
    • true if initialisation is successful.
    • false if initialisation failed.

NB: The appropriate baud rate and configuration is done for you.

The example below demonstrates how to initialise CRSF for Arduino.
NB: The return check on the initialiser has been omitted for simplicity's sake.

#include "CRSFforArduino.h"

/* Declare a null pointer to CRSF for Arduino. */
CRSFforArduino *crsf = nullptr;

void setup()
{
    /* Instantiate CRSF for Arduino. */
    crsf = new CRSFforArduino();

    /* Initialise CRSF for Arduino. */
    crsf->begin();
}

void loop()
{
    /* Your main code here... */
}

De-initialiser

  • CRSFforArduino::end()

This de-initialises CRSF for Arduino by disabling hardware serial communications with your RC receiver, & freeing up the previously configured hardware serial port.

[!WARN] This does not delete any previously allocated memory for CRSF for Arduino.
You MUST delete this memory yourself using delete <YOUR_CRSF_OBJECT>, where <YOUR_CRSF_OBJECT> is the name you gave to your instance of > CRSF for Arduino. EG If you're following the examples above, the name given is crsf. So delete crsf;

The example below shows how to correctly de-initialise CRSF for Arduino and free up the resources that were allocated to it.
In this example, a limited number of executions are done before CRSF for Arduino is completely destroyed.

#include "CRSFforArduino.h"

int loopExecutions = 0;

CRSFforArduino *crsf = nullptr;

void setup()
{
    crsf = new CRSFforArduino();

    if (crsf->begin() != true)
    {
        /* CRSF for Arduino failed to initialise.
        Clean-up the resources that it previously allocated, and then free up the memory it allocated. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API with a null check. */
    if (crsf != nullptr)
    {
        /* Increment the loop executions counter. */
        loopExecutions++;

        /* After five loop executions, destroy CRSF for Arduino. */
        if (loopExecutions >= 5)
        {
            crsf->end();
            delete crsf;
            crsf = nullptr;
        }
    }
}

Running CRSF for Arduino

CRSF for Arduino uses an event-driven API for reading RC Channels, Link Statistics, and setting Flight Modes.
These events are handled internally and they have their own respective callback functions that you can register in your sketches.

The main function

  • CRSFforArduino::update()

This function SHOULD be placed inside your main loop() or in some other function or service routine that is updated as quickly as possible.
It should be updated at least every 4 milliseconds or faster.

The example below is a sample of how CRSF for Arduino's main function SHOULD be implemented in your firmware.

#include "CRSFforArduino.h"

CRSFforArduino *crsf = nullptr;

void setup()
{
    crsf = new CRSFforArduino();

    /* Initialise CRSF for Arduino, and clean up
    any allocated resources if initialisation fails. */
    if (crsf->begin() != true)
    {
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API with a null check. */
    if (crsf != nullptr)
    {
        /* Call CRSF for Arduino's main function here. */
        crsf->update();
    }
}

Events - Link Statistics

This is your first port of call, because it gives you insight as to the stability of your receiver's connection with your transmitter.
Here, you have access to the following information:

  • RSSI (dBm)
  • Link Quality Information (%)
  • Signal-to-Noise Ratio (dB)
  • Transmitter Module Power (W)

The following API function is used to set the Link Statistics callback handler:

  • CRSFforArduino::setLinkStatisticsCallback(void (*callback)(serialReceiverLayer::link_statistics_t linkStatistics))
    • This API function is used to set the Link Statistics callback handler.
    • Parameter:
      • linkStatistics
        • This structure contains all of your link statistics data.

The example below is a sample of how Link Statistics MAY be implemented in your firmware.

#include "CRSFforArduino.h"

CRSFforArduino *crsf = nullptr;

void onLinkStatisticsUpdate(serialReceiverLayer::link_statistics_t);

void setup()
{
    crsf = new CRSFforArduino();

    /* Initialise CRSF for Arduino. */
    if (crsf->begin() == true)
    {
        /* CRSF for Arduino initialised successfully.
        We can now register the Link Statistics event. */
        crsf->setLinkStatisticsCallback(onLinkStatisticsUpdate);
    }
    else
    {
        /* Clean up any resources,
        if initialisation fails. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API with a null check. */
    if (crsf != nullptr)
    {
        /* Call CRSF for Arduino's main function here. */
        crsf->update();
    }
}

void onLinkStatisticsUpdate(serialReceiverLayer::link_statistics_t linkStatistics)
{
    /* This is your Link Statistics Event Callback.
    By using the linkStatistics parameter that's passed in,
    you have access to the following:
    - linkStatistics.rssi
    - linkStatistics.lqi
    - linkStatistics.snr
    - linkStatistics.tx_power

    For the purposes of this example, these values are simply
    printed to the Serial Monitor at a rate of 5 Hz. */

    static unsigned long lastPrint = 0;
    if (millis() - lastPrint >= 200)
    {
        lastPrint = millis();
        Serial.print("Link Statistics: ");
        Serial.print("RSSI: ");
        Serial.print(linkStatistics.rssi);
        Serial.print(", Link Quality: ");
        Serial.print(linkStatistics.lqi);
        Serial.print(", Signal-to-Noise Ratio: ");
        Serial.print(linkStatistics.snr);
        Serial.print(", Transmitter Power: ");
        Serial.println(linkStatistics.tx_power);
    }
}

Events - RC Channels

Setting up RC channels is done by telling CRSF for Arduino what callback handler you are using.
You can do this using this API function:

  • CRSFforArduino::setRcChannelsCallback(void (*callback)(serialReceiverLayer::rcChannels_t *rcChannels))
    • This sets the callback handler function for your RC Channels.
    • Parameter:
      • rcChannels
        • This structure is automatically passed in, and it contains all sixteen of your RC channels, plus an additional fail-safe flag.

NB: The RC Channels values themselves are raw values. IE These are the values that have come directly off of your receiver.
If you need to convert these values over to "microseconds", you SHOULD use CRSFforArduino::rcToUs(uint16_t rc).

The example below is a sample of how RC Channels MAY be implemented in your firmware.

#include "CRSFforArduino.h"

CRSFforArduino *crsf = nullptr;

/* A flag to hold the fail-safe status. */
bool isFailsafeActive = false;

/* RC Channels data. */
int rcChannelCount = 8;
const char *rcChannelNames[] = {
    "A",
    "E",
    "T",
    "R",
    "Aux1",
    "Aux2",
    "Aux3",
    "Aux4",

    "Aux5",
    "Aux6",
    "Aux7",
    "Aux8",
    "Aux9",
    "Aux10",
    "Aux11",
    "Aux12"};

/* RC Channels Event Callback. */
void onReceiveRcChannels(serialReceiverLayer::rcChannels_t *rcData);

void setup()
{
    crsf = new CRSFforArduino();

    /* Initialise CRSF for Arduino. */
    if (crsf->begin() == true)
    {
        /* CRSF for Arduino initialised successfully.
        We can now register the RC Channels event. */
        crsf->setRcChannelsCallback(onReceiveRcChannels);

        /* Constrain the RC Channels Count to the maximum number
        of channels that are specified by The Crossfire Protocol.*/
        rcChannelCount = rcChannelCount > crsfProtocol::RC_CHANNEL_COUNT ? crsfProtocol::RC_CHANNEL_COUNT : rcChannelCount;
    }
    else
    {
        /* Clean up any resources,
        if initialisation fails. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API with a null check. */
    if (crsf != nullptr)
    {
        /* Call CRSF for Arduino's main function here. */
        crsf->update();
    }
}

void onReceiveRcChannels(serialReceiverLayer::rcChannels_t *rcData)
{
    /* This is your RC Channels Event Callback.
    Here, you have access to all 16 11-bit channels,
    plus an additional "failsafe" flag that tells you whether-or-not
    your receiver is connected to your controller's transmitter module.

    Using the rcData parameter that was passed in,
    you have access to the following:

    - failsafe - A boolean flag indicating the "Fail-safe" status.
    - value[16] - An array consisting of all 16 received RC channels.
      NB: RC Channels are RAW values and are NOT in "microseconds" units.

    For the purposes of this example, the fail-safe flag is used to centre
    all channels except for Channel 5 (AKA Aux1). Aux1 is set to the
    "Disarmed" position.
    The RC Channels themselves are all converted to "microseconds" for
    visualisation purposes, and printed to the Serial Monitor at a rate
    of 100 Hz. */

    if (rcData->failsafe)
    {
        if (!isFailsafeActive)
        {
            isFailsafeActive = true;

            /* Centre all RC Channels, except for Channel 5 (Aux1). */
            for (int i = 0; i < rcChannelCount; i++)
            {
                if (i != 4)
                {
                    rcData->value[i] = 992;
                }
            }

            /* Set Channel 5 (Aux1) to its minimum value. */
            rcData->value[4] = 191;

            Serial.println("[Sketch | WARN]: Failsafe detected.");
        }
    }
    else
    {
        /* Set the failsafe status to false. */
        if (isFailsafeActive)
        {
            isFailsafeActive = false;
            Serial.println("[Sketch | INFO]: Failsafe cleared.");
        }
    }

    /* Here is where you may write your RC channels implementation.
    For this example, RC Channels are simply sent to the Serial Monitor. */
    static uint32_t lastPrint = millis();

    if (millis() - lastPrint >= 10)
    {
        lastPrint = millis();

        Serial.print("[Sketch | INFO]: RC Channels: <");
        for (int i = 0; i < rcChannelCount; i++)
        {
            Serial.print(rcChannelNames[i]);
            Serial.print(": ");
            Serial.print(crsf->rcToUs(rcData->value[i]));

            if (i < (rcChannelCount - 1))
            {
                Serial.print(", ");
            }
        }
        Serial.println(">");
    }
}

Events - Flight Modes

Flight Modes are divided up into two subsections:

  1. Standard modes
    These are based on the Flight Modes used by Betaflight 4.3.
  2. Custom modes
    You assign a text-based string to your desired flight mode.

In either case, the API functions are universal, and are as follows:

  • CRSFforArduino::setFlightMode(serialReceiverLayer::flightModeId_t flightModeId, const char *flightModeName, uint8_t channel, uint16_t min, uint16_t max)
    • Use this to set your desired Flight Mode, assign the Flight Mode to your desired RC Channel, and provide the minimum and maximum values in which the Flight Mode will be active.
    • Parameters:
      • flightModeId
        • This is the index ID of your chosen Flight Mode.
      • flightModeName
        • This is where you provide your Flight Mode with a name.
          It is particularly useful with Custom Flight Modes.
      • channel
        • The channel number you are assigning your flight mode to. When this channel is between the min and max values, this Flight Mode is active.
      • min
        • The minimum channel value (in microseconds) that the Flight Mode will be activated.
      • max
        • The maximum channel value (in microseconds) that the Flight Mode will be activated.
    • Return type: bool
      • true if the Flight Mode was set.
      • false otherwise.
        Additionally, if CRSF_FLIGHTMODES_ENABLED in CFA_Config.hpp is 0, setFlightMode() will always return false.
  • CRSFforArduino::setFlightModeCallback(void (*callback)(serialReceiverLayer::flightModeId_t flightMode))
    • This is where you set the callback handler for the Flight Modes Event.
    • Parameter:
      • flightMode
        • This is automatically passed in, and you can read from this inside the Flight Modes Event Callback to determine what Flight Mode is active.
Standard Flight Modes

The example below is a sample of how Flight Modes MAY be implemented in your firmware.

#include "CRSFforArduino.hpp"

#define FLIGHT_MODE_ARM_CHANNEL 5
#define FLIGHT_MODE_ARM_MIN     1000
#define FLIGHT_MODE_ARM_MAX     1800

#define FLIGHT_MODE_ACRO_CHANNEL 6
#define FLIGHT_MODE_ACRO_MIN     750
#define FLIGHT_MODE_ACRO_MAX     1250

#define FLIGHT_MODE_ANGLE_CHANNEL 6
#define FLIGHT_MODE_ANGLE_MIN     1250
#define FLIGHT_MODE_ANGLE_MAX     1750

#define FLIGHT_MODE_HORIZON_CHANNEL 6
#define FLIGHT_MODE_HORIZON_MIN     1750
#define FLIGHT_MODE_HORIZON_MAX     2250

CRSFforArduino *crsf = nullptr;

/* Declare the onFlightModeUpdate callback function. */
void onFlightModeUpdate(serialReceiverLayer::flightModeId_t);

void setup()
{
    /* Initialise CRSF for Arduino. */
    crsf = new CRSFforArduino();
    if (crsf->begin() != false)
    {
        /* CRSF for Arduino initialised successfully.
        Now, the Flight Modes can be configured. */
        crsf->setFlightMode(serialReceiverLayer::FLIGHT_MODE_DISARMED, FLIGHT_MODE_ARM_CHANNEL, FLIGHT_MODE_ARM_MIN, FLIGHT_MODE_ARM_MAX);
        crsf->setFlightMode(serialReceiverLayer::FLIGHT_MODE_ACRO, FLIGHT_MODE_ACRO_CHANNEL, FLIGHT_MODE_ACRO_MIN, FLIGHT_MODE_ACRO_MAX);
        crsf->setFlightMode(serialReceiverLayer::FLIGHT_MODE_ANGLE, FLIGHT_MODE_ANGLE_CHANNEL, FLIGHT_MODE_ANGLE_MIN, FLIGHT_MODE_ANGLE_MAX);
        crsf->setFlightMode(serialReceiverLayer::FLIGHT_MODE_HORIZON, FLIGHT_MODE_HORIZON_CHANNEL, FLIGHT_MODE_HORIZON_MIN, FLIGHT_MODE_HORIZON_MAX);

        crsf->setFlightModeCallback(onFlightModeUpdate);
    }
    else
    {
        /* CRSF for Arduino failed to initialise. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API functions. */
    if (crsf != nullptr)
    {
        /* Run CRSF for Arduino's main function. */
        crsf->update();
    }
}

void onFlightModeUpdate(serialReceiverLayer::flightModeId_t flightMode)
{
    /* This is where you would normally write your own Flight Modes implementation.
    For the purposes of this example, the current Flight Mode is printed to the Serial Monitor. */

    /* Are we disarmed? */
    bool isDisarmed = true;
    if (flightMode != serialReceiverLayer::FLIGHT_MODE_DISARMED)
    {
        isDisarmed = false;
    }
    else if (isFailsafeActive)
    {
        isDisarmed = true;
    }

    /* Update the flight mode telemetry with the new value. */
    crsf->telemetryWriteFlightMode(flightMode, isDisarmed);

    /* Print the flight mode to the Serial Monitor, but only if the flight mode has changed. */
    static serialReceiverLayer::flightModeId_t lastFlightMode = serialReceiverLayer::FLIGHT_MODE_DISARMED;
    if (flightMode != lastFlightMode)
    {
        lastFlightMode = flightMode;

        Serial.print("[Sketch | INFO]: Flight Mode: ");
        switch (flightMode)
        {
            case serialReceiverLayer::FLIGHT_MODE_DISARMED:
                Serial.println("Disarmed");
                break;
            case serialReceiverLayer::FLIGHT_MODE_ACRO:
                Serial.println("Acro");
                break;
            case serialReceiverLayer::FLIGHT_MODE_WAIT:
                Serial.println("Wait for GPS Lock");
                break;
            case serialReceiverLayer::FLIGHT_MODE_FAILSAFE:
                Serial.println("Failsafe");
                break;
            case serialReceiverLayer::FLIGHT_MODE_GPS_RESCUE:
                Serial.println("GPS Rescue");
                break;
            case serialReceiverLayer::FLIGHT_MODE_PASSTHROUGH:
                Serial.println("Passthrough");
                break;
            case serialReceiverLayer::FLIGHT_MODE_ANGLE:
                Serial.println("Angle");
                break;
            case serialReceiverLayer::FLIGHT_MODE_HORIZON:
                Serial.println("Horizon");
                break;
            case serialReceiverLayer::FLIGHT_MODE_AIRMODE:
                Serial.println("Airmode");
                break;
            default:
                Serial.println("Unknown");
                break;
        }
    }
}
Custom Flight Modes

The example below is a sample of how Custom Flight Modes MAY be implemented in your firmware. Helicopter-centric flight modes (IE "Normal", "Idle-Up 1", and "Idle-Up 2") are used to convey the implementation of Custom Flight Modes.

#include "CRSFforArduino.hpp"

#define FLIGHT_MODE_ARM_CHANNEL 5
#define FLIGHT_MODE_ARM_MIN     1000
#define FLIGHT_MODE_ARM_MAX     1800

#define FLIGHT_MODE_NORMAL_CHANNEL  6
#define FLIGHT_MODE_NORMAL_MIN      750
#define FLIGHT_MODE_NORMAL_MAX      1250

#define FLIGHT_MODE_IDLEUP1_CHANNEL   6
#define FLIGHT_MODE_IDLEUP1_MIN       1250
#define FLIGHT_MODE_IDLEUP1_MAX       1750

#define FLIGHT_MODE_IDLEUP2_CHANNEL   6
#define FLIGHT_MODE_IDLEUP2_MIN       1750
#define FLIGHT_MODE_IDLEUP2_MAX       2250

CRSFforArduino *crsf = nullptr;

/* Declare the onFlightModeUpdate callback function. */
void onFlightModeUpdate(serialReceiverLayer::flightModeId_t);

void setup()
{
    /* Initialise CRSF for Arduino. */
    crsf = new CRSFforArduino();
    if (crsf->begin() != false)
    {
        /* CRSF for Arduino initialised successfully.
        Now, the Flight Modes can be configured. */
        crsf->setFlightMode(serialReceiverLayer::FLIGHT_MODE_DISARMED, "Disarmed", FLIGHT_MODE_ARM_CHANNEL, FLIGHT_MODE_ARM_MIN, FLIGHT_MODE_ARM_MAX);
        crsf->setFlightMode(serialReceiverLayer::CUSTOM_FLIGHT_MODE1, "Normal", FLIGHT_MODE_NORMAL_CHANNEL, FLIGHT_MODE_NORMAL_MIN, FLIGHT_MODE_NORMAL_MAX);
        crsf->setFlightMode(serialReceiverLayer::CUSTOM_FLIGHT_MODE2, "Idle-Up 1", FLIGHT_MODE_IDLEUP1_CHANNEL, FLIGHT_MODE_IDLEUP1_MIN, FLIGHT_MODE_IDLEUP1_MAX);
        crsf->setFlightMode(serialReceiverLayer::CUSTOM_FLIGHT_MODE3, "Idle-Up 2", FLIGHT_MODE_IDLEUP2_CHANNEL, FLIGHT_MODE_IDLEUP2_MIN, FLIGHT_MODE_IDLEUP2_MAX);

        crsf->setFlightModeCallback(onFlightModeUpdate);
    }
    else
    {
        /* CRSF for Arduino failed to initialise. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API functions. */
    if (crsf != nullptr)
    {
        /* Run CRSF for Arduino's main function. */
        crsf->update();
    }
}

void onFlightModeUpdate(serialReceiverLayer::flightModeId_t flightMode)
{
    /* This is where you would normally write your own Flight Modes implementation.
    For the purposes of this example, the current Flight Mode is printed to the Serial Monitor. */

    /* Are we disarmed? */
    bool isDisarmed = true;
    if (flightMode != serialReceiverLayer::FLIGHT_MODE_DISARMED)
    {
        isDisarmed = false;
    }
    else if (isFailsafeActive)
    {
        isDisarmed = true;
    }

    /* Update the flight mode telemetry with the new value. */
    crsf->telemetryWriteFlightMode(flightMode, isDisarmed);

    /* Print the flight mode to the Serial Monitor, but only if the flight mode has changed. */
    static serialReceiverLayer::flightModeId_t lastFlightMode = serialReceiverLayer::FLIGHT_MODE_DISARMED;
    if (flightMode != lastFlightMode)
    {
        lastFlightMode = flightMode;

        Serial.print("[Sketch | INFO]: Flight Mode: ");
        switch (flightMode)
        {
            case serialReceiverLayer::FLIGHT_MODE_DISARMED:
                Serial.println("Disarmed");
                break;
            case serialReceiverLayer::CUSTOM_FLIGHT_MODE1:
                Serial.println("Normal");
                break;
            case serialReceiverLayer::FLIGHT_MODE_WAIT:
                Serial.println("Wait for GPS Lock");
                break;
            case serialReceiverLayer::FLIGHT_MODE_FAILSAFE:
                Serial.println("Failsafe");
                break;
            case serialReceiverLayer::FLIGHT_MODE_GPS_RESCUE:
                Serial.println("GPS Rescue");
                break;
            case serialReceiverLayer::FLIGHT_MODE_PASSTHROUGH:
                Serial.println("Passthrough");
                break;
            case serialReceiverLayer::CUSTOM_FLIGHT_MODE2:
                Serial.println("Idle-Up 2");
                break;
            case serialReceiverLayer::CUSTOM_FLIGHT_MODE3:
                Serial.println("Idle-Up 3");
                break;
            default:
                Serial.println("Unknown");
                break;
        }
    }
}

Telemetry

CRSF for Arduino supports telemetry feedback to your controller.

Note

CRSF for Arduino does not provide any hardware sensor drivers.
You MUST provide these drivers yourself.
There are many fantastic sensor drivers available on GitHub, PlatformIO Registry and Arduino Library Manager.
Writing (and maintaining) sensor drivers is outside the scope of CRSF for Arduino.

You can implement Telemetry in your firmware with the following API functions:

  • CRSFforArduino::telemetryWriteAttitude(int16_t roll, int16_t pitch, int16_t yaw)
    • This function sends attitude telemetry back to your controller.
      All three parameters are in decidegress - EG Passing in a value of 100 will translate to 10 degrees on your controller's telemetry screen.
      If you're using either a IMU or 9-DoF fused orientation sensor, you MAY use the data from that sensor to provide attitude data input to the Attitude Telemetry.
    • Parameters:
      • roll
        Roll axis data.
      • pitch
        Pitch axis data.
      • yaw
        Yaw axis data.
  • CRSFforArduino::telemetryWriteBaroAltitude(uint16_t altitude, int16_t vario)
    • This function sends barometric altitude and variometer telemetry back to your controller.
      If you have a barometric pressure sensor, you MAY use the data from that sensor to provide altitude and variometer data input to the Barometric Altimeter Telemetry.
    • Parameters:
      • altitude Altitude data in decimetres - EG Passing in a value of 100 will translate to 10 metres on your controller's telemetry screen.
      • vario Variometer (AKA "Rate-of-climb/descent") data in centimetres per second. This unit is 1:1, so a value of 10 is 10 centimetres per second on your controller's telemetry screen.
  • CRSFforArduino::telemetryWriteBattery(float voltage, float current, uint32_t fuel, uint8_t percent)
    • One of the most vital telemetric categories is battery telemetry.
      This function sends your battery telemetry back to your controller.
    • Parameters:
      • voltage
        Battery voltage in millivolts * 100 - EG Passing in a value of 380.0F will translate to 3.8 volts on your controller's telemetry screen.
      • current Current drawn in milliamperes * 10 - EG Passing in a value of 150.0F will translate to 1.5 amperes on your controller's telemetry screen.
      • fuel The battery's "fuel" (AKA The amount of energy the battery has consumed) in milliampere-hours. This unit is 1:1, so passing in a value of 2200 will translate to 2200 mAh on your controller's telemetry screen.
        NB: You will need to provide your own calculations for fuel, as none is provided for you.
      • percent The remaining percentage of your battery's "fuel" as a percentage of the maximum "fuel" available.
        NB: You will need to provide your own calculations for percentage, as none is provided for you.
  • CRSFforArduino::telemetryWriteFlightMode(serialReceiverLayer::flightModeId_t flightMode, bool disarmed)
    • This function sends the flightMode and whether-or-not your firmware is disarmed back to your controller.
    • Parameters:
      • flightMode
        The Flight Mode that your firmware is currently in.
      • disarmed
        Wether-or-not your firmware is disarmed.
        true is disarmed and false is armed. CRSFforArduino::telemetryWriteGPS(float latitude, float longitude, float altitude, float speed, float groundCourse, uint8_t satellites)
    • This function sends location, speed, altitude, and number of satellites in view data back to your controller.
      If you have a GPS/GNSS (Collectively referred to as "GPS"), you MAY use the data from that to provide location, altitude, speed, course, and satellites-in-view data for GPS Telemetry.
    • Parameters:
      • latitude
        Latitude data is in decimal degrees.
      • longitude
        Longitude data is in decimal degrees.
      • altitude
        Altitude data is in centimetres
      • speed Speed data is in centimetres per second.
      • groundCourse Course data is in degrees.
      • satellites
        Number of satellites in view of the GPS.
        This is an indicator of how accurate the aforementioned GPS data is.

The code example below demonstrates how Telemetry MAY be implemented in your firmware.
NB: "Sensor Values" are picked by arbitrary numbers or are randomly generated.
In a real situation, you MUST provide your own sensor drivers to provide accurate data for your Telemetry.

#include "CRSFforArduino.hpp"

CRSFforArduino *crsf = nullptr;

/* Initialise the attitude telemetry with default values. */
int16_t roll = 0;  // Roll is in decided degrees (eg -200 = -20.0 degrees).
int16_t pitch = 0; // Pitch is in decided degrees (eg 150 = 15.0 degrees).
uint16_t yaw = 0;  // Yaw is in decided degrees (eg 2758 = 275.8 degrees).

/* Initialise the barometric altitude telemetry with default values. */
uint16_t baroAltitude = 0; // Barometric altitude is in decimetres (eg 10 = 1.0 metres).
int16_t verticalSpeed = 0; // Vertical speed is in centimetres per second (eg 50 = 0.5 metres per second).

/* Initialise the battery sensor telemetry with default values. */
float batteryVoltage = 420.0F; // Battery voltage is in millivolts (mV * 100).
float batteryCurrent = 000.0F; // Battery current is in milliamps (mA * 10).
uint32_t batteryFuel = 100;    // Battery fuel is in milliamp hours (mAh).
uint8_t batteryPercent = 100;  // Battery percentage remaining is in percent (0 - 100).

/* Initialise the GPS telemetry data with default values. */
float latitude = -41.18219482686493F; // Latitude is in decimal degrees.
float longitude = 174.9497131419602F; // Longitude is in decimal degrees.
float altitude = 100.0F;              // Altitude is in centimetres.
float speed = 0.0F;                   // Speed is in cm/s
float groundCourse = 275.8F;          // Ground Course is in degrees.
uint8_t satellites = 7;               // 7 satellites are in view (implies a 3D fix).

void pollSensors();

void setup()
{
    /* Initialise CRSF for Arduino. */
    crsf = new CRSFforArduino();
    if (crsf->begin() != false)
    {
        /* CRSF for Arduino initialised successfully.
        Nothing to configure for this example. */
    }
    else
    {
        /* CRSF for Arduino failed to initialise. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API functions. */
    if (crsf != nullptr)
    {
        /* Poll sensors and send telemetry. */
        pollSensors();

        /* Run CRSF for Arduino's main function. */
        crsf->update();
    }
}

void pollSensors()
{
    /* Update sensor data at a rate of 10 Hz. */
    static unsigned long lastSensorUpdate = millis();
    if (millis() - lastSensorUpdate >= 200)
    {
        lastSensorUpdate = millis();

        /* Update the attitude telemetry with the new values. */
        crsf->telemetryWriteAttitude(roll, pitch, yaw);

        /* Update the barometric altitude telemetry with the new values. */
        crsf->telemetryWriteBaroAltitude(baroAltitude, verticalSpeed);

        /* Update the battery sensor telemetry with the new values. */
        crsf->telemetryWriteBattery(batteryVoltage, batteryCurrent, batteryFuel, batteryPercent);

        /* Update the GPS telemetry data with the new values. */
        crsf->telemetryWriteGPS(latitude, longitude, altitude, speed, groundCourse, satellites);
    }
}

Tip

If you're having issues with your telemetry being slow to respond, try some of the following:

  • Set the Telemetry Ratio on your transmitter module to suit your need.
    You can do this either via the ExpressLRS Lua Script or you can do this on the module itself (if applicable).
    The smaller the Telemetry Ratio number is, the faster your telemetry will update. EG 1:16 is significantly faster than 1:128.
  • Avoid the use of functions such as delay() and delayMicroseconds() in the main loop() of your sketches.
    The use of these functions is considered bad practice, because it can cause other tasks to lag behind and completely miss their cue to operate.
    If you need to keep a track of time, make use of watching the return values of either millis() or micros() in the form of time stamps and compare the last time that time stamp was updated with the latest time stamp value.
  • Avoid long-running for, while and do{}while(/* condition */) statements.
    The use of these loops SHOULD be used sparingly and their implementation SHOULD be restricted to iterating through data buffers or performing repetitive tasks that would otherwise involve writing finite amounts of the same function over-and-over again.
  • If your receiver is connected to your development board through "solder-less" hook-up (AKA jumper) wires, check your connections, as these are often the causes of intermittent fail-safes and your controller spamming "telemetry lost"-"telemetry recovered".