2 * This file is part of Cleanflight and Betaflight.
4 * Cleanflight and Betaflight are free software. You can redistribute
5 * this software and/or modify this software under the terms of the
6 * GNU General Public License as published by the Free Software
7 * Foundation, either version 3 of the License, or (at your option)
10 * Cleanflight and Betaflight are distributed in the hope that they
11 * will be useful, but WITHOUT ANY WARRANTY; without even the implied
12 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 * See the GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this software.
18 * If not, see <http://www.gnu.org/licenses/>.
22 Created by Marcin Baliniak
23 some functions based on MinimOSD
25 OSD-CMS separation by jflyper
39 #include "blackbox/blackbox.h"
40 #include "blackbox/blackbox_io.h"
42 #include "build/build_config.h"
43 #include "build/debug.h"
44 #include "build/version.h"
47 #include "cms/cms_types.h"
49 #include "common/axis.h"
50 #include "common/maths.h"
51 #include "common/printf.h"
52 #include "common/typeconversion.h"
53 #include "common/utils.h"
55 #include "config/feature.h"
57 #include "drivers/display.h"
58 #include "drivers/flash.h"
59 #include "drivers/max7456_symbols.h"
60 #include "drivers/sdcard.h"
61 #include "drivers/time.h"
63 #include "fc/config.h"
64 #include "fc/fc_core.h"
65 #include "fc/rc_adjustments.h"
66 #include "fc/rc_controls.h"
68 #include "fc/runtime_config.h"
70 #include "flight/position.h"
71 #include "flight/imu.h"
73 #include "flight/mixer.h"
75 #include "flight/pid.h"
77 #include "io/asyncfatfs/asyncfatfs.h"
78 #include "io/beeper.h"
79 #include "io/flashfs.h"
82 #include "io/vtx_string.h"
86 #include "pg/pg_ids.h"
91 #include "sensors/acceleration.h"
92 #include "sensors/adcinternal.h"
93 #include "sensors/barometer.h"
94 #include "sensors/battery.h"
95 #include "sensors/esc_sensor.h"
96 #include "sensors/sensors.h"
98 #ifdef USE_HARDWARE_REVISION_DETECTION
99 #include "hardware_revision.h"
102 #define VIDEO_BUFFER_CHARS_PAL 480
103 #define FULL_CIRCLE 360
105 const char * const osdTimerSourceNames
[] = {
113 static bool blinkState
= true;
114 static bool showVisualBeeper
= false;
116 static uint32_t blinkBits
[(OSD_ITEM_COUNT
+ 31)/32];
117 #define SET_BLINK(item) (blinkBits[(item) / 32] |= (1 << ((item) % 32)))
118 #define CLR_BLINK(item) (blinkBits[(item) / 32] &= ~(1 << ((item) % 32)))
119 #define IS_BLINK(item) (blinkBits[(item) / 32] & (1 << ((item) % 32)))
120 #define BLINK(item) (IS_BLINK(item) && blinkState)
122 // Things in both OSD and CMS
124 #define IS_HI(X) (rcData[X] > 1750)
125 #define IS_LO(X) (rcData[X] < 1250)
126 #define IS_MID(X) (rcData[X] > 1250 && rcData[X] < 1750)
128 static timeUs_t flyTime
= 0;
130 typedef struct statistic_s
{
133 int16_t min_voltage
; // /10
134 int16_t max_current
; // /10
136 int32_t max_altitude
;
137 int16_t max_distance
;
140 static statistic_t stats
;
142 timeUs_t resumeRefreshAt
= 0;
143 #define REFRESH_1S 1000 * 1000
145 static uint8_t armState
;
146 static bool lastArmState
;
148 static displayPort_t
*osdDisplayPort
;
150 #ifdef USE_ESC_SENSOR
151 static escSensorData_t
*escDataCombined
;
154 #define AH_SYMBOL_COUNT 9
155 #define AH_SIDEBAR_WIDTH_POS 7
156 #define AH_SIDEBAR_HEIGHT_POS 3
158 static const char compassBar
[] = {
160 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
162 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
164 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
166 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
168 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
,
170 SYM_HEADING_LINE
, SYM_HEADING_DIVIDED_LINE
, SYM_HEADING_LINE
173 static const uint8_t osdElementDisplayOrder
[] = {
174 OSD_MAIN_BATT_VOLTAGE
,
177 OSD_HORIZON_SIDEBARS
,
180 OSD_REMAINING_TIME_ESTIMATE
,
194 OSD_AVG_CELL_VOLTAGE
,
200 OSD_NUMERICAL_HEADING
,
206 PG_REGISTER_WITH_RESET_FN(osdConfig_t
, osdConfig
, PG_OSD_CONFIG
, 3);
209 * Gets the correct altitude symbol for the current unit system
211 static char osdGetMetersToSelectedUnitSymbol(void)
213 switch (osdConfig()->units
) {
214 case OSD_UNIT_IMPERIAL
:
222 * Gets average battery cell voltage in 0.01V units.
224 static int osdGetBatteryAverageCellVoltage(void)
226 return (getBatteryVoltage() * 10) / getBatteryCellCount();
229 static char osdGetBatterySymbol(int cellVoltage
)
231 if (getBatteryState() == BATTERY_CRITICAL
) {
232 return SYM_MAIN_BATT
; // FIXME: currently the BAT- symbol, ideally replace with a battery with exclamation mark
234 // Calculate a symbol offset using cell voltage over full cell voltage range
235 const int symOffset
= scaleRange(cellVoltage
, batteryConfig()->vbatmincellvoltage
* 10, batteryConfig()->vbatmaxcellvoltage
* 10, 0, 7);
236 return SYM_BATT_EMPTY
- constrain(symOffset
, 0, 6);
241 * Converts altitude based on the current unit system.
242 * @param meters Value in meters to convert
244 static int32_t osdGetMetersToSelectedUnit(int32_t meters
)
246 switch (osdConfig()->units
) {
247 case OSD_UNIT_IMPERIAL
:
248 return (meters
* 328) / 100; // Convert to feet / 100
250 return meters
; // Already in metre / 100
254 #if defined(USE_ADC_INTERNAL) || defined(USE_ESC_SENSOR)
255 STATIC_UNIT_TESTED
int osdConvertTemperatureToSelectedUnit(int tempInDegreesCelcius
)
257 switch (osdConfig()->units
) {
258 case OSD_UNIT_IMPERIAL
:
259 return lrintf(((tempInDegreesCelcius
* 9.0f
) / 5) + 32);
261 return tempInDegreesCelcius
;
265 static char osdGetTemperatureSymbolForSelectedUnit(void)
267 switch (osdConfig()->units
) {
268 case OSD_UNIT_IMPERIAL
:
276 static void osdFormatAltitudeString(char * buff
, int altitude
)
278 const int alt
= osdGetMetersToSelectedUnit(altitude
) / 10;
280 tfp_sprintf(buff
, "%5d %c", alt
, osdGetMetersToSelectedUnitSymbol());
285 static void osdFormatPID(char * buff
, const char * label
, const pidf_t
* pid
)
287 tfp_sprintf(buff
, "%s %3d %3d %3d", label
, pid
->P
, pid
->I
, pid
->D
);
290 static uint8_t osdGetHeadingIntoDiscreteDirections(int heading
, unsigned directions
)
292 heading
+= FULL_CIRCLE
; // Ensure positive value
294 // Split input heading 0..359 into sectors 0..(directions-1), but offset
295 // by half a sector so that sector 0 gets centered around heading 0.
296 // We multiply heading by directions to not loose precision in divisions
297 // In this way each segment will be a FULL_CIRCLE length
298 int direction
= (heading
* directions
+ FULL_CIRCLE
/ 2) / FULL_CIRCLE
; // scale with rounding
299 direction
%= directions
; // normalize
301 return direction
; // return segment number
304 static uint8_t osdGetDirectionSymbolFromHeading(int heading
)
306 heading
= osdGetHeadingIntoDiscreteDirections(heading
, 16);
308 // Now heading has a heading with Up=0, Right=4, Down=8 and Left=12
309 // Our symbols are Down=0, Right=4, Up=8 and Left=12
310 // There're 16 arrow symbols. Transform it.
311 heading
= 16 - heading
;
312 heading
= (heading
+ 8) % 16;
314 return SYM_ARROW_SOUTH
+ heading
;
317 static char osdGetTimerSymbol(osd_timer_source_e src
)
320 case OSD_TIMER_SRC_ON
:
322 case OSD_TIMER_SRC_TOTAL_ARMED
:
323 case OSD_TIMER_SRC_LAST_ARMED
:
330 static timeUs_t
osdGetTimerValue(osd_timer_source_e src
)
333 case OSD_TIMER_SRC_ON
:
335 case OSD_TIMER_SRC_TOTAL_ARMED
:
337 case OSD_TIMER_SRC_LAST_ARMED
:
338 return stats
.armed_time
;
344 STATIC_UNIT_TESTED
void osdFormatTime(char * buff
, osd_timer_precision_e precision
, timeUs_t time
)
346 int seconds
= time
/ 1000000;
347 const int minutes
= seconds
/ 60;
348 seconds
= seconds
% 60;
351 case OSD_TIMER_PREC_SECOND
:
353 tfp_sprintf(buff
, "%02d:%02d", minutes
, seconds
);
355 case OSD_TIMER_PREC_HUNDREDTHS
:
357 const int hundredths
= (time
/ 10000) % 100;
358 tfp_sprintf(buff
, "%02d:%02d.%02d", minutes
, seconds
, hundredths
);
364 STATIC_UNIT_TESTED
void osdFormatTimer(char *buff
, bool showSymbol
, bool usePrecision
, int timerIndex
)
366 const uint16_t timer
= osdConfig()->timers
[timerIndex
];
367 const uint8_t src
= OSD_TIMER_SRC(timer
);
370 *(buff
++) = osdGetTimerSymbol(src
);
373 osdFormatTime(buff
, (usePrecision
? OSD_TIMER_PRECISION(timer
) : OSD_TIMER_PREC_SECOND
), osdGetTimerValue(src
));
377 static void osdFormatCoordinate(char *buff
, char sym
, int32_t val
)
379 // latitude maximum integer width is 3 (-90).
380 // longitude maximum integer width is 4 (-180).
381 // We show 7 decimals, so we need to use 12 characters:
382 // eg: s-180.1234567z s=symbol, z=zero terminator, decimal separator between 0 and 1
384 static const int coordinateMaxLength
= 13;//12 for the number (4 + dot + 7) + 1 for the symbol
387 const int32_t integerPart
= val
/ GPS_DEGREES_DIVIDER
;
388 const int32_t decimalPart
= labs(val
% GPS_DEGREES_DIVIDER
);
389 const int written
= tfp_sprintf(buff
+ 1, "%d.%07d", integerPart
, decimalPart
);
390 // pad with blanks to coordinateMaxLength
391 for (int pos
= 1 + written
; pos
< coordinateMaxLength
; ++pos
) {
392 buff
[pos
] = SYM_BLANK
;
394 buff
[coordinateMaxLength
] = '\0';
398 static void osdFormatMessage(char *buff
, size_t size
, const char *message
)
400 memset(buff
, SYM_BLANK
, size
);
402 memcpy(buff
, message
, strlen(message
));
404 // Ensure buff is zero terminated
405 buff
[size
- 1] = '\0';
409 static bool osdFormatRtcDateTime(char *buffer
)
412 if (!rtcGetDateTime(&dateTime
)) {
418 dateTimeFormatLocalShort(buffer
, &dateTime
);
424 void osdStatSetState(uint8_t statIndex
, bool enabled
)
427 osdConfigMutable()->enabled_stats
|= (1 << statIndex
);
429 osdConfigMutable()->enabled_stats
&= ~(1 << statIndex
);
433 bool osdStatGetState(uint8_t statIndex
)
435 return osdConfig()->enabled_stats
& (1 << statIndex
);
438 void osdWarnSetState(uint8_t warningIndex
, bool enabled
)
441 osdConfigMutable()->enabledWarnings
|= (1 << warningIndex
);
443 osdConfigMutable()->enabledWarnings
&= ~(1 << warningIndex
);
447 bool osdWarnGetState(uint8_t warningIndex
)
449 return osdConfig()->enabledWarnings
& (1 << warningIndex
);
452 static bool osdDrawSingleElement(uint8_t item
)
454 if (!VISIBLE(osdConfig()->item_pos
[item
]) || BLINK(item
)) {
458 uint8_t elemPosX
= OSD_X(osdConfig()->item_pos
[item
]);
459 uint8_t elemPosY
= OSD_Y(osdConfig()->item_pos
[item
]);
460 char buff
[OSD_ELEMENT_BUFFER_LENGTH
] = "";
465 uint16_t osdRssi
= getRssi() * 100 / 1024; // change range
469 tfp_sprintf(buff
, "%c%2d", SYM_RSSI
, osdRssi
);
473 case OSD_MAIN_BATT_VOLTAGE
:
474 buff
[0] = osdGetBatterySymbol(osdGetBatteryAverageCellVoltage());
475 tfp_sprintf(buff
+ 1, "%2d.%1d%c", getBatteryVoltage() / 10, getBatteryVoltage() % 10, SYM_VOLT
);
478 case OSD_CURRENT_DRAW
:
480 const int32_t amperage
= getAmperage();
481 tfp_sprintf(buff
, "%3d.%02d%c", abs(amperage
) / 100, abs(amperage
) % 100, SYM_AMP
);
486 tfp_sprintf(buff
, "%4d%c", getMAhDrawn(), SYM_MAH
);
491 tfp_sprintf(buff
, "%c%c%2d", SYM_SAT_L
, SYM_SAT_R
, gpsSol
.numSat
);
495 // 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)
496 switch (osdConfig()->units
) {
497 case OSD_UNIT_IMPERIAL
:
498 tfp_sprintf(buff
, "%3dM", CM_S_TO_MPH(gpsSol
.groundSpeed
));
501 tfp_sprintf(buff
, "%3dK", CM_S_TO_KM_H(gpsSol
.groundSpeed
));
507 // The SYM_LAT symbol in the actual font contains only blank, so we use the SYM_ARROW_NORTH
508 osdFormatCoordinate(buff
, SYM_ARROW_NORTH
, gpsSol
.llh
.lat
);
512 // The SYM_LON symbol in the actual font contains only blank, so we use the SYM_ARROW_EAST
513 osdFormatCoordinate(buff
, SYM_ARROW_EAST
, gpsSol
.llh
.lon
);
517 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
518 if (GPS_distanceToHome
> 0) {
519 const int h
= GPS_directionToHome
- DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
520 buff
[0] = osdGetDirectionSymbolFromHeading(h
);
522 // We don't have a HOME symbol in the font, by now we use this
527 // We use this symbol when we don't have a FIX
536 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
537 const int32_t distance
= osdGetMetersToSelectedUnit(GPS_distanceToHome
);
538 tfp_sprintf(buff
, "%d%c", distance
, osdGetMetersToSelectedUnitSymbol());
540 // We use this symbol when we don't have a FIX
542 // overwrite any previous distance with blanks
543 memset(buff
+ 1, SYM_BLANK
, 6);
550 case OSD_COMPASS_BAR
:
551 memcpy(buff
, compassBar
+ osdGetHeadingIntoDiscreteDirections(DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
), 16), 9);
557 bool haveBaro
= false;
558 bool haveGps
= false;
560 haveBaro
= sensors(SENSOR_BARO
);
563 haveGps
= sensors(SENSOR_GPS
) && STATE(GPS_FIX
);
565 if (haveBaro
|| haveGps
) {
566 osdFormatAltitudeString(buff
, getEstimatedAltitude());
568 // We use this symbol when we don't have a valid measure
570 // overwrite any previous altitude with blanks
571 memset(buff
+ 1, SYM_BLANK
, 6);
578 case OSD_ITEM_TIMER_1
:
579 case OSD_ITEM_TIMER_2
:
580 osdFormatTimer(buff
, true, true, item
- OSD_ITEM_TIMER_1
);
583 case OSD_REMAINING_TIME_ESTIMATE
:
585 const int mAhDrawn
= getMAhDrawn();
587 if (mAhDrawn
<= 0.1 * osdConfig()->cap_alarm
) { // also handles the mAhDrawn == 0 condition
588 tfp_sprintf(buff
, "--:--");
589 } else if (mAhDrawn
> osdConfig()->cap_alarm
) {
590 tfp_sprintf(buff
, "00:00");
592 const int remaining_time
= (int)((osdConfig()->cap_alarm
- mAhDrawn
) * ((float)flyTime
) / mAhDrawn
);
593 osdFormatTime(buff
, OSD_TIMER_PREC_SECOND
, remaining_time
);
600 // Note that flight mode display has precedence in what to display.
603 // 3. ANGLE, HORIZON, ACRO TRAINER
607 if (FLIGHT_MODE(FAILSAFE_MODE
)) {
608 strcpy(buff
, "!FS!");
609 } else if (FLIGHT_MODE(GPS_RESCUE_MODE
)) {
610 strcpy(buff
, "RESC");
611 } else if (FLIGHT_MODE(HEADFREE_MODE
)) {
612 strcpy(buff
, "HEAD");
613 } else if (FLIGHT_MODE(ANGLE_MODE
)) {
614 strcpy(buff
, "STAB");
615 } else if (FLIGHT_MODE(HORIZON_MODE
)) {
616 strcpy(buff
, "HOR ");
617 } else if (IS_RC_MODE_ACTIVE(BOXACROTRAINER
)) {
618 strcpy(buff
, "ATRN");
619 } else if (isAirmodeActive()) {
620 strcpy(buff
, "AIR ");
622 strcpy(buff
, "ACRO");
628 case OSD_ANTI_GRAVITY
:
630 if (pidOsdAntiGravityActive()) {
638 // 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.
639 //TODO: When iterative updating is implemented, change this so the craft name is only printed once whenever the OSD 'flight' screen is entered.
641 if (strlen(pilotConfig()->name
) == 0) {
642 strcpy(buff
, "CRAFT_NAME");
645 for (i
= 0; i
< MAX_NAME_LENGTH
; i
++) {
646 if (pilotConfig()->name
[i
]) {
647 buff
[i
] = toupper((unsigned char)pilotConfig()->name
[i
]);
657 case OSD_THROTTLE_POS
:
660 tfp_sprintf(buff
+ 2, "%3d", (constrain(rcData
[THROTTLE
], PWM_RANGE_MIN
, PWM_RANGE_MAX
) - PWM_RANGE_MIN
) * 100 / (PWM_RANGE_MAX
- PWM_RANGE_MIN
));
663 #if defined(USE_VTX_COMMON)
664 case OSD_VTX_CHANNEL
:
666 const char vtxBandLetter
= vtx58BandLetter
[vtxSettingsConfig()->band
];
667 const char *vtxChannelName
= vtx58ChannelNames
[vtxSettingsConfig()->channel
];
668 uint8_t vtxPower
= vtxSettingsConfig()->power
;
669 const vtxDevice_t
*vtxDevice
= vtxCommonDevice();
670 if (vtxDevice
&& vtxSettingsConfig()->lowPowerDisarm
) {
671 vtxCommonGetPowerIndex(vtxDevice
, &vtxPower
);
673 tfp_sprintf(buff
, "%c:%s:%1d", vtxBandLetter
, vtxChannelName
, vtxPower
);
679 buff
[0] = SYM_AH_CENTER_LINE
;
680 buff
[1] = SYM_AH_CENTER
;
681 buff
[2] = SYM_AH_CENTER_LINE_RIGHT
;
685 case OSD_ARTIFICIAL_HORIZON
:
687 // Get pitch and roll limits in tenths of degrees
688 const int maxPitch
= osdConfig()->ahMaxPitch
* 10;
689 const int maxRoll
= osdConfig()->ahMaxRoll
* 10;
690 const int rollAngle
= constrain(attitude
.values
.roll
, -maxRoll
, maxRoll
);
691 int pitchAngle
= constrain(attitude
.values
.pitch
, -maxPitch
, maxPitch
);
692 // Convert pitchAngle to y compensation value
693 // (maxPitch / 25) divisor matches previous settings of fixed divisor of 8 and fixed max AHI pitch angle of 20.0 degrees
695 pitchAngle
= ((pitchAngle
* 25) / maxPitch
);
697 pitchAngle
-= 41; // 41 = 4 * AH_SYMBOL_COUNT + 5
699 for (int x
= -4; x
<= 4; x
++) {
700 const int y
= ((-rollAngle
* x
) / 64) - pitchAngle
;
701 if (y
>= 0 && y
<= 81) {
702 displayWriteChar(osdDisplayPort
, elemPosX
+ x
, elemPosY
+ (y
/ AH_SYMBOL_COUNT
), (SYM_AH_BAR9_0
+ (y
% AH_SYMBOL_COUNT
)));
709 case OSD_HORIZON_SIDEBARS
:
712 const int8_t hudwidth
= AH_SIDEBAR_WIDTH_POS
;
713 const int8_t hudheight
= AH_SIDEBAR_HEIGHT_POS
;
714 for (int y
= -hudheight
; y
<= hudheight
; y
++) {
715 displayWriteChar(osdDisplayPort
, elemPosX
- hudwidth
, elemPosY
+ y
, SYM_AH_DECORATION
);
716 displayWriteChar(osdDisplayPort
, elemPosX
+ hudwidth
, elemPosY
+ y
, SYM_AH_DECORATION
);
719 // AH level indicators
720 displayWriteChar(osdDisplayPort
, elemPosX
- hudwidth
+ 1, elemPosY
, SYM_AH_LEFT
);
721 displayWriteChar(osdDisplayPort
, elemPosX
+ hudwidth
- 1, elemPosY
, SYM_AH_RIGHT
);
729 for (int axis
= 0; axis
< XYZ_AXIS_COUNT
; axis
++) {
730 const float a
= accAverage
[axis
];
733 osdGForce
= sqrtf(osdGForce
) / acc
.dev
.acc_1G
;
734 tfp_sprintf(buff
, "%01d.%01dG", (int)osdGForce
, (int)(osdGForce
* 10) % 10);
739 osdFormatPID(buff
, "ROL", ¤tPidProfile
->pid
[PID_ROLL
]);
743 osdFormatPID(buff
, "PIT", ¤tPidProfile
->pid
[PID_PITCH
]);
747 osdFormatPID(buff
, "YAW", ¤tPidProfile
->pid
[PID_YAW
]);
751 tfp_sprintf(buff
, "%4dW", getAmperage() * getBatteryVoltage() / 1000);
754 case OSD_PIDRATE_PROFILE
:
755 tfp_sprintf(buff
, "%d-%d", getCurrentPidProfileIndex() + 1, getCurrentControlRateProfileIndex() + 1);
761 #define OSD_WARNINGS_MAX_SIZE 11
762 #define OSD_FORMAT_MESSAGE_BUFFER_SIZE (OSD_WARNINGS_MAX_SIZE + 1)
764 STATIC_ASSERT(OSD_FORMAT_MESSAGE_BUFFER_SIZE
<= sizeof(buff
), osd_warnings_size_exceeds_buffer_size
);
766 const batteryState_e batteryState
= getBatteryState();
769 if (isTryingToArm() && !ARMING_FLAG(ARMED
)) {
770 int armingDelayTime
= (getLastDshotBeaconCommandTimeUs() + DSHOT_BEACON_GUARD_DELAY_US
- micros()) / 1e5
;
771 if (armingDelayTime
< 0) {
774 if (armingDelayTime
>= (DSHOT_BEACON_GUARD_DELAY_US
/ 1e5
- 5)) {
775 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, " BEACON ON"); // Display this message for the first 0.5 seconds
777 char armingDelayMessage
[OSD_FORMAT_MESSAGE_BUFFER_SIZE
];
778 tfp_sprintf(armingDelayMessage
, "ARM IN %d.%d", armingDelayTime
/ 10, armingDelayTime
% 10);
779 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, armingDelayMessage
);
785 // Warn when in flip over after crash mode
786 if (osdWarnGetState(OSD_WARNING_CRASH_FLIP
) && isFlipOverAfterCrashMode()) {
787 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, "CRASH FLIP");
791 if (osdWarnGetState(OSD_WARNING_BATTERY_CRITICAL
) && batteryState
== BATTERY_CRITICAL
) {
792 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, " LAND NOW");
796 // Show warning if in HEADFREE flight mode
797 if (FLIGHT_MODE(HEADFREE_MODE
)) {
798 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, "HEADFREE");
802 #ifdef USE_ADC_INTERNAL
803 const int16_t coreTemperature
= getCoreTemperatureCelsius();
804 if (osdWarnGetState(OSD_WARNING_CORE_TEMPERATURE
) && coreTemperature
>= osdConfig()->core_temp_alarm
) {
805 char coreTemperatureWarningMsg
[OSD_FORMAT_MESSAGE_BUFFER_SIZE
];
806 tfp_sprintf(coreTemperatureWarningMsg
, "CORE: %3d%c", osdConvertTemperatureToSelectedUnit(coreTemperature
), osdGetTemperatureSymbolForSelectedUnit());
808 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, coreTemperatureWarningMsg
);
814 #ifdef USE_ESC_SENSOR
815 // Show warning if we lose motor output, the ESC is overheating or excessive current draw
816 if (feature(FEATURE_ESC_SENSOR
) && osdWarnGetState(OSD_WARNING_ESC_FAIL
)) {
817 char escWarningMsg
[OSD_FORMAT_MESSAGE_BUFFER_SIZE
];
820 const char *title
= "ESC";
822 // center justify message
823 while (pos
< (OSD_WARNINGS_MAX_SIZE
- (strlen(title
) + getMotorCount())) / 2) {
824 escWarningMsg
[pos
++] = ' ';
827 strcpy(escWarningMsg
+ pos
, title
);
828 pos
+= strlen(title
);
831 unsigned escWarningCount
= 0;
832 while (i
< getMotorCount() && pos
< OSD_FORMAT_MESSAGE_BUFFER_SIZE
- 1) {
833 escSensorData_t
*escData
= getEscSensorData(i
);
834 const char motorNumber
= '1' + i
;
835 // if everything is OK just display motor number else R, T or C
836 char warnFlag
= motorNumber
;
837 if (ARMING_FLAG(ARMED
) && osdConfig()->esc_rpm_alarm
!= ESC_RPM_ALARM_OFF
&& calcEscRpm(escData
->rpm
) <= osdConfig()->esc_rpm_alarm
) {
840 if (osdConfig()->esc_temp_alarm
!= ESC_TEMP_ALARM_OFF
&& escData
->temperature
>= osdConfig()->esc_temp_alarm
) {
843 if (ARMING_FLAG(ARMED
) && osdConfig()->esc_current_alarm
!= ESC_CURRENT_ALARM_OFF
&& escData
->current
>= osdConfig()->esc_current_alarm
) {
847 escWarningMsg
[pos
++] = warnFlag
;
849 if (warnFlag
!= motorNumber
) {
856 escWarningMsg
[pos
] = '\0';
858 if (escWarningCount
> 0) {
859 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, escWarningMsg
);
865 // Show most severe reason for arming being disabled
866 if (osdWarnGetState(OSD_WARNING_ARMING_DISABLE
) && IS_RC_MODE_ACTIVE(BOXARM
) && isArmingDisabled()) {
867 const armingDisableFlags_e flags
= getArmingDisableFlags();
868 for (int i
= 0; i
< ARMING_DISABLE_FLAGS_COUNT
; i
++) {
869 if (flags
& (1 << i
)) {
870 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, armingDisableFlagNames
[i
]);
877 if (osdWarnGetState(OSD_WARNING_BATTERY_WARNING
) && batteryState
== BATTERY_WARNING
) {
878 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, "LOW BATTERY");
882 #ifdef USE_RC_SMOOTHING_FILTER
883 // Show warning if rc smoothing hasn't initialized the filters
884 if (osdWarnGetState(OSD_WARNING_RC_SMOOTHING
) && ARMING_FLAG(ARMED
) && !rcSmoothingInitializationComplete()) {
885 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, "RCSMOOTHING");
890 // Show warning if battery is not fresh
891 if (osdWarnGetState(OSD_WARNING_BATTERY_NOT_FULL
) && !ARMING_FLAG(WAS_EVER_ARMED
) && (getBatteryState() == BATTERY_OK
)
892 && getBatteryAverageCellVoltage() < batteryConfig()->vbatfullcellvoltage
) {
893 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, "BATT < FULL");
898 if (osdWarnGetState(OSD_WARNING_VISUAL_BEEPER
) && showVisualBeeper
) {
899 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, " * * * *");
903 osdFormatMessage(buff
, OSD_FORMAT_MESSAGE_BUFFER_SIZE
, NULL
);
907 case OSD_AVG_CELL_VOLTAGE
:
909 const int cellV
= osdGetBatteryAverageCellVoltage();
910 buff
[0] = osdGetBatterySymbol(cellV
);
911 tfp_sprintf(buff
+ 1, "%d.%02d%c", cellV
/ 100, cellV
% 100, SYM_VOLT
);
916 tfp_sprintf(buff
, "DBG %5d %5d %5d %5d", debug
[0], debug
[1], debug
[2], debug
[3]);
919 case OSD_PITCH_ANGLE
:
922 const int angle
= (item
== OSD_PITCH_ANGLE
) ? attitude
.values
.pitch
: attitude
.values
.roll
;
923 tfp_sprintf(buff
, "%c%02d.%01d", angle
< 0 ? '-' : ' ', abs(angle
/ 10), abs(angle
% 10));
927 case OSD_MAIN_BATT_USAGE
:
929 // Set length of indicator bar
930 #define MAIN_BATT_USAGE_STEPS 11 // Use an odd number so the bar can be centered.
932 // Calculate constrained value
933 const float value
= constrain(batteryConfig()->batteryCapacity
- getMAhDrawn(), 0, batteryConfig()->batteryCapacity
);
935 // Calculate mAh used progress
936 const uint8_t mAhUsedProgress
= ceilf((value
/ (batteryConfig()->batteryCapacity
/ MAIN_BATT_USAGE_STEPS
)));
938 // Create empty battery indicator bar
939 buff
[0] = SYM_PB_START
;
940 for (int i
= 1; i
<= MAIN_BATT_USAGE_STEPS
; i
++) {
941 buff
[i
] = i
<= mAhUsedProgress
? SYM_PB_FULL
: SYM_PB_EMPTY
;
943 buff
[MAIN_BATT_USAGE_STEPS
+ 1] = SYM_PB_CLOSE
;
944 if (mAhUsedProgress
> 0 && mAhUsedProgress
< MAIN_BATT_USAGE_STEPS
) {
945 buff
[1 + mAhUsedProgress
] = SYM_PB_END
;
947 buff
[MAIN_BATT_USAGE_STEPS
+2] = '\0';
952 if (!ARMING_FLAG(ARMED
)) {
953 tfp_sprintf(buff
, "DISARMED");
955 if (!lastArmState
) { // previously disarmed - blank out the message one time
956 tfp_sprintf(buff
, " ");
961 case OSD_NUMERICAL_HEADING
:
963 const int heading
= DECIDEGREES_TO_DEGREES(attitude
.values
.yaw
);
964 tfp_sprintf(buff
, "%c%03d", osdGetDirectionSymbolFromHeading(heading
), heading
);
968 case OSD_NUMERICAL_VARIO
:
970 bool haveBaro
= false;
971 bool haveGps
= false;
973 haveBaro
= sensors(SENSOR_BARO
);
976 haveGps
= sensors(SENSOR_GPS
) && STATE(GPS_FIX
);
978 if (haveBaro
|| haveGps
) {
979 const int verticalSpeed
= osdGetMetersToSelectedUnit(getEstimatedVario());
980 const char directionSymbol
= verticalSpeed
< 0 ? SYM_ARROW_SOUTH
: SYM_ARROW_NORTH
;
981 tfp_sprintf(buff
, "%c%01d.%01d", directionSymbol
, abs(verticalSpeed
/ 100), abs((verticalSpeed
% 100) / 10));
983 // We use this symbol when we don't have a valid measure
985 // overwrite any previous vertical speed with blanks
986 memset(buff
+ 1, SYM_BLANK
, 6);
992 #ifdef USE_ESC_SENSOR
994 if (feature(FEATURE_ESC_SENSOR
)) {
995 tfp_sprintf(buff
, "%3d%c", osdConvertTemperatureToSelectedUnit(escDataCombined
->temperature
), osdGetTemperatureSymbolForSelectedUnit());
1000 if (feature(FEATURE_ESC_SENSOR
)) {
1001 tfp_sprintf(buff
, "%5d", escDataCombined
== NULL
? 0 : calcEscRpm(escDataCombined
->rpm
));
1007 case OSD_RTC_DATETIME
:
1008 osdFormatRtcDateTime(&buff
[0]);
1012 #ifdef USE_OSD_ADJUSTMENTS
1013 case OSD_ADJUSTMENT_RANGE
:
1014 if (getAdjustmentsRangeName()) {
1015 tfp_sprintf(buff
, "%s: %3d", getAdjustmentsRangeName(), getAdjustmentsRangeValue());
1020 #ifdef USE_ADC_INTERNAL
1021 case OSD_CORE_TEMPERATURE
:
1022 tfp_sprintf(buff
, "%3d%c", osdConvertTemperatureToSelectedUnit(getCoreTemperatureCelsius()), osdGetTemperatureSymbolForSelectedUnit());
1030 displayWrite(osdDisplayPort
, elemPosX
, elemPosY
, buff
);
1035 static void osdDrawElements(void)
1037 displayClearScreen(osdDisplayPort
);
1039 // Hide OSD when OSDSW mode is active
1040 if (IS_RC_MODE_ACTIVE(BOXOSD
)) {
1044 if (sensors(SENSOR_ACC
)) {
1045 osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON
);
1046 osdDrawSingleElement(OSD_G_FORCE
);
1050 for (unsigned i
= 0; i
< sizeof(osdElementDisplayOrder
); i
++) {
1051 osdDrawSingleElement(osdElementDisplayOrder
[i
]);
1055 if (sensors(SENSOR_GPS
)) {
1056 osdDrawSingleElement(OSD_GPS_SATS
);
1057 osdDrawSingleElement(OSD_GPS_SPEED
);
1058 osdDrawSingleElement(OSD_GPS_LAT
);
1059 osdDrawSingleElement(OSD_GPS_LON
);
1060 osdDrawSingleElement(OSD_HOME_DIST
);
1061 osdDrawSingleElement(OSD_HOME_DIR
);
1065 #ifdef USE_ESC_SENSOR
1066 if (feature(FEATURE_ESC_SENSOR
)) {
1067 osdDrawSingleElement(OSD_ESC_TMP
);
1068 osdDrawSingleElement(OSD_ESC_RPM
);
1073 osdDrawSingleElement(OSD_RTC_DATETIME
);
1076 #ifdef USE_OSD_ADJUSTMENTS
1077 osdDrawSingleElement(OSD_ADJUSTMENT_RANGE
);
1080 #ifdef USE_ADC_INTERNAL
1081 osdDrawSingleElement(OSD_CORE_TEMPERATURE
);
1085 void pgResetFn_osdConfig(osdConfig_t
*osdConfig
)
1087 // Position elements near centre of screen and disabled by default
1088 for (int i
= 0; i
< OSD_ITEM_COUNT
; i
++) {
1089 osdConfig
->item_pos
[i
] = OSD_POS(10, 7);
1092 // Always enable warnings elements by default
1093 osdConfig
->item_pos
[OSD_WARNINGS
] = OSD_POS(9, 10) | VISIBLE_FLAG
;
1095 // Default to old fixed positions for these elements
1096 osdConfig
->item_pos
[OSD_CROSSHAIRS
] = OSD_POS(13, 6);
1097 osdConfig
->item_pos
[OSD_ARTIFICIAL_HORIZON
] = OSD_POS(14, 2);
1098 osdConfig
->item_pos
[OSD_HORIZON_SIDEBARS
] = OSD_POS(14, 6);
1100 // Enable the default stats
1101 osdConfig
->enabled_stats
= 0; // reset all to off and enable only a few initially
1102 osdStatSetState(OSD_STAT_MAX_SPEED
, true);
1103 osdStatSetState(OSD_STAT_MIN_BATTERY
, true);
1104 osdStatSetState(OSD_STAT_MIN_RSSI
, true);
1105 osdStatSetState(OSD_STAT_MAX_CURRENT
, true);
1106 osdStatSetState(OSD_STAT_USED_MAH
, true);
1107 osdStatSetState(OSD_STAT_BLACKBOX
, true);
1108 osdStatSetState(OSD_STAT_BLACKBOX_NUMBER
, true);
1109 osdStatSetState(OSD_STAT_TIMER_2
, true);
1111 osdConfig
->units
= OSD_UNIT_METRIC
;
1113 // Enable all warnings by default
1114 for (int i
=0; i
< OSD_WARNING_COUNT
; i
++) {
1115 osdWarnSetState(i
, true);
1118 osdConfig
->timers
[OSD_TIMER_1
] = OSD_TIMER(OSD_TIMER_SRC_ON
, OSD_TIMER_PREC_SECOND
, 10);
1119 osdConfig
->timers
[OSD_TIMER_2
] = OSD_TIMER(OSD_TIMER_SRC_TOTAL_ARMED
, OSD_TIMER_PREC_SECOND
, 10);
1121 osdConfig
->rssi_alarm
= 20;
1122 osdConfig
->cap_alarm
= 2200;
1123 osdConfig
->alt_alarm
= 100; // meters or feet depend on configuration
1124 osdConfig
->esc_temp_alarm
= ESC_TEMP_ALARM_OFF
; // off by default
1125 osdConfig
->esc_rpm_alarm
= ESC_RPM_ALARM_OFF
; // off by default
1126 osdConfig
->esc_current_alarm
= ESC_CURRENT_ALARM_OFF
; // off by default
1127 osdConfig
->core_temp_alarm
= 70; // a temperature above 70C should produce a warning, lockups have been reported above 80C
1129 osdConfig
->ahMaxPitch
= 20; // 20 degrees
1130 osdConfig
->ahMaxRoll
= 40; // 40 degrees
1133 static void osdDrawLogo(int x
, int y
)
1135 // display logo and help
1136 int fontOffset
= 160;
1137 for (int row
= 0; row
< 4; row
++) {
1138 for (int column
= 0; column
< 24; column
++) {
1139 if (fontOffset
<= SYM_END_OF_FONT
)
1140 displayWriteChar(osdDisplayPort
, x
+ column
, y
+ row
, fontOffset
++);
1145 void osdInit(displayPort_t
*osdDisplayPortToUse
)
1147 if (!osdDisplayPortToUse
) {
1151 BUILD_BUG_ON(OSD_POS_MAX
!= OSD_POS(31,31));
1153 osdDisplayPort
= osdDisplayPortToUse
;
1155 cmsDisplayPortRegister(osdDisplayPort
);
1158 armState
= ARMING_FLAG(ARMED
);
1160 memset(blinkBits
, 0, sizeof(blinkBits
));
1162 displayClearScreen(osdDisplayPort
);
1166 char string_buffer
[30];
1167 tfp_sprintf(string_buffer
, "V%s", FC_VERSION_STRING
);
1168 displayWrite(osdDisplayPort
, 20, 6, string_buffer
);
1170 displayWrite(osdDisplayPort
, 7, 8, CMS_STARTUP_HELP_TEXT1
);
1171 displayWrite(osdDisplayPort
, 11, 9, CMS_STARTUP_HELP_TEXT2
);
1172 displayWrite(osdDisplayPort
, 11, 10, CMS_STARTUP_HELP_TEXT3
);
1176 char dateTimeBuffer
[FORMATTED_DATE_TIME_BUFSIZE
];
1177 if (osdFormatRtcDateTime(&dateTimeBuffer
[0])) {
1178 displayWrite(osdDisplayPort
, 5, 12, dateTimeBuffer
);
1182 displayResync(osdDisplayPort
);
1184 resumeRefreshAt
= micros() + (4 * REFRESH_1S
);
1187 bool osdInitialized(void)
1189 return osdDisplayPort
;
1192 void osdUpdateAlarms(void)
1194 // This is overdone?
1196 int32_t alt
= osdGetMetersToSelectedUnit(getEstimatedAltitude()) / 100;
1198 if (getRssiPercent() < osdConfig()->rssi_alarm
) {
1199 SET_BLINK(OSD_RSSI_VALUE
);
1201 CLR_BLINK(OSD_RSSI_VALUE
);
1204 // Determine if the OSD_WARNINGS should blink
1205 if (getBatteryState() != BATTERY_OK
1206 && (osdWarnGetState(OSD_WARNING_BATTERY_CRITICAL
) || osdWarnGetState(OSD_WARNING_BATTERY_WARNING
))
1208 && (!isTryingToArm())
1211 SET_BLINK(OSD_WARNINGS
);
1213 CLR_BLINK(OSD_WARNINGS
);
1216 if (getBatteryState() == BATTERY_OK
) {
1217 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1218 CLR_BLINK(OSD_AVG_CELL_VOLTAGE
);
1220 SET_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1221 SET_BLINK(OSD_AVG_CELL_VOLTAGE
);
1224 if (STATE(GPS_FIX
) == 0) {
1225 SET_BLINK(OSD_GPS_SATS
);
1227 CLR_BLINK(OSD_GPS_SATS
);
1230 for (int i
= 0; i
< OSD_TIMER_COUNT
; i
++) {
1231 const uint16_t timer
= osdConfig()->timers
[i
];
1232 const timeUs_t time
= osdGetTimerValue(OSD_TIMER_SRC(timer
));
1233 const timeUs_t alarmTime
= OSD_TIMER_ALARM(timer
) * 60000000; // convert from minutes to us
1234 if (alarmTime
!= 0 && time
>= alarmTime
) {
1235 SET_BLINK(OSD_ITEM_TIMER_1
+ i
);
1237 CLR_BLINK(OSD_ITEM_TIMER_1
+ i
);
1241 if (getMAhDrawn() >= osdConfig()->cap_alarm
) {
1242 SET_BLINK(OSD_MAH_DRAWN
);
1243 SET_BLINK(OSD_MAIN_BATT_USAGE
);
1244 SET_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1246 CLR_BLINK(OSD_MAH_DRAWN
);
1247 CLR_BLINK(OSD_MAIN_BATT_USAGE
);
1248 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1251 if (alt
>= osdConfig()->alt_alarm
) {
1252 SET_BLINK(OSD_ALTITUDE
);
1254 CLR_BLINK(OSD_ALTITUDE
);
1257 #ifdef USE_ESC_SENSOR
1258 if (feature(FEATURE_ESC_SENSOR
)) {
1259 // This works because the combined ESC data contains the maximum temperature seen amongst all ESCs
1260 if (osdConfig()->esc_temp_alarm
!= ESC_TEMP_ALARM_OFF
&& escDataCombined
->temperature
>= osdConfig()->esc_temp_alarm
) {
1261 SET_BLINK(OSD_ESC_TMP
);
1263 CLR_BLINK(OSD_ESC_TMP
);
1269 void osdResetAlarms(void)
1271 CLR_BLINK(OSD_RSSI_VALUE
);
1272 CLR_BLINK(OSD_MAIN_BATT_VOLTAGE
);
1273 CLR_BLINK(OSD_WARNINGS
);
1274 CLR_BLINK(OSD_GPS_SATS
);
1275 CLR_BLINK(OSD_MAH_DRAWN
);
1276 CLR_BLINK(OSD_ALTITUDE
);
1277 CLR_BLINK(OSD_AVG_CELL_VOLTAGE
);
1278 CLR_BLINK(OSD_MAIN_BATT_USAGE
);
1279 CLR_BLINK(OSD_ITEM_TIMER_1
);
1280 CLR_BLINK(OSD_ITEM_TIMER_2
);
1281 CLR_BLINK(OSD_REMAINING_TIME_ESTIMATE
);
1282 CLR_BLINK(OSD_ESC_TMP
);
1285 static void osdResetStats(void)
1287 stats
.max_current
= 0;
1288 stats
.max_speed
= 0;
1289 stats
.min_voltage
= 500;
1290 stats
.min_rssi
= 99;
1291 stats
.max_altitude
= 0;
1292 stats
.max_distance
= 0;
1293 stats
.armed_time
= 0;
1296 static void osdUpdateStats(void)
1300 switch (osdConfig()->units
) {
1301 case OSD_UNIT_IMPERIAL
:
1302 value
= CM_S_TO_MPH(gpsSol
.groundSpeed
);
1305 value
= CM_S_TO_KM_H(gpsSol
.groundSpeed
);
1309 if (stats
.max_speed
< value
) {
1310 stats
.max_speed
= value
;
1313 value
= getBatteryVoltage();
1314 if (stats
.min_voltage
> value
) {
1315 stats
.min_voltage
= value
;
1318 value
= getAmperage() / 100;
1319 if (stats
.max_current
< value
) {
1320 stats
.max_current
= value
;
1323 value
= getRssiPercent();
1324 if (stats
.min_rssi
> value
) {
1325 stats
.min_rssi
= value
;
1328 int altitude
= getEstimatedAltitude();
1329 if (stats
.max_altitude
< altitude
) {
1330 stats
.max_altitude
= altitude
;
1334 if (STATE(GPS_FIX
) && STATE(GPS_FIX_HOME
)) {
1335 value
= GPS_distanceToHome
;
1337 if (stats
.max_distance
< GPS_distanceToHome
) {
1338 stats
.max_distance
= GPS_distanceToHome
;
1345 static void osdGetBlackboxStatusString(char * buff
)
1347 bool storageDeviceIsWorking
= false;
1348 uint32_t storageUsed
= 0;
1349 uint32_t storageTotal
= 0;
1351 switch (blackboxConfig()->device
) {
1353 case BLACKBOX_DEVICE_SDCARD
:
1354 storageDeviceIsWorking
= sdcard_isInserted() && sdcard_isFunctional() && (afatfs_getFilesystemState() == AFATFS_FILESYSTEM_STATE_READY
);
1355 if (storageDeviceIsWorking
) {
1356 storageTotal
= sdcard_getMetadata()->numBlocks
/ 2000;
1357 storageUsed
= storageTotal
- (afatfs_getContiguousFreeSpace() / 1024000);
1363 case BLACKBOX_DEVICE_FLASH
:
1364 storageDeviceIsWorking
= flashfsIsSupported();
1365 if (storageDeviceIsWorking
) {
1366 const flashGeometry_t
*geometry
= flashfsGetGeometry();
1367 storageTotal
= geometry
->totalSize
/ 1024;
1368 storageUsed
= flashfsGetOffset() / 1024;
1377 if (storageDeviceIsWorking
) {
1378 const uint16_t storageUsedPercent
= (storageUsed
* 100) / storageTotal
;
1379 tfp_sprintf(buff
, "%d%%", storageUsedPercent
);
1381 tfp_sprintf(buff
, "FAULT");
1386 static void osdDisplayStatisticLabel(uint8_t y
, const char * text
, const char * value
)
1388 displayWrite(osdDisplayPort
, 2, y
, text
);
1389 displayWrite(osdDisplayPort
, 20, y
, ":");
1390 displayWrite(osdDisplayPort
, 22, y
, value
);
1394 * Test if there's some stat enabled
1396 static bool isSomeStatEnabled(void)
1398 return (osdConfig()->enabled_stats
!= 0);
1401 // *** IMPORTANT ***
1402 // The order of the OSD stats as displayed on-screen must match the osd_stats_e enumeration.
1403 // This is because the fields are presented in the configurator in the order of the enumeration
1404 // and we want the configuration order to match the on-screen display order. If you change the
1405 // display order you *must* update the osd_stats_e enumeration to match. Additionally the
1406 // changes to the stats display order *must* be implemented in the configurator otherwise the
1407 // stats selections will not be populated correctly and the settings will become corrupted.
1409 static void osdShowStats(uint16_t endBatteryVoltage
)
1412 char buff
[OSD_ELEMENT_BUFFER_LENGTH
];
1414 displayClearScreen(osdDisplayPort
);
1415 displayWrite(osdDisplayPort
, 2, top
++, " --- STATS ---");
1417 if (osdStatGetState(OSD_STAT_RTC_DATE_TIME
)) {
1418 bool success
= false;
1420 success
= osdFormatRtcDateTime(&buff
[0]);
1423 tfp_sprintf(buff
, "NO RTC");
1426 displayWrite(osdDisplayPort
, 2, top
++, buff
);
1429 if (osdStatGetState(OSD_STAT_TIMER_1
)) {
1430 osdFormatTimer(buff
, false, (OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_1
]) == OSD_TIMER_SRC_ON
? false : true), OSD_TIMER_1
);
1431 osdDisplayStatisticLabel(top
++, osdTimerSourceNames
[OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_1
])], buff
);
1434 if (osdStatGetState(OSD_STAT_TIMER_2
)) {
1435 osdFormatTimer(buff
, false, (OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_2
]) == OSD_TIMER_SRC_ON
? false : true), OSD_TIMER_2
);
1436 osdDisplayStatisticLabel(top
++, osdTimerSourceNames
[OSD_TIMER_SRC(osdConfig()->timers
[OSD_TIMER_2
])], buff
);
1439 if (osdStatGetState(OSD_STAT_MAX_SPEED
) && STATE(GPS_FIX
)) {
1440 itoa(stats
.max_speed
, buff
, 10);
1441 osdDisplayStatisticLabel(top
++, "MAX SPEED", buff
);
1444 if (osdStatGetState(OSD_STAT_MAX_DISTANCE
)) {
1445 tfp_sprintf(buff
, "%d%c", osdGetMetersToSelectedUnit(stats
.max_distance
), osdGetMetersToSelectedUnitSymbol());
1446 osdDisplayStatisticLabel(top
++, "MAX DISTANCE", buff
);
1449 if (osdStatGetState(OSD_STAT_MIN_BATTERY
)) {
1450 tfp_sprintf(buff
, "%d.%1d%c", stats
.min_voltage
/ 10, stats
.min_voltage
% 10, SYM_VOLT
);
1451 osdDisplayStatisticLabel(top
++, "MIN BATTERY", buff
);
1454 if (osdStatGetState(OSD_STAT_END_BATTERY
)) {
1455 tfp_sprintf(buff
, "%d.%1d%c", endBatteryVoltage
/ 10, endBatteryVoltage
% 10, SYM_VOLT
);
1456 osdDisplayStatisticLabel(top
++, "END BATTERY", buff
);
1459 if (osdStatGetState(OSD_STAT_BATTERY
)) {
1460 tfp_sprintf(buff
, "%d.%1d%c", getBatteryVoltage() / 10, getBatteryVoltage() % 10, SYM_VOLT
);
1461 osdDisplayStatisticLabel(top
++, "BATTERY", buff
);
1464 if (osdStatGetState(OSD_STAT_MIN_RSSI
)) {
1465 itoa(stats
.min_rssi
, buff
, 10);
1467 osdDisplayStatisticLabel(top
++, "MIN RSSI", buff
);
1470 if (batteryConfig()->currentMeterSource
!= CURRENT_METER_NONE
) {
1471 if (osdStatGetState(OSD_STAT_MAX_CURRENT
)) {
1472 itoa(stats
.max_current
, buff
, 10);
1474 osdDisplayStatisticLabel(top
++, "MAX CURRENT", buff
);
1477 if (osdStatGetState(OSD_STAT_USED_MAH
)) {
1478 tfp_sprintf(buff
, "%d%c", getMAhDrawn(), SYM_MAH
);
1479 osdDisplayStatisticLabel(top
++, "USED MAH", buff
);
1483 if (osdStatGetState(OSD_STAT_MAX_ALTITUDE
)) {
1484 osdFormatAltitudeString(buff
, stats
.max_altitude
);
1485 osdDisplayStatisticLabel(top
++, "MAX ALTITUDE", buff
);
1489 if (osdStatGetState(OSD_STAT_BLACKBOX
) && blackboxConfig()->device
&& blackboxConfig()->device
!= BLACKBOX_DEVICE_SERIAL
) {
1490 osdGetBlackboxStatusString(buff
);
1491 osdDisplayStatisticLabel(top
++, "BLACKBOX", buff
);
1494 if (osdStatGetState(OSD_STAT_BLACKBOX_NUMBER
) && blackboxConfig()->device
&& blackboxConfig()->device
!= BLACKBOX_DEVICE_SERIAL
) {
1495 itoa(blackboxGetLogNumber(), buff
, 10);
1496 osdDisplayStatisticLabel(top
++, "BB LOG NUM", buff
);
1501 static void osdShowArmed(void)
1503 displayClearScreen(osdDisplayPort
);
1504 displayWrite(osdDisplayPort
, 12, 7, "ARMED");
1507 STATIC_UNIT_TESTED
void osdRefresh(timeUs_t currentTimeUs
)
1509 static timeUs_t lastTimeUs
= 0;
1510 static bool osdStatsEnabled
= false;
1511 static bool osdStatsVisible
= false;
1512 static timeUs_t osdStatsRefreshTimeUs
;
1513 static uint16_t endBatteryVoltage
;
1515 // detect arm/disarm
1516 if (armState
!= ARMING_FLAG(ARMED
)) {
1517 if (ARMING_FLAG(ARMED
)) {
1518 osdStatsEnabled
= false;
1519 osdStatsVisible
= false;
1522 resumeRefreshAt
= currentTimeUs
+ (REFRESH_1S
/ 2);
1523 } else if (isSomeStatEnabled()
1524 && (!(getArmingDisableFlags() & ARMING_DISABLED_RUNAWAY_TAKEOFF
)
1525 || !VISIBLE(osdConfig()->item_pos
[OSD_WARNINGS
]))) { // suppress stats if runaway takeoff triggered disarm and WARNINGS element is visible
1526 osdStatsEnabled
= true;
1527 resumeRefreshAt
= currentTimeUs
+ (60 * REFRESH_1S
);
1528 endBatteryVoltage
= getBatteryVoltage();
1531 armState
= ARMING_FLAG(ARMED
);
1535 if (ARMING_FLAG(ARMED
)) {
1537 timeUs_t deltaT
= currentTimeUs
- lastTimeUs
;
1539 stats
.armed_time
+= deltaT
;
1540 } else if (osdStatsEnabled
) { // handle showing/hiding stats based on OSD disable switch position
1541 if (displayIsGrabbed(osdDisplayPort
)) {
1542 osdStatsEnabled
= false;
1543 resumeRefreshAt
= 0;
1544 stats
.armed_time
= 0;
1546 if (IS_RC_MODE_ACTIVE(BOXOSD
) && osdStatsVisible
) {
1547 osdStatsVisible
= false;
1548 displayClearScreen(osdDisplayPort
);
1549 } else if (!IS_RC_MODE_ACTIVE(BOXOSD
)) {
1550 if (!osdStatsVisible
) {
1551 osdStatsVisible
= true;
1552 osdStatsRefreshTimeUs
= 0;
1554 if (currentTimeUs
>= osdStatsRefreshTimeUs
) {
1555 osdStatsRefreshTimeUs
= currentTimeUs
+ REFRESH_1S
;
1556 osdShowStats(endBatteryVoltage
);
1561 lastTimeUs
= currentTimeUs
;
1563 if (resumeRefreshAt
) {
1564 if (cmp32(currentTimeUs
, resumeRefreshAt
) < 0) {
1565 // in timeout period, check sticks for activity to resume display.
1566 if (IS_HI(THROTTLE
) || IS_HI(PITCH
)) {
1567 resumeRefreshAt
= currentTimeUs
;
1569 displayHeartbeat(osdDisplayPort
);
1572 displayClearScreen(osdDisplayPort
);
1573 resumeRefreshAt
= 0;
1574 osdStatsEnabled
= false;
1575 stats
.armed_time
= 0;
1579 blinkState
= (currentTimeUs
/ 200000) % 2;
1581 #ifdef USE_ESC_SENSOR
1582 if (feature(FEATURE_ESC_SENSOR
)) {
1583 escDataCombined
= getEscSensorData(ESC_SENSOR_COMBINED
);
1588 if (!displayIsGrabbed(osdDisplayPort
))
1593 displayHeartbeat(osdDisplayPort
);
1595 lastArmState
= ARMING_FLAG(ARMED
);
1599 * Called periodically by the scheduler
1601 void osdUpdate(timeUs_t currentTimeUs
)
1603 static uint32_t counter
= 0;
1606 showVisualBeeper
= true;
1609 #ifdef MAX7456_DMA_CHANNEL_TX
1610 // don't touch buffers if DMA transaction is in progress
1611 if (displayIsTransferInProgress(osdDisplayPort
)) {
1614 #endif // MAX7456_DMA_CHANNEL_TX
1616 #ifdef USE_SLOW_MSP_DISPLAYPORT_RATE_WHEN_UNARMED
1617 static uint32_t idlecounter
= 0;
1618 if (!ARMING_FLAG(ARMED
)) {
1619 if (idlecounter
++ % 4 != 0) {
1625 // redraw values in buffer
1627 #define DRAW_FREQ_DENOM 5
1629 #define DRAW_FREQ_DENOM 10 // MWOSD @ 115200 baud (
1631 #define STATS_FREQ_DENOM 50
1633 if (counter
% DRAW_FREQ_DENOM
== 0) {
1634 osdRefresh(currentTimeUs
);
1635 showVisualBeeper
= false;
1637 // rest of time redraw screen 10 chars per idle so it doesn't lock the main idle
1638 displayDrawScreen(osdDisplayPort
);
1643 // do not allow ARM if we are in menu
1644 if (displayIsGrabbed(osdDisplayPort
)) {
1645 setArmingDisabled(ARMING_DISABLED_OSD_MENU
);
1647 unsetArmingDisabled(ARMING_DISABLED_OSD_MENU
);