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
37 #include "build/debug.h"
38 #include "build/version.h"
41 #include "cms/cms_types.h"
42 #include "cms/cms_menu_osd.h"
44 #include "common/axis.h"
45 #include "common/constants.h"
46 #include "common/filter.h"
47 #include "common/log.h"
48 #include "common/olc.h"
49 #include "common/printf.h"
50 #include "common/string_light.h"
51 #include "common/time.h"
52 #include "common/typeconversion.h"
53 #include "common/utils.h"
55 #include "config/feature.h"
56 #include "config/parameter_group.h"
57 #include "config/parameter_group_ids.h"
59 #include "drivers/display.h"
60 #include "drivers/display_canvas.h"
61 #include "drivers/display_font_metadata.h"
62 #include "drivers/osd_symbols.h"
63 #include "drivers/time.h"
64 #include "drivers/vtx_common.h"
65 #include "drivers/gimbal_common.h"
68 #include "io/flashfs.h"
71 #include "io/osd_common.h"
72 #include "io/osd_hud.h"
73 #include "io/osd_utils.h"
74 #include "io/displayport_msp_dji_compat.h"
76 #include "io/vtx_string.h"
78 #include "io/osd/custom_elements.h"
80 #include "fc/config.h"
81 #include "fc/controlrate_profile.h"
82 #include "fc/fc_core.h"
83 #include "fc/fc_tasks.h"
84 #include "fc/multifunction.h"
85 #include "fc/rc_adjustments.h"
86 #include "fc/rc_controls.h"
87 #include "fc/rc_modes.h"
88 #include "fc/runtime_config.h"
89 #include "fc/settings.h"
91 #include "flight/imu.h"
92 #include "flight/mixer.h"
93 #include "flight/pid.h"
94 #include "flight/power_limits.h"
95 #include "flight/rth_estimator.h"
96 #include "flight/servos.h"
97 #include "flight/wind_estimator.h"
99 #include "navigation/navigation.h"
100 #include "navigation/navigation_private.h"
103 #include "rx/msp_override.h"
105 #include "sensors/acceleration.h"
106 #include "sensors/battery.h"
107 #include "sensors/boardalignment.h"
108 #include "sensors/compass.h"
109 #include "sensors/diagnostics.h"
110 #include "sensors/sensors.h"
111 #include "sensors/pitotmeter.h"
112 #include "sensors/temperature.h"
113 #include "sensors/esc_sensor.h"
114 #include "sensors/rangefinder.h"
116 #include "programming/logic_condition.h"
117 #include "programming/global_variables.h"
120 #include "blackbox/blackbox_io.h"
123 #ifdef USE_HARDWARE_REVISION_DETECTION
124 #include "hardware_revision.h"
127 #define VIDEO_BUFFER_CHARS_PAL 480
128 #define VIDEO_BUFFER_CHARS_HDZERO 900
129 #define VIDEO_BUFFER_CHARS_DJIWTF 1320
131 #define GFORCE_FILTER_TC 0.2
133 #define OSD_STATS_SINGLE_PAGE_MIN_ROWS 18
134 #define IS_HI(X) (rxGetChannelValue(X) > 1750)
135 #define IS_LO(X) (rxGetChannelValue(X) < 1250)
136 #define IS_MID(X) (rxGetChannelValue(X) > 1250 && rxGetChannelValue(X) < 1750)
138 #define OSD_RESUME_UPDATES_STICK_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
139 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
140 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
142 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
143 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
145 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
147 // Adjust OSD_MESSAGE's default position when
148 // changing OSD_MESSAGE_LENGTH
149 #define OSD_MESSAGE_LENGTH 28
150 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
151 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
152 // Wrap all string constants intenteded for display as messages with
153 // this macro to ensure compile time length validation.
154 #define OSD_MESSAGE_STR(x) ({ \
155 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
159 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
161 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
162 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
164 #define OSD_MIN_FONT_VERSION 3
166 static timeMs_t linearDescentMessageMs
= 0;
167 static timeMs_t notify_settings_saved
= 0;
168 static bool savingSettings
= false;
170 static unsigned currentLayout
= 0;
171 static int layoutOverride
= -1;
172 static bool hasExtendedFont
= false; // Wether the font supports characters > 256
173 static timeMs_t layoutOverrideUntil
= 0;
174 static pt1Filter_t GForceFilter
, GForceFilterAxis
[XYZ_AXIS_COUNT
];
175 static float GForce
, GForceAxis
[XYZ_AXIS_COUNT
];
177 typedef struct statistic_s
{
179 uint16_t max_3D_speed
;
180 uint16_t max_air_speed
;
181 uint16_t min_voltage
; // /100
185 int16_t min_lq
; // for CRSF
186 int16_t min_rssi_dbm
; // for CRSF
187 int32_t max_altitude
;
188 uint32_t max_distance
;
191 int16_t max_esc_temp
;
192 int16_t min_esc_temp
;
193 int32_t flightStartMAh
;
194 int32_t flightStartMWh
;
197 static statistic_t stats
;
199 static timeUs_t resumeRefreshAt
= 0;
200 static bool refreshWaitForResumeCmdRelease
;
202 static bool fullRedraw
= false;
204 static uint8_t armState
;
206 static textAttributes_t
osdGetMultiFunctionMessage(char *buff
);
207 static uint8_t osdWarningsFlags
= 0;
209 typedef struct osdMapData_s
{
211 char referenceSymbol
;
214 static osdMapData_t osdMapData
;
216 static displayPort_t
*osdDisplayPort
;
217 static bool osdDisplayIsReady
= false;
218 #if defined(USE_CANVAS)
219 static displayCanvas_t osdCanvas
;
220 static bool osdDisplayHasCanvas
;
222 #define osdDisplayHasCanvas false
225 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
227 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t
, osdConfig
, PG_OSD_CONFIG
, 12);
228 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t
, osdLayoutsConfig
, PG_OSD_LAYOUTS_CONFIG
, 1);
230 void osdStartedSaveProcess(void) {
231 savingSettings
= true;
234 void osdShowEEPROMSavedNotification(void) {
235 savingSettings
= false;
236 notify_settings_saved
= millis() + 5000;
239 bool osdDisplayIsPAL(void)
241 return displayScreenSize(osdDisplayPort
) == VIDEO_BUFFER_CHARS_PAL
;
244 bool osdDisplayIsHD(void)
246 if (displayScreenSize(osdDisplayPort
) >= VIDEO_BUFFER_CHARS_HDZERO
)
253 bool osdIsNotMetric(void) {
254 return !(osdConfig()->units
== OSD_UNIT_METRIC
|| osdConfig()->units
== OSD_UNIT_METRIC_MPH
);
258 * Converts distance into a string based on the current unit system
259 * prefixed by a a symbol to indicate the unit used.
260 * @param dist Distance in centimeters
262 static void osdFormatDistanceSymbol(char *buff
, int32_t dist
, uint8_t decimals
)
264 uint8_t digits
= 3U; // Total number of digits (including decimal point)
265 uint8_t sym_index
= 3U; // Position (index) at buffer of units symbol
266 uint8_t symbol_m
= SYM_DIST_M
;
267 uint8_t symbol_km
= SYM_DIST_KM
;
268 uint8_t symbol_ft
= SYM_DIST_FT
;
269 uint8_t symbol_mi
= SYM_DIST_MI
;
270 uint8_t symbol_nm
= SYM_DIST_NM
;
272 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
273 if (isDJICompatibleVideoSystem(osdConfig())) {
274 // Add one digit so up no switch to scaled decimal occurs above 99
277 // Use altitude symbols on purpose, as it seems distance symbols are not defined in DJICOMPAT mode
278 symbol_m
= SYM_ALT_M
;
279 symbol_km
= SYM_ALT_KM
;
280 symbol_ft
= SYM_ALT_FT
;
286 switch ((osd_unit_e
)osdConfig()->units
) {
289 case OSD_UNIT_IMPERIAL
:
290 if (osdFormatCentiNumber(buff
, CENTIMETERS_TO_CENTIFEET(dist
), FEET_PER_MILE
, decimals
, 3, digits
, false)) {
291 buff
[sym_index
] = symbol_mi
;
293 buff
[sym_index
] = symbol_ft
;
295 buff
[sym_index
+ 1] = '\0';
297 case OSD_UNIT_METRIC_MPH
:
299 case OSD_UNIT_METRIC
:
300 if (osdFormatCentiNumber(buff
, dist
, METERS_PER_KILOMETER
, decimals
, 3, digits
, false)) {
301 buff
[sym_index
] = symbol_km
;
303 buff
[sym_index
] = symbol_m
;
305 buff
[sym_index
+ 1] = '\0';
308 if (osdFormatCentiNumber(buff
, CENTIMETERS_TO_CENTIFEET(dist
), (uint32_t)FEET_PER_NAUTICALMILE
, decimals
, 3, digits
, false)) {
309 buff
[sym_index
] = symbol_nm
;
311 buff
[sym_index
] = symbol_ft
;
313 buff
[sym_index
+ 1] = '\0';
319 * Converts distance into a string based on the current unit system.
320 * @param dist Distance in centimeters
322 static void osdFormatDistanceStr(char *buff
, int32_t dist
)
325 switch ((osd_unit_e
)osdConfig()->units
) {
328 case OSD_UNIT_IMPERIAL
:
329 centifeet
= CENTIMETERS_TO_CENTIFEET(dist
);
330 if (abs(centifeet
) < FEET_PER_MILE
* 100 / 2) {
331 // Show feet when dist < 0.5mi
332 tfp_sprintf(buff
, "%d%c", (int)(centifeet
/ 100), SYM_FT
);
334 // Show miles when dist >= 0.5mi
335 tfp_sprintf(buff
, "%d.%02d%c", (int)(centifeet
/ (100*FEET_PER_MILE
)),
336 (abs(centifeet
) % (100 * FEET_PER_MILE
)) / FEET_PER_MILE
, SYM_MI
);
339 case OSD_UNIT_METRIC_MPH
:
341 case OSD_UNIT_METRIC
:
342 if (abs(dist
) < METERS_PER_KILOMETER
* 100) {
343 // Show meters when dist < 1km
344 tfp_sprintf(buff
, "%d%c", (int)(dist
/ 100), SYM_M
);
346 // Show kilometers when dist >= 1km
347 tfp_sprintf(buff
, "%d.%02d%c", (int)(dist
/ (100*METERS_PER_KILOMETER
)),
348 (abs(dist
) % (100 * METERS_PER_KILOMETER
)) / METERS_PER_KILOMETER
, SYM_KM
);
352 centifeet
= CENTIMETERS_TO_CENTIFEET(dist
);
353 if (abs(centifeet
) < 100000) {
354 // Show feet when dist < 1000ft
355 tfp_sprintf(buff
, "%d%c", (int)(centifeet
/ 100), SYM_FT
);
357 // Show nautical miles when dist >= 1000ft
358 tfp_sprintf(buff
, "%d.%02d%c", (int)(centifeet
/ (100 * FEET_PER_NAUTICALMILE
)),
359 (int)((abs(centifeet
) % (int)(100 * FEET_PER_NAUTICALMILE
)) / FEET_PER_NAUTICALMILE
), SYM_NM
);
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_METRIC_MPH
:
376 case OSD_UNIT_IMPERIAL
:
377 return CMSEC_TO_CENTIMPH(vel
) / 100; // Convert to mph
378 case OSD_UNIT_METRIC
:
379 return CMSEC_TO_CENTIKPH(vel
) / 100; // Convert to kmh
381 return CMSEC_TO_CENTIKNOTS(vel
) / 100; // Convert to Knots
388 * Converts velocity into a string based on the current unit system.
389 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
390 * @param _3D is a 3D velocity
391 * @param _max is a maximum velocity
393 void osdFormatVelocityStr(char* buff
, int32_t vel
, bool _3D
, bool _max
)
395 switch ((osd_unit_e
)osdConfig()->units
) {
398 case OSD_UNIT_METRIC_MPH
:
400 case OSD_UNIT_IMPERIAL
:
402 tfp_sprintf(buff
, "%c%3d%c", SYM_MAX
, (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_MPH
: SYM_MPH
));
404 tfp_sprintf(buff
, "%3d%c", (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_MPH
: SYM_MPH
));
407 case OSD_UNIT_METRIC
:
409 tfp_sprintf(buff
, "%c%3d%c", SYM_MAX
, (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_KMH
: SYM_KMH
));
411 tfp_sprintf(buff
, "%3d%c", (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_KMH
: SYM_KMH
));
416 tfp_sprintf(buff
, "%c%3d%c", SYM_MAX
, (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_KT
: SYM_KT
));
418 tfp_sprintf(buff
, "%3d%c", (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_KT
: SYM_KT
));
425 * Returns the average velocity. This always uses stats, so can be called as an OSD element later if wanted, to show a real time average
427 static void osdGenerateAverageVelocityStr(char* buff
) {
428 uint32_t cmPerSec
= getTotalTravelDistance() / getFlightTime();
429 osdFormatVelocityStr(buff
, cmPerSec
, false, false);
433 * Converts wind speed into a string based on the current unit system, using
434 * always 3 digits and an additional character for the unit at the right. buff
435 * is null terminated.
436 * @param ws Raw wind speed in cm/s
438 #ifdef USE_WIND_ESTIMATOR
439 static void osdFormatWindSpeedStr(char *buff
, int32_t ws
, bool isValid
)
443 switch (osdConfig()->units
) {
446 case OSD_UNIT_METRIC_MPH
:
448 case OSD_UNIT_IMPERIAL
:
449 centivalue
= CMSEC_TO_CENTIMPH(ws
);
453 centivalue
= CMSEC_TO_CENTIKNOTS(ws
);
457 case OSD_UNIT_METRIC
:
458 if (osdConfig()->estimations_wind_mps
)
465 centivalue
= CMSEC_TO_CENTIKPH(ws
);
471 osdFormatCentiNumber(buff
, centivalue
, 0, 2, 0, 3, false);
473 if (!isValid
&& ((millis() / 1000) % 4 < 2))
482 * Converts altitude into a string based on the current unit system
483 * prefixed by a a symbol to indicate the unit used.
484 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
486 void osdFormatAltitudeSymbol(char *buff
, int32_t alt
)
488 uint8_t totalDigits
= 4U;
490 uint8_t symbolIndex
= 4U;
491 uint8_t symbolKFt
= SYM_ALT_KFT
;
498 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
499 if (isDJICompatibleVideoSystem(osdConfig())) {
503 symbolKFt
= SYM_ALT_FT
;
507 switch ((osd_unit_e
)osdConfig()->units
) {
512 case OSD_UNIT_IMPERIAL
:
513 if (osdFormatCentiNumber(buff
+ totalDigits
- digits
, CENTIMETERS_TO_CENTIFEET(alt
), 1000, 0, 2, digits
, false)) {
515 buff
[symbolIndex
++] = symbolKFt
;
518 buff
[symbolIndex
++] = SYM_ALT_FT
;
520 buff
[symbolIndex
] = '\0';
522 case OSD_UNIT_METRIC_MPH
:
524 case OSD_UNIT_METRIC
:
525 // alt is alredy in cm
526 if (osdFormatCentiNumber(buff
+ totalDigits
- digits
, alt
, 1000, 0, 2, digits
, false)) {
528 buff
[symbolIndex
++] = SYM_ALT_KM
;
531 buff
[symbolIndex
++] = SYM_ALT_M
;
533 buff
[symbolIndex
] = '\0';
539 * Converts altitude into a string based on the current unit system.
540 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
542 static void osdFormatAltitudeStr(char *buff
, int32_t alt
)
545 switch ((osd_unit_e
)osdConfig()->units
) {
550 case OSD_UNIT_IMPERIAL
:
551 value
= CENTIMETERS_TO_FEET(alt
);
552 tfp_sprintf(buff
, "%d%c", (int)value
, SYM_FT
);
554 case OSD_UNIT_METRIC_MPH
:
556 case OSD_UNIT_METRIC
:
557 value
= CENTIMETERS_TO_METERS(alt
);
558 tfp_sprintf(buff
, "%d%c", (int)value
, SYM_M
);
563 static void osdFormatTime(char *buff
, uint32_t seconds
, char sym_m
, char sym_h
)
565 uint32_t value
= seconds
;
567 // Maximum value we can show in minutes is 99 minutes and 59 seconds
568 if (seconds
> (99 * 60) + 59) {
570 value
= seconds
/ 60;
573 tfp_sprintf(buff
+ 1, "%02d:%02d", (int)(value
/ 60), (int)(value
% 60));
576 static inline void osdFormatOnTime(char *buff
)
578 osdFormatTime(buff
, micros() / 1000000, SYM_ON_M
, SYM_ON_H
);
581 static inline void osdFormatFlyTime(char *buff
, textAttributes_t
*attr
)
583 uint32_t seconds
= getFlightTime();
584 osdFormatTime(buff
, seconds
, SYM_FLY_M
, SYM_FLY_H
);
585 if (attr
&& osdConfig()->time_alarm
> 0) {
586 if (seconds
/ 60 >= osdConfig()->time_alarm
&& ARMING_FLAG(ARMED
)) {
587 TEXT_ATTRIBUTES_ADD_BLINK(*attr
);
593 * Trim whitespace from string.
594 * Used in Stats screen on lines with multiple values.
596 char *osdFormatTrimWhiteSpace(char *buff
)
600 // Trim leading spaces
601 while(isspace((unsigned char)*buff
)) buff
++;
607 // Trim trailing spaces
608 end
= buff
+ strlen(buff
) - 1;
609 while(end
> buff
&& isspace((unsigned char)*end
)) end
--;
611 // Write new null terminator character
618 * Converts RSSI into a % value used by the OSD.
620 static uint16_t osdConvertRSSI(void)
622 // change range to [0, 99]
623 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE
, 0, 99);
626 static uint16_t osdGetCrsfLQ(void)
628 int16_t statsLQ
= rxLinkStatistics
.uplinkLQ
;
629 int16_t scaledLQ
= scaleRange(constrain(statsLQ
, 0, 100), 0, 100, 170, 300);
630 int16_t displayedLQ
= 0;
631 switch (osdConfig()->crsf_lq_format
) {
632 case OSD_CRSF_LQ_TYPE1
:
633 displayedLQ
= statsLQ
;
635 case OSD_CRSF_LQ_TYPE2
:
636 displayedLQ
= statsLQ
;
638 case OSD_CRSF_LQ_TYPE3
:
639 displayedLQ
= rxLinkStatistics
.rfMode
>= 2 ? scaledLQ
: statsLQ
;
645 static int16_t osdGetCrsfdBm(void)
647 return rxLinkStatistics
.uplinkRSSI
;
650 * Displays a temperature postfixed with a symbol depending on the current unit system
651 * @param label to display
652 * @param valid true if measurement is valid
653 * @param temperature in deciDegrees Celcius
655 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
)
657 char buff
[TEMPERATURE_LABEL_LEN
+ 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN
+ 2];
658 textAttributes_t elemAttr
= valid
? TEXT_ATTRIBUTES_NONE
: _TEXT_ATTRIBUTES_BLINK_BIT
;
659 uint8_t valueXOffset
= 0;
664 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
667 #ifdef USE_TEMPERATURE_SENSOR
668 else if (label
[0] != '\0') {
669 uint8_t label_len
= strnlen(label
, TEMPERATURE_LABEL_LEN
);
670 memcpy(buff
, label
, label_len
);
671 memset(buff
+ label_len
, ' ', TEMPERATURE_LABEL_LEN
+ 1 - label_len
);
673 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
674 valueXOffset
= osdConfig()->temp_label_align
== OSD_ALIGN_LEFT
? 5 : label_len
+ 1;
682 if ((temperature
<= alarm_min
) || (temperature
>= alarm_max
)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
683 if (osdConfig()->units
== OSD_UNIT_IMPERIAL
) temperature
= temperature
* 9 / 5.0f
+ 320;
684 tfp_sprintf(buff
, "%3d", temperature
/ 10);
689 buff
[3] = osdConfig()->units
== OSD_UNIT_IMPERIAL
? SYM_TEMP_F
: SYM_TEMP_C
;
692 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ valueXOffset
, elemPosY
, buff
, elemAttr
);
695 #ifdef USE_TEMPERATURE_SENSOR
696 static void osdDisplayTemperatureSensor(uint8_t elemPosX
, uint8_t elemPosY
, uint8_t sensorIndex
)
699 const bool valid
= getSensorTemperature(sensorIndex
, &temperature
);
700 const tempSensorConfig_t
*sensorConfig
= tempSensorConfig(sensorIndex
);
701 uint16_t symbol
= sensorConfig
->osdSymbol
? SYM_TEMP_SENSOR_FIRST
+ sensorConfig
->osdSymbol
- 1 : 0;
702 osdDisplayTemperature(elemPosX
, elemPosY
, symbol
, sensorConfig
->label
, valid
, temperature
, sensorConfig
->alarm_min
, sensorConfig
->alarm_max
);
706 static void osdFormatCoordinate(char *buff
, char sym
, int32_t val
)
708 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
709 const int coordinateLength
= osdConfig()->coordinate_digits
+ 1;
712 int32_t integerPart
= val
/ GPS_DEGREES_DIVIDER
;
713 // Latitude maximum integer width is 3 (-90) while
714 // longitude maximum integer width is 4 (-180).
715 int integerDigits
= tfp_sprintf(buff
+ 1, (integerPart
== 0 && val
< 0) ? "-%d" : "%d", (int)integerPart
);
716 // We can show up to 7 digits in decimalPart.
717 int32_t decimalPart
= abs(val
% (int)GPS_DEGREES_DIVIDER
);
718 STATIC_ASSERT(GPS_DEGREES_DIVIDER
== 1e7
, adjust_max_decimal_digits
);
720 bool djiCompat
= false; // Assume DJICOMPAT mode is no enabled
722 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
723 if(isDJICompatibleVideoSystem(osdConfig())) {
729 decimalDigits
= tfp_sprintf(buff
+ 1 + integerDigits
, "%07d", (int)decimalPart
);
730 // Embbed the decimal separator
731 buff
[1 + integerDigits
- 1] += SYM_ZERO_HALF_TRAILING_DOT
- '0';
732 buff
[1 + integerDigits
] += SYM_ZERO_HALF_LEADING_DOT
- '0';
734 // DJICOMPAT mode enabled
735 decimalDigits
= tfp_sprintf(buff
+ 1 + integerDigits
, ".%06d", (int)decimalPart
);
737 // Fill up to coordinateLength with zeros
738 int total
= 1 + integerDigits
+ decimalDigits
;
739 while(total
< coordinateLength
) {
743 buff
[coordinateLength
] = '\0';
746 static void osdFormatCraftName(char *buff
)
748 if (strlen(systemConfig()->craftName
) == 0)
749 strcpy(buff
, "CRAFT_NAME");
751 for (int i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
752 buff
[i
] = sl_toupper((unsigned char)systemConfig()->craftName
[i
]);
753 if (systemConfig()->craftName
[i
] == 0)
759 void osdFormatPilotName(char *buff
)
761 if (strlen(systemConfig()->pilotName
) == 0)
762 strcpy(buff
, "PILOT_NAME");
764 for (int i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
765 buff
[i
] = sl_toupper((unsigned char)systemConfig()->pilotName
[i
]);
766 if (systemConfig()->pilotName
[i
] == 0)
772 static const char * osdArmingDisabledReasonMessage(void)
774 const char *message
= NULL
;
775 static char messageBuf
[OSD_MESSAGE_LENGTH
+1];
777 switch (isArmingDisabledReason()) {
778 case ARMING_DISABLED_FAILSAFE_SYSTEM
:
779 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
780 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING
) {
781 if (failsafeIsReceivingRxData()) {
782 // reminder to disarm to exit FAILSAFE_RX_LOSS_MONITORING once timeout period ends
783 if (IS_RC_MODE_ACTIVE(BOXARM
)) {
784 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF
);
787 // Not receiving RX data
788 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST
);
791 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS
);
792 case ARMING_DISABLED_NOT_LEVEL
:
793 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL
);
794 case ARMING_DISABLED_SENSORS_CALIBRATING
:
795 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL
);
796 case ARMING_DISABLED_SYSTEM_OVERLOADED
:
797 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED
);
798 case ARMING_DISABLED_NAVIGATION_UNSAFE
:
799 // Check the exact reason
800 switch (navigationIsBlockingArming(NULL
)) {
802 case NAV_ARMING_BLOCKER_NONE
:
804 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX
:
805 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX
);
806 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE
:
807 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST
);
808 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR
:
809 osdFormatDistanceSymbol(buf
, distanceToFirstWP(), 0);
810 tfp_sprintf(messageBuf
, "FIRST WP TOO FAR (%s)", buf
);
811 return message
= messageBuf
;
812 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR
:
813 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG
);
816 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED
:
817 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL
);
818 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED
:
819 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL
);
820 case ARMING_DISABLED_ARM_SWITCH
:
821 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST
);
822 case ARMING_DISABLED_HARDWARE_FAILURE
:
824 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
825 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE
);
827 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
828 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL
);
830 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
831 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL
);
833 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
834 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL
);
836 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
837 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL
);
839 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
840 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL
);
842 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
843 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL
);
846 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL
);
847 case ARMING_DISABLED_BOXFAILSAFE
:
848 return OSD_MESSAGE_STR(OSD_MSG_FS_EN
);
849 case ARMING_DISABLED_RC_LINK
:
850 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK
);
851 case ARMING_DISABLED_THROTTLE
:
852 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW
);
853 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED
:
854 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER
);
855 case ARMING_DISABLED_SERVO_AUTOTRIM
:
856 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE
);
857 case ARMING_DISABLED_OOM
:
858 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY
);
859 case ARMING_DISABLED_INVALID_SETTING
:
860 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING
);
861 case ARMING_DISABLED_CLI
:
862 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE
);
863 case ARMING_DISABLED_PWM_OUTPUT_ERROR
:
864 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR
);
865 case ARMING_DISABLED_NO_PREARM
:
866 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM
);
867 case ARMING_DISABLED_DSHOT_BEEPER
:
868 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER
);
869 // Cases without message
870 case ARMING_DISABLED_LANDING_DETECTED
:
872 case ARMING_DISABLED_CMS_MENU
:
874 case ARMING_DISABLED_OSD_MENU
:
876 case ARMING_DISABLED_ALL_FLAGS
:
880 case SIMULATOR_MODE_HITL
:
882 case SIMULATOR_MODE_SITL
:
890 static const char * osdFailsafePhaseMessage(void)
892 // See failsafe.h for each phase explanation
893 switch (failsafePhase()) {
894 case FAILSAFE_RETURN_TO_HOME
:
895 // XXX: Keep this in sync with OSD_FLYMODE.
896 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS
);
897 case FAILSAFE_LANDING
:
898 // This should be considered an emergengy landing
899 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS
);
900 case FAILSAFE_RX_LOSS_MONITORING
:
901 // Only reachable from FAILSAFE_LANDED, which performs
902 // a disarm. Since aircraft has been disarmed, we no
903 // longer show failsafe details.
905 case FAILSAFE_LANDED
:
906 // Very brief, disarms and transitions into
907 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
908 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
909 // so we'll show the user how to re-arm in when
910 // that flag is the reason to prevent arming.
912 case FAILSAFE_RX_LOSS_IDLE
:
913 // This only happens when user has chosen NONE as FS
914 // procedure. The recovery messages should be enough.
917 // Failsafe not active
919 case FAILSAFE_RX_LOSS_DETECTED
:
920 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
921 // or the FS procedure immediately.
923 case FAILSAFE_RX_LOSS_RECOVERED
:
930 static const char * osdFailsafeInfoMessage(void)
932 if (failsafeIsReceivingRxData() && !FLIGHT_MODE(NAV_FW_AUTOLAND
)) {
933 // User must move sticks to exit FS mode
934 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS
);
936 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST
);
939 #if defined(USE_SAFE_HOME)
940 static const char * divertingToSafehomeMessage(void)
942 #ifdef USE_FW_AUTOLAND
943 if (!posControl
.fwLandState
.landWp
&& (NAV_Status
.state
!= MW_NAV_STATE_HOVER_ABOVE_HOME
&& posControl
.safehomeState
.isApplied
)) {
945 if (NAV_Status
.state
!= MW_NAV_STATE_HOVER_ABOVE_HOME
&& posControl
.safehomeState
.isApplied
) {
947 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME
);
954 static const char * navigationStateMessage(void)
956 if (!posControl
.rthState
.rthLinearDescentActive
&& linearDescentMessageMs
!= 0)
957 linearDescentMessageMs
= 0;
959 switch (NAV_Status
.state
) {
960 case MW_NAV_STATE_NONE
:
962 case MW_NAV_STATE_RTH_START
:
963 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH
);
964 case MW_NAV_STATE_RTH_CLIMB
:
965 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB
);
966 case MW_NAV_STATE_RTH_ENROUTE
:
967 if (posControl
.flags
.rthTrackbackActive
) {
968 return OSD_MESSAGE_STR(OSD_MSG_RTH_TRACKBACK
);
970 if (posControl
.rthState
.rthLinearDescentActive
&& (linearDescentMessageMs
== 0 || linearDescentMessageMs
> millis())) {
971 if (linearDescentMessageMs
== 0)
972 linearDescentMessageMs
= millis() + 5000; // Show message for 5 seconds.
974 return OSD_MESSAGE_STR(OSD_MSG_RTH_LINEAR_DESCENT
);
976 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME
);
978 case MW_NAV_STATE_HOLD_INFINIT
:
979 // Used by HOLD flight modes. No information to add.
981 case MW_NAV_STATE_HOLD_TIMED
:
982 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
984 case MW_NAV_STATE_WP_ENROUTE
:
985 // "TO WP" + WP countdown added in osdGetSystemMessage
987 case MW_NAV_STATE_PROCESS_NEXT
:
988 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP
);
989 case MW_NAV_STATE_DO_JUMP
:
992 case MW_NAV_STATE_LAND_START
:
995 case MW_NAV_STATE_EMERGENCY_LANDING
:
996 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING
);
997 case MW_NAV_STATE_LAND_IN_PROGRESS
:
998 return OSD_MESSAGE_STR(OSD_MSG_LANDING
);
999 case MW_NAV_STATE_HOVER_ABOVE_HOME
:
1000 if (STATE(FIXED_WING_LEGACY
)) {
1001 #if defined(USE_SAFE_HOME)
1002 if (posControl
.safehomeState
.isApplied
) {
1003 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME
);
1006 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME
);
1008 return OSD_MESSAGE_STR(OSD_MSG_HOVERING
);
1009 case MW_NAV_STATE_LANDED
:
1010 return OSD_MESSAGE_STR(OSD_MSG_LANDED
);
1011 case MW_NAV_STATE_LAND_SETTLE
:
1013 // If there is a FS landing delay occurring. That is handled by the calling function.
1014 if (posControl
.landingDelay
> 0)
1017 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND
);
1019 case MW_NAV_STATE_LAND_START_DESCENT
:
1027 static void osdFormatMessage(char *buff
, size_t size
, const char *message
, bool isCenteredText
)
1029 // String is always filled with Blanks
1030 memset(buff
, SYM_BLANK
, size
);
1032 size_t messageLength
= strlen(message
);
1033 int rem
= isCenteredText
? MAX(0, (int)size
- (int)messageLength
) : 0;
1034 strncpy(buff
+ rem
/ 2, message
, MIN((int)size
- rem
/ 2, (int)messageLength
));
1036 // Ensure buff is zero terminated
1041 * Draws the battery symbol filled in accordingly to the
1042 * battery voltage to buff[0].
1044 static void osdFormatBatteryChargeSymbol(char *buff
)
1046 uint8_t p
= calculateBatteryPercentage();
1047 p
= (100 - p
) / 16.6;
1048 buff
[0] = SYM_BATT_FULL
+ p
;
1051 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t
*attr
)
1053 const batteryState_e batteryState
= getBatteryState();
1055 if (batteryState
== BATTERY_WARNING
|| batteryState
== BATTERY_CRITICAL
) {
1056 TEXT_ATTRIBUTES_ADD_BLINK(*attr
);
1060 void osdCrosshairPosition(uint8_t *x
, uint8_t *y
)
1062 *x
= osdDisplayPort
->cols
/ 2;
1063 *y
= osdDisplayPort
->rows
/ 2;
1064 *y
-= osdConfig()->horizon_offset
; // positive horizon_offset moves the HUD up, negative moves down
1068 * Check if this OSD layout is using scaled or unscaled throttle.
1069 * If both are used, it will default to scaled.
1071 bool osdUsingScaledThrottle(void)
1073 bool usingScaledThrottle
= OSD_VISIBLE(osdLayoutsConfig()->item_pos
[currentLayout
][OSD_SCALED_THROTTLE_POS
]);
1074 bool usingRCThrottle
= OSD_VISIBLE(osdLayoutsConfig()->item_pos
[currentLayout
][OSD_THROTTLE_POS
]);
1076 if (!usingScaledThrottle
&& !usingRCThrottle
)
1077 usingScaledThrottle
= true;
1079 return usingScaledThrottle
;
1083 * Formats throttle position prefixed by its symbol.
1084 * Shows unscaled or scaled (output to motor) throttle percentage
1086 static void osdFormatThrottlePosition(char *buff
, bool useScaled
, textAttributes_t
*elemAttr
)
1088 buff
[0] = SYM_BLANK
;
1090 if (navigationIsControllingThrottle()) {
1091 buff
[0] = SYM_AUTO_THR0
;
1092 buff
[1] = SYM_AUTO_THR1
;
1093 if (isFixedWingAutoThrottleManuallyIncreased()) {
1094 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr
);
1098 #ifdef USE_POWER_LIMITS
1099 if (powerLimiterIsLimiting()) {
1100 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr
);
1103 int8_t throttlePercent
= getThrottlePercent(useScaled
);
1104 if ((useScaled
&& throttlePercent
<= 0) || !ARMING_FLAG(ARMED
)) {
1105 const char* message
= ARMING_FLAG(ARMED
) ? (throttlePercent
== 0 && !ifMotorstopFeatureEnabled()) ? "IDLE" : "STOP" : "DARM";
1107 strcpy(buff
+ 1, message
);
1110 tfp_sprintf(buff
+ 2, "%3d", throttlePercent
);
1114 * Formats gvars prefixed by its number (0-indexed). If autoThr
1116 static void osdFormatGVar(char *buff
, uint8_t index
)
1119 buff
[1] = '0'+index
;
1121 #ifdef USE_PROGRAMMING_FRAMEWORK
1122 osdFormatCentiNumber(buff
+ 3, (int32_t)gvGet(index
)*(int32_t)100, 1, 0, 0, 5, false);
1126 #if defined(USE_ESC_SENSOR)
1127 static void osdFormatRpm(char *buff
, uint32_t rpm
)
1131 if ( digitCount(rpm
) > osdConfig()->esc_rpm_precision
) {
1132 uint8_t rpmMaxDecimals
= (osdConfig()->esc_rpm_precision
- 3);
1133 osdFormatCentiNumber(buff
+ 1, rpm
/ 10, 0, rpmMaxDecimals
, rpmMaxDecimals
, osdConfig()->esc_rpm_precision
-1, false);
1134 buff
[osdConfig()->esc_rpm_precision
] = 'K';
1135 buff
[osdConfig()->esc_rpm_precision
+1] = '\0';
1138 switch(osdConfig()->esc_rpm_precision
) {
1140 tfp_sprintf(buff
+ 1, "%6lu", rpm
);
1143 tfp_sprintf(buff
+ 1, "%5lu", rpm
);
1146 tfp_sprintf(buff
+ 1, "%4lu", rpm
);
1150 tfp_sprintf(buff
+ 1, "%3lu", rpm
);
1158 uint8_t buffPos
= 1;
1159 while (buffPos
<=( osdConfig()->esc_rpm_precision
)) {
1160 strcpy(buff
+ buffPos
++, "-");
1166 int32_t osdGetAltitude(void)
1168 return getEstimatedActualPosition(Z
);
1171 static inline int32_t osdGetAltitudeMsl(void)
1173 return getEstimatedActualPosition(Z
) + posControl
.gpsOrigin
.alt
;
1176 uint16_t osdGetRemainingGlideTime(void) {
1177 float value
= getEstimatedActualVelocity(Z
);
1178 static pt1Filter_t glideTimeFilterState
;
1179 const timeMs_t curTimeMs
= millis();
1180 static timeMs_t glideTimeUpdatedMs
;
1182 value
= pt1FilterApply4(&glideTimeFilterState
, isnormal(value
) ? value
: 0, 0.5, MS2S(curTimeMs
- glideTimeUpdatedMs
));
1183 glideTimeUpdatedMs
= curTimeMs
;
1186 value
= osdGetAltitude() / abs((int)value
);
1191 return (uint16_t)roundf(value
);
1194 static bool osdIsHeadingValid(void)
1196 return isImuHeadingValid();
1199 int16_t osdGetHeading(void)
1201 return attitude
.values
.yaw
;
1204 int16_t osdGetPanServoOffset(void)
1206 int8_t servoIndex
= osdConfig()->pan_servo_index
;
1207 int16_t servoMiddle
= servoParams(servoIndex
)->middle
;
1208 int16_t servoPosition
= servo
[servoIndex
];
1210 gimbalDevice_t
*dev
= gimbalCommonDevice();
1211 if (dev
&& gimbalCommonIsReady(dev
)) {
1212 servoPosition
= gimbalCommonGetPanPwm(dev
);
1213 servoMiddle
= PWM_RANGE_MIDDLE
+ gimbalConfig()->panTrim
;
1216 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition
- servoMiddle
) * osdConfig()->pan_servo_pwm2centideg
);
1219 // Returns a heading angle in degrees normalized to [0, 360).
1220 int osdGetHeadingAngle(int angle
)
1225 while (angle
>= 360) {
1231 #if defined(USE_GPS)
1233 /* Draws a map with the given symbol in the center and given point of interest
1234 * defined by its distance in meters and direction in degrees.
1235 * referenceHeading indicates the up direction in the map, in degrees, while
1236 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1237 * arrow to indicate the map reference to the user. The drawn argument is an
1238 * in-out used to store the last position where the craft was drawn to avoid
1239 * erasing all screen on each redraw.
1241 static void osdDrawMap(int referenceHeading
, uint16_t referenceSym
, uint16_t centerSym
,
1242 uint32_t poiDistance
, int16_t poiDirection
, uint16_t poiSymbol
,
1243 uint16_t *drawn
, uint32_t *usedScale
)
1245 // TODO: These need to be tested with several setups. We might
1246 // need to make them configurable.
1247 const int hMargin
= 5;
1248 const int vMargin
= 3;
1250 // TODO: Get this from the display driver?
1251 const int charWidth
= 12;
1252 const int charHeight
= 18;
1254 uint8_t minX
= hMargin
;
1255 uint8_t maxX
= osdDisplayPort
->cols
- 1 - hMargin
;
1256 uint8_t minY
= vMargin
;
1257 uint8_t maxY
= osdDisplayPort
->rows
- 1 - vMargin
;
1258 uint8_t midX
= osdDisplayPort
->cols
/ 2;
1259 uint8_t midY
= osdDisplayPort
->rows
/ 2;
1262 displayWriteChar(osdDisplayPort
, midX
, midY
, centerSym
);
1264 // First, erase the previous drawing.
1265 if (OSD_VISIBLE(*drawn
)) {
1266 displayWriteChar(osdDisplayPort
, OSD_X(*drawn
), OSD_Y(*drawn
), SYM_BLANK
);
1270 uint32_t initialScale
;
1271 const unsigned scaleMultiplier
= 2;
1272 // We try to reduce the scale when the POI will be around half the distance
1273 // between the center and the closers map edge, to avoid too much jumping
1274 const int scaleReductionMultiplier
= MIN(midX
- hMargin
, midY
- vMargin
) / 2;
1276 switch (osdConfig()->units
) {
1279 case OSD_UNIT_IMPERIAL
:
1280 initialScale
= 16; // 16m ~= 0.01miles
1283 initialScale
= 18; // 18m ~= 0.01 nautical miles
1286 case OSD_UNIT_METRIC_MPH
:
1288 case OSD_UNIT_METRIC
:
1289 initialScale
= 10; // 10m as initial scale
1293 // Try to keep the same scale when getting closer until we draw over the center point
1294 uint32_t scale
= initialScale
;
1297 if (scale
> initialScale
&& poiDistance
< *usedScale
* scaleReductionMultiplier
) {
1298 scale
/= scaleMultiplier
;
1303 #ifdef USE_GPS_FIX_ESTIMATION
1304 || STATE(GPS_ESTIMATED_FIX
)
1308 int directionToPoi
= osdGetHeadingAngle(poiDirection
- referenceHeading
);
1309 float poiAngle
= DEGREES_TO_RADIANS(directionToPoi
);
1310 float poiSin
= sin_approx(poiAngle
);
1311 float poiCos
= cos_approx(poiAngle
);
1313 // Now start looking for a valid scale that lets us draw everything
1315 for (ii
= 0; ii
< 50; ii
++) {
1316 // Calculate location of the aircraft in map
1317 int points
= poiDistance
/ ((float)scale
/ charHeight
);
1319 float pointsX
= points
* poiSin
;
1320 int poiX
= midX
- roundf(pointsX
/ charWidth
);
1321 if (poiX
< minX
|| poiX
> maxX
) {
1322 scale
*= scaleMultiplier
;
1326 float pointsY
= points
* poiCos
;
1327 int poiY
= midY
+ roundf(pointsY
/ charHeight
);
1328 if (poiY
< minY
|| poiY
> maxY
) {
1329 scale
*= scaleMultiplier
;
1333 if (poiX
== midX
&& poiY
== midY
) {
1334 // We're over the map center symbol, so we would be drawing
1335 // over it even if we increased the scale. Alternate between
1336 // drawing the center symbol or drawing the POI.
1337 if (centerSym
!= SYM_BLANK
&& OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1343 if (displayReadCharWithAttr(osdDisplayPort
, poiX
, poiY
, &c
, NULL
) && c
!= SYM_BLANK
) {
1344 // Something else written here, increase scale. If the display doesn't support reading
1345 // back characters, we assume there's nothing.
1347 // If we're close to the center, decrease scale. Otherwise increase it.
1348 uint8_t centerDeltaX
= (maxX
- minX
) / (scaleMultiplier
* 2);
1349 uint8_t centerDeltaY
= (maxY
- minY
) / (scaleMultiplier
* 2);
1350 if (poiX
>= midX
- centerDeltaX
&& poiX
<= midX
+ centerDeltaX
&&
1351 poiY
>= midY
- centerDeltaY
&& poiY
<= midY
+ centerDeltaY
&&
1352 scale
> scaleMultiplier
) {
1354 scale
/= scaleMultiplier
;
1356 scale
*= scaleMultiplier
;
1362 // Draw the point on the map
1363 if (poiSymbol
== SYM_ARROW_UP
) {
1364 // Drawing aircraft, rotate
1365 int mapHeading
= osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading
);
1366 poiSymbol
+= mapHeading
* 2 / 45;
1368 displayWriteChar(osdDisplayPort
, poiX
, poiY
, poiSymbol
);
1370 // Update saved location
1371 *drawn
= OSD_POS(poiX
, poiY
) | OSD_VISIBLE_FLAG
;
1378 // Update global map data for scale and reference
1379 osdMapData
.scale
= scale
;
1380 osdMapData
.referenceSymbol
= referenceSym
;
1383 /* Draws a map with the home in the center and the craft moving around.
1384 * See osdDrawMap() for reference.
1386 static void osdDrawHomeMap(int referenceHeading
, uint8_t referenceSym
, uint16_t *drawn
, uint32_t *usedScale
)
1388 osdDrawMap(referenceHeading
, referenceSym
, SYM_HOME
, GPS_distanceToHome
, GPS_directionToHome
, SYM_ARROW_UP
, drawn
, usedScale
);
1391 /* Draws a map with the aircraft in the center and the home moving around.
1392 * See osdDrawMap() for reference.
1394 static void osdDrawRadar(uint16_t *drawn
, uint32_t *usedScale
)
1396 int16_t reference
= DECIDEGREES_TO_DEGREES(osdGetHeading());
1397 int16_t poiDirection
= osdGetHeadingAngle(GPS_directionToHome
+ 180);
1398 osdDrawMap(reference
, 0, SYM_ARROW_UP
, GPS_distanceToHome
, poiDirection
, SYM_HOME
, drawn
, usedScale
);
1401 static uint16_t crc_accumulate(uint8_t data
, uint16_t crcAccum
)
1404 tmp
= data
^ (uint8_t)(crcAccum
& 0xff);
1406 crcAccum
= (crcAccum
>> 8) ^ (tmp
<< 8) ^ (tmp
<< 3) ^ (tmp
>> 4);
1411 static void osdDisplayTelemetry(void)
1414 uint16_t trk_crc
= 0;
1415 char trk_buffer
[31];
1416 static int16_t trk_elevation
= 127;
1417 static uint16_t trk_bearing
= 0;
1419 if (ARMING_FLAG(ARMED
)) {
1421 #ifdef USE_GPS_FIX_ESTIMATION
1422 || STATE(GPS_ESTIMATED_FIX
)
1425 if (GPS_distanceToHome
> 5) {
1426 trk_bearing
= GPS_directionToHome
;
1427 trk_bearing
+= 360 + 180;
1429 int32_t alt
= CENTIMETERS_TO_METERS(osdGetAltitude());
1430 float at
= atan2(alt
, GPS_distanceToHome
);
1431 trk_elevation
= at
* 57.2957795f
; // 57.2957795 = 1 rad
1432 trk_elevation
+= 37; // because elevation in telemetry should be from -37 to 90
1433 if (trk_elevation
< 0) {
1440 trk_elevation
= 127;
1444 trk_data
= 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1445 trk_data
= trk_data
| (uint32_t)(0x7F & trk_elevation
) << 1; // bits 1-7 - elevation angle to target. NOTE number is abused. constrained value of -37 to 90 sent as 0 to 127.
1446 trk_data
= trk_data
| (uint32_t)trk_bearing
<< 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1447 trk_crc
= crc_accumulate(0xFF & trk_data
, trk_crc
); // CRC First Byte bits 0-7
1448 trk_crc
= crc_accumulate(0xFF & trk_bearing
, trk_crc
); // CRC Second Byte bits 8-15
1449 trk_crc
= crc_accumulate(trk_bearing
>> 8, trk_crc
); // CRC Third Byte bits 16-17
1450 trk_data
= trk_data
| (uint32_t)trk_crc
<< 17; // bits 18-29 CRC & 0x3FFFF
1452 for (uint8_t t_ctr
= 0; t_ctr
< 30; t_ctr
++) { // Prepare screen buffer and write data line.
1453 if (trk_data
& (uint32_t)1 << t_ctr
){
1454 trk_buffer
[29 - t_ctr
] = SYM_TELEMETRY_0
;
1457 trk_buffer
[29 - t_ctr
] = SYM_TELEMETRY_1
;
1461 displayWrite(osdDisplayPort
, 0, 0, trk_buffer
);
1462 if (osdConfig()->telemetry
>1){
1463 displayWrite(osdDisplayPort
, 0, 3, trk_buffer
); // Test display because normal telemetry line is not visible
1468 static void osdFormatPidControllerOutput(char *buff
, const char *label
, const pidController_t
*pidController
, uint8_t scale
, bool showDecimal
) {
1469 strcpy(buff
, label
);
1470 for (uint8_t i
= strlen(label
); i
< 5; ++i
) buff
[i
] = ' ';
1471 uint8_t decimals
= showDecimal
? 1 : 0;
1472 osdFormatCentiNumber(buff
+ 5, pidController
->proportional
* scale
, 0, decimals
, 0, 4, false);
1474 osdFormatCentiNumber(buff
+ 10, pidController
->integrator
* scale
, 0, decimals
, 0, 4, false);
1476 osdFormatCentiNumber(buff
+ 15, pidController
->derivative
* scale
, 0, decimals
, 0, 4, false);
1478 osdFormatCentiNumber(buff
+ 20, pidController
->output_constrained
* scale
, 0, decimals
, 0, 4, false);
1482 static void osdDisplayBatteryVoltage(uint8_t elemPosX
, uint8_t elemPosY
, uint16_t voltage
, uint8_t digits
, uint8_t decimals
)
1485 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
1487 osdFormatBatteryChargeSymbol(buff
);
1489 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1490 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
1492 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1493 digits
= MIN(digits
, 5);
1494 osdFormatCentiNumber(buff
, voltage
, 0, decimals
, 0, digits
, false);
1495 buff
[digits
] = SYM_VOLT
;
1496 buff
[digits
+1] = '\0';
1497 const batteryState_e batteryVoltageState
= checkBatteryVoltageState();
1498 if (batteryVoltageState
== BATTERY_CRITICAL
|| batteryVoltageState
== BATTERY_WARNING
) {
1499 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1501 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 1, elemPosY
, buff
, elemAttr
);
1504 static void osdDisplayFlightPIDValues(uint8_t elemPosX
, uint8_t elemPosY
, const char *str
, pidIndex_e pidIndex
, adjustmentFunction_e adjFuncP
, adjustmentFunction_e adjFuncI
, adjustmentFunction_e adjFuncD
, adjustmentFunction_e adjFuncFF
)
1506 textAttributes_t elemAttr
;
1509 const pid8_t
*pid
= &pidBank()->pid
[pidIndex
];
1510 pidType_e pidType
= pidIndexGetType(pidIndex
);
1512 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, str
);
1514 if (pidType
== PID_TYPE_NONE
) {
1515 // PID is not used in this configuration. Draw dashes.
1516 // XXX: Keep this in sync with the %3d format and spacing used below
1517 displayWrite(osdDisplayPort
, elemPosX
+ 6, elemPosY
, "- - - -");
1521 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1522 tfp_sprintf(buff
, "%3d", pid
->P
);
1523 if ((isAdjustmentFunctionSelected(adjFuncP
)) || (((adjFuncP
== ADJUSTMENT_ROLL_P
) || (adjFuncP
== ADJUSTMENT_PITCH_P
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P
))))
1524 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1525 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
1527 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1528 tfp_sprintf(buff
, "%3d", pid
->I
);
1529 if ((isAdjustmentFunctionSelected(adjFuncI
)) || (((adjFuncI
== ADJUSTMENT_ROLL_I
) || (adjFuncI
== ADJUSTMENT_PITCH_I
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I
))))
1530 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1531 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 8, elemPosY
, buff
, elemAttr
);
1533 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1534 tfp_sprintf(buff
, "%3d", pid
->D
);
1535 if ((isAdjustmentFunctionSelected(adjFuncD
)) || (((adjFuncD
== ADJUSTMENT_ROLL_D
) || (adjFuncD
== ADJUSTMENT_PITCH_D
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D
))))
1536 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1537 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 12, elemPosY
, buff
, elemAttr
);
1539 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1540 tfp_sprintf(buff
, "%3d", pid
->FF
);
1541 if ((isAdjustmentFunctionSelected(adjFuncFF
)) || (((adjFuncFF
== ADJUSTMENT_ROLL_FF
) || (adjFuncFF
== ADJUSTMENT_PITCH_FF
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF
))))
1542 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1543 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 16, elemPosY
, buff
, elemAttr
);
1546 static void osdDisplayNavPIDValues(uint8_t elemPosX
, uint8_t elemPosY
, const char *str
, pidIndex_e pidIndex
, adjustmentFunction_e adjFuncP
, adjustmentFunction_e adjFuncI
, adjustmentFunction_e adjFuncD
)
1548 textAttributes_t elemAttr
;
1551 const pid8_t
*pid
= &pidBank()->pid
[pidIndex
];
1552 pidType_e pidType
= pidIndexGetType(pidIndex
);
1554 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, str
);
1556 if (pidType
== PID_TYPE_NONE
) {
1557 // PID is not used in this configuration. Draw dashes.
1558 // XXX: Keep this in sync with the %3d format and spacing used below
1559 displayWrite(osdDisplayPort
, elemPosX
+ 6, elemPosY
, "- - -");
1563 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1564 tfp_sprintf(buff
, "%3d", pid
->P
);
1565 if ((isAdjustmentFunctionSelected(adjFuncP
)) || (((adjFuncP
== ADJUSTMENT_ROLL_P
) || (adjFuncP
== ADJUSTMENT_PITCH_P
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P
))))
1566 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1567 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
1569 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1570 tfp_sprintf(buff
, "%3d", pid
->I
);
1571 if ((isAdjustmentFunctionSelected(adjFuncI
)) || (((adjFuncI
== ADJUSTMENT_ROLL_I
) || (adjFuncI
== ADJUSTMENT_PITCH_I
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I
))))
1572 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1573 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 8, elemPosY
, buff
, elemAttr
);
1575 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1576 tfp_sprintf(buff
, "%3d", pidType
== PID_TYPE_PIFF
? pid
->FF
: pid
->D
);
1577 if ((isAdjustmentFunctionSelected(adjFuncD
)) || (((adjFuncD
== ADJUSTMENT_ROLL_D
) || (adjFuncD
== ADJUSTMENT_PITCH_D
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D
))))
1578 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1579 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 12, elemPosY
, buff
, elemAttr
);
1582 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
) {
1584 textAttributes_t elemAttr
;
1585 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, str
);
1587 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1588 osdFormatCentiNumber(buff
, value
* 100, 0, maxDecimals
, 0, MIN(valueLength
, 8), false);
1589 if (isAdjustmentFunctionSelected(adjFunc
))
1590 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1591 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ strlen(str
) + 1 + valueOffset
, elemPosY
, buff
, elemAttr
);
1594 int8_t getGeoWaypointNumber(int8_t waypointIndex
)
1596 static int8_t lastWaypointIndex
= 1;
1597 static int8_t geoWaypointIndex
;
1599 if (waypointIndex
!= lastWaypointIndex
) {
1600 lastWaypointIndex
= geoWaypointIndex
= waypointIndex
;
1601 for (uint8_t i
= posControl
.startWpIndex
; i
<= waypointIndex
; i
++) {
1602 if (posControl
.waypointList
[i
].action
== NAV_WP_ACTION_SET_POI
||
1603 posControl
.waypointList
[i
].action
== NAV_WP_ACTION_SET_HEAD
||
1604 posControl
.waypointList
[i
].action
== NAV_WP_ACTION_JUMP
) {
1605 geoWaypointIndex
-= 1;
1610 return geoWaypointIndex
- posControl
.startWpIndex
+ 1;
1613 void osdDisplaySwitchIndicator(const char *swName
, int rcValue
, char *buff
) {
1616 if (osdConfig()->osd_switch_indicators_align_left
) {
1617 for (ptr
= 0; ptr
< constrain(strlen(swName
), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH
); ptr
++) {
1618 buff
[ptr
] = swName
[ptr
];
1621 if ( rcValue
< 1333) {
1622 buff
[ptr
++] = SYM_SWITCH_INDICATOR_LOW
;
1623 } else if ( rcValue
> 1666) {
1624 buff
[ptr
++] = SYM_SWITCH_INDICATOR_HIGH
;
1626 buff
[ptr
++] = SYM_SWITCH_INDICATOR_MID
;
1629 if ( rcValue
< 1333) {
1630 buff
[ptr
++] = SYM_SWITCH_INDICATOR_LOW
;
1631 } else if ( rcValue
> 1666) {
1632 buff
[ptr
++] = SYM_SWITCH_INDICATOR_HIGH
;
1634 buff
[ptr
++] = SYM_SWITCH_INDICATOR_MID
;
1637 for (ptr
= 1; ptr
< constrain(strlen(swName
), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH
) + 1; ptr
++) {
1638 buff
[ptr
] = swName
[ptr
-1];
1647 static bool osdDrawSingleElement(uint8_t item
)
1649 uint16_t pos
= osdLayoutsConfig()->item_pos
[currentLayout
][item
];
1650 if (!OSD_VISIBLE(pos
)) {
1653 uint8_t elemPosX
= OSD_X(pos
);
1654 uint8_t elemPosY
= OSD_Y(pos
);
1655 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
1656 char buff
[32] = {0};
1659 case OSD_CUSTOM_ELEMENT_1
:
1661 customElementDrawElement(buff
, 0);
1664 case OSD_CUSTOM_ELEMENT_2
:
1666 customElementDrawElement(buff
, 1);
1669 case OSD_CUSTOM_ELEMENT_3
:
1671 customElementDrawElement(buff
, 2);
1674 case OSD_RSSI_VALUE
:
1676 uint16_t osdRssi
= osdConvertRSSI();
1678 tfp_sprintf(buff
+ 1, "%2d", osdRssi
);
1679 if (osdRssi
< osdConfig()->rssi_alarm
) {
1680 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1685 case OSD_MAIN_BATT_VOLTAGE
: {
1686 uint8_t base_digits
= 2U;
1687 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1688 if(isDJICompatibleVideoSystem(osdConfig())) {
1689 base_digits
= 3U; // Add extra digit to account for decimal point taking an extra character space
1692 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatteryRawVoltage(), base_digits
+ osdConfig()->main_voltage_decimals
, osdConfig()->main_voltage_decimals
);
1696 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
: {
1697 uint8_t base_digits
= 2U;
1698 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1699 if(isDJICompatibleVideoSystem(osdConfig())) {
1700 base_digits
= 3U; // Add extra digit to account for decimal point taking an extra character space
1703 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatterySagCompensatedVoltage(), base_digits
+ osdConfig()->main_voltage_decimals
, osdConfig()->main_voltage_decimals
);
1707 case OSD_CURRENT_DRAW
: {
1708 osdFormatCentiNumber(buff
, getAmperage(), 0, 2, 0, 3, false);
1712 uint8_t current_alarm
= osdConfig()->current_alarm
;
1713 if ((current_alarm
> 0) && ((getAmperage() / 100.0f
) > current_alarm
)) {
1714 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1719 case OSD_MAH_DRAWN
: {
1720 uint8_t mah_digits
= osdConfig()->mAh_precision
; // Initialize to config value
1722 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1723 if (isDJICompatibleVideoSystem(osdConfig())) {
1724 //DJIcompat is unable to work with scaled values and it only has mAh symbol to work with
1725 tfp_sprintf(buff
, "%5d", (int)getMAhDrawn()); // Use 5 digits to allow packs below 100Ah
1731 if (osdFormatCentiNumber(buff
, getMAhDrawn() * 100, 1000, 0, (mah_digits
- 2), mah_digits
, false)) {
1733 buff
[mah_digits
] = SYM_AH
;
1736 buff
[mah_digits
] = SYM_MAH
;
1738 buff
[mah_digits
+ 1] = '\0';
1741 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1746 osdFormatCentiNumber(buff
, getMWhDrawn() / 10, 0, 2, 0, 3, false);
1747 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1752 case OSD_BATTERY_REMAINING_CAPACITY
:
1754 bool unitsDrawn
= false;
1756 if (currentBatteryProfile
->capacity
.value
== 0)
1757 tfp_sprintf(buff
, " NA");
1758 else if (!batteryWasFullWhenPluggedIn())
1759 tfp_sprintf(buff
, " NF");
1760 else if (batteryMetersConfig()->capacity_unit
== BAT_CAPACITY_UNIT_MAH
) {
1761 uint8_t mah_digits
= osdConfig()->mAh_precision
; // Initialize to config value
1763 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
1764 if (isDJICompatibleVideoSystem(osdConfig())) {
1765 //DJIcompat is unable to work with scaled values and it only has mAh symbol to work with
1766 tfp_sprintf(buff
, "%5d", (int)getBatteryRemainingCapacity()); // Use 5 digits to allow packs below 100Ah
1773 if (osdFormatCentiNumber(buff
, getBatteryRemainingCapacity() * 100, 1000, 0, (mah_digits
- 2), mah_digits
, false)) {
1775 buff
[mah_digits
] = SYM_AH
;
1778 buff
[mah_digits
] = SYM_MAH
;
1780 buff
[mah_digits
+ 1] = '\0';
1783 } else // batteryMetersConfig()->capacityUnit == BAT_CAPACITY_UNIT_MWH
1784 osdFormatCentiNumber(buff
+ 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3, false);
1787 buff
[4] = batteryMetersConfig()->capacity_unit
== BAT_CAPACITY_UNIT_MAH
? SYM_MAH
: SYM_WH
;
1791 if (batteryUsesCapacityThresholds()) {
1792 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1797 case OSD_BATTERY_REMAINING_PERCENT
:
1798 osdFormatBatteryChargeSymbol(buff
);
1799 tfp_sprintf(buff
+ 1, "%3d%%", calculateBatteryPercentage());
1800 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1803 case OSD_POWER_SUPPLY_IMPEDANCE
:
1804 if (isPowerSupplyImpedanceValid())
1805 tfp_sprintf(buff
, "%3d", getPowerSupplyImpedance());
1807 strcpy(buff
, "---");
1808 buff
[3] = SYM_MILLIOHM
;
1814 buff
[0] = SYM_SAT_L
;
1815 buff
[1] = SYM_SAT_R
;
1816 tfp_sprintf(buff
+ 2, "%2d", gpsSol
.numSat
);
1817 #ifdef USE_GPS_FIX_ESTIMATION
1818 if (STATE(GPS_ESTIMATED_FIX
)) {
1819 strcpy(buff
+ 2, "ES");
1820 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1823 if (!STATE(GPS_FIX
)) {
1824 hardwareSensorStatus_e sensorStatus
= getHwGPSStatus();
1825 if (sensorStatus
== HW_SENSOR_UNAVAILABLE
|| sensorStatus
== HW_SENSOR_UNHEALTHY
) {
1826 buff
[2] = SYM_ALERT
;
1829 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1834 osdFormatVelocityStr(buff
, gpsSol
.groundSpeed
, false, false);
1837 case OSD_GPS_MAX_SPEED
:
1838 osdFormatVelocityStr(buff
, stats
.max_speed
, false, true);
1842 osdFormatVelocityStr(buff
, osdGet3DSpeed(), true, false);
1845 case OSD_3D_MAX_SPEED
:
1846 osdFormatVelocityStr(buff
, stats
.max_3D_speed
, true, true);
1849 case OSD_GLIDESLOPE
:
1851 float horizontalSpeed
= gpsSol
.groundSpeed
;
1852 float sinkRate
= -getEstimatedActualVelocity(Z
);
1853 static pt1Filter_t gsFilterState
;
1854 const timeMs_t currentTimeMs
= millis();
1855 static timeMs_t gsUpdatedTimeMs
;
1856 float glideSlope
= horizontalSpeed
/ sinkRate
;
1857 glideSlope
= pt1FilterApply4(&gsFilterState
, isnormal(glideSlope
) ? glideSlope
: 200, 0.5, MS2S(currentTimeMs
- gsUpdatedTimeMs
));
1858 gsUpdatedTimeMs
= currentTimeMs
;
1860 buff
[0] = SYM_GLIDESLOPE
;
1861 if (glideSlope
> 0.0f
&& glideSlope
< 100.0f
) {
1862 osdFormatCentiNumber(buff
+ 1, glideSlope
* 100.0f
, 0, 2, 0, 3, false);
1864 buff
[1] = buff
[2] = buff
[3] = '-';
1871 osdFormatCoordinate(buff
, SYM_LAT
, gpsSol
.llh
.lat
);
1875 osdFormatCoordinate(buff
, SYM_LON
, gpsSol
.llh
.lon
);
1881 #ifdef USE_GPS_FIX_ESTIMATION
1882 || STATE(GPS_ESTIMATED_FIX
)
1884 ) && STATE(GPS_FIX_HOME
) && isImuHeadingValid()) {
1885 if (GPS_distanceToHome
< (navConfig()->general
.min_rth_distance
/ 100) ) {
1886 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_HOME_NEAR
);
1890 int16_t panHomeDirOffset
= 0;
1891 if (!(osdConfig()->pan_servo_pwm2centideg
== 0)){
1892 panHomeDirOffset
= osdGetPanServoOffset();
1894 int16_t flightDirection
= STATE(AIRPLANE
) ? CENTIDEGREES_TO_DEGREES(posControl
.actualState
.cog
) : DECIDEGREES_TO_DEGREES(osdGetHeading());
1895 int homeDirection
= GPS_directionToHome
- flightDirection
+ panHomeDirOffset
;
1896 osdDrawDirArrow(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), homeDirection
);
1899 // No home or no fix or unknown heading, blink.
1900 // If we're unarmed, show the arrow pointing up so users can see the arrow
1901 // while configuring the OSD. If we're armed, show a '-' indicating that
1902 // we don't know the direction to home.
1903 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1904 displayWriteCharWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, ARMING_FLAG(ARMED
) ? '-' : SYM_ARROW_UP
, elemAttr
);
1909 case OSD_HOME_HEADING_ERROR
:
1912 buff
[1] = SYM_HEADING
;
1914 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1915 int16_t h
= lrintf(CENTIDEGREES_TO_DEGREES((float)wrap_18000(DEGREES_TO_CENTIDEGREES((int32_t)GPS_directionToHome
) - (STATE(AIRPLANE
) ? posControl
.actualState
.cog
: DECIDEGREES_TO_CENTIDEGREES((int32_t)osdGetHeading())))));
1916 tfp_sprintf(buff
+ 2, "%4d", h
);
1918 strcpy(buff
+ 2, "----");
1921 buff
[6] = SYM_DEGREES
;
1929 uint32_t distance_to_home_cm
= GPS_distanceToHome
* 100;
1930 osdFormatDistanceSymbol(&buff
[1], distance_to_home_cm
, 0);
1932 uint16_t dist_alarm
= osdConfig()->dist_alarm
;
1933 if (dist_alarm
> 0 && GPS_distanceToHome
> dist_alarm
) {
1934 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1940 buff
[0] = SYM_TOTAL
;
1941 osdFormatDistanceSymbol(buff
+ 1, getTotalTravelDistance(), 0);
1947 if (IS_RC_MODE_ACTIVE(BOXBLACKBOX
)) {
1948 if (!isBlackboxDeviceWorking()) {
1949 tfp_sprintf(buff
, "%c%c", SYM_BLACKBOX
, SYM_ALERT
);
1950 } else if (isBlackboxDeviceFull()) {
1951 tfp_sprintf(buff
, "%cFULL", SYM_BLACKBOX
);
1953 int32_t logNumber
= blackboxGetLogNumber();
1954 if (logNumber
>= 0) {
1955 tfp_sprintf(buff
, "%c%05" PRId32
, SYM_BLACKBOX
, logNumber
);
1957 tfp_sprintf(buff
, "%c", SYM_BLACKBOX
);
1961 #endif // USE_BLACKBOX
1967 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_ODOMETER
);
1968 float_t odometerDist
= CENTIMETERS_TO_METERS(getTotalTravelDistance());
1970 odometerDist
+= statsConfig()->stats_total_dist
;
1973 switch (osdConfig()->units
) {
1976 case OSD_UNIT_IMPERIAL
:
1977 osdFormatCentiNumber(buff
, METERS_TO_MILES(odometerDist
) * 100, 1, 1, 1, 6, true);
1982 osdFormatCentiNumber(buff
, METERS_TO_NAUTICALMILES(odometerDist
) * 100, 1, 1, 1, 6, true);
1985 case OSD_UNIT_METRIC_MPH
:
1987 case OSD_UNIT_METRIC
:
1988 osdFormatCentiNumber(buff
, METERS_TO_KILOMETERS(odometerDist
) * 100, 1, 1, 1, 6, true);
1997 case OSD_GROUND_COURSE
:
1999 buff
[0] = SYM_GROUND_COURSE
;
2000 if (osdIsHeadingValid()) {
2001 tfp_sprintf(&buff
[1], "%3d", (int16_t)CENTIDEGREES_TO_DEGREES(posControl
.actualState
.cog
));
2003 buff
[1] = buff
[2] = buff
[3] = '-';
2005 buff
[4] = SYM_DEGREES
;
2010 case OSD_COURSE_HOLD_ERROR
:
2012 if (ARMING_FLAG(ARMED
) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) {
2013 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
2017 buff
[0] = SYM_HEADING
;
2019 if ((!ARMING_FLAG(ARMED
)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
) && isAdjustingPosition())) {
2020 buff
[1] = buff
[2] = buff
[3] = '-';
2021 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) {
2022 int16_t herr
= lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
2024 strcpy(buff
+ 1, ">99");
2026 tfp_sprintf(buff
+ 1, "%3d", herr
);
2029 buff
[4] = SYM_DEGREES
;
2034 case OSD_COURSE_HOLD_ADJUSTMENT
:
2036 int16_t heading_adjust
= lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
2038 if (ARMING_FLAG(ARMED
) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust
!= 0)))) {
2039 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
2043 buff
[0] = SYM_HEADING
;
2045 if (!ARMING_FLAG(ARMED
)) {
2046 buff
[1] = buff
[2] = buff
[3] = buff
[4] = '-';
2047 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) {
2048 tfp_sprintf(buff
+ 1, "%4d", heading_adjust
);
2051 buff
[5] = SYM_DEGREES
;
2056 case OSD_CROSS_TRACK_ERROR
:
2058 if (isWaypointNavTrackingActive()) {
2059 buff
[0] = SYM_CROSS_TRACK_ERROR
;
2060 osdFormatDistanceSymbol(buff
+ 1, navigationGetCrossTrackError(), 0);
2062 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
2070 buff
[0] = SYM_HDP_L
;
2071 buff
[1] = SYM_HDP_R
;
2072 int32_t centiHDOP
= 100 * gpsSol
.hdop
/ HDOP_SCALE
;
2073 uint8_t digits
= 2U;
2074 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
2075 if (isDJICompatibleVideoSystem(osdConfig())) {
2079 osdFormatCentiNumber(&buff
[2], centiHDOP
, 0, 1, 0, digits
, false);
2083 case OSD_ADSB_WARNING
:
2085 static uint8_t adsblen
= 1;
2086 uint8_t arrowPositionX
= 0;
2088 for (int i
= 0; i
<= adsblen
; i
++) {
2089 buff
[i
] = SYM_BLANK
;
2093 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
); // clear any previous chars because variable element size
2095 adsbVehicle_t
*vehicle
= findVehicleClosest();
2097 if(vehicle
!= NULL
){
2098 recalculateVehicle(vehicle
);
2103 (vehicle
->calculatedVehicleValues
.dist
> 0) &&
2104 vehicle
->calculatedVehicleValues
.dist
< METERS_TO_CENTIMETERS(osdConfig()->adsb_distance_warning
) &&
2105 (osdConfig()->adsb_ignore_plane_above_me_limit
== 0 || METERS_TO_CENTIMETERS(osdConfig()->adsb_ignore_plane_above_me_limit
) > vehicle
->calculatedVehicleValues
.verticalDistance
)
2108 osdFormatDistanceStr(&buff
[1], (int32_t)vehicle
->calculatedVehicleValues
.dist
);
2109 adsblen
= strlen(buff
);
2111 buff
[adsblen
-1] = SYM_BLANK
;
2113 arrowPositionX
= adsblen
-1;
2114 osdFormatDistanceStr(&buff
[adsblen
], vehicle
->calculatedVehicleValues
.verticalDistance
);
2115 adsblen
= strlen(buff
)-1;
2117 if (vehicle
->calculatedVehicleValues
.dist
< METERS_TO_CENTIMETERS(osdConfig()->adsb_distance_alert
)) {
2118 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2123 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
2125 if (arrowPositionX
> 0){
2126 int16_t panHomeDirOffset
= 0;
2127 if (osdConfig()->pan_servo_pwm2centideg
!= 0){
2128 panHomeDirOffset
= osdGetPanServoOffset();
2130 int16_t flightDirection
= STATE(AIRPLANE
) ? CENTIDEGREES_TO_DEGREES(posControl
.actualState
.cog
) : DECIDEGREES_TO_DEGREES(osdGetHeading());
2131 osdDrawDirArrow(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
+ arrowPositionX
, elemPosY
), CENTIDEGREES_TO_DEGREES(vehicle
->calculatedVehicleValues
.dir
) - flightDirection
+ panHomeDirOffset
);
2139 if(getAdsbStatus()->vehiclesMessagesTotal
> 0){
2140 tfp_sprintf(buff
+ 1, "%2d", getActiveVehiclesCount());
2151 static uint16_t drawn
= 0;
2152 static uint32_t scale
= 0;
2153 osdDrawHomeMap(0, 'N', &drawn
, &scale
);
2156 case OSD_MAP_TAKEOFF
:
2158 static uint16_t drawn
= 0;
2159 static uint32_t scale
= 0;
2160 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn
, &scale
);
2165 static uint16_t drawn
= 0;
2166 static uint32_t scale
= 0;
2167 osdDrawRadar(&drawn
, &scale
);
2174 int32_t alt
= osdGetAltitude();
2175 osdFormatAltitudeSymbol(buff
, alt
);
2177 uint16_t alt_alarm
= osdConfig()->alt_alarm
;
2178 uint16_t neg_alt_alarm
= osdConfig()->neg_alt_alarm
;
2179 if ((alt_alarm
> 0 && CENTIMETERS_TO_METERS(alt
) > alt_alarm
) ||
2180 (neg_alt_alarm
> 0 && alt
< 0 && -CENTIMETERS_TO_METERS(alt
) > neg_alt_alarm
)) {
2182 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2184 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
2186 if (STATE(MULTIROTOR
) && posControl
.flags
.isAdjustingAltitude
) {
2187 /* Indicate MR altitude adjustment active with constant symbol at first blank position.
2188 * Alternate symbol on/off with 600ms cycle if first position not blank (to maintain visibility of -ve sign) */
2190 for (blankPos
= 2; blankPos
>= 0; blankPos
--) {
2191 if (buff
[blankPos
] == SYM_BLANK
) {
2195 if (blankPos
>= 0 || OSD_ALTERNATING_CHOICES(600, 2) == 0) {
2196 blankPos
= blankPos
< 0 ? 0 : blankPos
;
2197 displayWriteChar(osdDisplayPort
, elemPosX
+ blankPos
, elemPosY
, SYM_TERRAIN_FOLLOWING
);
2203 case OSD_ALTITUDE_MSL
:
2205 int32_t alt
= osdGetAltitudeMsl();
2206 osdFormatAltitudeSymbol(buff
, alt
);
2210 #ifdef USE_RANGEFINDER
2211 case OSD_RANGEFINDER
:
2213 int32_t range
= rangefinderGetLatestRawAltitude();
2219 osdFormatDistanceSymbol(buff
, range
, 1);
2227 osdFormatOnTime(buff
);
2233 osdFormatFlyTime(buff
, &elemAttr
);
2237 case OSD_ONTIME_FLYTIME
:
2239 if (ARMING_FLAG(ARMED
)) {
2240 osdFormatFlyTime(buff
, &elemAttr
);
2242 osdFormatOnTime(buff
);
2247 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH
:
2249 /*static int32_t updatedTimeSeconds = 0;*/
2250 static int32_t timeSeconds
= -1;
2251 #if defined(USE_ADC) && defined(USE_GPS)
2252 static timeUs_t updatedTimestamp
= 0;
2253 timeUs_t currentTimeUs
= micros();
2254 if (cmpTimeUs(currentTimeUs
, updatedTimestamp
) >= MS2US(1000)) {
2255 #ifdef USE_WIND_ESTIMATOR
2256 timeSeconds
= calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation
);
2258 timeSeconds
= calculateRemainingFlightTimeBeforeRTH(false);
2260 updatedTimestamp
= currentTimeUs
;
2263 if ((!ARMING_FLAG(ARMED
)) || (timeSeconds
== -1)) {
2264 buff
[0] = SYM_FLIGHT_MINS_REMAINING
;
2265 strcpy(buff
+ 1, "--:--");
2266 #if defined(USE_ADC) && defined(USE_GPS)
2267 updatedTimestamp
= 0;
2269 } else if (timeSeconds
== -2) {
2270 // Wind is too strong to come back with cruise throttle
2271 buff
[0] = SYM_FLIGHT_MINS_REMAINING
;
2272 buff
[1] = buff
[2] = buff
[4] = buff
[5] = SYM_WIND_HORIZONTAL
;
2275 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2277 osdFormatTime(buff
, timeSeconds
, SYM_FLIGHT_MINS_REMAINING
, SYM_FLIGHT_HOURS_REMAINING
);
2278 if (timeSeconds
== 0)
2279 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2284 case OSD_REMAINING_DISTANCE_BEFORE_RTH
:;
2285 static int32_t distanceMeters
= -1;
2286 #if defined(USE_ADC) && defined(USE_GPS)
2287 static timeUs_t updatedTimestamp
= 0;
2288 timeUs_t currentTimeUs
= micros();
2289 if (cmpTimeUs(currentTimeUs
, updatedTimestamp
) >= MS2US(1000)) {
2290 #ifdef USE_WIND_ESTIMATOR
2291 distanceMeters
= calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation
);
2293 distanceMeters
= calculateRemainingDistanceBeforeRTH(false);
2295 updatedTimestamp
= currentTimeUs
;
2298 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_FLIGHT_DIST_REMAINING
);
2300 if ((!ARMING_FLAG(ARMED
)) || (distanceMeters
== -1)) {
2301 buff
[3] = SYM_BLANK
;
2303 strcpy(buff
, "---");
2304 } else if (distanceMeters
== -2) {
2305 // Wind is too strong to come back with cruise throttle
2306 buff
[0] = buff
[1] = buff
[2] = SYM_WIND_HORIZONTAL
;
2307 switch ((osd_unit_e
)osdConfig()->units
){
2310 case OSD_UNIT_IMPERIAL
:
2311 buff
[3] = SYM_DIST_MI
;
2313 case OSD_UNIT_METRIC_MPH
:
2315 case OSD_UNIT_METRIC
:
2316 buff
[3] = SYM_DIST_KM
;
2319 buff
[3] = SYM_DIST_NM
;
2323 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2325 osdFormatDistanceSymbol(buff
, distanceMeters
* 100, 0);
2326 if (distanceMeters
== 0)
2327 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2335 #ifdef USE_FW_AUTOLAND
2336 if (FLIGHT_MODE(NAV_FW_AUTOLAND
))
2340 if (FLIGHT_MODE(FAILSAFE_MODE
))
2342 else if (FLIGHT_MODE(MANUAL_MODE
))
2344 else if (FLIGHT_MODE(TURTLE_MODE
))
2346 else if (FLIGHT_MODE(NAV_RTH_MODE
))
2347 p
= isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
2348 else if (FLIGHT_MODE(NAV_POSHOLD_MODE
) && STATE(AIRPLANE
))
2350 else if (FLIGHT_MODE(NAV_POSHOLD_MODE
))
2352 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
) && FLIGHT_MODE(NAV_ALTHOLD_MODE
))
2354 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
))
2356 else if (FLIGHT_MODE(NAV_WP_MODE
))
2358 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE
) && navigationRequiresAngleMode()) {
2359 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
2360 // it means it can be combined with ANGLE, HORIZON, ACRO, etc...
2361 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
2362 // (Currently only applies to multirotor).
2365 else if (FLIGHT_MODE(ANGLE_MODE
))
2367 else if (FLIGHT_MODE(HORIZON_MODE
))
2369 else if (FLIGHT_MODE(ANGLEHOLD_MODE
))
2372 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, p
);
2376 case OSD_CRAFT_NAME
:
2377 osdFormatCraftName(buff
);
2380 case OSD_PILOT_NAME
:
2381 osdFormatPilotName(buff
);
2384 case OSD_PILOT_LOGO
:
2385 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_PILOT_LOGO_SML_L
);
2386 displayWriteChar(osdDisplayPort
, elemPosX
+1, elemPosY
, SYM_PILOT_LOGO_SML_C
);
2387 displayWriteChar(osdDisplayPort
, elemPosX
+2, elemPosY
, SYM_PILOT_LOGO_SML_R
);
2390 case OSD_THROTTLE_POS
:
2392 osdFormatThrottlePosition(buff
, false, &elemAttr
);
2396 case OSD_VTX_CHANNEL
:
2398 vtxDeviceOsdInfo_t osdInfo
;
2399 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo
);
2401 tfp_sprintf(buff
, "CH:%c%s:", osdInfo
.bandLetter
, osdInfo
.channelName
);
2402 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
2404 tfp_sprintf(buff
, "%c", osdInfo
.powerIndexLetter
);
2405 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL
)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2406 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 6, elemPosY
, buff
, elemAttr
);
2413 vtxDeviceOsdInfo_t osdInfo
;
2414 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo
);
2416 tfp_sprintf(buff
, "%c", SYM_VTX_POWER
);
2417 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
2419 tfp_sprintf(buff
, "%c", osdInfo
.powerIndexLetter
);
2420 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL
)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2421 displayWriteWithAttr(osdDisplayPort
, elemPosX
+1, elemPosY
, buff
, elemAttr
);
2425 #if defined(USE_SERIALRX_CRSF)
2426 case OSD_CRSF_RSSI_DBM
:
2428 int16_t rssi
= rxLinkStatistics
.uplinkRSSI
;
2429 buff
[0] = (rxLinkStatistics
.activeAntenna
== 0) ? SYM_RSSI
: SYM_2RSS
; // Separate symbols for each antenna
2431 tfp_sprintf(buff
+ 1, "%4d%c", rssi
, SYM_DBM
);
2433 tfp_sprintf(buff
+ 1, "%3d%c%c", rssi
, SYM_DBM
, ' ');
2435 if (!failsafeIsReceivingRxData()){
2436 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2437 } else if (osdConfig()->rssi_dbm_alarm
&& rssi
< osdConfig()->rssi_dbm_alarm
) {
2438 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2445 int16_t statsLQ
= rxLinkStatistics
.uplinkLQ
;
2446 int16_t scaledLQ
= scaleRange(constrain(statsLQ
, 0, 100), 0, 100, 170, 300);
2447 switch (osdConfig()->crsf_lq_format
) {
2448 case OSD_CRSF_LQ_TYPE1
:
2449 if (!failsafeIsReceivingRxData()) {
2450 tfp_sprintf(buff
+1, "%3d", 0);
2452 tfp_sprintf(buff
+1, "%3d", rxLinkStatistics
.uplinkLQ
);
2455 case OSD_CRSF_LQ_TYPE2
:
2456 if (!failsafeIsReceivingRxData()) {
2457 tfp_sprintf(buff
+1, "%s:%3d", " ", 0);
2459 tfp_sprintf(buff
+1, "%d:%3d", rxLinkStatistics
.rfMode
, rxLinkStatistics
.uplinkLQ
);
2462 case OSD_CRSF_LQ_TYPE3
:
2463 if (!failsafeIsReceivingRxData()) {
2464 tfp_sprintf(buff
+1, "%3d", 0);
2466 tfp_sprintf(buff
+1, "%3d", rxLinkStatistics
.rfMode
>= 2 ? scaledLQ
: rxLinkStatistics
.uplinkLQ
);
2470 if (!failsafeIsReceivingRxData()) {
2471 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2472 } else if (rxLinkStatistics
.uplinkLQ
< osdConfig()->link_quality_alarm
) {
2473 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2478 case OSD_CRSF_SNR_DB
:
2480 static pt1Filter_t snrFilterState
;
2481 static timeMs_t snrUpdated
= 0;
2482 int8_t snrFiltered
= pt1FilterApply4(&snrFilterState
, rxLinkStatistics
.uplinkSNR
, 0.5f
, MS2S(millis() - snrUpdated
));
2483 snrUpdated
= millis();
2485 const char* showsnr
= "-20";
2486 const char* hidesnr
= " ";
2487 if (snrFiltered
> osdConfig()->snr_alarm
) {
2490 tfp_sprintf(buff
+ 1, "%s%c", showsnr
, SYM_DB
);
2492 buff
[0] = SYM_BLANK
;
2493 tfp_sprintf(buff
+ 1, "%s%c", hidesnr
, SYM_BLANK
);
2495 } else if (snrFiltered
<= osdConfig()->snr_alarm
) {
2497 if (snrFiltered
<= -10) {
2498 tfp_sprintf(buff
+ 1, "%3d%c", snrFiltered
, SYM_DB
);
2500 tfp_sprintf(buff
+ 1, "%2d%c%c", snrFiltered
, SYM_DB
, ' ');
2506 case OSD_CRSF_TX_POWER
:
2508 if (!failsafeIsReceivingRxData())
2509 tfp_sprintf(buff
, "%s%c", " ", SYM_BLANK
);
2511 tfp_sprintf(buff
, "%4d%c", rxLinkStatistics
.uplinkTXPower
, SYM_MW
);
2516 case OSD_FORMATION_FLIGHT
:
2518 static uint8_t currentPeerIndex
= 0;
2519 static timeMs_t lastPeerSwitch
;
2521 if ((STATE(GPS_FIX
) && isImuHeadingValid())) {
2522 if ((radar_pois
[currentPeerIndex
].gps
.lat
== 0 || radar_pois
[currentPeerIndex
].gps
.lon
== 0 || radar_pois
[currentPeerIndex
].state
>= 2) || (millis() > (osdConfig()->radar_peers_display_time
* 1000) + lastPeerSwitch
)) {
2523 lastPeerSwitch
= millis();
2525 for(uint8_t i
= 1; i
< RADAR_MAX_POIS
- 1; i
++) {
2526 uint8_t nextPeerIndex
= (currentPeerIndex
+ i
) % (RADAR_MAX_POIS
- 1);
2527 if (radar_pois
[nextPeerIndex
].gps
.lat
!= 0 && radar_pois
[nextPeerIndex
].gps
.lon
!= 0 && radar_pois
[nextPeerIndex
].state
< 2) {
2528 currentPeerIndex
= nextPeerIndex
;
2534 radar_pois_t
*currentPeer
= &(radar_pois
[currentPeerIndex
]);
2535 if (currentPeer
->gps
.lat
!= 0 && currentPeer
->gps
.lon
!= 0 && currentPeer
->state
< 2) {
2537 geoConvertGeodeticToLocal(&poi
, &posControl
.gpsOrigin
, ¤tPeer
->gps
, GEO_ALT_RELATIVE
);
2539 currentPeer
->distance
= calculateDistanceToDestination(&poi
) / 100; // In m
2540 currentPeer
->altitude
= (int16_t )((currentPeer
->gps
.alt
- osdGetAltitudeMsl()) / 100);
2541 currentPeer
->direction
= (int16_t )(calculateBearingToDestination(&poi
) / 100); // In °
2543 int16_t panServoDirOffset
= 0;
2544 if (osdConfig()->pan_servo_pwm2centideg
!= 0){
2545 panServoDirOffset
= osdGetPanServoOffset();
2549 //[peer heading][peer ID][LQ][direction to peer]
2552 int relativePeerHeading
= osdGetHeadingAngle(currentPeer
->heading
- (int)DECIDEGREES_TO_DEGREES(osdGetHeading()));
2553 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_DECORATION
+ ((relativePeerHeading
+ 22) / 45) % 8);
2556 displayWriteChar(osdDisplayPort
, elemPosX
+ 1, elemPosY
, 65 + currentPeerIndex
);
2559 displayWriteChar(osdDisplayPort
, elemPosX
+ 2, elemPosY
, SYM_HUD_SIGNAL_0
+ currentPeer
->lq
);
2561 //[direction to peer]
2562 int directionToPeerError
= wrap_180(osdGetHeadingAngle(currentPeer
->direction
) + panServoDirOffset
- (int)DECIDEGREES_TO_DEGREES(osdGetHeading()));
2563 uint16_t iconIndexOffset
= constrain(((directionToPeerError
+ 180) / 30), 0, 12);
2564 if (iconIndexOffset
== 12) {
2565 iconIndexOffset
= 0; // Directly behind
2567 displayWriteChar(osdDisplayPort
, elemPosX
+ 3, elemPosY
, SYM_HUD_CARDINAL
+ iconIndexOffset
);
2571 switch ((osd_unit_e
)osdConfig()->units
) {
2574 case OSD_UNIT_IMPERIAL
:
2575 osdFormatCentiNumber(buff
, CENTIMETERS_TO_CENTIFEET(currentPeer
->distance
* 100), FEET_PER_MILE
, 0, 4, 4, false);
2578 osdFormatCentiNumber(buff
, CENTIMETERS_TO_CENTIFEET(currentPeer
->distance
* 100), (uint32_t)FEET_PER_NAUTICALMILE
, 0, 4, 4, false);
2582 case OSD_UNIT_METRIC_MPH
:
2584 case OSD_UNIT_METRIC
:
2585 osdFormatCentiNumber(buff
, currentPeer
->distance
* 100, METERS_PER_KILOMETER
, 0, 4, 4, false);
2588 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
+ 1, buff
);
2592 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
+ 2, (currentPeer
->altitude
>= 0) ? SYM_AH_DECORATION_UP
: SYM_AH_DECORATION_DOWN
);
2594 int altc
= currentPeer
->altitude
;
2595 switch ((osd_unit_e
)osdConfig()->units
) {
2600 case OSD_UNIT_IMPERIAL
:
2602 altc
= CENTIMETERS_TO_FEET(altc
* 100);
2606 case OSD_UNIT_METRIC_MPH
:
2608 case OSD_UNIT_METRIC
:
2609 // Already in metres
2613 altc
= ABS(constrain(altc
, -999, 999));
2614 tfp_sprintf(buff
, "%3d", altc
);
2615 displayWrite(osdDisplayPort
, elemPosX
+ 1, elemPosY
+ 2, buff
);
2622 for(uint8_t i
= 0; i
< 4; i
++){
2623 displayWriteChar(osdDisplayPort
, elemPosX
+ i
, elemPosY
, SYM_BLANK
);
2624 displayWriteChar(osdDisplayPort
, elemPosX
+ i
, elemPosY
+ 1, SYM_BLANK
);
2625 displayWriteChar(osdDisplayPort
, elemPosX
+ i
, elemPosY
+ 2, SYM_BLANK
);
2631 case OSD_CROSSHAIRS
: // Hud is a sub-element of the crosshair
2633 osdCrosshairPosition(&elemPosX
, &elemPosY
);
2634 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX
, elemPosY
);
2636 if (osdConfig()->hud_homing
&& (STATE(GPS_FIX
)
2637 #ifdef USE_GPS_FIX_ESTIMATION
2638 || STATE(GPS_ESTIMATED_FIX
)
2640 ) && STATE(GPS_FIX_HOME
) && isImuHeadingValid()) {
2641 osdHudDrawHoming(elemPosX
, elemPosY
);
2645 #ifdef USE_GPS_FIX_ESTIMATION
2646 || STATE(GPS_ESTIMATED_FIX
)
2648 ) && isImuHeadingValid()) {
2650 if (osdConfig()->hud_homepoint
|| osdConfig()->hud_radar_disp
> 0 || osdConfig()->hud_wp_disp
> 0) {
2654 // -------- POI : Home point
2656 if (osdConfig()->hud_homepoint
) { // Display the home point (H)
2657 osdHudDrawPoi(GPS_distanceToHome
, GPS_directionToHome
, -osdGetAltitude() / 100, 0, SYM_HOME
, 0 , 0);
2660 // -------- POI : Nearby aircrafts from ESP32 radar
2662 if (osdConfig()->hud_radar_disp
> 0) { // Display the POI from the radar
2663 for (uint8_t i
= 0; i
< osdConfig()->hud_radar_disp
; i
++) {
2664 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
2666 geoConvertGeodeticToLocal(&poi
, &posControl
.gpsOrigin
, &radar_pois
[i
].gps
, GEO_ALT_RELATIVE
);
2667 radar_pois
[i
].distance
= calculateDistanceToDestination(&poi
) / 100; // In meters
2669 if (radar_pois
[i
].distance
>= osdConfig()->hud_radar_range_min
&& radar_pois
[i
].distance
<= osdConfig()->hud_radar_range_max
) {
2670 radar_pois
[i
].direction
= calculateBearingToDestination(&poi
) / 100; // In °
2671 radar_pois
[i
].altitude
= (radar_pois
[i
].gps
.alt
- osdGetAltitudeMsl()) / 100;
2672 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
);
2678 // -------- POI : Next waypoints from navigation
2680 if (osdConfig()->hud_wp_disp
> 0 && posControl
.waypointListValid
&& posControl
.waypointCount
> 0) { // Display the next waypoints
2684 for (int i
= osdConfig()->hud_wp_disp
- 1; i
>= 0 ; i
--) { // Display in reverse order so the next WP is always written on top
2685 j
= posControl
.activeWaypointIndex
+ i
;
2686 if (j
> posControl
.startWpIndex
+ posControl
.waypointCount
- 1) { // limit to max WP index for mission
2689 if (posControl
.waypointList
[j
].lat
!= 0 && posControl
.waypointList
[j
].lon
!= 0) {
2690 wp2
.lat
= posControl
.waypointList
[j
].lat
;
2691 wp2
.lon
= posControl
.waypointList
[j
].lon
;
2692 wp2
.alt
= posControl
.waypointList
[j
].alt
;
2694 geoConvertGeodeticToLocal(&poi
, &posControl
.gpsOrigin
, &wp2
, waypointMissionAltConvMode(posControl
.waypointList
[j
].p3
));
2695 int32_t altConvModeAltitude
= waypointMissionAltConvMode(posControl
.waypointList
[j
].p3
) == GEO_ALT_ABSOLUTE
? osdGetAltitudeMsl() : osdGetAltitude();
2696 j
= getGeoWaypointNumber(j
);
2697 while (j
> 9) j
-= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2698 osdHudDrawPoi(calculateDistanceToDestination(&poi
) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi
) / 100), (posControl
.waypointList
[j
].alt
- altConvModeAltitude
)/ 100, 2, SYM_WAYPOINT
, 48 + j
, i
);
2707 case OSD_ATTITUDE_ROLL
:
2708 buff
[0] = SYM_ROLL_LEVEL
;
2709 if (ABS(attitude
.values
.roll
) >= 1)
2710 buff
[0] += (attitude
.values
.roll
< 0 ? -1 : 1);
2711 osdFormatCentiNumber(buff
+ 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude
.values
.roll
)), 0, 1, 0, 3, false);
2714 case OSD_ATTITUDE_PITCH
:
2715 if (ABS(attitude
.values
.pitch
) < 1)
2717 else if (attitude
.values
.pitch
> 0)
2718 buff
[0] = SYM_PITCH_DOWN
;
2719 else if (attitude
.values
.pitch
< 0)
2720 buff
[0] = SYM_PITCH_UP
;
2721 osdFormatCentiNumber(buff
+ 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude
.values
.pitch
)), 0, 1, 0, 3, false);
2724 case OSD_ARTIFICIAL_HORIZON
:
2726 float rollAngle
= DECIDEGREES_TO_RADIANS(attitude
.values
.roll
);
2727 float pitchAngle
= DECIDEGREES_TO_RADIANS(attitude
.values
.pitch
);
2729 pitchAngle
-= osdConfig()->ahi_camera_uptilt_comp
? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt
) : 0;
2730 pitchAngle
+= DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2731 if (osdConfig()->ahi_reverse_roll
) {
2732 rollAngle
= -rollAngle
;
2734 osdDrawArtificialHorizon(osdDisplayPort
, osdGetDisplayPortCanvas(),
2735 OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), rollAngle
, pitchAngle
);
2736 osdDrawSingleElement(OSD_HORIZON_SIDEBARS
);
2737 osdDrawSingleElement(OSD_CROSSHAIRS
);
2742 case OSD_HORIZON_SIDEBARS
:
2744 osdDrawSidebars(osdDisplayPort
, osdGetDisplayPortCanvas());
2748 #if defined(USE_BARO) || defined(USE_GPS)
2751 float zvel
= getEstimatedActualVelocity(Z
);
2752 osdDrawVario(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), zvel
);
2758 int16_t value
= getEstimatedActualVelocity(Z
);
2760 switch ((osd_unit_e
)osdConfig()->units
) {
2763 case OSD_UNIT_IMPERIAL
:
2764 // Convert to centifeet/s
2765 value
= CENTIMETERS_TO_CENTIFEET(value
);
2769 // Convert to centi-100feet/min
2770 value
= CENTIMETERS_TO_FEET(value
* 60);
2774 case OSD_UNIT_METRIC_MPH
:
2776 case OSD_UNIT_METRIC
:
2782 osdFormatCentiNumber(buff
, value
, 0, 1, 0, 3, false);
2787 case OSD_CLIMB_EFFICIENCY
:
2789 // amperage is in centi amps (10mA), vertical speed is in cms/s. We want
2790 // Ah/dist only to show when vertical speed > 1m/s.
2791 static pt1Filter_t veFilterState
;
2792 static timeUs_t vEfficiencyUpdated
= 0;
2794 timeUs_t currentTimeUs
= micros();
2795 timeDelta_t vEfficiencyTimeDelta
= cmpTimeUs(currentTimeUs
, vEfficiencyUpdated
);
2796 if (getEstimatedActualVelocity(Z
) > 0) {
2797 if (vEfficiencyTimeDelta
>= EFFICIENCY_UPDATE_INTERVAL
) {
2798 // Centiamps (kept for osdFormatCentiNumber) / m/s - Will appear as A / m/s in OSD
2799 value
= pt1FilterApply4(&veFilterState
, (float)getAmperage() / (getEstimatedActualVelocity(Z
) / 100.0f
), 1, US2S(vEfficiencyTimeDelta
));
2801 vEfficiencyUpdated
= currentTimeUs
;
2803 value
= veFilterState
.state
;
2806 bool efficiencyValid
= (value
> 0) && (getEstimatedActualVelocity(Z
) > 100);
2807 switch (osdConfig()->units
) {
2812 case OSD_UNIT_IMPERIAL
:
2814 if (efficiencyValid
) {
2815 osdFormatCentiNumber(buff
, (value
* METERS_PER_FOOT
), 1, 2, 2, 3, false);
2816 tfp_sprintf(buff
+ strlen(buff
), "%c%c", SYM_AH_V_FT_0
, SYM_AH_V_FT_1
);
2818 buff
[0] = buff
[1] = buff
[2] = '-';
2819 buff
[3] = SYM_AH_V_FT_0
;
2820 buff
[4] = SYM_AH_V_FT_1
;
2824 case OSD_UNIT_METRIC_MPH
:
2826 case OSD_UNIT_METRIC
:
2828 if (efficiencyValid
) {
2829 osdFormatCentiNumber(buff
, value
, 1, 2, 2, 3, false);
2830 tfp_sprintf(buff
+ strlen(buff
), "%c%c", SYM_AH_V_M_0
, SYM_AH_V_M_1
);
2832 buff
[0] = buff
[1] = buff
[2] = '-';
2833 buff
[3] = SYM_AH_V_M_0
;
2834 buff
[4] = SYM_AH_V_M_1
;
2841 case OSD_GLIDE_TIME_REMAINING
:
2843 uint16_t glideTime
= osdGetRemainingGlideTime();
2844 buff
[0] = SYM_GLIDE_MINS
;
2845 if (glideTime
> 0) {
2846 // Maximum value we can show in minutes is 99 minutes and 59 seconds. It is extremely unlikely that glide
2847 // time will be longer than 99 minutes. If it is, it will show 99:^^
2848 if (glideTime
> (99 * 60) + 59) {
2849 tfp_sprintf(buff
+ 1, "%02d:", (int)(glideTime
/ 60));
2850 buff
[4] = SYM_DECORATION
;
2851 buff
[5] = SYM_DECORATION
;
2853 tfp_sprintf(buff
+ 1, "%02d:%02d", (int)(glideTime
/ 60), (int)(glideTime
% 60));
2856 tfp_sprintf(buff
+ 1, "%s", "--:--");
2861 case OSD_GLIDE_RANGE
:
2863 uint16_t glideSeconds
= osdGetRemainingGlideTime();
2864 buff
[0] = SYM_GLIDE_DIST
;
2865 if (glideSeconds
> 0) {
2866 uint32_t glideRangeCM
= glideSeconds
* gpsSol
.groundSpeed
;
2867 osdFormatDistanceSymbol(buff
+ 1, glideRangeCM
, 0);
2869 tfp_sprintf(buff
+ 1, "%s%c", "---", SYM_BLANK
);
2876 case OSD_SWITCH_INDICATOR_0
:
2877 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name
, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channel
- 1), buff
);
2880 case OSD_SWITCH_INDICATOR_1
:
2881 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name
, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channel
- 1), buff
);
2884 case OSD_SWITCH_INDICATOR_2
:
2885 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name
, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channel
- 1), buff
);
2888 case OSD_SWITCH_INDICATOR_3
:
2889 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name
, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channel
- 1), buff
);
2892 case OSD_PAN_SERVO_CENTRED
:
2894 int16_t panOffset
= osdGetPanServoOffset();
2895 const timeMs_t panServoTimeNow
= millis();
2896 static timeMs_t panServoTimeOffCentre
= 0;
2898 if (panOffset
< 0) {
2899 if (osdConfig()->pan_servo_offcentre_warning
!= 0 && panOffset
>= -osdConfig()->pan_servo_offcentre_warning
) {
2900 if (panServoTimeOffCentre
== 0) {
2901 panServoTimeOffCentre
= panServoTimeNow
;
2902 } else if (panServoTimeNow
>= (panServoTimeOffCentre
+ 10000 )) {
2903 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2906 panServoTimeOffCentre
= 0;
2909 if (osdConfig()->pan_servo_indicator_show_degrees
) {
2910 tfp_sprintf(buff
, "%3d%c", -panOffset
, SYM_DEGREES
);
2911 displayWriteWithAttr(osdDisplayPort
, elemPosX
+1, elemPosY
, buff
, elemAttr
);
2913 displayWriteCharWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, SYM_SERVO_PAN_IS_OFFSET_R
, elemAttr
);
2914 } else if (panOffset
> 0) {
2915 if (osdConfig()->pan_servo_offcentre_warning
!= 0 && panOffset
<= osdConfig()->pan_servo_offcentre_warning
) {
2916 if (panServoTimeOffCentre
== 0) {
2917 panServoTimeOffCentre
= panServoTimeNow
;
2918 } else if (panServoTimeNow
>= (panServoTimeOffCentre
+ 10000 )) {
2919 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2922 panServoTimeOffCentre
= 0;
2925 if (osdConfig()->pan_servo_indicator_show_degrees
) {
2926 tfp_sprintf(buff
, "%3d%c", panOffset
, SYM_DEGREES
);
2927 displayWriteWithAttr(osdDisplayPort
, elemPosX
+1, elemPosY
, buff
, elemAttr
);
2929 displayWriteCharWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, SYM_SERVO_PAN_IS_OFFSET_L
, elemAttr
);
2931 panServoTimeOffCentre
= 0;
2933 if (osdConfig()->pan_servo_indicator_show_degrees
) {
2934 tfp_sprintf(buff
, "%3d%c", panOffset
, SYM_DEGREES
);
2935 displayWriteWithAttr(osdDisplayPort
, elemPosX
+1, elemPosY
, buff
, elemAttr
);
2937 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_SERVO_PAN_IS_CENTRED
);
2944 case OSD_ACTIVE_PROFILE
:
2945 tfp_sprintf(buff
, "%c%u", SYM_PROFILE
, (getConfigProfile() + 1));
2946 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
2950 osdDisplayFlightPIDValues(elemPosX
, elemPosY
, "ROL", PID_ROLL
, ADJUSTMENT_ROLL_P
, ADJUSTMENT_ROLL_I
, ADJUSTMENT_ROLL_D
, ADJUSTMENT_ROLL_FF
);
2953 case OSD_PITCH_PIDS
:
2954 osdDisplayFlightPIDValues(elemPosX
, elemPosY
, "PIT", PID_PITCH
, ADJUSTMENT_PITCH_P
, ADJUSTMENT_PITCH_I
, ADJUSTMENT_PITCH_D
, ADJUSTMENT_PITCH_FF
);
2958 osdDisplayFlightPIDValues(elemPosX
, elemPosY
, "YAW", PID_YAW
, ADJUSTMENT_YAW_P
, ADJUSTMENT_YAW_I
, ADJUSTMENT_YAW_D
, ADJUSTMENT_YAW_FF
);
2961 case OSD_LEVEL_PIDS
:
2962 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "LEV", PID_LEVEL
, ADJUSTMENT_LEVEL_P
, ADJUSTMENT_LEVEL_I
, ADJUSTMENT_LEVEL_D
);
2965 case OSD_POS_XY_PIDS
:
2966 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "PXY", PID_POS_XY
, ADJUSTMENT_POS_XY_P
, ADJUSTMENT_POS_XY_I
, ADJUSTMENT_POS_XY_D
);
2969 case OSD_POS_Z_PIDS
:
2970 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "PZ", PID_POS_Z
, ADJUSTMENT_POS_Z_P
, ADJUSTMENT_POS_Z_I
, ADJUSTMENT_POS_Z_D
);
2973 case OSD_VEL_XY_PIDS
:
2974 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "VXY", PID_VEL_XY
, ADJUSTMENT_VEL_XY_P
, ADJUSTMENT_VEL_XY_I
, ADJUSTMENT_VEL_XY_D
);
2977 case OSD_VEL_Z_PIDS
:
2978 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "VZ", PID_VEL_Z
, ADJUSTMENT_VEL_Z_P
, ADJUSTMENT_VEL_Z_I
, ADJUSTMENT_VEL_Z_D
);
2982 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "HP", 0, pidBank()->pid
[PID_HEADING
].P
, 3, 0, ADJUSTMENT_HEADING_P
);
2985 case OSD_BOARD_ALIGN_ROLL
:
2986 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees
), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT
);
2989 case OSD_BOARD_ALIGN_PITCH
:
2990 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees
), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT
);
2994 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "EXP", 0, currentControlRateProfile
->stabilized
.rcExpo8
, 3, 0, ADJUSTMENT_RC_EXPO
);
2997 case OSD_RC_YAW_EXPO
:
2998 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "YEX", 0, currentControlRateProfile
->stabilized
.rcYawExpo8
, 3, 0, ADJUSTMENT_RC_YAW_EXPO
);
3001 case OSD_THROTTLE_EXPO
:
3002 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "TEX", 0, currentControlRateProfile
->throttle
.rcExpo8
, 3, 0, ADJUSTMENT_THROTTLE_EXPO
);
3005 case OSD_PITCH_RATE
:
3006 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "SPR");
3008 elemAttr
= TEXT_ATTRIBUTES_NONE
;
3009 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->stabilized
.rates
[FD_PITCH
]);
3010 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE
))
3011 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3012 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
3016 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "SRR");
3018 elemAttr
= TEXT_ATTRIBUTES_NONE
;
3019 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->stabilized
.rates
[FD_ROLL
]);
3020 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE
))
3021 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3022 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
3026 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "SYR", 0, currentControlRateProfile
->stabilized
.rates
[FD_YAW
], 3, 0, ADJUSTMENT_YAW_RATE
);
3029 case OSD_MANUAL_RC_EXPO
:
3030 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "MEX", 0, currentControlRateProfile
->manual
.rcExpo8
, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO
);
3033 case OSD_MANUAL_RC_YAW_EXPO
:
3034 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "MYX", 0, currentControlRateProfile
->manual
.rcYawExpo8
, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO
);
3037 case OSD_MANUAL_PITCH_RATE
:
3038 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "MPR");
3040 elemAttr
= TEXT_ATTRIBUTES_NONE
;
3041 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->manual
.rates
[FD_PITCH
]);
3042 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE
))
3043 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3044 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
3047 case OSD_MANUAL_ROLL_RATE
:
3048 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "MRR");
3050 elemAttr
= TEXT_ATTRIBUTES_NONE
;
3051 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->manual
.rates
[FD_ROLL
]);
3052 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE
))
3053 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3054 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
3057 case OSD_MANUAL_YAW_RATE
:
3058 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "MYR", 0, currentControlRateProfile
->stabilized
.rates
[FD_YAW
], 3, 0, ADJUSTMENT_YAW_RATE
);
3061 case OSD_NAV_FW_CRUISE_THR
:
3062 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "CRZ", 0, currentBatteryProfile
->nav
.fw
.cruise_throttle
, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR
);
3065 case OSD_NAV_FW_PITCH2THR
:
3066 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "P2T", 0, currentBatteryProfile
->nav
.fw
.pitch_to_throttle
, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR
);
3069 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE
:
3070 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "0TP", 0, (float)navConfig()->fw
.minThrottleDownPitchAngle
/ 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE
);
3073 case OSD_FW_ALT_PID_OUTPUTS
:
3075 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
3076 osdFormatPidControllerOutput(buff
, "PZO", &nav_pids
->fw_alt
, 10, true); // display requested pitch degrees
3080 case OSD_FW_POS_PID_OUTPUTS
:
3082 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers(); // display requested roll degrees
3083 osdFormatPidControllerOutput(buff
, "PXYO", &nav_pids
->fw_nav
, 1, true);
3087 case OSD_MC_VEL_Z_PID_OUTPUTS
:
3089 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
3090 osdFormatPidControllerOutput(buff
, "VZO", &nav_pids
->vel
[Z
], 100, false); // display throttle adjustment µs
3094 case OSD_MC_VEL_X_PID_OUTPUTS
:
3096 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
3097 osdFormatPidControllerOutput(buff
, "VXO", &nav_pids
->vel
[X
], 100, false); // display requested acceleration cm/s^2
3101 case OSD_MC_VEL_Y_PID_OUTPUTS
:
3103 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
3104 osdFormatPidControllerOutput(buff
, "VYO", &nav_pids
->vel
[Y
], 100, false); // display requested acceleration cm/s^2
3108 case OSD_MC_POS_XYZ_P_OUTPUTS
:
3110 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
3111 strcpy(buff
, "POSO ");
3112 // display requested velocity cm/s
3113 tfp_sprintf(buff
+ 5, "%4d", (int)lrintf(nav_pids
->pos
[X
].output_constrained
* 100));
3115 tfp_sprintf(buff
+ 10, "%4d", (int)lrintf(nav_pids
->pos
[Y
].output_constrained
* 100));
3117 tfp_sprintf(buff
+ 15, "%4d", (int)lrintf(nav_pids
->pos
[Z
].output_constrained
* 100));
3124 bool kiloWatt
= osdFormatCentiNumber(buff
, getPower(), 1000, 2, 2, 3, false);
3125 buff
[3] = kiloWatt
? SYM_KILOWATT
: SYM_WATT
;
3128 uint8_t current_alarm
= osdConfig()->current_alarm
;
3129 if ((current_alarm
> 0) && ((getAmperage() / 100.0f
) > current_alarm
)) {
3130 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3140 if (pitotIsHealthy())
3142 const float airspeed_estimate
= getAirspeedEstimate();
3143 osdFormatVelocityStr(buff
+ 1, airspeed_estimate
, false, false);
3144 if ((osdConfig()->airspeed_alarm_min
!= 0 && airspeed_estimate
< osdConfig()->airspeed_alarm_min
) ||
3145 (osdConfig()->airspeed_alarm_max
!= 0 && airspeed_estimate
> osdConfig()->airspeed_alarm_max
)) {
3146 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3151 strcpy(buff
+ 1, " X!");
3152 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3160 case OSD_AIR_MAX_SPEED
:
3165 osdFormatVelocityStr(buff
+ 2, stats
.max_air_speed
, false, false);
3174 // RTC not configured will show 00:00
3175 dateTime_t dateTime
;
3176 rtcGetDateTimeLocal(&dateTime
);
3177 buff
[0] = SYM_CLOCK
;
3178 tfp_sprintf(buff
+ 1, "%02u:%02u:%02u", dateTime
.hours
, dateTime
.minutes
, dateTime
.seconds
);
3184 elemAttr
= osdGetSystemMessage(buff
, OSD_MESSAGE_LENGTH
, true);
3190 tfp_sprintf(buff
, "INAV %s", FC_VERSION_STRING
);
3191 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
3195 case OSD_MAIN_BATT_CELL_VOLTAGE
:
3197 uint8_t base_digits
= 3U;
3198 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
3199 if(isDJICompatibleVideoSystem(osdConfig())) {
3200 base_digits
= 4U; // Add extra digit to account for decimal point taking an extra character space
3203 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatteryRawAverageCellVoltage(), base_digits
, 2);
3207 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE
:
3209 uint8_t base_digits
= 3U;
3210 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it
3211 if(isDJICompatibleVideoSystem(osdConfig())) {
3212 base_digits
= 4U; // Add extra digit to account for decimal point taking an extra character space
3215 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatterySagCompensatedAverageCellVoltage(), base_digits
, 2);
3219 case OSD_SCALED_THROTTLE_POS
:
3221 osdFormatThrottlePosition(buff
, true, &elemAttr
);
3227 buff
[0] = SYM_HEADING
;
3228 if (osdIsHeadingValid()) {
3229 int16_t h
= DECIDEGREES_TO_DEGREES(osdGetHeading());
3233 tfp_sprintf(&buff
[1], "%3d", h
);
3235 buff
[1] = buff
[2] = buff
[3] = '-';
3237 buff
[4] = SYM_DEGREES
;
3242 case OSD_HEADING_GRAPH
:
3244 if (osdIsHeadingValid()) {
3245 osdDrawHeadingGraph(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), osdGetHeading());
3248 buff
[0] = buff
[2] = buff
[4] = buff
[6] = buff
[8] = SYM_HEADING_LINE
;
3249 buff
[1] = buff
[3] = buff
[5] = buff
[7] = SYM_HEADING_DIVIDED_LINE
;
3250 buff
[OSD_HEADING_GRAPH_WIDTH
] = '\0';
3255 case OSD_EFFICIENCY_MAH_PER_KM
:
3257 // amperage is in centi amps, speed is in cms/s. We want
3258 // mah/km. Only show when ground speed > 1m/s.
3259 static pt1Filter_t eFilterState
;
3260 static timeUs_t efficiencyUpdated
= 0;
3262 bool moreThanAh
= false;
3263 timeUs_t currentTimeUs
= micros();
3264 timeDelta_t efficiencyTimeDelta
= cmpTimeUs(currentTimeUs
, efficiencyUpdated
);
3265 uint8_t digits
= 3U;
3266 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
3267 if (isDJICompatibleVideoSystem(osdConfig())) {
3268 // Increase number of digits so values above 99 don't get scaled by osdFormatCentiNumber
3273 #ifdef USE_GPS_FIX_ESTIMATION
3274 || STATE(GPS_ESTIMATED_FIX
)
3276 ) && gpsSol
.groundSpeed
> 0) {
3277 if (efficiencyTimeDelta
>= EFFICIENCY_UPDATE_INTERVAL
) {
3278 value
= pt1FilterApply4(&eFilterState
, ((float)getAmperage() / gpsSol
.groundSpeed
) / 0.0036f
,
3279 1, US2S(efficiencyTimeDelta
));
3281 efficiencyUpdated
= currentTimeUs
;
3283 value
= eFilterState
.state
;
3286 bool efficiencyValid
= (value
> 0) && (gpsSol
.groundSpeed
> 100);
3287 switch (osdConfig()->units
) {
3290 case OSD_UNIT_IMPERIAL
:
3291 moreThanAh
= osdFormatCentiNumber(buff
, value
* METERS_PER_MILE
/ 10, 1000, 0, 2, digits
, false);
3293 tfp_sprintf(buff
+ strlen(buff
), "%c%c", SYM_MAH_MI_0
, SYM_MAH_MI_1
);
3295 tfp_sprintf(buff
+ strlen(buff
), "%c", SYM_AH_MI
);
3297 if (!efficiencyValid
) {
3298 buff
[0] = buff
[1] = buff
[2] = buff
[3] = '-';
3299 buff
[digits
] = SYM_MAH_MI_0
; // This will overwrite the "-" at buff[3] if not in DJICOMPAT mode
3300 buff
[digits
+ 1] = SYM_MAH_MI_1
;
3301 buff
[digits
+ 2] = '\0';
3305 moreThanAh
= osdFormatCentiNumber(buff
, value
* METERS_PER_NAUTICALMILE
/ 10, 1000, 0, 2, digits
, false);
3307 tfp_sprintf(buff
+ strlen(buff
), "%c%c", SYM_MAH_NM_0
, SYM_MAH_NM_1
);
3309 tfp_sprintf(buff
+ strlen(buff
), "%c", SYM_AH_NM
);
3311 if (!efficiencyValid
) {
3312 buff
[0] = buff
[1] = buff
[2] = buff
[3] = '-';
3313 buff
[digits
] = SYM_MAH_NM_0
;
3314 buff
[digits
+ 1] = SYM_MAH_NM_1
;
3315 buff
[digits
+ 2] = '\0';
3318 case OSD_UNIT_METRIC_MPH
:
3320 case OSD_UNIT_METRIC
:
3321 moreThanAh
= osdFormatCentiNumber(buff
, value
* 100, 1000, 0, 2, digits
, false);
3323 tfp_sprintf(buff
+ strlen(buff
), "%c%c", SYM_MAH_KM_0
, SYM_MAH_KM_1
);
3325 tfp_sprintf(buff
+ strlen(buff
), "%c", SYM_AH_KM
);
3327 if (!efficiencyValid
) {
3328 buff
[0] = buff
[1] = buff
[2] = buff
[3] = '-';
3329 buff
[digits
] = SYM_MAH_KM_0
;
3330 buff
[digits
+ 1] = SYM_MAH_KM_1
;
3331 buff
[digits
+ 2] = '\0';
3338 case OSD_EFFICIENCY_WH_PER_KM
:
3340 // amperage is in centi amps, speed is in cms/s. We want
3341 // mWh/km. Only show when ground speed > 1m/s.
3342 static pt1Filter_t eFilterState
;
3343 static timeUs_t efficiencyUpdated
= 0;
3345 timeUs_t currentTimeUs
= micros();
3346 timeDelta_t efficiencyTimeDelta
= cmpTimeUs(currentTimeUs
, efficiencyUpdated
);
3348 #ifdef USE_GPS_FIX_ESTIMATION
3349 || STATE(GPS_ESTIMATED_FIX
)
3351 ) && gpsSol
.groundSpeed
> 0) {
3352 if (efficiencyTimeDelta
>= EFFICIENCY_UPDATE_INTERVAL
) {
3353 value
= pt1FilterApply4(&eFilterState
, ((float)getPower() / gpsSol
.groundSpeed
) / 0.0036f
,
3354 1, US2S(efficiencyTimeDelta
));
3356 efficiencyUpdated
= currentTimeUs
;
3358 value
= eFilterState
.state
;
3361 bool efficiencyValid
= (value
> 0) && (gpsSol
.groundSpeed
> 100);
3362 switch (osdConfig()->units
) {
3365 case OSD_UNIT_IMPERIAL
:
3366 osdFormatCentiNumber(buff
, value
* METERS_PER_MILE
/ 10000, 0, 2, 0, 3, false);
3367 buff
[3] = SYM_WH_MI
;
3370 osdFormatCentiNumber(buff
, value
* METERS_PER_NAUTICALMILE
/ 10000, 0, 2, 0, 3, false);
3371 buff
[3] = SYM_WH_NM
;
3373 case OSD_UNIT_METRIC_MPH
:
3375 case OSD_UNIT_METRIC
:
3376 osdFormatCentiNumber(buff
, value
/ 10, 0, 2, 0, 3, false);
3377 buff
[3] = SYM_WH_KM
;
3381 if (!efficiencyValid
) {
3382 buff
[0] = buff
[1] = buff
[2] = '-';
3389 buff
[0] = SYM_GFORCE
;
3390 osdFormatCentiNumber(buff
+ 1, GForce
, 0, 2, 0, 3, false);
3391 if (GForce
> osdConfig()->gforce_alarm
* 100) {
3392 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3401 float GForceValue
= GForceAxis
[item
- OSD_GFORCE_X
];
3402 buff
[0] = SYM_GFORCE_X
+ item
- OSD_GFORCE_X
;
3403 osdFormatCentiNumber(buff
+ 1, GForceValue
, 0, 2, 0, 4, false);
3404 if ((GForceValue
< osdConfig()->gforce_axis_alarm_min
* 100) || (GForceValue
> osdConfig()->gforce_axis_alarm_max
* 100)) {
3405 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3412 * Longest representable string is -2147483648 does not fit in the screen.
3413 * Only 7 digits for negative and 8 digits for positive values allowed
3415 for (uint8_t bufferIndex
= 0; bufferIndex
< DEBUG32_VALUE_COUNT
; ++elemPosY
, bufferIndex
+= 2) {
3418 "[%u]=%8ld [%u]=%8ld",
3420 (long)constrain(debug
[bufferIndex
], -9999999, 99999999),
3422 (long)constrain(debug
[bufferIndex
+1], -9999999, 99999999)
3424 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
3429 case OSD_IMU_TEMPERATURE
:
3431 int16_t temperature
;
3432 const bool valid
= getIMUTemperature(&temperature
);
3433 osdDisplayTemperature(elemPosX
, elemPosY
, SYM_IMU_TEMP
, NULL
, valid
, temperature
, osdConfig()->imu_temp_alarm_min
, osdConfig()->imu_temp_alarm_max
);
3437 case OSD_BARO_TEMPERATURE
:
3439 int16_t temperature
;
3440 const bool valid
= getBaroTemperature(&temperature
);
3441 osdDisplayTemperature(elemPosX
, elemPosY
, SYM_BARO_TEMP
, NULL
, valid
, temperature
, osdConfig()->imu_temp_alarm_min
, osdConfig()->imu_temp_alarm_max
);
3445 #ifdef USE_TEMPERATURE_SENSOR
3446 case OSD_TEMP_SENSOR_0_TEMPERATURE
:
3447 case OSD_TEMP_SENSOR_1_TEMPERATURE
:
3448 case OSD_TEMP_SENSOR_2_TEMPERATURE
:
3449 case OSD_TEMP_SENSOR_3_TEMPERATURE
:
3450 case OSD_TEMP_SENSOR_4_TEMPERATURE
:
3451 case OSD_TEMP_SENSOR_5_TEMPERATURE
:
3452 case OSD_TEMP_SENSOR_6_TEMPERATURE
:
3453 case OSD_TEMP_SENSOR_7_TEMPERATURE
:
3455 osdDisplayTemperatureSensor(elemPosX
, elemPosY
, item
- OSD_TEMP_SENSOR_0_TEMPERATURE
);
3458 #endif /* ifdef USE_TEMPERATURE_SENSOR */
3460 case OSD_WIND_SPEED_HORIZONTAL
:
3461 #ifdef USE_WIND_ESTIMATOR
3463 bool valid
= isEstimatedWindSpeedValid();
3464 float horizontalWindSpeed
;
3466 horizontalWindSpeed
= getEstimatedHorizontalWindSpeed(&angle
);
3467 int16_t windDirection
= osdGetHeadingAngle( CENTIDEGREES_TO_DEGREES((int)angle
) - DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
) + 22);
3468 buff
[0] = SYM_WIND_HORIZONTAL
;
3469 buff
[1] = SYM_DECORATION
+ (windDirection
*2 / 90);
3470 osdFormatWindSpeedStr(buff
+ 2, horizontalWindSpeed
, valid
);
3477 case OSD_WIND_SPEED_VERTICAL
:
3478 #ifdef USE_WIND_ESTIMATOR
3480 buff
[0] = SYM_WIND_VERTICAL
;
3481 buff
[1] = SYM_BLANK
;
3482 bool valid
= isEstimatedWindSpeedValid();
3483 float verticalWindSpeed
;
3484 verticalWindSpeed
= -getEstimatedWindSpeed(Z
); //from NED to NEU
3485 if (verticalWindSpeed
< 0) {
3486 buff
[1] = SYM_AH_DECORATION_DOWN
;
3487 verticalWindSpeed
= -verticalWindSpeed
;
3489 buff
[1] = SYM_AH_DECORATION_UP
;
3491 osdFormatWindSpeedStr(buff
+ 2, verticalWindSpeed
, valid
);
3500 STATIC_ASSERT(GPS_DEGREES_DIVIDER
== OLC_DEG_MULTIPLIER
, invalid_olc_deg_multiplier
);
3501 int digits
= osdConfig()->plus_code_digits
;
3502 int digitsRemoved
= osdConfig()->plus_code_short
* 2;
3504 #ifdef USE_GPS_FIX_ESTIMATION
3505 || STATE(GPS_ESTIMATED_FIX
)
3508 olc_encode(gpsSol
.llh
.lat
, gpsSol
.llh
.lon
, digits
, buff
, sizeof(buff
));
3510 // +codes with > 8 digits have a + at the 9th digit
3511 // and we only support 10 and up.
3512 memset(buff
, '-', digits
+ 1);
3514 buff
[digits
+ 1] = '\0';
3516 // Optionally trim digits from the left
3517 memmove(buff
, buff
+digitsRemoved
, strlen(buff
) + digitsRemoved
);
3518 buff
[digits
+ 1 - digitsRemoved
] = '\0';
3525 buff
[0] = SYM_AZIMUTH
;
3526 if (osdIsHeadingValid()) {
3527 int16_t h
= GPS_directionToHome
;
3536 tfp_sprintf(&buff
[1], "%3d", h
);
3538 buff
[1] = buff
[2] = buff
[3] = '-';
3540 buff
[4] = SYM_DEGREES
;
3548 int scaleUnitDivisor
;
3553 switch (osdConfig()->units
) {
3556 case OSD_UNIT_IMPERIAL
:
3557 scaleToUnit
= 100 / 1609.3440f
; // scale to 0.01mi for osdFormatCentiNumber()
3558 scaleUnitDivisor
= 0;
3559 symUnscaled
= SYM_MI
;
3564 scaleToUnit
= 100 / 1852.0010f
; // scale to 0.01mi for osdFormatCentiNumber()
3565 scaleUnitDivisor
= 0;
3566 symUnscaled
= SYM_NM
;
3571 case OSD_UNIT_METRIC_MPH
:
3573 case OSD_UNIT_METRIC
:
3574 scaleToUnit
= 100; // scale to cm for osdFormatCentiNumber()
3575 scaleUnitDivisor
= 1000; // Convert to km when scale gets bigger than 999m
3576 symUnscaled
= SYM_M
;
3581 buff
[0] = SYM_SCALE
;
3582 if (osdMapData
.scale
> 0) {
3583 bool scaled
= osdFormatCentiNumber(&buff
[1], osdMapData
.scale
* scaleToUnit
, scaleUnitDivisor
, maxDecimals
, 2, 3, false);
3584 buff
[4] = scaled
? symScaled
: symUnscaled
;
3585 // Make sure this is cleared if the map stops being drawn
3586 osdMapData
.scale
= 0;
3588 memset(&buff
[1], '-', 4);
3593 case OSD_MAP_REFERENCE
:
3595 char referenceSymbol
;
3596 if (osdMapData
.referenceSymbol
) {
3597 referenceSymbol
= osdMapData
.referenceSymbol
;
3598 // Make sure this is cleared if the map stops being drawn
3599 osdMapData
.referenceSymbol
= 0;
3601 referenceSymbol
= '-';
3603 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_DECORATION
);
3604 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
+ 1, referenceSymbol
);
3610 osdFormatGVar(buff
, 0);
3615 osdFormatGVar(buff
, 1);
3620 osdFormatGVar(buff
, 2);
3625 osdFormatGVar(buff
, 3);
3629 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3632 const char *source_text
= IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE
) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
3633 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE
) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3634 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, source_text
, elemAttr
);
3639 #if defined(USE_ESC_SENSOR)
3642 escSensorData_t
* escSensor
= escSensorGetData();
3643 if (escSensor
&& escSensor
->dataAge
<= ESC_DATA_MAX_AGE
) {
3644 osdFormatRpm(buff
, escSensor
->rpm
);
3647 osdFormatRpm(buff
, 0);
3651 case OSD_ESC_TEMPERATURE
:
3653 escSensorData_t
* escSensor
= escSensorGetData();
3654 bool escTemperatureValid
= escSensor
&& escSensor
->dataAge
<= ESC_DATA_MAX_AGE
;
3655 osdDisplayTemperature(elemPosX
, elemPosY
, SYM_ESC_TEMP
, NULL
, escTemperatureValid
, (escSensor
->temperature
)*10, osdConfig()->esc_temp_alarm_min
, osdConfig()->esc_temp_alarm_max
);
3662 textAttributes_t attr
;
3664 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "TPA");
3665 attr
= TEXT_ATTRIBUTES_NONE
;
3666 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->throttle
.dynPID
);
3667 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA
)) {
3668 TEXT_ATTRIBUTES_ADD_BLINK(attr
);
3670 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 5, elemPosY
, buff
, attr
);
3672 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
+ 1, "BP");
3673 attr
= TEXT_ATTRIBUTES_NONE
;
3674 tfp_sprintf(buff
, "%4d", currentControlRateProfile
->throttle
.pa_breakpoint
);
3675 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT
)) {
3676 TEXT_ATTRIBUTES_ADD_BLINK(attr
);
3678 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
+ 1, buff
, attr
);
3682 case OSD_TPA_TIME_CONSTANT
:
3684 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "TPA TC", 0, currentControlRateProfile
->throttle
.fixedWingTauMs
, 4, 0, ADJUSTMENT_FW_TPA_TIME_CONSTANT
);
3687 case OSD_FW_LEVEL_TRIM
:
3689 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "LEVEL", 0, getFixedWingLevelTrim(), 3, 1, ADJUSTMENT_FW_LEVEL_TRIM
);
3693 case OSD_NAV_FW_CONTROL_SMOOTHNESS
:
3695 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "CTL S", 0, navConfig()->fw
.control_smoothness
, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS
);
3698 #ifdef USE_MULTI_MISSION
3699 case OSD_NAV_WP_MULTI_MISSION_INDEX
:
3701 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "WP NO", 0, navConfig()->general
.waypoint_multi_mission_index
, 1, 0, ADJUSTMENT_NAV_WP_MULTI_MISSION_INDEX
);
3707 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION
)) {
3709 switch (posControl
.wpMissionPlannerStatus
) {
3711 strcpy(buf
, "WAIT");
3714 strcpy(buf
, "SAVE");
3717 strcpy(buf
, " OK ");
3720 strcpy(buf
, "FULL");
3722 tfp_sprintf(buff
, "%s>%2uWP", buf
, posControl
.wpPlannerActiveWPIndex
);
3723 } else if (posControl
.wpPlannerActiveWPIndex
){
3724 tfp_sprintf(buff
, "PLAN>%2uWP", posControl
.waypointCount
); // mission planner mision active
3726 #ifdef USE_MULTI_MISSION
3728 if (ARMING_FLAG(ARMED
) && !(IS_RC_MODE_ACTIVE(BOXCHANGEMISSION
) && posControl
.multiMissionCount
> 1)){
3729 // Limit field size when Armed, only show selected mission
3730 tfp_sprintf(buff
, "M%u ", posControl
.loadedMultiMissionIndex
);
3731 } else if (posControl
.multiMissionCount
) {
3732 if (navConfig()->general
.waypoint_multi_mission_index
!= posControl
.loadedMultiMissionIndex
) {
3733 tfp_sprintf(buff
, "M%u/%u>LOAD", navConfig()->general
.waypoint_multi_mission_index
, posControl
.multiMissionCount
);
3735 if (posControl
.waypointListValid
&& posControl
.waypointCount
> 0) {
3736 tfp_sprintf(buff
, "M%u/%u>%2uWP", posControl
.loadedMultiMissionIndex
, posControl
.multiMissionCount
, posControl
.waypointCount
);
3738 tfp_sprintf(buff
, "M0/%u> 0WP", posControl
.multiMissionCount
);
3741 } else { // no multi mission loaded - show active WP count from other source
3742 tfp_sprintf(buff
, "WP CNT>%2u", posControl
.waypointCount
);
3746 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
3750 #ifdef USE_POWER_LIMITS
3751 case OSD_PLIMIT_REMAINING_BURST_TIME
:
3752 osdFormatCentiNumber(buff
, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3, false);
3757 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT
:
3758 if (currentBatteryProfile
->powerLimits
.continuousCurrent
) {
3759 osdFormatCentiNumber(buff
, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3, false);
3763 if (powerLimiterIsLimitingCurrent()) {
3764 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3770 case OSD_PLIMIT_ACTIVE_POWER_LIMIT
:
3772 if (currentBatteryProfile
->powerLimits
.continuousPower
) {
3773 bool kiloWatt
= osdFormatCentiNumber(buff
, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3, false);
3774 buff
[3] = kiloWatt
? SYM_KILOWATT
: SYM_WATT
;
3777 if (powerLimiterIsLimitingPower()) {
3778 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3784 #endif // USE_POWER_LIMITS
3785 case OSD_MULTI_FUNCTION
:
3787 // message shown infrequently so only write when needed
3788 static bool clearMultiFunction
= true;
3789 elemAttr
= osdGetMultiFunctionMessage(buff
);
3791 if (clearMultiFunction
) {
3792 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
3793 clearMultiFunction
= false;
3797 clearMultiFunction
= true;
3805 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
3809 uint8_t osdIncElementIndex(uint8_t elementIndex
)
3813 if (elementIndex
== OSD_ARTIFICIAL_HORIZON
) { // always drawn last so skip
3817 #ifndef USE_TEMPERATURE_SENSOR
3818 if (elementIndex
== OSD_TEMP_SENSOR_0_TEMPERATURE
) {
3819 elementIndex
= OSD_ALTITUDE_MSL
;
3823 if (!(feature(FEATURE_VBAT
) && feature(FEATURE_CURRENT_METER
))) {
3824 if (elementIndex
== OSD_POWER
) {
3825 elementIndex
= OSD_GPS_LON
;
3827 if (elementIndex
== OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
) {
3828 elementIndex
= OSD_LEVEL_PIDS
;
3830 #ifdef USE_POWER_LIMITS
3831 if (elementIndex
== OSD_PLIMIT_REMAINING_BURST_TIME
) {
3832 elementIndex
= OSD_GLIDESLOPE
;
3837 #ifndef USE_POWER_LIMITS
3838 if (elementIndex
== OSD_PLIMIT_REMAINING_BURST_TIME
) {
3839 elementIndex
= OSD_GLIDESLOPE
;
3843 if (!feature(FEATURE_CURRENT_METER
)) {
3844 if (elementIndex
== OSD_CURRENT_DRAW
) {
3845 elementIndex
= OSD_GPS_SPEED
;
3847 if (elementIndex
== OSD_EFFICIENCY_MAH_PER_KM
) {
3848 elementIndex
= OSD_BATTERY_REMAINING_PERCENT
;
3850 if (elementIndex
== OSD_EFFICIENCY_WH_PER_KM
) {
3851 elementIndex
= OSD_TRIP_DIST
;
3853 if (elementIndex
== OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH
) {
3854 elementIndex
= OSD_HOME_HEADING_ERROR
;
3856 if (elementIndex
== OSD_CLIMB_EFFICIENCY
) {
3857 elementIndex
= OSD_NAV_WP_MULTI_MISSION_INDEX
;
3861 if (!STATE(ESC_SENSOR_ENABLED
)) {
3862 if (elementIndex
== OSD_ESC_RPM
) {
3863 elementIndex
= OSD_AZIMUTH
;
3867 if (!feature(FEATURE_GPS
)) {
3868 if (elementIndex
== OSD_GPS_HDOP
|| elementIndex
== OSD_TRIP_DIST
|| elementIndex
== OSD_3D_SPEED
|| elementIndex
== OSD_MISSION
||
3869 elementIndex
== OSD_AZIMUTH
|| elementIndex
== OSD_BATTERY_REMAINING_CAPACITY
|| elementIndex
== OSD_EFFICIENCY_MAH_PER_KM
) {
3872 if (elementIndex
== OSD_HEADING_GRAPH
&& !sensors(SENSOR_MAG
)) {
3873 elementIndex
= feature(FEATURE_CURRENT_METER
) ? OSD_WH_DRAWN
: OSD_BATTERY_REMAINING_PERCENT
;
3875 if (elementIndex
== OSD_EFFICIENCY_WH_PER_KM
) {
3876 elementIndex
= OSD_ATTITUDE_PITCH
;
3878 if (elementIndex
== OSD_GPS_SPEED
) {
3879 elementIndex
= OSD_ALTITUDE
;
3881 if (elementIndex
== OSD_GPS_LON
) {
3882 elementIndex
= sensors(SENSOR_MAG
) ? OSD_HEADING
: OSD_VARIO
;
3884 if (elementIndex
== OSD_MAP_NORTH
) {
3885 elementIndex
= feature(FEATURE_CURRENT_METER
) ? OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
: OSD_LEVEL_PIDS
;
3887 if (elementIndex
== OSD_PLUS_CODE
) {
3888 elementIndex
= OSD_GFORCE
;
3890 if (elementIndex
== OSD_GLIDESLOPE
) {
3891 elementIndex
= OSD_AIR_MAX_SPEED
;
3893 if (elementIndex
== OSD_GLIDE_RANGE
) {
3894 elementIndex
= feature(FEATURE_CURRENT_METER
) ? OSD_CLIMB_EFFICIENCY
: OSD_PILOT_NAME
;
3896 if (elementIndex
== OSD_NAV_WP_MULTI_MISSION_INDEX
) {
3897 elementIndex
= OSD_PILOT_NAME
;
3901 if (!sensors(SENSOR_ACC
)) {
3902 if (elementIndex
== OSD_CROSSHAIRS
) {
3903 elementIndex
= OSD_ONTIME
;
3905 if (elementIndex
== OSD_GFORCE
) {
3906 elementIndex
= OSD_RC_SOURCE
;
3910 if (elementIndex
== OSD_ITEM_COUNT
) {
3913 return elementIndex
;
3916 void osdDrawNextElement(void)
3918 static uint8_t elementIndex
= 0;
3919 // Flag for end of loop, also prevents infinite loop when no elements are enabled
3920 uint8_t index
= elementIndex
;
3922 elementIndex
= osdIncElementIndex(elementIndex
);
3923 } while (!osdDrawSingleElement(elementIndex
) && index
!= elementIndex
);
3925 // Draw artificial horizon + tracking telemetry last
3926 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON
);
3927 if (osdConfig()->telemetry
>0){
3928 osdDisplayTelemetry();
3932 PG_RESET_TEMPLATE(osdConfig_t
, osdConfig
,
3933 .rssi_alarm
= SETTING_OSD_RSSI_ALARM_DEFAULT
,
3934 .time_alarm
= SETTING_OSD_TIME_ALARM_DEFAULT
,
3935 .alt_alarm
= SETTING_OSD_ALT_ALARM_DEFAULT
,
3936 .dist_alarm
= SETTING_OSD_DIST_ALARM_DEFAULT
,
3937 .neg_alt_alarm
= SETTING_OSD_NEG_ALT_ALARM_DEFAULT
,
3938 .current_alarm
= SETTING_OSD_CURRENT_ALARM_DEFAULT
,
3939 .imu_temp_alarm_min
= SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT
,
3940 .imu_temp_alarm_max
= SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT
,
3941 .esc_temp_alarm_min
= SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT
,
3942 .esc_temp_alarm_max
= SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT
,
3943 .gforce_alarm
= SETTING_OSD_GFORCE_ALARM_DEFAULT
,
3944 .gforce_axis_alarm_min
= SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT
,
3945 .gforce_axis_alarm_max
= SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT
,
3947 .baro_temp_alarm_min
= SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT
,
3948 .baro_temp_alarm_max
= SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT
,
3951 .adsb_distance_warning
= SETTING_OSD_ADSB_DISTANCE_WARNING_DEFAULT
,
3952 .adsb_distance_alert
= SETTING_OSD_ADSB_DISTANCE_ALERT_DEFAULT
,
3953 .adsb_ignore_plane_above_me_limit
= SETTING_OSD_ADSB_IGNORE_PLANE_ABOVE_ME_LIMIT_DEFAULT
,
3955 #ifdef USE_SERIALRX_CRSF
3956 .snr_alarm
= SETTING_OSD_SNR_ALARM_DEFAULT
,
3957 .crsf_lq_format
= SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT
,
3958 .link_quality_alarm
= SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT
,
3959 .rssi_dbm_alarm
= SETTING_OSD_RSSI_DBM_ALARM_DEFAULT
,
3960 .rssi_dbm_max
= SETTING_OSD_RSSI_DBM_MAX_DEFAULT
,
3961 .rssi_dbm_min
= SETTING_OSD_RSSI_DBM_MIN_DEFAULT
,
3963 #ifdef USE_TEMPERATURE_SENSOR
3964 .temp_label_align
= SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT
,
3967 .airspeed_alarm_min
= SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT
,
3968 .airspeed_alarm_max
= SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT
,
3970 #ifndef DISABLE_MSP_DJI_COMPAT
3971 .highlight_djis_missing_characters
= SETTING_OSD_HIGHLIGHT_DJIS_MISSING_FONT_SYMBOLS_DEFAULT
,
3974 .video_system
= SETTING_OSD_VIDEO_SYSTEM_DEFAULT
,
3975 .row_shiftdown
= SETTING_OSD_ROW_SHIFTDOWN_DEFAULT
,
3976 .msp_displayport_fullframe_interval
= SETTING_OSD_MSP_DISPLAYPORT_FULLFRAME_INTERVAL_DEFAULT
,
3978 .ahi_reverse_roll
= SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT
,
3979 .ahi_max_pitch
= SETTING_OSD_AHI_MAX_PITCH_DEFAULT
,
3980 .crosshairs_style
= SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT
,
3981 .horizon_offset
= SETTING_OSD_HORIZON_OFFSET_DEFAULT
,
3982 .camera_uptilt
= SETTING_OSD_CAMERA_UPTILT_DEFAULT
,
3983 .ahi_camera_uptilt_comp
= SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT
,
3984 .camera_fov_h
= SETTING_OSD_CAMERA_FOV_H_DEFAULT
,
3985 .camera_fov_v
= SETTING_OSD_CAMERA_FOV_V_DEFAULT
,
3986 .hud_margin_h
= SETTING_OSD_HUD_MARGIN_H_DEFAULT
,
3987 .hud_margin_v
= SETTING_OSD_HUD_MARGIN_V_DEFAULT
,
3988 .hud_homing
= SETTING_OSD_HUD_HOMING_DEFAULT
,
3989 .hud_homepoint
= SETTING_OSD_HUD_HOMEPOINT_DEFAULT
,
3990 .hud_radar_disp
= SETTING_OSD_HUD_RADAR_DISP_DEFAULT
,
3991 .hud_radar_range_min
= SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT
,
3992 .hud_radar_range_max
= SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT
,
3993 .hud_radar_alt_difference_display_time
= SETTING_OSD_HUD_RADAR_ALT_DIFFERENCE_DISPLAY_TIME_DEFAULT
,
3994 .hud_radar_distance_display_time
= SETTING_OSD_HUD_RADAR_DISTANCE_DISPLAY_TIME_DEFAULT
,
3995 .hud_wp_disp
= SETTING_OSD_HUD_WP_DISP_DEFAULT
,
3996 .left_sidebar_scroll
= SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT
,
3997 .right_sidebar_scroll
= SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT
,
3998 .sidebar_scroll_arrows
= SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT
,
3999 .sidebar_horizontal_offset
= SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT
,
4000 .left_sidebar_scroll_step
= SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT
,
4001 .right_sidebar_scroll_step
= SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT
,
4002 .sidebar_height
= SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT
,
4003 .ahi_pitch_interval
= SETTING_OSD_AHI_PITCH_INTERVAL_DEFAULT
,
4004 .osd_home_position_arm_screen
= SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT
,
4005 .pan_servo_index
= SETTING_OSD_PAN_SERVO_INDEX_DEFAULT
,
4006 .pan_servo_pwm2centideg
= SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT
,
4007 .pan_servo_offcentre_warning
= SETTING_OSD_PAN_SERVO_OFFCENTRE_WARNING_DEFAULT
,
4008 .pan_servo_indicator_show_degrees
= SETTING_OSD_PAN_SERVO_INDICATOR_SHOW_DEGREES_DEFAULT
,
4009 .esc_rpm_precision
= SETTING_OSD_ESC_RPM_PRECISION_DEFAULT
,
4010 .mAh_precision
= SETTING_OSD_MAH_PRECISION_DEFAULT
,
4011 .osd_switch_indicator0_name
= SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT
,
4012 .osd_switch_indicator0_channel
= SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNEL_DEFAULT
,
4013 .osd_switch_indicator1_name
= SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT
,
4014 .osd_switch_indicator1_channel
= SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNEL_DEFAULT
,
4015 .osd_switch_indicator2_name
= SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT
,
4016 .osd_switch_indicator2_channel
= SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNEL_DEFAULT
,
4017 .osd_switch_indicator3_name
= SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT
,
4018 .osd_switch_indicator3_channel
= SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNEL_DEFAULT
,
4019 .osd_switch_indicators_align_left
= SETTING_OSD_SWITCH_INDICATORS_ALIGN_LEFT_DEFAULT
,
4020 .system_msg_display_time
= SETTING_OSD_SYSTEM_MSG_DISPLAY_TIME_DEFAULT
,
4021 .units
= SETTING_OSD_UNITS_DEFAULT
,
4022 .main_voltage_decimals
= SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT
,
4023 .use_pilot_logo
= SETTING_OSD_USE_PILOT_LOGO_DEFAULT
,
4024 .inav_to_pilot_logo_spacing
= SETTING_OSD_INAV_TO_PILOT_LOGO_SPACING_DEFAULT
,
4025 .arm_screen_display_time
= SETTING_OSD_ARM_SCREEN_DISPLAY_TIME_DEFAULT
,
4027 #ifdef USE_WIND_ESTIMATOR
4028 .estimations_wind_compensation
= SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT
,
4031 .coordinate_digits
= SETTING_OSD_COORDINATE_DIGITS_DEFAULT
,
4033 .osd_failsafe_switch_layout
= SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT
,
4035 .plus_code_digits
= SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT
,
4036 .plus_code_short
= SETTING_OSD_PLUS_CODE_SHORT_DEFAULT
,
4038 .ahi_width
= SETTING_OSD_AHI_WIDTH_DEFAULT
,
4039 .ahi_height
= SETTING_OSD_AHI_HEIGHT_DEFAULT
,
4040 .ahi_vertical_offset
= SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT
,
4041 .ahi_bordered
= SETTING_OSD_AHI_BORDERED_DEFAULT
,
4042 .ahi_style
= SETTING_OSD_AHI_STYLE_DEFAULT
,
4044 .force_grid
= SETTING_OSD_FORCE_GRID_DEFAULT
,
4046 .stats_energy_unit
= SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT
,
4047 .stats_page_auto_swap_time
= SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT
,
4048 .stats_show_metric_efficiency
= SETTING_OSD_STATS_SHOW_METRIC_EFFICIENCY_DEFAULT
,
4050 .radar_peers_display_time
= SETTING_OSD_RADAR_PEERS_DISPLAY_TIME_DEFAULT
4053 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t
*osdLayoutsConfig
)
4055 osdLayoutsConfig
->item_pos
[0][OSD_ALTITUDE
] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG
;
4056 osdLayoutsConfig
->item_pos
[0][OSD_MAIN_BATT_VOLTAGE
] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG
;
4057 osdLayoutsConfig
->item_pos
[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
] = OSD_POS(12, 1);
4059 osdLayoutsConfig
->item_pos
[0][OSD_RSSI_VALUE
] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG
;
4061 osdLayoutsConfig
->item_pos
[0][OSD_HOME_DIST
] = OSD_POS(1, 1);
4062 osdLayoutsConfig
->item_pos
[0][OSD_TRIP_DIST
] = OSD_POS(1, 2);
4063 osdLayoutsConfig
->item_pos
[0][OSD_ODOMETER
] = OSD_POS(1, 3);
4064 osdLayoutsConfig
->item_pos
[0][OSD_MAIN_BATT_CELL_VOLTAGE
] = OSD_POS(12, 1);
4065 osdLayoutsConfig
->item_pos
[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE
] = OSD_POS(12, 1);
4066 osdLayoutsConfig
->item_pos
[0][OSD_GPS_SPEED
] = OSD_POS(23, 1);
4067 osdLayoutsConfig
->item_pos
[0][OSD_3D_SPEED
] = OSD_POS(23, 1);
4068 osdLayoutsConfig
->item_pos
[0][OSD_GLIDESLOPE
] = OSD_POS(23, 2);
4070 osdLayoutsConfig
->item_pos
[0][OSD_THROTTLE_POS
] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG
;
4071 osdLayoutsConfig
->item_pos
[0][OSD_SCALED_THROTTLE_POS
] = OSD_POS(6, 2);
4072 osdLayoutsConfig
->item_pos
[0][OSD_HEADING
] = OSD_POS(12, 2);
4073 osdLayoutsConfig
->item_pos
[0][OSD_GROUND_COURSE
] = OSD_POS(12, 3);
4074 osdLayoutsConfig
->item_pos
[0][OSD_COURSE_HOLD_ERROR
] = OSD_POS(12, 2);
4075 osdLayoutsConfig
->item_pos
[0][OSD_COURSE_HOLD_ADJUSTMENT
] = OSD_POS(12, 2);
4076 osdLayoutsConfig
->item_pos
[0][OSD_CROSS_TRACK_ERROR
] = OSD_POS(12, 3);
4077 osdLayoutsConfig
->item_pos
[0][OSD_HEADING_GRAPH
] = OSD_POS(18, 2);
4078 osdLayoutsConfig
->item_pos
[0][OSD_CURRENT_DRAW
] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG
;
4079 osdLayoutsConfig
->item_pos
[0][OSD_MAH_DRAWN
] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG
;
4080 osdLayoutsConfig
->item_pos
[0][OSD_WH_DRAWN
] = OSD_POS(1, 5);
4081 osdLayoutsConfig
->item_pos
[0][OSD_BATTERY_REMAINING_CAPACITY
] = OSD_POS(1, 6);
4082 osdLayoutsConfig
->item_pos
[0][OSD_BATTERY_REMAINING_PERCENT
] = OSD_POS(1, 7);
4083 osdLayoutsConfig
->item_pos
[0][OSD_POWER_SUPPLY_IMPEDANCE
] = OSD_POS(1, 8);
4085 osdLayoutsConfig
->item_pos
[0][OSD_EFFICIENCY_MAH_PER_KM
] = OSD_POS(1, 5);
4086 osdLayoutsConfig
->item_pos
[0][OSD_EFFICIENCY_WH_PER_KM
] = OSD_POS(1, 5);
4088 osdLayoutsConfig
->item_pos
[0][OSD_ATTITUDE_ROLL
] = OSD_POS(1, 7);
4089 osdLayoutsConfig
->item_pos
[0][OSD_ATTITUDE_PITCH
] = OSD_POS(1, 8);
4091 // avoid OSD_VARIO under OSD_CROSSHAIRS
4092 osdLayoutsConfig
->item_pos
[0][OSD_VARIO
] = OSD_POS(23, 5);
4093 // OSD_VARIO_NUM at the right of OSD_VARIO
4094 osdLayoutsConfig
->item_pos
[0][OSD_VARIO_NUM
] = OSD_POS(24, 7);
4095 osdLayoutsConfig
->item_pos
[0][OSD_HOME_DIR
] = OSD_POS(14, 11);
4096 osdLayoutsConfig
->item_pos
[0][OSD_ARTIFICIAL_HORIZON
] = OSD_POS(8, 6);
4097 osdLayoutsConfig
->item_pos
[0][OSD_HORIZON_SIDEBARS
] = OSD_POS(8, 6);
4099 osdLayoutsConfig
->item_pos
[0][OSD_CRAFT_NAME
] = OSD_POS(20, 2);
4100 osdLayoutsConfig
->item_pos
[0][OSD_PILOT_NAME
] = OSD_POS(20, 3);
4101 osdLayoutsConfig
->item_pos
[0][OSD_PILOT_LOGO
] = OSD_POS(20, 3);
4102 osdLayoutsConfig
->item_pos
[0][OSD_VTX_CHANNEL
] = OSD_POS(8, 6);
4104 #ifdef USE_SERIALRX_CRSF
4105 osdLayoutsConfig
->item_pos
[0][OSD_CRSF_RSSI_DBM
] = OSD_POS(23, 12);
4106 osdLayoutsConfig
->item_pos
[0][OSD_CRSF_LQ
] = OSD_POS(23, 11);
4107 osdLayoutsConfig
->item_pos
[0][OSD_CRSF_SNR_DB
] = OSD_POS(24, 9);
4108 osdLayoutsConfig
->item_pos
[0][OSD_CRSF_TX_POWER
] = OSD_POS(24, 10);
4111 osdLayoutsConfig
->item_pos
[0][OSD_ONTIME
] = OSD_POS(23, 8);
4112 osdLayoutsConfig
->item_pos
[0][OSD_FLYTIME
] = OSD_POS(23, 9);
4113 osdLayoutsConfig
->item_pos
[0][OSD_ONTIME_FLYTIME
] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG
;
4114 osdLayoutsConfig
->item_pos
[0][OSD_RTC_TIME
] = OSD_POS(23, 12);
4115 osdLayoutsConfig
->item_pos
[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH
] = OSD_POS(23, 7);
4116 osdLayoutsConfig
->item_pos
[0][OSD_REMAINING_DISTANCE_BEFORE_RTH
] = OSD_POS(23, 6);
4118 osdLayoutsConfig
->item_pos
[0][OSD_MISSION
] = OSD_POS(0, 10);
4119 osdLayoutsConfig
->item_pos
[0][OSD_GPS_SATS
] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG
;
4120 osdLayoutsConfig
->item_pos
[0][OSD_GPS_HDOP
] = OSD_POS(0, 10);
4122 osdLayoutsConfig
->item_pos
[0][OSD_GPS_LAT
] = OSD_POS(0, 12);
4123 // Put this on top of the latitude, since it's very unlikely
4124 // that users will want to use both at the same time.
4125 osdLayoutsConfig
->item_pos
[0][OSD_PLUS_CODE
] = OSD_POS(0, 12);
4126 osdLayoutsConfig
->item_pos
[0][OSD_FLYMODE
] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG
;
4127 osdLayoutsConfig
->item_pos
[0][OSD_GPS_LON
] = OSD_POS(18, 12);
4129 osdLayoutsConfig
->item_pos
[0][OSD_AZIMUTH
] = OSD_POS(2, 12);
4131 osdLayoutsConfig
->item_pos
[0][OSD_ROLL_PIDS
] = OSD_POS(2, 10);
4132 osdLayoutsConfig
->item_pos
[0][OSD_PITCH_PIDS
] = OSD_POS(2, 11);
4133 osdLayoutsConfig
->item_pos
[0][OSD_YAW_PIDS
] = OSD_POS(2, 12);
4134 osdLayoutsConfig
->item_pos
[0][OSD_LEVEL_PIDS
] = OSD_POS(2, 12);
4135 osdLayoutsConfig
->item_pos
[0][OSD_POS_XY_PIDS
] = OSD_POS(2, 12);
4136 osdLayoutsConfig
->item_pos
[0][OSD_POS_Z_PIDS
] = OSD_POS(2, 12);
4137 osdLayoutsConfig
->item_pos
[0][OSD_VEL_XY_PIDS
] = OSD_POS(2, 12);
4138 osdLayoutsConfig
->item_pos
[0][OSD_VEL_Z_PIDS
] = OSD_POS(2, 12);
4139 osdLayoutsConfig
->item_pos
[0][OSD_HEADING_P
] = OSD_POS(2, 12);
4140 osdLayoutsConfig
->item_pos
[0][OSD_BOARD_ALIGN_ROLL
] = OSD_POS(2, 10);
4141 osdLayoutsConfig
->item_pos
[0][OSD_BOARD_ALIGN_PITCH
] = OSD_POS(2, 11);
4142 osdLayoutsConfig
->item_pos
[0][OSD_RC_EXPO
] = OSD_POS(2, 12);
4143 osdLayoutsConfig
->item_pos
[0][OSD_RC_YAW_EXPO
] = OSD_POS(2, 12);
4144 osdLayoutsConfig
->item_pos
[0][OSD_THROTTLE_EXPO
] = OSD_POS(2, 12);
4145 osdLayoutsConfig
->item_pos
[0][OSD_PITCH_RATE
] = OSD_POS(2, 12);
4146 osdLayoutsConfig
->item_pos
[0][OSD_ROLL_RATE
] = OSD_POS(2, 12);
4147 osdLayoutsConfig
->item_pos
[0][OSD_YAW_RATE
] = OSD_POS(2, 12);
4148 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_RC_EXPO
] = OSD_POS(2, 12);
4149 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_RC_YAW_EXPO
] = OSD_POS(2, 12);
4150 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_PITCH_RATE
] = OSD_POS(2, 12);
4151 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_ROLL_RATE
] = OSD_POS(2, 12);
4152 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_YAW_RATE
] = OSD_POS(2, 12);
4153 osdLayoutsConfig
->item_pos
[0][OSD_NAV_FW_CRUISE_THR
] = OSD_POS(2, 12);
4154 osdLayoutsConfig
->item_pos
[0][OSD_NAV_FW_PITCH2THR
] = OSD_POS(2, 12);
4155 osdLayoutsConfig
->item_pos
[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE
] = OSD_POS(2, 12);
4156 osdLayoutsConfig
->item_pos
[0][OSD_FW_ALT_PID_OUTPUTS
] = OSD_POS(2, 12);
4157 osdLayoutsConfig
->item_pos
[0][OSD_FW_POS_PID_OUTPUTS
] = OSD_POS(2, 12);
4158 osdLayoutsConfig
->item_pos
[0][OSD_MC_VEL_X_PID_OUTPUTS
] = OSD_POS(2, 12);
4159 osdLayoutsConfig
->item_pos
[0][OSD_MC_VEL_Y_PID_OUTPUTS
] = OSD_POS(2, 12);
4160 osdLayoutsConfig
->item_pos
[0][OSD_MC_VEL_Z_PID_OUTPUTS
] = OSD_POS(2, 12);
4161 osdLayoutsConfig
->item_pos
[0][OSD_MC_POS_XYZ_P_OUTPUTS
] = OSD_POS(2, 12);
4163 osdLayoutsConfig
->item_pos
[0][OSD_POWER
] = OSD_POS(15, 1);
4165 osdLayoutsConfig
->item_pos
[0][OSD_IMU_TEMPERATURE
] = OSD_POS(19, 2);
4166 osdLayoutsConfig
->item_pos
[0][OSD_BARO_TEMPERATURE
] = OSD_POS(19, 3);
4167 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_0_TEMPERATURE
] = OSD_POS(19, 4);
4168 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_1_TEMPERATURE
] = OSD_POS(19, 5);
4169 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_2_TEMPERATURE
] = OSD_POS(19, 6);
4170 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_3_TEMPERATURE
] = OSD_POS(19, 7);
4171 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_4_TEMPERATURE
] = OSD_POS(19, 8);
4172 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_5_TEMPERATURE
] = OSD_POS(19, 9);
4173 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_6_TEMPERATURE
] = OSD_POS(19, 10);
4174 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_7_TEMPERATURE
] = OSD_POS(19, 11);
4176 osdLayoutsConfig
->item_pos
[0][OSD_AIR_SPEED
] = OSD_POS(3, 5);
4177 osdLayoutsConfig
->item_pos
[0][OSD_WIND_SPEED_HORIZONTAL
] = OSD_POS(3, 6);
4178 osdLayoutsConfig
->item_pos
[0][OSD_WIND_SPEED_VERTICAL
] = OSD_POS(3, 7);
4180 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE
] = OSD_POS(12, 4);
4181 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE_X
] = OSD_POS(12, 5);
4182 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE_Y
] = OSD_POS(12, 6);
4183 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE_Z
] = OSD_POS(12, 7);
4185 osdLayoutsConfig
->item_pos
[0][OSD_VTX_POWER
] = OSD_POS(3, 5);
4187 osdLayoutsConfig
->item_pos
[0][OSD_GVAR_0
] = OSD_POS(1, 1);
4188 osdLayoutsConfig
->item_pos
[0][OSD_GVAR_1
] = OSD_POS(1, 2);
4189 osdLayoutsConfig
->item_pos
[0][OSD_GVAR_2
] = OSD_POS(1, 3);
4190 osdLayoutsConfig
->item_pos
[0][OSD_GVAR_3
] = OSD_POS(1, 4);
4192 osdLayoutsConfig
->item_pos
[0][OSD_MULTI_FUNCTION
] = OSD_POS(1, 4);
4194 osdLayoutsConfig
->item_pos
[0][OSD_SWITCH_INDICATOR_0
] = OSD_POS(2, 7);
4195 osdLayoutsConfig
->item_pos
[0][OSD_SWITCH_INDICATOR_1
] = OSD_POS(2, 8);
4196 osdLayoutsConfig
->item_pos
[0][OSD_SWITCH_INDICATOR_2
] = OSD_POS(2, 9);
4197 osdLayoutsConfig
->item_pos
[0][OSD_SWITCH_INDICATOR_3
] = OSD_POS(2, 10);
4199 osdLayoutsConfig
->item_pos
[0][OSD_ADSB_WARNING
] = OSD_POS(2, 7);
4200 osdLayoutsConfig
->item_pos
[0][OSD_ADSB_INFO
] = OSD_POS(2, 8);
4201 #if defined(USE_ESC_SENSOR)
4202 osdLayoutsConfig
->item_pos
[0][OSD_ESC_RPM
] = OSD_POS(1, 2);
4203 osdLayoutsConfig
->item_pos
[0][OSD_ESC_TEMPERATURE
] = OSD_POS(1, 3);
4206 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
4207 osdLayoutsConfig
->item_pos
[0][OSD_RC_SOURCE
] = OSD_POS(3, 4);
4210 #ifdef USE_POWER_LIMITS
4211 osdLayoutsConfig
->item_pos
[0][OSD_PLIMIT_REMAINING_BURST_TIME
] = OSD_POS(3, 4);
4212 osdLayoutsConfig
->item_pos
[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT
] = OSD_POS(3, 5);
4213 osdLayoutsConfig
->item_pos
[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT
] = OSD_POS(3, 6);
4217 osdLayoutsConfig
->item_pos
[0][OSD_BLACKBOX
] = OSD_POS(2, 10);
4220 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
4221 osdLayoutsConfig
->item_pos
[0][OSD_MESSAGES
] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG
;
4223 for (unsigned ii
= 1; ii
< OSD_LAYOUT_COUNT
; ii
++) {
4224 for (unsigned jj
= 0; jj
< ARRAYLEN(osdLayoutsConfig
->item_pos
[0]); jj
++) {
4225 osdLayoutsConfig
->item_pos
[ii
][jj
] = osdLayoutsConfig
->item_pos
[0][jj
] & ~OSD_VISIBLE_FLAG
;
4231 * @brief Draws the INAV and/or pilot logos on the display
4233 * @param singular If true, only one logo will be drawn. If false, both logos will be drawn, separated by osdConfig()->inav_to_pilot_logo_spacing characters
4234 * @param row The row number to start drawing the logos. If not singular, both logos are drawn on the same rows.
4235 * @return uint8_t The row number after the logo(s).
4237 uint8_t drawLogos(bool singular
, uint8_t row
) {
4238 uint8_t logoRow
= row
;
4239 uint8_t logoColOffset
= 0;
4240 bool usePilotLogo
= (osdConfig()->use_pilot_logo
&& osdDisplayIsHD());
4241 bool useINAVLogo
= (singular
&& !usePilotLogo
) || !singular
;
4243 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is in use, the pilot logo cannot be used, due to font issues.
4244 if (isDJICompatibleVideoSystem(osdConfig())) {
4245 usePilotLogo
= false;
4246 useINAVLogo
= false;
4250 uint8_t logoSpacing
= osdConfig()->inav_to_pilot_logo_spacing
;
4252 if (logoSpacing
> 0 && ((osdDisplayPort
->cols
% 2) != (logoSpacing
% 2))) {
4253 logoSpacing
++; // Add extra 1 character space between logos, if the odd/even of the OSD cols doesn't match the odd/even of the logo spacing
4257 if (usePilotLogo
&& !singular
) {
4258 logoColOffset
= ((osdDisplayPort
->cols
- (SYM_LOGO_WIDTH
* 2)) - logoSpacing
) / 2;
4260 logoColOffset
= floorf((osdDisplayPort
->cols
- SYM_LOGO_WIDTH
) / 2.0f
);
4265 unsigned logo_c
= SYM_LOGO_START
;
4266 uint8_t logo_x
= logoColOffset
;
4267 for (uint8_t lRow
= 0; lRow
< SYM_LOGO_HEIGHT
; lRow
++) {
4268 for (uint8_t lCol
= 0; lCol
< SYM_LOGO_WIDTH
; lCol
++) {
4269 displayWriteChar(osdDisplayPort
, logo_x
+ lCol
, logoRow
, logo_c
++);
4275 // Draw the pilot logo
4277 unsigned logo_c
= SYM_PILOT_LOGO_LRG_START
;
4281 logo_x
= logoColOffset
;
4283 logo_x
= logoColOffset
+ SYM_LOGO_WIDTH
+ logoSpacing
;
4286 for (uint8_t lRow
= 0; lRow
< SYM_LOGO_HEIGHT
; lRow
++) {
4287 for (uint8_t lCol
= 0; lCol
< SYM_LOGO_WIDTH
; lCol
++) {
4288 displayWriteChar(osdDisplayPort
, logo_x
+ lCol
, logoRow
, logo_c
++);
4294 if (!usePilotLogo
&& !useINAVLogo
) {
4295 logoRow
+= SYM_LOGO_HEIGHT
;
4302 uint8_t drawStat_Stats(uint8_t statNameX
, uint8_t row
, uint8_t statValueX
, bool isBootStats
)
4304 uint8_t buffLen
= 0;
4305 char string_buffer
[osdDisplayPort
->cols
- statValueX
];
4307 if (statsConfig()->stats_enabled
) {
4309 displayWrite(osdDisplayPort
, statNameX
, row
, "ODOMETER:");
4311 displayWrite(osdDisplayPort
, statNameX
, row
, "ODOMETER");
4313 switch (osdConfig()->units
) {
4316 case OSD_UNIT_IMPERIAL
:
4318 tfp_sprintf(string_buffer
, "%5d", (uint16_t)(statsConfig()->stats_total_dist
/ METERS_PER_MILE
));
4321 uint16_t statTotalDist
= (uint16_t)(statsConfig()->stats_total_dist
/ METERS_PER_MILE
);
4322 tfp_sprintf(string_buffer
, ": %d", statTotalDist
);
4323 buffLen
= 3 + sizeof(statTotalDist
);
4326 string_buffer
[buffLen
++] = SYM_MI
;
4331 tfp_sprintf(string_buffer
, "%5d", (uint16_t)(statsConfig()->stats_total_dist
/ METERS_PER_NAUTICALMILE
));
4334 uint16_t statTotalDist
= (uint16_t)(statsConfig()->stats_total_dist
/ METERS_PER_NAUTICALMILE
);
4335 tfp_sprintf(string_buffer
, ": %d", statTotalDist
);
4336 buffLen
= 3 + sizeof(statTotalDist
);
4339 string_buffer
[buffLen
++] = SYM_NM
;
4341 case OSD_UNIT_METRIC_MPH
:
4343 case OSD_UNIT_METRIC
:
4345 tfp_sprintf(string_buffer
, "%5d", (uint16_t)(statsConfig()->stats_total_dist
/ METERS_PER_KILOMETER
));
4348 uint16_t statTotalDist
= (uint16_t)(statsConfig()->stats_total_dist
/ METERS_PER_KILOMETER
);
4349 tfp_sprintf(string_buffer
, ": %d", statTotalDist
);
4350 buffLen
= 3 + sizeof(statTotalDist
);
4353 string_buffer
[buffLen
++] = SYM_KM
;
4356 string_buffer
[buffLen
] = '\0';
4357 displayWrite(osdDisplayPort
, statValueX
-(isBootStats
? 5 : 0), row
, string_buffer
);
4360 displayWrite(osdDisplayPort
, statNameX
, ++row
, "TOTAL TIME:");
4362 displayWrite(osdDisplayPort
, statNameX
, ++row
, "TOTAL TIME");
4364 uint32_t tot_mins
= statsConfig()->stats_total_time
/ 60;
4366 tfp_sprintf(string_buffer
, "%d:%02dH:M%c", (int)(tot_mins
/ 60), (int)(tot_mins
% 60), '\0');
4368 tfp_sprintf(string_buffer
, ": %d:%02d H:M%c", (int)(tot_mins
/ 60), (int)(tot_mins
% 60), '\0');
4370 displayWrite(osdDisplayPort
, statValueX
-(isBootStats
? 7 : 0), row
, string_buffer
);
4373 if (feature(FEATURE_VBAT
) && feature(FEATURE_CURRENT_METER
) && statsConfig()->stats_total_energy
) {
4374 uint8_t buffOffset
= 0;
4376 displayWrite(osdDisplayPort
, statNameX
, ++row
, "TOTAL ENERGY:");
4378 displayWrite(osdDisplayPort
, statNameX
, ++row
, "TOTAL ENERGY");
4379 string_buffer
[0] = ':';
4383 osdFormatCentiNumber(string_buffer
+ buffOffset
, statsConfig()->stats_total_energy
/ 10, 0, 2, 0, 6, true);
4384 displayWrite(osdDisplayPort
, statValueX
- (isBootStats
? 6 : 0), row
, string_buffer
);
4385 displayWriteChar(osdDisplayPort
, statValueX
+ (isBootStats
? 0 : 8), row
, SYM_WH
);
4387 char avgEffBuff
[osdDisplayPort
->cols
- statValueX
];
4389 for (uint8_t i
= 0; i
< osdDisplayPort
->cols
- statValueX
; i
++) {
4390 avgEffBuff
[i
] = '\0';
4391 string_buffer
[i
] = '\0';
4394 if (statsConfig()->stats_total_dist
) {
4396 displayWrite(osdDisplayPort
, statNameX
, ++row
, "AVG EFFICIENCY:");
4398 displayWrite(osdDisplayPort
, statNameX
, ++row
, "AVG EFFICIENCY");
4399 strcat(avgEffBuff
, ": ");
4402 float_t avg_efficiency
= MWH_TO_WH(statsConfig()->stats_total_energy
) / METERS_TO_KILOMETERS(statsConfig()->stats_total_dist
); // Wh/km
4403 switch (osdConfig()->units
) {
4406 case OSD_UNIT_IMPERIAL
:
4407 osdFormatCentiNumber(string_buffer
, (int32_t)(avg_efficiency
* METERS_PER_MILE
/ 10), 0, 2, 2, 4, false);
4408 string_buffer
[4] = SYM_WH_MI
;
4411 osdFormatCentiNumber(string_buffer
, (int32_t)(avg_efficiency
* METERS_PER_NAUTICALMILE
/ 10), 0, 2, 2, 4, false);
4412 string_buffer
[4] = SYM_WH_NM
;
4415 case OSD_UNIT_METRIC_MPH
:
4417 case OSD_UNIT_METRIC
:
4418 osdFormatCentiNumber(string_buffer
, (int32_t)(avg_efficiency
* 100), 0, 2, 2, 4, false);
4419 string_buffer
[4] = SYM_WH_KM
;
4424 strcat(avgEffBuff
, string_buffer
);
4426 strcat(avgEffBuff
, osdFormatTrimWhiteSpace(string_buffer
));
4428 strcat(avgEffBuff
, "----");
4431 displayWrite(osdDisplayPort
, statValueX
-(isBootStats
? 4 : 0), row
++, avgEffBuff
);
4438 uint8_t drawStats(uint8_t row
)
4440 uint8_t statNameX
= (osdDisplayPort
->cols
- 22) / 2;
4441 uint8_t statValueX
= statNameX
+ 21;
4443 return drawStat_Stats(statNameX
, row
, statValueX
, true);
4447 static void osdSetNextRefreshIn(uint32_t timeMs
)
4449 resumeRefreshAt
= micros() + timeMs
* 1000;
4450 refreshWaitForResumeCmdRelease
= true;
4453 static void osdCompleteAsyncInitialization(void)
4455 if (!displayIsReady(osdDisplayPort
)) {
4456 // Update the display.
4457 // XXX: Rename displayDrawScreen() and associated functions
4458 // to displayUpdate()
4459 displayDrawScreen(osdDisplayPort
);
4463 osdDisplayIsReady
= true;
4465 #if defined(USE_CANVAS)
4466 if (osdConfig()->force_grid
) {
4467 osdDisplayHasCanvas
= false;
4469 osdDisplayHasCanvas
= displayGetCanvas(&osdCanvas
, osdDisplayPort
);
4473 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
4474 displayClearScreen(osdDisplayPort
);
4477 displayFontMetadata_t metadata
;
4478 bool fontHasMetadata
= displayGetFontMetadata(&metadata
, osdDisplayPort
);
4479 LOG_DEBUG(OSD
, "Font metadata version %s: %u (%u chars)",
4480 fontHasMetadata
? "Y" : "N", metadata
.version
, metadata
.charCount
);
4482 if (fontHasMetadata
&& metadata
.charCount
> 256) {
4483 hasExtendedFont
= true;
4485 y
= drawLogos(false, y
);
4487 } else if (!fontHasMetadata
) {
4488 const char *m
= "INVALID FONT";
4489 displayWrite(osdDisplayPort
, OSD_CENTER_S(m
), y
++, m
);
4492 if (fontHasMetadata
&& metadata
.version
< OSD_MIN_FONT_VERSION
) {
4493 const char *m
= "INVALID FONT VERSION";
4494 displayWrite(osdDisplayPort
, OSD_CENTER_S(m
), y
++, m
);
4497 char string_buffer
[30];
4498 tfp_sprintf(string_buffer
, "INAV VERSION: %s", FC_VERSION_STRING
);
4499 uint8_t xPos
= (osdDisplayPort
->cols
- 19) / 2; // Automatically centre, regardless of resolution. In the case of odd number screens, bias to the left.
4500 displayWrite(osdDisplayPort
, xPos
, y
++, string_buffer
);
4502 displayWrite(osdDisplayPort
, xPos
+2, y
++, CMS_STARTUP_HELP_TEXT1
);
4503 displayWrite(osdDisplayPort
, xPos
+6, y
++, CMS_STARTUP_HELP_TEXT2
);
4504 displayWrite(osdDisplayPort
, xPos
+6, y
++, CMS_STARTUP_HELP_TEXT3
);
4511 displayCommitTransaction(osdDisplayPort
);
4512 displayResync(osdDisplayPort
);
4513 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME
);
4516 void osdInit(displayPort_t
*osdDisplayPortToUse
)
4518 if (!osdDisplayPortToUse
)
4521 BUILD_BUG_ON(OSD_POS_MAX
!= OSD_POS(63,63));
4523 osdDisplayPort
= osdDisplayPortToUse
;
4526 cmsDisplayPortRegister(osdDisplayPort
);
4529 armState
= ARMING_FLAG(ARMED
);
4530 osdCompleteAsyncInitialization();
4533 static void osdResetStats(void)
4535 // Reset internal OSD stats
4536 stats
.max_distance
= 0;
4537 stats
.max_current
= 0;
4538 stats
.max_power
= 0;
4539 stats
.max_speed
= 0;
4540 stats
.max_3D_speed
= 0;
4541 stats
.max_air_speed
= 0;
4542 stats
.min_voltage
= 12000;
4543 stats
.min_rssi
= 99;
4545 stats
.min_rssi_dbm
= 0;
4546 stats
.max_altitude
= 0;
4547 stats
.min_sats
= 255;
4549 stats
.min_esc_temp
= 300;
4550 stats
.max_esc_temp
= 0;
4551 stats
.flightStartMAh
= getMAhDrawn();
4552 stats
.flightStartMWh
= getMWhDrawn();
4554 // Reset external stats
4555 posControl
.totalTripDistance
= 0.0f
;
4560 static void osdUpdateStats(void)
4564 if (feature(FEATURE_GPS
)) {
4565 value
= osdGet3DSpeed();
4566 const float airspeed_estimate
= getAirspeedEstimate();
4568 if (stats
.max_3D_speed
< value
)
4569 stats
.max_3D_speed
= value
;
4571 if (stats
.max_speed
< gpsSol
.groundSpeed
)
4572 stats
.max_speed
= gpsSol
.groundSpeed
;
4574 if (stats
.max_air_speed
< airspeed_estimate
)
4575 stats
.max_air_speed
= airspeed_estimate
;
4577 if (stats
.max_distance
< GPS_distanceToHome
)
4578 stats
.max_distance
= GPS_distanceToHome
;
4580 if (stats
.min_sats
> gpsSol
.numSat
)
4581 stats
.min_sats
= gpsSol
.numSat
;
4583 if (stats
.max_sats
< gpsSol
.numSat
)
4584 stats
.max_sats
= gpsSol
.numSat
;
4586 #if defined(USE_ESC_SENSOR)
4587 if (STATE(ESC_SENSOR_ENABLED
)) {
4588 escSensorData_t
* escSensor
= escSensorGetData();
4589 bool escTemperatureValid
= escSensor
&& escSensor
->dataAge
<= ESC_DATA_MAX_AGE
;
4591 if (escTemperatureValid
) {
4592 if (stats
.min_esc_temp
> escSensor
->temperature
)
4593 stats
.min_esc_temp
= escSensor
->temperature
;
4595 if (stats
.max_esc_temp
< escSensor
->temperature
)
4596 stats
.max_esc_temp
= escSensor
->temperature
;
4601 value
= getBatteryVoltage();
4602 if (stats
.min_voltage
> value
)
4603 stats
.min_voltage
= value
;
4605 value
= abs(getAmperage());
4606 if (stats
.max_current
< value
)
4607 stats
.max_current
= value
;
4609 value
= labs(getPower());
4610 if (stats
.max_power
< value
)
4611 stats
.max_power
= value
;
4613 value
= osdConvertRSSI();
4614 if (stats
.min_rssi
> value
)
4615 stats
.min_rssi
= value
;
4617 value
= osdGetCrsfLQ();
4618 if (stats
.min_lq
> value
)
4619 stats
.min_lq
= value
;
4621 if (!failsafeIsReceivingRxData())
4624 value
= osdGetCrsfdBm();
4625 if (stats
.min_rssi_dbm
> value
)
4626 stats
.min_rssi_dbm
= value
;
4628 stats
.max_altitude
= MAX(stats
.max_altitude
, osdGetAltitude());
4631 uint8_t drawStat_FlightTime(uint8_t col
, uint8_t row
, uint8_t statValX
)
4634 displayWrite(osdDisplayPort
, col
, row
, "FLIGHT TIME");
4635 uint16_t flySeconds
= getFlightTime();
4636 uint16_t flyMinutes
= flySeconds
/ 60;
4638 uint16_t flyHours
= flyMinutes
/ 60;
4640 tfp_sprintf(buff
, ": %02u:%02u:%02u", flyHours
, flyMinutes
, flySeconds
);
4641 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4646 uint8_t drawStat_FlightDistance(uint8_t col
, uint8_t row
, uint8_t statValX
)
4650 displayWrite(osdDisplayPort
, col
, row
, "FLIGHT DISTANCE");
4651 tfp_sprintf(buff
, ": ");
4652 osdFormatDistanceStr(buff
+ 2, getTotalTravelDistance());
4653 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4658 uint8_t drawStat_MaxDistanceFromHome(uint8_t col
, uint8_t row
, uint8_t statValX
)
4661 uint8_t valueXOffset
= 0;
4662 if (!osdDisplayIsHD()) {
4663 displayWrite(osdDisplayPort
, col
, row
, "DISTANCE FROM ");
4666 displayWrite(osdDisplayPort
, col
, row
, "MAX DISTANCE FROM ");
4669 displayWriteChar(osdDisplayPort
, col
+ valueXOffset
, row
, SYM_HOME
);
4670 tfp_sprintf(buff
, ": ");
4671 osdFormatDistanceStr(buff
+ 2, stats
.max_distance
* 100);
4672 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4677 uint8_t drawStat_Speed(uint8_t col
, uint8_t row
, uint8_t statValX
)
4681 uint8_t multiValueXOffset
= 0;
4683 displayWrite(osdDisplayPort
, col
, row
, "MAX/AVG SPEED");
4685 osdFormatVelocityStr(buff2
, stats
.max_3D_speed
, true, false);
4686 tfp_sprintf(buff
, ": %s/", osdFormatTrimWhiteSpace(buff2
));
4687 multiValueXOffset
= strlen(buff
);
4688 displayWrite(osdDisplayPort
, statValX
, row
, buff
);
4690 osdGenerateAverageVelocityStr(buff2
);
4691 displayWrite(osdDisplayPort
, statValX
+ multiValueXOffset
, row
++, osdFormatTrimWhiteSpace(buff2
));
4696 uint8_t drawStat_MaximumAltitude(uint8_t col
, uint8_t row
, uint8_t statValX
)
4699 displayWrite(osdDisplayPort
, col
, row
, "MAX ALTITUDE");
4700 tfp_sprintf(buff
, ": ");
4701 osdFormatAltitudeStr(buff
+ 2, stats
.max_altitude
);
4702 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4707 uint8_t drawStat_BatteryVoltage(uint8_t col
, uint8_t row
, uint8_t statValX
)
4710 uint8_t multiValueXOffset
= 0;
4711 if (!osdDisplayIsHD())
4712 displayWrite(osdDisplayPort
, col
, row
, "MIN VOLTS P/C");
4714 displayWrite(osdDisplayPort
, col
, row
, "MIN VOLTS PACK/CELL");
4717 tfp_sprintf(buff
, ": ");
4718 osdFormatCentiNumber(buff
+ 2, stats
.min_voltage
, 0, osdConfig()->main_voltage_decimals
, 0, osdConfig()->main_voltage_decimals
+ 2, false);
4719 strcat(osdFormatTrimWhiteSpace(buff
), "/");
4720 multiValueXOffset
= strlen(buff
);
4722 osdFormatCentiNumber(buff
+ multiValueXOffset
, stats
.min_voltage
/ getBatteryCellCount(), 0, 2, 0, 3, false);
4723 tfp_sprintf(buff
+ strlen(buff
), "%c", SYM_VOLT
);
4725 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4730 uint8_t drawStat_MaximumPowerAndCurrent(uint8_t col
, uint8_t row
, uint8_t statValX
)
4734 tfp_sprintf(outBuff
, ": ");
4735 osdFormatCentiNumber(buff
, stats
.max_current
, 0, 2, 0, 3, false);
4736 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4737 strcat(outBuff
, "/");
4738 bool kiloWatt
= osdFormatCentiNumber(buff
, stats
.max_power
, 1000, 2, 2, 3, false);
4739 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4740 displayWrite(osdDisplayPort
, statValX
, row
, outBuff
);
4743 displayWrite(osdDisplayPort
, col
, row
, "MAX AMPS/K WATTS");
4745 displayWrite(osdDisplayPort
, col
, row
, "MAX AMPS/WATTS");
4750 uint8_t drawStat_UsedEnergy(uint8_t col
, uint8_t row
, uint8_t statValX
)
4754 if (osdDisplayIsHD())
4755 displayWrite(osdDisplayPort
, col
, row
, "USED ENERGY FLT/TOT");
4757 displayWrite(osdDisplayPort
, col
, row
, "USED ENERGY F/T");
4758 tfp_sprintf(buff
, ": ");
4759 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
) {
4760 tfp_sprintf(buff
+ 2, "%d/%d%c", (int)(getMAhDrawn() - stats
.flightStartMAh
), (int)getMAhDrawn(), SYM_MAH
);
4763 osdFormatCentiNumber(preBuff
, (getMWhDrawn() - stats
.flightStartMWh
) / 10, 0, 2, 0, 3, false);
4764 strcat(buff
, osdFormatTrimWhiteSpace(preBuff
));
4766 osdFormatCentiNumber(preBuff
, getMWhDrawn() / 10, 0, 2, 0, 3, false);
4767 strcat(buff
, osdFormatTrimWhiteSpace(preBuff
));
4768 tfp_sprintf(buff
+ strlen(buff
), "%s%c", buff
, SYM_WH
);
4770 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4775 uint8_t drawStat_AverageEfficiency(uint8_t col
, uint8_t row
, uint8_t statValX
, bool forceMetric
)
4779 int32_t totalDistance
= getTotalTravelDistance();
4780 bool moreThanAh
= false;
4781 bool efficiencyValid
= totalDistance
>= 10000;
4783 if (osdDisplayIsHD())
4784 displayWrite(osdDisplayPort
, col
, row
, "AVG EFFICIENCY FLT/TOT");
4786 displayWrite(osdDisplayPort
, col
, row
, "AV EFFICIENCY F/T");
4788 tfp_sprintf(outBuff
, ": ");
4789 uint8_t digits
= 3U; // Total number of digits (including decimal point)
4790 #ifndef DISABLE_MSP_DJI_COMPAT // IF DJICOMPAT is not supported, there's no need to check for it and change the values
4791 if (isDJICompatibleVideoSystem(osdConfig())) {
4792 // Add one digit so no switch to scaled decimal occurs above 99
4797 switch (osdConfig()->units
) {
4800 case OSD_UNIT_IMPERIAL
:
4801 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
) {
4802 if (efficiencyValid
) {
4803 moreThanAh
= osdFormatCentiNumber(buff
, (int32_t)((getMAhDrawn() - stats
.flightStartMAh
) * 10000.0f
* METERS_PER_MILE
/ totalDistance
), 1000, 0, 2, digits
, false);
4804 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4805 if (osdDisplayIsHD()) {
4807 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c%c", SYM_MAH_MI_0
, SYM_MAH_MI_1
);
4809 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", SYM_AH_MI
);
4814 strcat(outBuff
, "/");
4815 moreThanAh
= moreThanAh
|| osdFormatCentiNumber(buff
, (int32_t)(getMAhDrawn() * 10000.0f
* METERS_PER_MILE
/ totalDistance
), 1000, 0, 2, digits
, false);
4816 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4819 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c%c", SYM_MAH_MI_0
, SYM_MAH_MI_1
);
4821 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", SYM_AH_MI
);
4823 tfp_sprintf(outBuff
+ strlen(outBuff
), "---/---%c%c", SYM_MAH_MI_0
, SYM_MAH_MI_1
);
4826 if (efficiencyValid
) {
4827 osdFormatCentiNumber(buff
, (int32_t)((getMWhDrawn() - stats
.flightStartMWh
) * 10.0f
* METERS_PER_MILE
/ totalDistance
), 0, 2, 0, digits
, false);
4828 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4829 strcat(outBuff
, "/");
4830 osdFormatCentiNumber(buff
+ strlen(buff
), (int32_t)(getMWhDrawn() * 10.0f
* METERS_PER_MILE
/ totalDistance
), 0, 2, 0, digits
, false);
4831 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4833 strcat(outBuff
, "---/---");
4835 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", SYM_WH_MI
);
4839 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
) {
4840 if (efficiencyValid
) {
4841 moreThanAh
= osdFormatCentiNumber(buff
, (int32_t)((getMAhDrawn()-stats
.flightStartMAh
) * 10000.0f
* METERS_PER_NAUTICALMILE
/ totalDistance
), 1000, 0, 2, digits
, false);
4842 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4843 if (osdDisplayIsHD()) {
4845 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c%c", SYM_MAH_NM_0
, SYM_MAH_NM_1
);
4847 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", SYM_AH_NM
);
4852 strcat(outBuff
, "/");
4853 moreThanAh
= moreThanAh
|| osdFormatCentiNumber(buff
, (int32_t)(getMAhDrawn() * 10000.0f
* METERS_PER_NAUTICALMILE
/ totalDistance
), 1000, 0, 2, digits
, false);
4854 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4856 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c%c", SYM_MAH_NM_0
, SYM_MAH_NM_1
);
4858 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", SYM_AH_NM
);
4861 tfp_sprintf(outBuff
+ strlen(outBuff
), "---/---%c%c", SYM_MAH_NM_0
, SYM_MAH_NM_1
);
4864 if (efficiencyValid
) {
4865 osdFormatCentiNumber(buff
, (int32_t)((getMWhDrawn()-stats
.flightStartMWh
) * 10.0f
* METERS_PER_NAUTICALMILE
/ totalDistance
), 0, 2, 0, digits
, false);
4866 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4867 strcat(outBuff
, "/");
4868 osdFormatCentiNumber(buff
, (int32_t)(getMWhDrawn() * 10.0f
* METERS_PER_NAUTICALMILE
/ totalDistance
), 0, 2, 0, digits
, false);
4869 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4871 strcat(outBuff
, "---/---");
4873 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", SYM_WH_NM
);
4876 case OSD_UNIT_METRIC_MPH
:
4877 case OSD_UNIT_METRIC
:
4884 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
) {
4885 if (efficiencyValid
) {
4886 moreThanAh
= osdFormatCentiNumber(buff
, (int32_t)((getMAhDrawn() - stats
.flightStartMAh
) * 10000000.0f
/ totalDistance
), 1000, 0, 2, digits
, false);
4887 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4888 if (osdDisplayIsHD()) {
4890 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c%c", SYM_MAH_KM_0
, SYM_MAH_KM_1
);
4892 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", SYM_AH_KM
);
4897 strcat(outBuff
, "/");
4898 moreThanAh
= moreThanAh
|| osdFormatCentiNumber(buff
, (int32_t)(getMAhDrawn() * 10000000.0f
/ totalDistance
), 1000, 0, 2, digits
, false);
4899 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4901 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c%c", SYM_MAH_KM_0
, SYM_MAH_KM_1
);
4903 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", SYM_AH_KM
);
4906 tfp_sprintf(outBuff
+ strlen(outBuff
), "---/---%c%c", SYM_MAH_KM_0
, SYM_MAH_KM_1
);
4909 if (efficiencyValid
) {
4910 osdFormatCentiNumber(buff
, (int32_t)((getMWhDrawn() - stats
.flightStartMWh
) * 10000.0f
/ totalDistance
), 0, 2, 0, digits
, false);
4911 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4912 strcat(outBuff
, "/");
4913 osdFormatCentiNumber(buff
, (int32_t)(getMWhDrawn() * 10000.0f
/ totalDistance
), 0, 2, 0, digits
, false);
4914 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
4916 strcat(outBuff
, "---/---");
4918 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", SYM_WH_KM
);
4922 tfp_sprintf(outBuff
+ strlen(outBuff
), "%c", '\0');
4923 displayWrite(osdDisplayPort
, statValX
, row
++, outBuff
);
4928 uint8_t drawStat_RXStats(uint8_t col
, uint8_t row
, uint8_t statValX
)
4931 uint8_t multiValueXOffset
= 0;
4933 tfp_sprintf(buff
, "MIN RSSI");
4934 if (rxConfig()->serialrx_provider
== SERIALRX_CRSF
) {
4935 strcat(buff
, "/LQ");
4937 if (osdDisplayIsHD())
4938 strcat(buff
, "/DBM");
4940 displayWrite(osdDisplayPort
, col
, row
, buff
);
4942 memset(buff
, '\0', strlen(buff
));
4943 tfp_sprintf(buff
, ": ");
4944 itoa(stats
.min_rssi
, buff
+ 2, 10);
4945 strcat(osdFormatTrimWhiteSpace(buff
), "%");
4947 if (rxConfig()->serialrx_provider
== SERIALRX_CRSF
) {
4948 strcat(osdFormatTrimWhiteSpace(buff
), "/");
4949 multiValueXOffset
= strlen(buff
);
4950 itoa(stats
.min_lq
, buff
+ multiValueXOffset
, 10);
4951 strcat(osdFormatTrimWhiteSpace(buff
), "%");
4953 if (osdDisplayIsHD()) {
4954 strcat(osdFormatTrimWhiteSpace(buff
), "/");
4955 itoa(stats
.min_rssi_dbm
, buff
+ 2, 10);
4956 tfp_sprintf(buff
+ strlen(buff
), "%c", SYM_DBM
);
4957 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4961 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4963 if (!osdDisplayIsHD() && rxConfig()->serialrx_provider
== SERIALRX_CRSF
) {
4964 displayWrite(osdDisplayPort
, col
, row
, "MIN RX DBM");
4965 memset(buff
, '\0', strlen(buff
));
4966 tfp_sprintf(buff
, ": ");
4967 itoa(stats
.min_rssi_dbm
, buff
+ 2, 10);
4968 tfp_sprintf(buff
+ strlen(buff
), "%c", SYM_DBM
);
4969 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4975 uint8_t drawStat_GPS(uint8_t col
, uint8_t row
, uint8_t statValX
)
4978 displayWrite(osdDisplayPort
, col
, row
, "MIN/MAX GPS SATS");
4979 tfp_sprintf(buff
, ": %u/%u", stats
.min_sats
, stats
.max_sats
);
4980 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4985 uint8_t drawStat_ESCTemperature(uint8_t col
, uint8_t row
, uint8_t statValX
)
4988 displayWrite(osdDisplayPort
, col
, row
, "MIN/MAX ESC TEMP");
4989 tfp_sprintf(buff
, ": %3d/%3d%c",
4990 ((osdConfig()->units
== OSD_UNIT_IMPERIAL
) ? (int16_t)(stats
.min_esc_temp
* 9 / 5.0f
+ 320) : stats
.min_esc_temp
),
4991 ((osdConfig()->units
== OSD_UNIT_IMPERIAL
) ? (int16_t)(stats
.max_esc_temp
* 9 / 5.0f
+ 320) : stats
.max_esc_temp
),
4992 ((osdConfig()->units
== OSD_UNIT_IMPERIAL
) ? SYM_TEMP_F
: SYM_TEMP_C
));
4993 displayWrite(osdDisplayPort
, statValX
, row
++, buff
);
4998 uint8_t drawStat_GForce(uint8_t col
, uint8_t row
, uint8_t statValX
)
5003 const float max_gforce
= accGetMeasuredMaxG();
5004 const acc_extremes_t
*acc_extremes
= accGetMeasuredExtremes();
5005 const float acc_extremes_min
= acc_extremes
[Z
].min
;
5006 const float acc_extremes_max
= acc_extremes
[Z
].max
;
5008 if (!osdDisplayIsHD())
5009 displayWrite(osdDisplayPort
, col
, row
, "MAX G-FORCE");
5011 displayWrite(osdDisplayPort
, col
, row
, "MAX/MIN Z/MAX Z G-FORCE");
5013 tfp_sprintf(outBuff
, ": ");
5014 osdFormatCentiNumber(buff
, max_gforce
* 100, 0, 2, 0, 3, false);
5016 if (!osdDisplayIsHD()) {
5017 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
5018 displayWrite(osdDisplayPort
, statValX
, row
++, outBuff
);
5020 displayWrite(osdDisplayPort
, col
, row
, "MIN/MAX Z G-FORCE");
5021 memset(outBuff
, '\0', strlen(outBuff
));
5022 tfp_sprintf(outBuff
, ": ");
5024 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
5025 strcat(outBuff
, "/");
5027 osdFormatCentiNumber(buff
, acc_extremes_min
* 100, 0, 2, 0, 4, false);
5028 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
5029 strcat(outBuff
, "/");
5031 osdFormatCentiNumber(buff
, acc_extremes_max
* 100, 0, 2, 0, 3, false);
5032 strcat(outBuff
, osdFormatTrimWhiteSpace(buff
));
5033 displayWrite(osdDisplayPort
, statValX
, row
++, outBuff
);
5038 uint8_t drawStat_DisarmMethod(uint8_t col
, uint8_t row
, uint8_t statValX
)
5040 // We keep "" for backward compatibility with the Blackbox explorer and other potential usages
5041 const char * disarmReasonStr
[DISARM_REASON_COUNT
] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "", "FAILSAFE", "NAV SYS", "LANDING"};
5043 displayWrite(osdDisplayPort
, col
, row
, "DISARMED BY");
5044 displayWrite(osdDisplayPort
, statValX
, row
, ": ");
5045 displayWrite(osdDisplayPort
, statValX
+ 2, row
++, disarmReasonStr
[getDisarmReason()]);
5050 static void osdShowStats(bool isSinglePageStatsCompatible
, uint8_t page
)
5052 const char * statsHeader
[2] = {"*** STATS 1/2 -> ***", "*** STATS <- 2/2 ***"};
5053 uint8_t row
= 1; // Start one line down leaving space at the top of the screen.
5055 const uint8_t statNameX
= (osdDisplayPort
->cols
- (osdDisplayIsHD() ? 41 : 28)) / 2;
5056 const uint8_t statValuesX
= osdDisplayPort
->cols
- statNameX
- (osdDisplayIsHD() ? 15 : 11);
5061 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
5062 displayClearScreen(osdDisplayPort
);
5064 if (isSinglePageStatsCompatible
) {
5066 tfp_sprintf(buff
, "*** STATS ");
5069 if (feature(FEATURE_BLACKBOX
)) {
5070 int32_t logNumber
= blackboxGetLogNumber();
5072 tfp_sprintf(buff
+ strlen(buff
), " %c%05" PRId32
" ", SYM_BLACKBOX
, logNumber
);
5074 tfp_sprintf(buff
+ strlen(buff
), " %c ", SYM_BLACKBOX
);
5078 strcat(buff
, "***");
5080 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buff
)) / 2, row
++, buff
);
5082 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(statsHeader
[page
+ 1])) / 2, row
++, statsHeader
[page
]);
5084 if (isSinglePageStatsCompatible
) {
5085 // Top 15 rows for most important stats. Max 19 rows (WTF)
5086 row
= drawStat_FlightTime(statNameX
, row
, statValuesX
); // 1 row
5087 row
= drawStat_FlightDistance(statNameX
, row
, statValuesX
); // 1 row
5088 if (feature(FEATURE_GPS
)) row
= drawStat_MaxDistanceFromHome(statNameX
, row
, statValuesX
); // 1 row
5089 if (feature(FEATURE_GPS
)) row
= drawStat_Speed(statNameX
, row
, statValuesX
); // 1 row
5090 row
= drawStat_MaximumAltitude(statNameX
, row
, statValuesX
); // 1 row
5091 row
= drawStat_BatteryVoltage(statNameX
, row
, statValuesX
); // 1 row
5092 if (feature(FEATURE_CURRENT_METER
)) row
= drawStat_MaximumPowerAndCurrent(statNameX
, row
, statValuesX
); // 1 row
5093 if (feature(FEATURE_CURRENT_METER
)) row
= drawStat_UsedEnergy(statNameX
, row
, statValuesX
); // 1 row
5094 if (feature(FEATURE_CURRENT_METER
) && feature(FEATURE_GPS
)) row
= drawStat_AverageEfficiency(statNameX
, row
, statValuesX
, false); // 1 row
5095 if (osdConfig()->stats_show_metric_efficiency
&& osdIsNotMetric() && feature(FEATURE_CURRENT_METER
) && feature(FEATURE_GPS
)) row
= drawStat_AverageEfficiency(statNameX
, row
, statValuesX
, true); // 1 row
5096 row
= drawStat_RXStats(statNameX
, row
, statValuesX
); // 1 row if non-CRSF else 2 rows
5097 if (feature(FEATURE_GPS
)) row
= drawStat_GPS(statNameX
, row
, statValuesX
); // 1 row
5098 if (STATE(ESC_SENSOR_ENABLED
)) row
= drawStat_ESCTemperature(statNameX
, row
, statValuesX
); // 1 row
5100 // Draw these if there is space space
5101 if (row
< (osdDisplayPort
->cols
-3)) row
= drawStat_GForce(statNameX
, row
, statValuesX
); // 1 row HD or 2 rows SD
5103 if (row
< (osdDisplayPort
->cols
-7) && statsConfig()->stats_enabled
) row
= drawStat_Stats(statNameX
, row
, statValuesX
, false); // 4 rows
5109 row
= drawStat_FlightTime(statNameX
, row
, statValuesX
); // 1 row
5110 row
= drawStat_FlightDistance(statNameX
, row
, statValuesX
); // 1 row
5111 if (feature(FEATURE_GPS
)) row
= drawStat_MaxDistanceFromHome(statNameX
, row
, statValuesX
); // 1 row
5112 if (feature(FEATURE_GPS
)) row
= drawStat_Speed(statNameX
, row
, statValuesX
); // 1 row
5113 row
= drawStat_MaximumAltitude(statNameX
, row
, statValuesX
); // 1 row
5114 row
= drawStat_BatteryVoltage(statNameX
, row
, statValuesX
); // 1 row
5115 if (feature(FEATURE_CURRENT_METER
)) row
= drawStat_MaximumPowerAndCurrent(statNameX
, row
, statValuesX
); // 1 row
5116 if (feature(FEATURE_CURRENT_METER
))row
= drawStat_UsedEnergy(statNameX
, row
, statValuesX
); // 1 row
5117 if (feature(FEATURE_CURRENT_METER
) && feature(FEATURE_GPS
)) row
= drawStat_AverageEfficiency(statNameX
, row
, statValuesX
, false); // 1 row
5118 if (feature(FEATURE_GPS
))row
= drawStat_GPS(statNameX
, row
, statValuesX
); // 1 row
5122 row
= drawStat_RXStats(statNameX
, row
, statValuesX
); // 1 row if non-CRSF else 2 rows
5123 if (STATE(ESC_SENSOR_ENABLED
)) row
= drawStat_ESCTemperature(statNameX
, row
, statValuesX
); // 1 row
5124 row
= drawStat_GForce(statNameX
, row
, statValuesX
); // 1 row HD or 2 rows SD
5125 if (osdConfig()->stats_show_metric_efficiency
&& osdIsNotMetric() && feature(FEATURE_CURRENT_METER
) && feature(FEATURE_GPS
)) row
= drawStat_AverageEfficiency(statNameX
, row
, statValuesX
, true); // 1 row
5128 if (feature(FEATURE_BLACKBOX
)) {
5130 displayWrite(osdDisplayPort
, statNameX
, row
, "BLACKBOX FILE");
5132 tfp_sprintf(buff
, ": %u/%u", stats
.min_sats
, stats
.max_sats
);
5134 int32_t logNumber
= blackboxGetLogNumber();
5136 tfp_sprintf(buff
, ": %05ld ", logNumber
);
5138 strcat(buff
, ": INVALID");
5140 displayWrite(osdDisplayPort
, statValuesX
, row
++, buff
); // 1 row
5145 if (row
< (osdDisplayPort
->cols
-7) && statsConfig()->stats_enabled
) row
= drawStat_Stats(statNameX
, row
, statValuesX
, false); // 4 rows
5152 row
= drawStat_DisarmMethod(statNameX
, row
, statValuesX
);
5154 // The following has been commented out as it will be added in #9688
5155 // uint16_t rearmMs = (emergInflightRearmEnabled()) ? emergencyInFlightRearmTimeMS() : 0;
5157 if (savingSettings
== true) {
5158 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS
))) / 2, row
++, OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS
));
5159 /*} else if (rearmMs > 0) { // Show rearming time if settings not actively being saved. Ignore the settings saved message if rearm available.
5160 char emReArmMsg[23];
5161 tfp_sprintf(emReArmMsg, "** REARM PERIOD: ");
5162 tfp_sprintf(emReArmMsg + strlen(emReArmMsg), "%02d", (uint8_t)MS2S(rearmMs));
5163 strcat(emReArmMsg, " **\0");
5164 displayWrite(osdDisplayPort, statNameX, top++, OSD_MESSAGE_STR(emReArmMsg));*/
5165 } else if (notify_settings_saved
> 0) {
5166 if (millis() > notify_settings_saved
) {
5167 notify_settings_saved
= 0;
5169 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED
))) / 2, row
++, OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED
));
5173 displayCommitTransaction(osdDisplayPort
);
5176 // HD arming screen. based on the minimum HD OSD grid size of 50 x 18
5177 static void osdShowHDArmScreen(void)
5180 char buf
[MAX(osdDisplayPort
->cols
, FORMATTED_DATE_TIME_BUFSIZE
)];
5181 char buf2
[MAX(osdDisplayPort
->cols
, FORMATTED_DATE_TIME_BUFSIZE
)];
5182 char craftNameBuf
[MAX_NAME_LENGTH
];
5183 char versionBuf
[osdDisplayPort
->cols
];
5184 uint8_t safehomeRow
= 0;
5185 uint8_t armScreenRow
= 2; // Start at row 2
5187 armScreenRow
= drawLogos(false, armScreenRow
);
5190 if (strlen(systemConfig()->craftName
) > 0) {
5191 osdFormatCraftName(craftNameBuf
);
5192 strcpy(buf2
, "ARMED!");
5193 tfp_sprintf(buf
, "%s - %s", craftNameBuf
, buf2
);
5195 strcpy(buf
, "ARMED!");
5197 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5198 #if defined(USE_GPS)
5199 #if defined (USE_SAFE_HOME)
5200 if (posControl
.safehomeState
.distance
) {
5201 safehomeRow
= armScreenRow
;
5204 #endif // USE_SAFE_HOME
5208 if (posControl
.waypointListValid
&& posControl
.waypointCount
> 0) {
5209 #ifdef USE_MULTI_MISSION
5210 tfp_sprintf(buf
, "MISSION %u/%u (%u WP)", posControl
.loadedMultiMissionIndex
, posControl
.multiMissionCount
, posControl
.waypointCount
);
5211 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5213 strcpy(buf
, "*MISSION LOADED*");
5214 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5219 #if defined(USE_GPS)
5220 if (feature(FEATURE_GPS
)) {
5221 if (STATE(GPS_FIX_HOME
)) {
5222 if (osdConfig()->osd_home_position_arm_screen
){
5223 osdFormatCoordinate(buf
, SYM_LAT
, GPS_home
.lat
);
5224 osdFormatCoordinate(buf2
, SYM_LON
, GPS_home
.lon
);
5226 uint8_t col
= strlen(buf
) + strlen(buf2
) + gap
;
5228 if ((osdDisplayPort
->cols
%2) != (col
%2)) {
5233 col
= (osdDisplayPort
->cols
- col
) / 2;
5235 displayWrite(osdDisplayPort
, col
, armScreenRow
, buf
);
5236 displayWrite(osdDisplayPort
, col
+ strlen(buf
) + gap
, armScreenRow
++, buf2
);
5238 int digits
= osdConfig()->plus_code_digits
;
5239 olc_encode(GPS_home
.lat
, GPS_home
.lon
, digits
, buf
, sizeof(buf
));
5240 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5243 #if defined (USE_SAFE_HOME)
5244 if (posControl
.safehomeState
.distance
) { // safehome found during arming
5245 if (navConfig()->general
.flags
.safehome_usage_mode
== SAFEHOME_USAGE_OFF
) {
5246 strcpy(buf
, "SAFEHOME FOUND; MODE OFF");
5248 osdFormatDistanceStr(buf2
, posControl
.safehomeState
.distance
);
5249 tfp_sprintf(buf
, "%c SAFEHOME %u @ %s", SYM_HOME
, posControl
.safehomeState
.index
, buf2
);
5251 textAttributes_t elemAttr
= _TEXT_ATTRIBUTES_BLINK_BIT
;
5252 // write this message below the ARMED message to make it obvious
5253 displayWriteWithAttr(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, safehomeRow
, buf
, elemAttr
);
5257 strcpy(buf
, "!NO HOME POSITION!");
5258 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5264 if (rtcGetDateTimeLocal(&dt
)) {
5265 tfp_sprintf(buf
, "%04u-%02u-%02u %02u:%02u:%02u", dt
.year
, dt
.month
, dt
.day
, dt
.hours
, dt
.minutes
, dt
.seconds
);
5266 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5270 tfp_sprintf(versionBuf
, "INAV VERSION: %s", FC_VERSION_STRING
);
5271 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(versionBuf
)) / 2, armScreenRow
++, versionBuf
);
5275 if (armScreenRow
< (osdDisplayPort
->rows
- 4))
5276 armScreenRow
= drawStats(armScreenRow
);
5280 static void osdShowSDArmScreen(void)
5283 char buf
[MAX(osdDisplayPort
->cols
, FORMATTED_DATE_TIME_BUFSIZE
)];
5284 char buf2
[MAX(osdDisplayPort
->cols
, FORMATTED_DATE_TIME_BUFSIZE
)];
5285 char craftNameBuf
[MAX_NAME_LENGTH
];
5286 char versionBuf
[osdDisplayPort
->cols
];
5287 // We need 12 visible rows, start row never < first fully visible row 1
5288 uint8_t armScreenRow
= osdDisplayPort
->rows
> 13 ? (osdDisplayPort
->rows
- 12) / 2 : 1;
5289 uint8_t safehomeRow
= 0;
5291 strcpy(buf
, "ARMED!");
5292 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5293 safehomeRow
= armScreenRow
;
5296 if (strlen(systemConfig()->craftName
) > 0) {
5297 osdFormatCraftName(craftNameBuf
);
5298 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(systemConfig()->craftName
)) / 2, armScreenRow
++, craftNameBuf
);
5301 if (posControl
.waypointListValid
&& posControl
.waypointCount
> 0) {
5302 #ifdef USE_MULTI_MISSION
5303 tfp_sprintf(buf
, "MISSION %u/%u (%u WP)", posControl
.loadedMultiMissionIndex
, posControl
.multiMissionCount
, posControl
.waypointCount
);
5304 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
, buf
);
5306 strcpy(buf
, "*MISSION LOADED*");
5307 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
, buf
);
5312 #if defined(USE_GPS)
5313 if (feature(FEATURE_GPS
)) {
5314 if (STATE(GPS_FIX_HOME
)) {
5315 if (osdConfig()->osd_home_position_arm_screen
) {
5316 osdFormatCoordinate(buf
, SYM_LAT
, GPS_home
.lat
);
5317 osdFormatCoordinate(buf2
, SYM_LON
, GPS_home
.lon
);
5319 uint8_t gpsStartCol
= (osdDisplayPort
->cols
- (strlen(buf
) + strlen(buf2
) + 2)) / 2;
5320 displayWrite(osdDisplayPort
, gpsStartCol
, armScreenRow
, buf
);
5321 displayWrite(osdDisplayPort
, gpsStartCol
+ strlen(buf
) + 2, armScreenRow
++, buf2
);
5323 int digits
= osdConfig()->plus_code_digits
;
5324 olc_encode(GPS_home
.lat
, GPS_home
.lon
, digits
, buf
, sizeof(buf
));
5325 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5328 #if defined (USE_SAFE_HOME)
5329 if (posControl
.safehomeState
.distance
) { // safehome found during arming
5330 if (navConfig()->general
.flags
.safehome_usage_mode
== SAFEHOME_USAGE_OFF
) {
5331 strcpy(buf
, "SAFEHOME FOUND; MODE OFF");
5333 osdFormatDistanceStr(buf2
, posControl
.safehomeState
.distance
);
5334 tfp_sprintf(buf
, "%c SAFEHOME %u @ %s", SYM_HOME
, posControl
.safehomeState
.index
, buf2
);
5336 textAttributes_t elemAttr
= _TEXT_ATTRIBUTES_BLINK_BIT
;
5337 // write this message below the ARMED message to make it obvious
5338 displayWriteWithAttr(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, safehomeRow
, buf
, elemAttr
);
5342 strcpy(buf
, "!NO HOME POSITION!");
5343 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5349 if (rtcGetDateTimeLocal(&dt
)) {
5350 tfp_sprintf(buf
, "%04u-%02u-%02u %02u:%02u:%02u", dt
.year
, dt
.month
, dt
.day
, dt
.hours
, dt
.minutes
, dt
.seconds
);
5351 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
5355 tfp_sprintf(versionBuf
, "INAV VERSION: %s", FC_VERSION_STRING
);
5356 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(versionBuf
)) / 2, armScreenRow
++, versionBuf
);
5360 if (armScreenRow
< (osdDisplayPort
->rows
- 4))
5361 armScreenRow
= drawStats(armScreenRow
);
5365 // called when motors armed
5366 static void osdShowArmed(void)
5368 displayClearScreen(osdDisplayPort
);
5370 if (osdDisplayIsHD()) {
5371 osdShowHDArmScreen();
5373 osdShowSDArmScreen();
5377 static void osdFilterData(timeUs_t currentTimeUs
) {
5378 static timeUs_t lastRefresh
= 0;
5379 float refresh_dT
= US2S(cmpTimeUs(currentTimeUs
, lastRefresh
));
5381 GForce
= fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF
)) / GRAVITY_MSS
;
5382 for (uint8_t axis
= 0; axis
< XYZ_AXIS_COUNT
; ++axis
) GForceAxis
[axis
] = imuMeasuredAccelBF
.v
[axis
] / GRAVITY_MSS
;
5385 GForce
= pt1FilterApply3(&GForceFilter
, GForce
, refresh_dT
);
5386 for (uint8_t axis
= 0; axis
< XYZ_AXIS_COUNT
; ++axis
) pt1FilterApply3(GForceFilterAxis
+ axis
, GForceAxis
[axis
], refresh_dT
);
5388 pt1FilterInitRC(&GForceFilter
, GFORCE_FILTER_TC
, 0);
5389 pt1FilterReset(&GForceFilter
, GForce
);
5391 for (uint8_t axis
= 0; axis
< XYZ_AXIS_COUNT
; ++axis
) {
5392 pt1FilterInitRC(GForceFilterAxis
+ axis
, GFORCE_FILTER_TC
, 0);
5393 pt1FilterReset(GForceFilterAxis
+ axis
, GForceAxis
[axis
]);
5397 lastRefresh
= currentTimeUs
;
5400 // Detect when the user is holding the roll stick to the right
5401 static bool osdIsPageUpStickCommandHeld(void)
5403 static int pageUpHoldCount
= 1;
5405 bool keyHeld
= false;
5412 pageUpHoldCount
= 1;
5417 if (pageUpHoldCount
> 20) {
5418 pageUpHoldCount
= 1;
5425 // Detect when the user is holding the roll stick to the left
5426 static bool osdIsPageDownStickCommandHeld(void)
5428 static int pageDownHoldCount
= 1;
5430 bool keyHeld
= false;
5436 pageDownHoldCount
= 1;
5438 ++pageDownHoldCount
;
5441 if (pageDownHoldCount
> 20) {
5442 pageDownHoldCount
= 1;
5449 static void osdRefresh(timeUs_t currentTimeUs
)
5451 osdFilterData(currentTimeUs
);
5454 if (IS_RC_MODE_ACTIVE(BOXOSD
) && (!cmsInMenu
) && !(osdConfig()->osd_failsafe_switch_layout
&& FLIGHT_MODE(FAILSAFE_MODE
))) {
5456 if (IS_RC_MODE_ACTIVE(BOXOSD
) && !(osdConfig()->osd_failsafe_switch_layout
&& FLIGHT_MODE(FAILSAFE_MODE
))) {
5458 displayClearScreen(osdDisplayPort
);
5459 armState
= ARMING_FLAG(ARMED
);
5463 bool statsSinglePageCompatible
= (osdDisplayPort
->rows
>= OSD_STATS_SINGLE_PAGE_MIN_ROWS
);
5464 static uint8_t statsCurrentPage
= 0;
5465 static bool statsDisplayed
= false;
5466 static bool statsAutoPagingEnabled
= true;
5467 static bool isThrottleHigh
= false;
5469 // Detect arm/disarm
5470 if (armState
!= ARMING_FLAG(ARMED
)) {
5471 if (ARMING_FLAG(ARMED
)) {
5472 // Display the "Arming" screen
5473 statsDisplayed
= false;
5474 if (!STATE(IN_FLIGHT_EMERG_REARM
))
5478 uint16_t delay
= osdConfig()->arm_screen_display_time
;
5479 if (STATE(IN_FLIGHT_EMERG_REARM
))
5481 #if defined(USE_SAFE_HOME)
5482 else if (posControl
.safehomeState
.distance
)
5485 osdSetNextRefreshIn(delay
);
5487 // Display the "Stats" screen
5488 statsDisplayed
= true;
5489 statsCurrentPage
= 0;
5490 statsAutoPagingEnabled
= osdConfig()->stats_page_auto_swap_time
> 0 ? true : false;
5491 osdShowStats(statsSinglePageCompatible
, statsCurrentPage
);
5492 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME
);
5493 isThrottleHigh
= checkStickPosition(THR_HI
);
5496 armState
= ARMING_FLAG(ARMED
);
5499 // This block is entered when we're showing the "Splash", "Armed" or "Stats" screens
5500 if (resumeRefreshAt
) {
5502 // Handle events only when the "Stats" screen is being displayed.
5503 if (statsDisplayed
) {
5505 // Manual paging stick commands are only applicable to multi-page stats.
5506 // ******************************
5507 // For single-page stats, this effectively disables the ability to cancel the
5508 // automatic paging/updates with the stick commands. So unless stats_page_auto_swap_time
5509 // is set to 0 or greater than 4 (saved settings display interval is 5 seconds), then
5510 // "Saved Settings" should display if it is active within the refresh interval.
5511 // ******************************
5512 // With multi-page stats, "Saved Settings" could also be missed if the user
5513 // has canceled automatic paging using the stick commands, because that is only
5514 // updated when osdShowStats() is called. So, in that case, they would only see
5515 // the "Saved Settings" message if they happen to manually change pages using the
5516 // stick commands within the interval the message is displayed.
5517 bool manualPageUpRequested
= false;
5518 bool manualPageDownRequested
= false;
5519 if (!statsSinglePageCompatible
) {
5520 // These methods ensure the paging stick commands are held for a brief period
5521 // Otherwise it can result in a race condition where the stats are
5522 // updated too quickly and can result in partial blanks, etc.
5523 if (osdIsPageUpStickCommandHeld()) {
5524 manualPageUpRequested
= true;
5525 statsAutoPagingEnabled
= false;
5526 } else if (osdIsPageDownStickCommandHeld()) {
5527 manualPageDownRequested
= true;
5528 statsAutoPagingEnabled
= false;
5532 if (statsAutoPagingEnabled
) {
5533 // Alternate screens for multi-page stats.
5534 // Also, refreshes screen at swap interval for single-page stats.
5535 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time
* 1000), 2)) {
5536 if (statsCurrentPage
== 0) {
5537 osdShowStats(statsSinglePageCompatible
, statsCurrentPage
);
5538 statsCurrentPage
= 1;
5541 if (statsCurrentPage
== 1) {
5542 osdShowStats(statsSinglePageCompatible
, statsCurrentPage
);
5543 statsCurrentPage
= 0;
5547 // Process manual page change events for multi-page stats.
5548 if (manualPageUpRequested
) {
5549 osdShowStats(statsSinglePageCompatible
, 1);
5550 statsCurrentPage
= 1;
5551 } else if (manualPageDownRequested
) {
5552 osdShowStats(statsSinglePageCompatible
, 0);
5553 statsCurrentPage
= 0;
5558 // Handle events when either "Splash", "Armed" or "Stats" screens are displayed.
5559 if (currentTimeUs
> resumeRefreshAt
|| (OSD_RESUME_UPDATES_STICK_COMMAND
&& !isThrottleHigh
)) {
5560 // Time elapsed or canceled by stick commands.
5561 // Exit to normal OSD operation.
5562 displayClearScreen(osdDisplayPort
);
5563 resumeRefreshAt
= 0;
5564 statsDisplayed
= false;
5566 // Continue "Splash", "Armed" or "Stats" screens.
5567 displayHeartbeat(osdDisplayPort
);
5568 isThrottleHigh
= checkStickPosition(THR_HI
);
5575 if (!displayIsGrabbed(osdDisplayPort
)) {
5576 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
5578 displayClearScreen(osdDisplayPort
);
5581 osdDrawNextElement();
5582 displayHeartbeat(osdDisplayPort
);
5583 displayCommitTransaction(osdDisplayPort
);
5584 #ifdef OSD_CALLS_CMS
5586 cmsUpdate(currentTimeUs
);
5593 * Called periodically by the scheduler
5595 void osdUpdate(timeUs_t currentTimeUs
)
5597 static uint32_t counter
= 0;
5599 // don't touch buffers if DMA transaction is in progress
5600 if (displayIsTransferInProgress(osdDisplayPort
)) {
5604 if (!osdDisplayIsReady
) {
5605 osdCompleteAsyncInitialization();
5609 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
5610 // Check if the layout has changed. Higher numbered
5611 // boxes take priority.
5612 unsigned activeLayout
;
5613 if (layoutOverride
>= 0) {
5614 activeLayout
= layoutOverride
;
5615 // Check for timed override, it will go into effect on
5616 // the next OSD iteration
5617 if (layoutOverrideUntil
> 0 && millis() > layoutOverrideUntil
) {
5618 layoutOverrideUntil
= 0;
5619 layoutOverride
= -1;
5621 } else if (osdConfig()->osd_failsafe_switch_layout
&& FLIGHT_MODE(FAILSAFE_MODE
)) {
5624 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
5625 if (IS_RC_MODE_ACTIVE(BOXOSDALT3
))
5629 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
5630 if (IS_RC_MODE_ACTIVE(BOXOSDALT2
))
5634 if (IS_RC_MODE_ACTIVE(BOXOSDALT1
))
5637 #ifdef USE_PROGRAMMING_FRAMEWORK
5638 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT
))
5639 activeLayout
= constrain(logicConditionValuesByType
[LOGIC_CONDITION_SET_OSD_LAYOUT
], 0, OSD_ALTERNATE_LAYOUT_COUNT
);
5644 if (currentLayout
!= activeLayout
) {
5645 currentLayout
= activeLayout
;
5646 osdStartFullRedraw();
5650 #define DRAW_FREQ_DENOM 4
5651 #define STATS_FREQ_DENOM 50
5654 if ((counter
% STATS_FREQ_DENOM
) == 0 && ARMING_FLAG(ARMED
)) {
5658 if ((counter
% DRAW_FREQ_DENOM
) == 0) {
5659 // redraw values in buffer
5660 osdRefresh(currentTimeUs
);
5662 // rest of time redraw screen
5663 displayDrawScreen(osdDisplayPort
);
5667 // do not allow ARM if we are in menu
5668 if (displayIsGrabbed(osdDisplayPort
)) {
5669 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU
);
5671 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU
);
5676 void osdStartFullRedraw(void)
5681 void osdOverrideLayout(int layout
, timeMs_t duration
)
5683 layoutOverride
= constrain(layout
, -1, ARRAYLEN(osdLayoutsConfig()->item_pos
) - 1);
5684 if (layoutOverride
>= 0 && duration
> 0) {
5685 layoutOverrideUntil
= millis() + duration
;
5687 layoutOverrideUntil
= 0;
5691 int osdGetActiveLayout(bool *overridden
)
5694 *overridden
= layoutOverride
>= 0;
5696 return currentLayout
;
5699 bool osdItemIsFixed(osd_items_e item
)
5701 return item
== OSD_CROSSHAIRS
||
5702 item
== OSD_ARTIFICIAL_HORIZON
||
5703 item
== OSD_HORIZON_SIDEBARS
;
5706 displayPort_t
*osdGetDisplayPort(void)
5708 return osdDisplayPort
;
5711 displayCanvas_t
*osdGetDisplayPortCanvas(void)
5713 #if defined(USE_CANVAS)
5714 if (osdDisplayHasCanvas
) {
5721 timeMs_t
systemMessageCycleTime(unsigned messageCount
, const char **messages
){
5723 float factor
= 1.0f
;
5724 while (i
< messageCount
) {
5725 if ((float)strlen(messages
[i
]) / 15.0f
> factor
) {
5726 factor
= (float)strlen(messages
[i
]) / 15.0f
;
5730 return osdConfig()->system_msg_display_time
* factor
;
5733 textAttributes_t
osdGetSystemMessage(char *buff
, size_t buff_size
, bool isCenteredText
)
5735 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
5738 const char *message
= NULL
;
5739 /* WARNING: messageBuf is shared, use accordingly */
5740 char messageBuf
[MAX(SETTING_MAX_NAME_LENGTH
, OSD_MESSAGE_LENGTH
+ 1)];
5742 /* WARNING: ensure number of messages returned does not exceed messages array size
5743 * Messages array set 1 larger than maximum expected message count of 6 */
5744 const char *messages
[7];
5745 unsigned messageCount
= 0;
5747 const char *failsafeInfoMessage
= NULL
;
5748 const char *invertedInfoMessage
= NULL
;
5750 if (ARMING_FLAG(ARMED
)) {
5751 if (FLIGHT_MODE(FAILSAFE_MODE
) || FLIGHT_MODE(NAV_RTH_MODE
) || FLIGHT_MODE(NAV_WP_MODE
) || navigationIsExecutingAnEmergencyLanding()) {
5752 /* ADDS MAXIMUM OF 3 MESSAGES TO TOTAL NORMALLY, 5 MESSAGES DURING FAILSAFE */
5753 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE
) {
5754 messages
[messageCount
++] = STATE(LANDING_DETECTED
) ? OSD_MESSAGE_STR(OSD_MSG_WP_LANDED
) : OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED
);
5755 } else if (NAV_Status
.state
== MW_NAV_STATE_WP_ENROUTE
) {
5756 // Countdown display for remaining Waypoints
5758 osdFormatDistanceSymbol(buf
, posControl
.wpDistance
, 0);
5759 tfp_sprintf(messageBuf
, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl
.activeWaypointIndex
), posControl
.geoWaypointCount
, buf
);
5760 messages
[messageCount
++] = messageBuf
;
5761 } else if (NAV_Status
.state
== MW_NAV_STATE_HOLD_TIMED
) {
5762 if (navConfig()->general
.waypoint_enforce_altitude
&& !posControl
.wpAltitudeReached
) {
5763 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ADJUSTING_WP_ALT
);
5765 // WP hold time countdown in seconds
5766 timeMs_t currentTime
= millis();
5767 int holdTimeRemaining
= posControl
.waypointList
[posControl
.activeWaypointIndex
].p1
- (int)(MS2S(currentTime
- posControl
.wpReachedTime
));
5768 holdTimeRemaining
= holdTimeRemaining
>= 0 ? holdTimeRemaining
: 0;
5770 tfp_sprintf(messageBuf
, "HOLDING WP FOR %2u S", holdTimeRemaining
);
5772 messages
[messageCount
++] = messageBuf
;
5776 const char *navStateMessage
= navigationStateMessage();
5777 if (navStateMessage
) {
5778 messages
[messageCount
++] = navStateMessage
;
5781 #if defined(USE_SAFE_HOME)
5782 const char *safehomeMessage
= divertingToSafehomeMessage();
5783 if (safehomeMessage
) {
5784 messages
[messageCount
++] = safehomeMessage
;
5787 if (FLIGHT_MODE(FAILSAFE_MODE
)) { // In FS mode while armed
5788 if (NAV_Status
.state
== MW_NAV_STATE_LAND_SETTLE
&& posControl
.landingDelay
> 0) {
5789 uint16_t remainingHoldSec
= MS2S(posControl
.landingDelay
- millis());
5790 tfp_sprintf(messageBuf
, "LANDING DELAY: %3u SECONDS", remainingHoldSec
);
5792 messages
[messageCount
++] = messageBuf
;
5795 const char *failsafePhaseMessage
= osdFailsafePhaseMessage();
5796 failsafeInfoMessage
= osdFailsafeInfoMessage();
5798 if (failsafePhaseMessage
) {
5799 messages
[messageCount
++] = failsafePhaseMessage
;
5801 if (failsafeInfoMessage
) {
5802 messages
[messageCount
++] = failsafeInfoMessage
;
5804 } else if (isWaypointMissionRTHActive()) {
5805 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
5806 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL
);
5808 } else if (STATE(LANDING_DETECTED
)) {
5809 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_LANDED
);
5811 /* Messages shown only when Failsafe, WP, RTH or Emergency Landing not active and landed state inactive */
5812 /* ADDS MAXIMUM OF 3 MESSAGES TO TOTAL */
5813 if (STATE(AIRPLANE
)) { /* ADDS MAXIMUM OF 3 MESSAGES TO TOTAL */
5814 #ifdef USE_FW_AUTOLAND
5815 if (canFwLandingBeCancelled()) {
5816 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_MOVE_STICKS
);
5819 if (navGetCurrentStateFlags() & NAV_CTL_LAUNCH
) {
5820 messages
[messageCount
++] = navConfig()->fw
.launch_manual_throttle
? OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH_MANUAL
) :
5821 OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH
);
5822 const char *launchStateMessage
= fixedWingLaunchStateMessage();
5823 if (launchStateMessage
) {
5824 messages
[messageCount
++] = launchStateMessage
;
5826 } else if (FLIGHT_MODE(SOARING_MODE
)) {
5827 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING
);
5828 } else if (isFwAutoModeActive(BOXAUTOTUNE
)) {
5829 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE
);
5830 if (FLIGHT_MODE(MANUAL_MODE
)) {
5831 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO
);
5833 } else if (isFwAutoModeActive(BOXAUTOTRIM
) && !feature(FEATURE_FW_AUTOTRIM
)) {
5834 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM
);
5835 } else if (isFixedWingLevelTrimActive()) {
5836 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_AUTOLEVEL
);
5839 if (IS_RC_MODE_ACTIVE(BOXANGLEHOLD
)) {
5840 int8_t navAngleHoldAxis
= navCheckActiveAngleHoldAxis();
5841 if (isAngleHoldLevel()) {
5842 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_LEVEL
);
5843 } else if (navAngleHoldAxis
== FD_ROLL
) {
5844 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_ROLL
);
5845 } else if (navAngleHoldAxis
== FD_PITCH
) {
5846 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_PITCH
);
5849 } else if (STATE(MULTIROTOR
)) { /* ADDS MAXIMUM OF 2 MESSAGES TO TOTAL */
5850 if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) {
5851 if (posControl
.cruise
.multicopterSpeed
>= 50.0f
) {
5853 osdFormatVelocityStr(buf
, posControl
.cruise
.multicopterSpeed
, false, false);
5854 tfp_sprintf(messageBuf
, "(SPD %s)", buf
);
5856 strcpy(messageBuf
, "(HOLD)");
5858 messages
[messageCount
++] = messageBuf
;
5859 } else if (FLIGHT_MODE(HEADFREE_MODE
)) {
5860 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE
);
5862 if (FLIGHT_MODE(NAV_ALTHOLD_MODE
) && !navigationRequiresAngleMode()) {
5863 /* If ALTHOLD is separately enabled for multirotor together with ANGL/HORIZON/ACRO modes
5864 * then ANGL/HORIZON/ACRO are indicated by the OSD_FLYMODE field.
5865 * In this case indicate ALTHOLD is active via a system message */
5867 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD
);
5871 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS
)) { /* ADDS MAXIMUM OF 2 MESSAGES TO TOTAL */
5872 unsigned invalidIndex
;
5874 // Check if we're unable to arm for some reason
5875 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING
) && !settingsValidate(&invalidIndex
)) {
5876 const setting_t
*setting
= settingGet(invalidIndex
);
5877 settingGetName(setting
, messageBuf
);
5878 for (int ii
= 0; messageBuf
[ii
]; ii
++) {
5879 messageBuf
[ii
] = sl_toupper(messageBuf
[ii
]);
5881 invertedInfoMessage
= messageBuf
;
5882 messages
[messageCount
++] = invertedInfoMessage
;
5884 invertedInfoMessage
= OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING
);
5885 messages
[messageCount
++] = invertedInfoMessage
;
5887 invertedInfoMessage
= OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM
);
5888 messages
[messageCount
++] = invertedInfoMessage
;
5889 // Show the reason for not arming
5890 messages
[messageCount
++] = osdArmingDisabledReasonMessage();
5892 } else if (!ARMING_FLAG(ARMED
)) {
5893 if (isWaypointListValid()) {
5894 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED
);
5898 /* Messages that are shown regardless of Arming state */
5899 /* ADDS MAXIMUM OF 2 MESSAGES TO TOTAL NORMALLY, 1 MESSAGE DURING FAILSAFE */
5900 if (posControl
.flags
.wpMissionPlannerActive
&& !FLIGHT_MODE(FAILSAFE_MODE
)) {
5901 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER
);
5904 // The following has been commented out as it will be added in #9688
5905 // uint16_t rearmMs = (emergInflightRearmEnabled()) ? emergencyInFlightRearmTimeMS() : 0;
5907 if (savingSettings
== true) {
5908 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS
);
5909 /*} else if (rearmMs > 0) { // Show rearming time if settings not actively being saved. Ignore the settings saved message if rearm available.
5910 char emReArmMsg[23];
5911 tfp_sprintf(emReArmMsg, "** REARM PERIOD: ");
5912 tfp_sprintf(emReArmMsg + strlen(emReArmMsg), "%02d", (uint8_t)MS2S(rearmMs));
5913 strcat(emReArmMsg, " **\0");
5914 messages[messageCount++] = OSD_MESSAGE_STR(emReArmMsg);*/
5915 } else if (notify_settings_saved
> 0) {
5916 if (millis() > notify_settings_saved
) {
5917 notify_settings_saved
= 0;
5919 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED
);
5923 if (messageCount
> 0) {
5924 message
= messages
[OSD_ALTERNATING_CHOICES(systemMessageCycleTime(messageCount
, messages
), messageCount
)];
5925 if (message
== failsafeInfoMessage
) {
5926 // failsafeInfoMessage is not useful for recovering
5927 // a lost model, but might help avoiding a crash.
5928 // Blink to grab user attention.
5929 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
5930 } else if (message
== invertedInfoMessage
) {
5931 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr
);
5933 // We're showing either failsafePhaseMessage or
5934 // navStateMessage. Don't BLINK here since
5935 // having this text available might be crucial
5936 // during a lost aircraft recovery and blinking
5937 // will cause it to be missing from some frames.
5940 osdFormatMessage(buff
, buff_size
, message
, isCenteredText
);
5945 void osdResetWarningFlags(void)
5947 osdWarningsFlags
= 0;
5950 static bool osdCheckWarning(bool condition
, uint8_t warningFlag
, uint8_t *warningsCount
)
5952 #define WARNING_REDISPLAY_DURATION 5000; // milliseconds
5954 const timeMs_t currentTimeMs
= millis();
5955 static timeMs_t warningDisplayStartTime
= 0;
5956 static timeMs_t redisplayStartTimeMs
= 0;
5957 static uint16_t osdWarningTimerDuration
;
5958 static uint8_t newWarningFlags
;
5960 if (condition
) { // condition required to trigger warning
5961 if (!(osdWarningsFlags
& warningFlag
)) {
5962 osdWarningsFlags
|= warningFlag
;
5963 newWarningFlags
|= warningFlag
;
5964 redisplayStartTimeMs
= 0;
5966 #ifdef USE_DEV_TOOLS
5967 if (systemConfig()->groundTestMode
) {
5971 /* Warnings displayed in full for set time before shrinking down to alert symbol with warning count only.
5972 * All current warnings then redisplayed for 5s on 30s rolling cycle.
5973 * New warnings dislayed individually for 10s */
5974 if (currentTimeMs
> redisplayStartTimeMs
) {
5975 warningDisplayStartTime
= currentTimeMs
;
5976 osdWarningTimerDuration
= newWarningFlags
? 10000 : WARNING_REDISPLAY_DURATION
;
5977 redisplayStartTimeMs
= currentTimeMs
+ osdWarningTimerDuration
+ 30000;
5980 if (currentTimeMs
- warningDisplayStartTime
< osdWarningTimerDuration
) {
5981 return (newWarningFlags
& warningFlag
) || osdWarningTimerDuration
== WARNING_REDISPLAY_DURATION
;
5983 newWarningFlags
= 0;
5985 *warningsCount
+= 1;
5986 } else if (osdWarningsFlags
& warningFlag
) {
5987 osdWarningsFlags
&= ~warningFlag
;
5993 static textAttributes_t
osdGetMultiFunctionMessage(char *buff
)
5995 /* Message length limit 10 char max */
5997 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
5998 static uint8_t warningsCount
;
5999 const char *message
= NULL
;
6001 #ifdef USE_MULTI_FUNCTIONS
6002 /* --- FUNCTIONS --- */
6003 multi_function_e selectedFunction
= multiFunctionSelection();
6005 if (selectedFunction
) {
6006 multi_function_e activeFunction
= selectedFunction
;
6008 switch (selectedFunction
) {
6009 case MULTI_FUNC_NONE
:
6011 message
= warningsCount
? "WARNINGS !" : "0 WARNINGS";
6014 message
= posControl
.flags
.manualEmergLandActive
? "ABORT LAND" : "EMERG LAND";
6017 #if defined(USE_SAFE_HOME)
6018 if (navConfig()->general
.flags
.safehome_usage_mode
!= SAFEHOME_USAGE_OFF
) {
6019 message
= MULTI_FUNC_FLAG(MF_SUSPEND_SAFEHOMES
) ? "USE SFHOME" : "SUS SFHOME";
6026 if (navConfig()->general
.flags
.rth_trackback_mode
!= RTH_TRACKBACK_OFF
) {
6027 message
= MULTI_FUNC_FLAG(MF_SUSPEND_TRACKBACK
) ? "USE TKBACK" : "SUS TKBACK";
6034 if (STATE(MULTIROTOR
)) {
6035 message
= MULTI_FUNC_FLAG(MF_TURTLE_MODE
) ? "END TURTLE" : "USE TURTLE";
6042 message
= ARMING_FLAG(ARMED
) ? "NOW ARMED " : "EMERG ARM ";
6044 case MULTI_FUNC_END
:
6048 if (activeFunction
!= selectedFunction
) {
6049 setMultifunctionSelection(activeFunction
);
6052 strcpy(buff
, message
);
6054 if (isNextMultifunctionItemAvailable()) {
6055 // provides feedback indicating when a new selection command has been received by flight controller
6061 #endif // MULTIFUNCTION - functions only, warnings always defined
6063 /* --- WARNINGS --- */
6064 const char *messages
[7];
6065 uint8_t messageCount
= 0;
6066 bool warningCondition
= false;
6068 uint8_t warningFlagID
= 1;
6071 const batteryState_e batteryState
= getBatteryState();
6072 warningCondition
= batteryState
== BATTERY_CRITICAL
|| batteryState
== BATTERY_WARNING
;
6073 if (osdCheckWarning(warningCondition
, warningFlagID
, &warningsCount
)) {
6074 messages
[messageCount
++] = batteryState
== BATTERY_CRITICAL
? "BATT EMPTY" : "BATT LOW !";
6077 #if defined(USE_GPS)
6078 // GPS Fix and Failure
6079 if (feature(FEATURE_GPS
)) {
6080 if (osdCheckWarning(!STATE(GPS_FIX
), warningFlagID
<<= 1, &warningsCount
)) {
6081 bool gpsFailed
= getHwGPSStatus() == HW_SENSOR_UNAVAILABLE
;
6082 messages
[messageCount
++] = gpsFailed
? "GPS FAILED" : "NO GPS FIX";
6086 // RTH sanity (warning if RTH heads 200m further away from home than closest point)
6087 warningCondition
= NAV_Status
.state
== MW_NAV_STATE_RTH_ENROUTE
&& !posControl
.flags
.rthTrackbackActive
&&
6088 (posControl
.homeDistance
- posControl
.rthSanityChecker
.minimalDistanceToHome
) > 20000;
6089 if (osdCheckWarning(warningCondition
, warningFlagID
<<= 1, &warningsCount
)) {
6090 messages
[messageCount
++] = "RTH SANITY";
6093 // Altitude sanity (warning if significant mismatch between estimated and GPS altitude)
6094 if (osdCheckWarning(posControl
.flags
.gpsCfEstimatedAltitudeMismatch
, warningFlagID
<<= 1, &warningsCount
)) {
6095 messages
[messageCount
++] = "ALT SANITY";
6099 #if defined(USE_MAG)
6100 // Magnetometer failure
6101 if (requestedSensors
[SENSOR_INDEX_MAG
] != MAG_NONE
) {
6102 hardwareSensorStatus_e magStatus
= getHwCompassStatus();
6103 if (osdCheckWarning(magStatus
== HW_SENSOR_UNAVAILABLE
|| magStatus
== HW_SENSOR_UNHEALTHY
, warningFlagID
<<= 1, &warningsCount
)) {
6104 messages
[messageCount
++] = "MAG FAILED";
6108 // Vibration levels TODO - needs better vibration measurement to be useful
6109 // const float vibrationLevel = accGetVibrationLevel();
6110 // warningCondition = vibrationLevel > 1.5f;
6111 // if (osdCheckWarning(warningCondition, warningFlagID <<= 1, &warningsCount)) {
6112 // messages[messageCount++] = vibrationLevel > 2.5f ? "BAD VIBRTN" : "VIBRATION!";
6115 #ifdef USE_DEV_TOOLS
6116 if (osdCheckWarning(systemConfig()->groundTestMode
, warningFlagID
<<= 1, &warningsCount
)) {
6117 messages
[messageCount
++] = "GRD TEST !";
6122 message
= messages
[OSD_ALTERNATING_CHOICES(1000, messageCount
)]; // display each warning on 1s cycle
6123 strcpy(buff
, message
);
6124 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
6125 } else if (warningsCount
) {
6126 buff
[0] = SYM_ALERT
;
6127 tfp_sprintf(buff
+ 1, "%u ", warningsCount
);