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
101 type 1: Contains Band:Channel:Power:Pit
102 type 2: Contains only Power
112 #include "platform.h"
116 #include "blackbox/blackbox.h"
117 #include "blackbox/blackbox_io.h"
119 #include "build/build_config.h"
120 #include "build/debug.h"
122 #include "common/axis.h"
123 #include "common/maths.h"
124 #include "common/printf.h"
125 #include "common/typeconversion.h"
126 #include "common/utils.h"
127 #include "common/unit.h"
128 #include "common/filter.h"
130 #include "config/config.h"
131 #include "config/feature.h"
133 #include "drivers/display.h"
134 #include "drivers/dshot.h"
135 #include "drivers/osd_symbols.h"
136 #include "drivers/time.h"
137 #include "drivers/vtx_common.h"
139 #include "fc/controlrate_profile.h"
141 #include "fc/rc_adjustments.h"
142 #include "fc/rc_controls.h"
143 #include "fc/runtime_config.h"
145 #include "flight/gps_rescue.h"
146 #include "flight/position.h"
147 #include "flight/imu.h"
148 #include "flight/mixer.h"
149 #include "flight/pid.h"
155 #include "osd/osd_elements.h"
156 #include "osd/osd_warnings.h"
158 #include "pg/motor.h"
159 #include "pg/stats.h"
163 #include "sensors/adcinternal.h"
164 #include "sensors/barometer.h"
165 #include "sensors/battery.h"
166 #include "sensors/sensors.h"
168 #ifdef USE_GPS_PLUS_CODES
169 // located in lib/main/google/olc
173 #define AH_SYMBOL_COUNT 9
174 #define AH_SIDEBAR_WIDTH_POS 7
175 #define AH_SIDEBAR_HEIGHT_POS 3
177 // Stick overlay size
178 #define OSD_STICK_OVERLAY_WIDTH 7
179 #define OSD_STICK_OVERLAY_HEIGHT 5
180 #define OSD_STICK_OVERLAY_SPRITE_HEIGHT 3
181 #define OSD_STICK_OVERLAY_VERTICAL_POSITIONS (OSD_STICK_OVERLAY_HEIGHT * OSD_STICK_OVERLAY_SPRITE_HEIGHT)
183 #define FULL_CIRCLE 360
184 #define EFFICIENCY_MINIMUM_SPEED_CM_S 100
185 #define EFFICIENCY_CUTOFF_HZ 0.5f
187 static pt1Filter_t batteryEfficiencyFilt
;
189 #define MOTOR_STOPPED_THRESHOLD_RPM 1000
191 #define SINE_25_DEG 0.422618261740699f
193 #ifdef USE_OSD_STICK_OVERLAY
194 typedef struct radioControls_s
{
195 uint8_t left_vertical
;
196 uint8_t left_horizontal
;
197 uint8_t right_vertical
;
198 uint8_t right_horizontal
;
201 static const radioControls_t radioModes
[4] = {
202 { PITCH
, YAW
, THROTTLE
, ROLL
}, // Mode 1
203 { THROTTLE
, YAW
, PITCH
, ROLL
}, // Mode 2
204 { PITCH
, ROLL
, THROTTLE
, YAW
}, // Mode 3
205 { THROTTLE
, ROLL
, PITCH
, YAW
}, // Mode 4
209 static const char compassBar
[] = {
211 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
213 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
215 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
217 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
219 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
221 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
224 static unsigned activeOsdElementCount
= 0;
225 static uint8_t activeOsdElementArray
[OSD_ITEM_COUNT
];
226 static bool backgroundLayerSupported
= false;
229 #define OSD_BLINK_FREQUENCY_HZ 2
230 static bool blinkState
= true;
231 static uint32_t blinkBits
[(OSD_ITEM_COUNT
+ 31) / 32];
232 #define SET_BLINK(item) (blinkBits[(item) / 32] |= (1 << ((item) % 32)))
233 #define CLR_BLINK(item) (blinkBits[(item) / 32] &= ~(1 << ((item) % 32)))
234 #define IS_BLINK(item) (blinkBits[(item) / 32] & (1 << ((item) % 32)))
235 #define BLINK(item) (IS_BLINK(item) && blinkState)
239 static int osdDisplayWrite(osdElementParms_t
*element
, uint8_t x
, uint8_t y
, uint8_t attr
, const char *s
)
241 if (IS_BLINK(element
->item
)) {
242 attr
|= DISPLAYPORT_BLINK
;
245 return displayWrite(element
->osdDisplayPort
, x
, y
, attr
, s
);
248 static int osdDisplayWriteChar(osdElementParms_t
*element
, uint8_t x
, uint8_t y
, uint8_t attr
, char c
)
255 return osdDisplayWrite(element
, x
, y
, attr
, buf
);
258 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
259 typedef int (*getEscRpmOrFreqFnPtr
)(int i
);
261 static int getEscRpm(int i
)
263 #ifdef USE_DSHOT_TELEMETRY
264 if (motorConfig()->dev
.useDshotTelemetry
) {
265 return erpmToRpm(getDshotTelemetry(i
));
268 #ifdef USE_ESC_SENSOR
269 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
270 return erpmToRpm(getEscSensorData(i
)->rpm
);
276 static int getEscRpmFreq(int i
)
278 return getEscRpm(i
) / 60;
281 static void renderOsdEscRpmOrFreq(getEscRpmOrFreqFnPtr escFnPtr
, osdElementParms_t
*element
)
283 int x
= element
->elemPosX
;
284 int y
= element
->elemPosY
;
285 for (int i
=0; i
< getMotorCount(); i
++) {
287 const int rpm
= MIN((*escFnPtr
)(i
),99999);
288 const int len
= tfp_sprintf(rpmStr
, "%d", rpm
);
290 osdDisplayWrite(element
, x
, y
+ i
, DISPLAYPORT_SEVERITY_NORMAL
, rpmStr
);
292 element
->drawElement
= false;
296 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
297 int osdConvertTemperatureToSelectedUnit(int tempInDegreesCelcius
)
299 switch (osdConfig()->units
) {
301 return lrintf(((tempInDegreesCelcius
* 9.0f
) / 5) + 32);
303 return tempInDegreesCelcius
;
308 static void osdFormatAltitudeString(char * buff
, int32_t altitudeCm
, osdElementType_e variantType
)
310 const char unitSymbol
= osdGetMetersToSelectedUnitSymbol();
311 unsigned decimalPlaces
;
313 switch (variantType
) {
314 case OSD_ELEMENT_TYPE_2
: // whole number altitude (no decimal places)
317 case OSD_ELEMENT_TYPE_1
: // one decimal place (default)
322 osdPrintFloat(buff
, SYM_ALTITUDE
, osdGetMetersToSelectedUnit(altitudeCm
) / 100.0f
, "", decimalPlaces
, true, unitSymbol
);
326 static void osdFormatCoordinate(char *buff
, gpsCoordinateType_e coordinateType
, osdElementType_e variantType
)
328 int32_t gpsValue
= 0;
329 const char leadingSymbol
= (coordinateType
== GPS_LONGITUDE
) ? SYM_LON
: SYM_LAT
;
331 if (STATE(GPS_FIX_EVER
)) { // don't display interim coordinates until we get the first position fix
332 gpsValue
= (coordinateType
== GPS_LONGITUDE
) ? gpsSol
.llh
.lon
: gpsSol
.llh
.lat
;
335 const int degreesPart
= abs(gpsValue
) / GPS_DEGREES_DIVIDER
;
336 int fractionalPart
= abs(gpsValue
) % GPS_DEGREES_DIVIDER
;
338 switch (variantType
) {
339 #ifdef USE_GPS_PLUS_CODES
340 #define PLUS_CODE_DIGITS 11
341 case OSD_ELEMENT_TYPE_4
: // Open Location Code
345 if (STATE(GPS_FIX_EVER
)) {
347 location
.lat
= (double)gpsSol
.llh
.lat
/ GPS_DEGREES_DIVIDER
;
348 location
.lon
= (double)gpsSol
.llh
.lon
/ GPS_DEGREES_DIVIDER
;
349 OLC_Encode(&location
, PLUS_CODE_DIGITS
, buff
, OSD_ELEMENT_BUFFER_LENGTH
- 3);
351 memset(buff
, SYM_HYPHEN
, PLUS_CODE_DIGITS
+ 1);
353 buff
[PLUS_CODE_DIGITS
+ 1] = '\0';
357 #endif // USE_GPS_PLUS_CODES
359 case OSD_ELEMENT_TYPE_3
: // degree, minutes, seconds style. ddd^mm'ss.00"W
362 *buff
++ = leadingSymbol
;
364 const int minutes
= fractionalPart
* 60 / GPS_DEGREES_DIVIDER
;
365 const int fractionalMinutes
= fractionalPart
* 60 % GPS_DEGREES_DIVIDER
;
366 const int seconds
= fractionalMinutes
* 60 / GPS_DEGREES_DIVIDER
;
367 const int tenthSeconds
= (fractionalMinutes
* 60 % GPS_DEGREES_DIVIDER
) * 10 / GPS_DEGREES_DIVIDER
;
369 if (coordinateType
== GPS_LONGITUDE
) {
370 trailingSymbol
= (gpsValue
< 0) ? 'W' : 'E';
372 trailingSymbol
= (gpsValue
< 0) ? 'S' : 'N';
374 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
);
378 case OSD_ELEMENT_TYPE_2
:
379 fractionalPart
/= 1000;
382 case OSD_ELEMENT_TYPE_1
:
384 *buff
++ = leadingSymbol
;
386 *buff
++ = SYM_HYPHEN
;
388 tfp_sprintf(buff
, (variantType
== OSD_ELEMENT_TYPE_1
? "%u.%07u" : "%u.%04u"), degreesPart
, fractionalPart
);
394 void osdFormatDistanceString(char *ptr
, int distance
, char leadingSymbol
)
396 const float convertedDistance
= osdGetMetersToSelectedUnit(distance
);
398 char unitSymbolExtended
;
401 switch (osdConfig()->units
) {
403 unitTransition
= 5280;
405 unitSymbolExtended
= SYM_MILES
;
408 unitTransition
= 1000;
410 unitSymbolExtended
= SYM_KM
;
414 unsigned decimalPlaces
;
415 float displayDistance
;
417 if (convertedDistance
< unitTransition
) {
419 displayDistance
= convertedDistance
;
420 displaySymbol
= unitSymbol
;
422 displayDistance
= convertedDistance
/ unitTransition
;
423 displaySymbol
= unitSymbolExtended
;
424 if (displayDistance
>= 10) { // >= 10 miles or km - 1 decimal place
426 } else { // < 10 miles or km - 2 decimal places
430 osdPrintFloat(ptr
, leadingSymbol
, displayDistance
, "", decimalPlaces
, false, displaySymbol
);
433 static void osdFormatPID(char * buff
, const char * label
, const pidf_t
* pid
)
435 tfp_sprintf(buff
, "%s %3d %3d %3d %3d", label
, pid
->P
, pid
->I
, pid
->D
, pid
->F
);
439 bool osdFormatRtcDateTime(char *buffer
)
442 if (!rtcGetDateTime(&dateTime
)) {
448 dateTimeFormatLocalShort(buffer
, &dateTime
);
454 void osdFormatTime(char * buff
, osd_timer_precision_e precision
, timeUs_t time
)
456 int seconds
= time
/ 1000000;
457 const int minutes
= seconds
/ 60;
458 seconds
= seconds
% 60;
461 case OSD_TIMER_PREC_SECOND
:
463 tfp_sprintf(buff
, "%02d:%02d", minutes
, seconds
);
465 case OSD_TIMER_PREC_HUNDREDTHS
:
467 const int hundredths
= (time
/ 10000) % 100;
468 tfp_sprintf(buff
, "%02d:%02d.%02d", minutes
, seconds
, hundredths
);
471 case OSD_TIMER_PREC_TENTHS
:
473 const int tenths
= (time
/ 100000) % 10;
474 tfp_sprintf(buff
, "%02d:%02d.%01d", minutes
, seconds
, tenths
);
480 static char osdGetTimerSymbol(osd_timer_source_e src
)
483 case OSD_TIMER_SRC_ON
:
485 case OSD_TIMER_SRC_TOTAL_ARMED
:
486 case OSD_TIMER_SRC_LAST_ARMED
:
488 case OSD_TIMER_SRC_ON_OR_ARMED
:
489 return ARMING_FLAG(ARMED
) ? SYM_FLY_M
: SYM_ON_M
;
495 static timeUs_t
osdGetTimerValue(osd_timer_source_e src
)
498 case OSD_TIMER_SRC_ON
:
500 case OSD_TIMER_SRC_TOTAL_ARMED
:
502 case OSD_TIMER_SRC_LAST_ARMED
: {
503 statistic_t
*stats
= osdGetStats();
504 return stats
->armed_time
;
506 case OSD_TIMER_SRC_ON_OR_ARMED
:
507 return ARMING_FLAG(ARMED
) ? osdFlyTime
: micros();
513 void osdFormatTimer(char *buff
, bool showSymbol
, bool usePrecision
, int timerIndex
)
515 const uint16_t timer
= osdConfig()->timers
[timerIndex
];
516 const uint8_t src
= OSD_TIMER_SRC(timer
);
519 *(buff
++) = osdGetTimerSymbol(src
);
522 osdFormatTime(buff
, (usePrecision
? OSD_TIMER_PRECISION(timer
) : OSD_TIMER_PREC_SECOND
), osdGetTimerValue(src
));
525 static char osdGetBatterySymbol(int cellVoltage
)
527 if (getBatteryState() == BATTERY_CRITICAL
) {
528 return SYM_MAIN_BATT
; // FIXME: currently the BAT- symbol, ideally replace with a battery with exclamation mark
530 // Calculate a symbol offset using cell voltage over full cell voltage range
531 const int symOffset
= scaleRange(cellVoltage
, batteryConfig()->vbatmincellvoltage
, batteryConfig()->vbatmaxcellvoltage
, 0, 8);
532 return SYM_BATT_EMPTY
- constrain(symOffset
, 0, 6);
536 static uint8_t osdGetHeadingIntoDiscreteDirections(int heading
, unsigned directions
)
538 heading
+= FULL_CIRCLE
; // Ensure positive value
540 // Split input heading 0..359 into sectors 0..(directions-1), but offset
541 // by half a sector so that sector 0 gets centered around heading 0.
542 // We multiply heading by directions to not loose precision in divisions
543 // In this way each segment will be a FULL_CIRCLE length
544 int direction
= (heading
* directions
+ FULL_CIRCLE
/ 2) / FULL_CIRCLE
; // scale with rounding
545 direction
%= directions
; // normalize
547 return direction
; // return segment number
550 static uint8_t osdGetDirectionSymbolFromHeading(int heading
)
552 heading
= osdGetHeadingIntoDiscreteDirections(heading
, 16);
554 // Now heading has a heading with Up=0, Right=4, Down=8 and Left=12
555 // Our symbols are Down=0, Right=4, Up=8 and Left=12
556 // There're 16 arrow symbols. Transform it.
557 heading
= 16 - heading
;
558 heading
= (heading
+ 8) % 16;
560 return SYM_ARROW_SOUTH
+ heading
;
565 * Converts altitude based on the current unit system.
566 * @param meters Value in meters to convert
568 float osdGetMetersToSelectedUnit(int32_t meters
)
570 switch (osdConfig()->units
) {
572 return meters
* 3.28084f
; // Convert to feet
574 return meters
; // Already in meters
579 * Gets the correct altitude symbol for the current unit system
581 char osdGetMetersToSelectedUnitSymbol(void)
583 switch (osdConfig()->units
) {
592 * Converts speed based on the current unit system.
593 * @param value in cm/s to convert
595 int32_t osdGetSpeedToSelectedUnit(int32_t value
)
597 switch (osdConfig()->units
) {
600 return CM_S_TO_MPH(value
);
602 return CM_S_TO_KM_H(value
);
607 * Gets the correct speed symbol for the current unit system
609 char osdGetSpeedToSelectedUnitSymbol(void)
611 switch (osdConfig()->units
) {
620 char osdGetVarioToSelectedUnitSymbol(void)
622 switch (osdConfig()->units
) {
630 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
631 char osdGetTemperatureSymbolForSelectedUnit(void)
633 switch (osdConfig()->units
) {
642 // *************************
643 // Element drawing functions
644 // *************************
646 #ifdef USE_OSD_ADJUSTMENTS
647 static void osdElementAdjustmentRange(osdElementParms_t
*element
)
649 const char *name
= getAdjustmentsRangeName();
651 tfp_sprintf(element
->buff
, "%s: %3d", name
, getAdjustmentsRangeValue());
654 #endif // USE_OSD_ADJUSTMENTS
656 static void osdElementAltitude(osdElementParms_t
*element
)
658 bool haveBaro
= false;
659 bool haveGps
= false;
661 haveBaro
= sensors(SENSOR_BARO
);
664 haveGps
= sensors(SENSOR_GPS
) && STATE(GPS_FIX
);
666 int32_t alt
= osdGetMetersToSelectedUnit(getEstimatedAltitudeCm()) / 100;
668 if ((alt
>= osdConfig()->alt_alarm
) && ARMING_FLAG(ARMED
)) {
669 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
672 if (haveBaro
|| haveGps
) {
673 osdFormatAltitudeString(element
->buff
, getEstimatedAltitudeCm(), element
->type
);
675 element
->buff
[0] = SYM_ALTITUDE
;
676 element
->buff
[1] = SYM_HYPHEN
; // We use this symbol when we don't have a valid measure
677 element
->buff
[2] = '\0';
682 static void osdElementAngleRollPitch(osdElementParms_t
*element
)
684 const float angle
= ((element
->item
== OSD_PITCH_ANGLE
) ? attitude
.values
.pitch
: attitude
.values
.roll
) / 10.0f
;
685 osdPrintFloat(element
->buff
, (element
->item
== OSD_PITCH_ANGLE
) ? SYM_PITCH
: SYM_ROLL
, fabsf(angle
), ((angle
< 0) ? "-%02u" : " %02u"), 1, true, SYM_NONE
);
689 static void osdElementAntiGravity(osdElementParms_t
*element
)
691 if (pidOsdAntiGravityActive()) {
692 strcpy(element
->buff
, "AG");
698 static void osdElementArtificialHorizon(osdElementParms_t
*element
)
700 // Get pitch and roll limits in tenths of degrees
701 const int maxPitch
= osdConfig()->ahMaxPitch
* 10;
702 const int maxRoll
= osdConfig()->ahMaxRoll
* 10;
703 const int ahSign
= osdConfig()->ahInvert
? -1 : 1;
704 const int rollAngle
= constrain(attitude
.values
.roll
* ahSign
, -maxRoll
, maxRoll
);
705 int pitchAngle
= constrain(attitude
.values
.pitch
* ahSign
, -maxPitch
, maxPitch
);
706 // Convert pitchAngle to y compensation value
707 // (maxPitch / 25) divisor matches previous settings of fixed divisor of 8 and fixed max AHI pitch angle of 20.0 degrees
709 pitchAngle
= ((pitchAngle
* 25) / maxPitch
);
711 pitchAngle
-= 41; // 41 = 4 * AH_SYMBOL_COUNT + 5
713 for (int x
= -4; x
<= 4; x
++) {
714 const int y
= ((-rollAngle
* x
) / 64) - pitchAngle
;
715 if (y
>= 0 && y
<= 81) {
716 osdDisplayWriteChar(element
, element
->elemPosX
+ x
, element
->elemPosY
+ (y
/ AH_SYMBOL_COUNT
), DISPLAYPORT_SEVERITY_NORMAL
, (SYM_AH_BAR9_0
+ (y
% AH_SYMBOL_COUNT
)));
720 element
->drawElement
= false; // element already drawn
723 static void osdElementUpDownReference(osdElementParms_t
*element
)
725 // Up/Down reference feature displays reference points on the OSD at Zenith and Nadir
726 const float earthUpinBodyFrame
[3] = {-rMat
[2][0], -rMat
[2][1], -rMat
[2][2]}; //transforum the up vector to the body frame
728 if (fabsf(earthUpinBodyFrame
[2]) < SINE_25_DEG
&& fabsf(earthUpinBodyFrame
[1]) < SINE_25_DEG
) {
729 float thetaB
; // pitch from body frame to zenith/nadir
730 float psiB
; // psi from body frame to zenith/nadir
731 char *symbol
[2] = {"U", "D"}; // character buffer
734 if(attitude
.values
.pitch
>0.0){ //nose down
735 thetaB
= -earthUpinBodyFrame
[2]; // get pitch w/re to nadir (use small angle approx for sine)
736 psiB
= -earthUpinBodyFrame
[1]; // calculate the yaw w/re to nadir (use small angle approx for sine)
739 thetaB
= earthUpinBodyFrame
[2]; // get pitch w/re to zenith (use small angle approx for sine)
740 psiB
= earthUpinBodyFrame
[1]; // calculate the yaw w/re to zenith (use small angle approx for sine)
743 int posX
= element
->elemPosX
+ lrintf(scaleRangef(psiB
, -M_PIf
/ 4, M_PIf
/ 4, -14, 14));
744 int posY
= element
->elemPosY
+ lrintf(scaleRangef(thetaB
, -M_PIf
/ 4, M_PIf
/ 4, -8, 8));
746 osdDisplayWrite(element
, posX
, posY
, DISPLAYPORT_SEVERITY_NORMAL
, symbol
[direction
]);
748 element
->drawElement
= false; // element already drawn
752 static void osdElementAverageCellVoltage(osdElementParms_t
*element
)
754 const int cellV
= getBatteryAverageCellVoltage();
755 const batteryState_e batteryState
= getBatteryState();
757 switch (batteryState
) {
758 case BATTERY_WARNING
:
759 element
->attr
= DISPLAYPORT_SEVERITY_WARNING
;
761 case BATTERY_CRITICAL
:
762 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
768 osdPrintFloat(element
->buff
, osdGetBatterySymbol(cellV
), cellV
/ 100.0f
, "", 2, false, SYM_VOLT
);
771 static void osdElementCompassBar(osdElementParms_t
*element
)
773 memcpy(element
->buff
, compassBar
+ osdGetHeadingIntoDiscreteDirections(DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
), 16), 9);
774 element
->buff
[9] = 0;
777 #ifdef USE_ADC_INTERNAL
778 static void osdElementCoreTemperature(osdElementParms_t
*element
)
780 tfp_sprintf(element
->buff
, "C%c%3d%c", SYM_TEMPERATURE
, osdConvertTemperatureToSelectedUnit(getCoreTemperatureCelsius()), osdGetTemperatureSymbolForSelectedUnit());
782 #endif // USE_ADC_INTERNAL
784 static void osdBackgroundCameraFrame(osdElementParms_t
*element
)
786 const uint8_t xpos
= element
->elemPosX
;
787 const uint8_t ypos
= element
->elemPosY
;
788 const uint8_t width
= constrain(osdConfig()->camera_frame_width
, OSD_CAMERA_FRAME_MIN_WIDTH
, OSD_CAMERA_FRAME_MAX_WIDTH
);
789 const uint8_t height
= constrain(osdConfig()->camera_frame_height
, OSD_CAMERA_FRAME_MIN_HEIGHT
, OSD_CAMERA_FRAME_MAX_HEIGHT
);
791 element
->buff
[0] = SYM_STICK_OVERLAY_CENTER
;
792 for (int i
= 1; i
< (width
- 1); i
++) {
793 element
->buff
[i
] = SYM_STICK_OVERLAY_HORIZONTAL
;
795 element
->buff
[width
- 1] = SYM_STICK_OVERLAY_CENTER
;
796 element
->buff
[width
] = 0; // string terminator
798 osdDisplayWrite(element
, xpos
, ypos
, DISPLAYPORT_SEVERITY_NORMAL
, element
->buff
);
799 for (int i
= 1; i
< (height
- 1); i
++) {
800 osdDisplayWriteChar(element
, xpos
, ypos
+ i
, DISPLAYPORT_SEVERITY_NORMAL
, SYM_STICK_OVERLAY_VERTICAL
);
801 osdDisplayWriteChar(element
, xpos
+ width
- 1, ypos
+ i
, DISPLAYPORT_SEVERITY_NORMAL
, SYM_STICK_OVERLAY_VERTICAL
);
803 osdDisplayWrite(element
, xpos
, ypos
+ height
- 1, DISPLAYPORT_SEVERITY_NORMAL
, element
->buff
);
805 element
->drawElement
= false; // element already drawn
808 static void toUpperCase(char* dest
, const char* src
, unsigned int maxSrcLength
)
811 for (i
= 0; i
< maxSrcLength
&& src
[i
]; i
++) {
812 dest
[i
] = toupper((unsigned char)src
[i
]);
817 static void osdBackgroundCraftName(osdElementParms_t
*element
)
819 if (strlen(pilotConfig()->craftName
) == 0) {
820 strcpy(element
->buff
, "CRAFT_NAME");
822 toUpperCase(element
->buff
, pilotConfig()->craftName
, MAX_NAME_LENGTH
);
827 static void osdElementCrashFlipArrow(osdElementParms_t
*element
)
829 int rollAngle
= attitude
.values
.roll
/ 10;
830 const int pitchAngle
= attitude
.values
.pitch
/ 10;
831 if (abs(rollAngle
) > 90) {
832 rollAngle
= (rollAngle
< 0 ? -180 : 180) - rollAngle
;
835 if ((isFlipOverAfterCrashActive() || (!ARMING_FLAG(ARMED
) && !isUpright())) && !((imuConfig()->small_angle
< 180 && isUpright()) || (rollAngle
== 0 && pitchAngle
== 0))) {
836 if (abs(pitchAngle
) < 2 * abs(rollAngle
) && abs(rollAngle
) < 2 * abs(pitchAngle
)) {
837 if (pitchAngle
> 0) {
839 element
->buff
[0] = SYM_ARROW_WEST
+ 2;
841 element
->buff
[0] = SYM_ARROW_EAST
- 2;
845 element
->buff
[0] = SYM_ARROW_WEST
- 2;
847 element
->buff
[0] = SYM_ARROW_EAST
+ 2;
851 if (abs(pitchAngle
) > abs(rollAngle
)) {
852 if (pitchAngle
> 0) {
853 element
->buff
[0] = SYM_ARROW_SOUTH
;
855 element
->buff
[0] = SYM_ARROW_NORTH
;
859 element
->buff
[0] = SYM_ARROW_WEST
;
861 element
->buff
[0] = SYM_ARROW_EAST
;
865 element
->buff
[1] = '\0';
870 static void osdElementCrosshairs(osdElementParms_t
*element
)
872 element
->buff
[0] = SYM_AH_CENTER_LINE
;
873 element
->buff
[1] = SYM_AH_CENTER
;
874 element
->buff
[2] = SYM_AH_CENTER_LINE_RIGHT
;
875 element
->buff
[3] = 0;
878 static void osdElementCurrentDraw(osdElementParms_t
*element
)
880 const float amperage
= fabsf(getAmperage() / 100.0f
);
881 osdPrintFloat(element
->buff
, SYM_NONE
, amperage
, "%3u", 2, false, SYM_AMP
);
884 static void osdElementDebug(osdElementParms_t
*element
)
886 tfp_sprintf(element
->buff
, "DBG %5d %5d %5d %5d", debug
[0], debug
[1], debug
[2], debug
[3]);
889 static void osdElementDisarmed(osdElementParms_t
*element
)
891 if (!ARMING_FLAG(ARMED
)) {
892 tfp_sprintf(element
->buff
, "DISARMED");
896 static void osdBackgroundPilotName(osdElementParms_t
*element
)
898 if (strlen(pilotConfig()->pilotName
) == 0) {
899 strcpy(element
->buff
, "PILOT_NAME");
901 toUpperCase(element
->buff
, pilotConfig()->pilotName
, MAX_NAME_LENGTH
);
905 #ifdef USE_PERSISTENT_STATS
906 static void osdElementTotalFlights(osdElementParms_t
*element
)
908 const int32_t total_flights
= statsConfig()->stats_total_flights
;
909 tfp_sprintf(element
->buff
, "#%d", total_flights
);
913 #ifdef USE_PROFILE_NAMES
914 static void osdElementRateProfileName(osdElementParms_t
*element
)
916 if (strlen(currentControlRateProfile
->profileName
) == 0) {
917 tfp_sprintf(element
->buff
, "RATE_%u", getCurrentControlRateProfileIndex() + 1);
919 toUpperCase(element
->buff
, currentControlRateProfile
->profileName
, MAX_PROFILE_NAME_LENGTH
);
923 static void osdElementPidProfileName(osdElementParms_t
*element
)
925 if (strlen(currentPidProfile
->profileName
) == 0) {
926 tfp_sprintf(element
->buff
, "PID_%u", getCurrentPidProfileIndex() + 1);
928 toUpperCase(element
->buff
, currentPidProfile
->profileName
, MAX_PROFILE_NAME_LENGTH
);
933 #ifdef USE_OSD_PROFILES
934 static void osdElementOsdProfileName(osdElementParms_t
*element
)
936 uint8_t profileIndex
= getCurrentOsdProfileIndex();
938 if (strlen(osdConfig()->profile
[profileIndex
- 1]) == 0) {
939 tfp_sprintf(element
->buff
, "OSD_%u", profileIndex
);
941 toUpperCase(element
->buff
, osdConfig()->profile
[profileIndex
- 1], OSD_PROFILE_NAME_LENGTH
);
946 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
948 static void osdElementEscTemperature(osdElementParms_t
*element
)
950 #if defined(USE_ESC_SENSOR)
951 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
952 tfp_sprintf(element
->buff
, "E%c%3d%c", SYM_TEMPERATURE
, osdConvertTemperatureToSelectedUnit(osdEscDataCombined
->temperature
), osdGetTemperatureSymbolForSelectedUnit());
955 #if defined(USE_DSHOT_TELEMETRY)
957 uint32_t osdEleIx
= tfp_sprintf(element
->buff
, "E%c", SYM_TEMPERATURE
);
959 for (uint8_t k
= 0; k
< getMotorCount(); k
++) {
960 if ((dshotTelemetryState
.motorState
[k
].telemetryTypes
& (1 << DSHOT_TELEMETRY_TYPE_TEMPERATURE
)) != 0) {
961 osdEleIx
+= tfp_sprintf(element
->buff
+ osdEleIx
, "%3d%c",
962 osdConvertTemperatureToSelectedUnit(dshotTelemetryState
.motorState
[k
].telemetryData
[DSHOT_TELEMETRY_TYPE_TEMPERATURE
]),
963 osdGetTemperatureSymbolForSelectedUnit());
965 osdEleIx
+= tfp_sprintf(element
->buff
+ osdEleIx
, " 0%c", osdGetTemperatureSymbolForSelectedUnit());
974 static void osdElementEscRpm(osdElementParms_t
*element
)
976 renderOsdEscRpmOrFreq(&getEscRpm
,element
);
979 static void osdElementEscRpmFreq(osdElementParms_t
*element
)
981 renderOsdEscRpmOrFreq(&getEscRpmFreq
,element
);
986 static void osdElementFlymode(osdElementParms_t
*element
)
988 // Note that flight mode display has precedence in what to display.
991 // 3. ANGLE, HORIZON, ACRO TRAINER
995 if (FLIGHT_MODE(FAILSAFE_MODE
)) {
996 strcpy(element
->buff
, "!FS!");
997 } else if (FLIGHT_MODE(GPS_RESCUE_MODE
)) {
998 strcpy(element
->buff
, "RESC");
999 } else if (FLIGHT_MODE(HEADFREE_MODE
)) {
1000 strcpy(element
->buff
, "HEAD");
1001 } else if (FLIGHT_MODE(ANGLE_MODE
)) {
1002 strcpy(element
->buff
, "ANGL");
1003 } else if (FLIGHT_MODE(HORIZON_MODE
)) {
1004 strcpy(element
->buff
, "HOR ");
1005 } else if (IS_RC_MODE_ACTIVE(BOXACROTRAINER
)) {
1006 strcpy(element
->buff
, "ATRN");
1007 } else if (airmodeIsEnabled()) {
1008 strcpy(element
->buff
, "AIR ");
1010 strcpy(element
->buff
, "ACRO");
1014 static void osdElementReadyMode(osdElementParms_t
*element
)
1016 if (IS_RC_MODE_ACTIVE(BOXREADY
) && !ARMING_FLAG(ARMED
)) {
1017 strcpy(element
->buff
, "READY");
1022 static void osdElementGForce(osdElementParms_t
*element
)
1024 osdPrintFloat(element
->buff
, SYM_NONE
, osdGForce
, "", 1, true, 'G');
1029 static void osdElementGpsFlightDistance(osdElementParms_t
*element
)
1031 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1032 osdFormatDistanceString(element
->buff
, GPS_distanceFlownInCm
/ 100, SYM_TOTAL_DISTANCE
);
1034 // We use this symbol when we don't have a FIX
1035 tfp_sprintf(element
->buff
, "%c%c", SYM_TOTAL_DISTANCE
, SYM_HYPHEN
);
1039 static void osdElementGpsHomeDirection(osdElementParms_t
*element
)
1041 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1042 if (GPS_distanceToHome
> 0) {
1043 const int h
= DECIDEGREES_TO_DEGREES(GPS_directionToHome
- attitude
.values
.yaw
);
1044 element
->buff
[0] = osdGetDirectionSymbolFromHeading(h
);
1046 element
->buff
[0] = SYM_OVER_HOME
;
1050 // We use this symbol when we don't have a FIX
1051 element
->buff
[0] = SYM_HYPHEN
;
1054 element
->buff
[1] = 0;
1057 static void osdElementGpsHomeDistance(osdElementParms_t
*element
)
1059 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1060 osdFormatDistanceString(element
->buff
, GPS_distanceToHome
, SYM_HOMEFLAG
);
1062 element
->buff
[0] = SYM_HOMEFLAG
;
1063 // We use this symbol when we don't have a FIX
1064 element
->buff
[1] = SYM_HYPHEN
;
1065 element
->buff
[2] = '\0';
1069 static void osdElementGpsCoordinate(osdElementParms_t
*element
)
1071 const gpsCoordinateType_e coordinateType
= (element
->item
== OSD_GPS_LON
) ? GPS_LONGITUDE
: GPS_LATITUDE
;
1072 osdFormatCoordinate(element
->buff
, coordinateType
, element
->type
);
1073 if (STATE(GPS_FIX_EVER
) && !STATE(GPS_FIX
)) {
1074 SET_BLINK(element
->item
); // blink if we had a fix but have since lost it
1076 CLR_BLINK(element
->item
);
1080 static void osdElementGpsSats(osdElementParms_t
*element
)
1082 if ((STATE(GPS_FIX
) == 0) || (gpsSol
.numSat
< GPS_MIN_SAT_COUNT
) ) {
1083 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
1085 #ifdef USE_GPS_RESCUE
1086 else if ((gpsSol
.numSat
< gpsRescueConfig()->minSats
) && gpsRescueIsConfigured()) {
1087 element
->attr
= DISPLAYPORT_SEVERITY_WARNING
;
1091 element
->attr
= DISPLAYPORT_SEVERITY_INFO
;
1094 if (!gpsIsHealthy()) {
1095 tfp_sprintf(element
->buff
, "%c%cNC", SYM_SAT_L
, SYM_SAT_R
);
1097 int pos
= tfp_sprintf(element
->buff
, "%c%c%2d", SYM_SAT_L
, SYM_SAT_R
, gpsSol
.numSat
);
1098 if (osdConfig()->gps_sats_show_hdop
) { // add on the GPS module HDOP estimate
1099 element
->buff
[pos
++] = ' ';
1100 osdPrintFloat(element
->buff
+ pos
, SYM_NONE
, gpsSol
.dop
.hdop
/ 100.0f
, "", 1, true, SYM_NONE
);
1105 static void osdElementGpsSpeed(osdElementParms_t
*element
)
1107 if (STATE(GPS_FIX
)) {
1108 tfp_sprintf(element
->buff
, "%c%3d%c", SYM_SPEED
, osdGetSpeedToSelectedUnit(gpsConfig()->gps_use_3d_speed
? gpsSol
.speed3d
: gpsSol
.groundSpeed
), osdGetSpeedToSelectedUnitSymbol());
1110 tfp_sprintf(element
->buff
, "%c%c%c", SYM_SPEED
, SYM_HYPHEN
, osdGetSpeedToSelectedUnitSymbol());
1114 static void osdElementEfficiency(osdElementParms_t
*element
)
1117 if (sensors(SENSOR_GPS
) && ARMING_FLAG(ARMED
) && STATE(GPS_FIX
) && gpsSol
.groundSpeed
>= EFFICIENCY_MINIMUM_SPEED_CM_S
) {
1118 const float speed
= (float)osdGetSpeedToSelectedUnit(gpsSol
.groundSpeed
);
1119 const float mAmperage
= (float)getAmperage() * 10.f
; // Current in mA
1120 efficiency
= lrintf(pt1FilterApply(&batteryEfficiencyFilt
, (mAmperage
/ speed
)));
1123 const char unitSymbol
= osdConfig()->units
== UNIT_IMPERIAL
? SYM_MILES
: SYM_KM
;
1124 if (efficiency
> 0 && efficiency
<= 9999) {
1125 tfp_sprintf(element
->buff
, "%4d%c/%c", efficiency
, SYM_MAH
, unitSymbol
);
1127 tfp_sprintf(element
->buff
, "----%c/%c", SYM_MAH
, unitSymbol
);
1132 static void osdBackgroundHorizonSidebars(osdElementParms_t
*element
)
1135 const int8_t hudwidth
= AH_SIDEBAR_WIDTH_POS
;
1136 const int8_t hudheight
= AH_SIDEBAR_HEIGHT_POS
;
1137 for (int y
= -hudheight
; y
<= hudheight
; y
++) {
1138 osdDisplayWriteChar(element
, element
->elemPosX
- hudwidth
, element
->elemPosY
+ y
, DISPLAYPORT_SEVERITY_NORMAL
, SYM_AH_DECORATION
);
1139 osdDisplayWriteChar(element
, element
->elemPosX
+ hudwidth
, element
->elemPosY
+ y
, DISPLAYPORT_SEVERITY_NORMAL
, SYM_AH_DECORATION
);
1142 // AH level indicators
1143 osdDisplayWriteChar(element
, element
->elemPosX
- hudwidth
+ 1, element
->elemPosY
, DISPLAYPORT_SEVERITY_NORMAL
, SYM_AH_LEFT
);
1144 osdDisplayWriteChar(element
, element
->elemPosX
+ hudwidth
- 1, element
->elemPosY
, DISPLAYPORT_SEVERITY_NORMAL
, SYM_AH_RIGHT
);
1146 element
->drawElement
= false; // element already drawn
1149 #ifdef USE_RX_LINK_QUALITY_INFO
1150 static void osdElementLinkQuality(osdElementParms_t
*element
)
1152 uint16_t osdLinkQuality
= 0;
1154 if (rxGetLinkQualityPercent() < osdConfig()->link_quality_alarm
) {
1155 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
1158 if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_CRSF
) { // 0-99
1159 osdLinkQuality
= rxGetLinkQuality();
1160 const uint8_t osdRfMode
= rxGetRfMode();
1161 tfp_sprintf(element
->buff
, "%c%1d:%2d", SYM_LINK_QUALITY
, osdRfMode
, osdLinkQuality
);
1162 } else if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_GHST
) { // 0-100
1163 osdLinkQuality
= rxGetLinkQuality();
1164 tfp_sprintf(element
->buff
, "%c%2d", SYM_LINK_QUALITY
, osdLinkQuality
);
1166 osdLinkQuality
= rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE
;
1167 if (osdLinkQuality
>= 10) {
1170 tfp_sprintf(element
->buff
, "%c%1d", SYM_LINK_QUALITY
, osdLinkQuality
);
1173 #endif // USE_RX_LINK_QUALITY_INFO
1175 #ifdef USE_RX_LINK_UPLINK_POWER
1176 static void osdElementTxUplinkPower(osdElementParms_t
*element
)
1178 const uint16_t osdUplinkTxPowerMw
= rxGetUplinkTxPwrMw();
1179 if (osdUplinkTxPowerMw
< 1000) {
1180 tfp_sprintf(element
->buff
, "%c%3dMW", SYM_RSSI
, osdUplinkTxPowerMw
);
1182 osdPrintFloat(element
->buff
, SYM_RSSI
, osdUplinkTxPowerMw
/ 1000.0f
, "", 1, false, 'W');
1185 #endif // USE_RX_LINK_UPLINK_POWER
1188 static void osdElementLogStatus(osdElementParms_t
*element
)
1190 if (IS_RC_MODE_ACTIVE(BOXBLACKBOX
)) {
1191 if (!isBlackboxDeviceWorking()) {
1192 tfp_sprintf(element
->buff
, "%c!", SYM_BBLOG
);
1193 } else if (isBlackboxDeviceFull()) {
1194 tfp_sprintf(element
->buff
, "%c>", SYM_BBLOG
);
1196 int32_t logNumber
= blackboxGetLogNumber();
1197 if (logNumber
>= 0) {
1198 tfp_sprintf(element
->buff
, "%c%d", SYM_BBLOG
, logNumber
);
1200 tfp_sprintf(element
->buff
, "%c", SYM_BBLOG
);
1205 #endif // USE_BLACKBOX
1207 static void osdElementMahDrawn(osdElementParms_t
*element
)
1209 const int mAhDrawn
= getMAhDrawn();
1211 if (mAhDrawn
>= osdConfig()->cap_alarm
) {
1212 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
1215 tfp_sprintf(element
->buff
, "%4d%c", mAhDrawn
, SYM_MAH
);
1218 static void osdElementWattHoursDrawn(osdElementParms_t
*element
)
1220 const int mAhDrawn
= getMAhDrawn();
1221 const float wattHoursDrawn
= getWhDrawn();
1223 if (mAhDrawn
>= osdConfig()->cap_alarm
) {
1224 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
1227 if (wattHoursDrawn
< 1.0f
) {
1228 tfp_sprintf(element
->buff
, "%3dMWH", lrintf(wattHoursDrawn
* 1000));
1230 int wattHourWholeNumber
= (int)wattHoursDrawn
;
1231 int wattHourDecimalValue
= (int)((wattHoursDrawn
- wattHourWholeNumber
) * 100);
1233 tfp_sprintf(element
->buff
, wattHourDecimalValue
>= 10 ? "%3d.%2dWH" : "%3d.0%1dWH", wattHourWholeNumber
, wattHourDecimalValue
);
1237 static void osdElementMainBatteryUsage(osdElementParms_t
*element
)
1239 // Set length of indicator bar
1240 #define MAIN_BATT_USAGE_STEPS 11 // Use an odd number so the bar can be centered.
1241 const int mAhDrawn
= getMAhDrawn();
1242 const int usedCapacity
= getMAhDrawn();
1243 int displayBasis
= usedCapacity
;
1245 if (mAhDrawn
>= osdConfig()->cap_alarm
) {
1246 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
1249 switch (element
->type
) {
1250 case OSD_ELEMENT_TYPE_3
: // mAh remaining percentage (counts down as battery is used)
1251 displayBasis
= constrain(batteryConfig()->batteryCapacity
- usedCapacity
, 0, batteryConfig()->batteryCapacity
);
1254 case OSD_ELEMENT_TYPE_4
: // mAh used percentage (counts up as battery is used)
1256 int displayPercent
= 0;
1257 if (batteryConfig()->batteryCapacity
) {
1258 displayPercent
= constrain(lrintf(100.0f
* displayBasis
/ batteryConfig()->batteryCapacity
), 0, 100);
1260 tfp_sprintf(element
->buff
, "%c%d%%", SYM_MAH
, displayPercent
);
1264 case OSD_ELEMENT_TYPE_2
: // mAh used graphical progress bar (grows as battery is used)
1265 displayBasis
= constrain(batteryConfig()->batteryCapacity
- usedCapacity
, 0, batteryConfig()->batteryCapacity
);
1268 case OSD_ELEMENT_TYPE_1
: // mAh remaining graphical progress bar (shrinks as battery is used)
1271 uint8_t remainingCapacityBars
= 0;
1273 if (batteryConfig()->batteryCapacity
) {
1274 const float batteryRemaining
= constrain(batteryConfig()->batteryCapacity
- displayBasis
, 0, batteryConfig()->batteryCapacity
);
1275 remainingCapacityBars
= ceilf((batteryRemaining
/ (batteryConfig()->batteryCapacity
/ MAIN_BATT_USAGE_STEPS
)));
1278 // Create empty battery indicator bar
1279 element
->buff
[0] = SYM_PB_START
;
1280 for (int i
= 1; i
<= MAIN_BATT_USAGE_STEPS
; i
++) {
1281 element
->buff
[i
] = i
<= remainingCapacityBars
? SYM_PB_FULL
: SYM_PB_EMPTY
;
1283 element
->buff
[MAIN_BATT_USAGE_STEPS
+ 1] = SYM_PB_CLOSE
;
1284 if (remainingCapacityBars
> 0 && remainingCapacityBars
< MAIN_BATT_USAGE_STEPS
) {
1285 element
->buff
[1 + remainingCapacityBars
] = SYM_PB_END
;
1287 element
->buff
[MAIN_BATT_USAGE_STEPS
+2] = '\0';
1293 static void osdElementMainBatteryVoltage(osdElementParms_t
*element
)
1295 unsigned decimalPlaces
;
1296 const float batteryVoltage
= getBatteryVoltage() / 100.0f
;
1297 batteryState_e batteryState
= getBatteryState();
1299 switch (batteryState
) {
1300 case BATTERY_WARNING
:
1301 element
->attr
= DISPLAYPORT_SEVERITY_WARNING
;
1303 case BATTERY_CRITICAL
:
1304 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
1310 if (batteryVoltage
>= 10) { // if voltage is 10v or more then display only 1 decimal place
1315 osdPrintFloat(element
->buff
, osdGetBatterySymbol(getBatteryAverageCellVoltage()), batteryVoltage
, "", decimalPlaces
, true, SYM_VOLT
);
1318 static void osdElementMotorDiagnostics(osdElementParms_t
*element
)
1321 const bool motorsRunning
= areMotorsRunning();
1322 for (; i
< getMotorCount(); i
++) {
1323 if (motorsRunning
) {
1324 element
->buff
[i
] = 0x88 - scaleRange(motor
[i
], getMotorOutputLow(), getMotorOutputHigh(), 0, 8);
1325 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
1326 if (getEscRpm(i
) < MOTOR_STOPPED_THRESHOLD_RPM
) {
1327 // Motor is not spinning properly. Mark as Stopped
1328 element
->buff
[i
] = 'S';
1332 element
->buff
[i
] = 0x88;
1335 element
->buff
[i
] = '\0';
1338 static void osdElementNumericalHeading(osdElementParms_t
*element
)
1340 const int heading
= DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
1341 tfp_sprintf(element
->buff
, "%c%03d", osdGetDirectionSymbolFromHeading(heading
), heading
);
1345 static void osdElementNumericalVario(osdElementParms_t
*element
)
1347 bool haveBaro
= false;
1348 bool haveGps
= false;
1350 haveBaro
= sensors(SENSOR_BARO
);
1353 haveGps
= sensors(SENSOR_GPS
) && STATE(GPS_FIX
);
1355 if (haveBaro
|| haveGps
) {
1356 const float verticalSpeed
= osdGetMetersToSelectedUnit(getEstimatedVario()) / 100.0f
;
1357 const char directionSymbol
= verticalSpeed
< 0 ? SYM_ARROW_SMALL_DOWN
: SYM_ARROW_SMALL_UP
;
1358 osdPrintFloat(element
->buff
, directionSymbol
, fabsf(verticalSpeed
), "", 1, true, osdGetVarioToSelectedUnitSymbol());
1360 // We use this symbol when we don't have a valid measure
1361 element
->buff
[0] = SYM_HYPHEN
;
1362 element
->buff
[1] = '\0';
1367 static void osdElementPidRateProfile(osdElementParms_t
*element
)
1369 tfp_sprintf(element
->buff
, "%d-%d", getCurrentPidProfileIndex() + 1, getCurrentControlRateProfileIndex() + 1);
1372 static void osdElementPidsPitch(osdElementParms_t
*element
)
1374 osdFormatPID(element
->buff
, "PIT", ¤tPidProfile
->pid
[PID_PITCH
]);
1377 static void osdElementPidsRoll(osdElementParms_t
*element
)
1379 osdFormatPID(element
->buff
, "ROL", ¤tPidProfile
->pid
[PID_ROLL
]);
1382 static void osdElementPidsYaw(osdElementParms_t
*element
)
1384 osdFormatPID(element
->buff
, "YAW", ¤tPidProfile
->pid
[PID_YAW
]);
1387 static void osdElementPower(osdElementParms_t
*element
)
1389 tfp_sprintf(element
->buff
, "%4dW", getAmperage() * getBatteryVoltage() / 10000);
1392 static void osdElementRcChannels(osdElementParms_t
*element
)
1394 const uint8_t xpos
= element
->elemPosX
;
1395 const uint8_t ypos
= element
->elemPosY
;
1397 for (int i
= 0; i
< OSD_RCCHANNELS_COUNT
; i
++) {
1398 if (osdConfig()->rcChannels
[i
] >= 0) {
1399 // Translate (1000, 2000) to (-1000, 1000)
1400 int data
= scaleRange(rcData
[osdConfig()->rcChannels
[i
]], PWM_RANGE_MIN
, PWM_RANGE_MAX
, -1000, 1000);
1401 // Opt for the simplest formatting for now.
1402 // Decimal notation can be added when tfp_sprintf supports float among fancy options.
1404 tfp_sprintf(fmtbuf
, "%5d", data
);
1405 osdDisplayWrite(element
, xpos
, ypos
+ i
, DISPLAYPORT_SEVERITY_NORMAL
, fmtbuf
);
1409 element
->drawElement
= false; // element already drawn
1412 static void osdElementRemainingTimeEstimate(osdElementParms_t
*element
)
1414 const int mAhDrawn
= getMAhDrawn();
1416 if (mAhDrawn
>= osdConfig()->cap_alarm
) {
1417 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
1420 if (mAhDrawn
<= 0.1 * osdConfig()->cap_alarm
) { // also handles the mAhDrawn == 0 condition
1421 tfp_sprintf(element
->buff
, "--:--");
1422 } else if (mAhDrawn
> osdConfig()->cap_alarm
) {
1423 tfp_sprintf(element
->buff
, "00:00");
1425 const int remaining_time
= (int)((osdConfig()->cap_alarm
- mAhDrawn
) * ((float)osdFlyTime
) / mAhDrawn
);
1426 osdFormatTime(element
->buff
, OSD_TIMER_PREC_SECOND
, remaining_time
);
1430 static void osdElementRssi(osdElementParms_t
*element
)
1432 uint16_t osdRssi
= getRssi() * 100 / 1024; // change range
1433 if (osdRssi
>= 100) {
1437 if (getRssiPercent() < osdConfig()->rssi_alarm
) {
1438 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
1441 tfp_sprintf(element
->buff
, "%c%2d", SYM_RSSI
, osdRssi
);
1445 static void osdElementRtcTime(osdElementParms_t
*element
)
1447 osdFormatRtcDateTime(&element
->buff
[0]);
1449 #endif // USE_RTC_TIME
1451 #ifdef USE_RX_RSSI_DBM
1452 static void osdElementRssiDbm(osdElementParms_t
*element
)
1454 tfp_sprintf(element
->buff
, "%c%3d", SYM_RSSI
, getRssiDbm());
1456 #endif // USE_RX_RSSI_DBM
1459 static void osdElementRsnr(osdElementParms_t
*element
)
1461 tfp_sprintf(element
->buff
, "%c%3d", SYM_RSSI
, getRsnr());
1463 #endif // USE_RX_RSNR
1465 #ifdef USE_OSD_STICK_OVERLAY
1466 static void osdBackgroundStickOverlay(osdElementParms_t
*element
)
1468 const uint8_t xpos
= element
->elemPosX
;
1469 const uint8_t ypos
= element
->elemPosY
;
1471 // Draw the axis first
1472 for (unsigned x
= 0; x
< OSD_STICK_OVERLAY_WIDTH
; x
++) {
1473 for (unsigned y
= 0; y
< OSD_STICK_OVERLAY_HEIGHT
; y
++) {
1474 // draw the axes, vertical and horizonal
1475 if ((x
== ((OSD_STICK_OVERLAY_WIDTH
- 1) / 2)) && (y
== (OSD_STICK_OVERLAY_HEIGHT
- 1) / 2)) {
1476 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_SEVERITY_NORMAL
, SYM_STICK_OVERLAY_CENTER
);
1477 } else if (x
== ((OSD_STICK_OVERLAY_WIDTH
- 1) / 2)) {
1478 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_SEVERITY_NORMAL
, SYM_STICK_OVERLAY_VERTICAL
);
1479 } else if (y
== ((OSD_STICK_OVERLAY_HEIGHT
- 1) / 2)) {
1480 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_SEVERITY_NORMAL
, SYM_STICK_OVERLAY_HORIZONTAL
);
1485 element
->drawElement
= false; // element already drawn
1488 static void osdElementStickOverlay(osdElementParms_t
*element
)
1490 const uint8_t xpos
= element
->elemPosX
;
1491 const uint8_t ypos
= element
->elemPosY
;
1493 // Now draw the cursor
1494 rc_alias_e vertical_channel
, horizontal_channel
;
1496 if (element
->item
== OSD_STICK_OVERLAY_LEFT
) {
1497 vertical_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].left_vertical
;
1498 horizontal_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].left_horizontal
;
1500 vertical_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].right_vertical
;
1501 horizontal_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].right_horizontal
;
1504 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
);
1505 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
);
1506 const char cursor
= SYM_STICK_OVERLAY_SPRITE_HIGH
+ (cursorY
% OSD_STICK_OVERLAY_SPRITE_HEIGHT
);
1508 osdDisplayWriteChar(element
, xpos
+ cursorX
, ypos
+ cursorY
/ OSD_STICK_OVERLAY_SPRITE_HEIGHT
, DISPLAYPORT_SEVERITY_NORMAL
, cursor
);
1510 element
->drawElement
= false; // element already drawn
1512 #endif // USE_OSD_STICK_OVERLAY
1514 static void osdElementThrottlePosition(osdElementParms_t
*element
)
1516 tfp_sprintf(element
->buff
, "%c%3d", SYM_THR
, calculateThrottlePercent());
1519 static void osdElementTimer(osdElementParms_t
*element
)
1521 for (int i
= 0; i
< OSD_TIMER_COUNT
; i
++) {
1522 const uint16_t timer
= osdConfig()->timers
[i
];
1523 const timeUs_t time
= osdGetTimerValue(OSD_TIMER_SRC(timer
));
1524 const timeUs_t alarmTime
= OSD_TIMER_ALARM(timer
) * 60000000; // convert from minutes to us
1525 if (alarmTime
!= 0 && time
>= alarmTime
) {
1526 element
->attr
= DISPLAYPORT_SEVERITY_CRITICAL
;
1530 osdFormatTimer(element
->buff
, true, true, element
->item
- OSD_ITEM_TIMER_1
);
1533 #ifdef USE_VTX_COMMON
1534 static void osdElementVtxChannel(osdElementParms_t
*element
)
1536 const vtxDevice_t
*vtxDevice
= vtxCommonDevice();
1537 const char vtxBandLetter
= vtxCommonLookupBandLetter(vtxDevice
, vtxSettingsConfig()->band
);
1538 const char *vtxChannelName
= vtxCommonLookupChannelName(vtxDevice
, vtxSettingsConfig()->channel
);
1539 unsigned vtxStatus
= 0;
1540 uint8_t vtxPower
= vtxSettingsConfig()->power
;
1542 vtxCommonGetStatus(vtxDevice
, &vtxStatus
);
1544 if (vtxSettingsConfig()->lowPowerDisarm
) {
1545 vtxCommonGetPowerIndex(vtxDevice
, &vtxPower
);
1548 const char *vtxPowerLabel
= vtxCommonLookupPowerName(vtxDevice
, vtxPower
);
1550 char vtxStatusIndicator
= '\0';
1551 if (IS_RC_MODE_ACTIVE(BOXVTXCONTROLDISABLE
)) {
1552 vtxStatusIndicator
= 'D';
1553 } else if (vtxStatus
& VTX_STATUS_PIT_MODE
) {
1554 vtxStatusIndicator
= 'P';
1557 switch (element
->type
) {
1558 case OSD_ELEMENT_TYPE_2
:
1559 tfp_sprintf(element
->buff
, "%s", vtxPowerLabel
);
1563 if (vtxStatus
& VTX_STATUS_LOCKED
) {
1564 tfp_sprintf(element
->buff
, "-:-:-:L");
1565 } else if (vtxStatusIndicator
) {
1566 tfp_sprintf(element
->buff
, "%c:%s:%s:%c", vtxBandLetter
, vtxChannelName
, vtxPowerLabel
, vtxStatusIndicator
);
1568 tfp_sprintf(element
->buff
, "%c:%s:%s", vtxBandLetter
, vtxChannelName
, vtxPowerLabel
);
1573 #endif // USE_VTX_COMMON
1575 static void osdElementAuxValue(osdElementParms_t
*element
)
1577 tfp_sprintf(element
->buff
, "%c%d", osdConfig()->aux_symbol
, osdAuxValue
);
1580 static void osdElementWarnings(osdElementParms_t
*element
)
1582 bool elementBlinking
= false;
1583 renderOsdWarning(element
->buff
, &elementBlinking
, &element
->attr
);
1584 if (elementBlinking
) {
1585 SET_BLINK(OSD_WARNINGS
);
1587 CLR_BLINK(OSD_WARNINGS
);
1590 #ifdef USE_CRAFTNAME_MSGS
1591 // Injects data into the CraftName variable for systems which limit
1592 // the available MSP data field in their OSD.
1593 if (osdConfig()->osd_craftname_msgs
== true) {
1594 // if warning is not set, or blink is off, then display LQ & RSSI
1595 if (blinkState
|| (strlen(element
->buff
) == 0)) {
1596 #ifdef USE_RX_LINK_QUALITY_INFO
1597 // replicate the LQ functionality without the special font symbols
1598 uint16_t osdLinkQuality
= 0;
1599 if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_CRSF
) { // 0-99
1600 osdLinkQuality
= rxGetLinkQuality();
1601 #ifdef USE_RX_RSSI_DBM
1602 const uint8_t osdRfMode
= rxGetRfMode();
1603 tfp_sprintf(element
->buff
, "LQ %2d:%03d %3d", osdRfMode
, osdLinkQuality
, getRssiDbm());
1604 } else if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_GHST
) { // 0-100
1605 osdLinkQuality
= rxGetLinkQuality();
1606 tfp_sprintf(element
->buff
, "LQ %03d %3d", osdLinkQuality
, getRssiDbm());
1609 osdLinkQuality
= rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE
;
1610 if (osdLinkQuality
>= 10) {
1613 tfp_sprintf(element
->buff
, "LQ %1d", osdLinkQuality
);
1615 #endif // USE_RX_LINK_QUALITY_INFO
1617 strncpy(pilotConfigMutable()->craftName
, element
->buff
, MAX_NAME_LENGTH
- 1);
1619 #endif // USE_CRAFTNAME_MSGS
1622 #ifdef USE_MSP_DISPLAYPORT
1623 static void osdElementSys(osdElementParms_t
*element
)
1627 // Nothing to render for a system element
1631 // Define the order in which the elements are drawn.
1632 // Elements positioned later in the list will overlay the earlier
1633 // ones if their character positions overlap
1634 // Elements that need special runtime conditional processing should be added
1635 // to osdAddActiveElements()
1637 static const uint8_t osdElementDisplayOrder
[] = {
1638 OSD_MAIN_BATT_VOLTAGE
,
1641 OSD_HORIZON_SIDEBARS
,
1642 OSD_UP_DOWN_REFERENCE
,
1645 OSD_REMAINING_TIME_ESTIMATE
,
1651 OSD_WATT_HOURS_DRAWN
,
1658 OSD_PIDRATE_PROFILE
,
1660 OSD_AVG_CELL_VOLTAGE
,
1664 OSD_MAIN_BATT_USAGE
,
1666 OSD_NUMERICAL_HEADING
,
1669 OSD_NUMERICAL_VARIO
,
1684 #ifdef USE_OSD_ADJUSTMENTS
1685 OSD_ADJUSTMENT_RANGE
,
1687 #ifdef USE_ADC_INTERNAL
1688 OSD_CORE_TEMPERATURE
,
1690 #ifdef USE_RX_LINK_QUALITY_INFO
1693 #ifdef USE_RX_LINK_UPLINK_POWER
1694 OSD_TX_UPLINK_POWER
,
1696 #ifdef USE_RX_RSSI_DBM
1702 #ifdef USE_OSD_STICK_OVERLAY
1703 OSD_STICK_OVERLAY_LEFT
,
1704 OSD_STICK_OVERLAY_RIGHT
,
1706 #ifdef USE_PROFILE_NAMES
1707 OSD_RATE_PROFILE_NAME
,
1708 OSD_PID_PROFILE_NAME
,
1710 #ifdef USE_OSD_PROFILES
1715 #ifdef USE_PERSISTENT_STATS
1719 OSD_SYS_GOGGLE_VOLTAGE
,
1720 OSD_SYS_VTX_VOLTAGE
,
1732 // Define the mapping between the OSD element id and the function to draw it
1734 const osdElementDrawFn osdElementDrawFunction
[OSD_ITEM_COUNT
] = {
1735 [OSD_CAMERA_FRAME
] = NULL
, // only has background. Added first so it's the lowest "layer" and doesn't cover other elements
1736 [OSD_RSSI_VALUE
] = osdElementRssi
,
1737 [OSD_MAIN_BATT_VOLTAGE
] = osdElementMainBatteryVoltage
,
1738 [OSD_CROSSHAIRS
] = osdElementCrosshairs
, // only has background, but needs to be over other elements (like artificial horizon)
1740 [OSD_ARTIFICIAL_HORIZON
] = osdElementArtificialHorizon
,
1741 [OSD_UP_DOWN_REFERENCE
] = osdElementUpDownReference
,
1743 [OSD_HORIZON_SIDEBARS
] = NULL
, // only has background
1744 [OSD_ITEM_TIMER_1
] = osdElementTimer
,
1745 [OSD_ITEM_TIMER_2
] = osdElementTimer
,
1746 [OSD_FLYMODE
] = osdElementFlymode
,
1747 [OSD_CRAFT_NAME
] = NULL
, // only has background
1748 [OSD_THROTTLE_POS
] = osdElementThrottlePosition
,
1749 #ifdef USE_VTX_COMMON
1750 [OSD_VTX_CHANNEL
] = osdElementVtxChannel
,
1752 [OSD_CURRENT_DRAW
] = osdElementCurrentDraw
,
1753 [OSD_MAH_DRAWN
] = osdElementMahDrawn
,
1754 [OSD_WATT_HOURS_DRAWN
] = osdElementWattHoursDrawn
,
1756 [OSD_GPS_SPEED
] = osdElementGpsSpeed
,
1757 [OSD_GPS_SATS
] = osdElementGpsSats
,
1759 [OSD_ALTITUDE
] = osdElementAltitude
,
1760 [OSD_ROLL_PIDS
] = osdElementPidsRoll
,
1761 [OSD_PITCH_PIDS
] = osdElementPidsPitch
,
1762 [OSD_YAW_PIDS
] = osdElementPidsYaw
,
1763 [OSD_POWER
] = osdElementPower
,
1764 [OSD_PIDRATE_PROFILE
] = osdElementPidRateProfile
,
1765 [OSD_WARNINGS
] = osdElementWarnings
,
1766 [OSD_AVG_CELL_VOLTAGE
] = osdElementAverageCellVoltage
,
1767 [OSD_READY_MODE
] = osdElementReadyMode
,
1769 [OSD_GPS_LON
] = osdElementGpsCoordinate
,
1770 [OSD_GPS_LAT
] = osdElementGpsCoordinate
,
1772 [OSD_DEBUG
] = osdElementDebug
,
1774 [OSD_PITCH_ANGLE
] = osdElementAngleRollPitch
,
1775 [OSD_ROLL_ANGLE
] = osdElementAngleRollPitch
,
1777 [OSD_MAIN_BATT_USAGE
] = osdElementMainBatteryUsage
,
1778 [OSD_DISARMED
] = osdElementDisarmed
,
1780 [OSD_HOME_DIR
] = osdElementGpsHomeDirection
,
1781 [OSD_HOME_DIST
] = osdElementGpsHomeDistance
,
1783 [OSD_NUMERICAL_HEADING
] = osdElementNumericalHeading
,
1785 [OSD_NUMERICAL_VARIO
] = osdElementNumericalVario
,
1787 [OSD_COMPASS_BAR
] = osdElementCompassBar
,
1788 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1789 [OSD_ESC_TMP
] = osdElementEscTemperature
,
1790 [OSD_ESC_RPM
] = osdElementEscRpm
,
1792 [OSD_REMAINING_TIME_ESTIMATE
] = osdElementRemainingTimeEstimate
,
1794 [OSD_RTC_DATETIME
] = osdElementRtcTime
,
1796 #ifdef USE_OSD_ADJUSTMENTS
1797 [OSD_ADJUSTMENT_RANGE
] = osdElementAdjustmentRange
,
1799 #ifdef USE_ADC_INTERNAL
1800 [OSD_CORE_TEMPERATURE
] = osdElementCoreTemperature
,
1802 [OSD_ANTI_GRAVITY
] = osdElementAntiGravity
,
1804 [OSD_G_FORCE
] = osdElementGForce
,
1806 [OSD_MOTOR_DIAG
] = osdElementMotorDiagnostics
,
1808 [OSD_LOG_STATUS
] = osdElementLogStatus
,
1811 [OSD_FLIP_ARROW
] = osdElementCrashFlipArrow
,
1813 #ifdef USE_RX_LINK_QUALITY_INFO
1814 [OSD_LINK_QUALITY
] = osdElementLinkQuality
,
1816 #ifdef USE_RX_LINK_UPLINK_POWER
1817 [OSD_TX_UPLINK_POWER
] = osdElementTxUplinkPower
,
1820 [OSD_FLIGHT_DIST
] = osdElementGpsFlightDistance
,
1822 #ifdef USE_OSD_STICK_OVERLAY
1823 [OSD_STICK_OVERLAY_LEFT
] = osdElementStickOverlay
,
1824 [OSD_STICK_OVERLAY_RIGHT
] = osdElementStickOverlay
,
1826 [OSD_PILOT_NAME
] = NULL
, // only has background
1827 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1828 [OSD_ESC_RPM_FREQ
] = osdElementEscRpmFreq
,
1830 #ifdef USE_PROFILE_NAMES
1831 [OSD_RATE_PROFILE_NAME
] = osdElementRateProfileName
,
1832 [OSD_PID_PROFILE_NAME
] = osdElementPidProfileName
,
1834 #ifdef USE_OSD_PROFILES
1835 [OSD_PROFILE_NAME
] = osdElementOsdProfileName
,
1837 #ifdef USE_RX_RSSI_DBM
1838 [OSD_RSSI_DBM_VALUE
] = osdElementRssiDbm
,
1841 [OSD_RSNR_VALUE
] = osdElementRsnr
,
1843 [OSD_RC_CHANNELS
] = osdElementRcChannels
,
1845 [OSD_EFFICIENCY
] = osdElementEfficiency
,
1847 #ifdef USE_PERSISTENT_STATS
1848 [OSD_TOTAL_FLIGHTS
] = osdElementTotalFlights
,
1850 [OSD_AUX_VALUE
] = osdElementAuxValue
,
1851 #ifdef USE_MSP_DISPLAYPORT
1852 [OSD_SYS_GOGGLE_VOLTAGE
] = osdElementSys
,
1853 [OSD_SYS_VTX_VOLTAGE
] = osdElementSys
,
1854 [OSD_SYS_BITRATE
] = osdElementSys
,
1855 [OSD_SYS_DELAY
] = osdElementSys
,
1856 [OSD_SYS_DISTANCE
] = osdElementSys
,
1857 [OSD_SYS_LQ
] = osdElementSys
,
1858 [OSD_SYS_GOGGLE_DVR
] = osdElementSys
,
1859 [OSD_SYS_VTX_DVR
] = osdElementSys
,
1860 [OSD_SYS_WARNINGS
] = osdElementSys
,
1861 [OSD_SYS_VTX_TEMP
] = osdElementSys
,
1862 [OSD_SYS_FAN_SPEED
] = osdElementSys
,
1866 // Define the mapping between the OSD element id and the function to draw its background (static part)
1867 // Only necessary to define the entries that actually have a background function
1869 const osdElementDrawFn osdElementBackgroundFunction
[OSD_ITEM_COUNT
] = {
1870 [OSD_CAMERA_FRAME
] = osdBackgroundCameraFrame
,
1871 [OSD_HORIZON_SIDEBARS
] = osdBackgroundHorizonSidebars
,
1872 [OSD_CRAFT_NAME
] = osdBackgroundCraftName
,
1873 #ifdef USE_OSD_STICK_OVERLAY
1874 [OSD_STICK_OVERLAY_LEFT
] = osdBackgroundStickOverlay
,
1875 [OSD_STICK_OVERLAY_RIGHT
] = osdBackgroundStickOverlay
,
1877 [OSD_PILOT_NAME
] = osdBackgroundPilotName
,
1880 static void osdAddActiveElement(osd_items_e element
)
1882 if (VISIBLE(osdElementConfig()->item_pos
[element
])) {
1883 activeOsdElementArray
[activeOsdElementCount
++] = element
;
1887 // Examine the elements and build a list of only the active (enabled)
1888 // ones to speed up rendering.
1890 void osdAddActiveElements(void)
1892 activeOsdElementCount
= 0;
1895 if (sensors(SENSOR_ACC
)) {
1896 osdAddActiveElement(OSD_ARTIFICIAL_HORIZON
);
1897 osdAddActiveElement(OSD_G_FORCE
);
1898 osdAddActiveElement(OSD_UP_DOWN_REFERENCE
);
1902 for (unsigned i
= 0; i
< sizeof(osdElementDisplayOrder
); i
++) {
1903 osdAddActiveElement(osdElementDisplayOrder
[i
]);
1907 if (sensors(SENSOR_GPS
)) {
1908 osdAddActiveElement(OSD_GPS_SATS
);
1909 osdAddActiveElement(OSD_GPS_SPEED
);
1910 osdAddActiveElement(OSD_GPS_LAT
);
1911 osdAddActiveElement(OSD_GPS_LON
);
1912 osdAddActiveElement(OSD_HOME_DIST
);
1913 osdAddActiveElement(OSD_HOME_DIR
);
1914 osdAddActiveElement(OSD_FLIGHT_DIST
);
1915 osdAddActiveElement(OSD_EFFICIENCY
);
1919 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1920 if ((featureIsEnabled(FEATURE_ESC_SENSOR
)) || (motorConfig()->dev
.useDshotTelemetry
)) {
1921 osdAddActiveElement(OSD_ESC_TMP
);
1922 osdAddActiveElement(OSD_ESC_RPM
);
1923 osdAddActiveElement(OSD_ESC_RPM_FREQ
);
1927 #ifdef USE_PERSISTENT_STATS
1928 osdAddActiveElement(OSD_TOTAL_FLIGHTS
);
1932 static void osdDrawSingleElement(displayPort_t
*osdDisplayPort
, uint8_t item
)
1934 if (!osdElementDrawFunction
[item
]) {
1935 // Element has no drawing function
1938 if (!osdDisplayPort
->useDeviceBlink
&& BLINK(item
)) {
1942 uint8_t elemPosX
= OSD_X(osdElementConfig()->item_pos
[item
]);
1943 uint8_t elemPosY
= OSD_Y(osdElementConfig()->item_pos
[item
]);
1944 char buff
[OSD_ELEMENT_BUFFER_LENGTH
] = "";
1946 osdElementParms_t element
;
1947 element
.item
= item
;
1948 element
.elemPosX
= elemPosX
;
1949 element
.elemPosY
= elemPosY
;
1950 element
.type
= OSD_TYPE(osdElementConfig()->item_pos
[item
]);
1951 element
.buff
= (char *)&buff
;
1952 element
.osdDisplayPort
= osdDisplayPort
;
1953 element
.drawElement
= true;
1954 element
.attr
= DISPLAYPORT_SEVERITY_NORMAL
;
1956 // Call the element drawing function
1957 if ((item
>= OSD_SYS_GOGGLE_VOLTAGE
) && (item
< OSD_ITEM_COUNT
)) {
1958 displaySys(osdDisplayPort
, elemPosX
, elemPosY
, (displayPortSystemElement_e
)(item
- OSD_SYS_GOGGLE_VOLTAGE
+ DISPLAYPORT_SYS_GOGGLE_VOLTAGE
));
1960 osdElementDrawFunction
[item
](&element
);
1961 if (element
.drawElement
) {
1962 osdDisplayWrite(&element
, elemPosX
, elemPosY
, element
.attr
, buff
);
1967 static void osdDrawSingleElementBackground(displayPort_t
*osdDisplayPort
, uint8_t item
)
1969 if (!osdElementBackgroundFunction
[item
]) {
1970 // Element has no background drawing function
1974 uint8_t elemPosX
= OSD_X(osdElementConfig()->item_pos
[item
]);
1975 uint8_t elemPosY
= OSD_Y(osdElementConfig()->item_pos
[item
]);
1976 char buff
[OSD_ELEMENT_BUFFER_LENGTH
] = "";
1978 osdElementParms_t element
;
1979 element
.item
= item
;
1980 element
.elemPosX
= elemPosX
;
1981 element
.elemPosY
= elemPosY
;
1982 element
.type
= OSD_TYPE(osdElementConfig()->item_pos
[item
]);
1983 element
.buff
= (char *)&buff
;
1984 element
.osdDisplayPort
= osdDisplayPort
;
1985 element
.drawElement
= true;
1987 // Call the element background drawing function
1988 osdElementBackgroundFunction
[item
](&element
);
1989 if (element
.drawElement
) {
1990 osdDisplayWrite(&element
, elemPosX
, elemPosY
, DISPLAYPORT_SEVERITY_NORMAL
, buff
);
1994 static uint8_t activeElement
= 0;
1996 uint8_t osdGetActiveElement(void)
1998 return activeElement
;
2001 uint8_t osdGetActiveElementCount(void)
2003 return activeOsdElementCount
;
2006 // Return true if there are more elements to draw
2007 bool osdDrawNextActiveElement(displayPort_t
*osdDisplayPort
, timeUs_t currentTimeUs
)
2009 UNUSED(currentTimeUs
);
2012 if (activeElement
>= activeOsdElementCount
) {
2016 if (!backgroundLayerSupported
) {
2017 // If the background layer isn't supported then we
2018 // have to draw the element's static layer as well.
2019 osdDrawSingleElementBackground(osdDisplayPort
, activeOsdElementArray
[activeElement
]);
2022 osdDrawSingleElement(osdDisplayPort
, activeOsdElementArray
[activeElement
]);
2024 if (++activeElement
>= activeOsdElementCount
) {
2032 void osdDrawActiveElementsBackground(displayPort_t
*osdDisplayPort
)
2034 if (backgroundLayerSupported
) {
2035 displayLayerSelect(osdDisplayPort
, DISPLAYPORT_LAYER_BACKGROUND
);
2036 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_WAIT
);
2037 for (unsigned i
= 0; i
< activeOsdElementCount
; i
++) {
2038 osdDrawSingleElementBackground(osdDisplayPort
, activeOsdElementArray
[i
]);
2040 displayLayerSelect(osdDisplayPort
, DISPLAYPORT_LAYER_FOREGROUND
);
2044 void osdElementsInit(bool backgroundLayerFlag
)
2046 backgroundLayerSupported
= backgroundLayerFlag
;
2047 activeOsdElementCount
= 0;
2048 pt1FilterInit(&batteryEfficiencyFilt
, pt1FilterGain(EFFICIENCY_CUTOFF_HZ
, 1.0f
/ osdConfig()->framerate_hz
));
2051 void osdSyncBlink(void)
2053 static int blinkCount
= 0;
2055 // If the OSD blink is due a transition, do so
2056 // Task runs at osdConfig()->framerate_hz Hz, so this will cycle at 2Hz
2057 if (++blinkCount
== ((osdConfig()->framerate_hz
/ OSD_BLINK_FREQUENCY_HZ
) / 2)) {
2059 blinkState
= !blinkState
;
2063 void osdResetAlarms(void)
2065 memset(blinkBits
, 0, sizeof(blinkBits
));
2068 void osdUpdateAlarms(void)
2070 // This is overdone?
2072 int32_t alt
= osdGetMetersToSelectedUnit(getEstimatedAltitudeCm()) / 100;
2074 if (getRssiPercent() < osdConfig()->rssi_alarm
) {
2075 SET_BLINK(OSD_RSSI_VALUE
);
2077 CLR_BLINK(OSD_RSSI_VALUE
);
2080 #ifdef USE_RX_LINK_QUALITY_INFO
2081 if (rxGetLinkQualityPercent() < osdConfig()->link_quality_alarm
) {
2082 SET_BLINK(OSD_LINK_QUALITY
);
2084 CLR_BLINK(OSD_LINK_QUALITY
);
2086 #endif // USE_RX_LINK_QUALITY_INFO
2088 if (getBatteryState() == BATTERY_OK
) {
2089 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE
);
2090 CLR_BLINK(OSD_AVG_CELL_VOLTAGE
);
2092 SET_BLINK(OSD_MAIN_BATT_VOLTAGE
);
2093 SET_BLINK(OSD_AVG_CELL_VOLTAGE
);
2097 if ((STATE(GPS_FIX
) == 0) || (gpsSol
.numSat
< GPS_MIN_SAT_COUNT
)
2098 #ifdef USE_GPS_RESCUE
2099 || ((gpsSol
.numSat
< gpsRescueConfig()->minSats
) && gpsRescueIsConfigured())
2102 SET_BLINK(OSD_GPS_SATS
);
2104 CLR_BLINK(OSD_GPS_SATS
);
2108 for (int i
= 0; i
< OSD_TIMER_COUNT
; i
++) {
2109 const uint16_t timer
= osdConfig()->timers
[i
];
2110 const timeUs_t time
= osdGetTimerValue(OSD_TIMER_SRC(timer
));
2111 const timeUs_t alarmTime
= OSD_TIMER_ALARM(timer
) * 60000000; // convert from minutes to us
2112 if (alarmTime
!= 0 && time
>= alarmTime
) {
2113 SET_BLINK(OSD_ITEM_TIMER_1
+ i
);
2115 CLR_BLINK(OSD_ITEM_TIMER_1
+ i
);
2119 if (getMAhDrawn() >= osdConfig()->cap_alarm
) {
2120 SET_BLINK(OSD_MAH_DRAWN
);
2121 SET_BLINK(OSD_MAIN_BATT_USAGE
);
2122 SET_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
2124 CLR_BLINK(OSD_MAH_DRAWN
);
2125 CLR_BLINK(OSD_MAIN_BATT_USAGE
);
2126 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
2129 if ((alt
>= osdConfig()->alt_alarm
) && ARMING_FLAG(ARMED
)) {
2130 SET_BLINK(OSD_ALTITUDE
);
2132 CLR_BLINK(OSD_ALTITUDE
);
2136 if (sensors(SENSOR_GPS
) && ARMING_FLAG(ARMED
) && STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
2137 if (osdConfig()->distance_alarm
&& GPS_distanceToHome
>= osdConfig()->distance_alarm
) {
2138 SET_BLINK(OSD_HOME_DIST
);
2140 CLR_BLINK(OSD_HOME_DIST
);
2143 CLR_BLINK(OSD_HOME_DIST
);
2147 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
2150 #if defined(USE_ESC_SENSOR)
2151 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
2152 // This works because the combined ESC data contains the maximum temperature seen amongst all ESCs
2153 blink
= osdConfig()->esc_temp_alarm
!= ESC_TEMP_ALARM_OFF
&& osdEscDataCombined
->temperature
>= osdConfig()->esc_temp_alarm
;
2156 #if defined(USE_DSHOT_TELEMETRY)
2159 if (osdConfig()->esc_temp_alarm
!= ESC_TEMP_ALARM_OFF
) {
2160 for (uint32_t k
= 0; !blink
&& (k
< getMotorCount()); k
++) {
2161 blink
= (dshotTelemetryState
.motorState
[k
].telemetryTypes
& (1 << DSHOT_TELEMETRY_TYPE_TEMPERATURE
)) != 0 &&
2162 dshotTelemetryState
.motorState
[k
].telemetryData
[DSHOT_TELEMETRY_TYPE_TEMPERATURE
] >= osdConfig()->esc_temp_alarm
;
2171 SET_BLINK(OSD_ESC_TMP
);
2173 CLR_BLINK(OSD_ESC_TMP
);
2179 static bool osdElementIsActive(osd_items_e element
)
2181 for (unsigned i
= 0; i
< activeOsdElementCount
; i
++) {
2182 if (activeOsdElementArray
[i
] == element
) {
2189 // Determine if any active elements need the ACC
2190 bool osdElementsNeedAccelerometer(void)
2192 return osdElementIsActive(OSD_ARTIFICIAL_HORIZON
) ||
2193 osdElementIsActive(OSD_PITCH_ANGLE
) ||
2194 osdElementIsActive(OSD_ROLL_ANGLE
) ||
2195 osdElementIsActive(OSD_G_FORCE
) ||
2196 osdElementIsActive(OSD_FLIP_ARROW
) ||
2197 osdElementIsActive(OSD_UP_DOWN_REFERENCE
);