Removed a line that was for testing.
[inav.git] / src / main / io / osd.c
blob6ecfed740c6d68c6aa172ce539914226fb5ac16e
1 /*
2 * This file is part of Cleanflight.
4 * Cleanflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Cleanflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
19 Created by Marcin Baliniak
20 some functions based on MinimOSD
22 OSD-CMS separation by jflyper
25 #include <stdbool.h>
26 #include <stdint.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <ctype.h>
30 #include <math.h>
32 #include "platform.h"
34 FILE_COMPILE_FOR_SPEED
36 #ifdef USE_OSD
38 #include "build/debug.h"
39 #include "build/version.h"
41 #include "cms/cms.h"
42 #include "cms/cms_types.h"
43 #include "cms/cms_menu_osd.h"
45 #include "common/axis.h"
46 #include "common/constants.h"
47 #include "common/filter.h"
48 #include "common/log.h"
49 #include "common/olc.h"
50 #include "common/printf.h"
51 #include "common/string_light.h"
52 #include "common/time.h"
53 #include "common/typeconversion.h"
54 #include "common/utils.h"
56 #include "config/feature.h"
57 #include "config/parameter_group.h"
58 #include "config/parameter_group_ids.h"
60 #include "drivers/display.h"
61 #include "drivers/display_canvas.h"
62 #include "drivers/display_font_metadata.h"
63 #include "drivers/osd_symbols.h"
64 #include "drivers/time.h"
65 #include "drivers/vtx_common.h"
67 #include "io/flashfs.h"
68 #include "io/gps.h"
69 #include "io/osd.h"
70 #include "io/osd_common.h"
71 #include "io/osd_hud.h"
72 #include "io/vtx.h"
73 #include "io/vtx_string.h"
75 #include "fc/config.h"
76 #include "fc/controlrate_profile.h"
77 #include "fc/fc_core.h"
78 #include "fc/fc_tasks.h"
79 #include "fc/rc_adjustments.h"
80 #include "fc/rc_controls.h"
81 #include "fc/rc_modes.h"
82 #include "fc/runtime_config.h"
83 #include "fc/settings.h"
85 #include "flight/imu.h"
86 #include "flight/mixer.h"
87 #include "flight/pid.h"
88 #include "flight/power_limits.h"
89 #include "flight/rth_estimator.h"
90 #include "flight/secondary_imu.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
119 #define GFORCE_FILTER_TC 0.2
121 #define DELAYED_REFRESH_RESUME_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
122 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
123 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
125 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
126 #define ARMED_SCREEN_DISPLAY_TIME 1500 // ms
127 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
129 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
131 // Adjust OSD_MESSAGE's default position when
132 // changing OSD_MESSAGE_LENGTH
133 #define OSD_MESSAGE_LENGTH 28
134 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
135 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
136 // Wrap all string constants intenteded for display as messages with
137 // this macro to ensure compile time length validation.
138 #define OSD_MESSAGE_STR(x) ({ \
139 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
140 x; \
143 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
145 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
146 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
148 #define OSD_MIN_FONT_VERSION 3
150 static unsigned currentLayout = 0;
151 static int layoutOverride = -1;
152 static bool hasExtendedFont = false; // Wether the font supports characters > 256
153 static timeMs_t layoutOverrideUntil = 0;
154 static pt1Filter_t GForceFilter, GForceFilterAxis[XYZ_AXIS_COUNT];
155 static float GForce, GForceAxis[XYZ_AXIS_COUNT];
157 typedef struct statistic_s {
158 uint16_t max_speed;
159 uint16_t max_3D_speed;
160 uint16_t max_air_speed;
161 uint16_t min_voltage; // /100
162 int16_t max_current;
163 int32_t max_power;
164 int16_t min_rssi;
165 int16_t min_lq; // for CRSF
166 int16_t min_rssi_dbm; // for CRSF
167 int32_t max_altitude;
168 uint32_t max_distance;
169 } statistic_t;
171 static statistic_t stats;
173 static timeUs_t resumeRefreshAt = 0;
174 static bool refreshWaitForResumeCmdRelease;
176 static bool fullRedraw = false;
178 static uint8_t armState;
179 static uint8_t statsPagesCheck = 0;
181 typedef struct osdMapData_s {
182 uint32_t scale;
183 char referenceSymbol;
184 } osdMapData_t;
186 static osdMapData_t osdMapData;
188 static displayPort_t *osdDisplayPort;
189 static bool osdDisplayIsReady = false;
190 #if defined(USE_CANVAS)
191 static displayCanvas_t osdCanvas;
192 static bool osdDisplayHasCanvas;
193 #else
194 #define osdDisplayHasCanvas false
195 #endif
197 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
199 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t, osdConfig, PG_OSD_CONFIG, 5);
200 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t, osdLayoutsConfig, PG_OSD_LAYOUTS_CONFIG, 0);
202 static int digitCount(int32_t value)
204 int digits = 1;
205 while(1) {
206 value = value / 10;
207 if (value == 0) {
208 break;
210 digits++;
212 return digits;
215 bool osdDisplayIsPAL(void)
217 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL;
221 * Formats a number given in cents, to support non integer values
222 * without using floating point math. Value is always right aligned
223 * and spaces are inserted before the number to always yield a string
224 * of the same length. If the value doesn't fit into the provided length
225 * it will be divided by scale and true will be returned.
227 bool osdFormatCentiNumber(char *buff, int32_t centivalue, uint32_t scale, int maxDecimals, int maxScaledDecimals, int length)
229 char *ptr = buff;
230 char *dec;
231 int decimals = maxDecimals;
232 bool negative = false;
233 bool scaled = false;
235 buff[length] = '\0';
237 if (centivalue < 0) {
238 negative = true;
239 centivalue = -centivalue;
240 length--;
243 int32_t integerPart = centivalue / 100;
244 // 3 decimal digits
245 int32_t millis = (centivalue % 100) * 10;
247 int digits = digitCount(integerPart);
248 int remaining = length - digits;
250 if (remaining < 0 && scale > 0) {
251 // Reduce by scale
252 scaled = true;
253 decimals = maxScaledDecimals;
254 integerPart = integerPart / scale;
255 // Multiply by 10 to get 3 decimal digits
256 millis = ((centivalue % (100 * scale)) * 10) / scale;
257 digits = digitCount(integerPart);
258 remaining = length - digits;
261 // 3 decimals at most
262 decimals = MIN(remaining, MIN(decimals, 3));
263 remaining -= decimals;
265 // Done counting. Time to write the characters.
267 // Write spaces at the start
268 while (remaining > 0) {
269 *ptr = SYM_BLANK;
270 ptr++;
271 remaining--;
274 // Write the minus sign if required
275 if (negative) {
276 *ptr = '-';
277 ptr++;
279 // Now write the digits.
280 ui2a(integerPart, 10, 0, ptr);
281 ptr += digits;
282 if (decimals > 0) {
283 *(ptr-1) += SYM_ZERO_HALF_TRAILING_DOT - '0';
284 dec = ptr;
285 int factor = 3; // we're getting the decimal part in millis first
286 while (decimals < factor) {
287 factor--;
288 millis /= 10;
290 int decimalDigits = digitCount(millis);
291 while (decimalDigits < decimals) {
292 decimalDigits++;
293 *ptr = '0';
294 ptr++;
296 ui2a(millis, 10, 0, ptr);
297 *dec += SYM_ZERO_HALF_LEADING_DOT - '0';
299 return scaled;
303 * Aligns text to the left side. Adds spaces at the end to keep string length unchanged.
305 static void osdLeftAlignString(char *buff)
307 uint8_t sp = 0, ch = 0;
308 uint8_t len = strlen(buff);
309 while (buff[sp] == ' ') sp++;
310 for (ch = 0; ch < (len - sp); ch++) buff[ch] = buff[ch + sp];
311 for (sp = ch; sp < len; sp++) buff[sp] = ' ';
315 * Converts distance into a string based on the current unit system
316 * prefixed by a a symbol to indicate the unit used.
317 * @param dist Distance in centimeters
319 static void osdFormatDistanceSymbol(char *buff, int32_t dist, uint8_t decimals)
321 switch ((osd_unit_e)osdConfig()->units) {
322 case OSD_UNIT_UK:
323 FALLTHROUGH;
324 case OSD_UNIT_IMPERIAL:
325 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_MILE, decimals, 3, 3)) {
326 buff[3] = SYM_DIST_MI;
327 } else {
328 buff[3] = SYM_DIST_FT;
330 buff[4] = '\0';
331 break;
332 case OSD_UNIT_METRIC_MPH:
333 FALLTHROUGH;
334 case OSD_UNIT_METRIC:
335 if (osdFormatCentiNumber(buff, dist, METERS_PER_KILOMETER, decimals, 3, 3)) {
336 buff[3] = SYM_DIST_KM;
337 } else {
338 buff[3] = SYM_DIST_M;
340 buff[4] = '\0';
341 break;
342 case OSD_UNIT_GA:
343 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_NAUTICALMILE, decimals, 3, 3)) {
344 buff[3] = SYM_DIST_NM;
345 } else {
346 buff[3] = SYM_DIST_FT;
348 buff[4] = '\0';
349 break;
354 * Converts distance into a string based on the current unit system.
355 * @param dist Distance in centimeters
357 static void osdFormatDistanceStr(char *buff, int32_t dist)
359 int32_t centifeet;
360 switch ((osd_unit_e)osdConfig()->units) {
361 case OSD_UNIT_UK:
362 FALLTHROUGH;
363 case OSD_UNIT_IMPERIAL:
364 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
365 if (abs(centifeet) < FEET_PER_MILE * 100 / 2) {
366 // Show feet when dist < 0.5mi
367 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
368 } else {
369 // Show miles when dist >= 0.5mi
370 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100*FEET_PER_MILE)),
371 (abs(centifeet) % (100 * FEET_PER_MILE)) / FEET_PER_MILE, SYM_MI);
373 break;
374 case OSD_UNIT_METRIC_MPH:
375 FALLTHROUGH;
376 case OSD_UNIT_METRIC:
377 if (abs(dist) < METERS_PER_KILOMETER * 100) {
378 // Show meters when dist < 1km
379 tfp_sprintf(buff, "%d%c", (int)(dist / 100), SYM_M);
380 } else {
381 // Show kilometers when dist >= 1km
382 tfp_sprintf(buff, "%d.%02d%c", (int)(dist / (100*METERS_PER_KILOMETER)),
383 (abs(dist) % (100 * METERS_PER_KILOMETER)) / METERS_PER_KILOMETER, SYM_KM);
385 break;
386 case OSD_UNIT_GA:
387 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
388 if (abs(centifeet) < 100000) {
389 // Show feet when dist < 1000ft
390 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
391 } else {
392 // Show nautical miles when dist >= 1000ft
393 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100 * FEET_PER_NAUTICALMILE)),
394 (int)((abs(centifeet) % (int)(100 * FEET_PER_NAUTICALMILE)) / FEET_PER_NAUTICALMILE), SYM_NM);
396 break;
401 * Converts velocity based on the current unit system (kmh or mph).
402 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
404 static int32_t osdConvertVelocityToUnit(int32_t vel)
406 switch ((osd_unit_e)osdConfig()->units) {
407 case OSD_UNIT_UK:
408 FALLTHROUGH;
409 case OSD_UNIT_METRIC_MPH:
410 FALLTHROUGH;
411 case OSD_UNIT_IMPERIAL:
412 return CMSEC_TO_CENTIMPH(vel) / 100; // Convert to mph
413 case OSD_UNIT_METRIC:
414 return CMSEC_TO_CENTIKPH(vel) / 100; // Convert to kmh
415 case OSD_UNIT_GA:
416 return CMSEC_TO_CENTIKNOTS(vel) / 100; // Convert to Knots
418 // Unreachable
419 return -1;
423 * Converts velocity into a string based on the current unit system.
424 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
425 * @param _3D is a 3D velocity
426 * @param _max is a maximum velocity
428 void osdFormatVelocityStr(char* buff, int32_t vel, bool _3D, bool _max)
430 switch ((osd_unit_e)osdConfig()->units) {
431 case OSD_UNIT_UK:
432 FALLTHROUGH;
433 case OSD_UNIT_METRIC_MPH:
434 FALLTHROUGH;
435 case OSD_UNIT_IMPERIAL:
436 if (_max) {
437 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
438 } else {
439 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
441 break;
442 case OSD_UNIT_METRIC:
443 if (_max) {
444 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
445 } else {
446 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
448 break;
449 case OSD_UNIT_GA:
450 if (_max) {
451 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
452 } else {
453 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
455 break;
460 * 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
462 static void osdGenerateAverageVelocityStr(char* buff) {
463 uint32_t cmPerSec = getTotalTravelDistance() / getFlightTime();
464 osdFormatVelocityStr(buff, cmPerSec, false, false);
468 * Converts wind speed into a string based on the current unit system, using
469 * always 3 digits and an additional character for the unit at the right. buff
470 * is null terminated.
471 * @param ws Raw wind speed in cm/s
473 #ifdef USE_WIND_ESTIMATOR
474 static void osdFormatWindSpeedStr(char *buff, int32_t ws, bool isValid)
476 int32_t centivalue;
477 char suffix;
478 switch (osdConfig()->units) {
479 case OSD_UNIT_UK:
480 FALLTHROUGH;
481 case OSD_UNIT_METRIC_MPH:
482 FALLTHROUGH;
483 case OSD_UNIT_IMPERIAL:
484 centivalue = CMSEC_TO_CENTIMPH(ws);
485 suffix = SYM_MPH;
486 break;
487 case OSD_UNIT_GA:
488 centivalue = CMSEC_TO_CENTIKNOTS(ws);
489 suffix = SYM_KT;
490 break;
491 default:
492 case OSD_UNIT_METRIC:
493 centivalue = CMSEC_TO_CENTIKPH(ws);
494 suffix = SYM_KMH;
495 break;
497 if (isValid) {
498 osdFormatCentiNumber(buff, centivalue, 0, 2, 0, 3);
499 } else {
500 buff[0] = buff[1] = buff[2] = '-';
502 buff[3] = suffix;
503 buff[4] = '\0';
505 #endif
508 * Converts altitude into a string based on the current unit system
509 * prefixed by a a symbol to indicate the unit used.
510 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
512 void osdFormatAltitudeSymbol(char *buff, int32_t alt)
514 int digits;
515 if (alt < 0) {
516 digits = 4;
517 } else {
518 digits = 3;
519 buff[0] = ' ';
521 switch ((osd_unit_e)osdConfig()->units) {
522 case OSD_UNIT_UK:
523 FALLTHROUGH;
524 case OSD_UNIT_GA:
525 FALLTHROUGH;
526 case OSD_UNIT_IMPERIAL:
527 if (osdFormatCentiNumber(buff + 4 - digits, CENTIMETERS_TO_CENTIFEET(alt), 1000, 0, 2, digits)) {
528 // Scaled to kft
529 buff[4] = SYM_ALT_KFT;
530 } else {
531 // Formatted in feet
532 buff[4] = SYM_ALT_FT;
534 buff[5] = '\0';
535 break;
536 case OSD_UNIT_METRIC_MPH:
537 FALLTHROUGH;
538 case OSD_UNIT_METRIC:
539 // alt is alredy in cm
540 if (osdFormatCentiNumber(buff + 4 - digits, alt, 1000, 0, 2, digits)) {
541 // Scaled to km
542 buff[4] = SYM_ALT_KM;
543 } else {
544 // Formatted in m
545 buff[4] = SYM_ALT_M;
547 buff[5] = '\0';
548 break;
553 * Converts altitude into a string based on the current unit system.
554 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
556 static void osdFormatAltitudeStr(char *buff, int32_t alt)
558 int32_t value;
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 value = CENTIMETERS_TO_FEET(alt);
566 tfp_sprintf(buff, "%d%c", (int)value, SYM_FT);
567 break;
568 case OSD_UNIT_METRIC_MPH:
569 FALLTHROUGH;
570 case OSD_UNIT_METRIC:
571 value = CENTIMETERS_TO_METERS(alt);
572 tfp_sprintf(buff, "%d%c", (int)value, SYM_M);
573 break;
577 static void osdFormatTime(char *buff, uint32_t seconds, char sym_m, char sym_h)
579 uint32_t value = seconds;
580 char sym = sym_m;
581 // Maximum value we can show in minutes is 99 minutes and 59 seconds
582 if (seconds > (99 * 60) + 59) {
583 sym = sym_h;
584 value = seconds / 60;
586 buff[0] = sym;
587 tfp_sprintf(buff + 1, "%02d:%02d", (int)(value / 60), (int)(value % 60));
590 static inline void osdFormatOnTime(char *buff)
592 osdFormatTime(buff, micros() / 1000000, SYM_ON_M, SYM_ON_H);
595 static inline void osdFormatFlyTime(char *buff, textAttributes_t *attr)
597 uint32_t seconds = getFlightTime();
598 osdFormatTime(buff, seconds, SYM_FLY_M, SYM_FLY_H);
599 if (attr && osdConfig()->time_alarm > 0) {
600 if (seconds / 60 >= osdConfig()->time_alarm && ARMING_FLAG(ARMED)) {
601 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
607 * Converts RSSI into a % value used by the OSD.
609 static uint16_t osdConvertRSSI(void)
611 // change range to [0, 99]
612 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE, 0, 99);
615 static uint16_t osdGetCrsfLQ(void)
617 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
618 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
619 int16_t displayedLQ;
620 switch (osdConfig()->crsf_lq_format) {
621 case OSD_CRSF_LQ_TYPE1:
622 displayedLQ = statsLQ;
623 break;
624 case OSD_CRSF_LQ_TYPE2:
625 displayedLQ = statsLQ;
626 break;
627 case OSD_CRSF_LQ_TYPE3:
628 displayedLQ = rxLinkStatistics.rfMode >= 2 ? scaledLQ : statsLQ;
629 break;
631 return displayedLQ;
634 static int16_t osdGetCrsfdBm(void)
636 return rxLinkStatistics.uplinkRSSI;
639 * Displays a temperature postfixed with a symbol depending on the current unit system
640 * @param label to display
641 * @param valid true if measurement is valid
642 * @param temperature in deciDegrees Celcius
644 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)
646 char buff[TEMPERATURE_LABEL_LEN + 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN + 2];
647 textAttributes_t elemAttr = valid ? TEXT_ATTRIBUTES_NONE : _TEXT_ATTRIBUTES_BLINK_BIT;
648 uint8_t valueXOffset = 0;
650 if (symbol) {
651 buff[0] = symbol;
652 buff[1] = '\0';
653 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
654 valueXOffset = 1;
656 #ifdef USE_TEMPERATURE_SENSOR
657 else if (label[0] != '\0') {
658 uint8_t label_len = strnlen(label, TEMPERATURE_LABEL_LEN);
659 memcpy(buff, label, label_len);
660 memset(buff + label_len, ' ', TEMPERATURE_LABEL_LEN + 1 - label_len);
661 buff[5] = '\0';
662 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
663 valueXOffset = osdConfig()->temp_label_align == OSD_ALIGN_LEFT ? 5 : label_len + 1;
665 #else
666 UNUSED(label);
667 #endif
669 if (valid) {
671 if ((temperature <= alarm_min) || (temperature >= alarm_max)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
672 if (osdConfig()->units == OSD_UNIT_IMPERIAL) temperature = temperature * 9 / 5.0f + 320;
673 tfp_sprintf(buff, "%3d", temperature / 10);
675 } else
676 strcpy(buff, "---");
678 buff[3] = osdConfig()->units == OSD_UNIT_IMPERIAL ? SYM_TEMP_F : SYM_TEMP_C;
679 buff[4] = '\0';
681 displayWriteWithAttr(osdDisplayPort, elemPosX + valueXOffset, elemPosY, buff, elemAttr);
684 #ifdef USE_TEMPERATURE_SENSOR
685 static void osdDisplayTemperatureSensor(uint8_t elemPosX, uint8_t elemPosY, uint8_t sensorIndex)
687 int16_t temperature;
688 const bool valid = getSensorTemperature(sensorIndex, &temperature);
689 const tempSensorConfig_t *sensorConfig = tempSensorConfig(sensorIndex);
690 uint16_t symbol = sensorConfig->osdSymbol ? SYM_TEMP_SENSOR_FIRST + sensorConfig->osdSymbol - 1 : 0;
691 osdDisplayTemperature(elemPosX, elemPosY, symbol, sensorConfig->label, valid, temperature, sensorConfig->alarm_min, sensorConfig->alarm_max);
693 #endif
695 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
697 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
698 const int coordinateLength = osdConfig()->coordinate_digits + 1;
700 buff[0] = sym;
701 int32_t integerPart = val / GPS_DEGREES_DIVIDER;
702 // Latitude maximum integer width is 3 (-90) while
703 // longitude maximum integer width is 4 (-180).
704 int integerDigits = tfp_sprintf(buff + 1, (integerPart == 0 && val < 0) ? "-%d" : "%d", (int)integerPart);
705 // We can show up to 7 digits in decimalPart.
706 int32_t decimalPart = abs(val % GPS_DEGREES_DIVIDER);
707 STATIC_ASSERT(GPS_DEGREES_DIVIDER == 1e7, adjust_max_decimal_digits);
708 int decimalDigits = tfp_sprintf(buff + 1 + integerDigits, "%07d", (int)decimalPart);
709 // Embbed the decimal separator
710 buff[1 + integerDigits - 1] += SYM_ZERO_HALF_TRAILING_DOT - '0';
711 buff[1 + integerDigits] += SYM_ZERO_HALF_LEADING_DOT - '0';
712 // Fill up to coordinateLength with zeros
713 int total = 1 + integerDigits + decimalDigits;
714 while(total < coordinateLength) {
715 buff[total] = '0';
716 total++;
718 buff[coordinateLength] = '\0';
721 static void osdFormatCraftName(char *buff)
723 if (strlen(systemConfig()->name) == 0)
724 strcpy(buff, "CRAFT_NAME");
725 else {
726 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
727 buff[i] = sl_toupper((unsigned char)systemConfig()->name[i]);
728 if (systemConfig()->name[i] == 0)
729 break;
734 static const char * osdArmingDisabledReasonMessage(void)
736 const char *message = NULL;
737 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
739 switch (isArmingDisabledReason()) {
740 case ARMING_DISABLED_FAILSAFE_SYSTEM:
741 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
742 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING) {
743 if (failsafeIsReceivingRxData()) {
744 // If we're not using sticks, it means the ARM switch
745 // hasn't been off since entering FAILSAFE_RX_LOSS_MONITORING
746 // yet
747 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF);
749 // Not receiving RX data
750 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
752 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS);
753 case ARMING_DISABLED_NOT_LEVEL:
754 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL);
755 case ARMING_DISABLED_SENSORS_CALIBRATING:
756 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL);
757 case ARMING_DISABLED_SYSTEM_OVERLOADED:
758 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED);
759 case ARMING_DISABLED_NAVIGATION_UNSAFE:
760 // Check the exact reason
761 switch (navigationIsBlockingArming(NULL)) {
762 char buf[6];
763 case NAV_ARMING_BLOCKER_NONE:
764 break;
765 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX:
766 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX);
767 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE:
768 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST);
769 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR:
770 osdFormatDistanceSymbol(buf, distanceToFirstWP(), 0);
771 tfp_sprintf(messageBuf, "FIRST WP TOO FAR (%s)", buf);
772 return message = messageBuf;
773 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR:
774 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG);
776 break;
777 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED:
778 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL);
779 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED:
780 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL);
781 case ARMING_DISABLED_ARM_SWITCH:
782 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST);
783 case ARMING_DISABLED_HARDWARE_FAILURE:
785 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
786 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE);
788 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
789 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL);
791 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
792 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL);
794 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
795 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL);
797 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
798 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL);
800 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
801 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL);
803 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
804 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL);
807 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL);
808 case ARMING_DISABLED_BOXFAILSAFE:
809 return OSD_MESSAGE_STR(OSD_MSG_FS_EN);
810 case ARMING_DISABLED_BOXKILLSWITCH:
811 return OSD_MESSAGE_STR(OSD_MSG_KILL_SW_EN);
812 case ARMING_DISABLED_RC_LINK:
813 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK);
814 case ARMING_DISABLED_THROTTLE:
815 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW);
816 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED:
817 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER);
818 case ARMING_DISABLED_SERVO_AUTOTRIM:
819 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE);
820 case ARMING_DISABLED_OOM:
821 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY);
822 case ARMING_DISABLED_INVALID_SETTING:
823 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
824 case ARMING_DISABLED_CLI:
825 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE);
826 case ARMING_DISABLED_PWM_OUTPUT_ERROR:
827 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR);
828 case ARMING_DISABLED_NO_PREARM:
829 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM);
830 case ARMING_DISABLED_DSHOT_BEEPER:
831 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER);
832 // Cases without message
833 case ARMING_DISABLED_CMS_MENU:
834 FALLTHROUGH;
835 case ARMING_DISABLED_OSD_MENU:
836 FALLTHROUGH;
837 case ARMING_DISABLED_ALL_FLAGS:
838 FALLTHROUGH;
839 case ARMED:
840 FALLTHROUGH;
841 case WAS_EVER_ARMED:
842 break;
844 return NULL;
847 static const char * osdFailsafePhaseMessage(void)
849 // See failsafe.h for each phase explanation
850 switch (failsafePhase()) {
851 case FAILSAFE_RETURN_TO_HOME:
852 // XXX: Keep this in sync with OSD_FLYMODE.
853 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS);
854 case FAILSAFE_LANDING:
855 // This should be considered an emergengy landing
856 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS);
857 case FAILSAFE_RX_LOSS_MONITORING:
858 // Only reachable from FAILSAFE_LANDED, which performs
859 // a disarm. Since aircraft has been disarmed, we no
860 // longer show failsafe details.
861 FALLTHROUGH;
862 case FAILSAFE_LANDED:
863 // Very brief, disarms and transitions into
864 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
865 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
866 // so we'll show the user how to re-arm in when
867 // that flag is the reason to prevent arming.
868 FALLTHROUGH;
869 case FAILSAFE_RX_LOSS_IDLE:
870 // This only happens when user has chosen NONE as FS
871 // procedure. The recovery messages should be enough.
872 FALLTHROUGH;
873 case FAILSAFE_IDLE:
874 // Failsafe not active
875 FALLTHROUGH;
876 case FAILSAFE_RX_LOSS_DETECTED:
877 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
878 // or the FS procedure immediately.
879 FALLTHROUGH;
880 case FAILSAFE_RX_LOSS_RECOVERED:
881 // Exiting failsafe
882 break;
884 return NULL;
887 static const char * osdFailsafeInfoMessage(void)
889 if (failsafeIsReceivingRxData()) {
890 // User must move sticks to exit FS mode
891 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS);
893 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
895 #if defined(USE_SAFE_HOME)
896 static const char * divertingToSafehomeMessage(void)
898 if (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && safehome_applied) {
899 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME);
901 return NULL;
903 #endif
905 static const char * navigationStateMessage(void)
907 switch (NAV_Status.state) {
908 case MW_NAV_STATE_NONE:
909 break;
910 case MW_NAV_STATE_RTH_START:
911 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH);
912 case MW_NAV_STATE_RTH_CLIMB:
913 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB);
914 case MW_NAV_STATE_RTH_ENROUTE:
915 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME);
916 case MW_NAV_STATE_HOLD_INFINIT:
917 // Used by HOLD flight modes. No information to add.
918 break;
919 case MW_NAV_STATE_HOLD_TIMED:
920 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
921 break;
922 case MW_NAV_STATE_WP_ENROUTE:
923 // "TO WP" + WP countdown added in osdGetSystemMessage
924 break;
925 case MW_NAV_STATE_PROCESS_NEXT:
926 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP);
927 case MW_NAV_STATE_DO_JUMP:
928 // Not used
929 break;
930 case MW_NAV_STATE_LAND_START:
931 // Not used
932 break;
933 case MW_NAV_STATE_EMERGENCY_LANDING:
934 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING);
935 case MW_NAV_STATE_LAND_IN_PROGRESS:
936 return OSD_MESSAGE_STR(OSD_MSG_LANDING);
937 case MW_NAV_STATE_HOVER_ABOVE_HOME:
938 if (STATE(FIXED_WING_LEGACY)) {
939 #if defined(USE_SAFE_HOME)
940 if (safehome_applied) {
941 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME);
943 #endif
944 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME);
946 return OSD_MESSAGE_STR(OSD_MSG_HOVERING);
947 case MW_NAV_STATE_LANDED:
948 return OSD_MESSAGE_STR(OSD_MSG_LANDED);
949 case MW_NAV_STATE_LAND_SETTLE:
950 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND);
951 case MW_NAV_STATE_LAND_START_DESCENT:
952 // Not used
953 break;
955 return NULL;
958 static void osdFormatMessage(char *buff, size_t size, const char *message, bool isCenteredText)
960 // String is always filled with Blanks
961 memset(buff, SYM_BLANK, size);
962 if (message) {
963 size_t messageLength = strlen(message);
964 int rem = isCenteredText ? MAX(0, (int)size - (int)messageLength) : 0;
965 strncpy(buff + rem / 2, message, MIN((int)size - rem / 2, (int)messageLength));
967 // Ensure buff is zero terminated
968 buff[size - 1] = '\0';
972 * Draws the battery symbol filled in accordingly to the
973 * battery voltage to buff[0].
975 static void osdFormatBatteryChargeSymbol(char *buff)
977 uint8_t p = calculateBatteryPercentage();
978 p = (100 - p) / 16.6;
979 buff[0] = SYM_BATT_FULL + p;
982 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t *attr)
984 if ((getBatteryState() != BATTERY_NOT_PRESENT) && ((batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical)) || ((!batteryUsesCapacityThresholds()) && (getBatteryVoltage() <= getBatteryWarningVoltage()))))
985 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
988 void osdCrosshairPosition(uint8_t *x, uint8_t *y)
990 *x = osdDisplayPort->cols / 2;
991 *y = osdDisplayPort->rows / 2;
992 *y += osdConfig()->horizon_offset;
996 * Formats throttle position prefixed by its symbol.
997 * Shows output to motor, not stick position
999 static void osdFormatThrottlePosition(char *buff, bool autoThr, textAttributes_t *elemAttr)
1001 buff[0] = SYM_BLANK;
1002 buff[1] = SYM_THR;
1003 if (autoThr && navigationIsControllingThrottle()) {
1004 buff[0] = SYM_AUTO_THR0;
1005 buff[1] = SYM_AUTO_THR1;
1006 if (isFixedWingAutoThrottleManuallyIncreased()) {
1007 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1010 #ifdef USE_POWER_LIMITS
1011 if (powerLimiterIsLimiting()) {
1012 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1014 #endif
1015 tfp_sprintf(buff + 2, "%3d", getThrottlePercent());
1019 * Formats gvars prefixed by its number (0-indexed). If autoThr
1021 static void osdFormatGVar(char *buff, uint8_t index)
1023 buff[0] = 'G';
1024 buff[1] = '0'+index;
1025 buff[2] = ':';
1026 #ifdef USE_PROGRAMMING_FRAMEWORK
1027 osdFormatCentiNumber(buff + 3, (int32_t)gvGet(index)*(int32_t)100, 1, 0, 0, 5);
1028 #endif
1031 #if defined(USE_ESC_SENSOR)
1032 static void osdFormatRpm(char *buff, uint32_t rpm)
1034 buff[0] = SYM_RPM;
1035 if (rpm) {
1036 if ( digitCount(rpm) > osdConfig()->esc_rpm_precision) {
1037 uint8_t rpmMaxDecimals = (osdConfig()->esc_rpm_precision - 3);
1038 osdFormatCentiNumber(buff + 1, rpm / 10, 0, rpmMaxDecimals, rpmMaxDecimals, osdConfig()->esc_rpm_precision-1);
1039 buff[osdConfig()->esc_rpm_precision] = 'K';
1040 buff[osdConfig()->esc_rpm_precision+1] = '\0';
1042 else {
1043 switch(osdConfig()->esc_rpm_precision) {
1044 case 6:
1045 tfp_sprintf(buff + 1, "%6lu", rpm);
1046 break;
1047 case 5:
1048 tfp_sprintf(buff + 1, "%5lu", rpm);
1049 break;
1050 case 4:
1051 tfp_sprintf(buff + 1, "%4lu", rpm);
1052 break;
1053 case 3:
1054 default:
1055 tfp_sprintf(buff + 1, "%3lu", rpm);
1056 break;
1062 else {
1063 uint8_t buffPos = 1;
1064 while (buffPos <=( osdConfig()->esc_rpm_precision)) {
1065 strcpy(buff + buffPos++, "-");
1069 #endif
1071 int32_t osdGetAltitude(void)
1073 return getEstimatedActualPosition(Z);
1076 static inline int32_t osdGetAltitudeMsl(void)
1078 return getEstimatedActualPosition(Z)+GPS_home.alt;
1081 static bool osdIsHeadingValid(void)
1083 #ifdef USE_SECONDARY_IMU
1084 if (secondaryImuState.active && secondaryImuConfig()->useForOsdHeading) {
1085 return true;
1086 } else {
1087 return isImuHeadingValid();
1089 #else
1090 return isImuHeadingValid();
1091 #endif
1094 int16_t osdGetHeading(void)
1096 #ifdef USE_SECONDARY_IMU
1097 if (secondaryImuState.active && secondaryImuConfig()->useForOsdHeading) {
1098 return secondaryImuState.eulerAngles.values.yaw;
1099 } else {
1100 return attitude.values.yaw;
1102 #else
1103 return attitude.values.yaw;
1104 #endif
1107 int16_t osdPanServoHomeDirectionOffset(void)
1109 int8_t servoIndex = osdConfig()->pan_servo_index;
1110 int16_t servoPosition = servo[servoIndex];
1111 int16_t servoMiddle = servoParams(servoIndex)->middle;
1112 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition - servoMiddle) * osdConfig()->pan_servo_pwm2centideg);
1115 // Returns a heading angle in degrees normalized to [0, 360).
1116 int osdGetHeadingAngle(int angle)
1118 while (angle < 0) {
1119 angle += 360;
1121 while (angle >= 360) {
1122 angle -= 360;
1124 return angle;
1127 #if defined(USE_GPS)
1129 /* Draws a map with the given symbol in the center and given point of interest
1130 * defined by its distance in meters and direction in degrees.
1131 * referenceHeading indicates the up direction in the map, in degrees, while
1132 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1133 * arrow to indicate the map reference to the user. The drawn argument is an
1134 * in-out used to store the last position where the craft was drawn to avoid
1135 * erasing all screen on each redraw.
1137 static void osdDrawMap(int referenceHeading, uint16_t referenceSym, uint16_t centerSym,
1138 uint32_t poiDistance, int16_t poiDirection, uint16_t poiSymbol,
1139 uint16_t *drawn, uint32_t *usedScale)
1141 // TODO: These need to be tested with several setups. We might
1142 // need to make them configurable.
1143 const int hMargin = 5;
1144 const int vMargin = 3;
1146 // TODO: Get this from the display driver?
1147 const int charWidth = 12;
1148 const int charHeight = 18;
1150 uint8_t minX = hMargin;
1151 uint8_t maxX = osdDisplayPort->cols - 1 - hMargin;
1152 uint8_t minY = vMargin;
1153 uint8_t maxY = osdDisplayPort->rows - 1 - vMargin;
1154 uint8_t midX = osdDisplayPort->cols / 2;
1155 uint8_t midY = osdDisplayPort->rows / 2;
1157 // Fixed marks
1158 displayWriteChar(osdDisplayPort, midX, midY, centerSym);
1160 // First, erase the previous drawing.
1161 if (OSD_VISIBLE(*drawn)) {
1162 displayWriteChar(osdDisplayPort, OSD_X(*drawn), OSD_Y(*drawn), SYM_BLANK);
1163 *drawn = 0;
1166 uint32_t initialScale;
1167 const unsigned scaleMultiplier = 2;
1168 // We try to reduce the scale when the POI will be around half the distance
1169 // between the center and the closers map edge, to avoid too much jumping
1170 const int scaleReductionMultiplier = MIN(midX - hMargin, midY - vMargin) / 2;
1172 switch (osdConfig()->units) {
1173 case OSD_UNIT_UK:
1174 FALLTHROUGH;
1175 case OSD_UNIT_IMPERIAL:
1176 initialScale = 16; // 16m ~= 0.01miles
1177 break;
1178 case OSD_UNIT_GA:
1179 initialScale = 18; // 18m ~= 0.01 nautical miles
1180 break;
1181 default:
1182 case OSD_UNIT_METRIC_MPH:
1183 FALLTHROUGH;
1184 case OSD_UNIT_METRIC:
1185 initialScale = 10; // 10m as initial scale
1186 break;
1189 // Try to keep the same scale when getting closer until we draw over the center point
1190 uint32_t scale = initialScale;
1191 if (*usedScale) {
1192 scale = *usedScale;
1193 if (scale > initialScale && poiDistance < *usedScale * scaleReductionMultiplier) {
1194 scale /= scaleMultiplier;
1198 if (STATE(GPS_FIX)) {
1200 int directionToPoi = osdGetHeadingAngle(poiDirection - referenceHeading);
1201 float poiAngle = DEGREES_TO_RADIANS(directionToPoi);
1202 float poiSin = sin_approx(poiAngle);
1203 float poiCos = cos_approx(poiAngle);
1205 // Now start looking for a valid scale that lets us draw everything
1206 int ii;
1207 for (ii = 0; ii < 50; ii++) {
1208 // Calculate location of the aircraft in map
1209 int points = poiDistance / ((float)scale / charHeight);
1211 float pointsX = points * poiSin;
1212 int poiX = midX - roundf(pointsX / charWidth);
1213 if (poiX < minX || poiX > maxX) {
1214 scale *= scaleMultiplier;
1215 continue;
1218 float pointsY = points * poiCos;
1219 int poiY = midY + roundf(pointsY / charHeight);
1220 if (poiY < minY || poiY > maxY) {
1221 scale *= scaleMultiplier;
1222 continue;
1225 if (poiX == midX && poiY == midY) {
1226 // We're over the map center symbol, so we would be drawing
1227 // over it even if we increased the scale. Alternate between
1228 // drawing the center symbol or drawing the POI.
1229 if (centerSym != SYM_BLANK && OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1230 break;
1232 } else {
1234 uint16_t c;
1235 if (displayReadCharWithAttr(osdDisplayPort, poiX, poiY, &c, NULL) && c != SYM_BLANK) {
1236 // Something else written here, increase scale. If the display doesn't support reading
1237 // back characters, we assume there's nothing.
1239 // If we're close to the center, decrease scale. Otherwise increase it.
1240 uint8_t centerDeltaX = (maxX - minX) / (scaleMultiplier * 2);
1241 uint8_t centerDeltaY = (maxY - minY) / (scaleMultiplier * 2);
1242 if (poiX >= midX - centerDeltaX && poiX <= midX + centerDeltaX &&
1243 poiY >= midY - centerDeltaY && poiY <= midY + centerDeltaY &&
1244 scale > scaleMultiplier) {
1246 scale /= scaleMultiplier;
1247 } else {
1248 scale *= scaleMultiplier;
1250 continue;
1254 // Draw the point on the map
1255 if (poiSymbol == SYM_ARROW_UP) {
1256 // Drawing aircraft, rotate
1257 int mapHeading = osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading);
1258 poiSymbol += mapHeading * 2 / 45;
1260 displayWriteChar(osdDisplayPort, poiX, poiY, poiSymbol);
1262 // Update saved location
1263 *drawn = OSD_POS(poiX, poiY) | OSD_VISIBLE_FLAG;
1264 break;
1268 *usedScale = scale;
1270 // Update global map data for scale and reference
1271 osdMapData.scale = scale;
1272 osdMapData.referenceSymbol = referenceSym;
1275 /* Draws a map with the home in the center and the craft moving around.
1276 * See osdDrawMap() for reference.
1278 static void osdDrawHomeMap(int referenceHeading, uint8_t referenceSym, uint16_t *drawn, uint32_t *usedScale)
1280 osdDrawMap(referenceHeading, referenceSym, SYM_HOME, GPS_distanceToHome, GPS_directionToHome, SYM_ARROW_UP, drawn, usedScale);
1283 /* Draws a map with the aircraft in the center and the home moving around.
1284 * See osdDrawMap() for reference.
1286 static void osdDrawRadar(uint16_t *drawn, uint32_t *usedScale)
1288 int16_t reference = DECIDEGREES_TO_DEGREES(osdGetHeading());
1289 int16_t poiDirection = osdGetHeadingAngle(GPS_directionToHome + 180);
1290 osdDrawMap(reference, 0, SYM_ARROW_UP, GPS_distanceToHome, poiDirection, SYM_HOME, drawn, usedScale);
1293 static uint16_t crc_accumulate(uint8_t data, uint16_t crcAccum)
1295 uint8_t tmp;
1296 tmp = data ^ (uint8_t)(crcAccum & 0xff);
1297 tmp ^= (tmp << 4);
1298 crcAccum = (crcAccum >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
1299 return crcAccum;
1303 static void osdDisplayTelemetry(void)
1305 uint32_t trk_data;
1306 uint16_t trk_crc = 0;
1307 char trk_buffer[31];
1308 static int16_t trk_elevation = 127;
1309 static uint16_t trk_bearing = 0;
1311 if (ARMING_FLAG(ARMED)) {
1312 if (STATE(GPS_FIX)){
1313 if (GPS_distanceToHome > 5) {
1314 trk_bearing = GPS_directionToHome;
1315 trk_bearing += 360 + 180;
1316 trk_bearing %= 360;
1317 int32_t alt = CENTIMETERS_TO_METERS(osdGetAltitude());
1318 float at = atan2(alt, GPS_distanceToHome);
1319 trk_elevation = (float)at * 57.2957795; // 57.2957795 = 1 rad
1320 trk_elevation += 37; // because elevation in telemetry should be from -37 to 90
1321 if (trk_elevation < 0) {
1322 trk_elevation = 0;
1327 else{
1328 trk_elevation = 127;
1329 trk_bearing = 0;
1332 trk_data = 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1333 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.
1334 trk_data = trk_data | (uint32_t)trk_bearing << 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1335 trk_crc = crc_accumulate(0xFF & trk_data, trk_crc); // CRC First Byte bits 0-7
1336 trk_crc = crc_accumulate(0xFF & trk_bearing, trk_crc); // CRC Second Byte bits 8-15
1337 trk_crc = crc_accumulate(trk_bearing >> 8, trk_crc); // CRC Third Byte bits 16-17
1338 trk_data = trk_data | (uint32_t)trk_crc << 17; // bits 18-29 CRC & 0x3FFFF
1340 for (uint8_t t_ctr = 0; t_ctr < 30; t_ctr++) { // Prepare screen buffer and write data line.
1341 if (trk_data & (uint32_t)1 << t_ctr){
1342 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_0;
1344 else{
1345 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_1;
1348 trk_buffer[30] = 0;
1349 displayWrite(osdDisplayPort, 0, 0, trk_buffer);
1350 if (osdConfig()->telemetry>1){
1351 displayWrite(osdDisplayPort, 0, 3, trk_buffer); // Test display because normal telemetry line is not visible
1354 #endif
1356 static void osdFormatPidControllerOutput(char *buff, const char *label, const pidController_t *pidController, uint8_t scale, bool showDecimal) {
1357 strcpy(buff, label);
1358 for (uint8_t i = strlen(label); i < 5; ++i) buff[i] = ' ';
1359 uint8_t decimals = showDecimal ? 1 : 0;
1360 osdFormatCentiNumber(buff + 5, pidController->proportional * scale, 0, decimals, 0, 4);
1361 buff[9] = ' ';
1362 osdFormatCentiNumber(buff + 10, pidController->integrator * scale, 0, decimals, 0, 4);
1363 buff[14] = ' ';
1364 osdFormatCentiNumber(buff + 15, pidController->derivative * scale, 0, decimals, 0, 4);
1365 buff[19] = ' ';
1366 osdFormatCentiNumber(buff + 20, pidController->output_constrained * scale, 0, decimals, 0, 4);
1367 buff[24] = '\0';
1370 static void osdDisplayBatteryVoltage(uint8_t elemPosX, uint8_t elemPosY, uint16_t voltage, uint8_t digits, uint8_t decimals)
1372 char buff[6];
1373 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1375 osdFormatBatteryChargeSymbol(buff);
1376 buff[1] = '\0';
1377 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1378 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1380 elemAttr = TEXT_ATTRIBUTES_NONE;
1381 digits = MIN(digits, 4);
1382 osdFormatCentiNumber(buff, voltage, 0, decimals, 0, digits);
1383 buff[digits] = SYM_VOLT;
1384 buff[digits+1] = '\0';
1385 if ((getBatteryState() != BATTERY_NOT_PRESENT) && (getBatteryVoltage() <= getBatteryWarningVoltage()))
1386 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1387 displayWriteWithAttr(osdDisplayPort, elemPosX + 1, elemPosY, buff, elemAttr);
1390 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)
1392 textAttributes_t elemAttr;
1393 char buff[4];
1395 const pid8_t *pid = &pidBank()->pid[pidIndex];
1396 pidType_e pidType = pidIndexGetType(pidIndex);
1398 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1400 if (pidType == PID_TYPE_NONE) {
1401 // PID is not used in this configuration. Draw dashes.
1402 // XXX: Keep this in sync with the %3d format and spacing used below
1403 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - - -");
1404 return;
1407 elemAttr = TEXT_ATTRIBUTES_NONE;
1408 tfp_sprintf(buff, "%3d", pid->P);
1409 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1410 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1411 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1413 elemAttr = TEXT_ATTRIBUTES_NONE;
1414 tfp_sprintf(buff, "%3d", pid->I);
1415 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1416 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1417 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1419 elemAttr = TEXT_ATTRIBUTES_NONE;
1420 tfp_sprintf(buff, "%3d", pid->D);
1421 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1422 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1423 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1425 elemAttr = TEXT_ATTRIBUTES_NONE;
1426 tfp_sprintf(buff, "%3d", pid->FF);
1427 if ((isAdjustmentFunctionSelected(adjFuncFF)) || (((adjFuncFF == ADJUSTMENT_ROLL_FF) || (adjFuncFF == ADJUSTMENT_PITCH_FF)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF))))
1428 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1429 displayWriteWithAttr(osdDisplayPort, elemPosX + 16, elemPosY, buff, elemAttr);
1432 static void osdDisplayNavPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD)
1434 textAttributes_t elemAttr;
1435 char buff[4];
1437 const pid8_t *pid = &pidBank()->pid[pidIndex];
1438 pidType_e pidType = pidIndexGetType(pidIndex);
1440 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1442 if (pidType == PID_TYPE_NONE) {
1443 // PID is not used in this configuration. Draw dashes.
1444 // XXX: Keep this in sync with the %3d format and spacing used below
1445 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - -");
1446 return;
1449 elemAttr = TEXT_ATTRIBUTES_NONE;
1450 tfp_sprintf(buff, "%3d", pid->P);
1451 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1452 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1453 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1455 elemAttr = TEXT_ATTRIBUTES_NONE;
1456 tfp_sprintf(buff, "%3d", pid->I);
1457 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1458 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1459 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1461 elemAttr = TEXT_ATTRIBUTES_NONE;
1462 tfp_sprintf(buff, "%3d", pidType == PID_TYPE_PIFF ? pid->FF : pid->D);
1463 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1464 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1465 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1468 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) {
1469 char buff[8];
1470 textAttributes_t elemAttr;
1471 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1473 elemAttr = TEXT_ATTRIBUTES_NONE;
1474 osdFormatCentiNumber(buff, value * 100, 0, maxDecimals, 0, MIN(valueLength, 8));
1475 if (isAdjustmentFunctionSelected(adjFunc))
1476 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1477 displayWriteWithAttr(osdDisplayPort, elemPosX + strlen(str) + 1 + valueOffset, elemPosY, buff, elemAttr);
1480 int8_t getGeoWaypointNumber(int8_t waypointIndex)
1482 static int8_t lastWaypointIndex = 1;
1483 static int8_t geoWaypointIndex;
1485 if (waypointIndex != lastWaypointIndex) {
1486 lastWaypointIndex = geoWaypointIndex = waypointIndex;
1487 for (uint8_t i = 0; i <= waypointIndex; i++) {
1488 if (posControl.waypointList[i].action == NAV_WP_ACTION_SET_POI ||
1489 posControl.waypointList[i].action == NAV_WP_ACTION_SET_HEAD ||
1490 posControl.waypointList[i].action == NAV_WP_ACTION_JUMP) {
1491 geoWaypointIndex -= 1;
1496 return geoWaypointIndex + 1;
1499 void osdDisplaySwitchIndicator(const char *swName, int rcValue, char *buff) {
1500 int8_t charRemainder = OSD_SWITCH_INDICATOR_NAME_LENGTH;
1501 int8_t ptr = 0;
1503 for (ptr = strlen(swName); ptr > 0; ptr--) {
1504 buff[--charRemainder] = swName[ptr-1];
1507 if (charRemainder > 0) {
1508 for (ptr = 0; ptr < charRemainder; ptr++) {
1509 buff[ptr] = SYM_BLANK;
1513 if ( rcValue < 1333) {
1514 buff[OSD_SWITCH_INDICATOR_NAME_LENGTH] = SYM_SWITCH_INDICATOR_LOW;
1515 } else if ( rcValue > 1666) {
1516 buff[OSD_SWITCH_INDICATOR_NAME_LENGTH] = SYM_SWITCH_INDICATOR_HIGH;
1517 } else {
1518 buff[OSD_SWITCH_INDICATOR_NAME_LENGTH] = SYM_SWITCH_INDICATOR_MID;
1521 buff[OSD_SWITCH_INDICATOR_NAME_LENGTH + 1] = '\0';
1524 static bool osdDrawSingleElement(uint8_t item)
1526 uint16_t pos = osdLayoutsConfig()->item_pos[currentLayout][item];
1527 if (!OSD_VISIBLE(pos)) {
1528 return false;
1530 uint8_t elemPosX = OSD_X(pos);
1531 uint8_t elemPosY = OSD_Y(pos);
1532 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1533 char buff[32] = {0};
1535 switch (item) {
1536 case OSD_RSSI_VALUE:
1538 uint16_t osdRssi = osdConvertRSSI();
1539 buff[0] = SYM_RSSI;
1540 tfp_sprintf(buff + 1, "%2d", osdRssi);
1541 if (osdRssi < osdConfig()->rssi_alarm) {
1542 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1544 break;
1547 case OSD_MAIN_BATT_VOLTAGE:
1548 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawVoltage(), 2 + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1549 return true;
1551 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE:
1552 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedVoltage(), 2 + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1553 return true;
1555 case OSD_CURRENT_DRAW:
1556 osdFormatCentiNumber(buff, getAmperage(), 0, 2, 0, 3);
1557 buff[3] = SYM_AMP;
1558 buff[4] = '\0';
1560 uint8_t current_alarm = osdConfig()->current_alarm;
1561 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
1562 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1564 break;
1566 case OSD_MAH_DRAWN:
1567 tfp_sprintf(buff, "%4d", (int)getMAhDrawn());
1568 buff[4] = SYM_MAH;
1569 buff[5] = '\0';
1570 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1571 break;
1573 case OSD_WH_DRAWN:
1574 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
1575 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1576 buff[3] = SYM_WH;
1577 buff[4] = '\0';
1578 break;
1580 case OSD_BATTERY_REMAINING_CAPACITY:
1582 if (currentBatteryProfile->capacity.value == 0)
1583 tfp_sprintf(buff, " NA");
1584 else if (!batteryWasFullWhenPluggedIn())
1585 tfp_sprintf(buff, " NF");
1586 else if (currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH)
1587 tfp_sprintf(buff, "%4lu", getBatteryRemainingCapacity());
1588 else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1589 osdFormatCentiNumber(buff + 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3);
1591 buff[4] = currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH ? SYM_MAH : SYM_WH;
1592 buff[5] = '\0';
1594 if ((getBatteryState() != BATTERY_NOT_PRESENT) && batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical))
1595 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1597 break;
1599 case OSD_BATTERY_REMAINING_PERCENT:
1600 osdFormatBatteryChargeSymbol(buff);
1601 tfp_sprintf(buff + 1, "%3d%%", calculateBatteryPercentage());
1602 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1603 break;
1605 case OSD_POWER_SUPPLY_IMPEDANCE:
1606 if (isPowerSupplyImpedanceValid())
1607 tfp_sprintf(buff, "%3d", getPowerSupplyImpedance());
1608 else
1609 strcpy(buff, "---");
1610 buff[3] = SYM_MILLIOHM;
1611 buff[4] = '\0';
1612 break;
1614 #ifdef USE_GPS
1615 case OSD_GPS_SATS:
1616 buff[0] = SYM_SAT_L;
1617 buff[1] = SYM_SAT_R;
1618 tfp_sprintf(buff + 2, "%2d", gpsSol.numSat);
1619 if (!STATE(GPS_FIX)) {
1620 if (getHwGPSStatus() == HW_SENSOR_UNAVAILABLE || getHwGPSStatus() == HW_SENSOR_UNHEALTHY) {
1621 strcpy(buff + 2, "X!");
1623 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1625 break;
1627 case OSD_GPS_SPEED:
1628 osdFormatVelocityStr(buff, gpsSol.groundSpeed, false, false);
1629 break;
1631 case OSD_GPS_MAX_SPEED:
1632 osdFormatVelocityStr(buff, stats.max_speed, false, true);
1633 break;
1635 case OSD_3D_SPEED:
1636 osdFormatVelocityStr(buff, osdGet3DSpeed(), true, false);
1637 break;
1639 case OSD_3D_MAX_SPEED:
1640 osdFormatVelocityStr(buff, stats.max_3D_speed, true, true);
1641 break;
1643 case OSD_GLIDESLOPE:
1645 float horizontalSpeed = gpsSol.groundSpeed;
1646 float sinkRate = -getEstimatedActualVelocity(Z);
1647 static pt1Filter_t gsFilterState;
1648 const timeMs_t currentTimeMs = millis();
1649 static timeMs_t gsUpdatedTimeMs;
1650 float glideSlope = horizontalSpeed / sinkRate;
1651 glideSlope = pt1FilterApply4(&gsFilterState, isnormal(glideSlope) ? glideSlope : 200, 0.5, MS2S(currentTimeMs - gsUpdatedTimeMs));
1652 gsUpdatedTimeMs = currentTimeMs;
1654 buff[0] = SYM_GLIDESLOPE;
1655 if (glideSlope > 0.0f && glideSlope < 100.0f) {
1656 osdFormatCentiNumber(buff + 1, glideSlope * 100.0f, 0, 2, 0, 3);
1657 } else {
1658 buff[1] = buff[2] = buff[3] = '-';
1660 buff[4] = '\0';
1661 break;
1664 case OSD_GPS_LAT:
1665 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
1666 break;
1668 case OSD_GPS_LON:
1669 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
1670 break;
1672 case OSD_HOME_DIR:
1674 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
1675 if (GPS_distanceToHome < (navConfig()->general.min_rth_distance / 100) ) {
1676 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_HOME_NEAR);
1678 else
1680 int16_t panHomeDirOffset = 0;
1681 if (!(osdConfig()->pan_servo_pwm2centideg == 0)){
1682 panHomeDirOffset = osdPanServoHomeDirectionOffset();
1684 int homeDirection = GPS_directionToHome - DECIDEGREES_TO_DEGREES(osdGetHeading()) + panHomeDirOffset;
1685 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), homeDirection);
1687 } else {
1688 // No home or no fix or unknown heading, blink.
1689 // If we're unarmed, show the arrow pointing up so users can see the arrow
1690 // while configuring the OSD. If we're armed, show a '-' indicating that
1691 // we don't know the direction to home.
1692 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1693 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, ARMING_FLAG(ARMED) ? '-' : SYM_ARROW_UP, elemAttr);
1695 return true;
1698 case OSD_HOME_HEADING_ERROR:
1700 buff[0] = SYM_HOME;
1701 buff[1] = SYM_HEADING;
1703 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1704 int16_t h = lrintf(CENTIDEGREES_TO_DEGREES((float)wrap_18000(DEGREES_TO_CENTIDEGREES((int32_t)GPS_directionToHome) - DECIDEGREES_TO_CENTIDEGREES((int32_t)osdGetHeading()))));
1705 tfp_sprintf(buff + 2, "%4d", h);
1706 } else {
1707 strcpy(buff + 2, "----");
1710 buff[6] = SYM_DEGREES;
1711 buff[7] = '\0';
1712 break;
1715 case OSD_HOME_DIST:
1717 buff[0] = SYM_HOME;
1718 osdFormatDistanceSymbol(&buff[1], GPS_distanceToHome * 100, 0);
1719 uint16_t dist_alarm = osdConfig()->dist_alarm;
1720 if (dist_alarm > 0 && GPS_distanceToHome > dist_alarm) {
1721 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1724 break;
1726 case OSD_TRIP_DIST:
1727 buff[0] = SYM_TOTAL;
1728 osdFormatDistanceSymbol(buff + 1, getTotalTravelDistance(), 0);
1729 break;
1731 case OSD_HEADING:
1733 buff[0] = SYM_HEADING;
1734 if (osdIsHeadingValid()) {
1735 int16_t h = DECIDEGREES_TO_DEGREES(osdGetHeading());
1736 if (h < 0) {
1737 h += 360;
1739 tfp_sprintf(&buff[1], "%3d", h);
1740 } else {
1741 buff[1] = buff[2] = buff[3] = '-';
1743 buff[4] = SYM_DEGREES;
1744 buff[5] = '\0';
1745 break;
1748 case OSD_COURSE_HOLD_ERROR:
1750 if (ARMING_FLAG(ARMED) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1751 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1752 return true;
1755 buff[0] = SYM_HEADING;
1757 if ((!ARMING_FLAG(ARMED)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && isAdjustingPosition())) {
1758 buff[1] = buff[2] = buff[3] = '-';
1759 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1760 int16_t herr = lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
1761 if (ABS(herr) > 99)
1762 strcpy(buff + 1, ">99");
1763 else
1764 tfp_sprintf(buff + 1, "%3d", herr);
1767 buff[4] = SYM_DEGREES;
1768 buff[5] = '\0';
1769 break;
1772 case OSD_COURSE_HOLD_ADJUSTMENT:
1774 int16_t heading_adjust = lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
1776 if (ARMING_FLAG(ARMED) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust != 0)))) {
1777 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1778 return true;
1781 buff[0] = SYM_HEADING;
1783 if (!ARMING_FLAG(ARMED)) {
1784 buff[1] = buff[2] = buff[3] = buff[4] = '-';
1785 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1786 tfp_sprintf(buff + 1, "%4d", heading_adjust);
1789 buff[5] = SYM_DEGREES;
1790 buff[6] = '\0';
1791 break;
1794 case OSD_GPS_HDOP:
1796 buff[0] = SYM_HDP_L;
1797 buff[1] = SYM_HDP_R;
1798 int32_t centiHDOP = 100 * gpsSol.hdop / HDOP_SCALE;
1799 osdFormatCentiNumber(&buff[2], centiHDOP, 0, 1, 0, 2);
1800 break;
1803 case OSD_MAP_NORTH:
1805 static uint16_t drawn = 0;
1806 static uint32_t scale = 0;
1807 osdDrawHomeMap(0, 'N', &drawn, &scale);
1808 return true;
1810 case OSD_MAP_TAKEOFF:
1812 static uint16_t drawn = 0;
1813 static uint32_t scale = 0;
1814 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn, &scale);
1815 return true;
1817 case OSD_RADAR:
1819 static uint16_t drawn = 0;
1820 static uint32_t scale = 0;
1821 osdDrawRadar(&drawn, &scale);
1822 return true;
1824 #endif // GPS
1826 case OSD_ALTITUDE:
1828 int32_t alt = osdGetAltitude();
1829 osdFormatAltitudeSymbol(buff, alt);
1830 uint16_t alt_alarm = osdConfig()->alt_alarm;
1831 uint16_t neg_alt_alarm = osdConfig()->neg_alt_alarm;
1832 if ((alt_alarm > 0 && CENTIMETERS_TO_METERS(alt) > alt_alarm) ||
1833 (neg_alt_alarm > 0 && alt < 0 && -CENTIMETERS_TO_METERS(alt) > neg_alt_alarm)) {
1835 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1837 break;
1840 case OSD_ALTITUDE_MSL:
1842 int32_t alt = osdGetAltitudeMsl();
1843 osdFormatAltitudeSymbol(buff, alt);
1844 break;
1847 #ifdef USE_RANGEFINDER
1848 case OSD_RANGEFINDER:
1850 int32_t range = rangefinderGetLatestRawAltitude();
1851 if (range < 0) {
1852 buff[0] = '-';
1853 } else {
1854 osdFormatDistanceSymbol(buff, range, 1);
1857 break;
1858 #endif
1860 case OSD_ONTIME:
1862 osdFormatOnTime(buff);
1863 break;
1866 case OSD_FLYTIME:
1868 osdFormatFlyTime(buff, &elemAttr);
1869 break;
1872 case OSD_ONTIME_FLYTIME:
1874 if (ARMING_FLAG(ARMED)) {
1875 osdFormatFlyTime(buff, &elemAttr);
1876 } else {
1877 osdFormatOnTime(buff);
1879 break;
1882 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH:
1884 /*static int32_t updatedTimeSeconds = 0;*/
1885 static int32_t timeSeconds = -1;
1886 #if defined(USE_ADC) && defined(USE_GPS)
1887 static timeUs_t updatedTimestamp = 0;
1888 timeUs_t currentTimeUs = micros();
1889 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
1890 #ifdef USE_WIND_ESTIMATOR
1891 timeSeconds = calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation);
1892 #else
1893 timeSeconds = calculateRemainingFlightTimeBeforeRTH(false);
1894 #endif
1895 updatedTimestamp = currentTimeUs;
1897 #endif
1898 if ((!ARMING_FLAG(ARMED)) || (timeSeconds == -1)) {
1899 buff[0] = SYM_FLY_M;
1900 strcpy(buff + 1, "--:--");
1901 #if defined(USE_ADC) && defined(USE_GPS)
1902 updatedTimestamp = 0;
1903 #endif
1904 } else if (timeSeconds == -2) {
1905 // Wind is too strong to come back with cruise throttle
1906 buff[0] = SYM_FLY_M;
1907 buff[1] = buff[2] = buff[4] = buff[5] = SYM_WIND_HORIZONTAL;
1908 buff[3] = ':';
1909 buff[6] = '\0';
1910 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1911 } else {
1912 osdFormatTime(buff, timeSeconds, SYM_FLY_M, SYM_FLY_H);
1913 if (timeSeconds == 0)
1914 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1917 break;
1919 case OSD_REMAINING_DISTANCE_BEFORE_RTH:;
1920 static int32_t distanceMeters = -1;
1921 #if defined(USE_ADC) && defined(USE_GPS)
1922 static timeUs_t updatedTimestamp = 0;
1923 timeUs_t currentTimeUs = micros();
1924 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
1925 #ifdef USE_WIND_ESTIMATOR
1926 distanceMeters = calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation);
1927 #else
1928 distanceMeters = calculateRemainingDistanceBeforeRTH(false);
1929 #endif
1930 updatedTimestamp = currentTimeUs;
1932 #endif
1933 buff[0] = SYM_TRIP_DIST;
1934 if ((!ARMING_FLAG(ARMED)) || (distanceMeters == -1)) {
1935 buff[4] = SYM_BLANK;
1936 buff[5] = '\0';
1937 strcpy(buff + 1, "---");
1938 } else if (distanceMeters == -2) {
1939 // Wind is too strong to come back with cruise throttle
1940 buff[1] = buff[2] = buff[3] = SYM_WIND_HORIZONTAL;
1941 switch ((osd_unit_e)osdConfig()->units){
1942 case OSD_UNIT_UK:
1943 FALLTHROUGH;
1944 case OSD_UNIT_IMPERIAL:
1945 buff[4] = SYM_DIST_MI;
1946 break;
1947 case OSD_UNIT_METRIC_MPH:
1948 FALLTHROUGH;
1949 case OSD_UNIT_METRIC:
1950 buff[4] = SYM_DIST_KM;
1951 break;
1952 case OSD_UNIT_GA:
1953 buff[4] = SYM_DIST_NM;
1954 break;
1956 buff[5] = '\0';
1957 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1958 } else {
1959 osdFormatDistanceSymbol(buff + 1, distanceMeters * 100, 0);
1960 if (distanceMeters == 0)
1961 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1963 break;
1965 case OSD_FLYMODE:
1967 char *p = "ACRO";
1969 if (FLIGHT_MODE(FAILSAFE_MODE))
1970 p = "!FS!";
1971 else if (FLIGHT_MODE(MANUAL_MODE))
1972 p = "MANU";
1973 else if (FLIGHT_MODE(TURTLE_MODE))
1974 p = "TURT";
1975 else if (FLIGHT_MODE(NAV_RTH_MODE))
1976 p = isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
1977 else if (FLIGHT_MODE(NAV_POSHOLD_MODE))
1978 p = "HOLD";
1979 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && FLIGHT_MODE(NAV_ALTHOLD_MODE))
1980 p = "CRUZ";
1981 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE))
1982 p = "CRSH";
1983 else if (FLIGHT_MODE(NAV_WP_MODE))
1984 p = " WP ";
1985 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && navigationRequiresAngleMode()) {
1986 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
1987 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
1988 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
1989 p = " AH ";
1991 else if (FLIGHT_MODE(ANGLE_MODE))
1992 p = "ANGL";
1993 else if (FLIGHT_MODE(HORIZON_MODE))
1994 p = "HOR ";
1996 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
1997 return true;
2000 case OSD_CRAFT_NAME:
2001 osdFormatCraftName(buff);
2002 break;
2004 case OSD_THROTTLE_POS:
2006 osdFormatThrottlePosition(buff, false, &elemAttr);
2007 break;
2010 case OSD_VTX_CHANNEL:
2012 vtxDeviceOsdInfo_t osdInfo;
2013 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2015 tfp_sprintf(buff, "CH:%c%s:", osdInfo.bandLetter, osdInfo.channelName);
2016 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2018 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2019 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2020 displayWriteWithAttr(osdDisplayPort, elemPosX + 6, elemPosY, buff, elemAttr);
2021 return true;
2023 break;
2025 case OSD_VTX_POWER:
2027 vtxDeviceOsdInfo_t osdInfo;
2028 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2030 tfp_sprintf(buff, "%c", SYM_VTX_POWER);
2031 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2033 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2034 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2035 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2036 return true;
2039 #if defined(USE_SERIALRX_CRSF)
2040 case OSD_CRSF_RSSI_DBM:
2042 int16_t rssi = rxLinkStatistics.uplinkRSSI;
2043 buff[0] = (rxLinkStatistics.activeAnt == 0) ? SYM_RSSI : SYM_2RSS; // Separate symbols for each antenna
2044 if (rssi <= -100) {
2045 tfp_sprintf(buff + 1, "%4d%c", rssi, SYM_DBM);
2046 } else {
2047 tfp_sprintf(buff + 1, "%3d%c%c", rssi, SYM_DBM, ' ');
2049 if (!failsafeIsReceivingRxData()){
2050 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2051 } else if (osdConfig()->rssi_dbm_alarm && rssi < osdConfig()->rssi_dbm_alarm) {
2052 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2054 break;
2056 case OSD_CRSF_LQ:
2058 buff[0] = SYM_LQ;
2059 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
2060 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
2061 switch (osdConfig()->crsf_lq_format) {
2062 case OSD_CRSF_LQ_TYPE1:
2063 if (!failsafeIsReceivingRxData()) {
2064 tfp_sprintf(buff+1, "%3d", 0);
2065 } else {
2066 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.uplinkLQ);
2068 break;
2069 case OSD_CRSF_LQ_TYPE2:
2070 if (!failsafeIsReceivingRxData()) {
2071 tfp_sprintf(buff+1, "%s:%3d", " ", 0);
2072 } else {
2073 tfp_sprintf(buff+1, "%d:%3d", rxLinkStatistics.rfMode, rxLinkStatistics.uplinkLQ);
2075 break;
2076 case OSD_CRSF_LQ_TYPE3:
2077 if (!failsafeIsReceivingRxData()) {
2078 tfp_sprintf(buff+1, "%3d", 0);
2079 } else {
2080 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.rfMode >= 2 ? scaledLQ : rxLinkStatistics.uplinkLQ);
2082 break;
2084 if (!failsafeIsReceivingRxData()) {
2085 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2086 } else if (rxLinkStatistics.uplinkLQ < osdConfig()->link_quality_alarm) {
2087 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2089 break;
2092 case OSD_CRSF_SNR_DB:
2094 static pt1Filter_t snrFilterState;
2095 static timeMs_t snrUpdated = 0;
2096 int8_t snrFiltered = pt1FilterApply4(&snrFilterState, rxLinkStatistics.uplinkSNR, 0.5f, MS2S(millis() - snrUpdated));
2097 snrUpdated = millis();
2099 const char* showsnr = "-20";
2100 const char* hidesnr = " ";
2101 if (snrFiltered > osdConfig()->snr_alarm) {
2102 if (cmsInMenu) {
2103 buff[0] = SYM_SNR;
2104 tfp_sprintf(buff + 1, "%s%c", showsnr, SYM_DB);
2105 } else {
2106 buff[0] = SYM_BLANK;
2107 tfp_sprintf(buff + 1, "%s%c", hidesnr, SYM_BLANK);
2109 } else if (snrFiltered <= osdConfig()->snr_alarm) {
2110 buff[0] = SYM_SNR;
2111 if (snrFiltered <= -10) {
2112 tfp_sprintf(buff + 1, "%3d%c", snrFiltered, SYM_DB);
2113 } else {
2114 tfp_sprintf(buff + 1, "%2d%c%c", snrFiltered, SYM_DB, ' ');
2117 break;
2120 case OSD_CRSF_TX_POWER:
2122 if (!failsafeIsReceivingRxData())
2123 tfp_sprintf(buff, "%s%c", " ", SYM_BLANK);
2124 else
2125 tfp_sprintf(buff, "%4d%c", rxLinkStatistics.uplinkTXPower, SYM_MW);
2126 break;
2128 #endif
2130 case OSD_CROSSHAIRS: // Hud is a sub-element of the crosshair
2132 osdCrosshairPosition(&elemPosX, &elemPosY);
2133 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX, elemPosY);
2135 if (osdConfig()->hud_homing && STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
2136 osdHudDrawHoming(elemPosX, elemPosY);
2139 if (STATE(GPS_FIX) && isImuHeadingValid()) {
2141 if (osdConfig()->hud_homepoint || osdConfig()->hud_radar_disp > 0 || osdConfig()->hud_wp_disp > 0) {
2142 osdHudClear();
2145 // -------- POI : Home point
2147 if (osdConfig()->hud_homepoint) { // Display the home point (H)
2148 osdHudDrawPoi(GPS_distanceToHome, GPS_directionToHome, -osdGetAltitude() / 100, 0, SYM_HOME, 0 , 0);
2151 // -------- POI : Nearby aircrafts from ESP32 radar
2153 if (osdConfig()->hud_radar_disp > 0) { // Display the POI from the radar
2154 for (uint8_t i = 0; i < osdConfig()->hud_radar_disp; i++) {
2155 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
2156 fpVector3_t poi;
2157 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &radar_pois[i].gps, GEO_ALT_RELATIVE);
2158 radar_pois[i].distance = calculateDistanceToDestination(&poi) / 100; // In meters
2160 if (radar_pois[i].distance >= osdConfig()->hud_radar_range_min && radar_pois[i].distance <= osdConfig()->hud_radar_range_max) {
2161 radar_pois[i].direction = calculateBearingToDestination(&poi) / 100; // In °
2162 radar_pois[i].altitude = (radar_pois[i].gps.alt - osdGetAltitudeMsl()) / 100;
2163 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);
2169 // -------- POI : Next waypoints from navigation
2171 if (osdConfig()->hud_wp_disp > 0 && posControl.waypointListValid && posControl.waypointCount > 0) { // Display the next waypoints
2172 gpsLocation_t wp2;
2173 int j;
2175 for (int i = osdConfig()->hud_wp_disp - 1; i >= 0 ; i--) { // Display in reverse order so the next WP is always written on top
2176 j = posControl.activeWaypointIndex + i;
2177 if (j > posControl.waypointCount - 1) { // limit to max WP index for mission
2178 break;
2180 if (posControl.waypointList[j].lat != 0 && posControl.waypointList[j].lon != 0) {
2181 wp2.lat = posControl.waypointList[j].lat;
2182 wp2.lon = posControl.waypointList[j].lon;
2183 wp2.alt = posControl.waypointList[j].alt;
2184 fpVector3_t poi;
2185 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &wp2, waypointMissionAltConvMode(posControl.waypointList[j].p3));
2186 int32_t altConvModeAltitude = waypointMissionAltConvMode(posControl.waypointList[j].p3) == GEO_ALT_ABSOLUTE ? osdGetAltitudeMsl() : osdGetAltitude();
2187 j = getGeoWaypointNumber(j);
2188 while (j > 9) j -= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2189 osdHudDrawPoi(calculateDistanceToDestination(&poi) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi) / 100), (posControl.waypointList[j].alt - altConvModeAltitude)/ 100, 2, SYM_WAYPOINT, 48 + j, i);
2195 return true;
2196 break;
2198 case OSD_ATTITUDE_ROLL:
2199 buff[0] = SYM_ROLL_LEVEL;
2200 if (ABS(attitude.values.roll) >= 1)
2201 buff[0] += (attitude.values.roll < 0 ? -1 : 1);
2202 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.roll)), 0, 1, 0, 3);
2203 break;
2205 case OSD_ATTITUDE_PITCH:
2206 if (ABS(attitude.values.pitch) < 1)
2207 buff[0] = 'P';
2208 else if (attitude.values.pitch > 0)
2209 buff[0] = SYM_PITCH_DOWN;
2210 else if (attitude.values.pitch < 0)
2211 buff[0] = SYM_PITCH_UP;
2212 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.pitch)), 0, 1, 0, 3);
2213 break;
2215 case OSD_ARTIFICIAL_HORIZON:
2217 float rollAngle;
2218 float pitchAngle;
2220 #ifdef USE_SECONDARY_IMU
2221 if (secondaryImuState.active && secondaryImuConfig()->useForOsdAHI) {
2222 rollAngle = DECIDEGREES_TO_RADIANS(secondaryImuState.eulerAngles.values.roll);
2223 pitchAngle = DECIDEGREES_TO_RADIANS(secondaryImuState.eulerAngles.values.pitch);
2224 } else {
2225 rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2226 pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2228 #else
2229 rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2230 pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2231 #endif
2232 pitchAngle -= osdConfig()->ahi_camera_uptilt_comp ? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt) : 0;
2233 pitchAngle += DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2234 if (osdConfig()->ahi_reverse_roll) {
2235 rollAngle = -rollAngle;
2237 osdDrawArtificialHorizon(osdDisplayPort, osdGetDisplayPortCanvas(),
2238 OSD_DRAW_POINT_GRID(elemPosX, elemPosY), rollAngle, pitchAngle);
2239 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
2240 osdDrawSingleElement(OSD_CROSSHAIRS);
2242 return true;
2245 case OSD_HORIZON_SIDEBARS:
2247 osdDrawSidebars(osdDisplayPort, osdGetDisplayPortCanvas());
2248 return true;
2251 #if defined(USE_BARO) || defined(USE_GPS)
2252 case OSD_VARIO:
2254 float zvel = getEstimatedActualVelocity(Z);
2255 osdDrawVario(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), zvel);
2256 return true;
2259 case OSD_VARIO_NUM:
2261 int16_t value = getEstimatedActualVelocity(Z);
2262 char sym;
2263 switch ((osd_unit_e)osdConfig()->units) {
2264 case OSD_UNIT_UK:
2265 FALLTHROUGH;
2266 case OSD_UNIT_IMPERIAL:
2267 // Convert to centifeet/s
2268 value = CENTIMETERS_TO_CENTIFEET(value);
2269 sym = SYM_FTS;
2270 break;
2271 case OSD_UNIT_GA:
2272 // Convert to centi-100feet/min
2273 value = CENTIMETERS_TO_FEET(value * 60);
2274 sym = SYM_100FTM;
2275 break;
2276 default:
2277 case OSD_UNIT_METRIC_MPH:
2278 FALLTHROUGH;
2279 case OSD_UNIT_METRIC:
2280 // Already in cm/s
2281 sym = SYM_MS;
2282 break;
2285 osdFormatCentiNumber(buff, value, 0, 1, 0, 3);
2286 buff[3] = sym;
2287 buff[4] = '\0';
2288 break;
2290 #endif
2292 case OSD_SWITCH_INDICATOR_0:
2293 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channnel - 1), buff);
2294 break;
2296 case OSD_SWITCH_INDICATOR_1:
2297 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channnel - 1), buff);
2298 break;
2300 case OSD_SWITCH_INDICATOR_2:
2301 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channnel - 1), buff);
2302 break;
2304 case OSD_SWITCH_INDICATOR_3:
2305 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channnel - 1), buff);
2306 break;
2308 case OSD_ACTIVE_PROFILE:
2309 tfp_sprintf(buff, "%c%u", SYM_PROFILE, (getConfigProfile() + 1));
2310 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2311 break;
2313 case OSD_ROLL_PIDS:
2314 osdDisplayFlightPIDValues(elemPosX, elemPosY, "ROL", PID_ROLL, ADJUSTMENT_ROLL_P, ADJUSTMENT_ROLL_I, ADJUSTMENT_ROLL_D, ADJUSTMENT_ROLL_FF);
2315 return true;
2317 case OSD_PITCH_PIDS:
2318 osdDisplayFlightPIDValues(elemPosX, elemPosY, "PIT", PID_PITCH, ADJUSTMENT_PITCH_P, ADJUSTMENT_PITCH_I, ADJUSTMENT_PITCH_D, ADJUSTMENT_PITCH_FF);
2319 return true;
2321 case OSD_YAW_PIDS:
2322 osdDisplayFlightPIDValues(elemPosX, elemPosY, "YAW", PID_YAW, ADJUSTMENT_YAW_P, ADJUSTMENT_YAW_I, ADJUSTMENT_YAW_D, ADJUSTMENT_YAW_FF);
2323 return true;
2325 case OSD_LEVEL_PIDS:
2326 osdDisplayNavPIDValues(elemPosX, elemPosY, "LEV", PID_LEVEL, ADJUSTMENT_LEVEL_P, ADJUSTMENT_LEVEL_I, ADJUSTMENT_LEVEL_D);
2327 return true;
2329 case OSD_POS_XY_PIDS:
2330 osdDisplayNavPIDValues(elemPosX, elemPosY, "PXY", PID_POS_XY, ADJUSTMENT_POS_XY_P, ADJUSTMENT_POS_XY_I, ADJUSTMENT_POS_XY_D);
2331 return true;
2333 case OSD_POS_Z_PIDS:
2334 osdDisplayNavPIDValues(elemPosX, elemPosY, "PZ", PID_POS_Z, ADJUSTMENT_POS_Z_P, ADJUSTMENT_POS_Z_I, ADJUSTMENT_POS_Z_D);
2335 return true;
2337 case OSD_VEL_XY_PIDS:
2338 osdDisplayNavPIDValues(elemPosX, elemPosY, "VXY", PID_VEL_XY, ADJUSTMENT_VEL_XY_P, ADJUSTMENT_VEL_XY_I, ADJUSTMENT_VEL_XY_D);
2339 return true;
2341 case OSD_VEL_Z_PIDS:
2342 osdDisplayNavPIDValues(elemPosX, elemPosY, "VZ", PID_VEL_Z, ADJUSTMENT_VEL_Z_P, ADJUSTMENT_VEL_Z_I, ADJUSTMENT_VEL_Z_D);
2343 return true;
2345 case OSD_HEADING_P:
2346 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "HP", 0, pidBank()->pid[PID_HEADING].P, 3, 0, ADJUSTMENT_HEADING_P);
2347 return true;
2349 case OSD_BOARD_ALIGN_ROLL:
2350 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT);
2351 return true;
2353 case OSD_BOARD_ALIGN_PITCH:
2354 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT);
2355 return true;
2357 case OSD_RC_EXPO:
2358 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "EXP", 0, currentControlRateProfile->stabilized.rcExpo8, 3, 0, ADJUSTMENT_RC_EXPO);
2359 return true;
2361 case OSD_RC_YAW_EXPO:
2362 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "YEX", 0, currentControlRateProfile->stabilized.rcYawExpo8, 3, 0, ADJUSTMENT_RC_YAW_EXPO);
2363 return true;
2365 case OSD_THROTTLE_EXPO:
2366 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TEX", 0, currentControlRateProfile->throttle.rcExpo8, 3, 0, ADJUSTMENT_THROTTLE_EXPO);
2367 return true;
2369 case OSD_PITCH_RATE:
2370 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SPR");
2372 elemAttr = TEXT_ATTRIBUTES_NONE;
2373 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_PITCH]);
2374 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2375 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2376 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2377 return true;
2379 case OSD_ROLL_RATE:
2380 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SRR");
2382 elemAttr = TEXT_ATTRIBUTES_NONE;
2383 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_ROLL]);
2384 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2385 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2386 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2387 return true;
2389 case OSD_YAW_RATE:
2390 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "SYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2391 return true;
2393 case OSD_MANUAL_RC_EXPO:
2394 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MEX", 0, currentControlRateProfile->manual.rcExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO);
2395 return true;
2397 case OSD_MANUAL_RC_YAW_EXPO:
2398 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYX", 0, currentControlRateProfile->manual.rcYawExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO);
2399 return true;
2401 case OSD_MANUAL_PITCH_RATE:
2402 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MPR");
2404 elemAttr = TEXT_ATTRIBUTES_NONE;
2405 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_PITCH]);
2406 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2407 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2408 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2409 return true;
2411 case OSD_MANUAL_ROLL_RATE:
2412 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MRR");
2414 elemAttr = TEXT_ATTRIBUTES_NONE;
2415 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_ROLL]);
2416 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2417 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2418 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2419 return true;
2421 case OSD_MANUAL_YAW_RATE:
2422 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2423 return true;
2425 case OSD_NAV_FW_CRUISE_THR:
2426 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CRZ", 0, currentBatteryProfile->nav.fw.cruise_throttle, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR);
2427 return true;
2429 case OSD_NAV_FW_PITCH2THR:
2430 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "P2T", 0, currentBatteryProfile->nav.fw.pitch_to_throttle, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR);
2431 return true;
2433 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE:
2434 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "0TP", 0, (float)currentBatteryProfile->fwMinThrottleDownPitchAngle / 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE);
2435 return true;
2437 case OSD_FW_ALT_PID_OUTPUTS:
2439 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2440 osdFormatPidControllerOutput(buff, "PZO", &nav_pids->fw_alt, 10, true); // display requested pitch degrees
2441 break;
2444 case OSD_FW_POS_PID_OUTPUTS:
2446 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers(); // display requested roll degrees
2447 osdFormatPidControllerOutput(buff, "PXYO", &nav_pids->fw_nav, 1, true);
2448 break;
2451 case OSD_MC_VEL_Z_PID_OUTPUTS:
2453 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2454 osdFormatPidControllerOutput(buff, "VZO", &nav_pids->vel[Z], 100, false); // display throttle adjustment µs
2455 break;
2458 case OSD_MC_VEL_X_PID_OUTPUTS:
2460 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2461 osdFormatPidControllerOutput(buff, "VXO", &nav_pids->vel[X], 100, false); // display requested acceleration cm/s^2
2462 break;
2465 case OSD_MC_VEL_Y_PID_OUTPUTS:
2467 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2468 osdFormatPidControllerOutput(buff, "VYO", &nav_pids->vel[Y], 100, false); // display requested acceleration cm/s^2
2469 break;
2472 case OSD_MC_POS_XYZ_P_OUTPUTS:
2474 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2475 strcpy(buff, "POSO ");
2476 // display requested velocity cm/s
2477 tfp_sprintf(buff + 5, "%4d", (int)lrintf(nav_pids->pos[X].output_constrained * 100));
2478 buff[9] = ' ';
2479 tfp_sprintf(buff + 10, "%4d", (int)lrintf(nav_pids->pos[Y].output_constrained * 100));
2480 buff[14] = ' ';
2481 tfp_sprintf(buff + 15, "%4d", (int)lrintf(nav_pids->pos[Z].output_constrained * 100));
2482 buff[19] = '\0';
2483 break;
2486 case OSD_POWER:
2488 bool kiloWatt = osdFormatCentiNumber(buff, getPower(), 1000, 2, 2, 3);
2489 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
2490 buff[4] = '\0';
2492 uint8_t current_alarm = osdConfig()->current_alarm;
2493 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
2494 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2496 break;
2499 case OSD_AIR_SPEED:
2501 #ifdef USE_PITOT
2502 buff[0] = SYM_AIR;
2503 osdFormatVelocityStr(buff + 1, pitot.airSpeed, false, false);
2505 if ((osdConfig()->airspeed_alarm_min != 0 && pitot.airSpeed < osdConfig()->airspeed_alarm_min) ||
2506 (osdConfig()->airspeed_alarm_max != 0 && pitot.airSpeed > osdConfig()->airspeed_alarm_max)) {
2507 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2509 #else
2510 return false;
2511 #endif
2512 break;
2515 case OSD_AIR_MAX_SPEED:
2517 #ifdef USE_PITOT
2518 buff[0] = SYM_MAX;
2519 buff[1] = SYM_AIR;
2520 osdFormatVelocityStr(buff + 2, stats.max_air_speed, false, false);
2521 #else
2522 return false;
2523 #endif
2524 break;
2527 case OSD_RTC_TIME:
2529 // RTC not configured will show 00:00
2530 dateTime_t dateTime;
2531 rtcGetDateTimeLocal(&dateTime);
2532 buff[0] = SYM_CLOCK;
2533 tfp_sprintf(buff + 1, "%02u:%02u:%02u", dateTime.hours, dateTime.minutes, dateTime.seconds);
2534 break;
2537 case OSD_MESSAGES:
2539 elemAttr = osdGetSystemMessage(buff, OSD_MESSAGE_LENGTH, true);
2540 break;
2543 case OSD_VERSION:
2545 tfp_sprintf(buff, "INAV %s", FC_VERSION_STRING);
2546 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2547 break;
2550 case OSD_MAIN_BATT_CELL_VOLTAGE:
2552 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawAverageCellVoltage(), 3, 2);
2553 return true;
2556 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE:
2558 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedAverageCellVoltage(), 3, 2);
2559 return true;
2562 case OSD_THROTTLE_POS_AUTO_THR:
2564 osdFormatThrottlePosition(buff, true, &elemAttr);
2565 break;
2568 case OSD_HEADING_GRAPH:
2570 if (osdIsHeadingValid()) {
2571 osdDrawHeadingGraph(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), osdGetHeading());
2572 return true;
2573 } else {
2574 buff[0] = buff[2] = buff[4] = buff[6] = buff[8] = SYM_HEADING_LINE;
2575 buff[1] = buff[3] = buff[5] = buff[7] = SYM_HEADING_DIVIDED_LINE;
2576 buff[OSD_HEADING_GRAPH_WIDTH] = '\0';
2578 break;
2581 case OSD_EFFICIENCY_MAH_PER_KM:
2583 // amperage is in centi amps, speed is in cms/s. We want
2584 // mah/km. Only show when ground speed > 1m/s.
2585 static pt1Filter_t eFilterState;
2586 static timeUs_t efficiencyUpdated = 0;
2587 int32_t value = 0;
2588 bool moreThanAh = false;
2589 timeUs_t currentTimeUs = micros();
2590 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2591 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2592 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2593 value = pt1FilterApply4(&eFilterState, ((float)getAmperage() / gpsSol.groundSpeed) / 0.0036f,
2594 1, US2S(efficiencyTimeDelta));
2596 efficiencyUpdated = currentTimeUs;
2597 } else {
2598 value = eFilterState.state;
2601 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2602 switch (osdConfig()->units) {
2603 case OSD_UNIT_UK:
2604 FALLTHROUGH;
2605 case OSD_UNIT_IMPERIAL:
2606 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10, 1000, 0, 2, 3);
2607 if (!moreThanAh) {
2608 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
2609 } else {
2610 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
2612 if (!efficiencyValid) {
2613 buff[0] = buff[1] = buff[2] = '-';
2614 buff[3] = SYM_MAH_MI_0;
2615 buff[4] = SYM_MAH_MI_1;
2616 buff[5] = '\0';
2618 break;
2619 case OSD_UNIT_GA:
2620 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10, 1000, 0, 2, 3);
2621 if (!moreThanAh) {
2622 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
2623 } else {
2624 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
2626 if (!efficiencyValid) {
2627 buff[0] = buff[1] = buff[2] = '-';
2628 buff[3] = SYM_MAH_NM_0;
2629 buff[4] = SYM_MAH_NM_1;
2630 buff[5] = '\0';
2632 break;
2633 case OSD_UNIT_METRIC_MPH:
2634 FALLTHROUGH;
2635 case OSD_UNIT_METRIC:
2636 moreThanAh = osdFormatCentiNumber(buff, value * 100, 1000, 0, 2, 3);
2637 if (!moreThanAh) {
2638 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
2639 } else {
2640 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
2642 if (!efficiencyValid) {
2643 buff[0] = buff[1] = buff[2] = '-';
2644 buff[3] = SYM_MAH_KM_0;
2645 buff[4] = SYM_MAH_KM_1;
2646 buff[5] = '\0';
2648 break;
2650 break;
2653 case OSD_EFFICIENCY_WH_PER_KM:
2655 // amperage is in centi amps, speed is in cms/s. We want
2656 // mWh/km. Only show when ground speed > 1m/s.
2657 static pt1Filter_t eFilterState;
2658 static timeUs_t efficiencyUpdated = 0;
2659 int32_t value = 0;
2660 timeUs_t currentTimeUs = micros();
2661 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2662 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2663 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2664 value = pt1FilterApply4(&eFilterState, ((float)getPower() / gpsSol.groundSpeed) / 0.0036f,
2665 1, US2S(efficiencyTimeDelta));
2667 efficiencyUpdated = currentTimeUs;
2668 } else {
2669 value = eFilterState.state;
2672 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2673 switch (osdConfig()->units) {
2674 case OSD_UNIT_UK:
2675 FALLTHROUGH;
2676 case OSD_UNIT_IMPERIAL:
2677 osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10000, 0, 2, 0, 3);
2678 buff[3] = SYM_WH_MI;
2679 break;
2680 case OSD_UNIT_GA:
2681 osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10000, 0, 2, 0, 3);
2682 buff[3] = SYM_WH_NM;
2683 break;
2684 case OSD_UNIT_METRIC_MPH:
2685 FALLTHROUGH;
2686 case OSD_UNIT_METRIC:
2687 osdFormatCentiNumber(buff, value / 10, 0, 2, 0, 3);
2688 buff[3] = SYM_WH_KM;
2689 break;
2691 buff[4] = '\0';
2692 if (!efficiencyValid) {
2693 buff[0] = buff[1] = buff[2] = '-';
2695 break;
2698 case OSD_GFORCE:
2700 buff[0] = SYM_GFORCE;
2701 osdFormatCentiNumber(buff + 1, GForce, 0, 2, 0, 3);
2702 if (GForce > osdConfig()->gforce_alarm * 100) {
2703 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2705 break;
2708 case OSD_GFORCE_X:
2709 case OSD_GFORCE_Y:
2710 case OSD_GFORCE_Z:
2712 float GForceValue = GForceAxis[item - OSD_GFORCE_X];
2713 buff[0] = SYM_GFORCE_X + item - OSD_GFORCE_X;
2714 osdFormatCentiNumber(buff + 1, GForceValue, 0, 2, 0, 4);
2715 if ((GForceValue < osdConfig()->gforce_axis_alarm_min * 100) || (GForceValue > osdConfig()->gforce_axis_alarm_max * 100)) {
2716 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2718 break;
2720 case OSD_DEBUG:
2723 * Longest representable string is -2147483648 does not fit in the screen.
2724 * Only 7 digits for negative and 8 digits for positive values allowed
2726 for (uint8_t bufferIndex = 0; bufferIndex < DEBUG32_VALUE_COUNT; ++elemPosY, bufferIndex += 2) {
2727 tfp_sprintf(
2728 buff,
2729 "[%u]=%8ld [%u]=%8ld",
2730 bufferIndex,
2731 constrain(debug[bufferIndex], -9999999, 99999999),
2732 bufferIndex+1,
2733 constrain(debug[bufferIndex+1], -9999999, 99999999)
2735 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2737 break;
2740 case OSD_IMU_TEMPERATURE:
2742 int16_t temperature;
2743 const bool valid = getIMUTemperature(&temperature);
2744 osdDisplayTemperature(elemPosX, elemPosY, SYM_IMU_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2745 return true;
2748 case OSD_BARO_TEMPERATURE:
2750 int16_t temperature;
2751 const bool valid = getBaroTemperature(&temperature);
2752 osdDisplayTemperature(elemPosX, elemPosY, SYM_BARO_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2753 return true;
2756 #ifdef USE_TEMPERATURE_SENSOR
2757 case OSD_TEMP_SENSOR_0_TEMPERATURE:
2758 case OSD_TEMP_SENSOR_1_TEMPERATURE:
2759 case OSD_TEMP_SENSOR_2_TEMPERATURE:
2760 case OSD_TEMP_SENSOR_3_TEMPERATURE:
2761 case OSD_TEMP_SENSOR_4_TEMPERATURE:
2762 case OSD_TEMP_SENSOR_5_TEMPERATURE:
2763 case OSD_TEMP_SENSOR_6_TEMPERATURE:
2764 case OSD_TEMP_SENSOR_7_TEMPERATURE:
2766 osdDisplayTemperatureSensor(elemPosX, elemPosY, item - OSD_TEMP_SENSOR_0_TEMPERATURE);
2767 return true;
2769 #endif /* ifdef USE_TEMPERATURE_SENSOR */
2771 case OSD_WIND_SPEED_HORIZONTAL:
2772 #ifdef USE_WIND_ESTIMATOR
2774 bool valid = isEstimatedWindSpeedValid();
2775 float horizontalWindSpeed;
2776 if (valid) {
2777 uint16_t angle;
2778 horizontalWindSpeed = getEstimatedHorizontalWindSpeed(&angle);
2779 int16_t windDirection = osdGetHeadingAngle((int)angle - DECIDEGREES_TO_DEGREES(attitude.values.yaw));
2780 buff[1] = SYM_DIRECTION + (windDirection * 2 / 90);
2781 } else {
2782 horizontalWindSpeed = 0;
2783 buff[1] = SYM_BLANK;
2785 buff[0] = SYM_WIND_HORIZONTAL;
2786 osdFormatWindSpeedStr(buff + 2, horizontalWindSpeed, valid);
2787 break;
2789 #else
2790 return false;
2791 #endif
2793 case OSD_WIND_SPEED_VERTICAL:
2794 #ifdef USE_WIND_ESTIMATOR
2796 buff[0] = SYM_WIND_VERTICAL;
2797 buff[1] = SYM_BLANK;
2798 bool valid = isEstimatedWindSpeedValid();
2799 float verticalWindSpeed;
2800 if (valid) {
2801 verticalWindSpeed = getEstimatedWindSpeed(Z);
2802 if (verticalWindSpeed < 0) {
2803 buff[1] = SYM_AH_DECORATION_DOWN;
2804 verticalWindSpeed = -verticalWindSpeed;
2805 } else if (verticalWindSpeed > 0) {
2806 buff[1] = SYM_AH_DECORATION_UP;
2808 } else {
2809 verticalWindSpeed = 0;
2811 osdFormatWindSpeedStr(buff + 2, verticalWindSpeed, valid);
2812 break;
2814 #else
2815 return false;
2816 #endif
2818 case OSD_PLUS_CODE:
2820 STATIC_ASSERT(GPS_DEGREES_DIVIDER == OLC_DEG_MULTIPLIER, invalid_olc_deg_multiplier);
2821 int digits = osdConfig()->plus_code_digits;
2822 int digitsRemoved = osdConfig()->plus_code_short * 2;
2823 if (STATE(GPS_FIX)) {
2824 olc_encode(gpsSol.llh.lat, gpsSol.llh.lon, digits, buff, sizeof(buff));
2825 } else {
2826 // +codes with > 8 digits have a + at the 9th digit
2827 // and we only support 10 and up.
2828 memset(buff, '-', digits + 1);
2829 buff[8] = '+';
2830 buff[digits + 1] = '\0';
2832 // Optionally trim digits from the left
2833 memmove(buff, buff+digitsRemoved, strlen(buff) + digitsRemoved);
2834 buff[digits + 1 - digitsRemoved] = '\0';
2835 break;
2838 case OSD_AZIMUTH:
2841 buff[0] = SYM_AZIMUTH;
2842 if (osdIsHeadingValid()) {
2843 int16_t h = GPS_directionToHome;
2844 if (h < 0) {
2845 h += 360;
2847 if(h >= 180)
2848 h = h - 180;
2849 else
2850 h = h + 180;
2852 tfp_sprintf(&buff[1], "%3d", h);
2853 } else {
2854 buff[1] = buff[2] = buff[3] = '-';
2856 buff[4] = SYM_DEGREES;
2857 buff[5] = '\0';
2858 break;
2861 case OSD_MAP_SCALE:
2863 float scaleToUnit;
2864 int scaleUnitDivisor;
2865 char symUnscaled;
2866 char symScaled;
2867 int maxDecimals;
2869 switch (osdConfig()->units) {
2870 case OSD_UNIT_UK:
2871 FALLTHROUGH;
2872 case OSD_UNIT_IMPERIAL:
2873 scaleToUnit = 100 / 1609.3440f; // scale to 0.01mi for osdFormatCentiNumber()
2874 scaleUnitDivisor = 0;
2875 symUnscaled = SYM_MI;
2876 symScaled = SYM_MI;
2877 maxDecimals = 2;
2878 break;
2879 case OSD_UNIT_GA:
2880 scaleToUnit = 100 / 1852.0010f; // scale to 0.01mi for osdFormatCentiNumber()
2881 scaleUnitDivisor = 0;
2882 symUnscaled = SYM_NM;
2883 symScaled = SYM_NM;
2884 maxDecimals = 2;
2885 break;
2886 default:
2887 case OSD_UNIT_METRIC_MPH:
2888 FALLTHROUGH;
2889 case OSD_UNIT_METRIC:
2890 scaleToUnit = 100; // scale to cm for osdFormatCentiNumber()
2891 scaleUnitDivisor = 1000; // Convert to km when scale gets bigger than 999m
2892 symUnscaled = SYM_M;
2893 symScaled = SYM_KM;
2894 maxDecimals = 0;
2895 break;
2897 buff[0] = SYM_SCALE;
2898 if (osdMapData.scale > 0) {
2899 bool scaled = osdFormatCentiNumber(&buff[1], osdMapData.scale * scaleToUnit, scaleUnitDivisor, maxDecimals, 2, 3);
2900 buff[4] = scaled ? symScaled : symUnscaled;
2901 // Make sure this is cleared if the map stops being drawn
2902 osdMapData.scale = 0;
2903 } else {
2904 memset(&buff[1], '-', 4);
2906 buff[5] = '\0';
2907 break;
2909 case OSD_MAP_REFERENCE:
2911 char referenceSymbol;
2912 if (osdMapData.referenceSymbol) {
2913 referenceSymbol = osdMapData.referenceSymbol;
2914 // Make sure this is cleared if the map stops being drawn
2915 osdMapData.referenceSymbol = 0;
2916 } else {
2917 referenceSymbol = '-';
2919 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DIRECTION);
2920 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 1, referenceSymbol);
2921 return true;
2924 case OSD_GVAR_0:
2926 osdFormatGVar(buff, 0);
2927 break;
2929 case OSD_GVAR_1:
2931 osdFormatGVar(buff, 1);
2932 break;
2934 case OSD_GVAR_2:
2936 osdFormatGVar(buff, 2);
2937 break;
2939 case OSD_GVAR_3:
2941 osdFormatGVar(buff, 3);
2942 break;
2945 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
2946 case OSD_RC_SOURCE:
2948 const char *source_text = IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
2949 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2950 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, source_text, elemAttr);
2951 return true;
2953 #endif
2955 #if defined(USE_ESC_SENSOR)
2956 case OSD_ESC_RPM:
2958 escSensorData_t * escSensor = escSensorGetData();
2959 if (escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE) {
2960 osdFormatRpm(buff, escSensor->rpm);
2962 else {
2963 osdFormatRpm(buff, 0);
2965 break;
2967 case OSD_ESC_TEMPERATURE:
2969 escSensorData_t * escSensor = escSensorGetData();
2970 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
2971 osdDisplayTemperature(elemPosX, elemPosY, SYM_ESC_TEMP, NULL, escTemperatureValid, (escSensor->temperature)*10, osdConfig()->esc_temp_alarm_min, osdConfig()->esc_temp_alarm_max);
2972 return true;
2974 #endif
2975 case OSD_TPA:
2977 char buff[4];
2978 textAttributes_t attr;
2980 displayWrite(osdDisplayPort, elemPosX, elemPosY, "TPA");
2981 attr = TEXT_ATTRIBUTES_NONE;
2982 tfp_sprintf(buff, "%3d", currentControlRateProfile->throttle.dynPID);
2983 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA)) {
2984 TEXT_ATTRIBUTES_ADD_BLINK(attr);
2986 displayWriteWithAttr(osdDisplayPort, elemPosX + 5, elemPosY, buff, attr);
2988 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, "BP");
2989 attr = TEXT_ATTRIBUTES_NONE;
2990 tfp_sprintf(buff, "%4d", currentControlRateProfile->throttle.pa_breakpoint);
2991 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT)) {
2992 TEXT_ATTRIBUTES_ADD_BLINK(attr);
2994 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY + 1, buff, attr);
2996 return true;
2999 case OSD_NAV_FW_CONTROL_SMOOTHNESS:
3000 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CTL S", 0, navConfig()->fw.control_smoothness, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS);
3001 return true;
3003 case OSD_MISSION:
3005 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION)) {
3006 char buf[5];
3007 switch (posControl.wpMissionPlannerStatus) {
3008 case WP_PLAN_WAIT:
3009 strcpy(buf, "WAIT");
3010 break;
3011 case WP_PLAN_SAVE:
3012 strcpy(buf, "SAVE");
3013 break;
3014 case WP_PLAN_OK:
3015 strcpy(buf, " OK ");
3016 break;
3017 case WP_PLAN_FULL:
3018 strcpy(buf, "FULL");
3020 tfp_sprintf(buff, "%s>%2uWP", buf, posControl.wpPlannerActiveWPIndex);
3021 } else if (posControl.wpPlannerActiveWPIndex){
3022 tfp_sprintf(buff, "PLAN>%2uWP", posControl.waypointCount); // mission planner mision active
3024 #ifdef USE_MULTI_MISSION
3025 else {
3026 if (ARMING_FLAG(ARMED)){
3027 // Limit field size when Armed, only show selected mission
3028 tfp_sprintf(buff, "M%u ", posControl.loadedMultiMissionIndex);
3029 } else if (posControl.multiMissionCount && navConfig()->general.waypoint_multi_mission_index){
3030 if (navConfig()->general.waypoint_multi_mission_index != posControl.loadedMultiMissionIndex) {
3031 tfp_sprintf(buff, "M%u/%u>LOAD", navConfig()->general.waypoint_multi_mission_index, posControl.multiMissionCount);
3032 } else {
3033 // wpCount source for selected mission changes after Arming (until next mission load)
3034 int8_t wpCount = posControl.loadedMultiMissionWPCount ? posControl.loadedMultiMissionWPCount : posControl.waypointCount;
3035 if (posControl.waypointListValid && wpCount > 0) {
3036 tfp_sprintf(buff, "M%u/%u>%2uWP", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, wpCount);
3037 } else {
3038 tfp_sprintf(buff, "M0/%u> 0WP", posControl.multiMissionCount);
3041 } else { // multi_mission_index 0 - show active WP count
3042 tfp_sprintf(buff, "WP CNT>%2u", posControl.waypointCount);
3045 #endif
3046 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3047 return true;
3050 #ifdef USE_POWER_LIMITS
3051 case OSD_PLIMIT_REMAINING_BURST_TIME:
3052 osdFormatCentiNumber(buff, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3);
3053 buff[3] = 'S';
3054 buff[4] = '\0';
3055 break;
3057 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT:
3058 if (currentBatteryProfile->powerLimits.continuousCurrent) {
3059 osdFormatCentiNumber(buff, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3);
3060 buff[3] = SYM_AMP;
3061 buff[4] = '\0';
3063 if (powerLimiterIsLimitingCurrent()) {
3064 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3067 break;
3069 #ifdef USE_ADC
3070 case OSD_PLIMIT_ACTIVE_POWER_LIMIT:
3072 if (currentBatteryProfile->powerLimits.continuousPower) {
3073 bool kiloWatt = osdFormatCentiNumber(buff, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3);
3074 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3075 buff[4] = '\0';
3077 if (powerLimiterIsLimitingPower()) {
3078 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3081 break;
3083 #endif // USE_ADC
3084 #endif // USE_POWER_LIMITS
3086 default:
3087 return false;
3090 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
3091 return true;
3094 static uint8_t osdIncElementIndex(uint8_t elementIndex)
3096 ++elementIndex;
3098 if (elementIndex == OSD_ARTIFICIAL_HORIZON)
3099 ++elementIndex;
3101 #ifndef USE_TEMPERATURE_SENSOR
3102 if (elementIndex == OSD_TEMP_SENSOR_0_TEMPERATURE)
3103 elementIndex = OSD_ALTITUDE_MSL;
3104 #endif
3106 if (!sensors(SENSOR_ACC)) {
3107 if (elementIndex == OSD_CROSSHAIRS) {
3108 elementIndex = OSD_ONTIME;
3112 if (!feature(FEATURE_VBAT)) {
3113 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3114 elementIndex = OSD_LEVEL_PIDS;
3118 if (!feature(FEATURE_CURRENT_METER)) {
3119 if (elementIndex == OSD_CURRENT_DRAW) {
3120 elementIndex = OSD_GPS_SPEED;
3122 if (elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3123 elementIndex = OSD_TRIP_DIST;
3125 if (elementIndex == OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH) {
3126 elementIndex = OSD_HOME_HEADING_ERROR;
3128 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3129 elementIndex = OSD_LEVEL_PIDS;
3133 if (!feature(FEATURE_GPS)) {
3134 if (elementIndex == OSD_GPS_SPEED) {
3135 elementIndex = OSD_ALTITUDE;
3137 if (elementIndex == OSD_GPS_LON) {
3138 elementIndex = OSD_VARIO;
3140 if (elementIndex == OSD_GPS_HDOP) {
3141 elementIndex = OSD_MAIN_BATT_CELL_VOLTAGE;
3143 if (elementIndex == OSD_TRIP_DIST) {
3144 elementIndex = OSD_ATTITUDE_PITCH;
3146 if (elementIndex == OSD_WIND_SPEED_HORIZONTAL) {
3147 elementIndex = OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE;
3149 if (elementIndex == OSD_3D_SPEED) {
3150 elementIndex++;
3154 if (!STATE(ESC_SENSOR_ENABLED)) {
3155 if (elementIndex == OSD_ESC_RPM) {
3156 elementIndex++;
3160 #ifndef USE_POWER_LIMITS
3161 if (elementIndex == OSD_NAV_FW_CONTROL_SMOOTHNESS) {
3162 elementIndex = OSD_ITEM_COUNT;
3164 #endif
3166 if (elementIndex == OSD_ITEM_COUNT) {
3167 elementIndex = 0;
3169 return elementIndex;
3172 void osdDrawNextElement(void)
3174 static uint8_t elementIndex = 0;
3175 // Prevent infinite loop when no elements are enabled
3176 uint8_t index = elementIndex;
3177 do {
3178 elementIndex = osdIncElementIndex(elementIndex);
3179 } while(!osdDrawSingleElement(elementIndex) && index != elementIndex);
3181 // Draw artificial horizon + tracking telemtry last
3182 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
3183 if (osdConfig()->telemetry>0){
3184 osdDisplayTelemetry();
3188 PG_RESET_TEMPLATE(osdConfig_t, osdConfig,
3189 .rssi_alarm = SETTING_OSD_RSSI_ALARM_DEFAULT,
3190 .time_alarm = SETTING_OSD_TIME_ALARM_DEFAULT,
3191 .alt_alarm = SETTING_OSD_ALT_ALARM_DEFAULT,
3192 .dist_alarm = SETTING_OSD_DIST_ALARM_DEFAULT,
3193 .neg_alt_alarm = SETTING_OSD_NEG_ALT_ALARM_DEFAULT,
3194 .current_alarm = SETTING_OSD_CURRENT_ALARM_DEFAULT,
3195 .imu_temp_alarm_min = SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT,
3196 .imu_temp_alarm_max = SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT,
3197 .esc_temp_alarm_min = SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT,
3198 .esc_temp_alarm_max = SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT,
3199 .gforce_alarm = SETTING_OSD_GFORCE_ALARM_DEFAULT,
3200 .gforce_axis_alarm_min = SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT,
3201 .gforce_axis_alarm_max = SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT,
3202 #ifdef USE_BARO
3203 .baro_temp_alarm_min = SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT,
3204 .baro_temp_alarm_max = SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT,
3205 #endif
3206 #ifdef USE_SERIALRX_CRSF
3207 .snr_alarm = SETTING_OSD_SNR_ALARM_DEFAULT,
3208 .crsf_lq_format = SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT,
3209 .link_quality_alarm = SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT,
3210 .rssi_dbm_alarm = SETTING_OSD_RSSI_DBM_ALARM_DEFAULT,
3211 .rssi_dbm_max = SETTING_OSD_RSSI_DBM_MAX_DEFAULT,
3212 .rssi_dbm_min = SETTING_OSD_RSSI_DBM_MIN_DEFAULT,
3213 #endif
3214 #ifdef USE_TEMPERATURE_SENSOR
3215 .temp_label_align = SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT,
3216 #endif
3217 #ifdef USE_PITOT
3218 .airspeed_alarm_min = SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT,
3219 .airspeed_alarm_max = SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT,
3220 #endif
3222 .video_system = SETTING_OSD_VIDEO_SYSTEM_DEFAULT,
3223 .row_shiftdown = SETTING_OSD_ROW_SHIFTDOWN_DEFAULT,
3225 .ahi_reverse_roll = SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT,
3226 .ahi_max_pitch = SETTING_OSD_AHI_MAX_PITCH_DEFAULT,
3227 .crosshairs_style = SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT,
3228 .horizon_offset = SETTING_OSD_HORIZON_OFFSET_DEFAULT,
3229 .camera_uptilt = SETTING_OSD_CAMERA_UPTILT_DEFAULT,
3230 .ahi_camera_uptilt_comp = SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT,
3231 .camera_fov_h = SETTING_OSD_CAMERA_FOV_H_DEFAULT,
3232 .camera_fov_v = SETTING_OSD_CAMERA_FOV_V_DEFAULT,
3233 .hud_margin_h = SETTING_OSD_HUD_MARGIN_H_DEFAULT,
3234 .hud_margin_v = SETTING_OSD_HUD_MARGIN_V_DEFAULT,
3235 .hud_homing = SETTING_OSD_HUD_HOMING_DEFAULT,
3236 .hud_homepoint = SETTING_OSD_HUD_HOMEPOINT_DEFAULT,
3237 .hud_radar_disp = SETTING_OSD_HUD_RADAR_DISP_DEFAULT,
3238 .hud_radar_range_min = SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT,
3239 .hud_radar_range_max = SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT,
3240 .hud_wp_disp = SETTING_OSD_HUD_WP_DISP_DEFAULT,
3241 .left_sidebar_scroll = SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT,
3242 .right_sidebar_scroll = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT,
3243 .sidebar_scroll_arrows = SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT,
3244 .sidebar_horizontal_offset = SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT,
3245 .left_sidebar_scroll_step = SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT,
3246 .right_sidebar_scroll_step = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT,
3247 .sidebar_height = SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT,
3248 .osd_home_position_arm_screen = SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT,
3249 .pan_servo_index = SETTING_OSD_PAN_SERVO_INDEX_DEFAULT,
3250 .pan_servo_pwm2centideg = SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT,
3251 .esc_rpm_precision = SETTING_OSD_ESC_RPM_PRECISION_DEFAULT,
3252 .osd_switch_indicator0_name = SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT,
3253 .osd_switch_indicator0_channnel = SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNNEL_DEFAULT,
3254 .osd_switch_indicator1_name = SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT,
3255 .osd_switch_indicator1_channnel = SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNNEL_DEFAULT,
3256 .osd_switch_indicator2_name = SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT,
3257 .osd_switch_indicator2_channnel = SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNNEL_DEFAULT,
3258 .osd_switch_indicator3_name = SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT,
3259 .osd_switch_indicator3_channnel = SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNNEL_DEFAULT,
3261 .units = SETTING_OSD_UNITS_DEFAULT,
3262 .main_voltage_decimals = SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT,
3264 #ifdef USE_WIND_ESTIMATOR
3265 .estimations_wind_compensation = SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT,
3266 #endif
3268 .coordinate_digits = SETTING_OSD_COORDINATE_DIGITS_DEFAULT,
3270 .osd_failsafe_switch_layout = SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT,
3272 .plus_code_digits = SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT,
3273 .plus_code_short = SETTING_OSD_PLUS_CODE_SHORT_DEFAULT,
3275 .ahi_width = SETTING_OSD_AHI_WIDTH_DEFAULT,
3276 .ahi_height = SETTING_OSD_AHI_HEIGHT_DEFAULT,
3277 .ahi_vertical_offset = SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT,
3278 .ahi_bordered = SETTING_OSD_AHI_BORDERED_DEFAULT,
3279 .ahi_style = SETTING_OSD_AHI_STYLE_DEFAULT,
3281 .force_grid = SETTING_OSD_FORCE_GRID_DEFAULT,
3283 .stats_energy_unit = SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT,
3284 .stats_min_voltage_unit = SETTING_OSD_STATS_MIN_VOLTAGE_UNIT_DEFAULT,
3285 .stats_page_auto_swap_time = SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT
3288 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t *osdLayoutsConfig)
3290 osdLayoutsConfig->item_pos[0][OSD_ALTITUDE] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG;
3291 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_VOLTAGE] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG;
3292 osdLayoutsConfig->item_pos[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE] = OSD_POS(12, 1);
3294 osdLayoutsConfig->item_pos[0][OSD_RSSI_VALUE] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG;
3295 //line 2
3296 osdLayoutsConfig->item_pos[0][OSD_HOME_DIST] = OSD_POS(1, 1);
3297 osdLayoutsConfig->item_pos[0][OSD_TRIP_DIST] = OSD_POS(1, 2);
3298 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_CELL_VOLTAGE] = OSD_POS(12, 1);
3299 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE] = OSD_POS(12, 1);
3300 osdLayoutsConfig->item_pos[0][OSD_GPS_SPEED] = OSD_POS(23, 1);
3301 osdLayoutsConfig->item_pos[0][OSD_3D_SPEED] = OSD_POS(23, 1);
3302 osdLayoutsConfig->item_pos[0][OSD_GLIDESLOPE] = OSD_POS(23, 2);
3304 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG;
3305 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS_AUTO_THR] = OSD_POS(6, 2);
3306 osdLayoutsConfig->item_pos[0][OSD_HEADING] = OSD_POS(12, 2);
3307 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ERROR] = OSD_POS(12, 2);
3308 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ADJUSTMENT] = OSD_POS(12, 2);
3309 osdLayoutsConfig->item_pos[0][OSD_HEADING_GRAPH] = OSD_POS(18, 2);
3310 osdLayoutsConfig->item_pos[0][OSD_CURRENT_DRAW] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG;
3311 osdLayoutsConfig->item_pos[0][OSD_MAH_DRAWN] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG;
3312 osdLayoutsConfig->item_pos[0][OSD_WH_DRAWN] = OSD_POS(1, 5);
3313 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_CAPACITY] = OSD_POS(1, 6);
3314 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_PERCENT] = OSD_POS(1, 7);
3315 osdLayoutsConfig->item_pos[0][OSD_POWER_SUPPLY_IMPEDANCE] = OSD_POS(1, 8);
3317 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_MAH_PER_KM] = OSD_POS(1, 5);
3318 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_WH_PER_KM] = OSD_POS(1, 5);
3320 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_ROLL] = OSD_POS(1, 7);
3321 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_PITCH] = OSD_POS(1, 8);
3323 // avoid OSD_VARIO under OSD_CROSSHAIRS
3324 osdLayoutsConfig->item_pos[0][OSD_VARIO] = OSD_POS(23, 5);
3325 // OSD_VARIO_NUM at the right of OSD_VARIO
3326 osdLayoutsConfig->item_pos[0][OSD_VARIO_NUM] = OSD_POS(24, 7);
3327 osdLayoutsConfig->item_pos[0][OSD_HOME_DIR] = OSD_POS(14, 11);
3328 osdLayoutsConfig->item_pos[0][OSD_ARTIFICIAL_HORIZON] = OSD_POS(8, 6);
3329 osdLayoutsConfig->item_pos[0][OSD_HORIZON_SIDEBARS] = OSD_POS(8, 6);
3331 osdLayoutsConfig->item_pos[0][OSD_CRAFT_NAME] = OSD_POS(20, 2);
3332 osdLayoutsConfig->item_pos[0][OSD_VTX_CHANNEL] = OSD_POS(8, 6);
3334 #ifdef USE_SERIALRX_CRSF
3335 osdLayoutsConfig->item_pos[0][OSD_CRSF_RSSI_DBM] = OSD_POS(23, 12);
3336 osdLayoutsConfig->item_pos[0][OSD_CRSF_LQ] = OSD_POS(23, 11);
3337 osdLayoutsConfig->item_pos[0][OSD_CRSF_SNR_DB] = OSD_POS(24, 9);
3338 osdLayoutsConfig->item_pos[0][OSD_CRSF_TX_POWER] = OSD_POS(24, 10);
3339 #endif
3341 osdLayoutsConfig->item_pos[0][OSD_ONTIME] = OSD_POS(23, 8);
3342 osdLayoutsConfig->item_pos[0][OSD_FLYTIME] = OSD_POS(23, 9);
3343 osdLayoutsConfig->item_pos[0][OSD_ONTIME_FLYTIME] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG;
3344 osdLayoutsConfig->item_pos[0][OSD_RTC_TIME] = OSD_POS(23, 12);
3345 osdLayoutsConfig->item_pos[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH] = OSD_POS(23, 7);
3346 osdLayoutsConfig->item_pos[0][OSD_REMAINING_DISTANCE_BEFORE_RTH] = OSD_POS(23, 6);
3348 osdLayoutsConfig->item_pos[0][OSD_MISSION] = OSD_POS(0, 10);
3349 osdLayoutsConfig->item_pos[0][OSD_GPS_SATS] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG;
3350 osdLayoutsConfig->item_pos[0][OSD_GPS_HDOP] = OSD_POS(0, 10);
3352 osdLayoutsConfig->item_pos[0][OSD_GPS_LAT] = OSD_POS(0, 12);
3353 // Put this on top of the latitude, since it's very unlikely
3354 // that users will want to use both at the same time.
3355 osdLayoutsConfig->item_pos[0][OSD_PLUS_CODE] = OSD_POS(0, 12);
3356 osdLayoutsConfig->item_pos[0][OSD_FLYMODE] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG;
3357 osdLayoutsConfig->item_pos[0][OSD_GPS_LON] = OSD_POS(18, 12);
3359 osdLayoutsConfig->item_pos[0][OSD_AZIMUTH] = OSD_POS(2, 12);
3361 osdLayoutsConfig->item_pos[0][OSD_ROLL_PIDS] = OSD_POS(2, 10);
3362 osdLayoutsConfig->item_pos[0][OSD_PITCH_PIDS] = OSD_POS(2, 11);
3363 osdLayoutsConfig->item_pos[0][OSD_YAW_PIDS] = OSD_POS(2, 12);
3364 osdLayoutsConfig->item_pos[0][OSD_LEVEL_PIDS] = OSD_POS(2, 12);
3365 osdLayoutsConfig->item_pos[0][OSD_POS_XY_PIDS] = OSD_POS(2, 12);
3366 osdLayoutsConfig->item_pos[0][OSD_POS_Z_PIDS] = OSD_POS(2, 12);
3367 osdLayoutsConfig->item_pos[0][OSD_VEL_XY_PIDS] = OSD_POS(2, 12);
3368 osdLayoutsConfig->item_pos[0][OSD_VEL_Z_PIDS] = OSD_POS(2, 12);
3369 osdLayoutsConfig->item_pos[0][OSD_HEADING_P] = OSD_POS(2, 12);
3370 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_ROLL] = OSD_POS(2, 10);
3371 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_PITCH] = OSD_POS(2, 11);
3372 osdLayoutsConfig->item_pos[0][OSD_RC_EXPO] = OSD_POS(2, 12);
3373 osdLayoutsConfig->item_pos[0][OSD_RC_YAW_EXPO] = OSD_POS(2, 12);
3374 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_EXPO] = OSD_POS(2, 12);
3375 osdLayoutsConfig->item_pos[0][OSD_PITCH_RATE] = OSD_POS(2, 12);
3376 osdLayoutsConfig->item_pos[0][OSD_ROLL_RATE] = OSD_POS(2, 12);
3377 osdLayoutsConfig->item_pos[0][OSD_YAW_RATE] = OSD_POS(2, 12);
3378 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_EXPO] = OSD_POS(2, 12);
3379 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_YAW_EXPO] = OSD_POS(2, 12);
3380 osdLayoutsConfig->item_pos[0][OSD_MANUAL_PITCH_RATE] = OSD_POS(2, 12);
3381 osdLayoutsConfig->item_pos[0][OSD_MANUAL_ROLL_RATE] = OSD_POS(2, 12);
3382 osdLayoutsConfig->item_pos[0][OSD_MANUAL_YAW_RATE] = OSD_POS(2, 12);
3383 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_CRUISE_THR] = OSD_POS(2, 12);
3384 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_PITCH2THR] = OSD_POS(2, 12);
3385 osdLayoutsConfig->item_pos[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE] = OSD_POS(2, 12);
3386 osdLayoutsConfig->item_pos[0][OSD_FW_ALT_PID_OUTPUTS] = OSD_POS(2, 12);
3387 osdLayoutsConfig->item_pos[0][OSD_FW_POS_PID_OUTPUTS] = OSD_POS(2, 12);
3388 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_X_PID_OUTPUTS] = OSD_POS(2, 12);
3389 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Y_PID_OUTPUTS] = OSD_POS(2, 12);
3390 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Z_PID_OUTPUTS] = OSD_POS(2, 12);
3391 osdLayoutsConfig->item_pos[0][OSD_MC_POS_XYZ_P_OUTPUTS] = OSD_POS(2, 12);
3393 osdLayoutsConfig->item_pos[0][OSD_POWER] = OSD_POS(15, 1);
3395 osdLayoutsConfig->item_pos[0][OSD_IMU_TEMPERATURE] = OSD_POS(19, 2);
3396 osdLayoutsConfig->item_pos[0][OSD_BARO_TEMPERATURE] = OSD_POS(19, 3);
3397 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_0_TEMPERATURE] = OSD_POS(19, 4);
3398 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_1_TEMPERATURE] = OSD_POS(19, 5);
3399 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_2_TEMPERATURE] = OSD_POS(19, 6);
3400 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_3_TEMPERATURE] = OSD_POS(19, 7);
3401 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_4_TEMPERATURE] = OSD_POS(19, 8);
3402 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_5_TEMPERATURE] = OSD_POS(19, 9);
3403 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_6_TEMPERATURE] = OSD_POS(19, 10);
3404 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_7_TEMPERATURE] = OSD_POS(19, 11);
3406 osdLayoutsConfig->item_pos[0][OSD_AIR_SPEED] = OSD_POS(3, 5);
3407 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_HORIZONTAL] = OSD_POS(3, 6);
3408 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_VERTICAL] = OSD_POS(3, 7);
3410 osdLayoutsConfig->item_pos[0][OSD_GFORCE] = OSD_POS(12, 4);
3411 osdLayoutsConfig->item_pos[0][OSD_GFORCE_X] = OSD_POS(12, 5);
3412 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Y] = OSD_POS(12, 6);
3413 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Z] = OSD_POS(12, 7);
3415 osdLayoutsConfig->item_pos[0][OSD_VTX_POWER] = OSD_POS(3, 5);
3417 osdLayoutsConfig->item_pos[0][OSD_GVAR_0] = OSD_POS(1, 1);
3418 osdLayoutsConfig->item_pos[0][OSD_GVAR_1] = OSD_POS(1, 2);
3419 osdLayoutsConfig->item_pos[0][OSD_GVAR_2] = OSD_POS(1, 3);
3420 osdLayoutsConfig->item_pos[0][OSD_GVAR_3] = OSD_POS(1, 4);
3422 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_0] = OSD_POS(2, 7);
3423 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_1] = OSD_POS(2, 8);
3424 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_2] = OSD_POS(2, 9);
3425 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_3] = OSD_POS(2, 10);
3427 #if defined(USE_ESC_SENSOR)
3428 osdLayoutsConfig->item_pos[0][OSD_ESC_RPM] = OSD_POS(1, 2);
3429 osdLayoutsConfig->item_pos[0][OSD_ESC_TEMPERATURE] = OSD_POS(1, 3);
3430 #endif
3432 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3433 osdLayoutsConfig->item_pos[0][OSD_RC_SOURCE] = OSD_POS(3, 4);
3434 #endif
3436 #ifdef USE_POWER_LIMITS
3437 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_REMAINING_BURST_TIME] = OSD_POS(3, 4);
3438 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT] = OSD_POS(3, 5);
3439 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT] = OSD_POS(3, 6);
3440 #endif
3442 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
3443 osdLayoutsConfig->item_pos[0][OSD_MESSAGES] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG;
3445 for (unsigned ii = 1; ii < OSD_LAYOUT_COUNT; ii++) {
3446 for (unsigned jj = 0; jj < ARRAYLEN(osdLayoutsConfig->item_pos[0]); jj++) {
3447 osdLayoutsConfig->item_pos[ii][jj] = osdLayoutsConfig->item_pos[0][jj] & ~OSD_VISIBLE_FLAG;
3452 static void osdSetNextRefreshIn(uint32_t timeMs) {
3453 resumeRefreshAt = micros() + timeMs * 1000;
3454 refreshWaitForResumeCmdRelease = true;
3457 static void osdCompleteAsyncInitialization(void)
3459 if (!displayIsReady(osdDisplayPort)) {
3460 // Update the display.
3461 // XXX: Rename displayDrawScreen() and associated functions
3462 // to displayUpdate()
3463 displayDrawScreen(osdDisplayPort);
3464 return;
3467 osdDisplayIsReady = true;
3469 #if defined(USE_CANVAS)
3470 if (osdConfig()->force_grid) {
3471 osdDisplayHasCanvas = false;
3472 } else {
3473 osdDisplayHasCanvas = displayGetCanvas(&osdCanvas, osdDisplayPort);
3475 #endif
3477 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3478 displayClearScreen(osdDisplayPort);
3480 uint8_t y = 1;
3481 displayFontMetadata_t metadata;
3482 bool fontHasMetadata = displayGetFontMetadata(&metadata, osdDisplayPort);
3483 LOG_D(OSD, "Font metadata version %s: %u (%u chars)",
3484 fontHasMetadata ? "Y" : "N", metadata.version, metadata.charCount);
3486 if (fontHasMetadata && metadata.charCount > 256) {
3487 hasExtendedFont = true;
3488 unsigned logo_c = SYM_LOGO_START;
3489 unsigned logo_x = OSD_CENTER_LEN(SYM_LOGO_WIDTH);
3490 for (unsigned ii = 0; ii < SYM_LOGO_HEIGHT; ii++) {
3491 for (unsigned jj = 0; jj < SYM_LOGO_WIDTH; jj++) {
3492 displayWriteChar(osdDisplayPort, logo_x + jj, y, logo_c++);
3494 y++;
3496 y++;
3497 } else if (!fontHasMetadata) {
3498 const char *m = "INVALID FONT";
3499 displayWrite(osdDisplayPort, OSD_CENTER_S(m), 3, m);
3500 y = 4;
3503 if (fontHasMetadata && metadata.version < OSD_MIN_FONT_VERSION) {
3504 const char *m = "INVALID FONT VERSION";
3505 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
3508 char string_buffer[30];
3509 tfp_sprintf(string_buffer, "INAV VERSION: %s", FC_VERSION_STRING);
3510 displayWrite(osdDisplayPort, 5, y++, string_buffer);
3511 #ifdef USE_CMS
3512 displayWrite(osdDisplayPort, 7, y++, CMS_STARTUP_HELP_TEXT1);
3513 displayWrite(osdDisplayPort, 11, y++, CMS_STARTUP_HELP_TEXT2);
3514 displayWrite(osdDisplayPort, 11, y++, CMS_STARTUP_HELP_TEXT3);
3515 #endif
3517 #ifdef USE_STATS
3518 #define STATS_LABEL_X_POS 4
3519 #define STATS_VALUE_X_POS 24
3520 if (statsConfig()->stats_enabled) {
3521 displayWrite(osdDisplayPort, STATS_LABEL_X_POS, ++y, "ODOMETER:");
3522 switch (osdConfig()->units) {
3523 case OSD_UNIT_UK:
3524 FALLTHROUGH;
3525 case OSD_UNIT_IMPERIAL:
3526 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_MILE));
3527 string_buffer[5] = SYM_MI;
3528 break;
3529 default:
3530 case OSD_UNIT_GA:
3531 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE));
3532 string_buffer[5] = SYM_NM;
3533 break;
3534 case OSD_UNIT_METRIC_MPH:
3535 FALLTHROUGH;
3536 case OSD_UNIT_METRIC:
3537 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER));
3538 string_buffer[5] = SYM_KM;
3539 break;
3541 string_buffer[6] = '\0';
3542 displayWrite(osdDisplayPort, STATS_VALUE_X_POS-5, y, string_buffer);
3544 displayWrite(osdDisplayPort, STATS_LABEL_X_POS, ++y, "TOTAL TIME:");
3545 uint32_t tot_mins = statsConfig()->stats_total_time / 60;
3546 tfp_sprintf(string_buffer, "%2d:%02dHM", (int)(tot_mins / 60), (int)(tot_mins % 60));
3547 displayWrite(osdDisplayPort, STATS_VALUE_X_POS-5, y, string_buffer);
3549 #ifdef USE_ADC
3550 if (feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER)) {
3551 displayWrite(osdDisplayPort, STATS_LABEL_X_POS, ++y, "TOTAL ENERGY:");
3552 osdFormatCentiNumber(string_buffer, statsConfig()->stats_total_energy / 10, 0, 2, 0, 4);
3553 strcat(string_buffer, "\xAB"); // SYM_WH
3554 displayWrite(osdDisplayPort, STATS_VALUE_X_POS-4, y, string_buffer);
3556 displayWrite(osdDisplayPort, STATS_LABEL_X_POS, ++y, "AVG EFFICIENCY:");
3557 if (statsConfig()->stats_total_dist) {
3558 uint32_t avg_efficiency = statsConfig()->stats_total_energy / (statsConfig()->stats_total_dist / METERS_PER_KILOMETER); // mWh/km
3559 switch (osdConfig()->units) {
3560 case OSD_UNIT_UK:
3561 FALLTHROUGH;
3562 case OSD_UNIT_IMPERIAL:
3563 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3564 string_buffer[3] = SYM_WH_MI;
3565 break;
3566 case OSD_UNIT_GA:
3567 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3568 string_buffer[3] = SYM_WH_NM;
3569 break;
3570 default:
3571 case OSD_UNIT_METRIC_MPH:
3572 FALLTHROUGH;
3573 case OSD_UNIT_METRIC:
3574 osdFormatCentiNumber(string_buffer, avg_efficiency / 10000 * METERS_PER_MILE, 0, 2, 0, 3);
3575 string_buffer[3] = SYM_WH_KM;
3576 break;
3578 } else {
3579 string_buffer[0] = string_buffer[1] = string_buffer[2] = '-';
3581 string_buffer[4] = '\0';
3582 displayWrite(osdDisplayPort, STATS_VALUE_X_POS-3, y, string_buffer);
3584 #endif // USE_ADC
3586 #endif
3588 displayCommitTransaction(osdDisplayPort);
3589 displayResync(osdDisplayPort);
3590 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME);
3593 void osdInit(displayPort_t *osdDisplayPortToUse)
3595 if (!osdDisplayPortToUse)
3596 return;
3598 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(31,31));
3600 osdDisplayPort = osdDisplayPortToUse;
3602 #ifdef USE_CMS
3603 cmsDisplayPortRegister(osdDisplayPort);
3604 #endif
3606 armState = ARMING_FLAG(ARMED);
3607 osdCompleteAsyncInitialization();
3610 static void osdResetStats(void)
3612 stats.max_current = 0;
3613 stats.max_power = 0;
3614 stats.max_speed = 0;
3615 stats.max_3D_speed = 0;
3616 stats.max_air_speed = 0;
3617 stats.min_voltage = 5000;
3618 stats.min_rssi = 99;
3619 stats.min_lq = 300;
3620 stats.min_rssi_dbm = 0;
3621 stats.max_altitude = 0;
3624 static void osdUpdateStats(void)
3626 int32_t value;
3628 if (feature(FEATURE_GPS)) {
3629 value = osdGet3DSpeed();
3630 if (stats.max_3D_speed < value)
3631 stats.max_3D_speed = value;
3633 if (stats.max_speed < gpsSol.groundSpeed)
3634 stats.max_speed = gpsSol.groundSpeed;
3636 if (stats.max_air_speed < pitot.airSpeed)
3637 stats.max_air_speed = pitot.airSpeed;
3639 if (stats.max_distance < GPS_distanceToHome)
3640 stats.max_distance = GPS_distanceToHome;
3643 value = getBatteryVoltage();
3644 if (stats.min_voltage > value)
3645 stats.min_voltage = value;
3647 value = abs(getAmperage());
3648 if (stats.max_current < value)
3649 stats.max_current = value;
3651 value = labs(getPower());
3652 if (stats.max_power < value)
3653 stats.max_power = value;
3655 value = osdConvertRSSI();
3656 if (stats.min_rssi > value)
3657 stats.min_rssi = value;
3659 value = osdGetCrsfLQ();
3660 if (stats.min_lq > value)
3661 stats.min_lq = value;
3663 if (!failsafeIsReceivingRxData())
3664 stats.min_lq = 0;
3666 value = osdGetCrsfdBm();
3667 if (stats.min_rssi_dbm > value)
3668 stats.min_rssi_dbm = value;
3670 stats.max_altitude = MAX(stats.max_altitude, osdGetAltitude());
3673 static void osdShowStatsPage1(void)
3675 const char * disarmReasonStr[DISARM_REASON_COUNT] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "KILLSW", "FAILSAFE", "NAV SYS" };
3676 uint8_t top = 1; /* first fully visible line */
3677 const uint8_t statNameX = 1;
3678 const uint8_t statValuesX = 20;
3679 char buff[10];
3680 statsPagesCheck = 1;
3682 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3683 displayClearScreen(osdDisplayPort);
3685 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- 1/2 ->");
3687 if (feature(FEATURE_GPS)) {
3688 displayWrite(osdDisplayPort, statNameX, top, "MAX SPEED :");
3689 osdFormatVelocityStr(buff, stats.max_3D_speed, true, false);
3690 osdLeftAlignString(buff);
3691 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3693 displayWrite(osdDisplayPort, statNameX, top, "AVG SPEED :");
3694 osdGenerateAverageVelocityStr(buff);
3695 osdLeftAlignString(buff);
3696 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3698 displayWrite(osdDisplayPort, statNameX, top, "MAX DISTANCE :");
3699 osdFormatDistanceStr(buff, stats.max_distance*100);
3700 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3702 displayWrite(osdDisplayPort, statNameX, top, "TRAVELED DISTANCE:");
3703 osdFormatDistanceStr(buff, getTotalTravelDistance());
3704 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3707 displayWrite(osdDisplayPort, statNameX, top, "MAX ALTITUDE :");
3708 osdFormatAltitudeStr(buff, stats.max_altitude);
3709 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3711 switch (rxConfig()->serialrx_provider) {
3712 case SERIALRX_CRSF:
3713 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI % :");
3714 itoa(stats.min_rssi, buff, 10);
3715 strcat(buff, "%");
3716 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3718 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI DBM :");
3719 itoa(stats.min_rssi_dbm, buff, 10);
3720 tfp_sprintf(buff, "%s%c", buff, SYM_DBM);
3721 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3723 displayWrite(osdDisplayPort, statNameX, top, "MIN LQ :");
3724 itoa(stats.min_lq, buff, 10);
3725 strcat(buff, "%");
3726 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3727 break;
3728 default:
3729 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI :");
3730 itoa(stats.min_rssi, buff, 10);
3731 strcat(buff, "%");
3732 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3735 displayWrite(osdDisplayPort, statNameX, top, "FLY TIME :");
3736 uint16_t flySeconds = getFlightTime();
3737 uint16_t flyMinutes = flySeconds / 60;
3738 flySeconds %= 60;
3739 uint16_t flyHours = flyMinutes / 60;
3740 flyMinutes %= 60;
3741 tfp_sprintf(buff, "%02u:%02u:%02u", flyHours, flyMinutes, flySeconds);
3742 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3744 displayWrite(osdDisplayPort, statNameX, top, "DISARMED BY :");
3745 displayWrite(osdDisplayPort, statValuesX, top++, disarmReasonStr[getDisarmReason()]);
3746 displayCommitTransaction(osdDisplayPort);
3749 static void osdShowStatsPage2(void)
3751 uint8_t top = 1; /* first fully visible line */
3752 const uint8_t statNameX = 1;
3753 const uint8_t statValuesX = 20;
3754 char buff[10];
3755 statsPagesCheck = 1;
3757 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3758 displayClearScreen(osdDisplayPort);
3760 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- <- 2/2");
3762 if (osdConfig()->stats_min_voltage_unit == OSD_STATS_MIN_VOLTAGE_UNIT_BATTERY) {
3763 displayWrite(osdDisplayPort, statNameX, top, "MIN BATTERY VOLT :");
3764 osdFormatCentiNumber(buff, stats.min_voltage, 0, osdConfig()->main_voltage_decimals, 0, osdConfig()->main_voltage_decimals + 2);
3765 } else {
3766 displayWrite(osdDisplayPort, statNameX, top, "MIN CELL VOLTAGE :");
3767 osdFormatCentiNumber(buff, stats.min_voltage/getBatteryCellCount(), 0, 2, 0, 3);
3769 tfp_sprintf(buff, "%s%c", buff, SYM_VOLT);
3770 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3772 if (feature(FEATURE_CURRENT_METER)) {
3773 displayWrite(osdDisplayPort, statNameX, top, "MAX CURRENT :");
3774 osdFormatCentiNumber(buff, stats.max_current, 0, 2, 0, 3);
3775 tfp_sprintf(buff, "%s%c", buff, SYM_AMP);
3776 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3778 displayWrite(osdDisplayPort, statNameX, top, "MAX POWER :");
3779 bool kiloWatt = osdFormatCentiNumber(buff, stats.max_power, 1000, 2, 2, 3);
3780 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3781 buff[4] = '\0';
3782 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3784 displayWrite(osdDisplayPort, statNameX, top, "USED CAPACITY :");
3785 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3786 tfp_sprintf(buff, "%d%c", (int)getMAhDrawn(), SYM_MAH);
3787 } else {
3788 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
3789 tfp_sprintf(buff, "%s%c", buff, SYM_WH);
3791 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3793 int32_t totalDistance = getTotalTravelDistance();
3794 bool moreThanAh = false;
3795 bool efficiencyValid = totalDistance >= 10000;
3796 if (feature(FEATURE_GPS)) {
3797 displayWrite(osdDisplayPort, statNameX, top, "AVG EFFICIENCY :");
3798 switch (osdConfig()->units) {
3799 case OSD_UNIT_UK:
3800 FALLTHROUGH;
3801 case OSD_UNIT_IMPERIAL:
3802 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3803 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, 3);
3804 if (!moreThanAh) {
3805 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
3806 } else {
3807 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
3809 if (!efficiencyValid) {
3810 buff[0] = buff[1] = buff[2] = '-';
3811 buff[3] = SYM_MAH_MI_0;
3812 buff[4] = SYM_MAH_MI_1;
3813 buff[5] = '\0';
3815 } else {
3816 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, 3);
3817 tfp_sprintf(buff, "%s%c", buff, SYM_WH_MI);
3818 if (!efficiencyValid) {
3819 buff[0] = buff[1] = buff[2] = '-';
3822 break;
3823 case OSD_UNIT_GA:
3824 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3825 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, 3);
3826 if (!moreThanAh) {
3827 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
3828 } else {
3829 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
3831 if (!efficiencyValid) {
3832 buff[0] = buff[1] = buff[2] = '-';
3833 buff[3] = SYM_MAH_NM_0;
3834 buff[4] = SYM_MAH_NM_1;
3835 buff[5] = '\0';
3837 } else {
3838 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, 3);
3839 tfp_sprintf(buff, "%s%c", buff, SYM_WH_NM);
3840 if (!efficiencyValid) {
3841 buff[0] = buff[1] = buff[2] = '-';
3844 break;
3845 case OSD_UNIT_METRIC_MPH:
3846 FALLTHROUGH;
3847 case OSD_UNIT_METRIC:
3848 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3849 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000000.0f / totalDistance), 1000, 0, 2, 3);
3850 if (!moreThanAh) {
3851 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
3852 } else {
3853 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
3855 if (!efficiencyValid) {
3856 buff[0] = buff[1] = buff[2] = '-';
3857 buff[3] = SYM_MAH_KM_0;
3858 buff[4] = SYM_MAH_KM_1;
3859 buff[5] = '\0';
3861 } else {
3862 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10000.0f / totalDistance), 0, 2, 0, 3);
3863 tfp_sprintf(buff, "%s%c", buff, SYM_WH_KM);
3864 if (!efficiencyValid) {
3865 buff[0] = buff[1] = buff[2] = '-';
3868 break;
3870 osdLeftAlignString(buff);
3871 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3875 const float max_gforce = accGetMeasuredMaxG();
3876 displayWrite(osdDisplayPort, statNameX, top, "MAX G-FORCE :");
3877 osdFormatCentiNumber(buff, max_gforce * 100, 0, 2, 0, 3);
3878 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3880 const acc_extremes_t *acc_extremes = accGetMeasuredExtremes();
3881 displayWrite(osdDisplayPort, statNameX, top, "MIN/MAX Z G-FORCE:");
3882 osdFormatCentiNumber(buff, acc_extremes[Z].min * 100, 0, 2, 0, 4);
3883 strcat(buff,"/");
3884 displayWrite(osdDisplayPort, statValuesX - 1, top, buff);
3885 osdFormatCentiNumber(buff, acc_extremes[Z].max * 100, 0, 2, 0, 3);
3886 displayWrite(osdDisplayPort, statValuesX + 4, top++, buff);
3887 displayCommitTransaction(osdDisplayPort);
3890 // called when motors armed
3891 static void osdShowArmed(void)
3893 dateTime_t dt;
3894 char buf[MAX(32, FORMATTED_DATE_TIME_BUFSIZE)];
3895 char craftNameBuf[MAX_NAME_LENGTH];
3896 char versionBuf[30];
3897 char *date;
3898 char *time;
3899 // We need 12 visible rows, start row never < first fully visible row 1
3900 uint8_t y = osdDisplayPort->rows > 13 ? (osdDisplayPort->rows - 12) / 2 : 1;
3902 displayClearScreen(osdDisplayPort);
3903 displayWrite(osdDisplayPort, 12, y, "ARMED");
3904 y += 2;
3906 if (strlen(systemConfig()->name) > 0) {
3907 osdFormatCraftName(craftNameBuf);
3908 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(systemConfig() -> name)) / 2, y, craftNameBuf );
3909 y += 1;
3911 if (posControl.waypointListValid && posControl.waypointCount > 0) {
3912 #ifdef USE_MULTI_MISSION
3913 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
3914 displayWrite(osdDisplayPort, 6, y, buf);
3915 #else
3916 displayWrite(osdDisplayPort, 7, y, "*MISSION LOADED*");
3917 #endif
3919 y += 1;
3921 #if defined(USE_GPS)
3922 if (feature(FEATURE_GPS)) {
3923 if (STATE(GPS_FIX_HOME)) {
3924 if (osdConfig()->osd_home_position_arm_screen){
3925 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
3926 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
3927 osdFormatCoordinate(buf, SYM_LON, GPS_home.lon);
3928 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 1, buf);
3929 int digits = osdConfig()->plus_code_digits;
3930 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
3931 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 2, buf);
3933 y += 4;
3934 #if defined (USE_SAFE_HOME)
3935 if (safehome_distance) { // safehome found during arming
3936 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
3937 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
3938 } else {
3939 char buf2[12]; // format the distance first
3940 osdFormatDistanceStr(buf2, safehome_distance);
3941 tfp_sprintf(buf, "%c - %s -> SAFEHOME %u", SYM_HOME, buf2, safehome_index);
3943 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
3944 // write this message above the ARMED message to make it obvious
3945 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y - 8, buf, elemAttr);
3947 #endif
3948 } else {
3949 strcpy(buf, "!NO HOME POSITION!");
3950 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
3951 y += 1;
3954 #endif
3956 if (rtcGetDateTime(&dt)) {
3957 dateTimeFormatLocal(buf, &dt);
3958 dateTimeSplitFormatted(buf, &date, &time);
3960 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(date)) / 2, y, date);
3961 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(time)) / 2, y + 1, time);
3962 y += 3;
3965 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
3966 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, y, versionBuf);
3969 static void osdFilterData(timeUs_t currentTimeUs) {
3970 static timeUs_t lastRefresh = 0;
3971 float refresh_dT = US2S(cmpTimeUs(currentTimeUs, lastRefresh));
3973 GForce = fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF)) / GRAVITY_MSS;
3974 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) GForceAxis[axis] = imuMeasuredAccelBF.v[axis] / GRAVITY_MSS;
3976 if (lastRefresh) {
3977 GForce = pt1FilterApply3(&GForceFilter, GForce, refresh_dT);
3978 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) pt1FilterApply3(GForceFilterAxis + axis, GForceAxis[axis], refresh_dT);
3979 } else {
3980 pt1FilterInitRC(&GForceFilter, GFORCE_FILTER_TC, 0);
3981 pt1FilterReset(&GForceFilter, GForce);
3983 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) {
3984 pt1FilterInitRC(GForceFilterAxis + axis, GFORCE_FILTER_TC, 0);
3985 pt1FilterReset(GForceFilterAxis + axis, GForceAxis[axis]);
3989 lastRefresh = currentTimeUs;
3992 static void osdRefresh(timeUs_t currentTimeUs)
3994 osdFilterData(currentTimeUs);
3996 #ifdef USE_CMS
3997 if (IS_RC_MODE_ACTIVE(BOXOSD) && (!cmsInMenu) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
3998 #else
3999 if (IS_RC_MODE_ACTIVE(BOXOSD) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4000 #endif
4001 displayClearScreen(osdDisplayPort);
4002 armState = ARMING_FLAG(ARMED);
4003 return;
4006 // detect arm/disarm
4007 static uint8_t statsPageAutoSwapCntl = 2;
4008 if (armState != ARMING_FLAG(ARMED)) {
4009 if (ARMING_FLAG(ARMED)) {
4010 osdResetStats();
4011 statsPageAutoSwapCntl = 2;
4012 osdShowArmed(); // reset statistic etc
4013 uint32_t delay = ARMED_SCREEN_DISPLAY_TIME;
4014 statsPagesCheck = 0;
4015 #if defined(USE_SAFE_HOME)
4016 if (safehome_distance)
4017 delay *= 3;
4018 #endif
4019 osdSetNextRefreshIn(delay);
4020 } else {
4021 osdShowStatsPage1(); // show first page of statistics
4022 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME);
4023 statsPageAutoSwapCntl = osdConfig()->stats_page_auto_swap_time > 0 ? 0 : 2; // disable swapping pages when time = 0
4026 armState = ARMING_FLAG(ARMED);
4029 if (resumeRefreshAt) {
4030 // If we already reached he time for the next refresh,
4031 // or THR is high or PITCH is high, resume refreshing.
4032 // Clear the screen first to erase other elements which
4033 // might have been drawn while the OSD wasn't refreshing.
4035 // auto swap stats pages when first shown
4036 // auto swap cancelled using roll stick
4037 if (statsPageAutoSwapCntl != 2) {
4038 if (STATS_PAGE1 || STATS_PAGE2) {
4039 statsPageAutoSwapCntl = 2;
4040 } else {
4041 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time * 1000), 2)) {
4042 if (statsPageAutoSwapCntl == 0) {
4043 osdShowStatsPage1();
4044 statsPageAutoSwapCntl = 1;
4046 } else {
4047 if (statsPageAutoSwapCntl == 1) {
4048 osdShowStatsPage2();
4049 statsPageAutoSwapCntl = 0;
4055 if (!DELAYED_REFRESH_RESUME_COMMAND)
4056 refreshWaitForResumeCmdRelease = false;
4058 if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && DELAYED_REFRESH_RESUME_COMMAND)) {
4059 displayClearScreen(osdDisplayPort);
4060 resumeRefreshAt = 0;
4061 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE1)) {
4062 if (statsPagesCheck == 1) {
4063 osdShowStatsPage1();
4065 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE2)) {
4066 if (statsPagesCheck == 1) {
4067 osdShowStatsPage2();
4069 } else {
4070 displayHeartbeat(osdDisplayPort);
4072 return;
4075 #ifdef USE_CMS
4076 if (!displayIsGrabbed(osdDisplayPort)) {
4077 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4078 if (fullRedraw) {
4079 displayClearScreen(osdDisplayPort);
4080 fullRedraw = false;
4082 osdDrawNextElement();
4083 displayHeartbeat(osdDisplayPort);
4084 displayCommitTransaction(osdDisplayPort);
4085 #ifdef OSD_CALLS_CMS
4086 } else {
4087 cmsUpdate(currentTimeUs);
4088 #endif
4090 #endif
4094 * Called periodically by the scheduler
4096 void osdUpdate(timeUs_t currentTimeUs)
4098 static uint32_t counter = 0;
4100 // don't touch buffers if DMA transaction is in progress
4101 if (displayIsTransferInProgress(osdDisplayPort)) {
4102 return;
4105 if (!osdDisplayIsReady) {
4106 osdCompleteAsyncInitialization();
4107 return;
4110 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
4111 // Check if the layout has changed. Higher numbered
4112 // boxes take priority.
4113 unsigned activeLayout;
4114 if (layoutOverride >= 0) {
4115 activeLayout = layoutOverride;
4116 // Check for timed override, it will go into effect on
4117 // the next OSD iteration
4118 if (layoutOverrideUntil > 0 && millis() > layoutOverrideUntil) {
4119 layoutOverrideUntil = 0;
4120 layoutOverride = -1;
4122 } else if (osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE)) {
4123 activeLayout = 0;
4124 } else {
4125 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
4126 if (IS_RC_MODE_ACTIVE(BOXOSDALT3))
4127 activeLayout = 3;
4128 else
4129 #endif
4130 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
4131 if (IS_RC_MODE_ACTIVE(BOXOSDALT2))
4132 activeLayout = 2;
4133 else
4134 #endif
4135 if (IS_RC_MODE_ACTIVE(BOXOSDALT1))
4136 activeLayout = 1;
4137 else
4138 #ifdef USE_PROGRAMMING_FRAMEWORK
4139 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT))
4140 activeLayout = constrain(logicConditionValuesByType[LOGIC_CONDITION_SET_OSD_LAYOUT], 0, OSD_ALTERNATE_LAYOUT_COUNT);
4141 else
4142 #endif
4143 activeLayout = 0;
4145 if (currentLayout != activeLayout) {
4146 currentLayout = activeLayout;
4147 osdStartFullRedraw();
4149 #endif
4151 #define DRAW_FREQ_DENOM 4
4152 #define STATS_FREQ_DENOM 50
4153 counter++;
4155 if ((counter % STATS_FREQ_DENOM) == 0) {
4156 osdUpdateStats();
4159 if ((counter & DRAW_FREQ_DENOM) == 0) {
4160 // redraw values in buffer
4161 osdRefresh(currentTimeUs);
4162 } else {
4163 // rest of time redraw screen
4164 displayDrawScreen(osdDisplayPort);
4167 #ifdef USE_CMS
4168 // do not allow ARM if we are in menu
4169 if (displayIsGrabbed(osdDisplayPort)) {
4170 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4171 } else {
4172 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4174 #endif
4177 void osdStartFullRedraw(void)
4179 fullRedraw = true;
4182 void osdOverrideLayout(int layout, timeMs_t duration)
4184 layoutOverride = constrain(layout, -1, ARRAYLEN(osdLayoutsConfig()->item_pos) - 1);
4185 if (layoutOverride >= 0 && duration > 0) {
4186 layoutOverrideUntil = millis() + duration;
4187 } else {
4188 layoutOverrideUntil = 0;
4192 int osdGetActiveLayout(bool *overridden)
4194 if (overridden) {
4195 *overridden = layoutOverride >= 0;
4197 return currentLayout;
4200 bool osdItemIsFixed(osd_items_e item)
4202 return item == OSD_CROSSHAIRS ||
4203 item == OSD_ARTIFICIAL_HORIZON ||
4204 item == OSD_HORIZON_SIDEBARS;
4207 displayPort_t *osdGetDisplayPort(void)
4209 return osdDisplayPort;
4212 displayCanvas_t *osdGetDisplayPortCanvas(void)
4214 #if defined(USE_CANVAS)
4215 if (osdDisplayHasCanvas) {
4216 return &osdCanvas;
4218 #endif
4219 return NULL;
4222 textAttributes_t osdGetSystemMessage(char *buff, size_t buff_size, bool isCenteredText)
4224 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
4226 if (buff != NULL) {
4227 const char *message = NULL;
4228 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
4229 // We might have up to 5 messages to show.
4230 const char *messages[5];
4231 unsigned messageCount = 0;
4232 const char *failsafeInfoMessage = NULL;
4233 const char *invertedInfoMessage = NULL;
4235 if (ARMING_FLAG(ARMED)) {
4236 if (FLIGHT_MODE(FAILSAFE_MODE) || FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding()) {
4237 if (isWaypointMissionRTHActive()) {
4238 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
4239 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL);
4241 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE) {
4242 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED);
4243 } else if (NAV_Status.state == MW_NAV_STATE_WP_ENROUTE) {
4244 // Countdown display for remaining Waypoints
4245 char buf[6];
4246 osdFormatDistanceSymbol(buf, posControl.wpDistance, 0);
4247 tfp_sprintf(messageBuf, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl.activeWaypointIndex), posControl.geoWaypointCount, buf);
4248 messages[messageCount++] = messageBuf;
4249 } else if (NAV_Status.state == MW_NAV_STATE_HOLD_TIMED) {
4250 // WP hold time countdown in seconds
4251 timeMs_t currentTime = millis();
4252 int holdTimeRemaining = posControl.waypointList[posControl.activeWaypointIndex].p1 - (int)((currentTime - posControl.wpReachedTime)/1000);
4253 if (holdTimeRemaining >=0) {
4254 tfp_sprintf(messageBuf, "HOLDING WP FOR %2u S", holdTimeRemaining);
4256 messages[messageCount++] = messageBuf;
4257 } else {
4258 const char *navStateMessage = navigationStateMessage();
4259 if (navStateMessage) {
4260 messages[messageCount++] = navStateMessage;
4263 #if defined(USE_SAFE_HOME)
4264 const char *safehomeMessage = divertingToSafehomeMessage();
4265 if (safehomeMessage) {
4266 messages[messageCount++] = safehomeMessage;
4268 #endif
4269 if (FLIGHT_MODE(FAILSAFE_MODE)) {
4270 // In FS mode while being armed too
4271 const char *failsafePhaseMessage = osdFailsafePhaseMessage();
4272 failsafeInfoMessage = osdFailsafeInfoMessage();
4274 if (failsafePhaseMessage) {
4275 messages[messageCount++] = failsafePhaseMessage;
4277 if (failsafeInfoMessage) {
4278 messages[messageCount++] = failsafeInfoMessage;
4281 } else { /* messages shown only when Failsafe, WP, RTH or Emergency Landing not active */
4282 if (STATE(FIXED_WING_LEGACY) && (navGetCurrentStateFlags() & NAV_CTL_LAUNCH)) {
4283 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH);
4284 const char *launchStateMessage = fixedWingLaunchStateMessage();
4285 if (launchStateMessage) {
4286 messages[messageCount++] = launchStateMessage;
4288 } else {
4289 if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && !navigationRequiresAngleMode()) {
4290 // ALTHOLD might be enabled alongside ANGLE/HORIZON/ACRO
4291 // when it doesn't require ANGLE mode (required only in FW
4292 // right now). If if requires ANGLE, its display is handled
4293 // by OSD_FLYMODE.
4294 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD);
4296 if (IS_RC_MODE_ACTIVE(BOXAUTOTRIM) && !feature(FEATURE_FW_AUTOTRIM)) {
4297 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM);
4299 if (IS_RC_MODE_ACTIVE(BOXAUTOTUNE)) {
4300 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE);
4301 if (FLIGHT_MODE(MANUAL_MODE)) {
4302 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO);
4305 if (FLIGHT_MODE(HEADFREE_MODE)) {
4306 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE);
4308 if (FLIGHT_MODE(SOARING_MODE)) {
4309 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING);
4311 if (posControl.flags.wpMissionPlannerActive) {
4312 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER);
4316 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS)) {
4317 unsigned invalidIndex;
4319 // Check if we're unable to arm for some reason
4320 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING) && !settingsValidate(&invalidIndex)) {
4322 const setting_t *setting = settingGet(invalidIndex);
4323 settingGetName(setting, messageBuf);
4324 for (int ii = 0; messageBuf[ii]; ii++) {
4325 messageBuf[ii] = sl_toupper(messageBuf[ii]);
4327 invertedInfoMessage = messageBuf;
4328 messages[messageCount++] = invertedInfoMessage;
4330 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
4331 messages[messageCount++] = invertedInfoMessage;
4333 } else {
4335 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM);
4336 messages[messageCount++] = invertedInfoMessage;
4338 // Show the reason for not arming
4339 messages[messageCount++] = osdArmingDisabledReasonMessage();
4342 } else if (!ARMING_FLAG(ARMED)) {
4343 if (isWaypointListValid()) {
4344 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED);
4348 if (messageCount > 0) {
4349 message = messages[OSD_ALTERNATING_CHOICES(1000, messageCount)];
4350 if (message == failsafeInfoMessage) {
4351 // failsafeInfoMessage is not useful for recovering
4352 // a lost model, but might help avoiding a crash.
4353 // Blink to grab user attention.
4354 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
4355 } else if (message == invertedInfoMessage) {
4356 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
4358 // We're shoing either failsafePhaseMessage or
4359 // navStateMessage. Don't BLINK here since
4360 // having this text available might be crucial
4361 // during a lost aircraft recovery and blinking
4362 // will cause it to be missing from some frames.
4365 osdFormatMessage(buff, buff_size, message, isCenteredText);
4367 return elemAttr;
4370 #endif // OSD