Added option to display RX SNR dB for CRSF instead of RSSI dBm.
[betaflight.git] / src / main / osd / osd.c
blob4c5036ef0e04fc4e969159a167ccd4b8a717d718
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"
53 #include "config/feature.h"
55 #include "drivers/display.h"
56 #include "drivers/flash.h"
57 #include "drivers/osd_symbols.h"
58 #include "drivers/sdcard.h"
59 #include "drivers/time.h"
61 #include "fc/rc_controls.h"
62 #include "fc/rc_modes.h"
63 #include "fc/runtime_config.h"
65 #if defined(USE_GYRO_DATA_ANALYSE)
66 #include "flight/gyroanalyse.h"
67 #endif
68 #include "flight/imu.h"
69 #include "flight/position.h"
71 #include "io/asyncfatfs/asyncfatfs.h"
72 #include "io/beeper.h"
73 #include "io/flashfs.h"
74 #include "io/gps.h"
76 #include "osd/osd.h"
77 #include "osd/osd_elements.h"
79 #include "pg/pg.h"
80 #include "pg/pg_ids.h"
81 #include "pg/stats.h"
83 #include "rx/crsf.h"
84 #include "rx/rx.h"
86 #include "sensors/acceleration.h"
87 #include "sensors/battery.h"
88 #include "sensors/esc_sensor.h"
89 #include "sensors/sensors.h"
91 #ifdef USE_HARDWARE_REVISION_DETECTION
92 #include "hardware_revision.h"
93 #endif
95 typedef enum {
96 OSD_LOGO_ARMING_OFF,
97 OSD_LOGO_ARMING_ON,
98 OSD_LOGO_ARMING_FIRST
99 } osd_logo_on_arming_e;
101 const char * const osdTimerSourceNames[] = {
102 "ON TIME ",
103 "TOTAL ARM",
104 "LAST ARM ",
105 "ON/ARM "
108 // Things in both OSD and CMS
110 #define IS_HI(X) (rcData[X] > 1750)
111 #define IS_LO(X) (rcData[X] < 1250)
112 #define IS_MID(X) (rcData[X] > 1250 && rcData[X] < 1750)
114 timeUs_t osdFlyTime = 0;
115 #if defined(USE_ACC)
116 float osdGForce = 0;
117 #endif
119 static bool showVisualBeeper = false;
121 static statistic_t stats;
122 timeUs_t resumeRefreshAt = 0;
123 #define REFRESH_1S 1000 * 1000
125 static uint8_t armState;
126 #ifdef USE_OSD_PROFILES
127 static uint8_t osdProfile = 1;
128 #endif
129 static displayPort_t *osdDisplayPort;
130 static osdDisplayPortDevice_e osdDisplayPortDevice;
131 static bool osdIsReady;
133 static bool suppressStatsDisplay = false;
134 static uint8_t osdStatsRowCount = 0;
136 static bool backgroundLayerSupported = false;
138 #ifdef USE_ESC_SENSOR
139 escSensorData_t *osdEscDataCombined;
140 #endif
142 STATIC_ASSERT(OSD_POS_MAX == OSD_POS(31,31), OSD_POS_MAX_incorrect);
144 PG_REGISTER_WITH_RESET_FN(osdConfig_t, osdConfig, PG_OSD_CONFIG, 8);
146 PG_REGISTER_WITH_RESET_FN(osdElementConfig_t, osdElementConfig, PG_OSD_ELEMENT_CONFIG, 0);
148 // Controls the display order of the OSD post-flight statistics.
149 // Adjust the ordering here to control how the post-flight stats are presented.
150 // Every entry in osd_stats_e should be represented. Any that are missing will not
151 // be shown on the the post-flight statistics page.
152 // If you reorder the stats it's likely that you'll need to make likewise updates
153 // to the unit tests.
155 // If adding new stats, please add to the osdStatsNeedAccelerometer() function
156 // if the statistic utilizes the accelerometer.
158 const osd_stats_e osdStatsDisplayOrder[OSD_STAT_COUNT] = {
159 OSD_STAT_RTC_DATE_TIME,
160 OSD_STAT_TIMER_1,
161 OSD_STAT_TIMER_2,
162 OSD_STAT_MAX_ALTITUDE,
163 OSD_STAT_MAX_SPEED,
164 OSD_STAT_MAX_DISTANCE,
165 OSD_STAT_FLIGHT_DISTANCE,
166 OSD_STAT_MIN_BATTERY,
167 OSD_STAT_END_BATTERY,
168 OSD_STAT_BATTERY,
169 OSD_STAT_MIN_RSSI,
170 OSD_STAT_MAX_CURRENT,
171 OSD_STAT_USED_MAH,
172 OSD_STAT_BLACKBOX,
173 OSD_STAT_BLACKBOX_NUMBER,
174 OSD_STAT_MAX_G_FORCE,
175 OSD_STAT_MAX_ESC_TEMP,
176 OSD_STAT_MAX_ESC_RPM,
177 OSD_STAT_MIN_LINK_QUALITY,
178 OSD_STAT_MAX_FFT,
179 OSD_STAT_MIN_RSSI_DBM,
180 OSD_STAT_TOTAL_FLIGHTS,
181 OSD_STAT_TOTAL_TIME,
182 OSD_STAT_TOTAL_DIST,
185 void osdStatSetState(uint8_t statIndex, bool enabled)
187 if (enabled) {
188 osdConfigMutable()->enabled_stats |= (1 << statIndex);
189 } else {
190 osdConfigMutable()->enabled_stats &= ~(1 << statIndex);
194 bool osdStatGetState(uint8_t statIndex)
196 return osdConfig()->enabled_stats & (1 << statIndex);
199 void osdWarnSetState(uint8_t warningIndex, bool enabled)
201 if (enabled) {
202 osdConfigMutable()->enabledWarnings |= (1 << warningIndex);
203 } else {
204 osdConfigMutable()->enabledWarnings &= ~(1 << warningIndex);
208 bool osdWarnGetState(uint8_t warningIndex)
210 return osdConfig()->enabledWarnings & (1 << warningIndex);
213 #ifdef USE_OSD_PROFILES
214 void setOsdProfile(uint8_t value)
216 // 1 ->> 001
217 // 2 ->> 010
218 // 3 ->> 100
219 if (value <= OSD_PROFILE_COUNT) {
220 if (value == 0) {
221 osdProfile = 1;
222 } else {
223 osdProfile = 1 << (value - 1);
228 uint8_t getCurrentOsdProfileIndex(void)
230 return osdConfig()->osdProfileIndex;
233 void changeOsdProfileIndex(uint8_t profileIndex)
235 if (profileIndex <= OSD_PROFILE_COUNT) {
236 osdConfigMutable()->osdProfileIndex = profileIndex;
237 setOsdProfile(profileIndex);
238 osdAnalyzeActiveElements();
241 #endif
243 void osdAnalyzeActiveElements(void)
245 osdAddActiveElements();
246 osdDrawActiveElementsBackground(osdDisplayPort);
249 static void osdDrawElements(timeUs_t currentTimeUs)
251 // Hide OSD when OSDSW mode is active
252 if (IS_RC_MODE_ACTIVE(BOXOSD)) {
253 displayClearScreen(osdDisplayPort);
254 return;
257 if (backgroundLayerSupported) {
258 // Background layer is supported, overlay it onto the foreground
259 // so that we only need to draw the active parts of the elements.
260 displayLayerCopy(osdDisplayPort, DISPLAYPORT_LAYER_FOREGROUND, DISPLAYPORT_LAYER_BACKGROUND);
261 } else {
262 // Background layer not supported, just clear the foreground in preparation
263 // for drawing the elements including their backgrounds.
264 displayClearScreen(osdDisplayPort);
267 osdDrawActiveElements(osdDisplayPort, currentTimeUs);
270 const uint16_t osdTimerDefault[OSD_TIMER_COUNT] = {
271 OSD_TIMER(OSD_TIMER_SRC_ON, OSD_TIMER_PREC_SECOND, 10),
272 OSD_TIMER(OSD_TIMER_SRC_TOTAL_ARMED, OSD_TIMER_PREC_SECOND, 10)
275 void pgResetFn_osdConfig(osdConfig_t *osdConfig)
277 // Enable the default stats
278 osdConfig->enabled_stats = 0; // reset all to off and enable only a few initially
279 osdStatSetState(OSD_STAT_MAX_SPEED, true);
280 osdStatSetState(OSD_STAT_MIN_BATTERY, true);
281 osdStatSetState(OSD_STAT_MIN_RSSI, true);
282 osdStatSetState(OSD_STAT_MAX_CURRENT, true);
283 osdStatSetState(OSD_STAT_USED_MAH, true);
284 osdStatSetState(OSD_STAT_BLACKBOX, true);
285 osdStatSetState(OSD_STAT_BLACKBOX_NUMBER, true);
286 osdStatSetState(OSD_STAT_TIMER_2, true);
288 osdConfig->units = OSD_UNIT_METRIC;
290 // Enable all warnings by default
291 for (int i=0; i < OSD_WARNING_COUNT; i++) {
292 osdWarnSetState(i, true);
294 // turn off RSSI & Link Quality warnings by default
295 osdWarnSetState(OSD_WARNING_RSSI, false);
296 osdWarnSetState(OSD_WARNING_LINK_QUALITY, false);
297 osdWarnSetState(OSD_WARNING_RSSI_DBM, false);
298 // turn off the over mah capacity warning
299 osdWarnSetState(OSD_WARNING_OVER_CAP, false);
301 osdConfig->timers[OSD_TIMER_1] = osdTimerDefault[OSD_TIMER_1];
302 osdConfig->timers[OSD_TIMER_2] = osdTimerDefault[OSD_TIMER_2];
304 osdConfig->overlay_radio_mode = 2;
306 osdConfig->rssi_alarm = 20;
307 osdConfig->link_quality_alarm = 80;
308 osdConfig->cap_alarm = 2200;
309 osdConfig->alt_alarm = 100; // meters or feet depend on configuration
310 osdConfig->esc_temp_alarm = ESC_TEMP_ALARM_OFF; // off by default
311 osdConfig->esc_rpm_alarm = ESC_RPM_ALARM_OFF; // off by default
312 osdConfig->esc_current_alarm = ESC_CURRENT_ALARM_OFF; // off by default
313 osdConfig->core_temp_alarm = 70; // a temperature above 70C should produce a warning, lockups have been reported above 80C
315 osdConfig->ahMaxPitch = 20; // 20 degrees
316 osdConfig->ahMaxRoll = 40; // 40 degrees
318 osdConfig->osdProfileIndex = 1;
319 osdConfig->ahInvert = false;
320 for (int i=0; i < OSD_PROFILE_COUNT; i++) {
321 osdConfig->profile[i][0] = '\0';
323 osdConfig->rssi_dbm_alarm = -60;
324 osdConfig->gps_sats_show_hdop = false;
326 for (int i = 0; i < OSD_RCCHANNELS_COUNT; i++) {
327 osdConfig->rcChannels[i] = -1;
330 osdConfig->displayPortDevice = OSD_DISPLAYPORT_DEVICE_AUTO;
332 osdConfig->distance_alarm = 0;
333 osdConfig->logo_on_arming = OSD_LOGO_ARMING_OFF;
334 osdConfig->logo_on_arming_duration = 5; // 0.5 seconds
336 osdConfig->camera_frame_width = 24;
337 osdConfig->camera_frame_height = 11;
340 void pgResetFn_osdElementConfig(osdElementConfig_t *osdElementConfig)
342 // Position elements near centre of screen and disabled by default
343 for (int i = 0; i < OSD_ITEM_COUNT; i++) {
344 osdElementConfig->item_pos[i] = OSD_POS(10, 7);
347 // Always enable warnings elements by default
348 uint16_t profileFlags = 0;
349 for (unsigned i = 1; i <= OSD_PROFILE_COUNT; i++) {
350 profileFlags |= OSD_PROFILE_FLAG(i);
352 osdElementConfig->item_pos[OSD_WARNINGS] = OSD_POS(9, 10) | profileFlags;
354 // Default to old fixed positions for these elements
355 osdElementConfig->item_pos[OSD_CROSSHAIRS] = OSD_POS(13, 6);
356 osdElementConfig->item_pos[OSD_ARTIFICIAL_HORIZON] = OSD_POS(14, 2);
357 osdElementConfig->item_pos[OSD_HORIZON_SIDEBARS] = OSD_POS(14, 6);
358 osdElementConfig->item_pos[OSD_CAMERA_FRAME] = OSD_POS(3, 1);
361 static void osdDrawLogo(int x, int y)
363 // display logo and help
364 int fontOffset = 160;
365 for (int row = 0; row < 4; row++) {
366 for (int column = 0; column < 24; column++) {
367 if (fontOffset <= SYM_END_OF_FONT)
368 displayWriteChar(osdDisplayPort, x + column, y + row, DISPLAYPORT_ATTR_NONE, fontOffset++);
373 static void osdCompleteInitialization(void)
375 armState = ARMING_FLAG(ARMED);
377 osdResetAlarms();
379 backgroundLayerSupported = displayLayerSupported(osdDisplayPort, DISPLAYPORT_LAYER_BACKGROUND);
380 displayLayerSelect(osdDisplayPort, DISPLAYPORT_LAYER_FOREGROUND);
382 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
383 displayClearScreen(osdDisplayPort);
385 osdDrawLogo(3, 1);
387 char string_buffer[30];
388 tfp_sprintf(string_buffer, "V%s", FC_VERSION_STRING);
389 displayWrite(osdDisplayPort, 20, 6, DISPLAYPORT_ATTR_NONE, string_buffer);
390 #ifdef USE_CMS
391 displayWrite(osdDisplayPort, 7, 8, DISPLAYPORT_ATTR_NONE, CMS_STARTUP_HELP_TEXT1);
392 displayWrite(osdDisplayPort, 11, 9, DISPLAYPORT_ATTR_NONE, CMS_STARTUP_HELP_TEXT2);
393 displayWrite(osdDisplayPort, 11, 10, DISPLAYPORT_ATTR_NONE, CMS_STARTUP_HELP_TEXT3);
394 #endif
396 #ifdef USE_RTC_TIME
397 char dateTimeBuffer[FORMATTED_DATE_TIME_BUFSIZE];
398 if (osdFormatRtcDateTime(&dateTimeBuffer[0])) {
399 displayWrite(osdDisplayPort, 5, 12, DISPLAYPORT_ATTR_NONE, dateTimeBuffer);
401 #endif
403 resumeRefreshAt = micros() + (4 * REFRESH_1S);
404 #ifdef USE_OSD_PROFILES
405 setOsdProfile(osdConfig()->osdProfileIndex);
406 #endif
408 osdElementsInit(backgroundLayerSupported);
409 osdAnalyzeActiveElements();
410 displayCommitTransaction(osdDisplayPort);
412 osdIsReady = true;
415 void osdInit(displayPort_t *osdDisplayPortToUse, osdDisplayPortDevice_e displayPortDeviceToUse)
417 if (!osdDisplayPortToUse) {
418 return;
421 osdDisplayPort = osdDisplayPortToUse;
422 osdDisplayPortDevice = displayPortDeviceToUse;
423 #ifdef USE_CMS
424 cmsDisplayPortRegister(osdDisplayPort);
425 #endif
427 if (displayIsReady(osdDisplayPort)) {
428 osdCompleteInitialization();
432 bool osdInitialized(void)
434 return osdDisplayPort;
437 static void osdResetStats(void)
439 stats.max_current = 0;
440 stats.max_speed = 0;
441 stats.min_voltage = 5000;
442 stats.end_voltage = 0;
443 stats.min_rssi = 99; // percent
444 stats.max_altitude = 0;
445 stats.max_distance = 0;
446 stats.armed_time = 0;
447 stats.max_g_force = 0;
448 stats.max_esc_temp = 0;
449 stats.max_esc_rpm = 0;
450 stats.min_link_quality = (linkQualitySource == LQ_SOURCE_RX_PROTOCOL_CRSF) ? 300 : 99; // CRSF : percent
451 stats.min_rssi_dbm = CRSF_SNR_MAX;
454 static void osdUpdateStats(void)
456 int16_t value = 0;
458 #ifdef USE_GPS
459 if (gpsConfig()->gps_use_3d_speed) {
460 value = gpsSol.speed3d;
461 } else {
462 value = gpsSol.groundSpeed;
464 if (stats.max_speed < value) {
465 stats.max_speed = value;
467 #endif
469 value = getBatteryVoltage();
470 if (stats.min_voltage > value) {
471 stats.min_voltage = value;
474 value = getAmperage() / 100;
475 if (stats.max_current < value) {
476 stats.max_current = value;
479 value = getRssiPercent();
480 if (stats.min_rssi > value) {
481 stats.min_rssi = value;
484 int32_t altitudeCm = getEstimatedAltitudeCm();
485 if (stats.max_altitude < altitudeCm) {
486 stats.max_altitude = altitudeCm;
489 #if defined(USE_ACC)
490 if (stats.max_g_force < osdGForce) {
491 stats.max_g_force = osdGForce;
493 #endif
495 #ifdef USE_RX_LINK_QUALITY_INFO
496 value = rxGetLinkQualityPercent();
497 if (stats.min_link_quality > value) {
498 stats.min_link_quality = value;
500 #endif
502 #ifdef USE_RX_RSSI_DBM
503 value = getRssiDbm();
504 if (stats.min_rssi_dbm > value) {
505 stats.min_rssi_dbm = value;
507 #endif
509 #ifdef USE_GPS
510 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) {
511 if (stats.max_distance < GPS_distanceToHome) {
512 stats.max_distance = GPS_distanceToHome;
515 #endif
516 #ifdef USE_ESC_SENSOR
517 if (featureIsEnabled(FEATURE_ESC_SENSOR)) {
518 value = osdEscDataCombined->temperature;
519 if (stats.max_esc_temp < value) {
520 stats.max_esc_temp = value;
522 value = calcEscRpm(osdEscDataCombined->rpm);
523 if (stats.max_esc_rpm < value) {
524 stats.max_esc_rpm = value;
527 #endif
530 #ifdef USE_BLACKBOX
532 static void osdGetBlackboxStatusString(char * buff)
534 bool storageDeviceIsWorking = isBlackboxDeviceWorking();
535 uint32_t storageUsed = 0;
536 uint32_t storageTotal = 0;
538 switch (blackboxConfig()->device) {
539 #ifdef USE_SDCARD
540 case BLACKBOX_DEVICE_SDCARD:
541 if (storageDeviceIsWorking) {
542 storageTotal = sdcard_getMetadata()->numBlocks / 2000;
543 storageUsed = storageTotal - (afatfs_getContiguousFreeSpace() / 1024000);
545 break;
546 #endif
548 #ifdef USE_FLASHFS
549 case BLACKBOX_DEVICE_FLASH:
550 if (storageDeviceIsWorking) {
552 const flashPartition_t *flashPartition = flashPartitionFindByType(FLASH_PARTITION_TYPE_FLASHFS);
553 const flashGeometry_t *flashGeometry = flashGetGeometry();
555 storageTotal = ((FLASH_PARTITION_SECTOR_COUNT(flashPartition) * flashGeometry->sectorSize) / 1024);
556 storageUsed = flashfsGetOffset() / 1024;
558 break;
559 #endif
561 default:
562 break;
565 if (storageDeviceIsWorking) {
566 const uint16_t storageUsedPercent = (storageUsed * 100) / storageTotal;
567 tfp_sprintf(buff, "%d%%", storageUsedPercent);
568 } else {
569 tfp_sprintf(buff, "FAULT");
572 #endif
574 static void osdDisplayStatisticLabel(uint8_t y, const char * text, const char * value)
576 displayWrite(osdDisplayPort, 2, y, DISPLAYPORT_ATTR_NONE, text);
577 displayWrite(osdDisplayPort, 20, y, DISPLAYPORT_ATTR_NONE, ":");
578 displayWrite(osdDisplayPort, 22, y, DISPLAYPORT_ATTR_NONE, value);
582 * Test if there's some stat enabled
584 static bool isSomeStatEnabled(void)
586 return (osdConfig()->enabled_stats != 0);
589 // *** IMPORTANT ***
590 // The stats display order was previously required to match the enumeration definition so it matched
591 // the order shown in the configurator. However, to allow reordering this screen without breaking the
592 // compatibility, this requirement has been relaxed to a best effort approach. Reordering the elements
593 // on the stats screen will have to be more beneficial than the hassle of not matching exactly to the
594 // configurator list.
596 static bool osdDisplayStat(int statistic, uint8_t displayRow)
598 char buff[OSD_ELEMENT_BUFFER_LENGTH];
600 switch (statistic) {
601 case OSD_STAT_RTC_DATE_TIME: {
602 bool success = false;
603 #ifdef USE_RTC_TIME
604 success = osdFormatRtcDateTime(&buff[0]);
605 #endif
606 if (!success) {
607 tfp_sprintf(buff, "NO RTC");
610 displayWrite(osdDisplayPort, 2, displayRow, DISPLAYPORT_ATTR_NONE, buff);
611 return true;
614 case OSD_STAT_TIMER_1:
615 osdFormatTimer(buff, false, (OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_1]) == OSD_TIMER_SRC_ON ? false : true), OSD_TIMER_1);
616 osdDisplayStatisticLabel(displayRow, osdTimerSourceNames[OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_1])], buff);
617 return true;
619 case OSD_STAT_TIMER_2:
620 osdFormatTimer(buff, false, (OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_2]) == OSD_TIMER_SRC_ON ? false : true), OSD_TIMER_2);
621 osdDisplayStatisticLabel(displayRow, osdTimerSourceNames[OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_2])], buff);
622 return true;
624 case OSD_STAT_MAX_ALTITUDE: {
625 const int alt = osdGetMetersToSelectedUnit(stats.max_altitude) / 10;
626 tfp_sprintf(buff, "%d.%d%c", alt / 10, alt % 10, osdGetMetersToSelectedUnitSymbol());
627 osdDisplayStatisticLabel(displayRow, "MAX ALTITUDE", buff);
628 return true;
631 #ifdef USE_GPS
632 case OSD_STAT_MAX_SPEED:
633 if (featureIsEnabled(FEATURE_GPS)) {
634 tfp_sprintf(buff, "%d%c", osdGetSpeedToSelectedUnit(stats.max_speed), osdGetSpeedToSelectedUnitSymbol());
635 osdDisplayStatisticLabel(displayRow, "MAX SPEED", buff);
636 return true;
638 break;
640 case OSD_STAT_MAX_DISTANCE:
641 if (featureIsEnabled(FEATURE_GPS)) {
642 osdFormatDistanceString(buff, stats.max_distance, SYM_NONE);
643 osdDisplayStatisticLabel(displayRow, "MAX DISTANCE", buff);
644 return true;
646 break;
648 case OSD_STAT_FLIGHT_DISTANCE:
649 if (featureIsEnabled(FEATURE_GPS)) {
650 const int distanceFlown = GPS_distanceFlownInCm / 100;
651 osdFormatDistanceString(buff, distanceFlown, SYM_NONE);
652 osdDisplayStatisticLabel(displayRow, "FLIGHT DISTANCE", buff);
653 return true;
655 break;
656 #endif
658 case OSD_STAT_MIN_BATTERY:
659 tfp_sprintf(buff, "%d.%02d%c", stats.min_voltage / 100, stats.min_voltage % 100, SYM_VOLT);
660 osdDisplayStatisticLabel(displayRow, "MIN BATTERY", buff);
661 return true;
663 case OSD_STAT_END_BATTERY:
664 tfp_sprintf(buff, "%d.%02d%c", stats.end_voltage / 100, stats.end_voltage % 100, SYM_VOLT);
665 osdDisplayStatisticLabel(displayRow, "END BATTERY", buff);
666 return true;
668 case OSD_STAT_BATTERY:
669 tfp_sprintf(buff, "%d.%02d%c", getBatteryVoltage() / 100, getBatteryVoltage() % 100, SYM_VOLT);
670 osdDisplayStatisticLabel(displayRow, "BATTERY", buff);
671 return true;
673 case OSD_STAT_MIN_RSSI:
674 itoa(stats.min_rssi, buff, 10);
675 strcat(buff, "%");
676 osdDisplayStatisticLabel(displayRow, "MIN RSSI", buff);
677 return true;
679 case OSD_STAT_MAX_CURRENT:
680 if (batteryConfig()->currentMeterSource != CURRENT_METER_NONE) {
681 tfp_sprintf(buff, "%d%c", stats.max_current, SYM_AMP);
682 osdDisplayStatisticLabel(displayRow, "MAX CURRENT", buff);
683 return true;
685 break;
687 case OSD_STAT_USED_MAH:
688 if (batteryConfig()->currentMeterSource != CURRENT_METER_NONE) {
689 tfp_sprintf(buff, "%d%c", getMAhDrawn(), SYM_MAH);
690 osdDisplayStatisticLabel(displayRow, "USED MAH", buff);
691 return true;
693 break;
695 #ifdef USE_BLACKBOX
696 case OSD_STAT_BLACKBOX:
697 if (blackboxConfig()->device && blackboxConfig()->device != BLACKBOX_DEVICE_SERIAL) {
698 osdGetBlackboxStatusString(buff);
699 osdDisplayStatisticLabel(displayRow, "BLACKBOX", buff);
700 return true;
702 break;
704 case OSD_STAT_BLACKBOX_NUMBER:
706 int32_t logNumber = blackboxGetLogNumber();
707 if (logNumber >= 0) {
708 itoa(logNumber, buff, 10);
709 osdDisplayStatisticLabel(displayRow, "BB LOG NUM", buff);
710 return true;
713 break;
714 #endif
716 #if defined(USE_ACC)
717 case OSD_STAT_MAX_G_FORCE:
718 if (sensors(SENSOR_ACC)) {
719 const int gForce = lrintf(stats.max_g_force * 10);
720 tfp_sprintf(buff, "%01d.%01dG", gForce / 10, gForce % 10);
721 osdDisplayStatisticLabel(displayRow, "MAX G-FORCE", buff);
722 return true;
724 break;
725 #endif
727 #ifdef USE_ESC_SENSOR
728 case OSD_STAT_MAX_ESC_TEMP:
729 tfp_sprintf(buff, "%d%c", osdConvertTemperatureToSelectedUnit(stats.max_esc_temp), osdGetTemperatureSymbolForSelectedUnit());
730 osdDisplayStatisticLabel(displayRow, "MAX ESC TEMP", buff);
731 return true;
733 case OSD_STAT_MAX_ESC_RPM:
734 itoa(stats.max_esc_rpm, buff, 10);
735 osdDisplayStatisticLabel(displayRow, "MAX ESC RPM", buff);
736 return true;
737 #endif
739 #ifdef USE_RX_LINK_QUALITY_INFO
740 case OSD_STAT_MIN_LINK_QUALITY:
741 tfp_sprintf(buff, "%d", stats.min_link_quality);
742 strcat(buff, "%");
743 osdDisplayStatisticLabel(displayRow, "MIN LINK", buff);
744 return true;
745 #endif
747 #if defined(USE_GYRO_DATA_ANALYSE)
748 case OSD_STAT_MAX_FFT:
749 if (featureIsEnabled(FEATURE_DYNAMIC_FILTER)) {
750 int value = getMaxFFT();
751 if (value > 0) {
752 tfp_sprintf(buff, "%dHZ", value);
753 osdDisplayStatisticLabel(displayRow, "PEAK FFT", buff);
754 } else {
755 osdDisplayStatisticLabel(displayRow, "PEAK FFT", "THRT<20%");
757 return true;
759 break;
760 #endif
762 #ifdef USE_RX_RSSI_DBM
763 case OSD_STAT_MIN_RSSI_DBM:
764 tfp_sprintf(buff, "%3d", stats.min_rssi_dbm);
765 osdDisplayStatisticLabel(displayRow, "MIN RSSI DBM", buff);
766 return true;
767 #endif
769 #ifdef USE_PERSISTENT_STATS
770 case OSD_STAT_TOTAL_FLIGHTS:
771 itoa(statsConfig()->stats_total_flights, buff, 10);
772 osdDisplayStatisticLabel(displayRow, "TOTAL FLIGHTS", buff);
773 return true;
775 case OSD_STAT_TOTAL_TIME: {
776 int minutes = statsConfig()->stats_total_time_s / 60;
777 tfp_sprintf(buff, "%d:%02dH", minutes / 60, minutes % 60);
778 osdDisplayStatisticLabel(displayRow, "TOTAL FLIGHT TIME", buff);
779 return true;
782 case OSD_STAT_TOTAL_DIST:
783 #define METERS_PER_KILOMETER 1000
784 #define METERS_PER_MILE 1609
785 if (osdConfig()->units == OSD_UNIT_IMPERIAL) {
786 tfp_sprintf(buff, "%d%c", statsConfig()->stats_total_dist_m / METERS_PER_MILE, SYM_MILES);
787 } else {
788 tfp_sprintf(buff, "%d%c", statsConfig()->stats_total_dist_m / METERS_PER_KILOMETER, SYM_KM);
790 osdDisplayStatisticLabel(displayRow, "TOTAL DISTANCE", buff);
791 return true;
792 #endif
794 return false;
797 static uint8_t osdShowStats(int statsRowCount)
799 uint8_t top = 0;
800 bool displayLabel = false;
802 // if statsRowCount is 0 then we're running an initial analysis of the active stats items
803 if (statsRowCount > 0) {
804 const int availableRows = osdDisplayPort->rows;
805 int displayRows = MIN(statsRowCount, availableRows);
806 if (statsRowCount < availableRows) {
807 displayLabel = true;
808 displayRows++;
810 top = (availableRows - displayRows) / 2; // center the stats vertically
813 if (displayLabel) {
814 displayWrite(osdDisplayPort, 2, top++, DISPLAYPORT_ATTR_NONE, " --- STATS ---");
817 for (int i = 0; i < OSD_STAT_COUNT; i++) {
818 if (osdStatGetState(osdStatsDisplayOrder[i])) {
819 if (osdDisplayStat(osdStatsDisplayOrder[i], top)) {
820 top++;
824 return top;
827 static void osdRefreshStats(void)
829 displayClearScreen(osdDisplayPort);
830 if (osdStatsRowCount == 0) {
831 // No stats row count has been set yet.
832 // Go through the logic one time to determine how many stats are actually displayed.
833 osdStatsRowCount = osdShowStats(0);
834 // Then clear the screen and commence with normal stats display which will
835 // determine if the heading should be displayed and also center the content vertically.
836 displayClearScreen(osdDisplayPort);
838 osdShowStats(osdStatsRowCount);
841 static timeDelta_t osdShowArmed(void)
843 timeDelta_t ret;
845 displayClearScreen(osdDisplayPort);
847 if ((osdConfig()->logo_on_arming == OSD_LOGO_ARMING_ON) || ((osdConfig()->logo_on_arming == OSD_LOGO_ARMING_FIRST) && !ARMING_FLAG(WAS_EVER_ARMED))) {
848 osdDrawLogo(3, 1);
849 ret = osdConfig()->logo_on_arming_duration * 1e5;
850 } else {
851 ret = (REFRESH_1S / 2);
853 displayWrite(osdDisplayPort, 12, 7, DISPLAYPORT_ATTR_NONE, "ARMED");
855 return ret;
858 STATIC_UNIT_TESTED void osdRefresh(timeUs_t currentTimeUs)
860 static timeUs_t lastTimeUs = 0;
861 static bool osdStatsEnabled = false;
862 static bool osdStatsVisible = false;
863 static timeUs_t osdStatsRefreshTimeUs;
865 if (!osdIsReady) {
866 if (!displayIsReady(osdDisplayPort)) {
867 displayResync(osdDisplayPort);
868 return;
870 osdCompleteInitialization();
873 // detect arm/disarm
874 if (armState != ARMING_FLAG(ARMED)) {
875 if (ARMING_FLAG(ARMED)) {
876 osdStatsEnabled = false;
877 osdStatsVisible = false;
878 osdResetStats();
879 resumeRefreshAt = osdShowArmed() + currentTimeUs;
880 } else if (isSomeStatEnabled()
881 && !suppressStatsDisplay
882 && (!(getArmingDisableFlags() & (ARMING_DISABLED_RUNAWAY_TAKEOFF | ARMING_DISABLED_CRASH_DETECTED))
883 || !VISIBLE(osdElementConfig()->item_pos[OSD_WARNINGS]))) { // suppress stats if runaway takeoff triggered disarm and WARNINGS element is visible
884 osdStatsEnabled = true;
885 resumeRefreshAt = currentTimeUs + (60 * REFRESH_1S);
886 stats.end_voltage = getBatteryVoltage();
887 osdStatsRowCount = 0; // reset to 0 so it will be recalculated on the next stats refresh
890 armState = ARMING_FLAG(ARMED);
894 if (ARMING_FLAG(ARMED)) {
895 osdUpdateStats();
896 timeUs_t deltaT = currentTimeUs - lastTimeUs;
897 osdFlyTime += deltaT;
898 stats.armed_time += deltaT;
899 } else if (osdStatsEnabled) { // handle showing/hiding stats based on OSD disable switch position
900 if (displayIsGrabbed(osdDisplayPort)) {
901 osdStatsEnabled = false;
902 resumeRefreshAt = 0;
903 stats.armed_time = 0;
904 } else {
905 if (IS_RC_MODE_ACTIVE(BOXOSD) && osdStatsVisible) {
906 osdStatsVisible = false;
907 displayClearScreen(osdDisplayPort);
908 } else if (!IS_RC_MODE_ACTIVE(BOXOSD)) {
909 if (!osdStatsVisible) {
910 osdStatsVisible = true;
911 osdStatsRefreshTimeUs = 0;
913 if (currentTimeUs >= osdStatsRefreshTimeUs) {
914 osdStatsRefreshTimeUs = currentTimeUs + REFRESH_1S;
915 osdRefreshStats();
920 lastTimeUs = currentTimeUs;
922 displayBeginTransaction(osdDisplayPort, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);
924 if (resumeRefreshAt) {
925 if (cmp32(currentTimeUs, resumeRefreshAt) < 0) {
926 // in timeout period, check sticks for activity to resume display.
927 if (IS_HI(THROTTLE) || IS_HI(PITCH)) {
928 resumeRefreshAt = currentTimeUs;
930 displayHeartbeat(osdDisplayPort);
931 return;
932 } else {
933 displayClearScreen(osdDisplayPort);
934 resumeRefreshAt = 0;
935 osdStatsEnabled = false;
936 stats.armed_time = 0;
940 #ifdef USE_ESC_SENSOR
941 if (featureIsEnabled(FEATURE_ESC_SENSOR)) {
942 osdEscDataCombined = getEscSensorData(ESC_SENSOR_COMBINED);
944 #endif
946 #if defined(USE_ACC)
947 if (sensors(SENSOR_ACC)
948 && (VISIBLE(osdElementConfig()->item_pos[OSD_G_FORCE]) || osdStatGetState(OSD_STAT_MAX_G_FORCE))) {
949 // only calculate the G force if the element is visible or the stat is enabled
950 for (int axis = 0; axis < XYZ_AXIS_COUNT; axis++) {
951 const float a = accAverage[axis];
952 osdGForce += a * a;
954 osdGForce = sqrtf(osdGForce) * acc.dev.acc_1G_rec;
956 #endif
958 #ifdef USE_CMS
959 if (!displayIsGrabbed(osdDisplayPort))
960 #endif
962 osdUpdateAlarms();
963 osdDrawElements(currentTimeUs);
964 displayHeartbeat(osdDisplayPort);
966 displayCommitTransaction(osdDisplayPort);
970 * Called periodically by the scheduler
972 void osdUpdate(timeUs_t currentTimeUs)
974 static uint32_t counter = 0;
976 if (isBeeperOn()) {
977 showVisualBeeper = true;
980 #ifdef MAX7456_DMA_CHANNEL_TX
981 // don't touch buffers if DMA transaction is in progress
982 if (displayIsTransferInProgress(osdDisplayPort)) {
983 return;
985 #endif // MAX7456_DMA_CHANNEL_TX
987 #ifdef USE_SLOW_MSP_DISPLAYPORT_RATE_WHEN_UNARMED
988 static uint32_t idlecounter = 0;
989 if (!ARMING_FLAG(ARMED)) {
990 if (idlecounter++ % 4 != 0) {
991 return;
994 #endif
996 // redraw values in buffer
997 #ifdef USE_MAX7456
998 #define DRAW_FREQ_DENOM 5
999 #else
1000 #define DRAW_FREQ_DENOM 10 // MWOSD @ 115200 baud (
1001 #endif
1003 if (counter % DRAW_FREQ_DENOM == 0) {
1004 osdRefresh(currentTimeUs);
1005 showVisualBeeper = false;
1006 } else {
1007 // rest of time redraw screen 10 chars per idle so it doesn't lock the main idle
1008 displayDrawScreen(osdDisplayPort);
1010 ++counter;
1013 void osdSuppressStats(bool flag)
1015 suppressStatsDisplay = flag;
1018 #ifdef USE_OSD_PROFILES
1019 bool osdElementVisible(uint16_t value)
1021 return (bool)((((value & OSD_PROFILE_MASK) >> OSD_PROFILE_BITS_POS) & osdProfile) != 0);
1023 #endif
1025 bool osdGetVisualBeeperState(void)
1027 return showVisualBeeper;
1030 statistic_t *osdGetStats(void)
1032 return &stats;
1035 #ifdef USE_ACC
1036 // Determine if there are any enabled stats that need
1037 // the ACC (currently only MAX_G_FORCE).
1038 static bool osdStatsNeedAccelerometer(void)
1040 return osdStatGetState(OSD_STAT_MAX_G_FORCE);
1043 // Check if any enabled elements or stats need the ACC
1044 bool osdNeedsAccelerometer(void)
1046 return osdStatsNeedAccelerometer() || osdElementsNeedAccelerometer();
1048 #endif // USE_ACC
1050 displayPort_t *osdGetDisplayPort(osdDisplayPortDevice_e *displayPortDevice)
1052 if (displayPortDevice) {
1053 *displayPortDevice = osdDisplayPortDevice;
1055 return osdDisplayPort;
1058 #endif // USE_OSD