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
34 FILE_COMPILE_FOR_SPEED
38 #include "build/debug.h"
39 #include "build/version.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"
70 #include "io/osd_common.h"
71 #include "io/osd_hud.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/rth_estimator.h"
89 #include "flight/wind_estimator.h"
91 #include "navigation/navigation.h"
92 #include "navigation/navigation_private.h"
95 #include "rx/msp_override.h"
97 #include "sensors/acceleration.h"
98 #include "sensors/battery.h"
99 #include "sensors/boardalignment.h"
100 #include "sensors/diagnostics.h"
101 #include "sensors/sensors.h"
102 #include "sensors/pitotmeter.h"
103 #include "sensors/temperature.h"
104 #include "sensors/esc_sensor.h"
106 #include "programming/logic_condition.h"
108 #ifdef USE_HARDWARE_REVISION_DETECTION
109 #include "hardware_revision.h"
112 #define VIDEO_BUFFER_CHARS_PAL 480
114 #define GFORCE_FILTER_TC 0.2
116 #define DELAYED_REFRESH_RESUME_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
118 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
119 #define ARMED_SCREEN_DISPLAY_TIME 1500 // ms
120 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
122 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
124 // Adjust OSD_MESSAGE's default position when
125 // changing OSD_MESSAGE_LENGTH
126 #define OSD_MESSAGE_LENGTH 28
127 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
128 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
129 // Wrap all string constants intenteded for display as messages with
130 // this macro to ensure compile time length validation.
131 #define OSD_MESSAGE_STR(x) ({ \
132 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
136 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
138 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
139 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
141 #define OSD_MIN_FONT_VERSION 1
143 static unsigned currentLayout
= 0;
144 static int layoutOverride
= -1;
145 static bool hasExtendedFont
= false; // Wether the font supports characters > 256
146 static timeMs_t layoutOverrideUntil
= 0;
147 static pt1Filter_t GForceFilter
, GForceFilterAxis
[XYZ_AXIS_COUNT
];
148 static float GForce
, GForceAxis
[XYZ_AXIS_COUNT
];
150 typedef struct statistic_s
{
152 uint16_t min_voltage
; // /100
153 int16_t max_current
; // /100
154 int16_t max_power
; // /100
156 int32_t max_altitude
;
157 uint32_t max_distance
;
160 static statistic_t stats
;
162 static timeUs_t resumeRefreshAt
= 0;
163 static bool refreshWaitForResumeCmdRelease
;
165 static bool fullRedraw
= false;
167 static uint8_t armState
;
169 typedef struct osdMapData_s
{
171 char referenceSymbol
;
174 static osdMapData_t osdMapData
;
176 static displayPort_t
*osdDisplayPort
;
177 static bool osdDisplayIsReady
= false;
178 #if defined(USE_CANVAS)
179 static displayCanvas_t osdCanvas
;
180 static bool osdDisplayHasCanvas
;
182 #define osdDisplayHasCanvas false
185 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
187 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t
, osdConfig
, PG_OSD_CONFIG
, 13);
188 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t
, osdLayoutsConfig
, PG_OSD_LAYOUTS_CONFIG
, 0);
190 static int digitCount(int32_t value
)
203 bool osdDisplayIsPAL(void)
205 return displayScreenSize(osdDisplayPort
) == VIDEO_BUFFER_CHARS_PAL
;
209 * Formats a number given in cents, to support non integer values
210 * without using floating point math. Value is always right aligned
211 * and spaces are inserted before the number to always yield a string
212 * of the same length. If the value doesn't fit into the provided length
213 * it will be divided by scale and true will be returned.
215 bool osdFormatCentiNumber(char *buff
, int32_t centivalue
, uint32_t scale
, int maxDecimals
, int maxScaledDecimals
, int length
)
219 int decimals
= maxDecimals
;
220 bool negative
= false;
225 if (centivalue
< 0) {
227 centivalue
= -centivalue
;
231 int32_t integerPart
= centivalue
/ 100;
233 int32_t millis
= (centivalue
% 100) * 10;
235 int digits
= digitCount(integerPart
);
236 int remaining
= length
- digits
;
238 if (remaining
< 0 && scale
> 0) {
241 decimals
= maxScaledDecimals
;
242 integerPart
= integerPart
/ scale
;
243 // Multiply by 10 to get 3 decimal digits
244 millis
= ((centivalue
% (100 * scale
)) * 10) / scale
;
245 digits
= digitCount(integerPart
);
246 remaining
= length
- digits
;
249 // 3 decimals at most
250 decimals
= MIN(remaining
, MIN(decimals
, 3));
251 remaining
-= decimals
;
253 // Done counting. Time to write the characters.
255 // Write spaces at the start
256 while (remaining
> 0) {
262 // Write the minus sign if required
267 // Now write the digits.
268 ui2a(integerPart
, 10, 0, ptr
);
271 *(ptr
-1) += SYM_ZERO_HALF_TRAILING_DOT
- '0';
273 int factor
= 3; // we're getting the decimal part in millis first
274 while (decimals
< factor
) {
278 int decimalDigits
= digitCount(millis
);
279 while (decimalDigits
< decimals
) {
284 ui2a(millis
, 10, 0, ptr
);
285 *dec
+= SYM_ZERO_HALF_LEADING_DOT
- '0';
291 * Aligns text to the left side. Adds spaces at the end to keep string length unchanged.
293 static void osdLeftAlignString(char *buff
)
295 uint8_t sp
= 0, ch
= 0;
296 uint8_t len
= strlen(buff
);
297 while (buff
[sp
] == ' ') sp
++;
298 for (ch
= 0; ch
< (len
- sp
); ch
++) buff
[ch
] = buff
[ch
+ sp
];
299 for (sp
= ch
; sp
< len
; sp
++) buff
[sp
] = ' ';
303 * Converts distance into a string based on the current unit system
304 * prefixed by a a symbol to indicate the unit used.
305 * @param dist Distance in centimeters
307 static void osdFormatDistanceSymbol(char *buff
, int32_t dist
)
309 switch ((osd_unit_e
)osdConfig()->units
) {
310 case OSD_UNIT_IMPERIAL
:
311 if (osdFormatCentiNumber(buff
, CENTIMETERS_TO_CENTIFEET(dist
), FEET_PER_MILE
, 0, 3, 3)) {
312 buff
[3] = SYM_DIST_MI
;
314 buff
[3] = SYM_DIST_FT
;
320 case OSD_UNIT_METRIC
:
321 if (osdFormatCentiNumber(buff
, dist
, METERS_PER_KILOMETER
, 0, 3, 3)) {
322 buff
[3] = SYM_DIST_KM
;
324 buff
[3] = SYM_DIST_M
;
332 * Converts distance into a string based on the current unit system.
333 * @param dist Distance in centimeters
335 static void osdFormatDistanceStr(char *buff
, int32_t dist
)
338 switch ((osd_unit_e
)osdConfig()->units
) {
339 case OSD_UNIT_IMPERIAL
:
340 centifeet
= CENTIMETERS_TO_CENTIFEET(dist
);
341 if (abs(centifeet
) < FEET_PER_MILE
* 100 / 2) {
342 // Show feet when dist < 0.5mi
343 tfp_sprintf(buff
, "%d%c", (int)(centifeet
/ 100), SYM_FT
);
345 // Show miles when dist >= 0.5mi
346 tfp_sprintf(buff
, "%d.%02d%c", (int)(centifeet
/ (100*FEET_PER_MILE
)),
347 (abs(centifeet
) % (100 * FEET_PER_MILE
)) / FEET_PER_MILE
, SYM_MI
);
352 case OSD_UNIT_METRIC
:
353 if (abs(dist
) < METERS_PER_KILOMETER
* 100) {
354 // Show meters when dist < 1km
355 tfp_sprintf(buff
, "%d%c", (int)(dist
/ 100), SYM_M
);
357 // Show kilometers when dist >= 1km
358 tfp_sprintf(buff
, "%d.%02d%c", (int)(dist
/ (100*METERS_PER_KILOMETER
)),
359 (abs(dist
) % (100 * METERS_PER_KILOMETER
)) / METERS_PER_KILOMETER
, SYM_KM
);
366 * Converts velocity based on the current unit system (kmh or mph).
367 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
369 static int32_t osdConvertVelocityToUnit(int32_t vel
)
371 switch ((osd_unit_e
)osdConfig()->units
) {
374 case OSD_UNIT_IMPERIAL
:
375 return (vel
* 224) / 10000; // Convert to mph
376 case OSD_UNIT_METRIC
:
377 return (vel
* 36) / 1000; // Convert to kmh
384 * Converts velocity into a string based on the current unit system.
385 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
387 void osdFormatVelocityStr(char* buff
, int32_t vel
, bool _3D
)
389 switch ((osd_unit_e
)osdConfig()->units
) {
392 case OSD_UNIT_IMPERIAL
:
393 tfp_sprintf(buff
, "%3d%c", (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_MPH
: SYM_MPH
));
395 case OSD_UNIT_METRIC
:
396 tfp_sprintf(buff
, "%3d%c", (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_KMH
: SYM_KMH
));
402 * Converts wind speed into a string based on the current unit system, using
403 * always 3 digits and an additional character for the unit at the right. buff
404 * is null terminated.
405 * @param ws Raw wind speed in cm/s
407 #ifdef USE_WIND_ESTIMATOR
408 static void osdFormatWindSpeedStr(char *buff
, int32_t ws
, bool isValid
)
412 switch (osdConfig()->units
) {
415 case OSD_UNIT_IMPERIAL
:
416 centivalue
= (ws
* 224) / 100;
419 case OSD_UNIT_METRIC
:
420 centivalue
= (ws
* 36) / 10;
425 osdFormatCentiNumber(buff
, centivalue
, 0, 2, 0, 3);
427 buff
[0] = buff
[1] = buff
[2] = '-';
435 * Converts altitude into a string based on the current unit system
436 * prefixed by a a symbol to indicate the unit used.
437 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
439 void osdFormatAltitudeSymbol(char *buff
, int32_t alt
)
441 switch ((osd_unit_e
)osdConfig()->units
) {
444 case OSD_UNIT_IMPERIAL
:
445 if (osdFormatCentiNumber(buff
, CENTIMETERS_TO_CENTIFEET(alt
), 1000, 0, 2, 3)) {
447 buff
[3] = SYM_ALT_KFT
;
450 buff
[3] = SYM_ALT_FT
;
454 case OSD_UNIT_METRIC
:
455 // alt is alredy in cm
456 if (osdFormatCentiNumber(buff
, alt
, 1000, 0, 2, 3)) {
458 buff
[3] = SYM_ALT_KM
;
469 * Converts altitude into a string based on the current unit system.
470 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
472 static void osdFormatAltitudeStr(char *buff
, int32_t alt
)
475 switch ((osd_unit_e
)osdConfig()->units
) {
476 case OSD_UNIT_IMPERIAL
:
477 value
= CENTIMETERS_TO_FEET(alt
);
478 tfp_sprintf(buff
, "%d%c", (int)value
, SYM_FT
);
482 case OSD_UNIT_METRIC
:
483 value
= CENTIMETERS_TO_METERS(alt
);
484 tfp_sprintf(buff
, "%d%c", (int)value
, SYM_M
);
489 static void osdFormatTime(char *buff
, uint32_t seconds
, char sym_m
, char sym_h
)
491 uint32_t value
= seconds
;
493 // Maximum value we can show in minutes is 99 minutes and 59 seconds
494 if (seconds
> (99 * 60) + 59) {
496 value
= seconds
/ 60;
499 tfp_sprintf(buff
+ 1, "%02d:%02d", (int)(value
/ 60), (int)(value
% 60));
502 static inline void osdFormatOnTime(char *buff
)
504 osdFormatTime(buff
, micros() / 1000000, SYM_ON_M
, SYM_ON_H
);
507 static inline void osdFormatFlyTime(char *buff
, textAttributes_t
*attr
)
509 uint32_t seconds
= getFlightTime();
510 osdFormatTime(buff
, seconds
, SYM_FLY_M
, SYM_FLY_H
);
511 if (attr
&& osdConfig()->time_alarm
> 0) {
512 if (seconds
/ 60 >= osdConfig()->time_alarm
&& ARMING_FLAG(ARMED
)) {
513 TEXT_ATTRIBUTES_ADD_BLINK(*attr
);
519 * Converts RSSI into a % value used by the OSD.
521 static uint16_t osdConvertRSSI(void)
523 // change range to [0, 99]
524 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE
, 0, 99);
528 * Displays a temperature postfixed with a symbol depending on the current unit system
529 * @param label to display
530 * @param valid true if measurement is valid
531 * @param temperature in deciDegrees Celcius
533 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
)
535 char buff
[TEMPERATURE_LABEL_LEN
+ 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN
+ 2];
536 textAttributes_t elemAttr
= valid
? TEXT_ATTRIBUTES_NONE
: _TEXT_ATTRIBUTES_BLINK_BIT
;
537 uint8_t valueXOffset
= 0;
542 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
545 #ifdef USE_TEMPERATURE_SENSOR
546 else if (label
[0] != '\0') {
547 uint8_t label_len
= strnlen(label
, TEMPERATURE_LABEL_LEN
);
548 memcpy(buff
, label
, label_len
);
549 memset(buff
+ label_len
, ' ', TEMPERATURE_LABEL_LEN
+ 1 - label_len
);
551 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
552 valueXOffset
= osdConfig()->temp_label_align
== OSD_ALIGN_LEFT
? 5 : label_len
+ 1;
560 if ((temperature
<= alarm_min
) || (temperature
>= alarm_max
)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
561 if (osdConfig()->units
== OSD_UNIT_IMPERIAL
) temperature
= temperature
* 9 / 5.0f
+ 320;
562 tfp_sprintf(buff
, "%3d", temperature
/ 10);
567 buff
[3] = osdConfig()->units
== OSD_UNIT_IMPERIAL
? SYM_TEMP_F
: SYM_TEMP_C
;
570 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ valueXOffset
, elemPosY
, buff
, elemAttr
);
573 #ifdef USE_TEMPERATURE_SENSOR
574 static void osdDisplayTemperatureSensor(uint8_t elemPosX
, uint8_t elemPosY
, uint8_t sensorIndex
)
577 const bool valid
= getSensorTemperature(sensorIndex
, &temperature
);
578 const tempSensorConfig_t
*sensorConfig
= tempSensorConfig(sensorIndex
);
579 uint16_t symbol
= sensorConfig
->osdSymbol
? SYM_TEMP_SENSOR_FIRST
+ sensorConfig
->osdSymbol
- 1 : 0;
580 osdDisplayTemperature(elemPosX
, elemPosY
, symbol
, sensorConfig
->label
, valid
, temperature
, sensorConfig
->alarm_min
, sensorConfig
->alarm_max
);
584 static void osdFormatCoordinate(char *buff
, char sym
, int32_t val
)
586 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
587 const int coordinateLength
= osdConfig()->coordinate_digits
+ 1;
590 int32_t integerPart
= val
/ GPS_DEGREES_DIVIDER
;
591 // Latitude maximum integer width is 3 (-90) while
592 // longitude maximum integer width is 4 (-180).
593 int integerDigits
= tfp_sprintf(buff
+ 1, (integerPart
== 0 && val
< 0) ? "-%d" : "%d", (int)integerPart
);
594 // We can show up to 7 digits in decimalPart.
595 int32_t decimalPart
= abs(val
% GPS_DEGREES_DIVIDER
);
596 STATIC_ASSERT(GPS_DEGREES_DIVIDER
== 1e7
, adjust_max_decimal_digits
);
597 int decimalDigits
= tfp_sprintf(buff
+ 1 + integerDigits
, "%07d", (int)decimalPart
);
598 // Embbed the decimal separator
599 buff
[1 + integerDigits
- 1] += SYM_ZERO_HALF_TRAILING_DOT
- '0';
600 buff
[1 + integerDigits
] += SYM_ZERO_HALF_LEADING_DOT
- '0';
601 // Fill up to coordinateLength with zeros
602 int total
= 1 + integerDigits
+ decimalDigits
;
603 while(total
< coordinateLength
) {
607 buff
[coordinateLength
] = '\0';
610 static void osdFormatCraftName(char *buff
)
612 if (strlen(systemConfig()->name
) == 0)
613 strcpy(buff
, "CRAFT_NAME");
615 for (int i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
616 buff
[i
] = sl_toupper((unsigned char)systemConfig()->name
[i
]);
617 if (systemConfig()->name
[i
] == 0)
623 // Used twice, make sure it's exactly the same string
624 // to save some memory
625 #define RC_RX_LINK_LOST_MSG "!RC RX LINK LOST!"
627 static const char * osdArmingDisabledReasonMessage(void)
629 switch (isArmingDisabledReason()) {
630 case ARMING_DISABLED_FAILSAFE_SYSTEM
:
631 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
632 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING
) {
633 if (failsafeIsReceivingRxData()) {
634 // If we're not using sticks, it means the ARM switch
635 // hasn't been off since entering FAILSAFE_RX_LOSS_MONITORING
637 return OSD_MESSAGE_STR("TURN ARM SWITCH OFF");
639 // Not receiving RX data
640 return OSD_MESSAGE_STR(RC_RX_LINK_LOST_MSG
);
642 return OSD_MESSAGE_STR("DISABLED BY FAILSAFE");
643 case ARMING_DISABLED_NOT_LEVEL
:
644 return OSD_MESSAGE_STR("AIRCRAFT IS NOT LEVEL");
645 case ARMING_DISABLED_SENSORS_CALIBRATING
:
646 return OSD_MESSAGE_STR("SENSORS CALIBRATING");
647 case ARMING_DISABLED_SYSTEM_OVERLOADED
:
648 return OSD_MESSAGE_STR("SYSTEM OVERLOADED");
649 case ARMING_DISABLED_NAVIGATION_UNSAFE
:
651 // Check the exact reason
652 switch (navigationIsBlockingArming(NULL
)) {
653 case NAV_ARMING_BLOCKER_NONE
:
655 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX
:
656 return OSD_MESSAGE_STR("WAITING FOR GPS FIX");
657 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE
:
658 return OSD_MESSAGE_STR("DISABLE NAVIGATION FIRST");
659 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR
:
660 return OSD_MESSAGE_STR("FIRST WAYPOINT IS TOO FAR");
661 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR
:
662 return OSD_MESSAGE_STR("JUMP WAYPOINT MISCONFIGURED");
666 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED
:
667 return OSD_MESSAGE_STR("COMPASS NOT CALIBRATED");
668 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED
:
669 return OSD_MESSAGE_STR("ACCELEROMETER NOT CALIBRATED");
670 case ARMING_DISABLED_ARM_SWITCH
:
671 return OSD_MESSAGE_STR("DISABLE ARM SWITCH FIRST");
672 case ARMING_DISABLED_HARDWARE_FAILURE
:
674 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
675 return OSD_MESSAGE_STR("GYRO FAILURE");
677 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
678 return OSD_MESSAGE_STR("ACCELEROMETER FAILURE");
680 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
681 return OSD_MESSAGE_STR("COMPASS FAILURE");
683 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
684 return OSD_MESSAGE_STR("BAROMETER FAILURE");
686 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
687 return OSD_MESSAGE_STR("GPS FAILURE");
689 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
690 return OSD_MESSAGE_STR("RANGE FINDER FAILURE");
692 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
693 return OSD_MESSAGE_STR("PITOT METER FAILURE");
696 return OSD_MESSAGE_STR("HARDWARE FAILURE");
697 case ARMING_DISABLED_BOXFAILSAFE
:
698 return OSD_MESSAGE_STR("FAILSAFE MODE ENABLED");
699 case ARMING_DISABLED_BOXKILLSWITCH
:
700 return OSD_MESSAGE_STR("KILLSWITCH MODE ENABLED");
701 case ARMING_DISABLED_RC_LINK
:
702 return OSD_MESSAGE_STR("NO RC LINK");
703 case ARMING_DISABLED_THROTTLE
:
704 return OSD_MESSAGE_STR("THROTTLE IS NOT LOW");
705 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED
:
706 return OSD_MESSAGE_STR("ROLLPITCH NOT CENTERED");
707 case ARMING_DISABLED_SERVO_AUTOTRIM
:
708 return OSD_MESSAGE_STR("AUTOTRIM IS ACTIVE");
709 case ARMING_DISABLED_OOM
:
710 return OSD_MESSAGE_STR("NOT ENOUGH MEMORY");
711 case ARMING_DISABLED_INVALID_SETTING
:
712 return OSD_MESSAGE_STR("INVALID SETTING");
713 case ARMING_DISABLED_CLI
:
714 return OSD_MESSAGE_STR("CLI IS ACTIVE");
715 case ARMING_DISABLED_PWM_OUTPUT_ERROR
:
716 return OSD_MESSAGE_STR("PWM INIT ERROR");
717 // Cases without message
718 case ARMING_DISABLED_CMS_MENU
:
720 case ARMING_DISABLED_OSD_MENU
:
722 case ARMING_DISABLED_ALL_FLAGS
:
732 static const char * osdFailsafePhaseMessage(void)
734 // See failsafe.h for each phase explanation
735 switch (failsafePhase()) {
737 case FAILSAFE_RETURN_TO_HOME
:
738 // XXX: Keep this in sync with OSD_FLYMODE.
739 return OSD_MESSAGE_STR("(RTH)");
741 case FAILSAFE_LANDING
:
742 // This should be considered an emergengy landing
743 return OSD_MESSAGE_STR("(EMERGENCY LANDING)");
744 case FAILSAFE_RX_LOSS_MONITORING
:
745 // Only reachable from FAILSAFE_LANDED, which performs
746 // a disarm. Since aircraft has been disarmed, we no
747 // longer show failsafe details.
749 case FAILSAFE_LANDED
:
750 // Very brief, disarms and transitions into
751 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
752 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
753 // so we'll show the user how to re-arm in when
754 // that flag is the reason to prevent arming.
756 case FAILSAFE_RX_LOSS_IDLE
:
757 // This only happens when user has chosen NONE as FS
758 // procedure. The recovery messages should be enough.
761 // Failsafe not active
763 case FAILSAFE_RX_LOSS_DETECTED
:
764 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
765 // or the FS procedure immediately.
767 case FAILSAFE_RX_LOSS_RECOVERED
:
774 static const char * osdFailsafeInfoMessage(void)
776 if (failsafeIsReceivingRxData()) {
777 // User must move sticks to exit FS mode
778 return OSD_MESSAGE_STR("!MOVE STICKS TO EXIT FS!");
780 return OSD_MESSAGE_STR(RC_RX_LINK_LOST_MSG
);
783 static const char * navigationStateMessage(void)
785 switch (NAV_Status
.state
) {
786 case MW_NAV_STATE_NONE
:
788 case MW_NAV_STATE_RTH_START
:
789 return OSD_MESSAGE_STR("STARTING RTH");
790 case MW_NAV_STATE_RTH_ENROUTE
:
791 // TODO: Break this up between climb and head home
792 return OSD_MESSAGE_STR("EN ROUTE TO HOME");
793 case MW_NAV_STATE_HOLD_INFINIT
:
794 // Used by HOLD flight modes. No information to add.
796 case MW_NAV_STATE_HOLD_TIMED
:
797 // TODO: Maybe we can display a count down
798 return OSD_MESSAGE_STR("HOLDING WAYPOINT");
800 case MW_NAV_STATE_WP_ENROUTE
:
801 // TODO: Show WP number
802 return OSD_MESSAGE_STR("TO WP");
803 case MW_NAV_STATE_PROCESS_NEXT
:
804 return OSD_MESSAGE_STR("PREPARING FOR NEXT WAYPOINT");
805 case MW_NAV_STATE_DO_JUMP
:
808 case MW_NAV_STATE_LAND_START
:
811 case MW_NAV_STATE_EMERGENCY_LANDING
:
812 return OSD_MESSAGE_STR("EMERGENCY LANDING");
813 case MW_NAV_STATE_LAND_IN_PROGRESS
:
814 return OSD_MESSAGE_STR("LANDING");
815 case MW_NAV_STATE_HOVER_ABOVE_HOME
:
816 if (STATE(FIXED_WING_LEGACY
)) {
817 return OSD_MESSAGE_STR("LOITERING AROUND HOME");
819 return OSD_MESSAGE_STR("HOVERING");
820 case MW_NAV_STATE_LANDED
:
821 return OSD_MESSAGE_STR("LANDED");
822 case MW_NAV_STATE_LAND_SETTLE
:
823 return OSD_MESSAGE_STR("PREPARING TO LAND");
824 case MW_NAV_STATE_LAND_START_DESCENT
:
831 static void osdFormatMessage(char *buff
, size_t size
, const char *message
)
833 memset(buff
, SYM_BLANK
, size
);
835 int messageLength
= strlen(message
);
836 int rem
= MAX(0, OSD_MESSAGE_LENGTH
- (int)messageLength
);
837 // Don't finish the string at the end of the message,
838 // write the rest of the blanks.
839 strncpy(buff
+ rem
/ 2, message
, MIN(OSD_MESSAGE_LENGTH
- rem
/ 2, messageLength
));
841 // Ensure buff is zero terminated
842 buff
[size
- 1] = '\0';
846 * Draws the battery symbol filled in accordingly to the
847 * battery voltage to buff[0].
849 static void osdFormatBatteryChargeSymbol(char *buff
)
851 uint8_t p
= calculateBatteryPercentage();
852 p
= (100 - p
) / 16.6;
853 buff
[0] = SYM_BATT_FULL
+ p
;
856 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t
*attr
)
858 if ((getBatteryState() != BATTERY_NOT_PRESENT
) && ((batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile
->capacity
.warning
- currentBatteryProfile
->capacity
.critical
)) || ((!batteryUsesCapacityThresholds()) && (getBatteryVoltage() <= getBatteryWarningVoltage()))))
859 TEXT_ATTRIBUTES_ADD_BLINK(*attr
);
862 void osdCrosshairPosition(uint8_t *x
, uint8_t *y
)
864 *x
= osdDisplayPort
->cols
/ 2;
865 *y
= osdDisplayPort
->rows
/ 2;
866 *y
+= osdConfig()->horizon_offset
;
870 * Formats throttle position prefixed by its symbol. If autoThr
871 * is true and the navigation system is controlling THR, it
872 * uses the THR value applied by the system rather than the
873 * input value received by the sticks.
875 static void osdFormatThrottlePosition(char *buff
, bool autoThr
, textAttributes_t
*elemAttr
)
879 int16_t thr
= rxGetChannelValue(THROTTLE
);
880 if (autoThr
&& navigationIsControllingThrottle()) {
881 buff
[0] = SYM_AUTO_THR0
;
882 buff
[1] = SYM_AUTO_THR1
;
883 thr
= rcCommand
[THROTTLE
];
884 if (isFixedWingAutoThrottleManuallyIncreased())
885 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr
);
887 tfp_sprintf(buff
+ 2, "%3d", (constrain(thr
, PWM_RANGE_MIN
, PWM_RANGE_MAX
) - PWM_RANGE_MIN
) * 100 / (PWM_RANGE_MAX
- PWM_RANGE_MIN
));
890 #if defined(USE_ESC_SENSOR)
891 static void osdFormatRpm(char *buff
, uint32_t rpm
)
896 osdFormatCentiNumber(buff
+ 1, rpm
/ 10, 0, 1, 1, 2);
901 tfp_sprintf(buff
+ 1, "%3lu", rpm
);
905 strcpy(buff
+ 1, "---");
910 int32_t osdGetAltitude(void)
913 return getEstimatedActualPosition(Z
);
914 #elif defined(USE_BARO)
921 static inline int32_t osdGetAltitudeMsl(void)
924 return getEstimatedActualPosition(Z
)+GPS_home
.alt
;
925 #elif defined(USE_BARO)
926 return baro
.alt
+GPS_home
.alt
;
932 static bool osdIsHeadingValid(void)
934 return isImuHeadingValid();
937 int16_t osdGetHeading(void)
939 return attitude
.values
.yaw
;
942 // Returns a heading angle in degrees normalized to [0, 360).
943 int osdGetHeadingAngle(int angle
)
948 while (angle
>= 360) {
956 /* Draws a map with the given symbol in the center and given point of interest
957 * defined by its distance in meters and direction in degrees.
958 * referenceHeading indicates the up direction in the map, in degrees, while
959 * referenceSym (if non-zero) is drawn at the upper right corner below a small
960 * arrow to indicate the map reference to the user. The drawn argument is an
961 * in-out used to store the last position where the craft was drawn to avoid
962 * erasing all screen on each redraw.
964 static void osdDrawMap(int referenceHeading
, uint8_t referenceSym
, uint8_t centerSym
,
965 uint32_t poiDistance
, int16_t poiDirection
, uint8_t poiSymbol
,
966 uint16_t *drawn
, uint32_t *usedScale
)
968 // TODO: These need to be tested with several setups. We might
969 // need to make them configurable.
970 const int hMargin
= 5;
971 const int vMargin
= 3;
973 // TODO: Get this from the display driver?
974 const int charWidth
= 12;
975 const int charHeight
= 18;
977 uint8_t minX
= hMargin
;
978 uint8_t maxX
= osdDisplayPort
->cols
- 1 - hMargin
;
979 uint8_t minY
= vMargin
;
980 uint8_t maxY
= osdDisplayPort
->rows
- 1 - vMargin
;
981 uint8_t midX
= osdDisplayPort
->cols
/ 2;
982 uint8_t midY
= osdDisplayPort
->rows
/ 2;
985 displayWriteChar(osdDisplayPort
, midX
, midY
, centerSym
);
987 // First, erase the previous drawing.
988 if (OSD_VISIBLE(*drawn
)) {
989 displayWriteChar(osdDisplayPort
, OSD_X(*drawn
), OSD_Y(*drawn
), SYM_BLANK
);
993 uint32_t initialScale
;
994 const unsigned scaleMultiplier
= 2;
995 // We try to reduce the scale when the POI will be around half the distance
996 // between the center and the closers map edge, to avoid too much jumping
997 const int scaleReductionMultiplier
= MIN(midX
- hMargin
, midY
- vMargin
) / 2;
999 switch (osdConfig()->units
) {
1000 case OSD_UNIT_IMPERIAL
:
1001 initialScale
= 16; // 16m ~= 0.01miles
1005 case OSD_UNIT_METRIC
:
1006 initialScale
= 10; // 10m as initial scale
1010 // Try to keep the same scale when getting closer until we draw over the center point
1011 uint32_t scale
= initialScale
;
1014 if (scale
> initialScale
&& poiDistance
< *usedScale
* scaleReductionMultiplier
) {
1015 scale
/= scaleMultiplier
;
1019 if (STATE(GPS_FIX
)) {
1021 int directionToPoi
= osdGetHeadingAngle(poiDirection
- referenceHeading
);
1022 float poiAngle
= DEGREES_TO_RADIANS(directionToPoi
);
1023 float poiSin
= sin_approx(poiAngle
);
1024 float poiCos
= cos_approx(poiAngle
);
1026 // Now start looking for a valid scale that lets us draw everything
1028 for (ii
= 0; ii
< 50; ii
++) {
1029 // Calculate location of the aircraft in map
1030 int points
= poiDistance
/ ((float)scale
/ charHeight
);
1032 float pointsX
= points
* poiSin
;
1033 int poiX
= midX
- roundf(pointsX
/ charWidth
);
1034 if (poiX
< minX
|| poiX
> maxX
) {
1035 scale
*= scaleMultiplier
;
1039 float pointsY
= points
* poiCos
;
1040 int poiY
= midY
+ roundf(pointsY
/ charHeight
);
1041 if (poiY
< minY
|| poiY
> maxY
) {
1042 scale
*= scaleMultiplier
;
1046 if (poiX
== midX
&& poiY
== midY
) {
1047 // We're over the map center symbol, so we would be drawing
1048 // over it even if we increased the scale. Alternate between
1049 // drawing the center symbol or drawing the POI.
1050 if (centerSym
!= SYM_BLANK
&& OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1056 if (displayReadCharWithAttr(osdDisplayPort
, poiX
, poiY
, &c
, NULL
) && c
!= SYM_BLANK
) {
1057 // Something else written here, increase scale. If the display doesn't support reading
1058 // back characters, we assume there's nothing.
1060 // If we're close to the center, decrease scale. Otherwise increase it.
1061 uint8_t centerDeltaX
= (maxX
- minX
) / (scaleMultiplier
* 2);
1062 uint8_t centerDeltaY
= (maxY
- minY
) / (scaleMultiplier
* 2);
1063 if (poiX
>= midX
- centerDeltaX
&& poiX
<= midX
+ centerDeltaX
&&
1064 poiY
>= midY
- centerDeltaY
&& poiY
<= midY
+ centerDeltaY
&&
1065 scale
> scaleMultiplier
) {
1067 scale
/= scaleMultiplier
;
1069 scale
*= scaleMultiplier
;
1075 // Draw the point on the map
1076 if (poiSymbol
== SYM_ARROW_UP
) {
1077 // Drawing aircraft, rotate
1078 int mapHeading
= osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading
);
1079 poiSymbol
+= mapHeading
* 2 / 45;
1081 displayWriteChar(osdDisplayPort
, poiX
, poiY
, poiSymbol
);
1083 // Update saved location
1084 *drawn
= OSD_POS(poiX
, poiY
) | OSD_VISIBLE_FLAG
;
1091 // Update global map data for scale and reference
1092 osdMapData
.scale
= scale
;
1093 osdMapData
.referenceSymbol
= referenceSym
;
1096 /* Draws a map with the home in the center and the craft moving around.
1097 * See osdDrawMap() for reference.
1099 static void osdDrawHomeMap(int referenceHeading
, uint8_t referenceSym
, uint16_t *drawn
, uint32_t *usedScale
)
1101 osdDrawMap(referenceHeading
, referenceSym
, SYM_HOME
, GPS_distanceToHome
, GPS_directionToHome
, SYM_ARROW_UP
, drawn
, usedScale
);
1104 /* Draws a map with the aircraft in the center and the home moving around.
1105 * See osdDrawMap() for reference.
1107 static void osdDrawRadar(uint16_t *drawn
, uint32_t *usedScale
)
1109 int16_t reference
= DECIDEGREES_TO_DEGREES(osdGetHeading());
1110 int16_t poiDirection
= osdGetHeadingAngle(GPS_directionToHome
+ 180);
1111 osdDrawMap(reference
, 0, SYM_ARROW_UP
, GPS_distanceToHome
, poiDirection
, SYM_HOME
, drawn
, usedScale
);
1114 static int16_t osdGet3DSpeed(void)
1116 int16_t vert_speed
= getEstimatedActualVelocity(Z
);
1117 int16_t hor_speed
= gpsSol
.groundSpeed
;
1118 return (int16_t)sqrtf(sq(hor_speed
) + sq(vert_speed
));
1123 static void osdFormatPidControllerOutput(char *buff
, const char *label
, const pidController_t
*pidController
, uint8_t scale
, bool showDecimal
) {
1124 strcpy(buff
, label
);
1125 for (uint8_t i
= strlen(label
); i
< 5; ++i
) buff
[i
] = ' ';
1126 uint8_t decimals
= showDecimal
? 1 : 0;
1127 osdFormatCentiNumber(buff
+ 5, pidController
->proportional
* scale
, 0, decimals
, 0, 4);
1129 osdFormatCentiNumber(buff
+ 10, pidController
->integrator
* scale
, 0, decimals
, 0, 4);
1131 osdFormatCentiNumber(buff
+ 15, pidController
->derivative
* scale
, 0, decimals
, 0, 4);
1133 osdFormatCentiNumber(buff
+ 20, pidController
->output_constrained
* scale
, 0, decimals
, 0, 4);
1137 static void osdDisplayBatteryVoltage(uint8_t elemPosX
, uint8_t elemPosY
, uint16_t voltage
, uint8_t digits
, uint8_t decimals
)
1140 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
1142 osdFormatBatteryChargeSymbol(buff
);
1144 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1145 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
1147 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1148 digits
= MIN(digits
, 4);
1149 osdFormatCentiNumber(buff
, voltage
, 0, decimals
, 0, digits
);
1150 buff
[digits
] = SYM_VOLT
;
1151 buff
[digits
+1] = '\0';
1152 if ((getBatteryState() != BATTERY_NOT_PRESENT
) && (getBatteryVoltage() <= getBatteryWarningVoltage()))
1153 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1154 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 1, elemPosY
, buff
, elemAttr
);
1157 static void osdDisplayPIDValues(uint8_t elemPosX
, uint8_t elemPosY
, const char *str
, pidIndex_e pidIndex
, adjustmentFunction_e adjFuncP
, adjustmentFunction_e adjFuncI
, adjustmentFunction_e adjFuncD
)
1159 textAttributes_t elemAttr
;
1162 const pid8_t
*pid
= &pidBank()->pid
[pidIndex
];
1163 pidType_e pidType
= pidIndexGetType(pidIndex
);
1165 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, str
);
1167 if (pidType
== PID_TYPE_NONE
) {
1168 // PID is not used in this configuration. Draw dashes.
1169 // XXX: Keep this in sync with the %3d format and spacing used below
1170 displayWrite(osdDisplayPort
, elemPosX
+ 6, elemPosY
, "- - -");
1174 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1175 tfp_sprintf(buff
, "%3d", pid
->P
);
1176 if ((isAdjustmentFunctionSelected(adjFuncP
)) || (((adjFuncP
== ADJUSTMENT_ROLL_P
) || (adjFuncP
== ADJUSTMENT_PITCH_P
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P
))))
1177 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1178 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
1180 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1181 tfp_sprintf(buff
, "%3d", pid
->I
);
1182 if ((isAdjustmentFunctionSelected(adjFuncI
)) || (((adjFuncI
== ADJUSTMENT_ROLL_I
) || (adjFuncI
== ADJUSTMENT_PITCH_I
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I
))))
1183 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1184 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 8, elemPosY
, buff
, elemAttr
);
1186 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1187 tfp_sprintf(buff
, "%3d", pidType
== PID_TYPE_PIFF
? pid
->FF
: pid
->D
);
1188 if ((isAdjustmentFunctionSelected(adjFuncD
)) || (((adjFuncD
== ADJUSTMENT_ROLL_D
) || (adjFuncD
== ADJUSTMENT_PITCH_D
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D
))))
1189 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1190 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 12, elemPosY
, buff
, elemAttr
);
1193 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
) {
1195 textAttributes_t elemAttr
;
1196 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, str
);
1198 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1199 osdFormatCentiNumber(buff
, value
* 100, 0, maxDecimals
, 0, MIN(valueLength
, 8));
1200 if (isAdjustmentFunctionSelected(adjFunc
))
1201 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1202 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ strlen(str
) + 1 + valueOffset
, elemPosY
, buff
, elemAttr
);
1205 static bool osdDrawSingleElement(uint8_t item
)
1207 uint16_t pos
= osdLayoutsConfig()->item_pos
[currentLayout
][item
];
1208 if (!OSD_VISIBLE(pos
)) {
1211 uint8_t elemPosX
= OSD_X(pos
);
1212 uint8_t elemPosY
= OSD_Y(pos
);
1213 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
1217 case OSD_RSSI_VALUE
:
1219 uint16_t osdRssi
= osdConvertRSSI();
1221 tfp_sprintf(buff
+ 1, "%2d", osdRssi
);
1222 if (osdRssi
< osdConfig()->rssi_alarm
) {
1223 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1228 case OSD_MAIN_BATT_VOLTAGE
:
1229 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatteryRawVoltage(), 2 + osdConfig()->main_voltage_decimals
, osdConfig()->main_voltage_decimals
);
1232 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
:
1233 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatterySagCompensatedVoltage(), 2 + osdConfig()->main_voltage_decimals
, osdConfig()->main_voltage_decimals
);
1236 case OSD_CURRENT_DRAW
:
1237 osdFormatCentiNumber(buff
, getAmperage(), 0, 2, 0, 3);
1241 uint8_t current_alarm
= osdConfig()->current_alarm
;
1242 if ((current_alarm
> 0) && ((getAmperage() / 100.0f
) > current_alarm
)) {
1243 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1248 tfp_sprintf(buff
, "%4d", (int)getMAhDrawn());
1251 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1255 osdFormatCentiNumber(buff
, getMWhDrawn() / 10, 0, 2, 0, 3);
1256 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1261 case OSD_BATTERY_REMAINING_CAPACITY
:
1263 if (currentBatteryProfile
->capacity
.value
== 0)
1264 tfp_sprintf(buff
, " NA");
1265 else if (!batteryWasFullWhenPluggedIn())
1266 tfp_sprintf(buff
, " NF");
1267 else if (currentBatteryProfile
->capacity
.unit
== BAT_CAPACITY_UNIT_MAH
)
1268 tfp_sprintf(buff
, "%4lu", getBatteryRemainingCapacity());
1269 else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1270 osdFormatCentiNumber(buff
+ 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3);
1272 buff
[4] = currentBatteryProfile
->capacity
.unit
== BAT_CAPACITY_UNIT_MAH
? SYM_MAH
: SYM_WH
;
1275 if ((getBatteryState() != BATTERY_NOT_PRESENT
) && batteryUsesCapacityThresholds() && (getBatteryRemainingCapacity() <= currentBatteryProfile
->capacity
.warning
- currentBatteryProfile
->capacity
.critical
))
1276 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1280 case OSD_BATTERY_REMAINING_PERCENT
:
1281 tfp_sprintf(buff
, "%3d%%", calculateBatteryPercentage());
1282 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1285 case OSD_POWER_SUPPLY_IMPEDANCE
:
1286 if (isPowerSupplyImpedanceValid())
1287 tfp_sprintf(buff
, "%3d", getPowerSupplyImpedance());
1289 strcpy(buff
, "---");
1290 buff
[3] = SYM_MILLIOHM
;
1296 buff
[0] = SYM_SAT_L
;
1297 buff
[1] = SYM_SAT_R
;
1298 tfp_sprintf(buff
+ 2, "%2d", gpsSol
.numSat
);
1299 if (!STATE(GPS_FIX
)) {
1300 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1305 osdFormatVelocityStr(buff
, gpsSol
.groundSpeed
, false);
1310 osdFormatVelocityStr(buff
, osdGet3DSpeed(), true);
1315 osdFormatCoordinate(buff
, SYM_LAT
, gpsSol
.llh
.lat
);
1319 osdFormatCoordinate(buff
, SYM_LON
, gpsSol
.llh
.lon
);
1324 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
) && isImuHeadingValid()) {
1325 if (GPS_distanceToHome
< (navConfig()->general
.min_rth_distance
/ 100) ) {
1326 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_HOME_NEAR
);
1330 int homeDirection
= GPS_directionToHome
- DECIDEGREES_TO_DEGREES(osdGetHeading());
1331 osdDrawDirArrow(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), homeDirection
);
1334 // No home or no fix or unknown heading, blink.
1335 // If we're unarmed, show the arrow pointing up so users can see the arrow
1336 // while configuring the OSD. If we're armed, show a '-' indicating that
1337 // we don't know the direction to home.
1338 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1339 displayWriteCharWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, ARMING_FLAG(ARMED
) ? '-' : SYM_ARROW_UP
, elemAttr
);
1344 case OSD_HOME_HEADING_ERROR
:
1347 buff
[1] = SYM_HEADING
;
1349 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1350 int16_t h
= lrintf(CENTIDEGREES_TO_DEGREES((float)wrap_18000(DEGREES_TO_CENTIDEGREES((int32_t)GPS_directionToHome
) - DECIDEGREES_TO_CENTIDEGREES((int32_t)osdGetHeading()))));
1351 tfp_sprintf(buff
+ 2, "%4d", h
);
1353 strcpy(buff
+ 2, "----");
1356 buff
[6] = SYM_DEGREES
;
1364 osdFormatDistanceSymbol(&buff
[1], GPS_distanceToHome
* 100);
1365 uint16_t dist_alarm
= osdConfig()->dist_alarm
;
1366 if (dist_alarm
> 0 && GPS_distanceToHome
> dist_alarm
) {
1367 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1373 buff
[0] = SYM_TOTAL
;
1374 osdFormatDistanceSymbol(buff
+ 1, getTotalTravelDistance());
1379 buff
[0] = SYM_HEADING
;
1380 if (osdIsHeadingValid()) {
1381 int16_t h
= DECIDEGREES_TO_DEGREES(osdGetHeading());
1385 tfp_sprintf(&buff
[1], "%3d", h
);
1387 buff
[1] = buff
[2] = buff
[3] = '-';
1393 case OSD_CRUISE_HEADING_ERROR
:
1395 if (ARMING_FLAG(ARMED
) && !FLIGHT_MODE(NAV_CRUISE_MODE
)) {
1396 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
1400 buff
[0] = SYM_HEADING
;
1402 if ((!ARMING_FLAG(ARMED
)) || (FLIGHT_MODE(NAV_CRUISE_MODE
) && isAdjustingPosition())) {
1403 buff
[1] = buff
[2] = buff
[3] = '-';
1404 } else if (FLIGHT_MODE(NAV_CRUISE_MODE
)) {
1405 int16_t herr
= lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
1407 strcpy(buff
+ 1, ">99");
1409 tfp_sprintf(buff
+ 1, "%3d", herr
);
1412 buff
[4] = SYM_DEGREES
;
1417 case OSD_CRUISE_HEADING_ADJUSTMENT
:
1419 int16_t heading_adjust
= lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
1421 if (ARMING_FLAG(ARMED
) && ((!FLIGHT_MODE(NAV_CRUISE_MODE
)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust
!= 0)))) {
1422 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
1426 buff
[0] = SYM_HEADING
;
1428 if (!ARMING_FLAG(ARMED
)) {
1429 buff
[1] = buff
[2] = buff
[3] = buff
[4] = '-';
1430 } else if (FLIGHT_MODE(NAV_CRUISE_MODE
)) {
1431 tfp_sprintf(buff
+ 1, "%4d", heading_adjust
);
1434 buff
[5] = SYM_DEGREES
;
1441 buff
[0] = SYM_HDP_L
;
1442 buff
[1] = SYM_HDP_R
;
1443 int32_t centiHDOP
= 100 * gpsSol
.hdop
/ HDOP_SCALE
;
1444 osdFormatCentiNumber(&buff
[2], centiHDOP
, 0, 1, 0, 2);
1450 static uint16_t drawn
= 0;
1451 static uint32_t scale
= 0;
1452 osdDrawHomeMap(0, 'N', &drawn
, &scale
);
1455 case OSD_MAP_TAKEOFF
:
1457 static uint16_t drawn
= 0;
1458 static uint32_t scale
= 0;
1459 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn
, &scale
);
1464 static uint16_t drawn
= 0;
1465 static uint32_t scale
= 0;
1466 osdDrawRadar(&drawn
, &scale
);
1473 int32_t alt
= osdGetAltitude();
1474 osdFormatAltitudeSymbol(buff
, alt
);
1475 uint16_t alt_alarm
= osdConfig()->alt_alarm
;
1476 uint16_t neg_alt_alarm
= osdConfig()->neg_alt_alarm
;
1477 if ((alt_alarm
> 0 && CENTIMETERS_TO_METERS(alt
) > alt_alarm
) ||
1478 (neg_alt_alarm
> 0 && alt
< 0 && -CENTIMETERS_TO_METERS(alt
) > neg_alt_alarm
)) {
1480 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1485 case OSD_ALTITUDE_MSL
:
1487 int32_t alt
= osdGetAltitudeMsl();
1488 osdFormatAltitudeSymbol(buff
, alt
);
1494 osdFormatOnTime(buff
);
1500 osdFormatFlyTime(buff
, &elemAttr
);
1504 case OSD_ONTIME_FLYTIME
:
1506 if (ARMING_FLAG(ARMED
)) {
1507 osdFormatFlyTime(buff
, &elemAttr
);
1509 osdFormatOnTime(buff
);
1514 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH
:
1516 static timeUs_t updatedTimestamp
= 0;
1517 /*static int32_t updatedTimeSeconds = 0;*/
1518 timeUs_t currentTimeUs
= micros();
1519 static int32_t timeSeconds
= -1;
1520 if (cmpTimeUs(currentTimeUs
, updatedTimestamp
) >= 1000000) {
1521 timeSeconds
= calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation
);
1522 updatedTimestamp
= currentTimeUs
;
1524 if ((!ARMING_FLAG(ARMED
)) || (timeSeconds
== -1)) {
1525 buff
[0] = SYM_FLY_M
;
1526 strcpy(buff
+ 1, "--:--");
1527 updatedTimestamp
= 0;
1528 } else if (timeSeconds
== -2) {
1529 // Wind is too strong to come back with cruise throttle
1530 buff
[0] = SYM_FLY_M
;
1531 buff
[1] = buff
[2] = buff
[4] = buff
[5] = SYM_WIND_HORIZONTAL
;
1534 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1536 osdFormatTime(buff
, timeSeconds
, SYM_FLY_M
, SYM_FLY_H
);
1537 if (timeSeconds
== 0)
1538 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1543 case OSD_REMAINING_DISTANCE_BEFORE_RTH
:;
1544 static timeUs_t updatedTimestamp
= 0;
1545 timeUs_t currentTimeUs
= micros();
1546 static int32_t distanceMeters
= -1;
1547 if (cmpTimeUs(currentTimeUs
, updatedTimestamp
) >= 1000000) {
1548 distanceMeters
= calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation
);
1549 updatedTimestamp
= currentTimeUs
;
1551 buff
[0] = SYM_TRIP_DIST
;
1552 if ((!ARMING_FLAG(ARMED
)) || (distanceMeters
== -1)) {
1553 buff
[4] = SYM_DIST_M
;
1555 strcpy(buff
+ 1, "---");
1556 } else if (distanceMeters
== -2) {
1557 // Wind is too strong to come back with cruise throttle
1558 buff
[1] = buff
[2] = buff
[3] = SYM_WIND_HORIZONTAL
;
1559 buff
[4] = SYM_DIST_M
;
1561 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1563 osdFormatDistanceSymbol(buff
+ 1, distanceMeters
* 100);
1564 if (distanceMeters
== 0)
1565 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1573 if (FLIGHT_MODE(FAILSAFE_MODE
))
1575 else if (FLIGHT_MODE(MANUAL_MODE
))
1577 else if (FLIGHT_MODE(NAV_RTH_MODE
))
1579 else if (FLIGHT_MODE(NAV_POSHOLD_MODE
))
1581 else if (FLIGHT_MODE(NAV_CRUISE_MODE
) && FLIGHT_MODE(NAV_ALTHOLD_MODE
))
1583 else if (FLIGHT_MODE(NAV_CRUISE_MODE
))
1585 else if (FLIGHT_MODE(NAV_WP_MODE
))
1587 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE
) && navigationRequiresAngleMode()) {
1588 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
1589 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
1590 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
1593 else if (FLIGHT_MODE(ANGLE_MODE
))
1595 else if (FLIGHT_MODE(HORIZON_MODE
))
1598 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, p
);
1602 case OSD_CRAFT_NAME
:
1603 osdFormatCraftName(buff
);
1606 case OSD_THROTTLE_POS
:
1608 osdFormatThrottlePosition(buff
, false, NULL
);
1612 case OSD_VTX_CHANNEL
:
1614 vtxDeviceOsdInfo_t osdInfo
;
1615 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo
);
1617 tfp_sprintf(buff
, "CH:%c%s:", osdInfo
.bandLetter
, osdInfo
.channelName
);
1618 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
1620 tfp_sprintf(buff
, "%c", osdInfo
.powerIndexLetter
);
1621 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL
)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1622 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 6, elemPosY
, buff
, elemAttr
);
1629 vtxDeviceOsdInfo_t osdInfo
;
1630 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo
);
1632 tfp_sprintf(buff
, "%c", osdInfo
.powerIndexLetter
);
1633 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL
)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1634 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
1638 case OSD_CROSSHAIRS
: // Hud is a sub-element of the crosshair
1640 osdCrosshairPosition(&elemPosX
, &elemPosY
);
1641 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX
, elemPosY
);
1643 if (osdConfig()->hud_homing
&& STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
) && isImuHeadingValid()) {
1644 osdHudDrawHoming(elemPosX
, elemPosY
);
1647 if (STATE(GPS_FIX
) && isImuHeadingValid()) {
1649 if (osdConfig()->hud_homepoint
|| osdConfig()->hud_radar_disp
> 0 || osdConfig()->hud_wp_disp
> 0) {
1653 // -------- POI : Home point
1655 if (osdConfig()->hud_homepoint
) { // Display the home point (H)
1656 osdHudDrawPoi(GPS_distanceToHome
, GPS_directionToHome
, -osdGetAltitude() / 100, 0, SYM_HOME
, 0 , 0);
1659 // -------- POI : Nearby aircrafts from ESP32 radar
1661 if (osdConfig()->hud_radar_disp
> 0) { // Display the POI from the radar
1662 for (uint8_t i
= 0; i
< osdConfig()->hud_radar_disp
; i
++) {
1663 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
1665 geoConvertGeodeticToLocal(&poi
, &posControl
.gpsOrigin
, &radar_pois
[i
].gps
, GEO_ALT_RELATIVE
);
1666 radar_pois
[i
].distance
= calculateDistanceToDestination(&poi
) / 100; // In meters
1668 if (radar_pois
[i
].distance
>= osdConfig()->hud_radar_range_min
&& radar_pois
[i
].distance
<= osdConfig()->hud_radar_range_max
) {
1669 radar_pois
[i
].direction
= calculateBearingToDestination(&poi
) / 100; // In °
1670 radar_pois
[i
].altitude
= (radar_pois
[i
].gps
.alt
- osdGetAltitudeMsl()) / 100;
1671 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
);
1676 if (osdConfig()->hud_radar_nearest
> 0) { // Display extra datas for 1 POI closer than a set distance
1677 int poi_id
= radarGetNearestPOI();
1678 if (poi_id
>= 0 && radar_pois
[poi_id
].distance
<= osdConfig()->hud_radar_nearest
) {
1679 osdHudDrawExtras(poi_id
);
1684 // -------- POI : Next waypoints from navigation
1686 if (osdConfig()->hud_wp_disp
> 0 && posControl
.waypointListValid
&& posControl
.waypointCount
> 0) { // Display the next waypoints
1690 tfp_sprintf(buff
, "W%u/%u", posControl
.activeWaypointIndex
, posControl
.waypointCount
);
1691 displayWrite(osdGetDisplayPort(), 13, osdConfig()->hud_margin_v
- 1, buff
);
1693 for (int i
= osdConfig()->hud_wp_disp
- 1; i
>= 0 ; i
--) { // Display in reverse order so the next WP is always written on top
1694 j
= posControl
.activeWaypointIndex
+ i
;
1695 if (posControl
.waypointList
[j
].lat
!= 0 && posControl
.waypointList
[j
].lon
!= 0 && j
<= posControl
.waypointCount
) {
1696 wp2
.lat
= posControl
.waypointList
[j
].lat
;
1697 wp2
.lon
= posControl
.waypointList
[j
].lon
;
1698 wp2
.alt
= posControl
.waypointList
[j
].alt
;
1700 geoConvertGeodeticToLocal(&poi
, &posControl
.gpsOrigin
, &wp2
, GEO_ALT_RELATIVE
);
1701 while (j
> 9) j
-= 10; // Only the last digit displayed if WP>=10, no room for more
1702 osdHudDrawPoi(calculateDistanceToDestination(&poi
) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi
) / 100), (posControl
.waypointList
[j
].alt
- osdGetAltitude())/ 100, 2, SYM_WAYPOINT
, 49 + j
, i
);
1711 case OSD_ATTITUDE_ROLL
:
1712 buff
[0] = SYM_ROLL_LEVEL
;
1713 if (ABS(attitude
.values
.roll
) >= 1)
1714 buff
[0] += (attitude
.values
.roll
< 0 ? -1 : 1);
1715 osdFormatCentiNumber(buff
+ 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude
.values
.roll
)), 0, 1, 0, 3);
1718 case OSD_ATTITUDE_PITCH
:
1719 if (ABS(attitude
.values
.pitch
) < 1)
1721 else if (attitude
.values
.pitch
> 0)
1722 buff
[0] = SYM_PITCH_DOWN
;
1723 else if (attitude
.values
.pitch
< 0)
1724 buff
[0] = SYM_PITCH_UP
;
1725 osdFormatCentiNumber(buff
+ 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude
.values
.pitch
)), 0, 1, 0, 3);
1728 case OSD_ARTIFICIAL_HORIZON
:
1730 float rollAngle
= DECIDEGREES_TO_RADIANS(attitude
.values
.roll
);
1731 float pitchAngle
= DECIDEGREES_TO_RADIANS(attitude
.values
.pitch
);
1733 if (osdConfig()->ahi_reverse_roll
) {
1734 rollAngle
= -rollAngle
;
1736 osdDrawArtificialHorizon(osdDisplayPort
, osdGetDisplayPortCanvas(),
1737 OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), rollAngle
, pitchAngle
);
1738 osdDrawSingleElement(OSD_HORIZON_SIDEBARS
);
1739 osdDrawSingleElement(OSD_CROSSHAIRS
);
1744 case OSD_HORIZON_SIDEBARS
:
1746 osdDrawSidebars(osdDisplayPort
, osdGetDisplayPortCanvas());
1750 #if defined(USE_BARO) || defined(USE_GPS)
1753 float zvel
= getEstimatedActualVelocity(Z
);
1754 osdDrawVario(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), zvel
);
1760 int16_t value
= getEstimatedActualVelocity(Z
);
1762 switch ((osd_unit_e
)osdConfig()->units
) {
1765 case OSD_UNIT_IMPERIAL
:
1766 // Convert to centifeet/s
1767 value
= CENTIMETERS_TO_CENTIFEET(value
);
1770 case OSD_UNIT_METRIC
:
1776 osdFormatCentiNumber(buff
, value
, 0, 1, 0, 3);
1784 osdDisplayPIDValues(elemPosX
, elemPosY
, "ROL", PID_ROLL
, ADJUSTMENT_ROLL_P
, ADJUSTMENT_ROLL_I
, ADJUSTMENT_ROLL_D
);
1787 case OSD_PITCH_PIDS
:
1788 osdDisplayPIDValues(elemPosX
, elemPosY
, "PIT", PID_PITCH
, ADJUSTMENT_PITCH_P
, ADJUSTMENT_PITCH_I
, ADJUSTMENT_PITCH_D
);
1792 osdDisplayPIDValues(elemPosX
, elemPosY
, "YAW", PID_YAW
, ADJUSTMENT_YAW_P
, ADJUSTMENT_YAW_I
, ADJUSTMENT_YAW_D
);
1795 case OSD_LEVEL_PIDS
:
1796 osdDisplayPIDValues(elemPosX
, elemPosY
, "LEV", PID_LEVEL
, ADJUSTMENT_LEVEL_P
, ADJUSTMENT_LEVEL_I
, ADJUSTMENT_LEVEL_D
);
1799 case OSD_POS_XY_PIDS
:
1800 osdDisplayPIDValues(elemPosX
, elemPosY
, "PXY", PID_POS_XY
, ADJUSTMENT_POS_XY_P
, ADJUSTMENT_POS_XY_I
, ADJUSTMENT_POS_XY_D
);
1803 case OSD_POS_Z_PIDS
:
1804 osdDisplayPIDValues(elemPosX
, elemPosY
, "PZ", PID_POS_Z
, ADJUSTMENT_POS_Z_P
, ADJUSTMENT_POS_Z_I
, ADJUSTMENT_POS_Z_D
);
1807 case OSD_VEL_XY_PIDS
:
1808 osdDisplayPIDValues(elemPosX
, elemPosY
, "VXY", PID_VEL_XY
, ADJUSTMENT_VEL_XY_P
, ADJUSTMENT_VEL_XY_I
, ADJUSTMENT_VEL_XY_D
);
1811 case OSD_VEL_Z_PIDS
:
1812 osdDisplayPIDValues(elemPosX
, elemPosY
, "VZ", PID_VEL_Z
, ADJUSTMENT_VEL_Z_P
, ADJUSTMENT_VEL_Z_I
, ADJUSTMENT_VEL_Z_D
);
1816 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "HP", 0, pidBank()->pid
[PID_HEADING
].P
, 3, 0, ADJUSTMENT_HEADING_P
);
1819 case OSD_BOARD_ALIGN_ROLL
:
1820 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees
), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT
);
1823 case OSD_BOARD_ALIGN_PITCH
:
1824 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees
), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT
);
1828 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "EXP", 0, currentControlRateProfile
->stabilized
.rcExpo8
, 3, 0, ADJUSTMENT_RC_EXPO
);
1831 case OSD_RC_YAW_EXPO
:
1832 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "YEX", 0, currentControlRateProfile
->stabilized
.rcYawExpo8
, 3, 0, ADJUSTMENT_RC_YAW_EXPO
);
1835 case OSD_THROTTLE_EXPO
:
1836 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "TEX", 0, currentControlRateProfile
->throttle
.rcExpo8
, 3, 0, ADJUSTMENT_THROTTLE_EXPO
);
1839 case OSD_PITCH_RATE
:
1840 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "SPR");
1842 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1843 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->stabilized
.rates
[FD_PITCH
]);
1844 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE
))
1845 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1846 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
1850 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "SRR");
1852 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1853 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->stabilized
.rates
[FD_ROLL
]);
1854 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE
))
1855 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1856 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
1860 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "SYR", 0, currentControlRateProfile
->stabilized
.rates
[FD_YAW
], 3, 0, ADJUSTMENT_YAW_RATE
);
1863 case OSD_MANUAL_RC_EXPO
:
1864 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "MEX", 0, currentControlRateProfile
->manual
.rcExpo8
, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO
);
1867 case OSD_MANUAL_RC_YAW_EXPO
:
1868 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "MYX", 0, currentControlRateProfile
->manual
.rcYawExpo8
, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO
);
1871 case OSD_MANUAL_PITCH_RATE
:
1872 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "MPR");
1874 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1875 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->manual
.rates
[FD_PITCH
]);
1876 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE
))
1877 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1878 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
1881 case OSD_MANUAL_ROLL_RATE
:
1882 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "MRR");
1884 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1885 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->manual
.rates
[FD_ROLL
]);
1886 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE
))
1887 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1888 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
1891 case OSD_MANUAL_YAW_RATE
:
1892 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "MYR", 0, currentControlRateProfile
->stabilized
.rates
[FD_YAW
], 3, 0, ADJUSTMENT_YAW_RATE
);
1895 case OSD_NAV_FW_CRUISE_THR
:
1896 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "CRS", 0, navConfig()->fw
.cruise_throttle
, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR
);
1899 case OSD_NAV_FW_PITCH2THR
:
1900 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "P2T", 0, navConfig()->fw
.pitch_to_throttle
, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR
);
1903 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE
:
1904 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "0TP", 0, (float)mixerConfig()->fwMinThrottleDownPitchAngle
/ 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE
);
1907 case OSD_FW_ALT_PID_OUTPUTS
:
1909 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
1910 osdFormatPidControllerOutput(buff
, "PZO", &nav_pids
->fw_alt
, 10, true); // display requested pitch degrees
1914 case OSD_FW_POS_PID_OUTPUTS
:
1916 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers(); // display requested roll degrees
1917 osdFormatPidControllerOutput(buff
, "PXYO", &nav_pids
->fw_nav
, 1, true);
1921 case OSD_MC_VEL_Z_PID_OUTPUTS
:
1923 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
1924 osdFormatPidControllerOutput(buff
, "VZO", &nav_pids
->vel
[Z
], 100, false); // display throttle adjustment µs
1928 case OSD_MC_VEL_X_PID_OUTPUTS
:
1930 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
1931 osdFormatPidControllerOutput(buff
, "VXO", &nav_pids
->vel
[X
], 100, false); // display requested acceleration cm/s^2
1935 case OSD_MC_VEL_Y_PID_OUTPUTS
:
1937 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
1938 osdFormatPidControllerOutput(buff
, "VYO", &nav_pids
->vel
[Y
], 100, false); // display requested acceleration cm/s^2
1942 case OSD_MC_POS_XYZ_P_OUTPUTS
:
1944 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
1945 strcpy(buff
, "POSO ");
1946 // display requested velocity cm/s
1947 tfp_sprintf(buff
+ 5, "%4d", (int)lrintf(nav_pids
->pos
[X
].output_constrained
* 100));
1949 tfp_sprintf(buff
+ 10, "%4d", (int)lrintf(nav_pids
->pos
[Y
].output_constrained
* 100));
1951 tfp_sprintf(buff
+ 15, "%4d", (int)lrintf(nav_pids
->pos
[Z
].output_constrained
* 100));
1958 osdFormatCentiNumber(buff
, getPower(), 0, 2, 0, 3);
1968 osdFormatVelocityStr(buff
+ 1, pitot
.airSpeed
, false);
1977 // RTC not configured will show 00:00
1978 dateTime_t dateTime
;
1979 rtcGetDateTimeLocal(&dateTime
);
1980 buff
[0] = SYM_CLOCK
;
1981 tfp_sprintf(buff
+ 1, "%02u:%02u", dateTime
.hours
, dateTime
.minutes
);
1987 const char *message
= NULL
;
1988 char messageBuf
[MAX(SETTING_MAX_NAME_LENGTH
, OSD_MESSAGE_LENGTH
+1)];
1989 if (ARMING_FLAG(ARMED
)) {
1990 // Aircraft is armed. We might have up to 5
1991 // messages to show.
1992 const char *messages
[5];
1993 unsigned messageCount
= 0;
1994 if (FLIGHT_MODE(FAILSAFE_MODE
)) {
1995 // In FS mode while being armed too
1996 const char *failsafePhaseMessage
= osdFailsafePhaseMessage();
1997 const char *failsafeInfoMessage
= osdFailsafeInfoMessage();
1998 const char *navStateFSMessage
= navigationStateMessage();
1999 if (failsafePhaseMessage
) {
2000 messages
[messageCount
++] = failsafePhaseMessage
;
2002 if (failsafeInfoMessage
) {
2003 messages
[messageCount
++] = failsafeInfoMessage
;
2005 if (navStateFSMessage
) {
2006 messages
[messageCount
++] = navStateFSMessage
;
2008 if (messageCount
> 0) {
2009 message
= messages
[OSD_ALTERNATING_CHOICES(1000, messageCount
)];
2010 if (message
== failsafeInfoMessage
) {
2011 // failsafeInfoMessage is not useful for recovering
2012 // a lost model, but might help avoiding a crash.
2013 // Blink to grab user attention.
2014 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2016 // We're shoing either failsafePhaseMessage or
2017 // navStateFSMessage. Don't BLINK here since
2018 // having this text available might be crucial
2019 // during a lost aircraft recovery and blinking
2020 // will cause it to be missing from some frames.
2023 if (FLIGHT_MODE(NAV_RTH_MODE
) || FLIGHT_MODE(NAV_WP_MODE
) || navigationIsExecutingAnEmergencyLanding()) {
2024 const char *navStateMessage
= navigationStateMessage();
2025 if (navStateMessage
) {
2026 messages
[messageCount
++] = navStateMessage
;
2028 } else if (STATE(FIXED_WING_LEGACY
) && (navGetCurrentStateFlags() & NAV_CTL_LAUNCH
)) {
2029 messages
[messageCount
++] = "AUTOLAUNCH";
2031 if (FLIGHT_MODE(NAV_ALTHOLD_MODE
) && !navigationRequiresAngleMode()) {
2032 // ALTHOLD might be enabled alongside ANGLE/HORIZON/ACRO
2033 // when it doesn't require ANGLE mode (required only in FW
2034 // right now). If if requires ANGLE, its display is handled
2036 messages
[messageCount
++] = "(ALTITUDE HOLD)";
2038 if (IS_RC_MODE_ACTIVE(BOXAUTOTRIM
)) {
2039 messages
[messageCount
++] = "(AUTOTRIM)";
2041 if (IS_RC_MODE_ACTIVE(BOXAUTOTUNE
)) {
2042 messages
[messageCount
++] = "(AUTOTUNE)";
2044 if (FLIGHT_MODE(HEADFREE_MODE
)) {
2045 messages
[messageCount
++] = "(HEADFREE)";
2048 // Pick one of the available messages. Each message lasts
2050 if (messageCount
> 0) {
2051 message
= messages
[OSD_ALTERNATING_CHOICES(1000, messageCount
)];
2054 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS
)) {
2055 unsigned invalidIndex
;
2056 // Check if we're unable to arm for some reason
2057 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING
) && !settingsValidate(&invalidIndex
)) {
2058 if (OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
2059 const setting_t
*setting
= settingGet(invalidIndex
);
2060 settingGetName(setting
, messageBuf
);
2061 for (int ii
= 0; messageBuf
[ii
]; ii
++) {
2062 messageBuf
[ii
] = sl_toupper(messageBuf
[ii
]);
2064 message
= messageBuf
;
2066 message
= "INVALID SETTING";
2067 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr
);
2070 if (OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
2071 message
= "UNABLE TO ARM";
2072 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr
);
2074 // Show the reason for not arming
2075 message
= osdArmingDisabledReasonMessage();
2079 osdFormatMessage(buff
, sizeof(buff
), message
);
2083 case OSD_MAIN_BATT_CELL_VOLTAGE
:
2085 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatteryRawAverageCellVoltage(), 3, 2);
2089 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE
:
2091 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatterySagCompensatedAverageCellVoltage(), 3, 2);
2095 case OSD_THROTTLE_POS_AUTO_THR
:
2097 osdFormatThrottlePosition(buff
, true, &elemAttr
);
2101 case OSD_HEADING_GRAPH
:
2103 if (osdIsHeadingValid()) {
2104 osdDrawHeadingGraph(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), osdGetHeading());
2107 buff
[0] = buff
[2] = buff
[4] = buff
[6] = buff
[8] = SYM_HEADING_LINE
;
2108 buff
[1] = buff
[3] = buff
[5] = buff
[7] = SYM_HEADING_DIVIDED_LINE
;
2109 buff
[OSD_HEADING_GRAPH_WIDTH
] = '\0';
2114 case OSD_EFFICIENCY_MAH_PER_KM
:
2116 // amperage is in centi amps, speed is in cms/s. We want
2117 // mah/km. Values over 999 are considered useless and
2118 // displayed as "---""
2119 static pt1Filter_t eFilterState
;
2120 static timeUs_t efficiencyUpdated
= 0;
2122 timeUs_t currentTimeUs
= micros();
2123 timeDelta_t efficiencyTimeDelta
= cmpTimeUs(currentTimeUs
, efficiencyUpdated
);
2124 if (STATE(GPS_FIX
) && gpsSol
.groundSpeed
> 0) {
2125 if (efficiencyTimeDelta
>= EFFICIENCY_UPDATE_INTERVAL
) {
2126 value
= pt1FilterApply4(&eFilterState
, ((float)getAmperage() / gpsSol
.groundSpeed
) / 0.0036f
,
2127 1, efficiencyTimeDelta
* 1e-6f
);
2129 efficiencyUpdated
= currentTimeUs
;
2131 value
= eFilterState
.state
;
2134 if (value
> 0 && value
<= 999) {
2135 tfp_sprintf(buff
, "%3d", (int)value
);
2137 buff
[0] = buff
[1] = buff
[2] = '-';
2139 buff
[3] = SYM_MAH_KM_0
;
2140 buff
[4] = SYM_MAH_KM_1
;
2145 case OSD_EFFICIENCY_WH_PER_KM
:
2147 // amperage is in centi amps, speed is in cms/s. We want
2148 // mWh/km. Values over 999Wh/km are considered useless and
2149 // displayed as "---""
2150 static pt1Filter_t eFilterState
;
2151 static timeUs_t efficiencyUpdated
= 0;
2153 timeUs_t currentTimeUs
= micros();
2154 timeDelta_t efficiencyTimeDelta
= cmpTimeUs(currentTimeUs
, efficiencyUpdated
);
2155 if (STATE(GPS_FIX
) && gpsSol
.groundSpeed
> 0) {
2156 if (efficiencyTimeDelta
>= EFFICIENCY_UPDATE_INTERVAL
) {
2157 value
= pt1FilterApply4(&eFilterState
, ((float)getPower() / gpsSol
.groundSpeed
) / 0.0036f
,
2158 1, efficiencyTimeDelta
* 1e-6f
);
2160 efficiencyUpdated
= currentTimeUs
;
2162 value
= eFilterState
.state
;
2165 if (value
> 0 && value
<= 999999) {
2166 osdFormatCentiNumber(buff
, value
/ 10, 0, 2, 0, 3);
2168 buff
[0] = buff
[1] = buff
[2] = '-';
2170 buff
[3] = SYM_WH_KM_0
;
2171 buff
[4] = SYM_WH_KM_1
;
2178 buff
[0] = SYM_GFORCE
;
2179 osdFormatCentiNumber(buff
+ 1, GForce
, 0, 2, 0, 3);
2180 if (GForce
> osdConfig()->gforce_alarm
* 100) {
2181 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2190 float GForceValue
= GForceAxis
[item
- OSD_GFORCE_X
];
2191 buff
[0] = SYM_GFORCE_X
+ item
- OSD_GFORCE_X
;
2192 osdFormatCentiNumber(buff
+ 1, GForceValue
, 0, 2, 0, 4);
2193 if ((GForceValue
< osdConfig()->gforce_axis_alarm_min
* 100) || (GForceValue
> osdConfig()->gforce_axis_alarm_max
* 100)) {
2194 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2201 // Longest representable string is -2147483648, hence 11 characters
2202 for (uint8_t bufferIndex
= 0; bufferIndex
< DEBUG32_VALUE_COUNT
; ++elemPosY
, bufferIndex
+= 2) {
2203 tfp_sprintf(buff
, "[%u]=%11ld [%u]=%11ld", bufferIndex
, debug
[bufferIndex
], bufferIndex
+1, debug
[bufferIndex
+1]);
2204 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
2209 case OSD_IMU_TEMPERATURE
:
2211 int16_t temperature
;
2212 const bool valid
= getIMUTemperature(&temperature
);
2213 osdDisplayTemperature(elemPosX
, elemPosY
, SYM_IMU_TEMP
, NULL
, valid
, temperature
, osdConfig()->imu_temp_alarm_min
, osdConfig()->imu_temp_alarm_max
);
2217 case OSD_BARO_TEMPERATURE
:
2219 int16_t temperature
;
2220 const bool valid
= getBaroTemperature(&temperature
);
2221 osdDisplayTemperature(elemPosX
, elemPosY
, SYM_BARO_TEMP
, NULL
, valid
, temperature
, osdConfig()->imu_temp_alarm_min
, osdConfig()->imu_temp_alarm_max
);
2225 #ifdef USE_TEMPERATURE_SENSOR
2226 case OSD_TEMP_SENSOR_0_TEMPERATURE
:
2227 case OSD_TEMP_SENSOR_1_TEMPERATURE
:
2228 case OSD_TEMP_SENSOR_2_TEMPERATURE
:
2229 case OSD_TEMP_SENSOR_3_TEMPERATURE
:
2230 case OSD_TEMP_SENSOR_4_TEMPERATURE
:
2231 case OSD_TEMP_SENSOR_5_TEMPERATURE
:
2232 case OSD_TEMP_SENSOR_6_TEMPERATURE
:
2233 case OSD_TEMP_SENSOR_7_TEMPERATURE
:
2235 osdDisplayTemperatureSensor(elemPosX
, elemPosY
, item
- OSD_TEMP_SENSOR_0_TEMPERATURE
);
2238 #endif /* ifdef USE_TEMPERATURE_SENSOR */
2240 case OSD_WIND_SPEED_HORIZONTAL
:
2241 #ifdef USE_WIND_ESTIMATOR
2243 bool valid
= isEstimatedWindSpeedValid();
2244 float horizontalWindSpeed
;
2247 horizontalWindSpeed
= getEstimatedHorizontalWindSpeed(&angle
);
2248 int16_t windDirection
= osdGetHeadingAngle((int)angle
- DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
));
2249 buff
[1] = SYM_DIRECTION
+ (windDirection
* 2 / 90);
2251 horizontalWindSpeed
= 0;
2252 buff
[1] = SYM_BLANK
;
2254 buff
[0] = SYM_WIND_HORIZONTAL
;
2255 osdFormatWindSpeedStr(buff
+ 2, horizontalWindSpeed
, valid
);
2262 case OSD_WIND_SPEED_VERTICAL
:
2263 #ifdef USE_WIND_ESTIMATOR
2265 buff
[0] = SYM_WIND_VERTICAL
;
2266 buff
[1] = SYM_BLANK
;
2267 bool valid
= isEstimatedWindSpeedValid();
2268 float verticalWindSpeed
;
2270 verticalWindSpeed
= getEstimatedWindSpeed(Z
);
2271 if (verticalWindSpeed
< 0) {
2272 buff
[1] = SYM_AH_DECORATION_DOWN
;
2273 verticalWindSpeed
= -verticalWindSpeed
;
2274 } else if (verticalWindSpeed
> 0) {
2275 buff
[1] = SYM_AH_DECORATION_UP
;
2278 verticalWindSpeed
= 0;
2280 osdFormatWindSpeedStr(buff
+ 2, verticalWindSpeed
, valid
);
2289 STATIC_ASSERT(GPS_DEGREES_DIVIDER
== OLC_DEG_MULTIPLIER
, invalid_olc_deg_multiplier
);
2290 int digits
= osdConfig()->plus_code_digits
;
2291 if (STATE(GPS_FIX
)) {
2292 olc_encode(gpsSol
.llh
.lat
, gpsSol
.llh
.lon
, digits
, buff
, sizeof(buff
));
2294 // +codes with > 8 digits have a + at the 9th digit
2295 // and we only support 10 and up.
2296 memset(buff
, '-', digits
+ 1);
2298 buff
[digits
+ 1] = '\0';
2306 buff
[0] = SYM_AZIMUTH
;
2307 if (osdIsHeadingValid()) {
2308 int16_t h
= GPS_directionToHome
;
2317 tfp_sprintf(&buff
[1], "%3d", h
);
2319 buff
[1] = buff
[2] = buff
[3] = '-';
2328 int scaleUnitDivisor
;
2333 switch (osdConfig()->units
) {
2334 case OSD_UNIT_IMPERIAL
:
2335 scaleToUnit
= 100 / 1609.3440f
; // scale to 0.01mi for osdFormatCentiNumber()
2336 scaleUnitDivisor
= 0;
2337 symUnscaled
= SYM_MI
;
2343 case OSD_UNIT_METRIC
:
2344 scaleToUnit
= 100; // scale to cm for osdFormatCentiNumber()
2345 scaleUnitDivisor
= 1000; // Convert to km when scale gets bigger than 999m
2346 symUnscaled
= SYM_M
;
2351 buff
[0] = SYM_SCALE
;
2352 if (osdMapData
.scale
> 0) {
2353 bool scaled
= osdFormatCentiNumber(&buff
[1], osdMapData
.scale
* scaleToUnit
, scaleUnitDivisor
, maxDecimals
, 2, 3);
2354 buff
[4] = scaled
? symScaled
: symUnscaled
;
2355 // Make sure this is cleared if the map stops being drawn
2356 osdMapData
.scale
= 0;
2358 memset(&buff
[1], '-', 4);
2363 case OSD_MAP_REFERENCE
:
2365 char referenceSymbol
;
2366 if (osdMapData
.referenceSymbol
) {
2367 referenceSymbol
= osdMapData
.referenceSymbol
;
2368 // Make sure this is cleared if the map stops being drawn
2369 osdMapData
.referenceSymbol
= 0;
2371 referenceSymbol
= '-';
2373 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_DIRECTION
);
2374 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
+ 1, referenceSymbol
);
2378 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
2381 const char *source_text
= IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE
) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
2382 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE
) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2383 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, source_text
, elemAttr
);
2388 #if defined(USE_ESC_SENSOR)
2391 escSensorData_t
* escSensor
= escSensorGetData();
2392 if (escSensor
&& escSensor
->dataAge
<= ESC_DATA_MAX_AGE
) {
2393 osdFormatRpm(buff
, escSensor
->rpm
);
2396 osdFormatRpm(buff
, 0);
2400 case OSD_ESC_TEMPERATURE
:
2402 escSensorData_t
* escSensor
= escSensorGetData();
2403 bool escTemperatureValid
= escSensor
&& escSensor
->dataAge
<= ESC_DATA_MAX_AGE
;
2404 osdDisplayTemperature(elemPosX
, elemPosY
, SYM_ESC_TEMP
, NULL
, escTemperatureValid
, (escSensor
->temperature
)*10, osdConfig()->esc_temp_alarm_min
, osdConfig()->esc_temp_alarm_max
);
2413 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
2417 static uint8_t osdIncElementIndex(uint8_t elementIndex
)
2421 if (elementIndex
== OSD_ARTIFICIAL_HORIZON
)
2424 #ifndef USE_TEMPERATURE_SENSOR
2425 if (elementIndex
== OSD_TEMP_SENSOR_0_TEMPERATURE
)
2426 elementIndex
= OSD_ALTITUDE_MSL
;
2429 if (!sensors(SENSOR_ACC
)) {
2430 if (elementIndex
== OSD_CROSSHAIRS
) {
2431 elementIndex
= OSD_ONTIME
;
2435 if (!feature(FEATURE_VBAT
)) {
2436 if (elementIndex
== OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
) {
2437 elementIndex
= OSD_LEVEL_PIDS
;
2441 if (!feature(FEATURE_CURRENT_METER
)) {
2442 if (elementIndex
== OSD_CURRENT_DRAW
) {
2443 elementIndex
= OSD_GPS_SPEED
;
2445 if (elementIndex
== OSD_EFFICIENCY_MAH_PER_KM
) {
2446 elementIndex
= OSD_TRIP_DIST
;
2448 if (elementIndex
== OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH
) {
2449 elementIndex
= OSD_HOME_HEADING_ERROR
;
2451 if (elementIndex
== OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
) {
2452 elementIndex
= OSD_LEVEL_PIDS
;
2456 if (!feature(FEATURE_GPS
)) {
2457 if (elementIndex
== OSD_GPS_SPEED
) {
2458 elementIndex
= OSD_ALTITUDE
;
2460 if (elementIndex
== OSD_GPS_LON
) {
2461 elementIndex
= OSD_VARIO
;
2463 if (elementIndex
== OSD_GPS_HDOP
) {
2464 elementIndex
= OSD_MAIN_BATT_CELL_VOLTAGE
;
2466 if (elementIndex
== OSD_TRIP_DIST
) {
2467 elementIndex
= OSD_ATTITUDE_PITCH
;
2469 if (elementIndex
== OSD_WIND_SPEED_HORIZONTAL
) {
2470 elementIndex
= OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
;
2472 if (elementIndex
== OSD_3D_SPEED
) {
2477 if (!STATE(ESC_SENSOR_ENABLED
)) {
2478 if (elementIndex
== OSD_ESC_RPM
) {
2483 if (elementIndex
== OSD_ITEM_COUNT
) {
2486 return elementIndex
;
2489 void osdDrawNextElement(void)
2491 static uint8_t elementIndex
= 0;
2492 // Prevent infinite loop when no elements are enabled
2493 uint8_t index
= elementIndex
;
2495 elementIndex
= osdIncElementIndex(elementIndex
);
2496 } while(!osdDrawSingleElement(elementIndex
) && index
!= elementIndex
);
2498 // Draw artificial horizon last
2499 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON
);
2502 PG_RESET_TEMPLATE(osdConfig_t
, osdConfig
,
2509 .imu_temp_alarm_min
= -200,
2510 .imu_temp_alarm_max
= 600,
2511 .esc_temp_alarm_min
= -200,
2512 .esc_temp_alarm_max
= 900,
2514 .gforce_axis_alarm_min
= -5,
2515 .gforce_axis_alarm_max
= 5,
2517 .baro_temp_alarm_min
= -200,
2518 .baro_temp_alarm_max
= 600,
2521 #ifdef USE_TEMPERATURE_SENSOR
2522 .temp_label_align
= OSD_ALIGN_LEFT
,
2525 .video_system
= VIDEO_SYSTEM_AUTO
,
2527 .ahi_reverse_roll
= 0,
2528 .ahi_max_pitch
= AH_MAX_PITCH_DEFAULT
,
2529 .crosshairs_style
= OSD_CROSSHAIRS_STYLE_DEFAULT
,
2530 .horizon_offset
= 0,
2532 .camera_fov_h
= 135,
2538 .hud_radar_disp
= 0,
2539 .hud_radar_range_min
= 3,
2540 .hud_radar_range_max
= 4000,
2541 .hud_radar_nearest
= 0,
2543 .left_sidebar_scroll
= OSD_SIDEBAR_SCROLL_NONE
,
2544 .right_sidebar_scroll
= OSD_SIDEBAR_SCROLL_NONE
,
2545 .sidebar_scroll_arrows
= 0,
2547 .units
= OSD_UNIT_METRIC
,
2548 .main_voltage_decimals
= 1,
2550 .estimations_wind_compensation
= true,
2551 .coordinate_digits
= 9,
2553 .osd_failsafe_switch_layout
= false,
2555 .plus_code_digits
= 11,
2557 .ahi_width
= OSD_AHI_WIDTH
* OSD_CHAR_WIDTH
,
2558 .ahi_height
= OSD_AHI_HEIGHT
* OSD_CHAR_HEIGHT
,
2559 .ahi_vertical_offset
= -OSD_CHAR_HEIGHT
,
2560 .sidebar_horizontal_offset
= OSD_AH_SIDEBAR_WIDTH_POS
* OSD_CHAR_WIDTH
,
2563 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t
*osdLayoutsConfig
)
2565 osdLayoutsConfig
->item_pos
[0][OSD_ALTITUDE
] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG
;
2566 osdLayoutsConfig
->item_pos
[0][OSD_MAIN_BATT_VOLTAGE
] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG
;
2567 osdLayoutsConfig
->item_pos
[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
] = OSD_POS(12, 1);
2569 osdLayoutsConfig
->item_pos
[0][OSD_RSSI_VALUE
] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG
;
2571 osdLayoutsConfig
->item_pos
[0][OSD_HOME_DIST
] = OSD_POS(1, 1);
2572 osdLayoutsConfig
->item_pos
[0][OSD_TRIP_DIST
] = OSD_POS(1, 2);
2573 osdLayoutsConfig
->item_pos
[0][OSD_MAIN_BATT_CELL_VOLTAGE
] = OSD_POS(12, 1);
2574 osdLayoutsConfig
->item_pos
[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE
] = OSD_POS(12, 1);
2575 osdLayoutsConfig
->item_pos
[0][OSD_GPS_SPEED
] = OSD_POS(23, 1);
2576 osdLayoutsConfig
->item_pos
[0][OSD_3D_SPEED
] = OSD_POS(23, 1);
2578 osdLayoutsConfig
->item_pos
[0][OSD_THROTTLE_POS
] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG
;
2579 osdLayoutsConfig
->item_pos
[0][OSD_THROTTLE_POS_AUTO_THR
] = OSD_POS(6, 2);
2580 osdLayoutsConfig
->item_pos
[0][OSD_HEADING
] = OSD_POS(12, 2);
2581 osdLayoutsConfig
->item_pos
[0][OSD_CRUISE_HEADING_ERROR
] = OSD_POS(12, 2);
2582 osdLayoutsConfig
->item_pos
[0][OSD_CRUISE_HEADING_ADJUSTMENT
] = OSD_POS(12, 2);
2583 osdLayoutsConfig
->item_pos
[0][OSD_HEADING_GRAPH
] = OSD_POS(18, 2);
2584 osdLayoutsConfig
->item_pos
[0][OSD_CURRENT_DRAW
] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG
;
2585 osdLayoutsConfig
->item_pos
[0][OSD_MAH_DRAWN
] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG
;
2586 osdLayoutsConfig
->item_pos
[0][OSD_WH_DRAWN
] = OSD_POS(1, 5);
2587 osdLayoutsConfig
->item_pos
[0][OSD_BATTERY_REMAINING_CAPACITY
] = OSD_POS(1, 6);
2588 osdLayoutsConfig
->item_pos
[0][OSD_BATTERY_REMAINING_PERCENT
] = OSD_POS(1, 7);
2589 osdLayoutsConfig
->item_pos
[0][OSD_POWER_SUPPLY_IMPEDANCE
] = OSD_POS(1, 8);
2591 osdLayoutsConfig
->item_pos
[0][OSD_EFFICIENCY_MAH_PER_KM
] = OSD_POS(1, 5);
2592 osdLayoutsConfig
->item_pos
[0][OSD_EFFICIENCY_WH_PER_KM
] = OSD_POS(1, 5);
2594 osdLayoutsConfig
->item_pos
[0][OSD_ATTITUDE_ROLL
] = OSD_POS(1, 7);
2595 osdLayoutsConfig
->item_pos
[0][OSD_ATTITUDE_PITCH
] = OSD_POS(1, 8);
2597 // avoid OSD_VARIO under OSD_CROSSHAIRS
2598 osdLayoutsConfig
->item_pos
[0][OSD_VARIO
] = OSD_POS(23, 5);
2599 // OSD_VARIO_NUM at the right of OSD_VARIO
2600 osdLayoutsConfig
->item_pos
[0][OSD_VARIO_NUM
] = OSD_POS(24, 7);
2601 osdLayoutsConfig
->item_pos
[0][OSD_HOME_DIR
] = OSD_POS(14, 11);
2602 osdLayoutsConfig
->item_pos
[0][OSD_ARTIFICIAL_HORIZON
] = OSD_POS(8, 6) | OSD_VISIBLE_FLAG
;
2603 osdLayoutsConfig
->item_pos
[0][OSD_HORIZON_SIDEBARS
] = OSD_POS(8, 6) | OSD_VISIBLE_FLAG
;
2605 osdLayoutsConfig
->item_pos
[0][OSD_CRAFT_NAME
] = OSD_POS(20, 2);
2606 osdLayoutsConfig
->item_pos
[0][OSD_VTX_CHANNEL
] = OSD_POS(8, 6);
2608 osdLayoutsConfig
->item_pos
[0][OSD_ONTIME
] = OSD_POS(23, 8);
2609 osdLayoutsConfig
->item_pos
[0][OSD_FLYTIME
] = OSD_POS(23, 9);
2610 osdLayoutsConfig
->item_pos
[0][OSD_ONTIME_FLYTIME
] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG
;
2611 osdLayoutsConfig
->item_pos
[0][OSD_RTC_TIME
] = OSD_POS(23, 12);
2612 osdLayoutsConfig
->item_pos
[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH
] = OSD_POS(23, 7);
2613 osdLayoutsConfig
->item_pos
[0][OSD_REMAINING_DISTANCE_BEFORE_RTH
] = OSD_POS(23, 6);
2615 osdLayoutsConfig
->item_pos
[0][OSD_GPS_SATS
] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG
;
2616 osdLayoutsConfig
->item_pos
[0][OSD_GPS_HDOP
] = OSD_POS(0, 10);
2618 osdLayoutsConfig
->item_pos
[0][OSD_GPS_LAT
] = OSD_POS(0, 12);
2619 // Put this on top of the latitude, since it's very unlikely
2620 // that users will want to use both at the same time.
2621 osdLayoutsConfig
->item_pos
[0][OSD_PLUS_CODE
] = OSD_POS(0, 12);
2622 osdLayoutsConfig
->item_pos
[0][OSD_FLYMODE
] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG
;
2623 osdLayoutsConfig
->item_pos
[0][OSD_GPS_LON
] = OSD_POS(18, 12);
2625 osdLayoutsConfig
->item_pos
[0][OSD_AZIMUTH
] = OSD_POS(2, 12);
2627 osdLayoutsConfig
->item_pos
[0][OSD_ROLL_PIDS
] = OSD_POS(2, 10);
2628 osdLayoutsConfig
->item_pos
[0][OSD_PITCH_PIDS
] = OSD_POS(2, 11);
2629 osdLayoutsConfig
->item_pos
[0][OSD_YAW_PIDS
] = OSD_POS(2, 12);
2630 osdLayoutsConfig
->item_pos
[0][OSD_LEVEL_PIDS
] = OSD_POS(2, 12);
2631 osdLayoutsConfig
->item_pos
[0][OSD_POS_XY_PIDS
] = OSD_POS(2, 12);
2632 osdLayoutsConfig
->item_pos
[0][OSD_POS_Z_PIDS
] = OSD_POS(2, 12);
2633 osdLayoutsConfig
->item_pos
[0][OSD_VEL_XY_PIDS
] = OSD_POS(2, 12);
2634 osdLayoutsConfig
->item_pos
[0][OSD_VEL_Z_PIDS
] = OSD_POS(2, 12);
2635 osdLayoutsConfig
->item_pos
[0][OSD_HEADING_P
] = OSD_POS(2, 12);
2636 osdLayoutsConfig
->item_pos
[0][OSD_BOARD_ALIGN_ROLL
] = OSD_POS(2, 10);
2637 osdLayoutsConfig
->item_pos
[0][OSD_BOARD_ALIGN_PITCH
] = OSD_POS(2, 11);
2638 osdLayoutsConfig
->item_pos
[0][OSD_RC_EXPO
] = OSD_POS(2, 12);
2639 osdLayoutsConfig
->item_pos
[0][OSD_RC_YAW_EXPO
] = OSD_POS(2, 12);
2640 osdLayoutsConfig
->item_pos
[0][OSD_THROTTLE_EXPO
] = OSD_POS(2, 12);
2641 osdLayoutsConfig
->item_pos
[0][OSD_PITCH_RATE
] = OSD_POS(2, 12);
2642 osdLayoutsConfig
->item_pos
[0][OSD_ROLL_RATE
] = OSD_POS(2, 12);
2643 osdLayoutsConfig
->item_pos
[0][OSD_YAW_RATE
] = OSD_POS(2, 12);
2644 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_RC_EXPO
] = OSD_POS(2, 12);
2645 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_RC_YAW_EXPO
] = OSD_POS(2, 12);
2646 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_PITCH_RATE
] = OSD_POS(2, 12);
2647 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_ROLL_RATE
] = OSD_POS(2, 12);
2648 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_YAW_RATE
] = OSD_POS(2, 12);
2649 osdLayoutsConfig
->item_pos
[0][OSD_NAV_FW_CRUISE_THR
] = OSD_POS(2, 12);
2650 osdLayoutsConfig
->item_pos
[0][OSD_NAV_FW_PITCH2THR
] = OSD_POS(2, 12);
2651 osdLayoutsConfig
->item_pos
[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE
] = OSD_POS(2, 12);
2652 osdLayoutsConfig
->item_pos
[0][OSD_FW_ALT_PID_OUTPUTS
] = OSD_POS(2, 12);
2653 osdLayoutsConfig
->item_pos
[0][OSD_FW_POS_PID_OUTPUTS
] = OSD_POS(2, 12);
2654 osdLayoutsConfig
->item_pos
[0][OSD_MC_VEL_X_PID_OUTPUTS
] = OSD_POS(2, 12);
2655 osdLayoutsConfig
->item_pos
[0][OSD_MC_VEL_Y_PID_OUTPUTS
] = OSD_POS(2, 12);
2656 osdLayoutsConfig
->item_pos
[0][OSD_MC_VEL_Z_PID_OUTPUTS
] = OSD_POS(2, 12);
2657 osdLayoutsConfig
->item_pos
[0][OSD_MC_POS_XYZ_P_OUTPUTS
] = OSD_POS(2, 12);
2659 osdLayoutsConfig
->item_pos
[0][OSD_POWER
] = OSD_POS(15, 1);
2661 osdLayoutsConfig
->item_pos
[0][OSD_IMU_TEMPERATURE
] = OSD_POS(19, 2);
2662 osdLayoutsConfig
->item_pos
[0][OSD_BARO_TEMPERATURE
] = OSD_POS(19, 3);
2663 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_0_TEMPERATURE
] = OSD_POS(19, 4);
2664 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_1_TEMPERATURE
] = OSD_POS(19, 5);
2665 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_2_TEMPERATURE
] = OSD_POS(19, 6);
2666 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_3_TEMPERATURE
] = OSD_POS(19, 7);
2667 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_4_TEMPERATURE
] = OSD_POS(19, 8);
2668 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_5_TEMPERATURE
] = OSD_POS(19, 9);
2669 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_6_TEMPERATURE
] = OSD_POS(19, 10);
2670 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_7_TEMPERATURE
] = OSD_POS(19, 11);
2672 osdLayoutsConfig
->item_pos
[0][OSD_AIR_SPEED
] = OSD_POS(3, 5);
2673 osdLayoutsConfig
->item_pos
[0][OSD_WIND_SPEED_HORIZONTAL
] = OSD_POS(3, 6);
2674 osdLayoutsConfig
->item_pos
[0][OSD_WIND_SPEED_VERTICAL
] = OSD_POS(3, 7);
2676 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE
] = OSD_POS(12, 4);
2677 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE_X
] = OSD_POS(12, 5);
2678 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE_Y
] = OSD_POS(12, 6);
2679 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE_Z
] = OSD_POS(12, 7);
2681 osdLayoutsConfig
->item_pos
[0][OSD_VTX_POWER
] = OSD_POS(3, 5);
2683 #if defined(USE_ESC_SENSOR)
2684 osdLayoutsConfig
->item_pos
[0][OSD_ESC_RPM
] = OSD_POS(1, 2);
2685 osdLayoutsConfig
->item_pos
[0][OSD_ESC_TEMPERATURE
] = OSD_POS(1, 3);
2688 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
2689 osdLayoutsConfig
->item_pos
[0][OSD_RC_SOURCE
] = OSD_POS(3, 4);
2692 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
2693 osdLayoutsConfig
->item_pos
[0][OSD_MESSAGES
] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG
;
2695 for (unsigned ii
= 1; ii
< OSD_LAYOUT_COUNT
; ii
++) {
2696 for (unsigned jj
= 0; jj
< ARRAYLEN(osdLayoutsConfig
->item_pos
[0]); jj
++) {
2697 osdLayoutsConfig
->item_pos
[ii
][jj
] = osdLayoutsConfig
->item_pos
[0][jj
] & ~OSD_VISIBLE_FLAG
;
2702 static void osdSetNextRefreshIn(uint32_t timeMs
) {
2703 resumeRefreshAt
= micros() + timeMs
* 1000;
2704 refreshWaitForResumeCmdRelease
= true;
2707 static void osdCompleteAsyncInitialization(void)
2709 if (!displayIsReady(osdDisplayPort
)) {
2710 // Update the display.
2711 // XXX: Rename displayDrawScreen() and associated functions
2712 // to displayUpdate()
2713 displayDrawScreen(osdDisplayPort
);
2717 osdDisplayIsReady
= true;
2719 #if defined(USE_CANVAS)
2720 if (osdConfig()->force_grid
) {
2721 osdDisplayHasCanvas
= false;
2723 osdDisplayHasCanvas
= displayGetCanvas(&osdCanvas
, osdDisplayPort
);
2727 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
2728 displayClearScreen(osdDisplayPort
);
2731 displayFontMetadata_t metadata
;
2732 bool fontHasMetadata
= displayGetFontMetadata(&metadata
, osdDisplayPort
);
2733 LOG_D(OSD
, "Font metadata version %s: %u (%u chars)",
2734 fontHasMetadata
? "Y" : "N", metadata
.version
, metadata
.charCount
);
2736 if (fontHasMetadata
&& metadata
.charCount
> 256) {
2737 hasExtendedFont
= true;
2738 unsigned logo_c
= SYM_LOGO_START
;
2739 unsigned logo_x
= OSD_CENTER_LEN(SYM_LOGO_WIDTH
);
2740 for (unsigned ii
= 0; ii
< SYM_LOGO_HEIGHT
; ii
++) {
2741 for (unsigned jj
= 0; jj
< SYM_LOGO_WIDTH
; jj
++) {
2742 displayWriteChar(osdDisplayPort
, logo_x
+ jj
, y
, logo_c
++);
2748 if (!fontHasMetadata
|| metadata
.version
< OSD_MIN_FONT_VERSION
) {
2749 const char *m
= "INVALID FONT";
2750 displayWrite(osdDisplayPort
, OSD_CENTER_S(m
), 3, m
);
2755 char string_buffer
[30];
2756 tfp_sprintf(string_buffer
, "INAV VERSION: %s", FC_VERSION_STRING
);
2757 displayWrite(osdDisplayPort
, 5, y
++, string_buffer
);
2759 displayWrite(osdDisplayPort
, 7, y
++, CMS_STARTUP_HELP_TEXT1
);
2760 displayWrite(osdDisplayPort
, 11, y
++, CMS_STARTUP_HELP_TEXT2
);
2761 displayWrite(osdDisplayPort
, 11, y
++, CMS_STARTUP_HELP_TEXT3
);
2765 #define STATS_LABEL_X_POS 4
2766 #define STATS_VALUE_X_POS 24
2767 if (statsConfig()->stats_enabled
) {
2768 displayWrite(osdDisplayPort
, STATS_LABEL_X_POS
, ++y
, "ODOMETER:");
2769 if (osdConfig()->units
== OSD_UNIT_IMPERIAL
) {
2770 tfp_sprintf(string_buffer
, "%5d", (int)(statsConfig()->stats_total_dist
/ METERS_PER_MILE
));
2771 string_buffer
[5] = SYM_MI
;
2773 tfp_sprintf(string_buffer
, "%5d", (int)(statsConfig()->stats_total_dist
/ METERS_PER_KILOMETER
));
2774 string_buffer
[5] = SYM_KM
;
2776 string_buffer
[6] = '\0';
2777 displayWrite(osdDisplayPort
, STATS_VALUE_X_POS
-5, y
, string_buffer
);
2779 displayWrite(osdDisplayPort
, STATS_LABEL_X_POS
, ++y
, "TOTAL TIME:");
2780 uint32_t tot_mins
= statsConfig()->stats_total_time
/ 60;
2781 tfp_sprintf(string_buffer
, "%2d:%02dHM", (int)(tot_mins
/ 60), (int)(tot_mins
% 60));
2782 displayWrite(osdDisplayPort
, STATS_VALUE_X_POS
-5, y
, string_buffer
);
2785 if (feature(FEATURE_VBAT
) && feature(FEATURE_CURRENT_METER
)) {
2786 displayWrite(osdDisplayPort
, STATS_LABEL_X_POS
, ++y
, "TOTAL ENERGY:");
2787 osdFormatCentiNumber(string_buffer
, statsConfig()->stats_total_energy
/ 10, 0, 2, 0, 4);
2788 strcat(string_buffer
, "\xAB"); // SYM_WH
2789 displayWrite(osdDisplayPort
, STATS_VALUE_X_POS
-4, y
, string_buffer
);
2791 displayWrite(osdDisplayPort
, STATS_LABEL_X_POS
, ++y
, "AVG EFFICIENCY:");
2792 if (statsConfig()->stats_total_dist
) {
2793 uint32_t avg_efficiency
= statsConfig()->stats_total_energy
/ (statsConfig()->stats_total_dist
/ METERS_PER_KILOMETER
); // mWh/km
2794 osdFormatCentiNumber(string_buffer
, avg_efficiency
/ 10, 0, 2, 0, 3);
2796 strcpy(string_buffer
, "---");
2797 string_buffer
[3] = SYM_WH_KM_0
;
2798 string_buffer
[4] = SYM_WH_KM_1
;
2799 string_buffer
[5] = '\0';
2800 displayWrite(osdDisplayPort
, STATS_VALUE_X_POS
-3, y
, string_buffer
);
2806 displayCommitTransaction(osdDisplayPort
);
2807 displayResync(osdDisplayPort
);
2808 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME
);
2811 void osdInit(displayPort_t
*osdDisplayPortToUse
)
2813 if (!osdDisplayPortToUse
)
2816 BUILD_BUG_ON(OSD_POS_MAX
!= OSD_POS(31,31));
2818 osdDisplayPort
= osdDisplayPortToUse
;
2821 cmsDisplayPortRegister(osdDisplayPort
);
2824 armState
= ARMING_FLAG(ARMED
);
2825 osdCompleteAsyncInitialization();
2828 static void osdResetStats(void)
2830 stats
.max_current
= 0;
2831 stats
.max_power
= 0;
2832 stats
.max_speed
= 0;
2833 stats
.min_voltage
= 5000;
2834 stats
.min_rssi
= 99;
2835 stats
.max_altitude
= 0;
2838 static void osdUpdateStats(void)
2842 if (feature(FEATURE_GPS
)) {
2843 value
= osdGet3DSpeed();
2844 if (stats
.max_speed
< value
)
2845 stats
.max_speed
= value
;
2847 if (stats
.max_distance
< GPS_distanceToHome
)
2848 stats
.max_distance
= GPS_distanceToHome
;
2851 value
= getBatteryVoltage();
2852 if (stats
.min_voltage
> value
)
2853 stats
.min_voltage
= value
;
2855 value
= abs(getAmperage() / 100);
2856 if (stats
.max_current
< value
)
2857 stats
.max_current
= value
;
2859 value
= abs(getPower() / 100);
2860 if (stats
.max_power
< value
)
2861 stats
.max_power
= value
;
2863 value
= osdConvertRSSI();
2864 if (stats
.min_rssi
> value
)
2865 stats
.min_rssi
= value
;
2867 stats
.max_altitude
= MAX(stats
.max_altitude
, osdGetAltitude());
2870 /* Attention: NTSC screen only has 12 fully visible lines - it is FULL now! */
2871 static void osdShowStats(void)
2873 const char * disarmReasonStr
[DISARM_REASON_COUNT
] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "KILLSW", "FAILSAFE", "NAV SYS" };
2874 uint8_t top
= 1; /* first fully visible line */
2875 const uint8_t statNameX
= 1;
2876 const uint8_t statValuesX
= 20;
2879 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
2880 displayClearScreen(osdDisplayPort
);
2881 if (osdDisplayIsPAL())
2882 displayWrite(osdDisplayPort
, statNameX
, top
++, " --- STATS ---");
2884 if (STATE(GPS_FIX
)) {
2885 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX SPEED :");
2886 osdFormatVelocityStr(buff
, stats
.max_speed
, true);
2887 osdLeftAlignString(buff
);
2888 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2890 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX DISTANCE :");
2891 osdFormatDistanceStr(buff
, stats
.max_distance
*100);
2892 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2894 displayWrite(osdDisplayPort
, statNameX
, top
, "TRAVELED DISTANCE:");
2895 osdFormatDistanceStr(buff
, getTotalTravelDistance());
2896 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2899 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX ALTITUDE :");
2900 osdFormatAltitudeStr(buff
, stats
.max_altitude
);
2901 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2903 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN BATTERY VOLT :");
2904 osdFormatCentiNumber(buff
, stats
.min_voltage
, 0, osdConfig()->main_voltage_decimals
, 0, osdConfig()->main_voltage_decimals
+ 2);
2906 osdLeftAlignString(buff
);
2907 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2909 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN RSSI :");
2910 itoa(stats
.min_rssi
, buff
, 10);
2912 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2914 if (feature(FEATURE_CURRENT_METER
)) {
2915 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX CURRENT :");
2916 itoa(stats
.max_current
, buff
, 10);
2918 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2920 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX POWER :");
2921 itoa(stats
.max_power
, buff
, 10);
2923 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2925 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
) {
2926 displayWrite(osdDisplayPort
, statNameX
, top
, "USED MAH :");
2927 tfp_sprintf(buff
, "%d%c", (int)getMAhDrawn(), SYM_MAH
);
2929 displayWrite(osdDisplayPort
, statNameX
, top
, "USED WH :");
2930 osdFormatCentiNumber(buff
, getMWhDrawn() / 10, 0, 2, 0, 3);
2931 strcat(buff
, "\xAB"); // SYM_WH
2933 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2935 int32_t totalDistance
= getTotalTravelDistance();
2936 if (totalDistance
> 0) {
2937 displayWrite(osdDisplayPort
, statNameX
, top
, "AVG EFFICIENCY :");
2938 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
)
2939 tfp_sprintf(buff
, "%d%c%c", (int)(getMAhDrawn() * 100000 / totalDistance
),
2940 SYM_MAH_KM_0
, SYM_MAH_KM_1
);
2942 osdFormatCentiNumber(buff
, getMWhDrawn() * 10000 / totalDistance
, 0, 2, 0, 3);
2943 buff
[3] = SYM_WH_KM_0
;
2944 buff
[4] = SYM_WH_KM_1
;
2947 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2951 displayWrite(osdDisplayPort
, statNameX
, top
, "FLY TIME :");
2952 uint16_t flySeconds
= getFlightTime();
2953 uint16_t flyMinutes
= flySeconds
/ 60;
2955 uint16_t flyHours
= flyMinutes
/ 60;
2957 tfp_sprintf(buff
, "%02u:%02u:%02u", flyHours
, flyMinutes
, flySeconds
);
2958 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2960 const float max_gforce
= accGetMeasuredMaxG();
2961 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX G-FORCE :");
2962 osdFormatCentiNumber(buff
, max_gforce
* 100, 0, 2, 0, 3);
2963 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
2965 const acc_extremes_t
*acc_extremes
= accGetMeasuredExtremes();
2966 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN/MAX Z G-FORCE:");
2967 osdFormatCentiNumber(buff
, acc_extremes
[Z
].min
* 100, 0, 2, 0, 4);
2969 displayWrite(osdDisplayPort
, statValuesX
, top
, buff
);
2970 osdFormatCentiNumber(buff
, acc_extremes
[Z
].max
* 100, 0, 2, 0, 3);
2971 displayWrite(osdDisplayPort
, statValuesX
+ 5, top
++, buff
);
2973 displayWrite(osdDisplayPort
, statNameX
, top
, "DISARMED BY :");
2974 displayWrite(osdDisplayPort
, statValuesX
, top
++, disarmReasonStr
[getDisarmReason()]);
2975 displayCommitTransaction(osdDisplayPort
);
2978 // called when motors armed
2979 static void osdShowArmed(void)
2982 char buf
[MAX(32, FORMATTED_DATE_TIME_BUFSIZE
)];
2983 char craftNameBuf
[MAX_NAME_LENGTH
];
2986 // We need 10 visible rows
2987 uint8_t y
= MIN((osdDisplayPort
->rows
/ 2) - 1, osdDisplayPort
->rows
- 10 - 1);
2989 displayClearScreen(osdDisplayPort
);
2990 displayWrite(osdDisplayPort
, 12, y
, "ARMED");
2993 if (strlen(systemConfig()->name
) > 0) {
2994 osdFormatCraftName(craftNameBuf
);
2995 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(systemConfig() -> name
)) / 2, y
, craftNameBuf
);
2999 #if defined(USE_GPS)
3000 if (feature(FEATURE_GPS
)) {
3001 if (STATE(GPS_FIX_HOME
)) {
3002 osdFormatCoordinate(buf
, SYM_LAT
, GPS_home
.lat
);
3003 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, y
, buf
);
3004 osdFormatCoordinate(buf
, SYM_LON
, GPS_home
.lon
);
3005 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, y
+ 1, buf
);
3006 int digits
= osdConfig()->plus_code_digits
;
3007 olc_encode(GPS_home
.lat
, GPS_home
.lon
, digits
, buf
, sizeof(buf
));
3008 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, y
+ 2, buf
);
3010 #if defined (USE_SAFE_HOME)
3011 if (isSafeHomeInUse()) {
3012 textAttributes_t elemAttr
= _TEXT_ATTRIBUTES_BLINK_BIT
;
3013 char buf2
[12]; // format the distance first
3014 osdFormatDistanceStr(buf2
, safehome_distance
);
3015 tfp_sprintf(buf
, "%c - %s -> SAFEHOME %u", SYM_HOME
, buf2
, safehome_used
);
3016 // write this message above the ARMED message to make it obvious
3017 displayWriteWithAttr(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, y
- 8, buf
, elemAttr
);
3021 strcpy(buf
, "!NO HOME POSITION!");
3022 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, y
, buf
);
3028 if (rtcGetDateTime(&dt
)) {
3029 dateTimeFormatLocal(buf
, &dt
);
3030 dateTimeSplitFormatted(buf
, &date
, &time
);
3032 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(date
)) / 2, y
, date
);
3033 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(time
)) / 2, y
+ 1, time
);
3037 static void osdFilterData(timeUs_t currentTimeUs
) {
3038 static timeUs_t lastRefresh
= 0;
3039 float refresh_dT
= cmpTimeUs(currentTimeUs
, lastRefresh
) * 1e-6;
3041 GForce
= sqrtf(vectorNormSquared(&imuMeasuredAccelBF
)) / GRAVITY_MSS
;
3042 for (uint8_t axis
= 0; axis
< XYZ_AXIS_COUNT
; ++axis
) GForceAxis
[axis
] = imuMeasuredAccelBF
.v
[axis
] / GRAVITY_MSS
;
3045 GForce
= pt1FilterApply3(&GForceFilter
, GForce
, refresh_dT
);
3046 for (uint8_t axis
= 0; axis
< XYZ_AXIS_COUNT
; ++axis
) pt1FilterApply3(GForceFilterAxis
+ axis
, GForceAxis
[axis
], refresh_dT
);
3048 pt1FilterInitRC(&GForceFilter
, GFORCE_FILTER_TC
, 0);
3049 pt1FilterReset(&GForceFilter
, GForce
);
3051 for (uint8_t axis
= 0; axis
< XYZ_AXIS_COUNT
; ++axis
) {
3052 pt1FilterInitRC(GForceFilterAxis
+ axis
, GFORCE_FILTER_TC
, 0);
3053 pt1FilterReset(GForceFilterAxis
+ axis
, GForceAxis
[axis
]);
3057 lastRefresh
= currentTimeUs
;
3060 static void osdRefresh(timeUs_t currentTimeUs
)
3062 osdFilterData(currentTimeUs
);
3065 if (IS_RC_MODE_ACTIVE(BOXOSD
) && (!cmsInMenu
) && !(osdConfig()->osd_failsafe_switch_layout
&& FLIGHT_MODE(FAILSAFE_MODE
))) {
3067 if (IS_RC_MODE_ACTIVE(BOXOSD
) && !(osdConfig()->osd_failsafe_switch_layout
&& FLIGHT_MODE(FAILSAFE_MODE
))) {
3069 displayClearScreen(osdDisplayPort
);
3070 armState
= ARMING_FLAG(ARMED
);
3074 // detect arm/disarm
3075 if (armState
!= ARMING_FLAG(ARMED
)) {
3076 if (ARMING_FLAG(ARMED
)) {
3078 osdShowArmed(); // reset statistic etc
3079 uint32_t delay
= ARMED_SCREEN_DISPLAY_TIME
;
3080 #if defined(USE_SAFE_HOME)
3081 if (isSafeHomeInUse())
3084 osdSetNextRefreshIn(delay
);
3086 osdShowStats(); // show statistic
3087 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME
);
3090 armState
= ARMING_FLAG(ARMED
);
3093 if (resumeRefreshAt
) {
3094 // If we already reached he time for the next refresh,
3095 // or THR is high or PITCH is high, resume refreshing.
3096 // Clear the screen first to erase other elements which
3097 // might have been drawn while the OSD wasn't refreshing.
3099 if (!DELAYED_REFRESH_RESUME_COMMAND
)
3100 refreshWaitForResumeCmdRelease
= false;
3102 if ((currentTimeUs
> resumeRefreshAt
) || ((!refreshWaitForResumeCmdRelease
) && DELAYED_REFRESH_RESUME_COMMAND
)) {
3103 displayClearScreen(osdDisplayPort
);
3104 resumeRefreshAt
= 0;
3106 displayHeartbeat(osdDisplayPort
);
3112 if (!displayIsGrabbed(osdDisplayPort
)) {
3113 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
3115 displayClearScreen(osdDisplayPort
);
3118 osdDrawNextElement();
3119 displayHeartbeat(osdDisplayPort
);
3120 displayCommitTransaction(osdDisplayPort
);
3121 #ifdef OSD_CALLS_CMS
3123 cmsUpdate(currentTimeUs
);
3130 * Called periodically by the scheduler
3132 void osdUpdate(timeUs_t currentTimeUs
)
3134 static uint32_t counter
= 0;
3136 // don't touch buffers if DMA transaction is in progress
3137 if (displayIsTransferInProgress(osdDisplayPort
)) {
3141 if (!osdDisplayIsReady
) {
3142 osdCompleteAsyncInitialization();
3146 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
3147 // Check if the layout has changed. Higher numbered
3148 // boxes take priority.
3149 unsigned activeLayout
;
3150 if (layoutOverride
>= 0) {
3151 activeLayout
= layoutOverride
;
3152 // Check for timed override, it will go into effect on
3153 // the next OSD iteration
3154 if (layoutOverrideUntil
> 0 && millis() > layoutOverrideUntil
) {
3155 layoutOverrideUntil
= 0;
3156 layoutOverride
= -1;
3158 } else if (osdConfig()->osd_failsafe_switch_layout
&& FLIGHT_MODE(FAILSAFE_MODE
)) {
3161 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
3162 if (IS_RC_MODE_ACTIVE(BOXOSDALT3
))
3166 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
3167 if (IS_RC_MODE_ACTIVE(BOXOSDALT2
))
3171 if (IS_RC_MODE_ACTIVE(BOXOSDALT1
))
3174 #ifdef USE_PROGRAMMING_FRAMEWORK
3175 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT
))
3176 activeLayout
= constrain(logicConditionValuesByType
[LOGIC_CONDITION_SET_OSD_LAYOUT
], 0, OSD_ALTERNATE_LAYOUT_COUNT
);
3181 if (currentLayout
!= activeLayout
) {
3182 currentLayout
= activeLayout
;
3183 osdStartFullRedraw();
3187 #define DRAW_FREQ_DENOM 4
3188 #define STATS_FREQ_DENOM 50
3191 if ((counter
% STATS_FREQ_DENOM
) == 0) {
3195 if ((counter
& DRAW_FREQ_DENOM
) == 0) {
3196 // redraw values in buffer
3197 osdRefresh(currentTimeUs
);
3199 // rest of time redraw screen
3200 displayDrawScreen(osdDisplayPort
);
3204 // do not allow ARM if we are in menu
3205 if (displayIsGrabbed(osdDisplayPort
)) {
3206 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU
);
3208 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU
);
3213 void osdStartFullRedraw(void)
3218 void osdOverrideLayout(int layout
, timeMs_t duration
)
3220 layoutOverride
= constrain(layout
, -1, ARRAYLEN(osdLayoutsConfig()->item_pos
) - 1);
3221 if (layoutOverride
>= 0 && duration
> 0) {
3222 layoutOverrideUntil
= millis() + duration
;
3224 layoutOverrideUntil
= 0;
3228 int osdGetActiveLayout(bool *overridden
)
3231 *overridden
= layoutOverride
>= 0;
3233 return currentLayout
;
3236 bool osdItemIsFixed(osd_items_e item
)
3238 return item
== OSD_CROSSHAIRS
||
3239 item
== OSD_ARTIFICIAL_HORIZON
||
3240 item
== OSD_HORIZON_SIDEBARS
;
3243 displayPort_t
*osdGetDisplayPort(void)
3245 return osdDisplayPort
;
3248 displayCanvas_t
*osdGetDisplayPortCanvas(void)
3250 #if defined(USE_CANVAS)
3251 if (osdDisplayHasCanvas
) {