Moved MAX symbol before number as it looks better
[inav.git] / src / main / io / osd.c
blob52aa9e3367f3f99d46e4985854051278fda2cb95
1 /*
2 * This file is part of Cleanflight.
4 * Cleanflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Cleanflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
19 Created by Marcin Baliniak
20 some functions based on MinimOSD
22 OSD-CMS separation by jflyper
25 #include <stdbool.h>
26 #include <stdint.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <ctype.h>
30 #include <math.h>
32 #include "platform.h"
34 FILE_COMPILE_FOR_SPEED
36 #ifdef USE_OSD
38 #include "build/debug.h"
39 #include "build/version.h"
41 #include "cms/cms.h"
42 #include "cms/cms_types.h"
43 #include "cms/cms_menu_osd.h"
45 #include "common/axis.h"
46 #include "common/constants.h"
47 #include "common/filter.h"
48 #include "common/log.h"
49 #include "common/olc.h"
50 #include "common/printf.h"
51 #include "common/string_light.h"
52 #include "common/time.h"
53 #include "common/typeconversion.h"
54 #include "common/utils.h"
56 #include "config/feature.h"
57 #include "config/parameter_group.h"
58 #include "config/parameter_group_ids.h"
60 #include "drivers/display.h"
61 #include "drivers/display_canvas.h"
62 #include "drivers/display_font_metadata.h"
63 #include "drivers/osd_symbols.h"
64 #include "drivers/time.h"
65 #include "drivers/vtx_common.h"
67 #include "io/flashfs.h"
68 #include "io/gps.h"
69 #include "io/osd.h"
70 #include "io/osd_common.h"
71 #include "io/osd_hud.h"
72 #include "io/vtx.h"
73 #include "io/vtx_string.h"
75 #include "fc/config.h"
76 #include "fc/controlrate_profile.h"
77 #include "fc/fc_core.h"
78 #include "fc/fc_tasks.h"
79 #include "fc/rc_adjustments.h"
80 #include "fc/rc_controls.h"
81 #include "fc/rc_modes.h"
82 #include "fc/runtime_config.h"
83 #include "fc/settings.h"
85 #include "flight/imu.h"
86 #include "flight/mixer.h"
87 #include "flight/pid.h"
88 #include "flight/power_limits.h"
89 #include "flight/rth_estimator.h"
90 #include "flight/secondary_imu.h"
91 #include "flight/servos.h"
92 #include "flight/wind_estimator.h"
94 #include "navigation/navigation.h"
95 #include "navigation/navigation_private.h"
97 #include "rx/rx.h"
98 #include "rx/msp_override.h"
100 #include "sensors/acceleration.h"
101 #include "sensors/battery.h"
102 #include "sensors/boardalignment.h"
103 #include "sensors/diagnostics.h"
104 #include "sensors/sensors.h"
105 #include "sensors/pitotmeter.h"
106 #include "sensors/temperature.h"
107 #include "sensors/esc_sensor.h"
108 #include "sensors/rangefinder.h"
110 #include "programming/logic_condition.h"
111 #include "programming/global_variables.h"
113 #ifdef USE_HARDWARE_REVISION_DETECTION
114 #include "hardware_revision.h"
115 #endif
117 #define VIDEO_BUFFER_CHARS_PAL 480
119 #define GFORCE_FILTER_TC 0.2
121 #define DELAYED_REFRESH_RESUME_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
122 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
123 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
125 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
126 #define ARMED_SCREEN_DISPLAY_TIME 1500 // ms
127 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
129 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
131 // Adjust OSD_MESSAGE's default position when
132 // changing OSD_MESSAGE_LENGTH
133 #define OSD_MESSAGE_LENGTH 28
134 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
135 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
136 // Wrap all string constants intenteded for display as messages with
137 // this macro to ensure compile time length validation.
138 #define OSD_MESSAGE_STR(x) ({ \
139 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
140 x; \
143 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
145 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
146 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
148 #define OSD_MIN_FONT_VERSION 3
150 static unsigned currentLayout = 0;
151 static int layoutOverride = -1;
152 static bool hasExtendedFont = false; // Wether the font supports characters > 256
153 static timeMs_t layoutOverrideUntil = 0;
154 static pt1Filter_t GForceFilter, GForceFilterAxis[XYZ_AXIS_COUNT];
155 static float GForce, GForceAxis[XYZ_AXIS_COUNT];
157 typedef struct statistic_s {
158 uint16_t max_speed;
159 uint16_t max_3D_speed;
160 uint16_t min_voltage; // /100
161 int16_t max_current;
162 int32_t max_power;
163 int16_t min_rssi;
164 int16_t min_lq; // for CRSF
165 int16_t min_rssi_dbm; // for CRSF
166 int32_t max_altitude;
167 uint32_t max_distance;
168 } statistic_t;
170 static statistic_t stats;
172 static timeUs_t resumeRefreshAt = 0;
173 static bool refreshWaitForResumeCmdRelease;
175 static bool fullRedraw = false;
177 static uint8_t armState;
178 static uint8_t statsPagesCheck = 0;
180 typedef struct osdMapData_s {
181 uint32_t scale;
182 char referenceSymbol;
183 } osdMapData_t;
185 static osdMapData_t osdMapData;
187 static displayPort_t *osdDisplayPort;
188 static bool osdDisplayIsReady = false;
189 #if defined(USE_CANVAS)
190 static displayCanvas_t osdCanvas;
191 static bool osdDisplayHasCanvas;
192 #else
193 #define osdDisplayHasCanvas false
194 #endif
196 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
198 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t, osdConfig, PG_OSD_CONFIG, 4);
199 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t, osdLayoutsConfig, PG_OSD_LAYOUTS_CONFIG, 0);
201 static int digitCount(int32_t value)
203 int digits = 1;
204 while(1) {
205 value = value / 10;
206 if (value == 0) {
207 break;
209 digits++;
211 return digits;
214 bool osdDisplayIsPAL(void)
216 return displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL;
220 * Formats a number given in cents, to support non integer values
221 * without using floating point math. Value is always right aligned
222 * and spaces are inserted before the number to always yield a string
223 * of the same length. If the value doesn't fit into the provided length
224 * it will be divided by scale and true will be returned.
226 bool osdFormatCentiNumber(char *buff, int32_t centivalue, uint32_t scale, int maxDecimals, int maxScaledDecimals, int length)
228 char *ptr = buff;
229 char *dec;
230 int decimals = maxDecimals;
231 bool negative = false;
232 bool scaled = false;
234 buff[length] = '\0';
236 if (centivalue < 0) {
237 negative = true;
238 centivalue = -centivalue;
239 length--;
242 int32_t integerPart = centivalue / 100;
243 // 3 decimal digits
244 int32_t millis = (centivalue % 100) * 10;
246 int digits = digitCount(integerPart);
247 int remaining = length - digits;
249 if (remaining < 0 && scale > 0) {
250 // Reduce by scale
251 scaled = true;
252 decimals = maxScaledDecimals;
253 integerPart = integerPart / scale;
254 // Multiply by 10 to get 3 decimal digits
255 millis = ((centivalue % (100 * scale)) * 10) / scale;
256 digits = digitCount(integerPart);
257 remaining = length - digits;
260 // 3 decimals at most
261 decimals = MIN(remaining, MIN(decimals, 3));
262 remaining -= decimals;
264 // Done counting. Time to write the characters.
266 // Write spaces at the start
267 while (remaining > 0) {
268 *ptr = SYM_BLANK;
269 ptr++;
270 remaining--;
273 // Write the minus sign if required
274 if (negative) {
275 *ptr = '-';
276 ptr++;
278 // Now write the digits.
279 ui2a(integerPart, 10, 0, ptr);
280 ptr += digits;
281 if (decimals > 0) {
282 *(ptr-1) += SYM_ZERO_HALF_TRAILING_DOT - '0';
283 dec = ptr;
284 int factor = 3; // we're getting the decimal part in millis first
285 while (decimals < factor) {
286 factor--;
287 millis /= 10;
289 int decimalDigits = digitCount(millis);
290 while (decimalDigits < decimals) {
291 decimalDigits++;
292 *ptr = '0';
293 ptr++;
295 ui2a(millis, 10, 0, ptr);
296 *dec += SYM_ZERO_HALF_LEADING_DOT - '0';
298 return scaled;
302 * Aligns text to the left side. Adds spaces at the end to keep string length unchanged.
304 static void osdLeftAlignString(char *buff)
306 uint8_t sp = 0, ch = 0;
307 uint8_t len = strlen(buff);
308 while (buff[sp] == ' ') sp++;
309 for (ch = 0; ch < (len - sp); ch++) buff[ch] = buff[ch + sp];
310 for (sp = ch; sp < len; sp++) buff[sp] = ' ';
314 * Converts distance into a string based on the current unit system
315 * prefixed by a a symbol to indicate the unit used.
316 * @param dist Distance in centimeters
318 static void osdFormatDistanceSymbol(char *buff, int32_t dist, uint8_t decimals)
320 switch ((osd_unit_e)osdConfig()->units) {
321 case OSD_UNIT_UK:
322 FALLTHROUGH;
323 case OSD_UNIT_IMPERIAL:
324 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_MILE, decimals, 3, 3)) {
325 buff[3] = SYM_DIST_MI;
326 } else {
327 buff[3] = SYM_DIST_FT;
329 buff[4] = '\0';
330 break;
331 case OSD_UNIT_METRIC_MPH:
332 FALLTHROUGH;
333 case OSD_UNIT_METRIC:
334 if (osdFormatCentiNumber(buff, dist, METERS_PER_KILOMETER, decimals, 3, 3)) {
335 buff[3] = SYM_DIST_KM;
336 } else {
337 buff[3] = SYM_DIST_M;
339 buff[4] = '\0';
340 break;
341 case OSD_UNIT_GA:
342 if (osdFormatCentiNumber(buff, CENTIMETERS_TO_CENTIFEET(dist), FEET_PER_NAUTICALMILE, decimals, 3, 3)) {
343 buff[3] = SYM_DIST_NM;
344 } else {
345 buff[3] = SYM_DIST_FT;
347 buff[4] = '\0';
348 break;
353 * Converts distance into a string based on the current unit system.
354 * @param dist Distance in centimeters
356 static void osdFormatDistanceStr(char *buff, int32_t dist)
358 int32_t centifeet;
359 switch ((osd_unit_e)osdConfig()->units) {
360 case OSD_UNIT_UK:
361 FALLTHROUGH;
362 case OSD_UNIT_IMPERIAL:
363 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
364 if (abs(centifeet) < FEET_PER_MILE * 100 / 2) {
365 // Show feet when dist < 0.5mi
366 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
367 } else {
368 // Show miles when dist >= 0.5mi
369 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100*FEET_PER_MILE)),
370 (abs(centifeet) % (100 * FEET_PER_MILE)) / FEET_PER_MILE, SYM_MI);
372 break;
373 case OSD_UNIT_METRIC_MPH:
374 FALLTHROUGH;
375 case OSD_UNIT_METRIC:
376 if (abs(dist) < METERS_PER_KILOMETER * 100) {
377 // Show meters when dist < 1km
378 tfp_sprintf(buff, "%d%c", (int)(dist / 100), SYM_M);
379 } else {
380 // Show kilometers when dist >= 1km
381 tfp_sprintf(buff, "%d.%02d%c", (int)(dist / (100*METERS_PER_KILOMETER)),
382 (abs(dist) % (100 * METERS_PER_KILOMETER)) / METERS_PER_KILOMETER, SYM_KM);
384 break;
385 case OSD_UNIT_GA:
386 centifeet = CENTIMETERS_TO_CENTIFEET(dist);
387 if (abs(centifeet) < 100000) {
388 // Show feet when dist < 1000ft
389 tfp_sprintf(buff, "%d%c", (int)(centifeet / 100), SYM_FT);
390 } else {
391 // Show nautical miles when dist >= 1000ft
392 tfp_sprintf(buff, "%d.%02d%c", (int)(centifeet / (100 * FEET_PER_NAUTICALMILE)),
393 (int)((abs(centifeet) % (int)(100 * FEET_PER_NAUTICALMILE)) / FEET_PER_NAUTICALMILE), SYM_NM);
395 break;
400 * Converts velocity based on the current unit system (kmh or mph).
401 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
403 static int32_t osdConvertVelocityToUnit(int32_t vel)
405 switch ((osd_unit_e)osdConfig()->units) {
406 case OSD_UNIT_UK:
407 FALLTHROUGH;
408 case OSD_UNIT_METRIC_MPH:
409 FALLTHROUGH;
410 case OSD_UNIT_IMPERIAL:
411 return CMSEC_TO_CENTIMPH(vel) / 100; // Convert to mph
412 case OSD_UNIT_METRIC:
413 return CMSEC_TO_CENTIKPH(vel) / 100; // Convert to kmh
414 case OSD_UNIT_GA:
415 return CMSEC_TO_CENTIKNOTS(vel) / 100; // Convert to Knots
417 // Unreachable
418 return -1;
422 * Converts velocity into a string based on the current unit system.
423 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
425 void osdFormatVelocityStr(char* buff, int32_t vel, bool _3D, bool _max)
427 switch ((osd_unit_e)osdConfig()->units) {
428 case OSD_UNIT_UK:
429 FALLTHROUGH;
430 case OSD_UNIT_METRIC_MPH:
431 FALLTHROUGH;
432 case OSD_UNIT_IMPERIAL:
433 if (_max) {
434 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
435 } else {
436 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_MPH : SYM_MPH));
438 break;
439 case OSD_UNIT_METRIC:
440 if (_max) {
441 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
442 } else {
443 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KMH : SYM_KMH));
445 break;
446 case OSD_UNIT_GA:
447 if (_max) {
448 tfp_sprintf(buff, "%c%3d%c", SYM_MAX, (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
449 } else {
450 tfp_sprintf(buff, "%3d%c", (int)osdConvertVelocityToUnit(vel), (_3D ? SYM_3D_KT : SYM_KT));
452 break;
457 * Converts wind speed into a string based on the current unit system, using
458 * always 3 digits and an additional character for the unit at the right. buff
459 * is null terminated.
460 * @param ws Raw wind speed in cm/s
462 #ifdef USE_WIND_ESTIMATOR
463 static void osdFormatWindSpeedStr(char *buff, int32_t ws, bool isValid)
465 int32_t centivalue;
466 char suffix;
467 switch (osdConfig()->units) {
468 case OSD_UNIT_UK:
469 FALLTHROUGH;
470 case OSD_UNIT_METRIC_MPH:
471 FALLTHROUGH;
472 case OSD_UNIT_IMPERIAL:
473 centivalue = CMSEC_TO_CENTIMPH(ws);
474 suffix = SYM_MPH;
475 break;
476 case OSD_UNIT_GA:
477 centivalue = CMSEC_TO_CENTIKNOTS(ws);
478 suffix = SYM_KT;
479 break;
480 default:
481 case OSD_UNIT_METRIC:
482 centivalue = CMSEC_TO_CENTIKPH(ws);
483 suffix = SYM_KMH;
484 break;
486 if (isValid) {
487 osdFormatCentiNumber(buff, centivalue, 0, 2, 0, 3);
488 } else {
489 buff[0] = buff[1] = buff[2] = '-';
491 buff[3] = suffix;
492 buff[4] = '\0';
494 #endif
497 * Converts altitude into a string based on the current unit system
498 * prefixed by a a symbol to indicate the unit used.
499 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
501 void osdFormatAltitudeSymbol(char *buff, int32_t alt)
503 int digits;
504 if (alt < 0) {
505 digits = 4;
506 } else {
507 digits = 3;
508 buff[0] = ' ';
510 switch ((osd_unit_e)osdConfig()->units) {
511 case OSD_UNIT_UK:
512 FALLTHROUGH;
513 case OSD_UNIT_GA:
514 FALLTHROUGH;
515 case OSD_UNIT_IMPERIAL:
516 if (osdFormatCentiNumber(buff + 4 - digits, CENTIMETERS_TO_CENTIFEET(alt), 1000, 0, 2, digits)) {
517 // Scaled to kft
518 buff[4] = SYM_ALT_KFT;
519 } else {
520 // Formatted in feet
521 buff[4] = SYM_ALT_FT;
523 buff[5] = '\0';
524 break;
525 case OSD_UNIT_METRIC_MPH:
526 FALLTHROUGH;
527 case OSD_UNIT_METRIC:
528 // alt is alredy in cm
529 if (osdFormatCentiNumber(buff + 4 - digits, alt, 1000, 0, 2, digits)) {
530 // Scaled to km
531 buff[4] = SYM_ALT_KM;
532 } else {
533 // Formatted in m
534 buff[4] = SYM_ALT_M;
536 buff[5] = '\0';
537 break;
542 * Converts altitude into a string based on the current unit system.
543 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
545 static void osdFormatAltitudeStr(char *buff, int32_t alt)
547 int32_t value;
548 switch ((osd_unit_e)osdConfig()->units) {
549 case OSD_UNIT_UK:
550 FALLTHROUGH;
551 case OSD_UNIT_GA:
552 FALLTHROUGH;
553 case OSD_UNIT_IMPERIAL:
554 value = CENTIMETERS_TO_FEET(alt);
555 tfp_sprintf(buff, "%d%c", (int)value, SYM_FT);
556 break;
557 case OSD_UNIT_METRIC_MPH:
558 FALLTHROUGH;
559 case OSD_UNIT_METRIC:
560 value = CENTIMETERS_TO_METERS(alt);
561 tfp_sprintf(buff, "%d%c", (int)value, SYM_M);
562 break;
566 static void osdFormatTime(char *buff, uint32_t seconds, char sym_m, char sym_h)
568 uint32_t value = seconds;
569 char sym = sym_m;
570 // Maximum value we can show in minutes is 99 minutes and 59 seconds
571 if (seconds > (99 * 60) + 59) {
572 sym = sym_h;
573 value = seconds / 60;
575 buff[0] = sym;
576 tfp_sprintf(buff + 1, "%02d:%02d", (int)(value / 60), (int)(value % 60));
579 static inline void osdFormatOnTime(char *buff)
581 osdFormatTime(buff, micros() / 1000000, SYM_ON_M, SYM_ON_H);
584 static inline void osdFormatFlyTime(char *buff, textAttributes_t *attr)
586 uint32_t seconds = getFlightTime();
587 osdFormatTime(buff, seconds, SYM_FLY_M, SYM_FLY_H);
588 if (attr && osdConfig()->time_alarm > 0) {
589 if (seconds / 60 >= osdConfig()->time_alarm && ARMING_FLAG(ARMED)) {
590 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
596 * Converts RSSI into a % value used by the OSD.
598 static uint16_t osdConvertRSSI(void)
600 // change range to [0, 99]
601 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE, 0, 99);
604 static uint16_t osdGetCrsfLQ(void)
606 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
607 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
608 int16_t displayedLQ;
609 switch (osdConfig()->crsf_lq_format) {
610 case OSD_CRSF_LQ_TYPE1:
611 case OSD_CRSF_LQ_TYPE2:
612 displayedLQ = statsLQ;
613 break;
614 case OSD_CRSF_LQ_TYPE3:
615 displayedLQ = rxLinkStatistics.rfMode >= 2 ? scaledLQ : statsLQ;
616 break;
618 return displayedLQ;
621 static int16_t osdGetCrsfdBm(void)
623 return rxLinkStatistics.uplinkRSSI;
626 * Displays a temperature postfixed with a symbol depending on the current unit system
627 * @param label to display
628 * @param valid true if measurement is valid
629 * @param temperature in deciDegrees Celcius
631 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)
633 char buff[TEMPERATURE_LABEL_LEN + 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN + 2];
634 textAttributes_t elemAttr = valid ? TEXT_ATTRIBUTES_NONE : _TEXT_ATTRIBUTES_BLINK_BIT;
635 uint8_t valueXOffset = 0;
637 if (symbol) {
638 buff[0] = symbol;
639 buff[1] = '\0';
640 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
641 valueXOffset = 1;
643 #ifdef USE_TEMPERATURE_SENSOR
644 else if (label[0] != '\0') {
645 uint8_t label_len = strnlen(label, TEMPERATURE_LABEL_LEN);
646 memcpy(buff, label, label_len);
647 memset(buff + label_len, ' ', TEMPERATURE_LABEL_LEN + 1 - label_len);
648 buff[5] = '\0';
649 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
650 valueXOffset = osdConfig()->temp_label_align == OSD_ALIGN_LEFT ? 5 : label_len + 1;
652 #else
653 UNUSED(label);
654 #endif
656 if (valid) {
658 if ((temperature <= alarm_min) || (temperature >= alarm_max)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
659 if (osdConfig()->units == OSD_UNIT_IMPERIAL) temperature = temperature * 9 / 5.0f + 320;
660 tfp_sprintf(buff, "%3d", temperature / 10);
662 } else
663 strcpy(buff, "---");
665 buff[3] = osdConfig()->units == OSD_UNIT_IMPERIAL ? SYM_TEMP_F : SYM_TEMP_C;
666 buff[4] = '\0';
668 displayWriteWithAttr(osdDisplayPort, elemPosX + valueXOffset, elemPosY, buff, elemAttr);
671 #ifdef USE_TEMPERATURE_SENSOR
672 static void osdDisplayTemperatureSensor(uint8_t elemPosX, uint8_t elemPosY, uint8_t sensorIndex)
674 int16_t temperature;
675 const bool valid = getSensorTemperature(sensorIndex, &temperature);
676 const tempSensorConfig_t *sensorConfig = tempSensorConfig(sensorIndex);
677 uint16_t symbol = sensorConfig->osdSymbol ? SYM_TEMP_SENSOR_FIRST + sensorConfig->osdSymbol - 1 : 0;
678 osdDisplayTemperature(elemPosX, elemPosY, symbol, sensorConfig->label, valid, temperature, sensorConfig->alarm_min, sensorConfig->alarm_max);
680 #endif
682 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
684 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
685 const int coordinateLength = osdConfig()->coordinate_digits + 1;
687 buff[0] = sym;
688 int32_t integerPart = val / GPS_DEGREES_DIVIDER;
689 // Latitude maximum integer width is 3 (-90) while
690 // longitude maximum integer width is 4 (-180).
691 int integerDigits = tfp_sprintf(buff + 1, (integerPart == 0 && val < 0) ? "-%d" : "%d", (int)integerPart);
692 // We can show up to 7 digits in decimalPart.
693 int32_t decimalPart = abs(val % GPS_DEGREES_DIVIDER);
694 STATIC_ASSERT(GPS_DEGREES_DIVIDER == 1e7, adjust_max_decimal_digits);
695 int decimalDigits = tfp_sprintf(buff + 1 + integerDigits, "%07d", (int)decimalPart);
696 // Embbed the decimal separator
697 buff[1 + integerDigits - 1] += SYM_ZERO_HALF_TRAILING_DOT - '0';
698 buff[1 + integerDigits] += SYM_ZERO_HALF_LEADING_DOT - '0';
699 // Fill up to coordinateLength with zeros
700 int total = 1 + integerDigits + decimalDigits;
701 while(total < coordinateLength) {
702 buff[total] = '0';
703 total++;
705 buff[coordinateLength] = '\0';
708 static void osdFormatCraftName(char *buff)
710 if (strlen(systemConfig()->name) == 0)
711 strcpy(buff, "CRAFT_NAME");
712 else {
713 for (int i = 0; i < MAX_NAME_LENGTH; i++) {
714 buff[i] = sl_toupper((unsigned char)systemConfig()->name[i]);
715 if (systemConfig()->name[i] == 0)
716 break;
721 static const char * osdArmingDisabledReasonMessage(void)
723 const char *message = NULL;
724 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
726 switch (isArmingDisabledReason()) {
727 case ARMING_DISABLED_FAILSAFE_SYSTEM:
728 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
729 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING) {
730 if (failsafeIsReceivingRxData()) {
731 // If we're not using sticks, it means the ARM switch
732 // hasn't been off since entering FAILSAFE_RX_LOSS_MONITORING
733 // yet
734 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF);
736 // Not receiving RX data
737 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
739 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS);
740 case ARMING_DISABLED_NOT_LEVEL:
741 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL);
742 case ARMING_DISABLED_SENSORS_CALIBRATING:
743 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL);
744 case ARMING_DISABLED_SYSTEM_OVERLOADED:
745 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED);
746 case ARMING_DISABLED_NAVIGATION_UNSAFE:
747 #if defined(USE_NAV)
748 // Check the exact reason
749 switch (navigationIsBlockingArming(NULL)) {
750 char buf[6];
751 case NAV_ARMING_BLOCKER_NONE:
752 break;
753 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX:
754 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX);
755 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE:
756 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST);
757 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR:
758 osdFormatDistanceSymbol(buf, distanceToFirstWP(), 0);
759 tfp_sprintf(messageBuf, "FIRST WP TOO FAR (%s)", buf);
760 return message = messageBuf;
761 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR:
762 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG);
764 #endif
765 break;
766 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED:
767 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL);
768 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED:
769 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL);
770 case ARMING_DISABLED_ARM_SWITCH:
771 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST);
772 case ARMING_DISABLED_HARDWARE_FAILURE:
774 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
775 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE);
777 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
778 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL);
780 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
781 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL);
783 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
784 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL);
786 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
787 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL);
789 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
790 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL);
792 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
793 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL);
796 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL);
797 case ARMING_DISABLED_BOXFAILSAFE:
798 return OSD_MESSAGE_STR(OSD_MSG_FS_EN);
799 case ARMING_DISABLED_BOXKILLSWITCH:
800 return OSD_MESSAGE_STR(OSD_MSG_KILL_SW_EN);
801 case ARMING_DISABLED_RC_LINK:
802 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK);
803 case ARMING_DISABLED_THROTTLE:
804 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW);
805 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED:
806 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER);
807 case ARMING_DISABLED_SERVO_AUTOTRIM:
808 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE);
809 case ARMING_DISABLED_OOM:
810 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY);
811 case ARMING_DISABLED_INVALID_SETTING:
812 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
813 case ARMING_DISABLED_CLI:
814 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE);
815 case ARMING_DISABLED_PWM_OUTPUT_ERROR:
816 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR);
817 case ARMING_DISABLED_NO_PREARM:
818 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM);
819 case ARMING_DISABLED_DSHOT_BEEPER:
820 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER);
821 // Cases without message
822 case ARMING_DISABLED_CMS_MENU:
823 FALLTHROUGH;
824 case ARMING_DISABLED_OSD_MENU:
825 FALLTHROUGH;
826 case ARMING_DISABLED_ALL_FLAGS:
827 FALLTHROUGH;
828 case ARMED:
829 FALLTHROUGH;
830 case WAS_EVER_ARMED:
831 break;
833 return NULL;
836 static const char * osdFailsafePhaseMessage(void)
838 // See failsafe.h for each phase explanation
839 switch (failsafePhase()) {
840 #ifdef USE_NAV
841 case FAILSAFE_RETURN_TO_HOME:
842 // XXX: Keep this in sync with OSD_FLYMODE.
843 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS);
844 #endif
845 case FAILSAFE_LANDING:
846 // This should be considered an emergengy landing
847 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS);
848 case FAILSAFE_RX_LOSS_MONITORING:
849 // Only reachable from FAILSAFE_LANDED, which performs
850 // a disarm. Since aircraft has been disarmed, we no
851 // longer show failsafe details.
852 FALLTHROUGH;
853 case FAILSAFE_LANDED:
854 // Very brief, disarms and transitions into
855 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
856 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
857 // so we'll show the user how to re-arm in when
858 // that flag is the reason to prevent arming.
859 FALLTHROUGH;
860 case FAILSAFE_RX_LOSS_IDLE:
861 // This only happens when user has chosen NONE as FS
862 // procedure. The recovery messages should be enough.
863 FALLTHROUGH;
864 case FAILSAFE_IDLE:
865 // Failsafe not active
866 FALLTHROUGH;
867 case FAILSAFE_RX_LOSS_DETECTED:
868 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
869 // or the FS procedure immediately.
870 FALLTHROUGH;
871 case FAILSAFE_RX_LOSS_RECOVERED:
872 // Exiting failsafe
873 break;
875 return NULL;
878 static const char * osdFailsafeInfoMessage(void)
880 if (failsafeIsReceivingRxData()) {
881 // User must move sticks to exit FS mode
882 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS);
884 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST);
886 #if defined(USE_SAFE_HOME)
887 static const char * divertingToSafehomeMessage(void)
889 if (NAV_Status.state != MW_NAV_STATE_HOVER_ABOVE_HOME && safehome_applied) {
890 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME);
892 return NULL;
894 #endif
896 static const char * navigationStateMessage(void)
898 switch (NAV_Status.state) {
899 case MW_NAV_STATE_NONE:
900 break;
901 case MW_NAV_STATE_RTH_START:
902 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH);
903 case MW_NAV_STATE_RTH_CLIMB:
904 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB);
905 case MW_NAV_STATE_RTH_ENROUTE:
906 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME);
907 case MW_NAV_STATE_HOLD_INFINIT:
908 // Used by HOLD flight modes. No information to add.
909 break;
910 case MW_NAV_STATE_HOLD_TIMED:
911 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
912 break;
913 case MW_NAV_STATE_WP_ENROUTE:
914 // "TO WP" + WP countdown added in osdGetSystemMessage
915 break;
916 case MW_NAV_STATE_PROCESS_NEXT:
917 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP);
918 case MW_NAV_STATE_DO_JUMP:
919 // Not used
920 break;
921 case MW_NAV_STATE_LAND_START:
922 // Not used
923 break;
924 case MW_NAV_STATE_EMERGENCY_LANDING:
925 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING);
926 case MW_NAV_STATE_LAND_IN_PROGRESS:
927 return OSD_MESSAGE_STR(OSD_MSG_LANDING);
928 case MW_NAV_STATE_HOVER_ABOVE_HOME:
929 if (STATE(FIXED_WING_LEGACY)) {
930 #if defined(USE_SAFE_HOME)
931 if (safehome_applied) {
932 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME);
934 #endif
935 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME);
937 return OSD_MESSAGE_STR(OSD_MSG_HOVERING);
938 case MW_NAV_STATE_LANDED:
939 return OSD_MESSAGE_STR(OSD_MSG_LANDED);
940 case MW_NAV_STATE_LAND_SETTLE:
941 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND);
942 case MW_NAV_STATE_LAND_START_DESCENT:
943 // Not used
944 break;
946 return NULL;
949 static void osdFormatMessage(char *buff, size_t size, const char *message, bool isCenteredText)
951 // String is always filled with Blanks
952 memset(buff, SYM_BLANK, size);
953 if (message) {
954 size_t messageLength = strlen(message);
955 int rem = isCenteredText ? MAX(0, (int)size - (int)messageLength) : 0;
956 strncpy(buff + rem / 2, message, MIN((int)size - rem / 2, (int)messageLength));
958 // Ensure buff is zero terminated
959 buff[size - 1] = '\0';
963 * Draws the battery symbol filled in accordingly to the
964 * battery voltage to buff[0].
966 static void osdFormatBatteryChargeSymbol(char *buff)
968 uint8_t p = calculateBatteryPercentage();
969 p = (100 - p) / 16.6;
970 buff[0] = SYM_BATT_FULL + p;
973 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t *attr)
975 if ((getBatteryState() != BATTERY_NOT_PRESENT) && ((batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical)) || ((!batteryUsesCapacityThresholds()) && (getBatteryVoltage() <= getBatteryWarningVoltage()))))
976 TEXT_ATTRIBUTES_ADD_BLINK(*attr);
979 void osdCrosshairPosition(uint8_t *x, uint8_t *y)
981 *x = osdDisplayPort->cols / 2;
982 *y = osdDisplayPort->rows / 2;
983 *y += osdConfig()->horizon_offset;
987 * Formats throttle position prefixed by its symbol.
988 * Shows output to motor, not stick position
990 static void osdFormatThrottlePosition(char *buff, bool autoThr, textAttributes_t *elemAttr)
992 buff[0] = SYM_BLANK;
993 buff[1] = SYM_THR;
994 if (autoThr && navigationIsControllingThrottle()) {
995 buff[0] = SYM_AUTO_THR0;
996 buff[1] = SYM_AUTO_THR1;
997 if (isFixedWingAutoThrottleManuallyIncreased()) {
998 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1001 #ifdef USE_POWER_LIMITS
1002 if (powerLimiterIsLimiting()) {
1003 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr);
1005 #endif
1006 tfp_sprintf(buff + 2, "%3d", getThrottlePercent());
1010 * Formats gvars prefixed by its number (0-indexed). If autoThr
1012 static void osdFormatGVar(char *buff, uint8_t index)
1014 buff[0] = 'G';
1015 buff[1] = '0'+index;
1016 buff[2] = ':';
1017 #ifdef USE_PROGRAMMING_FRAMEWORK
1018 osdFormatCentiNumber(buff + 3, (int32_t)gvGet(index)*(int32_t)100, 1, 0, 0, 5);
1019 #endif
1022 #if defined(USE_ESC_SENSOR)
1023 static void osdFormatRpm(char *buff, uint32_t rpm)
1025 buff[0] = SYM_RPM;
1026 if (rpm) {
1027 if (rpm >= 1000) {
1028 osdFormatCentiNumber(buff + 1, rpm / 10, 0, 1, 1, 2);
1029 buff[3] = 'K';
1030 buff[4] = '\0';
1032 else {
1033 tfp_sprintf(buff + 1, "%3lu", rpm);
1036 else {
1037 strcpy(buff + 1, "---");
1040 #endif
1042 int32_t osdGetAltitude(void)
1044 #if defined(USE_NAV)
1045 return getEstimatedActualPosition(Z);
1046 #elif defined(USE_BARO)
1047 return baro.alt;
1048 #else
1049 return 0;
1050 #endif
1053 static inline int32_t osdGetAltitudeMsl(void)
1055 #if defined(USE_NAV)
1056 return getEstimatedActualPosition(Z)+GPS_home.alt;
1057 #elif defined(USE_BARO)
1058 return baro.alt+GPS_home.alt;
1059 #else
1060 return 0;
1061 #endif
1064 static bool osdIsHeadingValid(void)
1066 #ifdef USE_SECONDARY_IMU
1067 if (secondaryImuState.active && secondaryImuConfig()->useForOsdHeading) {
1068 return true;
1069 } else {
1070 return isImuHeadingValid();
1072 #else
1073 return isImuHeadingValid();
1074 #endif
1077 int16_t osdGetHeading(void)
1079 #ifdef USE_SECONDARY_IMU
1080 if (secondaryImuState.active && secondaryImuConfig()->useForOsdHeading) {
1081 return secondaryImuState.eulerAngles.values.yaw;
1082 } else {
1083 return attitude.values.yaw;
1085 #else
1086 return attitude.values.yaw;
1087 #endif
1090 int16_t osdPanServoHomeDirectionOffset(void)
1092 int8_t servoIndex = osdConfig()->pan_servo_index;
1093 int16_t servoPosition = servo[servoIndex];
1094 int16_t servoMiddle = servoParams(servoIndex)->middle;
1095 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition - servoMiddle) * osdConfig()->pan_servo_pwm2centideg);
1098 // Returns a heading angle in degrees normalized to [0, 360).
1099 int osdGetHeadingAngle(int angle)
1101 while (angle < 0) {
1102 angle += 360;
1104 while (angle >= 360) {
1105 angle -= 360;
1107 return angle;
1110 #if defined(USE_GPS)
1112 /* Draws a map with the given symbol in the center and given point of interest
1113 * defined by its distance in meters and direction in degrees.
1114 * referenceHeading indicates the up direction in the map, in degrees, while
1115 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1116 * arrow to indicate the map reference to the user. The drawn argument is an
1117 * in-out used to store the last position where the craft was drawn to avoid
1118 * erasing all screen on each redraw.
1120 static void osdDrawMap(int referenceHeading, uint16_t referenceSym, uint16_t centerSym,
1121 uint32_t poiDistance, int16_t poiDirection, uint16_t poiSymbol,
1122 uint16_t *drawn, uint32_t *usedScale)
1124 // TODO: These need to be tested with several setups. We might
1125 // need to make them configurable.
1126 const int hMargin = 5;
1127 const int vMargin = 3;
1129 // TODO: Get this from the display driver?
1130 const int charWidth = 12;
1131 const int charHeight = 18;
1133 uint8_t minX = hMargin;
1134 uint8_t maxX = osdDisplayPort->cols - 1 - hMargin;
1135 uint8_t minY = vMargin;
1136 uint8_t maxY = osdDisplayPort->rows - 1 - vMargin;
1137 uint8_t midX = osdDisplayPort->cols / 2;
1138 uint8_t midY = osdDisplayPort->rows / 2;
1140 // Fixed marks
1141 displayWriteChar(osdDisplayPort, midX, midY, centerSym);
1143 // First, erase the previous drawing.
1144 if (OSD_VISIBLE(*drawn)) {
1145 displayWriteChar(osdDisplayPort, OSD_X(*drawn), OSD_Y(*drawn), SYM_BLANK);
1146 *drawn = 0;
1149 uint32_t initialScale;
1150 const unsigned scaleMultiplier = 2;
1151 // We try to reduce the scale when the POI will be around half the distance
1152 // between the center and the closers map edge, to avoid too much jumping
1153 const int scaleReductionMultiplier = MIN(midX - hMargin, midY - vMargin) / 2;
1155 switch (osdConfig()->units) {
1156 case OSD_UNIT_UK:
1157 FALLTHROUGH;
1158 case OSD_UNIT_IMPERIAL:
1159 initialScale = 16; // 16m ~= 0.01miles
1160 break;
1161 case OSD_UNIT_GA:
1162 initialScale = 18; // 18m ~= 0.01 nautical miles
1163 break;
1164 default:
1165 case OSD_UNIT_METRIC_MPH:
1166 FALLTHROUGH;
1167 case OSD_UNIT_METRIC:
1168 initialScale = 10; // 10m as initial scale
1169 break;
1172 // Try to keep the same scale when getting closer until we draw over the center point
1173 uint32_t scale = initialScale;
1174 if (*usedScale) {
1175 scale = *usedScale;
1176 if (scale > initialScale && poiDistance < *usedScale * scaleReductionMultiplier) {
1177 scale /= scaleMultiplier;
1181 if (STATE(GPS_FIX)) {
1183 int directionToPoi = osdGetHeadingAngle(poiDirection - referenceHeading);
1184 float poiAngle = DEGREES_TO_RADIANS(directionToPoi);
1185 float poiSin = sin_approx(poiAngle);
1186 float poiCos = cos_approx(poiAngle);
1188 // Now start looking for a valid scale that lets us draw everything
1189 int ii;
1190 for (ii = 0; ii < 50; ii++) {
1191 // Calculate location of the aircraft in map
1192 int points = poiDistance / ((float)scale / charHeight);
1194 float pointsX = points * poiSin;
1195 int poiX = midX - roundf(pointsX / charWidth);
1196 if (poiX < minX || poiX > maxX) {
1197 scale *= scaleMultiplier;
1198 continue;
1201 float pointsY = points * poiCos;
1202 int poiY = midY + roundf(pointsY / charHeight);
1203 if (poiY < minY || poiY > maxY) {
1204 scale *= scaleMultiplier;
1205 continue;
1208 if (poiX == midX && poiY == midY) {
1209 // We're over the map center symbol, so we would be drawing
1210 // over it even if we increased the scale. Alternate between
1211 // drawing the center symbol or drawing the POI.
1212 if (centerSym != SYM_BLANK && OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1213 break;
1215 } else {
1217 uint16_t c;
1218 if (displayReadCharWithAttr(osdDisplayPort, poiX, poiY, &c, NULL) && c != SYM_BLANK) {
1219 // Something else written here, increase scale. If the display doesn't support reading
1220 // back characters, we assume there's nothing.
1222 // If we're close to the center, decrease scale. Otherwise increase it.
1223 uint8_t centerDeltaX = (maxX - minX) / (scaleMultiplier * 2);
1224 uint8_t centerDeltaY = (maxY - minY) / (scaleMultiplier * 2);
1225 if (poiX >= midX - centerDeltaX && poiX <= midX + centerDeltaX &&
1226 poiY >= midY - centerDeltaY && poiY <= midY + centerDeltaY &&
1227 scale > scaleMultiplier) {
1229 scale /= scaleMultiplier;
1230 } else {
1231 scale *= scaleMultiplier;
1233 continue;
1237 // Draw the point on the map
1238 if (poiSymbol == SYM_ARROW_UP) {
1239 // Drawing aircraft, rotate
1240 int mapHeading = osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading);
1241 poiSymbol += mapHeading * 2 / 45;
1243 displayWriteChar(osdDisplayPort, poiX, poiY, poiSymbol);
1245 // Update saved location
1246 *drawn = OSD_POS(poiX, poiY) | OSD_VISIBLE_FLAG;
1247 break;
1251 *usedScale = scale;
1253 // Update global map data for scale and reference
1254 osdMapData.scale = scale;
1255 osdMapData.referenceSymbol = referenceSym;
1258 /* Draws a map with the home in the center and the craft moving around.
1259 * See osdDrawMap() for reference.
1261 static void osdDrawHomeMap(int referenceHeading, uint8_t referenceSym, uint16_t *drawn, uint32_t *usedScale)
1263 osdDrawMap(referenceHeading, referenceSym, SYM_HOME, GPS_distanceToHome, GPS_directionToHome, SYM_ARROW_UP, drawn, usedScale);
1266 /* Draws a map with the aircraft in the center and the home moving around.
1267 * See osdDrawMap() for reference.
1269 static void osdDrawRadar(uint16_t *drawn, uint32_t *usedScale)
1271 int16_t reference = DECIDEGREES_TO_DEGREES(osdGetHeading());
1272 int16_t poiDirection = osdGetHeadingAngle(GPS_directionToHome + 180);
1273 osdDrawMap(reference, 0, SYM_ARROW_UP, GPS_distanceToHome, poiDirection, SYM_HOME, drawn, usedScale);
1276 static uint16_t crc_accumulate(uint8_t data, uint16_t crcAccum)
1278 uint8_t tmp;
1279 tmp = data ^ (uint8_t)(crcAccum & 0xff);
1280 tmp ^= (tmp << 4);
1281 crcAccum = (crcAccum >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
1282 return crcAccum;
1286 static void osdDisplayTelemetry(void)
1288 uint32_t trk_data;
1289 uint16_t trk_crc = 0;
1290 char trk_buffer[31];
1291 static int16_t trk_elevation = 127;
1292 static uint16_t trk_bearing = 0;
1294 if (ARMING_FLAG(ARMED)) {
1295 if (STATE(GPS_FIX)){
1296 if (GPS_distanceToHome > 5) {
1297 trk_bearing = GPS_directionToHome;
1298 trk_bearing += 360 + 180;
1299 trk_bearing %= 360;
1300 int32_t alt = CENTIMETERS_TO_METERS(osdGetAltitude());
1301 float at = atan2(alt, GPS_distanceToHome);
1302 trk_elevation = (float)at * 57.2957795; // 57.2957795 = 1 rad
1303 trk_elevation += 37; // because elevation in telemetry should be from -37 to 90
1304 if (trk_elevation < 0) {
1305 trk_elevation = 0;
1310 else{
1311 trk_elevation = 127;
1312 trk_bearing = 0;
1315 trk_data = 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1316 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.
1317 trk_data = trk_data | (uint32_t)trk_bearing << 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1318 trk_crc = crc_accumulate(0xFF & trk_data, trk_crc); // CRC First Byte bits 0-7
1319 trk_crc = crc_accumulate(0xFF & trk_bearing, trk_crc); // CRC Second Byte bits 8-15
1320 trk_crc = crc_accumulate(trk_bearing >> 8, trk_crc); // CRC Third Byte bits 16-17
1321 trk_data = trk_data | (uint32_t)trk_crc << 17; // bits 18-29 CRC & 0x3FFFF
1323 for (uint8_t t_ctr = 0; t_ctr < 30; t_ctr++) { // Prepare screen buffer and write data line.
1324 if (trk_data & (uint32_t)1 << t_ctr){
1325 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_0;
1327 else{
1328 trk_buffer[29 - t_ctr] = SYM_TELEMETRY_1;
1331 trk_buffer[30] = 0;
1332 displayWrite(osdDisplayPort, 0, 0, trk_buffer);
1333 if (osdConfig()->telemetry>1){
1334 displayWrite(osdDisplayPort, 0, 3, trk_buffer); // Test display because normal telemetry line is not visible
1337 #endif
1339 static void osdFormatPidControllerOutput(char *buff, const char *label, const pidController_t *pidController, uint8_t scale, bool showDecimal) {
1340 strcpy(buff, label);
1341 for (uint8_t i = strlen(label); i < 5; ++i) buff[i] = ' ';
1342 uint8_t decimals = showDecimal ? 1 : 0;
1343 osdFormatCentiNumber(buff + 5, pidController->proportional * scale, 0, decimals, 0, 4);
1344 buff[9] = ' ';
1345 osdFormatCentiNumber(buff + 10, pidController->integrator * scale, 0, decimals, 0, 4);
1346 buff[14] = ' ';
1347 osdFormatCentiNumber(buff + 15, pidController->derivative * scale, 0, decimals, 0, 4);
1348 buff[19] = ' ';
1349 osdFormatCentiNumber(buff + 20, pidController->output_constrained * scale, 0, decimals, 0, 4);
1350 buff[24] = '\0';
1353 static void osdDisplayBatteryVoltage(uint8_t elemPosX, uint8_t elemPosY, uint16_t voltage, uint8_t digits, uint8_t decimals)
1355 char buff[6];
1356 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1358 osdFormatBatteryChargeSymbol(buff);
1359 buff[1] = '\0';
1360 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1361 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1363 elemAttr = TEXT_ATTRIBUTES_NONE;
1364 digits = MIN(digits, 4);
1365 osdFormatCentiNumber(buff, voltage, 0, decimals, 0, digits);
1366 buff[digits] = SYM_VOLT;
1367 buff[digits+1] = '\0';
1368 if ((getBatteryState() != BATTERY_NOT_PRESENT) && (getBatteryVoltage() <= getBatteryWarningVoltage()))
1369 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1370 displayWriteWithAttr(osdDisplayPort, elemPosX + 1, elemPosY, buff, elemAttr);
1373 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)
1375 textAttributes_t elemAttr;
1376 char buff[4];
1378 const pid8_t *pid = &pidBank()->pid[pidIndex];
1379 pidType_e pidType = pidIndexGetType(pidIndex);
1381 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1383 if (pidType == PID_TYPE_NONE) {
1384 // PID is not used in this configuration. Draw dashes.
1385 // XXX: Keep this in sync with the %3d format and spacing used below
1386 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - - -");
1387 return;
1390 elemAttr = TEXT_ATTRIBUTES_NONE;
1391 tfp_sprintf(buff, "%3d", pid->P);
1392 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1393 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1394 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1396 elemAttr = TEXT_ATTRIBUTES_NONE;
1397 tfp_sprintf(buff, "%3d", pid->I);
1398 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1399 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1400 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1402 elemAttr = TEXT_ATTRIBUTES_NONE;
1403 tfp_sprintf(buff, "%3d", pid->D);
1404 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1405 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1406 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1408 elemAttr = TEXT_ATTRIBUTES_NONE;
1409 tfp_sprintf(buff, "%3d", pid->FF);
1410 if ((isAdjustmentFunctionSelected(adjFuncFF)) || (((adjFuncFF == ADJUSTMENT_ROLL_FF) || (adjFuncFF == ADJUSTMENT_PITCH_FF)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF))))
1411 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1412 displayWriteWithAttr(osdDisplayPort, elemPosX + 16, elemPosY, buff, elemAttr);
1415 static void osdDisplayNavPIDValues(uint8_t elemPosX, uint8_t elemPosY, const char *str, pidIndex_e pidIndex, adjustmentFunction_e adjFuncP, adjustmentFunction_e adjFuncI, adjustmentFunction_e adjFuncD)
1417 textAttributes_t elemAttr;
1418 char buff[4];
1420 const pid8_t *pid = &pidBank()->pid[pidIndex];
1421 pidType_e pidType = pidIndexGetType(pidIndex);
1423 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1425 if (pidType == PID_TYPE_NONE) {
1426 // PID is not used in this configuration. Draw dashes.
1427 // XXX: Keep this in sync with the %3d format and spacing used below
1428 displayWrite(osdDisplayPort, elemPosX + 6, elemPosY, "- - -");
1429 return;
1432 elemAttr = TEXT_ATTRIBUTES_NONE;
1433 tfp_sprintf(buff, "%3d", pid->P);
1434 if ((isAdjustmentFunctionSelected(adjFuncP)) || (((adjFuncP == ADJUSTMENT_ROLL_P) || (adjFuncP == ADJUSTMENT_PITCH_P)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P))))
1435 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1436 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
1438 elemAttr = TEXT_ATTRIBUTES_NONE;
1439 tfp_sprintf(buff, "%3d", pid->I);
1440 if ((isAdjustmentFunctionSelected(adjFuncI)) || (((adjFuncI == ADJUSTMENT_ROLL_I) || (adjFuncI == ADJUSTMENT_PITCH_I)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I))))
1441 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1442 displayWriteWithAttr(osdDisplayPort, elemPosX + 8, elemPosY, buff, elemAttr);
1444 elemAttr = TEXT_ATTRIBUTES_NONE;
1445 tfp_sprintf(buff, "%3d", pidType == PID_TYPE_PIFF ? pid->FF : pid->D);
1446 if ((isAdjustmentFunctionSelected(adjFuncD)) || (((adjFuncD == ADJUSTMENT_ROLL_D) || (adjFuncD == ADJUSTMENT_PITCH_D)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D))))
1447 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1448 displayWriteWithAttr(osdDisplayPort, elemPosX + 12, elemPosY, buff, elemAttr);
1451 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) {
1452 char buff[8];
1453 textAttributes_t elemAttr;
1454 displayWrite(osdDisplayPort, elemPosX, elemPosY, str);
1456 elemAttr = TEXT_ATTRIBUTES_NONE;
1457 osdFormatCentiNumber(buff, value * 100, 0, maxDecimals, 0, MIN(valueLength, 8));
1458 if (isAdjustmentFunctionSelected(adjFunc))
1459 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1460 displayWriteWithAttr(osdDisplayPort, elemPosX + strlen(str) + 1 + valueOffset, elemPosY, buff, elemAttr);
1463 int8_t getGeoWaypointNumber(int8_t waypointIndex)
1465 static int8_t lastWaypointIndex = 1;
1466 static int8_t geoWaypointIndex;
1468 if (waypointIndex != lastWaypointIndex) {
1469 lastWaypointIndex = geoWaypointIndex = waypointIndex;
1470 for (uint8_t i = 0; i <= waypointIndex; i++) {
1471 if (posControl.waypointList[i].action == NAV_WP_ACTION_SET_POI ||
1472 posControl.waypointList[i].action == NAV_WP_ACTION_SET_HEAD ||
1473 posControl.waypointList[i].action == NAV_WP_ACTION_JUMP) {
1474 geoWaypointIndex -= 1;
1479 return geoWaypointIndex + 1;
1482 static bool osdDrawSingleElement(uint8_t item)
1484 uint16_t pos = osdLayoutsConfig()->item_pos[currentLayout][item];
1485 if (!OSD_VISIBLE(pos)) {
1486 return false;
1488 uint8_t elemPosX = OSD_X(pos);
1489 uint8_t elemPosY = OSD_Y(pos);
1490 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
1491 char buff[32] = {0};
1493 switch (item) {
1494 case OSD_RSSI_VALUE:
1496 uint16_t osdRssi = osdConvertRSSI();
1497 buff[0] = SYM_RSSI;
1498 tfp_sprintf(buff + 1, "%2d", osdRssi);
1499 if (osdRssi < osdConfig()->rssi_alarm) {
1500 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1502 break;
1505 case OSD_MAIN_BATT_VOLTAGE:
1506 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawVoltage(), 2 + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1507 return true;
1509 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE:
1510 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedVoltage(), 2 + osdConfig()->main_voltage_decimals, osdConfig()->main_voltage_decimals);
1511 return true;
1513 case OSD_CURRENT_DRAW:
1514 osdFormatCentiNumber(buff, getAmperage(), 0, 2, 0, 3);
1515 buff[3] = SYM_AMP;
1516 buff[4] = '\0';
1518 uint8_t current_alarm = osdConfig()->current_alarm;
1519 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
1520 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1522 break;
1524 case OSD_MAH_DRAWN:
1525 tfp_sprintf(buff, "%4d", (int)getMAhDrawn());
1526 buff[4] = SYM_MAH;
1527 buff[5] = '\0';
1528 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1529 break;
1531 case OSD_WH_DRAWN:
1532 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
1533 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1534 buff[3] = SYM_WH;
1535 buff[4] = '\0';
1536 break;
1538 case OSD_BATTERY_REMAINING_CAPACITY:
1540 if (currentBatteryProfile->capacity.value == 0)
1541 tfp_sprintf(buff, " NA");
1542 else if (!batteryWasFullWhenPluggedIn())
1543 tfp_sprintf(buff, " NF");
1544 else if (currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH)
1545 tfp_sprintf(buff, "%4lu", getBatteryRemainingCapacity());
1546 else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1547 osdFormatCentiNumber(buff + 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3);
1549 buff[4] = currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MAH ? SYM_MAH : SYM_WH;
1550 buff[5] = '\0';
1552 if ((getBatteryState() != BATTERY_NOT_PRESENT) && batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile->capacity.warning - currentBatteryProfile->capacity.critical))
1553 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1555 break;
1557 case OSD_BATTERY_REMAINING_PERCENT:
1558 tfp_sprintf(buff, "%3d%%", calculateBatteryPercentage());
1559 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr);
1560 break;
1562 case OSD_POWER_SUPPLY_IMPEDANCE:
1563 if (isPowerSupplyImpedanceValid())
1564 tfp_sprintf(buff, "%3d", getPowerSupplyImpedance());
1565 else
1566 strcpy(buff, "---");
1567 buff[3] = SYM_MILLIOHM;
1568 buff[4] = '\0';
1569 break;
1571 #ifdef USE_GPS
1572 case OSD_GPS_SATS:
1573 buff[0] = SYM_SAT_L;
1574 buff[1] = SYM_SAT_R;
1575 tfp_sprintf(buff + 2, "%2d", gpsSol.numSat);
1576 if (!STATE(GPS_FIX)) {
1577 if (getHwGPSStatus() == HW_SENSOR_UNAVAILABLE || getHwGPSStatus() == HW_SENSOR_UNHEALTHY) {
1578 strcpy(buff + 2, "X!");
1580 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1582 break;
1584 case OSD_GPS_SPEED:
1585 osdFormatVelocityStr(buff, gpsSol.groundSpeed, false, false);
1586 break;
1588 case OSD_GPS_MAX_SPEED:
1589 osdFormatVelocityStr(buff, stats.max_speed, false, true);
1590 break;
1592 case OSD_3D_SPEED:
1593 osdFormatVelocityStr(buff, osdGet3DSpeed(), true, false);
1594 break;
1596 case OSD_3D_MAX_SPEED:
1597 osdFormatVelocityStr(buff, stats.max_3D_speed, true, true);
1598 break;
1600 case OSD_GLIDESLOPE:
1602 float horizontalSpeed = gpsSol.groundSpeed;
1603 float sinkRate = -getEstimatedActualVelocity(Z);
1604 static pt1Filter_t gsFilterState;
1605 const timeMs_t currentTimeMs = millis();
1606 static timeMs_t gsUpdatedTimeMs;
1607 float glideSlope = horizontalSpeed / sinkRate;
1608 glideSlope = pt1FilterApply4(&gsFilterState, isnormal(glideSlope) ? glideSlope : 200, 0.5, MS2S(currentTimeMs - gsUpdatedTimeMs));
1609 gsUpdatedTimeMs = currentTimeMs;
1611 buff[0] = SYM_GLIDESLOPE;
1612 if (glideSlope > 0.0f && glideSlope < 100.0f) {
1613 osdFormatCentiNumber(buff + 1, glideSlope * 100.0f, 0, 2, 0, 3);
1614 } else {
1615 buff[1] = buff[2] = buff[3] = '-';
1617 buff[4] = '\0';
1618 break;
1621 case OSD_GPS_LAT:
1622 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
1623 break;
1625 case OSD_GPS_LON:
1626 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
1627 break;
1629 case OSD_HOME_DIR:
1631 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
1632 if (GPS_distanceToHome < (navConfig()->general.min_rth_distance / 100) ) {
1633 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_HOME_NEAR);
1635 else
1637 int16_t panHomeDirOffset = 0;
1638 if (!(osdConfig()->pan_servo_pwm2centideg == 0)){
1639 panHomeDirOffset = osdPanServoHomeDirectionOffset();
1641 int homeDirection = GPS_directionToHome - DECIDEGREES_TO_DEGREES(osdGetHeading()) + panHomeDirOffset;
1642 osdDrawDirArrow(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), homeDirection);
1644 } else {
1645 // No home or no fix or unknown heading, blink.
1646 // If we're unarmed, show the arrow pointing up so users can see the arrow
1647 // while configuring the OSD. If we're armed, show a '-' indicating that
1648 // we don't know the direction to home.
1649 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1650 displayWriteCharWithAttr(osdDisplayPort, elemPosX, elemPosY, ARMING_FLAG(ARMED) ? '-' : SYM_ARROW_UP, elemAttr);
1652 return true;
1655 case OSD_HOME_HEADING_ERROR:
1657 buff[0] = SYM_HOME;
1658 buff[1] = SYM_HEADING;
1660 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1661 int16_t h = lrintf(CENTIDEGREES_TO_DEGREES((float)wrap_18000(DEGREES_TO_CENTIDEGREES((int32_t)GPS_directionToHome) - DECIDEGREES_TO_CENTIDEGREES((int32_t)osdGetHeading()))));
1662 tfp_sprintf(buff + 2, "%4d", h);
1663 } else {
1664 strcpy(buff + 2, "----");
1667 buff[6] = SYM_DEGREES;
1668 buff[7] = '\0';
1669 break;
1672 case OSD_HOME_DIST:
1674 buff[0] = SYM_HOME;
1675 osdFormatDistanceSymbol(&buff[1], GPS_distanceToHome * 100, 0);
1676 uint16_t dist_alarm = osdConfig()->dist_alarm;
1677 if (dist_alarm > 0 && GPS_distanceToHome > dist_alarm) {
1678 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1681 break;
1683 case OSD_TRIP_DIST:
1684 buff[0] = SYM_TOTAL;
1685 osdFormatDistanceSymbol(buff + 1, getTotalTravelDistance(), 0);
1686 break;
1688 case OSD_HEADING:
1690 buff[0] = SYM_HEADING;
1691 if (osdIsHeadingValid()) {
1692 int16_t h = DECIDEGREES_TO_DEGREES(osdGetHeading());
1693 if (h < 0) {
1694 h += 360;
1696 tfp_sprintf(&buff[1], "%3d", h);
1697 } else {
1698 buff[1] = buff[2] = buff[3] = '-';
1700 buff[4] = SYM_DEGREES;
1701 buff[5] = '\0';
1702 break;
1705 case OSD_COURSE_HOLD_ERROR:
1707 if (ARMING_FLAG(ARMED) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1708 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1709 return true;
1712 buff[0] = SYM_HEADING;
1714 if ((!ARMING_FLAG(ARMED)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && isAdjustingPosition())) {
1715 buff[1] = buff[2] = buff[3] = '-';
1716 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1717 int16_t herr = lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
1718 if (ABS(herr) > 99)
1719 strcpy(buff + 1, ">99");
1720 else
1721 tfp_sprintf(buff + 1, "%3d", herr);
1724 buff[4] = SYM_DEGREES;
1725 buff[5] = '\0';
1726 break;
1729 case OSD_COURSE_HOLD_ADJUSTMENT:
1731 int16_t heading_adjust = lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
1733 if (ARMING_FLAG(ARMED) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust != 0)))) {
1734 displayWrite(osdDisplayPort, elemPosX, elemPosY, " ");
1735 return true;
1738 buff[0] = SYM_HEADING;
1740 if (!ARMING_FLAG(ARMED)) {
1741 buff[1] = buff[2] = buff[3] = buff[4] = '-';
1742 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE)) {
1743 tfp_sprintf(buff + 1, "%4d", heading_adjust);
1746 buff[5] = SYM_DEGREES;
1747 buff[6] = '\0';
1748 break;
1751 case OSD_GPS_HDOP:
1753 buff[0] = SYM_HDP_L;
1754 buff[1] = SYM_HDP_R;
1755 int32_t centiHDOP = 100 * gpsSol.hdop / HDOP_SCALE;
1756 osdFormatCentiNumber(&buff[2], centiHDOP, 0, 1, 0, 2);
1757 break;
1760 case OSD_MAP_NORTH:
1762 static uint16_t drawn = 0;
1763 static uint32_t scale = 0;
1764 osdDrawHomeMap(0, 'N', &drawn, &scale);
1765 return true;
1767 case OSD_MAP_TAKEOFF:
1769 static uint16_t drawn = 0;
1770 static uint32_t scale = 0;
1771 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn, &scale);
1772 return true;
1774 case OSD_RADAR:
1776 static uint16_t drawn = 0;
1777 static uint32_t scale = 0;
1778 osdDrawRadar(&drawn, &scale);
1779 return true;
1781 #endif // GPS
1783 case OSD_ALTITUDE:
1785 int32_t alt = osdGetAltitude();
1786 osdFormatAltitudeSymbol(buff, alt);
1787 uint16_t alt_alarm = osdConfig()->alt_alarm;
1788 uint16_t neg_alt_alarm = osdConfig()->neg_alt_alarm;
1789 if ((alt_alarm > 0 && CENTIMETERS_TO_METERS(alt) > alt_alarm) ||
1790 (neg_alt_alarm > 0 && alt < 0 && -CENTIMETERS_TO_METERS(alt) > neg_alt_alarm)) {
1792 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1794 break;
1797 case OSD_ALTITUDE_MSL:
1799 int32_t alt = osdGetAltitudeMsl();
1800 osdFormatAltitudeSymbol(buff, alt);
1801 break;
1804 #ifdef USE_RANGEFINDER
1805 case OSD_RANGEFINDER:
1807 int32_t range = rangefinderGetLatestRawAltitude();
1808 if (range < 0) {
1809 buff[0] = '-';
1810 } else {
1811 osdFormatDistanceSymbol(buff, range, 1);
1814 break;
1815 #endif
1817 case OSD_ONTIME:
1819 osdFormatOnTime(buff);
1820 break;
1823 case OSD_FLYTIME:
1825 osdFormatFlyTime(buff, &elemAttr);
1826 break;
1829 case OSD_ONTIME_FLYTIME:
1831 if (ARMING_FLAG(ARMED)) {
1832 osdFormatFlyTime(buff, &elemAttr);
1833 } else {
1834 osdFormatOnTime(buff);
1836 break;
1839 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH:
1841 /*static int32_t updatedTimeSeconds = 0;*/
1842 static int32_t timeSeconds = -1;
1843 #if defined(USE_ADC) && defined(USE_GPS)
1844 static timeUs_t updatedTimestamp = 0;
1845 timeUs_t currentTimeUs = micros();
1846 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
1847 #ifdef USE_WIND_ESTIMATOR
1848 timeSeconds = calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation);
1849 #else
1850 timeSeconds = calculateRemainingFlightTimeBeforeRTH(false);
1851 #endif
1852 updatedTimestamp = currentTimeUs;
1854 #endif
1855 if ((!ARMING_FLAG(ARMED)) || (timeSeconds == -1)) {
1856 buff[0] = SYM_FLY_M;
1857 strcpy(buff + 1, "--:--");
1858 #if defined(USE_ADC) && defined(USE_GPS)
1859 updatedTimestamp = 0;
1860 #endif
1861 } else if (timeSeconds == -2) {
1862 // Wind is too strong to come back with cruise throttle
1863 buff[0] = SYM_FLY_M;
1864 buff[1] = buff[2] = buff[4] = buff[5] = SYM_WIND_HORIZONTAL;
1865 buff[3] = ':';
1866 buff[6] = '\0';
1867 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1868 } else {
1869 osdFormatTime(buff, timeSeconds, SYM_FLY_M, SYM_FLY_H);
1870 if (timeSeconds == 0)
1871 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1874 break;
1876 case OSD_REMAINING_DISTANCE_BEFORE_RTH:;
1877 static int32_t distanceMeters = -1;
1878 #if defined(USE_ADC) && defined(USE_GPS)
1879 static timeUs_t updatedTimestamp = 0;
1880 timeUs_t currentTimeUs = micros();
1881 if (cmpTimeUs(currentTimeUs, updatedTimestamp) >= MS2US(1000)) {
1882 #ifdef USE_WIND_ESTIMATOR
1883 distanceMeters = calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation);
1884 #else
1885 distanceMeters = calculateRemainingDistanceBeforeRTH(false);
1886 #endif
1887 updatedTimestamp = currentTimeUs;
1889 #endif
1890 buff[0] = SYM_TRIP_DIST;
1891 if ((!ARMING_FLAG(ARMED)) || (distanceMeters == -1)) {
1892 buff[4] = SYM_BLANK;
1893 buff[5] = '\0';
1894 strcpy(buff + 1, "---");
1895 } else if (distanceMeters == -2) {
1896 // Wind is too strong to come back with cruise throttle
1897 buff[1] = buff[2] = buff[3] = SYM_WIND_HORIZONTAL;
1898 switch ((osd_unit_e)osdConfig()->units){
1899 case OSD_UNIT_UK:
1900 FALLTHROUGH;
1901 case OSD_UNIT_IMPERIAL:
1902 buff[4] = SYM_DIST_MI;
1903 break;
1904 case OSD_UNIT_METRIC_MPH:
1905 FALLTHROUGH;
1906 case OSD_UNIT_METRIC:
1907 buff[4] = SYM_DIST_KM;
1908 break;
1909 case OSD_UNIT_GA:
1910 buff[4] = SYM_DIST_NM;
1911 break;
1913 buff[5] = '\0';
1914 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1915 } else {
1916 osdFormatDistanceSymbol(buff + 1, distanceMeters * 100, 0);
1917 if (distanceMeters == 0)
1918 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1920 break;
1922 case OSD_FLYMODE:
1924 char *p = "ACRO";
1926 if (FLIGHT_MODE(FAILSAFE_MODE))
1927 p = "!FS!";
1928 else if (FLIGHT_MODE(MANUAL_MODE))
1929 p = "MANU";
1930 else if (FLIGHT_MODE(TURTLE_MODE))
1931 p = "TURT";
1932 else if (FLIGHT_MODE(NAV_RTH_MODE))
1933 p = isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
1934 else if (FLIGHT_MODE(NAV_POSHOLD_MODE))
1935 p = "HOLD";
1936 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE) && FLIGHT_MODE(NAV_ALTHOLD_MODE))
1937 p = "CRUZ";
1938 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE))
1939 p = "CRSH";
1940 else if (FLIGHT_MODE(NAV_WP_MODE))
1941 p = " WP ";
1942 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && navigationRequiresAngleMode()) {
1943 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
1944 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
1945 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
1946 p = " AH ";
1948 else if (FLIGHT_MODE(ANGLE_MODE))
1949 p = "ANGL";
1950 else if (FLIGHT_MODE(HORIZON_MODE))
1951 p = "HOR ";
1953 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
1954 return true;
1957 case OSD_CRAFT_NAME:
1958 osdFormatCraftName(buff);
1959 break;
1961 case OSD_THROTTLE_POS:
1963 osdFormatThrottlePosition(buff, false, NULL);
1964 break;
1967 case OSD_VTX_CHANNEL:
1969 vtxDeviceOsdInfo_t osdInfo;
1970 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
1972 tfp_sprintf(buff, "CH:%c%s:", osdInfo.bandLetter, osdInfo.channelName);
1973 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
1975 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
1976 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1977 displayWriteWithAttr(osdDisplayPort, elemPosX + 6, elemPosY, buff, elemAttr);
1978 return true;
1980 break;
1982 case OSD_VTX_POWER:
1984 vtxDeviceOsdInfo_t osdInfo;
1985 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo);
1987 tfp_sprintf(buff, "%c", osdInfo.powerIndexLetter);
1988 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
1989 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
1990 return true;
1993 #if defined(USE_SERIALRX_CRSF)
1994 case OSD_CRSF_RSSI_DBM:
1996 int16_t rssi = rxLinkStatistics.uplinkRSSI;
1997 buff[0] = (rxLinkStatistics.activeAnt == 0) ? SYM_RSSI : SYM_2RSS; // Separate symbols for each antenna
1998 if (rssi <= -100) {
1999 tfp_sprintf(buff + 1, "%4d%c", rssi, SYM_DBM);
2000 } else {
2001 tfp_sprintf(buff + 1, "%3d%c%c", rssi, SYM_DBM, ' ');
2003 if (!failsafeIsReceivingRxData()){
2004 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2005 } else if (osdConfig()->rssi_dbm_alarm && rssi < osdConfig()->rssi_dbm_alarm) {
2006 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2008 break;
2010 case OSD_CRSF_LQ:
2012 buff[0] = SYM_LQ;
2013 int16_t statsLQ = rxLinkStatistics.uplinkLQ;
2014 int16_t scaledLQ = scaleRange(constrain(statsLQ, 0, 100), 0, 100, 170, 300);
2015 switch (osdConfig()->crsf_lq_format) {
2016 case OSD_CRSF_LQ_TYPE1:
2017 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.uplinkLQ);
2018 break;
2019 case OSD_CRSF_LQ_TYPE2:
2020 tfp_sprintf(buff+1, "%d:%3d", rxLinkStatistics.rfMode, rxLinkStatistics.uplinkLQ);
2021 break;
2022 case OSD_CRSF_LQ_TYPE3:
2023 tfp_sprintf(buff+1, "%3d", rxLinkStatistics.rfMode >= 2 ? scaledLQ : rxLinkStatistics.uplinkLQ);
2024 break;
2026 if (!failsafeIsReceivingRxData()){
2027 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2028 } else if (rxLinkStatistics.uplinkLQ < osdConfig()->link_quality_alarm) {
2029 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2031 break;
2034 case OSD_CRSF_SNR_DB:
2036 static pt1Filter_t snrFilterState;
2037 static timeMs_t snrUpdated = 0;
2038 int8_t snrFiltered = pt1FilterApply4(&snrFilterState, rxLinkStatistics.uplinkSNR, 0.5f, MS2S(millis() - snrUpdated));
2039 snrUpdated = millis();
2041 const char* showsnr = "-20";
2042 const char* hidesnr = " ";
2043 if (snrFiltered > osdConfig()->snr_alarm) {
2044 if (cmsInMenu) {
2045 buff[0] = SYM_SNR;
2046 tfp_sprintf(buff + 1, "%s%c", showsnr, SYM_DB);
2047 } else {
2048 buff[0] = SYM_BLANK;
2049 tfp_sprintf(buff + 1, "%s%c", hidesnr, SYM_BLANK);
2051 } else if (snrFiltered <= osdConfig()->snr_alarm) {
2052 buff[0] = SYM_SNR;
2053 if (snrFiltered <= -10) {
2054 tfp_sprintf(buff + 1, "%3d%c", snrFiltered, SYM_DB);
2055 } else {
2056 tfp_sprintf(buff + 1, "%2d%c%c", snrFiltered, SYM_DB, ' ');
2059 break;
2062 case OSD_CRSF_TX_POWER:
2064 tfp_sprintf(buff, "%4d%c", rxLinkStatistics.uplinkTXPower, SYM_MW);
2065 break;
2067 #endif
2069 case OSD_CROSSHAIRS: // Hud is a sub-element of the crosshair
2071 osdCrosshairPosition(&elemPosX, &elemPosY);
2072 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX, elemPosY);
2074 if (osdConfig()->hud_homing && STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && isImuHeadingValid()) {
2075 osdHudDrawHoming(elemPosX, elemPosY);
2078 if (STATE(GPS_FIX) && isImuHeadingValid()) {
2080 if (osdConfig()->hud_homepoint || osdConfig()->hud_radar_disp > 0 || osdConfig()->hud_wp_disp > 0) {
2081 osdHudClear();
2084 // -------- POI : Home point
2086 if (osdConfig()->hud_homepoint) { // Display the home point (H)
2087 osdHudDrawPoi(GPS_distanceToHome, GPS_directionToHome, -osdGetAltitude() / 100, 0, SYM_HOME, 0 , 0);
2090 // -------- POI : Nearby aircrafts from ESP32 radar
2092 if (osdConfig()->hud_radar_disp > 0) { // Display the POI from the radar
2093 for (uint8_t i = 0; i < osdConfig()->hud_radar_disp; i++) {
2094 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
2095 fpVector3_t poi;
2096 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &radar_pois[i].gps, GEO_ALT_RELATIVE);
2097 radar_pois[i].distance = calculateDistanceToDestination(&poi) / 100; // In meters
2099 if (radar_pois[i].distance >= osdConfig()->hud_radar_range_min && radar_pois[i].distance <= osdConfig()->hud_radar_range_max) {
2100 radar_pois[i].direction = calculateBearingToDestination(&poi) / 100; // In °
2101 radar_pois[i].altitude = (radar_pois[i].gps.alt - osdGetAltitudeMsl()) / 100;
2102 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);
2107 if (osdConfig()->hud_radar_nearest > 0) { // Display extra datas for 1 POI closer than a set distance
2108 int poi_id = radarGetNearestPOI();
2109 if (poi_id >= 0 && radar_pois[poi_id].distance <= osdConfig()->hud_radar_nearest) {
2110 osdHudDrawExtras(poi_id);
2115 // -------- POI : Next waypoints from navigation
2117 if (osdConfig()->hud_wp_disp > 0 && posControl.waypointListValid && posControl.waypointCount > 0) { // Display the next waypoints
2118 gpsLocation_t wp2;
2119 int j;
2121 for (int i = osdConfig()->hud_wp_disp - 1; i >= 0 ; i--) { // Display in reverse order so the next WP is always written on top
2122 j = posControl.activeWaypointIndex + i;
2123 if (j > posControl.waypointCount - 1) { // limit to max WP index for mission
2124 break;
2126 if (posControl.waypointList[j].lat != 0 && posControl.waypointList[j].lon != 0) {
2127 wp2.lat = posControl.waypointList[j].lat;
2128 wp2.lon = posControl.waypointList[j].lon;
2129 wp2.alt = posControl.waypointList[j].alt;
2130 fpVector3_t poi;
2131 geoConvertGeodeticToLocal(&poi, &posControl.gpsOrigin, &wp2, waypointMissionAltConvMode(posControl.waypointList[j].p3));
2132 int32_t altConvModeAltitude = waypointMissionAltConvMode(posControl.waypointList[j].p3) == GEO_ALT_ABSOLUTE ? osdGetAltitudeMsl() : osdGetAltitude();
2133 j = getGeoWaypointNumber(j);
2134 while (j > 9) j -= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2135 osdHudDrawPoi(calculateDistanceToDestination(&poi) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi) / 100), (posControl.waypointList[j].alt - altConvModeAltitude)/ 100, 2, SYM_WAYPOINT, 48 + j, i);
2141 return true;
2142 break;
2144 case OSD_ATTITUDE_ROLL:
2145 buff[0] = SYM_ROLL_LEVEL;
2146 if (ABS(attitude.values.roll) >= 1)
2147 buff[0] += (attitude.values.roll < 0 ? -1 : 1);
2148 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.roll)), 0, 1, 0, 3);
2149 break;
2151 case OSD_ATTITUDE_PITCH:
2152 if (ABS(attitude.values.pitch) < 1)
2153 buff[0] = 'P';
2154 else if (attitude.values.pitch > 0)
2155 buff[0] = SYM_PITCH_DOWN;
2156 else if (attitude.values.pitch < 0)
2157 buff[0] = SYM_PITCH_UP;
2158 osdFormatCentiNumber(buff + 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude.values.pitch)), 0, 1, 0, 3);
2159 break;
2161 case OSD_ARTIFICIAL_HORIZON:
2163 float rollAngle;
2164 float pitchAngle;
2166 #ifdef USE_SECONDARY_IMU
2167 if (secondaryImuState.active && secondaryImuConfig()->useForOsdAHI) {
2168 rollAngle = DECIDEGREES_TO_RADIANS(secondaryImuState.eulerAngles.values.roll);
2169 pitchAngle = DECIDEGREES_TO_RADIANS(secondaryImuState.eulerAngles.values.pitch);
2170 } else {
2171 rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2172 pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2174 #else
2175 rollAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
2176 pitchAngle = DECIDEGREES_TO_RADIANS(attitude.values.pitch);
2177 #endif
2178 pitchAngle -= osdConfig()->ahi_camera_uptilt_comp ? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt) : 0;
2179 pitchAngle += DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2180 if (osdConfig()->ahi_reverse_roll) {
2181 rollAngle = -rollAngle;
2183 osdDrawArtificialHorizon(osdDisplayPort, osdGetDisplayPortCanvas(),
2184 OSD_DRAW_POINT_GRID(elemPosX, elemPosY), rollAngle, pitchAngle);
2185 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
2186 osdDrawSingleElement(OSD_CROSSHAIRS);
2188 return true;
2191 case OSD_HORIZON_SIDEBARS:
2193 osdDrawSidebars(osdDisplayPort, osdGetDisplayPortCanvas());
2194 return true;
2197 #if defined(USE_BARO) || defined(USE_GPS)
2198 case OSD_VARIO:
2200 float zvel = getEstimatedActualVelocity(Z);
2201 osdDrawVario(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), zvel);
2202 return true;
2205 case OSD_VARIO_NUM:
2207 int16_t value = getEstimatedActualVelocity(Z);
2208 char sym;
2209 switch ((osd_unit_e)osdConfig()->units) {
2210 case OSD_UNIT_UK:
2211 FALLTHROUGH;
2212 case OSD_UNIT_IMPERIAL:
2213 // Convert to centifeet/s
2214 value = CENTIMETERS_TO_CENTIFEET(value);
2215 sym = SYM_FTS;
2216 break;
2217 case OSD_UNIT_GA:
2218 // Convert to centi-100feet/min
2219 value = CENTIMETERS_TO_FEET(value * 60);
2220 sym = SYM_100FTM;
2221 break;
2222 default:
2223 case OSD_UNIT_METRIC_MPH:
2224 FALLTHROUGH;
2225 case OSD_UNIT_METRIC:
2226 // Already in cm/s
2227 sym = SYM_MS;
2228 break;
2231 osdFormatCentiNumber(buff, value, 0, 1, 0, 3);
2232 buff[3] = sym;
2233 buff[4] = '\0';
2234 break;
2236 #endif
2238 case OSD_ROLL_PIDS:
2239 osdDisplayFlightPIDValues(elemPosX, elemPosY, "ROL", PID_ROLL, ADJUSTMENT_ROLL_P, ADJUSTMENT_ROLL_I, ADJUSTMENT_ROLL_D, ADJUSTMENT_ROLL_FF);
2240 return true;
2242 case OSD_PITCH_PIDS:
2243 osdDisplayFlightPIDValues(elemPosX, elemPosY, "PIT", PID_PITCH, ADJUSTMENT_PITCH_P, ADJUSTMENT_PITCH_I, ADJUSTMENT_PITCH_D, ADJUSTMENT_PITCH_FF);
2244 return true;
2246 case OSD_YAW_PIDS:
2247 osdDisplayFlightPIDValues(elemPosX, elemPosY, "YAW", PID_YAW, ADJUSTMENT_YAW_P, ADJUSTMENT_YAW_I, ADJUSTMENT_YAW_D, ADJUSTMENT_YAW_FF);
2248 return true;
2250 case OSD_LEVEL_PIDS:
2251 osdDisplayNavPIDValues(elemPosX, elemPosY, "LEV", PID_LEVEL, ADJUSTMENT_LEVEL_P, ADJUSTMENT_LEVEL_I, ADJUSTMENT_LEVEL_D);
2252 return true;
2254 case OSD_POS_XY_PIDS:
2255 osdDisplayNavPIDValues(elemPosX, elemPosY, "PXY", PID_POS_XY, ADJUSTMENT_POS_XY_P, ADJUSTMENT_POS_XY_I, ADJUSTMENT_POS_XY_D);
2256 return true;
2258 case OSD_POS_Z_PIDS:
2259 osdDisplayNavPIDValues(elemPosX, elemPosY, "PZ", PID_POS_Z, ADJUSTMENT_POS_Z_P, ADJUSTMENT_POS_Z_I, ADJUSTMENT_POS_Z_D);
2260 return true;
2262 case OSD_VEL_XY_PIDS:
2263 osdDisplayNavPIDValues(elemPosX, elemPosY, "VXY", PID_VEL_XY, ADJUSTMENT_VEL_XY_P, ADJUSTMENT_VEL_XY_I, ADJUSTMENT_VEL_XY_D);
2264 return true;
2266 case OSD_VEL_Z_PIDS:
2267 osdDisplayNavPIDValues(elemPosX, elemPosY, "VZ", PID_VEL_Z, ADJUSTMENT_VEL_Z_P, ADJUSTMENT_VEL_Z_I, ADJUSTMENT_VEL_Z_D);
2268 return true;
2270 case OSD_HEADING_P:
2271 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "HP", 0, pidBank()->pid[PID_HEADING].P, 3, 0, ADJUSTMENT_HEADING_P);
2272 return true;
2274 case OSD_BOARD_ALIGN_ROLL:
2275 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT);
2276 return true;
2278 case OSD_BOARD_ALIGN_PITCH:
2279 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT);
2280 return true;
2282 case OSD_RC_EXPO:
2283 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "EXP", 0, currentControlRateProfile->stabilized.rcExpo8, 3, 0, ADJUSTMENT_RC_EXPO);
2284 return true;
2286 case OSD_RC_YAW_EXPO:
2287 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "YEX", 0, currentControlRateProfile->stabilized.rcYawExpo8, 3, 0, ADJUSTMENT_RC_YAW_EXPO);
2288 return true;
2290 case OSD_THROTTLE_EXPO:
2291 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "TEX", 0, currentControlRateProfile->throttle.rcExpo8, 3, 0, ADJUSTMENT_THROTTLE_EXPO);
2292 return true;
2294 case OSD_PITCH_RATE:
2295 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SPR");
2297 elemAttr = TEXT_ATTRIBUTES_NONE;
2298 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_PITCH]);
2299 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2300 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2301 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2302 return true;
2304 case OSD_ROLL_RATE:
2305 displayWrite(osdDisplayPort, elemPosX, elemPosY, "SRR");
2307 elemAttr = TEXT_ATTRIBUTES_NONE;
2308 tfp_sprintf(buff, "%3d", currentControlRateProfile->stabilized.rates[FD_ROLL]);
2309 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE))
2310 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2311 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2312 return true;
2314 case OSD_YAW_RATE:
2315 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "SYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2316 return true;
2318 case OSD_MANUAL_RC_EXPO:
2319 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MEX", 0, currentControlRateProfile->manual.rcExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO);
2320 return true;
2322 case OSD_MANUAL_RC_YAW_EXPO:
2323 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYX", 0, currentControlRateProfile->manual.rcYawExpo8, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO);
2324 return true;
2326 case OSD_MANUAL_PITCH_RATE:
2327 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MPR");
2329 elemAttr = TEXT_ATTRIBUTES_NONE;
2330 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_PITCH]);
2331 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2332 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2333 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2334 return true;
2336 case OSD_MANUAL_ROLL_RATE:
2337 displayWrite(osdDisplayPort, elemPosX, elemPosY, "MRR");
2339 elemAttr = TEXT_ATTRIBUTES_NONE;
2340 tfp_sprintf(buff, "%3d", currentControlRateProfile->manual.rates[FD_ROLL]);
2341 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE))
2342 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2343 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY, buff, elemAttr);
2344 return true;
2346 case OSD_MANUAL_YAW_RATE:
2347 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "MYR", 0, currentControlRateProfile->stabilized.rates[FD_YAW], 3, 0, ADJUSTMENT_YAW_RATE);
2348 return true;
2350 case OSD_NAV_FW_CRUISE_THR:
2351 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CRZ", 0, currentBatteryProfile->nav.fw.cruise_throttle, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR);
2352 return true;
2354 case OSD_NAV_FW_PITCH2THR:
2355 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "P2T", 0, currentBatteryProfile->nav.fw.pitch_to_throttle, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR);
2356 return true;
2358 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE:
2359 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "0TP", 0, (float)currentBatteryProfile->fwMinThrottleDownPitchAngle / 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE);
2360 return true;
2362 case OSD_FW_ALT_PID_OUTPUTS:
2364 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2365 osdFormatPidControllerOutput(buff, "PZO", &nav_pids->fw_alt, 10, true); // display requested pitch degrees
2366 break;
2369 case OSD_FW_POS_PID_OUTPUTS:
2371 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers(); // display requested roll degrees
2372 osdFormatPidControllerOutput(buff, "PXYO", &nav_pids->fw_nav, 1, true);
2373 break;
2376 case OSD_MC_VEL_Z_PID_OUTPUTS:
2378 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2379 osdFormatPidControllerOutput(buff, "VZO", &nav_pids->vel[Z], 100, false); // display throttle adjustment µs
2380 break;
2383 case OSD_MC_VEL_X_PID_OUTPUTS:
2385 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2386 osdFormatPidControllerOutput(buff, "VXO", &nav_pids->vel[X], 100, false); // display requested acceleration cm/s^2
2387 break;
2390 case OSD_MC_VEL_Y_PID_OUTPUTS:
2392 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2393 osdFormatPidControllerOutput(buff, "VYO", &nav_pids->vel[Y], 100, false); // display requested acceleration cm/s^2
2394 break;
2397 case OSD_MC_POS_XYZ_P_OUTPUTS:
2399 const navigationPIDControllers_t *nav_pids = getNavigationPIDControllers();
2400 strcpy(buff, "POSO ");
2401 // display requested velocity cm/s
2402 tfp_sprintf(buff + 5, "%4d", (int)lrintf(nav_pids->pos[X].output_constrained * 100));
2403 buff[9] = ' ';
2404 tfp_sprintf(buff + 10, "%4d", (int)lrintf(nav_pids->pos[Y].output_constrained * 100));
2405 buff[14] = ' ';
2406 tfp_sprintf(buff + 15, "%4d", (int)lrintf(nav_pids->pos[Z].output_constrained * 100));
2407 buff[19] = '\0';
2408 break;
2411 case OSD_POWER:
2413 bool kiloWatt = osdFormatCentiNumber(buff, getPower(), 1000, 2, 2, 3);
2414 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
2415 buff[4] = '\0';
2417 uint8_t current_alarm = osdConfig()->current_alarm;
2418 if ((current_alarm > 0) && ((getAmperage() / 100.0f) > current_alarm)) {
2419 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2421 break;
2424 case OSD_AIR_SPEED:
2426 #ifdef USE_PITOT
2427 buff[0] = SYM_AIR;
2428 osdFormatVelocityStr(buff + 1, pitot.airSpeed, false, false);
2429 #else
2430 return false;
2431 #endif
2432 break;
2435 case OSD_RTC_TIME:
2437 // RTC not configured will show 00:00
2438 dateTime_t dateTime;
2439 rtcGetDateTimeLocal(&dateTime);
2440 buff[0] = SYM_CLOCK;
2441 tfp_sprintf(buff + 1, "%02u:%02u:%02u", dateTime.hours, dateTime.minutes, dateTime.seconds);
2442 break;
2445 case OSD_MESSAGES:
2447 elemAttr = osdGetSystemMessage(buff, OSD_MESSAGE_LENGTH, true);
2448 break;
2451 case OSD_VERSION:
2453 tfp_sprintf(buff, "INAV %s", FC_VERSION_STRING);
2454 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2455 break;
2458 case OSD_MAIN_BATT_CELL_VOLTAGE:
2460 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatteryRawAverageCellVoltage(), 3, 2);
2461 return true;
2464 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE:
2466 osdDisplayBatteryVoltage(elemPosX, elemPosY, getBatterySagCompensatedAverageCellVoltage(), 3, 2);
2467 return true;
2470 case OSD_THROTTLE_POS_AUTO_THR:
2472 osdFormatThrottlePosition(buff, true, &elemAttr);
2473 break;
2476 case OSD_HEADING_GRAPH:
2478 if (osdIsHeadingValid()) {
2479 osdDrawHeadingGraph(osdDisplayPort, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX, elemPosY), osdGetHeading());
2480 return true;
2481 } else {
2482 buff[0] = buff[2] = buff[4] = buff[6] = buff[8] = SYM_HEADING_LINE;
2483 buff[1] = buff[3] = buff[5] = buff[7] = SYM_HEADING_DIVIDED_LINE;
2484 buff[OSD_HEADING_GRAPH_WIDTH] = '\0';
2486 break;
2489 case OSD_EFFICIENCY_MAH_PER_KM:
2491 // amperage is in centi amps, speed is in cms/s. We want
2492 // mah/km. Only show when ground speed > 1m/s.
2493 static pt1Filter_t eFilterState;
2494 static timeUs_t efficiencyUpdated = 0;
2495 int32_t value = 0;
2496 bool moreThanAh = false;
2497 timeUs_t currentTimeUs = micros();
2498 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2499 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2500 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2501 value = pt1FilterApply4(&eFilterState, ((float)getAmperage() / gpsSol.groundSpeed) / 0.0036f,
2502 1, US2S(efficiencyTimeDelta));
2504 efficiencyUpdated = currentTimeUs;
2505 } else {
2506 value = eFilterState.state;
2509 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2510 switch (osdConfig()->units) {
2511 case OSD_UNIT_UK:
2512 FALLTHROUGH;
2513 case OSD_UNIT_IMPERIAL:
2514 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10, 1000, 0, 2, 3);
2515 if (!moreThanAh) {
2516 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
2517 } else {
2518 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
2520 if (!efficiencyValid) {
2521 buff[0] = buff[1] = buff[2] = '-';
2522 buff[3] = SYM_MAH_MI_0;
2523 buff[4] = SYM_MAH_MI_1;
2524 buff[5] = '\0';
2526 break;
2527 case OSD_UNIT_GA:
2528 moreThanAh = osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10, 1000, 0, 2, 3);
2529 if (!moreThanAh) {
2530 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
2531 } else {
2532 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
2534 if (!efficiencyValid) {
2535 buff[0] = buff[1] = buff[2] = '-';
2536 buff[3] = SYM_MAH_NM_0;
2537 buff[4] = SYM_MAH_NM_1;
2538 buff[5] = '\0';
2540 break;
2541 case OSD_UNIT_METRIC_MPH:
2542 FALLTHROUGH;
2543 case OSD_UNIT_METRIC:
2544 moreThanAh = osdFormatCentiNumber(buff, value * 100, 1000, 0, 2, 3);
2545 if (!moreThanAh) {
2546 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
2547 } else {
2548 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
2550 if (!efficiencyValid) {
2551 buff[0] = buff[1] = buff[2] = '-';
2552 buff[3] = SYM_MAH_KM_0;
2553 buff[4] = SYM_MAH_KM_1;
2554 buff[5] = '\0';
2556 break;
2558 break;
2561 case OSD_EFFICIENCY_WH_PER_KM:
2563 // amperage is in centi amps, speed is in cms/s. We want
2564 // mWh/km. Only show when ground speed > 1m/s.
2565 static pt1Filter_t eFilterState;
2566 static timeUs_t efficiencyUpdated = 0;
2567 int32_t value = 0;
2568 timeUs_t currentTimeUs = micros();
2569 timeDelta_t efficiencyTimeDelta = cmpTimeUs(currentTimeUs, efficiencyUpdated);
2570 if (STATE(GPS_FIX) && gpsSol.groundSpeed > 0) {
2571 if (efficiencyTimeDelta >= EFFICIENCY_UPDATE_INTERVAL) {
2572 value = pt1FilterApply4(&eFilterState, ((float)getPower() / gpsSol.groundSpeed) / 0.0036f,
2573 1, US2S(efficiencyTimeDelta));
2575 efficiencyUpdated = currentTimeUs;
2576 } else {
2577 value = eFilterState.state;
2580 bool efficiencyValid = (value > 0) && (gpsSol.groundSpeed > 100);
2581 switch (osdConfig()->units) {
2582 case OSD_UNIT_UK:
2583 FALLTHROUGH;
2584 case OSD_UNIT_IMPERIAL:
2585 osdFormatCentiNumber(buff, value * METERS_PER_MILE / 10000, 0, 2, 0, 3);
2586 buff[3] = SYM_WH_MI;
2587 break;
2588 case OSD_UNIT_GA:
2589 osdFormatCentiNumber(buff, value * METERS_PER_NAUTICALMILE / 10000, 0, 2, 0, 3);
2590 buff[3] = SYM_WH_NM;
2591 break;
2592 case OSD_UNIT_METRIC_MPH:
2593 FALLTHROUGH;
2594 case OSD_UNIT_METRIC:
2595 osdFormatCentiNumber(buff, value / 10, 0, 2, 0, 3);
2596 buff[3] = SYM_WH_KM;
2597 break;
2599 buff[4] = '\0';
2600 if (!efficiencyValid) {
2601 buff[0] = buff[1] = buff[2] = '-';
2603 break;
2606 case OSD_GFORCE:
2608 buff[0] = SYM_GFORCE;
2609 osdFormatCentiNumber(buff + 1, GForce, 0, 2, 0, 3);
2610 if (GForce > osdConfig()->gforce_alarm * 100) {
2611 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2613 break;
2616 case OSD_GFORCE_X:
2617 case OSD_GFORCE_Y:
2618 case OSD_GFORCE_Z:
2620 float GForceValue = GForceAxis[item - OSD_GFORCE_X];
2621 buff[0] = SYM_GFORCE_X + item - OSD_GFORCE_X;
2622 osdFormatCentiNumber(buff + 1, GForceValue, 0, 2, 0, 4);
2623 if ((GForceValue < osdConfig()->gforce_axis_alarm_min * 100) || (GForceValue > osdConfig()->gforce_axis_alarm_max * 100)) {
2624 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2626 break;
2628 case OSD_DEBUG:
2631 * Longest representable string is -2147483648 does not fit in the screen.
2632 * Only 7 digits for negative and 8 digits for positive values allowed
2634 for (uint8_t bufferIndex = 0; bufferIndex < DEBUG32_VALUE_COUNT; ++elemPosY, bufferIndex += 2) {
2635 tfp_sprintf(
2636 buff,
2637 "[%u]=%8ld [%u]=%8ld",
2638 bufferIndex,
2639 constrain(debug[bufferIndex], -9999999, 99999999),
2640 bufferIndex+1,
2641 constrain(debug[bufferIndex+1], -9999999, 99999999)
2643 displayWrite(osdDisplayPort, elemPosX, elemPosY, buff);
2645 break;
2648 case OSD_IMU_TEMPERATURE:
2650 int16_t temperature;
2651 const bool valid = getIMUTemperature(&temperature);
2652 osdDisplayTemperature(elemPosX, elemPosY, SYM_IMU_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2653 return true;
2656 case OSD_BARO_TEMPERATURE:
2658 int16_t temperature;
2659 const bool valid = getBaroTemperature(&temperature);
2660 osdDisplayTemperature(elemPosX, elemPosY, SYM_BARO_TEMP, NULL, valid, temperature, osdConfig()->imu_temp_alarm_min, osdConfig()->imu_temp_alarm_max);
2661 return true;
2664 #ifdef USE_TEMPERATURE_SENSOR
2665 case OSD_TEMP_SENSOR_0_TEMPERATURE:
2666 case OSD_TEMP_SENSOR_1_TEMPERATURE:
2667 case OSD_TEMP_SENSOR_2_TEMPERATURE:
2668 case OSD_TEMP_SENSOR_3_TEMPERATURE:
2669 case OSD_TEMP_SENSOR_4_TEMPERATURE:
2670 case OSD_TEMP_SENSOR_5_TEMPERATURE:
2671 case OSD_TEMP_SENSOR_6_TEMPERATURE:
2672 case OSD_TEMP_SENSOR_7_TEMPERATURE:
2674 osdDisplayTemperatureSensor(elemPosX, elemPosY, item - OSD_TEMP_SENSOR_0_TEMPERATURE);
2675 return true;
2677 #endif /* ifdef USE_TEMPERATURE_SENSOR */
2679 case OSD_WIND_SPEED_HORIZONTAL:
2680 #ifdef USE_WIND_ESTIMATOR
2682 bool valid = isEstimatedWindSpeedValid();
2683 float horizontalWindSpeed;
2684 if (valid) {
2685 uint16_t angle;
2686 horizontalWindSpeed = getEstimatedHorizontalWindSpeed(&angle);
2687 int16_t windDirection = osdGetHeadingAngle((int)angle - DECIDEGREES_TO_DEGREES(attitude.values.yaw));
2688 buff[1] = SYM_DIRECTION + (windDirection * 2 / 90);
2689 } else {
2690 horizontalWindSpeed = 0;
2691 buff[1] = SYM_BLANK;
2693 buff[0] = SYM_WIND_HORIZONTAL;
2694 osdFormatWindSpeedStr(buff + 2, horizontalWindSpeed, valid);
2695 break;
2697 #else
2698 return false;
2699 #endif
2701 case OSD_WIND_SPEED_VERTICAL:
2702 #ifdef USE_WIND_ESTIMATOR
2704 buff[0] = SYM_WIND_VERTICAL;
2705 buff[1] = SYM_BLANK;
2706 bool valid = isEstimatedWindSpeedValid();
2707 float verticalWindSpeed;
2708 if (valid) {
2709 verticalWindSpeed = getEstimatedWindSpeed(Z);
2710 if (verticalWindSpeed < 0) {
2711 buff[1] = SYM_AH_DECORATION_DOWN;
2712 verticalWindSpeed = -verticalWindSpeed;
2713 } else if (verticalWindSpeed > 0) {
2714 buff[1] = SYM_AH_DECORATION_UP;
2716 } else {
2717 verticalWindSpeed = 0;
2719 osdFormatWindSpeedStr(buff + 2, verticalWindSpeed, valid);
2720 break;
2722 #else
2723 return false;
2724 #endif
2726 case OSD_PLUS_CODE:
2728 STATIC_ASSERT(GPS_DEGREES_DIVIDER == OLC_DEG_MULTIPLIER, invalid_olc_deg_multiplier);
2729 int digits = osdConfig()->plus_code_digits;
2730 int digitsRemoved = osdConfig()->plus_code_short * 2;
2731 if (STATE(GPS_FIX)) {
2732 olc_encode(gpsSol.llh.lat, gpsSol.llh.lon, digits, buff, sizeof(buff));
2733 } else {
2734 // +codes with > 8 digits have a + at the 9th digit
2735 // and we only support 10 and up.
2736 memset(buff, '-', digits + 1);
2737 buff[8] = '+';
2738 buff[digits + 1] = '\0';
2740 // Optionally trim digits from the left
2741 memmove(buff, buff+digitsRemoved, strlen(buff) + digitsRemoved);
2742 buff[digits + 1 - digitsRemoved] = '\0';
2743 break;
2746 case OSD_AZIMUTH:
2749 buff[0] = SYM_AZIMUTH;
2750 if (osdIsHeadingValid()) {
2751 int16_t h = GPS_directionToHome;
2752 if (h < 0) {
2753 h += 360;
2755 if(h >= 180)
2756 h = h - 180;
2757 else
2758 h = h + 180;
2760 tfp_sprintf(&buff[1], "%3d", h);
2761 } else {
2762 buff[1] = buff[2] = buff[3] = '-';
2764 buff[4] = SYM_DEGREES;
2765 buff[5] = '\0';
2766 break;
2769 case OSD_MAP_SCALE:
2771 float scaleToUnit;
2772 int scaleUnitDivisor;
2773 char symUnscaled;
2774 char symScaled;
2775 int maxDecimals;
2777 switch (osdConfig()->units) {
2778 case OSD_UNIT_UK:
2779 FALLTHROUGH;
2780 case OSD_UNIT_IMPERIAL:
2781 scaleToUnit = 100 / 1609.3440f; // scale to 0.01mi for osdFormatCentiNumber()
2782 scaleUnitDivisor = 0;
2783 symUnscaled = SYM_MI;
2784 symScaled = SYM_MI;
2785 maxDecimals = 2;
2786 break;
2787 case OSD_UNIT_GA:
2788 scaleToUnit = 100 / 1852.0010f; // scale to 0.01mi for osdFormatCentiNumber()
2789 scaleUnitDivisor = 0;
2790 symUnscaled = SYM_NM;
2791 symScaled = SYM_NM;
2792 maxDecimals = 2;
2793 break;
2794 default:
2795 case OSD_UNIT_METRIC_MPH:
2796 FALLTHROUGH;
2797 case OSD_UNIT_METRIC:
2798 scaleToUnit = 100; // scale to cm for osdFormatCentiNumber()
2799 scaleUnitDivisor = 1000; // Convert to km when scale gets bigger than 999m
2800 symUnscaled = SYM_M;
2801 symScaled = SYM_KM;
2802 maxDecimals = 0;
2803 break;
2805 buff[0] = SYM_SCALE;
2806 if (osdMapData.scale > 0) {
2807 bool scaled = osdFormatCentiNumber(&buff[1], osdMapData.scale * scaleToUnit, scaleUnitDivisor, maxDecimals, 2, 3);
2808 buff[4] = scaled ? symScaled : symUnscaled;
2809 // Make sure this is cleared if the map stops being drawn
2810 osdMapData.scale = 0;
2811 } else {
2812 memset(&buff[1], '-', 4);
2814 buff[5] = '\0';
2815 break;
2817 case OSD_MAP_REFERENCE:
2819 char referenceSymbol;
2820 if (osdMapData.referenceSymbol) {
2821 referenceSymbol = osdMapData.referenceSymbol;
2822 // Make sure this is cleared if the map stops being drawn
2823 osdMapData.referenceSymbol = 0;
2824 } else {
2825 referenceSymbol = '-';
2827 displayWriteChar(osdDisplayPort, elemPosX, elemPosY, SYM_DIRECTION);
2828 displayWriteChar(osdDisplayPort, elemPosX, elemPosY + 1, referenceSymbol);
2829 return true;
2832 case OSD_GVAR_0:
2834 osdFormatGVar(buff, 0);
2835 break;
2837 case OSD_GVAR_1:
2839 osdFormatGVar(buff, 1);
2840 break;
2842 case OSD_GVAR_2:
2844 osdFormatGVar(buff, 2);
2845 break;
2847 case OSD_GVAR_3:
2849 osdFormatGVar(buff, 3);
2850 break;
2853 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
2854 case OSD_RC_SOURCE:
2856 const char *source_text = IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
2857 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2858 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, source_text, elemAttr);
2859 return true;
2861 #endif
2863 #if defined(USE_ESC_SENSOR)
2864 case OSD_ESC_RPM:
2866 escSensorData_t * escSensor = escSensorGetData();
2867 if (escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE) {
2868 osdFormatRpm(buff, escSensor->rpm);
2870 else {
2871 osdFormatRpm(buff, 0);
2873 break;
2875 case OSD_ESC_TEMPERATURE:
2877 escSensorData_t * escSensor = escSensorGetData();
2878 bool escTemperatureValid = escSensor && escSensor->dataAge <= ESC_DATA_MAX_AGE;
2879 osdDisplayTemperature(elemPosX, elemPosY, SYM_ESC_TEMP, NULL, escTemperatureValid, (escSensor->temperature)*10, osdConfig()->esc_temp_alarm_min, osdConfig()->esc_temp_alarm_max);
2880 return true;
2882 #endif
2883 case OSD_TPA:
2885 char buff[4];
2886 textAttributes_t attr;
2888 displayWrite(osdDisplayPort, elemPosX, elemPosY, "TPA");
2889 attr = TEXT_ATTRIBUTES_NONE;
2890 tfp_sprintf(buff, "%3d", currentControlRateProfile->throttle.dynPID);
2891 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA)) {
2892 TEXT_ATTRIBUTES_ADD_BLINK(attr);
2894 displayWriteWithAttr(osdDisplayPort, elemPosX + 5, elemPosY, buff, attr);
2896 displayWrite(osdDisplayPort, elemPosX, elemPosY + 1, "BP");
2897 attr = TEXT_ATTRIBUTES_NONE;
2898 tfp_sprintf(buff, "%4d", currentControlRateProfile->throttle.pa_breakpoint);
2899 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT)) {
2900 TEXT_ATTRIBUTES_ADD_BLINK(attr);
2902 displayWriteWithAttr(osdDisplayPort, elemPosX + 4, elemPosY + 1, buff, attr);
2904 return true;
2907 case OSD_NAV_FW_CONTROL_SMOOTHNESS:
2908 osdDisplayAdjustableDecimalValue(elemPosX, elemPosY, "CTL S", 0, navConfig()->fw.control_smoothness, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS);
2909 return true;
2911 #ifdef USE_POWER_LIMITS
2912 case OSD_PLIMIT_REMAINING_BURST_TIME:
2913 osdFormatCentiNumber(buff, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3);
2914 buff[3] = 'S';
2915 buff[4] = '\0';
2916 break;
2918 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT:
2919 if (currentBatteryProfile->powerLimits.continuousCurrent) {
2920 osdFormatCentiNumber(buff, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3);
2921 buff[3] = SYM_AMP;
2922 buff[4] = '\0';
2924 if (powerLimiterIsLimitingCurrent()) {
2925 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2928 break;
2930 #ifdef USE_ADC
2931 case OSD_PLIMIT_ACTIVE_POWER_LIMIT:
2933 if (currentBatteryProfile->powerLimits.continuousPower) {
2934 bool kiloWatt = osdFormatCentiNumber(buff, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3);
2935 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
2936 buff[4] = '\0';
2938 if (powerLimiterIsLimitingPower()) {
2939 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
2942 break;
2944 #endif // USE_ADC
2945 #endif // USE_POWER_LIMITS
2947 default:
2948 return false;
2951 displayWriteWithAttr(osdDisplayPort, elemPosX, elemPosY, buff, elemAttr);
2952 return true;
2955 static uint8_t osdIncElementIndex(uint8_t elementIndex)
2957 ++elementIndex;
2959 if (elementIndex == OSD_ARTIFICIAL_HORIZON)
2960 ++elementIndex;
2962 #ifndef USE_TEMPERATURE_SENSOR
2963 if (elementIndex == OSD_TEMP_SENSOR_0_TEMPERATURE)
2964 elementIndex = OSD_ALTITUDE_MSL;
2965 #endif
2967 if (!sensors(SENSOR_ACC)) {
2968 if (elementIndex == OSD_CROSSHAIRS) {
2969 elementIndex = OSD_ONTIME;
2973 if (!feature(FEATURE_VBAT)) {
2974 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
2975 elementIndex = OSD_LEVEL_PIDS;
2979 if (!feature(FEATURE_CURRENT_METER)) {
2980 if (elementIndex == OSD_CURRENT_DRAW) {
2981 elementIndex = OSD_GPS_SPEED;
2983 if (elementIndex == OSD_EFFICIENCY_MAH_PER_KM) {
2984 elementIndex = OSD_TRIP_DIST;
2986 if (elementIndex == OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH) {
2987 elementIndex = OSD_HOME_HEADING_ERROR;
2989 if (elementIndex == OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE) {
2990 elementIndex = OSD_LEVEL_PIDS;
2994 if (!feature(FEATURE_GPS)) {
2995 if (elementIndex == OSD_GPS_SPEED) {
2996 elementIndex = OSD_ALTITUDE;
2998 if (elementIndex == OSD_GPS_LON) {
2999 elementIndex = OSD_VARIO;
3001 if (elementIndex == OSD_GPS_HDOP) {
3002 elementIndex = OSD_MAIN_BATT_CELL_VOLTAGE;
3004 if (elementIndex == OSD_TRIP_DIST) {
3005 elementIndex = OSD_ATTITUDE_PITCH;
3007 if (elementIndex == OSD_WIND_SPEED_HORIZONTAL) {
3008 elementIndex = OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE;
3010 if (elementIndex == OSD_3D_SPEED) {
3011 elementIndex++;
3015 if (!STATE(ESC_SENSOR_ENABLED)) {
3016 if (elementIndex == OSD_ESC_RPM) {
3017 elementIndex++;
3021 #ifndef USE_POWER_LIMITS
3022 if (elementIndex == OSD_NAV_FW_CONTROL_SMOOTHNESS) {
3023 elementIndex = OSD_ITEM_COUNT;
3025 #endif
3027 if (elementIndex == OSD_ITEM_COUNT) {
3028 elementIndex = 0;
3030 return elementIndex;
3033 void osdDrawNextElement(void)
3035 static uint8_t elementIndex = 0;
3036 // Prevent infinite loop when no elements are enabled
3037 uint8_t index = elementIndex;
3038 do {
3039 elementIndex = osdIncElementIndex(elementIndex);
3040 } while(!osdDrawSingleElement(elementIndex) && index != elementIndex);
3042 // Draw artificial horizon + tracking telemtry last
3043 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
3044 if (osdConfig()->telemetry>0){
3045 osdDisplayTelemetry();
3049 PG_RESET_TEMPLATE(osdConfig_t, osdConfig,
3050 .rssi_alarm = SETTING_OSD_RSSI_ALARM_DEFAULT,
3051 .time_alarm = SETTING_OSD_TIME_ALARM_DEFAULT,
3052 .alt_alarm = SETTING_OSD_ALT_ALARM_DEFAULT,
3053 .dist_alarm = SETTING_OSD_DIST_ALARM_DEFAULT,
3054 .neg_alt_alarm = SETTING_OSD_NEG_ALT_ALARM_DEFAULT,
3055 .current_alarm = SETTING_OSD_CURRENT_ALARM_DEFAULT,
3056 .imu_temp_alarm_min = SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT,
3057 .imu_temp_alarm_max = SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT,
3058 .esc_temp_alarm_min = SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT,
3059 .esc_temp_alarm_max = SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT,
3060 .gforce_alarm = SETTING_OSD_GFORCE_ALARM_DEFAULT,
3061 .gforce_axis_alarm_min = SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT,
3062 .gforce_axis_alarm_max = SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT,
3063 #ifdef USE_BARO
3064 .baro_temp_alarm_min = SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT,
3065 .baro_temp_alarm_max = SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT,
3066 #endif
3067 #ifdef USE_SERIALRX_CRSF
3068 .snr_alarm = SETTING_OSD_SNR_ALARM_DEFAULT,
3069 .crsf_lq_format = SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT,
3070 .link_quality_alarm = SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT,
3071 .rssi_dbm_alarm = SETTING_OSD_RSSI_DBM_ALARM_DEFAULT,
3072 #endif
3073 #ifdef USE_TEMPERATURE_SENSOR
3074 .temp_label_align = SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT,
3075 #endif
3077 .video_system = SETTING_OSD_VIDEO_SYSTEM_DEFAULT,
3078 .row_shiftdown = SETTING_OSD_ROW_SHIFTDOWN_DEFAULT,
3080 .ahi_reverse_roll = SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT,
3081 .ahi_max_pitch = SETTING_OSD_AHI_MAX_PITCH_DEFAULT,
3082 .crosshairs_style = SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT,
3083 .horizon_offset = SETTING_OSD_HORIZON_OFFSET_DEFAULT,
3084 .camera_uptilt = SETTING_OSD_CAMERA_UPTILT_DEFAULT,
3085 .ahi_camera_uptilt_comp = SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT,
3086 .camera_fov_h = SETTING_OSD_CAMERA_FOV_H_DEFAULT,
3087 .camera_fov_v = SETTING_OSD_CAMERA_FOV_V_DEFAULT,
3088 .hud_margin_h = SETTING_OSD_HUD_MARGIN_H_DEFAULT,
3089 .hud_margin_v = SETTING_OSD_HUD_MARGIN_V_DEFAULT,
3090 .hud_homing = SETTING_OSD_HUD_HOMING_DEFAULT,
3091 .hud_homepoint = SETTING_OSD_HUD_HOMEPOINT_DEFAULT,
3092 .hud_radar_disp = SETTING_OSD_HUD_RADAR_DISP_DEFAULT,
3093 .hud_radar_range_min = SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT,
3094 .hud_radar_range_max = SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT,
3095 .hud_radar_nearest = SETTING_OSD_HUD_RADAR_NEAREST_DEFAULT,
3096 .hud_wp_disp = SETTING_OSD_HUD_WP_DISP_DEFAULT,
3097 .left_sidebar_scroll = SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT,
3098 .right_sidebar_scroll = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT,
3099 .sidebar_scroll_arrows = SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT,
3100 .sidebar_horizontal_offset = SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT,
3101 .left_sidebar_scroll_step = SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT,
3102 .right_sidebar_scroll_step = SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT,
3103 .sidebar_height = SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT,
3104 .osd_home_position_arm_screen = SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT,
3105 .pan_servo_index = SETTING_OSD_PAN_SERVO_INDEX_DEFAULT,
3106 .pan_servo_pwm2centideg = SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT,
3108 .units = SETTING_OSD_UNITS_DEFAULT,
3109 .main_voltage_decimals = SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT,
3111 #ifdef USE_WIND_ESTIMATOR
3112 .estimations_wind_compensation = SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT,
3113 #endif
3115 .coordinate_digits = SETTING_OSD_COORDINATE_DIGITS_DEFAULT,
3117 .osd_failsafe_switch_layout = SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT,
3119 .plus_code_digits = SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT,
3120 .plus_code_short = SETTING_OSD_PLUS_CODE_SHORT_DEFAULT,
3122 .ahi_width = SETTING_OSD_AHI_WIDTH_DEFAULT,
3123 .ahi_height = SETTING_OSD_AHI_HEIGHT_DEFAULT,
3124 .ahi_vertical_offset = SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT,
3125 .ahi_bordered = SETTING_OSD_AHI_BORDERED_DEFAULT,
3126 .ahi_style = SETTING_OSD_AHI_STYLE_DEFAULT,
3128 .force_grid = SETTING_OSD_FORCE_GRID_DEFAULT,
3130 .stats_energy_unit = SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT,
3131 .stats_min_voltage_unit = SETTING_OSD_STATS_MIN_VOLTAGE_UNIT_DEFAULT,
3132 .stats_page_auto_swap_time = SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT
3135 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t *osdLayoutsConfig)
3137 osdLayoutsConfig->item_pos[0][OSD_ALTITUDE] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG;
3138 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_VOLTAGE] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG;
3139 osdLayoutsConfig->item_pos[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE] = OSD_POS(12, 1);
3141 osdLayoutsConfig->item_pos[0][OSD_RSSI_VALUE] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG;
3142 //line 2
3143 osdLayoutsConfig->item_pos[0][OSD_HOME_DIST] = OSD_POS(1, 1);
3144 osdLayoutsConfig->item_pos[0][OSD_TRIP_DIST] = OSD_POS(1, 2);
3145 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_CELL_VOLTAGE] = OSD_POS(12, 1);
3146 osdLayoutsConfig->item_pos[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE] = OSD_POS(12, 1);
3147 osdLayoutsConfig->item_pos[0][OSD_GPS_SPEED] = OSD_POS(23, 1);
3148 osdLayoutsConfig->item_pos[0][OSD_3D_SPEED] = OSD_POS(23, 1);
3149 osdLayoutsConfig->item_pos[0][OSD_GLIDESLOPE] = OSD_POS(23, 2);
3151 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG;
3152 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_POS_AUTO_THR] = OSD_POS(6, 2);
3153 osdLayoutsConfig->item_pos[0][OSD_HEADING] = OSD_POS(12, 2);
3154 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ERROR] = OSD_POS(12, 2);
3155 osdLayoutsConfig->item_pos[0][OSD_COURSE_HOLD_ADJUSTMENT] = OSD_POS(12, 2);
3156 osdLayoutsConfig->item_pos[0][OSD_HEADING_GRAPH] = OSD_POS(18, 2);
3157 osdLayoutsConfig->item_pos[0][OSD_CURRENT_DRAW] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG;
3158 osdLayoutsConfig->item_pos[0][OSD_MAH_DRAWN] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG;
3159 osdLayoutsConfig->item_pos[0][OSD_WH_DRAWN] = OSD_POS(1, 5);
3160 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_CAPACITY] = OSD_POS(1, 6);
3161 osdLayoutsConfig->item_pos[0][OSD_BATTERY_REMAINING_PERCENT] = OSD_POS(1, 7);
3162 osdLayoutsConfig->item_pos[0][OSD_POWER_SUPPLY_IMPEDANCE] = OSD_POS(1, 8);
3164 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_MAH_PER_KM] = OSD_POS(1, 5);
3165 osdLayoutsConfig->item_pos[0][OSD_EFFICIENCY_WH_PER_KM] = OSD_POS(1, 5);
3167 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_ROLL] = OSD_POS(1, 7);
3168 osdLayoutsConfig->item_pos[0][OSD_ATTITUDE_PITCH] = OSD_POS(1, 8);
3170 // avoid OSD_VARIO under OSD_CROSSHAIRS
3171 osdLayoutsConfig->item_pos[0][OSD_VARIO] = OSD_POS(23, 5);
3172 // OSD_VARIO_NUM at the right of OSD_VARIO
3173 osdLayoutsConfig->item_pos[0][OSD_VARIO_NUM] = OSD_POS(24, 7);
3174 osdLayoutsConfig->item_pos[0][OSD_HOME_DIR] = OSD_POS(14, 11);
3175 osdLayoutsConfig->item_pos[0][OSD_ARTIFICIAL_HORIZON] = OSD_POS(8, 6);
3176 osdLayoutsConfig->item_pos[0][OSD_HORIZON_SIDEBARS] = OSD_POS(8, 6);
3178 osdLayoutsConfig->item_pos[0][OSD_CRAFT_NAME] = OSD_POS(20, 2);
3179 osdLayoutsConfig->item_pos[0][OSD_VTX_CHANNEL] = OSD_POS(8, 6);
3181 #ifdef USE_SERIALRX_CRSF
3182 osdLayoutsConfig->item_pos[0][OSD_CRSF_RSSI_DBM] = OSD_POS(23, 12);
3183 osdLayoutsConfig->item_pos[0][OSD_CRSF_LQ] = OSD_POS(23, 11);
3184 osdLayoutsConfig->item_pos[0][OSD_CRSF_SNR_DB] = OSD_POS(24, 9);
3185 osdLayoutsConfig->item_pos[0][OSD_CRSF_TX_POWER] = OSD_POS(24, 10);
3186 #endif
3188 osdLayoutsConfig->item_pos[0][OSD_ONTIME] = OSD_POS(23, 8);
3189 osdLayoutsConfig->item_pos[0][OSD_FLYTIME] = OSD_POS(23, 9);
3190 osdLayoutsConfig->item_pos[0][OSD_ONTIME_FLYTIME] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG;
3191 osdLayoutsConfig->item_pos[0][OSD_RTC_TIME] = OSD_POS(23, 12);
3192 osdLayoutsConfig->item_pos[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH] = OSD_POS(23, 7);
3193 osdLayoutsConfig->item_pos[0][OSD_REMAINING_DISTANCE_BEFORE_RTH] = OSD_POS(23, 6);
3195 osdLayoutsConfig->item_pos[0][OSD_GPS_SATS] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG;
3196 osdLayoutsConfig->item_pos[0][OSD_GPS_HDOP] = OSD_POS(0, 10);
3198 osdLayoutsConfig->item_pos[0][OSD_GPS_LAT] = OSD_POS(0, 12);
3199 // Put this on top of the latitude, since it's very unlikely
3200 // that users will want to use both at the same time.
3201 osdLayoutsConfig->item_pos[0][OSD_PLUS_CODE] = OSD_POS(0, 12);
3202 osdLayoutsConfig->item_pos[0][OSD_FLYMODE] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG;
3203 osdLayoutsConfig->item_pos[0][OSD_GPS_LON] = OSD_POS(18, 12);
3205 osdLayoutsConfig->item_pos[0][OSD_AZIMUTH] = OSD_POS(2, 12);
3207 osdLayoutsConfig->item_pos[0][OSD_ROLL_PIDS] = OSD_POS(2, 10);
3208 osdLayoutsConfig->item_pos[0][OSD_PITCH_PIDS] = OSD_POS(2, 11);
3209 osdLayoutsConfig->item_pos[0][OSD_YAW_PIDS] = OSD_POS(2, 12);
3210 osdLayoutsConfig->item_pos[0][OSD_LEVEL_PIDS] = OSD_POS(2, 12);
3211 osdLayoutsConfig->item_pos[0][OSD_POS_XY_PIDS] = OSD_POS(2, 12);
3212 osdLayoutsConfig->item_pos[0][OSD_POS_Z_PIDS] = OSD_POS(2, 12);
3213 osdLayoutsConfig->item_pos[0][OSD_VEL_XY_PIDS] = OSD_POS(2, 12);
3214 osdLayoutsConfig->item_pos[0][OSD_VEL_Z_PIDS] = OSD_POS(2, 12);
3215 osdLayoutsConfig->item_pos[0][OSD_HEADING_P] = OSD_POS(2, 12);
3216 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_ROLL] = OSD_POS(2, 10);
3217 osdLayoutsConfig->item_pos[0][OSD_BOARD_ALIGN_PITCH] = OSD_POS(2, 11);
3218 osdLayoutsConfig->item_pos[0][OSD_RC_EXPO] = OSD_POS(2, 12);
3219 osdLayoutsConfig->item_pos[0][OSD_RC_YAW_EXPO] = OSD_POS(2, 12);
3220 osdLayoutsConfig->item_pos[0][OSD_THROTTLE_EXPO] = OSD_POS(2, 12);
3221 osdLayoutsConfig->item_pos[0][OSD_PITCH_RATE] = OSD_POS(2, 12);
3222 osdLayoutsConfig->item_pos[0][OSD_ROLL_RATE] = OSD_POS(2, 12);
3223 osdLayoutsConfig->item_pos[0][OSD_YAW_RATE] = OSD_POS(2, 12);
3224 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_EXPO] = OSD_POS(2, 12);
3225 osdLayoutsConfig->item_pos[0][OSD_MANUAL_RC_YAW_EXPO] = OSD_POS(2, 12);
3226 osdLayoutsConfig->item_pos[0][OSD_MANUAL_PITCH_RATE] = OSD_POS(2, 12);
3227 osdLayoutsConfig->item_pos[0][OSD_MANUAL_ROLL_RATE] = OSD_POS(2, 12);
3228 osdLayoutsConfig->item_pos[0][OSD_MANUAL_YAW_RATE] = OSD_POS(2, 12);
3229 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_CRUISE_THR] = OSD_POS(2, 12);
3230 osdLayoutsConfig->item_pos[0][OSD_NAV_FW_PITCH2THR] = OSD_POS(2, 12);
3231 osdLayoutsConfig->item_pos[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE] = OSD_POS(2, 12);
3232 osdLayoutsConfig->item_pos[0][OSD_FW_ALT_PID_OUTPUTS] = OSD_POS(2, 12);
3233 osdLayoutsConfig->item_pos[0][OSD_FW_POS_PID_OUTPUTS] = OSD_POS(2, 12);
3234 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_X_PID_OUTPUTS] = OSD_POS(2, 12);
3235 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Y_PID_OUTPUTS] = OSD_POS(2, 12);
3236 osdLayoutsConfig->item_pos[0][OSD_MC_VEL_Z_PID_OUTPUTS] = OSD_POS(2, 12);
3237 osdLayoutsConfig->item_pos[0][OSD_MC_POS_XYZ_P_OUTPUTS] = OSD_POS(2, 12);
3239 osdLayoutsConfig->item_pos[0][OSD_POWER] = OSD_POS(15, 1);
3241 osdLayoutsConfig->item_pos[0][OSD_IMU_TEMPERATURE] = OSD_POS(19, 2);
3242 osdLayoutsConfig->item_pos[0][OSD_BARO_TEMPERATURE] = OSD_POS(19, 3);
3243 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_0_TEMPERATURE] = OSD_POS(19, 4);
3244 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_1_TEMPERATURE] = OSD_POS(19, 5);
3245 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_2_TEMPERATURE] = OSD_POS(19, 6);
3246 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_3_TEMPERATURE] = OSD_POS(19, 7);
3247 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_4_TEMPERATURE] = OSD_POS(19, 8);
3248 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_5_TEMPERATURE] = OSD_POS(19, 9);
3249 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_6_TEMPERATURE] = OSD_POS(19, 10);
3250 osdLayoutsConfig->item_pos[0][OSD_TEMP_SENSOR_7_TEMPERATURE] = OSD_POS(19, 11);
3252 osdLayoutsConfig->item_pos[0][OSD_AIR_SPEED] = OSD_POS(3, 5);
3253 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_HORIZONTAL] = OSD_POS(3, 6);
3254 osdLayoutsConfig->item_pos[0][OSD_WIND_SPEED_VERTICAL] = OSD_POS(3, 7);
3256 osdLayoutsConfig->item_pos[0][OSD_GFORCE] = OSD_POS(12, 4);
3257 osdLayoutsConfig->item_pos[0][OSD_GFORCE_X] = OSD_POS(12, 5);
3258 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Y] = OSD_POS(12, 6);
3259 osdLayoutsConfig->item_pos[0][OSD_GFORCE_Z] = OSD_POS(12, 7);
3261 osdLayoutsConfig->item_pos[0][OSD_VTX_POWER] = OSD_POS(3, 5);
3263 osdLayoutsConfig->item_pos[0][OSD_GVAR_0] = OSD_POS(1, 1);
3264 osdLayoutsConfig->item_pos[0][OSD_GVAR_1] = OSD_POS(1, 2);
3265 osdLayoutsConfig->item_pos[0][OSD_GVAR_2] = OSD_POS(1, 3);
3266 osdLayoutsConfig->item_pos[0][OSD_GVAR_3] = OSD_POS(1, 4);
3268 #if defined(USE_ESC_SENSOR)
3269 osdLayoutsConfig->item_pos[0][OSD_ESC_RPM] = OSD_POS(1, 2);
3270 osdLayoutsConfig->item_pos[0][OSD_ESC_TEMPERATURE] = OSD_POS(1, 3);
3271 #endif
3273 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3274 osdLayoutsConfig->item_pos[0][OSD_RC_SOURCE] = OSD_POS(3, 4);
3275 #endif
3277 #ifdef USE_POWER_LIMITS
3278 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_REMAINING_BURST_TIME] = OSD_POS(3, 4);
3279 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT] = OSD_POS(3, 5);
3280 osdLayoutsConfig->item_pos[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT] = OSD_POS(3, 6);
3281 #endif
3283 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
3284 osdLayoutsConfig->item_pos[0][OSD_MESSAGES] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG;
3286 for (unsigned ii = 1; ii < OSD_LAYOUT_COUNT; ii++) {
3287 for (unsigned jj = 0; jj < ARRAYLEN(osdLayoutsConfig->item_pos[0]); jj++) {
3288 osdLayoutsConfig->item_pos[ii][jj] = osdLayoutsConfig->item_pos[0][jj] & ~OSD_VISIBLE_FLAG;
3293 static void osdSetNextRefreshIn(uint32_t timeMs) {
3294 resumeRefreshAt = micros() + timeMs * 1000;
3295 refreshWaitForResumeCmdRelease = true;
3298 static void osdCompleteAsyncInitialization(void)
3300 if (!displayIsReady(osdDisplayPort)) {
3301 // Update the display.
3302 // XXX: Rename displayDrawScreen() and associated functions
3303 // to displayUpdate()
3304 displayDrawScreen(osdDisplayPort);
3305 return;
3308 osdDisplayIsReady = true;
3310 #if defined(USE_CANVAS)
3311 if (osdConfig()->force_grid) {
3312 osdDisplayHasCanvas = false;
3313 } else {
3314 osdDisplayHasCanvas = displayGetCanvas(&osdCanvas, osdDisplayPort);
3316 #endif
3318 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3319 displayClearScreen(osdDisplayPort);
3321 uint8_t y = 1;
3322 displayFontMetadata_t metadata;
3323 bool fontHasMetadata = displayGetFontMetadata(&metadata, osdDisplayPort);
3324 LOG_D(OSD, "Font metadata version %s: %u (%u chars)",
3325 fontHasMetadata ? "Y" : "N", metadata.version, metadata.charCount);
3327 if (fontHasMetadata && metadata.charCount > 256) {
3328 hasExtendedFont = true;
3329 unsigned logo_c = SYM_LOGO_START;
3330 unsigned logo_x = OSD_CENTER_LEN(SYM_LOGO_WIDTH);
3331 for (unsigned ii = 0; ii < SYM_LOGO_HEIGHT; ii++) {
3332 for (unsigned jj = 0; jj < SYM_LOGO_WIDTH; jj++) {
3333 displayWriteChar(osdDisplayPort, logo_x + jj, y, logo_c++);
3335 y++;
3337 y++;
3338 } else if (!fontHasMetadata) {
3339 const char *m = "INVALID FONT";
3340 displayWrite(osdDisplayPort, OSD_CENTER_S(m), 3, m);
3341 y = 4;
3344 if (fontHasMetadata && metadata.version < OSD_MIN_FONT_VERSION) {
3345 const char *m = "INVALID FONT VERSION";
3346 displayWrite(osdDisplayPort, OSD_CENTER_S(m), y++, m);
3349 char string_buffer[30];
3350 tfp_sprintf(string_buffer, "INAV VERSION: %s", FC_VERSION_STRING);
3351 displayWrite(osdDisplayPort, 5, y++, string_buffer);
3352 #ifdef USE_CMS
3353 displayWrite(osdDisplayPort, 7, y++, CMS_STARTUP_HELP_TEXT1);
3354 displayWrite(osdDisplayPort, 11, y++, CMS_STARTUP_HELP_TEXT2);
3355 displayWrite(osdDisplayPort, 11, y++, CMS_STARTUP_HELP_TEXT3);
3356 #endif
3358 #ifdef USE_STATS
3359 #define STATS_LABEL_X_POS 4
3360 #define STATS_VALUE_X_POS 24
3361 if (statsConfig()->stats_enabled) {
3362 displayWrite(osdDisplayPort, STATS_LABEL_X_POS, ++y, "ODOMETER:");
3363 switch (osdConfig()->units) {
3364 case OSD_UNIT_UK:
3365 FALLTHROUGH;
3366 case OSD_UNIT_IMPERIAL:
3367 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_MILE));
3368 string_buffer[5] = SYM_MI;
3369 break;
3370 default:
3371 case OSD_UNIT_GA:
3372 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_NAUTICALMILE));
3373 string_buffer[5] = SYM_NM;
3374 break;
3375 case OSD_UNIT_METRIC_MPH:
3376 FALLTHROUGH;
3377 case OSD_UNIT_METRIC:
3378 tfp_sprintf(string_buffer, "%5d", (int)(statsConfig()->stats_total_dist / METERS_PER_KILOMETER));
3379 string_buffer[5] = SYM_KM;
3380 break;
3382 string_buffer[6] = '\0';
3383 displayWrite(osdDisplayPort, STATS_VALUE_X_POS-5, y, string_buffer);
3385 displayWrite(osdDisplayPort, STATS_LABEL_X_POS, ++y, "TOTAL TIME:");
3386 uint32_t tot_mins = statsConfig()->stats_total_time / 60;
3387 tfp_sprintf(string_buffer, "%2d:%02dHM", (int)(tot_mins / 60), (int)(tot_mins % 60));
3388 displayWrite(osdDisplayPort, STATS_VALUE_X_POS-5, y, string_buffer);
3390 #ifdef USE_ADC
3391 if (feature(FEATURE_VBAT) && feature(FEATURE_CURRENT_METER)) {
3392 displayWrite(osdDisplayPort, STATS_LABEL_X_POS, ++y, "TOTAL ENERGY:");
3393 osdFormatCentiNumber(string_buffer, statsConfig()->stats_total_energy / 10, 0, 2, 0, 4);
3394 strcat(string_buffer, "\xAB"); // SYM_WH
3395 displayWrite(osdDisplayPort, STATS_VALUE_X_POS-4, y, string_buffer);
3397 displayWrite(osdDisplayPort, STATS_LABEL_X_POS, ++y, "AVG EFFICIENCY:");
3398 if (statsConfig()->stats_total_dist) {
3399 uint32_t avg_efficiency = statsConfig()->stats_total_energy / (statsConfig()->stats_total_dist / METERS_PER_KILOMETER); // mWh/km
3400 switch (osdConfig()->units) {
3401 case OSD_UNIT_UK:
3402 FALLTHROUGH;
3403 case OSD_UNIT_IMPERIAL:
3404 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3405 string_buffer[3] = SYM_WH_MI;
3406 break;
3407 case OSD_UNIT_GA:
3408 osdFormatCentiNumber(string_buffer, avg_efficiency / 10, 0, 2, 0, 3);
3409 string_buffer[3] = SYM_WH_NM;
3410 break;
3411 default:
3412 case OSD_UNIT_METRIC_MPH:
3413 FALLTHROUGH;
3414 case OSD_UNIT_METRIC:
3415 osdFormatCentiNumber(string_buffer, avg_efficiency / 10000 * METERS_PER_MILE, 0, 2, 0, 3);
3416 string_buffer[3] = SYM_WH_KM;
3417 break;
3419 } else {
3420 string_buffer[0] = string_buffer[1] = string_buffer[2] = '-';
3422 string_buffer[4] = '\0';
3423 displayWrite(osdDisplayPort, STATS_VALUE_X_POS-3, y, string_buffer);
3425 #endif // USE_ADC
3427 #endif
3429 displayCommitTransaction(osdDisplayPort);
3430 displayResync(osdDisplayPort);
3431 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME);
3434 void osdInit(displayPort_t *osdDisplayPortToUse)
3436 if (!osdDisplayPortToUse)
3437 return;
3439 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(31,31));
3441 osdDisplayPort = osdDisplayPortToUse;
3443 #ifdef USE_CMS
3444 cmsDisplayPortRegister(osdDisplayPort);
3445 #endif
3447 armState = ARMING_FLAG(ARMED);
3448 osdCompleteAsyncInitialization();
3451 static void osdResetStats(void)
3453 stats.max_current = 0;
3454 stats.max_power = 0;
3455 stats.max_speed = 0;
3456 stats.max_3D_speed = 0;
3457 stats.min_voltage = 5000;
3458 stats.min_rssi = 99;
3459 stats.min_lq = 300;
3460 stats.min_rssi_dbm = 0;
3461 stats.max_altitude = 0;
3464 static void osdUpdateStats(void)
3466 int32_t value;
3468 if (feature(FEATURE_GPS)) {
3469 value = osdGet3DSpeed();
3470 if (stats.max_3D_speed < value)
3471 stats.max_3D_speed = value;
3473 if (stats.max_speed < gpsSol.groundSpeed)
3474 stats.max_speed = gpsSol.groundSpeed;
3476 if (stats.max_distance < GPS_distanceToHome)
3477 stats.max_distance = GPS_distanceToHome;
3480 value = getBatteryVoltage();
3481 if (stats.min_voltage > value)
3482 stats.min_voltage = value;
3484 value = abs(getAmperage());
3485 if (stats.max_current < value)
3486 stats.max_current = value;
3488 value = labs(getPower());
3489 if (stats.max_power < value)
3490 stats.max_power = value;
3492 value = osdConvertRSSI();
3493 if (stats.min_rssi > value)
3494 stats.min_rssi = value;
3496 value = osdGetCrsfLQ();
3497 if (stats.min_lq > value)
3498 stats.min_lq = value;
3500 value = osdGetCrsfdBm();
3501 if (stats.min_rssi_dbm > value)
3502 stats.min_rssi_dbm = value;
3504 stats.max_altitude = MAX(stats.max_altitude, osdGetAltitude());
3507 static void osdShowStatsPage1(void)
3509 const char * disarmReasonStr[DISARM_REASON_COUNT] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "KILLSW", "FAILSAFE", "NAV SYS" };
3510 uint8_t top = 1; /* first fully visible line */
3511 const uint8_t statNameX = 1;
3512 const uint8_t statValuesX = 20;
3513 char buff[10];
3514 statsPagesCheck = 1;
3516 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3517 displayClearScreen(osdDisplayPort);
3519 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- 1/2 ->");
3521 if (feature(FEATURE_GPS)) {
3522 displayWrite(osdDisplayPort, statNameX, top, "MAX SPEED :");
3523 osdFormatVelocityStr(buff, stats.max_3D_speed, true, false);
3524 osdLeftAlignString(buff);
3525 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3527 displayWrite(osdDisplayPort, statNameX, top, "MAX DISTANCE :");
3528 osdFormatDistanceStr(buff, stats.max_distance*100);
3529 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3531 displayWrite(osdDisplayPort, statNameX, top, "TRAVELED DISTANCE:");
3532 osdFormatDistanceStr(buff, getTotalTravelDistance());
3533 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3536 displayWrite(osdDisplayPort, statNameX, top, "MAX ALTITUDE :");
3537 osdFormatAltitudeStr(buff, stats.max_altitude);
3538 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3540 switch (rxConfig()->serialrx_provider) {
3541 case SERIALRX_CRSF:
3542 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI % :");
3543 itoa(stats.min_rssi, buff, 10);
3544 strcat(buff, "%");
3545 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3547 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI DBM :");
3548 itoa(stats.min_rssi_dbm, buff, 10);
3549 tfp_sprintf(buff, "%s%c", buff, SYM_DBM);
3550 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3552 displayWrite(osdDisplayPort, statNameX, top, "MIN LQ :");
3553 itoa(stats.min_lq, buff, 10);
3554 strcat(buff, "%");
3555 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3556 break;
3557 default:
3558 displayWrite(osdDisplayPort, statNameX, top, "MIN RSSI :");
3559 itoa(stats.min_rssi, buff, 10);
3560 strcat(buff, "%");
3561 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3564 displayWrite(osdDisplayPort, statNameX, top, "FLY TIME :");
3565 uint16_t flySeconds = getFlightTime();
3566 uint16_t flyMinutes = flySeconds / 60;
3567 flySeconds %= 60;
3568 uint16_t flyHours = flyMinutes / 60;
3569 flyMinutes %= 60;
3570 tfp_sprintf(buff, "%02u:%02u:%02u", flyHours, flyMinutes, flySeconds);
3571 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3573 displayWrite(osdDisplayPort, statNameX, top, "DISARMED BY :");
3574 displayWrite(osdDisplayPort, statValuesX, top++, disarmReasonStr[getDisarmReason()]);
3575 displayCommitTransaction(osdDisplayPort);
3578 static void osdShowStatsPage2(void)
3580 uint8_t top = 1; /* first fully visible line */
3581 const uint8_t statNameX = 1;
3582 const uint8_t statValuesX = 20;
3583 char buff[10];
3584 statsPagesCheck = 1;
3586 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3587 displayClearScreen(osdDisplayPort);
3589 displayWrite(osdDisplayPort, statNameX, top++, "--- STATS --- <- 2/2");
3591 if (osdConfig()->stats_min_voltage_unit == OSD_STATS_MIN_VOLTAGE_UNIT_BATTERY) {
3592 displayWrite(osdDisplayPort, statNameX, top, "MIN BATTERY VOLT :");
3593 osdFormatCentiNumber(buff, stats.min_voltage, 0, osdConfig()->main_voltage_decimals, 0, osdConfig()->main_voltage_decimals + 2);
3594 } else {
3595 displayWrite(osdDisplayPort, statNameX, top, "MIN CELL VOLTAGE :");
3596 osdFormatCentiNumber(buff, stats.min_voltage/getBatteryCellCount(), 0, 2, 0, 3);
3598 tfp_sprintf(buff, "%s%c", buff, SYM_VOLT);
3599 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3601 if (feature(FEATURE_CURRENT_METER)) {
3602 displayWrite(osdDisplayPort, statNameX, top, "MAX CURRENT :");
3603 osdFormatCentiNumber(buff, stats.max_current, 0, 2, 0, 3);
3604 tfp_sprintf(buff, "%s%c", buff, SYM_AMP);
3605 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3607 displayWrite(osdDisplayPort, statNameX, top, "MAX POWER :");
3608 bool kiloWatt = osdFormatCentiNumber(buff, stats.max_power, 1000, 2, 2, 3);
3609 buff[3] = kiloWatt ? SYM_KILOWATT : SYM_WATT;
3610 buff[4] = '\0';
3611 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3613 displayWrite(osdDisplayPort, statNameX, top, "USED CAPACITY :");
3614 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3615 tfp_sprintf(buff, "%d%c", (int)getMAhDrawn(), SYM_MAH);
3616 } else {
3617 osdFormatCentiNumber(buff, getMWhDrawn() / 10, 0, 2, 0, 3);
3618 tfp_sprintf(buff, "%s%c", buff, SYM_WH);
3620 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3622 int32_t totalDistance = getTotalTravelDistance();
3623 bool moreThanAh = false;
3624 bool efficiencyValid = totalDistance >= 10000;
3625 if (feature(FEATURE_GPS)) {
3626 displayWrite(osdDisplayPort, statNameX, top, "AVG EFFICIENCY :");
3627 switch (osdConfig()->units) {
3628 case OSD_UNIT_UK:
3629 FALLTHROUGH;
3630 case OSD_UNIT_IMPERIAL:
3631 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3632 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_MILE / totalDistance), 1000, 0, 2, 3);
3633 if (!moreThanAh) {
3634 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_MI_0, SYM_MAH_MI_1);
3635 } else {
3636 tfp_sprintf(buff, "%s%c", buff, SYM_AH_MI);
3638 if (!efficiencyValid) {
3639 buff[0] = buff[1] = buff[2] = '-';
3640 buff[3] = SYM_MAH_MI_0;
3641 buff[4] = SYM_MAH_MI_1;
3642 buff[5] = '\0';
3644 } else {
3645 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_MILE / totalDistance), 0, 2, 0, 3);
3646 tfp_sprintf(buff, "%s%c", buff, SYM_WH_MI);
3647 if (!efficiencyValid) {
3648 buff[0] = buff[1] = buff[2] = '-';
3651 break;
3652 case OSD_UNIT_GA:
3653 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3654 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000.0f * METERS_PER_NAUTICALMILE / totalDistance), 1000, 0, 2, 3);
3655 if (!moreThanAh) {
3656 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_NM_0, SYM_MAH_NM_1);
3657 } else {
3658 tfp_sprintf(buff, "%s%c", buff, SYM_AH_NM);
3660 if (!efficiencyValid) {
3661 buff[0] = buff[1] = buff[2] = '-';
3662 buff[3] = SYM_MAH_NM_0;
3663 buff[4] = SYM_MAH_NM_1;
3664 buff[5] = '\0';
3666 } else {
3667 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10.0f * METERS_PER_NAUTICALMILE / totalDistance), 0, 2, 0, 3);
3668 tfp_sprintf(buff, "%s%c", buff, SYM_WH_NM);
3669 if (!efficiencyValid) {
3670 buff[0] = buff[1] = buff[2] = '-';
3673 break;
3674 case OSD_UNIT_METRIC_MPH:
3675 FALLTHROUGH;
3676 case OSD_UNIT_METRIC:
3677 if (osdConfig()->stats_energy_unit == OSD_STATS_ENERGY_UNIT_MAH) {
3678 moreThanAh = osdFormatCentiNumber(buff, (int32_t)(getMAhDrawn() * 10000000.0f / totalDistance), 1000, 0, 2, 3);
3679 if (!moreThanAh) {
3680 tfp_sprintf(buff, "%s%c%c", buff, SYM_MAH_KM_0, SYM_MAH_KM_1);
3681 } else {
3682 tfp_sprintf(buff, "%s%c", buff, SYM_AH_KM);
3684 if (!efficiencyValid) {
3685 buff[0] = buff[1] = buff[2] = '-';
3686 buff[3] = SYM_MAH_KM_0;
3687 buff[4] = SYM_MAH_KM_1;
3688 buff[5] = '\0';
3690 } else {
3691 osdFormatCentiNumber(buff, (int32_t)(getMWhDrawn() * 10000.0f / totalDistance), 0, 2, 0, 3);
3692 tfp_sprintf(buff, "%s%c", buff, SYM_WH_KM);
3693 if (!efficiencyValid) {
3694 buff[0] = buff[1] = buff[2] = '-';
3697 break;
3699 osdLeftAlignString(buff);
3700 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3704 const float max_gforce = accGetMeasuredMaxG();
3705 displayWrite(osdDisplayPort, statNameX, top, "MAX G-FORCE :");
3706 osdFormatCentiNumber(buff, max_gforce * 100, 0, 2, 0, 3);
3707 displayWrite(osdDisplayPort, statValuesX, top++, buff);
3709 const acc_extremes_t *acc_extremes = accGetMeasuredExtremes();
3710 displayWrite(osdDisplayPort, statNameX, top, "MIN/MAX Z G-FORCE:");
3711 osdFormatCentiNumber(buff, acc_extremes[Z].min * 100, 0, 2, 0, 4);
3712 strcat(buff,"/");
3713 displayWrite(osdDisplayPort, statValuesX - 1, top, buff);
3714 osdFormatCentiNumber(buff, acc_extremes[Z].max * 100, 0, 2, 0, 3);
3715 displayWrite(osdDisplayPort, statValuesX + 4, top++, buff);
3716 displayCommitTransaction(osdDisplayPort);
3719 // called when motors armed
3720 static void osdShowArmed(void)
3722 dateTime_t dt;
3723 char buf[MAX(32, FORMATTED_DATE_TIME_BUFSIZE)];
3724 char craftNameBuf[MAX_NAME_LENGTH];
3725 char versionBuf[30];
3726 char *date;
3727 char *time;
3728 // We need 12 visible rows, start row never < first fully visible row 1
3729 uint8_t y = osdDisplayPort->rows > 13 ? (osdDisplayPort->rows - 12) / 2 : 1;
3731 displayClearScreen(osdDisplayPort);
3732 displayWrite(osdDisplayPort, 12, y, "ARMED");
3733 y += 2;
3735 if (strlen(systemConfig()->name) > 0) {
3736 osdFormatCraftName(craftNameBuf);
3737 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(systemConfig() -> name)) / 2, y, craftNameBuf );
3738 y += 1;
3741 if (posControl.waypointListValid && posControl.waypointCount > 0) {
3742 displayWrite(osdDisplayPort, 7, y, "*MISSION LOADED*");
3744 y += 1;
3746 #if defined(USE_GPS)
3747 if (feature(FEATURE_GPS)) {
3748 if (STATE(GPS_FIX_HOME)) {
3749 if (osdConfig()->osd_home_position_arm_screen){
3750 osdFormatCoordinate(buf, SYM_LAT, GPS_home.lat);
3751 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
3752 osdFormatCoordinate(buf, SYM_LON, GPS_home.lon);
3753 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 1, buf);
3754 int digits = osdConfig()->plus_code_digits;
3755 olc_encode(GPS_home.lat, GPS_home.lon, digits, buf, sizeof(buf));
3756 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y + 2, buf);
3758 y += 4;
3759 #if defined (USE_SAFE_HOME)
3760 if (safehome_distance) { // safehome found during arming
3761 if (navConfig()->general.flags.safehome_usage_mode == SAFEHOME_USAGE_OFF) {
3762 strcpy(buf, "SAFEHOME FOUND; MODE OFF");
3763 } else {
3764 char buf2[12]; // format the distance first
3765 osdFormatDistanceStr(buf2, safehome_distance);
3766 tfp_sprintf(buf, "%c - %s -> SAFEHOME %u", SYM_HOME, buf2, safehome_index);
3768 textAttributes_t elemAttr = _TEXT_ATTRIBUTES_BLINK_BIT;
3769 // write this message above the ARMED message to make it obvious
3770 displayWriteWithAttr(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y - 8, buf, elemAttr);
3772 #endif
3773 } else {
3774 strcpy(buf, "!NO HOME POSITION!");
3775 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(buf)) / 2, y, buf);
3776 y += 1;
3779 #endif
3781 if (rtcGetDateTime(&dt)) {
3782 dateTimeFormatLocal(buf, &dt);
3783 dateTimeSplitFormatted(buf, &date, &time);
3785 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(date)) / 2, y, date);
3786 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(time)) / 2, y + 1, time);
3787 y += 3;
3790 tfp_sprintf(versionBuf, "INAV VERSION: %s", FC_VERSION_STRING);
3791 displayWrite(osdDisplayPort, (osdDisplayPort->cols - strlen(versionBuf)) / 2, y, versionBuf);
3794 static void osdFilterData(timeUs_t currentTimeUs) {
3795 static timeUs_t lastRefresh = 0;
3796 float refresh_dT = US2S(cmpTimeUs(currentTimeUs, lastRefresh));
3798 GForce = fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF)) / GRAVITY_MSS;
3799 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) GForceAxis[axis] = imuMeasuredAccelBF.v[axis] / GRAVITY_MSS;
3801 if (lastRefresh) {
3802 GForce = pt1FilterApply3(&GForceFilter, GForce, refresh_dT);
3803 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) pt1FilterApply3(GForceFilterAxis + axis, GForceAxis[axis], refresh_dT);
3804 } else {
3805 pt1FilterInitRC(&GForceFilter, GFORCE_FILTER_TC, 0);
3806 pt1FilterReset(&GForceFilter, GForce);
3808 for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; ++axis) {
3809 pt1FilterInitRC(GForceFilterAxis + axis, GFORCE_FILTER_TC, 0);
3810 pt1FilterReset(GForceFilterAxis + axis, GForceAxis[axis]);
3814 lastRefresh = currentTimeUs;
3817 static void osdRefresh(timeUs_t currentTimeUs)
3819 osdFilterData(currentTimeUs);
3821 #ifdef USE_CMS
3822 if (IS_RC_MODE_ACTIVE(BOXOSD) && (!cmsInMenu) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
3823 #else
3824 if (IS_RC_MODE_ACTIVE(BOXOSD) && !(osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE))) {
3825 #endif
3826 displayClearScreen(osdDisplayPort);
3827 armState = ARMING_FLAG(ARMED);
3828 return;
3831 // detect arm/disarm
3832 static uint8_t statsPageAutoSwapCntl = 2;
3833 if (armState != ARMING_FLAG(ARMED)) {
3834 if (ARMING_FLAG(ARMED)) {
3835 osdResetStats();
3836 statsPageAutoSwapCntl = 2;
3837 osdShowArmed(); // reset statistic etc
3838 uint32_t delay = ARMED_SCREEN_DISPLAY_TIME;
3839 statsPagesCheck = 0;
3840 #if defined(USE_SAFE_HOME)
3841 if (safehome_distance)
3842 delay *= 3;
3843 #endif
3844 osdSetNextRefreshIn(delay);
3845 } else {
3846 osdShowStatsPage1(); // show first page of statistics
3847 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME);
3848 statsPageAutoSwapCntl = osdConfig()->stats_page_auto_swap_time > 0 ? 0 : 2; // disable swapping pages when time = 0
3851 armState = ARMING_FLAG(ARMED);
3854 if (resumeRefreshAt) {
3855 // If we already reached he time for the next refresh,
3856 // or THR is high or PITCH is high, resume refreshing.
3857 // Clear the screen first to erase other elements which
3858 // might have been drawn while the OSD wasn't refreshing.
3860 // auto swap stats pages when first shown
3861 // auto swap cancelled using roll stick
3862 if (statsPageAutoSwapCntl != 2) {
3863 if (STATS_PAGE1 || STATS_PAGE2) {
3864 statsPageAutoSwapCntl = 2;
3865 } else {
3866 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time * 1000), 2)) {
3867 if (statsPageAutoSwapCntl == 0) {
3868 osdShowStatsPage1();
3869 statsPageAutoSwapCntl = 1;
3871 } else {
3872 if (statsPageAutoSwapCntl == 1) {
3873 osdShowStatsPage2();
3874 statsPageAutoSwapCntl = 0;
3880 if (!DELAYED_REFRESH_RESUME_COMMAND)
3881 refreshWaitForResumeCmdRelease = false;
3883 if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && DELAYED_REFRESH_RESUME_COMMAND)) {
3884 displayClearScreen(osdDisplayPort);
3885 resumeRefreshAt = 0;
3886 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE1)) {
3887 if (statsPagesCheck == 1) {
3888 osdShowStatsPage1();
3890 } else if ((currentTimeUs > resumeRefreshAt) || ((!refreshWaitForResumeCmdRelease) && STATS_PAGE2)) {
3891 if (statsPagesCheck == 1) {
3892 osdShowStatsPage2();
3894 } else {
3895 displayHeartbeat(osdDisplayPort);
3897 return;
3900 #ifdef USE_CMS
3901 if (!displayIsGrabbed(osdDisplayPort)) {
3902 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
3903 if (fullRedraw) {
3904 displayClearScreen(osdDisplayPort);
3905 fullRedraw = false;
3907 osdDrawNextElement();
3908 displayHeartbeat(osdDisplayPort);
3909 displayCommitTransaction(osdDisplayPort);
3910 #ifdef OSD_CALLS_CMS
3911 } else {
3912 cmsUpdate(currentTimeUs);
3913 #endif
3915 #endif
3919 * Called periodically by the scheduler
3921 void osdUpdate(timeUs_t currentTimeUs)
3923 static uint32_t counter = 0;
3925 // don't touch buffers if DMA transaction is in progress
3926 if (displayIsTransferInProgress(osdDisplayPort)) {
3927 return;
3930 if (!osdDisplayIsReady) {
3931 osdCompleteAsyncInitialization();
3932 return;
3935 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
3936 // Check if the layout has changed. Higher numbered
3937 // boxes take priority.
3938 unsigned activeLayout;
3939 if (layoutOverride >= 0) {
3940 activeLayout = layoutOverride;
3941 // Check for timed override, it will go into effect on
3942 // the next OSD iteration
3943 if (layoutOverrideUntil > 0 && millis() > layoutOverrideUntil) {
3944 layoutOverrideUntil = 0;
3945 layoutOverride = -1;
3947 } else if (osdConfig()->osd_failsafe_switch_layout && FLIGHT_MODE(FAILSAFE_MODE)) {
3948 activeLayout = 0;
3949 } else {
3950 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
3951 if (IS_RC_MODE_ACTIVE(BOXOSDALT3))
3952 activeLayout = 3;
3953 else
3954 #endif
3955 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
3956 if (IS_RC_MODE_ACTIVE(BOXOSDALT2))
3957 activeLayout = 2;
3958 else
3959 #endif
3960 if (IS_RC_MODE_ACTIVE(BOXOSDALT1))
3961 activeLayout = 1;
3962 else
3963 #ifdef USE_PROGRAMMING_FRAMEWORK
3964 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT))
3965 activeLayout = constrain(logicConditionValuesByType[LOGIC_CONDITION_SET_OSD_LAYOUT], 0, OSD_ALTERNATE_LAYOUT_COUNT);
3966 else
3967 #endif
3968 activeLayout = 0;
3970 if (currentLayout != activeLayout) {
3971 currentLayout = activeLayout;
3972 osdStartFullRedraw();
3974 #endif
3976 #define DRAW_FREQ_DENOM 4
3977 #define STATS_FREQ_DENOM 50
3978 counter++;
3980 if ((counter % STATS_FREQ_DENOM) == 0) {
3981 osdUpdateStats();
3984 if ((counter & DRAW_FREQ_DENOM) == 0) {
3985 // redraw values in buffer
3986 osdRefresh(currentTimeUs);
3987 } else {
3988 // rest of time redraw screen
3989 displayDrawScreen(osdDisplayPort);
3992 #ifdef USE_CMS
3993 // do not allow ARM if we are in menu
3994 if (displayIsGrabbed(osdDisplayPort)) {
3995 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
3996 } else {
3997 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU);
3999 #endif
4002 void osdStartFullRedraw(void)
4004 fullRedraw = true;
4007 void osdOverrideLayout(int layout, timeMs_t duration)
4009 layoutOverride = constrain(layout, -1, ARRAYLEN(osdLayoutsConfig()->item_pos) - 1);
4010 if (layoutOverride >= 0 && duration > 0) {
4011 layoutOverrideUntil = millis() + duration;
4012 } else {
4013 layoutOverrideUntil = 0;
4017 int osdGetActiveLayout(bool *overridden)
4019 if (overridden) {
4020 *overridden = layoutOverride >= 0;
4022 return currentLayout;
4025 bool osdItemIsFixed(osd_items_e item)
4027 return item == OSD_CROSSHAIRS ||
4028 item == OSD_ARTIFICIAL_HORIZON ||
4029 item == OSD_HORIZON_SIDEBARS;
4032 displayPort_t *osdGetDisplayPort(void)
4034 return osdDisplayPort;
4037 displayCanvas_t *osdGetDisplayPortCanvas(void)
4039 #if defined(USE_CANVAS)
4040 if (osdDisplayHasCanvas) {
4041 return &osdCanvas;
4043 #endif
4044 return NULL;
4047 textAttributes_t osdGetSystemMessage(char *buff, size_t buff_size, bool isCenteredText)
4049 textAttributes_t elemAttr = TEXT_ATTRIBUTES_NONE;
4051 if (buff != NULL) {
4052 const char *message = NULL;
4053 char messageBuf[MAX(SETTING_MAX_NAME_LENGTH, OSD_MESSAGE_LENGTH+1)];
4054 if (ARMING_FLAG(ARMED)) {
4055 // Aircraft is armed. We might have up to 5
4056 // messages to show.
4057 const char *messages[5];
4058 unsigned messageCount = 0;
4059 if (FLIGHT_MODE(FAILSAFE_MODE)) {
4060 // In FS mode while being armed too
4061 const char *failsafePhaseMessage = osdFailsafePhaseMessage();
4062 const char *failsafeInfoMessage = osdFailsafeInfoMessage();
4063 const char *navStateFSMessage = navigationStateMessage();
4065 if (failsafePhaseMessage) {
4066 messages[messageCount++] = failsafePhaseMessage;
4068 if (failsafeInfoMessage) {
4069 messages[messageCount++] = failsafeInfoMessage;
4071 if (navStateFSMessage) {
4072 messages[messageCount++] = navStateFSMessage;
4074 #if defined(USE_SAFE_HOME)
4075 const char *safehomeMessage = divertingToSafehomeMessage();
4076 if (safehomeMessage) {
4077 messages[messageCount++] = safehomeMessage;
4079 #endif
4080 if (messageCount > 0) {
4081 message = messages[OSD_ALTERNATING_CHOICES(1000, messageCount)];
4082 if (message == failsafeInfoMessage) {
4083 // failsafeInfoMessage is not useful for recovering
4084 // a lost model, but might help avoiding a crash.
4085 // Blink to grab user attention.
4086 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr);
4088 // We're shoing either failsafePhaseMessage or
4089 // navStateFSMessage. Don't BLINK here since
4090 // having this text available might be crucial
4091 // during a lost aircraft recovery and blinking
4092 // will cause it to be missing from some frames.
4094 } else {
4095 if (FLIGHT_MODE(NAV_RTH_MODE) || FLIGHT_MODE(NAV_WP_MODE) || navigationIsExecutingAnEmergencyLanding()) {
4096 if (isWaypointMissionRTHActive()) {
4097 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
4098 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL);
4101 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE) {
4102 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED);
4103 } else if (NAV_Status.state == MW_NAV_STATE_WP_ENROUTE) {
4104 // Countdown display for remaining Waypoints
4105 char buf[6];
4106 osdFormatDistanceSymbol(buf, posControl.wpDistance, 0);
4107 tfp_sprintf(messageBuf, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl.activeWaypointIndex), posControl.geoWaypointCount, buf);
4108 messages[messageCount++] = messageBuf;
4109 } else if (NAV_Status.state == MW_NAV_STATE_HOLD_TIMED) {
4110 // WP hold time countdown in seconds
4111 timeMs_t currentTime = millis();
4112 int holdTimeRemaining = posControl.waypointList[posControl.activeWaypointIndex].p1 - (int)((currentTime - posControl.wpReachedTime)/1000);
4113 if (holdTimeRemaining >=0) {
4114 tfp_sprintf(messageBuf, "HOLDING WP FOR %2u S", holdTimeRemaining);
4115 messages[messageCount++] = messageBuf;
4117 } else {
4118 const char *navStateMessage = navigationStateMessage();
4119 if (navStateMessage) {
4120 messages[messageCount++] = navStateMessage;
4123 #if defined(USE_SAFE_HOME)
4124 const char *safehomeMessage = divertingToSafehomeMessage();
4125 if (safehomeMessage) {
4126 messages[messageCount++] = safehomeMessage;
4128 #endif
4129 } else if (STATE(FIXED_WING_LEGACY) && (navGetCurrentStateFlags() & NAV_CTL_LAUNCH)) {
4130 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH);
4131 const char *launchStateMessage = fixedWingLaunchStateMessage();
4132 if (launchStateMessage) {
4133 messages[messageCount++] = launchStateMessage;
4135 } else {
4136 if (FLIGHT_MODE(NAV_ALTHOLD_MODE) && !navigationRequiresAngleMode()) {
4137 // ALTHOLD might be enabled alongside ANGLE/HORIZON/ACRO
4138 // when it doesn't require ANGLE mode (required only in FW
4139 // right now). If if requires ANGLE, its display is handled
4140 // by OSD_FLYMODE.
4141 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD);
4143 if (IS_RC_MODE_ACTIVE(BOXAUTOTRIM) && !feature(FEATURE_FW_AUTOTRIM)) {
4144 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM);
4146 if (IS_RC_MODE_ACTIVE(BOXAUTOTUNE)) {
4147 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE);
4148 if (FLIGHT_MODE(MANUAL_MODE)) {
4149 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO);
4152 if (FLIGHT_MODE(HEADFREE_MODE)) {
4153 messages[messageCount++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE);
4156 // Pick one of the available messages. Each message lasts
4157 // a second.
4158 if (messageCount > 0) {
4159 message = messages[OSD_ALTERNATING_CHOICES(1000, messageCount)];
4162 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS)) {
4163 unsigned invalidIndex;
4164 // Check if we're unable to arm for some reason
4165 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING) && !settingsValidate(&invalidIndex)) {
4166 if (OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
4167 const setting_t *setting = settingGet(invalidIndex);
4168 settingGetName(setting, messageBuf);
4169 for (int ii = 0; messageBuf[ii]; ii++) {
4170 messageBuf[ii] = sl_toupper(messageBuf[ii]);
4172 message = messageBuf;
4173 } else {
4174 message = OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING);
4175 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
4177 } else {
4178 if (OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
4179 message = OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM);
4180 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr);
4181 } else {
4182 // Show the reason for not arming
4183 message = osdArmingDisabledReasonMessage();
4187 osdFormatMessage(buff, buff_size, message, isCenteredText);
4189 return elemAttr;
4192 #endif // OSD