2 * This file is part of Cleanflight and Betaflight.
4 * Cleanflight and Betaflight are free software. You can redistribute
5 * this software and/or modify this software under the terms of the
6 * GNU General Public License as published by the Free Software
7 * Foundation, either version 3 of the License, or (at your option)
10 * Cleanflight and Betaflight are distributed in the hope that they
11 * will be useful, but WITHOUT ANY WARRANTY; without even the implied
12 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 * See the GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this software.
18 * If not, see <http://www.gnu.org/licenses/>.
22 *****************************************
23 Instructions for adding new OSD Elements:
24 *****************************************
26 First add the new element to the osd_items_e enumeration in osd/osd.h. The
27 element must be added to the end just before OSD_ITEM_COUNT.
29 Next add the element to the osdElementDisplayOrder array defined in this file.
30 If the element needs special runtime conditional processing then it should be added
31 to the osdAddActiveElements() function instead.
33 Create the function to "draw" the element.
34 ------------------------------------------
35 It should be named like "osdElementSomething()" where the "Something" describes
36 the element. The drawing function should only render the dynamic portions of the
37 element. If the element has static (unchanging) portions then those should be
38 rendered in the background function. The exception to this is elements that are
39 expected to blink (have a warning associated). In this case the entire element
40 must be handled in the main draw function and you can't use the background capability.
42 Add the mapping from the element ID added in the first step to the function
43 created in the third step to the osdElementDrawFunction array.
45 Create the function to draw the element's static (background) portion.
46 ---------------------------------------------------------------------
47 If an element has static (unchanging) portions then create a function to draw only those
48 parts. It should be named like "osdBackgroundSomething()" where the "Something" matches
49 the related element function.
51 Add the mapping for the element ID to the background drawing function to the
52 osdElementBackgroundFunction array.
54 Accelerometer reqirement:
55 -------------------------
56 If the new element utilizes the accelerometer, add it to the osdElementsNeedAccelerometer() function.
58 Finally add a CLI parameter for the new element in cli/settings.c.
59 CLI parameters should be added before line #endif // end of #ifdef USE_OSD
67 Each element can have up to 4 display variants. "Type 1" is always the default and every
68 every element has an implicit type 1 variant even if no additional options exist. The
69 purpose is to allow the user to choose a different element display or rendering style to
70 fit their needs. Like displaying GPS coordinates in a different format, displaying a voltage
71 with a different number of decimal places, etc. The purpose is NOT to display unrelated
72 information in different variants of the element. For example it would be inappropriate
73 to use variants to display RSSI for one type and link quality for another. In this case
74 they should be separate elements. Remember that element variants are mutually exclusive
75 and only one type can be displayed at a time. So they shouldn't be used in cases where
76 the user would want to display different types at the same time - like in the above example
77 where the user might want to display both RSSI and link quality at the same time.
79 As variants are added to the firmware, support must also be included in the Configurator.
81 The following lists the variants implemented so far (please update this as variants are added):
84 type 1: Altitude with one decimal place
85 type 2: Altitude with no decimal (whole number only)
89 type 1: Decimal representation with 7 digits
90 type 2: Decimal representation with 4 digits
91 type 3: Degrees, minutes, seconds
92 type 4: Open location code (Google Plus Code)
95 type 1: Graphical bar showing remaining battery (shrinks as used)
96 type 2: Graphical bar showing battery used (grows as used)
97 type 3: Numeric % of remaining battery
98 type 4: Numeric % or used battery
108 #include "platform.h"
112 #include "blackbox/blackbox.h"
113 #include "blackbox/blackbox_io.h"
115 #include "build/build_config.h"
116 #include "build/debug.h"
118 #include "common/axis.h"
119 #include "common/maths.h"
120 #include "common/printf.h"
121 #include "common/typeconversion.h"
122 #include "common/utils.h"
123 #include "common/unit.h"
125 #include "config/config.h"
126 #include "config/feature.h"
128 #include "drivers/display.h"
129 #include "drivers/dshot.h"
130 #include "drivers/osd_symbols.h"
131 #include "drivers/time.h"
132 #include "drivers/vtx_common.h"
134 #include "fc/controlrate_profile.h"
136 #include "fc/rc_adjustments.h"
137 #include "fc/rc_controls.h"
138 #include "fc/runtime_config.h"
140 #include "flight/gps_rescue.h"
141 #include "flight/position.h"
142 #include "flight/imu.h"
143 #include "flight/mixer.h"
144 #include "flight/pid.h"
150 #include "osd/osd_elements.h"
151 #include "osd/osd_warnings.h"
153 #include "pg/motor.h"
154 #include "pg/stats.h"
158 #include "sensors/adcinternal.h"
159 #include "sensors/barometer.h"
160 #include "sensors/battery.h"
161 #include "sensors/esc_sensor.h"
162 #include "sensors/sensors.h"
164 #ifdef USE_GPS_PLUS_CODES
165 // located in lib/main/google/olc
169 #define AH_SYMBOL_COUNT 9
170 #define AH_SIDEBAR_WIDTH_POS 7
171 #define AH_SIDEBAR_HEIGHT_POS 3
173 // Stick overlay size
174 #define OSD_STICK_OVERLAY_WIDTH 7
175 #define OSD_STICK_OVERLAY_HEIGHT 5
176 #define OSD_STICK_OVERLAY_SPRITE_HEIGHT 3
177 #define OSD_STICK_OVERLAY_VERTICAL_POSITIONS (OSD_STICK_OVERLAY_HEIGHT * OSD_STICK_OVERLAY_SPRITE_HEIGHT)
179 #define FULL_CIRCLE 360
180 #define EFFICIENCY_MINIMUM_SPEED_CM_S 100
182 #define MOTOR_STOPPED_THRESHOLD_RPM 1000
184 #define SINE_25_DEG 0.422618261740699f
186 #ifdef USE_OSD_STICK_OVERLAY
187 typedef struct radioControls_s
{
188 uint8_t left_vertical
;
189 uint8_t left_horizontal
;
190 uint8_t right_vertical
;
191 uint8_t right_horizontal
;
194 static const radioControls_t radioModes
[4] = {
195 { PITCH
, YAW
, THROTTLE
, ROLL
}, // Mode 1
196 { THROTTLE
, YAW
, PITCH
, ROLL
}, // Mode 2
197 { PITCH
, ROLL
, THROTTLE
, YAW
}, // Mode 3
198 { THROTTLE
, ROLL
, PITCH
, YAW
}, // Mode 4
202 static const char compassBar
[] = {
204 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
206 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
208 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
210 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
212 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
214 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
217 static unsigned activeOsdElementCount
= 0;
218 static uint8_t activeOsdElementArray
[OSD_ITEM_COUNT
];
219 static bool backgroundLayerSupported
= false;
222 static bool blinkState
= true;
223 static uint32_t blinkBits
[(OSD_ITEM_COUNT
+ 31) / 32];
224 #define SET_BLINK(item) (blinkBits[(item) / 32] |= (1 << ((item) % 32)))
225 #define CLR_BLINK(item) (blinkBits[(item) / 32] &= ~(1 << ((item) % 32)))
226 #define IS_BLINK(item) (blinkBits[(item) / 32] & (1 << ((item) % 32)))
227 #define BLINK(item) (IS_BLINK(item) && blinkState)
231 static int osdDisplayWrite(osdElementParms_t
*element
, uint8_t x
, uint8_t y
, uint8_t attr
, const char *s
)
233 if (IS_BLINK(element
->item
)) {
234 attr
|= DISPLAYPORT_ATTR_BLINK
;
237 return displayWrite(element
->osdDisplayPort
, x
, y
, attr
, s
);
240 static int osdDisplayWriteChar(osdElementParms_t
*element
, uint8_t x
, uint8_t y
, uint8_t attr
, char c
)
247 return osdDisplayWrite(element
, x
, y
, attr
, buf
);
250 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
251 typedef int (*getEscRpmOrFreqFnPtr
)(int i
);
253 static int getEscRpm(int i
)
255 #ifdef USE_DSHOT_TELEMETRY
256 if (motorConfig()->dev
.useDshotTelemetry
) {
257 return 100.0f
/ (motorConfig()->motorPoleCount
/ 2.0f
) * getDshotTelemetry(i
);
260 #ifdef USE_ESC_SENSOR
261 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
262 return calcEscRpm(getEscSensorData(i
)->rpm
);
268 static int getEscRpmFreq(int i
)
270 return getEscRpm(i
) / 60;
273 static void renderOsdEscRpmOrFreq(getEscRpmOrFreqFnPtr escFnPtr
, osdElementParms_t
*element
)
275 int x
= element
->elemPosX
;
276 int y
= element
->elemPosY
;
277 for (int i
=0; i
< getMotorCount(); i
++) {
279 const int rpm
= MIN((*escFnPtr
)(i
),99999);
280 const int len
= tfp_sprintf(rpmStr
, "%d", rpm
);
282 osdDisplayWrite(element
, x
, y
+ i
, DISPLAYPORT_ATTR_NONE
, rpmStr
);
284 element
->drawElement
= false;
288 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
289 int osdConvertTemperatureToSelectedUnit(int tempInDegreesCelcius
)
291 switch (osdConfig()->units
) {
293 return lrintf(((tempInDegreesCelcius
* 9.0f
) / 5) + 32);
295 return tempInDegreesCelcius
;
300 static void osdFormatAltitudeString(char * buff
, int32_t altitudeCm
, osdElementType_e variantType
)
302 const char unitSymbol
= osdGetMetersToSelectedUnitSymbol();
303 unsigned decimalPlaces
;
305 switch (variantType
) {
306 case OSD_ELEMENT_TYPE_2
: // whole number altitude (no decimal places)
309 case OSD_ELEMENT_TYPE_1
: // one decimal place (default)
314 osdPrintFloat(buff
, SYM_ALTITUDE
, osdGetMetersToSelectedUnit(altitudeCm
) / 100.0f
, "", decimalPlaces
, true, unitSymbol
);
318 static void osdFormatCoordinate(char *buff
, gpsCoordinateType_e coordinateType
, osdElementType_e variantType
)
320 int32_t gpsValue
= 0;
321 const char leadingSymbol
= (coordinateType
== GPS_LONGITUDE
) ? SYM_LON
: SYM_LAT
;
323 if (STATE(GPS_FIX_EVER
)) { // don't display interim coordinates until we get the first position fix
324 gpsValue
= (coordinateType
== GPS_LONGITUDE
) ? gpsSol
.llh
.lon
: gpsSol
.llh
.lat
;
327 const int degreesPart
= ABS(gpsValue
) / GPS_DEGREES_DIVIDER
;
328 int fractionalPart
= ABS(gpsValue
) % GPS_DEGREES_DIVIDER
;
330 switch (variantType
) {
331 #ifdef USE_GPS_PLUS_CODES
332 #define PLUS_CODE_DIGITS 11
333 case OSD_ELEMENT_TYPE_4
: // Open Location Code
337 if (STATE(GPS_FIX_EVER
)) {
339 location
.lat
= (double)gpsSol
.llh
.lat
/ GPS_DEGREES_DIVIDER
;
340 location
.lon
= (double)gpsSol
.llh
.lon
/ GPS_DEGREES_DIVIDER
;
341 OLC_Encode(&location
, PLUS_CODE_DIGITS
, buff
, OSD_ELEMENT_BUFFER_LENGTH
- 3);
343 memset(buff
, SYM_HYPHEN
, PLUS_CODE_DIGITS
+ 1);
345 buff
[PLUS_CODE_DIGITS
+ 1] = '\0';
349 #endif // USE_GPS_PLUS_CODES
351 case OSD_ELEMENT_TYPE_3
: // degree, minutes, seconds style. ddd^mm'ss.00"W
354 *buff
++ = leadingSymbol
;
356 const int minutes
= fractionalPart
* 60 / GPS_DEGREES_DIVIDER
;
357 const int fractionalMinutes
= fractionalPart
* 60 % GPS_DEGREES_DIVIDER
;
358 const int seconds
= fractionalMinutes
* 60 / GPS_DEGREES_DIVIDER
;
359 const int tenthSeconds
= (fractionalMinutes
* 60 % GPS_DEGREES_DIVIDER
) * 10 / GPS_DEGREES_DIVIDER
;
361 if (coordinateType
== GPS_LONGITUDE
) {
362 trailingSymbol
= (gpsValue
< 0) ? 'W' : 'E';
364 trailingSymbol
= (gpsValue
< 0) ? 'S' : 'N';
366 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
);
370 case OSD_ELEMENT_TYPE_2
:
371 fractionalPart
/= 1000;
374 case OSD_ELEMENT_TYPE_1
:
376 *buff
++ = leadingSymbol
;
378 *buff
++ = SYM_HYPHEN
;
380 tfp_sprintf(buff
, (variantType
== OSD_ELEMENT_TYPE_1
? "%u.%07u" : "%u.%04u"), degreesPart
, fractionalPart
);
386 void osdFormatDistanceString(char *ptr
, int distance
, char leadingSymbol
)
388 const float convertedDistance
= osdGetMetersToSelectedUnit(distance
);
390 char unitSymbolExtended
;
393 switch (osdConfig()->units
) {
395 unitTransition
= 5280;
397 unitSymbolExtended
= SYM_MILES
;
400 unitTransition
= 1000;
402 unitSymbolExtended
= SYM_KM
;
406 unsigned decimalPlaces
;
407 float displayDistance
;
409 if (convertedDistance
< unitTransition
) {
411 displayDistance
= convertedDistance
;
412 displaySymbol
= unitSymbol
;
414 displayDistance
= convertedDistance
/ unitTransition
;
415 displaySymbol
= unitSymbolExtended
;
416 if (displayDistance
>= 10) { // >= 10 miles or km - 1 decimal place
418 } else { // < 10 miles or km - 2 decimal places
422 osdPrintFloat(ptr
, leadingSymbol
, displayDistance
, "", decimalPlaces
, false, displaySymbol
);
425 static void osdFormatPID(char * buff
, const char * label
, const pidf_t
* pid
)
427 tfp_sprintf(buff
, "%s %3d %3d %3d %3d", label
, pid
->P
, pid
->I
, pid
->D
, pid
->F
);
431 bool osdFormatRtcDateTime(char *buffer
)
434 if (!rtcGetDateTime(&dateTime
)) {
440 dateTimeFormatLocalShort(buffer
, &dateTime
);
446 void osdFormatTime(char * buff
, osd_timer_precision_e precision
, timeUs_t time
)
448 int seconds
= time
/ 1000000;
449 const int minutes
= seconds
/ 60;
450 seconds
= seconds
% 60;
453 case OSD_TIMER_PREC_SECOND
:
455 tfp_sprintf(buff
, "%02d:%02d", minutes
, seconds
);
457 case OSD_TIMER_PREC_HUNDREDTHS
:
459 const int hundredths
= (time
/ 10000) % 100;
460 tfp_sprintf(buff
, "%02d:%02d.%02d", minutes
, seconds
, hundredths
);
463 case OSD_TIMER_PREC_TENTHS
:
465 const int tenths
= (time
/ 100000) % 10;
466 tfp_sprintf(buff
, "%02d:%02d.%01d", minutes
, seconds
, tenths
);
472 static char osdGetTimerSymbol(osd_timer_source_e src
)
475 case OSD_TIMER_SRC_ON
:
477 case OSD_TIMER_SRC_TOTAL_ARMED
:
478 case OSD_TIMER_SRC_LAST_ARMED
:
480 case OSD_TIMER_SRC_ON_OR_ARMED
:
481 return ARMING_FLAG(ARMED
) ? SYM_FLY_M
: SYM_ON_M
;
487 static timeUs_t
osdGetTimerValue(osd_timer_source_e src
)
490 case OSD_TIMER_SRC_ON
:
492 case OSD_TIMER_SRC_TOTAL_ARMED
:
494 case OSD_TIMER_SRC_LAST_ARMED
: {
495 statistic_t
*stats
= osdGetStats();
496 return stats
->armed_time
;
498 case OSD_TIMER_SRC_ON_OR_ARMED
:
499 return ARMING_FLAG(ARMED
) ? osdFlyTime
: micros();
505 void osdFormatTimer(char *buff
, bool showSymbol
, bool usePrecision
, int timerIndex
)
507 const uint16_t timer
= osdConfig()->timers
[timerIndex
];
508 const uint8_t src
= OSD_TIMER_SRC(timer
);
511 *(buff
++) = osdGetTimerSymbol(src
);
514 osdFormatTime(buff
, (usePrecision
? OSD_TIMER_PRECISION(timer
) : OSD_TIMER_PREC_SECOND
), osdGetTimerValue(src
));
517 static char osdGetBatterySymbol(int cellVoltage
)
519 if (getBatteryState() == BATTERY_CRITICAL
) {
520 return SYM_MAIN_BATT
; // FIXME: currently the BAT- symbol, ideally replace with a battery with exclamation mark
522 // Calculate a symbol offset using cell voltage over full cell voltage range
523 const int symOffset
= scaleRange(cellVoltage
, batteryConfig()->vbatmincellvoltage
, batteryConfig()->vbatmaxcellvoltage
, 0, 8);
524 return SYM_BATT_EMPTY
- constrain(symOffset
, 0, 6);
528 static uint8_t osdGetHeadingIntoDiscreteDirections(int heading
, unsigned directions
)
530 heading
+= FULL_CIRCLE
; // Ensure positive value
532 // Split input heading 0..359 into sectors 0..(directions-1), but offset
533 // by half a sector so that sector 0 gets centered around heading 0.
534 // We multiply heading by directions to not loose precision in divisions
535 // In this way each segment will be a FULL_CIRCLE length
536 int direction
= (heading
* directions
+ FULL_CIRCLE
/ 2) / FULL_CIRCLE
; // scale with rounding
537 direction
%= directions
; // normalize
539 return direction
; // return segment number
542 static uint8_t osdGetDirectionSymbolFromHeading(int heading
)
544 heading
= osdGetHeadingIntoDiscreteDirections(heading
, 16);
546 // Now heading has a heading with Up=0, Right=4, Down=8 and Left=12
547 // Our symbols are Down=0, Right=4, Up=8 and Left=12
548 // There're 16 arrow symbols. Transform it.
549 heading
= 16 - heading
;
550 heading
= (heading
+ 8) % 16;
552 return SYM_ARROW_SOUTH
+ heading
;
557 * Converts altitude based on the current unit system.
558 * @param meters Value in meters to convert
560 float osdGetMetersToSelectedUnit(int32_t meters
)
562 switch (osdConfig()->units
) {
564 return meters
* 3.28084f
; // Convert to feet
566 return meters
; // Already in meters
571 * Gets the correct altitude symbol for the current unit system
573 char osdGetMetersToSelectedUnitSymbol(void)
575 switch (osdConfig()->units
) {
584 * Converts speed based on the current unit system.
585 * @param value in cm/s to convert
587 int32_t osdGetSpeedToSelectedUnit(int32_t value
)
589 switch (osdConfig()->units
) {
592 return CM_S_TO_MPH(value
);
594 return CM_S_TO_KM_H(value
);
599 * Gets the correct speed symbol for the current unit system
601 char osdGetSpeedToSelectedUnitSymbol(void)
603 switch (osdConfig()->units
) {
612 char osdGetVarioToSelectedUnitSymbol(void)
614 switch (osdConfig()->units
) {
622 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
623 char osdGetTemperatureSymbolForSelectedUnit(void)
625 switch (osdConfig()->units
) {
634 // *************************
635 // Element drawing functions
636 // *************************
638 #ifdef USE_OSD_ADJUSTMENTS
639 static void osdElementAdjustmentRange(osdElementParms_t
*element
)
641 const char *name
= getAdjustmentsRangeName();
643 tfp_sprintf(element
->buff
, "%s: %3d", name
, getAdjustmentsRangeValue());
646 #endif // USE_OSD_ADJUSTMENTS
648 static void osdElementAltitude(osdElementParms_t
*element
)
650 bool haveBaro
= false;
651 bool haveGps
= false;
653 haveBaro
= sensors(SENSOR_BARO
);
656 haveGps
= sensors(SENSOR_GPS
) && STATE(GPS_FIX
);
658 if (haveBaro
|| haveGps
) {
659 osdFormatAltitudeString(element
->buff
, getEstimatedAltitudeCm(), element
->type
);
661 element
->buff
[0] = SYM_ALTITUDE
;
662 element
->buff
[1] = SYM_HYPHEN
; // We use this symbol when we don't have a valid measure
663 element
->buff
[2] = '\0';
668 static void osdElementAngleRollPitch(osdElementParms_t
*element
)
670 const float angle
= ((element
->item
== OSD_PITCH_ANGLE
) ? attitude
.values
.pitch
: attitude
.values
.roll
) / 10.0f
;
671 osdPrintFloat(element
->buff
, (element
->item
== OSD_PITCH_ANGLE
) ? SYM_PITCH
: SYM_ROLL
, fabsf(angle
), ((angle
< 0) ? "-%02u" : " %02u"), 1, true, SYM_NONE
);
675 static void osdElementAntiGravity(osdElementParms_t
*element
)
677 if (pidOsdAntiGravityActive()) {
678 strcpy(element
->buff
, "AG");
684 static void osdElementArtificialHorizon(osdElementParms_t
*element
)
686 // Get pitch and roll limits in tenths of degrees
687 const int maxPitch
= osdConfig()->ahMaxPitch
* 10;
688 const int maxRoll
= osdConfig()->ahMaxRoll
* 10;
689 const int ahSign
= osdConfig()->ahInvert
? -1 : 1;
690 const int rollAngle
= constrain(attitude
.values
.roll
* ahSign
, -maxRoll
, maxRoll
);
691 int pitchAngle
= constrain(attitude
.values
.pitch
* ahSign
, -maxPitch
, maxPitch
);
692 // Convert pitchAngle to y compensation value
693 // (maxPitch / 25) divisor matches previous settings of fixed divisor of 8 and fixed max AHI pitch angle of 20.0 degrees
695 pitchAngle
= ((pitchAngle
* 25) / maxPitch
);
697 pitchAngle
-= 41; // 41 = 4 * AH_SYMBOL_COUNT + 5
699 for (int x
= -4; x
<= 4; x
++) {
700 const int y
= ((-rollAngle
* x
) / 64) - pitchAngle
;
701 if (y
>= 0 && y
<= 81) {
702 osdDisplayWriteChar(element
, element
->elemPosX
+ x
, element
->elemPosY
+ (y
/ AH_SYMBOL_COUNT
), DISPLAYPORT_ATTR_NONE
, (SYM_AH_BAR9_0
+ (y
% AH_SYMBOL_COUNT
)));
706 element
->drawElement
= false; // element already drawn
709 static void osdElementUpDownReference(osdElementParms_t
*element
)
711 // Up/Down reference feature displays reference points on the OSD at Zenith and Nadir
712 const float earthUpinBodyFrame
[3] = {-rMat
[2][0], -rMat
[2][1], -rMat
[2][2]}; //transforum the up vector to the body frame
714 if (ABS(earthUpinBodyFrame
[2]) < SINE_25_DEG
&& ABS(earthUpinBodyFrame
[1]) < SINE_25_DEG
) {
715 float thetaB
; // pitch from body frame to zenith/nadir
716 float psiB
; // psi from body frame to zenith/nadir
717 char *symbol
[2] = {"U", "D"}; // character buffer
720 if(attitude
.values
.pitch
>0.0){ //nose down
721 thetaB
= -earthUpinBodyFrame
[2]; // get pitch w/re to nadir (use small angle approx for sine)
722 psiB
= -earthUpinBodyFrame
[1]; // calculate the yaw w/re to nadir (use small angle approx for sine)
725 thetaB
= earthUpinBodyFrame
[2]; // get pitch w/re to zenith (use small angle approx for sine)
726 psiB
= earthUpinBodyFrame
[1]; // calculate the yaw w/re to zenith (use small angle approx for sine)
729 int posX
= element
->elemPosX
+ round(scaleRangef(psiB
, -M_PIf
/ 4, M_PIf
/ 4, -14, 14));
730 int posY
= element
->elemPosY
+ round(scaleRangef(thetaB
, -M_PIf
/ 4, M_PIf
/ 4, -8, 8));
732 osdDisplayWrite(element
, posX
, posY
, DISPLAYPORT_ATTR_NONE
, symbol
[direction
]);
734 element
->drawElement
= false; // element already drawn
738 static void osdElementAverageCellVoltage(osdElementParms_t
*element
)
740 const int cellV
= getBatteryAverageCellVoltage();
741 osdPrintFloat(element
->buff
, osdGetBatterySymbol(cellV
), cellV
/ 100.0f
, "", 2, false, SYM_VOLT
);
744 static void osdElementCompassBar(osdElementParms_t
*element
)
746 memcpy(element
->buff
, compassBar
+ osdGetHeadingIntoDiscreteDirections(DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
), 16), 9);
747 element
->buff
[9] = 0;
750 #ifdef USE_ADC_INTERNAL
751 static void osdElementCoreTemperature(osdElementParms_t
*element
)
753 tfp_sprintf(element
->buff
, "C%c%3d%c", SYM_TEMPERATURE
, osdConvertTemperatureToSelectedUnit(getCoreTemperatureCelsius()), osdGetTemperatureSymbolForSelectedUnit());
755 #endif // USE_ADC_INTERNAL
757 static void osdBackgroundCameraFrame(osdElementParms_t
*element
)
759 const uint8_t xpos
= element
->elemPosX
;
760 const uint8_t ypos
= element
->elemPosY
;
761 const uint8_t width
= constrain(osdConfig()->camera_frame_width
, OSD_CAMERA_FRAME_MIN_WIDTH
, OSD_CAMERA_FRAME_MAX_WIDTH
);
762 const uint8_t height
= constrain(osdConfig()->camera_frame_height
, OSD_CAMERA_FRAME_MIN_HEIGHT
, OSD_CAMERA_FRAME_MAX_HEIGHT
);
764 element
->buff
[0] = SYM_STICK_OVERLAY_CENTER
;
765 for (int i
= 1; i
< (width
- 1); i
++) {
766 element
->buff
[i
] = SYM_STICK_OVERLAY_HORIZONTAL
;
768 element
->buff
[width
- 1] = SYM_STICK_OVERLAY_CENTER
;
769 element
->buff
[width
] = 0; // string terminator
771 osdDisplayWrite(element
, xpos
, ypos
, DISPLAYPORT_ATTR_NONE
, element
->buff
);
772 for (int i
= 1; i
< (height
- 1); i
++) {
773 osdDisplayWriteChar(element
, xpos
, ypos
+ i
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_VERTICAL
);
774 osdDisplayWriteChar(element
, xpos
+ width
- 1, ypos
+ i
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_VERTICAL
);
776 osdDisplayWrite(element
, xpos
, ypos
+ height
- 1, DISPLAYPORT_ATTR_NONE
, element
->buff
);
778 element
->drawElement
= false; // element already drawn
781 static void osdBackgroundCraftName(osdElementParms_t
*element
)
783 if (strlen(pilotConfig()->name
) == 0) {
784 strcpy(element
->buff
, "CRAFT_NAME");
787 for (i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
788 if (pilotConfig()->name
[i
]) {
789 element
->buff
[i
] = toupper((unsigned char)pilotConfig()->name
[i
]);
794 element
->buff
[i
] = '\0';
799 static void osdElementCrashFlipArrow(osdElementParms_t
*element
)
801 int rollAngle
= attitude
.values
.roll
/ 10;
802 const int pitchAngle
= attitude
.values
.pitch
/ 10;
803 if (abs(rollAngle
) > 90) {
804 rollAngle
= (rollAngle
< 0 ? -180 : 180) - rollAngle
;
807 if ((isFlipOverAfterCrashActive() || (!ARMING_FLAG(ARMED
) && !isUpright())) && !((imuConfig()->small_angle
< 180 && isUpright()) || (rollAngle
== 0 && pitchAngle
== 0))) {
808 if (abs(pitchAngle
) < 2 * abs(rollAngle
) && abs(rollAngle
) < 2 * abs(pitchAngle
)) {
809 if (pitchAngle
> 0) {
811 element
->buff
[0] = SYM_ARROW_WEST
+ 2;
813 element
->buff
[0] = SYM_ARROW_EAST
- 2;
817 element
->buff
[0] = SYM_ARROW_WEST
- 2;
819 element
->buff
[0] = SYM_ARROW_EAST
+ 2;
823 if (abs(pitchAngle
) > abs(rollAngle
)) {
824 if (pitchAngle
> 0) {
825 element
->buff
[0] = SYM_ARROW_SOUTH
;
827 element
->buff
[0] = SYM_ARROW_NORTH
;
831 element
->buff
[0] = SYM_ARROW_WEST
;
833 element
->buff
[0] = SYM_ARROW_EAST
;
837 element
->buff
[1] = '\0';
842 static void osdElementCrosshairs(osdElementParms_t
*element
)
844 element
->buff
[0] = SYM_AH_CENTER_LINE
;
845 element
->buff
[1] = SYM_AH_CENTER
;
846 element
->buff
[2] = SYM_AH_CENTER_LINE_RIGHT
;
847 element
->buff
[3] = 0;
850 static void osdElementCurrentDraw(osdElementParms_t
*element
)
852 const float amperage
= fabsf(getAmperage() / 100.0f
);
853 osdPrintFloat(element
->buff
, SYM_NONE
, amperage
, "%3u", 2, false, SYM_AMP
);
856 static void osdElementDebug(osdElementParms_t
*element
)
858 tfp_sprintf(element
->buff
, "DBG %5d %5d %5d %5d", debug
[0], debug
[1], debug
[2], debug
[3]);
861 static void osdElementDisarmed(osdElementParms_t
*element
)
863 if (!ARMING_FLAG(ARMED
)) {
864 tfp_sprintf(element
->buff
, "DISARMED");
868 static void osdBackgroundDisplayName(osdElementParms_t
*element
)
870 if (strlen(pilotConfig()->displayName
) == 0) {
871 strcpy(element
->buff
, "DISPLAY_NAME");
874 for (i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
875 if (pilotConfig()->displayName
[i
]) {
876 element
->buff
[i
] = toupper((unsigned char)pilotConfig()->displayName
[i
]);
881 element
->buff
[i
] = '\0';
885 #ifdef USE_PERSISTENT_STATS
886 static void osdElementTotalFlights(osdElementParms_t
*element
)
888 const int32_t total_flights
= statsConfig()->stats_total_flights
;
889 tfp_sprintf(element
->buff
, "#%d", total_flights
);
893 #ifdef USE_PROFILE_NAMES
894 static void osdElementRateProfileName(osdElementParms_t
*element
)
896 if (strlen(currentControlRateProfile
->profileName
) == 0) {
897 tfp_sprintf(element
->buff
, "RATE_%u", getCurrentControlRateProfileIndex() + 1);
900 for (i
= 0; i
< MAX_PROFILE_NAME_LENGTH
; i
++) {
901 if (currentControlRateProfile
->profileName
[i
]) {
902 element
->buff
[i
] = toupper((unsigned char)currentControlRateProfile
->profileName
[i
]);
907 element
->buff
[i
] = '\0';
911 static void osdElementPidProfileName(osdElementParms_t
*element
)
913 if (strlen(currentPidProfile
->profileName
) == 0) {
914 tfp_sprintf(element
->buff
, "PID_%u", getCurrentPidProfileIndex() + 1);
917 for (i
= 0; i
< MAX_PROFILE_NAME_LENGTH
; i
++) {
918 if (currentPidProfile
->profileName
[i
]) {
919 element
->buff
[i
] = toupper((unsigned char)currentPidProfile
->profileName
[i
]);
924 element
->buff
[i
] = '\0';
929 #ifdef USE_OSD_PROFILES
930 static void osdElementOsdProfileName(osdElementParms_t
*element
)
932 uint8_t profileIndex
= getCurrentOsdProfileIndex();
934 if (strlen(osdConfig()->profile
[profileIndex
- 1]) == 0) {
935 tfp_sprintf(element
->buff
, "OSD_%u", profileIndex
);
938 for (i
= 0; i
< OSD_PROFILE_NAME_LENGTH
; i
++) {
939 if (osdConfig()->profile
[profileIndex
- 1][i
]) {
940 element
->buff
[i
] = toupper((unsigned char)osdConfig()->profile
[profileIndex
- 1][i
]);
945 element
->buff
[i
] = '\0';
950 #ifdef USE_ESC_SENSOR
951 static void osdElementEscTemperature(osdElementParms_t
*element
)
953 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
954 tfp_sprintf(element
->buff
, "E%c%3d%c", SYM_TEMPERATURE
, osdConvertTemperatureToSelectedUnit(osdEscDataCombined
->temperature
), osdGetTemperatureSymbolForSelectedUnit());
957 #endif // USE_ESC_SENSOR
959 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
960 static void osdElementEscRpm(osdElementParms_t
*element
)
962 renderOsdEscRpmOrFreq(&getEscRpm
,element
);
965 static void osdElementEscRpmFreq(osdElementParms_t
*element
)
967 renderOsdEscRpmOrFreq(&getEscRpmFreq
,element
);
971 static void osdElementFlymode(osdElementParms_t
*element
)
973 // Note that flight mode display has precedence in what to display.
976 // 3. ANGLE, HORIZON, ACRO TRAINER
980 if (FLIGHT_MODE(FAILSAFE_MODE
)) {
981 strcpy(element
->buff
, "!FS!");
982 } else if (FLIGHT_MODE(GPS_RESCUE_MODE
)) {
983 strcpy(element
->buff
, "RESC");
984 } else if (FLIGHT_MODE(HEADFREE_MODE
)) {
985 strcpy(element
->buff
, "HEAD");
986 } else if (FLIGHT_MODE(ANGLE_MODE
)) {
987 strcpy(element
->buff
, "ANGL");
988 } else if (FLIGHT_MODE(HORIZON_MODE
)) {
989 strcpy(element
->buff
, "HOR ");
990 } else if (IS_RC_MODE_ACTIVE(BOXACROTRAINER
)) {
991 strcpy(element
->buff
, "ATRN");
992 } else if (airmodeIsEnabled()) {
993 strcpy(element
->buff
, "AIR ");
995 strcpy(element
->buff
, "ACRO");
1000 static void osdElementGForce(osdElementParms_t
*element
)
1002 osdPrintFloat(element
->buff
, SYM_NONE
, osdGForce
, "", 1, true, 'G');
1007 static void osdElementGpsFlightDistance(osdElementParms_t
*element
)
1009 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1010 osdFormatDistanceString(element
->buff
, GPS_distanceFlownInCm
/ 100, SYM_TOTAL_DISTANCE
);
1012 // We use this symbol when we don't have a FIX
1013 tfp_sprintf(element
->buff
, "%c%c", SYM_TOTAL_DISTANCE
, SYM_HYPHEN
);
1017 static void osdElementGpsHomeDirection(osdElementParms_t
*element
)
1019 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1020 if (GPS_distanceToHome
> 0) {
1021 const int h
= GPS_directionToHome
- DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
1022 element
->buff
[0] = osdGetDirectionSymbolFromHeading(h
);
1024 element
->buff
[0] = SYM_OVER_HOME
;
1028 // We use this symbol when we don't have a FIX
1029 element
->buff
[0] = SYM_HYPHEN
;
1032 element
->buff
[1] = 0;
1035 static void osdElementGpsHomeDistance(osdElementParms_t
*element
)
1037 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1038 osdFormatDistanceString(element
->buff
, GPS_distanceToHome
, SYM_HOMEFLAG
);
1040 element
->buff
[0] = SYM_HOMEFLAG
;
1041 // We use this symbol when we don't have a FIX
1042 element
->buff
[1] = SYM_HYPHEN
;
1043 element
->buff
[2] = '\0';
1047 static void osdElementGpsCoordinate(osdElementParms_t
*element
)
1049 const gpsCoordinateType_e coordinateType
= (element
->item
== OSD_GPS_LON
) ? GPS_LONGITUDE
: GPS_LATITUDE
;
1050 osdFormatCoordinate(element
->buff
, coordinateType
, element
->type
);
1051 if (STATE(GPS_FIX_EVER
) && !STATE(GPS_FIX
)) {
1052 SET_BLINK(element
->item
); // blink if we had a fix but have since lost it
1054 CLR_BLINK(element
->item
);
1058 static void osdElementGpsSats(osdElementParms_t
*element
)
1060 if (!gpsIsHealthy()) {
1061 tfp_sprintf(element
->buff
, "%c%cNC", SYM_SAT_L
, SYM_SAT_R
);
1063 int pos
= tfp_sprintf(element
->buff
, "%c%c%2d", SYM_SAT_L
, SYM_SAT_R
, gpsSol
.numSat
);
1064 if (osdConfig()->gps_sats_show_hdop
) { // add on the GPS module HDOP estimate
1065 element
->buff
[pos
++] = ' ';
1066 osdPrintFloat(element
->buff
+ pos
, SYM_NONE
, gpsSol
.hdop
/ 100.0f
, "", 1, true, SYM_NONE
);
1071 static void osdElementGpsSpeed(osdElementParms_t
*element
)
1073 if (STATE(GPS_FIX
)) {
1074 tfp_sprintf(element
->buff
, "%c%3d%c", SYM_SPEED
, osdGetSpeedToSelectedUnit(gpsConfig()->gps_use_3d_speed
? gpsSol
.speed3d
: gpsSol
.groundSpeed
), osdGetSpeedToSelectedUnitSymbol());
1076 tfp_sprintf(element
->buff
, "%c%c%c", SYM_SPEED
, SYM_HYPHEN
, osdGetSpeedToSelectedUnitSymbol());
1080 static void osdElementEfficiency(osdElementParms_t
*element
)
1083 if (sensors(SENSOR_GPS
) && ARMING_FLAG(ARMED
) && STATE(GPS_FIX
) && gpsSol
.groundSpeed
>= EFFICIENCY_MINIMUM_SPEED_CM_S
) {
1084 const int speedX100
= osdGetSpeedToSelectedUnit(gpsSol
.groundSpeed
* 100); // speed * 100 for improved resolution at slow speeds
1086 if (speedX100
> 0) {
1087 const int mAmperage
= getAmperage() * 10; // Current in mA
1088 efficiency
= mAmperage
* 100 / speedX100
; // mAmperage * 100 to cancel out speed * 100 from above
1092 const char unitSymbol
= osdConfig()->units
== UNIT_IMPERIAL
? SYM_MILES
: SYM_KM
;
1093 if (efficiency
> 0 && efficiency
<= 9999) {
1094 tfp_sprintf(element
->buff
, "%4d%c/%c", efficiency
, SYM_MAH
, unitSymbol
);
1096 tfp_sprintf(element
->buff
, "----%c/%c", SYM_MAH
, unitSymbol
);
1101 static void osdBackgroundHorizonSidebars(osdElementParms_t
*element
)
1104 const int8_t hudwidth
= AH_SIDEBAR_WIDTH_POS
;
1105 const int8_t hudheight
= AH_SIDEBAR_HEIGHT_POS
;
1106 for (int y
= -hudheight
; y
<= hudheight
; y
++) {
1107 osdDisplayWriteChar(element
, element
->elemPosX
- hudwidth
, element
->elemPosY
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_AH_DECORATION
);
1108 osdDisplayWriteChar(element
, element
->elemPosX
+ hudwidth
, element
->elemPosY
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_AH_DECORATION
);
1111 // AH level indicators
1112 osdDisplayWriteChar(element
, element
->elemPosX
- hudwidth
+ 1, element
->elemPosY
, DISPLAYPORT_ATTR_NONE
, SYM_AH_LEFT
);
1113 osdDisplayWriteChar(element
, element
->elemPosX
+ hudwidth
- 1, element
->elemPosY
, DISPLAYPORT_ATTR_NONE
, SYM_AH_RIGHT
);
1115 element
->drawElement
= false; // element already drawn
1118 #ifdef USE_RX_LINK_QUALITY_INFO
1119 static void osdElementLinkQuality(osdElementParms_t
*element
)
1121 uint16_t osdLinkQuality
= 0;
1122 if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_CRSF
) { // 0-99
1123 osdLinkQuality
= rxGetLinkQuality();
1124 const uint8_t osdRfMode
= rxGetRfMode();
1125 tfp_sprintf(element
->buff
, "%c%1d:%2d", SYM_LINK_QUALITY
, osdRfMode
, osdLinkQuality
);
1126 } else if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_GHST
) { // 0-100
1127 osdLinkQuality
= rxGetLinkQuality();
1128 tfp_sprintf(element
->buff
, "%c%2d", SYM_LINK_QUALITY
, osdLinkQuality
);
1130 osdLinkQuality
= rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE
;
1131 if (osdLinkQuality
>= 10) {
1134 tfp_sprintf(element
->buff
, "%c%1d", SYM_LINK_QUALITY
, osdLinkQuality
);
1137 #endif // USE_RX_LINK_QUALITY_INFO
1139 #ifdef USE_RX_LINK_UPLINK_POWER
1140 static void osdElementTxUplinkPower(osdElementParms_t
*element
)
1142 const uint16_t osdUplinkTxPowerMw
= rxGetUplinkTxPwrMw();
1143 if (osdUplinkTxPowerMw
< 1000) {
1144 tfp_sprintf(element
->buff
, "%c%3dMW", SYM_RSSI
, osdUplinkTxPowerMw
);
1146 osdPrintFloat(element
->buff
, SYM_RSSI
, osdUplinkTxPowerMw
/ 1000.0f
, "", 1, false, 'W');
1149 #endif // USE_RX_LINK_UPLINK_POWER
1152 static void osdElementLogStatus(osdElementParms_t
*element
)
1154 if (IS_RC_MODE_ACTIVE(BOXBLACKBOX
)) {
1155 if (!isBlackboxDeviceWorking()) {
1156 tfp_sprintf(element
->buff
, "%c!", SYM_BBLOG
);
1157 } else if (isBlackboxDeviceFull()) {
1158 tfp_sprintf(element
->buff
, "%c>", SYM_BBLOG
);
1160 int32_t logNumber
= blackboxGetLogNumber();
1161 if (logNumber
>= 0) {
1162 tfp_sprintf(element
->buff
, "%c%d", SYM_BBLOG
, logNumber
);
1164 tfp_sprintf(element
->buff
, "%c", SYM_BBLOG
);
1169 #endif // USE_BLACKBOX
1171 static void osdElementMahDrawn(osdElementParms_t
*element
)
1173 tfp_sprintf(element
->buff
, "%4d%c", getMAhDrawn(), SYM_MAH
);
1176 static void osdElementMainBatteryUsage(osdElementParms_t
*element
)
1178 // Set length of indicator bar
1179 #define MAIN_BATT_USAGE_STEPS 11 // Use an odd number so the bar can be centered.
1181 const int usedCapacity
= getMAhDrawn();
1182 int displayBasis
= usedCapacity
;
1184 switch (element
->type
) {
1185 case OSD_ELEMENT_TYPE_3
: // mAh remaining percentage (counts down as battery is used)
1186 displayBasis
= constrain(batteryConfig()->batteryCapacity
- usedCapacity
, 0, batteryConfig()->batteryCapacity
);
1189 case OSD_ELEMENT_TYPE_4
: // mAh used percentage (counts up as battery is used)
1191 int displayPercent
= 0;
1192 if (batteryConfig()->batteryCapacity
) {
1193 displayPercent
= constrain(lrintf(100.0f
* displayBasis
/ batteryConfig()->batteryCapacity
), 0, 100);
1195 tfp_sprintf(element
->buff
, "%c%d%%", SYM_MAH
, displayPercent
);
1199 case OSD_ELEMENT_TYPE_2
: // mAh used graphical progress bar (grows as battery is used)
1200 displayBasis
= constrain(batteryConfig()->batteryCapacity
- usedCapacity
, 0, batteryConfig()->batteryCapacity
);
1203 case OSD_ELEMENT_TYPE_1
: // mAh remaining graphical progress bar (shrinks as battery is used)
1206 uint8_t remainingCapacityBars
= 0;
1208 if (batteryConfig()->batteryCapacity
) {
1209 const float batteryRemaining
= constrain(batteryConfig()->batteryCapacity
- displayBasis
, 0, batteryConfig()->batteryCapacity
);
1210 remainingCapacityBars
= ceilf((batteryRemaining
/ (batteryConfig()->batteryCapacity
/ MAIN_BATT_USAGE_STEPS
)));
1213 // Create empty battery indicator bar
1214 element
->buff
[0] = SYM_PB_START
;
1215 for (int i
= 1; i
<= MAIN_BATT_USAGE_STEPS
; i
++) {
1216 element
->buff
[i
] = i
<= remainingCapacityBars
? SYM_PB_FULL
: SYM_PB_EMPTY
;
1218 element
->buff
[MAIN_BATT_USAGE_STEPS
+ 1] = SYM_PB_CLOSE
;
1219 if (remainingCapacityBars
> 0 && remainingCapacityBars
< MAIN_BATT_USAGE_STEPS
) {
1220 element
->buff
[1 + remainingCapacityBars
] = SYM_PB_END
;
1222 element
->buff
[MAIN_BATT_USAGE_STEPS
+2] = '\0';
1228 static void osdElementMainBatteryVoltage(osdElementParms_t
*element
)
1230 unsigned decimalPlaces
;
1231 const float batteryVoltage
= getBatteryVoltage() / 100.0f
;
1233 if (batteryVoltage
>= 10) { // if voltage is 10v or more then display only 1 decimal place
1238 osdPrintFloat(element
->buff
, osdGetBatterySymbol(getBatteryAverageCellVoltage()), batteryVoltage
, "", decimalPlaces
, true, SYM_VOLT
);
1241 static void osdElementMotorDiagnostics(osdElementParms_t
*element
)
1244 const bool motorsRunning
= areMotorsRunning();
1245 for (; i
< getMotorCount(); i
++) {
1246 if (motorsRunning
) {
1247 element
->buff
[i
] = 0x88 - scaleRange(motor
[i
], getMotorOutputLow(), getMotorOutputHigh(), 0, 8);
1248 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
1249 if (getEscRpm(i
) < MOTOR_STOPPED_THRESHOLD_RPM
) {
1250 // Motor is not spinning properly. Mark as Stopped
1251 element
->buff
[i
] = 'S';
1255 element
->buff
[i
] = 0x88;
1258 element
->buff
[i
] = '\0';
1261 static void osdElementNumericalHeading(osdElementParms_t
*element
)
1263 const int heading
= DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
1264 tfp_sprintf(element
->buff
, "%c%03d", osdGetDirectionSymbolFromHeading(heading
), heading
);
1268 static void osdElementNumericalVario(osdElementParms_t
*element
)
1270 bool haveBaro
= false;
1271 bool haveGps
= false;
1273 haveBaro
= sensors(SENSOR_BARO
);
1276 haveGps
= sensors(SENSOR_GPS
) && STATE(GPS_FIX
);
1278 if (haveBaro
|| haveGps
) {
1279 const float verticalSpeed
= osdGetMetersToSelectedUnit(getEstimatedVario()) / 100.0f
;
1280 const char directionSymbol
= verticalSpeed
< 0 ? SYM_ARROW_SMALL_DOWN
: SYM_ARROW_SMALL_UP
;
1281 osdPrintFloat(element
->buff
, directionSymbol
, fabsf(verticalSpeed
), "", 1, true, osdGetVarioToSelectedUnitSymbol());
1283 // We use this symbol when we don't have a valid measure
1284 element
->buff
[0] = SYM_HYPHEN
;
1285 element
->buff
[1] = '\0';
1290 static void osdElementPidRateProfile(osdElementParms_t
*element
)
1292 tfp_sprintf(element
->buff
, "%d-%d", getCurrentPidProfileIndex() + 1, getCurrentControlRateProfileIndex() + 1);
1295 static void osdElementPidsPitch(osdElementParms_t
*element
)
1297 osdFormatPID(element
->buff
, "PIT", ¤tPidProfile
->pid
[PID_PITCH
]);
1300 static void osdElementPidsRoll(osdElementParms_t
*element
)
1302 osdFormatPID(element
->buff
, "ROL", ¤tPidProfile
->pid
[PID_ROLL
]);
1305 static void osdElementPidsYaw(osdElementParms_t
*element
)
1307 osdFormatPID(element
->buff
, "YAW", ¤tPidProfile
->pid
[PID_YAW
]);
1310 static void osdElementPower(osdElementParms_t
*element
)
1312 tfp_sprintf(element
->buff
, "%4dW", getAmperage() * getBatteryVoltage() / 10000);
1315 static void osdElementRcChannels(osdElementParms_t
*element
)
1317 const uint8_t xpos
= element
->elemPosX
;
1318 const uint8_t ypos
= element
->elemPosY
;
1320 for (int i
= 0; i
< OSD_RCCHANNELS_COUNT
; i
++) {
1321 if (osdConfig()->rcChannels
[i
] >= 0) {
1322 // Translate (1000, 2000) to (-1000, 1000)
1323 int data
= scaleRange(rcData
[osdConfig()->rcChannels
[i
]], PWM_RANGE_MIN
, PWM_RANGE_MAX
, -1000, 1000);
1324 // Opt for the simplest formatting for now.
1325 // Decimal notation can be added when tfp_sprintf supports float among fancy options.
1327 tfp_sprintf(fmtbuf
, "%5d", data
);
1328 osdDisplayWrite(element
, xpos
, ypos
+ i
, DISPLAYPORT_ATTR_NONE
, fmtbuf
);
1332 element
->drawElement
= false; // element already drawn
1335 static void osdElementRemainingTimeEstimate(osdElementParms_t
*element
)
1337 const int mAhDrawn
= getMAhDrawn();
1339 if (mAhDrawn
<= 0.1 * osdConfig()->cap_alarm
) { // also handles the mAhDrawn == 0 condition
1340 tfp_sprintf(element
->buff
, "--:--");
1341 } else if (mAhDrawn
> osdConfig()->cap_alarm
) {
1342 tfp_sprintf(element
->buff
, "00:00");
1344 const int remaining_time
= (int)((osdConfig()->cap_alarm
- mAhDrawn
) * ((float)osdFlyTime
) / mAhDrawn
);
1345 osdFormatTime(element
->buff
, OSD_TIMER_PREC_SECOND
, remaining_time
);
1349 static void osdElementRssi(osdElementParms_t
*element
)
1351 uint16_t osdRssi
= getRssi() * 100 / 1024; // change range
1352 if (osdRssi
>= 100) {
1356 tfp_sprintf(element
->buff
, "%c%2d", SYM_RSSI
, osdRssi
);
1360 static void osdElementRtcTime(osdElementParms_t
*element
)
1362 osdFormatRtcDateTime(&element
->buff
[0]);
1364 #endif // USE_RTC_TIME
1366 #ifdef USE_RX_RSSI_DBM
1367 static void osdElementRssiDbm(osdElementParms_t
*element
)
1369 tfp_sprintf(element
->buff
, "%c%3d", SYM_RSSI
, getRssiDbm());
1371 #endif // USE_RX_RSSI_DBM
1373 #ifdef USE_OSD_STICK_OVERLAY
1374 static void osdBackgroundStickOverlay(osdElementParms_t
*element
)
1376 const uint8_t xpos
= element
->elemPosX
;
1377 const uint8_t ypos
= element
->elemPosY
;
1379 // Draw the axis first
1380 for (unsigned x
= 0; x
< OSD_STICK_OVERLAY_WIDTH
; x
++) {
1381 for (unsigned y
= 0; y
< OSD_STICK_OVERLAY_HEIGHT
; y
++) {
1382 // draw the axes, vertical and horizonal
1383 if ((x
== ((OSD_STICK_OVERLAY_WIDTH
- 1) / 2)) && (y
== (OSD_STICK_OVERLAY_HEIGHT
- 1) / 2)) {
1384 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_CENTER
);
1385 } else if (x
== ((OSD_STICK_OVERLAY_WIDTH
- 1) / 2)) {
1386 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_VERTICAL
);
1387 } else if (y
== ((OSD_STICK_OVERLAY_HEIGHT
- 1) / 2)) {
1388 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_HORIZONTAL
);
1393 element
->drawElement
= false; // element already drawn
1396 static void osdElementStickOverlay(osdElementParms_t
*element
)
1398 const uint8_t xpos
= element
->elemPosX
;
1399 const uint8_t ypos
= element
->elemPosY
;
1401 // Now draw the cursor
1402 rc_alias_e vertical_channel
, horizontal_channel
;
1404 if (element
->item
== OSD_STICK_OVERLAY_LEFT
) {
1405 vertical_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].left_vertical
;
1406 horizontal_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].left_horizontal
;
1408 vertical_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].right_vertical
;
1409 horizontal_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].right_horizontal
;
1412 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
);
1413 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
);
1414 const char cursor
= SYM_STICK_OVERLAY_SPRITE_HIGH
+ (cursorY
% OSD_STICK_OVERLAY_SPRITE_HEIGHT
);
1416 osdDisplayWriteChar(element
, xpos
+ cursorX
, ypos
+ cursorY
/ OSD_STICK_OVERLAY_SPRITE_HEIGHT
, DISPLAYPORT_ATTR_NONE
, cursor
);
1418 element
->drawElement
= false; // element already drawn
1420 #endif // USE_OSD_STICK_OVERLAY
1422 static void osdElementThrottlePosition(osdElementParms_t
*element
)
1424 tfp_sprintf(element
->buff
, "%c%3d", SYM_THR
, calculateThrottlePercent());
1427 static void osdElementTimer(osdElementParms_t
*element
)
1429 osdFormatTimer(element
->buff
, true, true, element
->item
- OSD_ITEM_TIMER_1
);
1432 #ifdef USE_VTX_COMMON
1433 static void osdElementVtxChannel(osdElementParms_t
*element
)
1435 const vtxDevice_t
*vtxDevice
= vtxCommonDevice();
1436 const char vtxBandLetter
= vtxCommonLookupBandLetter(vtxDevice
, vtxSettingsConfig()->band
);
1437 const char *vtxChannelName
= vtxCommonLookupChannelName(vtxDevice
, vtxSettingsConfig()->channel
);
1438 unsigned vtxStatus
= 0;
1439 uint8_t vtxPower
= vtxSettingsConfig()->power
;
1441 vtxCommonGetStatus(vtxDevice
, &vtxStatus
);
1443 if (vtxSettingsConfig()->lowPowerDisarm
) {
1444 vtxCommonGetPowerIndex(vtxDevice
, &vtxPower
);
1447 const char *vtxPowerLabel
= vtxCommonLookupPowerName(vtxDevice
, vtxPower
);
1449 char vtxStatusIndicator
= '\0';
1450 if (IS_RC_MODE_ACTIVE(BOXVTXCONTROLDISABLE
)) {
1451 vtxStatusIndicator
= 'D';
1452 } else if (vtxStatus
& VTX_STATUS_PIT_MODE
) {
1453 vtxStatusIndicator
= 'P';
1456 if (vtxStatus
& VTX_STATUS_LOCKED
) {
1457 tfp_sprintf(element
->buff
, "-:-:-:L");
1458 } else if (vtxStatusIndicator
) {
1459 tfp_sprintf(element
->buff
, "%c:%s:%s:%c", vtxBandLetter
, vtxChannelName
, vtxPowerLabel
, vtxStatusIndicator
);
1461 tfp_sprintf(element
->buff
, "%c:%s:%s", vtxBandLetter
, vtxChannelName
, vtxPowerLabel
);
1464 #endif // USE_VTX_COMMON
1466 static void osdElementWarnings(osdElementParms_t
*element
)
1468 bool elementBlinking
= false;
1469 renderOsdWarning(element
->buff
, &elementBlinking
, &element
->attr
);
1470 if (elementBlinking
) {
1471 SET_BLINK(OSD_WARNINGS
);
1473 CLR_BLINK(OSD_WARNINGS
);
1477 // Define the order in which the elements are drawn.
1478 // Elements positioned later in the list will overlay the earlier
1479 // ones if their character positions overlap
1480 // Elements that need special runtime conditional processing should be added
1481 // to osdAddActiveElements()
1483 static const uint8_t osdElementDisplayOrder
[] = {
1484 OSD_MAIN_BATT_VOLTAGE
,
1487 OSD_HORIZON_SIDEBARS
,
1488 OSD_UP_DOWN_REFERENCE
,
1491 OSD_REMAINING_TIME_ESTIMATE
,
1503 OSD_PIDRATE_PROFILE
,
1505 OSD_AVG_CELL_VOLTAGE
,
1509 OSD_MAIN_BATT_USAGE
,
1511 OSD_NUMERICAL_HEADING
,
1513 OSD_NUMERICAL_VARIO
,
1528 #ifdef USE_OSD_ADJUSTMENTS
1529 OSD_ADJUSTMENT_RANGE
,
1531 #ifdef USE_ADC_INTERNAL
1532 OSD_CORE_TEMPERATURE
,
1534 #ifdef USE_RX_LINK_QUALITY_INFO
1537 #ifdef USE_RX_LINK_UPLINK_POWER
1538 OSD_TX_UPLINK_POWER
,
1540 #ifdef USE_RX_RSSI_DBM
1543 #ifdef USE_OSD_STICK_OVERLAY
1544 OSD_STICK_OVERLAY_LEFT
,
1545 OSD_STICK_OVERLAY_RIGHT
,
1547 #ifdef USE_PROFILE_NAMES
1548 OSD_RATE_PROFILE_NAME
,
1549 OSD_PID_PROFILE_NAME
,
1551 #ifdef USE_OSD_PROFILES
1556 #ifdef USE_PERSISTENT_STATS
1561 // Define the mapping between the OSD element id and the function to draw it
1563 const osdElementDrawFn osdElementDrawFunction
[OSD_ITEM_COUNT
] = {
1564 [OSD_CAMERA_FRAME
] = NULL
, // only has background. Added first so it's the lowest "layer" and doesn't cover other elements
1565 [OSD_RSSI_VALUE
] = osdElementRssi
,
1566 [OSD_MAIN_BATT_VOLTAGE
] = osdElementMainBatteryVoltage
,
1567 [OSD_CROSSHAIRS
] = osdElementCrosshairs
, // only has background, but needs to be over other elements (like artificial horizon)
1569 [OSD_ARTIFICIAL_HORIZON
] = osdElementArtificialHorizon
,
1570 [OSD_UP_DOWN_REFERENCE
] = osdElementUpDownReference
,
1572 [OSD_HORIZON_SIDEBARS
] = NULL
, // only has background
1573 [OSD_ITEM_TIMER_1
] = osdElementTimer
,
1574 [OSD_ITEM_TIMER_2
] = osdElementTimer
,
1575 [OSD_FLYMODE
] = osdElementFlymode
,
1576 [OSD_CRAFT_NAME
] = NULL
, // only has background
1577 [OSD_THROTTLE_POS
] = osdElementThrottlePosition
,
1578 #ifdef USE_VTX_COMMON
1579 [OSD_VTX_CHANNEL
] = osdElementVtxChannel
,
1581 [OSD_CURRENT_DRAW
] = osdElementCurrentDraw
,
1582 [OSD_MAH_DRAWN
] = osdElementMahDrawn
,
1584 [OSD_GPS_SPEED
] = osdElementGpsSpeed
,
1585 [OSD_GPS_SATS
] = osdElementGpsSats
,
1587 [OSD_ALTITUDE
] = osdElementAltitude
,
1588 [OSD_ROLL_PIDS
] = osdElementPidsRoll
,
1589 [OSD_PITCH_PIDS
] = osdElementPidsPitch
,
1590 [OSD_YAW_PIDS
] = osdElementPidsYaw
,
1591 [OSD_POWER
] = osdElementPower
,
1592 [OSD_PIDRATE_PROFILE
] = osdElementPidRateProfile
,
1593 [OSD_WARNINGS
] = osdElementWarnings
,
1594 [OSD_AVG_CELL_VOLTAGE
] = osdElementAverageCellVoltage
,
1596 [OSD_GPS_LON
] = osdElementGpsCoordinate
,
1597 [OSD_GPS_LAT
] = osdElementGpsCoordinate
,
1599 [OSD_DEBUG
] = osdElementDebug
,
1601 [OSD_PITCH_ANGLE
] = osdElementAngleRollPitch
,
1602 [OSD_ROLL_ANGLE
] = osdElementAngleRollPitch
,
1604 [OSD_MAIN_BATT_USAGE
] = osdElementMainBatteryUsage
,
1605 [OSD_DISARMED
] = osdElementDisarmed
,
1607 [OSD_HOME_DIR
] = osdElementGpsHomeDirection
,
1608 [OSD_HOME_DIST
] = osdElementGpsHomeDistance
,
1610 [OSD_NUMERICAL_HEADING
] = osdElementNumericalHeading
,
1612 [OSD_NUMERICAL_VARIO
] = osdElementNumericalVario
,
1614 [OSD_COMPASS_BAR
] = osdElementCompassBar
,
1615 #ifdef USE_ESC_SENSOR
1616 [OSD_ESC_TMP
] = osdElementEscTemperature
,
1618 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1619 [OSD_ESC_RPM
] = osdElementEscRpm
,
1621 [OSD_REMAINING_TIME_ESTIMATE
] = osdElementRemainingTimeEstimate
,
1623 [OSD_RTC_DATETIME
] = osdElementRtcTime
,
1625 #ifdef USE_OSD_ADJUSTMENTS
1626 [OSD_ADJUSTMENT_RANGE
] = osdElementAdjustmentRange
,
1628 #ifdef USE_ADC_INTERNAL
1629 [OSD_CORE_TEMPERATURE
] = osdElementCoreTemperature
,
1631 [OSD_ANTI_GRAVITY
] = osdElementAntiGravity
,
1633 [OSD_G_FORCE
] = osdElementGForce
,
1635 [OSD_MOTOR_DIAG
] = osdElementMotorDiagnostics
,
1637 [OSD_LOG_STATUS
] = osdElementLogStatus
,
1640 [OSD_FLIP_ARROW
] = osdElementCrashFlipArrow
,
1642 #ifdef USE_RX_LINK_QUALITY_INFO
1643 [OSD_LINK_QUALITY
] = osdElementLinkQuality
,
1645 #ifdef USE_RX_LINK_UPLINK_POWER
1646 [OSD_TX_UPLINK_POWER
] = osdElementTxUplinkPower
,
1649 [OSD_FLIGHT_DIST
] = osdElementGpsFlightDistance
,
1651 #ifdef USE_OSD_STICK_OVERLAY
1652 [OSD_STICK_OVERLAY_LEFT
] = osdElementStickOverlay
,
1653 [OSD_STICK_OVERLAY_RIGHT
] = osdElementStickOverlay
,
1655 [OSD_DISPLAY_NAME
] = NULL
, // only has background
1656 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1657 [OSD_ESC_RPM_FREQ
] = osdElementEscRpmFreq
,
1659 #ifdef USE_PROFILE_NAMES
1660 [OSD_RATE_PROFILE_NAME
] = osdElementRateProfileName
,
1661 [OSD_PID_PROFILE_NAME
] = osdElementPidProfileName
,
1663 #ifdef USE_OSD_PROFILES
1664 [OSD_PROFILE_NAME
] = osdElementOsdProfileName
,
1666 #ifdef USE_RX_RSSI_DBM
1667 [OSD_RSSI_DBM_VALUE
] = osdElementRssiDbm
,
1669 [OSD_RC_CHANNELS
] = osdElementRcChannels
,
1671 [OSD_EFFICIENCY
] = osdElementEfficiency
,
1673 #ifdef USE_PERSISTENT_STATS
1674 [OSD_TOTAL_FLIGHTS
] = osdElementTotalFlights
,
1678 // Define the mapping between the OSD element id and the function to draw its background (static part)
1679 // Only necessary to define the entries that actually have a background function
1681 const osdElementDrawFn osdElementBackgroundFunction
[OSD_ITEM_COUNT
] = {
1682 [OSD_CAMERA_FRAME
] = osdBackgroundCameraFrame
,
1683 [OSD_HORIZON_SIDEBARS
] = osdBackgroundHorizonSidebars
,
1684 [OSD_CRAFT_NAME
] = osdBackgroundCraftName
,
1685 #ifdef USE_OSD_STICK_OVERLAY
1686 [OSD_STICK_OVERLAY_LEFT
] = osdBackgroundStickOverlay
,
1687 [OSD_STICK_OVERLAY_RIGHT
] = osdBackgroundStickOverlay
,
1689 [OSD_DISPLAY_NAME
] = osdBackgroundDisplayName
,
1692 static void osdAddActiveElement(osd_items_e element
)
1694 if (VISIBLE(osdElementConfig()->item_pos
[element
])) {
1695 activeOsdElementArray
[activeOsdElementCount
++] = element
;
1699 // Examine the elements and build a list of only the active (enabled)
1700 // ones to speed up rendering.
1702 void osdAddActiveElements(void)
1704 activeOsdElementCount
= 0;
1707 if (sensors(SENSOR_ACC
)) {
1708 osdAddActiveElement(OSD_ARTIFICIAL_HORIZON
);
1709 osdAddActiveElement(OSD_G_FORCE
);
1710 osdAddActiveElement(OSD_UP_DOWN_REFERENCE
);
1714 for (unsigned i
= 0; i
< sizeof(osdElementDisplayOrder
); i
++) {
1715 osdAddActiveElement(osdElementDisplayOrder
[i
]);
1719 if (sensors(SENSOR_GPS
)) {
1720 osdAddActiveElement(OSD_GPS_SATS
);
1721 osdAddActiveElement(OSD_GPS_SPEED
);
1722 osdAddActiveElement(OSD_GPS_LAT
);
1723 osdAddActiveElement(OSD_GPS_LON
);
1724 osdAddActiveElement(OSD_HOME_DIST
);
1725 osdAddActiveElement(OSD_HOME_DIR
);
1726 osdAddActiveElement(OSD_FLIGHT_DIST
);
1727 osdAddActiveElement(OSD_EFFICIENCY
);
1730 #ifdef USE_ESC_SENSOR
1731 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
1732 osdAddActiveElement(OSD_ESC_TMP
);
1736 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1737 if ((featureIsEnabled(FEATURE_ESC_SENSOR
)) || (motorConfig()->dev
.useDshotTelemetry
)) {
1738 osdAddActiveElement(OSD_ESC_RPM
);
1739 osdAddActiveElement(OSD_ESC_RPM_FREQ
);
1743 #ifdef USE_PERSISTENT_STATS
1744 osdAddActiveElement(OSD_TOTAL_FLIGHTS
);
1748 static void osdDrawSingleElement(displayPort_t
*osdDisplayPort
, uint8_t item
)
1750 if (!osdElementDrawFunction
[item
]) {
1751 // Element has no drawing function
1754 if (!osdDisplayPort
->useDeviceBlink
&& BLINK(item
)) {
1758 uint8_t elemPosX
= OSD_X(osdElementConfig()->item_pos
[item
]);
1759 uint8_t elemPosY
= OSD_Y(osdElementConfig()->item_pos
[item
]);
1760 char buff
[OSD_ELEMENT_BUFFER_LENGTH
] = "";
1762 osdElementParms_t element
;
1763 element
.item
= item
;
1764 element
.elemPosX
= elemPosX
;
1765 element
.elemPosY
= elemPosY
;
1766 element
.type
= OSD_TYPE(osdElementConfig()->item_pos
[item
]);
1767 element
.buff
= (char *)&buff
;
1768 element
.osdDisplayPort
= osdDisplayPort
;
1769 element
.drawElement
= true;
1770 element
.attr
= DISPLAYPORT_ATTR_NONE
;
1772 // Call the element drawing function
1773 osdElementDrawFunction
[item
](&element
);
1774 if (element
.drawElement
) {
1775 osdDisplayWrite(&element
, elemPosX
, elemPosY
, element
.attr
, buff
);
1779 static void osdDrawSingleElementBackground(displayPort_t
*osdDisplayPort
, uint8_t item
)
1781 if (!osdElementBackgroundFunction
[item
]) {
1782 // Element has no background drawing function
1786 uint8_t elemPosX
= OSD_X(osdElementConfig()->item_pos
[item
]);
1787 uint8_t elemPosY
= OSD_Y(osdElementConfig()->item_pos
[item
]);
1788 char buff
[OSD_ELEMENT_BUFFER_LENGTH
] = "";
1790 osdElementParms_t element
;
1791 element
.item
= item
;
1792 element
.elemPosX
= elemPosX
;
1793 element
.elemPosY
= elemPosY
;
1794 element
.type
= OSD_TYPE(osdElementConfig()->item_pos
[item
]);
1795 element
.buff
= (char *)&buff
;
1796 element
.osdDisplayPort
= osdDisplayPort
;
1797 element
.drawElement
= true;
1799 // Call the element background drawing function
1800 osdElementBackgroundFunction
[item
](&element
);
1801 if (element
.drawElement
) {
1802 osdDisplayWrite(&element
, elemPosX
, elemPosY
, DISPLAYPORT_ATTR_NONE
, buff
);
1806 #define OSD_BLINK_FREQUENCY_HZ 2.5f
1808 void osdDrawActiveElements(displayPort_t
*osdDisplayPort
)
1810 static unsigned blinkLoopCounter
= 0;
1813 static bool lastGpsSensorState
;
1814 // Handle the case that the GPS_SENSOR may be delayed in activation
1815 // or deactivate if communication is lost with the module.
1816 const bool currentGpsSensorState
= sensors(SENSOR_GPS
);
1817 if (lastGpsSensorState
!= currentGpsSensorState
) {
1818 lastGpsSensorState
= currentGpsSensorState
;
1819 osdAnalyzeActiveElements();
1823 // synchronize the blinking with the OSD task loop
1824 if (++blinkLoopCounter
>= lrintf(osdConfig()->task_frequency
/ OSD_DRAW_FREQ_DENOM
/ (OSD_BLINK_FREQUENCY_HZ
* 2))) {
1825 blinkState
= !blinkState
;
1826 blinkLoopCounter
= 0;
1829 for (unsigned i
= 0; i
< activeOsdElementCount
; i
++) {
1830 if (!backgroundLayerSupported
) {
1831 // If the background layer isn't supported then we
1832 // have to draw the element's static layer as well.
1833 osdDrawSingleElementBackground(osdDisplayPort
, activeOsdElementArray
[i
]);
1835 osdDrawSingleElement(osdDisplayPort
, activeOsdElementArray
[i
]);
1839 void osdDrawActiveElementsBackground(displayPort_t
*osdDisplayPort
)
1841 if (backgroundLayerSupported
) {
1842 displayLayerSelect(osdDisplayPort
, DISPLAYPORT_LAYER_BACKGROUND
);
1843 displayClearScreen(osdDisplayPort
);
1844 for (unsigned i
= 0; i
< activeOsdElementCount
; i
++) {
1845 osdDrawSingleElementBackground(osdDisplayPort
, activeOsdElementArray
[i
]);
1847 displayLayerSelect(osdDisplayPort
, DISPLAYPORT_LAYER_FOREGROUND
);
1851 void osdElementsInit(bool backgroundLayerFlag
)
1853 backgroundLayerSupported
= backgroundLayerFlag
;
1854 activeOsdElementCount
= 0;
1857 void osdResetAlarms(void)
1859 memset(blinkBits
, 0, sizeof(blinkBits
));
1862 void osdUpdateAlarms(void)
1864 // This is overdone?
1866 int32_t alt
= osdGetMetersToSelectedUnit(getEstimatedAltitudeCm()) / 100;
1868 if (getRssiPercent() < osdConfig()->rssi_alarm
) {
1869 SET_BLINK(OSD_RSSI_VALUE
);
1871 CLR_BLINK(OSD_RSSI_VALUE
);
1874 #ifdef USE_RX_LINK_QUALITY_INFO
1875 if (rxGetLinkQualityPercent() < osdConfig()->link_quality_alarm
) {
1876 SET_BLINK(OSD_LINK_QUALITY
);
1878 CLR_BLINK(OSD_LINK_QUALITY
);
1880 #endif // USE_RX_LINK_QUALITY_INFO
1882 if (getBatteryState() == BATTERY_OK
) {
1883 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1884 CLR_BLINK(OSD_AVG_CELL_VOLTAGE
);
1886 SET_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1887 SET_BLINK(OSD_AVG_CELL_VOLTAGE
);
1891 if ((STATE(GPS_FIX
) == 0) || (gpsSol
.numSat
< 5)
1892 #ifdef USE_GPS_RESCUE
1893 || ((gpsSol
.numSat
< gpsRescueConfig()->minSats
) && gpsRescueIsConfigured())
1896 SET_BLINK(OSD_GPS_SATS
);
1898 CLR_BLINK(OSD_GPS_SATS
);
1902 for (int i
= 0; i
< OSD_TIMER_COUNT
; i
++) {
1903 const uint16_t timer
= osdConfig()->timers
[i
];
1904 const timeUs_t time
= osdGetTimerValue(OSD_TIMER_SRC(timer
));
1905 const timeUs_t alarmTime
= OSD_TIMER_ALARM(timer
) * 60000000; // convert from minutes to us
1906 if (alarmTime
!= 0 && time
>= alarmTime
) {
1907 SET_BLINK(OSD_ITEM_TIMER_1
+ i
);
1909 CLR_BLINK(OSD_ITEM_TIMER_1
+ i
);
1913 if (getMAhDrawn() >= osdConfig()->cap_alarm
) {
1914 SET_BLINK(OSD_MAH_DRAWN
);
1915 SET_BLINK(OSD_MAIN_BATT_USAGE
);
1916 SET_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1918 CLR_BLINK(OSD_MAH_DRAWN
);
1919 CLR_BLINK(OSD_MAIN_BATT_USAGE
);
1920 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1923 if ((alt
>= osdConfig()->alt_alarm
) && ARMING_FLAG(ARMED
)) {
1924 SET_BLINK(OSD_ALTITUDE
);
1926 CLR_BLINK(OSD_ALTITUDE
);
1930 if (sensors(SENSOR_GPS
) && ARMING_FLAG(ARMED
) && STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1931 if (osdConfig()->distance_alarm
&& GPS_distanceToHome
>= osdConfig()->distance_alarm
) {
1932 SET_BLINK(OSD_HOME_DIST
);
1934 CLR_BLINK(OSD_HOME_DIST
);
1937 CLR_BLINK(OSD_HOME_DIST
);;
1941 #ifdef USE_ESC_SENSOR
1942 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
1943 // This works because the combined ESC data contains the maximum temperature seen amongst all ESCs
1944 if (osdConfig()->esc_temp_alarm
!= ESC_TEMP_ALARM_OFF
&& osdEscDataCombined
->temperature
>= osdConfig()->esc_temp_alarm
) {
1945 SET_BLINK(OSD_ESC_TMP
);
1947 CLR_BLINK(OSD_ESC_TMP
);
1954 static bool osdElementIsActive(osd_items_e element
)
1956 for (unsigned i
= 0; i
< activeOsdElementCount
; i
++) {
1957 if (activeOsdElementArray
[i
] == element
) {
1964 // Determine if any active elements need the ACC
1965 bool osdElementsNeedAccelerometer(void)
1967 return osdElementIsActive(OSD_ARTIFICIAL_HORIZON
) ||
1968 osdElementIsActive(OSD_PITCH_ANGLE
) ||
1969 osdElementIsActive(OSD_ROLL_ANGLE
) ||
1970 osdElementIsActive(OSD_G_FORCE
) ||
1971 osdElementIsActive(OSD_FLIP_ARROW
) ||
1972 osdElementIsActive(OSD_UP_DOWN_REFERENCE
);