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
36 #include "build/debug.h"
37 #include "build/version.h"
40 #include "cms/cms_types.h"
41 #include "cms/cms_menu_osd.h"
43 #include "common/axis.h"
44 #include "common/constants.h"
45 #include "common/filter.h"
46 #include "common/log.h"
47 #include "common/olc.h"
48 #include "common/printf.h"
49 #include "common/string_light.h"
50 #include "common/time.h"
51 #include "common/typeconversion.h"
52 #include "common/utils.h"
54 #include "config/feature.h"
55 #include "config/parameter_group.h"
56 #include "config/parameter_group_ids.h"
58 #include "drivers/display.h"
59 #include "drivers/display_canvas.h"
60 #include "drivers/display_font_metadata.h"
61 #include "drivers/osd_symbols.h"
62 #include "drivers/time.h"
63 #include "drivers/vtx_common.h"
65 #include "io/flashfs.h"
68 #include "io/osd_common.h"
69 #include "io/osd_hud.h"
70 #include "io/osd_utils.h"
71 #include "io/displayport_msp_bf_compat.h"
73 #include "io/vtx_string.h"
75 #include "io/osd/custom_elements.h"
77 #include "fc/config.h"
78 #include "fc/controlrate_profile.h"
79 #include "fc/fc_core.h"
80 #include "fc/fc_tasks.h"
81 #include "fc/multifunction.h"
82 #include "fc/rc_adjustments.h"
83 #include "fc/rc_controls.h"
84 #include "fc/rc_modes.h"
85 #include "fc/runtime_config.h"
86 #include "fc/settings.h"
88 #include "flight/imu.h"
89 #include "flight/mixer.h"
90 #include "flight/pid.h"
91 #include "flight/power_limits.h"
92 #include "flight/rth_estimator.h"
93 #include "flight/servos.h"
94 #include "flight/wind_estimator.h"
96 #include "navigation/navigation.h"
97 #include "navigation/navigation_private.h"
100 #include "rx/msp_override.h"
102 #include "sensors/acceleration.h"
103 #include "sensors/battery.h"
104 #include "sensors/boardalignment.h"
105 #include "sensors/compass.h"
106 #include "sensors/diagnostics.h"
107 #include "sensors/sensors.h"
108 #include "sensors/pitotmeter.h"
109 #include "sensors/temperature.h"
110 #include "sensors/esc_sensor.h"
111 #include "sensors/rangefinder.h"
113 #include "programming/logic_condition.h"
114 #include "programming/global_variables.h"
116 #ifdef USE_HARDWARE_REVISION_DETECTION
117 #include "hardware_revision.h"
120 #define VIDEO_BUFFER_CHARS_PAL 480
121 #define VIDEO_BUFFER_CHARS_HDZERO 900
122 #define VIDEO_BUFFER_CHARS_DJIWTF 1320
124 #define GFORCE_FILTER_TC 0.2
126 #define OSD_STATS_SINGLE_PAGE_MIN_ROWS 18
127 #define IS_HI(X) (rxGetChannelValue(X) > 1750)
128 #define IS_LO(X) (rxGetChannelValue(X) < 1250)
129 #define IS_MID(X) (rxGetChannelValue(X) > 1250 && rxGetChannelValue(X) < 1750)
131 #define OSD_RESUME_UPDATES_STICK_COMMAND (checkStickPosition(THR_HI) || checkStickPosition(PIT_HI))
132 #define STATS_PAGE2 (checkStickPosition(ROL_HI))
133 #define STATS_PAGE1 (checkStickPosition(ROL_LO))
135 #define SPLASH_SCREEN_DISPLAY_TIME 4000 // ms
136 #define STATS_SCREEN_DISPLAY_TIME 60000 // ms
138 #define EFFICIENCY_UPDATE_INTERVAL (5 * 1000)
140 // Adjust OSD_MESSAGE's default position when
141 // changing OSD_MESSAGE_LENGTH
142 #define OSD_MESSAGE_LENGTH 28
143 #define OSD_ALTERNATING_CHOICES(ms, num_choices) ((millis() / ms) % num_choices)
144 #define _CONST_STR_SIZE(s) ((sizeof(s)/sizeof(s[0]))-1) // -1 to avoid counting final '\0'
145 // Wrap all string constants intenteded for display as messages with
146 // this macro to ensure compile time length validation.
147 #define OSD_MESSAGE_STR(x) ({ \
148 STATIC_ASSERT(_CONST_STR_SIZE(x) <= OSD_MESSAGE_LENGTH, message_string_ ## __COUNTER__ ## _too_long); \
152 #define OSD_CHR_IS_NUM(c) (c >= '0' && c <= '9')
154 #define OSD_CENTER_LEN(x) ((osdDisplayPort->cols - x) / 2)
155 #define OSD_CENTER_S(s) OSD_CENTER_LEN(strlen(s))
157 #define OSD_MIN_FONT_VERSION 3
159 static timeMs_t linearDescentMessageMs
= 0;
160 static timeMs_t notify_settings_saved
= 0;
161 static bool savingSettings
= false;
163 static unsigned currentLayout
= 0;
164 static int layoutOverride
= -1;
165 static bool hasExtendedFont
= false; // Wether the font supports characters > 256
166 static timeMs_t layoutOverrideUntil
= 0;
167 static pt1Filter_t GForceFilter
, GForceFilterAxis
[XYZ_AXIS_COUNT
];
168 static float GForce
, GForceAxis
[XYZ_AXIS_COUNT
];
170 typedef struct statistic_s
{
172 uint16_t max_3D_speed
;
173 uint16_t max_air_speed
;
174 uint16_t min_voltage
; // /100
178 int16_t min_lq
; // for CRSF
179 int16_t min_rssi_dbm
; // for CRSF
180 int32_t max_altitude
;
181 uint32_t max_distance
;
184 static statistic_t stats
;
186 static timeUs_t resumeRefreshAt
= 0;
187 static bool refreshWaitForResumeCmdRelease
;
189 static bool fullRedraw
= false;
191 static uint8_t armState
;
193 static textAttributes_t
osdGetMultiFunctionMessage(char *buff
);
194 static uint8_t osdWarningsFlags
= 0;
196 typedef struct osdMapData_s
{
198 char referenceSymbol
;
201 static osdMapData_t osdMapData
;
203 static displayPort_t
*osdDisplayPort
;
204 static bool osdDisplayIsReady
= false;
205 #if defined(USE_CANVAS)
206 static displayCanvas_t osdCanvas
;
207 static bool osdDisplayHasCanvas
;
209 #define osdDisplayHasCanvas false
212 #define AH_MAX_PITCH_DEFAULT 20 // Specify default maximum AHI pitch value displayed (degrees)
214 PG_REGISTER_WITH_RESET_TEMPLATE(osdConfig_t
, osdConfig
, PG_OSD_CONFIG
, 9);
215 PG_REGISTER_WITH_RESET_FN(osdLayoutsConfig_t
, osdLayoutsConfig
, PG_OSD_LAYOUTS_CONFIG
, 1);
217 void osdStartedSaveProcess(void) {
218 savingSettings
= true;
221 void osdShowEEPROMSavedNotification(void) {
222 savingSettings
= false;
223 notify_settings_saved
= millis() + 5000;
228 bool osdDisplayIsPAL(void)
230 return displayScreenSize(osdDisplayPort
) == VIDEO_BUFFER_CHARS_PAL
;
233 bool osdDisplayIsHD(void)
235 if (displayScreenSize(osdDisplayPort
) >= VIDEO_BUFFER_CHARS_HDZERO
)
245 * Aligns text to the left side. Adds spaces at the end to keep string length unchanged.
247 static void osdLeftAlignString(char *buff
)
249 uint8_t sp
= 0, ch
= 0;
250 uint8_t len
= strlen(buff
);
251 while (buff
[sp
] == ' ') sp
++;
252 for (ch
= 0; ch
< (len
- sp
); ch
++) buff
[ch
] = buff
[ch
+ sp
];
253 for (sp
= ch
; sp
< len
; sp
++) buff
[sp
] = ' ';
257 * This is a simplified distance conversion code that does not use any scaling
258 * but is fully compatible with the DJI G2 MSP Displayport OSD implementation.
259 * (Based on osdSimpleAltitudeSymbol() implementation)
261 /* void osdSimpleDistanceSymbol(char *buff, int32_t dist) {
263 int32_t convertedDistance;
266 switch ((osd_unit_e)osdConfig()->units) {
271 case OSD_UNIT_IMPERIAL:
272 convertedDistance = CENTIMETERS_TO_FEET(dist);
275 case OSD_UNIT_METRIC_MPH:
277 case OSD_UNIT_METRIC:
278 convertedDistance = CENTIMETERS_TO_METERS(dist);
279 suffix = SYM_ALT_M; // Intentionally use the altitude symbol, as the distance symbol is not defined in BFCOMPAT mode
283 tfp_sprintf(buff, "%5d", (int) convertedDistance); // 5 digits, allowing up to 99999 meters/feet, which should be plenty for 99.9% of use cases
289 * Converts distance into a string based on the current unit system
290 * prefixed by a a symbol to indicate the unit used.
291 * @param dist Distance in centimeters
293 static void osdFormatDistanceSymbol(char *buff
, int32_t dist
, uint8_t decimals
)
295 uint8_t digits
= 3U; // Total number of digits (including decimal point)
296 uint8_t sym_index
= 3U; // Position (index) at buffer of units symbol
297 uint8_t symbol_m
= SYM_DIST_M
;
298 uint8_t symbol_km
= SYM_DIST_KM
;
299 uint8_t symbol_ft
= SYM_DIST_FT
;
300 uint8_t symbol_mi
= SYM_DIST_MI
;
301 uint8_t symbol_nm
= SYM_DIST_NM
;
303 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
304 if (isBfCompatibleVideoSystem(osdConfig())) {
305 // Add one digit so up no switch to scaled decimal occurs above 99
308 // Use altitude symbols on purpose, as it seems distance symbols are not defined in BFCOMPAT mode
309 symbol_m
= SYM_ALT_M
;
310 symbol_km
= SYM_ALT_KM
;
311 symbol_ft
= SYM_ALT_FT
;
317 switch ((osd_unit_e
)osdConfig()->units
) {
320 case OSD_UNIT_IMPERIAL
:
321 if (osdFormatCentiNumber(buff
, CENTIMETERS_TO_CENTIFEET(dist
), FEET_PER_MILE
, decimals
, 3, digits
, false)) {
322 buff
[sym_index
] = symbol_mi
;
324 buff
[sym_index
] = symbol_ft
;
326 buff
[sym_index
+ 1] = '\0';
328 case OSD_UNIT_METRIC_MPH
:
330 case OSD_UNIT_METRIC
:
331 if (osdFormatCentiNumber(buff
, dist
, METERS_PER_KILOMETER
, decimals
, 3, digits
, false)) {
332 buff
[sym_index
] = symbol_km
;
334 buff
[sym_index
] = symbol_m
;
336 buff
[sym_index
+ 1] = '\0';
339 if (osdFormatCentiNumber(buff
, CENTIMETERS_TO_CENTIFEET(dist
), (uint32_t)FEET_PER_NAUTICALMILE
, decimals
, 3, digits
, false)) {
340 buff
[sym_index
] = symbol_nm
;
342 buff
[sym_index
] = symbol_ft
;
344 buff
[sym_index
+ 1] = '\0';
350 * Converts distance into a string based on the current unit system.
351 * @param dist Distance in centimeters
353 static void osdFormatDistanceStr(char *buff
, int32_t dist
)
356 switch ((osd_unit_e
)osdConfig()->units
) {
359 case OSD_UNIT_IMPERIAL
:
360 centifeet
= CENTIMETERS_TO_CENTIFEET(dist
);
361 if (abs(centifeet
) < FEET_PER_MILE
* 100 / 2) {
362 // Show feet when dist < 0.5mi
363 tfp_sprintf(buff
, "%d%c", (int)(centifeet
/ 100), SYM_FT
);
365 // Show miles when dist >= 0.5mi
366 tfp_sprintf(buff
, "%d.%02d%c", (int)(centifeet
/ (100*FEET_PER_MILE
)),
367 (abs(centifeet
) % (100 * FEET_PER_MILE
)) / FEET_PER_MILE
, SYM_MI
);
370 case OSD_UNIT_METRIC_MPH
:
372 case OSD_UNIT_METRIC
:
373 if (abs(dist
) < METERS_PER_KILOMETER
* 100) {
374 // Show meters when dist < 1km
375 tfp_sprintf(buff
, "%d%c", (int)(dist
/ 100), SYM_M
);
377 // Show kilometers when dist >= 1km
378 tfp_sprintf(buff
, "%d.%02d%c", (int)(dist
/ (100*METERS_PER_KILOMETER
)),
379 (abs(dist
) % (100 * METERS_PER_KILOMETER
)) / METERS_PER_KILOMETER
, SYM_KM
);
383 centifeet
= CENTIMETERS_TO_CENTIFEET(dist
);
384 if (abs(centifeet
) < 100000) {
385 // Show feet when dist < 1000ft
386 tfp_sprintf(buff
, "%d%c", (int)(centifeet
/ 100), SYM_FT
);
388 // Show nautical miles when dist >= 1000ft
389 tfp_sprintf(buff
, "%d.%02d%c", (int)(centifeet
/ (100 * FEET_PER_NAUTICALMILE
)),
390 (int)((abs(centifeet
) % (int)(100 * FEET_PER_NAUTICALMILE
)) / FEET_PER_NAUTICALMILE
), SYM_NM
);
397 * Converts velocity based on the current unit system (kmh or mph).
398 * @param alt Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/second)
400 static int32_t osdConvertVelocityToUnit(int32_t vel
)
402 switch ((osd_unit_e
)osdConfig()->units
) {
405 case OSD_UNIT_METRIC_MPH
:
407 case OSD_UNIT_IMPERIAL
:
408 return CMSEC_TO_CENTIMPH(vel
) / 100; // Convert to mph
409 case OSD_UNIT_METRIC
:
410 return CMSEC_TO_CENTIKPH(vel
) / 100; // Convert to kmh
412 return CMSEC_TO_CENTIKNOTS(vel
) / 100; // Convert to Knots
419 * Converts velocity into a string based on the current unit system.
420 * @param vel Raw velocity (i.e. as taken from gpsSol.groundSpeed in centimeters/seconds)
421 * @param _3D is a 3D velocity
422 * @param _max is a maximum velocity
424 void osdFormatVelocityStr(char* buff
, int32_t vel
, bool _3D
, bool _max
)
426 switch ((osd_unit_e
)osdConfig()->units
) {
429 case OSD_UNIT_METRIC_MPH
:
431 case OSD_UNIT_IMPERIAL
:
433 tfp_sprintf(buff
, "%c%3d%c", SYM_MAX
, (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_MPH
: SYM_MPH
));
435 tfp_sprintf(buff
, "%3d%c", (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_MPH
: SYM_MPH
));
438 case OSD_UNIT_METRIC
:
440 tfp_sprintf(buff
, "%c%3d%c", SYM_MAX
, (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_KMH
: SYM_KMH
));
442 tfp_sprintf(buff
, "%3d%c", (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_KMH
: SYM_KMH
));
447 tfp_sprintf(buff
, "%c%3d%c", SYM_MAX
, (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_KT
: SYM_KT
));
449 tfp_sprintf(buff
, "%3d%c", (int)osdConvertVelocityToUnit(vel
), (_3D
? SYM_3D_KT
: SYM_KT
));
456 * 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
458 static void osdGenerateAverageVelocityStr(char* buff
) {
459 uint32_t cmPerSec
= getTotalTravelDistance() / getFlightTime();
460 osdFormatVelocityStr(buff
, cmPerSec
, false, false);
464 * Converts wind speed into a string based on the current unit system, using
465 * always 3 digits and an additional character for the unit at the right. buff
466 * is null terminated.
467 * @param ws Raw wind speed in cm/s
469 #ifdef USE_WIND_ESTIMATOR
470 static void osdFormatWindSpeedStr(char *buff
, int32_t ws
, bool isValid
)
474 switch (osdConfig()->units
) {
477 case OSD_UNIT_METRIC_MPH
:
479 case OSD_UNIT_IMPERIAL
:
480 centivalue
= CMSEC_TO_CENTIMPH(ws
);
484 centivalue
= CMSEC_TO_CENTIKNOTS(ws
);
488 case OSD_UNIT_METRIC
:
489 centivalue
= CMSEC_TO_CENTIKPH(ws
);
494 osdFormatCentiNumber(buff
, centivalue
, 0, 2, 0, 3, false);
496 if (!isValid
&& ((millis() / 1000) % 4 < 2))
505 * This is a simplified altitude conversion code that does not use any scaling
506 * but is fully compatible with the DJI G2 MSP Displayport OSD implementation.
508 /* void osdSimpleAltitudeSymbol(char *buff, int32_t alt) {
510 int32_t convertedAltutude = 0;
513 switch ((osd_unit_e)osdConfig()->units) {
518 case OSD_UNIT_IMPERIAL:
519 convertedAltutude = CENTIMETERS_TO_FEET(alt);
522 case OSD_UNIT_METRIC_MPH:
524 case OSD_UNIT_METRIC:
525 convertedAltutude = CENTIMETERS_TO_METERS(alt);
530 tfp_sprintf(buff, "%4d", (int) convertedAltutude);
536 * Converts altitude into a string based on the current unit system
537 * prefixed by a a symbol to indicate the unit used.
538 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
540 void osdFormatAltitudeSymbol(char *buff
, int32_t alt
)
542 uint8_t totalDigits
= 4U;
544 uint8_t symbolIndex
= 4U;
545 uint8_t symbolKFt
= SYM_ALT_KFT
;
552 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
553 if (isBfCompatibleVideoSystem(osdConfig())) {
557 symbolKFt
= SYM_ALT_FT
;
561 switch ((osd_unit_e
)osdConfig()->units
) {
566 case OSD_UNIT_IMPERIAL
:
567 if (osdFormatCentiNumber(buff
+ totalDigits
- digits
, CENTIMETERS_TO_CENTIFEET(alt
), 1000, 0, 2, digits
, false)) {
569 buff
[symbolIndex
++] = symbolKFt
;
572 buff
[symbolIndex
++] = SYM_ALT_FT
;
574 buff
[symbolIndex
] = '\0';
576 case OSD_UNIT_METRIC_MPH
:
578 case OSD_UNIT_METRIC
:
579 // alt is alredy in cm
580 if (osdFormatCentiNumber(buff
+ totalDigits
- digits
, alt
, 1000, 0, 2, digits
, false)) {
582 buff
[symbolIndex
++] = SYM_ALT_KM
;
585 buff
[symbolIndex
++] = SYM_ALT_M
;
587 buff
[symbolIndex
] = '\0';
593 * Converts altitude into a string based on the current unit system.
594 * @param alt Raw altitude/distance (i.e. as taken from baro.BaroAlt in centimeters)
596 static void osdFormatAltitudeStr(char *buff
, int32_t alt
)
599 switch ((osd_unit_e
)osdConfig()->units
) {
604 case OSD_UNIT_IMPERIAL
:
605 value
= CENTIMETERS_TO_FEET(alt
);
606 tfp_sprintf(buff
, "%d%c", (int)value
, SYM_FT
);
608 case OSD_UNIT_METRIC_MPH
:
610 case OSD_UNIT_METRIC
:
611 value
= CENTIMETERS_TO_METERS(alt
);
612 tfp_sprintf(buff
, "%d%c", (int)value
, SYM_M
);
617 static void osdFormatTime(char *buff
, uint32_t seconds
, char sym_m
, char sym_h
)
619 uint32_t value
= seconds
;
621 // Maximum value we can show in minutes is 99 minutes and 59 seconds
622 if (seconds
> (99 * 60) + 59) {
624 value
= seconds
/ 60;
627 tfp_sprintf(buff
+ 1, "%02d:%02d", (int)(value
/ 60), (int)(value
% 60));
630 static inline void osdFormatOnTime(char *buff
)
632 osdFormatTime(buff
, micros() / 1000000, SYM_ON_M
, SYM_ON_H
);
635 static inline void osdFormatFlyTime(char *buff
, textAttributes_t
*attr
)
637 uint32_t seconds
= getFlightTime();
638 osdFormatTime(buff
, seconds
, SYM_FLY_M
, SYM_FLY_H
);
639 if (attr
&& osdConfig()->time_alarm
> 0) {
640 if (seconds
/ 60 >= osdConfig()->time_alarm
&& ARMING_FLAG(ARMED
)) {
641 TEXT_ATTRIBUTES_ADD_BLINK(*attr
);
647 * Trim whitespace from string.
648 * Used in Stats screen on lines with multiple values.
650 char *osdFormatTrimWhiteSpace(char *buff
)
654 // Trim leading spaces
655 while(isspace((unsigned char)*buff
)) buff
++;
661 // Trim trailing spaces
662 end
= buff
+ strlen(buff
) - 1;
663 while(end
> buff
&& isspace((unsigned char)*end
)) end
--;
665 // Write new null terminator character
672 * Converts RSSI into a % value used by the OSD.
674 static uint16_t osdConvertRSSI(void)
676 // change range to [0, 99]
677 return constrain(getRSSI() * 100 / RSSI_MAX_VALUE
, 0, 99);
680 static uint16_t osdGetCrsfLQ(void)
682 int16_t statsLQ
= rxLinkStatistics
.uplinkLQ
;
683 int16_t scaledLQ
= scaleRange(constrain(statsLQ
, 0, 100), 0, 100, 170, 300);
684 int16_t displayedLQ
= 0;
685 switch (osdConfig()->crsf_lq_format
) {
686 case OSD_CRSF_LQ_TYPE1
:
687 displayedLQ
= statsLQ
;
689 case OSD_CRSF_LQ_TYPE2
:
690 displayedLQ
= statsLQ
;
692 case OSD_CRSF_LQ_TYPE3
:
693 displayedLQ
= rxLinkStatistics
.rfMode
>= 2 ? scaledLQ
: statsLQ
;
699 static int16_t osdGetCrsfdBm(void)
701 return rxLinkStatistics
.uplinkRSSI
;
704 * Displays a temperature postfixed with a symbol depending on the current unit system
705 * @param label to display
706 * @param valid true if measurement is valid
707 * @param temperature in deciDegrees Celcius
709 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
)
711 char buff
[TEMPERATURE_LABEL_LEN
+ 2 < 6 ? 6 : TEMPERATURE_LABEL_LEN
+ 2];
712 textAttributes_t elemAttr
= valid
? TEXT_ATTRIBUTES_NONE
: _TEXT_ATTRIBUTES_BLINK_BIT
;
713 uint8_t valueXOffset
= 0;
718 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
721 #ifdef USE_TEMPERATURE_SENSOR
722 else if (label
[0] != '\0') {
723 uint8_t label_len
= strnlen(label
, TEMPERATURE_LABEL_LEN
);
724 memcpy(buff
, label
, label_len
);
725 memset(buff
+ label_len
, ' ', TEMPERATURE_LABEL_LEN
+ 1 - label_len
);
727 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
728 valueXOffset
= osdConfig()->temp_label_align
== OSD_ALIGN_LEFT
? 5 : label_len
+ 1;
736 if ((temperature
<= alarm_min
) || (temperature
>= alarm_max
)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
737 if (osdConfig()->units
== OSD_UNIT_IMPERIAL
) temperature
= temperature
* 9 / 5.0f
+ 320;
738 tfp_sprintf(buff
, "%3d", temperature
/ 10);
743 buff
[3] = osdConfig()->units
== OSD_UNIT_IMPERIAL
? SYM_TEMP_F
: SYM_TEMP_C
;
746 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ valueXOffset
, elemPosY
, buff
, elemAttr
);
749 #ifdef USE_TEMPERATURE_SENSOR
750 static void osdDisplayTemperatureSensor(uint8_t elemPosX
, uint8_t elemPosY
, uint8_t sensorIndex
)
753 const bool valid
= getSensorTemperature(sensorIndex
, &temperature
);
754 const tempSensorConfig_t
*sensorConfig
= tempSensorConfig(sensorIndex
);
755 uint16_t symbol
= sensorConfig
->osdSymbol
? SYM_TEMP_SENSOR_FIRST
+ sensorConfig
->osdSymbol
- 1 : 0;
756 osdDisplayTemperature(elemPosX
, elemPosY
, symbol
, sensorConfig
->label
, valid
, temperature
, sensorConfig
->alarm_min
, sensorConfig
->alarm_max
);
760 static void osdFormatCoordinate(char *buff
, char sym
, int32_t val
)
762 // up to 4 for number + 1 for the symbol + null terminator + fill the rest with decimals
763 const int coordinateLength
= osdConfig()->coordinate_digits
+ 1;
766 int32_t integerPart
= val
/ GPS_DEGREES_DIVIDER
;
767 // Latitude maximum integer width is 3 (-90) while
768 // longitude maximum integer width is 4 (-180).
769 int integerDigits
= tfp_sprintf(buff
+ 1, (integerPart
== 0 && val
< 0) ? "-%d" : "%d", (int)integerPart
);
770 // We can show up to 7 digits in decimalPart.
771 int32_t decimalPart
= abs(val
% (int)GPS_DEGREES_DIVIDER
);
772 STATIC_ASSERT(GPS_DEGREES_DIVIDER
== 1e7
, adjust_max_decimal_digits
);
774 bool bfcompat
= false; // Assume BFCOMPAT mode is no enabled
776 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
777 if(isBfCompatibleVideoSystem(osdConfig())) {
783 decimalDigits
= tfp_sprintf(buff
+ 1 + integerDigits
, "%07d", (int)decimalPart
);
784 // Embbed the decimal separator
785 buff
[1 + integerDigits
- 1] += SYM_ZERO_HALF_TRAILING_DOT
- '0';
786 buff
[1 + integerDigits
] += SYM_ZERO_HALF_LEADING_DOT
- '0';
788 // BFCOMPAT mode enabled
789 decimalDigits
= tfp_sprintf(buff
+ 1 + integerDigits
, ".%06d", (int)decimalPart
);
791 // Fill up to coordinateLength with zeros
792 int total
= 1 + integerDigits
+ decimalDigits
;
793 while(total
< coordinateLength
) {
797 buff
[coordinateLength
] = '\0';
800 static void osdFormatCraftName(char *buff
)
802 if (strlen(systemConfig()->craftName
) == 0)
803 strcpy(buff
, "CRAFT_NAME");
805 for (int i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
806 buff
[i
] = sl_toupper((unsigned char)systemConfig()->craftName
[i
]);
807 if (systemConfig()->craftName
[i
] == 0)
813 void osdFormatPilotName(char *buff
)
815 if (strlen(systemConfig()->pilotName
) == 0)
816 strcpy(buff
, "PILOT_NAME");
818 for (int i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
819 buff
[i
] = sl_toupper((unsigned char)systemConfig()->pilotName
[i
]);
820 if (systemConfig()->pilotName
[i
] == 0)
826 static const char * osdArmingDisabledReasonMessage(void)
828 const char *message
= NULL
;
829 char messageBuf
[MAX(SETTING_MAX_NAME_LENGTH
, OSD_MESSAGE_LENGTH
+1)];
831 switch (isArmingDisabledReason()) {
832 case ARMING_DISABLED_FAILSAFE_SYSTEM
:
833 // See handling of FAILSAFE_RX_LOSS_MONITORING in failsafe.c
834 if (failsafePhase() == FAILSAFE_RX_LOSS_MONITORING
) {
835 if (failsafeIsReceivingRxData()) {
836 // reminder to disarm to exit FAILSAFE_RX_LOSS_MONITORING once timeout period ends
837 if (IS_RC_MODE_ACTIVE(BOXARM
)) {
838 return OSD_MESSAGE_STR(OSD_MSG_TURN_ARM_SW_OFF
);
841 // Not receiving RX data
842 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST
);
845 return OSD_MESSAGE_STR(OSD_MSG_DISABLED_BY_FS
);
846 case ARMING_DISABLED_NOT_LEVEL
:
847 return OSD_MESSAGE_STR(OSD_MSG_AIRCRAFT_UNLEVEL
);
848 case ARMING_DISABLED_SENSORS_CALIBRATING
:
849 return OSD_MESSAGE_STR(OSD_MSG_SENSORS_CAL
);
850 case ARMING_DISABLED_SYSTEM_OVERLOADED
:
851 return OSD_MESSAGE_STR(OSD_MSG_SYS_OVERLOADED
);
852 case ARMING_DISABLED_NAVIGATION_UNSAFE
:
853 // Check the exact reason
854 switch (navigationIsBlockingArming(NULL
)) {
856 case NAV_ARMING_BLOCKER_NONE
:
858 case NAV_ARMING_BLOCKER_MISSING_GPS_FIX
:
859 return OSD_MESSAGE_STR(OSD_MSG_WAITING_GPS_FIX
);
860 case NAV_ARMING_BLOCKER_NAV_IS_ALREADY_ACTIVE
:
861 return OSD_MESSAGE_STR(OSD_MSG_DISABLE_NAV_FIRST
);
862 case NAV_ARMING_BLOCKER_FIRST_WAYPOINT_TOO_FAR
:
863 osdFormatDistanceSymbol(buf
, distanceToFirstWP(), 0);
864 tfp_sprintf(messageBuf
, "FIRST WP TOO FAR (%s)", buf
);
865 return message
= messageBuf
;
866 case NAV_ARMING_BLOCKER_JUMP_WAYPOINT_ERROR
:
867 return OSD_MESSAGE_STR(OSD_MSG_JUMP_WP_MISCONFIG
);
870 case ARMING_DISABLED_COMPASS_NOT_CALIBRATED
:
871 return OSD_MESSAGE_STR(OSD_MSG_MAG_NOT_CAL
);
872 case ARMING_DISABLED_ACCELEROMETER_NOT_CALIBRATED
:
873 return OSD_MESSAGE_STR(OSD_MSG_ACC_NOT_CAL
);
874 case ARMING_DISABLED_ARM_SWITCH
:
875 return OSD_MESSAGE_STR(OSD_MSG_DISARM_1ST
);
876 case ARMING_DISABLED_HARDWARE_FAILURE
:
878 if (!HW_SENSOR_IS_HEALTHY(getHwGyroStatus())) {
879 return OSD_MESSAGE_STR(OSD_MSG_GYRO_FAILURE
);
881 if (!HW_SENSOR_IS_HEALTHY(getHwAccelerometerStatus())) {
882 return OSD_MESSAGE_STR(OSD_MSG_ACC_FAIL
);
884 if (!HW_SENSOR_IS_HEALTHY(getHwCompassStatus())) {
885 return OSD_MESSAGE_STR(OSD_MSG_MAG_FAIL
);
887 if (!HW_SENSOR_IS_HEALTHY(getHwBarometerStatus())) {
888 return OSD_MESSAGE_STR(OSD_MSG_BARO_FAIL
);
890 if (!HW_SENSOR_IS_HEALTHY(getHwGPSStatus())) {
891 return OSD_MESSAGE_STR(OSD_MSG_GPS_FAIL
);
893 if (!HW_SENSOR_IS_HEALTHY(getHwRangefinderStatus())) {
894 return OSD_MESSAGE_STR(OSD_MSG_RANGEFINDER_FAIL
);
896 if (!HW_SENSOR_IS_HEALTHY(getHwPitotmeterStatus())) {
897 return OSD_MESSAGE_STR(OSD_MSG_PITOT_FAIL
);
900 return OSD_MESSAGE_STR(OSD_MSG_HW_FAIL
);
901 case ARMING_DISABLED_BOXFAILSAFE
:
902 return OSD_MESSAGE_STR(OSD_MSG_FS_EN
);
903 case ARMING_DISABLED_BOXKILLSWITCH
:
904 return OSD_MESSAGE_STR(OSD_MSG_KILL_SW_EN
);
905 case ARMING_DISABLED_RC_LINK
:
906 return OSD_MESSAGE_STR(OSD_MSG_NO_RC_LINK
);
907 case ARMING_DISABLED_THROTTLE
:
908 return OSD_MESSAGE_STR(OSD_MSG_THROTTLE_NOT_LOW
);
909 case ARMING_DISABLED_ROLLPITCH_NOT_CENTERED
:
910 return OSD_MESSAGE_STR(OSD_MSG_ROLLPITCH_OFFCENTER
);
911 case ARMING_DISABLED_SERVO_AUTOTRIM
:
912 return OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM_ACTIVE
);
913 case ARMING_DISABLED_OOM
:
914 return OSD_MESSAGE_STR(OSD_MSG_NOT_ENOUGH_MEMORY
);
915 case ARMING_DISABLED_INVALID_SETTING
:
916 return OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING
);
917 case ARMING_DISABLED_CLI
:
918 return OSD_MESSAGE_STR(OSD_MSG_CLI_ACTIVE
);
919 case ARMING_DISABLED_PWM_OUTPUT_ERROR
:
920 return OSD_MESSAGE_STR(OSD_MSG_PWM_INIT_ERROR
);
921 case ARMING_DISABLED_NO_PREARM
:
922 return OSD_MESSAGE_STR(OSD_MSG_NO_PREARM
);
923 case ARMING_DISABLED_DSHOT_BEEPER
:
924 return OSD_MESSAGE_STR(OSD_MSG_DSHOT_BEEPER
);
925 // Cases without message
926 case ARMING_DISABLED_LANDING_DETECTED
:
928 case ARMING_DISABLED_CMS_MENU
:
930 case ARMING_DISABLED_OSD_MENU
:
932 case ARMING_DISABLED_ALL_FLAGS
:
936 case SIMULATOR_MODE_HITL
:
938 case SIMULATOR_MODE_SITL
:
946 static const char * osdFailsafePhaseMessage(void)
948 // See failsafe.h for each phase explanation
949 switch (failsafePhase()) {
950 case FAILSAFE_RETURN_TO_HOME
:
951 // XXX: Keep this in sync with OSD_FLYMODE.
952 return OSD_MESSAGE_STR(OSD_MSG_RTH_FS
);
953 case FAILSAFE_LANDING
:
954 // This should be considered an emergengy landing
955 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING_FS
);
956 case FAILSAFE_RX_LOSS_MONITORING
:
957 // Only reachable from FAILSAFE_LANDED, which performs
958 // a disarm. Since aircraft has been disarmed, we no
959 // longer show failsafe details.
961 case FAILSAFE_LANDED
:
962 // Very brief, disarms and transitions into
963 // FAILSAFE_RX_LOSS_MONITORING. Note that it prevents
964 // further rearming via ARMING_DISABLED_FAILSAFE_SYSTEM,
965 // so we'll show the user how to re-arm in when
966 // that flag is the reason to prevent arming.
968 case FAILSAFE_RX_LOSS_IDLE
:
969 // This only happens when user has chosen NONE as FS
970 // procedure. The recovery messages should be enough.
973 // Failsafe not active
975 case FAILSAFE_RX_LOSS_DETECTED
:
976 // Very brief, changes to FAILSAFE_RX_LOSS_RECOVERED
977 // or the FS procedure immediately.
979 case FAILSAFE_RX_LOSS_RECOVERED
:
986 static const char * osdFailsafeInfoMessage(void)
988 if (failsafeIsReceivingRxData() && !FLIGHT_MODE(NAV_FW_AUTOLAND
)) {
989 // User must move sticks to exit FS mode
990 return OSD_MESSAGE_STR(OSD_MSG_MOVE_EXIT_FS
);
992 return OSD_MESSAGE_STR(OSD_MSG_RC_RX_LINK_LOST
);
995 #if defined(USE_SAFE_HOME)
996 static const char * divertingToSafehomeMessage(void)
998 #ifdef USE_FW_AUTOLAND
999 if (!posControl
.fwLandState
.landWp
&& (NAV_Status
.state
!= MW_NAV_STATE_HOVER_ABOVE_HOME
&& posControl
.safehomeState
.isApplied
)) {
1001 if (NAV_Status
.state
!= MW_NAV_STATE_HOVER_ABOVE_HOME
&& posControl
.safehomeState
.isApplied
) {
1003 return OSD_MESSAGE_STR(OSD_MSG_DIVERT_SAFEHOME
);
1010 static const char * navigationStateMessage(void)
1012 if (!posControl
.rthState
.rthLinearDescentActive
&& linearDescentMessageMs
!= 0)
1013 linearDescentMessageMs
= 0;
1015 switch (NAV_Status
.state
) {
1016 case MW_NAV_STATE_NONE
:
1018 case MW_NAV_STATE_RTH_START
:
1019 return OSD_MESSAGE_STR(OSD_MSG_STARTING_RTH
);
1020 case MW_NAV_STATE_RTH_CLIMB
:
1021 return OSD_MESSAGE_STR(OSD_MSG_RTH_CLIMB
);
1022 case MW_NAV_STATE_RTH_ENROUTE
:
1023 if (posControl
.flags
.rthTrackbackActive
) {
1024 return OSD_MESSAGE_STR(OSD_MSG_RTH_TRACKBACK
);
1026 if (posControl
.rthState
.rthLinearDescentActive
&& (linearDescentMessageMs
== 0 || linearDescentMessageMs
> millis())) {
1027 if (linearDescentMessageMs
== 0)
1028 linearDescentMessageMs
= millis() + 5000; // Show message for 5 seconds.
1030 return OSD_MESSAGE_STR(OSD_MSG_RTH_LINEAR_DESCENT
);
1032 return OSD_MESSAGE_STR(OSD_MSG_HEADING_HOME
);
1034 case MW_NAV_STATE_HOLD_INFINIT
:
1035 // Used by HOLD flight modes. No information to add.
1037 case MW_NAV_STATE_HOLD_TIMED
:
1038 // "HOLDING WP FOR xx S" Countdown added in osdGetSystemMessage
1040 case MW_NAV_STATE_WP_ENROUTE
:
1041 // "TO WP" + WP countdown added in osdGetSystemMessage
1043 case MW_NAV_STATE_PROCESS_NEXT
:
1044 return OSD_MESSAGE_STR(OSD_MSG_PREPARE_NEXT_WP
);
1045 case MW_NAV_STATE_DO_JUMP
:
1048 case MW_NAV_STATE_LAND_START
:
1051 case MW_NAV_STATE_EMERGENCY_LANDING
:
1052 return OSD_MESSAGE_STR(OSD_MSG_EMERG_LANDING
);
1053 case MW_NAV_STATE_LAND_IN_PROGRESS
:
1054 return OSD_MESSAGE_STR(OSD_MSG_LANDING
);
1055 case MW_NAV_STATE_HOVER_ABOVE_HOME
:
1056 if (STATE(FIXED_WING_LEGACY
)) {
1057 #if defined(USE_SAFE_HOME)
1058 if (posControl
.safehomeState
.isApplied
) {
1059 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_SAFEHOME
);
1062 return OSD_MESSAGE_STR(OSD_MSG_LOITERING_HOME
);
1064 return OSD_MESSAGE_STR(OSD_MSG_HOVERING
);
1065 case MW_NAV_STATE_LANDED
:
1066 return OSD_MESSAGE_STR(OSD_MSG_LANDED
);
1067 case MW_NAV_STATE_LAND_SETTLE
:
1069 // If there is a FS landing delay occurring. That is handled by the calling function.
1070 if (posControl
.landingDelay
> 0)
1073 return OSD_MESSAGE_STR(OSD_MSG_PREPARING_LAND
);
1075 case MW_NAV_STATE_LAND_START_DESCENT
:
1083 static void osdFormatMessage(char *buff
, size_t size
, const char *message
, bool isCenteredText
)
1085 // String is always filled with Blanks
1086 memset(buff
, SYM_BLANK
, size
);
1088 size_t messageLength
= strlen(message
);
1089 int rem
= isCenteredText
? MAX(0, (int)size
- (int)messageLength
) : 0;
1090 strncpy(buff
+ rem
/ 2, message
, MIN((int)size
- rem
/ 2, (int)messageLength
));
1092 // Ensure buff is zero terminated
1097 * Draws the battery symbol filled in accordingly to the
1098 * battery voltage to buff[0].
1100 static void osdFormatBatteryChargeSymbol(char *buff
)
1102 uint8_t p
= calculateBatteryPercentage();
1103 p
= (100 - p
) / 16.6;
1104 buff
[0] = SYM_BATT_FULL
+ p
;
1107 static void osdUpdateBatteryCapacityOrVoltageTextAttributes(textAttributes_t
*attr
)
1109 const batteryState_e batteryState
= getBatteryState();
1111 if (batteryState
== BATTERY_WARNING
|| batteryState
== BATTERY_CRITICAL
) {
1112 TEXT_ATTRIBUTES_ADD_BLINK(*attr
);
1116 void osdCrosshairPosition(uint8_t *x
, uint8_t *y
)
1118 *x
= osdDisplayPort
->cols
/ 2;
1119 *y
= osdDisplayPort
->rows
/ 2;
1120 *y
-= osdConfig()->horizon_offset
; // positive horizon_offset moves the HUD up, negative moves down
1124 * Check if this OSD layout is using scaled or unscaled throttle.
1125 * If both are used, it will default to scaled.
1127 bool osdUsingScaledThrottle(void)
1129 bool usingScaledThrottle
= OSD_VISIBLE(osdLayoutsConfig()->item_pos
[currentLayout
][OSD_SCALED_THROTTLE_POS
]);
1130 bool usingRCThrottle
= OSD_VISIBLE(osdLayoutsConfig()->item_pos
[currentLayout
][OSD_THROTTLE_POS
]);
1132 if (!usingScaledThrottle
&& !usingRCThrottle
)
1133 usingScaledThrottle
= true;
1135 return usingScaledThrottle
;
1139 * Formats throttle position prefixed by its symbol.
1140 * Shows unscaled or scaled (output to motor) throttle percentage
1142 static void osdFormatThrottlePosition(char *buff
, bool useScaled
, textAttributes_t
*elemAttr
)
1144 buff
[0] = SYM_BLANK
;
1146 if (navigationIsControllingThrottle()) {
1147 buff
[0] = SYM_AUTO_THR0
;
1148 buff
[1] = SYM_AUTO_THR1
;
1149 if (isFixedWingAutoThrottleManuallyIncreased()) {
1150 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr
);
1154 #ifdef USE_POWER_LIMITS
1155 if (powerLimiterIsLimiting()) {
1156 TEXT_ATTRIBUTES_ADD_BLINK(*elemAttr
);
1159 int8_t throttlePercent
= getThrottlePercent(useScaled
);
1160 if ((useScaled
&& throttlePercent
<= 0) || !ARMING_FLAG(ARMED
)) {
1161 const char* message
= ARMING_FLAG(ARMED
) ? (throttlePercent
== 0 && !ifMotorstopFeatureEnabled()) ? "IDLE" : "STOP" : "DARM";
1163 strcpy(buff
+ 1, message
);
1166 tfp_sprintf(buff
+ 2, "%3d", throttlePercent
);
1170 * Formats gvars prefixed by its number (0-indexed). If autoThr
1172 static void osdFormatGVar(char *buff
, uint8_t index
)
1175 buff
[1] = '0'+index
;
1177 #ifdef USE_PROGRAMMING_FRAMEWORK
1178 osdFormatCentiNumber(buff
+ 3, (int32_t)gvGet(index
)*(int32_t)100, 1, 0, 0, 5, false);
1182 #if defined(USE_ESC_SENSOR)
1183 static void osdFormatRpm(char *buff
, uint32_t rpm
)
1187 if ( digitCount(rpm
) > osdConfig()->esc_rpm_precision
) {
1188 uint8_t rpmMaxDecimals
= (osdConfig()->esc_rpm_precision
- 3);
1189 osdFormatCentiNumber(buff
+ 1, rpm
/ 10, 0, rpmMaxDecimals
, rpmMaxDecimals
, osdConfig()->esc_rpm_precision
-1, false);
1190 buff
[osdConfig()->esc_rpm_precision
] = 'K';
1191 buff
[osdConfig()->esc_rpm_precision
+1] = '\0';
1194 switch(osdConfig()->esc_rpm_precision
) {
1196 tfp_sprintf(buff
+ 1, "%6lu", rpm
);
1199 tfp_sprintf(buff
+ 1, "%5lu", rpm
);
1202 tfp_sprintf(buff
+ 1, "%4lu", rpm
);
1206 tfp_sprintf(buff
+ 1, "%3lu", rpm
);
1214 uint8_t buffPos
= 1;
1215 while (buffPos
<=( osdConfig()->esc_rpm_precision
)) {
1216 strcpy(buff
+ buffPos
++, "-");
1222 int32_t osdGetAltitude(void)
1224 return getEstimatedActualPosition(Z
);
1227 static inline int32_t osdGetAltitudeMsl(void)
1229 return getEstimatedActualPosition(Z
) + posControl
.gpsOrigin
.alt
;
1232 uint16_t osdGetRemainingGlideTime(void) {
1233 float value
= getEstimatedActualVelocity(Z
);
1234 static pt1Filter_t glideTimeFilterState
;
1235 const timeMs_t curTimeMs
= millis();
1236 static timeMs_t glideTimeUpdatedMs
;
1238 value
= pt1FilterApply4(&glideTimeFilterState
, isnormal(value
) ? value
: 0, 0.5, MS2S(curTimeMs
- glideTimeUpdatedMs
));
1239 glideTimeUpdatedMs
= curTimeMs
;
1242 value
= osdGetAltitude() / abs((int)value
);
1247 return (uint16_t)roundf(value
);
1250 static bool osdIsHeadingValid(void)
1252 return isImuHeadingValid();
1255 int16_t osdGetHeading(void)
1257 return attitude
.values
.yaw
;
1260 int16_t osdGetPanServoOffset(void)
1262 int8_t servoIndex
= osdConfig()->pan_servo_index
;
1263 int16_t servoPosition
= servo
[servoIndex
];
1264 int16_t servoMiddle
= servoParams(servoIndex
)->middle
;
1265 return (int16_t)CENTIDEGREES_TO_DEGREES((servoPosition
- servoMiddle
) * osdConfig()->pan_servo_pwm2centideg
);
1268 // Returns a heading angle in degrees normalized to [0, 360).
1269 int osdGetHeadingAngle(int angle
)
1274 while (angle
>= 360) {
1280 #if defined(USE_GPS)
1282 /* Draws a map with the given symbol in the center and given point of interest
1283 * defined by its distance in meters and direction in degrees.
1284 * referenceHeading indicates the up direction in the map, in degrees, while
1285 * referenceSym (if non-zero) is drawn at the upper right corner below a small
1286 * arrow to indicate the map reference to the user. The drawn argument is an
1287 * in-out used to store the last position where the craft was drawn to avoid
1288 * erasing all screen on each redraw.
1290 static void osdDrawMap(int referenceHeading
, uint16_t referenceSym
, uint16_t centerSym
,
1291 uint32_t poiDistance
, int16_t poiDirection
, uint16_t poiSymbol
,
1292 uint16_t *drawn
, uint32_t *usedScale
)
1294 // TODO: These need to be tested with several setups. We might
1295 // need to make them configurable.
1296 const int hMargin
= 5;
1297 const int vMargin
= 3;
1299 // TODO: Get this from the display driver?
1300 const int charWidth
= 12;
1301 const int charHeight
= 18;
1303 uint8_t minX
= hMargin
;
1304 uint8_t maxX
= osdDisplayPort
->cols
- 1 - hMargin
;
1305 uint8_t minY
= vMargin
;
1306 uint8_t maxY
= osdDisplayPort
->rows
- 1 - vMargin
;
1307 uint8_t midX
= osdDisplayPort
->cols
/ 2;
1308 uint8_t midY
= osdDisplayPort
->rows
/ 2;
1311 displayWriteChar(osdDisplayPort
, midX
, midY
, centerSym
);
1313 // First, erase the previous drawing.
1314 if (OSD_VISIBLE(*drawn
)) {
1315 displayWriteChar(osdDisplayPort
, OSD_X(*drawn
), OSD_Y(*drawn
), SYM_BLANK
);
1319 uint32_t initialScale
;
1320 const unsigned scaleMultiplier
= 2;
1321 // We try to reduce the scale when the POI will be around half the distance
1322 // between the center and the closers map edge, to avoid too much jumping
1323 const int scaleReductionMultiplier
= MIN(midX
- hMargin
, midY
- vMargin
) / 2;
1325 switch (osdConfig()->units
) {
1328 case OSD_UNIT_IMPERIAL
:
1329 initialScale
= 16; // 16m ~= 0.01miles
1332 initialScale
= 18; // 18m ~= 0.01 nautical miles
1335 case OSD_UNIT_METRIC_MPH
:
1337 case OSD_UNIT_METRIC
:
1338 initialScale
= 10; // 10m as initial scale
1342 // Try to keep the same scale when getting closer until we draw over the center point
1343 uint32_t scale
= initialScale
;
1346 if (scale
> initialScale
&& poiDistance
< *usedScale
* scaleReductionMultiplier
) {
1347 scale
/= scaleMultiplier
;
1351 if (STATE(GPS_FIX
)) {
1353 int directionToPoi
= osdGetHeadingAngle(poiDirection
- referenceHeading
);
1354 float poiAngle
= DEGREES_TO_RADIANS(directionToPoi
);
1355 float poiSin
= sin_approx(poiAngle
);
1356 float poiCos
= cos_approx(poiAngle
);
1358 // Now start looking for a valid scale that lets us draw everything
1360 for (ii
= 0; ii
< 50; ii
++) {
1361 // Calculate location of the aircraft in map
1362 int points
= poiDistance
/ ((float)scale
/ charHeight
);
1364 float pointsX
= points
* poiSin
;
1365 int poiX
= midX
- roundf(pointsX
/ charWidth
);
1366 if (poiX
< minX
|| poiX
> maxX
) {
1367 scale
*= scaleMultiplier
;
1371 float pointsY
= points
* poiCos
;
1372 int poiY
= midY
+ roundf(pointsY
/ charHeight
);
1373 if (poiY
< minY
|| poiY
> maxY
) {
1374 scale
*= scaleMultiplier
;
1378 if (poiX
== midX
&& poiY
== midY
) {
1379 // We're over the map center symbol, so we would be drawing
1380 // over it even if we increased the scale. Alternate between
1381 // drawing the center symbol or drawing the POI.
1382 if (centerSym
!= SYM_BLANK
&& OSD_ALTERNATING_CHOICES(1000, 2) == 0) {
1388 if (displayReadCharWithAttr(osdDisplayPort
, poiX
, poiY
, &c
, NULL
) && c
!= SYM_BLANK
) {
1389 // Something else written here, increase scale. If the display doesn't support reading
1390 // back characters, we assume there's nothing.
1392 // If we're close to the center, decrease scale. Otherwise increase it.
1393 uint8_t centerDeltaX
= (maxX
- minX
) / (scaleMultiplier
* 2);
1394 uint8_t centerDeltaY
= (maxY
- minY
) / (scaleMultiplier
* 2);
1395 if (poiX
>= midX
- centerDeltaX
&& poiX
<= midX
+ centerDeltaX
&&
1396 poiY
>= midY
- centerDeltaY
&& poiY
<= midY
+ centerDeltaY
&&
1397 scale
> scaleMultiplier
) {
1399 scale
/= scaleMultiplier
;
1401 scale
*= scaleMultiplier
;
1407 // Draw the point on the map
1408 if (poiSymbol
== SYM_ARROW_UP
) {
1409 // Drawing aircraft, rotate
1410 int mapHeading
= osdGetHeadingAngle(DECIDEGREES_TO_DEGREES(osdGetHeading()) - referenceHeading
);
1411 poiSymbol
+= mapHeading
* 2 / 45;
1413 displayWriteChar(osdDisplayPort
, poiX
, poiY
, poiSymbol
);
1415 // Update saved location
1416 *drawn
= OSD_POS(poiX
, poiY
) | OSD_VISIBLE_FLAG
;
1423 // Update global map data for scale and reference
1424 osdMapData
.scale
= scale
;
1425 osdMapData
.referenceSymbol
= referenceSym
;
1428 /* Draws a map with the home in the center and the craft moving around.
1429 * See osdDrawMap() for reference.
1431 static void osdDrawHomeMap(int referenceHeading
, uint8_t referenceSym
, uint16_t *drawn
, uint32_t *usedScale
)
1433 osdDrawMap(referenceHeading
, referenceSym
, SYM_HOME
, GPS_distanceToHome
, GPS_directionToHome
, SYM_ARROW_UP
, drawn
, usedScale
);
1436 /* Draws a map with the aircraft in the center and the home moving around.
1437 * See osdDrawMap() for reference.
1439 static void osdDrawRadar(uint16_t *drawn
, uint32_t *usedScale
)
1441 int16_t reference
= DECIDEGREES_TO_DEGREES(osdGetHeading());
1442 int16_t poiDirection
= osdGetHeadingAngle(GPS_directionToHome
+ 180);
1443 osdDrawMap(reference
, 0, SYM_ARROW_UP
, GPS_distanceToHome
, poiDirection
, SYM_HOME
, drawn
, usedScale
);
1446 static uint16_t crc_accumulate(uint8_t data
, uint16_t crcAccum
)
1449 tmp
= data
^ (uint8_t)(crcAccum
& 0xff);
1451 crcAccum
= (crcAccum
>> 8) ^ (tmp
<< 8) ^ (tmp
<< 3) ^ (tmp
>> 4);
1456 static void osdDisplayTelemetry(void)
1459 uint16_t trk_crc
= 0;
1460 char trk_buffer
[31];
1461 static int16_t trk_elevation
= 127;
1462 static uint16_t trk_bearing
= 0;
1464 if (ARMING_FLAG(ARMED
)) {
1465 if (STATE(GPS_FIX
)){
1466 if (GPS_distanceToHome
> 5) {
1467 trk_bearing
= GPS_directionToHome
;
1468 trk_bearing
+= 360 + 180;
1470 int32_t alt
= CENTIMETERS_TO_METERS(osdGetAltitude());
1471 float at
= atan2(alt
, GPS_distanceToHome
);
1472 trk_elevation
= at
* 57.2957795f
; // 57.2957795 = 1 rad
1473 trk_elevation
+= 37; // because elevation in telemetry should be from -37 to 90
1474 if (trk_elevation
< 0) {
1481 trk_elevation
= 127;
1485 trk_data
= 0; // bit 0 - packet type 0 = bearing/elevation, 1 = 2 byte data packet
1486 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.
1487 trk_data
= trk_data
| (uint32_t)trk_bearing
<< 8; // bits 8-17 - bearing angle to target. 0 = true north. 0 to 360
1488 trk_crc
= crc_accumulate(0xFF & trk_data
, trk_crc
); // CRC First Byte bits 0-7
1489 trk_crc
= crc_accumulate(0xFF & trk_bearing
, trk_crc
); // CRC Second Byte bits 8-15
1490 trk_crc
= crc_accumulate(trk_bearing
>> 8, trk_crc
); // CRC Third Byte bits 16-17
1491 trk_data
= trk_data
| (uint32_t)trk_crc
<< 17; // bits 18-29 CRC & 0x3FFFF
1493 for (uint8_t t_ctr
= 0; t_ctr
< 30; t_ctr
++) { // Prepare screen buffer and write data line.
1494 if (trk_data
& (uint32_t)1 << t_ctr
){
1495 trk_buffer
[29 - t_ctr
] = SYM_TELEMETRY_0
;
1498 trk_buffer
[29 - t_ctr
] = SYM_TELEMETRY_1
;
1502 displayWrite(osdDisplayPort
, 0, 0, trk_buffer
);
1503 if (osdConfig()->telemetry
>1){
1504 displayWrite(osdDisplayPort
, 0, 3, trk_buffer
); // Test display because normal telemetry line is not visible
1509 static void osdFormatPidControllerOutput(char *buff
, const char *label
, const pidController_t
*pidController
, uint8_t scale
, bool showDecimal
) {
1510 strcpy(buff
, label
);
1511 for (uint8_t i
= strlen(label
); i
< 5; ++i
) buff
[i
] = ' ';
1512 uint8_t decimals
= showDecimal
? 1 : 0;
1513 osdFormatCentiNumber(buff
+ 5, pidController
->proportional
* scale
, 0, decimals
, 0, 4, false);
1515 osdFormatCentiNumber(buff
+ 10, pidController
->integrator
* scale
, 0, decimals
, 0, 4, false);
1517 osdFormatCentiNumber(buff
+ 15, pidController
->derivative
* scale
, 0, decimals
, 0, 4, false);
1519 osdFormatCentiNumber(buff
+ 20, pidController
->output_constrained
* scale
, 0, decimals
, 0, 4, false);
1523 static void osdDisplayBatteryVoltage(uint8_t elemPosX
, uint8_t elemPosY
, uint16_t voltage
, uint8_t digits
, uint8_t decimals
)
1526 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
1528 osdFormatBatteryChargeSymbol(buff
);
1530 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1531 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
1533 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1534 digits
= MIN(digits
, 5);
1535 osdFormatCentiNumber(buff
, voltage
, 0, decimals
, 0, digits
, false);
1536 buff
[digits
] = SYM_VOLT
;
1537 buff
[digits
+1] = '\0';
1538 const batteryState_e batteryVoltageState
= checkBatteryVoltageState();
1539 if (batteryVoltageState
== BATTERY_CRITICAL
|| batteryVoltageState
== BATTERY_WARNING
) {
1540 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1542 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 1, elemPosY
, buff
, elemAttr
);
1545 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
)
1547 textAttributes_t elemAttr
;
1550 const pid8_t
*pid
= &pidBank()->pid
[pidIndex
];
1551 pidType_e pidType
= pidIndexGetType(pidIndex
);
1553 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, str
);
1555 if (pidType
== PID_TYPE_NONE
) {
1556 // PID is not used in this configuration. Draw dashes.
1557 // XXX: Keep this in sync with the %3d format and spacing used below
1558 displayWrite(osdDisplayPort
, elemPosX
+ 6, elemPosY
, "- - - -");
1562 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1563 tfp_sprintf(buff
, "%3d", pid
->P
);
1564 if ((isAdjustmentFunctionSelected(adjFuncP
)) || (((adjFuncP
== ADJUSTMENT_ROLL_P
) || (adjFuncP
== ADJUSTMENT_PITCH_P
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P
))))
1565 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1566 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
1568 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1569 tfp_sprintf(buff
, "%3d", pid
->I
);
1570 if ((isAdjustmentFunctionSelected(adjFuncI
)) || (((adjFuncI
== ADJUSTMENT_ROLL_I
) || (adjFuncI
== ADJUSTMENT_PITCH_I
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I
))))
1571 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1572 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 8, elemPosY
, buff
, elemAttr
);
1574 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1575 tfp_sprintf(buff
, "%3d", pid
->D
);
1576 if ((isAdjustmentFunctionSelected(adjFuncD
)) || (((adjFuncD
== ADJUSTMENT_ROLL_D
) || (adjFuncD
== ADJUSTMENT_PITCH_D
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D
))))
1577 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1578 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 12, elemPosY
, buff
, elemAttr
);
1580 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1581 tfp_sprintf(buff
, "%3d", pid
->FF
);
1582 if ((isAdjustmentFunctionSelected(adjFuncFF
)) || (((adjFuncFF
== ADJUSTMENT_ROLL_FF
) || (adjFuncFF
== ADJUSTMENT_PITCH_FF
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_FF
))))
1583 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1584 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 16, elemPosY
, buff
, elemAttr
);
1587 static void osdDisplayNavPIDValues(uint8_t elemPosX
, uint8_t elemPosY
, const char *str
, pidIndex_e pidIndex
, adjustmentFunction_e adjFuncP
, adjustmentFunction_e adjFuncI
, adjustmentFunction_e adjFuncD
)
1589 textAttributes_t elemAttr
;
1592 const pid8_t
*pid
= &pidBank()->pid
[pidIndex
];
1593 pidType_e pidType
= pidIndexGetType(pidIndex
);
1595 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, str
);
1597 if (pidType
== PID_TYPE_NONE
) {
1598 // PID is not used in this configuration. Draw dashes.
1599 // XXX: Keep this in sync with the %3d format and spacing used below
1600 displayWrite(osdDisplayPort
, elemPosX
+ 6, elemPosY
, "- - -");
1604 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1605 tfp_sprintf(buff
, "%3d", pid
->P
);
1606 if ((isAdjustmentFunctionSelected(adjFuncP
)) || (((adjFuncP
== ADJUSTMENT_ROLL_P
) || (adjFuncP
== ADJUSTMENT_PITCH_P
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_P
))))
1607 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1608 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
1610 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1611 tfp_sprintf(buff
, "%3d", pid
->I
);
1612 if ((isAdjustmentFunctionSelected(adjFuncI
)) || (((adjFuncI
== ADJUSTMENT_ROLL_I
) || (adjFuncI
== ADJUSTMENT_PITCH_I
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_I
))))
1613 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1614 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 8, elemPosY
, buff
, elemAttr
);
1616 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1617 tfp_sprintf(buff
, "%3d", pidType
== PID_TYPE_PIFF
? pid
->FF
: pid
->D
);
1618 if ((isAdjustmentFunctionSelected(adjFuncD
)) || (((adjFuncD
== ADJUSTMENT_ROLL_D
) || (adjFuncD
== ADJUSTMENT_PITCH_D
)) && (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_D
))))
1619 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1620 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 12, elemPosY
, buff
, elemAttr
);
1623 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
) {
1625 textAttributes_t elemAttr
;
1626 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, str
);
1628 elemAttr
= TEXT_ATTRIBUTES_NONE
;
1629 osdFormatCentiNumber(buff
, value
* 100, 0, maxDecimals
, 0, MIN(valueLength
, 8), false);
1630 if (isAdjustmentFunctionSelected(adjFunc
))
1631 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1632 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ strlen(str
) + 1 + valueOffset
, elemPosY
, buff
, elemAttr
);
1635 int8_t getGeoWaypointNumber(int8_t waypointIndex
)
1637 static int8_t lastWaypointIndex
= 1;
1638 static int8_t geoWaypointIndex
;
1640 if (waypointIndex
!= lastWaypointIndex
) {
1641 lastWaypointIndex
= geoWaypointIndex
= waypointIndex
;
1642 for (uint8_t i
= posControl
.startWpIndex
; i
<= waypointIndex
; i
++) {
1643 if (posControl
.waypointList
[i
].action
== NAV_WP_ACTION_SET_POI
||
1644 posControl
.waypointList
[i
].action
== NAV_WP_ACTION_SET_HEAD
||
1645 posControl
.waypointList
[i
].action
== NAV_WP_ACTION_JUMP
) {
1646 geoWaypointIndex
-= 1;
1651 return geoWaypointIndex
- posControl
.startWpIndex
+ 1;
1654 void osdDisplaySwitchIndicator(const char *swName
, int rcValue
, char *buff
) {
1657 if (osdConfig()->osd_switch_indicators_align_left
) {
1658 for (ptr
= 0; ptr
< constrain(strlen(swName
), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH
); ptr
++) {
1659 buff
[ptr
] = swName
[ptr
];
1662 if ( rcValue
< 1333) {
1663 buff
[ptr
++] = SYM_SWITCH_INDICATOR_LOW
;
1664 } else if ( rcValue
> 1666) {
1665 buff
[ptr
++] = SYM_SWITCH_INDICATOR_HIGH
;
1667 buff
[ptr
++] = SYM_SWITCH_INDICATOR_MID
;
1670 if ( rcValue
< 1333) {
1671 buff
[ptr
++] = SYM_SWITCH_INDICATOR_LOW
;
1672 } else if ( rcValue
> 1666) {
1673 buff
[ptr
++] = SYM_SWITCH_INDICATOR_HIGH
;
1675 buff
[ptr
++] = SYM_SWITCH_INDICATOR_MID
;
1678 for (ptr
= 1; ptr
< constrain(strlen(swName
), 0, OSD_SWITCH_INDICATOR_NAME_LENGTH
) + 1; ptr
++) {
1679 buff
[ptr
] = swName
[ptr
-1];
1688 static bool osdDrawSingleElement(uint8_t item
)
1690 uint16_t pos
= osdLayoutsConfig()->item_pos
[currentLayout
][item
];
1691 if (!OSD_VISIBLE(pos
)) {
1694 uint8_t elemPosX
= OSD_X(pos
);
1695 uint8_t elemPosY
= OSD_Y(pos
);
1696 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
1697 char buff
[32] = {0};
1700 case OSD_CUSTOM_ELEMENT_1
:
1702 customElementDrawElement(buff
, 0);
1705 case OSD_CUSTOM_ELEMENT_2
:
1707 customElementDrawElement(buff
, 1);
1710 case OSD_CUSTOM_ELEMENT_3
:
1712 customElementDrawElement(buff
, 2);
1715 case OSD_RSSI_VALUE
:
1717 uint16_t osdRssi
= osdConvertRSSI();
1719 tfp_sprintf(buff
+ 1, "%2d", osdRssi
);
1720 if (osdRssi
< osdConfig()->rssi_alarm
) {
1721 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1726 case OSD_MAIN_BATT_VOLTAGE
: {
1727 uint8_t base_digits
= 2U;
1728 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1729 if(isBfCompatibleVideoSystem(osdConfig())) {
1730 base_digits
= 3U; // Add extra digit to account for decimal point taking an extra character space
1733 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatteryRawVoltage(), base_digits
+ osdConfig()->main_voltage_decimals
, osdConfig()->main_voltage_decimals
);
1737 case OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
: {
1738 uint8_t base_digits
= 2U;
1739 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1740 if(isBfCompatibleVideoSystem(osdConfig())) {
1741 base_digits
= 3U; // Add extra digit to account for decimal point taking an extra character space
1744 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatterySagCompensatedVoltage(), base_digits
+ osdConfig()->main_voltage_decimals
, osdConfig()->main_voltage_decimals
);
1748 case OSD_CURRENT_DRAW
: {
1749 osdFormatCentiNumber(buff
, getAmperage(), 0, 2, 0, 3, false);
1753 uint8_t current_alarm
= osdConfig()->current_alarm
;
1754 if ((current_alarm
> 0) && ((getAmperage() / 100.0f
) > current_alarm
)) {
1755 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1760 case OSD_MAH_DRAWN
: {
1761 uint8_t mah_digits
= osdConfig()->mAh_precision
; // Initialize to config value
1763 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1764 if (isBfCompatibleVideoSystem(osdConfig())) {
1765 //BFcompat is unable to work with scaled values and it only has mAh symbol to work with
1766 tfp_sprintf(buff
, "%5d", (int)getMAhDrawn()); // Use 5 digits to allow packs below 100Ah
1772 if (osdFormatCentiNumber(buff
, getMAhDrawn() * 100, 1000, 0, (mah_digits
- 2), mah_digits
, false)) {
1774 buff
[mah_digits
] = SYM_AH
;
1777 buff
[mah_digits
] = SYM_MAH
;
1779 buff
[mah_digits
+ 1] = '\0';
1782 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1787 osdFormatCentiNumber(buff
, getMWhDrawn() / 10, 0, 2, 0, 3, false);
1788 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1793 case OSD_BATTERY_REMAINING_CAPACITY
:
1795 bool unitsDrawn
= false;
1797 if (currentBatteryProfile
->capacity
.value
== 0)
1798 tfp_sprintf(buff
, " NA");
1799 else if (!batteryWasFullWhenPluggedIn())
1800 tfp_sprintf(buff
, " NF");
1801 else if (currentBatteryProfile
->capacity
.unit
== BAT_CAPACITY_UNIT_MAH
) {
1802 uint8_t mah_digits
= osdConfig()->mAh_precision
; // Initialize to config value
1804 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
1805 if (isBfCompatibleVideoSystem(osdConfig())) {
1806 //BFcompat is unable to work with scaled values and it only has mAh symbol to work with
1807 tfp_sprintf(buff
, "%5d", (int)getBatteryRemainingCapacity()); // Use 5 digits to allow packs below 100Ah
1814 if (osdFormatCentiNumber(buff
, getBatteryRemainingCapacity() * 100, 1000, 0, (mah_digits
- 2), mah_digits
, false)) {
1816 buff
[mah_digits
] = SYM_AH
;
1819 buff
[mah_digits
] = SYM_MAH
;
1821 buff
[mah_digits
+ 1] = '\0';
1824 } else // currentBatteryProfile->capacity.unit == BAT_CAPACITY_UNIT_MWH
1825 osdFormatCentiNumber(buff
+ 1, getBatteryRemainingCapacity() / 10, 0, 2, 0, 3, false);
1828 buff
[4] = currentBatteryProfile
->capacity
.unit
== BAT_CAPACITY_UNIT_MAH
? SYM_MAH
: SYM_WH
;
1832 if (batteryUsesCapacityThresholds()) {
1833 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1838 case OSD_BATTERY_REMAINING_PERCENT
:
1839 osdFormatBatteryChargeSymbol(buff
);
1840 tfp_sprintf(buff
+ 1, "%3d%%", calculateBatteryPercentage());
1841 osdUpdateBatteryCapacityOrVoltageTextAttributes(&elemAttr
);
1844 case OSD_POWER_SUPPLY_IMPEDANCE
:
1845 if (isPowerSupplyImpedanceValid())
1846 tfp_sprintf(buff
, "%3d", getPowerSupplyImpedance());
1848 strcpy(buff
, "---");
1849 buff
[3] = SYM_MILLIOHM
;
1855 buff
[0] = SYM_SAT_L
;
1856 buff
[1] = SYM_SAT_R
;
1857 tfp_sprintf(buff
+ 2, "%2d", gpsSol
.numSat
);
1858 if (!STATE(GPS_FIX
)) {
1859 hardwareSensorStatus_e sensorStatus
= getHwGPSStatus();
1860 if (sensorStatus
== HW_SENSOR_UNAVAILABLE
|| sensorStatus
== HW_SENSOR_UNHEALTHY
) {
1861 buff
[2] = SYM_ALERT
;
1864 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1869 osdFormatVelocityStr(buff
, gpsSol
.groundSpeed
, false, false);
1872 case OSD_GPS_MAX_SPEED
:
1873 osdFormatVelocityStr(buff
, stats
.max_speed
, false, true);
1877 osdFormatVelocityStr(buff
, osdGet3DSpeed(), true, false);
1880 case OSD_3D_MAX_SPEED
:
1881 osdFormatVelocityStr(buff
, stats
.max_3D_speed
, true, true);
1884 case OSD_GLIDESLOPE
:
1886 float horizontalSpeed
= gpsSol
.groundSpeed
;
1887 float sinkRate
= -getEstimatedActualVelocity(Z
);
1888 static pt1Filter_t gsFilterState
;
1889 const timeMs_t currentTimeMs
= millis();
1890 static timeMs_t gsUpdatedTimeMs
;
1891 float glideSlope
= horizontalSpeed
/ sinkRate
;
1892 glideSlope
= pt1FilterApply4(&gsFilterState
, isnormal(glideSlope
) ? glideSlope
: 200, 0.5, MS2S(currentTimeMs
- gsUpdatedTimeMs
));
1893 gsUpdatedTimeMs
= currentTimeMs
;
1895 buff
[0] = SYM_GLIDESLOPE
;
1896 if (glideSlope
> 0.0f
&& glideSlope
< 100.0f
) {
1897 osdFormatCentiNumber(buff
+ 1, glideSlope
* 100.0f
, 0, 2, 0, 3, false);
1899 buff
[1] = buff
[2] = buff
[3] = '-';
1906 osdFormatCoordinate(buff
, SYM_LAT
, gpsSol
.llh
.lat
);
1910 osdFormatCoordinate(buff
, SYM_LON
, gpsSol
.llh
.lon
);
1915 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
) && isImuHeadingValid()) {
1916 if (GPS_distanceToHome
< (navConfig()->general
.min_rth_distance
/ 100) ) {
1917 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_HOME_NEAR
);
1921 int16_t panHomeDirOffset
= 0;
1922 if (!(osdConfig()->pan_servo_pwm2centideg
== 0)){
1923 panHomeDirOffset
= osdGetPanServoOffset();
1925 int16_t flightDirection
= STATE(AIRPLANE
) ? CENTIDEGREES_TO_DEGREES(posControl
.actualState
.cog
) : DECIDEGREES_TO_DEGREES(osdGetHeading());
1926 int homeDirection
= GPS_directionToHome
- flightDirection
+ panHomeDirOffset
;
1927 osdDrawDirArrow(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), homeDirection
);
1930 // No home or no fix or unknown heading, blink.
1931 // If we're unarmed, show the arrow pointing up so users can see the arrow
1932 // while configuring the OSD. If we're armed, show a '-' indicating that
1933 // we don't know the direction to home.
1934 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1935 displayWriteCharWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, ARMING_FLAG(ARMED
) ? '-' : SYM_ARROW_UP
, elemAttr
);
1940 case OSD_HOME_HEADING_ERROR
:
1943 buff
[1] = SYM_HEADING
;
1945 if (isImuHeadingValid() && navigationPositionEstimateIsHealthy()) {
1946 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())))));
1947 tfp_sprintf(buff
+ 2, "%4d", h
);
1949 strcpy(buff
+ 2, "----");
1952 buff
[6] = SYM_DEGREES
;
1960 uint32_t distance_to_home_cm
= GPS_distanceToHome
* 100;
1961 osdFormatDistanceSymbol(&buff
[1], distance_to_home_cm
, 0);
1963 uint16_t dist_alarm
= osdConfig()->dist_alarm
;
1964 if (dist_alarm
> 0 && GPS_distanceToHome
> dist_alarm
) {
1965 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
1971 buff
[0] = SYM_TOTAL
;
1972 osdFormatDistanceSymbol(buff
+ 1, getTotalTravelDistance(), 0);
1977 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_ODOMETER
);
1978 float_t odometerDist
= CENTIMETERS_TO_METERS(getTotalTravelDistance());
1980 odometerDist
+= statsConfig()->stats_total_dist
;
1983 switch (osdConfig()->units
) {
1986 case OSD_UNIT_IMPERIAL
:
1987 osdFormatCentiNumber(buff
, METERS_TO_MILES(odometerDist
) * 100, 1, 1, 1, 6, true);
1992 osdFormatCentiNumber(buff
, METERS_TO_NAUTICALMILES(odometerDist
) * 100, 1, 1, 1, 6, true);
1995 case OSD_UNIT_METRIC_MPH
:
1997 case OSD_UNIT_METRIC
:
1998 osdFormatCentiNumber(buff
, METERS_TO_KILOMETERS(odometerDist
) * 100, 1, 1, 1, 6, true);
2007 case OSD_GROUND_COURSE
:
2009 buff
[0] = SYM_GROUND_COURSE
;
2010 if (osdIsHeadingValid()) {
2011 tfp_sprintf(&buff
[1], "%3d", (int16_t)CENTIDEGREES_TO_DEGREES(posControl
.actualState
.cog
));
2013 buff
[1] = buff
[2] = buff
[3] = '-';
2015 buff
[4] = SYM_DEGREES
;
2020 case OSD_COURSE_HOLD_ERROR
:
2022 if (ARMING_FLAG(ARMED
) && !FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) {
2023 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
2027 buff
[0] = SYM_HEADING
;
2029 if ((!ARMING_FLAG(ARMED
)) || (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
) && isAdjustingPosition())) {
2030 buff
[1] = buff
[2] = buff
[3] = '-';
2031 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) {
2032 int16_t herr
= lrintf(CENTIDEGREES_TO_DEGREES((float)navigationGetHeadingError()));
2034 strcpy(buff
+ 1, ">99");
2036 tfp_sprintf(buff
+ 1, "%3d", herr
);
2039 buff
[4] = SYM_DEGREES
;
2044 case OSD_COURSE_HOLD_ADJUSTMENT
:
2046 int16_t heading_adjust
= lrintf(CENTIDEGREES_TO_DEGREES((float)getCruiseHeadingAdjustment()));
2048 if (ARMING_FLAG(ARMED
) && ((!FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) || !(isAdjustingPosition() || isAdjustingHeading() || (heading_adjust
!= 0)))) {
2049 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
2053 buff
[0] = SYM_HEADING
;
2055 if (!ARMING_FLAG(ARMED
)) {
2056 buff
[1] = buff
[2] = buff
[3] = buff
[4] = '-';
2057 } else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) {
2058 tfp_sprintf(buff
+ 1, "%4d", heading_adjust
);
2061 buff
[5] = SYM_DEGREES
;
2066 case OSD_CROSS_TRACK_ERROR
:
2068 if (isWaypointNavTrackingActive()) {
2069 buff
[0] = SYM_CROSS_TRACK_ERROR
;
2070 osdFormatDistanceSymbol(buff
+ 1, navigationGetCrossTrackError(), 0);
2072 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
2080 buff
[0] = SYM_HDP_L
;
2081 buff
[1] = SYM_HDP_R
;
2082 int32_t centiHDOP
= 100 * gpsSol
.hdop
/ HDOP_SCALE
;
2083 uint8_t digits
= 2U;
2084 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
2085 if (isBfCompatibleVideoSystem(osdConfig())) {
2089 osdFormatCentiNumber(&buff
[2], centiHDOP
, 0, 1, 0, digits
, false);
2095 static uint16_t drawn
= 0;
2096 static uint32_t scale
= 0;
2097 osdDrawHomeMap(0, 'N', &drawn
, &scale
);
2100 case OSD_MAP_TAKEOFF
:
2102 static uint16_t drawn
= 0;
2103 static uint32_t scale
= 0;
2104 osdDrawHomeMap(CENTIDEGREES_TO_DEGREES(navigationGetHomeHeading()), 'T', &drawn
, &scale
);
2109 static uint16_t drawn
= 0;
2110 static uint32_t scale
= 0;
2111 osdDrawRadar(&drawn
, &scale
);
2118 int32_t alt
= osdGetAltitude();
2119 osdFormatAltitudeSymbol(buff
, alt
);
2121 uint16_t alt_alarm
= osdConfig()->alt_alarm
;
2122 uint16_t neg_alt_alarm
= osdConfig()->neg_alt_alarm
;
2123 if ((alt_alarm
> 0 && CENTIMETERS_TO_METERS(alt
) > alt_alarm
) ||
2124 (neg_alt_alarm
> 0 && alt
< 0 && -CENTIMETERS_TO_METERS(alt
) > neg_alt_alarm
)) {
2126 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2128 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
2130 if (STATE(MULTIROTOR
) && posControl
.flags
.isAdjustingAltitude
) {
2131 /* Indicate MR altitude adjustment active with constant symbol at first blank position.
2132 * Alternate symbol on/off with 600ms cycle if first position not blank (to maintain visibility of -ve sign) */
2134 for (blankPos
= 2; blankPos
>= 0; blankPos
--) {
2135 if (buff
[blankPos
] == SYM_BLANK
) {
2139 if (blankPos
>= 0 || OSD_ALTERNATING_CHOICES(600, 2) == 0) {
2140 blankPos
= blankPos
< 0 ? 0 : blankPos
;
2141 displayWriteChar(osdDisplayPort
, elemPosX
+ blankPos
, elemPosY
, SYM_TERRAIN_FOLLOWING
);
2147 case OSD_ALTITUDE_MSL
:
2149 int32_t alt
= osdGetAltitudeMsl();
2150 osdFormatAltitudeSymbol(buff
, alt
);
2154 #ifdef USE_RANGEFINDER
2155 case OSD_RANGEFINDER
:
2157 int32_t range
= rangefinderGetLatestRawAltitude();
2163 osdFormatDistanceSymbol(buff
, range
, 1);
2171 osdFormatOnTime(buff
);
2177 osdFormatFlyTime(buff
, &elemAttr
);
2181 case OSD_ONTIME_FLYTIME
:
2183 if (ARMING_FLAG(ARMED
)) {
2184 osdFormatFlyTime(buff
, &elemAttr
);
2186 osdFormatOnTime(buff
);
2191 case OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH
:
2193 /*static int32_t updatedTimeSeconds = 0;*/
2194 static int32_t timeSeconds
= -1;
2195 #if defined(USE_ADC) && defined(USE_GPS)
2196 static timeUs_t updatedTimestamp
= 0;
2197 timeUs_t currentTimeUs
= micros();
2198 if (cmpTimeUs(currentTimeUs
, updatedTimestamp
) >= MS2US(1000)) {
2199 #ifdef USE_WIND_ESTIMATOR
2200 timeSeconds
= calculateRemainingFlightTimeBeforeRTH(osdConfig()->estimations_wind_compensation
);
2202 timeSeconds
= calculateRemainingFlightTimeBeforeRTH(false);
2204 updatedTimestamp
= currentTimeUs
;
2207 if ((!ARMING_FLAG(ARMED
)) || (timeSeconds
== -1)) {
2208 buff
[0] = SYM_FLIGHT_MINS_REMAINING
;
2209 strcpy(buff
+ 1, "--:--");
2210 #if defined(USE_ADC) && defined(USE_GPS)
2211 updatedTimestamp
= 0;
2213 } else if (timeSeconds
== -2) {
2214 // Wind is too strong to come back with cruise throttle
2215 buff
[0] = SYM_FLIGHT_MINS_REMAINING
;
2216 buff
[1] = buff
[2] = buff
[4] = buff
[5] = SYM_WIND_HORIZONTAL
;
2219 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2221 osdFormatTime(buff
, timeSeconds
, SYM_FLIGHT_MINS_REMAINING
, SYM_FLIGHT_HOURS_REMAINING
);
2222 if (timeSeconds
== 0)
2223 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2228 case OSD_REMAINING_DISTANCE_BEFORE_RTH
:;
2229 static int32_t distanceMeters
= -1;
2230 #if defined(USE_ADC) && defined(USE_GPS)
2231 static timeUs_t updatedTimestamp
= 0;
2232 timeUs_t currentTimeUs
= micros();
2233 if (cmpTimeUs(currentTimeUs
, updatedTimestamp
) >= MS2US(1000)) {
2234 #ifdef USE_WIND_ESTIMATOR
2235 distanceMeters
= calculateRemainingDistanceBeforeRTH(osdConfig()->estimations_wind_compensation
);
2237 distanceMeters
= calculateRemainingDistanceBeforeRTH(false);
2239 updatedTimestamp
= currentTimeUs
;
2242 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_FLIGHT_DIST_REMAINING
);
2244 if ((!ARMING_FLAG(ARMED
)) || (distanceMeters
== -1)) {
2245 buff
[3] = SYM_BLANK
;
2247 strcpy(buff
, "---");
2248 } else if (distanceMeters
== -2) {
2249 // Wind is too strong to come back with cruise throttle
2250 buff
[0] = buff
[1] = buff
[2] = SYM_WIND_HORIZONTAL
;
2251 switch ((osd_unit_e
)osdConfig()->units
){
2254 case OSD_UNIT_IMPERIAL
:
2255 buff
[3] = SYM_DIST_MI
;
2257 case OSD_UNIT_METRIC_MPH
:
2259 case OSD_UNIT_METRIC
:
2260 buff
[3] = SYM_DIST_KM
;
2263 buff
[3] = SYM_DIST_NM
;
2267 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2269 osdFormatDistanceSymbol(buff
, distanceMeters
* 100, 0);
2270 if (distanceMeters
== 0)
2271 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2279 #ifdef USE_FW_AUTOLAND
2280 if (FLIGHT_MODE(NAV_FW_AUTOLAND
))
2284 if (FLIGHT_MODE(FAILSAFE_MODE
))
2286 else if (FLIGHT_MODE(MANUAL_MODE
))
2288 else if (FLIGHT_MODE(TURTLE_MODE
))
2290 else if (FLIGHT_MODE(NAV_RTH_MODE
))
2291 p
= isWaypointMissionRTHActive() ? "WRTH" : "RTH ";
2292 else if (FLIGHT_MODE(NAV_POSHOLD_MODE
) && STATE(AIRPLANE
))
2294 else if (FLIGHT_MODE(NAV_POSHOLD_MODE
))
2296 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
) && FLIGHT_MODE(NAV_ALTHOLD_MODE
))
2298 else if (FLIGHT_MODE(NAV_COURSE_HOLD_MODE
))
2300 else if (FLIGHT_MODE(NAV_WP_MODE
))
2302 else if (FLIGHT_MODE(NAV_ALTHOLD_MODE
) && navigationRequiresAngleMode()) {
2303 // If navigationRequiresAngleMode() returns false when ALTHOLD is active,
2304 // it means it can be combined with ANGLE, HORIZON, ANGLEHOLD, ACRO, etc...
2305 // and its display is handled by OSD_MESSAGES rather than OSD_FLYMODE.
2308 else if (FLIGHT_MODE(ANGLE_MODE
))
2310 else if (FLIGHT_MODE(HORIZON_MODE
))
2312 else if (FLIGHT_MODE(ANGLEHOLD_MODE
))
2315 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, p
);
2319 case OSD_CRAFT_NAME
:
2320 osdFormatCraftName(buff
);
2323 case OSD_PILOT_NAME
:
2324 osdFormatPilotName(buff
);
2327 case OSD_PILOT_LOGO
:
2328 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_PILOT_LOGO_SML_L
);
2329 displayWriteChar(osdDisplayPort
, elemPosX
+1, elemPosY
, SYM_PILOT_LOGO_SML_C
);
2330 displayWriteChar(osdDisplayPort
, elemPosX
+2, elemPosY
, SYM_PILOT_LOGO_SML_R
);
2333 case OSD_THROTTLE_POS
:
2335 osdFormatThrottlePosition(buff
, false, &elemAttr
);
2339 case OSD_VTX_CHANNEL
:
2341 vtxDeviceOsdInfo_t osdInfo
;
2342 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo
);
2344 tfp_sprintf(buff
, "CH:%c%s:", osdInfo
.bandLetter
, osdInfo
.channelName
);
2345 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
2347 tfp_sprintf(buff
, "%c", osdInfo
.powerIndexLetter
);
2348 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL
)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2349 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 6, elemPosY
, buff
, elemAttr
);
2356 vtxDeviceOsdInfo_t osdInfo
;
2357 vtxCommonGetOsdInfo(vtxCommonDevice(), &osdInfo
);
2359 tfp_sprintf(buff
, "%c", SYM_VTX_POWER
);
2360 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
2362 tfp_sprintf(buff
, "%c", osdInfo
.powerIndexLetter
);
2363 if (isAdjustmentFunctionSelected(ADJUSTMENT_VTX_POWER_LEVEL
)) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2364 displayWriteWithAttr(osdDisplayPort
, elemPosX
+1, elemPosY
, buff
, elemAttr
);
2368 #if defined(USE_SERIALRX_CRSF)
2369 case OSD_CRSF_RSSI_DBM
:
2371 int16_t rssi
= rxLinkStatistics
.uplinkRSSI
;
2372 buff
[0] = (rxLinkStatistics
.activeAntenna
== 0) ? SYM_RSSI
: SYM_2RSS
; // Separate symbols for each antenna
2374 tfp_sprintf(buff
+ 1, "%4d%c", rssi
, SYM_DBM
);
2376 tfp_sprintf(buff
+ 1, "%3d%c%c", rssi
, SYM_DBM
, ' ');
2378 if (!failsafeIsReceivingRxData()){
2379 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2380 } else if (osdConfig()->rssi_dbm_alarm
&& rssi
< osdConfig()->rssi_dbm_alarm
) {
2381 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2388 int16_t statsLQ
= rxLinkStatistics
.uplinkLQ
;
2389 int16_t scaledLQ
= scaleRange(constrain(statsLQ
, 0, 100), 0, 100, 170, 300);
2390 switch (osdConfig()->crsf_lq_format
) {
2391 case OSD_CRSF_LQ_TYPE1
:
2392 if (!failsafeIsReceivingRxData()) {
2393 tfp_sprintf(buff
+1, "%3d", 0);
2395 tfp_sprintf(buff
+1, "%3d", rxLinkStatistics
.uplinkLQ
);
2398 case OSD_CRSF_LQ_TYPE2
:
2399 if (!failsafeIsReceivingRxData()) {
2400 tfp_sprintf(buff
+1, "%s:%3d", " ", 0);
2402 tfp_sprintf(buff
+1, "%d:%3d", rxLinkStatistics
.rfMode
, rxLinkStatistics
.uplinkLQ
);
2405 case OSD_CRSF_LQ_TYPE3
:
2406 if (!failsafeIsReceivingRxData()) {
2407 tfp_sprintf(buff
+1, "%3d", 0);
2409 tfp_sprintf(buff
+1, "%3d", rxLinkStatistics
.rfMode
>= 2 ? scaledLQ
: rxLinkStatistics
.uplinkLQ
);
2413 if (!failsafeIsReceivingRxData()) {
2414 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2415 } else if (rxLinkStatistics
.uplinkLQ
< osdConfig()->link_quality_alarm
) {
2416 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2421 case OSD_CRSF_SNR_DB
:
2423 static pt1Filter_t snrFilterState
;
2424 static timeMs_t snrUpdated
= 0;
2425 int8_t snrFiltered
= pt1FilterApply4(&snrFilterState
, rxLinkStatistics
.uplinkSNR
, 0.5f
, MS2S(millis() - snrUpdated
));
2426 snrUpdated
= millis();
2428 const char* showsnr
= "-20";
2429 const char* hidesnr
= " ";
2430 if (snrFiltered
> osdConfig()->snr_alarm
) {
2433 tfp_sprintf(buff
+ 1, "%s%c", showsnr
, SYM_DB
);
2435 buff
[0] = SYM_BLANK
;
2436 tfp_sprintf(buff
+ 1, "%s%c", hidesnr
, SYM_BLANK
);
2438 } else if (snrFiltered
<= osdConfig()->snr_alarm
) {
2440 if (snrFiltered
<= -10) {
2441 tfp_sprintf(buff
+ 1, "%3d%c", snrFiltered
, SYM_DB
);
2443 tfp_sprintf(buff
+ 1, "%2d%c%c", snrFiltered
, SYM_DB
, ' ');
2449 case OSD_CRSF_TX_POWER
:
2451 if (!failsafeIsReceivingRxData())
2452 tfp_sprintf(buff
, "%s%c", " ", SYM_BLANK
);
2454 tfp_sprintf(buff
, "%4d%c", rxLinkStatistics
.uplinkTXPower
, SYM_MW
);
2459 case OSD_CROSSHAIRS
: // Hud is a sub-element of the crosshair
2461 osdCrosshairPosition(&elemPosX
, &elemPosY
);
2462 osdHudDrawCrosshair(osdGetDisplayPortCanvas(), elemPosX
, elemPosY
);
2464 if (osdConfig()->hud_homing
&& STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
) && isImuHeadingValid()) {
2465 osdHudDrawHoming(elemPosX
, elemPosY
);
2468 if (STATE(GPS_FIX
) && isImuHeadingValid()) {
2470 if (osdConfig()->hud_homepoint
|| osdConfig()->hud_radar_disp
> 0 || osdConfig()->hud_wp_disp
> 0) {
2474 // -------- POI : Home point
2476 if (osdConfig()->hud_homepoint
) { // Display the home point (H)
2477 osdHudDrawPoi(GPS_distanceToHome
, GPS_directionToHome
, -osdGetAltitude() / 100, 0, SYM_HOME
, 0 , 0);
2480 // -------- POI : Nearby aircrafts from ESP32 radar
2482 if (osdConfig()->hud_radar_disp
> 0) { // Display the POI from the radar
2483 for (uint8_t i
= 0; i
< osdConfig()->hud_radar_disp
; i
++) {
2484 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
2486 geoConvertGeodeticToLocal(&poi
, &posControl
.gpsOrigin
, &radar_pois
[i
].gps
, GEO_ALT_RELATIVE
);
2487 radar_pois
[i
].distance
= calculateDistanceToDestination(&poi
) / 100; // In meters
2489 if (radar_pois
[i
].distance
>= osdConfig()->hud_radar_range_min
&& radar_pois
[i
].distance
<= osdConfig()->hud_radar_range_max
) {
2490 radar_pois
[i
].direction
= calculateBearingToDestination(&poi
) / 100; // In °
2491 radar_pois
[i
].altitude
= (radar_pois
[i
].gps
.alt
- osdGetAltitudeMsl()) / 100;
2492 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
);
2498 // -------- POI : Next waypoints from navigation
2500 if (osdConfig()->hud_wp_disp
> 0 && posControl
.waypointListValid
&& posControl
.waypointCount
> 0) { // Display the next waypoints
2504 for (int i
= osdConfig()->hud_wp_disp
- 1; i
>= 0 ; i
--) { // Display in reverse order so the next WP is always written on top
2505 j
= posControl
.activeWaypointIndex
+ i
;
2506 if (j
> posControl
.startWpIndex
+ posControl
.waypointCount
- 1) { // limit to max WP index for mission
2509 if (posControl
.waypointList
[j
].lat
!= 0 && posControl
.waypointList
[j
].lon
!= 0) {
2510 wp2
.lat
= posControl
.waypointList
[j
].lat
;
2511 wp2
.lon
= posControl
.waypointList
[j
].lon
;
2512 wp2
.alt
= posControl
.waypointList
[j
].alt
;
2514 geoConvertGeodeticToLocal(&poi
, &posControl
.gpsOrigin
, &wp2
, waypointMissionAltConvMode(posControl
.waypointList
[j
].p3
));
2515 int32_t altConvModeAltitude
= waypointMissionAltConvMode(posControl
.waypointList
[j
].p3
) == GEO_ALT_ABSOLUTE
? osdGetAltitudeMsl() : osdGetAltitude();
2516 j
= getGeoWaypointNumber(j
);
2517 while (j
> 9) j
-= 10; // Only the last digit displayed if WP>=10, no room for more (48 = ascii 0)
2518 osdHudDrawPoi(calculateDistanceToDestination(&poi
) / 100, osdGetHeadingAngle(calculateBearingToDestination(&poi
) / 100), (posControl
.waypointList
[j
].alt
- altConvModeAltitude
)/ 100, 2, SYM_WAYPOINT
, 48 + j
, i
);
2527 case OSD_ATTITUDE_ROLL
:
2528 buff
[0] = SYM_ROLL_LEVEL
;
2529 if (ABS(attitude
.values
.roll
) >= 1)
2530 buff
[0] += (attitude
.values
.roll
< 0 ? -1 : 1);
2531 osdFormatCentiNumber(buff
+ 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude
.values
.roll
)), 0, 1, 0, 3, false);
2534 case OSD_ATTITUDE_PITCH
:
2535 if (ABS(attitude
.values
.pitch
) < 1)
2537 else if (attitude
.values
.pitch
> 0)
2538 buff
[0] = SYM_PITCH_DOWN
;
2539 else if (attitude
.values
.pitch
< 0)
2540 buff
[0] = SYM_PITCH_UP
;
2541 osdFormatCentiNumber(buff
+ 1, DECIDEGREES_TO_CENTIDEGREES(ABS(attitude
.values
.pitch
)), 0, 1, 0, 3, false);
2544 case OSD_ARTIFICIAL_HORIZON
:
2546 float rollAngle
= DECIDEGREES_TO_RADIANS(attitude
.values
.roll
);
2547 float pitchAngle
= DECIDEGREES_TO_RADIANS(attitude
.values
.pitch
);
2549 pitchAngle
-= osdConfig()->ahi_camera_uptilt_comp
? DEGREES_TO_RADIANS(osdConfig()->camera_uptilt
) : 0;
2550 pitchAngle
+= DEGREES_TO_RADIANS(getFixedWingLevelTrim());
2551 if (osdConfig()->ahi_reverse_roll
) {
2552 rollAngle
= -rollAngle
;
2554 osdDrawArtificialHorizon(osdDisplayPort
, osdGetDisplayPortCanvas(),
2555 OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), rollAngle
, pitchAngle
);
2556 osdDrawSingleElement(OSD_HORIZON_SIDEBARS
);
2557 osdDrawSingleElement(OSD_CROSSHAIRS
);
2562 case OSD_HORIZON_SIDEBARS
:
2564 osdDrawSidebars(osdDisplayPort
, osdGetDisplayPortCanvas());
2568 #if defined(USE_BARO) || defined(USE_GPS)
2571 float zvel
= getEstimatedActualVelocity(Z
);
2572 osdDrawVario(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), zvel
);
2578 int16_t value
= getEstimatedActualVelocity(Z
);
2580 switch ((osd_unit_e
)osdConfig()->units
) {
2583 case OSD_UNIT_IMPERIAL
:
2584 // Convert to centifeet/s
2585 value
= CENTIMETERS_TO_CENTIFEET(value
);
2589 // Convert to centi-100feet/min
2590 value
= CENTIMETERS_TO_FEET(value
* 60);
2594 case OSD_UNIT_METRIC_MPH
:
2596 case OSD_UNIT_METRIC
:
2602 osdFormatCentiNumber(buff
, value
, 0, 1, 0, 3, false);
2607 case OSD_CLIMB_EFFICIENCY
:
2609 // amperage is in centi amps (10mA), vertical speed is in cms/s. We want
2610 // Ah/dist only to show when vertical speed > 1m/s.
2611 static pt1Filter_t veFilterState
;
2612 static timeUs_t vEfficiencyUpdated
= 0;
2614 timeUs_t currentTimeUs
= micros();
2615 timeDelta_t vEfficiencyTimeDelta
= cmpTimeUs(currentTimeUs
, vEfficiencyUpdated
);
2616 if (getEstimatedActualVelocity(Z
) > 0) {
2617 if (vEfficiencyTimeDelta
>= EFFICIENCY_UPDATE_INTERVAL
) {
2618 // Centiamps (kept for osdFormatCentiNumber) / m/s - Will appear as A / m/s in OSD
2619 value
= pt1FilterApply4(&veFilterState
, (float)getAmperage() / (getEstimatedActualVelocity(Z
) / 100.0f
), 1, US2S(vEfficiencyTimeDelta
));
2621 vEfficiencyUpdated
= currentTimeUs
;
2623 value
= veFilterState
.state
;
2626 bool efficiencyValid
= (value
> 0) && (getEstimatedActualVelocity(Z
) > 100);
2627 switch (osdConfig()->units
) {
2632 case OSD_UNIT_IMPERIAL
:
2634 if (efficiencyValid
) {
2635 osdFormatCentiNumber(buff
, (value
* METERS_PER_FOOT
), 1, 2, 2, 3, false);
2636 tfp_sprintf(buff
, "%s%c%c", buff
, SYM_AH_V_FT_0
, SYM_AH_V_FT_1
);
2638 buff
[0] = buff
[1] = buff
[2] = '-';
2639 buff
[3] = SYM_AH_V_FT_0
;
2640 buff
[4] = SYM_AH_V_FT_1
;
2644 case OSD_UNIT_METRIC_MPH
:
2646 case OSD_UNIT_METRIC
:
2648 if (efficiencyValid
) {
2649 osdFormatCentiNumber(buff
, value
, 1, 2, 2, 3, false);
2650 tfp_sprintf(buff
, "%s%c%c", buff
, SYM_AH_V_M_0
, SYM_AH_V_M_1
);
2652 buff
[0] = buff
[1] = buff
[2] = '-';
2653 buff
[3] = SYM_AH_V_M_0
;
2654 buff
[4] = SYM_AH_V_M_1
;
2661 case OSD_GLIDE_TIME_REMAINING
:
2663 uint16_t glideTime
= osdGetRemainingGlideTime();
2664 buff
[0] = SYM_GLIDE_MINS
;
2665 if (glideTime
> 0) {
2666 // Maximum value we can show in minutes is 99 minutes and 59 seconds. It is extremely unlikely that glide
2667 // time will be longer than 99 minutes. If it is, it will show 99:^^
2668 if (glideTime
> (99 * 60) + 59) {
2669 tfp_sprintf(buff
+ 1, "%02d:", (int)(glideTime
/ 60));
2670 buff
[4] = SYM_DIRECTION
;
2671 buff
[5] = SYM_DIRECTION
;
2673 tfp_sprintf(buff
+ 1, "%02d:%02d", (int)(glideTime
/ 60), (int)(glideTime
% 60));
2676 tfp_sprintf(buff
+ 1, "%s", "--:--");
2681 case OSD_GLIDE_RANGE
:
2683 uint16_t glideSeconds
= osdGetRemainingGlideTime();
2684 buff
[0] = SYM_GLIDE_DIST
;
2685 if (glideSeconds
> 0) {
2686 uint32_t glideRangeCM
= glideSeconds
* gpsSol
.groundSpeed
;
2687 osdFormatDistanceSymbol(buff
+ 1, glideRangeCM
, 0);
2689 tfp_sprintf(buff
+ 1, "%s%c", "---", SYM_BLANK
);
2696 case OSD_SWITCH_INDICATOR_0
:
2697 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator0_name
, rxGetChannelValue(osdConfig()->osd_switch_indicator0_channel
- 1), buff
);
2700 case OSD_SWITCH_INDICATOR_1
:
2701 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator1_name
, rxGetChannelValue(osdConfig()->osd_switch_indicator1_channel
- 1), buff
);
2704 case OSD_SWITCH_INDICATOR_2
:
2705 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator2_name
, rxGetChannelValue(osdConfig()->osd_switch_indicator2_channel
- 1), buff
);
2708 case OSD_SWITCH_INDICATOR_3
:
2709 osdDisplaySwitchIndicator(osdConfig()->osd_switch_indicator3_name
, rxGetChannelValue(osdConfig()->osd_switch_indicator3_channel
- 1), buff
);
2712 case OSD_PAN_SERVO_CENTRED
:
2714 int16_t panOffset
= osdGetPanServoOffset();
2715 const timeMs_t panServoTimeNow
= millis();
2716 static timeMs_t panServoTimeOffCentre
= 0;
2718 if (panOffset
< 0) {
2719 if (osdConfig()->pan_servo_offcentre_warning
!= 0 && panOffset
>= -osdConfig()->pan_servo_offcentre_warning
) {
2720 if (panServoTimeOffCentre
== 0) {
2721 panServoTimeOffCentre
= panServoTimeNow
;
2722 } else if (panServoTimeNow
>= (panServoTimeOffCentre
+ 10000 )) {
2723 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2726 panServoTimeOffCentre
= 0;
2729 if (osdConfig()->pan_servo_indicator_show_degrees
) {
2730 tfp_sprintf(buff
, "%3d%c", -panOffset
, SYM_DEGREES
);
2731 displayWriteWithAttr(osdDisplayPort
, elemPosX
+1, elemPosY
, buff
, elemAttr
);
2733 displayWriteCharWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, SYM_SERVO_PAN_IS_OFFSET_R
, elemAttr
);
2734 } else if (panOffset
> 0) {
2735 if (osdConfig()->pan_servo_offcentre_warning
!= 0 && panOffset
<= osdConfig()->pan_servo_offcentre_warning
) {
2736 if (panServoTimeOffCentre
== 0) {
2737 panServoTimeOffCentre
= panServoTimeNow
;
2738 } else if (panServoTimeNow
>= (panServoTimeOffCentre
+ 10000 )) {
2739 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2742 panServoTimeOffCentre
= 0;
2745 if (osdConfig()->pan_servo_indicator_show_degrees
) {
2746 tfp_sprintf(buff
, "%3d%c", panOffset
, SYM_DEGREES
);
2747 displayWriteWithAttr(osdDisplayPort
, elemPosX
+1, elemPosY
, buff
, elemAttr
);
2749 displayWriteCharWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, SYM_SERVO_PAN_IS_OFFSET_L
, elemAttr
);
2751 panServoTimeOffCentre
= 0;
2753 if (osdConfig()->pan_servo_indicator_show_degrees
) {
2754 tfp_sprintf(buff
, "%3d%c", panOffset
, SYM_DEGREES
);
2755 displayWriteWithAttr(osdDisplayPort
, elemPosX
+1, elemPosY
, buff
, elemAttr
);
2757 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_SERVO_PAN_IS_CENTRED
);
2764 case OSD_ACTIVE_PROFILE
:
2765 tfp_sprintf(buff
, "%c%u", SYM_PROFILE
, (getConfigProfile() + 1));
2766 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
2770 osdDisplayFlightPIDValues(elemPosX
, elemPosY
, "ROL", PID_ROLL
, ADJUSTMENT_ROLL_P
, ADJUSTMENT_ROLL_I
, ADJUSTMENT_ROLL_D
, ADJUSTMENT_ROLL_FF
);
2773 case OSD_PITCH_PIDS
:
2774 osdDisplayFlightPIDValues(elemPosX
, elemPosY
, "PIT", PID_PITCH
, ADJUSTMENT_PITCH_P
, ADJUSTMENT_PITCH_I
, ADJUSTMENT_PITCH_D
, ADJUSTMENT_PITCH_FF
);
2778 osdDisplayFlightPIDValues(elemPosX
, elemPosY
, "YAW", PID_YAW
, ADJUSTMENT_YAW_P
, ADJUSTMENT_YAW_I
, ADJUSTMENT_YAW_D
, ADJUSTMENT_YAW_FF
);
2781 case OSD_LEVEL_PIDS
:
2782 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "LEV", PID_LEVEL
, ADJUSTMENT_LEVEL_P
, ADJUSTMENT_LEVEL_I
, ADJUSTMENT_LEVEL_D
);
2785 case OSD_POS_XY_PIDS
:
2786 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "PXY", PID_POS_XY
, ADJUSTMENT_POS_XY_P
, ADJUSTMENT_POS_XY_I
, ADJUSTMENT_POS_XY_D
);
2789 case OSD_POS_Z_PIDS
:
2790 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "PZ", PID_POS_Z
, ADJUSTMENT_POS_Z_P
, ADJUSTMENT_POS_Z_I
, ADJUSTMENT_POS_Z_D
);
2793 case OSD_VEL_XY_PIDS
:
2794 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "VXY", PID_VEL_XY
, ADJUSTMENT_VEL_XY_P
, ADJUSTMENT_VEL_XY_I
, ADJUSTMENT_VEL_XY_D
);
2797 case OSD_VEL_Z_PIDS
:
2798 osdDisplayNavPIDValues(elemPosX
, elemPosY
, "VZ", PID_VEL_Z
, ADJUSTMENT_VEL_Z_P
, ADJUSTMENT_VEL_Z_I
, ADJUSTMENT_VEL_Z_D
);
2802 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "HP", 0, pidBank()->pid
[PID_HEADING
].P
, 3, 0, ADJUSTMENT_HEADING_P
);
2805 case OSD_BOARD_ALIGN_ROLL
:
2806 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "AR", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->rollDeciDegrees
), 4, 1, ADJUSTMENT_ROLL_BOARD_ALIGNMENT
);
2809 case OSD_BOARD_ALIGN_PITCH
:
2810 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "AP", 0, DECIDEGREES_TO_DEGREES((float)boardAlignment()->pitchDeciDegrees
), 4, 1, ADJUSTMENT_PITCH_BOARD_ALIGNMENT
);
2814 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "EXP", 0, currentControlRateProfile
->stabilized
.rcExpo8
, 3, 0, ADJUSTMENT_RC_EXPO
);
2817 case OSD_RC_YAW_EXPO
:
2818 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "YEX", 0, currentControlRateProfile
->stabilized
.rcYawExpo8
, 3, 0, ADJUSTMENT_RC_YAW_EXPO
);
2821 case OSD_THROTTLE_EXPO
:
2822 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "TEX", 0, currentControlRateProfile
->throttle
.rcExpo8
, 3, 0, ADJUSTMENT_THROTTLE_EXPO
);
2825 case OSD_PITCH_RATE
:
2826 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "SPR");
2828 elemAttr
= TEXT_ATTRIBUTES_NONE
;
2829 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->stabilized
.rates
[FD_PITCH
]);
2830 if (isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE
))
2831 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2832 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
2836 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "SRR");
2838 elemAttr
= TEXT_ATTRIBUTES_NONE
;
2839 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->stabilized
.rates
[FD_ROLL
]);
2840 if (isAdjustmentFunctionSelected(ADJUSTMENT_ROLL_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_PITCH_ROLL_RATE
))
2841 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2842 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
2846 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "SYR", 0, currentControlRateProfile
->stabilized
.rates
[FD_YAW
], 3, 0, ADJUSTMENT_YAW_RATE
);
2849 case OSD_MANUAL_RC_EXPO
:
2850 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "MEX", 0, currentControlRateProfile
->manual
.rcExpo8
, 3, 0, ADJUSTMENT_MANUAL_RC_EXPO
);
2853 case OSD_MANUAL_RC_YAW_EXPO
:
2854 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "MYX", 0, currentControlRateProfile
->manual
.rcYawExpo8
, 3, 0, ADJUSTMENT_MANUAL_RC_YAW_EXPO
);
2857 case OSD_MANUAL_PITCH_RATE
:
2858 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "MPR");
2860 elemAttr
= TEXT_ATTRIBUTES_NONE
;
2861 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->manual
.rates
[FD_PITCH
]);
2862 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE
))
2863 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2864 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
2867 case OSD_MANUAL_ROLL_RATE
:
2868 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "MRR");
2870 elemAttr
= TEXT_ATTRIBUTES_NONE
;
2871 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->manual
.rates
[FD_ROLL
]);
2872 if (isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_ROLL_RATE
) || isAdjustmentFunctionSelected(ADJUSTMENT_MANUAL_PITCH_ROLL_RATE
))
2873 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2874 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
, buff
, elemAttr
);
2877 case OSD_MANUAL_YAW_RATE
:
2878 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "MYR", 0, currentControlRateProfile
->stabilized
.rates
[FD_YAW
], 3, 0, ADJUSTMENT_YAW_RATE
);
2881 case OSD_NAV_FW_CRUISE_THR
:
2882 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "CRZ", 0, currentBatteryProfile
->nav
.fw
.cruise_throttle
, 4, 0, ADJUSTMENT_NAV_FW_CRUISE_THR
);
2885 case OSD_NAV_FW_PITCH2THR
:
2886 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "P2T", 0, currentBatteryProfile
->nav
.fw
.pitch_to_throttle
, 3, 0, ADJUSTMENT_NAV_FW_PITCH2THR
);
2889 case OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE
:
2890 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "0TP", 0, (float)navConfig()->fw
.minThrottleDownPitchAngle
/ 10, 3, 1, ADJUSTMENT_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE
);
2893 case OSD_FW_ALT_PID_OUTPUTS
:
2895 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
2896 osdFormatPidControllerOutput(buff
, "PZO", &nav_pids
->fw_alt
, 10, true); // display requested pitch degrees
2900 case OSD_FW_POS_PID_OUTPUTS
:
2902 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers(); // display requested roll degrees
2903 osdFormatPidControllerOutput(buff
, "PXYO", &nav_pids
->fw_nav
, 1, true);
2907 case OSD_MC_VEL_Z_PID_OUTPUTS
:
2909 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
2910 osdFormatPidControllerOutput(buff
, "VZO", &nav_pids
->vel
[Z
], 100, false); // display throttle adjustment µs
2914 case OSD_MC_VEL_X_PID_OUTPUTS
:
2916 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
2917 osdFormatPidControllerOutput(buff
, "VXO", &nav_pids
->vel
[X
], 100, false); // display requested acceleration cm/s^2
2921 case OSD_MC_VEL_Y_PID_OUTPUTS
:
2923 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
2924 osdFormatPidControllerOutput(buff
, "VYO", &nav_pids
->vel
[Y
], 100, false); // display requested acceleration cm/s^2
2928 case OSD_MC_POS_XYZ_P_OUTPUTS
:
2930 const navigationPIDControllers_t
*nav_pids
= getNavigationPIDControllers();
2931 strcpy(buff
, "POSO ");
2932 // display requested velocity cm/s
2933 tfp_sprintf(buff
+ 5, "%4d", (int)lrintf(nav_pids
->pos
[X
].output_constrained
* 100));
2935 tfp_sprintf(buff
+ 10, "%4d", (int)lrintf(nav_pids
->pos
[Y
].output_constrained
* 100));
2937 tfp_sprintf(buff
+ 15, "%4d", (int)lrintf(nav_pids
->pos
[Z
].output_constrained
* 100));
2944 bool kiloWatt
= osdFormatCentiNumber(buff
, getPower(), 1000, 2, 2, 3, false);
2945 buff
[3] = kiloWatt
? SYM_KILOWATT
: SYM_WATT
;
2948 uint8_t current_alarm
= osdConfig()->current_alarm
;
2949 if ((current_alarm
> 0) && ((getAmperage() / 100.0f
) > current_alarm
)) {
2950 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2960 if (pitotIsHealthy())
2962 const float airspeed_estimate
= getAirspeedEstimate();
2963 osdFormatVelocityStr(buff
+ 1, airspeed_estimate
, false, false);
2964 if ((osdConfig()->airspeed_alarm_min
!= 0 && airspeed_estimate
< osdConfig()->airspeed_alarm_min
) ||
2965 (osdConfig()->airspeed_alarm_max
!= 0 && airspeed_estimate
> osdConfig()->airspeed_alarm_max
)) {
2966 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2971 strcpy(buff
+ 1, " X!");
2972 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
2980 case OSD_AIR_MAX_SPEED
:
2985 osdFormatVelocityStr(buff
+ 2, stats
.max_air_speed
, false, false);
2994 // RTC not configured will show 00:00
2995 dateTime_t dateTime
;
2996 rtcGetDateTimeLocal(&dateTime
);
2997 buff
[0] = SYM_CLOCK
;
2998 tfp_sprintf(buff
+ 1, "%02u:%02u:%02u", dateTime
.hours
, dateTime
.minutes
, dateTime
.seconds
);
3004 elemAttr
= osdGetSystemMessage(buff
, OSD_MESSAGE_LENGTH
, true);
3010 tfp_sprintf(buff
, "INAV %s", FC_VERSION_STRING
);
3011 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
3015 case OSD_MAIN_BATT_CELL_VOLTAGE
:
3017 uint8_t base_digits
= 3U;
3018 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
3019 if(isBfCompatibleVideoSystem(osdConfig())) {
3020 base_digits
= 4U; // Add extra digit to account for decimal point taking an extra character space
3023 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatteryRawAverageCellVoltage(), base_digits
, 2);
3027 case OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE
:
3029 uint8_t base_digits
= 3U;
3030 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it
3031 if(isBfCompatibleVideoSystem(osdConfig())) {
3032 base_digits
= 4U; // Add extra digit to account for decimal point taking an extra character space
3035 osdDisplayBatteryVoltage(elemPosX
, elemPosY
, getBatterySagCompensatedAverageCellVoltage(), base_digits
, 2);
3039 case OSD_SCALED_THROTTLE_POS
:
3041 osdFormatThrottlePosition(buff
, true, &elemAttr
);
3047 buff
[0] = SYM_HEADING
;
3048 if (osdIsHeadingValid()) {
3049 int16_t h
= DECIDEGREES_TO_DEGREES(osdGetHeading());
3053 tfp_sprintf(&buff
[1], "%3d", h
);
3055 buff
[1] = buff
[2] = buff
[3] = '-';
3057 buff
[4] = SYM_DEGREES
;
3062 case OSD_HEADING_GRAPH
:
3064 if (osdIsHeadingValid()) {
3065 osdDrawHeadingGraph(osdDisplayPort
, osdGetDisplayPortCanvas(), OSD_DRAW_POINT_GRID(elemPosX
, elemPosY
), osdGetHeading());
3068 buff
[0] = buff
[2] = buff
[4] = buff
[6] = buff
[8] = SYM_HEADING_LINE
;
3069 buff
[1] = buff
[3] = buff
[5] = buff
[7] = SYM_HEADING_DIVIDED_LINE
;
3070 buff
[OSD_HEADING_GRAPH_WIDTH
] = '\0';
3075 case OSD_EFFICIENCY_MAH_PER_KM
:
3077 // amperage is in centi amps, speed is in cms/s. We want
3078 // mah/km. Only show when ground speed > 1m/s.
3079 static pt1Filter_t eFilterState
;
3080 static timeUs_t efficiencyUpdated
= 0;
3082 bool moreThanAh
= false;
3083 timeUs_t currentTimeUs
= micros();
3084 timeDelta_t efficiencyTimeDelta
= cmpTimeUs(currentTimeUs
, efficiencyUpdated
);
3085 uint8_t digits
= 3U;
3086 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
3087 if (isBfCompatibleVideoSystem(osdConfig())) {
3088 // Increase number of digits so values above 99 don't get scaled by osdFormatCentiNumber
3092 if (STATE(GPS_FIX
) && gpsSol
.groundSpeed
> 0) {
3093 if (efficiencyTimeDelta
>= EFFICIENCY_UPDATE_INTERVAL
) {
3094 value
= pt1FilterApply4(&eFilterState
, ((float)getAmperage() / gpsSol
.groundSpeed
) / 0.0036f
,
3095 1, US2S(efficiencyTimeDelta
));
3097 efficiencyUpdated
= currentTimeUs
;
3099 value
= eFilterState
.state
;
3102 bool efficiencyValid
= (value
> 0) && (gpsSol
.groundSpeed
> 100);
3103 switch (osdConfig()->units
) {
3106 case OSD_UNIT_IMPERIAL
:
3107 moreThanAh
= osdFormatCentiNumber(buff
, value
* METERS_PER_MILE
/ 10, 1000, 0, 2, digits
, false);
3109 tfp_sprintf(buff
, "%s%c%c", buff
, SYM_MAH_MI_0
, SYM_MAH_MI_1
);
3111 tfp_sprintf(buff
, "%s%c", buff
, SYM_AH_MI
);
3113 if (!efficiencyValid
) {
3114 buff
[0] = buff
[1] = buff
[2] = buff
[3] = '-';
3115 buff
[digits
] = SYM_MAH_MI_0
; // This will overwrite the "-" at buff[3] if not in BFCOMPAT mode
3116 buff
[digits
+ 1] = SYM_MAH_MI_1
;
3117 buff
[digits
+ 2] = '\0';
3121 moreThanAh
= osdFormatCentiNumber(buff
, value
* METERS_PER_NAUTICALMILE
/ 10, 1000, 0, 2, digits
, false);
3123 tfp_sprintf(buff
, "%s%c%c", buff
, SYM_MAH_NM_0
, SYM_MAH_NM_1
);
3125 tfp_sprintf(buff
, "%s%c", buff
, SYM_AH_NM
);
3127 if (!efficiencyValid
) {
3128 buff
[0] = buff
[1] = buff
[2] = buff
[3] = '-';
3129 buff
[digits
] = SYM_MAH_NM_0
;
3130 buff
[digits
+ 1] = SYM_MAH_NM_1
;
3131 buff
[digits
+ 2] = '\0';
3134 case OSD_UNIT_METRIC_MPH
:
3136 case OSD_UNIT_METRIC
:
3137 moreThanAh
= osdFormatCentiNumber(buff
, value
* 100, 1000, 0, 2, digits
, false);
3139 tfp_sprintf(buff
, "%s%c%c", buff
, SYM_MAH_KM_0
, SYM_MAH_KM_1
);
3141 tfp_sprintf(buff
, "%s%c", buff
, SYM_AH_KM
);
3143 if (!efficiencyValid
) {
3144 buff
[0] = buff
[1] = buff
[2] = buff
[3] = '-';
3145 buff
[digits
] = SYM_MAH_KM_0
;
3146 buff
[digits
+ 1] = SYM_MAH_KM_1
;
3147 buff
[digits
+ 2] = '\0';
3154 case OSD_EFFICIENCY_WH_PER_KM
:
3156 // amperage is in centi amps, speed is in cms/s. We want
3157 // mWh/km. Only show when ground speed > 1m/s.
3158 static pt1Filter_t eFilterState
;
3159 static timeUs_t efficiencyUpdated
= 0;
3161 timeUs_t currentTimeUs
= micros();
3162 timeDelta_t efficiencyTimeDelta
= cmpTimeUs(currentTimeUs
, efficiencyUpdated
);
3163 if (STATE(GPS_FIX
) && gpsSol
.groundSpeed
> 0) {
3164 if (efficiencyTimeDelta
>= EFFICIENCY_UPDATE_INTERVAL
) {
3165 value
= pt1FilterApply4(&eFilterState
, ((float)getPower() / gpsSol
.groundSpeed
) / 0.0036f
,
3166 1, US2S(efficiencyTimeDelta
));
3168 efficiencyUpdated
= currentTimeUs
;
3170 value
= eFilterState
.state
;
3173 bool efficiencyValid
= (value
> 0) && (gpsSol
.groundSpeed
> 100);
3174 switch (osdConfig()->units
) {
3177 case OSD_UNIT_IMPERIAL
:
3178 osdFormatCentiNumber(buff
, value
* METERS_PER_MILE
/ 10000, 0, 2, 0, 3, false);
3179 buff
[3] = SYM_WH_MI
;
3182 osdFormatCentiNumber(buff
, value
* METERS_PER_NAUTICALMILE
/ 10000, 0, 2, 0, 3, false);
3183 buff
[3] = SYM_WH_NM
;
3185 case OSD_UNIT_METRIC_MPH
:
3187 case OSD_UNIT_METRIC
:
3188 osdFormatCentiNumber(buff
, value
/ 10, 0, 2, 0, 3, false);
3189 buff
[3] = SYM_WH_KM
;
3193 if (!efficiencyValid
) {
3194 buff
[0] = buff
[1] = buff
[2] = '-';
3201 buff
[0] = SYM_GFORCE
;
3202 osdFormatCentiNumber(buff
+ 1, GForce
, 0, 2, 0, 3, false);
3203 if (GForce
> osdConfig()->gforce_alarm
* 100) {
3204 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3213 float GForceValue
= GForceAxis
[item
- OSD_GFORCE_X
];
3214 buff
[0] = SYM_GFORCE_X
+ item
- OSD_GFORCE_X
;
3215 osdFormatCentiNumber(buff
+ 1, GForceValue
, 0, 2, 0, 4, false);
3216 if ((GForceValue
< osdConfig()->gforce_axis_alarm_min
* 100) || (GForceValue
> osdConfig()->gforce_axis_alarm_max
* 100)) {
3217 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3224 * Longest representable string is -2147483648 does not fit in the screen.
3225 * Only 7 digits for negative and 8 digits for positive values allowed
3227 for (uint8_t bufferIndex
= 0; bufferIndex
< DEBUG32_VALUE_COUNT
; ++elemPosY
, bufferIndex
+= 2) {
3230 "[%u]=%8ld [%u]=%8ld",
3232 (long)constrain(debug
[bufferIndex
], -9999999, 99999999),
3234 (long)constrain(debug
[bufferIndex
+1], -9999999, 99999999)
3236 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
3241 case OSD_IMU_TEMPERATURE
:
3243 int16_t temperature
;
3244 const bool valid
= getIMUTemperature(&temperature
);
3245 osdDisplayTemperature(elemPosX
, elemPosY
, SYM_IMU_TEMP
, NULL
, valid
, temperature
, osdConfig()->imu_temp_alarm_min
, osdConfig()->imu_temp_alarm_max
);
3249 case OSD_BARO_TEMPERATURE
:
3251 int16_t temperature
;
3252 const bool valid
= getBaroTemperature(&temperature
);
3253 osdDisplayTemperature(elemPosX
, elemPosY
, SYM_BARO_TEMP
, NULL
, valid
, temperature
, osdConfig()->imu_temp_alarm_min
, osdConfig()->imu_temp_alarm_max
);
3257 #ifdef USE_TEMPERATURE_SENSOR
3258 case OSD_TEMP_SENSOR_0_TEMPERATURE
:
3259 case OSD_TEMP_SENSOR_1_TEMPERATURE
:
3260 case OSD_TEMP_SENSOR_2_TEMPERATURE
:
3261 case OSD_TEMP_SENSOR_3_TEMPERATURE
:
3262 case OSD_TEMP_SENSOR_4_TEMPERATURE
:
3263 case OSD_TEMP_SENSOR_5_TEMPERATURE
:
3264 case OSD_TEMP_SENSOR_6_TEMPERATURE
:
3265 case OSD_TEMP_SENSOR_7_TEMPERATURE
:
3267 osdDisplayTemperatureSensor(elemPosX
, elemPosY
, item
- OSD_TEMP_SENSOR_0_TEMPERATURE
);
3270 #endif /* ifdef USE_TEMPERATURE_SENSOR */
3272 case OSD_WIND_SPEED_HORIZONTAL
:
3273 #ifdef USE_WIND_ESTIMATOR
3275 bool valid
= isEstimatedWindSpeedValid();
3276 float horizontalWindSpeed
;
3278 horizontalWindSpeed
= getEstimatedHorizontalWindSpeed(&angle
);
3279 int16_t windDirection
= osdGetHeadingAngle( CENTIDEGREES_TO_DEGREES((int)angle
) - DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
) + 22);
3280 buff
[0] = SYM_WIND_HORIZONTAL
;
3281 buff
[1] = SYM_DIRECTION
+ (windDirection
*2 / 90);
3282 osdFormatWindSpeedStr(buff
+ 2, horizontalWindSpeed
, valid
);
3289 case OSD_WIND_SPEED_VERTICAL
:
3290 #ifdef USE_WIND_ESTIMATOR
3292 buff
[0] = SYM_WIND_VERTICAL
;
3293 buff
[1] = SYM_BLANK
;
3294 bool valid
= isEstimatedWindSpeedValid();
3295 float verticalWindSpeed
;
3296 verticalWindSpeed
= -getEstimatedWindSpeed(Z
); //from NED to NEU
3297 if (verticalWindSpeed
< 0) {
3298 buff
[1] = SYM_AH_DIRECTION_DOWN
;
3299 verticalWindSpeed
= -verticalWindSpeed
;
3301 buff
[1] = SYM_AH_DIRECTION_UP
;
3303 osdFormatWindSpeedStr(buff
+ 2, verticalWindSpeed
, valid
);
3312 STATIC_ASSERT(GPS_DEGREES_DIVIDER
== OLC_DEG_MULTIPLIER
, invalid_olc_deg_multiplier
);
3313 int digits
= osdConfig()->plus_code_digits
;
3314 int digitsRemoved
= osdConfig()->plus_code_short
* 2;
3315 if (STATE(GPS_FIX
)) {
3316 olc_encode(gpsSol
.llh
.lat
, gpsSol
.llh
.lon
, digits
, buff
, sizeof(buff
));
3318 // +codes with > 8 digits have a + at the 9th digit
3319 // and we only support 10 and up.
3320 memset(buff
, '-', digits
+ 1);
3322 buff
[digits
+ 1] = '\0';
3324 // Optionally trim digits from the left
3325 memmove(buff
, buff
+digitsRemoved
, strlen(buff
) + digitsRemoved
);
3326 buff
[digits
+ 1 - digitsRemoved
] = '\0';
3333 buff
[0] = SYM_AZIMUTH
;
3334 if (osdIsHeadingValid()) {
3335 int16_t h
= GPS_directionToHome
;
3344 tfp_sprintf(&buff
[1], "%3d", h
);
3346 buff
[1] = buff
[2] = buff
[3] = '-';
3348 buff
[4] = SYM_DEGREES
;
3356 int scaleUnitDivisor
;
3361 switch (osdConfig()->units
) {
3364 case OSD_UNIT_IMPERIAL
:
3365 scaleToUnit
= 100 / 1609.3440f
; // scale to 0.01mi for osdFormatCentiNumber()
3366 scaleUnitDivisor
= 0;
3367 symUnscaled
= SYM_MI
;
3372 scaleToUnit
= 100 / 1852.0010f
; // scale to 0.01mi for osdFormatCentiNumber()
3373 scaleUnitDivisor
= 0;
3374 symUnscaled
= SYM_NM
;
3379 case OSD_UNIT_METRIC_MPH
:
3381 case OSD_UNIT_METRIC
:
3382 scaleToUnit
= 100; // scale to cm for osdFormatCentiNumber()
3383 scaleUnitDivisor
= 1000; // Convert to km when scale gets bigger than 999m
3384 symUnscaled
= SYM_M
;
3389 buff
[0] = SYM_SCALE
;
3390 if (osdMapData
.scale
> 0) {
3391 bool scaled
= osdFormatCentiNumber(&buff
[1], osdMapData
.scale
* scaleToUnit
, scaleUnitDivisor
, maxDecimals
, 2, 3, false);
3392 buff
[4] = scaled
? symScaled
: symUnscaled
;
3393 // Make sure this is cleared if the map stops being drawn
3394 osdMapData
.scale
= 0;
3396 memset(&buff
[1], '-', 4);
3401 case OSD_MAP_REFERENCE
:
3403 char referenceSymbol
;
3404 if (osdMapData
.referenceSymbol
) {
3405 referenceSymbol
= osdMapData
.referenceSymbol
;
3406 // Make sure this is cleared if the map stops being drawn
3407 osdMapData
.referenceSymbol
= 0;
3409 referenceSymbol
= '-';
3411 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
, SYM_DIRECTION
);
3412 displayWriteChar(osdDisplayPort
, elemPosX
, elemPosY
+ 1, referenceSymbol
);
3418 osdFormatGVar(buff
, 0);
3423 osdFormatGVar(buff
, 1);
3428 osdFormatGVar(buff
, 2);
3433 osdFormatGVar(buff
, 3);
3437 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
3440 const char *source_text
= IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE
) && !mspOverrideIsInFailsafe() ? "MSP" : "STD";
3441 if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE
) && mspOverrideIsInFailsafe()) TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3442 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, source_text
, elemAttr
);
3447 #if defined(USE_ESC_SENSOR)
3450 escSensorData_t
* escSensor
= escSensorGetData();
3451 if (escSensor
&& escSensor
->dataAge
<= ESC_DATA_MAX_AGE
) {
3452 osdFormatRpm(buff
, escSensor
->rpm
);
3455 osdFormatRpm(buff
, 0);
3459 case OSD_ESC_TEMPERATURE
:
3461 escSensorData_t
* escSensor
= escSensorGetData();
3462 bool escTemperatureValid
= escSensor
&& escSensor
->dataAge
<= ESC_DATA_MAX_AGE
;
3463 osdDisplayTemperature(elemPosX
, elemPosY
, SYM_ESC_TEMP
, NULL
, escTemperatureValid
, (escSensor
->temperature
)*10, osdConfig()->esc_temp_alarm_min
, osdConfig()->esc_temp_alarm_max
);
3470 textAttributes_t attr
;
3472 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, "TPA");
3473 attr
= TEXT_ATTRIBUTES_NONE
;
3474 tfp_sprintf(buff
, "%3d", currentControlRateProfile
->throttle
.dynPID
);
3475 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA
)) {
3476 TEXT_ATTRIBUTES_ADD_BLINK(attr
);
3478 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 5, elemPosY
, buff
, attr
);
3480 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
+ 1, "BP");
3481 attr
= TEXT_ATTRIBUTES_NONE
;
3482 tfp_sprintf(buff
, "%4d", currentControlRateProfile
->throttle
.pa_breakpoint
);
3483 if (isAdjustmentFunctionSelected(ADJUSTMENT_TPA_BREAKPOINT
)) {
3484 TEXT_ATTRIBUTES_ADD_BLINK(attr
);
3486 displayWriteWithAttr(osdDisplayPort
, elemPosX
+ 4, elemPosY
+ 1, buff
, attr
);
3490 case OSD_TPA_TIME_CONSTANT
:
3492 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "TPA TC", 0, currentControlRateProfile
->throttle
.fixedWingTauMs
, 4, 0, ADJUSTMENT_FW_TPA_TIME_CONSTANT
);
3495 case OSD_FW_LEVEL_TRIM
:
3497 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "LEVEL", 0, getFixedWingLevelTrim(), 3, 1, ADJUSTMENT_FW_LEVEL_TRIM
);
3501 case OSD_NAV_FW_CONTROL_SMOOTHNESS
:
3503 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "CTL S", 0, navConfig()->fw
.control_smoothness
, 1, 0, ADJUSTMENT_NAV_FW_CONTROL_SMOOTHNESS
);
3506 #ifdef USE_MULTI_MISSION
3507 case OSD_NAV_WP_MULTI_MISSION_INDEX
:
3509 osdDisplayAdjustableDecimalValue(elemPosX
, elemPosY
, "WP NO", 0, navConfig()->general
.waypoint_multi_mission_index
, 1, 0, ADJUSTMENT_NAV_WP_MULTI_MISSION_INDEX
);
3515 if (IS_RC_MODE_ACTIVE(BOXPLANWPMISSION
)) {
3517 switch (posControl
.wpMissionPlannerStatus
) {
3519 strcpy(buf
, "WAIT");
3522 strcpy(buf
, "SAVE");
3525 strcpy(buf
, " OK ");
3528 strcpy(buf
, "FULL");
3530 tfp_sprintf(buff
, "%s>%2uWP", buf
, posControl
.wpPlannerActiveWPIndex
);
3531 } else if (posControl
.wpPlannerActiveWPIndex
){
3532 tfp_sprintf(buff
, "PLAN>%2uWP", posControl
.waypointCount
); // mission planner mision active
3534 #ifdef USE_MULTI_MISSION
3536 if (ARMING_FLAG(ARMED
) && !(IS_RC_MODE_ACTIVE(BOXCHANGEMISSION
) && posControl
.multiMissionCount
> 1)){
3537 // Limit field size when Armed, only show selected mission
3538 tfp_sprintf(buff
, "M%u ", posControl
.loadedMultiMissionIndex
);
3539 } else if (posControl
.multiMissionCount
) {
3540 if (navConfig()->general
.waypoint_multi_mission_index
!= posControl
.loadedMultiMissionIndex
) {
3541 tfp_sprintf(buff
, "M%u/%u>LOAD", navConfig()->general
.waypoint_multi_mission_index
, posControl
.multiMissionCount
);
3543 if (posControl
.waypointListValid
&& posControl
.waypointCount
> 0) {
3544 tfp_sprintf(buff
, "M%u/%u>%2uWP", posControl
.loadedMultiMissionIndex
, posControl
.multiMissionCount
, posControl
.waypointCount
);
3546 tfp_sprintf(buff
, "M0/%u> 0WP", posControl
.multiMissionCount
);
3549 } else { // no multi mission loaded - show active WP count from other source
3550 tfp_sprintf(buff
, "WP CNT>%2u", posControl
.waypointCount
);
3554 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
3558 #ifdef USE_POWER_LIMITS
3559 case OSD_PLIMIT_REMAINING_BURST_TIME
:
3560 osdFormatCentiNumber(buff
, powerLimiterGetRemainingBurstTime() * 100, 0, 1, 0, 3, false);
3565 case OSD_PLIMIT_ACTIVE_CURRENT_LIMIT
:
3566 if (currentBatteryProfile
->powerLimits
.continuousCurrent
) {
3567 osdFormatCentiNumber(buff
, powerLimiterGetActiveCurrentLimit(), 0, 2, 0, 3, false);
3571 if (powerLimiterIsLimitingCurrent()) {
3572 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3578 case OSD_PLIMIT_ACTIVE_POWER_LIMIT
:
3580 if (currentBatteryProfile
->powerLimits
.continuousPower
) {
3581 bool kiloWatt
= osdFormatCentiNumber(buff
, powerLimiterGetActivePowerLimit(), 1000, 2, 2, 3, false);
3582 buff
[3] = kiloWatt
? SYM_KILOWATT
: SYM_WATT
;
3585 if (powerLimiterIsLimitingPower()) {
3586 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
3592 #endif // USE_POWER_LIMITS
3593 case OSD_MULTI_FUNCTION
:
3595 // message shown infrequently so only write when needed
3596 static bool clearMultiFunction
= true;
3597 elemAttr
= osdGetMultiFunctionMessage(buff
);
3599 if (clearMultiFunction
) {
3600 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, " ");
3601 clearMultiFunction
= false;
3605 clearMultiFunction
= true;
3613 displayWriteWithAttr(osdDisplayPort
, elemPosX
, elemPosY
, buff
, elemAttr
);
3617 uint8_t osdIncElementIndex(uint8_t elementIndex
)
3621 if (elementIndex
== OSD_ARTIFICIAL_HORIZON
) { // always drawn last so skip
3625 #ifndef USE_TEMPERATURE_SENSOR
3626 if (elementIndex
== OSD_TEMP_SENSOR_0_TEMPERATURE
) {
3627 elementIndex
= OSD_ALTITUDE_MSL
;
3631 if (!(feature(FEATURE_VBAT
) && feature(FEATURE_CURRENT_METER
))) {
3632 if (elementIndex
== OSD_POWER
) {
3633 elementIndex
= OSD_GPS_LON
;
3635 if (elementIndex
== OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
) {
3636 elementIndex
= OSD_LEVEL_PIDS
;
3638 #ifdef USE_POWER_LIMITS
3639 if (elementIndex
== OSD_PLIMIT_REMAINING_BURST_TIME
) {
3640 elementIndex
= OSD_GLIDESLOPE
;
3645 #ifndef USE_POWER_LIMITS
3646 if (elementIndex
== OSD_PLIMIT_REMAINING_BURST_TIME
) {
3647 elementIndex
= OSD_GLIDESLOPE
;
3651 if (!feature(FEATURE_CURRENT_METER
)) {
3652 if (elementIndex
== OSD_CURRENT_DRAW
) {
3653 elementIndex
= OSD_GPS_SPEED
;
3655 if (elementIndex
== OSD_EFFICIENCY_MAH_PER_KM
) {
3656 elementIndex
= OSD_BATTERY_REMAINING_PERCENT
;
3658 if (elementIndex
== OSD_EFFICIENCY_WH_PER_KM
) {
3659 elementIndex
= OSD_TRIP_DIST
;
3661 if (elementIndex
== OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH
) {
3662 elementIndex
= OSD_HOME_HEADING_ERROR
;
3664 if (elementIndex
== OSD_CLIMB_EFFICIENCY
) {
3665 elementIndex
= OSD_NAV_WP_MULTI_MISSION_INDEX
;
3669 if (!STATE(ESC_SENSOR_ENABLED
)) {
3670 if (elementIndex
== OSD_ESC_RPM
) {
3671 elementIndex
= OSD_AZIMUTH
;
3675 if (!feature(FEATURE_GPS
)) {
3676 if (elementIndex
== OSD_GPS_HDOP
|| elementIndex
== OSD_TRIP_DIST
|| elementIndex
== OSD_3D_SPEED
|| elementIndex
== OSD_MISSION
||
3677 elementIndex
== OSD_AZIMUTH
|| elementIndex
== OSD_BATTERY_REMAINING_CAPACITY
|| elementIndex
== OSD_EFFICIENCY_MAH_PER_KM
) {
3680 if (elementIndex
== OSD_HEADING_GRAPH
&& !sensors(SENSOR_MAG
)) {
3681 elementIndex
= feature(FEATURE_CURRENT_METER
) ? OSD_WH_DRAWN
: OSD_BATTERY_REMAINING_PERCENT
;
3683 if (elementIndex
== OSD_EFFICIENCY_WH_PER_KM
) {
3684 elementIndex
= OSD_ATTITUDE_PITCH
;
3686 if (elementIndex
== OSD_GPS_SPEED
) {
3687 elementIndex
= OSD_ALTITUDE
;
3689 if (elementIndex
== OSD_GPS_LON
) {
3690 elementIndex
= sensors(SENSOR_MAG
) ? OSD_HEADING
: OSD_VARIO
;
3692 if (elementIndex
== OSD_MAP_NORTH
) {
3693 elementIndex
= feature(FEATURE_CURRENT_METER
) ? OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
: OSD_LEVEL_PIDS
;
3695 if (elementIndex
== OSD_PLUS_CODE
) {
3696 elementIndex
= OSD_GFORCE
;
3698 if (elementIndex
== OSD_GLIDESLOPE
) {
3699 elementIndex
= OSD_AIR_MAX_SPEED
;
3701 if (elementIndex
== OSD_GLIDE_RANGE
) {
3702 elementIndex
= feature(FEATURE_CURRENT_METER
) ? OSD_CLIMB_EFFICIENCY
: OSD_PILOT_NAME
;
3704 if (elementIndex
== OSD_NAV_WP_MULTI_MISSION_INDEX
) {
3705 elementIndex
= OSD_PILOT_NAME
;
3709 if (!sensors(SENSOR_ACC
)) {
3710 if (elementIndex
== OSD_CROSSHAIRS
) {
3711 elementIndex
= OSD_ONTIME
;
3713 if (elementIndex
== OSD_GFORCE
) {
3714 elementIndex
= OSD_RC_SOURCE
;
3718 if (elementIndex
== OSD_ITEM_COUNT
) {
3721 return elementIndex
;
3724 void osdDrawNextElement(void)
3726 static uint8_t elementIndex
= 0;
3727 // Flag for end of loop, also prevents infinite loop when no elements are enabled
3728 uint8_t index
= elementIndex
;
3730 elementIndex
= osdIncElementIndex(elementIndex
);
3731 } while (!osdDrawSingleElement(elementIndex
) && index
!= elementIndex
);
3733 // Draw artificial horizon + tracking telemtry last
3734 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON
);
3735 if (osdConfig()->telemetry
>0){
3736 osdDisplayTelemetry();
3740 PG_RESET_TEMPLATE(osdConfig_t
, osdConfig
,
3741 .rssi_alarm
= SETTING_OSD_RSSI_ALARM_DEFAULT
,
3742 .time_alarm
= SETTING_OSD_TIME_ALARM_DEFAULT
,
3743 .alt_alarm
= SETTING_OSD_ALT_ALARM_DEFAULT
,
3744 .dist_alarm
= SETTING_OSD_DIST_ALARM_DEFAULT
,
3745 .neg_alt_alarm
= SETTING_OSD_NEG_ALT_ALARM_DEFAULT
,
3746 .current_alarm
= SETTING_OSD_CURRENT_ALARM_DEFAULT
,
3747 .imu_temp_alarm_min
= SETTING_OSD_IMU_TEMP_ALARM_MIN_DEFAULT
,
3748 .imu_temp_alarm_max
= SETTING_OSD_IMU_TEMP_ALARM_MAX_DEFAULT
,
3749 .esc_temp_alarm_min
= SETTING_OSD_ESC_TEMP_ALARM_MIN_DEFAULT
,
3750 .esc_temp_alarm_max
= SETTING_OSD_ESC_TEMP_ALARM_MAX_DEFAULT
,
3751 .gforce_alarm
= SETTING_OSD_GFORCE_ALARM_DEFAULT
,
3752 .gforce_axis_alarm_min
= SETTING_OSD_GFORCE_AXIS_ALARM_MIN_DEFAULT
,
3753 .gforce_axis_alarm_max
= SETTING_OSD_GFORCE_AXIS_ALARM_MAX_DEFAULT
,
3755 .baro_temp_alarm_min
= SETTING_OSD_BARO_TEMP_ALARM_MIN_DEFAULT
,
3756 .baro_temp_alarm_max
= SETTING_OSD_BARO_TEMP_ALARM_MAX_DEFAULT
,
3758 #ifdef USE_SERIALRX_CRSF
3759 .snr_alarm
= SETTING_OSD_SNR_ALARM_DEFAULT
,
3760 .crsf_lq_format
= SETTING_OSD_CRSF_LQ_FORMAT_DEFAULT
,
3761 .link_quality_alarm
= SETTING_OSD_LINK_QUALITY_ALARM_DEFAULT
,
3762 .rssi_dbm_alarm
= SETTING_OSD_RSSI_DBM_ALARM_DEFAULT
,
3763 .rssi_dbm_max
= SETTING_OSD_RSSI_DBM_MAX_DEFAULT
,
3764 .rssi_dbm_min
= SETTING_OSD_RSSI_DBM_MIN_DEFAULT
,
3766 #ifdef USE_TEMPERATURE_SENSOR
3767 .temp_label_align
= SETTING_OSD_TEMP_LABEL_ALIGN_DEFAULT
,
3770 .airspeed_alarm_min
= SETTING_OSD_AIRSPEED_ALARM_MIN_DEFAULT
,
3771 .airspeed_alarm_max
= SETTING_OSD_AIRSPEED_ALARM_MAX_DEFAULT
,
3774 .video_system
= SETTING_OSD_VIDEO_SYSTEM_DEFAULT
,
3775 .row_shiftdown
= SETTING_OSD_ROW_SHIFTDOWN_DEFAULT
,
3776 .msp_displayport_fullframe_interval
= SETTING_OSD_MSP_DISPLAYPORT_FULLFRAME_INTERVAL_DEFAULT
,
3778 .ahi_reverse_roll
= SETTING_OSD_AHI_REVERSE_ROLL_DEFAULT
,
3779 .ahi_max_pitch
= SETTING_OSD_AHI_MAX_PITCH_DEFAULT
,
3780 .crosshairs_style
= SETTING_OSD_CROSSHAIRS_STYLE_DEFAULT
,
3781 .horizon_offset
= SETTING_OSD_HORIZON_OFFSET_DEFAULT
,
3782 .camera_uptilt
= SETTING_OSD_CAMERA_UPTILT_DEFAULT
,
3783 .ahi_camera_uptilt_comp
= SETTING_OSD_AHI_CAMERA_UPTILT_COMP_DEFAULT
,
3784 .camera_fov_h
= SETTING_OSD_CAMERA_FOV_H_DEFAULT
,
3785 .camera_fov_v
= SETTING_OSD_CAMERA_FOV_V_DEFAULT
,
3786 .hud_margin_h
= SETTING_OSD_HUD_MARGIN_H_DEFAULT
,
3787 .hud_margin_v
= SETTING_OSD_HUD_MARGIN_V_DEFAULT
,
3788 .hud_homing
= SETTING_OSD_HUD_HOMING_DEFAULT
,
3789 .hud_homepoint
= SETTING_OSD_HUD_HOMEPOINT_DEFAULT
,
3790 .hud_radar_disp
= SETTING_OSD_HUD_RADAR_DISP_DEFAULT
,
3791 .hud_radar_range_min
= SETTING_OSD_HUD_RADAR_RANGE_MIN_DEFAULT
,
3792 .hud_radar_range_max
= SETTING_OSD_HUD_RADAR_RANGE_MAX_DEFAULT
,
3793 .hud_radar_alt_difference_display_time
= SETTING_OSD_HUD_RADAR_ALT_DIFFERENCE_DISPLAY_TIME_DEFAULT
,
3794 .hud_radar_distance_display_time
= SETTING_OSD_HUD_RADAR_DISTANCE_DISPLAY_TIME_DEFAULT
,
3795 .hud_wp_disp
= SETTING_OSD_HUD_WP_DISP_DEFAULT
,
3796 .left_sidebar_scroll
= SETTING_OSD_LEFT_SIDEBAR_SCROLL_DEFAULT
,
3797 .right_sidebar_scroll
= SETTING_OSD_RIGHT_SIDEBAR_SCROLL_DEFAULT
,
3798 .sidebar_scroll_arrows
= SETTING_OSD_SIDEBAR_SCROLL_ARROWS_DEFAULT
,
3799 .sidebar_horizontal_offset
= SETTING_OSD_SIDEBAR_HORIZONTAL_OFFSET_DEFAULT
,
3800 .left_sidebar_scroll_step
= SETTING_OSD_LEFT_SIDEBAR_SCROLL_STEP_DEFAULT
,
3801 .right_sidebar_scroll_step
= SETTING_OSD_RIGHT_SIDEBAR_SCROLL_STEP_DEFAULT
,
3802 .sidebar_height
= SETTING_OSD_SIDEBAR_HEIGHT_DEFAULT
,
3803 .ahi_pitch_interval
= SETTING_OSD_AHI_PITCH_INTERVAL_DEFAULT
,
3804 .osd_home_position_arm_screen
= SETTING_OSD_HOME_POSITION_ARM_SCREEN_DEFAULT
,
3805 .pan_servo_index
= SETTING_OSD_PAN_SERVO_INDEX_DEFAULT
,
3806 .pan_servo_pwm2centideg
= SETTING_OSD_PAN_SERVO_PWM2CENTIDEG_DEFAULT
,
3807 .pan_servo_offcentre_warning
= SETTING_OSD_PAN_SERVO_OFFCENTRE_WARNING_DEFAULT
,
3808 .pan_servo_indicator_show_degrees
= SETTING_OSD_PAN_SERVO_INDICATOR_SHOW_DEGREES_DEFAULT
,
3809 .esc_rpm_precision
= SETTING_OSD_ESC_RPM_PRECISION_DEFAULT
,
3810 .mAh_precision
= SETTING_OSD_MAH_PRECISION_DEFAULT
,
3811 .osd_switch_indicator0_name
= SETTING_OSD_SWITCH_INDICATOR_ZERO_NAME_DEFAULT
,
3812 .osd_switch_indicator0_channel
= SETTING_OSD_SWITCH_INDICATOR_ZERO_CHANNEL_DEFAULT
,
3813 .osd_switch_indicator1_name
= SETTING_OSD_SWITCH_INDICATOR_ONE_NAME_DEFAULT
,
3814 .osd_switch_indicator1_channel
= SETTING_OSD_SWITCH_INDICATOR_ONE_CHANNEL_DEFAULT
,
3815 .osd_switch_indicator2_name
= SETTING_OSD_SWITCH_INDICATOR_TWO_NAME_DEFAULT
,
3816 .osd_switch_indicator2_channel
= SETTING_OSD_SWITCH_INDICATOR_TWO_CHANNEL_DEFAULT
,
3817 .osd_switch_indicator3_name
= SETTING_OSD_SWITCH_INDICATOR_THREE_NAME_DEFAULT
,
3818 .osd_switch_indicator3_channel
= SETTING_OSD_SWITCH_INDICATOR_THREE_CHANNEL_DEFAULT
,
3819 .osd_switch_indicators_align_left
= SETTING_OSD_SWITCH_INDICATORS_ALIGN_LEFT_DEFAULT
,
3820 .system_msg_display_time
= SETTING_OSD_SYSTEM_MSG_DISPLAY_TIME_DEFAULT
,
3821 .units
= SETTING_OSD_UNITS_DEFAULT
,
3822 .main_voltage_decimals
= SETTING_OSD_MAIN_VOLTAGE_DECIMALS_DEFAULT
,
3823 .use_pilot_logo
= SETTING_OSD_USE_PILOT_LOGO_DEFAULT
,
3824 .inav_to_pilot_logo_spacing
= SETTING_OSD_INAV_TO_PILOT_LOGO_SPACING_DEFAULT
,
3825 .arm_screen_display_time
= SETTING_OSD_ARM_SCREEN_DISPLAY_TIME_DEFAULT
,
3827 #ifdef USE_WIND_ESTIMATOR
3828 .estimations_wind_compensation
= SETTING_OSD_ESTIMATIONS_WIND_COMPENSATION_DEFAULT
,
3831 .coordinate_digits
= SETTING_OSD_COORDINATE_DIGITS_DEFAULT
,
3833 .osd_failsafe_switch_layout
= SETTING_OSD_FAILSAFE_SWITCH_LAYOUT_DEFAULT
,
3835 .plus_code_digits
= SETTING_OSD_PLUS_CODE_DIGITS_DEFAULT
,
3836 .plus_code_short
= SETTING_OSD_PLUS_CODE_SHORT_DEFAULT
,
3838 .ahi_width
= SETTING_OSD_AHI_WIDTH_DEFAULT
,
3839 .ahi_height
= SETTING_OSD_AHI_HEIGHT_DEFAULT
,
3840 .ahi_vertical_offset
= SETTING_OSD_AHI_VERTICAL_OFFSET_DEFAULT
,
3841 .ahi_bordered
= SETTING_OSD_AHI_BORDERED_DEFAULT
,
3842 .ahi_style
= SETTING_OSD_AHI_STYLE_DEFAULT
,
3844 .force_grid
= SETTING_OSD_FORCE_GRID_DEFAULT
,
3846 .stats_energy_unit
= SETTING_OSD_STATS_ENERGY_UNIT_DEFAULT
,
3847 .stats_min_voltage_unit
= SETTING_OSD_STATS_MIN_VOLTAGE_UNIT_DEFAULT
,
3848 .stats_page_auto_swap_time
= SETTING_OSD_STATS_PAGE_AUTO_SWAP_TIME_DEFAULT
3851 void pgResetFn_osdLayoutsConfig(osdLayoutsConfig_t
*osdLayoutsConfig
)
3853 osdLayoutsConfig
->item_pos
[0][OSD_ALTITUDE
] = OSD_POS(1, 0) | OSD_VISIBLE_FLAG
;
3854 osdLayoutsConfig
->item_pos
[0][OSD_MAIN_BATT_VOLTAGE
] = OSD_POS(12, 0) | OSD_VISIBLE_FLAG
;
3855 osdLayoutsConfig
->item_pos
[0][OSD_SAG_COMPENSATED_MAIN_BATT_VOLTAGE
] = OSD_POS(12, 1);
3857 osdLayoutsConfig
->item_pos
[0][OSD_RSSI_VALUE
] = OSD_POS(23, 0) | OSD_VISIBLE_FLAG
;
3859 osdLayoutsConfig
->item_pos
[0][OSD_HOME_DIST
] = OSD_POS(1, 1);
3860 osdLayoutsConfig
->item_pos
[0][OSD_TRIP_DIST
] = OSD_POS(1, 2);
3861 osdLayoutsConfig
->item_pos
[0][OSD_ODOMETER
] = OSD_POS(1, 3);
3862 osdLayoutsConfig
->item_pos
[0][OSD_MAIN_BATT_CELL_VOLTAGE
] = OSD_POS(12, 1);
3863 osdLayoutsConfig
->item_pos
[0][OSD_MAIN_BATT_SAG_COMPENSATED_CELL_VOLTAGE
] = OSD_POS(12, 1);
3864 osdLayoutsConfig
->item_pos
[0][OSD_GPS_SPEED
] = OSD_POS(23, 1);
3865 osdLayoutsConfig
->item_pos
[0][OSD_3D_SPEED
] = OSD_POS(23, 1);
3866 osdLayoutsConfig
->item_pos
[0][OSD_GLIDESLOPE
] = OSD_POS(23, 2);
3868 osdLayoutsConfig
->item_pos
[0][OSD_THROTTLE_POS
] = OSD_POS(1, 2) | OSD_VISIBLE_FLAG
;
3869 osdLayoutsConfig
->item_pos
[0][OSD_SCALED_THROTTLE_POS
] = OSD_POS(6, 2);
3870 osdLayoutsConfig
->item_pos
[0][OSD_HEADING
] = OSD_POS(12, 2);
3871 osdLayoutsConfig
->item_pos
[0][OSD_GROUND_COURSE
] = OSD_POS(12, 3);
3872 osdLayoutsConfig
->item_pos
[0][OSD_COURSE_HOLD_ERROR
] = OSD_POS(12, 2);
3873 osdLayoutsConfig
->item_pos
[0][OSD_COURSE_HOLD_ADJUSTMENT
] = OSD_POS(12, 2);
3874 osdLayoutsConfig
->item_pos
[0][OSD_CROSS_TRACK_ERROR
] = OSD_POS(12, 3);
3875 osdLayoutsConfig
->item_pos
[0][OSD_HEADING_GRAPH
] = OSD_POS(18, 2);
3876 osdLayoutsConfig
->item_pos
[0][OSD_CURRENT_DRAW
] = OSD_POS(2, 3) | OSD_VISIBLE_FLAG
;
3877 osdLayoutsConfig
->item_pos
[0][OSD_MAH_DRAWN
] = OSD_POS(1, 4) | OSD_VISIBLE_FLAG
;
3878 osdLayoutsConfig
->item_pos
[0][OSD_WH_DRAWN
] = OSD_POS(1, 5);
3879 osdLayoutsConfig
->item_pos
[0][OSD_BATTERY_REMAINING_CAPACITY
] = OSD_POS(1, 6);
3880 osdLayoutsConfig
->item_pos
[0][OSD_BATTERY_REMAINING_PERCENT
] = OSD_POS(1, 7);
3881 osdLayoutsConfig
->item_pos
[0][OSD_POWER_SUPPLY_IMPEDANCE
] = OSD_POS(1, 8);
3883 osdLayoutsConfig
->item_pos
[0][OSD_EFFICIENCY_MAH_PER_KM
] = OSD_POS(1, 5);
3884 osdLayoutsConfig
->item_pos
[0][OSD_EFFICIENCY_WH_PER_KM
] = OSD_POS(1, 5);
3886 osdLayoutsConfig
->item_pos
[0][OSD_ATTITUDE_ROLL
] = OSD_POS(1, 7);
3887 osdLayoutsConfig
->item_pos
[0][OSD_ATTITUDE_PITCH
] = OSD_POS(1, 8);
3889 // avoid OSD_VARIO under OSD_CROSSHAIRS
3890 osdLayoutsConfig
->item_pos
[0][OSD_VARIO
] = OSD_POS(23, 5);
3891 // OSD_VARIO_NUM at the right of OSD_VARIO
3892 osdLayoutsConfig
->item_pos
[0][OSD_VARIO_NUM
] = OSD_POS(24, 7);
3893 osdLayoutsConfig
->item_pos
[0][OSD_HOME_DIR
] = OSD_POS(14, 11);
3894 osdLayoutsConfig
->item_pos
[0][OSD_ARTIFICIAL_HORIZON
] = OSD_POS(8, 6);
3895 osdLayoutsConfig
->item_pos
[0][OSD_HORIZON_SIDEBARS
] = OSD_POS(8, 6);
3897 osdLayoutsConfig
->item_pos
[0][OSD_CRAFT_NAME
] = OSD_POS(20, 2);
3898 osdLayoutsConfig
->item_pos
[0][OSD_PILOT_NAME
] = OSD_POS(20, 3);
3899 osdLayoutsConfig
->item_pos
[0][OSD_PILOT_LOGO
] = OSD_POS(20, 3);
3900 osdLayoutsConfig
->item_pos
[0][OSD_VTX_CHANNEL
] = OSD_POS(8, 6);
3902 #ifdef USE_SERIALRX_CRSF
3903 osdLayoutsConfig
->item_pos
[0][OSD_CRSF_RSSI_DBM
] = OSD_POS(23, 12);
3904 osdLayoutsConfig
->item_pos
[0][OSD_CRSF_LQ
] = OSD_POS(23, 11);
3905 osdLayoutsConfig
->item_pos
[0][OSD_CRSF_SNR_DB
] = OSD_POS(24, 9);
3906 osdLayoutsConfig
->item_pos
[0][OSD_CRSF_TX_POWER
] = OSD_POS(24, 10);
3909 osdLayoutsConfig
->item_pos
[0][OSD_ONTIME
] = OSD_POS(23, 8);
3910 osdLayoutsConfig
->item_pos
[0][OSD_FLYTIME
] = OSD_POS(23, 9);
3911 osdLayoutsConfig
->item_pos
[0][OSD_ONTIME_FLYTIME
] = OSD_POS(23, 11) | OSD_VISIBLE_FLAG
;
3912 osdLayoutsConfig
->item_pos
[0][OSD_RTC_TIME
] = OSD_POS(23, 12);
3913 osdLayoutsConfig
->item_pos
[0][OSD_REMAINING_FLIGHT_TIME_BEFORE_RTH
] = OSD_POS(23, 7);
3914 osdLayoutsConfig
->item_pos
[0][OSD_REMAINING_DISTANCE_BEFORE_RTH
] = OSD_POS(23, 6);
3916 osdLayoutsConfig
->item_pos
[0][OSD_MISSION
] = OSD_POS(0, 10);
3917 osdLayoutsConfig
->item_pos
[0][OSD_GPS_SATS
] = OSD_POS(0, 11) | OSD_VISIBLE_FLAG
;
3918 osdLayoutsConfig
->item_pos
[0][OSD_GPS_HDOP
] = OSD_POS(0, 10);
3920 osdLayoutsConfig
->item_pos
[0][OSD_GPS_LAT
] = OSD_POS(0, 12);
3921 // Put this on top of the latitude, since it's very unlikely
3922 // that users will want to use both at the same time.
3923 osdLayoutsConfig
->item_pos
[0][OSD_PLUS_CODE
] = OSD_POS(0, 12);
3924 osdLayoutsConfig
->item_pos
[0][OSD_FLYMODE
] = OSD_POS(13, 12) | OSD_VISIBLE_FLAG
;
3925 osdLayoutsConfig
->item_pos
[0][OSD_GPS_LON
] = OSD_POS(18, 12);
3927 osdLayoutsConfig
->item_pos
[0][OSD_AZIMUTH
] = OSD_POS(2, 12);
3929 osdLayoutsConfig
->item_pos
[0][OSD_ROLL_PIDS
] = OSD_POS(2, 10);
3930 osdLayoutsConfig
->item_pos
[0][OSD_PITCH_PIDS
] = OSD_POS(2, 11);
3931 osdLayoutsConfig
->item_pos
[0][OSD_YAW_PIDS
] = OSD_POS(2, 12);
3932 osdLayoutsConfig
->item_pos
[0][OSD_LEVEL_PIDS
] = OSD_POS(2, 12);
3933 osdLayoutsConfig
->item_pos
[0][OSD_POS_XY_PIDS
] = OSD_POS(2, 12);
3934 osdLayoutsConfig
->item_pos
[0][OSD_POS_Z_PIDS
] = OSD_POS(2, 12);
3935 osdLayoutsConfig
->item_pos
[0][OSD_VEL_XY_PIDS
] = OSD_POS(2, 12);
3936 osdLayoutsConfig
->item_pos
[0][OSD_VEL_Z_PIDS
] = OSD_POS(2, 12);
3937 osdLayoutsConfig
->item_pos
[0][OSD_HEADING_P
] = OSD_POS(2, 12);
3938 osdLayoutsConfig
->item_pos
[0][OSD_BOARD_ALIGN_ROLL
] = OSD_POS(2, 10);
3939 osdLayoutsConfig
->item_pos
[0][OSD_BOARD_ALIGN_PITCH
] = OSD_POS(2, 11);
3940 osdLayoutsConfig
->item_pos
[0][OSD_RC_EXPO
] = OSD_POS(2, 12);
3941 osdLayoutsConfig
->item_pos
[0][OSD_RC_YAW_EXPO
] = OSD_POS(2, 12);
3942 osdLayoutsConfig
->item_pos
[0][OSD_THROTTLE_EXPO
] = OSD_POS(2, 12);
3943 osdLayoutsConfig
->item_pos
[0][OSD_PITCH_RATE
] = OSD_POS(2, 12);
3944 osdLayoutsConfig
->item_pos
[0][OSD_ROLL_RATE
] = OSD_POS(2, 12);
3945 osdLayoutsConfig
->item_pos
[0][OSD_YAW_RATE
] = OSD_POS(2, 12);
3946 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_RC_EXPO
] = OSD_POS(2, 12);
3947 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_RC_YAW_EXPO
] = OSD_POS(2, 12);
3948 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_PITCH_RATE
] = OSD_POS(2, 12);
3949 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_ROLL_RATE
] = OSD_POS(2, 12);
3950 osdLayoutsConfig
->item_pos
[0][OSD_MANUAL_YAW_RATE
] = OSD_POS(2, 12);
3951 osdLayoutsConfig
->item_pos
[0][OSD_NAV_FW_CRUISE_THR
] = OSD_POS(2, 12);
3952 osdLayoutsConfig
->item_pos
[0][OSD_NAV_FW_PITCH2THR
] = OSD_POS(2, 12);
3953 osdLayoutsConfig
->item_pos
[0][OSD_FW_MIN_THROTTLE_DOWN_PITCH_ANGLE
] = OSD_POS(2, 12);
3954 osdLayoutsConfig
->item_pos
[0][OSD_FW_ALT_PID_OUTPUTS
] = OSD_POS(2, 12);
3955 osdLayoutsConfig
->item_pos
[0][OSD_FW_POS_PID_OUTPUTS
] = OSD_POS(2, 12);
3956 osdLayoutsConfig
->item_pos
[0][OSD_MC_VEL_X_PID_OUTPUTS
] = OSD_POS(2, 12);
3957 osdLayoutsConfig
->item_pos
[0][OSD_MC_VEL_Y_PID_OUTPUTS
] = OSD_POS(2, 12);
3958 osdLayoutsConfig
->item_pos
[0][OSD_MC_VEL_Z_PID_OUTPUTS
] = OSD_POS(2, 12);
3959 osdLayoutsConfig
->item_pos
[0][OSD_MC_POS_XYZ_P_OUTPUTS
] = OSD_POS(2, 12);
3961 osdLayoutsConfig
->item_pos
[0][OSD_POWER
] = OSD_POS(15, 1);
3963 osdLayoutsConfig
->item_pos
[0][OSD_IMU_TEMPERATURE
] = OSD_POS(19, 2);
3964 osdLayoutsConfig
->item_pos
[0][OSD_BARO_TEMPERATURE
] = OSD_POS(19, 3);
3965 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_0_TEMPERATURE
] = OSD_POS(19, 4);
3966 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_1_TEMPERATURE
] = OSD_POS(19, 5);
3967 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_2_TEMPERATURE
] = OSD_POS(19, 6);
3968 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_3_TEMPERATURE
] = OSD_POS(19, 7);
3969 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_4_TEMPERATURE
] = OSD_POS(19, 8);
3970 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_5_TEMPERATURE
] = OSD_POS(19, 9);
3971 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_6_TEMPERATURE
] = OSD_POS(19, 10);
3972 osdLayoutsConfig
->item_pos
[0][OSD_TEMP_SENSOR_7_TEMPERATURE
] = OSD_POS(19, 11);
3974 osdLayoutsConfig
->item_pos
[0][OSD_AIR_SPEED
] = OSD_POS(3, 5);
3975 osdLayoutsConfig
->item_pos
[0][OSD_WIND_SPEED_HORIZONTAL
] = OSD_POS(3, 6);
3976 osdLayoutsConfig
->item_pos
[0][OSD_WIND_SPEED_VERTICAL
] = OSD_POS(3, 7);
3978 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE
] = OSD_POS(12, 4);
3979 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE_X
] = OSD_POS(12, 5);
3980 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE_Y
] = OSD_POS(12, 6);
3981 osdLayoutsConfig
->item_pos
[0][OSD_GFORCE_Z
] = OSD_POS(12, 7);
3983 osdLayoutsConfig
->item_pos
[0][OSD_VTX_POWER
] = OSD_POS(3, 5);
3985 osdLayoutsConfig
->item_pos
[0][OSD_GVAR_0
] = OSD_POS(1, 1);
3986 osdLayoutsConfig
->item_pos
[0][OSD_GVAR_1
] = OSD_POS(1, 2);
3987 osdLayoutsConfig
->item_pos
[0][OSD_GVAR_2
] = OSD_POS(1, 3);
3988 osdLayoutsConfig
->item_pos
[0][OSD_GVAR_3
] = OSD_POS(1, 4);
3990 osdLayoutsConfig
->item_pos
[0][OSD_MULTI_FUNCTION
] = OSD_POS(1, 4);
3992 osdLayoutsConfig
->item_pos
[0][OSD_SWITCH_INDICATOR_0
] = OSD_POS(2, 7);
3993 osdLayoutsConfig
->item_pos
[0][OSD_SWITCH_INDICATOR_1
] = OSD_POS(2, 8);
3994 osdLayoutsConfig
->item_pos
[0][OSD_SWITCH_INDICATOR_2
] = OSD_POS(2, 9);
3995 osdLayoutsConfig
->item_pos
[0][OSD_SWITCH_INDICATOR_3
] = OSD_POS(2, 10);
3997 #if defined(USE_ESC_SENSOR)
3998 osdLayoutsConfig
->item_pos
[0][OSD_ESC_RPM
] = OSD_POS(1, 2);
3999 osdLayoutsConfig
->item_pos
[0][OSD_ESC_TEMPERATURE
] = OSD_POS(1, 3);
4002 #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
4003 osdLayoutsConfig
->item_pos
[0][OSD_RC_SOURCE
] = OSD_POS(3, 4);
4006 #ifdef USE_POWER_LIMITS
4007 osdLayoutsConfig
->item_pos
[0][OSD_PLIMIT_REMAINING_BURST_TIME
] = OSD_POS(3, 4);
4008 osdLayoutsConfig
->item_pos
[0][OSD_PLIMIT_ACTIVE_CURRENT_LIMIT
] = OSD_POS(3, 5);
4009 osdLayoutsConfig
->item_pos
[0][OSD_PLIMIT_ACTIVE_POWER_LIMIT
] = OSD_POS(3, 6);
4012 // Under OSD_FLYMODE. TODO: Might not be visible on NTSC?
4013 osdLayoutsConfig
->item_pos
[0][OSD_MESSAGES
] = OSD_POS(1, 13) | OSD_VISIBLE_FLAG
;
4015 for (unsigned ii
= 1; ii
< OSD_LAYOUT_COUNT
; ii
++) {
4016 for (unsigned jj
= 0; jj
< ARRAYLEN(osdLayoutsConfig
->item_pos
[0]); jj
++) {
4017 osdLayoutsConfig
->item_pos
[ii
][jj
] = osdLayoutsConfig
->item_pos
[0][jj
] & ~OSD_VISIBLE_FLAG
;
4023 * @brief Draws the INAV and/or pilot logos on the display
4025 * @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
4026 * @param row The row number to start drawing the logos. If not singular, both logos are drawn on the same rows.
4027 * @return uint8_t The row number after the logo(s).
4029 uint8_t drawLogos(bool singular
, uint8_t row
) {
4030 uint8_t logoRow
= row
;
4031 uint8_t logoColOffset
= 0;
4032 bool usePilotLogo
= (osdConfig()->use_pilot_logo
&& osdDisplayIsHD());
4034 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is in use, the pilot logo cannot be used, due to font issues.
4035 if (isBfCompatibleVideoSystem(osdConfig()))
4036 usePilotLogo
= false;
4039 uint8_t logoSpacing
= osdConfig()->inav_to_pilot_logo_spacing
;
4041 if (logoSpacing
> 0 && ((osdDisplayPort
->cols
% 2) != (logoSpacing
% 2))) {
4042 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
4046 if (usePilotLogo
&& !singular
) {
4047 logoColOffset
= ((osdDisplayPort
->cols
- (SYM_LOGO_WIDTH
* 2)) - logoSpacing
) / 2;
4049 logoColOffset
= floorf((osdDisplayPort
->cols
- SYM_LOGO_WIDTH
) / 2.0f
);
4053 if ((singular
&& !usePilotLogo
) || !singular
) {
4054 unsigned logo_c
= SYM_LOGO_START
;
4055 uint8_t logo_x
= logoColOffset
;
4056 for (uint8_t lRow
= 0; lRow
< SYM_LOGO_HEIGHT
; lRow
++) {
4057 for (uint8_t lCol
= 0; lCol
< SYM_LOGO_WIDTH
; lCol
++) {
4058 displayWriteChar(osdDisplayPort
, logo_x
+ lCol
, logoRow
, logo_c
++);
4064 // Draw the pilot logo
4066 unsigned logo_c
= SYM_PILOT_LOGO_LRG_START
;
4070 logo_x
= logoColOffset
;
4072 logo_x
= logoColOffset
+ SYM_LOGO_WIDTH
+ logoSpacing
;
4075 for (uint8_t lRow
= 0; lRow
< SYM_LOGO_HEIGHT
; lRow
++) {
4076 for (uint8_t lCol
= 0; lCol
< SYM_LOGO_WIDTH
; lCol
++) {
4077 displayWriteChar(osdDisplayPort
, logo_x
+ lCol
, logoRow
, logo_c
++);
4086 uint8_t drawStats(uint8_t row
) {
4088 char string_buffer
[30];
4089 uint8_t statNameX
= (osdDisplayPort
->cols
- 22) / 2;
4090 uint8_t statValueX
= statNameX
+ 21;
4092 if (statsConfig()->stats_enabled
) {
4093 displayWrite(osdDisplayPort
, statNameX
, row
, "ODOMETER:");
4094 switch (osdConfig()->units
) {
4097 case OSD_UNIT_IMPERIAL
:
4098 tfp_sprintf(string_buffer
, "%5d", (int)(statsConfig()->stats_total_dist
/ METERS_PER_MILE
));
4099 string_buffer
[5] = SYM_MI
;
4103 tfp_sprintf(string_buffer
, "%5d", (int)(statsConfig()->stats_total_dist
/ METERS_PER_NAUTICALMILE
));
4104 string_buffer
[5] = SYM_NM
;
4106 case OSD_UNIT_METRIC_MPH
:
4108 case OSD_UNIT_METRIC
:
4109 tfp_sprintf(string_buffer
, "%5d", (int)(statsConfig()->stats_total_dist
/ METERS_PER_KILOMETER
));
4110 string_buffer
[5] = SYM_KM
;
4113 string_buffer
[6] = '\0';
4114 displayWrite(osdDisplayPort
, statValueX
-5, row
, string_buffer
);
4116 displayWrite(osdDisplayPort
, statNameX
, ++row
, "TOTAL TIME:");
4117 uint32_t tot_mins
= statsConfig()->stats_total_time
/ 60;
4118 tfp_sprintf(string_buffer
, "%2d:%02dH:M", (int)(tot_mins
/ 60), (int)(tot_mins
% 60));
4119 displayWrite(osdDisplayPort
, statValueX
-7, row
, string_buffer
);
4122 if (feature(FEATURE_VBAT
) && feature(FEATURE_CURRENT_METER
)) {
4123 displayWrite(osdDisplayPort
, statNameX
, ++row
, "TOTAL ENERGY:");
4124 osdFormatCentiNumber(string_buffer
, statsConfig()->stats_total_energy
/ 10, 0, 2, 0, 4, false);
4125 displayWrite(osdDisplayPort
, statValueX
-4, row
, string_buffer
);
4126 displayWriteChar(osdDisplayPort
, statValueX
, row
, SYM_WH
);
4128 displayWrite(osdDisplayPort
, statNameX
, ++row
, "AVG EFFICIENCY:");
4129 if (statsConfig()->stats_total_dist
) {
4130 uint32_t avg_efficiency
= statsConfig()->stats_total_energy
/ (statsConfig()->stats_total_dist
/ METERS_PER_KILOMETER
); // mWh/km
4131 switch (osdConfig()->units
) {
4134 case OSD_UNIT_IMPERIAL
:
4135 osdFormatCentiNumber(string_buffer
, avg_efficiency
/ 10, 0, 2, 0, 3, false);
4136 string_buffer
[3] = SYM_WH_MI
;
4139 osdFormatCentiNumber(string_buffer
, avg_efficiency
/ 10, 0, 2, 0, 3, false);
4140 string_buffer
[3] = SYM_WH_NM
;
4143 case OSD_UNIT_METRIC_MPH
:
4145 case OSD_UNIT_METRIC
:
4146 osdFormatCentiNumber(string_buffer
, avg_efficiency
/ 10000 * METERS_PER_MILE
, 0, 2, 0, 3, false);
4147 string_buffer
[3] = SYM_WH_KM
;
4151 string_buffer
[0] = string_buffer
[1] = string_buffer
[2] = '-';
4153 string_buffer
[4] = '\0';
4154 displayWrite(osdDisplayPort
, statValueX
-3, row
++, string_buffer
);
4162 static void osdSetNextRefreshIn(uint32_t timeMs
) {
4163 resumeRefreshAt
= micros() + timeMs
* 1000;
4164 refreshWaitForResumeCmdRelease
= true;
4167 static void osdCompleteAsyncInitialization(void)
4169 if (!displayIsReady(osdDisplayPort
)) {
4170 // Update the display.
4171 // XXX: Rename displayDrawScreen() and associated functions
4172 // to displayUpdate()
4173 displayDrawScreen(osdDisplayPort
);
4177 osdDisplayIsReady
= true;
4179 #if defined(USE_CANVAS)
4180 if (osdConfig()->force_grid
) {
4181 osdDisplayHasCanvas
= false;
4183 osdDisplayHasCanvas
= displayGetCanvas(&osdCanvas
, osdDisplayPort
);
4187 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
4188 displayClearScreen(osdDisplayPort
);
4191 displayFontMetadata_t metadata
;
4192 bool fontHasMetadata
= displayGetFontMetadata(&metadata
, osdDisplayPort
);
4193 LOG_DEBUG(OSD
, "Font metadata version %s: %u (%u chars)",
4194 fontHasMetadata
? "Y" : "N", metadata
.version
, metadata
.charCount
);
4196 if (fontHasMetadata
&& metadata
.charCount
> 256) {
4197 hasExtendedFont
= true;
4199 y
= drawLogos(false, y
);
4201 } else if (!fontHasMetadata
) {
4202 const char *m
= "INVALID FONT";
4203 displayWrite(osdDisplayPort
, OSD_CENTER_S(m
), y
++, m
);
4206 if (fontHasMetadata
&& metadata
.version
< OSD_MIN_FONT_VERSION
) {
4207 const char *m
= "INVALID FONT VERSION";
4208 displayWrite(osdDisplayPort
, OSD_CENTER_S(m
), y
++, m
);
4211 char string_buffer
[30];
4212 tfp_sprintf(string_buffer
, "INAV VERSION: %s", FC_VERSION_STRING
);
4213 uint8_t xPos
= (osdDisplayPort
->cols
- 19) / 2; // Automatically centre, regardless of resolution. In the case of odd number screens, bias to the left.
4214 displayWrite(osdDisplayPort
, xPos
, y
++, string_buffer
);
4216 displayWrite(osdDisplayPort
, xPos
+2, y
++, CMS_STARTUP_HELP_TEXT1
);
4217 displayWrite(osdDisplayPort
, xPos
+6, y
++, CMS_STARTUP_HELP_TEXT2
);
4218 displayWrite(osdDisplayPort
, xPos
+6, y
++, CMS_STARTUP_HELP_TEXT3
);
4225 displayCommitTransaction(osdDisplayPort
);
4226 displayResync(osdDisplayPort
);
4227 osdSetNextRefreshIn(SPLASH_SCREEN_DISPLAY_TIME
);
4230 void osdInit(displayPort_t
*osdDisplayPortToUse
)
4232 if (!osdDisplayPortToUse
)
4235 BUILD_BUG_ON(OSD_POS_MAX
!= OSD_POS(63,63));
4237 osdDisplayPort
= osdDisplayPortToUse
;
4240 cmsDisplayPortRegister(osdDisplayPort
);
4243 armState
= ARMING_FLAG(ARMED
);
4244 osdCompleteAsyncInitialization();
4247 static void osdResetStats(void)
4249 stats
.max_current
= 0;
4250 stats
.max_power
= 0;
4251 stats
.max_speed
= 0;
4252 stats
.max_3D_speed
= 0;
4253 stats
.max_air_speed
= 0;
4254 stats
.min_voltage
= 5000;
4255 stats
.min_rssi
= 99;
4257 stats
.min_rssi_dbm
= 0;
4258 stats
.max_altitude
= 0;
4261 static void osdUpdateStats(void)
4265 if (feature(FEATURE_GPS
)) {
4266 value
= osdGet3DSpeed();
4267 const float airspeed_estimate
= getAirspeedEstimate();
4269 if (stats
.max_3D_speed
< value
)
4270 stats
.max_3D_speed
= value
;
4272 if (stats
.max_speed
< gpsSol
.groundSpeed
)
4273 stats
.max_speed
= gpsSol
.groundSpeed
;
4275 if (stats
.max_air_speed
< airspeed_estimate
)
4276 stats
.max_air_speed
= airspeed_estimate
;
4278 if (stats
.max_distance
< GPS_distanceToHome
)
4279 stats
.max_distance
= GPS_distanceToHome
;
4282 value
= getBatteryVoltage();
4283 if (stats
.min_voltage
> value
)
4284 stats
.min_voltage
= value
;
4286 value
= abs(getAmperage());
4287 if (stats
.max_current
< value
)
4288 stats
.max_current
= value
;
4290 value
= labs(getPower());
4291 if (stats
.max_power
< value
)
4292 stats
.max_power
= value
;
4294 value
= osdConvertRSSI();
4295 if (stats
.min_rssi
> value
)
4296 stats
.min_rssi
= value
;
4298 value
= osdGetCrsfLQ();
4299 if (stats
.min_lq
> value
)
4300 stats
.min_lq
= value
;
4302 if (!failsafeIsReceivingRxData())
4305 value
= osdGetCrsfdBm();
4306 if (stats
.min_rssi_dbm
> value
)
4307 stats
.min_rssi_dbm
= value
;
4309 stats
.max_altitude
= MAX(stats
.max_altitude
, osdGetAltitude());
4312 static void osdShowStats(bool isSinglePageStatsCompatible
, uint8_t page
)
4314 const char * disarmReasonStr
[DISARM_REASON_COUNT
] = { "UNKNOWN", "TIMEOUT", "STICKS", "SWITCH", "SWITCH", "KILLSW", "FAILSAFE", "NAV SYS", "LANDING"};
4315 uint8_t top
= 1; // Start one line down leaving space at the top of the screen.
4316 size_t multiValueLengthOffset
= 0;
4318 const uint8_t statNameX
= osdDisplayIsHD() ? 11 : 1;
4319 const uint8_t statValuesX
= osdDisplayIsHD() ? 30 : 20;
4325 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
4326 displayClearScreen(osdDisplayPort
);
4328 if (isSinglePageStatsCompatible
) {
4329 displayWrite(osdDisplayPort
, statNameX
, top
++, "--- STATS ---");
4330 } else if (page
== 0) {
4331 displayWrite(osdDisplayPort
, statNameX
, top
++, "--- STATS --- 1/2 ->");
4332 } else if (page
== 1) {
4333 displayWrite(osdDisplayPort
, statNameX
, top
++, "--- STATS --- <- 2/2");
4336 if (isSinglePageStatsCompatible
|| page
== 0) {
4337 if (feature(FEATURE_GPS
)) {
4338 if (isSinglePageStatsCompatible
) {
4339 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX/AVG SPEED :");
4340 osdFormatVelocityStr(buff
, stats
.max_3D_speed
, true, false);
4341 osdLeftAlignString(buff
);
4342 strcat(osdFormatTrimWhiteSpace(buff
),"/");
4343 multiValueLengthOffset
= strlen(buff
);
4344 displayWrite(osdDisplayPort
, statValuesX
, top
, buff
);
4345 osdGenerateAverageVelocityStr(buff
);
4346 osdLeftAlignString(buff
);
4347 displayWrite(osdDisplayPort
, statValuesX
+ multiValueLengthOffset
, top
++, buff
);
4349 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX SPEED :");
4350 osdFormatVelocityStr(buff
, stats
.max_3D_speed
, true, false);
4351 osdLeftAlignString(buff
);
4352 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4354 displayWrite(osdDisplayPort
, statNameX
, top
, "AVG SPEED :");
4355 osdGenerateAverageVelocityStr(buff
);
4356 osdLeftAlignString(buff
);
4357 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4360 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX DISTANCE :");
4361 osdFormatDistanceStr(buff
, stats
.max_distance
*100);
4362 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4364 displayWrite(osdDisplayPort
, statNameX
, top
, "TRAVELED DISTANCE:");
4365 osdFormatDistanceStr(buff
, getTotalTravelDistance());
4366 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4369 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX ALTITUDE :");
4370 osdFormatAltitudeStr(buff
, stats
.max_altitude
);
4371 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4373 switch (rxConfig()->serialrx_provider
) {
4375 if (isSinglePageStatsCompatible
) {
4376 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN RSSI %/DBM :");
4377 itoa(stats
.min_rssi
, buff
, 10);
4378 osdLeftAlignString(buff
);
4379 strcat(osdFormatTrimWhiteSpace(buff
), "%/");
4380 multiValueLengthOffset
= strlen(buff
);
4381 displayWrite(osdDisplayPort
, statValuesX
, top
, buff
);
4382 itoa(stats
.min_rssi_dbm
, buff
, 10);
4383 tfp_sprintf(buff
, "%s%c", buff
, SYM_DBM
);
4384 osdLeftAlignString(buff
);
4385 displayWrite(osdDisplayPort
, statValuesX
+ multiValueLengthOffset
, top
++, buff
);
4387 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN RSSI % :");
4388 itoa(stats
.min_rssi
, buff
, 10);
4390 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4392 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN RSSI DBM :");
4393 itoa(stats
.min_rssi_dbm
, buff
, 10);
4394 tfp_sprintf(buff
, "%s%c", buff
, SYM_DBM
);
4395 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4398 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN LQ :");
4399 itoa(stats
.min_lq
, buff
, 10);
4401 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4404 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN RSSI :");
4405 itoa(stats
.min_rssi
, buff
, 10);
4407 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4410 displayWrite(osdDisplayPort
, statNameX
, top
, "FLY TIME :");
4411 uint16_t flySeconds
= getFlightTime();
4412 uint16_t flyMinutes
= flySeconds
/ 60;
4414 uint16_t flyHours
= flyMinutes
/ 60;
4416 tfp_sprintf(buff
, "%02u:%02u:%02u", flyHours
, flyMinutes
, flySeconds
);
4417 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4419 displayWrite(osdDisplayPort
, statNameX
, top
, "DISARMED BY :");
4420 displayWrite(osdDisplayPort
, statValuesX
, top
++, disarmReasonStr
[getDisarmReason()]);
4423 if (isSinglePageStatsCompatible
|| page
== 1) {
4424 if (osdConfig()->stats_min_voltage_unit
== OSD_STATS_MIN_VOLTAGE_UNIT_BATTERY
) {
4425 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN BATTERY VOLT :");
4426 osdFormatCentiNumber(buff
, stats
.min_voltage
, 0, osdConfig()->main_voltage_decimals
, 0, osdConfig()->main_voltage_decimals
+ 2, false);
4428 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN CELL VOLTAGE :");
4429 osdFormatCentiNumber(buff
, stats
.min_voltage
/getBatteryCellCount(), 0, 2, 0, 3, false);
4431 tfp_sprintf(buff
, "%s%c", buff
, SYM_VOLT
);
4432 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4434 if (feature(FEATURE_CURRENT_METER
)) {
4435 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX CURRENT :");
4436 osdFormatCentiNumber(buff
, stats
.max_current
, 0, 2, 0, 3, false);
4437 tfp_sprintf(buff
, "%s%c", buff
, SYM_AMP
);
4438 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4440 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX POWER :");
4441 bool kiloWatt
= osdFormatCentiNumber(buff
, stats
.max_power
, 1000, 2, 2, 3, false);
4442 buff
[3] = kiloWatt
? SYM_KILOWATT
: SYM_WATT
;
4444 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4446 displayWrite(osdDisplayPort
, statNameX
, top
, "USED CAPACITY :");
4447 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
) {
4448 tfp_sprintf(buff
, "%d%c", (int)getMAhDrawn(), SYM_MAH
);
4450 osdFormatCentiNumber(buff
, getMWhDrawn() / 10, 0, 2, 0, 3, false);
4451 tfp_sprintf(buff
, "%s%c", buff
, SYM_WH
);
4453 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4455 int32_t totalDistance
= getTotalTravelDistance();
4456 bool moreThanAh
= false;
4457 bool efficiencyValid
= totalDistance
>= 10000;
4458 if (feature(FEATURE_GPS
)) {
4459 displayWrite(osdDisplayPort
, statNameX
, top
, "AVG EFFICIENCY :");
4460 uint8_t digits
= 3U; // Total number of digits (including decimal point)
4461 #ifndef DISABLE_MSP_BF_COMPAT // IF BFCOMPAT is not supported, there's no need to check for it and change the values
4462 if (isBfCompatibleVideoSystem(osdConfig())) {
4463 // Add one digit so no switch to scaled decimal occurs above 99
4467 switch (osdConfig()->units
) {
4470 case OSD_UNIT_IMPERIAL
:
4471 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
) {
4472 moreThanAh
= osdFormatCentiNumber(buff
, (int32_t)(getMAhDrawn() * 10000.0f
* METERS_PER_MILE
/ totalDistance
), 1000, 0, 2, digits
, false);
4474 tfp_sprintf(buff
, "%s%c%c", buff
, SYM_MAH_MI_0
, SYM_MAH_MI_1
);
4476 tfp_sprintf(buff
, "%s%c", buff
, SYM_AH_MI
);
4478 if (!efficiencyValid
) {
4479 buff
[0] = buff
[1] = buff
[2] = '-';
4480 buff
[3] = SYM_MAH_MI_0
;
4481 buff
[4] = SYM_MAH_MI_1
;
4485 osdFormatCentiNumber(buff
, (int32_t)(getMWhDrawn() * 10.0f
* METERS_PER_MILE
/ totalDistance
), 0, 2, 0, digits
, false);
4486 tfp_sprintf(buff
, "%s%c", buff
, SYM_WH_MI
);
4487 if (!efficiencyValid
) {
4488 buff
[0] = buff
[1] = buff
[2] = '-';
4493 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
) {
4494 moreThanAh
= osdFormatCentiNumber(buff
, (int32_t)(getMAhDrawn() * 10000.0f
* METERS_PER_NAUTICALMILE
/ totalDistance
), 1000, 0, 2, digits
, false);
4496 tfp_sprintf(buff
, "%s%c%c", buff
, SYM_MAH_NM_0
, SYM_MAH_NM_1
);
4498 tfp_sprintf(buff
, "%s%c", buff
, SYM_AH_NM
);
4500 if (!efficiencyValid
) {
4501 buff
[0] = buff
[1] = buff
[2] = '-';
4502 buff
[3] = SYM_MAH_NM_0
;
4503 buff
[4] = SYM_MAH_NM_1
;
4507 osdFormatCentiNumber(buff
, (int32_t)(getMWhDrawn() * 10.0f
* METERS_PER_NAUTICALMILE
/ totalDistance
), 0, 2, 0, digits
, false);
4508 tfp_sprintf(buff
, "%s%c", buff
, SYM_WH_NM
);
4509 if (!efficiencyValid
) {
4510 buff
[0] = buff
[1] = buff
[2] = '-';
4514 case OSD_UNIT_METRIC_MPH
:
4516 case OSD_UNIT_METRIC
:
4517 if (osdConfig()->stats_energy_unit
== OSD_STATS_ENERGY_UNIT_MAH
) {
4518 moreThanAh
= osdFormatCentiNumber(buff
, (int32_t)(getMAhDrawn() * 10000000.0f
/ totalDistance
), 1000, 0, 2, digits
, false);
4520 tfp_sprintf(buff
, "%s%c%c", buff
, SYM_MAH_KM_0
, SYM_MAH_KM_1
);
4522 tfp_sprintf(buff
, "%s%c", buff
, SYM_AH_KM
);
4524 if (!efficiencyValid
) {
4525 buff
[0] = buff
[1] = buff
[2] = '-';
4526 buff
[3] = SYM_MAH_KM_0
;
4527 buff
[4] = SYM_MAH_KM_1
;
4531 osdFormatCentiNumber(buff
, (int32_t)(getMWhDrawn() * 10000.0f
/ totalDistance
), 0, 2, 0, digits
, false);
4532 tfp_sprintf(buff
, "%s%c", buff
, SYM_WH_KM
);
4533 if (!efficiencyValid
) {
4534 buff
[0] = buff
[1] = buff
[2] = '-';
4539 osdLeftAlignString(buff
);
4540 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4544 const float max_gforce
= accGetMeasuredMaxG();
4545 displayWrite(osdDisplayPort
, statNameX
, top
, "MAX G-FORCE :");
4546 osdFormatCentiNumber(buff
, max_gforce
* 100, 0, 2, 0, 3, false);
4547 displayWrite(osdDisplayPort
, statValuesX
, top
++, buff
);
4549 const acc_extremes_t
*acc_extremes
= accGetMeasuredExtremes();
4550 const float acc_extremes_min
= acc_extremes
[Z
].min
;
4551 const float acc_extremes_max
= acc_extremes
[Z
].max
;
4552 displayWrite(osdDisplayPort
, statNameX
, top
, "MIN/MAX Z G-FORCE:");
4553 osdFormatCentiNumber(buff
, acc_extremes_min
* 100, 0, 2, 0, 4, false);
4554 osdLeftAlignString(buff
);
4555 strcat(osdFormatTrimWhiteSpace(buff
),"/");
4556 multiValueLengthOffset
= strlen(buff
);
4557 displayWrite(osdDisplayPort
, statValuesX
, top
, buff
);
4558 osdFormatCentiNumber(buff
, acc_extremes_max
* 100, 0, 2, 0, 3, false);
4559 osdLeftAlignString(buff
);
4560 displayWrite(osdDisplayPort
, statValuesX
+ multiValueLengthOffset
, top
++, buff
);
4563 if (savingSettings
== true) {
4564 displayWrite(osdDisplayPort
, statNameX
, top
++, OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS
));
4565 } else if (notify_settings_saved
> 0) {
4566 if (millis() > notify_settings_saved
) {
4567 notify_settings_saved
= 0;
4569 displayWrite(osdDisplayPort
, statNameX
, top
++, OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED
));
4573 displayCommitTransaction(osdDisplayPort
);
4576 // HD arming screen. based on the minimum HD OSD grid size of 50 x 18
4577 static void osdShowHDArmScreen(void)
4580 char buf
[MAX(osdDisplayPort
->cols
, FORMATTED_DATE_TIME_BUFSIZE
)];
4581 char buf2
[MAX(osdDisplayPort
->cols
, FORMATTED_DATE_TIME_BUFSIZE
)];
4582 char craftNameBuf
[MAX_NAME_LENGTH
];
4583 char versionBuf
[osdDisplayPort
->cols
];
4584 uint8_t safehomeRow
= 0;
4585 uint8_t armScreenRow
= 2; // Start at row 2
4587 armScreenRow
= drawLogos(false, armScreenRow
);
4590 if (strlen(systemConfig()->craftName
) > 0) {
4591 osdFormatCraftName(craftNameBuf
);
4592 strcpy(buf2
, "ARMED!");
4593 tfp_sprintf(buf
, "%s - %s", craftNameBuf
, buf2
);
4595 strcpy(buf
, "ARMED!");
4597 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4598 #if defined(USE_GPS)
4599 #if defined (USE_SAFE_HOME)
4600 if (posControl
.safehomeState
.distance
) {
4601 safehomeRow
= armScreenRow
;
4604 #endif // USE_SAFE_HOME
4608 if (posControl
.waypointListValid
&& posControl
.waypointCount
> 0) {
4609 #ifdef USE_MULTI_MISSION
4610 tfp_sprintf(buf
, "MISSION %u/%u (%u WP)", posControl
.loadedMultiMissionIndex
, posControl
.multiMissionCount
, posControl
.waypointCount
);
4611 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4613 strcpy(buf
, "*MISSION LOADED*");
4614 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4619 #if defined(USE_GPS)
4620 if (feature(FEATURE_GPS
)) {
4621 if (STATE(GPS_FIX_HOME
)) {
4622 if (osdConfig()->osd_home_position_arm_screen
) {
4623 osdFormatCoordinate(buf
, SYM_LAT
, GPS_home
.lat
);
4624 osdFormatCoordinate(buf2
, SYM_LON
, GPS_home
.lon
);
4626 uint8_t col
= strlen(buf
) + strlen(buf2
) + gap
;
4628 if ((osdDisplayPort
->cols
%2) != (col
%2)) {
4633 col
= (osdDisplayPort
->cols
- col
) / 2;
4635 displayWrite(osdDisplayPort
, col
, armScreenRow
, buf
);
4636 displayWrite(osdDisplayPort
, col
+ strlen(buf
) + gap
, armScreenRow
++, buf2
);
4638 int digits
= osdConfig()->plus_code_digits
;
4639 olc_encode(GPS_home
.lat
, GPS_home
.lon
, digits
, buf
, sizeof(buf
));
4640 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4643 #if defined (USE_SAFE_HOME)
4644 if (posControl
.safehomeState
.distance
) { // safehome found during arming
4645 if (navConfig()->general
.flags
.safehome_usage_mode
== SAFEHOME_USAGE_OFF
) {
4646 strcpy(buf
, "SAFEHOME FOUND; MODE OFF");
4648 osdFormatDistanceStr(buf2
, posControl
.safehomeState
.distance
);
4649 tfp_sprintf(buf
, "%c SAFEHOME %u @ %s", SYM_HOME
, posControl
.safehomeState
.index
, buf2
);
4651 textAttributes_t elemAttr
= _TEXT_ATTRIBUTES_BLINK_BIT
;
4652 // write this message below the ARMED message to make it obvious
4653 displayWriteWithAttr(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, safehomeRow
, buf
, elemAttr
);
4657 strcpy(buf
, "!NO HOME POSITION!");
4658 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4664 if (rtcGetDateTimeLocal(&dt
)) {
4665 tfp_sprintf(buf
, "%04u-%02u-%02u %02u:%02u:%02u", dt
.year
, dt
.month
, dt
.day
, dt
.hours
, dt
.minutes
, dt
.seconds
);
4666 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4670 tfp_sprintf(versionBuf
, "INAV VERSION: %s", FC_VERSION_STRING
);
4671 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(versionBuf
)) / 2, armScreenRow
++, versionBuf
);
4675 if (armScreenRow
< (osdDisplayPort
->rows
- 4))
4676 armScreenRow
= drawStats(armScreenRow
);
4680 static void osdShowSDArmScreen(void)
4683 char buf
[MAX(osdDisplayPort
->cols
, FORMATTED_DATE_TIME_BUFSIZE
)];
4684 char buf2
[MAX(osdDisplayPort
->cols
, FORMATTED_DATE_TIME_BUFSIZE
)];
4685 char craftNameBuf
[MAX_NAME_LENGTH
];
4686 char versionBuf
[osdDisplayPort
->cols
];
4687 // We need 12 visible rows, start row never < first fully visible row 1
4688 uint8_t armScreenRow
= osdDisplayPort
->rows
> 13 ? (osdDisplayPort
->rows
- 12) / 2 : 1;
4689 uint8_t safehomeRow
= 0;
4691 strcpy(buf
, "ARMED!");
4692 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4693 safehomeRow
= armScreenRow
;
4696 if (strlen(systemConfig()->craftName
) > 0) {
4697 osdFormatCraftName(craftNameBuf
);
4698 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(systemConfig()->craftName
)) / 2, armScreenRow
++, craftNameBuf
);
4701 if (posControl
.waypointListValid
&& posControl
.waypointCount
> 0) {
4702 #ifdef USE_MULTI_MISSION
4703 tfp_sprintf(buf
, "MISSION %u/%u (%u WP)", posControl
.loadedMultiMissionIndex
, posControl
.multiMissionCount
, posControl
.waypointCount
);
4704 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
, buf
);
4706 strcpy(buf
, "*MISSION LOADED*");
4707 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
, buf
);
4712 #if defined(USE_GPS)
4713 if (feature(FEATURE_GPS
)) {
4714 if (STATE(GPS_FIX_HOME
)) {
4715 if (osdConfig()->osd_home_position_arm_screen
) {
4716 osdFormatCoordinate(buf
, SYM_LAT
, GPS_home
.lat
);
4717 osdFormatCoordinate(buf2
, SYM_LON
, GPS_home
.lon
);
4719 uint8_t gpsStartCol
= (osdDisplayPort
->cols
- (strlen(buf
) + strlen(buf2
) + 2)) / 2;
4720 displayWrite(osdDisplayPort
, gpsStartCol
, armScreenRow
, buf
);
4721 displayWrite(osdDisplayPort
, gpsStartCol
+ strlen(buf
) + 2, armScreenRow
++, buf2
);
4723 int digits
= osdConfig()->plus_code_digits
;
4724 olc_encode(GPS_home
.lat
, GPS_home
.lon
, digits
, buf
, sizeof(buf
));
4725 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4728 #if defined (USE_SAFE_HOME)
4729 if (posControl
.safehomeState
.distance
) { // safehome found during arming
4730 if (navConfig()->general
.flags
.safehome_usage_mode
== SAFEHOME_USAGE_OFF
) {
4731 strcpy(buf
, "SAFEHOME FOUND; MODE OFF");
4733 osdFormatDistanceStr(buf2
, posControl
.safehomeState
.distance
);
4734 tfp_sprintf(buf
, "%c SAFEHOME %u @ %s", SYM_HOME
, posControl
.safehomeState
.index
, buf2
);
4736 textAttributes_t elemAttr
= _TEXT_ATTRIBUTES_BLINK_BIT
;
4737 // write this message below the ARMED message to make it obvious
4738 displayWriteWithAttr(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, safehomeRow
, buf
, elemAttr
);
4742 strcpy(buf
, "!NO HOME POSITION!");
4743 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4749 if (rtcGetDateTimeLocal(&dt
)) {
4750 tfp_sprintf(buf
, "%04u-%02u-%02u %02u:%02u:%02u", dt
.year
, dt
.month
, dt
.day
, dt
.hours
, dt
.minutes
, dt
.seconds
);
4751 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(buf
)) / 2, armScreenRow
++, buf
);
4755 tfp_sprintf(versionBuf
, "INAV VERSION: %s", FC_VERSION_STRING
);
4756 displayWrite(osdDisplayPort
, (osdDisplayPort
->cols
- strlen(versionBuf
)) / 2, armScreenRow
++, versionBuf
);
4760 if (armScreenRow
< (osdDisplayPort
->rows
- 4))
4761 armScreenRow
= drawStats(armScreenRow
);
4765 // called when motors armed
4766 static void osdShowArmed(void)
4768 displayClearScreen(osdDisplayPort
);
4770 if (osdDisplayIsHD()) {
4771 osdShowHDArmScreen();
4773 osdShowSDArmScreen();
4777 static void osdFilterData(timeUs_t currentTimeUs
) {
4778 static timeUs_t lastRefresh
= 0;
4779 float refresh_dT
= US2S(cmpTimeUs(currentTimeUs
, lastRefresh
));
4781 GForce
= fast_fsqrtf(vectorNormSquared(&imuMeasuredAccelBF
)) / GRAVITY_MSS
;
4782 for (uint8_t axis
= 0; axis
< XYZ_AXIS_COUNT
; ++axis
) GForceAxis
[axis
] = imuMeasuredAccelBF
.v
[axis
] / GRAVITY_MSS
;
4785 GForce
= pt1FilterApply3(&GForceFilter
, GForce
, refresh_dT
);
4786 for (uint8_t axis
= 0; axis
< XYZ_AXIS_COUNT
; ++axis
) pt1FilterApply3(GForceFilterAxis
+ axis
, GForceAxis
[axis
], refresh_dT
);
4788 pt1FilterInitRC(&GForceFilter
, GFORCE_FILTER_TC
, 0);
4789 pt1FilterReset(&GForceFilter
, GForce
);
4791 for (uint8_t axis
= 0; axis
< XYZ_AXIS_COUNT
; ++axis
) {
4792 pt1FilterInitRC(GForceFilterAxis
+ axis
, GFORCE_FILTER_TC
, 0);
4793 pt1FilterReset(GForceFilterAxis
+ axis
, GForceAxis
[axis
]);
4797 lastRefresh
= currentTimeUs
;
4800 // Detect when the user is holding the roll stick to the right
4801 static bool osdIsPageUpStickCommandHeld(void)
4803 static int pageUpHoldCount
= 1;
4805 bool keyHeld
= false;
4812 pageUpHoldCount
= 1;
4817 if (pageUpHoldCount
> 20) {
4818 pageUpHoldCount
= 1;
4825 // Detect when the user is holding the roll stick to the left
4826 static bool osdIsPageDownStickCommandHeld(void)
4828 static int pageDownHoldCount
= 1;
4830 bool keyHeld
= false;
4836 pageDownHoldCount
= 1;
4838 ++pageDownHoldCount
;
4841 if (pageDownHoldCount
> 20) {
4842 pageDownHoldCount
= 1;
4849 static void osdRefresh(timeUs_t currentTimeUs
)
4851 osdFilterData(currentTimeUs
);
4854 if (IS_RC_MODE_ACTIVE(BOXOSD
) && (!cmsInMenu
) && !(osdConfig()->osd_failsafe_switch_layout
&& FLIGHT_MODE(FAILSAFE_MODE
))) {
4856 if (IS_RC_MODE_ACTIVE(BOXOSD
) && !(osdConfig()->osd_failsafe_switch_layout
&& FLIGHT_MODE(FAILSAFE_MODE
))) {
4858 displayClearScreen(osdDisplayPort
);
4859 armState
= ARMING_FLAG(ARMED
);
4863 bool statsSinglePageCompatible
= (osdDisplayPort
->rows
>= OSD_STATS_SINGLE_PAGE_MIN_ROWS
);
4864 static uint8_t statsCurrentPage
= 0;
4865 static bool statsDisplayed
= false;
4866 static bool statsAutoPagingEnabled
= true;
4868 // Detect arm/disarm
4869 if (armState
!= ARMING_FLAG(ARMED
)) {
4870 if (ARMING_FLAG(ARMED
)) {
4871 // Display the "Arming" screen
4872 statsDisplayed
= false;
4875 uint16_t delay
= osdConfig()->arm_screen_display_time
;
4876 if (STATE(IN_FLIGHT_EMERG_REARM
)) {
4879 #if defined(USE_SAFE_HOME)
4880 else if (posControl
.safehomeState
.distance
) {
4884 osdSetNextRefreshIn(delay
);
4886 // Display the "Stats" screen
4887 statsDisplayed
= true;
4888 statsCurrentPage
= 0;
4889 statsAutoPagingEnabled
= osdConfig()->stats_page_auto_swap_time
> 0 ? true : false;
4890 osdShowStats(statsSinglePageCompatible
, statsCurrentPage
);
4891 osdSetNextRefreshIn(STATS_SCREEN_DISPLAY_TIME
);
4894 armState
= ARMING_FLAG(ARMED
);
4897 // This block is entered when we're showing the "Splash", "Armed" or "Stats" screens
4898 if (resumeRefreshAt
) {
4900 // Handle events only when the "Stats" screen is being displayed.
4901 if (statsDisplayed
) {
4903 // Manual paging stick commands are only applicable to multi-page stats.
4904 // ******************************
4905 // For single-page stats, this effectively disables the ability to cancel the
4906 // automatic paging/updates with the stick commands. So unless stats_page_auto_swap_time
4907 // is set to 0 or greater than 4 (saved settings display interval is 5 seconds), then
4908 // "Saved Settings" should display if it is active within the refresh interval.
4909 // ******************************
4910 // With multi-page stats, "Saved Settings" could also be missed if the user
4911 // has canceled automatic paging using the stick commands, because that is only
4912 // updated when osdShowStats() is called. So, in that case, they would only see
4913 // the "Saved Settings" message if they happen to manually change pages using the
4914 // stick commands within the interval the message is displayed.
4915 bool manualPageUpRequested
= false;
4916 bool manualPageDownRequested
= false;
4917 if (!statsSinglePageCompatible
) {
4918 // These methods ensure the paging stick commands are held for a brief period
4919 // Otherwise it can result in a race condition where the stats are
4920 // updated too quickly and can result in partial blanks, etc.
4921 if (osdIsPageUpStickCommandHeld()) {
4922 manualPageUpRequested
= true;
4923 statsAutoPagingEnabled
= false;
4924 } else if (osdIsPageDownStickCommandHeld()) {
4925 manualPageDownRequested
= true;
4926 statsAutoPagingEnabled
= false;
4930 if (statsAutoPagingEnabled
) {
4931 // Alternate screens for multi-page stats.
4932 // Also, refreshes screen at swap interval for single-page stats.
4933 if (OSD_ALTERNATING_CHOICES((osdConfig()->stats_page_auto_swap_time
* 1000), 2)) {
4934 if (statsCurrentPage
== 0) {
4935 osdShowStats(statsSinglePageCompatible
, statsCurrentPage
);
4936 statsCurrentPage
= 1;
4939 if (statsCurrentPage
== 1) {
4940 osdShowStats(statsSinglePageCompatible
, statsCurrentPage
);
4941 statsCurrentPage
= 0;
4945 // Process manual page change events for multi-page stats.
4946 if (manualPageUpRequested
) {
4947 osdShowStats(statsSinglePageCompatible
, 1);
4948 statsCurrentPage
= 1;
4949 } else if (manualPageDownRequested
) {
4950 osdShowStats(statsSinglePageCompatible
, 0);
4951 statsCurrentPage
= 0;
4956 // Handle events when either "Splash", "Armed" or "Stats" screens are displayed.
4957 if ((currentTimeUs
> resumeRefreshAt
) || OSD_RESUME_UPDATES_STICK_COMMAND
) {
4958 // Time elapsed or canceled by stick commands.
4959 // Exit to normal OSD operation.
4960 displayClearScreen(osdDisplayPort
);
4961 resumeRefreshAt
= 0;
4962 statsDisplayed
= false;
4964 // Continue "Splash", "Armed" or "Stats" screens.
4965 displayHeartbeat(osdDisplayPort
);
4972 if (!displayIsGrabbed(osdDisplayPort
)) {
4973 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
4975 displayClearScreen(osdDisplayPort
);
4978 osdDrawNextElement();
4979 displayHeartbeat(osdDisplayPort
);
4980 displayCommitTransaction(osdDisplayPort
);
4981 #ifdef OSD_CALLS_CMS
4983 cmsUpdate(currentTimeUs
);
4990 * Called periodically by the scheduler
4992 void osdUpdate(timeUs_t currentTimeUs
)
4994 static uint32_t counter
= 0;
4996 // don't touch buffers if DMA transaction is in progress
4997 if (displayIsTransferInProgress(osdDisplayPort
)) {
5001 if (!osdDisplayIsReady
) {
5002 osdCompleteAsyncInitialization();
5006 #if defined(OSD_ALTERNATE_LAYOUT_COUNT) && OSD_ALTERNATE_LAYOUT_COUNT > 0
5007 // Check if the layout has changed. Higher numbered
5008 // boxes take priority.
5009 unsigned activeLayout
;
5010 if (layoutOverride
>= 0) {
5011 activeLayout
= layoutOverride
;
5012 // Check for timed override, it will go into effect on
5013 // the next OSD iteration
5014 if (layoutOverrideUntil
> 0 && millis() > layoutOverrideUntil
) {
5015 layoutOverrideUntil
= 0;
5016 layoutOverride
= -1;
5018 } else if (osdConfig()->osd_failsafe_switch_layout
&& FLIGHT_MODE(FAILSAFE_MODE
)) {
5021 #if OSD_ALTERNATE_LAYOUT_COUNT > 2
5022 if (IS_RC_MODE_ACTIVE(BOXOSDALT3
))
5026 #if OSD_ALTERNATE_LAYOUT_COUNT > 1
5027 if (IS_RC_MODE_ACTIVE(BOXOSDALT2
))
5031 if (IS_RC_MODE_ACTIVE(BOXOSDALT1
))
5034 #ifdef USE_PROGRAMMING_FRAMEWORK
5035 if (LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_OSD_LAYOUT
))
5036 activeLayout
= constrain(logicConditionValuesByType
[LOGIC_CONDITION_SET_OSD_LAYOUT
], 0, OSD_ALTERNATE_LAYOUT_COUNT
);
5041 if (currentLayout
!= activeLayout
) {
5042 currentLayout
= activeLayout
;
5043 osdStartFullRedraw();
5047 #define DRAW_FREQ_DENOM 4
5048 #define STATS_FREQ_DENOM 50
5051 if ((counter
% STATS_FREQ_DENOM
) == 0) {
5055 if ((counter
% DRAW_FREQ_DENOM
) == 0) {
5056 // redraw values in buffer
5057 osdRefresh(currentTimeUs
);
5059 // rest of time redraw screen
5060 displayDrawScreen(osdDisplayPort
);
5064 // do not allow ARM if we are in menu
5065 if (displayIsGrabbed(osdDisplayPort
)) {
5066 ENABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU
);
5068 DISABLE_ARMING_FLAG(ARMING_DISABLED_OSD_MENU
);
5073 void osdStartFullRedraw(void)
5078 void osdOverrideLayout(int layout
, timeMs_t duration
)
5080 layoutOverride
= constrain(layout
, -1, ARRAYLEN(osdLayoutsConfig()->item_pos
) - 1);
5081 if (layoutOverride
>= 0 && duration
> 0) {
5082 layoutOverrideUntil
= millis() + duration
;
5084 layoutOverrideUntil
= 0;
5088 int osdGetActiveLayout(bool *overridden
)
5091 *overridden
= layoutOverride
>= 0;
5093 return currentLayout
;
5096 bool osdItemIsFixed(osd_items_e item
)
5098 return item
== OSD_CROSSHAIRS
||
5099 item
== OSD_ARTIFICIAL_HORIZON
||
5100 item
== OSD_HORIZON_SIDEBARS
;
5103 displayPort_t
*osdGetDisplayPort(void)
5105 return osdDisplayPort
;
5108 displayCanvas_t
*osdGetDisplayPortCanvas(void)
5110 #if defined(USE_CANVAS)
5111 if (osdDisplayHasCanvas
) {
5118 timeMs_t
systemMessageCycleTime(unsigned messageCount
, const char **messages
){
5120 float factor
= 1.0f
;
5121 while (i
< messageCount
) {
5122 if ((float)strlen(messages
[i
]) / 15.0f
> factor
) {
5123 factor
= (float)strlen(messages
[i
]) / 15.0f
;
5127 return osdConfig()->system_msg_display_time
* factor
;
5130 textAttributes_t
osdGetSystemMessage(char *buff
, size_t buff_size
, bool isCenteredText
)
5132 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
5135 const char *message
= NULL
;
5136 char messageBuf
[MAX(SETTING_MAX_NAME_LENGTH
, OSD_MESSAGE_LENGTH
+1)];
5137 // We might have up to 5 messages to show.
5138 const char *messages
[5];
5139 unsigned messageCount
= 0;
5140 const char *failsafeInfoMessage
= NULL
;
5141 const char *invertedInfoMessage
= NULL
;
5143 if (ARMING_FLAG(ARMED
)) {
5144 #ifdef USE_FW_AUTOLAND
5145 if (FLIGHT_MODE(FAILSAFE_MODE
) || FLIGHT_MODE(NAV_RTH_MODE
) || FLIGHT_MODE(NAV_WP_MODE
) || navigationIsExecutingAnEmergencyLanding() || FLIGHT_MODE(NAV_FW_AUTOLAND
)) {
5146 if (isWaypointMissionRTHActive() && !posControl
.fwLandState
.landWp
) {
5148 if (FLIGHT_MODE(FAILSAFE_MODE
) || FLIGHT_MODE(NAV_RTH_MODE
) || FLIGHT_MODE(NAV_WP_MODE
) || navigationIsExecutingAnEmergencyLanding()) {
5149 if (isWaypointMissionRTHActive()) {
5151 // if RTH activated whilst WP mode selected, remind pilot to cancel WP mode to exit RTH
5152 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_WP_RTH_CANCEL
);
5154 if (navGetCurrentStateFlags() & NAV_AUTO_WP_DONE
) {
5155 messages
[messageCount
++] = STATE(LANDING_DETECTED
) ? OSD_MESSAGE_STR(OSD_MSG_WP_LANDED
) : OSD_MESSAGE_STR(OSD_MSG_WP_FINISHED
);
5156 } else if (NAV_Status
.state
== MW_NAV_STATE_WP_ENROUTE
) {
5157 // Countdown display for remaining Waypoints
5159 osdFormatDistanceSymbol(buf
, posControl
.wpDistance
, 0);
5160 tfp_sprintf(messageBuf
, "TO WP %u/%u (%s)", getGeoWaypointNumber(posControl
.activeWaypointIndex
), posControl
.geoWaypointCount
, buf
);
5161 messages
[messageCount
++] = messageBuf
;
5162 } else if (NAV_Status
.state
== MW_NAV_STATE_HOLD_TIMED
) {
5163 if (navConfig()->general
.waypoint_enforce_altitude
&& !posControl
.wpAltitudeReached
) {
5164 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ADJUSTING_WP_ALT
);
5166 // WP hold time countdown in seconds
5167 timeMs_t currentTime
= millis();
5168 int holdTimeRemaining
= posControl
.waypointList
[posControl
.activeWaypointIndex
].p1
- (int)(MS2S(currentTime
- posControl
.wpReachedTime
));
5169 holdTimeRemaining
= holdTimeRemaining
>= 0 ? holdTimeRemaining
: 0;
5171 tfp_sprintf(messageBuf
, "HOLDING WP FOR %2u S", holdTimeRemaining
);
5173 messages
[messageCount
++] = messageBuf
;
5175 } else if (NAV_Status
.state
== MW_NAV_STATE_LAND_SETTLE
&& posControl
.landingDelay
> 0) {
5176 uint16_t remainingHoldSec
= MS2S(posControl
.landingDelay
- millis());
5177 tfp_sprintf(messageBuf
, "LANDING DELAY: %3u SECONDS", remainingHoldSec
);
5179 messages
[messageCount
++] = messageBuf
;
5183 #ifdef USE_FW_AUTOLAND
5184 if (canFwLandingBeCancelled()) {
5185 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_MOVE_STICKS
);
5186 } else if (!FLIGHT_MODE(NAV_FW_AUTOLAND
)) {
5188 const char *navStateMessage
= navigationStateMessage();
5189 if (navStateMessage
) {
5190 messages
[messageCount
++] = navStateMessage
;
5192 #ifdef USE_FW_AUTOLAND
5197 #if defined(USE_SAFE_HOME)
5198 const char *safehomeMessage
= divertingToSafehomeMessage();
5199 if (safehomeMessage
) {
5200 messages
[messageCount
++] = safehomeMessage
;
5203 if (FLIGHT_MODE(FAILSAFE_MODE
)) {
5204 // In FS mode while being armed too
5205 const char *failsafePhaseMessage
= osdFailsafePhaseMessage();
5206 failsafeInfoMessage
= osdFailsafeInfoMessage();
5208 if (failsafePhaseMessage
) {
5209 messages
[messageCount
++] = failsafePhaseMessage
;
5211 if (failsafeInfoMessage
) {
5212 messages
[messageCount
++] = failsafeInfoMessage
;
5215 } else { /* messages shown only when Failsafe, WP, RTH or Emergency Landing not active */
5216 if (STATE(FIXED_WING_LEGACY
) && (navGetCurrentStateFlags() & NAV_CTL_LAUNCH
)) {
5217 messages
[messageCount
++] = navConfig()->fw
.launch_manual_throttle
? OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH_MANUAL
) :
5218 OSD_MESSAGE_STR(OSD_MSG_AUTOLAUNCH
);
5219 const char *launchStateMessage
= fixedWingLaunchStateMessage();
5220 if (launchStateMessage
) {
5221 messages
[messageCount
++] = launchStateMessage
;
5224 if (FLIGHT_MODE(NAV_ALTHOLD_MODE
) && !navigationRequiresAngleMode()) {
5225 // ALTHOLD might be enabled alongside ANGLE/HORIZON/ANGLEHOLD/ACRO
5226 // when it doesn't require ANGLE mode (required only in FW
5227 // right now). If it requires ANGLE, its display is handled by OSD_FLYMODE.
5228 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ALTITUDE_HOLD
);
5230 if (STATE(MULTIROTOR
) && FLIGHT_MODE(NAV_COURSE_HOLD_MODE
)) {
5231 if (posControl
.cruise
.multicopterSpeed
>= 50.0f
) {
5233 osdFormatVelocityStr(buf
, posControl
.cruise
.multicopterSpeed
, false, false);
5234 tfp_sprintf(messageBuf
, "(SPD %s)", buf
);
5236 strcpy(messageBuf
, "(HOLD)");
5238 messages
[messageCount
++] = messageBuf
;
5240 if (IS_RC_MODE_ACTIVE(BOXAUTOTRIM
) && !feature(FEATURE_FW_AUTOTRIM
)) {
5241 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTRIM
);
5243 if (IS_RC_MODE_ACTIVE(BOXAUTOTUNE
)) {
5244 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE
);
5245 if (FLIGHT_MODE(MANUAL_MODE
)) {
5246 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_AUTOTUNE_ACRO
);
5249 if (isFixedWingLevelTrimActive()) {
5250 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_AUTOLEVEL
);
5252 if (FLIGHT_MODE(HEADFREE_MODE
)) {
5253 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_HEADFREE
);
5255 if (FLIGHT_MODE(SOARING_MODE
)) {
5256 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_NAV_SOARING
);
5258 if (posControl
.flags
.wpMissionPlannerActive
) {
5259 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_MISSION_PLANNER
);
5261 if (STATE(LANDING_DETECTED
)) {
5262 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_LANDED
);
5264 if (IS_RC_MODE_ACTIVE(BOXANGLEHOLD
)) {
5265 int8_t navAngleHoldAxis
= navCheckActiveAngleHoldAxis();
5266 if (isAngleHoldLevel()) {
5267 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_LEVEL
);
5268 } else if (navAngleHoldAxis
== FD_ROLL
) {
5269 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_ROLL
);
5270 } else if (navAngleHoldAxis
== FD_PITCH
) {
5271 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_ANGLEHOLD_PITCH
);
5276 } else if (ARMING_FLAG(ARMING_DISABLED_ALL_FLAGS
)) {
5277 unsigned invalidIndex
;
5279 // Check if we're unable to arm for some reason
5280 if (ARMING_FLAG(ARMING_DISABLED_INVALID_SETTING
) && !settingsValidate(&invalidIndex
)) {
5282 const setting_t
*setting
= settingGet(invalidIndex
);
5283 settingGetName(setting
, messageBuf
);
5284 for (int ii
= 0; messageBuf
[ii
]; ii
++) {
5285 messageBuf
[ii
] = sl_toupper(messageBuf
[ii
]);
5287 invertedInfoMessage
= messageBuf
;
5288 messages
[messageCount
++] = invertedInfoMessage
;
5290 invertedInfoMessage
= OSD_MESSAGE_STR(OSD_MSG_INVALID_SETTING
);
5291 messages
[messageCount
++] = invertedInfoMessage
;
5295 invertedInfoMessage
= OSD_MESSAGE_STR(OSD_MSG_UNABLE_ARM
);
5296 messages
[messageCount
++] = invertedInfoMessage
;
5298 // Show the reason for not arming
5299 messages
[messageCount
++] = osdArmingDisabledReasonMessage();
5302 } else if (!ARMING_FLAG(ARMED
)) {
5303 if (isWaypointListValid()) {
5304 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_WP_MISSION_LOADED
);
5308 /* Messages that are shown regardless of Arming state */
5310 if (savingSettings
== true) {
5311 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_SAVING_SETTNGS
);
5312 } else if (notify_settings_saved
> 0) {
5313 if (millis() > notify_settings_saved
) {
5314 notify_settings_saved
= 0;
5316 messages
[messageCount
++] = OSD_MESSAGE_STR(OSD_MSG_SETTINGS_SAVED
);
5320 if (messageCount
> 0) {
5321 message
= messages
[OSD_ALTERNATING_CHOICES(systemMessageCycleTime(messageCount
, messages
), messageCount
)];
5322 if (message
== failsafeInfoMessage
) {
5323 // failsafeInfoMessage is not useful for recovering
5324 // a lost model, but might help avoiding a crash.
5325 // Blink to grab user attention.
5326 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
5327 } else if (message
== invertedInfoMessage
) {
5328 TEXT_ATTRIBUTES_ADD_INVERTED(elemAttr
);
5330 // We're shoing either failsafePhaseMessage or
5331 // navStateMessage. Don't BLINK here since
5332 // having this text available might be crucial
5333 // during a lost aircraft recovery and blinking
5334 // will cause it to be missing from some frames.
5337 osdFormatMessage(buff
, buff_size
, message
, isCenteredText
);
5342 void osdResetWarningFlags(void)
5344 osdWarningsFlags
= 0;
5347 static bool osdCheckWarning(bool condition
, uint8_t warningFlag
, uint8_t *warningsCount
)
5349 #define WARNING_REDISPLAY_DURATION 5000; // milliseconds
5351 const timeMs_t currentTimeMs
= millis();
5352 static timeMs_t warningDisplayStartTime
= 0;
5353 static timeMs_t redisplayStartTimeMs
= 0;
5354 static uint16_t osdWarningTimerDuration
;
5355 static uint8_t newWarningFlags
;
5357 if (condition
) { // condition required to trigger warning
5358 if (!(osdWarningsFlags
& warningFlag
)) {
5359 osdWarningsFlags
|= warningFlag
;
5360 newWarningFlags
|= warningFlag
;
5361 redisplayStartTimeMs
= 0;
5363 #ifdef USE_DEV_TOOLS
5364 if (systemConfig()->groundTestMode
) {
5368 /* Warnings displayed in full for set time before shrinking down to alert symbol with warning count only.
5369 * All current warnings then redisplayed for 5s on 30s rolling cycle.
5370 * New warnings dislayed individually for 10s */
5371 if (currentTimeMs
> redisplayStartTimeMs
) {
5372 warningDisplayStartTime
= currentTimeMs
;
5373 osdWarningTimerDuration
= newWarningFlags
? 10000 : WARNING_REDISPLAY_DURATION
;
5374 redisplayStartTimeMs
= currentTimeMs
+ osdWarningTimerDuration
+ 30000;
5377 if (currentTimeMs
- warningDisplayStartTime
< osdWarningTimerDuration
) {
5378 return (newWarningFlags
& warningFlag
) || osdWarningTimerDuration
== WARNING_REDISPLAY_DURATION
;
5380 newWarningFlags
= 0;
5382 *warningsCount
+= 1;
5383 } else if (osdWarningsFlags
& warningFlag
) {
5384 osdWarningsFlags
&= ~warningFlag
;
5390 static textAttributes_t
osdGetMultiFunctionMessage(char *buff
)
5392 /* Message length limit 10 char max */
5394 textAttributes_t elemAttr
= TEXT_ATTRIBUTES_NONE
;
5395 static uint8_t warningsCount
;
5396 const char *message
= NULL
;
5398 #ifdef USE_MULTI_FUNCTIONS
5399 /* --- FUNCTIONS --- */
5400 multi_function_e selectedFunction
= multiFunctionSelection();
5402 if (selectedFunction
) {
5403 multi_function_e activeFunction
= selectedFunction
;
5405 switch (selectedFunction
) {
5406 case MULTI_FUNC_NONE
:
5408 message
= warningsCount
? "WARNINGS !" : "0 WARNINGS";
5411 message
= posControl
.flags
.manualEmergLandActive
? "ABORT LAND" : "EMERG LAND";
5414 #if defined(USE_SAFE_HOME)
5415 if (navConfig()->general
.flags
.safehome_usage_mode
!= SAFEHOME_USAGE_OFF
) {
5416 message
= MULTI_FUNC_FLAG(MF_SUSPEND_SAFEHOMES
) ? "USE SFHOME" : "SUS SFHOME";
5423 if (navConfig()->general
.flags
.rth_trackback_mode
!= RTH_TRACKBACK_OFF
) {
5424 message
= MULTI_FUNC_FLAG(MF_SUSPEND_TRACKBACK
) ? "USE TKBACK" : "SUS TKBACK";
5431 if (STATE(MULTIROTOR
)) {
5432 message
= MULTI_FUNC_FLAG(MF_TURTLE_MODE
) ? "END TURTLE" : "USE TURTLE";
5439 message
= ARMING_FLAG(ARMED
) ? "NOW ARMED " : "EMERG ARM ";
5441 case MULTI_FUNC_END
:
5445 if (activeFunction
!= selectedFunction
) {
5446 setMultifunctionSelection(activeFunction
);
5449 strcpy(buff
, message
);
5451 if (isNextMultifunctionItemAvailable()) {
5452 // provides feedback indicating when a new selection command has been received by flight controller
5458 #endif // MULTIFUNCTION - functions only, warnings always defined
5460 /* --- WARNINGS --- */
5461 const char *messages
[7];
5462 uint8_t messageCount
= 0;
5463 bool warningCondition
= false;
5465 uint8_t warningFlagID
= 1;
5468 const batteryState_e batteryState
= getBatteryState();
5469 warningCondition
= batteryState
== BATTERY_CRITICAL
|| batteryState
== BATTERY_WARNING
;
5470 if (osdCheckWarning(warningCondition
, warningFlagID
, &warningsCount
)) {
5471 messages
[messageCount
++] = batteryState
== BATTERY_CRITICAL
? "BATT EMPTY" : "BATT LOW !";
5474 #if defined(USE_GPS)
5475 // GPS Fix and Failure
5476 if (feature(FEATURE_GPS
)) {
5477 if (osdCheckWarning(!STATE(GPS_FIX
), warningFlagID
<<= 1, &warningsCount
)) {
5478 bool gpsFailed
= getHwGPSStatus() == HW_SENSOR_UNAVAILABLE
;
5479 messages
[messageCount
++] = gpsFailed
? "GPS FAILED" : "NO GPS FIX";
5483 // RTH sanity (warning if RTH heads 200m further away from home than closest point)
5484 warningCondition
= NAV_Status
.state
== MW_NAV_STATE_RTH_ENROUTE
&& !posControl
.flags
.rthTrackbackActive
&&
5485 (posControl
.homeDistance
- posControl
.rthSanityChecker
.minimalDistanceToHome
) > 20000;
5486 if (osdCheckWarning(warningCondition
, warningFlagID
<<= 1, &warningsCount
)) {
5487 messages
[messageCount
++] = "RTH SANITY";
5490 // Altitude sanity (warning if significant mismatch between estimated and GPS altitude)
5491 if (osdCheckWarning(posControl
.flags
.gpsCfEstimatedAltitudeMismatch
, warningFlagID
<<= 1, &warningsCount
)) {
5492 messages
[messageCount
++] = "ALT SANITY";
5496 #if defined(USE_MAG)
5497 // Magnetometer failure
5498 if (requestedSensors
[SENSOR_INDEX_MAG
] != MAG_NONE
) {
5499 hardwareSensorStatus_e magStatus
= getHwCompassStatus();
5500 if (osdCheckWarning(magStatus
== HW_SENSOR_UNAVAILABLE
|| magStatus
== HW_SENSOR_UNHEALTHY
, warningFlagID
<<= 1, &warningsCount
)) {
5501 messages
[messageCount
++] = "MAG FAILED";
5505 // Vibration levels TODO - needs better vibration measurement to be useful
5506 // const float vibrationLevel = accGetVibrationLevel();
5507 // warningCondition = vibrationLevel > 1.5f;
5508 // if (osdCheckWarning(warningCondition, warningFlagID <<= 1, &warningsCount)) {
5509 // messages[messageCount++] = vibrationLevel > 2.5f ? "BAD VIBRTN" : "VIBRATION!";
5512 #ifdef USE_DEV_TOOLS
5513 if (osdCheckWarning(systemConfig()->groundTestMode
, warningFlagID
<<= 1, &warningsCount
)) {
5514 messages
[messageCount
++] = "GRD TEST !";
5519 message
= messages
[OSD_ALTERNATING_CHOICES(1000, messageCount
)]; // display each warning on 1s cycle
5520 strcpy(buff
, message
);
5521 TEXT_ATTRIBUTES_ADD_BLINK(elemAttr
);
5522 } else if (warningsCount
) {
5523 buff
[0] = SYM_ALERT
;
5524 tfp_sprintf(buff
+ 1, "%u ", warningsCount
);