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"
124 #include "common/filter.h"
126 #include "config/config.h"
127 #include "config/feature.h"
129 #include "drivers/display.h"
130 #include "drivers/dshot.h"
131 #include "drivers/osd_symbols.h"
132 #include "drivers/time.h"
133 #include "drivers/vtx_common.h"
135 #include "fc/controlrate_profile.h"
137 #include "fc/rc_adjustments.h"
138 #include "fc/rc_controls.h"
139 #include "fc/runtime_config.h"
141 #include "flight/gps_rescue.h"
142 #include "flight/position.h"
143 #include "flight/imu.h"
144 #include "flight/mixer.h"
145 #include "flight/pid.h"
151 #include "osd/osd_elements.h"
152 #include "osd/osd_warnings.h"
154 #include "pg/motor.h"
155 #include "pg/stats.h"
159 #include "sensors/adcinternal.h"
160 #include "sensors/barometer.h"
161 #include "sensors/battery.h"
162 #include "sensors/esc_sensor.h"
163 #include "sensors/sensors.h"
165 #ifdef USE_GPS_PLUS_CODES
166 // located in lib/main/google/olc
170 #define AH_SYMBOL_COUNT 9
171 #define AH_SIDEBAR_WIDTH_POS 7
172 #define AH_SIDEBAR_HEIGHT_POS 3
174 // Stick overlay size
175 #define OSD_STICK_OVERLAY_WIDTH 7
176 #define OSD_STICK_OVERLAY_HEIGHT 5
177 #define OSD_STICK_OVERLAY_SPRITE_HEIGHT 3
178 #define OSD_STICK_OVERLAY_VERTICAL_POSITIONS (OSD_STICK_OVERLAY_HEIGHT * OSD_STICK_OVERLAY_SPRITE_HEIGHT)
180 #define FULL_CIRCLE 360
181 #define EFFICIENCY_MINIMUM_SPEED_CM_S 100
182 #define EFFICIENCY_CUTOFF_HZ 0.5f
184 static pt1Filter_t batteryEfficiencyFilt
;
186 #define MOTOR_STOPPED_THRESHOLD_RPM 1000
188 #define SINE_25_DEG 0.422618261740699f
190 #ifdef USE_OSD_STICK_OVERLAY
191 typedef struct radioControls_s
{
192 uint8_t left_vertical
;
193 uint8_t left_horizontal
;
194 uint8_t right_vertical
;
195 uint8_t right_horizontal
;
198 static const radioControls_t radioModes
[4] = {
199 { PITCH
, YAW
, THROTTLE
, ROLL
}, // Mode 1
200 { THROTTLE
, YAW
, PITCH
, ROLL
}, // Mode 2
201 { PITCH
, ROLL
, THROTTLE
, YAW
}, // Mode 3
202 { THROTTLE
, ROLL
, PITCH
, YAW
}, // Mode 4
206 static const char compassBar
[] = {
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
,
216 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
218 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
221 static unsigned activeOsdElementCount
= 0;
222 static uint8_t activeOsdElementArray
[OSD_ITEM_COUNT
];
223 static bool backgroundLayerSupported
= false;
226 #define OSD_BLINK_FREQUENCY_HZ 2
227 static bool blinkState
= true;
228 static uint32_t blinkBits
[(OSD_ITEM_COUNT
+ 31) / 32];
229 #define SET_BLINK(item) (blinkBits[(item) / 32] |= (1 << ((item) % 32)))
230 #define CLR_BLINK(item) (blinkBits[(item) / 32] &= ~(1 << ((item) % 32)))
231 #define IS_BLINK(item) (blinkBits[(item) / 32] & (1 << ((item) % 32)))
232 #define BLINK(item) (IS_BLINK(item) && blinkState)
236 static int osdDisplayWrite(osdElementParms_t
*element
, uint8_t x
, uint8_t y
, uint8_t attr
, const char *s
)
238 if (IS_BLINK(element
->item
)) {
239 attr
|= DISPLAYPORT_ATTR_BLINK
;
242 return displayWrite(element
->osdDisplayPort
, x
, y
, attr
, s
);
245 static int osdDisplayWriteChar(osdElementParms_t
*element
, uint8_t x
, uint8_t y
, uint8_t attr
, char c
)
252 return osdDisplayWrite(element
, x
, y
, attr
, buf
);
255 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
256 typedef int (*getEscRpmOrFreqFnPtr
)(int i
);
258 static int getEscRpm(int i
)
260 #ifdef USE_DSHOT_TELEMETRY
261 if (motorConfig()->dev
.useDshotTelemetry
) {
262 return 100.0f
/ (motorConfig()->motorPoleCount
/ 2.0f
) * getDshotTelemetry(i
);
265 #ifdef USE_ESC_SENSOR
266 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
267 return calcEscRpm(getEscSensorData(i
)->rpm
);
273 static int getEscRpmFreq(int i
)
275 return getEscRpm(i
) / 60;
278 static void renderOsdEscRpmOrFreq(getEscRpmOrFreqFnPtr escFnPtr
, osdElementParms_t
*element
)
280 int x
= element
->elemPosX
;
281 int y
= element
->elemPosY
;
282 for (int i
=0; i
< getMotorCount(); i
++) {
284 const int rpm
= MIN((*escFnPtr
)(i
),99999);
285 const int len
= tfp_sprintf(rpmStr
, "%d", rpm
);
287 osdDisplayWrite(element
, x
, y
+ i
, DISPLAYPORT_ATTR_NONE
, rpmStr
);
289 element
->drawElement
= false;
293 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
294 int osdConvertTemperatureToSelectedUnit(int tempInDegreesCelcius
)
296 switch (osdConfig()->units
) {
298 return lrintf(((tempInDegreesCelcius
* 9.0f
) / 5) + 32);
300 return tempInDegreesCelcius
;
305 static void osdFormatAltitudeString(char * buff
, int32_t altitudeCm
, osdElementType_e variantType
)
307 const char unitSymbol
= osdGetMetersToSelectedUnitSymbol();
308 unsigned decimalPlaces
;
310 switch (variantType
) {
311 case OSD_ELEMENT_TYPE_2
: // whole number altitude (no decimal places)
314 case OSD_ELEMENT_TYPE_1
: // one decimal place (default)
319 osdPrintFloat(buff
, SYM_ALTITUDE
, osdGetMetersToSelectedUnit(altitudeCm
) / 100.0f
, "", decimalPlaces
, true, unitSymbol
);
323 static void osdFormatCoordinate(char *buff
, gpsCoordinateType_e coordinateType
, osdElementType_e variantType
)
325 int32_t gpsValue
= 0;
326 const char leadingSymbol
= (coordinateType
== GPS_LONGITUDE
) ? SYM_LON
: SYM_LAT
;
328 if (STATE(GPS_FIX_EVER
)) { // don't display interim coordinates until we get the first position fix
329 gpsValue
= (coordinateType
== GPS_LONGITUDE
) ? gpsSol
.llh
.lon
: gpsSol
.llh
.lat
;
332 const int degreesPart
= ABS(gpsValue
) / GPS_DEGREES_DIVIDER
;
333 int fractionalPart
= ABS(gpsValue
) % GPS_DEGREES_DIVIDER
;
335 switch (variantType
) {
336 #ifdef USE_GPS_PLUS_CODES
337 #define PLUS_CODE_DIGITS 11
338 case OSD_ELEMENT_TYPE_4
: // Open Location Code
342 if (STATE(GPS_FIX_EVER
)) {
344 location
.lat
= (double)gpsSol
.llh
.lat
/ GPS_DEGREES_DIVIDER
;
345 location
.lon
= (double)gpsSol
.llh
.lon
/ GPS_DEGREES_DIVIDER
;
346 OLC_Encode(&location
, PLUS_CODE_DIGITS
, buff
, OSD_ELEMENT_BUFFER_LENGTH
- 3);
348 memset(buff
, SYM_HYPHEN
, PLUS_CODE_DIGITS
+ 1);
350 buff
[PLUS_CODE_DIGITS
+ 1] = '\0';
354 #endif // USE_GPS_PLUS_CODES
356 case OSD_ELEMENT_TYPE_3
: // degree, minutes, seconds style. ddd^mm'ss.00"W
359 *buff
++ = leadingSymbol
;
361 const int minutes
= fractionalPart
* 60 / GPS_DEGREES_DIVIDER
;
362 const int fractionalMinutes
= fractionalPart
* 60 % GPS_DEGREES_DIVIDER
;
363 const int seconds
= fractionalMinutes
* 60 / GPS_DEGREES_DIVIDER
;
364 const int tenthSeconds
= (fractionalMinutes
* 60 % GPS_DEGREES_DIVIDER
) * 10 / GPS_DEGREES_DIVIDER
;
366 if (coordinateType
== GPS_LONGITUDE
) {
367 trailingSymbol
= (gpsValue
< 0) ? 'W' : 'E';
369 trailingSymbol
= (gpsValue
< 0) ? 'S' : 'N';
371 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
);
375 case OSD_ELEMENT_TYPE_2
:
376 fractionalPart
/= 1000;
379 case OSD_ELEMENT_TYPE_1
:
381 *buff
++ = leadingSymbol
;
383 *buff
++ = SYM_HYPHEN
;
385 tfp_sprintf(buff
, (variantType
== OSD_ELEMENT_TYPE_1
? "%u.%07u" : "%u.%04u"), degreesPart
, fractionalPart
);
391 void osdFormatDistanceString(char *ptr
, int distance
, char leadingSymbol
)
393 const float convertedDistance
= osdGetMetersToSelectedUnit(distance
);
395 char unitSymbolExtended
;
398 switch (osdConfig()->units
) {
400 unitTransition
= 5280;
402 unitSymbolExtended
= SYM_MILES
;
405 unitTransition
= 1000;
407 unitSymbolExtended
= SYM_KM
;
411 unsigned decimalPlaces
;
412 float displayDistance
;
414 if (convertedDistance
< unitTransition
) {
416 displayDistance
= convertedDistance
;
417 displaySymbol
= unitSymbol
;
419 displayDistance
= convertedDistance
/ unitTransition
;
420 displaySymbol
= unitSymbolExtended
;
421 if (displayDistance
>= 10) { // >= 10 miles or km - 1 decimal place
423 } else { // < 10 miles or km - 2 decimal places
427 osdPrintFloat(ptr
, leadingSymbol
, displayDistance
, "", decimalPlaces
, false, displaySymbol
);
430 static void osdFormatPID(char * buff
, const char * label
, const pidf_t
* pid
)
432 tfp_sprintf(buff
, "%s %3d %3d %3d %3d", label
, pid
->P
, pid
->I
, pid
->D
, pid
->F
);
436 bool osdFormatRtcDateTime(char *buffer
)
439 if (!rtcGetDateTime(&dateTime
)) {
445 dateTimeFormatLocalShort(buffer
, &dateTime
);
451 void osdFormatTime(char * buff
, osd_timer_precision_e precision
, timeUs_t time
)
453 int seconds
= time
/ 1000000;
454 const int minutes
= seconds
/ 60;
455 seconds
= seconds
% 60;
458 case OSD_TIMER_PREC_SECOND
:
460 tfp_sprintf(buff
, "%02d:%02d", minutes
, seconds
);
462 case OSD_TIMER_PREC_HUNDREDTHS
:
464 const int hundredths
= (time
/ 10000) % 100;
465 tfp_sprintf(buff
, "%02d:%02d.%02d", minutes
, seconds
, hundredths
);
468 case OSD_TIMER_PREC_TENTHS
:
470 const int tenths
= (time
/ 100000) % 10;
471 tfp_sprintf(buff
, "%02d:%02d.%01d", minutes
, seconds
, tenths
);
477 static char osdGetTimerSymbol(osd_timer_source_e src
)
480 case OSD_TIMER_SRC_ON
:
482 case OSD_TIMER_SRC_TOTAL_ARMED
:
483 case OSD_TIMER_SRC_LAST_ARMED
:
485 case OSD_TIMER_SRC_ON_OR_ARMED
:
486 return ARMING_FLAG(ARMED
) ? SYM_FLY_M
: SYM_ON_M
;
492 static timeUs_t
osdGetTimerValue(osd_timer_source_e src
)
495 case OSD_TIMER_SRC_ON
:
497 case OSD_TIMER_SRC_TOTAL_ARMED
:
499 case OSD_TIMER_SRC_LAST_ARMED
: {
500 statistic_t
*stats
= osdGetStats();
501 return stats
->armed_time
;
503 case OSD_TIMER_SRC_ON_OR_ARMED
:
504 return ARMING_FLAG(ARMED
) ? osdFlyTime
: micros();
510 void osdFormatTimer(char *buff
, bool showSymbol
, bool usePrecision
, int timerIndex
)
512 const uint16_t timer
= osdConfig()->timers
[timerIndex
];
513 const uint8_t src
= OSD_TIMER_SRC(timer
);
516 *(buff
++) = osdGetTimerSymbol(src
);
519 osdFormatTime(buff
, (usePrecision
? OSD_TIMER_PRECISION(timer
) : OSD_TIMER_PREC_SECOND
), osdGetTimerValue(src
));
522 static char osdGetBatterySymbol(int cellVoltage
)
524 if (getBatteryState() == BATTERY_CRITICAL
) {
525 return SYM_MAIN_BATT
; // FIXME: currently the BAT- symbol, ideally replace with a battery with exclamation mark
527 // Calculate a symbol offset using cell voltage over full cell voltage range
528 const int symOffset
= scaleRange(cellVoltage
, batteryConfig()->vbatmincellvoltage
, batteryConfig()->vbatmaxcellvoltage
, 0, 8);
529 return SYM_BATT_EMPTY
- constrain(symOffset
, 0, 6);
533 static uint8_t osdGetHeadingIntoDiscreteDirections(int heading
, unsigned directions
)
535 heading
+= FULL_CIRCLE
; // Ensure positive value
537 // Split input heading 0..359 into sectors 0..(directions-1), but offset
538 // by half a sector so that sector 0 gets centered around heading 0.
539 // We multiply heading by directions to not loose precision in divisions
540 // In this way each segment will be a FULL_CIRCLE length
541 int direction
= (heading
* directions
+ FULL_CIRCLE
/ 2) / FULL_CIRCLE
; // scale with rounding
542 direction
%= directions
; // normalize
544 return direction
; // return segment number
547 static uint8_t osdGetDirectionSymbolFromHeading(int heading
)
549 heading
= osdGetHeadingIntoDiscreteDirections(heading
, 16);
551 // Now heading has a heading with Up=0, Right=4, Down=8 and Left=12
552 // Our symbols are Down=0, Right=4, Up=8 and Left=12
553 // There're 16 arrow symbols. Transform it.
554 heading
= 16 - heading
;
555 heading
= (heading
+ 8) % 16;
557 return SYM_ARROW_SOUTH
+ heading
;
562 * Converts altitude based on the current unit system.
563 * @param meters Value in meters to convert
565 float osdGetMetersToSelectedUnit(int32_t meters
)
567 switch (osdConfig()->units
) {
569 return meters
* 3.28084f
; // Convert to feet
571 return meters
; // Already in meters
576 * Gets the correct altitude symbol for the current unit system
578 char osdGetMetersToSelectedUnitSymbol(void)
580 switch (osdConfig()->units
) {
589 * Converts speed based on the current unit system.
590 * @param value in cm/s to convert
592 int32_t osdGetSpeedToSelectedUnit(int32_t value
)
594 switch (osdConfig()->units
) {
597 return CM_S_TO_MPH(value
);
599 return CM_S_TO_KM_H(value
);
604 * Gets the correct speed symbol for the current unit system
606 char osdGetSpeedToSelectedUnitSymbol(void)
608 switch (osdConfig()->units
) {
617 char osdGetVarioToSelectedUnitSymbol(void)
619 switch (osdConfig()->units
) {
627 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
628 char osdGetTemperatureSymbolForSelectedUnit(void)
630 switch (osdConfig()->units
) {
639 // *************************
640 // Element drawing functions
641 // *************************
643 #ifdef USE_OSD_ADJUSTMENTS
644 static void osdElementAdjustmentRange(osdElementParms_t
*element
)
646 const char *name
= getAdjustmentsRangeName();
648 tfp_sprintf(element
->buff
, "%s: %3d", name
, getAdjustmentsRangeValue());
651 #endif // USE_OSD_ADJUSTMENTS
653 static void osdElementAltitude(osdElementParms_t
*element
)
655 bool haveBaro
= false;
656 bool haveGps
= false;
658 haveBaro
= sensors(SENSOR_BARO
);
661 haveGps
= sensors(SENSOR_GPS
) && STATE(GPS_FIX
);
663 if (haveBaro
|| haveGps
) {
664 osdFormatAltitudeString(element
->buff
, getEstimatedAltitudeCm(), element
->type
);
666 element
->buff
[0] = SYM_ALTITUDE
;
667 element
->buff
[1] = SYM_HYPHEN
; // We use this symbol when we don't have a valid measure
668 element
->buff
[2] = '\0';
673 static void osdElementAngleRollPitch(osdElementParms_t
*element
)
675 const float angle
= ((element
->item
== OSD_PITCH_ANGLE
) ? attitude
.values
.pitch
: attitude
.values
.roll
) / 10.0f
;
676 osdPrintFloat(element
->buff
, (element
->item
== OSD_PITCH_ANGLE
) ? SYM_PITCH
: SYM_ROLL
, fabsf(angle
), ((angle
< 0) ? "-%02u" : " %02u"), 1, true, SYM_NONE
);
680 static void osdElementAntiGravity(osdElementParms_t
*element
)
682 if (pidOsdAntiGravityActive()) {
683 strcpy(element
->buff
, "AG");
689 static void osdElementArtificialHorizon(osdElementParms_t
*element
)
691 // Get pitch and roll limits in tenths of degrees
692 const int maxPitch
= osdConfig()->ahMaxPitch
* 10;
693 const int maxRoll
= osdConfig()->ahMaxRoll
* 10;
694 const int ahSign
= osdConfig()->ahInvert
? -1 : 1;
695 const int rollAngle
= constrain(attitude
.values
.roll
* ahSign
, -maxRoll
, maxRoll
);
696 int pitchAngle
= constrain(attitude
.values
.pitch
* ahSign
, -maxPitch
, maxPitch
);
697 // Convert pitchAngle to y compensation value
698 // (maxPitch / 25) divisor matches previous settings of fixed divisor of 8 and fixed max AHI pitch angle of 20.0 degrees
700 pitchAngle
= ((pitchAngle
* 25) / maxPitch
);
702 pitchAngle
-= 41; // 41 = 4 * AH_SYMBOL_COUNT + 5
704 for (int x
= -4; x
<= 4; x
++) {
705 const int y
= ((-rollAngle
* x
) / 64) - pitchAngle
;
706 if (y
>= 0 && y
<= 81) {
707 osdDisplayWriteChar(element
, element
->elemPosX
+ x
, element
->elemPosY
+ (y
/ AH_SYMBOL_COUNT
), DISPLAYPORT_ATTR_NONE
, (SYM_AH_BAR9_0
+ (y
% AH_SYMBOL_COUNT
)));
711 element
->drawElement
= false; // element already drawn
714 static void osdElementUpDownReference(osdElementParms_t
*element
)
716 // Up/Down reference feature displays reference points on the OSD at Zenith and Nadir
717 const float earthUpinBodyFrame
[3] = {-rMat
[2][0], -rMat
[2][1], -rMat
[2][2]}; //transforum the up vector to the body frame
719 if (ABS(earthUpinBodyFrame
[2]) < SINE_25_DEG
&& ABS(earthUpinBodyFrame
[1]) < SINE_25_DEG
) {
720 float thetaB
; // pitch from body frame to zenith/nadir
721 float psiB
; // psi from body frame to zenith/nadir
722 char *symbol
[2] = {"U", "D"}; // character buffer
725 if(attitude
.values
.pitch
>0.0){ //nose down
726 thetaB
= -earthUpinBodyFrame
[2]; // get pitch w/re to nadir (use small angle approx for sine)
727 psiB
= -earthUpinBodyFrame
[1]; // calculate the yaw w/re to nadir (use small angle approx for sine)
730 thetaB
= earthUpinBodyFrame
[2]; // get pitch w/re to zenith (use small angle approx for sine)
731 psiB
= earthUpinBodyFrame
[1]; // calculate the yaw w/re to zenith (use small angle approx for sine)
734 int posX
= element
->elemPosX
+ lrintf(scaleRangef(psiB
, -M_PIf
/ 4, M_PIf
/ 4, -14, 14));
735 int posY
= element
->elemPosY
+ lrintf(scaleRangef(thetaB
, -M_PIf
/ 4, M_PIf
/ 4, -8, 8));
737 osdDisplayWrite(element
, posX
, posY
, DISPLAYPORT_ATTR_NONE
, symbol
[direction
]);
739 element
->drawElement
= false; // element already drawn
743 static void osdElementAverageCellVoltage(osdElementParms_t
*element
)
745 const int cellV
= getBatteryAverageCellVoltage();
746 osdPrintFloat(element
->buff
, osdGetBatterySymbol(cellV
), cellV
/ 100.0f
, "", 2, false, SYM_VOLT
);
749 static void osdElementCompassBar(osdElementParms_t
*element
)
751 memcpy(element
->buff
, compassBar
+ osdGetHeadingIntoDiscreteDirections(DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
), 16), 9);
752 element
->buff
[9] = 0;
755 #ifdef USE_ADC_INTERNAL
756 static void osdElementCoreTemperature(osdElementParms_t
*element
)
758 tfp_sprintf(element
->buff
, "C%c%3d%c", SYM_TEMPERATURE
, osdConvertTemperatureToSelectedUnit(getCoreTemperatureCelsius()), osdGetTemperatureSymbolForSelectedUnit());
760 #endif // USE_ADC_INTERNAL
762 static void osdBackgroundCameraFrame(osdElementParms_t
*element
)
764 const uint8_t xpos
= element
->elemPosX
;
765 const uint8_t ypos
= element
->elemPosY
;
766 const uint8_t width
= constrain(osdConfig()->camera_frame_width
, OSD_CAMERA_FRAME_MIN_WIDTH
, OSD_CAMERA_FRAME_MAX_WIDTH
);
767 const uint8_t height
= constrain(osdConfig()->camera_frame_height
, OSD_CAMERA_FRAME_MIN_HEIGHT
, OSD_CAMERA_FRAME_MAX_HEIGHT
);
769 element
->buff
[0] = SYM_STICK_OVERLAY_CENTER
;
770 for (int i
= 1; i
< (width
- 1); i
++) {
771 element
->buff
[i
] = SYM_STICK_OVERLAY_HORIZONTAL
;
773 element
->buff
[width
- 1] = SYM_STICK_OVERLAY_CENTER
;
774 element
->buff
[width
] = 0; // string terminator
776 osdDisplayWrite(element
, xpos
, ypos
, DISPLAYPORT_ATTR_NONE
, element
->buff
);
777 for (int i
= 1; i
< (height
- 1); i
++) {
778 osdDisplayWriteChar(element
, xpos
, ypos
+ i
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_VERTICAL
);
779 osdDisplayWriteChar(element
, xpos
+ width
- 1, ypos
+ i
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_VERTICAL
);
781 osdDisplayWrite(element
, xpos
, ypos
+ height
- 1, DISPLAYPORT_ATTR_NONE
, element
->buff
);
783 element
->drawElement
= false; // element already drawn
786 static void osdBackgroundCraftName(osdElementParms_t
*element
)
788 if (strlen(pilotConfig()->name
) == 0) {
789 strcpy(element
->buff
, "CRAFT_NAME");
792 for (i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
793 if (pilotConfig()->name
[i
]) {
794 element
->buff
[i
] = toupper((unsigned char)pilotConfig()->name
[i
]);
799 element
->buff
[i
] = '\0';
804 static void osdElementCrashFlipArrow(osdElementParms_t
*element
)
806 int rollAngle
= attitude
.values
.roll
/ 10;
807 const int pitchAngle
= attitude
.values
.pitch
/ 10;
808 if (abs(rollAngle
) > 90) {
809 rollAngle
= (rollAngle
< 0 ? -180 : 180) - rollAngle
;
812 if ((isFlipOverAfterCrashActive() || (!ARMING_FLAG(ARMED
) && !isUpright())) && !((imuConfig()->small_angle
< 180 && isUpright()) || (rollAngle
== 0 && pitchAngle
== 0))) {
813 if (abs(pitchAngle
) < 2 * abs(rollAngle
) && abs(rollAngle
) < 2 * abs(pitchAngle
)) {
814 if (pitchAngle
> 0) {
816 element
->buff
[0] = SYM_ARROW_WEST
+ 2;
818 element
->buff
[0] = SYM_ARROW_EAST
- 2;
822 element
->buff
[0] = SYM_ARROW_WEST
- 2;
824 element
->buff
[0] = SYM_ARROW_EAST
+ 2;
828 if (abs(pitchAngle
) > abs(rollAngle
)) {
829 if (pitchAngle
> 0) {
830 element
->buff
[0] = SYM_ARROW_SOUTH
;
832 element
->buff
[0] = SYM_ARROW_NORTH
;
836 element
->buff
[0] = SYM_ARROW_WEST
;
838 element
->buff
[0] = SYM_ARROW_EAST
;
842 element
->buff
[1] = '\0';
847 static void osdElementCrosshairs(osdElementParms_t
*element
)
849 element
->buff
[0] = SYM_AH_CENTER_LINE
;
850 element
->buff
[1] = SYM_AH_CENTER
;
851 element
->buff
[2] = SYM_AH_CENTER_LINE_RIGHT
;
852 element
->buff
[3] = 0;
855 static void osdElementCurrentDraw(osdElementParms_t
*element
)
857 const float amperage
= fabsf(getAmperage() / 100.0f
);
858 osdPrintFloat(element
->buff
, SYM_NONE
, amperage
, "%3u", 2, false, SYM_AMP
);
861 static void osdElementDebug(osdElementParms_t
*element
)
863 tfp_sprintf(element
->buff
, "DBG %5d %5d %5d %5d", debug
[0], debug
[1], debug
[2], debug
[3]);
866 static void osdElementDisarmed(osdElementParms_t
*element
)
868 if (!ARMING_FLAG(ARMED
)) {
869 tfp_sprintf(element
->buff
, "DISARMED");
873 static void osdBackgroundDisplayName(osdElementParms_t
*element
)
875 if (strlen(pilotConfig()->displayName
) == 0) {
876 strcpy(element
->buff
, "DISPLAY_NAME");
879 for (i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
880 if (pilotConfig()->displayName
[i
]) {
881 element
->buff
[i
] = toupper((unsigned char)pilotConfig()->displayName
[i
]);
886 element
->buff
[i
] = '\0';
890 #ifdef USE_PERSISTENT_STATS
891 static void osdElementTotalFlights(osdElementParms_t
*element
)
893 const int32_t total_flights
= statsConfig()->stats_total_flights
;
894 tfp_sprintf(element
->buff
, "#%d", total_flights
);
898 #ifdef USE_PROFILE_NAMES
899 static void osdElementRateProfileName(osdElementParms_t
*element
)
901 if (strlen(currentControlRateProfile
->profileName
) == 0) {
902 tfp_sprintf(element
->buff
, "RATE_%u", getCurrentControlRateProfileIndex() + 1);
905 for (i
= 0; i
< MAX_PROFILE_NAME_LENGTH
; i
++) {
906 if (currentControlRateProfile
->profileName
[i
]) {
907 element
->buff
[i
] = toupper((unsigned char)currentControlRateProfile
->profileName
[i
]);
912 element
->buff
[i
] = '\0';
916 static void osdElementPidProfileName(osdElementParms_t
*element
)
918 if (strlen(currentPidProfile
->profileName
) == 0) {
919 tfp_sprintf(element
->buff
, "PID_%u", getCurrentPidProfileIndex() + 1);
922 for (i
= 0; i
< MAX_PROFILE_NAME_LENGTH
; i
++) {
923 if (currentPidProfile
->profileName
[i
]) {
924 element
->buff
[i
] = toupper((unsigned char)currentPidProfile
->profileName
[i
]);
929 element
->buff
[i
] = '\0';
934 #ifdef USE_OSD_PROFILES
935 static void osdElementOsdProfileName(osdElementParms_t
*element
)
937 uint8_t profileIndex
= getCurrentOsdProfileIndex();
939 if (strlen(osdConfig()->profile
[profileIndex
- 1]) == 0) {
940 tfp_sprintf(element
->buff
, "OSD_%u", profileIndex
);
943 for (i
= 0; i
< OSD_PROFILE_NAME_LENGTH
; i
++) {
944 if (osdConfig()->profile
[profileIndex
- 1][i
]) {
945 element
->buff
[i
] = toupper((unsigned char)osdConfig()->profile
[profileIndex
- 1][i
]);
950 element
->buff
[i
] = '\0';
955 #ifdef USE_ESC_SENSOR
956 static void osdElementEscTemperature(osdElementParms_t
*element
)
958 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
959 tfp_sprintf(element
->buff
, "E%c%3d%c", SYM_TEMPERATURE
, osdConvertTemperatureToSelectedUnit(osdEscDataCombined
->temperature
), osdGetTemperatureSymbolForSelectedUnit());
962 #endif // USE_ESC_SENSOR
964 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
965 static void osdElementEscRpm(osdElementParms_t
*element
)
967 renderOsdEscRpmOrFreq(&getEscRpm
,element
);
970 static void osdElementEscRpmFreq(osdElementParms_t
*element
)
972 renderOsdEscRpmOrFreq(&getEscRpmFreq
,element
);
976 static void osdElementFlymode(osdElementParms_t
*element
)
978 // Note that flight mode display has precedence in what to display.
981 // 3. ANGLE, HORIZON, ACRO TRAINER
985 if (FLIGHT_MODE(FAILSAFE_MODE
)) {
986 strcpy(element
->buff
, "!FS!");
987 } else if (FLIGHT_MODE(GPS_RESCUE_MODE
)) {
988 strcpy(element
->buff
, "RESC");
989 } else if (FLIGHT_MODE(HEADFREE_MODE
)) {
990 strcpy(element
->buff
, "HEAD");
991 } else if (FLIGHT_MODE(ANGLE_MODE
)) {
992 strcpy(element
->buff
, "ANGL");
993 } else if (FLIGHT_MODE(HORIZON_MODE
)) {
994 strcpy(element
->buff
, "HOR ");
995 } else if (IS_RC_MODE_ACTIVE(BOXACROTRAINER
)) {
996 strcpy(element
->buff
, "ATRN");
997 } else if (airmodeIsEnabled()) {
998 strcpy(element
->buff
, "AIR ");
1000 strcpy(element
->buff
, "ACRO");
1005 static void osdElementGForce(osdElementParms_t
*element
)
1007 osdPrintFloat(element
->buff
, SYM_NONE
, osdGForce
, "", 1, true, 'G');
1012 static void osdElementGpsFlightDistance(osdElementParms_t
*element
)
1014 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1015 osdFormatDistanceString(element
->buff
, GPS_distanceFlownInCm
/ 100, SYM_TOTAL_DISTANCE
);
1017 // We use this symbol when we don't have a FIX
1018 tfp_sprintf(element
->buff
, "%c%c", SYM_TOTAL_DISTANCE
, SYM_HYPHEN
);
1022 static void osdElementGpsHomeDirection(osdElementParms_t
*element
)
1024 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1025 if (GPS_distanceToHome
> 0) {
1026 const int h
= GPS_directionToHome
- DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
1027 element
->buff
[0] = osdGetDirectionSymbolFromHeading(h
);
1029 element
->buff
[0] = SYM_OVER_HOME
;
1033 // We use this symbol when we don't have a FIX
1034 element
->buff
[0] = SYM_HYPHEN
;
1037 element
->buff
[1] = 0;
1040 static void osdElementGpsHomeDistance(osdElementParms_t
*element
)
1042 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1043 osdFormatDistanceString(element
->buff
, GPS_distanceToHome
, SYM_HOMEFLAG
);
1045 element
->buff
[0] = SYM_HOMEFLAG
;
1046 // We use this symbol when we don't have a FIX
1047 element
->buff
[1] = SYM_HYPHEN
;
1048 element
->buff
[2] = '\0';
1052 static void osdElementGpsCoordinate(osdElementParms_t
*element
)
1054 const gpsCoordinateType_e coordinateType
= (element
->item
== OSD_GPS_LON
) ? GPS_LONGITUDE
: GPS_LATITUDE
;
1055 osdFormatCoordinate(element
->buff
, coordinateType
, element
->type
);
1056 if (STATE(GPS_FIX_EVER
) && !STATE(GPS_FIX
)) {
1057 SET_BLINK(element
->item
); // blink if we had a fix but have since lost it
1059 CLR_BLINK(element
->item
);
1063 static void osdElementGpsSats(osdElementParms_t
*element
)
1065 if (!gpsIsHealthy()) {
1066 tfp_sprintf(element
->buff
, "%c%cNC", SYM_SAT_L
, SYM_SAT_R
);
1068 int pos
= tfp_sprintf(element
->buff
, "%c%c%2d", SYM_SAT_L
, SYM_SAT_R
, gpsSol
.numSat
);
1069 if (osdConfig()->gps_sats_show_hdop
) { // add on the GPS module HDOP estimate
1070 element
->buff
[pos
++] = ' ';
1071 osdPrintFloat(element
->buff
+ pos
, SYM_NONE
, gpsSol
.hdop
/ 100.0f
, "", 1, true, SYM_NONE
);
1076 static void osdElementGpsSpeed(osdElementParms_t
*element
)
1078 if (STATE(GPS_FIX
)) {
1079 tfp_sprintf(element
->buff
, "%c%3d%c", SYM_SPEED
, osdGetSpeedToSelectedUnit(gpsConfig()->gps_use_3d_speed
? gpsSol
.speed3d
: gpsSol
.groundSpeed
), osdGetSpeedToSelectedUnitSymbol());
1081 tfp_sprintf(element
->buff
, "%c%c%c", SYM_SPEED
, SYM_HYPHEN
, osdGetSpeedToSelectedUnitSymbol());
1085 static void osdElementEfficiency(osdElementParms_t
*element
)
1088 if (sensors(SENSOR_GPS
) && ARMING_FLAG(ARMED
) && STATE(GPS_FIX
) && gpsSol
.groundSpeed
>= EFFICIENCY_MINIMUM_SPEED_CM_S
) {
1089 const float speed
= (float)osdGetSpeedToSelectedUnit(gpsSol
.groundSpeed
);
1090 const float mAmperage
= (float)getAmperage() * 10.f
; // Current in mA
1091 efficiency
= lrintf(pt1FilterApply(&batteryEfficiencyFilt
, (mAmperage
/ speed
)));
1094 const char unitSymbol
= osdConfig()->units
== UNIT_IMPERIAL
? SYM_MILES
: SYM_KM
;
1095 if (efficiency
> 0 && efficiency
<= 9999) {
1096 tfp_sprintf(element
->buff
, "%4d%c/%c", efficiency
, SYM_MAH
, unitSymbol
);
1098 tfp_sprintf(element
->buff
, "----%c/%c", SYM_MAH
, unitSymbol
);
1103 static void osdBackgroundHorizonSidebars(osdElementParms_t
*element
)
1106 const int8_t hudwidth
= AH_SIDEBAR_WIDTH_POS
;
1107 const int8_t hudheight
= AH_SIDEBAR_HEIGHT_POS
;
1108 for (int y
= -hudheight
; y
<= hudheight
; y
++) {
1109 osdDisplayWriteChar(element
, element
->elemPosX
- hudwidth
, element
->elemPosY
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_AH_DECORATION
);
1110 osdDisplayWriteChar(element
, element
->elemPosX
+ hudwidth
, element
->elemPosY
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_AH_DECORATION
);
1113 // AH level indicators
1114 osdDisplayWriteChar(element
, element
->elemPosX
- hudwidth
+ 1, element
->elemPosY
, DISPLAYPORT_ATTR_NONE
, SYM_AH_LEFT
);
1115 osdDisplayWriteChar(element
, element
->elemPosX
+ hudwidth
- 1, element
->elemPosY
, DISPLAYPORT_ATTR_NONE
, SYM_AH_RIGHT
);
1117 element
->drawElement
= false; // element already drawn
1120 #ifdef USE_RX_LINK_QUALITY_INFO
1121 static void osdElementLinkQuality(osdElementParms_t
*element
)
1123 uint16_t osdLinkQuality
= 0;
1124 if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_CRSF
) { // 0-99
1125 osdLinkQuality
= rxGetLinkQuality();
1126 const uint8_t osdRfMode
= rxGetRfMode();
1127 tfp_sprintf(element
->buff
, "%c%1d:%2d", SYM_LINK_QUALITY
, osdRfMode
, osdLinkQuality
);
1128 } else if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_GHST
) { // 0-100
1129 osdLinkQuality
= rxGetLinkQuality();
1130 tfp_sprintf(element
->buff
, "%c%2d", SYM_LINK_QUALITY
, osdLinkQuality
);
1132 osdLinkQuality
= rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE
;
1133 if (osdLinkQuality
>= 10) {
1136 tfp_sprintf(element
->buff
, "%c%1d", SYM_LINK_QUALITY
, osdLinkQuality
);
1139 #endif // USE_RX_LINK_QUALITY_INFO
1141 #ifdef USE_RX_LINK_UPLINK_POWER
1142 static void osdElementTxUplinkPower(osdElementParms_t
*element
)
1144 const uint16_t osdUplinkTxPowerMw
= rxGetUplinkTxPwrMw();
1145 if (osdUplinkTxPowerMw
< 1000) {
1146 tfp_sprintf(element
->buff
, "%c%3dMW", SYM_RSSI
, osdUplinkTxPowerMw
);
1148 osdPrintFloat(element
->buff
, SYM_RSSI
, osdUplinkTxPowerMw
/ 1000.0f
, "", 1, false, 'W');
1151 #endif // USE_RX_LINK_UPLINK_POWER
1154 static void osdElementLogStatus(osdElementParms_t
*element
)
1156 if (IS_RC_MODE_ACTIVE(BOXBLACKBOX
)) {
1157 if (!isBlackboxDeviceWorking()) {
1158 tfp_sprintf(element
->buff
, "%c!", SYM_BBLOG
);
1159 } else if (isBlackboxDeviceFull()) {
1160 tfp_sprintf(element
->buff
, "%c>", SYM_BBLOG
);
1162 int32_t logNumber
= blackboxGetLogNumber();
1163 if (logNumber
>= 0) {
1164 tfp_sprintf(element
->buff
, "%c%d", SYM_BBLOG
, logNumber
);
1166 tfp_sprintf(element
->buff
, "%c", SYM_BBLOG
);
1171 #endif // USE_BLACKBOX
1173 static void osdElementMahDrawn(osdElementParms_t
*element
)
1175 tfp_sprintf(element
->buff
, "%4d%c", getMAhDrawn(), SYM_MAH
);
1178 static void osdElementMainBatteryUsage(osdElementParms_t
*element
)
1180 // Set length of indicator bar
1181 #define MAIN_BATT_USAGE_STEPS 11 // Use an odd number so the bar can be centered.
1183 const int usedCapacity
= getMAhDrawn();
1184 int displayBasis
= usedCapacity
;
1186 switch (element
->type
) {
1187 case OSD_ELEMENT_TYPE_3
: // mAh remaining percentage (counts down as battery is used)
1188 displayBasis
= constrain(batteryConfig()->batteryCapacity
- usedCapacity
, 0, batteryConfig()->batteryCapacity
);
1191 case OSD_ELEMENT_TYPE_4
: // mAh used percentage (counts up as battery is used)
1193 int displayPercent
= 0;
1194 if (batteryConfig()->batteryCapacity
) {
1195 displayPercent
= constrain(lrintf(100.0f
* displayBasis
/ batteryConfig()->batteryCapacity
), 0, 100);
1197 tfp_sprintf(element
->buff
, "%c%d%%", SYM_MAH
, displayPercent
);
1201 case OSD_ELEMENT_TYPE_2
: // mAh used graphical progress bar (grows as battery is used)
1202 displayBasis
= constrain(batteryConfig()->batteryCapacity
- usedCapacity
, 0, batteryConfig()->batteryCapacity
);
1205 case OSD_ELEMENT_TYPE_1
: // mAh remaining graphical progress bar (shrinks as battery is used)
1208 uint8_t remainingCapacityBars
= 0;
1210 if (batteryConfig()->batteryCapacity
) {
1211 const float batteryRemaining
= constrain(batteryConfig()->batteryCapacity
- displayBasis
, 0, batteryConfig()->batteryCapacity
);
1212 remainingCapacityBars
= ceilf((batteryRemaining
/ (batteryConfig()->batteryCapacity
/ MAIN_BATT_USAGE_STEPS
)));
1215 // Create empty battery indicator bar
1216 element
->buff
[0] = SYM_PB_START
;
1217 for (int i
= 1; i
<= MAIN_BATT_USAGE_STEPS
; i
++) {
1218 element
->buff
[i
] = i
<= remainingCapacityBars
? SYM_PB_FULL
: SYM_PB_EMPTY
;
1220 element
->buff
[MAIN_BATT_USAGE_STEPS
+ 1] = SYM_PB_CLOSE
;
1221 if (remainingCapacityBars
> 0 && remainingCapacityBars
< MAIN_BATT_USAGE_STEPS
) {
1222 element
->buff
[1 + remainingCapacityBars
] = SYM_PB_END
;
1224 element
->buff
[MAIN_BATT_USAGE_STEPS
+2] = '\0';
1230 static void osdElementMainBatteryVoltage(osdElementParms_t
*element
)
1232 unsigned decimalPlaces
;
1233 const float batteryVoltage
= getBatteryVoltage() / 100.0f
;
1235 if (batteryVoltage
>= 10) { // if voltage is 10v or more then display only 1 decimal place
1240 osdPrintFloat(element
->buff
, osdGetBatterySymbol(getBatteryAverageCellVoltage()), batteryVoltage
, "", decimalPlaces
, true, SYM_VOLT
);
1243 static void osdElementMotorDiagnostics(osdElementParms_t
*element
)
1246 const bool motorsRunning
= areMotorsRunning();
1247 for (; i
< getMotorCount(); i
++) {
1248 if (motorsRunning
) {
1249 element
->buff
[i
] = 0x88 - scaleRange(motor
[i
], getMotorOutputLow(), getMotorOutputHigh(), 0, 8);
1250 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
1251 if (getEscRpm(i
) < MOTOR_STOPPED_THRESHOLD_RPM
) {
1252 // Motor is not spinning properly. Mark as Stopped
1253 element
->buff
[i
] = 'S';
1257 element
->buff
[i
] = 0x88;
1260 element
->buff
[i
] = '\0';
1263 static void osdElementNumericalHeading(osdElementParms_t
*element
)
1265 const int heading
= DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
1266 tfp_sprintf(element
->buff
, "%c%03d", osdGetDirectionSymbolFromHeading(heading
), heading
);
1270 static void osdElementNumericalVario(osdElementParms_t
*element
)
1272 bool haveBaro
= false;
1273 bool haveGps
= false;
1275 haveBaro
= sensors(SENSOR_BARO
);
1278 haveGps
= sensors(SENSOR_GPS
) && STATE(GPS_FIX
);
1280 if (haveBaro
|| haveGps
) {
1281 const float verticalSpeed
= osdGetMetersToSelectedUnit(getEstimatedVario()) / 100.0f
;
1282 const char directionSymbol
= verticalSpeed
< 0 ? SYM_ARROW_SMALL_DOWN
: SYM_ARROW_SMALL_UP
;
1283 osdPrintFloat(element
->buff
, directionSymbol
, fabsf(verticalSpeed
), "", 1, true, osdGetVarioToSelectedUnitSymbol());
1285 // We use this symbol when we don't have a valid measure
1286 element
->buff
[0] = SYM_HYPHEN
;
1287 element
->buff
[1] = '\0';
1292 static void osdElementPidRateProfile(osdElementParms_t
*element
)
1294 tfp_sprintf(element
->buff
, "%d-%d", getCurrentPidProfileIndex() + 1, getCurrentControlRateProfileIndex() + 1);
1297 static void osdElementPidsPitch(osdElementParms_t
*element
)
1299 osdFormatPID(element
->buff
, "PIT", ¤tPidProfile
->pid
[PID_PITCH
]);
1302 static void osdElementPidsRoll(osdElementParms_t
*element
)
1304 osdFormatPID(element
->buff
, "ROL", ¤tPidProfile
->pid
[PID_ROLL
]);
1307 static void osdElementPidsYaw(osdElementParms_t
*element
)
1309 osdFormatPID(element
->buff
, "YAW", ¤tPidProfile
->pid
[PID_YAW
]);
1312 static void osdElementPower(osdElementParms_t
*element
)
1314 tfp_sprintf(element
->buff
, "%4dW", getAmperage() * getBatteryVoltage() / 10000);
1317 static void osdElementRcChannels(osdElementParms_t
*element
)
1319 const uint8_t xpos
= element
->elemPosX
;
1320 const uint8_t ypos
= element
->elemPosY
;
1322 for (int i
= 0; i
< OSD_RCCHANNELS_COUNT
; i
++) {
1323 if (osdConfig()->rcChannels
[i
] >= 0) {
1324 // Translate (1000, 2000) to (-1000, 1000)
1325 int data
= scaleRange(rcData
[osdConfig()->rcChannels
[i
]], PWM_RANGE_MIN
, PWM_RANGE_MAX
, -1000, 1000);
1326 // Opt for the simplest formatting for now.
1327 // Decimal notation can be added when tfp_sprintf supports float among fancy options.
1329 tfp_sprintf(fmtbuf
, "%5d", data
);
1330 osdDisplayWrite(element
, xpos
, ypos
+ i
, DISPLAYPORT_ATTR_NONE
, fmtbuf
);
1334 element
->drawElement
= false; // element already drawn
1337 static void osdElementRemainingTimeEstimate(osdElementParms_t
*element
)
1339 const int mAhDrawn
= getMAhDrawn();
1341 if (mAhDrawn
<= 0.1 * osdConfig()->cap_alarm
) { // also handles the mAhDrawn == 0 condition
1342 tfp_sprintf(element
->buff
, "--:--");
1343 } else if (mAhDrawn
> osdConfig()->cap_alarm
) {
1344 tfp_sprintf(element
->buff
, "00:00");
1346 const int remaining_time
= (int)((osdConfig()->cap_alarm
- mAhDrawn
) * ((float)osdFlyTime
) / mAhDrawn
);
1347 osdFormatTime(element
->buff
, OSD_TIMER_PREC_SECOND
, remaining_time
);
1351 static void osdElementRssi(osdElementParms_t
*element
)
1353 uint16_t osdRssi
= getRssi() * 100 / 1024; // change range
1354 if (osdRssi
>= 100) {
1358 tfp_sprintf(element
->buff
, "%c%2d", SYM_RSSI
, osdRssi
);
1362 static void osdElementRtcTime(osdElementParms_t
*element
)
1364 osdFormatRtcDateTime(&element
->buff
[0]);
1366 #endif // USE_RTC_TIME
1368 #ifdef USE_RX_RSSI_DBM
1369 static void osdElementRssiDbm(osdElementParms_t
*element
)
1371 tfp_sprintf(element
->buff
, "%c%3d", SYM_RSSI
, getRssiDbm());
1373 #endif // USE_RX_RSSI_DBM
1375 #ifdef USE_OSD_STICK_OVERLAY
1376 static void osdBackgroundStickOverlay(osdElementParms_t
*element
)
1378 const uint8_t xpos
= element
->elemPosX
;
1379 const uint8_t ypos
= element
->elemPosY
;
1381 // Draw the axis first
1382 for (unsigned x
= 0; x
< OSD_STICK_OVERLAY_WIDTH
; x
++) {
1383 for (unsigned y
= 0; y
< OSD_STICK_OVERLAY_HEIGHT
; y
++) {
1384 // draw the axes, vertical and horizonal
1385 if ((x
== ((OSD_STICK_OVERLAY_WIDTH
- 1) / 2)) && (y
== (OSD_STICK_OVERLAY_HEIGHT
- 1) / 2)) {
1386 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_CENTER
);
1387 } else if (x
== ((OSD_STICK_OVERLAY_WIDTH
- 1) / 2)) {
1388 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_VERTICAL
);
1389 } else if (y
== ((OSD_STICK_OVERLAY_HEIGHT
- 1) / 2)) {
1390 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_HORIZONTAL
);
1395 element
->drawElement
= false; // element already drawn
1398 static void osdElementStickOverlay(osdElementParms_t
*element
)
1400 const uint8_t xpos
= element
->elemPosX
;
1401 const uint8_t ypos
= element
->elemPosY
;
1403 // Now draw the cursor
1404 rc_alias_e vertical_channel
, horizontal_channel
;
1406 if (element
->item
== OSD_STICK_OVERLAY_LEFT
) {
1407 vertical_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].left_vertical
;
1408 horizontal_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].left_horizontal
;
1410 vertical_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].right_vertical
;
1411 horizontal_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].right_horizontal
;
1414 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
);
1415 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
);
1416 const char cursor
= SYM_STICK_OVERLAY_SPRITE_HIGH
+ (cursorY
% OSD_STICK_OVERLAY_SPRITE_HEIGHT
);
1418 osdDisplayWriteChar(element
, xpos
+ cursorX
, ypos
+ cursorY
/ OSD_STICK_OVERLAY_SPRITE_HEIGHT
, DISPLAYPORT_ATTR_NONE
, cursor
);
1420 element
->drawElement
= false; // element already drawn
1422 #endif // USE_OSD_STICK_OVERLAY
1424 static void osdElementThrottlePosition(osdElementParms_t
*element
)
1426 tfp_sprintf(element
->buff
, "%c%3d", SYM_THR
, calculateThrottlePercent());
1429 static void osdElementTimer(osdElementParms_t
*element
)
1431 osdFormatTimer(element
->buff
, true, true, element
->item
- OSD_ITEM_TIMER_1
);
1434 #ifdef USE_VTX_COMMON
1435 static void osdElementVtxChannel(osdElementParms_t
*element
)
1437 const vtxDevice_t
*vtxDevice
= vtxCommonDevice();
1438 const char vtxBandLetter
= vtxCommonLookupBandLetter(vtxDevice
, vtxSettingsConfig()->band
);
1439 const char *vtxChannelName
= vtxCommonLookupChannelName(vtxDevice
, vtxSettingsConfig()->channel
);
1440 unsigned vtxStatus
= 0;
1441 uint8_t vtxPower
= vtxSettingsConfig()->power
;
1443 vtxCommonGetStatus(vtxDevice
, &vtxStatus
);
1445 if (vtxSettingsConfig()->lowPowerDisarm
) {
1446 vtxCommonGetPowerIndex(vtxDevice
, &vtxPower
);
1449 const char *vtxPowerLabel
= vtxCommonLookupPowerName(vtxDevice
, vtxPower
);
1451 char vtxStatusIndicator
= '\0';
1452 if (IS_RC_MODE_ACTIVE(BOXVTXCONTROLDISABLE
)) {
1453 vtxStatusIndicator
= 'D';
1454 } else if (vtxStatus
& VTX_STATUS_PIT_MODE
) {
1455 vtxStatusIndicator
= 'P';
1458 if (vtxStatus
& VTX_STATUS_LOCKED
) {
1459 tfp_sprintf(element
->buff
, "-:-:-:L");
1460 } else if (vtxStatusIndicator
) {
1461 tfp_sprintf(element
->buff
, "%c:%s:%s:%c", vtxBandLetter
, vtxChannelName
, vtxPowerLabel
, vtxStatusIndicator
);
1463 tfp_sprintf(element
->buff
, "%c:%s:%s", vtxBandLetter
, vtxChannelName
, vtxPowerLabel
);
1466 #endif // USE_VTX_COMMON
1468 static void osdElementWarnings(osdElementParms_t
*element
)
1470 bool elementBlinking
= false;
1471 renderOsdWarning(element
->buff
, &elementBlinking
, &element
->attr
);
1472 if (elementBlinking
) {
1473 SET_BLINK(OSD_WARNINGS
);
1475 CLR_BLINK(OSD_WARNINGS
);
1479 // Define the order in which the elements are drawn.
1480 // Elements positioned later in the list will overlay the earlier
1481 // ones if their character positions overlap
1482 // Elements that need special runtime conditional processing should be added
1483 // to osdAddActiveElements()
1485 static const uint8_t osdElementDisplayOrder
[] = {
1486 OSD_MAIN_BATT_VOLTAGE
,
1489 OSD_HORIZON_SIDEBARS
,
1490 OSD_UP_DOWN_REFERENCE
,
1493 OSD_REMAINING_TIME_ESTIMATE
,
1505 OSD_PIDRATE_PROFILE
,
1507 OSD_AVG_CELL_VOLTAGE
,
1511 OSD_MAIN_BATT_USAGE
,
1513 OSD_NUMERICAL_HEADING
,
1515 OSD_NUMERICAL_VARIO
,
1530 #ifdef USE_OSD_ADJUSTMENTS
1531 OSD_ADJUSTMENT_RANGE
,
1533 #ifdef USE_ADC_INTERNAL
1534 OSD_CORE_TEMPERATURE
,
1536 #ifdef USE_RX_LINK_QUALITY_INFO
1539 #ifdef USE_RX_LINK_UPLINK_POWER
1540 OSD_TX_UPLINK_POWER
,
1542 #ifdef USE_RX_RSSI_DBM
1545 #ifdef USE_OSD_STICK_OVERLAY
1546 OSD_STICK_OVERLAY_LEFT
,
1547 OSD_STICK_OVERLAY_RIGHT
,
1549 #ifdef USE_PROFILE_NAMES
1550 OSD_RATE_PROFILE_NAME
,
1551 OSD_PID_PROFILE_NAME
,
1553 #ifdef USE_OSD_PROFILES
1558 #ifdef USE_PERSISTENT_STATS
1563 // Define the mapping between the OSD element id and the function to draw it
1565 const osdElementDrawFn osdElementDrawFunction
[OSD_ITEM_COUNT
] = {
1566 [OSD_CAMERA_FRAME
] = NULL
, // only has background. Added first so it's the lowest "layer" and doesn't cover other elements
1567 [OSD_RSSI_VALUE
] = osdElementRssi
,
1568 [OSD_MAIN_BATT_VOLTAGE
] = osdElementMainBatteryVoltage
,
1569 [OSD_CROSSHAIRS
] = osdElementCrosshairs
, // only has background, but needs to be over other elements (like artificial horizon)
1571 [OSD_ARTIFICIAL_HORIZON
] = osdElementArtificialHorizon
,
1572 [OSD_UP_DOWN_REFERENCE
] = osdElementUpDownReference
,
1574 [OSD_HORIZON_SIDEBARS
] = NULL
, // only has background
1575 [OSD_ITEM_TIMER_1
] = osdElementTimer
,
1576 [OSD_ITEM_TIMER_2
] = osdElementTimer
,
1577 [OSD_FLYMODE
] = osdElementFlymode
,
1578 [OSD_CRAFT_NAME
] = NULL
, // only has background
1579 [OSD_THROTTLE_POS
] = osdElementThrottlePosition
,
1580 #ifdef USE_VTX_COMMON
1581 [OSD_VTX_CHANNEL
] = osdElementVtxChannel
,
1583 [OSD_CURRENT_DRAW
] = osdElementCurrentDraw
,
1584 [OSD_MAH_DRAWN
] = osdElementMahDrawn
,
1586 [OSD_GPS_SPEED
] = osdElementGpsSpeed
,
1587 [OSD_GPS_SATS
] = osdElementGpsSats
,
1589 [OSD_ALTITUDE
] = osdElementAltitude
,
1590 [OSD_ROLL_PIDS
] = osdElementPidsRoll
,
1591 [OSD_PITCH_PIDS
] = osdElementPidsPitch
,
1592 [OSD_YAW_PIDS
] = osdElementPidsYaw
,
1593 [OSD_POWER
] = osdElementPower
,
1594 [OSD_PIDRATE_PROFILE
] = osdElementPidRateProfile
,
1595 [OSD_WARNINGS
] = osdElementWarnings
,
1596 [OSD_AVG_CELL_VOLTAGE
] = osdElementAverageCellVoltage
,
1598 [OSD_GPS_LON
] = osdElementGpsCoordinate
,
1599 [OSD_GPS_LAT
] = osdElementGpsCoordinate
,
1601 [OSD_DEBUG
] = osdElementDebug
,
1603 [OSD_PITCH_ANGLE
] = osdElementAngleRollPitch
,
1604 [OSD_ROLL_ANGLE
] = osdElementAngleRollPitch
,
1606 [OSD_MAIN_BATT_USAGE
] = osdElementMainBatteryUsage
,
1607 [OSD_DISARMED
] = osdElementDisarmed
,
1609 [OSD_HOME_DIR
] = osdElementGpsHomeDirection
,
1610 [OSD_HOME_DIST
] = osdElementGpsHomeDistance
,
1612 [OSD_NUMERICAL_HEADING
] = osdElementNumericalHeading
,
1614 [OSD_NUMERICAL_VARIO
] = osdElementNumericalVario
,
1616 [OSD_COMPASS_BAR
] = osdElementCompassBar
,
1617 #ifdef USE_ESC_SENSOR
1618 [OSD_ESC_TMP
] = osdElementEscTemperature
,
1620 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1621 [OSD_ESC_RPM
] = osdElementEscRpm
,
1623 [OSD_REMAINING_TIME_ESTIMATE
] = osdElementRemainingTimeEstimate
,
1625 [OSD_RTC_DATETIME
] = osdElementRtcTime
,
1627 #ifdef USE_OSD_ADJUSTMENTS
1628 [OSD_ADJUSTMENT_RANGE
] = osdElementAdjustmentRange
,
1630 #ifdef USE_ADC_INTERNAL
1631 [OSD_CORE_TEMPERATURE
] = osdElementCoreTemperature
,
1633 [OSD_ANTI_GRAVITY
] = osdElementAntiGravity
,
1635 [OSD_G_FORCE
] = osdElementGForce
,
1637 [OSD_MOTOR_DIAG
] = osdElementMotorDiagnostics
,
1639 [OSD_LOG_STATUS
] = osdElementLogStatus
,
1642 [OSD_FLIP_ARROW
] = osdElementCrashFlipArrow
,
1644 #ifdef USE_RX_LINK_QUALITY_INFO
1645 [OSD_LINK_QUALITY
] = osdElementLinkQuality
,
1647 #ifdef USE_RX_LINK_UPLINK_POWER
1648 [OSD_TX_UPLINK_POWER
] = osdElementTxUplinkPower
,
1651 [OSD_FLIGHT_DIST
] = osdElementGpsFlightDistance
,
1653 #ifdef USE_OSD_STICK_OVERLAY
1654 [OSD_STICK_OVERLAY_LEFT
] = osdElementStickOverlay
,
1655 [OSD_STICK_OVERLAY_RIGHT
] = osdElementStickOverlay
,
1657 [OSD_DISPLAY_NAME
] = NULL
, // only has background
1658 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1659 [OSD_ESC_RPM_FREQ
] = osdElementEscRpmFreq
,
1661 #ifdef USE_PROFILE_NAMES
1662 [OSD_RATE_PROFILE_NAME
] = osdElementRateProfileName
,
1663 [OSD_PID_PROFILE_NAME
] = osdElementPidProfileName
,
1665 #ifdef USE_OSD_PROFILES
1666 [OSD_PROFILE_NAME
] = osdElementOsdProfileName
,
1668 #ifdef USE_RX_RSSI_DBM
1669 [OSD_RSSI_DBM_VALUE
] = osdElementRssiDbm
,
1671 [OSD_RC_CHANNELS
] = osdElementRcChannels
,
1673 [OSD_EFFICIENCY
] = osdElementEfficiency
,
1675 #ifdef USE_PERSISTENT_STATS
1676 [OSD_TOTAL_FLIGHTS
] = osdElementTotalFlights
,
1680 // Define the mapping between the OSD element id and the function to draw its background (static part)
1681 // Only necessary to define the entries that actually have a background function
1683 const osdElementDrawFn osdElementBackgroundFunction
[OSD_ITEM_COUNT
] = {
1684 [OSD_CAMERA_FRAME
] = osdBackgroundCameraFrame
,
1685 [OSD_HORIZON_SIDEBARS
] = osdBackgroundHorizonSidebars
,
1686 [OSD_CRAFT_NAME
] = osdBackgroundCraftName
,
1687 #ifdef USE_OSD_STICK_OVERLAY
1688 [OSD_STICK_OVERLAY_LEFT
] = osdBackgroundStickOverlay
,
1689 [OSD_STICK_OVERLAY_RIGHT
] = osdBackgroundStickOverlay
,
1691 [OSD_DISPLAY_NAME
] = osdBackgroundDisplayName
,
1694 static void osdAddActiveElement(osd_items_e element
)
1696 if (VISIBLE(osdElementConfig()->item_pos
[element
])) {
1697 activeOsdElementArray
[activeOsdElementCount
++] = element
;
1701 // Examine the elements and build a list of only the active (enabled)
1702 // ones to speed up rendering.
1704 void osdAddActiveElements(void)
1706 activeOsdElementCount
= 0;
1709 if (sensors(SENSOR_ACC
)) {
1710 osdAddActiveElement(OSD_ARTIFICIAL_HORIZON
);
1711 osdAddActiveElement(OSD_G_FORCE
);
1712 osdAddActiveElement(OSD_UP_DOWN_REFERENCE
);
1716 for (unsigned i
= 0; i
< sizeof(osdElementDisplayOrder
); i
++) {
1717 osdAddActiveElement(osdElementDisplayOrder
[i
]);
1721 if (sensors(SENSOR_GPS
)) {
1722 osdAddActiveElement(OSD_GPS_SATS
);
1723 osdAddActiveElement(OSD_GPS_SPEED
);
1724 osdAddActiveElement(OSD_GPS_LAT
);
1725 osdAddActiveElement(OSD_GPS_LON
);
1726 osdAddActiveElement(OSD_HOME_DIST
);
1727 osdAddActiveElement(OSD_HOME_DIR
);
1728 osdAddActiveElement(OSD_FLIGHT_DIST
);
1729 osdAddActiveElement(OSD_EFFICIENCY
);
1732 #ifdef USE_ESC_SENSOR
1733 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
1734 osdAddActiveElement(OSD_ESC_TMP
);
1738 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1739 if ((featureIsEnabled(FEATURE_ESC_SENSOR
)) || (motorConfig()->dev
.useDshotTelemetry
)) {
1740 osdAddActiveElement(OSD_ESC_RPM
);
1741 osdAddActiveElement(OSD_ESC_RPM_FREQ
);
1745 #ifdef USE_PERSISTENT_STATS
1746 osdAddActiveElement(OSD_TOTAL_FLIGHTS
);
1750 static void osdDrawSingleElement(displayPort_t
*osdDisplayPort
, uint8_t item
)
1752 if (!osdElementDrawFunction
[item
]) {
1753 // Element has no drawing function
1756 if (!osdDisplayPort
->useDeviceBlink
&& BLINK(item
)) {
1760 uint8_t elemPosX
= OSD_X(osdElementConfig()->item_pos
[item
]);
1761 uint8_t elemPosY
= OSD_Y(osdElementConfig()->item_pos
[item
]);
1762 char buff
[OSD_ELEMENT_BUFFER_LENGTH
] = "";
1764 osdElementParms_t element
;
1765 element
.item
= item
;
1766 element
.elemPosX
= elemPosX
;
1767 element
.elemPosY
= elemPosY
;
1768 element
.type
= OSD_TYPE(osdElementConfig()->item_pos
[item
]);
1769 element
.buff
= (char *)&buff
;
1770 element
.osdDisplayPort
= osdDisplayPort
;
1771 element
.drawElement
= true;
1772 element
.attr
= DISPLAYPORT_ATTR_NONE
;
1774 // Call the element drawing function
1775 osdElementDrawFunction
[item
](&element
);
1776 if (element
.drawElement
) {
1777 osdDisplayWrite(&element
, elemPosX
, elemPosY
, element
.attr
, buff
);
1781 static void osdDrawSingleElementBackground(displayPort_t
*osdDisplayPort
, uint8_t item
)
1783 if (!osdElementBackgroundFunction
[item
]) {
1784 // Element has no background drawing function
1788 uint8_t elemPosX
= OSD_X(osdElementConfig()->item_pos
[item
]);
1789 uint8_t elemPosY
= OSD_Y(osdElementConfig()->item_pos
[item
]);
1790 char buff
[OSD_ELEMENT_BUFFER_LENGTH
] = "";
1792 osdElementParms_t element
;
1793 element
.item
= item
;
1794 element
.elemPosX
= elemPosX
;
1795 element
.elemPosY
= elemPosY
;
1796 element
.type
= OSD_TYPE(osdElementConfig()->item_pos
[item
]);
1797 element
.buff
= (char *)&buff
;
1798 element
.osdDisplayPort
= osdDisplayPort
;
1799 element
.drawElement
= true;
1801 // Call the element background drawing function
1802 osdElementBackgroundFunction
[item
](&element
);
1803 if (element
.drawElement
) {
1804 osdDisplayWrite(&element
, elemPosX
, elemPosY
, DISPLAYPORT_ATTR_NONE
, buff
);
1808 static uint8_t activeElement
= 0;
1810 uint8_t osdGetActiveElement()
1812 return activeElement
;
1815 uint8_t osdGetActiveElementCount()
1817 return activeOsdElementCount
;
1820 // Return true if there are more elements to draw
1821 bool osdDrawNextActiveElement(displayPort_t
*osdDisplayPort
, timeUs_t currentTimeUs
)
1823 UNUSED(currentTimeUs
);
1826 if (activeElement
>= activeOsdElementCount
) {
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
[activeElement
]);
1836 osdDrawSingleElement(osdDisplayPort
, activeOsdElementArray
[activeElement
]);
1838 if (++activeElement
>= activeOsdElementCount
) {
1846 void osdDrawActiveElementsBackground(displayPort_t
*osdDisplayPort
)
1848 if (backgroundLayerSupported
) {
1849 displayLayerSelect(osdDisplayPort
, DISPLAYPORT_LAYER_BACKGROUND
);
1850 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_WAIT
);
1851 for (unsigned i
= 0; i
< activeOsdElementCount
; i
++) {
1852 osdDrawSingleElementBackground(osdDisplayPort
, activeOsdElementArray
[i
]);
1854 displayLayerSelect(osdDisplayPort
, DISPLAYPORT_LAYER_FOREGROUND
);
1858 void osdElementsInit(bool backgroundLayerFlag
)
1860 backgroundLayerSupported
= backgroundLayerFlag
;
1861 activeOsdElementCount
= 0;
1862 pt1FilterInit(&batteryEfficiencyFilt
, pt1FilterGain(EFFICIENCY_CUTOFF_HZ
, 1.0f
/ osdConfig()->framerate_hz
));
1865 void osdSyncBlink() {
1866 static int blinkCount
= 0;
1868 // If the OSD blink is due a transition, do so
1869 // Task runs at osdConfig()->framerate_hz Hz, so this will cycle at 2Hz
1870 if (++blinkCount
== ((osdConfig()->framerate_hz
/ OSD_BLINK_FREQUENCY_HZ
) / 2)) {
1872 blinkState
= !blinkState
;
1876 void osdResetAlarms(void)
1878 memset(blinkBits
, 0, sizeof(blinkBits
));
1881 void osdUpdateAlarms(void)
1883 // This is overdone?
1885 int32_t alt
= osdGetMetersToSelectedUnit(getEstimatedAltitudeCm()) / 100;
1887 if (getRssiPercent() < osdConfig()->rssi_alarm
) {
1888 SET_BLINK(OSD_RSSI_VALUE
);
1890 CLR_BLINK(OSD_RSSI_VALUE
);
1893 #ifdef USE_RX_LINK_QUALITY_INFO
1894 if (rxGetLinkQualityPercent() < osdConfig()->link_quality_alarm
) {
1895 SET_BLINK(OSD_LINK_QUALITY
);
1897 CLR_BLINK(OSD_LINK_QUALITY
);
1899 #endif // USE_RX_LINK_QUALITY_INFO
1901 if (getBatteryState() == BATTERY_OK
) {
1902 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1903 CLR_BLINK(OSD_AVG_CELL_VOLTAGE
);
1905 SET_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1906 SET_BLINK(OSD_AVG_CELL_VOLTAGE
);
1910 if ((STATE(GPS_FIX
) == 0) || (gpsSol
.numSat
< 5)
1911 #ifdef USE_GPS_RESCUE
1912 || ((gpsSol
.numSat
< gpsRescueConfig()->minSats
) && gpsRescueIsConfigured())
1915 SET_BLINK(OSD_GPS_SATS
);
1917 CLR_BLINK(OSD_GPS_SATS
);
1921 for (int i
= 0; i
< OSD_TIMER_COUNT
; i
++) {
1922 const uint16_t timer
= osdConfig()->timers
[i
];
1923 const timeUs_t time
= osdGetTimerValue(OSD_TIMER_SRC(timer
));
1924 const timeUs_t alarmTime
= OSD_TIMER_ALARM(timer
) * 60000000; // convert from minutes to us
1925 if (alarmTime
!= 0 && time
>= alarmTime
) {
1926 SET_BLINK(OSD_ITEM_TIMER_1
+ i
);
1928 CLR_BLINK(OSD_ITEM_TIMER_1
+ i
);
1932 if (getMAhDrawn() >= osdConfig()->cap_alarm
) {
1933 SET_BLINK(OSD_MAH_DRAWN
);
1934 SET_BLINK(OSD_MAIN_BATT_USAGE
);
1935 SET_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1937 CLR_BLINK(OSD_MAH_DRAWN
);
1938 CLR_BLINK(OSD_MAIN_BATT_USAGE
);
1939 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1942 if ((alt
>= osdConfig()->alt_alarm
) && ARMING_FLAG(ARMED
)) {
1943 SET_BLINK(OSD_ALTITUDE
);
1945 CLR_BLINK(OSD_ALTITUDE
);
1949 if (sensors(SENSOR_GPS
) && ARMING_FLAG(ARMED
) && STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1950 if (osdConfig()->distance_alarm
&& GPS_distanceToHome
>= osdConfig()->distance_alarm
) {
1951 SET_BLINK(OSD_HOME_DIST
);
1953 CLR_BLINK(OSD_HOME_DIST
);
1956 CLR_BLINK(OSD_HOME_DIST
);;
1960 #ifdef USE_ESC_SENSOR
1961 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
1962 // This works because the combined ESC data contains the maximum temperature seen amongst all ESCs
1963 if (osdConfig()->esc_temp_alarm
!= ESC_TEMP_ALARM_OFF
&& osdEscDataCombined
->temperature
>= osdConfig()->esc_temp_alarm
) {
1964 SET_BLINK(OSD_ESC_TMP
);
1966 CLR_BLINK(OSD_ESC_TMP
);
1973 static bool osdElementIsActive(osd_items_e element
)
1975 for (unsigned i
= 0; i
< activeOsdElementCount
; i
++) {
1976 if (activeOsdElementArray
[i
] == element
) {
1983 // Determine if any active elements need the ACC
1984 bool osdElementsNeedAccelerometer(void)
1986 return osdElementIsActive(OSD_ARTIFICIAL_HORIZON
) ||
1987 osdElementIsActive(OSD_PITCH_ANGLE
) ||
1988 osdElementIsActive(OSD_ROLL_ANGLE
) ||
1989 osdElementIsActive(OSD_G_FORCE
) ||
1990 osdElementIsActive(OSD_FLIP_ARROW
) ||
1991 osdElementIsActive(OSD_UP_DOWN_REFERENCE
);