[4.4.2] Remove 15 m/s limit on estimated vario (#12788)
[betaflight.git] / src / main / osd / osd_elements.c
blobe5f022791f6b2e87168863427f37787fd4eeef31
1 /*
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)
8 * any later version.
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
63 *********************
64 OSD element variants:
65 *********************
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):
83 OSD_ALTITUDE
84 type 1: Altitude with one decimal place
85 type 2: Altitude with no decimal (whole number only)
87 OSD_GPS_LON
88 OSD_GPS_LAT
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)
94 OSD_MAIN_BATT_USAGE
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
100 VTX_CHANNEL
101 type 1: Contains Band:Channel:Power:Pit
102 type 2: Contains only Power
105 #include <stdbool.h>
106 #include <stdint.h>
107 #include <stdlib.h>
108 #include <string.h>
109 #include <ctype.h>
110 #include <math.h>
112 #include "platform.h"
114 #ifdef USE_OSD
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"
140 #include "fc/core.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"
151 #include "io/gps.h"
152 #include "io/vtx.h"
154 #include "osd/osd.h"
155 #include "osd/osd_elements.h"
156 #include "osd/osd_warnings.h"
158 #include "pg/motor.h"
159 #include "pg/stats.h"
161 #include "rx/rx.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
170 #include "olc.h"
171 #endif
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;
199 } radioControls_t;
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
207 #endif
209 static const char compassBar[] = {
210 SYM_HEADING_W,
211 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
212 SYM_HEADING_N,
213 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
214 SYM_HEADING_E,
215 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
216 SYM_HEADING_S,
217 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
218 SYM_HEADING_W,
219 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
220 SYM_HEADING_N,
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;
228 // Blink control
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)
237 enum {UP, DOWN};
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)
250 char buf[2];
252 buf[0] = c;
253 buf[1] = 0;
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));
267 #endif
268 #ifdef USE_ESC_SENSOR
269 if (featureIsEnabled(FEATURE_ESC_SENSOR)) {
270 return erpmToRpm(getEscSensorData(i)->rpm);
272 #endif
273 return 0;
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++) {
286 char rpmStr[6];
287 const int rpm = MIN((*escFnPtr)(i),99999);
288 const int len = tfp_sprintf(rpmStr, "%d", rpm);
289 rpmStr[len] = '\0';
290 osdDisplayWrite(element, x, y + i, DISPLAYPORT_SEVERITY_NORMAL, rpmStr);
292 element->drawElement = false;
294 #endif
296 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
297 int osdConvertTemperatureToSelectedUnit(int tempInDegreesCelcius)
299 switch (osdConfig()->units) {
300 case UNIT_IMPERIAL:
301 return lrintf(((tempInDegreesCelcius * 9.0f) / 5) + 32);
302 default:
303 return tempInDegreesCelcius;
306 #endif
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)
315 decimalPlaces = 0;
316 break;
317 case OSD_ELEMENT_TYPE_1: // one decimal place (default)
318 default:
319 decimalPlaces = 1;
320 break;
322 osdPrintFloat(buff, SYM_ALTITUDE, osdGetMetersToSelectedUnit(altitudeCm) / 100.0f, "", decimalPlaces, true, unitSymbol);
325 #ifdef USE_GPS
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
343 *buff++ = SYM_SAT_L;
344 *buff++ = SYM_SAT_R;
345 if (STATE(GPS_FIX_EVER)) {
346 OLC_LatLon location;
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);
350 } else {
351 memset(buff, SYM_HYPHEN, PLUS_CODE_DIGITS + 1);
352 buff[8] = '+';
353 buff[PLUS_CODE_DIGITS + 1] = '\0';
355 break;
357 #endif // USE_GPS_PLUS_CODES
359 case OSD_ELEMENT_TYPE_3: // degree, minutes, seconds style. ddd^mm'ss.00"W
361 char trailingSymbol;
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';
371 } else {
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);
375 break;
378 case OSD_ELEMENT_TYPE_2:
379 fractionalPart /= 1000;
380 FALLTHROUGH;
382 case OSD_ELEMENT_TYPE_1:
383 default:
384 *buff++ = leadingSymbol;
385 if (gpsValue < 0) {
386 *buff++ = SYM_HYPHEN;
388 tfp_sprintf(buff, (variantType == OSD_ELEMENT_TYPE_1 ? "%u.%07u" : "%u.%04u"), degreesPart, fractionalPart);
389 break;
392 #endif // USE_GPS
394 void osdFormatDistanceString(char *ptr, int distance, char leadingSymbol)
396 const float convertedDistance = osdGetMetersToSelectedUnit(distance);
397 char unitSymbol;
398 char unitSymbolExtended;
399 int unitTransition;
401 switch (osdConfig()->units) {
402 case UNIT_IMPERIAL:
403 unitTransition = 5280;
404 unitSymbol = SYM_FT;
405 unitSymbolExtended = SYM_MILES;
406 break;
407 default:
408 unitTransition = 1000;
409 unitSymbol = SYM_M;
410 unitSymbolExtended = SYM_KM;
411 break;
414 unsigned decimalPlaces;
415 float displayDistance;
416 char displaySymbol;
417 if (convertedDistance < unitTransition) {
418 decimalPlaces = 0;
419 displayDistance = convertedDistance;
420 displaySymbol = unitSymbol;
421 } else {
422 displayDistance = convertedDistance / unitTransition;
423 displaySymbol = unitSymbolExtended;
424 if (displayDistance >= 10) { // >= 10 miles or km - 1 decimal place
425 decimalPlaces = 1;
426 } else { // < 10 miles or km - 2 decimal places
427 decimalPlaces = 2;
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);
438 #ifdef USE_RTC_TIME
439 bool osdFormatRtcDateTime(char *buffer)
441 dateTime_t dateTime;
442 if (!rtcGetDateTime(&dateTime)) {
443 buffer[0] = '\0';
445 return false;
448 dateTimeFormatLocalShort(buffer, &dateTime);
450 return true;
452 #endif
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;
460 switch (precision) {
461 case OSD_TIMER_PREC_SECOND:
462 default:
463 tfp_sprintf(buff, "%02d:%02d", minutes, seconds);
464 break;
465 case OSD_TIMER_PREC_HUNDREDTHS:
467 const int hundredths = (time / 10000) % 100;
468 tfp_sprintf(buff, "%02d:%02d.%02d", minutes, seconds, hundredths);
469 break;
471 case OSD_TIMER_PREC_TENTHS:
473 const int tenths = (time / 100000) % 10;
474 tfp_sprintf(buff, "%02d:%02d.%01d", minutes, seconds, tenths);
475 break;
480 static char osdGetTimerSymbol(osd_timer_source_e src)
482 switch (src) {
483 case OSD_TIMER_SRC_ON:
484 return SYM_ON_M;
485 case OSD_TIMER_SRC_TOTAL_ARMED:
486 case OSD_TIMER_SRC_LAST_ARMED:
487 return SYM_FLY_M;
488 case OSD_TIMER_SRC_ON_OR_ARMED:
489 return ARMING_FLAG(ARMED) ? SYM_FLY_M : SYM_ON_M;
490 default:
491 return ' ';
495 static timeUs_t osdGetTimerValue(osd_timer_source_e src)
497 switch (src) {
498 case OSD_TIMER_SRC_ON:
499 return micros();
500 case OSD_TIMER_SRC_TOTAL_ARMED:
501 return osdFlyTime;
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();
508 default:
509 return 0;
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);
518 if (showSymbol) {
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
529 } else {
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) {
571 case UNIT_IMPERIAL:
572 return meters * 3.28084f; // Convert to feet
573 default:
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) {
584 case UNIT_IMPERIAL:
585 return SYM_FT;
586 default:
587 return SYM_M;
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) {
598 case UNIT_IMPERIAL:
599 case UNIT_BRITISH:
600 return CM_S_TO_MPH(value);
601 default:
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) {
612 case UNIT_IMPERIAL:
613 case UNIT_BRITISH:
614 return SYM_MPH;
615 default:
616 return SYM_KPH;
620 char osdGetVarioToSelectedUnitSymbol(void)
622 switch (osdConfig()->units) {
623 case UNIT_IMPERIAL:
624 return SYM_FTPS;
625 default:
626 return SYM_MPS;
630 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
631 char osdGetTemperatureSymbolForSelectedUnit(void)
633 switch (osdConfig()->units) {
634 case UNIT_IMPERIAL:
635 return SYM_F;
636 default:
637 return SYM_C;
640 #endif
642 // *************************
643 // Element drawing functions
644 // *************************
646 #ifdef USE_OSD_ADJUSTMENTS
647 static void osdElementAdjustmentRange(osdElementParms_t *element)
649 const char *name = getAdjustmentsRangeName();
650 if (name) {
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;
660 #ifdef USE_BARO
661 haveBaro = sensors(SENSOR_BARO);
662 #endif // USE_BARO
663 #ifdef USE_GPS
664 haveGps = sensors(SENSOR_GPS) && STATE(GPS_FIX);
665 #endif // USE_GPS
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);
674 } else {
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';
681 #ifdef USE_ACC
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);
687 #endif
689 static void osdElementAntiGravity(osdElementParms_t *element)
691 if (pidOsdAntiGravityActive()) {
692 strcpy(element->buff, "AG");
696 #ifdef USE_ACC
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
708 if (maxPitch > 0) {
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
732 int direction;
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)
737 direction = DOWN;
738 } else { // nose up
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)
741 direction = UP;
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
750 #endif // USE_ACC
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;
760 break;
761 case BATTERY_CRITICAL:
762 element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
763 break;
764 default:
765 break;
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)
810 unsigned int i;
811 for (i = 0; i < maxSrcLength && src[i]; i++) {
812 dest[i] = toupper((unsigned char)src[i]);
814 dest[i] = '\0';
817 static void osdBackgroundCraftName(osdElementParms_t *element)
819 if (strlen(pilotConfig()->craftName) == 0) {
820 strcpy(element->buff, "CRAFT_NAME");
821 } else {
822 toUpperCase(element->buff, pilotConfig()->craftName, MAX_NAME_LENGTH);
826 #ifdef USE_ACC
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) {
838 if (rollAngle > 0) {
839 element->buff[0] = SYM_ARROW_WEST + 2;
840 } else {
841 element->buff[0] = SYM_ARROW_EAST - 2;
843 } else {
844 if (rollAngle > 0) {
845 element->buff[0] = SYM_ARROW_WEST - 2;
846 } else {
847 element->buff[0] = SYM_ARROW_EAST + 2;
850 } else {
851 if (abs(pitchAngle) > abs(rollAngle)) {
852 if (pitchAngle > 0) {
853 element->buff[0] = SYM_ARROW_SOUTH;
854 } else {
855 element->buff[0] = SYM_ARROW_NORTH;
857 } else {
858 if (rollAngle > 0) {
859 element->buff[0] = SYM_ARROW_WEST;
860 } else {
861 element->buff[0] = SYM_ARROW_EAST;
865 element->buff[1] = '\0';
868 #endif // USE_ACC
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");
900 } else {
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);
911 #endif
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);
918 } else {
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);
927 } else {
928 toUpperCase(element->buff, currentPidProfile->profileName, MAX_PROFILE_NAME_LENGTH);
931 #endif
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);
940 } else {
941 toUpperCase(element->buff, osdConfig()->profile[profileIndex - 1], OSD_PROFILE_NAME_LENGTH);
944 #endif
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());
953 } else
954 #endif
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());
964 } else {
965 osdEleIx += tfp_sprintf(element->buff + osdEleIx, " 0%c", osdGetTemperatureSymbolForSelectedUnit());
969 #else
971 #endif
974 static void osdElementEscRpm(osdElementParms_t *element)
976 renderOsdEscRpmOrFreq(&getEscRpm,element);
979 static void osdElementEscRpmFreq(osdElementParms_t *element)
981 renderOsdEscRpmOrFreq(&getEscRpmFreq,element);
984 #endif
986 static void osdElementFlymode(osdElementParms_t *element)
988 // Note that flight mode display has precedence in what to display.
989 // 1. FS
990 // 2. GPS RESCUE
991 // 3. ANGLE, HORIZON, ACRO TRAINER
992 // 4. AIR
993 // 5. ACRO
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 ");
1009 } else {
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");
1021 #ifdef USE_ACC
1022 static void osdElementGForce(osdElementParms_t *element)
1024 osdPrintFloat(element->buff, SYM_NONE, osdGForce, "", 1, true, 'G');
1026 #endif // USE_ACC
1028 #ifdef USE_GPS
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);
1033 } else {
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);
1045 } else {
1046 element->buff[0] = SYM_OVER_HOME;
1049 } else {
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);
1061 } else {
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
1075 } else {
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;
1089 #endif
1090 else {
1091 element->attr = DISPLAYPORT_SEVERITY_INFO;
1094 if (!gpsIsHealthy()) {
1095 tfp_sprintf(element->buff, "%c%cNC", SYM_SAT_L, SYM_SAT_R);
1096 } else {
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());
1109 } else {
1110 tfp_sprintf(element->buff, "%c%c%c", SYM_SPEED, SYM_HYPHEN, osdGetSpeedToSelectedUnitSymbol());
1114 static void osdElementEfficiency(osdElementParms_t *element)
1116 int efficiency = 0;
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);
1126 } else {
1127 tfp_sprintf(element->buff, "----%c/%c", SYM_MAH, unitSymbol);
1130 #endif // USE_GPS
1132 static void osdBackgroundHorizonSidebars(osdElementParms_t *element)
1134 // Draw AH sides
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);
1165 } else { // 0-9
1166 osdLinkQuality = rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE;
1167 if (osdLinkQuality >= 10) {
1168 osdLinkQuality = 9;
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);
1181 } else {
1182 osdPrintFloat(element->buff, SYM_RSSI, osdUplinkTxPowerMw / 1000.0f, "", 1, false, 'W');
1185 #endif // USE_RX_LINK_UPLINK_POWER
1187 #ifdef USE_BLACKBOX
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);
1195 } else {
1196 int32_t logNumber = blackboxGetLogNumber();
1197 if (logNumber >= 0) {
1198 tfp_sprintf(element->buff, "%c%d", SYM_BBLOG, logNumber);
1199 } else {
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));
1229 } else {
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);
1252 FALLTHROUGH;
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);
1261 break;
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);
1266 FALLTHROUGH;
1268 case OSD_ELEMENT_TYPE_1: // mAh remaining graphical progress bar (shrinks as battery is used)
1269 default:
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';
1288 break;
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;
1302 break;
1303 case BATTERY_CRITICAL:
1304 element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
1305 break;
1306 default:
1307 break;
1310 if (batteryVoltage >= 10) { // if voltage is 10v or more then display only 1 decimal place
1311 decimalPlaces = 1;
1312 } else {
1313 decimalPlaces = 2;
1315 osdPrintFloat(element->buff, osdGetBatterySymbol(getBatteryAverageCellVoltage()), batteryVoltage, "", decimalPlaces, true, SYM_VOLT);
1318 static void osdElementMotorDiagnostics(osdElementParms_t *element)
1320 int i = 0;
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';
1330 #endif
1331 } else {
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);
1344 #ifdef USE_VARIO
1345 static void osdElementNumericalVario(osdElementParms_t *element)
1347 bool haveBaro = false;
1348 bool haveGps = false;
1349 #ifdef USE_BARO
1350 haveBaro = sensors(SENSOR_BARO);
1351 #endif // USE_BARO
1352 #ifdef USE_GPS
1353 haveGps = sensors(SENSOR_GPS) && STATE(GPS_FIX);
1354 #endif // USE_GPS
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());
1359 } else {
1360 // We use this symbol when we don't have a valid measure
1361 element->buff[0] = SYM_HYPHEN;
1362 element->buff[1] = '\0';
1365 #endif // USE_VARIO
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", &currentPidProfile->pid[PID_PITCH]);
1377 static void osdElementPidsRoll(osdElementParms_t *element)
1379 osdFormatPID(element->buff, "ROL", &currentPidProfile->pid[PID_ROLL]);
1382 static void osdElementPidsYaw(osdElementParms_t *element)
1384 osdFormatPID(element->buff, "YAW", &currentPidProfile->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.
1403 char fmtbuf[6];
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");
1424 } else {
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) {
1434 osdRssi = 99;
1437 if (getRssiPercent() < osdConfig()->rssi_alarm) {
1438 element->attr = DISPLAYPORT_SEVERITY_CRITICAL;
1441 tfp_sprintf(element->buff, "%c%2d", SYM_RSSI, osdRssi);
1444 #ifdef USE_RTC_TIME
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
1458 #ifdef USE_RX_RSNR
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;
1499 } else {
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;
1541 if (vtxDevice) {
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);
1560 break;
1562 default:
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);
1567 } else {
1568 tfp_sprintf(element->buff, "%c:%s:%s", vtxBandLetter, vtxChannelName, vtxPowerLabel);
1570 break;
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);
1586 } else {
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());
1607 #endif
1608 } else { // 0-9
1609 osdLinkQuality = rxGetLinkQuality() * 10 / LINK_QUALITY_MAX_VALUE;
1610 if (osdLinkQuality >= 10) {
1611 osdLinkQuality = 9;
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)
1625 UNUSED(element);
1627 // Nothing to render for a system element
1629 #endif
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,
1639 OSD_RSSI_VALUE,
1640 OSD_CROSSHAIRS,
1641 OSD_HORIZON_SIDEBARS,
1642 OSD_UP_DOWN_REFERENCE,
1643 OSD_ITEM_TIMER_1,
1644 OSD_ITEM_TIMER_2,
1645 OSD_REMAINING_TIME_ESTIMATE,
1646 OSD_FLYMODE,
1647 OSD_THROTTLE_POS,
1648 OSD_VTX_CHANNEL,
1649 OSD_CURRENT_DRAW,
1650 OSD_MAH_DRAWN,
1651 OSD_WATT_HOURS_DRAWN,
1652 OSD_CRAFT_NAME,
1653 OSD_ALTITUDE,
1654 OSD_ROLL_PIDS,
1655 OSD_PITCH_PIDS,
1656 OSD_YAW_PIDS,
1657 OSD_POWER,
1658 OSD_PIDRATE_PROFILE,
1659 OSD_WARNINGS,
1660 OSD_AVG_CELL_VOLTAGE,
1661 OSD_DEBUG,
1662 OSD_PITCH_ANGLE,
1663 OSD_ROLL_ANGLE,
1664 OSD_MAIN_BATT_USAGE,
1665 OSD_DISARMED,
1666 OSD_NUMERICAL_HEADING,
1667 OSD_READY_MODE,
1668 #ifdef USE_VARIO
1669 OSD_NUMERICAL_VARIO,
1670 #endif
1671 OSD_COMPASS_BAR,
1672 OSD_ANTI_GRAVITY,
1673 #ifdef USE_BLACKBOX
1674 OSD_LOG_STATUS,
1675 #endif
1676 OSD_MOTOR_DIAG,
1677 #ifdef USE_ACC
1678 OSD_FLIP_ARROW,
1679 #endif
1680 OSD_PILOT_NAME,
1681 #ifdef USE_RTC_TIME
1682 OSD_RTC_DATETIME,
1683 #endif
1684 #ifdef USE_OSD_ADJUSTMENTS
1685 OSD_ADJUSTMENT_RANGE,
1686 #endif
1687 #ifdef USE_ADC_INTERNAL
1688 OSD_CORE_TEMPERATURE,
1689 #endif
1690 #ifdef USE_RX_LINK_QUALITY_INFO
1691 OSD_LINK_QUALITY,
1692 #endif
1693 #ifdef USE_RX_LINK_UPLINK_POWER
1694 OSD_TX_UPLINK_POWER,
1695 #endif
1696 #ifdef USE_RX_RSSI_DBM
1697 OSD_RSSI_DBM_VALUE,
1698 #endif
1699 #ifdef USE_RX_RSNR
1700 OSD_RSNR_VALUE,
1701 #endif
1702 #ifdef USE_OSD_STICK_OVERLAY
1703 OSD_STICK_OVERLAY_LEFT,
1704 OSD_STICK_OVERLAY_RIGHT,
1705 #endif
1706 #ifdef USE_PROFILE_NAMES
1707 OSD_RATE_PROFILE_NAME,
1708 OSD_PID_PROFILE_NAME,
1709 #endif
1710 #ifdef USE_OSD_PROFILES
1711 OSD_PROFILE_NAME,
1712 #endif
1713 OSD_RC_CHANNELS,
1714 OSD_CAMERA_FRAME,
1715 #ifdef USE_PERSISTENT_STATS
1716 OSD_TOTAL_FLIGHTS,
1717 #endif
1718 OSD_AUX_VALUE,
1719 OSD_SYS_GOGGLE_VOLTAGE,
1720 OSD_SYS_VTX_VOLTAGE,
1721 OSD_SYS_BITRATE,
1722 OSD_SYS_DELAY,
1723 OSD_SYS_DISTANCE,
1724 OSD_SYS_LQ,
1725 OSD_SYS_GOGGLE_DVR,
1726 OSD_SYS_VTX_DVR,
1727 OSD_SYS_WARNINGS,
1728 OSD_SYS_VTX_TEMP,
1729 OSD_SYS_FAN_SPEED,
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)
1739 #ifdef USE_ACC
1740 [OSD_ARTIFICIAL_HORIZON] = osdElementArtificialHorizon,
1741 [OSD_UP_DOWN_REFERENCE] = osdElementUpDownReference,
1742 #endif
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,
1751 #endif
1752 [OSD_CURRENT_DRAW] = osdElementCurrentDraw,
1753 [OSD_MAH_DRAWN] = osdElementMahDrawn,
1754 [OSD_WATT_HOURS_DRAWN] = osdElementWattHoursDrawn,
1755 #ifdef USE_GPS
1756 [OSD_GPS_SPEED] = osdElementGpsSpeed,
1757 [OSD_GPS_SATS] = osdElementGpsSats,
1758 #endif
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,
1768 #ifdef USE_GPS
1769 [OSD_GPS_LON] = osdElementGpsCoordinate,
1770 [OSD_GPS_LAT] = osdElementGpsCoordinate,
1771 #endif
1772 [OSD_DEBUG] = osdElementDebug,
1773 #ifdef USE_ACC
1774 [OSD_PITCH_ANGLE] = osdElementAngleRollPitch,
1775 [OSD_ROLL_ANGLE] = osdElementAngleRollPitch,
1776 #endif
1777 [OSD_MAIN_BATT_USAGE] = osdElementMainBatteryUsage,
1778 [OSD_DISARMED] = osdElementDisarmed,
1779 #ifdef USE_GPS
1780 [OSD_HOME_DIR] = osdElementGpsHomeDirection,
1781 [OSD_HOME_DIST] = osdElementGpsHomeDistance,
1782 #endif
1783 [OSD_NUMERICAL_HEADING] = osdElementNumericalHeading,
1784 #ifdef USE_VARIO
1785 [OSD_NUMERICAL_VARIO] = osdElementNumericalVario,
1786 #endif
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,
1791 #endif
1792 [OSD_REMAINING_TIME_ESTIMATE] = osdElementRemainingTimeEstimate,
1793 #ifdef USE_RTC_TIME
1794 [OSD_RTC_DATETIME] = osdElementRtcTime,
1795 #endif
1796 #ifdef USE_OSD_ADJUSTMENTS
1797 [OSD_ADJUSTMENT_RANGE] = osdElementAdjustmentRange,
1798 #endif
1799 #ifdef USE_ADC_INTERNAL
1800 [OSD_CORE_TEMPERATURE] = osdElementCoreTemperature,
1801 #endif
1802 [OSD_ANTI_GRAVITY] = osdElementAntiGravity,
1803 #ifdef USE_ACC
1804 [OSD_G_FORCE] = osdElementGForce,
1805 #endif
1806 [OSD_MOTOR_DIAG] = osdElementMotorDiagnostics,
1807 #ifdef USE_BLACKBOX
1808 [OSD_LOG_STATUS] = osdElementLogStatus,
1809 #endif
1810 #ifdef USE_ACC
1811 [OSD_FLIP_ARROW] = osdElementCrashFlipArrow,
1812 #endif
1813 #ifdef USE_RX_LINK_QUALITY_INFO
1814 [OSD_LINK_QUALITY] = osdElementLinkQuality,
1815 #endif
1816 #ifdef USE_RX_LINK_UPLINK_POWER
1817 [OSD_TX_UPLINK_POWER] = osdElementTxUplinkPower,
1818 #endif
1819 #ifdef USE_GPS
1820 [OSD_FLIGHT_DIST] = osdElementGpsFlightDistance,
1821 #endif
1822 #ifdef USE_OSD_STICK_OVERLAY
1823 [OSD_STICK_OVERLAY_LEFT] = osdElementStickOverlay,
1824 [OSD_STICK_OVERLAY_RIGHT] = osdElementStickOverlay,
1825 #endif
1826 [OSD_PILOT_NAME] = NULL, // only has background
1827 #if defined(USE_DSHOT_TELEMETRY) || defined(USE_ESC_SENSOR)
1828 [OSD_ESC_RPM_FREQ] = osdElementEscRpmFreq,
1829 #endif
1830 #ifdef USE_PROFILE_NAMES
1831 [OSD_RATE_PROFILE_NAME] = osdElementRateProfileName,
1832 [OSD_PID_PROFILE_NAME] = osdElementPidProfileName,
1833 #endif
1834 #ifdef USE_OSD_PROFILES
1835 [OSD_PROFILE_NAME] = osdElementOsdProfileName,
1836 #endif
1837 #ifdef USE_RX_RSSI_DBM
1838 [OSD_RSSI_DBM_VALUE] = osdElementRssiDbm,
1839 #endif
1840 #ifdef USE_RX_RSNR
1841 [OSD_RSNR_VALUE] = osdElementRsnr,
1842 #endif
1843 [OSD_RC_CHANNELS] = osdElementRcChannels,
1844 #ifdef USE_GPS
1845 [OSD_EFFICIENCY] = osdElementEfficiency,
1846 #endif
1847 #ifdef USE_PERSISTENT_STATS
1848 [OSD_TOTAL_FLIGHTS] = osdElementTotalFlights,
1849 #endif
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,
1863 #endif
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,
1876 #endif
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;
1894 #ifdef USE_ACC
1895 if (sensors(SENSOR_ACC)) {
1896 osdAddActiveElement(OSD_ARTIFICIAL_HORIZON);
1897 osdAddActiveElement(OSD_G_FORCE);
1898 osdAddActiveElement(OSD_UP_DOWN_REFERENCE);
1900 #endif
1902 for (unsigned i = 0; i < sizeof(osdElementDisplayOrder); i++) {
1903 osdAddActiveElement(osdElementDisplayOrder[i]);
1906 #ifdef USE_GPS
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);
1917 #endif // GPS
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);
1925 #endif
1927 #ifdef USE_PERSISTENT_STATS
1928 osdAddActiveElement(OSD_TOTAL_FLIGHTS);
1929 #endif
1932 static void osdDrawSingleElement(displayPort_t *osdDisplayPort, uint8_t item)
1934 if (!osdElementDrawFunction[item]) {
1935 // Element has no drawing function
1936 return;
1938 if (!osdDisplayPort->useDeviceBlink && BLINK(item)) {
1939 return;
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));
1959 } else {
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
1971 return;
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);
2010 bool retval = true;
2012 if (activeElement >= activeOsdElementCount) {
2013 return false;
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) {
2025 activeElement = 0;
2026 retval = false;
2029 return retval;
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)) {
2058 blinkCount = 0;
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);
2076 } else {
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);
2083 } else {
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);
2091 } else {
2092 SET_BLINK(OSD_MAIN_BATT_VOLTAGE);
2093 SET_BLINK(OSD_AVG_CELL_VOLTAGE);
2096 #ifdef USE_GPS
2097 if ((STATE(GPS_FIX) == 0) || (gpsSol.numSat < GPS_MIN_SAT_COUNT)
2098 #ifdef USE_GPS_RESCUE
2099 || ((gpsSol.numSat < gpsRescueConfig()->minSats) && gpsRescueIsConfigured())
2100 #endif
2102 SET_BLINK(OSD_GPS_SATS);
2103 } else {
2104 CLR_BLINK(OSD_GPS_SATS);
2106 #endif //USE_GPS
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);
2114 } else {
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);
2123 } else {
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);
2131 } else {
2132 CLR_BLINK(OSD_ALTITUDE);
2135 #ifdef USE_GPS
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);
2139 } else {
2140 CLR_BLINK(OSD_HOME_DIST);
2142 } else {
2143 CLR_BLINK(OSD_HOME_DIST);
2145 #endif
2147 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
2148 bool blink;
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;
2154 } else
2155 #endif
2156 #if defined(USE_DSHOT_TELEMETRY)
2158 blink = false;
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;
2166 #else
2168 #endif
2170 if (blink) {
2171 SET_BLINK(OSD_ESC_TMP);
2172 } else {
2173 CLR_BLINK(OSD_ESC_TMP);
2175 #endif
2178 #ifdef USE_ACC
2179 static bool osdElementIsActive(osd_items_e element)
2181 for (unsigned i = 0; i < activeOsdElementCount; i++) {
2182 if (activeOsdElementArray[i] == element) {
2183 return true;
2186 return false;
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);
2200 #endif // USE_ACC
2202 #endif // USE_OSD