Fix WS2812 led definition
[inav.git] / src / main / io / osd.c
blobeed14a2764f24def0ff9a6146d63115e19fb315d
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/displayport_msp_bf_compat.h"
73 #include "io/vtx.h"
74 #include "io/vtx_string.h"
76 #include "fc/config.h"
77 #include "fc/controlrate_profile.h"
78 #include "fc/fc_core.h"
79 #include "fc/fc_tasks.h"
80 #include "fc/rc_adjustments.h"
81 #include "fc/rc_controls.h"
82 #include "fc/rc_modes.h"
83 #include "fc/runtime_config.h"
84 #include "fc/settings.h"
86 #include "flight/imu.h"
87 #include "flight/mixer.h"
88 #include "flight/pid.h"
89 #include "flight/power_limits.h"
90 #include "flight/rth_estimator.h"
91 #include "flight/servos.h"
92 #include "flight/wind_estimator.h"
94 #include "navigation/navigation.h"
95 #include "navigation/navigation_private.h"
97 #include "rx/rx.h"
98 #include "rx/msp_override.h"
100 #include "sensors/acceleration.h"
101 #include "sensors/battery.h"
102 #include "sensors/boardalignment.h"
103 #include "sensors/diagnostics.h"
104 #include "sensors/sensors.h"
105 #include "sensors/pitotmeter.h"
106 #include "sensors/temperature.h"
107 #include "sensors/esc_sensor.h"
108 #include "sensors/rangefinder.h"
110 #include "programming/logic_condition.h"
111 #include "programming/global_variables.h"
113 #ifdef USE_HARDWARE_REVISION_DETECTION
114 #include "hardware_revision.h"
115 #endif
117 #define VIDEO_BUFFER_CHARS_PAL 480
118 #define VIDEO_BUFFER_CHARS_HDZERO 900
119 #define VIDEO_BUFFER_CHARS_DJIWTF 1320
121 #define GFORCE_FILTER_TC 0.2
123 #define DELAYED_REFRESH_RESUME_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
124 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
125 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
127 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
128 #define ARMED_SCREEN_DISPLAY_TIME 1500 // ms
129 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
131 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
133 // Adjust OSD_MESSAGE's default position when
134 // changing OSD_MESSAGE_LENGTH
135 #define OSD_MESSAGE_LENGTH 28
136 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
137 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
138 // Wrap all string constants intenteded for display as messages with
139 // this macro to ensure compile time length validation.
140 #define OSD_MESSAGE_STR(x) ({ \
141 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
142 x; \
145 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
147 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
148 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
150 #define OSD_MIN_FONT_VERSION 3
152 static timeMs_t notify_settings_saved = 0;
153 static bool savingSettings = false;
155 static unsigned currentLayout = 0;
156 static int layoutOverride = -1;
157 static bool hasExtendedFont = false; // Wether the font supports characters > 256
158 static timeMs_t layoutOverrideUntil = 0;
159 static pt1Filter_t GForceFilter, GForceFilterAxis[XYZ_AXIS_COUNT];
160 static float GForce, GForceAxis[XYZ_AXIS_COUNT];
162 typedef struct statistic_s {
163 uint16_t max_speed;
164 uint16_t max_3D_speed;
165 uint16_t max_air_speed;
166 uint16_t min_voltage; // /100
167 int16_t max_current;
168 int32_t max_power;
169 int16_t min_rssi;
170 int16_t min_lq; // for CRSF
171 int16_t min_rssi_dbm; // for CRSF
172 int32_t max_altitude;
173 uint32_t max_distance;
174 } statistic_t;
176 static statistic_t stats;
178 static timeUs_t resumeRefreshAt = 0;
179 static bool refreshWaitForResumeCmdRelease;
181 static bool fullRedraw = false;
183 static uint8_t armState;
184 static uint8_t statsPagesCheck = 0;
186 typedef struct osdMapData_s {
187 uint32_t scale;
188 char referenceSymbol;
189 } osdMapData_t;
191 static osdMapData_t osdMapData;
193 static displayPort_t *osdDisplayPort;
194 static bool osdDisplayIsReady = false;
195 #if defined(USE_CANVAS)
196 static displayCanvas_t osdCanvas;
197 static bool osdDisplayHasCanvas;
198 #else
199 #define osdDisplayHasCanvas false
200 #endif
202 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
204 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t, osdConfig, PG_OSD_CONFIG, 7);
205 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t, osdLayoutsConfig, PG_OSD_LAYOUTS_CONFIG, 1);
207 void osdStartedSaveProcess() {
208 savingSettings = true;
211 void osdShowEEPROMSavedNotification() {
212 savingSettings = false;
213 notify_settings_saved = millis() + 5000;
216 static int digitCount(int32_t value)
218 int digits = 1;
219 while(1) {
220 value = value / 10;
221 if (value == 0) {
222 break;
224 digits++;
226 return digits;
229 bool osdDisplayIsPAL(void)
231 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL;
234 bool osdDisplayIsHD(void)
236 if (displayScreenSize(osdDisplayPort) >= VIDEO_BUFFER_CHARS_HDZERO)
238 return true;
240 return false;
244 * Formats a number given in cents, to support non integer values
245 * without using floating point math. Value is always right aligned
246 * and spaces are inserted before the number to always yield a string
247 * of the same length. If the value doesn't fit into the provided length
248 * it will be divided by scale and true will be returned.
250 bool osdFormatCentiNumber(char *buff, int32_t centivalue, uint32_t scale, int maxDecimals, int maxScaledDecimals, int length)
252 char *ptr = buff;
253 char *dec;
254 int decimals = maxDecimals;
255 bool negative = false;
256 bool scaled = false;
257 bool explicitDecimal = isBfCompatibleVideoSystem(osdConfig());
259 buff[length] = '\0';
261 if (centivalue < 0) {
262 negative = true;
263 centivalue = -centivalue;
264 length--;
267 int32_t integerPart = centivalue / 100;
268 // 3 decimal digits
269 int32_t millis = (centivalue % 100) * 10;
271 int digits = digitCount(integerPart);
272 int remaining = length - digits;
273 if (explicitDecimal) {
274 remaining--;
277 if (remaining < 0 && scale > 0) {
278 // Reduce by scale
279 scaled = true;
280 decimals = maxScaledDecimals;
281 integerPart = integerPart / scale;
282 // Multiply by 10 to get 3 decimal digits
283 millis = ((centivalue % (100 * scale)) * 10) / scale;
284 digits = digitCount(integerPart);
285 remaining = length - digits;
286 if (explicitDecimal) {
287 remaining--;
291 // 3 decimals at most
292 decimals = MIN(remaining, MIN(decimals, 3));
293 remaining -= decimals;
295 // Done counting. Time to write the characters.
297 // Write spaces at the start
298 while (remaining > 0) {
299 *ptr = SYM_BLANK;
300 ptr++;
301 remaining--;
304 // Write the minus sign if required
305 if (negative) {
306 *ptr = '-';
307 ptr++;
309 // Now write the digits.
310 ui2a(integerPart, 10, 0, ptr);
311 ptr += digits;
313 if (decimals > 0) {
314 if (explicitDecimal) {
315 *ptr = '.';
316 ptr++;
317 } else {
318 *(ptr - 1) += SYM_ZERO_HALF_TRAILING_DOT - '0';
320 dec = ptr;
321 int factor = 3; // we're getting the decimal part in millis first
322 while (decimals < factor) {
323 factor--;
324 millis /= 10;
326 int decimalDigits = digitCount(millis);
327 while (decimalDigits < decimals) {
328 decimalDigits++;
329 *ptr = '0';
330 ptr++;
332 ui2a(millis, 10, 0, ptr);
333 if (!explicitDecimal) {
334 *dec += SYM_ZERO_HALF_LEADING_DOT - '0';
337 return scaled;
341 * Aligns text to the left side. Adds spaces at the end to keep string length unchanged.
343 static void osdLeftAlignString(char *buff)
345 uint8_t sp = 0, ch = 0;
346 uint8_t len = strlen(buff);
347 while (buff[sp] == ' ') sp++;
348 for (ch = 0; ch < (len - sp); ch++) buff[ch] = buff[ch + sp];
349 for (sp = ch; sp < len; sp++) buff[sp] = ' ';
353 * Converts distance into a string based on the current unit system
354 * prefixed by a a symbol to indicate the unit used.
355 * @param dist Distance in centimeters
357 static void osdFormatDistanceSymbol(char *buff, int32_t dist, uint8_t decimals)
359 switch ((osd_unit_e)osdConfig()->units) {
360 case OSD_UNIT_UK:
361 FALLTHROUGH;
362 case OSD_UNIT_IMPERIAL:
363 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_MILE, decimals, 3, 3)) {
364 buff[3] = SYM_DIST_MI;
365 } else {
366 buff[3] = SYM_DIST_FT;
368 buff[4] = '\0';
369 break;
370 case OSD_UNIT_METRIC_MPH:
371 FALLTHROUGH;
372 case OSD_UNIT_METRIC:
373 if (osdFormatCentiNumber(buff, dist, METERS_PER_KILOMETER, decimals, 3, 3)) {
374 buff[3] = SYM_DIST_KM;
375 } else {
376 buff[3] = SYM_DIST_M;
378 buff[4] = '\0';
379 break;
380 case OSD_UNIT_GA:
381 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_NAUTICALMILE, decimals, 3, 3)) {
382 buff[3] = SYM_DIST_NM;
383 } else {
384 buff[3] = SYM_DIST_FT;
386 buff[4] = '\0';
387 break;
392 * Converts distance into a string based on the current unit system.
393 * @param dist Distance in centimeters
395 static void osdFormatDistanceStr(char *buff, int32_t dist)
397 int32_t centifeet;
398 switch ((osd_unit_e)osdConfig()->units) {
399 case OSD_UNIT_UK:
400 FALLTHROUGH;
401 case OSD_UNIT_IMPERIAL:
402 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
403 if (abs(centifeet) < FEET_PER_MILE * 100 / 2) {
404 // Show feet when dist < 0.5mi
405 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
406 } else {
407 // Show miles when dist >= 0.5mi
408 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100*FEET_PER_MILE)),
409 (abs(centifeet) % (100 * FEET_PER_MILE)) / FEET_PER_MILE, SYM_MI);
411 break;
412 case OSD_UNIT_METRIC_MPH:
413 FALLTHROUGH;
414 case OSD_UNIT_METRIC:
415 if (abs(dist) < METERS_PER_KILOMETER * 100) {
416 // Show meters when dist < 1km
417 tfp_sprintf(buff, "%d%c", (int)(dist / 100), SYM_M);
418 } else {
419 // Show kilometers when dist >= 1km
420 tfp_sprintf(buff, "%d.%02d%c", (int)(dist / (100*METERS_PER_KILOMETER)),
421 (abs(dist) % (100 * METERS_PER_KILOMETER)) / METERS_PER_KILOMETER, SYM_KM);
423 break;
424 case OSD_UNIT_GA:
425 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
426 if (abs(centifeet) < 100000) {
427 // Show feet when dist < 1000ft
428 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
429 } else {
430 // Show nautical miles when dist >= 1000ft
431 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100 * FEET_PER_NAUTICALMILE)),
432 (int)((abs(centifeet) % (int)(100 * FEET_PER_NAUTICALMILE)) / FEET_PER_NAUTICALMILE), SYM_NM);
434 break;
439 * Converts velocity based on the current unit system (kmh or mph).
440 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
442 static int32_t osdConvertVelocityToUnit(int32_t vel)
444 switch ((osd_unit_e)osdConfig()->units) {
445 case OSD_UNIT_UK:
446 FALLTHROUGH;
447 case OSD_UNIT_METRIC_MPH:
448 FALLTHROUGH;
449 case OSD_UNIT_IMPERIAL:
450 return CMSEC_TO_CENTIMPH(vel) / 100; // Convert to mph
451 case OSD_UNIT_METRIC:
452 return CMSEC_TO_CENTIKPH(vel) / 100; // Convert to kmh
453 case OSD_UNIT_GA:
454 return CMSEC_TO_CENTIKNOTS(vel) / 100; // Convert to Knots
456 // Unreachable
457 return -1;
461 * Converts velocity into a string based on the current unit system.
462 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
463 * @param _3D is a 3D velocity
464 * @param _max is a maximum velocity
466 void osdFormatVelocityStr(char* buff, int32_t vel, bool _3D, bool _max)
468 switch ((osd_unit_e)osdConfig()->units) {
469 case OSD_UNIT_UK:
470 FALLTHROUGH;
471 case OSD_UNIT_METRIC_MPH:
472 FALLTHROUGH;
473 case OSD_UNIT_IMPERIAL:
474 if (_max) {
475 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
476 } else {
477 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
479 break;
480 case OSD_UNIT_METRIC:
481 if (_max) {
482 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
483 } else {
484 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
486 break;
487 case OSD_UNIT_GA:
488 if (_max) {
489 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
490 } else {
491 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
493 break;
498 * 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
500 static void osdGenerateAverageVelocityStr(char* buff) {
501 uint32_t cmPerSec = getTotalTravelDistance() / getFlightTime();
502 osdFormatVelocityStr(buff, cmPerSec, false, false);
506 * Converts wind speed into a string based on the current unit system, using
507 * always 3 digits and an additional character for the unit at the right. buff
508 * is null terminated.
509 * @param ws Raw wind speed in cm/s
511 #ifdef USE_WIND_ESTIMATOR
512 static void osdFormatWindSpeedStr(char *buff, int32_t ws, bool isValid)
514 int32_t centivalue;
515 char suffix;
516 switch (osdConfig()->units) {
517 case OSD_UNIT_UK:
518 FALLTHROUGH;
519 case OSD_UNIT_METRIC_MPH:
520 FALLTHROUGH;
521 case OSD_UNIT_IMPERIAL:
522 centivalue = CMSEC_TO_CENTIMPH(ws);
523 suffix = SYM_MPH;
524 break;
525 case OSD_UNIT_GA:
526 centivalue = CMSEC_TO_CENTIKNOTS(ws);
527 suffix = SYM_KT;
528 break;
529 default:
530 case OSD_UNIT_METRIC:
531 centivalue = CMSEC_TO_CENTIKPH(ws);
532 suffix = SYM_KMH;
533 break;
535 osdFormatCentiNumber(buff, centivalue, 0, 2, 0, 3);
536 if (!isValid)
538 suffix = '*';
540 buff[3] = suffix;
541 buff[4] = '\0';
543 #endif
546 * Converts altitude into a string based on the current unit system
547 * prefixed by a a symbol to indicate the unit used.
548 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
550 void osdFormatAltitudeSymbol(char *buff, int32_t alt)
552 int digits;
553 if (alt < 0) {
554 digits = 4;
555 } else {
556 digits = 3;
557 buff[0] = ' ';
559 switch ((osd_unit_e)osdConfig()->units) {
560 case OSD_UNIT_UK:
561 FALLTHROUGH;
562 case OSD_UNIT_GA:
563 FALLTHROUGH;
564 case OSD_UNIT_IMPERIAL:
565 if (osdFormatCentiNumber(buff + 4 - digits, CENTIMETERS_TO_CENTIFEET(alt), 1000, 0, 2, digits)) {
566 // Scaled to kft
567 buff[4] = SYM_ALT_KFT;
568 } else {
569 // Formatted in feet
570 buff[4] = SYM_ALT_FT;
572 buff[5] = '\0';
573 break;
574 case OSD_UNIT_METRIC_MPH:
575 FALLTHROUGH;
576 case OSD_UNIT_METRIC:
577 // alt is alredy in cm
578 if (osdFormatCentiNumber(buff + 4 - digits, alt, 1000, 0, 2, digits)) {
579 // Scaled to km
580 buff[4] = SYM_ALT_KM;
581 } else {
582 // Formatted in m
583 buff[4] = SYM_ALT_M;
585 buff[5] = '\0';
586 break;
591 * Converts altitude into a string based on the current unit system.
592 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
594 static void osdFormatAltitudeStr(char *buff, int32_t alt)
596 int32_t value;
597 switch ((osd_unit_e)osdConfig()->units) {
598 case OSD_UNIT_UK:
599 FALLTHROUGH;
600 case OSD_UNIT_GA:
601 FALLTHROUGH;
602 case OSD_UNIT_IMPERIAL:
603 value = CENTIMETERS_TO_FEET(alt);
604 tfp_sprintf(buff, "%d%c", (int)value, SYM_FT);
605 break;
606 case OSD_UNIT_METRIC_MPH:
607 FALLTHROUGH;
608 case OSD_UNIT_METRIC:
609 value = CENTIMETERS_TO_METERS(alt);
610 tfp_sprintf(buff, "%d%c", (int)value, SYM_M);
611 break;
615 static void osdFormatTime(char *buff, uint32_t seconds, char sym_m, char sym_h)
617 uint32_t value = seconds;
618 char sym = sym_m;
619 // Maximum value we can show in minutes is 99 minutes and 59 seconds
620 if (seconds > (99 * 60) + 59) {
621 sym = sym_h;
622 value = seconds / 60;
624 buff[0] = sym;
625 tfp_sprintf(buff + 1, "%02d:%02d", (int)(value / 60), (int)(value % 60));
628 static inline void osdFormatOnTime(char *buff)
630 osdFormatTime(buff, micros() / 1000000, SYM_ON_M, SYM_ON_H);
633 static inline void osdFormatFlyTime(char *buff, textAttributes_t *attr)
635 uint32_t seconds = getFlightTime();
636 osdFormatTime(buff, seconds, SYM_FLY_M, SYM_FLY_H);
637 if (attr && osdConfig()->time_alarm > 0) {
638 if (seconds / 60 >= osdConfig()->time_alarm && ARMING_FLAG(ARMED)) {
639 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
645 * Converts RSSI into a % value used by the OSD.
647 static uint16_t osdConvertRSSI(void)
649 // change range to [0, 99]
650 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE, 0, 99);
653 static uint16_t osdGetCrsfLQ(void)
655 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
656 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
657 int16_t displayedLQ;
658 switch (osdConfig()->crsf_lq_format) {
659 case OSD_CRSF_LQ_TYPE1:
660 displayedLQ = statsLQ;
661 break;
662 case OSD_CRSF_LQ_TYPE2:
663 displayedLQ = statsLQ;
664 break;
665 case OSD_CRSF_LQ_TYPE3:
666 displayedLQ = rxLinkStatistics.rfMode >= 2 ? scaledLQ : statsLQ;
667 break;
669 return displayedLQ;
672 static int16_t osdGetCrsfdBm(void)
674 return rxLinkStatistics.uplinkRSSI;
677 * Displays a temperature postfixed with a symbol depending on the current unit system
678 * @param label to display
679 * @param valid true if measurement is valid
680 * @param temperature in deciDegrees Celcius
682 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)
684 char buff[TEMPERATURE_LABEL_LEN + 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN + 2];
685 textAttributes_t elemAttr = valid ? TEXT_ATTRIBUTES_NONE : _TEXT_ATTRIBUTES_BLINK_BIT;
686 uint8_t valueXOffset = 0;
688 if (symbol) {
689 buff[0] = symbol;
690 buff[1] = '\0';
691 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
692 valueXOffset = 1;
694 #ifdef USE_TEMPERATURE_SENSOR
695 else if (label[0] != '\0') {
696 uint8_t label_len = strnlen(label, TEMPERATURE_LABEL_LEN);
697 memcpy(buff, label, label_len);
698 memset(buff + label_len, ' ', TEMPERATURE_LABEL_LEN + 1 - label_len);
699 buff[5] = '\0';
700 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
701 valueXOffset = osdConfig()->temp_label_align == OSD_ALIGN_LEFT ? 5 : label_len + 1;
703 #else
704 UNUSED(label);
705 #endif
707 if (valid) {
709 if ((temperature <= alarm_min) || (temperature >= alarm_max)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
710 if (osdConfig()->units == OSD_UNIT_IMPERIAL) temperature = temperature * 9 / 5.0f + 320;
711 tfp_sprintf(buff, "%3d", temperature / 10);
713 } else
714 strcpy(buff, "---");
716 buff[3] = osdConfig()->units == OSD_UNIT_IMPERIAL ? SYM_TEMP_F : SYM_TEMP_C;
717 buff[4] = '\0';
719 displayWriteWithAttr(osdDisplayPort, elemPosX + valueXOffset, elemPosY, buff, elemAttr);
722 #ifdef USE_TEMPERATURE_SENSOR
723 static void osdDisplayTemperatureSensor(uint8_t elemPosX, uint8_t elemPosY, uint8_t sensorIndex)
725 int16_t temperature;
726 const bool valid = getSensorTemperature(sensorIndex, &temperature);
727 const tempSensorConfig_t *sensorConfig = tempSensorConfig(sensorIndex);
728 uint16_t symbol = sensorConfig->osdSymbol ? SYM_TEMP_SENSOR_FIRST + sensorConfig->osdSymbol - 1 : 0;
729 osdDisplayTemperature(elemPosX, elemPosY, symbol, sensorConfig->label, valid, temperature, sensorConfig->alarm_min, sensorConfig->alarm_max);
731 #endif
733 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
735 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
736 const int coordinateLength = osdConfig()->coordinate_digits + 1;
738 buff[0] = sym;
739 int32_t integerPart = val / GPS_DEGREES_DIVIDER;
740 // Latitude maximum integer width is 3 (-90) while
741 // longitude maximum integer width is 4 (-180).
742 int integerDigits = tfp_sprintf(buff + 1, (integerPart == 0 && val < 0) ? "-%d" : "%d", (int)integerPart);
743 // We can show up to 7 digits in decimalPart.
744 int32_t decimalPart = abs(val % GPS_DEGREES_DIVIDER);
745 STATIC_ASSERT(GPS_DEGREES_DIVIDER == 1e7, adjust_max_decimal_digits);
746 int decimalDigits;
747 if (!isBfCompatibleVideoSystem(osdConfig())) {
748 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, "%07d", (int)decimalPart);
749 // Embbed the decimal separator
750 buff[1 + integerDigits - 1] += SYM_ZERO_HALF_TRAILING_DOT - '0';
751 buff[1 + integerDigits] += SYM_ZERO_HALF_LEADING_DOT - '0';
752 } else {
753 decimalDigits = tfp_sprintf(buff + 1 + integerDigits, ".%06d", (int)decimalPart);
755 // Fill up to coordinateLength with zeros
756 int total = 1 + integerDigits + decimalDigits;
757 while(total < coordinateLength) {
758 buff[total] = '0';
759 total++;
761 buff[coordinateLength] = '\0';
764 static void osdFormatCraftName(char *buff)
766 if (strlen(systemConfig()->craftName) == 0)
767 strcpy(buff, "CRAFT_NAME");
768 else {
769 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
770 buff[i] = sl_toupper((unsigned char)systemConfig()->craftName[i]);
771 if (systemConfig()->craftName[i] == 0)
772 break;
777 void osdFormatPilotName(char *buff)
779 if (strlen(systemConfig()->pilotName) == 0)
780 strcpy(buff, "PILOT_NAME");
781 else {
782 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
783 buff[i] = sl_toupper((unsigned char)systemConfig()->pilotName[i]);
784 if (systemConfig()->pilotName[i] == 0)
785 break;
790 static const char * osdArmingDisabledReasonMessage(void)
792 const char *message = NULL;
793 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
795 switch (isArmingDisabledReason()) {
796 case ARMING_DISABLED_FAILSAFE_SYSTEM:
797 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
798 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING) {
799 if (failsafeIsReceivingRxData()) {
800 // If we're not using sticks, it means the ARM switch
801 // hasn't been off since entering FAILSAFE_RX_LOSS_MONITORING
802 // yet
803 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF);
805 // Not receiving RX data
806 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
808 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS);
809 case ARMING_DISABLED_NOT_LEVEL:
810 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL);
811 case ARMING_DISABLED_SENSORS_CALIBRATING:
812 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL);
813 case ARMING_DISABLED_SYSTEM_OVERLOADED:
814 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED);
815 case ARMING_DISABLED_NAVIGATION_UNSAFE:
816 // Check the exact reason
817 switch (navigationIsBlockingArming(NULL)) {
818 char buf[6];
819 case NAV_ARMING_BLOCKER_NONE:
820 break;
821 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX:
822 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX);
823 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE:
824 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST);
825 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR:
826 osdFormatDistanceSymbol(buf, distanceToFirstWP(), 0);
827 tfp_sprintf(messageBuf, "FIRST WP TOO FAR (%s)", buf);
828 return message = messageBuf;
829 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR:
830 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG);
832 break;
833 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED:
834 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL);
835 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED:
836 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL);
837 case ARMING_DISABLED_ARM_SWITCH:
838 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST);
839 case ARMING_DISABLED_HARDWARE_FAILURE:
841 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
842 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE);
844 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
845 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL);
847 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
848 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL);
850 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
851 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL);
853 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
854 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL);
856 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
857 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL);
859 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
860 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL);
863 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL);
864 case ARMING_DISABLED_BOXFAILSAFE:
865 return OSD_MESSAGE_STR(OSD_MSG_FS_EN);
866 case ARMING_DISABLED_BOXKILLSWITCH:
867 return OSD_MESSAGE_STR(OSD_MSG_KILL_SW_EN);
868 case ARMING_DISABLED_RC_LINK:
869 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK);
870 case ARMING_DISABLED_THROTTLE:
871 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW);
872 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED:
873 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER);
874 case ARMING_DISABLED_SERVO_AUTOTRIM:
875 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE);
876 case ARMING_DISABLED_OOM:
877 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY);
878 case ARMING_DISABLED_INVALID_SETTING:
879 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
880 case ARMING_DISABLED_CLI:
881 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE);
882 case ARMING_DISABLED_PWM_OUTPUT_ERROR:
883 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR);
884 case ARMING_DISABLED_NO_PREARM:
885 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM);
886 case ARMING_DISABLED_DSHOT_BEEPER:
887 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER);
888 // Cases without message
889 case ARMING_DISABLED_LANDING_DETECTED:
890 FALLTHROUGH;
891 case ARMING_DISABLED_CMS_MENU:
892 FALLTHROUGH;
893 case ARMING_DISABLED_OSD_MENU:
894 FALLTHROUGH;
895 case ARMING_DISABLED_ALL_FLAGS:
896 FALLTHROUGH;
897 case ARMED:
898 FALLTHROUGH;
899 case SIMULATOR_MODE:
900 FALLTHROUGH;
901 case WAS_EVER_ARMED:
902 break;
904 return NULL;
907 static const char * osdFailsafePhaseMessage(void)
909 // See failsafe.h for each phase explanation
910 switch (failsafePhase()) {
911 case FAILSAFE_RETURN_TO_HOME:
912 // XXX: Keep this in sync with OSD_FLYMODE.
913 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS);
914 case FAILSAFE_LANDING:
915 // This should be considered an emergengy landing
916 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS);
917 case FAILSAFE_RX_LOSS_MONITORING:
918 // Only reachable from FAILSAFE_LANDED, which performs
919 // a disarm. Since aircraft has been disarmed, we no
920 // longer show failsafe details.
921 FALLTHROUGH;
922 case FAILSAFE_LANDED:
923 // Very brief, disarms and transitions into
924 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
925 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
926 // so we'll show the user how to re-arm in when
927 // that flag is the reason to prevent arming.
928 FALLTHROUGH;
929 case FAILSAFE_RX_LOSS_IDLE:
930 // This only happens when user has chosen NONE as FS
931 // procedure. The recovery messages should be enough.
932 FALLTHROUGH;
933 case FAILSAFE_IDLE:
934 // Failsafe not active
935 FALLTHROUGH;
936 case FAILSAFE_RX_LOSS_DETECTED:
937 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
938 // or the FS procedure immediately.
939 FALLTHROUGH;
940 case FAILSAFE_RX_LOSS_RECOVERED:
941 // Exiting failsafe
942 break;
944 return NULL;
947 static const char * osdFailsafeInfoMessage(void)
949 if (failsafeIsReceivingRxData()) {
950 // User must move sticks to exit FS mode
951 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS);
953 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
955 #if defined(USE_SAFE_HOME)
956 static const char * divertingToSafehomeMessage(void)
958 if (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && safehome_applied) {
959 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME);
961 return NULL;
963 #endif
965 static const char * navigationStateMessage(void)
967 switch (NAV_Status.state) {
968 case MW_NAV_STATE_NONE:
969 break;
970 case MW_NAV_STATE_RTH_START:
971 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH);
972 case MW_NAV_STATE_RTH_CLIMB:
973 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB);
974 case MW_NAV_STATE_RTH_ENROUTE:
975 if (posControl.flags.rthTrackbackActive) {
976 return OSD_MESSAGE_STR(OSD_MSG_RTH_TRACKBACK);
977 } else {
978 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME);
980 case MW_NAV_STATE_HOLD_INFINIT:
981 // Used by HOLD flight modes. No information to add.
982 break;
983 case MW_NAV_STATE_HOLD_TIMED:
984 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
985 break;
986 case MW_NAV_STATE_WP_ENROUTE:
987 // "TO WP" + WP countdown added in osdGetSystemMessage
988 break;
989 case MW_NAV_STATE_PROCESS_NEXT:
990 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP);
991 case MW_NAV_STATE_DO_JUMP:
992 // Not used
993 break;
994 case MW_NAV_STATE_LAND_START:
995 // Not used
996 break;
997 case MW_NAV_STATE_EMERGENCY_LANDING:
998 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING);
999 case MW_NAV_STATE_LAND_IN_PROGRESS:
1000 return OSD_MESSAGE_STR(OSD_MSG_LANDING);
1001 case MW_NAV_STATE_HOVER_ABOVE_HOME:
1002 if (STATE(FIXED_WING_LEGACY)) {
1003 #if defined(USE_SAFE_HOME)
1004 if (safehome_applied) {
1005 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME);
1007 #endif
1008 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME);
1010 return OSD_MESSAGE_STR(OSD_MSG_HOVERING);
1011 case MW_NAV_STATE_LANDED:
1012 return OSD_MESSAGE_STR(OSD_MSG_LANDED);
1013 case MW_NAV_STATE_LAND_SETTLE:
1014 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND);
1015 case MW_NAV_STATE_LAND_START_DESCENT:
1016 // Not used
1017 break;
1019 return NULL;
1022 static void osdFormatMessage(char *buff, size_t size, const char *message, bool isCenteredText)
1024 // String is always filled with Blanks
1025 memset(buff, SYM_BLANK, size);
1026 if (message) {
1027 size_t messageLength = strlen(message);
1028 int rem = isCenteredText ? MAX(0, (int)size - (int)messageLength) : 0;
1029 strncpy(buff + rem / 2, message, MIN((int)size - rem / 2, (int)messageLength));
1031 // Ensure buff is zero terminated
1032 buff[size] = '\0';
1036 * Draws the battery symbol filled in accordingly to the
1037 * battery voltage to buff[0].
1039 static void osdFormatBatteryChargeSymbol(char *buff)
1041 uint8_t p = calculateBatteryPercentage();
1042 p = (100 - p) / 16.6;
1043 buff[0] = SYM_BATT_FULL + p;
1046 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t *attr)
1048 if ((getBatteryState() != BATTERY_NOT_PRESENT) && ((batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical)) || ((!batteryUsesCapacityThresholds()) && (getBatteryVoltage() <= getBatteryWarningVoltage()))))
1049 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
1052 void osdCrosshairPosition(uint8_t *x, uint8_t *y)
1054 *x = osdDisplayPort->cols / 2;
1055 *y = osdDisplayPort->rows / 2;
1056 *y -= osdConfig()->horizon_offset; // positive horizon_offset moves the HUD up, negative moves down
1060 * Formats throttle position prefixed by its symbol.
1061 * Shows output to motor, not stick position
1063 static void osdFormatThrottlePosition(char *buff, bool autoThr, textAttributes_t *elemAttr)
1065 buff[0] = SYM_BLANK;
1066 buff[1] = SYM_THR;
1067 if (autoThr && navigationIsControllingThrottle()) {
1068 buff[0] = SYM_AUTO_THR0;
1069 buff[1] = SYM_AUTO_THR1;
1070 if (isFixedWingAutoThrottleManuallyIncreased()) {
1071 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1074 #ifdef USE_POWER_LIMITS
1075 if (powerLimiterIsLimiting()) {
1076 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1078 #endif
1079 tfp_sprintf(buff + 2, "%3d", getThrottlePercent());
1083 * Formats gvars prefixed by its number (0-indexed). If autoThr
1085 static void osdFormatGVar(char *buff, uint8_t index)
1087 buff[0] = 'G';
1088 buff[1] = '0'+index;
1089 buff[2] = ':';
1090 #ifdef USE_PROGRAMMING_FRAMEWORK
1091 osdFormatCentiNumber(buff + 3, (int32_t)gvGet(index)*(int32_t)100, 1, 0, 0, 5);
1092 #endif
1095 #if defined(USE_ESC_SENSOR)
1096 static void osdFormatRpm(char *buff, uint32_t rpm)
1098 buff[0] = SYM_RPM;
1099 if (rpm) {
1100 if ( digitCount(rpm) > osdConfig()->esc_rpm_precision) {
1101 uint8_t rpmMaxDecimals = (osdConfig()->esc_rpm_precision - 3);
1102 osdFormatCentiNumber(buff + 1, rpm / 10, 0, rpmMaxDecimals, rpmMaxDecimals, osdConfig()->esc_rpm_precision-1);
1103 buff[osdConfig()->esc_rpm_precision] = 'K';
1104 buff[osdConfig()->esc_rpm_precision+1] = '\0';
1106 else {
1107 switch(osdConfig()->esc_rpm_precision) {
1108 case 6:
1109 tfp_sprintf(buff + 1, "%6lu", rpm);
1110 break;
1111 case 5:
1112 tfp_sprintf(buff + 1, "%5lu", rpm);
1113 break;
1114 case 4:
1115 tfp_sprintf(buff + 1, "%4lu", rpm);
1116 break;
1117 case 3:
1118 default:
1119 tfp_sprintf(buff + 1, "%3lu", rpm);
1120 break;
1126 else {
1127 uint8_t buffPos = 1;
1128 while (buffPos <=( osdConfig()->esc_rpm_precision)) {
1129 strcpy(buff + buffPos++, "-");
1133 #endif
1135 int32_t osdGetAltitude(void)
1137 return getEstimatedActualPosition(Z);
1140 static inline int32_t osdGetAltitudeMsl(void)
1142 return getEstimatedActualPosition(Z)+GPS_home.alt;
1145 uint16_t osdGetRemainingGlideTime(void) {
1146 float value = getEstimatedActualVelocity(Z);
1147 static pt1Filter_t glideTimeFilterState;
1148 const timeMs_t curTimeMs = millis();
1149 static timeMs_t glideTimeUpdatedMs;
1151 value = pt1FilterApply4(&glideTimeFilterState, isnormal(value) ? value : 0, 0.5, MS2S(curTimeMs - glideTimeUpdatedMs));
1152 glideTimeUpdatedMs = curTimeMs;
1154 if (value < 0) {
1155 value = osdGetAltitude() / abs((int)value);
1156 } else {
1157 value = 0;
1160 return (uint16_t)round(value);
1163 static bool osdIsHeadingValid(void)
1165 return isImuHeadingValid();
1168 int16_t osdGetHeading(void)
1170 return attitude.values.yaw;
1173 int16_t osdPanServoHomeDirectionOffset(void)
1175 int8_t servoIndex = osdConfig()->pan_servo_index;
1176 int16_t servoPosition = servo[servoIndex];
1177 int16_t servoMiddle = servoParams(servoIndex)->middle;
1178 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition - servoMiddle) * osdConfig()->pan_servo_pwm2centideg);
1181 // Returns a heading angle in degrees normalized to [0, 360).
1182 int osdGetHeadingAngle(int angle)
1184 while (angle < 0) {
1185 angle += 360;
1187 while (angle >= 360) {
1188 angle -= 360;
1190 return angle;
1193 #if defined(USE_GPS)
1195 /* Draws a map with the given symbol in the center and given point of interest
1196 * defined by its distance in meters and direction in degrees.
1197 * referenceHeading indicates the up direction in the map, in degrees, while
1198 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1199 * arrow to indicate the map reference to the user. The drawn argument is an
1200 * in-out used to store the last position where the craft was drawn to avoid
1201 * erasing all screen on each redraw.
1203 static void osdDrawMap(int referenceHeading, uint16_t referenceSym, uint16_t centerSym,
1204 uint32_t poiDistance, int16_t poiDirection, uint16_t poiSymbol,
1205 uint16_t *drawn, uint32_t *usedScale)
1207 // TODO: These need to be tested with several setups. We might
1208 // need to make them configurable.
1209 const int hMargin = 5;
1210 const int vMargin = 3;
1212 // TODO: Get this from the display driver?
1213 const int charWidth = 12;
1214 const int charHeight = 18;
1216 uint8_t minX = hMargin;
1217 uint8_t maxX = osdDisplayPort->cols - 1 - hMargin;
1218 uint8_t minY = vMargin;
1219 uint8_t maxY = osdDisplayPort->rows - 1 - vMargin;
1220 uint8_t midX = osdDisplayPort->cols / 2;
1221 uint8_t midY = osdDisplayPort->rows / 2;
1223 // Fixed marks
1224 displayWriteChar(osdDisplayPort, midX, midY, centerSym);
1226 // First, erase the previous drawing.
1227 if (OSD_VISIBLE(*drawn)) {
1228 displayWriteChar(osdDisplayPort, OSD_X(*drawn), OSD_Y(*drawn), SYM_BLANK);
1229 *drawn = 0;
1232 uint32_t initialScale;
1233 const unsigned scaleMultiplier = 2;
1234 // We try to reduce the scale when the POI will be around half the distance
1235 // between the center and the closers map edge, to avoid too much jumping
1236 const int scaleReductionMultiplier = MIN(midX - hMargin, midY - vMargin) / 2;
1238 switch (osdConfig()->units) {
1239 case OSD_UNIT_UK:
1240 FALLTHROUGH;
1241 case OSD_UNIT_IMPERIAL:
1242 initialScale = 16; // 16m ~= 0.01miles
1243 break;
1244 case OSD_UNIT_GA:
1245 initialScale = 18; // 18m ~= 0.01 nautical miles
1246 break;
1247 default:
1248 case OSD_UNIT_METRIC_MPH:
1249 FALLTHROUGH;
1250 case OSD_UNIT_METRIC:
1251 initialScale = 10; // 10m as initial scale
1252 break;
1255 // Try to keep the same scale when getting closer until we draw over the center point
1256 uint32_t scale = initialScale;
1257 if (*usedScale) {
1258 scale = *usedScale;
1259 if (scale > initialScale && poiDistance < *usedScale * scaleReductionMultiplier) {
1260 scale /= scaleMultiplier;
1264 if (STATE(GPS_FIX)) {
1266 int directionToPoi = osdGetHeadingAngle(poiDirection - referenceHeading);
1267 float poiAngle = DEGREES_TO_RADIANS(directionToPoi);
1268 float poiSin = sin_approx(poiAngle);
1269 float poiCos = cos_approx(poiAngle);
1271 // Now start looking for a valid scale that lets us draw everything
1272 int ii;
1273 for (ii = 0; ii < 50; ii++) {
1274 // Calculate location of the aircraft in map
1275 int points = poiDistance / ((float)scale / charHeight);
1277 float pointsX = points * poiSin;
1278 int poiX = midX - roundf(pointsX / charWidth);
1279 if (poiX < minX || poiX > maxX) {
1280 scale *= scaleMultiplier;
1281 continue;
1284 float pointsY = points * poiCos;
1285 int poiY = midY + roundf(pointsY / charHeight);
1286 if (poiY < minY || poiY > maxY) {
1287 scale *= scaleMultiplier;
1288 continue;
1291 if (poiX == midX && poiY == midY) {
1292 // We're over the map center symbol, so we would be drawing
1293 // over it even if we increased the scale. Alternate between
1294 // drawing the center symbol or drawing the POI.
1295 if (centerSym != SYM_BLANK && OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1296 break;
1298 } else {
1300 uint16_t c;
1301 if (displayReadCharWithAttr(osdDisplayPort, poiX, poiY, &c, NULL) && c != SYM_BLANK) {
1302 // Something else written here, increase scale. If the display doesn't support reading
1303 // back characters, we assume there's nothing.
1305 // If we're close to the center, decrease scale. Otherwise increase it.
1306 uint8_t centerDeltaX = (maxX - minX) / (scaleMultiplier * 2);
1307 uint8_t centerDeltaY = (maxY - minY) / (scaleMultiplier * 2);
1308 if (poiX >= midX - centerDeltaX && poiX <= midX + centerDeltaX &&
1309 poiY >= midY - centerDeltaY && poiY <= midY + centerDeltaY &&
1310 scale > scaleMultiplier) {
1312 scale /= scaleMultiplier;
1313 } else {
1314 scale *= scaleMultiplier;
1316 continue;
1320 // Draw the point on the map
1321 if (poiSymbol == SYM_ARROW_UP) {
1322 // Drawing aircraft, rotate
1323 int mapHeading = osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading);
1324 poiSymbol += mapHeading * 2 / 45;
1326 displayWriteChar(osdDisplayPort, poiX, poiY, poiSymbol);
1328 // Update saved location
1329 *drawn = OSD_POS(poiX, poiY) | OSD_VISIBLE_FLAG;
1330 break;
1334 *usedScale = scale;
1336 // Update global map data for scale and reference
1337 osdMapData.scale = scale;
1338 osdMapData.referenceSymbol = referenceSym;
1341 /* Draws a map with the home in the center and the craft moving around.
1342 * See osdDrawMap() for reference.
1344 static void osdDrawHomeMap(int referenceHeading, uint8_t referenceSym, uint16_t *drawn, uint32_t *usedScale)
1346 osdDrawMap(referenceHeading, referenceSym, SYM_HOME, GPS_distanceToHome, GPS_directionToHome, SYM_ARROW_UP, drawn, usedScale);
1349 /* Draws a map with the aircraft in the center and the home moving around.
1350 * See osdDrawMap() for reference.
1352 static void osdDrawRadar(uint16_t *drawn, uint32_t *usedScale)
1354 int16_t reference = DECIDEGREES_TO_DEGREES(osdGetHeading());
1355 int16_t poiDirection = osdGetHeadingAngle(GPS_directionToHome + 180);
1356 osdDrawMap(reference, 0, SYM_ARROW_UP, GPS_distanceToHome, poiDirection, SYM_HOME, drawn, usedScale);
1359 static uint16_t crc_accumulate(uint8_t data, uint16_t crcAccum)
1361 uint8_t tmp;
1362 tmp = data ^ (uint8_t)(crcAccum & 0xff);
1363 tmp ^= (tmp << 4);
1364 crcAccum = (crcAccum >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
1365 return crcAccum;
1369 static void osdDisplayTelemetry(void)
1371 uint32_t trk_data;
1372 uint16_t trk_crc = 0;
1373 char trk_buffer[31];
1374 static int16_t trk_elevation = 127;
1375 static uint16_t trk_bearing = 0;
1377 if (ARMING_FLAG(ARMED)) {
1378 if (STATE(GPS_FIX)){
1379 if (GPS_distanceToHome > 5) {
1380 trk_bearing = GPS_directionToHome;
1381 trk_bearing += 360 + 180;
1382 trk_bearing %= 360;
1383 int32_t alt = CENTIMETERS_TO_METERS(osdGetAltitude());
1384 float at = atan2(alt, GPS_distanceToHome);
1385 trk_elevation = (float)at * 57.2957795; // 57.2957795 = 1 rad
1386 trk_elevation += 37; // because elevation in telemetry should be from -37 to 90
1387 if (trk_elevation < 0) {
1388 trk_elevation = 0;
1393 else{
1394 trk_elevation = 127;
1395 trk_bearing = 0;
1398 trk_data = 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1399 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.
1400 trk_data = trk_data | (uint32_t)trk_bearing << 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1401 trk_crc = crc_accumulate(0xFF & trk_data, trk_crc); // CRC First Byte bits 0-7
1402 trk_crc = crc_accumulate(0xFF & trk_bearing, trk_crc); // CRC Second Byte bits 8-15
1403 trk_crc = crc_accumulate(trk_bearing >> 8, trk_crc); // CRC Third Byte bits 16-17
1404 trk_data = trk_data | (uint32_t)trk_crc << 17; // bits 18-29 CRC & 0x3FFFF
1406 for (uint8_t t_ctr = 0; t_ctr < 30; t_ctr++) { // Prepare screen buffer and write data line.
1407 if (trk_data & (uint32_t)1 << t_ctr){
1408 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_0;
1410 else{
1411 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_1;
1414 trk_buffer[30] = 0;
1415 displayWrite(osdDisplayPort, 0, 0, trk_buffer);
1416 if (osdConfig()->telemetry>1){
1417 displayWrite(osdDisplayPort, 0, 3, trk_buffer); // Test display because normal telemetry line is not visible
1420 #endif
1422 static void osdFormatPidControllerOutput(char *buff, const char *label, const pidController_t *pidController, uint8_t scale, bool showDecimal) {
1423 strcpy(buff, label);
1424 for (uint8_t i = strlen(label); i < 5; ++i) buff[i] = ' ';
1425 uint8_t decimals = showDecimal ? 1 : 0;
1426 osdFormatCentiNumber(buff + 5, pidController->proportional * scale, 0, decimals, 0, 4);
1427 buff[9] = ' ';
1428 osdFormatCentiNumber(buff + 10, pidController->integrator * scale, 0, decimals, 0, 4);
1429 buff[14] = ' ';
1430 osdFormatCentiNumber(buff + 15, pidController->derivative * scale, 0, decimals, 0, 4);
1431 buff[19] = ' ';
1432 osdFormatCentiNumber(buff + 20, pidController->output_constrained * scale, 0, decimals, 0, 4);
1433 buff[24] = '\0';
1436 static void osdDisplayBatteryVoltage(uint8_t elemPosX, uint8_t elemPosY, uint16_t voltage, uint8_t digits, uint8_t decimals)
1438 char buff[7];
1439 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1441 osdFormatBatteryChargeSymbol(buff);
1442 buff[1] = '\0';
1443 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1444 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1446 elemAttr = TEXT_ATTRIBUTES_NONE;
1447 digits = MIN(digits, 5);
1448 osdFormatCentiNumber(buff, voltage, 0, decimals, 0, digits);
1449 buff[digits] = SYM_VOLT;
1450 buff[digits+1] = '\0';
1451 if ((getBatteryState() != BATTERY_NOT_PRESENT) && (getBatteryVoltage() <= getBatteryWarningVoltage()))
1452 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1453 displayWriteWithAttr(osdDisplayPort, elemPosX + 1, elemPosY, buff, elemAttr);
1456 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)
1458 textAttributes_t elemAttr;
1459 char buff[4];
1461 const pid8_t *pid = &pidBank()->pid[pidIndex];
1462 pidType_e pidType = pidIndexGetType(pidIndex);
1464 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1466 if (pidType == PID_TYPE_NONE) {
1467 // PID is not used in this configuration. Draw dashes.
1468 // XXX: Keep this in sync with the %3d format and spacing used below
1469 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - - -");
1470 return;
1473 elemAttr = TEXT_ATTRIBUTES_NONE;
1474 tfp_sprintf(buff, "%3d", pid->P);
1475 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1476 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1477 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1479 elemAttr = TEXT_ATTRIBUTES_NONE;
1480 tfp_sprintf(buff, "%3d", pid->I);
1481 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1482 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1483 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1485 elemAttr = TEXT_ATTRIBUTES_NONE;
1486 tfp_sprintf(buff, "%3d", pid->D);
1487 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1488 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1489 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1491 elemAttr = TEXT_ATTRIBUTES_NONE;
1492 tfp_sprintf(buff, "%3d", pid->FF);
1493 if ((isAdjustmentFunctionSelected(adjFuncFF)) || (((adjFuncFF == ADJUSTMENT_ROLL_FF) || (adjFuncFF == ADJUSTMENT_PITCH_FF)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF))))
1494 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1495 displayWriteWithAttr(osdDisplayPort, elemPosX + 16, elemPosY, buff, elemAttr);
1498 static void osdDisplayNavPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD)
1500 textAttributes_t elemAttr;
1501 char buff[4];
1503 const pid8_t *pid = &pidBank()->pid[pidIndex];
1504 pidType_e pidType = pidIndexGetType(pidIndex);
1506 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1508 if (pidType == PID_TYPE_NONE) {
1509 // PID is not used in this configuration. Draw dashes.
1510 // XXX: Keep this in sync with the %3d format and spacing used below
1511 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - -");
1512 return;
1515 elemAttr = TEXT_ATTRIBUTES_NONE;
1516 tfp_sprintf(buff, "%3d", pid->P);
1517 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1518 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1519 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1521 elemAttr = TEXT_ATTRIBUTES_NONE;
1522 tfp_sprintf(buff, "%3d", pid->I);
1523 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1524 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1525 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1527 elemAttr = TEXT_ATTRIBUTES_NONE;
1528 tfp_sprintf(buff, "%3d", pidType == PID_TYPE_PIFF ? pid->FF : pid->D);
1529 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1530 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1531 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1534 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) {
1535 char buff[8];
1536 textAttributes_t elemAttr;
1537 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1539 elemAttr = TEXT_ATTRIBUTES_NONE;
1540 osdFormatCentiNumber(buff, value * 100, 0, maxDecimals, 0, MIN(valueLength, 8));
1541 if (isAdjustmentFunctionSelected(adjFunc))
1542 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1543 displayWriteWithAttr(osdDisplayPort, elemPosX + strlen(str) + 1 + valueOffset, elemPosY, buff, elemAttr);
1546 int8_t getGeoWaypointNumber(int8_t waypointIndex)
1548 static int8_t lastWaypointIndex = 1;
1549 static int8_t geoWaypointIndex;
1551 if (waypointIndex != lastWaypointIndex) {
1552 lastWaypointIndex = geoWaypointIndex = waypointIndex;
1553 for (uint8_t i = posControl.startWpIndex; i <= waypointIndex; i++) {
1554 if (posControl.waypointList[i].action == NAV_WP_ACTION_SET_POI ||
1555 posControl.waypointList[i].action == NAV_WP_ACTION_SET_HEAD ||
1556 posControl.waypointList[i].action == NAV_WP_ACTION_JUMP) {
1557 geoWaypointIndex -= 1;
1562 return geoWaypointIndex - posControl.startWpIndex + 1;
1565 void osdDisplaySwitchIndicator(const char *swName, int rcValue, char *buff) {
1566 int8_t ptr = 0;
1568 if (osdConfig()->osd_switch_indicators_align_left) {
1569 for (ptr = 0; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH); ptr++) {
1570 buff[ptr] = swName[ptr];
1573 if ( rcValue < 1333) {
1574 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1575 } else if ( rcValue > 1666) {
1576 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1577 } else {
1578 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1580 } else {
1581 if ( rcValue < 1333) {
1582 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1583 } else if ( rcValue > 1666) {
1584 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1585 } else {
1586 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1589 for (ptr = 1; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH) + 1; ptr++) {
1590 buff[ptr] = swName[ptr-1];
1593 ptr++;
1596 buff[ptr] = '\0';
1599 static bool osdDrawSingleElement(uint8_t item)
1601 uint16_t pos = osdLayoutsConfig()->item_pos[currentLayout][item];
1602 if (!OSD_VISIBLE(pos)) {
1603 return false;
1605 uint8_t elemPosX = OSD_X(pos);
1606 uint8_t elemPosY = OSD_Y(pos);
1607 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1608 char buff[32] = {0};
1610 switch (item) {
1611 case OSD_RSSI_VALUE:
1613 uint16_t osdRssi = osdConvertRSSI();
1614 buff[0] = SYM_RSSI;
1615 tfp_sprintf(buff + 1, "%2d", osdRssi);
1616 if (osdRssi < osdConfig()->rssi_alarm) {
1617 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1619 break;
1622 case OSD_MAIN_BATT_VOLTAGE:
1623 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawVoltage(), (isBfCompatibleVideoSystem(osdConfig()) ? 3 : 2) + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1624 return true;
1626 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE:
1627 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedVoltage(), (isBfCompatibleVideoSystem(osdConfig()) ? 3 : 2) + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1628 return true;
1630 case OSD_CURRENT_DRAW:
1631 osdFormatCentiNumber(buff, getAmperage(), 0, 2, 0, 3);
1632 buff[3] = SYM_AMP;
1633 buff[4] = '\0';
1635 uint8_t current_alarm = osdConfig()->current_alarm;
1636 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
1637 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1639 break;
1641 case OSD_MAH_DRAWN: {
1643 if (isBfCompatibleVideoSystem(osdConfig())) {
1644 //BFcompat is unable to work with scaled values and it only has mAh symbol to work with
1645 tfp_sprintf(buff, "%4d", (int)getMAhDrawn());
1646 buff[4] = SYM_MAH;
1647 buff[5] = '\0';
1648 } else {
1649 if (osdFormatCentiNumber(buff, getMAhDrawn() * 100, 1000, 0, (osdConfig()->mAh_used_precision - 2), osdConfig()->mAh_used_precision)) {
1650 // Shown in mAh
1651 buff[osdConfig()->mAh_used_precision] = SYM_AH;
1652 } else {
1653 // Shown in Ah
1654 buff[osdConfig()->mAh_used_precision] = SYM_MAH;
1656 buff[(osdConfig()->mAh_used_precision + 1)] = '\0';
1658 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1659 break;
1661 case OSD_WH_DRAWN:
1662 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
1663 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1664 buff[3] = SYM_WH;
1665 buff[4] = '\0';
1666 break;
1668 case OSD_BATTERY_REMAINING_CAPACITY:
1670 if (currentBatteryProfile->capacity.value == 0)
1671 tfp_sprintf(buff, " NA");
1672 else if (!batteryWasFullWhenPluggedIn())
1673 tfp_sprintf(buff, " NF");
1674 else if (currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH)
1675 tfp_sprintf(buff, "%4lu", getBatteryRemainingCapacity());
1676 else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1677 osdFormatCentiNumber(buff + 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3);
1679 buff[4] = currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH ? SYM_MAH : SYM_WH;
1680 buff[5] = '\0';
1682 if ((getBatteryState() != BATTERY_NOT_PRESENT) && batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical))
1683 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1685 break;
1687 case OSD_BATTERY_REMAINING_PERCENT:
1688 osdFormatBatteryChargeSymbol(buff);
1689 tfp_sprintf(buff + 1, "%3d%%", calculateBatteryPercentage());
1690 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1691 break;
1693 case OSD_POWER_SUPPLY_IMPEDANCE:
1694 if (isPowerSupplyImpedanceValid())
1695 tfp_sprintf(buff, "%3d", getPowerSupplyImpedance());
1696 else
1697 strcpy(buff, "---");
1698 buff[3] = SYM_MILLIOHM;
1699 buff[4] = '\0';
1700 break;
1702 #ifdef USE_GPS
1703 case OSD_GPS_SATS:
1704 buff[0] = SYM_SAT_L;
1705 buff[1] = SYM_SAT_R;
1706 tfp_sprintf(buff + 2, "%2d", gpsSol.numSat);
1707 if (!STATE(GPS_FIX)) {
1708 if (getHwGPSStatus() == HW_SENSOR_UNAVAILABLE || getHwGPSStatus() == HW_SENSOR_UNHEALTHY) {
1709 strcpy(buff + 2, "X!");
1711 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1713 break;
1715 case OSD_GPS_SPEED:
1716 osdFormatVelocityStr(buff, gpsSol.groundSpeed, false, false);
1717 break;
1719 case OSD_GPS_MAX_SPEED:
1720 osdFormatVelocityStr(buff, stats.max_speed, false, true);
1721 break;
1723 case OSD_3D_SPEED:
1724 osdFormatVelocityStr(buff, osdGet3DSpeed(), true, false);
1725 break;
1727 case OSD_3D_MAX_SPEED:
1728 osdFormatVelocityStr(buff, stats.max_3D_speed, true, true);
1729 break;
1731 case OSD_GLIDESLOPE:
1733 float horizontalSpeed = gpsSol.groundSpeed;
1734 float sinkRate = -getEstimatedActualVelocity(Z);
1735 static pt1Filter_t gsFilterState;
1736 const timeMs_t currentTimeMs = millis();
1737 static timeMs_t gsUpdatedTimeMs;
1738 float glideSlope = horizontalSpeed / sinkRate;
1739 glideSlope = pt1FilterApply4(&gsFilterState, isnormal(glideSlope) ? glideSlope : 200, 0.5, MS2S(currentTimeMs - gsUpdatedTimeMs));
1740 gsUpdatedTimeMs = currentTimeMs;
1742 buff[0] = SYM_GLIDESLOPE;
1743 if (glideSlope > 0.0f && glideSlope < 100.0f) {
1744 osdFormatCentiNumber(buff + 1, glideSlope * 100.0f, 0, 2, 0, 3);
1745 } else {
1746 buff[1] = buff[2] = buff[3] = '-';
1748 buff[4] = '\0';
1749 break;
1752 case OSD_GPS_LAT:
1753 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
1754 break;
1756 case OSD_GPS_LON:
1757 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
1758 break;
1760 case OSD_HOME_DIR:
1762 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
1763 if (GPS_distanceToHome < (navConfig()->general.min_rth_distance / 100) ) {
1764 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_HOME_NEAR);
1766 else
1768 int16_t panHomeDirOffset = 0;
1769 if (!(osdConfig()->pan_servo_pwm2centideg == 0)){
1770 panHomeDirOffset = osdPanServoHomeDirectionOffset();
1772 int16_t flightDirection = STATE(AIRPLANE) ? CENTIDEGREES_TO_DEGREES(posControl.actualState.cog) : DECIDEGREES_TO_DEGREES(osdGetHeading());
1773 int homeDirection = GPS_directionToHome - flightDirection + panHomeDirOffset;
1774 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), homeDirection);
1776 } else {
1777 // No home or no fix or unknown heading, blink.
1778 // If we're unarmed, show the arrow pointing up so users can see the arrow
1779 // while configuring the OSD. If we're armed, show a '-' indicating that
1780 // we don't know the direction to home.
1781 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1782 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, ARMING_FLAG(ARMED) ? '-' : SYM_ARROW_UP, elemAttr);
1784 return true;
1787 case OSD_HOME_HEADING_ERROR:
1789 buff[0] = SYM_HOME;
1790 buff[1] = SYM_HEADING;
1792 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1793 int16_t h = lrintf(CENTIDEGREES_TO_DEGREES((float)wrap_18000(DEGREES_TO_CENTIDEGREES((int32_t)GPS_directionToHome) - (STATE(AIRPLANE) ? posControl.actualState.cog : DECIDEGREES_TO_CENTIDEGREES((int32_t)osdGetHeading())))));
1794 tfp_sprintf(buff + 2, "%4d", h);
1795 } else {
1796 strcpy(buff + 2, "----");
1799 buff[6] = SYM_DEGREES;
1800 buff[7] = '\0';
1801 break;
1804 case OSD_HOME_DIST:
1806 buff[0] = SYM_HOME;
1807 osdFormatDistanceSymbol(&buff[1], GPS_distanceToHome * 100, 0);
1808 uint16_t dist_alarm = osdConfig()->dist_alarm;
1809 if (dist_alarm > 0 && GPS_distanceToHome > dist_alarm) {
1810 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1813 break;
1815 case OSD_TRIP_DIST:
1816 buff[0] = SYM_TOTAL;
1817 osdFormatDistanceSymbol(buff + 1, getTotalTravelDistance(), 0);
1818 break;
1820 case OSD_GROUND_COURSE:
1822 buff[0] = SYM_GROUND_COURSE;
1823 if (osdIsHeadingValid()) {
1824 tfp_sprintf(&buff[1], "%3d", (int16_t)CENTIDEGREES_TO_DEGREES(posControl.actualState.cog));
1825 } else {
1826 buff[1] = buff[2] = buff[3] = '-';
1828 buff[4] = SYM_DEGREES;
1829 buff[5] = '\0';
1830 break;
1833 case OSD_COURSE_HOLD_ERROR:
1835 if (ARMING_FLAG(ARMED) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1836 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1837 return true;
1840 buff[0] = SYM_HEADING;
1842 if ((!ARMING_FLAG(ARMED)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && isAdjustingPosition())) {
1843 buff[1] = buff[2] = buff[3] = '-';
1844 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1845 int16_t herr = lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
1846 if (ABS(herr) > 99)
1847 strcpy(buff + 1, ">99");
1848 else
1849 tfp_sprintf(buff + 1, "%3d", herr);
1852 buff[4] = SYM_DEGREES;
1853 buff[5] = '\0';
1854 break;
1857 case OSD_COURSE_HOLD_ADJUSTMENT:
1859 int16_t heading_adjust = lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
1861 if (ARMING_FLAG(ARMED) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust != 0)))) {
1862 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1863 return true;
1866 buff[0] = SYM_HEADING;
1868 if (!ARMING_FLAG(ARMED)) {
1869 buff[1] = buff[2] = buff[3] = buff[4] = '-';
1870 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1871 tfp_sprintf(buff + 1, "%4d", heading_adjust);
1874 buff[5] = SYM_DEGREES;
1875 buff[6] = '\0';
1876 break;
1879 case OSD_CROSS_TRACK_ERROR:
1881 if (isWaypointNavTrackingActive()) {
1882 buff[0] = SYM_CROSS_TRACK_ERROR;
1883 osdFormatDistanceSymbol(buff + 1, navigationGetCrossTrackError(), 0);
1884 } else {
1885 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1886 return true;
1888 break;
1891 case OSD_GPS_HDOP:
1893 buff[0] = SYM_HDP_L;
1894 buff[1] = SYM_HDP_R;
1895 int32_t centiHDOP = 100 * gpsSol.hdop / HDOP_SCALE;
1896 osdFormatCentiNumber(&buff[2], centiHDOP, 0, 1, 0, 2);
1897 break;
1900 case OSD_MAP_NORTH:
1902 static uint16_t drawn = 0;
1903 static uint32_t scale = 0;
1904 osdDrawHomeMap(0, 'N', &drawn, &scale);
1905 return true;
1907 case OSD_MAP_TAKEOFF:
1909 static uint16_t drawn = 0;
1910 static uint32_t scale = 0;
1911 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn, &scale);
1912 return true;
1914 case OSD_RADAR:
1916 static uint16_t drawn = 0;
1917 static uint32_t scale = 0;
1918 osdDrawRadar(&drawn, &scale);
1919 return true;
1921 #endif // GPS
1923 case OSD_ALTITUDE:
1925 int32_t alt = osdGetAltitude();
1926 osdFormatAltitudeSymbol(buff, alt);
1927 uint16_t alt_alarm = osdConfig()->alt_alarm;
1928 uint16_t neg_alt_alarm = osdConfig()->neg_alt_alarm;
1929 if ((alt_alarm > 0 && CENTIMETERS_TO_METERS(alt) > alt_alarm) ||
1930 (neg_alt_alarm > 0 && alt < 0 && -CENTIMETERS_TO_METERS(alt) > neg_alt_alarm)) {
1932 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1934 break;
1937 case OSD_ALTITUDE_MSL:
1939 int32_t alt = osdGetAltitudeMsl();
1940 osdFormatAltitudeSymbol(buff, alt);
1941 break;
1944 #ifdef USE_RANGEFINDER
1945 case OSD_RANGEFINDER:
1947 int32_t range = rangefinderGetLatestRawAltitude();
1948 if (range < 0) {
1949 buff[0] = '-';
1950 } else {
1951 osdFormatDistanceSymbol(buff, range, 1);
1954 break;
1955 #endif
1957 case OSD_ONTIME:
1959 osdFormatOnTime(buff);
1960 break;
1963 case OSD_FLYTIME:
1965 osdFormatFlyTime(buff, &elemAttr);
1966 break;
1969 case OSD_ONTIME_FLYTIME:
1971 if (ARMING_FLAG(ARMED)) {
1972 osdFormatFlyTime(buff, &elemAttr);
1973 } else {
1974 osdFormatOnTime(buff);
1976 break;
1979 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH:
1981 /*static int32_t updatedTimeSeconds = 0;*/
1982 static int32_t timeSeconds = -1;
1983 #if defined(USE_ADC) && defined(USE_GPS)
1984 static timeUs_t updatedTimestamp = 0;
1985 timeUs_t currentTimeUs = micros();
1986 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
1987 #ifdef USE_WIND_ESTIMATOR
1988 timeSeconds = calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation);
1989 #else
1990 timeSeconds = calculateRemainingFlightTimeBeforeRTH(false);
1991 #endif
1992 updatedTimestamp = currentTimeUs;
1994 #endif
1995 if ((!ARMING_FLAG(ARMED)) || (timeSeconds == -1)) {
1996 buff[0] = SYM_FLIGHT_MINS_REMAINING;
1997 strcpy(buff + 1, "--:--");
1998 #if defined(USE_ADC) && defined(USE_GPS)
1999 updatedTimestamp = 0;
2000 #endif
2001 } else if (timeSeconds == -2) {
2002 // Wind is too strong to come back with cruise throttle
2003 buff[0] = SYM_FLIGHT_MINS_REMAINING;
2004 buff[1] = buff[2] = buff[4] = buff[5] = SYM_WIND_HORIZONTAL;
2005 buff[3] = ':';
2006 buff[6] = '\0';
2007 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2008 } else {
2009 osdFormatTime(buff, timeSeconds, SYM_FLIGHT_MINS_REMAINING, SYM_FLIGHT_HOURS_REMAINING);
2010 if (timeSeconds == 0)
2011 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2014 break;
2016 case OSD_REMAINING_DISTANCE_BEFORE_RTH:;
2017 static int32_t distanceMeters = -1;
2018 #if defined(USE_ADC) && defined(USE_GPS)
2019 static timeUs_t updatedTimestamp = 0;
2020 timeUs_t currentTimeUs = micros();
2021 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
2022 #ifdef USE_WIND_ESTIMATOR
2023 distanceMeters = calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation);
2024 #else
2025 distanceMeters = calculateRemainingDistanceBeforeRTH(false);
2026 #endif
2027 updatedTimestamp = currentTimeUs;
2029 #endif
2030 //buff[0] = SYM_TRIP_DIST;
2031 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_FLIGHT_DIST_REMAINING);
2032 if ((!ARMING_FLAG(ARMED)) || (distanceMeters == -1)) {
2033 buff[4] = SYM_BLANK;
2034 buff[5] = '\0';
2035 strcpy(buff + 1, "---");
2036 } else if (distanceMeters == -2) {
2037 // Wind is too strong to come back with cruise throttle
2038 buff[1] = buff[2] = buff[3] = SYM_WIND_HORIZONTAL;
2039 switch ((osd_unit_e)osdConfig()->units){
2040 case OSD_UNIT_UK:
2041 FALLTHROUGH;
2042 case OSD_UNIT_IMPERIAL:
2043 buff[4] = SYM_DIST_MI;
2044 break;
2045 case OSD_UNIT_METRIC_MPH:
2046 FALLTHROUGH;
2047 case OSD_UNIT_METRIC:
2048 buff[4] = SYM_DIST_KM;
2049 break;
2050 case OSD_UNIT_GA:
2051 buff[4] = SYM_DIST_NM;
2052 break;
2054 buff[5] = '\0';
2055 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2056 } else {
2057 osdFormatDistanceSymbol(buff + 1, distanceMeters * 100, 0);
2058 if (distanceMeters == 0)
2059 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2061 break;
2063 case OSD_FLYMODE:
2065 char *p = "ACRO";
2067 if (FLIGHT_MODE(FAILSAFE_MODE))
2068 p = "!FS!";
2069 else if (FLIGHT_MODE(MANUAL_MODE))
2070 p = "MANU";
2071 else if (FLIGHT_MODE(TURTLE_MODE))
2072 p = "TURT";
2073 else if (FLIGHT_MODE(NAV_RTH_MODE))
2074 p = isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
2075 else if (FLIGHT_MODE(NAV_POSHOLD_MODE))
2076 p = "HOLD";
2077 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && FLIGHT_MODE(NAV_ALTHOLD_MODE))
2078 p = "CRUZ";
2079 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE))
2080 p = "CRSH";
2081 else if (FLIGHT_MODE(NAV_WP_MODE))
2082 p = " WP ";
2083 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && navigationRequiresAngleMode()) {
2084 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
2085 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
2086 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
2087 p = " AH ";
2089 else if (FLIGHT_MODE(ANGLE_MODE))
2090 p = "ANGL";
2091 else if (FLIGHT_MODE(HORIZON_MODE))
2092 p = "HOR ";
2094 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
2095 return true;
2098 case OSD_CRAFT_NAME:
2099 osdFormatCraftName(buff);
2100 break;
2102 case OSD_PILOT_NAME:
2103 osdFormatPilotName(buff);
2104 break;
2106 case OSD_THROTTLE_POS:
2108 osdFormatThrottlePosition(buff, false, &elemAttr);
2109 break;
2112 case OSD_VTX_CHANNEL:
2114 vtxDeviceOsdInfo_t osdInfo;
2115 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2117 tfp_sprintf(buff, "CH:%c%s:", osdInfo.bandLetter, osdInfo.channelName);
2118 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2120 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2121 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2122 displayWriteWithAttr(osdDisplayPort, elemPosX + 6, elemPosY, buff, elemAttr);
2123 return true;
2125 break;
2127 case OSD_VTX_POWER:
2129 vtxDeviceOsdInfo_t osdInfo;
2130 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2132 tfp_sprintf(buff, "%c", SYM_VTX_POWER);
2133 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2135 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2136 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2137 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2138 return true;
2141 #if defined(USE_SERIALRX_CRSF)
2142 case OSD_CRSF_RSSI_DBM:
2144 int16_t rssi = rxLinkStatistics.uplinkRSSI;
2145 buff[0] = (rxLinkStatistics.activeAntenna == 0) ? SYM_RSSI : SYM_2RSS; // Separate symbols for each antenna
2146 if (rssi <= -100) {
2147 tfp_sprintf(buff + 1, "%4d%c", rssi, SYM_DBM);
2148 } else {
2149 tfp_sprintf(buff + 1, "%3d%c%c", rssi, SYM_DBM, ' ');
2151 if (!failsafeIsReceivingRxData()){
2152 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2153 } else if (osdConfig()->rssi_dbm_alarm && rssi < osdConfig()->rssi_dbm_alarm) {
2154 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2156 break;
2158 case OSD_CRSF_LQ:
2160 buff[0] = SYM_LQ;
2161 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
2162 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
2163 switch (osdConfig()->crsf_lq_format) {
2164 case OSD_CRSF_LQ_TYPE1:
2165 if (!failsafeIsReceivingRxData()) {
2166 tfp_sprintf(buff+1, "%3d", 0);
2167 } else {
2168 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.uplinkLQ);
2170 break;
2171 case OSD_CRSF_LQ_TYPE2:
2172 if (!failsafeIsReceivingRxData()) {
2173 tfp_sprintf(buff+1, "%s:%3d", " ", 0);
2174 } else {
2175 tfp_sprintf(buff+1, "%d:%3d", rxLinkStatistics.rfMode, rxLinkStatistics.uplinkLQ);
2177 break;
2178 case OSD_CRSF_LQ_TYPE3:
2179 if (!failsafeIsReceivingRxData()) {
2180 tfp_sprintf(buff+1, "%3d", 0);
2181 } else {
2182 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.rfMode >= 2 ? scaledLQ : rxLinkStatistics.uplinkLQ);
2184 break;
2186 if (!failsafeIsReceivingRxData()) {
2187 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2188 } else if (rxLinkStatistics.uplinkLQ < osdConfig()->link_quality_alarm) {
2189 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2191 break;
2194 case OSD_CRSF_SNR_DB:
2196 static pt1Filter_t snrFilterState;
2197 static timeMs_t snrUpdated = 0;
2198 int8_t snrFiltered = pt1FilterApply4(&snrFilterState, rxLinkStatistics.uplinkSNR, 0.5f, MS2S(millis() - snrUpdated));
2199 snrUpdated = millis();
2201 const char* showsnr = "-20";
2202 const char* hidesnr = " ";
2203 if (snrFiltered > osdConfig()->snr_alarm) {
2204 if (cmsInMenu) {
2205 buff[0] = SYM_SNR;
2206 tfp_sprintf(buff + 1, "%s%c", showsnr, SYM_DB);
2207 } else {
2208 buff[0] = SYM_BLANK;
2209 tfp_sprintf(buff + 1, "%s%c", hidesnr, SYM_BLANK);
2211 } else if (snrFiltered <= osdConfig()->snr_alarm) {
2212 buff[0] = SYM_SNR;
2213 if (snrFiltered <= -10) {
2214 tfp_sprintf(buff + 1, "%3d%c", snrFiltered, SYM_DB);
2215 } else {
2216 tfp_sprintf(buff + 1, "%2d%c%c", snrFiltered, SYM_DB, ' ');
2219 break;
2222 case OSD_CRSF_TX_POWER:
2224 if (!failsafeIsReceivingRxData())
2225 tfp_sprintf(buff, "%s%c", " ", SYM_BLANK);
2226 else
2227 tfp_sprintf(buff, "%4d%c", rxLinkStatistics.uplinkTXPower, SYM_MW);
2228 break;
2230 #endif
2232 case OSD_CROSSHAIRS: // Hud is a sub-element of the crosshair
2234 osdCrosshairPosition(&elemPosX, &elemPosY);
2235 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX, elemPosY);
2237 if (osdConfig()->hud_homing && STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
2238 osdHudDrawHoming(elemPosX, elemPosY);
2241 if (STATE(GPS_FIX) && isImuHeadingValid()) {
2243 if (osdConfig()->hud_homepoint || osdConfig()->hud_radar_disp > 0 || osdConfig()->hud_wp_disp > 0) {
2244 osdHudClear();
2247 // -------- POI : Home point
2249 if (osdConfig()->hud_homepoint) { // Display the home point (H)
2250 osdHudDrawPoi(GPS_distanceToHome, GPS_directionToHome, -osdGetAltitude() / 100, 0, SYM_HOME, 0 , 0);
2253 // -------- POI : Nearby aircrafts from ESP32 radar
2255 if (osdConfig()->hud_radar_disp > 0) { // Display the POI from the radar
2256 for (uint8_t i = 0; i < osdConfig()->hud_radar_disp; i++) {
2257 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
2258 fpVector3_t poi;
2259 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &radar_pois[i].gps, GEO_ALT_RELATIVE);
2260 radar_pois[i].distance = calculateDistanceToDestination(&poi) / 100; // In meters
2262 if (radar_pois[i].distance >= osdConfig()->hud_radar_range_min && radar_pois[i].distance <= osdConfig()->hud_radar_range_max) {
2263 radar_pois[i].direction = calculateBearingToDestination(&poi) / 100; // In °
2264 radar_pois[i].altitude = (radar_pois[i].gps.alt - osdGetAltitudeMsl()) / 100;
2265 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);
2271 // -------- POI : Next waypoints from navigation
2273 if (osdConfig()->hud_wp_disp > 0 && posControl.waypointListValid && posControl.waypointCount > 0) { // Display the next waypoints
2274 gpsLocation_t wp2;
2275 int j;
2277 for (int i = osdConfig()->hud_wp_disp - 1; i >= 0 ; i--) { // Display in reverse order so the next WP is always written on top
2278 j = posControl.activeWaypointIndex + i;
2279 if (j > posControl.startWpIndex + posControl.waypointCount - 1) { // limit to max WP index for mission
2280 break;
2282 if (posControl.waypointList[j].lat != 0 && posControl.waypointList[j].lon != 0) {
2283 wp2.lat = posControl.waypointList[j].lat;
2284 wp2.lon = posControl.waypointList[j].lon;
2285 wp2.alt = posControl.waypointList[j].alt;
2286 fpVector3_t poi;
2287 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &wp2, waypointMissionAltConvMode(posControl.waypointList[j].p3));
2288 int32_t altConvModeAltitude = waypointMissionAltConvMode(posControl.waypointList[j].p3) == GEO_ALT_ABSOLUTE ? osdGetAltitudeMsl() : osdGetAltitude();
2289 j = getGeoWaypointNumber(j);
2290 while (j > 9) j -= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2291 osdHudDrawPoi(calculateDistanceToDestination(&poi) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi) / 100), (posControl.waypointList[j].alt - altConvModeAltitude)/ 100, 2, SYM_WAYPOINT, 48 + j, i);
2297 return true;
2298 break;
2300 case OSD_ATTITUDE_ROLL:
2301 buff[0] = SYM_ROLL_LEVEL;
2302 if (ABS(attitude.values.roll) >= 1)
2303 buff[0] += (attitude.values.roll < 0 ? -1 : 1);
2304 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.roll)), 0, 1, 0, 3);
2305 break;
2307 case OSD_ATTITUDE_PITCH:
2308 if (ABS(attitude.values.pitch) < 1)
2309 buff[0] = 'P';
2310 else if (attitude.values.pitch > 0)
2311 buff[0] = SYM_PITCH_DOWN;
2312 else if (attitude.values.pitch < 0)
2313 buff[0] = SYM_PITCH_UP;
2314 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.pitch)), 0, 1, 0, 3);
2315 break;
2317 case OSD_ARTIFICIAL_HORIZON:
2319 float rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2320 float pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2322 pitchAngle -= osdConfig()->ahi_camera_uptilt_comp ? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt) : 0;
2323 pitchAngle += DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2324 if (osdConfig()->ahi_reverse_roll) {
2325 rollAngle = -rollAngle;
2327 osdDrawArtificialHorizon(osdDisplayPort, osdGetDisplayPortCanvas(),
2328 OSD_DRAW_POINT_GRID(elemPosX, elemPosY), rollAngle, pitchAngle);
2329 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
2330 osdDrawSingleElement(OSD_CROSSHAIRS);
2332 return true;
2335 case OSD_HORIZON_SIDEBARS:
2337 osdDrawSidebars(osdDisplayPort, osdGetDisplayPortCanvas());
2338 return true;
2341 #if defined(USE_BARO) || defined(USE_GPS)
2342 case OSD_VARIO:
2344 float zvel = getEstimatedActualVelocity(Z);
2345 osdDrawVario(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), zvel);
2346 return true;
2349 case OSD_VARIO_NUM:
2351 int16_t value = getEstimatedActualVelocity(Z);
2352 char sym;
2353 switch ((osd_unit_e)osdConfig()->units) {
2354 case OSD_UNIT_UK:
2355 FALLTHROUGH;
2356 case OSD_UNIT_IMPERIAL:
2357 // Convert to centifeet/s
2358 value = CENTIMETERS_TO_CENTIFEET(value);
2359 sym = SYM_FTS;
2360 break;
2361 case OSD_UNIT_GA:
2362 // Convert to centi-100feet/min
2363 value = CENTIMETERS_TO_FEET(value * 60);
2364 sym = SYM_100FTM;
2365 break;
2366 default:
2367 case OSD_UNIT_METRIC_MPH:
2368 FALLTHROUGH;
2369 case OSD_UNIT_METRIC:
2370 // Already in cm/s
2371 sym = SYM_MS;
2372 break;
2375 osdFormatCentiNumber(buff, value, 0, 1, 0, 3);
2376 buff[3] = sym;
2377 buff[4] = '\0';
2378 break;
2380 case OSD_CLIMB_EFFICIENCY:
2382 // amperage is in centi amps (10mA), vertical speed is in cms/s. We want
2383 // Ah/dist only to show when vertical speed > 1m/s.
2384 static pt1Filter_t veFilterState;
2385 static timeUs_t vEfficiencyUpdated = 0;
2386 int32_t value = 0;
2387 timeUs_t currentTimeUs = micros();
2388 timeDelta_t vEfficiencyTimeDelta = cmpTimeUs(currentTimeUs, vEfficiencyUpdated);
2389 if (getEstimatedActualVelocity(Z) > 0) {
2390 if (vEfficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2391 // Centiamps (kept for osdFormatCentiNumber) / m/s - Will appear as A / m/s in OSD
2392 value = pt1FilterApply4(&veFilterState, (float)getAmperage() / (getEstimatedActualVelocity(Z) / 100.0f), 1, US2S(vEfficiencyTimeDelta));
2394 vEfficiencyUpdated = currentTimeUs;
2395 } else {
2396 value = veFilterState.state;
2399 bool efficiencyValid = (value > 0) && (getEstimatedActualVelocity(Z) > 100);
2400 switch (osdConfig()->units) {
2401 case OSD_UNIT_UK:
2402 FALLTHROUGH;
2403 case OSD_UNIT_GA:
2404 FALLTHROUGH;
2405 case OSD_UNIT_IMPERIAL:
2406 // mAh/foot
2407 if (efficiencyValid) {
2408 osdFormatCentiNumber(buff, (value * METERS_PER_FOOT), 1, 2, 2, 3);
2409 tfp_sprintf(buff, "%s%c%c", buff, SYM_AH_V_FT_0, SYM_AH_V_FT_1);
2410 } else {
2411 buff[0] = buff[1] = buff[2] = '-';
2412 buff[3] = SYM_AH_V_FT_0;
2413 buff[4] = SYM_AH_V_FT_1;
2414 buff[5] = '\0';
2416 break;
2417 case OSD_UNIT_METRIC_MPH:
2418 FALLTHROUGH;
2419 case OSD_UNIT_METRIC:
2420 // mAh/metre
2421 if (efficiencyValid) {
2422 osdFormatCentiNumber(buff, value, 1, 2, 2, 3);
2423 tfp_sprintf(buff, "%s%c%c", buff, SYM_AH_V_M_0, SYM_AH_V_M_1);
2424 } else {
2425 buff[0] = buff[1] = buff[2] = '-';
2426 buff[3] = SYM_AH_V_M_0;
2427 buff[4] = SYM_AH_V_M_1;
2428 buff[5] = '\0';
2430 break;
2432 break;
2434 case OSD_GLIDE_TIME_REMAINING:
2436 uint16_t glideTime = osdGetRemainingGlideTime();
2437 buff[0] = SYM_GLIDE_MINS;
2438 if (glideTime > 0) {
2439 // Maximum value we can show in minutes is 99 minutes and 59 seconds. It is extremely unlikely that glide
2440 // time will be longer than 99 minutes. If it is, it will show 99:^^
2441 if (glideTime > (99 * 60) + 59) {
2442 tfp_sprintf(buff + 1, "%02d:", (int)(glideTime / 60));
2443 buff[4] = SYM_DIRECTION;
2444 buff[5] = SYM_DIRECTION;
2445 } else {
2446 tfp_sprintf(buff + 1, "%02d:%02d", (int)(glideTime / 60), (int)(glideTime % 60));
2448 } else {
2449 tfp_sprintf(buff + 1, "%s", "--:--");
2451 buff[6] = '\0';
2452 break;
2454 case OSD_GLIDE_RANGE:
2456 uint16_t glideSeconds = osdGetRemainingGlideTime();
2457 buff[0] = SYM_GLIDE_DIST;
2458 if (glideSeconds > 0) {
2459 uint32_t glideRangeCM = glideSeconds * gpsSol.groundSpeed;
2460 osdFormatDistanceSymbol(buff + 1, glideRangeCM, 0);
2461 } else {
2462 tfp_sprintf(buff + 1, "%s%c", "---", SYM_BLANK);
2463 buff[5] = '\0';
2465 break;
2467 #endif
2469 case OSD_SWITCH_INDICATOR_0:
2470 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channel - 1), buff);
2471 break;
2473 case OSD_SWITCH_INDICATOR_1:
2474 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channel - 1), buff);
2475 break;
2477 case OSD_SWITCH_INDICATOR_2:
2478 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channel - 1), buff);
2479 break;
2481 case OSD_SWITCH_INDICATOR_3:
2482 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channel - 1), buff);
2483 break;
2485 case OSD_ACTIVE_PROFILE:
2486 tfp_sprintf(buff, "%c%u", SYM_PROFILE, (getConfigProfile() + 1));
2487 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2488 break;
2490 case OSD_ROLL_PIDS:
2491 osdDisplayFlightPIDValues(elemPosX, elemPosY, "ROL", PID_ROLL, ADJUSTMENT_ROLL_P, ADJUSTMENT_ROLL_I, ADJUSTMENT_ROLL_D, ADJUSTMENT_ROLL_FF);
2492 return true;
2494 case OSD_PITCH_PIDS:
2495 osdDisplayFlightPIDValues(elemPosX, elemPosY, "PIT", PID_PITCH, ADJUSTMENT_PITCH_P, ADJUSTMENT_PITCH_I, ADJUSTMENT_PITCH_D, ADJUSTMENT_PITCH_FF);
2496 return true;
2498 case OSD_YAW_PIDS:
2499 osdDisplayFlightPIDValues(elemPosX, elemPosY, "YAW", PID_YAW, ADJUSTMENT_YAW_P, ADJUSTMENT_YAW_I, ADJUSTMENT_YAW_D, ADJUSTMENT_YAW_FF);
2500 return true;
2502 case OSD_LEVEL_PIDS:
2503 osdDisplayNavPIDValues(elemPosX, elemPosY, "LEV", PID_LEVEL, ADJUSTMENT_LEVEL_P, ADJUSTMENT_LEVEL_I, ADJUSTMENT_LEVEL_D);
2504 return true;
2506 case OSD_POS_XY_PIDS:
2507 osdDisplayNavPIDValues(elemPosX, elemPosY, "PXY", PID_POS_XY, ADJUSTMENT_POS_XY_P, ADJUSTMENT_POS_XY_I, ADJUSTMENT_POS_XY_D);
2508 return true;
2510 case OSD_POS_Z_PIDS:
2511 osdDisplayNavPIDValues(elemPosX, elemPosY, "PZ", PID_POS_Z, ADJUSTMENT_POS_Z_P, ADJUSTMENT_POS_Z_I, ADJUSTMENT_POS_Z_D);
2512 return true;
2514 case OSD_VEL_XY_PIDS:
2515 osdDisplayNavPIDValues(elemPosX, elemPosY, "VXY", PID_VEL_XY, ADJUSTMENT_VEL_XY_P, ADJUSTMENT_VEL_XY_I, ADJUSTMENT_VEL_XY_D);
2516 return true;
2518 case OSD_VEL_Z_PIDS:
2519 osdDisplayNavPIDValues(elemPosX, elemPosY, "VZ", PID_VEL_Z, ADJUSTMENT_VEL_Z_P, ADJUSTMENT_VEL_Z_I, ADJUSTMENT_VEL_Z_D);
2520 return true;
2522 case OSD_HEADING_P:
2523 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "HP", 0, pidBank()->pid[PID_HEADING].P, 3, 0, ADJUSTMENT_HEADING_P);
2524 return true;
2526 case OSD_BOARD_ALIGN_ROLL:
2527 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT);
2528 return true;
2530 case OSD_BOARD_ALIGN_PITCH:
2531 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT);
2532 return true;
2534 case OSD_RC_EXPO:
2535 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "EXP", 0, currentControlRateProfile->stabilized.rcExpo8, 3, 0, ADJUSTMENT_RC_EXPO);
2536 return true;
2538 case OSD_RC_YAW_EXPO:
2539 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "YEX", 0, currentControlRateProfile->stabilized.rcYawExpo8, 3, 0, ADJUSTMENT_RC_YAW_EXPO);
2540 return true;
2542 case OSD_THROTTLE_EXPO:
2543 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TEX", 0, currentControlRateProfile->throttle.rcExpo8, 3, 0, ADJUSTMENT_THROTTLE_EXPO);
2544 return true;
2546 case OSD_PITCH_RATE:
2547 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SPR");
2549 elemAttr = TEXT_ATTRIBUTES_NONE;
2550 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_PITCH]);
2551 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2552 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2553 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2554 return true;
2556 case OSD_ROLL_RATE:
2557 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SRR");
2559 elemAttr = TEXT_ATTRIBUTES_NONE;
2560 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_ROLL]);
2561 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2562 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2563 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2564 return true;
2566 case OSD_YAW_RATE:
2567 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "SYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2568 return true;
2570 case OSD_MANUAL_RC_EXPO:
2571 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MEX", 0, currentControlRateProfile->manual.rcExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO);
2572 return true;
2574 case OSD_MANUAL_RC_YAW_EXPO:
2575 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYX", 0, currentControlRateProfile->manual.rcYawExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO);
2576 return true;
2578 case OSD_MANUAL_PITCH_RATE:
2579 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MPR");
2581 elemAttr = TEXT_ATTRIBUTES_NONE;
2582 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_PITCH]);
2583 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2584 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2585 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2586 return true;
2588 case OSD_MANUAL_ROLL_RATE:
2589 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MRR");
2591 elemAttr = TEXT_ATTRIBUTES_NONE;
2592 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_ROLL]);
2593 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2594 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2595 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2596 return true;
2598 case OSD_MANUAL_YAW_RATE:
2599 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2600 return true;
2602 case OSD_NAV_FW_CRUISE_THR:
2603 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CRZ", 0, currentBatteryProfile->nav.fw.cruise_throttle, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR);
2604 return true;
2606 case OSD_NAV_FW_PITCH2THR:
2607 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "P2T", 0, currentBatteryProfile->nav.fw.pitch_to_throttle, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR);
2608 return true;
2610 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE:
2611 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "0TP", 0, (float)navConfig()->fw.minThrottleDownPitchAngle / 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE);
2612 return true;
2614 case OSD_FW_ALT_PID_OUTPUTS:
2616 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2617 osdFormatPidControllerOutput(buff, "PZO", &nav_pids->fw_alt, 10, true); // display requested pitch degrees
2618 break;
2621 case OSD_FW_POS_PID_OUTPUTS:
2623 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers(); // display requested roll degrees
2624 osdFormatPidControllerOutput(buff, "PXYO", &nav_pids->fw_nav, 1, true);
2625 break;
2628 case OSD_MC_VEL_Z_PID_OUTPUTS:
2630 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2631 osdFormatPidControllerOutput(buff, "VZO", &nav_pids->vel[Z], 100, false); // display throttle adjustment µs
2632 break;
2635 case OSD_MC_VEL_X_PID_OUTPUTS:
2637 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2638 osdFormatPidControllerOutput(buff, "VXO", &nav_pids->vel[X], 100, false); // display requested acceleration cm/s^2
2639 break;
2642 case OSD_MC_VEL_Y_PID_OUTPUTS:
2644 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2645 osdFormatPidControllerOutput(buff, "VYO", &nav_pids->vel[Y], 100, false); // display requested acceleration cm/s^2
2646 break;
2649 case OSD_MC_POS_XYZ_P_OUTPUTS:
2651 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2652 strcpy(buff, "POSO ");
2653 // display requested velocity cm/s
2654 tfp_sprintf(buff + 5, "%4d", (int)lrintf(nav_pids->pos[X].output_constrained * 100));
2655 buff[9] = ' ';
2656 tfp_sprintf(buff + 10, "%4d", (int)lrintf(nav_pids->pos[Y].output_constrained * 100));
2657 buff[14] = ' ';
2658 tfp_sprintf(buff + 15, "%4d", (int)lrintf(nav_pids->pos[Z].output_constrained * 100));
2659 buff[19] = '\0';
2660 break;
2663 case OSD_POWER:
2665 bool kiloWatt = osdFormatCentiNumber(buff, getPower(), 1000, 2, 2, 3);
2666 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
2667 buff[4] = '\0';
2669 uint8_t current_alarm = osdConfig()->current_alarm;
2670 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
2671 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2673 break;
2676 case OSD_AIR_SPEED:
2678 #ifdef USE_PITOT
2679 const float airspeed_estimate = getAirspeedEstimate();
2680 buff[0] = SYM_AIR;
2681 osdFormatVelocityStr(buff + 1, airspeed_estimate, false, false);
2683 if ((osdConfig()->airspeed_alarm_min != 0 && airspeed_estimate < osdConfig()->airspeed_alarm_min) ||
2684 (osdConfig()->airspeed_alarm_max != 0 && airspeed_estimate > osdConfig()->airspeed_alarm_max)) {
2685 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2687 #else
2688 return false;
2689 #endif
2690 break;
2693 case OSD_AIR_MAX_SPEED:
2695 #ifdef USE_PITOT
2696 buff[0] = SYM_MAX;
2697 buff[1] = SYM_AIR;
2698 osdFormatVelocityStr(buff + 2, stats.max_air_speed, false, false);
2699 #else
2700 return false;
2701 #endif
2702 break;
2705 case OSD_RTC_TIME:
2707 // RTC not configured will show 00:00
2708 dateTime_t dateTime;
2709 rtcGetDateTimeLocal(&dateTime);
2710 buff[0] = SYM_CLOCK;
2711 tfp_sprintf(buff + 1, "%02u:%02u:%02u", dateTime.hours, dateTime.minutes, dateTime.seconds);
2712 break;
2715 case OSD_MESSAGES:
2717 elemAttr = osdGetSystemMessage(buff, OSD_MESSAGE_LENGTH, true);
2718 break;
2721 case OSD_VERSION:
2723 tfp_sprintf(buff, "INAV %s", FC_VERSION_STRING);
2724 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2725 break;
2728 case OSD_MAIN_BATT_CELL_VOLTAGE:
2730 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawAverageCellVoltage(), (isBfCompatibleVideoSystem(osdConfig()) ? 4 : 3), 2);
2731 return true;
2734 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE:
2736 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedAverageCellVoltage(), (isBfCompatibleVideoSystem(osdConfig()) ? 4 : 3), 2);
2737 return true;
2740 case OSD_THROTTLE_POS_AUTO_THR:
2742 osdFormatThrottlePosition(buff, true, &elemAttr);
2743 break;
2746 case OSD_HEADING:
2748 buff[0] = SYM_HEADING;
2749 if (osdIsHeadingValid()) {
2750 int16_t h = DECIDEGREES_TO_DEGREES(osdGetHeading());
2751 if (h < 0) {
2752 h += 360;
2754 tfp_sprintf(&buff[1], "%3d", h);
2755 } else {
2756 buff[1] = buff[2] = buff[3] = '-';
2758 buff[4] = SYM_DEGREES;
2759 buff[5] = '\0';
2760 break;
2763 case OSD_HEADING_GRAPH:
2765 if (osdIsHeadingValid()) {
2766 osdDrawHeadingGraph(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), osdGetHeading());
2767 return true;
2768 } else {
2769 buff[0] = buff[2] = buff[4] = buff[6] = buff[8] = SYM_HEADING_LINE;
2770 buff[1] = buff[3] = buff[5] = buff[7] = SYM_HEADING_DIVIDED_LINE;
2771 buff[OSD_HEADING_GRAPH_WIDTH] = '\0';
2773 break;
2776 case OSD_EFFICIENCY_MAH_PER_KM:
2778 // amperage is in centi amps, speed is in cms/s. We want
2779 // mah/km. Only show when ground speed > 1m/s.
2780 static pt1Filter_t eFilterState;
2781 static timeUs_t efficiencyUpdated = 0;
2782 int32_t value = 0;
2783 bool moreThanAh = false;
2784 timeUs_t currentTimeUs = micros();
2785 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2786 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2787 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2788 value = pt1FilterApply4(&eFilterState, ((float)getAmperage() / gpsSol.groundSpeed) / 0.0036f,
2789 1, US2S(efficiencyTimeDelta));
2791 efficiencyUpdated = currentTimeUs;
2792 } else {
2793 value = eFilterState.state;
2796 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2797 switch (osdConfig()->units) {
2798 case OSD_UNIT_UK:
2799 FALLTHROUGH;
2800 case OSD_UNIT_IMPERIAL:
2801 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10, 1000, 0, 2, 3);
2802 if (!moreThanAh) {
2803 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
2804 } else {
2805 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
2807 if (!efficiencyValid) {
2808 buff[0] = buff[1] = buff[2] = '-';
2809 buff[3] = SYM_MAH_MI_0;
2810 buff[4] = SYM_MAH_MI_1;
2811 buff[5] = '\0';
2813 break;
2814 case OSD_UNIT_GA:
2815 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10, 1000, 0, 2, 3);
2816 if (!moreThanAh) {
2817 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
2818 } else {
2819 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
2821 if (!efficiencyValid) {
2822 buff[0] = buff[1] = buff[2] = '-';
2823 buff[3] = SYM_MAH_NM_0;
2824 buff[4] = SYM_MAH_NM_1;
2825 buff[5] = '\0';
2827 break;
2828 case OSD_UNIT_METRIC_MPH:
2829 FALLTHROUGH;
2830 case OSD_UNIT_METRIC:
2831 moreThanAh = osdFormatCentiNumber(buff, value * 100, 1000, 0, 2, 3);
2832 if (!moreThanAh) {
2833 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
2834 } else {
2835 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
2837 if (!efficiencyValid) {
2838 buff[0] = buff[1] = buff[2] = '-';
2839 buff[3] = SYM_MAH_KM_0;
2840 buff[4] = SYM_MAH_KM_1;
2841 buff[5] = '\0';
2843 break;
2845 break;
2848 case OSD_EFFICIENCY_WH_PER_KM:
2850 // amperage is in centi amps, speed is in cms/s. We want
2851 // mWh/km. Only show when ground speed > 1m/s.
2852 static pt1Filter_t eFilterState;
2853 static timeUs_t efficiencyUpdated = 0;
2854 int32_t value = 0;
2855 timeUs_t currentTimeUs = micros();
2856 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2857 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2858 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2859 value = pt1FilterApply4(&eFilterState, ((float)getPower() / gpsSol.groundSpeed) / 0.0036f,
2860 1, US2S(efficiencyTimeDelta));
2862 efficiencyUpdated = currentTimeUs;
2863 } else {
2864 value = eFilterState.state;
2867 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2868 switch (osdConfig()->units) {
2869 case OSD_UNIT_UK:
2870 FALLTHROUGH;
2871 case OSD_UNIT_IMPERIAL:
2872 osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10000, 0, 2, 0, 3);
2873 buff[3] = SYM_WH_MI;
2874 break;
2875 case OSD_UNIT_GA:
2876 osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10000, 0, 2, 0, 3);
2877 buff[3] = SYM_WH_NM;
2878 break;
2879 case OSD_UNIT_METRIC_MPH:
2880 FALLTHROUGH;
2881 case OSD_UNIT_METRIC:
2882 osdFormatCentiNumber(buff, value / 10, 0, 2, 0, 3);
2883 buff[3] = SYM_WH_KM;
2884 break;
2886 buff[4] = '\0';
2887 if (!efficiencyValid) {
2888 buff[0] = buff[1] = buff[2] = '-';
2890 break;
2893 case OSD_GFORCE:
2895 buff[0] = SYM_GFORCE;
2896 osdFormatCentiNumber(buff + 1, GForce, 0, 2, 0, 3);
2897 if (GForce > osdConfig()->gforce_alarm * 100) {
2898 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2900 break;
2903 case OSD_GFORCE_X:
2904 case OSD_GFORCE_Y:
2905 case OSD_GFORCE_Z:
2907 float GForceValue = GForceAxis[item - OSD_GFORCE_X];
2908 buff[0] = SYM_GFORCE_X + item - OSD_GFORCE_X;
2909 osdFormatCentiNumber(buff + 1, GForceValue, 0, 2, 0, 4);
2910 if ((GForceValue < osdConfig()->gforce_axis_alarm_min * 100) || (GForceValue > osdConfig()->gforce_axis_alarm_max * 100)) {
2911 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2913 break;
2915 case OSD_DEBUG:
2918 * Longest representable string is -2147483648 does not fit in the screen.
2919 * Only 7 digits for negative and 8 digits for positive values allowed
2921 for (uint8_t bufferIndex = 0; bufferIndex < DEBUG32_VALUE_COUNT; ++elemPosY, bufferIndex += 2) {
2922 tfp_sprintf(
2923 buff,
2924 "[%u]=%8ld [%u]=%8ld",
2925 bufferIndex,
2926 constrain(debug[bufferIndex], -9999999, 99999999),
2927 bufferIndex+1,
2928 constrain(debug[bufferIndex+1], -9999999, 99999999)
2930 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2932 break;
2935 case OSD_IMU_TEMPERATURE:
2937 int16_t temperature;
2938 const bool valid = getIMUTemperature(&temperature);
2939 osdDisplayTemperature(elemPosX, elemPosY, SYM_IMU_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2940 return true;
2943 case OSD_BARO_TEMPERATURE:
2945 int16_t temperature;
2946 const bool valid = getBaroTemperature(&temperature);
2947 osdDisplayTemperature(elemPosX, elemPosY, SYM_BARO_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2948 return true;
2951 #ifdef USE_TEMPERATURE_SENSOR
2952 case OSD_TEMP_SENSOR_0_TEMPERATURE:
2953 case OSD_TEMP_SENSOR_1_TEMPERATURE:
2954 case OSD_TEMP_SENSOR_2_TEMPERATURE:
2955 case OSD_TEMP_SENSOR_3_TEMPERATURE:
2956 case OSD_TEMP_SENSOR_4_TEMPERATURE:
2957 case OSD_TEMP_SENSOR_5_TEMPERATURE:
2958 case OSD_TEMP_SENSOR_6_TEMPERATURE:
2959 case OSD_TEMP_SENSOR_7_TEMPERATURE:
2961 osdDisplayTemperatureSensor(elemPosX, elemPosY, item - OSD_TEMP_SENSOR_0_TEMPERATURE);
2962 return true;
2964 #endif /* ifdef USE_TEMPERATURE_SENSOR */
2966 case OSD_WIND_SPEED_HORIZONTAL:
2967 #ifdef USE_WIND_ESTIMATOR
2969 bool valid = isEstimatedWindSpeedValid();
2970 float horizontalWindSpeed;
2971 uint16_t angle;
2972 horizontalWindSpeed = getEstimatedHorizontalWindSpeed(&angle);
2973 int16_t windDirection = osdGetHeadingAngle( CENTIDEGREES_TO_DEGREES((int)angle) - DECIDEGREES_TO_DEGREES(attitude.values.yaw) + 22);
2974 buff[0] = SYM_WIND_HORIZONTAL;
2975 buff[1] = SYM_DIRECTION + (windDirection*2 / 90);
2976 osdFormatWindSpeedStr(buff + 2, horizontalWindSpeed, valid);
2977 break;
2979 #else
2980 return false;
2981 #endif
2983 case OSD_WIND_SPEED_VERTICAL:
2984 #ifdef USE_WIND_ESTIMATOR
2986 buff[0] = SYM_WIND_VERTICAL;
2987 buff[1] = SYM_BLANK;
2988 bool valid = isEstimatedWindSpeedValid();
2989 float verticalWindSpeed;
2990 verticalWindSpeed = -getEstimatedWindSpeed(Z); //from NED to NEU
2991 if (verticalWindSpeed < 0) {
2992 buff[1] = SYM_AH_DECORATION_DOWN;
2993 verticalWindSpeed = -verticalWindSpeed;
2994 } else {
2995 buff[1] = SYM_AH_DECORATION_UP;
2997 osdFormatWindSpeedStr(buff + 2, verticalWindSpeed, valid);
2998 break;
3000 #else
3001 return false;
3002 #endif
3004 case OSD_PLUS_CODE:
3006 STATIC_ASSERT(GPS_DEGREES_DIVIDER == OLC_DEG_MULTIPLIER, invalid_olc_deg_multiplier);
3007 int digits = osdConfig()->plus_code_digits;
3008 int digitsRemoved = osdConfig()->plus_code_short * 2;
3009 if (STATE(GPS_FIX)) {
3010 olc_encode(gpsSol.llh.lat, gpsSol.llh.lon, digits, buff, sizeof(buff));
3011 } else {
3012 // +codes with > 8 digits have a + at the 9th digit
3013 // and we only support 10 and up.
3014 memset(buff, '-', digits + 1);
3015 buff[8] = '+';
3016 buff[digits + 1] = '\0';
3018 // Optionally trim digits from the left
3019 memmove(buff, buff+digitsRemoved, strlen(buff) + digitsRemoved);
3020 buff[digits + 1 - digitsRemoved] = '\0';
3021 break;
3024 case OSD_AZIMUTH:
3027 buff[0] = SYM_AZIMUTH;
3028 if (osdIsHeadingValid()) {
3029 int16_t h = GPS_directionToHome;
3030 if (h < 0) {
3031 h += 360;
3033 if (h >= 180)
3034 h = h - 180;
3035 else
3036 h = h + 180;
3038 tfp_sprintf(&buff[1], "%3d", h);
3039 } else {
3040 buff[1] = buff[2] = buff[3] = '-';
3042 buff[4] = SYM_DEGREES;
3043 buff[5] = '\0';
3044 break;
3047 case OSD_MAP_SCALE:
3049 float scaleToUnit;
3050 int scaleUnitDivisor;
3051 char symUnscaled;
3052 char symScaled;
3053 int maxDecimals;
3055 switch (osdConfig()->units) {
3056 case OSD_UNIT_UK:
3057 FALLTHROUGH;
3058 case OSD_UNIT_IMPERIAL:
3059 scaleToUnit = 100 / 1609.3440f; // scale to 0.01mi for osdFormatCentiNumber()
3060 scaleUnitDivisor = 0;
3061 symUnscaled = SYM_MI;
3062 symScaled = SYM_MI;
3063 maxDecimals = 2;
3064 break;
3065 case OSD_UNIT_GA:
3066 scaleToUnit = 100 / 1852.0010f; // scale to 0.01mi for osdFormatCentiNumber()
3067 scaleUnitDivisor = 0;
3068 symUnscaled = SYM_NM;
3069 symScaled = SYM_NM;
3070 maxDecimals = 2;
3071 break;
3072 default:
3073 case OSD_UNIT_METRIC_MPH:
3074 FALLTHROUGH;
3075 case OSD_UNIT_METRIC:
3076 scaleToUnit = 100; // scale to cm for osdFormatCentiNumber()
3077 scaleUnitDivisor = 1000; // Convert to km when scale gets bigger than 999m
3078 symUnscaled = SYM_M;
3079 symScaled = SYM_KM;
3080 maxDecimals = 0;
3081 break;
3083 buff[0] = SYM_SCALE;
3084 if (osdMapData.scale > 0) {
3085 bool scaled = osdFormatCentiNumber(&buff[1], osdMapData.scale * scaleToUnit, scaleUnitDivisor, maxDecimals, 2, 3);
3086 buff[4] = scaled ? symScaled : symUnscaled;
3087 // Make sure this is cleared if the map stops being drawn
3088 osdMapData.scale = 0;
3089 } else {
3090 memset(&buff[1], '-', 4);
3092 buff[5] = '\0';
3093 break;
3095 case OSD_MAP_REFERENCE:
3097 char referenceSymbol;
3098 if (osdMapData.referenceSymbol) {
3099 referenceSymbol = osdMapData.referenceSymbol;
3100 // Make sure this is cleared if the map stops being drawn
3101 osdMapData.referenceSymbol = 0;
3102 } else {
3103 referenceSymbol = '-';
3105 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DIRECTION);
3106 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 1, referenceSymbol);
3107 return true;
3110 case OSD_GVAR_0:
3112 osdFormatGVar(buff, 0);
3113 break;
3115 case OSD_GVAR_1:
3117 osdFormatGVar(buff, 1);
3118 break;
3120 case OSD_GVAR_2:
3122 osdFormatGVar(buff, 2);
3123 break;
3125 case OSD_GVAR_3:
3127 osdFormatGVar(buff, 3);
3128 break;
3131 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3132 case OSD_RC_SOURCE:
3134 const char *source_text = IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
3135 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3136 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, source_text, elemAttr);
3137 return true;
3139 #endif
3141 #if defined(USE_ESC_SENSOR)
3142 case OSD_ESC_RPM:
3144 escSensorData_t * escSensor = escSensorGetData();
3145 if (escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE) {
3146 osdFormatRpm(buff, escSensor->rpm);
3148 else {
3149 osdFormatRpm(buff, 0);
3151 break;
3153 case OSD_ESC_TEMPERATURE:
3155 escSensorData_t * escSensor = escSensorGetData();
3156 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
3157 osdDisplayTemperature(elemPosX, elemPosY, SYM_ESC_TEMP, NULL, escTemperatureValid, (escSensor->temperature)*10, osdConfig()->esc_temp_alarm_min, osdConfig()->esc_temp_alarm_max);
3158 return true;
3160 #endif
3161 case OSD_TPA:
3163 char buff[4];
3164 textAttributes_t attr;
3166 displayWrite(osdDisplayPort, elemPosX, elemPosY, "TPA");
3167 attr = TEXT_ATTRIBUTES_NONE;
3168 tfp_sprintf(buff, "%3d", currentControlRateProfile->throttle.dynPID);
3169 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA)) {
3170 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3172 displayWriteWithAttr(osdDisplayPort, elemPosX + 5, elemPosY, buff, attr);
3174 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, "BP");
3175 attr = TEXT_ATTRIBUTES_NONE;
3176 tfp_sprintf(buff, "%4d", currentControlRateProfile->throttle.pa_breakpoint);
3177 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT)) {
3178 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3180 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY + 1, buff, attr);
3182 return true;
3184 case OSD_TPA_TIME_CONSTANT:
3186 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TPA TC", 0, currentControlRateProfile->throttle.fixedWingTauMs, 4, 0, ADJUSTMENT_FW_TPA_TIME_CONSTANT);
3187 return true;
3189 case OSD_FW_LEVEL_TRIM:
3191 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "LEVEL", 0, pidProfileMutable()->fixedWingLevelTrim, 3, 1, ADJUSTMENT_FW_LEVEL_TRIM);
3192 return true;
3195 case OSD_NAV_FW_CONTROL_SMOOTHNESS:
3197 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CTL S", 0, navConfig()->fw.control_smoothness, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS);
3198 return true;
3200 #ifdef USE_MULTI_MISSION
3201 case OSD_NAV_WP_MULTI_MISSION_INDEX:
3203 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "WP NO", 0, navConfig()->general.waypoint_multi_mission_index, 1, 0, ADJUSTMENT_NAV_WP_MULTI_MISSION_INDEX);
3204 return true;
3206 #endif
3207 case OSD_MISSION:
3209 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION)) {
3210 char buf[5];
3211 switch (posControl.wpMissionPlannerStatus) {
3212 case WP_PLAN_WAIT:
3213 strcpy(buf, "WAIT");
3214 break;
3215 case WP_PLAN_SAVE:
3216 strcpy(buf, "SAVE");
3217 break;
3218 case WP_PLAN_OK:
3219 strcpy(buf, " OK ");
3220 break;
3221 case WP_PLAN_FULL:
3222 strcpy(buf, "FULL");
3224 tfp_sprintf(buff, "%s>%2uWP", buf, posControl.wpPlannerActiveWPIndex);
3225 } else if (posControl.wpPlannerActiveWPIndex){
3226 tfp_sprintf(buff, "PLAN>%2uWP", posControl.waypointCount); // mission planner mision active
3228 #ifdef USE_MULTI_MISSION
3229 else {
3230 if (ARMING_FLAG(ARMED) && !(IS_RC_MODE_ACTIVE(BOXCHANGEMISSION) && posControl.multiMissionCount > 1)){
3231 // Limit field size when Armed, only show selected mission
3232 tfp_sprintf(buff, "M%u ", posControl.loadedMultiMissionIndex);
3233 } else if (posControl.multiMissionCount) {
3234 if (navConfig()->general.waypoint_multi_mission_index != posControl.loadedMultiMissionIndex) {
3235 tfp_sprintf(buff, "M%u/%u>LOAD", navConfig()->general.waypoint_multi_mission_index, posControl.multiMissionCount);
3236 } else {
3237 if (posControl.waypointListValid && posControl.waypointCount > 0) {
3238 tfp_sprintf(buff, "M%u/%u>%2uWP", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
3239 } else {
3240 tfp_sprintf(buff, "M0/%u> 0WP", posControl.multiMissionCount);
3243 } else { // no multi mission loaded - show active WP count from other source
3244 tfp_sprintf(buff, "WP CNT>%2u", posControl.waypointCount);
3247 #endif
3248 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3249 return true;
3252 #ifdef USE_POWER_LIMITS
3253 case OSD_PLIMIT_REMAINING_BURST_TIME:
3254 osdFormatCentiNumber(buff, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3);
3255 buff[3] = 'S';
3256 buff[4] = '\0';
3257 break;
3259 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT:
3260 if (currentBatteryProfile->powerLimits.continuousCurrent) {
3261 osdFormatCentiNumber(buff, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3);
3262 buff[3] = SYM_AMP;
3263 buff[4] = '\0';
3265 if (powerLimiterIsLimitingCurrent()) {
3266 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3269 break;
3271 #ifdef USE_ADC
3272 case OSD_PLIMIT_ACTIVE_POWER_LIMIT:
3274 if (currentBatteryProfile->powerLimits.continuousPower) {
3275 bool kiloWatt = osdFormatCentiNumber(buff, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3);
3276 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3277 buff[4] = '\0';
3279 if (powerLimiterIsLimitingPower()) {
3280 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3283 break;
3285 #endif // USE_ADC
3286 #endif // USE_POWER_LIMITS
3288 default:
3289 return false;
3292 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
3293 return true;
3296 uint8_t osdIncElementIndex(uint8_t elementIndex)
3298 ++elementIndex;
3300 if (elementIndex == OSD_ARTIFICIAL_HORIZON) { // always drawn last so skip
3301 elementIndex++;
3304 #ifndef USE_TEMPERATURE_SENSOR
3305 if (elementIndex == OSD_TEMP_SENSOR_0_TEMPERATURE) {
3306 elementIndex = OSD_ALTITUDE_MSL;
3308 #endif
3310 if (!(feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER))) {
3311 if (elementIndex == OSD_POWER) {
3312 elementIndex = OSD_GPS_LON;
3314 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3315 elementIndex = OSD_LEVEL_PIDS;
3317 #ifdef USE_POWER_LIMITS
3318 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3319 elementIndex = OSD_GLIDESLOPE;
3321 #endif
3324 #ifndef USE_POWER_LIMITS
3325 if (elementIndex == OSD_PLIMIT_REMAINING_BURST_TIME) {
3326 elementIndex = OSD_GLIDESLOPE;
3328 #endif
3330 if (!feature(FEATURE_CURRENT_METER)) {
3331 if (elementIndex == OSD_CURRENT_DRAW) {
3332 elementIndex = OSD_GPS_SPEED;
3334 if (elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3335 elementIndex = OSD_BATTERY_REMAINING_PERCENT;
3337 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3338 elementIndex = OSD_TRIP_DIST;
3340 if (elementIndex == OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH) {
3341 elementIndex = OSD_HOME_HEADING_ERROR;
3343 if (elementIndex == OSD_CLIMB_EFFICIENCY) {
3344 elementIndex = OSD_NAV_WP_MULTI_MISSION_INDEX;
3348 if (!STATE(ESC_SENSOR_ENABLED)) {
3349 if (elementIndex == OSD_ESC_RPM) {
3350 elementIndex = OSD_AZIMUTH;
3354 if (!feature(FEATURE_GPS)) {
3355 if (elementIndex == OSD_GPS_HDOP || elementIndex == OSD_TRIP_DIST || elementIndex == OSD_3D_SPEED || elementIndex == OSD_MISSION ||
3356 elementIndex == OSD_AZIMUTH || elementIndex == OSD_BATTERY_REMAINING_CAPACITY || elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3357 elementIndex++;
3359 if (elementIndex == OSD_HEADING_GRAPH && !sensors(SENSOR_MAG)) {
3360 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_WH_DRAWN : OSD_BATTERY_REMAINING_PERCENT;
3362 if (elementIndex == OSD_EFFICIENCY_WH_PER_KM) {
3363 elementIndex = OSD_ATTITUDE_PITCH;
3365 if (elementIndex == OSD_GPS_SPEED) {
3366 elementIndex = OSD_ALTITUDE;
3368 if (elementIndex == OSD_GPS_LON) {
3369 elementIndex = sensors(SENSOR_MAG) ? OSD_HEADING : OSD_VARIO;
3371 if (elementIndex == OSD_MAP_NORTH) {
3372 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE : OSD_LEVEL_PIDS;
3374 if (elementIndex == OSD_PLUS_CODE) {
3375 elementIndex = OSD_GFORCE;
3377 if (elementIndex == OSD_GLIDESLOPE) {
3378 elementIndex = OSD_AIR_MAX_SPEED;
3380 if (elementIndex == OSD_GLIDE_RANGE) {
3381 elementIndex = feature(FEATURE_CURRENT_METER) ? OSD_CLIMB_EFFICIENCY : OSD_ITEM_COUNT;
3383 if (elementIndex == OSD_NAV_WP_MULTI_MISSION_INDEX) {
3384 elementIndex = OSD_ITEM_COUNT;
3388 if (!sensors(SENSOR_ACC)) {
3389 if (elementIndex == OSD_CROSSHAIRS) {
3390 elementIndex = OSD_ONTIME;
3392 if (elementIndex == OSD_GFORCE) {
3393 elementIndex = OSD_RC_SOURCE;
3397 if (elementIndex == OSD_ITEM_COUNT) {
3398 elementIndex = 0;
3400 return elementIndex;
3403 void osdDrawNextElement(void)
3405 static uint8_t elementIndex = 0;
3406 // Flag for end of loop, also prevents infinite loop when no elements are enabled
3407 uint8_t index = elementIndex;
3408 do {
3409 elementIndex = osdIncElementIndex(elementIndex);
3410 } while (!osdDrawSingleElement(elementIndex) && index != elementIndex);
3412 // Draw artificial horizon + tracking telemtry last
3413 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
3414 if (osdConfig()->telemetry>0){
3415 osdDisplayTelemetry();
3419 PG_RESET_TEMPLATE(osdConfig_t, osdConfig,
3420 .rssi_alarm = SETTING_OSD_RSSI_ALARM_DEFAULT,
3421 .time_alarm = SETTING_OSD_TIME_ALARM_DEFAULT,
3422 .alt_alarm = SETTING_OSD_ALT_ALARM_DEFAULT,
3423 .dist_alarm = SETTING_OSD_DIST_ALARM_DEFAULT,
3424 .neg_alt_alarm = SETTING_OSD_NEG_ALT_ALARM_DEFAULT,
3425 .current_alarm = SETTING_OSD_CURRENT_ALARM_DEFAULT,
3426 .imu_temp_alarm_min = SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT,
3427 .imu_temp_alarm_max = SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT,
3428 .esc_temp_alarm_min = SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT,
3429 .esc_temp_alarm_max = SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT,
3430 .gforce_alarm = SETTING_OSD_GFORCE_ALARM_DEFAULT,
3431 .gforce_axis_alarm_min = SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT,
3432 .gforce_axis_alarm_max = SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT,
3433 #ifdef USE_BARO
3434 .baro_temp_alarm_min = SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT,
3435 .baro_temp_alarm_max = SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT,
3436 #endif
3437 #ifdef USE_SERIALRX_CRSF
3438 .snr_alarm = SETTING_OSD_SNR_ALARM_DEFAULT,
3439 .crsf_lq_format = SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT,
3440 .link_quality_alarm = SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT,
3441 .rssi_dbm_alarm = SETTING_OSD_RSSI_DBM_ALARM_DEFAULT,
3442 .rssi_dbm_max = SETTING_OSD_RSSI_DBM_MAX_DEFAULT,
3443 .rssi_dbm_min = SETTING_OSD_RSSI_DBM_MIN_DEFAULT,
3444 #endif
3445 #ifdef USE_TEMPERATURE_SENSOR
3446 .temp_label_align = SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT,
3447 #endif
3448 #ifdef USE_PITOT
3449 .airspeed_alarm_min = SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT,
3450 .airspeed_alarm_max = SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT,
3451 #endif
3453 .video_system = SETTING_OSD_VIDEO_SYSTEM_DEFAULT,
3454 .row_shiftdown = SETTING_OSD_ROW_SHIFTDOWN_DEFAULT,
3455 .msp_displayport_fullframe_interval = SETTING_OSD_MSP_DISPLAYPORT_FULLFRAME_INTERVAL_DEFAULT,
3457 .ahi_reverse_roll = SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT,
3458 .ahi_max_pitch = SETTING_OSD_AHI_MAX_PITCH_DEFAULT,
3459 .crosshairs_style = SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT,
3460 .horizon_offset = SETTING_OSD_HORIZON_OFFSET_DEFAULT,
3461 .camera_uptilt = SETTING_OSD_CAMERA_UPTILT_DEFAULT,
3462 .ahi_camera_uptilt_comp = SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT,
3463 .camera_fov_h = SETTING_OSD_CAMERA_FOV_H_DEFAULT,
3464 .camera_fov_v = SETTING_OSD_CAMERA_FOV_V_DEFAULT,
3465 .hud_margin_h = SETTING_OSD_HUD_MARGIN_H_DEFAULT,
3466 .hud_margin_v = SETTING_OSD_HUD_MARGIN_V_DEFAULT,
3467 .hud_homing = SETTING_OSD_HUD_HOMING_DEFAULT,
3468 .hud_homepoint = SETTING_OSD_HUD_HOMEPOINT_DEFAULT,
3469 .hud_radar_disp = SETTING_OSD_HUD_RADAR_DISP_DEFAULT,
3470 .hud_radar_range_min = SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT,
3471 .hud_radar_range_max = SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT,
3472 .hud_radar_alt_difference_display_time = SETTING_OSD_HUD_RADAR_ALT_DIFFERENCE_DISPLAY_TIME_DEFAULT,
3473 .hud_radar_distance_display_time = SETTING_OSD_HUD_RADAR_DISTANCE_DISPLAY_TIME_DEFAULT,
3474 .hud_wp_disp = SETTING_OSD_HUD_WP_DISP_DEFAULT,
3475 .left_sidebar_scroll = SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT,
3476 .right_sidebar_scroll = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT,
3477 .sidebar_scroll_arrows = SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT,
3478 .sidebar_horizontal_offset = SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT,
3479 .left_sidebar_scroll_step = SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT,
3480 .right_sidebar_scroll_step = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT,
3481 .sidebar_height = SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT,
3482 .ahi_pitch_interval = SETTING_OSD_AHI_PITCH_INTERVAL_DEFAULT,
3483 .osd_home_position_arm_screen = SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT,
3484 .pan_servo_index = SETTING_OSD_PAN_SERVO_INDEX_DEFAULT,
3485 .pan_servo_pwm2centideg = SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT,
3486 .esc_rpm_precision = SETTING_OSD_ESC_RPM_PRECISION_DEFAULT,
3487 .mAh_used_precision = SETTING_OSD_MAH_USED_PRECISION_DEFAULT,
3488 .osd_switch_indicator0_name = SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT,
3489 .osd_switch_indicator0_channel = SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNEL_DEFAULT,
3490 .osd_switch_indicator1_name = SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT,
3491 .osd_switch_indicator1_channel = SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNEL_DEFAULT,
3492 .osd_switch_indicator2_name = SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT,
3493 .osd_switch_indicator2_channel = SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNEL_DEFAULT,
3494 .osd_switch_indicator3_name = SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT,
3495 .osd_switch_indicator3_channel = SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNEL_DEFAULT,
3496 .osd_switch_indicators_align_left = SETTING_OSD_SWITCH_INDICATORS_ALIGN_LEFT_DEFAULT,
3497 .system_msg_display_time = SETTING_OSD_SYSTEM_MSG_DISPLAY_TIME_DEFAULT,
3498 .units = SETTING_OSD_UNITS_DEFAULT,
3499 .main_voltage_decimals = SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT,
3501 #ifdef USE_WIND_ESTIMATOR
3502 .estimations_wind_compensation = SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT,
3503 #endif
3505 .coordinate_digits = SETTING_OSD_COORDINATE_DIGITS_DEFAULT,
3507 .osd_failsafe_switch_layout = SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT,
3509 .plus_code_digits = SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT,
3510 .plus_code_short = SETTING_OSD_PLUS_CODE_SHORT_DEFAULT,
3512 .ahi_width = SETTING_OSD_AHI_WIDTH_DEFAULT,
3513 .ahi_height = SETTING_OSD_AHI_HEIGHT_DEFAULT,
3514 .ahi_vertical_offset = SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT,
3515 .ahi_bordered = SETTING_OSD_AHI_BORDERED_DEFAULT,
3516 .ahi_style = SETTING_OSD_AHI_STYLE_DEFAULT,
3518 .force_grid = SETTING_OSD_FORCE_GRID_DEFAULT,
3520 .stats_energy_unit = SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT,
3521 .stats_min_voltage_unit = SETTING_OSD_STATS_MIN_VOLTAGE_UNIT_DEFAULT,
3522 .stats_page_auto_swap_time = SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT
3525 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t *osdLayoutsConfig)
3527 osdLayoutsConfig->item_pos[0][OSD_ALTITUDE] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG;
3528 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_VOLTAGE] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG;
3529 osdLayoutsConfig->item_pos[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE] = OSD_POS(12, 1);
3531 osdLayoutsConfig->item_pos[0][OSD_RSSI_VALUE] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG;
3532 //line 2
3533 osdLayoutsConfig->item_pos[0][OSD_HOME_DIST] = OSD_POS(1, 1);
3534 osdLayoutsConfig->item_pos[0][OSD_TRIP_DIST] = OSD_POS(1, 2);
3535 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_CELL_VOLTAGE] = OSD_POS(12, 1);
3536 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE] = OSD_POS(12, 1);
3537 osdLayoutsConfig->item_pos[0][OSD_GPS_SPEED] = OSD_POS(23, 1);
3538 osdLayoutsConfig->item_pos[0][OSD_3D_SPEED] = OSD_POS(23, 1);
3539 osdLayoutsConfig->item_pos[0][OSD_GLIDESLOPE] = OSD_POS(23, 2);
3541 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG;
3542 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS_AUTO_THR] = OSD_POS(6, 2);
3543 osdLayoutsConfig->item_pos[0][OSD_HEADING] = OSD_POS(12, 2);
3544 osdLayoutsConfig->item_pos[0][OSD_GROUND_COURSE] = OSD_POS(12, 3);
3545 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ERROR] = OSD_POS(12, 2);
3546 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ADJUSTMENT] = OSD_POS(12, 2);
3547 osdLayoutsConfig->item_pos[0][OSD_CROSS_TRACK_ERROR] = OSD_POS(12, 3);
3548 osdLayoutsConfig->item_pos[0][OSD_HEADING_GRAPH] = OSD_POS(18, 2);
3549 osdLayoutsConfig->item_pos[0][OSD_CURRENT_DRAW] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG;
3550 osdLayoutsConfig->item_pos[0][OSD_MAH_DRAWN] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG;
3551 osdLayoutsConfig->item_pos[0][OSD_WH_DRAWN] = OSD_POS(1, 5);
3552 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_CAPACITY] = OSD_POS(1, 6);
3553 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_PERCENT] = OSD_POS(1, 7);
3554 osdLayoutsConfig->item_pos[0][OSD_POWER_SUPPLY_IMPEDANCE] = OSD_POS(1, 8);
3556 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_MAH_PER_KM] = OSD_POS(1, 5);
3557 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_WH_PER_KM] = OSD_POS(1, 5);
3559 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_ROLL] = OSD_POS(1, 7);
3560 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_PITCH] = OSD_POS(1, 8);
3562 // avoid OSD_VARIO under OSD_CROSSHAIRS
3563 osdLayoutsConfig->item_pos[0][OSD_VARIO] = OSD_POS(23, 5);
3564 // OSD_VARIO_NUM at the right of OSD_VARIO
3565 osdLayoutsConfig->item_pos[0][OSD_VARIO_NUM] = OSD_POS(24, 7);
3566 osdLayoutsConfig->item_pos[0][OSD_HOME_DIR] = OSD_POS(14, 11);
3567 osdLayoutsConfig->item_pos[0][OSD_ARTIFICIAL_HORIZON] = OSD_POS(8, 6);
3568 osdLayoutsConfig->item_pos[0][OSD_HORIZON_SIDEBARS] = OSD_POS(8, 6);
3570 osdLayoutsConfig->item_pos[0][OSD_CRAFT_NAME] = OSD_POS(20, 2);
3571 osdLayoutsConfig->item_pos[0][OSD_PILOT_NAME] = OSD_POS(20, 3);
3572 osdLayoutsConfig->item_pos[0][OSD_VTX_CHANNEL] = OSD_POS(8, 6);
3574 #ifdef USE_SERIALRX_CRSF
3575 osdLayoutsConfig->item_pos[0][OSD_CRSF_RSSI_DBM] = OSD_POS(23, 12);
3576 osdLayoutsConfig->item_pos[0][OSD_CRSF_LQ] = OSD_POS(23, 11);
3577 osdLayoutsConfig->item_pos[0][OSD_CRSF_SNR_DB] = OSD_POS(24, 9);
3578 osdLayoutsConfig->item_pos[0][OSD_CRSF_TX_POWER] = OSD_POS(24, 10);
3579 #endif
3581 osdLayoutsConfig->item_pos[0][OSD_ONTIME] = OSD_POS(23, 8);
3582 osdLayoutsConfig->item_pos[0][OSD_FLYTIME] = OSD_POS(23, 9);
3583 osdLayoutsConfig->item_pos[0][OSD_ONTIME_FLYTIME] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG;
3584 osdLayoutsConfig->item_pos[0][OSD_RTC_TIME] = OSD_POS(23, 12);
3585 osdLayoutsConfig->item_pos[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH] = OSD_POS(23, 7);
3586 osdLayoutsConfig->item_pos[0][OSD_REMAINING_DISTANCE_BEFORE_RTH] = OSD_POS(23, 6);
3588 osdLayoutsConfig->item_pos[0][OSD_MISSION] = OSD_POS(0, 10);
3589 osdLayoutsConfig->item_pos[0][OSD_GPS_SATS] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG;
3590 osdLayoutsConfig->item_pos[0][OSD_GPS_HDOP] = OSD_POS(0, 10);
3592 osdLayoutsConfig->item_pos[0][OSD_GPS_LAT] = OSD_POS(0, 12);
3593 // Put this on top of the latitude, since it's very unlikely
3594 // that users will want to use both at the same time.
3595 osdLayoutsConfig->item_pos[0][OSD_PLUS_CODE] = OSD_POS(0, 12);
3596 osdLayoutsConfig->item_pos[0][OSD_FLYMODE] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG;
3597 osdLayoutsConfig->item_pos[0][OSD_GPS_LON] = OSD_POS(18, 12);
3599 osdLayoutsConfig->item_pos[0][OSD_AZIMUTH] = OSD_POS(2, 12);
3601 osdLayoutsConfig->item_pos[0][OSD_ROLL_PIDS] = OSD_POS(2, 10);
3602 osdLayoutsConfig->item_pos[0][OSD_PITCH_PIDS] = OSD_POS(2, 11);
3603 osdLayoutsConfig->item_pos[0][OSD_YAW_PIDS] = OSD_POS(2, 12);
3604 osdLayoutsConfig->item_pos[0][OSD_LEVEL_PIDS] = OSD_POS(2, 12);
3605 osdLayoutsConfig->item_pos[0][OSD_POS_XY_PIDS] = OSD_POS(2, 12);
3606 osdLayoutsConfig->item_pos[0][OSD_POS_Z_PIDS] = OSD_POS(2, 12);
3607 osdLayoutsConfig->item_pos[0][OSD_VEL_XY_PIDS] = OSD_POS(2, 12);
3608 osdLayoutsConfig->item_pos[0][OSD_VEL_Z_PIDS] = OSD_POS(2, 12);
3609 osdLayoutsConfig->item_pos[0][OSD_HEADING_P] = OSD_POS(2, 12);
3610 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_ROLL] = OSD_POS(2, 10);
3611 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_PITCH] = OSD_POS(2, 11);
3612 osdLayoutsConfig->item_pos[0][OSD_RC_EXPO] = OSD_POS(2, 12);
3613 osdLayoutsConfig->item_pos[0][OSD_RC_YAW_EXPO] = OSD_POS(2, 12);
3614 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_EXPO] = OSD_POS(2, 12);
3615 osdLayoutsConfig->item_pos[0][OSD_PITCH_RATE] = OSD_POS(2, 12);
3616 osdLayoutsConfig->item_pos[0][OSD_ROLL_RATE] = OSD_POS(2, 12);
3617 osdLayoutsConfig->item_pos[0][OSD_YAW_RATE] = OSD_POS(2, 12);
3618 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_EXPO] = OSD_POS(2, 12);
3619 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_YAW_EXPO] = OSD_POS(2, 12);
3620 osdLayoutsConfig->item_pos[0][OSD_MANUAL_PITCH_RATE] = OSD_POS(2, 12);
3621 osdLayoutsConfig->item_pos[0][OSD_MANUAL_ROLL_RATE] = OSD_POS(2, 12);
3622 osdLayoutsConfig->item_pos[0][OSD_MANUAL_YAW_RATE] = OSD_POS(2, 12);
3623 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_CRUISE_THR] = OSD_POS(2, 12);
3624 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_PITCH2THR] = OSD_POS(2, 12);
3625 osdLayoutsConfig->item_pos[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE] = OSD_POS(2, 12);
3626 osdLayoutsConfig->item_pos[0][OSD_FW_ALT_PID_OUTPUTS] = OSD_POS(2, 12);
3627 osdLayoutsConfig->item_pos[0][OSD_FW_POS_PID_OUTPUTS] = OSD_POS(2, 12);
3628 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_X_PID_OUTPUTS] = OSD_POS(2, 12);
3629 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Y_PID_OUTPUTS] = OSD_POS(2, 12);
3630 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Z_PID_OUTPUTS] = OSD_POS(2, 12);
3631 osdLayoutsConfig->item_pos[0][OSD_MC_POS_XYZ_P_OUTPUTS] = OSD_POS(2, 12);
3633 osdLayoutsConfig->item_pos[0][OSD_POWER] = OSD_POS(15, 1);
3635 osdLayoutsConfig->item_pos[0][OSD_IMU_TEMPERATURE] = OSD_POS(19, 2);
3636 osdLayoutsConfig->item_pos[0][OSD_BARO_TEMPERATURE] = OSD_POS(19, 3);
3637 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_0_TEMPERATURE] = OSD_POS(19, 4);
3638 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_1_TEMPERATURE] = OSD_POS(19, 5);
3639 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_2_TEMPERATURE] = OSD_POS(19, 6);
3640 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_3_TEMPERATURE] = OSD_POS(19, 7);
3641 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_4_TEMPERATURE] = OSD_POS(19, 8);
3642 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_5_TEMPERATURE] = OSD_POS(19, 9);
3643 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_6_TEMPERATURE] = OSD_POS(19, 10);
3644 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_7_TEMPERATURE] = OSD_POS(19, 11);
3646 osdLayoutsConfig->item_pos[0][OSD_AIR_SPEED] = OSD_POS(3, 5);
3647 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_HORIZONTAL] = OSD_POS(3, 6);
3648 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_VERTICAL] = OSD_POS(3, 7);
3650 osdLayoutsConfig->item_pos[0][OSD_GFORCE] = OSD_POS(12, 4);
3651 osdLayoutsConfig->item_pos[0][OSD_GFORCE_X] = OSD_POS(12, 5);
3652 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Y] = OSD_POS(12, 6);
3653 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Z] = OSD_POS(12, 7);
3655 osdLayoutsConfig->item_pos[0][OSD_VTX_POWER] = OSD_POS(3, 5);
3657 osdLayoutsConfig->item_pos[0][OSD_GVAR_0] = OSD_POS(1, 1);
3658 osdLayoutsConfig->item_pos[0][OSD_GVAR_1] = OSD_POS(1, 2);
3659 osdLayoutsConfig->item_pos[0][OSD_GVAR_2] = OSD_POS(1, 3);
3660 osdLayoutsConfig->item_pos[0][OSD_GVAR_3] = OSD_POS(1, 4);
3662 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_0] = OSD_POS(2, 7);
3663 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_1] = OSD_POS(2, 8);
3664 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_2] = OSD_POS(2, 9);
3665 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_3] = OSD_POS(2, 10);
3667 #if defined(USE_ESC_SENSOR)
3668 osdLayoutsConfig->item_pos[0][OSD_ESC_RPM] = OSD_POS(1, 2);
3669 osdLayoutsConfig->item_pos[0][OSD_ESC_TEMPERATURE] = OSD_POS(1, 3);
3670 #endif
3672 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3673 osdLayoutsConfig->item_pos[0][OSD_RC_SOURCE] = OSD_POS(3, 4);
3674 #endif
3676 #ifdef USE_POWER_LIMITS
3677 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_REMAINING_BURST_TIME] = OSD_POS(3, 4);
3678 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT] = OSD_POS(3, 5);
3679 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT] = OSD_POS(3, 6);
3680 #endif
3682 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
3683 osdLayoutsConfig->item_pos[0][OSD_MESSAGES] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG;
3685 for (unsigned ii = 1; ii < OSD_LAYOUT_COUNT; ii++) {
3686 for (unsigned jj = 0; jj < ARRAYLEN(osdLayoutsConfig->item_pos[0]); jj++) {
3687 osdLayoutsConfig->item_pos[ii][jj] = osdLayoutsConfig->item_pos[0][jj] & ~OSD_VISIBLE_FLAG;
3692 static void osdSetNextRefreshIn(uint32_t timeMs) {
3693 resumeRefreshAt = micros() + timeMs * 1000;
3694 refreshWaitForResumeCmdRelease = true;
3697 static void osdCompleteAsyncInitialization(void)
3699 if (!displayIsReady(osdDisplayPort)) {
3700 // Update the display.
3701 // XXX: Rename displayDrawScreen() and associated functions
3702 // to displayUpdate()
3703 displayDrawScreen(osdDisplayPort);
3704 return;
3707 osdDisplayIsReady = true;
3709 #if defined(USE_CANVAS)
3710 if (osdConfig()->force_grid) {
3711 osdDisplayHasCanvas = false;
3712 } else {
3713 osdDisplayHasCanvas = displayGetCanvas(&osdCanvas, osdDisplayPort);
3715 #endif
3717 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3718 displayClearScreen(osdDisplayPort);
3720 uint8_t y = 1;
3721 displayFontMetadata_t metadata;
3722 bool fontHasMetadata = displayGetFontMetadata(&metadata, osdDisplayPort);
3723 LOG_DEBUG(OSD, "Font metadata version %s: %u (%u chars)",
3724 fontHasMetadata ? "Y" : "N", metadata.version, metadata.charCount);
3726 if (fontHasMetadata && metadata.charCount > 256) {
3727 hasExtendedFont = true;
3728 unsigned logo_c = SYM_LOGO_START;
3729 unsigned logo_x = OSD_CENTER_LEN(SYM_LOGO_WIDTH);
3730 for (unsigned ii = 0; ii < SYM_LOGO_HEIGHT; ii++) {
3731 for (unsigned jj = 0; jj < SYM_LOGO_WIDTH; jj++) {
3732 displayWriteChar(osdDisplayPort, logo_x + jj, y, logo_c++);
3734 y++;
3736 y++;
3737 } else if (!fontHasMetadata) {
3738 const char *m = "INVALID FONT";
3739 displayWrite(osdDisplayPort, OSD_CENTER_S(m), 3, m);
3740 y = 4;
3743 if (fontHasMetadata && metadata.version < OSD_MIN_FONT_VERSION) {
3744 const char *m = "INVALID FONT VERSION";
3745 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
3748 char string_buffer[30];
3749 tfp_sprintf(string_buffer, "INAV VERSION: %s", FC_VERSION_STRING);
3750 uint8_t xPos = osdDisplayIsHD() ? 15 : 5;
3751 displayWrite(osdDisplayPort, xPos, y++, string_buffer);
3752 #ifdef USE_CMS
3753 displayWrite(osdDisplayPort, xPos+2, y++, CMS_STARTUP_HELP_TEXT1);
3754 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT2);
3755 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT3);
3756 #endif
3757 #ifdef USE_STATS
3760 uint8_t statNameX = osdDisplayIsHD() ? 14 : 4;
3761 uint8_t statValueX = osdDisplayIsHD() ? 34 : 24;
3763 if (statsConfig()->stats_enabled) {
3764 displayWrite(osdDisplayPort, statNameX, ++y, "ODOMETER:");
3765 switch (osdConfig()->units) {
3766 case OSD_UNIT_UK:
3767 FALLTHROUGH;
3768 case OSD_UNIT_IMPERIAL:
3769 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_MILE));
3770 string_buffer[5] = SYM_MI;
3771 break;
3772 default:
3773 case OSD_UNIT_GA:
3774 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE));
3775 string_buffer[5] = SYM_NM;
3776 break;
3777 case OSD_UNIT_METRIC_MPH:
3778 FALLTHROUGH;
3779 case OSD_UNIT_METRIC:
3780 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER));
3781 string_buffer[5] = SYM_KM;
3782 break;
3784 string_buffer[6] = '\0';
3785 displayWrite(osdDisplayPort, statValueX-5, y, string_buffer);
3787 displayWrite(osdDisplayPort, statNameX, ++y, "TOTAL TIME:");
3788 uint32_t tot_mins = statsConfig()->stats_total_time / 60;
3789 tfp_sprintf(string_buffer, "%2d:%02dHM", (int)(tot_mins / 60), (int)(tot_mins % 60));
3790 displayWrite(osdDisplayPort, statValueX-5, y, string_buffer);
3792 #ifdef USE_ADC
3793 if (feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER)) {
3794 displayWrite(osdDisplayPort, statNameX, ++y, "TOTAL ENERGY:");
3795 osdFormatCentiNumber(string_buffer, statsConfig()->stats_total_energy / 10, 0, 2, 0, 4);
3796 strcat(string_buffer, "\xAB"); // SYM_WH
3797 displayWrite(osdDisplayPort, statValueX-4, y, string_buffer);
3799 displayWrite(osdDisplayPort, statNameX, ++y, "AVG EFFICIENCY:");
3800 if (statsConfig()->stats_total_dist) {
3801 uint32_t avg_efficiency = statsConfig()->stats_total_energy / (statsConfig()->stats_total_dist / METERS_PER_KILOMETER); // mWh/km
3802 switch (osdConfig()->units) {
3803 case OSD_UNIT_UK:
3804 FALLTHROUGH;
3805 case OSD_UNIT_IMPERIAL:
3806 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3807 string_buffer[3] = SYM_WH_MI;
3808 break;
3809 case OSD_UNIT_GA:
3810 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3811 string_buffer[3] = SYM_WH_NM;
3812 break;
3813 default:
3814 case OSD_UNIT_METRIC_MPH:
3815 FALLTHROUGH;
3816 case OSD_UNIT_METRIC:
3817 osdFormatCentiNumber(string_buffer, avg_efficiency / 10000 * METERS_PER_MILE, 0, 2, 0, 3);
3818 string_buffer[3] = SYM_WH_KM;
3819 break;
3821 } else {
3822 string_buffer[0] = string_buffer[1] = string_buffer[2] = '-';
3824 string_buffer[4] = '\0';
3825 displayWrite(osdDisplayPort, statValueX-3, y, string_buffer);
3827 #endif // USE_ADC
3829 #endif
3831 displayCommitTransaction(osdDisplayPort);
3832 displayResync(osdDisplayPort);
3833 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME);
3836 void osdInit(displayPort_t *osdDisplayPortToUse)
3838 if (!osdDisplayPortToUse)
3839 return;
3841 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(63,63));
3843 osdDisplayPort = osdDisplayPortToUse;
3845 #ifdef USE_CMS
3846 cmsDisplayPortRegister(osdDisplayPort);
3847 #endif
3849 armState = ARMING_FLAG(ARMED);
3850 osdCompleteAsyncInitialization();
3853 static void osdResetStats(void)
3855 stats.max_current = 0;
3856 stats.max_power = 0;
3857 stats.max_speed = 0;
3858 stats.max_3D_speed = 0;
3859 stats.max_air_speed = 0;
3860 stats.min_voltage = 5000;
3861 stats.min_rssi = 99;
3862 stats.min_lq = 300;
3863 stats.min_rssi_dbm = 0;
3864 stats.max_altitude = 0;
3867 static void osdUpdateStats(void)
3869 int32_t value;
3871 if (feature(FEATURE_GPS)) {
3872 value = osdGet3DSpeed();
3873 const float airspeed_estimate = getAirspeedEstimate();
3875 if (stats.max_3D_speed < value)
3876 stats.max_3D_speed = value;
3878 if (stats.max_speed < gpsSol.groundSpeed)
3879 stats.max_speed = gpsSol.groundSpeed;
3881 if (stats.max_air_speed < airspeed_estimate)
3882 stats.max_air_speed = airspeed_estimate;
3884 if (stats.max_distance < GPS_distanceToHome)
3885 stats.max_distance = GPS_distanceToHome;
3888 value = getBatteryVoltage();
3889 if (stats.min_voltage > value)
3890 stats.min_voltage = value;
3892 value = abs(getAmperage());
3893 if (stats.max_current < value)
3894 stats.max_current = value;
3896 value = labs(getPower());
3897 if (stats.max_power < value)
3898 stats.max_power = value;
3900 value = osdConvertRSSI();
3901 if (stats.min_rssi > value)
3902 stats.min_rssi = value;
3904 value = osdGetCrsfLQ();
3905 if (stats.min_lq > value)
3906 stats.min_lq = value;
3908 if (!failsafeIsReceivingRxData())
3909 stats.min_lq = 0;
3911 value = osdGetCrsfdBm();
3912 if (stats.min_rssi_dbm > value)
3913 stats.min_rssi_dbm = value;
3915 stats.max_altitude = MAX(stats.max_altitude, osdGetAltitude());
3918 static void osdShowStatsPage1(void)
3920 const char * disarmReasonStr[DISARM_REASON_COUNT] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "KILLSW", "FAILSAFE", "NAV SYS", "LANDING"};
3921 uint8_t top = 1; /* first fully visible line */
3922 const uint8_t statNameX = osdDisplayIsHD() ? 11 : 1;
3923 const uint8_t statValuesX = osdDisplayIsHD() ? 30 : 20;
3924 char buff[10];
3925 statsPagesCheck = 1;
3927 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3928 displayClearScreen(osdDisplayPort);
3930 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- 1/2 ->");
3932 if (feature(FEATURE_GPS)) {
3933 displayWrite(osdDisplayPort, statNameX, top, "MAX SPEED :");
3934 osdFormatVelocityStr(buff, stats.max_3D_speed, true, false);
3935 osdLeftAlignString(buff);
3936 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3938 displayWrite(osdDisplayPort, statNameX, top, "AVG SPEED :");
3939 osdGenerateAverageVelocityStr(buff);
3940 osdLeftAlignString(buff);
3941 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3943 displayWrite(osdDisplayPort, statNameX, top, "MAX DISTANCE :");
3944 osdFormatDistanceStr(buff, stats.max_distance*100);
3945 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3947 displayWrite(osdDisplayPort, statNameX, top, "TRAVELED DISTANCE:");
3948 osdFormatDistanceStr(buff, getTotalTravelDistance());
3949 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3952 displayWrite(osdDisplayPort, statNameX, top, "MAX ALTITUDE :");
3953 osdFormatAltitudeStr(buff, stats.max_altitude);
3954 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3956 switch (rxConfig()->serialrx_provider) {
3957 case SERIALRX_CRSF:
3958 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI % :");
3959 itoa(stats.min_rssi, buff, 10);
3960 strcat(buff, "%");
3961 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3963 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI DBM :");
3964 itoa(stats.min_rssi_dbm, buff, 10);
3965 tfp_sprintf(buff, "%s%c", buff, SYM_DBM);
3966 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3968 displayWrite(osdDisplayPort, statNameX, top, "MIN LQ :");
3969 itoa(stats.min_lq, buff, 10);
3970 strcat(buff, "%");
3971 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3972 break;
3973 default:
3974 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI :");
3975 itoa(stats.min_rssi, buff, 10);
3976 strcat(buff, "%");
3977 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3980 displayWrite(osdDisplayPort, statNameX, top, "FLY TIME :");
3981 uint16_t flySeconds = getFlightTime();
3982 uint16_t flyMinutes = flySeconds / 60;
3983 flySeconds %= 60;
3984 uint16_t flyHours = flyMinutes / 60;
3985 flyMinutes %= 60;
3986 tfp_sprintf(buff, "%02u:%02u:%02u", flyHours, flyMinutes, flySeconds);
3987 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3989 displayWrite(osdDisplayPort, statNameX, top, "DISARMED BY :");
3990 displayWrite(osdDisplayPort, statValuesX, top++, disarmReasonStr[getDisarmReason()]);
3992 if (savingSettings == true) {
3993 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS));
3994 } else if (notify_settings_saved > 0) {
3995 if (millis() > notify_settings_saved) {
3996 notify_settings_saved = 0;
3997 } else {
3998 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED));
4002 displayCommitTransaction(osdDisplayPort);
4005 static void osdShowStatsPage2(void)
4007 uint8_t top = 1; /* first fully visible line */
4008 const uint8_t statNameX = osdDisplayIsHD() ? 11 : 1;
4009 const uint8_t statValuesX = osdDisplayIsHD() ? 30 : 20;
4010 char buff[10];
4011 statsPagesCheck = 1;
4013 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4014 displayClearScreen(osdDisplayPort);
4016 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- <- 2/2");
4018 if (osdConfig()->stats_min_voltage_unit == OSD_STATS_MIN_VOLTAGE_UNIT_BATTERY) {
4019 displayWrite(osdDisplayPort, statNameX, top, "MIN BATTERY VOLT :");
4020 osdFormatCentiNumber(buff, stats.min_voltage, 0, osdConfig()->main_voltage_decimals, 0, osdConfig()->main_voltage_decimals + 2);
4021 } else {
4022 displayWrite(osdDisplayPort, statNameX, top, "MIN CELL VOLTAGE :");
4023 osdFormatCentiNumber(buff, stats.min_voltage/getBatteryCellCount(), 0, 2, 0, 3);
4025 tfp_sprintf(buff, "%s%c", buff, SYM_VOLT);
4026 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4028 if (feature(FEATURE_CURRENT_METER)) {
4029 displayWrite(osdDisplayPort, statNameX, top, "MAX CURRENT :");
4030 osdFormatCentiNumber(buff, stats.max_current, 0, 2, 0, 3);
4031 tfp_sprintf(buff, "%s%c", buff, SYM_AMP);
4032 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4034 displayWrite(osdDisplayPort, statNameX, top, "MAX POWER :");
4035 bool kiloWatt = osdFormatCentiNumber(buff, stats.max_power, 1000, 2, 2, 3);
4036 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
4037 buff[4] = '\0';
4038 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4040 displayWrite(osdDisplayPort, statNameX, top, "USED CAPACITY :");
4041 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4042 tfp_sprintf(buff, "%d%c", (int)getMAhDrawn(), SYM_MAH);
4043 } else {
4044 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
4045 tfp_sprintf(buff, "%s%c", buff, SYM_WH);
4047 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4049 int32_t totalDistance = getTotalTravelDistance();
4050 bool moreThanAh = false;
4051 bool efficiencyValid = totalDistance >= 10000;
4052 if (feature(FEATURE_GPS)) {
4053 displayWrite(osdDisplayPort, statNameX, top, "AVG EFFICIENCY :");
4054 switch (osdConfig()->units) {
4055 case OSD_UNIT_UK:
4056 FALLTHROUGH;
4057 case OSD_UNIT_IMPERIAL:
4058 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4059 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, 3);
4060 if (!moreThanAh) {
4061 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
4062 } else {
4063 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
4065 if (!efficiencyValid) {
4066 buff[0] = buff[1] = buff[2] = '-';
4067 buff[3] = SYM_MAH_MI_0;
4068 buff[4] = SYM_MAH_MI_1;
4069 buff[5] = '\0';
4071 } else {
4072 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, 3);
4073 tfp_sprintf(buff, "%s%c", buff, SYM_WH_MI);
4074 if (!efficiencyValid) {
4075 buff[0] = buff[1] = buff[2] = '-';
4078 break;
4079 case OSD_UNIT_GA:
4080 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4081 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, 3);
4082 if (!moreThanAh) {
4083 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
4084 } else {
4085 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
4087 if (!efficiencyValid) {
4088 buff[0] = buff[1] = buff[2] = '-';
4089 buff[3] = SYM_MAH_NM_0;
4090 buff[4] = SYM_MAH_NM_1;
4091 buff[5] = '\0';
4093 } else {
4094 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, 3);
4095 tfp_sprintf(buff, "%s%c", buff, SYM_WH_NM);
4096 if (!efficiencyValid) {
4097 buff[0] = buff[1] = buff[2] = '-';
4100 break;
4101 case OSD_UNIT_METRIC_MPH:
4102 FALLTHROUGH;
4103 case OSD_UNIT_METRIC:
4104 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
4105 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000000.0f / totalDistance), 1000, 0, 2, 3);
4106 if (!moreThanAh) {
4107 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
4108 } else {
4109 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
4111 if (!efficiencyValid) {
4112 buff[0] = buff[1] = buff[2] = '-';
4113 buff[3] = SYM_MAH_KM_0;
4114 buff[4] = SYM_MAH_KM_1;
4115 buff[5] = '\0';
4117 } else {
4118 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10000.0f / totalDistance), 0, 2, 0, 3);
4119 tfp_sprintf(buff, "%s%c", buff, SYM_WH_KM);
4120 if (!efficiencyValid) {
4121 buff[0] = buff[1] = buff[2] = '-';
4124 break;
4126 osdLeftAlignString(buff);
4127 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4131 const float max_gforce = accGetMeasuredMaxG();
4132 displayWrite(osdDisplayPort, statNameX, top, "MAX G-FORCE :");
4133 osdFormatCentiNumber(buff, max_gforce * 100, 0, 2, 0, 3);
4134 displayWrite(osdDisplayPort, statValuesX, top++, buff);
4136 const acc_extremes_t *acc_extremes = accGetMeasuredExtremes();
4137 displayWrite(osdDisplayPort, statNameX, top, "MIN/MAX Z G-FORCE:");
4138 osdFormatCentiNumber(buff, acc_extremes[Z].min * 100, 0, 2, 0, 4);
4139 strcat(buff,"/");
4140 displayWrite(osdDisplayPort, statValuesX - 1, top, buff);
4141 osdFormatCentiNumber(buff, acc_extremes[Z].max * 100, 0, 2, 0, 3);
4142 displayWrite(osdDisplayPort, statValuesX + 4, top++, buff);
4144 if (savingSettings == true) {
4145 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS));
4146 } else if (notify_settings_saved > 0) {
4147 if (millis() > notify_settings_saved) {
4148 notify_settings_saved = 0;
4149 } else {
4150 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED));
4154 displayCommitTransaction(osdDisplayPort);
4157 // called when motors armed
4158 static void osdShowArmed(void)
4160 dateTime_t dt;
4161 char buf[MAX(32, FORMATTED_DATE_TIME_BUFSIZE)];
4162 char craftNameBuf[MAX_NAME_LENGTH];
4163 char versionBuf[30];
4164 char *date;
4165 char *time;
4166 // We need 12 visible rows, start row never < first fully visible row 1
4167 uint8_t y = osdDisplayPort->rows > 13 ? (osdDisplayPort->rows - 12) / 2 : 1;
4169 displayClearScreen(osdDisplayPort);
4170 strcpy(buf, "ARMED");
4171 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4172 y += 2;
4174 if (strlen(systemConfig()->craftName) > 0) {
4175 osdFormatCraftName(craftNameBuf);
4176 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(systemConfig()->craftName)) / 2, y, craftNameBuf );
4177 y += 1;
4179 if (posControl.waypointListValid && posControl.waypointCount > 0) {
4180 #ifdef USE_MULTI_MISSION
4181 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
4182 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4183 #else
4184 strcpy(buf, "*MISSION LOADED*");
4185 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4186 #endif
4188 y += 1;
4190 #if defined(USE_GPS)
4191 if (feature(FEATURE_GPS)) {
4192 if (STATE(GPS_FIX_HOME)) {
4193 if (osdConfig()->osd_home_position_arm_screen){
4194 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
4195 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4196 osdFormatCoordinate(buf, SYM_LON, GPS_home.lon);
4197 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 1, buf);
4198 int digits = osdConfig()->plus_code_digits;
4199 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
4200 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 2, buf);
4202 y += 4;
4203 #if defined (USE_SAFE_HOME)
4204 if (safehome_distance) { // safehome found during arming
4205 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
4206 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
4207 } else {
4208 char buf2[12]; // format the distance first
4209 osdFormatDistanceStr(buf2, safehome_distance);
4210 tfp_sprintf(buf, "%c - %s -> SAFEHOME %u", SYM_HOME, buf2, safehome_index);
4212 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
4213 // write this message above the ARMED message to make it obvious
4214 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y - 8, buf, elemAttr);
4216 #endif
4217 } else {
4218 strcpy(buf, "!NO HOME POSITION!");
4219 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
4220 y += 1;
4223 #endif
4225 if (rtcGetDateTime(&dt)) {
4226 dateTimeFormatLocal(buf, &dt);
4227 dateTimeSplitFormatted(buf, &date, &time);
4229 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(date)) / 2, y, date);
4230 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(time)) / 2, y + 1, time);
4231 y += 3;
4234 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
4235 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, y, versionBuf);
4238 static void osdFilterData(timeUs_t currentTimeUs) {
4239 static timeUs_t lastRefresh = 0;
4240 float refresh_dT = US2S(cmpTimeUs(currentTimeUs, lastRefresh));
4242 GForce = fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF)) / GRAVITY_MSS;
4243 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) GForceAxis[axis] = imuMeasuredAccelBF.v[axis] / GRAVITY_MSS;
4245 if (lastRefresh) {
4246 GForce = pt1FilterApply3(&GForceFilter, GForce, refresh_dT);
4247 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) pt1FilterApply3(GForceFilterAxis + axis, GForceAxis[axis], refresh_dT);
4248 } else {
4249 pt1FilterInitRC(&GForceFilter, GFORCE_FILTER_TC, 0);
4250 pt1FilterReset(&GForceFilter, GForce);
4252 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) {
4253 pt1FilterInitRC(GForceFilterAxis + axis, GFORCE_FILTER_TC, 0);
4254 pt1FilterReset(GForceFilterAxis + axis, GForceAxis[axis]);
4258 lastRefresh = currentTimeUs;
4261 static void osdRefresh(timeUs_t currentTimeUs)
4263 osdFilterData(currentTimeUs);
4265 #ifdef USE_CMS
4266 if (IS_RC_MODE_ACTIVE(BOXOSD) && (!cmsInMenu) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4267 #else
4268 if (IS_RC_MODE_ACTIVE(BOXOSD) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4269 #endif
4270 displayClearScreen(osdDisplayPort);
4271 armState = ARMING_FLAG(ARMED);
4272 return;
4275 // detect arm/disarm
4276 static uint8_t statsPageAutoSwapCntl = 2;
4277 if (armState != ARMING_FLAG(ARMED)) {
4278 if (ARMING_FLAG(ARMED)) {
4279 osdResetStats();
4280 statsPageAutoSwapCntl = 2;
4281 osdShowArmed(); // reset statistic etc
4282 uint32_t delay = ARMED_SCREEN_DISPLAY_TIME;
4283 statsPagesCheck = 0;
4284 #if defined(USE_SAFE_HOME)
4285 if (safehome_distance)
4286 delay *= 3;
4287 #endif
4288 osdSetNextRefreshIn(delay);
4289 } else {
4290 osdShowStatsPage1(); // show first page of statistics
4291 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME);
4292 statsPageAutoSwapCntl = osdConfig()->stats_page_auto_swap_time > 0 ? 0 : 2; // disable swapping pages when time = 0
4295 armState = ARMING_FLAG(ARMED);
4298 if (resumeRefreshAt) {
4299 // If we already reached he time for the next refresh,
4300 // or THR is high or PITCH is high, resume refreshing.
4301 // Clear the screen first to erase other elements which
4302 // might have been drawn while the OSD wasn't refreshing.
4304 // auto swap stats pages when first shown
4305 // auto swap cancelled using roll stick
4306 if (statsPageAutoSwapCntl != 2) {
4307 if (STATS_PAGE1 || STATS_PAGE2) {
4308 statsPageAutoSwapCntl = 2;
4309 } else {
4310 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time * 1000), 2)) {
4311 if (statsPageAutoSwapCntl == 0) {
4312 osdShowStatsPage1();
4313 statsPageAutoSwapCntl = 1;
4315 } else {
4316 if (statsPageAutoSwapCntl == 1) {
4317 osdShowStatsPage2();
4318 statsPageAutoSwapCntl = 0;
4324 if (!DELAYED_REFRESH_RESUME_COMMAND)
4325 refreshWaitForResumeCmdRelease = false;
4327 if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && DELAYED_REFRESH_RESUME_COMMAND)) {
4328 displayClearScreen(osdDisplayPort);
4329 resumeRefreshAt = 0;
4330 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE1)) {
4331 if (statsPagesCheck == 1) {
4332 osdShowStatsPage1();
4334 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE2)) {
4335 if (statsPagesCheck == 1) {
4336 osdShowStatsPage2();
4338 } else {
4339 displayHeartbeat(osdDisplayPort);
4341 return;
4344 #ifdef USE_CMS
4345 if (!displayIsGrabbed(osdDisplayPort)) {
4346 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4347 if (fullRedraw) {
4348 displayClearScreen(osdDisplayPort);
4349 fullRedraw = false;
4351 osdDrawNextElement();
4352 displayHeartbeat(osdDisplayPort);
4353 displayCommitTransaction(osdDisplayPort);
4354 #ifdef OSD_CALLS_CMS
4355 } else {
4356 cmsUpdate(currentTimeUs);
4357 #endif
4359 #endif
4363 * Called periodically by the scheduler
4365 void osdUpdate(timeUs_t currentTimeUs)
4367 static uint32_t counter = 0;
4369 // don't touch buffers if DMA transaction is in progress
4370 if (displayIsTransferInProgress(osdDisplayPort)) {
4371 return;
4374 if (!osdDisplayIsReady) {
4375 osdCompleteAsyncInitialization();
4376 return;
4379 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
4380 // Check if the layout has changed. Higher numbered
4381 // boxes take priority.
4382 unsigned activeLayout;
4383 if (layoutOverride >= 0) {
4384 activeLayout = layoutOverride;
4385 // Check for timed override, it will go into effect on
4386 // the next OSD iteration
4387 if (layoutOverrideUntil > 0 && millis() > layoutOverrideUntil) {
4388 layoutOverrideUntil = 0;
4389 layoutOverride = -1;
4391 } else if (osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE)) {
4392 activeLayout = 0;
4393 } else {
4394 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
4395 if (IS_RC_MODE_ACTIVE(BOXOSDALT3))
4396 activeLayout = 3;
4397 else
4398 #endif
4399 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
4400 if (IS_RC_MODE_ACTIVE(BOXOSDALT2))
4401 activeLayout = 2;
4402 else
4403 #endif
4404 if (IS_RC_MODE_ACTIVE(BOXOSDALT1))
4405 activeLayout = 1;
4406 else
4407 #ifdef USE_PROGRAMMING_FRAMEWORK
4408 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT))
4409 activeLayout = constrain(logicConditionValuesByType[LOGIC_CONDITION_SET_OSD_LAYOUT], 0, OSD_ALTERNATE_LAYOUT_COUNT);
4410 else
4411 #endif
4412 activeLayout = 0;
4414 if (currentLayout != activeLayout) {
4415 currentLayout = activeLayout;
4416 osdStartFullRedraw();
4418 #endif
4420 #define DRAW_FREQ_DENOM 4
4421 #define STATS_FREQ_DENOM 50
4422 counter++;
4424 if ((counter % STATS_FREQ_DENOM) == 0) {
4425 osdUpdateStats();
4428 if ((counter % DRAW_FREQ_DENOM) == 0) {
4429 // redraw values in buffer
4430 osdRefresh(currentTimeUs);
4431 } else {
4432 // rest of time redraw screen
4433 displayDrawScreen(osdDisplayPort);
4436 #ifdef USE_CMS
4437 // do not allow ARM if we are in menu
4438 if (displayIsGrabbed(osdDisplayPort)) {
4439 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4440 } else {
4441 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4443 #endif
4446 void osdStartFullRedraw(void)
4448 fullRedraw = true;
4451 void osdOverrideLayout(int layout, timeMs_t duration)
4453 layoutOverride = constrain(layout, -1, ARRAYLEN(osdLayoutsConfig()->item_pos) - 1);
4454 if (layoutOverride >= 0 && duration > 0) {
4455 layoutOverrideUntil = millis() + duration;
4456 } else {
4457 layoutOverrideUntil = 0;
4461 int osdGetActiveLayout(bool *overridden)
4463 if (overridden) {
4464 *overridden = layoutOverride >= 0;
4466 return currentLayout;
4469 bool osdItemIsFixed(osd_items_e item)
4471 return item == OSD_CROSSHAIRS ||
4472 item == OSD_ARTIFICIAL_HORIZON ||
4473 item == OSD_HORIZON_SIDEBARS;
4476 displayPort_t *osdGetDisplayPort(void)
4478 return osdDisplayPort;
4481 displayCanvas_t *osdGetDisplayPortCanvas(void)
4483 #if defined(USE_CANVAS)
4484 if (osdDisplayHasCanvas) {
4485 return &osdCanvas;
4487 #endif
4488 return NULL;
4491 timeMs_t systemMessageCycleTime(unsigned messageCount, const char **messages){
4492 uint8_t i = 0;
4493 float factor = 1.0f;
4494 while (i < messageCount) {
4495 if ((float)strlen(messages[i]) / 15.0f > factor) {
4496 factor = (float)strlen(messages[i]) / 15.0f;
4498 i++;
4500 return osdConfig()->system_msg_display_time * factor;
4503 textAttributes_t osdGetSystemMessage(char *buff, size_t buff_size, bool isCenteredText)
4505 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
4507 if (buff != NULL) {
4508 const char *message = NULL;
4509 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
4510 // We might have up to 5 messages to show.
4511 const char *messages[5];
4512 unsigned messageCount = 0;
4513 const char *failsafeInfoMessage = NULL;
4514 const char *invertedInfoMessage = NULL;
4516 if (ARMING_FLAG(ARMED)) {
4517 if (FLIGHT_MODE(FAILSAFE_MODE) || FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding()) {
4518 if (isWaypointMissionRTHActive()) {
4519 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
4520 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL);
4522 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE) {
4523 messages[messageCount++] = STATE(LANDING_DETECTED) ? OSD_MESSAGE_STR(OSD_MSG_WP_LANDED) : OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED);
4524 } else if (NAV_Status.state == MW_NAV_STATE_WP_ENROUTE) {
4525 // Countdown display for remaining Waypoints
4526 char buf[6];
4527 osdFormatDistanceSymbol(buf, posControl.wpDistance, 0);
4528 tfp_sprintf(messageBuf, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl.activeWaypointIndex), posControl.geoWaypointCount, buf);
4529 messages[messageCount++] = messageBuf;
4530 } else if (NAV_Status.state == MW_NAV_STATE_HOLD_TIMED) {
4531 if (navConfig()->general.waypoint_enforce_altitude && !posControl.wpAltitudeReached) {
4532 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ADJUSTING_WP_ALT);
4533 } else {
4534 // WP hold time countdown in seconds
4535 timeMs_t currentTime = millis();
4536 int holdTimeRemaining = posControl.waypointList[posControl.activeWaypointIndex].p1 - (int)(MS2S(currentTime - posControl.wpReachedTime));
4537 holdTimeRemaining = holdTimeRemaining >= 0 ? holdTimeRemaining : 0;
4539 tfp_sprintf(messageBuf, "HOLDING WP FOR %2u S", holdTimeRemaining);
4541 messages[messageCount++] = messageBuf;
4543 } else {
4544 const char *navStateMessage = navigationStateMessage();
4545 if (navStateMessage) {
4546 messages[messageCount++] = navStateMessage;
4549 #if defined(USE_SAFE_HOME)
4550 const char *safehomeMessage = divertingToSafehomeMessage();
4551 if (safehomeMessage) {
4552 messages[messageCount++] = safehomeMessage;
4554 #endif
4555 if (FLIGHT_MODE(FAILSAFE_MODE)) {
4556 // In FS mode while being armed too
4557 const char *failsafePhaseMessage = osdFailsafePhaseMessage();
4558 failsafeInfoMessage = osdFailsafeInfoMessage();
4560 if (failsafePhaseMessage) {
4561 messages[messageCount++] = failsafePhaseMessage;
4563 if (failsafeInfoMessage) {
4564 messages[messageCount++] = failsafeInfoMessage;
4567 } else { /* messages shown only when Failsafe, WP, RTH or Emergency Landing not active */
4568 if (STATE(FIXED_WING_LEGACY) && (navGetCurrentStateFlags() & NAV_CTL_LAUNCH)) {
4569 messages[messageCount++] = navConfig()->fw.launch_manual_throttle ? OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH_MANUAL) :
4570 OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH);
4571 const char *launchStateMessage = fixedWingLaunchStateMessage();
4572 if (launchStateMessage) {
4573 messages[messageCount++] = launchStateMessage;
4575 } else {
4576 if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && !navigationRequiresAngleMode()) {
4577 // ALTHOLD might be enabled alongside ANGLE/HORIZON/ACRO
4578 // when it doesn't require ANGLE mode (required only in FW
4579 // right now). If if requires ANGLE, its display is handled
4580 // by OSD_FLYMODE.
4581 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD);
4583 if (IS_RC_MODE_ACTIVE(BOXAUTOTRIM) && !feature(FEATURE_FW_AUTOTRIM)) {
4584 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM);
4586 if (IS_RC_MODE_ACTIVE(BOXAUTOTUNE)) {
4587 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE);
4588 if (FLIGHT_MODE(MANUAL_MODE)) {
4589 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO);
4592 if (FLIGHT_MODE(HEADFREE_MODE)) {
4593 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE);
4595 if (FLIGHT_MODE(SOARING_MODE)) {
4596 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING);
4598 if (posControl.flags.wpMissionPlannerActive) {
4599 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER);
4601 if (STATE(LANDING_DETECTED)) {
4602 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_LANDED);
4606 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS)) {
4607 unsigned invalidIndex;
4609 // Check if we're unable to arm for some reason
4610 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING) && !settingsValidate(&invalidIndex)) {
4612 const setting_t *setting = settingGet(invalidIndex);
4613 settingGetName(setting, messageBuf);
4614 for (int ii = 0; messageBuf[ii]; ii++) {
4615 messageBuf[ii] = sl_toupper(messageBuf[ii]);
4617 invertedInfoMessage = messageBuf;
4618 messages[messageCount++] = invertedInfoMessage;
4620 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
4621 messages[messageCount++] = invertedInfoMessage;
4623 } else {
4625 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM);
4626 messages[messageCount++] = invertedInfoMessage;
4628 // Show the reason for not arming
4629 messages[messageCount++] = osdArmingDisabledReasonMessage();
4632 } else if (!ARMING_FLAG(ARMED)) {
4633 if (isWaypointListValid()) {
4634 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED);
4638 /* Messages that are shown regardless of Arming state */
4640 if (savingSettings == true) {
4641 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS);
4642 } else if (notify_settings_saved > 0) {
4643 if (millis() > notify_settings_saved) {
4644 notify_settings_saved = 0;
4645 } else {
4646 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED);
4650 #ifdef USE_DEV_TOOLS
4651 if (systemConfig()->groundTestMode) {
4652 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_GRD_TEST_MODE);
4654 #endif
4656 if (messageCount > 0) {
4657 message = messages[OSD_ALTERNATING_CHOICES(systemMessageCycleTime(messageCount, messages), messageCount)];
4658 if (message == failsafeInfoMessage) {
4659 // failsafeInfoMessage is not useful for recovering
4660 // a lost model, but might help avoiding a crash.
4661 // Blink to grab user attention.
4662 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
4663 } else if (message == invertedInfoMessage) {
4664 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
4666 // We're shoing either failsafePhaseMessage or
4667 // navStateMessage. Don't BLINK here since
4668 // having this text available might be crucial
4669 // during a lost aircraft recovery and blinking
4670 // will cause it to be missing from some frames.
4673 osdFormatMessage(buff, buff_size, message, isCenteredText);
4675 return elemAttr;
4678 #endif // OSD