Added mAh precision to Battery Capacity Remaining
[inav.git] / src / main / io / osd.c
blob9bf68685ba8e96f05dcb53b5c252ab26687e247c
1 /*
2 * This file is part of Cleanflight.
4 * Cleanflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Cleanflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
19 Created by Marcin Baliniak
20 some functions based on MinimOSD
22 OSD-CMS separation by jflyper
25 #include <stdbool.h>
26 #include <stdint.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <ctype.h>
30 #include <math.h>
32 #include "platform.h"
34 #ifdef USE_OSD
36 #include "build/debug.h"
37 #include "build/version.h"
39 #include "cms/cms.h"
40 #include "cms/cms_types.h"
41 #include "cms/cms_menu_osd.h"
43 #include "common/axis.h"
44 #include "common/constants.h"
45 #include "common/filter.h"
46 #include "common/log.h"
47 #include "common/olc.h"
48 #include "common/printf.h"
49 #include "common/string_light.h"
50 #include "common/time.h"
51 #include "common/typeconversion.h"
52 #include "common/utils.h"
54 #include "config/feature.h"
55 #include "config/parameter_group.h"
56 #include "config/parameter_group_ids.h"
58 #include "drivers/display.h"
59 #include "drivers/display_canvas.h"
60 #include "drivers/display_font_metadata.h"
61 #include "drivers/osd_symbols.h"
62 #include "drivers/time.h"
63 #include "drivers/vtx_common.h"
65 #include "io/flashfs.h"
66 #include "io/gps.h"
67 #include "io/osd.h"
68 #include "io/osd_common.h"
69 #include "io/osd_hud.h"
70 #include "io/osd_utils.h"
71 #include "io/displayport_msp_bf_compat.h"
72 #include "io/vtx.h"
73 #include "io/vtx_string.h"
75 #include "fc/config.h"
76 #include "fc/controlrate_profile.h"
77 #include "fc/fc_core.h"
78 #include "fc/fc_tasks.h"
79 #include "fc/rc_adjustments.h"
80 #include "fc/rc_controls.h"
81 #include "fc/rc_modes.h"
82 #include "fc/runtime_config.h"
83 #include "fc/settings.h"
85 #include "flight/imu.h"
86 #include "flight/mixer.h"
87 #include "flight/pid.h"
88 #include "flight/power_limits.h"
89 #include "flight/rth_estimator.h"
90 #include "flight/servos.h"
91 #include "flight/wind_estimator.h"
93 #include "navigation/navigation.h"
94 #include "navigation/navigation_private.h"
96 #include "rx/rx.h"
97 #include "rx/msp_override.h"
99 #include "sensors/acceleration.h"
100 #include "sensors/battery.h"
101 #include "sensors/boardalignment.h"
102 #include "sensors/diagnostics.h"
103 #include "sensors/sensors.h"
104 #include "sensors/pitotmeter.h"
105 #include "sensors/temperature.h"
106 #include "sensors/esc_sensor.h"
107 #include "sensors/rangefinder.h"
109 #include "programming/logic_condition.h"
110 #include "programming/global_variables.h"
112 #ifdef USE_HARDWARE_REVISION_DETECTION
113 #include "hardware_revision.h"
114 #endif
116 #define VIDEO_BUFFER_CHARS_PAL 480
117 #define VIDEO_BUFFER_CHARS_HDZERO 900
118 #define VIDEO_BUFFER_CHARS_DJIWTF 1320
120 #define GFORCE_FILTER_TC 0.2
122 #define OSD_STATS_SINGLE_PAGE_MIN_ROWS 18
123 #define IS_HI(X) (rxGetChannelValue(X) > 1750)
124 #define IS_LO(X) (rxGetChannelValue(X) < 1250)
125 #define IS_MID(X) (rxGetChannelValue(X) > 1250 && rxGetChannelValue(X) < 1750)
127 #define OSD_RESUME_UPDATES_STICK_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
128 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
129 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
131 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
132 #define ARMED_SCREEN_DISPLAY_TIME 1500 // ms
133 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
135 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
137 // Adjust OSD_MESSAGE's default position when
138 // changing OSD_MESSAGE_LENGTH
139 #define OSD_MESSAGE_LENGTH 28
140 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
141 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
142 // Wrap all string constants intenteded for display as messages with
143 // this macro to ensure compile time length validation.
144 #define OSD_MESSAGE_STR(x) ({ \
145 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
146 x; \
149 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
151 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
152 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
154 #define OSD_MIN_FONT_VERSION 3
156 static timeMs_t notify_settings_saved = 0;
157 static bool savingSettings = false;
159 static unsigned currentLayout = 0;
160 static int layoutOverride = -1;
161 static bool hasExtendedFont = false; // Wether the font supports characters > 256
162 static timeMs_t layoutOverrideUntil = 0;
163 static pt1Filter_t GForceFilter, GForceFilterAxis[XYZ_AXIS_COUNT];
164 static float GForce, GForceAxis[XYZ_AXIS_COUNT];
166 typedef struct statistic_s {
167 uint16_t max_speed;
168 uint16_t max_3D_speed;
169 uint16_t max_air_speed;
170 uint16_t min_voltage; // /100
171 int16_t max_current;
172 int32_t max_power;
173 int16_t min_rssi;
174 int16_t min_lq; // for CRSF
175 int16_t min_rssi_dbm; // for CRSF
176 int32_t max_altitude;
177 uint32_t max_distance;
178 } statistic_t;
180 static statistic_t stats;
182 static timeUs_t resumeRefreshAt = 0;
183 static bool refreshWaitForResumeCmdRelease;
185 static bool fullRedraw = false;
187 static uint8_t armState;
189 typedef struct osdMapData_s {
190 uint32_t scale;
191 char referenceSymbol;
192 } osdMapData_t;
194 static osdMapData_t osdMapData;
196 static displayPort_t *osdDisplayPort;
197 static bool osdDisplayIsReady = false;
198 #if defined(USE_CANVAS)
199 static displayCanvas_t osdCanvas;
200 static bool osdDisplayHasCanvas;
201 #else
202 #define osdDisplayHasCanvas false
203 #endif
205 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
207 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t, osdConfig, PG_OSD_CONFIG, 8);
208 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t, osdLayoutsConfig, PG_OSD_LAYOUTS_CONFIG, 1);
210 void osdStartedSaveProcess(void) {
211 savingSettings = true;
214 void osdShowEEPROMSavedNotification(void) {
215 savingSettings = false;
216 notify_settings_saved = millis() + 5000;
221 bool osdDisplayIsPAL(void)
223 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL;
226 bool osdDisplayIsHD(void)
228 if (displayScreenSize(osdDisplayPort) >= VIDEO_BUFFER_CHARS_HDZERO)
230 return true;
232 return false;
238 * Aligns text to the left side. Adds spaces at the end to keep string length unchanged.
240 static void osdLeftAlignString(char *buff)
242 uint8_t sp = 0, ch = 0;
243 uint8_t len = strlen(buff);
244 while (buff[sp] == ' ') sp++;
245 for (ch = 0; ch < (len - sp); ch++) buff[ch] = buff[ch + sp];
246 for (sp = ch; sp < len; sp++) buff[sp] = ' ';
250 * This is a simplified distance conversion code that does not use any scaling
251 * but is fully compatible with the DJI G2 MSP Displayport OSD implementation.
252 * (Based on osdSimpleAltitudeSymbol() implementation)
254 /* void osdSimpleDistanceSymbol(char *buff, int32_t dist) {
256 int32_t convertedDistance;
257 char suffix;
259 switch ((osd_unit_e)osdConfig()->units) {
260 case OSD_UNIT_UK:
261 FALLTHROUGH;
262 case OSD_UNIT_GA:
263 FALLTHROUGH;
264 case OSD_UNIT_IMPERIAL:
265 convertedDistance = CENTIMETERS_TO_FEET(dist);
266 suffix = SYM_ALT_FT;
267 break;
268 case OSD_UNIT_METRIC_MPH:
269 FALLTHROUGH;
270 case OSD_UNIT_METRIC:
271 convertedDistance = CENTIMETERS_TO_METERS(dist);
272 suffix = SYM_ALT_M; // Intentionally use the altitude symbol, as the distance symbol is not defined in BFCOMPAT mode
273 break;
276 tfp_sprintf(buff, "%5d", (int) convertedDistance); // 5 digits, allowing up to 99999 meters/feet, which should be plenty for 99.9% of use cases
277 buff[5] = suffix;
278 buff[6] = '\0';
279 } */
282 * Converts distance into a string based on the current unit system
283 * prefixed by a a symbol to indicate the unit used.
284 * @param dist Distance in centimeters
286 static void osdFormatDistanceSymbol(char *buff, int32_t dist, uint8_t decimals)
288 uint8_t digits = 3U; // Total number of digits (including decimal point)
289 uint8_t sym_index = 3U; // Position (index) at buffer of units symbol
290 uint8_t symbol_m = SYM_DIST_M;
291 uint8_t symbol_km = SYM_DIST_KM;
292 uint8_t symbol_ft = SYM_DIST_FT;
293 uint8_t symbol_mi = SYM_DIST_MI;
294 uint8_t symbol_nm = SYM_DIST_NM;
296 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
297 if (isBfCompatibleVideoSystem(osdConfig())) {
298 // Add one digit so up no switch to scaled decimal occurs above 99
299 digits = 4U;
300 sym_index = 4U;
301 // Use altitude symbols on purpose, as it seems distance symbols are not defined in BFCOMPAT mode
302 symbol_m = SYM_ALT_M;
303 symbol_km = SYM_ALT_KM;
304 symbol_ft = SYM_ALT_FT;
305 symbol_mi = SYM_MI;
306 symbol_nm = SYM_MI;
308 #endif
310 switch ((osd_unit_e)osdConfig()->units) {
311 case OSD_UNIT_UK:
312 FALLTHROUGH;
313 case OSD_UNIT_IMPERIAL:
314 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_MILE, decimals, 3, digits)) {
315 buff[sym_index] = symbol_mi;
316 } else {
317 buff[sym_index] = symbol_ft;
319 buff[sym_index + 1] = '\0';
320 break;
321 case OSD_UNIT_METRIC_MPH:
322 FALLTHROUGH;
323 case OSD_UNIT_METRIC:
324 if (osdFormatCentiNumber(buff, dist, METERS_PER_KILOMETER, decimals, 3, digits)) {
325 buff[sym_index] = symbol_km;
326 } else {
327 buff[sym_index] = symbol_m;
329 buff[sym_index + 1] = '\0';
330 break;
331 case OSD_UNIT_GA:
332 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), (uint32_t)FEET_PER_NAUTICALMILE, decimals, 3, digits)) {
333 buff[sym_index] = symbol_nm;
334 } else {
335 buff[sym_index] = symbol_ft;
337 buff[sym_index + 1] = '\0';
338 break;
343 * Converts distance into a string based on the current unit system.
344 * @param dist Distance in centimeters
346 static void osdFormatDistanceStr(char *buff, int32_t dist)
348 int32_t centifeet;
349 switch ((osd_unit_e)osdConfig()->units) {
350 case OSD_UNIT_UK:
351 FALLTHROUGH;
352 case OSD_UNIT_IMPERIAL:
353 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
354 if (abs(centifeet) < FEET_PER_MILE * 100 / 2) {
355 // Show feet when dist < 0.5mi
356 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
357 } else {
358 // Show miles when dist >= 0.5mi
359 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100*FEET_PER_MILE)),
360 (abs(centifeet) % (100 * FEET_PER_MILE)) / FEET_PER_MILE, SYM_MI);
362 break;
363 case OSD_UNIT_METRIC_MPH:
364 FALLTHROUGH;
365 case OSD_UNIT_METRIC:
366 if (abs(dist) < METERS_PER_KILOMETER * 100) {
367 // Show meters when dist < 1km
368 tfp_sprintf(buff, "%d%c", (int)(dist / 100), SYM_M);
369 } else {
370 // Show kilometers when dist >= 1km
371 tfp_sprintf(buff, "%d.%02d%c", (int)(dist / (100*METERS_PER_KILOMETER)),
372 (abs(dist) % (100 * METERS_PER_KILOMETER)) / METERS_PER_KILOMETER, SYM_KM);
374 break;
375 case OSD_UNIT_GA:
376 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
377 if (abs(centifeet) < 100000) {
378 // Show feet when dist < 1000ft
379 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
380 } else {
381 // Show nautical miles when dist >= 1000ft
382 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100 * FEET_PER_NAUTICALMILE)),
383 (int)((abs(centifeet) % (int)(100 * FEET_PER_NAUTICALMILE)) / FEET_PER_NAUTICALMILE), SYM_NM);
385 break;
390 * Converts velocity based on the current unit system (kmh or mph).
391 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
393 static int32_t osdConvertVelocityToUnit(int32_t vel)
395 switch ((osd_unit_e)osdConfig()->units) {
396 case OSD_UNIT_UK:
397 FALLTHROUGH;
398 case OSD_UNIT_METRIC_MPH:
399 FALLTHROUGH;
400 case OSD_UNIT_IMPERIAL:
401 return CMSEC_TO_CENTIMPH(vel) / 100; // Convert to mph
402 case OSD_UNIT_METRIC:
403 return CMSEC_TO_CENTIKPH(vel) / 100; // Convert to kmh
404 case OSD_UNIT_GA:
405 return CMSEC_TO_CENTIKNOTS(vel) / 100; // Convert to Knots
407 // Unreachable
408 return -1;
412 * Converts velocity into a string based on the current unit system.
413 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
414 * @param _3D is a 3D velocity
415 * @param _max is a maximum velocity
417 void osdFormatVelocityStr(char* buff, int32_t vel, bool _3D, bool _max)
419 switch ((osd_unit_e)osdConfig()->units) {
420 case OSD_UNIT_UK:
421 FALLTHROUGH;
422 case OSD_UNIT_METRIC_MPH:
423 FALLTHROUGH;
424 case OSD_UNIT_IMPERIAL:
425 if (_max) {
426 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
427 } else {
428 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
430 break;
431 case OSD_UNIT_METRIC:
432 if (_max) {
433 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
434 } else {
435 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
437 break;
438 case OSD_UNIT_GA:
439 if (_max) {
440 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
441 } else {
442 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
444 break;
449 * 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
451 static void osdGenerateAverageVelocityStr(char* buff) {
452 uint32_t cmPerSec = getTotalTravelDistance() / getFlightTime();
453 osdFormatVelocityStr(buff, cmPerSec, false, false);
457 * Converts wind speed into a string based on the current unit system, using
458 * always 3 digits and an additional character for the unit at the right. buff
459 * is null terminated.
460 * @param ws Raw wind speed in cm/s
462 #ifdef USE_WIND_ESTIMATOR
463 static void osdFormatWindSpeedStr(char *buff, int32_t ws, bool isValid)
465 int32_t centivalue;
466 char suffix;
467 switch (osdConfig()->units) {
468 case OSD_UNIT_UK:
469 FALLTHROUGH;
470 case OSD_UNIT_METRIC_MPH:
471 FALLTHROUGH;
472 case OSD_UNIT_IMPERIAL:
473 centivalue = CMSEC_TO_CENTIMPH(ws);
474 suffix = SYM_MPH;
475 break;
476 case OSD_UNIT_GA:
477 centivalue = CMSEC_TO_CENTIKNOTS(ws);
478 suffix = SYM_KT;
479 break;
480 default:
481 case OSD_UNIT_METRIC:
482 centivalue = CMSEC_TO_CENTIKPH(ws);
483 suffix = SYM_KMH;
484 break;
487 osdFormatCentiNumber(buff, centivalue, 0, 2, 0, 3);
489 if (!isValid && ((millis() / 1000) % 4 < 2))
490 suffix = '*';
492 buff[3] = suffix;
493 buff[4] = '\0';
495 #endif
498 * This is a simplified altitude conversion code that does not use any scaling
499 * but is fully compatible with the DJI G2 MSP Displayport OSD implementation.
501 /* void osdSimpleAltitudeSymbol(char *buff, int32_t alt) {
503 int32_t convertedAltutude = 0;
504 char suffix = '\0';
506 switch ((osd_unit_e)osdConfig()->units) {
507 case OSD_UNIT_UK:
508 FALLTHROUGH;
509 case OSD_UNIT_GA:
510 FALLTHROUGH;
511 case OSD_UNIT_IMPERIAL:
512 convertedAltutude = CENTIMETERS_TO_FEET(alt);
513 suffix = SYM_ALT_FT;
514 break;
515 case OSD_UNIT_METRIC_MPH:
516 FALLTHROUGH;
517 case OSD_UNIT_METRIC:
518 convertedAltutude = CENTIMETERS_TO_METERS(alt);
519 suffix = SYM_ALT_M;
520 break;
523 tfp_sprintf(buff, "%4d", (int) convertedAltutude);
524 buff[4] = suffix;
525 buff[5] = '\0';
526 } */
529 * Converts altitude into a string based on the current unit system
530 * prefixed by a a symbol to indicate the unit used.
531 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
533 void osdFormatAltitudeSymbol(char *buff, int32_t alt)
535 uint8_t totalDigits = 4U;
536 uint8_t digits = 4U;
537 uint8_t symbolIndex = 4U;
538 uint8_t symbolKFt = SYM_ALT_KFT;
540 if (alt >= 0) {
541 digits = 3U;
542 buff[0] = ' ';
545 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
546 if (isBfCompatibleVideoSystem(osdConfig())) {
547 totalDigits++;
548 digits++;
549 symbolIndex++;
550 symbolKFt = SYM_ALT_FT;
552 #endif
554 switch ((osd_unit_e)osdConfig()->units) {
555 case OSD_UNIT_UK:
556 FALLTHROUGH;
557 case OSD_UNIT_GA:
558 FALLTHROUGH;
559 case OSD_UNIT_IMPERIAL:
560 if (osdFormatCentiNumber(buff + totalDigits - digits, CENTIMETERS_TO_CENTIFEET(alt), 1000, 0, 2, digits)) {
561 // Scaled to kft
562 buff[symbolIndex++] = symbolKFt;
563 } else {
564 // Formatted in feet
565 buff[symbolIndex++] = SYM_ALT_FT;
567 buff[symbolIndex] = '\0';
568 break;
569 case OSD_UNIT_METRIC_MPH:
570 FALLTHROUGH;
571 case OSD_UNIT_METRIC:
572 // alt is alredy in cm
573 if (osdFormatCentiNumber(buff + totalDigits - digits, alt, 1000, 0, 2, digits)) {
574 // Scaled to km
575 buff[symbolIndex++] = SYM_ALT_KM;
576 } else {
577 // Formatted in m
578 buff[symbolIndex++] = SYM_ALT_M;
580 buff[symbolIndex] = '\0';
581 break;
586 * Converts altitude into a string based on the current unit system.
587 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
589 static void osdFormatAltitudeStr(char *buff, int32_t alt)
591 int32_t value;
592 switch ((osd_unit_e)osdConfig()->units) {
593 case OSD_UNIT_UK:
594 FALLTHROUGH;
595 case OSD_UNIT_GA:
596 FALLTHROUGH;
597 case OSD_UNIT_IMPERIAL:
598 value = CENTIMETERS_TO_FEET(alt);
599 tfp_sprintf(buff, "%d%c", (int)value, SYM_FT);
600 break;
601 case OSD_UNIT_METRIC_MPH:
602 FALLTHROUGH;
603 case OSD_UNIT_METRIC:
604 value = CENTIMETERS_TO_METERS(alt);
605 tfp_sprintf(buff, "%d%c", (int)value, SYM_M);
606 break;
610 static void osdFormatTime(char *buff, uint32_t seconds, char sym_m, char sym_h)
612 uint32_t value = seconds;
613 char sym = sym_m;
614 // Maximum value we can show in minutes is 99 minutes and 59 seconds
615 if (seconds > (99 * 60) + 59) {
616 sym = sym_h;
617 value = seconds / 60;
619 buff[0] = sym;
620 tfp_sprintf(buff + 1, "%02d:%02d", (int)(value / 60), (int)(value % 60));
623 static inline void osdFormatOnTime(char *buff)
625 osdFormatTime(buff, micros() / 1000000, SYM_ON_M, SYM_ON_H);
628 static inline void osdFormatFlyTime(char *buff, textAttributes_t *attr)
630 uint32_t seconds = getFlightTime();
631 osdFormatTime(buff, seconds, SYM_FLY_M, SYM_FLY_H);
632 if (attr && osdConfig()->time_alarm > 0) {
633 if (seconds / 60 >= osdConfig()->time_alarm && ARMING_FLAG(ARMED)) {
634 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
639 /**
640 * Trim whitespace from string.
641 * Used in Stats screen on lines with multiple values.
643 char *osdFormatTrimWhiteSpace(char *buff)
645 char *end;
647 // Trim leading spaces
648 while(isspace((unsigned char)*buff)) buff++;
650 // All spaces?
651 if(*buff == 0)
652 return buff;
654 // Trim trailing spaces
655 end = buff + strlen(buff) - 1;
656 while(end > buff && isspace((unsigned char)*end)) end--;
658 // Write new null terminator character
659 end[1] = '\0';
661 return buff;
665 * Converts RSSI into a % value used by the OSD.
667 static uint16_t osdConvertRSSI(void)
669 // change range to [0, 99]
670 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE, 0, 99);
673 static uint16_t osdGetCrsfLQ(void)
675 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
676 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
677 int16_t displayedLQ = 0;
678 switch (osdConfig()->crsf_lq_format) {
679 case OSD_CRSF_LQ_TYPE1:
680 displayedLQ = statsLQ;
681 break;
682 case OSD_CRSF_LQ_TYPE2:
683 displayedLQ = statsLQ;
684 break;
685 case OSD_CRSF_LQ_TYPE3:
686 displayedLQ = rxLinkStatistics.rfMode >= 2 ? scaledLQ : statsLQ;
687 break;
689 return displayedLQ;
692 static int16_t osdGetCrsfdBm(void)
694 return rxLinkStatistics.uplinkRSSI;
697 * Displays a temperature postfixed with a symbol depending on the current unit system
698 * @param label to display
699 * @param valid true if measurement is valid
700 * @param temperature in deciDegrees Celcius
702 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)
704 char buff[TEMPERATURE_LABEL_LEN + 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN + 2];
705 textAttributes_t elemAttr = valid ? TEXT_ATTRIBUTES_NONE : _TEXT_ATTRIBUTES_BLINK_BIT;
706 uint8_t valueXOffset = 0;
708 if (symbol) {
709 buff[0] = symbol;
710 buff[1] = '\0';
711 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
712 valueXOffset = 1;
714 #ifdef USE_TEMPERATURE_SENSOR
715 else if (label[0] != '\0') {
716 uint8_t label_len = strnlen(label, TEMPERATURE_LABEL_LEN);
717 memcpy(buff, label, label_len);
718 memset(buff + label_len, ' ', TEMPERATURE_LABEL_LEN + 1 - label_len);
719 buff[5] = '\0';
720 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
721 valueXOffset = osdConfig()->temp_label_align == OSD_ALIGN_LEFT ? 5 : label_len + 1;
723 #else
724 UNUSED(label);
725 #endif
727 if (valid) {
729 if ((temperature <= alarm_min) || (temperature >= alarm_max)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
730 if (osdConfig()->units == OSD_UNIT_IMPERIAL) temperature = temperature * 9 / 5.0f + 320;
731 tfp_sprintf(buff, "%3d", temperature / 10);
733 } else
734 strcpy(buff, "---");
736 buff[3] = osdConfig()->units == OSD_UNIT_IMPERIAL ? SYM_TEMP_F : SYM_TEMP_C;
737 buff[4] = '\0';
739 displayWriteWithAttr(osdDisplayPort, elemPosX + valueXOffset, elemPosY, buff, elemAttr);
742 #ifdef USE_TEMPERATURE_SENSOR
743 static void osdDisplayTemperatureSensor(uint8_t elemPosX, uint8_t elemPosY, uint8_t sensorIndex)
745 int16_t temperature;
746 const bool valid = getSensorTemperature(sensorIndex, &temperature);
747 const tempSensorConfig_t *sensorConfig = tempSensorConfig(sensorIndex);
748 uint16_t symbol = sensorConfig->osdSymbol ? SYM_TEMP_SENSOR_FIRST + sensorConfig->osdSymbol - 1 : 0;
749 osdDisplayTemperature(elemPosX, elemPosY, symbol, sensorConfig->label, valid, temperature, sensorConfig->alarm_min, sensorConfig->alarm_max);
751 #endif
753 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
755 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
756 const int coordinateLength = osdConfig()->coordinate_digits + 1;
758 buff[0] = sym;
759 int32_t integerPart = val / GPS_DEGREES_DIVIDER;
760 // Latitude maximum integer width is 3 (-90) while
761 // longitude maximum integer width is 4 (-180).
762 int integerDigits = tfp_sprintf(buff + 1, (integerPart == 0 && val < 0) ? "-%d" : "%d", (int)integerPart);
763 // We can show up to 7 digits in decimalPart.
764 int32_t decimalPart = abs(val % (int)GPS_DEGREES_DIVIDER);
765 STATIC_ASSERT(GPS_DEGREES_DIVIDER == 1e7, adjust_max_decimal_digits);
766 int decimalDigits;
767 bool bfcompat = false; // Assume BFCOMPAT mode is no enabled
769 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
770 if(isBfCompatibleVideoSystem(osdConfig())) {
771 bfcompat = true;
773 #endif
775 if (!bfcompat) {
776 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, "%07d", (int)decimalPart);
777 // Embbed the decimal separator
778 buff[1 + integerDigits - 1] += SYM_ZERO_HALF_TRAILING_DOT - '0';
779 buff[1 + integerDigits] += SYM_ZERO_HALF_LEADING_DOT - '0';
780 } else {
781 // BFCOMPAT mode enabled
782 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, ".%06d", (int)decimalPart);
784 // Fill up to coordinateLength with zeros
785 int total = 1 + integerDigits + decimalDigits;
786 while(total < coordinateLength) {
787 buff[total] = '0';
788 total++;
790 buff[coordinateLength] = '\0';
793 static void osdFormatCraftName(char *buff)
795 if (strlen(systemConfig()->craftName) == 0)
796 strcpy(buff, "CRAFT_NAME");
797 else {
798 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
799 buff[i] = sl_toupper((unsigned char)systemConfig()->craftName[i]);
800 if (systemConfig()->craftName[i] == 0)
801 break;
806 void osdFormatPilotName(char *buff)
808 if (strlen(systemConfig()->pilotName) == 0)
809 strcpy(buff, "PILOT_NAME");
810 else {
811 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
812 buff[i] = sl_toupper((unsigned char)systemConfig()->pilotName[i]);
813 if (systemConfig()->pilotName[i] == 0)
814 break;
819 static const char * osdArmingDisabledReasonMessage(void)
821 const char *message = NULL;
822 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
824 switch (isArmingDisabledReason()) {
825 case ARMING_DISABLED_FAILSAFE_SYSTEM:
826 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
827 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING) {
828 if (failsafeIsReceivingRxData()) {
829 // If we're not using sticks, it means the ARM switch
830 // hasn't been off since entering FAILSAFE_RX_LOSS_MONITORING
831 // yet
832 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF);
834 // Not receiving RX data
835 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
837 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS);
838 case ARMING_DISABLED_NOT_LEVEL:
839 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL);
840 case ARMING_DISABLED_SENSORS_CALIBRATING:
841 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL);
842 case ARMING_DISABLED_SYSTEM_OVERLOADED:
843 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED);
844 case ARMING_DISABLED_NAVIGATION_UNSAFE:
845 // Check the exact reason
846 switch (navigationIsBlockingArming(NULL)) {
847 char buf[6];
848 case NAV_ARMING_BLOCKER_NONE:
849 break;
850 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX:
851 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX);
852 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE:
853 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST);
854 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR:
855 osdFormatDistanceSymbol(buf, distanceToFirstWP(), 0);
856 tfp_sprintf(messageBuf, "FIRST WP TOO FAR (%s)", buf);
857 return message = messageBuf;
858 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR:
859 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG);
861 break;
862 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED:
863 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL);
864 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED:
865 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL);
866 case ARMING_DISABLED_ARM_SWITCH:
867 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST);
868 case ARMING_DISABLED_HARDWARE_FAILURE:
870 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
871 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE);
873 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
874 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL);
876 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
877 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL);
879 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
880 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL);
882 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
883 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL);
885 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
886 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL);
888 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
889 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL);
892 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL);
893 case ARMING_DISABLED_BOXFAILSAFE:
894 return OSD_MESSAGE_STR(OSD_MSG_FS_EN);
895 case ARMING_DISABLED_BOXKILLSWITCH:
896 return OSD_MESSAGE_STR(OSD_MSG_KILL_SW_EN);
897 case ARMING_DISABLED_RC_LINK:
898 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK);
899 case ARMING_DISABLED_THROTTLE:
900 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW);
901 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED:
902 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER);
903 case ARMING_DISABLED_SERVO_AUTOTRIM:
904 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE);
905 case ARMING_DISABLED_OOM:
906 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY);
907 case ARMING_DISABLED_INVALID_SETTING:
908 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
909 case ARMING_DISABLED_CLI:
910 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE);
911 case ARMING_DISABLED_PWM_OUTPUT_ERROR:
912 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR);
913 case ARMING_DISABLED_NO_PREARM:
914 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM);
915 case ARMING_DISABLED_DSHOT_BEEPER:
916 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER);
917 // Cases without message
918 case ARMING_DISABLED_LANDING_DETECTED:
919 FALLTHROUGH;
920 case ARMING_DISABLED_CMS_MENU:
921 FALLTHROUGH;
922 case ARMING_DISABLED_OSD_MENU:
923 FALLTHROUGH;
924 case ARMING_DISABLED_ALL_FLAGS:
925 FALLTHROUGH;
926 case ARMED:
927 FALLTHROUGH;
928 case SIMULATOR_MODE_HITL:
929 FALLTHROUGH;
930 case SIMULATOR_MODE_SITL:
931 FALLTHROUGH;
932 case WAS_EVER_ARMED:
933 break;
935 return NULL;
938 static const char * osdFailsafePhaseMessage(void)
940 // See failsafe.h for each phase explanation
941 switch (failsafePhase()) {
942 case FAILSAFE_RETURN_TO_HOME:
943 // XXX: Keep this in sync with OSD_FLYMODE.
944 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS);
945 case FAILSAFE_LANDING:
946 // This should be considered an emergengy landing
947 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS);
948 case FAILSAFE_RX_LOSS_MONITORING:
949 // Only reachable from FAILSAFE_LANDED, which performs
950 // a disarm. Since aircraft has been disarmed, we no
951 // longer show failsafe details.
952 FALLTHROUGH;
953 case FAILSAFE_LANDED:
954 // Very brief, disarms and transitions into
955 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
956 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
957 // so we'll show the user how to re-arm in when
958 // that flag is the reason to prevent arming.
959 FALLTHROUGH;
960 case FAILSAFE_RX_LOSS_IDLE:
961 // This only happens when user has chosen NONE as FS
962 // procedure. The recovery messages should be enough.
963 FALLTHROUGH;
964 case FAILSAFE_IDLE:
965 // Failsafe not active
966 FALLTHROUGH;
967 case FAILSAFE_RX_LOSS_DETECTED:
968 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
969 // or the FS procedure immediately.
970 FALLTHROUGH;
971 case FAILSAFE_RX_LOSS_RECOVERED:
972 // Exiting failsafe
973 break;
975 return NULL;
978 static const char * osdFailsafeInfoMessage(void)
980 if (failsafeIsReceivingRxData()) {
981 // User must move sticks to exit FS mode
982 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS);
984 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
986 #if defined(USE_SAFE_HOME)
987 static const char * divertingToSafehomeMessage(void)
989 if (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && safehome_applied) {
990 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME);
992 return NULL;
994 #endif
996 static const char * navigationStateMessage(void)
998 switch (NAV_Status.state) {
999 case MW_NAV_STATE_NONE:
1000 break;
1001 case MW_NAV_STATE_RTH_START:
1002 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH);
1003 case MW_NAV_STATE_RTH_CLIMB:
1004 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB);
1005 case MW_NAV_STATE_RTH_ENROUTE:
1006 if (posControl.flags.rthTrackbackActive) {
1007 return OSD_MESSAGE_STR(OSD_MSG_RTH_TRACKBACK);
1008 } else {
1009 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME);
1011 case MW_NAV_STATE_HOLD_INFINIT:
1012 // Used by HOLD flight modes. No information to add.
1013 break;
1014 case MW_NAV_STATE_HOLD_TIMED:
1015 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
1016 break;
1017 case MW_NAV_STATE_WP_ENROUTE:
1018 // "TO WP" + WP countdown added in osdGetSystemMessage
1019 break;
1020 case MW_NAV_STATE_PROCESS_NEXT:
1021 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP);
1022 case MW_NAV_STATE_DO_JUMP:
1023 // Not used
1024 break;
1025 case MW_NAV_STATE_LAND_START:
1026 // Not used
1027 break;
1028 case MW_NAV_STATE_EMERGENCY_LANDING:
1029 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING);
1030 case MW_NAV_STATE_LAND_IN_PROGRESS:
1031 return OSD_MESSAGE_STR(OSD_MSG_LANDING);
1032 case MW_NAV_STATE_HOVER_ABOVE_HOME:
1033 if (STATE(FIXED_WING_LEGACY)) {
1034 #if defined(USE_SAFE_HOME)
1035 if (safehome_applied) {
1036 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME);
1038 #endif
1039 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME);
1041 return OSD_MESSAGE_STR(OSD_MSG_HOVERING);
1042 case MW_NAV_STATE_LANDED:
1043 return OSD_MESSAGE_STR(OSD_MSG_LANDED);
1044 case MW_NAV_STATE_LAND_SETTLE:
1045 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND);
1046 case MW_NAV_STATE_LAND_START_DESCENT:
1047 // Not used
1048 break;
1050 return NULL;
1053 static void osdFormatMessage(char *buff, size_t size, const char *message, bool isCenteredText)
1055 // String is always filled with Blanks
1056 memset(buff, SYM_BLANK, size);
1057 if (message) {
1058 size_t messageLength = strlen(message);
1059 int rem = isCenteredText ? MAX(0, (int)size - (int)messageLength) : 0;
1060 strncpy(buff + rem / 2, message, MIN((int)size - rem / 2, (int)messageLength));
1062 // Ensure buff is zero terminated
1063 buff[size] = '\0';
1067 * Draws the battery symbol filled in accordingly to the
1068 * battery voltage to buff[0].
1070 static void osdFormatBatteryChargeSymbol(char *buff)
1072 uint8_t p = calculateBatteryPercentage();
1073 p = (100 - p) / 16.6;
1074 buff[0] = SYM_BATT_FULL + p;
1077 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t *attr)
1079 const batteryState_e batteryState = getBatteryState();
1081 if (batteryState == BATTERY_WARNING || batteryState == BATTERY_CRITICAL) {
1082 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
1086 void osdCrosshairPosition(uint8_t *x, uint8_t *y)
1088 *x = osdDisplayPort->cols / 2;
1089 *y = osdDisplayPort->rows / 2;
1090 *y -= osdConfig()->horizon_offset; // positive horizon_offset moves the HUD up, negative moves down
1094 * Check if this OSD layout is using scaled or unscaled throttle.
1095 * If both are used, it will default to scaled.
1097 bool osdUsingScaledThrottle(void)
1099 bool usingScaledThrottle = OSD_VISIBLE(osdLayoutsConfig()->item_pos[currentLayout][OSD_SCALED_THROTTLE_POS]);
1100 bool usingRCThrottle = OSD_VISIBLE(osdLayoutsConfig()->item_pos[currentLayout][OSD_THROTTLE_POS]);
1102 if (!usingScaledThrottle && !usingRCThrottle)
1103 usingScaledThrottle = true;
1105 return usingScaledThrottle;
1109 * Formats throttle position prefixed by its symbol.
1110 * Shows unscaled or scaled (output to motor) throttle percentage
1112 static void osdFormatThrottlePosition(char *buff, bool useScaled, textAttributes_t *elemAttr)
1114 if (useScaled) {
1115 buff[0] = SYM_SCALE;
1116 } else {
1117 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 tfp_sprintf(buff + 2, "%3d", getThrottlePercent(useScaled));
1137 * Formats gvars prefixed by its number (0-indexed). If autoThr
1139 static void osdFormatGVar(char *buff, uint8_t index)
1141 buff[0] = 'G';
1142 buff[1] = '0'+index;
1143 buff[2] = ':';
1144 #ifdef USE_PROGRAMMING_FRAMEWORK
1145 osdFormatCentiNumber(buff + 3, (int32_t)gvGet(index)*(int32_t)100, 1, 0, 0, 5);
1146 #endif
1149 #if defined(USE_ESC_SENSOR)
1150 static void osdFormatRpm(char *buff, uint32_t rpm)
1152 buff[0] = SYM_RPM;
1153 if (rpm) {
1154 if ( digitCount(rpm) > osdConfig()->esc_rpm_precision) {
1155 uint8_t rpmMaxDecimals = (osdConfig()->esc_rpm_precision - 3);
1156 osdFormatCentiNumber(buff + 1, rpm / 10, 0, rpmMaxDecimals, rpmMaxDecimals, osdConfig()->esc_rpm_precision-1);
1157 buff[osdConfig()->esc_rpm_precision] = 'K';
1158 buff[osdConfig()->esc_rpm_precision+1] = '\0';
1160 else {
1161 switch(osdConfig()->esc_rpm_precision) {
1162 case 6:
1163 tfp_sprintf(buff + 1, "%6lu", rpm);
1164 break;
1165 case 5:
1166 tfp_sprintf(buff + 1, "%5lu", rpm);
1167 break;
1168 case 4:
1169 tfp_sprintf(buff + 1, "%4lu", rpm);
1170 break;
1171 case 3:
1172 default:
1173 tfp_sprintf(buff + 1, "%3lu", rpm);
1174 break;
1180 else {
1181 uint8_t buffPos = 1;
1182 while (buffPos <=( osdConfig()->esc_rpm_precision)) {
1183 strcpy(buff + buffPos++, "-");
1187 #endif
1189 int32_t osdGetAltitude(void)
1191 return getEstimatedActualPosition(Z);
1194 static inline int32_t osdGetAltitudeMsl(void)
1196 return getEstimatedActualPosition(Z)+GPS_home.alt;
1199 uint16_t osdGetRemainingGlideTime(void) {
1200 float value = getEstimatedActualVelocity(Z);
1201 static pt1Filter_t glideTimeFilterState;
1202 const timeMs_t curTimeMs = millis();
1203 static timeMs_t glideTimeUpdatedMs;
1205 value = pt1FilterApply4(&glideTimeFilterState, isnormal(value) ? value : 0, 0.5, MS2S(curTimeMs - glideTimeUpdatedMs));
1206 glideTimeUpdatedMs = curTimeMs;
1208 if (value < 0) {
1209 value = osdGetAltitude() / abs((int)value);
1210 } else {
1211 value = 0;
1214 return (uint16_t)roundf(value);
1217 static bool osdIsHeadingValid(void)
1219 return isImuHeadingValid();
1222 int16_t osdGetHeading(void)
1224 return attitude.values.yaw;
1227 int16_t osdGetPanServoOffset(void)
1229 int8_t servoIndex = osdConfig()->pan_servo_index;
1230 int16_t servoPosition = servo[servoIndex];
1231 int16_t servoMiddle = servoParams(servoIndex)->middle;
1232 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition - servoMiddle) * osdConfig()->pan_servo_pwm2centideg);
1235 // Returns a heading angle in degrees normalized to [0, 360).
1236 int osdGetHeadingAngle(int angle)
1238 while (angle < 0) {
1239 angle += 360;
1241 while (angle >= 360) {
1242 angle -= 360;
1244 return angle;
1247 #if defined(USE_GPS)
1249 /* Draws a map with the given symbol in the center and given point of interest
1250 * defined by its distance in meters and direction in degrees.
1251 * referenceHeading indicates the up direction in the map, in degrees, while
1252 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1253 * arrow to indicate the map reference to the user. The drawn argument is an
1254 * in-out used to store the last position where the craft was drawn to avoid
1255 * erasing all screen on each redraw.
1257 static void osdDrawMap(int referenceHeading, uint16_t referenceSym, uint16_t centerSym,
1258 uint32_t poiDistance, int16_t poiDirection, uint16_t poiSymbol,
1259 uint16_t *drawn, uint32_t *usedScale)
1261 // TODO: These need to be tested with several setups. We might
1262 // need to make them configurable.
1263 const int hMargin = 5;
1264 const int vMargin = 3;
1266 // TODO: Get this from the display driver?
1267 const int charWidth = 12;
1268 const int charHeight = 18;
1270 uint8_t minX = hMargin;
1271 uint8_t maxX = osdDisplayPort->cols - 1 - hMargin;
1272 uint8_t minY = vMargin;
1273 uint8_t maxY = osdDisplayPort->rows - 1 - vMargin;
1274 uint8_t midX = osdDisplayPort->cols / 2;
1275 uint8_t midY = osdDisplayPort->rows / 2;
1277 // Fixed marks
1278 displayWriteChar(osdDisplayPort, midX, midY, centerSym);
1280 // First, erase the previous drawing.
1281 if (OSD_VISIBLE(*drawn)) {
1282 displayWriteChar(osdDisplayPort, OSD_X(*drawn), OSD_Y(*drawn), SYM_BLANK);
1283 *drawn = 0;
1286 uint32_t initialScale;
1287 const unsigned scaleMultiplier = 2;
1288 // We try to reduce the scale when the POI will be around half the distance
1289 // between the center and the closers map edge, to avoid too much jumping
1290 const int scaleReductionMultiplier = MIN(midX - hMargin, midY - vMargin) / 2;
1292 switch (osdConfig()->units) {
1293 case OSD_UNIT_UK:
1294 FALLTHROUGH;
1295 case OSD_UNIT_IMPERIAL:
1296 initialScale = 16; // 16m ~= 0.01miles
1297 break;
1298 case OSD_UNIT_GA:
1299 initialScale = 18; // 18m ~= 0.01 nautical miles
1300 break;
1301 default:
1302 case OSD_UNIT_METRIC_MPH:
1303 FALLTHROUGH;
1304 case OSD_UNIT_METRIC:
1305 initialScale = 10; // 10m as initial scale
1306 break;
1309 // Try to keep the same scale when getting closer until we draw over the center point
1310 uint32_t scale = initialScale;
1311 if (*usedScale) {
1312 scale = *usedScale;
1313 if (scale > initialScale && poiDistance < *usedScale * scaleReductionMultiplier) {
1314 scale /= scaleMultiplier;
1318 if (STATE(GPS_FIX)) {
1320 int directionToPoi = osdGetHeadingAngle(poiDirection - referenceHeading);
1321 float poiAngle = DEGREES_TO_RADIANS(directionToPoi);
1322 float poiSin = sin_approx(poiAngle);
1323 float poiCos = cos_approx(poiAngle);
1325 // Now start looking for a valid scale that lets us draw everything
1326 int ii;
1327 for (ii = 0; ii < 50; ii++) {
1328 // Calculate location of the aircraft in map
1329 int points = poiDistance / ((float)scale / charHeight);
1331 float pointsX = points * poiSin;
1332 int poiX = midX - roundf(pointsX / charWidth);
1333 if (poiX < minX || poiX > maxX) {
1334 scale *= scaleMultiplier;
1335 continue;
1338 float pointsY = points * poiCos;
1339 int poiY = midY + roundf(pointsY / charHeight);
1340 if (poiY < minY || poiY > maxY) {
1341 scale *= scaleMultiplier;
1342 continue;
1345 if (poiX == midX && poiY == midY) {
1346 // We're over the map center symbol, so we would be drawing
1347 // over it even if we increased the scale. Alternate between
1348 // drawing the center symbol or drawing the POI.
1349 if (centerSym != SYM_BLANK && OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1350 break;
1352 } else {
1354 uint16_t c;
1355 if (displayReadCharWithAttr(osdDisplayPort, poiX, poiY, &c, NULL) && c != SYM_BLANK) {
1356 // Something else written here, increase scale. If the display doesn't support reading
1357 // back characters, we assume there's nothing.
1359 // If we're close to the center, decrease scale. Otherwise increase it.
1360 uint8_t centerDeltaX = (maxX - minX) / (scaleMultiplier * 2);
1361 uint8_t centerDeltaY = (maxY - minY) / (scaleMultiplier * 2);
1362 if (poiX >= midX - centerDeltaX && poiX <= midX + centerDeltaX &&
1363 poiY >= midY - centerDeltaY && poiY <= midY + centerDeltaY &&
1364 scale > scaleMultiplier) {
1366 scale /= scaleMultiplier;
1367 } else {
1368 scale *= scaleMultiplier;
1370 continue;
1374 // Draw the point on the map
1375 if (poiSymbol == SYM_ARROW_UP) {
1376 // Drawing aircraft, rotate
1377 int mapHeading = osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading);
1378 poiSymbol += mapHeading * 2 / 45;
1380 displayWriteChar(osdDisplayPort, poiX, poiY, poiSymbol);
1382 // Update saved location
1383 *drawn = OSD_POS(poiX, poiY) | OSD_VISIBLE_FLAG;
1384 break;
1388 *usedScale = scale;
1390 // Update global map data for scale and reference
1391 osdMapData.scale = scale;
1392 osdMapData.referenceSymbol = referenceSym;
1395 /* Draws a map with the home in the center and the craft moving around.
1396 * See osdDrawMap() for reference.
1398 static void osdDrawHomeMap(int referenceHeading, uint8_t referenceSym, uint16_t *drawn, uint32_t *usedScale)
1400 osdDrawMap(referenceHeading, referenceSym, SYM_HOME, GPS_distanceToHome, GPS_directionToHome, SYM_ARROW_UP, drawn, usedScale);
1403 /* Draws a map with the aircraft in the center and the home moving around.
1404 * See osdDrawMap() for reference.
1406 static void osdDrawRadar(uint16_t *drawn, uint32_t *usedScale)
1408 int16_t reference = DECIDEGREES_TO_DEGREES(osdGetHeading());
1409 int16_t poiDirection = osdGetHeadingAngle(GPS_directionToHome + 180);
1410 osdDrawMap(reference, 0, SYM_ARROW_UP, GPS_distanceToHome, poiDirection, SYM_HOME, drawn, usedScale);
1413 static uint16_t crc_accumulate(uint8_t data, uint16_t crcAccum)
1415 uint8_t tmp;
1416 tmp = data ^ (uint8_t)(crcAccum & 0xff);
1417 tmp ^= (tmp << 4);
1418 crcAccum = (crcAccum >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
1419 return crcAccum;
1423 static void osdDisplayTelemetry(void)
1425 uint32_t trk_data;
1426 uint16_t trk_crc = 0;
1427 char trk_buffer[31];
1428 static int16_t trk_elevation = 127;
1429 static uint16_t trk_bearing = 0;
1431 if (ARMING_FLAG(ARMED)) {
1432 if (STATE(GPS_FIX)){
1433 if (GPS_distanceToHome > 5) {
1434 trk_bearing = GPS_directionToHome;
1435 trk_bearing += 360 + 180;
1436 trk_bearing %= 360;
1437 int32_t alt = CENTIMETERS_TO_METERS(osdGetAltitude());
1438 float at = atan2(alt, GPS_distanceToHome);
1439 trk_elevation = at * 57.2957795f; // 57.2957795 = 1 rad
1440 trk_elevation += 37; // because elevation in telemetry should be from -37 to 90
1441 if (trk_elevation < 0) {
1442 trk_elevation = 0;
1447 else{
1448 trk_elevation = 127;
1449 trk_bearing = 0;
1452 trk_data = 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1453 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.
1454 trk_data = trk_data | (uint32_t)trk_bearing << 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1455 trk_crc = crc_accumulate(0xFF & trk_data, trk_crc); // CRC First Byte bits 0-7
1456 trk_crc = crc_accumulate(0xFF & trk_bearing, trk_crc); // CRC Second Byte bits 8-15
1457 trk_crc = crc_accumulate(trk_bearing >> 8, trk_crc); // CRC Third Byte bits 16-17
1458 trk_data = trk_data | (uint32_t)trk_crc << 17; // bits 18-29 CRC & 0x3FFFF
1460 for (uint8_t t_ctr = 0; t_ctr < 30; t_ctr++) { // Prepare screen buffer and write data line.
1461 if (trk_data & (uint32_t)1 << t_ctr){
1462 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_0;
1464 else{
1465 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_1;
1468 trk_buffer[30] = 0;
1469 displayWrite(osdDisplayPort, 0, 0, trk_buffer);
1470 if (osdConfig()->telemetry>1){
1471 displayWrite(osdDisplayPort, 0, 3, trk_buffer); // Test display because normal telemetry line is not visible
1474 #endif
1476 static void osdFormatPidControllerOutput(char *buff, const char *label, const pidController_t *pidController, uint8_t scale, bool showDecimal) {
1477 strcpy(buff, label);
1478 for (uint8_t i = strlen(label); i < 5; ++i) buff[i] = ' ';
1479 uint8_t decimals = showDecimal ? 1 : 0;
1480 osdFormatCentiNumber(buff + 5, pidController->proportional * scale, 0, decimals, 0, 4);
1481 buff[9] = ' ';
1482 osdFormatCentiNumber(buff + 10, pidController->integrator * scale, 0, decimals, 0, 4);
1483 buff[14] = ' ';
1484 osdFormatCentiNumber(buff + 15, pidController->derivative * scale, 0, decimals, 0, 4);
1485 buff[19] = ' ';
1486 osdFormatCentiNumber(buff + 20, pidController->output_constrained * scale, 0, decimals, 0, 4);
1487 buff[24] = '\0';
1490 static void osdDisplayBatteryVoltage(uint8_t elemPosX, uint8_t elemPosY, uint16_t voltage, uint8_t digits, uint8_t decimals)
1492 char buff[7];
1493 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1495 osdFormatBatteryChargeSymbol(buff);
1496 buff[1] = '\0';
1497 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1498 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1500 elemAttr = TEXT_ATTRIBUTES_NONE;
1501 digits = MIN(digits, 5);
1502 osdFormatCentiNumber(buff, voltage, 0, decimals, 0, digits);
1503 buff[digits] = SYM_VOLT;
1504 buff[digits+1] = '\0';
1505 const batteryState_e batteryVoltageState = checkBatteryVoltageState();
1506 if (batteryVoltageState == BATTERY_CRITICAL || batteryVoltageState == BATTERY_WARNING) {
1507 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1509 displayWriteWithAttr(osdDisplayPort, elemPosX + 1, elemPosY, buff, elemAttr);
1512 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)
1514 textAttributes_t elemAttr;
1515 char buff[4];
1517 const pid8_t *pid = &pidBank()->pid[pidIndex];
1518 pidType_e pidType = pidIndexGetType(pidIndex);
1520 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1522 if (pidType == PID_TYPE_NONE) {
1523 // PID is not used in this configuration. Draw dashes.
1524 // XXX: Keep this in sync with the %3d format and spacing used below
1525 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - - -");
1526 return;
1529 elemAttr = TEXT_ATTRIBUTES_NONE;
1530 tfp_sprintf(buff, "%3d", pid->P);
1531 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1532 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1533 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1535 elemAttr = TEXT_ATTRIBUTES_NONE;
1536 tfp_sprintf(buff, "%3d", pid->I);
1537 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1538 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1539 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1541 elemAttr = TEXT_ATTRIBUTES_NONE;
1542 tfp_sprintf(buff, "%3d", pid->D);
1543 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1544 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1545 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1547 elemAttr = TEXT_ATTRIBUTES_NONE;
1548 tfp_sprintf(buff, "%3d", pid->FF);
1549 if ((isAdjustmentFunctionSelected(adjFuncFF)) || (((adjFuncFF == ADJUSTMENT_ROLL_FF) || (adjFuncFF == ADJUSTMENT_PITCH_FF)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF))))
1550 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1551 displayWriteWithAttr(osdDisplayPort, elemPosX + 16, elemPosY, buff, elemAttr);
1554 static void osdDisplayNavPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD)
1556 textAttributes_t elemAttr;
1557 char buff[4];
1559 const pid8_t *pid = &pidBank()->pid[pidIndex];
1560 pidType_e pidType = pidIndexGetType(pidIndex);
1562 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1564 if (pidType == PID_TYPE_NONE) {
1565 // PID is not used in this configuration. Draw dashes.
1566 // XXX: Keep this in sync with the %3d format and spacing used below
1567 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - -");
1568 return;
1571 elemAttr = TEXT_ATTRIBUTES_NONE;
1572 tfp_sprintf(buff, "%3d", pid->P);
1573 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1574 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1575 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1577 elemAttr = TEXT_ATTRIBUTES_NONE;
1578 tfp_sprintf(buff, "%3d", pid->I);
1579 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1580 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1581 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1583 elemAttr = TEXT_ATTRIBUTES_NONE;
1584 tfp_sprintf(buff, "%3d", pidType == PID_TYPE_PIFF ? pid->FF : pid->D);
1585 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1586 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1587 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1590 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) {
1591 char buff[8];
1592 textAttributes_t elemAttr;
1593 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1595 elemAttr = TEXT_ATTRIBUTES_NONE;
1596 osdFormatCentiNumber(buff, value * 100, 0, maxDecimals, 0, MIN(valueLength, 8));
1597 if (isAdjustmentFunctionSelected(adjFunc))
1598 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1599 displayWriteWithAttr(osdDisplayPort, elemPosX + strlen(str) + 1 + valueOffset, elemPosY, buff, elemAttr);
1602 int8_t getGeoWaypointNumber(int8_t waypointIndex)
1604 static int8_t lastWaypointIndex = 1;
1605 static int8_t geoWaypointIndex;
1607 if (waypointIndex != lastWaypointIndex) {
1608 lastWaypointIndex = geoWaypointIndex = waypointIndex;
1609 for (uint8_t i = posControl.startWpIndex; i <= waypointIndex; i++) {
1610 if (posControl.waypointList[i].action == NAV_WP_ACTION_SET_POI ||
1611 posControl.waypointList[i].action == NAV_WP_ACTION_SET_HEAD ||
1612 posControl.waypointList[i].action == NAV_WP_ACTION_JUMP) {
1613 geoWaypointIndex -= 1;
1618 return geoWaypointIndex - posControl.startWpIndex + 1;
1621 void osdDisplaySwitchIndicator(const char *swName, int rcValue, char *buff) {
1622 int8_t ptr = 0;
1624 if (osdConfig()->osd_switch_indicators_align_left) {
1625 for (ptr = 0; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH); ptr++) {
1626 buff[ptr] = swName[ptr];
1629 if ( rcValue < 1333) {
1630 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1631 } else if ( rcValue > 1666) {
1632 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1633 } else {
1634 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1636 } else {
1637 if ( rcValue < 1333) {
1638 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1639 } else if ( rcValue > 1666) {
1640 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1641 } else {
1642 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1645 for (ptr = 1; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH) + 1; ptr++) {
1646 buff[ptr] = swName[ptr-1];
1649 ptr++;
1652 buff[ptr] = '\0';
1655 static bool osdDrawSingleElement(uint8_t item)
1657 uint16_t pos = osdLayoutsConfig()->item_pos[currentLayout][item];
1658 if (!OSD_VISIBLE(pos)) {
1659 return false;
1661 uint8_t elemPosX = OSD_X(pos);
1662 uint8_t elemPosY = OSD_Y(pos);
1663 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1664 char buff[32] = {0};
1666 switch (item) {
1667 case OSD_RSSI_VALUE:
1669 uint16_t osdRssi = osdConvertRSSI();
1670 buff[0] = SYM_RSSI;
1671 tfp_sprintf(buff + 1, "%2d", osdRssi);
1672 if (osdRssi < osdConfig()->rssi_alarm) {
1673 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1675 break;
1678 case OSD_MAIN_BATT_VOLTAGE: {
1679 uint8_t base_digits = 2U;
1680 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1681 if(isBfCompatibleVideoSystem(osdConfig())) {
1682 base_digits = 3U; // Add extra digit to account for decimal point taking an extra character space
1684 #endif
1685 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawVoltage(), base_digits + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1686 return true;
1689 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE: {
1690 uint8_t base_digits = 2U;
1691 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1692 if(isBfCompatibleVideoSystem(osdConfig())) {
1693 base_digits = 3U; // Add extra digit to account for decimal point taking an extra character space
1695 #endif
1696 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedVoltage(), base_digits + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1697 return true;
1700 case OSD_CURRENT_DRAW: {
1701 osdFormatCentiNumber(buff, getAmperage(), 0, 2, 0, 3);
1702 buff[3] = SYM_AMP;
1703 buff[4] = '\0';
1705 uint8_t current_alarm = osdConfig()->current_alarm;
1706 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
1707 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1709 break;
1712 case OSD_MAH_DRAWN: {
1713 uint8_t mah_digits = osdConfig()->mAh_precision; // Initialize to config value
1715 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1716 if (isBfCompatibleVideoSystem(osdConfig())) {
1717 //BFcompat is unable to work with scaled values and it only has mAh symbol to work with
1718 tfp_sprintf(buff, "%5d", (int)getMAhDrawn()); // Use 5 digits to allow packs below 100Ah
1719 buff[5] = SYM_MAH;
1720 buff[6] = '\0';
1721 } else
1722 #endif
1724 if (osdFormatCentiNumber(buff, getMAhDrawn() * 100, 1000, 0, (mah_digits - 2), mah_digits)) {
1725 // Shown in Ah
1726 buff[mah_digits] = SYM_AH;
1727 } else {
1728 // Shown in mAh
1729 buff[mah_digits] = SYM_MAH;
1731 buff[mah_digits + 1] = '\0';
1734 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1735 break;
1738 case OSD_WH_DRAWN:
1739 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
1740 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1741 buff[3] = SYM_WH;
1742 buff[4] = '\0';
1743 break;
1745 case OSD_BATTERY_REMAINING_CAPACITY:
1747 bool unitsDrawn = false;
1749 if (currentBatteryProfile->capacity.value == 0)
1750 tfp_sprintf(buff, " NA");
1751 else if (!batteryWasFullWhenPluggedIn())
1752 tfp_sprintf(buff, " NF");
1753 else if (currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH) {
1754 uint8_t mah_digits = osdConfig()->mAh_precision; // Initialize to config value
1756 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1757 if (isBfCompatibleVideoSystem(osdConfig())) {
1758 //BFcompat is unable to work with scaled values and it only has mAh symbol to work with
1759 tfp_sprintf(buff, "%5d", (int)getBatteryRemainingCapacity()); // Use 5 digits to allow packs below 100Ah
1760 buff[5] = SYM_MAH;
1761 buff[6] = '\0';
1762 unitsDrawn = true;
1763 } else
1764 #endif
1766 if (osdFormatCentiNumber(buff, getBatteryRemainingCapacity() * 100, 1000, 0, (mah_digits - 2), mah_digits)) {
1767 // Shown in Ah
1768 buff[mah_digits] = SYM_AH;
1769 } else {
1770 // Shown in mAh
1771 buff[mah_digits] = SYM_MAH;
1773 buff[mah_digits + 1] = '\0';
1774 unitsDrawn = true;
1776 } else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1777 osdFormatCentiNumber(buff + 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3);
1779 if (!unitsDrawn) {
1780 buff[4] = currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH ? SYM_MAH : SYM_WH;
1781 buff[5] = '\0';
1784 if (batteryUsesCapacityThresholds()) {
1785 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1788 break;
1790 case OSD_BATTERY_REMAINING_PERCENT:
1791 osdFormatBatteryChargeSymbol(buff);
1792 tfp_sprintf(buff + 1, "%3d%%", calculateBatteryPercentage());
1793 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1794 break;
1796 case OSD_POWER_SUPPLY_IMPEDANCE:
1797 if (isPowerSupplyImpedanceValid())
1798 tfp_sprintf(buff, "%3d", getPowerSupplyImpedance());
1799 else
1800 strcpy(buff, "---");
1801 buff[3] = SYM_MILLIOHM;
1802 buff[4] = '\0';
1803 break;
1805 #ifdef USE_GPS
1806 case OSD_GPS_SATS:
1807 buff[0] = SYM_SAT_L;
1808 buff[1] = SYM_SAT_R;
1809 tfp_sprintf(buff + 2, "%2d", gpsSol.numSat);
1810 if (!STATE(GPS_FIX)) {
1811 if (getHwGPSStatus() == HW_SENSOR_UNAVAILABLE || getHwGPSStatus() == HW_SENSOR_UNHEALTHY) {
1812 strcpy(buff + 2, "X!");
1814 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1816 break;
1818 case OSD_GPS_SPEED:
1819 osdFormatVelocityStr(buff, gpsSol.groundSpeed, false, false);
1820 break;
1822 case OSD_GPS_MAX_SPEED:
1823 osdFormatVelocityStr(buff, stats.max_speed, false, true);
1824 break;
1826 case OSD_3D_SPEED:
1827 osdFormatVelocityStr(buff, osdGet3DSpeed(), true, false);
1828 break;
1830 case OSD_3D_MAX_SPEED:
1831 osdFormatVelocityStr(buff, stats.max_3D_speed, true, true);
1832 break;
1834 case OSD_GLIDESLOPE:
1836 float horizontalSpeed = gpsSol.groundSpeed;
1837 float sinkRate = -getEstimatedActualVelocity(Z);
1838 static pt1Filter_t gsFilterState;
1839 const timeMs_t currentTimeMs = millis();
1840 static timeMs_t gsUpdatedTimeMs;
1841 float glideSlope = horizontalSpeed / sinkRate;
1842 glideSlope = pt1FilterApply4(&gsFilterState, isnormal(glideSlope) ? glideSlope : 200, 0.5, MS2S(currentTimeMs - gsUpdatedTimeMs));
1843 gsUpdatedTimeMs = currentTimeMs;
1845 buff[0] = SYM_GLIDESLOPE;
1846 if (glideSlope > 0.0f && glideSlope < 100.0f) {
1847 osdFormatCentiNumber(buff + 1, glideSlope * 100.0f, 0, 2, 0, 3);
1848 } else {
1849 buff[1] = buff[2] = buff[3] = '-';
1851 buff[4] = '\0';
1852 break;
1855 case OSD_GPS_LAT:
1856 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
1857 break;
1859 case OSD_GPS_LON:
1860 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
1861 break;
1863 case OSD_HOME_DIR:
1865 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
1866 if (GPS_distanceToHome < (navConfig()->general.min_rth_distance / 100) ) {
1867 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_HOME_NEAR);
1869 else
1871 int16_t panHomeDirOffset = 0;
1872 if (!(osdConfig()->pan_servo_pwm2centideg == 0)){
1873 panHomeDirOffset = osdGetPanServoOffset();
1875 int16_t flightDirection = STATE(AIRPLANE) ? CENTIDEGREES_TO_DEGREES(posControl.actualState.cog) : DECIDEGREES_TO_DEGREES(osdGetHeading());
1876 int homeDirection = GPS_directionToHome - flightDirection + panHomeDirOffset;
1877 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), homeDirection);
1879 } else {
1880 // No home or no fix or unknown heading, blink.
1881 // If we're unarmed, show the arrow pointing up so users can see the arrow
1882 // while configuring the OSD. If we're armed, show a '-' indicating that
1883 // we don't know the direction to home.
1884 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1885 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, ARMING_FLAG(ARMED) ? '-' : SYM_ARROW_UP, elemAttr);
1887 return true;
1890 case OSD_HOME_HEADING_ERROR:
1892 buff[0] = SYM_HOME;
1893 buff[1] = SYM_HEADING;
1895 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1896 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())))));
1897 tfp_sprintf(buff + 2, "%4d", h);
1898 } else {
1899 strcpy(buff + 2, "----");
1902 buff[6] = SYM_DEGREES;
1903 buff[7] = '\0';
1904 break;
1907 case OSD_HOME_DIST:
1909 buff[0] = SYM_HOME;
1910 uint32_t distance_to_home_cm = GPS_distanceToHome * 100;
1911 osdFormatDistanceSymbol(&buff[1], distance_to_home_cm, 0);
1913 uint16_t dist_alarm = osdConfig()->dist_alarm;
1914 if (dist_alarm > 0 && GPS_distanceToHome > dist_alarm) {
1915 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1918 break;
1920 case OSD_TRIP_DIST:
1921 buff[0] = SYM_TOTAL;
1922 osdFormatDistanceSymbol(buff + 1, getTotalTravelDistance(), 0);
1923 break;
1925 case OSD_GROUND_COURSE:
1927 buff[0] = SYM_GROUND_COURSE;
1928 if (osdIsHeadingValid()) {
1929 tfp_sprintf(&buff[1], "%3d", (int16_t)CENTIDEGREES_TO_DEGREES(posControl.actualState.cog));
1930 } else {
1931 buff[1] = buff[2] = buff[3] = '-';
1933 buff[4] = SYM_DEGREES;
1934 buff[5] = '\0';
1935 break;
1938 case OSD_COURSE_HOLD_ERROR:
1940 if (ARMING_FLAG(ARMED) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1941 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1942 return true;
1945 buff[0] = SYM_HEADING;
1947 if ((!ARMING_FLAG(ARMED)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && isAdjustingPosition())) {
1948 buff[1] = buff[2] = buff[3] = '-';
1949 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1950 int16_t herr = lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
1951 if (ABS(herr) > 99)
1952 strcpy(buff + 1, ">99");
1953 else
1954 tfp_sprintf(buff + 1, "%3d", herr);
1957 buff[4] = SYM_DEGREES;
1958 buff[5] = '\0';
1959 break;
1962 case OSD_COURSE_HOLD_ADJUSTMENT:
1964 int16_t heading_adjust = lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
1966 if (ARMING_FLAG(ARMED) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust != 0)))) {
1967 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1968 return true;
1971 buff[0] = SYM_HEADING;
1973 if (!ARMING_FLAG(ARMED)) {
1974 buff[1] = buff[2] = buff[3] = buff[4] = '-';
1975 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1976 tfp_sprintf(buff + 1, "%4d", heading_adjust);
1979 buff[5] = SYM_DEGREES;
1980 buff[6] = '\0';
1981 break;
1984 case OSD_CROSS_TRACK_ERROR:
1986 if (isWaypointNavTrackingActive()) {
1987 buff[0] = SYM_CROSS_TRACK_ERROR;
1988 osdFormatDistanceSymbol(buff + 1, navigationGetCrossTrackError(), 0);
1989 } else {
1990 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1991 return true;
1993 break;
1996 case OSD_GPS_HDOP:
1998 buff[0] = SYM_HDP_L;
1999 buff[1] = SYM_HDP_R;
2000 int32_t centiHDOP = 100 * gpsSol.hdop / HDOP_SCALE;
2001 uint8_t digits = 2U;
2002 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
2003 if (isBfCompatibleVideoSystem(osdConfig())) {
2004 digits = 3U;
2006 #endif
2007 osdFormatCentiNumber(&buff[2], centiHDOP, 0, 1, 0, digits);
2008 break;
2011 case OSD_MAP_NORTH:
2013 static uint16_t drawn = 0;
2014 static uint32_t scale = 0;
2015 osdDrawHomeMap(0, 'N', &drawn, &scale);
2016 return true;
2018 case OSD_MAP_TAKEOFF:
2020 static uint16_t drawn = 0;
2021 static uint32_t scale = 0;
2022 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn, &scale);
2023 return true;
2025 case OSD_RADAR:
2027 static uint16_t drawn = 0;
2028 static uint32_t scale = 0;
2029 osdDrawRadar(&drawn, &scale);
2030 return true;
2032 #endif // GPS
2034 case OSD_ALTITUDE:
2036 int32_t alt = osdGetAltitude();
2037 osdFormatAltitudeSymbol(buff, alt);
2039 uint16_t alt_alarm = osdConfig()->alt_alarm;
2040 uint16_t neg_alt_alarm = osdConfig()->neg_alt_alarm;
2041 if ((alt_alarm > 0 && CENTIMETERS_TO_METERS(alt) > alt_alarm) ||
2042 (neg_alt_alarm > 0 && alt < 0 && -CENTIMETERS_TO_METERS(alt) > neg_alt_alarm)) {
2044 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2046 break;
2049 case OSD_ALTITUDE_MSL:
2051 int32_t alt = osdGetAltitudeMsl();
2052 osdFormatAltitudeSymbol(buff, alt);
2053 break;
2056 #ifdef USE_RANGEFINDER
2057 case OSD_RANGEFINDER:
2059 int32_t range = rangefinderGetLatestRawAltitude();
2060 if (range < 0) {
2061 buff[0] = '-';
2062 buff[1] = '-';
2063 buff[2] = '-';
2064 } else {
2065 osdFormatDistanceSymbol(buff, range, 1);
2068 break;
2069 #endif
2071 case OSD_ONTIME:
2073 osdFormatOnTime(buff);
2074 break;
2077 case OSD_FLYTIME:
2079 osdFormatFlyTime(buff, &elemAttr);
2080 break;
2083 case OSD_ONTIME_FLYTIME:
2085 if (ARMING_FLAG(ARMED)) {
2086 osdFormatFlyTime(buff, &elemAttr);
2087 } else {
2088 osdFormatOnTime(buff);
2090 break;
2093 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH:
2095 /*static int32_t updatedTimeSeconds = 0;*/
2096 static int32_t timeSeconds = -1;
2097 #if defined(USE_ADC) && defined(USE_GPS)
2098 static timeUs_t updatedTimestamp = 0;
2099 timeUs_t currentTimeUs = micros();
2100 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2101 #ifdef USE_WIND_ESTIMATOR
2102 timeSeconds = calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation);
2103 #else
2104 timeSeconds = calculateRemainingFlightTimeBeforeRTH(false);
2105 #endif
2106 updatedTimestamp = currentTimeUs;
2108 #endif
2109 if ((!ARMING_FLAG(ARMED)) || (timeSeconds == -1)) {
2110 buff[0] = SYM_FLIGHT_MINS_REMAINING;
2111 strcpy(buff + 1, "--:--");
2112 #if defined(USE_ADC) && defined(USE_GPS)
2113 updatedTimestamp = 0;
2114 #endif
2115 } else if (timeSeconds == -2) {
2116 // Wind is too strong to come back with cruise throttle
2117 buff[0] = SYM_FLIGHT_MINS_REMAINING;
2118 buff[1] = buff[2] = buff[4] = buff[5] = SYM_WIND_HORIZONTAL;
2119 buff[3] = ':';
2120 buff[6] = '\0';
2121 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2122 } else {
2123 osdFormatTime(buff, timeSeconds, SYM_FLIGHT_MINS_REMAINING, SYM_FLIGHT_HOURS_REMAINING);
2124 if (timeSeconds == 0)
2125 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2128 break;
2130 case OSD_REMAINING_DISTANCE_BEFORE_RTH:;
2131 static int32_t distanceMeters = -1;
2132 #if defined(USE_ADC) && defined(USE_GPS)
2133 static timeUs_t updatedTimestamp = 0;
2134 timeUs_t currentTimeUs = micros();
2135 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2136 #ifdef USE_WIND_ESTIMATOR
2137 distanceMeters = calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation);
2138 #else
2139 distanceMeters = calculateRemainingDistanceBeforeRTH(false);
2140 #endif
2141 updatedTimestamp = currentTimeUs;
2143 #endif
2144 //buff[0] = SYM_TRIP_DIST;
2145 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_FLIGHT_DIST_REMAINING);
2146 if ((!ARMING_FLAG(ARMED)) || (distanceMeters == -1)) {
2147 buff[4] = SYM_BLANK;
2148 buff[5] = '\0';
2149 strcpy(buff + 1, "---");
2150 } else if (distanceMeters == -2) {
2151 // Wind is too strong to come back with cruise throttle
2152 buff[1] = buff[2] = buff[3] = SYM_WIND_HORIZONTAL;
2153 switch ((osd_unit_e)osdConfig()->units){
2154 case OSD_UNIT_UK:
2155 FALLTHROUGH;
2156 case OSD_UNIT_IMPERIAL:
2157 buff[4] = SYM_DIST_MI;
2158 break;
2159 case OSD_UNIT_METRIC_MPH:
2160 FALLTHROUGH;
2161 case OSD_UNIT_METRIC:
2162 buff[4] = SYM_DIST_KM;
2163 break;
2164 case OSD_UNIT_GA:
2165 buff[4] = SYM_DIST_NM;
2166 break;
2168 buff[5] = '\0';
2169 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2170 } else {
2171 osdFormatDistanceSymbol(buff + 1, distanceMeters * 100, 0);
2172 if (distanceMeters == 0)
2173 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2175 break;
2177 case OSD_FLYMODE:
2179 char *p = "ACRO";
2181 if (FLIGHT_MODE(FAILSAFE_MODE))
2182 p = "!FS!";
2183 else if (FLIGHT_MODE(MANUAL_MODE))
2184 p = "MANU";
2185 else if (FLIGHT_MODE(TURTLE_MODE))
2186 p = "TURT";
2187 else if (FLIGHT_MODE(NAV_RTH_MODE))
2188 p = isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
2189 else if (FLIGHT_MODE(NAV_POSHOLD_MODE) && STATE(AIRPLANE))
2190 p = "LOTR";
2191 else if (FLIGHT_MODE(NAV_POSHOLD_MODE))
2192 p = "HOLD";
2193 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && FLIGHT_MODE(NAV_ALTHOLD_MODE))
2194 p = "CRUZ";
2195 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE))
2196 p = "CRSH";
2197 else if (FLIGHT_MODE(NAV_WP_MODE))
2198 p = " WP ";
2199 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && navigationRequiresAngleMode()) {
2200 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
2201 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
2202 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
2203 p = " AH ";
2205 else if (FLIGHT_MODE(ANGLE_MODE))
2206 p = "ANGL";
2207 else if (FLIGHT_MODE(HORIZON_MODE))
2208 p = "HOR ";
2210 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
2211 return true;
2214 case OSD_CRAFT_NAME:
2215 osdFormatCraftName(buff);
2216 break;
2218 case OSD_PILOT_NAME:
2219 osdFormatPilotName(buff);
2220 break;
2222 case OSD_THROTTLE_POS:
2224 osdFormatThrottlePosition(buff, false, &elemAttr);
2225 break;
2228 case OSD_VTX_CHANNEL:
2230 vtxDeviceOsdInfo_t osdInfo;
2231 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2233 tfp_sprintf(buff, "CH:%c%s:", osdInfo.bandLetter, osdInfo.channelName);
2234 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2236 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2237 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2238 displayWriteWithAttr(osdDisplayPort, elemPosX + 6, elemPosY, buff, elemAttr);
2239 return true;
2241 break;
2243 case OSD_VTX_POWER:
2245 vtxDeviceOsdInfo_t osdInfo;
2246 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2248 tfp_sprintf(buff, "%c", SYM_VTX_POWER);
2249 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2251 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2252 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2253 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2254 return true;
2257 #if defined(USE_SERIALRX_CRSF)
2258 case OSD_CRSF_RSSI_DBM:
2260 int16_t rssi = rxLinkStatistics.uplinkRSSI;
2261 buff[0] = (rxLinkStatistics.activeAntenna == 0) ? SYM_RSSI : SYM_2RSS; // Separate symbols for each antenna
2262 if (rssi <= -100) {
2263 tfp_sprintf(buff + 1, "%4d%c", rssi, SYM_DBM);
2264 } else {
2265 tfp_sprintf(buff + 1, "%3d%c%c", rssi, SYM_DBM, ' ');
2267 if (!failsafeIsReceivingRxData()){
2268 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2269 } else if (osdConfig()->rssi_dbm_alarm && rssi < osdConfig()->rssi_dbm_alarm) {
2270 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2272 break;
2274 case OSD_CRSF_LQ:
2276 buff[0] = SYM_LQ;
2277 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
2278 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
2279 switch (osdConfig()->crsf_lq_format) {
2280 case OSD_CRSF_LQ_TYPE1:
2281 if (!failsafeIsReceivingRxData()) {
2282 tfp_sprintf(buff+1, "%3d", 0);
2283 } else {
2284 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.uplinkLQ);
2286 break;
2287 case OSD_CRSF_LQ_TYPE2:
2288 if (!failsafeIsReceivingRxData()) {
2289 tfp_sprintf(buff+1, "%s:%3d", " ", 0);
2290 } else {
2291 tfp_sprintf(buff+1, "%d:%3d", rxLinkStatistics.rfMode, rxLinkStatistics.uplinkLQ);
2293 break;
2294 case OSD_CRSF_LQ_TYPE3:
2295 if (!failsafeIsReceivingRxData()) {
2296 tfp_sprintf(buff+1, "%3d", 0);
2297 } else {
2298 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.rfMode >= 2 ? scaledLQ : rxLinkStatistics.uplinkLQ);
2300 break;
2302 if (!failsafeIsReceivingRxData()) {
2303 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2304 } else if (rxLinkStatistics.uplinkLQ < osdConfig()->link_quality_alarm) {
2305 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2307 break;
2310 case OSD_CRSF_SNR_DB:
2312 static pt1Filter_t snrFilterState;
2313 static timeMs_t snrUpdated = 0;
2314 int8_t snrFiltered = pt1FilterApply4(&snrFilterState, rxLinkStatistics.uplinkSNR, 0.5f, MS2S(millis() - snrUpdated));
2315 snrUpdated = millis();
2317 const char* showsnr = "-20";
2318 const char* hidesnr = " ";
2319 if (snrFiltered > osdConfig()->snr_alarm) {
2320 if (cmsInMenu) {
2321 buff[0] = SYM_SNR;
2322 tfp_sprintf(buff + 1, "%s%c", showsnr, SYM_DB);
2323 } else {
2324 buff[0] = SYM_BLANK;
2325 tfp_sprintf(buff + 1, "%s%c", hidesnr, SYM_BLANK);
2327 } else if (snrFiltered <= osdConfig()->snr_alarm) {
2328 buff[0] = SYM_SNR;
2329 if (snrFiltered <= -10) {
2330 tfp_sprintf(buff + 1, "%3d%c", snrFiltered, SYM_DB);
2331 } else {
2332 tfp_sprintf(buff + 1, "%2d%c%c", snrFiltered, SYM_DB, ' ');
2335 break;
2338 case OSD_CRSF_TX_POWER:
2340 if (!failsafeIsReceivingRxData())
2341 tfp_sprintf(buff, "%s%c", " ", SYM_BLANK);
2342 else
2343 tfp_sprintf(buff, "%4d%c", rxLinkStatistics.uplinkTXPower, SYM_MW);
2344 break;
2346 #endif
2348 case OSD_CROSSHAIRS: // Hud is a sub-element of the crosshair
2350 osdCrosshairPosition(&elemPosX, &elemPosY);
2351 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX, elemPosY);
2353 if (osdConfig()->hud_homing && STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
2354 osdHudDrawHoming(elemPosX, elemPosY);
2357 if (STATE(GPS_FIX) && isImuHeadingValid()) {
2359 if (osdConfig()->hud_homepoint || osdConfig()->hud_radar_disp > 0 || osdConfig()->hud_wp_disp > 0) {
2360 osdHudClear();
2363 // -------- POI : Home point
2365 if (osdConfig()->hud_homepoint) { // Display the home point (H)
2366 osdHudDrawPoi(GPS_distanceToHome, GPS_directionToHome, -osdGetAltitude() / 100, 0, SYM_HOME, 0 , 0);
2369 // -------- POI : Nearby aircrafts from ESP32 radar
2371 if (osdConfig()->hud_radar_disp > 0) { // Display the POI from the radar
2372 for (uint8_t i = 0; i < osdConfig()->hud_radar_disp; i++) {
2373 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
2374 fpVector3_t poi;
2375 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &radar_pois[i].gps, GEO_ALT_RELATIVE);
2376 radar_pois[i].distance = calculateDistanceToDestination(&poi) / 100; // In meters
2378 if (radar_pois[i].distance >= osdConfig()->hud_radar_range_min && radar_pois[i].distance <= osdConfig()->hud_radar_range_max) {
2379 radar_pois[i].direction = calculateBearingToDestination(&poi) / 100; // In °
2380 radar_pois[i].altitude = (radar_pois[i].gps.alt - osdGetAltitudeMsl()) / 100;
2381 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);
2387 // -------- POI : Next waypoints from navigation
2389 if (osdConfig()->hud_wp_disp > 0 && posControl.waypointListValid && posControl.waypointCount > 0) { // Display the next waypoints
2390 gpsLocation_t wp2;
2391 int j;
2393 for (int i = osdConfig()->hud_wp_disp - 1; i >= 0 ; i--) { // Display in reverse order so the next WP is always written on top
2394 j = posControl.activeWaypointIndex + i;
2395 if (j > posControl.startWpIndex + posControl.waypointCount - 1) { // limit to max WP index for mission
2396 break;
2398 if (posControl.waypointList[j].lat != 0 && posControl.waypointList[j].lon != 0) {
2399 wp2.lat = posControl.waypointList[j].lat;
2400 wp2.lon = posControl.waypointList[j].lon;
2401 wp2.alt = posControl.waypointList[j].alt;
2402 fpVector3_t poi;
2403 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &wp2, waypointMissionAltConvMode(posControl.waypointList[j].p3));
2404 int32_t altConvModeAltitude = waypointMissionAltConvMode(posControl.waypointList[j].p3) == GEO_ALT_ABSOLUTE ? osdGetAltitudeMsl() : osdGetAltitude();
2405 j = getGeoWaypointNumber(j);
2406 while (j > 9) j -= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2407 osdHudDrawPoi(calculateDistanceToDestination(&poi) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi) / 100), (posControl.waypointList[j].alt - altConvModeAltitude)/ 100, 2, SYM_WAYPOINT, 48 + j, i);
2413 return true;
2414 break;
2416 case OSD_ATTITUDE_ROLL:
2417 buff[0] = SYM_ROLL_LEVEL;
2418 if (ABS(attitude.values.roll) >= 1)
2419 buff[0] += (attitude.values.roll < 0 ? -1 : 1);
2420 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.roll)), 0, 1, 0, 3);
2421 break;
2423 case OSD_ATTITUDE_PITCH:
2424 if (ABS(attitude.values.pitch) < 1)
2425 buff[0] = 'P';
2426 else if (attitude.values.pitch > 0)
2427 buff[0] = SYM_PITCH_DOWN;
2428 else if (attitude.values.pitch < 0)
2429 buff[0] = SYM_PITCH_UP;
2430 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.pitch)), 0, 1, 0, 3);
2431 break;
2433 case OSD_ARTIFICIAL_HORIZON:
2435 float rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2436 float pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2438 pitchAngle -= osdConfig()->ahi_camera_uptilt_comp ? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt) : 0;
2439 pitchAngle += DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2440 if (osdConfig()->ahi_reverse_roll) {
2441 rollAngle = -rollAngle;
2443 osdDrawArtificialHorizon(osdDisplayPort, osdGetDisplayPortCanvas(),
2444 OSD_DRAW_POINT_GRID(elemPosX, elemPosY), rollAngle, pitchAngle);
2445 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
2446 osdDrawSingleElement(OSD_CROSSHAIRS);
2448 return true;
2451 case OSD_HORIZON_SIDEBARS:
2453 osdDrawSidebars(osdDisplayPort, osdGetDisplayPortCanvas());
2454 return true;
2457 #if defined(USE_BARO) || defined(USE_GPS)
2458 case OSD_VARIO:
2460 float zvel = getEstimatedActualVelocity(Z);
2461 osdDrawVario(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), zvel);
2462 return true;
2465 case OSD_VARIO_NUM:
2467 int16_t value = getEstimatedActualVelocity(Z);
2468 char sym;
2469 switch ((osd_unit_e)osdConfig()->units) {
2470 case OSD_UNIT_UK:
2471 FALLTHROUGH;
2472 case OSD_UNIT_IMPERIAL:
2473 // Convert to centifeet/s
2474 value = CENTIMETERS_TO_CENTIFEET(value);
2475 sym = SYM_FTS;
2476 break;
2477 case OSD_UNIT_GA:
2478 // Convert to centi-100feet/min
2479 value = CENTIMETERS_TO_FEET(value * 60);
2480 sym = SYM_100FTM;
2481 break;
2482 default:
2483 case OSD_UNIT_METRIC_MPH:
2484 FALLTHROUGH;
2485 case OSD_UNIT_METRIC:
2486 // Already in cm/s
2487 sym = SYM_MS;
2488 break;
2491 osdFormatCentiNumber(buff, value, 0, 1, 0, 3);
2492 buff[3] = sym;
2493 buff[4] = '\0';
2494 break;
2496 case OSD_CLIMB_EFFICIENCY:
2498 // amperage is in centi amps (10mA), vertical speed is in cms/s. We want
2499 // Ah/dist only to show when vertical speed > 1m/s.
2500 static pt1Filter_t veFilterState;
2501 static timeUs_t vEfficiencyUpdated = 0;
2502 int32_t value = 0;
2503 timeUs_t currentTimeUs = micros();
2504 timeDelta_t vEfficiencyTimeDelta = cmpTimeUs(currentTimeUs, vEfficiencyUpdated);
2505 if (getEstimatedActualVelocity(Z) > 0) {
2506 if (vEfficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2507 // Centiamps (kept for osdFormatCentiNumber) / m/s - Will appear as A / m/s in OSD
2508 value = pt1FilterApply4(&veFilterState, (float)getAmperage() / (getEstimatedActualVelocity(Z) / 100.0f), 1, US2S(vEfficiencyTimeDelta));
2510 vEfficiencyUpdated = currentTimeUs;
2511 } else {
2512 value = veFilterState.state;
2515 bool efficiencyValid = (value > 0) && (getEstimatedActualVelocity(Z) > 100);
2516 switch (osdConfig()->units) {
2517 case OSD_UNIT_UK:
2518 FALLTHROUGH;
2519 case OSD_UNIT_GA:
2520 FALLTHROUGH;
2521 case OSD_UNIT_IMPERIAL:
2522 // mAh/foot
2523 if (efficiencyValid) {
2524 osdFormatCentiNumber(buff, (value * METERS_PER_FOOT), 1, 2, 2, 3);
2525 tfp_sprintf(buff, "%s%c%c", buff, SYM_AH_V_FT_0, SYM_AH_V_FT_1);
2526 } else {
2527 buff[0] = buff[1] = buff[2] = '-';
2528 buff[3] = SYM_AH_V_FT_0;
2529 buff[4] = SYM_AH_V_FT_1;
2530 buff[5] = '\0';
2532 break;
2533 case OSD_UNIT_METRIC_MPH:
2534 FALLTHROUGH;
2535 case OSD_UNIT_METRIC:
2536 // mAh/metre
2537 if (efficiencyValid) {
2538 osdFormatCentiNumber(buff, value, 1, 2, 2, 3);
2539 tfp_sprintf(buff, "%s%c%c", buff, SYM_AH_V_M_0, SYM_AH_V_M_1);
2540 } else {
2541 buff[0] = buff[1] = buff[2] = '-';
2542 buff[3] = SYM_AH_V_M_0;
2543 buff[4] = SYM_AH_V_M_1;
2544 buff[5] = '\0';
2546 break;
2548 break;
2550 case OSD_GLIDE_TIME_REMAINING:
2552 uint16_t glideTime = osdGetRemainingGlideTime();
2553 buff[0] = SYM_GLIDE_MINS;
2554 if (glideTime > 0) {
2555 // Maximum value we can show in minutes is 99 minutes and 59 seconds. It is extremely unlikely that glide
2556 // time will be longer than 99 minutes. If it is, it will show 99:^^
2557 if (glideTime > (99 * 60) + 59) {
2558 tfp_sprintf(buff + 1, "%02d:", (int)(glideTime / 60));
2559 buff[4] = SYM_DIRECTION;
2560 buff[5] = SYM_DIRECTION;
2561 } else {
2562 tfp_sprintf(buff + 1, "%02d:%02d", (int)(glideTime / 60), (int)(glideTime % 60));
2564 } else {
2565 tfp_sprintf(buff + 1, "%s", "--:--");
2567 buff[6] = '\0';
2568 break;
2570 case OSD_GLIDE_RANGE:
2572 uint16_t glideSeconds = osdGetRemainingGlideTime();
2573 buff[0] = SYM_GLIDE_DIST;
2574 if (glideSeconds > 0) {
2575 uint32_t glideRangeCM = glideSeconds * gpsSol.groundSpeed;
2576 osdFormatDistanceSymbol(buff + 1, glideRangeCM, 0);
2577 } else {
2578 tfp_sprintf(buff + 1, "%s%c", "---", SYM_BLANK);
2579 buff[5] = '\0';
2581 break;
2583 #endif
2585 case OSD_SWITCH_INDICATOR_0:
2586 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channel - 1), buff);
2587 break;
2589 case OSD_SWITCH_INDICATOR_1:
2590 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channel - 1), buff);
2591 break;
2593 case OSD_SWITCH_INDICATOR_2:
2594 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channel - 1), buff);
2595 break;
2597 case OSD_SWITCH_INDICATOR_3:
2598 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channel - 1), buff);
2599 break;
2601 case OSD_PAN_SERVO_CENTRED:
2603 int16_t panOffset = osdGetPanServoOffset();
2604 const timeMs_t panServoTimeNow = millis();
2605 static timeMs_t panServoTimeOffCentre = 0;
2607 if (panOffset < 0) {
2608 if (osdConfig()->pan_servo_offcentre_warning != 0 && panOffset >= -osdConfig()->pan_servo_offcentre_warning) {
2609 if (panServoTimeOffCentre == 0) {
2610 panServoTimeOffCentre = panServoTimeNow;
2611 } else if (panServoTimeNow >= (panServoTimeOffCentre + 10000 )) {
2612 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2614 } else {
2615 panServoTimeOffCentre = 0;
2618 if (osdConfig()->pan_servo_indicator_show_degrees) {
2619 tfp_sprintf(buff, "%3d%c", -panOffset, SYM_DEGREES);
2620 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2622 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_OFFSET_R, elemAttr);
2623 } else if (panOffset > 0) {
2624 if (osdConfig()->pan_servo_offcentre_warning != 0 && panOffset <= osdConfig()->pan_servo_offcentre_warning) {
2625 if (panServoTimeOffCentre == 0) {
2626 panServoTimeOffCentre = panServoTimeNow;
2627 } else if (panServoTimeNow >= (panServoTimeOffCentre + 10000 )) {
2628 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2630 } else {
2631 panServoTimeOffCentre = 0;
2634 if (osdConfig()->pan_servo_indicator_show_degrees) {
2635 tfp_sprintf(buff, "%3d%c", panOffset, SYM_DEGREES);
2636 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2638 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_OFFSET_L, elemAttr);
2639 } else {
2640 panServoTimeOffCentre = 0;
2642 if (osdConfig()->pan_servo_indicator_show_degrees) {
2643 tfp_sprintf(buff, "%3d%c", panOffset, SYM_DEGREES);
2644 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2646 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_SERVO_PAN_IS_CENTRED);
2649 return true;
2651 break;
2653 case OSD_ACTIVE_PROFILE:
2654 tfp_sprintf(buff, "%c%u", SYM_PROFILE, (getConfigProfile() + 1));
2655 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2656 break;
2658 case OSD_ROLL_PIDS:
2659 osdDisplayFlightPIDValues(elemPosX, elemPosY, "ROL", PID_ROLL, ADJUSTMENT_ROLL_P, ADJUSTMENT_ROLL_I, ADJUSTMENT_ROLL_D, ADJUSTMENT_ROLL_FF);
2660 return true;
2662 case OSD_PITCH_PIDS:
2663 osdDisplayFlightPIDValues(elemPosX, elemPosY, "PIT", PID_PITCH, ADJUSTMENT_PITCH_P, ADJUSTMENT_PITCH_I, ADJUSTMENT_PITCH_D, ADJUSTMENT_PITCH_FF);
2664 return true;
2666 case OSD_YAW_PIDS:
2667 osdDisplayFlightPIDValues(elemPosX, elemPosY, "YAW", PID_YAW, ADJUSTMENT_YAW_P, ADJUSTMENT_YAW_I, ADJUSTMENT_YAW_D, ADJUSTMENT_YAW_FF);
2668 return true;
2670 case OSD_LEVEL_PIDS:
2671 osdDisplayNavPIDValues(elemPosX, elemPosY, "LEV", PID_LEVEL, ADJUSTMENT_LEVEL_P, ADJUSTMENT_LEVEL_I, ADJUSTMENT_LEVEL_D);
2672 return true;
2674 case OSD_POS_XY_PIDS:
2675 osdDisplayNavPIDValues(elemPosX, elemPosY, "PXY", PID_POS_XY, ADJUSTMENT_POS_XY_P, ADJUSTMENT_POS_XY_I, ADJUSTMENT_POS_XY_D);
2676 return true;
2678 case OSD_POS_Z_PIDS:
2679 osdDisplayNavPIDValues(elemPosX, elemPosY, "PZ", PID_POS_Z, ADJUSTMENT_POS_Z_P, ADJUSTMENT_POS_Z_I, ADJUSTMENT_POS_Z_D);
2680 return true;
2682 case OSD_VEL_XY_PIDS:
2683 osdDisplayNavPIDValues(elemPosX, elemPosY, "VXY", PID_VEL_XY, ADJUSTMENT_VEL_XY_P, ADJUSTMENT_VEL_XY_I, ADJUSTMENT_VEL_XY_D);
2684 return true;
2686 case OSD_VEL_Z_PIDS:
2687 osdDisplayNavPIDValues(elemPosX, elemPosY, "VZ", PID_VEL_Z, ADJUSTMENT_VEL_Z_P, ADJUSTMENT_VEL_Z_I, ADJUSTMENT_VEL_Z_D);
2688 return true;
2690 case OSD_HEADING_P:
2691 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "HP", 0, pidBank()->pid[PID_HEADING].P, 3, 0, ADJUSTMENT_HEADING_P);
2692 return true;
2694 case OSD_BOARD_ALIGN_ROLL:
2695 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT);
2696 return true;
2698 case OSD_BOARD_ALIGN_PITCH:
2699 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT);
2700 return true;
2702 case OSD_RC_EXPO:
2703 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "EXP", 0, currentControlRateProfile->stabilized.rcExpo8, 3, 0, ADJUSTMENT_RC_EXPO);
2704 return true;
2706 case OSD_RC_YAW_EXPO:
2707 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "YEX", 0, currentControlRateProfile->stabilized.rcYawExpo8, 3, 0, ADJUSTMENT_RC_YAW_EXPO);
2708 return true;
2710 case OSD_THROTTLE_EXPO:
2711 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TEX", 0, currentControlRateProfile->throttle.rcExpo8, 3, 0, ADJUSTMENT_THROTTLE_EXPO);
2712 return true;
2714 case OSD_PITCH_RATE:
2715 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SPR");
2717 elemAttr = TEXT_ATTRIBUTES_NONE;
2718 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_PITCH]);
2719 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2720 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2721 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2722 return true;
2724 case OSD_ROLL_RATE:
2725 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SRR");
2727 elemAttr = TEXT_ATTRIBUTES_NONE;
2728 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_ROLL]);
2729 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2730 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2731 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2732 return true;
2734 case OSD_YAW_RATE:
2735 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "SYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2736 return true;
2738 case OSD_MANUAL_RC_EXPO:
2739 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MEX", 0, currentControlRateProfile->manual.rcExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO);
2740 return true;
2742 case OSD_MANUAL_RC_YAW_EXPO:
2743 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYX", 0, currentControlRateProfile->manual.rcYawExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO);
2744 return true;
2746 case OSD_MANUAL_PITCH_RATE:
2747 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MPR");
2749 elemAttr = TEXT_ATTRIBUTES_NONE;
2750 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_PITCH]);
2751 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2752 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2753 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2754 return true;
2756 case OSD_MANUAL_ROLL_RATE:
2757 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MRR");
2759 elemAttr = TEXT_ATTRIBUTES_NONE;
2760 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_ROLL]);
2761 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2762 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2763 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2764 return true;
2766 case OSD_MANUAL_YAW_RATE:
2767 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2768 return true;
2770 case OSD_NAV_FW_CRUISE_THR:
2771 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CRZ", 0, currentBatteryProfile->nav.fw.cruise_throttle, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR);
2772 return true;
2774 case OSD_NAV_FW_PITCH2THR:
2775 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "P2T", 0, currentBatteryProfile->nav.fw.pitch_to_throttle, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR);
2776 return true;
2778 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE:
2779 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "0TP", 0, (float)navConfig()->fw.minThrottleDownPitchAngle / 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE);
2780 return true;
2782 case OSD_FW_ALT_PID_OUTPUTS:
2784 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2785 osdFormatPidControllerOutput(buff, "PZO", &nav_pids->fw_alt, 10, true); // display requested pitch degrees
2786 break;
2789 case OSD_FW_POS_PID_OUTPUTS:
2791 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers(); // display requested roll degrees
2792 osdFormatPidControllerOutput(buff, "PXYO", &nav_pids->fw_nav, 1, true);
2793 break;
2796 case OSD_MC_VEL_Z_PID_OUTPUTS:
2798 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2799 osdFormatPidControllerOutput(buff, "VZO", &nav_pids->vel[Z], 100, false); // display throttle adjustment µs
2800 break;
2803 case OSD_MC_VEL_X_PID_OUTPUTS:
2805 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2806 osdFormatPidControllerOutput(buff, "VXO", &nav_pids->vel[X], 100, false); // display requested acceleration cm/s^2
2807 break;
2810 case OSD_MC_VEL_Y_PID_OUTPUTS:
2812 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2813 osdFormatPidControllerOutput(buff, "VYO", &nav_pids->vel[Y], 100, false); // display requested acceleration cm/s^2
2814 break;
2817 case OSD_MC_POS_XYZ_P_OUTPUTS:
2819 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2820 strcpy(buff, "POSO ");
2821 // display requested velocity cm/s
2822 tfp_sprintf(buff + 5, "%4d", (int)lrintf(nav_pids->pos[X].output_constrained * 100));
2823 buff[9] = ' ';
2824 tfp_sprintf(buff + 10, "%4d", (int)lrintf(nav_pids->pos[Y].output_constrained * 100));
2825 buff[14] = ' ';
2826 tfp_sprintf(buff + 15, "%4d", (int)lrintf(nav_pids->pos[Z].output_constrained * 100));
2827 buff[19] = '\0';
2828 break;
2831 case OSD_POWER:
2833 bool kiloWatt = osdFormatCentiNumber(buff, getPower(), 1000, 2, 2, 3);
2834 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
2835 buff[4] = '\0';
2837 uint8_t current_alarm = osdConfig()->current_alarm;
2838 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
2839 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2841 break;
2844 case OSD_AIR_SPEED:
2846 #ifdef USE_PITOT
2847 buff[0] = SYM_AIR;
2849 if (pitotIsHealthy())
2851 const float airspeed_estimate = getAirspeedEstimate();
2852 osdFormatVelocityStr(buff + 1, airspeed_estimate, false, false);
2853 if ((osdConfig()->airspeed_alarm_min != 0 && airspeed_estimate < osdConfig()->airspeed_alarm_min) ||
2854 (osdConfig()->airspeed_alarm_max != 0 && airspeed_estimate > osdConfig()->airspeed_alarm_max)) {
2855 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2858 else
2860 strcpy(buff + 1, " X!");
2861 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2863 #else
2864 return false;
2865 #endif
2866 break;
2869 case OSD_AIR_MAX_SPEED:
2871 #ifdef USE_PITOT
2872 buff[0] = SYM_MAX;
2873 buff[1] = SYM_AIR;
2874 osdFormatVelocityStr(buff + 2, stats.max_air_speed, false, false);
2875 #else
2876 return false;
2877 #endif
2878 break;
2881 case OSD_RTC_TIME:
2883 // RTC not configured will show 00:00
2884 dateTime_t dateTime;
2885 rtcGetDateTimeLocal(&dateTime);
2886 buff[0] = SYM_CLOCK;
2887 tfp_sprintf(buff + 1, "%02u:%02u:%02u", dateTime.hours, dateTime.minutes, dateTime.seconds);
2888 break;
2891 case OSD_MESSAGES:
2893 elemAttr = osdGetSystemMessage(buff, OSD_MESSAGE_LENGTH, true);
2894 break;
2897 case OSD_VERSION:
2899 tfp_sprintf(buff, "INAV %s", FC_VERSION_STRING);
2900 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2901 break;
2904 case OSD_MAIN_BATT_CELL_VOLTAGE:
2906 uint8_t base_digits = 3U;
2907 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
2908 if(isBfCompatibleVideoSystem(osdConfig())) {
2909 base_digits = 4U; // Add extra digit to account for decimal point taking an extra character space
2911 #endif
2912 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawAverageCellVoltage(), base_digits, 2);
2913 return true;
2916 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE:
2918 uint8_t base_digits = 3U;
2919 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
2920 if(isBfCompatibleVideoSystem(osdConfig())) {
2921 base_digits = 4U; // Add extra digit to account for decimal point taking an extra character space
2923 #endif
2924 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedAverageCellVoltage(), base_digits, 2);
2925 return true;
2928 case OSD_SCALED_THROTTLE_POS:
2930 osdFormatThrottlePosition(buff, true, &elemAttr);
2931 break;
2934 case OSD_HEADING:
2936 buff[0] = SYM_HEADING;
2937 if (osdIsHeadingValid()) {
2938 int16_t h = DECIDEGREES_TO_DEGREES(osdGetHeading());
2939 if (h < 0) {
2940 h += 360;
2942 tfp_sprintf(&buff[1], "%3d", h);
2943 } else {
2944 buff[1] = buff[2] = buff[3] = '-';
2946 buff[4] = SYM_DEGREES;
2947 buff[5] = '\0';
2948 break;
2951 case OSD_HEADING_GRAPH:
2953 if (osdIsHeadingValid()) {
2954 osdDrawHeadingGraph(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), osdGetHeading());
2955 return true;
2956 } else {
2957 buff[0] = buff[2] = buff[4] = buff[6] = buff[8] = SYM_HEADING_LINE;
2958 buff[1] = buff[3] = buff[5] = buff[7] = SYM_HEADING_DIVIDED_LINE;
2959 buff[OSD_HEADING_GRAPH_WIDTH] = '\0';
2961 break;
2964 case OSD_EFFICIENCY_MAH_PER_KM:
2966 // amperage is in centi amps, speed is in cms/s. We want
2967 // mah/km. Only show when ground speed > 1m/s.
2968 static pt1Filter_t eFilterState;
2969 static timeUs_t efficiencyUpdated = 0;
2970 int32_t value = 0;
2971 bool moreThanAh = false;
2972 timeUs_t currentTimeUs = micros();
2973 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2974 uint8_t digits = 3U;
2975 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
2976 if (isBfCompatibleVideoSystem(osdConfig())) {
2977 // Increase number of digits so values above 99 don't get scaled by osdFormatCentiNumber
2978 digits = 4U;
2980 #endif
2981 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2982 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2983 value = pt1FilterApply4(&eFilterState, ((float)getAmperage() / gpsSol.groundSpeed) / 0.0036f,
2984 1, US2S(efficiencyTimeDelta));
2986 efficiencyUpdated = currentTimeUs;
2987 } else {
2988 value = eFilterState.state;
2991 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2992 switch (osdConfig()->units) {
2993 case OSD_UNIT_UK:
2994 FALLTHROUGH;
2995 case OSD_UNIT_IMPERIAL:
2996 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10, 1000, 0, 2, digits);
2997 if (!moreThanAh) {
2998 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
2999 } else {
3000 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
3002 if (!efficiencyValid) {
3003 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3004 buff[digits] = SYM_MAH_MI_0; // This will overwrite the "-" at buff[3] if not in BFCOMPAT mode
3005 buff[digits + 1] = SYM_MAH_MI_1;
3006 buff[digits + 2] = '\0';
3008 break;
3009 case OSD_UNIT_GA:
3010 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10, 1000, 0, 2, digits);
3011 if (!moreThanAh) {
3012 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
3013 } else {
3014 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
3016 if (!efficiencyValid) {
3017 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3018 buff[digits] = SYM_MAH_NM_0;
3019 buff[digits + 1] = SYM_MAH_NM_1;
3020 buff[digits + 2] = '\0';
3022 break;
3023 case OSD_UNIT_METRIC_MPH:
3024 FALLTHROUGH;
3025 case OSD_UNIT_METRIC:
3026 moreThanAh = osdFormatCentiNumber(buff, value * 100, 1000, 0, 2, digits);
3027 if (!moreThanAh) {
3028 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
3029 } else {
3030 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
3032 if (!efficiencyValid) {
3033 buff[0] = buff[1] = buff[2] = buff[3] = '-';
3034 buff[digits] = SYM_MAH_KM_0;
3035 buff[digits + 1] = SYM_MAH_KM_1;
3036 buff[digits + 2] = '\0';
3038 break;
3040 break;
3043 case OSD_EFFICIENCY_WH_PER_KM:
3045 // amperage is in centi amps, speed is in cms/s. We want
3046 // mWh/km. Only show when ground speed > 1m/s.
3047 static pt1Filter_t eFilterState;
3048 static timeUs_t efficiencyUpdated = 0;
3049 int32_t value = 0;
3050 timeUs_t currentTimeUs = micros();
3051 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
3052 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
3053 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
3054 value = pt1FilterApply4(&eFilterState, ((float)getPower() / gpsSol.groundSpeed) / 0.0036f,
3055 1, US2S(efficiencyTimeDelta));
3057 efficiencyUpdated = currentTimeUs;
3058 } else {
3059 value = eFilterState.state;
3062 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
3063 switch (osdConfig()->units) {
3064 case OSD_UNIT_UK:
3065 FALLTHROUGH;
3066 case OSD_UNIT_IMPERIAL:
3067 osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10000, 0, 2, 0, 3);
3068 buff[3] = SYM_WH_MI;
3069 break;
3070 case OSD_UNIT_GA:
3071 osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10000, 0, 2, 0, 3);
3072 buff[3] = SYM_WH_NM;
3073 break;
3074 case OSD_UNIT_METRIC_MPH:
3075 FALLTHROUGH;
3076 case OSD_UNIT_METRIC:
3077 osdFormatCentiNumber(buff, value / 10, 0, 2, 0, 3);
3078 buff[3] = SYM_WH_KM;
3079 break;
3081 buff[4] = '\0';
3082 if (!efficiencyValid) {
3083 buff[0] = buff[1] = buff[2] = '-';
3085 break;
3088 case OSD_GFORCE:
3090 buff[0] = SYM_GFORCE;
3091 osdFormatCentiNumber(buff + 1, GForce, 0, 2, 0, 3);
3092 if (GForce > osdConfig()->gforce_alarm * 100) {
3093 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3095 break;
3098 case OSD_GFORCE_X:
3099 case OSD_GFORCE_Y:
3100 case OSD_GFORCE_Z:
3102 float GForceValue = GForceAxis[item - OSD_GFORCE_X];
3103 buff[0] = SYM_GFORCE_X + item - OSD_GFORCE_X;
3104 osdFormatCentiNumber(buff + 1, GForceValue, 0, 2, 0, 4);
3105 if ((GForceValue < osdConfig()->gforce_axis_alarm_min * 100) || (GForceValue > osdConfig()->gforce_axis_alarm_max * 100)) {
3106 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3108 break;
3110 case OSD_DEBUG:
3113 * Longest representable string is -2147483648 does not fit in the screen.
3114 * Only 7 digits for negative and 8 digits for positive values allowed
3116 for (uint8_t bufferIndex = 0; bufferIndex < DEBUG32_VALUE_COUNT; ++elemPosY, bufferIndex += 2) {
3117 tfp_sprintf(
3118 buff,
3119 "[%u]=%8ld [%u]=%8ld",
3120 bufferIndex,
3121 (long)constrain(debug[bufferIndex], -9999999, 99999999),
3122 bufferIndex+1,
3123 (long)constrain(debug[bufferIndex+1], -9999999, 99999999)
3125 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3127 break;
3130 case OSD_IMU_TEMPERATURE:
3132 int16_t temperature;
3133 const bool valid = getIMUTemperature(&temperature);
3134 osdDisplayTemperature(elemPosX, elemPosY, SYM_IMU_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
3135 return true;
3138 case OSD_BARO_TEMPERATURE:
3140 int16_t temperature;
3141 const bool valid = getBaroTemperature(&temperature);
3142 osdDisplayTemperature(elemPosX, elemPosY, SYM_BARO_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
3143 return true;
3146 #ifdef USE_TEMPERATURE_SENSOR
3147 case OSD_TEMP_SENSOR_0_TEMPERATURE:
3148 case OSD_TEMP_SENSOR_1_TEMPERATURE:
3149 case OSD_TEMP_SENSOR_2_TEMPERATURE:
3150 case OSD_TEMP_SENSOR_3_TEMPERATURE:
3151 case OSD_TEMP_SENSOR_4_TEMPERATURE:
3152 case OSD_TEMP_SENSOR_5_TEMPERATURE:
3153 case OSD_TEMP_SENSOR_6_TEMPERATURE:
3154 case OSD_TEMP_SENSOR_7_TEMPERATURE:
3156 osdDisplayTemperatureSensor(elemPosX, elemPosY, item - OSD_TEMP_SENSOR_0_TEMPERATURE);
3157 return true;
3159 #endif /* ifdef USE_TEMPERATURE_SENSOR */
3161 case OSD_WIND_SPEED_HORIZONTAL:
3162 #ifdef USE_WIND_ESTIMATOR
3164 bool valid = isEstimatedWindSpeedValid();
3165 float horizontalWindSpeed;
3166 uint16_t angle;
3167 horizontalWindSpeed = getEstimatedHorizontalWindSpeed(&angle);
3168 int16_t windDirection = osdGetHeadingAngle( CENTIDEGREES_TO_DEGREES((int)angle) - DECIDEGREES_TO_DEGREES(attitude.values.yaw) + 22);
3169 buff[0] = SYM_WIND_HORIZONTAL;
3170 buff[1] = SYM_DIRECTION + (windDirection*2 / 90);
3171 osdFormatWindSpeedStr(buff + 2, horizontalWindSpeed, valid);
3172 break;
3174 #else
3175 return false;
3176 #endif
3178 case OSD_WIND_SPEED_VERTICAL:
3179 #ifdef USE_WIND_ESTIMATOR
3181 buff[0] = SYM_WIND_VERTICAL;
3182 buff[1] = SYM_BLANK;
3183 bool valid = isEstimatedWindSpeedValid();
3184 float verticalWindSpeed;
3185 verticalWindSpeed = -getEstimatedWindSpeed(Z); //from NED to NEU
3186 if (verticalWindSpeed < 0) {
3187 buff[1] = SYM_AH_DIRECTION_DOWN;
3188 verticalWindSpeed = -verticalWindSpeed;
3189 } else {
3190 buff[1] = SYM_AH_DIRECTION_UP;
3192 osdFormatWindSpeedStr(buff + 2, verticalWindSpeed, valid);
3193 break;
3195 #else
3196 return false;
3197 #endif
3199 case OSD_PLUS_CODE:
3201 STATIC_ASSERT(GPS_DEGREES_DIVIDER == OLC_DEG_MULTIPLIER, invalid_olc_deg_multiplier);
3202 int digits = osdConfig()->plus_code_digits;
3203 int digitsRemoved = osdConfig()->plus_code_short * 2;
3204 if (STATE(GPS_FIX)) {
3205 olc_encode(gpsSol.llh.lat, gpsSol.llh.lon, digits, buff, sizeof(buff));
3206 } else {
3207 // +codes with > 8 digits have a + at the 9th digit
3208 // and we only support 10 and up.
3209 memset(buff, '-', digits + 1);
3210 buff[8] = '+';
3211 buff[digits + 1] = '\0';
3213 // Optionally trim digits from the left
3214 memmove(buff, buff+digitsRemoved, strlen(buff) + digitsRemoved);
3215 buff[digits + 1 - digitsRemoved] = '\0';
3216 break;
3219 case OSD_AZIMUTH:
3222 buff[0] = SYM_AZIMUTH;
3223 if (osdIsHeadingValid()) {
3224 int16_t h = GPS_directionToHome;
3225 if (h < 0) {
3226 h += 360;
3228 if (h >= 180)
3229 h = h - 180;
3230 else
3231 h = h + 180;
3233 tfp_sprintf(&buff[1], "%3d", h);
3234 } else {
3235 buff[1] = buff[2] = buff[3] = '-';
3237 buff[4] = SYM_DEGREES;
3238 buff[5] = '\0';
3239 break;
3242 case OSD_MAP_SCALE:
3244 float scaleToUnit;
3245 int scaleUnitDivisor;
3246 char symUnscaled;
3247 char symScaled;
3248 int maxDecimals;
3250 switch (osdConfig()->units) {
3251 case OSD_UNIT_UK:
3252 FALLTHROUGH;
3253 case OSD_UNIT_IMPERIAL:
3254 scaleToUnit = 100 / 1609.3440f; // scale to 0.01mi for osdFormatCentiNumber()
3255 scaleUnitDivisor = 0;
3256 symUnscaled = SYM_MI;
3257 symScaled = SYM_MI;
3258 maxDecimals = 2;
3259 break;
3260 case OSD_UNIT_GA:
3261 scaleToUnit = 100 / 1852.0010f; // scale to 0.01mi for osdFormatCentiNumber()
3262 scaleUnitDivisor = 0;
3263 symUnscaled = SYM_NM;
3264 symScaled = SYM_NM;
3265 maxDecimals = 2;
3266 break;
3267 default:
3268 case OSD_UNIT_METRIC_MPH:
3269 FALLTHROUGH;
3270 case OSD_UNIT_METRIC:
3271 scaleToUnit = 100; // scale to cm for osdFormatCentiNumber()
3272 scaleUnitDivisor = 1000; // Convert to km when scale gets bigger than 999m
3273 symUnscaled = SYM_M;
3274 symScaled = SYM_KM;
3275 maxDecimals = 0;
3276 break;
3278 buff[0] = SYM_SCALE;
3279 if (osdMapData.scale > 0) {
3280 bool scaled = osdFormatCentiNumber(&buff[1], osdMapData.scale * scaleToUnit, scaleUnitDivisor, maxDecimals, 2, 3);
3281 buff[4] = scaled ? symScaled : symUnscaled;
3282 // Make sure this is cleared if the map stops being drawn
3283 osdMapData.scale = 0;
3284 } else {
3285 memset(&buff[1], '-', 4);
3287 buff[5] = '\0';
3288 break;
3290 case OSD_MAP_REFERENCE:
3292 char referenceSymbol;
3293 if (osdMapData.referenceSymbol) {
3294 referenceSymbol = osdMapData.referenceSymbol;
3295 // Make sure this is cleared if the map stops being drawn
3296 osdMapData.referenceSymbol = 0;
3297 } else {
3298 referenceSymbol = '-';
3300 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DIRECTION);
3301 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 1, referenceSymbol);
3302 return true;
3305 case OSD_GVAR_0:
3307 osdFormatGVar(buff, 0);
3308 break;
3310 case OSD_GVAR_1:
3312 osdFormatGVar(buff, 1);
3313 break;
3315 case OSD_GVAR_2:
3317 osdFormatGVar(buff, 2);
3318 break;
3320 case OSD_GVAR_3:
3322 osdFormatGVar(buff, 3);
3323 break;
3326 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3327 case OSD_RC_SOURCE:
3329 const char *source_text = IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
3330 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3331 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, source_text, elemAttr);
3332 return true;
3334 #endif
3336 #if defined(USE_ESC_SENSOR)
3337 case OSD_ESC_RPM:
3339 escSensorData_t * escSensor = escSensorGetData();
3340 if (escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE) {
3341 osdFormatRpm(buff, escSensor->rpm);
3343 else {
3344 osdFormatRpm(buff, 0);
3346 break;
3348 case OSD_ESC_TEMPERATURE:
3350 escSensorData_t * escSensor = escSensorGetData();
3351 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
3352 osdDisplayTemperature(elemPosX, elemPosY, SYM_ESC_TEMP, NULL, escTemperatureValid, (escSensor->temperature)*10, osdConfig()->esc_temp_alarm_min, osdConfig()->esc_temp_alarm_max);
3353 return true;
3355 #endif
3356 case OSD_TPA:
3358 char buff[4];
3359 textAttributes_t attr;
3361 displayWrite(osdDisplayPort, elemPosX, elemPosY, "TPA");
3362 attr = TEXT_ATTRIBUTES_NONE;
3363 tfp_sprintf(buff, "%3d", currentControlRateProfile->throttle.dynPID);
3364 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA)) {
3365 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3367 displayWriteWithAttr(osdDisplayPort, elemPosX + 5, elemPosY, buff, attr);
3369 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, "BP");
3370 attr = TEXT_ATTRIBUTES_NONE;
3371 tfp_sprintf(buff, "%4d", currentControlRateProfile->throttle.pa_breakpoint);
3372 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT)) {
3373 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3375 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY + 1, buff, attr);
3377 return true;
3379 case OSD_TPA_TIME_CONSTANT:
3381 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TPA TC", 0, currentControlRateProfile->throttle.fixedWingTauMs, 4, 0, ADJUSTMENT_FW_TPA_TIME_CONSTANT);
3382 return true;
3384 case OSD_FW_LEVEL_TRIM:
3386 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "LEVEL", 0, getFixedWingLevelTrim(), 3, 1, ADJUSTMENT_FW_LEVEL_TRIM);
3387 return true;
3390 case OSD_NAV_FW_CONTROL_SMOOTHNESS:
3392 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CTL S", 0, navConfig()->fw.control_smoothness, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS);
3393 return true;
3395 #ifdef USE_MULTI_MISSION
3396 case OSD_NAV_WP_MULTI_MISSION_INDEX:
3398 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "WP NO", 0, navConfig()->general.waypoint_multi_mission_index, 1, 0, ADJUSTMENT_NAV_WP_MULTI_MISSION_INDEX);
3399 return true;
3401 #endif
3402 case OSD_MISSION:
3404 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION)) {
3405 char buf[5];
3406 switch (posControl.wpMissionPlannerStatus) {
3407 case WP_PLAN_WAIT:
3408 strcpy(buf, "WAIT");
3409 break;
3410 case WP_PLAN_SAVE:
3411 strcpy(buf, "SAVE");
3412 break;
3413 case WP_PLAN_OK:
3414 strcpy(buf, " OK ");
3415 break;
3416 case WP_PLAN_FULL:
3417 strcpy(buf, "FULL");
3419 tfp_sprintf(buff, "%s>%2uWP", buf, posControl.wpPlannerActiveWPIndex);
3420 } else if (posControl.wpPlannerActiveWPIndex){
3421 tfp_sprintf(buff, "PLAN>%2uWP", posControl.waypointCount); // mission planner mision active
3423 #ifdef USE_MULTI_MISSION
3424 else {
3425 if (ARMING_FLAG(ARMED) && !(IS_RC_MODE_ACTIVE(BOXCHANGEMISSION) && posControl.multiMissionCount > 1)){
3426 // Limit field size when Armed, only show selected mission
3427 tfp_sprintf(buff, "M%u ", posControl.loadedMultiMissionIndex);
3428 } else if (posControl.multiMissionCount) {
3429 if (navConfig()->general.waypoint_multi_mission_index != posControl.loadedMultiMissionIndex) {
3430 tfp_sprintf(buff, "M%u/%u>LOAD", navConfig()->general.waypoint_multi_mission_index, posControl.multiMissionCount);
3431 } else {
3432 if (posControl.waypointListValid && posControl.waypointCount > 0) {
3433 tfp_sprintf(buff, "M%u/%u>%2uWP", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
3434 } else {
3435 tfp_sprintf(buff, "M0/%u> 0WP", posControl.multiMissionCount);
3438 } else { // no multi mission loaded - show active WP count from other source
3439 tfp_sprintf(buff, "WP CNT>%2u", posControl.waypointCount);
3442 #endif
3443 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3444 return true;
3447 #ifdef USE_POWER_LIMITS
3448 case OSD_PLIMIT_REMAINING_BURST_TIME:
3449 osdFormatCentiNumber(buff, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3);
3450 buff[3] = 'S';
3451 buff[4] = '\0';
3452 break;
3454 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT:
3455 if (currentBatteryProfile->powerLimits.continuousCurrent) {
3456 osdFormatCentiNumber(buff, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3);
3457 buff[3] = SYM_AMP;
3458 buff[4] = '\0';
3460 if (powerLimiterIsLimitingCurrent()) {
3461 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3464 break;
3466 #ifdef USE_ADC
3467 case OSD_PLIMIT_ACTIVE_POWER_LIMIT:
3469 if (currentBatteryProfile->powerLimits.continuousPower) {
3470 bool kiloWatt = osdFormatCentiNumber(buff, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3);
3471 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3472 buff[4] = '\0';
3474 if (powerLimiterIsLimitingPower()) {
3475 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3478 break;
3480 #endif // USE_ADC
3481 #endif // USE_POWER_LIMITS
3483 default:
3484 return false;
3487 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
3488 return true;
3491 uint8_t osdIncElementIndex(uint8_t elementIndex)
3493 ++elementIndex;
3495 if (elementIndex == OSD_ARTIFICIAL_HORIZON) { // always drawn last so skip
3496 elementIndex++;
3499 #ifndef USE_TEMPERATURE_SENSOR
3500 if (elementIndex == OSD_TEMP_SENSOR_0_TEMPERATURE) {
3501 elementIndex = OSD_ALTITUDE_MSL;
3503 #endif
3505 if (!(feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER))) {
3506 if (elementIndex == OSD_POWER) {
3507 elementIndex = OSD_GPS_LON;
3509 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3510 elementIndex = OSD_LEVEL_PIDS;
3512 #ifdef USE_POWER_LIMITS
3513 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3514 elementIndex = OSD_GLIDESLOPE;
3516 #endif
3519 #ifndef USE_POWER_LIMITS
3520 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3521 elementIndex = OSD_GLIDESLOPE;
3523 #endif
3525 if (!feature(FEATURE_CURRENT_METER)) {
3526 if (elementIndex == OSD_CURRENT_DRAW) {
3527 elementIndex = OSD_GPS_SPEED;
3529 if (elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3530 elementIndex = OSD_BATTERY_REMAINING_PERCENT;
3532 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3533 elementIndex = OSD_TRIP_DIST;
3535 if (elementIndex == OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH) {
3536 elementIndex = OSD_HOME_HEADING_ERROR;
3538 if (elementIndex == OSD_CLIMB_EFFICIENCY) {
3539 elementIndex = OSD_NAV_WP_MULTI_MISSION_INDEX;
3543 if (!STATE(ESC_SENSOR_ENABLED)) {
3544 if (elementIndex == OSD_ESC_RPM) {
3545 elementIndex = OSD_AZIMUTH;
3549 if (!feature(FEATURE_GPS)) {
3550 if (elementIndex == OSD_GPS_HDOP || elementIndex == OSD_TRIP_DIST || elementIndex == OSD_3D_SPEED || elementIndex == OSD_MISSION ||
3551 elementIndex == OSD_AZIMUTH || elementIndex == OSD_BATTERY_REMAINING_CAPACITY || elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3552 elementIndex++;
3554 if (elementIndex == OSD_HEADING_GRAPH && !sensors(SENSOR_MAG)) {
3555 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_WH_DRAWN : OSD_BATTERY_REMAINING_PERCENT;
3557 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3558 elementIndex = OSD_ATTITUDE_PITCH;
3560 if (elementIndex == OSD_GPS_SPEED) {
3561 elementIndex = OSD_ALTITUDE;
3563 if (elementIndex == OSD_GPS_LON) {
3564 elementIndex = sensors(SENSOR_MAG) ? OSD_HEADING : OSD_VARIO;
3566 if (elementIndex == OSD_MAP_NORTH) {
3567 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE : OSD_LEVEL_PIDS;
3569 if (elementIndex == OSD_PLUS_CODE) {
3570 elementIndex = OSD_GFORCE;
3572 if (elementIndex == OSD_GLIDESLOPE) {
3573 elementIndex = OSD_AIR_MAX_SPEED;
3575 if (elementIndex == OSD_GLIDE_RANGE) {
3576 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_CLIMB_EFFICIENCY : OSD_ITEM_COUNT;
3578 if (elementIndex == OSD_NAV_WP_MULTI_MISSION_INDEX) {
3579 elementIndex = OSD_ITEM_COUNT;
3583 if (!sensors(SENSOR_ACC)) {
3584 if (elementIndex == OSD_CROSSHAIRS) {
3585 elementIndex = OSD_ONTIME;
3587 if (elementIndex == OSD_GFORCE) {
3588 elementIndex = OSD_RC_SOURCE;
3592 if (elementIndex == OSD_ITEM_COUNT) {
3593 elementIndex = 0;
3595 return elementIndex;
3598 void osdDrawNextElement(void)
3600 static uint8_t elementIndex = 0;
3601 // Flag for end of loop, also prevents infinite loop when no elements are enabled
3602 uint8_t index = elementIndex;
3603 do {
3604 elementIndex = osdIncElementIndex(elementIndex);
3605 } while (!osdDrawSingleElement(elementIndex) && index != elementIndex);
3607 // Draw artificial horizon + tracking telemtry last
3608 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
3609 if (osdConfig()->telemetry>0){
3610 osdDisplayTelemetry();
3614 PG_RESET_TEMPLATE(osdConfig_t, osdConfig,
3615 .rssi_alarm = SETTING_OSD_RSSI_ALARM_DEFAULT,
3616 .time_alarm = SETTING_OSD_TIME_ALARM_DEFAULT,
3617 .alt_alarm = SETTING_OSD_ALT_ALARM_DEFAULT,
3618 .dist_alarm = SETTING_OSD_DIST_ALARM_DEFAULT,
3619 .neg_alt_alarm = SETTING_OSD_NEG_ALT_ALARM_DEFAULT,
3620 .current_alarm = SETTING_OSD_CURRENT_ALARM_DEFAULT,
3621 .imu_temp_alarm_min = SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT,
3622 .imu_temp_alarm_max = SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT,
3623 .esc_temp_alarm_min = SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT,
3624 .esc_temp_alarm_max = SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT,
3625 .gforce_alarm = SETTING_OSD_GFORCE_ALARM_DEFAULT,
3626 .gforce_axis_alarm_min = SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT,
3627 .gforce_axis_alarm_max = SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT,
3628 #ifdef USE_BARO
3629 .baro_temp_alarm_min = SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT,
3630 .baro_temp_alarm_max = SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT,
3631 #endif
3632 #ifdef USE_SERIALRX_CRSF
3633 .snr_alarm = SETTING_OSD_SNR_ALARM_DEFAULT,
3634 .crsf_lq_format = SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT,
3635 .link_quality_alarm = SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT,
3636 .rssi_dbm_alarm = SETTING_OSD_RSSI_DBM_ALARM_DEFAULT,
3637 .rssi_dbm_max = SETTING_OSD_RSSI_DBM_MAX_DEFAULT,
3638 .rssi_dbm_min = SETTING_OSD_RSSI_DBM_MIN_DEFAULT,
3639 #endif
3640 #ifdef USE_TEMPERATURE_SENSOR
3641 .temp_label_align = SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT,
3642 #endif
3643 #ifdef USE_PITOT
3644 .airspeed_alarm_min = SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT,
3645 .airspeed_alarm_max = SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT,
3646 #endif
3648 .video_system = SETTING_OSD_VIDEO_SYSTEM_DEFAULT,
3649 .row_shiftdown = SETTING_OSD_ROW_SHIFTDOWN_DEFAULT,
3650 .msp_displayport_fullframe_interval = SETTING_OSD_MSP_DISPLAYPORT_FULLFRAME_INTERVAL_DEFAULT,
3652 .ahi_reverse_roll = SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT,
3653 .ahi_max_pitch = SETTING_OSD_AHI_MAX_PITCH_DEFAULT,
3654 .crosshairs_style = SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT,
3655 .horizon_offset = SETTING_OSD_HORIZON_OFFSET_DEFAULT,
3656 .camera_uptilt = SETTING_OSD_CAMERA_UPTILT_DEFAULT,
3657 .ahi_camera_uptilt_comp = SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT,
3658 .camera_fov_h = SETTING_OSD_CAMERA_FOV_H_DEFAULT,
3659 .camera_fov_v = SETTING_OSD_CAMERA_FOV_V_DEFAULT,
3660 .hud_margin_h = SETTING_OSD_HUD_MARGIN_H_DEFAULT,
3661 .hud_margin_v = SETTING_OSD_HUD_MARGIN_V_DEFAULT,
3662 .hud_homing = SETTING_OSD_HUD_HOMING_DEFAULT,
3663 .hud_homepoint = SETTING_OSD_HUD_HOMEPOINT_DEFAULT,
3664 .hud_radar_disp = SETTING_OSD_HUD_RADAR_DISP_DEFAULT,
3665 .hud_radar_range_min = SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT,
3666 .hud_radar_range_max = SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT,
3667 .hud_radar_alt_difference_display_time = SETTING_OSD_HUD_RADAR_ALT_DIFFERENCE_DISPLAY_TIME_DEFAULT,
3668 .hud_radar_distance_display_time = SETTING_OSD_HUD_RADAR_DISTANCE_DISPLAY_TIME_DEFAULT,
3669 .hud_wp_disp = SETTING_OSD_HUD_WP_DISP_DEFAULT,
3670 .left_sidebar_scroll = SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT,
3671 .right_sidebar_scroll = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT,
3672 .sidebar_scroll_arrows = SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT,
3673 .sidebar_horizontal_offset = SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT,
3674 .left_sidebar_scroll_step = SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT,
3675 .right_sidebar_scroll_step = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT,
3676 .sidebar_height = SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT,
3677 .ahi_pitch_interval = SETTING_OSD_AHI_PITCH_INTERVAL_DEFAULT,
3678 .osd_home_position_arm_screen = SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT,
3679 .pan_servo_index = SETTING_OSD_PAN_SERVO_INDEX_DEFAULT,
3680 .pan_servo_pwm2centideg = SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT,
3681 .pan_servo_offcentre_warning = SETTING_OSD_PAN_SERVO_OFFCENTRE_WARNING_DEFAULT,
3682 .pan_servo_indicator_show_degrees = SETTING_OSD_PAN_SERVO_INDICATOR_SHOW_DEGREES_DEFAULT,
3683 .esc_rpm_precision = SETTING_OSD_ESC_RPM_PRECISION_DEFAULT,
3684 .mAh_precision = SETTING_OSD_MAH_PRECISION_DEFAULT,
3685 .osd_switch_indicator0_name = SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT,
3686 .osd_switch_indicator0_channel = SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNEL_DEFAULT,
3687 .osd_switch_indicator1_name = SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT,
3688 .osd_switch_indicator1_channel = SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNEL_DEFAULT,
3689 .osd_switch_indicator2_name = SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT,
3690 .osd_switch_indicator2_channel = SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNEL_DEFAULT,
3691 .osd_switch_indicator3_name = SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT,
3692 .osd_switch_indicator3_channel = SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNEL_DEFAULT,
3693 .osd_switch_indicators_align_left = SETTING_OSD_SWITCH_INDICATORS_ALIGN_LEFT_DEFAULT,
3694 .system_msg_display_time = SETTING_OSD_SYSTEM_MSG_DISPLAY_TIME_DEFAULT,
3695 .units = SETTING_OSD_UNITS_DEFAULT,
3696 .main_voltage_decimals = SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT,
3698 #ifdef USE_WIND_ESTIMATOR
3699 .estimations_wind_compensation = SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT,
3700 #endif
3702 .coordinate_digits = SETTING_OSD_COORDINATE_DIGITS_DEFAULT,
3704 .osd_failsafe_switch_layout = SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT,
3706 .plus_code_digits = SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT,
3707 .plus_code_short = SETTING_OSD_PLUS_CODE_SHORT_DEFAULT,
3709 .ahi_width = SETTING_OSD_AHI_WIDTH_DEFAULT,
3710 .ahi_height = SETTING_OSD_AHI_HEIGHT_DEFAULT,
3711 .ahi_vertical_offset = SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT,
3712 .ahi_bordered = SETTING_OSD_AHI_BORDERED_DEFAULT,
3713 .ahi_style = SETTING_OSD_AHI_STYLE_DEFAULT,
3715 .force_grid = SETTING_OSD_FORCE_GRID_DEFAULT,
3717 .stats_energy_unit = SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT,
3718 .stats_min_voltage_unit = SETTING_OSD_STATS_MIN_VOLTAGE_UNIT_DEFAULT,
3719 .stats_page_auto_swap_time = SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT
3722 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t *osdLayoutsConfig)
3724 osdLayoutsConfig->item_pos[0][OSD_ALTITUDE] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG;
3725 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_VOLTAGE] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG;
3726 osdLayoutsConfig->item_pos[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE] = OSD_POS(12, 1);
3728 osdLayoutsConfig->item_pos[0][OSD_RSSI_VALUE] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG;
3729 //line 2
3730 osdLayoutsConfig->item_pos[0][OSD_HOME_DIST] = OSD_POS(1, 1);
3731 osdLayoutsConfig->item_pos[0][OSD_TRIP_DIST] = OSD_POS(1, 2);
3732 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_CELL_VOLTAGE] = OSD_POS(12, 1);
3733 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE] = OSD_POS(12, 1);
3734 osdLayoutsConfig->item_pos[0][OSD_GPS_SPEED] = OSD_POS(23, 1);
3735 osdLayoutsConfig->item_pos[0][OSD_3D_SPEED] = OSD_POS(23, 1);
3736 osdLayoutsConfig->item_pos[0][OSD_GLIDESLOPE] = OSD_POS(23, 2);
3738 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG;
3739 osdLayoutsConfig->item_pos[0][OSD_SCALED_THROTTLE_POS] = OSD_POS(6, 2);
3740 osdLayoutsConfig->item_pos[0][OSD_HEADING] = OSD_POS(12, 2);
3741 osdLayoutsConfig->item_pos[0][OSD_GROUND_COURSE] = OSD_POS(12, 3);
3742 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ERROR] = OSD_POS(12, 2);
3743 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ADJUSTMENT] = OSD_POS(12, 2);
3744 osdLayoutsConfig->item_pos[0][OSD_CROSS_TRACK_ERROR] = OSD_POS(12, 3);
3745 osdLayoutsConfig->item_pos[0][OSD_HEADING_GRAPH] = OSD_POS(18, 2);
3746 osdLayoutsConfig->item_pos[0][OSD_CURRENT_DRAW] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG;
3747 osdLayoutsConfig->item_pos[0][OSD_MAH_DRAWN] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG;
3748 osdLayoutsConfig->item_pos[0][OSD_WH_DRAWN] = OSD_POS(1, 5);
3749 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_CAPACITY] = OSD_POS(1, 6);
3750 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_PERCENT] = OSD_POS(1, 7);
3751 osdLayoutsConfig->item_pos[0][OSD_POWER_SUPPLY_IMPEDANCE] = OSD_POS(1, 8);
3753 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_MAH_PER_KM] = OSD_POS(1, 5);
3754 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_WH_PER_KM] = OSD_POS(1, 5);
3756 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_ROLL] = OSD_POS(1, 7);
3757 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_PITCH] = OSD_POS(1, 8);
3759 // avoid OSD_VARIO under OSD_CROSSHAIRS
3760 osdLayoutsConfig->item_pos[0][OSD_VARIO] = OSD_POS(23, 5);
3761 // OSD_VARIO_NUM at the right of OSD_VARIO
3762 osdLayoutsConfig->item_pos[0][OSD_VARIO_NUM] = OSD_POS(24, 7);
3763 osdLayoutsConfig->item_pos[0][OSD_HOME_DIR] = OSD_POS(14, 11);
3764 osdLayoutsConfig->item_pos[0][OSD_ARTIFICIAL_HORIZON] = OSD_POS(8, 6);
3765 osdLayoutsConfig->item_pos[0][OSD_HORIZON_SIDEBARS] = OSD_POS(8, 6);
3767 osdLayoutsConfig->item_pos[0][OSD_CRAFT_NAME] = OSD_POS(20, 2);
3768 osdLayoutsConfig->item_pos[0][OSD_PILOT_NAME] = OSD_POS(20, 3);
3769 osdLayoutsConfig->item_pos[0][OSD_VTX_CHANNEL] = OSD_POS(8, 6);
3771 #ifdef USE_SERIALRX_CRSF
3772 osdLayoutsConfig->item_pos[0][OSD_CRSF_RSSI_DBM] = OSD_POS(23, 12);
3773 osdLayoutsConfig->item_pos[0][OSD_CRSF_LQ] = OSD_POS(23, 11);
3774 osdLayoutsConfig->item_pos[0][OSD_CRSF_SNR_DB] = OSD_POS(24, 9);
3775 osdLayoutsConfig->item_pos[0][OSD_CRSF_TX_POWER] = OSD_POS(24, 10);
3776 #endif
3778 osdLayoutsConfig->item_pos[0][OSD_ONTIME] = OSD_POS(23, 8);
3779 osdLayoutsConfig->item_pos[0][OSD_FLYTIME] = OSD_POS(23, 9);
3780 osdLayoutsConfig->item_pos[0][OSD_ONTIME_FLYTIME] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG;
3781 osdLayoutsConfig->item_pos[0][OSD_RTC_TIME] = OSD_POS(23, 12);
3782 osdLayoutsConfig->item_pos[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH] = OSD_POS(23, 7);
3783 osdLayoutsConfig->item_pos[0][OSD_REMAINING_DISTANCE_BEFORE_RTH] = OSD_POS(23, 6);
3785 osdLayoutsConfig->item_pos[0][OSD_MISSION] = OSD_POS(0, 10);
3786 osdLayoutsConfig->item_pos[0][OSD_GPS_SATS] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG;
3787 osdLayoutsConfig->item_pos[0][OSD_GPS_HDOP] = OSD_POS(0, 10);
3789 osdLayoutsConfig->item_pos[0][OSD_GPS_LAT] = OSD_POS(0, 12);
3790 // Put this on top of the latitude, since it's very unlikely
3791 // that users will want to use both at the same time.
3792 osdLayoutsConfig->item_pos[0][OSD_PLUS_CODE] = OSD_POS(0, 12);
3793 osdLayoutsConfig->item_pos[0][OSD_FLYMODE] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG;
3794 osdLayoutsConfig->item_pos[0][OSD_GPS_LON] = OSD_POS(18, 12);
3796 osdLayoutsConfig->item_pos[0][OSD_AZIMUTH] = OSD_POS(2, 12);
3798 osdLayoutsConfig->item_pos[0][OSD_ROLL_PIDS] = OSD_POS(2, 10);
3799 osdLayoutsConfig->item_pos[0][OSD_PITCH_PIDS] = OSD_POS(2, 11);
3800 osdLayoutsConfig->item_pos[0][OSD_YAW_PIDS] = OSD_POS(2, 12);
3801 osdLayoutsConfig->item_pos[0][OSD_LEVEL_PIDS] = OSD_POS(2, 12);
3802 osdLayoutsConfig->item_pos[0][OSD_POS_XY_PIDS] = OSD_POS(2, 12);
3803 osdLayoutsConfig->item_pos[0][OSD_POS_Z_PIDS] = OSD_POS(2, 12);
3804 osdLayoutsConfig->item_pos[0][OSD_VEL_XY_PIDS] = OSD_POS(2, 12);
3805 osdLayoutsConfig->item_pos[0][OSD_VEL_Z_PIDS] = OSD_POS(2, 12);
3806 osdLayoutsConfig->item_pos[0][OSD_HEADING_P] = OSD_POS(2, 12);
3807 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_ROLL] = OSD_POS(2, 10);
3808 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_PITCH] = OSD_POS(2, 11);
3809 osdLayoutsConfig->item_pos[0][OSD_RC_EXPO] = OSD_POS(2, 12);
3810 osdLayoutsConfig->item_pos[0][OSD_RC_YAW_EXPO] = OSD_POS(2, 12);
3811 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_EXPO] = OSD_POS(2, 12);
3812 osdLayoutsConfig->item_pos[0][OSD_PITCH_RATE] = OSD_POS(2, 12);
3813 osdLayoutsConfig->item_pos[0][OSD_ROLL_RATE] = OSD_POS(2, 12);
3814 osdLayoutsConfig->item_pos[0][OSD_YAW_RATE] = OSD_POS(2, 12);
3815 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_EXPO] = OSD_POS(2, 12);
3816 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_YAW_EXPO] = OSD_POS(2, 12);
3817 osdLayoutsConfig->item_pos[0][OSD_MANUAL_PITCH_RATE] = OSD_POS(2, 12);
3818 osdLayoutsConfig->item_pos[0][OSD_MANUAL_ROLL_RATE] = OSD_POS(2, 12);
3819 osdLayoutsConfig->item_pos[0][OSD_MANUAL_YAW_RATE] = OSD_POS(2, 12);
3820 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_CRUISE_THR] = OSD_POS(2, 12);
3821 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_PITCH2THR] = OSD_POS(2, 12);
3822 osdLayoutsConfig->item_pos[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE] = OSD_POS(2, 12);
3823 osdLayoutsConfig->item_pos[0][OSD_FW_ALT_PID_OUTPUTS] = OSD_POS(2, 12);
3824 osdLayoutsConfig->item_pos[0][OSD_FW_POS_PID_OUTPUTS] = OSD_POS(2, 12);
3825 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_X_PID_OUTPUTS] = OSD_POS(2, 12);
3826 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Y_PID_OUTPUTS] = OSD_POS(2, 12);
3827 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Z_PID_OUTPUTS] = OSD_POS(2, 12);
3828 osdLayoutsConfig->item_pos[0][OSD_MC_POS_XYZ_P_OUTPUTS] = OSD_POS(2, 12);
3830 osdLayoutsConfig->item_pos[0][OSD_POWER] = OSD_POS(15, 1);
3832 osdLayoutsConfig->item_pos[0][OSD_IMU_TEMPERATURE] = OSD_POS(19, 2);
3833 osdLayoutsConfig->item_pos[0][OSD_BARO_TEMPERATURE] = OSD_POS(19, 3);
3834 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_0_TEMPERATURE] = OSD_POS(19, 4);
3835 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_1_TEMPERATURE] = OSD_POS(19, 5);
3836 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_2_TEMPERATURE] = OSD_POS(19, 6);
3837 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_3_TEMPERATURE] = OSD_POS(19, 7);
3838 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_4_TEMPERATURE] = OSD_POS(19, 8);
3839 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_5_TEMPERATURE] = OSD_POS(19, 9);
3840 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_6_TEMPERATURE] = OSD_POS(19, 10);
3841 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_7_TEMPERATURE] = OSD_POS(19, 11);
3843 osdLayoutsConfig->item_pos[0][OSD_AIR_SPEED] = OSD_POS(3, 5);
3844 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_HORIZONTAL] = OSD_POS(3, 6);
3845 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_VERTICAL] = OSD_POS(3, 7);
3847 osdLayoutsConfig->item_pos[0][OSD_GFORCE] = OSD_POS(12, 4);
3848 osdLayoutsConfig->item_pos[0][OSD_GFORCE_X] = OSD_POS(12, 5);
3849 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Y] = OSD_POS(12, 6);
3850 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Z] = OSD_POS(12, 7);
3852 osdLayoutsConfig->item_pos[0][OSD_VTX_POWER] = OSD_POS(3, 5);
3854 osdLayoutsConfig->item_pos[0][OSD_GVAR_0] = OSD_POS(1, 1);
3855 osdLayoutsConfig->item_pos[0][OSD_GVAR_1] = OSD_POS(1, 2);
3856 osdLayoutsConfig->item_pos[0][OSD_GVAR_2] = OSD_POS(1, 3);
3857 osdLayoutsConfig->item_pos[0][OSD_GVAR_3] = OSD_POS(1, 4);
3859 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_0] = OSD_POS(2, 7);
3860 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_1] = OSD_POS(2, 8);
3861 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_2] = OSD_POS(2, 9);
3862 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_3] = OSD_POS(2, 10);
3864 #if defined(USE_ESC_SENSOR)
3865 osdLayoutsConfig->item_pos[0][OSD_ESC_RPM] = OSD_POS(1, 2);
3866 osdLayoutsConfig->item_pos[0][OSD_ESC_TEMPERATURE] = OSD_POS(1, 3);
3867 #endif
3869 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3870 osdLayoutsConfig->item_pos[0][OSD_RC_SOURCE] = OSD_POS(3, 4);
3871 #endif
3873 #ifdef USE_POWER_LIMITS
3874 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_REMAINING_BURST_TIME] = OSD_POS(3, 4);
3875 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT] = OSD_POS(3, 5);
3876 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT] = OSD_POS(3, 6);
3877 #endif
3879 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
3880 osdLayoutsConfig->item_pos[0][OSD_MESSAGES] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG;
3882 for (unsigned ii = 1; ii < OSD_LAYOUT_COUNT; ii++) {
3883 for (unsigned jj = 0; jj < ARRAYLEN(osdLayoutsConfig->item_pos[0]); jj++) {
3884 osdLayoutsConfig->item_pos[ii][jj] = osdLayoutsConfig->item_pos[0][jj] & ~OSD_VISIBLE_FLAG;
3889 static void osdSetNextRefreshIn(uint32_t timeMs) {
3890 resumeRefreshAt = micros() + timeMs * 1000;
3891 refreshWaitForResumeCmdRelease = true;
3894 static void osdCompleteAsyncInitialization(void)
3896 if (!displayIsReady(osdDisplayPort)) {
3897 // Update the display.
3898 // XXX: Rename displayDrawScreen() and associated functions
3899 // to displayUpdate()
3900 displayDrawScreen(osdDisplayPort);
3901 return;
3904 osdDisplayIsReady = true;
3906 #if defined(USE_CANVAS)
3907 if (osdConfig()->force_grid) {
3908 osdDisplayHasCanvas = false;
3909 } else {
3910 osdDisplayHasCanvas = displayGetCanvas(&osdCanvas, osdDisplayPort);
3912 #endif
3914 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3915 displayClearScreen(osdDisplayPort);
3917 uint8_t y = 1;
3918 displayFontMetadata_t metadata;
3919 bool fontHasMetadata = displayGetFontMetadata(&metadata, osdDisplayPort);
3920 LOG_DEBUG(OSD, "Font metadata version %s: %u (%u chars)",
3921 fontHasMetadata ? "Y" : "N", metadata.version, metadata.charCount);
3923 if (fontHasMetadata && metadata.charCount > 256) {
3924 hasExtendedFont = true;
3925 unsigned logo_c = SYM_LOGO_START;
3926 unsigned logo_x = OSD_CENTER_LEN(SYM_LOGO_WIDTH);
3927 for (unsigned ii = 0; ii < SYM_LOGO_HEIGHT; ii++) {
3928 for (unsigned jj = 0; jj < SYM_LOGO_WIDTH; jj++) {
3929 displayWriteChar(osdDisplayPort, logo_x + jj, y, logo_c++);
3931 y++;
3933 y++;
3934 } else if (!fontHasMetadata) {
3935 const char *m = "INVALID FONT";
3936 displayWrite(osdDisplayPort, OSD_CENTER_S(m), 3, m);
3937 y = 4;
3940 if (fontHasMetadata && metadata.version < OSD_MIN_FONT_VERSION) {
3941 const char *m = "INVALID FONT VERSION";
3942 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
3945 char string_buffer[30];
3946 tfp_sprintf(string_buffer, "INAV VERSION: %s", FC_VERSION_STRING);
3947 uint8_t xPos = osdDisplayIsHD() ? 15 : 5;
3948 displayWrite(osdDisplayPort, xPos, y++, string_buffer);
3949 #ifdef USE_CMS
3950 displayWrite(osdDisplayPort, xPos+2, y++, CMS_STARTUP_HELP_TEXT1);
3951 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT2);
3952 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT3);
3953 #endif
3954 #ifdef USE_STATS
3957 uint8_t statNameX = osdDisplayIsHD() ? 14 : 4;
3958 uint8_t statValueX = osdDisplayIsHD() ? 34 : 24;
3960 if (statsConfig()->stats_enabled) {
3961 displayWrite(osdDisplayPort, statNameX, ++y, "ODOMETER:");
3962 switch (osdConfig()->units) {
3963 case OSD_UNIT_UK:
3964 FALLTHROUGH;
3965 case OSD_UNIT_IMPERIAL:
3966 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_MILE));
3967 string_buffer[5] = SYM_MI;
3968 break;
3969 default:
3970 case OSD_UNIT_GA:
3971 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE));
3972 string_buffer[5] = SYM_NM;
3973 break;
3974 case OSD_UNIT_METRIC_MPH:
3975 FALLTHROUGH;
3976 case OSD_UNIT_METRIC:
3977 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER));
3978 string_buffer[5] = SYM_KM;
3979 break;
3981 string_buffer[6] = '\0';
3982 displayWrite(osdDisplayPort, statValueX-5, y, string_buffer);
3984 displayWrite(osdDisplayPort, statNameX, ++y, "TOTAL TIME:");
3985 uint32_t tot_mins = statsConfig()->stats_total_time / 60;
3986 tfp_sprintf(string_buffer, "%2d:%02dHM", (int)(tot_mins / 60), (int)(tot_mins % 60));
3987 displayWrite(osdDisplayPort, statValueX-5, y, string_buffer);
3989 #ifdef USE_ADC
3990 if (feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER)) {
3991 displayWrite(osdDisplayPort, statNameX, ++y, "TOTAL ENERGY:");
3992 osdFormatCentiNumber(string_buffer, statsConfig()->stats_total_energy / 10, 0, 2, 0, 4);
3993 strcat(string_buffer, "\xAB"); // SYM_WH
3994 displayWrite(osdDisplayPort, statValueX-4, y, string_buffer);
3996 displayWrite(osdDisplayPort, statNameX, ++y, "AVG EFFICIENCY:");
3997 if (statsConfig()->stats_total_dist) {
3998 uint32_t avg_efficiency = statsConfig()->stats_total_energy / (statsConfig()->stats_total_dist / METERS_PER_KILOMETER); // mWh/km
3999 switch (osdConfig()->units) {
4000 case OSD_UNIT_UK:
4001 FALLTHROUGH;
4002 case OSD_UNIT_IMPERIAL:
4003 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
4004 string_buffer[3] = SYM_WH_MI;
4005 break;
4006 case OSD_UNIT_GA:
4007 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
4008 string_buffer[3] = SYM_WH_NM;
4009 break;
4010 default:
4011 case OSD_UNIT_METRIC_MPH:
4012 FALLTHROUGH;
4013 case OSD_UNIT_METRIC:
4014 osdFormatCentiNumber(string_buffer, avg_efficiency / 10000 * METERS_PER_MILE, 0, 2, 0, 3);
4015 string_buffer[3] = SYM_WH_KM;
4016 break;
4018 } else {
4019 string_buffer[0] = string_buffer[1] = string_buffer[2] = '-';
4021 string_buffer[4] = '\0';
4022 displayWrite(osdDisplayPort, statValueX-3, y, string_buffer);
4024 #endif // USE_ADC
4026 #endif
4028 displayCommitTransaction(osdDisplayPort);
4029 displayResync(osdDisplayPort);
4030 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME);
4033 void osdInit(displayPort_t *osdDisplayPortToUse)
4035 if (!osdDisplayPortToUse)
4036 return;
4038 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(63,63));
4040 osdDisplayPort = osdDisplayPortToUse;
4042 #ifdef USE_CMS
4043 cmsDisplayPortRegister(osdDisplayPort);
4044 #endif
4046 armState = ARMING_FLAG(ARMED);
4047 osdCompleteAsyncInitialization();
4050 static void osdResetStats(void)
4052 stats.max_current = 0;
4053 stats.max_power = 0;
4054 stats.max_speed = 0;
4055 stats.max_3D_speed = 0;
4056 stats.max_air_speed = 0;
4057 stats.min_voltage = 5000;
4058 stats.min_rssi = 99;
4059 stats.min_lq = 300;
4060 stats.min_rssi_dbm = 0;
4061 stats.max_altitude = 0;
4064 static void osdUpdateStats(void)
4066 int32_t value;
4068 if (feature(FEATURE_GPS)) {
4069 value = osdGet3DSpeed();
4070 const float airspeed_estimate = getAirspeedEstimate();
4072 if (stats.max_3D_speed < value)
4073 stats.max_3D_speed = value;
4075 if (stats.max_speed < gpsSol.groundSpeed)
4076 stats.max_speed = gpsSol.groundSpeed;
4078 if (stats.max_air_speed < airspeed_estimate)
4079 stats.max_air_speed = airspeed_estimate;
4081 if (stats.max_distance < GPS_distanceToHome)
4082 stats.max_distance = GPS_distanceToHome;
4085 value = getBatteryVoltage();
4086 if (stats.min_voltage > value)
4087 stats.min_voltage = value;
4089 value = abs(getAmperage());
4090 if (stats.max_current < value)
4091 stats.max_current = value;
4093 value = labs(getPower());
4094 if (stats.max_power < value)
4095 stats.max_power = value;
4097 value = osdConvertRSSI();
4098 if (stats.min_rssi > value)
4099 stats.min_rssi = value;
4101 value = osdGetCrsfLQ();
4102 if (stats.min_lq > value)
4103 stats.min_lq = value;
4105 if (!failsafeIsReceivingRxData())
4106 stats.min_lq = 0;
4108 value = osdGetCrsfdBm();
4109 if (stats.min_rssi_dbm > value)
4110 stats.min_rssi_dbm = value;
4112 stats.max_altitude = MAX(stats.max_altitude, osdGetAltitude());
4115 static void osdShowStats(bool isSinglePageStatsCompatible, uint8_t page)
4117 const char * disarmReasonStr[DISARM_REASON_COUNT] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "KILLSW", "FAILSAFE", "NAV SYS", "LANDING"};
4118 uint8_t top = 1; // Start one line down leaving space at the top of the screen.
4119 size_t multiValueLengthOffset = 0;
4121 const uint8_t statNameX = osdDisplayIsHD() ? 11 : 1;
4122 const uint8_t statValuesX = osdDisplayIsHD() ? 30 : 20;
4123 char buff[10];
4125 if (page > 1)
4126 page = 0;
4128 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4129 displayClearScreen(osdDisplayPort);
4131 if (isSinglePageStatsCompatible) {
4132 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS ---");
4133 } else if (page == 0) {
4134 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- 1/2 ->");
4135 } else if (page == 1) {
4136 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- <- 2/2");
4139 if (isSinglePageStatsCompatible || page == 0) {
4140 if (feature(FEATURE_GPS)) {
4141 if (isSinglePageStatsCompatible) {
4142 displayWrite(osdDisplayPort, statNameX, top, "MAX/AVG SPEED :");
4143 osdFormatVelocityStr(buff, stats.max_3D_speed, true, false);
4144 osdLeftAlignString(buff);
4145 strcat(osdFormatTrimWhiteSpace(buff),"/");
4146 multiValueLengthOffset = strlen(buff);
4147 displayWrite(osdDisplayPort, statValuesX, top, buff);
4148 osdGenerateAverageVelocityStr(buff);
4149 osdLeftAlignString(buff);
4150 displayWrite(osdDisplayPort, statValuesX + multiValueLengthOffset, top++, buff);
4151 } else {
4152 displayWrite(osdDisplayPort, statNameX, top, "MAX SPEED :");
4153 osdFormatVelocityStr(buff, stats.max_3D_speed, true, false);
4154 osdLeftAlignString(buff);
4155 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4157 displayWrite(osdDisplayPort, statNameX, top, "AVG SPEED :");
4158 osdGenerateAverageVelocityStr(buff);
4159 osdLeftAlignString(buff);
4160 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4163 displayWrite(osdDisplayPort, statNameX, top, "MAX DISTANCE :");
4164 osdFormatDistanceStr(buff, stats.max_distance*100);
4165 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4167 displayWrite(osdDisplayPort, statNameX, top, "TRAVELED DISTANCE:");
4168 osdFormatDistanceStr(buff, getTotalTravelDistance());
4169 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4172 displayWrite(osdDisplayPort, statNameX, top, "MAX ALTITUDE :");
4173 osdFormatAltitudeStr(buff, stats.max_altitude);
4174 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4176 switch (rxConfig()->serialrx_provider) {
4177 case SERIALRX_CRSF:
4178 if (isSinglePageStatsCompatible) {
4179 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI %/DBM :");
4180 itoa(stats.min_rssi, buff, 10);
4181 osdLeftAlignString(buff);
4182 strcat(osdFormatTrimWhiteSpace(buff), "%/");
4183 multiValueLengthOffset = strlen(buff);
4184 displayWrite(osdDisplayPort, statValuesX, top, buff);
4185 itoa(stats.min_rssi_dbm, buff, 10);
4186 tfp_sprintf(buff, "%s%c", buff, SYM_DBM);
4187 osdLeftAlignString(buff);
4188 displayWrite(osdDisplayPort, statValuesX + multiValueLengthOffset, top++, buff);
4189 } else {
4190 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI % :");
4191 itoa(stats.min_rssi, buff, 10);
4192 strcat(buff, "%");
4193 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4195 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI DBM :");
4196 itoa(stats.min_rssi_dbm, buff, 10);
4197 tfp_sprintf(buff, "%s%c", buff, SYM_DBM);
4198 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4201 displayWrite(osdDisplayPort, statNameX, top, "MIN LQ :");
4202 itoa(stats.min_lq, buff, 10);
4203 strcat(buff, "%");
4204 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4205 break;
4206 default:
4207 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI :");
4208 itoa(stats.min_rssi, buff, 10);
4209 strcat(buff, "%");
4210 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4213 displayWrite(osdDisplayPort, statNameX, top, "FLY TIME :");
4214 uint16_t flySeconds = getFlightTime();
4215 uint16_t flyMinutes = flySeconds / 60;
4216 flySeconds %= 60;
4217 uint16_t flyHours = flyMinutes / 60;
4218 flyMinutes %= 60;
4219 tfp_sprintf(buff, "%02u:%02u:%02u", flyHours, flyMinutes, flySeconds);
4220 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4222 displayWrite(osdDisplayPort, statNameX, top, "DISARMED BY :");
4223 displayWrite(osdDisplayPort, statValuesX, top++, disarmReasonStr[getDisarmReason()]);
4226 if (isSinglePageStatsCompatible || page == 1) {
4227 if (osdConfig()->stats_min_voltage_unit == OSD_STATS_MIN_VOLTAGE_UNIT_BATTERY) {
4228 displayWrite(osdDisplayPort, statNameX, top, "MIN BATTERY VOLT :");
4229 osdFormatCentiNumber(buff, stats.min_voltage, 0, osdConfig()->main_voltage_decimals, 0, osdConfig()->main_voltage_decimals + 2);
4230 } else {
4231 displayWrite(osdDisplayPort, statNameX, top, "MIN CELL VOLTAGE :");
4232 osdFormatCentiNumber(buff, stats.min_voltage/getBatteryCellCount(), 0, 2, 0, 3);
4234 tfp_sprintf(buff, "%s%c", buff, SYM_VOLT);
4235 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4237 if (feature(FEATURE_CURRENT_METER)) {
4238 displayWrite(osdDisplayPort, statNameX, top, "MAX CURRENT :");
4239 osdFormatCentiNumber(buff, stats.max_current, 0, 2, 0, 3);
4240 tfp_sprintf(buff, "%s%c", buff, SYM_AMP);
4241 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4243 displayWrite(osdDisplayPort, statNameX, top, "MAX POWER :");
4244 bool kiloWatt = osdFormatCentiNumber(buff, stats.max_power, 1000, 2, 2, 3);
4245 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
4246 buff[4] = '\0';
4247 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4249 displayWrite(osdDisplayPort, statNameX, top, "USED CAPACITY :");
4250 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4251 tfp_sprintf(buff, "%d%c", (int)getMAhDrawn(), SYM_MAH);
4252 } else {
4253 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
4254 tfp_sprintf(buff, "%s%c", buff, SYM_WH);
4256 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4258 int32_t totalDistance = getTotalTravelDistance();
4259 bool moreThanAh = false;
4260 bool efficiencyValid = totalDistance >= 10000;
4261 if (feature(FEATURE_GPS)) {
4262 displayWrite(osdDisplayPort, statNameX, top, "AVG EFFICIENCY :");
4263 switch (osdConfig()->units) {
4264 case OSD_UNIT_UK:
4265 FALLTHROUGH;
4266 case OSD_UNIT_IMPERIAL:
4267 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4268 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, 3);
4269 if (!moreThanAh) {
4270 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
4271 } else {
4272 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
4274 if (!efficiencyValid) {
4275 buff[0] = buff[1] = buff[2] = '-';
4276 buff[3] = SYM_MAH_MI_0;
4277 buff[4] = SYM_MAH_MI_1;
4278 buff[5] = '\0';
4280 } else {
4281 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, 3);
4282 tfp_sprintf(buff, "%s%c", buff, SYM_WH_MI);
4283 if (!efficiencyValid) {
4284 buff[0] = buff[1] = buff[2] = '-';
4287 break;
4288 case OSD_UNIT_GA:
4289 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4290 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, 3);
4291 if (!moreThanAh) {
4292 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
4293 } else {
4294 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
4296 if (!efficiencyValid) {
4297 buff[0] = buff[1] = buff[2] = '-';
4298 buff[3] = SYM_MAH_NM_0;
4299 buff[4] = SYM_MAH_NM_1;
4300 buff[5] = '\0';
4302 } else {
4303 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, 3);
4304 tfp_sprintf(buff, "%s%c", buff, SYM_WH_NM);
4305 if (!efficiencyValid) {
4306 buff[0] = buff[1] = buff[2] = '-';
4309 break;
4310 case OSD_UNIT_METRIC_MPH:
4311 FALLTHROUGH;
4312 case OSD_UNIT_METRIC:
4313 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4314 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000000.0f / totalDistance), 1000, 0, 2, 3);
4315 if (!moreThanAh) {
4316 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
4317 } else {
4318 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
4320 if (!efficiencyValid) {
4321 buff[0] = buff[1] = buff[2] = '-';
4322 buff[3] = SYM_MAH_KM_0;
4323 buff[4] = SYM_MAH_KM_1;
4324 buff[5] = '\0';
4326 } else {
4327 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10000.0f / totalDistance), 0, 2, 0, 3);
4328 tfp_sprintf(buff, "%s%c", buff, SYM_WH_KM);
4329 if (!efficiencyValid) {
4330 buff[0] = buff[1] = buff[2] = '-';
4333 break;
4335 osdLeftAlignString(buff);
4336 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4340 const float max_gforce = accGetMeasuredMaxG();
4341 displayWrite(osdDisplayPort, statNameX, top, "MAX G-FORCE :");
4342 osdFormatCentiNumber(buff, max_gforce * 100, 0, 2, 0, 3);
4343 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4345 const acc_extremes_t *acc_extremes = accGetMeasuredExtremes();
4346 const float acc_extremes_min = acc_extremes[Z].min;
4347 const float acc_extremes_max = acc_extremes[Z].max;
4348 displayWrite(osdDisplayPort, statNameX, top, "MIN/MAX Z G-FORCE:");
4349 osdFormatCentiNumber(buff, acc_extremes_min * 100, 0, 2, 0, 4);
4350 osdLeftAlignString(buff);
4351 strcat(osdFormatTrimWhiteSpace(buff),"/");
4352 multiValueLengthOffset = strlen(buff);
4353 displayWrite(osdDisplayPort, statValuesX, top, buff);
4354 osdFormatCentiNumber(buff, acc_extremes_max * 100, 0, 2, 0, 3);
4355 osdLeftAlignString(buff);
4356 displayWrite(osdDisplayPort, statValuesX + multiValueLengthOffset, top++, buff);
4359 if (savingSettings == true) {
4360 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS));
4361 } else if (notify_settings_saved > 0) {
4362 if (millis() > notify_settings_saved) {
4363 notify_settings_saved = 0;
4364 } else {
4365 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED));
4369 displayCommitTransaction(osdDisplayPort);
4372 // called when motors armed
4373 static void osdShowArmed(void)
4375 dateTime_t dt;
4376 char buf[MAX(32, FORMATTED_DATE_TIME_BUFSIZE)];
4377 char craftNameBuf[MAX_NAME_LENGTH];
4378 char versionBuf[30];
4379 char *date;
4380 char *time;
4381 // We need 12 visible rows, start row never < first fully visible row 1
4382 uint8_t y = osdDisplayPort->rows > 13 ? (osdDisplayPort->rows - 12) / 2 : 1;
4384 displayClearScreen(osdDisplayPort);
4385 strcpy(buf, "ARMED");
4386 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4387 y += 2;
4389 if (strlen(systemConfig()->craftName) > 0) {
4390 osdFormatCraftName(craftNameBuf);
4391 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(systemConfig()->craftName)) / 2, y, craftNameBuf );
4392 y += 1;
4394 if (posControl.waypointListValid && posControl.waypointCount > 0) {
4395 #ifdef USE_MULTI_MISSION
4396 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
4397 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4398 #else
4399 strcpy(buf, "*MISSION LOADED*");
4400 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4401 #endif
4403 y += 1;
4405 #if defined(USE_GPS)
4406 if (feature(FEATURE_GPS)) {
4407 if (STATE(GPS_FIX_HOME)) {
4408 if (osdConfig()->osd_home_position_arm_screen){
4409 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
4410 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4411 osdFormatCoordinate(buf, SYM_LON, GPS_home.lon);
4412 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 1, buf);
4413 int digits = osdConfig()->plus_code_digits;
4414 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
4415 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 2, buf);
4417 y += 4;
4418 #if defined (USE_SAFE_HOME)
4419 if (safehome_distance) { // safehome found during arming
4420 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
4421 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
4422 } else {
4423 char buf2[12]; // format the distance first
4424 osdFormatDistanceStr(buf2, safehome_distance);
4425 tfp_sprintf(buf, "%c - %s -> SAFEHOME %u", SYM_HOME, buf2, safehome_index);
4427 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
4428 // write this message above the ARMED message to make it obvious
4429 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y - 8, buf, elemAttr);
4431 #endif
4432 } else {
4433 strcpy(buf, "!NO HOME POSITION!");
4434 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4435 y += 1;
4438 #endif
4440 if (rtcGetDateTime(&dt)) {
4441 dateTimeFormatLocal(buf, &dt);
4442 dateTimeSplitFormatted(buf, &date, &time);
4444 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(date)) / 2, y, date);
4445 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(time)) / 2, y + 1, time);
4446 y += 3;
4449 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
4450 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, y, versionBuf);
4453 static void osdFilterData(timeUs_t currentTimeUs) {
4454 static timeUs_t lastRefresh = 0;
4455 float refresh_dT = US2S(cmpTimeUs(currentTimeUs, lastRefresh));
4457 GForce = fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF)) / GRAVITY_MSS;
4458 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) GForceAxis[axis] = imuMeasuredAccelBF.v[axis] / GRAVITY_MSS;
4460 if (lastRefresh) {
4461 GForce = pt1FilterApply3(&GForceFilter, GForce, refresh_dT);
4462 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) pt1FilterApply3(GForceFilterAxis + axis, GForceAxis[axis], refresh_dT);
4463 } else {
4464 pt1FilterInitRC(&GForceFilter, GFORCE_FILTER_TC, 0);
4465 pt1FilterReset(&GForceFilter, GForce);
4467 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) {
4468 pt1FilterInitRC(GForceFilterAxis + axis, GFORCE_FILTER_TC, 0);
4469 pt1FilterReset(GForceFilterAxis + axis, GForceAxis[axis]);
4473 lastRefresh = currentTimeUs;
4476 // Detect when the user is holding the roll stick to the right
4477 static bool osdIsPageUpStickCommandHeld(void)
4479 static int pageUpHoldCount = 1;
4481 bool keyHeld = false;
4483 if (IS_HI(ROLL)) {
4484 keyHeld = true;
4487 if (!keyHeld) {
4488 pageUpHoldCount = 1;
4489 } else {
4490 ++pageUpHoldCount;
4493 if (pageUpHoldCount > 20) {
4494 pageUpHoldCount = 1;
4495 return true;
4498 return false;
4501 // Detect when the user is holding the roll stick to the left
4502 static bool osdIsPageDownStickCommandHeld(void)
4504 static int pageDownHoldCount = 1;
4506 bool keyHeld = false;
4507 if (IS_LO(ROLL)) {
4508 keyHeld = true;
4511 if (!keyHeld) {
4512 pageDownHoldCount = 1;
4513 } else {
4514 ++pageDownHoldCount;
4517 if (pageDownHoldCount > 20) {
4518 pageDownHoldCount = 1;
4519 return true;
4522 return false;
4525 static void osdRefresh(timeUs_t currentTimeUs)
4527 osdFilterData(currentTimeUs);
4529 #ifdef USE_CMS
4530 if (IS_RC_MODE_ACTIVE(BOXOSD) && (!cmsInMenu) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4531 #else
4532 if (IS_RC_MODE_ACTIVE(BOXOSD) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4533 #endif
4534 displayClearScreen(osdDisplayPort);
4535 armState = ARMING_FLAG(ARMED);
4536 return;
4539 bool statsSinglePageCompatible = (osdDisplayPort->rows >= OSD_STATS_SINGLE_PAGE_MIN_ROWS);
4540 static uint8_t statsCurrentPage = 0;
4541 static bool statsDisplayed = false;
4542 static bool statsAutoPagingEnabled = true;
4544 // Detect arm/disarm
4545 if (armState != ARMING_FLAG(ARMED)) {
4546 if (ARMING_FLAG(ARMED)) {
4547 // Display the "Arming" screen
4548 statsDisplayed = false;
4549 osdResetStats();
4550 osdShowArmed();
4551 uint32_t delay = ARMED_SCREEN_DISPLAY_TIME;
4552 #if defined(USE_SAFE_HOME)
4553 if (safehome_distance)
4554 delay *= 3;
4555 #endif
4556 osdSetNextRefreshIn(delay);
4557 } else {
4558 // Display the "Stats" screen
4559 statsDisplayed = true;
4560 statsCurrentPage = 0;
4561 statsAutoPagingEnabled = osdConfig()->stats_page_auto_swap_time > 0 ? true : false;
4562 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
4563 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME);
4566 armState = ARMING_FLAG(ARMED);
4569 // This block is entered when we're showing the "Splash", "Armed" or "Stats" screens
4570 if (resumeRefreshAt) {
4572 // Handle events only when the "Stats" screen is being displayed.
4573 if (statsDisplayed) {
4575 // Manual paging stick commands are only applicable to multi-page stats.
4576 // ******************************
4577 // For single-page stats, this effectively disables the ability to cancel the
4578 // automatic paging/updates with the stick commands. So unless stats_page_auto_swap_time
4579 // is set to 0 or greater than 4 (saved settings display interval is 5 seconds), then
4580 // "Saved Settings" should display if it is active within the refresh interval.
4581 // ******************************
4582 // With multi-page stats, "Saved Settings" could also be missed if the user
4583 // has canceled automatic paging using the stick commands, because that is only
4584 // updated when osdShowStats() is called. So, in that case, they would only see
4585 // the "Saved Settings" message if they happen to manually change pages using the
4586 // stick commands within the interval the message is displayed.
4587 bool manualPageUpRequested = false;
4588 bool manualPageDownRequested = false;
4589 if (!statsSinglePageCompatible) {
4590 // These methods ensure the paging stick commands are held for a brief period
4591 // Otherwise it can result in a race condition where the stats are
4592 // updated too quickly and can result in partial blanks, etc.
4593 if (osdIsPageUpStickCommandHeld()) {
4594 manualPageUpRequested = true;
4595 statsAutoPagingEnabled = false;
4596 } else if (osdIsPageDownStickCommandHeld()) {
4597 manualPageDownRequested = true;
4598 statsAutoPagingEnabled = false;
4602 if (statsAutoPagingEnabled) {
4603 // Alternate screens for multi-page stats.
4604 // Also, refreshes screen at swap interval for single-page stats.
4605 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time * 1000), 2)) {
4606 if (statsCurrentPage == 0) {
4607 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
4608 statsCurrentPage = 1;
4610 } else {
4611 if (statsCurrentPage == 1) {
4612 osdShowStats(statsSinglePageCompatible, statsCurrentPage);
4613 statsCurrentPage = 0;
4616 } else {
4617 // Process manual page change events for multi-page stats.
4618 if (manualPageUpRequested) {
4619 osdShowStats(statsSinglePageCompatible, 1);
4620 statsCurrentPage = 1;
4621 } else if (manualPageDownRequested) {
4622 osdShowStats(statsSinglePageCompatible, 0);
4623 statsCurrentPage = 0;
4628 // Handle events when either "Splash", "Armed" or "Stats" screens are displayed.
4629 if ((currentTimeUs > resumeRefreshAt) || OSD_RESUME_UPDATES_STICK_COMMAND) {
4630 // Time elapsed or canceled by stick commands.
4631 // Exit to normal OSD operation.
4632 displayClearScreen(osdDisplayPort);
4633 resumeRefreshAt = 0;
4634 statsDisplayed = false;
4635 } else {
4636 // Continue "Splash", "Armed" or "Stats" screens.
4637 displayHeartbeat(osdDisplayPort);
4640 return;
4643 #ifdef USE_CMS
4644 if (!displayIsGrabbed(osdDisplayPort)) {
4645 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4646 if (fullRedraw) {
4647 displayClearScreen(osdDisplayPort);
4648 fullRedraw = false;
4650 osdDrawNextElement();
4651 displayHeartbeat(osdDisplayPort);
4652 displayCommitTransaction(osdDisplayPort);
4653 #ifdef OSD_CALLS_CMS
4654 } else {
4655 cmsUpdate(currentTimeUs);
4656 #endif
4658 #endif
4662 * Called periodically by the scheduler
4664 void osdUpdate(timeUs_t currentTimeUs)
4666 static uint32_t counter = 0;
4668 // don't touch buffers if DMA transaction is in progress
4669 if (displayIsTransferInProgress(osdDisplayPort)) {
4670 return;
4673 if (!osdDisplayIsReady) {
4674 osdCompleteAsyncInitialization();
4675 return;
4678 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
4679 // Check if the layout has changed. Higher numbered
4680 // boxes take priority.
4681 unsigned activeLayout;
4682 if (layoutOverride >= 0) {
4683 activeLayout = layoutOverride;
4684 // Check for timed override, it will go into effect on
4685 // the next OSD iteration
4686 if (layoutOverrideUntil > 0 && millis() > layoutOverrideUntil) {
4687 layoutOverrideUntil = 0;
4688 layoutOverride = -1;
4690 } else if (osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE)) {
4691 activeLayout = 0;
4692 } else {
4693 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
4694 if (IS_RC_MODE_ACTIVE(BOXOSDALT3))
4695 activeLayout = 3;
4696 else
4697 #endif
4698 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
4699 if (IS_RC_MODE_ACTIVE(BOXOSDALT2))
4700 activeLayout = 2;
4701 else
4702 #endif
4703 if (IS_RC_MODE_ACTIVE(BOXOSDALT1))
4704 activeLayout = 1;
4705 else
4706 #ifdef USE_PROGRAMMING_FRAMEWORK
4707 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT))
4708 activeLayout = constrain(logicConditionValuesByType[LOGIC_CONDITION_SET_OSD_LAYOUT], 0, OSD_ALTERNATE_LAYOUT_COUNT);
4709 else
4710 #endif
4711 activeLayout = 0;
4713 if (currentLayout != activeLayout) {
4714 currentLayout = activeLayout;
4715 osdStartFullRedraw();
4717 #endif
4719 #define DRAW_FREQ_DENOM 4
4720 #define STATS_FREQ_DENOM 50
4721 counter++;
4723 if ((counter % STATS_FREQ_DENOM) == 0) {
4724 osdUpdateStats();
4727 if ((counter % DRAW_FREQ_DENOM) == 0) {
4728 // redraw values in buffer
4729 osdRefresh(currentTimeUs);
4730 } else {
4731 // rest of time redraw screen
4732 displayDrawScreen(osdDisplayPort);
4735 #ifdef USE_CMS
4736 // do not allow ARM if we are in menu
4737 if (displayIsGrabbed(osdDisplayPort)) {
4738 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4739 } else {
4740 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4742 #endif
4745 void osdStartFullRedraw(void)
4747 fullRedraw = true;
4750 void osdOverrideLayout(int layout, timeMs_t duration)
4752 layoutOverride = constrain(layout, -1, ARRAYLEN(osdLayoutsConfig()->item_pos) - 1);
4753 if (layoutOverride >= 0 && duration > 0) {
4754 layoutOverrideUntil = millis() + duration;
4755 } else {
4756 layoutOverrideUntil = 0;
4760 int osdGetActiveLayout(bool *overridden)
4762 if (overridden) {
4763 *overridden = layoutOverride >= 0;
4765 return currentLayout;
4768 bool osdItemIsFixed(osd_items_e item)
4770 return item == OSD_CROSSHAIRS ||
4771 item == OSD_ARTIFICIAL_HORIZON ||
4772 item == OSD_HORIZON_SIDEBARS;
4775 displayPort_t *osdGetDisplayPort(void)
4777 return osdDisplayPort;
4780 displayCanvas_t *osdGetDisplayPortCanvas(void)
4782 #if defined(USE_CANVAS)
4783 if (osdDisplayHasCanvas) {
4784 return &osdCanvas;
4786 #endif
4787 return NULL;
4790 timeMs_t systemMessageCycleTime(unsigned messageCount, const char **messages){
4791 uint8_t i = 0;
4792 float factor = 1.0f;
4793 while (i < messageCount) {
4794 if ((float)strlen(messages[i]) / 15.0f > factor) {
4795 factor = (float)strlen(messages[i]) / 15.0f;
4797 i++;
4799 return osdConfig()->system_msg_display_time * factor;
4802 textAttributes_t osdGetSystemMessage(char *buff, size_t buff_size, bool isCenteredText)
4804 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
4806 if (buff != NULL) {
4807 const char *message = NULL;
4808 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
4809 // We might have up to 5 messages to show.
4810 const char *messages[5];
4811 unsigned messageCount = 0;
4812 const char *failsafeInfoMessage = NULL;
4813 const char *invertedInfoMessage = NULL;
4815 if (ARMING_FLAG(ARMED)) {
4816 if (FLIGHT_MODE(FAILSAFE_MODE) || FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding()) {
4817 if (isWaypointMissionRTHActive()) {
4818 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
4819 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL);
4821 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE) {
4822 messages[messageCount++] = STATE(LANDING_DETECTED) ? OSD_MESSAGE_STR(OSD_MSG_WP_LANDED) : OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED);
4823 } else if (NAV_Status.state == MW_NAV_STATE_WP_ENROUTE) {
4824 // Countdown display for remaining Waypoints
4825 char buf[6];
4826 osdFormatDistanceSymbol(buf, posControl.wpDistance, 0);
4827 tfp_sprintf(messageBuf, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl.activeWaypointIndex), posControl.geoWaypointCount, buf);
4828 messages[messageCount++] = messageBuf;
4829 } else if (NAV_Status.state == MW_NAV_STATE_HOLD_TIMED) {
4830 if (navConfig()->general.waypoint_enforce_altitude && !posControl.wpAltitudeReached) {
4831 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ADJUSTING_WP_ALT);
4832 } else {
4833 // WP hold time countdown in seconds
4834 timeMs_t currentTime = millis();
4835 int holdTimeRemaining = posControl.waypointList[posControl.activeWaypointIndex].p1 - (int)(MS2S(currentTime - posControl.wpReachedTime));
4836 holdTimeRemaining = holdTimeRemaining >= 0 ? holdTimeRemaining : 0;
4838 tfp_sprintf(messageBuf, "HOLDING WP FOR %2u S", holdTimeRemaining);
4840 messages[messageCount++] = messageBuf;
4842 } else {
4843 const char *navStateMessage = navigationStateMessage();
4844 if (navStateMessage) {
4845 messages[messageCount++] = navStateMessage;
4848 #if defined(USE_SAFE_HOME)
4849 const char *safehomeMessage = divertingToSafehomeMessage();
4850 if (safehomeMessage) {
4851 messages[messageCount++] = safehomeMessage;
4853 #endif
4854 if (FLIGHT_MODE(FAILSAFE_MODE)) {
4855 // In FS mode while being armed too
4856 const char *failsafePhaseMessage = osdFailsafePhaseMessage();
4857 failsafeInfoMessage = osdFailsafeInfoMessage();
4859 if (failsafePhaseMessage) {
4860 messages[messageCount++] = failsafePhaseMessage;
4862 if (failsafeInfoMessage) {
4863 messages[messageCount++] = failsafeInfoMessage;
4866 } else { /* messages shown only when Failsafe, WP, RTH or Emergency Landing not active */
4867 if (STATE(FIXED_WING_LEGACY) && (navGetCurrentStateFlags() & NAV_CTL_LAUNCH)) {
4868 messages[messageCount++] = navConfig()->fw.launch_manual_throttle ? OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH_MANUAL) :
4869 OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH);
4870 const char *launchStateMessage = fixedWingLaunchStateMessage();
4871 if (launchStateMessage) {
4872 messages[messageCount++] = launchStateMessage;
4874 } else {
4875 if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && !navigationRequiresAngleMode()) {
4876 // ALTHOLD might be enabled alongside ANGLE/HORIZON/ACRO
4877 // when it doesn't require ANGLE mode (required only in FW
4878 // right now). If if requires ANGLE, its display is handled
4879 // by OSD_FLYMODE.
4880 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD);
4882 if (IS_RC_MODE_ACTIVE(BOXAUTOTRIM) && !feature(FEATURE_FW_AUTOTRIM)) {
4883 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM);
4885 if (IS_RC_MODE_ACTIVE(BOXAUTOTUNE)) {
4886 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE);
4887 if (FLIGHT_MODE(MANUAL_MODE)) {
4888 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO);
4891 if (IS_RC_MODE_ACTIVE(BOXAUTOLEVEL) && (FLIGHT_MODE(ANGLE_MODE) || FLIGHT_MODE(HORIZON_MODE) || (navigationRequiresAngleMode() && !navigationIsControllingAltitude()))) {
4892 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOLEVEL);
4894 if (FLIGHT_MODE(HEADFREE_MODE)) {
4895 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE);
4897 if (FLIGHT_MODE(SOARING_MODE)) {
4898 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING);
4900 if (posControl.flags.wpMissionPlannerActive) {
4901 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER);
4903 if (STATE(LANDING_DETECTED)) {
4904 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_LANDED);
4908 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS)) {
4909 unsigned invalidIndex;
4911 // Check if we're unable to arm for some reason
4912 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING) && !settingsValidate(&invalidIndex)) {
4914 const setting_t *setting = settingGet(invalidIndex);
4915 settingGetName(setting, messageBuf);
4916 for (int ii = 0; messageBuf[ii]; ii++) {
4917 messageBuf[ii] = sl_toupper(messageBuf[ii]);
4919 invertedInfoMessage = messageBuf;
4920 messages[messageCount++] = invertedInfoMessage;
4922 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
4923 messages[messageCount++] = invertedInfoMessage;
4925 } else {
4927 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM);
4928 messages[messageCount++] = invertedInfoMessage;
4930 // Show the reason for not arming
4931 messages[messageCount++] = osdArmingDisabledReasonMessage();
4934 } else if (!ARMING_FLAG(ARMED)) {
4935 if (isWaypointListValid()) {
4936 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED);
4940 /* Messages that are shown regardless of Arming state */
4942 if (savingSettings == true) {
4943 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS);
4944 } else if (notify_settings_saved > 0) {
4945 if (millis() > notify_settings_saved) {
4946 notify_settings_saved = 0;
4947 } else {
4948 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED);
4952 #ifdef USE_DEV_TOOLS
4953 if (systemConfig()->groundTestMode) {
4954 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_GRD_TEST_MODE);
4956 #endif
4958 if (messageCount > 0) {
4959 message = messages[OSD_ALTERNATING_CHOICES(systemMessageCycleTime(messageCount, messages), messageCount)];
4960 if (message == failsafeInfoMessage) {
4961 // failsafeInfoMessage is not useful for recovering
4962 // a lost model, but might help avoiding a crash.
4963 // Blink to grab user attention.
4964 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
4965 } else if (message == invertedInfoMessage) {
4966 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
4968 // We're shoing either failsafePhaseMessage or
4969 // navStateMessage. Don't BLINK here since
4970 // having this text available might be crucial
4971 // during a lost aircraft recovery and blinking
4972 // will cause it to be missing from some frames.
4975 osdFormatMessage(buff, buff_size, message, isCenteredText);
4977 return elemAttr;
4980 #endif // OSD