Missed one if in style changes
[inav.git] / src / main / io / osd.c
blob5a42b8b3530f5f01edec91d1dd6043e116908b23
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 FILE_COMPILE_FOR_SPEED
36 #ifdef USE_OSD
38 #include "build/debug.h"
39 #include "build/version.h"
41 #include "cms/cms.h"
42 #include "cms/cms_types.h"
43 #include "cms/cms_menu_osd.h"
45 #include "common/axis.h"
46 #include "common/constants.h"
47 #include "common/filter.h"
48 #include "common/log.h"
49 #include "common/olc.h"
50 #include "common/printf.h"
51 #include "common/string_light.h"
52 #include "common/time.h"
53 #include "common/typeconversion.h"
54 #include "common/utils.h"
56 #include "config/feature.h"
57 #include "config/parameter_group.h"
58 #include "config/parameter_group_ids.h"
60 #include "drivers/display.h"
61 #include "drivers/display_canvas.h"
62 #include "drivers/display_font_metadata.h"
63 #include "drivers/osd_symbols.h"
64 #include "drivers/time.h"
65 #include "drivers/vtx_common.h"
67 #include "io/flashfs.h"
68 #include "io/gps.h"
69 #include "io/osd.h"
70 #include "io/osd_common.h"
71 #include "io/osd_hud.h"
72 #include "io/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 DELAYED_REFRESH_RESUME_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
123 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
124 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
126 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
127 #define ARMED_SCREEN_DISPLAY_TIME 1500 // ms
128 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
130 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
132 // Adjust OSD_MESSAGE's default position when
133 // changing OSD_MESSAGE_LENGTH
134 #define OSD_MESSAGE_LENGTH 28
135 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
136 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
137 // Wrap all string constants intenteded for display as messages with
138 // this macro to ensure compile time length validation.
139 #define OSD_MESSAGE_STR(x) ({ \
140 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
141 x; \
144 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
146 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
147 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
149 #define OSD_MIN_FONT_VERSION 3
151 static unsigned currentLayout = 0;
152 static int layoutOverride = -1;
153 static bool hasExtendedFont = false; // Wether the font supports characters > 256
154 static timeMs_t layoutOverrideUntil = 0;
155 static pt1Filter_t GForceFilter, GForceFilterAxis[XYZ_AXIS_COUNT];
156 static float GForce, GForceAxis[XYZ_AXIS_COUNT];
158 typedef struct statistic_s {
159 uint16_t max_speed;
160 uint16_t max_3D_speed;
161 uint16_t max_air_speed;
162 uint16_t min_voltage; // /100
163 int16_t max_current;
164 int32_t max_power;
165 int16_t min_rssi;
166 int16_t min_lq; // for CRSF
167 int16_t min_rssi_dbm; // for CRSF
168 int32_t max_altitude;
169 uint32_t max_distance;
170 } statistic_t;
172 static statistic_t stats;
174 static timeUs_t resumeRefreshAt = 0;
175 static bool refreshWaitForResumeCmdRelease;
177 static bool fullRedraw = false;
179 static uint8_t armState;
180 static uint8_t statsPagesCheck = 0;
182 typedef struct osdMapData_s {
183 uint32_t scale;
184 char referenceSymbol;
185 } osdMapData_t;
187 static osdMapData_t osdMapData;
189 static displayPort_t *osdDisplayPort;
190 static bool osdDisplayIsReady = false;
191 #if defined(USE_CANVAS)
192 static displayCanvas_t osdCanvas;
193 static bool osdDisplayHasCanvas;
194 #else
195 #define osdDisplayHasCanvas false
196 #endif
198 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
200 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t, osdConfig, PG_OSD_CONFIG, 7);
201 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t, osdLayoutsConfig, PG_OSD_LAYOUTS_CONFIG, 1);
203 static int digitCount(int32_t value)
205 int digits = 1;
206 while(1) {
207 value = value / 10;
208 if (value == 0) {
209 break;
211 digits++;
213 return digits;
216 bool osdDisplayIsPAL(void)
218 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL;
221 bool osdDisplayIsHD(void)
223 if (displayScreenSize(osdDisplayPort) >= VIDEO_BUFFER_CHARS_HDZERO)
225 return true;
227 return false;
231 * Formats a number given in cents, to support non integer values
232 * without using floating point math. Value is always right aligned
233 * and spaces are inserted before the number to always yield a string
234 * of the same length. If the value doesn't fit into the provided length
235 * it will be divided by scale and true will be returned.
237 bool osdFormatCentiNumber(char *buff, int32_t centivalue, uint32_t scale, int maxDecimals, int maxScaledDecimals, int length)
239 char *ptr = buff;
240 char *dec;
241 int decimals = maxDecimals;
242 bool negative = false;
243 bool scaled = false;
244 bool explicitDecimal = osdConfig()->video_system == VIDEO_SYSTEM_BFCOMPAT;
246 buff[length] = '\0';
248 if (centivalue < 0) {
249 negative = true;
250 centivalue = -centivalue;
251 length--;
254 int32_t integerPart = centivalue / 100;
255 // 3 decimal digits
256 int32_t millis = (centivalue % 100) * 10;
258 int digits = digitCount(integerPart);
259 int remaining = length - digits;
260 if (explicitDecimal) {
261 remaining--;
264 if (remaining < 0 && scale > 0) {
265 // Reduce by scale
266 scaled = true;
267 decimals = maxScaledDecimals;
268 integerPart = integerPart / scale;
269 // Multiply by 10 to get 3 decimal digits
270 millis = ((centivalue % (100 * scale)) * 10) / scale;
271 digits = digitCount(integerPart);
272 remaining = length - digits;
273 if (explicitDecimal) {
274 remaining--;
278 // 3 decimals at most
279 decimals = MIN(remaining, MIN(decimals, 3));
280 remaining -= decimals;
282 // Done counting. Time to write the characters.
284 // Write spaces at the start
285 while (remaining > 0) {
286 *ptr = SYM_BLANK;
287 ptr++;
288 remaining--;
291 // Write the minus sign if required
292 if (negative) {
293 *ptr = '-';
294 ptr++;
296 // Now write the digits.
297 ui2a(integerPart, 10, 0, ptr);
298 ptr += digits;
300 if (decimals > 0) {
301 if (explicitDecimal) {
302 *ptr = '.';
303 ptr++;
304 } else {
305 *(ptr - 1) += SYM_ZERO_HALF_TRAILING_DOT - '0';
307 dec = ptr;
308 int factor = 3; // we're getting the decimal part in millis first
309 while (decimals < factor) {
310 factor--;
311 millis /= 10;
313 int decimalDigits = digitCount(millis);
314 while (decimalDigits < decimals) {
315 decimalDigits++;
316 *ptr = '0';
317 ptr++;
319 ui2a(millis, 10, 0, ptr);
320 if (!explicitDecimal) {
321 *dec += SYM_ZERO_HALF_LEADING_DOT - '0';
324 return scaled;
328 * Aligns text to the left side. Adds spaces at the end to keep string length unchanged.
330 static void osdLeftAlignString(char *buff)
332 uint8_t sp = 0, ch = 0;
333 uint8_t len = strlen(buff);
334 while (buff[sp] == ' ') sp++;
335 for (ch = 0; ch < (len - sp); ch++) buff[ch] = buff[ch + sp];
336 for (sp = ch; sp < len; sp++) buff[sp] = ' ';
340 * Converts distance into a string based on the current unit system
341 * prefixed by a a symbol to indicate the unit used.
342 * @param dist Distance in centimeters
344 static void osdFormatDistanceSymbol(char *buff, int32_t dist, uint8_t decimals)
346 switch ((osd_unit_e)osdConfig()->units) {
347 case OSD_UNIT_UK:
348 FALLTHROUGH;
349 case OSD_UNIT_IMPERIAL:
350 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_MILE, decimals, 3, 3)) {
351 buff[3] = SYM_DIST_MI;
352 } else {
353 buff[3] = SYM_DIST_FT;
355 buff[4] = '\0';
356 break;
357 case OSD_UNIT_METRIC_MPH:
358 FALLTHROUGH;
359 case OSD_UNIT_METRIC:
360 if (osdFormatCentiNumber(buff, dist, METERS_PER_KILOMETER, decimals, 3, 3)) {
361 buff[3] = SYM_DIST_KM;
362 } else {
363 buff[3] = SYM_DIST_M;
365 buff[4] = '\0';
366 break;
367 case OSD_UNIT_GA:
368 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_NAUTICALMILE, decimals, 3, 3)) {
369 buff[3] = SYM_DIST_NM;
370 } else {
371 buff[3] = SYM_DIST_FT;
373 buff[4] = '\0';
374 break;
379 * Converts distance into a string based on the current unit system.
380 * @param dist Distance in centimeters
382 static void osdFormatDistanceStr(char *buff, int32_t dist)
384 int32_t centifeet;
385 switch ((osd_unit_e)osdConfig()->units) {
386 case OSD_UNIT_UK:
387 FALLTHROUGH;
388 case OSD_UNIT_IMPERIAL:
389 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
390 if (abs(centifeet) < FEET_PER_MILE * 100 / 2) {
391 // Show feet when dist < 0.5mi
392 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
393 } else {
394 // Show miles when dist >= 0.5mi
395 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100*FEET_PER_MILE)),
396 (abs(centifeet) % (100 * FEET_PER_MILE)) / FEET_PER_MILE, SYM_MI);
398 break;
399 case OSD_UNIT_METRIC_MPH:
400 FALLTHROUGH;
401 case OSD_UNIT_METRIC:
402 if (abs(dist) < METERS_PER_KILOMETER * 100) {
403 // Show meters when dist < 1km
404 tfp_sprintf(buff, "%d%c", (int)(dist / 100), SYM_M);
405 } else {
406 // Show kilometers when dist >= 1km
407 tfp_sprintf(buff, "%d.%02d%c", (int)(dist / (100*METERS_PER_KILOMETER)),
408 (abs(dist) % (100 * METERS_PER_KILOMETER)) / METERS_PER_KILOMETER, SYM_KM);
410 break;
411 case OSD_UNIT_GA:
412 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
413 if (abs(centifeet) < 100000) {
414 // Show feet when dist < 1000ft
415 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
416 } else {
417 // Show nautical miles when dist >= 1000ft
418 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100 * FEET_PER_NAUTICALMILE)),
419 (int)((abs(centifeet) % (int)(100 * FEET_PER_NAUTICALMILE)) / FEET_PER_NAUTICALMILE), SYM_NM);
421 break;
426 * Converts velocity based on the current unit system (kmh or mph).
427 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
429 static int32_t osdConvertVelocityToUnit(int32_t vel)
431 switch ((osd_unit_e)osdConfig()->units) {
432 case OSD_UNIT_UK:
433 FALLTHROUGH;
434 case OSD_UNIT_METRIC_MPH:
435 FALLTHROUGH;
436 case OSD_UNIT_IMPERIAL:
437 return CMSEC_TO_CENTIMPH(vel) / 100; // Convert to mph
438 case OSD_UNIT_METRIC:
439 return CMSEC_TO_CENTIKPH(vel) / 100; // Convert to kmh
440 case OSD_UNIT_GA:
441 return CMSEC_TO_CENTIKNOTS(vel) / 100; // Convert to Knots
443 // Unreachable
444 return -1;
448 * Converts velocity into a string based on the current unit system.
449 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
450 * @param _3D is a 3D velocity
451 * @param _max is a maximum velocity
453 void osdFormatVelocityStr(char* buff, int32_t vel, bool _3D, bool _max)
455 switch ((osd_unit_e)osdConfig()->units) {
456 case OSD_UNIT_UK:
457 FALLTHROUGH;
458 case OSD_UNIT_METRIC_MPH:
459 FALLTHROUGH;
460 case OSD_UNIT_IMPERIAL:
461 if (_max) {
462 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
463 } else {
464 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
466 break;
467 case OSD_UNIT_METRIC:
468 if (_max) {
469 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
470 } else {
471 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
473 break;
474 case OSD_UNIT_GA:
475 if (_max) {
476 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
477 } else {
478 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
480 break;
485 * 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
487 static void osdGenerateAverageVelocityStr(char* buff) {
488 uint32_t cmPerSec = getTotalTravelDistance() / getFlightTime();
489 osdFormatVelocityStr(buff, cmPerSec, false, false);
493 * Converts wind speed into a string based on the current unit system, using
494 * always 3 digits and an additional character for the unit at the right. buff
495 * is null terminated.
496 * @param ws Raw wind speed in cm/s
498 #ifdef USE_WIND_ESTIMATOR
499 static void osdFormatWindSpeedStr(char *buff, int32_t ws, bool isValid)
501 int32_t centivalue;
502 char suffix;
503 switch (osdConfig()->units) {
504 case OSD_UNIT_UK:
505 FALLTHROUGH;
506 case OSD_UNIT_METRIC_MPH:
507 FALLTHROUGH;
508 case OSD_UNIT_IMPERIAL:
509 centivalue = CMSEC_TO_CENTIMPH(ws);
510 suffix = SYM_MPH;
511 break;
512 case OSD_UNIT_GA:
513 centivalue = CMSEC_TO_CENTIKNOTS(ws);
514 suffix = SYM_KT;
515 break;
516 default:
517 case OSD_UNIT_METRIC:
518 centivalue = CMSEC_TO_CENTIKPH(ws);
519 suffix = SYM_KMH;
520 break;
522 if (isValid) {
523 osdFormatCentiNumber(buff, centivalue, 0, 2, 0, 3);
524 } else {
525 buff[0] = buff[1] = buff[2] = '-';
527 buff[3] = suffix;
528 buff[4] = '\0';
530 #endif
533 * Converts altitude into a string based on the current unit system
534 * prefixed by a a symbol to indicate the unit used.
535 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
537 void osdFormatAltitudeSymbol(char *buff, int32_t alt)
539 int digits;
540 if (alt < 0) {
541 digits = 4;
542 } else {
543 digits = 3;
544 buff[0] = ' ';
546 switch ((osd_unit_e)osdConfig()->units) {
547 case OSD_UNIT_UK:
548 FALLTHROUGH;
549 case OSD_UNIT_GA:
550 FALLTHROUGH;
551 case OSD_UNIT_IMPERIAL:
552 if (osdFormatCentiNumber(buff + 4 - digits, CENTIMETERS_TO_CENTIFEET(alt), 1000, 0, 2, digits)) {
553 // Scaled to kft
554 buff[4] = SYM_ALT_KFT;
555 } else {
556 // Formatted in feet
557 buff[4] = SYM_ALT_FT;
559 buff[5] = '\0';
560 break;
561 case OSD_UNIT_METRIC_MPH:
562 FALLTHROUGH;
563 case OSD_UNIT_METRIC:
564 // alt is alredy in cm
565 if (osdFormatCentiNumber(buff + 4 - digits, alt, 1000, 0, 2, digits)) {
566 // Scaled to km
567 buff[4] = SYM_ALT_KM;
568 } else {
569 // Formatted in m
570 buff[4] = SYM_ALT_M;
572 buff[5] = '\0';
573 break;
578 * Converts altitude into a string based on the current unit system.
579 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
581 static void osdFormatAltitudeStr(char *buff, int32_t alt)
583 int32_t value;
584 switch ((osd_unit_e)osdConfig()->units) {
585 case OSD_UNIT_UK:
586 FALLTHROUGH;
587 case OSD_UNIT_GA:
588 FALLTHROUGH;
589 case OSD_UNIT_IMPERIAL:
590 value = CENTIMETERS_TO_FEET(alt);
591 tfp_sprintf(buff, "%d%c", (int)value, SYM_FT);
592 break;
593 case OSD_UNIT_METRIC_MPH:
594 FALLTHROUGH;
595 case OSD_UNIT_METRIC:
596 value = CENTIMETERS_TO_METERS(alt);
597 tfp_sprintf(buff, "%d%c", (int)value, SYM_M);
598 break;
602 static void osdFormatTime(char *buff, uint32_t seconds, char sym_m, char sym_h)
604 uint32_t value = seconds;
605 char sym = sym_m;
606 // Maximum value we can show in minutes is 99 minutes and 59 seconds
607 if (seconds > (99 * 60) + 59) {
608 sym = sym_h;
609 value = seconds / 60;
611 buff[0] = sym;
612 tfp_sprintf(buff + 1, "%02d:%02d", (int)(value / 60), (int)(value % 60));
615 static inline void osdFormatOnTime(char *buff)
617 osdFormatTime(buff, micros() / 1000000, SYM_ON_M, SYM_ON_H);
620 static inline void osdFormatFlyTime(char *buff, textAttributes_t *attr)
622 uint32_t seconds = getFlightTime();
623 osdFormatTime(buff, seconds, SYM_FLY_M, SYM_FLY_H);
624 if (attr && osdConfig()->time_alarm > 0) {
625 if (seconds / 60 >= osdConfig()->time_alarm && ARMING_FLAG(ARMED)) {
626 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
632 * Converts RSSI into a % value used by the OSD.
634 static uint16_t osdConvertRSSI(void)
636 // change range to [0, 99]
637 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE, 0, 99);
640 static uint16_t osdGetCrsfLQ(void)
642 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
643 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
644 int16_t displayedLQ;
645 switch (osdConfig()->crsf_lq_format) {
646 case OSD_CRSF_LQ_TYPE1:
647 displayedLQ = statsLQ;
648 break;
649 case OSD_CRSF_LQ_TYPE2:
650 displayedLQ = statsLQ;
651 break;
652 case OSD_CRSF_LQ_TYPE3:
653 displayedLQ = rxLinkStatistics.rfMode >= 2 ? scaledLQ : statsLQ;
654 break;
656 return displayedLQ;
659 static int16_t osdGetCrsfdBm(void)
661 return rxLinkStatistics.uplinkRSSI;
664 * Displays a temperature postfixed with a symbol depending on the current unit system
665 * @param label to display
666 * @param valid true if measurement is valid
667 * @param temperature in deciDegrees Celcius
669 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)
671 char buff[TEMPERATURE_LABEL_LEN + 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN + 2];
672 textAttributes_t elemAttr = valid ? TEXT_ATTRIBUTES_NONE : _TEXT_ATTRIBUTES_BLINK_BIT;
673 uint8_t valueXOffset = 0;
675 if (symbol) {
676 buff[0] = symbol;
677 buff[1] = '\0';
678 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
679 valueXOffset = 1;
681 #ifdef USE_TEMPERATURE_SENSOR
682 else if (label[0] != '\0') {
683 uint8_t label_len = strnlen(label, TEMPERATURE_LABEL_LEN);
684 memcpy(buff, label, label_len);
685 memset(buff + label_len, ' ', TEMPERATURE_LABEL_LEN + 1 - label_len);
686 buff[5] = '\0';
687 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
688 valueXOffset = osdConfig()->temp_label_align == OSD_ALIGN_LEFT ? 5 : label_len + 1;
690 #else
691 UNUSED(label);
692 #endif
694 if (valid) {
696 if ((temperature <= alarm_min) || (temperature >= alarm_max)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
697 if (osdConfig()->units == OSD_UNIT_IMPERIAL) temperature = temperature * 9 / 5.0f + 320;
698 tfp_sprintf(buff, "%3d", temperature / 10);
700 } else
701 strcpy(buff, "---");
703 buff[3] = osdConfig()->units == OSD_UNIT_IMPERIAL ? SYM_TEMP_F : SYM_TEMP_C;
704 buff[4] = '\0';
706 displayWriteWithAttr(osdDisplayPort, elemPosX + valueXOffset, elemPosY, buff, elemAttr);
709 #ifdef USE_TEMPERATURE_SENSOR
710 static void osdDisplayTemperatureSensor(uint8_t elemPosX, uint8_t elemPosY, uint8_t sensorIndex)
712 int16_t temperature;
713 const bool valid = getSensorTemperature(sensorIndex, &temperature);
714 const tempSensorConfig_t *sensorConfig = tempSensorConfig(sensorIndex);
715 uint16_t symbol = sensorConfig->osdSymbol ? SYM_TEMP_SENSOR_FIRST + sensorConfig->osdSymbol - 1 : 0;
716 osdDisplayTemperature(elemPosX, elemPosY, symbol, sensorConfig->label, valid, temperature, sensorConfig->alarm_min, sensorConfig->alarm_max);
718 #endif
720 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
722 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
723 const int coordinateLength = osdConfig()->coordinate_digits + 1;
725 buff[0] = sym;
726 int32_t integerPart = val / GPS_DEGREES_DIVIDER;
727 // Latitude maximum integer width is 3 (-90) while
728 // longitude maximum integer width is 4 (-180).
729 int integerDigits = tfp_sprintf(buff + 1, (integerPart == 0 && val < 0) ? "-%d" : "%d", (int)integerPart);
730 // We can show up to 7 digits in decimalPart.
731 int32_t decimalPart = abs(val % GPS_DEGREES_DIVIDER);
732 STATIC_ASSERT(GPS_DEGREES_DIVIDER == 1e7, adjust_max_decimal_digits);
733 int decimalDigits;
734 if (osdConfig()->video_system != VIDEO_SYSTEM_BFCOMPAT) {
735 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, "%07d", (int)decimalPart);
736 // Embbed the decimal separator
737 buff[1 + integerDigits - 1] += SYM_ZERO_HALF_TRAILING_DOT - '0';
738 buff[1 + integerDigits] += SYM_ZERO_HALF_LEADING_DOT - '0';
739 } else {
740 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, ".%06d", (int)decimalPart);
742 // Fill up to coordinateLength with zeros
743 int total = 1 + integerDigits + decimalDigits;
744 while(total < coordinateLength) {
745 buff[total] = '0';
746 total++;
748 buff[coordinateLength] = '\0';
751 static void osdFormatCraftName(char *buff)
753 if (strlen(systemConfig()->name) == 0)
754 strcpy(buff, "CRAFT_NAME");
755 else {
756 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
757 buff[i] = sl_toupper((unsigned char)systemConfig()->name[i]);
758 if (systemConfig()->name[i] == 0)
759 break;
764 static const char * osdArmingDisabledReasonMessage(void)
766 const char *message = NULL;
767 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
769 switch (isArmingDisabledReason()) {
770 case ARMING_DISABLED_FAILSAFE_SYSTEM:
771 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
772 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING) {
773 if (failsafeIsReceivingRxData()) {
774 // If we're not using sticks, it means the ARM switch
775 // hasn't been off since entering FAILSAFE_RX_LOSS_MONITORING
776 // yet
777 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF);
779 // Not receiving RX data
780 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
782 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS);
783 case ARMING_DISABLED_NOT_LEVEL:
784 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL);
785 case ARMING_DISABLED_SENSORS_CALIBRATING:
786 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL);
787 case ARMING_DISABLED_SYSTEM_OVERLOADED:
788 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED);
789 case ARMING_DISABLED_NAVIGATION_UNSAFE:
790 // Check the exact reason
791 switch (navigationIsBlockingArming(NULL)) {
792 char buf[6];
793 case NAV_ARMING_BLOCKER_NONE:
794 break;
795 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX:
796 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX);
797 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE:
798 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST);
799 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR:
800 osdFormatDistanceSymbol(buf, distanceToFirstWP(), 0);
801 tfp_sprintf(messageBuf, "FIRST WP TOO FAR (%s)", buf);
802 return message = messageBuf;
803 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR:
804 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG);
806 break;
807 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED:
808 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL);
809 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED:
810 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL);
811 case ARMING_DISABLED_ARM_SWITCH:
812 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST);
813 case ARMING_DISABLED_HARDWARE_FAILURE:
815 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
816 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE);
818 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
819 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL);
821 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
822 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL);
824 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
825 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL);
827 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
828 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL);
830 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
831 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL);
833 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
834 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL);
837 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL);
838 case ARMING_DISABLED_BOXFAILSAFE:
839 return OSD_MESSAGE_STR(OSD_MSG_FS_EN);
840 case ARMING_DISABLED_BOXKILLSWITCH:
841 return OSD_MESSAGE_STR(OSD_MSG_KILL_SW_EN);
842 case ARMING_DISABLED_RC_LINK:
843 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK);
844 case ARMING_DISABLED_THROTTLE:
845 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW);
846 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED:
847 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER);
848 case ARMING_DISABLED_SERVO_AUTOTRIM:
849 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE);
850 case ARMING_DISABLED_OOM:
851 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY);
852 case ARMING_DISABLED_INVALID_SETTING:
853 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
854 case ARMING_DISABLED_CLI:
855 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE);
856 case ARMING_DISABLED_PWM_OUTPUT_ERROR:
857 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR);
858 case ARMING_DISABLED_NO_PREARM:
859 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM);
860 case ARMING_DISABLED_DSHOT_BEEPER:
861 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER);
862 // Cases without message
863 case ARMING_DISABLED_LANDING_DETECTED:
864 FALLTHROUGH;
865 case ARMING_DISABLED_CMS_MENU:
866 FALLTHROUGH;
867 case ARMING_DISABLED_OSD_MENU:
868 FALLTHROUGH;
869 case ARMING_DISABLED_ALL_FLAGS:
870 FALLTHROUGH;
871 case ARMED:
872 FALLTHROUGH;
873 case SIMULATOR_MODE:
874 FALLTHROUGH;
875 case WAS_EVER_ARMED:
876 break;
878 return NULL;
881 static const char * osdFailsafePhaseMessage(void)
883 // See failsafe.h for each phase explanation
884 switch (failsafePhase()) {
885 case FAILSAFE_RETURN_TO_HOME:
886 // XXX: Keep this in sync with OSD_FLYMODE.
887 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS);
888 case FAILSAFE_LANDING:
889 // This should be considered an emergengy landing
890 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS);
891 case FAILSAFE_RX_LOSS_MONITORING:
892 // Only reachable from FAILSAFE_LANDED, which performs
893 // a disarm. Since aircraft has been disarmed, we no
894 // longer show failsafe details.
895 FALLTHROUGH;
896 case FAILSAFE_LANDED:
897 // Very brief, disarms and transitions into
898 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
899 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
900 // so we'll show the user how to re-arm in when
901 // that flag is the reason to prevent arming.
902 FALLTHROUGH;
903 case FAILSAFE_RX_LOSS_IDLE:
904 // This only happens when user has chosen NONE as FS
905 // procedure. The recovery messages should be enough.
906 FALLTHROUGH;
907 case FAILSAFE_IDLE:
908 // Failsafe not active
909 FALLTHROUGH;
910 case FAILSAFE_RX_LOSS_DETECTED:
911 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
912 // or the FS procedure immediately.
913 FALLTHROUGH;
914 case FAILSAFE_RX_LOSS_RECOVERED:
915 // Exiting failsafe
916 break;
918 return NULL;
921 static const char * osdFailsafeInfoMessage(void)
923 if (failsafeIsReceivingRxData()) {
924 // User must move sticks to exit FS mode
925 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS);
927 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
929 #if defined(USE_SAFE_HOME)
930 static const char * divertingToSafehomeMessage(void)
932 if (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && safehome_applied) {
933 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME);
935 return NULL;
937 #endif
939 static const char * navigationStateMessage(void)
941 switch (NAV_Status.state) {
942 case MW_NAV_STATE_NONE:
943 break;
944 case MW_NAV_STATE_RTH_START:
945 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH);
946 case MW_NAV_STATE_RTH_CLIMB:
947 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB);
948 case MW_NAV_STATE_RTH_ENROUTE:
949 if (posControl.flags.rthTrackbackActive) {
950 return OSD_MESSAGE_STR(OSD_MSG_RTH_TRACKBACK);
951 } else {
952 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME);
954 case MW_NAV_STATE_HOLD_INFINIT:
955 // Used by HOLD flight modes. No information to add.
956 break;
957 case MW_NAV_STATE_HOLD_TIMED:
958 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
959 break;
960 case MW_NAV_STATE_WP_ENROUTE:
961 // "TO WP" + WP countdown added in osdGetSystemMessage
962 break;
963 case MW_NAV_STATE_PROCESS_NEXT:
964 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP);
965 case MW_NAV_STATE_DO_JUMP:
966 // Not used
967 break;
968 case MW_NAV_STATE_LAND_START:
969 // Not used
970 break;
971 case MW_NAV_STATE_EMERGENCY_LANDING:
972 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING);
973 case MW_NAV_STATE_LAND_IN_PROGRESS:
974 return OSD_MESSAGE_STR(OSD_MSG_LANDING);
975 case MW_NAV_STATE_HOVER_ABOVE_HOME:
976 if (STATE(FIXED_WING_LEGACY)) {
977 #if defined(USE_SAFE_HOME)
978 if (safehome_applied) {
979 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME);
981 #endif
982 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME);
984 return OSD_MESSAGE_STR(OSD_MSG_HOVERING);
985 case MW_NAV_STATE_LANDED:
986 return OSD_MESSAGE_STR(OSD_MSG_LANDED);
987 case MW_NAV_STATE_LAND_SETTLE:
988 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND);
989 case MW_NAV_STATE_LAND_START_DESCENT:
990 // Not used
991 break;
993 return NULL;
996 static void osdFormatMessage(char *buff, size_t size, const char *message, bool isCenteredText)
998 // String is always filled with Blanks
999 memset(buff, SYM_BLANK, size);
1000 if (message) {
1001 size_t messageLength = strlen(message);
1002 int rem = isCenteredText ? MAX(0, (int)size - (int)messageLength) : 0;
1003 strncpy(buff + rem / 2, message, MIN((int)size - rem / 2, (int)messageLength));
1005 // Ensure buff is zero terminated
1006 buff[size] = '\0';
1010 * Draws the battery symbol filled in accordingly to the
1011 * battery voltage to buff[0].
1013 static void osdFormatBatteryChargeSymbol(char *buff)
1015 uint8_t p = calculateBatteryPercentage();
1016 p = (100 - p) / 16.6;
1017 buff[0] = SYM_BATT_FULL + p;
1020 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t *attr)
1022 if ((getBatteryState() != BATTERY_NOT_PRESENT) && ((batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical)) || ((!batteryUsesCapacityThresholds()) && (getBatteryVoltage() <= getBatteryWarningVoltage()))))
1023 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
1026 void osdCrosshairPosition(uint8_t *x, uint8_t *y)
1028 *x = osdDisplayPort->cols / 2;
1029 *y = osdDisplayPort->rows / 2;
1030 *y -= osdConfig()->horizon_offset; // positive horizon_offset moves the HUD up, negative moves down
1034 * Formats throttle position prefixed by its symbol.
1035 * Shows output to motor, not stick position
1037 static void osdFormatThrottlePosition(char *buff, bool autoThr, textAttributes_t *elemAttr)
1039 buff[0] = SYM_BLANK;
1040 buff[1] = SYM_THR;
1041 if (autoThr && navigationIsControllingThrottle()) {
1042 buff[0] = SYM_AUTO_THR0;
1043 buff[1] = SYM_AUTO_THR1;
1044 if (isFixedWingAutoThrottleManuallyIncreased()) {
1045 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1048 #ifdef USE_POWER_LIMITS
1049 if (powerLimiterIsLimiting()) {
1050 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1052 #endif
1053 tfp_sprintf(buff + 2, "%3d", getThrottlePercent());
1057 * Formats gvars prefixed by its number (0-indexed). If autoThr
1059 static void osdFormatGVar(char *buff, uint8_t index)
1061 buff[0] = 'G';
1062 buff[1] = '0'+index;
1063 buff[2] = ':';
1064 #ifdef USE_PROGRAMMING_FRAMEWORK
1065 osdFormatCentiNumber(buff + 3, (int32_t)gvGet(index)*(int32_t)100, 1, 0, 0, 5);
1066 #endif
1069 #if defined(USE_ESC_SENSOR)
1070 static void osdFormatRpm(char *buff, uint32_t rpm)
1072 buff[0] = SYM_RPM;
1073 if (rpm) {
1074 if ( digitCount(rpm) > osdConfig()->esc_rpm_precision) {
1075 uint8_t rpmMaxDecimals = (osdConfig()->esc_rpm_precision - 3);
1076 osdFormatCentiNumber(buff + 1, rpm / 10, 0, rpmMaxDecimals, rpmMaxDecimals, osdConfig()->esc_rpm_precision-1);
1077 buff[osdConfig()->esc_rpm_precision] = 'K';
1078 buff[osdConfig()->esc_rpm_precision+1] = '\0';
1080 else {
1081 switch(osdConfig()->esc_rpm_precision) {
1082 case 6:
1083 tfp_sprintf(buff + 1, "%6lu", rpm);
1084 break;
1085 case 5:
1086 tfp_sprintf(buff + 1, "%5lu", rpm);
1087 break;
1088 case 4:
1089 tfp_sprintf(buff + 1, "%4lu", rpm);
1090 break;
1091 case 3:
1092 default:
1093 tfp_sprintf(buff + 1, "%3lu", rpm);
1094 break;
1100 else {
1101 uint8_t buffPos = 1;
1102 while (buffPos <=( osdConfig()->esc_rpm_precision)) {
1103 strcpy(buff + buffPos++, "-");
1107 #endif
1109 int32_t osdGetAltitude(void)
1111 return getEstimatedActualPosition(Z);
1114 static inline int32_t osdGetAltitudeMsl(void)
1116 return getEstimatedActualPosition(Z)+GPS_home.alt;
1119 uint16_t osdGetRemainingGlideTime(void) {
1120 float value = getEstimatedActualVelocity(Z);
1121 static pt1Filter_t glideTimeFilterState;
1122 const timeMs_t curTimeMs = millis();
1123 static timeMs_t glideTimeUpdatedMs;
1125 value = pt1FilterApply4(&glideTimeFilterState, isnormal(value) ? value : 0, 0.5, MS2S(curTimeMs - glideTimeUpdatedMs));
1126 glideTimeUpdatedMs = curTimeMs;
1128 if (value < 0) {
1129 value = osdGetAltitude() / abs((int)value);
1130 } else {
1131 value = 0;
1134 return (uint16_t)round(value);
1137 static bool osdIsHeadingValid(void)
1139 return isImuHeadingValid();
1142 int16_t osdGetHeading(void)
1144 return attitude.values.yaw;
1147 int16_t osdPanServoHomeDirectionOffset(void)
1149 int8_t servoIndex = osdConfig()->pan_servo_index;
1150 int16_t servoPosition = servo[servoIndex];
1151 int16_t servoMiddle = servoParams(servoIndex)->middle;
1152 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition - servoMiddle) * osdConfig()->pan_servo_pwm2centideg);
1155 // Returns a heading angle in degrees normalized to [0, 360).
1156 int osdGetHeadingAngle(int angle)
1158 while (angle < 0) {
1159 angle += 360;
1161 while (angle >= 360) {
1162 angle -= 360;
1164 return angle;
1167 #if defined(USE_GPS)
1169 /* Draws a map with the given symbol in the center and given point of interest
1170 * defined by its distance in meters and direction in degrees.
1171 * referenceHeading indicates the up direction in the map, in degrees, while
1172 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1173 * arrow to indicate the map reference to the user. The drawn argument is an
1174 * in-out used to store the last position where the craft was drawn to avoid
1175 * erasing all screen on each redraw.
1177 static void osdDrawMap(int referenceHeading, uint16_t referenceSym, uint16_t centerSym,
1178 uint32_t poiDistance, int16_t poiDirection, uint16_t poiSymbol,
1179 uint16_t *drawn, uint32_t *usedScale)
1181 // TODO: These need to be tested with several setups. We might
1182 // need to make them configurable.
1183 const int hMargin = 5;
1184 const int vMargin = 3;
1186 // TODO: Get this from the display driver?
1187 const int charWidth = 12;
1188 const int charHeight = 18;
1190 uint8_t minX = hMargin;
1191 uint8_t maxX = osdDisplayPort->cols - 1 - hMargin;
1192 uint8_t minY = vMargin;
1193 uint8_t maxY = osdDisplayPort->rows - 1 - vMargin;
1194 uint8_t midX = osdDisplayPort->cols / 2;
1195 uint8_t midY = osdDisplayPort->rows / 2;
1197 // Fixed marks
1198 displayWriteChar(osdDisplayPort, midX, midY, centerSym);
1200 // First, erase the previous drawing.
1201 if (OSD_VISIBLE(*drawn)) {
1202 displayWriteChar(osdDisplayPort, OSD_X(*drawn), OSD_Y(*drawn), SYM_BLANK);
1203 *drawn = 0;
1206 uint32_t initialScale;
1207 const unsigned scaleMultiplier = 2;
1208 // We try to reduce the scale when the POI will be around half the distance
1209 // between the center and the closers map edge, to avoid too much jumping
1210 const int scaleReductionMultiplier = MIN(midX - hMargin, midY - vMargin) / 2;
1212 switch (osdConfig()->units) {
1213 case OSD_UNIT_UK:
1214 FALLTHROUGH;
1215 case OSD_UNIT_IMPERIAL:
1216 initialScale = 16; // 16m ~= 0.01miles
1217 break;
1218 case OSD_UNIT_GA:
1219 initialScale = 18; // 18m ~= 0.01 nautical miles
1220 break;
1221 default:
1222 case OSD_UNIT_METRIC_MPH:
1223 FALLTHROUGH;
1224 case OSD_UNIT_METRIC:
1225 initialScale = 10; // 10m as initial scale
1226 break;
1229 // Try to keep the same scale when getting closer until we draw over the center point
1230 uint32_t scale = initialScale;
1231 if (*usedScale) {
1232 scale = *usedScale;
1233 if (scale > initialScale && poiDistance < *usedScale * scaleReductionMultiplier) {
1234 scale /= scaleMultiplier;
1238 if (STATE(GPS_FIX)) {
1240 int directionToPoi = osdGetHeadingAngle(poiDirection - referenceHeading);
1241 float poiAngle = DEGREES_TO_RADIANS(directionToPoi);
1242 float poiSin = sin_approx(poiAngle);
1243 float poiCos = cos_approx(poiAngle);
1245 // Now start looking for a valid scale that lets us draw everything
1246 int ii;
1247 for (ii = 0; ii < 50; ii++) {
1248 // Calculate location of the aircraft in map
1249 int points = poiDistance / ((float)scale / charHeight);
1251 float pointsX = points * poiSin;
1252 int poiX = midX - roundf(pointsX / charWidth);
1253 if (poiX < minX || poiX > maxX) {
1254 scale *= scaleMultiplier;
1255 continue;
1258 float pointsY = points * poiCos;
1259 int poiY = midY + roundf(pointsY / charHeight);
1260 if (poiY < minY || poiY > maxY) {
1261 scale *= scaleMultiplier;
1262 continue;
1265 if (poiX == midX && poiY == midY) {
1266 // We're over the map center symbol, so we would be drawing
1267 // over it even if we increased the scale. Alternate between
1268 // drawing the center symbol or drawing the POI.
1269 if (centerSym != SYM_BLANK && OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1270 break;
1272 } else {
1274 uint16_t c;
1275 if (displayReadCharWithAttr(osdDisplayPort, poiX, poiY, &c, NULL) && c != SYM_BLANK) {
1276 // Something else written here, increase scale. If the display doesn't support reading
1277 // back characters, we assume there's nothing.
1279 // If we're close to the center, decrease scale. Otherwise increase it.
1280 uint8_t centerDeltaX = (maxX - minX) / (scaleMultiplier * 2);
1281 uint8_t centerDeltaY = (maxY - minY) / (scaleMultiplier * 2);
1282 if (poiX >= midX - centerDeltaX && poiX <= midX + centerDeltaX &&
1283 poiY >= midY - centerDeltaY && poiY <= midY + centerDeltaY &&
1284 scale > scaleMultiplier) {
1286 scale /= scaleMultiplier;
1287 } else {
1288 scale *= scaleMultiplier;
1290 continue;
1294 // Draw the point on the map
1295 if (poiSymbol == SYM_ARROW_UP) {
1296 // Drawing aircraft, rotate
1297 int mapHeading = osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading);
1298 poiSymbol += mapHeading * 2 / 45;
1300 displayWriteChar(osdDisplayPort, poiX, poiY, poiSymbol);
1302 // Update saved location
1303 *drawn = OSD_POS(poiX, poiY) | OSD_VISIBLE_FLAG;
1304 break;
1308 *usedScale = scale;
1310 // Update global map data for scale and reference
1311 osdMapData.scale = scale;
1312 osdMapData.referenceSymbol = referenceSym;
1315 /* Draws a map with the home in the center and the craft moving around.
1316 * See osdDrawMap() for reference.
1318 static void osdDrawHomeMap(int referenceHeading, uint8_t referenceSym, uint16_t *drawn, uint32_t *usedScale)
1320 osdDrawMap(referenceHeading, referenceSym, SYM_HOME, GPS_distanceToHome, GPS_directionToHome, SYM_ARROW_UP, drawn, usedScale);
1323 /* Draws a map with the aircraft in the center and the home moving around.
1324 * See osdDrawMap() for reference.
1326 static void osdDrawRadar(uint16_t *drawn, uint32_t *usedScale)
1328 int16_t reference = DECIDEGREES_TO_DEGREES(osdGetHeading());
1329 int16_t poiDirection = osdGetHeadingAngle(GPS_directionToHome + 180);
1330 osdDrawMap(reference, 0, SYM_ARROW_UP, GPS_distanceToHome, poiDirection, SYM_HOME, drawn, usedScale);
1333 static uint16_t crc_accumulate(uint8_t data, uint16_t crcAccum)
1335 uint8_t tmp;
1336 tmp = data ^ (uint8_t)(crcAccum & 0xff);
1337 tmp ^= (tmp << 4);
1338 crcAccum = (crcAccum >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
1339 return crcAccum;
1343 static void osdDisplayTelemetry(void)
1345 uint32_t trk_data;
1346 uint16_t trk_crc = 0;
1347 char trk_buffer[31];
1348 static int16_t trk_elevation = 127;
1349 static uint16_t trk_bearing = 0;
1351 if (ARMING_FLAG(ARMED)) {
1352 if (STATE(GPS_FIX)){
1353 if (GPS_distanceToHome > 5) {
1354 trk_bearing = GPS_directionToHome;
1355 trk_bearing += 360 + 180;
1356 trk_bearing %= 360;
1357 int32_t alt = CENTIMETERS_TO_METERS(osdGetAltitude());
1358 float at = atan2(alt, GPS_distanceToHome);
1359 trk_elevation = (float)at * 57.2957795; // 57.2957795 = 1 rad
1360 trk_elevation += 37; // because elevation in telemetry should be from -37 to 90
1361 if (trk_elevation < 0) {
1362 trk_elevation = 0;
1367 else{
1368 trk_elevation = 127;
1369 trk_bearing = 0;
1372 trk_data = 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1373 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.
1374 trk_data = trk_data | (uint32_t)trk_bearing << 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1375 trk_crc = crc_accumulate(0xFF & trk_data, trk_crc); // CRC First Byte bits 0-7
1376 trk_crc = crc_accumulate(0xFF & trk_bearing, trk_crc); // CRC Second Byte bits 8-15
1377 trk_crc = crc_accumulate(trk_bearing >> 8, trk_crc); // CRC Third Byte bits 16-17
1378 trk_data = trk_data | (uint32_t)trk_crc << 17; // bits 18-29 CRC & 0x3FFFF
1380 for (uint8_t t_ctr = 0; t_ctr < 30; t_ctr++) { // Prepare screen buffer and write data line.
1381 if (trk_data & (uint32_t)1 << t_ctr){
1382 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_0;
1384 else{
1385 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_1;
1388 trk_buffer[30] = 0;
1389 displayWrite(osdDisplayPort, 0, 0, trk_buffer);
1390 if (osdConfig()->telemetry>1){
1391 displayWrite(osdDisplayPort, 0, 3, trk_buffer); // Test display because normal telemetry line is not visible
1394 #endif
1396 static void osdFormatPidControllerOutput(char *buff, const char *label, const pidController_t *pidController, uint8_t scale, bool showDecimal) {
1397 strcpy(buff, label);
1398 for (uint8_t i = strlen(label); i < 5; ++i) buff[i] = ' ';
1399 uint8_t decimals = showDecimal ? 1 : 0;
1400 osdFormatCentiNumber(buff + 5, pidController->proportional * scale, 0, decimals, 0, 4);
1401 buff[9] = ' ';
1402 osdFormatCentiNumber(buff + 10, pidController->integrator * scale, 0, decimals, 0, 4);
1403 buff[14] = ' ';
1404 osdFormatCentiNumber(buff + 15, pidController->derivative * scale, 0, decimals, 0, 4);
1405 buff[19] = ' ';
1406 osdFormatCentiNumber(buff + 20, pidController->output_constrained * scale, 0, decimals, 0, 4);
1407 buff[24] = '\0';
1410 static void osdDisplayBatteryVoltage(uint8_t elemPosX, uint8_t elemPosY, uint16_t voltage, uint8_t digits, uint8_t decimals)
1412 char buff[6];
1413 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1415 osdFormatBatteryChargeSymbol(buff);
1416 buff[1] = '\0';
1417 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1418 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1420 elemAttr = TEXT_ATTRIBUTES_NONE;
1421 digits = MIN(digits, 4);
1422 osdFormatCentiNumber(buff, voltage, 0, decimals, 0, digits);
1423 buff[digits] = SYM_VOLT;
1424 buff[digits+1] = '\0';
1425 if ((getBatteryState() != BATTERY_NOT_PRESENT) && (getBatteryVoltage() <= getBatteryWarningVoltage()))
1426 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1427 displayWriteWithAttr(osdDisplayPort, elemPosX + 1, elemPosY, buff, elemAttr);
1430 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)
1432 textAttributes_t elemAttr;
1433 char buff[4];
1435 const pid8_t *pid = &pidBank()->pid[pidIndex];
1436 pidType_e pidType = pidIndexGetType(pidIndex);
1438 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1440 if (pidType == PID_TYPE_NONE) {
1441 // PID is not used in this configuration. Draw dashes.
1442 // XXX: Keep this in sync with the %3d format and spacing used below
1443 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - - -");
1444 return;
1447 elemAttr = TEXT_ATTRIBUTES_NONE;
1448 tfp_sprintf(buff, "%3d", pid->P);
1449 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1450 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1451 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1453 elemAttr = TEXT_ATTRIBUTES_NONE;
1454 tfp_sprintf(buff, "%3d", pid->I);
1455 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1456 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1457 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1459 elemAttr = TEXT_ATTRIBUTES_NONE;
1460 tfp_sprintf(buff, "%3d", pid->D);
1461 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1462 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1463 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1465 elemAttr = TEXT_ATTRIBUTES_NONE;
1466 tfp_sprintf(buff, "%3d", pid->FF);
1467 if ((isAdjustmentFunctionSelected(adjFuncFF)) || (((adjFuncFF == ADJUSTMENT_ROLL_FF) || (adjFuncFF == ADJUSTMENT_PITCH_FF)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF))))
1468 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1469 displayWriteWithAttr(osdDisplayPort, elemPosX + 16, elemPosY, buff, elemAttr);
1472 static void osdDisplayNavPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD)
1474 textAttributes_t elemAttr;
1475 char buff[4];
1477 const pid8_t *pid = &pidBank()->pid[pidIndex];
1478 pidType_e pidType = pidIndexGetType(pidIndex);
1480 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1482 if (pidType == PID_TYPE_NONE) {
1483 // PID is not used in this configuration. Draw dashes.
1484 // XXX: Keep this in sync with the %3d format and spacing used below
1485 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - -");
1486 return;
1489 elemAttr = TEXT_ATTRIBUTES_NONE;
1490 tfp_sprintf(buff, "%3d", pid->P);
1491 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1492 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1493 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1495 elemAttr = TEXT_ATTRIBUTES_NONE;
1496 tfp_sprintf(buff, "%3d", pid->I);
1497 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1498 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1499 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1501 elemAttr = TEXT_ATTRIBUTES_NONE;
1502 tfp_sprintf(buff, "%3d", pidType == PID_TYPE_PIFF ? pid->FF : pid->D);
1503 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1504 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1505 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1508 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) {
1509 char buff[8];
1510 textAttributes_t elemAttr;
1511 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1513 elemAttr = TEXT_ATTRIBUTES_NONE;
1514 osdFormatCentiNumber(buff, value * 100, 0, maxDecimals, 0, MIN(valueLength, 8));
1515 if (isAdjustmentFunctionSelected(adjFunc))
1516 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1517 displayWriteWithAttr(osdDisplayPort, elemPosX + strlen(str) + 1 + valueOffset, elemPosY, buff, elemAttr);
1520 int8_t getGeoWaypointNumber(int8_t waypointIndex)
1522 static int8_t lastWaypointIndex = 1;
1523 static int8_t geoWaypointIndex;
1525 if (waypointIndex != lastWaypointIndex) {
1526 lastWaypointIndex = geoWaypointIndex = waypointIndex;
1527 for (uint8_t i = posControl.startWpIndex; i <= waypointIndex; i++) {
1528 if (posControl.waypointList[i].action == NAV_WP_ACTION_SET_POI ||
1529 posControl.waypointList[i].action == NAV_WP_ACTION_SET_HEAD ||
1530 posControl.waypointList[i].action == NAV_WP_ACTION_JUMP) {
1531 geoWaypointIndex -= 1;
1536 return geoWaypointIndex - posControl.startWpIndex + 1;
1539 void osdDisplaySwitchIndicator(const char *swName, int rcValue, char *buff) {
1540 int8_t ptr = 0;
1542 if (osdConfig()->osd_switch_indicators_align_left) {
1543 for (ptr = 0; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH); ptr++) {
1544 buff[ptr] = swName[ptr];
1547 if ( rcValue < 1333) {
1548 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1549 } else if ( rcValue > 1666) {
1550 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1551 } else {
1552 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1554 } else {
1555 if ( rcValue < 1333) {
1556 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1557 } else if ( rcValue > 1666) {
1558 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1559 } else {
1560 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1563 for (ptr = 1; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH) + 1; ptr++) {
1564 buff[ptr] = swName[ptr-1];
1567 ptr++;
1570 buff[ptr] = '\0';
1573 static bool osdDrawSingleElement(uint8_t item)
1575 uint16_t pos = osdLayoutsConfig()->item_pos[currentLayout][item];
1576 if (!OSD_VISIBLE(pos)) {
1577 return false;
1579 uint8_t elemPosX = OSD_X(pos);
1580 uint8_t elemPosY = OSD_Y(pos);
1581 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1582 char buff[32] = {0};
1584 switch (item) {
1585 case OSD_RSSI_VALUE:
1587 uint16_t osdRssi = osdConvertRSSI();
1588 buff[0] = SYM_RSSI;
1589 tfp_sprintf(buff + 1, "%2d", osdRssi);
1590 if (osdRssi < osdConfig()->rssi_alarm) {
1591 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1593 break;
1596 case OSD_MAIN_BATT_VOLTAGE:
1597 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawVoltage(), 2 + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1598 return true;
1600 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE:
1601 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedVoltage(), 2 + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1602 return true;
1604 case OSD_CURRENT_DRAW:
1605 osdFormatCentiNumber(buff, getAmperage(), 0, 2, 0, 3);
1606 buff[3] = SYM_AMP;
1607 buff[4] = '\0';
1609 uint8_t current_alarm = osdConfig()->current_alarm;
1610 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
1611 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1613 break;
1615 case OSD_MAH_DRAWN: {
1616 if (osdFormatCentiNumber(buff, getMAhDrawn() * 100, 1000, 0, (osdConfig()->mAh_used_precision - 2), osdConfig()->mAh_used_precision)) {
1617 // Shown in mAh
1618 buff[osdConfig()->mAh_used_precision] = SYM_AH;
1619 } else {
1620 // Shown in Ah
1621 buff[osdConfig()->mAh_used_precision] = SYM_MAH;
1623 buff[(osdConfig()->mAh_used_precision + 1)] = '\0';
1624 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1625 break;
1627 case OSD_WH_DRAWN:
1628 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
1629 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1630 buff[3] = SYM_WH;
1631 buff[4] = '\0';
1632 break;
1634 case OSD_BATTERY_REMAINING_CAPACITY:
1636 if (currentBatteryProfile->capacity.value == 0)
1637 tfp_sprintf(buff, " NA");
1638 else if (!batteryWasFullWhenPluggedIn())
1639 tfp_sprintf(buff, " NF");
1640 else if (currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH)
1641 tfp_sprintf(buff, "%4lu", getBatteryRemainingCapacity());
1642 else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1643 osdFormatCentiNumber(buff + 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3);
1645 buff[4] = currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH ? SYM_MAH : SYM_WH;
1646 buff[5] = '\0';
1648 if ((getBatteryState() != BATTERY_NOT_PRESENT) && batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical))
1649 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1651 break;
1653 case OSD_BATTERY_REMAINING_PERCENT:
1654 osdFormatBatteryChargeSymbol(buff);
1655 tfp_sprintf(buff + 1, "%3d%%", calculateBatteryPercentage());
1656 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1657 break;
1659 case OSD_POWER_SUPPLY_IMPEDANCE:
1660 if (isPowerSupplyImpedanceValid())
1661 tfp_sprintf(buff, "%3d", getPowerSupplyImpedance());
1662 else
1663 strcpy(buff, "---");
1664 buff[3] = SYM_MILLIOHM;
1665 buff[4] = '\0';
1666 break;
1668 #ifdef USE_GPS
1669 case OSD_GPS_SATS:
1670 buff[0] = SYM_SAT_L;
1671 buff[1] = SYM_SAT_R;
1672 tfp_sprintf(buff + 2, "%2d", gpsSol.numSat);
1673 if (!STATE(GPS_FIX)) {
1674 if (getHwGPSStatus() == HW_SENSOR_UNAVAILABLE || getHwGPSStatus() == HW_SENSOR_UNHEALTHY) {
1675 strcpy(buff + 2, "X!");
1677 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1679 break;
1681 case OSD_GPS_SPEED:
1682 osdFormatVelocityStr(buff, gpsSol.groundSpeed, false, false);
1683 break;
1685 case OSD_GPS_MAX_SPEED:
1686 osdFormatVelocityStr(buff, stats.max_speed, false, true);
1687 break;
1689 case OSD_3D_SPEED:
1690 osdFormatVelocityStr(buff, osdGet3DSpeed(), true, false);
1691 break;
1693 case OSD_3D_MAX_SPEED:
1694 osdFormatVelocityStr(buff, stats.max_3D_speed, true, true);
1695 break;
1697 case OSD_GLIDESLOPE:
1699 float horizontalSpeed = gpsSol.groundSpeed;
1700 float sinkRate = -getEstimatedActualVelocity(Z);
1701 static pt1Filter_t gsFilterState;
1702 const timeMs_t currentTimeMs = millis();
1703 static timeMs_t gsUpdatedTimeMs;
1704 float glideSlope = horizontalSpeed / sinkRate;
1705 glideSlope = pt1FilterApply4(&gsFilterState, isnormal(glideSlope) ? glideSlope : 200, 0.5, MS2S(currentTimeMs - gsUpdatedTimeMs));
1706 gsUpdatedTimeMs = currentTimeMs;
1708 buff[0] = SYM_GLIDESLOPE;
1709 if (glideSlope > 0.0f && glideSlope < 100.0f) {
1710 osdFormatCentiNumber(buff + 1, glideSlope * 100.0f, 0, 2, 0, 3);
1711 } else {
1712 buff[1] = buff[2] = buff[3] = '-';
1714 buff[4] = '\0';
1715 break;
1718 case OSD_GPS_LAT:
1719 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
1720 break;
1722 case OSD_GPS_LON:
1723 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
1724 break;
1726 case OSD_HOME_DIR:
1728 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
1729 if (GPS_distanceToHome < (navConfig()->general.min_rth_distance / 100) ) {
1730 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_HOME_NEAR);
1732 else
1734 int16_t panHomeDirOffset = 0;
1735 if (!(osdConfig()->pan_servo_pwm2centideg == 0)){
1736 panHomeDirOffset = osdPanServoHomeDirectionOffset();
1738 int homeDirection = GPS_directionToHome - DECIDEGREES_TO_DEGREES(osdGetHeading()) + panHomeDirOffset;
1739 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), homeDirection);
1741 } else {
1742 // No home or no fix or unknown heading, blink.
1743 // If we're unarmed, show the arrow pointing up so users can see the arrow
1744 // while configuring the OSD. If we're armed, show a '-' indicating that
1745 // we don't know the direction to home.
1746 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1747 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, ARMING_FLAG(ARMED) ? '-' : SYM_ARROW_UP, elemAttr);
1749 return true;
1752 case OSD_HOME_HEADING_ERROR:
1754 buff[0] = SYM_HOME;
1755 buff[1] = SYM_HEADING;
1757 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1758 int16_t h = lrintf(CENTIDEGREES_TO_DEGREES((float)wrap_18000(DEGREES_TO_CENTIDEGREES((int32_t)GPS_directionToHome) - DECIDEGREES_TO_CENTIDEGREES((int32_t)osdGetHeading()))));
1759 tfp_sprintf(buff + 2, "%4d", h);
1760 } else {
1761 strcpy(buff + 2, "----");
1764 buff[6] = SYM_DEGREES;
1765 buff[7] = '\0';
1766 break;
1769 case OSD_HOME_DIST:
1771 buff[0] = SYM_HOME;
1772 osdFormatDistanceSymbol(&buff[1], GPS_distanceToHome * 100, 0);
1773 uint16_t dist_alarm = osdConfig()->dist_alarm;
1774 if (dist_alarm > 0 && GPS_distanceToHome > dist_alarm) {
1775 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1778 break;
1780 case OSD_TRIP_DIST:
1781 buff[0] = SYM_TOTAL;
1782 osdFormatDistanceSymbol(buff + 1, getTotalTravelDistance(), 0);
1783 break;
1785 case OSD_HEADING:
1787 buff[0] = SYM_HEADING;
1788 if (osdIsHeadingValid()) {
1789 int16_t h = DECIDEGREES_TO_DEGREES(osdGetHeading());
1790 if (h < 0) {
1791 h += 360;
1793 tfp_sprintf(&buff[1], "%3d", h);
1794 } else {
1795 buff[1] = buff[2] = buff[3] = '-';
1797 buff[4] = SYM_DEGREES;
1798 buff[5] = '\0';
1799 break;
1802 case OSD_GROUND_COURSE:
1804 buff[0] = SYM_GROUND_COURSE;
1805 if (osdIsHeadingValid()) {
1806 tfp_sprintf(&buff[1], "%3d", (int16_t)CENTIDEGREES_TO_DEGREES(posControl.actualState.cog));
1807 } else {
1808 buff[1] = buff[2] = buff[3] = '-';
1810 buff[4] = SYM_DEGREES;
1811 buff[5] = '\0';
1812 break;
1815 case OSD_COURSE_HOLD_ERROR:
1817 if (ARMING_FLAG(ARMED) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1818 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1819 return true;
1822 buff[0] = SYM_HEADING;
1824 if ((!ARMING_FLAG(ARMED)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && isAdjustingPosition())) {
1825 buff[1] = buff[2] = buff[3] = '-';
1826 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1827 int16_t herr = lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
1828 if (ABS(herr) > 99)
1829 strcpy(buff + 1, ">99");
1830 else
1831 tfp_sprintf(buff + 1, "%3d", herr);
1834 buff[4] = SYM_DEGREES;
1835 buff[5] = '\0';
1836 break;
1839 case OSD_COURSE_HOLD_ADJUSTMENT:
1841 int16_t heading_adjust = lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
1843 if (ARMING_FLAG(ARMED) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust != 0)))) {
1844 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1845 return true;
1848 buff[0] = SYM_HEADING;
1850 if (!ARMING_FLAG(ARMED)) {
1851 buff[1] = buff[2] = buff[3] = buff[4] = '-';
1852 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1853 tfp_sprintf(buff + 1, "%4d", heading_adjust);
1856 buff[5] = SYM_DEGREES;
1857 buff[6] = '\0';
1858 break;
1861 case OSD_CROSS_TRACK_ERROR:
1863 if (isWaypointNavTrackingActive()) {
1864 buff[0] = SYM_CROSS_TRACK_ERROR;
1865 osdFormatDistanceSymbol(buff + 1, navigationGetCrossTrackError(), 0);
1866 } else {
1867 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1868 return true;
1870 break;
1873 case OSD_GPS_HDOP:
1875 buff[0] = SYM_HDP_L;
1876 buff[1] = SYM_HDP_R;
1877 int32_t centiHDOP = 100 * gpsSol.hdop / HDOP_SCALE;
1878 osdFormatCentiNumber(&buff[2], centiHDOP, 0, 1, 0, 2);
1879 break;
1882 case OSD_MAP_NORTH:
1884 static uint16_t drawn = 0;
1885 static uint32_t scale = 0;
1886 osdDrawHomeMap(0, 'N', &drawn, &scale);
1887 return true;
1889 case OSD_MAP_TAKEOFF:
1891 static uint16_t drawn = 0;
1892 static uint32_t scale = 0;
1893 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn, &scale);
1894 return true;
1896 case OSD_RADAR:
1898 static uint16_t drawn = 0;
1899 static uint32_t scale = 0;
1900 osdDrawRadar(&drawn, &scale);
1901 return true;
1903 #endif // GPS
1905 case OSD_ALTITUDE:
1907 int32_t alt = osdGetAltitude();
1908 osdFormatAltitudeSymbol(buff, alt);
1909 uint16_t alt_alarm = osdConfig()->alt_alarm;
1910 uint16_t neg_alt_alarm = osdConfig()->neg_alt_alarm;
1911 if ((alt_alarm > 0 && CENTIMETERS_TO_METERS(alt) > alt_alarm) ||
1912 (neg_alt_alarm > 0 && alt < 0 && -CENTIMETERS_TO_METERS(alt) > neg_alt_alarm)) {
1914 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1916 break;
1919 case OSD_ALTITUDE_MSL:
1921 int32_t alt = osdGetAltitudeMsl();
1922 osdFormatAltitudeSymbol(buff, alt);
1923 break;
1926 #ifdef USE_RANGEFINDER
1927 case OSD_RANGEFINDER:
1929 int32_t range = rangefinderGetLatestRawAltitude();
1930 if (range < 0) {
1931 buff[0] = '-';
1932 } else {
1933 osdFormatDistanceSymbol(buff, range, 1);
1936 break;
1937 #endif
1939 case OSD_ONTIME:
1941 osdFormatOnTime(buff);
1942 break;
1945 case OSD_FLYTIME:
1947 osdFormatFlyTime(buff, &elemAttr);
1948 break;
1951 case OSD_ONTIME_FLYTIME:
1953 if (ARMING_FLAG(ARMED)) {
1954 osdFormatFlyTime(buff, &elemAttr);
1955 } else {
1956 osdFormatOnTime(buff);
1958 break;
1961 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH:
1963 /*static int32_t updatedTimeSeconds = 0;*/
1964 static int32_t timeSeconds = -1;
1965 #if defined(USE_ADC) && defined(USE_GPS)
1966 static timeUs_t updatedTimestamp = 0;
1967 timeUs_t currentTimeUs = micros();
1968 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
1969 #ifdef USE_WIND_ESTIMATOR
1970 timeSeconds = calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation);
1971 #else
1972 timeSeconds = calculateRemainingFlightTimeBeforeRTH(false);
1973 #endif
1974 updatedTimestamp = currentTimeUs;
1976 #endif
1977 if ((!ARMING_FLAG(ARMED)) || (timeSeconds == -1)) {
1978 buff[0] = SYM_FLIGHT_MINS_REMAINING;
1979 strcpy(buff + 1, "--:--");
1980 #if defined(USE_ADC) && defined(USE_GPS)
1981 updatedTimestamp = 0;
1982 #endif
1983 } else if (timeSeconds == -2) {
1984 // Wind is too strong to come back with cruise throttle
1985 buff[0] = SYM_FLIGHT_MINS_REMAINING;
1986 buff[1] = buff[2] = buff[4] = buff[5] = SYM_WIND_HORIZONTAL;
1987 buff[3] = ':';
1988 buff[6] = '\0';
1989 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1990 } else {
1991 osdFormatTime(buff, timeSeconds, SYM_FLIGHT_MINS_REMAINING, SYM_FLIGHT_HOURS_REMAINING);
1992 if (timeSeconds == 0)
1993 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1996 break;
1998 case OSD_REMAINING_DISTANCE_BEFORE_RTH:;
1999 static int32_t distanceMeters = -1;
2000 #if defined(USE_ADC) && defined(USE_GPS)
2001 static timeUs_t updatedTimestamp = 0;
2002 timeUs_t currentTimeUs = micros();
2003 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2004 #ifdef USE_WIND_ESTIMATOR
2005 distanceMeters = calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation);
2006 #else
2007 distanceMeters = calculateRemainingDistanceBeforeRTH(false);
2008 #endif
2009 updatedTimestamp = currentTimeUs;
2011 #endif
2012 //buff[0] = SYM_TRIP_DIST;
2013 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_FLIGHT_DIST_REMAINING);
2014 if ((!ARMING_FLAG(ARMED)) || (distanceMeters == -1)) {
2015 buff[4] = SYM_BLANK;
2016 buff[5] = '\0';
2017 strcpy(buff + 1, "---");
2018 } else if (distanceMeters == -2) {
2019 // Wind is too strong to come back with cruise throttle
2020 buff[1] = buff[2] = buff[3] = SYM_WIND_HORIZONTAL;
2021 switch ((osd_unit_e)osdConfig()->units){
2022 case OSD_UNIT_UK:
2023 FALLTHROUGH;
2024 case OSD_UNIT_IMPERIAL:
2025 buff[4] = SYM_DIST_MI;
2026 break;
2027 case OSD_UNIT_METRIC_MPH:
2028 FALLTHROUGH;
2029 case OSD_UNIT_METRIC:
2030 buff[4] = SYM_DIST_KM;
2031 break;
2032 case OSD_UNIT_GA:
2033 buff[4] = SYM_DIST_NM;
2034 break;
2036 buff[5] = '\0';
2037 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2038 } else {
2039 osdFormatDistanceSymbol(buff + 1, distanceMeters * 100, 0);
2040 if (distanceMeters == 0)
2041 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2043 break;
2045 case OSD_FLYMODE:
2047 char *p = "ACRO";
2049 if (FLIGHT_MODE(FAILSAFE_MODE))
2050 p = "!FS!";
2051 else if (FLIGHT_MODE(MANUAL_MODE))
2052 p = "MANU";
2053 else if (FLIGHT_MODE(TURTLE_MODE))
2054 p = "TURT";
2055 else if (FLIGHT_MODE(NAV_RTH_MODE))
2056 p = isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
2057 else if (FLIGHT_MODE(NAV_POSHOLD_MODE))
2058 p = "HOLD";
2059 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && FLIGHT_MODE(NAV_ALTHOLD_MODE))
2060 p = "CRUZ";
2061 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE))
2062 p = "CRSH";
2063 else if (FLIGHT_MODE(NAV_WP_MODE))
2064 p = " WP ";
2065 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && navigationRequiresAngleMode()) {
2066 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
2067 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
2068 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
2069 p = " AH ";
2071 else if (FLIGHT_MODE(ANGLE_MODE))
2072 p = "ANGL";
2073 else if (FLIGHT_MODE(HORIZON_MODE))
2074 p = "HOR ";
2076 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
2077 return true;
2080 case OSD_CRAFT_NAME:
2081 osdFormatCraftName(buff);
2082 break;
2084 case OSD_THROTTLE_POS:
2086 osdFormatThrottlePosition(buff, false, &elemAttr);
2087 break;
2090 case OSD_VTX_CHANNEL:
2092 vtxDeviceOsdInfo_t osdInfo;
2093 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2095 tfp_sprintf(buff, "CH:%c%s:", osdInfo.bandLetter, osdInfo.channelName);
2096 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2098 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2099 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2100 displayWriteWithAttr(osdDisplayPort, elemPosX + 6, elemPosY, buff, elemAttr);
2101 return true;
2103 break;
2105 case OSD_VTX_POWER:
2107 vtxDeviceOsdInfo_t osdInfo;
2108 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2110 tfp_sprintf(buff, "%c", SYM_VTX_POWER);
2111 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2113 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2114 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2115 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2116 return true;
2119 #if defined(USE_SERIALRX_CRSF)
2120 case OSD_CRSF_RSSI_DBM:
2122 int16_t rssi = rxLinkStatistics.uplinkRSSI;
2123 buff[0] = (rxLinkStatistics.activeAntenna == 0) ? SYM_RSSI : SYM_2RSS; // Separate symbols for each antenna
2124 if (rssi <= -100) {
2125 tfp_sprintf(buff + 1, "%4d%c", rssi, SYM_DBM);
2126 } else {
2127 tfp_sprintf(buff + 1, "%3d%c%c", rssi, SYM_DBM, ' ');
2129 if (!failsafeIsReceivingRxData()){
2130 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2131 } else if (osdConfig()->rssi_dbm_alarm && rssi < osdConfig()->rssi_dbm_alarm) {
2132 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2134 break;
2136 case OSD_CRSF_LQ:
2138 buff[0] = SYM_LQ;
2139 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
2140 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
2141 switch (osdConfig()->crsf_lq_format) {
2142 case OSD_CRSF_LQ_TYPE1:
2143 if (!failsafeIsReceivingRxData()) {
2144 tfp_sprintf(buff+1, "%3d", 0);
2145 } else {
2146 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.uplinkLQ);
2148 break;
2149 case OSD_CRSF_LQ_TYPE2:
2150 if (!failsafeIsReceivingRxData()) {
2151 tfp_sprintf(buff+1, "%s:%3d", " ", 0);
2152 } else {
2153 tfp_sprintf(buff+1, "%d:%3d", rxLinkStatistics.rfMode, rxLinkStatistics.uplinkLQ);
2155 break;
2156 case OSD_CRSF_LQ_TYPE3:
2157 if (!failsafeIsReceivingRxData()) {
2158 tfp_sprintf(buff+1, "%3d", 0);
2159 } else {
2160 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.rfMode >= 2 ? scaledLQ : rxLinkStatistics.uplinkLQ);
2162 break;
2164 if (!failsafeIsReceivingRxData()) {
2165 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2166 } else if (rxLinkStatistics.uplinkLQ < osdConfig()->link_quality_alarm) {
2167 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2169 break;
2172 case OSD_CRSF_SNR_DB:
2174 static pt1Filter_t snrFilterState;
2175 static timeMs_t snrUpdated = 0;
2176 int8_t snrFiltered = pt1FilterApply4(&snrFilterState, rxLinkStatistics.uplinkSNR, 0.5f, MS2S(millis() - snrUpdated));
2177 snrUpdated = millis();
2179 const char* showsnr = "-20";
2180 const char* hidesnr = " ";
2181 if (snrFiltered > osdConfig()->snr_alarm) {
2182 if (cmsInMenu) {
2183 buff[0] = SYM_SNR;
2184 tfp_sprintf(buff + 1, "%s%c", showsnr, SYM_DB);
2185 } else {
2186 buff[0] = SYM_BLANK;
2187 tfp_sprintf(buff + 1, "%s%c", hidesnr, SYM_BLANK);
2189 } else if (snrFiltered <= osdConfig()->snr_alarm) {
2190 buff[0] = SYM_SNR;
2191 if (snrFiltered <= -10) {
2192 tfp_sprintf(buff + 1, "%3d%c", snrFiltered, SYM_DB);
2193 } else {
2194 tfp_sprintf(buff + 1, "%2d%c%c", snrFiltered, SYM_DB, ' ');
2197 break;
2200 case OSD_CRSF_TX_POWER:
2202 if (!failsafeIsReceivingRxData())
2203 tfp_sprintf(buff, "%s%c", " ", SYM_BLANK);
2204 else
2205 tfp_sprintf(buff, "%4d%c", rxLinkStatistics.uplinkTXPower, SYM_MW);
2206 break;
2208 #endif
2210 case OSD_CROSSHAIRS: // Hud is a sub-element of the crosshair
2212 osdCrosshairPosition(&elemPosX, &elemPosY);
2213 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX, elemPosY);
2215 if (osdConfig()->hud_homing && STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
2216 osdHudDrawHoming(elemPosX, elemPosY);
2219 if (STATE(GPS_FIX) && isImuHeadingValid()) {
2221 if (osdConfig()->hud_homepoint || osdConfig()->hud_radar_disp > 0 || osdConfig()->hud_wp_disp > 0) {
2222 osdHudClear();
2225 // -------- POI : Home point
2227 if (osdConfig()->hud_homepoint) { // Display the home point (H)
2228 osdHudDrawPoi(GPS_distanceToHome, GPS_directionToHome, -osdGetAltitude() / 100, 0, SYM_HOME, 0 , 0);
2231 // -------- POI : Nearby aircrafts from ESP32 radar
2233 if (osdConfig()->hud_radar_disp > 0) { // Display the POI from the radar
2234 for (uint8_t i = 0; i < osdConfig()->hud_radar_disp; i++) {
2235 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
2236 fpVector3_t poi;
2237 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &radar_pois[i].gps, GEO_ALT_RELATIVE);
2238 radar_pois[i].distance = calculateDistanceToDestination(&poi) / 100; // In meters
2240 if (radar_pois[i].distance >= osdConfig()->hud_radar_range_min && radar_pois[i].distance <= osdConfig()->hud_radar_range_max) {
2241 radar_pois[i].direction = calculateBearingToDestination(&poi) / 100; // In °
2242 radar_pois[i].altitude = (radar_pois[i].gps.alt - osdGetAltitudeMsl()) / 100;
2243 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);
2249 // -------- POI : Next waypoints from navigation
2251 if (osdConfig()->hud_wp_disp > 0 && posControl.waypointListValid && posControl.waypointCount > 0) { // Display the next waypoints
2252 gpsLocation_t wp2;
2253 int j;
2255 for (int i = osdConfig()->hud_wp_disp - 1; i >= 0 ; i--) { // Display in reverse order so the next WP is always written on top
2256 j = posControl.activeWaypointIndex + i;
2257 if (j > posControl.startWpIndex + posControl.waypointCount - 1) { // limit to max WP index for mission
2258 break;
2260 if (posControl.waypointList[j].lat != 0 && posControl.waypointList[j].lon != 0) {
2261 wp2.lat = posControl.waypointList[j].lat;
2262 wp2.lon = posControl.waypointList[j].lon;
2263 wp2.alt = posControl.waypointList[j].alt;
2264 fpVector3_t poi;
2265 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &wp2, waypointMissionAltConvMode(posControl.waypointList[j].p3));
2266 int32_t altConvModeAltitude = waypointMissionAltConvMode(posControl.waypointList[j].p3) == GEO_ALT_ABSOLUTE ? osdGetAltitudeMsl() : osdGetAltitude();
2267 j = getGeoWaypointNumber(j);
2268 while (j > 9) j -= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2269 osdHudDrawPoi(calculateDistanceToDestination(&poi) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi) / 100), (posControl.waypointList[j].alt - altConvModeAltitude)/ 100, 2, SYM_WAYPOINT, 48 + j, i);
2275 return true;
2276 break;
2278 case OSD_ATTITUDE_ROLL:
2279 buff[0] = SYM_ROLL_LEVEL;
2280 if (ABS(attitude.values.roll) >= 1)
2281 buff[0] += (attitude.values.roll < 0 ? -1 : 1);
2282 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.roll)), 0, 1, 0, 3);
2283 break;
2285 case OSD_ATTITUDE_PITCH:
2286 if (ABS(attitude.values.pitch) < 1)
2287 buff[0] = 'P';
2288 else if (attitude.values.pitch > 0)
2289 buff[0] = SYM_PITCH_DOWN;
2290 else if (attitude.values.pitch < 0)
2291 buff[0] = SYM_PITCH_UP;
2292 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.pitch)), 0, 1, 0, 3);
2293 break;
2295 case OSD_ARTIFICIAL_HORIZON:
2297 float rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2298 float pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2300 pitchAngle -= osdConfig()->ahi_camera_uptilt_comp ? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt) : 0;
2301 pitchAngle += DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2302 if (osdConfig()->ahi_reverse_roll) {
2303 rollAngle = -rollAngle;
2305 osdDrawArtificialHorizon(osdDisplayPort, osdGetDisplayPortCanvas(),
2306 OSD_DRAW_POINT_GRID(elemPosX, elemPosY), rollAngle, pitchAngle);
2307 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
2308 osdDrawSingleElement(OSD_CROSSHAIRS);
2310 return true;
2313 case OSD_HORIZON_SIDEBARS:
2315 osdDrawSidebars(osdDisplayPort, osdGetDisplayPortCanvas());
2316 return true;
2319 #if defined(USE_BARO) || defined(USE_GPS)
2320 case OSD_VARIO:
2322 float zvel = getEstimatedActualVelocity(Z);
2323 osdDrawVario(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), zvel);
2324 return true;
2327 case OSD_VARIO_NUM:
2329 int16_t value = getEstimatedActualVelocity(Z);
2330 char sym;
2331 switch ((osd_unit_e)osdConfig()->units) {
2332 case OSD_UNIT_UK:
2333 FALLTHROUGH;
2334 case OSD_UNIT_IMPERIAL:
2335 // Convert to centifeet/s
2336 value = CENTIMETERS_TO_CENTIFEET(value);
2337 sym = SYM_FTS;
2338 break;
2339 case OSD_UNIT_GA:
2340 // Convert to centi-100feet/min
2341 value = CENTIMETERS_TO_FEET(value * 60);
2342 sym = SYM_100FTM;
2343 break;
2344 default:
2345 case OSD_UNIT_METRIC_MPH:
2346 FALLTHROUGH;
2347 case OSD_UNIT_METRIC:
2348 // Already in cm/s
2349 sym = SYM_MS;
2350 break;
2353 osdFormatCentiNumber(buff, value, 0, 1, 0, 3);
2354 buff[3] = sym;
2355 buff[4] = '\0';
2356 break;
2358 case OSD_CLIMB_EFFICIENCY:
2360 // amperage is in centi amps (10mA), vertical speed is in cms/s. We want
2361 // Ah/dist only to show when vertical speed > 1m/s.
2362 static pt1Filter_t veFilterState;
2363 static timeUs_t vEfficiencyUpdated = 0;
2364 int32_t value = 0;
2365 timeUs_t currentTimeUs = micros();
2366 timeDelta_t vEfficiencyTimeDelta = cmpTimeUs(currentTimeUs, vEfficiencyUpdated);
2367 if (getEstimatedActualVelocity(Z) > 0) {
2368 if (vEfficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2369 // Centiamps (kept for osdFormatCentiNumber) / m/s - Will appear as A / m/s in OSD
2370 value = pt1FilterApply4(&veFilterState, (float)getAmperage() / (getEstimatedActualVelocity(Z) / 100.0f), 1, US2S(vEfficiencyTimeDelta));
2372 vEfficiencyUpdated = currentTimeUs;
2373 } else {
2374 value = veFilterState.state;
2377 bool efficiencyValid = (value > 0) && (getEstimatedActualVelocity(Z) > 100);
2378 switch (osdConfig()->units) {
2379 case OSD_UNIT_UK:
2380 FALLTHROUGH;
2381 case OSD_UNIT_GA:
2382 FALLTHROUGH;
2383 case OSD_UNIT_IMPERIAL:
2384 // mAh/foot
2385 if (efficiencyValid) {
2386 osdFormatCentiNumber(buff, (value * METERS_PER_FOOT), 1, 2, 2, 3);
2387 tfp_sprintf(buff, "%s%c%c", buff, SYM_AH_V_FT_0, SYM_AH_V_FT_1);
2388 } else {
2389 buff[0] = buff[1] = buff[2] = '-';
2390 buff[3] = SYM_AH_V_FT_0;
2391 buff[4] = SYM_AH_V_FT_1;
2392 buff[5] = '\0';
2394 break;
2395 case OSD_UNIT_METRIC_MPH:
2396 FALLTHROUGH;
2397 case OSD_UNIT_METRIC:
2398 // mAh/metre
2399 if (efficiencyValid) {
2400 osdFormatCentiNumber(buff, value, 1, 2, 2, 3);
2401 tfp_sprintf(buff, "%s%c%c", buff, SYM_AH_V_M_0, SYM_AH_V_M_1);
2402 } else {
2403 buff[0] = buff[1] = buff[2] = '-';
2404 buff[3] = SYM_AH_V_M_0;
2405 buff[4] = SYM_AH_V_M_1;
2406 buff[5] = '\0';
2408 break;
2410 break;
2412 case OSD_GLIDE_TIME_REMAINING:
2414 uint16_t glideTime = osdGetRemainingGlideTime();
2415 buff[0] = SYM_GLIDE_MINS;
2416 if (glideTime > 0) {
2417 // Maximum value we can show in minutes is 99 minutes and 59 seconds. It is extremely unlikely that glide
2418 // time will be longer than 99 minutes. If it is, it will show 99:^^
2419 if (glideTime > (99 * 60) + 59) {
2420 tfp_sprintf(buff + 1, "%02d:", (int)(glideTime / 60));
2421 buff[4] = SYM_DIRECTION;
2422 buff[5] = SYM_DIRECTION;
2423 } else {
2424 tfp_sprintf(buff + 1, "%02d:%02d", (int)(glideTime / 60), (int)(glideTime % 60));
2426 } else {
2427 tfp_sprintf(buff + 1, "%s", "--:--");
2429 buff[6] = '\0';
2430 break;
2432 case OSD_GLIDE_RANGE:
2434 uint16_t glideSeconds = osdGetRemainingGlideTime();
2435 buff[0] = SYM_GLIDE_DIST;
2436 if (glideSeconds > 0) {
2437 uint32_t glideRangeCM = glideSeconds * gpsSol.groundSpeed;
2438 osdFormatDistanceSymbol(buff + 1, glideRangeCM, 0);
2439 } else {
2440 tfp_sprintf(buff + 1, "%s%c", "---", SYM_BLANK);
2441 buff[5] = '\0';
2443 break;
2445 #endif
2447 case OSD_SWITCH_INDICATOR_0:
2448 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channel - 1), buff);
2449 break;
2451 case OSD_SWITCH_INDICATOR_1:
2452 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channel - 1), buff);
2453 break;
2455 case OSD_SWITCH_INDICATOR_2:
2456 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channel - 1), buff);
2457 break;
2459 case OSD_SWITCH_INDICATOR_3:
2460 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channel - 1), buff);
2461 break;
2463 case OSD_ACTIVE_PROFILE:
2464 tfp_sprintf(buff, "%c%u", SYM_PROFILE, (getConfigProfile() + 1));
2465 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2466 break;
2468 case OSD_ROLL_PIDS:
2469 osdDisplayFlightPIDValues(elemPosX, elemPosY, "ROL", PID_ROLL, ADJUSTMENT_ROLL_P, ADJUSTMENT_ROLL_I, ADJUSTMENT_ROLL_D, ADJUSTMENT_ROLL_FF);
2470 return true;
2472 case OSD_PITCH_PIDS:
2473 osdDisplayFlightPIDValues(elemPosX, elemPosY, "PIT", PID_PITCH, ADJUSTMENT_PITCH_P, ADJUSTMENT_PITCH_I, ADJUSTMENT_PITCH_D, ADJUSTMENT_PITCH_FF);
2474 return true;
2476 case OSD_YAW_PIDS:
2477 osdDisplayFlightPIDValues(elemPosX, elemPosY, "YAW", PID_YAW, ADJUSTMENT_YAW_P, ADJUSTMENT_YAW_I, ADJUSTMENT_YAW_D, ADJUSTMENT_YAW_FF);
2478 return true;
2480 case OSD_LEVEL_PIDS:
2481 osdDisplayNavPIDValues(elemPosX, elemPosY, "LEV", PID_LEVEL, ADJUSTMENT_LEVEL_P, ADJUSTMENT_LEVEL_I, ADJUSTMENT_LEVEL_D);
2482 return true;
2484 case OSD_POS_XY_PIDS:
2485 osdDisplayNavPIDValues(elemPosX, elemPosY, "PXY", PID_POS_XY, ADJUSTMENT_POS_XY_P, ADJUSTMENT_POS_XY_I, ADJUSTMENT_POS_XY_D);
2486 return true;
2488 case OSD_POS_Z_PIDS:
2489 osdDisplayNavPIDValues(elemPosX, elemPosY, "PZ", PID_POS_Z, ADJUSTMENT_POS_Z_P, ADJUSTMENT_POS_Z_I, ADJUSTMENT_POS_Z_D);
2490 return true;
2492 case OSD_VEL_XY_PIDS:
2493 osdDisplayNavPIDValues(elemPosX, elemPosY, "VXY", PID_VEL_XY, ADJUSTMENT_VEL_XY_P, ADJUSTMENT_VEL_XY_I, ADJUSTMENT_VEL_XY_D);
2494 return true;
2496 case OSD_VEL_Z_PIDS:
2497 osdDisplayNavPIDValues(elemPosX, elemPosY, "VZ", PID_VEL_Z, ADJUSTMENT_VEL_Z_P, ADJUSTMENT_VEL_Z_I, ADJUSTMENT_VEL_Z_D);
2498 return true;
2500 case OSD_HEADING_P:
2501 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "HP", 0, pidBank()->pid[PID_HEADING].P, 3, 0, ADJUSTMENT_HEADING_P);
2502 return true;
2504 case OSD_BOARD_ALIGN_ROLL:
2505 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT);
2506 return true;
2508 case OSD_BOARD_ALIGN_PITCH:
2509 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT);
2510 return true;
2512 case OSD_RC_EXPO:
2513 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "EXP", 0, currentControlRateProfile->stabilized.rcExpo8, 3, 0, ADJUSTMENT_RC_EXPO);
2514 return true;
2516 case OSD_RC_YAW_EXPO:
2517 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "YEX", 0, currentControlRateProfile->stabilized.rcYawExpo8, 3, 0, ADJUSTMENT_RC_YAW_EXPO);
2518 return true;
2520 case OSD_THROTTLE_EXPO:
2521 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TEX", 0, currentControlRateProfile->throttle.rcExpo8, 3, 0, ADJUSTMENT_THROTTLE_EXPO);
2522 return true;
2524 case OSD_PITCH_RATE:
2525 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SPR");
2527 elemAttr = TEXT_ATTRIBUTES_NONE;
2528 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_PITCH]);
2529 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2530 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2531 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2532 return true;
2534 case OSD_ROLL_RATE:
2535 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SRR");
2537 elemAttr = TEXT_ATTRIBUTES_NONE;
2538 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_ROLL]);
2539 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2540 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2541 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2542 return true;
2544 case OSD_YAW_RATE:
2545 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "SYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2546 return true;
2548 case OSD_MANUAL_RC_EXPO:
2549 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MEX", 0, currentControlRateProfile->manual.rcExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO);
2550 return true;
2552 case OSD_MANUAL_RC_YAW_EXPO:
2553 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYX", 0, currentControlRateProfile->manual.rcYawExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO);
2554 return true;
2556 case OSD_MANUAL_PITCH_RATE:
2557 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MPR");
2559 elemAttr = TEXT_ATTRIBUTES_NONE;
2560 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_PITCH]);
2561 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2562 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2563 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2564 return true;
2566 case OSD_MANUAL_ROLL_RATE:
2567 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MRR");
2569 elemAttr = TEXT_ATTRIBUTES_NONE;
2570 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_ROLL]);
2571 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2572 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2573 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2574 return true;
2576 case OSD_MANUAL_YAW_RATE:
2577 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2578 return true;
2580 case OSD_NAV_FW_CRUISE_THR:
2581 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CRZ", 0, currentBatteryProfile->nav.fw.cruise_throttle, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR);
2582 return true;
2584 case OSD_NAV_FW_PITCH2THR:
2585 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "P2T", 0, currentBatteryProfile->nav.fw.pitch_to_throttle, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR);
2586 return true;
2588 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE:
2589 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "0TP", 0, (float)navConfig()->fw.minThrottleDownPitchAngle / 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE);
2590 return true;
2592 case OSD_FW_ALT_PID_OUTPUTS:
2594 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2595 osdFormatPidControllerOutput(buff, "PZO", &nav_pids->fw_alt, 10, true); // display requested pitch degrees
2596 break;
2599 case OSD_FW_POS_PID_OUTPUTS:
2601 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers(); // display requested roll degrees
2602 osdFormatPidControllerOutput(buff, "PXYO", &nav_pids->fw_nav, 1, true);
2603 break;
2606 case OSD_MC_VEL_Z_PID_OUTPUTS:
2608 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2609 osdFormatPidControllerOutput(buff, "VZO", &nav_pids->vel[Z], 100, false); // display throttle adjustment µs
2610 break;
2613 case OSD_MC_VEL_X_PID_OUTPUTS:
2615 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2616 osdFormatPidControllerOutput(buff, "VXO", &nav_pids->vel[X], 100, false); // display requested acceleration cm/s^2
2617 break;
2620 case OSD_MC_VEL_Y_PID_OUTPUTS:
2622 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2623 osdFormatPidControllerOutput(buff, "VYO", &nav_pids->vel[Y], 100, false); // display requested acceleration cm/s^2
2624 break;
2627 case OSD_MC_POS_XYZ_P_OUTPUTS:
2629 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2630 strcpy(buff, "POSO ");
2631 // display requested velocity cm/s
2632 tfp_sprintf(buff + 5, "%4d", (int)lrintf(nav_pids->pos[X].output_constrained * 100));
2633 buff[9] = ' ';
2634 tfp_sprintf(buff + 10, "%4d", (int)lrintf(nav_pids->pos[Y].output_constrained * 100));
2635 buff[14] = ' ';
2636 tfp_sprintf(buff + 15, "%4d", (int)lrintf(nav_pids->pos[Z].output_constrained * 100));
2637 buff[19] = '\0';
2638 break;
2641 case OSD_POWER:
2643 bool kiloWatt = osdFormatCentiNumber(buff, getPower(), 1000, 2, 2, 3);
2644 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
2645 buff[4] = '\0';
2647 uint8_t current_alarm = osdConfig()->current_alarm;
2648 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
2649 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2651 break;
2654 case OSD_AIR_SPEED:
2656 #ifdef USE_PITOT
2657 const float airspeed_estimate = getAirspeedEstimate();
2658 buff[0] = SYM_AIR;
2659 osdFormatVelocityStr(buff + 1, airspeed_estimate, false, false);
2661 if ((osdConfig()->airspeed_alarm_min != 0 && airspeed_estimate < osdConfig()->airspeed_alarm_min) ||
2662 (osdConfig()->airspeed_alarm_max != 0 && airspeed_estimate > osdConfig()->airspeed_alarm_max)) {
2663 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2665 #else
2666 return false;
2667 #endif
2668 break;
2671 case OSD_AIR_MAX_SPEED:
2673 #ifdef USE_PITOT
2674 buff[0] = SYM_MAX;
2675 buff[1] = SYM_AIR;
2676 osdFormatVelocityStr(buff + 2, stats.max_air_speed, false, false);
2677 #else
2678 return false;
2679 #endif
2680 break;
2683 case OSD_RTC_TIME:
2685 // RTC not configured will show 00:00
2686 dateTime_t dateTime;
2687 rtcGetDateTimeLocal(&dateTime);
2688 buff[0] = SYM_CLOCK;
2689 tfp_sprintf(buff + 1, "%02u:%02u:%02u", dateTime.hours, dateTime.minutes, dateTime.seconds);
2690 break;
2693 case OSD_MESSAGES:
2695 elemAttr = osdGetSystemMessage(buff, OSD_MESSAGE_LENGTH, true);
2696 break;
2699 case OSD_VERSION:
2701 tfp_sprintf(buff, "INAV %s", FC_VERSION_STRING);
2702 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2703 break;
2706 case OSD_MAIN_BATT_CELL_VOLTAGE:
2708 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawAverageCellVoltage(), 3, 2);
2709 return true;
2712 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE:
2714 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedAverageCellVoltage(), 3, 2);
2715 return true;
2718 case OSD_THROTTLE_POS_AUTO_THR:
2720 osdFormatThrottlePosition(buff, true, &elemAttr);
2721 break;
2724 case OSD_HEADING_GRAPH:
2726 if (osdIsHeadingValid()) {
2727 osdDrawHeadingGraph(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), osdGetHeading());
2728 return true;
2729 } else {
2730 buff[0] = buff[2] = buff[4] = buff[6] = buff[8] = SYM_HEADING_LINE;
2731 buff[1] = buff[3] = buff[5] = buff[7] = SYM_HEADING_DIVIDED_LINE;
2732 buff[OSD_HEADING_GRAPH_WIDTH] = '\0';
2734 break;
2737 case OSD_EFFICIENCY_MAH_PER_KM:
2739 // amperage is in centi amps, speed is in cms/s. We want
2740 // mah/km. Only show when ground speed > 1m/s.
2741 static pt1Filter_t eFilterState;
2742 static timeUs_t efficiencyUpdated = 0;
2743 int32_t value = 0;
2744 bool moreThanAh = false;
2745 timeUs_t currentTimeUs = micros();
2746 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2747 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2748 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2749 value = pt1FilterApply4(&eFilterState, ((float)getAmperage() / gpsSol.groundSpeed) / 0.0036f,
2750 1, US2S(efficiencyTimeDelta));
2752 efficiencyUpdated = currentTimeUs;
2753 } else {
2754 value = eFilterState.state;
2757 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2758 switch (osdConfig()->units) {
2759 case OSD_UNIT_UK:
2760 FALLTHROUGH;
2761 case OSD_UNIT_IMPERIAL:
2762 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10, 1000, 0, 2, 3);
2763 if (!moreThanAh) {
2764 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
2765 } else {
2766 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
2768 if (!efficiencyValid) {
2769 buff[0] = buff[1] = buff[2] = '-';
2770 buff[3] = SYM_MAH_MI_0;
2771 buff[4] = SYM_MAH_MI_1;
2772 buff[5] = '\0';
2774 break;
2775 case OSD_UNIT_GA:
2776 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10, 1000, 0, 2, 3);
2777 if (!moreThanAh) {
2778 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
2779 } else {
2780 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
2782 if (!efficiencyValid) {
2783 buff[0] = buff[1] = buff[2] = '-';
2784 buff[3] = SYM_MAH_NM_0;
2785 buff[4] = SYM_MAH_NM_1;
2786 buff[5] = '\0';
2788 break;
2789 case OSD_UNIT_METRIC_MPH:
2790 FALLTHROUGH;
2791 case OSD_UNIT_METRIC:
2792 moreThanAh = osdFormatCentiNumber(buff, value * 100, 1000, 0, 2, 3);
2793 if (!moreThanAh) {
2794 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
2795 } else {
2796 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
2798 if (!efficiencyValid) {
2799 buff[0] = buff[1] = buff[2] = '-';
2800 buff[3] = SYM_MAH_KM_0;
2801 buff[4] = SYM_MAH_KM_1;
2802 buff[5] = '\0';
2804 break;
2806 break;
2809 case OSD_EFFICIENCY_WH_PER_KM:
2811 // amperage is in centi amps, speed is in cms/s. We want
2812 // mWh/km. Only show when ground speed > 1m/s.
2813 static pt1Filter_t eFilterState;
2814 static timeUs_t efficiencyUpdated = 0;
2815 int32_t value = 0;
2816 timeUs_t currentTimeUs = micros();
2817 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2818 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2819 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2820 value = pt1FilterApply4(&eFilterState, ((float)getPower() / gpsSol.groundSpeed) / 0.0036f,
2821 1, US2S(efficiencyTimeDelta));
2823 efficiencyUpdated = currentTimeUs;
2824 } else {
2825 value = eFilterState.state;
2828 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2829 switch (osdConfig()->units) {
2830 case OSD_UNIT_UK:
2831 FALLTHROUGH;
2832 case OSD_UNIT_IMPERIAL:
2833 osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10000, 0, 2, 0, 3);
2834 buff[3] = SYM_WH_MI;
2835 break;
2836 case OSD_UNIT_GA:
2837 osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10000, 0, 2, 0, 3);
2838 buff[3] = SYM_WH_NM;
2839 break;
2840 case OSD_UNIT_METRIC_MPH:
2841 FALLTHROUGH;
2842 case OSD_UNIT_METRIC:
2843 osdFormatCentiNumber(buff, value / 10, 0, 2, 0, 3);
2844 buff[3] = SYM_WH_KM;
2845 break;
2847 buff[4] = '\0';
2848 if (!efficiencyValid) {
2849 buff[0] = buff[1] = buff[2] = '-';
2851 break;
2854 case OSD_GFORCE:
2856 buff[0] = SYM_GFORCE;
2857 osdFormatCentiNumber(buff + 1, GForce, 0, 2, 0, 3);
2858 if (GForce > osdConfig()->gforce_alarm * 100) {
2859 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2861 break;
2864 case OSD_GFORCE_X:
2865 case OSD_GFORCE_Y:
2866 case OSD_GFORCE_Z:
2868 float GForceValue = GForceAxis[item - OSD_GFORCE_X];
2869 buff[0] = SYM_GFORCE_X + item - OSD_GFORCE_X;
2870 osdFormatCentiNumber(buff + 1, GForceValue, 0, 2, 0, 4);
2871 if ((GForceValue < osdConfig()->gforce_axis_alarm_min * 100) || (GForceValue > osdConfig()->gforce_axis_alarm_max * 100)) {
2872 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2874 break;
2876 case OSD_DEBUG:
2879 * Longest representable string is -2147483648 does not fit in the screen.
2880 * Only 7 digits for negative and 8 digits for positive values allowed
2882 for (uint8_t bufferIndex = 0; bufferIndex < DEBUG32_VALUE_COUNT; ++elemPosY, bufferIndex += 2) {
2883 tfp_sprintf(
2884 buff,
2885 "[%u]=%8ld [%u]=%8ld",
2886 bufferIndex,
2887 constrain(debug[bufferIndex], -9999999, 99999999),
2888 bufferIndex+1,
2889 constrain(debug[bufferIndex+1], -9999999, 99999999)
2891 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2893 break;
2896 case OSD_IMU_TEMPERATURE:
2898 int16_t temperature;
2899 const bool valid = getIMUTemperature(&temperature);
2900 osdDisplayTemperature(elemPosX, elemPosY, SYM_IMU_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2901 return true;
2904 case OSD_BARO_TEMPERATURE:
2906 int16_t temperature;
2907 const bool valid = getBaroTemperature(&temperature);
2908 osdDisplayTemperature(elemPosX, elemPosY, SYM_BARO_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2909 return true;
2912 #ifdef USE_TEMPERATURE_SENSOR
2913 case OSD_TEMP_SENSOR_0_TEMPERATURE:
2914 case OSD_TEMP_SENSOR_1_TEMPERATURE:
2915 case OSD_TEMP_SENSOR_2_TEMPERATURE:
2916 case OSD_TEMP_SENSOR_3_TEMPERATURE:
2917 case OSD_TEMP_SENSOR_4_TEMPERATURE:
2918 case OSD_TEMP_SENSOR_5_TEMPERATURE:
2919 case OSD_TEMP_SENSOR_6_TEMPERATURE:
2920 case OSD_TEMP_SENSOR_7_TEMPERATURE:
2922 osdDisplayTemperatureSensor(elemPosX, elemPosY, item - OSD_TEMP_SENSOR_0_TEMPERATURE);
2923 return true;
2925 #endif /* ifdef USE_TEMPERATURE_SENSOR */
2927 case OSD_WIND_SPEED_HORIZONTAL:
2928 #ifdef USE_WIND_ESTIMATOR
2930 bool valid = isEstimatedWindSpeedValid();
2931 float horizontalWindSpeed;
2932 if (valid) {
2933 uint16_t angle;
2934 horizontalWindSpeed = getEstimatedHorizontalWindSpeed(&angle);
2935 int16_t windDirection = osdGetHeadingAngle( CENTIDEGREES_TO_DEGREES((int)angle) - DECIDEGREES_TO_DEGREES(attitude.values.yaw) + 22);
2936 buff[1] = SYM_DIRECTION + (windDirection*2 / 90);
2937 } else {
2938 horizontalWindSpeed = 0;
2939 buff[1] = SYM_BLANK;
2941 buff[0] = SYM_WIND_HORIZONTAL;
2942 osdFormatWindSpeedStr(buff + 2, horizontalWindSpeed, valid);
2943 break;
2945 #else
2946 return false;
2947 #endif
2949 case OSD_WIND_SPEED_VERTICAL:
2950 #ifdef USE_WIND_ESTIMATOR
2952 buff[0] = SYM_WIND_VERTICAL;
2953 buff[1] = SYM_BLANK;
2954 bool valid = isEstimatedWindSpeedValid();
2955 float verticalWindSpeed;
2956 if (valid) {
2957 verticalWindSpeed = -getEstimatedWindSpeed(Z); //from NED to NEU
2958 if (verticalWindSpeed < 0) {
2959 buff[1] = SYM_AH_DECORATION_DOWN;
2960 verticalWindSpeed = -verticalWindSpeed;
2961 } else if (verticalWindSpeed > 0) {
2962 buff[1] = SYM_AH_DECORATION_UP;
2964 } else {
2965 verticalWindSpeed = 0;
2967 osdFormatWindSpeedStr(buff + 2, verticalWindSpeed, valid);
2968 break;
2970 #else
2971 return false;
2972 #endif
2974 case OSD_PLUS_CODE:
2976 STATIC_ASSERT(GPS_DEGREES_DIVIDER == OLC_DEG_MULTIPLIER, invalid_olc_deg_multiplier);
2977 int digits = osdConfig()->plus_code_digits;
2978 int digitsRemoved = osdConfig()->plus_code_short * 2;
2979 if (STATE(GPS_FIX)) {
2980 olc_encode(gpsSol.llh.lat, gpsSol.llh.lon, digits, buff, sizeof(buff));
2981 } else {
2982 // +codes with > 8 digits have a + at the 9th digit
2983 // and we only support 10 and up.
2984 memset(buff, '-', digits + 1);
2985 buff[8] = '+';
2986 buff[digits + 1] = '\0';
2988 // Optionally trim digits from the left
2989 memmove(buff, buff+digitsRemoved, strlen(buff) + digitsRemoved);
2990 buff[digits + 1 - digitsRemoved] = '\0';
2991 break;
2994 case OSD_AZIMUTH:
2997 buff[0] = SYM_AZIMUTH;
2998 if (osdIsHeadingValid()) {
2999 int16_t h = GPS_directionToHome;
3000 if (h < 0) {
3001 h += 360;
3003 if (h >= 180)
3004 h = h - 180;
3005 else
3006 h = h + 180;
3008 tfp_sprintf(&buff[1], "%3d", h);
3009 } else {
3010 buff[1] = buff[2] = buff[3] = '-';
3012 buff[4] = SYM_DEGREES;
3013 buff[5] = '\0';
3014 break;
3017 case OSD_MAP_SCALE:
3019 float scaleToUnit;
3020 int scaleUnitDivisor;
3021 char symUnscaled;
3022 char symScaled;
3023 int maxDecimals;
3025 switch (osdConfig()->units) {
3026 case OSD_UNIT_UK:
3027 FALLTHROUGH;
3028 case OSD_UNIT_IMPERIAL:
3029 scaleToUnit = 100 / 1609.3440f; // scale to 0.01mi for osdFormatCentiNumber()
3030 scaleUnitDivisor = 0;
3031 symUnscaled = SYM_MI;
3032 symScaled = SYM_MI;
3033 maxDecimals = 2;
3034 break;
3035 case OSD_UNIT_GA:
3036 scaleToUnit = 100 / 1852.0010f; // scale to 0.01mi for osdFormatCentiNumber()
3037 scaleUnitDivisor = 0;
3038 symUnscaled = SYM_NM;
3039 symScaled = SYM_NM;
3040 maxDecimals = 2;
3041 break;
3042 default:
3043 case OSD_UNIT_METRIC_MPH:
3044 FALLTHROUGH;
3045 case OSD_UNIT_METRIC:
3046 scaleToUnit = 100; // scale to cm for osdFormatCentiNumber()
3047 scaleUnitDivisor = 1000; // Convert to km when scale gets bigger than 999m
3048 symUnscaled = SYM_M;
3049 symScaled = SYM_KM;
3050 maxDecimals = 0;
3051 break;
3053 buff[0] = SYM_SCALE;
3054 if (osdMapData.scale > 0) {
3055 bool scaled = osdFormatCentiNumber(&buff[1], osdMapData.scale * scaleToUnit, scaleUnitDivisor, maxDecimals, 2, 3);
3056 buff[4] = scaled ? symScaled : symUnscaled;
3057 // Make sure this is cleared if the map stops being drawn
3058 osdMapData.scale = 0;
3059 } else {
3060 memset(&buff[1], '-', 4);
3062 buff[5] = '\0';
3063 break;
3065 case OSD_MAP_REFERENCE:
3067 char referenceSymbol;
3068 if (osdMapData.referenceSymbol) {
3069 referenceSymbol = osdMapData.referenceSymbol;
3070 // Make sure this is cleared if the map stops being drawn
3071 osdMapData.referenceSymbol = 0;
3072 } else {
3073 referenceSymbol = '-';
3075 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DIRECTION);
3076 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 1, referenceSymbol);
3077 return true;
3080 case OSD_GVAR_0:
3082 osdFormatGVar(buff, 0);
3083 break;
3085 case OSD_GVAR_1:
3087 osdFormatGVar(buff, 1);
3088 break;
3090 case OSD_GVAR_2:
3092 osdFormatGVar(buff, 2);
3093 break;
3095 case OSD_GVAR_3:
3097 osdFormatGVar(buff, 3);
3098 break;
3101 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3102 case OSD_RC_SOURCE:
3104 const char *source_text = IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
3105 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3106 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, source_text, elemAttr);
3107 return true;
3109 #endif
3111 #if defined(USE_ESC_SENSOR)
3112 case OSD_ESC_RPM:
3114 escSensorData_t * escSensor = escSensorGetData();
3115 if (escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE) {
3116 osdFormatRpm(buff, escSensor->rpm);
3118 else {
3119 osdFormatRpm(buff, 0);
3121 break;
3123 case OSD_ESC_TEMPERATURE:
3125 escSensorData_t * escSensor = escSensorGetData();
3126 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
3127 osdDisplayTemperature(elemPosX, elemPosY, SYM_ESC_TEMP, NULL, escTemperatureValid, (escSensor->temperature)*10, osdConfig()->esc_temp_alarm_min, osdConfig()->esc_temp_alarm_max);
3128 return true;
3130 #endif
3131 case OSD_TPA:
3133 char buff[4];
3134 textAttributes_t attr;
3136 displayWrite(osdDisplayPort, elemPosX, elemPosY, "TPA");
3137 attr = TEXT_ATTRIBUTES_NONE;
3138 tfp_sprintf(buff, "%3d", currentControlRateProfile->throttle.dynPID);
3139 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA)) {
3140 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3142 displayWriteWithAttr(osdDisplayPort, elemPosX + 5, elemPosY, buff, attr);
3144 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, "BP");
3145 attr = TEXT_ATTRIBUTES_NONE;
3146 tfp_sprintf(buff, "%4d", currentControlRateProfile->throttle.pa_breakpoint);
3147 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT)) {
3148 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3150 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY + 1, buff, attr);
3152 return true;
3154 case OSD_TPA_TIME_CONSTANT:
3156 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TPA TC", 0, currentControlRateProfile->throttle.fixedWingTauMs, 4, 0, ADJUSTMENT_FW_TPA_TIME_CONSTANT);
3157 return true;
3159 case OSD_FW_LEVEL_TRIM:
3161 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "LEVEL", 0, pidProfileMutable()->fixedWingLevelTrim, 3, 1, ADJUSTMENT_FW_LEVEL_TRIM);
3162 return true;
3165 case OSD_NAV_FW_CONTROL_SMOOTHNESS:
3167 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CTL S", 0, navConfig()->fw.control_smoothness, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS);
3168 return true;
3170 #ifdef USE_MULTI_MISSION
3171 case OSD_NAV_WP_MULTI_MISSION_INDEX:
3173 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "WP NO", 0, navConfig()->general.waypoint_multi_mission_index, 1, 0, ADJUSTMENT_NAV_WP_MULTI_MISSION_INDEX);
3174 return true;
3176 #endif
3177 case OSD_MISSION:
3179 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION)) {
3180 char buf[5];
3181 switch (posControl.wpMissionPlannerStatus) {
3182 case WP_PLAN_WAIT:
3183 strcpy(buf, "WAIT");
3184 break;
3185 case WP_PLAN_SAVE:
3186 strcpy(buf, "SAVE");
3187 break;
3188 case WP_PLAN_OK:
3189 strcpy(buf, " OK ");
3190 break;
3191 case WP_PLAN_FULL:
3192 strcpy(buf, "FULL");
3194 tfp_sprintf(buff, "%s>%2uWP", buf, posControl.wpPlannerActiveWPIndex);
3195 } else if (posControl.wpPlannerActiveWPIndex){
3196 tfp_sprintf(buff, "PLAN>%2uWP", posControl.waypointCount); // mission planner mision active
3198 #ifdef USE_MULTI_MISSION
3199 else {
3200 if (ARMING_FLAG(ARMED) && !(IS_RC_MODE_ACTIVE(BOXCHANGEMISSION) && posControl.multiMissionCount > 1)){
3201 // Limit field size when Armed, only show selected mission
3202 tfp_sprintf(buff, "M%u ", posControl.loadedMultiMissionIndex);
3203 } else if (posControl.multiMissionCount) {
3204 if (navConfig()->general.waypoint_multi_mission_index != posControl.loadedMultiMissionIndex) {
3205 tfp_sprintf(buff, "M%u/%u>LOAD", navConfig()->general.waypoint_multi_mission_index, posControl.multiMissionCount);
3206 } else {
3207 if (posControl.waypointListValid && posControl.waypointCount > 0) {
3208 tfp_sprintf(buff, "M%u/%u>%2uWP", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
3209 } else {
3210 tfp_sprintf(buff, "M0/%u> 0WP", posControl.multiMissionCount);
3213 } else { // no multi mission loaded - show active WP count from other source
3214 tfp_sprintf(buff, "WP CNT>%2u", posControl.waypointCount);
3217 #endif
3218 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3219 return true;
3222 #ifdef USE_POWER_LIMITS
3223 case OSD_PLIMIT_REMAINING_BURST_TIME:
3224 osdFormatCentiNumber(buff, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3);
3225 buff[3] = 'S';
3226 buff[4] = '\0';
3227 break;
3229 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT:
3230 if (currentBatteryProfile->powerLimits.continuousCurrent) {
3231 osdFormatCentiNumber(buff, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3);
3232 buff[3] = SYM_AMP;
3233 buff[4] = '\0';
3235 if (powerLimiterIsLimitingCurrent()) {
3236 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3239 break;
3241 #ifdef USE_ADC
3242 case OSD_PLIMIT_ACTIVE_POWER_LIMIT:
3244 if (currentBatteryProfile->powerLimits.continuousPower) {
3245 bool kiloWatt = osdFormatCentiNumber(buff, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3);
3246 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3247 buff[4] = '\0';
3249 if (powerLimiterIsLimitingPower()) {
3250 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3253 break;
3255 #endif // USE_ADC
3256 #endif // USE_POWER_LIMITS
3258 default:
3259 return false;
3262 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
3263 return true;
3266 uint8_t osdIncElementIndex(uint8_t elementIndex)
3268 ++elementIndex;
3270 if (elementIndex == OSD_ARTIFICIAL_HORIZON)
3271 ++elementIndex;
3273 #ifndef USE_TEMPERATURE_SENSOR
3274 if (elementIndex == OSD_TEMP_SENSOR_0_TEMPERATURE)
3275 elementIndex = OSD_ALTITUDE_MSL;
3276 #endif
3278 if (!sensors(SENSOR_ACC)) {
3279 if (elementIndex == OSD_CROSSHAIRS) {
3280 elementIndex = OSD_ONTIME;
3284 if (!feature(FEATURE_VBAT)) {
3285 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3286 elementIndex = OSD_LEVEL_PIDS;
3290 if (!feature(FEATURE_CURRENT_METER)) {
3291 if (elementIndex == OSD_CURRENT_DRAW) {
3292 elementIndex = OSD_GPS_SPEED;
3294 if (elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3295 elementIndex = OSD_TRIP_DIST;
3297 if (elementIndex == OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH) {
3298 elementIndex = OSD_HOME_HEADING_ERROR;
3300 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3301 elementIndex = OSD_LEVEL_PIDS;
3305 if (!feature(FEATURE_GPS)) {
3306 if (elementIndex == OSD_GPS_SPEED) {
3307 elementIndex = OSD_ALTITUDE;
3309 if (elementIndex == OSD_GPS_LON) {
3310 elementIndex = OSD_VARIO;
3312 if (elementIndex == OSD_GPS_HDOP) {
3313 elementIndex = OSD_MAIN_BATT_CELL_VOLTAGE;
3315 if (elementIndex == OSD_TRIP_DIST) {
3316 elementIndex = OSD_ATTITUDE_PITCH;
3318 if (elementIndex == OSD_WIND_SPEED_HORIZONTAL) {
3319 elementIndex = OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE;
3321 if (elementIndex == OSD_3D_SPEED) {
3322 elementIndex++;
3326 if (!STATE(ESC_SENSOR_ENABLED)) {
3327 if (elementIndex == OSD_ESC_RPM) {
3328 elementIndex++;
3332 #ifndef USE_POWER_LIMITS
3333 if (elementIndex == OSD_NAV_FW_CONTROL_SMOOTHNESS) {
3334 elementIndex = OSD_ITEM_COUNT;
3336 #endif
3338 if (elementIndex == OSD_ITEM_COUNT) {
3339 elementIndex = 0;
3341 return elementIndex;
3344 void osdDrawNextElement(void)
3346 static uint8_t elementIndex = 0;
3347 // Prevent infinite loop when no elements are enabled
3348 uint8_t index = elementIndex;
3349 do {
3350 elementIndex = osdIncElementIndex(elementIndex);
3351 } while(!osdDrawSingleElement(elementIndex) && index != elementIndex);
3353 // Draw artificial horizon + tracking telemtry last
3354 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
3355 if (osdConfig()->telemetry>0){
3356 osdDisplayTelemetry();
3360 PG_RESET_TEMPLATE(osdConfig_t, osdConfig,
3361 .rssi_alarm = SETTING_OSD_RSSI_ALARM_DEFAULT,
3362 .time_alarm = SETTING_OSD_TIME_ALARM_DEFAULT,
3363 .alt_alarm = SETTING_OSD_ALT_ALARM_DEFAULT,
3364 .dist_alarm = SETTING_OSD_DIST_ALARM_DEFAULT,
3365 .neg_alt_alarm = SETTING_OSD_NEG_ALT_ALARM_DEFAULT,
3366 .current_alarm = SETTING_OSD_CURRENT_ALARM_DEFAULT,
3367 .imu_temp_alarm_min = SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT,
3368 .imu_temp_alarm_max = SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT,
3369 .esc_temp_alarm_min = SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT,
3370 .esc_temp_alarm_max = SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT,
3371 .gforce_alarm = SETTING_OSD_GFORCE_ALARM_DEFAULT,
3372 .gforce_axis_alarm_min = SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT,
3373 .gforce_axis_alarm_max = SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT,
3374 #ifdef USE_BARO
3375 .baro_temp_alarm_min = SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT,
3376 .baro_temp_alarm_max = SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT,
3377 #endif
3378 #ifdef USE_SERIALRX_CRSF
3379 .snr_alarm = SETTING_OSD_SNR_ALARM_DEFAULT,
3380 .crsf_lq_format = SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT,
3381 .link_quality_alarm = SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT,
3382 .rssi_dbm_alarm = SETTING_OSD_RSSI_DBM_ALARM_DEFAULT,
3383 .rssi_dbm_max = SETTING_OSD_RSSI_DBM_MAX_DEFAULT,
3384 .rssi_dbm_min = SETTING_OSD_RSSI_DBM_MIN_DEFAULT,
3385 #endif
3386 #ifdef USE_TEMPERATURE_SENSOR
3387 .temp_label_align = SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT,
3388 #endif
3389 #ifdef USE_PITOT
3390 .airspeed_alarm_min = SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT,
3391 .airspeed_alarm_max = SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT,
3392 #endif
3394 .video_system = SETTING_OSD_VIDEO_SYSTEM_DEFAULT,
3395 .row_shiftdown = SETTING_OSD_ROW_SHIFTDOWN_DEFAULT,
3397 .ahi_reverse_roll = SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT,
3398 .ahi_max_pitch = SETTING_OSD_AHI_MAX_PITCH_DEFAULT,
3399 .crosshairs_style = SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT,
3400 .horizon_offset = SETTING_OSD_HORIZON_OFFSET_DEFAULT,
3401 .camera_uptilt = SETTING_OSD_CAMERA_UPTILT_DEFAULT,
3402 .ahi_camera_uptilt_comp = SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT,
3403 .camera_fov_h = SETTING_OSD_CAMERA_FOV_H_DEFAULT,
3404 .camera_fov_v = SETTING_OSD_CAMERA_FOV_V_DEFAULT,
3405 .hud_margin_h = SETTING_OSD_HUD_MARGIN_H_DEFAULT,
3406 .hud_margin_v = SETTING_OSD_HUD_MARGIN_V_DEFAULT,
3407 .hud_homing = SETTING_OSD_HUD_HOMING_DEFAULT,
3408 .hud_homepoint = SETTING_OSD_HUD_HOMEPOINT_DEFAULT,
3409 .hud_radar_disp = SETTING_OSD_HUD_RADAR_DISP_DEFAULT,
3410 .hud_radar_range_min = SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT,
3411 .hud_radar_range_max = SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT,
3412 .hud_radar_alt_difference_display_time = SETTING_OSD_HUD_RADAR_ALT_DIFFERENCE_DISPLAY_TIME_DEFAULT,
3413 .hud_radar_distance_display_time = SETTING_OSD_HUD_RADAR_DISTANCE_DISPLAY_TIME_DEFAULT,
3414 .hud_wp_disp = SETTING_OSD_HUD_WP_DISP_DEFAULT,
3415 .left_sidebar_scroll = SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT,
3416 .right_sidebar_scroll = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT,
3417 .sidebar_scroll_arrows = SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT,
3418 .sidebar_horizontal_offset = SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT,
3419 .left_sidebar_scroll_step = SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT,
3420 .right_sidebar_scroll_step = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT,
3421 .sidebar_height = SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT,
3422 .ahi_pitch_interval = SETTING_OSD_AHI_PITCH_INTERVAL_DEFAULT,
3423 .osd_home_position_arm_screen = SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT,
3424 .pan_servo_index = SETTING_OSD_PAN_SERVO_INDEX_DEFAULT,
3425 .pan_servo_pwm2centideg = SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT,
3426 .esc_rpm_precision = SETTING_OSD_ESC_RPM_PRECISION_DEFAULT,
3427 .mAh_used_precision = SETTING_OSD_MAH_USED_PRECISION_DEFAULT,
3428 .osd_switch_indicator0_name = SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT,
3429 .osd_switch_indicator0_channel = SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNEL_DEFAULT,
3430 .osd_switch_indicator1_name = SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT,
3431 .osd_switch_indicator1_channel = SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNEL_DEFAULT,
3432 .osd_switch_indicator2_name = SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT,
3433 .osd_switch_indicator2_channel = SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNEL_DEFAULT,
3434 .osd_switch_indicator3_name = SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT,
3435 .osd_switch_indicator3_channel = SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNEL_DEFAULT,
3436 .osd_switch_indicators_align_left = SETTING_OSD_SWITCH_INDICATORS_ALIGN_LEFT_DEFAULT,
3437 .system_msg_display_time = SETTING_OSD_SYSTEM_MSG_DISPLAY_TIME_DEFAULT,
3438 .units = SETTING_OSD_UNITS_DEFAULT,
3439 .main_voltage_decimals = SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT,
3441 #ifdef USE_WIND_ESTIMATOR
3442 .estimations_wind_compensation = SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT,
3443 #endif
3445 .coordinate_digits = SETTING_OSD_COORDINATE_DIGITS_DEFAULT,
3447 .osd_failsafe_switch_layout = SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT,
3449 .plus_code_digits = SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT,
3450 .plus_code_short = SETTING_OSD_PLUS_CODE_SHORT_DEFAULT,
3452 .ahi_width = SETTING_OSD_AHI_WIDTH_DEFAULT,
3453 .ahi_height = SETTING_OSD_AHI_HEIGHT_DEFAULT,
3454 .ahi_vertical_offset = SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT,
3455 .ahi_bordered = SETTING_OSD_AHI_BORDERED_DEFAULT,
3456 .ahi_style = SETTING_OSD_AHI_STYLE_DEFAULT,
3458 .force_grid = SETTING_OSD_FORCE_GRID_DEFAULT,
3460 .stats_energy_unit = SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT,
3461 .stats_min_voltage_unit = SETTING_OSD_STATS_MIN_VOLTAGE_UNIT_DEFAULT,
3462 .stats_page_auto_swap_time = SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT
3465 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t *osdLayoutsConfig)
3467 osdLayoutsConfig->item_pos[0][OSD_ALTITUDE] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG;
3468 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_VOLTAGE] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG;
3469 osdLayoutsConfig->item_pos[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE] = OSD_POS(12, 1);
3471 osdLayoutsConfig->item_pos[0][OSD_RSSI_VALUE] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG;
3472 //line 2
3473 osdLayoutsConfig->item_pos[0][OSD_HOME_DIST] = OSD_POS(1, 1);
3474 osdLayoutsConfig->item_pos[0][OSD_TRIP_DIST] = OSD_POS(1, 2);
3475 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_CELL_VOLTAGE] = OSD_POS(12, 1);
3476 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE] = OSD_POS(12, 1);
3477 osdLayoutsConfig->item_pos[0][OSD_GPS_SPEED] = OSD_POS(23, 1);
3478 osdLayoutsConfig->item_pos[0][OSD_3D_SPEED] = OSD_POS(23, 1);
3479 osdLayoutsConfig->item_pos[0][OSD_GLIDESLOPE] = OSD_POS(23, 2);
3481 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG;
3482 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS_AUTO_THR] = OSD_POS(6, 2);
3483 osdLayoutsConfig->item_pos[0][OSD_HEADING] = OSD_POS(12, 2);
3484 osdLayoutsConfig->item_pos[0][OSD_GROUND_COURSE] = OSD_POS(12, 3);
3485 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ERROR] = OSD_POS(12, 2);
3486 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ADJUSTMENT] = OSD_POS(12, 2);
3487 osdLayoutsConfig->item_pos[0][OSD_CROSS_TRACK_ERROR] = OSD_POS(12, 3);
3488 osdLayoutsConfig->item_pos[0][OSD_HEADING_GRAPH] = OSD_POS(18, 2);
3489 osdLayoutsConfig->item_pos[0][OSD_CURRENT_DRAW] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG;
3490 osdLayoutsConfig->item_pos[0][OSD_MAH_DRAWN] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG;
3491 osdLayoutsConfig->item_pos[0][OSD_WH_DRAWN] = OSD_POS(1, 5);
3492 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_CAPACITY] = OSD_POS(1, 6);
3493 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_PERCENT] = OSD_POS(1, 7);
3494 osdLayoutsConfig->item_pos[0][OSD_POWER_SUPPLY_IMPEDANCE] = OSD_POS(1, 8);
3496 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_MAH_PER_KM] = OSD_POS(1, 5);
3497 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_WH_PER_KM] = OSD_POS(1, 5);
3499 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_ROLL] = OSD_POS(1, 7);
3500 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_PITCH] = OSD_POS(1, 8);
3502 // avoid OSD_VARIO under OSD_CROSSHAIRS
3503 osdLayoutsConfig->item_pos[0][OSD_VARIO] = OSD_POS(23, 5);
3504 // OSD_VARIO_NUM at the right of OSD_VARIO
3505 osdLayoutsConfig->item_pos[0][OSD_VARIO_NUM] = OSD_POS(24, 7);
3506 osdLayoutsConfig->item_pos[0][OSD_HOME_DIR] = OSD_POS(14, 11);
3507 osdLayoutsConfig->item_pos[0][OSD_ARTIFICIAL_HORIZON] = OSD_POS(8, 6);
3508 osdLayoutsConfig->item_pos[0][OSD_HORIZON_SIDEBARS] = OSD_POS(8, 6);
3510 osdLayoutsConfig->item_pos[0][OSD_CRAFT_NAME] = OSD_POS(20, 2);
3511 osdLayoutsConfig->item_pos[0][OSD_VTX_CHANNEL] = OSD_POS(8, 6);
3513 #ifdef USE_SERIALRX_CRSF
3514 osdLayoutsConfig->item_pos[0][OSD_CRSF_RSSI_DBM] = OSD_POS(23, 12);
3515 osdLayoutsConfig->item_pos[0][OSD_CRSF_LQ] = OSD_POS(23, 11);
3516 osdLayoutsConfig->item_pos[0][OSD_CRSF_SNR_DB] = OSD_POS(24, 9);
3517 osdLayoutsConfig->item_pos[0][OSD_CRSF_TX_POWER] = OSD_POS(24, 10);
3518 #endif
3520 osdLayoutsConfig->item_pos[0][OSD_ONTIME] = OSD_POS(23, 8);
3521 osdLayoutsConfig->item_pos[0][OSD_FLYTIME] = OSD_POS(23, 9);
3522 osdLayoutsConfig->item_pos[0][OSD_ONTIME_FLYTIME] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG;
3523 osdLayoutsConfig->item_pos[0][OSD_RTC_TIME] = OSD_POS(23, 12);
3524 osdLayoutsConfig->item_pos[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH] = OSD_POS(23, 7);
3525 osdLayoutsConfig->item_pos[0][OSD_REMAINING_DISTANCE_BEFORE_RTH] = OSD_POS(23, 6);
3527 osdLayoutsConfig->item_pos[0][OSD_MISSION] = OSD_POS(0, 10);
3528 osdLayoutsConfig->item_pos[0][OSD_GPS_SATS] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG;
3529 osdLayoutsConfig->item_pos[0][OSD_GPS_HDOP] = OSD_POS(0, 10);
3531 osdLayoutsConfig->item_pos[0][OSD_GPS_LAT] = OSD_POS(0, 12);
3532 // Put this on top of the latitude, since it's very unlikely
3533 // that users will want to use both at the same time.
3534 osdLayoutsConfig->item_pos[0][OSD_PLUS_CODE] = OSD_POS(0, 12);
3535 osdLayoutsConfig->item_pos[0][OSD_FLYMODE] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG;
3536 osdLayoutsConfig->item_pos[0][OSD_GPS_LON] = OSD_POS(18, 12);
3538 osdLayoutsConfig->item_pos[0][OSD_AZIMUTH] = OSD_POS(2, 12);
3540 osdLayoutsConfig->item_pos[0][OSD_ROLL_PIDS] = OSD_POS(2, 10);
3541 osdLayoutsConfig->item_pos[0][OSD_PITCH_PIDS] = OSD_POS(2, 11);
3542 osdLayoutsConfig->item_pos[0][OSD_YAW_PIDS] = OSD_POS(2, 12);
3543 osdLayoutsConfig->item_pos[0][OSD_LEVEL_PIDS] = OSD_POS(2, 12);
3544 osdLayoutsConfig->item_pos[0][OSD_POS_XY_PIDS] = OSD_POS(2, 12);
3545 osdLayoutsConfig->item_pos[0][OSD_POS_Z_PIDS] = OSD_POS(2, 12);
3546 osdLayoutsConfig->item_pos[0][OSD_VEL_XY_PIDS] = OSD_POS(2, 12);
3547 osdLayoutsConfig->item_pos[0][OSD_VEL_Z_PIDS] = OSD_POS(2, 12);
3548 osdLayoutsConfig->item_pos[0][OSD_HEADING_P] = OSD_POS(2, 12);
3549 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_ROLL] = OSD_POS(2, 10);
3550 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_PITCH] = OSD_POS(2, 11);
3551 osdLayoutsConfig->item_pos[0][OSD_RC_EXPO] = OSD_POS(2, 12);
3552 osdLayoutsConfig->item_pos[0][OSD_RC_YAW_EXPO] = OSD_POS(2, 12);
3553 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_EXPO] = OSD_POS(2, 12);
3554 osdLayoutsConfig->item_pos[0][OSD_PITCH_RATE] = OSD_POS(2, 12);
3555 osdLayoutsConfig->item_pos[0][OSD_ROLL_RATE] = OSD_POS(2, 12);
3556 osdLayoutsConfig->item_pos[0][OSD_YAW_RATE] = OSD_POS(2, 12);
3557 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_EXPO] = OSD_POS(2, 12);
3558 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_YAW_EXPO] = OSD_POS(2, 12);
3559 osdLayoutsConfig->item_pos[0][OSD_MANUAL_PITCH_RATE] = OSD_POS(2, 12);
3560 osdLayoutsConfig->item_pos[0][OSD_MANUAL_ROLL_RATE] = OSD_POS(2, 12);
3561 osdLayoutsConfig->item_pos[0][OSD_MANUAL_YAW_RATE] = OSD_POS(2, 12);
3562 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_CRUISE_THR] = OSD_POS(2, 12);
3563 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_PITCH2THR] = OSD_POS(2, 12);
3564 osdLayoutsConfig->item_pos[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE] = OSD_POS(2, 12);
3565 osdLayoutsConfig->item_pos[0][OSD_FW_ALT_PID_OUTPUTS] = OSD_POS(2, 12);
3566 osdLayoutsConfig->item_pos[0][OSD_FW_POS_PID_OUTPUTS] = OSD_POS(2, 12);
3567 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_X_PID_OUTPUTS] = OSD_POS(2, 12);
3568 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Y_PID_OUTPUTS] = OSD_POS(2, 12);
3569 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Z_PID_OUTPUTS] = OSD_POS(2, 12);
3570 osdLayoutsConfig->item_pos[0][OSD_MC_POS_XYZ_P_OUTPUTS] = OSD_POS(2, 12);
3572 osdLayoutsConfig->item_pos[0][OSD_POWER] = OSD_POS(15, 1);
3574 osdLayoutsConfig->item_pos[0][OSD_IMU_TEMPERATURE] = OSD_POS(19, 2);
3575 osdLayoutsConfig->item_pos[0][OSD_BARO_TEMPERATURE] = OSD_POS(19, 3);
3576 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_0_TEMPERATURE] = OSD_POS(19, 4);
3577 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_1_TEMPERATURE] = OSD_POS(19, 5);
3578 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_2_TEMPERATURE] = OSD_POS(19, 6);
3579 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_3_TEMPERATURE] = OSD_POS(19, 7);
3580 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_4_TEMPERATURE] = OSD_POS(19, 8);
3581 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_5_TEMPERATURE] = OSD_POS(19, 9);
3582 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_6_TEMPERATURE] = OSD_POS(19, 10);
3583 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_7_TEMPERATURE] = OSD_POS(19, 11);
3585 osdLayoutsConfig->item_pos[0][OSD_AIR_SPEED] = OSD_POS(3, 5);
3586 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_HORIZONTAL] = OSD_POS(3, 6);
3587 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_VERTICAL] = OSD_POS(3, 7);
3589 osdLayoutsConfig->item_pos[0][OSD_GFORCE] = OSD_POS(12, 4);
3590 osdLayoutsConfig->item_pos[0][OSD_GFORCE_X] = OSD_POS(12, 5);
3591 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Y] = OSD_POS(12, 6);
3592 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Z] = OSD_POS(12, 7);
3594 osdLayoutsConfig->item_pos[0][OSD_VTX_POWER] = OSD_POS(3, 5);
3596 osdLayoutsConfig->item_pos[0][OSD_GVAR_0] = OSD_POS(1, 1);
3597 osdLayoutsConfig->item_pos[0][OSD_GVAR_1] = OSD_POS(1, 2);
3598 osdLayoutsConfig->item_pos[0][OSD_GVAR_2] = OSD_POS(1, 3);
3599 osdLayoutsConfig->item_pos[0][OSD_GVAR_3] = OSD_POS(1, 4);
3601 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_0] = OSD_POS(2, 7);
3602 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_1] = OSD_POS(2, 8);
3603 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_2] = OSD_POS(2, 9);
3604 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_3] = OSD_POS(2, 10);
3606 #if defined(USE_ESC_SENSOR)
3607 osdLayoutsConfig->item_pos[0][OSD_ESC_RPM] = OSD_POS(1, 2);
3608 osdLayoutsConfig->item_pos[0][OSD_ESC_TEMPERATURE] = OSD_POS(1, 3);
3609 #endif
3611 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3612 osdLayoutsConfig->item_pos[0][OSD_RC_SOURCE] = OSD_POS(3, 4);
3613 #endif
3615 #ifdef USE_POWER_LIMITS
3616 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_REMAINING_BURST_TIME] = OSD_POS(3, 4);
3617 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT] = OSD_POS(3, 5);
3618 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT] = OSD_POS(3, 6);
3619 #endif
3621 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
3622 osdLayoutsConfig->item_pos[0][OSD_MESSAGES] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG;
3624 for (unsigned ii = 1; ii < OSD_LAYOUT_COUNT; ii++) {
3625 for (unsigned jj = 0; jj < ARRAYLEN(osdLayoutsConfig->item_pos[0]); jj++) {
3626 osdLayoutsConfig->item_pos[ii][jj] = osdLayoutsConfig->item_pos[0][jj] & ~OSD_VISIBLE_FLAG;
3631 static void osdSetNextRefreshIn(uint32_t timeMs) {
3632 resumeRefreshAt = micros() + timeMs * 1000;
3633 refreshWaitForResumeCmdRelease = true;
3636 static void osdCompleteAsyncInitialization(void)
3638 if (!displayIsReady(osdDisplayPort)) {
3639 // Update the display.
3640 // XXX: Rename displayDrawScreen() and associated functions
3641 // to displayUpdate()
3642 displayDrawScreen(osdDisplayPort);
3643 return;
3646 osdDisplayIsReady = true;
3648 #if defined(USE_CANVAS)
3649 if (osdConfig()->force_grid) {
3650 osdDisplayHasCanvas = false;
3651 } else {
3652 osdDisplayHasCanvas = displayGetCanvas(&osdCanvas, osdDisplayPort);
3654 #endif
3656 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3657 displayClearScreen(osdDisplayPort);
3659 uint8_t y = 1;
3660 displayFontMetadata_t metadata;
3661 bool fontHasMetadata = displayGetFontMetadata(&metadata, osdDisplayPort);
3662 LOG_DEBUG(OSD, "Font metadata version %s: %u (%u chars)",
3663 fontHasMetadata ? "Y" : "N", metadata.version, metadata.charCount);
3665 if (fontHasMetadata && metadata.charCount > 256) {
3666 hasExtendedFont = true;
3667 unsigned logo_c = SYM_LOGO_START;
3668 unsigned logo_x = OSD_CENTER_LEN(SYM_LOGO_WIDTH);
3669 for (unsigned ii = 0; ii < SYM_LOGO_HEIGHT; ii++) {
3670 for (unsigned jj = 0; jj < SYM_LOGO_WIDTH; jj++) {
3671 displayWriteChar(osdDisplayPort, logo_x + jj, y, logo_c++);
3673 y++;
3675 y++;
3676 } else if (!fontHasMetadata) {
3677 const char *m = "INVALID FONT";
3678 displayWrite(osdDisplayPort, OSD_CENTER_S(m), 3, m);
3679 y = 4;
3682 if (fontHasMetadata && metadata.version < OSD_MIN_FONT_VERSION) {
3683 const char *m = "INVALID FONT VERSION";
3684 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
3687 char string_buffer[30];
3688 tfp_sprintf(string_buffer, "INAV VERSION: %s", FC_VERSION_STRING);
3689 uint8_t xPos = osdDisplayIsHD() ? 15 : 5;
3690 displayWrite(osdDisplayPort, xPos, y++, string_buffer);
3691 #ifdef USE_CMS
3692 displayWrite(osdDisplayPort, xPos+2, y++, CMS_STARTUP_HELP_TEXT1);
3693 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT2);
3694 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT3);
3695 #endif
3696 #ifdef USE_STATS
3699 uint8_t statNameX = osdDisplayIsHD() ? 14 : 4;
3700 uint8_t statValueX = osdDisplayIsHD() ? 34 : 24;
3702 if (statsConfig()->stats_enabled) {
3703 displayWrite(osdDisplayPort, statNameX, ++y, "ODOMETER:");
3704 switch (osdConfig()->units) {
3705 case OSD_UNIT_UK:
3706 FALLTHROUGH;
3707 case OSD_UNIT_IMPERIAL:
3708 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_MILE));
3709 string_buffer[5] = SYM_MI;
3710 break;
3711 default:
3712 case OSD_UNIT_GA:
3713 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE));
3714 string_buffer[5] = SYM_NM;
3715 break;
3716 case OSD_UNIT_METRIC_MPH:
3717 FALLTHROUGH;
3718 case OSD_UNIT_METRIC:
3719 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER));
3720 string_buffer[5] = SYM_KM;
3721 break;
3723 string_buffer[6] = '\0';
3724 displayWrite(osdDisplayPort, statValueX-5, y, string_buffer);
3726 displayWrite(osdDisplayPort, statNameX, ++y, "TOTAL TIME:");
3727 uint32_t tot_mins = statsConfig()->stats_total_time / 60;
3728 tfp_sprintf(string_buffer, "%2d:%02dHM", (int)(tot_mins / 60), (int)(tot_mins % 60));
3729 displayWrite(osdDisplayPort, statValueX-5, y, string_buffer);
3731 #ifdef USE_ADC
3732 if (feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER)) {
3733 displayWrite(osdDisplayPort, statNameX, ++y, "TOTAL ENERGY:");
3734 osdFormatCentiNumber(string_buffer, statsConfig()->stats_total_energy / 10, 0, 2, 0, 4);
3735 strcat(string_buffer, "\xAB"); // SYM_WH
3736 displayWrite(osdDisplayPort, statValueX-4, y, string_buffer);
3738 displayWrite(osdDisplayPort, statNameX, ++y, "AVG EFFICIENCY:");
3739 if (statsConfig()->stats_total_dist) {
3740 uint32_t avg_efficiency = statsConfig()->stats_total_energy / (statsConfig()->stats_total_dist / METERS_PER_KILOMETER); // mWh/km
3741 switch (osdConfig()->units) {
3742 case OSD_UNIT_UK:
3743 FALLTHROUGH;
3744 case OSD_UNIT_IMPERIAL:
3745 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3746 string_buffer[3] = SYM_WH_MI;
3747 break;
3748 case OSD_UNIT_GA:
3749 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3750 string_buffer[3] = SYM_WH_NM;
3751 break;
3752 default:
3753 case OSD_UNIT_METRIC_MPH:
3754 FALLTHROUGH;
3755 case OSD_UNIT_METRIC:
3756 osdFormatCentiNumber(string_buffer, avg_efficiency / 10000 * METERS_PER_MILE, 0, 2, 0, 3);
3757 string_buffer[3] = SYM_WH_KM;
3758 break;
3760 } else {
3761 string_buffer[0] = string_buffer[1] = string_buffer[2] = '-';
3763 string_buffer[4] = '\0';
3764 displayWrite(osdDisplayPort, statValueX-3, y, string_buffer);
3766 #endif // USE_ADC
3768 #endif
3770 displayCommitTransaction(osdDisplayPort);
3771 displayResync(osdDisplayPort);
3772 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME);
3775 void osdInit(displayPort_t *osdDisplayPortToUse)
3777 if (!osdDisplayPortToUse)
3778 return;
3780 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(63,63));
3782 osdDisplayPort = osdDisplayPortToUse;
3784 #ifdef USE_CMS
3785 cmsDisplayPortRegister(osdDisplayPort);
3786 #endif
3788 armState = ARMING_FLAG(ARMED);
3789 osdCompleteAsyncInitialization();
3792 static void osdResetStats(void)
3794 stats.max_current = 0;
3795 stats.max_power = 0;
3796 stats.max_speed = 0;
3797 stats.max_3D_speed = 0;
3798 stats.max_air_speed = 0;
3799 stats.min_voltage = 5000;
3800 stats.min_rssi = 99;
3801 stats.min_lq = 300;
3802 stats.min_rssi_dbm = 0;
3803 stats.max_altitude = 0;
3806 static void osdUpdateStats(void)
3808 int32_t value;
3810 if (feature(FEATURE_GPS)) {
3811 value = osdGet3DSpeed();
3812 const float airspeed_estimate = getAirspeedEstimate();
3814 if (stats.max_3D_speed < value)
3815 stats.max_3D_speed = value;
3817 if (stats.max_speed < gpsSol.groundSpeed)
3818 stats.max_speed = gpsSol.groundSpeed;
3820 if (stats.max_air_speed < airspeed_estimate)
3821 stats.max_air_speed = airspeed_estimate;
3823 if (stats.max_distance < GPS_distanceToHome)
3824 stats.max_distance = GPS_distanceToHome;
3827 value = getBatteryVoltage();
3828 if (stats.min_voltage > value)
3829 stats.min_voltage = value;
3831 value = abs(getAmperage());
3832 if (stats.max_current < value)
3833 stats.max_current = value;
3835 value = labs(getPower());
3836 if (stats.max_power < value)
3837 stats.max_power = value;
3839 value = osdConvertRSSI();
3840 if (stats.min_rssi > value)
3841 stats.min_rssi = value;
3843 value = osdGetCrsfLQ();
3844 if (stats.min_lq > value)
3845 stats.min_lq = value;
3847 if (!failsafeIsReceivingRxData())
3848 stats.min_lq = 0;
3850 value = osdGetCrsfdBm();
3851 if (stats.min_rssi_dbm > value)
3852 stats.min_rssi_dbm = value;
3854 stats.max_altitude = MAX(stats.max_altitude, osdGetAltitude());
3857 static void osdShowStatsPage1(void)
3859 const char * disarmReasonStr[DISARM_REASON_COUNT] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "KILLSW", "FAILSAFE", "NAV SYS", "LANDING"};
3860 uint8_t top = 1; /* first fully visible line */
3861 const uint8_t statNameX = osdDisplayIsHD() ? 11 : 1;
3862 const uint8_t statValuesX = osdDisplayIsHD() ? 30 : 20;
3863 char buff[10];
3864 statsPagesCheck = 1;
3866 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3867 displayClearScreen(osdDisplayPort);
3869 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- 1/2 ->");
3871 if (feature(FEATURE_GPS)) {
3872 displayWrite(osdDisplayPort, statNameX, top, "MAX SPEED :");
3873 osdFormatVelocityStr(buff, stats.max_3D_speed, true, false);
3874 osdLeftAlignString(buff);
3875 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3877 displayWrite(osdDisplayPort, statNameX, top, "AVG SPEED :");
3878 osdGenerateAverageVelocityStr(buff);
3879 osdLeftAlignString(buff);
3880 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3882 displayWrite(osdDisplayPort, statNameX, top, "MAX DISTANCE :");
3883 osdFormatDistanceStr(buff, stats.max_distance*100);
3884 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3886 displayWrite(osdDisplayPort, statNameX, top, "TRAVELED DISTANCE:");
3887 osdFormatDistanceStr(buff, getTotalTravelDistance());
3888 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3891 displayWrite(osdDisplayPort, statNameX, top, "MAX ALTITUDE :");
3892 osdFormatAltitudeStr(buff, stats.max_altitude);
3893 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3895 switch (rxConfig()->serialrx_provider) {
3896 case SERIALRX_CRSF:
3897 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI % :");
3898 itoa(stats.min_rssi, buff, 10);
3899 strcat(buff, "%");
3900 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3902 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI DBM :");
3903 itoa(stats.min_rssi_dbm, buff, 10);
3904 tfp_sprintf(buff, "%s%c", buff, SYM_DBM);
3905 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3907 displayWrite(osdDisplayPort, statNameX, top, "MIN LQ :");
3908 itoa(stats.min_lq, buff, 10);
3909 strcat(buff, "%");
3910 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3911 break;
3912 default:
3913 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI :");
3914 itoa(stats.min_rssi, buff, 10);
3915 strcat(buff, "%");
3916 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3919 displayWrite(osdDisplayPort, statNameX, top, "FLY TIME :");
3920 uint16_t flySeconds = getFlightTime();
3921 uint16_t flyMinutes = flySeconds / 60;
3922 flySeconds %= 60;
3923 uint16_t flyHours = flyMinutes / 60;
3924 flyMinutes %= 60;
3925 tfp_sprintf(buff, "%02u:%02u:%02u", flyHours, flyMinutes, flySeconds);
3926 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3928 displayWrite(osdDisplayPort, statNameX, top, "DISARMED BY :");
3929 displayWrite(osdDisplayPort, statValuesX, top++, disarmReasonStr[getDisarmReason()]);
3930 displayCommitTransaction(osdDisplayPort);
3933 static void osdShowStatsPage2(void)
3935 uint8_t top = 1; /* first fully visible line */
3936 const uint8_t statNameX = osdDisplayIsHD() ? 11 : 1;
3937 const uint8_t statValuesX = osdDisplayIsHD() ? 30 : 20;
3938 char buff[10];
3939 statsPagesCheck = 1;
3941 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3942 displayClearScreen(osdDisplayPort);
3944 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- <- 2/2");
3946 if (osdConfig()->stats_min_voltage_unit == OSD_STATS_MIN_VOLTAGE_UNIT_BATTERY) {
3947 displayWrite(osdDisplayPort, statNameX, top, "MIN BATTERY VOLT :");
3948 osdFormatCentiNumber(buff, stats.min_voltage, 0, osdConfig()->main_voltage_decimals, 0, osdConfig()->main_voltage_decimals + 2);
3949 } else {
3950 displayWrite(osdDisplayPort, statNameX, top, "MIN CELL VOLTAGE :");
3951 osdFormatCentiNumber(buff, stats.min_voltage/getBatteryCellCount(), 0, 2, 0, 3);
3953 tfp_sprintf(buff, "%s%c", buff, SYM_VOLT);
3954 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3956 if (feature(FEATURE_CURRENT_METER)) {
3957 displayWrite(osdDisplayPort, statNameX, top, "MAX CURRENT :");
3958 osdFormatCentiNumber(buff, stats.max_current, 0, 2, 0, 3);
3959 tfp_sprintf(buff, "%s%c", buff, SYM_AMP);
3960 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3962 displayWrite(osdDisplayPort, statNameX, top, "MAX POWER :");
3963 bool kiloWatt = osdFormatCentiNumber(buff, stats.max_power, 1000, 2, 2, 3);
3964 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3965 buff[4] = '\0';
3966 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3968 displayWrite(osdDisplayPort, statNameX, top, "USED CAPACITY :");
3969 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3970 tfp_sprintf(buff, "%d%c", (int)getMAhDrawn(), SYM_MAH);
3971 } else {
3972 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
3973 tfp_sprintf(buff, "%s%c", buff, SYM_WH);
3975 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3977 int32_t totalDistance = getTotalTravelDistance();
3978 bool moreThanAh = false;
3979 bool efficiencyValid = totalDistance >= 10000;
3980 if (feature(FEATURE_GPS)) {
3981 displayWrite(osdDisplayPort, statNameX, top, "AVG EFFICIENCY :");
3982 switch (osdConfig()->units) {
3983 case OSD_UNIT_UK:
3984 FALLTHROUGH;
3985 case OSD_UNIT_IMPERIAL:
3986 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3987 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, 3);
3988 if (!moreThanAh) {
3989 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
3990 } else {
3991 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
3993 if (!efficiencyValid) {
3994 buff[0] = buff[1] = buff[2] = '-';
3995 buff[3] = SYM_MAH_MI_0;
3996 buff[4] = SYM_MAH_MI_1;
3997 buff[5] = '\0';
3999 } else {
4000 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, 3);
4001 tfp_sprintf(buff, "%s%c", buff, SYM_WH_MI);
4002 if (!efficiencyValid) {
4003 buff[0] = buff[1] = buff[2] = '-';
4006 break;
4007 case OSD_UNIT_GA:
4008 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4009 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, 3);
4010 if (!moreThanAh) {
4011 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
4012 } else {
4013 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
4015 if (!efficiencyValid) {
4016 buff[0] = buff[1] = buff[2] = '-';
4017 buff[3] = SYM_MAH_NM_0;
4018 buff[4] = SYM_MAH_NM_1;
4019 buff[5] = '\0';
4021 } else {
4022 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, 3);
4023 tfp_sprintf(buff, "%s%c", buff, SYM_WH_NM);
4024 if (!efficiencyValid) {
4025 buff[0] = buff[1] = buff[2] = '-';
4028 break;
4029 case OSD_UNIT_METRIC_MPH:
4030 FALLTHROUGH;
4031 case OSD_UNIT_METRIC:
4032 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4033 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000000.0f / totalDistance), 1000, 0, 2, 3);
4034 if (!moreThanAh) {
4035 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
4036 } else {
4037 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
4039 if (!efficiencyValid) {
4040 buff[0] = buff[1] = buff[2] = '-';
4041 buff[3] = SYM_MAH_KM_0;
4042 buff[4] = SYM_MAH_KM_1;
4043 buff[5] = '\0';
4045 } else {
4046 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10000.0f / totalDistance), 0, 2, 0, 3);
4047 tfp_sprintf(buff, "%s%c", buff, SYM_WH_KM);
4048 if (!efficiencyValid) {
4049 buff[0] = buff[1] = buff[2] = '-';
4052 break;
4054 osdLeftAlignString(buff);
4055 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4059 const float max_gforce = accGetMeasuredMaxG();
4060 displayWrite(osdDisplayPort, statNameX, top, "MAX G-FORCE :");
4061 osdFormatCentiNumber(buff, max_gforce * 100, 0, 2, 0, 3);
4062 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4064 const acc_extremes_t *acc_extremes = accGetMeasuredExtremes();
4065 displayWrite(osdDisplayPort, statNameX, top, "MIN/MAX Z G-FORCE:");
4066 osdFormatCentiNumber(buff, acc_extremes[Z].min * 100, 0, 2, 0, 4);
4067 strcat(buff,"/");
4068 displayWrite(osdDisplayPort, statValuesX - 1, top, buff);
4069 osdFormatCentiNumber(buff, acc_extremes[Z].max * 100, 0, 2, 0, 3);
4070 displayWrite(osdDisplayPort, statValuesX + 4, top++, buff);
4071 displayCommitTransaction(osdDisplayPort);
4074 // called when motors armed
4075 static void osdShowArmed(void)
4077 dateTime_t dt;
4078 char buf[MAX(32, FORMATTED_DATE_TIME_BUFSIZE)];
4079 char craftNameBuf[MAX_NAME_LENGTH];
4080 char versionBuf[30];
4081 char *date;
4082 char *time;
4083 // We need 12 visible rows, start row never < first fully visible row 1
4084 uint8_t y = osdDisplayPort->rows > 13 ? (osdDisplayPort->rows - 12) / 2 : 1;
4086 displayClearScreen(osdDisplayPort);
4087 strcpy(buf, "ARMED");
4088 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4089 y += 2;
4091 if (strlen(systemConfig()->name) > 0) {
4092 osdFormatCraftName(craftNameBuf);
4093 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(systemConfig() -> name)) / 2, y, craftNameBuf );
4094 y += 1;
4096 if (posControl.waypointListValid && posControl.waypointCount > 0) {
4097 #ifdef USE_MULTI_MISSION
4098 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
4099 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4100 #else
4101 strcpy(buf, "*MISSION LOADED*");
4102 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4103 #endif
4105 y += 1;
4107 #if defined(USE_GPS)
4108 if (feature(FEATURE_GPS)) {
4109 if (STATE(GPS_FIX_HOME)) {
4110 if (osdConfig()->osd_home_position_arm_screen){
4111 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
4112 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4113 osdFormatCoordinate(buf, SYM_LON, GPS_home.lon);
4114 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 1, buf);
4115 int digits = osdConfig()->plus_code_digits;
4116 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
4117 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 2, buf);
4119 y += 4;
4120 #if defined (USE_SAFE_HOME)
4121 if (safehome_distance) { // safehome found during arming
4122 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
4123 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
4124 } else {
4125 char buf2[12]; // format the distance first
4126 osdFormatDistanceStr(buf2, safehome_distance);
4127 tfp_sprintf(buf, "%c - %s -> SAFEHOME %u", SYM_HOME, buf2, safehome_index);
4129 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
4130 // write this message above the ARMED message to make it obvious
4131 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y - 8, buf, elemAttr);
4133 #endif
4134 } else {
4135 strcpy(buf, "!NO HOME POSITION!");
4136 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4137 y += 1;
4140 #endif
4142 if (rtcGetDateTime(&dt)) {
4143 dateTimeFormatLocal(buf, &dt);
4144 dateTimeSplitFormatted(buf, &date, &time);
4146 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(date)) / 2, y, date);
4147 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(time)) / 2, y + 1, time);
4148 y += 3;
4151 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
4152 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, y, versionBuf);
4155 static void osdFilterData(timeUs_t currentTimeUs) {
4156 static timeUs_t lastRefresh = 0;
4157 float refresh_dT = US2S(cmpTimeUs(currentTimeUs, lastRefresh));
4159 GForce = fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF)) / GRAVITY_MSS;
4160 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) GForceAxis[axis] = imuMeasuredAccelBF.v[axis] / GRAVITY_MSS;
4162 if (lastRefresh) {
4163 GForce = pt1FilterApply3(&GForceFilter, GForce, refresh_dT);
4164 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) pt1FilterApply3(GForceFilterAxis + axis, GForceAxis[axis], refresh_dT);
4165 } else {
4166 pt1FilterInitRC(&GForceFilter, GFORCE_FILTER_TC, 0);
4167 pt1FilterReset(&GForceFilter, GForce);
4169 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) {
4170 pt1FilterInitRC(GForceFilterAxis + axis, GFORCE_FILTER_TC, 0);
4171 pt1FilterReset(GForceFilterAxis + axis, GForceAxis[axis]);
4175 lastRefresh = currentTimeUs;
4178 static void osdRefresh(timeUs_t currentTimeUs)
4180 osdFilterData(currentTimeUs);
4182 #ifdef USE_CMS
4183 if (IS_RC_MODE_ACTIVE(BOXOSD) && (!cmsInMenu) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4184 #else
4185 if (IS_RC_MODE_ACTIVE(BOXOSD) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4186 #endif
4187 displayClearScreen(osdDisplayPort);
4188 armState = ARMING_FLAG(ARMED);
4189 return;
4192 // detect arm/disarm
4193 static uint8_t statsPageAutoSwapCntl = 2;
4194 if (armState != ARMING_FLAG(ARMED)) {
4195 if (ARMING_FLAG(ARMED)) {
4196 osdResetStats();
4197 statsPageAutoSwapCntl = 2;
4198 osdShowArmed(); // reset statistic etc
4199 uint32_t delay = ARMED_SCREEN_DISPLAY_TIME;
4200 statsPagesCheck = 0;
4201 #if defined(USE_SAFE_HOME)
4202 if (safehome_distance)
4203 delay *= 3;
4204 #endif
4205 osdSetNextRefreshIn(delay);
4206 } else {
4207 osdShowStatsPage1(); // show first page of statistics
4208 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME);
4209 statsPageAutoSwapCntl = osdConfig()->stats_page_auto_swap_time > 0 ? 0 : 2; // disable swapping pages when time = 0
4212 armState = ARMING_FLAG(ARMED);
4215 if (resumeRefreshAt) {
4216 // If we already reached he time for the next refresh,
4217 // or THR is high or PITCH is high, resume refreshing.
4218 // Clear the screen first to erase other elements which
4219 // might have been drawn while the OSD wasn't refreshing.
4221 // auto swap stats pages when first shown
4222 // auto swap cancelled using roll stick
4223 if (statsPageAutoSwapCntl != 2) {
4224 if (STATS_PAGE1 || STATS_PAGE2) {
4225 statsPageAutoSwapCntl = 2;
4226 } else {
4227 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time * 1000), 2)) {
4228 if (statsPageAutoSwapCntl == 0) {
4229 osdShowStatsPage1();
4230 statsPageAutoSwapCntl = 1;
4232 } else {
4233 if (statsPageAutoSwapCntl == 1) {
4234 osdShowStatsPage2();
4235 statsPageAutoSwapCntl = 0;
4241 if (!DELAYED_REFRESH_RESUME_COMMAND)
4242 refreshWaitForResumeCmdRelease = false;
4244 if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && DELAYED_REFRESH_RESUME_COMMAND)) {
4245 displayClearScreen(osdDisplayPort);
4246 resumeRefreshAt = 0;
4247 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE1)) {
4248 if (statsPagesCheck == 1) {
4249 osdShowStatsPage1();
4251 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE2)) {
4252 if (statsPagesCheck == 1) {
4253 osdShowStatsPage2();
4255 } else {
4256 displayHeartbeat(osdDisplayPort);
4258 return;
4261 #ifdef USE_CMS
4262 if (!displayIsGrabbed(osdDisplayPort)) {
4263 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4264 if (fullRedraw) {
4265 displayClearScreen(osdDisplayPort);
4266 fullRedraw = false;
4268 osdDrawNextElement();
4269 displayHeartbeat(osdDisplayPort);
4270 displayCommitTransaction(osdDisplayPort);
4271 #ifdef OSD_CALLS_CMS
4272 } else {
4273 cmsUpdate(currentTimeUs);
4274 #endif
4276 #endif
4280 * Called periodically by the scheduler
4282 void osdUpdate(timeUs_t currentTimeUs)
4284 static uint32_t counter = 0;
4286 // don't touch buffers if DMA transaction is in progress
4287 if (displayIsTransferInProgress(osdDisplayPort)) {
4288 return;
4291 if (!osdDisplayIsReady) {
4292 osdCompleteAsyncInitialization();
4293 return;
4296 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
4297 // Check if the layout has changed. Higher numbered
4298 // boxes take priority.
4299 unsigned activeLayout;
4300 if (layoutOverride >= 0) {
4301 activeLayout = layoutOverride;
4302 // Check for timed override, it will go into effect on
4303 // the next OSD iteration
4304 if (layoutOverrideUntil > 0 && millis() > layoutOverrideUntil) {
4305 layoutOverrideUntil = 0;
4306 layoutOverride = -1;
4308 } else if (osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE)) {
4309 activeLayout = 0;
4310 } else {
4311 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
4312 if (IS_RC_MODE_ACTIVE(BOXOSDALT3))
4313 activeLayout = 3;
4314 else
4315 #endif
4316 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
4317 if (IS_RC_MODE_ACTIVE(BOXOSDALT2))
4318 activeLayout = 2;
4319 else
4320 #endif
4321 if (IS_RC_MODE_ACTIVE(BOXOSDALT1))
4322 activeLayout = 1;
4323 else
4324 #ifdef USE_PROGRAMMING_FRAMEWORK
4325 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT))
4326 activeLayout = constrain(logicConditionValuesByType[LOGIC_CONDITION_SET_OSD_LAYOUT], 0, OSD_ALTERNATE_LAYOUT_COUNT);
4327 else
4328 #endif
4329 activeLayout = 0;
4331 if (currentLayout != activeLayout) {
4332 currentLayout = activeLayout;
4333 osdStartFullRedraw();
4335 #endif
4337 #define DRAW_FREQ_DENOM 4
4338 #define STATS_FREQ_DENOM 50
4339 counter++;
4341 if ((counter % STATS_FREQ_DENOM) == 0) {
4342 osdUpdateStats();
4345 if ((counter % DRAW_FREQ_DENOM) == 0) {
4346 // redraw values in buffer
4347 osdRefresh(currentTimeUs);
4348 } else {
4349 // rest of time redraw screen
4350 displayDrawScreen(osdDisplayPort);
4353 #ifdef USE_CMS
4354 // do not allow ARM if we are in menu
4355 if (displayIsGrabbed(osdDisplayPort)) {
4356 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4357 } else {
4358 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4360 #endif
4363 void osdStartFullRedraw(void)
4365 fullRedraw = true;
4368 void osdOverrideLayout(int layout, timeMs_t duration)
4370 layoutOverride = constrain(layout, -1, ARRAYLEN(osdLayoutsConfig()->item_pos) - 1);
4371 if (layoutOverride >= 0 && duration > 0) {
4372 layoutOverrideUntil = millis() + duration;
4373 } else {
4374 layoutOverrideUntil = 0;
4378 int osdGetActiveLayout(bool *overridden)
4380 if (overridden) {
4381 *overridden = layoutOverride >= 0;
4383 return currentLayout;
4386 bool osdItemIsFixed(osd_items_e item)
4388 return item == OSD_CROSSHAIRS ||
4389 item == OSD_ARTIFICIAL_HORIZON ||
4390 item == OSD_HORIZON_SIDEBARS;
4393 displayPort_t *osdGetDisplayPort(void)
4395 return osdDisplayPort;
4398 displayCanvas_t *osdGetDisplayPortCanvas(void)
4400 #if defined(USE_CANVAS)
4401 if (osdDisplayHasCanvas) {
4402 return &osdCanvas;
4404 #endif
4405 return NULL;
4408 timeMs_t systemMessageCycleTime(unsigned messageCount, const char **messages){
4409 uint8_t i = 0;
4410 float factor = 1.0f;
4411 while (i < messageCount) {
4412 if ((float)strlen(messages[i]) / 15.0f > factor) {
4413 factor = (float)strlen(messages[i]) / 15.0f;
4415 i++;
4417 return osdConfig()->system_msg_display_time * factor;
4420 textAttributes_t osdGetSystemMessage(char *buff, size_t buff_size, bool isCenteredText)
4422 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
4424 if (buff != NULL) {
4425 const char *message = NULL;
4426 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
4427 // We might have up to 5 messages to show.
4428 const char *messages[5];
4429 unsigned messageCount = 0;
4430 const char *failsafeInfoMessage = NULL;
4431 const char *invertedInfoMessage = NULL;
4433 if (ARMING_FLAG(ARMED)) {
4434 if (FLIGHT_MODE(FAILSAFE_MODE) || FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding()) {
4435 if (isWaypointMissionRTHActive()) {
4436 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
4437 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL);
4439 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE) {
4440 messages[messageCount++] = STATE(LANDING_DETECTED) ? OSD_MESSAGE_STR(OSD_MSG_WP_LANDED) : OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED);
4441 } else if (NAV_Status.state == MW_NAV_STATE_WP_ENROUTE) {
4442 // Countdown display for remaining Waypoints
4443 char buf[6];
4444 osdFormatDistanceSymbol(buf, posControl.wpDistance, 0);
4445 tfp_sprintf(messageBuf, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl.activeWaypointIndex), posControl.geoWaypointCount, buf);
4446 messages[messageCount++] = messageBuf;
4447 } else if (NAV_Status.state == MW_NAV_STATE_HOLD_TIMED) {
4448 if (navConfig()->general.waypoint_enforce_altitude && !posControl.wpAltitudeReached) {
4449 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ADJUSTING_WP_ALT);
4450 } else {
4451 // WP hold time countdown in seconds
4452 timeMs_t currentTime = millis();
4453 int holdTimeRemaining = posControl.waypointList[posControl.activeWaypointIndex].p1 - (int)(MS2S(currentTime - posControl.wpReachedTime));
4454 holdTimeRemaining = holdTimeRemaining >= 0 ? holdTimeRemaining : 0;
4456 tfp_sprintf(messageBuf, "HOLDING WP FOR %2u S", holdTimeRemaining);
4458 messages[messageCount++] = messageBuf;
4460 } else {
4461 const char *navStateMessage = navigationStateMessage();
4462 if (navStateMessage) {
4463 messages[messageCount++] = navStateMessage;
4466 #if defined(USE_SAFE_HOME)
4467 const char *safehomeMessage = divertingToSafehomeMessage();
4468 if (safehomeMessage) {
4469 messages[messageCount++] = safehomeMessage;
4471 #endif
4472 if (FLIGHT_MODE(FAILSAFE_MODE)) {
4473 // In FS mode while being armed too
4474 const char *failsafePhaseMessage = osdFailsafePhaseMessage();
4475 failsafeInfoMessage = osdFailsafeInfoMessage();
4477 if (failsafePhaseMessage) {
4478 messages[messageCount++] = failsafePhaseMessage;
4480 if (failsafeInfoMessage) {
4481 messages[messageCount++] = failsafeInfoMessage;
4484 } else { /* messages shown only when Failsafe, WP, RTH or Emergency Landing not active */
4485 if (STATE(FIXED_WING_LEGACY) && (navGetCurrentStateFlags() & NAV_CTL_LAUNCH)) {
4486 messages[messageCount++] = navConfig()->fw.launch_manual_throttle ? OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH_MANUAL) :
4487 OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH);
4488 const char *launchStateMessage = fixedWingLaunchStateMessage();
4489 if (launchStateMessage) {
4490 messages[messageCount++] = launchStateMessage;
4492 } else {
4493 if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && !navigationRequiresAngleMode()) {
4494 // ALTHOLD might be enabled alongside ANGLE/HORIZON/ACRO
4495 // when it doesn't require ANGLE mode (required only in FW
4496 // right now). If if requires ANGLE, its display is handled
4497 // by OSD_FLYMODE.
4498 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD);
4500 if (IS_RC_MODE_ACTIVE(BOXAUTOTRIM) && !feature(FEATURE_FW_AUTOTRIM)) {
4501 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM);
4503 if (IS_RC_MODE_ACTIVE(BOXAUTOTUNE)) {
4504 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE);
4505 if (FLIGHT_MODE(MANUAL_MODE)) {
4506 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO);
4509 if (FLIGHT_MODE(HEADFREE_MODE)) {
4510 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE);
4512 if (FLIGHT_MODE(SOARING_MODE)) {
4513 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING);
4515 if (posControl.flags.wpMissionPlannerActive) {
4516 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER);
4518 if (STATE(LANDING_DETECTED)) {
4519 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_LANDED);
4523 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS)) {
4524 unsigned invalidIndex;
4526 // Check if we're unable to arm for some reason
4527 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING) && !settingsValidate(&invalidIndex)) {
4529 const setting_t *setting = settingGet(invalidIndex);
4530 settingGetName(setting, messageBuf);
4531 for (int ii = 0; messageBuf[ii]; ii++) {
4532 messageBuf[ii] = sl_toupper(messageBuf[ii]);
4534 invertedInfoMessage = messageBuf;
4535 messages[messageCount++] = invertedInfoMessage;
4537 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
4538 messages[messageCount++] = invertedInfoMessage;
4540 } else {
4542 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM);
4543 messages[messageCount++] = invertedInfoMessage;
4545 // Show the reason for not arming
4546 messages[messageCount++] = osdArmingDisabledReasonMessage();
4549 } else if (!ARMING_FLAG(ARMED)) {
4550 if (isWaypointListValid()) {
4551 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED);
4555 /* Messages that are shown regardless of Arming state */
4557 #ifdef USE_DEV_TOOLS
4558 if (systemConfig()->groundTestMode) {
4559 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_GRD_TEST_MODE);
4561 #endif
4563 if (messageCount > 0) {
4564 message = messages[OSD_ALTERNATING_CHOICES(systemMessageCycleTime(messageCount, messages), messageCount)];
4565 if (message == failsafeInfoMessage) {
4566 // failsafeInfoMessage is not useful for recovering
4567 // a lost model, but might help avoiding a crash.
4568 // Blink to grab user attention.
4569 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
4570 } else if (message == invertedInfoMessage) {
4571 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
4573 // We're shoing either failsafePhaseMessage or
4574 // navStateMessage. Don't BLINK here since
4575 // having this text available might be crucial
4576 // during a lost aircraft recovery and blinking
4577 // will cause it to be missing from some frames.
4580 osdFormatMessage(buff, buff_size, message, isCenteredText);
4582 return elemAttr;
4585 #endif // OSD