2 * This file is part of Cleanflight and Betaflight.
4 * Cleanflight and Betaflight are free software. You can redistribute
5 * this software and/or modify this software under the terms of the
6 * GNU General Public License as published by the Free Software
7 * Foundation, either version 3 of the License, or (at your option)
10 * Cleanflight and Betaflight are distributed in the hope that they
11 * will be useful, but WITHOUT ANY WARRANTY; without even the implied
12 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 * See the GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this software.
18 * If not, see <http://www.gnu.org/licenses/>.
22 Created by Marcin Baliniak
23 some functions based on MinimOSD
25 OSD-CMS separation by jflyper
39 #include "blackbox/blackbox.h"
40 #include "blackbox/blackbox_io.h"
42 #include "build/build_config.h"
43 #include "build/version.h"
47 #include "common/axis.h"
48 #include "common/maths.h"
49 #include "common/printf.h"
50 #include "common/typeconversion.h"
51 #include "common/utils.h"
52 #include "common/unit.h"
54 #include "config/feature.h"
56 #include "drivers/display.h"
57 #include "drivers/dshot.h"
58 #include "drivers/flash.h"
59 #include "drivers/osd_symbols.h"
60 #include "drivers/sdcard.h"
61 #include "drivers/time.h"
64 #include "fc/gps_lap_timer.h"
65 #include "fc/rc_controls.h"
66 #include "fc/rc_modes.h"
67 #include "fc/runtime_config.h"
69 #if defined(USE_DYN_NOTCH_FILTER)
70 #include "flight/dyn_notch_filter.h"
72 #include "flight/failsafe.h"
73 #include "flight/imu.h"
74 #include "flight/mixer.h"
75 #include "flight/position.h"
77 #include "io/asyncfatfs/asyncfatfs.h"
78 #include "io/beeper.h"
79 #include "io/flashfs.h"
83 #include "osd/osd_elements.h"
84 #include "osd/osd_warnings.h"
88 #include "pg/pg_ids.h"
92 #include "rx/rc_stats.h"
95 #include "scheduler/scheduler.h"
97 #include "sensors/acceleration.h"
98 #include "sensors/battery.h"
99 #include "sensors/sensors.h"
101 #ifdef USE_HARDWARE_REVISION_DETECTION
102 #include "hardware_revision.h"
108 OSD_LOGO_ARMING_FIRST
109 } osd_logo_on_arming_e
;
111 const char * const osdTimerSourceNames
[] = {
118 #define OSD_LOGO_ROWS 4
119 #define OSD_LOGO_COLS 24
121 // Things in both OSD and CMS
123 #define IS_HI(X) (rcData[X] > 1750)
124 #define IS_LO(X) (rcData[X] < 1250)
125 #define IS_MID(X) (rcData[X] > 1250 && rcData[X] < 1750)
127 timeUs_t osdFlyTime
= 0;
131 uint16_t osdAuxValue
= 0;
133 static bool showVisualBeeper
= false;
135 static statistic_t stats
;
136 timeUs_t resumeRefreshAt
= 0;
137 #define REFRESH_1S 1000 * 1000
139 static uint8_t armState
;
140 #ifdef USE_OSD_PROFILES
141 static uint8_t osdProfile
= 1;
143 static displayPort_t
*osdDisplayPort
;
144 static osdDisplayPortDevice_e osdDisplayPortDeviceType
;
145 static bool osdIsReady
;
147 static bool suppressStatsDisplay
= false;
149 static bool backgroundLayerSupported
= false;
151 #ifdef USE_ESC_SENSOR
152 escSensorData_t
*osdEscDataCombined
;
155 STATIC_ASSERT(OSD_POS_MAX
== OSD_POS(63,31), OSD_POS_MAX_incorrect
);
157 PG_REGISTER_WITH_RESET_FN(osdConfig_t
, osdConfig
, PG_OSD_CONFIG
, 12);
159 PG_REGISTER_WITH_RESET_FN(osdElementConfig_t
, osdElementConfig
, PG_OSD_ELEMENT_CONFIG
, 1);
161 // Controls the display order of the OSD post-flight statistics.
162 // Adjust the ordering here to control how the post-flight stats are presented.
163 // Every entry in osd_stats_e should be represented. Any that are missing will not
164 // be shown on the the post-flight statistics page.
165 // If you reorder the stats it's likely that you'll need to make likewise updates
166 // to the unit tests.
168 // If adding new stats, please add to the osdStatsNeedAccelerometer() function
169 // if the statistic utilizes the accelerometer.
171 const osd_stats_e osdStatsDisplayOrder
[OSD_STAT_COUNT
] = {
172 OSD_STAT_RTC_DATE_TIME
,
175 OSD_STAT_MAX_ALTITUDE
,
177 OSD_STAT_MAX_DISTANCE
,
178 OSD_STAT_FLIGHT_DISTANCE
,
179 OSD_STAT_MIN_BATTERY
,
180 OSD_STAT_END_BATTERY
,
183 OSD_STAT_MAX_CURRENT
,
186 OSD_STAT_BLACKBOX_NUMBER
,
187 OSD_STAT_MAX_G_FORCE
,
188 OSD_STAT_MAX_ESC_TEMP
,
189 OSD_STAT_MAX_ESC_RPM
,
190 OSD_STAT_MIN_LINK_QUALITY
,
192 OSD_STAT_MIN_RSSI_DBM
,
194 OSD_STAT_TOTAL_FLIGHTS
,
197 OSD_STAT_WATT_HOURS_DRAWN
,
198 OSD_STAT_BEST_3_CONSEC_LAPS
,
200 OSD_STAT_FULL_THROTTLE_TIME
,
201 OSD_STAT_FULL_THROTTLE_COUNTER
,
202 OSD_STAT_AVG_THROTTLE
,
205 // Group elements in a number of groups to reduce task scheduling overhead
206 #define OSD_GROUP_COUNT OSD_ITEM_COUNT
207 // Aim to render a group of elements within a target time
208 #define OSD_ELEMENT_RENDER_TARGET 30
209 // Allow a margin by which a group render can exceed that of the sum of the elements before declaring insane
210 // This will most likely be violated by a USB interrupt whilst using the CLI
211 #if defined(STM32F411xE)
212 #define OSD_ELEMENT_RENDER_GROUP_MARGIN 7
214 #define OSD_ELEMENT_RENDER_GROUP_MARGIN 2
216 #define OSD_TASK_MARGIN 1
217 // Decay the estimated max task duration by 1/(1 << OSD_EXEC_TIME_SHIFT) on every invocation
218 #define OSD_EXEC_TIME_SHIFT 8
220 // Format a float to the specified number of decimal places with optional rounding.
221 // OSD symbols can optionally be placed before and after the formatted number (use SYM_NONE for no symbol).
222 // The formatString can be used for customized formatting of the integer part. Follow the printf style.
223 // Pass an empty formatString for default.
224 int osdPrintFloat(char *buffer
, char leadingSymbol
, float value
, char *formatString
, unsigned decimalPlaces
, bool round
, char trailingSymbol
)
229 for (unsigned i
= 0; i
< decimalPlaces
; i
++) {
234 const int scaledValueAbs
= abs(round
? (int)lrintf(value
) : (int)value
);
235 const int integerPart
= scaledValueAbs
/ multiplier
;
236 const int fractionalPart
= scaledValueAbs
% multiplier
;
238 if (leadingSymbol
!= SYM_NONE
) {
239 buffer
[pos
++] = leadingSymbol
;
241 if (value
< 0 && (integerPart
|| fractionalPart
)) {
245 pos
+= tfp_sprintf(buffer
+ pos
, (strlen(formatString
) ? formatString
: "%01u"), integerPart
);
247 tfp_sprintf((char *)&mask
, ".%%0%uu", decimalPlaces
); // builds up the format string to be like ".%03u" for decimalPlaces == 3 as an example
248 pos
+= tfp_sprintf(buffer
+ pos
, mask
, fractionalPart
);
251 if (trailingSymbol
!= SYM_NONE
) {
252 buffer
[pos
++] = trailingSymbol
;
259 void osdStatSetState(uint8_t statIndex
, bool enabled
)
262 osdConfigMutable()->enabled_stats
|= (1 << statIndex
);
264 osdConfigMutable()->enabled_stats
&= ~(1 << statIndex
);
268 bool osdStatGetState(uint8_t statIndex
)
270 return osdConfig()->enabled_stats
& (1 << statIndex
);
273 void osdWarnSetState(uint8_t warningIndex
, bool enabled
)
276 osdConfigMutable()->enabledWarnings
|= (1 << warningIndex
);
278 osdConfigMutable()->enabledWarnings
&= ~(1 << warningIndex
);
282 bool osdWarnGetState(uint8_t warningIndex
)
284 return osdConfig()->enabledWarnings
& (1 << warningIndex
);
287 #ifdef USE_OSD_PROFILES
288 void setOsdProfile(uint8_t value
)
293 if (value
<= OSD_PROFILE_COUNT
) {
297 osdProfile
= 1 << (value
- 1);
302 uint8_t getCurrentOsdProfileIndex(void)
304 return osdConfig()->osdProfileIndex
;
307 void changeOsdProfileIndex(uint8_t profileIndex
)
309 if (profileIndex
<= OSD_PROFILE_COUNT
) {
310 osdConfigMutable()->osdProfileIndex
= profileIndex
;
311 setOsdProfile(profileIndex
);
312 osdAnalyzeActiveElements();
317 void osdAnalyzeActiveElements(void)
319 /* This code results in a total RX task RX_STATE_MODES state time of ~68us on an F411 overclocked to 108MHz
320 * This upsets the scheduler task duration estimation and will break SPI RX communication. This can
321 * occur in flight, e.g. when the OSD profile is changed by switch so can be ignored, or GPS sensor comms
322 * is lost - only causing one late task instance.
324 schedulerIgnoreTaskExecTime();
326 osdAddActiveElements();
327 osdDrawActiveElementsBackground(osdDisplayPort
);
330 const uint16_t osdTimerDefault
[OSD_TIMER_COUNT
] = {
331 OSD_TIMER(OSD_TIMER_SRC_ON
, OSD_TIMER_PREC_SECOND
, 10),
332 OSD_TIMER(OSD_TIMER_SRC_TOTAL_ARMED
, OSD_TIMER_PREC_SECOND
, 10)
335 void pgResetFn_osdConfig(osdConfig_t
*osdConfig
)
337 // Enable the default stats
338 osdConfig
->enabled_stats
= 0; // reset all to off and enable only a few initially
339 osdStatSetState(OSD_STAT_MAX_SPEED
, true);
340 osdStatSetState(OSD_STAT_MIN_BATTERY
, true);
341 osdStatSetState(OSD_STAT_MIN_RSSI
, true);
342 osdStatSetState(OSD_STAT_MAX_CURRENT
, true);
343 osdStatSetState(OSD_STAT_USED_MAH
, true);
344 osdStatSetState(OSD_STAT_BLACKBOX
, true);
345 osdStatSetState(OSD_STAT_BLACKBOX_NUMBER
, true);
346 osdStatSetState(OSD_STAT_TIMER_2
, true);
348 osdConfig
->units
= UNIT_METRIC
;
350 // Enable all warnings by default
351 for (int i
=0; i
< OSD_WARNING_COUNT
; i
++) {
352 osdWarnSetState(i
, true);
354 // turn off RSSI & Link Quality warnings by default
355 osdWarnSetState(OSD_WARNING_RSSI
, false);
356 osdWarnSetState(OSD_WARNING_LINK_QUALITY
, false);
357 osdWarnSetState(OSD_WARNING_RSSI_DBM
, false);
358 osdWarnSetState(OSD_WARNING_RSNR
, false);
359 // turn off the over mah capacity warning
360 osdWarnSetState(OSD_WARNING_OVER_CAP
, false);
363 osdStatSetState(OSD_STAT_FULL_THROTTLE_TIME
, true);
364 osdStatSetState(OSD_STAT_FULL_THROTTLE_COUNTER
, true);
365 osdStatSetState(OSD_STAT_AVG_THROTTLE
, true);
368 osdConfig
->timers
[OSD_TIMER_1
] = osdTimerDefault
[OSD_TIMER_1
];
369 osdConfig
->timers
[OSD_TIMER_2
] = osdTimerDefault
[OSD_TIMER_2
];
371 osdConfig
->overlay_radio_mode
= 2;
373 osdConfig
->rssi_alarm
= 20;
374 osdConfig
->link_quality_alarm
= 80;
375 osdConfig
->cap_alarm
= 2200;
376 osdConfig
->alt_alarm
= 100; // meters or feet depend on configuration
377 osdConfig
->esc_temp_alarm
= ESC_TEMP_ALARM_OFF
; // off by default
378 osdConfig
->esc_rpm_alarm
= ESC_RPM_ALARM_OFF
; // off by default
379 osdConfig
->esc_current_alarm
= ESC_CURRENT_ALARM_OFF
; // off by default
380 osdConfig
->core_temp_alarm
= 70; // a temperature above 70C should produce a warning, lockups have been reported above 80C
382 osdConfig
->ahMaxPitch
= 20; // 20 degrees
383 osdConfig
->ahMaxRoll
= 40; // 40 degrees
385 osdConfig
->osdProfileIndex
= 1;
386 osdConfig
->ahInvert
= false;
387 for (int i
=0; i
< OSD_PROFILE_COUNT
; i
++) {
388 osdConfig
->profile
[i
][0] = '\0';
390 osdConfig
->rssi_dbm_alarm
= -60;
391 osdConfig
->rsnr_alarm
= 4;
392 osdConfig
->gps_sats_show_hdop
= false;
394 for (int i
= 0; i
< OSD_RCCHANNELS_COUNT
; i
++) {
395 osdConfig
->rcChannels
[i
] = -1;
398 osdConfig
->displayPortDevice
= OSD_DISPLAYPORT_DEVICE_AUTO
;
400 osdConfig
->distance_alarm
= 0;
401 osdConfig
->logo_on_arming
= OSD_LOGO_ARMING_OFF
;
402 osdConfig
->logo_on_arming_duration
= 5; // 0.5 seconds
404 osdConfig
->camera_frame_width
= 24;
405 osdConfig
->camera_frame_height
= 11;
407 osdConfig
->stat_show_cell_value
= false;
408 osdConfig
->framerate_hz
= OSD_FRAMERATE_DEFAULT_HZ
;
409 osdConfig
->cms_background_type
= DISPLAY_BACKGROUND_TRANSPARENT
;
410 #ifdef USE_CRAFTNAME_MSGS
411 osdConfig
->osd_craftname_msgs
= false; // Insert LQ/RSSI-dBm and warnings into CraftName
412 #endif //USE_CRAFTNAME_MSGS
414 osdConfig
->aux_channel
= 1;
415 osdConfig
->aux_scale
= 200;
416 osdConfig
->aux_symbol
= 'A';
418 // Make it obvious on the configurator that the FC doesn't support HD
420 osdConfig
->canvas_cols
= OSD_HD_COLS
;
421 osdConfig
->canvas_rows
= OSD_HD_ROWS
;
423 osdConfig
->canvas_cols
= OSD_SD_COLS
;
424 osdConfig
->canvas_rows
= OSD_SD_ROWS
;
427 #ifdef USE_QUICK_OSD_MENU
428 osdConfig
->osd_use_quick_menu
= true;
429 #endif // USE_QUICK_OSD_MENU
432 void pgResetFn_osdElementConfig(osdElementConfig_t
*osdElementConfig
)
442 // Position elements near centre of screen and disabled by default
443 for (int i
= 0; i
< OSD_ITEM_COUNT
; i
++) {
444 osdElementConfig
->item_pos
[i
] = OSD_POS((midCol
- 5), midRow
);
447 // Always enable warnings elements by default
448 uint16_t profileFlags
= 0;
449 for (unsigned i
= 1; i
<= OSD_PROFILE_COUNT
; i
++) {
450 profileFlags
|= OSD_PROFILE_FLAG(i
);
452 osdElementConfig
->item_pos
[OSD_WARNINGS
] = OSD_POS((midCol
- 6), (midRow
+ 3)) | profileFlags
;
454 // Default to old fixed positions for these elements
455 osdElementConfig
->item_pos
[OSD_CROSSHAIRS
] = OSD_POS((midCol
- 2), (midRow
- 1));
456 osdElementConfig
->item_pos
[OSD_ARTIFICIAL_HORIZON
] = OSD_POS((midCol
- 1), (midRow
- 5));
457 osdElementConfig
->item_pos
[OSD_HORIZON_SIDEBARS
] = OSD_POS((midCol
- 1), (midRow
- 1));
458 osdElementConfig
->item_pos
[OSD_CAMERA_FRAME
] = OSD_POS((midCol
- 12), (midRow
- 6));
459 osdElementConfig
->item_pos
[OSD_UP_DOWN_REFERENCE
] = OSD_POS((midCol
- 2), (midRow
- 1));
462 static void osdDrawLogo(int x
, int y
)
464 // display logo and help
465 int fontOffset
= 160;
466 for (int row
= 0; row
< OSD_LOGO_ROWS
; row
++) {
467 for (int column
= 0; column
< OSD_LOGO_COLS
; column
++) {
468 if (fontOffset
<= SYM_END_OF_FONT
)
469 displayWriteChar(osdDisplayPort
, x
+ column
, y
+ row
, DISPLAYPORT_SEVERITY_NORMAL
, fontOffset
++);
474 static void osdCompleteInitialization(void)
476 uint8_t midRow
= osdDisplayPort
->rows
/ 2;
477 uint8_t midCol
= osdDisplayPort
->cols
/ 2;
479 armState
= ARMING_FLAG(ARMED
);
483 backgroundLayerSupported
= displayLayerSupported(osdDisplayPort
, DISPLAYPORT_LAYER_BACKGROUND
);
484 displayLayerSelect(osdDisplayPort
, DISPLAYPORT_LAYER_FOREGROUND
);
486 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
487 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_WAIT
);
489 osdDrawLogo(midCol
- (OSD_LOGO_COLS
) / 2, midRow
- 5);
491 char string_buffer
[30];
492 tfp_sprintf(string_buffer
, "V%s", FC_VERSION_STRING
);
493 displayWrite(osdDisplayPort
, midCol
+ 5, midRow
, DISPLAYPORT_SEVERITY_NORMAL
, string_buffer
);
495 displayWrite(osdDisplayPort
, midCol
- 8, midRow
+ 2, DISPLAYPORT_SEVERITY_NORMAL
, CMS_STARTUP_HELP_TEXT1
);
496 displayWrite(osdDisplayPort
, midCol
- 4, midRow
+ 3, DISPLAYPORT_SEVERITY_NORMAL
, CMS_STARTUP_HELP_TEXT2
);
497 displayWrite(osdDisplayPort
, midCol
- 4, midRow
+ 4, DISPLAYPORT_SEVERITY_NORMAL
, CMS_STARTUP_HELP_TEXT3
);
501 char dateTimeBuffer
[FORMATTED_DATE_TIME_BUFSIZE
];
502 if (osdFormatRtcDateTime(&dateTimeBuffer
[0])) {
503 displayWrite(osdDisplayPort
, midCol
- 10, midRow
+ 6, DISPLAYPORT_SEVERITY_NORMAL
, dateTimeBuffer
);
507 resumeRefreshAt
= micros() + (4 * REFRESH_1S
);
508 #ifdef USE_OSD_PROFILES
509 setOsdProfile(osdConfig()->osdProfileIndex
);
512 osdElementsInit(backgroundLayerSupported
);
513 osdAnalyzeActiveElements();
518 void osdInit(displayPort_t
*osdDisplayPortToUse
, osdDisplayPortDevice_e displayPortDeviceType
)
520 osdDisplayPortDeviceType
= displayPortDeviceType
;
522 if (!osdDisplayPortToUse
) {
526 osdDisplayPort
= osdDisplayPortToUse
;
528 cmsDisplayPortRegister(osdDisplayPort
);
531 if (osdDisplayPort
->cols
&& osdDisplayPort
->rows
) {
532 // Ensure that osd_canvas_width/height are correct
533 if (osdConfig()->canvas_cols
!= osdDisplayPort
->cols
) {
534 osdConfigMutable()->canvas_cols
= osdDisplayPort
->cols
;
536 if (osdConfig()->canvas_rows
!= osdDisplayPort
->rows
) {
537 osdConfigMutable()->canvas_rows
= osdDisplayPort
->rows
;
540 // Ensure that all OSD elements are on the canvas once number of row/columns is known
541 for (int i
= 0; i
< OSD_ITEM_COUNT
; i
++) {
542 uint16_t itemPos
= osdElementConfig()->item_pos
[i
];
543 uint8_t elemPosX
= OSD_X(itemPos
);
544 uint8_t elemPosY
= OSD_Y(itemPos
);
545 uint16_t elemProfileType
= itemPos
& (OSD_PROFILE_MASK
| OSD_TYPE_MASK
);
546 bool pos_reset
= false;
548 if (elemPosX
>= osdDisplayPort
->cols
) {
549 elemPosX
= osdDisplayPort
->cols
- 1;
553 if (elemPosY
>= osdDisplayPort
->rows
) {
554 elemPosY
= osdDisplayPort
->rows
- 1;
559 osdElementConfigMutable()->item_pos
[i
] = elemProfileType
| OSD_POS(elemPosX
, elemPosY
);
565 #ifdef USE_GPS_LAP_TIMER
566 void printLapTime(char *buffer
, const uint32_t timeMs
) {
568 const uint32_t timeRoundMs
= timeMs
+ 5; // round value in division by 10
569 const int timeSeconds
= timeRoundMs
/ 1000;
570 const int timeDecimals
= (timeRoundMs
% 1000) / 10;
571 tfp_sprintf(buffer
, "%3u.%02u", timeSeconds
, timeDecimals
);
573 tfp_sprintf(buffer
, " -.--");
576 #endif // USE_GPS_LAP_TIMER
578 static void osdResetStats(void)
580 stats
.max_current
= 0;
582 stats
.min_voltage
= 5000;
583 stats
.end_voltage
= 0;
584 stats
.min_rssi
= 99; // percent
585 stats
.max_altitude
= 0;
586 stats
.max_distance
= 0;
587 stats
.armed_time
= 0;
588 stats
.max_g_force
= 0;
589 stats
.max_esc_temp_ix
= 0;
590 stats
.max_esc_temp
= 0;
591 stats
.max_esc_rpm
= 0;
592 stats
.min_link_quality
= (linkQualitySource
== LQ_SOURCE_NONE
) ? 99 : 100; // percent
593 stats
.min_rssi_dbm
= CRSF_RSSI_MAX
;
594 stats
.min_rsnr
= CRSF_SNR_MAX
;
597 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
598 static int32_t getAverageEscRpm(void)
600 #ifdef USE_ESC_SENSOR
601 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
602 return lrintf(erpmToRpm(osdEscDataCombined
->rpm
));
605 #ifdef USE_DSHOT_TELEMETRY
606 if (motorConfig()->dev
.useDshotTelemetry
) {
607 return lrintf(getDshotRpmAverage());
614 static uint16_t getStatsVoltage(void)
616 return osdConfig()->stat_show_cell_value
? getBatteryAverageCellVoltage() : getBatteryVoltage();
619 static void osdUpdateStats(void)
624 if (gpsConfig()->gps_use_3d_speed
) {
625 value
= gpsSol
.speed3d
;
627 value
= gpsSol
.groundSpeed
;
629 if (stats
.max_speed
< value
) {
630 stats
.max_speed
= value
;
634 value
= getStatsVoltage();
635 if (stats
.min_voltage
> value
) {
636 stats
.min_voltage
= value
;
639 value
= getAmperage() / 100;
640 if (stats
.max_current
< value
) {
641 stats
.max_current
= value
;
644 value
= getRssiPercent();
645 if (stats
.min_rssi
> value
) {
646 stats
.min_rssi
= value
;
649 int32_t altitudeCm
= getEstimatedAltitudeCm();
650 if (stats
.max_altitude
< altitudeCm
) {
651 stats
.max_altitude
= altitudeCm
;
655 if (stats
.max_g_force
< osdGForce
) {
656 stats
.max_g_force
= osdGForce
;
660 #ifdef USE_RX_LINK_QUALITY_INFO
661 value
= rxGetLinkQualityPercent();
662 if (stats
.min_link_quality
> value
) {
663 stats
.min_link_quality
= value
;
667 #ifdef USE_RX_RSSI_DBM
668 value
= getRssiDbm();
669 if (stats
.min_rssi_dbm
> value
) {
670 stats
.min_rssi_dbm
= value
;
676 if (stats
.min_rsnr
> value
) {
677 stats
.min_rsnr
= value
;
682 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
683 if (stats
.max_distance
< GPS_distanceToHome
) {
684 stats
.max_distance
= GPS_distanceToHome
;
689 #if defined(USE_ESC_SENSOR)
690 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
691 value
= osdEscDataCombined
->temperature
;
692 if (stats
.max_esc_temp
< value
) {
693 stats
.max_esc_temp
= value
;
697 #if defined(USE_DSHOT_TELEMETRY)
699 // Take max temp from dshot telemetry
700 for (uint8_t k
= 0; k
< getMotorCount(); k
++) {
701 if (dshotTelemetryState
.motorState
[k
].maxTemp
> stats
.max_esc_temp
) {
702 stats
.max_esc_temp_ix
= k
+ 1;
703 stats
.max_esc_temp
= dshotTelemetryState
.motorState
[k
].maxTemp
;
711 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
712 int32_t rpm
= getAverageEscRpm();
713 if (stats
.max_esc_rpm
< rpm
) {
714 stats
.max_esc_rpm
= rpm
;
721 static void osdGetBlackboxStatusString(char * buff
)
723 bool storageDeviceIsWorking
= isBlackboxDeviceWorking();
724 uint32_t storageUsed
= 0;
725 uint32_t storageTotal
= 0;
727 switch (blackboxConfig()->device
) {
729 case BLACKBOX_DEVICE_SDCARD
:
730 if (storageDeviceIsWorking
) {
731 storageTotal
= sdcard_getMetadata()->numBlocks
/ 2000;
732 storageUsed
= storageTotal
- (afatfs_getContiguousFreeSpace() / 1024000);
738 case BLACKBOX_DEVICE_FLASH
:
739 if (storageDeviceIsWorking
) {
741 const flashPartition_t
*flashPartition
= flashPartitionFindByType(FLASH_PARTITION_TYPE_FLASHFS
);
742 const flashGeometry_t
*flashGeometry
= flashGetGeometry();
744 storageTotal
= ((FLASH_PARTITION_SECTOR_COUNT(flashPartition
) * flashGeometry
->sectorSize
) / 1024);
745 storageUsed
= flashfsGetOffset() / 1024;
754 if (storageDeviceIsWorking
) {
755 const uint16_t storageUsedPercent
= (storageUsed
* 100) / storageTotal
;
756 tfp_sprintf(buff
, "%d%%", storageUsedPercent
);
758 tfp_sprintf(buff
, "FAULT");
763 static void osdDisplayStatisticLabel(uint8_t x
, uint8_t y
, const char * text
, const char * value
)
765 displayWrite(osdDisplayPort
, x
- 13, y
, DISPLAYPORT_SEVERITY_NORMAL
, text
);
766 displayWrite(osdDisplayPort
, x
+ 5, y
, DISPLAYPORT_SEVERITY_NORMAL
, ":");
767 displayWrite(osdDisplayPort
, x
+ 7, y
, DISPLAYPORT_SEVERITY_NORMAL
, value
);
771 * Test if there's some stat enabled
773 static bool isSomeStatEnabled(void)
775 return (osdConfig()->enabled_stats
!= 0);
779 // The stats display order was previously required to match the enumeration definition so it matched
780 // the order shown in the configurator. However, to allow reordering this screen without breaking the
781 // compatibility, this requirement has been relaxed to a best effort approach. Reordering the elements
782 // on the stats screen will have to be more beneficial than the hassle of not matching exactly to the
783 // configurator list.
785 static bool osdDisplayStat(int statistic
, uint8_t displayRow
)
787 uint8_t midCol
= osdDisplayPort
->cols
/ 2;
788 char buff
[OSD_ELEMENT_BUFFER_LENGTH
];
791 case OSD_STAT_RTC_DATE_TIME
: {
792 bool success
= false;
794 success
= osdFormatRtcDateTime(&buff
[0]);
797 tfp_sprintf(buff
, "NO RTC");
800 displayWrite(osdDisplayPort
, midCol
- 13, displayRow
, DISPLAYPORT_SEVERITY_NORMAL
, buff
);
804 case OSD_STAT_TIMER_1
:
805 osdFormatTimer(buff
, false, (OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_1
]) == OSD_TIMER_SRC_ON
? false : true), OSD_TIMER_1
);
806 osdDisplayStatisticLabel(midCol
, displayRow
, osdTimerSourceNames
[OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_1
])], buff
);
809 case OSD_STAT_TIMER_2
:
810 osdFormatTimer(buff
, false, (OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_2
]) == OSD_TIMER_SRC_ON
? false : true), OSD_TIMER_2
);
811 osdDisplayStatisticLabel(midCol
, displayRow
, osdTimerSourceNames
[OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_2
])], buff
);
814 case OSD_STAT_MAX_ALTITUDE
: {
815 osdPrintFloat(buff
, SYM_NONE
, osdGetMetersToSelectedUnit(stats
.max_altitude
) / 100.0f
, "", 1, true, osdGetMetersToSelectedUnitSymbol());
816 osdDisplayStatisticLabel(midCol
, displayRow
, "MAX ALTITUDE", buff
);
821 case OSD_STAT_MAX_SPEED
:
822 if (featureIsEnabled(FEATURE_GPS
)) {
823 tfp_sprintf(buff
, "%d%c", osdGetSpeedToSelectedUnit(stats
.max_speed
), osdGetSpeedToSelectedUnitSymbol());
824 osdDisplayStatisticLabel(midCol
, displayRow
, "MAX SPEED", buff
);
829 case OSD_STAT_MAX_DISTANCE
:
830 if (featureIsEnabled(FEATURE_GPS
)) {
831 osdFormatDistanceString(buff
, stats
.max_distance
, SYM_NONE
);
832 osdDisplayStatisticLabel(midCol
, displayRow
, "MAX DISTANCE", buff
);
837 case OSD_STAT_FLIGHT_DISTANCE
:
838 if (featureIsEnabled(FEATURE_GPS
)) {
839 const int distanceFlown
= GPS_distanceFlownInCm
/ 100;
840 osdFormatDistanceString(buff
, distanceFlown
, SYM_NONE
);
841 osdDisplayStatisticLabel(midCol
, displayRow
, "FLIGHT DISTANCE", buff
);
847 case OSD_STAT_MIN_BATTERY
:
848 osdPrintFloat(buff
, SYM_NONE
, stats
.min_voltage
/ 100.0f
, "", 2, true, SYM_VOLT
);
849 osdDisplayStatisticLabel(midCol
, displayRow
, osdConfig()->stat_show_cell_value
? "MIN AVG CELL" : "MIN BATTERY", buff
);
852 case OSD_STAT_END_BATTERY
:
853 osdPrintFloat(buff
, SYM_NONE
, stats
.end_voltage
/ 100.0f
, "", 2, true, SYM_VOLT
);
854 osdDisplayStatisticLabel(midCol
, displayRow
, osdConfig()->stat_show_cell_value
? "END AVG CELL" : "END BATTERY", buff
);
857 case OSD_STAT_BATTERY
:
859 const uint16_t statsVoltage
= getStatsVoltage();
860 osdPrintFloat(buff
, SYM_NONE
, statsVoltage
/ 100.0f
, "", 2, true, SYM_VOLT
);
861 osdDisplayStatisticLabel(midCol
, displayRow
, osdConfig()->stat_show_cell_value
? "AVG BATT CELL" : "BATTERY", buff
);
866 case OSD_STAT_MIN_RSSI
:
867 itoa(stats
.min_rssi
, buff
, 10);
869 osdDisplayStatisticLabel(midCol
, displayRow
, "MIN RSSI", buff
);
872 case OSD_STAT_MAX_CURRENT
:
873 if (batteryConfig()->currentMeterSource
!= CURRENT_METER_NONE
) {
874 tfp_sprintf(buff
, "%d%c", stats
.max_current
, SYM_AMP
);
875 osdDisplayStatisticLabel(midCol
, displayRow
, "MAX CURRENT", buff
);
880 case OSD_STAT_USED_MAH
:
881 if (batteryConfig()->currentMeterSource
!= CURRENT_METER_NONE
) {
882 tfp_sprintf(buff
, "%d%c", getMAhDrawn(), SYM_MAH
);
883 osdDisplayStatisticLabel(midCol
, displayRow
, "USED MAH", buff
);
888 case OSD_STAT_WATT_HOURS_DRAWN
:
889 if (batteryConfig()->currentMeterSource
!= CURRENT_METER_NONE
) {
890 osdPrintFloat(buff
, SYM_NONE
, getWhDrawn(), "", 2, true, SYM_NONE
);
891 osdDisplayStatisticLabel(midCol
, displayRow
, "USED WATT HOURS", buff
);
897 case OSD_STAT_BLACKBOX
:
898 if (blackboxConfig()->device
&& blackboxConfig()->device
!= BLACKBOX_DEVICE_SERIAL
) {
899 osdGetBlackboxStatusString(buff
);
900 osdDisplayStatisticLabel(midCol
, displayRow
, "BLACKBOX", buff
);
905 case OSD_STAT_BLACKBOX_NUMBER
:
907 int32_t logNumber
= blackboxGetLogNumber();
908 if (logNumber
>= 0) {
909 itoa(logNumber
, buff
, 10);
910 osdDisplayStatisticLabel(midCol
, displayRow
, "BB LOG NUM", buff
);
918 case OSD_STAT_MAX_G_FORCE
:
919 if (sensors(SENSOR_ACC
)) {
920 osdPrintFloat(buff
, SYM_NONE
, stats
.max_g_force
, "", 1, true, 'G');
921 osdDisplayStatisticLabel(midCol
, displayRow
, "MAX G-FORCE", buff
);
927 #ifdef USE_ESC_SENSOR
928 case OSD_STAT_MAX_ESC_TEMP
:
931 if (stats
.max_esc_temp_ix
> 0) {
932 ix
= tfp_sprintf(buff
, "%d ", stats
.max_esc_temp_ix
);
934 tfp_sprintf(buff
+ ix
, "%d%c", osdConvertTemperatureToSelectedUnit(stats
.max_esc_temp
), osdGetTemperatureSymbolForSelectedUnit());
935 osdDisplayStatisticLabel(midCol
, displayRow
, "MAX ESC TEMP", buff
);
940 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
941 case OSD_STAT_MAX_ESC_RPM
:
942 itoa(stats
.max_esc_rpm
, buff
, 10);
943 osdDisplayStatisticLabel(midCol
, displayRow
, "MAX ESC RPM", buff
);
947 #ifdef USE_RX_LINK_QUALITY_INFO
948 case OSD_STAT_MIN_LINK_QUALITY
:
949 tfp_sprintf(buff
, "%d", stats
.min_link_quality
);
951 osdDisplayStatisticLabel(midCol
, displayRow
, "MIN LINK", buff
);
955 #if defined(USE_DYN_NOTCH_FILTER)
956 case OSD_STAT_MAX_FFT
:
957 if (isDynNotchActive()) {
958 int value
= getMaxFFT();
960 tfp_sprintf(buff
, "%dHZ", value
);
961 osdDisplayStatisticLabel(midCol
, displayRow
, "PEAK FFT", buff
);
963 osdDisplayStatisticLabel(midCol
, displayRow
, "PEAK FFT", "THRT<20%");
970 #ifdef USE_RX_RSSI_DBM
971 case OSD_STAT_MIN_RSSI_DBM
:
972 tfp_sprintf(buff
, "%3d", stats
.min_rssi_dbm
);
973 osdDisplayStatisticLabel(midCol
, displayRow
, "MIN RSSI DBM", buff
);
978 case OSD_STAT_MIN_RSNR
:
979 tfp_sprintf(buff
, "%3d", stats
.min_rsnr
);
980 osdDisplayStatisticLabel(midCol
, displayRow
, "MIN RSNR", buff
);
984 #ifdef USE_GPS_LAP_TIMER
985 case OSD_STAT_BEST_3_CONSEC_LAPS
: {
986 printLapTime(buff
, gpsLapTimerData
.best3Consec
);
987 osdDisplayStatisticLabel(midCol
, displayRow
, "BEST 3 CON", buff
);
991 case OSD_STAT_BEST_LAP
: {
992 printLapTime(buff
, gpsLapTimerData
.bestLapTime
);
993 osdDisplayStatisticLabel(midCol
, displayRow
, "BEST LAP", buff
);
996 #endif // USE_GPS_LAP_TIMER
998 #ifdef USE_PERSISTENT_STATS
999 case OSD_STAT_TOTAL_FLIGHTS
:
1000 itoa(statsConfig()->stats_total_flights
, buff
, 10);
1001 osdDisplayStatisticLabel(midCol
, displayRow
, "TOTAL FLIGHTS", buff
);
1004 case OSD_STAT_TOTAL_TIME
: {
1005 int minutes
= statsConfig()->stats_total_time_s
/ 60;
1006 tfp_sprintf(buff
, "%d:%02dH", minutes
/ 60, minutes
% 60);
1007 osdDisplayStatisticLabel(midCol
, displayRow
, "TOTAL FLIGHT TIME", buff
);
1011 case OSD_STAT_TOTAL_DIST
:
1012 #define METERS_PER_KILOMETER 1000
1013 #define METERS_PER_MILE 1609
1014 if (osdConfig()->units
== UNIT_IMPERIAL
) {
1015 tfp_sprintf(buff
, "%d%c", statsConfig()->stats_total_dist_m
/ METERS_PER_MILE
, SYM_MILES
);
1017 tfp_sprintf(buff
, "%d%c", statsConfig()->stats_total_dist_m
/ METERS_PER_KILOMETER
, SYM_KM
);
1019 osdDisplayStatisticLabel(midCol
, displayRow
, "TOTAL DISTANCE", buff
);
1023 case OSD_STAT_FULL_THROTTLE_TIME
: {
1024 int seconds
= RcStatsGetFullThrottleTimeUs() / 1000000;
1025 const int minutes
= seconds
/ 60;
1026 seconds
= seconds
% 60;
1027 tfp_sprintf(buff
, "%02d:%02d", minutes
, seconds
);
1028 osdDisplayStatisticLabel(midCol
, displayRow
, "100% THRT TIME", buff
);
1032 case OSD_STAT_FULL_THROTTLE_COUNTER
: {
1033 itoa(RcStatsGetFullThrottleCounter(), buff
, 10);
1034 osdDisplayStatisticLabel(midCol
, displayRow
, "100% THRT COUNT", buff
);
1038 case OSD_STAT_AVG_THROTTLE
: {
1039 itoa(RcStatsGetAverageThrottle(), buff
, 10);
1040 osdDisplayStatisticLabel(midCol
, displayRow
, "AVG THROTTLE", buff
);
1043 #endif // USE_RC_STATS
1048 typedef struct osdStatsRenderingState_s
{
1052 } osdStatsRenderingState_t
;
1054 static osdStatsRenderingState_t osdStatsRenderingState
;
1056 static void osdRenderStatsReset(void)
1058 // reset to 0 so it will be recalculated on the next stats refresh
1059 osdStatsRenderingState
.rowCount
= 0;
1062 static void osdRenderStatsBegin(void)
1064 osdStatsRenderingState
.row
= 0;
1065 osdStatsRenderingState
.index
= 0;
1069 // call repeatedly until it returns true which indicates that all stats have been rendered.
1070 static bool osdRenderStatsContinue(void)
1072 uint8_t midCol
= osdDisplayPort
->cols
/ 2;
1074 if (osdStatsRenderingState
.row
== 0) {
1076 bool displayLabel
= false;
1078 // if rowCount is 0 then we're running an initial analysis of the active stats items
1079 if (osdStatsRenderingState
.rowCount
> 0) {
1080 const int availableRows
= osdDisplayPort
->rows
;
1081 int displayRows
= MIN(osdStatsRenderingState
.rowCount
, availableRows
);
1082 if (osdStatsRenderingState
.rowCount
< availableRows
) {
1083 displayLabel
= true;
1086 osdStatsRenderingState
.row
= (availableRows
- displayRows
) / 2; // center the stats vertically
1090 displayWrite(osdDisplayPort
, midCol
- (strlen("--- STATS ---") / 2), osdStatsRenderingState
.row
++, DISPLAYPORT_SEVERITY_NORMAL
, "--- STATS ---");
1096 bool renderedStat
= false;
1098 while (osdStatsRenderingState
.index
< OSD_STAT_COUNT
) {
1099 int index
= osdStatsRenderingState
.index
;
1101 // prepare for the next call to the method
1102 osdStatsRenderingState
.index
++;
1104 // look for something to render
1105 if (osdStatGetState(osdStatsDisplayOrder
[index
])) {
1106 if (osdDisplayStat(osdStatsDisplayOrder
[index
], osdStatsRenderingState
.row
)) {
1107 osdStatsRenderingState
.row
++;
1108 renderedStat
= true;
1114 bool moreSpaceAvailable
= osdStatsRenderingState
.row
< osdDisplayPort
->rows
;
1116 if (renderedStat
&& moreSpaceAvailable
) {
1120 if (osdStatsRenderingState
.rowCount
== 0) {
1121 osdStatsRenderingState
.rowCount
= osdStatsRenderingState
.row
;
1127 // returns true when all phases are complete
1128 static bool osdRefreshStats(void)
1130 bool completed
= false;
1133 INITIAL_CLEAR_SCREEN
= 0,
1137 } osdRefreshStatsPhase_e
;
1139 static osdRefreshStatsPhase_e phase
= INITIAL_CLEAR_SCREEN
;
1143 case INITIAL_CLEAR_SCREEN
:
1144 osdRenderStatsBegin();
1145 if (osdStatsRenderingState
.rowCount
> 0) {
1146 phase
= RENDER_STATS
;
1148 phase
= COUNT_STATS
;
1150 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_NONE
);
1154 // No stats row count has been set yet.
1155 // Go through the logic one time to determine how many stats are actually displayed.
1156 bool count_phase_complete
= osdRenderStatsContinue();
1157 if (count_phase_complete
) {
1158 phase
= CLEAR_SCREEN
;
1163 osdRenderStatsBegin();
1164 // Then clear the screen and commence with normal stats display which will
1165 // determine if the heading should be displayed and also center the content vertically.
1166 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_NONE
);
1167 phase
= RENDER_STATS
;
1170 completed
= osdRenderStatsContinue();
1175 phase
= INITIAL_CLEAR_SCREEN
;
1181 static timeDelta_t
osdShowArmed(void)
1183 uint8_t midRow
= osdDisplayPort
->rows
/ 2;
1184 uint8_t midCol
= osdDisplayPort
->cols
/ 2;
1187 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_WAIT
);
1189 if ((osdConfig()->logo_on_arming
== OSD_LOGO_ARMING_ON
) || ((osdConfig()->logo_on_arming
== OSD_LOGO_ARMING_FIRST
) && !ARMING_FLAG(WAS_EVER_ARMED
))) {
1190 uint8_t midRow
= osdDisplayPort
->rows
/ 2;
1191 uint8_t midCol
= osdDisplayPort
->cols
/ 2;
1192 osdDrawLogo(midCol
- (OSD_LOGO_COLS
) / 2, midRow
- 5);
1193 ret
= osdConfig()->logo_on_arming_duration
* 1e5
;
1195 ret
= (REFRESH_1S
/ 2);
1197 displayWrite(osdDisplayPort
, midCol
- (strlen("ARMED") / 2), midRow
, DISPLAYPORT_SEVERITY_NORMAL
, "ARMED");
1199 if (isFlipOverAfterCrashActive()) {
1200 displayWrite(osdDisplayPort
, midCol
- (strlen(CRASH_FLIP_WARNING
) / 2), midRow
+ 1, DISPLAYPORT_SEVERITY_NORMAL
, CRASH_FLIP_WARNING
);
1206 static bool osdStatsVisible
= false;
1207 static bool osdStatsEnabled
= false;
1209 STATIC_UNIT_TESTED
bool osdProcessStats1(timeUs_t currentTimeUs
)
1211 static timeUs_t lastTimeUs
= 0;
1212 static timeUs_t osdStatsRefreshTimeUs
;
1213 static timeUs_t osdAuxRefreshTimeUs
= 0;
1215 bool refreshStatsRequired
= false;
1217 // detect arm/disarm
1218 if (armState
!= ARMING_FLAG(ARMED
)) {
1219 if (ARMING_FLAG(ARMED
)) {
1220 osdStatsEnabled
= false;
1221 osdStatsVisible
= false;
1223 resumeRefreshAt
= osdShowArmed() + currentTimeUs
;
1224 } else if (isSomeStatEnabled()
1225 && !suppressStatsDisplay
1226 && !failsafeIsActive()
1227 && (!(getArmingDisableFlags() & (ARMING_DISABLED_RUNAWAY_TAKEOFF
| ARMING_DISABLED_CRASH_DETECTED
))
1228 || !VISIBLE(osdElementConfig()->item_pos
[OSD_WARNINGS
]))) { // suppress stats if runaway takeoff triggered disarm and WARNINGS element is visible
1229 osdStatsEnabled
= true;
1230 resumeRefreshAt
= currentTimeUs
+ (60 * REFRESH_1S
);
1231 stats
.end_voltage
= getStatsVoltage();
1232 osdRenderStatsReset();
1235 armState
= ARMING_FLAG(ARMED
);
1238 if (ARMING_FLAG(ARMED
)) {
1240 timeUs_t deltaT
= currentTimeUs
- lastTimeUs
;
1241 osdFlyTime
+= deltaT
;
1242 stats
.armed_time
+= deltaT
;
1243 } else if (osdStatsEnabled
) { // handle showing/hiding stats based on OSD disable switch position
1244 if (displayIsGrabbed(osdDisplayPort
)) {
1245 osdStatsEnabled
= false;
1246 resumeRefreshAt
= 0;
1247 stats
.armed_time
= 0;
1249 if (IS_RC_MODE_ACTIVE(BOXOSD
) && osdStatsVisible
) {
1250 osdStatsVisible
= false;
1251 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_NONE
);
1252 } else if (!IS_RC_MODE_ACTIVE(BOXOSD
)) {
1253 if (!osdStatsVisible
) {
1254 osdStatsVisible
= true;
1255 osdStatsRefreshTimeUs
= 0;
1257 if (currentTimeUs
>= osdStatsRefreshTimeUs
) {
1258 osdStatsRefreshTimeUs
= currentTimeUs
+ REFRESH_1S
;
1259 refreshStatsRequired
= true;
1265 if (VISIBLE(osdElementConfig()->item_pos
[OSD_AUX_VALUE
])) {
1266 const uint8_t auxChannel
= osdConfig()->aux_channel
+ NON_AUX_CHANNEL_COUNT
- 1;
1267 if (currentTimeUs
> osdAuxRefreshTimeUs
) {
1268 // aux channel start after main channels
1269 osdAuxValue
= (constrain(rcData
[auxChannel
], PWM_RANGE_MIN
, PWM_RANGE_MAX
) - PWM_RANGE_MIN
) * osdConfig()->aux_scale
/ PWM_RANGE
;
1270 osdAuxRefreshTimeUs
= currentTimeUs
+ REFRESH_1S
;
1274 lastTimeUs
= currentTimeUs
;
1276 return refreshStatsRequired
;
1279 void osdProcessStats2(timeUs_t currentTimeUs
)
1281 displayBeginTransaction(osdDisplayPort
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
1283 if (resumeRefreshAt
) {
1284 if (cmp32(currentTimeUs
, resumeRefreshAt
) < 0) {
1285 // in timeout period, check sticks for activity or CRASH FLIP switch to resume display.
1286 if (!ARMING_FLAG(ARMED
) &&
1287 (IS_HI(THROTTLE
) || IS_HI(PITCH
) || IS_RC_MODE_ACTIVE(BOXFLIPOVERAFTERCRASH
))) {
1288 resumeRefreshAt
= currentTimeUs
;
1292 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_NONE
);
1293 resumeRefreshAt
= 0;
1294 osdStatsEnabled
= false;
1295 stats
.armed_time
= 0;
1298 schedulerIgnoreTaskExecTime();
1300 #ifdef USE_ESC_SENSOR
1301 if (featureIsEnabled(FEATURE_ESC_SENSOR
)) {
1302 osdEscDataCombined
= getEscSensorData(ESC_SENSOR_COMBINED
);
1307 void osdProcessStats3(void)
1309 #if defined(USE_ACC)
1311 if (sensors(SENSOR_ACC
)
1312 && (VISIBLE(osdElementConfig()->item_pos
[OSD_G_FORCE
]) || osdStatGetState(OSD_STAT_MAX_G_FORCE
))) {
1313 // only calculate the G force if the element is visible or the stat is enabled
1314 for (int axis
= 0; axis
< XYZ_AXIS_COUNT
; axis
++) {
1315 const float a
= acc
.accADC
[axis
];
1318 osdGForce
= sqrtf(osdGForce
) * acc
.dev
.acc_1G_rec
;
1327 OSD_STATE_PROCESS_STATS1
,
1328 OSD_STATE_REFRESH_STATS
,
1329 OSD_STATE_PROCESS_STATS2
,
1330 OSD_STATE_PROCESS_STATS3
,
1331 OSD_STATE_UPDATE_ALARMS
,
1332 OSD_STATE_UPDATE_CANVAS
,
1333 OSD_STATE_GROUP_ELEMENTS
,
1334 OSD_STATE_UPDATE_ELEMENTS
,
1335 OSD_STATE_UPDATE_HEARTBEAT
,
1341 osdState_e osdState
= OSD_STATE_INIT
;
1343 #define OSD_UPDATE_INTERVAL_US (1000000 / osdConfig()->framerate_hz)
1345 // Called periodically by the scheduler
1346 bool osdUpdateCheck(timeUs_t currentTimeUs
, timeDelta_t currentDeltaTimeUs
)
1348 UNUSED(currentDeltaTimeUs
);
1349 static timeUs_t osdUpdateDueUs
= 0;
1351 if (osdState
== OSD_STATE_IDLE
) {
1352 // If the OSD is due a refresh, mark that as being the case
1353 if (cmpTimeUs(currentTimeUs
, osdUpdateDueUs
) > 0) {
1354 osdState
= OSD_STATE_CHECK
;
1356 // Determine time of next update
1357 if (osdUpdateDueUs
) {
1358 osdUpdateDueUs
+= OSD_UPDATE_INTERVAL_US
;
1360 osdUpdateDueUs
= currentTimeUs
+ OSD_UPDATE_INTERVAL_US
;
1365 return (osdState
!= OSD_STATE_IDLE
);
1368 // Called when there is OSD update work to be done
1369 void osdUpdate(timeUs_t currentTimeUs
)
1371 static uint16_t osdStateDurationFractionUs
[OSD_STATE_COUNT
] = { 0 };
1372 static uint32_t osdElementDurationUs
[OSD_ITEM_COUNT
] = { 0 };
1373 static uint8_t osdElementGroupMemberships
[OSD_ITEM_COUNT
];
1374 static uint16_t osdElementGroupTargetFractionUs
[OSD_GROUP_COUNT
] = { 0 };
1375 static uint16_t osdElementGroupDurationFractionUs
[OSD_GROUP_COUNT
] = { 0 };
1376 static uint8_t osdElementGroup
;
1377 static bool firstPass
= true;
1378 uint8_t osdCurrentElementGroup
= 0;
1379 timeUs_t executeTimeUs
;
1380 osdState_e osdCurrentState
= osdState
;
1382 if (osdState
!= OSD_STATE_UPDATE_CANVAS
) {
1383 schedulerIgnoreTaskExecRate();
1387 case OSD_STATE_INIT
:
1388 if (!displayCheckReady(osdDisplayPort
, false)) {
1389 // Frsky osd need a display redraw after search for MAX7456 devices
1390 if (osdDisplayPortDeviceType
== OSD_DISPLAYPORT_DEVICE_FRSKYOSD
) {
1391 displayRedraw(osdDisplayPort
);
1393 schedulerIgnoreTaskExecTime();
1398 osdCompleteInitialization();
1399 displayRedraw(osdDisplayPort
);
1400 osdState
= OSD_STATE_COMMIT
;
1404 case OSD_STATE_CHECK
:
1405 // don't touch buffers if DMA transaction is in progress
1406 if (displayIsTransferInProgress(osdDisplayPort
)) {
1410 osdState
= OSD_STATE_UPDATE_HEARTBEAT
;
1413 case OSD_STATE_UPDATE_HEARTBEAT
:
1414 if (displayHeartbeat(osdDisplayPort
)) {
1415 // Extraordinary action was taken, so return without allowing osdStateDurationFractionUs table to be updated
1419 osdState
= OSD_STATE_PROCESS_STATS1
;
1422 case OSD_STATE_PROCESS_STATS1
:
1424 bool refreshStatsRequired
= osdProcessStats1(currentTimeUs
);
1426 if (refreshStatsRequired
) {
1427 osdState
= OSD_STATE_REFRESH_STATS
;
1429 osdState
= OSD_STATE_PROCESS_STATS2
;
1433 case OSD_STATE_REFRESH_STATS
:
1435 bool completed
= osdRefreshStats();
1437 osdState
= OSD_STATE_PROCESS_STATS2
;
1441 case OSD_STATE_PROCESS_STATS2
:
1442 osdProcessStats2(currentTimeUs
);
1444 osdState
= OSD_STATE_PROCESS_STATS3
;
1446 case OSD_STATE_PROCESS_STATS3
:
1450 if (!displayIsGrabbed(osdDisplayPort
))
1453 osdState
= OSD_STATE_UPDATE_ALARMS
;
1457 osdState
= OSD_STATE_COMMIT
;
1460 case OSD_STATE_UPDATE_ALARMS
:
1463 if (resumeRefreshAt
) {
1464 osdState
= OSD_STATE_TRANSFER
;
1466 osdState
= OSD_STATE_UPDATE_CANVAS
;
1470 case OSD_STATE_UPDATE_CANVAS
:
1471 // Hide OSD when OSDSW mode is active
1472 if (IS_RC_MODE_ACTIVE(BOXOSD
)) {
1473 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_NONE
);
1474 osdState
= OSD_STATE_COMMIT
;
1478 if (backgroundLayerSupported
) {
1479 // Background layer is supported, overlay it onto the foreground
1480 // so that we only need to draw the active parts of the elements.
1481 displayLayerCopy(osdDisplayPort
, DISPLAYPORT_LAYER_FOREGROUND
, DISPLAYPORT_LAYER_BACKGROUND
);
1483 // Background layer not supported, just clear the foreground in preparation
1484 // for drawing the elements including their backgrounds.
1485 displayClearScreen(osdDisplayPort
, DISPLAY_CLEAR_NONE
);
1489 static bool lastGpsSensorState
;
1490 // Handle the case that the GPS_SENSOR may be delayed in activation
1491 // or deactivate if communication is lost with the module.
1492 const bool currentGpsSensorState
= sensors(SENSOR_GPS
);
1493 if (lastGpsSensorState
!= currentGpsSensorState
) {
1494 lastGpsSensorState
= currentGpsSensorState
;
1495 osdAnalyzeActiveElements();
1501 osdState
= OSD_STATE_GROUP_ELEMENTS
;
1505 case OSD_STATE_GROUP_ELEMENTS
:
1507 uint8_t elementGroup
;
1508 uint8_t activeElements
= osdGetActiveElementCount();
1511 for (elementGroup
= 0; elementGroup
< OSD_GROUP_COUNT
; elementGroup
++) {
1512 if (osdElementGroupDurationFractionUs
[elementGroup
] > (OSD_ELEMENT_RENDER_TARGET
<< OSD_EXEC_TIME_SHIFT
)) {
1513 osdElementGroupDurationFractionUs
[elementGroup
] = 0;
1515 osdElementGroupTargetFractionUs
[elementGroup
] = 0;
1520 // Based on the current element rendering, group to execute in approx 40us
1521 for (uint8_t curElement
= 0; curElement
< activeElements
; curElement
++) {
1522 if ((osdElementGroupTargetFractionUs
[elementGroup
] == 0) ||
1523 (osdElementGroupTargetFractionUs
[elementGroup
] + (osdElementDurationUs
[curElement
]) <= (OSD_ELEMENT_RENDER_TARGET
<< OSD_EXEC_TIME_SHIFT
)) ||
1524 (elementGroup
== (OSD_GROUP_COUNT
- 1))) {
1525 osdElementGroupTargetFractionUs
[elementGroup
] += osdElementDurationUs
[curElement
];
1526 // If group membership changes, reset the stats for the group
1527 if (osdElementGroupMemberships
[curElement
] != elementGroup
) {
1528 osdElementGroupDurationFractionUs
[elementGroup
] = osdElementGroupTargetFractionUs
[elementGroup
] + (OSD_ELEMENT_RENDER_GROUP_MARGIN
<< OSD_EXEC_TIME_SHIFT
);
1530 osdElementGroupMemberships
[curElement
] = elementGroup
;
1533 // Try again for this element
1538 // Start with group 0
1539 osdElementGroup
= 0;
1541 if (activeElements
> 0) {
1542 osdState
= OSD_STATE_UPDATE_ELEMENTS
;
1544 osdState
= OSD_STATE_COMMIT
;
1549 case OSD_STATE_UPDATE_ELEMENTS
:
1551 osdCurrentElementGroup
= osdElementGroup
;
1552 bool moreElements
= true;
1555 timeUs_t startElementTime
= micros();
1556 uint8_t osdCurrentElement
= osdGetActiveElement();
1558 // This element should be rendered in the next group
1559 if (osdElementGroupMemberships
[osdCurrentElement
] != osdElementGroup
) {
1564 moreElements
= osdDrawNextActiveElement(osdDisplayPort
, currentTimeUs
);
1566 executeTimeUs
= micros() - startElementTime
;
1568 if (executeTimeUs
> (osdElementDurationUs
[osdCurrentElement
] >> OSD_EXEC_TIME_SHIFT
)) {
1569 osdElementDurationUs
[osdCurrentElement
] = executeTimeUs
<< OSD_EXEC_TIME_SHIFT
;
1570 } else if (osdElementDurationUs
[osdCurrentElement
] > 0) {
1571 // Slowly decay the max time
1572 osdElementDurationUs
[osdCurrentElement
]--;
1574 } while (moreElements
);
1577 // There are more elements to draw
1581 osdElementGroup
= 0;
1583 osdState
= OSD_STATE_COMMIT
;
1587 case OSD_STATE_COMMIT
:
1588 displayCommitTransaction(osdDisplayPort
);
1590 if (resumeRefreshAt
) {
1591 osdState
= OSD_STATE_IDLE
;
1593 osdState
= OSD_STATE_TRANSFER
;
1597 case OSD_STATE_TRANSFER
:
1598 // Wait for any current transfer to complete
1599 if (displayIsTransferInProgress(osdDisplayPort
)) {
1603 // Transfer may be broken into many parts
1604 if (displayDrawScreen(osdDisplayPort
)) {
1609 osdState
= OSD_STATE_IDLE
;
1613 case OSD_STATE_IDLE
:
1615 osdState
= OSD_STATE_IDLE
;
1619 if (!schedulerGetIgnoreTaskExecTime()) {
1620 executeTimeUs
= micros() - currentTimeUs
;
1623 // On the first pass no element groups will have been formed, so all elements will have been
1624 // rendered which is unrepresentative, so ignore
1626 if (osdCurrentState
== OSD_STATE_UPDATE_ELEMENTS
) {
1627 if (executeTimeUs
> (osdElementGroupDurationFractionUs
[osdCurrentElementGroup
] >> OSD_EXEC_TIME_SHIFT
)) {
1628 osdElementGroupDurationFractionUs
[osdCurrentElementGroup
] = executeTimeUs
<< OSD_EXEC_TIME_SHIFT
;
1629 } else if (osdElementGroupDurationFractionUs
[osdCurrentElementGroup
] > 0) {
1630 // Slowly decay the max time
1631 osdElementGroupDurationFractionUs
[osdCurrentElementGroup
]--;
1635 if (executeTimeUs
> (osdStateDurationFractionUs
[osdCurrentState
] >> OSD_EXEC_TIME_SHIFT
)) {
1636 osdStateDurationFractionUs
[osdCurrentState
] = executeTimeUs
<< OSD_EXEC_TIME_SHIFT
;
1637 } else if (osdStateDurationFractionUs
[osdCurrentState
] > 0) {
1638 // Slowly decay the max time
1639 osdStateDurationFractionUs
[osdCurrentState
]--;
1644 if (osdState
== OSD_STATE_UPDATE_ELEMENTS
) {
1645 schedulerSetNextStateTime((osdElementGroupDurationFractionUs
[osdElementGroup
] >> OSD_EXEC_TIME_SHIFT
) + OSD_ELEMENT_RENDER_GROUP_MARGIN
);
1647 if (osdState
== OSD_STATE_IDLE
) {
1648 schedulerSetNextStateTime((osdStateDurationFractionUs
[OSD_STATE_CHECK
] >> OSD_EXEC_TIME_SHIFT
) + OSD_TASK_MARGIN
);
1650 schedulerSetNextStateTime((osdStateDurationFractionUs
[osdState
] >> OSD_EXEC_TIME_SHIFT
) + OSD_TASK_MARGIN
);
1655 void osdSuppressStats(bool flag
)
1657 suppressStatsDisplay
= flag
;
1660 #ifdef USE_OSD_PROFILES
1661 bool osdElementVisible(uint16_t value
)
1663 return (bool)((((value
& OSD_PROFILE_MASK
) >> OSD_PROFILE_BITS_POS
) & osdProfile
) != 0);
1667 bool osdGetVisualBeeperState(void)
1669 return showVisualBeeper
;
1672 void osdSetVisualBeeperState(bool state
)
1674 showVisualBeeper
= state
;
1677 statistic_t
*osdGetStats(void)
1683 // Determine if there are any enabled stats that need
1684 // the ACC (currently only MAX_G_FORCE).
1685 static bool osdStatsNeedAccelerometer(void)
1687 return osdStatGetState(OSD_STAT_MAX_G_FORCE
);
1690 // Check if any enabled elements or stats need the ACC
1691 bool osdNeedsAccelerometer(void)
1693 return osdStatsNeedAccelerometer() || osdElementsNeedAccelerometer();
1697 displayPort_t
*osdGetDisplayPort(osdDisplayPortDevice_e
*displayPortDeviceType
)
1699 if (displayPortDeviceType
) {
1700 *displayPortDeviceType
= osdDisplayPortDeviceType
;
1702 return osdDisplayPort
;