updates
[inav.git] / src / main / io / osd.c
blob435b09f9e3f3d4b9571f56aca35c0739f35dc2a6
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>
32 #include "platform.h"
34 #ifdef USE_OSD
36 #include "build/debug.h"
37 #include "build/version.h"
39 #include "cms/cms.h"
40 #include "cms/cms_types.h"
41 #include "cms/cms_menu_osd.h"
43 #include "common/axis.h"
44 #include "common/constants.h"
45 #include "common/filter.h"
46 #include "common/log.h"
47 #include "common/olc.h"
48 #include "common/printf.h"
49 #include "common/string_light.h"
50 #include "common/time.h"
51 #include "common/typeconversion.h"
52 #include "common/utils.h"
54 #include "config/feature.h"
55 #include "config/parameter_group.h"
56 #include "config/parameter_group_ids.h"
58 #include "drivers/display.h"
59 #include "drivers/display_canvas.h"
60 #include "drivers/display_font_metadata.h"
61 #include "drivers/osd_symbols.h"
62 #include "drivers/time.h"
63 #include "drivers/vtx_common.h"
65 #include "io/flashfs.h"
66 #include "io/gps.h"
67 #include "io/osd.h"
68 #include "io/osd_common.h"
69 #include "io/osd_hud.h"
70 #include "io/osd_utils.h"
71 #include "io/displayport_msp_bf_compat.h"
72 #include "io/vtx.h"
73 #include "io/vtx_string.h"
75 #include "io/osd/custom_elements.h"
77 #include "fc/config.h"
78 #include "fc/controlrate_profile.h"
79 #include "fc/fc_core.h"
80 #include "fc/fc_tasks.h"
81 #include "fc/multifunction.h"
82 #include "fc/rc_adjustments.h"
83 #include "fc/rc_controls.h"
84 #include "fc/rc_modes.h"
85 #include "fc/runtime_config.h"
86 #include "fc/settings.h"
88 #include "flight/imu.h"
89 #include "flight/mixer.h"
90 #include "flight/pid.h"
91 #include "flight/power_limits.h"
92 #include "flight/rth_estimator.h"
93 #include "flight/servos.h"
94 #include "flight/wind_estimator.h"
96 #include "navigation/navigation.h"
97 #include "navigation/navigation_private.h"
99 #include "rx/rx.h"
100 #include "rx/msp_override.h"
102 #include "sensors/acceleration.h"
103 #include "sensors/battery.h"
104 #include "sensors/boardalignment.h"
105 #include "sensors/compass.h"
106 #include "sensors/diagnostics.h"
107 #include "sensors/sensors.h"
108 #include "sensors/pitotmeter.h"
109 #include "sensors/temperature.h"
110 #include "sensors/esc_sensor.h"
111 #include "sensors/rangefinder.h"
113 #include "programming/logic_condition.h"
114 #include "programming/global_variables.h"
116 #ifdef USE_HARDWARE_REVISION_DETECTION
117 #include "hardware_revision.h"
118 #endif
120 #define VIDEO_BUFFER_CHARS_PAL 480
121 #define VIDEO_BUFFER_CHARS_HDZERO 900
122 #define VIDEO_BUFFER_CHARS_DJIWTF 1320
124 #define GFORCE_FILTER_TC 0.2
126 #define OSD_STATS_SINGLE_PAGE_MIN_ROWS 18
127 #define IS_HI(X) (rxGetChannelValue(X) > 1750)
128 #define IS_LO(X) (rxGetChannelValue(X) < 1250)
129 #define IS_MID(X) (rxGetChannelValue(X) > 1250 && rxGetChannelValue(X) < 1750)
131 #define OSD_RESUME_UPDATES_STICK_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
132 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
133 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
135 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
136 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
138 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
140 // Adjust OSD_MESSAGE's default position when
141 // changing OSD_MESSAGE_LENGTH
142 #define OSD_MESSAGE_LENGTH 28
143 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
144 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
145 // Wrap all string constants intenteded for display as messages with
146 // this macro to ensure compile time length validation.
147 #define OSD_MESSAGE_STR(x) ({ \
148 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
149 x; \
152 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
154 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
155 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
157 #define OSD_MIN_FONT_VERSION 3
159 static timeMs_t linearDescentMessageMs = 0;
160 static timeMs_t notify_settings_saved = 0;
161 static bool savingSettings = false;
163 static unsigned currentLayout = 0;
164 static int layoutOverride = -1;
165 static bool hasExtendedFont = false; // Wether the font supports characters > 256
166 static timeMs_t layoutOverrideUntil = 0;
167 static pt1Filter_t GForceFilter, GForceFilterAxis[XYZ_AXIS_COUNT];
168 static float GForce, GForceAxis[XYZ_AXIS_COUNT];
170 typedef struct statistic_s {
171 uint16_t max_speed;
172 uint16_t max_3D_speed;
173 uint16_t max_air_speed;
174 uint16_t min_voltage; // /100
175 int16_t max_current;
176 int32_t max_power;
177 int16_t min_rssi;
178 int16_t min_lq; // for CRSF
179 int16_t min_rssi_dbm; // for CRSF
180 int32_t max_altitude;
181 uint32_t max_distance;
182 } statistic_t;
184 static statistic_t stats;
186 static timeUs_t resumeRefreshAt = 0;
187 static bool refreshWaitForResumeCmdRelease;
189 static bool fullRedraw = false;
191 static uint8_t armState;
193 static textAttributes_t osdGetMultiFunctionMessage(char *buff);
194 static uint8_t osdWarningsFlags = 0;
196 typedef struct osdMapData_s {
197 uint32_t scale;
198 char referenceSymbol;
199 } osdMapData_t;
201 static osdMapData_t osdMapData;
203 static displayPort_t *osdDisplayPort;
204 static bool osdDisplayIsReady = false;
205 #if defined(USE_CANVAS)
206 static displayCanvas_t osdCanvas;
207 static bool osdDisplayHasCanvas;
208 #else
209 #define osdDisplayHasCanvas false
210 #endif
212 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
214 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t, osdConfig, PG_OSD_CONFIG, 9);
215 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t, osdLayoutsConfig, PG_OSD_LAYOUTS_CONFIG, 1);
217 void osdStartedSaveProcess(void) {
218 savingSettings = true;
221 void osdShowEEPROMSavedNotification(void) {
222 savingSettings = false;
223 notify_settings_saved = millis() + 5000;
228 bool osdDisplayIsPAL(void)
230 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL;
233 bool osdDisplayIsHD(void)
235 if (displayScreenSize(osdDisplayPort) >= VIDEO_BUFFER_CHARS_HDZERO)
237 return true;
239 return false;
245 * Aligns text to the left side. Adds spaces at the end to keep string length unchanged.
247 static void osdLeftAlignString(char *buff)
249 uint8_t sp = 0, ch = 0;
250 uint8_t len = strlen(buff);
251 while (buff[sp] == ' ') sp++;
252 for (ch = 0; ch < (len - sp); ch++) buff[ch] = buff[ch + sp];
253 for (sp = ch; sp < len; sp++) buff[sp] = ' ';
257 * This is a simplified distance conversion code that does not use any scaling
258 * but is fully compatible with the DJI G2 MSP Displayport OSD implementation.
259 * (Based on osdSimpleAltitudeSymbol() implementation)
261 /* void osdSimpleDistanceSymbol(char *buff, int32_t dist) {
263 int32_t convertedDistance;
264 char suffix;
266 switch ((osd_unit_e)osdConfig()->units) {
267 case OSD_UNIT_UK:
268 FALLTHROUGH;
269 case OSD_UNIT_GA:
270 FALLTHROUGH;
271 case OSD_UNIT_IMPERIAL:
272 convertedDistance = CENTIMETERS_TO_FEET(dist);
273 suffix = SYM_ALT_FT;
274 break;
275 case OSD_UNIT_METRIC_MPH:
276 FALLTHROUGH;
277 case OSD_UNIT_METRIC:
278 convertedDistance = CENTIMETERS_TO_METERS(dist);
279 suffix = SYM_ALT_M; // Intentionally use the altitude symbol, as the distance symbol is not defined in BFCOMPAT mode
280 break;
283 tfp_sprintf(buff, "%5d", (int) convertedDistance); // 5 digits, allowing up to 99999 meters/feet, which should be plenty for 99.9% of use cases
284 buff[5] = suffix;
285 buff[6] = '\0';
286 } */
289 * Converts distance into a string based on the current unit system
290 * prefixed by a a symbol to indicate the unit used.
291 * @param dist Distance in centimeters
293 static void osdFormatDistanceSymbol(char *buff, int32_t dist, uint8_t decimals)
295 uint8_t digits = 3U; // Total number of digits (including decimal point)
296 uint8_t sym_index = 3U; // Position (index) at buffer of units symbol
297 uint8_t symbol_m = SYM_DIST_M;
298 uint8_t symbol_km = SYM_DIST_KM;
299 uint8_t symbol_ft = SYM_DIST_FT;
300 uint8_t symbol_mi = SYM_DIST_MI;
301 uint8_t symbol_nm = SYM_DIST_NM;
303 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
304 if (isBfCompatibleVideoSystem(osdConfig())) {
305 // Add one digit so up no switch to scaled decimal occurs above 99
306 digits = 4U;
307 sym_index = 4U;
308 // Use altitude symbols on purpose, as it seems distance symbols are not defined in BFCOMPAT mode
309 symbol_m = SYM_ALT_M;
310 symbol_km = SYM_ALT_KM;
311 symbol_ft = SYM_ALT_FT;
312 symbol_mi = SYM_MI;
313 symbol_nm = SYM_MI;
315 #endif
317 switch ((osd_unit_e)osdConfig()->units) {
318 case OSD_UNIT_UK:
319 FALLTHROUGH;
320 case OSD_UNIT_IMPERIAL:
321 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_MILE, decimals, 3, digits, false)) {
322 buff[sym_index] = symbol_mi;
323 } else {
324 buff[sym_index] = symbol_ft;
326 buff[sym_index + 1] = '\0';
327 break;
328 case OSD_UNIT_METRIC_MPH:
329 FALLTHROUGH;
330 case OSD_UNIT_METRIC:
331 if (osdFormatCentiNumber(buff, dist, METERS_PER_KILOMETER, decimals, 3, digits, false)) {
332 buff[sym_index] = symbol_km;
333 } else {
334 buff[sym_index] = symbol_m;
336 buff[sym_index + 1] = '\0';
337 break;
338 case OSD_UNIT_GA:
339 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), (uint32_t)FEET_PER_NAUTICALMILE, decimals, 3, digits, false)) {
340 buff[sym_index] = symbol_nm;
341 } else {
342 buff[sym_index] = symbol_ft;
344 buff[sym_index + 1] = '\0';
345 break;
350 * Converts distance into a string based on the current unit system.
351 * @param dist Distance in centimeters
353 static void osdFormatDistanceStr(char *buff, int32_t dist)
355 int32_t centifeet;
356 switch ((osd_unit_e)osdConfig()->units) {
357 case OSD_UNIT_UK:
358 FALLTHROUGH;
359 case OSD_UNIT_IMPERIAL:
360 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
361 if (abs(centifeet) < FEET_PER_MILE * 100 / 2) {
362 // Show feet when dist < 0.5mi
363 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
364 } else {
365 // Show miles when dist >= 0.5mi
366 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100*FEET_PER_MILE)),
367 (abs(centifeet) % (100 * FEET_PER_MILE)) / FEET_PER_MILE, SYM_MI);
369 break;
370 case OSD_UNIT_METRIC_MPH:
371 FALLTHROUGH;
372 case OSD_UNIT_METRIC:
373 if (abs(dist) < METERS_PER_KILOMETER * 100) {
374 // Show meters when dist < 1km
375 tfp_sprintf(buff, "%d%c", (int)(dist / 100), SYM_M);
376 } else {
377 // Show kilometers when dist >= 1km
378 tfp_sprintf(buff, "%d.%02d%c", (int)(dist / (100*METERS_PER_KILOMETER)),
379 (abs(dist) % (100 * METERS_PER_KILOMETER)) / METERS_PER_KILOMETER, SYM_KM);
381 break;
382 case OSD_UNIT_GA:
383 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
384 if (abs(centifeet) < 100000) {
385 // Show feet when dist < 1000ft
386 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
387 } else {
388 // Show nautical miles when dist >= 1000ft
389 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100 * FEET_PER_NAUTICALMILE)),
390 (int)((abs(centifeet) % (int)(100 * FEET_PER_NAUTICALMILE)) / FEET_PER_NAUTICALMILE), SYM_NM);
392 break;
397 * Converts velocity based on the current unit system (kmh or mph).
398 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
400 static int32_t osdConvertVelocityToUnit(int32_t vel)
402 switch ((osd_unit_e)osdConfig()->units) {
403 case OSD_UNIT_UK:
404 FALLTHROUGH;
405 case OSD_UNIT_METRIC_MPH:
406 FALLTHROUGH;
407 case OSD_UNIT_IMPERIAL:
408 return CMSEC_TO_CENTIMPH(vel) / 100; // Convert to mph
409 case OSD_UNIT_METRIC:
410 return CMSEC_TO_CENTIKPH(vel) / 100; // Convert to kmh
411 case OSD_UNIT_GA:
412 return CMSEC_TO_CENTIKNOTS(vel) / 100; // Convert to Knots
414 // Unreachable
415 return -1;
419 * Converts velocity into a string based on the current unit system.
420 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
421 * @param _3D is a 3D velocity
422 * @param _max is a maximum velocity
424 void osdFormatVelocityStr(char* buff, int32_t vel, bool _3D, bool _max)
426 switch ((osd_unit_e)osdConfig()->units) {
427 case OSD_UNIT_UK:
428 FALLTHROUGH;
429 case OSD_UNIT_METRIC_MPH:
430 FALLTHROUGH;
431 case OSD_UNIT_IMPERIAL:
432 if (_max) {
433 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
434 } else {
435 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
437 break;
438 case OSD_UNIT_METRIC:
439 if (_max) {
440 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
441 } else {
442 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
444 break;
445 case OSD_UNIT_GA:
446 if (_max) {
447 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
448 } else {
449 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
451 break;
456 * 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
458 static void osdGenerateAverageVelocityStr(char* buff) {
459 uint32_t cmPerSec = getTotalTravelDistance() / getFlightTime();
460 osdFormatVelocityStr(buff, cmPerSec, false, false);
464 * Converts wind speed into a string based on the current unit system, using
465 * always 3 digits and an additional character for the unit at the right. buff
466 * is null terminated.
467 * @param ws Raw wind speed in cm/s
469 #ifdef USE_WIND_ESTIMATOR
470 static void osdFormatWindSpeedStr(char *buff, int32_t ws, bool isValid)
472 int32_t centivalue;
473 char suffix;
474 switch (osdConfig()->units) {
475 case OSD_UNIT_UK:
476 FALLTHROUGH;
477 case OSD_UNIT_METRIC_MPH:
478 FALLTHROUGH;
479 case OSD_UNIT_IMPERIAL:
480 centivalue = CMSEC_TO_CENTIMPH(ws);
481 suffix = SYM_MPH;
482 break;
483 case OSD_UNIT_GA:
484 centivalue = CMSEC_TO_CENTIKNOTS(ws);
485 suffix = SYM_KT;
486 break;
487 default:
488 case OSD_UNIT_METRIC:
489 centivalue = CMSEC_TO_CENTIKPH(ws);
490 suffix = SYM_KMH;
491 break;
494 osdFormatCentiNumber(buff, centivalue, 0, 2, 0, 3, false);
496 if (!isValid && ((millis() / 1000) % 4 < 2))
497 suffix = '*';
499 buff[3] = suffix;
500 buff[4] = '\0';
502 #endif
505 * This is a simplified altitude conversion code that does not use any scaling
506 * but is fully compatible with the DJI G2 MSP Displayport OSD implementation.
508 /* void osdSimpleAltitudeSymbol(char *buff, int32_t alt) {
510 int32_t convertedAltutude = 0;
511 char suffix = '\0';
513 switch ((osd_unit_e)osdConfig()->units) {
514 case OSD_UNIT_UK:
515 FALLTHROUGH;
516 case OSD_UNIT_GA:
517 FALLTHROUGH;
518 case OSD_UNIT_IMPERIAL:
519 convertedAltutude = CENTIMETERS_TO_FEET(alt);
520 suffix = SYM_ALT_FT;
521 break;
522 case OSD_UNIT_METRIC_MPH:
523 FALLTHROUGH;
524 case OSD_UNIT_METRIC:
525 convertedAltutude = CENTIMETERS_TO_METERS(alt);
526 suffix = SYM_ALT_M;
527 break;
530 tfp_sprintf(buff, "%4d", (int) convertedAltutude);
531 buff[4] = suffix;
532 buff[5] = '\0';
533 } */
536 * Converts altitude into a string based on the current unit system
537 * prefixed by a a symbol to indicate the unit used.
538 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
540 void osdFormatAltitudeSymbol(char *buff, int32_t alt)
542 uint8_t totalDigits = 4U;
543 uint8_t digits = 4U;
544 uint8_t symbolIndex = 4U;
545 uint8_t symbolKFt = SYM_ALT_KFT;
547 if (alt >= 0) {
548 digits = 3U;
549 buff[0] = ' ';
552 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
553 if (isBfCompatibleVideoSystem(osdConfig())) {
554 totalDigits++;
555 digits++;
556 symbolIndex++;
557 symbolKFt = SYM_ALT_FT;
559 #endif
561 switch ((osd_unit_e)osdConfig()->units) {
562 case OSD_UNIT_UK:
563 FALLTHROUGH;
564 case OSD_UNIT_GA:
565 FALLTHROUGH;
566 case OSD_UNIT_IMPERIAL:
567 if (osdFormatCentiNumber(buff + totalDigits - digits, CENTIMETERS_TO_CENTIFEET(alt), 1000, 0, 2, digits, false)) {
568 // Scaled to kft
569 buff[symbolIndex++] = symbolKFt;
570 } else {
571 // Formatted in feet
572 buff[symbolIndex++] = SYM_ALT_FT;
574 buff[symbolIndex] = '\0';
575 break;
576 case OSD_UNIT_METRIC_MPH:
577 FALLTHROUGH;
578 case OSD_UNIT_METRIC:
579 // alt is alredy in cm
580 if (osdFormatCentiNumber(buff + totalDigits - digits, alt, 1000, 0, 2, digits, false)) {
581 // Scaled to km
582 buff[symbolIndex++] = SYM_ALT_KM;
583 } else {
584 // Formatted in m
585 buff[symbolIndex++] = SYM_ALT_M;
587 buff[symbolIndex] = '\0';
588 break;
593 * Converts altitude into a string based on the current unit system.
594 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
596 static void osdFormatAltitudeStr(char *buff, int32_t alt)
598 int32_t value;
599 switch ((osd_unit_e)osdConfig()->units) {
600 case OSD_UNIT_UK:
601 FALLTHROUGH;
602 case OSD_UNIT_GA:
603 FALLTHROUGH;
604 case OSD_UNIT_IMPERIAL:
605 value = CENTIMETERS_TO_FEET(alt);
606 tfp_sprintf(buff, "%d%c", (int)value, SYM_FT);
607 break;
608 case OSD_UNIT_METRIC_MPH:
609 FALLTHROUGH;
610 case OSD_UNIT_METRIC:
611 value = CENTIMETERS_TO_METERS(alt);
612 tfp_sprintf(buff, "%d%c", (int)value, SYM_M);
613 break;
617 static void osdFormatTime(char *buff, uint32_t seconds, char sym_m, char sym_h)
619 uint32_t value = seconds;
620 char sym = sym_m;
621 // Maximum value we can show in minutes is 99 minutes and 59 seconds
622 if (seconds > (99 * 60) + 59) {
623 sym = sym_h;
624 value = seconds / 60;
626 buff[0] = sym;
627 tfp_sprintf(buff + 1, "%02d:%02d", (int)(value / 60), (int)(value % 60));
630 static inline void osdFormatOnTime(char *buff)
632 osdFormatTime(buff, micros() / 1000000, SYM_ON_M, SYM_ON_H);
635 static inline void osdFormatFlyTime(char *buff, textAttributes_t *attr)
637 uint32_t seconds = getFlightTime();
638 osdFormatTime(buff, seconds, SYM_FLY_M, SYM_FLY_H);
639 if (attr && osdConfig()->time_alarm > 0) {
640 if (seconds / 60 >= osdConfig()->time_alarm && ARMING_FLAG(ARMED)) {
641 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
647 * Trim whitespace from string.
648 * Used in Stats screen on lines with multiple values.
650 char *osdFormatTrimWhiteSpace(char *buff)
652 char *end;
654 // Trim leading spaces
655 while(isspace((unsigned char)*buff)) buff++;
657 // All spaces?
658 if(*buff == 0)
659 return buff;
661 // Trim trailing spaces
662 end = buff + strlen(buff) - 1;
663 while(end > buff && isspace((unsigned char)*end)) end--;
665 // Write new null terminator character
666 end[1] = '\0';
668 return buff;
672 * Converts RSSI into a % value used by the OSD.
674 static uint16_t osdConvertRSSI(void)
676 // change range to [0, 99]
677 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE, 0, 99);
680 static uint16_t osdGetCrsfLQ(void)
682 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
683 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
684 int16_t displayedLQ = 0;
685 switch (osdConfig()->crsf_lq_format) {
686 case OSD_CRSF_LQ_TYPE1:
687 displayedLQ = statsLQ;
688 break;
689 case OSD_CRSF_LQ_TYPE2:
690 displayedLQ = statsLQ;
691 break;
692 case OSD_CRSF_LQ_TYPE3:
693 displayedLQ = rxLinkStatistics.rfMode >= 2 ? scaledLQ : statsLQ;
694 break;
696 return displayedLQ;
699 static int16_t osdGetCrsfdBm(void)
701 return rxLinkStatistics.uplinkRSSI;
704 * Displays a temperature postfixed with a symbol depending on the current unit system
705 * @param label to display
706 * @param valid true if measurement is valid
707 * @param temperature in deciDegrees Celcius
709 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)
711 char buff[TEMPERATURE_LABEL_LEN + 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN + 2];
712 textAttributes_t elemAttr = valid ? TEXT_ATTRIBUTES_NONE : _TEXT_ATTRIBUTES_BLINK_BIT;
713 uint8_t valueXOffset = 0;
715 if (symbol) {
716 buff[0] = symbol;
717 buff[1] = '\0';
718 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
719 valueXOffset = 1;
721 #ifdef USE_TEMPERATURE_SENSOR
722 else if (label[0] != '\0') {
723 uint8_t label_len = strnlen(label, TEMPERATURE_LABEL_LEN);
724 memcpy(buff, label, label_len);
725 memset(buff + label_len, ' ', TEMPERATURE_LABEL_LEN + 1 - label_len);
726 buff[5] = '\0';
727 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
728 valueXOffset = osdConfig()->temp_label_align == OSD_ALIGN_LEFT ? 5 : label_len + 1;
730 #else
731 UNUSED(label);
732 #endif
734 if (valid) {
736 if ((temperature <= alarm_min) || (temperature >= alarm_max)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
737 if (osdConfig()->units == OSD_UNIT_IMPERIAL) temperature = temperature * 9 / 5.0f + 320;
738 tfp_sprintf(buff, "%3d", temperature / 10);
740 } else
741 strcpy(buff, "---");
743 buff[3] = osdConfig()->units == OSD_UNIT_IMPERIAL ? SYM_TEMP_F : SYM_TEMP_C;
744 buff[4] = '\0';
746 displayWriteWithAttr(osdDisplayPort, elemPosX + valueXOffset, elemPosY, buff, elemAttr);
749 #ifdef USE_TEMPERATURE_SENSOR
750 static void osdDisplayTemperatureSensor(uint8_t elemPosX, uint8_t elemPosY, uint8_t sensorIndex)
752 int16_t temperature;
753 const bool valid = getSensorTemperature(sensorIndex, &temperature);
754 const tempSensorConfig_t *sensorConfig = tempSensorConfig(sensorIndex);
755 uint16_t symbol = sensorConfig->osdSymbol ? SYM_TEMP_SENSOR_FIRST + sensorConfig->osdSymbol - 1 : 0;
756 osdDisplayTemperature(elemPosX, elemPosY, symbol, sensorConfig->label, valid, temperature, sensorConfig->alarm_min, sensorConfig->alarm_max);
758 #endif
760 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
762 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
763 const int coordinateLength = osdConfig()->coordinate_digits + 1;
765 buff[0] = sym;
766 int32_t integerPart = val / GPS_DEGREES_DIVIDER;
767 // Latitude maximum integer width is 3 (-90) while
768 // longitude maximum integer width is 4 (-180).
769 int integerDigits = tfp_sprintf(buff + 1, (integerPart == 0 && val < 0) ? "-%d" : "%d", (int)integerPart);
770 // We can show up to 7 digits in decimalPart.
771 int32_t decimalPart = abs(val % (int)GPS_DEGREES_DIVIDER);
772 STATIC_ASSERT(GPS_DEGREES_DIVIDER == 1e7, adjust_max_decimal_digits);
773 int decimalDigits;
774 bool bfcompat = false; // Assume BFCOMPAT mode is no enabled
776 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
777 if(isBfCompatibleVideoSystem(osdConfig())) {
778 bfcompat = true;
780 #endif
782 if (!bfcompat) {
783 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, "%07d", (int)decimalPart);
784 // Embbed the decimal separator
785 buff[1 + integerDigits - 1] += SYM_ZERO_HALF_TRAILING_DOT - '0';
786 buff[1 + integerDigits] += SYM_ZERO_HALF_LEADING_DOT - '0';
787 } else {
788 // BFCOMPAT mode enabled
789 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, ".%06d", (int)decimalPart);
791 // Fill up to coordinateLength with zeros
792 int total = 1 + integerDigits + decimalDigits;
793 while(total < coordinateLength) {
794 buff[total] = '0';
795 total++;
797 buff[coordinateLength] = '\0';
800 static void osdFormatCraftName(char *buff)
802 if (strlen(systemConfig()->craftName) == 0)
803 strcpy(buff, "CRAFT_NAME");
804 else {
805 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
806 buff[i] = sl_toupper((unsigned char)systemConfig()->craftName[i]);
807 if (systemConfig()->craftName[i] == 0)
808 break;
813 void osdFormatPilotName(char *buff)
815 if (strlen(systemConfig()->pilotName) == 0)
816 strcpy(buff, "PILOT_NAME");
817 else {
818 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
819 buff[i] = sl_toupper((unsigned char)systemConfig()->pilotName[i]);
820 if (systemConfig()->pilotName[i] == 0)
821 break;
826 static const char * osdArmingDisabledReasonMessage(void)
828 const char *message = NULL;
829 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
831 switch (isArmingDisabledReason()) {
832 case ARMING_DISABLED_FAILSAFE_SYSTEM:
833 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
834 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING) {
835 if (failsafeIsReceivingRxData()) {
836 // reminder to disarm to exit FAILSAFE_RX_LOSS_MONITORING once timeout period ends
837 if (IS_RC_MODE_ACTIVE(BOXARM)) {
838 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF);
840 } else {
841 // Not receiving RX data
842 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
845 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS);
846 case ARMING_DISABLED_NOT_LEVEL:
847 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL);
848 case ARMING_DISABLED_SENSORS_CALIBRATING:
849 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL);
850 case ARMING_DISABLED_SYSTEM_OVERLOADED:
851 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED);
852 case ARMING_DISABLED_NAVIGATION_UNSAFE:
853 // Check the exact reason
854 switch (navigationIsBlockingArming(NULL)) {
855 char buf[6];
856 case NAV_ARMING_BLOCKER_NONE:
857 break;
858 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX:
859 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX);
860 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE:
861 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST);
862 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR:
863 osdFormatDistanceSymbol(buf, distanceToFirstWP(), 0);
864 tfp_sprintf(messageBuf, "FIRST WP TOO FAR (%s)", buf);
865 return message = messageBuf;
866 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR:
867 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG);
869 break;
870 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED:
871 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL);
872 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED:
873 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL);
874 case ARMING_DISABLED_ARM_SWITCH:
875 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST);
876 case ARMING_DISABLED_HARDWARE_FAILURE:
878 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
879 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE);
881 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
882 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL);
884 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
885 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL);
887 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
888 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL);
890 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
891 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL);
893 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
894 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL);
896 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
897 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL);
900 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL);
901 case ARMING_DISABLED_BOXFAILSAFE:
902 return OSD_MESSAGE_STR(OSD_MSG_FS_EN);
903 case ARMING_DISABLED_BOXKILLSWITCH:
904 return OSD_MESSAGE_STR(OSD_MSG_KILL_SW_EN);
905 case ARMING_DISABLED_RC_LINK:
906 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK);
907 case ARMING_DISABLED_THROTTLE:
908 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW);
909 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED:
910 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER);
911 case ARMING_DISABLED_SERVO_AUTOTRIM:
912 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE);
913 case ARMING_DISABLED_OOM:
914 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY);
915 case ARMING_DISABLED_INVALID_SETTING:
916 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
917 case ARMING_DISABLED_CLI:
918 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE);
919 case ARMING_DISABLED_PWM_OUTPUT_ERROR:
920 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR);
921 case ARMING_DISABLED_NO_PREARM:
922 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM);
923 case ARMING_DISABLED_DSHOT_BEEPER:
924 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER);
925 // Cases without message
926 case ARMING_DISABLED_LANDING_DETECTED:
927 FALLTHROUGH;
928 case ARMING_DISABLED_CMS_MENU:
929 FALLTHROUGH;
930 case ARMING_DISABLED_OSD_MENU:
931 FALLTHROUGH;
932 case ARMING_DISABLED_ALL_FLAGS:
933 FALLTHROUGH;
934 case ARMED:
935 FALLTHROUGH;
936 case SIMULATOR_MODE_HITL:
937 FALLTHROUGH;
938 case SIMULATOR_MODE_SITL:
939 FALLTHROUGH;
940 case WAS_EVER_ARMED:
941 break;
943 return NULL;
946 static const char * osdFailsafePhaseMessage(void)
948 // See failsafe.h for each phase explanation
949 switch (failsafePhase()) {
950 case FAILSAFE_RETURN_TO_HOME:
951 // XXX: Keep this in sync with OSD_FLYMODE.
952 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS);
953 case FAILSAFE_LANDING:
954 // This should be considered an emergengy landing
955 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS);
956 case FAILSAFE_RX_LOSS_MONITORING:
957 // Only reachable from FAILSAFE_LANDED, which performs
958 // a disarm. Since aircraft has been disarmed, we no
959 // longer show failsafe details.
960 FALLTHROUGH;
961 case FAILSAFE_LANDED:
962 // Very brief, disarms and transitions into
963 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
964 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
965 // so we'll show the user how to re-arm in when
966 // that flag is the reason to prevent arming.
967 FALLTHROUGH;
968 case FAILSAFE_RX_LOSS_IDLE:
969 // This only happens when user has chosen NONE as FS
970 // procedure. The recovery messages should be enough.
971 FALLTHROUGH;
972 case FAILSAFE_IDLE:
973 // Failsafe not active
974 FALLTHROUGH;
975 case FAILSAFE_RX_LOSS_DETECTED:
976 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
977 // or the FS procedure immediately.
978 FALLTHROUGH;
979 case FAILSAFE_RX_LOSS_RECOVERED:
980 // Exiting failsafe
981 break;
983 return NULL;
986 static const char * osdFailsafeInfoMessage(void)
988 if (failsafeIsReceivingRxData() && !FLIGHT_MODE(NAV_FW_AUTOLAND)) {
989 // User must move sticks to exit FS mode
990 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS);
992 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
995 #if defined(USE_SAFE_HOME)
996 static const char * divertingToSafehomeMessage(void)
998 #ifdef USE_FW_AUTOLAND
999 if (!posControl.fwLandState.landWp && (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && posControl.safehomeState.isApplied)) {
1000 #else
1001 if (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && posControl.safehomeState.isApplied) {
1002 #endif
1003 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME);
1005 #endif
1006 return NULL;
1010 static const char * navigationStateMessage(void)
1012 if (!posControl.rthState.rthLinearDescentActive && linearDescentMessageMs != 0)
1013 linearDescentMessageMs = 0;
1015 switch (NAV_Status.state) {
1016 case MW_NAV_STATE_NONE:
1017 break;
1018 case MW_NAV_STATE_RTH_START:
1019 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH);
1020 case MW_NAV_STATE_RTH_CLIMB:
1021 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB);
1022 case MW_NAV_STATE_RTH_ENROUTE:
1023 if (posControl.flags.rthTrackbackActive) {
1024 return OSD_MESSAGE_STR(OSD_MSG_RTH_TRACKBACK);
1025 } else {
1026 if (posControl.rthState.rthLinearDescentActive && (linearDescentMessageMs == 0 || linearDescentMessageMs > millis())) {
1027 if (linearDescentMessageMs == 0)
1028 linearDescentMessageMs = millis() + 5000; // Show message for 5 seconds.
1030 return OSD_MESSAGE_STR(OSD_MSG_RTH_LINEAR_DESCENT);
1031 } else
1032 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME);
1034 case MW_NAV_STATE_HOLD_INFINIT:
1035 // Used by HOLD flight modes. No information to add.
1036 break;
1037 case MW_NAV_STATE_HOLD_TIMED:
1038 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
1039 break;
1040 case MW_NAV_STATE_WP_ENROUTE:
1041 // "TO WP" + WP countdown added in osdGetSystemMessage
1042 break;
1043 case MW_NAV_STATE_PROCESS_NEXT:
1044 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP);
1045 case MW_NAV_STATE_DO_JUMP:
1046 // Not used
1047 break;
1048 case MW_NAV_STATE_LAND_START:
1049 // Not used
1050 break;
1051 case MW_NAV_STATE_EMERGENCY_LANDING:
1052 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING);
1053 case MW_NAV_STATE_LAND_IN_PROGRESS:
1054 return OSD_MESSAGE_STR(OSD_MSG_LANDING);
1055 case MW_NAV_STATE_HOVER_ABOVE_HOME:
1056 if (STATE(FIXED_WING_LEGACY)) {
1057 #if defined(USE_SAFE_HOME)
1058 if (posControl.safehomeState.isApplied) {
1059 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME);
1061 #endif
1062 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME);
1064 return OSD_MESSAGE_STR(OSD_MSG_HOVERING);
1065 case MW_NAV_STATE_LANDED:
1066 return OSD_MESSAGE_STR(OSD_MSG_LANDED);
1067 case MW_NAV_STATE_LAND_SETTLE:
1069 // If there is a FS landing delay occurring. That is handled by the calling function.
1070 if (posControl.landingDelay > 0)
1071 break;
1073 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND);
1075 case MW_NAV_STATE_LAND_START_DESCENT:
1076 // Not used
1077 break;
1080 return NULL;
1083 static void osdFormatMessage(char *buff, size_t size, const char *message, bool isCenteredText)
1085 // String is always filled with Blanks
1086 memset(buff, SYM_BLANK, size);
1087 if (message) {
1088 size_t messageLength = strlen(message);
1089 int rem = isCenteredText ? MAX(0, (int)size - (int)messageLength) : 0;
1090 strncpy(buff + rem / 2, message, MIN((int)size - rem / 2, (int)messageLength));
1092 // Ensure buff is zero terminated
1093 buff[size] = '\0';
1097 * Draws the battery symbol filled in accordingly to the
1098 * battery voltage to buff[0].
1100 static void osdFormatBatteryChargeSymbol(char *buff)
1102 uint8_t p = calculateBatteryPercentage();
1103 p = (100 - p) / 16.6;
1104 buff[0] = SYM_BATT_FULL + p;
1107 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t *attr)
1109 const batteryState_e batteryState = getBatteryState();
1111 if (batteryState == BATTERY_WARNING || batteryState == BATTERY_CRITICAL) {
1112 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
1116 void osdCrosshairPosition(uint8_t *x, uint8_t *y)
1118 *x = osdDisplayPort->cols / 2;
1119 *y = osdDisplayPort->rows / 2;
1120 *y -= osdConfig()->horizon_offset; // positive horizon_offset moves the HUD up, negative moves down
1124 * Check if this OSD layout is using scaled or unscaled throttle.
1125 * If both are used, it will default to scaled.
1127 bool osdUsingScaledThrottle(void)
1129 bool usingScaledThrottle = OSD_VISIBLE(osdLayoutsConfig()->item_pos[currentLayout][OSD_SCALED_THROTTLE_POS]);
1130 bool usingRCThrottle = OSD_VISIBLE(osdLayoutsConfig()->item_pos[currentLayout][OSD_THROTTLE_POS]);
1132 if (!usingScaledThrottle && !usingRCThrottle)
1133 usingScaledThrottle = true;
1135 return usingScaledThrottle;
1139 * Formats throttle position prefixed by its symbol.
1140 * Shows unscaled or scaled (output to motor) throttle percentage
1142 static void osdFormatThrottlePosition(char *buff, bool useScaled, textAttributes_t *elemAttr)
1144 buff[0] = SYM_BLANK;
1145 buff[1] = SYM_THR;
1146 if (navigationIsControllingThrottle()) {
1147 buff[0] = SYM_AUTO_THR0;
1148 buff[1] = SYM_AUTO_THR1;
1149 if (isFixedWingAutoThrottleManuallyIncreased()) {
1150 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1152 useScaled = true;
1154 #ifdef USE_POWER_LIMITS
1155 if (powerLimiterIsLimiting()) {
1156 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1158 #endif
1159 int8_t throttlePercent = getThrottlePercent(useScaled);
1160 if ((useScaled && throttlePercent <= 0) || !ARMING_FLAG(ARMED)) {
1161 const char* message = ARMING_FLAG(ARMED) ? (throttlePercent == 0 && !ifMotorstopFeatureEnabled()) ? "IDLE" : "STOP" : "DARM";
1162 buff[0] = SYM_THR;
1163 strcpy(buff + 1, message);
1164 return;
1166 tfp_sprintf(buff + 2, "%3d", throttlePercent);
1170 * Formats gvars prefixed by its number (0-indexed). If autoThr
1172 static void osdFormatGVar(char *buff, uint8_t index)
1174 buff[0] = 'G';
1175 buff[1] = '0'+index;
1176 buff[2] = ':';
1177 #ifdef USE_PROGRAMMING_FRAMEWORK
1178 osdFormatCentiNumber(buff + 3, (int32_t)gvGet(index)*(int32_t)100, 1, 0, 0, 5, false);
1179 #endif
1182 #if defined(USE_ESC_SENSOR)
1183 static void osdFormatRpm(char *buff, uint32_t rpm)
1185 buff[0] = SYM_RPM;
1186 if (rpm) {
1187 if ( digitCount(rpm) > osdConfig()->esc_rpm_precision) {
1188 uint8_t rpmMaxDecimals = (osdConfig()->esc_rpm_precision - 3);
1189 osdFormatCentiNumber(buff + 1, rpm / 10, 0, rpmMaxDecimals, rpmMaxDecimals, osdConfig()->esc_rpm_precision-1, false);
1190 buff[osdConfig()->esc_rpm_precision] = 'K';
1191 buff[osdConfig()->esc_rpm_precision+1] = '\0';
1193 else {
1194 switch(osdConfig()->esc_rpm_precision) {
1195 case 6:
1196 tfp_sprintf(buff + 1, "%6lu", rpm);
1197 break;
1198 case 5:
1199 tfp_sprintf(buff + 1, "%5lu", rpm);
1200 break;
1201 case 4:
1202 tfp_sprintf(buff + 1, "%4lu", rpm);
1203 break;
1204 case 3:
1205 default:
1206 tfp_sprintf(buff + 1, "%3lu", rpm);
1207 break;
1213 else {
1214 uint8_t buffPos = 1;
1215 while (buffPos <=( osdConfig()->esc_rpm_precision)) {
1216 strcpy(buff + buffPos++, "-");
1220 #endif
1222 int32_t osdGetAltitude(void)
1224 return getEstimatedActualPosition(Z);
1227 static inline int32_t osdGetAltitudeMsl(void)
1229 return getEstimatedActualPosition(Z) + posControl.gpsOrigin.alt;
1232 uint16_t osdGetRemainingGlideTime(void) {
1233 float value = getEstimatedActualVelocity(Z);
1234 static pt1Filter_t glideTimeFilterState;
1235 const timeMs_t curTimeMs = millis();
1236 static timeMs_t glideTimeUpdatedMs;
1238 value = pt1FilterApply4(&glideTimeFilterState, isnormal(value) ? value : 0, 0.5, MS2S(curTimeMs - glideTimeUpdatedMs));
1239 glideTimeUpdatedMs = curTimeMs;
1241 if (value < 0) {
1242 value = osdGetAltitude() / abs((int)value);
1243 } else {
1244 value = 0;
1247 return (uint16_t)roundf(value);
1250 static bool osdIsHeadingValid(void)
1252 return isImuHeadingValid();
1255 int16_t osdGetHeading(void)
1257 return attitude.values.yaw;
1260 int16_t osdGetPanServoOffset(void)
1262 int8_t servoIndex = osdConfig()->pan_servo_index;
1263 int16_t servoPosition = servo[servoIndex];
1264 int16_t servoMiddle = servoParams(servoIndex)->middle;
1265 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition - servoMiddle) * osdConfig()->pan_servo_pwm2centideg);
1268 // Returns a heading angle in degrees normalized to [0, 360).
1269 int osdGetHeadingAngle(int angle)
1271 while (angle < 0) {
1272 angle += 360;
1274 while (angle >= 360) {
1275 angle -= 360;
1277 return angle;
1280 #if defined(USE_GPS)
1282 /* Draws a map with the given symbol in the center and given point of interest
1283 * defined by its distance in meters and direction in degrees.
1284 * referenceHeading indicates the up direction in the map, in degrees, while
1285 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1286 * arrow to indicate the map reference to the user. The drawn argument is an
1287 * in-out used to store the last position where the craft was drawn to avoid
1288 * erasing all screen on each redraw.
1290 static void osdDrawMap(int referenceHeading, uint16_t referenceSym, uint16_t centerSym,
1291 uint32_t poiDistance, int16_t poiDirection, uint16_t poiSymbol,
1292 uint16_t *drawn, uint32_t *usedScale)
1294 // TODO: These need to be tested with several setups. We might
1295 // need to make them configurable.
1296 const int hMargin = 5;
1297 const int vMargin = 3;
1299 // TODO: Get this from the display driver?
1300 const int charWidth = 12;
1301 const int charHeight = 18;
1303 uint8_t minX = hMargin;
1304 uint8_t maxX = osdDisplayPort->cols - 1 - hMargin;
1305 uint8_t minY = vMargin;
1306 uint8_t maxY = osdDisplayPort->rows - 1 - vMargin;
1307 uint8_t midX = osdDisplayPort->cols / 2;
1308 uint8_t midY = osdDisplayPort->rows / 2;
1310 // Fixed marks
1311 displayWriteChar(osdDisplayPort, midX, midY, centerSym);
1313 // First, erase the previous drawing.
1314 if (OSD_VISIBLE(*drawn)) {
1315 displayWriteChar(osdDisplayPort, OSD_X(*drawn), OSD_Y(*drawn), SYM_BLANK);
1316 *drawn = 0;
1319 uint32_t initialScale;
1320 const unsigned scaleMultiplier = 2;
1321 // We try to reduce the scale when the POI will be around half the distance
1322 // between the center and the closers map edge, to avoid too much jumping
1323 const int scaleReductionMultiplier = MIN(midX - hMargin, midY - vMargin) / 2;
1325 switch (osdConfig()->units) {
1326 case OSD_UNIT_UK:
1327 FALLTHROUGH;
1328 case OSD_UNIT_IMPERIAL:
1329 initialScale = 16; // 16m ~= 0.01miles
1330 break;
1331 case OSD_UNIT_GA:
1332 initialScale = 18; // 18m ~= 0.01 nautical miles
1333 break;
1334 default:
1335 case OSD_UNIT_METRIC_MPH:
1336 FALLTHROUGH;
1337 case OSD_UNIT_METRIC:
1338 initialScale = 10; // 10m as initial scale
1339 break;
1342 // Try to keep the same scale when getting closer until we draw over the center point
1343 uint32_t scale = initialScale;
1344 if (*usedScale) {
1345 scale = *usedScale;
1346 if (scale > initialScale && poiDistance < *usedScale * scaleReductionMultiplier) {
1347 scale /= scaleMultiplier;
1351 if (STATE(GPS_FIX)) {
1353 int directionToPoi = osdGetHeadingAngle(poiDirection - referenceHeading);
1354 float poiAngle = DEGREES_TO_RADIANS(directionToPoi);
1355 float poiSin = sin_approx(poiAngle);
1356 float poiCos = cos_approx(poiAngle);
1358 // Now start looking for a valid scale that lets us draw everything
1359 int ii;
1360 for (ii = 0; ii < 50; ii++) {
1361 // Calculate location of the aircraft in map
1362 int points = poiDistance / ((float)scale / charHeight);
1364 float pointsX = points * poiSin;
1365 int poiX = midX - roundf(pointsX / charWidth);
1366 if (poiX < minX || poiX > maxX) {
1367 scale *= scaleMultiplier;
1368 continue;
1371 float pointsY = points * poiCos;
1372 int poiY = midY + roundf(pointsY / charHeight);
1373 if (poiY < minY || poiY > maxY) {
1374 scale *= scaleMultiplier;
1375 continue;
1378 if (poiX == midX && poiY == midY) {
1379 // We're over the map center symbol, so we would be drawing
1380 // over it even if we increased the scale. Alternate between
1381 // drawing the center symbol or drawing the POI.
1382 if (centerSym != SYM_BLANK && OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1383 break;
1385 } else {
1387 uint16_t c;
1388 if (displayReadCharWithAttr(osdDisplayPort, poiX, poiY, &c, NULL) && c != SYM_BLANK) {
1389 // Something else written here, increase scale. If the display doesn't support reading
1390 // back characters, we assume there's nothing.
1392 // If we're close to the center, decrease scale. Otherwise increase it.
1393 uint8_t centerDeltaX = (maxX - minX) / (scaleMultiplier * 2);
1394 uint8_t centerDeltaY = (maxY - minY) / (scaleMultiplier * 2);
1395 if (poiX >= midX - centerDeltaX && poiX <= midX + centerDeltaX &&
1396 poiY >= midY - centerDeltaY && poiY <= midY + centerDeltaY &&
1397 scale > scaleMultiplier) {
1399 scale /= scaleMultiplier;
1400 } else {
1401 scale *= scaleMultiplier;
1403 continue;
1407 // Draw the point on the map
1408 if (poiSymbol == SYM_ARROW_UP) {
1409 // Drawing aircraft, rotate
1410 int mapHeading = osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading);
1411 poiSymbol += mapHeading * 2 / 45;
1413 displayWriteChar(osdDisplayPort, poiX, poiY, poiSymbol);
1415 // Update saved location
1416 *drawn = OSD_POS(poiX, poiY) | OSD_VISIBLE_FLAG;
1417 break;
1421 *usedScale = scale;
1423 // Update global map data for scale and reference
1424 osdMapData.scale = scale;
1425 osdMapData.referenceSymbol = referenceSym;
1428 /* Draws a map with the home in the center and the craft moving around.
1429 * See osdDrawMap() for reference.
1431 static void osdDrawHomeMap(int referenceHeading, uint8_t referenceSym, uint16_t *drawn, uint32_t *usedScale)
1433 osdDrawMap(referenceHeading, referenceSym, SYM_HOME, GPS_distanceToHome, GPS_directionToHome, SYM_ARROW_UP, drawn, usedScale);
1436 /* Draws a map with the aircraft in the center and the home moving around.
1437 * See osdDrawMap() for reference.
1439 static void osdDrawRadar(uint16_t *drawn, uint32_t *usedScale)
1441 int16_t reference = DECIDEGREES_TO_DEGREES(osdGetHeading());
1442 int16_t poiDirection = osdGetHeadingAngle(GPS_directionToHome + 180);
1443 osdDrawMap(reference, 0, SYM_ARROW_UP, GPS_distanceToHome, poiDirection, SYM_HOME, drawn, usedScale);
1446 static uint16_t crc_accumulate(uint8_t data, uint16_t crcAccum)
1448 uint8_t tmp;
1449 tmp = data ^ (uint8_t)(crcAccum & 0xff);
1450 tmp ^= (tmp << 4);
1451 crcAccum = (crcAccum >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
1452 return crcAccum;
1456 static void osdDisplayTelemetry(void)
1458 uint32_t trk_data;
1459 uint16_t trk_crc = 0;
1460 char trk_buffer[31];
1461 static int16_t trk_elevation = 127;
1462 static uint16_t trk_bearing = 0;
1464 if (ARMING_FLAG(ARMED)) {
1465 if (STATE(GPS_FIX)){
1466 if (GPS_distanceToHome > 5) {
1467 trk_bearing = GPS_directionToHome;
1468 trk_bearing += 360 + 180;
1469 trk_bearing %= 360;
1470 int32_t alt = CENTIMETERS_TO_METERS(osdGetAltitude());
1471 float at = atan2(alt, GPS_distanceToHome);
1472 trk_elevation = at * 57.2957795f; // 57.2957795 = 1 rad
1473 trk_elevation += 37; // because elevation in telemetry should be from -37 to 90
1474 if (trk_elevation < 0) {
1475 trk_elevation = 0;
1480 else{
1481 trk_elevation = 127;
1482 trk_bearing = 0;
1485 trk_data = 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1486 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.
1487 trk_data = trk_data | (uint32_t)trk_bearing << 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1488 trk_crc = crc_accumulate(0xFF & trk_data, trk_crc); // CRC First Byte bits 0-7
1489 trk_crc = crc_accumulate(0xFF & trk_bearing, trk_crc); // CRC Second Byte bits 8-15
1490 trk_crc = crc_accumulate(trk_bearing >> 8, trk_crc); // CRC Third Byte bits 16-17
1491 trk_data = trk_data | (uint32_t)trk_crc << 17; // bits 18-29 CRC & 0x3FFFF
1493 for (uint8_t t_ctr = 0; t_ctr < 30; t_ctr++) { // Prepare screen buffer and write data line.
1494 if (trk_data & (uint32_t)1 << t_ctr){
1495 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_0;
1497 else{
1498 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_1;
1501 trk_buffer[30] = 0;
1502 displayWrite(osdDisplayPort, 0, 0, trk_buffer);
1503 if (osdConfig()->telemetry>1){
1504 displayWrite(osdDisplayPort, 0, 3, trk_buffer); // Test display because normal telemetry line is not visible
1507 #endif
1509 static void osdFormatPidControllerOutput(char *buff, const char *label, const pidController_t *pidController, uint8_t scale, bool showDecimal) {
1510 strcpy(buff, label);
1511 for (uint8_t i = strlen(label); i < 5; ++i) buff[i] = ' ';
1512 uint8_t decimals = showDecimal ? 1 : 0;
1513 osdFormatCentiNumber(buff + 5, pidController->proportional * scale, 0, decimals, 0, 4, false);
1514 buff[9] = ' ';
1515 osdFormatCentiNumber(buff + 10, pidController->integrator * scale, 0, decimals, 0, 4, false);
1516 buff[14] = ' ';
1517 osdFormatCentiNumber(buff + 15, pidController->derivative * scale, 0, decimals, 0, 4, false);
1518 buff[19] = ' ';
1519 osdFormatCentiNumber(buff + 20, pidController->output_constrained * scale, 0, decimals, 0, 4, false);
1520 buff[24] = '\0';
1523 static void osdDisplayBatteryVoltage(uint8_t elemPosX, uint8_t elemPosY, uint16_t voltage, uint8_t digits, uint8_t decimals)
1525 char buff[7];
1526 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1528 osdFormatBatteryChargeSymbol(buff);
1529 buff[1] = '\0';
1530 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1531 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1533 elemAttr = TEXT_ATTRIBUTES_NONE;
1534 digits = MIN(digits, 5);
1535 osdFormatCentiNumber(buff, voltage, 0, decimals, 0, digits, false);
1536 buff[digits] = SYM_VOLT;
1537 buff[digits+1] = '\0';
1538 const batteryState_e batteryVoltageState = checkBatteryVoltageState();
1539 if (batteryVoltageState == BATTERY_CRITICAL || batteryVoltageState == BATTERY_WARNING) {
1540 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1542 displayWriteWithAttr(osdDisplayPort, elemPosX + 1, elemPosY, buff, elemAttr);
1545 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)
1547 textAttributes_t elemAttr;
1548 char buff[4];
1550 const pid8_t *pid = &pidBank()->pid[pidIndex];
1551 pidType_e pidType = pidIndexGetType(pidIndex);
1553 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1555 if (pidType == PID_TYPE_NONE) {
1556 // PID is not used in this configuration. Draw dashes.
1557 // XXX: Keep this in sync with the %3d format and spacing used below
1558 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - - -");
1559 return;
1562 elemAttr = TEXT_ATTRIBUTES_NONE;
1563 tfp_sprintf(buff, "%3d", pid->P);
1564 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1565 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1566 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1568 elemAttr = TEXT_ATTRIBUTES_NONE;
1569 tfp_sprintf(buff, "%3d", pid->I);
1570 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1571 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1572 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1574 elemAttr = TEXT_ATTRIBUTES_NONE;
1575 tfp_sprintf(buff, "%3d", pid->D);
1576 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1577 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1578 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1580 elemAttr = TEXT_ATTRIBUTES_NONE;
1581 tfp_sprintf(buff, "%3d", pid->FF);
1582 if ((isAdjustmentFunctionSelected(adjFuncFF)) || (((adjFuncFF == ADJUSTMENT_ROLL_FF) || (adjFuncFF == ADJUSTMENT_PITCH_FF)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF))))
1583 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1584 displayWriteWithAttr(osdDisplayPort, elemPosX + 16, elemPosY, buff, elemAttr);
1587 static void osdDisplayNavPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD)
1589 textAttributes_t elemAttr;
1590 char buff[4];
1592 const pid8_t *pid = &pidBank()->pid[pidIndex];
1593 pidType_e pidType = pidIndexGetType(pidIndex);
1595 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1597 if (pidType == PID_TYPE_NONE) {
1598 // PID is not used in this configuration. Draw dashes.
1599 // XXX: Keep this in sync with the %3d format and spacing used below
1600 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - -");
1601 return;
1604 elemAttr = TEXT_ATTRIBUTES_NONE;
1605 tfp_sprintf(buff, "%3d", pid->P);
1606 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1607 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1608 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1610 elemAttr = TEXT_ATTRIBUTES_NONE;
1611 tfp_sprintf(buff, "%3d", pid->I);
1612 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1613 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1614 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1616 elemAttr = TEXT_ATTRIBUTES_NONE;
1617 tfp_sprintf(buff, "%3d", pidType == PID_TYPE_PIFF ? pid->FF : pid->D);
1618 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1619 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1620 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1623 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) {
1624 char buff[8];
1625 textAttributes_t elemAttr;
1626 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1628 elemAttr = TEXT_ATTRIBUTES_NONE;
1629 osdFormatCentiNumber(buff, value * 100, 0, maxDecimals, 0, MIN(valueLength, 8), false);
1630 if (isAdjustmentFunctionSelected(adjFunc))
1631 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1632 displayWriteWithAttr(osdDisplayPort, elemPosX + strlen(str) + 1 + valueOffset, elemPosY, buff, elemAttr);
1635 int8_t getGeoWaypointNumber(int8_t waypointIndex)
1637 static int8_t lastWaypointIndex = 1;
1638 static int8_t geoWaypointIndex;
1640 if (waypointIndex != lastWaypointIndex) {
1641 lastWaypointIndex = geoWaypointIndex = waypointIndex;
1642 for (uint8_t i = posControl.startWpIndex; i <= waypointIndex; i++) {
1643 if (posControl.waypointList[i].action == NAV_WP_ACTION_SET_POI ||
1644 posControl.waypointList[i].action == NAV_WP_ACTION_SET_HEAD ||
1645 posControl.waypointList[i].action == NAV_WP_ACTION_JUMP) {
1646 geoWaypointIndex -= 1;
1651 return geoWaypointIndex - posControl.startWpIndex + 1;
1654 void osdDisplaySwitchIndicator(const char *swName, int rcValue, char *buff) {
1655 int8_t ptr = 0;
1657 if (osdConfig()->osd_switch_indicators_align_left) {
1658 for (ptr = 0; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH); ptr++) {
1659 buff[ptr] = swName[ptr];
1662 if ( rcValue < 1333) {
1663 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1664 } else if ( rcValue > 1666) {
1665 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1666 } else {
1667 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1669 } else {
1670 if ( rcValue < 1333) {
1671 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1672 } else if ( rcValue > 1666) {
1673 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1674 } else {
1675 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1678 for (ptr = 1; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH) + 1; ptr++) {
1679 buff[ptr] = swName[ptr-1];
1682 ptr++;
1685 buff[ptr] = '\0';
1688 static bool osdDrawSingleElement(uint8_t item)
1690 uint16_t pos = osdLayoutsConfig()->item_pos[currentLayout][item];
1691 if (!OSD_VISIBLE(pos)) {
1692 return false;
1694 uint8_t elemPosX = OSD_X(pos);
1695 uint8_t elemPosY = OSD_Y(pos);
1696 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1697 char buff[32] = {0};
1699 switch (item) {
1700 case OSD_CUSTOM_ELEMENT_1:
1702 customElementDrawElement(buff, 0);
1703 break;
1705 case OSD_CUSTOM_ELEMENT_2:
1707 customElementDrawElement(buff, 1);
1708 break;
1710 case OSD_CUSTOM_ELEMENT_3:
1712 customElementDrawElement(buff, 2);
1713 break;
1715 case OSD_RSSI_VALUE:
1717 uint16_t osdRssi = osdConvertRSSI();
1718 buff[0] = SYM_RSSI;
1719 tfp_sprintf(buff + 1, "%2d", osdRssi);
1720 if (osdRssi < osdConfig()->rssi_alarm) {
1721 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1723 break;
1726 case OSD_MAIN_BATT_VOLTAGE: {
1727 uint8_t base_digits = 2U;
1728 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1729 if(isBfCompatibleVideoSystem(osdConfig())) {
1730 base_digits = 3U; // Add extra digit to account for decimal point taking an extra character space
1732 #endif
1733 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawVoltage(), base_digits + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1734 return true;
1737 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE: {
1738 uint8_t base_digits = 2U;
1739 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1740 if(isBfCompatibleVideoSystem(osdConfig())) {
1741 base_digits = 3U; // Add extra digit to account for decimal point taking an extra character space
1743 #endif
1744 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedVoltage(), base_digits + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1745 return true;
1748 case OSD_CURRENT_DRAW: {
1749 osdFormatCentiNumber(buff, getAmperage(), 0, 2, 0, 3, false);
1750 buff[3] = SYM_AMP;
1751 buff[4] = '\0';
1753 uint8_t current_alarm = osdConfig()->current_alarm;
1754 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
1755 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1757 break;
1760 case OSD_MAH_DRAWN: {
1761 uint8_t mah_digits = osdConfig()->mAh_precision; // Initialize to config value
1763 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1764 if (isBfCompatibleVideoSystem(osdConfig())) {
1765 //BFcompat is unable to work with scaled values and it only has mAh symbol to work with
1766 tfp_sprintf(buff, "%5d", (int)getMAhDrawn()); // Use 5 digits to allow packs below 100Ah
1767 buff[5] = SYM_MAH;
1768 buff[6] = '\0';
1769 } else
1770 #endif
1772 if (osdFormatCentiNumber(buff, getMAhDrawn() * 100, 1000, 0, (mah_digits - 2), mah_digits, false)) {
1773 // Shown in Ah
1774 buff[mah_digits] = SYM_AH;
1775 } else {
1776 // Shown in mAh
1777 buff[mah_digits] = SYM_MAH;
1779 buff[mah_digits + 1] = '\0';
1782 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1783 break;
1786 case OSD_WH_DRAWN:
1787 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3, false);
1788 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1789 buff[3] = SYM_WH;
1790 buff[4] = '\0';
1791 break;
1793 case OSD_BATTERY_REMAINING_CAPACITY:
1795 bool unitsDrawn = false;
1797 if (currentBatteryProfile->capacity.value == 0)
1798 tfp_sprintf(buff, " NA");
1799 else if (!batteryWasFullWhenPluggedIn())
1800 tfp_sprintf(buff, " NF");
1801 else if (currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH) {
1802 uint8_t mah_digits = osdConfig()->mAh_precision; // Initialize to config value
1804 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1805 if (isBfCompatibleVideoSystem(osdConfig())) {
1806 //BFcompat is unable to work with scaled values and it only has mAh symbol to work with
1807 tfp_sprintf(buff, "%5d", (int)getBatteryRemainingCapacity()); // Use 5 digits to allow packs below 100Ah
1808 buff[5] = SYM_MAH;
1809 buff[6] = '\0';
1810 unitsDrawn = true;
1811 } else
1812 #endif
1814 if (osdFormatCentiNumber(buff, getBatteryRemainingCapacity() * 100, 1000, 0, (mah_digits - 2), mah_digits, false)) {
1815 // Shown in Ah
1816 buff[mah_digits] = SYM_AH;
1817 } else {
1818 // Shown in mAh
1819 buff[mah_digits] = SYM_MAH;
1821 buff[mah_digits + 1] = '\0';
1822 unitsDrawn = true;
1824 } else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1825 osdFormatCentiNumber(buff + 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3, false);
1827 if (!unitsDrawn) {
1828 buff[4] = currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH ? SYM_MAH : SYM_WH;
1829 buff[5] = '\0';
1832 if (batteryUsesCapacityThresholds()) {
1833 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1836 break;
1838 case OSD_BATTERY_REMAINING_PERCENT:
1839 osdFormatBatteryChargeSymbol(buff);
1840 tfp_sprintf(buff + 1, "%3d%%", calculateBatteryPercentage());
1841 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1842 break;
1844 case OSD_POWER_SUPPLY_IMPEDANCE:
1845 if (isPowerSupplyImpedanceValid())
1846 tfp_sprintf(buff, "%3d", getPowerSupplyImpedance());
1847 else
1848 strcpy(buff, "---");
1849 buff[3] = SYM_MILLIOHM;
1850 buff[4] = '\0';
1851 break;
1853 #ifdef USE_GPS
1854 case OSD_GPS_SATS:
1855 buff[0] = SYM_SAT_L;
1856 buff[1] = SYM_SAT_R;
1857 tfp_sprintf(buff + 2, "%2d", gpsSol.numSat);
1858 if (!STATE(GPS_FIX)) {
1859 hardwareSensorStatus_e sensorStatus = getHwGPSStatus();
1860 if (sensorStatus == HW_SENSOR_UNAVAILABLE || sensorStatus == HW_SENSOR_UNHEALTHY) {
1861 buff[2] = SYM_ALERT;
1862 buff[3] = '\0';
1864 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1866 break;
1868 case OSD_GPS_SPEED:
1869 osdFormatVelocityStr(buff, gpsSol.groundSpeed, false, false);
1870 break;
1872 case OSD_GPS_MAX_SPEED:
1873 osdFormatVelocityStr(buff, stats.max_speed, false, true);
1874 break;
1876 case OSD_3D_SPEED:
1877 osdFormatVelocityStr(buff, osdGet3DSpeed(), true, false);
1878 break;
1880 case OSD_3D_MAX_SPEED:
1881 osdFormatVelocityStr(buff, stats.max_3D_speed, true, true);
1882 break;
1884 case OSD_GLIDESLOPE:
1886 float horizontalSpeed = gpsSol.groundSpeed;
1887 float sinkRate = -getEstimatedActualVelocity(Z);
1888 static pt1Filter_t gsFilterState;
1889 const timeMs_t currentTimeMs = millis();
1890 static timeMs_t gsUpdatedTimeMs;
1891 float glideSlope = horizontalSpeed / sinkRate;
1892 glideSlope = pt1FilterApply4(&gsFilterState, isnormal(glideSlope) ? glideSlope : 200, 0.5, MS2S(currentTimeMs - gsUpdatedTimeMs));
1893 gsUpdatedTimeMs = currentTimeMs;
1895 buff[0] = SYM_GLIDESLOPE;
1896 if (glideSlope > 0.0f && glideSlope < 100.0f) {
1897 osdFormatCentiNumber(buff + 1, glideSlope * 100.0f, 0, 2, 0, 3, false);
1898 } else {
1899 buff[1] = buff[2] = buff[3] = '-';
1901 buff[4] = '\0';
1902 break;
1905 case OSD_GPS_LAT:
1906 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
1907 break;
1909 case OSD_GPS_LON:
1910 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
1911 break;
1913 case OSD_HOME_DIR:
1915 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
1916 if (GPS_distanceToHome < (navConfig()->general.min_rth_distance / 100) ) {
1917 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_HOME_NEAR);
1919 else
1921 int16_t panHomeDirOffset = 0;
1922 if (!(osdConfig()->pan_servo_pwm2centideg == 0)){
1923 panHomeDirOffset = osdGetPanServoOffset();
1925 int16_t flightDirection = STATE(AIRPLANE) ? CENTIDEGREES_TO_DEGREES(posControl.actualState.cog) : DECIDEGREES_TO_DEGREES(osdGetHeading());
1926 int homeDirection = GPS_directionToHome - flightDirection + panHomeDirOffset;
1927 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), homeDirection);
1929 } else {
1930 // No home or no fix or unknown heading, blink.
1931 // If we're unarmed, show the arrow pointing up so users can see the arrow
1932 // while configuring the OSD. If we're armed, show a '-' indicating that
1933 // we don't know the direction to home.
1934 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1935 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, ARMING_FLAG(ARMED) ? '-' : SYM_ARROW_UP, elemAttr);
1937 return true;
1940 case OSD_HOME_HEADING_ERROR:
1942 buff[0] = SYM_HOME;
1943 buff[1] = SYM_HEADING;
1945 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1946 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())))));
1947 tfp_sprintf(buff + 2, "%4d", h);
1948 } else {
1949 strcpy(buff + 2, "----");
1952 buff[6] = SYM_DEGREES;
1953 buff[7] = '\0';
1954 break;
1957 case OSD_HOME_DIST:
1959 buff[0] = SYM_HOME;
1960 uint32_t distance_to_home_cm = GPS_distanceToHome * 100;
1961 osdFormatDistanceSymbol(&buff[1], distance_to_home_cm, 0);
1963 uint16_t dist_alarm = osdConfig()->dist_alarm;
1964 if (dist_alarm > 0 && GPS_distanceToHome > dist_alarm) {
1965 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1968 break;
1970 case OSD_TRIP_DIST:
1971 buff[0] = SYM_TOTAL;
1972 osdFormatDistanceSymbol(buff + 1, getTotalTravelDistance(), 0);
1973 break;
1975 case OSD_ODOMETER:
1977 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_ODOMETER);
1978 float_t odometerDist = CENTIMETERS_TO_METERS(getTotalTravelDistance());
1979 #ifdef USE_STATS
1980 odometerDist+= statsConfig()->stats_total_dist;
1981 #endif
1983 switch (osdConfig()->units) {
1984 case OSD_UNIT_UK:
1985 FALLTHROUGH;
1986 case OSD_UNIT_IMPERIAL:
1987 osdFormatCentiNumber(buff, METERS_TO_MILES(odometerDist) * 100, 1, 1, 1, 6, true);
1988 buff[6] = SYM_MI;
1989 break;
1990 default:
1991 case OSD_UNIT_GA:
1992 osdFormatCentiNumber(buff, METERS_TO_NAUTICALMILES(odometerDist) * 100, 1, 1, 1, 6, true);
1993 buff[6] = SYM_NM;
1994 break;
1995 case OSD_UNIT_METRIC_MPH:
1996 FALLTHROUGH;
1997 case OSD_UNIT_METRIC:
1998 osdFormatCentiNumber(buff, METERS_TO_KILOMETERS(odometerDist) * 100, 1, 1, 1, 6, true);
1999 buff[6] = SYM_KM;
2000 break;
2002 buff[7] = '\0';
2003 elemPosX++;
2005 break;
2007 case OSD_GROUND_COURSE:
2009 buff[0] = SYM_GROUND_COURSE;
2010 if (osdIsHeadingValid()) {
2011 tfp_sprintf(&buff[1], "%3d", (int16_t)CENTIDEGREES_TO_DEGREES(posControl.actualState.cog));
2012 } else {
2013 buff[1] = buff[2] = buff[3] = '-';
2015 buff[4] = SYM_DEGREES;
2016 buff[5] = '\0';
2017 break;
2020 case OSD_COURSE_HOLD_ERROR:
2022 if (ARMING_FLAG(ARMED) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
2023 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
2024 return true;
2027 buff[0] = SYM_HEADING;
2029 if ((!ARMING_FLAG(ARMED)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && isAdjustingPosition())) {
2030 buff[1] = buff[2] = buff[3] = '-';
2031 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
2032 int16_t herr = lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
2033 if (ABS(herr) > 99)
2034 strcpy(buff + 1, ">99");
2035 else
2036 tfp_sprintf(buff + 1, "%3d", herr);
2039 buff[4] = SYM_DEGREES;
2040 buff[5] = '\0';
2041 break;
2044 case OSD_COURSE_HOLD_ADJUSTMENT:
2046 int16_t heading_adjust = lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
2048 if (ARMING_FLAG(ARMED) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust != 0)))) {
2049 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
2050 return true;
2053 buff[0] = SYM_HEADING;
2055 if (!ARMING_FLAG(ARMED)) {
2056 buff[1] = buff[2] = buff[3] = buff[4] = '-';
2057 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
2058 tfp_sprintf(buff + 1, "%4d", heading_adjust);
2061 buff[5] = SYM_DEGREES;
2062 buff[6] = '\0';
2063 break;
2066 case OSD_CROSS_TRACK_ERROR:
2068 if (isWaypointNavTrackingActive()) {
2069 buff[0] = SYM_CROSS_TRACK_ERROR;
2070 osdFormatDistanceSymbol(buff + 1, navigationGetCrossTrackError(), 0);
2071 } else {
2072 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
2073 return true;
2075 break;
2078 case OSD_GPS_HDOP:
2080 buff[0] = SYM_HDP_L;
2081 buff[1] = SYM_HDP_R;
2082 int32_t centiHDOP = 100 * gpsSol.hdop / HDOP_SCALE;
2083 uint8_t digits = 2U;
2084 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
2085 if (isBfCompatibleVideoSystem(osdConfig())) {
2086 digits = 3U;
2088 #endif
2089 osdFormatCentiNumber(&buff[2], centiHDOP, 0, 1, 0, digits, false);
2090 break;
2093 case OSD_MAP_NORTH:
2095 static uint16_t drawn = 0;
2096 static uint32_t scale = 0;
2097 osdDrawHomeMap(0, 'N', &drawn, &scale);
2098 return true;
2100 case OSD_MAP_TAKEOFF:
2102 static uint16_t drawn = 0;
2103 static uint32_t scale = 0;
2104 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn, &scale);
2105 return true;
2107 case OSD_RADAR:
2109 static uint16_t drawn = 0;
2110 static uint32_t scale = 0;
2111 osdDrawRadar(&drawn, &scale);
2112 return true;
2114 #endif // GPS
2116 case OSD_ALTITUDE:
2118 int32_t alt = osdGetAltitude();
2119 osdFormatAltitudeSymbol(buff, alt);
2121 uint16_t alt_alarm = osdConfig()->alt_alarm;
2122 uint16_t neg_alt_alarm = osdConfig()->neg_alt_alarm;
2123 if ((alt_alarm > 0 && CENTIMETERS_TO_METERS(alt) > alt_alarm) ||
2124 (neg_alt_alarm > 0 && alt < 0 && -CENTIMETERS_TO_METERS(alt) > neg_alt_alarm)) {
2126 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2128 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
2130 if (STATE(MULTIROTOR) && posControl.flags.isAdjustingAltitude) {
2131 /* Indicate MR altitude adjustment active with constant symbol at first blank position.
2132 * Alternate symbol on/off with 600ms cycle if first position not blank (to maintain visibility of -ve sign) */
2133 int8_t blankPos;
2134 for (blankPos = 2; blankPos >= 0; blankPos--) {
2135 if (buff[blankPos] == SYM_BLANK) {
2136 break;
2139 if (blankPos >= 0 || OSD_ALTERNATING_CHOICES(600, 2) == 0) {
2140 blankPos = blankPos < 0 ? 0 : blankPos;
2141 displayWriteChar(osdDisplayPort, elemPosX + blankPos, elemPosY, SYM_TERRAIN_FOLLOWING);
2144 return true;
2147 case OSD_ALTITUDE_MSL:
2149 int32_t alt = osdGetAltitudeMsl();
2150 osdFormatAltitudeSymbol(buff, alt);
2151 break;
2154 #ifdef USE_RANGEFINDER
2155 case OSD_RANGEFINDER:
2157 int32_t range = rangefinderGetLatestRawAltitude();
2158 if (range < 0) {
2159 buff[0] = '-';
2160 buff[1] = '-';
2161 buff[2] = '-';
2162 } else {
2163 osdFormatDistanceSymbol(buff, range, 1);
2166 break;
2167 #endif
2169 case OSD_ONTIME:
2171 osdFormatOnTime(buff);
2172 break;
2175 case OSD_FLYTIME:
2177 osdFormatFlyTime(buff, &elemAttr);
2178 break;
2181 case OSD_ONTIME_FLYTIME:
2183 if (ARMING_FLAG(ARMED)) {
2184 osdFormatFlyTime(buff, &elemAttr);
2185 } else {
2186 osdFormatOnTime(buff);
2188 break;
2191 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH:
2193 /*static int32_t updatedTimeSeconds = 0;*/
2194 static int32_t timeSeconds = -1;
2195 #if defined(USE_ADC) && defined(USE_GPS)
2196 static timeUs_t updatedTimestamp = 0;
2197 timeUs_t currentTimeUs = micros();
2198 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2199 #ifdef USE_WIND_ESTIMATOR
2200 timeSeconds = calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation);
2201 #else
2202 timeSeconds = calculateRemainingFlightTimeBeforeRTH(false);
2203 #endif
2204 updatedTimestamp = currentTimeUs;
2206 #endif
2207 if ((!ARMING_FLAG(ARMED)) || (timeSeconds == -1)) {
2208 buff[0] = SYM_FLIGHT_MINS_REMAINING;
2209 strcpy(buff + 1, "--:--");
2210 #if defined(USE_ADC) && defined(USE_GPS)
2211 updatedTimestamp = 0;
2212 #endif
2213 } else if (timeSeconds == -2) {
2214 // Wind is too strong to come back with cruise throttle
2215 buff[0] = SYM_FLIGHT_MINS_REMAINING;
2216 buff[1] = buff[2] = buff[4] = buff[5] = SYM_WIND_HORIZONTAL;
2217 buff[3] = ':';
2218 buff[6] = '\0';
2219 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2220 } else {
2221 osdFormatTime(buff, timeSeconds, SYM_FLIGHT_MINS_REMAINING, SYM_FLIGHT_HOURS_REMAINING);
2222 if (timeSeconds == 0)
2223 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2226 break;
2228 case OSD_REMAINING_DISTANCE_BEFORE_RTH:;
2229 static int32_t distanceMeters = -1;
2230 #if defined(USE_ADC) && defined(USE_GPS)
2231 static timeUs_t updatedTimestamp = 0;
2232 timeUs_t currentTimeUs = micros();
2233 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2234 #ifdef USE_WIND_ESTIMATOR
2235 distanceMeters = calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation);
2236 #else
2237 distanceMeters = calculateRemainingDistanceBeforeRTH(false);
2238 #endif
2239 updatedTimestamp = currentTimeUs;
2241 #endif
2242 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_FLIGHT_DIST_REMAINING);
2244 if ((!ARMING_FLAG(ARMED)) || (distanceMeters == -1)) {
2245 buff[3] = SYM_BLANK;
2246 buff[4] = '\0';
2247 strcpy(buff, "---");
2248 } else if (distanceMeters == -2) {
2249 // Wind is too strong to come back with cruise throttle
2250 buff[0] = buff[1] = buff[2] = SYM_WIND_HORIZONTAL;
2251 switch ((osd_unit_e)osdConfig()->units){
2252 case OSD_UNIT_UK:
2253 FALLTHROUGH;
2254 case OSD_UNIT_IMPERIAL:
2255 buff[3] = SYM_DIST_MI;
2256 break;
2257 case OSD_UNIT_METRIC_MPH:
2258 FALLTHROUGH;
2259 case OSD_UNIT_METRIC:
2260 buff[3] = SYM_DIST_KM;
2261 break;
2262 case OSD_UNIT_GA:
2263 buff[3] = SYM_DIST_NM;
2264 break;
2266 buff[4] = '\0';
2267 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2268 } else {
2269 osdFormatDistanceSymbol(buff, distanceMeters * 100, 0);
2270 if (distanceMeters == 0)
2271 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2273 elemPosX++;
2274 break;
2276 case OSD_FLYMODE:
2278 char *p = "ACRO";
2279 #ifdef USE_FW_AUTOLAND
2280 if (FLIGHT_MODE(NAV_FW_AUTOLAND))
2281 p = "LAND";
2282 else
2283 #endif
2284 if (FLIGHT_MODE(FAILSAFE_MODE))
2285 p = "!FS!";
2286 else if (FLIGHT_MODE(MANUAL_MODE))
2287 p = "MANU";
2288 else if (FLIGHT_MODE(TURTLE_MODE))
2289 p = "TURT";
2290 else if (FLIGHT_MODE(NAV_RTH_MODE))
2291 p = isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
2292 else if (FLIGHT_MODE(NAV_POSHOLD_MODE) && STATE(AIRPLANE))
2293 p = "LOTR";
2294 else if (FLIGHT_MODE(NAV_POSHOLD_MODE))
2295 p = "HOLD";
2296 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && FLIGHT_MODE(NAV_ALTHOLD_MODE))
2297 p = "CRUZ";
2298 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE))
2299 p = "CRSH";
2300 else if (FLIGHT_MODE(NAV_WP_MODE))
2301 p = " WP ";
2302 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && navigationRequiresAngleMode()) {
2303 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
2304 // it means it can be combined with ANGLE, HORIZON, ANGLEHOLD, ACRO, etc...
2305 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
2306 p = " AH ";
2308 else if (FLIGHT_MODE(ANGLE_MODE))
2309 p = "ANGL";
2310 else if (FLIGHT_MODE(HORIZON_MODE))
2311 p = "HOR ";
2312 else if (FLIGHT_MODE(ANGLEHOLD_MODE))
2313 p = "ANGH";
2315 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
2316 return true;
2319 case OSD_CRAFT_NAME:
2320 osdFormatCraftName(buff);
2321 break;
2323 case OSD_PILOT_NAME:
2324 osdFormatPilotName(buff);
2325 break;
2327 case OSD_PILOT_LOGO:
2328 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_PILOT_LOGO_SML_L);
2329 displayWriteChar(osdDisplayPort, elemPosX+1, elemPosY, SYM_PILOT_LOGO_SML_C);
2330 displayWriteChar(osdDisplayPort, elemPosX+2, elemPosY, SYM_PILOT_LOGO_SML_R);
2331 break;
2333 case OSD_THROTTLE_POS:
2335 osdFormatThrottlePosition(buff, false, &elemAttr);
2336 break;
2339 case OSD_VTX_CHANNEL:
2341 vtxDeviceOsdInfo_t osdInfo;
2342 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2344 tfp_sprintf(buff, "CH:%c%s:", osdInfo.bandLetter, osdInfo.channelName);
2345 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2347 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2348 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2349 displayWriteWithAttr(osdDisplayPort, elemPosX + 6, elemPosY, buff, elemAttr);
2350 return true;
2352 break;
2354 case OSD_VTX_POWER:
2356 vtxDeviceOsdInfo_t osdInfo;
2357 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2359 tfp_sprintf(buff, "%c", SYM_VTX_POWER);
2360 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2362 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2363 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2364 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2365 return true;
2368 #if defined(USE_SERIALRX_CRSF)
2369 case OSD_CRSF_RSSI_DBM:
2371 int16_t rssi = rxLinkStatistics.uplinkRSSI;
2372 buff[0] = (rxLinkStatistics.activeAntenna == 0) ? SYM_RSSI : SYM_2RSS; // Separate symbols for each antenna
2373 if (rssi <= -100) {
2374 tfp_sprintf(buff + 1, "%4d%c", rssi, SYM_DBM);
2375 } else {
2376 tfp_sprintf(buff + 1, "%3d%c%c", rssi, SYM_DBM, ' ');
2378 if (!failsafeIsReceivingRxData()){
2379 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2380 } else if (osdConfig()->rssi_dbm_alarm && rssi < osdConfig()->rssi_dbm_alarm) {
2381 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2383 break;
2385 case OSD_CRSF_LQ:
2387 buff[0] = SYM_LQ;
2388 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
2389 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
2390 switch (osdConfig()->crsf_lq_format) {
2391 case OSD_CRSF_LQ_TYPE1:
2392 if (!failsafeIsReceivingRxData()) {
2393 tfp_sprintf(buff+1, "%3d", 0);
2394 } else {
2395 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.uplinkLQ);
2397 break;
2398 case OSD_CRSF_LQ_TYPE2:
2399 if (!failsafeIsReceivingRxData()) {
2400 tfp_sprintf(buff+1, "%s:%3d", " ", 0);
2401 } else {
2402 tfp_sprintf(buff+1, "%d:%3d", rxLinkStatistics.rfMode, rxLinkStatistics.uplinkLQ);
2404 break;
2405 case OSD_CRSF_LQ_TYPE3:
2406 if (!failsafeIsReceivingRxData()) {
2407 tfp_sprintf(buff+1, "%3d", 0);
2408 } else {
2409 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.rfMode >= 2 ? scaledLQ : rxLinkStatistics.uplinkLQ);
2411 break;
2413 if (!failsafeIsReceivingRxData()) {
2414 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2415 } else if (rxLinkStatistics.uplinkLQ < osdConfig()->link_quality_alarm) {
2416 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2418 break;
2421 case OSD_CRSF_SNR_DB:
2423 static pt1Filter_t snrFilterState;
2424 static timeMs_t snrUpdated = 0;
2425 int8_t snrFiltered = pt1FilterApply4(&snrFilterState, rxLinkStatistics.uplinkSNR, 0.5f, MS2S(millis() - snrUpdated));
2426 snrUpdated = millis();
2428 const char* showsnr = "-20";
2429 const char* hidesnr = " ";
2430 if (snrFiltered > osdConfig()->snr_alarm) {
2431 if (cmsInMenu) {
2432 buff[0] = SYM_SNR;
2433 tfp_sprintf(buff + 1, "%s%c", showsnr, SYM_DB);
2434 } else {
2435 buff[0] = SYM_BLANK;
2436 tfp_sprintf(buff + 1, "%s%c", hidesnr, SYM_BLANK);
2438 } else if (snrFiltered <= osdConfig()->snr_alarm) {
2439 buff[0] = SYM_SNR;
2440 if (snrFiltered <= -10) {
2441 tfp_sprintf(buff + 1, "%3d%c", snrFiltered, SYM_DB);
2442 } else {
2443 tfp_sprintf(buff + 1, "%2d%c%c", snrFiltered, SYM_DB, ' ');
2446 break;
2449 case OSD_CRSF_TX_POWER:
2451 if (!failsafeIsReceivingRxData())
2452 tfp_sprintf(buff, "%s%c", " ", SYM_BLANK);
2453 else
2454 tfp_sprintf(buff, "%4d%c", rxLinkStatistics.uplinkTXPower, SYM_MW);
2455 break;
2457 #endif
2459 case OSD_CROSSHAIRS: // Hud is a sub-element of the crosshair
2461 osdCrosshairPosition(&elemPosX, &elemPosY);
2462 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX, elemPosY);
2464 if (osdConfig()->hud_homing && STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
2465 osdHudDrawHoming(elemPosX, elemPosY);
2468 if (STATE(GPS_FIX) && isImuHeadingValid()) {
2470 if (osdConfig()->hud_homepoint || osdConfig()->hud_radar_disp > 0 || osdConfig()->hud_wp_disp > 0) {
2471 osdHudClear();
2474 // -------- POI : Home point
2476 if (osdConfig()->hud_homepoint) { // Display the home point (H)
2477 osdHudDrawPoi(GPS_distanceToHome, GPS_directionToHome, -osdGetAltitude() / 100, 0, SYM_HOME, 0 , 0);
2480 // -------- POI : Nearby aircrafts from ESP32 radar
2482 if (osdConfig()->hud_radar_disp > 0) { // Display the POI from the radar
2483 for (uint8_t i = 0; i < osdConfig()->hud_radar_disp; i++) {
2484 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
2485 fpVector3_t poi;
2486 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &radar_pois[i].gps, GEO_ALT_RELATIVE);
2487 radar_pois[i].distance = calculateDistanceToDestination(&poi) / 100; // In meters
2489 if (radar_pois[i].distance >= osdConfig()->hud_radar_range_min && radar_pois[i].distance <= osdConfig()->hud_radar_range_max) {
2490 radar_pois[i].direction = calculateBearingToDestination(&poi) / 100; // In °
2491 radar_pois[i].altitude = (radar_pois[i].gps.alt - osdGetAltitudeMsl()) / 100;
2492 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);
2498 // -------- POI : Next waypoints from navigation
2500 if (osdConfig()->hud_wp_disp > 0 && posControl.waypointListValid && posControl.waypointCount > 0) { // Display the next waypoints
2501 gpsLocation_t wp2;
2502 int j;
2504 for (int i = osdConfig()->hud_wp_disp - 1; i >= 0 ; i--) { // Display in reverse order so the next WP is always written on top
2505 j = posControl.activeWaypointIndex + i;
2506 if (j > posControl.startWpIndex + posControl.waypointCount - 1) { // limit to max WP index for mission
2507 break;
2509 if (posControl.waypointList[j].lat != 0 && posControl.waypointList[j].lon != 0) {
2510 wp2.lat = posControl.waypointList[j].lat;
2511 wp2.lon = posControl.waypointList[j].lon;
2512 wp2.alt = posControl.waypointList[j].alt;
2513 fpVector3_t poi;
2514 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &wp2, waypointMissionAltConvMode(posControl.waypointList[j].p3));
2515 int32_t altConvModeAltitude = waypointMissionAltConvMode(posControl.waypointList[j].p3) == GEO_ALT_ABSOLUTE ? osdGetAltitudeMsl() : osdGetAltitude();
2516 j = getGeoWaypointNumber(j);
2517 while (j > 9) j -= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2518 osdHudDrawPoi(calculateDistanceToDestination(&poi) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi) / 100), (posControl.waypointList[j].alt - altConvModeAltitude)/ 100, 2, SYM_WAYPOINT, 48 + j, i);
2524 return true;
2525 break;
2527 case OSD_ATTITUDE_ROLL:
2528 buff[0] = SYM_ROLL_LEVEL;
2529 if (ABS(attitude.values.roll) >= 1)
2530 buff[0] += (attitude.values.roll < 0 ? -1 : 1);
2531 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.roll)), 0, 1, 0, 3, false);
2532 break;
2534 case OSD_ATTITUDE_PITCH:
2535 if (ABS(attitude.values.pitch) < 1)
2536 buff[0] = 'P';
2537 else if (attitude.values.pitch > 0)
2538 buff[0] = SYM_PITCH_DOWN;
2539 else if (attitude.values.pitch < 0)
2540 buff[0] = SYM_PITCH_UP;
2541 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.pitch)), 0, 1, 0, 3, false);
2542 break;
2544 case OSD_ARTIFICIAL_HORIZON:
2546 float rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2547 float pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2549 pitchAngle -= osdConfig()->ahi_camera_uptilt_comp ? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt) : 0;
2550 pitchAngle += DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2551 if (osdConfig()->ahi_reverse_roll) {
2552 rollAngle = -rollAngle;
2554 osdDrawArtificialHorizon(osdDisplayPort, osdGetDisplayPortCanvas(),
2555 OSD_DRAW_POINT_GRID(elemPosX, elemPosY), rollAngle, pitchAngle);
2556 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
2557 osdDrawSingleElement(OSD_CROSSHAIRS);
2559 return true;
2562 case OSD_HORIZON_SIDEBARS:
2564 osdDrawSidebars(osdDisplayPort, osdGetDisplayPortCanvas());
2565 return true;
2568 #if defined(USE_BARO) || defined(USE_GPS)
2569 case OSD_VARIO:
2571 float zvel = getEstimatedActualVelocity(Z);
2572 osdDrawVario(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), zvel);
2573 return true;
2576 case OSD_VARIO_NUM:
2578 int16_t value = getEstimatedActualVelocity(Z);
2579 char sym;
2580 switch ((osd_unit_e)osdConfig()->units) {
2581 case OSD_UNIT_UK:
2582 FALLTHROUGH;
2583 case OSD_UNIT_IMPERIAL:
2584 // Convert to centifeet/s
2585 value = CENTIMETERS_TO_CENTIFEET(value);
2586 sym = SYM_FTS;
2587 break;
2588 case OSD_UNIT_GA:
2589 // Convert to centi-100feet/min
2590 value = CENTIMETERS_TO_FEET(value * 60);
2591 sym = SYM_100FTM;
2592 break;
2593 default:
2594 case OSD_UNIT_METRIC_MPH:
2595 FALLTHROUGH;
2596 case OSD_UNIT_METRIC:
2597 // Already in cm/s
2598 sym = SYM_MS;
2599 break;
2602 osdFormatCentiNumber(buff, value, 0, 1, 0, 3, false);
2603 buff[3] = sym;
2604 buff[4] = '\0';
2605 break;
2607 case OSD_CLIMB_EFFICIENCY:
2609 // amperage is in centi amps (10mA), vertical speed is in cms/s. We want
2610 // Ah/dist only to show when vertical speed > 1m/s.
2611 static pt1Filter_t veFilterState;
2612 static timeUs_t vEfficiencyUpdated = 0;
2613 int32_t value = 0;
2614 timeUs_t currentTimeUs = micros();
2615 timeDelta_t vEfficiencyTimeDelta = cmpTimeUs(currentTimeUs, vEfficiencyUpdated);
2616 if (getEstimatedActualVelocity(Z) > 0) {
2617 if (vEfficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2618 // Centiamps (kept for osdFormatCentiNumber) / m/s - Will appear as A / m/s in OSD
2619 value = pt1FilterApply4(&veFilterState, (float)getAmperage() / (getEstimatedActualVelocity(Z) / 100.0f), 1, US2S(vEfficiencyTimeDelta));
2621 vEfficiencyUpdated = currentTimeUs;
2622 } else {
2623 value = veFilterState.state;
2626 bool efficiencyValid = (value > 0) && (getEstimatedActualVelocity(Z) > 100);
2627 switch (osdConfig()->units) {
2628 case OSD_UNIT_UK:
2629 FALLTHROUGH;
2630 case OSD_UNIT_GA:
2631 FALLTHROUGH;
2632 case OSD_UNIT_IMPERIAL:
2633 // mAh/foot
2634 if (efficiencyValid) {
2635 osdFormatCentiNumber(buff, (value * METERS_PER_FOOT), 1, 2, 2, 3, false);
2636 tfp_sprintf(buff, "%s%c%c", buff, SYM_AH_V_FT_0, SYM_AH_V_FT_1);
2637 } else {
2638 buff[0] = buff[1] = buff[2] = '-';
2639 buff[3] = SYM_AH_V_FT_0;
2640 buff[4] = SYM_AH_V_FT_1;
2641 buff[5] = '\0';
2643 break;
2644 case OSD_UNIT_METRIC_MPH:
2645 FALLTHROUGH;
2646 case OSD_UNIT_METRIC:
2647 // mAh/metre
2648 if (efficiencyValid) {
2649 osdFormatCentiNumber(buff, value, 1, 2, 2, 3, false);
2650 tfp_sprintf(buff, "%s%c%c", buff, SYM_AH_V_M_0, SYM_AH_V_M_1);
2651 } else {
2652 buff[0] = buff[1] = buff[2] = '-';
2653 buff[3] = SYM_AH_V_M_0;
2654 buff[4] = SYM_AH_V_M_1;
2655 buff[5] = '\0';
2657 break;
2659 break;
2661 case OSD_GLIDE_TIME_REMAINING:
2663 uint16_t glideTime = osdGetRemainingGlideTime();
2664 buff[0] = SYM_GLIDE_MINS;
2665 if (glideTime > 0) {
2666 // Maximum value we can show in minutes is 99 minutes and 59 seconds. It is extremely unlikely that glide
2667 // time will be longer than 99 minutes. If it is, it will show 99:^^
2668 if (glideTime > (99 * 60) + 59) {
2669 tfp_sprintf(buff + 1, "%02d:", (int)(glideTime / 60));
2670 buff[4] = SYM_DIRECTION;
2671 buff[5] = SYM_DIRECTION;
2672 } else {
2673 tfp_sprintf(buff + 1, "%02d:%02d", (int)(glideTime / 60), (int)(glideTime % 60));
2675 } else {
2676 tfp_sprintf(buff + 1, "%s", "--:--");
2678 buff[6] = '\0';
2679 break;
2681 case OSD_GLIDE_RANGE:
2683 uint16_t glideSeconds = osdGetRemainingGlideTime();
2684 buff[0] = SYM_GLIDE_DIST;
2685 if (glideSeconds > 0) {
2686 uint32_t glideRangeCM = glideSeconds * gpsSol.groundSpeed;
2687 osdFormatDistanceSymbol(buff + 1, glideRangeCM, 0);
2688 } else {
2689 tfp_sprintf(buff + 1, "%s%c", "---", SYM_BLANK);
2690 buff[5] = '\0';
2692 break;
2694 #endif
2696 case OSD_SWITCH_INDICATOR_0:
2697 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channel - 1), buff);
2698 break;
2700 case OSD_SWITCH_INDICATOR_1:
2701 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channel - 1), buff);
2702 break;
2704 case OSD_SWITCH_INDICATOR_2:
2705 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channel - 1), buff);
2706 break;
2708 case OSD_SWITCH_INDICATOR_3:
2709 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channel - 1), buff);
2710 break;
2712 case OSD_PAN_SERVO_CENTRED:
2714 int16_t panOffset = osdGetPanServoOffset();
2715 const timeMs_t panServoTimeNow = millis();
2716 static timeMs_t panServoTimeOffCentre = 0;
2718 if (panOffset < 0) {
2719 if (osdConfig()->pan_servo_offcentre_warning != 0 && panOffset >= -osdConfig()->pan_servo_offcentre_warning) {
2720 if (panServoTimeOffCentre == 0) {
2721 panServoTimeOffCentre = panServoTimeNow;
2722 } else if (panServoTimeNow >= (panServoTimeOffCentre + 10000 )) {
2723 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2725 } else {
2726 panServoTimeOffCentre = 0;
2729 if (osdConfig()->pan_servo_indicator_show_degrees) {
2730 tfp_sprintf(buff, "%3d%c", -panOffset, SYM_DEGREES);
2731 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2733 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_OFFSET_R, elemAttr);
2734 } else if (panOffset > 0) {
2735 if (osdConfig()->pan_servo_offcentre_warning != 0 && panOffset <= osdConfig()->pan_servo_offcentre_warning) {
2736 if (panServoTimeOffCentre == 0) {
2737 panServoTimeOffCentre = panServoTimeNow;
2738 } else if (panServoTimeNow >= (panServoTimeOffCentre + 10000 )) {
2739 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2741 } else {
2742 panServoTimeOffCentre = 0;
2745 if (osdConfig()->pan_servo_indicator_show_degrees) {
2746 tfp_sprintf(buff, "%3d%c", panOffset, SYM_DEGREES);
2747 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2749 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_OFFSET_L, elemAttr);
2750 } else {
2751 panServoTimeOffCentre = 0;
2753 if (osdConfig()->pan_servo_indicator_show_degrees) {
2754 tfp_sprintf(buff, "%3d%c", panOffset, SYM_DEGREES);
2755 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2757 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_CENTRED);
2760 return true;
2762 break;
2764 case OSD_ACTIVE_PROFILE:
2765 tfp_sprintf(buff, "%c%u", SYM_PROFILE, (getConfigProfile() + 1));
2766 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2767 break;
2769 case OSD_ROLL_PIDS:
2770 osdDisplayFlightPIDValues(elemPosX, elemPosY, "ROL", PID_ROLL, ADJUSTMENT_ROLL_P, ADJUSTMENT_ROLL_I, ADJUSTMENT_ROLL_D, ADJUSTMENT_ROLL_FF);
2771 return true;
2773 case OSD_PITCH_PIDS:
2774 osdDisplayFlightPIDValues(elemPosX, elemPosY, "PIT", PID_PITCH, ADJUSTMENT_PITCH_P, ADJUSTMENT_PITCH_I, ADJUSTMENT_PITCH_D, ADJUSTMENT_PITCH_FF);
2775 return true;
2777 case OSD_YAW_PIDS:
2778 osdDisplayFlightPIDValues(elemPosX, elemPosY, "YAW", PID_YAW, ADJUSTMENT_YAW_P, ADJUSTMENT_YAW_I, ADJUSTMENT_YAW_D, ADJUSTMENT_YAW_FF);
2779 return true;
2781 case OSD_LEVEL_PIDS:
2782 osdDisplayNavPIDValues(elemPosX, elemPosY, "LEV", PID_LEVEL, ADJUSTMENT_LEVEL_P, ADJUSTMENT_LEVEL_I, ADJUSTMENT_LEVEL_D);
2783 return true;
2785 case OSD_POS_XY_PIDS:
2786 osdDisplayNavPIDValues(elemPosX, elemPosY, "PXY", PID_POS_XY, ADJUSTMENT_POS_XY_P, ADJUSTMENT_POS_XY_I, ADJUSTMENT_POS_XY_D);
2787 return true;
2789 case OSD_POS_Z_PIDS:
2790 osdDisplayNavPIDValues(elemPosX, elemPosY, "PZ", PID_POS_Z, ADJUSTMENT_POS_Z_P, ADJUSTMENT_POS_Z_I, ADJUSTMENT_POS_Z_D);
2791 return true;
2793 case OSD_VEL_XY_PIDS:
2794 osdDisplayNavPIDValues(elemPosX, elemPosY, "VXY", PID_VEL_XY, ADJUSTMENT_VEL_XY_P, ADJUSTMENT_VEL_XY_I, ADJUSTMENT_VEL_XY_D);
2795 return true;
2797 case OSD_VEL_Z_PIDS:
2798 osdDisplayNavPIDValues(elemPosX, elemPosY, "VZ", PID_VEL_Z, ADJUSTMENT_VEL_Z_P, ADJUSTMENT_VEL_Z_I, ADJUSTMENT_VEL_Z_D);
2799 return true;
2801 case OSD_HEADING_P:
2802 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "HP", 0, pidBank()->pid[PID_HEADING].P, 3, 0, ADJUSTMENT_HEADING_P);
2803 return true;
2805 case OSD_BOARD_ALIGN_ROLL:
2806 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT);
2807 return true;
2809 case OSD_BOARD_ALIGN_PITCH:
2810 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT);
2811 return true;
2813 case OSD_RC_EXPO:
2814 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "EXP", 0, currentControlRateProfile->stabilized.rcExpo8, 3, 0, ADJUSTMENT_RC_EXPO);
2815 return true;
2817 case OSD_RC_YAW_EXPO:
2818 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "YEX", 0, currentControlRateProfile->stabilized.rcYawExpo8, 3, 0, ADJUSTMENT_RC_YAW_EXPO);
2819 return true;
2821 case OSD_THROTTLE_EXPO:
2822 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TEX", 0, currentControlRateProfile->throttle.rcExpo8, 3, 0, ADJUSTMENT_THROTTLE_EXPO);
2823 return true;
2825 case OSD_PITCH_RATE:
2826 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SPR");
2828 elemAttr = TEXT_ATTRIBUTES_NONE;
2829 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_PITCH]);
2830 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2831 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2832 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2833 return true;
2835 case OSD_ROLL_RATE:
2836 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SRR");
2838 elemAttr = TEXT_ATTRIBUTES_NONE;
2839 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_ROLL]);
2840 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2841 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2842 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2843 return true;
2845 case OSD_YAW_RATE:
2846 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "SYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2847 return true;
2849 case OSD_MANUAL_RC_EXPO:
2850 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MEX", 0, currentControlRateProfile->manual.rcExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO);
2851 return true;
2853 case OSD_MANUAL_RC_YAW_EXPO:
2854 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYX", 0, currentControlRateProfile->manual.rcYawExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO);
2855 return true;
2857 case OSD_MANUAL_PITCH_RATE:
2858 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MPR");
2860 elemAttr = TEXT_ATTRIBUTES_NONE;
2861 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_PITCH]);
2862 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2863 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2864 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2865 return true;
2867 case OSD_MANUAL_ROLL_RATE:
2868 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MRR");
2870 elemAttr = TEXT_ATTRIBUTES_NONE;
2871 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_ROLL]);
2872 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2873 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2874 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2875 return true;
2877 case OSD_MANUAL_YAW_RATE:
2878 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2879 return true;
2881 case OSD_NAV_FW_CRUISE_THR:
2882 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CRZ", 0, currentBatteryProfile->nav.fw.cruise_throttle, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR);
2883 return true;
2885 case OSD_NAV_FW_PITCH2THR:
2886 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "P2T", 0, currentBatteryProfile->nav.fw.pitch_to_throttle, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR);
2887 return true;
2889 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE:
2890 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "0TP", 0, (float)navConfig()->fw.minThrottleDownPitchAngle / 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE);
2891 return true;
2893 case OSD_FW_ALT_PID_OUTPUTS:
2895 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2896 osdFormatPidControllerOutput(buff, "PZO", &nav_pids->fw_alt, 10, true); // display requested pitch degrees
2897 break;
2900 case OSD_FW_POS_PID_OUTPUTS:
2902 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers(); // display requested roll degrees
2903 osdFormatPidControllerOutput(buff, "PXYO", &nav_pids->fw_nav, 1, true);
2904 break;
2907 case OSD_MC_VEL_Z_PID_OUTPUTS:
2909 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2910 osdFormatPidControllerOutput(buff, "VZO", &nav_pids->vel[Z], 100, false); // display throttle adjustment µs
2911 break;
2914 case OSD_MC_VEL_X_PID_OUTPUTS:
2916 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2917 osdFormatPidControllerOutput(buff, "VXO", &nav_pids->vel[X], 100, false); // display requested acceleration cm/s^2
2918 break;
2921 case OSD_MC_VEL_Y_PID_OUTPUTS:
2923 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2924 osdFormatPidControllerOutput(buff, "VYO", &nav_pids->vel[Y], 100, false); // display requested acceleration cm/s^2
2925 break;
2928 case OSD_MC_POS_XYZ_P_OUTPUTS:
2930 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2931 strcpy(buff, "POSO ");
2932 // display requested velocity cm/s
2933 tfp_sprintf(buff + 5, "%4d", (int)lrintf(nav_pids->pos[X].output_constrained * 100));
2934 buff[9] = ' ';
2935 tfp_sprintf(buff + 10, "%4d", (int)lrintf(nav_pids->pos[Y].output_constrained * 100));
2936 buff[14] = ' ';
2937 tfp_sprintf(buff + 15, "%4d", (int)lrintf(nav_pids->pos[Z].output_constrained * 100));
2938 buff[19] = '\0';
2939 break;
2942 case OSD_POWER:
2944 bool kiloWatt = osdFormatCentiNumber(buff, getPower(), 1000, 2, 2, 3, false);
2945 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
2946 buff[4] = '\0';
2948 uint8_t current_alarm = osdConfig()->current_alarm;
2949 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
2950 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2952 break;
2955 case OSD_AIR_SPEED:
2957 #ifdef USE_PITOT
2958 buff[0] = SYM_AIR;
2960 if (pitotIsHealthy())
2962 const float airspeed_estimate = getAirspeedEstimate();
2963 osdFormatVelocityStr(buff + 1, airspeed_estimate, false, false);
2964 if ((osdConfig()->airspeed_alarm_min != 0 && airspeed_estimate < osdConfig()->airspeed_alarm_min) ||
2965 (osdConfig()->airspeed_alarm_max != 0 && airspeed_estimate > osdConfig()->airspeed_alarm_max)) {
2966 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2969 else
2971 strcpy(buff + 1, " X!");
2972 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2974 #else
2975 return false;
2976 #endif
2977 break;
2980 case OSD_AIR_MAX_SPEED:
2982 #ifdef USE_PITOT
2983 buff[0] = SYM_MAX;
2984 buff[1] = SYM_AIR;
2985 osdFormatVelocityStr(buff + 2, stats.max_air_speed, false, false);
2986 #else
2987 return false;
2988 #endif
2989 break;
2992 case OSD_RTC_TIME:
2994 // RTC not configured will show 00:00
2995 dateTime_t dateTime;
2996 rtcGetDateTimeLocal(&dateTime);
2997 buff[0] = SYM_CLOCK;
2998 tfp_sprintf(buff + 1, "%02u:%02u:%02u", dateTime.hours, dateTime.minutes, dateTime.seconds);
2999 break;
3002 case OSD_MESSAGES:
3004 elemAttr = osdGetSystemMessage(buff, OSD_MESSAGE_LENGTH, true);
3005 break;
3008 case OSD_VERSION:
3010 tfp_sprintf(buff, "INAV %s", FC_VERSION_STRING);
3011 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3012 break;
3015 case OSD_MAIN_BATT_CELL_VOLTAGE:
3017 uint8_t base_digits = 3U;
3018 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
3019 if(isBfCompatibleVideoSystem(osdConfig())) {
3020 base_digits = 4U; // Add extra digit to account for decimal point taking an extra character space
3022 #endif
3023 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawAverageCellVoltage(), base_digits, 2);
3024 return true;
3027 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE:
3029 uint8_t base_digits = 3U;
3030 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
3031 if(isBfCompatibleVideoSystem(osdConfig())) {
3032 base_digits = 4U; // Add extra digit to account for decimal point taking an extra character space
3034 #endif
3035 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedAverageCellVoltage(), base_digits, 2);
3036 return true;
3039 case OSD_SCALED_THROTTLE_POS:
3041 osdFormatThrottlePosition(buff, true, &elemAttr);
3042 break;
3045 case OSD_HEADING:
3047 buff[0] = SYM_HEADING;
3048 if (osdIsHeadingValid()) {
3049 int16_t h = DECIDEGREES_TO_DEGREES(osdGetHeading());
3050 if (h < 0) {
3051 h += 360;
3053 tfp_sprintf(&buff[1], "%3d", h);
3054 } else {
3055 buff[1] = buff[2] = buff[3] = '-';
3057 buff[4] = SYM_DEGREES;
3058 buff[5] = '\0';
3059 break;
3062 case OSD_HEADING_GRAPH:
3064 if (osdIsHeadingValid()) {
3065 osdDrawHeadingGraph(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), osdGetHeading());
3066 return true;
3067 } else {
3068 buff[0] = buff[2] = buff[4] = buff[6] = buff[8] = SYM_HEADING_LINE;
3069 buff[1] = buff[3] = buff[5] = buff[7] = SYM_HEADING_DIVIDED_LINE;
3070 buff[OSD_HEADING_GRAPH_WIDTH] = '\0';
3072 break;
3075 case OSD_EFFICIENCY_MAH_PER_KM:
3077 // amperage is in centi amps, speed is in cms/s. We want
3078 // mah/km. Only show when ground speed > 1m/s.
3079 static pt1Filter_t eFilterState;
3080 static timeUs_t efficiencyUpdated = 0;
3081 int32_t value = 0;
3082 bool moreThanAh = false;
3083 timeUs_t currentTimeUs = micros();
3084 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
3085 uint8_t digits = 3U;
3086 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
3087 if (isBfCompatibleVideoSystem(osdConfig())) {
3088 // Increase number of digits so values above 99 don't get scaled by osdFormatCentiNumber
3089 digits = 4U;
3091 #endif
3092 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
3093 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
3094 value = pt1FilterApply4(&eFilterState, ((float)getAmperage() / gpsSol.groundSpeed) / 0.0036f,
3095 1, US2S(efficiencyTimeDelta));
3097 efficiencyUpdated = currentTimeUs;
3098 } else {
3099 value = eFilterState.state;
3102 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
3103 switch (osdConfig()->units) {
3104 case OSD_UNIT_UK:
3105 FALLTHROUGH;
3106 case OSD_UNIT_IMPERIAL:
3107 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10, 1000, 0, 2, digits, false);
3108 if (!moreThanAh) {
3109 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
3110 } else {
3111 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
3113 if (!efficiencyValid) {
3114 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3115 buff[digits] = SYM_MAH_MI_0; // This will overwrite the "-" at buff[3] if not in BFCOMPAT mode
3116 buff[digits + 1] = SYM_MAH_MI_1;
3117 buff[digits + 2] = '\0';
3119 break;
3120 case OSD_UNIT_GA:
3121 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10, 1000, 0, 2, digits, false);
3122 if (!moreThanAh) {
3123 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
3124 } else {
3125 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
3127 if (!efficiencyValid) {
3128 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3129 buff[digits] = SYM_MAH_NM_0;
3130 buff[digits + 1] = SYM_MAH_NM_1;
3131 buff[digits + 2] = '\0';
3133 break;
3134 case OSD_UNIT_METRIC_MPH:
3135 FALLTHROUGH;
3136 case OSD_UNIT_METRIC:
3137 moreThanAh = osdFormatCentiNumber(buff, value * 100, 1000, 0, 2, digits, false);
3138 if (!moreThanAh) {
3139 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
3140 } else {
3141 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
3143 if (!efficiencyValid) {
3144 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3145 buff[digits] = SYM_MAH_KM_0;
3146 buff[digits + 1] = SYM_MAH_KM_1;
3147 buff[digits + 2] = '\0';
3149 break;
3151 break;
3154 case OSD_EFFICIENCY_WH_PER_KM:
3156 // amperage is in centi amps, speed is in cms/s. We want
3157 // mWh/km. Only show when ground speed > 1m/s.
3158 static pt1Filter_t eFilterState;
3159 static timeUs_t efficiencyUpdated = 0;
3160 int32_t value = 0;
3161 timeUs_t currentTimeUs = micros();
3162 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
3163 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
3164 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
3165 value = pt1FilterApply4(&eFilterState, ((float)getPower() / gpsSol.groundSpeed) / 0.0036f,
3166 1, US2S(efficiencyTimeDelta));
3168 efficiencyUpdated = currentTimeUs;
3169 } else {
3170 value = eFilterState.state;
3173 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
3174 switch (osdConfig()->units) {
3175 case OSD_UNIT_UK:
3176 FALLTHROUGH;
3177 case OSD_UNIT_IMPERIAL:
3178 osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10000, 0, 2, 0, 3, false);
3179 buff[3] = SYM_WH_MI;
3180 break;
3181 case OSD_UNIT_GA:
3182 osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10000, 0, 2, 0, 3, false);
3183 buff[3] = SYM_WH_NM;
3184 break;
3185 case OSD_UNIT_METRIC_MPH:
3186 FALLTHROUGH;
3187 case OSD_UNIT_METRIC:
3188 osdFormatCentiNumber(buff, value / 10, 0, 2, 0, 3, false);
3189 buff[3] = SYM_WH_KM;
3190 break;
3192 buff[4] = '\0';
3193 if (!efficiencyValid) {
3194 buff[0] = buff[1] = buff[2] = '-';
3196 break;
3199 case OSD_GFORCE:
3201 buff[0] = SYM_GFORCE;
3202 osdFormatCentiNumber(buff + 1, GForce, 0, 2, 0, 3, false);
3203 if (GForce > osdConfig()->gforce_alarm * 100) {
3204 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3206 break;
3209 case OSD_GFORCE_X:
3210 case OSD_GFORCE_Y:
3211 case OSD_GFORCE_Z:
3213 float GForceValue = GForceAxis[item - OSD_GFORCE_X];
3214 buff[0] = SYM_GFORCE_X + item - OSD_GFORCE_X;
3215 osdFormatCentiNumber(buff + 1, GForceValue, 0, 2, 0, 4, false);
3216 if ((GForceValue < osdConfig()->gforce_axis_alarm_min * 100) || (GForceValue > osdConfig()->gforce_axis_alarm_max * 100)) {
3217 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3219 break;
3221 case OSD_DEBUG:
3224 * Longest representable string is -2147483648 does not fit in the screen.
3225 * Only 7 digits for negative and 8 digits for positive values allowed
3227 for (uint8_t bufferIndex = 0; bufferIndex < DEBUG32_VALUE_COUNT; ++elemPosY, bufferIndex += 2) {
3228 tfp_sprintf(
3229 buff,
3230 "[%u]=%8ld [%u]=%8ld",
3231 bufferIndex,
3232 (long)constrain(debug[bufferIndex], -9999999, 99999999),
3233 bufferIndex+1,
3234 (long)constrain(debug[bufferIndex+1], -9999999, 99999999)
3236 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3238 break;
3241 case OSD_IMU_TEMPERATURE:
3243 int16_t temperature;
3244 const bool valid = getIMUTemperature(&temperature);
3245 osdDisplayTemperature(elemPosX, elemPosY, SYM_IMU_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
3246 return true;
3249 case OSD_BARO_TEMPERATURE:
3251 int16_t temperature;
3252 const bool valid = getBaroTemperature(&temperature);
3253 osdDisplayTemperature(elemPosX, elemPosY, SYM_BARO_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
3254 return true;
3257 #ifdef USE_TEMPERATURE_SENSOR
3258 case OSD_TEMP_SENSOR_0_TEMPERATURE:
3259 case OSD_TEMP_SENSOR_1_TEMPERATURE:
3260 case OSD_TEMP_SENSOR_2_TEMPERATURE:
3261 case OSD_TEMP_SENSOR_3_TEMPERATURE:
3262 case OSD_TEMP_SENSOR_4_TEMPERATURE:
3263 case OSD_TEMP_SENSOR_5_TEMPERATURE:
3264 case OSD_TEMP_SENSOR_6_TEMPERATURE:
3265 case OSD_TEMP_SENSOR_7_TEMPERATURE:
3267 osdDisplayTemperatureSensor(elemPosX, elemPosY, item - OSD_TEMP_SENSOR_0_TEMPERATURE);
3268 return true;
3270 #endif /* ifdef USE_TEMPERATURE_SENSOR */
3272 case OSD_WIND_SPEED_HORIZONTAL:
3273 #ifdef USE_WIND_ESTIMATOR
3275 bool valid = isEstimatedWindSpeedValid();
3276 float horizontalWindSpeed;
3277 uint16_t angle;
3278 horizontalWindSpeed = getEstimatedHorizontalWindSpeed(&angle);
3279 int16_t windDirection = osdGetHeadingAngle( CENTIDEGREES_TO_DEGREES((int)angle) - DECIDEGREES_TO_DEGREES(attitude.values.yaw) + 22);
3280 buff[0] = SYM_WIND_HORIZONTAL;
3281 buff[1] = SYM_DIRECTION + (windDirection*2 / 90);
3282 osdFormatWindSpeedStr(buff + 2, horizontalWindSpeed, valid);
3283 break;
3285 #else
3286 return false;
3287 #endif
3289 case OSD_WIND_SPEED_VERTICAL:
3290 #ifdef USE_WIND_ESTIMATOR
3292 buff[0] = SYM_WIND_VERTICAL;
3293 buff[1] = SYM_BLANK;
3294 bool valid = isEstimatedWindSpeedValid();
3295 float verticalWindSpeed;
3296 verticalWindSpeed = -getEstimatedWindSpeed(Z); //from NED to NEU
3297 if (verticalWindSpeed < 0) {
3298 buff[1] = SYM_AH_DIRECTION_DOWN;
3299 verticalWindSpeed = -verticalWindSpeed;
3300 } else {
3301 buff[1] = SYM_AH_DIRECTION_UP;
3303 osdFormatWindSpeedStr(buff + 2, verticalWindSpeed, valid);
3304 break;
3306 #else
3307 return false;
3308 #endif
3310 case OSD_PLUS_CODE:
3312 STATIC_ASSERT(GPS_DEGREES_DIVIDER == OLC_DEG_MULTIPLIER, invalid_olc_deg_multiplier);
3313 int digits = osdConfig()->plus_code_digits;
3314 int digitsRemoved = osdConfig()->plus_code_short * 2;
3315 if (STATE(GPS_FIX)) {
3316 olc_encode(gpsSol.llh.lat, gpsSol.llh.lon, digits, buff, sizeof(buff));
3317 } else {
3318 // +codes with > 8 digits have a + at the 9th digit
3319 // and we only support 10 and up.
3320 memset(buff, '-', digits + 1);
3321 buff[8] = '+';
3322 buff[digits + 1] = '\0';
3324 // Optionally trim digits from the left
3325 memmove(buff, buff+digitsRemoved, strlen(buff) + digitsRemoved);
3326 buff[digits + 1 - digitsRemoved] = '\0';
3327 break;
3330 case OSD_AZIMUTH:
3333 buff[0] = SYM_AZIMUTH;
3334 if (osdIsHeadingValid()) {
3335 int16_t h = GPS_directionToHome;
3336 if (h < 0) {
3337 h += 360;
3339 if (h >= 180)
3340 h = h - 180;
3341 else
3342 h = h + 180;
3344 tfp_sprintf(&buff[1], "%3d", h);
3345 } else {
3346 buff[1] = buff[2] = buff[3] = '-';
3348 buff[4] = SYM_DEGREES;
3349 buff[5] = '\0';
3350 break;
3353 case OSD_MAP_SCALE:
3355 float scaleToUnit;
3356 int scaleUnitDivisor;
3357 char symUnscaled;
3358 char symScaled;
3359 int maxDecimals;
3361 switch (osdConfig()->units) {
3362 case OSD_UNIT_UK:
3363 FALLTHROUGH;
3364 case OSD_UNIT_IMPERIAL:
3365 scaleToUnit = 100 / 1609.3440f; // scale to 0.01mi for osdFormatCentiNumber()
3366 scaleUnitDivisor = 0;
3367 symUnscaled = SYM_MI;
3368 symScaled = SYM_MI;
3369 maxDecimals = 2;
3370 break;
3371 case OSD_UNIT_GA:
3372 scaleToUnit = 100 / 1852.0010f; // scale to 0.01mi for osdFormatCentiNumber()
3373 scaleUnitDivisor = 0;
3374 symUnscaled = SYM_NM;
3375 symScaled = SYM_NM;
3376 maxDecimals = 2;
3377 break;
3378 default:
3379 case OSD_UNIT_METRIC_MPH:
3380 FALLTHROUGH;
3381 case OSD_UNIT_METRIC:
3382 scaleToUnit = 100; // scale to cm for osdFormatCentiNumber()
3383 scaleUnitDivisor = 1000; // Convert to km when scale gets bigger than 999m
3384 symUnscaled = SYM_M;
3385 symScaled = SYM_KM;
3386 maxDecimals = 0;
3387 break;
3389 buff[0] = SYM_SCALE;
3390 if (osdMapData.scale > 0) {
3391 bool scaled = osdFormatCentiNumber(&buff[1], osdMapData.scale * scaleToUnit, scaleUnitDivisor, maxDecimals, 2, 3, false);
3392 buff[4] = scaled ? symScaled : symUnscaled;
3393 // Make sure this is cleared if the map stops being drawn
3394 osdMapData.scale = 0;
3395 } else {
3396 memset(&buff[1], '-', 4);
3398 buff[5] = '\0';
3399 break;
3401 case OSD_MAP_REFERENCE:
3403 char referenceSymbol;
3404 if (osdMapData.referenceSymbol) {
3405 referenceSymbol = osdMapData.referenceSymbol;
3406 // Make sure this is cleared if the map stops being drawn
3407 osdMapData.referenceSymbol = 0;
3408 } else {
3409 referenceSymbol = '-';
3411 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DIRECTION);
3412 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 1, referenceSymbol);
3413 return true;
3416 case OSD_GVAR_0:
3418 osdFormatGVar(buff, 0);
3419 break;
3421 case OSD_GVAR_1:
3423 osdFormatGVar(buff, 1);
3424 break;
3426 case OSD_GVAR_2:
3428 osdFormatGVar(buff, 2);
3429 break;
3431 case OSD_GVAR_3:
3433 osdFormatGVar(buff, 3);
3434 break;
3437 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3438 case OSD_RC_SOURCE:
3440 const char *source_text = IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
3441 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3442 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, source_text, elemAttr);
3443 return true;
3445 #endif
3447 #if defined(USE_ESC_SENSOR)
3448 case OSD_ESC_RPM:
3450 escSensorData_t * escSensor = escSensorGetData();
3451 if (escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE) {
3452 osdFormatRpm(buff, escSensor->rpm);
3454 else {
3455 osdFormatRpm(buff, 0);
3457 break;
3459 case OSD_ESC_TEMPERATURE:
3461 escSensorData_t * escSensor = escSensorGetData();
3462 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
3463 osdDisplayTemperature(elemPosX, elemPosY, SYM_ESC_TEMP, NULL, escTemperatureValid, (escSensor->temperature)*10, osdConfig()->esc_temp_alarm_min, osdConfig()->esc_temp_alarm_max);
3464 return true;
3466 #endif
3467 case OSD_TPA:
3469 char buff[4];
3470 textAttributes_t attr;
3472 displayWrite(osdDisplayPort, elemPosX, elemPosY, "TPA");
3473 attr = TEXT_ATTRIBUTES_NONE;
3474 tfp_sprintf(buff, "%3d", currentControlRateProfile->throttle.dynPID);
3475 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA)) {
3476 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3478 displayWriteWithAttr(osdDisplayPort, elemPosX + 5, elemPosY, buff, attr);
3480 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, "BP");
3481 attr = TEXT_ATTRIBUTES_NONE;
3482 tfp_sprintf(buff, "%4d", currentControlRateProfile->throttle.pa_breakpoint);
3483 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT)) {
3484 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3486 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY + 1, buff, attr);
3488 return true;
3490 case OSD_TPA_TIME_CONSTANT:
3492 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TPA TC", 0, currentControlRateProfile->throttle.fixedWingTauMs, 4, 0, ADJUSTMENT_FW_TPA_TIME_CONSTANT);
3493 return true;
3495 case OSD_FW_LEVEL_TRIM:
3497 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "LEVEL", 0, getFixedWingLevelTrim(), 3, 1, ADJUSTMENT_FW_LEVEL_TRIM);
3498 return true;
3501 case OSD_NAV_FW_CONTROL_SMOOTHNESS:
3503 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CTL S", 0, navConfig()->fw.control_smoothness, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS);
3504 return true;
3506 #ifdef USE_MULTI_MISSION
3507 case OSD_NAV_WP_MULTI_MISSION_INDEX:
3509 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "WP NO", 0, navConfig()->general.waypoint_multi_mission_index, 1, 0, ADJUSTMENT_NAV_WP_MULTI_MISSION_INDEX);
3510 return true;
3512 #endif
3513 case OSD_MISSION:
3515 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION)) {
3516 char buf[5];
3517 switch (posControl.wpMissionPlannerStatus) {
3518 case WP_PLAN_WAIT:
3519 strcpy(buf, "WAIT");
3520 break;
3521 case WP_PLAN_SAVE:
3522 strcpy(buf, "SAVE");
3523 break;
3524 case WP_PLAN_OK:
3525 strcpy(buf, " OK ");
3526 break;
3527 case WP_PLAN_FULL:
3528 strcpy(buf, "FULL");
3530 tfp_sprintf(buff, "%s>%2uWP", buf, posControl.wpPlannerActiveWPIndex);
3531 } else if (posControl.wpPlannerActiveWPIndex){
3532 tfp_sprintf(buff, "PLAN>%2uWP", posControl.waypointCount); // mission planner mision active
3534 #ifdef USE_MULTI_MISSION
3535 else {
3536 if (ARMING_FLAG(ARMED) && !(IS_RC_MODE_ACTIVE(BOXCHANGEMISSION) && posControl.multiMissionCount > 1)){
3537 // Limit field size when Armed, only show selected mission
3538 tfp_sprintf(buff, "M%u ", posControl.loadedMultiMissionIndex);
3539 } else if (posControl.multiMissionCount) {
3540 if (navConfig()->general.waypoint_multi_mission_index != posControl.loadedMultiMissionIndex) {
3541 tfp_sprintf(buff, "M%u/%u>LOAD", navConfig()->general.waypoint_multi_mission_index, posControl.multiMissionCount);
3542 } else {
3543 if (posControl.waypointListValid && posControl.waypointCount > 0) {
3544 tfp_sprintf(buff, "M%u/%u>%2uWP", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
3545 } else {
3546 tfp_sprintf(buff, "M0/%u> 0WP", posControl.multiMissionCount);
3549 } else { // no multi mission loaded - show active WP count from other source
3550 tfp_sprintf(buff, "WP CNT>%2u", posControl.waypointCount);
3553 #endif
3554 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3555 return true;
3558 #ifdef USE_POWER_LIMITS
3559 case OSD_PLIMIT_REMAINING_BURST_TIME:
3560 osdFormatCentiNumber(buff, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3, false);
3561 buff[3] = 'S';
3562 buff[4] = '\0';
3563 break;
3565 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT:
3566 if (currentBatteryProfile->powerLimits.continuousCurrent) {
3567 osdFormatCentiNumber(buff, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3, false);
3568 buff[3] = SYM_AMP;
3569 buff[4] = '\0';
3571 if (powerLimiterIsLimitingCurrent()) {
3572 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3575 break;
3577 #ifdef USE_ADC
3578 case OSD_PLIMIT_ACTIVE_POWER_LIMIT:
3580 if (currentBatteryProfile->powerLimits.continuousPower) {
3581 bool kiloWatt = osdFormatCentiNumber(buff, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3, false);
3582 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3583 buff[4] = '\0';
3585 if (powerLimiterIsLimitingPower()) {
3586 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3589 break;
3591 #endif // USE_ADC
3592 #endif // USE_POWER_LIMITS
3593 case OSD_MULTI_FUNCTION:
3595 // message shown infrequently so only write when needed
3596 static bool clearMultiFunction = true;
3597 elemAttr = osdGetMultiFunctionMessage(buff);
3598 if (buff[0] == 0) {
3599 if (clearMultiFunction) {
3600 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
3601 clearMultiFunction = false;
3603 return true;
3605 clearMultiFunction = true;
3606 break;
3609 default:
3610 return false;
3613 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
3614 return true;
3617 uint8_t osdIncElementIndex(uint8_t elementIndex)
3619 ++elementIndex;
3621 if (elementIndex == OSD_ARTIFICIAL_HORIZON) { // always drawn last so skip
3622 elementIndex++;
3625 #ifndef USE_TEMPERATURE_SENSOR
3626 if (elementIndex == OSD_TEMP_SENSOR_0_TEMPERATURE) {
3627 elementIndex = OSD_ALTITUDE_MSL;
3629 #endif
3631 if (!(feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER))) {
3632 if (elementIndex == OSD_POWER) {
3633 elementIndex = OSD_GPS_LON;
3635 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3636 elementIndex = OSD_LEVEL_PIDS;
3638 #ifdef USE_POWER_LIMITS
3639 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3640 elementIndex = OSD_GLIDESLOPE;
3642 #endif
3645 #ifndef USE_POWER_LIMITS
3646 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3647 elementIndex = OSD_GLIDESLOPE;
3649 #endif
3651 if (!feature(FEATURE_CURRENT_METER)) {
3652 if (elementIndex == OSD_CURRENT_DRAW) {
3653 elementIndex = OSD_GPS_SPEED;
3655 if (elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3656 elementIndex = OSD_BATTERY_REMAINING_PERCENT;
3658 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3659 elementIndex = OSD_TRIP_DIST;
3661 if (elementIndex == OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH) {
3662 elementIndex = OSD_HOME_HEADING_ERROR;
3664 if (elementIndex == OSD_CLIMB_EFFICIENCY) {
3665 elementIndex = OSD_NAV_WP_MULTI_MISSION_INDEX;
3669 if (!STATE(ESC_SENSOR_ENABLED)) {
3670 if (elementIndex == OSD_ESC_RPM) {
3671 elementIndex = OSD_AZIMUTH;
3675 if (!feature(FEATURE_GPS)) {
3676 if (elementIndex == OSD_GPS_HDOP || elementIndex == OSD_TRIP_DIST || elementIndex == OSD_3D_SPEED || elementIndex == OSD_MISSION ||
3677 elementIndex == OSD_AZIMUTH || elementIndex == OSD_BATTERY_REMAINING_CAPACITY || elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3678 elementIndex++;
3680 if (elementIndex == OSD_HEADING_GRAPH && !sensors(SENSOR_MAG)) {
3681 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_WH_DRAWN : OSD_BATTERY_REMAINING_PERCENT;
3683 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3684 elementIndex = OSD_ATTITUDE_PITCH;
3686 if (elementIndex == OSD_GPS_SPEED) {
3687 elementIndex = OSD_ALTITUDE;
3689 if (elementIndex == OSD_GPS_LON) {
3690 elementIndex = sensors(SENSOR_MAG) ? OSD_HEADING : OSD_VARIO;
3692 if (elementIndex == OSD_MAP_NORTH) {
3693 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE : OSD_LEVEL_PIDS;
3695 if (elementIndex == OSD_PLUS_CODE) {
3696 elementIndex = OSD_GFORCE;
3698 if (elementIndex == OSD_GLIDESLOPE) {
3699 elementIndex = OSD_AIR_MAX_SPEED;
3701 if (elementIndex == OSD_GLIDE_RANGE) {
3702 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_CLIMB_EFFICIENCY : OSD_PILOT_NAME;
3704 if (elementIndex == OSD_NAV_WP_MULTI_MISSION_INDEX) {
3705 elementIndex = OSD_PILOT_NAME;
3709 if (!sensors(SENSOR_ACC)) {
3710 if (elementIndex == OSD_CROSSHAIRS) {
3711 elementIndex = OSD_ONTIME;
3713 if (elementIndex == OSD_GFORCE) {
3714 elementIndex = OSD_RC_SOURCE;
3718 if (elementIndex == OSD_ITEM_COUNT) {
3719 elementIndex = 0;
3721 return elementIndex;
3724 void osdDrawNextElement(void)
3726 static uint8_t elementIndex = 0;
3727 // Flag for end of loop, also prevents infinite loop when no elements are enabled
3728 uint8_t index = elementIndex;
3729 do {
3730 elementIndex = osdIncElementIndex(elementIndex);
3731 } while (!osdDrawSingleElement(elementIndex) && index != elementIndex);
3733 // Draw artificial horizon + tracking telemtry last
3734 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
3735 if (osdConfig()->telemetry>0){
3736 osdDisplayTelemetry();
3740 PG_RESET_TEMPLATE(osdConfig_t, osdConfig,
3741 .rssi_alarm = SETTING_OSD_RSSI_ALARM_DEFAULT,
3742 .time_alarm = SETTING_OSD_TIME_ALARM_DEFAULT,
3743 .alt_alarm = SETTING_OSD_ALT_ALARM_DEFAULT,
3744 .dist_alarm = SETTING_OSD_DIST_ALARM_DEFAULT,
3745 .neg_alt_alarm = SETTING_OSD_NEG_ALT_ALARM_DEFAULT,
3746 .current_alarm = SETTING_OSD_CURRENT_ALARM_DEFAULT,
3747 .imu_temp_alarm_min = SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT,
3748 .imu_temp_alarm_max = SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT,
3749 .esc_temp_alarm_min = SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT,
3750 .esc_temp_alarm_max = SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT,
3751 .gforce_alarm = SETTING_OSD_GFORCE_ALARM_DEFAULT,
3752 .gforce_axis_alarm_min = SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT,
3753 .gforce_axis_alarm_max = SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT,
3754 #ifdef USE_BARO
3755 .baro_temp_alarm_min = SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT,
3756 .baro_temp_alarm_max = SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT,
3757 #endif
3758 #ifdef USE_SERIALRX_CRSF
3759 .snr_alarm = SETTING_OSD_SNR_ALARM_DEFAULT,
3760 .crsf_lq_format = SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT,
3761 .link_quality_alarm = SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT,
3762 .rssi_dbm_alarm = SETTING_OSD_RSSI_DBM_ALARM_DEFAULT,
3763 .rssi_dbm_max = SETTING_OSD_RSSI_DBM_MAX_DEFAULT,
3764 .rssi_dbm_min = SETTING_OSD_RSSI_DBM_MIN_DEFAULT,
3765 #endif
3766 #ifdef USE_TEMPERATURE_SENSOR
3767 .temp_label_align = SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT,
3768 #endif
3769 #ifdef USE_PITOT
3770 .airspeed_alarm_min = SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT,
3771 .airspeed_alarm_max = SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT,
3772 #endif
3774 .video_system = SETTING_OSD_VIDEO_SYSTEM_DEFAULT,
3775 .row_shiftdown = SETTING_OSD_ROW_SHIFTDOWN_DEFAULT,
3776 .msp_displayport_fullframe_interval = SETTING_OSD_MSP_DISPLAYPORT_FULLFRAME_INTERVAL_DEFAULT,
3778 .ahi_reverse_roll = SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT,
3779 .ahi_max_pitch = SETTING_OSD_AHI_MAX_PITCH_DEFAULT,
3780 .crosshairs_style = SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT,
3781 .horizon_offset = SETTING_OSD_HORIZON_OFFSET_DEFAULT,
3782 .camera_uptilt = SETTING_OSD_CAMERA_UPTILT_DEFAULT,
3783 .ahi_camera_uptilt_comp = SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT,
3784 .camera_fov_h = SETTING_OSD_CAMERA_FOV_H_DEFAULT,
3785 .camera_fov_v = SETTING_OSD_CAMERA_FOV_V_DEFAULT,
3786 .hud_margin_h = SETTING_OSD_HUD_MARGIN_H_DEFAULT,
3787 .hud_margin_v = SETTING_OSD_HUD_MARGIN_V_DEFAULT,
3788 .hud_homing = SETTING_OSD_HUD_HOMING_DEFAULT,
3789 .hud_homepoint = SETTING_OSD_HUD_HOMEPOINT_DEFAULT,
3790 .hud_radar_disp = SETTING_OSD_HUD_RADAR_DISP_DEFAULT,
3791 .hud_radar_range_min = SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT,
3792 .hud_radar_range_max = SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT,
3793 .hud_radar_alt_difference_display_time = SETTING_OSD_HUD_RADAR_ALT_DIFFERENCE_DISPLAY_TIME_DEFAULT,
3794 .hud_radar_distance_display_time = SETTING_OSD_HUD_RADAR_DISTANCE_DISPLAY_TIME_DEFAULT,
3795 .hud_wp_disp = SETTING_OSD_HUD_WP_DISP_DEFAULT,
3796 .left_sidebar_scroll = SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT,
3797 .right_sidebar_scroll = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT,
3798 .sidebar_scroll_arrows = SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT,
3799 .sidebar_horizontal_offset = SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT,
3800 .left_sidebar_scroll_step = SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT,
3801 .right_sidebar_scroll_step = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT,
3802 .sidebar_height = SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT,
3803 .ahi_pitch_interval = SETTING_OSD_AHI_PITCH_INTERVAL_DEFAULT,
3804 .osd_home_position_arm_screen = SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT,
3805 .pan_servo_index = SETTING_OSD_PAN_SERVO_INDEX_DEFAULT,
3806 .pan_servo_pwm2centideg = SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT,
3807 .pan_servo_offcentre_warning = SETTING_OSD_PAN_SERVO_OFFCENTRE_WARNING_DEFAULT,
3808 .pan_servo_indicator_show_degrees = SETTING_OSD_PAN_SERVO_INDICATOR_SHOW_DEGREES_DEFAULT,
3809 .esc_rpm_precision = SETTING_OSD_ESC_RPM_PRECISION_DEFAULT,
3810 .mAh_precision = SETTING_OSD_MAH_PRECISION_DEFAULT,
3811 .osd_switch_indicator0_name = SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT,
3812 .osd_switch_indicator0_channel = SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNEL_DEFAULT,
3813 .osd_switch_indicator1_name = SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT,
3814 .osd_switch_indicator1_channel = SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNEL_DEFAULT,
3815 .osd_switch_indicator2_name = SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT,
3816 .osd_switch_indicator2_channel = SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNEL_DEFAULT,
3817 .osd_switch_indicator3_name = SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT,
3818 .osd_switch_indicator3_channel = SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNEL_DEFAULT,
3819 .osd_switch_indicators_align_left = SETTING_OSD_SWITCH_INDICATORS_ALIGN_LEFT_DEFAULT,
3820 .system_msg_display_time = SETTING_OSD_SYSTEM_MSG_DISPLAY_TIME_DEFAULT,
3821 .units = SETTING_OSD_UNITS_DEFAULT,
3822 .main_voltage_decimals = SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT,
3823 .use_pilot_logo = SETTING_OSD_USE_PILOT_LOGO_DEFAULT,
3824 .inav_to_pilot_logo_spacing = SETTING_OSD_INAV_TO_PILOT_LOGO_SPACING_DEFAULT,
3825 .arm_screen_display_time = SETTING_OSD_ARM_SCREEN_DISPLAY_TIME_DEFAULT,
3827 #ifdef USE_WIND_ESTIMATOR
3828 .estimations_wind_compensation = SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT,
3829 #endif
3831 .coordinate_digits = SETTING_OSD_COORDINATE_DIGITS_DEFAULT,
3833 .osd_failsafe_switch_layout = SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT,
3835 .plus_code_digits = SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT,
3836 .plus_code_short = SETTING_OSD_PLUS_CODE_SHORT_DEFAULT,
3838 .ahi_width = SETTING_OSD_AHI_WIDTH_DEFAULT,
3839 .ahi_height = SETTING_OSD_AHI_HEIGHT_DEFAULT,
3840 .ahi_vertical_offset = SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT,
3841 .ahi_bordered = SETTING_OSD_AHI_BORDERED_DEFAULT,
3842 .ahi_style = SETTING_OSD_AHI_STYLE_DEFAULT,
3844 .force_grid = SETTING_OSD_FORCE_GRID_DEFAULT,
3846 .stats_energy_unit = SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT,
3847 .stats_min_voltage_unit = SETTING_OSD_STATS_MIN_VOLTAGE_UNIT_DEFAULT,
3848 .stats_page_auto_swap_time = SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT
3851 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t *osdLayoutsConfig)
3853 osdLayoutsConfig->item_pos[0][OSD_ALTITUDE] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG;
3854 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_VOLTAGE] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG;
3855 osdLayoutsConfig->item_pos[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE] = OSD_POS(12, 1);
3857 osdLayoutsConfig->item_pos[0][OSD_RSSI_VALUE] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG;
3858 //line 2
3859 osdLayoutsConfig->item_pos[0][OSD_HOME_DIST] = OSD_POS(1, 1);
3860 osdLayoutsConfig->item_pos[0][OSD_TRIP_DIST] = OSD_POS(1, 2);
3861 osdLayoutsConfig->item_pos[0][OSD_ODOMETER] = OSD_POS(1, 3);
3862 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_CELL_VOLTAGE] = OSD_POS(12, 1);
3863 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE] = OSD_POS(12, 1);
3864 osdLayoutsConfig->item_pos[0][OSD_GPS_SPEED] = OSD_POS(23, 1);
3865 osdLayoutsConfig->item_pos[0][OSD_3D_SPEED] = OSD_POS(23, 1);
3866 osdLayoutsConfig->item_pos[0][OSD_GLIDESLOPE] = OSD_POS(23, 2);
3868 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG;
3869 osdLayoutsConfig->item_pos[0][OSD_SCALED_THROTTLE_POS] = OSD_POS(6, 2);
3870 osdLayoutsConfig->item_pos[0][OSD_HEADING] = OSD_POS(12, 2);
3871 osdLayoutsConfig->item_pos[0][OSD_GROUND_COURSE] = OSD_POS(12, 3);
3872 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ERROR] = OSD_POS(12, 2);
3873 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ADJUSTMENT] = OSD_POS(12, 2);
3874 osdLayoutsConfig->item_pos[0][OSD_CROSS_TRACK_ERROR] = OSD_POS(12, 3);
3875 osdLayoutsConfig->item_pos[0][OSD_HEADING_GRAPH] = OSD_POS(18, 2);
3876 osdLayoutsConfig->item_pos[0][OSD_CURRENT_DRAW] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG;
3877 osdLayoutsConfig->item_pos[0][OSD_MAH_DRAWN] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG;
3878 osdLayoutsConfig->item_pos[0][OSD_WH_DRAWN] = OSD_POS(1, 5);
3879 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_CAPACITY] = OSD_POS(1, 6);
3880 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_PERCENT] = OSD_POS(1, 7);
3881 osdLayoutsConfig->item_pos[0][OSD_POWER_SUPPLY_IMPEDANCE] = OSD_POS(1, 8);
3883 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_MAH_PER_KM] = OSD_POS(1, 5);
3884 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_WH_PER_KM] = OSD_POS(1, 5);
3886 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_ROLL] = OSD_POS(1, 7);
3887 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_PITCH] = OSD_POS(1, 8);
3889 // avoid OSD_VARIO under OSD_CROSSHAIRS
3890 osdLayoutsConfig->item_pos[0][OSD_VARIO] = OSD_POS(23, 5);
3891 // OSD_VARIO_NUM at the right of OSD_VARIO
3892 osdLayoutsConfig->item_pos[0][OSD_VARIO_NUM] = OSD_POS(24, 7);
3893 osdLayoutsConfig->item_pos[0][OSD_HOME_DIR] = OSD_POS(14, 11);
3894 osdLayoutsConfig->item_pos[0][OSD_ARTIFICIAL_HORIZON] = OSD_POS(8, 6);
3895 osdLayoutsConfig->item_pos[0][OSD_HORIZON_SIDEBARS] = OSD_POS(8, 6);
3897 osdLayoutsConfig->item_pos[0][OSD_CRAFT_NAME] = OSD_POS(20, 2);
3898 osdLayoutsConfig->item_pos[0][OSD_PILOT_NAME] = OSD_POS(20, 3);
3899 osdLayoutsConfig->item_pos[0][OSD_PILOT_LOGO] = OSD_POS(20, 3);
3900 osdLayoutsConfig->item_pos[0][OSD_VTX_CHANNEL] = OSD_POS(8, 6);
3902 #ifdef USE_SERIALRX_CRSF
3903 osdLayoutsConfig->item_pos[0][OSD_CRSF_RSSI_DBM] = OSD_POS(23, 12);
3904 osdLayoutsConfig->item_pos[0][OSD_CRSF_LQ] = OSD_POS(23, 11);
3905 osdLayoutsConfig->item_pos[0][OSD_CRSF_SNR_DB] = OSD_POS(24, 9);
3906 osdLayoutsConfig->item_pos[0][OSD_CRSF_TX_POWER] = OSD_POS(24, 10);
3907 #endif
3909 osdLayoutsConfig->item_pos[0][OSD_ONTIME] = OSD_POS(23, 8);
3910 osdLayoutsConfig->item_pos[0][OSD_FLYTIME] = OSD_POS(23, 9);
3911 osdLayoutsConfig->item_pos[0][OSD_ONTIME_FLYTIME] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG;
3912 osdLayoutsConfig->item_pos[0][OSD_RTC_TIME] = OSD_POS(23, 12);
3913 osdLayoutsConfig->item_pos[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH] = OSD_POS(23, 7);
3914 osdLayoutsConfig->item_pos[0][OSD_REMAINING_DISTANCE_BEFORE_RTH] = OSD_POS(23, 6);
3916 osdLayoutsConfig->item_pos[0][OSD_MISSION] = OSD_POS(0, 10);
3917 osdLayoutsConfig->item_pos[0][OSD_GPS_SATS] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG;
3918 osdLayoutsConfig->item_pos[0][OSD_GPS_HDOP] = OSD_POS(0, 10);
3920 osdLayoutsConfig->item_pos[0][OSD_GPS_LAT] = OSD_POS(0, 12);
3921 // Put this on top of the latitude, since it's very unlikely
3922 // that users will want to use both at the same time.
3923 osdLayoutsConfig->item_pos[0][OSD_PLUS_CODE] = OSD_POS(0, 12);
3924 osdLayoutsConfig->item_pos[0][OSD_FLYMODE] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG;
3925 osdLayoutsConfig->item_pos[0][OSD_GPS_LON] = OSD_POS(18, 12);
3927 osdLayoutsConfig->item_pos[0][OSD_AZIMUTH] = OSD_POS(2, 12);
3929 osdLayoutsConfig->item_pos[0][OSD_ROLL_PIDS] = OSD_POS(2, 10);
3930 osdLayoutsConfig->item_pos[0][OSD_PITCH_PIDS] = OSD_POS(2, 11);
3931 osdLayoutsConfig->item_pos[0][OSD_YAW_PIDS] = OSD_POS(2, 12);
3932 osdLayoutsConfig->item_pos[0][OSD_LEVEL_PIDS] = OSD_POS(2, 12);
3933 osdLayoutsConfig->item_pos[0][OSD_POS_XY_PIDS] = OSD_POS(2, 12);
3934 osdLayoutsConfig->item_pos[0][OSD_POS_Z_PIDS] = OSD_POS(2, 12);
3935 osdLayoutsConfig->item_pos[0][OSD_VEL_XY_PIDS] = OSD_POS(2, 12);
3936 osdLayoutsConfig->item_pos[0][OSD_VEL_Z_PIDS] = OSD_POS(2, 12);
3937 osdLayoutsConfig->item_pos[0][OSD_HEADING_P] = OSD_POS(2, 12);
3938 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_ROLL] = OSD_POS(2, 10);
3939 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_PITCH] = OSD_POS(2, 11);
3940 osdLayoutsConfig->item_pos[0][OSD_RC_EXPO] = OSD_POS(2, 12);
3941 osdLayoutsConfig->item_pos[0][OSD_RC_YAW_EXPO] = OSD_POS(2, 12);
3942 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_EXPO] = OSD_POS(2, 12);
3943 osdLayoutsConfig->item_pos[0][OSD_PITCH_RATE] = OSD_POS(2, 12);
3944 osdLayoutsConfig->item_pos[0][OSD_ROLL_RATE] = OSD_POS(2, 12);
3945 osdLayoutsConfig->item_pos[0][OSD_YAW_RATE] = OSD_POS(2, 12);
3946 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_EXPO] = OSD_POS(2, 12);
3947 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_YAW_EXPO] = OSD_POS(2, 12);
3948 osdLayoutsConfig->item_pos[0][OSD_MANUAL_PITCH_RATE] = OSD_POS(2, 12);
3949 osdLayoutsConfig->item_pos[0][OSD_MANUAL_ROLL_RATE] = OSD_POS(2, 12);
3950 osdLayoutsConfig->item_pos[0][OSD_MANUAL_YAW_RATE] = OSD_POS(2, 12);
3951 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_CRUISE_THR] = OSD_POS(2, 12);
3952 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_PITCH2THR] = OSD_POS(2, 12);
3953 osdLayoutsConfig->item_pos[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE] = OSD_POS(2, 12);
3954 osdLayoutsConfig->item_pos[0][OSD_FW_ALT_PID_OUTPUTS] = OSD_POS(2, 12);
3955 osdLayoutsConfig->item_pos[0][OSD_FW_POS_PID_OUTPUTS] = OSD_POS(2, 12);
3956 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_X_PID_OUTPUTS] = OSD_POS(2, 12);
3957 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Y_PID_OUTPUTS] = OSD_POS(2, 12);
3958 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Z_PID_OUTPUTS] = OSD_POS(2, 12);
3959 osdLayoutsConfig->item_pos[0][OSD_MC_POS_XYZ_P_OUTPUTS] = OSD_POS(2, 12);
3961 osdLayoutsConfig->item_pos[0][OSD_POWER] = OSD_POS(15, 1);
3963 osdLayoutsConfig->item_pos[0][OSD_IMU_TEMPERATURE] = OSD_POS(19, 2);
3964 osdLayoutsConfig->item_pos[0][OSD_BARO_TEMPERATURE] = OSD_POS(19, 3);
3965 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_0_TEMPERATURE] = OSD_POS(19, 4);
3966 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_1_TEMPERATURE] = OSD_POS(19, 5);
3967 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_2_TEMPERATURE] = OSD_POS(19, 6);
3968 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_3_TEMPERATURE] = OSD_POS(19, 7);
3969 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_4_TEMPERATURE] = OSD_POS(19, 8);
3970 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_5_TEMPERATURE] = OSD_POS(19, 9);
3971 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_6_TEMPERATURE] = OSD_POS(19, 10);
3972 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_7_TEMPERATURE] = OSD_POS(19, 11);
3974 osdLayoutsConfig->item_pos[0][OSD_AIR_SPEED] = OSD_POS(3, 5);
3975 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_HORIZONTAL] = OSD_POS(3, 6);
3976 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_VERTICAL] = OSD_POS(3, 7);
3978 osdLayoutsConfig->item_pos[0][OSD_GFORCE] = OSD_POS(12, 4);
3979 osdLayoutsConfig->item_pos[0][OSD_GFORCE_X] = OSD_POS(12, 5);
3980 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Y] = OSD_POS(12, 6);
3981 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Z] = OSD_POS(12, 7);
3983 osdLayoutsConfig->item_pos[0][OSD_VTX_POWER] = OSD_POS(3, 5);
3985 osdLayoutsConfig->item_pos[0][OSD_GVAR_0] = OSD_POS(1, 1);
3986 osdLayoutsConfig->item_pos[0][OSD_GVAR_1] = OSD_POS(1, 2);
3987 osdLayoutsConfig->item_pos[0][OSD_GVAR_2] = OSD_POS(1, 3);
3988 osdLayoutsConfig->item_pos[0][OSD_GVAR_3] = OSD_POS(1, 4);
3990 osdLayoutsConfig->item_pos[0][OSD_MULTI_FUNCTION] = OSD_POS(1, 4);
3992 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_0] = OSD_POS(2, 7);
3993 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_1] = OSD_POS(2, 8);
3994 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_2] = OSD_POS(2, 9);
3995 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_3] = OSD_POS(2, 10);
3997 #if defined(USE_ESC_SENSOR)
3998 osdLayoutsConfig->item_pos[0][OSD_ESC_RPM] = OSD_POS(1, 2);
3999 osdLayoutsConfig->item_pos[0][OSD_ESC_TEMPERATURE] = OSD_POS(1, 3);
4000 #endif
4002 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
4003 osdLayoutsConfig->item_pos[0][OSD_RC_SOURCE] = OSD_POS(3, 4);
4004 #endif
4006 #ifdef USE_POWER_LIMITS
4007 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_REMAINING_BURST_TIME] = OSD_POS(3, 4);
4008 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT] = OSD_POS(3, 5);
4009 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT] = OSD_POS(3, 6);
4010 #endif
4012 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
4013 osdLayoutsConfig->item_pos[0][OSD_MESSAGES] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG;
4015 for (unsigned ii = 1; ii < OSD_LAYOUT_COUNT; ii++) {
4016 for (unsigned jj = 0; jj < ARRAYLEN(osdLayoutsConfig->item_pos[0]); jj++) {
4017 osdLayoutsConfig->item_pos[ii][jj] = osdLayoutsConfig->item_pos[0][jj] & ~OSD_VISIBLE_FLAG;
4023 * @brief Draws the INAV and/or pilot logos on the display
4025 * @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
4026 * @param row The row number to start drawing the logos. If not singular, both logos are drawn on the same rows.
4027 * @return uint8_t The row number after the logo(s).
4029 uint8_t drawLogos(bool singular, uint8_t row) {
4030 uint8_t logoRow = row;
4031 uint8_t logoColOffset = 0;
4032 bool usePilotLogo = (osdConfig()->use_pilot_logo && osdDisplayIsHD());
4034 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is in use, the pilot logo cannot be used, due to font issues.
4035 if (isBfCompatibleVideoSystem(osdConfig()))
4036 usePilotLogo = false;
4037 #endif
4039 uint8_t logoSpacing = osdConfig()->inav_to_pilot_logo_spacing;
4041 if (logoSpacing > 0 && ((osdDisplayPort->cols % 2) != (logoSpacing % 2))) {
4042 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
4045 // Draw Logo(s)
4046 if (usePilotLogo && !singular) {
4047 logoColOffset = ((osdDisplayPort->cols - (SYM_LOGO_WIDTH * 2)) - logoSpacing) / 2;
4048 } else {
4049 logoColOffset = floorf((osdDisplayPort->cols - SYM_LOGO_WIDTH) / 2.0f);
4052 // Draw INAV logo
4053 if ((singular && !usePilotLogo) || !singular) {
4054 unsigned logo_c = SYM_LOGO_START;
4055 uint8_t logo_x = logoColOffset;
4056 for (uint8_t lRow = 0; lRow < SYM_LOGO_HEIGHT; lRow++) {
4057 for (uint8_t lCol = 0; lCol < SYM_LOGO_WIDTH; lCol++) {
4058 displayWriteChar(osdDisplayPort, logo_x + lCol, logoRow, logo_c++);
4060 logoRow++;
4064 // Draw the pilot logo
4065 if (usePilotLogo) {
4066 unsigned logo_c = SYM_PILOT_LOGO_LRG_START;
4067 uint8_t logo_x = 0;
4068 logoRow = row;
4069 if (singular) {
4070 logo_x = logoColOffset;
4071 } else {
4072 logo_x = logoColOffset + SYM_LOGO_WIDTH + logoSpacing;
4075 for (uint8_t lRow = 0; lRow < SYM_LOGO_HEIGHT; lRow++) {
4076 for (uint8_t lCol = 0; lCol < SYM_LOGO_WIDTH; lCol++) {
4077 displayWriteChar(osdDisplayPort, logo_x + lCol, logoRow, logo_c++);
4079 logoRow++;
4083 return logoRow;
4086 uint8_t drawStats(uint8_t row) {
4087 #ifdef USE_STATS
4088 char string_buffer[30];
4089 uint8_t statNameX = (osdDisplayPort->cols - 22) / 2;
4090 uint8_t statValueX = statNameX + 21;
4092 if (statsConfig()->stats_enabled) {
4093 displayWrite(osdDisplayPort, statNameX, row, "ODOMETER:");
4094 switch (osdConfig()->units) {
4095 case OSD_UNIT_UK:
4096 FALLTHROUGH;
4097 case OSD_UNIT_IMPERIAL:
4098 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_MILE));
4099 string_buffer[5] = SYM_MI;
4100 break;
4101 default:
4102 case OSD_UNIT_GA:
4103 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE));
4104 string_buffer[5] = SYM_NM;
4105 break;
4106 case OSD_UNIT_METRIC_MPH:
4107 FALLTHROUGH;
4108 case OSD_UNIT_METRIC:
4109 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER));
4110 string_buffer[5] = SYM_KM;
4111 break;
4113 string_buffer[6] = '\0';
4114 displayWrite(osdDisplayPort, statValueX-5, row, string_buffer);
4116 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL TIME:");
4117 uint32_t tot_mins = statsConfig()->stats_total_time / 60;
4118 tfp_sprintf(string_buffer, "%2d:%02dH:M", (int)(tot_mins / 60), (int)(tot_mins % 60));
4119 displayWrite(osdDisplayPort, statValueX-7, row, string_buffer);
4121 #ifdef USE_ADC
4122 if (feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER)) {
4123 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL ENERGY:");
4124 osdFormatCentiNumber(string_buffer, statsConfig()->stats_total_energy / 10, 0, 2, 0, 4, false);
4125 displayWrite(osdDisplayPort, statValueX-4, row, string_buffer);
4126 displayWriteChar(osdDisplayPort, statValueX, row, SYM_WH);
4128 displayWrite(osdDisplayPort, statNameX, ++row, "AVG EFFICIENCY:");
4129 if (statsConfig()->stats_total_dist) {
4130 uint32_t avg_efficiency = statsConfig()->stats_total_energy / (statsConfig()->stats_total_dist / METERS_PER_KILOMETER); // mWh/km
4131 switch (osdConfig()->units) {
4132 case OSD_UNIT_UK:
4133 FALLTHROUGH;
4134 case OSD_UNIT_IMPERIAL:
4135 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3, false);
4136 string_buffer[3] = SYM_WH_MI;
4137 break;
4138 case OSD_UNIT_GA:
4139 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3, false);
4140 string_buffer[3] = SYM_WH_NM;
4141 break;
4142 default:
4143 case OSD_UNIT_METRIC_MPH:
4144 FALLTHROUGH;
4145 case OSD_UNIT_METRIC:
4146 osdFormatCentiNumber(string_buffer, avg_efficiency / 10000 * METERS_PER_MILE, 0, 2, 0, 3, false);
4147 string_buffer[3] = SYM_WH_KM;
4148 break;
4150 } else {
4151 string_buffer[0] = string_buffer[1] = string_buffer[2] = '-';
4153 string_buffer[4] = '\0';
4154 displayWrite(osdDisplayPort, statValueX-3, row++, string_buffer);
4156 #endif // USE_ADC
4158 #endif // USE_STATS
4159 return row;
4162 static void osdSetNextRefreshIn(uint32_t timeMs) {
4163 resumeRefreshAt = micros() + timeMs * 1000;
4164 refreshWaitForResumeCmdRelease = true;
4167 static void osdCompleteAsyncInitialization(void)
4169 if (!displayIsReady(osdDisplayPort)) {
4170 // Update the display.
4171 // XXX: Rename displayDrawScreen() and associated functions
4172 // to displayUpdate()
4173 displayDrawScreen(osdDisplayPort);
4174 return;
4177 osdDisplayIsReady = true;
4179 #if defined(USE_CANVAS)
4180 if (osdConfig()->force_grid) {
4181 osdDisplayHasCanvas = false;
4182 } else {
4183 osdDisplayHasCanvas = displayGetCanvas(&osdCanvas, osdDisplayPort);
4185 #endif
4187 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4188 displayClearScreen(osdDisplayPort);
4190 uint8_t y = 1;
4191 displayFontMetadata_t metadata;
4192 bool fontHasMetadata = displayGetFontMetadata(&metadata, osdDisplayPort);
4193 LOG_DEBUG(OSD, "Font metadata version %s: %u (%u chars)",
4194 fontHasMetadata ? "Y" : "N", metadata.version, metadata.charCount);
4196 if (fontHasMetadata && metadata.charCount > 256) {
4197 hasExtendedFont = true;
4199 y = drawLogos(false, y);
4200 y++;
4201 } else if (!fontHasMetadata) {
4202 const char *m = "INVALID FONT";
4203 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
4206 if (fontHasMetadata && metadata.version < OSD_MIN_FONT_VERSION) {
4207 const char *m = "INVALID FONT VERSION";
4208 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
4211 char string_buffer[30];
4212 tfp_sprintf(string_buffer, "INAV VERSION: %s", FC_VERSION_STRING);
4213 uint8_t xPos = (osdDisplayPort->cols - 19) / 2; // Automatically centre, regardless of resolution. In the case of odd number screens, bias to the left.
4214 displayWrite(osdDisplayPort, xPos, y++, string_buffer);
4215 #ifdef USE_CMS
4216 displayWrite(osdDisplayPort, xPos+2, y++, CMS_STARTUP_HELP_TEXT1);
4217 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT2);
4218 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT3);
4219 #endif
4221 #ifdef USE_STATS
4222 y = drawStats(++y);
4223 #endif
4225 displayCommitTransaction(osdDisplayPort);
4226 displayResync(osdDisplayPort);
4227 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME);
4230 void osdInit(displayPort_t *osdDisplayPortToUse)
4232 if (!osdDisplayPortToUse)
4233 return;
4235 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(63,63));
4237 osdDisplayPort = osdDisplayPortToUse;
4239 #ifdef USE_CMS
4240 cmsDisplayPortRegister(osdDisplayPort);
4241 #endif
4243 armState = ARMING_FLAG(ARMED);
4244 osdCompleteAsyncInitialization();
4247 static void osdResetStats(void)
4249 stats.max_current = 0;
4250 stats.max_power = 0;
4251 stats.max_speed = 0;
4252 stats.max_3D_speed = 0;
4253 stats.max_air_speed = 0;
4254 stats.min_voltage = 5000;
4255 stats.min_rssi = 99;
4256 stats.min_lq = 300;
4257 stats.min_rssi_dbm = 0;
4258 stats.max_altitude = 0;
4261 static void osdUpdateStats(void)
4263 int32_t value;
4265 if (feature(FEATURE_GPS)) {
4266 value = osdGet3DSpeed();
4267 const float airspeed_estimate = getAirspeedEstimate();
4269 if (stats.max_3D_speed < value)
4270 stats.max_3D_speed = value;
4272 if (stats.max_speed < gpsSol.groundSpeed)
4273 stats.max_speed = gpsSol.groundSpeed;
4275 if (stats.max_air_speed < airspeed_estimate)
4276 stats.max_air_speed = airspeed_estimate;
4278 if (stats.max_distance < GPS_distanceToHome)
4279 stats.max_distance = GPS_distanceToHome;
4282 value = getBatteryVoltage();
4283 if (stats.min_voltage > value)
4284 stats.min_voltage = value;
4286 value = abs(getAmperage());
4287 if (stats.max_current < value)
4288 stats.max_current = value;
4290 value = labs(getPower());
4291 if (stats.max_power < value)
4292 stats.max_power = value;
4294 value = osdConvertRSSI();
4295 if (stats.min_rssi > value)
4296 stats.min_rssi = value;
4298 value = osdGetCrsfLQ();
4299 if (stats.min_lq > value)
4300 stats.min_lq = value;
4302 if (!failsafeIsReceivingRxData())
4303 stats.min_lq = 0;
4305 value = osdGetCrsfdBm();
4306 if (stats.min_rssi_dbm > value)
4307 stats.min_rssi_dbm = value;
4309 stats.max_altitude = MAX(stats.max_altitude, osdGetAltitude());
4312 static void osdShowStats(bool isSinglePageStatsCompatible, uint8_t page)
4314 const char * disarmReasonStr[DISARM_REASON_COUNT] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "KILLSW", "FAILSAFE", "NAV SYS", "LANDING"};
4315 uint8_t top = 1; // Start one line down leaving space at the top of the screen.
4316 size_t multiValueLengthOffset = 0;
4318 const uint8_t statNameX = osdDisplayIsHD() ? 11 : 1;
4319 const uint8_t statValuesX = osdDisplayIsHD() ? 30 : 20;
4320 char buff[10];
4322 if (page > 1)
4323 page = 0;
4325 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4326 displayClearScreen(osdDisplayPort);
4328 if (isSinglePageStatsCompatible) {
4329 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS ---");
4330 } else if (page == 0) {
4331 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- 1/2 ->");
4332 } else if (page == 1) {
4333 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- <- 2/2");
4336 if (isSinglePageStatsCompatible || page == 0) {
4337 if (feature(FEATURE_GPS)) {
4338 if (isSinglePageStatsCompatible) {
4339 displayWrite(osdDisplayPort, statNameX, top, "MAX/AVG SPEED :");
4340 osdFormatVelocityStr(buff, stats.max_3D_speed, true, false);
4341 osdLeftAlignString(buff);
4342 strcat(osdFormatTrimWhiteSpace(buff),"/");
4343 multiValueLengthOffset = strlen(buff);
4344 displayWrite(osdDisplayPort, statValuesX, top, buff);
4345 osdGenerateAverageVelocityStr(buff);
4346 osdLeftAlignString(buff);
4347 displayWrite(osdDisplayPort, statValuesX + multiValueLengthOffset, top++, buff);
4348 } else {
4349 displayWrite(osdDisplayPort, statNameX, top, "MAX SPEED :");
4350 osdFormatVelocityStr(buff, stats.max_3D_speed, true, false);
4351 osdLeftAlignString(buff);
4352 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4354 displayWrite(osdDisplayPort, statNameX, top, "AVG SPEED :");
4355 osdGenerateAverageVelocityStr(buff);
4356 osdLeftAlignString(buff);
4357 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4360 displayWrite(osdDisplayPort, statNameX, top, "MAX DISTANCE :");
4361 osdFormatDistanceStr(buff, stats.max_distance*100);
4362 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4364 displayWrite(osdDisplayPort, statNameX, top, "TRAVELED DISTANCE:");
4365 osdFormatDistanceStr(buff, getTotalTravelDistance());
4366 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4369 displayWrite(osdDisplayPort, statNameX, top, "MAX ALTITUDE :");
4370 osdFormatAltitudeStr(buff, stats.max_altitude);
4371 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4373 switch (rxConfig()->serialrx_provider) {
4374 case SERIALRX_CRSF:
4375 if (isSinglePageStatsCompatible) {
4376 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI %/DBM :");
4377 itoa(stats.min_rssi, buff, 10);
4378 osdLeftAlignString(buff);
4379 strcat(osdFormatTrimWhiteSpace(buff), "%/");
4380 multiValueLengthOffset = strlen(buff);
4381 displayWrite(osdDisplayPort, statValuesX, top, buff);
4382 itoa(stats.min_rssi_dbm, buff, 10);
4383 tfp_sprintf(buff, "%s%c", buff, SYM_DBM);
4384 osdLeftAlignString(buff);
4385 displayWrite(osdDisplayPort, statValuesX + multiValueLengthOffset, top++, buff);
4386 } else {
4387 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI % :");
4388 itoa(stats.min_rssi, buff, 10);
4389 strcat(buff, "%");
4390 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4392 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI DBM :");
4393 itoa(stats.min_rssi_dbm, buff, 10);
4394 tfp_sprintf(buff, "%s%c", buff, SYM_DBM);
4395 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4398 displayWrite(osdDisplayPort, statNameX, top, "MIN LQ :");
4399 itoa(stats.min_lq, buff, 10);
4400 strcat(buff, "%");
4401 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4402 break;
4403 default:
4404 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI :");
4405 itoa(stats.min_rssi, buff, 10);
4406 strcat(buff, "%");
4407 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4410 displayWrite(osdDisplayPort, statNameX, top, "FLY TIME :");
4411 uint16_t flySeconds = getFlightTime();
4412 uint16_t flyMinutes = flySeconds / 60;
4413 flySeconds %= 60;
4414 uint16_t flyHours = flyMinutes / 60;
4415 flyMinutes %= 60;
4416 tfp_sprintf(buff, "%02u:%02u:%02u", flyHours, flyMinutes, flySeconds);
4417 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4419 displayWrite(osdDisplayPort, statNameX, top, "DISARMED BY :");
4420 displayWrite(osdDisplayPort, statValuesX, top++, disarmReasonStr[getDisarmReason()]);
4423 if (isSinglePageStatsCompatible || page == 1) {
4424 if (osdConfig()->stats_min_voltage_unit == OSD_STATS_MIN_VOLTAGE_UNIT_BATTERY) {
4425 displayWrite(osdDisplayPort, statNameX, top, "MIN BATTERY VOLT :");
4426 osdFormatCentiNumber(buff, stats.min_voltage, 0, osdConfig()->main_voltage_decimals, 0, osdConfig()->main_voltage_decimals + 2, false);
4427 } else {
4428 displayWrite(osdDisplayPort, statNameX, top, "MIN CELL VOLTAGE :");
4429 osdFormatCentiNumber(buff, stats.min_voltage/getBatteryCellCount(), 0, 2, 0, 3, false);
4431 tfp_sprintf(buff, "%s%c", buff, SYM_VOLT);
4432 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4434 if (feature(FEATURE_CURRENT_METER)) {
4435 displayWrite(osdDisplayPort, statNameX, top, "MAX CURRENT :");
4436 osdFormatCentiNumber(buff, stats.max_current, 0, 2, 0, 3, false);
4437 tfp_sprintf(buff, "%s%c", buff, SYM_AMP);
4438 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4440 displayWrite(osdDisplayPort, statNameX, top, "MAX POWER :");
4441 bool kiloWatt = osdFormatCentiNumber(buff, stats.max_power, 1000, 2, 2, 3, false);
4442 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
4443 buff[4] = '\0';
4444 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4446 displayWrite(osdDisplayPort, statNameX, top, "USED CAPACITY :");
4447 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4448 tfp_sprintf(buff, "%d%c", (int)getMAhDrawn(), SYM_MAH);
4449 } else {
4450 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3, false);
4451 tfp_sprintf(buff, "%s%c", buff, SYM_WH);
4453 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4455 int32_t totalDistance = getTotalTravelDistance();
4456 bool moreThanAh = false;
4457 bool efficiencyValid = totalDistance >= 10000;
4458 if (feature(FEATURE_GPS)) {
4459 displayWrite(osdDisplayPort, statNameX, top, "AVG EFFICIENCY :");
4460 uint8_t digits = 3U; // Total number of digits (including decimal point)
4461 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
4462 if (isBfCompatibleVideoSystem(osdConfig())) {
4463 // Add one digit so no switch to scaled decimal occurs above 99
4464 digits = 4U;
4466 #endif
4467 switch (osdConfig()->units) {
4468 case OSD_UNIT_UK:
4469 FALLTHROUGH;
4470 case OSD_UNIT_IMPERIAL:
4471 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4472 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, digits, false);
4473 if (!moreThanAh) {
4474 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
4475 } else {
4476 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
4478 if (!efficiencyValid) {
4479 buff[0] = buff[1] = buff[2] = '-';
4480 buff[3] = SYM_MAH_MI_0;
4481 buff[4] = SYM_MAH_MI_1;
4482 buff[5] = '\0';
4484 } else {
4485 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, digits, false);
4486 tfp_sprintf(buff, "%s%c", buff, SYM_WH_MI);
4487 if (!efficiencyValid) {
4488 buff[0] = buff[1] = buff[2] = '-';
4491 break;
4492 case OSD_UNIT_GA:
4493 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4494 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, digits, false);
4495 if (!moreThanAh) {
4496 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
4497 } else {
4498 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
4500 if (!efficiencyValid) {
4501 buff[0] = buff[1] = buff[2] = '-';
4502 buff[3] = SYM_MAH_NM_0;
4503 buff[4] = SYM_MAH_NM_1;
4504 buff[5] = '\0';
4506 } else {
4507 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, digits, false);
4508 tfp_sprintf(buff, "%s%c", buff, SYM_WH_NM);
4509 if (!efficiencyValid) {
4510 buff[0] = buff[1] = buff[2] = '-';
4513 break;
4514 case OSD_UNIT_METRIC_MPH:
4515 FALLTHROUGH;
4516 case OSD_UNIT_METRIC:
4517 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4518 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000000.0f / totalDistance), 1000, 0, 2, digits, false);
4519 if (!moreThanAh) {
4520 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
4521 } else {
4522 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
4524 if (!efficiencyValid) {
4525 buff[0] = buff[1] = buff[2] = '-';
4526 buff[3] = SYM_MAH_KM_0;
4527 buff[4] = SYM_MAH_KM_1;
4528 buff[5] = '\0';
4530 } else {
4531 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10000.0f / totalDistance), 0, 2, 0, digits, false);
4532 tfp_sprintf(buff, "%s%c", buff, SYM_WH_KM);
4533 if (!efficiencyValid) {
4534 buff[0] = buff[1] = buff[2] = '-';
4537 break;
4539 osdLeftAlignString(buff);
4540 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4544 const float max_gforce = accGetMeasuredMaxG();
4545 displayWrite(osdDisplayPort, statNameX, top, "MAX G-FORCE :");
4546 osdFormatCentiNumber(buff, max_gforce * 100, 0, 2, 0, 3, false);
4547 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4549 const acc_extremes_t *acc_extremes = accGetMeasuredExtremes();
4550 const float acc_extremes_min = acc_extremes[Z].min;
4551 const float acc_extremes_max = acc_extremes[Z].max;
4552 displayWrite(osdDisplayPort, statNameX, top, "MIN/MAX Z G-FORCE:");
4553 osdFormatCentiNumber(buff, acc_extremes_min * 100, 0, 2, 0, 4, false);
4554 osdLeftAlignString(buff);
4555 strcat(osdFormatTrimWhiteSpace(buff),"/");
4556 multiValueLengthOffset = strlen(buff);
4557 displayWrite(osdDisplayPort, statValuesX, top, buff);
4558 osdFormatCentiNumber(buff, acc_extremes_max * 100, 0, 2, 0, 3, false);
4559 osdLeftAlignString(buff);
4560 displayWrite(osdDisplayPort, statValuesX + multiValueLengthOffset, top++, buff);
4563 if (savingSettings == true) {
4564 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS));
4565 } else if (notify_settings_saved > 0) {
4566 if (millis() > notify_settings_saved) {
4567 notify_settings_saved = 0;
4568 } else {
4569 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED));
4573 displayCommitTransaction(osdDisplayPort);
4576 // HD arming screen. based on the minimum HD OSD grid size of 50 x 18
4577 static void osdShowHDArmScreen(void)
4579 dateTime_t dt;
4580 char buf[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
4581 char buf2[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
4582 char craftNameBuf[MAX_NAME_LENGTH];
4583 char versionBuf[osdDisplayPort->cols];
4584 uint8_t safehomeRow = 0;
4585 uint8_t armScreenRow = 2; // Start at row 2
4587 armScreenRow = drawLogos(false, armScreenRow);
4588 armScreenRow++;
4590 if (strlen(systemConfig()->craftName) > 0) {
4591 osdFormatCraftName(craftNameBuf);
4592 strcpy(buf2, "ARMED!");
4593 tfp_sprintf(buf, "%s - %s", craftNameBuf, buf2);
4594 } else {
4595 strcpy(buf, "ARMED!");
4597 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4598 #if defined(USE_GPS)
4599 #if defined (USE_SAFE_HOME)
4600 if (posControl.safehomeState.distance) {
4601 safehomeRow = armScreenRow;
4602 armScreenRow++;
4604 #endif // USE_SAFE_HOME
4605 #endif // USE_GPS
4606 armScreenRow++;
4608 if (posControl.waypointListValid && posControl.waypointCount > 0) {
4609 #ifdef USE_MULTI_MISSION
4610 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
4611 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4612 #else
4613 strcpy(buf, "*MISSION LOADED*");
4614 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4615 #endif
4617 armScreenRow++;
4619 #if defined(USE_GPS)
4620 if (feature(FEATURE_GPS)) {
4621 if (STATE(GPS_FIX_HOME)) {
4622 if (osdConfig()->osd_home_position_arm_screen) {
4623 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
4624 osdFormatCoordinate(buf2, SYM_LON, GPS_home.lon);
4625 uint8_t gap = 1;
4626 uint8_t col = strlen(buf) + strlen(buf2) + gap;
4628 if ((osdDisplayPort->cols %2) != (col %2)) {
4629 gap++;
4630 col++;
4633 col = (osdDisplayPort->cols - col) / 2;
4635 displayWrite(osdDisplayPort, col, armScreenRow, buf);
4636 displayWrite(osdDisplayPort, col + strlen(buf) + gap, armScreenRow++, buf2);
4638 int digits = osdConfig()->plus_code_digits;
4639 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
4640 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4643 #if defined (USE_SAFE_HOME)
4644 if (posControl.safehomeState.distance) { // safehome found during arming
4645 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
4646 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
4647 } else {
4648 osdFormatDistanceStr(buf2, posControl.safehomeState.distance);
4649 tfp_sprintf(buf, "%c SAFEHOME %u @ %s", SYM_HOME, posControl.safehomeState.index, buf2);
4651 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
4652 // write this message below the ARMED message to make it obvious
4653 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, safehomeRow, buf, elemAttr);
4655 #endif
4656 } else {
4657 strcpy(buf, "!NO HOME POSITION!");
4658 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4660 armScreenRow++;
4662 #endif
4664 if (rtcGetDateTimeLocal(&dt)) {
4665 tfp_sprintf(buf, "%04u-%02u-%02u %02u:%02u:%02u", dt.year, dt.month, dt.day, dt.hours, dt.minutes, dt.seconds);
4666 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4667 armScreenRow++;
4670 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
4671 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, armScreenRow++, versionBuf);
4672 armScreenRow++;
4674 #ifdef USE_STATS
4675 if (armScreenRow < (osdDisplayPort->rows - 4))
4676 armScreenRow = drawStats(armScreenRow);
4677 #endif // USE_STATS
4680 static void osdShowSDArmScreen(void)
4682 dateTime_t dt;
4683 char buf[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
4684 char buf2[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
4685 char craftNameBuf[MAX_NAME_LENGTH];
4686 char versionBuf[osdDisplayPort->cols];
4687 // We need 12 visible rows, start row never < first fully visible row 1
4688 uint8_t armScreenRow = osdDisplayPort->rows > 13 ? (osdDisplayPort->rows - 12) / 2 : 1;
4689 uint8_t safehomeRow = 0;
4691 strcpy(buf, "ARMED!");
4692 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4693 safehomeRow = armScreenRow;
4694 armScreenRow++;
4696 if (strlen(systemConfig()->craftName) > 0) {
4697 osdFormatCraftName(craftNameBuf);
4698 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(systemConfig()->craftName)) / 2, armScreenRow++, craftNameBuf );
4701 if (posControl.waypointListValid && posControl.waypointCount > 0) {
4702 #ifdef USE_MULTI_MISSION
4703 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
4704 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow, buf);
4705 #else
4706 strcpy(buf, "*MISSION LOADED*");
4707 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow, buf);
4708 #endif
4710 armScreenRow++;
4712 #if defined(USE_GPS)
4713 if (feature(FEATURE_GPS)) {
4714 if (STATE(GPS_FIX_HOME)) {
4715 if (osdConfig()->osd_home_position_arm_screen) {
4716 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
4717 osdFormatCoordinate(buf2, SYM_LON, GPS_home.lon);
4719 uint8_t gpsStartCol = (osdDisplayPort->cols - (strlen(buf) + strlen(buf2) + 2)) / 2;
4720 displayWrite(osdDisplayPort, gpsStartCol, armScreenRow, buf);
4721 displayWrite(osdDisplayPort, gpsStartCol + strlen(buf) + 2, armScreenRow++, buf2);
4723 int digits = osdConfig()->plus_code_digits;
4724 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
4725 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4728 #if defined (USE_SAFE_HOME)
4729 if (posControl.safehomeState.distance) { // safehome found during arming
4730 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
4731 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
4732 } else {
4733 osdFormatDistanceStr(buf2, posControl.safehomeState.distance);
4734 tfp_sprintf(buf, "%c SAFEHOME %u @ %s", SYM_HOME, posControl.safehomeState.index, buf2);
4736 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
4737 // write this message below the ARMED message to make it obvious
4738 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, safehomeRow, buf, elemAttr);
4740 #endif
4741 } else {
4742 strcpy(buf, "!NO HOME POSITION!");
4743 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4745 armScreenRow++;
4747 #endif
4749 if (rtcGetDateTimeLocal(&dt)) {
4750 tfp_sprintf(buf, "%04u-%02u-%02u %02u:%02u:%02u", dt.year, dt.month, dt.day, dt.hours, dt.minutes, dt.seconds);
4751 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
4752 armScreenRow++;
4755 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
4756 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, armScreenRow++, versionBuf);
4757 armScreenRow++;
4759 #ifdef USE_STATS
4760 if (armScreenRow < (osdDisplayPort->rows - 4))
4761 armScreenRow = drawStats(armScreenRow);
4762 #endif // USE_STATS
4765 // called when motors armed
4766 static void osdShowArmed(void)
4768 displayClearScreen(osdDisplayPort);
4770 if (osdDisplayIsHD()) {
4771 osdShowHDArmScreen();
4772 } else {
4773 osdShowSDArmScreen();
4777 static void osdFilterData(timeUs_t currentTimeUs) {
4778 static timeUs_t lastRefresh = 0;
4779 float refresh_dT = US2S(cmpTimeUs(currentTimeUs, lastRefresh));
4781 GForce = fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF)) / GRAVITY_MSS;
4782 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) GForceAxis[axis] = imuMeasuredAccelBF.v[axis] / GRAVITY_MSS;
4784 if (lastRefresh) {
4785 GForce = pt1FilterApply3(&GForceFilter, GForce, refresh_dT);
4786 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) pt1FilterApply3(GForceFilterAxis + axis, GForceAxis[axis], refresh_dT);
4787 } else {
4788 pt1FilterInitRC(&GForceFilter, GFORCE_FILTER_TC, 0);
4789 pt1FilterReset(&GForceFilter, GForce);
4791 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) {
4792 pt1FilterInitRC(GForceFilterAxis + axis, GFORCE_FILTER_TC, 0);
4793 pt1FilterReset(GForceFilterAxis + axis, GForceAxis[axis]);
4797 lastRefresh = currentTimeUs;
4800 // Detect when the user is holding the roll stick to the right
4801 static bool osdIsPageUpStickCommandHeld(void)
4803 static int pageUpHoldCount = 1;
4805 bool keyHeld = false;
4807 if (IS_HI(ROLL)) {
4808 keyHeld = true;
4811 if (!keyHeld) {
4812 pageUpHoldCount = 1;
4813 } else {
4814 ++pageUpHoldCount;
4817 if (pageUpHoldCount > 20) {
4818 pageUpHoldCount = 1;
4819 return true;
4822 return false;
4825 // Detect when the user is holding the roll stick to the left
4826 static bool osdIsPageDownStickCommandHeld(void)
4828 static int pageDownHoldCount = 1;
4830 bool keyHeld = false;
4831 if (IS_LO(ROLL)) {
4832 keyHeld = true;
4835 if (!keyHeld) {
4836 pageDownHoldCount = 1;
4837 } else {
4838 ++pageDownHoldCount;
4841 if (pageDownHoldCount > 20) {
4842 pageDownHoldCount = 1;
4843 return true;
4846 return false;
4849 static void osdRefresh(timeUs_t currentTimeUs)
4851 osdFilterData(currentTimeUs);
4853 #ifdef USE_CMS
4854 if (IS_RC_MODE_ACTIVE(BOXOSD) && (!cmsInMenu) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4855 #else
4856 if (IS_RC_MODE_ACTIVE(BOXOSD) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4857 #endif
4858 displayClearScreen(osdDisplayPort);
4859 armState = ARMING_FLAG(ARMED);
4860 return;
4863 bool statsSinglePageCompatible = (osdDisplayPort->rows >= OSD_STATS_SINGLE_PAGE_MIN_ROWS);
4864 static uint8_t statsCurrentPage = 0;
4865 static bool statsDisplayed = false;
4866 static bool statsAutoPagingEnabled = true;
4868 // Detect arm/disarm
4869 if (armState != ARMING_FLAG(ARMED)) {
4870 if (ARMING_FLAG(ARMED)) {
4871 // Display the "Arming" screen
4872 statsDisplayed = false;
4873 osdResetStats();
4874 osdShowArmed();
4875 uint16_t delay = osdConfig()->arm_screen_display_time;
4876 if (STATE(IN_FLIGHT_EMERG_REARM)) {
4877 delay = 500;
4879 #if defined(USE_SAFE_HOME)
4880 else if (posControl.safehomeState.distance) {
4881 delay += 3000;
4883 #endif
4884 osdSetNextRefreshIn(delay);
4885 } else {
4886 // Display the "Stats" screen
4887 statsDisplayed = true;
4888 statsCurrentPage = 0;
4889 statsAutoPagingEnabled = osdConfig()->stats_page_auto_swap_time > 0 ? true : false;
4890 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
4891 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME);
4894 armState = ARMING_FLAG(ARMED);
4897 // This block is entered when we're showing the "Splash", "Armed" or "Stats" screens
4898 if (resumeRefreshAt) {
4900 // Handle events only when the "Stats" screen is being displayed.
4901 if (statsDisplayed) {
4903 // Manual paging stick commands are only applicable to multi-page stats.
4904 // ******************************
4905 // For single-page stats, this effectively disables the ability to cancel the
4906 // automatic paging/updates with the stick commands. So unless stats_page_auto_swap_time
4907 // is set to 0 or greater than 4 (saved settings display interval is 5 seconds), then
4908 // "Saved Settings" should display if it is active within the refresh interval.
4909 // ******************************
4910 // With multi-page stats, "Saved Settings" could also be missed if the user
4911 // has canceled automatic paging using the stick commands, because that is only
4912 // updated when osdShowStats() is called. So, in that case, they would only see
4913 // the "Saved Settings" message if they happen to manually change pages using the
4914 // stick commands within the interval the message is displayed.
4915 bool manualPageUpRequested = false;
4916 bool manualPageDownRequested = false;
4917 if (!statsSinglePageCompatible) {
4918 // These methods ensure the paging stick commands are held for a brief period
4919 // Otherwise it can result in a race condition where the stats are
4920 // updated too quickly and can result in partial blanks, etc.
4921 if (osdIsPageUpStickCommandHeld()) {
4922 manualPageUpRequested = true;
4923 statsAutoPagingEnabled = false;
4924 } else if (osdIsPageDownStickCommandHeld()) {
4925 manualPageDownRequested = true;
4926 statsAutoPagingEnabled = false;
4930 if (statsAutoPagingEnabled) {
4931 // Alternate screens for multi-page stats.
4932 // Also, refreshes screen at swap interval for single-page stats.
4933 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time * 1000), 2)) {
4934 if (statsCurrentPage == 0) {
4935 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
4936 statsCurrentPage = 1;
4938 } else {
4939 if (statsCurrentPage == 1) {
4940 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
4941 statsCurrentPage = 0;
4944 } else {
4945 // Process manual page change events for multi-page stats.
4946 if (manualPageUpRequested) {
4947 osdShowStats(statsSinglePageCompatible, 1);
4948 statsCurrentPage = 1;
4949 } else if (manualPageDownRequested) {
4950 osdShowStats(statsSinglePageCompatible, 0);
4951 statsCurrentPage = 0;
4956 // Handle events when either "Splash", "Armed" or "Stats" screens are displayed.
4957 if ((currentTimeUs > resumeRefreshAt) || OSD_RESUME_UPDATES_STICK_COMMAND) {
4958 // Time elapsed or canceled by stick commands.
4959 // Exit to normal OSD operation.
4960 displayClearScreen(osdDisplayPort);
4961 resumeRefreshAt = 0;
4962 statsDisplayed = false;
4963 } else {
4964 // Continue "Splash", "Armed" or "Stats" screens.
4965 displayHeartbeat(osdDisplayPort);
4968 return;
4971 #ifdef USE_CMS
4972 if (!displayIsGrabbed(osdDisplayPort)) {
4973 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4974 if (fullRedraw) {
4975 displayClearScreen(osdDisplayPort);
4976 fullRedraw = false;
4978 osdDrawNextElement();
4979 displayHeartbeat(osdDisplayPort);
4980 displayCommitTransaction(osdDisplayPort);
4981 #ifdef OSD_CALLS_CMS
4982 } else {
4983 cmsUpdate(currentTimeUs);
4984 #endif
4986 #endif
4990 * Called periodically by the scheduler
4992 void osdUpdate(timeUs_t currentTimeUs)
4994 static uint32_t counter = 0;
4996 // don't touch buffers if DMA transaction is in progress
4997 if (displayIsTransferInProgress(osdDisplayPort)) {
4998 return;
5001 if (!osdDisplayIsReady) {
5002 osdCompleteAsyncInitialization();
5003 return;
5006 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
5007 // Check if the layout has changed. Higher numbered
5008 // boxes take priority.
5009 unsigned activeLayout;
5010 if (layoutOverride >= 0) {
5011 activeLayout = layoutOverride;
5012 // Check for timed override, it will go into effect on
5013 // the next OSD iteration
5014 if (layoutOverrideUntil > 0 && millis() > layoutOverrideUntil) {
5015 layoutOverrideUntil = 0;
5016 layoutOverride = -1;
5018 } else if (osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE)) {
5019 activeLayout = 0;
5020 } else {
5021 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
5022 if (IS_RC_MODE_ACTIVE(BOXOSDALT3))
5023 activeLayout = 3;
5024 else
5025 #endif
5026 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
5027 if (IS_RC_MODE_ACTIVE(BOXOSDALT2))
5028 activeLayout = 2;
5029 else
5030 #endif
5031 if (IS_RC_MODE_ACTIVE(BOXOSDALT1))
5032 activeLayout = 1;
5033 else
5034 #ifdef USE_PROGRAMMING_FRAMEWORK
5035 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT))
5036 activeLayout = constrain(logicConditionValuesByType[LOGIC_CONDITION_SET_OSD_LAYOUT], 0, OSD_ALTERNATE_LAYOUT_COUNT);
5037 else
5038 #endif
5039 activeLayout = 0;
5041 if (currentLayout != activeLayout) {
5042 currentLayout = activeLayout;
5043 osdStartFullRedraw();
5045 #endif
5047 #define DRAW_FREQ_DENOM 4
5048 #define STATS_FREQ_DENOM 50
5049 counter++;
5051 if ((counter % STATS_FREQ_DENOM) == 0) {
5052 osdUpdateStats();
5055 if ((counter % DRAW_FREQ_DENOM) == 0) {
5056 // redraw values in buffer
5057 osdRefresh(currentTimeUs);
5058 } else {
5059 // rest of time redraw screen
5060 displayDrawScreen(osdDisplayPort);
5063 #ifdef USE_CMS
5064 // do not allow ARM if we are in menu
5065 if (displayIsGrabbed(osdDisplayPort)) {
5066 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
5067 } else {
5068 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
5070 #endif
5073 void osdStartFullRedraw(void)
5075 fullRedraw = true;
5078 void osdOverrideLayout(int layout, timeMs_t duration)
5080 layoutOverride = constrain(layout, -1, ARRAYLEN(osdLayoutsConfig()->item_pos) - 1);
5081 if (layoutOverride >= 0 && duration > 0) {
5082 layoutOverrideUntil = millis() + duration;
5083 } else {
5084 layoutOverrideUntil = 0;
5088 int osdGetActiveLayout(bool *overridden)
5090 if (overridden) {
5091 *overridden = layoutOverride >= 0;
5093 return currentLayout;
5096 bool osdItemIsFixed(osd_items_e item)
5098 return item == OSD_CROSSHAIRS ||
5099 item == OSD_ARTIFICIAL_HORIZON ||
5100 item == OSD_HORIZON_SIDEBARS;
5103 displayPort_t *osdGetDisplayPort(void)
5105 return osdDisplayPort;
5108 displayCanvas_t *osdGetDisplayPortCanvas(void)
5110 #if defined(USE_CANVAS)
5111 if (osdDisplayHasCanvas) {
5112 return &osdCanvas;
5114 #endif
5115 return NULL;
5118 timeMs_t systemMessageCycleTime(unsigned messageCount, const char **messages){
5119 uint8_t i = 0;
5120 float factor = 1.0f;
5121 while (i < messageCount) {
5122 if ((float)strlen(messages[i]) / 15.0f > factor) {
5123 factor = (float)strlen(messages[i]) / 15.0f;
5125 i++;
5127 return osdConfig()->system_msg_display_time * factor;
5130 textAttributes_t osdGetSystemMessage(char *buff, size_t buff_size, bool isCenteredText)
5132 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
5134 if (buff != NULL) {
5135 const char *message = NULL;
5136 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
5137 // We might have up to 5 messages to show.
5138 const char *messages[5];
5139 unsigned messageCount = 0;
5140 const char *failsafeInfoMessage = NULL;
5141 const char *invertedInfoMessage = NULL;
5143 if (ARMING_FLAG(ARMED)) {
5144 #ifdef USE_FW_AUTOLAND
5145 if (FLIGHT_MODE(FAILSAFE_MODE) || FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding() || FLIGHT_MODE(NAV_FW_AUTOLAND)) {
5146 if (isWaypointMissionRTHActive() && !posControl.fwLandState.landWp) {
5147 #else
5148 if (FLIGHT_MODE(FAILSAFE_MODE) || FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding()) {
5149 if (isWaypointMissionRTHActive()) {
5150 #endif
5151 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
5152 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL);
5154 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE) {
5155 messages[messageCount++] = STATE(LANDING_DETECTED) ? OSD_MESSAGE_STR(OSD_MSG_WP_LANDED) : OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED);
5156 } else if (NAV_Status.state == MW_NAV_STATE_WP_ENROUTE) {
5157 // Countdown display for remaining Waypoints
5158 char buf[6];
5159 osdFormatDistanceSymbol(buf, posControl.wpDistance, 0);
5160 tfp_sprintf(messageBuf, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl.activeWaypointIndex), posControl.geoWaypointCount, buf);
5161 messages[messageCount++] = messageBuf;
5162 } else if (NAV_Status.state == MW_NAV_STATE_HOLD_TIMED) {
5163 if (navConfig()->general.waypoint_enforce_altitude && !posControl.wpAltitudeReached) {
5164 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ADJUSTING_WP_ALT);
5165 } else {
5166 // WP hold time countdown in seconds
5167 timeMs_t currentTime = millis();
5168 int holdTimeRemaining = posControl.waypointList[posControl.activeWaypointIndex].p1 - (int)(MS2S(currentTime - posControl.wpReachedTime));
5169 holdTimeRemaining = holdTimeRemaining >= 0 ? holdTimeRemaining : 0;
5171 tfp_sprintf(messageBuf, "HOLDING WP FOR %2u S", holdTimeRemaining);
5173 messages[messageCount++] = messageBuf;
5175 } else if (NAV_Status.state == MW_NAV_STATE_LAND_SETTLE && posControl.landingDelay > 0) {
5176 uint16_t remainingHoldSec = MS2S(posControl.landingDelay - millis());
5177 tfp_sprintf(messageBuf, "LANDING DELAY: %3u SECONDS", remainingHoldSec);
5179 messages[messageCount++] = messageBuf;
5182 else {
5183 #ifdef USE_FW_AUTOLAND
5184 if (canFwLandingBeCancelled()) {
5185 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MOVE_STICKS);
5186 } else if (!FLIGHT_MODE(NAV_FW_AUTOLAND)) {
5187 #endif
5188 const char *navStateMessage = navigationStateMessage();
5189 if (navStateMessage) {
5190 messages[messageCount++] = navStateMessage;
5192 #ifdef USE_FW_AUTOLAND
5194 #endif
5197 #if defined(USE_SAFE_HOME)
5198 const char *safehomeMessage = divertingToSafehomeMessage();
5199 if (safehomeMessage) {
5200 messages[messageCount++] = safehomeMessage;
5202 #endif
5203 if (FLIGHT_MODE(FAILSAFE_MODE)) {
5204 // In FS mode while being armed too
5205 const char *failsafePhaseMessage = osdFailsafePhaseMessage();
5206 failsafeInfoMessage = osdFailsafeInfoMessage();
5208 if (failsafePhaseMessage) {
5209 messages[messageCount++] = failsafePhaseMessage;
5211 if (failsafeInfoMessage) {
5212 messages[messageCount++] = failsafeInfoMessage;
5215 } else { /* messages shown only when Failsafe, WP, RTH or Emergency Landing not active */
5216 if (STATE(FIXED_WING_LEGACY) && (navGetCurrentStateFlags() & NAV_CTL_LAUNCH)) {
5217 messages[messageCount++] = navConfig()->fw.launch_manual_throttle ? OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH_MANUAL) :
5218 OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH);
5219 const char *launchStateMessage = fixedWingLaunchStateMessage();
5220 if (launchStateMessage) {
5221 messages[messageCount++] = launchStateMessage;
5223 } else {
5224 if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && !navigationRequiresAngleMode()) {
5225 // ALTHOLD might be enabled alongside ANGLE/HORIZON/ANGLEHOLD/ACRO
5226 // when it doesn't require ANGLE mode (required only in FW
5227 // right now). If it requires ANGLE, its display is handled by OSD_FLYMODE.
5228 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD);
5230 if (STATE(MULTIROTOR) && FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
5231 if (posControl.cruise.multicopterSpeed >= 50.0f) {
5232 char buf[6];
5233 osdFormatVelocityStr(buf, posControl.cruise.multicopterSpeed, false, false);
5234 tfp_sprintf(messageBuf, "(SPD %s)", buf);
5235 } else {
5236 strcpy(messageBuf, "(HOLD)");
5238 messages[messageCount++] = messageBuf;
5240 if (IS_RC_MODE_ACTIVE(BOXAUTOTRIM) && !feature(FEATURE_FW_AUTOTRIM)) {
5241 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM);
5243 if (IS_RC_MODE_ACTIVE(BOXAUTOTUNE)) {
5244 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE);
5245 if (FLIGHT_MODE(MANUAL_MODE)) {
5246 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO);
5249 if (isFixedWingLevelTrimActive()) {
5250 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOLEVEL);
5252 if (FLIGHT_MODE(HEADFREE_MODE)) {
5253 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE);
5255 if (FLIGHT_MODE(SOARING_MODE)) {
5256 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING);
5258 if (posControl.flags.wpMissionPlannerActive) {
5259 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER);
5261 if (STATE(LANDING_DETECTED)) {
5262 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_LANDED);
5264 if (IS_RC_MODE_ACTIVE(BOXANGLEHOLD)) {
5265 int8_t navAngleHoldAxis = navCheckActiveAngleHoldAxis();
5266 if (isAngleHoldLevel()) {
5267 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_LEVEL);
5268 } else if (navAngleHoldAxis == FD_ROLL) {
5269 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_ROLL);
5270 } else if (navAngleHoldAxis == FD_PITCH) {
5271 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_PITCH);
5276 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS)) {
5277 unsigned invalidIndex;
5279 // Check if we're unable to arm for some reason
5280 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING) && !settingsValidate(&invalidIndex)) {
5282 const setting_t *setting = settingGet(invalidIndex);
5283 settingGetName(setting, messageBuf);
5284 for (int ii = 0; messageBuf[ii]; ii++) {
5285 messageBuf[ii] = sl_toupper(messageBuf[ii]);
5287 invertedInfoMessage = messageBuf;
5288 messages[messageCount++] = invertedInfoMessage;
5290 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
5291 messages[messageCount++] = invertedInfoMessage;
5293 } else {
5295 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM);
5296 messages[messageCount++] = invertedInfoMessage;
5298 // Show the reason for not arming
5299 messages[messageCount++] = osdArmingDisabledReasonMessage();
5302 } else if (!ARMING_FLAG(ARMED)) {
5303 if (isWaypointListValid()) {
5304 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED);
5308 /* Messages that are shown regardless of Arming state */
5310 if (savingSettings == true) {
5311 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS);
5312 } else if (notify_settings_saved > 0) {
5313 if (millis() > notify_settings_saved) {
5314 notify_settings_saved = 0;
5315 } else {
5316 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED);
5320 if (messageCount > 0) {
5321 message = messages[OSD_ALTERNATING_CHOICES(systemMessageCycleTime(messageCount, messages), messageCount)];
5322 if (message == failsafeInfoMessage) {
5323 // failsafeInfoMessage is not useful for recovering
5324 // a lost model, but might help avoiding a crash.
5325 // Blink to grab user attention.
5326 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
5327 } else if (message == invertedInfoMessage) {
5328 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
5330 // We're shoing either failsafePhaseMessage or
5331 // navStateMessage. Don't BLINK here since
5332 // having this text available might be crucial
5333 // during a lost aircraft recovery and blinking
5334 // will cause it to be missing from some frames.
5337 osdFormatMessage(buff, buff_size, message, isCenteredText);
5339 return elemAttr;
5342 void osdResetWarningFlags(void)
5344 osdWarningsFlags = 0;
5347 static bool osdCheckWarning(bool condition, uint8_t warningFlag, uint8_t *warningsCount)
5349 #define WARNING_REDISPLAY_DURATION 5000; // milliseconds
5351 const timeMs_t currentTimeMs = millis();
5352 static timeMs_t warningDisplayStartTime = 0;
5353 static timeMs_t redisplayStartTimeMs = 0;
5354 static uint16_t osdWarningTimerDuration;
5355 static uint8_t newWarningFlags;
5357 if (condition) { // condition required to trigger warning
5358 if (!(osdWarningsFlags & warningFlag)) {
5359 osdWarningsFlags |= warningFlag;
5360 newWarningFlags |= warningFlag;
5361 redisplayStartTimeMs = 0;
5363 #ifdef USE_DEV_TOOLS
5364 if (systemConfig()->groundTestMode) {
5365 return true;
5367 #endif
5368 /* Warnings displayed in full for set time before shrinking down to alert symbol with warning count only.
5369 * All current warnings then redisplayed for 5s on 30s rolling cycle.
5370 * New warnings dislayed individually for 10s */
5371 if (currentTimeMs > redisplayStartTimeMs) {
5372 warningDisplayStartTime = currentTimeMs;
5373 osdWarningTimerDuration = newWarningFlags ? 10000 : WARNING_REDISPLAY_DURATION;
5374 redisplayStartTimeMs = currentTimeMs + osdWarningTimerDuration + 30000;
5377 if (currentTimeMs - warningDisplayStartTime < osdWarningTimerDuration) {
5378 return (newWarningFlags & warningFlag) || osdWarningTimerDuration == WARNING_REDISPLAY_DURATION;
5379 } else {
5380 newWarningFlags = 0;
5382 *warningsCount += 1;
5383 } else if (osdWarningsFlags & warningFlag) {
5384 osdWarningsFlags &= ~warningFlag;
5387 return false;
5390 static textAttributes_t osdGetMultiFunctionMessage(char *buff)
5392 /* Message length limit 10 char max */
5394 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
5395 static uint8_t warningsCount;
5396 const char *message = NULL;
5398 #ifdef USE_MULTI_FUNCTIONS
5399 /* --- FUNCTIONS --- */
5400 multi_function_e selectedFunction = multiFunctionSelection();
5402 if (selectedFunction) {
5403 multi_function_e activeFunction = selectedFunction;
5405 switch (selectedFunction) {
5406 case MULTI_FUNC_NONE:
5407 case MULTI_FUNC_1:
5408 message = warningsCount ? "WARNINGS !" : "0 WARNINGS";
5409 break;
5410 case MULTI_FUNC_2:
5411 message = posControl.flags.manualEmergLandActive ? "ABORT LAND" : "EMERG LAND";
5412 break;
5413 case MULTI_FUNC_3:
5414 #if defined(USE_SAFE_HOME)
5415 if (navConfig()->general.flags.safehome_usage_mode != SAFEHOME_USAGE_OFF) {
5416 message = MULTI_FUNC_FLAG(MF_SUSPEND_SAFEHOMES) ? "USE SFHOME" : "SUS SFHOME";
5417 break;
5419 #endif
5420 activeFunction++;
5421 FALLTHROUGH;
5422 case MULTI_FUNC_4:
5423 if (navConfig()->general.flags.rth_trackback_mode != RTH_TRACKBACK_OFF) {
5424 message = MULTI_FUNC_FLAG(MF_SUSPEND_TRACKBACK) ? "USE TKBACK" : "SUS TKBACK";
5425 break;
5427 activeFunction++;
5428 FALLTHROUGH;
5429 case MULTI_FUNC_5:
5430 #ifdef USE_DSHOT
5431 if (STATE(MULTIROTOR)) {
5432 message = MULTI_FUNC_FLAG(MF_TURTLE_MODE) ? "END TURTLE" : "USE TURTLE";
5433 break;
5435 #endif
5436 activeFunction++;
5437 FALLTHROUGH;
5438 case MULTI_FUNC_6:
5439 message = ARMING_FLAG(ARMED) ? "NOW ARMED " : "EMERG ARM ";
5440 break;
5441 case MULTI_FUNC_END:
5442 break;
5445 if (activeFunction != selectedFunction) {
5446 setMultifunctionSelection(activeFunction);
5449 strcpy(buff, message);
5451 if (isNextMultifunctionItemAvailable()) {
5452 // provides feedback indicating when a new selection command has been received by flight controller
5453 buff[9] = '>';
5456 return elemAttr;
5458 #endif // MULTIFUNCTION - functions only, warnings always defined
5460 /* --- WARNINGS --- */
5461 const char *messages[7];
5462 uint8_t messageCount = 0;
5463 bool warningCondition = false;
5464 warningsCount = 0;
5465 uint8_t warningFlagID = 1;
5467 // Low Battery
5468 const batteryState_e batteryState = getBatteryState();
5469 warningCondition = batteryState == BATTERY_CRITICAL || batteryState == BATTERY_WARNING;
5470 if (osdCheckWarning(warningCondition, warningFlagID, &warningsCount)) {
5471 messages[messageCount++] = batteryState == BATTERY_CRITICAL ? "BATT EMPTY" : "BATT LOW !";
5474 #if defined(USE_GPS)
5475 // GPS Fix and Failure
5476 if (feature(FEATURE_GPS)) {
5477 if (osdCheckWarning(!STATE(GPS_FIX), warningFlagID <<= 1, &warningsCount)) {
5478 bool gpsFailed = getHwGPSStatus() == HW_SENSOR_UNAVAILABLE;
5479 messages[messageCount++] = gpsFailed ? "GPS FAILED" : "NO GPS FIX";
5483 // RTH sanity (warning if RTH heads 200m further away from home than closest point)
5484 warningCondition = NAV_Status.state == MW_NAV_STATE_RTH_ENROUTE && !posControl.flags.rthTrackbackActive &&
5485 (posControl.homeDistance - posControl.rthSanityChecker.minimalDistanceToHome) > 20000;
5486 if (osdCheckWarning(warningCondition, warningFlagID <<= 1, &warningsCount)) {
5487 messages[messageCount++] = "RTH SANITY";
5490 // Altitude sanity (warning if significant mismatch between estimated and GPS altitude)
5491 if (osdCheckWarning(posControl.flags.gpsCfEstimatedAltitudeMismatch, warningFlagID <<= 1, &warningsCount)) {
5492 messages[messageCount++] = "ALT SANITY";
5494 #endif
5496 #if defined(USE_MAG)
5497 // Magnetometer failure
5498 if (requestedSensors[SENSOR_INDEX_MAG] != MAG_NONE) {
5499 hardwareSensorStatus_e magStatus = getHwCompassStatus();
5500 if (osdCheckWarning(magStatus == HW_SENSOR_UNAVAILABLE || magStatus == HW_SENSOR_UNHEALTHY, warningFlagID <<= 1, &warningsCount)) {
5501 messages[messageCount++] = "MAG FAILED";
5504 #endif
5505 // Vibration levels TODO - needs better vibration measurement to be useful
5506 // const float vibrationLevel = accGetVibrationLevel();
5507 // warningCondition = vibrationLevel > 1.5f;
5508 // if (osdCheckWarning(warningCondition, warningFlagID <<= 1, &warningsCount)) {
5509 // messages[messageCount++] = vibrationLevel > 2.5f ? "BAD VIBRTN" : "VIBRATION!";
5510 // }
5512 #ifdef USE_DEV_TOOLS
5513 if (osdCheckWarning(systemConfig()->groundTestMode, warningFlagID <<= 1, &warningsCount)) {
5514 messages[messageCount++] = "GRD TEST !";
5516 #endif
5518 if (messageCount) {
5519 message = messages[OSD_ALTERNATING_CHOICES(1000, messageCount)]; // display each warning on 1s cycle
5520 strcpy(buff, message);
5521 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
5522 } else if (warningsCount) {
5523 buff[0] = SYM_ALERT;
5524 tfp_sprintf(buff + 1, "%u ", warningsCount);
5527 return elemAttr;
5530 #endif // OSD