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.
427 tfp_sprintf(buff
, "%3dK", CM_S_TO_KM_H(gpsSol
.groundSpeed
));
431 osdFormatCoordinate(buff
, SYM_LAT
, gpsSol
.llh
.lat
);
435 osdFormatCoordinate(buff
, SYM_LON
, gpsSol
.llh
.lon
);
439 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
440 if (GPS_distanceToHome
> 0) {
441 const int h
= GPS_directionToHome
- DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
442 buff
[0] = osdGetDirectionSymbolFromHeading(h
);
444 // We don't have a HOME symbol in the font, by now we use this
449 // We use this symbol when we don't have a FIX
458 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
459 const int32_t distance
= osdGetMetersToSelectedUnit(GPS_distanceToHome
);
460 tfp_sprintf(buff
, "%d%c", distance
, osdGetMetersToSelectedUnitSymbol());
462 // We use this symbol when we don't have a FIX
464 // overwrite any previous distance with blanks
465 memset(buff
+ 1, SYM_BLANK
, 6);
472 case OSD_COMPASS_BAR
:
473 memcpy(buff
, compassBar
+ osdGetHeadingIntoDiscreteDirections(DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
), 16), 9);
478 osdFormatAltitudeString(buff
, getEstimatedAltitude(), true);
481 case OSD_ITEM_TIMER_1
:
482 case OSD_ITEM_TIMER_2
:
483 osdFormatTimer(buff
, true, item
- OSD_ITEM_TIMER_1
);
486 case OSD_REMAINING_TIME_ESTIMATE
:
488 const int mAhDrawn
= getMAhDrawn();
489 const int remaining_time
= (int)((osdConfig()->cap_alarm
- mAhDrawn
) * ((float)flyTime
) / mAhDrawn
);
491 if (mAhDrawn
< 0.1 * osdConfig()->cap_alarm
) {
492 tfp_sprintf(buff
, "--:--");
493 } else if (mAhDrawn
> osdConfig()->cap_alarm
) {
494 tfp_sprintf(buff
, "00:00");
496 osdFormatTime(buff
, OSD_TIMER_PREC_SECOND
, remaining_time
);
505 if (isAirmodeActive()) {
509 if (FLIGHT_MODE(FAILSAFE_MODE
)) {
511 } else if (FLIGHT_MODE(ANGLE_MODE
)) {
513 } else if (FLIGHT_MODE(HORIZON_MODE
)) {
517 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, p
);
522 // 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.
523 //TODO: When iterative updating is implemented, change this so the craft name is only printed once whenever the OSD 'flight' screen is entered.
525 if (strlen(pilotConfig()->name
) == 0) {
526 strcpy(buff
, "CRAFT_NAME");
529 for (i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
530 if (pilotConfig()->name
[i
]) {
531 buff
[i
] = toupper((unsigned char)pilotConfig()->name
[i
]);
541 case OSD_THROTTLE_POS
:
544 tfp_sprintf(buff
+ 2, "%3d", (constrain(rcData
[THROTTLE
], PWM_RANGE_MIN
, PWM_RANGE_MAX
) - PWM_RANGE_MIN
) * 100 / (PWM_RANGE_MAX
- PWM_RANGE_MIN
));
547 #if defined(USE_VTX_COMMON)
548 case OSD_VTX_CHANNEL
:
550 const char vtxBandLetter
= vtx58BandLetter
[vtxSettingsConfig()->band
];
551 const char *vtxChannelName
= vtx58ChannelNames
[vtxSettingsConfig()->channel
];
552 uint8_t vtxPower
= vtxSettingsConfig()->power
;
553 const vtxDevice_t
*vtxDevice
= vtxCommonDevice();
554 if (vtxDevice
&& vtxSettingsConfig()->lowPowerDisarm
) {
555 vtxCommonGetPowerIndex(vtxDevice
, &vtxPower
);
557 tfp_sprintf(buff
, "%c:%s:%2d", vtxBandLetter
, vtxChannelName
, vtxPower
);
563 elemPosX
= 14 - 1; // Offset for 1 char to the left
565 if (displayScreenSize(osdDisplayPort
) == VIDEO_BUFFER_CHARS_PAL
) {
568 buff
[0] = SYM_AH_CENTER_LINE
;
569 buff
[1] = SYM_AH_CENTER
;
570 buff
[2] = SYM_AH_CENTER_LINE_RIGHT
;
574 case OSD_ARTIFICIAL_HORIZON
:
577 elemPosY
= 6 - 4; // Top center of the AH area
578 if (displayScreenSize(osdDisplayPort
) == VIDEO_BUFFER_CHARS_PAL
) {
582 // Get pitch and roll limits in tenths of degrees
583 const int maxPitch
= osdConfig()->ahMaxPitch
* 10;
584 const int maxRoll
= osdConfig()->ahMaxRoll
* 10;
585 const int rollAngle
= constrain(attitude
.values
.roll
, -maxRoll
, maxRoll
);
586 int pitchAngle
= constrain(attitude
.values
.pitch
, -maxPitch
, maxPitch
);
587 // Convert pitchAngle to y compensation value
588 // (maxPitch / 25) divisor matches previous settings of fixed divisor of 8 and fixed max AHI pitch angle of 20.0 degrees
589 pitchAngle
= ((pitchAngle
* 25) / maxPitch
) - 41; // 41 = 4 * AH_SYMBOL_COUNT + 5
591 for (int x
= -4; x
<= 4; x
++) {
592 const int y
= ((-rollAngle
* x
) / 64) - pitchAngle
;
593 if (y
>= 0 && y
<= 81) {
594 displayWriteChar(osdDisplayPort
, elemPosX
+ x
, elemPosY
+ (y
/ AH_SYMBOL_COUNT
), (SYM_AH_BAR9_0
+ (y
% AH_SYMBOL_COUNT
)));
598 osdDrawSingleElement(OSD_HORIZON_SIDEBARS
);
603 case OSD_HORIZON_SIDEBARS
:
607 if (displayScreenSize(osdDisplayPort
) == VIDEO_BUFFER_CHARS_PAL
) {
612 const int8_t hudwidth
= AH_SIDEBAR_WIDTH_POS
;
613 const int8_t hudheight
= AH_SIDEBAR_HEIGHT_POS
;
614 for (int y
= -hudheight
; y
<= hudheight
; y
++) {
615 displayWriteChar(osdDisplayPort
, elemPosX
- hudwidth
, elemPosY
+ y
, SYM_AH_DECORATION
);
616 displayWriteChar(osdDisplayPort
, elemPosX
+ hudwidth
, elemPosY
+ y
, SYM_AH_DECORATION
);
619 // AH level indicators
620 displayWriteChar(osdDisplayPort
, elemPosX
- hudwidth
+ 1, elemPosY
, SYM_AH_LEFT
);
621 displayWriteChar(osdDisplayPort
, elemPosX
+ hudwidth
- 1, elemPosY
, SYM_AH_RIGHT
);
627 osdFormatPID(buff
, "ROL", ¤tPidProfile
->pid
[PID_ROLL
]);
631 osdFormatPID(buff
, "PIT", ¤tPidProfile
->pid
[PID_PITCH
]);
635 osdFormatPID(buff
, "YAW", ¤tPidProfile
->pid
[PID_YAW
]);
639 tfp_sprintf(buff
, "%4dW", getAmperage() * getBatteryVoltage() / 1000);
642 case OSD_PIDRATE_PROFILE
:
643 tfp_sprintf(buff
, "%d-%d", getCurrentPidProfileIndex() + 1, getCurrentControlRateProfileIndex() + 1);
649 #define OSD_WARNINGS_MAX_SIZE 11
650 #define OSD_FORMAT_MESSAGE_BUFFER_SIZE (OSD_WARNINGS_MAX_SIZE + 1)
652 const uint16_t enabledWarnings
= osdConfig()->enabledWarnings
;
654 const batteryState_e batteryState
= getBatteryState();
656 if (enabledWarnings
& OSD_WARNING_BATTERY_CRITICAL
&& batteryState
== BATTERY_CRITICAL
) {
657 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, " LAND NOW");
661 // Warn when in flip over after crash mode
662 if ((enabledWarnings
& OSD_WARNING_CRASH_FLIP
)
663 && (isFlipOverAfterCrashMode())) {
664 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, "CRASH FLIP");
668 // Show most severe reason for arming being disabled
669 if (enabledWarnings
& OSD_WARNING_ARMING_DISABLE
&& IS_RC_MODE_ACTIVE(BOXARM
) && isArmingDisabled()) {
670 const armingDisableFlags_e flags
= getArmingDisableFlags();
671 for (int i
= 0; i
< ARMING_DISABLE_FLAGS_COUNT
; i
++) {
672 if (flags
& (1 << i
)) {
673 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, armingDisableFlagNames
[i
]);
680 if (enabledWarnings
& OSD_WARNING_BATTERY_WARNING
&& batteryState
== BATTERY_WARNING
) {
681 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, "LOW BATTERY");
685 // Show warning if battery is not fresh
686 if (enabledWarnings
& OSD_WARNING_BATTERY_NOT_FULL
&& !ARMING_FLAG(WAS_EVER_ARMED
) && (getBatteryState() == BATTERY_OK
)
687 && getBatteryAverageCellVoltage() < batteryConfig()->vbatfullcellvoltage
) {
688 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, "BATT < FULL");
693 if (enabledWarnings
& OSD_WARNING_VISUAL_BEEPER
&& showVisualBeeper
) {
694 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, " * * * *");
698 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, NULL
);
702 case OSD_AVG_CELL_VOLTAGE
:
704 const int cellV
= osdGetBatteryAverageCellVoltage();
705 buff
[0] = osdGetBatterySymbol(cellV
);
706 tfp_sprintf(buff
+ 1, "%d.%02d%c", cellV
/ 100, cellV
% 100, SYM_VOLT
);
711 tfp_sprintf(buff
, "DBG %5d %5d %5d %5d", debug
[0], debug
[1], debug
[2], debug
[3]);
714 case OSD_PITCH_ANGLE
:
717 const int angle
= (item
== OSD_PITCH_ANGLE
) ? attitude
.values
.pitch
: attitude
.values
.roll
;
718 tfp_sprintf(buff
, "%c%02d.%01d", angle
< 0 ? '-' : ' ', abs(angle
/ 10), abs(angle
% 10));
722 case OSD_MAIN_BATT_USAGE
:
724 // Set length of indicator bar
725 #define MAIN_BATT_USAGE_STEPS 11 // Use an odd number so the bar can be centered.
727 // Calculate constrained value
728 const float value
= constrain(batteryConfig()->batteryCapacity
- getMAhDrawn(), 0, batteryConfig()->batteryCapacity
);
730 // Calculate mAh used progress
731 const uint8_t mAhUsedProgress
= ceilf((value
/ (batteryConfig()->batteryCapacity
/ MAIN_BATT_USAGE_STEPS
)));
733 // Create empty battery indicator bar
734 buff
[0] = SYM_PB_START
;
735 for (int i
= 1; i
<= MAIN_BATT_USAGE_STEPS
; i
++) {
736 buff
[i
] = i
<= mAhUsedProgress
? SYM_PB_FULL
: SYM_PB_EMPTY
;
738 buff
[MAIN_BATT_USAGE_STEPS
+ 1] = SYM_PB_CLOSE
;
739 if (mAhUsedProgress
> 0 && mAhUsedProgress
< MAIN_BATT_USAGE_STEPS
) {
740 buff
[1 + mAhUsedProgress
] = SYM_PB_END
;
742 buff
[MAIN_BATT_USAGE_STEPS
+2] = '\0';
747 if (!ARMING_FLAG(ARMED
)) {
748 tfp_sprintf(buff
, "DISARMED");
750 tfp_sprintf(buff
, " ");
754 case OSD_NUMERICAL_HEADING
:
756 const int heading
= DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
757 tfp_sprintf(buff
, "%c%03d", osdGetDirectionSymbolFromHeading(heading
), heading
);
761 case OSD_NUMERICAL_VARIO
:
763 const int verticalSpeed
= osdGetMetersToSelectedUnit(getEstimatedVario());
764 const char directionSymbol
= verticalSpeed
< 0 ? SYM_ARROW_SOUTH
: SYM_ARROW_NORTH
;
765 tfp_sprintf(buff
, "%c%01d.%01d", directionSymbol
, abs(verticalSpeed
/ 100), abs((verticalSpeed
% 100) / 10));
769 #ifdef USE_ESC_SENSOR
771 tfp_sprintf(buff
, "%3d%c", osdConvertTemperatureToSelectedUnit(escData
->temperature
* 10) / 10, osdGetTemperatureSymbolForSelectedUnit());
775 tfp_sprintf(buff
, "%5d", escData
== NULL
? 0 : escData
->rpm
);
780 case OSD_RTC_DATETIME
:
781 osdFormatRtcDateTime(&buff
[0]);
785 #ifdef USE_OSD_ADJUSTMENTS
786 case OSD_ADJUSTMENT_RANGE
:
787 tfp_sprintf(buff
, "%s: %3d", adjustmentRangeName
, adjustmentRangeValue
);
791 #ifdef USE_ADC_INTERNAL
792 case OSD_CORE_TEMPERATURE
:
793 tfp_sprintf(buff
, "%3d%c", osdConvertTemperatureToSelectedUnit(getCoreTemperatureCelsius() * 10) / 10, osdGetTemperatureSymbolForSelectedUnit());
801 displayWrite(osdDisplayPort
, elemPosX
+ elemOffsetX
, elemPosY
, buff
);
806 static void osdDrawElements(void)
808 displayClearScreen(osdDisplayPort
);
810 // Hide OSD when OSDSW mode is active
811 if (IS_RC_MODE_ACTIVE(BOXOSD
)) {
815 if (sensors(SENSOR_ACC
)) {
816 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON
);
819 osdDrawSingleElement(OSD_MAIN_BATT_VOLTAGE
);
820 osdDrawSingleElement(OSD_RSSI_VALUE
);
821 osdDrawSingleElement(OSD_CROSSHAIRS
);
822 osdDrawSingleElement(OSD_ITEM_TIMER_1
);
823 osdDrawSingleElement(OSD_ITEM_TIMER_2
);
824 osdDrawSingleElement(OSD_REMAINING_TIME_ESTIMATE
);
825 osdDrawSingleElement(OSD_FLYMODE
);
826 osdDrawSingleElement(OSD_THROTTLE_POS
);
827 osdDrawSingleElement(OSD_VTX_CHANNEL
);
828 osdDrawSingleElement(OSD_CURRENT_DRAW
);
829 osdDrawSingleElement(OSD_MAH_DRAWN
);
830 osdDrawSingleElement(OSD_CRAFT_NAME
);
831 osdDrawSingleElement(OSD_ALTITUDE
);
832 osdDrawSingleElement(OSD_ROLL_PIDS
);
833 osdDrawSingleElement(OSD_PITCH_PIDS
);
834 osdDrawSingleElement(OSD_YAW_PIDS
);
835 osdDrawSingleElement(OSD_POWER
);
836 osdDrawSingleElement(OSD_PIDRATE_PROFILE
);
837 osdDrawSingleElement(OSD_WARNINGS
);
838 osdDrawSingleElement(OSD_AVG_CELL_VOLTAGE
);
839 osdDrawSingleElement(OSD_DEBUG
);
840 osdDrawSingleElement(OSD_PITCH_ANGLE
);
841 osdDrawSingleElement(OSD_ROLL_ANGLE
);
842 osdDrawSingleElement(OSD_MAIN_BATT_USAGE
);
843 osdDrawSingleElement(OSD_DISARMED
);
844 osdDrawSingleElement(OSD_NUMERICAL_HEADING
);
845 osdDrawSingleElement(OSD_NUMERICAL_VARIO
);
846 osdDrawSingleElement(OSD_COMPASS_BAR
);
849 if (sensors(SENSOR_GPS
)) {
850 osdDrawSingleElement(OSD_GPS_SATS
);
851 osdDrawSingleElement(OSD_GPS_SPEED
);
852 osdDrawSingleElement(OSD_GPS_LAT
);
853 osdDrawSingleElement(OSD_GPS_LON
);
854 osdDrawSingleElement(OSD_HOME_DIST
);
855 osdDrawSingleElement(OSD_HOME_DIR
);
859 #ifdef USE_ESC_SENSOR
860 if (feature(FEATURE_ESC_SENSOR
)) {
861 osdDrawSingleElement(OSD_ESC_TMP
);
862 osdDrawSingleElement(OSD_ESC_RPM
);
867 osdDrawSingleElement(OSD_RTC_DATETIME
);
870 #ifdef USE_OSD_ADJUSTMENTS
871 osdDrawSingleElement(OSD_ADJUSTMENT_RANGE
);
874 #ifdef USE_ADC_INTERNAL
875 osdDrawSingleElement(OSD_CORE_TEMPERATURE
);
879 void pgResetFn_osdConfig(osdConfig_t
*osdConfig
)
881 // Position elements near centre of screen and disabled by default
882 for (int i
= 0; i
< OSD_ITEM_COUNT
; i
++) {
883 osdConfig
->item_pos
[i
] = OSD_POS(10, 7);
886 // Always enable warnings elements by default
887 osdConfig
->item_pos
[OSD_WARNINGS
] = OSD_POS(9, 10) | VISIBLE_FLAG
;
889 osdConfig
->enabled_stats
[OSD_STAT_MAX_SPEED
] = true;
890 osdConfig
->enabled_stats
[OSD_STAT_MIN_BATTERY
] = true;
891 osdConfig
->enabled_stats
[OSD_STAT_MIN_RSSI
] = true;
892 osdConfig
->enabled_stats
[OSD_STAT_MAX_CURRENT
] = true;
893 osdConfig
->enabled_stats
[OSD_STAT_USED_MAH
] = true;
894 osdConfig
->enabled_stats
[OSD_STAT_MAX_ALTITUDE
] = false;
895 osdConfig
->enabled_stats
[OSD_STAT_BLACKBOX
] = true;
896 osdConfig
->enabled_stats
[OSD_STAT_END_BATTERY
] = false;
897 osdConfig
->enabled_stats
[OSD_STAT_MAX_DISTANCE
] = false;
898 osdConfig
->enabled_stats
[OSD_STAT_BLACKBOX_NUMBER
] = true;
899 osdConfig
->enabled_stats
[OSD_STAT_TIMER_1
] = false;
900 osdConfig
->enabled_stats
[OSD_STAT_TIMER_2
] = true;
901 osdConfig
->enabled_stats
[OSD_STAT_RTC_DATE_TIME
] = false;
903 osdConfig
->units
= OSD_UNIT_METRIC
;
905 // Enable all warnings by default
906 osdConfig
->enabledWarnings
= UINT16_MAX
;
908 osdConfig
->timers
[OSD_TIMER_1
] = OSD_TIMER(OSD_TIMER_SRC_ON
, OSD_TIMER_PREC_SECOND
, 10);
909 osdConfig
->timers
[OSD_TIMER_2
] = OSD_TIMER(OSD_TIMER_SRC_TOTAL_ARMED
, OSD_TIMER_PREC_SECOND
, 10);
911 osdConfig
->rssi_alarm
= 20;
912 osdConfig
->cap_alarm
= 2200;
913 osdConfig
->alt_alarm
= 100; // meters or feet depend on configuration
915 osdConfig
->ahMaxPitch
= 20; // 20 degrees
916 osdConfig
->ahMaxRoll
= 40; // 40 degrees
919 static void osdDrawLogo(int x
, int y
)
921 // display logo and help
922 int fontOffset
= 160;
923 for (int row
= 0; row
< 4; row
++) {
924 for (int column
= 0; column
< 24; column
++) {
925 if (fontOffset
<= SYM_END_OF_FONT
)
926 displayWriteChar(osdDisplayPort
, x
+ column
, y
+ row
, fontOffset
++);
931 void osdInit(displayPort_t
*osdDisplayPortToUse
)
933 if (!osdDisplayPortToUse
) {
937 BUILD_BUG_ON(OSD_POS_MAX
!= OSD_POS(31,31));
939 osdDisplayPort
= osdDisplayPortToUse
;
941 cmsDisplayPortRegister(osdDisplayPort
);
944 armState
= ARMING_FLAG(ARMED
);
946 memset(blinkBits
, 0, sizeof(blinkBits
));
948 displayClearScreen(osdDisplayPort
);
952 char string_buffer
[30];
953 tfp_sprintf(string_buffer
, "V%s", FC_VERSION_STRING
);
954 displayWrite(osdDisplayPort
, 20, 6, string_buffer
);
956 displayWrite(osdDisplayPort
, 7, 8, CMS_STARTUP_HELP_TEXT1
);
957 displayWrite(osdDisplayPort
, 11, 9, CMS_STARTUP_HELP_TEXT2
);
958 displayWrite(osdDisplayPort
, 11, 10, CMS_STARTUP_HELP_TEXT3
);
962 char dateTimeBuffer
[FORMATTED_DATE_TIME_BUFSIZE
];
963 if (osdFormatRtcDateTime(&dateTimeBuffer
[0])) {
964 displayWrite(osdDisplayPort
, 5, 12, dateTimeBuffer
);
968 displayResync(osdDisplayPort
);
970 resumeRefreshAt
= micros() + (4 * REFRESH_1S
);
973 void osdUpdateAlarms(void)
977 int32_t alt
= osdGetMetersToSelectedUnit(getEstimatedAltitude()) / 100;
979 if (statRssi
< osdConfig()->rssi_alarm
) {
980 SET_BLINK(OSD_RSSI_VALUE
);
982 CLR_BLINK(OSD_RSSI_VALUE
);
985 if (getBatteryState() == BATTERY_OK
) {
986 CLR_BLINK(OSD_WARNINGS
);
987 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE
);
988 CLR_BLINK(OSD_AVG_CELL_VOLTAGE
);
990 SET_BLINK(OSD_WARNINGS
);
991 SET_BLINK(OSD_MAIN_BATT_VOLTAGE
);
992 SET_BLINK(OSD_AVG_CELL_VOLTAGE
);
995 if (STATE(GPS_FIX
) == 0) {
996 SET_BLINK(OSD_GPS_SATS
);
998 CLR_BLINK(OSD_GPS_SATS
);
1001 for (int i
= 0; i
< OSD_TIMER_COUNT
; i
++) {
1002 const uint16_t timer
= osdConfig()->timers
[i
];
1003 const timeUs_t time
= osdGetTimerValue(OSD_TIMER_SRC(timer
));
1004 const timeUs_t alarmTime
= OSD_TIMER_ALARM(timer
) * 60000000; // convert from minutes to us
1005 if (alarmTime
!= 0 && time
>= alarmTime
) {
1006 SET_BLINK(OSD_ITEM_TIMER_1
+ i
);
1008 CLR_BLINK(OSD_ITEM_TIMER_1
+ i
);
1012 if (getMAhDrawn() >= osdConfig()->cap_alarm
) {
1013 SET_BLINK(OSD_MAH_DRAWN
);
1014 SET_BLINK(OSD_MAIN_BATT_USAGE
);
1015 SET_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1017 CLR_BLINK(OSD_MAH_DRAWN
);
1018 CLR_BLINK(OSD_MAIN_BATT_USAGE
);
1019 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1022 if (alt
>= osdConfig()->alt_alarm
) {
1023 SET_BLINK(OSD_ALTITUDE
);
1025 CLR_BLINK(OSD_ALTITUDE
);
1029 void osdResetAlarms(void)
1031 CLR_BLINK(OSD_RSSI_VALUE
);
1032 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1033 CLR_BLINK(OSD_WARNINGS
);
1034 CLR_BLINK(OSD_GPS_SATS
);
1035 CLR_BLINK(OSD_MAH_DRAWN
);
1036 CLR_BLINK(OSD_ALTITUDE
);
1037 CLR_BLINK(OSD_AVG_CELL_VOLTAGE
);
1038 CLR_BLINK(OSD_MAIN_BATT_USAGE
);
1039 CLR_BLINK(OSD_ITEM_TIMER_1
);
1040 CLR_BLINK(OSD_ITEM_TIMER_2
);
1041 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1044 static void osdResetStats(void)
1046 stats
.max_current
= 0;
1047 stats
.max_speed
= 0;
1048 stats
.min_voltage
= 500;
1049 stats
.max_current
= 0;
1050 stats
.min_rssi
= 99;
1051 stats
.max_altitude
= 0;
1052 stats
.max_distance
= 0;
1053 stats
.armed_time
= 0;
1056 static void osdUpdateStats(void)
1061 value
= CM_S_TO_KM_H(gpsSol
.groundSpeed
);
1063 if (stats
.max_speed
< value
) {
1064 stats
.max_speed
= value
;
1067 if (stats
.min_voltage
> getBatteryVoltage()) {
1068 stats
.min_voltage
= getBatteryVoltage();
1071 value
= getAmperage() / 100;
1072 if (stats
.max_current
< value
) {
1073 stats
.max_current
= value
;
1076 statRssi
= scaleRange(getRssi(), 0, 1024, 0, 100);
1077 if (stats
.min_rssi
> statRssi
) {
1078 stats
.min_rssi
= statRssi
;
1081 if (stats
.max_altitude
< getEstimatedAltitude()) {
1082 stats
.max_altitude
= getEstimatedAltitude();
1086 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
) && (stats
.max_distance
< GPS_distanceToHome
)) {
1087 stats
.max_distance
= GPS_distanceToHome
;
1093 static void osdGetBlackboxStatusString(char * buff
)
1095 bool storageDeviceIsWorking
= false;
1096 uint32_t storageUsed
= 0;
1097 uint32_t storageTotal
= 0;
1099 switch (blackboxConfig()->device
) {
1101 case BLACKBOX_DEVICE_SDCARD
:
1102 storageDeviceIsWorking
= sdcard_isInserted() && sdcard_isFunctional() && (afatfs_getFilesystemState() == AFATFS_FILESYSTEM_STATE_READY
);
1103 if (storageDeviceIsWorking
) {
1104 storageTotal
= sdcard_getMetadata()->numBlocks
/ 2000;
1105 storageUsed
= storageTotal
- (afatfs_getContiguousFreeSpace() / 1024000);
1111 case BLACKBOX_DEVICE_FLASH
:
1112 storageDeviceIsWorking
= flashfsIsReady();
1113 if (storageDeviceIsWorking
) {
1114 const flashGeometry_t
*geometry
= flashfsGetGeometry();
1115 storageTotal
= geometry
->totalSize
/ 1024;
1116 storageUsed
= flashfsGetOffset() / 1024;
1125 if (storageDeviceIsWorking
) {
1126 const uint16_t storageUsedPercent
= (storageUsed
* 100) / storageTotal
;
1127 tfp_sprintf(buff
, "%d%%", storageUsedPercent
);
1129 tfp_sprintf(buff
, "FAULT");
1134 static void osdDisplayStatisticLabel(uint8_t y
, const char * text
, const char * value
)
1136 displayWrite(osdDisplayPort
, 2, y
, text
);
1137 displayWrite(osdDisplayPort
, 20, y
, ":");
1138 displayWrite(osdDisplayPort
, 22, y
, value
);
1142 * Test if there's some stat enabled
1144 static bool isSomeStatEnabled(void)
1146 for (int i
= 0; i
< OSD_STAT_COUNT
; i
++) {
1147 if (osdConfig()->enabled_stats
[i
]) {
1154 static void osdShowStats(void)
1157 char buff
[OSD_ELEMENT_BUFFER_LENGTH
];
1159 displayClearScreen(osdDisplayPort
);
1160 displayWrite(osdDisplayPort
, 2, top
++, " --- STATS ---");
1162 if (osdConfig()->enabled_stats
[OSD_STAT_RTC_DATE_TIME
]) {
1163 bool success
= false;
1165 success
= osdFormatRtcDateTime(&buff
[0]);
1168 tfp_sprintf(buff
, "NO RTC");
1171 displayWrite(osdDisplayPort
, 2, top
++, buff
);
1174 if (osdConfig()->enabled_stats
[OSD_STAT_TIMER_1
]) {
1175 osdFormatTimer(buff
, false, OSD_TIMER_1
);
1176 osdDisplayStatisticLabel(top
++, osdTimerSourceNames
[OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_1
])], buff
);
1179 if (osdConfig()->enabled_stats
[OSD_STAT_TIMER_2
]) {
1180 osdFormatTimer(buff
, false, OSD_TIMER_2
);
1181 osdDisplayStatisticLabel(top
++, osdTimerSourceNames
[OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_2
])], buff
);
1184 if (osdConfig()->enabled_stats
[OSD_STAT_MAX_SPEED
] && STATE(GPS_FIX
)) {
1185 itoa(stats
.max_speed
, buff
, 10);
1186 osdDisplayStatisticLabel(top
++, "MAX SPEED", buff
);
1189 if (osdConfig()->enabled_stats
[OSD_STAT_MAX_DISTANCE
]) {
1190 tfp_sprintf(buff
, "%d%c", osdGetMetersToSelectedUnit(stats
.max_distance
), osdGetMetersToSelectedUnitSymbol());
1191 osdDisplayStatisticLabel(top
++, "MAX DISTANCE", buff
);
1194 if (osdConfig()->enabled_stats
[OSD_STAT_MIN_BATTERY
]) {
1195 tfp_sprintf(buff
, "%d.%1d%c", stats
.min_voltage
/ 10, stats
.min_voltage
% 10, SYM_VOLT
);
1196 osdDisplayStatisticLabel(top
++, "MIN BATTERY", buff
);
1199 if (osdConfig()->enabled_stats
[OSD_STAT_END_BATTERY
]) {
1200 tfp_sprintf(buff
, "%d.%1d%c", getBatteryVoltage() / 10, getBatteryVoltage() % 10, SYM_VOLT
);
1201 osdDisplayStatisticLabel(top
++, "END BATTERY", buff
);
1204 if (osdConfig()->enabled_stats
[OSD_STAT_MIN_RSSI
]) {
1205 itoa(stats
.min_rssi
, buff
, 10);
1207 osdDisplayStatisticLabel(top
++, "MIN RSSI", buff
);
1210 if (batteryConfig()->currentMeterSource
!= CURRENT_METER_NONE
) {
1211 if (osdConfig()->enabled_stats
[OSD_STAT_MAX_CURRENT
]) {
1212 itoa(stats
.max_current
, buff
, 10);
1214 osdDisplayStatisticLabel(top
++, "MAX CURRENT", buff
);
1217 if (osdConfig()->enabled_stats
[OSD_STAT_USED_MAH
]) {
1218 tfp_sprintf(buff
, "%d%c", getMAhDrawn(), SYM_MAH
);
1219 osdDisplayStatisticLabel(top
++, "USED MAH", buff
);
1223 if (osdConfig()->enabled_stats
[OSD_STAT_MAX_ALTITUDE
]) {
1224 osdFormatAltitudeString(buff
, stats
.max_altitude
, false);
1225 osdDisplayStatisticLabel(top
++, "MAX ALTITUDE", buff
);
1229 if (osdConfig()->enabled_stats
[OSD_STAT_BLACKBOX
] && blackboxConfig()->device
&& blackboxConfig()->device
!= BLACKBOX_DEVICE_SERIAL
) {
1230 osdGetBlackboxStatusString(buff
);
1231 osdDisplayStatisticLabel(top
++, "BLACKBOX", buff
);
1234 if (osdConfig()->enabled_stats
[OSD_STAT_BLACKBOX_NUMBER
] && blackboxConfig()->device
&& blackboxConfig()->device
!= BLACKBOX_DEVICE_SERIAL
) {
1235 itoa(blackboxGetLogNumber(), buff
, 10);
1236 osdDisplayStatisticLabel(top
++, "BB LOG NUM", buff
);
1240 // Reset time since last armed here to ensure this timer is at zero when back at "main" OSD screen
1241 stats
.armed_time
= 0;
1244 static void osdShowArmed(void)
1246 displayClearScreen(osdDisplayPort
);
1247 displayWrite(osdDisplayPort
, 12, 7, "ARMED");
1250 STATIC_UNIT_TESTED
void osdRefresh(timeUs_t currentTimeUs
)
1252 static timeUs_t lastTimeUs
= 0;
1254 // detect arm/disarm
1255 if (armState
!= ARMING_FLAG(ARMED
)) {
1256 if (ARMING_FLAG(ARMED
)) {
1259 resumeRefreshAt
= currentTimeUs
+ (REFRESH_1S
/ 2);
1260 } else if (isSomeStatEnabled()) {
1262 resumeRefreshAt
= currentTimeUs
+ (60 * REFRESH_1S
);
1265 armState
= ARMING_FLAG(ARMED
);
1270 if (ARMING_FLAG(ARMED
)) {
1271 timeUs_t deltaT
= currentTimeUs
- lastTimeUs
;
1273 stats
.armed_time
+= deltaT
;
1275 lastTimeUs
= currentTimeUs
;
1277 if (resumeRefreshAt
) {
1278 if (cmp32(currentTimeUs
, resumeRefreshAt
) < 0) {
1279 // in timeout period, check sticks for activity to resume display.
1280 if (IS_HI(THROTTLE
) || IS_HI(PITCH
)) {
1281 resumeRefreshAt
= 0;
1284 displayHeartbeat(osdDisplayPort
);
1287 displayClearScreen(osdDisplayPort
);
1288 resumeRefreshAt
= 0;
1292 blinkState
= (currentTimeUs
/ 200000) % 2;
1294 #ifdef USE_ESC_SENSOR
1295 if (feature(FEATURE_ESC_SENSOR
)) {
1296 escData
= getEscSensorData(ESC_SENSOR_COMBINED
);
1301 if (!displayIsGrabbed(osdDisplayPort
)) {
1304 displayHeartbeat(osdDisplayPort
);
1305 #ifdef OSD_CALLS_CMS
1307 cmsUpdate(currentTimeUs
);
1314 * Called periodically by the scheduler
1316 void osdUpdate(timeUs_t currentTimeUs
)
1318 static uint32_t counter
= 0;
1321 showVisualBeeper
= true;
1324 #ifdef MAX7456_DMA_CHANNEL_TX
1325 // don't touch buffers if DMA transaction is in progress
1326 if (displayIsTransferInProgress(osdDisplayPort
)) {
1329 #endif // MAX7456_DMA_CHANNEL_TX
1331 #ifdef USE_SLOW_MSP_DISPLAYPORT_RATE_WHEN_UNARMED
1332 static uint32_t idlecounter
= 0;
1333 if (!ARMING_FLAG(ARMED
)) {
1334 if (idlecounter
++ % 4 != 0) {
1340 // redraw values in buffer
1342 #define DRAW_FREQ_DENOM 5
1344 #define DRAW_FREQ_DENOM 10 // MWOSD @ 115200 baud (
1346 #define STATS_FREQ_DENOM 50
1348 if (counter
% DRAW_FREQ_DENOM
== 0) {
1349 osdRefresh(currentTimeUs
);
1350 showVisualBeeper
= false;
1352 // rest of time redraw screen 10 chars per idle so it doesn't lock the main idle
1353 displayDrawScreen(osdDisplayPort
);
1358 // do not allow ARM if we are in menu
1359 if (displayIsGrabbed(osdDisplayPort
)) {
1360 setArmingDisabled(ARMING_DISABLED_OSD_MENU
);
1362 unsetArmingDisabled(ARMING_DISABLED_OSD_MENU
);