fix mission planner message
[inav.git] / src / main / io / osd.c
blob01bdc1764fb40d788d1eb790dfd008b163dbc5c3
1 /*
2 * This file is part of Cleanflight.
4 * Cleanflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Cleanflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
19 Created by Marcin Baliniak
20 some functions based on MinimOSD
22 OSD-CMS separation by jflyper
25 #include <stdbool.h>
26 #include <stdint.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <ctype.h>
30 #include <math.h>
31 #include <inttypes.h>
33 #include "platform.h"
35 #ifdef USE_OSD
37 #include "build/debug.h"
38 #include "build/version.h"
40 #include "cms/cms.h"
41 #include "cms/cms_types.h"
42 #include "cms/cms_menu_osd.h"
44 #include "common/axis.h"
45 #include "common/constants.h"
46 #include "common/filter.h"
47 #include "common/log.h"
48 #include "common/olc.h"
49 #include "common/printf.h"
50 #include "common/string_light.h"
51 #include "common/time.h"
52 #include "common/typeconversion.h"
53 #include "common/utils.h"
55 #include "config/feature.h"
56 #include "config/parameter_group.h"
57 #include "config/parameter_group_ids.h"
59 #include "drivers/display.h"
60 #include "drivers/display_canvas.h"
61 #include "drivers/display_font_metadata.h"
62 #include "drivers/osd_symbols.h"
63 #include "drivers/time.h"
64 #include "drivers/vtx_common.h"
66 #include "io/adsb.h"
67 #include "io/flashfs.h"
68 #include "io/gps.h"
69 #include "io/osd.h"
70 #include "io/osd_common.h"
71 #include "io/osd_hud.h"
72 #include "io/osd_utils.h"
73 #include "io/displayport_msp_dji_compat.h"
74 #include "io/vtx.h"
75 #include "io/vtx_string.h"
77 #include "io/osd/custom_elements.h"
79 #include "fc/config.h"
80 #include "fc/controlrate_profile.h"
81 #include "fc/fc_core.h"
82 #include "fc/fc_tasks.h"
83 #include "fc/multifunction.h"
84 #include "fc/rc_adjustments.h"
85 #include "fc/rc_controls.h"
86 #include "fc/rc_modes.h"
87 #include "fc/runtime_config.h"
88 #include "fc/settings.h"
90 #include "flight/imu.h"
91 #include "flight/mixer.h"
92 #include "flight/pid.h"
93 #include "flight/power_limits.h"
94 #include "flight/rth_estimator.h"
95 #include "flight/servos.h"
96 #include "flight/wind_estimator.h"
98 #include "navigation/navigation.h"
99 #include "navigation/navigation_private.h"
101 #include "rx/rx.h"
102 #include "rx/msp_override.h"
104 #include "sensors/acceleration.h"
105 #include "sensors/battery.h"
106 #include "sensors/boardalignment.h"
107 #include "sensors/compass.h"
108 #include "sensors/diagnostics.h"
109 #include "sensors/sensors.h"
110 #include "sensors/pitotmeter.h"
111 #include "sensors/temperature.h"
112 #include "sensors/esc_sensor.h"
113 #include "sensors/rangefinder.h"
115 #include "programming/logic_condition.h"
116 #include "programming/global_variables.h"
118 #ifdef USE_BLACKBOX
119 #include "blackbox/blackbox_io.h"
120 #endif
122 #ifdef USE_HARDWARE_REVISION_DETECTION
123 #include "hardware_revision.h"
124 #endif
126 #define VIDEO_BUFFER_CHARS_PAL 480
127 #define VIDEO_BUFFER_CHARS_HDZERO 900
128 #define VIDEO_BUFFER_CHARS_DJIWTF 1320
130 #define GFORCE_FILTER_TC 0.2
132 #define OSD_STATS_SINGLE_PAGE_MIN_ROWS 18
133 #define IS_HI(X) (rxGetChannelValue(X) > 1750)
134 #define IS_LO(X) (rxGetChannelValue(X) < 1250)
135 #define IS_MID(X) (rxGetChannelValue(X) > 1250 && rxGetChannelValue(X) < 1750)
137 #define OSD_RESUME_UPDATES_STICK_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
138 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
139 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
141 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
142 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
144 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
146 // Adjust OSD_MESSAGE's default position when
147 // changing OSD_MESSAGE_LENGTH
148 #define OSD_MESSAGE_LENGTH 28
149 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
150 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
151 // Wrap all string constants intenteded for display as messages with
152 // this macro to ensure compile time length validation.
153 #define OSD_MESSAGE_STR(x) ({ \
154 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
155 x; \
158 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
160 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
161 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
163 #define OSD_MIN_FONT_VERSION 3
165 static timeMs_t linearDescentMessageMs = 0;
166 static timeMs_t notify_settings_saved = 0;
167 static bool savingSettings = false;
169 static unsigned currentLayout = 0;
170 static int layoutOverride = -1;
171 static bool hasExtendedFont = false; // Wether the font supports characters > 256
172 static timeMs_t layoutOverrideUntil = 0;
173 static pt1Filter_t GForceFilter, GForceFilterAxis[XYZ_AXIS_COUNT];
174 static float GForce, GForceAxis[XYZ_AXIS_COUNT];
176 typedef struct statistic_s {
177 uint16_t max_speed;
178 uint16_t max_3D_speed;
179 uint16_t max_air_speed;
180 uint16_t min_voltage; // /100
181 int16_t max_current;
182 int32_t max_power;
183 int16_t min_rssi;
184 int16_t min_lq; // for CRSF
185 int16_t min_rssi_dbm; // for CRSF
186 int32_t max_altitude;
187 uint32_t max_distance;
188 uint8_t min_sats;
189 uint8_t max_sats;
190 int16_t max_esc_temp;
191 int16_t min_esc_temp;
192 int32_t flightStartMAh;
193 int32_t flightStartMWh;
194 } statistic_t;
196 static statistic_t stats;
198 static timeUs_t resumeRefreshAt = 0;
199 static bool refreshWaitForResumeCmdRelease;
201 static bool fullRedraw = false;
203 static uint8_t armState;
205 static textAttributes_t osdGetMultiFunctionMessage(char *buff);
206 static uint8_t osdWarningsFlags = 0;
208 typedef struct osdMapData_s {
209 uint32_t scale;
210 char referenceSymbol;
211 } osdMapData_t;
213 static osdMapData_t osdMapData;
215 static displayPort_t *osdDisplayPort;
216 static bool osdDisplayIsReady = false;
217 #if defined(USE_CANVAS)
218 static displayCanvas_t osdCanvas;
219 static bool osdDisplayHasCanvas;
220 #else
221 #define osdDisplayHasCanvas false
222 #endif
224 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
226 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t, osdConfig, PG_OSD_CONFIG, 10);
227 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t, osdLayoutsConfig, PG_OSD_LAYOUTS_CONFIG, 1);
229 void osdStartedSaveProcess(void) {
230 savingSettings = true;
233 void osdShowEEPROMSavedNotification(void) {
234 savingSettings = false;
235 notify_settings_saved = millis() + 5000;
238 bool osdDisplayIsPAL(void)
240 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL;
243 bool osdDisplayIsHD(void)
245 if (displayScreenSize(osdDisplayPort) >= VIDEO_BUFFER_CHARS_HDZERO)
247 return true;
249 return false;
252 bool osdIsNotMetric(void) {
253 return !(osdConfig()->units == OSD_UNIT_METRIC || osdConfig()->units == OSD_UNIT_METRIC_MPH);
257 * Converts distance into a string based on the current unit system
258 * prefixed by a a symbol to indicate the unit used.
259 * @param dist Distance in centimeters
261 static void osdFormatDistanceSymbol(char *buff, int32_t dist, uint8_t decimals)
263 uint8_t digits = 3U; // Total number of digits (including decimal point)
264 uint8_t sym_index = 3U; // Position (index) at buffer of units symbol
265 uint8_t symbol_m = SYM_DIST_M;
266 uint8_t symbol_km = SYM_DIST_KM;
267 uint8_t symbol_ft = SYM_DIST_FT;
268 uint8_t symbol_mi = SYM_DIST_MI;
269 uint8_t symbol_nm = SYM_DIST_NM;
271 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
272 if (isDJICompatibleVideoSystem(osdConfig())) {
273 // Add one digit so up no switch to scaled decimal occurs above 99
274 digits = 4U;
275 sym_index = 4U;
276 // Use altitude symbols on purpose, as it seems distance symbols are not defined in DJICOMPAT mode
277 symbol_m = SYM_ALT_M;
278 symbol_km = SYM_ALT_KM;
279 symbol_ft = SYM_ALT_FT;
280 symbol_mi = SYM_MI;
281 symbol_nm = SYM_MI;
283 #endif
285 switch ((osd_unit_e)osdConfig()->units) {
286 case OSD_UNIT_UK:
287 FALLTHROUGH;
288 case OSD_UNIT_IMPERIAL:
289 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_MILE, decimals, 3, digits, false)) {
290 buff[sym_index] = symbol_mi;
291 } else {
292 buff[sym_index] = symbol_ft;
294 buff[sym_index + 1] = '\0';
295 break;
296 case OSD_UNIT_METRIC_MPH:
297 FALLTHROUGH;
298 case OSD_UNIT_METRIC:
299 if (osdFormatCentiNumber(buff, dist, METERS_PER_KILOMETER, decimals, 3, digits, false)) {
300 buff[sym_index] = symbol_km;
301 } else {
302 buff[sym_index] = symbol_m;
304 buff[sym_index + 1] = '\0';
305 break;
306 case OSD_UNIT_GA:
307 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), (uint32_t)FEET_PER_NAUTICALMILE, decimals, 3, digits, false)) {
308 buff[sym_index] = symbol_nm;
309 } else {
310 buff[sym_index] = symbol_ft;
312 buff[sym_index + 1] = '\0';
313 break;
318 * Converts distance into a string based on the current unit system.
319 * @param dist Distance in centimeters
321 static void osdFormatDistanceStr(char *buff, int32_t dist)
323 int32_t centifeet;
324 switch ((osd_unit_e)osdConfig()->units) {
325 case OSD_UNIT_UK:
326 FALLTHROUGH;
327 case OSD_UNIT_IMPERIAL:
328 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
329 if (abs(centifeet) < FEET_PER_MILE * 100 / 2) {
330 // Show feet when dist < 0.5mi
331 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
332 } else {
333 // Show miles when dist >= 0.5mi
334 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100*FEET_PER_MILE)),
335 (abs(centifeet) % (100 * FEET_PER_MILE)) / FEET_PER_MILE, SYM_MI);
337 break;
338 case OSD_UNIT_METRIC_MPH:
339 FALLTHROUGH;
340 case OSD_UNIT_METRIC:
341 if (abs(dist) < METERS_PER_KILOMETER * 100) {
342 // Show meters when dist < 1km
343 tfp_sprintf(buff, "%d%c", (int)(dist / 100), SYM_M);
344 } else {
345 // Show kilometers when dist >= 1km
346 tfp_sprintf(buff, "%d.%02d%c", (int)(dist / (100*METERS_PER_KILOMETER)),
347 (abs(dist) % (100 * METERS_PER_KILOMETER)) / METERS_PER_KILOMETER, SYM_KM);
349 break;
350 case OSD_UNIT_GA:
351 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
352 if (abs(centifeet) < 100000) {
353 // Show feet when dist < 1000ft
354 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
355 } else {
356 // Show nautical miles when dist >= 1000ft
357 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100 * FEET_PER_NAUTICALMILE)),
358 (int)((abs(centifeet) % (int)(100 * FEET_PER_NAUTICALMILE)) / FEET_PER_NAUTICALMILE), SYM_NM);
360 break;
365 * Converts velocity based on the current unit system (kmh or mph).
366 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
368 static int32_t osdConvertVelocityToUnit(int32_t vel)
370 switch ((osd_unit_e)osdConfig()->units) {
371 case OSD_UNIT_UK:
372 FALLTHROUGH;
373 case OSD_UNIT_METRIC_MPH:
374 FALLTHROUGH;
375 case OSD_UNIT_IMPERIAL:
376 return CMSEC_TO_CENTIMPH(vel) / 100; // Convert to mph
377 case OSD_UNIT_METRIC:
378 return CMSEC_TO_CENTIKPH(vel) / 100; // Convert to kmh
379 case OSD_UNIT_GA:
380 return CMSEC_TO_CENTIKNOTS(vel) / 100; // Convert to Knots
382 // Unreachable
383 return -1;
387 * Converts velocity into a string based on the current unit system.
388 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
389 * @param _3D is a 3D velocity
390 * @param _max is a maximum velocity
392 void osdFormatVelocityStr(char* buff, int32_t vel, bool _3D, bool _max)
394 switch ((osd_unit_e)osdConfig()->units) {
395 case OSD_UNIT_UK:
396 FALLTHROUGH;
397 case OSD_UNIT_METRIC_MPH:
398 FALLTHROUGH;
399 case OSD_UNIT_IMPERIAL:
400 if (_max) {
401 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
402 } else {
403 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
405 break;
406 case OSD_UNIT_METRIC:
407 if (_max) {
408 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
409 } else {
410 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
412 break;
413 case OSD_UNIT_GA:
414 if (_max) {
415 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
416 } else {
417 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
419 break;
424 * 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
426 static void osdGenerateAverageVelocityStr(char* buff) {
427 uint32_t cmPerSec = getTotalTravelDistance() / getFlightTime();
428 osdFormatVelocityStr(buff, cmPerSec, false, false);
432 * Converts wind speed into a string based on the current unit system, using
433 * always 3 digits and an additional character for the unit at the right. buff
434 * is null terminated.
435 * @param ws Raw wind speed in cm/s
437 #ifdef USE_WIND_ESTIMATOR
438 static void osdFormatWindSpeedStr(char *buff, int32_t ws, bool isValid)
440 int32_t centivalue;
441 char suffix;
442 switch (osdConfig()->units) {
443 case OSD_UNIT_UK:
444 FALLTHROUGH;
445 case OSD_UNIT_METRIC_MPH:
446 FALLTHROUGH;
447 case OSD_UNIT_IMPERIAL:
448 centivalue = CMSEC_TO_CENTIMPH(ws);
449 suffix = SYM_MPH;
450 break;
451 case OSD_UNIT_GA:
452 centivalue = CMSEC_TO_CENTIKNOTS(ws);
453 suffix = SYM_KT;
454 break;
455 default:
456 case OSD_UNIT_METRIC:
457 if (osdConfig()->estimations_wind_mps)
459 centivalue = ws;
460 suffix = SYM_MS;
462 else
464 centivalue = CMSEC_TO_CENTIKPH(ws);
465 suffix = SYM_KMH;
467 break;
470 osdFormatCentiNumber(buff, centivalue, 0, 2, 0, 3, false);
472 if (!isValid && ((millis() / 1000) % 4 < 2))
473 suffix = '*';
475 buff[3] = suffix;
476 buff[4] = '\0';
478 #endif
481 * This is a simplified altitude conversion code that does not use any scaling
482 * but is fully compatible with the DJI G2 MSP Displayport OSD implementation.
484 /* void osdSimpleAltitudeSymbol(char *buff, int32_t alt) {
486 int32_t convertedAltutude = 0;
487 char suffix = '\0';
489 switch ((osd_unit_e)osdConfig()->units) {
490 case OSD_UNIT_UK:
491 FALLTHROUGH;
492 case OSD_UNIT_GA:
493 FALLTHROUGH;
494 case OSD_UNIT_IMPERIAL:
495 convertedAltutude = CENTIMETERS_TO_FEET(alt);
496 suffix = SYM_ALT_FT;
497 break;
498 case OSD_UNIT_METRIC_MPH:
499 FALLTHROUGH;
500 case OSD_UNIT_METRIC:
501 convertedAltutude = CENTIMETERS_TO_METERS(alt);
502 suffix = SYM_ALT_M;
503 break;
506 tfp_sprintf(buff, "%4d", (int) convertedAltutude);
507 buff[4] = suffix;
508 buff[5] = '\0';
509 } */
512 * Converts altitude into a string based on the current unit system
513 * prefixed by a a symbol to indicate the unit used.
514 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
516 void osdFormatAltitudeSymbol(char *buff, int32_t alt)
518 uint8_t totalDigits = 4U;
519 uint8_t digits = 4U;
520 uint8_t symbolIndex = 4U;
521 uint8_t symbolKFt = SYM_ALT_KFT;
523 if (alt >= 0) {
524 digits = 3U;
525 buff[0] = ' ';
528 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
529 if (isDJICompatibleVideoSystem(osdConfig())) {
530 totalDigits++;
531 digits++;
532 symbolIndex++;
533 symbolKFt = SYM_ALT_FT;
535 #endif
537 switch ((osd_unit_e)osdConfig()->units) {
538 case OSD_UNIT_UK:
539 FALLTHROUGH;
540 case OSD_UNIT_GA:
541 FALLTHROUGH;
542 case OSD_UNIT_IMPERIAL:
543 if (osdFormatCentiNumber(buff + totalDigits - digits, CENTIMETERS_TO_CENTIFEET(alt), 1000, 0, 2, digits, false)) {
544 // Scaled to kft
545 buff[symbolIndex++] = symbolKFt;
546 } else {
547 // Formatted in feet
548 buff[symbolIndex++] = SYM_ALT_FT;
550 buff[symbolIndex] = '\0';
551 break;
552 case OSD_UNIT_METRIC_MPH:
553 FALLTHROUGH;
554 case OSD_UNIT_METRIC:
555 // alt is alredy in cm
556 if (osdFormatCentiNumber(buff + totalDigits - digits, alt, 1000, 0, 2, digits, false)) {
557 // Scaled to km
558 buff[symbolIndex++] = SYM_ALT_KM;
559 } else {
560 // Formatted in m
561 buff[symbolIndex++] = SYM_ALT_M;
563 buff[symbolIndex] = '\0';
564 break;
569 * Converts altitude into a string based on the current unit system.
570 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
572 static void osdFormatAltitudeStr(char *buff, int32_t alt)
574 int32_t value;
575 switch ((osd_unit_e)osdConfig()->units) {
576 case OSD_UNIT_UK:
577 FALLTHROUGH;
578 case OSD_UNIT_GA:
579 FALLTHROUGH;
580 case OSD_UNIT_IMPERIAL:
581 value = CENTIMETERS_TO_FEET(alt);
582 tfp_sprintf(buff, "%d%c", (int)value, SYM_FT);
583 break;
584 case OSD_UNIT_METRIC_MPH:
585 FALLTHROUGH;
586 case OSD_UNIT_METRIC:
587 value = CENTIMETERS_TO_METERS(alt);
588 tfp_sprintf(buff, "%d%c", (int)value, SYM_M);
589 break;
593 static void osdFormatTime(char *buff, uint32_t seconds, char sym_m, char sym_h)
595 uint32_t value = seconds;
596 char sym = sym_m;
597 // Maximum value we can show in minutes is 99 minutes and 59 seconds
598 if (seconds > (99 * 60) + 59) {
599 sym = sym_h;
600 value = seconds / 60;
602 buff[0] = sym;
603 tfp_sprintf(buff + 1, "%02d:%02d", (int)(value / 60), (int)(value % 60));
606 static inline void osdFormatOnTime(char *buff)
608 osdFormatTime(buff, micros() / 1000000, SYM_ON_M, SYM_ON_H);
611 static inline void osdFormatFlyTime(char *buff, textAttributes_t *attr)
613 uint32_t seconds = getFlightTime();
614 osdFormatTime(buff, seconds, SYM_FLY_M, SYM_FLY_H);
615 if (attr && osdConfig()->time_alarm > 0) {
616 if (seconds / 60 >= osdConfig()->time_alarm && ARMING_FLAG(ARMED)) {
617 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
623 * Trim whitespace from string.
624 * Used in Stats screen on lines with multiple values.
626 char *osdFormatTrimWhiteSpace(char *buff)
628 char *end;
630 // Trim leading spaces
631 while(isspace((unsigned char)*buff)) buff++;
633 // All spaces?
634 if(*buff == 0)
635 return buff;
637 // Trim trailing spaces
638 end = buff + strlen(buff) - 1;
639 while(end > buff && isspace((unsigned char)*end)) end--;
641 // Write new null terminator character
642 end[1] = '\0';
644 return buff;
648 * Converts RSSI into a % value used by the OSD.
650 static uint16_t osdConvertRSSI(void)
652 // change range to [0, 99]
653 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE, 0, 99);
656 static uint16_t osdGetCrsfLQ(void)
658 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
659 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
660 int16_t displayedLQ = 0;
661 switch (osdConfig()->crsf_lq_format) {
662 case OSD_CRSF_LQ_TYPE1:
663 displayedLQ = statsLQ;
664 break;
665 case OSD_CRSF_LQ_TYPE2:
666 displayedLQ = statsLQ;
667 break;
668 case OSD_CRSF_LQ_TYPE3:
669 displayedLQ = rxLinkStatistics.rfMode >= 2 ? scaledLQ : statsLQ;
670 break;
672 return displayedLQ;
675 static int16_t osdGetCrsfdBm(void)
677 return rxLinkStatistics.uplinkRSSI;
680 * Displays a temperature postfixed with a symbol depending on the current unit system
681 * @param label to display
682 * @param valid true if measurement is valid
683 * @param temperature in deciDegrees Celcius
685 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)
687 char buff[TEMPERATURE_LABEL_LEN + 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN + 2];
688 textAttributes_t elemAttr = valid ? TEXT_ATTRIBUTES_NONE : _TEXT_ATTRIBUTES_BLINK_BIT;
689 uint8_t valueXOffset = 0;
691 if (symbol) {
692 buff[0] = symbol;
693 buff[1] = '\0';
694 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
695 valueXOffset = 1;
697 #ifdef USE_TEMPERATURE_SENSOR
698 else if (label[0] != '\0') {
699 uint8_t label_len = strnlen(label, TEMPERATURE_LABEL_LEN);
700 memcpy(buff, label, label_len);
701 memset(buff + label_len, ' ', TEMPERATURE_LABEL_LEN + 1 - label_len);
702 buff[5] = '\0';
703 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
704 valueXOffset = osdConfig()->temp_label_align == OSD_ALIGN_LEFT ? 5 : label_len + 1;
706 #else
707 UNUSED(label);
708 #endif
710 if (valid) {
712 if ((temperature <= alarm_min) || (temperature >= alarm_max)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
713 if (osdConfig()->units == OSD_UNIT_IMPERIAL) temperature = temperature * 9 / 5.0f + 320;
714 tfp_sprintf(buff, "%3d", temperature / 10);
716 } else
717 strcpy(buff, "---");
719 buff[3] = osdConfig()->units == OSD_UNIT_IMPERIAL ? SYM_TEMP_F : SYM_TEMP_C;
720 buff[4] = '\0';
722 displayWriteWithAttr(osdDisplayPort, elemPosX + valueXOffset, elemPosY, buff, elemAttr);
725 #ifdef USE_TEMPERATURE_SENSOR
726 static void osdDisplayTemperatureSensor(uint8_t elemPosX, uint8_t elemPosY, uint8_t sensorIndex)
728 int16_t temperature;
729 const bool valid = getSensorTemperature(sensorIndex, &temperature);
730 const tempSensorConfig_t *sensorConfig = tempSensorConfig(sensorIndex);
731 uint16_t symbol = sensorConfig->osdSymbol ? SYM_TEMP_SENSOR_FIRST + sensorConfig->osdSymbol - 1 : 0;
732 osdDisplayTemperature(elemPosX, elemPosY, symbol, sensorConfig->label, valid, temperature, sensorConfig->alarm_min, sensorConfig->alarm_max);
734 #endif
736 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
738 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
739 const int coordinateLength = osdConfig()->coordinate_digits + 1;
741 buff[0] = sym;
742 int32_t integerPart = val / GPS_DEGREES_DIVIDER;
743 // Latitude maximum integer width is 3 (-90) while
744 // longitude maximum integer width is 4 (-180).
745 int integerDigits = tfp_sprintf(buff + 1, (integerPart == 0 && val < 0) ? "-%d" : "%d", (int)integerPart);
746 // We can show up to 7 digits in decimalPart.
747 int32_t decimalPart = abs(val % (int)GPS_DEGREES_DIVIDER);
748 STATIC_ASSERT(GPS_DEGREES_DIVIDER == 1e7, adjust_max_decimal_digits);
749 int decimalDigits;
750 bool djiCompat = false; // Assume DJICOMPAT mode is no enabled
752 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
753 if(isDJICompatibleVideoSystem(osdConfig())) {
754 djiCompat = true;
756 #endif
758 if (!djiCompat) {
759 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, "%07d", (int)decimalPart);
760 // Embbed the decimal separator
761 buff[1 + integerDigits - 1] += SYM_ZERO_HALF_TRAILING_DOT - '0';
762 buff[1 + integerDigits] += SYM_ZERO_HALF_LEADING_DOT - '0';
763 } else {
764 // DJICOMPAT mode enabled
765 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, ".%06d", (int)decimalPart);
767 // Fill up to coordinateLength with zeros
768 int total = 1 + integerDigits + decimalDigits;
769 while(total < coordinateLength) {
770 buff[total] = '0';
771 total++;
773 buff[coordinateLength] = '\0';
776 static void osdFormatCraftName(char *buff)
778 if (strlen(systemConfig()->craftName) == 0)
779 strcpy(buff, "CRAFT_NAME");
780 else {
781 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
782 buff[i] = sl_toupper((unsigned char)systemConfig()->craftName[i]);
783 if (systemConfig()->craftName[i] == 0)
784 break;
789 void osdFormatPilotName(char *buff)
791 if (strlen(systemConfig()->pilotName) == 0)
792 strcpy(buff, "PILOT_NAME");
793 else {
794 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
795 buff[i] = sl_toupper((unsigned char)systemConfig()->pilotName[i]);
796 if (systemConfig()->pilotName[i] == 0)
797 break;
802 static const char * osdArmingDisabledReasonMessage(void)
804 const char *message = NULL;
805 static char messageBuf[OSD_MESSAGE_LENGTH+1];
807 switch (isArmingDisabledReason()) {
808 case ARMING_DISABLED_FAILSAFE_SYSTEM:
809 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
810 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING) {
811 if (failsafeIsReceivingRxData()) {
812 // reminder to disarm to exit FAILSAFE_RX_LOSS_MONITORING once timeout period ends
813 if (IS_RC_MODE_ACTIVE(BOXARM)) {
814 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF);
816 } else {
817 // Not receiving RX data
818 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
821 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS);
822 case ARMING_DISABLED_NOT_LEVEL:
823 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL);
824 case ARMING_DISABLED_SENSORS_CALIBRATING:
825 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL);
826 case ARMING_DISABLED_SYSTEM_OVERLOADED:
827 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED);
828 case ARMING_DISABLED_NAVIGATION_UNSAFE:
829 // Check the exact reason
830 switch (navigationIsBlockingArming(NULL)) {
831 char buf[6];
832 case NAV_ARMING_BLOCKER_NONE:
833 break;
834 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX:
835 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX);
836 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE:
837 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST);
838 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR:
839 osdFormatDistanceSymbol(buf, distanceToFirstWP(), 0);
840 tfp_sprintf(messageBuf, "FIRST WP TOO FAR (%s)", buf);
841 return message = messageBuf;
842 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR:
843 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG);
845 break;
846 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED:
847 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL);
848 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED:
849 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL);
850 case ARMING_DISABLED_ARM_SWITCH:
851 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST);
852 case ARMING_DISABLED_HARDWARE_FAILURE:
854 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
855 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE);
857 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
858 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL);
860 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
861 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL);
863 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
864 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL);
866 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
867 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL);
869 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
870 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL);
872 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
873 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL);
876 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL);
877 case ARMING_DISABLED_BOXFAILSAFE:
878 return OSD_MESSAGE_STR(OSD_MSG_FS_EN);
879 case ARMING_DISABLED_RC_LINK:
880 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK);
881 case ARMING_DISABLED_THROTTLE:
882 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW);
883 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED:
884 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER);
885 case ARMING_DISABLED_SERVO_AUTOTRIM:
886 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE);
887 case ARMING_DISABLED_OOM:
888 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY);
889 case ARMING_DISABLED_INVALID_SETTING:
890 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
891 case ARMING_DISABLED_CLI:
892 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE);
893 case ARMING_DISABLED_PWM_OUTPUT_ERROR:
894 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR);
895 case ARMING_DISABLED_NO_PREARM:
896 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM);
897 case ARMING_DISABLED_DSHOT_BEEPER:
898 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER);
899 // Cases without message
900 case ARMING_DISABLED_LANDING_DETECTED:
901 FALLTHROUGH;
902 case ARMING_DISABLED_CMS_MENU:
903 FALLTHROUGH;
904 case ARMING_DISABLED_OSD_MENU:
905 FALLTHROUGH;
906 case ARMING_DISABLED_ALL_FLAGS:
907 FALLTHROUGH;
908 case ARMED:
909 FALLTHROUGH;
910 case SIMULATOR_MODE_HITL:
911 FALLTHROUGH;
912 case SIMULATOR_MODE_SITL:
913 FALLTHROUGH;
914 case WAS_EVER_ARMED:
915 break;
917 return NULL;
920 static const char * osdFailsafePhaseMessage(void)
922 // See failsafe.h for each phase explanation
923 switch (failsafePhase()) {
924 case FAILSAFE_RETURN_TO_HOME:
925 // XXX: Keep this in sync with OSD_FLYMODE.
926 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS);
927 case FAILSAFE_LANDING:
928 // This should be considered an emergengy landing
929 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS);
930 case FAILSAFE_RX_LOSS_MONITORING:
931 // Only reachable from FAILSAFE_LANDED, which performs
932 // a disarm. Since aircraft has been disarmed, we no
933 // longer show failsafe details.
934 FALLTHROUGH;
935 case FAILSAFE_LANDED:
936 // Very brief, disarms and transitions into
937 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
938 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
939 // so we'll show the user how to re-arm in when
940 // that flag is the reason to prevent arming.
941 FALLTHROUGH;
942 case FAILSAFE_RX_LOSS_IDLE:
943 // This only happens when user has chosen NONE as FS
944 // procedure. The recovery messages should be enough.
945 FALLTHROUGH;
946 case FAILSAFE_IDLE:
947 // Failsafe not active
948 FALLTHROUGH;
949 case FAILSAFE_RX_LOSS_DETECTED:
950 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
951 // or the FS procedure immediately.
952 FALLTHROUGH;
953 case FAILSAFE_RX_LOSS_RECOVERED:
954 // Exiting failsafe
955 break;
957 return NULL;
960 static const char * osdFailsafeInfoMessage(void)
962 if (failsafeIsReceivingRxData() && !FLIGHT_MODE(NAV_FW_AUTOLAND)) {
963 // User must move sticks to exit FS mode
964 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS);
966 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
969 #if defined(USE_SAFE_HOME)
970 static const char * divertingToSafehomeMessage(void)
972 #ifdef USE_FW_AUTOLAND
973 if (!posControl.fwLandState.landWp && (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && posControl.safehomeState.isApplied)) {
974 #else
975 if (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && posControl.safehomeState.isApplied) {
976 #endif
977 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME);
979 #endif
980 return NULL;
984 static const char * navigationStateMessage(void)
986 if (!posControl.rthState.rthLinearDescentActive && linearDescentMessageMs != 0)
987 linearDescentMessageMs = 0;
989 switch (NAV_Status.state) {
990 case MW_NAV_STATE_NONE:
991 break;
992 case MW_NAV_STATE_RTH_START:
993 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH);
994 case MW_NAV_STATE_RTH_CLIMB:
995 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB);
996 case MW_NAV_STATE_RTH_ENROUTE:
997 if (posControl.flags.rthTrackbackActive) {
998 return OSD_MESSAGE_STR(OSD_MSG_RTH_TRACKBACK);
999 } else {
1000 if (posControl.rthState.rthLinearDescentActive && (linearDescentMessageMs == 0 || linearDescentMessageMs > millis())) {
1001 if (linearDescentMessageMs == 0)
1002 linearDescentMessageMs = millis() + 5000; // Show message for 5 seconds.
1004 return OSD_MESSAGE_STR(OSD_MSG_RTH_LINEAR_DESCENT);
1005 } else
1006 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME);
1008 case MW_NAV_STATE_HOLD_INFINIT:
1009 // Used by HOLD flight modes. No information to add.
1010 break;
1011 case MW_NAV_STATE_HOLD_TIMED:
1012 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
1013 break;
1014 case MW_NAV_STATE_WP_ENROUTE:
1015 // "TO WP" + WP countdown added in osdGetSystemMessage
1016 break;
1017 case MW_NAV_STATE_PROCESS_NEXT:
1018 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP);
1019 case MW_NAV_STATE_DO_JUMP:
1020 // Not used
1021 break;
1022 case MW_NAV_STATE_LAND_START:
1023 // Not used
1024 break;
1025 case MW_NAV_STATE_EMERGENCY_LANDING:
1026 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING);
1027 case MW_NAV_STATE_LAND_IN_PROGRESS:
1028 return OSD_MESSAGE_STR(OSD_MSG_LANDING);
1029 case MW_NAV_STATE_HOVER_ABOVE_HOME:
1030 if (STATE(FIXED_WING_LEGACY)) {
1031 #if defined(USE_SAFE_HOME)
1032 if (posControl.safehomeState.isApplied) {
1033 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME);
1035 #endif
1036 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME);
1038 return OSD_MESSAGE_STR(OSD_MSG_HOVERING);
1039 case MW_NAV_STATE_LANDED:
1040 return OSD_MESSAGE_STR(OSD_MSG_LANDED);
1041 case MW_NAV_STATE_LAND_SETTLE:
1043 // If there is a FS landing delay occurring. That is handled by the calling function.
1044 if (posControl.landingDelay > 0)
1045 break;
1047 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND);
1049 case MW_NAV_STATE_LAND_START_DESCENT:
1050 // Not used
1051 break;
1054 return NULL;
1057 static void osdFormatMessage(char *buff, size_t size, const char *message, bool isCenteredText)
1059 // String is always filled with Blanks
1060 memset(buff, SYM_BLANK, size);
1061 if (message) {
1062 size_t messageLength = strlen(message);
1063 int rem = isCenteredText ? MAX(0, (int)size - (int)messageLength) : 0;
1064 strncpy(buff + rem / 2, message, MIN((int)size - rem / 2, (int)messageLength));
1066 // Ensure buff is zero terminated
1067 buff[size] = '\0';
1071 * Draws the battery symbol filled in accordingly to the
1072 * battery voltage to buff[0].
1074 static void osdFormatBatteryChargeSymbol(char *buff)
1076 uint8_t p = calculateBatteryPercentage();
1077 p = (100 - p) / 16.6;
1078 buff[0] = SYM_BATT_FULL + p;
1081 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t *attr)
1083 const batteryState_e batteryState = getBatteryState();
1085 if (batteryState == BATTERY_WARNING || batteryState == BATTERY_CRITICAL) {
1086 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
1090 void osdCrosshairPosition(uint8_t *x, uint8_t *y)
1092 *x = osdDisplayPort->cols / 2;
1093 *y = osdDisplayPort->rows / 2;
1094 *y -= osdConfig()->horizon_offset; // positive horizon_offset moves the HUD up, negative moves down
1098 * Check if this OSD layout is using scaled or unscaled throttle.
1099 * If both are used, it will default to scaled.
1101 bool osdUsingScaledThrottle(void)
1103 bool usingScaledThrottle = OSD_VISIBLE(osdLayoutsConfig()->item_pos[currentLayout][OSD_SCALED_THROTTLE_POS]);
1104 bool usingRCThrottle = OSD_VISIBLE(osdLayoutsConfig()->item_pos[currentLayout][OSD_THROTTLE_POS]);
1106 if (!usingScaledThrottle && !usingRCThrottle)
1107 usingScaledThrottle = true;
1109 return usingScaledThrottle;
1113 * Formats throttle position prefixed by its symbol.
1114 * Shows unscaled or scaled (output to motor) throttle percentage
1116 static void osdFormatThrottlePosition(char *buff, bool useScaled, textAttributes_t *elemAttr)
1118 buff[0] = SYM_BLANK;
1119 buff[1] = SYM_THR;
1120 if (navigationIsControllingThrottle()) {
1121 buff[0] = SYM_AUTO_THR0;
1122 buff[1] = SYM_AUTO_THR1;
1123 if (isFixedWingAutoThrottleManuallyIncreased()) {
1124 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1126 useScaled = true;
1128 #ifdef USE_POWER_LIMITS
1129 if (powerLimiterIsLimiting()) {
1130 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1132 #endif
1133 int8_t throttlePercent = getThrottlePercent(useScaled);
1134 if ((useScaled && throttlePercent <= 0) || !ARMING_FLAG(ARMED)) {
1135 const char* message = ARMING_FLAG(ARMED) ? (throttlePercent == 0 && !ifMotorstopFeatureEnabled()) ? "IDLE" : "STOP" : "DARM";
1136 buff[0] = SYM_THR;
1137 strcpy(buff + 1, message);
1138 return;
1140 tfp_sprintf(buff + 2, "%3d", throttlePercent);
1144 * Formats gvars prefixed by its number (0-indexed). If autoThr
1146 static void osdFormatGVar(char *buff, uint8_t index)
1148 buff[0] = 'G';
1149 buff[1] = '0'+index;
1150 buff[2] = ':';
1151 #ifdef USE_PROGRAMMING_FRAMEWORK
1152 osdFormatCentiNumber(buff + 3, (int32_t)gvGet(index)*(int32_t)100, 1, 0, 0, 5, false);
1153 #endif
1156 #if defined(USE_ESC_SENSOR)
1157 static void osdFormatRpm(char *buff, uint32_t rpm)
1159 buff[0] = SYM_RPM;
1160 if (rpm) {
1161 if ( digitCount(rpm) > osdConfig()->esc_rpm_precision) {
1162 uint8_t rpmMaxDecimals = (osdConfig()->esc_rpm_precision - 3);
1163 osdFormatCentiNumber(buff + 1, rpm / 10, 0, rpmMaxDecimals, rpmMaxDecimals, osdConfig()->esc_rpm_precision-1, false);
1164 buff[osdConfig()->esc_rpm_precision] = 'K';
1165 buff[osdConfig()->esc_rpm_precision+1] = '\0';
1167 else {
1168 switch(osdConfig()->esc_rpm_precision) {
1169 case 6:
1170 tfp_sprintf(buff + 1, "%6lu", rpm);
1171 break;
1172 case 5:
1173 tfp_sprintf(buff + 1, "%5lu", rpm);
1174 break;
1175 case 4:
1176 tfp_sprintf(buff + 1, "%4lu", rpm);
1177 break;
1178 case 3:
1179 default:
1180 tfp_sprintf(buff + 1, "%3lu", rpm);
1181 break;
1187 else {
1188 uint8_t buffPos = 1;
1189 while (buffPos <=( osdConfig()->esc_rpm_precision)) {
1190 strcpy(buff + buffPos++, "-");
1194 #endif
1196 int32_t osdGetAltitude(void)
1198 return getEstimatedActualPosition(Z);
1201 static inline int32_t osdGetAltitudeMsl(void)
1203 return getEstimatedActualPosition(Z) + posControl.gpsOrigin.alt;
1206 uint16_t osdGetRemainingGlideTime(void) {
1207 float value = getEstimatedActualVelocity(Z);
1208 static pt1Filter_t glideTimeFilterState;
1209 const timeMs_t curTimeMs = millis();
1210 static timeMs_t glideTimeUpdatedMs;
1212 value = pt1FilterApply4(&glideTimeFilterState, isnormal(value) ? value : 0, 0.5, MS2S(curTimeMs - glideTimeUpdatedMs));
1213 glideTimeUpdatedMs = curTimeMs;
1215 if (value < 0) {
1216 value = osdGetAltitude() / abs((int)value);
1217 } else {
1218 value = 0;
1221 return (uint16_t)roundf(value);
1224 static bool osdIsHeadingValid(void)
1226 return isImuHeadingValid();
1229 int16_t osdGetHeading(void)
1231 return attitude.values.yaw;
1234 int16_t osdGetPanServoOffset(void)
1236 int8_t servoIndex = osdConfig()->pan_servo_index;
1237 int16_t servoPosition = servo[servoIndex];
1238 int16_t servoMiddle = servoParams(servoIndex)->middle;
1239 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition - servoMiddle) * osdConfig()->pan_servo_pwm2centideg);
1242 // Returns a heading angle in degrees normalized to [0, 360).
1243 int osdGetHeadingAngle(int angle)
1245 while (angle < 0) {
1246 angle += 360;
1248 while (angle >= 360) {
1249 angle -= 360;
1251 return angle;
1254 #if defined(USE_GPS)
1256 /* Draws a map with the given symbol in the center and given point of interest
1257 * defined by its distance in meters and direction in degrees.
1258 * referenceHeading indicates the up direction in the map, in degrees, while
1259 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1260 * arrow to indicate the map reference to the user. The drawn argument is an
1261 * in-out used to store the last position where the craft was drawn to avoid
1262 * erasing all screen on each redraw.
1264 static void osdDrawMap(int referenceHeading, uint16_t referenceSym, uint16_t centerSym,
1265 uint32_t poiDistance, int16_t poiDirection, uint16_t poiSymbol,
1266 uint16_t *drawn, uint32_t *usedScale)
1268 // TODO: These need to be tested with several setups. We might
1269 // need to make them configurable.
1270 const int hMargin = 5;
1271 const int vMargin = 3;
1273 // TODO: Get this from the display driver?
1274 const int charWidth = 12;
1275 const int charHeight = 18;
1277 uint8_t minX = hMargin;
1278 uint8_t maxX = osdDisplayPort->cols - 1 - hMargin;
1279 uint8_t minY = vMargin;
1280 uint8_t maxY = osdDisplayPort->rows - 1 - vMargin;
1281 uint8_t midX = osdDisplayPort->cols / 2;
1282 uint8_t midY = osdDisplayPort->rows / 2;
1284 // Fixed marks
1285 displayWriteChar(osdDisplayPort, midX, midY, centerSym);
1287 // First, erase the previous drawing.
1288 if (OSD_VISIBLE(*drawn)) {
1289 displayWriteChar(osdDisplayPort, OSD_X(*drawn), OSD_Y(*drawn), SYM_BLANK);
1290 *drawn = 0;
1293 uint32_t initialScale;
1294 const unsigned scaleMultiplier = 2;
1295 // We try to reduce the scale when the POI will be around half the distance
1296 // between the center and the closers map edge, to avoid too much jumping
1297 const int scaleReductionMultiplier = MIN(midX - hMargin, midY - vMargin) / 2;
1299 switch (osdConfig()->units) {
1300 case OSD_UNIT_UK:
1301 FALLTHROUGH;
1302 case OSD_UNIT_IMPERIAL:
1303 initialScale = 16; // 16m ~= 0.01miles
1304 break;
1305 case OSD_UNIT_GA:
1306 initialScale = 18; // 18m ~= 0.01 nautical miles
1307 break;
1308 default:
1309 case OSD_UNIT_METRIC_MPH:
1310 FALLTHROUGH;
1311 case OSD_UNIT_METRIC:
1312 initialScale = 10; // 10m as initial scale
1313 break;
1316 // Try to keep the same scale when getting closer until we draw over the center point
1317 uint32_t scale = initialScale;
1318 if (*usedScale) {
1319 scale = *usedScale;
1320 if (scale > initialScale && poiDistance < *usedScale * scaleReductionMultiplier) {
1321 scale /= scaleMultiplier;
1325 if (STATE(GPS_FIX)
1326 #ifdef USE_GPS_FIX_ESTIMATION
1327 || STATE(GPS_ESTIMATED_FIX)
1328 #endif
1331 int directionToPoi = osdGetHeadingAngle(poiDirection - referenceHeading);
1332 float poiAngle = DEGREES_TO_RADIANS(directionToPoi);
1333 float poiSin = sin_approx(poiAngle);
1334 float poiCos = cos_approx(poiAngle);
1336 // Now start looking for a valid scale that lets us draw everything
1337 int ii;
1338 for (ii = 0; ii < 50; ii++) {
1339 // Calculate location of the aircraft in map
1340 int points = poiDistance / ((float)scale / charHeight);
1342 float pointsX = points * poiSin;
1343 int poiX = midX - roundf(pointsX / charWidth);
1344 if (poiX < minX || poiX > maxX) {
1345 scale *= scaleMultiplier;
1346 continue;
1349 float pointsY = points * poiCos;
1350 int poiY = midY + roundf(pointsY / charHeight);
1351 if (poiY < minY || poiY > maxY) {
1352 scale *= scaleMultiplier;
1353 continue;
1356 if (poiX == midX && poiY == midY) {
1357 // We're over the map center symbol, so we would be drawing
1358 // over it even if we increased the scale. Alternate between
1359 // drawing the center symbol or drawing the POI.
1360 if (centerSym != SYM_BLANK && OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1361 break;
1363 } else {
1365 uint16_t c;
1366 if (displayReadCharWithAttr(osdDisplayPort, poiX, poiY, &c, NULL) && c != SYM_BLANK) {
1367 // Something else written here, increase scale. If the display doesn't support reading
1368 // back characters, we assume there's nothing.
1370 // If we're close to the center, decrease scale. Otherwise increase it.
1371 uint8_t centerDeltaX = (maxX - minX) / (scaleMultiplier * 2);
1372 uint8_t centerDeltaY = (maxY - minY) / (scaleMultiplier * 2);
1373 if (poiX >= midX - centerDeltaX && poiX <= midX + centerDeltaX &&
1374 poiY >= midY - centerDeltaY && poiY <= midY + centerDeltaY &&
1375 scale > scaleMultiplier) {
1377 scale /= scaleMultiplier;
1378 } else {
1379 scale *= scaleMultiplier;
1381 continue;
1385 // Draw the point on the map
1386 if (poiSymbol == SYM_ARROW_UP) {
1387 // Drawing aircraft, rotate
1388 int mapHeading = osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading);
1389 poiSymbol += mapHeading * 2 / 45;
1391 displayWriteChar(osdDisplayPort, poiX, poiY, poiSymbol);
1393 // Update saved location
1394 *drawn = OSD_POS(poiX, poiY) | OSD_VISIBLE_FLAG;
1395 break;
1399 *usedScale = scale;
1401 // Update global map data for scale and reference
1402 osdMapData.scale = scale;
1403 osdMapData.referenceSymbol = referenceSym;
1406 /* Draws a map with the home in the center and the craft moving around.
1407 * See osdDrawMap() for reference.
1409 static void osdDrawHomeMap(int referenceHeading, uint8_t referenceSym, uint16_t *drawn, uint32_t *usedScale)
1411 osdDrawMap(referenceHeading, referenceSym, SYM_HOME, GPS_distanceToHome, GPS_directionToHome, SYM_ARROW_UP, drawn, usedScale);
1414 /* Draws a map with the aircraft in the center and the home moving around.
1415 * See osdDrawMap() for reference.
1417 static void osdDrawRadar(uint16_t *drawn, uint32_t *usedScale)
1419 int16_t reference = DECIDEGREES_TO_DEGREES(osdGetHeading());
1420 int16_t poiDirection = osdGetHeadingAngle(GPS_directionToHome + 180);
1421 osdDrawMap(reference, 0, SYM_ARROW_UP, GPS_distanceToHome, poiDirection, SYM_HOME, drawn, usedScale);
1424 static uint16_t crc_accumulate(uint8_t data, uint16_t crcAccum)
1426 uint8_t tmp;
1427 tmp = data ^ (uint8_t)(crcAccum & 0xff);
1428 tmp ^= (tmp << 4);
1429 crcAccum = (crcAccum >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
1430 return crcAccum;
1434 static void osdDisplayTelemetry(void)
1436 uint32_t trk_data;
1437 uint16_t trk_crc = 0;
1438 char trk_buffer[31];
1439 static int16_t trk_elevation = 127;
1440 static uint16_t trk_bearing = 0;
1442 if (ARMING_FLAG(ARMED)) {
1443 if (STATE(GPS_FIX)
1444 #ifdef USE_GPS_FIX_ESTIMATION
1445 || STATE(GPS_ESTIMATED_FIX)
1446 #endif
1448 if (GPS_distanceToHome > 5) {
1449 trk_bearing = GPS_directionToHome;
1450 trk_bearing += 360 + 180;
1451 trk_bearing %= 360;
1452 int32_t alt = CENTIMETERS_TO_METERS(osdGetAltitude());
1453 float at = atan2(alt, GPS_distanceToHome);
1454 trk_elevation = at * 57.2957795f; // 57.2957795 = 1 rad
1455 trk_elevation += 37; // because elevation in telemetry should be from -37 to 90
1456 if (trk_elevation < 0) {
1457 trk_elevation = 0;
1462 else{
1463 trk_elevation = 127;
1464 trk_bearing = 0;
1467 trk_data = 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1468 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.
1469 trk_data = trk_data | (uint32_t)trk_bearing << 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1470 trk_crc = crc_accumulate(0xFF & trk_data, trk_crc); // CRC First Byte bits 0-7
1471 trk_crc = crc_accumulate(0xFF & trk_bearing, trk_crc); // CRC Second Byte bits 8-15
1472 trk_crc = crc_accumulate(trk_bearing >> 8, trk_crc); // CRC Third Byte bits 16-17
1473 trk_data = trk_data | (uint32_t)trk_crc << 17; // bits 18-29 CRC & 0x3FFFF
1475 for (uint8_t t_ctr = 0; t_ctr < 30; t_ctr++) { // Prepare screen buffer and write data line.
1476 if (trk_data & (uint32_t)1 << t_ctr){
1477 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_0;
1479 else{
1480 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_1;
1483 trk_buffer[30] = 0;
1484 displayWrite(osdDisplayPort, 0, 0, trk_buffer);
1485 if (osdConfig()->telemetry>1){
1486 displayWrite(osdDisplayPort, 0, 3, trk_buffer); // Test display because normal telemetry line is not visible
1489 #endif
1491 static void osdFormatPidControllerOutput(char *buff, const char *label, const pidController_t *pidController, uint8_t scale, bool showDecimal) {
1492 strcpy(buff, label);
1493 for (uint8_t i = strlen(label); i < 5; ++i) buff[i] = ' ';
1494 uint8_t decimals = showDecimal ? 1 : 0;
1495 osdFormatCentiNumber(buff + 5, pidController->proportional * scale, 0, decimals, 0, 4, false);
1496 buff[9] = ' ';
1497 osdFormatCentiNumber(buff + 10, pidController->integrator * scale, 0, decimals, 0, 4, false);
1498 buff[14] = ' ';
1499 osdFormatCentiNumber(buff + 15, pidController->derivative * scale, 0, decimals, 0, 4, false);
1500 buff[19] = ' ';
1501 osdFormatCentiNumber(buff + 20, pidController->output_constrained * scale, 0, decimals, 0, 4, false);
1502 buff[24] = '\0';
1505 static void osdDisplayBatteryVoltage(uint8_t elemPosX, uint8_t elemPosY, uint16_t voltage, uint8_t digits, uint8_t decimals)
1507 char buff[7];
1508 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1510 osdFormatBatteryChargeSymbol(buff);
1511 buff[1] = '\0';
1512 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1513 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1515 elemAttr = TEXT_ATTRIBUTES_NONE;
1516 digits = MIN(digits, 5);
1517 osdFormatCentiNumber(buff, voltage, 0, decimals, 0, digits, false);
1518 buff[digits] = SYM_VOLT;
1519 buff[digits+1] = '\0';
1520 const batteryState_e batteryVoltageState = checkBatteryVoltageState();
1521 if (batteryVoltageState == BATTERY_CRITICAL || batteryVoltageState == BATTERY_WARNING) {
1522 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1524 displayWriteWithAttr(osdDisplayPort, elemPosX + 1, elemPosY, buff, elemAttr);
1527 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)
1529 textAttributes_t elemAttr;
1530 char buff[4];
1532 const pid8_t *pid = &pidBank()->pid[pidIndex];
1533 pidType_e pidType = pidIndexGetType(pidIndex);
1535 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1537 if (pidType == PID_TYPE_NONE) {
1538 // PID is not used in this configuration. Draw dashes.
1539 // XXX: Keep this in sync with the %3d format and spacing used below
1540 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - - -");
1541 return;
1544 elemAttr = TEXT_ATTRIBUTES_NONE;
1545 tfp_sprintf(buff, "%3d", pid->P);
1546 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1547 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1548 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1550 elemAttr = TEXT_ATTRIBUTES_NONE;
1551 tfp_sprintf(buff, "%3d", pid->I);
1552 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1553 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1554 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1556 elemAttr = TEXT_ATTRIBUTES_NONE;
1557 tfp_sprintf(buff, "%3d", pid->D);
1558 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1559 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1560 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1562 elemAttr = TEXT_ATTRIBUTES_NONE;
1563 tfp_sprintf(buff, "%3d", pid->FF);
1564 if ((isAdjustmentFunctionSelected(adjFuncFF)) || (((adjFuncFF == ADJUSTMENT_ROLL_FF) || (adjFuncFF == ADJUSTMENT_PITCH_FF)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF))))
1565 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1566 displayWriteWithAttr(osdDisplayPort, elemPosX + 16, elemPosY, buff, elemAttr);
1569 static void osdDisplayNavPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD)
1571 textAttributes_t elemAttr;
1572 char buff[4];
1574 const pid8_t *pid = &pidBank()->pid[pidIndex];
1575 pidType_e pidType = pidIndexGetType(pidIndex);
1577 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1579 if (pidType == PID_TYPE_NONE) {
1580 // PID is not used in this configuration. Draw dashes.
1581 // XXX: Keep this in sync with the %3d format and spacing used below
1582 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - -");
1583 return;
1586 elemAttr = TEXT_ATTRIBUTES_NONE;
1587 tfp_sprintf(buff, "%3d", pid->P);
1588 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1589 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1590 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1592 elemAttr = TEXT_ATTRIBUTES_NONE;
1593 tfp_sprintf(buff, "%3d", pid->I);
1594 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1595 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1596 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1598 elemAttr = TEXT_ATTRIBUTES_NONE;
1599 tfp_sprintf(buff, "%3d", pidType == PID_TYPE_PIFF ? pid->FF : pid->D);
1600 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1601 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1602 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1605 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) {
1606 char buff[8];
1607 textAttributes_t elemAttr;
1608 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1610 elemAttr = TEXT_ATTRIBUTES_NONE;
1611 osdFormatCentiNumber(buff, value * 100, 0, maxDecimals, 0, MIN(valueLength, 8), false);
1612 if (isAdjustmentFunctionSelected(adjFunc))
1613 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1614 displayWriteWithAttr(osdDisplayPort, elemPosX + strlen(str) + 1 + valueOffset, elemPosY, buff, elemAttr);
1617 int8_t getGeoWaypointNumber(int8_t waypointIndex)
1619 static int8_t lastWaypointIndex = 1;
1620 static int8_t geoWaypointIndex;
1622 if (waypointIndex != lastWaypointIndex) {
1623 lastWaypointIndex = geoWaypointIndex = waypointIndex;
1624 for (uint8_t i = posControl.startWpIndex; i <= waypointIndex; i++) {
1625 if (posControl.waypointList[i].action == NAV_WP_ACTION_SET_POI ||
1626 posControl.waypointList[i].action == NAV_WP_ACTION_SET_HEAD ||
1627 posControl.waypointList[i].action == NAV_WP_ACTION_JUMP) {
1628 geoWaypointIndex -= 1;
1633 return geoWaypointIndex - posControl.startWpIndex + 1;
1636 void osdDisplaySwitchIndicator(const char *swName, int rcValue, char *buff) {
1637 int8_t ptr = 0;
1639 if (osdConfig()->osd_switch_indicators_align_left) {
1640 for (ptr = 0; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH); ptr++) {
1641 buff[ptr] = swName[ptr];
1644 if ( rcValue < 1333) {
1645 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1646 } else if ( rcValue > 1666) {
1647 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1648 } else {
1649 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1651 } else {
1652 if ( rcValue < 1333) {
1653 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1654 } else if ( rcValue > 1666) {
1655 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1656 } else {
1657 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1660 for (ptr = 1; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH) + 1; ptr++) {
1661 buff[ptr] = swName[ptr-1];
1664 ptr++;
1667 buff[ptr] = '\0';
1670 static bool osdDrawSingleElement(uint8_t item)
1672 uint16_t pos = osdLayoutsConfig()->item_pos[currentLayout][item];
1673 if (!OSD_VISIBLE(pos)) {
1674 return false;
1676 uint8_t elemPosX = OSD_X(pos);
1677 uint8_t elemPosY = OSD_Y(pos);
1678 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1679 char buff[32] = {0};
1681 switch (item) {
1682 case OSD_CUSTOM_ELEMENT_1:
1684 customElementDrawElement(buff, 0);
1685 break;
1687 case OSD_CUSTOM_ELEMENT_2:
1689 customElementDrawElement(buff, 1);
1690 break;
1692 case OSD_CUSTOM_ELEMENT_3:
1694 customElementDrawElement(buff, 2);
1695 break;
1697 case OSD_RSSI_VALUE:
1699 uint16_t osdRssi = osdConvertRSSI();
1700 buff[0] = SYM_RSSI;
1701 tfp_sprintf(buff + 1, "%2d", osdRssi);
1702 if (osdRssi < osdConfig()->rssi_alarm) {
1703 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1705 break;
1708 case OSD_MAIN_BATT_VOLTAGE: {
1709 uint8_t base_digits = 2U;
1710 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1711 if(isDJICompatibleVideoSystem(osdConfig())) {
1712 base_digits = 3U; // Add extra digit to account for decimal point taking an extra character space
1714 #endif
1715 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawVoltage(), base_digits + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1716 return true;
1719 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE: {
1720 uint8_t base_digits = 2U;
1721 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1722 if(isDJICompatibleVideoSystem(osdConfig())) {
1723 base_digits = 3U; // Add extra digit to account for decimal point taking an extra character space
1725 #endif
1726 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedVoltage(), base_digits + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1727 return true;
1730 case OSD_CURRENT_DRAW: {
1731 osdFormatCentiNumber(buff, getAmperage(), 0, 2, 0, 3, false);
1732 buff[3] = SYM_AMP;
1733 buff[4] = '\0';
1735 uint8_t current_alarm = osdConfig()->current_alarm;
1736 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
1737 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1739 break;
1742 case OSD_MAH_DRAWN: {
1743 uint8_t mah_digits = osdConfig()->mAh_precision; // Initialize to config value
1745 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1746 if (isDJICompatibleVideoSystem(osdConfig())) {
1747 //DJIcompat is unable to work with scaled values and it only has mAh symbol to work with
1748 tfp_sprintf(buff, "%5d", (int)getMAhDrawn()); // Use 5 digits to allow packs below 100Ah
1749 buff[5] = SYM_MAH;
1750 buff[6] = '\0';
1751 } else
1752 #endif
1754 if (osdFormatCentiNumber(buff, getMAhDrawn() * 100, 1000, 0, (mah_digits - 2), mah_digits, false)) {
1755 // Shown in Ah
1756 buff[mah_digits] = SYM_AH;
1757 } else {
1758 // Shown in mAh
1759 buff[mah_digits] = SYM_MAH;
1761 buff[mah_digits + 1] = '\0';
1764 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1765 break;
1768 case OSD_WH_DRAWN:
1769 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3, false);
1770 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1771 buff[3] = SYM_WH;
1772 buff[4] = '\0';
1773 break;
1775 case OSD_BATTERY_REMAINING_CAPACITY:
1777 bool unitsDrawn = false;
1779 if (currentBatteryProfile->capacity.value == 0)
1780 tfp_sprintf(buff, " NA");
1781 else if (!batteryWasFullWhenPluggedIn())
1782 tfp_sprintf(buff, " NF");
1783 else if (currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH) {
1784 uint8_t mah_digits = osdConfig()->mAh_precision; // Initialize to config value
1786 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1787 if (isDJICompatibleVideoSystem(osdConfig())) {
1788 //DJIcompat is unable to work with scaled values and it only has mAh symbol to work with
1789 tfp_sprintf(buff, "%5d", (int)getBatteryRemainingCapacity()); // Use 5 digits to allow packs below 100Ah
1790 buff[5] = SYM_MAH;
1791 buff[6] = '\0';
1792 unitsDrawn = true;
1793 } else
1794 #endif
1796 if (osdFormatCentiNumber(buff, getBatteryRemainingCapacity() * 100, 1000, 0, (mah_digits - 2), mah_digits, false)) {
1797 // Shown in Ah
1798 buff[mah_digits] = SYM_AH;
1799 } else {
1800 // Shown in mAh
1801 buff[mah_digits] = SYM_MAH;
1803 buff[mah_digits + 1] = '\0';
1804 unitsDrawn = true;
1806 } else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1807 osdFormatCentiNumber(buff + 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3, false);
1809 if (!unitsDrawn) {
1810 buff[4] = currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH ? SYM_MAH : SYM_WH;
1811 buff[5] = '\0';
1814 if (batteryUsesCapacityThresholds()) {
1815 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1818 break;
1820 case OSD_BATTERY_REMAINING_PERCENT:
1821 osdFormatBatteryChargeSymbol(buff);
1822 tfp_sprintf(buff + 1, "%3d%%", calculateBatteryPercentage());
1823 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1824 break;
1826 case OSD_POWER_SUPPLY_IMPEDANCE:
1827 if (isPowerSupplyImpedanceValid())
1828 tfp_sprintf(buff, "%3d", getPowerSupplyImpedance());
1829 else
1830 strcpy(buff, "---");
1831 buff[3] = SYM_MILLIOHM;
1832 buff[4] = '\0';
1833 break;
1835 #ifdef USE_GPS
1836 case OSD_GPS_SATS:
1837 buff[0] = SYM_SAT_L;
1838 buff[1] = SYM_SAT_R;
1839 tfp_sprintf(buff + 2, "%2d", gpsSol.numSat);
1840 #ifdef USE_GPS_FIX_ESTIMATION
1841 if (STATE(GPS_ESTIMATED_FIX)) {
1842 strcpy(buff + 2, "ES");
1843 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1844 } else
1845 #endif
1846 if (!STATE(GPS_FIX)) {
1847 hardwareSensorStatus_e sensorStatus = getHwGPSStatus();
1848 if (sensorStatus == HW_SENSOR_UNAVAILABLE || sensorStatus == HW_SENSOR_UNHEALTHY) {
1849 buff[2] = SYM_ALERT;
1850 buff[3] = '\0';
1852 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1854 break;
1856 case OSD_GPS_SPEED:
1857 osdFormatVelocityStr(buff, gpsSol.groundSpeed, false, false);
1858 break;
1860 case OSD_GPS_MAX_SPEED:
1861 osdFormatVelocityStr(buff, stats.max_speed, false, true);
1862 break;
1864 case OSD_3D_SPEED:
1865 osdFormatVelocityStr(buff, osdGet3DSpeed(), true, false);
1866 break;
1868 case OSD_3D_MAX_SPEED:
1869 osdFormatVelocityStr(buff, stats.max_3D_speed, true, true);
1870 break;
1872 case OSD_GLIDESLOPE:
1874 float horizontalSpeed = gpsSol.groundSpeed;
1875 float sinkRate = -getEstimatedActualVelocity(Z);
1876 static pt1Filter_t gsFilterState;
1877 const timeMs_t currentTimeMs = millis();
1878 static timeMs_t gsUpdatedTimeMs;
1879 float glideSlope = horizontalSpeed / sinkRate;
1880 glideSlope = pt1FilterApply4(&gsFilterState, isnormal(glideSlope) ? glideSlope : 200, 0.5, MS2S(currentTimeMs - gsUpdatedTimeMs));
1881 gsUpdatedTimeMs = currentTimeMs;
1883 buff[0] = SYM_GLIDESLOPE;
1884 if (glideSlope > 0.0f && glideSlope < 100.0f) {
1885 osdFormatCentiNumber(buff + 1, glideSlope * 100.0f, 0, 2, 0, 3, false);
1886 } else {
1887 buff[1] = buff[2] = buff[3] = '-';
1889 buff[4] = '\0';
1890 break;
1893 case OSD_GPS_LAT:
1894 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
1895 break;
1897 case OSD_GPS_LON:
1898 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
1899 break;
1901 case OSD_HOME_DIR:
1903 if ((STATE(GPS_FIX)
1904 #ifdef USE_GPS_FIX_ESTIMATION
1905 || STATE(GPS_ESTIMATED_FIX)
1906 #endif
1907 ) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
1908 if (GPS_distanceToHome < (navConfig()->general.min_rth_distance / 100) ) {
1909 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_HOME_NEAR);
1911 else
1913 int16_t panHomeDirOffset = 0;
1914 if (!(osdConfig()->pan_servo_pwm2centideg == 0)){
1915 panHomeDirOffset = osdGetPanServoOffset();
1917 int16_t flightDirection = STATE(AIRPLANE) ? CENTIDEGREES_TO_DEGREES(posControl.actualState.cog) : DECIDEGREES_TO_DEGREES(osdGetHeading());
1918 int homeDirection = GPS_directionToHome - flightDirection + panHomeDirOffset;
1919 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), homeDirection);
1921 } else {
1922 // No home or no fix or unknown heading, blink.
1923 // If we're unarmed, show the arrow pointing up so users can see the arrow
1924 // while configuring the OSD. If we're armed, show a '-' indicating that
1925 // we don't know the direction to home.
1926 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1927 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, ARMING_FLAG(ARMED) ? '-' : SYM_ARROW_UP, elemAttr);
1929 return true;
1932 case OSD_HOME_HEADING_ERROR:
1934 buff[0] = SYM_HOME;
1935 buff[1] = SYM_HEADING;
1937 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1938 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())))));
1939 tfp_sprintf(buff + 2, "%4d", h);
1940 } else {
1941 strcpy(buff + 2, "----");
1944 buff[6] = SYM_DEGREES;
1945 buff[7] = '\0';
1946 break;
1949 case OSD_HOME_DIST:
1951 buff[0] = SYM_HOME;
1952 uint32_t distance_to_home_cm = GPS_distanceToHome * 100;
1953 osdFormatDistanceSymbol(&buff[1], distance_to_home_cm, 0);
1955 uint16_t dist_alarm = osdConfig()->dist_alarm;
1956 if (dist_alarm > 0 && GPS_distanceToHome > dist_alarm) {
1957 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1960 break;
1962 case OSD_TRIP_DIST:
1963 buff[0] = SYM_TOTAL;
1964 osdFormatDistanceSymbol(buff + 1, getTotalTravelDistance(), 0);
1965 break;
1967 case OSD_BLACKBOX:
1969 #ifdef USE_BLACKBOX
1970 if (IS_RC_MODE_ACTIVE(BOXBLACKBOX)) {
1971 if (!isBlackboxDeviceWorking()) {
1972 tfp_sprintf(buff, "%c%c", SYM_BLACKBOX, SYM_ALERT);
1973 } else if (isBlackboxDeviceFull()) {
1974 tfp_sprintf(buff, "%cFULL", SYM_BLACKBOX);
1975 } else {
1976 int32_t logNumber = blackboxGetLogNumber();
1977 if (logNumber >= 0) {
1978 tfp_sprintf(buff, "%c%05" PRId32, SYM_BLACKBOX, logNumber);
1979 } else {
1980 tfp_sprintf(buff, "%c", SYM_BLACKBOX);
1984 #endif // USE_BLACKBOX
1986 break;
1988 case OSD_ODOMETER:
1990 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_ODOMETER);
1991 float_t odometerDist = CENTIMETERS_TO_METERS(getTotalTravelDistance());
1992 #ifdef USE_STATS
1993 odometerDist+= statsConfig()->stats_total_dist;
1994 #endif
1996 switch (osdConfig()->units) {
1997 case OSD_UNIT_UK:
1998 FALLTHROUGH;
1999 case OSD_UNIT_IMPERIAL:
2000 osdFormatCentiNumber(buff, METERS_TO_MILES(odometerDist) * 100, 1, 1, 1, 6, true);
2001 buff[6] = SYM_MI;
2002 break;
2003 default:
2004 case OSD_UNIT_GA:
2005 osdFormatCentiNumber(buff, METERS_TO_NAUTICALMILES(odometerDist) * 100, 1, 1, 1, 6, true);
2006 buff[6] = SYM_NM;
2007 break;
2008 case OSD_UNIT_METRIC_MPH:
2009 FALLTHROUGH;
2010 case OSD_UNIT_METRIC:
2011 osdFormatCentiNumber(buff, METERS_TO_KILOMETERS(odometerDist) * 100, 1, 1, 1, 6, true);
2012 buff[6] = SYM_KM;
2013 break;
2015 buff[7] = '\0';
2016 elemPosX++;
2018 break;
2020 case OSD_GROUND_COURSE:
2022 buff[0] = SYM_GROUND_COURSE;
2023 if (osdIsHeadingValid()) {
2024 tfp_sprintf(&buff[1], "%3d", (int16_t)CENTIDEGREES_TO_DEGREES(posControl.actualState.cog));
2025 } else {
2026 buff[1] = buff[2] = buff[3] = '-';
2028 buff[4] = SYM_DEGREES;
2029 buff[5] = '\0';
2030 break;
2033 case OSD_COURSE_HOLD_ERROR:
2035 if (ARMING_FLAG(ARMED) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
2036 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
2037 return true;
2040 buff[0] = SYM_HEADING;
2042 if ((!ARMING_FLAG(ARMED)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && isAdjustingPosition())) {
2043 buff[1] = buff[2] = buff[3] = '-';
2044 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
2045 int16_t herr = lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
2046 if (ABS(herr) > 99)
2047 strcpy(buff + 1, ">99");
2048 else
2049 tfp_sprintf(buff + 1, "%3d", herr);
2052 buff[4] = SYM_DEGREES;
2053 buff[5] = '\0';
2054 break;
2057 case OSD_COURSE_HOLD_ADJUSTMENT:
2059 int16_t heading_adjust = lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
2061 if (ARMING_FLAG(ARMED) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust != 0)))) {
2062 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
2063 return true;
2066 buff[0] = SYM_HEADING;
2068 if (!ARMING_FLAG(ARMED)) {
2069 buff[1] = buff[2] = buff[3] = buff[4] = '-';
2070 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
2071 tfp_sprintf(buff + 1, "%4d", heading_adjust);
2074 buff[5] = SYM_DEGREES;
2075 buff[6] = '\0';
2076 break;
2079 case OSD_CROSS_TRACK_ERROR:
2081 if (isWaypointNavTrackingActive()) {
2082 buff[0] = SYM_CROSS_TRACK_ERROR;
2083 osdFormatDistanceSymbol(buff + 1, navigationGetCrossTrackError(), 0);
2084 } else {
2085 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
2086 return true;
2088 break;
2091 case OSD_GPS_HDOP:
2093 buff[0] = SYM_HDP_L;
2094 buff[1] = SYM_HDP_R;
2095 int32_t centiHDOP = 100 * gpsSol.hdop / HDOP_SCALE;
2096 uint8_t digits = 2U;
2097 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
2098 if (isDJICompatibleVideoSystem(osdConfig())) {
2099 digits = 3U;
2101 #endif
2102 osdFormatCentiNumber(&buff[2], centiHDOP, 0, 1, 0, digits, false);
2103 break;
2105 #ifdef USE_ADSB
2106 case OSD_ADSB_WARNING:
2108 static uint8_t adsblen = 1;
2109 uint8_t arrowPositionX = 0;
2111 for (int i = 0; i <= adsblen; i++) {
2112 buff[i] = SYM_BLANK;
2115 buff[adsblen]='\0';
2116 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff); // clear any previous chars because variable element size
2117 adsblen=1;
2118 adsbVehicle_t *vehicle = findVehicleClosest();
2120 if(vehicle != NULL){
2121 recalculateVehicle(vehicle);
2124 if (
2125 vehicle != NULL &&
2126 (vehicle->calculatedVehicleValues.dist > 0) &&
2127 vehicle->calculatedVehicleValues.dist < METERS_TO_CENTIMETERS(osdConfig()->adsb_distance_warning) &&
2128 (osdConfig()->adsb_ignore_plane_above_me_limit == 0 || METERS_TO_CENTIMETERS(osdConfig()->adsb_ignore_plane_above_me_limit) > vehicle->calculatedVehicleValues.verticalDistance)
2130 buff[0] = SYM_ADSB;
2131 osdFormatDistanceStr(&buff[1], (int32_t)vehicle->calculatedVehicleValues.dist);
2132 adsblen = strlen(buff);
2134 buff[adsblen-1] = SYM_BLANK;
2136 arrowPositionX = adsblen-1;
2137 osdFormatDistanceStr(&buff[adsblen], vehicle->calculatedVehicleValues.verticalDistance);
2138 adsblen = strlen(buff)-1;
2140 if (vehicle->calculatedVehicleValues.dist < METERS_TO_CENTIMETERS(osdConfig()->adsb_distance_alert)) {
2141 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2145 buff[adsblen]='\0';
2146 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
2148 if (arrowPositionX > 0){
2149 int16_t panHomeDirOffset = 0;
2150 if (osdConfig()->pan_servo_pwm2centideg != 0){
2151 panHomeDirOffset = osdGetPanServoOffset();
2153 int16_t flightDirection = STATE(AIRPLANE) ? CENTIDEGREES_TO_DEGREES(posControl.actualState.cog) : DECIDEGREES_TO_DEGREES(osdGetHeading());
2154 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX + arrowPositionX, elemPosY), CENTIDEGREES_TO_DEGREES(vehicle->calculatedVehicleValues.dir) - flightDirection + panHomeDirOffset);
2157 return true;
2159 case OSD_ADSB_INFO:
2161 buff[0] = SYM_ADSB;
2162 if(getAdsbStatus()->vehiclesMessagesTotal > 0){
2163 tfp_sprintf(buff + 1, "%2d", getActiveVehiclesCount());
2164 }else{
2165 buff[1] = '-';
2168 break;
2171 #endif
2172 case OSD_MAP_NORTH:
2174 static uint16_t drawn = 0;
2175 static uint32_t scale = 0;
2176 osdDrawHomeMap(0, 'N', &drawn, &scale);
2177 return true;
2179 case OSD_MAP_TAKEOFF:
2181 static uint16_t drawn = 0;
2182 static uint32_t scale = 0;
2183 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn, &scale);
2184 return true;
2186 case OSD_RADAR:
2188 static uint16_t drawn = 0;
2189 static uint32_t scale = 0;
2190 osdDrawRadar(&drawn, &scale);
2191 return true;
2193 #endif // GPS
2195 case OSD_ALTITUDE:
2197 int32_t alt = osdGetAltitude();
2198 osdFormatAltitudeSymbol(buff, alt);
2200 uint16_t alt_alarm = osdConfig()->alt_alarm;
2201 uint16_t neg_alt_alarm = osdConfig()->neg_alt_alarm;
2202 if ((alt_alarm > 0 && CENTIMETERS_TO_METERS(alt) > alt_alarm) ||
2203 (neg_alt_alarm > 0 && alt < 0 && -CENTIMETERS_TO_METERS(alt) > neg_alt_alarm)) {
2205 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2207 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
2209 if (STATE(MULTIROTOR) && posControl.flags.isAdjustingAltitude) {
2210 /* Indicate MR altitude adjustment active with constant symbol at first blank position.
2211 * Alternate symbol on/off with 600ms cycle if first position not blank (to maintain visibility of -ve sign) */
2212 int8_t blankPos;
2213 for (blankPos = 2; blankPos >= 0; blankPos--) {
2214 if (buff[blankPos] == SYM_BLANK) {
2215 break;
2218 if (blankPos >= 0 || OSD_ALTERNATING_CHOICES(600, 2) == 0) {
2219 blankPos = blankPos < 0 ? 0 : blankPos;
2220 displayWriteChar(osdDisplayPort, elemPosX + blankPos, elemPosY, SYM_TERRAIN_FOLLOWING);
2223 return true;
2226 case OSD_ALTITUDE_MSL:
2228 int32_t alt = osdGetAltitudeMsl();
2229 osdFormatAltitudeSymbol(buff, alt);
2230 break;
2233 #ifdef USE_RANGEFINDER
2234 case OSD_RANGEFINDER:
2236 int32_t range = rangefinderGetLatestRawAltitude();
2237 if (range < 0) {
2238 buff[0] = '-';
2239 buff[1] = '-';
2240 buff[2] = '-';
2241 } else {
2242 osdFormatDistanceSymbol(buff, range, 1);
2245 break;
2246 #endif
2248 case OSD_ONTIME:
2250 osdFormatOnTime(buff);
2251 break;
2254 case OSD_FLYTIME:
2256 osdFormatFlyTime(buff, &elemAttr);
2257 break;
2260 case OSD_ONTIME_FLYTIME:
2262 if (ARMING_FLAG(ARMED)) {
2263 osdFormatFlyTime(buff, &elemAttr);
2264 } else {
2265 osdFormatOnTime(buff);
2267 break;
2270 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH:
2272 /*static int32_t updatedTimeSeconds = 0;*/
2273 static int32_t timeSeconds = -1;
2274 #if defined(USE_ADC) && defined(USE_GPS)
2275 static timeUs_t updatedTimestamp = 0;
2276 timeUs_t currentTimeUs = micros();
2277 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2278 #ifdef USE_WIND_ESTIMATOR
2279 timeSeconds = calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation);
2280 #else
2281 timeSeconds = calculateRemainingFlightTimeBeforeRTH(false);
2282 #endif
2283 updatedTimestamp = currentTimeUs;
2285 #endif
2286 if ((!ARMING_FLAG(ARMED)) || (timeSeconds == -1)) {
2287 buff[0] = SYM_FLIGHT_MINS_REMAINING;
2288 strcpy(buff + 1, "--:--");
2289 #if defined(USE_ADC) && defined(USE_GPS)
2290 updatedTimestamp = 0;
2291 #endif
2292 } else if (timeSeconds == -2) {
2293 // Wind is too strong to come back with cruise throttle
2294 buff[0] = SYM_FLIGHT_MINS_REMAINING;
2295 buff[1] = buff[2] = buff[4] = buff[5] = SYM_WIND_HORIZONTAL;
2296 buff[3] = ':';
2297 buff[6] = '\0';
2298 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2299 } else {
2300 osdFormatTime(buff, timeSeconds, SYM_FLIGHT_MINS_REMAINING, SYM_FLIGHT_HOURS_REMAINING);
2301 if (timeSeconds == 0)
2302 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2305 break;
2307 case OSD_REMAINING_DISTANCE_BEFORE_RTH:;
2308 static int32_t distanceMeters = -1;
2309 #if defined(USE_ADC) && defined(USE_GPS)
2310 static timeUs_t updatedTimestamp = 0;
2311 timeUs_t currentTimeUs = micros();
2312 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2313 #ifdef USE_WIND_ESTIMATOR
2314 distanceMeters = calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation);
2315 #else
2316 distanceMeters = calculateRemainingDistanceBeforeRTH(false);
2317 #endif
2318 updatedTimestamp = currentTimeUs;
2320 #endif
2321 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_FLIGHT_DIST_REMAINING);
2323 if ((!ARMING_FLAG(ARMED)) || (distanceMeters == -1)) {
2324 buff[3] = SYM_BLANK;
2325 buff[4] = '\0';
2326 strcpy(buff, "---");
2327 } else if (distanceMeters == -2) {
2328 // Wind is too strong to come back with cruise throttle
2329 buff[0] = buff[1] = buff[2] = SYM_WIND_HORIZONTAL;
2330 switch ((osd_unit_e)osdConfig()->units){
2331 case OSD_UNIT_UK:
2332 FALLTHROUGH;
2333 case OSD_UNIT_IMPERIAL:
2334 buff[3] = SYM_DIST_MI;
2335 break;
2336 case OSD_UNIT_METRIC_MPH:
2337 FALLTHROUGH;
2338 case OSD_UNIT_METRIC:
2339 buff[3] = SYM_DIST_KM;
2340 break;
2341 case OSD_UNIT_GA:
2342 buff[3] = SYM_DIST_NM;
2343 break;
2345 buff[4] = '\0';
2346 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2347 } else {
2348 osdFormatDistanceSymbol(buff, distanceMeters * 100, 0);
2349 if (distanceMeters == 0)
2350 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2352 elemPosX++;
2353 break;
2355 case OSD_FLYMODE:
2357 char *p = "ACRO";
2358 #ifdef USE_FW_AUTOLAND
2359 if (FLIGHT_MODE(NAV_FW_AUTOLAND))
2360 p = "LAND";
2361 else
2362 #endif
2363 if (FLIGHT_MODE(FAILSAFE_MODE))
2364 p = "!FS!";
2365 else if (FLIGHT_MODE(MANUAL_MODE))
2366 p = "MANU";
2367 else if (FLIGHT_MODE(TURTLE_MODE))
2368 p = "TURT";
2369 else if (FLIGHT_MODE(NAV_RTH_MODE))
2370 p = isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
2371 else if (FLIGHT_MODE(NAV_POSHOLD_MODE) && STATE(AIRPLANE))
2372 p = "LOTR";
2373 else if (FLIGHT_MODE(NAV_POSHOLD_MODE))
2374 p = "HOLD";
2375 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && FLIGHT_MODE(NAV_ALTHOLD_MODE))
2376 p = "CRUZ";
2377 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE))
2378 p = "CRSH";
2379 else if (FLIGHT_MODE(NAV_WP_MODE))
2380 p = " WP ";
2381 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && navigationRequiresAngleMode()) {
2382 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
2383 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
2384 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
2385 // (Currently only applies to multirotor).
2386 p = " AH ";
2388 else if (FLIGHT_MODE(ANGLE_MODE))
2389 p = "ANGL";
2390 else if (FLIGHT_MODE(HORIZON_MODE))
2391 p = "HOR ";
2392 else if (FLIGHT_MODE(ANGLEHOLD_MODE))
2393 p = "ANGH";
2395 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
2396 return true;
2399 case OSD_CRAFT_NAME:
2400 osdFormatCraftName(buff);
2401 break;
2403 case OSD_PILOT_NAME:
2404 osdFormatPilotName(buff);
2405 break;
2407 case OSD_PILOT_LOGO:
2408 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_PILOT_LOGO_SML_L);
2409 displayWriteChar(osdDisplayPort, elemPosX+1, elemPosY, SYM_PILOT_LOGO_SML_C);
2410 displayWriteChar(osdDisplayPort, elemPosX+2, elemPosY, SYM_PILOT_LOGO_SML_R);
2411 break;
2413 case OSD_THROTTLE_POS:
2415 osdFormatThrottlePosition(buff, false, &elemAttr);
2416 break;
2419 case OSD_VTX_CHANNEL:
2421 vtxDeviceOsdInfo_t osdInfo;
2422 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2424 tfp_sprintf(buff, "CH:%c%s:", osdInfo.bandLetter, osdInfo.channelName);
2425 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2427 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2428 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2429 displayWriteWithAttr(osdDisplayPort, elemPosX + 6, elemPosY, buff, elemAttr);
2430 return true;
2432 break;
2434 case OSD_VTX_POWER:
2436 vtxDeviceOsdInfo_t osdInfo;
2437 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2439 tfp_sprintf(buff, "%c", SYM_VTX_POWER);
2440 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2442 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2443 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2444 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2445 return true;
2448 #if defined(USE_SERIALRX_CRSF)
2449 case OSD_CRSF_RSSI_DBM:
2451 int16_t rssi = rxLinkStatistics.uplinkRSSI;
2452 buff[0] = (rxLinkStatistics.activeAntenna == 0) ? SYM_RSSI : SYM_2RSS; // Separate symbols for each antenna
2453 if (rssi <= -100) {
2454 tfp_sprintf(buff + 1, "%4d%c", rssi, SYM_DBM);
2455 } else {
2456 tfp_sprintf(buff + 1, "%3d%c%c", rssi, SYM_DBM, ' ');
2458 if (!failsafeIsReceivingRxData()){
2459 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2460 } else if (osdConfig()->rssi_dbm_alarm && rssi < osdConfig()->rssi_dbm_alarm) {
2461 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2463 break;
2465 case OSD_CRSF_LQ:
2467 buff[0] = SYM_LQ;
2468 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
2469 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
2470 switch (osdConfig()->crsf_lq_format) {
2471 case OSD_CRSF_LQ_TYPE1:
2472 if (!failsafeIsReceivingRxData()) {
2473 tfp_sprintf(buff+1, "%3d", 0);
2474 } else {
2475 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.uplinkLQ);
2477 break;
2478 case OSD_CRSF_LQ_TYPE2:
2479 if (!failsafeIsReceivingRxData()) {
2480 tfp_sprintf(buff+1, "%s:%3d", " ", 0);
2481 } else {
2482 tfp_sprintf(buff+1, "%d:%3d", rxLinkStatistics.rfMode, rxLinkStatistics.uplinkLQ);
2484 break;
2485 case OSD_CRSF_LQ_TYPE3:
2486 if (!failsafeIsReceivingRxData()) {
2487 tfp_sprintf(buff+1, "%3d", 0);
2488 } else {
2489 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.rfMode >= 2 ? scaledLQ : rxLinkStatistics.uplinkLQ);
2491 break;
2493 if (!failsafeIsReceivingRxData()) {
2494 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2495 } else if (rxLinkStatistics.uplinkLQ < osdConfig()->link_quality_alarm) {
2496 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2498 break;
2501 case OSD_CRSF_SNR_DB:
2503 static pt1Filter_t snrFilterState;
2504 static timeMs_t snrUpdated = 0;
2505 int8_t snrFiltered = pt1FilterApply4(&snrFilterState, rxLinkStatistics.uplinkSNR, 0.5f, MS2S(millis() - snrUpdated));
2506 snrUpdated = millis();
2508 const char* showsnr = "-20";
2509 const char* hidesnr = " ";
2510 if (snrFiltered > osdConfig()->snr_alarm) {
2511 if (cmsInMenu) {
2512 buff[0] = SYM_SNR;
2513 tfp_sprintf(buff + 1, "%s%c", showsnr, SYM_DB);
2514 } else {
2515 buff[0] = SYM_BLANK;
2516 tfp_sprintf(buff + 1, "%s%c", hidesnr, SYM_BLANK);
2518 } else if (snrFiltered <= osdConfig()->snr_alarm) {
2519 buff[0] = SYM_SNR;
2520 if (snrFiltered <= -10) {
2521 tfp_sprintf(buff + 1, "%3d%c", snrFiltered, SYM_DB);
2522 } else {
2523 tfp_sprintf(buff + 1, "%2d%c%c", snrFiltered, SYM_DB, ' ');
2526 break;
2529 case OSD_CRSF_TX_POWER:
2531 if (!failsafeIsReceivingRxData())
2532 tfp_sprintf(buff, "%s%c", " ", SYM_BLANK);
2533 else
2534 tfp_sprintf(buff, "%4d%c", rxLinkStatistics.uplinkTXPower, SYM_MW);
2535 break;
2537 #endif
2539 case OSD_CROSSHAIRS: // Hud is a sub-element of the crosshair
2541 osdCrosshairPosition(&elemPosX, &elemPosY);
2542 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX, elemPosY);
2544 if (osdConfig()->hud_homing && (STATE(GPS_FIX)
2545 #ifdef USE_GPS_FIX_ESTIMATION
2546 || STATE(GPS_ESTIMATED_FIX)
2547 #endif
2548 ) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
2549 osdHudDrawHoming(elemPosX, elemPosY);
2552 if ((STATE(GPS_FIX)
2553 #ifdef USE_GPS_FIX_ESTIMATION
2554 || STATE(GPS_ESTIMATED_FIX)
2555 #endif
2556 ) && isImuHeadingValid()) {
2558 if (osdConfig()->hud_homepoint || osdConfig()->hud_radar_disp > 0 || osdConfig()->hud_wp_disp > 0) {
2559 osdHudClear();
2562 // -------- POI : Home point
2564 if (osdConfig()->hud_homepoint) { // Display the home point (H)
2565 osdHudDrawPoi(GPS_distanceToHome, GPS_directionToHome, -osdGetAltitude() / 100, 0, SYM_HOME, 0 , 0);
2568 // -------- POI : Nearby aircrafts from ESP32 radar
2570 if (osdConfig()->hud_radar_disp > 0) { // Display the POI from the radar
2571 for (uint8_t i = 0; i < osdConfig()->hud_radar_disp; i++) {
2572 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
2573 fpVector3_t poi;
2574 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &radar_pois[i].gps, GEO_ALT_RELATIVE);
2575 radar_pois[i].distance = calculateDistanceToDestination(&poi) / 100; // In meters
2577 if (radar_pois[i].distance >= osdConfig()->hud_radar_range_min && radar_pois[i].distance <= osdConfig()->hud_radar_range_max) {
2578 radar_pois[i].direction = calculateBearingToDestination(&poi) / 100; // In °
2579 radar_pois[i].altitude = (radar_pois[i].gps.alt - osdGetAltitudeMsl()) / 100;
2580 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);
2586 // -------- POI : Next waypoints from navigation
2588 if (osdConfig()->hud_wp_disp > 0 && posControl.waypointListValid && posControl.waypointCount > 0) { // Display the next waypoints
2589 gpsLocation_t wp2;
2590 int j;
2592 for (int i = osdConfig()->hud_wp_disp - 1; i >= 0 ; i--) { // Display in reverse order so the next WP is always written on top
2593 j = posControl.activeWaypointIndex + i;
2594 if (j > posControl.startWpIndex + posControl.waypointCount - 1) { // limit to max WP index for mission
2595 break;
2597 if (posControl.waypointList[j].lat != 0 && posControl.waypointList[j].lon != 0) {
2598 wp2.lat = posControl.waypointList[j].lat;
2599 wp2.lon = posControl.waypointList[j].lon;
2600 wp2.alt = posControl.waypointList[j].alt;
2601 fpVector3_t poi;
2602 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &wp2, waypointMissionAltConvMode(posControl.waypointList[j].p3));
2603 int32_t altConvModeAltitude = waypointMissionAltConvMode(posControl.waypointList[j].p3) == GEO_ALT_ABSOLUTE ? osdGetAltitudeMsl() : osdGetAltitude();
2604 j = getGeoWaypointNumber(j);
2605 while (j > 9) j -= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2606 osdHudDrawPoi(calculateDistanceToDestination(&poi) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi) / 100), (posControl.waypointList[j].alt - altConvModeAltitude)/ 100, 2, SYM_WAYPOINT, 48 + j, i);
2612 return true;
2613 break;
2615 case OSD_ATTITUDE_ROLL:
2616 buff[0] = SYM_ROLL_LEVEL;
2617 if (ABS(attitude.values.roll) >= 1)
2618 buff[0] += (attitude.values.roll < 0 ? -1 : 1);
2619 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.roll)), 0, 1, 0, 3, false);
2620 break;
2622 case OSD_ATTITUDE_PITCH:
2623 if (ABS(attitude.values.pitch) < 1)
2624 buff[0] = 'P';
2625 else if (attitude.values.pitch > 0)
2626 buff[0] = SYM_PITCH_DOWN;
2627 else if (attitude.values.pitch < 0)
2628 buff[0] = SYM_PITCH_UP;
2629 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.pitch)), 0, 1, 0, 3, false);
2630 break;
2632 case OSD_ARTIFICIAL_HORIZON:
2634 float rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2635 float pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2637 pitchAngle -= osdConfig()->ahi_camera_uptilt_comp ? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt) : 0;
2638 pitchAngle += DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2639 if (osdConfig()->ahi_reverse_roll) {
2640 rollAngle = -rollAngle;
2642 osdDrawArtificialHorizon(osdDisplayPort, osdGetDisplayPortCanvas(),
2643 OSD_DRAW_POINT_GRID(elemPosX, elemPosY), rollAngle, pitchAngle);
2644 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
2645 osdDrawSingleElement(OSD_CROSSHAIRS);
2647 return true;
2650 case OSD_HORIZON_SIDEBARS:
2652 osdDrawSidebars(osdDisplayPort, osdGetDisplayPortCanvas());
2653 return true;
2656 #if defined(USE_BARO) || defined(USE_GPS)
2657 case OSD_VARIO:
2659 float zvel = getEstimatedActualVelocity(Z);
2660 osdDrawVario(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), zvel);
2661 return true;
2664 case OSD_VARIO_NUM:
2666 int16_t value = getEstimatedActualVelocity(Z);
2667 char sym;
2668 switch ((osd_unit_e)osdConfig()->units) {
2669 case OSD_UNIT_UK:
2670 FALLTHROUGH;
2671 case OSD_UNIT_IMPERIAL:
2672 // Convert to centifeet/s
2673 value = CENTIMETERS_TO_CENTIFEET(value);
2674 sym = SYM_FTS;
2675 break;
2676 case OSD_UNIT_GA:
2677 // Convert to centi-100feet/min
2678 value = CENTIMETERS_TO_FEET(value * 60);
2679 sym = SYM_100FTM;
2680 break;
2681 default:
2682 case OSD_UNIT_METRIC_MPH:
2683 FALLTHROUGH;
2684 case OSD_UNIT_METRIC:
2685 // Already in cm/s
2686 sym = SYM_MS;
2687 break;
2690 osdFormatCentiNumber(buff, value, 0, 1, 0, 3, false);
2691 buff[3] = sym;
2692 buff[4] = '\0';
2693 break;
2695 case OSD_CLIMB_EFFICIENCY:
2697 // amperage is in centi amps (10mA), vertical speed is in cms/s. We want
2698 // Ah/dist only to show when vertical speed > 1m/s.
2699 static pt1Filter_t veFilterState;
2700 static timeUs_t vEfficiencyUpdated = 0;
2701 int32_t value = 0;
2702 timeUs_t currentTimeUs = micros();
2703 timeDelta_t vEfficiencyTimeDelta = cmpTimeUs(currentTimeUs, vEfficiencyUpdated);
2704 if (getEstimatedActualVelocity(Z) > 0) {
2705 if (vEfficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2706 // Centiamps (kept for osdFormatCentiNumber) / m/s - Will appear as A / m/s in OSD
2707 value = pt1FilterApply4(&veFilterState, (float)getAmperage() / (getEstimatedActualVelocity(Z) / 100.0f), 1, US2S(vEfficiencyTimeDelta));
2709 vEfficiencyUpdated = currentTimeUs;
2710 } else {
2711 value = veFilterState.state;
2714 bool efficiencyValid = (value > 0) && (getEstimatedActualVelocity(Z) > 100);
2715 switch (osdConfig()->units) {
2716 case OSD_UNIT_UK:
2717 FALLTHROUGH;
2718 case OSD_UNIT_GA:
2719 FALLTHROUGH;
2720 case OSD_UNIT_IMPERIAL:
2721 // mAh/foot
2722 if (efficiencyValid) {
2723 osdFormatCentiNumber(buff, (value * METERS_PER_FOOT), 1, 2, 2, 3, false);
2724 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_AH_V_FT_0, SYM_AH_V_FT_1);
2725 } else {
2726 buff[0] = buff[1] = buff[2] = '-';
2727 buff[3] = SYM_AH_V_FT_0;
2728 buff[4] = SYM_AH_V_FT_1;
2729 buff[5] = '\0';
2731 break;
2732 case OSD_UNIT_METRIC_MPH:
2733 FALLTHROUGH;
2734 case OSD_UNIT_METRIC:
2735 // mAh/metre
2736 if (efficiencyValid) {
2737 osdFormatCentiNumber(buff, value, 1, 2, 2, 3, false);
2738 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_AH_V_M_0, SYM_AH_V_M_1);
2739 } else {
2740 buff[0] = buff[1] = buff[2] = '-';
2741 buff[3] = SYM_AH_V_M_0;
2742 buff[4] = SYM_AH_V_M_1;
2743 buff[5] = '\0';
2745 break;
2747 break;
2749 case OSD_GLIDE_TIME_REMAINING:
2751 uint16_t glideTime = osdGetRemainingGlideTime();
2752 buff[0] = SYM_GLIDE_MINS;
2753 if (glideTime > 0) {
2754 // Maximum value we can show in minutes is 99 minutes and 59 seconds. It is extremely unlikely that glide
2755 // time will be longer than 99 minutes. If it is, it will show 99:^^
2756 if (glideTime > (99 * 60) + 59) {
2757 tfp_sprintf(buff + 1, "%02d:", (int)(glideTime / 60));
2758 buff[4] = SYM_DECORATION;
2759 buff[5] = SYM_DECORATION;
2760 } else {
2761 tfp_sprintf(buff + 1, "%02d:%02d", (int)(glideTime / 60), (int)(glideTime % 60));
2763 } else {
2764 tfp_sprintf(buff + 1, "%s", "--:--");
2766 buff[6] = '\0';
2767 break;
2769 case OSD_GLIDE_RANGE:
2771 uint16_t glideSeconds = osdGetRemainingGlideTime();
2772 buff[0] = SYM_GLIDE_DIST;
2773 if (glideSeconds > 0) {
2774 uint32_t glideRangeCM = glideSeconds * gpsSol.groundSpeed;
2775 osdFormatDistanceSymbol(buff + 1, glideRangeCM, 0);
2776 } else {
2777 tfp_sprintf(buff + 1, "%s%c", "---", SYM_BLANK);
2778 buff[5] = '\0';
2780 break;
2782 #endif
2784 case OSD_SWITCH_INDICATOR_0:
2785 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channel - 1), buff);
2786 break;
2788 case OSD_SWITCH_INDICATOR_1:
2789 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channel - 1), buff);
2790 break;
2792 case OSD_SWITCH_INDICATOR_2:
2793 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channel - 1), buff);
2794 break;
2796 case OSD_SWITCH_INDICATOR_3:
2797 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channel - 1), buff);
2798 break;
2800 case OSD_PAN_SERVO_CENTRED:
2802 int16_t panOffset = osdGetPanServoOffset();
2803 const timeMs_t panServoTimeNow = millis();
2804 static timeMs_t panServoTimeOffCentre = 0;
2806 if (panOffset < 0) {
2807 if (osdConfig()->pan_servo_offcentre_warning != 0 && panOffset >= -osdConfig()->pan_servo_offcentre_warning) {
2808 if (panServoTimeOffCentre == 0) {
2809 panServoTimeOffCentre = panServoTimeNow;
2810 } else if (panServoTimeNow >= (panServoTimeOffCentre + 10000 )) {
2811 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2813 } else {
2814 panServoTimeOffCentre = 0;
2817 if (osdConfig()->pan_servo_indicator_show_degrees) {
2818 tfp_sprintf(buff, "%3d%c", -panOffset, SYM_DEGREES);
2819 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2821 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_OFFSET_R, elemAttr);
2822 } else if (panOffset > 0) {
2823 if (osdConfig()->pan_servo_offcentre_warning != 0 && panOffset <= osdConfig()->pan_servo_offcentre_warning) {
2824 if (panServoTimeOffCentre == 0) {
2825 panServoTimeOffCentre = panServoTimeNow;
2826 } else if (panServoTimeNow >= (panServoTimeOffCentre + 10000 )) {
2827 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2829 } else {
2830 panServoTimeOffCentre = 0;
2833 if (osdConfig()->pan_servo_indicator_show_degrees) {
2834 tfp_sprintf(buff, "%3d%c", panOffset, SYM_DEGREES);
2835 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2837 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_OFFSET_L, elemAttr);
2838 } else {
2839 panServoTimeOffCentre = 0;
2841 if (osdConfig()->pan_servo_indicator_show_degrees) {
2842 tfp_sprintf(buff, "%3d%c", panOffset, SYM_DEGREES);
2843 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2845 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_CENTRED);
2848 return true;
2850 break;
2852 case OSD_ACTIVE_PROFILE:
2853 tfp_sprintf(buff, "%c%u", SYM_PROFILE, (getConfigProfile() + 1));
2854 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2855 break;
2857 case OSD_ROLL_PIDS:
2858 osdDisplayFlightPIDValues(elemPosX, elemPosY, "ROL", PID_ROLL, ADJUSTMENT_ROLL_P, ADJUSTMENT_ROLL_I, ADJUSTMENT_ROLL_D, ADJUSTMENT_ROLL_FF);
2859 return true;
2861 case OSD_PITCH_PIDS:
2862 osdDisplayFlightPIDValues(elemPosX, elemPosY, "PIT", PID_PITCH, ADJUSTMENT_PITCH_P, ADJUSTMENT_PITCH_I, ADJUSTMENT_PITCH_D, ADJUSTMENT_PITCH_FF);
2863 return true;
2865 case OSD_YAW_PIDS:
2866 osdDisplayFlightPIDValues(elemPosX, elemPosY, "YAW", PID_YAW, ADJUSTMENT_YAW_P, ADJUSTMENT_YAW_I, ADJUSTMENT_YAW_D, ADJUSTMENT_YAW_FF);
2867 return true;
2869 case OSD_LEVEL_PIDS:
2870 osdDisplayNavPIDValues(elemPosX, elemPosY, "LEV", PID_LEVEL, ADJUSTMENT_LEVEL_P, ADJUSTMENT_LEVEL_I, ADJUSTMENT_LEVEL_D);
2871 return true;
2873 case OSD_POS_XY_PIDS:
2874 osdDisplayNavPIDValues(elemPosX, elemPosY, "PXY", PID_POS_XY, ADJUSTMENT_POS_XY_P, ADJUSTMENT_POS_XY_I, ADJUSTMENT_POS_XY_D);
2875 return true;
2877 case OSD_POS_Z_PIDS:
2878 osdDisplayNavPIDValues(elemPosX, elemPosY, "PZ", PID_POS_Z, ADJUSTMENT_POS_Z_P, ADJUSTMENT_POS_Z_I, ADJUSTMENT_POS_Z_D);
2879 return true;
2881 case OSD_VEL_XY_PIDS:
2882 osdDisplayNavPIDValues(elemPosX, elemPosY, "VXY", PID_VEL_XY, ADJUSTMENT_VEL_XY_P, ADJUSTMENT_VEL_XY_I, ADJUSTMENT_VEL_XY_D);
2883 return true;
2885 case OSD_VEL_Z_PIDS:
2886 osdDisplayNavPIDValues(elemPosX, elemPosY, "VZ", PID_VEL_Z, ADJUSTMENT_VEL_Z_P, ADJUSTMENT_VEL_Z_I, ADJUSTMENT_VEL_Z_D);
2887 return true;
2889 case OSD_HEADING_P:
2890 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "HP", 0, pidBank()->pid[PID_HEADING].P, 3, 0, ADJUSTMENT_HEADING_P);
2891 return true;
2893 case OSD_BOARD_ALIGN_ROLL:
2894 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT);
2895 return true;
2897 case OSD_BOARD_ALIGN_PITCH:
2898 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT);
2899 return true;
2901 case OSD_RC_EXPO:
2902 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "EXP", 0, currentControlRateProfile->stabilized.rcExpo8, 3, 0, ADJUSTMENT_RC_EXPO);
2903 return true;
2905 case OSD_RC_YAW_EXPO:
2906 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "YEX", 0, currentControlRateProfile->stabilized.rcYawExpo8, 3, 0, ADJUSTMENT_RC_YAW_EXPO);
2907 return true;
2909 case OSD_THROTTLE_EXPO:
2910 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TEX", 0, currentControlRateProfile->throttle.rcExpo8, 3, 0, ADJUSTMENT_THROTTLE_EXPO);
2911 return true;
2913 case OSD_PITCH_RATE:
2914 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SPR");
2916 elemAttr = TEXT_ATTRIBUTES_NONE;
2917 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_PITCH]);
2918 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2919 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2920 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2921 return true;
2923 case OSD_ROLL_RATE:
2924 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SRR");
2926 elemAttr = TEXT_ATTRIBUTES_NONE;
2927 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_ROLL]);
2928 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2929 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2930 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2931 return true;
2933 case OSD_YAW_RATE:
2934 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "SYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2935 return true;
2937 case OSD_MANUAL_RC_EXPO:
2938 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MEX", 0, currentControlRateProfile->manual.rcExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO);
2939 return true;
2941 case OSD_MANUAL_RC_YAW_EXPO:
2942 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYX", 0, currentControlRateProfile->manual.rcYawExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO);
2943 return true;
2945 case OSD_MANUAL_PITCH_RATE:
2946 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MPR");
2948 elemAttr = TEXT_ATTRIBUTES_NONE;
2949 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_PITCH]);
2950 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2951 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2952 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2953 return true;
2955 case OSD_MANUAL_ROLL_RATE:
2956 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MRR");
2958 elemAttr = TEXT_ATTRIBUTES_NONE;
2959 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_ROLL]);
2960 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2961 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2962 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2963 return true;
2965 case OSD_MANUAL_YAW_RATE:
2966 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2967 return true;
2969 case OSD_NAV_FW_CRUISE_THR:
2970 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CRZ", 0, currentBatteryProfile->nav.fw.cruise_throttle, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR);
2971 return true;
2973 case OSD_NAV_FW_PITCH2THR:
2974 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "P2T", 0, currentBatteryProfile->nav.fw.pitch_to_throttle, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR);
2975 return true;
2977 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE:
2978 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "0TP", 0, (float)navConfig()->fw.minThrottleDownPitchAngle / 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE);
2979 return true;
2981 case OSD_FW_ALT_PID_OUTPUTS:
2983 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2984 osdFormatPidControllerOutput(buff, "PZO", &nav_pids->fw_alt, 10, true); // display requested pitch degrees
2985 break;
2988 case OSD_FW_POS_PID_OUTPUTS:
2990 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers(); // display requested roll degrees
2991 osdFormatPidControllerOutput(buff, "PXYO", &nav_pids->fw_nav, 1, true);
2992 break;
2995 case OSD_MC_VEL_Z_PID_OUTPUTS:
2997 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2998 osdFormatPidControllerOutput(buff, "VZO", &nav_pids->vel[Z], 100, false); // display throttle adjustment µs
2999 break;
3002 case OSD_MC_VEL_X_PID_OUTPUTS:
3004 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
3005 osdFormatPidControllerOutput(buff, "VXO", &nav_pids->vel[X], 100, false); // display requested acceleration cm/s^2
3006 break;
3009 case OSD_MC_VEL_Y_PID_OUTPUTS:
3011 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
3012 osdFormatPidControllerOutput(buff, "VYO", &nav_pids->vel[Y], 100, false); // display requested acceleration cm/s^2
3013 break;
3016 case OSD_MC_POS_XYZ_P_OUTPUTS:
3018 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
3019 strcpy(buff, "POSO ");
3020 // display requested velocity cm/s
3021 tfp_sprintf(buff + 5, "%4d", (int)lrintf(nav_pids->pos[X].output_constrained * 100));
3022 buff[9] = ' ';
3023 tfp_sprintf(buff + 10, "%4d", (int)lrintf(nav_pids->pos[Y].output_constrained * 100));
3024 buff[14] = ' ';
3025 tfp_sprintf(buff + 15, "%4d", (int)lrintf(nav_pids->pos[Z].output_constrained * 100));
3026 buff[19] = '\0';
3027 break;
3030 case OSD_POWER:
3032 bool kiloWatt = osdFormatCentiNumber(buff, getPower(), 1000, 2, 2, 3, false);
3033 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3034 buff[4] = '\0';
3036 uint8_t current_alarm = osdConfig()->current_alarm;
3037 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
3038 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3040 break;
3043 case OSD_AIR_SPEED:
3045 #ifdef USE_PITOT
3046 buff[0] = SYM_AIR;
3048 if (pitotIsHealthy())
3050 const float airspeed_estimate = getAirspeedEstimate();
3051 osdFormatVelocityStr(buff + 1, airspeed_estimate, false, false);
3052 if ((osdConfig()->airspeed_alarm_min != 0 && airspeed_estimate < osdConfig()->airspeed_alarm_min) ||
3053 (osdConfig()->airspeed_alarm_max != 0 && airspeed_estimate > osdConfig()->airspeed_alarm_max)) {
3054 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3057 else
3059 strcpy(buff + 1, " X!");
3060 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3062 #else
3063 return false;
3064 #endif
3065 break;
3068 case OSD_AIR_MAX_SPEED:
3070 #ifdef USE_PITOT
3071 buff[0] = SYM_MAX;
3072 buff[1] = SYM_AIR;
3073 osdFormatVelocityStr(buff + 2, stats.max_air_speed, false, false);
3074 #else
3075 return false;
3076 #endif
3077 break;
3080 case OSD_RTC_TIME:
3082 // RTC not configured will show 00:00
3083 dateTime_t dateTime;
3084 rtcGetDateTimeLocal(&dateTime);
3085 buff[0] = SYM_CLOCK;
3086 tfp_sprintf(buff + 1, "%02u:%02u:%02u", dateTime.hours, dateTime.minutes, dateTime.seconds);
3087 break;
3090 case OSD_MESSAGES:
3092 elemAttr = osdGetSystemMessage(buff, OSD_MESSAGE_LENGTH, true);
3093 break;
3096 case OSD_VERSION:
3098 tfp_sprintf(buff, "INAV %s", FC_VERSION_STRING);
3099 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3100 break;
3103 case OSD_MAIN_BATT_CELL_VOLTAGE:
3105 uint8_t base_digits = 3U;
3106 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
3107 if(isDJICompatibleVideoSystem(osdConfig())) {
3108 base_digits = 4U; // Add extra digit to account for decimal point taking an extra character space
3110 #endif
3111 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawAverageCellVoltage(), base_digits, 2);
3112 return true;
3115 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE:
3117 uint8_t base_digits = 3U;
3118 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
3119 if(isDJICompatibleVideoSystem(osdConfig())) {
3120 base_digits = 4U; // Add extra digit to account for decimal point taking an extra character space
3122 #endif
3123 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedAverageCellVoltage(), base_digits, 2);
3124 return true;
3127 case OSD_SCALED_THROTTLE_POS:
3129 osdFormatThrottlePosition(buff, true, &elemAttr);
3130 break;
3133 case OSD_HEADING:
3135 buff[0] = SYM_HEADING;
3136 if (osdIsHeadingValid()) {
3137 int16_t h = DECIDEGREES_TO_DEGREES(osdGetHeading());
3138 if (h < 0) {
3139 h += 360;
3141 tfp_sprintf(&buff[1], "%3d", h);
3142 } else {
3143 buff[1] = buff[2] = buff[3] = '-';
3145 buff[4] = SYM_DEGREES;
3146 buff[5] = '\0';
3147 break;
3150 case OSD_HEADING_GRAPH:
3152 if (osdIsHeadingValid()) {
3153 osdDrawHeadingGraph(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), osdGetHeading());
3154 return true;
3155 } else {
3156 buff[0] = buff[2] = buff[4] = buff[6] = buff[8] = SYM_HEADING_LINE;
3157 buff[1] = buff[3] = buff[5] = buff[7] = SYM_HEADING_DIVIDED_LINE;
3158 buff[OSD_HEADING_GRAPH_WIDTH] = '\0';
3160 break;
3163 case OSD_EFFICIENCY_MAH_PER_KM:
3165 // amperage is in centi amps, speed is in cms/s. We want
3166 // mah/km. Only show when ground speed > 1m/s.
3167 static pt1Filter_t eFilterState;
3168 static timeUs_t efficiencyUpdated = 0;
3169 int32_t value = 0;
3170 bool moreThanAh = false;
3171 timeUs_t currentTimeUs = micros();
3172 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
3173 uint8_t digits = 3U;
3174 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
3175 if (isDJICompatibleVideoSystem(osdConfig())) {
3176 // Increase number of digits so values above 99 don't get scaled by osdFormatCentiNumber
3177 digits = 4U;
3179 #endif
3180 if ((STATE(GPS_FIX)
3181 #ifdef USE_GPS_FIX_ESTIMATION
3182 || STATE(GPS_ESTIMATED_FIX)
3183 #endif
3184 ) && gpsSol.groundSpeed > 0) {
3185 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
3186 value = pt1FilterApply4(&eFilterState, ((float)getAmperage() / gpsSol.groundSpeed) / 0.0036f,
3187 1, US2S(efficiencyTimeDelta));
3189 efficiencyUpdated = currentTimeUs;
3190 } else {
3191 value = eFilterState.state;
3194 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
3195 switch (osdConfig()->units) {
3196 case OSD_UNIT_UK:
3197 FALLTHROUGH;
3198 case OSD_UNIT_IMPERIAL:
3199 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10, 1000, 0, 2, digits, false);
3200 if (!moreThanAh) {
3201 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_MAH_MI_0, SYM_MAH_MI_1);
3202 } else {
3203 tfp_sprintf(buff + strlen(buff), "%c", SYM_AH_MI);
3205 if (!efficiencyValid) {
3206 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3207 buff[digits] = SYM_MAH_MI_0; // This will overwrite the "-" at buff[3] if not in DJICOMPAT mode
3208 buff[digits + 1] = SYM_MAH_MI_1;
3209 buff[digits + 2] = '\0';
3211 break;
3212 case OSD_UNIT_GA:
3213 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10, 1000, 0, 2, digits, false);
3214 if (!moreThanAh) {
3215 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_MAH_NM_0, SYM_MAH_NM_1);
3216 } else {
3217 tfp_sprintf(buff + strlen(buff), "%c", SYM_AH_NM);
3219 if (!efficiencyValid) {
3220 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3221 buff[digits] = SYM_MAH_NM_0;
3222 buff[digits + 1] = SYM_MAH_NM_1;
3223 buff[digits + 2] = '\0';
3225 break;
3226 case OSD_UNIT_METRIC_MPH:
3227 FALLTHROUGH;
3228 case OSD_UNIT_METRIC:
3229 moreThanAh = osdFormatCentiNumber(buff, value * 100, 1000, 0, 2, digits, false);
3230 if (!moreThanAh) {
3231 tfp_sprintf(buff + strlen(buff), "%c%c", SYM_MAH_KM_0, SYM_MAH_KM_1);
3232 } else {
3233 tfp_sprintf(buff + strlen(buff), "%c", SYM_AH_KM);
3235 if (!efficiencyValid) {
3236 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3237 buff[digits] = SYM_MAH_KM_0;
3238 buff[digits + 1] = SYM_MAH_KM_1;
3239 buff[digits + 2] = '\0';
3241 break;
3243 break;
3246 case OSD_EFFICIENCY_WH_PER_KM:
3248 // amperage is in centi amps, speed is in cms/s. We want
3249 // mWh/km. Only show when ground speed > 1m/s.
3250 static pt1Filter_t eFilterState;
3251 static timeUs_t efficiencyUpdated = 0;
3252 int32_t value = 0;
3253 timeUs_t currentTimeUs = micros();
3254 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
3255 if ((STATE(GPS_FIX)
3256 #ifdef USE_GPS_FIX_ESTIMATION
3257 || STATE(GPS_ESTIMATED_FIX)
3258 #endif
3259 ) && gpsSol.groundSpeed > 0) {
3260 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
3261 value = pt1FilterApply4(&eFilterState, ((float)getPower() / gpsSol.groundSpeed) / 0.0036f,
3262 1, US2S(efficiencyTimeDelta));
3264 efficiencyUpdated = currentTimeUs;
3265 } else {
3266 value = eFilterState.state;
3269 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
3270 switch (osdConfig()->units) {
3271 case OSD_UNIT_UK:
3272 FALLTHROUGH;
3273 case OSD_UNIT_IMPERIAL:
3274 osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10000, 0, 2, 0, 3, false);
3275 buff[3] = SYM_WH_MI;
3276 break;
3277 case OSD_UNIT_GA:
3278 osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10000, 0, 2, 0, 3, false);
3279 buff[3] = SYM_WH_NM;
3280 break;
3281 case OSD_UNIT_METRIC_MPH:
3282 FALLTHROUGH;
3283 case OSD_UNIT_METRIC:
3284 osdFormatCentiNumber(buff, value / 10, 0, 2, 0, 3, false);
3285 buff[3] = SYM_WH_KM;
3286 break;
3288 buff[4] = '\0';
3289 if (!efficiencyValid) {
3290 buff[0] = buff[1] = buff[2] = '-';
3292 break;
3295 case OSD_GFORCE:
3297 buff[0] = SYM_GFORCE;
3298 osdFormatCentiNumber(buff + 1, GForce, 0, 2, 0, 3, false);
3299 if (GForce > osdConfig()->gforce_alarm * 100) {
3300 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3302 break;
3305 case OSD_GFORCE_X:
3306 case OSD_GFORCE_Y:
3307 case OSD_GFORCE_Z:
3309 float GForceValue = GForceAxis[item - OSD_GFORCE_X];
3310 buff[0] = SYM_GFORCE_X + item - OSD_GFORCE_X;
3311 osdFormatCentiNumber(buff + 1, GForceValue, 0, 2, 0, 4, false);
3312 if ((GForceValue < osdConfig()->gforce_axis_alarm_min * 100) || (GForceValue > osdConfig()->gforce_axis_alarm_max * 100)) {
3313 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3315 break;
3317 case OSD_DEBUG:
3320 * Longest representable string is -2147483648 does not fit in the screen.
3321 * Only 7 digits for negative and 8 digits for positive values allowed
3323 for (uint8_t bufferIndex = 0; bufferIndex < DEBUG32_VALUE_COUNT; ++elemPosY, bufferIndex += 2) {
3324 tfp_sprintf(
3325 buff,
3326 "[%u]=%8ld [%u]=%8ld",
3327 bufferIndex,
3328 (long)constrain(debug[bufferIndex], -9999999, 99999999),
3329 bufferIndex+1,
3330 (long)constrain(debug[bufferIndex+1], -9999999, 99999999)
3332 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3334 break;
3337 case OSD_IMU_TEMPERATURE:
3339 int16_t temperature;
3340 const bool valid = getIMUTemperature(&temperature);
3341 osdDisplayTemperature(elemPosX, elemPosY, SYM_IMU_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
3342 return true;
3345 case OSD_BARO_TEMPERATURE:
3347 int16_t temperature;
3348 const bool valid = getBaroTemperature(&temperature);
3349 osdDisplayTemperature(elemPosX, elemPosY, SYM_BARO_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
3350 return true;
3353 #ifdef USE_TEMPERATURE_SENSOR
3354 case OSD_TEMP_SENSOR_0_TEMPERATURE:
3355 case OSD_TEMP_SENSOR_1_TEMPERATURE:
3356 case OSD_TEMP_SENSOR_2_TEMPERATURE:
3357 case OSD_TEMP_SENSOR_3_TEMPERATURE:
3358 case OSD_TEMP_SENSOR_4_TEMPERATURE:
3359 case OSD_TEMP_SENSOR_5_TEMPERATURE:
3360 case OSD_TEMP_SENSOR_6_TEMPERATURE:
3361 case OSD_TEMP_SENSOR_7_TEMPERATURE:
3363 osdDisplayTemperatureSensor(elemPosX, elemPosY, item - OSD_TEMP_SENSOR_0_TEMPERATURE);
3364 return true;
3366 #endif /* ifdef USE_TEMPERATURE_SENSOR */
3368 case OSD_WIND_SPEED_HORIZONTAL:
3369 #ifdef USE_WIND_ESTIMATOR
3371 bool valid = isEstimatedWindSpeedValid();
3372 float horizontalWindSpeed;
3373 uint16_t angle;
3374 horizontalWindSpeed = getEstimatedHorizontalWindSpeed(&angle);
3375 int16_t windDirection = osdGetHeadingAngle( CENTIDEGREES_TO_DEGREES((int)angle) - DECIDEGREES_TO_DEGREES(attitude.values.yaw) + 22);
3376 buff[0] = SYM_WIND_HORIZONTAL;
3377 buff[1] = SYM_DECORATION + (windDirection*2 / 90);
3378 osdFormatWindSpeedStr(buff + 2, horizontalWindSpeed, valid);
3379 break;
3381 #else
3382 return false;
3383 #endif
3385 case OSD_WIND_SPEED_VERTICAL:
3386 #ifdef USE_WIND_ESTIMATOR
3388 buff[0] = SYM_WIND_VERTICAL;
3389 buff[1] = SYM_BLANK;
3390 bool valid = isEstimatedWindSpeedValid();
3391 float verticalWindSpeed;
3392 verticalWindSpeed = -getEstimatedWindSpeed(Z); //from NED to NEU
3393 if (verticalWindSpeed < 0) {
3394 buff[1] = SYM_AH_DECORATION_DOWN;
3395 verticalWindSpeed = -verticalWindSpeed;
3396 } else {
3397 buff[1] = SYM_AH_DECORATION_UP;
3399 osdFormatWindSpeedStr(buff + 2, verticalWindSpeed, valid);
3400 break;
3402 #else
3403 return false;
3404 #endif
3406 case OSD_PLUS_CODE:
3408 STATIC_ASSERT(GPS_DEGREES_DIVIDER == OLC_DEG_MULTIPLIER, invalid_olc_deg_multiplier);
3409 int digits = osdConfig()->plus_code_digits;
3410 int digitsRemoved = osdConfig()->plus_code_short * 2;
3411 if ((STATE(GPS_FIX)
3412 #ifdef USE_GPS_FIX_ESTIMATION
3413 || STATE(GPS_ESTIMATED_FIX)
3414 #endif
3415 )) {
3416 olc_encode(gpsSol.llh.lat, gpsSol.llh.lon, digits, buff, sizeof(buff));
3417 } else {
3418 // +codes with > 8 digits have a + at the 9th digit
3419 // and we only support 10 and up.
3420 memset(buff, '-', digits + 1);
3421 buff[8] = '+';
3422 buff[digits + 1] = '\0';
3424 // Optionally trim digits from the left
3425 memmove(buff, buff+digitsRemoved, strlen(buff) + digitsRemoved);
3426 buff[digits + 1 - digitsRemoved] = '\0';
3427 break;
3430 case OSD_AZIMUTH:
3433 buff[0] = SYM_AZIMUTH;
3434 if (osdIsHeadingValid()) {
3435 int16_t h = GPS_directionToHome;
3436 if (h < 0) {
3437 h += 360;
3439 if (h >= 180)
3440 h = h - 180;
3441 else
3442 h = h + 180;
3444 tfp_sprintf(&buff[1], "%3d", h);
3445 } else {
3446 buff[1] = buff[2] = buff[3] = '-';
3448 buff[4] = SYM_DEGREES;
3449 buff[5] = '\0';
3450 break;
3453 case OSD_MAP_SCALE:
3455 float scaleToUnit;
3456 int scaleUnitDivisor;
3457 char symUnscaled;
3458 char symScaled;
3459 int maxDecimals;
3461 switch (osdConfig()->units) {
3462 case OSD_UNIT_UK:
3463 FALLTHROUGH;
3464 case OSD_UNIT_IMPERIAL:
3465 scaleToUnit = 100 / 1609.3440f; // scale to 0.01mi for osdFormatCentiNumber()
3466 scaleUnitDivisor = 0;
3467 symUnscaled = SYM_MI;
3468 symScaled = SYM_MI;
3469 maxDecimals = 2;
3470 break;
3471 case OSD_UNIT_GA:
3472 scaleToUnit = 100 / 1852.0010f; // scale to 0.01mi for osdFormatCentiNumber()
3473 scaleUnitDivisor = 0;
3474 symUnscaled = SYM_NM;
3475 symScaled = SYM_NM;
3476 maxDecimals = 2;
3477 break;
3478 default:
3479 case OSD_UNIT_METRIC_MPH:
3480 FALLTHROUGH;
3481 case OSD_UNIT_METRIC:
3482 scaleToUnit = 100; // scale to cm for osdFormatCentiNumber()
3483 scaleUnitDivisor = 1000; // Convert to km when scale gets bigger than 999m
3484 symUnscaled = SYM_M;
3485 symScaled = SYM_KM;
3486 maxDecimals = 0;
3487 break;
3489 buff[0] = SYM_SCALE;
3490 if (osdMapData.scale > 0) {
3491 bool scaled = osdFormatCentiNumber(&buff[1], osdMapData.scale * scaleToUnit, scaleUnitDivisor, maxDecimals, 2, 3, false);
3492 buff[4] = scaled ? symScaled : symUnscaled;
3493 // Make sure this is cleared if the map stops being drawn
3494 osdMapData.scale = 0;
3495 } else {
3496 memset(&buff[1], '-', 4);
3498 buff[5] = '\0';
3499 break;
3501 case OSD_MAP_REFERENCE:
3503 char referenceSymbol;
3504 if (osdMapData.referenceSymbol) {
3505 referenceSymbol = osdMapData.referenceSymbol;
3506 // Make sure this is cleared if the map stops being drawn
3507 osdMapData.referenceSymbol = 0;
3508 } else {
3509 referenceSymbol = '-';
3511 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DECORATION);
3512 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 1, referenceSymbol);
3513 return true;
3516 case OSD_GVAR_0:
3518 osdFormatGVar(buff, 0);
3519 break;
3521 case OSD_GVAR_1:
3523 osdFormatGVar(buff, 1);
3524 break;
3526 case OSD_GVAR_2:
3528 osdFormatGVar(buff, 2);
3529 break;
3531 case OSD_GVAR_3:
3533 osdFormatGVar(buff, 3);
3534 break;
3537 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3538 case OSD_RC_SOURCE:
3540 const char *source_text = IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
3541 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3542 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, source_text, elemAttr);
3543 return true;
3545 #endif
3547 #if defined(USE_ESC_SENSOR)
3548 case OSD_ESC_RPM:
3550 escSensorData_t * escSensor = escSensorGetData();
3551 if (escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE) {
3552 osdFormatRpm(buff, escSensor->rpm);
3554 else {
3555 osdFormatRpm(buff, 0);
3557 break;
3559 case OSD_ESC_TEMPERATURE:
3561 escSensorData_t * escSensor = escSensorGetData();
3562 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
3563 osdDisplayTemperature(elemPosX, elemPosY, SYM_ESC_TEMP, NULL, escTemperatureValid, (escSensor->temperature)*10, osdConfig()->esc_temp_alarm_min, osdConfig()->esc_temp_alarm_max);
3564 return true;
3566 #endif
3567 case OSD_TPA:
3569 char buff[4];
3570 textAttributes_t attr;
3572 displayWrite(osdDisplayPort, elemPosX, elemPosY, "TPA");
3573 attr = TEXT_ATTRIBUTES_NONE;
3574 tfp_sprintf(buff, "%3d", currentControlRateProfile->throttle.dynPID);
3575 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA)) {
3576 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3578 displayWriteWithAttr(osdDisplayPort, elemPosX + 5, elemPosY, buff, attr);
3580 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, "BP");
3581 attr = TEXT_ATTRIBUTES_NONE;
3582 tfp_sprintf(buff, "%4d", currentControlRateProfile->throttle.pa_breakpoint);
3583 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT)) {
3584 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3586 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY + 1, buff, attr);
3588 return true;
3590 case OSD_TPA_TIME_CONSTANT:
3592 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TPA TC", 0, currentControlRateProfile->throttle.fixedWingTauMs, 4, 0, ADJUSTMENT_FW_TPA_TIME_CONSTANT);
3593 return true;
3595 case OSD_FW_LEVEL_TRIM:
3597 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "LEVEL", 0, getFixedWingLevelTrim(), 3, 1, ADJUSTMENT_FW_LEVEL_TRIM);
3598 return true;
3601 case OSD_NAV_FW_CONTROL_SMOOTHNESS:
3603 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CTL S", 0, navConfig()->fw.control_smoothness, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS);
3604 return true;
3606 #ifdef USE_MULTI_MISSION
3607 case OSD_NAV_WP_MULTI_MISSION_INDEX:
3609 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "WP NO", 0, navConfig()->general.waypoint_multi_mission_index, 1, 0, ADJUSTMENT_NAV_WP_MULTI_MISSION_INDEX);
3610 return true;
3612 #endif
3613 case OSD_MISSION:
3615 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION)) {
3616 char buf[5];
3617 switch (posControl.wpMissionPlannerStatus) {
3618 case WP_PLAN_WAIT:
3619 strcpy(buf, "WAIT");
3620 break;
3621 case WP_PLAN_SAVE:
3622 strcpy(buf, "SAVE");
3623 break;
3624 case WP_PLAN_OK:
3625 strcpy(buf, " OK ");
3626 break;
3627 case WP_PLAN_FULL:
3628 strcpy(buf, "FULL");
3630 tfp_sprintf(buff, "%s>%2uWP", buf, posControl.wpPlannerActiveWPIndex);
3631 } else if (posControl.wpPlannerActiveWPIndex){
3632 tfp_sprintf(buff, "PLAN>%2uWP", posControl.waypointCount); // mission planner mision active
3634 #ifdef USE_MULTI_MISSION
3635 else {
3636 if (ARMING_FLAG(ARMED) && !(IS_RC_MODE_ACTIVE(BOXCHANGEMISSION) && posControl.multiMissionCount > 1)){
3637 // Limit field size when Armed, only show selected mission
3638 tfp_sprintf(buff, "M%u ", posControl.loadedMultiMissionIndex);
3639 } else if (posControl.multiMissionCount) {
3640 if (navConfig()->general.waypoint_multi_mission_index != posControl.loadedMultiMissionIndex) {
3641 tfp_sprintf(buff, "M%u/%u>LOAD", navConfig()->general.waypoint_multi_mission_index, posControl.multiMissionCount);
3642 } else {
3643 if (posControl.waypointListValid && posControl.waypointCount > 0) {
3644 tfp_sprintf(buff, "M%u/%u>%2uWP", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
3645 } else {
3646 tfp_sprintf(buff, "M0/%u> 0WP", posControl.multiMissionCount);
3649 } else { // no multi mission loaded - show active WP count from other source
3650 tfp_sprintf(buff, "WP CNT>%2u", posControl.waypointCount);
3653 #endif
3654 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3655 return true;
3658 #ifdef USE_POWER_LIMITS
3659 case OSD_PLIMIT_REMAINING_BURST_TIME:
3660 osdFormatCentiNumber(buff, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3, false);
3661 buff[3] = 'S';
3662 buff[4] = '\0';
3663 break;
3665 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT:
3666 if (currentBatteryProfile->powerLimits.continuousCurrent) {
3667 osdFormatCentiNumber(buff, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3, false);
3668 buff[3] = SYM_AMP;
3669 buff[4] = '\0';
3671 if (powerLimiterIsLimitingCurrent()) {
3672 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3675 break;
3677 #ifdef USE_ADC
3678 case OSD_PLIMIT_ACTIVE_POWER_LIMIT:
3680 if (currentBatteryProfile->powerLimits.continuousPower) {
3681 bool kiloWatt = osdFormatCentiNumber(buff, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3, false);
3682 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3683 buff[4] = '\0';
3685 if (powerLimiterIsLimitingPower()) {
3686 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3689 break;
3691 #endif // USE_ADC
3692 #endif // USE_POWER_LIMITS
3693 case OSD_MULTI_FUNCTION:
3695 // message shown infrequently so only write when needed
3696 static bool clearMultiFunction = true;
3697 elemAttr = osdGetMultiFunctionMessage(buff);
3698 if (buff[0] == 0) {
3699 if (clearMultiFunction) {
3700 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
3701 clearMultiFunction = false;
3703 return true;
3705 clearMultiFunction = true;
3706 break;
3709 default:
3710 return false;
3713 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
3714 return true;
3717 uint8_t osdIncElementIndex(uint8_t elementIndex)
3719 ++elementIndex;
3721 if (elementIndex == OSD_ARTIFICIAL_HORIZON) { // always drawn last so skip
3722 elementIndex++;
3725 #ifndef USE_TEMPERATURE_SENSOR
3726 if (elementIndex == OSD_TEMP_SENSOR_0_TEMPERATURE) {
3727 elementIndex = OSD_ALTITUDE_MSL;
3729 #endif
3731 if (!(feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER))) {
3732 if (elementIndex == OSD_POWER) {
3733 elementIndex = OSD_GPS_LON;
3735 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3736 elementIndex = OSD_LEVEL_PIDS;
3738 #ifdef USE_POWER_LIMITS
3739 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3740 elementIndex = OSD_GLIDESLOPE;
3742 #endif
3745 #ifndef USE_POWER_LIMITS
3746 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3747 elementIndex = OSD_GLIDESLOPE;
3749 #endif
3751 if (!feature(FEATURE_CURRENT_METER)) {
3752 if (elementIndex == OSD_CURRENT_DRAW) {
3753 elementIndex = OSD_GPS_SPEED;
3755 if (elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3756 elementIndex = OSD_BATTERY_REMAINING_PERCENT;
3758 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3759 elementIndex = OSD_TRIP_DIST;
3761 if (elementIndex == OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH) {
3762 elementIndex = OSD_HOME_HEADING_ERROR;
3764 if (elementIndex == OSD_CLIMB_EFFICIENCY) {
3765 elementIndex = OSD_NAV_WP_MULTI_MISSION_INDEX;
3769 if (!STATE(ESC_SENSOR_ENABLED)) {
3770 if (elementIndex == OSD_ESC_RPM) {
3771 elementIndex = OSD_AZIMUTH;
3775 if (!feature(FEATURE_GPS)) {
3776 if (elementIndex == OSD_GPS_HDOP || elementIndex == OSD_TRIP_DIST || elementIndex == OSD_3D_SPEED || elementIndex == OSD_MISSION ||
3777 elementIndex == OSD_AZIMUTH || elementIndex == OSD_BATTERY_REMAINING_CAPACITY || elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3778 elementIndex++;
3780 if (elementIndex == OSD_HEADING_GRAPH && !sensors(SENSOR_MAG)) {
3781 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_WH_DRAWN : OSD_BATTERY_REMAINING_PERCENT;
3783 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3784 elementIndex = OSD_ATTITUDE_PITCH;
3786 if (elementIndex == OSD_GPS_SPEED) {
3787 elementIndex = OSD_ALTITUDE;
3789 if (elementIndex == OSD_GPS_LON) {
3790 elementIndex = sensors(SENSOR_MAG) ? OSD_HEADING : OSD_VARIO;
3792 if (elementIndex == OSD_MAP_NORTH) {
3793 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE : OSD_LEVEL_PIDS;
3795 if (elementIndex == OSD_PLUS_CODE) {
3796 elementIndex = OSD_GFORCE;
3798 if (elementIndex == OSD_GLIDESLOPE) {
3799 elementIndex = OSD_AIR_MAX_SPEED;
3801 if (elementIndex == OSD_GLIDE_RANGE) {
3802 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_CLIMB_EFFICIENCY : OSD_PILOT_NAME;
3804 if (elementIndex == OSD_NAV_WP_MULTI_MISSION_INDEX) {
3805 elementIndex = OSD_PILOT_NAME;
3809 if (!sensors(SENSOR_ACC)) {
3810 if (elementIndex == OSD_CROSSHAIRS) {
3811 elementIndex = OSD_ONTIME;
3813 if (elementIndex == OSD_GFORCE) {
3814 elementIndex = OSD_RC_SOURCE;
3818 if (elementIndex == OSD_ITEM_COUNT) {
3819 elementIndex = 0;
3821 return elementIndex;
3824 void osdDrawNextElement(void)
3826 static uint8_t elementIndex = 0;
3827 // Flag for end of loop, also prevents infinite loop when no elements are enabled
3828 uint8_t index = elementIndex;
3829 do {
3830 elementIndex = osdIncElementIndex(elementIndex);
3831 } while (!osdDrawSingleElement(elementIndex) && index != elementIndex);
3833 // Draw artificial horizon + tracking telemetry last
3834 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
3835 if (osdConfig()->telemetry>0){
3836 osdDisplayTelemetry();
3840 PG_RESET_TEMPLATE(osdConfig_t, osdConfig,
3841 .rssi_alarm = SETTING_OSD_RSSI_ALARM_DEFAULT,
3842 .time_alarm = SETTING_OSD_TIME_ALARM_DEFAULT,
3843 .alt_alarm = SETTING_OSD_ALT_ALARM_DEFAULT,
3844 .dist_alarm = SETTING_OSD_DIST_ALARM_DEFAULT,
3845 .neg_alt_alarm = SETTING_OSD_NEG_ALT_ALARM_DEFAULT,
3846 .current_alarm = SETTING_OSD_CURRENT_ALARM_DEFAULT,
3847 .imu_temp_alarm_min = SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT,
3848 .imu_temp_alarm_max = SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT,
3849 .esc_temp_alarm_min = SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT,
3850 .esc_temp_alarm_max = SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT,
3851 .gforce_alarm = SETTING_OSD_GFORCE_ALARM_DEFAULT,
3852 .gforce_axis_alarm_min = SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT,
3853 .gforce_axis_alarm_max = SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT,
3854 #ifdef USE_BARO
3855 .baro_temp_alarm_min = SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT,
3856 .baro_temp_alarm_max = SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT,
3857 #endif
3858 #ifdef USE_ADSB
3859 .adsb_distance_warning = SETTING_OSD_ADSB_DISTANCE_WARNING_DEFAULT,
3860 .adsb_distance_alert = SETTING_OSD_ADSB_DISTANCE_ALERT_DEFAULT,
3861 .adsb_ignore_plane_above_me_limit = SETTING_OSD_ADSB_IGNORE_PLANE_ABOVE_ME_LIMIT_DEFAULT,
3862 #endif
3863 #ifdef USE_SERIALRX_CRSF
3864 .snr_alarm = SETTING_OSD_SNR_ALARM_DEFAULT,
3865 .crsf_lq_format = SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT,
3866 .link_quality_alarm = SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT,
3867 .rssi_dbm_alarm = SETTING_OSD_RSSI_DBM_ALARM_DEFAULT,
3868 .rssi_dbm_max = SETTING_OSD_RSSI_DBM_MAX_DEFAULT,
3869 .rssi_dbm_min = SETTING_OSD_RSSI_DBM_MIN_DEFAULT,
3870 #endif
3871 #ifdef USE_TEMPERATURE_SENSOR
3872 .temp_label_align = SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT,
3873 #endif
3874 #ifdef USE_PITOT
3875 .airspeed_alarm_min = SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT,
3876 .airspeed_alarm_max = SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT,
3877 #endif
3879 .video_system = SETTING_OSD_VIDEO_SYSTEM_DEFAULT,
3880 .row_shiftdown = SETTING_OSD_ROW_SHIFTDOWN_DEFAULT,
3881 .msp_displayport_fullframe_interval = SETTING_OSD_MSP_DISPLAYPORT_FULLFRAME_INTERVAL_DEFAULT,
3883 .ahi_reverse_roll = SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT,
3884 .ahi_max_pitch = SETTING_OSD_AHI_MAX_PITCH_DEFAULT,
3885 .crosshairs_style = SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT,
3886 .horizon_offset = SETTING_OSD_HORIZON_OFFSET_DEFAULT,
3887 .camera_uptilt = SETTING_OSD_CAMERA_UPTILT_DEFAULT,
3888 .ahi_camera_uptilt_comp = SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT,
3889 .camera_fov_h = SETTING_OSD_CAMERA_FOV_H_DEFAULT,
3890 .camera_fov_v = SETTING_OSD_CAMERA_FOV_V_DEFAULT,
3891 .hud_margin_h = SETTING_OSD_HUD_MARGIN_H_DEFAULT,
3892 .hud_margin_v = SETTING_OSD_HUD_MARGIN_V_DEFAULT,
3893 .hud_homing = SETTING_OSD_HUD_HOMING_DEFAULT,
3894 .hud_homepoint = SETTING_OSD_HUD_HOMEPOINT_DEFAULT,
3895 .hud_radar_disp = SETTING_OSD_HUD_RADAR_DISP_DEFAULT,
3896 .hud_radar_range_min = SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT,
3897 .hud_radar_range_max = SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT,
3898 .hud_radar_alt_difference_display_time = SETTING_OSD_HUD_RADAR_ALT_DIFFERENCE_DISPLAY_TIME_DEFAULT,
3899 .hud_radar_distance_display_time = SETTING_OSD_HUD_RADAR_DISTANCE_DISPLAY_TIME_DEFAULT,
3900 .hud_wp_disp = SETTING_OSD_HUD_WP_DISP_DEFAULT,
3901 .left_sidebar_scroll = SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT,
3902 .right_sidebar_scroll = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT,
3903 .sidebar_scroll_arrows = SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT,
3904 .sidebar_horizontal_offset = SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT,
3905 .left_sidebar_scroll_step = SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT,
3906 .right_sidebar_scroll_step = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT,
3907 .sidebar_height = SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT,
3908 .ahi_pitch_interval = SETTING_OSD_AHI_PITCH_INTERVAL_DEFAULT,
3909 .osd_home_position_arm_screen = SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT,
3910 .pan_servo_index = SETTING_OSD_PAN_SERVO_INDEX_DEFAULT,
3911 .pan_servo_pwm2centideg = SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT,
3912 .pan_servo_offcentre_warning = SETTING_OSD_PAN_SERVO_OFFCENTRE_WARNING_DEFAULT,
3913 .pan_servo_indicator_show_degrees = SETTING_OSD_PAN_SERVO_INDICATOR_SHOW_DEGREES_DEFAULT,
3914 .esc_rpm_precision = SETTING_OSD_ESC_RPM_PRECISION_DEFAULT,
3915 .mAh_precision = SETTING_OSD_MAH_PRECISION_DEFAULT,
3916 .osd_switch_indicator0_name = SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT,
3917 .osd_switch_indicator0_channel = SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNEL_DEFAULT,
3918 .osd_switch_indicator1_name = SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT,
3919 .osd_switch_indicator1_channel = SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNEL_DEFAULT,
3920 .osd_switch_indicator2_name = SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT,
3921 .osd_switch_indicator2_channel = SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNEL_DEFAULT,
3922 .osd_switch_indicator3_name = SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT,
3923 .osd_switch_indicator3_channel = SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNEL_DEFAULT,
3924 .osd_switch_indicators_align_left = SETTING_OSD_SWITCH_INDICATORS_ALIGN_LEFT_DEFAULT,
3925 .system_msg_display_time = SETTING_OSD_SYSTEM_MSG_DISPLAY_TIME_DEFAULT,
3926 .units = SETTING_OSD_UNITS_DEFAULT,
3927 .main_voltage_decimals = SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT,
3928 .use_pilot_logo = SETTING_OSD_USE_PILOT_LOGO_DEFAULT,
3929 .inav_to_pilot_logo_spacing = SETTING_OSD_INAV_TO_PILOT_LOGO_SPACING_DEFAULT,
3930 .arm_screen_display_time = SETTING_OSD_ARM_SCREEN_DISPLAY_TIME_DEFAULT,
3932 #ifdef USE_WIND_ESTIMATOR
3933 .estimations_wind_compensation = SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT,
3934 #endif
3936 .coordinate_digits = SETTING_OSD_COORDINATE_DIGITS_DEFAULT,
3938 .osd_failsafe_switch_layout = SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT,
3940 .plus_code_digits = SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT,
3941 .plus_code_short = SETTING_OSD_PLUS_CODE_SHORT_DEFAULT,
3943 .ahi_width = SETTING_OSD_AHI_WIDTH_DEFAULT,
3944 .ahi_height = SETTING_OSD_AHI_HEIGHT_DEFAULT,
3945 .ahi_vertical_offset = SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT,
3946 .ahi_bordered = SETTING_OSD_AHI_BORDERED_DEFAULT,
3947 .ahi_style = SETTING_OSD_AHI_STYLE_DEFAULT,
3949 .force_grid = SETTING_OSD_FORCE_GRID_DEFAULT,
3951 .stats_energy_unit = SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT,
3952 .stats_page_auto_swap_time = SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT,
3953 .stats_show_metric_efficiency = SETTING_OSD_STATS_SHOW_METRIC_EFFICIENCY_DEFAULT
3956 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t *osdLayoutsConfig)
3958 osdLayoutsConfig->item_pos[0][OSD_ALTITUDE] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG;
3959 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_VOLTAGE] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG;
3960 osdLayoutsConfig->item_pos[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE] = OSD_POS(12, 1);
3962 osdLayoutsConfig->item_pos[0][OSD_RSSI_VALUE] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG;
3963 //line 2
3964 osdLayoutsConfig->item_pos[0][OSD_HOME_DIST] = OSD_POS(1, 1);
3965 osdLayoutsConfig->item_pos[0][OSD_TRIP_DIST] = OSD_POS(1, 2);
3966 osdLayoutsConfig->item_pos[0][OSD_ODOMETER] = OSD_POS(1, 3);
3967 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_CELL_VOLTAGE] = OSD_POS(12, 1);
3968 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE] = OSD_POS(12, 1);
3969 osdLayoutsConfig->item_pos[0][OSD_GPS_SPEED] = OSD_POS(23, 1);
3970 osdLayoutsConfig->item_pos[0][OSD_3D_SPEED] = OSD_POS(23, 1);
3971 osdLayoutsConfig->item_pos[0][OSD_GLIDESLOPE] = OSD_POS(23, 2);
3973 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG;
3974 osdLayoutsConfig->item_pos[0][OSD_SCALED_THROTTLE_POS] = OSD_POS(6, 2);
3975 osdLayoutsConfig->item_pos[0][OSD_HEADING] = OSD_POS(12, 2);
3976 osdLayoutsConfig->item_pos[0][OSD_GROUND_COURSE] = OSD_POS(12, 3);
3977 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ERROR] = OSD_POS(12, 2);
3978 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ADJUSTMENT] = OSD_POS(12, 2);
3979 osdLayoutsConfig->item_pos[0][OSD_CROSS_TRACK_ERROR] = OSD_POS(12, 3);
3980 osdLayoutsConfig->item_pos[0][OSD_HEADING_GRAPH] = OSD_POS(18, 2);
3981 osdLayoutsConfig->item_pos[0][OSD_CURRENT_DRAW] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG;
3982 osdLayoutsConfig->item_pos[0][OSD_MAH_DRAWN] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG;
3983 osdLayoutsConfig->item_pos[0][OSD_WH_DRAWN] = OSD_POS(1, 5);
3984 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_CAPACITY] = OSD_POS(1, 6);
3985 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_PERCENT] = OSD_POS(1, 7);
3986 osdLayoutsConfig->item_pos[0][OSD_POWER_SUPPLY_IMPEDANCE] = OSD_POS(1, 8);
3988 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_MAH_PER_KM] = OSD_POS(1, 5);
3989 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_WH_PER_KM] = OSD_POS(1, 5);
3991 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_ROLL] = OSD_POS(1, 7);
3992 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_PITCH] = OSD_POS(1, 8);
3994 // avoid OSD_VARIO under OSD_CROSSHAIRS
3995 osdLayoutsConfig->item_pos[0][OSD_VARIO] = OSD_POS(23, 5);
3996 // OSD_VARIO_NUM at the right of OSD_VARIO
3997 osdLayoutsConfig->item_pos[0][OSD_VARIO_NUM] = OSD_POS(24, 7);
3998 osdLayoutsConfig->item_pos[0][OSD_HOME_DIR] = OSD_POS(14, 11);
3999 osdLayoutsConfig->item_pos[0][OSD_ARTIFICIAL_HORIZON] = OSD_POS(8, 6);
4000 osdLayoutsConfig->item_pos[0][OSD_HORIZON_SIDEBARS] = OSD_POS(8, 6);
4002 osdLayoutsConfig->item_pos[0][OSD_CRAFT_NAME] = OSD_POS(20, 2);
4003 osdLayoutsConfig->item_pos[0][OSD_PILOT_NAME] = OSD_POS(20, 3);
4004 osdLayoutsConfig->item_pos[0][OSD_PILOT_LOGO] = OSD_POS(20, 3);
4005 osdLayoutsConfig->item_pos[0][OSD_VTX_CHANNEL] = OSD_POS(8, 6);
4007 #ifdef USE_SERIALRX_CRSF
4008 osdLayoutsConfig->item_pos[0][OSD_CRSF_RSSI_DBM] = OSD_POS(23, 12);
4009 osdLayoutsConfig->item_pos[0][OSD_CRSF_LQ] = OSD_POS(23, 11);
4010 osdLayoutsConfig->item_pos[0][OSD_CRSF_SNR_DB] = OSD_POS(24, 9);
4011 osdLayoutsConfig->item_pos[0][OSD_CRSF_TX_POWER] = OSD_POS(24, 10);
4012 #endif
4014 osdLayoutsConfig->item_pos[0][OSD_ONTIME] = OSD_POS(23, 8);
4015 osdLayoutsConfig->item_pos[0][OSD_FLYTIME] = OSD_POS(23, 9);
4016 osdLayoutsConfig->item_pos[0][OSD_ONTIME_FLYTIME] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG;
4017 osdLayoutsConfig->item_pos[0][OSD_RTC_TIME] = OSD_POS(23, 12);
4018 osdLayoutsConfig->item_pos[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH] = OSD_POS(23, 7);
4019 osdLayoutsConfig->item_pos[0][OSD_REMAINING_DISTANCE_BEFORE_RTH] = OSD_POS(23, 6);
4021 osdLayoutsConfig->item_pos[0][OSD_MISSION] = OSD_POS(0, 10);
4022 osdLayoutsConfig->item_pos[0][OSD_GPS_SATS] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG;
4023 osdLayoutsConfig->item_pos[0][OSD_GPS_HDOP] = OSD_POS(0, 10);
4025 osdLayoutsConfig->item_pos[0][OSD_GPS_LAT] = OSD_POS(0, 12);
4026 // Put this on top of the latitude, since it's very unlikely
4027 // that users will want to use both at the same time.
4028 osdLayoutsConfig->item_pos[0][OSD_PLUS_CODE] = OSD_POS(0, 12);
4029 osdLayoutsConfig->item_pos[0][OSD_FLYMODE] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG;
4030 osdLayoutsConfig->item_pos[0][OSD_GPS_LON] = OSD_POS(18, 12);
4032 osdLayoutsConfig->item_pos[0][OSD_AZIMUTH] = OSD_POS(2, 12);
4034 osdLayoutsConfig->item_pos[0][OSD_ROLL_PIDS] = OSD_POS(2, 10);
4035 osdLayoutsConfig->item_pos[0][OSD_PITCH_PIDS] = OSD_POS(2, 11);
4036 osdLayoutsConfig->item_pos[0][OSD_YAW_PIDS] = OSD_POS(2, 12);
4037 osdLayoutsConfig->item_pos[0][OSD_LEVEL_PIDS] = OSD_POS(2, 12);
4038 osdLayoutsConfig->item_pos[0][OSD_POS_XY_PIDS] = OSD_POS(2, 12);
4039 osdLayoutsConfig->item_pos[0][OSD_POS_Z_PIDS] = OSD_POS(2, 12);
4040 osdLayoutsConfig->item_pos[0][OSD_VEL_XY_PIDS] = OSD_POS(2, 12);
4041 osdLayoutsConfig->item_pos[0][OSD_VEL_Z_PIDS] = OSD_POS(2, 12);
4042 osdLayoutsConfig->item_pos[0][OSD_HEADING_P] = OSD_POS(2, 12);
4043 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_ROLL] = OSD_POS(2, 10);
4044 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_PITCH] = OSD_POS(2, 11);
4045 osdLayoutsConfig->item_pos[0][OSD_RC_EXPO] = OSD_POS(2, 12);
4046 osdLayoutsConfig->item_pos[0][OSD_RC_YAW_EXPO] = OSD_POS(2, 12);
4047 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_EXPO] = OSD_POS(2, 12);
4048 osdLayoutsConfig->item_pos[0][OSD_PITCH_RATE] = OSD_POS(2, 12);
4049 osdLayoutsConfig->item_pos[0][OSD_ROLL_RATE] = OSD_POS(2, 12);
4050 osdLayoutsConfig->item_pos[0][OSD_YAW_RATE] = OSD_POS(2, 12);
4051 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_EXPO] = OSD_POS(2, 12);
4052 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_YAW_EXPO] = OSD_POS(2, 12);
4053 osdLayoutsConfig->item_pos[0][OSD_MANUAL_PITCH_RATE] = OSD_POS(2, 12);
4054 osdLayoutsConfig->item_pos[0][OSD_MANUAL_ROLL_RATE] = OSD_POS(2, 12);
4055 osdLayoutsConfig->item_pos[0][OSD_MANUAL_YAW_RATE] = OSD_POS(2, 12);
4056 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_CRUISE_THR] = OSD_POS(2, 12);
4057 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_PITCH2THR] = OSD_POS(2, 12);
4058 osdLayoutsConfig->item_pos[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE] = OSD_POS(2, 12);
4059 osdLayoutsConfig->item_pos[0][OSD_FW_ALT_PID_OUTPUTS] = OSD_POS(2, 12);
4060 osdLayoutsConfig->item_pos[0][OSD_FW_POS_PID_OUTPUTS] = OSD_POS(2, 12);
4061 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_X_PID_OUTPUTS] = OSD_POS(2, 12);
4062 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Y_PID_OUTPUTS] = OSD_POS(2, 12);
4063 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Z_PID_OUTPUTS] = OSD_POS(2, 12);
4064 osdLayoutsConfig->item_pos[0][OSD_MC_POS_XYZ_P_OUTPUTS] = OSD_POS(2, 12);
4066 osdLayoutsConfig->item_pos[0][OSD_POWER] = OSD_POS(15, 1);
4068 osdLayoutsConfig->item_pos[0][OSD_IMU_TEMPERATURE] = OSD_POS(19, 2);
4069 osdLayoutsConfig->item_pos[0][OSD_BARO_TEMPERATURE] = OSD_POS(19, 3);
4070 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_0_TEMPERATURE] = OSD_POS(19, 4);
4071 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_1_TEMPERATURE] = OSD_POS(19, 5);
4072 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_2_TEMPERATURE] = OSD_POS(19, 6);
4073 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_3_TEMPERATURE] = OSD_POS(19, 7);
4074 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_4_TEMPERATURE] = OSD_POS(19, 8);
4075 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_5_TEMPERATURE] = OSD_POS(19, 9);
4076 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_6_TEMPERATURE] = OSD_POS(19, 10);
4077 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_7_TEMPERATURE] = OSD_POS(19, 11);
4079 osdLayoutsConfig->item_pos[0][OSD_AIR_SPEED] = OSD_POS(3, 5);
4080 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_HORIZONTAL] = OSD_POS(3, 6);
4081 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_VERTICAL] = OSD_POS(3, 7);
4083 osdLayoutsConfig->item_pos[0][OSD_GFORCE] = OSD_POS(12, 4);
4084 osdLayoutsConfig->item_pos[0][OSD_GFORCE_X] = OSD_POS(12, 5);
4085 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Y] = OSD_POS(12, 6);
4086 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Z] = OSD_POS(12, 7);
4088 osdLayoutsConfig->item_pos[0][OSD_VTX_POWER] = OSD_POS(3, 5);
4090 osdLayoutsConfig->item_pos[0][OSD_GVAR_0] = OSD_POS(1, 1);
4091 osdLayoutsConfig->item_pos[0][OSD_GVAR_1] = OSD_POS(1, 2);
4092 osdLayoutsConfig->item_pos[0][OSD_GVAR_2] = OSD_POS(1, 3);
4093 osdLayoutsConfig->item_pos[0][OSD_GVAR_3] = OSD_POS(1, 4);
4095 osdLayoutsConfig->item_pos[0][OSD_MULTI_FUNCTION] = OSD_POS(1, 4);
4097 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_0] = OSD_POS(2, 7);
4098 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_1] = OSD_POS(2, 8);
4099 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_2] = OSD_POS(2, 9);
4100 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_3] = OSD_POS(2, 10);
4102 osdLayoutsConfig->item_pos[0][OSD_ADSB_WARNING] = OSD_POS(2, 7);
4103 osdLayoutsConfig->item_pos[0][OSD_ADSB_INFO] = OSD_POS(2, 8);
4104 #if defined(USE_ESC_SENSOR)
4105 osdLayoutsConfig->item_pos[0][OSD_ESC_RPM] = OSD_POS(1, 2);
4106 osdLayoutsConfig->item_pos[0][OSD_ESC_TEMPERATURE] = OSD_POS(1, 3);
4107 #endif
4109 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
4110 osdLayoutsConfig->item_pos[0][OSD_RC_SOURCE] = OSD_POS(3, 4);
4111 #endif
4113 #ifdef USE_POWER_LIMITS
4114 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_REMAINING_BURST_TIME] = OSD_POS(3, 4);
4115 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT] = OSD_POS(3, 5);
4116 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT] = OSD_POS(3, 6);
4117 #endif
4119 #ifdef USE_BLACKBOX
4120 osdLayoutsConfig->item_pos[0][OSD_BLACKBOX] = OSD_POS(2, 10);
4121 #endif
4123 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
4124 osdLayoutsConfig->item_pos[0][OSD_MESSAGES] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG;
4126 for (unsigned ii = 1; ii < OSD_LAYOUT_COUNT; ii++) {
4127 for (unsigned jj = 0; jj < ARRAYLEN(osdLayoutsConfig->item_pos[0]); jj++) {
4128 osdLayoutsConfig->item_pos[ii][jj] = osdLayoutsConfig->item_pos[0][jj] & ~OSD_VISIBLE_FLAG;
4134 * @brief Draws the INAV and/or pilot logos on the display
4136 * @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
4137 * @param row The row number to start drawing the logos. If not singular, both logos are drawn on the same rows.
4138 * @return uint8_t The row number after the logo(s).
4140 uint8_t drawLogos(bool singular, uint8_t row) {
4141 uint8_t logoRow = row;
4142 uint8_t logoColOffset = 0;
4143 bool usePilotLogo = (osdConfig()->use_pilot_logo && osdDisplayIsHD());
4145 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is in use, the pilot logo cannot be used, due to font issues.
4146 if (isDJICompatibleVideoSystem(osdConfig()))
4147 usePilotLogo = false;
4148 #endif
4150 uint8_t logoSpacing = osdConfig()->inav_to_pilot_logo_spacing;
4152 if (logoSpacing > 0 && ((osdDisplayPort->cols % 2) != (logoSpacing % 2))) {
4153 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
4156 // Draw Logo(s)
4157 if (usePilotLogo && !singular) {
4158 logoColOffset = ((osdDisplayPort->cols - (SYM_LOGO_WIDTH * 2)) - logoSpacing) / 2;
4159 } else {
4160 logoColOffset = floorf((osdDisplayPort->cols - SYM_LOGO_WIDTH) / 2.0f);
4163 // Draw INAV logo
4164 if ((singular && !usePilotLogo) || !singular) {
4165 unsigned logo_c = SYM_LOGO_START;
4166 uint8_t logo_x = logoColOffset;
4167 for (uint8_t lRow = 0; lRow < SYM_LOGO_HEIGHT; lRow++) {
4168 for (uint8_t lCol = 0; lCol < SYM_LOGO_WIDTH; lCol++) {
4169 displayWriteChar(osdDisplayPort, logo_x + lCol, logoRow, logo_c++);
4171 logoRow++;
4175 // Draw the pilot logo
4176 if (usePilotLogo) {
4177 unsigned logo_c = SYM_PILOT_LOGO_LRG_START;
4178 uint8_t logo_x = 0;
4179 logoRow = row;
4180 if (singular) {
4181 logo_x = logoColOffset;
4182 } else {
4183 logo_x = logoColOffset + SYM_LOGO_WIDTH + logoSpacing;
4186 for (uint8_t lRow = 0; lRow < SYM_LOGO_HEIGHT; lRow++) {
4187 for (uint8_t lCol = 0; lCol < SYM_LOGO_WIDTH; lCol++) {
4188 displayWriteChar(osdDisplayPort, logo_x + lCol, logoRow, logo_c++);
4190 logoRow++;
4194 return logoRow;
4197 #ifdef USE_STATS
4198 uint8_t drawStat_Stats(uint8_t statNameX, uint8_t row, uint8_t statValueX, bool isBootStats)
4200 uint8_t buffLen = 0;
4201 char string_buffer[osdDisplayPort->cols - statValueX];
4203 if (statsConfig()->stats_enabled) {
4204 if (isBootStats)
4205 displayWrite(osdDisplayPort, statNameX, row, "ODOMETER:");
4206 else
4207 displayWrite(osdDisplayPort, statNameX, row, "ODOMETER");
4209 switch (osdConfig()->units) {
4210 case OSD_UNIT_UK:
4211 FALLTHROUGH;
4212 case OSD_UNIT_IMPERIAL:
4213 if (isBootStats) {
4214 tfp_sprintf(string_buffer, "%5d", (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_MILE));
4215 buffLen = 5;
4216 } else {
4217 uint16_t statTotalDist = (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_MILE);
4218 tfp_sprintf(string_buffer, ": %d", statTotalDist);
4219 buffLen = 3 + sizeof(statTotalDist);
4222 string_buffer[buffLen++] = SYM_MI;
4223 break;
4224 default:
4225 case OSD_UNIT_GA:
4226 if (isBootStats) {
4227 tfp_sprintf(string_buffer, "%5d", (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE));
4228 buffLen = 5;
4229 } else {
4230 uint16_t statTotalDist = (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE);
4231 tfp_sprintf(string_buffer, ": %d", statTotalDist);
4232 buffLen = 3 + sizeof(statTotalDist);
4235 string_buffer[buffLen++] = SYM_NM;
4236 break;
4237 case OSD_UNIT_METRIC_MPH:
4238 FALLTHROUGH;
4239 case OSD_UNIT_METRIC:
4240 if (isBootStats) {
4241 tfp_sprintf(string_buffer, "%5d", (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER));
4242 buffLen = 5;
4243 } else {
4244 uint16_t statTotalDist = (uint16_t)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER);
4245 tfp_sprintf(string_buffer, ": %d", statTotalDist);
4246 buffLen = 3 + sizeof(statTotalDist);
4249 string_buffer[buffLen++] = SYM_KM;
4250 break;
4252 string_buffer[buffLen] = '\0';
4253 displayWrite(osdDisplayPort, statValueX-(isBootStats ? 5 : 0), row, string_buffer);
4255 if (isBootStats)
4256 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL TIME:");
4257 else
4258 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL TIME");
4260 uint32_t tot_mins = statsConfig()->stats_total_time / 60;
4261 if (isBootStats)
4262 tfp_sprintf(string_buffer, "%d:%02dH:M%c", (int)(tot_mins / 60), (int)(tot_mins % 60), '\0');
4263 else
4264 tfp_sprintf(string_buffer, ": %d:%02d H:M%c", (int)(tot_mins / 60), (int)(tot_mins % 60), '\0');
4266 displayWrite(osdDisplayPort, statValueX-(isBootStats ? 7 : 0), row, string_buffer);
4268 #ifdef USE_ADC
4269 if (feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER) && statsConfig()->stats_total_energy) {
4270 uint8_t buffOffset = 0;
4271 if (isBootStats)
4272 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL ENERGY:");
4273 else {
4274 displayWrite(osdDisplayPort, statNameX, ++row, "TOTAL ENERGY");
4275 string_buffer[0] = ':';
4276 buffOffset = 2;
4279 osdFormatCentiNumber(string_buffer + buffOffset, statsConfig()->stats_total_energy / 10, 0, 2, 0, 6, true);
4280 displayWrite(osdDisplayPort, statValueX - (isBootStats ? 6 : 0), row, string_buffer);
4281 displayWriteChar(osdDisplayPort, statValueX + (isBootStats ? 0 : 8), row, SYM_WH);
4283 char avgEffBuff[osdDisplayPort->cols - statValueX];
4285 for (uint8_t i = 0; i < osdDisplayPort->cols - statValueX; i++) {
4286 avgEffBuff[i] = '\0';
4287 string_buffer[i] = '\0';
4290 if (statsConfig()->stats_total_dist) {
4291 if (isBootStats)
4292 displayWrite(osdDisplayPort, statNameX, ++row, "AVG EFFICIENCY:");
4293 else {
4294 displayWrite(osdDisplayPort, statNameX, ++row, "AVG EFFICIENCY");
4295 strcat(avgEffBuff, ": ");
4298 float_t avg_efficiency = MWH_TO_WH(statsConfig()->stats_total_energy) / METERS_TO_KILOMETERS(statsConfig()->stats_total_dist); // Wh/km
4299 switch (osdConfig()->units) {
4300 case OSD_UNIT_UK:
4301 FALLTHROUGH;
4302 case OSD_UNIT_IMPERIAL:
4303 osdFormatCentiNumber(string_buffer, (int32_t)(avg_efficiency * METERS_PER_MILE / 10), 0, 2, 2, 4, false);
4304 string_buffer[4] = SYM_WH_MI;
4305 break;
4306 case OSD_UNIT_GA:
4307 osdFormatCentiNumber(string_buffer, (int32_t)(avg_efficiency * METERS_PER_NAUTICALMILE / 10), 0, 2, 2, 4, false);
4308 string_buffer[4] = SYM_WH_NM;
4309 break;
4310 default:
4311 case OSD_UNIT_METRIC_MPH:
4312 FALLTHROUGH;
4313 case OSD_UNIT_METRIC:
4314 osdFormatCentiNumber(string_buffer, (int32_t)(avg_efficiency * 100), 0, 2, 2, 4, false);
4315 string_buffer[4] = SYM_WH_KM;
4316 break;
4319 if (isBootStats)
4320 strcat(avgEffBuff, string_buffer);
4321 else
4322 strcat(avgEffBuff, osdFormatTrimWhiteSpace(string_buffer));
4323 } else {
4324 strcat(avgEffBuff, "----");
4327 displayWrite(osdDisplayPort, statValueX-(isBootStats ? 4 : 0), row++, avgEffBuff);
4329 #endif // USE_ADC
4331 return row;
4334 uint8_t drawStats(uint8_t row)
4336 uint8_t statNameX = (osdDisplayPort->cols - 22) / 2;
4337 uint8_t statValueX = statNameX + 21;
4339 return drawStat_Stats(statNameX, row, statValueX, true);
4341 #endif // USE STATS
4343 static void osdSetNextRefreshIn(uint32_t timeMs)
4345 resumeRefreshAt = micros() + timeMs * 1000;
4346 refreshWaitForResumeCmdRelease = true;
4349 static void osdCompleteAsyncInitialization(void)
4351 if (!displayIsReady(osdDisplayPort)) {
4352 // Update the display.
4353 // XXX: Rename displayDrawScreen() and associated functions
4354 // to displayUpdate()
4355 displayDrawScreen(osdDisplayPort);
4356 return;
4359 osdDisplayIsReady = true;
4361 #if defined(USE_CANVAS)
4362 if (osdConfig()->force_grid) {
4363 osdDisplayHasCanvas = false;
4364 } else {
4365 osdDisplayHasCanvas = displayGetCanvas(&osdCanvas, osdDisplayPort);
4367 #endif
4369 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4370 displayClearScreen(osdDisplayPort);
4372 uint8_t y = 1;
4373 displayFontMetadata_t metadata;
4374 bool fontHasMetadata = displayGetFontMetadata(&metadata, osdDisplayPort);
4375 LOG_DEBUG(OSD, "Font metadata version %s: %u (%u chars)",
4376 fontHasMetadata ? "Y" : "N", metadata.version, metadata.charCount);
4378 if (fontHasMetadata && metadata.charCount > 256) {
4379 hasExtendedFont = true;
4381 y = drawLogos(false, y);
4382 y++;
4383 } else if (!fontHasMetadata) {
4384 const char *m = "INVALID FONT";
4385 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
4388 if (fontHasMetadata && metadata.version < OSD_MIN_FONT_VERSION) {
4389 const char *m = "INVALID FONT VERSION";
4390 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
4393 char string_buffer[30];
4394 tfp_sprintf(string_buffer, "INAV VERSION: %s", FC_VERSION_STRING);
4395 uint8_t xPos = (osdDisplayPort->cols - 19) / 2; // Automatically centre, regardless of resolution. In the case of odd number screens, bias to the left.
4396 displayWrite(osdDisplayPort, xPos, y++, string_buffer);
4397 #ifdef USE_CMS
4398 displayWrite(osdDisplayPort, xPos+2, y++, CMS_STARTUP_HELP_TEXT1);
4399 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT2);
4400 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT3);
4401 #endif
4403 #ifdef USE_STATS
4404 y = drawStats(++y);
4405 #endif
4407 displayCommitTransaction(osdDisplayPort);
4408 displayResync(osdDisplayPort);
4409 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME);
4412 void osdInit(displayPort_t *osdDisplayPortToUse)
4414 if (!osdDisplayPortToUse)
4415 return;
4417 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(63,63));
4419 osdDisplayPort = osdDisplayPortToUse;
4421 #ifdef USE_CMS
4422 cmsDisplayPortRegister(osdDisplayPort);
4423 #endif
4425 armState = ARMING_FLAG(ARMED);
4426 osdCompleteAsyncInitialization();
4429 static void osdResetStats(void)
4431 // Reset internal OSD stats
4432 stats.max_distance = 0;
4433 stats.max_current = 0;
4434 stats.max_power = 0;
4435 stats.max_speed = 0;
4436 stats.max_3D_speed = 0;
4437 stats.max_air_speed = 0;
4438 stats.min_voltage = 12000;
4439 stats.min_rssi = 99;
4440 stats.min_lq = 300;
4441 stats.min_rssi_dbm = 0;
4442 stats.max_altitude = 0;
4443 stats.min_sats = 255;
4444 stats.max_sats = 0;
4445 stats.min_esc_temp = 300;
4446 stats.max_esc_temp = 0;
4447 stats.flightStartMAh = getMAhDrawn();
4448 stats.flightStartMWh = getMWhDrawn();
4450 // Reset external stats
4451 posControl.totalTripDistance = 0.0f;
4452 resetFlightTime();
4453 resetGForceStats();
4456 static void osdUpdateStats(void)
4458 int32_t value;
4460 if (feature(FEATURE_GPS)) {
4461 value = osdGet3DSpeed();
4462 const float airspeed_estimate = getAirspeedEstimate();
4464 if (stats.max_3D_speed < value)
4465 stats.max_3D_speed = value;
4467 if (stats.max_speed < gpsSol.groundSpeed)
4468 stats.max_speed = gpsSol.groundSpeed;
4470 if (stats.max_air_speed < airspeed_estimate)
4471 stats.max_air_speed = airspeed_estimate;
4473 if (stats.max_distance < GPS_distanceToHome)
4474 stats.max_distance = GPS_distanceToHome;
4476 if (stats.min_sats > gpsSol.numSat)
4477 stats.min_sats = gpsSol.numSat;
4479 if (stats.max_sats < gpsSol.numSat)
4480 stats.max_sats = gpsSol.numSat;
4482 #if defined(USE_ESC_SENSOR)
4483 if (STATE(ESC_SENSOR_ENABLED)) {
4484 escSensorData_t * escSensor = escSensorGetData();
4485 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
4487 if (escTemperatureValid) {
4488 if (stats.min_esc_temp > escSensor->temperature)
4489 stats.min_esc_temp = escSensor->temperature;
4491 if (stats.max_esc_temp < escSensor->temperature)
4492 stats.max_esc_temp = escSensor->temperature;
4495 #endif
4497 value = getBatteryVoltage();
4498 if (stats.min_voltage > value)
4499 stats.min_voltage = value;
4501 value = abs(getAmperage());
4502 if (stats.max_current < value)
4503 stats.max_current = value;
4505 value = labs(getPower());
4506 if (stats.max_power < value)
4507 stats.max_power = value;
4509 value = osdConvertRSSI();
4510 if (stats.min_rssi > value)
4511 stats.min_rssi = value;
4513 value = osdGetCrsfLQ();
4514 if (stats.min_lq > value)
4515 stats.min_lq = value;
4517 if (!failsafeIsReceivingRxData())
4518 stats.min_lq = 0;
4520 value = osdGetCrsfdBm();
4521 if (stats.min_rssi_dbm > value)
4522 stats.min_rssi_dbm = value;
4524 stats.max_altitude = MAX(stats.max_altitude, osdGetAltitude());
4527 uint8_t drawStat_FlightTime(uint8_t col, uint8_t row, uint8_t statValX)
4529 char buff[12];
4530 displayWrite(osdDisplayPort, col, row, "FLIGHT TIME");
4531 uint16_t flySeconds = getFlightTime();
4532 uint16_t flyMinutes = flySeconds / 60;
4533 flySeconds %= 60;
4534 uint16_t flyHours = flyMinutes / 60;
4535 flyMinutes %= 60;
4536 tfp_sprintf(buff, ": %02u:%02u:%02u", flyHours, flyMinutes, flySeconds);
4537 displayWrite(osdDisplayPort, statValX, row++, buff);
4539 return row;
4542 uint8_t drawStat_FlightDistance(uint8_t col, uint8_t row, uint8_t statValX)
4544 char buff[12];
4546 displayWrite(osdDisplayPort, col, row, "FLIGHT DISTANCE");
4547 tfp_sprintf(buff, ": ");
4548 osdFormatDistanceStr(buff + 2, getTotalTravelDistance());
4549 displayWrite(osdDisplayPort, statValX, row++, buff);
4551 return row;
4554 uint8_t drawStat_MaxDistanceFromHome(uint8_t col, uint8_t row, uint8_t statValX)
4556 char buff[12];
4557 uint8_t valueXOffset = 0;
4558 if (!osdDisplayIsHD()) {
4559 displayWrite(osdDisplayPort, col, row, "DISTANCE FROM ");
4560 valueXOffset = 14;
4561 } else {
4562 displayWrite(osdDisplayPort, col, row, "MAX DISTANCE FROM ");
4563 valueXOffset = 18;
4565 displayWriteChar(osdDisplayPort, col + valueXOffset, row, SYM_HOME);
4566 tfp_sprintf(buff, ": ");
4567 osdFormatDistanceStr(buff + 2, stats.max_distance * 100);
4568 displayWrite(osdDisplayPort, statValX, row++, buff);
4570 return row;
4573 uint8_t drawStat_Speed(uint8_t col, uint8_t row, uint8_t statValX)
4575 char buff[12];
4576 char buff2[12];
4577 uint8_t multiValueXOffset = 0;
4579 displayWrite(osdDisplayPort, col, row, "MAX/AVG SPEED");
4581 osdFormatVelocityStr(buff2, stats.max_3D_speed, true, false);
4582 tfp_sprintf(buff, ": %s/", osdFormatTrimWhiteSpace(buff2));
4583 multiValueXOffset = strlen(buff);
4584 displayWrite(osdDisplayPort, statValX, row, buff);
4586 osdGenerateAverageVelocityStr(buff2);
4587 displayWrite(osdDisplayPort, statValX + multiValueXOffset, row++, osdFormatTrimWhiteSpace(buff2));
4589 return row;
4592 uint8_t drawStat_MaximumAltitude(uint8_t col, uint8_t row, uint8_t statValX)
4594 char buff[12];
4595 displayWrite(osdDisplayPort, col, row, "MAX ALTITUDE");
4596 tfp_sprintf(buff, ": ");
4597 osdFormatAltitudeStr(buff + 2, stats.max_altitude);
4598 displayWrite(osdDisplayPort, statValX, row++, buff);
4600 return row;
4603 uint8_t drawStat_BatteryVoltage(uint8_t col, uint8_t row, uint8_t statValX)
4605 char buff[12];
4606 uint8_t multiValueXOffset = 0;
4607 if (!osdDisplayIsHD())
4608 displayWrite(osdDisplayPort, col, row, "MIN VOLTS P/C");
4609 else
4610 displayWrite(osdDisplayPort, col, row, "MIN VOLTS PACK/CELL");
4612 // Pack voltage
4613 tfp_sprintf(buff, ": ");
4614 osdFormatCentiNumber(buff + 2, stats.min_voltage, 0, osdConfig()->main_voltage_decimals, 0, osdConfig()->main_voltage_decimals + 2, false);
4615 strcat(osdFormatTrimWhiteSpace(buff), "/");
4616 multiValueXOffset = strlen(buff);
4617 // AverageCell
4618 osdFormatCentiNumber(buff + multiValueXOffset, stats.min_voltage / getBatteryCellCount(), 0, 2, 0, 3, false);
4619 tfp_sprintf(buff + strlen(buff), "%c", SYM_VOLT);
4621 displayWrite(osdDisplayPort, statValX, row++, buff);
4623 return row;
4626 uint8_t drawStat_MaximumPowerAndCurrent(uint8_t col, uint8_t row, uint8_t statValX)
4628 char buff[12];
4629 char outBuff[12];
4630 tfp_sprintf(outBuff, ": ");
4631 osdFormatCentiNumber(buff, stats.max_current, 0, 2, 0, 3, false);
4632 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4633 strcat(outBuff, "/");
4634 bool kiloWatt = osdFormatCentiNumber(buff, stats.max_power, 1000, 2, 2, 3, false);
4635 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4636 displayWrite(osdDisplayPort, statValX, row, outBuff);
4638 if (kiloWatt)
4639 displayWrite(osdDisplayPort, col, row, "MAX AMPS/K WATTS");
4640 else
4641 displayWrite(osdDisplayPort, col, row, "MAX AMPS/WATTS");
4643 return ++row;
4646 uint8_t drawStat_UsedEnergy(uint8_t col, uint8_t row, uint8_t statValX)
4648 char buff[12];
4650 if (osdDisplayIsHD())
4651 displayWrite(osdDisplayPort, col, row, "USED ENERGY FLT/TOT");
4652 else
4653 displayWrite(osdDisplayPort, col, row, "USED ENERGY F/T");
4654 tfp_sprintf(buff, ": ");
4655 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4656 tfp_sprintf(buff + 2, "%d/%d%c", (int)(getMAhDrawn() - stats.flightStartMAh), (int)getMAhDrawn(), SYM_MAH);
4657 } else {
4658 char preBuff[12];
4659 osdFormatCentiNumber(preBuff, (getMWhDrawn() - stats.flightStartMWh) / 10, 0, 2, 0, 3, false);
4660 strcat(buff, osdFormatTrimWhiteSpace(preBuff));
4661 strcat(buff, "/");
4662 osdFormatCentiNumber(preBuff, getMWhDrawn() / 10, 0, 2, 0, 3, false);
4663 strcat(buff, osdFormatTrimWhiteSpace(preBuff));
4664 tfp_sprintf(buff + strlen(buff), "%s%c", buff, SYM_WH);
4666 displayWrite(osdDisplayPort, statValX, row++, buff);
4668 return row;
4671 uint8_t drawStat_AverageEfficiency(uint8_t col, uint8_t row, uint8_t statValX, bool forceMetric)
4673 char buff[15];
4674 char outBuff[15];
4675 int32_t totalDistance = getTotalTravelDistance();
4676 bool moreThanAh = false;
4677 bool efficiencyValid = totalDistance >= 10000;
4679 if (osdDisplayIsHD())
4680 displayWrite(osdDisplayPort, col, row, "AVG EFFICIENCY FLT/TOT");
4681 else
4682 displayWrite(osdDisplayPort, col, row, "AV EFFICIENCY F/T");
4684 tfp_sprintf(outBuff, ": ");
4685 uint8_t digits = 3U; // Total number of digits (including decimal point)
4686 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
4687 if (isDJICompatibleVideoSystem(osdConfig())) {
4688 // Add one digit so no switch to scaled decimal occurs above 99
4689 digits = 4U;
4691 #endif
4692 if (!forceMetric) {
4693 switch (osdConfig()->units) {
4694 case OSD_UNIT_UK:
4695 FALLTHROUGH;
4696 case OSD_UNIT_IMPERIAL:
4697 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4698 if (efficiencyValid) {
4699 moreThanAh = osdFormatCentiNumber(buff, (int32_t)((getMAhDrawn() - stats.flightStartMAh) * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, digits, false);
4700 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4701 if (osdDisplayIsHD()) {
4702 if (!moreThanAh)
4703 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_MI_0, SYM_MAH_MI_1);
4704 else
4705 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_MI);
4707 moreThanAh = false;
4710 strcat(outBuff, "/");
4711 moreThanAh = moreThanAh || osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, digits, false);
4712 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4714 if (!moreThanAh)
4715 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_MI_0, SYM_MAH_MI_1);
4716 else
4717 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_MI);
4718 } else {
4719 tfp_sprintf(outBuff + strlen(outBuff), "---/---%c%c", SYM_MAH_MI_0, SYM_MAH_MI_1);
4721 } else {
4722 if (efficiencyValid) {
4723 osdFormatCentiNumber(buff, (int32_t)((getMWhDrawn() - stats.flightStartMWh) * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, digits, false);
4724 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4725 strcat(outBuff, "/");
4726 osdFormatCentiNumber(buff + strlen(buff), (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, digits, false);
4727 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4728 } else {
4729 strcat(outBuff, "---/---");
4731 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_WH_MI);
4733 break;
4734 case OSD_UNIT_GA:
4735 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4736 if (efficiencyValid) {
4737 moreThanAh = osdFormatCentiNumber(buff, (int32_t)((getMAhDrawn()-stats.flightStartMAh) * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, digits, false);
4738 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4739 if (osdDisplayIsHD()) {
4740 if (!moreThanAh)
4741 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_NM_0, SYM_MAH_NM_1);
4742 else
4743 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_NM);
4745 moreThanAh = false;
4748 strcat(outBuff, "/");
4749 moreThanAh = moreThanAh || osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, digits, false);
4750 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4751 if (!moreThanAh) {
4752 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_NM_0, SYM_MAH_NM_1);
4753 } else {
4754 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_NM);
4756 } else {
4757 tfp_sprintf(outBuff + strlen(outBuff), "---/---%c%c", SYM_MAH_NM_0, SYM_MAH_NM_1);
4759 } else {
4760 if (efficiencyValid) {
4761 osdFormatCentiNumber(buff, (int32_t)((getMWhDrawn()-stats.flightStartMWh) * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, digits, false);
4762 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4763 strcat(outBuff, "/");
4764 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, digits, false);
4765 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4766 } else {
4767 strcat(outBuff, "---/---");
4769 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_WH_NM);
4771 break;
4772 case OSD_UNIT_METRIC_MPH:
4773 case OSD_UNIT_METRIC:
4774 forceMetric = true;
4775 break;
4779 if (forceMetric) {
4780 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4781 if (efficiencyValid) {
4782 moreThanAh = osdFormatCentiNumber(buff, (int32_t)((getMAhDrawn() - stats.flightStartMAh) * 10000000.0f / totalDistance), 1000, 0, 2, digits, false);
4783 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4784 if (osdDisplayIsHD()) {
4785 if (!moreThanAh)
4786 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_KM_0, SYM_MAH_KM_1);
4787 else
4788 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_KM);
4790 moreThanAh = false;
4793 strcat(outBuff, "/");
4794 moreThanAh = moreThanAh || osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000000.0f / totalDistance), 1000, 0, 2, digits, false);
4795 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4796 if (!moreThanAh) {
4797 tfp_sprintf(outBuff + strlen(outBuff), "%c%c", SYM_MAH_KM_0, SYM_MAH_KM_1);
4798 } else {
4799 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_AH_KM);
4801 } else {
4802 tfp_sprintf(outBuff + strlen(outBuff), "---/---%c%c", SYM_MAH_KM_0, SYM_MAH_KM_1);
4804 } else {
4805 if (efficiencyValid) {
4806 osdFormatCentiNumber(buff, (int32_t)((getMWhDrawn() - stats.flightStartMWh) * 10000.0f / totalDistance), 0, 2, 0, digits, false);
4807 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4808 strcat(outBuff, "/");
4809 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10000.0f / totalDistance), 0, 2, 0, digits, false);
4810 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4811 } else {
4812 strcat(outBuff, "---/---");
4814 tfp_sprintf(outBuff + strlen(outBuff), "%c", SYM_WH_KM);
4818 tfp_sprintf(outBuff + strlen(outBuff), "%c", '\0');
4819 displayWrite(osdDisplayPort, statValX, row++, outBuff);
4821 return row;
4824 uint8_t drawStat_RXStats(uint8_t col, uint8_t row, uint8_t statValX)
4826 char buff[20];
4827 uint8_t multiValueXOffset = 0;
4829 tfp_sprintf(buff, "MIN RSSI");
4830 if (rxConfig()->serialrx_provider == SERIALRX_CRSF) {
4831 strcat(buff, "/LQ");
4833 if (osdDisplayIsHD())
4834 strcat(buff, "/DBM");
4836 displayWrite(osdDisplayPort, col, row, buff);
4838 memset(buff, '\0', strlen(buff));
4839 tfp_sprintf(buff, ": ");
4840 itoa(stats.min_rssi, buff + 2, 10);
4841 strcat(osdFormatTrimWhiteSpace(buff), "%");
4843 if (rxConfig()->serialrx_provider == SERIALRX_CRSF) {
4844 strcat(osdFormatTrimWhiteSpace(buff), "/");
4845 multiValueXOffset = strlen(buff);
4846 itoa(stats.min_lq, buff + multiValueXOffset, 10);
4847 strcat(osdFormatTrimWhiteSpace(buff), "%");
4849 if (osdDisplayIsHD()) {
4850 strcat(osdFormatTrimWhiteSpace(buff), "/");
4851 itoa(stats.min_rssi_dbm, buff + 2, 10);
4852 tfp_sprintf(buff + strlen(buff), "%c", SYM_DBM);
4853 displayWrite(osdDisplayPort, statValX, row++, buff);
4857 displayWrite(osdDisplayPort, statValX, row++, buff);
4859 if (!osdDisplayIsHD() && rxConfig()->serialrx_provider == SERIALRX_CRSF) {
4860 displayWrite(osdDisplayPort, col, row, "MIN RX DBM");
4861 memset(buff, '\0', strlen(buff));
4862 tfp_sprintf(buff, ": ");
4863 itoa(stats.min_rssi_dbm, buff + 2, 10);
4864 tfp_sprintf(buff + strlen(buff), "%c", SYM_DBM);
4865 displayWrite(osdDisplayPort, statValX, row++, buff);
4868 return row;
4871 uint8_t drawStat_GPS(uint8_t col, uint8_t row, uint8_t statValX)
4873 char buff[12];
4874 displayWrite(osdDisplayPort, col, row, "MIN/MAX GPS SATS");
4875 tfp_sprintf(buff, ": %u/%u", stats.min_sats, stats.max_sats);
4876 displayWrite(osdDisplayPort, statValX, row++, buff);
4878 return row;
4881 uint8_t drawStat_ESCTemperature(uint8_t col, uint8_t row, uint8_t statValX)
4883 char buff[12];
4884 displayWrite(osdDisplayPort, col, row, "MIN/MAX ESC TEMP");
4885 tfp_sprintf(buff, ": %3d/%3d%c",
4886 ((osdConfig()->units == OSD_UNIT_IMPERIAL) ? (int16_t)(stats.min_esc_temp * 9 / 5.0f + 320) : stats.min_esc_temp),
4887 ((osdConfig()->units == OSD_UNIT_IMPERIAL) ? (int16_t)(stats.max_esc_temp * 9 / 5.0f + 320) : stats.max_esc_temp),
4888 ((osdConfig()->units == OSD_UNIT_IMPERIAL) ? SYM_TEMP_F : SYM_TEMP_C));
4889 displayWrite(osdDisplayPort, statValX, row++, buff);
4891 return row;
4894 uint8_t drawStat_GForce(uint8_t col, uint8_t row, uint8_t statValX)
4896 char buff[12];
4897 char outBuff[12];
4899 const float max_gforce = accGetMeasuredMaxG();
4900 const acc_extremes_t *acc_extremes = accGetMeasuredExtremes();
4901 const float acc_extremes_min = acc_extremes[Z].min;
4902 const float acc_extremes_max = acc_extremes[Z].max;
4904 if (!osdDisplayIsHD())
4905 displayWrite(osdDisplayPort, col, row, "MAX G-FORCE");
4906 else
4907 displayWrite(osdDisplayPort, col, row, "MAX/MIN Z/MAX Z G-FORCE");
4909 tfp_sprintf(outBuff, ": ");
4910 osdFormatCentiNumber(buff, max_gforce * 100, 0, 2, 0, 3, false);
4912 if (!osdDisplayIsHD()) {
4913 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4914 displayWrite(osdDisplayPort, statValX, row++, outBuff);
4916 displayWrite(osdDisplayPort, col, row, "MIN/MAX Z G-FORCE");
4917 memset(outBuff, '\0', strlen(outBuff));
4918 tfp_sprintf(outBuff, ": ");
4919 } else {
4920 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4921 strcat(outBuff, "/");
4923 osdFormatCentiNumber(buff, acc_extremes_min * 100, 0, 2, 0, 4, false);
4924 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4925 strcat(outBuff, "/");
4927 osdFormatCentiNumber(buff, acc_extremes_max * 100, 0, 2, 0, 3, false);
4928 strcat(outBuff, osdFormatTrimWhiteSpace(buff));
4929 displayWrite(osdDisplayPort, statValX, row++, outBuff);
4931 return row;
4934 uint8_t drawStat_DisarmMethod(uint8_t col, uint8_t row, uint8_t statValX)
4936 // We keep "" for backward compatibility with the Blackbox explorer and other potential usages
4937 const char * disarmReasonStr[DISARM_REASON_COUNT] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "", "FAILSAFE", "NAV SYS", "LANDING"};
4939 displayWrite(osdDisplayPort, col, row, "DISARMED BY");
4940 displayWrite(osdDisplayPort, statValX, row, ": ");
4941 displayWrite(osdDisplayPort, statValX + 2, row++, disarmReasonStr[getDisarmReason()]);
4943 return row;
4946 static void osdShowStats(bool isSinglePageStatsCompatible, uint8_t page)
4948 const char * statsHeader[2] = {"*** STATS 1/2 -> ***", "*** STATS <- 2/2 ***"};
4949 uint8_t row = 1; // Start one line down leaving space at the top of the screen.
4951 const uint8_t statNameX = (osdDisplayPort->cols - (osdDisplayIsHD() ? 41 : 28)) / 2;
4952 const uint8_t statValuesX = osdDisplayPort->cols - statNameX - (osdDisplayIsHD() ? 15 : 11);
4954 if (page > 1)
4955 page = 0;
4957 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4958 displayClearScreen(osdDisplayPort);
4960 if (isSinglePageStatsCompatible) {
4961 char buff[25];
4962 tfp_sprintf(buff, "*** STATS ");
4963 #ifdef USE_BLACKBOX
4964 #ifdef USE_SDCARD
4965 if (feature(FEATURE_BLACKBOX)) {
4966 int32_t logNumber = blackboxGetLogNumber();
4967 if (logNumber >= 0)
4968 tfp_sprintf(buff + strlen(buff), " %c%05" PRId32 " ", SYM_BLACKBOX, logNumber);
4969 else
4970 tfp_sprintf(buff + strlen(buff), " %c ", SYM_BLACKBOX);
4972 #endif
4973 #endif
4974 strcat(buff, "***");
4976 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buff)) / 2, row++, buff);
4977 } else
4978 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(statsHeader[page + 1])) / 2, row++, statsHeader[page]);
4980 if (isSinglePageStatsCompatible) {
4981 // Top 15 rows for most important stats. Max 19 rows (WTF)
4982 row = drawStat_FlightTime(statNameX, row, statValuesX); // 1 row
4983 row = drawStat_FlightDistance(statNameX, row, statValuesX); // 1 row
4984 if (feature(FEATURE_GPS)) row = drawStat_MaxDistanceFromHome(statNameX, row, statValuesX); // 1 row
4985 if (feature(FEATURE_GPS)) row = drawStat_Speed(statNameX, row, statValuesX); // 1 row
4986 row = drawStat_MaximumAltitude(statNameX, row, statValuesX); // 1 row
4987 row = drawStat_BatteryVoltage(statNameX, row, statValuesX); // 1 row
4988 if (feature(FEATURE_CURRENT_METER)) row = drawStat_MaximumPowerAndCurrent(statNameX, row, statValuesX); // 1 row
4989 if (feature(FEATURE_CURRENT_METER)) row = drawStat_UsedEnergy(statNameX, row, statValuesX); // 1 row
4990 if (feature(FEATURE_CURRENT_METER) && feature(FEATURE_GPS)) row = drawStat_AverageEfficiency(statNameX, row, statValuesX, false); // 1 row
4991 if (osdConfig()->stats_show_metric_efficiency && osdIsNotMetric() && feature(FEATURE_CURRENT_METER) && feature(FEATURE_GPS)) row = drawStat_AverageEfficiency(statNameX, row, statValuesX, true); // 1 row
4992 row = drawStat_RXStats(statNameX, row, statValuesX); // 1 row if non-CRSF else 2 rows
4993 if (feature(FEATURE_GPS)) row = drawStat_GPS(statNameX, row, statValuesX); // 1 row
4994 if (STATE(ESC_SENSOR_ENABLED)) row = drawStat_ESCTemperature(statNameX, row, statValuesX); // 1 row
4996 // Draw these if there is space space
4997 if (row < (osdDisplayPort->cols-3)) row = drawStat_GForce(statNameX, row, statValuesX); // 1 row HD or 2 rows SD
4998 #ifdef USE_STATS
4999 if (row < (osdDisplayPort->cols-7) && statsConfig()->stats_enabled) row = drawStat_Stats(statNameX, row, statValuesX, false); // 4 rows
5000 #endif
5001 } else {
5002 switch (page) {
5003 case 0:
5004 // Max 10 rows
5005 row = drawStat_FlightTime(statNameX, row, statValuesX); // 1 row
5006 row = drawStat_FlightDistance(statNameX, row, statValuesX); // 1 row
5007 if (feature(FEATURE_GPS)) row = drawStat_MaxDistanceFromHome(statNameX, row, statValuesX); // 1 row
5008 if (feature(FEATURE_GPS)) row = drawStat_Speed(statNameX, row, statValuesX); // 1 row
5009 row = drawStat_MaximumAltitude(statNameX, row, statValuesX); // 1 row
5010 row = drawStat_BatteryVoltage(statNameX, row, statValuesX); // 1 row
5011 if (feature(FEATURE_CURRENT_METER)) row = drawStat_MaximumPowerAndCurrent(statNameX, row, statValuesX); // 1 row
5012 if (feature(FEATURE_CURRENT_METER))row = drawStat_UsedEnergy(statNameX, row, statValuesX); // 1 row
5013 if (feature(FEATURE_CURRENT_METER) && feature(FEATURE_GPS)) row = drawStat_AverageEfficiency(statNameX, row, statValuesX, false); // 1 row
5014 if (feature(FEATURE_GPS))row = drawStat_GPS(statNameX, row, statValuesX); // 1 row
5015 break;
5016 case 1:
5017 // Max 10 rows
5018 row = drawStat_RXStats(statNameX, row, statValuesX); // 1 row if non-CRSF else 2 rows
5019 if (STATE(ESC_SENSOR_ENABLED)) row = drawStat_ESCTemperature(statNameX, row, statValuesX); // 1 row
5020 row = drawStat_GForce(statNameX, row, statValuesX); // 1 row HD or 2 rows SD
5021 if (osdConfig()->stats_show_metric_efficiency && osdIsNotMetric() && feature(FEATURE_CURRENT_METER) && feature(FEATURE_GPS)) row = drawStat_AverageEfficiency(statNameX, row, statValuesX, true); // 1 row
5022 #ifdef USE_BLACKBOX
5023 #ifdef USE_SDCARD
5024 if (feature(FEATURE_BLACKBOX)) {
5025 char buff[12];
5026 displayWrite(osdDisplayPort, statNameX, row, "BLACKBOX FILE");
5028 tfp_sprintf(buff, ": %u/%u", stats.min_sats, stats.max_sats);
5030 int32_t logNumber = blackboxGetLogNumber();
5031 if (logNumber >= 0)
5032 tfp_sprintf(buff, ": %05ld ", logNumber);
5033 else
5034 strcat(buff, ": INVALID");
5036 displayWrite(osdDisplayPort, statValuesX, row++, buff); // 1 row
5038 #endif
5039 #endif
5040 #ifdef USE_STATS
5041 if (row < (osdDisplayPort->cols-7) && statsConfig()->stats_enabled) row = drawStat_Stats(statNameX, row, statValuesX, false); // 4 rows
5042 #endif
5044 break;
5048 row = drawStat_DisarmMethod(statNameX, row, statValuesX);
5050 // The following has been commented out as it will be added in #9688
5051 // uint16_t rearmMs = (emergInflightRearmEnabled()) ? emergencyInFlightRearmTimeMS() : 0;
5053 if (savingSettings == true) {
5054 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS))) / 2, row++, OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS));
5055 /*} else if (rearmMs > 0) { // Show rearming time if settings not actively being saved. Ignore the settings saved message if rearm available.
5056 char emReArmMsg[23];
5057 tfp_sprintf(emReArmMsg, "** REARM PERIOD: ");
5058 tfp_sprintf(emReArmMsg + strlen(emReArmMsg), "%02d", (uint8_t)MS2S(rearmMs));
5059 strcat(emReArmMsg, " **\0");
5060 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(emReArmMsg));*/
5061 } else if (notify_settings_saved > 0) {
5062 if (millis() > notify_settings_saved) {
5063 notify_settings_saved = 0;
5064 } else {
5065 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED))) / 2, row++, OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED));
5069 displayCommitTransaction(osdDisplayPort);
5072 // HD arming screen. based on the minimum HD OSD grid size of 50 x 18
5073 static void osdShowHDArmScreen(void)
5075 dateTime_t dt;
5076 char buf[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
5077 char buf2[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
5078 char craftNameBuf[MAX_NAME_LENGTH];
5079 char versionBuf[osdDisplayPort->cols];
5080 uint8_t safehomeRow = 0;
5081 uint8_t armScreenRow = 2; // Start at row 2
5083 armScreenRow = drawLogos(false, armScreenRow);
5084 armScreenRow++;
5086 if (strlen(systemConfig()->craftName) > 0) {
5087 osdFormatCraftName(craftNameBuf);
5088 strcpy(buf2, "ARMED!");
5089 tfp_sprintf(buf, "%s - %s", craftNameBuf, buf2);
5090 } else {
5091 strcpy(buf, "ARMED!");
5093 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5094 #if defined(USE_GPS)
5095 #if defined (USE_SAFE_HOME)
5096 if (posControl.safehomeState.distance) {
5097 safehomeRow = armScreenRow;
5098 armScreenRow++;
5100 #endif // USE_SAFE_HOME
5101 #endif // USE_GPS
5102 armScreenRow++;
5104 if (posControl.waypointListValid && posControl.waypointCount > 0) {
5105 #ifdef USE_MULTI_MISSION
5106 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
5107 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5108 #else
5109 strcpy(buf, "*MISSION LOADED*");
5110 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5111 #endif
5113 armScreenRow++;
5115 #if defined(USE_GPS)
5116 if (feature(FEATURE_GPS)) {
5117 if (STATE(GPS_FIX_HOME)) {
5118 if (osdConfig()->osd_home_position_arm_screen){
5119 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
5120 osdFormatCoordinate(buf2, SYM_LON, GPS_home.lon);
5121 uint8_t gap = 1;
5122 uint8_t col = strlen(buf) + strlen(buf2) + gap;
5124 if ((osdDisplayPort->cols %2) != (col %2)) {
5125 gap++;
5126 col++;
5129 col = (osdDisplayPort->cols - col) / 2;
5131 displayWrite(osdDisplayPort, col, armScreenRow, buf);
5132 displayWrite(osdDisplayPort, col + strlen(buf) + gap, armScreenRow++, buf2);
5134 int digits = osdConfig()->plus_code_digits;
5135 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
5136 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5139 #if defined (USE_SAFE_HOME)
5140 if (posControl.safehomeState.distance) { // safehome found during arming
5141 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
5142 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
5143 } else {
5144 osdFormatDistanceStr(buf2, posControl.safehomeState.distance);
5145 tfp_sprintf(buf, "%c SAFEHOME %u @ %s", SYM_HOME, posControl.safehomeState.index, buf2);
5147 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
5148 // write this message below the ARMED message to make it obvious
5149 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, safehomeRow, buf, elemAttr);
5151 #endif
5152 } else {
5153 strcpy(buf, "!NO HOME POSITION!");
5154 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5156 armScreenRow++;
5158 #endif
5160 if (rtcGetDateTimeLocal(&dt)) {
5161 tfp_sprintf(buf, "%04u-%02u-%02u %02u:%02u:%02u", dt.year, dt.month, dt.day, dt.hours, dt.minutes, dt.seconds);
5162 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5163 armScreenRow++;
5166 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
5167 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, armScreenRow++, versionBuf);
5168 armScreenRow++;
5170 #ifdef USE_STATS
5171 if (armScreenRow < (osdDisplayPort->rows - 4))
5172 armScreenRow = drawStats(armScreenRow);
5173 #endif // USE_STATS
5176 static void osdShowSDArmScreen(void)
5178 dateTime_t dt;
5179 char buf[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
5180 char buf2[MAX(osdDisplayPort->cols, FORMATTED_DATE_TIME_BUFSIZE)];
5181 char craftNameBuf[MAX_NAME_LENGTH];
5182 char versionBuf[osdDisplayPort->cols];
5183 // We need 12 visible rows, start row never < first fully visible row 1
5184 uint8_t armScreenRow = osdDisplayPort->rows > 13 ? (osdDisplayPort->rows - 12) / 2 : 1;
5185 uint8_t safehomeRow = 0;
5187 strcpy(buf, "ARMED!");
5188 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5189 safehomeRow = armScreenRow;
5190 armScreenRow++;
5192 if (strlen(systemConfig()->craftName) > 0) {
5193 osdFormatCraftName(craftNameBuf);
5194 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(systemConfig()->craftName)) / 2, armScreenRow++, craftNameBuf );
5197 if (posControl.waypointListValid && posControl.waypointCount > 0) {
5198 #ifdef USE_MULTI_MISSION
5199 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
5200 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow, buf);
5201 #else
5202 strcpy(buf, "*MISSION LOADED*");
5203 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow, buf);
5204 #endif
5206 armScreenRow++;
5208 #if defined(USE_GPS)
5209 if (feature(FEATURE_GPS)) {
5210 if (STATE(GPS_FIX_HOME)) {
5211 if (osdConfig()->osd_home_position_arm_screen) {
5212 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
5213 osdFormatCoordinate(buf2, SYM_LON, GPS_home.lon);
5215 uint8_t gpsStartCol = (osdDisplayPort->cols - (strlen(buf) + strlen(buf2) + 2)) / 2;
5216 displayWrite(osdDisplayPort, gpsStartCol, armScreenRow, buf);
5217 displayWrite(osdDisplayPort, gpsStartCol + strlen(buf) + 2, armScreenRow++, buf2);
5219 int digits = osdConfig()->plus_code_digits;
5220 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
5221 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5224 #if defined (USE_SAFE_HOME)
5225 if (posControl.safehomeState.distance) { // safehome found during arming
5226 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
5227 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
5228 } else {
5229 osdFormatDistanceStr(buf2, posControl.safehomeState.distance);
5230 tfp_sprintf(buf, "%c SAFEHOME %u @ %s", SYM_HOME, posControl.safehomeState.index, buf2);
5232 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
5233 // write this message below the ARMED message to make it obvious
5234 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, safehomeRow, buf, elemAttr);
5236 #endif
5237 } else {
5238 strcpy(buf, "!NO HOME POSITION!");
5239 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5241 armScreenRow++;
5243 #endif
5245 if (rtcGetDateTimeLocal(&dt)) {
5246 tfp_sprintf(buf, "%04u-%02u-%02u %02u:%02u:%02u", dt.year, dt.month, dt.day, dt.hours, dt.minutes, dt.seconds);
5247 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, armScreenRow++, buf);
5248 armScreenRow++;
5251 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
5252 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, armScreenRow++, versionBuf);
5253 armScreenRow++;
5255 #ifdef USE_STATS
5256 if (armScreenRow < (osdDisplayPort->rows - 4))
5257 armScreenRow = drawStats(armScreenRow);
5258 #endif // USE_STATS
5261 // called when motors armed
5262 static void osdShowArmed(void)
5264 displayClearScreen(osdDisplayPort);
5266 if (osdDisplayIsHD()) {
5267 osdShowHDArmScreen();
5268 } else {
5269 osdShowSDArmScreen();
5273 static void osdFilterData(timeUs_t currentTimeUs) {
5274 static timeUs_t lastRefresh = 0;
5275 float refresh_dT = US2S(cmpTimeUs(currentTimeUs, lastRefresh));
5277 GForce = fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF)) / GRAVITY_MSS;
5278 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) GForceAxis[axis] = imuMeasuredAccelBF.v[axis] / GRAVITY_MSS;
5280 if (lastRefresh) {
5281 GForce = pt1FilterApply3(&GForceFilter, GForce, refresh_dT);
5282 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) pt1FilterApply3(GForceFilterAxis + axis, GForceAxis[axis], refresh_dT);
5283 } else {
5284 pt1FilterInitRC(&GForceFilter, GFORCE_FILTER_TC, 0);
5285 pt1FilterReset(&GForceFilter, GForce);
5287 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) {
5288 pt1FilterInitRC(GForceFilterAxis + axis, GFORCE_FILTER_TC, 0);
5289 pt1FilterReset(GForceFilterAxis + axis, GForceAxis[axis]);
5293 lastRefresh = currentTimeUs;
5296 // Detect when the user is holding the roll stick to the right
5297 static bool osdIsPageUpStickCommandHeld(void)
5299 static int pageUpHoldCount = 1;
5301 bool keyHeld = false;
5303 if (IS_HI(ROLL)) {
5304 keyHeld = true;
5307 if (!keyHeld) {
5308 pageUpHoldCount = 1;
5309 } else {
5310 ++pageUpHoldCount;
5313 if (pageUpHoldCount > 20) {
5314 pageUpHoldCount = 1;
5315 return true;
5318 return false;
5321 // Detect when the user is holding the roll stick to the left
5322 static bool osdIsPageDownStickCommandHeld(void)
5324 static int pageDownHoldCount = 1;
5326 bool keyHeld = false;
5327 if (IS_LO(ROLL)) {
5328 keyHeld = true;
5331 if (!keyHeld) {
5332 pageDownHoldCount = 1;
5333 } else {
5334 ++pageDownHoldCount;
5337 if (pageDownHoldCount > 20) {
5338 pageDownHoldCount = 1;
5339 return true;
5342 return false;
5345 static void osdRefresh(timeUs_t currentTimeUs)
5347 osdFilterData(currentTimeUs);
5349 #ifdef USE_CMS
5350 if (IS_RC_MODE_ACTIVE(BOXOSD) && (!cmsInMenu) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
5351 #else
5352 if (IS_RC_MODE_ACTIVE(BOXOSD) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
5353 #endif
5354 displayClearScreen(osdDisplayPort);
5355 armState = ARMING_FLAG(ARMED);
5356 return;
5359 bool statsSinglePageCompatible = (osdDisplayPort->rows >= OSD_STATS_SINGLE_PAGE_MIN_ROWS);
5360 static uint8_t statsCurrentPage = 0;
5361 static bool statsDisplayed = false;
5362 static bool statsAutoPagingEnabled = true;
5363 static bool isThrottleHigh = false;
5365 // Detect arm/disarm
5366 if (armState != ARMING_FLAG(ARMED)) {
5367 if (ARMING_FLAG(ARMED)) {
5368 // Display the "Arming" screen
5369 statsDisplayed = false;
5370 if (!STATE(IN_FLIGHT_EMERG_REARM))
5371 osdResetStats();
5373 osdShowArmed();
5374 uint16_t delay = osdConfig()->arm_screen_display_time;
5375 if (STATE(IN_FLIGHT_EMERG_REARM))
5376 delay = 500;
5377 #if defined(USE_SAFE_HOME)
5378 else if (posControl.safehomeState.distance)
5379 delay += 3000;
5380 #endif
5381 osdSetNextRefreshIn(delay);
5382 } else {
5383 // Display the "Stats" screen
5384 statsDisplayed = true;
5385 statsCurrentPage = 0;
5386 statsAutoPagingEnabled = osdConfig()->stats_page_auto_swap_time > 0 ? true : false;
5387 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
5388 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME);
5389 isThrottleHigh = checkStickPosition(THR_HI);
5392 armState = ARMING_FLAG(ARMED);
5395 // This block is entered when we're showing the "Splash", "Armed" or "Stats" screens
5396 if (resumeRefreshAt) {
5398 // Handle events only when the "Stats" screen is being displayed.
5399 if (statsDisplayed) {
5401 // Manual paging stick commands are only applicable to multi-page stats.
5402 // ******************************
5403 // For single-page stats, this effectively disables the ability to cancel the
5404 // automatic paging/updates with the stick commands. So unless stats_page_auto_swap_time
5405 // is set to 0 or greater than 4 (saved settings display interval is 5 seconds), then
5406 // "Saved Settings" should display if it is active within the refresh interval.
5407 // ******************************
5408 // With multi-page stats, "Saved Settings" could also be missed if the user
5409 // has canceled automatic paging using the stick commands, because that is only
5410 // updated when osdShowStats() is called. So, in that case, they would only see
5411 // the "Saved Settings" message if they happen to manually change pages using the
5412 // stick commands within the interval the message is displayed.
5413 bool manualPageUpRequested = false;
5414 bool manualPageDownRequested = false;
5415 if (!statsSinglePageCompatible) {
5416 // These methods ensure the paging stick commands are held for a brief period
5417 // Otherwise it can result in a race condition where the stats are
5418 // updated too quickly and can result in partial blanks, etc.
5419 if (osdIsPageUpStickCommandHeld()) {
5420 manualPageUpRequested = true;
5421 statsAutoPagingEnabled = false;
5422 } else if (osdIsPageDownStickCommandHeld()) {
5423 manualPageDownRequested = true;
5424 statsAutoPagingEnabled = false;
5428 if (statsAutoPagingEnabled) {
5429 // Alternate screens for multi-page stats.
5430 // Also, refreshes screen at swap interval for single-page stats.
5431 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time * 1000), 2)) {
5432 if (statsCurrentPage == 0) {
5433 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
5434 statsCurrentPage = 1;
5436 } else {
5437 if (statsCurrentPage == 1) {
5438 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
5439 statsCurrentPage = 0;
5442 } else {
5443 // Process manual page change events for multi-page stats.
5444 if (manualPageUpRequested) {
5445 osdShowStats(statsSinglePageCompatible, 1);
5446 statsCurrentPage = 1;
5447 } else if (manualPageDownRequested) {
5448 osdShowStats(statsSinglePageCompatible, 0);
5449 statsCurrentPage = 0;
5454 // Handle events when either "Splash", "Armed" or "Stats" screens are displayed.
5455 if (currentTimeUs > resumeRefreshAt || (OSD_RESUME_UPDATES_STICK_COMMAND && !isThrottleHigh)) {
5456 // Time elapsed or canceled by stick commands.
5457 // Exit to normal OSD operation.
5458 displayClearScreen(osdDisplayPort);
5459 resumeRefreshAt = 0;
5460 statsDisplayed = false;
5461 } else {
5462 // Continue "Splash", "Armed" or "Stats" screens.
5463 displayHeartbeat(osdDisplayPort);
5464 isThrottleHigh = checkStickPosition(THR_HI);
5467 return;
5470 #ifdef USE_CMS
5471 if (!displayIsGrabbed(osdDisplayPort)) {
5472 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
5473 if (fullRedraw) {
5474 displayClearScreen(osdDisplayPort);
5475 fullRedraw = false;
5477 osdDrawNextElement();
5478 displayHeartbeat(osdDisplayPort);
5479 displayCommitTransaction(osdDisplayPort);
5480 #ifdef OSD_CALLS_CMS
5481 } else {
5482 cmsUpdate(currentTimeUs);
5483 #endif
5485 #endif
5489 * Called periodically by the scheduler
5491 void osdUpdate(timeUs_t currentTimeUs)
5493 static uint32_t counter = 0;
5495 // don't touch buffers if DMA transaction is in progress
5496 if (displayIsTransferInProgress(osdDisplayPort)) {
5497 return;
5500 if (!osdDisplayIsReady) {
5501 osdCompleteAsyncInitialization();
5502 return;
5505 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
5506 // Check if the layout has changed. Higher numbered
5507 // boxes take priority.
5508 unsigned activeLayout;
5509 if (layoutOverride >= 0) {
5510 activeLayout = layoutOverride;
5511 // Check for timed override, it will go into effect on
5512 // the next OSD iteration
5513 if (layoutOverrideUntil > 0 && millis() > layoutOverrideUntil) {
5514 layoutOverrideUntil = 0;
5515 layoutOverride = -1;
5517 } else if (osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE)) {
5518 activeLayout = 0;
5519 } else {
5520 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
5521 if (IS_RC_MODE_ACTIVE(BOXOSDALT3))
5522 activeLayout = 3;
5523 else
5524 #endif
5525 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
5526 if (IS_RC_MODE_ACTIVE(BOXOSDALT2))
5527 activeLayout = 2;
5528 else
5529 #endif
5530 if (IS_RC_MODE_ACTIVE(BOXOSDALT1))
5531 activeLayout = 1;
5532 else
5533 #ifdef USE_PROGRAMMING_FRAMEWORK
5534 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT))
5535 activeLayout = constrain(logicConditionValuesByType[LOGIC_CONDITION_SET_OSD_LAYOUT], 0, OSD_ALTERNATE_LAYOUT_COUNT);
5536 else
5537 #endif
5538 activeLayout = 0;
5540 if (currentLayout != activeLayout) {
5541 currentLayout = activeLayout;
5542 osdStartFullRedraw();
5544 #endif
5546 #define DRAW_FREQ_DENOM 4
5547 #define STATS_FREQ_DENOM 50
5548 counter++;
5550 if ((counter % STATS_FREQ_DENOM) == 0 && ARMING_FLAG(ARMED)) {
5551 osdUpdateStats();
5554 if ((counter % DRAW_FREQ_DENOM) == 0) {
5555 // redraw values in buffer
5556 osdRefresh(currentTimeUs);
5557 } else {
5558 // rest of time redraw screen
5559 displayDrawScreen(osdDisplayPort);
5562 #ifdef USE_CMS
5563 // do not allow ARM if we are in menu
5564 if (displayIsGrabbed(osdDisplayPort)) {
5565 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
5566 } else {
5567 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
5569 #endif
5572 void osdStartFullRedraw(void)
5574 fullRedraw = true;
5577 void osdOverrideLayout(int layout, timeMs_t duration)
5579 layoutOverride = constrain(layout, -1, ARRAYLEN(osdLayoutsConfig()->item_pos) - 1);
5580 if (layoutOverride >= 0 && duration > 0) {
5581 layoutOverrideUntil = millis() + duration;
5582 } else {
5583 layoutOverrideUntil = 0;
5587 int osdGetActiveLayout(bool *overridden)
5589 if (overridden) {
5590 *overridden = layoutOverride >= 0;
5592 return currentLayout;
5595 bool osdItemIsFixed(osd_items_e item)
5597 return item == OSD_CROSSHAIRS ||
5598 item == OSD_ARTIFICIAL_HORIZON ||
5599 item == OSD_HORIZON_SIDEBARS;
5602 displayPort_t *osdGetDisplayPort(void)
5604 return osdDisplayPort;
5607 displayCanvas_t *osdGetDisplayPortCanvas(void)
5609 #if defined(USE_CANVAS)
5610 if (osdDisplayHasCanvas) {
5611 return &osdCanvas;
5613 #endif
5614 return NULL;
5617 timeMs_t systemMessageCycleTime(unsigned messageCount, const char **messages){
5618 uint8_t i = 0;
5619 float factor = 1.0f;
5620 while (i < messageCount) {
5621 if ((float)strlen(messages[i]) / 15.0f > factor) {
5622 factor = (float)strlen(messages[i]) / 15.0f;
5624 i++;
5626 return osdConfig()->system_msg_display_time * factor;
5629 textAttributes_t osdGetSystemMessage(char *buff, size_t buff_size, bool isCenteredText)
5631 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
5633 if (buff != NULL) {
5634 const char *message = NULL;
5635 /* WARNING: messageBuf is shared, use accordingly */
5636 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH + 1)];
5638 /* WARNING: ensure number of messages returned does not exceed messages array size
5639 * Messages array set 1 larger than maximum expected message count of 6 */
5640 const char *messages[7];
5641 unsigned messageCount = 0;
5643 const char *failsafeInfoMessage = NULL;
5644 const char *invertedInfoMessage = NULL;
5646 if (ARMING_FLAG(ARMED)) {
5647 if (FLIGHT_MODE(FAILSAFE_MODE) || FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding()) {
5648 /* ADDS MAXIMUM OF 3 MESSAGES TO TOTAL NORMALLY, 5 MESSAGES DURING FAILSAFE */
5649 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE) {
5650 messages[messageCount++] = STATE(LANDING_DETECTED) ? OSD_MESSAGE_STR(OSD_MSG_WP_LANDED) : OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED);
5651 } else if (NAV_Status.state == MW_NAV_STATE_WP_ENROUTE) {
5652 // Countdown display for remaining Waypoints
5653 char buf[6];
5654 osdFormatDistanceSymbol(buf, posControl.wpDistance, 0);
5655 tfp_sprintf(messageBuf, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl.activeWaypointIndex), posControl.geoWaypointCount, buf);
5656 messages[messageCount++] = messageBuf;
5657 } else if (NAV_Status.state == MW_NAV_STATE_HOLD_TIMED) {
5658 if (navConfig()->general.waypoint_enforce_altitude && !posControl.wpAltitudeReached) {
5659 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ADJUSTING_WP_ALT);
5660 } else {
5661 // WP hold time countdown in seconds
5662 timeMs_t currentTime = millis();
5663 int holdTimeRemaining = posControl.waypointList[posControl.activeWaypointIndex].p1 - (int)(MS2S(currentTime - posControl.wpReachedTime));
5664 holdTimeRemaining = holdTimeRemaining >= 0 ? holdTimeRemaining : 0;
5666 tfp_sprintf(messageBuf, "HOLDING WP FOR %2u S", holdTimeRemaining);
5668 messages[messageCount++] = messageBuf;
5671 else {
5672 const char *navStateMessage = navigationStateMessage();
5673 if (navStateMessage) {
5674 messages[messageCount++] = navStateMessage;
5677 #if defined(USE_SAFE_HOME)
5678 const char *safehomeMessage = divertingToSafehomeMessage();
5679 if (safehomeMessage) {
5680 messages[messageCount++] = safehomeMessage;
5682 #endif
5683 if (FLIGHT_MODE(FAILSAFE_MODE)) { // In FS mode while armed
5684 if (NAV_Status.state == MW_NAV_STATE_LAND_SETTLE && posControl.landingDelay > 0) {
5685 uint16_t remainingHoldSec = MS2S(posControl.landingDelay - millis());
5686 tfp_sprintf(messageBuf, "LANDING DELAY: %3u SECONDS", remainingHoldSec);
5688 messages[messageCount++] = messageBuf;
5691 const char *failsafePhaseMessage = osdFailsafePhaseMessage();
5692 failsafeInfoMessage = osdFailsafeInfoMessage();
5694 if (failsafePhaseMessage) {
5695 messages[messageCount++] = failsafePhaseMessage;
5697 if (failsafeInfoMessage) {
5698 messages[messageCount++] = failsafeInfoMessage;
5700 } else if (isWaypointMissionRTHActive()) {
5701 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
5702 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL);
5704 } else if (STATE(LANDING_DETECTED)) {
5705 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_LANDED);
5706 } else {
5707 /* Messages shown only when Failsafe, WP, RTH or Emergency Landing not active and landed state inactive */
5708 /* ADDS MAXIMUM OF 3 MESSAGES TO TOTAL */
5709 if (STATE(AIRPLANE)) { /* ADDS MAXIMUM OF 3 MESSAGES TO TOTAL */
5710 #ifdef USE_FW_AUTOLAND
5711 if (canFwLandingBeCancelled()) {
5712 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MOVE_STICKS);
5713 } else
5714 #endif
5715 if (navGetCurrentStateFlags() & NAV_CTL_LAUNCH) {
5716 messages[messageCount++] = navConfig()->fw.launch_manual_throttle ? OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH_MANUAL) :
5717 OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH);
5718 const char *launchStateMessage = fixedWingLaunchStateMessage();
5719 if (launchStateMessage) {
5720 messages[messageCount++] = launchStateMessage;
5722 } else if (FLIGHT_MODE(SOARING_MODE)) {
5723 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING);
5724 } else if (isFwAutoModeActive(BOXAUTOTUNE)) {
5725 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE);
5726 if (FLIGHT_MODE(MANUAL_MODE)) {
5727 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO);
5729 } else if (isFwAutoModeActive(BOXAUTOTRIM) && !feature(FEATURE_FW_AUTOTRIM)) {
5730 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM);
5731 } else if (isFixedWingLevelTrimActive()) {
5732 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOLEVEL);
5735 if (IS_RC_MODE_ACTIVE(BOXANGLEHOLD)) {
5736 int8_t navAngleHoldAxis = navCheckActiveAngleHoldAxis();
5737 if (isAngleHoldLevel()) {
5738 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_LEVEL);
5739 } else if (navAngleHoldAxis == FD_ROLL) {
5740 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_ROLL);
5741 } else if (navAngleHoldAxis == FD_PITCH) {
5742 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_PITCH);
5745 } else if (STATE(MULTIROTOR)) { /* ADDS MAXIMUM OF 2 MESSAGES TO TOTAL */
5746 if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
5747 if (posControl.cruise.multicopterSpeed >= 50.0f) {
5748 char buf[6];
5749 osdFormatVelocityStr(buf, posControl.cruise.multicopterSpeed, false, false);
5750 tfp_sprintf(messageBuf, "(SPD %s)", buf);
5751 } else {
5752 strcpy(messageBuf, "(HOLD)");
5754 messages[messageCount++] = messageBuf;
5755 } else if (FLIGHT_MODE(HEADFREE_MODE)) {
5756 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE);
5758 if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && !navigationRequiresAngleMode()) {
5759 /* If ALTHOLD is separately enabled for multirotor together with ANGL/HORIZON/ACRO modes
5760 * then ANGL/HORIZON/ACRO are indicated by the OSD_FLYMODE field.
5761 * In this case indicate ALTHOLD is active via a system message */
5763 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD);
5767 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS)) { /* ADDS MAXIMUM OF 2 MESSAGES TO TOTAL */
5768 unsigned invalidIndex;
5770 // Check if we're unable to arm for some reason
5771 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING) && !settingsValidate(&invalidIndex)) {
5772 const setting_t *setting = settingGet(invalidIndex);
5773 settingGetName(setting, messageBuf);
5774 for (int ii = 0; messageBuf[ii]; ii++) {
5775 messageBuf[ii] = sl_toupper(messageBuf[ii]);
5777 invertedInfoMessage = messageBuf;
5778 messages[messageCount++] = invertedInfoMessage;
5780 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
5781 messages[messageCount++] = invertedInfoMessage;
5782 } else {
5783 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM);
5784 messages[messageCount++] = invertedInfoMessage;
5785 // Show the reason for not arming
5786 messages[messageCount++] = osdArmingDisabledReasonMessage();
5788 } else if (!ARMING_FLAG(ARMED)) {
5789 if (isWaypointListValid()) {
5790 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED);
5794 /* Messages that are shown regardless of Arming state */
5795 /* ADDS MAXIMUM OF 2 MESSAGES TO TOTAL NORMALLY, 1 MESSAGE DURING FAILSAFE */
5796 if (posControl.flags.wpMissionPlannerActive && !FLIGHT_MODE(FAILSAFE_MODE)) {
5797 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER);
5800 // The following has been commented out as it will be added in #9688
5801 // uint16_t rearmMs = (emergInflightRearmEnabled()) ? emergencyInFlightRearmTimeMS() : 0;
5803 if (savingSettings == true) {
5804 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS);
5805 /*} else if (rearmMs > 0) { // Show rearming time if settings not actively being saved. Ignore the settings saved message if rearm available.
5806 char emReArmMsg[23];
5807 tfp_sprintf(emReArmMsg, "** REARM PERIOD: ");
5808 tfp_sprintf(emReArmMsg + strlen(emReArmMsg), "%02d", (uint8_t)MS2S(rearmMs));
5809 strcat(emReArmMsg, " **\0");
5810 messages[messageCount++] = OSD_MESSAGE_STR(emReArmMsg);*/
5811 } else if (notify_settings_saved > 0) {
5812 if (millis() > notify_settings_saved) {
5813 notify_settings_saved = 0;
5814 } else {
5815 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED);
5819 if (messageCount > 0) {
5820 message = messages[OSD_ALTERNATING_CHOICES(systemMessageCycleTime(messageCount, messages), messageCount)];
5821 if (message == failsafeInfoMessage) {
5822 // failsafeInfoMessage is not useful for recovering
5823 // a lost model, but might help avoiding a crash.
5824 // Blink to grab user attention.
5825 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
5826 } else if (message == invertedInfoMessage) {
5827 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
5829 // We're showing either failsafePhaseMessage or
5830 // navStateMessage. Don't BLINK here since
5831 // having this text available might be crucial
5832 // during a lost aircraft recovery and blinking
5833 // will cause it to be missing from some frames.
5836 osdFormatMessage(buff, buff_size, message, isCenteredText);
5838 return elemAttr;
5841 void osdResetWarningFlags(void)
5843 osdWarningsFlags = 0;
5846 static bool osdCheckWarning(bool condition, uint8_t warningFlag, uint8_t *warningsCount)
5848 #define WARNING_REDISPLAY_DURATION 5000; // milliseconds
5850 const timeMs_t currentTimeMs = millis();
5851 static timeMs_t warningDisplayStartTime = 0;
5852 static timeMs_t redisplayStartTimeMs = 0;
5853 static uint16_t osdWarningTimerDuration;
5854 static uint8_t newWarningFlags;
5856 if (condition) { // condition required to trigger warning
5857 if (!(osdWarningsFlags & warningFlag)) {
5858 osdWarningsFlags |= warningFlag;
5859 newWarningFlags |= warningFlag;
5860 redisplayStartTimeMs = 0;
5862 #ifdef USE_DEV_TOOLS
5863 if (systemConfig()->groundTestMode) {
5864 return true;
5866 #endif
5867 /* Warnings displayed in full for set time before shrinking down to alert symbol with warning count only.
5868 * All current warnings then redisplayed for 5s on 30s rolling cycle.
5869 * New warnings dislayed individually for 10s */
5870 if (currentTimeMs > redisplayStartTimeMs) {
5871 warningDisplayStartTime = currentTimeMs;
5872 osdWarningTimerDuration = newWarningFlags ? 10000 : WARNING_REDISPLAY_DURATION;
5873 redisplayStartTimeMs = currentTimeMs + osdWarningTimerDuration + 30000;
5876 if (currentTimeMs - warningDisplayStartTime < osdWarningTimerDuration) {
5877 return (newWarningFlags & warningFlag) || osdWarningTimerDuration == WARNING_REDISPLAY_DURATION;
5878 } else {
5879 newWarningFlags = 0;
5881 *warningsCount += 1;
5882 } else if (osdWarningsFlags & warningFlag) {
5883 osdWarningsFlags &= ~warningFlag;
5886 return false;
5889 static textAttributes_t osdGetMultiFunctionMessage(char *buff)
5891 /* Message length limit 10 char max */
5893 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
5894 static uint8_t warningsCount;
5895 const char *message = NULL;
5897 #ifdef USE_MULTI_FUNCTIONS
5898 /* --- FUNCTIONS --- */
5899 multi_function_e selectedFunction = multiFunctionSelection();
5901 if (selectedFunction) {
5902 multi_function_e activeFunction = selectedFunction;
5904 switch (selectedFunction) {
5905 case MULTI_FUNC_NONE:
5906 case MULTI_FUNC_1:
5907 message = warningsCount ? "WARNINGS !" : "0 WARNINGS";
5908 break;
5909 case MULTI_FUNC_2:
5910 message = posControl.flags.manualEmergLandActive ? "ABORT LAND" : "EMERG LAND";
5911 break;
5912 case MULTI_FUNC_3:
5913 #if defined(USE_SAFE_HOME)
5914 if (navConfig()->general.flags.safehome_usage_mode != SAFEHOME_USAGE_OFF) {
5915 message = MULTI_FUNC_FLAG(MF_SUSPEND_SAFEHOMES) ? "USE SFHOME" : "SUS SFHOME";
5916 break;
5918 #endif
5919 activeFunction++;
5920 FALLTHROUGH;
5921 case MULTI_FUNC_4:
5922 if (navConfig()->general.flags.rth_trackback_mode != RTH_TRACKBACK_OFF) {
5923 message = MULTI_FUNC_FLAG(MF_SUSPEND_TRACKBACK) ? "USE TKBACK" : "SUS TKBACK";
5924 break;
5926 activeFunction++;
5927 FALLTHROUGH;
5928 case MULTI_FUNC_5:
5929 #ifdef USE_DSHOT
5930 if (STATE(MULTIROTOR)) {
5931 message = MULTI_FUNC_FLAG(MF_TURTLE_MODE) ? "END TURTLE" : "USE TURTLE";
5932 break;
5934 #endif
5935 activeFunction++;
5936 FALLTHROUGH;
5937 case MULTI_FUNC_6:
5938 message = ARMING_FLAG(ARMED) ? "NOW ARMED " : "EMERG ARM ";
5939 break;
5940 case MULTI_FUNC_END:
5941 break;
5944 if (activeFunction != selectedFunction) {
5945 setMultifunctionSelection(activeFunction);
5948 strcpy(buff, message);
5950 if (isNextMultifunctionItemAvailable()) {
5951 // provides feedback indicating when a new selection command has been received by flight controller
5952 buff[9] = '>';
5955 return elemAttr;
5957 #endif // MULTIFUNCTION - functions only, warnings always defined
5959 /* --- WARNINGS --- */
5960 const char *messages[7];
5961 uint8_t messageCount = 0;
5962 bool warningCondition = false;
5963 warningsCount = 0;
5964 uint8_t warningFlagID = 1;
5966 // Low Battery
5967 const batteryState_e batteryState = getBatteryState();
5968 warningCondition = batteryState == BATTERY_CRITICAL || batteryState == BATTERY_WARNING;
5969 if (osdCheckWarning(warningCondition, warningFlagID, &warningsCount)) {
5970 messages[messageCount++] = batteryState == BATTERY_CRITICAL ? "BATT EMPTY" : "BATT LOW !";
5973 #if defined(USE_GPS)
5974 // GPS Fix and Failure
5975 if (feature(FEATURE_GPS)) {
5976 if (osdCheckWarning(!STATE(GPS_FIX), warningFlagID <<= 1, &warningsCount)) {
5977 bool gpsFailed = getHwGPSStatus() == HW_SENSOR_UNAVAILABLE;
5978 messages[messageCount++] = gpsFailed ? "GPS FAILED" : "NO GPS FIX";
5982 // RTH sanity (warning if RTH heads 200m further away from home than closest point)
5983 warningCondition = NAV_Status.state == MW_NAV_STATE_RTH_ENROUTE && !posControl.flags.rthTrackbackActive &&
5984 (posControl.homeDistance - posControl.rthSanityChecker.minimalDistanceToHome) > 20000;
5985 if (osdCheckWarning(warningCondition, warningFlagID <<= 1, &warningsCount)) {
5986 messages[messageCount++] = "RTH SANITY";
5989 // Altitude sanity (warning if significant mismatch between estimated and GPS altitude)
5990 if (osdCheckWarning(posControl.flags.gpsCfEstimatedAltitudeMismatch, warningFlagID <<= 1, &warningsCount)) {
5991 messages[messageCount++] = "ALT SANITY";
5993 #endif
5995 #if defined(USE_MAG)
5996 // Magnetometer failure
5997 if (requestedSensors[SENSOR_INDEX_MAG] != MAG_NONE) {
5998 hardwareSensorStatus_e magStatus = getHwCompassStatus();
5999 if (osdCheckWarning(magStatus == HW_SENSOR_UNAVAILABLE || magStatus == HW_SENSOR_UNHEALTHY, warningFlagID <<= 1, &warningsCount)) {
6000 messages[messageCount++] = "MAG FAILED";
6003 #endif
6004 // Vibration levels TODO - needs better vibration measurement to be useful
6005 // const float vibrationLevel = accGetVibrationLevel();
6006 // warningCondition = vibrationLevel > 1.5f;
6007 // if (osdCheckWarning(warningCondition, warningFlagID <<= 1, &warningsCount)) {
6008 // messages[messageCount++] = vibrationLevel > 2.5f ? "BAD VIBRTN" : "VIBRATION!";
6009 // }
6011 #ifdef USE_DEV_TOOLS
6012 if (osdCheckWarning(systemConfig()->groundTestMode, warningFlagID <<= 1, &warningsCount)) {
6013 messages[messageCount++] = "GRD TEST !";
6015 #endif
6017 if (messageCount) {
6018 message = messages[OSD_ALTERNATING_CHOICES(1000, messageCount)]; // display each warning on 1s cycle
6019 strcpy(buff, message);
6020 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
6021 } else if (warningsCount) {
6022 buff[0] = SYM_ALERT;
6023 tfp_sprintf(buff + 1, "%u ", warningsCount);
6026 return elemAttr;
6029 #endif // OSD