1
0
Fork 0
mirror of https://github.com/betaflight/betaflight.git synced 2025-07-13 03:20:00 +03:00
betaflight/src/main/osd/osd_elements.c
Kevin Plaizier 0d94108068
Update the google olc code to not have compiler warnings (#14484)
* Update the google olc code to not have compiler warnings

* With newer BF updates (double) is no longer needed in olc.c
2025-06-30 19:18:29 +02:00

2588 lines
84 KiB
C

/*
* This file is part of Cleanflight and Betaflight.
*
* Cleanflight and Betaflight are free software. You can redistribute
* this software and/or modify this software under the terms of the
* GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* Cleanflight and Betaflight are distributed in the hope that they
* will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software.
*
* If not, see <http://www.gnu.org/licenses/>.
*/
/*
*****************************************
Instructions for adding new OSD Elements:
*****************************************
First add the new element to the osd_items_e enumeration in osd/osd.h. The
element must be added to the end just before OSD_ITEM_COUNT.
Next add the element to the osdElementDisplayOrder array defined in this file.
If the element needs special runtime conditional processing then it should be added
to the osdAddActiveElements() function instead.
Create the function to "draw" the element.
------------------------------------------
It should be named like "osdElementSomething()" where the "Something" describes
the element. The drawing function should only render the dynamic portions of the
element. If the element has static (unchanging) portions then those should be
rendered in the background function. The exception to this is elements that are
expected to blink (have a warning associated). In this case the entire element
must be handled in the main draw function and you can't use the background capability.
Add the mapping from the element ID added in the first step to the function
created in the third step to the osdElementDrawFunction array.
Create the function to draw the element's static (background) portion.
---------------------------------------------------------------------
If an element has static (unchanging) portions then create a function to draw only those
parts. It should be named like "osdBackgroundSomething()" where the "Something" matches
the related element function.
Add the mapping for the element ID to the background drawing function to the
osdElementBackgroundFunction array.
You should also add a corresponding entry to the file: cms_menu_osd.c
Accelerometer reqirement:
-------------------------
If the new element utilizes the accelerometer, add it to the osdElementsNeedAccelerometer() function.
Finally add a CLI parameter for the new element in cli/settings.c.
CLI parameters should be added before line #endif // end of #ifdef USE_OSD
*/
/*
*********************
OSD element variants:
*********************
Each element can have up to 4 display variants. "Type 1" is always the default and every
every element has an implicit type 1 variant even if no additional options exist. The
purpose is to allow the user to choose a different element display or rendering style to
fit their needs. Like displaying GPS coordinates in a different format, displaying a voltage
with a different number of decimal places, etc. The purpose is NOT to display unrelated
information in different variants of the element. For example it would be inappropriate
to use variants to display RSSI for one type and link quality for another. In this case
they should be separate elements. Remember that element variants are mutually exclusive
and only one type can be displayed at a time. So they shouldn't be used in cases where
the user would want to display different types at the same time - like in the above example
where the user might want to display both RSSI and link quality at the same time.
As variants are added to the firmware, support must also be included in the Configurator.
The following lists the variants implemented so far (please update this as variants are added):
OSD_ALTITUDE
type 1: Altitude with one decimal place
type 2: Altitude with no decimal (whole number only)
OSD_GPS_LON
OSD_GPS_LAT
type 1: Decimal representation with 7 digits
type 2: Decimal representation with 4 digits
type 3: Degrees, minutes, seconds
type 4: Open location code (Google Plus Code)
OSD_MAIN_BATT_USAGE
type 1: Graphical bar showing remaining battery (shrinks as used)
type 2: Graphical bar showing battery used (grows as used)
type 3: Numeric % of remaining battery
type 4: Numeric % or used battery
VTX_CHANNEL
type 1: Contains Band:Channel:Power:Pit
type 2: Contains only Power
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include "platform.h"
#ifdef USE_OSD
#include "blackbox/blackbox.h"
#include "blackbox/blackbox_io.h"
#include "build/build_config.h"
#include "build/debug.h"
#include "cli/settings.h"
#include "common/axis.h"
#include "common/maths.h"
#include "common/printf.h"
#include "common/typeconversion.h"
#include "common/utils.h"
#include "common/unit.h"
#include "common/filter.h"
#include "config/config.h"
#include "config/feature.h"
#include "drivers/display.h"
#include "drivers/dshot.h"
#include "drivers/osd_symbols.h"
#include "drivers/time.h"
#include "drivers/vtx_common.h"
#include "drivers/pinio.h"
#include "fc/controlrate_profile.h"
#include "fc/core.h"
#include "fc/gps_lap_timer.h"
#include "fc/rc_adjustments.h"
#include "fc/rc_controls.h"
#include "fc/runtime_config.h"
#include "flight/gps_rescue.h"
#include "flight/position.h"
#include "flight/imu.h"
#include "flight/mixer.h"
#include "flight/pid.h"
#include "io/gps.h"
#include "io/vtx.h"
#include "osd/osd.h"
#include "osd/osd_elements.h"
#include "osd/osd_warnings.h"
#include "pg/motor.h"
#include "pg/pilot.h"
#include "pg/stats.h"
#include "rx/rx.h"
#include "sensors/adcinternal.h"
#include "sensors/barometer.h"
#include "sensors/battery.h"
#include "sensors/sensors.h"
#include "sensors/rangefinder.h"
#ifdef USE_GPS_PLUS_CODES
// located in lib/main/google/olc
#include "olc.h"
#endif
#define AH_SYMBOL_COUNT 9
#define AH_SIDEBAR_WIDTH_POS 7
#define AH_SIDEBAR_HEIGHT_POS 3
// Stick overlay size
#define OSD_STICK_OVERLAY_WIDTH 7
#define OSD_STICK_OVERLAY_HEIGHT 5
#define OSD_STICK_OVERLAY_SPRITE_HEIGHT 3
#define OSD_STICK_OVERLAY_VERTICAL_POSITIONS (OSD_STICK_OVERLAY_HEIGHT * OSD_STICK_OVERLAY_SPRITE_HEIGHT)
#define FULL_CIRCLE 360
#define EFFICIENCY_MINIMUM_SPEED_CM_S 100
#define EFFICIENCY_CUTOFF_HZ 0.5f
static pt1Filter_t batteryEfficiencyFilt;
#define MOTOR_STOPPED_THRESHOLD_RPM 1000
#define SINE_25_DEG 0.422618261740699f
#ifdef USE_OSD_STICK_OVERLAY
typedef struct radioControls_s {
uint8_t left_vertical;
uint8_t left_horizontal;
uint8_t right_vertical;
uint8_t right_horizontal;
} radioControls_t;
static const radioControls_t radioModes[4] = {
{ PITCH, YAW, THROTTLE, ROLL }, // Mode 1
{ THROTTLE, YAW, PITCH, ROLL }, // Mode 2
{ PITCH, ROLL, THROTTLE, YAW }, // Mode 3
{ THROTTLE, ROLL, PITCH, YAW }, // Mode 4
};
#endif
static const char compassBar[] = {
SYM_HEADING_W,
SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
SYM_HEADING_N,
SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
SYM_HEADING_E,
SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
SYM_HEADING_S,
SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
SYM_HEADING_W,
SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
SYM_HEADING_N,
SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE
};
static unsigned activeOsdElementCount = 0;
static uint8_t activeOsdElementArray[OSD_ITEM_COUNT];
static bool backgroundLayerSupported = false;
// Blink control
#define OSD_BLINK_FREQUENCY_HZ 2
static bool blinkState = true;
static uint32_t blinkBits[(OSD_ITEM_COUNT + 31) / 32];
#define SET_BLINK(item) (blinkBits[(item) / 32] |= (1 << ((item) % 32)))
#define CLR_BLINK(item) (blinkBits[(item) / 32] &= ~(1 << ((item) % 32)))
#define IS_BLINK(item) (blinkBits[(item) / 32] & (1 << ((item) % 32)))
#define BLINK(item) (IS_BLINK(item) && blinkState)
// Current element and render status
static osdElementParms_t activeElement;
static bool displayPendingForeground;
static bool displayPendingBackground;
static char elementBuff[OSD_ELEMENT_BUFFER_LENGTH];
// Return whether element is a SYS element and needs special handling
#define IS_SYS_OSD_ELEMENT(item) (item >= OSD_SYS_GOGGLE_VOLTAGE) && (item <= OSD_SYS_FAN_SPEED)
enum {UP, DOWN};
static int osdDisplayWrite(osdElementParms_t *element, uint8_t x, uint8_t y, uint8_t attr, const char *s)
{
if (IS_BLINK(element->item)) {
attr |= DISPLAYPORT_BLINK;
}
return displayWrite(element->osdDisplayPort, x, y, attr, s);
}
static int osdDisplayWriteChar(osdElementParms_t *element, uint8_t x, uint8_t y, uint8_t attr, char c)
{
char buf[2];
buf[0] = c;
buf[1] = 0;
return osdDisplayWrite(element, x, y, attr, buf);
}
#if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
typedef int (*getEscRpmOrFreqFnPtr)(int i);
static int getEscRpm(int i)
{
#ifdef USE_DSHOT_TELEMETRY
if (useDshotTelemetry) {
return lrintf(getDshotRpm(i));
}
#endif
#ifdef USE_ESC_SENSOR
if (featureIsEnabled(FEATURE_ESC_SENSOR)) {
return lrintf(erpmToRpm(getEscSensorData(i)->rpm));
}
#endif
return 0;
}
static int getEscRpmFreq(int i)
{
return getEscRpm(i) / 60;
}
static void renderOsdEscRpmOrFreq(getEscRpmOrFreqFnPtr escFnPtr, osdElementParms_t *element)
{
static uint8_t motor = 0;
const int rpm = MIN((*escFnPtr)(motor),99999);
tfp_sprintf(element->buff, "%d", rpm);
element->elemOffsetY = motor;
if (++motor == getMotorCount()) {
motor = 0;
} else {
element->rendered = false;
}
}
#endif
#if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
int osdConvertTemperatureToSelectedUnit(int tempInDegreesCelcius)
{
switch (osdConfig()->units) {
case UNIT_IMPERIAL:
return lrintf(((tempInDegreesCelcius * 9.0f) / 5) + 32);
default:
return tempInDegreesCelcius;
}
}
#endif
static void osdFormatAltitudeString(char * buff, int32_t altitudeCm, osdElementType_e variantType)
{
static const struct {
uint8_t decimals;
bool asl;
} variantMap[] = {
[OSD_ELEMENT_TYPE_1] = { 1, false },
[OSD_ELEMENT_TYPE_2] = { 0, false },
[OSD_ELEMENT_TYPE_3] = { 1, true },
[OSD_ELEMENT_TYPE_4] = { 0, true },
};
int32_t alt = altitudeCm;
#ifdef USE_GPS
if (variantMap[variantType].asl) {
alt = getAltitudeAsl();
}
#endif
unsigned decimalPlaces = variantMap[variantType].decimals;
const char unitSymbol = osdGetMetersToSelectedUnitSymbol();
osdPrintFloat(buff, SYM_ALTITUDE, osdGetMetersToSelectedUnit(alt) / 100.0f, "", decimalPlaces, true, unitSymbol);
}
#ifdef USE_GPS
static void osdFormatCoordinate(char *buff, gpsCoordinateType_e coordinateType, osdElementType_e variantType)
{
int32_t gpsValue = 0;
const char leadingSymbol = (coordinateType == GPS_LONGITUDE) ? SYM_LON : SYM_LAT;
if (STATE(GPS_FIX_EVER)) { // don't display interim coordinates until we get the first position fix
gpsValue = (coordinateType == GPS_LONGITUDE) ? gpsSol.llh.lon : gpsSol.llh.lat;
}
const int degreesPart = abs(gpsValue) / GPS_DEGREES_DIVIDER;
int fractionalPart = abs(gpsValue) % GPS_DEGREES_DIVIDER;
switch (variantType) {
#ifdef USE_GPS_PLUS_CODES
#define PLUS_CODE_DIGITS 11
case OSD_ELEMENT_TYPE_4: // Open Location Code
{
*buff++ = SYM_SAT_L;
*buff++ = SYM_SAT_R;
if (STATE(GPS_FIX_EVER)) {
OLC_LatLon location;
location.lat = (double)gpsSol.llh.lat / GPS_DEGREES_DIVIDER;
location.lon = (double)gpsSol.llh.lon / GPS_DEGREES_DIVIDER;
OLC_Encode(&location, PLUS_CODE_DIGITS, buff);
} else {
memset(buff, SYM_HYPHEN, PLUS_CODE_DIGITS + 1);
buff[8] = '+';
buff[PLUS_CODE_DIGITS + 1] = '\0';
}
break;
}
#endif // USE_GPS_PLUS_CODES
case OSD_ELEMENT_TYPE_3: // degree, minutes, seconds style. ddd^mm'ss.00"W
{
char trailingSymbol;
*buff++ = leadingSymbol;
const int minutes = fractionalPart * 60 / GPS_DEGREES_DIVIDER;
const int fractionalMinutes = fractionalPart * 60 % GPS_DEGREES_DIVIDER;
const int seconds = fractionalMinutes * 60 / GPS_DEGREES_DIVIDER;
const int tenthSeconds = (fractionalMinutes * 60 % GPS_DEGREES_DIVIDER) * 10 / GPS_DEGREES_DIVIDER;
if (coordinateType == GPS_LONGITUDE) {
trailingSymbol = (gpsValue < 0) ? 'W' : 'E';
} else {
trailingSymbol = (gpsValue < 0) ? 'S' : 'N';
}
tfp_sprintf(buff, "%u%c%02u%c%02u.%u%c%c", degreesPart, SYM_GPS_DEGREE, minutes, SYM_GPS_MINUTE, seconds, tenthSeconds, SYM_GPS_SECOND, trailingSymbol);
break;
}
case OSD_ELEMENT_TYPE_2:
fractionalPart /= 1000;
FALLTHROUGH;
case OSD_ELEMENT_TYPE_1:
default:
*buff++ = leadingSymbol;
if (gpsValue < 0) {
*buff++ = SYM_HYPHEN;
}
tfp_sprintf(buff, (variantType == OSD_ELEMENT_TYPE_1 ? "%u.%07u" : "%u.%04u"), degreesPart, fractionalPart);
break;
}
}
#endif // USE_GPS
void osdFormatDistanceString(char *ptr, int distance, char leadingSymbol)
{
const float convertedDistance = osdGetMetersToSelectedUnit(distance);
char unitSymbol;
char unitSymbolExtended;
int unitTransition;
switch (osdConfig()->units) {
case UNIT_IMPERIAL:
unitTransition = 5280;
unitSymbol = SYM_FT;
unitSymbolExtended = SYM_MILES;
break;
default:
unitTransition = 1000;
unitSymbol = SYM_M;
unitSymbolExtended = SYM_KM;
break;
}
unsigned decimalPlaces;
float displayDistance;
char displaySymbol;
if (convertedDistance < unitTransition) {
decimalPlaces = 0;
displayDistance = convertedDistance;
displaySymbol = unitSymbol;
} else {
displayDistance = convertedDistance / unitTransition;
displaySymbol = unitSymbolExtended;
if (displayDistance >= 10) { // >= 10 miles or km - 1 decimal place
decimalPlaces = 1;
} else { // < 10 miles or km - 2 decimal places
decimalPlaces = 2;
}
}
osdPrintFloat(ptr, leadingSymbol, displayDistance, "", decimalPlaces, false, displaySymbol);
}
static void osdFormatPID(char * buff, const char * label, uint8_t axis)
{
tfp_sprintf(buff, "%s %3d %3d %3d %3d %3d", label,
currentPidProfile->pid[axis].P,
currentPidProfile->pid[axis].I,
currentPidProfile->pid[axis].D,
currentPidProfile->d_max[axis],
currentPidProfile->pid[axis].F);
}
#ifdef USE_RTC_TIME
bool osdFormatRtcDateTime(char *buffer)
{
dateTime_t dateTime;
if (!rtcGetDateTime(&dateTime)) {
buffer[0] = '\0';
return false;
}
switch (activeElement.type) {
case OSD_ELEMENT_TYPE_2:
tfp_sprintf(buffer, "%02d.%02d %02d:%02d", dateTime.month, dateTime.day, dateTime.hours, dateTime.minutes);
break;
case OSD_ELEMENT_TYPE_1:
default:
dateTimeFormatLocalShort(buffer, &dateTime);
break;
}
return true;
}
#endif
void osdFormatTime(char * buff, osd_timer_precision_e precision, timeUs_t time)
{
int seconds = time / 1000000;
const int minutes = seconds / 60;
seconds = seconds % 60;
switch (precision) {
case OSD_TIMER_PREC_SECOND:
default:
tfp_sprintf(buff, "%02d:%02d", minutes, seconds);
break;
case OSD_TIMER_PREC_HUNDREDTHS:
{
const int hundredths = (time / 10000) % 100;
tfp_sprintf(buff, "%02d:%02d.%02d", minutes, seconds, hundredths);
break;
}
case OSD_TIMER_PREC_TENTHS:
{
const int tenths = (time / 100000) % 10;
tfp_sprintf(buff, "%02d:%02d.%01d", minutes, seconds, tenths);
break;
}
}
}
static char osdGetTimerSymbol(osd_timer_source_e src)
{
switch (src) {
case OSD_TIMER_SRC_ON:
return SYM_ON_M;
case OSD_TIMER_SRC_TOTAL_ARMED:
case OSD_TIMER_SRC_LAST_ARMED:
return SYM_FLY_M;
case OSD_TIMER_SRC_ON_OR_ARMED:
return ARMING_FLAG(ARMED) ? SYM_FLY_M : SYM_ON_M;
case OSD_TIMER_SRC_LAUNCH_TIME:
return 'L';
default:
return ' ';
}
}
static timeUs_t osdGetTimerValue(osd_timer_source_e src)
{
switch (src) {
case OSD_TIMER_SRC_ON:
return micros();
case OSD_TIMER_SRC_TOTAL_ARMED:
return osdFlyTime;
case OSD_TIMER_SRC_LAST_ARMED: {
statistic_t *stats = osdGetStats();
return stats->armed_time;
}
case OSD_TIMER_SRC_ON_OR_ARMED:
return ARMING_FLAG(ARMED) ? osdFlyTime : micros();
case OSD_TIMER_SRC_LAUNCH_TIME:
return osdLaunchTime;
default:
return 0;
}
}
void osdFormatTimer(char *buff, bool showSymbol, bool usePrecision, int timerIndex)
{
const uint16_t timer = osdConfig()->timers[timerIndex];
const uint8_t src = OSD_TIMER_SRC(timer);
if (showSymbol) {
*(buff++) = osdGetTimerSymbol(src);
}
osdFormatTime(buff, (usePrecision ? OSD_TIMER_PRECISION(timer) : OSD_TIMER_PREC_SECOND), osdGetTimerValue(src));
}
static char osdGetBatterySymbol(int cellVoltage)
{
if (getBatteryState() == BATTERY_CRITICAL) {
return SYM_MAIN_BATT; // FIXME: currently the BAT- symbol, ideally replace with a battery with exclamation mark
} else {
// Calculate a symbol offset using cell voltage over full cell voltage range
const int symOffset = scaleRange(cellVoltage, batteryConfig()->vbatmincellvoltage, batteryConfig()->vbatmaxcellvoltage, 0, 8);
return SYM_BATT_EMPTY - constrain(symOffset, 0, 6);
}
}
static uint8_t osdGetHeadingIntoDiscreteDirections(int heading, unsigned directions)
{
heading += FULL_CIRCLE; // Ensure positive value
// Split input heading 0..359 into sectors 0..(directions-1), but offset
// by half a sector so that sector 0 gets centered around heading 0.
// We multiply heading by directions to not loose precision in divisions
// In this way each segment will be a FULL_CIRCLE length
int direction = (heading * directions + FULL_CIRCLE / 2) / FULL_CIRCLE; // scale with rounding
direction %= directions; // normalize
return direction; // return segment number
}
static uint8_t osdGetDirectionSymbolFromHeading(int heading)
{
heading = osdGetHeadingIntoDiscreteDirections(heading, 16);
// Now heading has a heading with Up=0, Right=4, Down=8 and Left=12
// Our symbols are Down=0, Right=4, Up=8 and Left=12
// There're 16 arrow symbols. Transform it.
heading = 16 - heading;
heading = (heading + 8) % 16;
return SYM_ARROW_SOUTH + heading;
}
/**
* Converts altitude based on the current unit system.
* @param meters Value in meters to convert
*/
float osdGetMetersToSelectedUnit(int32_t meters)
{
switch (osdConfig()->units) {
case UNIT_IMPERIAL:
return meters * 3.28084f; // Convert to feet
default:
return meters; // Already in meters
}
}
/**
* Gets the correct altitude symbol for the current unit system
*/
char osdGetMetersToSelectedUnitSymbol(void)
{
switch (osdConfig()->units) {
case UNIT_IMPERIAL:
return SYM_FT;
default:
return SYM_M;
}
}
/**
* Converts speed based on the current unit system.
* @param value in cm/s to convert
*/
int32_t osdGetSpeedToSelectedUnit(int32_t value)
{
switch (osdConfig()->units) {
case UNIT_IMPERIAL:
case UNIT_BRITISH:
return CM_S_TO_MPH(value);
default:
return CM_S_TO_KM_H(value);
}
}
/**
* Gets the correct speed symbol for the current unit system
*/
char osdGetSpeedToSelectedUnitSymbol(void)
{
switch (osdConfig()->units) {
case UNIT_IMPERIAL:
case UNIT_BRITISH:
return SYM_MPH;
default:
return SYM_KPH;
}
}
MAYBE_UNUSED static char osdGetVarioToSelectedUnitSymbol(void)
{
switch (osdConfig()->units) {
case UNIT_IMPERIAL:
return SYM_FTPS;
default:
return SYM_MPS;
}
}
#if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
char osdGetTemperatureSymbolForSelectedUnit(void)
{
switch (osdConfig()->units) {
case UNIT_IMPERIAL:
return SYM_F;
default:
return SYM_C;
}
}
#endif
// *************************
// Element drawing functions
// *************************
#ifdef USE_RANGEFINDER
static void osdElementLidarDist(osdElementParms_t *element)
{
int16_t dist = rangefinderGetLatestAltitude();
if (dist > 0) {
tfp_sprintf(element->buff, "RF:%3d", dist);
} else {
tfp_sprintf(element->buff, "RF:---");
}
}
#endif
#ifdef USE_OSD_ADJUSTMENTS
static void osdElementAdjustmentRange(osdElementParms_t *element)
{
const char *name = getAdjustmentsRangeName();
if (name) {
tfp_sprintf(element->buff, "%s: %3d", name, getAdjustmentsRangeValue());
}
}
#endif // USE_OSD_ADJUSTMENTS
static void osdElementAltitude(osdElementParms_t *element)
{
bool haveBaro = false;
bool haveGps = false;
#ifdef USE_BARO
haveBaro = sensors(SENSOR_BARO);
#endif // USE_BARO
#ifdef USE_GPS
haveGps = sensors(SENSOR_GPS) && STATE(GPS_FIX);
#endif // USE_GPS
int32_t alt = osdGetMetersToSelectedUnit(getEstimatedAltitudeCm()) / 100;
if ((alt >= osdConfig()->alt_alarm) && ARMING_FLAG(ARMED)) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
if (haveBaro || haveGps) {
osdFormatAltitudeString(element->buff, getEstimatedAltitudeCm(), element->type);
} else {
element->buff[0] = SYM_ALTITUDE;
element->buff[1] = SYM_HYPHEN; // We use this symbol when we don't have a valid measure
element->buff[2] = '\0';
}
}
#ifdef USE_ACC
static void osdElementAngleRollPitch(osdElementParms_t *element)
{
const float angle = ((element->item == OSD_PITCH_ANGLE) ? attitude.values.pitch : attitude.values.roll) / 10.0f;
osdPrintFloat(element->buff, (element->item == OSD_PITCH_ANGLE) ? SYM_PITCH : SYM_ROLL, fabsf(angle), ((angle < 0) ? "-%02u" : " %02u"), 1, true, SYM_NONE);
}
#endif
static void osdElementAntiGravity(osdElementParms_t *element)
{
if (pidOsdAntiGravityActive()) {
strcpy(element->buff, "AG");
}
}
#ifdef USE_ACC
static void osdElementArtificialHorizon(osdElementParms_t *element)
{
static int x = -4;
// Get pitch and roll limits in tenths of degrees
const int maxPitch = osdConfig()->ahMaxPitch * 10;
const int maxRoll = osdConfig()->ahMaxRoll * 10;
const int ahSign = osdConfig()->ahInvert ? -1 : 1;
const int rollAngle = constrain(attitude.values.roll * ahSign, -maxRoll, maxRoll);
int pitchAngle = constrain(attitude.values.pitch * ahSign, -maxPitch, maxPitch);
// Convert pitchAngle to y compensation value
// (maxPitch / 25) divisor matches previous settings of fixed divisor of 8 and fixed max AHI pitch angle of 20.0 degrees
if (maxPitch > 0) {
pitchAngle = ((pitchAngle * 25) / maxPitch);
}
pitchAngle -= 41; // 41 = 4 * AH_SYMBOL_COUNT + 5
const int y = ((-rollAngle * x) / 64) - pitchAngle;
if (y >= 0 && y <= 81) {
element->elemOffsetX = x;
element->elemOffsetY = y / AH_SYMBOL_COUNT;
tfp_sprintf(element->buff, "%c", (SYM_AH_BAR9_0 + (y % AH_SYMBOL_COUNT)));
} else {
element->drawElement = false; // element does not need to be rendered
}
if (x == 4) {
// Rendering is complete, so prepare to start again
x = -4;
} else {
// Rendering not yet complete
element->rendered = false;
x++;
}
}
static void osdElementUpDownReference(osdElementParms_t *element)
{
// Up/Down reference feature displays reference points on the OSD at Zenith and Nadir
const float earthUpinBodyFrame[3] = {-rMat.m[2][0], -rMat.m[2][1], -rMat.m[2][2]}; //transforum the up vector to the body frame
if (fabsf(earthUpinBodyFrame[2]) < SINE_25_DEG && fabsf(earthUpinBodyFrame[1]) < SINE_25_DEG) {
float thetaB; // pitch from body frame to zenith/nadir
float psiB; // psi from body frame to zenith/nadir
char *symbol[2] = {"U", "D"}; // character buffer
int direction;
if (attitude.values.pitch > 0.0f){ //nose down
thetaB = -earthUpinBodyFrame[2]; // get pitch w/re to nadir (use small angle approx for sine)
psiB = -earthUpinBodyFrame[1]; // calculate the yaw w/re to nadir (use small angle approx for sine)
direction = DOWN;
} else { // nose up
thetaB = earthUpinBodyFrame[2]; // get pitch w/re to zenith (use small angle approx for sine)
psiB = earthUpinBodyFrame[1]; // calculate the yaw w/re to zenith (use small angle approx for sine)
direction = UP;
}
element->elemOffsetX = lrintf(scaleRangef(psiB, -M_PIf / 4, M_PIf / 4, -14, 14));
element->elemOffsetY = lrintf(scaleRangef(thetaB, -M_PIf / 4, M_PIf / 4, -8, 8));
tfp_sprintf(element->buff, "%c", symbol[direction]);
}
}
#endif // USE_ACC
static void osdElementAverageCellVoltage(osdElementParms_t *element)
{
const int cellV = getBatteryAverageCellVoltage();
const batteryState_e batteryState = getBatteryState();
switch (batteryState) {
case BATTERY_WARNING:
element->attr = DISPLAYPORT_SEVERITY_WARNING;
break;
case BATTERY_CRITICAL:
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
break;
default:
break;
}
osdPrintFloat(element->buff, osdGetBatterySymbol(cellV), cellV / 100.0f, "", 2, false, SYM_VOLT);
}
static void osdElementCompassBar(osdElementParms_t *element)
{
memcpy(element->buff, compassBar + osdGetHeadingIntoDiscreteDirections(DECIDEGREES_TO_DEGREES(attitude.values.yaw), 16), 9);
element->buff[9] = 0;
}
//display custom message from MSPv2
static void osdElementCustomMsg(osdElementParms_t *element)
{
int msgIndex = element->item - OSD_CUSTOM_MSG0;
if (msgIndex < 0 || msgIndex >= OSD_CUSTOM_MSG_COUNT || pilotConfig()->message[msgIndex][0] == '\0') {
tfp_sprintf(element->buff, "CUSTOM_MSG%d", msgIndex + 1);
} else {
strncpy(element->buff, pilotConfig()->message[msgIndex], MAX_NAME_LENGTH);
element->buff[MAX_NAME_LENGTH] = 0; // terminate maximum-length string
}
}
#ifdef USE_ADC_INTERNAL
static void osdElementCoreTemperature(osdElementParms_t *element)
{
tfp_sprintf(element->buff, "C%c%3d%c", SYM_TEMPERATURE, osdConvertTemperatureToSelectedUnit(getCoreTemperatureCelsius()), osdGetTemperatureSymbolForSelectedUnit());
}
#endif // USE_ADC_INTERNAL
static void osdBackgroundCameraFrame(osdElementParms_t *element)
{
static enum {TOP, MIDDLE, BOTTOM} renderPhase = TOP;
const uint8_t xpos = element->elemPosX;
const uint8_t ypos = element->elemPosY;
const uint8_t width = constrain(osdConfig()->camera_frame_width, OSD_CAMERA_FRAME_MIN_WIDTH, OSD_CAMERA_FRAME_MAX_WIDTH);
const uint8_t height = constrain(osdConfig()->camera_frame_height, OSD_CAMERA_FRAME_MIN_HEIGHT, OSD_CAMERA_FRAME_MAX_HEIGHT);
if (renderPhase != BOTTOM) {
// Rendering not yet complete
element->rendered = false;
}
if (renderPhase == MIDDLE) {
static uint8_t i = 1;
osdDisplayWriteChar(element, xpos, ypos + i, DISPLAYPORT_SEVERITY_NORMAL, SYM_STICK_OVERLAY_VERTICAL);
osdDisplayWriteChar(element, xpos + width - 1, ypos + i, DISPLAYPORT_SEVERITY_NORMAL, SYM_STICK_OVERLAY_VERTICAL);
element->drawElement = false; // element already drawn
if (++i == height) {
i = 1;
renderPhase = BOTTOM;
}
} else {
element->buff[0] = SYM_STICK_OVERLAY_CENTER;
for (uint8_t i = 1; i < (width - 1); i++) {
element->buff[i] = SYM_STICK_OVERLAY_HORIZONTAL;
}
element->buff[width - 1] = SYM_STICK_OVERLAY_CENTER;
element->buff[width] = 0; // string terminator
if (renderPhase == TOP) {
renderPhase = MIDDLE;
} else {
element->elemOffsetY = height - 1;
renderPhase = TOP;
}
}
}
static void toUpperCase(char* dest, const char* src, unsigned int maxSrcLength)
{
unsigned int i;
for (i = 0; i < maxSrcLength && src[i]; i++) {
dest[i] = toupper((unsigned char)src[i]);
}
dest[i] = '\0';
}
static void osdBackgroundCraftName(osdElementParms_t *element)
{
if (strlen(pilotConfig()->craftName) == 0) {
strcpy(element->buff, "CRAFT_NAME");
} else {
toUpperCase(element->buff, pilotConfig()->craftName, MAX_NAME_LENGTH);
}
}
#ifdef USE_ACC
static void osdElementCrashFlipArrow(osdElementParms_t *element)
{
int rollAngle = attitude.values.roll / 10;
const int pitchAngle = attitude.values.pitch / 10;
if (abs(rollAngle) > 90) {
rollAngle = (rollAngle < 0 ? -180 : 180) - rollAngle;
}
if ((isCrashFlipModeActive() || (!ARMING_FLAG(ARMED) && !isUpright())) && !((imuConfig()->small_angle < 180 && isUpright()) || (rollAngle == 0 && pitchAngle == 0))) {
element->attr = DISPLAYPORT_SEVERITY_INFO;
if (abs(pitchAngle) < 2 * abs(rollAngle) && abs(rollAngle) < 2 * abs(pitchAngle)) {
if (pitchAngle > 0) {
if (rollAngle > 0) {
element->buff[0] = SYM_ARROW_WEST + 2;
} else {
element->buff[0] = SYM_ARROW_EAST - 2;
}
} else {
if (rollAngle > 0) {
element->buff[0] = SYM_ARROW_WEST - 2;
} else {
element->buff[0] = SYM_ARROW_EAST + 2;
}
}
} else {
if (abs(pitchAngle) > abs(rollAngle)) {
if (pitchAngle > 0) {
element->buff[0] = SYM_ARROW_SOUTH;
} else {
element->buff[0] = SYM_ARROW_NORTH;
}
} else {
if (rollAngle > 0) {
element->buff[0] = SYM_ARROW_WEST;
} else {
element->buff[0] = SYM_ARROW_EAST;
}
}
}
element->buff[1] = '\0';
}
}
#endif // USE_ACC
static void osdElementCrosshairs(osdElementParms_t *element)
{
element->buff[0] = SYM_AH_CENTER_LINE;
element->buff[1] = SYM_AH_CENTER;
element->buff[2] = SYM_AH_CENTER_LINE_RIGHT;
element->buff[3] = 0;
}
static void osdElementCurrentDraw(osdElementParms_t *element)
{
const float amperage = fabsf(getAmperage() / 100.0f);
osdPrintFloat(element->buff, SYM_NONE, amperage, "%3u", 2, false, SYM_AMP);
}
static void osdElementDebug(osdElementParms_t *element)
{
tfp_sprintf(element->buff, "DBG %5d %5d %5d %5d", debug[0], debug[1], debug[2], debug[3]);
}
static void osdElementDebug2(osdElementParms_t *element)
{
tfp_sprintf(element->buff, "D2 %5d %5d %5d %5d", debug[4], debug[5], debug[6], debug[7]);
}
static void osdElementDisarmed(osdElementParms_t *element)
{
if (!ARMING_FLAG(ARMED)) {
tfp_sprintf(element->buff, "DISARMED");
}
}
static void osdBackgroundPilotName(osdElementParms_t *element)
{
if (strlen(pilotConfig()->pilotName) == 0) {
strcpy(element->buff, "PILOT_NAME");
} else {
toUpperCase(element->buff, pilotConfig()->pilotName, MAX_NAME_LENGTH);
}
}
#ifdef USE_PERSISTENT_STATS
static void osdElementTotalFlights(osdElementParms_t *element)
{
const int32_t total_flights = statsConfig()->stats_total_flights;
tfp_sprintf(element->buff, "#%d", total_flights);
}
#endif
#ifdef USE_PROFILE_NAMES
static void osdElementRateProfileName(osdElementParms_t *element)
{
if (strlen(currentControlRateProfile->profileName) == 0) {
tfp_sprintf(element->buff, "RATE_%u", getCurrentControlRateProfileIndex() + 1);
} else {
toUpperCase(element->buff, currentControlRateProfile->profileName, MAX_PROFILE_NAME_LENGTH);
}
}
static void osdElementPidProfileName(osdElementParms_t *element)
{
if (strlen(currentPidProfile->profileName) == 0) {
tfp_sprintf(element->buff, "PID_%u", getCurrentPidProfileIndex() + 1);
} else {
toUpperCase(element->buff, currentPidProfile->profileName, MAX_PROFILE_NAME_LENGTH);
}
}
#endif
#ifdef USE_OSD_PROFILES
static void osdElementOsdProfileName(osdElementParms_t *element)
{
uint8_t profileIndex = getCurrentOsdProfileIndex();
if (strlen(osdConfig()->profile[profileIndex - 1]) == 0) {
tfp_sprintf(element->buff, "OSD_%u", profileIndex);
} else {
toUpperCase(element->buff, osdConfig()->profile[profileIndex - 1], OSD_PROFILE_NAME_LENGTH);
}
}
#endif
#if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
static void osdElementEscTemperature(osdElementParms_t *element)
{
#if defined(USE_ESC_SENSOR)
if (featureIsEnabled(FEATURE_ESC_SENSOR)) {
tfp_sprintf(element->buff, "E%c%3d%c", SYM_TEMPERATURE, osdConvertTemperatureToSelectedUnit(osdEscDataCombined->temperature), osdGetTemperatureSymbolForSelectedUnit());
} else
#endif
#if defined(USE_DSHOT_TELEMETRY)
{
uint32_t osdEleIx = tfp_sprintf(element->buff, "E%c", SYM_TEMPERATURE);
for (uint8_t k = 0; k < getMotorCount(); k++) {
if ((dshotTelemetryState.motorState[k].telemetryTypes & (1 << DSHOT_TELEMETRY_TYPE_TEMPERATURE)) != 0) {
osdEleIx += tfp_sprintf(element->buff + osdEleIx, "%3d%c",
osdConvertTemperatureToSelectedUnit(dshotTelemetryState.motorState[k].telemetryData[DSHOT_TELEMETRY_TYPE_TEMPERATURE]),
osdGetTemperatureSymbolForSelectedUnit());
} else {
osdEleIx += tfp_sprintf(element->buff + osdEleIx, " 0%c", osdGetTemperatureSymbolForSelectedUnit());
}
}
}
#else
{}
#endif
}
static void osdElementEscRpm(osdElementParms_t *element)
{
renderOsdEscRpmOrFreq(&getEscRpm,element);
}
static void osdElementEscRpmFreq(osdElementParms_t *element)
{
renderOsdEscRpmOrFreq(&getEscRpmFreq,element);
}
#endif
static void osdElementFlymode(osdElementParms_t *element)
{
// Note that flight mode display has precedence in what to display.
// 1. FS
// 2. GPS RESCUE
// 3. PASSTHRU
// 4. HEAD, POSHOLD, ALTHOLD, ANGLE, HORIZON, ACRO TRAINER
// 5. AIR
// 6. ACRO
if (FLIGHT_MODE(FAILSAFE_MODE)) {
strcpy(element->buff, "!FS!");
} else if (FLIGHT_MODE(GPS_RESCUE_MODE)) {
strcpy(element->buff, "RESC");
} else if (FLIGHT_MODE(HEADFREE_MODE)) {
strcpy(element->buff, "HEAD");
} else if (FLIGHT_MODE(PASSTHRU_MODE)) {
strcpy(element->buff, "PASS");
} else if (FLIGHT_MODE(POS_HOLD_MODE)) {
strcpy(element->buff, "POSH");
} else if (FLIGHT_MODE(ALT_HOLD_MODE)) {
strcpy(element->buff, "ALTH");
} else if (FLIGHT_MODE(ANGLE_MODE)) {
strcpy(element->buff, "ANGL");
} else if (FLIGHT_MODE(HORIZON_MODE)) {
strcpy(element->buff, "HOR ");
} else if (IS_RC_MODE_ACTIVE(BOXACROTRAINER)) {
strcpy(element->buff, "ATRN");
#ifdef USE_CHIRP
// the additional check for pidChirpIsFinished() is to have visual feedback for user that don't have warnings enabled in their goggles
} else if (FLIGHT_MODE(CHIRP_MODE) && !pidChirpIsFinished()) {
strcpy(element->buff, "CHIR");
#endif
} else if (isAirmodeEnabled()) {
strcpy(element->buff, "AIR ");
} else {
strcpy(element->buff, "ACRO");
}
}
static void osdElementReadyMode(osdElementParms_t *element)
{
if (IS_RC_MODE_ACTIVE(BOXREADY) && !ARMING_FLAG(ARMED)) {
strcpy(element->buff, "READY");
}
}
#ifdef USE_ACC
static void osdElementGForce(osdElementParms_t *element)
{
osdPrintFloat(element->buff, SYM_NONE, osdGForce, "", 1, true, 'G');
}
#endif // USE_ACC
#ifdef USE_GPS
static void osdElementGpsFlightDistance(osdElementParms_t *element)
{
if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) {
osdFormatDistanceString(element->buff, GPS_distanceFlownInCm / 100, SYM_TOTAL_DISTANCE);
} else {
// We use this symbol when we don't have a FIX
tfp_sprintf(element->buff, "%c%c", SYM_TOTAL_DISTANCE, SYM_HYPHEN);
}
}
static void osdElementGpsHomeDirection(osdElementParms_t *element)
{
if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) {
if (GPS_distanceToHome > 0) {
int direction = GPS_directionToHome;
#ifdef USE_GPS_LAP_TIMER
// Override the "home" point to the start/finish location if the lap timer is running
if (gpsLapTimerData.timerRunning) {
direction = lrintf(gpsLapTimerData.dirToPoint * 0.1f); // Convert from centidegree to degree and round to nearest
}
#endif
element->buff[0] = osdGetDirectionSymbolFromHeading(DECIDEGREES_TO_DEGREES(direction - attitude.values.yaw));
} else {
element->buff[0] = SYM_OVER_HOME;
}
} else {
// We use this symbol when we don't have a FIX
element->buff[0] = SYM_HYPHEN;
}
element->buff[1] = 0;
}
static void osdElementGpsHomeDistance(osdElementParms_t *element)
{
if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) {
int distance = GPS_distanceToHome;
#ifdef USE_GPS_LAP_TIMER
// Change the "home" point to the start/finish location if the lap timer is running
if (gpsLapTimerData.timerRunning) {
distance = lrintf(gpsLapTimerData.distToPointCM * 0.01f); // Round to nearest natural number
}
#endif
osdFormatDistanceString(element->buff, distance, SYM_HOMEFLAG);
} else {
element->buff[0] = SYM_HOMEFLAG;
// We use this symbol when we don't have a FIX
element->buff[1] = SYM_HYPHEN;
element->buff[2] = '\0';
}
}
static void osdElementGpsCoordinate(osdElementParms_t *element)
{
const gpsCoordinateType_e coordinateType = (element->item == OSD_GPS_LON) ? GPS_LONGITUDE : GPS_LATITUDE;
osdFormatCoordinate(element->buff, coordinateType, element->type);
if (STATE(GPS_FIX_EVER) && !STATE(GPS_FIX)) {
SET_BLINK(element->item); // blink if we had a fix but have since lost it
} else {
CLR_BLINK(element->item);
}
}
static void osdElementGpsSats(osdElementParms_t *element)
{
if ((STATE(GPS_FIX) == 0) || (gpsSol.numSat < GPS_MIN_SAT_COUNT) ) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
#ifdef USE_GPS_RESCUE
else if ((gpsSol.numSat < gpsRescueConfig()->minSats) && gpsRescueIsConfigured()) {
element->attr = DISPLAYPORT_SEVERITY_WARNING;
}
#endif
else {
element->attr = DISPLAYPORT_SEVERITY_NORMAL;
}
if (!gpsIsHealthy()) {
tfp_sprintf(element->buff, "%c%cNC", SYM_SAT_L, SYM_SAT_R);
} else {
int pos = tfp_sprintf(element->buff, "%c%c%2d", SYM_SAT_L, SYM_SAT_R, gpsSol.numSat);
if (osdConfig()->gps_sats_show_pdop) { // add on the GPS module PDOP estimate
element->buff[pos++] = ' ';
osdPrintFloat(element->buff + pos, SYM_NONE, gpsSol.dop.pdop / 100.0f, "", 1, true, SYM_NONE);
}
}
}
static void osdElementGpsSpeed(osdElementParms_t *element)
{
if (STATE(GPS_FIX)) {
tfp_sprintf(element->buff, "%c%3d%c", SYM_SPEED, osdGetSpeedToSelectedUnit(gpsConfig()->gps_use_3d_speed ? gpsSol.speed3d : gpsSol.groundSpeed), osdGetSpeedToSelectedUnitSymbol());
} else {
tfp_sprintf(element->buff, "%c%c%c", SYM_SPEED, SYM_HYPHEN, osdGetSpeedToSelectedUnitSymbol());
}
}
static void osdElementEfficiency(osdElementParms_t *element)
{
int efficiency = 0;
if (sensors(SENSOR_GPS) && ARMING_FLAG(ARMED) && STATE(GPS_FIX) && gpsSol.groundSpeed >= EFFICIENCY_MINIMUM_SPEED_CM_S) {
const float speed = (float)osdGetSpeedToSelectedUnit(gpsSol.groundSpeed);
const float mAmperage = (float)getAmperage() * 10.f; // Current in mA
efficiency = lrintf(pt1FilterApply(&batteryEfficiencyFilt, (mAmperage / speed)));
}
const char unitSymbol = osdConfig()->units == UNIT_IMPERIAL ? SYM_MILES : SYM_KM;
if (efficiency > 0 && efficiency <= 9999) {
tfp_sprintf(element->buff, "%4d%c/%c", efficiency, SYM_MAH, unitSymbol);
} else {
tfp_sprintf(element->buff, "----%c/%c", SYM_MAH, unitSymbol);
}
}
#endif // USE_GPS
#ifdef USE_GPS_LAP_TIMER
static void osdFormatLapTime(osdElementParms_t *element, uint32_t timeMs, uint8_t symbol)
{
timeMs += 5; // round to nearest centisecond (+/- 5ms)
uint32_t seconds = timeMs / 1000;
uint32_t decimals = (timeMs % 1000) / 10;
tfp_sprintf(element->buff, "%c%3u.%02u", symbol, seconds, decimals);
}
static void osdElementGpsLapTimeCurrent(osdElementParms_t *element)
{
if (gpsLapTimerData.timerRunning) {
osdFormatLapTime(element, gpsSol.time - gpsLapTimerData.timeOfLastLap, SYM_TOTAL_DISTANCE);
} else {
osdFormatLapTime(element, 0, SYM_TOTAL_DISTANCE);
}
}
static void osdElementGpsLapTimePrevious(osdElementParms_t *element)
{
osdFormatLapTime(element, gpsLapTimerData.previousLaps[0], SYM_PREV_LAP_TIME);
}
static void osdElementGpsLapTimeBest3(osdElementParms_t *element)
{
osdFormatLapTime(element, gpsLapTimerData.best3Consec, SYM_CHECKERED_FLAG);
}
#endif // GPS_LAP_TIMER
static void osdBackgroundHorizonSidebars(osdElementParms_t *element)
{
static bool renderLevel = false;
static int8_t y = -AH_SIDEBAR_HEIGHT_POS;
// Draw AH sides
const int8_t hudwidth = AH_SIDEBAR_WIDTH_POS;
const int8_t hudheight = AH_SIDEBAR_HEIGHT_POS;
if (renderLevel) {
// AH level indicators
osdDisplayWriteChar(element, element->elemPosX - hudwidth + 1, element->elemPosY, DISPLAYPORT_SEVERITY_NORMAL, SYM_AH_LEFT);
osdDisplayWriteChar(element, element->elemPosX + hudwidth - 1, element->elemPosY, DISPLAYPORT_SEVERITY_NORMAL, SYM_AH_RIGHT);
renderLevel = false;
} else {
osdDisplayWriteChar(element, element->elemPosX - hudwidth, element->elemPosY + y, DISPLAYPORT_SEVERITY_NORMAL, SYM_AH_DECORATION);
osdDisplayWriteChar(element, element->elemPosX + hudwidth, element->elemPosY + y, DISPLAYPORT_SEVERITY_NORMAL, SYM_AH_DECORATION);
if (y == hudheight) {
// Rendering is complete, so prepare to start again
y = -hudheight;
// On next pass render the level markers
renderLevel = true;
} else {
y++;
}
// Rendering not yet complete
element->rendered = false;
}
element->drawElement = false; // element already drawn
}
#ifdef USE_RX_LINK_QUALITY_INFO
static void osdElementLinkQuality(osdElementParms_t *element)
{
uint16_t osdLinkQuality = 0;
if (rxGetLinkQualityPercent() < osdConfig()->link_quality_alarm) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
if (linkQualitySource == LQ_SOURCE_RX_PROTOCOL_CRSF) { // 0-99
osdLinkQuality = rxGetLinkQuality();
const uint8_t osdRfMode = rxGetRfMode();
tfp_sprintf(element->buff, "%c%1d:%2d", SYM_LINK_QUALITY, osdRfMode, osdLinkQuality);
} else if (linkQualitySource == LQ_SOURCE_RX_PROTOCOL_GHST) { // 0-100
osdLinkQuality = rxGetLinkQuality();
tfp_sprintf(element->buff, "%c%2d", SYM_LINK_QUALITY, osdLinkQuality);
} else { // 0-9
osdLinkQuality = rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE;
if (osdLinkQuality >= 10) {
osdLinkQuality = 9;
}
tfp_sprintf(element->buff, "%c%1d", SYM_LINK_QUALITY, osdLinkQuality);
}
}
#endif // USE_RX_LINK_QUALITY_INFO
#ifdef USE_RX_LINK_UPLINK_POWER
static void osdElementTxUplinkPower(osdElementParms_t *element)
{
const uint16_t osdUplinkTxPowerMw = rxGetUplinkTxPwrMw();
if (osdUplinkTxPowerMw < 1000) {
tfp_sprintf(element->buff, "%c%3dMW", SYM_RSSI, osdUplinkTxPowerMw);
} else {
osdPrintFloat(element->buff, SYM_RSSI, osdUplinkTxPowerMw / 1000.0f, "", 1, false, 'W');
}
}
#endif // USE_RX_LINK_UPLINK_POWER
#ifdef USE_BLACKBOX
static void osdElementLogStatus(osdElementParms_t *element)
{
if (IS_RC_MODE_ACTIVE(BOXBLACKBOX)) {
if (!isBlackboxDeviceWorking()) {
tfp_sprintf(element->buff, "%c!", SYM_BBLOG);
} else if (isBlackboxDeviceFull()) {
tfp_sprintf(element->buff, "%c>", SYM_BBLOG);
} else {
int32_t logNumber = blackboxGetLogNumber();
if (logNumber >= 0) {
tfp_sprintf(element->buff, "%c%d", SYM_BBLOG, logNumber);
} else {
tfp_sprintf(element->buff, "%c", SYM_BBLOG);
}
}
}
}
#endif // USE_BLACKBOX
static void osdElementMahDrawn(osdElementParms_t *element)
{
const int mAhDrawn = getMAhDrawn();
if (mAhDrawn >= osdConfig()->cap_alarm) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
tfp_sprintf(element->buff, "%4d%c", mAhDrawn, SYM_MAH);
}
static void osdElementWattHoursDrawn(osdElementParms_t *element)
{
const int mAhDrawn = getMAhDrawn();
const float wattHoursDrawn = getWhDrawn();
if (mAhDrawn >= osdConfig()->cap_alarm) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
if (wattHoursDrawn < 1.0f) {
tfp_sprintf(element->buff, "%3dMWH", lrintf(wattHoursDrawn * 1000));
} else {
int wattHourWholeNumber = (int)wattHoursDrawn;
int wattHourDecimalValue = (int)((wattHoursDrawn - wattHourWholeNumber) * 100);
tfp_sprintf(element->buff, wattHourDecimalValue >= 10 ? "%3d.%2dWH" : "%3d.0%1dWH", wattHourWholeNumber, wattHourDecimalValue);
}
}
static void osdElementMainBatteryUsage(osdElementParms_t *element)
{
// Set length of indicator bar
#define MAIN_BATT_USAGE_STEPS 11 // Use an odd number so the bar can be centered.
const int mAhDrawn = getMAhDrawn();
const int usedCapacity = getMAhDrawn();
int displayBasis = usedCapacity;
if (mAhDrawn >= osdConfig()->cap_alarm) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
switch (element->type) {
case OSD_ELEMENT_TYPE_3: // mAh remaining percentage (counts down as battery is used)
displayBasis = constrain(batteryConfig()->batteryCapacity - usedCapacity, 0, batteryConfig()->batteryCapacity);
FALLTHROUGH;
case OSD_ELEMENT_TYPE_4: // mAh used percentage (counts up as battery is used)
{
int displayPercent = 0;
if (batteryConfig()->batteryCapacity) {
displayPercent = constrain(lrintf(100.0f * displayBasis / batteryConfig()->batteryCapacity), 0, 100);
}
tfp_sprintf(element->buff, "%c%d%%", SYM_MAH, displayPercent);
break;
}
case OSD_ELEMENT_TYPE_2: // mAh used graphical progress bar (grows as battery is used)
displayBasis = constrain(batteryConfig()->batteryCapacity - usedCapacity, 0, batteryConfig()->batteryCapacity);
FALLTHROUGH;
case OSD_ELEMENT_TYPE_1: // mAh remaining graphical progress bar (shrinks as battery is used)
default:
{
uint8_t remainingCapacityBars = 0;
if (batteryConfig()->batteryCapacity) {
const float batteryRemaining = constrain(batteryConfig()->batteryCapacity - displayBasis, 0, batteryConfig()->batteryCapacity);
remainingCapacityBars = ceilf((batteryRemaining / (batteryConfig()->batteryCapacity / MAIN_BATT_USAGE_STEPS)));
}
// Create empty battery indicator bar
element->buff[0] = SYM_PB_START;
for (int i = 1; i <= MAIN_BATT_USAGE_STEPS; i++) {
element->buff[i] = i <= remainingCapacityBars ? SYM_PB_FULL : SYM_PB_EMPTY;
}
element->buff[MAIN_BATT_USAGE_STEPS + 1] = SYM_PB_CLOSE;
if (remainingCapacityBars > 0 && remainingCapacityBars < MAIN_BATT_USAGE_STEPS) {
element->buff[1 + remainingCapacityBars] = SYM_PB_END;
}
element->buff[MAIN_BATT_USAGE_STEPS+2] = '\0';
break;
}
}
}
static void osdElementMainBatteryVoltage(osdElementParms_t *element)
{
unsigned decimalPlaces;
const float batteryVoltage = getBatteryVoltage() / 100.0f;
batteryState_e batteryState = getBatteryState();
switch (batteryState) {
case BATTERY_WARNING:
element->attr = DISPLAYPORT_SEVERITY_WARNING;
break;
case BATTERY_CRITICAL:
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
break;
default:
break;
}
if (batteryVoltage >= 10) { // if voltage is 10v or more then display only 1 decimal place
decimalPlaces = 1;
} else {
decimalPlaces = 2;
}
osdPrintFloat(element->buff, osdGetBatterySymbol(getBatteryAverageCellVoltage()), batteryVoltage, "", decimalPlaces, true, SYM_VOLT);
}
static void osdElementMotorDiagnostics(osdElementParms_t *element)
{
int i = 0;
const bool motorsRunning = areMotorsRunning();
for (; i < getMotorCount(); i++) {
if (motorsRunning) {
element->buff[i] = 0x88 - scaleRange(motor[i], getMotorOutputLow(), getMotorOutputHigh(), 0, 8);
#if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
if (getEscRpm(i) < MOTOR_STOPPED_THRESHOLD_RPM) {
// Motor is not spinning properly. Mark as Stopped
element->buff[i] = 'S';
}
#endif
} else {
element->buff[i] = 0x88;
}
}
element->buff[i] = '\0';
}
static void osdElementNumericalHeading(osdElementParms_t *element)
{
const int heading = DECIDEGREES_TO_DEGREES(attitude.values.yaw);
tfp_sprintf(element->buff, "%c%03d", osdGetDirectionSymbolFromHeading(heading), heading);
}
#ifdef USE_VARIO
static void osdElementNumericalVario(osdElementParms_t *element)
{
bool haveBaro = false;
bool haveGps = false;
#ifdef USE_BARO
haveBaro = sensors(SENSOR_BARO);
#endif // USE_BARO
#ifdef USE_GPS
haveGps = sensors(SENSOR_GPS) && STATE(GPS_FIX);
#endif // USE_GPS
if (haveBaro || haveGps) {
const float verticalSpeed = osdGetMetersToSelectedUnit(getEstimatedVario()) / 100.0f;
const char directionSymbol = verticalSpeed < 0 ? SYM_ARROW_SMALL_DOWN : SYM_ARROW_SMALL_UP;
osdPrintFloat(element->buff, directionSymbol, fabsf(verticalSpeed), "", 1, true, osdGetVarioToSelectedUnitSymbol());
} else {
// We use this symbol when we don't have a valid measure
element->buff[0] = SYM_HYPHEN;
element->buff[1] = '\0';
}
}
#endif // USE_VARIO
static void osdElementPidRateProfile(osdElementParms_t *element)
{
tfp_sprintf(element->buff, "%d-%d", getCurrentPidProfileIndex() + 1, getCurrentControlRateProfileIndex() + 1);
}
static void osdElementPidsPitch(osdElementParms_t *element)
{
osdFormatPID(element->buff, "PIT", PID_PITCH);
}
static void osdElementPidsRoll(osdElementParms_t *element)
{
osdFormatPID(element->buff, "ROL", PID_ROLL);
}
static void osdElementPidsYaw(osdElementParms_t *element)
{
osdFormatPID(element->buff, "YAW", PID_YAW);
}
static void osdElementPower(osdElementParms_t *element)
{
tfp_sprintf(element->buff, "%4dW", getAmperage() * getBatteryVoltage() / 10000);
}
static void osdElementRcChannels(osdElementParms_t *element)
{
static uint8_t channel = 0;
if (osdConfig()->rcChannels[channel] >= 0) {
// Translate (1000, 2000) to (-1000, 1000)
int data = scaleRange(rcData[osdConfig()->rcChannels[channel]], PWM_RANGE_MIN, PWM_RANGE_MAX, -1000, 1000);
// Opt for the simplest formatting for now.
// Decimal notation can be added when tfp_sprintf supports float among fancy options.
tfp_sprintf(element->buff, "%5d", data);
element->elemOffsetY = channel;
}
if (++channel == OSD_RCCHANNELS_COUNT) {
channel = 0;
} else {
element->rendered = false;
}
}
static void osdElementRemainingTimeEstimate(osdElementParms_t *element)
{
const int mAhDrawn = getMAhDrawn();
if (mAhDrawn >= osdConfig()->cap_alarm) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
if (mAhDrawn <= 0.1f * osdConfig()->cap_alarm) { // also handles the mAhDrawn == 0 condition
tfp_sprintf(element->buff, "--:--");
} else if (mAhDrawn > osdConfig()->cap_alarm) {
tfp_sprintf(element->buff, "00:00");
} else {
const int remaining_time = (int)((osdConfig()->cap_alarm - mAhDrawn) * ((float)osdFlyTime) / mAhDrawn);
osdFormatTime(element->buff, OSD_TIMER_PREC_SECOND, remaining_time);
}
}
static void osdElementRssi(osdElementParms_t *element)
{
uint16_t osdRssi = getRssi() * 100 / 1024; // change range
if (osdRssi >= 100) {
osdRssi = 99;
}
if (getRssiPercent() < osdConfig()->rssi_alarm) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
tfp_sprintf(element->buff, "%c%2d", SYM_RSSI, osdRssi);
}
#ifdef USE_RTC_TIME
static void osdElementRtcTime(osdElementParms_t *element)
{
osdFormatRtcDateTime(&element->buff[0]);
}
#endif // USE_RTC_TIME
#ifdef USE_RX_RSSI_DBM
static void osdElementRssiDbm(osdElementParms_t *element)
{
const int8_t antenna = getActiveAntenna();
const int16_t osdRssiDbm = getRssiDbm();
static bool diversity = false;
if (osdRssiDbm < osdConfig()->rssi_dbm_alarm) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
if (antenna || diversity) {
diversity = true;
tfp_sprintf(element->buff, "%c%3d:%d", SYM_RSSI, osdRssiDbm, antenna + 1);
} else {
tfp_sprintf(element->buff, "%c%3d", SYM_RSSI, osdRssiDbm);
}
}
#endif // USE_RX_RSSI_DBM
#ifdef USE_RX_RSNR
static void osdElementRsnr(osdElementParms_t *element)
{
tfp_sprintf(element->buff, "%c%3d", SYM_RSSI, getRsnr());
}
#endif // USE_RX_RSNR
#ifdef USE_OSD_STICK_OVERLAY
static void osdBackgroundStickOverlay(osdElementParms_t *element)
{
static enum {VERT, HORZ} renderPhase = VERT;
if (renderPhase == VERT) {
static uint8_t y = 0;
tfp_sprintf(element->buff, "%c", SYM_STICK_OVERLAY_VERTICAL);
element->elemOffsetX = ((OSD_STICK_OVERLAY_WIDTH - 1) / 2);
element->elemOffsetY = y;
y++;
if (y == (OSD_STICK_OVERLAY_HEIGHT - 1) / 2) {
// Skip over horizontal
y++;
}
if (y == OSD_STICK_OVERLAY_HEIGHT) {
y = 0;
renderPhase = HORZ;
}
element->rendered = false;
} else {
for (uint8_t i = 0; i < OSD_STICK_OVERLAY_WIDTH; i++) {
element->buff[i] = SYM_STICK_OVERLAY_HORIZONTAL;
}
element->buff[((OSD_STICK_OVERLAY_WIDTH - 1) / 2)] = SYM_STICK_OVERLAY_CENTER;
element->buff[OSD_STICK_OVERLAY_WIDTH] = 0; // string terminator
element->elemOffsetY = ((OSD_STICK_OVERLAY_HEIGHT - 1) / 2);
renderPhase = VERT;
}
}
static void osdElementStickOverlay(osdElementParms_t *element)
{
// Now draw the cursor
rc_alias_e vertical_channel, horizontal_channel;
if (element->item == OSD_STICK_OVERLAY_LEFT) {
vertical_channel = radioModes[osdConfig()->overlay_radio_mode-1].left_vertical;
horizontal_channel = radioModes[osdConfig()->overlay_radio_mode-1].left_horizontal;
} else {
vertical_channel = radioModes[osdConfig()->overlay_radio_mode-1].right_vertical;
horizontal_channel = radioModes[osdConfig()->overlay_radio_mode-1].right_horizontal;
}
const uint8_t cursorX = scaleRange(constrain(rcData[horizontal_channel], PWM_RANGE_MIN, PWM_RANGE_MAX - 1), PWM_RANGE_MIN, PWM_RANGE_MAX, 0, OSD_STICK_OVERLAY_WIDTH);
const uint8_t cursorY = OSD_STICK_OVERLAY_VERTICAL_POSITIONS - 1 - scaleRange(constrain(rcData[vertical_channel], PWM_RANGE_MIN, PWM_RANGE_MAX - 1), PWM_RANGE_MIN, PWM_RANGE_MAX, 0, OSD_STICK_OVERLAY_VERTICAL_POSITIONS);
const char cursor = SYM_STICK_OVERLAY_SPRITE_HIGH + (cursorY % OSD_STICK_OVERLAY_SPRITE_HEIGHT);
tfp_sprintf(element->buff, "%c", cursor);
element->elemOffsetX = cursorX;
element->elemOffsetY = cursorY / OSD_STICK_OVERLAY_SPRITE_HEIGHT;
}
#endif // USE_OSD_STICK_OVERLAY
static void osdElementThrottlePosition(osdElementParms_t *element)
{
tfp_sprintf(element->buff, "%c%3d", SYM_THR, calculateThrottlePercent());
}
static void osdElementTimer(osdElementParms_t *element)
{
for (int i = 0; i < OSD_TIMER_COUNT; i++) {
const uint16_t timer = osdConfig()->timers[i];
const timeUs_t time = osdGetTimerValue(OSD_TIMER_SRC(timer));
const timeUs_t alarmTime = OSD_TIMER_ALARM(timer) * 60000000; // convert from minutes to us
if (alarmTime != 0 && time >= alarmTime) {
element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
}
}
osdFormatTimer(element->buff, true, true, element->item - OSD_ITEM_TIMER_1);
}
#ifdef USE_VTX_COMMON
static void osdElementVtxChannel(osdElementParms_t *element)
{
const vtxDevice_t *vtxDevice = vtxCommonDevice();
uint8_t band = vtxSettingsConfigMutable()->band;
uint8_t channel = vtxSettingsConfig()->channel;
if (band == 0) {
/* Direct frequency set is used */
vtxCommonLookupBandChan(vtxDevice, vtxSettingsConfig()->freq, &band, &channel);
}
const char vtxBandLetter = vtxCommonLookupBandLetter(vtxDevice, band);
const char *vtxChannelName = vtxCommonLookupChannelName(vtxDevice, channel);
unsigned vtxStatus = 0;
uint8_t vtxPower = vtxSettingsConfig()->power;
if (vtxDevice) {
vtxCommonGetStatus(vtxDevice, &vtxStatus);
if (vtxSettingsConfig()->lowPowerDisarm) {
vtxCommonGetPowerIndex(vtxDevice, &vtxPower);
}
}
const char *vtxPowerLabel = vtxCommonLookupPowerName(vtxDevice, vtxPower);
char vtxStatusIndicator = '\0';
if (IS_RC_MODE_ACTIVE(BOXVTXCONTROLDISABLE)) {
vtxStatusIndicator = 'D';
} else if (vtxStatus & VTX_STATUS_PIT_MODE) {
vtxStatusIndicator = 'P';
}
switch (element->type) {
case OSD_ELEMENT_TYPE_2:
tfp_sprintf(element->buff, "%s", vtxPowerLabel);
break;
default:
if (vtxStatus & VTX_STATUS_LOCKED) {
tfp_sprintf(element->buff, "-:-:-:L");
} else if (vtxStatusIndicator) {
tfp_sprintf(element->buff, "%c:%s:%s:%c", vtxBandLetter, vtxChannelName, vtxPowerLabel, vtxStatusIndicator);
} else {
tfp_sprintf(element->buff, "%c:%s:%s", vtxBandLetter, vtxChannelName, vtxPowerLabel);
}
break;
}
}
#endif // USE_VTX_COMMON
static void osdElementAuxValue(osdElementParms_t *element)
{
tfp_sprintf(element->buff, "%c%d", osdConfig()->aux_symbol, osdAuxValue);
}
static void osdElementWarnings(osdElementParms_t *element)
{
bool elementBlinking = false;
renderOsdWarning(element->buff, &elementBlinking, &element->attr);
if (elementBlinking) {
SET_BLINK(OSD_WARNINGS);
} else {
CLR_BLINK(OSD_WARNINGS);
}
#ifdef USE_CRAFTNAME_MSGS
// Injects data into the CraftName variable for systems which limit
// the available MSP data field in their OSD.
if (osdConfig()->osd_craftname_msgs == true) {
// if warning is not set, or blink is off, then display LQ & RSSI
if (blinkState || (strlen(element->buff) == 0)) {
#ifdef USE_RX_LINK_QUALITY_INFO
// replicate the LQ functionality without the special font symbols
uint16_t osdLinkQuality = 0;
if (linkQualitySource == LQ_SOURCE_RX_PROTOCOL_CRSF) { // 0-99
osdLinkQuality = rxGetLinkQuality();
#ifdef USE_RX_RSSI_DBM
const uint8_t osdRfMode = rxGetRfMode();
tfp_sprintf(element->buff, "LQ %2d:%03d %3d", osdRfMode, osdLinkQuality, getRssiDbm());
} else if (linkQualitySource == LQ_SOURCE_RX_PROTOCOL_GHST) { // 0-100
osdLinkQuality = rxGetLinkQuality();
tfp_sprintf(element->buff, "LQ %03d %3d", osdLinkQuality, getRssiDbm());
#endif
} else { // 0-9
osdLinkQuality = rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE;
if (osdLinkQuality >= 10) {
osdLinkQuality = 9;
}
tfp_sprintf(element->buff, "LQ %1d", osdLinkQuality);
}
#endif // USE_RX_LINK_QUALITY_INFO
}
strncpy(pilotConfigMutable()->craftName, element->buff, MAX_NAME_LENGTH - 1);
}
#endif // USE_CRAFTNAME_MSGS
}
#ifdef USE_MSP_DISPLAYPORT
static void osdElementSys(osdElementParms_t *element)
{
UNUSED(element);
// Nothing to render for a system element
}
#endif
// Define the order in which the elements are drawn.
// Elements positioned later in the list will overlay the earlier
// ones if their character positions overlap
// Elements that need special runtime conditional processing should be added
// to osdAddActiveElements()
static const uint8_t osdElementDisplayOrder[] = {
OSD_MAIN_BATT_VOLTAGE,
OSD_RSSI_VALUE,
OSD_CROSSHAIRS,
OSD_HORIZON_SIDEBARS,
OSD_UP_DOWN_REFERENCE,
OSD_ITEM_TIMER_1,
OSD_ITEM_TIMER_2,
OSD_REMAINING_TIME_ESTIMATE,
OSD_FLYMODE,
OSD_THROTTLE_POS,
OSD_VTX_CHANNEL,
OSD_CURRENT_DRAW,
OSD_MAH_DRAWN,
OSD_WATT_HOURS_DRAWN,
OSD_CRAFT_NAME,
OSD_CUSTOM_MSG0,
OSD_CUSTOM_MSG1,
OSD_CUSTOM_MSG2,
OSD_CUSTOM_MSG3,
OSD_ALTITUDE,
OSD_ROLL_PIDS,
OSD_PITCH_PIDS,
OSD_YAW_PIDS,
OSD_POWER,
OSD_PIDRATE_PROFILE,
OSD_WARNINGS,
OSD_AVG_CELL_VOLTAGE,
OSD_DEBUG,
OSD_DEBUG2,
OSD_PITCH_ANGLE,
OSD_ROLL_ANGLE,
OSD_MAIN_BATT_USAGE,
OSD_DISARMED,
OSD_NUMERICAL_HEADING,
OSD_READY_MODE,
#ifdef USE_VARIO
OSD_NUMERICAL_VARIO,
#endif
OSD_COMPASS_BAR,
OSD_ANTI_GRAVITY,
#ifdef USE_BLACKBOX
OSD_LOG_STATUS,
#endif
OSD_MOTOR_DIAG,
#ifdef USE_ACC
OSD_FLIP_ARROW,
#endif
OSD_PILOT_NAME,
#ifdef USE_RTC_TIME
OSD_RTC_DATETIME,
#endif
#ifdef USE_OSD_ADJUSTMENTS
OSD_ADJUSTMENT_RANGE,
#endif
#ifdef USE_ADC_INTERNAL
OSD_CORE_TEMPERATURE,
#endif
#ifdef USE_RX_LINK_QUALITY_INFO
OSD_LINK_QUALITY,
#endif
#ifdef USE_RX_LINK_UPLINK_POWER
OSD_TX_UPLINK_POWER,
#endif
#ifdef USE_RX_RSSI_DBM
OSD_RSSI_DBM_VALUE,
#endif
#ifdef USE_RX_RSNR
OSD_RSNR_VALUE,
#endif
#ifdef USE_OSD_STICK_OVERLAY
OSD_STICK_OVERLAY_LEFT,
OSD_STICK_OVERLAY_RIGHT,
#endif
#ifdef USE_PROFILE_NAMES
OSD_RATE_PROFILE_NAME,
OSD_PID_PROFILE_NAME,
#endif
#ifdef USE_OSD_PROFILES
OSD_PROFILE_NAME,
#endif
OSD_RC_CHANNELS,
OSD_CAMERA_FRAME,
#ifdef USE_PERSISTENT_STATS
OSD_TOTAL_FLIGHTS,
#endif
OSD_AUX_VALUE,
#ifdef USE_OSD_HD
OSD_SYS_GOGGLE_VOLTAGE,
OSD_SYS_VTX_VOLTAGE,
OSD_SYS_BITRATE,
OSD_SYS_DELAY,
OSD_SYS_DISTANCE,
OSD_SYS_LQ,
OSD_SYS_GOGGLE_DVR,
OSD_SYS_VTX_DVR,
OSD_SYS_WARNINGS,
OSD_SYS_VTX_TEMP,
OSD_SYS_FAN_SPEED,
#endif
#ifdef USE_RANGEFINDER
OSD_LIDAR_DIST,
#endif
};
// Define the mapping between the OSD element id and the function to draw it
const osdElementDrawFn osdElementDrawFunction[OSD_ITEM_COUNT] = {
[OSD_CAMERA_FRAME] = NULL, // only has background. Added first so it's the lowest "layer" and doesn't cover other elements
[OSD_RSSI_VALUE] = osdElementRssi,
[OSD_MAIN_BATT_VOLTAGE] = osdElementMainBatteryVoltage,
[OSD_CROSSHAIRS] = osdElementCrosshairs, // only has background, but needs to be over other elements (like artificial horizon)
#ifdef USE_ACC
[OSD_ARTIFICIAL_HORIZON] = osdElementArtificialHorizon,
[OSD_UP_DOWN_REFERENCE] = osdElementUpDownReference,
#endif
[OSD_HORIZON_SIDEBARS] = NULL, // only has background
[OSD_ITEM_TIMER_1] = osdElementTimer,
[OSD_ITEM_TIMER_2] = osdElementTimer,
[OSD_FLYMODE] = osdElementFlymode,
[OSD_CRAFT_NAME] = NULL, // only has background
[OSD_CUSTOM_MSG0] = osdElementCustomMsg,
[OSD_CUSTOM_MSG1] = osdElementCustomMsg,
[OSD_CUSTOM_MSG2] = osdElementCustomMsg,
[OSD_CUSTOM_MSG3] = osdElementCustomMsg,
[OSD_THROTTLE_POS] = osdElementThrottlePosition,
#ifdef USE_VTX_COMMON
[OSD_VTX_CHANNEL] = osdElementVtxChannel,
#endif
[OSD_CURRENT_DRAW] = osdElementCurrentDraw,
[OSD_MAH_DRAWN] = osdElementMahDrawn,
[OSD_WATT_HOURS_DRAWN] = osdElementWattHoursDrawn,
#ifdef USE_GPS
[OSD_GPS_SPEED] = osdElementGpsSpeed,
[OSD_GPS_SATS] = osdElementGpsSats,
#endif
[OSD_ALTITUDE] = osdElementAltitude,
[OSD_ROLL_PIDS] = osdElementPidsRoll,
[OSD_PITCH_PIDS] = osdElementPidsPitch,
[OSD_YAW_PIDS] = osdElementPidsYaw,
[OSD_POWER] = osdElementPower,
[OSD_PIDRATE_PROFILE] = osdElementPidRateProfile,
[OSD_WARNINGS] = osdElementWarnings,
[OSD_AVG_CELL_VOLTAGE] = osdElementAverageCellVoltage,
[OSD_READY_MODE] = osdElementReadyMode,
#ifdef USE_GPS
[OSD_GPS_LON] = osdElementGpsCoordinate,
[OSD_GPS_LAT] = osdElementGpsCoordinate,
#endif
[OSD_DEBUG] = osdElementDebug,
[OSD_DEBUG2] = osdElementDebug2,
#ifdef USE_ACC
[OSD_PITCH_ANGLE] = osdElementAngleRollPitch,
[OSD_ROLL_ANGLE] = osdElementAngleRollPitch,
#endif
[OSD_MAIN_BATT_USAGE] = osdElementMainBatteryUsage,
[OSD_DISARMED] = osdElementDisarmed,
#ifdef USE_GPS
[OSD_HOME_DIR] = osdElementGpsHomeDirection,
[OSD_HOME_DIST] = osdElementGpsHomeDistance,
#endif
[OSD_NUMERICAL_HEADING] = osdElementNumericalHeading,
#ifdef USE_VARIO
[OSD_NUMERICAL_VARIO] = osdElementNumericalVario,
#endif
[OSD_COMPASS_BAR] = osdElementCompassBar,
#if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
[OSD_ESC_TMP] = osdElementEscTemperature,
[OSD_ESC_RPM] = osdElementEscRpm,
#endif
[OSD_REMAINING_TIME_ESTIMATE] = osdElementRemainingTimeEstimate,
#ifdef USE_RTC_TIME
[OSD_RTC_DATETIME] = osdElementRtcTime,
#endif
#ifdef USE_OSD_ADJUSTMENTS
[OSD_ADJUSTMENT_RANGE] = osdElementAdjustmentRange,
#endif
#ifdef USE_ADC_INTERNAL
[OSD_CORE_TEMPERATURE] = osdElementCoreTemperature,
#endif
[OSD_ANTI_GRAVITY] = osdElementAntiGravity,
#ifdef USE_ACC
[OSD_G_FORCE] = osdElementGForce,
#endif
[OSD_MOTOR_DIAG] = osdElementMotorDiagnostics,
#ifdef USE_BLACKBOX
[OSD_LOG_STATUS] = osdElementLogStatus,
#endif
#ifdef USE_ACC
[OSD_FLIP_ARROW] = osdElementCrashFlipArrow,
#endif
#ifdef USE_RX_LINK_QUALITY_INFO
[OSD_LINK_QUALITY] = osdElementLinkQuality,
#endif
#ifdef USE_RX_LINK_UPLINK_POWER
[OSD_TX_UPLINK_POWER] = osdElementTxUplinkPower,
#endif
#ifdef USE_GPS
[OSD_FLIGHT_DIST] = osdElementGpsFlightDistance,
#endif
#ifdef USE_OSD_STICK_OVERLAY
[OSD_STICK_OVERLAY_LEFT] = osdElementStickOverlay,
[OSD_STICK_OVERLAY_RIGHT] = osdElementStickOverlay,
#endif
[OSD_PILOT_NAME] = NULL, // only has background
#if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
[OSD_ESC_RPM_FREQ] = osdElementEscRpmFreq,
#endif
#ifdef USE_PROFILE_NAMES
[OSD_RATE_PROFILE_NAME] = osdElementRateProfileName,
[OSD_PID_PROFILE_NAME] = osdElementPidProfileName,
#endif
#ifdef USE_OSD_PROFILES
[OSD_PROFILE_NAME] = osdElementOsdProfileName,
#endif
#ifdef USE_RX_RSSI_DBM
[OSD_RSSI_DBM_VALUE] = osdElementRssiDbm,
#endif
#ifdef USE_RX_RSNR
[OSD_RSNR_VALUE] = osdElementRsnr,
#endif
[OSD_RC_CHANNELS] = osdElementRcChannels,
#ifdef USE_GPS
[OSD_EFFICIENCY] = osdElementEfficiency,
#endif
#ifdef USE_GPS_LAP_TIMER
[OSD_GPS_LAP_TIME_CURRENT] = osdElementGpsLapTimeCurrent,
[OSD_GPS_LAP_TIME_PREVIOUS] = osdElementGpsLapTimePrevious,
[OSD_GPS_LAP_TIME_BEST3] = osdElementGpsLapTimeBest3,
#endif // GPS_LAP_TIMER
#ifdef USE_PERSISTENT_STATS
[OSD_TOTAL_FLIGHTS] = osdElementTotalFlights,
#endif
[OSD_AUX_VALUE] = osdElementAuxValue,
#ifdef USE_MSP_DISPLAYPORT
[OSD_SYS_GOGGLE_VOLTAGE] = osdElementSys,
[OSD_SYS_VTX_VOLTAGE] = osdElementSys,
[OSD_SYS_BITRATE] = osdElementSys,
[OSD_SYS_DELAY] = osdElementSys,
[OSD_SYS_DISTANCE] = osdElementSys,
[OSD_SYS_LQ] = osdElementSys,
[OSD_SYS_GOGGLE_DVR] = osdElementSys,
[OSD_SYS_VTX_DVR] = osdElementSys,
[OSD_SYS_WARNINGS] = osdElementSys,
[OSD_SYS_VTX_TEMP] = osdElementSys,
[OSD_SYS_FAN_SPEED] = osdElementSys,
#endif
#ifdef USE_RANGEFINDER
[OSD_LIDAR_DIST] = osdElementLidarDist,
#endif
};
// Define the mapping between the OSD element id and the function to draw its background (static part)
// Only necessary to define the entries that actually have a background function
const osdElementDrawFn osdElementBackgroundFunction[OSD_ITEM_COUNT] = {
[OSD_CAMERA_FRAME] = osdBackgroundCameraFrame,
[OSD_HORIZON_SIDEBARS] = osdBackgroundHorizonSidebars,
[OSD_CRAFT_NAME] = osdBackgroundCraftName,
#ifdef USE_OSD_STICK_OVERLAY
[OSD_STICK_OVERLAY_LEFT] = osdBackgroundStickOverlay,
[OSD_STICK_OVERLAY_RIGHT] = osdBackgroundStickOverlay,
#endif
[OSD_PILOT_NAME] = osdBackgroundPilotName,
};
static void osdAddActiveElement(osd_items_e element)
{
if (VISIBLE(osdElementConfig()->item_pos[element])) {
activeOsdElementArray[activeOsdElementCount++] = element;
}
}
// Examine the elements and build a list of only the active (enabled)
// ones to speed up rendering.
void osdAddActiveElements(void)
{
activeOsdElementCount = 0;
#ifdef USE_ACC
if (sensors(SENSOR_ACC)) {
osdAddActiveElement(OSD_ARTIFICIAL_HORIZON);
osdAddActiveElement(OSD_G_FORCE);
osdAddActiveElement(OSD_UP_DOWN_REFERENCE);
}
#endif
for (unsigned i = 0; i < sizeof(osdElementDisplayOrder); i++) {
osdAddActiveElement(osdElementDisplayOrder[i]);
}
#ifdef USE_GPS
if (sensors(SENSOR_GPS)) {
osdAddActiveElement(OSD_GPS_SATS);
osdAddActiveElement(OSD_GPS_SPEED);
osdAddActiveElement(OSD_GPS_LAT);
osdAddActiveElement(OSD_GPS_LON);
osdAddActiveElement(OSD_HOME_DIST);
osdAddActiveElement(OSD_HOME_DIR);
osdAddActiveElement(OSD_FLIGHT_DIST);
osdAddActiveElement(OSD_EFFICIENCY);
}
#endif // GPS
#if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
if ((featureIsEnabled(FEATURE_ESC_SENSOR)) || useDshotTelemetry) {
osdAddActiveElement(OSD_ESC_TMP);
osdAddActiveElement(OSD_ESC_RPM);
osdAddActiveElement(OSD_ESC_RPM_FREQ);
}
#endif
#ifdef USE_GPS_LAP_TIMER
if (sensors(SENSOR_GPS)) {
osdAddActiveElement(OSD_GPS_LAP_TIME_CURRENT);
osdAddActiveElement(OSD_GPS_LAP_TIME_PREVIOUS);
osdAddActiveElement(OSD_GPS_LAP_TIME_BEST3);
}
#endif // GPS_LAP_TIMER
#ifdef USE_PERSISTENT_STATS
osdAddActiveElement(OSD_TOTAL_FLIGHTS);
#endif
}
static bool osdDrawSingleElement(displayPort_t *osdDisplayPort, uint8_t item)
{
// By default mark the element as rendered in case it's in the off blink state
activeElement.rendered = true;
if (!osdElementDrawFunction[item]) {
// Element has no drawing function
return true;
}
if (!osdDisplayPort->useDeviceBlink && BLINK(item)) {
return true;
}
uint8_t elemPosX = OSD_X(osdElementConfig()->item_pos[item]);
uint8_t elemPosY = OSD_Y(osdElementConfig()->item_pos[item]);
activeElement.item = item;
activeElement.elemPosX = elemPosX;
activeElement.elemPosY = elemPosY;
activeElement.elemOffsetX = 0;
activeElement.elemOffsetY = 0;
activeElement.type = OSD_TYPE(osdElementConfig()->item_pos[item]);
activeElement.buff = elementBuff;
activeElement.osdDisplayPort = osdDisplayPort;
activeElement.drawElement = true;
activeElement.attr = DISPLAYPORT_SEVERITY_NORMAL;
// Call the element drawing function
if (IS_SYS_OSD_ELEMENT(item)) {
displaySys(osdDisplayPort, elemPosX, elemPosY, (displayPortSystemElement_e)(item - OSD_SYS_GOGGLE_VOLTAGE + DISPLAYPORT_SYS_GOGGLE_VOLTAGE));
} else {
osdElementDrawFunction[item](&activeElement);
if (activeElement.drawElement) {
displayPendingForeground = true;
}
}
return activeElement.rendered;
}
static bool osdDrawSingleElementBackground(displayPort_t *osdDisplayPort, uint8_t item)
{
if (!osdElementBackgroundFunction[item]) {
// Element has no background drawing function
return true;
}
uint8_t elemPosX = OSD_X(osdElementConfig()->item_pos[item]);
uint8_t elemPosY = OSD_Y(osdElementConfig()->item_pos[item]);
activeElement.item = item;
activeElement.elemPosX = elemPosX;
activeElement.elemPosY = elemPosY;
activeElement.elemOffsetX = 0;
activeElement.elemOffsetY = 0;
activeElement.type = OSD_TYPE(osdElementConfig()->item_pos[item]);
activeElement.buff = elementBuff;
activeElement.osdDisplayPort = osdDisplayPort;
activeElement.drawElement = true;
activeElement.rendered = true;
activeElement.attr = DISPLAYPORT_SEVERITY_NORMAL;
// Call the element background drawing function
osdElementBackgroundFunction[item](&activeElement);
if (activeElement.drawElement) {
displayPendingBackground = true;
}
return activeElement.rendered;
}
static uint8_t activeElementNumber = 0;
bool osdIsRenderPending(void)
{
return displayPendingForeground | displayPendingBackground;
}
uint8_t osdGetActiveElement(void)
{
return activeElementNumber;
}
uint8_t osdGetActiveElementCount(void)
{
return activeOsdElementCount;
}
// Return true if there is more to display
bool osdDisplayActiveElement(void)
{
if (activeElementNumber >= activeOsdElementCount) {
return false;
}
// If there's a previously drawn background string to be displayed, do that
if (displayPendingBackground) {
osdDisplayWrite(&activeElement,
activeElement.elemPosX + activeElement.elemOffsetX,
activeElement.elemPosY + activeElement.elemOffsetY,
activeElement.attr, activeElement.buff);
activeElement.buff[0] = '\0';
displayPendingBackground = false;
return displayPendingForeground;
}
// If there's a previously drawn foreground string to be displayed, do that
if (displayPendingForeground) {
osdDisplayWrite(&activeElement,
activeElement.elemPosX + activeElement.elemOffsetX,
activeElement.elemPosY + activeElement.elemOffsetY,
activeElement.attr, activeElement.buff);
activeElement.buff[0] = '\0';
displayPendingForeground = false;
}
return false;
}
// Return true if there are more elements to draw
bool osdDrawNextActiveElement(displayPort_t *osdDisplayPort)
{
static bool backgroundRendered = false;
if (activeElementNumber >= activeOsdElementCount) {
activeElementNumber = 0;
return false;
}
uint8_t item = activeOsdElementArray[activeElementNumber];
if (!backgroundLayerSupported && osdElementBackgroundFunction[item] && !backgroundRendered) {
// If the background layer isn't supported then we
// have to draw the element's static layer as well.
backgroundRendered = osdDrawSingleElementBackground(osdDisplayPort, item);
// After the background always come back to check for foreground
return true;
}
// Only advance to the next element if rendering is complete
if (osdDrawSingleElement(osdDisplayPort, item)) {
// If rendering is complete then advance to the next element
// Prepare to render the background of the next element
backgroundRendered = false;
if (++activeElementNumber >= activeOsdElementCount) {
activeElementNumber = 0;
return false;
}
}
return true;
}
#ifdef USE_SPEC_PREARM_SCREEN
bool osdDrawSpec(displayPort_t *osdDisplayPort)
{
static enum {RPM, POLES, MIXER, THR, MOTOR, BAT, VER} specState = RPM;
static int currentRow;
const uint8_t midRow = osdDisplayPort->rows / 2;
const uint8_t midCol = osdDisplayPort->cols / 2;
char buff[OSD_ELEMENT_BUFFER_LENGTH] = "";
int len = 0;
switch (specState) {
default:
case RPM:
currentRow = midRow - 3;
#ifdef USE_RPM_LIMIT
{
const bool rpmLimitActive = mixerConfig()->rpm_limit > 0 && isMotorProtocolBidirDshot();
if (rpmLimitActive) {
len = tfp_sprintf(buff, "RPM LIMIT ON %d", mixerConfig()->rpm_limit_value);
} else {
len = tfp_sprintf(buff, "%s", "RPM LIMIT OFF");
}
displayWrite(osdDisplayPort, midCol - (len / 2), currentRow++, DISPLAYPORT_SEVERITY_NORMAL, buff);
if (rpmLimitActive) {
specState = POLES;
} else {
specState = THR;
}
}
break;
case POLES:
len = tfp_sprintf(buff, "KV %d POLES %d", motorConfig()->kv, motorConfig()->motorPoleCount);
displayWrite(osdDisplayPort, midCol - (len / 2), currentRow++, DISPLAYPORT_SEVERITY_NORMAL, buff);
specState = MIXER;
break;
case MIXER:
len = tfp_sprintf(buff, "%d %d %d", mixerConfig()->rpm_limit_p, mixerConfig()->rpm_limit_i, mixerConfig()->rpm_limit_d);
displayWrite(osdDisplayPort, midCol - (len / 2), currentRow++, DISPLAYPORT_SEVERITY_NORMAL, buff);
specState = THR;
break;
case THR:
#endif // #USE_RPM_LIMIT
len = tfp_sprintf(buff, "THR LIMIT %s", lookupTableThrottleLimitType[currentControlRateProfile->throttle_limit_type]);
if (currentControlRateProfile->throttle_limit_type != THROTTLE_LIMIT_TYPE_OFF) {
len = tfp_sprintf(buff, "%s %d", buff, currentControlRateProfile->throttle_limit_percent);
}
displayWrite(osdDisplayPort, midCol - (len / 2), currentRow++, DISPLAYPORT_SEVERITY_NORMAL, buff);
specState = MOTOR;
break;
case MOTOR:
len = tfp_sprintf(buff, "MOTOR LIMIT %d", currentPidProfile->motor_output_limit);
displayWrite(osdDisplayPort, midCol - (len / 2), currentRow++, DISPLAYPORT_SEVERITY_NORMAL, buff);
specState = BAT;
break;
case BAT:
{
const float batteryVoltage = getBatteryVoltage() / 100.0f;
len = osdPrintFloat(buff, osdGetBatterySymbol(getBatteryAverageCellVoltage()), batteryVoltage, "", 2, true, SYM_VOLT);
displayWrite(osdDisplayPort, midCol - (len / 2), currentRow++, DISPLAYPORT_SEVERITY_NORMAL, buff);
}
specState = VER;
break;
case VER:
len = strlen(FC_VERSION_STRING);
displayWrite(osdDisplayPort, midCol - (len / 2), currentRow++, DISPLAYPORT_SEVERITY_NORMAL, FC_VERSION_STRING);
specState = RPM;
return true;
}
return false;
}
#endif // USE_SPEC_PREARM_SCREEN
void osdDrawActiveElementsBackground(displayPort_t *osdDisplayPort)
{
if (backgroundLayerSupported) {
displayLayerSelect(osdDisplayPort, DISPLAYPORT_LAYER_BACKGROUND);
displayClearScreen(osdDisplayPort, DISPLAY_CLEAR_WAIT);
for (unsigned i = 0; i < activeOsdElementCount; i++) {
while (!osdDrawSingleElementBackground(osdDisplayPort, activeOsdElementArray[i]));
}
displayLayerSelect(osdDisplayPort, DISPLAYPORT_LAYER_FOREGROUND);
}
}
void osdElementsInit(bool backgroundLayerFlag)
{
backgroundLayerSupported = backgroundLayerFlag;
activeOsdElementCount = 0;
pt1FilterInit(&batteryEfficiencyFilt, pt1FilterGain(EFFICIENCY_CUTOFF_HZ, 1.0f / osdConfig()->framerate_hz));
}
void osdSyncBlink(timeUs_t currentTimeUs)
{
const int period = 1000000/OSD_BLINK_FREQUENCY_HZ;
blinkState = ((currentTimeUs % period) < (period >> 1));
}
void osdResetAlarms(void)
{
memset(blinkBits, 0, sizeof(blinkBits));
}
void osdUpdateAlarms(void)
{
// This is overdone?
int32_t alt = osdGetMetersToSelectedUnit(getEstimatedAltitudeCm()) / 100;
if (getRssiPercent() < osdConfig()->rssi_alarm) {
SET_BLINK(OSD_RSSI_VALUE);
} else {
CLR_BLINK(OSD_RSSI_VALUE);
}
#ifdef USE_RX_RSSI_DBM
if (getRssiDbm() < osdConfig()->rssi_dbm_alarm) {
SET_BLINK(OSD_RSSI_DBM_VALUE);
} else {
CLR_BLINK(OSD_RSSI_DBM_VALUE);
}
#endif
#ifdef USE_RX_LINK_QUALITY_INFO
if (rxGetLinkQualityPercent() < osdConfig()->link_quality_alarm) {
SET_BLINK(OSD_LINK_QUALITY);
} else {
CLR_BLINK(OSD_LINK_QUALITY);
}
#endif // USE_RX_LINK_QUALITY_INFO
if (getBatteryState() == BATTERY_OK) {
CLR_BLINK(OSD_MAIN_BATT_VOLTAGE);
CLR_BLINK(OSD_AVG_CELL_VOLTAGE);
} else {
SET_BLINK(OSD_MAIN_BATT_VOLTAGE);
SET_BLINK(OSD_AVG_CELL_VOLTAGE);
}
#ifdef USE_GPS
if ((STATE(GPS_FIX) == 0) || (gpsSol.numSat < GPS_MIN_SAT_COUNT)
#ifdef USE_GPS_RESCUE
|| ((gpsSol.numSat < gpsRescueConfig()->minSats) && gpsRescueIsConfigured())
#endif
) {
SET_BLINK(OSD_GPS_SATS);
} else {
CLR_BLINK(OSD_GPS_SATS);
}
#endif //USE_GPS
for (int i = 0; i < OSD_TIMER_COUNT; i++) {
const uint16_t timer = osdConfig()->timers[i];
const timeUs_t time = osdGetTimerValue(OSD_TIMER_SRC(timer));
const timeUs_t alarmTime = OSD_TIMER_ALARM(timer) * 60000000; // convert from minutes to us
if (alarmTime != 0 && time >= alarmTime) {
SET_BLINK(OSD_ITEM_TIMER_1 + i);
} else {
CLR_BLINK(OSD_ITEM_TIMER_1 + i);
}
}
if (getMAhDrawn() >= osdConfig()->cap_alarm) {
SET_BLINK(OSD_MAH_DRAWN);
SET_BLINK(OSD_MAIN_BATT_USAGE);
SET_BLINK(OSD_REMAINING_TIME_ESTIMATE);
} else {
CLR_BLINK(OSD_MAH_DRAWN);
CLR_BLINK(OSD_MAIN_BATT_USAGE);
CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE);
}
if ((alt >= osdConfig()->alt_alarm) && ARMING_FLAG(ARMED)) {
SET_BLINK(OSD_ALTITUDE);
} else {
CLR_BLINK(OSD_ALTITUDE);
}
#ifdef USE_GPS
if (sensors(SENSOR_GPS) && ARMING_FLAG(ARMED) && STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) {
if (osdConfig()->distance_alarm && GPS_distanceToHome >= osdConfig()->distance_alarm) {
SET_BLINK(OSD_HOME_DIST);
} else {
CLR_BLINK(OSD_HOME_DIST);
}
} else {
CLR_BLINK(OSD_HOME_DIST);
}
#endif
#if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
bool blink = false;
#if defined(USE_ESC_SENSOR)
if (featureIsEnabled(FEATURE_ESC_SENSOR)) {
// This works because the combined ESC data contains the maximum temperature seen amongst all ESCs
blink = osdConfig()->esc_temp_alarm != ESC_TEMP_ALARM_OFF && osdEscDataCombined->temperature >= osdConfig()->esc_temp_alarm;
} else
#endif
#if defined(USE_DSHOT_TELEMETRY)
{
if (osdConfig()->esc_temp_alarm != ESC_TEMP_ALARM_OFF) {
for (uint32_t k = 0; !blink && (k < getMotorCount()); k++) {
blink = (dshotTelemetryState.motorState[k].telemetryTypes & (1 << DSHOT_TELEMETRY_TYPE_TEMPERATURE)) != 0 &&
dshotTelemetryState.motorState[k].telemetryData[DSHOT_TELEMETRY_TYPE_TEMPERATURE] >= osdConfig()->esc_temp_alarm;
}
}
}
#else
{}
#endif
if (blink) {
SET_BLINK(OSD_ESC_TMP);
} else {
CLR_BLINK(OSD_ESC_TMP);
}
#endif
}
#ifdef USE_ACC
static bool osdElementIsActive(osd_items_e element)
{
for (unsigned i = 0; i < activeOsdElementCount; i++) {
if (activeOsdElementArray[i] == element) {
return true;
}
}
return false;
}
// Determine if any active elements need the ACC
bool osdElementsNeedAccelerometer(void)
{
return osdElementIsActive(OSD_ARTIFICIAL_HORIZON) ||
osdElementIsActive(OSD_PITCH_ANGLE) ||
osdElementIsActive(OSD_ROLL_ANGLE) ||
osdElementIsActive(OSD_G_FORCE) ||
osdElementIsActive(OSD_FLIP_ARROW) ||
osdElementIsActive(OSD_UP_DOWN_REFERENCE);
}
#endif // USE_ACC
#endif // USE_OSD