Merge pull request #5765 from etracer65/gps_speed_units
[betaflight.git] / src / main / io / osd.c
blob9401fb9ccb634b8859156aa10993f4b5086ee21f
1 /*
2 * This file is part of Cleanflight.
4 * Cleanflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Cleanflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
19 Created by Marcin Baliniak
20 some functions based on MinimOSD
22 OSD-CMS separation by jflyper
25 #include <stdbool.h>
26 #include <stdint.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <ctype.h>
30 #include <math.h>
32 #include "platform.h"
34 #ifdef USE_OSD
36 #include "blackbox/blackbox.h"
37 #include "blackbox/blackbox_io.h"
39 #include "build/build_config.h"
40 #include "build/debug.h"
41 #include "build/version.h"
43 #include "cms/cms.h"
44 #include "cms/cms_types.h"
46 #include "common/maths.h"
47 #include "common/printf.h"
48 #include "common/typeconversion.h"
49 #include "common/utils.h"
51 #include "config/feature.h"
53 #include "drivers/display.h"
54 #include "drivers/flash.h"
55 #include "drivers/max7456_symbols.h"
56 #include "drivers/sdcard.h"
57 #include "drivers/time.h"
59 #include "fc/config.h"
60 #include "fc/fc_core.h"
61 #include "fc/rc_adjustments.h"
62 #include "fc/rc_controls.h"
63 #include "fc/runtime_config.h"
65 #include "flight/altitude.h"
66 #include "flight/imu.h"
67 #include "flight/pid.h"
69 #include "io/asyncfatfs/asyncfatfs.h"
70 #include "io/beeper.h"
71 #include "io/flashfs.h"
72 #include "io/gps.h"
73 #include "io/osd.h"
74 #include "io/vtx_string.h"
75 #include "io/vtx.h"
77 #include "pg/pg.h"
78 #include "pg/pg_ids.h"
80 #include "rx/rx.h"
82 #include "sensors/adcinternal.h"
83 #include "sensors/barometer.h"
84 #include "sensors/battery.h"
85 #include "sensors/esc_sensor.h"
86 #include "sensors/sensors.h"
88 #ifdef USE_HARDWARE_REVISION_DETECTION
89 #include "hardware_revision.h"
90 #endif
92 #define VIDEO_BUFFER_CHARS_PAL 480
93 #define FULL_CIRCLE 360
95 const char * const osdTimerSourceNames[] = {
96 "ON TIME ",
97 "TOTAL ARM",
98 "LAST ARM "
101 // Blink control
103 static bool blinkState = true;
104 static bool showVisualBeeper = false;
106 static uint32_t blinkBits[(OSD_ITEM_COUNT + 31)/32];
107 #define SET_BLINK(item) (blinkBits[(item) / 32] |= (1 << ((item) % 32)))
108 #define CLR_BLINK(item) (blinkBits[(item) / 32] &= ~(1 << ((item) % 32)))
109 #define IS_BLINK(item) (blinkBits[(item) / 32] & (1 << ((item) % 32)))
110 #define BLINK(item) (IS_BLINK(item) && blinkState)
112 // Things in both OSD and CMS
114 #define IS_HI(X) (rcData[X] > 1750)
115 #define IS_LO(X) (rcData[X] < 1250)
116 #define IS_MID(X) (rcData[X] > 1250 && rcData[X] < 1750)
118 static timeUs_t flyTime = 0;
119 static uint8_t statRssi;
121 typedef struct statistic_s {
122 timeUs_t armed_time;
123 int16_t max_speed;
124 int16_t min_voltage; // /10
125 int16_t max_current; // /10
126 int16_t min_rssi;
127 int16_t max_altitude;
128 int16_t max_distance;
129 } statistic_t;
131 static statistic_t stats;
133 timeUs_t resumeRefreshAt = 0;
134 #define REFRESH_1S 1000 * 1000
136 static uint8_t armState;
138 static displayPort_t *osdDisplayPort;
140 #ifdef USE_ESC_SENSOR
141 static escSensorData_t *escData;
142 #endif
144 #define AH_SYMBOL_COUNT 9
145 #define AH_SIDEBAR_WIDTH_POS 7
146 #define AH_SIDEBAR_HEIGHT_POS 3
148 static const char compassBar[] = {
149 SYM_HEADING_W,
150 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
151 SYM_HEADING_N,
152 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
153 SYM_HEADING_E,
154 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
155 SYM_HEADING_S,
156 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
157 SYM_HEADING_W,
158 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE,
159 SYM_HEADING_N,
160 SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE
163 PG_REGISTER_WITH_RESET_FN(osdConfig_t, osdConfig, PG_OSD_CONFIG, 2);
166 * Gets the correct altitude symbol for the current unit system
168 static char osdGetMetersToSelectedUnitSymbol(void)
170 switch (osdConfig()->units) {
171 case OSD_UNIT_IMPERIAL:
172 return SYM_FT;
173 default:
174 return SYM_M;
179 * Gets average battery cell voltage in 0.01V units.
181 static int osdGetBatteryAverageCellVoltage(void)
183 return (getBatteryVoltage() * 10) / getBatteryCellCount();
186 static char osdGetBatterySymbol(int cellVoltage)
188 if (getBatteryState() == BATTERY_CRITICAL) {
189 return SYM_MAIN_BATT; // FIXME: currently the BAT- symbol, ideally replace with a battery with exclamation mark
190 } else {
191 // Calculate a symbol offset using cell voltage over full cell voltage range
192 const int symOffset = scaleRange(cellVoltage, batteryConfig()->vbatmincellvoltage * 10, batteryConfig()->vbatmaxcellvoltage * 10, 0, 7);
193 return SYM_BATT_EMPTY - constrain(symOffset, 0, 6);
198 * Converts altitude based on the current unit system.
199 * @param meters Value in meters to convert
201 static int32_t osdGetMetersToSelectedUnit(int32_t meters)
203 switch (osdConfig()->units) {
204 case OSD_UNIT_IMPERIAL:
205 return (meters * 328) / 100; // Convert to feet / 100
206 default:
207 return meters; // Already in metre / 100
211 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
212 STATIC_UNIT_TESTED int osdConvertTemperatureToSelectedUnit(int tempInDeciDegrees)
214 switch (osdConfig()->units) {
215 case OSD_UNIT_IMPERIAL:
216 return ((tempInDeciDegrees * 9) / 5) + 320;
217 default:
218 return tempInDeciDegrees;
222 static char osdGetTemperatureSymbolForSelectedUnit(void)
224 switch (osdConfig()->units) {
225 case OSD_UNIT_IMPERIAL:
226 return 'F';
227 default:
228 return 'C';
231 #endif
233 static void osdFormatAltitudeString(char * buff, int altitude, bool pad)
235 const int alt = osdGetMetersToSelectedUnit(altitude);
236 int altitudeIntergerPart = abs(alt / 100);
237 if (alt < 0) {
238 altitudeIntergerPart *= -1;
240 tfp_sprintf(buff, pad ? "%4d.%01d%c" : "%d.%01d%c", altitudeIntergerPart, abs((alt % 100) / 10), osdGetMetersToSelectedUnitSymbol());
243 static void osdFormatPID(char * buff, const char * label, const pid8_t * pid)
245 tfp_sprintf(buff, "%s %3d %3d %3d", label, pid->P, pid->I, pid->D);
248 static uint8_t osdGetHeadingIntoDiscreteDirections(int heading, unsigned directions)
250 heading += FULL_CIRCLE; // Ensure positive value
252 // Split input heading 0..359 into sectors 0..(directions-1), but offset
253 // by half a sector so that sector 0 gets centered around heading 0.
254 // We multiply heading by directions to not loose precision in divisions
255 // In this way each segment will be a FULL_CIRCLE length
256 int direction = (heading * directions + FULL_CIRCLE / 2) / FULL_CIRCLE; // scale with rounding
257 direction %= directions; // normalize
259 return direction; // return segment number
262 static uint8_t osdGetDirectionSymbolFromHeading(int heading)
264 heading = osdGetHeadingIntoDiscreteDirections(heading, 16);
266 // Now heading has a heading with Up=0, Right=4, Down=8 and Left=12
267 // Our symbols are Down=0, Right=4, Up=8 and Left=12
268 // There're 16 arrow symbols. Transform it.
269 heading = 16 - heading;
270 heading = (heading + 8) % 16;
272 return SYM_ARROW_SOUTH + heading;
275 static char osdGetTimerSymbol(osd_timer_source_e src)
277 switch (src) {
278 case OSD_TIMER_SRC_ON:
279 return SYM_ON_M;
280 case OSD_TIMER_SRC_TOTAL_ARMED:
281 case OSD_TIMER_SRC_LAST_ARMED:
282 return SYM_FLY_M;
283 default:
284 return ' ';
288 static timeUs_t osdGetTimerValue(osd_timer_source_e src)
290 switch (src) {
291 case OSD_TIMER_SRC_ON:
292 return micros();
293 case OSD_TIMER_SRC_TOTAL_ARMED:
294 return flyTime;
295 case OSD_TIMER_SRC_LAST_ARMED:
296 return stats.armed_time;
297 default:
298 return 0;
302 STATIC_UNIT_TESTED void osdFormatTime(char * buff, osd_timer_precision_e precision, timeUs_t time)
304 int seconds = time / 1000000;
305 const int minutes = seconds / 60;
306 seconds = seconds % 60;
308 switch (precision) {
309 case OSD_TIMER_PREC_SECOND:
310 default:
311 tfp_sprintf(buff, "%02d:%02d", minutes, seconds);
312 break;
313 case OSD_TIMER_PREC_HUNDREDTHS:
315 const int hundredths = (time / 10000) % 100;
316 tfp_sprintf(buff, "%02d:%02d.%02d", minutes, seconds, hundredths);
317 break;
322 STATIC_UNIT_TESTED void osdFormatTimer(char *buff, bool showSymbol, int timerIndex)
324 const uint16_t timer = osdConfig()->timers[timerIndex];
325 const uint8_t src = OSD_TIMER_SRC(timer);
327 if (showSymbol) {
328 *(buff++) = osdGetTimerSymbol(src);
331 osdFormatTime(buff, OSD_TIMER_PRECISION(timer), osdGetTimerValue(src));
334 #ifdef USE_GPS
335 static void osdFormatCoordinate(char *buff, char sym, int32_t val)
337 // latitude maximum integer width is 3 (-90).
338 // longitude maximum integer width is 4 (-180).
339 // We show 7 decimals, so we need to use 12 characters:
340 // eg: s-180.1234567z s=symbol, z=zero terminator, decimal separator between 0 and 1
342 static const int coordinateMaxLength = 13;//12 for the number (4 + dot + 7) + 1 for the symbol
344 buff[0] = sym;
345 const int32_t integerPart = val / GPS_DEGREES_DIVIDER;
346 const int32_t decimalPart = labs(val % GPS_DEGREES_DIVIDER);
347 const int written = tfp_sprintf(buff + 1, "%d.%07d", integerPart, decimalPart);
348 // pad with blanks to coordinateMaxLength
349 for (int pos = 1 + written; pos < coordinateMaxLength; ++pos) {
350 buff[pos] = SYM_BLANK;
352 buff[coordinateMaxLength] = '\0';
354 #endif // USE_GPS
356 #ifdef USE_RTC_TIME
357 static bool osdFormatRtcDateTime(char *buffer)
359 dateTime_t dateTime;
360 if (!rtcGetDateTime(&dateTime)) {
361 buffer[0] = '\0';
363 return false;
366 dateTimeFormatLocalShort(buffer, &dateTime);
368 return true;
370 #endif
372 static void osdFormatMessage(char *buff, size_t size, const char *message)
374 memset(buff, SYM_BLANK, size);
375 if (message) {
376 memcpy(buff, message, strlen(message));
378 // Ensure buff is zero terminated
379 buff[size - 1] = '\0';
382 static bool osdDrawSingleElement(uint8_t item)
384 if (!VISIBLE(osdConfig()->item_pos[item]) || BLINK(item)) {
385 return false;
388 uint8_t elemPosX = OSD_X(osdConfig()->item_pos[item]);
389 uint8_t elemPosY = OSD_Y(osdConfig()->item_pos[item]);
390 uint8_t elemOffsetX = 0;
391 char buff[OSD_ELEMENT_BUFFER_LENGTH];
393 switch (item) {
394 case OSD_RSSI_VALUE:
396 uint16_t osdRssi = getRssi() * 100 / 1024; // change range
397 if (osdRssi >= 100)
398 osdRssi = 99;
400 tfp_sprintf(buff, "%c%2d", SYM_RSSI, osdRssi);
401 break;
404 case OSD_MAIN_BATT_VOLTAGE:
405 buff[0] = osdGetBatterySymbol(osdGetBatteryAverageCellVoltage());
406 tfp_sprintf(buff + 1, "%2d.%1d%c", getBatteryVoltage() / 10, getBatteryVoltage() % 10, SYM_VOLT);
407 break;
409 case OSD_CURRENT_DRAW:
411 const int32_t amperage = getAmperage();
412 tfp_sprintf(buff, "%3d.%02d%c", abs(amperage) / 100, abs(amperage) % 100, SYM_AMP);
413 break;
416 case OSD_MAH_DRAWN:
417 tfp_sprintf(buff, "%4d%c", getMAhDrawn(), SYM_MAH);
418 break;
420 #ifdef USE_GPS
421 case OSD_GPS_SATS:
422 tfp_sprintf(buff, "%c%c%2d", SYM_SAT_L, SYM_SAT_R, gpsSol.numSat);
423 break;
425 case OSD_GPS_SPEED:
426 // FIXME ideally we want to use SYM_KMH symbol but it's not in the font any more, so we use K (M for MPH)
427 switch (osdConfig()->units) {
428 case OSD_UNIT_IMPERIAL:
429 tfp_sprintf(buff, "%3dM", CM_S_TO_MPH(gpsSol.groundSpeed));
430 break;
431 default:
432 tfp_sprintf(buff, "%3dK", CM_S_TO_KM_H(gpsSol.groundSpeed));
433 break;
435 break;
437 case OSD_GPS_LAT:
438 osdFormatCoordinate(buff, SYM_LAT, gpsSol.llh.lat);
439 break;
441 case OSD_GPS_LON:
442 osdFormatCoordinate(buff, SYM_LON, gpsSol.llh.lon);
443 break;
445 case OSD_HOME_DIR:
446 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) {
447 if (GPS_distanceToHome > 0) {
448 const int h = GPS_directionToHome - DECIDEGREES_TO_DEGREES(attitude.values.yaw);
449 buff[0] = osdGetDirectionSymbolFromHeading(h);
450 } else {
451 // We don't have a HOME symbol in the font, by now we use this
452 buff[0] = SYM_THR1;
455 } else {
456 // We use this symbol when we don't have a FIX
457 buff[0] = SYM_COLON;
460 buff[1] = 0;
462 break;
464 case OSD_HOME_DIST:
465 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) {
466 const int32_t distance = osdGetMetersToSelectedUnit(GPS_distanceToHome);
467 tfp_sprintf(buff, "%d%c", distance, osdGetMetersToSelectedUnitSymbol());
468 } else {
469 // We use this symbol when we don't have a FIX
470 buff[0] = SYM_COLON;
471 // overwrite any previous distance with blanks
472 memset(buff + 1, SYM_BLANK, 6);
473 buff[7] = '\0';
475 break;
477 #endif // GPS
479 case OSD_COMPASS_BAR:
480 memcpy(buff, compassBar + osdGetHeadingIntoDiscreteDirections(DECIDEGREES_TO_DEGREES(attitude.values.yaw), 16), 9);
481 buff[9] = 0;
482 break;
484 case OSD_ALTITUDE:
485 osdFormatAltitudeString(buff, getEstimatedAltitude(), true);
486 break;
488 case OSD_ITEM_TIMER_1:
489 case OSD_ITEM_TIMER_2:
490 osdFormatTimer(buff, true, item - OSD_ITEM_TIMER_1);
491 break;
493 case OSD_REMAINING_TIME_ESTIMATE:
495 const int mAhDrawn = getMAhDrawn();
496 const int remaining_time = (int)((osdConfig()->cap_alarm - mAhDrawn) * ((float)flyTime) / mAhDrawn);
498 if (mAhDrawn < 0.1 * osdConfig()->cap_alarm) {
499 tfp_sprintf(buff, "--:--");
500 } else if (mAhDrawn > osdConfig()->cap_alarm) {
501 tfp_sprintf(buff, "00:00");
502 } else {
503 osdFormatTime(buff, OSD_TIMER_PREC_SECOND, remaining_time);
505 break;
508 case OSD_FLYMODE:
510 char *p = "ACRO";
512 if (isAirmodeActive()) {
513 p = "AIR ";
516 if (FLIGHT_MODE(FAILSAFE_MODE)) {
517 p = "!FS!";
518 } else if (FLIGHT_MODE(ANGLE_MODE)) {
519 p = "STAB";
520 } else if (FLIGHT_MODE(HORIZON_MODE)) {
521 p = "HOR ";
524 displayWrite(osdDisplayPort, elemPosX, elemPosY, p);
525 return true;
528 case OSD_CRAFT_NAME:
529 // This does not strictly support iterative updating if the craft name changes at run time. But since the craft name is not supposed to be changing this should not matter, and blanking the entire length of the craft name string on update will make it impossible to configure elements to be displayed on the right hand side of the craft name.
530 //TODO: When iterative updating is implemented, change this so the craft name is only printed once whenever the OSD 'flight' screen is entered.
532 if (strlen(pilotConfig()->name) == 0) {
533 strcpy(buff, "CRAFT_NAME");
534 } else {
535 unsigned i;
536 for (i = 0; i < MAX_NAME_LENGTH; i++) {
537 if (pilotConfig()->name[i]) {
538 buff[i] = toupper((unsigned char)pilotConfig()->name[i]);
539 } else {
540 break;
543 buff[i] = '\0';
546 break;
548 case OSD_THROTTLE_POS:
549 buff[0] = SYM_THR;
550 buff[1] = SYM_THR1;
551 tfp_sprintf(buff + 2, "%3d", (constrain(rcData[THROTTLE], PWM_RANGE_MIN, PWM_RANGE_MAX) - PWM_RANGE_MIN) * 100 / (PWM_RANGE_MAX - PWM_RANGE_MIN));
552 break;
554 #if defined(USE_VTX_COMMON)
555 case OSD_VTX_CHANNEL:
557 const char vtxBandLetter = vtx58BandLetter[vtxSettingsConfig()->band];
558 const char *vtxChannelName = vtx58ChannelNames[vtxSettingsConfig()->channel];
559 uint8_t vtxPower = vtxSettingsConfig()->power;
560 const vtxDevice_t *vtxDevice = vtxCommonDevice();
561 if (vtxDevice && vtxSettingsConfig()->lowPowerDisarm) {
562 vtxCommonGetPowerIndex(vtxDevice, &vtxPower);
564 tfp_sprintf(buff, "%c:%s:%1d", vtxBandLetter, vtxChannelName, vtxPower);
565 break;
567 #endif
569 case OSD_CROSSHAIRS:
570 elemPosX = 14 - 1; // Offset for 1 char to the left
571 elemPosY = 6;
572 if (displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL) {
573 ++elemPosY;
575 buff[0] = SYM_AH_CENTER_LINE;
576 buff[1] = SYM_AH_CENTER;
577 buff[2] = SYM_AH_CENTER_LINE_RIGHT;
578 buff[3] = 0;
579 break;
581 case OSD_ARTIFICIAL_HORIZON:
583 elemPosX = 14;
584 elemPosY = 6 - 4; // Top center of the AH area
585 if (displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL) {
586 ++elemPosY;
589 // Get pitch and roll limits in tenths of degrees
590 const int maxPitch = osdConfig()->ahMaxPitch * 10;
591 const int maxRoll = osdConfig()->ahMaxRoll * 10;
592 const int rollAngle = constrain(attitude.values.roll, -maxRoll, maxRoll);
593 int pitchAngle = constrain(attitude.values.pitch, -maxPitch, maxPitch);
594 // Convert pitchAngle to y compensation value
595 // (maxPitch / 25) divisor matches previous settings of fixed divisor of 8 and fixed max AHI pitch angle of 20.0 degrees
596 pitchAngle = ((pitchAngle * 25) / maxPitch) - 41; // 41 = 4 * AH_SYMBOL_COUNT + 5
598 for (int x = -4; x <= 4; x++) {
599 const int y = ((-rollAngle * x) / 64) - pitchAngle;
600 if (y >= 0 && y <= 81) {
601 displayWriteChar(osdDisplayPort, elemPosX + x, elemPosY + (y / AH_SYMBOL_COUNT), (SYM_AH_BAR9_0 + (y % AH_SYMBOL_COUNT)));
605 osdDrawSingleElement(OSD_HORIZON_SIDEBARS);
607 return true;
610 case OSD_HORIZON_SIDEBARS:
612 elemPosX = 14;
613 elemPosY = 6;
614 if (displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL) {
615 ++elemPosY;
618 // Draw AH sides
619 const int8_t hudwidth = AH_SIDEBAR_WIDTH_POS;
620 const int8_t hudheight = AH_SIDEBAR_HEIGHT_POS;
621 for (int y = -hudheight; y <= hudheight; y++) {
622 displayWriteChar(osdDisplayPort, elemPosX - hudwidth, elemPosY + y, SYM_AH_DECORATION);
623 displayWriteChar(osdDisplayPort, elemPosX + hudwidth, elemPosY + y, SYM_AH_DECORATION);
626 // AH level indicators
627 displayWriteChar(osdDisplayPort, elemPosX - hudwidth + 1, elemPosY, SYM_AH_LEFT);
628 displayWriteChar(osdDisplayPort, elemPosX + hudwidth - 1, elemPosY, SYM_AH_RIGHT);
630 return true;
633 case OSD_ROLL_PIDS:
634 osdFormatPID(buff, "ROL", &currentPidProfile->pid[PID_ROLL]);
635 break;
637 case OSD_PITCH_PIDS:
638 osdFormatPID(buff, "PIT", &currentPidProfile->pid[PID_PITCH]);
639 break;
641 case OSD_YAW_PIDS:
642 osdFormatPID(buff, "YAW", &currentPidProfile->pid[PID_YAW]);
643 break;
645 case OSD_POWER:
646 tfp_sprintf(buff, "%4dW", getAmperage() * getBatteryVoltage() / 1000);
647 break;
649 case OSD_PIDRATE_PROFILE:
650 tfp_sprintf(buff, "%d-%d", getCurrentPidProfileIndex() + 1, getCurrentControlRateProfileIndex() + 1);
651 break;
653 case OSD_WARNINGS:
656 #define OSD_WARNINGS_MAX_SIZE 11
657 #define OSD_FORMAT_MESSAGE_BUFFER_SIZE (OSD_WARNINGS_MAX_SIZE + 1)
659 STATIC_ASSERT(OSD_FORMAT_MESSAGE_BUFFER_SIZE <= sizeof(buff), osd_warnings_size_exceeds_buffer_size);
661 const uint16_t enabledWarnings = osdConfig()->enabledWarnings;
663 const batteryState_e batteryState = getBatteryState();
665 if (enabledWarnings & OSD_WARNING_BATTERY_CRITICAL && batteryState == BATTERY_CRITICAL) {
666 osdFormatMessage(buff, OSD_FORMAT_MESSAGE_BUFFER_SIZE, " LAND NOW");
667 break;
670 // Warn when in flip over after crash mode
671 if ((enabledWarnings & OSD_WARNING_CRASH_FLIP)
672 && (isFlipOverAfterCrashMode())) {
673 osdFormatMessage(buff, OSD_FORMAT_MESSAGE_BUFFER_SIZE, "CRASH FLIP");
674 break;
677 // Show most severe reason for arming being disabled
678 if (enabledWarnings & OSD_WARNING_ARMING_DISABLE && IS_RC_MODE_ACTIVE(BOXARM) && isArmingDisabled()) {
679 const armingDisableFlags_e flags = getArmingDisableFlags();
680 for (int i = 0; i < ARMING_DISABLE_FLAGS_COUNT; i++) {
681 if (flags & (1 << i)) {
682 osdFormatMessage(buff, OSD_FORMAT_MESSAGE_BUFFER_SIZE, armingDisableFlagNames[i]);
683 break;
686 break;
689 if (enabledWarnings & OSD_WARNING_BATTERY_WARNING && batteryState == BATTERY_WARNING) {
690 osdFormatMessage(buff, OSD_FORMAT_MESSAGE_BUFFER_SIZE, "LOW BATTERY");
691 break;
694 // Show warning if battery is not fresh
695 if (enabledWarnings & OSD_WARNING_BATTERY_NOT_FULL && !ARMING_FLAG(WAS_EVER_ARMED) && (getBatteryState() == BATTERY_OK)
696 && getBatteryAverageCellVoltage() < batteryConfig()->vbatfullcellvoltage) {
697 osdFormatMessage(buff, OSD_FORMAT_MESSAGE_BUFFER_SIZE, "BATT < FULL");
698 break;
701 // Visual beeper
702 if (enabledWarnings & OSD_WARNING_VISUAL_BEEPER && showVisualBeeper) {
703 osdFormatMessage(buff, OSD_FORMAT_MESSAGE_BUFFER_SIZE, " * * * *");
704 break;
707 osdFormatMessage(buff, OSD_FORMAT_MESSAGE_BUFFER_SIZE, NULL);
708 break;
711 case OSD_AVG_CELL_VOLTAGE:
713 const int cellV = osdGetBatteryAverageCellVoltage();
714 buff[0] = osdGetBatterySymbol(cellV);
715 tfp_sprintf(buff + 1, "%d.%02d%c", cellV / 100, cellV % 100, SYM_VOLT);
716 break;
719 case OSD_DEBUG:
720 tfp_sprintf(buff, "DBG %5d %5d %5d %5d", debug[0], debug[1], debug[2], debug[3]);
721 break;
723 case OSD_PITCH_ANGLE:
724 case OSD_ROLL_ANGLE:
726 const int angle = (item == OSD_PITCH_ANGLE) ? attitude.values.pitch : attitude.values.roll;
727 tfp_sprintf(buff, "%c%02d.%01d", angle < 0 ? '-' : ' ', abs(angle / 10), abs(angle % 10));
728 break;
731 case OSD_MAIN_BATT_USAGE:
733 // Set length of indicator bar
734 #define MAIN_BATT_USAGE_STEPS 11 // Use an odd number so the bar can be centered.
736 // Calculate constrained value
737 const float value = constrain(batteryConfig()->batteryCapacity - getMAhDrawn(), 0, batteryConfig()->batteryCapacity);
739 // Calculate mAh used progress
740 const uint8_t mAhUsedProgress = ceil((value / (batteryConfig()->batteryCapacity / MAIN_BATT_USAGE_STEPS)));
742 // Create empty battery indicator bar
743 buff[0] = SYM_PB_START;
744 for (int i = 1; i <= MAIN_BATT_USAGE_STEPS; i++) {
745 buff[i] = i <= mAhUsedProgress ? SYM_PB_FULL : SYM_PB_EMPTY;
747 buff[MAIN_BATT_USAGE_STEPS + 1] = SYM_PB_CLOSE;
748 if (mAhUsedProgress > 0 && mAhUsedProgress < MAIN_BATT_USAGE_STEPS) {
749 buff[1 + mAhUsedProgress] = SYM_PB_END;
751 buff[MAIN_BATT_USAGE_STEPS+2] = '\0';
752 break;
755 case OSD_DISARMED:
756 if (!ARMING_FLAG(ARMED)) {
757 tfp_sprintf(buff, "DISARMED");
758 } else {
759 tfp_sprintf(buff, " ");
761 break;
763 case OSD_NUMERICAL_HEADING:
765 const int heading = DECIDEGREES_TO_DEGREES(attitude.values.yaw);
766 tfp_sprintf(buff, "%c%03d", osdGetDirectionSymbolFromHeading(heading), heading);
767 break;
770 case OSD_NUMERICAL_VARIO:
772 const int verticalSpeed = osdGetMetersToSelectedUnit(getEstimatedVario());
773 const char directionSymbol = verticalSpeed < 0 ? SYM_ARROW_SOUTH : SYM_ARROW_NORTH;
774 tfp_sprintf(buff, "%c%01d.%01d", directionSymbol, abs(verticalSpeed / 100), abs((verticalSpeed % 100) / 10));
775 break;
778 #ifdef USE_ESC_SENSOR
779 case OSD_ESC_TMP:
780 tfp_sprintf(buff, "%3d%c", osdConvertTemperatureToSelectedUnit(escData->temperature * 10) / 10, osdGetTemperatureSymbolForSelectedUnit());
781 break;
783 case OSD_ESC_RPM:
784 tfp_sprintf(buff, "%5d", escData == NULL ? 0 : escData->rpm);
785 break;
786 #endif
788 #ifdef USE_RTC_TIME
789 case OSD_RTC_DATETIME:
790 osdFormatRtcDateTime(&buff[0]);
791 break;
792 #endif
794 #ifdef USE_OSD_ADJUSTMENTS
795 case OSD_ADJUSTMENT_RANGE:
796 tfp_sprintf(buff, "%s: %3d", adjustmentRangeName, adjustmentRangeValue);
797 break;
798 #endif
800 #ifdef USE_ADC_INTERNAL
801 case OSD_CORE_TEMPERATURE:
802 tfp_sprintf(buff, "%3d%c", osdConvertTemperatureToSelectedUnit(getCoreTemperatureCelsius() * 10) / 10, osdGetTemperatureSymbolForSelectedUnit());
803 break;
804 #endif
806 default:
807 return false;
810 displayWrite(osdDisplayPort, elemPosX + elemOffsetX, elemPosY, buff);
812 return true;
815 static void osdDrawElements(void)
817 displayClearScreen(osdDisplayPort);
819 // Hide OSD when OSDSW mode is active
820 if (IS_RC_MODE_ACTIVE(BOXOSD)) {
821 return;
824 if (sensors(SENSOR_ACC)) {
825 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
828 osdDrawSingleElement(OSD_MAIN_BATT_VOLTAGE);
829 osdDrawSingleElement(OSD_RSSI_VALUE);
830 osdDrawSingleElement(OSD_CROSSHAIRS);
831 osdDrawSingleElement(OSD_ITEM_TIMER_1);
832 osdDrawSingleElement(OSD_ITEM_TIMER_2);
833 osdDrawSingleElement(OSD_REMAINING_TIME_ESTIMATE);
834 osdDrawSingleElement(OSD_FLYMODE);
835 osdDrawSingleElement(OSD_THROTTLE_POS);
836 osdDrawSingleElement(OSD_VTX_CHANNEL);
837 osdDrawSingleElement(OSD_CURRENT_DRAW);
838 osdDrawSingleElement(OSD_MAH_DRAWN);
839 osdDrawSingleElement(OSD_CRAFT_NAME);
840 osdDrawSingleElement(OSD_ALTITUDE);
841 osdDrawSingleElement(OSD_ROLL_PIDS);
842 osdDrawSingleElement(OSD_PITCH_PIDS);
843 osdDrawSingleElement(OSD_YAW_PIDS);
844 osdDrawSingleElement(OSD_POWER);
845 osdDrawSingleElement(OSD_PIDRATE_PROFILE);
846 osdDrawSingleElement(OSD_WARNINGS);
847 osdDrawSingleElement(OSD_AVG_CELL_VOLTAGE);
848 osdDrawSingleElement(OSD_DEBUG);
849 osdDrawSingleElement(OSD_PITCH_ANGLE);
850 osdDrawSingleElement(OSD_ROLL_ANGLE);
851 osdDrawSingleElement(OSD_MAIN_BATT_USAGE);
852 osdDrawSingleElement(OSD_DISARMED);
853 osdDrawSingleElement(OSD_NUMERICAL_HEADING);
854 osdDrawSingleElement(OSD_NUMERICAL_VARIO);
855 osdDrawSingleElement(OSD_COMPASS_BAR);
857 #ifdef USE_GPS
858 if (sensors(SENSOR_GPS)) {
859 osdDrawSingleElement(OSD_GPS_SATS);
860 osdDrawSingleElement(OSD_GPS_SPEED);
861 osdDrawSingleElement(OSD_GPS_LAT);
862 osdDrawSingleElement(OSD_GPS_LON);
863 osdDrawSingleElement(OSD_HOME_DIST);
864 osdDrawSingleElement(OSD_HOME_DIR);
866 #endif // GPS
868 #ifdef USE_ESC_SENSOR
869 if (feature(FEATURE_ESC_SENSOR)) {
870 osdDrawSingleElement(OSD_ESC_TMP);
871 osdDrawSingleElement(OSD_ESC_RPM);
873 #endif
875 #ifdef USE_RTC_TIME
876 osdDrawSingleElement(OSD_RTC_DATETIME);
877 #endif
879 #ifdef USE_OSD_ADJUSTMENTS
880 osdDrawSingleElement(OSD_ADJUSTMENT_RANGE);
881 #endif
883 #ifdef USE_ADC_INTERNAL
884 osdDrawSingleElement(OSD_CORE_TEMPERATURE);
885 #endif
888 void pgResetFn_osdConfig(osdConfig_t *osdConfig)
890 // Position elements near centre of screen and disabled by default
891 for (int i = 0; i < OSD_ITEM_COUNT; i++) {
892 osdConfig->item_pos[i] = OSD_POS(10, 7);
895 // Always enable warnings elements by default
896 osdConfig->item_pos[OSD_WARNINGS] = OSD_POS(9, 10) | VISIBLE_FLAG;
898 osdConfig->enabled_stats[OSD_STAT_MAX_SPEED] = true;
899 osdConfig->enabled_stats[OSD_STAT_MIN_BATTERY] = true;
900 osdConfig->enabled_stats[OSD_STAT_MIN_RSSI] = true;
901 osdConfig->enabled_stats[OSD_STAT_MAX_CURRENT] = true;
902 osdConfig->enabled_stats[OSD_STAT_USED_MAH] = true;
903 osdConfig->enabled_stats[OSD_STAT_MAX_ALTITUDE] = false;
904 osdConfig->enabled_stats[OSD_STAT_BLACKBOX] = true;
905 osdConfig->enabled_stats[OSD_STAT_END_BATTERY] = false;
906 osdConfig->enabled_stats[OSD_STAT_MAX_DISTANCE] = false;
907 osdConfig->enabled_stats[OSD_STAT_BLACKBOX_NUMBER] = true;
908 osdConfig->enabled_stats[OSD_STAT_TIMER_1] = false;
909 osdConfig->enabled_stats[OSD_STAT_TIMER_2] = true;
910 osdConfig->enabled_stats[OSD_STAT_RTC_DATE_TIME] = false;
912 osdConfig->units = OSD_UNIT_METRIC;
914 // Enable all warnings by default
915 osdConfig->enabledWarnings = UINT16_MAX;
917 osdConfig->timers[OSD_TIMER_1] = OSD_TIMER(OSD_TIMER_SRC_ON, OSD_TIMER_PREC_SECOND, 10);
918 osdConfig->timers[OSD_TIMER_2] = OSD_TIMER(OSD_TIMER_SRC_TOTAL_ARMED, OSD_TIMER_PREC_SECOND, 10);
920 osdConfig->rssi_alarm = 20;
921 osdConfig->cap_alarm = 2200;
922 osdConfig->alt_alarm = 100; // meters or feet depend on configuration
924 osdConfig->ahMaxPitch = 20; // 20 degrees
925 osdConfig->ahMaxRoll = 40; // 40 degrees
928 static void osdDrawLogo(int x, int y)
930 // display logo and help
931 int fontOffset = 160;
932 for (int row = 0; row < 4; row++) {
933 for (int column = 0; column < 24; column++) {
934 if (fontOffset <= SYM_END_OF_FONT)
935 displayWriteChar(osdDisplayPort, x + column, y + row, fontOffset++);
940 void osdInit(displayPort_t *osdDisplayPortToUse)
942 if (!osdDisplayPortToUse) {
943 return;
946 BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(31,31));
948 osdDisplayPort = osdDisplayPortToUse;
949 #ifdef USE_CMS
950 cmsDisplayPortRegister(osdDisplayPort);
951 #endif
953 armState = ARMING_FLAG(ARMED);
955 memset(blinkBits, 0, sizeof(blinkBits));
957 displayClearScreen(osdDisplayPort);
959 osdDrawLogo(3, 1);
961 char string_buffer[30];
962 tfp_sprintf(string_buffer, "V%s", FC_VERSION_STRING);
963 displayWrite(osdDisplayPort, 20, 6, string_buffer);
964 #ifdef USE_CMS
965 displayWrite(osdDisplayPort, 7, 8, CMS_STARTUP_HELP_TEXT1);
966 displayWrite(osdDisplayPort, 11, 9, CMS_STARTUP_HELP_TEXT2);
967 displayWrite(osdDisplayPort, 11, 10, CMS_STARTUP_HELP_TEXT3);
968 #endif
970 #ifdef USE_RTC_TIME
971 char dateTimeBuffer[FORMATTED_DATE_TIME_BUFSIZE];
972 if (osdFormatRtcDateTime(&dateTimeBuffer[0])) {
973 displayWrite(osdDisplayPort, 5, 12, dateTimeBuffer);
975 #endif
977 displayResync(osdDisplayPort);
979 resumeRefreshAt = micros() + (4 * REFRESH_1S);
982 void osdUpdateAlarms(void)
984 // This is overdone?
986 int32_t alt = osdGetMetersToSelectedUnit(getEstimatedAltitude()) / 100;
988 if (statRssi < osdConfig()->rssi_alarm) {
989 SET_BLINK(OSD_RSSI_VALUE);
990 } else {
991 CLR_BLINK(OSD_RSSI_VALUE);
994 if (getBatteryState() == BATTERY_OK) {
995 CLR_BLINK(OSD_WARNINGS);
996 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE);
997 CLR_BLINK(OSD_AVG_CELL_VOLTAGE);
998 } else {
999 SET_BLINK(OSD_WARNINGS);
1000 SET_BLINK(OSD_MAIN_BATT_VOLTAGE);
1001 SET_BLINK(OSD_AVG_CELL_VOLTAGE);
1004 if (STATE(GPS_FIX) == 0) {
1005 SET_BLINK(OSD_GPS_SATS);
1006 } else {
1007 CLR_BLINK(OSD_GPS_SATS);
1010 for (int i = 0; i < OSD_TIMER_COUNT; i++) {
1011 const uint16_t timer = osdConfig()->timers[i];
1012 const timeUs_t time = osdGetTimerValue(OSD_TIMER_SRC(timer));
1013 const timeUs_t alarmTime = OSD_TIMER_ALARM(timer) * 60000000; // convert from minutes to us
1014 if (alarmTime != 0 && time >= alarmTime) {
1015 SET_BLINK(OSD_ITEM_TIMER_1 + i);
1016 } else {
1017 CLR_BLINK(OSD_ITEM_TIMER_1 + i);
1021 if (getMAhDrawn() >= osdConfig()->cap_alarm) {
1022 SET_BLINK(OSD_MAH_DRAWN);
1023 SET_BLINK(OSD_MAIN_BATT_USAGE);
1024 SET_BLINK(OSD_REMAINING_TIME_ESTIMATE);
1025 } else {
1026 CLR_BLINK(OSD_MAH_DRAWN);
1027 CLR_BLINK(OSD_MAIN_BATT_USAGE);
1028 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE);
1031 if (alt >= osdConfig()->alt_alarm) {
1032 SET_BLINK(OSD_ALTITUDE);
1033 } else {
1034 CLR_BLINK(OSD_ALTITUDE);
1038 void osdResetAlarms(void)
1040 CLR_BLINK(OSD_RSSI_VALUE);
1041 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE);
1042 CLR_BLINK(OSD_WARNINGS);
1043 CLR_BLINK(OSD_GPS_SATS);
1044 CLR_BLINK(OSD_MAH_DRAWN);
1045 CLR_BLINK(OSD_ALTITUDE);
1046 CLR_BLINK(OSD_AVG_CELL_VOLTAGE);
1047 CLR_BLINK(OSD_MAIN_BATT_USAGE);
1048 CLR_BLINK(OSD_ITEM_TIMER_1);
1049 CLR_BLINK(OSD_ITEM_TIMER_2);
1050 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE);
1053 static void osdResetStats(void)
1055 stats.max_current = 0;
1056 stats.max_speed = 0;
1057 stats.min_voltage = 500;
1058 stats.max_current = 0;
1059 stats.min_rssi = 99;
1060 stats.max_altitude = 0;
1061 stats.max_distance = 0;
1062 stats.armed_time = 0;
1065 static void osdUpdateStats(void)
1067 int16_t value = 0;
1069 #ifdef USE_GPS
1070 switch (osdConfig()->units) {
1071 case OSD_UNIT_IMPERIAL:
1072 value = CM_S_TO_MPH(gpsSol.groundSpeed);
1073 break;
1074 default:
1075 value = CM_S_TO_KM_H(gpsSol.groundSpeed);
1076 break;
1078 #endif
1079 if (stats.max_speed < value) {
1080 stats.max_speed = value;
1083 if (stats.min_voltage > getBatteryVoltage()) {
1084 stats.min_voltage = getBatteryVoltage();
1087 value = getAmperage() / 100;
1088 if (stats.max_current < value) {
1089 stats.max_current = value;
1092 statRssi = scaleRange(getRssi(), 0, 1024, 0, 100);
1093 if (stats.min_rssi > statRssi) {
1094 stats.min_rssi = statRssi;
1097 if (stats.max_altitude < getEstimatedAltitude()) {
1098 stats.max_altitude = getEstimatedAltitude();
1101 #ifdef USE_GPS
1102 if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && (stats.max_distance < GPS_distanceToHome)) {
1103 stats.max_distance = GPS_distanceToHome;
1105 #endif
1108 #ifdef USE_BLACKBOX
1109 static void osdGetBlackboxStatusString(char * buff)
1111 bool storageDeviceIsWorking = false;
1112 uint32_t storageUsed = 0;
1113 uint32_t storageTotal = 0;
1115 switch (blackboxConfig()->device) {
1116 #ifdef USE_SDCARD
1117 case BLACKBOX_DEVICE_SDCARD:
1118 storageDeviceIsWorking = sdcard_isInserted() && sdcard_isFunctional() && (afatfs_getFilesystemState() == AFATFS_FILESYSTEM_STATE_READY);
1119 if (storageDeviceIsWorking) {
1120 storageTotal = sdcard_getMetadata()->numBlocks / 2000;
1121 storageUsed = storageTotal - (afatfs_getContiguousFreeSpace() / 1024000);
1123 break;
1124 #endif
1126 #ifdef USE_FLASHFS
1127 case BLACKBOX_DEVICE_FLASH:
1128 storageDeviceIsWorking = flashfsIsReady();
1129 if (storageDeviceIsWorking) {
1130 const flashGeometry_t *geometry = flashfsGetGeometry();
1131 storageTotal = geometry->totalSize / 1024;
1132 storageUsed = flashfsGetOffset() / 1024;
1134 break;
1135 #endif
1137 default:
1138 break;
1141 if (storageDeviceIsWorking) {
1142 const uint16_t storageUsedPercent = (storageUsed * 100) / storageTotal;
1143 tfp_sprintf(buff, "%d%%", storageUsedPercent);
1144 } else {
1145 tfp_sprintf(buff, "FAULT");
1148 #endif
1150 static void osdDisplayStatisticLabel(uint8_t y, const char * text, const char * value)
1152 displayWrite(osdDisplayPort, 2, y, text);
1153 displayWrite(osdDisplayPort, 20, y, ":");
1154 displayWrite(osdDisplayPort, 22, y, value);
1158 * Test if there's some stat enabled
1160 static bool isSomeStatEnabled(void)
1162 for (int i = 0; i < OSD_STAT_COUNT; i++) {
1163 if (osdConfig()->enabled_stats[i]) {
1164 return true;
1167 return false;
1170 static void osdShowStats(void)
1172 uint8_t top = 2;
1173 char buff[OSD_ELEMENT_BUFFER_LENGTH];
1175 displayClearScreen(osdDisplayPort);
1176 displayWrite(osdDisplayPort, 2, top++, " --- STATS ---");
1178 if (osdConfig()->enabled_stats[OSD_STAT_RTC_DATE_TIME]) {
1179 bool success = false;
1180 #ifdef USE_RTC_TIME
1181 success = osdFormatRtcDateTime(&buff[0]);
1182 #endif
1183 if (!success) {
1184 tfp_sprintf(buff, "NO RTC");
1187 displayWrite(osdDisplayPort, 2, top++, buff);
1190 if (osdConfig()->enabled_stats[OSD_STAT_TIMER_1]) {
1191 osdFormatTimer(buff, false, OSD_TIMER_1);
1192 osdDisplayStatisticLabel(top++, osdTimerSourceNames[OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_1])], buff);
1195 if (osdConfig()->enabled_stats[OSD_STAT_TIMER_2]) {
1196 osdFormatTimer(buff, false, OSD_TIMER_2);
1197 osdDisplayStatisticLabel(top++, osdTimerSourceNames[OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_2])], buff);
1200 if (osdConfig()->enabled_stats[OSD_STAT_MAX_SPEED] && STATE(GPS_FIX)) {
1201 itoa(stats.max_speed, buff, 10);
1202 osdDisplayStatisticLabel(top++, "MAX SPEED", buff);
1205 if (osdConfig()->enabled_stats[OSD_STAT_MAX_DISTANCE]) {
1206 tfp_sprintf(buff, "%d%c", osdGetMetersToSelectedUnit(stats.max_distance), osdGetMetersToSelectedUnitSymbol());
1207 osdDisplayStatisticLabel(top++, "MAX DISTANCE", buff);
1210 if (osdConfig()->enabled_stats[OSD_STAT_MIN_BATTERY]) {
1211 tfp_sprintf(buff, "%d.%1d%c", stats.min_voltage / 10, stats.min_voltage % 10, SYM_VOLT);
1212 osdDisplayStatisticLabel(top++, "MIN BATTERY", buff);
1215 if (osdConfig()->enabled_stats[OSD_STAT_END_BATTERY]) {
1216 tfp_sprintf(buff, "%d.%1d%c", getBatteryVoltage() / 10, getBatteryVoltage() % 10, SYM_VOLT);
1217 osdDisplayStatisticLabel(top++, "END BATTERY", buff);
1220 if (osdConfig()->enabled_stats[OSD_STAT_MIN_RSSI]) {
1221 itoa(stats.min_rssi, buff, 10);
1222 strcat(buff, "%");
1223 osdDisplayStatisticLabel(top++, "MIN RSSI", buff);
1226 if (batteryConfig()->currentMeterSource != CURRENT_METER_NONE) {
1227 if (osdConfig()->enabled_stats[OSD_STAT_MAX_CURRENT]) {
1228 itoa(stats.max_current, buff, 10);
1229 strcat(buff, "A");
1230 osdDisplayStatisticLabel(top++, "MAX CURRENT", buff);
1233 if (osdConfig()->enabled_stats[OSD_STAT_USED_MAH]) {
1234 tfp_sprintf(buff, "%d%c", getMAhDrawn(), SYM_MAH);
1235 osdDisplayStatisticLabel(top++, "USED MAH", buff);
1239 if (osdConfig()->enabled_stats[OSD_STAT_MAX_ALTITUDE]) {
1240 osdFormatAltitudeString(buff, stats.max_altitude, false);
1241 osdDisplayStatisticLabel(top++, "MAX ALTITUDE", buff);
1244 #ifdef USE_BLACKBOX
1245 if (osdConfig()->enabled_stats[OSD_STAT_BLACKBOX] && blackboxConfig()->device && blackboxConfig()->device != BLACKBOX_DEVICE_SERIAL) {
1246 osdGetBlackboxStatusString(buff);
1247 osdDisplayStatisticLabel(top++, "BLACKBOX", buff);
1250 if (osdConfig()->enabled_stats[OSD_STAT_BLACKBOX_NUMBER] && blackboxConfig()->device && blackboxConfig()->device != BLACKBOX_DEVICE_SERIAL) {
1251 itoa(blackboxGetLogNumber(), buff, 10);
1252 osdDisplayStatisticLabel(top++, "BB LOG NUM", buff);
1254 #endif
1256 // Reset time since last armed here to ensure this timer is at zero when back at "main" OSD screen
1257 stats.armed_time = 0;
1260 static void osdShowArmed(void)
1262 displayClearScreen(osdDisplayPort);
1263 displayWrite(osdDisplayPort, 12, 7, "ARMED");
1266 STATIC_UNIT_TESTED void osdRefresh(timeUs_t currentTimeUs)
1268 static timeUs_t lastTimeUs = 0;
1270 // detect arm/disarm
1271 if (armState != ARMING_FLAG(ARMED)) {
1272 if (ARMING_FLAG(ARMED)) {
1273 osdResetStats();
1274 osdShowArmed();
1275 resumeRefreshAt = currentTimeUs + (REFRESH_1S / 2);
1276 } else if (isSomeStatEnabled()
1277 && (!(getArmingDisableFlags() & ARMING_DISABLED_RUNAWAY_TAKEOFF)
1278 || !VISIBLE(osdConfig()->item_pos[OSD_WARNINGS]))) { // suppress stats if runaway takeoff triggered disarm and WARNINGS element is visible
1279 osdShowStats();
1280 resumeRefreshAt = currentTimeUs + (60 * REFRESH_1S);
1283 armState = ARMING_FLAG(ARMED);
1286 osdUpdateStats();
1288 if (ARMING_FLAG(ARMED)) {
1289 timeUs_t deltaT = currentTimeUs - lastTimeUs;
1290 flyTime += deltaT;
1291 stats.armed_time += deltaT;
1293 lastTimeUs = currentTimeUs;
1295 if (resumeRefreshAt) {
1296 if (cmp32(currentTimeUs, resumeRefreshAt) < 0) {
1297 // in timeout period, check sticks for activity to resume display.
1298 if (IS_HI(THROTTLE) || IS_HI(PITCH)) {
1299 resumeRefreshAt = 0;
1302 displayHeartbeat(osdDisplayPort);
1303 return;
1304 } else {
1305 displayClearScreen(osdDisplayPort);
1306 resumeRefreshAt = 0;
1310 blinkState = (currentTimeUs / 200000) % 2;
1312 #ifdef USE_ESC_SENSOR
1313 if (feature(FEATURE_ESC_SENSOR)) {
1314 escData = getEscSensorData(ESC_SENSOR_COMBINED);
1316 #endif
1318 #ifdef USE_CMS
1319 if (!displayIsGrabbed(osdDisplayPort)) {
1320 osdUpdateAlarms();
1321 osdDrawElements();
1322 displayHeartbeat(osdDisplayPort);
1323 #ifdef OSD_CALLS_CMS
1324 } else {
1325 cmsUpdate(currentTimeUs);
1326 #endif
1328 #endif
1332 * Called periodically by the scheduler
1334 void osdUpdate(timeUs_t currentTimeUs)
1336 static uint32_t counter = 0;
1338 if (isBeeperOn()) {
1339 showVisualBeeper = true;
1342 #ifdef MAX7456_DMA_CHANNEL_TX
1343 // don't touch buffers if DMA transaction is in progress
1344 if (displayIsTransferInProgress(osdDisplayPort)) {
1345 return;
1347 #endif // MAX7456_DMA_CHANNEL_TX
1349 #ifdef USE_SLOW_MSP_DISPLAYPORT_RATE_WHEN_UNARMED
1350 static uint32_t idlecounter = 0;
1351 if (!ARMING_FLAG(ARMED)) {
1352 if (idlecounter++ % 4 != 0) {
1353 return;
1356 #endif
1358 // redraw values in buffer
1359 #ifdef USE_MAX7456
1360 #define DRAW_FREQ_DENOM 5
1361 #else
1362 #define DRAW_FREQ_DENOM 10 // MWOSD @ 115200 baud (
1363 #endif
1364 #define STATS_FREQ_DENOM 50
1366 if (counter % DRAW_FREQ_DENOM == 0) {
1367 osdRefresh(currentTimeUs);
1368 showVisualBeeper = false;
1369 } else {
1370 // rest of time redraw screen 10 chars per idle so it doesn't lock the main idle
1371 displayDrawScreen(osdDisplayPort);
1373 ++counter;
1375 #ifdef USE_CMS
1376 // do not allow ARM if we are in menu
1377 if (displayIsGrabbed(osdDisplayPort)) {
1378 setArmingDisabled(ARMING_DISABLED_OSD_MENU);
1379 } else {
1380 unsetArmingDisabled(ARMING_DISABLED_OSD_MENU);
1382 #endif
1385 #endif // USE_OSD