Blackbox device type 'file' (SITL) considered working when file handler is available
[inav.git] / src / main / io / osd.c
blobd682c0299ffb13b40aafaaecf30806ee0f802556
1 /*
2 * This file is part of Cleanflight.
4 * Cleanflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Cleanflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
19 Created by Marcin Baliniak
20 some functions based on MinimOSD
22 OSD-CMS separation by jflyper
25 #include <stdbool.h>
26 #include <stdint.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <ctype.h>
30 #include <math.h>
31 #include <inttypes.h>
33 #include "platform.h"
35 #ifdef USE_OSD
37 #include "build/debug.h"
38 #include "build/version.h"
40 #include "cms/cms.h"
41 #include "cms/cms_types.h"
42 #include "cms/cms_menu_osd.h"
44 #include "common/axis.h"
45 #include "common/constants.h"
46 #include "common/filter.h"
47 #include "common/log.h"
48 #include "common/olc.h"
49 #include "common/printf.h"
50 #include "common/string_light.h"
51 #include "common/time.h"
52 #include "common/typeconversion.h"
53 #include "common/utils.h"
55 #include "config/feature.h"
56 #include "config/parameter_group.h"
57 #include "config/parameter_group_ids.h"
59 #include "drivers/display.h"
60 #include "drivers/display_canvas.h"
61 #include "drivers/display_font_metadata.h"
62 #include "drivers/osd_symbols.h"
63 #include "drivers/time.h"
64 #include "drivers/vtx_common.h"
65 #include "drivers/gimbal_common.h"
67 #include "io/adsb.h"
68 #include "io/flashfs.h"
69 #include "io/gps.h"
70 #include "io/osd.h"
71 #include "io/osd_common.h"
72 #include "io/osd_hud.h"
73 #include "io/osd_utils.h"
74 #include "io/displayport_msp_dji_compat.h"
75 #include "io/vtx.h"
76 #include "io/vtx_string.h"
78 #include "io/osd/custom_elements.h"
80 #include "fc/config.h"
81 #include "fc/controlrate_profile.h"
82 #include "fc/fc_core.h"
83 #include "fc/fc_tasks.h"
84 #include "fc/multifunction.h"
85 #include "fc/rc_adjustments.h"
86 #include "fc/rc_controls.h"
87 #include "fc/rc_modes.h"
88 #include "fc/runtime_config.h"
89 #include "fc/settings.h"
91 #include "flight/imu.h"
92 #include "flight/mixer.h"
93 #include "flight/pid.h"
94 #include "flight/power_limits.h"
95 #include "flight/rth_estimator.h"
96 #include "flight/servos.h"
97 #include "flight/wind_estimator.h"
99 #include "navigation/navigation.h"
100 #include "navigation/navigation_private.h"
102 #include "rx/rx.h"
103 #include "rx/msp_override.h"
105 #include "sensors/acceleration.h"
106 #include "sensors/battery.h"
107 #include "sensors/boardalignment.h"
108 #include "sensors/compass.h"
109 #include "sensors/diagnostics.h"
110 #include "sensors/sensors.h"
111 #include "sensors/pitotmeter.h"
112 #include "sensors/temperature.h"
113 #include "sensors/esc_sensor.h"
114 #include "sensors/rangefinder.h"
116 #include "programming/logic_condition.h"
117 #include "programming/global_variables.h"
119 #ifdef USE_BLACKBOX
120 #include "blackbox/blackbox_io.h"
121 #endif
123 #ifdef USE_HARDWARE_REVISION_DETECTION
124 #include "hardware_revision.h"
125 #endif
127 #define VIDEO_BUFFER_CHARS_PAL 480
128 #define VIDEO_BUFFER_CHARS_HDZERO 900
129 #define VIDEO_BUFFER_CHARS_DJIWTF 1320
131 #define GFORCE_FILTER_TC 0.2
133 #define OSD_STATS_SINGLE_PAGE_MIN_ROWS 18
134 #define IS_HI(X) (rxGetChannelValue(X) > 1750)
135 #define IS_LO(X) (rxGetChannelValue(X) < 1250)
136 #define IS_MID(X) (rxGetChannelValue(X) > 1250 && rxGetChannelValue(X) < 1750)
138 #define OSD_RESUME_UPDATES_STICK_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
139 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
140 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
142 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
143 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
145 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
147 // Adjust OSD_MESSAGE's default position when
148 // changing OSD_MESSAGE_LENGTH
149 #define OSD_MESSAGE_LENGTH 28
150 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
151 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
152 // Wrap all string constants intenteded for display as messages with
153 // this macro to ensure compile time length validation.
154 #define OSD_MESSAGE_STR(x) ({ \
155 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
156 x; \
159 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
161 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
162 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
164 #define OSD_MIN_FONT_VERSION 3
166 static timeMs_t linearDescentMessageMs = 0;
167 static timeMs_t notify_settings_saved = 0;
168 static bool savingSettings = false;
170 static unsigned currentLayout = 0;
171 static int layoutOverride = -1;
172 static bool hasExtendedFont = false; // Wether the font supports characters > 256
173 static timeMs_t layoutOverrideUntil = 0;
174 static pt1Filter_t GForceFilter, GForceFilterAxis[XYZ_AXIS_COUNT];
175 static float GForce, GForceAxis[XYZ_AXIS_COUNT];
177 typedef struct statistic_s {
178 uint16_t max_speed;
179 uint16_t max_3D_speed;
180 uint16_t max_air_speed;
181 uint16_t min_voltage; // /100
182 int16_t max_current;
183 int32_t max_power;
184 int16_t min_rssi;
185 int16_t min_lq; // for CRSF
186 int16_t min_rssi_dbm; // for CRSF
187 int32_t max_altitude;
188 uint32_t max_distance;
189 uint8_t min_sats;
190 uint8_t max_sats;
191 int16_t max_esc_temp;
192 int16_t min_esc_temp;
193 int32_t flightStartMAh;
194 int32_t flightStartMWh;
195 } statistic_t;
197 static statistic_t stats;
199 static timeUs_t resumeRefreshAt = 0;
200 static bool refreshWaitForResumeCmdRelease;
202 static bool fullRedraw = false;
204 static uint8_t armState;
206 static textAttributes_t osdGetMultiFunctionMessage(char *buff);
207 static uint8_t osdWarningsFlags = 0;
209 typedef struct osdMapData_s {
210 uint32_t scale;
211 char referenceSymbol;
212 } osdMapData_t;
214 static osdMapData_t osdMapData;
216 static displayPort_t *osdDisplayPort;
217 static bool osdDisplayIsReady = false;
218 #if defined(USE_CANVAS)
219 static displayCanvas_t osdCanvas;
220 static bool osdDisplayHasCanvas;
221 #else
222 #define osdDisplayHasCanvas false
223 #endif
225 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
227 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t, osdConfig, PG_OSD_CONFIG, 12);
228 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t, osdLayoutsConfig, PG_OSD_LAYOUTS_CONFIG, 1);
230 void osdStartedSaveProcess(void) {
231 savingSettings = true;
234 void osdShowEEPROMSavedNotification(void) {
235 savingSettings = false;
236 notify_settings_saved = millis() + 5000;
239 bool osdDisplayIsPAL(void)
241 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL;
244 bool osdDisplayIsHD(void)
246 if (displayScreenSize(osdDisplayPort) >= VIDEO_BUFFER_CHARS_HDZERO)
248 return true;
250 return false;
253 bool osdIsNotMetric(void) {
254 return !(osdConfig()->units == OSD_UNIT_METRIC || osdConfig()->units == OSD_UNIT_METRIC_MPH);
258 * Converts distance into a string based on the current unit system
259 * prefixed by a a symbol to indicate the unit used.
260 * @param dist Distance in centimeters
262 static void osdFormatDistanceSymbol(char *buff, int32_t dist, uint8_t decimals)
264 uint8_t digits = 3U; // Total number of digits (including decimal point)
265 uint8_t sym_index = 3U; // Position (index) at buffer of units symbol
266 uint8_t symbol_m = SYM_DIST_M;
267 uint8_t symbol_km = SYM_DIST_KM;
268 uint8_t symbol_ft = SYM_DIST_FT;
269 uint8_t symbol_mi = SYM_DIST_MI;
270 uint8_t symbol_nm = SYM_DIST_NM;
272 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
273 if (isDJICompatibleVideoSystem(osdConfig())) {
274 // Add one digit so up no switch to scaled decimal occurs above 99
275 digits = 4U;
276 sym_index = 4U;
277 // Use altitude symbols on purpose, as it seems distance symbols are not defined in DJICOMPAT mode
278 symbol_m = SYM_ALT_M;
279 symbol_km = SYM_ALT_KM;
280 symbol_ft = SYM_ALT_FT;
281 symbol_mi = SYM_MI;
282 symbol_nm = SYM_MI;
284 #endif
286 switch ((osd_unit_e)osdConfig()->units) {
287 case OSD_UNIT_UK:
288 FALLTHROUGH;
289 case OSD_UNIT_IMPERIAL:
290 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_MILE, decimals, 3, digits, false)) {
291 buff[sym_index] = symbol_mi;
292 } else {
293 buff[sym_index] = symbol_ft;
295 buff[sym_index + 1] = '\0';
296 break;
297 case OSD_UNIT_METRIC_MPH:
298 FALLTHROUGH;
299 case OSD_UNIT_METRIC:
300 if (osdFormatCentiNumber(buff, dist, METERS_PER_KILOMETER, decimals, 3, digits, false)) {
301 buff[sym_index] = symbol_km;
302 } else {
303 buff[sym_index] = symbol_m;
305 buff[sym_index + 1] = '\0';
306 break;
307 case OSD_UNIT_GA:
308 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), (uint32_t)FEET_PER_NAUTICALMILE, decimals, 3, digits, false)) {
309 buff[sym_index] = symbol_nm;
310 } else {
311 buff[sym_index] = symbol_ft;
313 buff[sym_index + 1] = '\0';
314 break;
319 * Converts distance into a string based on the current unit system.
320 * @param dist Distance in centimeters
322 static void osdFormatDistanceStr(char *buff, int32_t dist)
324 int32_t centifeet;
325 switch ((osd_unit_e)osdConfig()->units) {
326 case OSD_UNIT_UK:
327 FALLTHROUGH;
328 case OSD_UNIT_IMPERIAL:
329 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
330 if (abs(centifeet) < FEET_PER_MILE * 100 / 2) {
331 // Show feet when dist < 0.5mi
332 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
333 } else {
334 // Show miles when dist >= 0.5mi
335 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100*FEET_PER_MILE)),
336 (abs(centifeet) % (100 * FEET_PER_MILE)) / FEET_PER_MILE, SYM_MI);
338 break;
339 case OSD_UNIT_METRIC_MPH:
340 FALLTHROUGH;
341 case OSD_UNIT_METRIC:
342 if (abs(dist) < METERS_PER_KILOMETER * 100) {
343 // Show meters when dist < 1km
344 tfp_sprintf(buff, "%d%c", (int)(dist / 100), SYM_M);
345 } else {
346 // Show kilometers when dist >= 1km
347 tfp_sprintf(buff, "%d.%02d%c", (int)(dist / (100*METERS_PER_KILOMETER)),
348 (abs(dist) % (100 * METERS_PER_KILOMETER)) / METERS_PER_KILOMETER, SYM_KM);
350 break;
351 case OSD_UNIT_GA:
352 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
353 if (abs(centifeet) < 100000) {
354 // Show feet when dist < 1000ft
355 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
356 } else {
357 // Show nautical miles when dist >= 1000ft
358 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100 * FEET_PER_NAUTICALMILE)),
359 (int)((abs(centifeet) % (int)(100 * FEET_PER_NAUTICALMILE)) / FEET_PER_NAUTICALMILE), SYM_NM);
361 break;
366 * Converts velocity based on the current unit system (kmh or mph).
367 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
369 static int32_t osdConvertVelocityToUnit(int32_t vel)
371 switch ((osd_unit_e)osdConfig()->units) {
372 case OSD_UNIT_UK:
373 FALLTHROUGH;
374 case OSD_UNIT_METRIC_MPH:
375 FALLTHROUGH;
376 case OSD_UNIT_IMPERIAL:
377 return CMSEC_TO_CENTIMPH(vel) / 100; // Convert to mph
378 case OSD_UNIT_METRIC:
379 return CMSEC_TO_CENTIKPH(vel) / 100; // Convert to kmh
380 case OSD_UNIT_GA:
381 return CMSEC_TO_CENTIKNOTS(vel) / 100; // Convert to Knots
383 // Unreachable
384 return -1;
388 * Converts velocity into a string based on the current unit system.
389 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
390 * @param _3D is a 3D velocity
391 * @param _max is a maximum velocity
393 void osdFormatVelocityStr(char* buff, int32_t vel, bool _3D, bool _max)
395 switch ((osd_unit_e)osdConfig()->units) {
396 case OSD_UNIT_UK:
397 FALLTHROUGH;
398 case OSD_UNIT_METRIC_MPH:
399 FALLTHROUGH;
400 case OSD_UNIT_IMPERIAL:
401 if (_max) {
402 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
403 } else {
404 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
406 break;
407 case OSD_UNIT_METRIC:
408 if (_max) {
409 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
410 } else {
411 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
413 break;
414 case OSD_UNIT_GA:
415 if (_max) {
416 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
417 } else {
418 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
420 break;
425 * Returns the average velocity. This always uses stats, so can be called as an OSD element later if wanted, to show a real time average
427 static void osdGenerateAverageVelocityStr(char* buff) {
428 uint32_t cmPerSec = getTotalTravelDistance() / getFlightTime();
429 osdFormatVelocityStr(buff, cmPerSec, false, false);
433 * Converts wind speed into a string based on the current unit system, using
434 * always 3 digits and an additional character for the unit at the right. buff
435 * is null terminated.
436 * @param ws Raw wind speed in cm/s
438 #ifdef USE_WIND_ESTIMATOR
439 static void osdFormatWindSpeedStr(char *buff, int32_t ws, bool isValid)
441 int32_t centivalue;
442 char suffix;
443 switch (osdConfig()->units) {
444 case OSD_UNIT_UK:
445 FALLTHROUGH;
446 case OSD_UNIT_METRIC_MPH:
447 FALLTHROUGH;
448 case OSD_UNIT_IMPERIAL:
449 centivalue = CMSEC_TO_CENTIMPH(ws);
450 suffix = SYM_MPH;
451 break;
452 case OSD_UNIT_GA:
453 centivalue = CMSEC_TO_CENTIKNOTS(ws);
454 suffix = SYM_KT;
455 break;
456 default:
457 case OSD_UNIT_METRIC:
458 if (osdConfig()->estimations_wind_mps)
460 centivalue = ws;
461 suffix = SYM_MS;
463 else
465 centivalue = CMSEC_TO_CENTIKPH(ws);
466 suffix = SYM_KMH;
468 break;
471 osdFormatCentiNumber(buff, centivalue, 0, 2, 0, 3, false);
473 if (!isValid && ((millis() / 1000) % 4 < 2))
474 suffix = '*';
476 buff[3] = suffix;
477 buff[4] = '\0';
479 #endif
482 * Converts altitude into a string based on the current unit system
483 * prefixed by a a symbol to indicate the unit used.
484 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
486 void osdFormatAltitudeSymbol(char *buff, int32_t alt)
488 uint8_t totalDigits = 4U;
489 uint8_t digits = 4U;
490 uint8_t symbolIndex = 4U;
491 uint8_t symbolKFt = SYM_ALT_KFT;
493 if (alt >= 0) {
494 digits = 3U;
495 buff[0] = ' ';
498 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
499 if (isDJICompatibleVideoSystem(osdConfig())) {
500 totalDigits++;
501 digits++;
502 symbolIndex++;
503 symbolKFt = SYM_ALT_FT;
505 #endif
507 switch ((osd_unit_e)osdConfig()->units) {
508 case OSD_UNIT_UK:
509 FALLTHROUGH;
510 case OSD_UNIT_GA:
511 FALLTHROUGH;
512 case OSD_UNIT_IMPERIAL:
513 if (osdFormatCentiNumber(buff + totalDigits - digits, CENTIMETERS_TO_CENTIFEET(alt), 1000, 0, 2, digits, false)) {
514 // Scaled to kft
515 buff[symbolIndex++] = symbolKFt;
516 } else {
517 // Formatted in feet
518 buff[symbolIndex++] = SYM_ALT_FT;
520 buff[symbolIndex] = '\0';
521 break;
522 case OSD_UNIT_METRIC_MPH:
523 FALLTHROUGH;
524 case OSD_UNIT_METRIC:
525 // alt is alredy in cm
526 if (osdFormatCentiNumber(buff + totalDigits - digits, alt, 1000, 0, 2, digits, false)) {
527 // Scaled to km
528 buff[symbolIndex++] = SYM_ALT_KM;
529 } else {
530 // Formatted in m
531 buff[symbolIndex++] = SYM_ALT_M;
533 buff[symbolIndex] = '\0';
534 break;
539 * Converts altitude into a string based on the current unit system.
540 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
542 static void osdFormatAltitudeStr(char *buff, int32_t alt)
544 int32_t value;
545 switch ((osd_unit_e)osdConfig()->units) {
546 case OSD_UNIT_UK:
547 FALLTHROUGH;
548 case OSD_UNIT_GA:
549 FALLTHROUGH;
550 case OSD_UNIT_IMPERIAL:
551 value = CENTIMETERS_TO_FEET(alt);
552 tfp_sprintf(buff, "%d%c", (int)value, SYM_FT);
553 break;
554 case OSD_UNIT_METRIC_MPH:
555 FALLTHROUGH;
556 case OSD_UNIT_METRIC:
557 value = CENTIMETERS_TO_METERS(alt);
558 tfp_sprintf(buff, "%d%c", (int)value, SYM_M);
559 break;
563 static void osdFormatTime(char *buff, uint32_t seconds, char sym_m, char sym_h)
565 uint32_t value = seconds;
566 char sym = sym_m;
567 // Maximum value we can show in minutes is 99 minutes and 59 seconds
568 if (seconds > (99 * 60) + 59) {
569 sym = sym_h;
570 value = seconds / 60;
572 buff[0] = sym;
573 tfp_sprintf(buff + 1, "%02d:%02d", (int)(value / 60), (int)(value % 60));
576 static inline void osdFormatOnTime(char *buff)
578 osdFormatTime(buff, micros() / 1000000, SYM_ON_M, SYM_ON_H);
581 static inline void osdFormatFlyTime(char *buff, textAttributes_t *attr)
583 uint32_t seconds = getFlightTime();
584 osdFormatTime(buff, seconds, SYM_FLY_M, SYM_FLY_H);
585 if (attr && osdConfig()->time_alarm > 0) {
586 if (seconds / 60 >= osdConfig()->time_alarm && ARMING_FLAG(ARMED)) {
587 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
593 * Trim whitespace from string.
594 * Used in Stats screen on lines with multiple values.
596 char *osdFormatTrimWhiteSpace(char *buff)
598 char *end;
600 // Trim leading spaces
601 while(isspace((unsigned char)*buff)) buff++;
603 // All spaces?
604 if(*buff == 0)
605 return buff;
607 // Trim trailing spaces
608 end = buff + strlen(buff) - 1;
609 while(end > buff && isspace((unsigned char)*end)) end--;
611 // Write new null terminator character
612 end[1] = '\0';
614 return buff;
618 * Converts RSSI into a % value used by the OSD.
620 static uint16_t osdConvertRSSI(void)
622 // change range to [0, 99]
623 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE, 0, 99);
626 static uint16_t osdGetCrsfLQ(void)
628 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
629 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
630 int16_t displayedLQ = 0;
631 switch (osdConfig()->crsf_lq_format) {
632 case OSD_CRSF_LQ_TYPE1:
633 displayedLQ = statsLQ;
634 break;
635 case OSD_CRSF_LQ_TYPE2:
636 displayedLQ = statsLQ;
637 break;
638 case OSD_CRSF_LQ_TYPE3:
639 displayedLQ = rxLinkStatistics.rfMode >= 2 ? scaledLQ : statsLQ;
640 break;
642 return displayedLQ;
645 static int16_t osdGetCrsfdBm(void)
647 return rxLinkStatistics.uplinkRSSI;
650 * Displays a temperature postfixed with a symbol depending on the current unit system
651 * @param label to display
652 * @param valid true if measurement is valid
653 * @param temperature in deciDegrees Celcius
655 static void osdDisplayTemperature(uint8_t elemPosX, uint8_t elemPosY, uint16_t symbol, const char *label, bool valid, int16_t temperature, int16_t alarm_min, int16_t alarm_max)
657 char buff[TEMPERATURE_LABEL_LEN + 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN + 2];
658 textAttributes_t elemAttr = valid ? TEXT_ATTRIBUTES_NONE : _TEXT_ATTRIBUTES_BLINK_BIT;
659 uint8_t valueXOffset = 0;
661 if (symbol) {
662 buff[0] = symbol;
663 buff[1] = '\0';
664 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
665 valueXOffset = 1;
667 #ifdef USE_TEMPERATURE_SENSOR
668 else if (label[0] != '\0') {
669 uint8_t label_len = strnlen(label, TEMPERATURE_LABEL_LEN);
670 memcpy(buff, label, label_len);
671 memset(buff + label_len, ' ', TEMPERATURE_LABEL_LEN + 1 - label_len);
672 buff[5] = '\0';
673 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
674 valueXOffset = osdConfig()->temp_label_align == OSD_ALIGN_LEFT ? 5 : label_len + 1;
676 #else
677 UNUSED(label);
678 #endif
680 if (valid) {
682 if ((temperature <= alarm_min) || (temperature >= alarm_max)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
683 if (osdConfig()->units == OSD_UNIT_IMPERIAL) temperature = temperature * 9 / 5.0f + 320;
684 tfp_sprintf(buff, "%3d", temperature / 10);
686 } else
687 strcpy(buff, "---");
689 buff[3] = osdConfig()->units == OSD_UNIT_IMPERIAL ? SYM_TEMP_F : SYM_TEMP_C;
690 buff[4] = '\0';
692 displayWriteWithAttr(osdDisplayPort, elemPosX + valueXOffset, elemPosY, buff, elemAttr);
695 #ifdef USE_TEMPERATURE_SENSOR
696 static void osdDisplayTemperatureSensor(uint8_t elemPosX, uint8_t elemPosY, uint8_t sensorIndex)
698 int16_t temperature;
699 const bool valid = getSensorTemperature(sensorIndex, &temperature);
700 const tempSensorConfig_t *sensorConfig = tempSensorConfig(sensorIndex);
701 uint16_t symbol = sensorConfig->osdSymbol ? SYM_TEMP_SENSOR_FIRST + sensorConfig->osdSymbol - 1 : 0;
702 osdDisplayTemperature(elemPosX, elemPosY, symbol, sensorConfig->label, valid, temperature, sensorConfig->alarm_min, sensorConfig->alarm_max);
704 #endif
706 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
708 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
709 const int coordinateLength = osdConfig()->coordinate_digits + 1;
711 buff[0] = sym;
712 int32_t integerPart = val / GPS_DEGREES_DIVIDER;
713 // Latitude maximum integer width is 3 (-90) while
714 // longitude maximum integer width is 4 (-180).
715 int integerDigits = tfp_sprintf(buff + 1, (integerPart == 0 && val < 0) ? "-%d" : "%d", (int)integerPart);
716 // We can show up to 7 digits in decimalPart.
717 int32_t decimalPart = abs(val % (int)GPS_DEGREES_DIVIDER);
718 STATIC_ASSERT(GPS_DEGREES_DIVIDER == 1e7, adjust_max_decimal_digits);
719 int decimalDigits;
720 bool djiCompat = false; // Assume DJICOMPAT mode is no enabled
722 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
723 if(isDJICompatibleVideoSystem(osdConfig())) {
724 djiCompat = true;
726 #endif
728 if (!djiCompat) {
729 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, "%07d", (int)decimalPart);
730 // Embbed the decimal separator
731 buff[1 + integerDigits - 1] += SYM_ZERO_HALF_TRAILING_DOT - '0';
732 buff[1 + integerDigits] += SYM_ZERO_HALF_LEADING_DOT - '0';
733 } else {
734 // DJICOMPAT mode enabled
735 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, ".%06d", (int)decimalPart);
737 // Fill up to coordinateLength with zeros
738 int total = 1 + integerDigits + decimalDigits;
739 while(total < coordinateLength) {
740 buff[total] = '0';
741 total++;
743 buff[coordinateLength] = '\0';
746 static void osdFormatCraftName(char *buff)
748 if (strlen(systemConfig()->craftName) == 0)
749 strcpy(buff, "CRAFT_NAME");
750 else {
751 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
752 buff[i] = sl_toupper((unsigned char)systemConfig()->craftName[i]);
753 if (systemConfig()->craftName[i] == 0)
754 break;
759 void osdFormatPilotName(char *buff)
761 if (strlen(systemConfig()->pilotName) == 0)
762 strcpy(buff, "PILOT_NAME");
763 else {
764 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
765 buff[i] = sl_toupper((unsigned char)systemConfig()->pilotName[i]);
766 if (systemConfig()->pilotName[i] == 0)
767 break;
772 static const char * osdArmingDisabledReasonMessage(void)
774 const char *message = NULL;
775 static char messageBuf[OSD_MESSAGE_LENGTH+1];
777 switch (isArmingDisabledReason()) {
778 case ARMING_DISABLED_FAILSAFE_SYSTEM:
779 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
780 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING) {
781 if (failsafeIsReceivingRxData()) {
782 // reminder to disarm to exit FAILSAFE_RX_LOSS_MONITORING once timeout period ends
783 if (IS_RC_MODE_ACTIVE(BOXARM)) {
784 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF);
786 } else {
787 // Not receiving RX data
788 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
791 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS);
792 case ARMING_DISABLED_NOT_LEVEL:
793 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL);
794 case ARMING_DISABLED_SENSORS_CALIBRATING:
795 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL);
796 case ARMING_DISABLED_SYSTEM_OVERLOADED:
797 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED);
798 case ARMING_DISABLED_NAVIGATION_UNSAFE:
799 // Check the exact reason
800 switch (navigationIsBlockingArming(NULL)) {
801 char buf[6];
802 case NAV_ARMING_BLOCKER_NONE:
803 break;
804 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX:
805 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX);
806 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE:
807 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST);
808 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR:
809 osdFormatDistanceSymbol(buf, distanceToFirstWP(), 0);
810 tfp_sprintf(messageBuf, "FIRST WP TOO FAR (%s)", buf);
811 return message = messageBuf;
812 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR:
813 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG);
815 break;
816 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED:
817 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL);
818 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED:
819 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL);
820 case ARMING_DISABLED_ARM_SWITCH:
821 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST);
822 case ARMING_DISABLED_HARDWARE_FAILURE:
824 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
825 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE);
827 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
828 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL);
830 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
831 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL);
833 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
834 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL);
836 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
837 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL);
839 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
840 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL);
842 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
843 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL);
846 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL);
847 case ARMING_DISABLED_BOXFAILSAFE:
848 return OSD_MESSAGE_STR(OSD_MSG_FS_EN);
849 case ARMING_DISABLED_RC_LINK:
850 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK);
851 case ARMING_DISABLED_THROTTLE:
852 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW);
853 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED:
854 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER);
855 case ARMING_DISABLED_SERVO_AUTOTRIM:
856 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE);
857 case ARMING_DISABLED_OOM:
858 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY);
859 case ARMING_DISABLED_INVALID_SETTING:
860 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
861 case ARMING_DISABLED_CLI:
862 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE);
863 case ARMING_DISABLED_PWM_OUTPUT_ERROR:
864 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR);
865 case ARMING_DISABLED_NO_PREARM:
866 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM);
867 case ARMING_DISABLED_DSHOT_BEEPER:
868 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER);
869 // Cases without message
870 case ARMING_DISABLED_LANDING_DETECTED:
871 FALLTHROUGH;
872 case ARMING_DISABLED_CMS_MENU:
873 FALLTHROUGH;
874 case ARMING_DISABLED_OSD_MENU:
875 FALLTHROUGH;
876 case ARMING_DISABLED_ALL_FLAGS:
877 FALLTHROUGH;
878 case ARMED:
879 FALLTHROUGH;
880 case SIMULATOR_MODE_HITL:
881 FALLTHROUGH;
882 case SIMULATOR_MODE_SITL:
883 FALLTHROUGH;
884 case WAS_EVER_ARMED:
885 break;
887 return NULL;
890 static const char * osdFailsafePhaseMessage(void)
892 // See failsafe.h for each phase explanation
893 switch (failsafePhase()) {
894 case FAILSAFE_RETURN_TO_HOME:
895 // XXX: Keep this in sync with OSD_FLYMODE.
896 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS);
897 case FAILSAFE_LANDING:
898 // This should be considered an emergengy landing
899 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS);
900 case FAILSAFE_RX_LOSS_MONITORING:
901 // Only reachable from FAILSAFE_LANDED, which performs
902 // a disarm. Since aircraft has been disarmed, we no
903 // longer show failsafe details.
904 FALLTHROUGH;
905 case FAILSAFE_LANDED:
906 // Very brief, disarms and transitions into
907 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
908 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
909 // so we'll show the user how to re-arm in when
910 // that flag is the reason to prevent arming.
911 FALLTHROUGH;
912 case FAILSAFE_RX_LOSS_IDLE:
913 // This only happens when user has chosen NONE as FS
914 // procedure. The recovery messages should be enough.
915 FALLTHROUGH;
916 case FAILSAFE_IDLE:
917 // Failsafe not active
918 FALLTHROUGH;
919 case FAILSAFE_RX_LOSS_DETECTED:
920 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
921 // or the FS procedure immediately.
922 FALLTHROUGH;
923 case FAILSAFE_RX_LOSS_RECOVERED:
924 // Exiting failsafe
925 break;
927 return NULL;
930 static const char * osdFailsafeInfoMessage(void)
932 if (failsafeIsReceivingRxData() && !FLIGHT_MODE(NAV_FW_AUTOLAND)) {
933 // User must move sticks to exit FS mode
934 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS);
936 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
939 #if defined(USE_SAFE_HOME)
940 static const char * divertingToSafehomeMessage(void)
942 #ifdef USE_FW_AUTOLAND
943 if (!posControl.fwLandState.landWp && (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && posControl.safehomeState.isApplied)) {
944 #else
945 if (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && posControl.safehomeState.isApplied) {
946 #endif
947 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME);
949 #endif
950 return NULL;
954 static const char * navigationStateMessage(void)
956 if (!posControl.rthState.rthLinearDescentActive && linearDescentMessageMs != 0)
957 linearDescentMessageMs = 0;
959 switch (NAV_Status.state) {
960 case MW_NAV_STATE_NONE:
961 break;
962 case MW_NAV_STATE_RTH_START:
963 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH);
964 case MW_NAV_STATE_RTH_CLIMB:
965 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB);
966 case MW_NAV_STATE_RTH_ENROUTE:
967 if (posControl.flags.rthTrackbackActive) {
968 return OSD_MESSAGE_STR(OSD_MSG_RTH_TRACKBACK);
969 } else {
970 if (posControl.rthState.rthLinearDescentActive && (linearDescentMessageMs == 0 || linearDescentMessageMs > millis())) {
971 if (linearDescentMessageMs == 0)
972 linearDescentMessageMs = millis() + 5000; // Show message for 5 seconds.
974 return OSD_MESSAGE_STR(OSD_MSG_RTH_LINEAR_DESCENT);
975 } else
976 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME);
978 case MW_NAV_STATE_HOLD_INFINIT:
979 // Used by HOLD flight modes. No information to add.
980 break;
981 case MW_NAV_STATE_HOLD_TIMED:
982 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
983 break;
984 case MW_NAV_STATE_WP_ENROUTE:
985 // "TO WP" + WP countdown added in osdGetSystemMessage
986 break;
987 case MW_NAV_STATE_PROCESS_NEXT:
988 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP);
989 case MW_NAV_STATE_DO_JUMP:
990 // Not used
991 break;
992 case MW_NAV_STATE_LAND_START:
993 // Not used
994 break;
995 case MW_NAV_STATE_EMERGENCY_LANDING:
996 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING);
997 case MW_NAV_STATE_LAND_IN_PROGRESS:
998 return OSD_MESSAGE_STR(OSD_MSG_LANDING);
999 case MW_NAV_STATE_HOVER_ABOVE_HOME:
1000 if (STATE(FIXED_WING_LEGACY)) {
1001 #if defined(USE_SAFE_HOME)
1002 if (posControl.safehomeState.isApplied) {
1003 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME);
1005 #endif
1006 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME);
1008 return OSD_MESSAGE_STR(OSD_MSG_HOVERING);
1009 case MW_NAV_STATE_LANDED:
1010 return OSD_MESSAGE_STR(OSD_MSG_LANDED);
1011 case MW_NAV_STATE_LAND_SETTLE:
1013 // If there is a FS landing delay occurring. That is handled by the calling function.
1014 if (posControl.landingDelay > 0)
1015 break;
1017 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND);
1019 case MW_NAV_STATE_LAND_START_DESCENT:
1020 // Not used
1021 break;
1024 return NULL;
1027 static void osdFormatMessage(char *buff, size_t size, const char *message, bool isCenteredText)
1029 // String is always filled with Blanks
1030 memset(buff, SYM_BLANK, size);
1031 if (message) {
1032 size_t messageLength = strlen(message);
1033 int rem = isCenteredText ? MAX(0, (int)size - (int)messageLength) : 0;
1034 strncpy(buff + rem / 2, message, MIN((int)size - rem / 2, (int)messageLength));
1036 // Ensure buff is zero terminated
1037 buff[size] = '\0';
1041 * Draws the battery symbol filled in accordingly to the
1042 * battery voltage to buff[0].
1044 static void osdFormatBatteryChargeSymbol(char *buff)
1046 uint8_t p = calculateBatteryPercentage();
1047 p = (100 - p) / 16.6;
1048 buff[0] = SYM_BATT_FULL + p;
1051 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t *attr)
1053 const batteryState_e batteryState = getBatteryState();
1055 if (batteryState == BATTERY_WARNING || batteryState == BATTERY_CRITICAL) {
1056 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
1060 void osdCrosshairPosition(uint8_t *x, uint8_t *y)
1062 *x = osdDisplayPort->cols / 2;
1063 *y = osdDisplayPort->rows / 2;
1064 *y -= osdConfig()->horizon_offset; // positive horizon_offset moves the HUD up, negative moves down
1068 * Check if this OSD layout is using scaled or unscaled throttle.
1069 * If both are used, it will default to scaled.
1071 bool osdUsingScaledThrottle(void)
1073 bool usingScaledThrottle = OSD_VISIBLE(osdLayoutsConfig()->item_pos[currentLayout][OSD_SCALED_THROTTLE_POS]);
1074 bool usingRCThrottle = OSD_VISIBLE(osdLayoutsConfig()->item_pos[currentLayout][OSD_THROTTLE_POS]);
1076 if (!usingScaledThrottle && !usingRCThrottle)
1077 usingScaledThrottle = true;
1079 return usingScaledThrottle;
1083 * Formats throttle position prefixed by its symbol.
1084 * Shows unscaled or scaled (output to motor) throttle percentage
1086 static void osdFormatThrottlePosition(char *buff, bool useScaled, textAttributes_t *elemAttr)
1088 buff[0] = SYM_BLANK;
1089 buff[1] = SYM_THR;
1090 if (navigationIsControllingThrottle()) {
1091 buff[0] = SYM_AUTO_THR0;
1092 buff[1] = SYM_AUTO_THR1;
1093 if (isFixedWingAutoThrottleManuallyIncreased()) {
1094 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1096 useScaled = true;
1098 #ifdef USE_POWER_LIMITS
1099 if (powerLimiterIsLimiting()) {
1100 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1102 #endif
1103 int8_t throttlePercent = getThrottlePercent(useScaled);
1104 if ((useScaled && throttlePercent <= 0) || !ARMING_FLAG(ARMED)) {
1105 const char* message = ARMING_FLAG(ARMED) ? (throttlePercent == 0 && !ifMotorstopFeatureEnabled()) ? "IDLE" : "STOP" : "DARM";
1106 buff[0] = SYM_THR;
1107 strcpy(buff + 1, message);
1108 return;
1110 tfp_sprintf(buff + 2, "%3d", throttlePercent);
1114 * Formats gvars prefixed by its number (0-indexed). If autoThr
1116 static void osdFormatGVar(char *buff, uint8_t index)
1118 buff[0] = 'G';
1119 buff[1] = '0'+index;
1120 buff[2] = ':';
1121 #ifdef USE_PROGRAMMING_FRAMEWORK
1122 osdFormatCentiNumber(buff + 3, (int32_t)gvGet(index)*(int32_t)100, 1, 0, 0, 5, false);
1123 #endif
1126 #if defined(USE_ESC_SENSOR)
1127 static void osdFormatRpm(char *buff, uint32_t rpm)
1129 buff[0] = SYM_RPM;
1130 if (rpm) {
1131 if ( digitCount(rpm) > osdConfig()->esc_rpm_precision) {
1132 uint8_t rpmMaxDecimals = (osdConfig()->esc_rpm_precision - 3);
1133 osdFormatCentiNumber(buff + 1, rpm / 10, 0, rpmMaxDecimals, rpmMaxDecimals, osdConfig()->esc_rpm_precision-1, false);
1134 buff[osdConfig()->esc_rpm_precision] = 'K';
1135 buff[osdConfig()->esc_rpm_precision+1] = '\0';
1137 else {
1138 switch(osdConfig()->esc_rpm_precision) {
1139 case 6:
1140 tfp_sprintf(buff + 1, "%6lu", rpm);
1141 break;
1142 case 5:
1143 tfp_sprintf(buff + 1, "%5lu", rpm);
1144 break;
1145 case 4:
1146 tfp_sprintf(buff + 1, "%4lu", rpm);
1147 break;
1148 case 3:
1149 default:
1150 tfp_sprintf(buff + 1, "%3lu", rpm);
1151 break;
1157 else {
1158 uint8_t buffPos = 1;
1159 while (buffPos <=( osdConfig()->esc_rpm_precision)) {
1160 strcpy(buff + buffPos++, "-");
1164 #endif
1166 int32_t osdGetAltitude(void)
1168 return getEstimatedActualPosition(Z);
1171 static inline int32_t osdGetAltitudeMsl(void)
1173 return getEstimatedActualPosition(Z) + posControl.gpsOrigin.alt;
1176 uint16_t osdGetRemainingGlideTime(void) {
1177 float value = getEstimatedActualVelocity(Z);
1178 static pt1Filter_t glideTimeFilterState;
1179 const timeMs_t curTimeMs = millis();
1180 static timeMs_t glideTimeUpdatedMs;
1182 value = pt1FilterApply4(&glideTimeFilterState, isnormal(value) ? value : 0, 0.5, MS2S(curTimeMs - glideTimeUpdatedMs));
1183 glideTimeUpdatedMs = curTimeMs;
1185 if (value < 0) {
1186 value = osdGetAltitude() / abs((int)value);
1187 } else {
1188 value = 0;
1191 return (uint16_t)roundf(value);
1194 static bool osdIsHeadingValid(void)
1196 return isImuHeadingValid();
1199 int16_t osdGetHeading(void)
1201 return attitude.values.yaw;
1204 int16_t osdGetPanServoOffset(void)
1206 int8_t servoIndex = osdConfig()->pan_servo_index;
1207 int16_t servoMiddle = servoParams(servoIndex)->middle;
1208 int16_t servoPosition = servo[servoIndex];
1210 gimbalDevice_t *dev = gimbalCommonDevice();
1211 if (dev && gimbalCommonIsReady(dev)) {
1212 servoPosition = gimbalCommonGetPanPwm(dev);
1213 servoMiddle = PWM_RANGE_MIDDLE + gimbalConfig()->panTrim;
1216 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition - servoMiddle) * osdConfig()->pan_servo_pwm2centideg);
1219 // Returns a heading angle in degrees normalized to [0, 360).
1220 int osdGetHeadingAngle(int angle)
1222 while (angle < 0) {
1223 angle += 360;
1225 while (angle >= 360) {
1226 angle -= 360;
1228 return angle;
1231 #if defined(USE_GPS)
1233 /* Draws a map with the given symbol in the center and given point of interest
1234 * defined by its distance in meters and direction in degrees.
1235 * referenceHeading indicates the up direction in the map, in degrees, while
1236 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1237 * arrow to indicate the map reference to the user. The drawn argument is an
1238 * in-out used to store the last position where the craft was drawn to avoid
1239 * erasing all screen on each redraw.
1241 static void osdDrawMap(int referenceHeading, uint16_t referenceSym, uint16_t centerSym,
1242 uint32_t poiDistance, int16_t poiDirection, uint16_t poiSymbol,
1243 uint16_t *drawn, uint32_t *usedScale)
1245 // TODO: These need to be tested with several setups. We might
1246 // need to make them configurable.
1247 const int hMargin = 5;
1248 const int vMargin = 3;
1250 // TODO: Get this from the display driver?
1251 const int charWidth = 12;
1252 const int charHeight = 18;
1254 uint8_t minX = hMargin;
1255 uint8_t maxX = osdDisplayPort->cols - 1 - hMargin;
1256 uint8_t minY = vMargin;
1257 uint8_t maxY = osdDisplayPort->rows - 1 - vMargin;
1258 uint8_t midX = osdDisplayPort->cols / 2;
1259 uint8_t midY = osdDisplayPort->rows / 2;
1261 // Fixed marks
1262 displayWriteChar(osdDisplayPort, midX, midY, centerSym);
1264 // First, erase the previous drawing.
1265 if (OSD_VISIBLE(*drawn)) {
1266 displayWriteChar(osdDisplayPort, OSD_X(*drawn), OSD_Y(*drawn), SYM_BLANK);
1267 *drawn = 0;
1270 uint32_t initialScale;
1271 const unsigned scaleMultiplier = 2;
1272 // We try to reduce the scale when the POI will be around half the distance
1273 // between the center and the closers map edge, to avoid too much jumping
1274 const int scaleReductionMultiplier = MIN(midX - hMargin, midY - vMargin) / 2;
1276 switch (osdConfig()->units) {
1277 case OSD_UNIT_UK:
1278 FALLTHROUGH;
1279 case OSD_UNIT_IMPERIAL:
1280 initialScale = 16; // 16m ~= 0.01miles
1281 break;
1282 case OSD_UNIT_GA:
1283 initialScale = 18; // 18m ~= 0.01 nautical miles
1284 break;
1285 default:
1286 case OSD_UNIT_METRIC_MPH:
1287 FALLTHROUGH;
1288 case OSD_UNIT_METRIC:
1289 initialScale = 10; // 10m as initial scale
1290 break;
1293 // Try to keep the same scale when getting closer until we draw over the center point
1294 uint32_t scale = initialScale;
1295 if (*usedScale) {
1296 scale = *usedScale;
1297 if (scale > initialScale && poiDistance < *usedScale * scaleReductionMultiplier) {
1298 scale /= scaleMultiplier;
1302 if (STATE(GPS_FIX)
1303 #ifdef USE_GPS_FIX_ESTIMATION
1304 || STATE(GPS_ESTIMATED_FIX)
1305 #endif
1308 int directionToPoi = osdGetHeadingAngle(poiDirection - referenceHeading);
1309 float poiAngle = DEGREES_TO_RADIANS(directionToPoi);
1310 float poiSin = sin_approx(poiAngle);
1311 float poiCos = cos_approx(poiAngle);
1313 // Now start looking for a valid scale that lets us draw everything
1314 int ii;
1315 for (ii = 0; ii < 50; ii++) {
1316 // Calculate location of the aircraft in map
1317 int points = poiDistance / ((float)scale / charHeight);
1319 float pointsX = points * poiSin;
1320 int poiX = midX - roundf(pointsX / charWidth);
1321 if (poiX < minX || poiX > maxX) {
1322 scale *= scaleMultiplier;
1323 continue;
1326 float pointsY = points * poiCos;
1327 int poiY = midY + roundf(pointsY / charHeight);
1328 if (poiY < minY || poiY > maxY) {
1329 scale *= scaleMultiplier;
1330 continue;
1333 if (poiX == midX && poiY == midY) {
1334 // We're over the map center symbol, so we would be drawing
1335 // over it even if we increased the scale. Alternate between
1336 // drawing the center symbol or drawing the POI.
1337 if (centerSym != SYM_BLANK && OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1338 break;
1340 } else {
1342 uint16_t c;
1343 if (displayReadCharWithAttr(osdDisplayPort, poiX, poiY, &c, NULL) && c != SYM_BLANK) {
1344 // Something else written here, increase scale. If the display doesn't support reading
1345 // back characters, we assume there's nothing.
1347 // If we're close to the center, decrease scale. Otherwise increase it.
1348 uint8_t centerDeltaX = (maxX - minX) / (scaleMultiplier * 2);
1349 uint8_t centerDeltaY = (maxY - minY) / (scaleMultiplier * 2);
1350 if (poiX >= midX - centerDeltaX && poiX <= midX + centerDeltaX &&
1351 poiY >= midY - centerDeltaY && poiY <= midY + centerDeltaY &&
1352 scale > scaleMultiplier) {
1354 scale /= scaleMultiplier;
1355 } else {
1356 scale *= scaleMultiplier;
1358 continue;
1362 // Draw the point on the map
1363 if (poiSymbol == SYM_ARROW_UP) {
1364 // Drawing aircraft, rotate
1365 int mapHeading = osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading);
1366 poiSymbol += mapHeading * 2 / 45;
1368 displayWriteChar(osdDisplayPort, poiX, poiY, poiSymbol);
1370 // Update saved location
1371 *drawn = OSD_POS(poiX, poiY) | OSD_VISIBLE_FLAG;
1372 break;
1376 *usedScale = scale;
1378 // Update global map data for scale and reference
1379 osdMapData.scale = scale;
1380 osdMapData.referenceSymbol = referenceSym;
1383 /* Draws a map with the home in the center and the craft moving around.
1384 * See osdDrawMap() for reference.
1386 static void osdDrawHomeMap(int referenceHeading, uint8_t referenceSym, uint16_t *drawn, uint32_t *usedScale)
1388 osdDrawMap(referenceHeading, referenceSym, SYM_HOME, GPS_distanceToHome, GPS_directionToHome, SYM_ARROW_UP, drawn, usedScale);
1391 /* Draws a map with the aircraft in the center and the home moving around.
1392 * See osdDrawMap() for reference.
1394 static void osdDrawRadar(uint16_t *drawn, uint32_t *usedScale)
1396 int16_t reference = DECIDEGREES_TO_DEGREES(osdGetHeading());
1397 int16_t poiDirection = osdGetHeadingAngle(GPS_directionToHome + 180);
1398 osdDrawMap(reference, 0, SYM_ARROW_UP, GPS_distanceToHome, poiDirection, SYM_HOME, drawn, usedScale);
1401 static uint16_t crc_accumulate(uint8_t data, uint16_t crcAccum)
1403 uint8_t tmp;
1404 tmp = data ^ (uint8_t)(crcAccum & 0xff);
1405 tmp ^= (tmp << 4);
1406 crcAccum = (crcAccum >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
1407 return crcAccum;
1411 static void osdDisplayTelemetry(void)
1413 uint32_t trk_data;
1414 uint16_t trk_crc = 0;
1415 char trk_buffer[31];
1416 static int16_t trk_elevation = 127;
1417 static uint16_t trk_bearing = 0;
1419 if (ARMING_FLAG(ARMED)) {
1420 if (STATE(GPS_FIX)
1421 #ifdef USE_GPS_FIX_ESTIMATION
1422 || STATE(GPS_ESTIMATED_FIX)
1423 #endif
1425 if (GPS_distanceToHome > 5) {
1426 trk_bearing = GPS_directionToHome;
1427 trk_bearing += 360 + 180;
1428 trk_bearing %= 360;
1429 int32_t alt = CENTIMETERS_TO_METERS(osdGetAltitude());
1430 float at = atan2(alt, GPS_distanceToHome);
1431 trk_elevation = at * 57.2957795f; // 57.2957795 = 1 rad
1432 trk_elevation += 37; // because elevation in telemetry should be from -37 to 90
1433 if (trk_elevation < 0) {
1434 trk_elevation = 0;
1439 else{
1440 trk_elevation = 127;
1441 trk_bearing = 0;
1444 trk_data = 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1445 trk_data = trk_data | (uint32_t)(0x7F & trk_elevation) << 1; // bits 1-7 - elevation angle to target. NOTE number is abused. constrained value of -37 to 90 sent as 0 to 127.
1446 trk_data = trk_data | (uint32_t)trk_bearing << 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1447 trk_crc = crc_accumulate(0xFF & trk_data, trk_crc); // CRC First Byte bits 0-7
1448 trk_crc = crc_accumulate(0xFF & trk_bearing, trk_crc); // CRC Second Byte bits 8-15
1449 trk_crc = crc_accumulate(trk_bearing >> 8, trk_crc); // CRC Third Byte bits 16-17
1450 trk_data = trk_data | (uint32_t)trk_crc << 17; // bits 18-29 CRC & 0x3FFFF
1452 for (uint8_t t_ctr = 0; t_ctr < 30; t_ctr++) { // Prepare screen buffer and write data line.
1453 if (trk_data & (uint32_t)1 << t_ctr){
1454 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_0;
1456 else{
1457 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_1;
1460 trk_buffer[30] = 0;
1461 displayWrite(osdDisplayPort, 0, 0, trk_buffer);
1462 if (osdConfig()->telemetry>1){
1463 displayWrite(osdDisplayPort, 0, 3, trk_buffer); // Test display because normal telemetry line is not visible
1466 #endif
1468 static void osdFormatPidControllerOutput(char *buff, const char *label, const pidController_t *pidController, uint8_t scale, bool showDecimal) {
1469 strcpy(buff, label);
1470 for (uint8_t i = strlen(label); i < 5; ++i) buff[i] = ' ';
1471 uint8_t decimals = showDecimal ? 1 : 0;
1472 osdFormatCentiNumber(buff + 5, pidController->proportional * scale, 0, decimals, 0, 4, false);
1473 buff[9] = ' ';
1474 osdFormatCentiNumber(buff + 10, pidController->integrator * scale, 0, decimals, 0, 4, false);
1475 buff[14] = ' ';
1476 osdFormatCentiNumber(buff + 15, pidController->derivative * scale, 0, decimals, 0, 4, false);
1477 buff[19] = ' ';
1478 osdFormatCentiNumber(buff + 20, pidController->output_constrained * scale, 0, decimals, 0, 4, false);
1479 buff[24] = '\0';
1482 static void osdDisplayBatteryVoltage(uint8_t elemPosX, uint8_t elemPosY, uint16_t voltage, uint8_t digits, uint8_t decimals)
1484 char buff[7];
1485 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1487 osdFormatBatteryChargeSymbol(buff);
1488 buff[1] = '\0';
1489 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1490 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1492 elemAttr = TEXT_ATTRIBUTES_NONE;
1493 digits = MIN(digits, 5);
1494 osdFormatCentiNumber(buff, voltage, 0, decimals, 0, digits, false);
1495 buff[digits] = SYM_VOLT;
1496 buff[digits+1] = '\0';
1497 const batteryState_e batteryVoltageState = checkBatteryVoltageState();
1498 if (batteryVoltageState == BATTERY_CRITICAL || batteryVoltageState == BATTERY_WARNING) {
1499 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1501 displayWriteWithAttr(osdDisplayPort, elemPosX + 1, elemPosY, buff, elemAttr);
1504 static void osdDisplayFlightPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD, adjustmentFunction_e adjFuncFF)
1506 textAttributes_t elemAttr;
1507 char buff[4];
1509 const pid8_t *pid = &pidBank()->pid[pidIndex];
1510 pidType_e pidType = pidIndexGetType(pidIndex);
1512 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1514 if (pidType == PID_TYPE_NONE) {
1515 // PID is not used in this configuration. Draw dashes.
1516 // XXX: Keep this in sync with the %3d format and spacing used below
1517 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - - -");
1518 return;
1521 elemAttr = TEXT_ATTRIBUTES_NONE;
1522 tfp_sprintf(buff, "%3d", pid->P);
1523 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1524 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1525 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1527 elemAttr = TEXT_ATTRIBUTES_NONE;
1528 tfp_sprintf(buff, "%3d", pid->I);
1529 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1530 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1531 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1533 elemAttr = TEXT_ATTRIBUTES_NONE;
1534 tfp_sprintf(buff, "%3d", pid->D);
1535 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1536 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1537 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1539 elemAttr = TEXT_ATTRIBUTES_NONE;
1540 tfp_sprintf(buff, "%3d", pid->FF);
1541 if ((isAdjustmentFunctionSelected(adjFuncFF)) || (((adjFuncFF == ADJUSTMENT_ROLL_FF) || (adjFuncFF == ADJUSTMENT_PITCH_FF)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF))))
1542 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1543 displayWriteWithAttr(osdDisplayPort, elemPosX + 16, elemPosY, buff, elemAttr);
1546 static void osdDisplayNavPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD)
1548 textAttributes_t elemAttr;
1549 char buff[4];
1551 const pid8_t *pid = &pidBank()->pid[pidIndex];
1552 pidType_e pidType = pidIndexGetType(pidIndex);
1554 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1556 if (pidType == PID_TYPE_NONE) {
1557 // PID is not used in this configuration. Draw dashes.
1558 // XXX: Keep this in sync with the %3d format and spacing used below
1559 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - -");
1560 return;
1563 elemAttr = TEXT_ATTRIBUTES_NONE;
1564 tfp_sprintf(buff, "%3d", pid->P);
1565 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1566 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1567 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1569 elemAttr = TEXT_ATTRIBUTES_NONE;
1570 tfp_sprintf(buff, "%3d", pid->I);
1571 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1572 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1573 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1575 elemAttr = TEXT_ATTRIBUTES_NONE;
1576 tfp_sprintf(buff, "%3d", pidType == PID_TYPE_PIFF ? pid->FF : pid->D);
1577 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1578 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1579 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1582 static void osdDisplayAdjustableDecimalValue(uint8_t elemPosX, uint8_t elemPosY, const char *str, const uint8_t valueOffset, const float value, const uint8_t valueLength, const uint8_t maxDecimals, adjustmentFunction_e adjFunc) {
1583 char buff[8];
1584 textAttributes_t elemAttr;
1585 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1587 elemAttr = TEXT_ATTRIBUTES_NONE;
1588 osdFormatCentiNumber(buff, value * 100, 0, maxDecimals, 0, MIN(valueLength, 8), false);
1589 if (isAdjustmentFunctionSelected(adjFunc))
1590 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1591 displayWriteWithAttr(osdDisplayPort, elemPosX + strlen(str) + 1 + valueOffset, elemPosY, buff, elemAttr);
1594 int8_t getGeoWaypointNumber(int8_t waypointIndex)
1596 static int8_t lastWaypointIndex = 1;
1597 static int8_t geoWaypointIndex;
1599 if (waypointIndex != lastWaypointIndex) {
1600 lastWaypointIndex = geoWaypointIndex = waypointIndex;
1601 for (uint8_t i = posControl.startWpIndex; i <= waypointIndex; i++) {
1602 if (posControl.waypointList[i].action == NAV_WP_ACTION_SET_POI ||
1603 posControl.waypointList[i].action == NAV_WP_ACTION_SET_HEAD ||
1604 posControl.waypointList[i].action == NAV_WP_ACTION_JUMP) {
1605 geoWaypointIndex -= 1;
1610 return geoWaypointIndex - posControl.startWpIndex + 1;
1613 void osdDisplaySwitchIndicator(const char *swName, int rcValue, char *buff) {
1614 int8_t ptr = 0;
1616 if (osdConfig()->osd_switch_indicators_align_left) {
1617 for (ptr = 0; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH); ptr++) {
1618 buff[ptr] = swName[ptr];
1621 if ( rcValue < 1333) {
1622 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1623 } else if ( rcValue > 1666) {
1624 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1625 } else {
1626 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1628 } else {
1629 if ( rcValue < 1333) {
1630 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1631 } else if ( rcValue > 1666) {
1632 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1633 } else {
1634 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1637 for (ptr = 1; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH) + 1; ptr++) {
1638 buff[ptr] = swName[ptr-1];
1641 ptr++;
1644 buff[ptr] = '\0';
1647 static bool osdDrawSingleElement(uint8_t item)
1649 uint16_t pos = osdLayoutsConfig()->item_pos[currentLayout][item];
1650 if (!OSD_VISIBLE(pos)) {
1651 return false;
1653 uint8_t elemPosX = OSD_X(pos);
1654 uint8_t elemPosY = OSD_Y(pos);
1655 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1656 char buff[32] = {0};
1658 switch (item) {
1659 case OSD_CUSTOM_ELEMENT_1:
1661 customElementDrawElement(buff, 0);
1662 break;
1664 case OSD_CUSTOM_ELEMENT_2:
1666 customElementDrawElement(buff, 1);
1667 break;
1669 case OSD_CUSTOM_ELEMENT_3:
1671 customElementDrawElement(buff, 2);
1672 break;
1674 case OSD_RSSI_VALUE:
1676 uint16_t osdRssi = osdConvertRSSI();
1677 buff[0] = SYM_RSSI;
1678 tfp_sprintf(buff + 1, "%2d", osdRssi);
1679 if (osdRssi < osdConfig()->rssi_alarm) {
1680 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1682 break;
1685 case OSD_MAIN_BATT_VOLTAGE: {
1686 uint8_t base_digits = 2U;
1687 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1688 if(isDJICompatibleVideoSystem(osdConfig())) {
1689 base_digits = 3U; // Add extra digit to account for decimal point taking an extra character space
1691 #endif
1692 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawVoltage(), base_digits + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1693 return true;
1696 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE: {
1697 uint8_t base_digits = 2U;
1698 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1699 if(isDJICompatibleVideoSystem(osdConfig())) {
1700 base_digits = 3U; // Add extra digit to account for decimal point taking an extra character space
1702 #endif
1703 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedVoltage(), base_digits + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1704 return true;
1707 case OSD_CURRENT_DRAW: {
1708 osdFormatCentiNumber(buff, getAmperage(), 0, 2, 0, 3, false);
1709 buff[3] = SYM_AMP;
1710 buff[4] = '\0';
1712 uint8_t current_alarm = osdConfig()->current_alarm;
1713 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
1714 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1716 break;
1719 case OSD_MAH_DRAWN: {
1720 uint8_t mah_digits = osdConfig()->mAh_precision; // Initialize to config value
1722 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1723 if (isDJICompatibleVideoSystem(osdConfig())) {
1724 //DJIcompat is unable to work with scaled values and it only has mAh symbol to work with
1725 tfp_sprintf(buff, "%5d", (int)getMAhDrawn()); // Use 5 digits to allow packs below 100Ah
1726 buff[5] = SYM_MAH;
1727 buff[6] = '\0';
1728 } else
1729 #endif
1731 if (osdFormatCentiNumber(buff, getMAhDrawn() * 100, 1000, 0, (mah_digits - 2), mah_digits, false)) {
1732 // Shown in Ah
1733 buff[mah_digits] = SYM_AH;
1734 } else {
1735 // Shown in mAh
1736 buff[mah_digits] = SYM_MAH;
1738 buff[mah_digits + 1] = '\0';
1741 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1742 break;
1745 case OSD_WH_DRAWN:
1746 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3, false);
1747 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1748 buff[3] = SYM_WH;
1749 buff[4] = '\0';
1750 break;
1752 case OSD_BATTERY_REMAINING_CAPACITY:
1754 bool unitsDrawn = false;
1756 if (currentBatteryProfile->capacity.value == 0)
1757 tfp_sprintf(buff, " NA");
1758 else if (!batteryWasFullWhenPluggedIn())
1759 tfp_sprintf(buff, " NF");
1760 else if (batteryMetersConfig()->capacity_unit == BAT_CAPACITY_UNIT_MAH) {
1761 uint8_t mah_digits = osdConfig()->mAh_precision; // Initialize to config value
1763 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1764 if (isDJICompatibleVideoSystem(osdConfig())) {
1765 //DJIcompat is unable to work with scaled values and it only has mAh symbol to work with
1766 tfp_sprintf(buff, "%5d", (int)getBatteryRemainingCapacity()); // Use 5 digits to allow packs below 100Ah
1767 buff[5] = SYM_MAH;
1768 buff[6] = '\0';
1769 unitsDrawn = true;
1770 } else
1771 #endif
1773 if (osdFormatCentiNumber(buff, getBatteryRemainingCapacity() * 100, 1000, 0, (mah_digits - 2), mah_digits, false)) {
1774 // Shown in Ah
1775 buff[mah_digits] = SYM_AH;
1776 } else {
1777 // Shown in mAh
1778 buff[mah_digits] = SYM_MAH;
1780 buff[mah_digits + 1] = '\0';
1781 unitsDrawn = true;
1783 } else // batteryMetersConfig()->capacityUnit == BAT_CAPACITY_UNIT_MWH
1784 osdFormatCentiNumber(buff + 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3, false);
1786 if (!unitsDrawn) {
1787 buff[4] = batteryMetersConfig()->capacity_unit == BAT_CAPACITY_UNIT_MAH ? SYM_MAH : SYM_WH;
1788 buff[5] = '\0';
1791 if (batteryUsesCapacityThresholds()) {
1792 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1795 break;
1797 case OSD_BATTERY_REMAINING_PERCENT:
1798 osdFormatBatteryChargeSymbol(buff);
1799 tfp_sprintf(buff + 1, "%3d%%", calculateBatteryPercentage());
1800 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1801 break;
1803 case OSD_POWER_SUPPLY_IMPEDANCE:
1804 if (isPowerSupplyImpedanceValid())
1805 tfp_sprintf(buff, "%3d", getPowerSupplyImpedance());
1806 else
1807 strcpy(buff, "---");
1808 buff[3] = SYM_MILLIOHM;
1809 buff[4] = '\0';
1810 break;
1812 #ifdef USE_GPS
1813 case OSD_GPS_SATS:
1814 buff[0] = SYM_SAT_L;
1815 buff[1] = SYM_SAT_R;
1816 tfp_sprintf(buff + 2, "%2d", gpsSol.numSat);
1817 #ifdef USE_GPS_FIX_ESTIMATION
1818 if (STATE(GPS_ESTIMATED_FIX)) {
1819 strcpy(buff + 2, "ES");
1820 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1821 } else
1822 #endif
1823 if (!STATE(GPS_FIX)) {
1824 hardwareSensorStatus_e sensorStatus = getHwGPSStatus();
1825 if (sensorStatus == HW_SENSOR_UNAVAILABLE || sensorStatus == HW_SENSOR_UNHEALTHY) {
1826 buff[2] = SYM_ALERT;
1827 buff[3] = '\0';
1829 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1831 break;
1833 case OSD_GPS_SPEED:
1834 osdFormatVelocityStr(buff, gpsSol.groundSpeed, false, false);
1835 break;
1837 case OSD_GPS_MAX_SPEED:
1838 osdFormatVelocityStr(buff, stats.max_speed, false, true);
1839 break;
1841 case OSD_3D_SPEED:
1842 osdFormatVelocityStr(buff, osdGet3DSpeed(), true, false);
1843 break;
1845 case OSD_3D_MAX_SPEED:
1846 osdFormatVelocityStr(buff, stats.max_3D_speed, true, true);
1847 break;
1849 case OSD_GLIDESLOPE:
1851 float horizontalSpeed = gpsSol.groundSpeed;
1852 float sinkRate = -getEstimatedActualVelocity(Z);
1853 static pt1Filter_t gsFilterState;
1854 const timeMs_t currentTimeMs = millis();
1855 static timeMs_t gsUpdatedTimeMs;
1856 float glideSlope = horizontalSpeed / sinkRate;
1857 glideSlope = pt1FilterApply4(&gsFilterState, isnormal(glideSlope) ? glideSlope : 200, 0.5, MS2S(currentTimeMs - gsUpdatedTimeMs));
1858 gsUpdatedTimeMs = currentTimeMs;
1860 buff[0] = SYM_GLIDESLOPE;
1861 if (glideSlope > 0.0f && glideSlope < 100.0f) {
1862 osdFormatCentiNumber(buff + 1, glideSlope * 100.0f, 0, 2, 0, 3, false);
1863 } else {
1864 buff[1] = buff[2] = buff[3] = '-';
1866 buff[4] = '\0';
1867 break;
1870 case OSD_GPS_LAT:
1871 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
1872 break;
1874 case OSD_GPS_LON:
1875 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
1876 break;
1878 case OSD_HOME_DIR:
1880 if ((STATE(GPS_FIX)
1881 #ifdef USE_GPS_FIX_ESTIMATION
1882 || STATE(GPS_ESTIMATED_FIX)
1883 #endif
1884 ) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
1885 if (GPS_distanceToHome < (navConfig()->general.min_rth_distance / 100) ) {
1886 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_HOME_NEAR);
1888 else
1890 int16_t panHomeDirOffset = 0;
1891 if (!(osdConfig()->pan_servo_pwm2centideg == 0)){
1892 panHomeDirOffset = osdGetPanServoOffset();
1894 int16_t flightDirection = STATE(AIRPLANE) ? CENTIDEGREES_TO_DEGREES(posControl.actualState.cog) : DECIDEGREES_TO_DEGREES(osdGetHeading());
1895 int homeDirection = GPS_directionToHome - flightDirection + panHomeDirOffset;
1896 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), homeDirection);
1898 } else {
1899 // No home or no fix or unknown heading, blink.
1900 // If we're unarmed, show the arrow pointing up so users can see the arrow
1901 // while configuring the OSD. If we're armed, show a '-' indicating that
1902 // we don't know the direction to home.
1903 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1904 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, ARMING_FLAG(ARMED) ? '-' : SYM_ARROW_UP, elemAttr);
1906 return true;
1909 case OSD_HOME_HEADING_ERROR:
1911 buff[0] = SYM_HOME;
1912 buff[1] = SYM_HEADING;
1914 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1915 int16_t h = lrintf(CENTIDEGREES_TO_DEGREES((float)wrap_18000(DEGREES_TO_CENTIDEGREES((int32_t)GPS_directionToHome) - (STATE(AIRPLANE) ? posControl.actualState.cog : DECIDEGREES_TO_CENTIDEGREES((int32_t)osdGetHeading())))));
1916 tfp_sprintf(buff + 2, "%4d", h);
1917 } else {
1918 strcpy(buff + 2, "----");
1921 buff[6] = SYM_DEGREES;
1922 buff[7] = '\0';
1923 break;
1926 case OSD_HOME_DIST:
1928 buff[0] = SYM_HOME;
1929 uint32_t distance_to_home_cm = GPS_distanceToHome * 100;
1930 osdFormatDistanceSymbol(&buff[1], distance_to_home_cm, 0);
1932 uint16_t dist_alarm = osdConfig()->dist_alarm;
1933 if (dist_alarm > 0 && GPS_distanceToHome > dist_alarm) {
1934 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1937 break;
1939 case OSD_TRIP_DIST:
1940 buff[0] = SYM_TOTAL;
1941 osdFormatDistanceSymbol(buff + 1, getTotalTravelDistance(), 0);
1942 break;
1944 case OSD_BLACKBOX:
1946 #ifdef USE_BLACKBOX
1947 if (IS_RC_MODE_ACTIVE(BOXBLACKBOX)) {
1948 if (!isBlackboxDeviceWorking()) {
1949 tfp_sprintf(buff, "%c%c", SYM_BLACKBOX, SYM_ALERT);
1950 } else if (isBlackboxDeviceFull()) {
1951 tfp_sprintf(buff, "%cFULL", SYM_BLACKBOX);
1952 } else {
1953 int32_t logNumber = blackboxGetLogNumber();
1954 if (logNumber >= 0) {
1955 tfp_sprintf(buff, "%c%05" PRId32, SYM_BLACKBOX, logNumber);
1956 } else {
1957 tfp_sprintf(buff, "%c", SYM_BLACKBOX);
1961 #endif // USE_BLACKBOX
1963 break;
1965 case OSD_ODOMETER:
1967 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_ODOMETER);
1968 float_t odometerDist = CENTIMETERS_TO_METERS(getTotalTravelDistance());
1969 #ifdef USE_STATS
1970 odometerDist+= statsConfig()->stats_total_dist;
1971 #endif
1973 switch (osdConfig()->units) {
1974 case OSD_UNIT_UK:
1975 FALLTHROUGH;
1976 case OSD_UNIT_IMPERIAL:
1977 osdFormatCentiNumber(buff, METERS_TO_MILES(odometerDist) * 100, 1, 1, 1, 6, true);
1978 buff[6] = SYM_MI;
1979 break;
1980 default:
1981 case OSD_UNIT_GA:
1982 osdFormatCentiNumber(buff, METERS_TO_NAUTICALMILES(odometerDist) * 100, 1, 1, 1, 6, true);
1983 buff[6] = SYM_NM;
1984 break;
1985 case OSD_UNIT_METRIC_MPH:
1986 FALLTHROUGH;
1987 case OSD_UNIT_METRIC:
1988 osdFormatCentiNumber(buff, METERS_TO_KILOMETERS(odometerDist) * 100, 1, 1, 1, 6, true);
1989 buff[6] = SYM_KM;
1990 break;
1992 buff[7] = '\0';
1993 elemPosX++;
1995 break;
1997 case OSD_GROUND_COURSE:
1999 buff[0] = SYM_GROUND_COURSE;
2000 if (osdIsHeadingValid()) {
2001 tfp_sprintf(&buff[1], "%3d", (int16_t)CENTIDEGREES_TO_DEGREES(posControl.actualState.cog));
2002 } else {
2003 buff[1] = buff[2] = buff[3] = '-';
2005 buff[4] = SYM_DEGREES;
2006 buff[5] = '\0';
2007 break;
2010 case OSD_COURSE_HOLD_ERROR:
2012 if (ARMING_FLAG(ARMED) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
2013 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
2014 return true;
2017 buff[0] = SYM_HEADING;
2019 if ((!ARMING_FLAG(ARMED)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && isAdjustingPosition())) {
2020 buff[1] = buff[2] = buff[3] = '-';
2021 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
2022 int16_t herr = lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
2023 if (ABS(herr) > 99)
2024 strcpy(buff + 1, ">99");
2025 else
2026 tfp_sprintf(buff + 1, "%3d", herr);
2029 buff[4] = SYM_DEGREES;
2030 buff[5] = '\0';
2031 break;
2034 case OSD_COURSE_HOLD_ADJUSTMENT:
2036 int16_t heading_adjust = lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
2038 if (ARMING_FLAG(ARMED) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust != 0)))) {
2039 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
2040 return true;
2043 buff[0] = SYM_HEADING;
2045 if (!ARMING_FLAG(ARMED)) {
2046 buff[1] = buff[2] = buff[3] = buff[4] = '-';
2047 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
2048 tfp_sprintf(buff + 1, "%4d", heading_adjust);
2051 buff[5] = SYM_DEGREES;
2052 buff[6] = '\0';
2053 break;
2056 case OSD_CROSS_TRACK_ERROR:
2058 if (isWaypointNavTrackingActive()) {
2059 buff[0] = SYM_CROSS_TRACK_ERROR;
2060 osdFormatDistanceSymbol(buff + 1, navigationGetCrossTrackError(), 0);
2061 } else {
2062 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
2063 return true;
2065 break;
2068 case OSD_GPS_HDOP:
2070 buff[0] = SYM_HDP_L;
2071 buff[1] = SYM_HDP_R;
2072 int32_t centiHDOP = 100 * gpsSol.hdop / HDOP_SCALE;
2073 uint8_t digits = 2U;
2074 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
2075 if (isDJICompatibleVideoSystem(osdConfig())) {
2076 digits = 3U;
2078 #endif
2079 osdFormatCentiNumber(&buff[2], centiHDOP, 0, 1, 0, digits, false);
2080 break;
2082 #ifdef USE_ADSB
2083 case OSD_ADSB_WARNING:
2085 static uint8_t adsblen = 1;
2086 uint8_t arrowPositionX = 0;
2088 for (int i = 0; i <= adsblen; i++) {
2089 buff[i] = SYM_BLANK;
2092 buff[adsblen]='\0';
2093 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff); // clear any previous chars because variable element size
2094 adsblen=1;
2095 adsbVehicle_t *vehicle = findVehicleClosest();
2097 if(vehicle != NULL){
2098 recalculateVehicle(vehicle);
2101 if (
2102 vehicle != NULL &&
2103 (vehicle->calculatedVehicleValues.dist > 0) &&
2104 vehicle->calculatedVehicleValues.dist < METERS_TO_CENTIMETERS(osdConfig()->adsb_distance_warning) &&
2105 (osdConfig()->adsb_ignore_plane_above_me_limit == 0 || METERS_TO_CENTIMETERS(osdConfig()->adsb_ignore_plane_above_me_limit) > vehicle->calculatedVehicleValues.verticalDistance)
2107 buff[0] = SYM_ADSB;
2108 osdFormatDistanceStr(&buff[1], (int32_t)vehicle->calculatedVehicleValues.dist);
2109 adsblen = strlen(buff);
2111 buff[adsblen-1] = SYM_BLANK;
2113 arrowPositionX = adsblen-1;
2114 osdFormatDistanceStr(&buff[adsblen], vehicle->calculatedVehicleValues.verticalDistance);
2115 adsblen = strlen(buff)-1;
2117 if (vehicle->calculatedVehicleValues.dist < METERS_TO_CENTIMETERS(osdConfig()->adsb_distance_alert)) {
2118 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2122 buff[adsblen]='\0';
2123 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
2125 if (arrowPositionX > 0){
2126 int16_t panHomeDirOffset = 0;
2127 if (osdConfig()->pan_servo_pwm2centideg != 0){
2128 panHomeDirOffset = osdGetPanServoOffset();
2130 int16_t flightDirection = STATE(AIRPLANE) ? CENTIDEGREES_TO_DEGREES(posControl.actualState.cog) : DECIDEGREES_TO_DEGREES(osdGetHeading());
2131 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX + arrowPositionX, elemPosY), CENTIDEGREES_TO_DEGREES(vehicle->calculatedVehicleValues.dir) - flightDirection + panHomeDirOffset);
2134 return true;
2136 case OSD_ADSB_INFO:
2138 buff[0] = SYM_ADSB;
2139 if(getAdsbStatus()->vehiclesMessagesTotal > 0){
2140 tfp_sprintf(buff + 1, "%2d", getActiveVehiclesCount());
2141 }else{
2142 buff[1] = '-';
2145 break;
2148 #endif
2149 case OSD_MAP_NORTH:
2151 static uint16_t drawn = 0;
2152 static uint32_t scale = 0;
2153 osdDrawHomeMap(0, 'N', &drawn, &scale);
2154 return true;
2156 case OSD_MAP_TAKEOFF:
2158 static uint16_t drawn = 0;
2159 static uint32_t scale = 0;
2160 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn, &scale);
2161 return true;
2163 case OSD_RADAR:
2165 static uint16_t drawn = 0;
2166 static uint32_t scale = 0;
2167 osdDrawRadar(&drawn, &scale);
2168 return true;
2170 #endif // GPS
2172 case OSD_ALTITUDE:
2174 int32_t alt = osdGetAltitude();
2175 osdFormatAltitudeSymbol(buff, alt);
2177 uint16_t alt_alarm = osdConfig()->alt_alarm;
2178 uint16_t neg_alt_alarm = osdConfig()->neg_alt_alarm;
2179 if ((alt_alarm > 0 && CENTIMETERS_TO_METERS(alt) > alt_alarm) ||
2180 (neg_alt_alarm > 0 && alt < 0 && -CENTIMETERS_TO_METERS(alt) > neg_alt_alarm)) {
2182 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2184 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
2186 if (STATE(MULTIROTOR) && posControl.flags.isAdjustingAltitude) {
2187 /* Indicate MR altitude adjustment active with constant symbol at first blank position.
2188 * Alternate symbol on/off with 600ms cycle if first position not blank (to maintain visibility of -ve sign) */
2189 int8_t blankPos;
2190 for (blankPos = 2; blankPos >= 0; blankPos--) {
2191 if (buff[blankPos] == SYM_BLANK) {
2192 break;
2195 if (blankPos >= 0 || OSD_ALTERNATING_CHOICES(600, 2) == 0) {
2196 blankPos = blankPos < 0 ? 0 : blankPos;
2197 displayWriteChar(osdDisplayPort, elemPosX + blankPos, elemPosY, SYM_TERRAIN_FOLLOWING);
2200 return true;
2203 case OSD_ALTITUDE_MSL:
2205 int32_t alt = osdGetAltitudeMsl();
2206 osdFormatAltitudeSymbol(buff, alt);
2207 break;
2210 #ifdef USE_RANGEFINDER
2211 case OSD_RANGEFINDER:
2213 int32_t range = rangefinderGetLatestRawAltitude();
2214 if (range < 0) {
2215 buff[0] = '-';
2216 buff[1] = '-';
2217 buff[2] = '-';
2218 } else {
2219 osdFormatDistanceSymbol(buff, range, 1);
2222 break;
2223 #endif
2225 case OSD_ONTIME:
2227 osdFormatOnTime(buff);
2228 break;
2231 case OSD_FLYTIME:
2233 osdFormatFlyTime(buff, &elemAttr);
2234 break;
2237 case OSD_ONTIME_FLYTIME:
2239 if (ARMING_FLAG(ARMED)) {
2240 osdFormatFlyTime(buff, &elemAttr);
2241 } else {
2242 osdFormatOnTime(buff);
2244 break;
2247 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH:
2249 /*static int32_t updatedTimeSeconds = 0;*/
2250 static int32_t timeSeconds = -1;
2251 #if defined(USE_ADC) && defined(USE_GPS)
2252 static timeUs_t updatedTimestamp = 0;
2253 timeUs_t currentTimeUs = micros();
2254 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2255 #ifdef USE_WIND_ESTIMATOR
2256 timeSeconds = calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation);
2257 #else
2258 timeSeconds = calculateRemainingFlightTimeBeforeRTH(false);
2259 #endif
2260 updatedTimestamp = currentTimeUs;
2262 #endif
2263 if ((!ARMING_FLAG(ARMED)) || (timeSeconds == -1)) {
2264 buff[0] = SYM_FLIGHT_MINS_REMAINING;
2265 strcpy(buff + 1, "--:--");
2266 #if defined(USE_ADC) && defined(USE_GPS)
2267 updatedTimestamp = 0;
2268 #endif
2269 } else if (timeSeconds == -2) {
2270 // Wind is too strong to come back with cruise throttle
2271 buff[0] = SYM_FLIGHT_MINS_REMAINING;
2272 buff[1] = buff[2] = buff[4] = buff[5] = SYM_WIND_HORIZONTAL;
2273 buff[3] = ':';
2274 buff[6] = '\0';
2275 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2276 } else {
2277 osdFormatTime(buff, timeSeconds, SYM_FLIGHT_MINS_REMAINING, SYM_FLIGHT_HOURS_REMAINING);
2278 if (timeSeconds == 0)
2279 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2282 break;
2284 case OSD_REMAINING_DISTANCE_BEFORE_RTH:;
2285 static int32_t distanceMeters = -1;
2286 #if defined(USE_ADC) && defined(USE_GPS)
2287 static timeUs_t updatedTimestamp = 0;
2288 timeUs_t currentTimeUs = micros();
2289 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2290 #ifdef USE_WIND_ESTIMATOR
2291 distanceMeters = calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation);
2292 #else
2293 distanceMeters = calculateRemainingDistanceBeforeRTH(false);
2294 #endif
2295 updatedTimestamp = currentTimeUs;
2297 #endif
2298 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_FLIGHT_DIST_REMAINING);
2300 if ((!ARMING_FLAG(ARMED)) || (distanceMeters == -1)) {
2301 buff[3] = SYM_BLANK;
2302 buff[4] = '\0';
2303 strcpy(buff, "---");
2304 } else if (distanceMeters == -2) {
2305 // Wind is too strong to come back with cruise throttle
2306 buff[0] = buff[1] = buff[2] = SYM_WIND_HORIZONTAL;
2307 switch ((osd_unit_e)osdConfig()->units){
2308 case OSD_UNIT_UK:
2309 FALLTHROUGH;
2310 case OSD_UNIT_IMPERIAL:
2311 buff[3] = SYM_DIST_MI;
2312 break;
2313 case OSD_UNIT_METRIC_MPH:
2314 FALLTHROUGH;
2315 case OSD_UNIT_METRIC:
2316 buff[3] = SYM_DIST_KM;
2317 break;
2318 case OSD_UNIT_GA:
2319 buff[3] = SYM_DIST_NM;
2320 break;
2322 buff[4] = '\0';
2323 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2324 } else {
2325 osdFormatDistanceSymbol(buff, distanceMeters * 100, 0);
2326 if (distanceMeters == 0)
2327 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2329 elemPosX++;
2330 break;
2332 case OSD_FLYMODE:
2334 char *p = "ACRO";
2335 #ifdef USE_FW_AUTOLAND
2336 if (FLIGHT_MODE(NAV_FW_AUTOLAND))
2337 p = "LAND";
2338 else
2339 #endif
2340 if (FLIGHT_MODE(FAILSAFE_MODE))
2341 p = "!FS!";
2342 else if (FLIGHT_MODE(MANUAL_MODE))
2343 p = "MANU";
2344 else if (FLIGHT_MODE(TURTLE_MODE))
2345 p = "TURT";
2346 else if (FLIGHT_MODE(NAV_RTH_MODE))
2347 p = isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
2348 else if (FLIGHT_MODE(NAV_POSHOLD_MODE) && STATE(AIRPLANE))
2349 p = "LOTR";
2350 else if (FLIGHT_MODE(NAV_POSHOLD_MODE))
2351 p = "HOLD";
2352 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && FLIGHT_MODE(NAV_ALTHOLD_MODE))
2353 p = "CRUZ";
2354 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE))
2355 p = "CRSH";
2356 else if (FLIGHT_MODE(NAV_WP_MODE))
2357 p = " WP ";
2358 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && navigationRequiresAngleMode()) {
2359 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
2360 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
2361 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
2362 // (Currently only applies to multirotor).
2363 p = " AH ";
2365 else if (FLIGHT_MODE(ANGLE_MODE))
2366 p = "ANGL";
2367 else if (FLIGHT_MODE(HORIZON_MODE))
2368 p = "HOR ";
2369 else if (FLIGHT_MODE(ANGLEHOLD_MODE))
2370 p = "ANGH";
2372 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
2373 return true;
2376 case OSD_CRAFT_NAME:
2377 osdFormatCraftName(buff);
2378 break;
2380 case OSD_PILOT_NAME:
2381 osdFormatPilotName(buff);
2382 break;
2384 case OSD_PILOT_LOGO:
2385 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_PILOT_LOGO_SML_L);
2386 displayWriteChar(osdDisplayPort, elemPosX+1, elemPosY, SYM_PILOT_LOGO_SML_C);
2387 displayWriteChar(osdDisplayPort, elemPosX+2, elemPosY, SYM_PILOT_LOGO_SML_R);
2388 break;
2390 case OSD_THROTTLE_POS:
2392 osdFormatThrottlePosition(buff, false, &elemAttr);
2393 break;
2396 case OSD_VTX_CHANNEL:
2398 vtxDeviceOsdInfo_t osdInfo;
2399 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2401 tfp_sprintf(buff, "CH:%c%s:", osdInfo.bandLetter, osdInfo.channelName);
2402 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2404 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2405 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2406 displayWriteWithAttr(osdDisplayPort, elemPosX + 6, elemPosY, buff, elemAttr);
2407 return true;
2409 break;
2411 case OSD_VTX_POWER:
2413 vtxDeviceOsdInfo_t osdInfo;
2414 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2416 tfp_sprintf(buff, "%c", SYM_VTX_POWER);
2417 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2419 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2420 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2421 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2422 return true;
2425 #if defined(USE_SERIALRX_CRSF)
2426 case OSD_CRSF_RSSI_DBM:
2428 int16_t rssi = rxLinkStatistics.uplinkRSSI;
2429 buff[0] = (rxLinkStatistics.activeAntenna == 0) ? SYM_RSSI : SYM_2RSS; // Separate symbols for each antenna
2430 if (rssi <= -100) {
2431 tfp_sprintf(buff + 1, "%4d%c", rssi, SYM_DBM);
2432 } else {
2433 tfp_sprintf(buff + 1, "%3d%c%c", rssi, SYM_DBM, ' ');
2435 if (!failsafeIsReceivingRxData()){
2436 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2437 } else if (osdConfig()->rssi_dbm_alarm && rssi < osdConfig()->rssi_dbm_alarm) {
2438 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2440 break;
2442 case OSD_CRSF_LQ:
2444 buff[0] = SYM_LQ;
2445 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
2446 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
2447 switch (osdConfig()->crsf_lq_format) {
2448 case OSD_CRSF_LQ_TYPE1:
2449 if (!failsafeIsReceivingRxData()) {
2450 tfp_sprintf(buff+1, "%3d", 0);
2451 } else {
2452 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.uplinkLQ);
2454 break;
2455 case OSD_CRSF_LQ_TYPE2:
2456 if (!failsafeIsReceivingRxData()) {
2457 tfp_sprintf(buff+1, "%s:%3d", " ", 0);
2458 } else {
2459 tfp_sprintf(buff+1, "%d:%3d", rxLinkStatistics.rfMode, rxLinkStatistics.uplinkLQ);
2461 break;
2462 case OSD_CRSF_LQ_TYPE3:
2463 if (!failsafeIsReceivingRxData()) {
2464 tfp_sprintf(buff+1, "%3d", 0);
2465 } else {
2466 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.rfMode >= 2 ? scaledLQ : rxLinkStatistics.uplinkLQ);
2468 break;
2470 if (!failsafeIsReceivingRxData()) {
2471 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2472 } else if (rxLinkStatistics.uplinkLQ < osdConfig()->link_quality_alarm) {
2473 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2475 break;
2478 case OSD_CRSF_SNR_DB:
2480 static pt1Filter_t snrFilterState;
2481 static timeMs_t snrUpdated = 0;
2482 int8_t snrFiltered = pt1FilterApply4(&snrFilterState, rxLinkStatistics.uplinkSNR, 0.5f, MS2S(millis() - snrUpdated));
2483 snrUpdated = millis();
2485 const char* showsnr = "-20";
2486 const char* hidesnr = " ";
2487 if (snrFiltered > osdConfig()->snr_alarm) {
2488 if (cmsInMenu) {
2489 buff[0] = SYM_SNR;
2490 tfp_sprintf(buff + 1, "%s%c", showsnr, SYM_DB);
2491 } else {
2492 buff[0] = SYM_BLANK;
2493 tfp_sprintf(buff + 1, "%s%c", hidesnr, SYM_BLANK);
2495 } else if (snrFiltered <= osdConfig()->snr_alarm) {
2496 buff[0] = SYM_SNR;
2497 if (snrFiltered <= -10) {
2498 tfp_sprintf(buff + 1, "%3d%c", snrFiltered, SYM_DB);
2499 } else {
2500 tfp_sprintf(buff + 1, "%2d%c%c", snrFiltered, SYM_DB, ' ');
2503 break;
2506 case OSD_CRSF_TX_POWER:
2508 if (!failsafeIsReceivingRxData())
2509 tfp_sprintf(buff, "%s%c", " ", SYM_BLANK);
2510 else
2511 tfp_sprintf(buff, "%4d%c", rxLinkStatistics.uplinkTXPower, SYM_MW);
2512 break;
2514 #endif
2516 case OSD_FORMATION_FLIGHT:
2518 static uint8_t currentPeerIndex = 0;
2519 static timeMs_t lastPeerSwitch;
2521 if ((STATE(GPS_FIX) && isImuHeadingValid())) {
2522 if ((radar_pois[currentPeerIndex].gps.lat == 0 || radar_pois[currentPeerIndex].gps.lon == 0 || radar_pois[currentPeerIndex].state >= 2) || (millis() > (osdConfig()->radar_peers_display_time * 1000) + lastPeerSwitch)) {
2523 lastPeerSwitch = millis();
2525 for(uint8_t i = 1; i < RADAR_MAX_POIS - 1; i++) {
2526 uint8_t nextPeerIndex = (currentPeerIndex + i) % (RADAR_MAX_POIS - 1);
2527 if (radar_pois[nextPeerIndex].gps.lat != 0 && radar_pois[nextPeerIndex].gps.lon != 0 && radar_pois[nextPeerIndex].state < 2) {
2528 currentPeerIndex = nextPeerIndex;
2529 break;
2534 radar_pois_t *currentPeer = &(radar_pois[currentPeerIndex]);
2535 if (currentPeer->gps.lat != 0 && currentPeer->gps.lon != 0 && currentPeer->state < 2) {
2536 fpVector3_t poi;
2537 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &currentPeer->gps, GEO_ALT_RELATIVE);
2539 currentPeer->distance = calculateDistanceToDestination(&poi) / 100; // In m
2540 currentPeer->altitude = (int16_t )((currentPeer->gps.alt - osdGetAltitudeMsl()) / 100);
2541 currentPeer->direction = (int16_t )(calculateBearingToDestination(&poi) / 100); // In °
2543 int16_t panServoDirOffset = 0;
2544 if (osdConfig()->pan_servo_pwm2centideg != 0){
2545 panServoDirOffset = osdGetPanServoOffset();
2548 //line 1
2549 //[peer heading][peer ID][LQ][direction to peer]
2551 //[peer heading]
2552 int relativePeerHeading = osdGetHeadingAngle(currentPeer->heading - (int)DECIDEGREES_TO_DEGREES(osdGetHeading()));
2553 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DECORATION + ((relativePeerHeading + 22) / 45) % 8);
2555 //[peer ID]
2556 displayWriteChar(osdDisplayPort, elemPosX + 1, elemPosY, 65 + currentPeerIndex);
2558 //[LQ]
2559 displayWriteChar(osdDisplayPort, elemPosX + 2, elemPosY, SYM_HUD_SIGNAL_0 + currentPeer->lq);
2561 //[direction to peer]
2562 int directionToPeerError = wrap_180(osdGetHeadingAngle(currentPeer->direction) + panServoDirOffset - (int)DECIDEGREES_TO_DEGREES(osdGetHeading()));
2563 uint16_t iconIndexOffset = constrain(((directionToPeerError + 180) / 30), 0, 12);
2564 if (iconIndexOffset == 12) {
2565 iconIndexOffset = 0; // Directly behind
2567 displayWriteChar(osdDisplayPort, elemPosX + 3, elemPosY, SYM_HUD_CARDINAL + iconIndexOffset);
2570 //line 2
2571 switch ((osd_unit_e)osdConfig()->units) {
2572 case OSD_UNIT_UK:
2573 FALLTHROUGH;
2574 case OSD_UNIT_IMPERIAL:
2575 osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(currentPeer->distance * 100), FEET_PER_MILE, 0, 4, 4, false);
2576 break;
2577 case OSD_UNIT_GA:
2578 osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(currentPeer->distance * 100), (uint32_t)FEET_PER_NAUTICALMILE, 0, 4, 4, false);
2579 break;
2580 default:
2581 FALLTHROUGH;
2582 case OSD_UNIT_METRIC_MPH:
2583 FALLTHROUGH;
2584 case OSD_UNIT_METRIC:
2585 osdFormatCentiNumber(buff, currentPeer->distance * 100, METERS_PER_KILOMETER, 0, 4, 4, false);
2586 break;
2588 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, buff);
2591 //line 3
2592 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 2, (currentPeer->altitude >= 0) ? SYM_AH_DECORATION_UP : SYM_AH_DECORATION_DOWN);
2594 int altc = currentPeer->altitude;
2595 switch ((osd_unit_e)osdConfig()->units) {
2596 case OSD_UNIT_UK:
2597 FALLTHROUGH;
2598 case OSD_UNIT_GA:
2599 FALLTHROUGH;
2600 case OSD_UNIT_IMPERIAL:
2601 // Convert to feet
2602 altc = CENTIMETERS_TO_FEET(altc * 100);
2603 break;
2604 default:
2605 FALLTHROUGH;
2606 case OSD_UNIT_METRIC_MPH:
2607 FALLTHROUGH;
2608 case OSD_UNIT_METRIC:
2609 // Already in metres
2610 break;
2613 altc = ABS(constrain(altc, -999, 999));
2614 tfp_sprintf(buff, "%3d", altc);
2615 displayWrite(osdDisplayPort, elemPosX + 1, elemPosY + 2, buff);
2617 return true;
2621 //clear screen
2622 for(uint8_t i = 0; i < 4; i++){
2623 displayWriteChar(osdDisplayPort, elemPosX + i, elemPosY, SYM_BLANK);
2624 displayWriteChar(osdDisplayPort, elemPosX + i, elemPosY + 1, SYM_BLANK);
2625 displayWriteChar(osdDisplayPort, elemPosX + i, elemPosY + 2, SYM_BLANK);
2628 return true;
2631 case OSD_CROSSHAIRS: // Hud is a sub-element of the crosshair
2633 osdCrosshairPosition(&elemPosX, &elemPosY);
2634 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX, elemPosY);
2636 if (osdConfig()->hud_homing && (STATE(GPS_FIX)
2637 #ifdef USE_GPS_FIX_ESTIMATION
2638 || STATE(GPS_ESTIMATED_FIX)
2639 #endif
2640 ) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
2641 osdHudDrawHoming(elemPosX, elemPosY);
2644 if ((STATE(GPS_FIX)
2645 #ifdef USE_GPS_FIX_ESTIMATION
2646 || STATE(GPS_ESTIMATED_FIX)
2647 #endif
2648 ) && isImuHeadingValid()) {
2650 if (osdConfig()->hud_homepoint || osdConfig()->hud_radar_disp > 0 || osdConfig()->hud_wp_disp > 0) {
2651 osdHudClear();
2654 // -------- POI : Home point
2656 if (osdConfig()->hud_homepoint) { // Display the home point (H)
2657 osdHudDrawPoi(GPS_distanceToHome, GPS_directionToHome, -osdGetAltitude() / 100, 0, SYM_HOME, 0 , 0);
2660 // -------- POI : Nearby aircrafts from ESP32 radar
2662 if (osdConfig()->hud_radar_disp > 0) { // Display the POI from the radar
2663 for (uint8_t i = 0; i < osdConfig()->hud_radar_disp; i++) {
2664 if (radar_pois[i].gps.lat != 0 && radar_pois[i].gps.lon != 0 && radar_pois[i].state < 2) { // state 2 means POI has been lost and must be skipped
2665 fpVector3_t poi;
2666 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &radar_pois[i].gps, GEO_ALT_RELATIVE);
2667 radar_pois[i].distance = calculateDistanceToDestination(&poi) / 100; // In meters
2669 if (radar_pois[i].distance >= osdConfig()->hud_radar_range_min && radar_pois[i].distance <= osdConfig()->hud_radar_range_max) {
2670 radar_pois[i].direction = calculateBearingToDestination(&poi) / 100; // In °
2671 radar_pois[i].altitude = (radar_pois[i].gps.alt - osdGetAltitudeMsl()) / 100;
2672 osdHudDrawPoi(radar_pois[i].distance, osdGetHeadingAngle(radar_pois[i].direction), radar_pois[i].altitude, 1, 65 + i, radar_pois[i].heading, radar_pois[i].lq);
2678 // -------- POI : Next waypoints from navigation
2680 if (osdConfig()->hud_wp_disp > 0 && posControl.waypointListValid && posControl.waypointCount > 0) { // Display the next waypoints
2681 gpsLocation_t wp2;
2682 int j;
2684 for (int i = osdConfig()->hud_wp_disp - 1; i >= 0 ; i--) { // Display in reverse order so the next WP is always written on top
2685 j = posControl.activeWaypointIndex + i;
2686 if (j > posControl.startWpIndex + posControl.waypointCount - 1) { // limit to max WP index for mission
2687 break;
2689 if (posControl.waypointList[j].lat != 0 && posControl.waypointList[j].lon != 0) {
2690 wp2.lat = posControl.waypointList[j].lat;
2691 wp2.lon = posControl.waypointList[j].lon;
2692 wp2.alt = posControl.waypointList[j].alt;
2693 fpVector3_t poi;
2694 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &wp2, waypointMissionAltConvMode(posControl.waypointList[j].p3));
2695 int32_t altConvModeAltitude = waypointMissionAltConvMode(posControl.waypointList[j].p3) == GEO_ALT_ABSOLUTE ? osdGetAltitudeMsl() : osdGetAltitude();
2696 j = getGeoWaypointNumber(j);
2697 while (j > 9) j -= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2698 osdHudDrawPoi(calculateDistanceToDestination(&poi) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi) / 100), (posControl.waypointList[j].alt - altConvModeAltitude)/ 100, 2, SYM_WAYPOINT, 48 + j, i);
2704 return true;
2705 break;
2707 case OSD_ATTITUDE_ROLL:
2708 buff[0] = SYM_ROLL_LEVEL;
2709 if (ABS(attitude.values.roll) >= 1)
2710 buff[0] += (attitude.values.roll < 0 ? -1 : 1);
2711 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.roll)), 0, 1, 0, 3, false);
2712 break;
2714 case OSD_ATTITUDE_PITCH:
2715 if (ABS(attitude.values.pitch) < 1)
2716 buff[0] = 'P';
2717 else if (attitude.values.pitch > 0)
2718 buff[0] = SYM_PITCH_DOWN;
2719 else if (attitude.values.pitch < 0)
2720 buff[0] = SYM_PITCH_UP;
2721 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.pitch)), 0, 1, 0, 3, false);
2722 break;
2724 case OSD_ARTIFICIAL_HORIZON:
2726 float rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2727 float pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2729 pitchAngle -= osdConfig()->ahi_camera_uptilt_comp ? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt) : 0;
2730 pitchAngle += DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2731 if (osdConfig()->ahi_reverse_roll) {
2732 rollAngle = -rollAngle;
2734 osdDrawArtificialHorizon(osdDisplayPort, osdGetDisplayPortCanvas(),
2735 OSD_DRAW_POINT_GRID(elemPosX, elemPosY), rollAngle, pitchAngle);
2736 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
2737 osdDrawSingleElement(OSD_CROSSHAIRS);
2739 return true;
2742 case OSD_HORIZON_SIDEBARS:
2744 osdDrawSidebars(osdDisplayPort, osdGetDisplayPortCanvas());
2745 return true;
2748 #if defined(USE_BARO) || defined(USE_GPS)
2749 case OSD_VARIO:
2751 float zvel = getEstimatedActualVelocity(Z);
2752 osdDrawVario(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), zvel);
2753 return true;
2756 case OSD_VARIO_NUM:
2758 int16_t value = getEstimatedActualVelocity(Z);
2759 char sym;
2760 switch ((osd_unit_e)osdConfig()->units) {
2761 case OSD_UNIT_UK:
2762 FALLTHROUGH;
2763 case OSD_UNIT_IMPERIAL:
2764 // Convert to centifeet/s
2765 value = CENTIMETERS_TO_CENTIFEET(value);
2766 sym = SYM_FTS;
2767 break;
2768 case OSD_UNIT_GA:
2769 // Convert to centi-100feet/min
2770 value = CENTIMETERS_TO_FEET(value * 60);
2771 sym = SYM_100FTM;
2772 break;
2773 default:
2774 case OSD_UNIT_METRIC_MPH:
2775 FALLTHROUGH;
2776 case OSD_UNIT_METRIC:
2777 // Already in cm/s
2778 sym = SYM_MS;
2779 break;
2782 osdFormatCentiNumber(buff, value, 0, 1, 0, 3, false);
2783 buff[3] = sym;
2784 buff[4] = '\0';
2785 break;
2787 case OSD_CLIMB_EFFICIENCY:
2789 // amperage is in centi amps (10mA), vertical speed is in cms/s. We want
2790 // Ah/dist only to show when vertical speed > 1m/s.
2791 static pt1Filter_t veFilterState;
2792 static timeUs_t vEfficiencyUpdated = 0;
2793 int32_t value = 0;
2794 timeUs_t currentTimeUs = micros();
2795 timeDelta_t vEfficiencyTimeDelta = cmpTimeUs(currentTimeUs, vEfficiencyUpdated);
2796 if (getEstimatedActualVelocity(Z) > 0) {
2797 if (vEfficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2798 // Centiamps (kept for osdFormatCentiNumber) / m/s - Will appear as A / m/s in OSD
2799 value = pt1FilterApply4(&veFilterState, (float)getAmperage() / (getEstimatedActualVelocity(Z) / 100.0f), 1, US2S(vEfficiencyTimeDelta));
2801 vEfficiencyUpdated = currentTimeUs;
2802 } else {
2803 value = veFilterState.state;
2806 bool efficiencyValid = (value > 0) && (getEstimatedActualVelocity(Z) > 100);
2807 switch (osdConfig()->units) {
2808 case OSD_UNIT_UK:
2809 FALLTHROUGH;
2810 case OSD_UNIT_GA:
2811 FALLTHROUGH;
2812 case OSD_UNIT_IMPERIAL:
2813 // mAh/foot
2814 if (efficiencyValid) {
2815 osdFormatCentiNumber(buff, (value * METERS_PER_FOOT), 1, 2, 2, 3, false);
2816 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_AH_V_FT_0, SYM_AH_V_FT_1);
2817 } else {
2818 buff[0] = buff[1] = buff[2] = '-';
2819 buff[3] = SYM_AH_V_FT_0;
2820 buff[4] = SYM_AH_V_FT_1;
2821 buff[5] = '\0';
2823 break;
2824 case OSD_UNIT_METRIC_MPH:
2825 FALLTHROUGH;
2826 case OSD_UNIT_METRIC:
2827 // mAh/metre
2828 if (efficiencyValid) {
2829 osdFormatCentiNumber(buff, value, 1, 2, 2, 3, false);
2830 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_AH_V_M_0, SYM_AH_V_M_1);
2831 } else {
2832 buff[0] = buff[1] = buff[2] = '-';
2833 buff[3] = SYM_AH_V_M_0;
2834 buff[4] = SYM_AH_V_M_1;
2835 buff[5] = '\0';
2837 break;
2839 break;
2841 case OSD_GLIDE_TIME_REMAINING:
2843 uint16_t glideTime = osdGetRemainingGlideTime();
2844 buff[0] = SYM_GLIDE_MINS;
2845 if (glideTime > 0) {
2846 // Maximum value we can show in minutes is 99 minutes and 59 seconds. It is extremely unlikely that glide
2847 // time will be longer than 99 minutes. If it is, it will show 99:^^
2848 if (glideTime > (99 * 60) + 59) {
2849 tfp_sprintf(buff + 1, "%02d:", (int)(glideTime / 60));
2850 buff[4] = SYM_DECORATION;
2851 buff[5] = SYM_DECORATION;
2852 } else {
2853 tfp_sprintf(buff + 1, "%02d:%02d", (int)(glideTime / 60), (int)(glideTime % 60));
2855 } else {
2856 tfp_sprintf(buff + 1, "%s", "--:--");
2858 buff[6] = '\0';
2859 break;
2861 case OSD_GLIDE_RANGE:
2863 uint16_t glideSeconds = osdGetRemainingGlideTime();
2864 buff[0] = SYM_GLIDE_DIST;
2865 if (glideSeconds > 0) {
2866 uint32_t glideRangeCM = glideSeconds * gpsSol.groundSpeed;
2867 osdFormatDistanceSymbol(buff + 1, glideRangeCM, 0);
2868 } else {
2869 tfp_sprintf(buff + 1, "%s%c", "---", SYM_BLANK);
2870 buff[5] = '\0';
2872 break;
2874 #endif
2876 case OSD_SWITCH_INDICATOR_0:
2877 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channel - 1), buff);
2878 break;
2880 case OSD_SWITCH_INDICATOR_1:
2881 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channel - 1), buff);
2882 break;
2884 case OSD_SWITCH_INDICATOR_2:
2885 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channel - 1), buff);
2886 break;
2888 case OSD_SWITCH_INDICATOR_3:
2889 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channel - 1), buff);
2890 break;
2892 case OSD_PAN_SERVO_CENTRED:
2894 int16_t panOffset = osdGetPanServoOffset();
2895 const timeMs_t panServoTimeNow = millis();
2896 static timeMs_t panServoTimeOffCentre = 0;
2898 if (panOffset < 0) {
2899 if (osdConfig()->pan_servo_offcentre_warning != 0 && panOffset >= -osdConfig()->pan_servo_offcentre_warning) {
2900 if (panServoTimeOffCentre == 0) {
2901 panServoTimeOffCentre = panServoTimeNow;
2902 } else if (panServoTimeNow >= (panServoTimeOffCentre + 10000 )) {
2903 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2905 } else {
2906 panServoTimeOffCentre = 0;
2909 if (osdConfig()->pan_servo_indicator_show_degrees) {
2910 tfp_sprintf(buff, "%3d%c", -panOffset, SYM_DEGREES);
2911 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2913 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_OFFSET_R, elemAttr);
2914 } else if (panOffset > 0) {
2915 if (osdConfig()->pan_servo_offcentre_warning != 0 && panOffset <= osdConfig()->pan_servo_offcentre_warning) {
2916 if (panServoTimeOffCentre == 0) {
2917 panServoTimeOffCentre = panServoTimeNow;
2918 } else if (panServoTimeNow >= (panServoTimeOffCentre + 10000 )) {
2919 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2921 } else {
2922 panServoTimeOffCentre = 0;
2925 if (osdConfig()->pan_servo_indicator_show_degrees) {
2926 tfp_sprintf(buff, "%3d%c", panOffset, SYM_DEGREES);
2927 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2929 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_OFFSET_L, elemAttr);
2930 } else {
2931 panServoTimeOffCentre = 0;
2933 if (osdConfig()->pan_servo_indicator_show_degrees) {
2934 tfp_sprintf(buff, "%3d%c", panOffset, SYM_DEGREES);
2935 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2937 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_CENTRED);
2940 return true;
2942 break;
2944 case OSD_ACTIVE_PROFILE:
2945 tfp_sprintf(buff, "%c%u", SYM_PROFILE, (getConfigProfile() + 1));
2946 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2947 break;
2949 case OSD_ROLL_PIDS:
2950 osdDisplayFlightPIDValues(elemPosX, elemPosY, "ROL", PID_ROLL, ADJUSTMENT_ROLL_P, ADJUSTMENT_ROLL_I, ADJUSTMENT_ROLL_D, ADJUSTMENT_ROLL_FF);
2951 return true;
2953 case OSD_PITCH_PIDS:
2954 osdDisplayFlightPIDValues(elemPosX, elemPosY, "PIT", PID_PITCH, ADJUSTMENT_PITCH_P, ADJUSTMENT_PITCH_I, ADJUSTMENT_PITCH_D, ADJUSTMENT_PITCH_FF);
2955 return true;
2957 case OSD_YAW_PIDS:
2958 osdDisplayFlightPIDValues(elemPosX, elemPosY, "YAW", PID_YAW, ADJUSTMENT_YAW_P, ADJUSTMENT_YAW_I, ADJUSTMENT_YAW_D, ADJUSTMENT_YAW_FF);
2959 return true;
2961 case OSD_LEVEL_PIDS:
2962 osdDisplayNavPIDValues(elemPosX, elemPosY, "LEV", PID_LEVEL, ADJUSTMENT_LEVEL_P, ADJUSTMENT_LEVEL_I, ADJUSTMENT_LEVEL_D);
2963 return true;
2965 case OSD_POS_XY_PIDS:
2966 osdDisplayNavPIDValues(elemPosX, elemPosY, "PXY", PID_POS_XY, ADJUSTMENT_POS_XY_P, ADJUSTMENT_POS_XY_I, ADJUSTMENT_POS_XY_D);
2967 return true;
2969 case OSD_POS_Z_PIDS:
2970 osdDisplayNavPIDValues(elemPosX, elemPosY, "PZ", PID_POS_Z, ADJUSTMENT_POS_Z_P, ADJUSTMENT_POS_Z_I, ADJUSTMENT_POS_Z_D);
2971 return true;
2973 case OSD_VEL_XY_PIDS:
2974 osdDisplayNavPIDValues(elemPosX, elemPosY, "VXY", PID_VEL_XY, ADJUSTMENT_VEL_XY_P, ADJUSTMENT_VEL_XY_I, ADJUSTMENT_VEL_XY_D);
2975 return true;
2977 case OSD_VEL_Z_PIDS:
2978 osdDisplayNavPIDValues(elemPosX, elemPosY, "VZ", PID_VEL_Z, ADJUSTMENT_VEL_Z_P, ADJUSTMENT_VEL_Z_I, ADJUSTMENT_VEL_Z_D);
2979 return true;
2981 case OSD_HEADING_P:
2982 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "HP", 0, pidBank()->pid[PID_HEADING].P, 3, 0, ADJUSTMENT_HEADING_P);
2983 return true;
2985 case OSD_BOARD_ALIGN_ROLL:
2986 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT);
2987 return true;
2989 case OSD_BOARD_ALIGN_PITCH:
2990 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT);
2991 return true;
2993 case OSD_RC_EXPO:
2994 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "EXP", 0, currentControlRateProfile->stabilized.rcExpo8, 3, 0, ADJUSTMENT_RC_EXPO);
2995 return true;
2997 case OSD_RC_YAW_EXPO:
2998 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "YEX", 0, currentControlRateProfile->stabilized.rcYawExpo8, 3, 0, ADJUSTMENT_RC_YAW_EXPO);
2999 return true;
3001 case OSD_THROTTLE_EXPO:
3002 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TEX", 0, currentControlRateProfile->throttle.rcExpo8, 3, 0, ADJUSTMENT_THROTTLE_EXPO);
3003 return true;
3005 case OSD_PITCH_RATE:
3006 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SPR");
3008 elemAttr = TEXT_ATTRIBUTES_NONE;
3009 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_PITCH]);
3010 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
3011 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3012 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
3013 return true;
3015 case OSD_ROLL_RATE:
3016 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SRR");
3018 elemAttr = TEXT_ATTRIBUTES_NONE;
3019 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_ROLL]);
3020 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
3021 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3022 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
3023 return true;
3025 case OSD_YAW_RATE:
3026 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "SYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
3027 return true;
3029 case OSD_MANUAL_RC_EXPO:
3030 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MEX", 0, currentControlRateProfile->manual.rcExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO);
3031 return true;
3033 case OSD_MANUAL_RC_YAW_EXPO:
3034 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYX", 0, currentControlRateProfile->manual.rcYawExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO);
3035 return true;
3037 case OSD_MANUAL_PITCH_RATE:
3038 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MPR");
3040 elemAttr = TEXT_ATTRIBUTES_NONE;
3041 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_PITCH]);
3042 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
3043 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3044 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
3045 return true;
3047 case OSD_MANUAL_ROLL_RATE:
3048 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MRR");
3050 elemAttr = TEXT_ATTRIBUTES_NONE;
3051 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_ROLL]);
3052 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
3053 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3054 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
3055 return true;
3057 case OSD_MANUAL_YAW_RATE:
3058 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
3059 return true;
3061 case OSD_NAV_FW_CRUISE_THR:
3062 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CRZ", 0, currentBatteryProfile->nav.fw.cruise_throttle, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR);
3063 return true;
3065 case OSD_NAV_FW_PITCH2THR:
3066 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "P2T", 0, currentBatteryProfile->nav.fw.pitch_to_throttle, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR);
3067 return true;
3069 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE:
3070 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "0TP", 0, (float)navConfig()->fw.minThrottleDownPitchAngle / 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE);
3071 return true;
3073 case OSD_FW_ALT_PID_OUTPUTS:
3075 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
3076 osdFormatPidControllerOutput(buff, "PZO", &nav_pids->fw_alt, 10, true); // display requested pitch degrees
3077 break;
3080 case OSD_FW_POS_PID_OUTPUTS:
3082 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers(); // display requested roll degrees
3083 osdFormatPidControllerOutput(buff, "PXYO", &nav_pids->fw_nav, 1, true);
3084 break;
3087 case OSD_MC_VEL_Z_PID_OUTPUTS:
3089 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
3090 osdFormatPidControllerOutput(buff, "VZO", &nav_pids->vel[Z], 100, false); // display throttle adjustment µs
3091 break;
3094 case OSD_MC_VEL_X_PID_OUTPUTS:
3096 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
3097 osdFormatPidControllerOutput(buff, "VXO", &nav_pids->vel[X], 100, false); // display requested acceleration cm/s^2
3098 break;
3101 case OSD_MC_VEL_Y_PID_OUTPUTS:
3103 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
3104 osdFormatPidControllerOutput(buff, "VYO", &nav_pids->vel[Y], 100, false); // display requested acceleration cm/s^2
3105 break;
3108 case OSD_MC_POS_XYZ_P_OUTPUTS:
3110 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
3111 strcpy(buff, "POSO ");
3112 // display requested velocity cm/s
3113 tfp_sprintf(buff + 5, "%4d", (int)lrintf(nav_pids->pos[X].output_constrained * 100));
3114 buff[9] = ' ';
3115 tfp_sprintf(buff + 10, "%4d", (int)lrintf(nav_pids->pos[Y].output_constrained * 100));
3116 buff[14] = ' ';
3117 tfp_sprintf(buff + 15, "%4d", (int)lrintf(nav_pids->pos[Z].output_constrained * 100));
3118 buff[19] = '\0';
3119 break;
3122 case OSD_POWER:
3124 bool kiloWatt = osdFormatCentiNumber(buff, getPower(), 1000, 2, 2, 3, false);
3125 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3126 buff[4] = '\0';
3128 uint8_t current_alarm = osdConfig()->current_alarm;
3129 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
3130 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3132 break;
3135 case OSD_AIR_SPEED:
3137 #ifdef USE_PITOT
3138 buff[0] = SYM_AIR;
3140 if (pitotIsHealthy())
3142 const float airspeed_estimate = getAirspeedEstimate();
3143 osdFormatVelocityStr(buff + 1, airspeed_estimate, false, false);
3144 if ((osdConfig()->airspeed_alarm_min != 0 && airspeed_estimate < osdConfig()->airspeed_alarm_min) ||
3145 (osdConfig()->airspeed_alarm_max != 0 && airspeed_estimate > osdConfig()->airspeed_alarm_max)) {
3146 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3149 else
3151 strcpy(buff + 1, " X!");
3152 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3154 #else
3155 return false;
3156 #endif
3157 break;
3160 case OSD_AIR_MAX_SPEED:
3162 #ifdef USE_PITOT
3163 buff[0] = SYM_MAX;
3164 buff[1] = SYM_AIR;
3165 osdFormatVelocityStr(buff + 2, stats.max_air_speed, false, false);
3166 #else
3167 return false;
3168 #endif
3169 break;
3172 case OSD_RTC_TIME:
3174 // RTC not configured will show 00:00
3175 dateTime_t dateTime;
3176 rtcGetDateTimeLocal(&dateTime);
3177 buff[0] = SYM_CLOCK;
3178 tfp_sprintf(buff + 1, "%02u:%02u:%02u", dateTime.hours, dateTime.minutes, dateTime.seconds);
3179 break;
3182 case OSD_MESSAGES:
3184 elemAttr = osdGetSystemMessage(buff, OSD_MESSAGE_LENGTH, true);
3185 break;
3188 case OSD_VERSION:
3190 tfp_sprintf(buff, "INAV %s", FC_VERSION_STRING);
3191 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3192 break;
3195 case OSD_MAIN_BATT_CELL_VOLTAGE:
3197 uint8_t base_digits = 3U;
3198 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
3199 if(isDJICompatibleVideoSystem(osdConfig())) {
3200 base_digits = 4U; // Add extra digit to account for decimal point taking an extra character space
3202 #endif
3203 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawAverageCellVoltage(), base_digits, 2);
3204 return true;
3207 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE:
3209 uint8_t base_digits = 3U;
3210 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
3211 if(isDJICompatibleVideoSystem(osdConfig())) {
3212 base_digits = 4U; // Add extra digit to account for decimal point taking an extra character space
3214 #endif
3215 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedAverageCellVoltage(), base_digits, 2);
3216 return true;
3219 case OSD_SCALED_THROTTLE_POS:
3221 osdFormatThrottlePosition(buff, true, &elemAttr);
3222 break;
3225 case OSD_HEADING:
3227 buff[0] = SYM_HEADING;
3228 if (osdIsHeadingValid()) {
3229 int16_t h = DECIDEGREES_TO_DEGREES(osdGetHeading());
3230 if (h < 0) {
3231 h += 360;
3233 tfp_sprintf(&buff[1], "%3d", h);
3234 } else {
3235 buff[1] = buff[2] = buff[3] = '-';
3237 buff[4] = SYM_DEGREES;
3238 buff[5] = '\0';
3239 break;
3242 case OSD_HEADING_GRAPH:
3244 if (osdIsHeadingValid()) {
3245 osdDrawHeadingGraph(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), osdGetHeading());
3246 return true;
3247 } else {
3248 buff[0] = buff[2] = buff[4] = buff[6] = buff[8] = SYM_HEADING_LINE;
3249 buff[1] = buff[3] = buff[5] = buff[7] = SYM_HEADING_DIVIDED_LINE;
3250 buff[OSD_HEADING_GRAPH_WIDTH] = '\0';
3252 break;
3255 case OSD_EFFICIENCY_MAH_PER_KM:
3257 // amperage is in centi amps, speed is in cms/s. We want
3258 // mah/km. Only show when ground speed > 1m/s.
3259 static pt1Filter_t eFilterState;
3260 static timeUs_t efficiencyUpdated = 0;
3261 int32_t value = 0;
3262 bool moreThanAh = false;
3263 timeUs_t currentTimeUs = micros();
3264 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
3265 uint8_t digits = 3U;
3266 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
3267 if (isDJICompatibleVideoSystem(osdConfig())) {
3268 // Increase number of digits so values above 99 don't get scaled by osdFormatCentiNumber
3269 digits = 4U;
3271 #endif
3272 if ((STATE(GPS_FIX)
3273 #ifdef USE_GPS_FIX_ESTIMATION
3274 || STATE(GPS_ESTIMATED_FIX)
3275 #endif
3276 ) && gpsSol.groundSpeed > 0) {
3277 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
3278 value = pt1FilterApply4(&eFilterState, ((float)getAmperage() / gpsSol.groundSpeed) / 0.0036f,
3279 1, US2S(efficiencyTimeDelta));
3281 efficiencyUpdated = currentTimeUs;
3282 } else {
3283 value = eFilterState.state;
3286 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
3287 switch (osdConfig()->units) {
3288 case OSD_UNIT_UK:
3289 FALLTHROUGH;
3290 case OSD_UNIT_IMPERIAL:
3291 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10, 1000, 0, 2, digits, false);
3292 if (!moreThanAh) {
3293 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_MAH_MI_0, SYM_MAH_MI_1);
3294 } else {
3295 tfp_sprintf(buff + strlen(buff), "%c", SYM_AH_MI);
3297 if (!efficiencyValid) {
3298 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3299 buff[digits] = SYM_MAH_MI_0; // This will overwrite the "-" at buff[3] if not in DJICOMPAT mode
3300 buff[digits + 1] = SYM_MAH_MI_1;
3301 buff[digits + 2] = '\0';
3303 break;
3304 case OSD_UNIT_GA:
3305 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10, 1000, 0, 2, digits, false);
3306 if (!moreThanAh) {
3307 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_MAH_NM_0, SYM_MAH_NM_1);
3308 } else {
3309 tfp_sprintf(buff + strlen(buff), "%c", SYM_AH_NM);
3311 if (!efficiencyValid) {
3312 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3313 buff[digits] = SYM_MAH_NM_0;
3314 buff[digits + 1] = SYM_MAH_NM_1;
3315 buff[digits + 2] = '\0';
3317 break;
3318 case OSD_UNIT_METRIC_MPH:
3319 FALLTHROUGH;
3320 case OSD_UNIT_METRIC:
3321 moreThanAh = osdFormatCentiNumber(buff, value * 100, 1000, 0, 2, digits, false);
3322 if (!moreThanAh) {
3323 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_MAH_KM_0, SYM_MAH_KM_1);
3324 } else {
3325 tfp_sprintf(buff + strlen(buff), "%c", SYM_AH_KM);
3327 if (!efficiencyValid) {
3328 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3329 buff[digits] = SYM_MAH_KM_0;
3330 buff[digits + 1] = SYM_MAH_KM_1;
3331 buff[digits + 2] = '\0';
3333 break;
3335 break;
3338 case OSD_EFFICIENCY_WH_PER_KM:
3340 // amperage is in centi amps, speed is in cms/s. We want
3341 // mWh/km. Only show when ground speed > 1m/s.
3342 static pt1Filter_t eFilterState;
3343 static timeUs_t efficiencyUpdated = 0;
3344 int32_t value = 0;
3345 timeUs_t currentTimeUs = micros();
3346 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
3347 if ((STATE(GPS_FIX)
3348 #ifdef USE_GPS_FIX_ESTIMATION
3349 || STATE(GPS_ESTIMATED_FIX)
3350 #endif
3351 ) && gpsSol.groundSpeed > 0) {
3352 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
3353 value = pt1FilterApply4(&eFilterState, ((float)getPower() / gpsSol.groundSpeed) / 0.0036f,
3354 1, US2S(efficiencyTimeDelta));
3356 efficiencyUpdated = currentTimeUs;
3357 } else {
3358 value = eFilterState.state;
3361 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
3362 switch (osdConfig()->units) {
3363 case OSD_UNIT_UK:
3364 FALLTHROUGH;
3365 case OSD_UNIT_IMPERIAL:
3366 osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10000, 0, 2, 0, 3, false);
3367 buff[3] = SYM_WH_MI;
3368 break;
3369 case OSD_UNIT_GA:
3370 osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10000, 0, 2, 0, 3, false);
3371 buff[3] = SYM_WH_NM;
3372 break;
3373 case OSD_UNIT_METRIC_MPH:
3374 FALLTHROUGH;
3375 case OSD_UNIT_METRIC:
3376 osdFormatCentiNumber(buff, value / 10, 0, 2, 0, 3, false);
3377 buff[3] = SYM_WH_KM;
3378 break;
3380 buff[4] = '\0';
3381 if (!efficiencyValid) {
3382 buff[0] = buff[1] = buff[2] = '-';
3384 break;
3387 case OSD_GFORCE:
3389 buff[0] = SYM_GFORCE;
3390 osdFormatCentiNumber(buff + 1, GForce, 0, 2, 0, 3, false);
3391 if (GForce > osdConfig()->gforce_alarm * 100) {
3392 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3394 break;
3397 case OSD_GFORCE_X:
3398 case OSD_GFORCE_Y:
3399 case OSD_GFORCE_Z:
3401 float GForceValue = GForceAxis[item - OSD_GFORCE_X];
3402 buff[0] = SYM_GFORCE_X + item - OSD_GFORCE_X;
3403 osdFormatCentiNumber(buff + 1, GForceValue, 0, 2, 0, 4, false);
3404 if ((GForceValue < osdConfig()->gforce_axis_alarm_min * 100) || (GForceValue > osdConfig()->gforce_axis_alarm_max * 100)) {
3405 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3407 break;
3409 case OSD_DEBUG:
3412 * Longest representable string is -2147483648 does not fit in the screen.
3413 * Only 7 digits for negative and 8 digits for positive values allowed
3415 for (uint8_t bufferIndex = 0; bufferIndex < DEBUG32_VALUE_COUNT; ++elemPosY, bufferIndex += 2) {
3416 tfp_sprintf(
3417 buff,
3418 "[%u]=%8ld [%u]=%8ld",
3419 bufferIndex,
3420 (long)constrain(debug[bufferIndex], -9999999, 99999999),
3421 bufferIndex+1,
3422 (long)constrain(debug[bufferIndex+1], -9999999, 99999999)
3424 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3426 break;
3429 case OSD_IMU_TEMPERATURE:
3431 int16_t temperature;
3432 const bool valid = getIMUTemperature(&temperature);
3433 osdDisplayTemperature(elemPosX, elemPosY, SYM_IMU_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
3434 return true;
3437 case OSD_BARO_TEMPERATURE:
3439 int16_t temperature;
3440 const bool valid = getBaroTemperature(&temperature);
3441 osdDisplayTemperature(elemPosX, elemPosY, SYM_BARO_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
3442 return true;
3445 #ifdef USE_TEMPERATURE_SENSOR
3446 case OSD_TEMP_SENSOR_0_TEMPERATURE:
3447 case OSD_TEMP_SENSOR_1_TEMPERATURE:
3448 case OSD_TEMP_SENSOR_2_TEMPERATURE:
3449 case OSD_TEMP_SENSOR_3_TEMPERATURE:
3450 case OSD_TEMP_SENSOR_4_TEMPERATURE:
3451 case OSD_TEMP_SENSOR_5_TEMPERATURE:
3452 case OSD_TEMP_SENSOR_6_TEMPERATURE:
3453 case OSD_TEMP_SENSOR_7_TEMPERATURE:
3455 osdDisplayTemperatureSensor(elemPosX, elemPosY, item - OSD_TEMP_SENSOR_0_TEMPERATURE);
3456 return true;
3458 #endif /* ifdef USE_TEMPERATURE_SENSOR */
3460 case OSD_WIND_SPEED_HORIZONTAL:
3461 #ifdef USE_WIND_ESTIMATOR
3463 bool valid = isEstimatedWindSpeedValid();
3464 float horizontalWindSpeed;
3465 uint16_t angle;
3466 horizontalWindSpeed = getEstimatedHorizontalWindSpeed(&angle);
3467 int16_t windDirection = osdGetHeadingAngle( CENTIDEGREES_TO_DEGREES((int)angle) - DECIDEGREES_TO_DEGREES(attitude.values.yaw) + 22);
3468 buff[0] = SYM_WIND_HORIZONTAL;
3469 buff[1] = SYM_DECORATION + (windDirection*2 / 90);
3470 osdFormatWindSpeedStr(buff + 2, horizontalWindSpeed, valid);
3471 break;
3473 #else
3474 return false;
3475 #endif
3477 case OSD_WIND_SPEED_VERTICAL:
3478 #ifdef USE_WIND_ESTIMATOR
3480 buff[0] = SYM_WIND_VERTICAL;
3481 buff[1] = SYM_BLANK;
3482 bool valid = isEstimatedWindSpeedValid();
3483 float verticalWindSpeed;
3484 verticalWindSpeed = -getEstimatedWindSpeed(Z); //from NED to NEU
3485 if (verticalWindSpeed < 0) {
3486 buff[1] = SYM_AH_DECORATION_DOWN;
3487 verticalWindSpeed = -verticalWindSpeed;
3488 } else {
3489 buff[1] = SYM_AH_DECORATION_UP;
3491 osdFormatWindSpeedStr(buff + 2, verticalWindSpeed, valid);
3492 break;
3494 #else
3495 return false;
3496 #endif
3498 case OSD_PLUS_CODE:
3500 STATIC_ASSERT(GPS_DEGREES_DIVIDER == OLC_DEG_MULTIPLIER, invalid_olc_deg_multiplier);
3501 int digits = osdConfig()->plus_code_digits;
3502 int digitsRemoved = osdConfig()->plus_code_short * 2;
3503 if ((STATE(GPS_FIX)
3504 #ifdef USE_GPS_FIX_ESTIMATION
3505 || STATE(GPS_ESTIMATED_FIX)
3506 #endif
3507 )) {
3508 olc_encode(gpsSol.llh.lat, gpsSol.llh.lon, digits, buff, sizeof(buff));
3509 } else {
3510 // +codes with > 8 digits have a + at the 9th digit
3511 // and we only support 10 and up.
3512 memset(buff, '-', digits + 1);
3513 buff[8] = '+';
3514 buff[digits + 1] = '\0';
3516 // Optionally trim digits from the left
3517 memmove(buff, buff+digitsRemoved, strlen(buff) + digitsRemoved);
3518 buff[digits + 1 - digitsRemoved] = '\0';
3519 break;
3522 case OSD_AZIMUTH:
3525 buff[0] = SYM_AZIMUTH;
3526 if (osdIsHeadingValid()) {
3527 int16_t h = GPS_directionToHome;
3528 if (h < 0) {
3529 h += 360;
3531 if (h >= 180)
3532 h = h - 180;
3533 else
3534 h = h + 180;
3536 tfp_sprintf(&buff[1], "%3d", h);
3537 } else {
3538 buff[1] = buff[2] = buff[3] = '-';
3540 buff[4] = SYM_DEGREES;
3541 buff[5] = '\0';
3542 break;
3545 case OSD_MAP_SCALE:
3547 float scaleToUnit;
3548 int scaleUnitDivisor;
3549 char symUnscaled;
3550 char symScaled;
3551 int maxDecimals;
3553 switch (osdConfig()->units) {
3554 case OSD_UNIT_UK:
3555 FALLTHROUGH;
3556 case OSD_UNIT_IMPERIAL:
3557 scaleToUnit = 100 / 1609.3440f; // scale to 0.01mi for osdFormatCentiNumber()
3558 scaleUnitDivisor = 0;
3559 symUnscaled = SYM_MI;
3560 symScaled = SYM_MI;
3561 maxDecimals = 2;
3562 break;
3563 case OSD_UNIT_GA:
3564 scaleToUnit = 100 / 1852.0010f; // scale to 0.01mi for osdFormatCentiNumber()
3565 scaleUnitDivisor = 0;
3566 symUnscaled = SYM_NM;
3567 symScaled = SYM_NM;
3568 maxDecimals = 2;
3569 break;
3570 default:
3571 case OSD_UNIT_METRIC_MPH:
3572 FALLTHROUGH;
3573 case OSD_UNIT_METRIC:
3574 scaleToUnit = 100; // scale to cm for osdFormatCentiNumber()
3575 scaleUnitDivisor = 1000; // Convert to km when scale gets bigger than 999m
3576 symUnscaled = SYM_M;
3577 symScaled = SYM_KM;
3578 maxDecimals = 0;
3579 break;
3581 buff[0] = SYM_SCALE;
3582 if (osdMapData.scale > 0) {
3583 bool scaled = osdFormatCentiNumber(&buff[1], osdMapData.scale * scaleToUnit, scaleUnitDivisor, maxDecimals, 2, 3, false);
3584 buff[4] = scaled ? symScaled : symUnscaled;
3585 // Make sure this is cleared if the map stops being drawn
3586 osdMapData.scale = 0;
3587 } else {
3588 memset(&buff[1], '-', 4);
3590 buff[5] = '\0';
3591 break;
3593 case OSD_MAP_REFERENCE:
3595 char referenceSymbol;
3596 if (osdMapData.referenceSymbol) {
3597 referenceSymbol = osdMapData.referenceSymbol;
3598 // Make sure this is cleared if the map stops being drawn
3599 osdMapData.referenceSymbol = 0;
3600 } else {
3601 referenceSymbol = '-';
3603 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DECORATION);
3604 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 1, referenceSymbol);
3605 return true;
3608 case OSD_GVAR_0:
3610 osdFormatGVar(buff, 0);
3611 break;
3613 case OSD_GVAR_1:
3615 osdFormatGVar(buff, 1);
3616 break;
3618 case OSD_GVAR_2:
3620 osdFormatGVar(buff, 2);
3621 break;
3623 case OSD_GVAR_3:
3625 osdFormatGVar(buff, 3);
3626 break;
3629 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3630 case OSD_RC_SOURCE:
3632 const char *source_text = IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
3633 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3634 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, source_text, elemAttr);
3635 return true;
3637 #endif
3639 #if defined(USE_ESC_SENSOR)
3640 case OSD_ESC_RPM:
3642 escSensorData_t * escSensor = escSensorGetData();
3643 if (escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE) {
3644 osdFormatRpm(buff, escSensor->rpm);
3646 else {
3647 osdFormatRpm(buff, 0);
3649 break;
3651 case OSD_ESC_TEMPERATURE:
3653 escSensorData_t * escSensor = escSensorGetData();
3654 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
3655 osdDisplayTemperature(elemPosX, elemPosY, SYM_ESC_TEMP, NULL, escTemperatureValid, (escSensor->temperature)*10, osdConfig()->esc_temp_alarm_min, osdConfig()->esc_temp_alarm_max);
3656 return true;
3658 #endif
3659 case OSD_TPA:
3661 char buff[4];
3662 textAttributes_t attr;
3664 displayWrite(osdDisplayPort, elemPosX, elemPosY, "TPA");
3665 attr = TEXT_ATTRIBUTES_NONE;
3666 tfp_sprintf(buff, "%3d", currentControlRateProfile->throttle.dynPID);
3667 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA)) {
3668 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3670 displayWriteWithAttr(osdDisplayPort, elemPosX + 5, elemPosY, buff, attr);
3672 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, "BP");
3673 attr = TEXT_ATTRIBUTES_NONE;
3674 tfp_sprintf(buff, "%4d", currentControlRateProfile->throttle.pa_breakpoint);
3675 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT)) {
3676 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3678 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY + 1, buff, attr);
3680 return true;
3682 case OSD_TPA_TIME_CONSTANT:
3684 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TPA TC", 0, currentControlRateProfile->throttle.fixedWingTauMs, 4, 0, ADJUSTMENT_FW_TPA_TIME_CONSTANT);
3685 return true;
3687 case OSD_FW_LEVEL_TRIM:
3689 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "LEVEL", 0, getFixedWingLevelTrim(), 3, 1, ADJUSTMENT_FW_LEVEL_TRIM);
3690 return true;
3693 case OSD_NAV_FW_CONTROL_SMOOTHNESS:
3695 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CTL S", 0, navConfig()->fw.control_smoothness, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS);
3696 return true;
3698 #ifdef USE_MULTI_MISSION
3699 case OSD_NAV_WP_MULTI_MISSION_INDEX:
3701 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "WP NO", 0, navConfig()->general.waypoint_multi_mission_index, 1, 0, ADJUSTMENT_NAV_WP_MULTI_MISSION_INDEX);
3702 return true;
3704 #endif
3705 case OSD_MISSION:
3707 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION)) {
3708 char buf[5];
3709 switch (posControl.wpMissionPlannerStatus) {
3710 case WP_PLAN_WAIT:
3711 strcpy(buf, "WAIT");
3712 break;
3713 case WP_PLAN_SAVE:
3714 strcpy(buf, "SAVE");
3715 break;
3716 case WP_PLAN_OK:
3717 strcpy(buf, " OK ");
3718 break;
3719 case WP_PLAN_FULL:
3720 strcpy(buf, "FULL");
3722 tfp_sprintf(buff, "%s>%2uWP", buf, posControl.wpPlannerActiveWPIndex);
3723 } else if (posControl.wpPlannerActiveWPIndex){
3724 tfp_sprintf(buff, "PLAN>%2uWP", posControl.waypointCount); // mission planner mision active
3726 #ifdef USE_MULTI_MISSION
3727 else {
3728 if (ARMING_FLAG(ARMED) && !(IS_RC_MODE_ACTIVE(BOXCHANGEMISSION) && posControl.multiMissionCount > 1)){
3729 // Limit field size when Armed, only show selected mission
3730 tfp_sprintf(buff, "M%u ", posControl.loadedMultiMissionIndex);
3731 } else if (posControl.multiMissionCount) {
3732 if (navConfig()->general.waypoint_multi_mission_index != posControl.loadedMultiMissionIndex) {
3733 tfp_sprintf(buff, "M%u/%u>LOAD", navConfig()->general.waypoint_multi_mission_index, posControl.multiMissionCount);
3734 } else {
3735 if (posControl.waypointListValid && posControl.waypointCount > 0) {
3736 tfp_sprintf(buff, "M%u/%u>%2uWP", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
3737 } else {
3738 tfp_sprintf(buff, "M0/%u> 0WP", posControl.multiMissionCount);
3741 } else { // no multi mission loaded - show active WP count from other source
3742 tfp_sprintf(buff, "WP CNT>%2u", posControl.waypointCount);
3745 #endif
3746 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3747 return true;
3750 #ifdef USE_POWER_LIMITS
3751 case OSD_PLIMIT_REMAINING_BURST_TIME:
3752 osdFormatCentiNumber(buff, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3, false);
3753 buff[3] = 'S';
3754 buff[4] = '\0';
3755 break;
3757 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT:
3758 if (currentBatteryProfile->powerLimits.continuousCurrent) {
3759 osdFormatCentiNumber(buff, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3, false);
3760 buff[3] = SYM_AMP;
3761 buff[4] = '\0';
3763 if (powerLimiterIsLimitingCurrent()) {
3764 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3767 break;
3769 #ifdef USE_ADC
3770 case OSD_PLIMIT_ACTIVE_POWER_LIMIT:
3772 if (currentBatteryProfile->powerLimits.continuousPower) {
3773 bool kiloWatt = osdFormatCentiNumber(buff, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3, false);
3774 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3775 buff[4] = '\0';
3777 if (powerLimiterIsLimitingPower()) {
3778 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3781 break;
3783 #endif // USE_ADC
3784 #endif // USE_POWER_LIMITS
3785 case OSD_MULTI_FUNCTION:
3787 // message shown infrequently so only write when needed
3788 static bool clearMultiFunction = true;
3789 elemAttr = osdGetMultiFunctionMessage(buff);
3790 if (buff[0] == 0) {
3791 if (clearMultiFunction) {
3792 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
3793 clearMultiFunction = false;
3795 return true;
3797 clearMultiFunction = true;
3798 break;
3801 default:
3802 return false;
3805 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
3806 return true;
3809 uint8_t osdIncElementIndex(uint8_t elementIndex)
3811 ++elementIndex;
3813 if (elementIndex == OSD_ARTIFICIAL_HORIZON) { // always drawn last so skip
3814 elementIndex++;
3817 #ifndef USE_TEMPERATURE_SENSOR
3818 if (elementIndex == OSD_TEMP_SENSOR_0_TEMPERATURE) {
3819 elementIndex = OSD_ALTITUDE_MSL;
3821 #endif
3823 if (!(feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER))) {
3824 if (elementIndex == OSD_POWER) {
3825 elementIndex = OSD_GPS_LON;
3827 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3828 elementIndex = OSD_LEVEL_PIDS;
3830 #ifdef USE_POWER_LIMITS
3831 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3832 elementIndex = OSD_GLIDESLOPE;
3834 #endif
3837 #ifndef USE_POWER_LIMITS
3838 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3839 elementIndex = OSD_GLIDESLOPE;
3841 #endif
3843 if (!feature(FEATURE_CURRENT_METER)) {
3844 if (elementIndex == OSD_CURRENT_DRAW) {
3845 elementIndex = OSD_GPS_SPEED;
3847 if (elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3848 elementIndex = OSD_BATTERY_REMAINING_PERCENT;
3850 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3851 elementIndex = OSD_TRIP_DIST;
3853 if (elementIndex == OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH) {
3854 elementIndex = OSD_HOME_HEADING_ERROR;
3856 if (elementIndex == OSD_CLIMB_EFFICIENCY) {
3857 elementIndex = OSD_NAV_WP_MULTI_MISSION_INDEX;
3861 if (!STATE(ESC_SENSOR_ENABLED)) {
3862 if (elementIndex == OSD_ESC_RPM) {
3863 elementIndex = OSD_AZIMUTH;
3867 if (!feature(FEATURE_GPS)) {
3868 if (elementIndex == OSD_GPS_HDOP || elementIndex == OSD_TRIP_DIST || elementIndex == OSD_3D_SPEED || elementIndex == OSD_MISSION ||
3869 elementIndex == OSD_AZIMUTH || elementIndex == OSD_BATTERY_REMAINING_CAPACITY || elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3870 elementIndex++;
3872 if (elementIndex == OSD_HEADING_GRAPH && !sensors(SENSOR_MAG)) {
3873 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_WH_DRAWN : OSD_BATTERY_REMAINING_PERCENT;
3875 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3876 elementIndex = OSD_ATTITUDE_PITCH;
3878 if (elementIndex == OSD_GPS_SPEED) {
3879 elementIndex = OSD_ALTITUDE;
3881 if (elementIndex == OSD_GPS_LON) {
3882 elementIndex = sensors(SENSOR_MAG) ? OSD_HEADING : OSD_VARIO;
3884 if (elementIndex == OSD_MAP_NORTH) {
3885 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE : OSD_LEVEL_PIDS;
3887 if (elementIndex == OSD_PLUS_CODE) {
3888 elementIndex = OSD_GFORCE;
3890 if (elementIndex == OSD_GLIDESLOPE) {
3891 elementIndex = OSD_AIR_MAX_SPEED;
3893 if (elementIndex == OSD_GLIDE_RANGE) {
3894 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_CLIMB_EFFICIENCY : OSD_PILOT_NAME;
3896 if (elementIndex == OSD_NAV_WP_MULTI_MISSION_INDEX) {
3897 elementIndex = OSD_PILOT_NAME;
3901 if (!sensors(SENSOR_ACC)) {
3902 if (elementIndex == OSD_CROSSHAIRS) {
3903 elementIndex = OSD_ONTIME;
3905 if (elementIndex == OSD_GFORCE) {
3906 elementIndex = OSD_RC_SOURCE;
3910 if (elementIndex == OSD_ITEM_COUNT) {
3911 elementIndex = 0;
3913 return elementIndex;
3916 void osdDrawNextElement(void)
3918 static uint8_t elementIndex = 0;
3919 // Flag for end of loop, also prevents infinite loop when no elements are enabled
3920 uint8_t index = elementIndex;
3921 do {
3922 elementIndex = osdIncElementIndex(elementIndex);
3923 } while (!osdDrawSingleElement(elementIndex) && index != elementIndex);
3925 // Draw artificial horizon + tracking telemetry last
3926 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
3927 if (osdConfig()->telemetry>0){
3928 osdDisplayTelemetry();
3932 PG_RESET_TEMPLATE(osdConfig_t, osdConfig,
3933 .rssi_alarm = SETTING_OSD_RSSI_ALARM_DEFAULT,
3934 .time_alarm = SETTING_OSD_TIME_ALARM_DEFAULT,
3935 .alt_alarm = SETTING_OSD_ALT_ALARM_DEFAULT,
3936 .dist_alarm = SETTING_OSD_DIST_ALARM_DEFAULT,
3937 .neg_alt_alarm = SETTING_OSD_NEG_ALT_ALARM_DEFAULT,
3938 .current_alarm = SETTING_OSD_CURRENT_ALARM_DEFAULT,
3939 .imu_temp_alarm_min = SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT,
3940 .imu_temp_alarm_max = SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT,
3941 .esc_temp_alarm_min = SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT,
3942 .esc_temp_alarm_max = SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT,
3943 .gforce_alarm = SETTING_OSD_GFORCE_ALARM_DEFAULT,
3944 .gforce_axis_alarm_min = SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT,
3945 .gforce_axis_alarm_max = SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT,
3946 #ifdef USE_BARO
3947 .baro_temp_alarm_min = SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT,
3948 .baro_temp_alarm_max = SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT,
3949 #endif
3950 #ifdef USE_ADSB
3951 .adsb_distance_warning = SETTING_OSD_ADSB_DISTANCE_WARNING_DEFAULT,
3952 .adsb_distance_alert = SETTING_OSD_ADSB_DISTANCE_ALERT_DEFAULT,
3953 .adsb_ignore_plane_above_me_limit = SETTING_OSD_ADSB_IGNORE_PLANE_ABOVE_ME_LIMIT_DEFAULT,
3954 #endif
3955 #ifdef USE_SERIALRX_CRSF
3956 .snr_alarm = SETTING_OSD_SNR_ALARM_DEFAULT,
3957 .crsf_lq_format = SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT,
3958 .link_quality_alarm = SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT,
3959 .rssi_dbm_alarm = SETTING_OSD_RSSI_DBM_ALARM_DEFAULT,
3960 .rssi_dbm_max = SETTING_OSD_RSSI_DBM_MAX_DEFAULT,
3961 .rssi_dbm_min = SETTING_OSD_RSSI_DBM_MIN_DEFAULT,
3962 #endif
3963 #ifdef USE_TEMPERATURE_SENSOR
3964 .temp_label_align = SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT,
3965 #endif
3966 #ifdef USE_PITOT
3967 .airspeed_alarm_min = SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT,
3968 .airspeed_alarm_max = SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT,
3969 #endif
3970 #ifndef DISABLE_MSP_DJI_COMPAT
3971 .highlight_djis_missing_characters = SETTING_OSD_HIGHLIGHT_DJIS_MISSING_FONT_SYMBOLS_DEFAULT,
3972 #endif
3974 .video_system = SETTING_OSD_VIDEO_SYSTEM_DEFAULT,
3975 .row_shiftdown = SETTING_OSD_ROW_SHIFTDOWN_DEFAULT,
3976 .msp_displayport_fullframe_interval = SETTING_OSD_MSP_DISPLAYPORT_FULLFRAME_INTERVAL_DEFAULT,
3978 .ahi_reverse_roll = SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT,
3979 .ahi_max_pitch = SETTING_OSD_AHI_MAX_PITCH_DEFAULT,
3980 .crosshairs_style = SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT,
3981 .horizon_offset = SETTING_OSD_HORIZON_OFFSET_DEFAULT,
3982 .camera_uptilt = SETTING_OSD_CAMERA_UPTILT_DEFAULT,
3983 .ahi_camera_uptilt_comp = SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT,
3984 .camera_fov_h = SETTING_OSD_CAMERA_FOV_H_DEFAULT,
3985 .camera_fov_v = SETTING_OSD_CAMERA_FOV_V_DEFAULT,
3986 .hud_margin_h = SETTING_OSD_HUD_MARGIN_H_DEFAULT,
3987 .hud_margin_v = SETTING_OSD_HUD_MARGIN_V_DEFAULT,
3988 .hud_homing = SETTING_OSD_HUD_HOMING_DEFAULT,
3989 .hud_homepoint = SETTING_OSD_HUD_HOMEPOINT_DEFAULT,
3990 .hud_radar_disp = SETTING_OSD_HUD_RADAR_DISP_DEFAULT,
3991 .hud_radar_range_min = SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT,
3992 .hud_radar_range_max = SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT,
3993 .hud_radar_alt_difference_display_time = SETTING_OSD_HUD_RADAR_ALT_DIFFERENCE_DISPLAY_TIME_DEFAULT,
3994 .hud_radar_distance_display_time = SETTING_OSD_HUD_RADAR_DISTANCE_DISPLAY_TIME_DEFAULT,
3995 .hud_wp_disp = SETTING_OSD_HUD_WP_DISP_DEFAULT,
3996 .left_sidebar_scroll = SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT,
3997 .right_sidebar_scroll = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT,
3998 .sidebar_scroll_arrows = SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT,
3999 .sidebar_horizontal_offset = SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT,
4000 .left_sidebar_scroll_step = SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT,
4001 .right_sidebar_scroll_step = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT,
4002 .sidebar_height = SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT,
4003 .ahi_pitch_interval = SETTING_OSD_AHI_PITCH_INTERVAL_DEFAULT,
4004 .osd_home_position_arm_screen = SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT,
4005 .pan_servo_index = SETTING_OSD_PAN_SERVO_INDEX_DEFAULT,
4006 .pan_servo_pwm2centideg = SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT,
4007 .pan_servo_offcentre_warning = SETTING_OSD_PAN_SERVO_OFFCENTRE_WARNING_DEFAULT,
4008 .pan_servo_indicator_show_degrees = SETTING_OSD_PAN_SERVO_INDICATOR_SHOW_DEGREES_DEFAULT,
4009 .esc_rpm_precision = SETTING_OSD_ESC_RPM_PRECISION_DEFAULT,
4010 .mAh_precision = SETTING_OSD_MAH_PRECISION_DEFAULT,
4011 .osd_switch_indicator0_name = SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT,
4012 .osd_switch_indicator0_channel = SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNEL_DEFAULT,
4013 .osd_switch_indicator1_name = SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT,
4014 .osd_switch_indicator1_channel = SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNEL_DEFAULT,
4015 .osd_switch_indicator2_name = SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT,
4016 .osd_switch_indicator2_channel = SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNEL_DEFAULT,
4017 .osd_switch_indicator3_name = SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT,
4018 .osd_switch_indicator3_channel = SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNEL_DEFAULT,
4019 .osd_switch_indicators_align_left = SETTING_OSD_SWITCH_INDICATORS_ALIGN_LEFT_DEFAULT,
4020 .system_msg_display_time = SETTING_OSD_SYSTEM_MSG_DISPLAY_TIME_DEFAULT,
4021 .units = SETTING_OSD_UNITS_DEFAULT,
4022 .main_voltage_decimals = SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT,
4023 .use_pilot_logo = SETTING_OSD_USE_PILOT_LOGO_DEFAULT,
4024 .inav_to_pilot_logo_spacing = SETTING_OSD_INAV_TO_PILOT_LOGO_SPACING_DEFAULT,
4025 .arm_screen_display_time = SETTING_OSD_ARM_SCREEN_DISPLAY_TIME_DEFAULT,
4027 #ifdef USE_WIND_ESTIMATOR
4028 .estimations_wind_compensation = SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT,
4029 #endif
4031 .coordinate_digits = SETTING_OSD_COORDINATE_DIGITS_DEFAULT,
4033 .osd_failsafe_switch_layout = SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT,
4035 .plus_code_digits = SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT,
4036 .plus_code_short = SETTING_OSD_PLUS_CODE_SHORT_DEFAULT,
4038 .ahi_width = SETTING_OSD_AHI_WIDTH_DEFAULT,
4039 .ahi_height = SETTING_OSD_AHI_HEIGHT_DEFAULT,
4040 .ahi_vertical_offset = SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT,
4041 .ahi_bordered = SETTING_OSD_AHI_BORDERED_DEFAULT,
4042 .ahi_style = SETTING_OSD_AHI_STYLE_DEFAULT,
4044 .force_grid = SETTING_OSD_FORCE_GRID_DEFAULT,
4046 .stats_energy_unit = SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT,
4047 .stats_page_auto_swap_time = SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT,
4048 .stats_show_metric_efficiency = SETTING_OSD_STATS_SHOW_METRIC_EFFICIENCY_DEFAULT,
4050 .radar_peers_display_time = SETTING_OSD_RADAR_PEERS_DISPLAY_TIME_DEFAULT
4053 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t *osdLayoutsConfig)
4055 osdLayoutsConfig->item_pos[0][OSD_ALTITUDE] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG;
4056 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_VOLTAGE] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG;
4057 osdLayoutsConfig->item_pos[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE] = OSD_POS(12, 1);
4059 osdLayoutsConfig->item_pos[0][OSD_RSSI_VALUE] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG;
4060 //line 2
4061 osdLayoutsConfig->item_pos[0][OSD_HOME_DIST] = OSD_POS(1, 1);
4062 osdLayoutsConfig->item_pos[0][OSD_TRIP_DIST] = OSD_POS(1, 2);
4063 osdLayoutsConfig->item_pos[0][OSD_ODOMETER] = OSD_POS(1, 3);
4064 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_CELL_VOLTAGE] = OSD_POS(12, 1);
4065 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE] = OSD_POS(12, 1);
4066 osdLayoutsConfig->item_pos[0][OSD_GPS_SPEED] = OSD_POS(23, 1);
4067 osdLayoutsConfig->item_pos[0][OSD_3D_SPEED] = OSD_POS(23, 1);
4068 osdLayoutsConfig->item_pos[0][OSD_GLIDESLOPE] = OSD_POS(23, 2);
4070 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG;
4071 osdLayoutsConfig->item_pos[0][OSD_SCALED_THROTTLE_POS] = OSD_POS(6, 2);
4072 osdLayoutsConfig->item_pos[0][OSD_HEADING] = OSD_POS(12, 2);
4073 osdLayoutsConfig->item_pos[0][OSD_GROUND_COURSE] = OSD_POS(12, 3);
4074 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ERROR] = OSD_POS(12, 2);
4075 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ADJUSTMENT] = OSD_POS(12, 2);
4076 osdLayoutsConfig->item_pos[0][OSD_CROSS_TRACK_ERROR] = OSD_POS(12, 3);
4077 osdLayoutsConfig->item_pos[0][OSD_HEADING_GRAPH] = OSD_POS(18, 2);
4078 osdLayoutsConfig->item_pos[0][OSD_CURRENT_DRAW] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG;
4079 osdLayoutsConfig->item_pos[0][OSD_MAH_DRAWN] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG;
4080 osdLayoutsConfig->item_pos[0][OSD_WH_DRAWN] = OSD_POS(1, 5);
4081 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_CAPACITY] = OSD_POS(1, 6);
4082 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_PERCENT] = OSD_POS(1, 7);
4083 osdLayoutsConfig->item_pos[0][OSD_POWER_SUPPLY_IMPEDANCE] = OSD_POS(1, 8);
4085 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_MAH_PER_KM] = OSD_POS(1, 5);
4086 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_WH_PER_KM] = OSD_POS(1, 5);
4088 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_ROLL] = OSD_POS(1, 7);
4089 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_PITCH] = OSD_POS(1, 8);
4091 // avoid OSD_VARIO under OSD_CROSSHAIRS
4092 osdLayoutsConfig->item_pos[0][OSD_VARIO] = OSD_POS(23, 5);
4093 // OSD_VARIO_NUM at the right of OSD_VARIO
4094 osdLayoutsConfig->item_pos[0][OSD_VARIO_NUM] = OSD_POS(24, 7);
4095 osdLayoutsConfig->item_pos[0][OSD_HOME_DIR] = OSD_POS(14, 11);
4096 osdLayoutsConfig->item_pos[0][OSD_ARTIFICIAL_HORIZON] = OSD_POS(8, 6);
4097 osdLayoutsConfig->item_pos[0][OSD_HORIZON_SIDEBARS] = OSD_POS(8, 6);
4099 osdLayoutsConfig->item_pos[0][OSD_CRAFT_NAME] = OSD_POS(20, 2);
4100 osdLayoutsConfig->item_pos[0][OSD_PILOT_NAME] = OSD_POS(20, 3);
4101 osdLayoutsConfig->item_pos[0][OSD_PILOT_LOGO] = OSD_POS(20, 3);
4102 osdLayoutsConfig->item_pos[0][OSD_VTX_CHANNEL] = OSD_POS(8, 6);
4104 #ifdef USE_SERIALRX_CRSF
4105 osdLayoutsConfig->item_pos[0][OSD_CRSF_RSSI_DBM] = OSD_POS(23, 12);
4106 osdLayoutsConfig->item_pos[0][OSD_CRSF_LQ] = OSD_POS(23, 11);
4107 osdLayoutsConfig->item_pos[0][OSD_CRSF_SNR_DB] = OSD_POS(24, 9);
4108 osdLayoutsConfig->item_pos[0][OSD_CRSF_TX_POWER] = OSD_POS(24, 10);
4109 #endif
4111 osdLayoutsConfig->item_pos[0][OSD_ONTIME] = OSD_POS(23, 8);
4112 osdLayoutsConfig->item_pos[0][OSD_FLYTIME] = OSD_POS(23, 9);
4113 osdLayoutsConfig->item_pos[0][OSD_ONTIME_FLYTIME] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG;
4114 osdLayoutsConfig->item_pos[0][OSD_RTC_TIME] = OSD_POS(23, 12);
4115 osdLayoutsConfig->item_pos[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH] = OSD_POS(23, 7);
4116 osdLayoutsConfig->item_pos[0][OSD_REMAINING_DISTANCE_BEFORE_RTH] = OSD_POS(23, 6);
4118 osdLayoutsConfig->item_pos[0][OSD_MISSION] = OSD_POS(0, 10);
4119 osdLayoutsConfig->item_pos[0][OSD_GPS_SATS] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG;
4120 osdLayoutsConfig->item_pos[0][OSD_GPS_HDOP] = OSD_POS(0, 10);
4122 osdLayoutsConfig->item_pos[0][OSD_GPS_LAT] = OSD_POS(0, 12);
4123 // Put this on top of the latitude, since it's very unlikely
4124 // that users will want to use both at the same time.
4125 osdLayoutsConfig->item_pos[0][OSD_PLUS_CODE] = OSD_POS(0, 12);
4126 osdLayoutsConfig->item_pos[0][OSD_FLYMODE] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG;
4127 osdLayoutsConfig->item_pos[0][OSD_GPS_LON] = OSD_POS(18, 12);
4129 osdLayoutsConfig->item_pos[0][OSD_AZIMUTH] = OSD_POS(2, 12);
4131 osdLayoutsConfig->item_pos[0][OSD_ROLL_PIDS] = OSD_POS(2, 10);
4132 osdLayoutsConfig->item_pos[0][OSD_PITCH_PIDS] = OSD_POS(2, 11);
4133 osdLayoutsConfig->item_pos[0][OSD_YAW_PIDS] = OSD_POS(2, 12);
4134 osdLayoutsConfig->item_pos[0][OSD_LEVEL_PIDS] = OSD_POS(2, 12);
4135 osdLayoutsConfig->item_pos[0][OSD_POS_XY_PIDS] = OSD_POS(2, 12);
4136 osdLayoutsConfig->item_pos[0][OSD_POS_Z_PIDS] = OSD_POS(2, 12);
4137 osdLayoutsConfig->item_pos[0][OSD_VEL_XY_PIDS] = OSD_POS(2, 12);
4138 osdLayoutsConfig->item_pos[0][OSD_VEL_Z_PIDS] = OSD_POS(2, 12);
4139 osdLayoutsConfig->item_pos[0][OSD_HEADING_P] = OSD_POS(2, 12);
4140 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_ROLL] = OSD_POS(2, 10);
4141 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_PITCH] = OSD_POS(2, 11);
4142 osdLayoutsConfig->item_pos[0][OSD_RC_EXPO] = OSD_POS(2, 12);
4143 osdLayoutsConfig->item_pos[0][OSD_RC_YAW_EXPO] = OSD_POS(2, 12);
4144 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_EXPO] = OSD_POS(2, 12);
4145 osdLayoutsConfig->item_pos[0][OSD_PITCH_RATE] = OSD_POS(2, 12);
4146 osdLayoutsConfig->item_pos[0][OSD_ROLL_RATE] = OSD_POS(2, 12);
4147 osdLayoutsConfig->item_pos[0][OSD_YAW_RATE] = OSD_POS(2, 12);
4148 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_EXPO] = OSD_POS(2, 12);
4149 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_YAW_EXPO] = OSD_POS(2, 12);
4150 osdLayoutsConfig->item_pos[0][OSD_MANUAL_PITCH_RATE] = OSD_POS(2, 12);
4151 osdLayoutsConfig->item_pos[0][OSD_MANUAL_ROLL_RATE] = OSD_POS(2, 12);
4152 osdLayoutsConfig->item_pos[0][OSD_MANUAL_YAW_RATE] = OSD_POS(2, 12);
4153 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_CRUISE_THR] = OSD_POS(2, 12);
4154 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_PITCH2THR] = OSD_POS(2, 12);
4155 osdLayoutsConfig->item_pos[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE] = OSD_POS(2, 12);
4156 osdLayoutsConfig->item_pos[0][OSD_FW_ALT_PID_OUTPUTS] = OSD_POS(2, 12);
4157 osdLayoutsConfig->item_pos[0][OSD_FW_POS_PID_OUTPUTS] = OSD_POS(2, 12);
4158 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_X_PID_OUTPUTS] = OSD_POS(2, 12);
4159 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Y_PID_OUTPUTS] = OSD_POS(2, 12);
4160 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Z_PID_OUTPUTS] = OSD_POS(2, 12);
4161 osdLayoutsConfig->item_pos[0][OSD_MC_POS_XYZ_P_OUTPUTS] = OSD_POS(2, 12);
4163 osdLayoutsConfig->item_pos[0][OSD_POWER] = OSD_POS(15, 1);
4165 osdLayoutsConfig->item_pos[0][OSD_IMU_TEMPERATURE] = OSD_POS(19, 2);
4166 osdLayoutsConfig->item_pos[0][OSD_BARO_TEMPERATURE] = OSD_POS(19, 3);
4167 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_0_TEMPERATURE] = OSD_POS(19, 4);
4168 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_1_TEMPERATURE] = OSD_POS(19, 5);
4169 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_2_TEMPERATURE] = OSD_POS(19, 6);
4170 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_3_TEMPERATURE] = OSD_POS(19, 7);
4171 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_4_TEMPERATURE] = OSD_POS(19, 8);
4172 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_5_TEMPERATURE] = OSD_POS(19, 9);
4173 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_6_TEMPERATURE] = OSD_POS(19, 10);
4174 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_7_TEMPERATURE] = OSD_POS(19, 11);
4176 osdLayoutsConfig->item_pos[0][OSD_AIR_SPEED] = OSD_POS(3, 5);
4177 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_HORIZONTAL] = OSD_POS(3, 6);
4178 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_VERTICAL] = OSD_POS(3, 7);
4180 osdLayoutsConfig->item_pos[0][OSD_GFORCE] = OSD_POS(12, 4);
4181 osdLayoutsConfig->item_pos[0][OSD_GFORCE_X] = OSD_POS(12, 5);
4182 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Y] = OSD_POS(12, 6);
4183 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Z] = OSD_POS(12, 7);
4185 osdLayoutsConfig->item_pos[0][OSD_VTX_POWER] = OSD_POS(3, 5);
4187 osdLayoutsConfig->item_pos[0][OSD_GVAR_0] = OSD_POS(1, 1);
4188 osdLayoutsConfig->item_pos[0][OSD_GVAR_1] = OSD_POS(1, 2);
4189 osdLayoutsConfig->item_pos[0][OSD_GVAR_2] = OSD_POS(1, 3);
4190 osdLayoutsConfig->item_pos[0][OSD_GVAR_3] = OSD_POS(1, 4);
4192 osdLayoutsConfig->item_pos[0][OSD_MULTI_FUNCTION] = OSD_POS(1, 4);
4194 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_0] = OSD_POS(2, 7);
4195 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_1] = OSD_POS(2, 8);
4196 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_2] = OSD_POS(2, 9);
4197 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_3] = OSD_POS(2, 10);
4199 osdLayoutsConfig->item_pos[0][OSD_ADSB_WARNING] = OSD_POS(2, 7);
4200 osdLayoutsConfig->item_pos[0][OSD_ADSB_INFO] = OSD_POS(2, 8);
4201 #if defined(USE_ESC_SENSOR)
4202 osdLayoutsConfig->item_pos[0][OSD_ESC_RPM] = OSD_POS(1, 2);
4203 osdLayoutsConfig->item_pos[0][OSD_ESC_TEMPERATURE] = OSD_POS(1, 3);
4204 #endif
4206 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
4207 osdLayoutsConfig->item_pos[0][OSD_RC_SOURCE] = OSD_POS(3, 4);
4208 #endif
4210 #ifdef USE_POWER_LIMITS
4211 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_REMAINING_BURST_TIME] = OSD_POS(3, 4);
4212 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT] = OSD_POS(3, 5);
4213 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT] = OSD_POS(3, 6);
4214 #endif
4216 #ifdef USE_BLACKBOX
4217 osdLayoutsConfig->item_pos[0][OSD_BLACKBOX] = OSD_POS(2, 10);
4218 #endif
4220 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
4221 osdLayoutsConfig->item_pos[0][OSD_MESSAGES] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG;
4223 for (unsigned ii = 1; ii < OSD_LAYOUT_COUNT; ii++) {
4224 for (unsigned jj = 0; jj < ARRAYLEN(osdLayoutsConfig->item_pos[0]); jj++) {
4225 osdLayoutsConfig->item_pos[ii][jj] = osdLayoutsConfig->item_pos[0][jj] & ~OSD_VISIBLE_FLAG;
4231 * @brief Draws the INAV and/or pilot logos on the display
4233 * @param singular If true, only one logo will be drawn. If false, both logos will be drawn, separated by osdConfig()->inav_to_pilot_logo_spacing characters
4234 * @param row The row number to start drawing the logos. If not singular, both logos are drawn on the same rows.
4235 * @return uint8_t The row number after the logo(s).
4237 uint8_t drawLogos(bool singular, uint8_t row) {
4238 uint8_t logoRow = row;
4239 uint8_t logoColOffset = 0;
4240 bool usePilotLogo = (osdConfig()->use_pilot_logo && osdDisplayIsHD());
4241 bool useINAVLogo = (singular && !usePilotLogo) || !singular;
4243 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is in use, the pilot logo cannot be used, due to font issues.
4244 if (isDJICompatibleVideoSystem(osdConfig())) {
4245 usePilotLogo = false;
4246 useINAVLogo = false;
4248 #endif
4250 uint8_t logoSpacing = osdConfig()->inav_to_pilot_logo_spacing;
4252 if (logoSpacing > 0 && ((osdDisplayPort->cols % 2) != (logoSpacing % 2))) {
4253 logoSpacing++; // Add extra 1 character space between logos, if the odd/even of the OSD cols doesn't match the odd/even of the logo spacing
4256 // Draw Logo(s)
4257 if (usePilotLogo && !singular) {
4258 logoColOffset = ((osdDisplayPort->cols - (SYM_LOGO_WIDTH * 2)) - logoSpacing) / 2;
4259 } else {
4260 logoColOffset = floorf((osdDisplayPort->cols - SYM_LOGO_WIDTH) / 2.0f);
4263 // Draw INAV logo
4264 if (useINAVLogo) {
4265 unsigned logo_c = SYM_LOGO_START;
4266 uint8_t logo_x = logoColOffset;
4267 for (uint8_t lRow = 0; lRow < SYM_LOGO_HEIGHT; lRow++) {
4268 for (uint8_t lCol = 0; lCol < SYM_LOGO_WIDTH; lCol++) {
4269 displayWriteChar(osdDisplayPort, logo_x + lCol, logoRow, logo_c++);
4271 logoRow++;
4275 // Draw the pilot logo
4276 if (usePilotLogo) {
4277 unsigned logo_c = SYM_PILOT_LOGO_LRG_START;
4278 uint8_t logo_x = 0;
4279 logoRow = row;
4280 if (singular) {
4281 logo_x = logoColOffset;
4282 } else {
4283 logo_x = logoColOffset + SYM_LOGO_WIDTH + logoSpacing;
4286 for (uint8_t lRow = 0; lRow < SYM_LOGO_HEIGHT; lRow++) {
4287 for (uint8_t lCol = 0; lCol < SYM_LOGO_WIDTH; lCol++) {
4288 displayWriteChar(osdDisplayPort, logo_x + lCol, logoRow, logo_c++);
4290 logoRow++;
4294 if (!usePilotLogo && !useINAVLogo) {
4295 logoRow += SYM_LOGO_HEIGHT;
4298 return logoRow;
4301 #ifdef USE_STATS
4302 uint8_t drawStat_Stats(uint8_t statNameX, uint8_t row, uint8_t statValueX, bool isBootStats)
4304 uint8_t buffLen = 0;
4305 char string_buffer[osdDisplayPort->cols - statValueX];
4307 if (statsConfig()->stats_enabled) {
4308 if (isBootStats)
4309 displayWrite(osdDisplayPort, statNameX, row, "ODOMETER:");
4310 else
4311 displayWrite(osdDisplayPort, statNameX, row, "ODOMETER");
4313 switch (osdConfig()->units) {
4314 case OSD_UNIT_UK:
4315 FALLTHROUGH;
4316 case OSD_UNIT_IMPERIAL:
4317 if (isBootStats) {
4318 tfp_sprintf(string_buffer, "%5d", (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_MILE));
4319 buffLen = 5;
4320 } else {
4321 uint16_t statTotalDist = (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_MILE);
4322 tfp_sprintf(string_buffer, ": %d", statTotalDist);
4323 buffLen = 3 + sizeof(statTotalDist);
4326 string_buffer[buffLen++] = SYM_MI;
4327 break;
4328 default:
4329 case OSD_UNIT_GA:
4330 if (isBootStats) {
4331 tfp_sprintf(string_buffer, "%5d", (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE));
4332 buffLen = 5;
4333 } else {
4334 uint16_t statTotalDist = (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE);
4335 tfp_sprintf(string_buffer, ": %d", statTotalDist);
4336 buffLen = 3 + sizeof(statTotalDist);
4339 string_buffer[buffLen++] = SYM_NM;
4340 break;
4341 case OSD_UNIT_METRIC_MPH:
4342 FALLTHROUGH;
4343 case OSD_UNIT_METRIC:
4344 if (isBootStats) {
4345 tfp_sprintf(string_buffer, "%5d", (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER));
4346 buffLen = 5;
4347 } else {
4348 uint16_t statTotalDist = (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER);
4349 tfp_sprintf(string_buffer, ": %d", statTotalDist);
4350 buffLen = 3 + sizeof(statTotalDist);
4353 string_buffer[buffLen++] = SYM_KM;
4354 break;
4356 string_buffer[buffLen] = '\0';
4357 displayWrite(osdDisplayPort, statValueX-(isBootStats ? 5 : 0), row, string_buffer);
4359 if (isBootStats)
4360 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL TIME:");
4361 else
4362 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL TIME");
4364 uint32_t tot_mins = statsConfig()->stats_total_time / 60;
4365 if (isBootStats)
4366 tfp_sprintf(string_buffer, "%d:%02dH:M%c", (int)(tot_mins / 60), (int)(tot_mins % 60), '\0');
4367 else
4368 tfp_sprintf(string_buffer, ": %d:%02d H:M%c", (int)(tot_mins / 60), (int)(tot_mins % 60), '\0');
4370 displayWrite(osdDisplayPort, statValueX-(isBootStats ? 7 : 0), row, string_buffer);
4372 #ifdef USE_ADC
4373 if (feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER) && statsConfig()->stats_total_energy) {
4374 uint8_t buffOffset = 0;
4375 if (isBootStats)
4376 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL ENERGY:");
4377 else {
4378 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL ENERGY");
4379 string_buffer[0] = ':';
4380 buffOffset = 2;
4383 osdFormatCentiNumber(string_buffer + buffOffset, statsConfig()->stats_total_energy / 10, 0, 2, 0, 6, true);
4384 displayWrite(osdDisplayPort, statValueX - (isBootStats ? 6 : 0), row, string_buffer);
4385 displayWriteChar(osdDisplayPort, statValueX + (isBootStats ? 0 : 8), row, SYM_WH);
4387 char avgEffBuff[osdDisplayPort->cols - statValueX];
4389 for (uint8_t i = 0; i < osdDisplayPort->cols - statValueX; i++) {
4390 avgEffBuff[i] = '\0';
4391 string_buffer[i] = '\0';
4394 if (statsConfig()->stats_total_dist) {
4395 if (isBootStats)
4396 displayWrite(osdDisplayPort, statNameX, ++row, "AVG EFFICIENCY:");
4397 else {
4398 displayWrite(osdDisplayPort, statNameX, ++row, "AVG EFFICIENCY");
4399 strcat(avgEffBuff, ": ");
4402 float_t avg_efficiency = MWH_TO_WH(statsConfig()->stats_total_energy) / METERS_TO_KILOMETERS(statsConfig()->stats_total_dist); // Wh/km
4403 switch (osdConfig()->units) {
4404 case OSD_UNIT_UK:
4405 FALLTHROUGH;
4406 case OSD_UNIT_IMPERIAL:
4407 osdFormatCentiNumber(string_buffer, (int32_t)(avg_efficiency * METERS_PER_MILE / 10), 0, 2, 2, 4, false);
4408 string_buffer[4] = SYM_WH_MI;
4409 break;
4410 case OSD_UNIT_GA:
4411 osdFormatCentiNumber(string_buffer, (int32_t)(avg_efficiency * METERS_PER_NAUTICALMILE / 10), 0, 2, 2, 4, false);
4412 string_buffer[4] = SYM_WH_NM;
4413 break;
4414 default:
4415 case OSD_UNIT_METRIC_MPH:
4416 FALLTHROUGH;
4417 case OSD_UNIT_METRIC:
4418 osdFormatCentiNumber(string_buffer, (int32_t)(avg_efficiency * 100), 0, 2, 2, 4, false);
4419 string_buffer[4] = SYM_WH_KM;
4420 break;
4423 if (isBootStats)
4424 strcat(avgEffBuff, string_buffer);
4425 else
4426 strcat(avgEffBuff, osdFormatTrimWhiteSpace(string_buffer));
4427 } else {
4428 strcat(avgEffBuff, "----");
4431 displayWrite(osdDisplayPort, statValueX-(isBootStats ? 4 : 0), row++, avgEffBuff);
4433 #endif // USE_ADC
4435 return row;
4438 uint8_t drawStats(uint8_t row)
4440 uint8_t statNameX = (osdDisplayPort->cols - 22) / 2;
4441 uint8_t statValueX = statNameX + 21;
4443 return drawStat_Stats(statNameX, row, statValueX, true);
4445 #endif // USE STATS
4447 static void osdSetNextRefreshIn(uint32_t timeMs)
4449 resumeRefreshAt = micros() + timeMs * 1000;
4450 refreshWaitForResumeCmdRelease = true;
4453 static void osdCompleteAsyncInitialization(void)
4455 if (!displayIsReady(osdDisplayPort)) {
4456 // Update the display.
4457 // XXX: Rename displayDrawScreen() and associated functions
4458 // to displayUpdate()
4459 displayDrawScreen(osdDisplayPort);
4460 return;
4463 osdDisplayIsReady = true;
4465 #if defined(USE_CANVAS)
4466 if (osdConfig()->force_grid) {
4467 osdDisplayHasCanvas = false;
4468 } else {
4469 osdDisplayHasCanvas = displayGetCanvas(&osdCanvas, osdDisplayPort);
4471 #endif
4473 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4474 displayClearScreen(osdDisplayPort);
4476 uint8_t y = 1;
4477 displayFontMetadata_t metadata;
4478 bool fontHasMetadata = displayGetFontMetadata(&metadata, osdDisplayPort);
4479 LOG_DEBUG(OSD, "Font metadata version %s: %u (%u chars)",
4480 fontHasMetadata ? "Y" : "N", metadata.version, metadata.charCount);
4482 if (fontHasMetadata && metadata.charCount > 256) {
4483 hasExtendedFont = true;
4485 y = drawLogos(false, y);
4486 y++;
4487 } else if (!fontHasMetadata) {
4488 const char *m = "INVALID FONT";
4489 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
4492 if (fontHasMetadata && metadata.version < OSD_MIN_FONT_VERSION) {
4493 const char *m = "INVALID FONT VERSION";
4494 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
4497 char string_buffer[30];
4498 tfp_sprintf(string_buffer, "INAV VERSION: %s", FC_VERSION_STRING);
4499 uint8_t xPos = (osdDisplayPort->cols - 19) / 2; // Automatically centre, regardless of resolution. In the case of odd number screens, bias to the left.
4500 displayWrite(osdDisplayPort, xPos, y++, string_buffer);
4501 #ifdef USE_CMS
4502 displayWrite(osdDisplayPort, xPos+2, y++, CMS_STARTUP_HELP_TEXT1);
4503 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT2);
4504 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT3);
4505 #endif
4507 #ifdef USE_STATS
4508 y = drawStats(++y);
4509 #endif
4511 displayCommitTransaction(osdDisplayPort);
4512 displayResync(osdDisplayPort);
4513 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME);
4516 void osdInit(displayPort_t *osdDisplayPortToUse)
4518 if (!osdDisplayPortToUse)
4519 return;
4521 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(63,63));
4523 osdDisplayPort = osdDisplayPortToUse;
4525 #ifdef USE_CMS
4526 cmsDisplayPortRegister(osdDisplayPort);
4527 #endif
4529 armState = ARMING_FLAG(ARMED);
4530 osdCompleteAsyncInitialization();
4533 static void osdResetStats(void)
4535 // Reset internal OSD stats
4536 stats.max_distance = 0;
4537 stats.max_current = 0;
4538 stats.max_power = 0;
4539 stats.max_speed = 0;
4540 stats.max_3D_speed = 0;
4541 stats.max_air_speed = 0;
4542 stats.min_voltage = 12000;
4543 stats.min_rssi = 99;
4544 stats.min_lq = 300;
4545 stats.min_rssi_dbm = 0;
4546 stats.max_altitude = 0;
4547 stats.min_sats = 255;
4548 stats.max_sats = 0;
4549 stats.min_esc_temp = 300;
4550 stats.max_esc_temp = 0;
4551 stats.flightStartMAh = getMAhDrawn();
4552 stats.flightStartMWh = getMWhDrawn();
4554 // Reset external stats
4555 posControl.totalTripDistance = 0.0f;
4556 resetFlightTime();
4557 resetGForceStats();
4560 static void osdUpdateStats(void)
4562 int32_t value;
4564 if (feature(FEATURE_GPS)) {
4565 value = osdGet3DSpeed();
4566 const float airspeed_estimate = getAirspeedEstimate();
4568 if (stats.max_3D_speed < value)
4569 stats.max_3D_speed = value;
4571 if (stats.max_speed < gpsSol.groundSpeed)
4572 stats.max_speed = gpsSol.groundSpeed;
4574 if (stats.max_air_speed < airspeed_estimate)
4575 stats.max_air_speed = airspeed_estimate;
4577 if (stats.max_distance < GPS_distanceToHome)
4578 stats.max_distance = GPS_distanceToHome;
4580 if (stats.min_sats > gpsSol.numSat)
4581 stats.min_sats = gpsSol.numSat;
4583 if (stats.max_sats < gpsSol.numSat)
4584 stats.max_sats = gpsSol.numSat;
4586 #if defined(USE_ESC_SENSOR)
4587 if (STATE(ESC_SENSOR_ENABLED)) {
4588 escSensorData_t * escSensor = escSensorGetData();
4589 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
4591 if (escTemperatureValid) {
4592 if (stats.min_esc_temp > escSensor->temperature)
4593 stats.min_esc_temp = escSensor->temperature;
4595 if (stats.max_esc_temp < escSensor->temperature)
4596 stats.max_esc_temp = escSensor->temperature;
4599 #endif
4601 value = getBatteryVoltage();
4602 if (stats.min_voltage > value)
4603 stats.min_voltage = value;
4605 value = abs(getAmperage());
4606 if (stats.max_current < value)
4607 stats.max_current = value;
4609 value = labs(getPower());
4610 if (stats.max_power < value)
4611 stats.max_power = value;
4613 value = osdConvertRSSI();
4614 if (stats.min_rssi > value)
4615 stats.min_rssi = value;
4617 value = osdGetCrsfLQ();
4618 if (stats.min_lq > value)
4619 stats.min_lq = value;
4621 if (!failsafeIsReceivingRxData())
4622 stats.min_lq = 0;
4624 value = osdGetCrsfdBm();
4625 if (stats.min_rssi_dbm > value)
4626 stats.min_rssi_dbm = value;
4628 stats.max_altitude = MAX(stats.max_altitude, osdGetAltitude());
4631 uint8_t drawStat_FlightTime(uint8_t col, uint8_t row, uint8_t statValX)
4633 char buff[12];
4634 displayWrite(osdDisplayPort, col, row, "FLIGHT TIME");
4635 uint16_t flySeconds = getFlightTime();
4636 uint16_t flyMinutes = flySeconds / 60;
4637 flySeconds %= 60;
4638 uint16_t flyHours = flyMinutes / 60;
4639 flyMinutes %= 60;
4640 tfp_sprintf(buff, ": %02u:%02u:%02u", flyHours, flyMinutes, flySeconds);
4641 displayWrite(osdDisplayPort, statValX, row++, buff);
4643 return row;
4646 uint8_t drawStat_FlightDistance(uint8_t col, uint8_t row, uint8_t statValX)
4648 char buff[12];
4650 displayWrite(osdDisplayPort, col, row, "FLIGHT DISTANCE");
4651 tfp_sprintf(buff, ": ");
4652 osdFormatDistanceStr(buff + 2, getTotalTravelDistance());
4653 displayWrite(osdDisplayPort, statValX, row++, buff);
4655 return row;
4658 uint8_t drawStat_MaxDistanceFromHome(uint8_t col, uint8_t row, uint8_t statValX)
4660 char buff[12];
4661 uint8_t valueXOffset = 0;
4662 if (!osdDisplayIsHD()) {
4663 displayWrite(osdDisplayPort, col, row, "DISTANCE FROM ");
4664 valueXOffset = 14;
4665 } else {
4666 displayWrite(osdDisplayPort, col, row, "MAX DISTANCE FROM ");
4667 valueXOffset = 18;
4669 displayWriteChar(osdDisplayPort, col + valueXOffset, row, SYM_HOME);
4670 tfp_sprintf(buff, ": ");
4671 osdFormatDistanceStr(buff + 2, stats.max_distance * 100);
4672 displayWrite(osdDisplayPort, statValX, row++, buff);
4674 return row;
4677 uint8_t drawStat_Speed(uint8_t col, uint8_t row, uint8_t statValX)
4679 char buff[12];
4680 char buff2[12];
4681 uint8_t multiValueXOffset = 0;
4683 displayWrite(osdDisplayPort, col, row, "MAX/AVG SPEED");
4685 osdFormatVelocityStr(buff2, stats.max_3D_speed, true, false);
4686 tfp_sprintf(buff, ": %s/", osdFormatTrimWhiteSpace(buff2));
4687 multiValueXOffset = strlen(buff);
4688 displayWrite(osdDisplayPort, statValX, row, buff);
4690 osdGenerateAverageVelocityStr(buff2);
4691 displayWrite(osdDisplayPort, statValX + multiValueXOffset, row++, osdFormatTrimWhiteSpace(buff2));
4693 return row;
4696 uint8_t drawStat_MaximumAltitude(uint8_t col, uint8_t row, uint8_t statValX)
4698 char buff[12];
4699 displayWrite(osdDisplayPort, col, row, "MAX ALTITUDE");
4700 tfp_sprintf(buff, ": ");
4701 osdFormatAltitudeStr(buff + 2, stats.max_altitude);
4702 displayWrite(osdDisplayPort, statValX, row++, buff);
4704 return row;
4707 uint8_t drawStat_BatteryVoltage(uint8_t col, uint8_t row, uint8_t statValX)
4709 char buff[12];
4710 uint8_t multiValueXOffset = 0;
4711 if (!osdDisplayIsHD())
4712 displayWrite(osdDisplayPort, col, row, "MIN VOLTS P/C");
4713 else
4714 displayWrite(osdDisplayPort, col, row, "MIN VOLTS PACK/CELL");
4716 // Pack voltage
4717 tfp_sprintf(buff, ": ");
4718 osdFormatCentiNumber(buff + 2, stats.min_voltage, 0, osdConfig()->main_voltage_decimals, 0, osdConfig()->main_voltage_decimals + 2, false);
4719 strcat(osdFormatTrimWhiteSpace(buff), "/");
4720 multiValueXOffset = strlen(buff);
4721 // AverageCell
4722 osdFormatCentiNumber(buff + multiValueXOffset, stats.min_voltage / getBatteryCellCount(), 0, 2, 0, 3, false);
4723 tfp_sprintf(buff + strlen(buff), "%c", SYM_VOLT);
4725 displayWrite(osdDisplayPort, statValX, row++, buff);
4727 return row;
4730 uint8_t drawStat_MaximumPowerAndCurrent(uint8_t col, uint8_t row, uint8_t statValX)
4732 char buff[12];
4733 char outBuff[12];
4734 tfp_sprintf(outBuff, ": ");
4735 osdFormatCentiNumber(buff, stats.max_current, 0, 2, 0, 3, false);
4736 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4737 strcat(outBuff, "/");
4738 bool kiloWatt = osdFormatCentiNumber(buff, stats.max_power, 1000, 2, 2, 3, false);
4739 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4740 displayWrite(osdDisplayPort, statValX, row, outBuff);
4742 if (kiloWatt)
4743 displayWrite(osdDisplayPort, col, row, "MAX AMPS/K WATTS");
4744 else
4745 displayWrite(osdDisplayPort, col, row, "MAX AMPS/WATTS");
4747 return ++row;
4750 uint8_t drawStat_UsedEnergy(uint8_t col, uint8_t row, uint8_t statValX)
4752 char buff[12];
4754 if (osdDisplayIsHD())
4755 displayWrite(osdDisplayPort, col, row, "USED ENERGY FLT/TOT");
4756 else
4757 displayWrite(osdDisplayPort, col, row, "USED ENERGY F/T");
4758 tfp_sprintf(buff, ": ");
4759 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4760 tfp_sprintf(buff + 2, "%d/%d%c", (int)(getMAhDrawn() - stats.flightStartMAh), (int)getMAhDrawn(), SYM_MAH);
4761 } else {
4762 char preBuff[12];
4763 osdFormatCentiNumber(preBuff, (getMWhDrawn() - stats.flightStartMWh) / 10, 0, 2, 0, 3, false);
4764 strcat(buff, osdFormatTrimWhiteSpace(preBuff));
4765 strcat(buff, "/");
4766 osdFormatCentiNumber(preBuff, getMWhDrawn() / 10, 0, 2, 0, 3, false);
4767 strcat(buff, osdFormatTrimWhiteSpace(preBuff));
4768 tfp_sprintf(buff + strlen(buff), "%s%c", buff, SYM_WH);
4770 displayWrite(osdDisplayPort, statValX, row++, buff);
4772 return row;
4775 uint8_t drawStat_AverageEfficiency(uint8_t col, uint8_t row, uint8_t statValX, bool forceMetric)
4777 char buff[15];
4778 char outBuff[15];
4779 int32_t totalDistance = getTotalTravelDistance();
4780 bool moreThanAh = false;
4781 bool efficiencyValid = totalDistance >= 10000;
4783 if (osdDisplayIsHD())
4784 displayWrite(osdDisplayPort, col, row, "AVG EFFICIENCY FLT/TOT");
4785 else
4786 displayWrite(osdDisplayPort, col, row, "AV EFFICIENCY F/T");
4788 tfp_sprintf(outBuff, ": ");
4789 uint8_t digits = 3U; // Total number of digits (including decimal point)
4790 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
4791 if (isDJICompatibleVideoSystem(osdConfig())) {
4792 // Add one digit so no switch to scaled decimal occurs above 99
4793 digits = 4U;
4795 #endif
4796 if (!forceMetric) {
4797 switch (osdConfig()->units) {
4798 case OSD_UNIT_UK:
4799 FALLTHROUGH;
4800 case OSD_UNIT_IMPERIAL:
4801 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4802 if (efficiencyValid) {
4803 moreThanAh = osdFormatCentiNumber(buff, (int32_t)((getMAhDrawn() - stats.flightStartMAh) * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, digits, false);
4804 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4805 if (osdDisplayIsHD()) {
4806 if (!moreThanAh)
4807 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_MI_0, SYM_MAH_MI_1);
4808 else
4809 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_MI);
4811 moreThanAh = false;
4814 strcat(outBuff, "/");
4815 moreThanAh = moreThanAh || osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, digits, false);
4816 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4818 if (!moreThanAh)
4819 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_MI_0, SYM_MAH_MI_1);
4820 else
4821 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_MI);
4822 } else {
4823 tfp_sprintf(outBuff + strlen(outBuff), "---/---%c%c", SYM_MAH_MI_0, SYM_MAH_MI_1);
4825 } else {
4826 if (efficiencyValid) {
4827 osdFormatCentiNumber(buff, (int32_t)((getMWhDrawn() - stats.flightStartMWh) * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, digits, false);
4828 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4829 strcat(outBuff, "/");
4830 osdFormatCentiNumber(buff + strlen(buff), (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, digits, false);
4831 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4832 } else {
4833 strcat(outBuff, "---/---");
4835 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_WH_MI);
4837 break;
4838 case OSD_UNIT_GA:
4839 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4840 if (efficiencyValid) {
4841 moreThanAh = osdFormatCentiNumber(buff, (int32_t)((getMAhDrawn()-stats.flightStartMAh) * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, digits, false);
4842 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4843 if (osdDisplayIsHD()) {
4844 if (!moreThanAh)
4845 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_NM_0, SYM_MAH_NM_1);
4846 else
4847 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_NM);
4849 moreThanAh = false;
4852 strcat(outBuff, "/");
4853 moreThanAh = moreThanAh || osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, digits, false);
4854 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4855 if (!moreThanAh) {
4856 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_NM_0, SYM_MAH_NM_1);
4857 } else {
4858 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_NM);
4860 } else {
4861 tfp_sprintf(outBuff + strlen(outBuff), "---/---%c%c", SYM_MAH_NM_0, SYM_MAH_NM_1);
4863 } else {
4864 if (efficiencyValid) {
4865 osdFormatCentiNumber(buff, (int32_t)((getMWhDrawn()-stats.flightStartMWh) * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, digits, false);
4866 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4867 strcat(outBuff, "/");
4868 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, digits, false);
4869 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4870 } else {
4871 strcat(outBuff, "---/---");
4873 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_WH_NM);
4875 break;
4876 case OSD_UNIT_METRIC_MPH:
4877 case OSD_UNIT_METRIC:
4878 forceMetric = true;
4879 break;
4883 if (forceMetric) {
4884 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4885 if (efficiencyValid) {
4886 moreThanAh = osdFormatCentiNumber(buff, (int32_t)((getMAhDrawn() - stats.flightStartMAh) * 10000000.0f / totalDistance), 1000, 0, 2, digits, false);
4887 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4888 if (osdDisplayIsHD()) {
4889 if (!moreThanAh)
4890 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_KM_0, SYM_MAH_KM_1);
4891 else
4892 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_KM);
4894 moreThanAh = false;
4897 strcat(outBuff, "/");
4898 moreThanAh = moreThanAh || osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000000.0f / totalDistance), 1000, 0, 2, digits, false);
4899 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4900 if (!moreThanAh) {
4901 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_KM_0, SYM_MAH_KM_1);
4902 } else {
4903 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_KM);
4905 } else {
4906 tfp_sprintf(outBuff + strlen(outBuff), "---/---%c%c", SYM_MAH_KM_0, SYM_MAH_KM_1);
4908 } else {
4909 if (efficiencyValid) {
4910 osdFormatCentiNumber(buff, (int32_t)((getMWhDrawn() - stats.flightStartMWh) * 10000.0f / totalDistance), 0, 2, 0, digits, false);
4911 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4912 strcat(outBuff, "/");
4913 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10000.0f / totalDistance), 0, 2, 0, digits, false);
4914 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4915 } else {
4916 strcat(outBuff, "---/---");
4918 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_WH_KM);
4922 tfp_sprintf(outBuff + strlen(outBuff), "%c", '\0');
4923 displayWrite(osdDisplayPort, statValX, row++, outBuff);
4925 return row;
4928 uint8_t drawStat_RXStats(uint8_t col, uint8_t row, uint8_t statValX)
4930 char buff[20];
4931 uint8_t multiValueXOffset = 0;
4933 tfp_sprintf(buff, "MIN RSSI");
4934 if (rxConfig()->serialrx_provider == SERIALRX_CRSF) {
4935 strcat(buff, "/LQ");
4937 if (osdDisplayIsHD())
4938 strcat(buff, "/DBM");
4940 displayWrite(osdDisplayPort, col, row, buff);
4942 memset(buff, '\0', strlen(buff));
4943 tfp_sprintf(buff, ": ");
4944 itoa(stats.min_rssi, buff + 2, 10);
4945 strcat(osdFormatTrimWhiteSpace(buff), "%");
4947 if (rxConfig()->serialrx_provider == SERIALRX_CRSF) {
4948 strcat(osdFormatTrimWhiteSpace(buff), "/");
4949 multiValueXOffset = strlen(buff);
4950 itoa(stats.min_lq, buff + multiValueXOffset, 10);
4951 strcat(osdFormatTrimWhiteSpace(buff), "%");
4953 if (osdDisplayIsHD()) {
4954 strcat(osdFormatTrimWhiteSpace(buff), "/");
4955 itoa(stats.min_rssi_dbm, buff + 2, 10);
4956 tfp_sprintf(buff + strlen(buff), "%c", SYM_DBM);
4957 displayWrite(osdDisplayPort, statValX, row++, buff);
4961 displayWrite(osdDisplayPort, statValX, row++, buff);
4963 if (!osdDisplayIsHD() && rxConfig()->serialrx_provider == SERIALRX_CRSF) {
4964 displayWrite(osdDisplayPort, col, row, "MIN RX DBM");
4965 memset(buff, '\0', strlen(buff));
4966 tfp_sprintf(buff, ": ");
4967 itoa(stats.min_rssi_dbm, buff + 2, 10);
4968 tfp_sprintf(buff + strlen(buff), "%c", SYM_DBM);
4969 displayWrite(osdDisplayPort, statValX, row++, buff);
4972 return row;
4975 uint8_t drawStat_GPS(uint8_t col, uint8_t row, uint8_t statValX)
4977 char buff[12];
4978 displayWrite(osdDisplayPort, col, row, "MIN/MAX GPS SATS");
4979 tfp_sprintf(buff, ": %u/%u", stats.min_sats, stats.max_sats);
4980 displayWrite(osdDisplayPort, statValX, row++, buff);
4982 return row;
4985 uint8_t drawStat_ESCTemperature(uint8_t col, uint8_t row, uint8_t statValX)
4987 char buff[12];
4988 displayWrite(osdDisplayPort, col, row, "MIN/MAX ESC TEMP");
4989 tfp_sprintf(buff, ": %3d/%3d%c",
4990 ((osdConfig()->units == OSD_UNIT_IMPERIAL) ? (int16_t)(stats.min_esc_temp * 9 / 5.0f + 320) : stats.min_esc_temp),
4991 ((osdConfig()->units == OSD_UNIT_IMPERIAL) ? (int16_t)(stats.max_esc_temp * 9 / 5.0f + 320) : stats.max_esc_temp),
4992 ((osdConfig()->units == OSD_UNIT_IMPERIAL) ? SYM_TEMP_F : SYM_TEMP_C));
4993 displayWrite(osdDisplayPort, statValX, row++, buff);
4995 return row;
4998 uint8_t drawStat_GForce(uint8_t col, uint8_t row, uint8_t statValX)
5000 char buff[12];
5001 char outBuff[12];
5003 const float max_gforce = accGetMeasuredMaxG();
5004 const acc_extremes_t *acc_extremes = accGetMeasuredExtremes();
5005 const float acc_extremes_min = acc_extremes[Z].min;
5006 const float acc_extremes_max = acc_extremes[Z].max;
5008 if (!osdDisplayIsHD())
5009 displayWrite(osdDisplayPort, col, row, "MAX G-FORCE");
5010 else
5011 displayWrite(osdDisplayPort, col, row, "MAX/MIN Z/MAX Z G-FORCE");
5013 tfp_sprintf(outBuff, ": ");
5014 osdFormatCentiNumber(buff, max_gforce * 100, 0, 2, 0, 3, false);
5016 if (!osdDisplayIsHD()) {
5017 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
5018 displayWrite(osdDisplayPort, statValX, row++, outBuff);
5020 displayWrite(osdDisplayPort, col, row, "MIN/MAX Z G-FORCE");
5021 memset(outBuff, '\0', strlen(outBuff));
5022 tfp_sprintf(outBuff, ": ");
5023 } else {
5024 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
5025 strcat(outBuff, "/");
5027 osdFormatCentiNumber(buff, acc_extremes_min * 100, 0, 2, 0, 4, false);
5028 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
5029 strcat(outBuff, "/");
5031 osdFormatCentiNumber(buff, acc_extremes_max * 100, 0, 2, 0, 3, false);
5032 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
5033 displayWrite(osdDisplayPort, statValX, row++, outBuff);
5035 return row;
5038 uint8_t drawStat_DisarmMethod(uint8_t col, uint8_t row, uint8_t statValX)
5040 // We keep "" for backward compatibility with the Blackbox explorer and other potential usages
5041 const char * disarmReasonStr[DISARM_REASON_COUNT] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "", "FAILSAFE", "NAV SYS", "LANDING"};
5043 displayWrite(osdDisplayPort, col, row, "DISARMED BY");
5044 displayWrite(osdDisplayPort, statValX, row, ": ");
5045 displayWrite(osdDisplayPort, statValX + 2, row++, disarmReasonStr[getDisarmReason()]);
5047 return row;
5050 static void osdShowStats(bool isSinglePageStatsCompatible, uint8_t page)
5052 const char * statsHeader[2] = {"*** STATS 1/2 -> ***", "*** STATS <- 2/2 ***"};
5053 uint8_t row = 1; // Start one line down leaving space at the top of the screen.
5055 const uint8_t statNameX = (osdDisplayPort->cols - (osdDisplayIsHD() ? 41 : 28)) / 2;
5056 const uint8_t statValuesX = osdDisplayPort->cols - statNameX - (osdDisplayIsHD() ? 15 : 11);
5058 if (page > 1)
5059 page = 0;
5061 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
5062 displayClearScreen(osdDisplayPort);
5064 if (isSinglePageStatsCompatible) {
5065 char buff[25];
5066 tfp_sprintf(buff, "*** STATS ");
5067 #ifdef USE_BLACKBOX
5068 #ifdef USE_SDCARD
5069 if (feature(FEATURE_BLACKBOX)) {
5070 int32_t logNumber = blackboxGetLogNumber();
5071 if (logNumber >= 0)
5072 tfp_sprintf(buff + strlen(buff), " %c%05" PRId32 " ", SYM_BLACKBOX, logNumber);
5073 else
5074 tfp_sprintf(buff + strlen(buff), " %c ", SYM_BLACKBOX);
5076 #endif
5077 #endif
5078 strcat(buff, "***");
5080 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buff)) / 2, row++, buff);
5081 } else
5082 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(statsHeader[page + 1])) / 2, row++, statsHeader[page]);
5084 if (isSinglePageStatsCompatible) {
5085 // Top 15 rows for most important stats. Max 19 rows (WTF)
5086 row = drawStat_FlightTime(statNameX, row, statValuesX); // 1 row
5087 row = drawStat_FlightDistance(statNameX, row, statValuesX); // 1 row
5088 if (feature(FEATURE_GPS)) row = drawStat_MaxDistanceFromHome(statNameX, row, statValuesX); // 1 row
5089 if (feature(FEATURE_GPS)) row = drawStat_Speed(statNameX, row, statValuesX); // 1 row
5090 row = drawStat_MaximumAltitude(statNameX, row, statValuesX); // 1 row
5091 row = drawStat_BatteryVoltage(statNameX, row, statValuesX); // 1 row
5092 if (feature(FEATURE_CURRENT_METER)) row = drawStat_MaximumPowerAndCurrent(statNameX, row, statValuesX); // 1 row
5093 if (feature(FEATURE_CURRENT_METER)) row = drawStat_UsedEnergy(statNameX, row, statValuesX); // 1 row
5094 if (feature(FEATURE_CURRENT_METER) && feature(FEATURE_GPS)) row = drawStat_AverageEfficiency(statNameX, row, statValuesX, false); // 1 row
5095 if (osdConfig()->stats_show_metric_efficiency && osdIsNotMetric() && feature(FEATURE_CURRENT_METER) && feature(FEATURE_GPS)) row = drawStat_AverageEfficiency(statNameX, row, statValuesX, true); // 1 row
5096 row = drawStat_RXStats(statNameX, row, statValuesX); // 1 row if non-CRSF else 2 rows
5097 if (feature(FEATURE_GPS)) row = drawStat_GPS(statNameX, row, statValuesX); // 1 row
5098 if (STATE(ESC_SENSOR_ENABLED)) row = drawStat_ESCTemperature(statNameX, row, statValuesX); // 1 row
5100 // Draw these if there is space space
5101 if (row < (osdDisplayPort->cols-3)) row = drawStat_GForce(statNameX, row, statValuesX); // 1 row HD or 2 rows SD
5102 #ifdef USE_STATS
5103 if (row < (osdDisplayPort->cols-7) && statsConfig()->stats_enabled) row = drawStat_Stats(statNameX, row, statValuesX, false); // 4 rows
5104 #endif
5105 } else {
5106 switch (page) {
5107 case 0:
5108 // Max 10 rows
5109 row = drawStat_FlightTime(statNameX, row, statValuesX); // 1 row
5110 row = drawStat_FlightDistance(statNameX, row, statValuesX); // 1 row
5111 if (feature(FEATURE_GPS)) row = drawStat_MaxDistanceFromHome(statNameX, row, statValuesX); // 1 row
5112 if (feature(FEATURE_GPS)) row = drawStat_Speed(statNameX, row, statValuesX); // 1 row
5113 row = drawStat_MaximumAltitude(statNameX, row, statValuesX); // 1 row
5114 row = drawStat_BatteryVoltage(statNameX, row, statValuesX); // 1 row
5115 if (feature(FEATURE_CURRENT_METER)) row = drawStat_MaximumPowerAndCurrent(statNameX, row, statValuesX); // 1 row
5116 if (feature(FEATURE_CURRENT_METER))row = drawStat_UsedEnergy(statNameX, row, statValuesX); // 1 row
5117 if (feature(FEATURE_CURRENT_METER) && feature(FEATURE_GPS)) row = drawStat_AverageEfficiency(statNameX, row, statValuesX, false); // 1 row
5118 if (feature(FEATURE_GPS))row = drawStat_GPS(statNameX, row, statValuesX); // 1 row
5119 break;
5120 case 1:
5121 // Max 10 rows
5122 row = drawStat_RXStats(statNameX, row, statValuesX); // 1 row if non-CRSF else 2 rows
5123 if (STATE(ESC_SENSOR_ENABLED)) row = drawStat_ESCTemperature(statNameX, row, statValuesX); // 1 row
5124 row = drawStat_GForce(statNameX, row, statValuesX); // 1 row HD or 2 rows SD
5125 if (osdConfig()->stats_show_metric_efficiency && osdIsNotMetric() && feature(FEATURE_CURRENT_METER) && feature(FEATURE_GPS)) row = drawStat_AverageEfficiency(statNameX, row, statValuesX, true); // 1 row
5126 #ifdef USE_BLACKBOX
5127 #ifdef USE_SDCARD
5128 if (feature(FEATURE_BLACKBOX)) {
5129 char buff[12];
5130 displayWrite(osdDisplayPort, statNameX, row, "BLACKBOX FILE");
5132 tfp_sprintf(buff, ": %u/%u", stats.min_sats, stats.max_sats);
5134 int32_t logNumber = blackboxGetLogNumber();
5135 if (logNumber >= 0)
5136 tfp_sprintf(buff, ": %05ld ", logNumber);
5137 else
5138 strcat(buff, ": INVALID");
5140 displayWrite(osdDisplayPort, statValuesX, row++, buff); // 1 row
5142 #endif
5143 #endif
5144 #ifdef USE_STATS
5145 if (row < (osdDisplayPort->cols-7) && statsConfig()->stats_enabled) row = drawStat_Stats(statNameX, row, statValuesX, false); // 4 rows
5146 #endif
5148 break;
5152 row = drawStat_DisarmMethod(statNameX, row, statValuesX);
5154 // The following has been commented out as it will be added in #9688
5155 // uint16_t rearmMs = (emergInflightRearmEnabled()) ? emergencyInFlightRearmTimeMS() : 0;
5157 if (savingSettings == true) {
5158 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS))) / 2, row++, OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS));
5159 /*} else if (rearmMs > 0) { // Show rearming time if settings not actively being saved. Ignore the settings saved message if rearm available.
5160 char emReArmMsg[23];
5161 tfp_sprintf(emReArmMsg, "** REARM PERIOD: ");
5162 tfp_sprintf(emReArmMsg + strlen(emReArmMsg), "%02d", (uint8_t)MS2S(rearmMs));
5163 strcat(emReArmMsg, " **\0");
5164 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(emReArmMsg));*/
5165 } else if (notify_settings_saved > 0) {
5166 if (millis() > notify_settings_saved) {
5167 notify_settings_saved = 0;
5168 } else {
5169 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED))) / 2, row++, OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED));
5173 displayCommitTransaction(osdDisplayPort);
5176 // HD arming screen. based on the minimum HD OSD grid size of 50 x 18
5177 static void osdShowHDArmScreen(void)
5179 dateTime_t dt;
5180 char buf[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
5181 char buf2[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
5182 char craftNameBuf[MAX_NAME_LENGTH];
5183 char versionBuf[osdDisplayPort->cols];
5184 uint8_t safehomeRow = 0;
5185 uint8_t armScreenRow = 2; // Start at row 2
5187 armScreenRow = drawLogos(false, armScreenRow);
5188 armScreenRow++;
5190 if (strlen(systemConfig()->craftName) > 0) {
5191 osdFormatCraftName(craftNameBuf);
5192 strcpy(buf2, "ARMED!");
5193 tfp_sprintf(buf, "%s - %s", craftNameBuf, buf2);
5194 } else {
5195 strcpy(buf, "ARMED!");
5197 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5198 #if defined(USE_GPS)
5199 #if defined (USE_SAFE_HOME)
5200 if (posControl.safehomeState.distance) {
5201 safehomeRow = armScreenRow;
5202 armScreenRow++;
5204 #endif // USE_SAFE_HOME
5205 #endif // USE_GPS
5206 armScreenRow++;
5208 if (posControl.waypointListValid && posControl.waypointCount > 0) {
5209 #ifdef USE_MULTI_MISSION
5210 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
5211 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5212 #else
5213 strcpy(buf, "*MISSION LOADED*");
5214 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5215 #endif
5217 armScreenRow++;
5219 #if defined(USE_GPS)
5220 if (feature(FEATURE_GPS)) {
5221 if (STATE(GPS_FIX_HOME)) {
5222 if (osdConfig()->osd_home_position_arm_screen){
5223 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
5224 osdFormatCoordinate(buf2, SYM_LON, GPS_home.lon);
5225 uint8_t gap = 1;
5226 uint8_t col = strlen(buf) + strlen(buf2) + gap;
5228 if ((osdDisplayPort->cols %2) != (col %2)) {
5229 gap++;
5230 col++;
5233 col = (osdDisplayPort->cols - col) / 2;
5235 displayWrite(osdDisplayPort, col, armScreenRow, buf);
5236 displayWrite(osdDisplayPort, col + strlen(buf) + gap, armScreenRow++, buf2);
5238 int digits = osdConfig()->plus_code_digits;
5239 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
5240 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5243 #if defined (USE_SAFE_HOME)
5244 if (posControl.safehomeState.distance) { // safehome found during arming
5245 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
5246 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
5247 } else {
5248 osdFormatDistanceStr(buf2, posControl.safehomeState.distance);
5249 tfp_sprintf(buf, "%c SAFEHOME %u @ %s", SYM_HOME, posControl.safehomeState.index, buf2);
5251 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
5252 // write this message below the ARMED message to make it obvious
5253 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, safehomeRow, buf, elemAttr);
5255 #endif
5256 } else {
5257 strcpy(buf, "!NO HOME POSITION!");
5258 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5260 armScreenRow++;
5262 #endif
5264 if (rtcGetDateTimeLocal(&dt)) {
5265 tfp_sprintf(buf, "%04u-%02u-%02u %02u:%02u:%02u", dt.year, dt.month, dt.day, dt.hours, dt.minutes, dt.seconds);
5266 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5267 armScreenRow++;
5270 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
5271 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, armScreenRow++, versionBuf);
5272 armScreenRow++;
5274 #ifdef USE_STATS
5275 if (armScreenRow < (osdDisplayPort->rows - 4))
5276 armScreenRow = drawStats(armScreenRow);
5277 #endif // USE_STATS
5280 static void osdShowSDArmScreen(void)
5282 dateTime_t dt;
5283 char buf[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
5284 char buf2[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
5285 char craftNameBuf[MAX_NAME_LENGTH];
5286 char versionBuf[osdDisplayPort->cols];
5287 // We need 12 visible rows, start row never < first fully visible row 1
5288 uint8_t armScreenRow = osdDisplayPort->rows > 13 ? (osdDisplayPort->rows - 12) / 2 : 1;
5289 uint8_t safehomeRow = 0;
5291 strcpy(buf, "ARMED!");
5292 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5293 safehomeRow = armScreenRow;
5294 armScreenRow++;
5296 if (strlen(systemConfig()->craftName) > 0) {
5297 osdFormatCraftName(craftNameBuf);
5298 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(systemConfig()->craftName)) / 2, armScreenRow++, craftNameBuf );
5301 if (posControl.waypointListValid && posControl.waypointCount > 0) {
5302 #ifdef USE_MULTI_MISSION
5303 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
5304 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow, buf);
5305 #else
5306 strcpy(buf, "*MISSION LOADED*");
5307 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow, buf);
5308 #endif
5310 armScreenRow++;
5312 #if defined(USE_GPS)
5313 if (feature(FEATURE_GPS)) {
5314 if (STATE(GPS_FIX_HOME)) {
5315 if (osdConfig()->osd_home_position_arm_screen) {
5316 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
5317 osdFormatCoordinate(buf2, SYM_LON, GPS_home.lon);
5319 uint8_t gpsStartCol = (osdDisplayPort->cols - (strlen(buf) + strlen(buf2) + 2)) / 2;
5320 displayWrite(osdDisplayPort, gpsStartCol, armScreenRow, buf);
5321 displayWrite(osdDisplayPort, gpsStartCol + strlen(buf) + 2, armScreenRow++, buf2);
5323 int digits = osdConfig()->plus_code_digits;
5324 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
5325 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5328 #if defined (USE_SAFE_HOME)
5329 if (posControl.safehomeState.distance) { // safehome found during arming
5330 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
5331 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
5332 } else {
5333 osdFormatDistanceStr(buf2, posControl.safehomeState.distance);
5334 tfp_sprintf(buf, "%c SAFEHOME %u @ %s", SYM_HOME, posControl.safehomeState.index, buf2);
5336 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
5337 // write this message below the ARMED message to make it obvious
5338 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, safehomeRow, buf, elemAttr);
5340 #endif
5341 } else {
5342 strcpy(buf, "!NO HOME POSITION!");
5343 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5345 armScreenRow++;
5347 #endif
5349 if (rtcGetDateTimeLocal(&dt)) {
5350 tfp_sprintf(buf, "%04u-%02u-%02u %02u:%02u:%02u", dt.year, dt.month, dt.day, dt.hours, dt.minutes, dt.seconds);
5351 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5352 armScreenRow++;
5355 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
5356 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, armScreenRow++, versionBuf);
5357 armScreenRow++;
5359 #ifdef USE_STATS
5360 if (armScreenRow < (osdDisplayPort->rows - 4))
5361 armScreenRow = drawStats(armScreenRow);
5362 #endif // USE_STATS
5365 // called when motors armed
5366 static void osdShowArmed(void)
5368 displayClearScreen(osdDisplayPort);
5370 if (osdDisplayIsHD()) {
5371 osdShowHDArmScreen();
5372 } else {
5373 osdShowSDArmScreen();
5377 static void osdFilterData(timeUs_t currentTimeUs) {
5378 static timeUs_t lastRefresh = 0;
5379 float refresh_dT = US2S(cmpTimeUs(currentTimeUs, lastRefresh));
5381 GForce = fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF)) / GRAVITY_MSS;
5382 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) GForceAxis[axis] = imuMeasuredAccelBF.v[axis] / GRAVITY_MSS;
5384 if (lastRefresh) {
5385 GForce = pt1FilterApply3(&GForceFilter, GForce, refresh_dT);
5386 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) pt1FilterApply3(GForceFilterAxis + axis, GForceAxis[axis], refresh_dT);
5387 } else {
5388 pt1FilterInitRC(&GForceFilter, GFORCE_FILTER_TC, 0);
5389 pt1FilterReset(&GForceFilter, GForce);
5391 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) {
5392 pt1FilterInitRC(GForceFilterAxis + axis, GFORCE_FILTER_TC, 0);
5393 pt1FilterReset(GForceFilterAxis + axis, GForceAxis[axis]);
5397 lastRefresh = currentTimeUs;
5400 // Detect when the user is holding the roll stick to the right
5401 static bool osdIsPageUpStickCommandHeld(void)
5403 static int pageUpHoldCount = 1;
5405 bool keyHeld = false;
5407 if (IS_HI(ROLL)) {
5408 keyHeld = true;
5411 if (!keyHeld) {
5412 pageUpHoldCount = 1;
5413 } else {
5414 ++pageUpHoldCount;
5417 if (pageUpHoldCount > 20) {
5418 pageUpHoldCount = 1;
5419 return true;
5422 return false;
5425 // Detect when the user is holding the roll stick to the left
5426 static bool osdIsPageDownStickCommandHeld(void)
5428 static int pageDownHoldCount = 1;
5430 bool keyHeld = false;
5431 if (IS_LO(ROLL)) {
5432 keyHeld = true;
5435 if (!keyHeld) {
5436 pageDownHoldCount = 1;
5437 } else {
5438 ++pageDownHoldCount;
5441 if (pageDownHoldCount > 20) {
5442 pageDownHoldCount = 1;
5443 return true;
5446 return false;
5449 static void osdRefresh(timeUs_t currentTimeUs)
5451 osdFilterData(currentTimeUs);
5453 #ifdef USE_CMS
5454 if (IS_RC_MODE_ACTIVE(BOXOSD) && (!cmsInMenu) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
5455 #else
5456 if (IS_RC_MODE_ACTIVE(BOXOSD) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
5457 #endif
5458 displayClearScreen(osdDisplayPort);
5459 armState = ARMING_FLAG(ARMED);
5460 return;
5463 bool statsSinglePageCompatible = (osdDisplayPort->rows >= OSD_STATS_SINGLE_PAGE_MIN_ROWS);
5464 static uint8_t statsCurrentPage = 0;
5465 static bool statsDisplayed = false;
5466 static bool statsAutoPagingEnabled = true;
5467 static bool isThrottleHigh = false;
5469 // Detect arm/disarm
5470 if (armState != ARMING_FLAG(ARMED)) {
5471 if (ARMING_FLAG(ARMED)) {
5472 // Display the "Arming" screen
5473 statsDisplayed = false;
5474 if (!STATE(IN_FLIGHT_EMERG_REARM))
5475 osdResetStats();
5477 osdShowArmed();
5478 uint16_t delay = osdConfig()->arm_screen_display_time;
5479 if (STATE(IN_FLIGHT_EMERG_REARM))
5480 delay = 500;
5481 #if defined(USE_SAFE_HOME)
5482 else if (posControl.safehomeState.distance)
5483 delay += 3000;
5484 #endif
5485 osdSetNextRefreshIn(delay);
5486 } else {
5487 // Display the "Stats" screen
5488 statsDisplayed = true;
5489 statsCurrentPage = 0;
5490 statsAutoPagingEnabled = osdConfig()->stats_page_auto_swap_time > 0 ? true : false;
5491 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
5492 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME);
5493 isThrottleHigh = checkStickPosition(THR_HI);
5496 armState = ARMING_FLAG(ARMED);
5499 // This block is entered when we're showing the "Splash", "Armed" or "Stats" screens
5500 if (resumeRefreshAt) {
5502 // Handle events only when the "Stats" screen is being displayed.
5503 if (statsDisplayed) {
5505 // Manual paging stick commands are only applicable to multi-page stats.
5506 // ******************************
5507 // For single-page stats, this effectively disables the ability to cancel the
5508 // automatic paging/updates with the stick commands. So unless stats_page_auto_swap_time
5509 // is set to 0 or greater than 4 (saved settings display interval is 5 seconds), then
5510 // "Saved Settings" should display if it is active within the refresh interval.
5511 // ******************************
5512 // With multi-page stats, "Saved Settings" could also be missed if the user
5513 // has canceled automatic paging using the stick commands, because that is only
5514 // updated when osdShowStats() is called. So, in that case, they would only see
5515 // the "Saved Settings" message if they happen to manually change pages using the
5516 // stick commands within the interval the message is displayed.
5517 bool manualPageUpRequested = false;
5518 bool manualPageDownRequested = false;
5519 if (!statsSinglePageCompatible) {
5520 // These methods ensure the paging stick commands are held for a brief period
5521 // Otherwise it can result in a race condition where the stats are
5522 // updated too quickly and can result in partial blanks, etc.
5523 if (osdIsPageUpStickCommandHeld()) {
5524 manualPageUpRequested = true;
5525 statsAutoPagingEnabled = false;
5526 } else if (osdIsPageDownStickCommandHeld()) {
5527 manualPageDownRequested = true;
5528 statsAutoPagingEnabled = false;
5532 if (statsAutoPagingEnabled) {
5533 // Alternate screens for multi-page stats.
5534 // Also, refreshes screen at swap interval for single-page stats.
5535 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time * 1000), 2)) {
5536 if (statsCurrentPage == 0) {
5537 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
5538 statsCurrentPage = 1;
5540 } else {
5541 if (statsCurrentPage == 1) {
5542 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
5543 statsCurrentPage = 0;
5546 } else {
5547 // Process manual page change events for multi-page stats.
5548 if (manualPageUpRequested) {
5549 osdShowStats(statsSinglePageCompatible, 1);
5550 statsCurrentPage = 1;
5551 } else if (manualPageDownRequested) {
5552 osdShowStats(statsSinglePageCompatible, 0);
5553 statsCurrentPage = 0;
5558 // Handle events when either "Splash", "Armed" or "Stats" screens are displayed.
5559 if (currentTimeUs > resumeRefreshAt || (OSD_RESUME_UPDATES_STICK_COMMAND && !isThrottleHigh)) {
5560 // Time elapsed or canceled by stick commands.
5561 // Exit to normal OSD operation.
5562 displayClearScreen(osdDisplayPort);
5563 resumeRefreshAt = 0;
5564 statsDisplayed = false;
5565 } else {
5566 // Continue "Splash", "Armed" or "Stats" screens.
5567 displayHeartbeat(osdDisplayPort);
5568 isThrottleHigh = checkStickPosition(THR_HI);
5571 return;
5574 #ifdef USE_CMS
5575 if (!displayIsGrabbed(osdDisplayPort)) {
5576 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
5577 if (fullRedraw) {
5578 displayClearScreen(osdDisplayPort);
5579 fullRedraw = false;
5581 osdDrawNextElement();
5582 displayHeartbeat(osdDisplayPort);
5583 displayCommitTransaction(osdDisplayPort);
5584 #ifdef OSD_CALLS_CMS
5585 } else {
5586 cmsUpdate(currentTimeUs);
5587 #endif
5589 #endif
5593 * Called periodically by the scheduler
5595 void osdUpdate(timeUs_t currentTimeUs)
5597 static uint32_t counter = 0;
5599 // don't touch buffers if DMA transaction is in progress
5600 if (displayIsTransferInProgress(osdDisplayPort)) {
5601 return;
5604 if (!osdDisplayIsReady) {
5605 osdCompleteAsyncInitialization();
5606 return;
5609 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
5610 // Check if the layout has changed. Higher numbered
5611 // boxes take priority.
5612 unsigned activeLayout;
5613 if (layoutOverride >= 0) {
5614 activeLayout = layoutOverride;
5615 // Check for timed override, it will go into effect on
5616 // the next OSD iteration
5617 if (layoutOverrideUntil > 0 && millis() > layoutOverrideUntil) {
5618 layoutOverrideUntil = 0;
5619 layoutOverride = -1;
5621 } else if (osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE)) {
5622 activeLayout = 0;
5623 } else {
5624 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
5625 if (IS_RC_MODE_ACTIVE(BOXOSDALT3))
5626 activeLayout = 3;
5627 else
5628 #endif
5629 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
5630 if (IS_RC_MODE_ACTIVE(BOXOSDALT2))
5631 activeLayout = 2;
5632 else
5633 #endif
5634 if (IS_RC_MODE_ACTIVE(BOXOSDALT1))
5635 activeLayout = 1;
5636 else
5637 #ifdef USE_PROGRAMMING_FRAMEWORK
5638 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT))
5639 activeLayout = constrain(logicConditionValuesByType[LOGIC_CONDITION_SET_OSD_LAYOUT], 0, OSD_ALTERNATE_LAYOUT_COUNT);
5640 else
5641 #endif
5642 activeLayout = 0;
5644 if (currentLayout != activeLayout) {
5645 currentLayout = activeLayout;
5646 osdStartFullRedraw();
5648 #endif
5650 #define DRAW_FREQ_DENOM 4
5651 #define STATS_FREQ_DENOM 50
5652 counter++;
5654 if ((counter % STATS_FREQ_DENOM) == 0 && ARMING_FLAG(ARMED)) {
5655 osdUpdateStats();
5658 if ((counter % DRAW_FREQ_DENOM) == 0) {
5659 // redraw values in buffer
5660 osdRefresh(currentTimeUs);
5661 } else {
5662 // rest of time redraw screen
5663 displayDrawScreen(osdDisplayPort);
5666 #ifdef USE_CMS
5667 // do not allow ARM if we are in menu
5668 if (displayIsGrabbed(osdDisplayPort)) {
5669 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
5670 } else {
5671 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
5673 #endif
5676 void osdStartFullRedraw(void)
5678 fullRedraw = true;
5681 void osdOverrideLayout(int layout, timeMs_t duration)
5683 layoutOverride = constrain(layout, -1, ARRAYLEN(osdLayoutsConfig()->item_pos) - 1);
5684 if (layoutOverride >= 0 && duration > 0) {
5685 layoutOverrideUntil = millis() + duration;
5686 } else {
5687 layoutOverrideUntil = 0;
5691 int osdGetActiveLayout(bool *overridden)
5693 if (overridden) {
5694 *overridden = layoutOverride >= 0;
5696 return currentLayout;
5699 bool osdItemIsFixed(osd_items_e item)
5701 return item == OSD_CROSSHAIRS ||
5702 item == OSD_ARTIFICIAL_HORIZON ||
5703 item == OSD_HORIZON_SIDEBARS;
5706 displayPort_t *osdGetDisplayPort(void)
5708 return osdDisplayPort;
5711 displayCanvas_t *osdGetDisplayPortCanvas(void)
5713 #if defined(USE_CANVAS)
5714 if (osdDisplayHasCanvas) {
5715 return &osdCanvas;
5717 #endif
5718 return NULL;
5721 timeMs_t systemMessageCycleTime(unsigned messageCount, const char **messages){
5722 uint8_t i = 0;
5723 float factor = 1.0f;
5724 while (i < messageCount) {
5725 if ((float)strlen(messages[i]) / 15.0f > factor) {
5726 factor = (float)strlen(messages[i]) / 15.0f;
5728 i++;
5730 return osdConfig()->system_msg_display_time * factor;
5733 textAttributes_t osdGetSystemMessage(char *buff, size_t buff_size, bool isCenteredText)
5735 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
5737 if (buff != NULL) {
5738 const char *message = NULL;
5739 /* WARNING: messageBuf is shared, use accordingly */
5740 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH + 1)];
5742 /* WARNING: ensure number of messages returned does not exceed messages array size
5743 * Messages array set 1 larger than maximum expected message count of 6 */
5744 const char *messages[7];
5745 unsigned messageCount = 0;
5747 const char *failsafeInfoMessage = NULL;
5748 const char *invertedInfoMessage = NULL;
5750 if (ARMING_FLAG(ARMED)) {
5751 if (FLIGHT_MODE(FAILSAFE_MODE) || FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding()) {
5752 /* ADDS MAXIMUM OF 3 MESSAGES TO TOTAL NORMALLY, 5 MESSAGES DURING FAILSAFE */
5753 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE) {
5754 messages[messageCount++] = STATE(LANDING_DETECTED) ? OSD_MESSAGE_STR(OSD_MSG_WP_LANDED) : OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED);
5755 } else if (NAV_Status.state == MW_NAV_STATE_WP_ENROUTE) {
5756 // Countdown display for remaining Waypoints
5757 char buf[6];
5758 osdFormatDistanceSymbol(buf, posControl.wpDistance, 0);
5759 tfp_sprintf(messageBuf, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl.activeWaypointIndex), posControl.geoWaypointCount, buf);
5760 messages[messageCount++] = messageBuf;
5761 } else if (NAV_Status.state == MW_NAV_STATE_HOLD_TIMED) {
5762 if (navConfig()->general.waypoint_enforce_altitude && !posControl.wpAltitudeReached) {
5763 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ADJUSTING_WP_ALT);
5764 } else {
5765 // WP hold time countdown in seconds
5766 timeMs_t currentTime = millis();
5767 int holdTimeRemaining = posControl.waypointList[posControl.activeWaypointIndex].p1 - (int)(MS2S(currentTime - posControl.wpReachedTime));
5768 holdTimeRemaining = holdTimeRemaining >= 0 ? holdTimeRemaining : 0;
5770 tfp_sprintf(messageBuf, "HOLDING WP FOR %2u S", holdTimeRemaining);
5772 messages[messageCount++] = messageBuf;
5775 else {
5776 const char *navStateMessage = navigationStateMessage();
5777 if (navStateMessage) {
5778 messages[messageCount++] = navStateMessage;
5781 #if defined(USE_SAFE_HOME)
5782 const char *safehomeMessage = divertingToSafehomeMessage();
5783 if (safehomeMessage) {
5784 messages[messageCount++] = safehomeMessage;
5786 #endif
5787 if (FLIGHT_MODE(FAILSAFE_MODE)) { // In FS mode while armed
5788 if (NAV_Status.state == MW_NAV_STATE_LAND_SETTLE && posControl.landingDelay > 0) {
5789 uint16_t remainingHoldSec = MS2S(posControl.landingDelay - millis());
5790 tfp_sprintf(messageBuf, "LANDING DELAY: %3u SECONDS", remainingHoldSec);
5792 messages[messageCount++] = messageBuf;
5795 const char *failsafePhaseMessage = osdFailsafePhaseMessage();
5796 failsafeInfoMessage = osdFailsafeInfoMessage();
5798 if (failsafePhaseMessage) {
5799 messages[messageCount++] = failsafePhaseMessage;
5801 if (failsafeInfoMessage) {
5802 messages[messageCount++] = failsafeInfoMessage;
5804 } else if (isWaypointMissionRTHActive()) {
5805 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
5806 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL);
5808 } else if (STATE(LANDING_DETECTED)) {
5809 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_LANDED);
5810 } else {
5811 /* Messages shown only when Failsafe, WP, RTH or Emergency Landing not active and landed state inactive */
5812 /* ADDS MAXIMUM OF 3 MESSAGES TO TOTAL */
5813 if (STATE(AIRPLANE)) { /* ADDS MAXIMUM OF 3 MESSAGES TO TOTAL */
5814 #ifdef USE_FW_AUTOLAND
5815 if (canFwLandingBeCancelled()) {
5816 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MOVE_STICKS);
5817 } else
5818 #endif
5819 if (navGetCurrentStateFlags() & NAV_CTL_LAUNCH) {
5820 messages[messageCount++] = navConfig()->fw.launch_manual_throttle ? OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH_MANUAL) :
5821 OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH);
5822 const char *launchStateMessage = fixedWingLaunchStateMessage();
5823 if (launchStateMessage) {
5824 messages[messageCount++] = launchStateMessage;
5826 } else if (FLIGHT_MODE(SOARING_MODE)) {
5827 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING);
5828 } else if (isFwAutoModeActive(BOXAUTOTUNE)) {
5829 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE);
5830 if (FLIGHT_MODE(MANUAL_MODE)) {
5831 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO);
5833 } else if (isFwAutoModeActive(BOXAUTOTRIM) && !feature(FEATURE_FW_AUTOTRIM)) {
5834 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM);
5835 } else if (isFixedWingLevelTrimActive()) {
5836 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOLEVEL);
5839 if (IS_RC_MODE_ACTIVE(BOXANGLEHOLD)) {
5840 int8_t navAngleHoldAxis = navCheckActiveAngleHoldAxis();
5841 if (isAngleHoldLevel()) {
5842 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_LEVEL);
5843 } else if (navAngleHoldAxis == FD_ROLL) {
5844 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_ROLL);
5845 } else if (navAngleHoldAxis == FD_PITCH) {
5846 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_PITCH);
5849 } else if (STATE(MULTIROTOR)) { /* ADDS MAXIMUM OF 2 MESSAGES TO TOTAL */
5850 if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
5851 if (posControl.cruise.multicopterSpeed >= 50.0f) {
5852 char buf[6];
5853 osdFormatVelocityStr(buf, posControl.cruise.multicopterSpeed, false, false);
5854 tfp_sprintf(messageBuf, "(SPD %s)", buf);
5855 } else {
5856 strcpy(messageBuf, "(HOLD)");
5858 messages[messageCount++] = messageBuf;
5859 } else if (FLIGHT_MODE(HEADFREE_MODE)) {
5860 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE);
5862 if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && !navigationRequiresAngleMode()) {
5863 /* If ALTHOLD is separately enabled for multirotor together with ANGL/HORIZON/ACRO modes
5864 * then ANGL/HORIZON/ACRO are indicated by the OSD_FLYMODE field.
5865 * In this case indicate ALTHOLD is active via a system message */
5867 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD);
5871 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS)) { /* ADDS MAXIMUM OF 2 MESSAGES TO TOTAL */
5872 unsigned invalidIndex;
5874 // Check if we're unable to arm for some reason
5875 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING) && !settingsValidate(&invalidIndex)) {
5876 const setting_t *setting = settingGet(invalidIndex);
5877 settingGetName(setting, messageBuf);
5878 for (int ii = 0; messageBuf[ii]; ii++) {
5879 messageBuf[ii] = sl_toupper(messageBuf[ii]);
5881 invertedInfoMessage = messageBuf;
5882 messages[messageCount++] = invertedInfoMessage;
5884 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
5885 messages[messageCount++] = invertedInfoMessage;
5886 } else {
5887 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM);
5888 messages[messageCount++] = invertedInfoMessage;
5889 // Show the reason for not arming
5890 messages[messageCount++] = osdArmingDisabledReasonMessage();
5892 } else if (!ARMING_FLAG(ARMED)) {
5893 if (isWaypointListValid()) {
5894 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED);
5898 /* Messages that are shown regardless of Arming state */
5899 /* ADDS MAXIMUM OF 2 MESSAGES TO TOTAL NORMALLY, 1 MESSAGE DURING FAILSAFE */
5900 if (posControl.flags.wpMissionPlannerActive && !FLIGHT_MODE(FAILSAFE_MODE)) {
5901 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER);
5904 // The following has been commented out as it will be added in #9688
5905 // uint16_t rearmMs = (emergInflightRearmEnabled()) ? emergencyInFlightRearmTimeMS() : 0;
5907 if (savingSettings == true) {
5908 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS);
5909 /*} else if (rearmMs > 0) { // Show rearming time if settings not actively being saved. Ignore the settings saved message if rearm available.
5910 char emReArmMsg[23];
5911 tfp_sprintf(emReArmMsg, "** REARM PERIOD: ");
5912 tfp_sprintf(emReArmMsg + strlen(emReArmMsg), "%02d", (uint8_t)MS2S(rearmMs));
5913 strcat(emReArmMsg, " **\0");
5914 messages[messageCount++] = OSD_MESSAGE_STR(emReArmMsg);*/
5915 } else if (notify_settings_saved > 0) {
5916 if (millis() > notify_settings_saved) {
5917 notify_settings_saved = 0;
5918 } else {
5919 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED);
5923 if (messageCount > 0) {
5924 message = messages[OSD_ALTERNATING_CHOICES(systemMessageCycleTime(messageCount, messages), messageCount)];
5925 if (message == failsafeInfoMessage) {
5926 // failsafeInfoMessage is not useful for recovering
5927 // a lost model, but might help avoiding a crash.
5928 // Blink to grab user attention.
5929 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
5930 } else if (message == invertedInfoMessage) {
5931 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
5933 // We're showing either failsafePhaseMessage or
5934 // navStateMessage. Don't BLINK here since
5935 // having this text available might be crucial
5936 // during a lost aircraft recovery and blinking
5937 // will cause it to be missing from some frames.
5940 osdFormatMessage(buff, buff_size, message, isCenteredText);
5942 return elemAttr;
5945 void osdResetWarningFlags(void)
5947 osdWarningsFlags = 0;
5950 static bool osdCheckWarning(bool condition, uint8_t warningFlag, uint8_t *warningsCount)
5952 #define WARNING_REDISPLAY_DURATION 5000; // milliseconds
5954 const timeMs_t currentTimeMs = millis();
5955 static timeMs_t warningDisplayStartTime = 0;
5956 static timeMs_t redisplayStartTimeMs = 0;
5957 static uint16_t osdWarningTimerDuration;
5958 static uint8_t newWarningFlags;
5960 if (condition) { // condition required to trigger warning
5961 if (!(osdWarningsFlags & warningFlag)) {
5962 osdWarningsFlags |= warningFlag;
5963 newWarningFlags |= warningFlag;
5964 redisplayStartTimeMs = 0;
5966 #ifdef USE_DEV_TOOLS
5967 if (systemConfig()->groundTestMode) {
5968 return true;
5970 #endif
5971 /* Warnings displayed in full for set time before shrinking down to alert symbol with warning count only.
5972 * All current warnings then redisplayed for 5s on 30s rolling cycle.
5973 * New warnings dislayed individually for 10s */
5974 if (currentTimeMs > redisplayStartTimeMs) {
5975 warningDisplayStartTime = currentTimeMs;
5976 osdWarningTimerDuration = newWarningFlags ? 10000 : WARNING_REDISPLAY_DURATION;
5977 redisplayStartTimeMs = currentTimeMs + osdWarningTimerDuration + 30000;
5980 if (currentTimeMs - warningDisplayStartTime < osdWarningTimerDuration) {
5981 return (newWarningFlags & warningFlag) || osdWarningTimerDuration == WARNING_REDISPLAY_DURATION;
5982 } else {
5983 newWarningFlags = 0;
5985 *warningsCount += 1;
5986 } else if (osdWarningsFlags & warningFlag) {
5987 osdWarningsFlags &= ~warningFlag;
5990 return false;
5993 static textAttributes_t osdGetMultiFunctionMessage(char *buff)
5995 /* Message length limit 10 char max */
5997 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
5998 static uint8_t warningsCount;
5999 const char *message = NULL;
6001 #ifdef USE_MULTI_FUNCTIONS
6002 /* --- FUNCTIONS --- */
6003 multi_function_e selectedFunction = multiFunctionSelection();
6005 if (selectedFunction) {
6006 multi_function_e activeFunction = selectedFunction;
6008 switch (selectedFunction) {
6009 case MULTI_FUNC_NONE:
6010 case MULTI_FUNC_1:
6011 message = warningsCount ? "WARNINGS !" : "0 WARNINGS";
6012 break;
6013 case MULTI_FUNC_2:
6014 message = posControl.flags.manualEmergLandActive ? "ABORT LAND" : "EMERG LAND";
6015 break;
6016 case MULTI_FUNC_3:
6017 #if defined(USE_SAFE_HOME)
6018 if (navConfig()->general.flags.safehome_usage_mode != SAFEHOME_USAGE_OFF) {
6019 message = MULTI_FUNC_FLAG(MF_SUSPEND_SAFEHOMES) ? "USE SFHOME" : "SUS SFHOME";
6020 break;
6022 #endif
6023 activeFunction++;
6024 FALLTHROUGH;
6025 case MULTI_FUNC_4:
6026 if (navConfig()->general.flags.rth_trackback_mode != RTH_TRACKBACK_OFF) {
6027 message = MULTI_FUNC_FLAG(MF_SUSPEND_TRACKBACK) ? "USE TKBACK" : "SUS TKBACK";
6028 break;
6030 activeFunction++;
6031 FALLTHROUGH;
6032 case MULTI_FUNC_5:
6033 #ifdef USE_DSHOT
6034 if (STATE(MULTIROTOR)) {
6035 message = MULTI_FUNC_FLAG(MF_TURTLE_MODE) ? "END TURTLE" : "USE TURTLE";
6036 break;
6038 #endif
6039 activeFunction++;
6040 FALLTHROUGH;
6041 case MULTI_FUNC_6:
6042 message = ARMING_FLAG(ARMED) ? "NOW ARMED " : "EMERG ARM ";
6043 break;
6044 case MULTI_FUNC_END:
6045 break;
6048 if (activeFunction != selectedFunction) {
6049 setMultifunctionSelection(activeFunction);
6052 strcpy(buff, message);
6054 if (isNextMultifunctionItemAvailable()) {
6055 // provides feedback indicating when a new selection command has been received by flight controller
6056 buff[9] = '>';
6059 return elemAttr;
6061 #endif // MULTIFUNCTION - functions only, warnings always defined
6063 /* --- WARNINGS --- */
6064 const char *messages[7];
6065 uint8_t messageCount = 0;
6066 bool warningCondition = false;
6067 warningsCount = 0;
6068 uint8_t warningFlagID = 1;
6070 // Low Battery
6071 const batteryState_e batteryState = getBatteryState();
6072 warningCondition = batteryState == BATTERY_CRITICAL || batteryState == BATTERY_WARNING;
6073 if (osdCheckWarning(warningCondition, warningFlagID, &warningsCount)) {
6074 messages[messageCount++] = batteryState == BATTERY_CRITICAL ? "BATT EMPTY" : "BATT LOW !";
6077 #if defined(USE_GPS)
6078 // GPS Fix and Failure
6079 if (feature(FEATURE_GPS)) {
6080 if (osdCheckWarning(!STATE(GPS_FIX), warningFlagID <<= 1, &warningsCount)) {
6081 bool gpsFailed = getHwGPSStatus() == HW_SENSOR_UNAVAILABLE;
6082 messages[messageCount++] = gpsFailed ? "GPS FAILED" : "NO GPS FIX";
6086 // RTH sanity (warning if RTH heads 200m further away from home than closest point)
6087 warningCondition = NAV_Status.state == MW_NAV_STATE_RTH_ENROUTE && !posControl.flags.rthTrackbackActive &&
6088 (posControl.homeDistance - posControl.rthSanityChecker.minimalDistanceToHome) > 20000;
6089 if (osdCheckWarning(warningCondition, warningFlagID <<= 1, &warningsCount)) {
6090 messages[messageCount++] = "RTH SANITY";
6093 // Altitude sanity (warning if significant mismatch between estimated and GPS altitude)
6094 if (osdCheckWarning(posControl.flags.gpsCfEstimatedAltitudeMismatch, warningFlagID <<= 1, &warningsCount)) {
6095 messages[messageCount++] = "ALT SANITY";
6097 #endif
6099 #if defined(USE_MAG)
6100 // Magnetometer failure
6101 if (requestedSensors[SENSOR_INDEX_MAG] != MAG_NONE) {
6102 hardwareSensorStatus_e magStatus = getHwCompassStatus();
6103 if (osdCheckWarning(magStatus == HW_SENSOR_UNAVAILABLE || magStatus == HW_SENSOR_UNHEALTHY, warningFlagID <<= 1, &warningsCount)) {
6104 messages[messageCount++] = "MAG FAILED";
6107 #endif
6108 // Vibration levels TODO - needs better vibration measurement to be useful
6109 // const float vibrationLevel = accGetVibrationLevel();
6110 // warningCondition = vibrationLevel > 1.5f;
6111 // if (osdCheckWarning(warningCondition, warningFlagID <<= 1, &warningsCount)) {
6112 // messages[messageCount++] = vibrationLevel > 2.5f ? "BAD VIBRTN" : "VIBRATION!";
6113 // }
6115 #ifdef USE_DEV_TOOLS
6116 if (osdCheckWarning(systemConfig()->groundTestMode, warningFlagID <<= 1, &warningsCount)) {
6117 messages[messageCount++] = "GRD TEST !";
6119 #endif
6121 if (messageCount) {
6122 message = messages[OSD_ALTERNATING_CHOICES(1000, messageCount)]; // display each warning on 1s cycle
6123 strcpy(buff, message);
6124 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
6125 } else if (warningsCount) {
6126 buff[0] = SYM_ALERT;
6127 tfp_sprintf(buff + 1, "%u ", warningsCount);
6130 return elemAttr;
6133 #endif // OSD