Rename USE_QUICK_OSD_MENU (#13273)
[betaflight.git] / src / main / osd / osd.c
blob0e5b485776c57c01f38ab112158b8dc8c09ce004
1 /*
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)
8 * any later version.
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
28 #include <stdbool.h>
29 #include <stdint.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <ctype.h>
33 #include <math.h>
35 #include "platform.h"
37 #ifdef USE_OSD
39 #include "blackbox/blackbox.h"
40 #include "blackbox/blackbox_io.h"
42 #include "build/build_config.h"
43 #include "build/version.h"
45 #include "cms/cms.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"
63 #include "fc/core.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"
71 #endif
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"
80 #include "io/gps.h"
82 #include "osd/osd.h"
83 #include "osd/osd_elements.h"
84 #include "osd/osd_warnings.h"
86 #include "pg/motor.h"
87 #include "pg/pg.h"
88 #include "pg/pg_ids.h"
89 #include "pg/stats.h"
91 #include "rx/crsf.h"
92 #include "rx/rc_stats.h"
93 #include "rx/rx.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"
103 #endif
105 typedef enum {
106 OSD_LOGO_ARMING_OFF,
107 OSD_LOGO_ARMING_ON,
108 OSD_LOGO_ARMING_FIRST
109 } osd_logo_on_arming_e;
111 const char * const osdTimerSourceNames[] = {
112 "ON TIME ",
113 "TOTAL ARM",
114 "LAST ARM ",
115 "ON/ARM "
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;
128 #if defined(USE_ACC)
129 float osdGForce = 0;
130 #endif
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;
142 #endif
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;
153 #endif
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,
173 OSD_STAT_TIMER_1,
174 OSD_STAT_TIMER_2,
175 OSD_STAT_MAX_ALTITUDE,
176 OSD_STAT_MAX_SPEED,
177 OSD_STAT_MAX_DISTANCE,
178 OSD_STAT_FLIGHT_DISTANCE,
179 OSD_STAT_MIN_BATTERY,
180 OSD_STAT_END_BATTERY,
181 OSD_STAT_BATTERY,
182 OSD_STAT_MIN_RSSI,
183 OSD_STAT_MAX_CURRENT,
184 OSD_STAT_USED_MAH,
185 OSD_STAT_BLACKBOX,
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,
191 OSD_STAT_MAX_FFT,
192 OSD_STAT_MIN_RSSI_DBM,
193 OSD_STAT_MIN_RSNR,
194 OSD_STAT_TOTAL_FLIGHTS,
195 OSD_STAT_TOTAL_TIME,
196 OSD_STAT_TOTAL_DIST,
197 OSD_STAT_WATT_HOURS_DRAWN,
198 OSD_STAT_BEST_3_CONSEC_LAPS,
199 OSD_STAT_BEST_LAP,
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
213 #else
214 #define OSD_ELEMENT_RENDER_GROUP_MARGIN 2
215 #endif
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)
226 char mask[7];
227 int pos = 0;
228 int multiplier = 1;
229 for (unsigned i = 0; i < decimalPlaces; i++) {
230 multiplier *= 10;
233 value *= multiplier;
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)) {
242 buffer[pos++] = '-';
245 pos += tfp_sprintf(buffer + pos, (strlen(formatString) ? formatString : "%01u"), integerPart);
246 if (decimalPlaces) {
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;
254 buffer[pos] = '\0';
256 return pos;
259 void osdStatSetState(uint8_t statIndex, bool enabled)
261 if (enabled) {
262 osdConfigMutable()->enabled_stats |= (1 << statIndex);
263 } else {
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)
275 if (enabled) {
276 osdConfigMutable()->enabledWarnings |= (1 << warningIndex);
277 } else {
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)
290 // 1 ->> 001
291 // 2 ->> 010
292 // 3 ->> 100
293 if (value <= OSD_PROFILE_COUNT) {
294 if (value == 0) {
295 osdProfile = 1;
296 } else {
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();
315 #endif
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 #ifdef USE_RACE_PRO
336 #define RACE_PRO true
337 #else
338 #define RACE_PRO false
339 #endif
341 void pgResetFn_osdConfig(osdConfig_t *osdConfig)
343 // Enable the default stats
344 osdConfig->enabled_stats = 0; // reset all to off and enable only a few initially
345 osdStatSetState(OSD_STAT_MAX_SPEED, !RACE_PRO);
346 osdStatSetState(OSD_STAT_MIN_BATTERY, true);
347 osdStatSetState(OSD_STAT_MIN_RSSI, !RACE_PRO);
348 osdStatSetState(OSD_STAT_MAX_CURRENT, !RACE_PRO);
349 osdStatSetState(OSD_STAT_USED_MAH, !RACE_PRO);
350 osdStatSetState(OSD_STAT_BLACKBOX, !RACE_PRO);
351 osdStatSetState(OSD_STAT_BLACKBOX_NUMBER, !RACE_PRO);
352 osdStatSetState(OSD_STAT_TIMER_2, true);
354 osdConfig->units = UNIT_METRIC;
356 // Enable all warnings by default
357 for (int i=0; i < OSD_WARNING_COUNT; i++) {
358 osdWarnSetState(i, true);
360 // turn off RSSI & Link Quality warnings by default
361 osdWarnSetState(OSD_WARNING_RSSI, false);
362 osdWarnSetState(OSD_WARNING_LINK_QUALITY, false);
363 osdWarnSetState(OSD_WARNING_RSSI_DBM, false);
364 osdWarnSetState(OSD_WARNING_RSNR, false);
365 // turn off the over mah capacity warning
366 osdWarnSetState(OSD_WARNING_OVER_CAP, false);
368 #ifdef USE_RC_STATS
369 osdStatSetState(OSD_STAT_FULL_THROTTLE_TIME, true);
370 osdStatSetState(OSD_STAT_FULL_THROTTLE_COUNTER, true);
371 osdStatSetState(OSD_STAT_AVG_THROTTLE, true);
372 #endif
374 osdConfig->timers[OSD_TIMER_1] = osdTimerDefault[OSD_TIMER_1];
375 osdConfig->timers[OSD_TIMER_2] = osdTimerDefault[OSD_TIMER_2];
377 osdConfig->overlay_radio_mode = 2;
379 osdConfig->rssi_alarm = 20;
380 osdConfig->link_quality_alarm = 80;
381 osdConfig->cap_alarm = 2200;
382 osdConfig->alt_alarm = 100; // meters or feet depend on configuration
383 osdConfig->esc_temp_alarm = ESC_TEMP_ALARM_OFF; // off by default
384 osdConfig->esc_rpm_alarm = ESC_RPM_ALARM_OFF; // off by default
385 osdConfig->esc_current_alarm = ESC_CURRENT_ALARM_OFF; // off by default
386 osdConfig->core_temp_alarm = 70; // a temperature above 70C should produce a warning, lockups have been reported above 80C
388 osdConfig->ahMaxPitch = 20; // 20 degrees
389 osdConfig->ahMaxRoll = 40; // 40 degrees
391 osdConfig->osdProfileIndex = 1;
392 osdConfig->ahInvert = false;
393 for (int i=0; i < OSD_PROFILE_COUNT; i++) {
394 osdConfig->profile[i][0] = '\0';
396 osdConfig->rssi_dbm_alarm = -60;
397 osdConfig->rsnr_alarm = 4;
398 osdConfig->gps_sats_show_hdop = false;
400 for (int i = 0; i < OSD_RCCHANNELS_COUNT; i++) {
401 osdConfig->rcChannels[i] = -1;
404 osdConfig->displayPortDevice = OSD_DISPLAYPORT_DEVICE_AUTO;
406 osdConfig->distance_alarm = 0;
407 osdConfig->logo_on_arming = OSD_LOGO_ARMING_OFF;
408 osdConfig->logo_on_arming_duration = 5; // 0.5 seconds
410 osdConfig->camera_frame_width = 24;
411 osdConfig->camera_frame_height = 11;
413 osdConfig->stat_show_cell_value = false;
414 osdConfig->framerate_hz = OSD_FRAMERATE_DEFAULT_HZ;
415 osdConfig->cms_background_type = DISPLAY_BACKGROUND_TRANSPARENT;
416 #ifdef USE_CRAFTNAME_MSGS
417 osdConfig->osd_craftname_msgs = false; // Insert LQ/RSSI-dBm and warnings into CraftName
418 #endif //USE_CRAFTNAME_MSGS
420 osdConfig->aux_channel = 1;
421 osdConfig->aux_scale = 200;
422 osdConfig->aux_symbol = 'A';
424 // Make it obvious on the configurator that the FC doesn't support HD
425 #ifdef USE_OSD_HD
426 osdConfig->canvas_cols = OSD_HD_COLS;
427 osdConfig->canvas_rows = OSD_HD_ROWS;
428 #else
429 osdConfig->canvas_cols = OSD_SD_COLS;
430 osdConfig->canvas_rows = OSD_SD_ROWS;
431 #endif
433 #ifdef USE_OSD_QUICK_MENU
434 osdConfig->osd_use_quick_menu = true;
435 #endif // USE_OSD_QUICK_MENU
436 #ifdef USE_SPEC_PREARM_SCREEN
437 osdConfig->osd_show_spec_prearm = true;
438 #endif // USE_SPEC_PREARM_SCREEN
441 void pgResetFn_osdElementConfig(osdElementConfig_t *osdElementConfig)
443 #ifdef USE_OSD_SD
444 uint8_t midRow = 7;
445 uint8_t midCol = 15;
446 #else
447 uint8_t midRow = 10;
448 uint8_t midCol = 26;
449 #endif
451 // Position elements near centre of screen and disabled by default
452 for (int i = 0; i < OSD_ITEM_COUNT; i++) {
453 osdElementConfig->item_pos[i] = OSD_POS((midCol - 5), midRow);
456 // Always enable warnings elements by default
457 uint16_t profileFlags = 0;
458 for (unsigned i = 1; i <= OSD_PROFILE_COUNT; i++) {
459 profileFlags |= OSD_PROFILE_FLAG(i);
461 osdElementConfig->item_pos[OSD_WARNINGS] = OSD_POS((midCol - 6), (midRow + 3)) | profileFlags;
463 // Default to old fixed positions for these elements
464 osdElementConfig->item_pos[OSD_CROSSHAIRS] = OSD_POS((midCol - 2), (midRow - 1));
465 osdElementConfig->item_pos[OSD_ARTIFICIAL_HORIZON] = OSD_POS((midCol - 1), (midRow - 5));
466 osdElementConfig->item_pos[OSD_HORIZON_SIDEBARS] = OSD_POS((midCol - 1), (midRow - 1));
467 osdElementConfig->item_pos[OSD_CAMERA_FRAME] = OSD_POS((midCol - 12), (midRow - 6));
468 osdElementConfig->item_pos[OSD_UP_DOWN_REFERENCE] = OSD_POS((midCol - 2), (midRow - 1));
471 static void osdDrawLogo(int x, int y)
473 // display logo and help
474 int fontOffset = 160;
475 for (int row = 0; row < OSD_LOGO_ROWS; row++) {
476 for (int column = 0; column < OSD_LOGO_COLS; column++) {
477 if (fontOffset <= SYM_END_OF_FONT)
478 displayWriteChar(osdDisplayPort, x + column, y + row, DISPLAYPORT_SEVERITY_NORMAL, fontOffset++);
483 static void osdCompleteInitialization(void)
485 uint8_t midRow = osdDisplayPort->rows / 2;
486 uint8_t midCol = osdDisplayPort->cols / 2;
488 armState = ARMING_FLAG(ARMED);
490 osdResetAlarms();
492 backgroundLayerSupported = displayLayerSupported(osdDisplayPort, DISPLAYPORT_LAYER_BACKGROUND);
493 displayLayerSelect(osdDisplayPort, DISPLAYPORT_LAYER_FOREGROUND);
495 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
496 displayClearScreen(osdDisplayPort, DISPLAY_CLEAR_WAIT);
498 osdDrawLogo(midCol - (OSD_LOGO_COLS) / 2, midRow - 5);
500 char string_buffer[30];
501 tfp_sprintf(string_buffer, "V%s", FC_VERSION_STRING);
502 displayWrite(osdDisplayPort, midCol + 5, midRow, DISPLAYPORT_SEVERITY_NORMAL, string_buffer);
503 #ifdef USE_CMS
504 displayWrite(osdDisplayPort, midCol - 8, midRow + 2, DISPLAYPORT_SEVERITY_NORMAL, CMS_STARTUP_HELP_TEXT1);
505 displayWrite(osdDisplayPort, midCol - 4, midRow + 3, DISPLAYPORT_SEVERITY_NORMAL, CMS_STARTUP_HELP_TEXT2);
506 displayWrite(osdDisplayPort, midCol - 4, midRow + 4, DISPLAYPORT_SEVERITY_NORMAL, CMS_STARTUP_HELP_TEXT3);
507 #endif
509 #ifdef USE_RTC_TIME
510 char dateTimeBuffer[FORMATTED_DATE_TIME_BUFSIZE];
511 if (osdFormatRtcDateTime(&dateTimeBuffer[0])) {
512 displayWrite(osdDisplayPort, midCol - 10, midRow + 6, DISPLAYPORT_SEVERITY_NORMAL, dateTimeBuffer);
514 #endif
516 resumeRefreshAt = micros() + (4 * REFRESH_1S);
517 #ifdef USE_OSD_PROFILES
518 setOsdProfile(osdConfig()->osdProfileIndex);
519 #endif
521 osdElementsInit(backgroundLayerSupported);
522 osdAnalyzeActiveElements();
524 osdIsReady = true;
527 void osdInit(displayPort_t *osdDisplayPortToUse, osdDisplayPortDevice_e displayPortDeviceType)
529 osdDisplayPortDeviceType = displayPortDeviceType;
531 if (!osdDisplayPortToUse) {
532 return;
535 osdDisplayPort = osdDisplayPortToUse;
536 #ifdef USE_CMS
537 cmsDisplayPortRegister(osdDisplayPort);
538 #endif
540 if (osdDisplayPort->cols && osdDisplayPort->rows) {
541 // Ensure that osd_canvas_width/height are correct
542 if (osdConfig()->canvas_cols != osdDisplayPort->cols) {
543 osdConfigMutable()->canvas_cols = osdDisplayPort->cols;
545 if (osdConfig()->canvas_rows != osdDisplayPort->rows) {
546 osdConfigMutable()->canvas_rows = osdDisplayPort->rows;
549 // Ensure that all OSD elements are on the canvas once number of row/columns is known
550 for (int i = 0; i < OSD_ITEM_COUNT; i++) {
551 uint16_t itemPos = osdElementConfig()->item_pos[i];
552 uint8_t elemPosX = OSD_X(itemPos);
553 uint8_t elemPosY = OSD_Y(itemPos);
554 uint16_t elemProfileType = itemPos & (OSD_PROFILE_MASK | OSD_TYPE_MASK);
555 bool pos_reset = false;
557 if (elemPosX >= osdDisplayPort->cols) {
558 elemPosX = osdDisplayPort->cols - 1;
559 pos_reset = true;
562 if (elemPosY >= osdDisplayPort->rows) {
563 elemPosY = osdDisplayPort->rows - 1;
564 pos_reset = true;
567 if (pos_reset) {
568 osdElementConfigMutable()->item_pos[i] = elemProfileType | OSD_POS(elemPosX, elemPosY);
574 #ifdef USE_GPS_LAP_TIMER
575 void printLapTime(char *buffer, const uint32_t timeMs) {
576 if (timeMs != 0) {
577 const uint32_t timeRoundMs = timeMs + 5; // round value in division by 10
578 const int timeSeconds = timeRoundMs / 1000;
579 const int timeDecimals = (timeRoundMs % 1000) / 10;
580 tfp_sprintf(buffer, "%3u.%02u", timeSeconds, timeDecimals);
581 } else {
582 tfp_sprintf(buffer, " -.--");
585 #endif // USE_GPS_LAP_TIMER
587 static void osdResetStats(void)
589 stats.max_current = 0;
590 stats.max_speed = 0;
591 stats.min_voltage = 5000;
592 stats.end_voltage = 0;
593 stats.min_rssi = 99; // percent
594 stats.max_altitude = 0;
595 stats.max_distance = 0;
596 stats.armed_time = 0;
597 stats.max_g_force = 0;
598 stats.max_esc_temp_ix = 0;
599 stats.max_esc_temp = 0;
600 stats.max_esc_rpm = 0;
601 stats.min_link_quality = (linkQualitySource == LQ_SOURCE_NONE) ? 99 : 100; // percent
602 stats.min_rssi_dbm = CRSF_RSSI_MAX;
603 stats.min_rsnr = CRSF_SNR_MAX;
606 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
607 static int32_t getAverageEscRpm(void)
609 #ifdef USE_ESC_SENSOR
610 if (featureIsEnabled(FEATURE_ESC_SENSOR)) {
611 return lrintf(erpmToRpm(osdEscDataCombined->rpm));
613 #endif
614 #ifdef USE_DSHOT_TELEMETRY
615 if (motorConfig()->dev.useDshotTelemetry) {
616 return lrintf(getDshotRpmAverage());
618 #endif
619 return 0;
621 #endif
623 static uint16_t getStatsVoltage(void)
625 return osdConfig()->stat_show_cell_value ? getBatteryAverageCellVoltage() : getBatteryVoltage();
628 static void osdUpdateStats(void)
630 int16_t value = 0;
632 #ifdef USE_GPS
633 if (gpsConfig()->gps_use_3d_speed) {
634 value = gpsSol.speed3d;
635 } else {
636 value = gpsSol.groundSpeed;
638 if (stats.max_speed < value) {
639 stats.max_speed = value;
641 #endif
643 value = getStatsVoltage();
644 if (stats.min_voltage > value) {
645 stats.min_voltage = value;
648 value = getAmperage() / 100;
649 if (stats.max_current < value) {
650 stats.max_current = value;
653 value = getRssiPercent();
654 if (stats.min_rssi > value) {
655 stats.min_rssi = value;
658 int32_t altitudeCm = getEstimatedAltitudeCm();
659 if (stats.max_altitude < altitudeCm) {
660 stats.max_altitude = altitudeCm;
663 #if defined(USE_ACC)
664 if (stats.max_g_force < osdGForce) {
665 stats.max_g_force = osdGForce;
667 #endif
669 #ifdef USE_RX_LINK_QUALITY_INFO
670 value = rxGetLinkQualityPercent();
671 if (stats.min_link_quality > value) {
672 stats.min_link_quality = value;
674 #endif
676 #ifdef USE_RX_RSSI_DBM
677 value = getRssiDbm();
678 if (stats.min_rssi_dbm > value) {
679 stats.min_rssi_dbm = value;
681 #endif
683 #ifdef USE_RX_RSNR
684 value = getRsnr();
685 if (stats.min_rsnr > value) {
686 stats.min_rsnr = value;
688 #endif
690 #ifdef USE_GPS
691 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) {
692 if (stats.max_distance < GPS_distanceToHome) {
693 stats.max_distance = GPS_distanceToHome;
696 #endif
698 #if defined(USE_ESC_SENSOR)
699 if (featureIsEnabled(FEATURE_ESC_SENSOR)) {
700 value = osdEscDataCombined->temperature;
701 if (stats.max_esc_temp < value) {
702 stats.max_esc_temp = value;
704 } else
705 #endif
706 #if defined(USE_DSHOT_TELEMETRY)
708 // Take max temp from dshot telemetry
709 for (uint8_t k = 0; k < getMotorCount(); k++) {
710 if (dshotTelemetryState.motorState[k].maxTemp > stats.max_esc_temp) {
711 stats.max_esc_temp_ix = k + 1;
712 stats.max_esc_temp = dshotTelemetryState.motorState[k].maxTemp;
716 #else
718 #endif
720 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
721 int32_t rpm = getAverageEscRpm();
722 if (stats.max_esc_rpm < rpm) {
723 stats.max_esc_rpm = rpm;
725 #endif
728 #ifdef USE_BLACKBOX
730 static void osdGetBlackboxStatusString(char * buff)
732 bool storageDeviceIsWorking = isBlackboxDeviceWorking();
733 uint32_t storageUsed = 0;
734 uint32_t storageTotal = 0;
736 switch (blackboxConfig()->device) {
737 #ifdef USE_SDCARD
738 case BLACKBOX_DEVICE_SDCARD:
739 if (storageDeviceIsWorking) {
740 storageTotal = sdcard_getMetadata()->numBlocks / 2000;
741 storageUsed = storageTotal - (afatfs_getContiguousFreeSpace() / 1024000);
743 break;
744 #endif
746 #ifdef USE_FLASHFS
747 case BLACKBOX_DEVICE_FLASH:
748 if (storageDeviceIsWorking) {
750 const flashPartition_t *flashPartition = flashPartitionFindByType(FLASH_PARTITION_TYPE_FLASHFS);
751 const flashGeometry_t *flashGeometry = flashGetGeometry();
753 storageTotal = ((FLASH_PARTITION_SECTOR_COUNT(flashPartition) * flashGeometry->sectorSize) / 1024);
754 storageUsed = flashfsGetOffset() / 1024;
756 break;
757 #endif
759 default:
760 break;
763 if (storageDeviceIsWorking) {
764 const uint16_t storageUsedPercent = (storageUsed * 100) / storageTotal;
765 tfp_sprintf(buff, "%d%%", storageUsedPercent);
766 } else {
767 tfp_sprintf(buff, "FAULT");
770 #endif
772 static void osdDisplayStatisticLabel(uint8_t x, uint8_t y, const char * text, const char * value)
774 displayWrite(osdDisplayPort, x - 13, y, DISPLAYPORT_SEVERITY_NORMAL, text);
775 displayWrite(osdDisplayPort, x + 5, y, DISPLAYPORT_SEVERITY_NORMAL, ":");
776 displayWrite(osdDisplayPort, x + 7, y, DISPLAYPORT_SEVERITY_NORMAL, value);
780 * Test if there's some stat enabled
782 static bool isSomeStatEnabled(void)
784 return (osdConfig()->enabled_stats != 0);
787 // *** IMPORTANT ***
788 // The stats display order was previously required to match the enumeration definition so it matched
789 // the order shown in the configurator. However, to allow reordering this screen without breaking the
790 // compatibility, this requirement has been relaxed to a best effort approach. Reordering the elements
791 // on the stats screen will have to be more beneficial than the hassle of not matching exactly to the
792 // configurator list.
794 static bool osdDisplayStat(int statistic, uint8_t displayRow)
796 uint8_t midCol = osdDisplayPort->cols / 2;
797 char buff[OSD_ELEMENT_BUFFER_LENGTH];
799 switch (statistic) {
800 case OSD_STAT_RTC_DATE_TIME: {
801 bool success = false;
802 #ifdef USE_RTC_TIME
803 success = osdFormatRtcDateTime(&buff[0]);
804 #endif
805 if (!success) {
806 tfp_sprintf(buff, "NO RTC");
809 displayWrite(osdDisplayPort, midCol - 13, displayRow, DISPLAYPORT_SEVERITY_NORMAL, buff);
810 return true;
813 case OSD_STAT_TIMER_1:
814 osdFormatTimer(buff, false, (OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_1]) == OSD_TIMER_SRC_ON ? false : true), OSD_TIMER_1);
815 osdDisplayStatisticLabel(midCol, displayRow, osdTimerSourceNames[OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_1])], buff);
816 return true;
818 case OSD_STAT_TIMER_2:
819 osdFormatTimer(buff, false, (OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_2]) == OSD_TIMER_SRC_ON ? false : true), OSD_TIMER_2);
820 osdDisplayStatisticLabel(midCol, displayRow, osdTimerSourceNames[OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_2])], buff);
821 return true;
823 case OSD_STAT_MAX_ALTITUDE: {
824 osdPrintFloat(buff, SYM_NONE, osdGetMetersToSelectedUnit(stats.max_altitude) / 100.0f, "", 1, true, osdGetMetersToSelectedUnitSymbol());
825 osdDisplayStatisticLabel(midCol, displayRow, "MAX ALTITUDE", buff);
826 return true;
829 #ifdef USE_GPS
830 case OSD_STAT_MAX_SPEED:
831 if (featureIsEnabled(FEATURE_GPS)) {
832 tfp_sprintf(buff, "%d%c", osdGetSpeedToSelectedUnit(stats.max_speed), osdGetSpeedToSelectedUnitSymbol());
833 osdDisplayStatisticLabel(midCol, displayRow, "MAX SPEED", buff);
834 return true;
836 break;
838 case OSD_STAT_MAX_DISTANCE:
839 if (featureIsEnabled(FEATURE_GPS)) {
840 osdFormatDistanceString(buff, stats.max_distance, SYM_NONE);
841 osdDisplayStatisticLabel(midCol, displayRow, "MAX DISTANCE", buff);
842 return true;
844 break;
846 case OSD_STAT_FLIGHT_DISTANCE:
847 if (featureIsEnabled(FEATURE_GPS)) {
848 const int distanceFlown = GPS_distanceFlownInCm / 100;
849 osdFormatDistanceString(buff, distanceFlown, SYM_NONE);
850 osdDisplayStatisticLabel(midCol, displayRow, "FLIGHT DISTANCE", buff);
851 return true;
853 break;
854 #endif
856 case OSD_STAT_MIN_BATTERY:
857 osdPrintFloat(buff, SYM_NONE, stats.min_voltage / 100.0f, "", 2, true, SYM_VOLT);
858 osdDisplayStatisticLabel(midCol, displayRow, osdConfig()->stat_show_cell_value? "MIN AVG CELL" : "MIN BATTERY", buff);
859 return true;
861 case OSD_STAT_END_BATTERY:
862 osdPrintFloat(buff, SYM_NONE, stats.end_voltage / 100.0f, "", 2, true, SYM_VOLT);
863 osdDisplayStatisticLabel(midCol, displayRow, osdConfig()->stat_show_cell_value ? "END AVG CELL" : "END BATTERY", buff);
864 return true;
866 case OSD_STAT_BATTERY:
868 const uint16_t statsVoltage = getStatsVoltage();
869 osdPrintFloat(buff, SYM_NONE, statsVoltage / 100.0f, "", 2, true, SYM_VOLT);
870 osdDisplayStatisticLabel(midCol, displayRow, osdConfig()->stat_show_cell_value ? "AVG BATT CELL" : "BATTERY", buff);
871 return true;
873 break;
875 case OSD_STAT_MIN_RSSI:
876 itoa(stats.min_rssi, buff, 10);
877 strcat(buff, "%");
878 osdDisplayStatisticLabel(midCol, displayRow, "MIN RSSI", buff);
879 return true;
881 case OSD_STAT_MAX_CURRENT:
882 if (batteryConfig()->currentMeterSource != CURRENT_METER_NONE) {
883 tfp_sprintf(buff, "%d%c", stats.max_current, SYM_AMP);
884 osdDisplayStatisticLabel(midCol, displayRow, "MAX CURRENT", buff);
885 return true;
887 break;
889 case OSD_STAT_USED_MAH:
890 if (batteryConfig()->currentMeterSource != CURRENT_METER_NONE) {
891 tfp_sprintf(buff, "%d%c", getMAhDrawn(), SYM_MAH);
892 osdDisplayStatisticLabel(midCol, displayRow, "USED MAH", buff);
893 return true;
895 break;
897 case OSD_STAT_WATT_HOURS_DRAWN:
898 if (batteryConfig()->currentMeterSource != CURRENT_METER_NONE) {
899 osdPrintFloat(buff, SYM_NONE, getWhDrawn(), "", 2, true, SYM_NONE);
900 osdDisplayStatisticLabel(midCol, displayRow, "USED WATT HOURS", buff);
901 return true;
903 break;
905 #ifdef USE_BLACKBOX
906 case OSD_STAT_BLACKBOX:
907 if (blackboxConfig()->device && blackboxConfig()->device != BLACKBOX_DEVICE_SERIAL) {
908 osdGetBlackboxStatusString(buff);
909 osdDisplayStatisticLabel(midCol, displayRow, "BLACKBOX", buff);
910 return true;
912 break;
914 case OSD_STAT_BLACKBOX_NUMBER:
916 int32_t logNumber = blackboxGetLogNumber();
917 if (logNumber >= 0) {
918 itoa(logNumber, buff, 10);
919 osdDisplayStatisticLabel(midCol, displayRow, "BB LOG NUM", buff);
920 return true;
923 break;
924 #endif
926 #if defined(USE_ACC)
927 case OSD_STAT_MAX_G_FORCE:
928 if (sensors(SENSOR_ACC)) {
929 osdPrintFloat(buff, SYM_NONE, stats.max_g_force, "", 1, true, 'G');
930 osdDisplayStatisticLabel(midCol, displayRow, "MAX G-FORCE", buff);
931 return true;
933 break;
934 #endif
936 #ifdef USE_ESC_SENSOR
937 case OSD_STAT_MAX_ESC_TEMP:
939 uint16_t ix = 0;
940 if (stats.max_esc_temp_ix > 0) {
941 ix = tfp_sprintf(buff, "%d ", stats.max_esc_temp_ix);
943 tfp_sprintf(buff + ix, "%d%c", osdConvertTemperatureToSelectedUnit(stats.max_esc_temp), osdGetTemperatureSymbolForSelectedUnit());
944 osdDisplayStatisticLabel(midCol, displayRow, "MAX ESC TEMP", buff);
945 return true;
947 #endif
949 #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY)
950 case OSD_STAT_MAX_ESC_RPM:
951 itoa(stats.max_esc_rpm, buff, 10);
952 osdDisplayStatisticLabel(midCol, displayRow, "MAX ESC RPM", buff);
953 return true;
954 #endif
956 #ifdef USE_RX_LINK_QUALITY_INFO
957 case OSD_STAT_MIN_LINK_QUALITY:
958 tfp_sprintf(buff, "%d", stats.min_link_quality);
959 strcat(buff, "%");
960 osdDisplayStatisticLabel(midCol, displayRow, "MIN LINK", buff);
961 return true;
962 #endif
964 #if defined(USE_DYN_NOTCH_FILTER)
965 case OSD_STAT_MAX_FFT:
966 if (isDynNotchActive()) {
967 int value = getMaxFFT();
968 if (value > 0) {
969 tfp_sprintf(buff, "%dHZ", value);
970 osdDisplayStatisticLabel(midCol, displayRow, "PEAK FFT", buff);
971 } else {
972 osdDisplayStatisticLabel(midCol, displayRow, "PEAK FFT", "THRT<20%");
974 return true;
976 break;
977 #endif
979 #ifdef USE_RX_RSSI_DBM
980 case OSD_STAT_MIN_RSSI_DBM:
981 tfp_sprintf(buff, "%3d", stats.min_rssi_dbm);
982 osdDisplayStatisticLabel(midCol, displayRow, "MIN RSSI DBM", buff);
983 return true;
984 #endif
986 #ifdef USE_RX_RSNR
987 case OSD_STAT_MIN_RSNR:
988 tfp_sprintf(buff, "%3d", stats.min_rsnr);
989 osdDisplayStatisticLabel(midCol, displayRow, "MIN RSNR", buff);
990 return true;
991 #endif
993 #ifdef USE_GPS_LAP_TIMER
994 case OSD_STAT_BEST_3_CONSEC_LAPS: {
995 printLapTime(buff, gpsLapTimerData.best3Consec);
996 osdDisplayStatisticLabel(midCol, displayRow, "BEST 3 CON", buff);
997 return true;
1000 case OSD_STAT_BEST_LAP: {
1001 printLapTime(buff, gpsLapTimerData.bestLapTime);
1002 osdDisplayStatisticLabel(midCol, displayRow, "BEST LAP", buff);
1003 return true;
1005 #endif // USE_GPS_LAP_TIMER
1007 #ifdef USE_PERSISTENT_STATS
1008 case OSD_STAT_TOTAL_FLIGHTS:
1009 itoa(statsConfig()->stats_total_flights, buff, 10);
1010 osdDisplayStatisticLabel(midCol, displayRow, "TOTAL FLIGHTS", buff);
1011 return true;
1013 case OSD_STAT_TOTAL_TIME: {
1014 int minutes = statsConfig()->stats_total_time_s / 60;
1015 tfp_sprintf(buff, "%d:%02dH", minutes / 60, minutes % 60);
1016 osdDisplayStatisticLabel(midCol, displayRow, "TOTAL FLIGHT TIME", buff);
1017 return true;
1020 case OSD_STAT_TOTAL_DIST:
1021 #define METERS_PER_KILOMETER 1000
1022 #define METERS_PER_MILE 1609
1023 if (osdConfig()->units == UNIT_IMPERIAL) {
1024 tfp_sprintf(buff, "%d%c", statsConfig()->stats_total_dist_m / METERS_PER_MILE, SYM_MILES);
1025 } else {
1026 tfp_sprintf(buff, "%d%c", statsConfig()->stats_total_dist_m / METERS_PER_KILOMETER, SYM_KM);
1028 osdDisplayStatisticLabel(midCol, displayRow, "TOTAL DISTANCE", buff);
1029 return true;
1030 #endif
1031 #ifdef USE_RC_STATS
1032 case OSD_STAT_FULL_THROTTLE_TIME: {
1033 int seconds = RcStatsGetFullThrottleTimeUs() / 1000000;
1034 const int minutes = seconds / 60;
1035 seconds = seconds % 60;
1036 tfp_sprintf(buff, "%02d:%02d", minutes, seconds);
1037 osdDisplayStatisticLabel(midCol, displayRow, "100% THRT TIME", buff);
1038 return true;
1041 case OSD_STAT_FULL_THROTTLE_COUNTER: {
1042 itoa(RcStatsGetFullThrottleCounter(), buff, 10);
1043 osdDisplayStatisticLabel(midCol, displayRow, "100% THRT COUNT", buff);
1044 return true;
1047 case OSD_STAT_AVG_THROTTLE: {
1048 itoa(RcStatsGetAverageThrottle(), buff, 10);
1049 osdDisplayStatisticLabel(midCol, displayRow, "AVG THROTTLE", buff);
1050 return true;
1052 #endif // USE_RC_STATS
1054 return false;
1057 typedef struct osdStatsRenderingState_s {
1058 uint8_t row;
1059 uint8_t index;
1060 uint8_t rowCount;
1061 } osdStatsRenderingState_t;
1063 static osdStatsRenderingState_t osdStatsRenderingState;
1065 static void osdRenderStatsReset(void)
1067 // reset to 0 so it will be recalculated on the next stats refresh
1068 osdStatsRenderingState.rowCount = 0;
1071 static void osdRenderStatsBegin(void)
1073 osdStatsRenderingState.row = 0;
1074 osdStatsRenderingState.index = 0;
1078 // call repeatedly until it returns true which indicates that all stats have been rendered.
1079 static bool osdRenderStatsContinue(void)
1081 uint8_t midCol = osdDisplayPort->cols / 2;
1083 if (osdStatsRenderingState.row == 0) {
1085 bool displayLabel = false;
1087 // if rowCount is 0 then we're running an initial analysis of the active stats items
1088 if (osdStatsRenderingState.rowCount > 0) {
1089 const int availableRows = osdDisplayPort->rows;
1090 int displayRows = MIN(osdStatsRenderingState.rowCount, availableRows);
1091 if (osdStatsRenderingState.rowCount < availableRows) {
1092 displayLabel = true;
1093 displayRows++;
1095 osdStatsRenderingState.row = (availableRows - displayRows) / 2; // center the stats vertically
1098 if (displayLabel) {
1099 displayWrite(osdDisplayPort, midCol - (strlen("--- STATS ---") / 2), osdStatsRenderingState.row++, DISPLAYPORT_SEVERITY_NORMAL, "--- STATS ---");
1100 return false;
1105 bool renderedStat = false;
1107 while (osdStatsRenderingState.index < OSD_STAT_COUNT) {
1108 int index = osdStatsRenderingState.index;
1110 // prepare for the next call to the method
1111 osdStatsRenderingState.index++;
1113 // look for something to render
1114 if (osdStatGetState(osdStatsDisplayOrder[index])) {
1115 if (osdDisplayStat(osdStatsDisplayOrder[index], osdStatsRenderingState.row)) {
1116 osdStatsRenderingState.row++;
1117 renderedStat = true;
1118 break;
1123 bool moreSpaceAvailable = osdStatsRenderingState.row < osdDisplayPort->rows;
1125 if (renderedStat && moreSpaceAvailable) {
1126 return false;
1129 if (osdStatsRenderingState.rowCount == 0) {
1130 osdStatsRenderingState.rowCount = osdStatsRenderingState.row;
1133 return true;
1136 // returns true when all phases are complete
1137 static bool osdRefreshStats(void)
1139 bool completed = false;
1141 typedef enum {
1142 INITIAL_CLEAR_SCREEN = 0,
1143 COUNT_STATS,
1144 CLEAR_SCREEN,
1145 RENDER_STATS,
1146 } osdRefreshStatsPhase_e;
1148 static osdRefreshStatsPhase_e phase = INITIAL_CLEAR_SCREEN;
1150 switch (phase) {
1151 default:
1152 case INITIAL_CLEAR_SCREEN:
1153 osdRenderStatsBegin();
1154 if (osdStatsRenderingState.rowCount > 0) {
1155 phase = RENDER_STATS;
1156 } else {
1157 phase = COUNT_STATS;
1159 displayClearScreen(osdDisplayPort, DISPLAY_CLEAR_NONE);
1160 break;
1161 case COUNT_STATS:
1163 // No stats row count has been set yet.
1164 // Go through the logic one time to determine how many stats are actually displayed.
1165 bool count_phase_complete = osdRenderStatsContinue();
1166 if (count_phase_complete) {
1167 phase = CLEAR_SCREEN;
1169 break;
1171 case CLEAR_SCREEN:
1172 osdRenderStatsBegin();
1173 // Then clear the screen and commence with normal stats display which will
1174 // determine if the heading should be displayed and also center the content vertically.
1175 displayClearScreen(osdDisplayPort, DISPLAY_CLEAR_NONE);
1176 phase = RENDER_STATS;
1177 break;
1178 case RENDER_STATS:
1179 completed = osdRenderStatsContinue();
1180 break;
1183 if (completed) {
1184 phase = INITIAL_CLEAR_SCREEN;
1187 return completed;
1190 static timeDelta_t osdShowArmed(void)
1192 uint8_t midRow = osdDisplayPort->rows / 2;
1193 uint8_t midCol = osdDisplayPort->cols / 2;
1194 timeDelta_t ret;
1196 displayClearScreen(osdDisplayPort, DISPLAY_CLEAR_WAIT);
1198 if ((osdConfig()->logo_on_arming == OSD_LOGO_ARMING_ON) || ((osdConfig()->logo_on_arming == OSD_LOGO_ARMING_FIRST) && !ARMING_FLAG(WAS_EVER_ARMED))) {
1199 uint8_t midRow = osdDisplayPort->rows / 2;
1200 uint8_t midCol = osdDisplayPort->cols / 2;
1201 osdDrawLogo(midCol - (OSD_LOGO_COLS) / 2, midRow - 5);
1202 ret = osdConfig()->logo_on_arming_duration * 1e5;
1203 } else {
1204 ret = (REFRESH_1S / 2);
1206 displayWrite(osdDisplayPort, midCol - (strlen("ARMED") / 2), midRow, DISPLAYPORT_SEVERITY_NORMAL, "ARMED");
1208 if (isFlipOverAfterCrashActive()) {
1209 displayWrite(osdDisplayPort, midCol - (strlen(CRASH_FLIP_WARNING) / 2), midRow + 1, DISPLAYPORT_SEVERITY_NORMAL, CRASH_FLIP_WARNING);
1212 return ret;
1215 static bool osdStatsVisible = false;
1216 static bool osdStatsEnabled = false;
1218 STATIC_UNIT_TESTED bool osdProcessStats1(timeUs_t currentTimeUs)
1220 static timeUs_t lastTimeUs = 0;
1221 static timeUs_t osdStatsRefreshTimeUs;
1222 static timeUs_t osdAuxRefreshTimeUs = 0;
1224 bool refreshStatsRequired = false;
1226 // detect arm/disarm
1227 if (armState != ARMING_FLAG(ARMED)) {
1228 if (ARMING_FLAG(ARMED)) {
1229 osdStatsEnabled = false;
1230 osdStatsVisible = false;
1231 osdResetStats();
1232 resumeRefreshAt = osdShowArmed() + currentTimeUs;
1233 } else if (isSomeStatEnabled()
1234 && !suppressStatsDisplay
1235 && !failsafeIsActive()
1236 && (!(getArmingDisableFlags() & (ARMING_DISABLED_RUNAWAY_TAKEOFF | ARMING_DISABLED_CRASH_DETECTED))
1237 || !VISIBLE(osdElementConfig()->item_pos[OSD_WARNINGS]))) { // suppress stats if runaway takeoff triggered disarm and WARNINGS element is visible
1238 osdStatsEnabled = true;
1239 resumeRefreshAt = currentTimeUs + (60 * REFRESH_1S);
1240 stats.end_voltage = getStatsVoltage();
1241 osdRenderStatsReset();
1244 armState = ARMING_FLAG(ARMED);
1247 if (ARMING_FLAG(ARMED)) {
1248 osdUpdateStats();
1249 timeUs_t deltaT = currentTimeUs - lastTimeUs;
1250 osdFlyTime += deltaT;
1251 stats.armed_time += deltaT;
1252 } else if (osdStatsEnabled) { // handle showing/hiding stats based on OSD disable switch position
1253 if (displayIsGrabbed(osdDisplayPort)) {
1254 osdStatsEnabled = false;
1255 resumeRefreshAt = 0;
1256 stats.armed_time = 0;
1257 } else {
1258 if (IS_RC_MODE_ACTIVE(BOXOSD) && osdStatsVisible) {
1259 osdStatsVisible = false;
1260 displayClearScreen(osdDisplayPort, DISPLAY_CLEAR_NONE);
1261 } else if (!IS_RC_MODE_ACTIVE(BOXOSD)) {
1262 if (!osdStatsVisible) {
1263 osdStatsVisible = true;
1264 osdStatsRefreshTimeUs = 0;
1266 if (currentTimeUs >= osdStatsRefreshTimeUs) {
1267 osdStatsRefreshTimeUs = currentTimeUs + REFRESH_1S;
1268 refreshStatsRequired = true;
1274 if (VISIBLE(osdElementConfig()->item_pos[OSD_AUX_VALUE])) {
1275 const uint8_t auxChannel = osdConfig()->aux_channel + NON_AUX_CHANNEL_COUNT - 1;
1276 if (currentTimeUs > osdAuxRefreshTimeUs) {
1277 // aux channel start after main channels
1278 osdAuxValue = (constrain(rcData[auxChannel], PWM_RANGE_MIN, PWM_RANGE_MAX) - PWM_RANGE_MIN) * osdConfig()->aux_scale / PWM_RANGE;
1279 osdAuxRefreshTimeUs = currentTimeUs + REFRESH_1S;
1283 lastTimeUs = currentTimeUs;
1285 return refreshStatsRequired;
1288 void osdProcessStats2(timeUs_t currentTimeUs)
1290 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
1292 if (resumeRefreshAt) {
1293 if (cmp32(currentTimeUs, resumeRefreshAt) < 0) {
1294 // in timeout period, check sticks for activity or CRASH FLIP switch to resume display.
1295 if (!ARMING_FLAG(ARMED) &&
1296 (IS_HI(THROTTLE) || IS_HI(PITCH) || IS_RC_MODE_ACTIVE(BOXFLIPOVERAFTERCRASH))) {
1297 resumeRefreshAt = currentTimeUs;
1299 return;
1300 } else {
1301 displayClearScreen(osdDisplayPort, DISPLAY_CLEAR_NONE);
1302 resumeRefreshAt = 0;
1303 osdStatsEnabled = false;
1304 stats.armed_time = 0;
1307 schedulerIgnoreTaskExecTime();
1309 #ifdef USE_ESC_SENSOR
1310 if (featureIsEnabled(FEATURE_ESC_SENSOR)) {
1311 osdEscDataCombined = getEscSensorData(ESC_SENSOR_COMBINED);
1313 #endif
1316 void osdProcessStats3(void)
1318 #if defined(USE_ACC)
1319 osdGForce = 0.0f;
1320 if (sensors(SENSOR_ACC)
1321 && (VISIBLE(osdElementConfig()->item_pos[OSD_G_FORCE]) || osdStatGetState(OSD_STAT_MAX_G_FORCE))) {
1322 // only calculate the G force if the element is visible or the stat is enabled
1323 for (int axis = 0; axis < XYZ_AXIS_COUNT; axis++) {
1324 const float a = acc.accADC[axis];
1325 osdGForce += a * a;
1327 osdGForce = sqrtf(osdGForce) * acc.dev.acc_1G_rec;
1329 #endif
1332 typedef enum {
1333 OSD_STATE_INIT,
1334 OSD_STATE_IDLE,
1335 OSD_STATE_CHECK,
1336 OSD_STATE_PROCESS_STATS1,
1337 OSD_STATE_REFRESH_STATS,
1338 OSD_STATE_PROCESS_STATS2,
1339 OSD_STATE_PROCESS_STATS3,
1340 OSD_STATE_UPDATE_ALARMS,
1341 OSD_STATE_UPDATE_CANVAS,
1342 OSD_STATE_GROUP_ELEMENTS,
1343 OSD_STATE_UPDATE_ELEMENTS,
1344 OSD_STATE_UPDATE_HEARTBEAT,
1345 OSD_STATE_COMMIT,
1346 OSD_STATE_TRANSFER,
1347 OSD_STATE_COUNT
1348 } osdState_e;
1350 osdState_e osdState = OSD_STATE_INIT;
1352 #define OSD_UPDATE_INTERVAL_US (1000000 / osdConfig()->framerate_hz)
1354 // Called periodically by the scheduler
1355 bool osdUpdateCheck(timeUs_t currentTimeUs, timeDelta_t currentDeltaTimeUs)
1357 UNUSED(currentDeltaTimeUs);
1358 static timeUs_t osdUpdateDueUs = 0;
1360 if (osdState == OSD_STATE_IDLE) {
1361 // If the OSD is due a refresh, mark that as being the case
1362 if (cmpTimeUs(currentTimeUs, osdUpdateDueUs) > 0) {
1363 osdState = OSD_STATE_CHECK;
1365 // Determine time of next update
1366 if (osdUpdateDueUs) {
1367 osdUpdateDueUs += OSD_UPDATE_INTERVAL_US;
1368 } else {
1369 osdUpdateDueUs = currentTimeUs + OSD_UPDATE_INTERVAL_US;
1374 return (osdState != OSD_STATE_IDLE);
1377 // Called when there is OSD update work to be done
1378 void osdUpdate(timeUs_t currentTimeUs)
1380 static uint16_t osdStateDurationFractionUs[OSD_STATE_COUNT] = { 0 };
1381 static uint32_t osdElementDurationUs[OSD_ITEM_COUNT] = { 0 };
1382 static uint8_t osdElementGroupMemberships[OSD_ITEM_COUNT];
1383 static uint16_t osdElementGroupTargetFractionUs[OSD_GROUP_COUNT] = { 0 };
1384 static uint16_t osdElementGroupDurationFractionUs[OSD_GROUP_COUNT] = { 0 };
1385 static uint8_t osdElementGroup;
1386 static bool firstPass = true;
1387 uint8_t osdCurrentElementGroup = 0;
1388 timeUs_t executeTimeUs;
1389 osdState_e osdCurrentState = osdState;
1391 if (osdState != OSD_STATE_UPDATE_CANVAS) {
1392 schedulerIgnoreTaskExecRate();
1395 switch (osdState) {
1396 case OSD_STATE_INIT:
1397 if (!displayCheckReady(osdDisplayPort, false)) {
1398 // Frsky osd need a display redraw after search for MAX7456 devices
1399 if (osdDisplayPortDeviceType == OSD_DISPLAYPORT_DEVICE_FRSKYOSD) {
1400 displayRedraw(osdDisplayPort);
1401 } else {
1402 schedulerIgnoreTaskExecTime();
1404 return;
1407 osdCompleteInitialization();
1408 displayRedraw(osdDisplayPort);
1409 osdState = OSD_STATE_COMMIT;
1411 break;
1413 case OSD_STATE_CHECK:
1414 // don't touch buffers if DMA transaction is in progress
1415 if (displayIsTransferInProgress(osdDisplayPort)) {
1416 break;
1419 osdState = OSD_STATE_UPDATE_HEARTBEAT;
1420 break;
1422 case OSD_STATE_UPDATE_HEARTBEAT:
1423 if (displayHeartbeat(osdDisplayPort)) {
1424 // Extraordinary action was taken, so return without allowing osdStateDurationFractionUs table to be updated
1425 return;
1428 osdState = OSD_STATE_PROCESS_STATS1;
1429 break;
1431 case OSD_STATE_PROCESS_STATS1:
1433 bool refreshStatsRequired = osdProcessStats1(currentTimeUs);
1435 if (refreshStatsRequired) {
1436 osdState = OSD_STATE_REFRESH_STATS;
1437 } else {
1438 osdState = OSD_STATE_PROCESS_STATS2;
1440 break;
1442 case OSD_STATE_REFRESH_STATS:
1444 bool completed = osdRefreshStats();
1445 if (completed) {
1446 osdState = OSD_STATE_PROCESS_STATS2;
1448 break;
1450 case OSD_STATE_PROCESS_STATS2:
1451 osdProcessStats2(currentTimeUs);
1453 osdState = OSD_STATE_PROCESS_STATS3;
1454 break;
1455 case OSD_STATE_PROCESS_STATS3:
1456 osdProcessStats3();
1458 #ifdef USE_CMS
1459 if (!displayIsGrabbed(osdDisplayPort))
1460 #endif
1462 osdState = OSD_STATE_UPDATE_ALARMS;
1463 break;
1466 osdState = OSD_STATE_COMMIT;
1467 break;
1469 case OSD_STATE_UPDATE_ALARMS:
1470 osdUpdateAlarms();
1472 if (resumeRefreshAt) {
1473 osdState = OSD_STATE_TRANSFER;
1474 } else {
1475 osdState = OSD_STATE_UPDATE_CANVAS;
1477 break;
1479 case OSD_STATE_UPDATE_CANVAS:
1480 // Hide OSD when OSDSW mode is active
1481 if (IS_RC_MODE_ACTIVE(BOXOSD)) {
1482 displayClearScreen(osdDisplayPort, DISPLAY_CLEAR_NONE);
1483 osdState = OSD_STATE_COMMIT;
1484 break;
1487 if (backgroundLayerSupported) {
1488 // Background layer is supported, overlay it onto the foreground
1489 // so that we only need to draw the active parts of the elements.
1490 displayLayerCopy(osdDisplayPort, DISPLAYPORT_LAYER_FOREGROUND, DISPLAYPORT_LAYER_BACKGROUND);
1491 } else {
1492 // Background layer not supported, just clear the foreground in preparation
1493 // for drawing the elements including their backgrounds.
1494 displayClearScreen(osdDisplayPort, DISPLAY_CLEAR_NONE);
1497 #ifdef USE_GPS
1498 static bool lastGpsSensorState;
1499 // Handle the case that the GPS_SENSOR may be delayed in activation
1500 // or deactivate if communication is lost with the module.
1501 const bool currentGpsSensorState = sensors(SENSOR_GPS);
1502 if (lastGpsSensorState != currentGpsSensorState) {
1503 lastGpsSensorState = currentGpsSensorState;
1504 osdAnalyzeActiveElements();
1506 #endif // USE_GPS
1508 osdSyncBlink();
1510 osdState = OSD_STATE_GROUP_ELEMENTS;
1512 break;
1514 case OSD_STATE_GROUP_ELEMENTS:
1516 uint8_t elementGroup;
1517 uint8_t activeElements = osdGetActiveElementCount();
1519 // Reset groupings
1520 for (elementGroup = 0; elementGroup < OSD_GROUP_COUNT; elementGroup++) {
1521 if (osdElementGroupDurationFractionUs[elementGroup] > (OSD_ELEMENT_RENDER_TARGET << OSD_EXEC_TIME_SHIFT)) {
1522 osdElementGroupDurationFractionUs[elementGroup] = 0;
1524 osdElementGroupTargetFractionUs[elementGroup] = 0;
1527 elementGroup = 0;
1529 // Based on the current element rendering, group to execute in approx 40us
1530 for (uint8_t curElement = 0; curElement < activeElements; curElement++) {
1531 if ((osdElementGroupTargetFractionUs[elementGroup] == 0) ||
1532 (osdElementGroupTargetFractionUs[elementGroup] + (osdElementDurationUs[curElement]) <= (OSD_ELEMENT_RENDER_TARGET << OSD_EXEC_TIME_SHIFT)) ||
1533 (elementGroup == (OSD_GROUP_COUNT - 1))) {
1534 osdElementGroupTargetFractionUs[elementGroup] += osdElementDurationUs[curElement];
1535 // If group membership changes, reset the stats for the group
1536 if (osdElementGroupMemberships[curElement] != elementGroup) {
1537 osdElementGroupDurationFractionUs[elementGroup] = osdElementGroupTargetFractionUs[elementGroup] + (OSD_ELEMENT_RENDER_GROUP_MARGIN << OSD_EXEC_TIME_SHIFT);
1539 osdElementGroupMemberships[curElement] = elementGroup;
1540 } else {
1541 elementGroup++;
1542 // Try again for this element
1543 curElement--;
1547 // Start with group 0
1548 osdElementGroup = 0;
1550 if (activeElements > 0) {
1551 osdState = OSD_STATE_UPDATE_ELEMENTS;
1552 } else {
1553 osdState = OSD_STATE_COMMIT;
1556 break;
1558 case OSD_STATE_UPDATE_ELEMENTS:
1560 osdCurrentElementGroup = osdElementGroup;
1561 bool moreElements = true;
1563 do {
1564 timeUs_t startElementTime = micros();
1565 uint8_t osdCurrentElement = osdGetActiveElement();
1567 // This element should be rendered in the next group
1568 if (osdElementGroupMemberships[osdCurrentElement] != osdElementGroup) {
1569 osdElementGroup++;
1570 break;
1573 moreElements = osdDrawNextActiveElement(osdDisplayPort, currentTimeUs);
1575 executeTimeUs = micros() - startElementTime;
1577 if (executeTimeUs > (osdElementDurationUs[osdCurrentElement] >> OSD_EXEC_TIME_SHIFT)) {
1578 osdElementDurationUs[osdCurrentElement] = executeTimeUs << OSD_EXEC_TIME_SHIFT;
1579 } else if (osdElementDurationUs[osdCurrentElement] > 0) {
1580 // Slowly decay the max time
1581 osdElementDurationUs[osdCurrentElement]--;
1583 } while (moreElements);
1585 if (moreElements) {
1586 // There are more elements to draw
1587 break;
1589 #ifdef USE_SPEC_PREARM_SCREEN
1590 osdDrawSpec(osdDisplayPort);
1591 #endif // USE_SPEC_PREARM_SCREEN
1593 osdElementGroup = 0;
1595 osdState = OSD_STATE_COMMIT;
1597 break;
1599 case OSD_STATE_COMMIT:
1600 displayCommitTransaction(osdDisplayPort);
1602 if (resumeRefreshAt) {
1603 osdState = OSD_STATE_IDLE;
1604 } else {
1605 osdState = OSD_STATE_TRANSFER;
1607 break;
1609 case OSD_STATE_TRANSFER:
1610 // Wait for any current transfer to complete
1611 if (displayIsTransferInProgress(osdDisplayPort)) {
1612 break;
1615 // Transfer may be broken into many parts
1616 if (displayDrawScreen(osdDisplayPort)) {
1617 break;
1620 firstPass = false;
1621 osdState = OSD_STATE_IDLE;
1623 break;
1625 case OSD_STATE_IDLE:
1626 default:
1627 osdState = OSD_STATE_IDLE;
1628 break;
1631 if (!schedulerGetIgnoreTaskExecTime()) {
1632 executeTimeUs = micros() - currentTimeUs;
1635 // On the first pass no element groups will have been formed, so all elements will have been
1636 // rendered which is unrepresentative, so ignore
1637 if (!firstPass) {
1638 if (osdCurrentState == OSD_STATE_UPDATE_ELEMENTS) {
1639 if (executeTimeUs > (osdElementGroupDurationFractionUs[osdCurrentElementGroup] >> OSD_EXEC_TIME_SHIFT)) {
1640 osdElementGroupDurationFractionUs[osdCurrentElementGroup] = executeTimeUs << OSD_EXEC_TIME_SHIFT;
1641 } else if (osdElementGroupDurationFractionUs[osdCurrentElementGroup] > 0) {
1642 // Slowly decay the max time
1643 osdElementGroupDurationFractionUs[osdCurrentElementGroup]--;
1647 if (executeTimeUs > (osdStateDurationFractionUs[osdCurrentState] >> OSD_EXEC_TIME_SHIFT)) {
1648 osdStateDurationFractionUs[osdCurrentState] = executeTimeUs << OSD_EXEC_TIME_SHIFT;
1649 } else if (osdStateDurationFractionUs[osdCurrentState] > 0) {
1650 // Slowly decay the max time
1651 osdStateDurationFractionUs[osdCurrentState]--;
1656 if (osdState == OSD_STATE_UPDATE_ELEMENTS) {
1657 schedulerSetNextStateTime((osdElementGroupDurationFractionUs[osdElementGroup] >> OSD_EXEC_TIME_SHIFT) + OSD_ELEMENT_RENDER_GROUP_MARGIN);
1658 } else {
1659 if (osdState == OSD_STATE_IDLE) {
1660 schedulerSetNextStateTime((osdStateDurationFractionUs[OSD_STATE_CHECK] >> OSD_EXEC_TIME_SHIFT) + OSD_TASK_MARGIN);
1661 } else {
1662 schedulerSetNextStateTime((osdStateDurationFractionUs[osdState] >> OSD_EXEC_TIME_SHIFT) + OSD_TASK_MARGIN);
1667 void osdSuppressStats(bool flag)
1669 suppressStatsDisplay = flag;
1672 #ifdef USE_OSD_PROFILES
1673 bool osdElementVisible(uint16_t value)
1675 return (bool)((((value & OSD_PROFILE_MASK) >> OSD_PROFILE_BITS_POS) & osdProfile) != 0);
1677 #endif
1679 bool osdGetVisualBeeperState(void)
1681 return showVisualBeeper;
1684 void osdSetVisualBeeperState(bool state)
1686 showVisualBeeper = state;
1689 statistic_t *osdGetStats(void)
1691 return &stats;
1694 #ifdef USE_ACC
1695 // Determine if there are any enabled stats that need
1696 // the ACC (currently only MAX_G_FORCE).
1697 static bool osdStatsNeedAccelerometer(void)
1699 return osdStatGetState(OSD_STAT_MAX_G_FORCE);
1702 // Check if any enabled elements or stats need the ACC
1703 bool osdNeedsAccelerometer(void)
1705 return osdStatsNeedAccelerometer() || osdElementsNeedAccelerometer();
1707 #endif // USE_ACC
1709 displayPort_t *osdGetDisplayPort(osdDisplayPortDevice_e *displayPortDeviceType)
1711 if (displayPortDeviceType) {
1712 *displayPortDeviceType = osdDisplayPortDeviceType;
1714 return osdDisplayPort;
1717 #endif // USE_OSD