constrain fastA2I to [0-9] (vice [0-9A])
[inav.git] / src / main / io / osd.c
blob5056ead41443a419941dfdb2981f5dc3ca9410bc
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
118 #define VIDEO_BUFFER_CHARS_HD 900
120 #define GFORCE_FILTER_TC 0.2
122 #define DELAYED_REFRESH_RESUME_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
123 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
124 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
126 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
127 #define ARMED_SCREEN_DISPLAY_TIME 1500 // ms
128 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
130 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
132 // Adjust OSD_MESSAGE's default position when
133 // changing OSD_MESSAGE_LENGTH
134 #define OSD_MESSAGE_LENGTH 28
135 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
136 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
137 // Wrap all string constants intenteded for display as messages with
138 // this macro to ensure compile time length validation.
139 #define OSD_MESSAGE_STR(x) ({ \
140 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
141 x; \
144 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
146 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
147 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
149 #define OSD_MIN_FONT_VERSION 3
151 static unsigned currentLayout = 0;
152 static int layoutOverride = -1;
153 static bool hasExtendedFont = false; // Wether the font supports characters > 256
154 static timeMs_t layoutOverrideUntil = 0;
155 static pt1Filter_t GForceFilter, GForceFilterAxis[XYZ_AXIS_COUNT];
156 static float GForce, GForceAxis[XYZ_AXIS_COUNT];
158 typedef struct statistic_s {
159 uint16_t max_speed;
160 uint16_t max_3D_speed;
161 uint16_t max_air_speed;
162 uint16_t min_voltage; // /100
163 int16_t max_current;
164 int32_t max_power;
165 int16_t min_rssi;
166 int16_t min_lq; // for CRSF
167 int16_t min_rssi_dbm; // for CRSF
168 int32_t max_altitude;
169 uint32_t max_distance;
170 } statistic_t;
172 static statistic_t stats;
174 static timeUs_t resumeRefreshAt = 0;
175 static bool refreshWaitForResumeCmdRelease;
177 static bool fullRedraw = false;
179 static uint8_t armState;
180 static uint8_t statsPagesCheck = 0;
182 typedef struct osdMapData_s {
183 uint32_t scale;
184 char referenceSymbol;
185 } osdMapData_t;
187 static osdMapData_t osdMapData;
189 static displayPort_t *osdDisplayPort;
190 static bool osdDisplayIsReady = false;
191 #if defined(USE_CANVAS)
192 static displayCanvas_t osdCanvas;
193 static bool osdDisplayHasCanvas;
194 #else
195 #define osdDisplayHasCanvas false
196 #endif
198 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
200 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t, osdConfig, PG_OSD_CONFIG, 6);
201 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t, osdLayoutsConfig, PG_OSD_LAYOUTS_CONFIG, 1);
203 static int digitCount(int32_t value)
205 int digits = 1;
206 while(1) {
207 value = value / 10;
208 if (value == 0) {
209 break;
211 digits++;
213 return digits;
216 bool osdDisplayIsPAL(void)
218 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL;
221 bool osdDisplayIsHD(void)
223 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_HD;
227 * Formats a number given in cents, to support non integer values
228 * without using floating point math. Value is always right aligned
229 * and spaces are inserted before the number to always yield a string
230 * of the same length. If the value doesn't fit into the provided length
231 * it will be divided by scale and true will be returned.
233 bool osdFormatCentiNumber(char *buff, int32_t centivalue, uint32_t scale, int maxDecimals, int maxScaledDecimals, int length)
235 char *ptr = buff;
236 char *dec;
237 int decimals = maxDecimals;
238 bool negative = false;
239 bool scaled = false;
241 buff[length] = '\0';
243 if (centivalue < 0) {
244 negative = true;
245 centivalue = -centivalue;
246 length--;
249 int32_t integerPart = centivalue / 100;
250 // 3 decimal digits
251 int32_t millis = (centivalue % 100) * 10;
253 int digits = digitCount(integerPart);
254 int remaining = length - digits;
256 if (remaining < 0 && scale > 0) {
257 // Reduce by scale
258 scaled = true;
259 decimals = maxScaledDecimals;
260 integerPart = integerPart / scale;
261 // Multiply by 10 to get 3 decimal digits
262 millis = ((centivalue % (100 * scale)) * 10) / scale;
263 digits = digitCount(integerPart);
264 remaining = length - digits;
267 // 3 decimals at most
268 decimals = MIN(remaining, MIN(decimals, 3));
269 remaining -= decimals;
271 // Done counting. Time to write the characters.
273 // Write spaces at the start
274 while (remaining > 0) {
275 *ptr = SYM_BLANK;
276 ptr++;
277 remaining--;
280 // Write the minus sign if required
281 if (negative) {
282 *ptr = '-';
283 ptr++;
285 // Now write the digits.
286 ui2a(integerPart, 10, 0, ptr);
287 ptr += digits;
288 if (decimals > 0) {
289 *(ptr-1) += SYM_ZERO_HALF_TRAILING_DOT - '0';
290 dec = ptr;
291 int factor = 3; // we're getting the decimal part in millis first
292 while (decimals < factor) {
293 factor--;
294 millis /= 10;
296 int decimalDigits = digitCount(millis);
297 while (decimalDigits < decimals) {
298 decimalDigits++;
299 *ptr = '0';
300 ptr++;
302 ui2a(millis, 10, 0, ptr);
303 *dec += SYM_ZERO_HALF_LEADING_DOT - '0';
305 return scaled;
309 * Aligns text to the left side. Adds spaces at the end to keep string length unchanged.
311 static void osdLeftAlignString(char *buff)
313 uint8_t sp = 0, ch = 0;
314 uint8_t len = strlen(buff);
315 while (buff[sp] == ' ') sp++;
316 for (ch = 0; ch < (len - sp); ch++) buff[ch] = buff[ch + sp];
317 for (sp = ch; sp < len; sp++) buff[sp] = ' ';
321 * Converts distance into a string based on the current unit system
322 * prefixed by a a symbol to indicate the unit used.
323 * @param dist Distance in centimeters
325 static void osdFormatDistanceSymbol(char *buff, int32_t dist, uint8_t decimals)
327 switch ((osd_unit_e)osdConfig()->units) {
328 case OSD_UNIT_UK:
329 FALLTHROUGH;
330 case OSD_UNIT_IMPERIAL:
331 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_MILE, decimals, 3, 3)) {
332 buff[3] = SYM_DIST_MI;
333 } else {
334 buff[3] = SYM_DIST_FT;
336 buff[4] = '\0';
337 break;
338 case OSD_UNIT_METRIC_MPH:
339 FALLTHROUGH;
340 case OSD_UNIT_METRIC:
341 if (osdFormatCentiNumber(buff, dist, METERS_PER_KILOMETER, decimals, 3, 3)) {
342 buff[3] = SYM_DIST_KM;
343 } else {
344 buff[3] = SYM_DIST_M;
346 buff[4] = '\0';
347 break;
348 case OSD_UNIT_GA:
349 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_NAUTICALMILE, decimals, 3, 3)) {
350 buff[3] = SYM_DIST_NM;
351 } else {
352 buff[3] = SYM_DIST_FT;
354 buff[4] = '\0';
355 break;
360 * Converts distance into a string based on the current unit system.
361 * @param dist Distance in centimeters
363 static void osdFormatDistanceStr(char *buff, int32_t dist)
365 int32_t centifeet;
366 switch ((osd_unit_e)osdConfig()->units) {
367 case OSD_UNIT_UK:
368 FALLTHROUGH;
369 case OSD_UNIT_IMPERIAL:
370 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
371 if (abs(centifeet) < FEET_PER_MILE * 100 / 2) {
372 // Show feet when dist < 0.5mi
373 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
374 } else {
375 // Show miles when dist >= 0.5mi
376 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100*FEET_PER_MILE)),
377 (abs(centifeet) % (100 * FEET_PER_MILE)) / FEET_PER_MILE, SYM_MI);
379 break;
380 case OSD_UNIT_METRIC_MPH:
381 FALLTHROUGH;
382 case OSD_UNIT_METRIC:
383 if (abs(dist) < METERS_PER_KILOMETER * 100) {
384 // Show meters when dist < 1km
385 tfp_sprintf(buff, "%d%c", (int)(dist / 100), SYM_M);
386 } else {
387 // Show kilometers when dist >= 1km
388 tfp_sprintf(buff, "%d.%02d%c", (int)(dist / (100*METERS_PER_KILOMETER)),
389 (abs(dist) % (100 * METERS_PER_KILOMETER)) / METERS_PER_KILOMETER, SYM_KM);
391 break;
392 case OSD_UNIT_GA:
393 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
394 if (abs(centifeet) < 100000) {
395 // Show feet when dist < 1000ft
396 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
397 } else {
398 // Show nautical miles when dist >= 1000ft
399 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100 * FEET_PER_NAUTICALMILE)),
400 (int)((abs(centifeet) % (int)(100 * FEET_PER_NAUTICALMILE)) / FEET_PER_NAUTICALMILE), SYM_NM);
402 break;
407 * Converts velocity based on the current unit system (kmh or mph).
408 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
410 static int32_t osdConvertVelocityToUnit(int32_t vel)
412 switch ((osd_unit_e)osdConfig()->units) {
413 case OSD_UNIT_UK:
414 FALLTHROUGH;
415 case OSD_UNIT_METRIC_MPH:
416 FALLTHROUGH;
417 case OSD_UNIT_IMPERIAL:
418 return CMSEC_TO_CENTIMPH(vel) / 100; // Convert to mph
419 case OSD_UNIT_METRIC:
420 return CMSEC_TO_CENTIKPH(vel) / 100; // Convert to kmh
421 case OSD_UNIT_GA:
422 return CMSEC_TO_CENTIKNOTS(vel) / 100; // Convert to Knots
424 // Unreachable
425 return -1;
429 * Converts velocity into a string based on the current unit system.
430 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
431 * @param _3D is a 3D velocity
432 * @param _max is a maximum velocity
434 void osdFormatVelocityStr(char* buff, int32_t vel, bool _3D, bool _max)
436 switch ((osd_unit_e)osdConfig()->units) {
437 case OSD_UNIT_UK:
438 FALLTHROUGH;
439 case OSD_UNIT_METRIC_MPH:
440 FALLTHROUGH;
441 case OSD_UNIT_IMPERIAL:
442 if (_max) {
443 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
444 } else {
445 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
447 break;
448 case OSD_UNIT_METRIC:
449 if (_max) {
450 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
451 } else {
452 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
454 break;
455 case OSD_UNIT_GA:
456 if (_max) {
457 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
458 } else {
459 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
461 break;
466 * 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
468 static void osdGenerateAverageVelocityStr(char* buff) {
469 uint32_t cmPerSec = getTotalTravelDistance() / getFlightTime();
470 osdFormatVelocityStr(buff, cmPerSec, false, false);
474 * Converts wind speed into a string based on the current unit system, using
475 * always 3 digits and an additional character for the unit at the right. buff
476 * is null terminated.
477 * @param ws Raw wind speed in cm/s
479 #ifdef USE_WIND_ESTIMATOR
480 static void osdFormatWindSpeedStr(char *buff, int32_t ws, bool isValid)
482 int32_t centivalue;
483 char suffix;
484 switch (osdConfig()->units) {
485 case OSD_UNIT_UK:
486 FALLTHROUGH;
487 case OSD_UNIT_METRIC_MPH:
488 FALLTHROUGH;
489 case OSD_UNIT_IMPERIAL:
490 centivalue = CMSEC_TO_CENTIMPH(ws);
491 suffix = SYM_MPH;
492 break;
493 case OSD_UNIT_GA:
494 centivalue = CMSEC_TO_CENTIKNOTS(ws);
495 suffix = SYM_KT;
496 break;
497 default:
498 case OSD_UNIT_METRIC:
499 centivalue = CMSEC_TO_CENTIKPH(ws);
500 suffix = SYM_KMH;
501 break;
503 if (isValid) {
504 osdFormatCentiNumber(buff, centivalue, 0, 2, 0, 3);
505 } else {
506 buff[0] = buff[1] = buff[2] = '-';
508 buff[3] = suffix;
509 buff[4] = '\0';
511 #endif
514 * Converts altitude into a string based on the current unit system
515 * prefixed by a a symbol to indicate the unit used.
516 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
518 void osdFormatAltitudeSymbol(char *buff, int32_t alt)
520 int digits;
521 if (alt < 0) {
522 digits = 4;
523 } else {
524 digits = 3;
525 buff[0] = ' ';
527 switch ((osd_unit_e)osdConfig()->units) {
528 case OSD_UNIT_UK:
529 FALLTHROUGH;
530 case OSD_UNIT_GA:
531 FALLTHROUGH;
532 case OSD_UNIT_IMPERIAL:
533 if (osdFormatCentiNumber(buff + 4 - digits, CENTIMETERS_TO_CENTIFEET(alt), 1000, 0, 2, digits)) {
534 // Scaled to kft
535 buff[4] = SYM_ALT_KFT;
536 } else {
537 // Formatted in feet
538 buff[4] = SYM_ALT_FT;
540 buff[5] = '\0';
541 break;
542 case OSD_UNIT_METRIC_MPH:
543 FALLTHROUGH;
544 case OSD_UNIT_METRIC:
545 // alt is alredy in cm
546 if (osdFormatCentiNumber(buff + 4 - digits, alt, 1000, 0, 2, digits)) {
547 // Scaled to km
548 buff[4] = SYM_ALT_KM;
549 } else {
550 // Formatted in m
551 buff[4] = SYM_ALT_M;
553 buff[5] = '\0';
554 break;
559 * Converts altitude into a string based on the current unit system.
560 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
562 static void osdFormatAltitudeStr(char *buff, int32_t alt)
564 int32_t value;
565 switch ((osd_unit_e)osdConfig()->units) {
566 case OSD_UNIT_UK:
567 FALLTHROUGH;
568 case OSD_UNIT_GA:
569 FALLTHROUGH;
570 case OSD_UNIT_IMPERIAL:
571 value = CENTIMETERS_TO_FEET(alt);
572 tfp_sprintf(buff, "%d%c", (int)value, SYM_FT);
573 break;
574 case OSD_UNIT_METRIC_MPH:
575 FALLTHROUGH;
576 case OSD_UNIT_METRIC:
577 value = CENTIMETERS_TO_METERS(alt);
578 tfp_sprintf(buff, "%d%c", (int)value, SYM_M);
579 break;
583 static void osdFormatTime(char *buff, uint32_t seconds, char sym_m, char sym_h)
585 uint32_t value = seconds;
586 char sym = sym_m;
587 // Maximum value we can show in minutes is 99 minutes and 59 seconds
588 if (seconds > (99 * 60) + 59) {
589 sym = sym_h;
590 value = seconds / 60;
592 buff[0] = sym;
593 tfp_sprintf(buff + 1, "%02d:%02d", (int)(value / 60), (int)(value % 60));
596 static inline void osdFormatOnTime(char *buff)
598 osdFormatTime(buff, micros() / 1000000, SYM_ON_M, SYM_ON_H);
601 static inline void osdFormatFlyTime(char *buff, textAttributes_t *attr)
603 uint32_t seconds = getFlightTime();
604 osdFormatTime(buff, seconds, SYM_FLY_M, SYM_FLY_H);
605 if (attr && osdConfig()->time_alarm > 0) {
606 if (seconds / 60 >= osdConfig()->time_alarm && ARMING_FLAG(ARMED)) {
607 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
613 * Converts RSSI into a % value used by the OSD.
615 static uint16_t osdConvertRSSI(void)
617 // change range to [0, 99]
618 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE, 0, 99);
621 static uint16_t osdGetCrsfLQ(void)
623 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
624 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
625 int16_t displayedLQ;
626 switch (osdConfig()->crsf_lq_format) {
627 case OSD_CRSF_LQ_TYPE1:
628 displayedLQ = statsLQ;
629 break;
630 case OSD_CRSF_LQ_TYPE2:
631 displayedLQ = statsLQ;
632 break;
633 case OSD_CRSF_LQ_TYPE3:
634 displayedLQ = rxLinkStatistics.rfMode >= 2 ? scaledLQ : statsLQ;
635 break;
637 return displayedLQ;
640 static int16_t osdGetCrsfdBm(void)
642 return rxLinkStatistics.uplinkRSSI;
645 * Displays a temperature postfixed with a symbol depending on the current unit system
646 * @param label to display
647 * @param valid true if measurement is valid
648 * @param temperature in deciDegrees Celcius
650 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)
652 char buff[TEMPERATURE_LABEL_LEN + 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN + 2];
653 textAttributes_t elemAttr = valid ? TEXT_ATTRIBUTES_NONE : _TEXT_ATTRIBUTES_BLINK_BIT;
654 uint8_t valueXOffset = 0;
656 if (symbol) {
657 buff[0] = symbol;
658 buff[1] = '\0';
659 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
660 valueXOffset = 1;
662 #ifdef USE_TEMPERATURE_SENSOR
663 else if (label[0] != '\0') {
664 uint8_t label_len = strnlen(label, TEMPERATURE_LABEL_LEN);
665 memcpy(buff, label, label_len);
666 memset(buff + label_len, ' ', TEMPERATURE_LABEL_LEN + 1 - label_len);
667 buff[5] = '\0';
668 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
669 valueXOffset = osdConfig()->temp_label_align == OSD_ALIGN_LEFT ? 5 : label_len + 1;
671 #else
672 UNUSED(label);
673 #endif
675 if (valid) {
677 if ((temperature <= alarm_min) || (temperature >= alarm_max)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
678 if (osdConfig()->units == OSD_UNIT_IMPERIAL) temperature = temperature * 9 / 5.0f + 320;
679 tfp_sprintf(buff, "%3d", temperature / 10);
681 } else
682 strcpy(buff, "---");
684 buff[3] = osdConfig()->units == OSD_UNIT_IMPERIAL ? SYM_TEMP_F : SYM_TEMP_C;
685 buff[4] = '\0';
687 displayWriteWithAttr(osdDisplayPort, elemPosX + valueXOffset, elemPosY, buff, elemAttr);
690 #ifdef USE_TEMPERATURE_SENSOR
691 static void osdDisplayTemperatureSensor(uint8_t elemPosX, uint8_t elemPosY, uint8_t sensorIndex)
693 int16_t temperature;
694 const bool valid = getSensorTemperature(sensorIndex, &temperature);
695 const tempSensorConfig_t *sensorConfig = tempSensorConfig(sensorIndex);
696 uint16_t symbol = sensorConfig->osdSymbol ? SYM_TEMP_SENSOR_FIRST + sensorConfig->osdSymbol - 1 : 0;
697 osdDisplayTemperature(elemPosX, elemPosY, symbol, sensorConfig->label, valid, temperature, sensorConfig->alarm_min, sensorConfig->alarm_max);
699 #endif
701 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
703 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
704 const int coordinateLength = osdConfig()->coordinate_digits + 1;
706 buff[0] = sym;
707 int32_t integerPart = val / GPS_DEGREES_DIVIDER;
708 // Latitude maximum integer width is 3 (-90) while
709 // longitude maximum integer width is 4 (-180).
710 int integerDigits = tfp_sprintf(buff + 1, (integerPart == 0 && val < 0) ? "-%d" : "%d", (int)integerPart);
711 // We can show up to 7 digits in decimalPart.
712 int32_t decimalPart = abs(val % GPS_DEGREES_DIVIDER);
713 STATIC_ASSERT(GPS_DEGREES_DIVIDER == 1e7, adjust_max_decimal_digits);
714 int decimalDigits = tfp_sprintf(buff + 1 + integerDigits, "%07d", (int)decimalPart);
715 // Embbed the decimal separator
716 buff[1 + integerDigits - 1] += SYM_ZERO_HALF_TRAILING_DOT - '0';
717 buff[1 + integerDigits] += SYM_ZERO_HALF_LEADING_DOT - '0';
718 // Fill up to coordinateLength with zeros
719 int total = 1 + integerDigits + decimalDigits;
720 while(total < coordinateLength) {
721 buff[total] = '0';
722 total++;
724 buff[coordinateLength] = '\0';
727 static void osdFormatCraftName(char *buff)
729 if (strlen(systemConfig()->name) == 0)
730 strcpy(buff, "CRAFT_NAME");
731 else {
732 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
733 buff[i] = sl_toupper((unsigned char)systemConfig()->name[i]);
734 if (systemConfig()->name[i] == 0)
735 break;
740 static const char * osdArmingDisabledReasonMessage(void)
742 const char *message = NULL;
743 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
745 switch (isArmingDisabledReason()) {
746 case ARMING_DISABLED_FAILSAFE_SYSTEM:
747 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
748 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING) {
749 if (failsafeIsReceivingRxData()) {
750 // If we're not using sticks, it means the ARM switch
751 // hasn't been off since entering FAILSAFE_RX_LOSS_MONITORING
752 // yet
753 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF);
755 // Not receiving RX data
756 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
758 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS);
759 case ARMING_DISABLED_NOT_LEVEL:
760 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL);
761 case ARMING_DISABLED_SENSORS_CALIBRATING:
762 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL);
763 case ARMING_DISABLED_SYSTEM_OVERLOADED:
764 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED);
765 case ARMING_DISABLED_NAVIGATION_UNSAFE:
766 // Check the exact reason
767 switch (navigationIsBlockingArming(NULL)) {
768 char buf[6];
769 case NAV_ARMING_BLOCKER_NONE:
770 break;
771 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX:
772 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX);
773 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE:
774 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST);
775 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR:
776 osdFormatDistanceSymbol(buf, distanceToFirstWP(), 0);
777 tfp_sprintf(messageBuf, "FIRST WP TOO FAR (%s)", buf);
778 return message = messageBuf;
779 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR:
780 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG);
782 break;
783 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED:
784 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL);
785 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED:
786 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL);
787 case ARMING_DISABLED_ARM_SWITCH:
788 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST);
789 case ARMING_DISABLED_HARDWARE_FAILURE:
791 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
792 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE);
794 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
795 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL);
797 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
798 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL);
800 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
801 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL);
803 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
804 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL);
806 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
807 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL);
809 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
810 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL);
813 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL);
814 case ARMING_DISABLED_BOXFAILSAFE:
815 return OSD_MESSAGE_STR(OSD_MSG_FS_EN);
816 case ARMING_DISABLED_BOXKILLSWITCH:
817 return OSD_MESSAGE_STR(OSD_MSG_KILL_SW_EN);
818 case ARMING_DISABLED_RC_LINK:
819 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK);
820 case ARMING_DISABLED_THROTTLE:
821 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW);
822 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED:
823 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER);
824 case ARMING_DISABLED_SERVO_AUTOTRIM:
825 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE);
826 case ARMING_DISABLED_OOM:
827 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY);
828 case ARMING_DISABLED_INVALID_SETTING:
829 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
830 case ARMING_DISABLED_CLI:
831 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE);
832 case ARMING_DISABLED_PWM_OUTPUT_ERROR:
833 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR);
834 case ARMING_DISABLED_NO_PREARM:
835 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM);
836 case ARMING_DISABLED_DSHOT_BEEPER:
837 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER);
838 // Cases without message
839 case ARMING_DISABLED_LANDING_DETECTED:
840 FALLTHROUGH;
841 case ARMING_DISABLED_CMS_MENU:
842 FALLTHROUGH;
843 case ARMING_DISABLED_OSD_MENU:
844 FALLTHROUGH;
845 case ARMING_DISABLED_ALL_FLAGS:
846 FALLTHROUGH;
847 case ARMED:
848 FALLTHROUGH;
849 case WAS_EVER_ARMED:
850 break;
852 return NULL;
855 static const char * osdFailsafePhaseMessage(void)
857 // See failsafe.h for each phase explanation
858 switch (failsafePhase()) {
859 case FAILSAFE_RETURN_TO_HOME:
860 // XXX: Keep this in sync with OSD_FLYMODE.
861 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS);
862 case FAILSAFE_LANDING:
863 // This should be considered an emergengy landing
864 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS);
865 case FAILSAFE_RX_LOSS_MONITORING:
866 // Only reachable from FAILSAFE_LANDED, which performs
867 // a disarm. Since aircraft has been disarmed, we no
868 // longer show failsafe details.
869 FALLTHROUGH;
870 case FAILSAFE_LANDED:
871 // Very brief, disarms and transitions into
872 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
873 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
874 // so we'll show the user how to re-arm in when
875 // that flag is the reason to prevent arming.
876 FALLTHROUGH;
877 case FAILSAFE_RX_LOSS_IDLE:
878 // This only happens when user has chosen NONE as FS
879 // procedure. The recovery messages should be enough.
880 FALLTHROUGH;
881 case FAILSAFE_IDLE:
882 // Failsafe not active
883 FALLTHROUGH;
884 case FAILSAFE_RX_LOSS_DETECTED:
885 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
886 // or the FS procedure immediately.
887 FALLTHROUGH;
888 case FAILSAFE_RX_LOSS_RECOVERED:
889 // Exiting failsafe
890 break;
892 return NULL;
895 static const char * osdFailsafeInfoMessage(void)
897 if (failsafeIsReceivingRxData()) {
898 // User must move sticks to exit FS mode
899 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS);
901 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
903 #if defined(USE_SAFE_HOME)
904 static const char * divertingToSafehomeMessage(void)
906 if (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && safehome_applied) {
907 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME);
909 return NULL;
911 #endif
913 static const char * navigationStateMessage(void)
915 switch (NAV_Status.state) {
916 case MW_NAV_STATE_NONE:
917 break;
918 case MW_NAV_STATE_RTH_START:
919 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH);
920 case MW_NAV_STATE_RTH_CLIMB:
921 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB);
922 case MW_NAV_STATE_RTH_ENROUTE:
923 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME);
924 case MW_NAV_STATE_HOLD_INFINIT:
925 // Used by HOLD flight modes. No information to add.
926 break;
927 case MW_NAV_STATE_HOLD_TIMED:
928 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
929 break;
930 case MW_NAV_STATE_WP_ENROUTE:
931 // "TO WP" + WP countdown added in osdGetSystemMessage
932 break;
933 case MW_NAV_STATE_PROCESS_NEXT:
934 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP);
935 case MW_NAV_STATE_DO_JUMP:
936 // Not used
937 break;
938 case MW_NAV_STATE_LAND_START:
939 // Not used
940 break;
941 case MW_NAV_STATE_EMERGENCY_LANDING:
942 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING);
943 case MW_NAV_STATE_LAND_IN_PROGRESS:
944 return OSD_MESSAGE_STR(OSD_MSG_LANDING);
945 case MW_NAV_STATE_HOVER_ABOVE_HOME:
946 if (STATE(FIXED_WING_LEGACY)) {
947 #if defined(USE_SAFE_HOME)
948 if (safehome_applied) {
949 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME);
951 #endif
952 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME);
954 return OSD_MESSAGE_STR(OSD_MSG_HOVERING);
955 case MW_NAV_STATE_LANDED:
956 return OSD_MESSAGE_STR(OSD_MSG_LANDED);
957 case MW_NAV_STATE_LAND_SETTLE:
958 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND);
959 case MW_NAV_STATE_LAND_START_DESCENT:
960 // Not used
961 break;
963 return NULL;
966 static void osdFormatMessage(char *buff, size_t size, const char *message, bool isCenteredText)
968 // String is always filled with Blanks
969 memset(buff, SYM_BLANK, size);
970 if (message) {
971 size_t messageLength = strlen(message);
972 int rem = isCenteredText ? MAX(0, (int)size - (int)messageLength) : 0;
973 strncpy(buff + rem / 2, message, MIN((int)size - rem / 2, (int)messageLength));
975 // Ensure buff is zero terminated
976 buff[size] = '\0';
980 * Draws the battery symbol filled in accordingly to the
981 * battery voltage to buff[0].
983 static void osdFormatBatteryChargeSymbol(char *buff)
985 uint8_t p = calculateBatteryPercentage();
986 p = (100 - p) / 16.6;
987 buff[0] = SYM_BATT_FULL + p;
990 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t *attr)
992 if ((getBatteryState() != BATTERY_NOT_PRESENT) && ((batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical)) || ((!batteryUsesCapacityThresholds()) && (getBatteryVoltage() <= getBatteryWarningVoltage()))))
993 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
996 void osdCrosshairPosition(uint8_t *x, uint8_t *y)
998 *x = osdDisplayPort->cols / 2;
999 *y = osdDisplayPort->rows / 2;
1000 *y += osdConfig()->horizon_offset;
1004 * Formats throttle position prefixed by its symbol.
1005 * Shows output to motor, not stick position
1007 static void osdFormatThrottlePosition(char *buff, bool autoThr, textAttributes_t *elemAttr)
1009 buff[0] = SYM_BLANK;
1010 buff[1] = SYM_THR;
1011 if (autoThr && navigationIsControllingThrottle()) {
1012 buff[0] = SYM_AUTO_THR0;
1013 buff[1] = SYM_AUTO_THR1;
1014 if (isFixedWingAutoThrottleManuallyIncreased()) {
1015 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1018 #ifdef USE_POWER_LIMITS
1019 if (powerLimiterIsLimiting()) {
1020 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1022 #endif
1023 tfp_sprintf(buff + 2, "%3d", getThrottlePercent());
1027 * Formats gvars prefixed by its number (0-indexed). If autoThr
1029 static void osdFormatGVar(char *buff, uint8_t index)
1031 buff[0] = 'G';
1032 buff[1] = '0'+index;
1033 buff[2] = ':';
1034 #ifdef USE_PROGRAMMING_FRAMEWORK
1035 osdFormatCentiNumber(buff + 3, (int32_t)gvGet(index)*(int32_t)100, 1, 0, 0, 5);
1036 #endif
1039 #if defined(USE_ESC_SENSOR)
1040 static void osdFormatRpm(char *buff, uint32_t rpm)
1042 buff[0] = SYM_RPM;
1043 if (rpm) {
1044 if ( digitCount(rpm) > osdConfig()->esc_rpm_precision) {
1045 uint8_t rpmMaxDecimals = (osdConfig()->esc_rpm_precision - 3);
1046 osdFormatCentiNumber(buff + 1, rpm / 10, 0, rpmMaxDecimals, rpmMaxDecimals, osdConfig()->esc_rpm_precision-1);
1047 buff[osdConfig()->esc_rpm_precision] = 'K';
1048 buff[osdConfig()->esc_rpm_precision+1] = '\0';
1050 else {
1051 switch(osdConfig()->esc_rpm_precision) {
1052 case 6:
1053 tfp_sprintf(buff + 1, "%6lu", rpm);
1054 break;
1055 case 5:
1056 tfp_sprintf(buff + 1, "%5lu", rpm);
1057 break;
1058 case 4:
1059 tfp_sprintf(buff + 1, "%4lu", rpm);
1060 break;
1061 case 3:
1062 default:
1063 tfp_sprintf(buff + 1, "%3lu", rpm);
1064 break;
1070 else {
1071 uint8_t buffPos = 1;
1072 while (buffPos <=( osdConfig()->esc_rpm_precision)) {
1073 strcpy(buff + buffPos++, "-");
1077 #endif
1079 int32_t osdGetAltitude(void)
1081 return getEstimatedActualPosition(Z);
1084 static inline int32_t osdGetAltitudeMsl(void)
1086 return getEstimatedActualPosition(Z)+GPS_home.alt;
1089 static bool osdIsHeadingValid(void)
1091 #ifdef USE_SECONDARY_IMU
1092 if (secondaryImuState.active && secondaryImuConfig()->useForOsdHeading) {
1093 return true;
1094 } else {
1095 return isImuHeadingValid();
1097 #else
1098 return isImuHeadingValid();
1099 #endif
1102 int16_t osdGetHeading(void)
1104 #ifdef USE_SECONDARY_IMU
1105 if (secondaryImuState.active && secondaryImuConfig()->useForOsdHeading) {
1106 return secondaryImuState.eulerAngles.values.yaw;
1107 } else {
1108 return attitude.values.yaw;
1110 #else
1111 return attitude.values.yaw;
1112 #endif
1115 int16_t osdPanServoHomeDirectionOffset(void)
1117 int8_t servoIndex = osdConfig()->pan_servo_index;
1118 int16_t servoPosition = servo[servoIndex];
1119 int16_t servoMiddle = servoParams(servoIndex)->middle;
1120 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition - servoMiddle) * osdConfig()->pan_servo_pwm2centideg);
1123 // Returns a heading angle in degrees normalized to [0, 360).
1124 int osdGetHeadingAngle(int angle)
1126 while (angle < 0) {
1127 angle += 360;
1129 while (angle >= 360) {
1130 angle -= 360;
1132 return angle;
1135 #if defined(USE_GPS)
1137 /* Draws a map with the given symbol in the center and given point of interest
1138 * defined by its distance in meters and direction in degrees.
1139 * referenceHeading indicates the up direction in the map, in degrees, while
1140 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1141 * arrow to indicate the map reference to the user. The drawn argument is an
1142 * in-out used to store the last position where the craft was drawn to avoid
1143 * erasing all screen on each redraw.
1145 static void osdDrawMap(int referenceHeading, uint16_t referenceSym, uint16_t centerSym,
1146 uint32_t poiDistance, int16_t poiDirection, uint16_t poiSymbol,
1147 uint16_t *drawn, uint32_t *usedScale)
1149 // TODO: These need to be tested with several setups. We might
1150 // need to make them configurable.
1151 const int hMargin = 5;
1152 const int vMargin = 3;
1154 // TODO: Get this from the display driver?
1155 const int charWidth = 12;
1156 const int charHeight = 18;
1158 uint8_t minX = hMargin;
1159 uint8_t maxX = osdDisplayPort->cols - 1 - hMargin;
1160 uint8_t minY = vMargin;
1161 uint8_t maxY = osdDisplayPort->rows - 1 - vMargin;
1162 uint8_t midX = osdDisplayPort->cols / 2;
1163 uint8_t midY = osdDisplayPort->rows / 2;
1165 // Fixed marks
1166 displayWriteChar(osdDisplayPort, midX, midY, centerSym);
1168 // First, erase the previous drawing.
1169 if (OSD_VISIBLE(*drawn)) {
1170 displayWriteChar(osdDisplayPort, OSD_X(*drawn), OSD_Y(*drawn), SYM_BLANK);
1171 *drawn = 0;
1174 uint32_t initialScale;
1175 const unsigned scaleMultiplier = 2;
1176 // We try to reduce the scale when the POI will be around half the distance
1177 // between the center and the closers map edge, to avoid too much jumping
1178 const int scaleReductionMultiplier = MIN(midX - hMargin, midY - vMargin) / 2;
1180 switch (osdConfig()->units) {
1181 case OSD_UNIT_UK:
1182 FALLTHROUGH;
1183 case OSD_UNIT_IMPERIAL:
1184 initialScale = 16; // 16m ~= 0.01miles
1185 break;
1186 case OSD_UNIT_GA:
1187 initialScale = 18; // 18m ~= 0.01 nautical miles
1188 break;
1189 default:
1190 case OSD_UNIT_METRIC_MPH:
1191 FALLTHROUGH;
1192 case OSD_UNIT_METRIC:
1193 initialScale = 10; // 10m as initial scale
1194 break;
1197 // Try to keep the same scale when getting closer until we draw over the center point
1198 uint32_t scale = initialScale;
1199 if (*usedScale) {
1200 scale = *usedScale;
1201 if (scale > initialScale && poiDistance < *usedScale * scaleReductionMultiplier) {
1202 scale /= scaleMultiplier;
1206 if (STATE(GPS_FIX)) {
1208 int directionToPoi = osdGetHeadingAngle(poiDirection - referenceHeading);
1209 float poiAngle = DEGREES_TO_RADIANS(directionToPoi);
1210 float poiSin = sin_approx(poiAngle);
1211 float poiCos = cos_approx(poiAngle);
1213 // Now start looking for a valid scale that lets us draw everything
1214 int ii;
1215 for (ii = 0; ii < 50; ii++) {
1216 // Calculate location of the aircraft in map
1217 int points = poiDistance / ((float)scale / charHeight);
1219 float pointsX = points * poiSin;
1220 int poiX = midX - roundf(pointsX / charWidth);
1221 if (poiX < minX || poiX > maxX) {
1222 scale *= scaleMultiplier;
1223 continue;
1226 float pointsY = points * poiCos;
1227 int poiY = midY + roundf(pointsY / charHeight);
1228 if (poiY < minY || poiY > maxY) {
1229 scale *= scaleMultiplier;
1230 continue;
1233 if (poiX == midX && poiY == midY) {
1234 // We're over the map center symbol, so we would be drawing
1235 // over it even if we increased the scale. Alternate between
1236 // drawing the center symbol or drawing the POI.
1237 if (centerSym != SYM_BLANK && OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1238 break;
1240 } else {
1242 uint16_t c;
1243 if (displayReadCharWithAttr(osdDisplayPort, poiX, poiY, &c, NULL) && c != SYM_BLANK) {
1244 // Something else written here, increase scale. If the display doesn't support reading
1245 // back characters, we assume there's nothing.
1247 // If we're close to the center, decrease scale. Otherwise increase it.
1248 uint8_t centerDeltaX = (maxX - minX) / (scaleMultiplier * 2);
1249 uint8_t centerDeltaY = (maxY - minY) / (scaleMultiplier * 2);
1250 if (poiX >= midX - centerDeltaX && poiX <= midX + centerDeltaX &&
1251 poiY >= midY - centerDeltaY && poiY <= midY + centerDeltaY &&
1252 scale > scaleMultiplier) {
1254 scale /= scaleMultiplier;
1255 } else {
1256 scale *= scaleMultiplier;
1258 continue;
1262 // Draw the point on the map
1263 if (poiSymbol == SYM_ARROW_UP) {
1264 // Drawing aircraft, rotate
1265 int mapHeading = osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading);
1266 poiSymbol += mapHeading * 2 / 45;
1268 displayWriteChar(osdDisplayPort, poiX, poiY, poiSymbol);
1270 // Update saved location
1271 *drawn = OSD_POS(poiX, poiY) | OSD_VISIBLE_FLAG;
1272 break;
1276 *usedScale = scale;
1278 // Update global map data for scale and reference
1279 osdMapData.scale = scale;
1280 osdMapData.referenceSymbol = referenceSym;
1283 /* Draws a map with the home in the center and the craft moving around.
1284 * See osdDrawMap() for reference.
1286 static void osdDrawHomeMap(int referenceHeading, uint8_t referenceSym, uint16_t *drawn, uint32_t *usedScale)
1288 osdDrawMap(referenceHeading, referenceSym, SYM_HOME, GPS_distanceToHome, GPS_directionToHome, SYM_ARROW_UP, drawn, usedScale);
1291 /* Draws a map with the aircraft in the center and the home moving around.
1292 * See osdDrawMap() for reference.
1294 static void osdDrawRadar(uint16_t *drawn, uint32_t *usedScale)
1296 int16_t reference = DECIDEGREES_TO_DEGREES(osdGetHeading());
1297 int16_t poiDirection = osdGetHeadingAngle(GPS_directionToHome + 180);
1298 osdDrawMap(reference, 0, SYM_ARROW_UP, GPS_distanceToHome, poiDirection, SYM_HOME, drawn, usedScale);
1301 static uint16_t crc_accumulate(uint8_t data, uint16_t crcAccum)
1303 uint8_t tmp;
1304 tmp = data ^ (uint8_t)(crcAccum & 0xff);
1305 tmp ^= (tmp << 4);
1306 crcAccum = (crcAccum >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
1307 return crcAccum;
1311 static void osdDisplayTelemetry(void)
1313 uint32_t trk_data;
1314 uint16_t trk_crc = 0;
1315 char trk_buffer[31];
1316 static int16_t trk_elevation = 127;
1317 static uint16_t trk_bearing = 0;
1319 if (ARMING_FLAG(ARMED)) {
1320 if (STATE(GPS_FIX)){
1321 if (GPS_distanceToHome > 5) {
1322 trk_bearing = GPS_directionToHome;
1323 trk_bearing += 360 + 180;
1324 trk_bearing %= 360;
1325 int32_t alt = CENTIMETERS_TO_METERS(osdGetAltitude());
1326 float at = atan2(alt, GPS_distanceToHome);
1327 trk_elevation = (float)at * 57.2957795; // 57.2957795 = 1 rad
1328 trk_elevation += 37; // because elevation in telemetry should be from -37 to 90
1329 if (trk_elevation < 0) {
1330 trk_elevation = 0;
1335 else{
1336 trk_elevation = 127;
1337 trk_bearing = 0;
1340 trk_data = 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1341 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.
1342 trk_data = trk_data | (uint32_t)trk_bearing << 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1343 trk_crc = crc_accumulate(0xFF & trk_data, trk_crc); // CRC First Byte bits 0-7
1344 trk_crc = crc_accumulate(0xFF & trk_bearing, trk_crc); // CRC Second Byte bits 8-15
1345 trk_crc = crc_accumulate(trk_bearing >> 8, trk_crc); // CRC Third Byte bits 16-17
1346 trk_data = trk_data | (uint32_t)trk_crc << 17; // bits 18-29 CRC & 0x3FFFF
1348 for (uint8_t t_ctr = 0; t_ctr < 30; t_ctr++) { // Prepare screen buffer and write data line.
1349 if (trk_data & (uint32_t)1 << t_ctr){
1350 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_0;
1352 else{
1353 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_1;
1356 trk_buffer[30] = 0;
1357 displayWrite(osdDisplayPort, 0, 0, trk_buffer);
1358 if (osdConfig()->telemetry>1){
1359 displayWrite(osdDisplayPort, 0, 3, trk_buffer); // Test display because normal telemetry line is not visible
1362 #endif
1364 static void osdFormatPidControllerOutput(char *buff, const char *label, const pidController_t *pidController, uint8_t scale, bool showDecimal) {
1365 strcpy(buff, label);
1366 for (uint8_t i = strlen(label); i < 5; ++i) buff[i] = ' ';
1367 uint8_t decimals = showDecimal ? 1 : 0;
1368 osdFormatCentiNumber(buff + 5, pidController->proportional * scale, 0, decimals, 0, 4);
1369 buff[9] = ' ';
1370 osdFormatCentiNumber(buff + 10, pidController->integrator * scale, 0, decimals, 0, 4);
1371 buff[14] = ' ';
1372 osdFormatCentiNumber(buff + 15, pidController->derivative * scale, 0, decimals, 0, 4);
1373 buff[19] = ' ';
1374 osdFormatCentiNumber(buff + 20, pidController->output_constrained * scale, 0, decimals, 0, 4);
1375 buff[24] = '\0';
1378 static void osdDisplayBatteryVoltage(uint8_t elemPosX, uint8_t elemPosY, uint16_t voltage, uint8_t digits, uint8_t decimals)
1380 char buff[6];
1381 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1383 osdFormatBatteryChargeSymbol(buff);
1384 buff[1] = '\0';
1385 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1386 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1388 elemAttr = TEXT_ATTRIBUTES_NONE;
1389 digits = MIN(digits, 4);
1390 osdFormatCentiNumber(buff, voltage, 0, decimals, 0, digits);
1391 buff[digits] = SYM_VOLT;
1392 buff[digits+1] = '\0';
1393 if ((getBatteryState() != BATTERY_NOT_PRESENT) && (getBatteryVoltage() <= getBatteryWarningVoltage()))
1394 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1395 displayWriteWithAttr(osdDisplayPort, elemPosX + 1, elemPosY, buff, elemAttr);
1398 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)
1400 textAttributes_t elemAttr;
1401 char buff[4];
1403 const pid8_t *pid = &pidBank()->pid[pidIndex];
1404 pidType_e pidType = pidIndexGetType(pidIndex);
1406 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1408 if (pidType == PID_TYPE_NONE) {
1409 // PID is not used in this configuration. Draw dashes.
1410 // XXX: Keep this in sync with the %3d format and spacing used below
1411 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - - -");
1412 return;
1415 elemAttr = TEXT_ATTRIBUTES_NONE;
1416 tfp_sprintf(buff, "%3d", pid->P);
1417 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1418 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1419 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1421 elemAttr = TEXT_ATTRIBUTES_NONE;
1422 tfp_sprintf(buff, "%3d", pid->I);
1423 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1424 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1425 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1427 elemAttr = TEXT_ATTRIBUTES_NONE;
1428 tfp_sprintf(buff, "%3d", pid->D);
1429 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1430 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1431 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1433 elemAttr = TEXT_ATTRIBUTES_NONE;
1434 tfp_sprintf(buff, "%3d", pid->FF);
1435 if ((isAdjustmentFunctionSelected(adjFuncFF)) || (((adjFuncFF == ADJUSTMENT_ROLL_FF) || (adjFuncFF == ADJUSTMENT_PITCH_FF)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF))))
1436 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1437 displayWriteWithAttr(osdDisplayPort, elemPosX + 16, elemPosY, buff, elemAttr);
1440 static void osdDisplayNavPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD)
1442 textAttributes_t elemAttr;
1443 char buff[4];
1445 const pid8_t *pid = &pidBank()->pid[pidIndex];
1446 pidType_e pidType = pidIndexGetType(pidIndex);
1448 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1450 if (pidType == PID_TYPE_NONE) {
1451 // PID is not used in this configuration. Draw dashes.
1452 // XXX: Keep this in sync with the %3d format and spacing used below
1453 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - -");
1454 return;
1457 elemAttr = TEXT_ATTRIBUTES_NONE;
1458 tfp_sprintf(buff, "%3d", pid->P);
1459 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1460 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1461 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1463 elemAttr = TEXT_ATTRIBUTES_NONE;
1464 tfp_sprintf(buff, "%3d", pid->I);
1465 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1466 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1467 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1469 elemAttr = TEXT_ATTRIBUTES_NONE;
1470 tfp_sprintf(buff, "%3d", pidType == PID_TYPE_PIFF ? pid->FF : pid->D);
1471 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1472 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1473 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1476 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) {
1477 char buff[8];
1478 textAttributes_t elemAttr;
1479 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1481 elemAttr = TEXT_ATTRIBUTES_NONE;
1482 osdFormatCentiNumber(buff, value * 100, 0, maxDecimals, 0, MIN(valueLength, 8));
1483 if (isAdjustmentFunctionSelected(adjFunc))
1484 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1485 displayWriteWithAttr(osdDisplayPort, elemPosX + strlen(str) + 1 + valueOffset, elemPosY, buff, elemAttr);
1488 int8_t getGeoWaypointNumber(int8_t waypointIndex)
1490 static int8_t lastWaypointIndex = 1;
1491 static int8_t geoWaypointIndex;
1493 if (waypointIndex != lastWaypointIndex) {
1494 lastWaypointIndex = geoWaypointIndex = waypointIndex;
1495 for (uint8_t i = 0; i <= waypointIndex; i++) {
1496 if (posControl.waypointList[i].action == NAV_WP_ACTION_SET_POI ||
1497 posControl.waypointList[i].action == NAV_WP_ACTION_SET_HEAD ||
1498 posControl.waypointList[i].action == NAV_WP_ACTION_JUMP) {
1499 geoWaypointIndex -= 1;
1504 return geoWaypointIndex + 1;
1507 void osdDisplaySwitchIndicator(const char *swName, int rcValue, char *buff) {
1508 int8_t ptr = 0;
1510 if (osdConfig()->osd_switch_indicators_align_left) {
1511 for (ptr = 0; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH); ptr++) {
1512 buff[ptr] = swName[ptr];
1515 if ( rcValue < 1333) {
1516 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1517 } else if ( rcValue > 1666) {
1518 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1519 } else {
1520 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1522 } else {
1523 if ( rcValue < 1333) {
1524 buff[ptr++] = SYM_SWITCH_INDICATOR_LOW;
1525 } else if ( rcValue > 1666) {
1526 buff[ptr++] = SYM_SWITCH_INDICATOR_HIGH;
1527 } else {
1528 buff[ptr++] = SYM_SWITCH_INDICATOR_MID;
1531 for (ptr = 1; ptr < constrain(strlen(swName), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH) + 1; ptr++) {
1532 buff[ptr] = swName[ptr-1];
1535 ptr++;
1538 buff[ptr] = '\0';
1541 static bool osdDrawSingleElement(uint8_t item)
1543 uint16_t pos = osdLayoutsConfig()->item_pos[currentLayout][item];
1544 if (!OSD_VISIBLE(pos)) {
1545 return false;
1547 uint8_t elemPosX = OSD_X(pos);
1548 uint8_t elemPosY = OSD_Y(pos);
1549 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1550 char buff[32] = {0};
1552 switch (item) {
1553 case OSD_RSSI_VALUE:
1555 uint16_t osdRssi = osdConvertRSSI();
1556 buff[0] = SYM_RSSI;
1557 tfp_sprintf(buff + 1, "%2d", osdRssi);
1558 if (osdRssi < osdConfig()->rssi_alarm) {
1559 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1561 break;
1564 case OSD_MAIN_BATT_VOLTAGE:
1565 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawVoltage(), 2 + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1566 return true;
1568 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE:
1569 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedVoltage(), 2 + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1570 return true;
1572 case OSD_CURRENT_DRAW:
1573 osdFormatCentiNumber(buff, getAmperage(), 0, 2, 0, 3);
1574 buff[3] = SYM_AMP;
1575 buff[4] = '\0';
1577 uint8_t current_alarm = osdConfig()->current_alarm;
1578 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
1579 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1581 break;
1583 case OSD_MAH_DRAWN: {
1584 if (osdFormatCentiNumber(buff, getMAhDrawn() * 100, 1000, 0, (osdConfig()->mAh_used_precision - 2), osdConfig()->mAh_used_precision)) {
1585 // Shown in mAh
1586 buff[osdConfig()->mAh_used_precision] = SYM_AH;
1587 } else {
1588 // Shown in Ah
1589 buff[osdConfig()->mAh_used_precision] = SYM_MAH;
1591 buff[(osdConfig()->mAh_used_precision + 1)] = '\0';
1592 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1593 break;
1595 case OSD_WH_DRAWN:
1596 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
1597 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1598 buff[3] = SYM_WH;
1599 buff[4] = '\0';
1600 break;
1602 case OSD_BATTERY_REMAINING_CAPACITY:
1604 if (currentBatteryProfile->capacity.value == 0)
1605 tfp_sprintf(buff, " NA");
1606 else if (!batteryWasFullWhenPluggedIn())
1607 tfp_sprintf(buff, " NF");
1608 else if (currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH)
1609 tfp_sprintf(buff, "%4lu", getBatteryRemainingCapacity());
1610 else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1611 osdFormatCentiNumber(buff + 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3);
1613 buff[4] = currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH ? SYM_MAH : SYM_WH;
1614 buff[5] = '\0';
1616 if ((getBatteryState() != BATTERY_NOT_PRESENT) && batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical))
1617 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1619 break;
1621 case OSD_BATTERY_REMAINING_PERCENT:
1622 osdFormatBatteryChargeSymbol(buff);
1623 tfp_sprintf(buff + 1, "%3d%%", calculateBatteryPercentage());
1624 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1625 break;
1627 case OSD_POWER_SUPPLY_IMPEDANCE:
1628 if (isPowerSupplyImpedanceValid())
1629 tfp_sprintf(buff, "%3d", getPowerSupplyImpedance());
1630 else
1631 strcpy(buff, "---");
1632 buff[3] = SYM_MILLIOHM;
1633 buff[4] = '\0';
1634 break;
1636 #ifdef USE_GPS
1637 case OSD_GPS_SATS:
1638 buff[0] = SYM_SAT_L;
1639 buff[1] = SYM_SAT_R;
1640 tfp_sprintf(buff + 2, "%2d", gpsSol.numSat);
1641 if (!STATE(GPS_FIX)) {
1642 if (getHwGPSStatus() == HW_SENSOR_UNAVAILABLE || getHwGPSStatus() == HW_SENSOR_UNHEALTHY) {
1643 strcpy(buff + 2, "X!");
1645 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1647 break;
1649 case OSD_GPS_SPEED:
1650 osdFormatVelocityStr(buff, gpsSol.groundSpeed, false, false);
1651 break;
1653 case OSD_GPS_MAX_SPEED:
1654 osdFormatVelocityStr(buff, stats.max_speed, false, true);
1655 break;
1657 case OSD_3D_SPEED:
1658 osdFormatVelocityStr(buff, osdGet3DSpeed(), true, false);
1659 break;
1661 case OSD_3D_MAX_SPEED:
1662 osdFormatVelocityStr(buff, stats.max_3D_speed, true, true);
1663 break;
1665 case OSD_GLIDESLOPE:
1667 float horizontalSpeed = gpsSol.groundSpeed;
1668 float sinkRate = -getEstimatedActualVelocity(Z);
1669 static pt1Filter_t gsFilterState;
1670 const timeMs_t currentTimeMs = millis();
1671 static timeMs_t gsUpdatedTimeMs;
1672 float glideSlope = horizontalSpeed / sinkRate;
1673 glideSlope = pt1FilterApply4(&gsFilterState, isnormal(glideSlope) ? glideSlope : 200, 0.5, MS2S(currentTimeMs - gsUpdatedTimeMs));
1674 gsUpdatedTimeMs = currentTimeMs;
1676 buff[0] = SYM_GLIDESLOPE;
1677 if (glideSlope > 0.0f && glideSlope < 100.0f) {
1678 osdFormatCentiNumber(buff + 1, glideSlope * 100.0f, 0, 2, 0, 3);
1679 } else {
1680 buff[1] = buff[2] = buff[3] = '-';
1682 buff[4] = '\0';
1683 break;
1686 case OSD_GPS_LAT:
1687 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
1688 break;
1690 case OSD_GPS_LON:
1691 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
1692 break;
1694 case OSD_HOME_DIR:
1696 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
1697 if (GPS_distanceToHome < (navConfig()->general.min_rth_distance / 100) ) {
1698 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_HOME_NEAR);
1700 else
1702 int16_t panHomeDirOffset = 0;
1703 if (!(osdConfig()->pan_servo_pwm2centideg == 0)){
1704 panHomeDirOffset = osdPanServoHomeDirectionOffset();
1706 int homeDirection = GPS_directionToHome - DECIDEGREES_TO_DEGREES(osdGetHeading()) + panHomeDirOffset;
1707 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), homeDirection);
1709 } else {
1710 // No home or no fix or unknown heading, blink.
1711 // If we're unarmed, show the arrow pointing up so users can see the arrow
1712 // while configuring the OSD. If we're armed, show a '-' indicating that
1713 // we don't know the direction to home.
1714 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1715 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, ARMING_FLAG(ARMED) ? '-' : SYM_ARROW_UP, elemAttr);
1717 return true;
1720 case OSD_HOME_HEADING_ERROR:
1722 buff[0] = SYM_HOME;
1723 buff[1] = SYM_HEADING;
1725 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1726 int16_t h = lrintf(CENTIDEGREES_TO_DEGREES((float)wrap_18000(DEGREES_TO_CENTIDEGREES((int32_t)GPS_directionToHome) - DECIDEGREES_TO_CENTIDEGREES((int32_t)osdGetHeading()))));
1727 tfp_sprintf(buff + 2, "%4d", h);
1728 } else {
1729 strcpy(buff + 2, "----");
1732 buff[6] = SYM_DEGREES;
1733 buff[7] = '\0';
1734 break;
1737 case OSD_HOME_DIST:
1739 buff[0] = SYM_HOME;
1740 osdFormatDistanceSymbol(&buff[1], GPS_distanceToHome * 100, 0);
1741 uint16_t dist_alarm = osdConfig()->dist_alarm;
1742 if (dist_alarm > 0 && GPS_distanceToHome > dist_alarm) {
1743 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1746 break;
1748 case OSD_TRIP_DIST:
1749 buff[0] = SYM_TOTAL;
1750 osdFormatDistanceSymbol(buff + 1, getTotalTravelDistance(), 0);
1751 break;
1753 case OSD_HEADING:
1755 buff[0] = SYM_HEADING;
1756 if (osdIsHeadingValid()) {
1757 int16_t h = DECIDEGREES_TO_DEGREES(osdGetHeading());
1758 if (h < 0) {
1759 h += 360;
1761 tfp_sprintf(&buff[1], "%3d", h);
1762 } else {
1763 buff[1] = buff[2] = buff[3] = '-';
1765 buff[4] = SYM_DEGREES;
1766 buff[5] = '\0';
1767 break;
1770 case OSD_COURSE_HOLD_ERROR:
1772 if (ARMING_FLAG(ARMED) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1773 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1774 return true;
1777 buff[0] = SYM_HEADING;
1779 if ((!ARMING_FLAG(ARMED)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && isAdjustingPosition())) {
1780 buff[1] = buff[2] = buff[3] = '-';
1781 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1782 int16_t herr = lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
1783 if (ABS(herr) > 99)
1784 strcpy(buff + 1, ">99");
1785 else
1786 tfp_sprintf(buff + 1, "%3d", herr);
1789 buff[4] = SYM_DEGREES;
1790 buff[5] = '\0';
1791 break;
1794 case OSD_COURSE_HOLD_ADJUSTMENT:
1796 int16_t heading_adjust = lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
1798 if (ARMING_FLAG(ARMED) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust != 0)))) {
1799 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1800 return true;
1803 buff[0] = SYM_HEADING;
1805 if (!ARMING_FLAG(ARMED)) {
1806 buff[1] = buff[2] = buff[3] = buff[4] = '-';
1807 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1808 tfp_sprintf(buff + 1, "%4d", heading_adjust);
1811 buff[5] = SYM_DEGREES;
1812 buff[6] = '\0';
1813 break;
1816 case OSD_GPS_HDOP:
1818 buff[0] = SYM_HDP_L;
1819 buff[1] = SYM_HDP_R;
1820 int32_t centiHDOP = 100 * gpsSol.hdop / HDOP_SCALE;
1821 osdFormatCentiNumber(&buff[2], centiHDOP, 0, 1, 0, 2);
1822 break;
1825 case OSD_MAP_NORTH:
1827 static uint16_t drawn = 0;
1828 static uint32_t scale = 0;
1829 osdDrawHomeMap(0, 'N', &drawn, &scale);
1830 return true;
1832 case OSD_MAP_TAKEOFF:
1834 static uint16_t drawn = 0;
1835 static uint32_t scale = 0;
1836 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn, &scale);
1837 return true;
1839 case OSD_RADAR:
1841 static uint16_t drawn = 0;
1842 static uint32_t scale = 0;
1843 osdDrawRadar(&drawn, &scale);
1844 return true;
1846 #endif // GPS
1848 case OSD_ALTITUDE:
1850 int32_t alt = osdGetAltitude();
1851 osdFormatAltitudeSymbol(buff, alt);
1852 uint16_t alt_alarm = osdConfig()->alt_alarm;
1853 uint16_t neg_alt_alarm = osdConfig()->neg_alt_alarm;
1854 if ((alt_alarm > 0 && CENTIMETERS_TO_METERS(alt) > alt_alarm) ||
1855 (neg_alt_alarm > 0 && alt < 0 && -CENTIMETERS_TO_METERS(alt) > neg_alt_alarm)) {
1857 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1859 break;
1862 case OSD_ALTITUDE_MSL:
1864 int32_t alt = osdGetAltitudeMsl();
1865 osdFormatAltitudeSymbol(buff, alt);
1866 break;
1869 #ifdef USE_RANGEFINDER
1870 case OSD_RANGEFINDER:
1872 int32_t range = rangefinderGetLatestRawAltitude();
1873 if (range < 0) {
1874 buff[0] = '-';
1875 } else {
1876 osdFormatDistanceSymbol(buff, range, 1);
1879 break;
1880 #endif
1882 case OSD_ONTIME:
1884 osdFormatOnTime(buff);
1885 break;
1888 case OSD_FLYTIME:
1890 osdFormatFlyTime(buff, &elemAttr);
1891 break;
1894 case OSD_ONTIME_FLYTIME:
1896 if (ARMING_FLAG(ARMED)) {
1897 osdFormatFlyTime(buff, &elemAttr);
1898 } else {
1899 osdFormatOnTime(buff);
1901 break;
1904 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH:
1906 /*static int32_t updatedTimeSeconds = 0;*/
1907 static int32_t timeSeconds = -1;
1908 #if defined(USE_ADC) && defined(USE_GPS)
1909 static timeUs_t updatedTimestamp = 0;
1910 timeUs_t currentTimeUs = micros();
1911 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
1912 #ifdef USE_WIND_ESTIMATOR
1913 timeSeconds = calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation);
1914 #else
1915 timeSeconds = calculateRemainingFlightTimeBeforeRTH(false);
1916 #endif
1917 updatedTimestamp = currentTimeUs;
1919 #endif
1920 if ((!ARMING_FLAG(ARMED)) || (timeSeconds == -1)) {
1921 buff[0] = SYM_FLY_M;
1922 strcpy(buff + 1, "--:--");
1923 #if defined(USE_ADC) && defined(USE_GPS)
1924 updatedTimestamp = 0;
1925 #endif
1926 } else if (timeSeconds == -2) {
1927 // Wind is too strong to come back with cruise throttle
1928 buff[0] = SYM_FLY_M;
1929 buff[1] = buff[2] = buff[4] = buff[5] = SYM_WIND_HORIZONTAL;
1930 buff[3] = ':';
1931 buff[6] = '\0';
1932 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1933 } else {
1934 osdFormatTime(buff, timeSeconds, SYM_FLY_M, SYM_FLY_H);
1935 if (timeSeconds == 0)
1936 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1939 break;
1941 case OSD_REMAINING_DISTANCE_BEFORE_RTH:;
1942 static int32_t distanceMeters = -1;
1943 #if defined(USE_ADC) && defined(USE_GPS)
1944 static timeUs_t updatedTimestamp = 0;
1945 timeUs_t currentTimeUs = micros();
1946 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
1947 #ifdef USE_WIND_ESTIMATOR
1948 distanceMeters = calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation);
1949 #else
1950 distanceMeters = calculateRemainingDistanceBeforeRTH(false);
1951 #endif
1952 updatedTimestamp = currentTimeUs;
1954 #endif
1955 buff[0] = SYM_TRIP_DIST;
1956 if ((!ARMING_FLAG(ARMED)) || (distanceMeters == -1)) {
1957 buff[4] = SYM_BLANK;
1958 buff[5] = '\0';
1959 strcpy(buff + 1, "---");
1960 } else if (distanceMeters == -2) {
1961 // Wind is too strong to come back with cruise throttle
1962 buff[1] = buff[2] = buff[3] = SYM_WIND_HORIZONTAL;
1963 switch ((osd_unit_e)osdConfig()->units){
1964 case OSD_UNIT_UK:
1965 FALLTHROUGH;
1966 case OSD_UNIT_IMPERIAL:
1967 buff[4] = SYM_DIST_MI;
1968 break;
1969 case OSD_UNIT_METRIC_MPH:
1970 FALLTHROUGH;
1971 case OSD_UNIT_METRIC:
1972 buff[4] = SYM_DIST_KM;
1973 break;
1974 case OSD_UNIT_GA:
1975 buff[4] = SYM_DIST_NM;
1976 break;
1978 buff[5] = '\0';
1979 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1980 } else {
1981 osdFormatDistanceSymbol(buff + 1, distanceMeters * 100, 0);
1982 if (distanceMeters == 0)
1983 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1985 break;
1987 case OSD_FLYMODE:
1989 char *p = "ACRO";
1991 if (FLIGHT_MODE(FAILSAFE_MODE))
1992 p = "!FS!";
1993 else if (FLIGHT_MODE(MANUAL_MODE))
1994 p = "MANU";
1995 else if (FLIGHT_MODE(TURTLE_MODE))
1996 p = "TURT";
1997 else if (FLIGHT_MODE(NAV_RTH_MODE))
1998 p = isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
1999 else if (FLIGHT_MODE(NAV_POSHOLD_MODE))
2000 p = "HOLD";
2001 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && FLIGHT_MODE(NAV_ALTHOLD_MODE))
2002 p = "CRUZ";
2003 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE))
2004 p = "CRSH";
2005 else if (FLIGHT_MODE(NAV_WP_MODE))
2006 p = " WP ";
2007 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && navigationRequiresAngleMode()) {
2008 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
2009 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
2010 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
2011 p = " AH ";
2013 else if (FLIGHT_MODE(ANGLE_MODE))
2014 p = "ANGL";
2015 else if (FLIGHT_MODE(HORIZON_MODE))
2016 p = "HOR ";
2018 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
2019 return true;
2022 case OSD_CRAFT_NAME:
2023 osdFormatCraftName(buff);
2024 break;
2026 case OSD_THROTTLE_POS:
2028 osdFormatThrottlePosition(buff, false, &elemAttr);
2029 break;
2032 case OSD_VTX_CHANNEL:
2034 vtxDeviceOsdInfo_t osdInfo;
2035 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2037 tfp_sprintf(buff, "CH:%c%s:", osdInfo.bandLetter, osdInfo.channelName);
2038 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2040 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2041 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2042 displayWriteWithAttr(osdDisplayPort, elemPosX + 6, elemPosY, buff, elemAttr);
2043 return true;
2045 break;
2047 case OSD_VTX_POWER:
2049 vtxDeviceOsdInfo_t osdInfo;
2050 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
2052 tfp_sprintf(buff, "%c", SYM_VTX_POWER);
2053 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2055 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
2056 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2057 displayWriteWithAttr(osdDisplayPort, elemPosX+1, elemPosY, buff, elemAttr);
2058 return true;
2061 #if defined(USE_SERIALRX_CRSF)
2062 case OSD_CRSF_RSSI_DBM:
2064 int16_t rssi = rxLinkStatistics.uplinkRSSI;
2065 buff[0] = (rxLinkStatistics.activeAntenna == 0) ? SYM_RSSI : SYM_2RSS; // Separate symbols for each antenna
2066 if (rssi <= -100) {
2067 tfp_sprintf(buff + 1, "%4d%c", rssi, SYM_DBM);
2068 } else {
2069 tfp_sprintf(buff + 1, "%3d%c%c", rssi, SYM_DBM, ' ');
2071 if (!failsafeIsReceivingRxData()){
2072 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2073 } else if (osdConfig()->rssi_dbm_alarm && rssi < osdConfig()->rssi_dbm_alarm) {
2074 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2076 break;
2078 case OSD_CRSF_LQ:
2080 buff[0] = SYM_LQ;
2081 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
2082 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
2083 switch (osdConfig()->crsf_lq_format) {
2084 case OSD_CRSF_LQ_TYPE1:
2085 if (!failsafeIsReceivingRxData()) {
2086 tfp_sprintf(buff+1, "%3d", 0);
2087 } else {
2088 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.uplinkLQ);
2090 break;
2091 case OSD_CRSF_LQ_TYPE2:
2092 if (!failsafeIsReceivingRxData()) {
2093 tfp_sprintf(buff+1, "%s:%3d", " ", 0);
2094 } else {
2095 tfp_sprintf(buff+1, "%d:%3d", rxLinkStatistics.rfMode, rxLinkStatistics.uplinkLQ);
2097 break;
2098 case OSD_CRSF_LQ_TYPE3:
2099 if (!failsafeIsReceivingRxData()) {
2100 tfp_sprintf(buff+1, "%3d", 0);
2101 } else {
2102 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.rfMode >= 2 ? scaledLQ : rxLinkStatistics.uplinkLQ);
2104 break;
2106 if (!failsafeIsReceivingRxData()) {
2107 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2108 } else if (rxLinkStatistics.uplinkLQ < osdConfig()->link_quality_alarm) {
2109 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2111 break;
2114 case OSD_CRSF_SNR_DB:
2116 static pt1Filter_t snrFilterState;
2117 static timeMs_t snrUpdated = 0;
2118 int8_t snrFiltered = pt1FilterApply4(&snrFilterState, rxLinkStatistics.uplinkSNR, 0.5f, MS2S(millis() - snrUpdated));
2119 snrUpdated = millis();
2121 const char* showsnr = "-20";
2122 const char* hidesnr = " ";
2123 if (snrFiltered > osdConfig()->snr_alarm) {
2124 if (cmsInMenu) {
2125 buff[0] = SYM_SNR;
2126 tfp_sprintf(buff + 1, "%s%c", showsnr, SYM_DB);
2127 } else {
2128 buff[0] = SYM_BLANK;
2129 tfp_sprintf(buff + 1, "%s%c", hidesnr, SYM_BLANK);
2131 } else if (snrFiltered <= osdConfig()->snr_alarm) {
2132 buff[0] = SYM_SNR;
2133 if (snrFiltered <= -10) {
2134 tfp_sprintf(buff + 1, "%3d%c", snrFiltered, SYM_DB);
2135 } else {
2136 tfp_sprintf(buff + 1, "%2d%c%c", snrFiltered, SYM_DB, ' ');
2139 break;
2142 case OSD_CRSF_TX_POWER:
2144 if (!failsafeIsReceivingRxData())
2145 tfp_sprintf(buff, "%s%c", " ", SYM_BLANK);
2146 else
2147 tfp_sprintf(buff, "%4d%c", rxLinkStatistics.uplinkTXPower, SYM_MW);
2148 break;
2150 #endif
2152 case OSD_CROSSHAIRS: // Hud is a sub-element of the crosshair
2154 osdCrosshairPosition(&elemPosX, &elemPosY);
2155 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX, elemPosY);
2157 if (osdConfig()->hud_homing && STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
2158 osdHudDrawHoming(elemPosX, elemPosY);
2161 if (STATE(GPS_FIX) && isImuHeadingValid()) {
2163 if (osdConfig()->hud_homepoint || osdConfig()->hud_radar_disp > 0 || osdConfig()->hud_wp_disp > 0) {
2164 osdHudClear();
2167 // -------- POI : Home point
2169 if (osdConfig()->hud_homepoint) { // Display the home point (H)
2170 osdHudDrawPoi(GPS_distanceToHome, GPS_directionToHome, -osdGetAltitude() / 100, 0, SYM_HOME, 0 , 0);
2173 // -------- POI : Nearby aircrafts from ESP32 radar
2175 if (osdConfig()->hud_radar_disp > 0) { // Display the POI from the radar
2176 for (uint8_t i = 0; i < osdConfig()->hud_radar_disp; i++) {
2177 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
2178 fpVector3_t poi;
2179 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &radar_pois[i].gps, GEO_ALT_RELATIVE);
2180 radar_pois[i].distance = calculateDistanceToDestination(&poi) / 100; // In meters
2182 if (radar_pois[i].distance >= osdConfig()->hud_radar_range_min && radar_pois[i].distance <= osdConfig()->hud_radar_range_max) {
2183 radar_pois[i].direction = calculateBearingToDestination(&poi) / 100; // In °
2184 radar_pois[i].altitude = (radar_pois[i].gps.alt - osdGetAltitudeMsl()) / 100;
2185 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);
2191 // -------- POI : Next waypoints from navigation
2193 if (osdConfig()->hud_wp_disp > 0 && posControl.waypointListValid && posControl.waypointCount > 0) { // Display the next waypoints
2194 gpsLocation_t wp2;
2195 int j;
2197 for (int i = osdConfig()->hud_wp_disp - 1; i >= 0 ; i--) { // Display in reverse order so the next WP is always written on top
2198 j = posControl.activeWaypointIndex + i;
2199 if (j > posControl.waypointCount - 1) { // limit to max WP index for mission
2200 break;
2202 if (posControl.waypointList[j].lat != 0 && posControl.waypointList[j].lon != 0) {
2203 wp2.lat = posControl.waypointList[j].lat;
2204 wp2.lon = posControl.waypointList[j].lon;
2205 wp2.alt = posControl.waypointList[j].alt;
2206 fpVector3_t poi;
2207 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &wp2, waypointMissionAltConvMode(posControl.waypointList[j].p3));
2208 int32_t altConvModeAltitude = waypointMissionAltConvMode(posControl.waypointList[j].p3) == GEO_ALT_ABSOLUTE ? osdGetAltitudeMsl() : osdGetAltitude();
2209 j = getGeoWaypointNumber(j);
2210 while (j > 9) j -= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2211 osdHudDrawPoi(calculateDistanceToDestination(&poi) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi) / 100), (posControl.waypointList[j].alt - altConvModeAltitude)/ 100, 2, SYM_WAYPOINT, 48 + j, i);
2217 return true;
2218 break;
2220 case OSD_ATTITUDE_ROLL:
2221 buff[0] = SYM_ROLL_LEVEL;
2222 if (ABS(attitude.values.roll) >= 1)
2223 buff[0] += (attitude.values.roll < 0 ? -1 : 1);
2224 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.roll)), 0, 1, 0, 3);
2225 break;
2227 case OSD_ATTITUDE_PITCH:
2228 if (ABS(attitude.values.pitch) < 1)
2229 buff[0] = 'P';
2230 else if (attitude.values.pitch > 0)
2231 buff[0] = SYM_PITCH_DOWN;
2232 else if (attitude.values.pitch < 0)
2233 buff[0] = SYM_PITCH_UP;
2234 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.pitch)), 0, 1, 0, 3);
2235 break;
2237 case OSD_ARTIFICIAL_HORIZON:
2239 float rollAngle;
2240 float pitchAngle;
2242 #ifdef USE_SECONDARY_IMU
2243 if (secondaryImuState.active && secondaryImuConfig()->useForOsdAHI) {
2244 rollAngle = DECIDEGREES_TO_RADIANS(secondaryImuState.eulerAngles.values.roll);
2245 pitchAngle = DECIDEGREES_TO_RADIANS(secondaryImuState.eulerAngles.values.pitch);
2246 } else {
2247 rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2248 pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2250 #else
2251 rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2252 pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2253 #endif
2254 pitchAngle -= osdConfig()->ahi_camera_uptilt_comp ? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt) : 0;
2255 pitchAngle += DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2256 if (osdConfig()->ahi_reverse_roll) {
2257 rollAngle = -rollAngle;
2259 osdDrawArtificialHorizon(osdDisplayPort, osdGetDisplayPortCanvas(),
2260 OSD_DRAW_POINT_GRID(elemPosX, elemPosY), rollAngle, pitchAngle);
2261 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
2262 osdDrawSingleElement(OSD_CROSSHAIRS);
2264 return true;
2267 case OSD_HORIZON_SIDEBARS:
2269 osdDrawSidebars(osdDisplayPort, osdGetDisplayPortCanvas());
2270 return true;
2273 #if defined(USE_BARO) || defined(USE_GPS)
2274 case OSD_VARIO:
2276 float zvel = getEstimatedActualVelocity(Z);
2277 osdDrawVario(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), zvel);
2278 return true;
2281 case OSD_VARIO_NUM:
2283 int16_t value = getEstimatedActualVelocity(Z);
2284 char sym;
2285 switch ((osd_unit_e)osdConfig()->units) {
2286 case OSD_UNIT_UK:
2287 FALLTHROUGH;
2288 case OSD_UNIT_IMPERIAL:
2289 // Convert to centifeet/s
2290 value = CENTIMETERS_TO_CENTIFEET(value);
2291 sym = SYM_FTS;
2292 break;
2293 case OSD_UNIT_GA:
2294 // Convert to centi-100feet/min
2295 value = CENTIMETERS_TO_FEET(value * 60);
2296 sym = SYM_100FTM;
2297 break;
2298 default:
2299 case OSD_UNIT_METRIC_MPH:
2300 FALLTHROUGH;
2301 case OSD_UNIT_METRIC:
2302 // Already in cm/s
2303 sym = SYM_MS;
2304 break;
2307 osdFormatCentiNumber(buff, value, 0, 1, 0, 3);
2308 buff[3] = sym;
2309 buff[4] = '\0';
2310 break;
2312 #endif
2314 case OSD_SWITCH_INDICATOR_0:
2315 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channel - 1), buff);
2316 break;
2318 case OSD_SWITCH_INDICATOR_1:
2319 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channel - 1), buff);
2320 break;
2322 case OSD_SWITCH_INDICATOR_2:
2323 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channel - 1), buff);
2324 break;
2326 case OSD_SWITCH_INDICATOR_3:
2327 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channel - 1), buff);
2328 break;
2330 case OSD_ACTIVE_PROFILE:
2331 tfp_sprintf(buff, "%c%u", SYM_PROFILE, (getConfigProfile() + 1));
2332 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2333 break;
2335 case OSD_ROLL_PIDS:
2336 osdDisplayFlightPIDValues(elemPosX, elemPosY, "ROL", PID_ROLL, ADJUSTMENT_ROLL_P, ADJUSTMENT_ROLL_I, ADJUSTMENT_ROLL_D, ADJUSTMENT_ROLL_FF);
2337 return true;
2339 case OSD_PITCH_PIDS:
2340 osdDisplayFlightPIDValues(elemPosX, elemPosY, "PIT", PID_PITCH, ADJUSTMENT_PITCH_P, ADJUSTMENT_PITCH_I, ADJUSTMENT_PITCH_D, ADJUSTMENT_PITCH_FF);
2341 return true;
2343 case OSD_YAW_PIDS:
2344 osdDisplayFlightPIDValues(elemPosX, elemPosY, "YAW", PID_YAW, ADJUSTMENT_YAW_P, ADJUSTMENT_YAW_I, ADJUSTMENT_YAW_D, ADJUSTMENT_YAW_FF);
2345 return true;
2347 case OSD_LEVEL_PIDS:
2348 osdDisplayNavPIDValues(elemPosX, elemPosY, "LEV", PID_LEVEL, ADJUSTMENT_LEVEL_P, ADJUSTMENT_LEVEL_I, ADJUSTMENT_LEVEL_D);
2349 return true;
2351 case OSD_POS_XY_PIDS:
2352 osdDisplayNavPIDValues(elemPosX, elemPosY, "PXY", PID_POS_XY, ADJUSTMENT_POS_XY_P, ADJUSTMENT_POS_XY_I, ADJUSTMENT_POS_XY_D);
2353 return true;
2355 case OSD_POS_Z_PIDS:
2356 osdDisplayNavPIDValues(elemPosX, elemPosY, "PZ", PID_POS_Z, ADJUSTMENT_POS_Z_P, ADJUSTMENT_POS_Z_I, ADJUSTMENT_POS_Z_D);
2357 return true;
2359 case OSD_VEL_XY_PIDS:
2360 osdDisplayNavPIDValues(elemPosX, elemPosY, "VXY", PID_VEL_XY, ADJUSTMENT_VEL_XY_P, ADJUSTMENT_VEL_XY_I, ADJUSTMENT_VEL_XY_D);
2361 return true;
2363 case OSD_VEL_Z_PIDS:
2364 osdDisplayNavPIDValues(elemPosX, elemPosY, "VZ", PID_VEL_Z, ADJUSTMENT_VEL_Z_P, ADJUSTMENT_VEL_Z_I, ADJUSTMENT_VEL_Z_D);
2365 return true;
2367 case OSD_HEADING_P:
2368 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "HP", 0, pidBank()->pid[PID_HEADING].P, 3, 0, ADJUSTMENT_HEADING_P);
2369 return true;
2371 case OSD_BOARD_ALIGN_ROLL:
2372 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT);
2373 return true;
2375 case OSD_BOARD_ALIGN_PITCH:
2376 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT);
2377 return true;
2379 case OSD_RC_EXPO:
2380 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "EXP", 0, currentControlRateProfile->stabilized.rcExpo8, 3, 0, ADJUSTMENT_RC_EXPO);
2381 return true;
2383 case OSD_RC_YAW_EXPO:
2384 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "YEX", 0, currentControlRateProfile->stabilized.rcYawExpo8, 3, 0, ADJUSTMENT_RC_YAW_EXPO);
2385 return true;
2387 case OSD_THROTTLE_EXPO:
2388 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TEX", 0, currentControlRateProfile->throttle.rcExpo8, 3, 0, ADJUSTMENT_THROTTLE_EXPO);
2389 return true;
2391 case OSD_PITCH_RATE:
2392 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SPR");
2394 elemAttr = TEXT_ATTRIBUTES_NONE;
2395 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_PITCH]);
2396 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2397 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2398 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2399 return true;
2401 case OSD_ROLL_RATE:
2402 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SRR");
2404 elemAttr = TEXT_ATTRIBUTES_NONE;
2405 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_ROLL]);
2406 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2407 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2408 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2409 return true;
2411 case OSD_YAW_RATE:
2412 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "SYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2413 return true;
2415 case OSD_MANUAL_RC_EXPO:
2416 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MEX", 0, currentControlRateProfile->manual.rcExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO);
2417 return true;
2419 case OSD_MANUAL_RC_YAW_EXPO:
2420 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYX", 0, currentControlRateProfile->manual.rcYawExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO);
2421 return true;
2423 case OSD_MANUAL_PITCH_RATE:
2424 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MPR");
2426 elemAttr = TEXT_ATTRIBUTES_NONE;
2427 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_PITCH]);
2428 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2429 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2430 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2431 return true;
2433 case OSD_MANUAL_ROLL_RATE:
2434 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MRR");
2436 elemAttr = TEXT_ATTRIBUTES_NONE;
2437 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_ROLL]);
2438 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2439 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2440 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2441 return true;
2443 case OSD_MANUAL_YAW_RATE:
2444 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2445 return true;
2447 case OSD_NAV_FW_CRUISE_THR:
2448 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CRZ", 0, currentBatteryProfile->nav.fw.cruise_throttle, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR);
2449 return true;
2451 case OSD_NAV_FW_PITCH2THR:
2452 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "P2T", 0, currentBatteryProfile->nav.fw.pitch_to_throttle, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR);
2453 return true;
2455 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE:
2456 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "0TP", 0, (float)currentBatteryProfile->fwMinThrottleDownPitchAngle / 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE);
2457 return true;
2459 case OSD_FW_ALT_PID_OUTPUTS:
2461 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2462 osdFormatPidControllerOutput(buff, "PZO", &nav_pids->fw_alt, 10, true); // display requested pitch degrees
2463 break;
2466 case OSD_FW_POS_PID_OUTPUTS:
2468 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers(); // display requested roll degrees
2469 osdFormatPidControllerOutput(buff, "PXYO", &nav_pids->fw_nav, 1, true);
2470 break;
2473 case OSD_MC_VEL_Z_PID_OUTPUTS:
2475 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2476 osdFormatPidControllerOutput(buff, "VZO", &nav_pids->vel[Z], 100, false); // display throttle adjustment µs
2477 break;
2480 case OSD_MC_VEL_X_PID_OUTPUTS:
2482 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2483 osdFormatPidControllerOutput(buff, "VXO", &nav_pids->vel[X], 100, false); // display requested acceleration cm/s^2
2484 break;
2487 case OSD_MC_VEL_Y_PID_OUTPUTS:
2489 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2490 osdFormatPidControllerOutput(buff, "VYO", &nav_pids->vel[Y], 100, false); // display requested acceleration cm/s^2
2491 break;
2494 case OSD_MC_POS_XYZ_P_OUTPUTS:
2496 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2497 strcpy(buff, "POSO ");
2498 // display requested velocity cm/s
2499 tfp_sprintf(buff + 5, "%4d", (int)lrintf(nav_pids->pos[X].output_constrained * 100));
2500 buff[9] = ' ';
2501 tfp_sprintf(buff + 10, "%4d", (int)lrintf(nav_pids->pos[Y].output_constrained * 100));
2502 buff[14] = ' ';
2503 tfp_sprintf(buff + 15, "%4d", (int)lrintf(nav_pids->pos[Z].output_constrained * 100));
2504 buff[19] = '\0';
2505 break;
2508 case OSD_POWER:
2510 bool kiloWatt = osdFormatCentiNumber(buff, getPower(), 1000, 2, 2, 3);
2511 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
2512 buff[4] = '\0';
2514 uint8_t current_alarm = osdConfig()->current_alarm;
2515 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
2516 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2518 break;
2521 case OSD_AIR_SPEED:
2523 #ifdef USE_PITOT
2524 buff[0] = SYM_AIR;
2525 osdFormatVelocityStr(buff + 1, pitot.airSpeed, false, false);
2527 if ((osdConfig()->airspeed_alarm_min != 0 && pitot.airSpeed < osdConfig()->airspeed_alarm_min) ||
2528 (osdConfig()->airspeed_alarm_max != 0 && pitot.airSpeed > osdConfig()->airspeed_alarm_max)) {
2529 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2531 #else
2532 return false;
2533 #endif
2534 break;
2537 case OSD_AIR_MAX_SPEED:
2539 #ifdef USE_PITOT
2540 buff[0] = SYM_MAX;
2541 buff[1] = SYM_AIR;
2542 osdFormatVelocityStr(buff + 2, stats.max_air_speed, false, false);
2543 #else
2544 return false;
2545 #endif
2546 break;
2549 case OSD_RTC_TIME:
2551 // RTC not configured will show 00:00
2552 dateTime_t dateTime;
2553 rtcGetDateTimeLocal(&dateTime);
2554 buff[0] = SYM_CLOCK;
2555 tfp_sprintf(buff + 1, "%02u:%02u:%02u", dateTime.hours, dateTime.minutes, dateTime.seconds);
2556 break;
2559 case OSD_MESSAGES:
2561 elemAttr = osdGetSystemMessage(buff, OSD_MESSAGE_LENGTH, true);
2562 break;
2565 case OSD_VERSION:
2567 tfp_sprintf(buff, "INAV %s", FC_VERSION_STRING);
2568 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2569 break;
2572 case OSD_MAIN_BATT_CELL_VOLTAGE:
2574 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawAverageCellVoltage(), 3, 2);
2575 return true;
2578 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE:
2580 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedAverageCellVoltage(), 3, 2);
2581 return true;
2584 case OSD_THROTTLE_POS_AUTO_THR:
2586 osdFormatThrottlePosition(buff, true, &elemAttr);
2587 break;
2590 case OSD_HEADING_GRAPH:
2592 if (osdIsHeadingValid()) {
2593 osdDrawHeadingGraph(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), osdGetHeading());
2594 return true;
2595 } else {
2596 buff[0] = buff[2] = buff[4] = buff[6] = buff[8] = SYM_HEADING_LINE;
2597 buff[1] = buff[3] = buff[5] = buff[7] = SYM_HEADING_DIVIDED_LINE;
2598 buff[OSD_HEADING_GRAPH_WIDTH] = '\0';
2600 break;
2603 case OSD_EFFICIENCY_MAH_PER_KM:
2605 // amperage is in centi amps, speed is in cms/s. We want
2606 // mah/km. Only show when ground speed > 1m/s.
2607 static pt1Filter_t eFilterState;
2608 static timeUs_t efficiencyUpdated = 0;
2609 int32_t value = 0;
2610 bool moreThanAh = false;
2611 timeUs_t currentTimeUs = micros();
2612 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2613 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2614 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2615 value = pt1FilterApply4(&eFilterState, ((float)getAmperage() / gpsSol.groundSpeed) / 0.0036f,
2616 1, US2S(efficiencyTimeDelta));
2618 efficiencyUpdated = currentTimeUs;
2619 } else {
2620 value = eFilterState.state;
2623 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2624 switch (osdConfig()->units) {
2625 case OSD_UNIT_UK:
2626 FALLTHROUGH;
2627 case OSD_UNIT_IMPERIAL:
2628 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10, 1000, 0, 2, 3);
2629 if (!moreThanAh) {
2630 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
2631 } else {
2632 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
2634 if (!efficiencyValid) {
2635 buff[0] = buff[1] = buff[2] = '-';
2636 buff[3] = SYM_MAH_MI_0;
2637 buff[4] = SYM_MAH_MI_1;
2638 buff[5] = '\0';
2640 break;
2641 case OSD_UNIT_GA:
2642 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10, 1000, 0, 2, 3);
2643 if (!moreThanAh) {
2644 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
2645 } else {
2646 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
2648 if (!efficiencyValid) {
2649 buff[0] = buff[1] = buff[2] = '-';
2650 buff[3] = SYM_MAH_NM_0;
2651 buff[4] = SYM_MAH_NM_1;
2652 buff[5] = '\0';
2654 break;
2655 case OSD_UNIT_METRIC_MPH:
2656 FALLTHROUGH;
2657 case OSD_UNIT_METRIC:
2658 moreThanAh = osdFormatCentiNumber(buff, value * 100, 1000, 0, 2, 3);
2659 if (!moreThanAh) {
2660 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
2661 } else {
2662 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
2664 if (!efficiencyValid) {
2665 buff[0] = buff[1] = buff[2] = '-';
2666 buff[3] = SYM_MAH_KM_0;
2667 buff[4] = SYM_MAH_KM_1;
2668 buff[5] = '\0';
2670 break;
2672 break;
2675 case OSD_EFFICIENCY_WH_PER_KM:
2677 // amperage is in centi amps, speed is in cms/s. We want
2678 // mWh/km. Only show when ground speed > 1m/s.
2679 static pt1Filter_t eFilterState;
2680 static timeUs_t efficiencyUpdated = 0;
2681 int32_t value = 0;
2682 timeUs_t currentTimeUs = micros();
2683 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2684 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2685 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2686 value = pt1FilterApply4(&eFilterState, ((float)getPower() / gpsSol.groundSpeed) / 0.0036f,
2687 1, US2S(efficiencyTimeDelta));
2689 efficiencyUpdated = currentTimeUs;
2690 } else {
2691 value = eFilterState.state;
2694 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2695 switch (osdConfig()->units) {
2696 case OSD_UNIT_UK:
2697 FALLTHROUGH;
2698 case OSD_UNIT_IMPERIAL:
2699 osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10000, 0, 2, 0, 3);
2700 buff[3] = SYM_WH_MI;
2701 break;
2702 case OSD_UNIT_GA:
2703 osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10000, 0, 2, 0, 3);
2704 buff[3] = SYM_WH_NM;
2705 break;
2706 case OSD_UNIT_METRIC_MPH:
2707 FALLTHROUGH;
2708 case OSD_UNIT_METRIC:
2709 osdFormatCentiNumber(buff, value / 10, 0, 2, 0, 3);
2710 buff[3] = SYM_WH_KM;
2711 break;
2713 buff[4] = '\0';
2714 if (!efficiencyValid) {
2715 buff[0] = buff[1] = buff[2] = '-';
2717 break;
2720 case OSD_GFORCE:
2722 buff[0] = SYM_GFORCE;
2723 osdFormatCentiNumber(buff + 1, GForce, 0, 2, 0, 3);
2724 if (GForce > osdConfig()->gforce_alarm * 100) {
2725 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2727 break;
2730 case OSD_GFORCE_X:
2731 case OSD_GFORCE_Y:
2732 case OSD_GFORCE_Z:
2734 float GForceValue = GForceAxis[item - OSD_GFORCE_X];
2735 buff[0] = SYM_GFORCE_X + item - OSD_GFORCE_X;
2736 osdFormatCentiNumber(buff + 1, GForceValue, 0, 2, 0, 4);
2737 if ((GForceValue < osdConfig()->gforce_axis_alarm_min * 100) || (GForceValue > osdConfig()->gforce_axis_alarm_max * 100)) {
2738 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2740 break;
2742 case OSD_DEBUG:
2745 * Longest representable string is -2147483648 does not fit in the screen.
2746 * Only 7 digits for negative and 8 digits for positive values allowed
2748 for (uint8_t bufferIndex = 0; bufferIndex < DEBUG32_VALUE_COUNT; ++elemPosY, bufferIndex += 2) {
2749 tfp_sprintf(
2750 buff,
2751 "[%u]=%8ld [%u]=%8ld",
2752 bufferIndex,
2753 constrain(debug[bufferIndex], -9999999, 99999999),
2754 bufferIndex+1,
2755 constrain(debug[bufferIndex+1], -9999999, 99999999)
2757 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2759 break;
2762 case OSD_IMU_TEMPERATURE:
2764 int16_t temperature;
2765 const bool valid = getIMUTemperature(&temperature);
2766 osdDisplayTemperature(elemPosX, elemPosY, SYM_IMU_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2767 return true;
2770 case OSD_BARO_TEMPERATURE:
2772 int16_t temperature;
2773 const bool valid = getBaroTemperature(&temperature);
2774 osdDisplayTemperature(elemPosX, elemPosY, SYM_BARO_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2775 return true;
2778 #ifdef USE_TEMPERATURE_SENSOR
2779 case OSD_TEMP_SENSOR_0_TEMPERATURE:
2780 case OSD_TEMP_SENSOR_1_TEMPERATURE:
2781 case OSD_TEMP_SENSOR_2_TEMPERATURE:
2782 case OSD_TEMP_SENSOR_3_TEMPERATURE:
2783 case OSD_TEMP_SENSOR_4_TEMPERATURE:
2784 case OSD_TEMP_SENSOR_5_TEMPERATURE:
2785 case OSD_TEMP_SENSOR_6_TEMPERATURE:
2786 case OSD_TEMP_SENSOR_7_TEMPERATURE:
2788 osdDisplayTemperatureSensor(elemPosX, elemPosY, item - OSD_TEMP_SENSOR_0_TEMPERATURE);
2789 return true;
2791 #endif /* ifdef USE_TEMPERATURE_SENSOR */
2793 case OSD_WIND_SPEED_HORIZONTAL:
2794 #ifdef USE_WIND_ESTIMATOR
2796 bool valid = isEstimatedWindSpeedValid();
2797 float horizontalWindSpeed;
2798 if (valid) {
2799 uint16_t angle;
2800 horizontalWindSpeed = getEstimatedHorizontalWindSpeed(&angle);
2801 int16_t windDirection = osdGetHeadingAngle((int)angle - DECIDEGREES_TO_DEGREES(attitude.values.yaw));
2802 buff[1] = SYM_DIRECTION + (windDirection * 2 / 90);
2803 } else {
2804 horizontalWindSpeed = 0;
2805 buff[1] = SYM_BLANK;
2807 buff[0] = SYM_WIND_HORIZONTAL;
2808 osdFormatWindSpeedStr(buff + 2, horizontalWindSpeed, valid);
2809 break;
2811 #else
2812 return false;
2813 #endif
2815 case OSD_WIND_SPEED_VERTICAL:
2816 #ifdef USE_WIND_ESTIMATOR
2818 buff[0] = SYM_WIND_VERTICAL;
2819 buff[1] = SYM_BLANK;
2820 bool valid = isEstimatedWindSpeedValid();
2821 float verticalWindSpeed;
2822 if (valid) {
2823 verticalWindSpeed = getEstimatedWindSpeed(Z);
2824 if (verticalWindSpeed < 0) {
2825 buff[1] = SYM_AH_DECORATION_DOWN;
2826 verticalWindSpeed = -verticalWindSpeed;
2827 } else if (verticalWindSpeed > 0) {
2828 buff[1] = SYM_AH_DECORATION_UP;
2830 } else {
2831 verticalWindSpeed = 0;
2833 osdFormatWindSpeedStr(buff + 2, verticalWindSpeed, valid);
2834 break;
2836 #else
2837 return false;
2838 #endif
2840 case OSD_PLUS_CODE:
2842 STATIC_ASSERT(GPS_DEGREES_DIVIDER == OLC_DEG_MULTIPLIER, invalid_olc_deg_multiplier);
2843 int digits = osdConfig()->plus_code_digits;
2844 int digitsRemoved = osdConfig()->plus_code_short * 2;
2845 if (STATE(GPS_FIX)) {
2846 olc_encode(gpsSol.llh.lat, gpsSol.llh.lon, digits, buff, sizeof(buff));
2847 } else {
2848 // +codes with > 8 digits have a + at the 9th digit
2849 // and we only support 10 and up.
2850 memset(buff, '-', digits + 1);
2851 buff[8] = '+';
2852 buff[digits + 1] = '\0';
2854 // Optionally trim digits from the left
2855 memmove(buff, buff+digitsRemoved, strlen(buff) + digitsRemoved);
2856 buff[digits + 1 - digitsRemoved] = '\0';
2857 break;
2860 case OSD_AZIMUTH:
2863 buff[0] = SYM_AZIMUTH;
2864 if (osdIsHeadingValid()) {
2865 int16_t h = GPS_directionToHome;
2866 if (h < 0) {
2867 h += 360;
2869 if(h >= 180)
2870 h = h - 180;
2871 else
2872 h = h + 180;
2874 tfp_sprintf(&buff[1], "%3d", h);
2875 } else {
2876 buff[1] = buff[2] = buff[3] = '-';
2878 buff[4] = SYM_DEGREES;
2879 buff[5] = '\0';
2880 break;
2883 case OSD_MAP_SCALE:
2885 float scaleToUnit;
2886 int scaleUnitDivisor;
2887 char symUnscaled;
2888 char symScaled;
2889 int maxDecimals;
2891 switch (osdConfig()->units) {
2892 case OSD_UNIT_UK:
2893 FALLTHROUGH;
2894 case OSD_UNIT_IMPERIAL:
2895 scaleToUnit = 100 / 1609.3440f; // scale to 0.01mi for osdFormatCentiNumber()
2896 scaleUnitDivisor = 0;
2897 symUnscaled = SYM_MI;
2898 symScaled = SYM_MI;
2899 maxDecimals = 2;
2900 break;
2901 case OSD_UNIT_GA:
2902 scaleToUnit = 100 / 1852.0010f; // scale to 0.01mi for osdFormatCentiNumber()
2903 scaleUnitDivisor = 0;
2904 symUnscaled = SYM_NM;
2905 symScaled = SYM_NM;
2906 maxDecimals = 2;
2907 break;
2908 default:
2909 case OSD_UNIT_METRIC_MPH:
2910 FALLTHROUGH;
2911 case OSD_UNIT_METRIC:
2912 scaleToUnit = 100; // scale to cm for osdFormatCentiNumber()
2913 scaleUnitDivisor = 1000; // Convert to km when scale gets bigger than 999m
2914 symUnscaled = SYM_M;
2915 symScaled = SYM_KM;
2916 maxDecimals = 0;
2917 break;
2919 buff[0] = SYM_SCALE;
2920 if (osdMapData.scale > 0) {
2921 bool scaled = osdFormatCentiNumber(&buff[1], osdMapData.scale * scaleToUnit, scaleUnitDivisor, maxDecimals, 2, 3);
2922 buff[4] = scaled ? symScaled : symUnscaled;
2923 // Make sure this is cleared if the map stops being drawn
2924 osdMapData.scale = 0;
2925 } else {
2926 memset(&buff[1], '-', 4);
2928 buff[5] = '\0';
2929 break;
2931 case OSD_MAP_REFERENCE:
2933 char referenceSymbol;
2934 if (osdMapData.referenceSymbol) {
2935 referenceSymbol = osdMapData.referenceSymbol;
2936 // Make sure this is cleared if the map stops being drawn
2937 osdMapData.referenceSymbol = 0;
2938 } else {
2939 referenceSymbol = '-';
2941 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DIRECTION);
2942 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 1, referenceSymbol);
2943 return true;
2946 case OSD_GVAR_0:
2948 osdFormatGVar(buff, 0);
2949 break;
2951 case OSD_GVAR_1:
2953 osdFormatGVar(buff, 1);
2954 break;
2956 case OSD_GVAR_2:
2958 osdFormatGVar(buff, 2);
2959 break;
2961 case OSD_GVAR_3:
2963 osdFormatGVar(buff, 3);
2964 break;
2967 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
2968 case OSD_RC_SOURCE:
2970 const char *source_text = IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
2971 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2972 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, source_text, elemAttr);
2973 return true;
2975 #endif
2977 #if defined(USE_ESC_SENSOR)
2978 case OSD_ESC_RPM:
2980 escSensorData_t * escSensor = escSensorGetData();
2981 if (escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE) {
2982 osdFormatRpm(buff, escSensor->rpm);
2984 else {
2985 osdFormatRpm(buff, 0);
2987 break;
2989 case OSD_ESC_TEMPERATURE:
2991 escSensorData_t * escSensor = escSensorGetData();
2992 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
2993 osdDisplayTemperature(elemPosX, elemPosY, SYM_ESC_TEMP, NULL, escTemperatureValid, (escSensor->temperature)*10, osdConfig()->esc_temp_alarm_min, osdConfig()->esc_temp_alarm_max);
2994 return true;
2996 #endif
2997 case OSD_TPA:
2999 char buff[4];
3000 textAttributes_t attr;
3002 displayWrite(osdDisplayPort, elemPosX, elemPosY, "TPA");
3003 attr = TEXT_ATTRIBUTES_NONE;
3004 tfp_sprintf(buff, "%3d", currentControlRateProfile->throttle.dynPID);
3005 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA)) {
3006 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3008 displayWriteWithAttr(osdDisplayPort, elemPosX + 5, elemPosY, buff, attr);
3010 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, "BP");
3011 attr = TEXT_ATTRIBUTES_NONE;
3012 tfp_sprintf(buff, "%4d", currentControlRateProfile->throttle.pa_breakpoint);
3013 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT)) {
3014 TEXT_ATTRIBUTES_ADD_BLINK(attr);
3016 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY + 1, buff, attr);
3018 return true;
3020 case OSD_TPA_TIME_CONSTANT:
3022 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TPA TC", 0, currentControlRateProfile->throttle.fixedWingTauMs, 4, 0, ADJUSTMENT_FW_TPA_TIME_CONSTANT);
3023 return true;
3025 case OSD_FW_LEVEL_TRIM:
3027 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "LEVEL", 0, pidProfileMutable()->fixedWingLevelTrim, 3, 1, ADJUSTMENT_FW_LEVEL_TRIM);
3028 return true;
3031 case OSD_NAV_FW_CONTROL_SMOOTHNESS:
3032 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CTL S", 0, navConfig()->fw.control_smoothness, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS);
3033 return true;
3035 case OSD_MISSION:
3037 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION)) {
3038 char buf[5];
3039 switch (posControl.wpMissionPlannerStatus) {
3040 case WP_PLAN_WAIT:
3041 strcpy(buf, "WAIT");
3042 break;
3043 case WP_PLAN_SAVE:
3044 strcpy(buf, "SAVE");
3045 break;
3046 case WP_PLAN_OK:
3047 strcpy(buf, " OK ");
3048 break;
3049 case WP_PLAN_FULL:
3050 strcpy(buf, "FULL");
3052 tfp_sprintf(buff, "%s>%2uWP", buf, posControl.wpPlannerActiveWPIndex);
3053 } else if (posControl.wpPlannerActiveWPIndex){
3054 tfp_sprintf(buff, "PLAN>%2uWP", posControl.waypointCount); // mission planner mision active
3056 #ifdef USE_MULTI_MISSION
3057 else {
3058 if (ARMING_FLAG(ARMED)){
3059 // Limit field size when Armed, only show selected mission
3060 tfp_sprintf(buff, "M%u ", posControl.loadedMultiMissionIndex);
3061 } else if (posControl.multiMissionCount && navConfig()->general.waypoint_multi_mission_index){
3062 if (navConfig()->general.waypoint_multi_mission_index != posControl.loadedMultiMissionIndex) {
3063 tfp_sprintf(buff, "M%u/%u>LOAD", navConfig()->general.waypoint_multi_mission_index, posControl.multiMissionCount);
3064 } else {
3065 // wpCount source for selected mission changes after Arming (until next mission load)
3066 int8_t wpCount = posControl.loadedMultiMissionWPCount ? posControl.loadedMultiMissionWPCount : posControl.waypointCount;
3067 if (posControl.waypointListValid && wpCount > 0) {
3068 tfp_sprintf(buff, "M%u/%u>%2uWP", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, wpCount);
3069 } else {
3070 tfp_sprintf(buff, "M0/%u> 0WP", posControl.multiMissionCount);
3073 } else { // multi_mission_index 0 - show active WP count
3074 tfp_sprintf(buff, "WP CNT>%2u", posControl.waypointCount);
3077 #endif
3078 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
3079 return true;
3082 #ifdef USE_POWER_LIMITS
3083 case OSD_PLIMIT_REMAINING_BURST_TIME:
3084 osdFormatCentiNumber(buff, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3);
3085 buff[3] = 'S';
3086 buff[4] = '\0';
3087 break;
3089 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT:
3090 if (currentBatteryProfile->powerLimits.continuousCurrent) {
3091 osdFormatCentiNumber(buff, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3);
3092 buff[3] = SYM_AMP;
3093 buff[4] = '\0';
3095 if (powerLimiterIsLimitingCurrent()) {
3096 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3099 break;
3101 #ifdef USE_ADC
3102 case OSD_PLIMIT_ACTIVE_POWER_LIMIT:
3104 if (currentBatteryProfile->powerLimits.continuousPower) {
3105 bool kiloWatt = osdFormatCentiNumber(buff, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3);
3106 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3107 buff[4] = '\0';
3109 if (powerLimiterIsLimitingPower()) {
3110 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
3113 break;
3115 #endif // USE_ADC
3116 #endif // USE_POWER_LIMITS
3118 default:
3119 return false;
3122 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
3123 return true;
3126 static uint8_t osdIncElementIndex(uint8_t elementIndex)
3128 ++elementIndex;
3130 if (elementIndex == OSD_ARTIFICIAL_HORIZON)
3131 ++elementIndex;
3133 #ifndef USE_TEMPERATURE_SENSOR
3134 if (elementIndex == OSD_TEMP_SENSOR_0_TEMPERATURE)
3135 elementIndex = OSD_ALTITUDE_MSL;
3136 #endif
3138 if (!sensors(SENSOR_ACC)) {
3139 if (elementIndex == OSD_CROSSHAIRS) {
3140 elementIndex = OSD_ONTIME;
3144 if (!feature(FEATURE_VBAT)) {
3145 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3146 elementIndex = OSD_LEVEL_PIDS;
3150 if (!feature(FEATURE_CURRENT_METER)) {
3151 if (elementIndex == OSD_CURRENT_DRAW) {
3152 elementIndex = OSD_GPS_SPEED;
3154 if (elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
3155 elementIndex = OSD_TRIP_DIST;
3157 if (elementIndex == OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH) {
3158 elementIndex = OSD_HOME_HEADING_ERROR;
3160 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
3161 elementIndex = OSD_LEVEL_PIDS;
3165 if (!feature(FEATURE_GPS)) {
3166 if (elementIndex == OSD_GPS_SPEED) {
3167 elementIndex = OSD_ALTITUDE;
3169 if (elementIndex == OSD_GPS_LON) {
3170 elementIndex = OSD_VARIO;
3172 if (elementIndex == OSD_GPS_HDOP) {
3173 elementIndex = OSD_MAIN_BATT_CELL_VOLTAGE;
3175 if (elementIndex == OSD_TRIP_DIST) {
3176 elementIndex = OSD_ATTITUDE_PITCH;
3178 if (elementIndex == OSD_WIND_SPEED_HORIZONTAL) {
3179 elementIndex = OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE;
3181 if (elementIndex == OSD_3D_SPEED) {
3182 elementIndex++;
3186 if (!STATE(ESC_SENSOR_ENABLED)) {
3187 if (elementIndex == OSD_ESC_RPM) {
3188 elementIndex++;
3192 #ifndef USE_POWER_LIMITS
3193 if (elementIndex == OSD_NAV_FW_CONTROL_SMOOTHNESS) {
3194 elementIndex = OSD_ITEM_COUNT;
3196 #endif
3198 if (elementIndex == OSD_ITEM_COUNT) {
3199 elementIndex = 0;
3201 return elementIndex;
3204 void osdDrawNextElement(void)
3206 static uint8_t elementIndex = 0;
3207 // Prevent infinite loop when no elements are enabled
3208 uint8_t index = elementIndex;
3209 do {
3210 elementIndex = osdIncElementIndex(elementIndex);
3211 } while(!osdDrawSingleElement(elementIndex) && index != elementIndex);
3213 // Draw artificial horizon + tracking telemtry last
3214 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
3215 if (osdConfig()->telemetry>0){
3216 osdDisplayTelemetry();
3220 PG_RESET_TEMPLATE(osdConfig_t, osdConfig,
3221 .rssi_alarm = SETTING_OSD_RSSI_ALARM_DEFAULT,
3222 .time_alarm = SETTING_OSD_TIME_ALARM_DEFAULT,
3223 .alt_alarm = SETTING_OSD_ALT_ALARM_DEFAULT,
3224 .dist_alarm = SETTING_OSD_DIST_ALARM_DEFAULT,
3225 .neg_alt_alarm = SETTING_OSD_NEG_ALT_ALARM_DEFAULT,
3226 .current_alarm = SETTING_OSD_CURRENT_ALARM_DEFAULT,
3227 .imu_temp_alarm_min = SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT,
3228 .imu_temp_alarm_max = SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT,
3229 .esc_temp_alarm_min = SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT,
3230 .esc_temp_alarm_max = SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT,
3231 .gforce_alarm = SETTING_OSD_GFORCE_ALARM_DEFAULT,
3232 .gforce_axis_alarm_min = SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT,
3233 .gforce_axis_alarm_max = SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT,
3234 #ifdef USE_BARO
3235 .baro_temp_alarm_min = SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT,
3236 .baro_temp_alarm_max = SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT,
3237 #endif
3238 #ifdef USE_SERIALRX_CRSF
3239 .snr_alarm = SETTING_OSD_SNR_ALARM_DEFAULT,
3240 .crsf_lq_format = SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT,
3241 .link_quality_alarm = SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT,
3242 .rssi_dbm_alarm = SETTING_OSD_RSSI_DBM_ALARM_DEFAULT,
3243 .rssi_dbm_max = SETTING_OSD_RSSI_DBM_MAX_DEFAULT,
3244 .rssi_dbm_min = SETTING_OSD_RSSI_DBM_MIN_DEFAULT,
3245 #endif
3246 #ifdef USE_TEMPERATURE_SENSOR
3247 .temp_label_align = SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT,
3248 #endif
3249 #ifdef USE_PITOT
3250 .airspeed_alarm_min = SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT,
3251 .airspeed_alarm_max = SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT,
3252 #endif
3254 .video_system = SETTING_OSD_VIDEO_SYSTEM_DEFAULT,
3255 .row_shiftdown = SETTING_OSD_ROW_SHIFTDOWN_DEFAULT,
3257 .ahi_reverse_roll = SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT,
3258 .ahi_max_pitch = SETTING_OSD_AHI_MAX_PITCH_DEFAULT,
3259 .crosshairs_style = SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT,
3260 .horizon_offset = SETTING_OSD_HORIZON_OFFSET_DEFAULT,
3261 .camera_uptilt = SETTING_OSD_CAMERA_UPTILT_DEFAULT,
3262 .ahi_camera_uptilt_comp = SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT,
3263 .camera_fov_h = SETTING_OSD_CAMERA_FOV_H_DEFAULT,
3264 .camera_fov_v = SETTING_OSD_CAMERA_FOV_V_DEFAULT,
3265 .hud_margin_h = SETTING_OSD_HUD_MARGIN_H_DEFAULT,
3266 .hud_margin_v = SETTING_OSD_HUD_MARGIN_V_DEFAULT,
3267 .hud_homing = SETTING_OSD_HUD_HOMING_DEFAULT,
3268 .hud_homepoint = SETTING_OSD_HUD_HOMEPOINT_DEFAULT,
3269 .hud_radar_disp = SETTING_OSD_HUD_RADAR_DISP_DEFAULT,
3270 .hud_radar_range_min = SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT,
3271 .hud_radar_range_max = SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT,
3272 .hud_radar_alt_difference_display_time = SETTING_OSD_HUD_RADAR_ALT_DIFFERENCE_DISPLAY_TIME_DEFAULT,
3273 .hud_radar_distance_display_time = SETTING_OSD_HUD_RADAR_DISTANCE_DISPLAY_TIME_DEFAULT,
3274 .hud_wp_disp = SETTING_OSD_HUD_WP_DISP_DEFAULT,
3275 .left_sidebar_scroll = SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT,
3276 .right_sidebar_scroll = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT,
3277 .sidebar_scroll_arrows = SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT,
3278 .sidebar_horizontal_offset = SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT,
3279 .left_sidebar_scroll_step = SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT,
3280 .right_sidebar_scroll_step = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT,
3281 .sidebar_height = SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT,
3282 .osd_home_position_arm_screen = SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT,
3283 .pan_servo_index = SETTING_OSD_PAN_SERVO_INDEX_DEFAULT,
3284 .pan_servo_pwm2centideg = SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT,
3285 .esc_rpm_precision = SETTING_OSD_ESC_RPM_PRECISION_DEFAULT,
3286 .mAh_used_precision = SETTING_OSD_MAH_USED_PRECISION_DEFAULT,
3287 .osd_switch_indicator0_name = SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT,
3288 .osd_switch_indicator0_channel = SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNEL_DEFAULT,
3289 .osd_switch_indicator1_name = SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT,
3290 .osd_switch_indicator1_channel = SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNEL_DEFAULT,
3291 .osd_switch_indicator2_name = SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT,
3292 .osd_switch_indicator2_channel = SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNEL_DEFAULT,
3293 .osd_switch_indicator3_name = SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT,
3294 .osd_switch_indicator3_channel = SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNEL_DEFAULT,
3295 .osd_switch_indicators_align_left = SETTING_OSD_SWITCH_INDICATORS_ALIGN_LEFT_DEFAULT,
3296 .system_msg_display_time = SETTING_OSD_SYSTEM_MSG_DISPLAY_TIME_DEFAULT,
3297 .units = SETTING_OSD_UNITS_DEFAULT,
3298 .main_voltage_decimals = SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT,
3300 #ifdef USE_WIND_ESTIMATOR
3301 .estimations_wind_compensation = SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT,
3302 #endif
3304 .coordinate_digits = SETTING_OSD_COORDINATE_DIGITS_DEFAULT,
3306 .osd_failsafe_switch_layout = SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT,
3308 .plus_code_digits = SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT,
3309 .plus_code_short = SETTING_OSD_PLUS_CODE_SHORT_DEFAULT,
3311 .ahi_width = SETTING_OSD_AHI_WIDTH_DEFAULT,
3312 .ahi_height = SETTING_OSD_AHI_HEIGHT_DEFAULT,
3313 .ahi_vertical_offset = SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT,
3314 .ahi_bordered = SETTING_OSD_AHI_BORDERED_DEFAULT,
3315 .ahi_style = SETTING_OSD_AHI_STYLE_DEFAULT,
3317 .force_grid = SETTING_OSD_FORCE_GRID_DEFAULT,
3319 .stats_energy_unit = SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT,
3320 .stats_min_voltage_unit = SETTING_OSD_STATS_MIN_VOLTAGE_UNIT_DEFAULT,
3321 .stats_page_auto_swap_time = SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT
3324 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t *osdLayoutsConfig)
3326 osdLayoutsConfig->item_pos[0][OSD_ALTITUDE] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG;
3327 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_VOLTAGE] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG;
3328 osdLayoutsConfig->item_pos[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE] = OSD_POS(12, 1);
3330 osdLayoutsConfig->item_pos[0][OSD_RSSI_VALUE] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG;
3331 //line 2
3332 osdLayoutsConfig->item_pos[0][OSD_HOME_DIST] = OSD_POS(1, 1);
3333 osdLayoutsConfig->item_pos[0][OSD_TRIP_DIST] = OSD_POS(1, 2);
3334 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_CELL_VOLTAGE] = OSD_POS(12, 1);
3335 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE] = OSD_POS(12, 1);
3336 osdLayoutsConfig->item_pos[0][OSD_GPS_SPEED] = OSD_POS(23, 1);
3337 osdLayoutsConfig->item_pos[0][OSD_3D_SPEED] = OSD_POS(23, 1);
3338 osdLayoutsConfig->item_pos[0][OSD_GLIDESLOPE] = OSD_POS(23, 2);
3340 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG;
3341 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS_AUTO_THR] = OSD_POS(6, 2);
3342 osdLayoutsConfig->item_pos[0][OSD_HEADING] = OSD_POS(12, 2);
3343 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ERROR] = OSD_POS(12, 2);
3344 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ADJUSTMENT] = OSD_POS(12, 2);
3345 osdLayoutsConfig->item_pos[0][OSD_HEADING_GRAPH] = OSD_POS(18, 2);
3346 osdLayoutsConfig->item_pos[0][OSD_CURRENT_DRAW] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG;
3347 osdLayoutsConfig->item_pos[0][OSD_MAH_DRAWN] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG;
3348 osdLayoutsConfig->item_pos[0][OSD_WH_DRAWN] = OSD_POS(1, 5);
3349 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_CAPACITY] = OSD_POS(1, 6);
3350 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_PERCENT] = OSD_POS(1, 7);
3351 osdLayoutsConfig->item_pos[0][OSD_POWER_SUPPLY_IMPEDANCE] = OSD_POS(1, 8);
3353 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_MAH_PER_KM] = OSD_POS(1, 5);
3354 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_WH_PER_KM] = OSD_POS(1, 5);
3356 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_ROLL] = OSD_POS(1, 7);
3357 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_PITCH] = OSD_POS(1, 8);
3359 // avoid OSD_VARIO under OSD_CROSSHAIRS
3360 osdLayoutsConfig->item_pos[0][OSD_VARIO] = OSD_POS(23, 5);
3361 // OSD_VARIO_NUM at the right of OSD_VARIO
3362 osdLayoutsConfig->item_pos[0][OSD_VARIO_NUM] = OSD_POS(24, 7);
3363 osdLayoutsConfig->item_pos[0][OSD_HOME_DIR] = OSD_POS(14, 11);
3364 osdLayoutsConfig->item_pos[0][OSD_ARTIFICIAL_HORIZON] = OSD_POS(8, 6);
3365 osdLayoutsConfig->item_pos[0][OSD_HORIZON_SIDEBARS] = OSD_POS(8, 6);
3367 osdLayoutsConfig->item_pos[0][OSD_CRAFT_NAME] = OSD_POS(20, 2);
3368 osdLayoutsConfig->item_pos[0][OSD_VTX_CHANNEL] = OSD_POS(8, 6);
3370 #ifdef USE_SERIALRX_CRSF
3371 osdLayoutsConfig->item_pos[0][OSD_CRSF_RSSI_DBM] = OSD_POS(23, 12);
3372 osdLayoutsConfig->item_pos[0][OSD_CRSF_LQ] = OSD_POS(23, 11);
3373 osdLayoutsConfig->item_pos[0][OSD_CRSF_SNR_DB] = OSD_POS(24, 9);
3374 osdLayoutsConfig->item_pos[0][OSD_CRSF_TX_POWER] = OSD_POS(24, 10);
3375 #endif
3377 osdLayoutsConfig->item_pos[0][OSD_ONTIME] = OSD_POS(23, 8);
3378 osdLayoutsConfig->item_pos[0][OSD_FLYTIME] = OSD_POS(23, 9);
3379 osdLayoutsConfig->item_pos[0][OSD_ONTIME_FLYTIME] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG;
3380 osdLayoutsConfig->item_pos[0][OSD_RTC_TIME] = OSD_POS(23, 12);
3381 osdLayoutsConfig->item_pos[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH] = OSD_POS(23, 7);
3382 osdLayoutsConfig->item_pos[0][OSD_REMAINING_DISTANCE_BEFORE_RTH] = OSD_POS(23, 6);
3384 osdLayoutsConfig->item_pos[0][OSD_MISSION] = OSD_POS(0, 10);
3385 osdLayoutsConfig->item_pos[0][OSD_GPS_SATS] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG;
3386 osdLayoutsConfig->item_pos[0][OSD_GPS_HDOP] = OSD_POS(0, 10);
3388 osdLayoutsConfig->item_pos[0][OSD_GPS_LAT] = OSD_POS(0, 12);
3389 // Put this on top of the latitude, since it's very unlikely
3390 // that users will want to use both at the same time.
3391 osdLayoutsConfig->item_pos[0][OSD_PLUS_CODE] = OSD_POS(0, 12);
3392 osdLayoutsConfig->item_pos[0][OSD_FLYMODE] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG;
3393 osdLayoutsConfig->item_pos[0][OSD_GPS_LON] = OSD_POS(18, 12);
3395 osdLayoutsConfig->item_pos[0][OSD_AZIMUTH] = OSD_POS(2, 12);
3397 osdLayoutsConfig->item_pos[0][OSD_ROLL_PIDS] = OSD_POS(2, 10);
3398 osdLayoutsConfig->item_pos[0][OSD_PITCH_PIDS] = OSD_POS(2, 11);
3399 osdLayoutsConfig->item_pos[0][OSD_YAW_PIDS] = OSD_POS(2, 12);
3400 osdLayoutsConfig->item_pos[0][OSD_LEVEL_PIDS] = OSD_POS(2, 12);
3401 osdLayoutsConfig->item_pos[0][OSD_POS_XY_PIDS] = OSD_POS(2, 12);
3402 osdLayoutsConfig->item_pos[0][OSD_POS_Z_PIDS] = OSD_POS(2, 12);
3403 osdLayoutsConfig->item_pos[0][OSD_VEL_XY_PIDS] = OSD_POS(2, 12);
3404 osdLayoutsConfig->item_pos[0][OSD_VEL_Z_PIDS] = OSD_POS(2, 12);
3405 osdLayoutsConfig->item_pos[0][OSD_HEADING_P] = OSD_POS(2, 12);
3406 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_ROLL] = OSD_POS(2, 10);
3407 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_PITCH] = OSD_POS(2, 11);
3408 osdLayoutsConfig->item_pos[0][OSD_RC_EXPO] = OSD_POS(2, 12);
3409 osdLayoutsConfig->item_pos[0][OSD_RC_YAW_EXPO] = OSD_POS(2, 12);
3410 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_EXPO] = OSD_POS(2, 12);
3411 osdLayoutsConfig->item_pos[0][OSD_PITCH_RATE] = OSD_POS(2, 12);
3412 osdLayoutsConfig->item_pos[0][OSD_ROLL_RATE] = OSD_POS(2, 12);
3413 osdLayoutsConfig->item_pos[0][OSD_YAW_RATE] = OSD_POS(2, 12);
3414 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_EXPO] = OSD_POS(2, 12);
3415 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_YAW_EXPO] = OSD_POS(2, 12);
3416 osdLayoutsConfig->item_pos[0][OSD_MANUAL_PITCH_RATE] = OSD_POS(2, 12);
3417 osdLayoutsConfig->item_pos[0][OSD_MANUAL_ROLL_RATE] = OSD_POS(2, 12);
3418 osdLayoutsConfig->item_pos[0][OSD_MANUAL_YAW_RATE] = OSD_POS(2, 12);
3419 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_CRUISE_THR] = OSD_POS(2, 12);
3420 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_PITCH2THR] = OSD_POS(2, 12);
3421 osdLayoutsConfig->item_pos[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE] = OSD_POS(2, 12);
3422 osdLayoutsConfig->item_pos[0][OSD_FW_ALT_PID_OUTPUTS] = OSD_POS(2, 12);
3423 osdLayoutsConfig->item_pos[0][OSD_FW_POS_PID_OUTPUTS] = OSD_POS(2, 12);
3424 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_X_PID_OUTPUTS] = OSD_POS(2, 12);
3425 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Y_PID_OUTPUTS] = OSD_POS(2, 12);
3426 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Z_PID_OUTPUTS] = OSD_POS(2, 12);
3427 osdLayoutsConfig->item_pos[0][OSD_MC_POS_XYZ_P_OUTPUTS] = OSD_POS(2, 12);
3429 osdLayoutsConfig->item_pos[0][OSD_POWER] = OSD_POS(15, 1);
3431 osdLayoutsConfig->item_pos[0][OSD_IMU_TEMPERATURE] = OSD_POS(19, 2);
3432 osdLayoutsConfig->item_pos[0][OSD_BARO_TEMPERATURE] = OSD_POS(19, 3);
3433 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_0_TEMPERATURE] = OSD_POS(19, 4);
3434 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_1_TEMPERATURE] = OSD_POS(19, 5);
3435 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_2_TEMPERATURE] = OSD_POS(19, 6);
3436 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_3_TEMPERATURE] = OSD_POS(19, 7);
3437 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_4_TEMPERATURE] = OSD_POS(19, 8);
3438 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_5_TEMPERATURE] = OSD_POS(19, 9);
3439 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_6_TEMPERATURE] = OSD_POS(19, 10);
3440 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_7_TEMPERATURE] = OSD_POS(19, 11);
3442 osdLayoutsConfig->item_pos[0][OSD_AIR_SPEED] = OSD_POS(3, 5);
3443 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_HORIZONTAL] = OSD_POS(3, 6);
3444 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_VERTICAL] = OSD_POS(3, 7);
3446 osdLayoutsConfig->item_pos[0][OSD_GFORCE] = OSD_POS(12, 4);
3447 osdLayoutsConfig->item_pos[0][OSD_GFORCE_X] = OSD_POS(12, 5);
3448 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Y] = OSD_POS(12, 6);
3449 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Z] = OSD_POS(12, 7);
3451 osdLayoutsConfig->item_pos[0][OSD_VTX_POWER] = OSD_POS(3, 5);
3453 osdLayoutsConfig->item_pos[0][OSD_GVAR_0] = OSD_POS(1, 1);
3454 osdLayoutsConfig->item_pos[0][OSD_GVAR_1] = OSD_POS(1, 2);
3455 osdLayoutsConfig->item_pos[0][OSD_GVAR_2] = OSD_POS(1, 3);
3456 osdLayoutsConfig->item_pos[0][OSD_GVAR_3] = OSD_POS(1, 4);
3458 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_0] = OSD_POS(2, 7);
3459 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_1] = OSD_POS(2, 8);
3460 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_2] = OSD_POS(2, 9);
3461 osdLayoutsConfig->item_pos[0][OSD_SWITCH_INDICATOR_3] = OSD_POS(2, 10);
3463 #if defined(USE_ESC_SENSOR)
3464 osdLayoutsConfig->item_pos[0][OSD_ESC_RPM] = OSD_POS(1, 2);
3465 osdLayoutsConfig->item_pos[0][OSD_ESC_TEMPERATURE] = OSD_POS(1, 3);
3466 #endif
3468 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3469 osdLayoutsConfig->item_pos[0][OSD_RC_SOURCE] = OSD_POS(3, 4);
3470 #endif
3472 #ifdef USE_POWER_LIMITS
3473 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_REMAINING_BURST_TIME] = OSD_POS(3, 4);
3474 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT] = OSD_POS(3, 5);
3475 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT] = OSD_POS(3, 6);
3476 #endif
3478 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
3479 osdLayoutsConfig->item_pos[0][OSD_MESSAGES] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG;
3481 for (unsigned ii = 1; ii < OSD_LAYOUT_COUNT; ii++) {
3482 for (unsigned jj = 0; jj < ARRAYLEN(osdLayoutsConfig->item_pos[0]); jj++) {
3483 osdLayoutsConfig->item_pos[ii][jj] = osdLayoutsConfig->item_pos[0][jj] & ~OSD_VISIBLE_FLAG;
3488 static void osdSetNextRefreshIn(uint32_t timeMs) {
3489 resumeRefreshAt = micros() + timeMs * 1000;
3490 refreshWaitForResumeCmdRelease = true;
3493 static void osdCompleteAsyncInitialization(void)
3495 if (!displayIsReady(osdDisplayPort)) {
3496 // Update the display.
3497 // XXX: Rename displayDrawScreen() and associated functions
3498 // to displayUpdate()
3499 displayDrawScreen(osdDisplayPort);
3500 return;
3503 osdDisplayIsReady = true;
3505 #if defined(USE_CANVAS)
3506 if (osdConfig()->force_grid) {
3507 osdDisplayHasCanvas = false;
3508 } else {
3509 osdDisplayHasCanvas = displayGetCanvas(&osdCanvas, osdDisplayPort);
3511 #endif
3513 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3514 displayClearScreen(osdDisplayPort);
3516 uint8_t y = 1;
3517 displayFontMetadata_t metadata;
3518 bool fontHasMetadata = displayGetFontMetadata(&metadata, osdDisplayPort);
3519 LOG_D(OSD, "Font metadata version %s: %u (%u chars)",
3520 fontHasMetadata ? "Y" : "N", metadata.version, metadata.charCount);
3522 if (fontHasMetadata && metadata.charCount > 256) {
3523 hasExtendedFont = true;
3524 unsigned logo_c = SYM_LOGO_START;
3525 unsigned logo_x = OSD_CENTER_LEN(SYM_LOGO_WIDTH);
3526 for (unsigned ii = 0; ii < SYM_LOGO_HEIGHT; ii++) {
3527 for (unsigned jj = 0; jj < SYM_LOGO_WIDTH; jj++) {
3528 displayWriteChar(osdDisplayPort, logo_x + jj, y, logo_c++);
3530 y++;
3532 y++;
3533 } else if (!fontHasMetadata) {
3534 const char *m = "INVALID FONT";
3535 displayWrite(osdDisplayPort, OSD_CENTER_S(m), 3, m);
3536 y = 4;
3539 if (fontHasMetadata && metadata.version < OSD_MIN_FONT_VERSION) {
3540 const char *m = "INVALID FONT VERSION";
3541 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
3544 char string_buffer[30];
3545 tfp_sprintf(string_buffer, "INAV VERSION: %s", FC_VERSION_STRING);
3546 uint8_t xPos = osdDisplayIsHD() ? 7 : 5;
3547 displayWrite(osdDisplayPort, xPos, y++, string_buffer);
3548 #ifdef USE_CMS
3549 displayWrite(osdDisplayPort, xPos+2, y++, CMS_STARTUP_HELP_TEXT1);
3550 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT2);
3551 displayWrite(osdDisplayPort, xPos+6, y++, CMS_STARTUP_HELP_TEXT3);
3552 #endif
3553 #ifdef USE_STATS
3556 uint8_t statNameX = osdDisplayIsHD() ? 7 : 4;
3557 uint8_t statValueX = osdDisplayIsHD() ? 27 : 24;
3559 if (statsConfig()->stats_enabled) {
3560 displayWrite(osdDisplayPort, statNameX, ++y, "ODOMETER:");
3561 switch (osdConfig()->units) {
3562 case OSD_UNIT_UK:
3563 FALLTHROUGH;
3564 case OSD_UNIT_IMPERIAL:
3565 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_MILE));
3566 string_buffer[5] = SYM_MI;
3567 break;
3568 default:
3569 case OSD_UNIT_GA:
3570 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE));
3571 string_buffer[5] = SYM_NM;
3572 break;
3573 case OSD_UNIT_METRIC_MPH:
3574 FALLTHROUGH;
3575 case OSD_UNIT_METRIC:
3576 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER));
3577 string_buffer[5] = SYM_KM;
3578 break;
3580 string_buffer[6] = '\0';
3581 displayWrite(osdDisplayPort, statValueX-5, y, string_buffer);
3583 displayWrite(osdDisplayPort, statNameX, ++y, "TOTAL TIME:");
3584 uint32_t tot_mins = statsConfig()->stats_total_time / 60;
3585 tfp_sprintf(string_buffer, "%2d:%02dHM", (int)(tot_mins / 60), (int)(tot_mins % 60));
3586 displayWrite(osdDisplayPort, statValueX-5, y, string_buffer);
3588 #ifdef USE_ADC
3589 if (feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER)) {
3590 displayWrite(osdDisplayPort, statNameX, ++y, "TOTAL ENERGY:");
3591 osdFormatCentiNumber(string_buffer, statsConfig()->stats_total_energy / 10, 0, 2, 0, 4);
3592 strcat(string_buffer, "\xAB"); // SYM_WH
3593 displayWrite(osdDisplayPort, statValueX-4, y, string_buffer);
3595 displayWrite(osdDisplayPort, statNameX, ++y, "AVG EFFICIENCY:");
3596 if (statsConfig()->stats_total_dist) {
3597 uint32_t avg_efficiency = statsConfig()->stats_total_energy / (statsConfig()->stats_total_dist / METERS_PER_KILOMETER); // mWh/km
3598 switch (osdConfig()->units) {
3599 case OSD_UNIT_UK:
3600 FALLTHROUGH;
3601 case OSD_UNIT_IMPERIAL:
3602 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3603 string_buffer[3] = SYM_WH_MI;
3604 break;
3605 case OSD_UNIT_GA:
3606 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3607 string_buffer[3] = SYM_WH_NM;
3608 break;
3609 default:
3610 case OSD_UNIT_METRIC_MPH:
3611 FALLTHROUGH;
3612 case OSD_UNIT_METRIC:
3613 osdFormatCentiNumber(string_buffer, avg_efficiency / 10000 * METERS_PER_MILE, 0, 2, 0, 3);
3614 string_buffer[3] = SYM_WH_KM;
3615 break;
3617 } else {
3618 string_buffer[0] = string_buffer[1] = string_buffer[2] = '-';
3620 string_buffer[4] = '\0';
3621 displayWrite(osdDisplayPort, statValueX-3, y, string_buffer);
3623 #endif // USE_ADC
3625 #endif
3627 displayCommitTransaction(osdDisplayPort);
3628 displayResync(osdDisplayPort);
3629 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME);
3632 void osdInit(displayPort_t *osdDisplayPortToUse)
3634 if (!osdDisplayPortToUse)
3635 return;
3637 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(63,63));
3639 osdDisplayPort = osdDisplayPortToUse;
3641 #ifdef USE_CMS
3642 cmsDisplayPortRegister(osdDisplayPort);
3643 #endif
3645 armState = ARMING_FLAG(ARMED);
3646 osdCompleteAsyncInitialization();
3649 static void osdResetStats(void)
3651 stats.max_current = 0;
3652 stats.max_power = 0;
3653 stats.max_speed = 0;
3654 stats.max_3D_speed = 0;
3655 stats.max_air_speed = 0;
3656 stats.min_voltage = 5000;
3657 stats.min_rssi = 99;
3658 stats.min_lq = 300;
3659 stats.min_rssi_dbm = 0;
3660 stats.max_altitude = 0;
3663 static void osdUpdateStats(void)
3665 int32_t value;
3667 if (feature(FEATURE_GPS)) {
3668 value = osdGet3DSpeed();
3669 if (stats.max_3D_speed < value)
3670 stats.max_3D_speed = value;
3672 if (stats.max_speed < gpsSol.groundSpeed)
3673 stats.max_speed = gpsSol.groundSpeed;
3675 if (stats.max_air_speed < pitot.airSpeed)
3676 stats.max_air_speed = pitot.airSpeed;
3678 if (stats.max_distance < GPS_distanceToHome)
3679 stats.max_distance = GPS_distanceToHome;
3682 value = getBatteryVoltage();
3683 if (stats.min_voltage > value)
3684 stats.min_voltage = value;
3686 value = abs(getAmperage());
3687 if (stats.max_current < value)
3688 stats.max_current = value;
3690 value = labs(getPower());
3691 if (stats.max_power < value)
3692 stats.max_power = value;
3694 value = osdConvertRSSI();
3695 if (stats.min_rssi > value)
3696 stats.min_rssi = value;
3698 value = osdGetCrsfLQ();
3699 if (stats.min_lq > value)
3700 stats.min_lq = value;
3702 if (!failsafeIsReceivingRxData())
3703 stats.min_lq = 0;
3705 value = osdGetCrsfdBm();
3706 if (stats.min_rssi_dbm > value)
3707 stats.min_rssi_dbm = value;
3709 stats.max_altitude = MAX(stats.max_altitude, osdGetAltitude());
3712 static void osdShowStatsPage1(void)
3714 const char * disarmReasonStr[DISARM_REASON_COUNT] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "KILLSW", "FAILSAFE", "NAV SYS", "LANDING"};
3715 uint8_t top = 1; /* first fully visible line */
3716 const uint8_t statNameX = osdDisplayIsHD() ? 7 : 1;
3717 const uint8_t statValuesX = osdDisplayIsHD() ? 33 : 20;
3718 char buff[10];
3719 statsPagesCheck = 1;
3721 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3722 displayClearScreen(osdDisplayPort);
3724 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- 1/2 ->");
3726 if (feature(FEATURE_GPS)) {
3727 displayWrite(osdDisplayPort, statNameX, top, "MAX SPEED :");
3728 osdFormatVelocityStr(buff, stats.max_3D_speed, true, false);
3729 osdLeftAlignString(buff);
3730 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3732 displayWrite(osdDisplayPort, statNameX, top, "AVG SPEED :");
3733 osdGenerateAverageVelocityStr(buff);
3734 osdLeftAlignString(buff);
3735 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3737 displayWrite(osdDisplayPort, statNameX, top, "MAX DISTANCE :");
3738 osdFormatDistanceStr(buff, stats.max_distance*100);
3739 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3741 displayWrite(osdDisplayPort, statNameX, top, "TRAVELED DISTANCE:");
3742 osdFormatDistanceStr(buff, getTotalTravelDistance());
3743 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3746 displayWrite(osdDisplayPort, statNameX, top, "MAX ALTITUDE :");
3747 osdFormatAltitudeStr(buff, stats.max_altitude);
3748 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3750 switch (rxConfig()->serialrx_provider) {
3751 case SERIALRX_CRSF:
3752 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI % :");
3753 itoa(stats.min_rssi, buff, 10);
3754 strcat(buff, "%");
3755 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3757 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI DBM :");
3758 itoa(stats.min_rssi_dbm, buff, 10);
3759 tfp_sprintf(buff, "%s%c", buff, SYM_DBM);
3760 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3762 displayWrite(osdDisplayPort, statNameX, top, "MIN LQ :");
3763 itoa(stats.min_lq, buff, 10);
3764 strcat(buff, "%");
3765 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3766 break;
3767 default:
3768 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI :");
3769 itoa(stats.min_rssi, buff, 10);
3770 strcat(buff, "%");
3771 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3774 displayWrite(osdDisplayPort, statNameX, top, "FLY TIME :");
3775 uint16_t flySeconds = getFlightTime();
3776 uint16_t flyMinutes = flySeconds / 60;
3777 flySeconds %= 60;
3778 uint16_t flyHours = flyMinutes / 60;
3779 flyMinutes %= 60;
3780 tfp_sprintf(buff, "%02u:%02u:%02u", flyHours, flyMinutes, flySeconds);
3781 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3783 displayWrite(osdDisplayPort, statNameX, top, "DISARMED BY :");
3784 displayWrite(osdDisplayPort, statValuesX, top++, disarmReasonStr[getDisarmReason()]);
3785 displayCommitTransaction(osdDisplayPort);
3788 static void osdShowStatsPage2(void)
3790 uint8_t top = 1; /* first fully visible line */
3791 const uint8_t statNameX = osdDisplayIsHD() ? 7 : 1;
3792 const uint8_t statValuesX = osdDisplayIsHD() ? 33 : 20;
3793 char buff[10];
3794 statsPagesCheck = 1;
3796 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3797 displayClearScreen(osdDisplayPort);
3799 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- <- 2/2");
3801 if (osdConfig()->stats_min_voltage_unit == OSD_STATS_MIN_VOLTAGE_UNIT_BATTERY) {
3802 displayWrite(osdDisplayPort, statNameX, top, "MIN BATTERY VOLT :");
3803 osdFormatCentiNumber(buff, stats.min_voltage, 0, osdConfig()->main_voltage_decimals, 0, osdConfig()->main_voltage_decimals + 2);
3804 } else {
3805 displayWrite(osdDisplayPort, statNameX, top, "MIN CELL VOLTAGE :");
3806 osdFormatCentiNumber(buff, stats.min_voltage/getBatteryCellCount(), 0, 2, 0, 3);
3808 tfp_sprintf(buff, "%s%c", buff, SYM_VOLT);
3809 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3811 if (feature(FEATURE_CURRENT_METER)) {
3812 displayWrite(osdDisplayPort, statNameX, top, "MAX CURRENT :");
3813 osdFormatCentiNumber(buff, stats.max_current, 0, 2, 0, 3);
3814 tfp_sprintf(buff, "%s%c", buff, SYM_AMP);
3815 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3817 displayWrite(osdDisplayPort, statNameX, top, "MAX POWER :");
3818 bool kiloWatt = osdFormatCentiNumber(buff, stats.max_power, 1000, 2, 2, 3);
3819 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3820 buff[4] = '\0';
3821 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3823 displayWrite(osdDisplayPort, statNameX, top, "USED CAPACITY :");
3824 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3825 tfp_sprintf(buff, "%d%c", (int)getMAhDrawn(), SYM_MAH);
3826 } else {
3827 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
3828 tfp_sprintf(buff, "%s%c", buff, SYM_WH);
3830 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3832 int32_t totalDistance = getTotalTravelDistance();
3833 bool moreThanAh = false;
3834 bool efficiencyValid = totalDistance >= 10000;
3835 if (feature(FEATURE_GPS)) {
3836 displayWrite(osdDisplayPort, statNameX, top, "AVG EFFICIENCY :");
3837 switch (osdConfig()->units) {
3838 case OSD_UNIT_UK:
3839 FALLTHROUGH;
3840 case OSD_UNIT_IMPERIAL:
3841 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3842 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, 3);
3843 if (!moreThanAh) {
3844 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
3845 } else {
3846 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
3848 if (!efficiencyValid) {
3849 buff[0] = buff[1] = buff[2] = '-';
3850 buff[3] = SYM_MAH_MI_0;
3851 buff[4] = SYM_MAH_MI_1;
3852 buff[5] = '\0';
3854 } else {
3855 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, 3);
3856 tfp_sprintf(buff, "%s%c", buff, SYM_WH_MI);
3857 if (!efficiencyValid) {
3858 buff[0] = buff[1] = buff[2] = '-';
3861 break;
3862 case OSD_UNIT_GA:
3863 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3864 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, 3);
3865 if (!moreThanAh) {
3866 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
3867 } else {
3868 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
3870 if (!efficiencyValid) {
3871 buff[0] = buff[1] = buff[2] = '-';
3872 buff[3] = SYM_MAH_NM_0;
3873 buff[4] = SYM_MAH_NM_1;
3874 buff[5] = '\0';
3876 } else {
3877 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, 3);
3878 tfp_sprintf(buff, "%s%c", buff, SYM_WH_NM);
3879 if (!efficiencyValid) {
3880 buff[0] = buff[1] = buff[2] = '-';
3883 break;
3884 case OSD_UNIT_METRIC_MPH:
3885 FALLTHROUGH;
3886 case OSD_UNIT_METRIC:
3887 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3888 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000000.0f / totalDistance), 1000, 0, 2, 3);
3889 if (!moreThanAh) {
3890 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
3891 } else {
3892 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
3894 if (!efficiencyValid) {
3895 buff[0] = buff[1] = buff[2] = '-';
3896 buff[3] = SYM_MAH_KM_0;
3897 buff[4] = SYM_MAH_KM_1;
3898 buff[5] = '\0';
3900 } else {
3901 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10000.0f / totalDistance), 0, 2, 0, 3);
3902 tfp_sprintf(buff, "%s%c", buff, SYM_WH_KM);
3903 if (!efficiencyValid) {
3904 buff[0] = buff[1] = buff[2] = '-';
3907 break;
3909 osdLeftAlignString(buff);
3910 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3914 const float max_gforce = accGetMeasuredMaxG();
3915 displayWrite(osdDisplayPort, statNameX, top, "MAX G-FORCE :");
3916 osdFormatCentiNumber(buff, max_gforce * 100, 0, 2, 0, 3);
3917 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3919 const acc_extremes_t *acc_extremes = accGetMeasuredExtremes();
3920 displayWrite(osdDisplayPort, statNameX, top, "MIN/MAX Z G-FORCE:");
3921 osdFormatCentiNumber(buff, acc_extremes[Z].min * 100, 0, 2, 0, 4);
3922 strcat(buff,"/");
3923 displayWrite(osdDisplayPort, statValuesX - 1, top, buff);
3924 osdFormatCentiNumber(buff, acc_extremes[Z].max * 100, 0, 2, 0, 3);
3925 displayWrite(osdDisplayPort, statValuesX + 4, top++, buff);
3926 displayCommitTransaction(osdDisplayPort);
3929 // called when motors armed
3930 static void osdShowArmed(void)
3932 dateTime_t dt;
3933 char buf[MAX(32, FORMATTED_DATE_TIME_BUFSIZE)];
3934 char craftNameBuf[MAX_NAME_LENGTH];
3935 char versionBuf[30];
3936 char *date;
3937 char *time;
3938 // We need 12 visible rows, start row never < first fully visible row 1
3939 uint8_t y = osdDisplayPort->rows > 13 ? (osdDisplayPort->rows - 12) / 2 : 1;
3941 displayClearScreen(osdDisplayPort);
3942 strcpy(buf, "ARMED");
3943 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
3944 y += 2;
3946 if (strlen(systemConfig()->name) > 0) {
3947 osdFormatCraftName(craftNameBuf);
3948 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(systemConfig() -> name)) / 2, y, craftNameBuf );
3949 y += 1;
3951 if (posControl.waypointListValid && posControl.waypointCount > 0) {
3952 #ifdef USE_MULTI_MISSION
3953 tfp_sprintf(buf, "MISSION %u/%u (%u WP)", posControl.loadedMultiMissionIndex, posControl.multiMissionCount, posControl.waypointCount);
3954 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
3955 #else
3956 strcpy(buf, "*MISSION LOADED*");
3957 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
3958 #endif
3960 y += 1;
3962 #if defined(USE_GPS)
3963 if (feature(FEATURE_GPS)) {
3964 if (STATE(GPS_FIX_HOME)) {
3965 if (osdConfig()->osd_home_position_arm_screen){
3966 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
3967 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
3968 osdFormatCoordinate(buf, SYM_LON, GPS_home.lon);
3969 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 1, buf);
3970 int digits = osdConfig()->plus_code_digits;
3971 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
3972 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 2, buf);
3974 y += 4;
3975 #if defined (USE_SAFE_HOME)
3976 if (safehome_distance) { // safehome found during arming
3977 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
3978 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
3979 } else {
3980 char buf2[12]; // format the distance first
3981 osdFormatDistanceStr(buf2, safehome_distance);
3982 tfp_sprintf(buf, "%c - %s -> SAFEHOME %u", SYM_HOME, buf2, safehome_index);
3984 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
3985 // write this message above the ARMED message to make it obvious
3986 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y - 8, buf, elemAttr);
3988 #endif
3989 } else {
3990 strcpy(buf, "!NO HOME POSITION!");
3991 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
3992 y += 1;
3995 #endif
3997 if (rtcGetDateTime(&dt)) {
3998 dateTimeFormatLocal(buf, &dt);
3999 dateTimeSplitFormatted(buf, &date, &time);
4001 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(date)) / 2, y, date);
4002 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(time)) / 2, y + 1, time);
4003 y += 3;
4006 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
4007 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, y, versionBuf);
4010 static void osdFilterData(timeUs_t currentTimeUs) {
4011 static timeUs_t lastRefresh = 0;
4012 float refresh_dT = US2S(cmpTimeUs(currentTimeUs, lastRefresh));
4014 GForce = fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF)) / GRAVITY_MSS;
4015 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) GForceAxis[axis] = imuMeasuredAccelBF.v[axis] / GRAVITY_MSS;
4017 if (lastRefresh) {
4018 GForce = pt1FilterApply3(&GForceFilter, GForce, refresh_dT);
4019 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) pt1FilterApply3(GForceFilterAxis + axis, GForceAxis[axis], refresh_dT);
4020 } else {
4021 pt1FilterInitRC(&GForceFilter, GFORCE_FILTER_TC, 0);
4022 pt1FilterReset(&GForceFilter, GForce);
4024 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) {
4025 pt1FilterInitRC(GForceFilterAxis + axis, GFORCE_FILTER_TC, 0);
4026 pt1FilterReset(GForceFilterAxis + axis, GForceAxis[axis]);
4030 lastRefresh = currentTimeUs;
4033 static void osdRefresh(timeUs_t currentTimeUs)
4035 osdFilterData(currentTimeUs);
4037 #ifdef USE_CMS
4038 if (IS_RC_MODE_ACTIVE(BOXOSD) && (!cmsInMenu) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4039 #else
4040 if (IS_RC_MODE_ACTIVE(BOXOSD) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
4041 #endif
4042 displayClearScreen(osdDisplayPort);
4043 armState = ARMING_FLAG(ARMED);
4044 return;
4047 // detect arm/disarm
4048 static uint8_t statsPageAutoSwapCntl = 2;
4049 if (armState != ARMING_FLAG(ARMED)) {
4050 if (ARMING_FLAG(ARMED)) {
4051 osdResetStats();
4052 statsPageAutoSwapCntl = 2;
4053 osdShowArmed(); // reset statistic etc
4054 uint32_t delay = ARMED_SCREEN_DISPLAY_TIME;
4055 statsPagesCheck = 0;
4056 #if defined(USE_SAFE_HOME)
4057 if (safehome_distance)
4058 delay *= 3;
4059 #endif
4060 osdSetNextRefreshIn(delay);
4061 } else {
4062 osdShowStatsPage1(); // show first page of statistics
4063 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME);
4064 statsPageAutoSwapCntl = osdConfig()->stats_page_auto_swap_time > 0 ? 0 : 2; // disable swapping pages when time = 0
4067 armState = ARMING_FLAG(ARMED);
4070 if (resumeRefreshAt) {
4071 // If we already reached he time for the next refresh,
4072 // or THR is high or PITCH is high, resume refreshing.
4073 // Clear the screen first to erase other elements which
4074 // might have been drawn while the OSD wasn't refreshing.
4076 // auto swap stats pages when first shown
4077 // auto swap cancelled using roll stick
4078 if (statsPageAutoSwapCntl != 2) {
4079 if (STATS_PAGE1 || STATS_PAGE2) {
4080 statsPageAutoSwapCntl = 2;
4081 } else {
4082 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time * 1000), 2)) {
4083 if (statsPageAutoSwapCntl == 0) {
4084 osdShowStatsPage1();
4085 statsPageAutoSwapCntl = 1;
4087 } else {
4088 if (statsPageAutoSwapCntl == 1) {
4089 osdShowStatsPage2();
4090 statsPageAutoSwapCntl = 0;
4096 if (!DELAYED_REFRESH_RESUME_COMMAND)
4097 refreshWaitForResumeCmdRelease = false;
4099 if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && DELAYED_REFRESH_RESUME_COMMAND)) {
4100 displayClearScreen(osdDisplayPort);
4101 resumeRefreshAt = 0;
4102 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE1)) {
4103 if (statsPagesCheck == 1) {
4104 osdShowStatsPage1();
4106 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE2)) {
4107 if (statsPagesCheck == 1) {
4108 osdShowStatsPage2();
4110 } else {
4111 displayHeartbeat(osdDisplayPort);
4113 return;
4116 #ifdef USE_CMS
4117 if (!displayIsGrabbed(osdDisplayPort)) {
4118 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
4119 if (fullRedraw) {
4120 displayClearScreen(osdDisplayPort);
4121 fullRedraw = false;
4123 osdDrawNextElement();
4124 displayHeartbeat(osdDisplayPort);
4125 displayCommitTransaction(osdDisplayPort);
4126 #ifdef OSD_CALLS_CMS
4127 } else {
4128 cmsUpdate(currentTimeUs);
4129 #endif
4131 #endif
4135 * Called periodically by the scheduler
4137 void osdUpdate(timeUs_t currentTimeUs)
4139 static uint32_t counter = 0;
4141 // don't touch buffers if DMA transaction is in progress
4142 if (displayIsTransferInProgress(osdDisplayPort)) {
4143 return;
4146 if (!osdDisplayIsReady) {
4147 osdCompleteAsyncInitialization();
4148 return;
4151 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
4152 // Check if the layout has changed. Higher numbered
4153 // boxes take priority.
4154 unsigned activeLayout;
4155 if (layoutOverride >= 0) {
4156 activeLayout = layoutOverride;
4157 // Check for timed override, it will go into effect on
4158 // the next OSD iteration
4159 if (layoutOverrideUntil > 0 && millis() > layoutOverrideUntil) {
4160 layoutOverrideUntil = 0;
4161 layoutOverride = -1;
4163 } else if (osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE)) {
4164 activeLayout = 0;
4165 } else {
4166 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
4167 if (IS_RC_MODE_ACTIVE(BOXOSDALT3))
4168 activeLayout = 3;
4169 else
4170 #endif
4171 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
4172 if (IS_RC_MODE_ACTIVE(BOXOSDALT2))
4173 activeLayout = 2;
4174 else
4175 #endif
4176 if (IS_RC_MODE_ACTIVE(BOXOSDALT1))
4177 activeLayout = 1;
4178 else
4179 #ifdef USE_PROGRAMMING_FRAMEWORK
4180 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT))
4181 activeLayout = constrain(logicConditionValuesByType[LOGIC_CONDITION_SET_OSD_LAYOUT], 0, OSD_ALTERNATE_LAYOUT_COUNT);
4182 else
4183 #endif
4184 activeLayout = 0;
4186 if (currentLayout != activeLayout) {
4187 currentLayout = activeLayout;
4188 osdStartFullRedraw();
4190 #endif
4192 #define DRAW_FREQ_DENOM 4
4193 #define STATS_FREQ_DENOM 50
4194 counter++;
4196 if ((counter % STATS_FREQ_DENOM) == 0) {
4197 osdUpdateStats();
4200 if ((counter % DRAW_FREQ_DENOM) == 0) {
4201 // redraw values in buffer
4202 osdRefresh(currentTimeUs);
4203 } else {
4204 // rest of time redraw screen
4205 displayDrawScreen(osdDisplayPort);
4208 #ifdef USE_CMS
4209 // do not allow ARM if we are in menu
4210 if (displayIsGrabbed(osdDisplayPort)) {
4211 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4212 } else {
4213 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
4215 #endif
4218 void osdStartFullRedraw(void)
4220 fullRedraw = true;
4223 void osdOverrideLayout(int layout, timeMs_t duration)
4225 layoutOverride = constrain(layout, -1, ARRAYLEN(osdLayoutsConfig()->item_pos) - 1);
4226 if (layoutOverride >= 0 && duration > 0) {
4227 layoutOverrideUntil = millis() + duration;
4228 } else {
4229 layoutOverrideUntil = 0;
4233 int osdGetActiveLayout(bool *overridden)
4235 if (overridden) {
4236 *overridden = layoutOverride >= 0;
4238 return currentLayout;
4241 bool osdItemIsFixed(osd_items_e item)
4243 return item == OSD_CROSSHAIRS ||
4244 item == OSD_ARTIFICIAL_HORIZON ||
4245 item == OSD_HORIZON_SIDEBARS;
4248 displayPort_t *osdGetDisplayPort(void)
4250 return osdDisplayPort;
4253 displayCanvas_t *osdGetDisplayPortCanvas(void)
4255 #if defined(USE_CANVAS)
4256 if (osdDisplayHasCanvas) {
4257 return &osdCanvas;
4259 #endif
4260 return NULL;
4263 timeMs_t systemMessageCycleTime(unsigned messageCount, const char **messages){
4264 uint8_t i = 0;
4265 float factor = 1.0f;
4266 while (i < messageCount) {
4267 if ((float)strlen(messages[i]) / 15.0f > factor) {
4268 factor = (float)strlen(messages[i]) / 15.0f;
4270 i++;
4272 return osdConfig()->system_msg_display_time * factor;
4275 textAttributes_t osdGetSystemMessage(char *buff, size_t buff_size, bool isCenteredText)
4277 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
4279 if (buff != NULL) {
4280 const char *message = NULL;
4281 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
4282 // We might have up to 5 messages to show.
4283 const char *messages[5];
4284 unsigned messageCount = 0;
4285 const char *failsafeInfoMessage = NULL;
4286 const char *invertedInfoMessage = NULL;
4288 if (ARMING_FLAG(ARMED)) {
4289 if (FLIGHT_MODE(FAILSAFE_MODE) || FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding()) {
4290 if (isWaypointMissionRTHActive()) {
4291 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
4292 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL);
4294 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE) {
4295 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED);
4296 } else if (NAV_Status.state == MW_NAV_STATE_WP_ENROUTE) {
4297 // Countdown display for remaining Waypoints
4298 char buf[6];
4299 osdFormatDistanceSymbol(buf, posControl.wpDistance, 0);
4300 tfp_sprintf(messageBuf, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl.activeWaypointIndex), posControl.geoWaypointCount, buf);
4301 messages[messageCount++] = messageBuf;
4302 } else if (NAV_Status.state == MW_NAV_STATE_HOLD_TIMED) {
4303 if (navConfig()->general.flags.waypoint_enforce_altitude && !posControl.wpAltitudeReached) {
4304 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ADJUSTING_WP_ALT);
4305 } else {
4306 // WP hold time countdown in seconds
4307 timeMs_t currentTime = millis();
4308 int holdTimeRemaining = posControl.waypointList[posControl.activeWaypointIndex].p1 - (int)(MS2S(currentTime - posControl.wpReachedTime));
4309 holdTimeRemaining = holdTimeRemaining >= 0 ? holdTimeRemaining : 0;
4311 tfp_sprintf(messageBuf, "HOLDING WP FOR %2u S", holdTimeRemaining);
4313 messages[messageCount++] = messageBuf;
4315 } else {
4316 const char *navStateMessage = navigationStateMessage();
4317 if (navStateMessage) {
4318 messages[messageCount++] = navStateMessage;
4321 #if defined(USE_SAFE_HOME)
4322 const char *safehomeMessage = divertingToSafehomeMessage();
4323 if (safehomeMessage) {
4324 messages[messageCount++] = safehomeMessage;
4326 #endif
4327 if (FLIGHT_MODE(FAILSAFE_MODE)) {
4328 // In FS mode while being armed too
4329 const char *failsafePhaseMessage = osdFailsafePhaseMessage();
4330 failsafeInfoMessage = osdFailsafeInfoMessage();
4332 if (failsafePhaseMessage) {
4333 messages[messageCount++] = failsafePhaseMessage;
4335 if (failsafeInfoMessage) {
4336 messages[messageCount++] = failsafeInfoMessage;
4339 } else { /* messages shown only when Failsafe, WP, RTH or Emergency Landing not active */
4340 if (STATE(FIXED_WING_LEGACY) && (navGetCurrentStateFlags() & NAV_CTL_LAUNCH)) {
4341 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH);
4342 const char *launchStateMessage = fixedWingLaunchStateMessage();
4343 if (launchStateMessage) {
4344 messages[messageCount++] = launchStateMessage;
4346 } else {
4347 if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && !navigationRequiresAngleMode()) {
4348 // ALTHOLD might be enabled alongside ANGLE/HORIZON/ACRO
4349 // when it doesn't require ANGLE mode (required only in FW
4350 // right now). If if requires ANGLE, its display is handled
4351 // by OSD_FLYMODE.
4352 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD);
4354 if (IS_RC_MODE_ACTIVE(BOXAUTOTRIM) && !feature(FEATURE_FW_AUTOTRIM)) {
4355 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM);
4357 if (IS_RC_MODE_ACTIVE(BOXAUTOTUNE)) {
4358 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE);
4359 if (FLIGHT_MODE(MANUAL_MODE)) {
4360 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO);
4363 if (FLIGHT_MODE(HEADFREE_MODE)) {
4364 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE);
4366 if (FLIGHT_MODE(SOARING_MODE)) {
4367 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING);
4369 if (posControl.flags.wpMissionPlannerActive) {
4370 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER);
4372 if (STATE(LANDING_DETECTED)) {
4373 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_LANDED);
4377 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS)) {
4378 unsigned invalidIndex;
4380 // Check if we're unable to arm for some reason
4381 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING) && !settingsValidate(&invalidIndex)) {
4383 const setting_t *setting = settingGet(invalidIndex);
4384 settingGetName(setting, messageBuf);
4385 for (int ii = 0; messageBuf[ii]; ii++) {
4386 messageBuf[ii] = sl_toupper(messageBuf[ii]);
4388 invertedInfoMessage = messageBuf;
4389 messages[messageCount++] = invertedInfoMessage;
4391 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
4392 messages[messageCount++] = invertedInfoMessage;
4394 } else {
4396 invertedInfoMessage = OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM);
4397 messages[messageCount++] = invertedInfoMessage;
4399 // Show the reason for not arming
4400 messages[messageCount++] = osdArmingDisabledReasonMessage();
4403 } else if (!ARMING_FLAG(ARMED)) {
4404 if (isWaypointListValid()) {
4405 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED);
4409 /* Messages that are shown regardless of Arming state */
4410 #ifdef USE_DEV_TOOLS
4411 if (systemConfig()->groundTestMode) {
4412 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_GRD_TEST_MODE);
4414 #endif
4416 if (messageCount > 0) {
4417 message = messages[OSD_ALTERNATING_CHOICES(systemMessageCycleTime(messageCount, messages), messageCount)];
4418 if (message == failsafeInfoMessage) {
4419 // failsafeInfoMessage is not useful for recovering
4420 // a lost model, but might help avoiding a crash.
4421 // Blink to grab user attention.
4422 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
4423 } else if (message == invertedInfoMessage) {
4424 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
4426 // We're shoing either failsafePhaseMessage or
4427 // navStateMessage. Don't BLINK here since
4428 // having this text available might be crucial
4429 // during a lost aircraft recovery and blinking
4430 // will cause it to be missing from some frames.
4433 osdFormatMessage(buff, buff_size, message, isCenteredText);
4435 return elemAttr;
4438 #endif // OSD