2 * This file is part of Cleanflight.
4 * Cleanflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Cleanflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
19 Created by Marcin Baliniak
20 some functions based on MinimOSD
22 OSD-CMS separation by jflyper
36 #include "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"
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"
74 #include "io/vtx_string.h"
78 #include "pg/pg_ids.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"
92 #define VIDEO_BUFFER_CHARS_PAL 480
93 #define FULL_CIRCLE 360
95 const char * const osdTimerSourceNames
[] = {
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
{
124 int16_t min_voltage
; // /10
125 int16_t max_current
; // /10
127 int16_t max_altitude
;
128 int16_t max_distance
;
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
;
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
[] = {
150 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
152 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
154 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
156 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
158 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
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
:
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
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
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;
218 return tempInDeciDegrees
;
222 static char osdGetTemperatureSymbolForSelectedUnit(void)
224 switch (osdConfig()->units
) {
225 case OSD_UNIT_IMPERIAL
:
233 static void osdFormatAltitudeString(char * buff
, int altitude
, bool pad
)
235 const int alt
= osdGetMetersToSelectedUnit(altitude
);
236 int altitudeIntergerPart
= abs(alt
/ 100);
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
)
278 case OSD_TIMER_SRC_ON
:
280 case OSD_TIMER_SRC_TOTAL_ARMED
:
281 case OSD_TIMER_SRC_LAST_ARMED
:
288 static timeUs_t
osdGetTimerValue(osd_timer_source_e src
)
291 case OSD_TIMER_SRC_ON
:
293 case OSD_TIMER_SRC_TOTAL_ARMED
:
295 case OSD_TIMER_SRC_LAST_ARMED
:
296 return stats
.armed_time
;
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;
309 case OSD_TIMER_PREC_SECOND
:
311 tfp_sprintf(buff
, "%02d:%02d", minutes
, seconds
);
313 case OSD_TIMER_PREC_HUNDREDTHS
:
315 const int hundredths
= (time
/ 10000) % 100;
316 tfp_sprintf(buff
, "%02d:%02d.%02d", minutes
, seconds
, hundredths
);
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
);
328 *(buff
++) = osdGetTimerSymbol(src
);
331 osdFormatTime(buff
, OSD_TIMER_PRECISION(timer
), osdGetTimerValue(src
));
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
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';
357 static bool osdFormatRtcDateTime(char *buffer
)
360 if (!rtcGetDateTime(&dateTime
)) {
366 dateTimeFormatLocalShort(buffer
, &dateTime
);
372 static void osdFormatMessage(char *buff
, size_t size
, const char *message
)
374 memset(buff
, SYM_BLANK
, size
);
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
)) {
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
];
396 uint16_t osdRssi
= getRssi() * 100 / 1024; // change range
400 tfp_sprintf(buff
, "%c%2d", SYM_RSSI
, osdRssi
);
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
);
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
);
417 tfp_sprintf(buff
, "%4d%c", getMAhDrawn(), SYM_MAH
);
422 tfp_sprintf(buff
, "%c%c%2d", SYM_SAT_L
, SYM_SAT_R
, gpsSol
.numSat
);
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
));
432 tfp_sprintf(buff
, "%3dK", CM_S_TO_KM_H(gpsSol
.groundSpeed
));
438 osdFormatCoordinate(buff
, SYM_LAT
, gpsSol
.llh
.lat
);
442 osdFormatCoordinate(buff
, SYM_LON
, gpsSol
.llh
.lon
);
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
);
451 // We don't have a HOME symbol in the font, by now we use this
456 // We use this symbol when we don't have a FIX
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());
469 // We use this symbol when we don't have a FIX
471 // overwrite any previous distance with blanks
472 memset(buff
+ 1, SYM_BLANK
, 6);
479 case OSD_COMPASS_BAR
:
480 memcpy(buff
, compassBar
+ osdGetHeadingIntoDiscreteDirections(DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
), 16), 9);
485 osdFormatAltitudeString(buff
, getEstimatedAltitude(), true);
488 case OSD_ITEM_TIMER_1
:
489 case OSD_ITEM_TIMER_2
:
490 osdFormatTimer(buff
, true, item
- OSD_ITEM_TIMER_1
);
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");
503 osdFormatTime(buff
, OSD_TIMER_PREC_SECOND
, remaining_time
);
512 if (isAirmodeActive()) {
516 if (FLIGHT_MODE(FAILSAFE_MODE
)) {
518 } else if (FLIGHT_MODE(ANGLE_MODE
)) {
520 } else if (FLIGHT_MODE(HORIZON_MODE
)) {
524 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, p
);
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");
536 for (i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
537 if (pilotConfig()->name
[i
]) {
538 buff
[i
] = toupper((unsigned char)pilotConfig()->name
[i
]);
548 case OSD_THROTTLE_POS
:
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
));
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
);
570 elemPosX
= 14 - 1; // Offset for 1 char to the left
572 if (displayScreenSize(osdDisplayPort
) == VIDEO_BUFFER_CHARS_PAL
) {
575 buff
[0] = SYM_AH_CENTER_LINE
;
576 buff
[1] = SYM_AH_CENTER
;
577 buff
[2] = SYM_AH_CENTER_LINE_RIGHT
;
581 case OSD_ARTIFICIAL_HORIZON
:
584 elemPosY
= 6 - 4; // Top center of the AH area
585 if (displayScreenSize(osdDisplayPort
) == VIDEO_BUFFER_CHARS_PAL
) {
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
);
610 case OSD_HORIZON_SIDEBARS
:
614 if (displayScreenSize(osdDisplayPort
) == VIDEO_BUFFER_CHARS_PAL
) {
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
);
634 osdFormatPID(buff
, "ROL", ¤tPidProfile
->pid
[PID_ROLL
]);
638 osdFormatPID(buff
, "PIT", ¤tPidProfile
->pid
[PID_PITCH
]);
642 osdFormatPID(buff
, "YAW", ¤tPidProfile
->pid
[PID_YAW
]);
646 tfp_sprintf(buff
, "%4dW", getAmperage() * getBatteryVoltage() / 1000);
649 case OSD_PIDRATE_PROFILE
:
650 tfp_sprintf(buff
, "%d-%d", getCurrentPidProfileIndex() + 1, getCurrentControlRateProfileIndex() + 1);
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");
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");
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
]);
689 if (enabledWarnings
& OSD_WARNING_BATTERY_WARNING
&& batteryState
== BATTERY_WARNING
) {
690 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, "LOW BATTERY");
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");
702 if (enabledWarnings
& OSD_WARNING_VISUAL_BEEPER
&& showVisualBeeper
) {
703 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, " * * * *");
707 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, NULL
);
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
);
720 tfp_sprintf(buff
, "DBG %5d %5d %5d %5d", debug
[0], debug
[1], debug
[2], debug
[3]);
723 case OSD_PITCH_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));
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';
756 if (!ARMING_FLAG(ARMED
)) {
757 tfp_sprintf(buff
, "DISARMED");
759 tfp_sprintf(buff
, " ");
763 case OSD_NUMERICAL_HEADING
:
765 const int heading
= DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
766 tfp_sprintf(buff
, "%c%03d", osdGetDirectionSymbolFromHeading(heading
), heading
);
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));
778 #ifdef USE_ESC_SENSOR
780 tfp_sprintf(buff
, "%3d%c", osdConvertTemperatureToSelectedUnit(escData
->temperature
* 10) / 10, osdGetTemperatureSymbolForSelectedUnit());
784 tfp_sprintf(buff
, "%5d", escData
== NULL
? 0 : escData
->rpm
);
789 case OSD_RTC_DATETIME
:
790 osdFormatRtcDateTime(&buff
[0]);
794 #ifdef USE_OSD_ADJUSTMENTS
795 case OSD_ADJUSTMENT_RANGE
:
796 tfp_sprintf(buff
, "%s: %3d", adjustmentRangeName
, adjustmentRangeValue
);
800 #ifdef USE_ADC_INTERNAL
801 case OSD_CORE_TEMPERATURE
:
802 tfp_sprintf(buff
, "%3d%c", osdConvertTemperatureToSelectedUnit(getCoreTemperatureCelsius() * 10) / 10, osdGetTemperatureSymbolForSelectedUnit());
810 displayWrite(osdDisplayPort
, elemPosX
+ elemOffsetX
, elemPosY
, buff
);
815 static void osdDrawElements(void)
817 displayClearScreen(osdDisplayPort
);
819 // Hide OSD when OSDSW mode is active
820 if (IS_RC_MODE_ACTIVE(BOXOSD
)) {
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
);
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
);
868 #ifdef USE_ESC_SENSOR
869 if (feature(FEATURE_ESC_SENSOR
)) {
870 osdDrawSingleElement(OSD_ESC_TMP
);
871 osdDrawSingleElement(OSD_ESC_RPM
);
876 osdDrawSingleElement(OSD_RTC_DATETIME
);
879 #ifdef USE_OSD_ADJUSTMENTS
880 osdDrawSingleElement(OSD_ADJUSTMENT_RANGE
);
883 #ifdef USE_ADC_INTERNAL
884 osdDrawSingleElement(OSD_CORE_TEMPERATURE
);
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
) {
946 BUILD_BUG_ON(OSD_POS_MAX
!= OSD_POS(31,31));
948 osdDisplayPort
= osdDisplayPortToUse
;
950 cmsDisplayPortRegister(osdDisplayPort
);
953 armState
= ARMING_FLAG(ARMED
);
955 memset(blinkBits
, 0, sizeof(blinkBits
));
957 displayClearScreen(osdDisplayPort
);
961 char string_buffer
[30];
962 tfp_sprintf(string_buffer
, "V%s", FC_VERSION_STRING
);
963 displayWrite(osdDisplayPort
, 20, 6, string_buffer
);
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
);
971 char dateTimeBuffer
[FORMATTED_DATE_TIME_BUFSIZE
];
972 if (osdFormatRtcDateTime(&dateTimeBuffer
[0])) {
973 displayWrite(osdDisplayPort
, 5, 12, dateTimeBuffer
);
977 displayResync(osdDisplayPort
);
979 resumeRefreshAt
= micros() + (4 * REFRESH_1S
);
982 void osdUpdateAlarms(void)
986 int32_t alt
= osdGetMetersToSelectedUnit(getEstimatedAltitude()) / 100;
988 if (statRssi
< osdConfig()->rssi_alarm
) {
989 SET_BLINK(OSD_RSSI_VALUE
);
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
);
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
);
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
);
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
);
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
);
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)
1070 switch (osdConfig()->units
) {
1071 case OSD_UNIT_IMPERIAL
:
1072 value
= CM_S_TO_MPH(gpsSol
.groundSpeed
);
1075 value
= CM_S_TO_KM_H(gpsSol
.groundSpeed
);
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();
1102 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
) && (stats
.max_distance
< GPS_distanceToHome
)) {
1103 stats
.max_distance
= GPS_distanceToHome
;
1109 static void osdGetBlackboxStatusString(char * buff
)
1111 bool storageDeviceIsWorking
= false;
1112 uint32_t storageUsed
= 0;
1113 uint32_t storageTotal
= 0;
1115 switch (blackboxConfig()->device
) {
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);
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;
1141 if (storageDeviceIsWorking
) {
1142 const uint16_t storageUsedPercent
= (storageUsed
* 100) / storageTotal
;
1143 tfp_sprintf(buff
, "%d%%", storageUsedPercent
);
1145 tfp_sprintf(buff
, "FAULT");
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
]) {
1170 static void osdShowStats(void)
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;
1181 success
= osdFormatRtcDateTime(&buff
[0]);
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);
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);
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
);
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
);
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
)) {
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
1280 resumeRefreshAt
= currentTimeUs
+ (60 * REFRESH_1S
);
1283 armState
= ARMING_FLAG(ARMED
);
1288 if (ARMING_FLAG(ARMED
)) {
1289 timeUs_t deltaT
= currentTimeUs
- lastTimeUs
;
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
);
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
);
1319 if (!displayIsGrabbed(osdDisplayPort
)) {
1322 displayHeartbeat(osdDisplayPort
);
1323 #ifdef OSD_CALLS_CMS
1325 cmsUpdate(currentTimeUs
);
1332 * Called periodically by the scheduler
1334 void osdUpdate(timeUs_t currentTimeUs
)
1336 static uint32_t counter
= 0;
1339 showVisualBeeper
= true;
1342 #ifdef MAX7456_DMA_CHANNEL_TX
1343 // don't touch buffers if DMA transaction is in progress
1344 if (displayIsTransferInProgress(osdDisplayPort
)) {
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) {
1358 // redraw values in buffer
1360 #define DRAW_FREQ_DENOM 5
1362 #define DRAW_FREQ_DENOM 10 // MWOSD @ 115200 baud (
1364 #define STATS_FREQ_DENOM 50
1366 if (counter
% DRAW_FREQ_DENOM
== 0) {
1367 osdRefresh(currentTimeUs
);
1368 showVisualBeeper
= false;
1370 // rest of time redraw screen 10 chars per idle so it doesn't lock the main idle
1371 displayDrawScreen(osdDisplayPort
);
1376 // do not allow ARM if we are in menu
1377 if (displayIsGrabbed(osdDisplayPort
)) {
1378 setArmingDisabled(ARMING_DISABLED_OSD_MENU
);
1380 unsetArmingDisabled(ARMING_DISABLED_OSD_MENU
);