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_ATTR_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_ATTR_NONE
, 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 if (haveBaro
|| haveGps
) {
667 osdFormatAltitudeString(element
->buff
, getEstimatedAltitudeCm(), element
->type
);
669 element
->buff
[0] = SYM_ALTITUDE
;
670 element
->buff
[1] = SYM_HYPHEN
; // We use this symbol when we don't have a valid measure
671 element
->buff
[2] = '\0';
676 static void osdElementAngleRollPitch(osdElementParms_t
*element
)
678 const float angle
= ((element
->item
== OSD_PITCH_ANGLE
) ? attitude
.values
.pitch
: attitude
.values
.roll
) / 10.0f
;
679 osdPrintFloat(element
->buff
, (element
->item
== OSD_PITCH_ANGLE
) ? SYM_PITCH
: SYM_ROLL
, fabsf(angle
), ((angle
< 0) ? "-%02u" : " %02u"), 1, true, SYM_NONE
);
683 static void osdElementAntiGravity(osdElementParms_t
*element
)
685 if (pidOsdAntiGravityActive()) {
686 strcpy(element
->buff
, "AG");
692 static void osdElementArtificialHorizon(osdElementParms_t
*element
)
694 // Get pitch and roll limits in tenths of degrees
695 const int maxPitch
= osdConfig()->ahMaxPitch
* 10;
696 const int maxRoll
= osdConfig()->ahMaxRoll
* 10;
697 const int ahSign
= osdConfig()->ahInvert
? -1 : 1;
698 const int rollAngle
= constrain(attitude
.values
.roll
* ahSign
, -maxRoll
, maxRoll
);
699 int pitchAngle
= constrain(attitude
.values
.pitch
* ahSign
, -maxPitch
, maxPitch
);
700 // Convert pitchAngle to y compensation value
701 // (maxPitch / 25) divisor matches previous settings of fixed divisor of 8 and fixed max AHI pitch angle of 20.0 degrees
703 pitchAngle
= ((pitchAngle
* 25) / maxPitch
);
705 pitchAngle
-= 41; // 41 = 4 * AH_SYMBOL_COUNT + 5
707 for (int x
= -4; x
<= 4; x
++) {
708 const int y
= ((-rollAngle
* x
) / 64) - pitchAngle
;
709 if (y
>= 0 && y
<= 81) {
710 osdDisplayWriteChar(element
, element
->elemPosX
+ x
, element
->elemPosY
+ (y
/ AH_SYMBOL_COUNT
), DISPLAYPORT_ATTR_NONE
, (SYM_AH_BAR9_0
+ (y
% AH_SYMBOL_COUNT
)));
714 element
->drawElement
= false; // element already drawn
717 static void osdElementUpDownReference(osdElementParms_t
*element
)
719 // Up/Down reference feature displays reference points on the OSD at Zenith and Nadir
720 const float earthUpinBodyFrame
[3] = {-rMat
[2][0], -rMat
[2][1], -rMat
[2][2]}; //transforum the up vector to the body frame
722 if (ABS(earthUpinBodyFrame
[2]) < SINE_25_DEG
&& ABS(earthUpinBodyFrame
[1]) < SINE_25_DEG
) {
723 float thetaB
; // pitch from body frame to zenith/nadir
724 float psiB
; // psi from body frame to zenith/nadir
725 char *symbol
[2] = {"U", "D"}; // character buffer
728 if(attitude
.values
.pitch
>0.0){ //nose down
729 thetaB
= -earthUpinBodyFrame
[2]; // get pitch w/re to nadir (use small angle approx for sine)
730 psiB
= -earthUpinBodyFrame
[1]; // calculate the yaw w/re to nadir (use small angle approx for sine)
733 thetaB
= earthUpinBodyFrame
[2]; // get pitch w/re to zenith (use small angle approx for sine)
734 psiB
= earthUpinBodyFrame
[1]; // calculate the yaw w/re to zenith (use small angle approx for sine)
737 int posX
= element
->elemPosX
+ lrintf(scaleRangef(psiB
, -M_PIf
/ 4, M_PIf
/ 4, -14, 14));
738 int posY
= element
->elemPosY
+ lrintf(scaleRangef(thetaB
, -M_PIf
/ 4, M_PIf
/ 4, -8, 8));
740 osdDisplayWrite(element
, posX
, posY
, DISPLAYPORT_ATTR_NONE
, symbol
[direction
]);
742 element
->drawElement
= false; // element already drawn
746 static void osdElementAverageCellVoltage(osdElementParms_t
*element
)
748 const int cellV
= getBatteryAverageCellVoltage();
749 osdPrintFloat(element
->buff
, osdGetBatterySymbol(cellV
), cellV
/ 100.0f
, "", 2, false, SYM_VOLT
);
752 static void osdElementCompassBar(osdElementParms_t
*element
)
754 memcpy(element
->buff
, compassBar
+ osdGetHeadingIntoDiscreteDirections(DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
), 16), 9);
755 element
->buff
[9] = 0;
758 #ifdef USE_ADC_INTERNAL
759 static void osdElementCoreTemperature(osdElementParms_t
*element
)
761 tfp_sprintf(element
->buff
, "C%c%3d%c", SYM_TEMPERATURE
, osdConvertTemperatureToSelectedUnit(getCoreTemperatureCelsius()), osdGetTemperatureSymbolForSelectedUnit());
763 #endif // USE_ADC_INTERNAL
765 static void osdBackgroundCameraFrame(osdElementParms_t
*element
)
767 const uint8_t xpos
= element
->elemPosX
;
768 const uint8_t ypos
= element
->elemPosY
;
769 const uint8_t width
= constrain(osdConfig()->camera_frame_width
, OSD_CAMERA_FRAME_MIN_WIDTH
, OSD_CAMERA_FRAME_MAX_WIDTH
);
770 const uint8_t height
= constrain(osdConfig()->camera_frame_height
, OSD_CAMERA_FRAME_MIN_HEIGHT
, OSD_CAMERA_FRAME_MAX_HEIGHT
);
772 element
->buff
[0] = SYM_STICK_OVERLAY_CENTER
;
773 for (int i
= 1; i
< (width
- 1); i
++) {
774 element
->buff
[i
] = SYM_STICK_OVERLAY_HORIZONTAL
;
776 element
->buff
[width
- 1] = SYM_STICK_OVERLAY_CENTER
;
777 element
->buff
[width
] = 0; // string terminator
779 osdDisplayWrite(element
, xpos
, ypos
, DISPLAYPORT_ATTR_NONE
, element
->buff
);
780 for (int i
= 1; i
< (height
- 1); i
++) {
781 osdDisplayWriteChar(element
, xpos
, ypos
+ i
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_VERTICAL
);
782 osdDisplayWriteChar(element
, xpos
+ width
- 1, ypos
+ i
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_VERTICAL
);
784 osdDisplayWrite(element
, xpos
, ypos
+ height
- 1, DISPLAYPORT_ATTR_NONE
, element
->buff
);
786 element
->drawElement
= false; // element already drawn
789 static void toUpperCase(char* dest
, const char* src
, unsigned int maxSrcLength
)
792 for (i
= 0; i
< maxSrcLength
&& src
[i
]; i
++) {
793 dest
[i
] = toupper((unsigned char)src
[i
]);
798 static void osdBackgroundCraftName(osdElementParms_t
*element
)
800 if (strlen(pilotConfig()->name
) == 0) {
801 strcpy(element
->buff
, "CRAFT_NAME");
803 toUpperCase(element
->buff
, pilotConfig()->name
, MAX_NAME_LENGTH
);
808 static void osdElementCrashFlipArrow(osdElementParms_t
*element
)
810 int rollAngle
= attitude
.values
.roll
/ 10;
811 const int pitchAngle
= attitude
.values
.pitch
/ 10;
812 if (abs(rollAngle
) > 90) {
813 rollAngle
= (rollAngle
< 0 ? -180 : 180) - rollAngle
;
816 if ((isFlipOverAfterCrashActive() || (!ARMING_FLAG(ARMED
) && !isUpright())) && !((imuConfig()->small_angle
< 180 && isUpright()) || (rollAngle
== 0 && pitchAngle
== 0))) {
817 if (abs(pitchAngle
) < 2 * abs(rollAngle
) && abs(rollAngle
) < 2 * abs(pitchAngle
)) {
818 if (pitchAngle
> 0) {
820 element
->buff
[0] = SYM_ARROW_WEST
+ 2;
822 element
->buff
[0] = SYM_ARROW_EAST
- 2;
826 element
->buff
[0] = SYM_ARROW_WEST
- 2;
828 element
->buff
[0] = SYM_ARROW_EAST
+ 2;
832 if (abs(pitchAngle
) > abs(rollAngle
)) {
833 if (pitchAngle
> 0) {
834 element
->buff
[0] = SYM_ARROW_SOUTH
;
836 element
->buff
[0] = SYM_ARROW_NORTH
;
840 element
->buff
[0] = SYM_ARROW_WEST
;
842 element
->buff
[0] = SYM_ARROW_EAST
;
846 element
->buff
[1] = '\0';
851 static void osdElementCrosshairs(osdElementParms_t
*element
)
853 element
->buff
[0] = SYM_AH_CENTER_LINE
;
854 element
->buff
[1] = SYM_AH_CENTER
;
855 element
->buff
[2] = SYM_AH_CENTER_LINE_RIGHT
;
856 element
->buff
[3] = 0;
859 static void osdElementCurrentDraw(osdElementParms_t
*element
)
861 const float amperage
= fabsf(getAmperage() / 100.0f
);
862 osdPrintFloat(element
->buff
, SYM_NONE
, amperage
, "%3u", 2, false, SYM_AMP
);
865 static void osdElementDebug(osdElementParms_t
*element
)
867 tfp_sprintf(element
->buff
, "DBG %5d %5d %5d %5d", debug
[0], debug
[1], debug
[2], debug
[3]);
870 static void osdElementDisarmed(osdElementParms_t
*element
)
872 if (!ARMING_FLAG(ARMED
)) {
873 tfp_sprintf(element
->buff
, "DISARMED");
877 static void osdBackgroundDisplayName(osdElementParms_t
*element
)
879 if (strlen(pilotConfig()->displayName
) == 0) {
880 strcpy(element
->buff
, "DISPLAY_NAME");
882 toUpperCase(element
->buff
, pilotConfig()->displayName
, MAX_NAME_LENGTH
);
886 #ifdef USE_PERSISTENT_STATS
887 static void osdElementTotalFlights(osdElementParms_t
*element
)
889 const int32_t total_flights
= statsConfig()->stats_total_flights
;
890 tfp_sprintf(element
->buff
, "#%d", total_flights
);
894 #ifdef USE_PROFILE_NAMES
895 static void osdElementRateProfileName(osdElementParms_t
*element
)
897 if (strlen(currentControlRateProfile
->profileName
) == 0) {
898 tfp_sprintf(element
->buff
, "RATE_%u", getCurrentControlRateProfileIndex() + 1);
900 toUpperCase(element
->buff
, currentControlRateProfile
->profileName
, MAX_PROFILE_NAME_LENGTH
);
904 static void osdElementPidProfileName(osdElementParms_t
*element
)
906 if (strlen(currentPidProfile
->profileName
) == 0) {
907 tfp_sprintf(element
->buff
, "PID_%u", getCurrentPidProfileIndex() + 1);
909 toUpperCase(element
->buff
, currentPidProfile
->profileName
, MAX_PROFILE_NAME_LENGTH
);
914 #ifdef USE_OSD_PROFILES
915 static void osdElementOsdProfileName(osdElementParms_t
*element
)
917 uint8_t profileIndex
= getCurrentOsdProfileIndex();
919 if (strlen(osdConfig()->profile
[profileIndex
- 1]) == 0) {
920 tfp_sprintf(element
->buff
, "OSD_%u", profileIndex
);
922 toUpperCase(element
->buff
, osdConfig()->profile
[profileIndex
- 1], OSD_PROFILE_NAME_LENGTH
);
927 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
929 static void osdElementEscTemperature(osdElementParms_t
*element
)
931 #if defined(USE_ESC_SENSOR)
932 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
933 tfp_sprintf(element
->buff
, "E%c%3d%c", SYM_TEMPERATURE
, osdConvertTemperatureToSelectedUnit(osdEscDataCombined
->temperature
), osdGetTemperatureSymbolForSelectedUnit());
936 #if defined(USE_DSHOT_TELEMETRY)
938 uint32_t osdEleIx
= tfp_sprintf(element
->buff
, "E%c", SYM_TEMPERATURE
);
940 for (uint8_t k
= 0; k
< getMotorCount(); k
++) {
941 if ((dshotTelemetryState
.motorState
[k
].telemetryTypes
& (1 << DSHOT_TELEMETRY_TYPE_TEMPERATURE
)) != 0) {
942 osdEleIx
+= tfp_sprintf(element
->buff
+ osdEleIx
, "%3d%c",
943 osdConvertTemperatureToSelectedUnit(dshotTelemetryState
.motorState
[k
].telemetryData
[DSHOT_TELEMETRY_TYPE_TEMPERATURE
]),
944 osdGetTemperatureSymbolForSelectedUnit());
946 osdEleIx
+= tfp_sprintf(element
->buff
+ osdEleIx
, " 0%c", osdGetTemperatureSymbolForSelectedUnit());
955 static void osdElementEscRpm(osdElementParms_t
*element
)
957 renderOsdEscRpmOrFreq(&getEscRpm
,element
);
960 static void osdElementEscRpmFreq(osdElementParms_t
*element
)
962 renderOsdEscRpmOrFreq(&getEscRpmFreq
,element
);
967 static void osdElementFlymode(osdElementParms_t
*element
)
969 // Note that flight mode display has precedence in what to display.
972 // 3. ANGLE, HORIZON, ACRO TRAINER
976 if (FLIGHT_MODE(FAILSAFE_MODE
)) {
977 strcpy(element
->buff
, "!FS!");
978 } else if (FLIGHT_MODE(GPS_RESCUE_MODE
)) {
979 strcpy(element
->buff
, "RESC");
980 } else if (FLIGHT_MODE(HEADFREE_MODE
)) {
981 strcpy(element
->buff
, "HEAD");
982 } else if (FLIGHT_MODE(ANGLE_MODE
)) {
983 strcpy(element
->buff
, "ANGL");
984 } else if (FLIGHT_MODE(HORIZON_MODE
)) {
985 strcpy(element
->buff
, "HOR ");
986 } else if (IS_RC_MODE_ACTIVE(BOXACROTRAINER
)) {
987 strcpy(element
->buff
, "ATRN");
988 } else if (airmodeIsEnabled()) {
989 strcpy(element
->buff
, "AIR ");
991 strcpy(element
->buff
, "ACRO");
996 static void osdElementGForce(osdElementParms_t
*element
)
998 osdPrintFloat(element
->buff
, SYM_NONE
, osdGForce
, "", 1, true, 'G');
1003 static void osdElementGpsFlightDistance(osdElementParms_t
*element
)
1005 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1006 osdFormatDistanceString(element
->buff
, GPS_distanceFlownInCm
/ 100, SYM_TOTAL_DISTANCE
);
1008 // We use this symbol when we don't have a FIX
1009 tfp_sprintf(element
->buff
, "%c%c", SYM_TOTAL_DISTANCE
, SYM_HYPHEN
);
1013 static void osdElementGpsHomeDirection(osdElementParms_t
*element
)
1015 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1016 if (GPS_distanceToHome
> 0) {
1017 const int h
= DECIDEGREES_TO_DEGREES(GPS_directionToHome
- attitude
.values
.yaw
);
1018 element
->buff
[0] = osdGetDirectionSymbolFromHeading(h
);
1020 element
->buff
[0] = SYM_OVER_HOME
;
1024 // We use this symbol when we don't have a FIX
1025 element
->buff
[0] = SYM_HYPHEN
;
1028 element
->buff
[1] = 0;
1031 static void osdElementGpsHomeDistance(osdElementParms_t
*element
)
1033 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1034 osdFormatDistanceString(element
->buff
, GPS_distanceToHome
, SYM_HOMEFLAG
);
1036 element
->buff
[0] = SYM_HOMEFLAG
;
1037 // We use this symbol when we don't have a FIX
1038 element
->buff
[1] = SYM_HYPHEN
;
1039 element
->buff
[2] = '\0';
1043 static void osdElementGpsCoordinate(osdElementParms_t
*element
)
1045 const gpsCoordinateType_e coordinateType
= (element
->item
== OSD_GPS_LON
) ? GPS_LONGITUDE
: GPS_LATITUDE
;
1046 osdFormatCoordinate(element
->buff
, coordinateType
, element
->type
);
1047 if (STATE(GPS_FIX_EVER
) && !STATE(GPS_FIX
)) {
1048 SET_BLINK(element
->item
); // blink if we had a fix but have since lost it
1050 CLR_BLINK(element
->item
);
1054 static void osdElementGpsSats(osdElementParms_t
*element
)
1056 if (!gpsIsHealthy()) {
1057 tfp_sprintf(element
->buff
, "%c%cNC", SYM_SAT_L
, SYM_SAT_R
);
1059 int pos
= tfp_sprintf(element
->buff
, "%c%c%2d", SYM_SAT_L
, SYM_SAT_R
, gpsSol
.numSat
);
1060 if (osdConfig()->gps_sats_show_hdop
) { // add on the GPS module HDOP estimate
1061 element
->buff
[pos
++] = ' ';
1062 osdPrintFloat(element
->buff
+ pos
, SYM_NONE
, gpsSol
.hdop
/ 100.0f
, "", 1, true, SYM_NONE
);
1067 static void osdElementGpsSpeed(osdElementParms_t
*element
)
1069 if (STATE(GPS_FIX
)) {
1070 tfp_sprintf(element
->buff
, "%c%3d%c", SYM_SPEED
, osdGetSpeedToSelectedUnit(gpsConfig()->gps_use_3d_speed
? gpsSol
.speed3d
: gpsSol
.groundSpeed
), osdGetSpeedToSelectedUnitSymbol());
1072 tfp_sprintf(element
->buff
, "%c%c%c", SYM_SPEED
, SYM_HYPHEN
, osdGetSpeedToSelectedUnitSymbol());
1076 static void osdElementEfficiency(osdElementParms_t
*element
)
1079 if (sensors(SENSOR_GPS
) && ARMING_FLAG(ARMED
) && STATE(GPS_FIX
) && gpsSol
.groundSpeed
>= EFFICIENCY_MINIMUM_SPEED_CM_S
) {
1080 const float speed
= (float)osdGetSpeedToSelectedUnit(gpsSol
.groundSpeed
);
1081 const float mAmperage
= (float)getAmperage() * 10.f
; // Current in mA
1082 efficiency
= lrintf(pt1FilterApply(&batteryEfficiencyFilt
, (mAmperage
/ speed
)));
1085 const char unitSymbol
= osdConfig()->units
== UNIT_IMPERIAL
? SYM_MILES
: SYM_KM
;
1086 if (efficiency
> 0 && efficiency
<= 9999) {
1087 tfp_sprintf(element
->buff
, "%4d%c/%c", efficiency
, SYM_MAH
, unitSymbol
);
1089 tfp_sprintf(element
->buff
, "----%c/%c", SYM_MAH
, unitSymbol
);
1094 static void osdBackgroundHorizonSidebars(osdElementParms_t
*element
)
1097 const int8_t hudwidth
= AH_SIDEBAR_WIDTH_POS
;
1098 const int8_t hudheight
= AH_SIDEBAR_HEIGHT_POS
;
1099 for (int y
= -hudheight
; y
<= hudheight
; y
++) {
1100 osdDisplayWriteChar(element
, element
->elemPosX
- hudwidth
, element
->elemPosY
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_AH_DECORATION
);
1101 osdDisplayWriteChar(element
, element
->elemPosX
+ hudwidth
, element
->elemPosY
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_AH_DECORATION
);
1104 // AH level indicators
1105 osdDisplayWriteChar(element
, element
->elemPosX
- hudwidth
+ 1, element
->elemPosY
, DISPLAYPORT_ATTR_NONE
, SYM_AH_LEFT
);
1106 osdDisplayWriteChar(element
, element
->elemPosX
+ hudwidth
- 1, element
->elemPosY
, DISPLAYPORT_ATTR_NONE
, SYM_AH_RIGHT
);
1108 element
->drawElement
= false; // element already drawn
1111 #ifdef USE_RX_LINK_QUALITY_INFO
1112 static void osdElementLinkQuality(osdElementParms_t
*element
)
1114 uint16_t osdLinkQuality
= 0;
1115 if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_CRSF
) { // 0-99
1116 osdLinkQuality
= rxGetLinkQuality();
1117 const uint8_t osdRfMode
= rxGetRfMode();
1118 tfp_sprintf(element
->buff
, "%c%1d:%2d", SYM_LINK_QUALITY
, osdRfMode
, osdLinkQuality
);
1119 } else if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_GHST
) { // 0-100
1120 osdLinkQuality
= rxGetLinkQuality();
1121 tfp_sprintf(element
->buff
, "%c%2d", SYM_LINK_QUALITY
, osdLinkQuality
);
1123 osdLinkQuality
= rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE
;
1124 if (osdLinkQuality
>= 10) {
1127 tfp_sprintf(element
->buff
, "%c%1d", SYM_LINK_QUALITY
, osdLinkQuality
);
1130 #endif // USE_RX_LINK_QUALITY_INFO
1132 #ifdef USE_RX_LINK_UPLINK_POWER
1133 static void osdElementTxUplinkPower(osdElementParms_t
*element
)
1135 const uint16_t osdUplinkTxPowerMw
= rxGetUplinkTxPwrMw();
1136 if (osdUplinkTxPowerMw
< 1000) {
1137 tfp_sprintf(element
->buff
, "%c%3dMW", SYM_RSSI
, osdUplinkTxPowerMw
);
1139 osdPrintFloat(element
->buff
, SYM_RSSI
, osdUplinkTxPowerMw
/ 1000.0f
, "", 1, false, 'W');
1142 #endif // USE_RX_LINK_UPLINK_POWER
1145 static void osdElementLogStatus(osdElementParms_t
*element
)
1147 if (IS_RC_MODE_ACTIVE(BOXBLACKBOX
)) {
1148 if (!isBlackboxDeviceWorking()) {
1149 tfp_sprintf(element
->buff
, "%c!", SYM_BBLOG
);
1150 } else if (isBlackboxDeviceFull()) {
1151 tfp_sprintf(element
->buff
, "%c>", SYM_BBLOG
);
1153 int32_t logNumber
= blackboxGetLogNumber();
1154 if (logNumber
>= 0) {
1155 tfp_sprintf(element
->buff
, "%c%d", SYM_BBLOG
, logNumber
);
1157 tfp_sprintf(element
->buff
, "%c", SYM_BBLOG
);
1162 #endif // USE_BLACKBOX
1164 static void osdElementMahDrawn(osdElementParms_t
*element
)
1166 tfp_sprintf(element
->buff
, "%4d%c", getMAhDrawn(), SYM_MAH
);
1169 static void osdElementWattHoursDrawn(osdElementParms_t
*element
)
1171 const float wattHoursDrawn
= getWhDrawn();
1173 if (wattHoursDrawn
< 1.0f
) {
1174 tfp_sprintf(element
->buff
, "%3dMWH", lrintf(wattHoursDrawn
* 1000));
1176 int wattHourWholeNumber
= (int)wattHoursDrawn
;
1177 int wattHourDecimalValue
= (int)((wattHoursDrawn
- wattHourWholeNumber
) * 100);
1179 tfp_sprintf(element
->buff
, wattHourDecimalValue
>= 10 ? "%3d.%2dWH" : "%3d.0%1dWH", wattHourWholeNumber
, wattHourDecimalValue
);
1183 static void osdElementMainBatteryUsage(osdElementParms_t
*element
)
1185 // Set length of indicator bar
1186 #define MAIN_BATT_USAGE_STEPS 11 // Use an odd number so the bar can be centered.
1188 const int usedCapacity
= getMAhDrawn();
1189 int displayBasis
= usedCapacity
;
1191 switch (element
->type
) {
1192 case OSD_ELEMENT_TYPE_3
: // mAh remaining percentage (counts down as battery is used)
1193 displayBasis
= constrain(batteryConfig()->batteryCapacity
- usedCapacity
, 0, batteryConfig()->batteryCapacity
);
1196 case OSD_ELEMENT_TYPE_4
: // mAh used percentage (counts up as battery is used)
1198 int displayPercent
= 0;
1199 if (batteryConfig()->batteryCapacity
) {
1200 displayPercent
= constrain(lrintf(100.0f
* displayBasis
/ batteryConfig()->batteryCapacity
), 0, 100);
1202 tfp_sprintf(element
->buff
, "%c%d%%", SYM_MAH
, displayPercent
);
1206 case OSD_ELEMENT_TYPE_2
: // mAh used graphical progress bar (grows as battery is used)
1207 displayBasis
= constrain(batteryConfig()->batteryCapacity
- usedCapacity
, 0, batteryConfig()->batteryCapacity
);
1210 case OSD_ELEMENT_TYPE_1
: // mAh remaining graphical progress bar (shrinks as battery is used)
1213 uint8_t remainingCapacityBars
= 0;
1215 if (batteryConfig()->batteryCapacity
) {
1216 const float batteryRemaining
= constrain(batteryConfig()->batteryCapacity
- displayBasis
, 0, batteryConfig()->batteryCapacity
);
1217 remainingCapacityBars
= ceilf((batteryRemaining
/ (batteryConfig()->batteryCapacity
/ MAIN_BATT_USAGE_STEPS
)));
1220 // Create empty battery indicator bar
1221 element
->buff
[0] = SYM_PB_START
;
1222 for (int i
= 1; i
<= MAIN_BATT_USAGE_STEPS
; i
++) {
1223 element
->buff
[i
] = i
<= remainingCapacityBars
? SYM_PB_FULL
: SYM_PB_EMPTY
;
1225 element
->buff
[MAIN_BATT_USAGE_STEPS
+ 1] = SYM_PB_CLOSE
;
1226 if (remainingCapacityBars
> 0 && remainingCapacityBars
< MAIN_BATT_USAGE_STEPS
) {
1227 element
->buff
[1 + remainingCapacityBars
] = SYM_PB_END
;
1229 element
->buff
[MAIN_BATT_USAGE_STEPS
+2] = '\0';
1235 static void osdElementMainBatteryVoltage(osdElementParms_t
*element
)
1237 unsigned decimalPlaces
;
1238 const float batteryVoltage
= getBatteryVoltage() / 100.0f
;
1240 if (batteryVoltage
>= 10) { // if voltage is 10v or more then display only 1 decimal place
1245 osdPrintFloat(element
->buff
, osdGetBatterySymbol(getBatteryAverageCellVoltage()), batteryVoltage
, "", decimalPlaces
, true, SYM_VOLT
);
1248 static void osdElementMotorDiagnostics(osdElementParms_t
*element
)
1251 const bool motorsRunning
= areMotorsRunning();
1252 for (; i
< getMotorCount(); i
++) {
1253 if (motorsRunning
) {
1254 element
->buff
[i
] = 0x88 - scaleRange(motor
[i
], getMotorOutputLow(), getMotorOutputHigh(), 0, 8);
1255 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
1256 if (getEscRpm(i
) < MOTOR_STOPPED_THRESHOLD_RPM
) {
1257 // Motor is not spinning properly. Mark as Stopped
1258 element
->buff
[i
] = 'S';
1262 element
->buff
[i
] = 0x88;
1265 element
->buff
[i
] = '\0';
1268 static void osdElementNumericalHeading(osdElementParms_t
*element
)
1270 const int heading
= DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
1271 tfp_sprintf(element
->buff
, "%c%03d", osdGetDirectionSymbolFromHeading(heading
), heading
);
1275 static void osdElementNumericalVario(osdElementParms_t
*element
)
1277 bool haveBaro
= false;
1278 bool haveGps
= false;
1280 haveBaro
= sensors(SENSOR_BARO
);
1283 haveGps
= sensors(SENSOR_GPS
) && STATE(GPS_FIX
);
1285 if (haveBaro
|| haveGps
) {
1286 const float verticalSpeed
= osdGetMetersToSelectedUnit(getEstimatedVario()) / 100.0f
;
1287 const char directionSymbol
= verticalSpeed
< 0 ? SYM_ARROW_SMALL_DOWN
: SYM_ARROW_SMALL_UP
;
1288 osdPrintFloat(element
->buff
, directionSymbol
, fabsf(verticalSpeed
), "", 1, true, osdGetVarioToSelectedUnitSymbol());
1290 // We use this symbol when we don't have a valid measure
1291 element
->buff
[0] = SYM_HYPHEN
;
1292 element
->buff
[1] = '\0';
1297 static void osdElementPidRateProfile(osdElementParms_t
*element
)
1299 tfp_sprintf(element
->buff
, "%d-%d", getCurrentPidProfileIndex() + 1, getCurrentControlRateProfileIndex() + 1);
1302 static void osdElementPidsPitch(osdElementParms_t
*element
)
1304 osdFormatPID(element
->buff
, "PIT", ¤tPidProfile
->pid
[PID_PITCH
]);
1307 static void osdElementPidsRoll(osdElementParms_t
*element
)
1309 osdFormatPID(element
->buff
, "ROL", ¤tPidProfile
->pid
[PID_ROLL
]);
1312 static void osdElementPidsYaw(osdElementParms_t
*element
)
1314 osdFormatPID(element
->buff
, "YAW", ¤tPidProfile
->pid
[PID_YAW
]);
1317 static void osdElementPower(osdElementParms_t
*element
)
1319 tfp_sprintf(element
->buff
, "%4dW", getAmperage() * getBatteryVoltage() / 10000);
1322 static void osdElementRcChannels(osdElementParms_t
*element
)
1324 const uint8_t xpos
= element
->elemPosX
;
1325 const uint8_t ypos
= element
->elemPosY
;
1327 for (int i
= 0; i
< OSD_RCCHANNELS_COUNT
; i
++) {
1328 if (osdConfig()->rcChannels
[i
] >= 0) {
1329 // Translate (1000, 2000) to (-1000, 1000)
1330 int data
= scaleRange(rcData
[osdConfig()->rcChannels
[i
]], PWM_RANGE_MIN
, PWM_RANGE_MAX
, -1000, 1000);
1331 // Opt for the simplest formatting for now.
1332 // Decimal notation can be added when tfp_sprintf supports float among fancy options.
1334 tfp_sprintf(fmtbuf
, "%5d", data
);
1335 osdDisplayWrite(element
, xpos
, ypos
+ i
, DISPLAYPORT_ATTR_NONE
, fmtbuf
);
1339 element
->drawElement
= false; // element already drawn
1342 static void osdElementRemainingTimeEstimate(osdElementParms_t
*element
)
1344 const int mAhDrawn
= getMAhDrawn();
1346 if (mAhDrawn
<= 0.1 * osdConfig()->cap_alarm
) { // also handles the mAhDrawn == 0 condition
1347 tfp_sprintf(element
->buff
, "--:--");
1348 } else if (mAhDrawn
> osdConfig()->cap_alarm
) {
1349 tfp_sprintf(element
->buff
, "00:00");
1351 const int remaining_time
= (int)((osdConfig()->cap_alarm
- mAhDrawn
) * ((float)osdFlyTime
) / mAhDrawn
);
1352 osdFormatTime(element
->buff
, OSD_TIMER_PREC_SECOND
, remaining_time
);
1356 static void osdElementRssi(osdElementParms_t
*element
)
1358 uint16_t osdRssi
= getRssi() * 100 / 1024; // change range
1359 if (osdRssi
>= 100) {
1363 tfp_sprintf(element
->buff
, "%c%2d", SYM_RSSI
, osdRssi
);
1367 static void osdElementRtcTime(osdElementParms_t
*element
)
1369 osdFormatRtcDateTime(&element
->buff
[0]);
1371 #endif // USE_RTC_TIME
1373 #ifdef USE_RX_RSSI_DBM
1374 static void osdElementRssiDbm(osdElementParms_t
*element
)
1376 tfp_sprintf(element
->buff
, "%c%3d", SYM_RSSI
, getRssiDbm());
1378 #endif // USE_RX_RSSI_DBM
1380 #ifdef USE_OSD_STICK_OVERLAY
1381 static void osdBackgroundStickOverlay(osdElementParms_t
*element
)
1383 const uint8_t xpos
= element
->elemPosX
;
1384 const uint8_t ypos
= element
->elemPosY
;
1386 // Draw the axis first
1387 for (unsigned x
= 0; x
< OSD_STICK_OVERLAY_WIDTH
; x
++) {
1388 for (unsigned y
= 0; y
< OSD_STICK_OVERLAY_HEIGHT
; y
++) {
1389 // draw the axes, vertical and horizonal
1390 if ((x
== ((OSD_STICK_OVERLAY_WIDTH
- 1) / 2)) && (y
== (OSD_STICK_OVERLAY_HEIGHT
- 1) / 2)) {
1391 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_CENTER
);
1392 } else if (x
== ((OSD_STICK_OVERLAY_WIDTH
- 1) / 2)) {
1393 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_VERTICAL
);
1394 } else if (y
== ((OSD_STICK_OVERLAY_HEIGHT
- 1) / 2)) {
1395 osdDisplayWriteChar(element
, xpos
+ x
, ypos
+ y
, DISPLAYPORT_ATTR_NONE
, SYM_STICK_OVERLAY_HORIZONTAL
);
1400 element
->drawElement
= false; // element already drawn
1403 static void osdElementStickOverlay(osdElementParms_t
*element
)
1405 const uint8_t xpos
= element
->elemPosX
;
1406 const uint8_t ypos
= element
->elemPosY
;
1408 // Now draw the cursor
1409 rc_alias_e vertical_channel
, horizontal_channel
;
1411 if (element
->item
== OSD_STICK_OVERLAY_LEFT
) {
1412 vertical_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].left_vertical
;
1413 horizontal_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].left_horizontal
;
1415 vertical_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].right_vertical
;
1416 horizontal_channel
= radioModes
[osdConfig()->overlay_radio_mode
-1].right_horizontal
;
1419 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
);
1420 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
);
1421 const char cursor
= SYM_STICK_OVERLAY_SPRITE_HIGH
+ (cursorY
% OSD_STICK_OVERLAY_SPRITE_HEIGHT
);
1423 osdDisplayWriteChar(element
, xpos
+ cursorX
, ypos
+ cursorY
/ OSD_STICK_OVERLAY_SPRITE_HEIGHT
, DISPLAYPORT_ATTR_NONE
, cursor
);
1425 element
->drawElement
= false; // element already drawn
1427 #endif // USE_OSD_STICK_OVERLAY
1429 static void osdElementThrottlePosition(osdElementParms_t
*element
)
1431 tfp_sprintf(element
->buff
, "%c%3d", SYM_THR
, calculateThrottlePercent());
1434 static void osdElementTimer(osdElementParms_t
*element
)
1436 osdFormatTimer(element
->buff
, true, true, element
->item
- OSD_ITEM_TIMER_1
);
1439 #ifdef USE_VTX_COMMON
1440 static void osdElementVtxChannel(osdElementParms_t
*element
)
1442 const vtxDevice_t
*vtxDevice
= vtxCommonDevice();
1443 const char vtxBandLetter
= vtxCommonLookupBandLetter(vtxDevice
, vtxSettingsConfig()->band
);
1444 const char *vtxChannelName
= vtxCommonLookupChannelName(vtxDevice
, vtxSettingsConfig()->channel
);
1445 unsigned vtxStatus
= 0;
1446 uint8_t vtxPower
= vtxSettingsConfig()->power
;
1448 vtxCommonGetStatus(vtxDevice
, &vtxStatus
);
1450 if (vtxSettingsConfig()->lowPowerDisarm
) {
1451 vtxCommonGetPowerIndex(vtxDevice
, &vtxPower
);
1454 const char *vtxPowerLabel
= vtxCommonLookupPowerName(vtxDevice
, vtxPower
);
1456 char vtxStatusIndicator
= '\0';
1457 if (IS_RC_MODE_ACTIVE(BOXVTXCONTROLDISABLE
)) {
1458 vtxStatusIndicator
= 'D';
1459 } else if (vtxStatus
& VTX_STATUS_PIT_MODE
) {
1460 vtxStatusIndicator
= 'P';
1463 switch (element
->type
) {
1464 case OSD_ELEMENT_TYPE_2
:
1465 tfp_sprintf(element
->buff
, "%s", vtxPowerLabel
);
1469 if (vtxStatus
& VTX_STATUS_LOCKED
) {
1470 tfp_sprintf(element
->buff
, "-:-:-:L");
1471 } else if (vtxStatusIndicator
) {
1472 tfp_sprintf(element
->buff
, "%c:%s:%s:%c", vtxBandLetter
, vtxChannelName
, vtxPowerLabel
, vtxStatusIndicator
);
1474 tfp_sprintf(element
->buff
, "%c:%s:%s", vtxBandLetter
, vtxChannelName
, vtxPowerLabel
);
1479 #endif // USE_VTX_COMMON
1481 static void osdElementWarnings(osdElementParms_t
*element
)
1483 bool elementBlinking
= false;
1484 renderOsdWarning(element
->buff
, &elementBlinking
, &element
->attr
);
1485 if (elementBlinking
) {
1486 SET_BLINK(OSD_WARNINGS
);
1488 CLR_BLINK(OSD_WARNINGS
);
1491 #ifdef USE_CRAFTNAME_MSGS
1492 // Injects data into the CraftName variable for systems which limit
1493 // the available MSP data field in their OSD.
1494 if (osdConfig()->osd_craftname_msgs
== true) {
1495 // if warning is not set, or blink is off, then display LQ & RSSI
1496 if (blinkState
|| (strlen(element
->buff
) == 0)) {
1497 #ifdef USE_RX_LINK_QUALITY_INFO
1498 // replicate the LQ functionality without the special font symbols
1499 uint16_t osdLinkQuality
= 0;
1500 if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_CRSF
) { // 0-99
1501 osdLinkQuality
= rxGetLinkQuality();
1502 const uint8_t osdRfMode
= rxGetRfMode();
1503 tfp_sprintf(element
->buff
, "LQ %2d:%03d %3d", osdRfMode
, osdLinkQuality
, getRssiDbm());
1504 } else if (linkQualitySource
== LQ_SOURCE_RX_PROTOCOL_GHST
) { // 0-100
1505 osdLinkQuality
= rxGetLinkQuality();
1506 tfp_sprintf(element
->buff
, "LQ %03d %3d", osdLinkQuality
, getRssiDbm());
1508 osdLinkQuality
= rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE
;
1509 if (osdLinkQuality
>= 10) {
1512 tfp_sprintf(element
->buff
, "LQ %1d", osdLinkQuality
);
1514 #endif // USE_RX_LINK_QUALITY_INFO
1516 strncpy(pilotConfigMutable()->name
, element
->buff
, MAX_NAME_LENGTH
- 1);
1518 #endif // USE_CRAFTNAME_MSGS
1521 // Define the order in which the elements are drawn.
1522 // Elements positioned later in the list will overlay the earlier
1523 // ones if their character positions overlap
1524 // Elements that need special runtime conditional processing should be added
1525 // to osdAddActiveElements()
1527 static const uint8_t osdElementDisplayOrder
[] = {
1528 OSD_MAIN_BATT_VOLTAGE
,
1531 OSD_HORIZON_SIDEBARS
,
1532 OSD_UP_DOWN_REFERENCE
,
1535 OSD_REMAINING_TIME_ESTIMATE
,
1541 OSD_WATT_HOURS_DRAWN
,
1548 OSD_PIDRATE_PROFILE
,
1550 OSD_AVG_CELL_VOLTAGE
,
1554 OSD_MAIN_BATT_USAGE
,
1556 OSD_NUMERICAL_HEADING
,
1558 OSD_NUMERICAL_VARIO
,
1573 #ifdef USE_OSD_ADJUSTMENTS
1574 OSD_ADJUSTMENT_RANGE
,
1576 #ifdef USE_ADC_INTERNAL
1577 OSD_CORE_TEMPERATURE
,
1579 #ifdef USE_RX_LINK_QUALITY_INFO
1582 #ifdef USE_RX_LINK_UPLINK_POWER
1583 OSD_TX_UPLINK_POWER
,
1585 #ifdef USE_RX_RSSI_DBM
1588 #ifdef USE_OSD_STICK_OVERLAY
1589 OSD_STICK_OVERLAY_LEFT
,
1590 OSD_STICK_OVERLAY_RIGHT
,
1592 #ifdef USE_PROFILE_NAMES
1593 OSD_RATE_PROFILE_NAME
,
1594 OSD_PID_PROFILE_NAME
,
1596 #ifdef USE_OSD_PROFILES
1601 #ifdef USE_PERSISTENT_STATS
1606 // Define the mapping between the OSD element id and the function to draw it
1608 const osdElementDrawFn osdElementDrawFunction
[OSD_ITEM_COUNT
] = {
1609 [OSD_CAMERA_FRAME
] = NULL
, // only has background. Added first so it's the lowest "layer" and doesn't cover other elements
1610 [OSD_RSSI_VALUE
] = osdElementRssi
,
1611 [OSD_MAIN_BATT_VOLTAGE
] = osdElementMainBatteryVoltage
,
1612 [OSD_CROSSHAIRS
] = osdElementCrosshairs
, // only has background, but needs to be over other elements (like artificial horizon)
1614 [OSD_ARTIFICIAL_HORIZON
] = osdElementArtificialHorizon
,
1615 [OSD_UP_DOWN_REFERENCE
] = osdElementUpDownReference
,
1617 [OSD_HORIZON_SIDEBARS
] = NULL
, // only has background
1618 [OSD_ITEM_TIMER_1
] = osdElementTimer
,
1619 [OSD_ITEM_TIMER_2
] = osdElementTimer
,
1620 [OSD_FLYMODE
] = osdElementFlymode
,
1621 [OSD_CRAFT_NAME
] = NULL
, // only has background
1622 [OSD_THROTTLE_POS
] = osdElementThrottlePosition
,
1623 #ifdef USE_VTX_COMMON
1624 [OSD_VTX_CHANNEL
] = osdElementVtxChannel
,
1626 [OSD_CURRENT_DRAW
] = osdElementCurrentDraw
,
1627 [OSD_MAH_DRAWN
] = osdElementMahDrawn
,
1628 [OSD_WATT_HOURS_DRAWN
] = osdElementWattHoursDrawn
,
1630 [OSD_GPS_SPEED
] = osdElementGpsSpeed
,
1631 [OSD_GPS_SATS
] = osdElementGpsSats
,
1633 [OSD_ALTITUDE
] = osdElementAltitude
,
1634 [OSD_ROLL_PIDS
] = osdElementPidsRoll
,
1635 [OSD_PITCH_PIDS
] = osdElementPidsPitch
,
1636 [OSD_YAW_PIDS
] = osdElementPidsYaw
,
1637 [OSD_POWER
] = osdElementPower
,
1638 [OSD_PIDRATE_PROFILE
] = osdElementPidRateProfile
,
1639 [OSD_WARNINGS
] = osdElementWarnings
,
1640 [OSD_AVG_CELL_VOLTAGE
] = osdElementAverageCellVoltage
,
1642 [OSD_GPS_LON
] = osdElementGpsCoordinate
,
1643 [OSD_GPS_LAT
] = osdElementGpsCoordinate
,
1645 [OSD_DEBUG
] = osdElementDebug
,
1647 [OSD_PITCH_ANGLE
] = osdElementAngleRollPitch
,
1648 [OSD_ROLL_ANGLE
] = osdElementAngleRollPitch
,
1650 [OSD_MAIN_BATT_USAGE
] = osdElementMainBatteryUsage
,
1651 [OSD_DISARMED
] = osdElementDisarmed
,
1653 [OSD_HOME_DIR
] = osdElementGpsHomeDirection
,
1654 [OSD_HOME_DIST
] = osdElementGpsHomeDistance
,
1656 [OSD_NUMERICAL_HEADING
] = osdElementNumericalHeading
,
1658 [OSD_NUMERICAL_VARIO
] = osdElementNumericalVario
,
1660 [OSD_COMPASS_BAR
] = osdElementCompassBar
,
1661 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1662 [OSD_ESC_TMP
] = osdElementEscTemperature
,
1663 [OSD_ESC_RPM
] = osdElementEscRpm
,
1665 [OSD_REMAINING_TIME_ESTIMATE
] = osdElementRemainingTimeEstimate
,
1667 [OSD_RTC_DATETIME
] = osdElementRtcTime
,
1669 #ifdef USE_OSD_ADJUSTMENTS
1670 [OSD_ADJUSTMENT_RANGE
] = osdElementAdjustmentRange
,
1672 #ifdef USE_ADC_INTERNAL
1673 [OSD_CORE_TEMPERATURE
] = osdElementCoreTemperature
,
1675 [OSD_ANTI_GRAVITY
] = osdElementAntiGravity
,
1677 [OSD_G_FORCE
] = osdElementGForce
,
1679 [OSD_MOTOR_DIAG
] = osdElementMotorDiagnostics
,
1681 [OSD_LOG_STATUS
] = osdElementLogStatus
,
1684 [OSD_FLIP_ARROW
] = osdElementCrashFlipArrow
,
1686 #ifdef USE_RX_LINK_QUALITY_INFO
1687 [OSD_LINK_QUALITY
] = osdElementLinkQuality
,
1689 #ifdef USE_RX_LINK_UPLINK_POWER
1690 [OSD_TX_UPLINK_POWER
] = osdElementTxUplinkPower
,
1693 [OSD_FLIGHT_DIST
] = osdElementGpsFlightDistance
,
1695 #ifdef USE_OSD_STICK_OVERLAY
1696 [OSD_STICK_OVERLAY_LEFT
] = osdElementStickOverlay
,
1697 [OSD_STICK_OVERLAY_RIGHT
] = osdElementStickOverlay
,
1699 [OSD_DISPLAY_NAME
] = NULL
, // only has background
1700 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1701 [OSD_ESC_RPM_FREQ
] = osdElementEscRpmFreq
,
1703 #ifdef USE_PROFILE_NAMES
1704 [OSD_RATE_PROFILE_NAME
] = osdElementRateProfileName
,
1705 [OSD_PID_PROFILE_NAME
] = osdElementPidProfileName
,
1707 #ifdef USE_OSD_PROFILES
1708 [OSD_PROFILE_NAME
] = osdElementOsdProfileName
,
1710 #ifdef USE_RX_RSSI_DBM
1711 [OSD_RSSI_DBM_VALUE
] = osdElementRssiDbm
,
1713 [OSD_RC_CHANNELS
] = osdElementRcChannels
,
1715 [OSD_EFFICIENCY
] = osdElementEfficiency
,
1717 #ifdef USE_PERSISTENT_STATS
1718 [OSD_TOTAL_FLIGHTS
] = osdElementTotalFlights
,
1722 // Define the mapping between the OSD element id and the function to draw its background (static part)
1723 // Only necessary to define the entries that actually have a background function
1725 const osdElementDrawFn osdElementBackgroundFunction
[OSD_ITEM_COUNT
] = {
1726 [OSD_CAMERA_FRAME
] = osdBackgroundCameraFrame
,
1727 [OSD_HORIZON_SIDEBARS
] = osdBackgroundHorizonSidebars
,
1728 [OSD_CRAFT_NAME
] = osdBackgroundCraftName
,
1729 #ifdef USE_OSD_STICK_OVERLAY
1730 [OSD_STICK_OVERLAY_LEFT
] = osdBackgroundStickOverlay
,
1731 [OSD_STICK_OVERLAY_RIGHT
] = osdBackgroundStickOverlay
,
1733 [OSD_DISPLAY_NAME
] = osdBackgroundDisplayName
,
1736 static void osdAddActiveElement(osd_items_e element
)
1738 if (VISIBLE(osdElementConfig()->item_pos
[element
])) {
1739 activeOsdElementArray
[activeOsdElementCount
++] = element
;
1743 // Examine the elements and build a list of only the active (enabled)
1744 // ones to speed up rendering.
1746 void osdAddActiveElements(void)
1748 activeOsdElementCount
= 0;
1751 if (sensors(SENSOR_ACC
)) {
1752 osdAddActiveElement(OSD_ARTIFICIAL_HORIZON
);
1753 osdAddActiveElement(OSD_G_FORCE
);
1754 osdAddActiveElement(OSD_UP_DOWN_REFERENCE
);
1758 for (unsigned i
= 0; i
< sizeof(osdElementDisplayOrder
); i
++) {
1759 osdAddActiveElement(osdElementDisplayOrder
[i
]);
1763 if (sensors(SENSOR_GPS
)) {
1764 osdAddActiveElement(OSD_GPS_SATS
);
1765 osdAddActiveElement(OSD_GPS_SPEED
);
1766 osdAddActiveElement(OSD_GPS_LAT
);
1767 osdAddActiveElement(OSD_GPS_LON
);
1768 osdAddActiveElement(OSD_HOME_DIST
);
1769 osdAddActiveElement(OSD_HOME_DIR
);
1770 osdAddActiveElement(OSD_FLIGHT_DIST
);
1771 osdAddActiveElement(OSD_EFFICIENCY
);
1775 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1776 if ((featureIsEnabled(FEATURE_ESC_SENSOR
)) || (motorConfig()->dev
.useDshotTelemetry
)) {
1777 osdAddActiveElement(OSD_ESC_TMP
);
1778 osdAddActiveElement(OSD_ESC_RPM
);
1779 osdAddActiveElement(OSD_ESC_RPM_FREQ
);
1783 #ifdef USE_PERSISTENT_STATS
1784 osdAddActiveElement(OSD_TOTAL_FLIGHTS
);
1788 static void osdDrawSingleElement(displayPort_t
*osdDisplayPort
, uint8_t item
)
1790 if (!osdElementDrawFunction
[item
]) {
1791 // Element has no drawing function
1794 if (!osdDisplayPort
->useDeviceBlink
&& BLINK(item
)) {
1798 uint8_t elemPosX
= OSD_X(osdElementConfig()->item_pos
[item
]);
1799 uint8_t elemPosY
= OSD_Y(osdElementConfig()->item_pos
[item
]);
1800 char buff
[OSD_ELEMENT_BUFFER_LENGTH
] = "";
1802 osdElementParms_t element
;
1803 element
.item
= item
;
1804 element
.elemPosX
= elemPosX
;
1805 element
.elemPosY
= elemPosY
;
1806 element
.type
= OSD_TYPE(osdElementConfig()->item_pos
[item
]);
1807 element
.buff
= (char *)&buff
;
1808 element
.osdDisplayPort
= osdDisplayPort
;
1809 element
.drawElement
= true;
1810 element
.attr
= DISPLAYPORT_ATTR_NONE
;
1812 // Call the element drawing function
1813 osdElementDrawFunction
[item
](&element
);
1814 if (element
.drawElement
) {
1815 osdDisplayWrite(&element
, elemPosX
, elemPosY
, element
.attr
, buff
);
1819 static void osdDrawSingleElementBackground(displayPort_t
*osdDisplayPort
, uint8_t item
)
1821 if (!osdElementBackgroundFunction
[item
]) {
1822 // Element has no background drawing function
1826 uint8_t elemPosX
= OSD_X(osdElementConfig()->item_pos
[item
]);
1827 uint8_t elemPosY
= OSD_Y(osdElementConfig()->item_pos
[item
]);
1828 char buff
[OSD_ELEMENT_BUFFER_LENGTH
] = "";
1830 osdElementParms_t element
;
1831 element
.item
= item
;
1832 element
.elemPosX
= elemPosX
;
1833 element
.elemPosY
= elemPosY
;
1834 element
.type
= OSD_TYPE(osdElementConfig()->item_pos
[item
]);
1835 element
.buff
= (char *)&buff
;
1836 element
.osdDisplayPort
= osdDisplayPort
;
1837 element
.drawElement
= true;
1839 // Call the element background drawing function
1840 osdElementBackgroundFunction
[item
](&element
);
1841 if (element
.drawElement
) {
1842 osdDisplayWrite(&element
, elemPosX
, elemPosY
, DISPLAYPORT_ATTR_NONE
, buff
);
1846 static uint8_t activeElement
= 0;
1848 uint8_t osdGetActiveElement()
1850 return activeElement
;
1853 uint8_t osdGetActiveElementCount()
1855 return activeOsdElementCount
;
1858 // Return true if there are more elements to draw
1859 bool osdDrawNextActiveElement(displayPort_t
*osdDisplayPort
, timeUs_t currentTimeUs
)
1861 UNUSED(currentTimeUs
);
1864 if (activeElement
>= activeOsdElementCount
) {
1868 if (!backgroundLayerSupported
) {
1869 // If the background layer isn't supported then we
1870 // have to draw the element's static layer as well.
1871 osdDrawSingleElementBackground(osdDisplayPort
, activeOsdElementArray
[activeElement
]);
1874 osdDrawSingleElement(osdDisplayPort
, activeOsdElementArray
[activeElement
]);
1876 if (++activeElement
>= activeOsdElementCount
) {
1884 void osdDrawActiveElementsBackground(displayPort_t
*osdDisplayPort
)
1886 if (backgroundLayerSupported
) {
1887 displayLayerSelect(osdDisplayPort
, DISPLAYPORT_LAYER_BACKGROUND
);
1888 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_WAIT
);
1889 for (unsigned i
= 0; i
< activeOsdElementCount
; i
++) {
1890 osdDrawSingleElementBackground(osdDisplayPort
, activeOsdElementArray
[i
]);
1892 displayLayerSelect(osdDisplayPort
, DISPLAYPORT_LAYER_FOREGROUND
);
1896 void osdElementsInit(bool backgroundLayerFlag
)
1898 backgroundLayerSupported
= backgroundLayerFlag
;
1899 activeOsdElementCount
= 0;
1900 pt1FilterInit(&batteryEfficiencyFilt
, pt1FilterGain(EFFICIENCY_CUTOFF_HZ
, 1.0f
/ osdConfig()->framerate_hz
));
1905 static int blinkCount
= 0;
1907 // If the OSD blink is due a transition, do so
1908 // Task runs at osdConfig()->framerate_hz Hz, so this will cycle at 2Hz
1909 if (++blinkCount
== ((osdConfig()->framerate_hz
/ OSD_BLINK_FREQUENCY_HZ
) / 2)) {
1911 blinkState
= !blinkState
;
1915 void osdResetAlarms(void)
1917 memset(blinkBits
, 0, sizeof(blinkBits
));
1920 void osdUpdateAlarms(void)
1922 // This is overdone?
1924 int32_t alt
= osdGetMetersToSelectedUnit(getEstimatedAltitudeCm()) / 100;
1926 if (getRssiPercent() < osdConfig()->rssi_alarm
) {
1927 SET_BLINK(OSD_RSSI_VALUE
);
1929 CLR_BLINK(OSD_RSSI_VALUE
);
1932 #ifdef USE_RX_LINK_QUALITY_INFO
1933 if (rxGetLinkQualityPercent() < osdConfig()->link_quality_alarm
) {
1934 SET_BLINK(OSD_LINK_QUALITY
);
1936 CLR_BLINK(OSD_LINK_QUALITY
);
1938 #endif // USE_RX_LINK_QUALITY_INFO
1940 if (getBatteryState() == BATTERY_OK
) {
1941 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1942 CLR_BLINK(OSD_AVG_CELL_VOLTAGE
);
1944 SET_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1945 SET_BLINK(OSD_AVG_CELL_VOLTAGE
);
1949 if ((STATE(GPS_FIX
) == 0) || (gpsSol
.numSat
< gpsConfig()->gpsMinimumSats
)
1950 #ifdef USE_GPS_RESCUE
1951 || ((gpsSol
.numSat
< gpsConfig()->gpsRequiredSats
) && gpsRescueIsConfigured())
1954 SET_BLINK(OSD_GPS_SATS
);
1956 CLR_BLINK(OSD_GPS_SATS
);
1960 for (int i
= 0; i
< OSD_TIMER_COUNT
; i
++) {
1961 const uint16_t timer
= osdConfig()->timers
[i
];
1962 const timeUs_t time
= osdGetTimerValue(OSD_TIMER_SRC(timer
));
1963 const timeUs_t alarmTime
= OSD_TIMER_ALARM(timer
) * 60000000; // convert from minutes to us
1964 if (alarmTime
!= 0 && time
>= alarmTime
) {
1965 SET_BLINK(OSD_ITEM_TIMER_1
+ i
);
1967 CLR_BLINK(OSD_ITEM_TIMER_1
+ i
);
1971 if (getMAhDrawn() >= osdConfig()->cap_alarm
) {
1972 SET_BLINK(OSD_MAH_DRAWN
);
1973 SET_BLINK(OSD_MAIN_BATT_USAGE
);
1974 SET_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1976 CLR_BLINK(OSD_MAH_DRAWN
);
1977 CLR_BLINK(OSD_MAIN_BATT_USAGE
);
1978 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1981 if ((alt
>= osdConfig()->alt_alarm
) && ARMING_FLAG(ARMED
)) {
1982 SET_BLINK(OSD_ALTITUDE
);
1984 CLR_BLINK(OSD_ALTITUDE
);
1988 if (sensors(SENSOR_GPS
) && ARMING_FLAG(ARMED
) && STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1989 if (osdConfig()->distance_alarm
&& GPS_distanceToHome
>= osdConfig()->distance_alarm
) {
1990 SET_BLINK(OSD_HOME_DIST
);
1992 CLR_BLINK(OSD_HOME_DIST
);
1995 CLR_BLINK(OSD_HOME_DIST
);
1999 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
2002 #if defined(USE_ESC_SENSOR)
2003 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
2004 // This works because the combined ESC data contains the maximum temperature seen amongst all ESCs
2005 blink
= osdConfig()->esc_temp_alarm
!= ESC_TEMP_ALARM_OFF
&& osdEscDataCombined
->temperature
>= osdConfig()->esc_temp_alarm
;
2008 #if defined(USE_DSHOT_TELEMETRY)
2011 if (osdConfig()->esc_temp_alarm
!= ESC_TEMP_ALARM_OFF
) {
2012 for (uint32_t k
= 0; !blink
&& (k
< getMotorCount()); k
++) {
2013 blink
= (dshotTelemetryState
.motorState
[k
].telemetryTypes
& (1 << DSHOT_TELEMETRY_TYPE_TEMPERATURE
)) != 0 &&
2014 dshotTelemetryState
.motorState
[k
].telemetryData
[DSHOT_TELEMETRY_TYPE_TEMPERATURE
] >= osdConfig()->esc_temp_alarm
;
2023 SET_BLINK(OSD_ESC_TMP
);
2025 CLR_BLINK(OSD_ESC_TMP
);
2031 static bool osdElementIsActive(osd_items_e element
)
2033 for (unsigned i
= 0; i
< activeOsdElementCount
; i
++) {
2034 if (activeOsdElementArray
[i
] == element
) {
2041 // Determine if any active elements need the ACC
2042 bool osdElementsNeedAccelerometer(void)
2044 return osdElementIsActive(OSD_ARTIFICIAL_HORIZON
) ||
2045 osdElementIsActive(OSD_PITCH_ANGLE
) ||
2046 osdElementIsActive(OSD_ROLL_ANGLE
) ||
2047 osdElementIsActive(OSD_G_FORCE
) ||
2048 osdElementIsActive(OSD_FLIP_ARROW
) ||
2049 osdElementIsActive(OSD_UP_DOWN_REFERENCE
);