/* * This file is part of Cleanflight. * * Cleanflight is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Cleanflight is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Cleanflight. If not, see . */ /* Created by Marcin Baliniak some functions based on MinimOSD OSD-CMS separation by jflyper */ #include #include #include #include #include #include #include "platform.h" #ifdef USE_OSD #include "blackbox/blackbox.h" #include "blackbox/blackbox_io.h" #include "build/build_config.h" #include "build/debug.h" #include "build/version.h" #include "cms/cms.h" #include "cms/cms_types.h" #include "cms/cms_menu_osd.h" #include "common/maths.h" #include "common/printf.h" #include "common/typeconversion.h" #include "common/utils.h" #include "config/feature.h" #include "config/parameter_group.h" #include "config/parameter_group_ids.h" #include "drivers/display.h" #include "drivers/max7456_symbols.h" #include "drivers/time.h" #include "drivers/vtx_common.h" #include "io/asyncfatfs/asyncfatfs.h" #include "io/beeper.h" #include "io/flashfs.h" #include "io/gps.h" #include "io/osd.h" #include "io/vtx_rtc6705.h" #include "io/vtx_control.h" #include "io/vtx_string.h" #include "fc/config.h" #include "fc/rc_controls.h" #include "fc/runtime_config.h" #include "fc/rc_adjustments.h" #include "flight/altitude.h" #include "flight/navigation.h" #include "flight/pid.h" #include "flight/imu.h" #include "rx/rx.h" #include "sensors/barometer.h" #include "sensors/battery.h" #include "sensors/sensors.h" #include "sensors/esc_sensor.h" #ifdef USE_HARDWARE_REVISION_DETECTION #include "hardware_revision.h" #endif #define VIDEO_BUFFER_CHARS_PAL 480 #define FULL_CIRCLE 360 const char * const osdTimerSourceNames[] = { "ON TIME ", "TOTAL ARM", "LAST ARM " }; // Blink control static bool blinkState = true; static bool showVisualBeeper = false; static uint32_t blinkBits[(OSD_ITEM_COUNT + 31)/32]; #define SET_BLINK(item) (blinkBits[(item) / 32] |= (1 << ((item) % 32))) #define CLR_BLINK(item) (blinkBits[(item) / 32] &= ~(1 << ((item) % 32))) #define IS_BLINK(item) (blinkBits[(item) / 32] & (1 << ((item) % 32))) #define BLINK(item) (IS_BLINK(item) && blinkState) // Things in both OSD and CMS #define IS_HI(X) (rcData[X] > 1750) #define IS_LO(X) (rcData[X] < 1250) #define IS_MID(X) (rcData[X] > 1250 && rcData[X] < 1750) static timeUs_t flyTime = 0; static uint8_t statRssi; typedef struct statistic_s { int16_t max_speed; int16_t min_voltage; // /10 int16_t max_current; // /10 int16_t min_rssi; int16_t max_altitude; int16_t max_distance; timeUs_t armed_time; } statistic_t; static statistic_t stats; uint32_t resumeRefreshAt = 0; #define REFRESH_1S 1000 * 1000 static uint8_t armState; static displayPort_t *osdDisplayPort; #define AH_SYMBOL_COUNT 9 #define AH_SIDEBAR_WIDTH_POS 7 #define AH_SIDEBAR_HEIGHT_POS 3 static const char compassBar[] = { SYM_HEADING_W, SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE, SYM_HEADING_N, SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE, SYM_HEADING_E, SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE, SYM_HEADING_S, SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE, SYM_HEADING_W, SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE, SYM_HEADING_N, SYM_HEADING_LINE, SYM_HEADING_DIVIDED_LINE, SYM_HEADING_LINE }; PG_REGISTER_WITH_RESET_FN(osdConfig_t, osdConfig, PG_OSD_CONFIG, 1); #ifdef USE_ESC_SENSOR static escSensorData_t *escData; #endif /** * Gets the correct altitude symbol for the current unit system */ static char osdGetMetersToSelectedUnitSymbol(void) { switch (osdConfig()->units) { case OSD_UNIT_IMPERIAL: return SYM_FT; default: return SYM_M; } } /** * Gets average battery cell voltage in 0.01V units. */ static int osdGetBatteryAverageCellVoltage(void) { return (getBatteryVoltage() * 10) / getBatteryCellCount(); } static char osdGetBatterySymbol(int cellVoltage) { if (getBatteryState() == BATTERY_CRITICAL) { return SYM_MAIN_BATT; // FIXME: currently the BAT- symbol, ideally replace with a battery with exclamation mark } else { /* Calculate a symbol offset using cell voltage over full cell voltage range */ int symOffset = scaleRange(cellVoltage, batteryConfig()->vbatmincellvoltage * 10, batteryConfig()->vbatmaxcellvoltage * 10, 0, 7); return SYM_BATT_EMPTY - constrain(symOffset, 0, 6); } } /** * Converts altitude based on the current unit system. * @param meters Value in meters to convert */ static int32_t osdGetMetersToSelectedUnit(int32_t meters) { switch (osdConfig()->units) { case OSD_UNIT_IMPERIAL: return (meters * 328) / 100; // Convert to feet / 100 default: return meters; // Already in metre / 100 } } static void osdFormatAltitudeString(char * buff, int altitude, bool pad) { const int alt = osdGetMetersToSelectedUnit(altitude); int altitudeIntergerPart = abs(alt / 100); if (alt < 0) { altitudeIntergerPart *= -1; } tfp_sprintf(buff, pad ? "%4d.%01d%c" : "%d.%01d%c", altitudeIntergerPart, abs((alt % 100) / 10), osdGetMetersToSelectedUnitSymbol()); } static void osdFormatPID(char * buff, const char * label, const pid8_t * pid) { tfp_sprintf(buff, "%s %3d %3d %3d", label, pid->P, pid->I, pid->D); } static uint8_t osdGetHeadingIntoDiscreteDirections(int heading, unsigned directions) { heading += FULL_CIRCLE; // Ensure positive value // Split input heading 0..359 into sectors 0..(directions-1), but offset // by half a sector so that sector 0 gets centered around heading 0. // We multiply heading by directions to not loose precision in divisions // In this way each segment will be a FULL_CIRCLE length int direction = (heading * directions + FULL_CIRCLE / 2) / FULL_CIRCLE; // scale with rounding direction %= directions; // normalize return direction; // return segment number } static uint8_t osdGetDirectionSymbolFromHeading(int heading) { heading = osdGetHeadingIntoDiscreteDirections(heading, 16); // Now heading has a heading with Up=0, Right=4, Down=8 and Left=12 // Our symbols are Down=0, Right=4, Up=8 and Left=12 // There're 16 arrow symbols. Transform it. heading = 16 - heading; heading = (heading + 8) % 16; return SYM_ARROW_SOUTH + heading; } static char osdGetTimerSymbol(osd_timer_source_e src) { switch (src) { case OSD_TIMER_SRC_ON: return SYM_ON_M; case OSD_TIMER_SRC_TOTAL_ARMED: case OSD_TIMER_SRC_LAST_ARMED: return SYM_FLY_M; default: return ' '; } } static timeUs_t osdGetTimerValue(osd_timer_source_e src) { switch (src) { case OSD_TIMER_SRC_ON: return micros(); case OSD_TIMER_SRC_TOTAL_ARMED: return flyTime; case OSD_TIMER_SRC_LAST_ARMED: return stats.armed_time; default: return 0; } } STATIC_UNIT_TESTED void osdFormatTime(char * buff, osd_timer_precision_e precision, timeUs_t time) { int seconds = time / 1000000; const int minutes = seconds / 60; seconds = seconds % 60; switch (precision) { case OSD_TIMER_PREC_SECOND: default: tfp_sprintf(buff, "%02d:%02d", minutes, seconds); break; case OSD_TIMER_PREC_HUNDREDTHS: { const int hundredths = (time / 10000) % 100; tfp_sprintf(buff, "%02d:%02d.%02d", minutes, seconds, hundredths); break; } } } STATIC_UNIT_TESTED void osdFormatTimer(char *buff, bool showSymbol, int timerIndex) { const uint16_t timer = osdConfig()->timers[timerIndex]; const uint8_t src = OSD_TIMER_SRC(timer); if (showSymbol) { *(buff++) = osdGetTimerSymbol(src); } osdFormatTime(buff, OSD_TIMER_PRECISION(timer), osdGetTimerValue(src)); } static void osdDrawSingleElement(uint8_t item) { if (!VISIBLE(osdConfig()->item_pos[item]) || BLINK(item)) { return; } uint8_t elemPosX = OSD_X(osdConfig()->item_pos[item]); uint8_t elemPosY = OSD_Y(osdConfig()->item_pos[item]); uint8_t elemOffsetX = 0; char buff[OSD_ELEMENT_BUFFER_LENGTH]; switch (item) { case OSD_RSSI_VALUE: { uint16_t osdRssi = rssi * 100 / 1024; // change range if (osdRssi >= 100) osdRssi = 99; tfp_sprintf(buff, "%c%d", SYM_RSSI, osdRssi); break; } case OSD_MAIN_BATT_VOLTAGE: buff[0] = osdGetBatterySymbol(osdGetBatteryAverageCellVoltage()); tfp_sprintf(buff + 1, "%d.%1d%c", getBatteryVoltage() / 10, getBatteryVoltage() % 10, SYM_VOLT); break; case OSD_CURRENT_DRAW: { const int32_t amperage = getAmperage(); tfp_sprintf(buff, "%3d.%02d%c", abs(amperage) / 100, abs(amperage) % 100, SYM_AMP); break; } case OSD_MAH_DRAWN: tfp_sprintf(buff, "%4d%c", getMAhDrawn(), SYM_MAH); break; #ifdef USE_GPS case OSD_GPS_SATS: tfp_sprintf(buff, "%c%c%d", SYM_SAT_L, SYM_SAT_R, gpsSol.numSat); break; case OSD_GPS_SPEED: // FIXME ideally we want to use SYM_KMH symbol but it's not in the font any more, so we use K. tfp_sprintf(buff, "%3dK", CM_S_TO_KM_H(gpsSol.groundSpeed)); break; case OSD_GPS_LAT: case OSD_GPS_LON: { int32_t val; if (item == OSD_GPS_LAT) { buff[0] = SYM_ARROW_EAST; val = gpsSol.llh.lat; } else { buff[0] = SYM_ARROW_SOUTH; val = gpsSol.llh.lon; } char wholeDegreeString[5]; tfp_sprintf(wholeDegreeString, "%d", val / GPS_DEGREES_DIVIDER); char wholeUnshifted[12]; tfp_sprintf(wholeUnshifted, "%d", val); tfp_sprintf(buff + 1, "%s.%s", wholeDegreeString, wholeUnshifted + strlen(wholeDegreeString)); break; } case OSD_HOME_DIST: if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) { int32_t distance = osdGetMetersToSelectedUnit(GPS_distanceToHome); tfp_sprintf(buff, "%d%c", distance, osdGetMetersToSelectedUnitSymbol()); } else { // We use this symbol when we don't have a FIX buff[0] = SYM_COLON; buff[1] = 0; } break; case OSD_HOME_DIR: if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME)) { if (GPS_distanceToHome > 0) { const int h = GPS_directionToHome - DECIDEGREES_TO_DEGREES(attitude.values.yaw); buff[0] = osdGetDirectionSymbolFromHeading(h); } else { // We don't have a HOME symbol in the font, by now we use this buff[0] = SYM_THR1; } } else { // We use this symbol when we don't have a FIX buff[0] = SYM_COLON; } buff[1] = 0; break; #endif // GPS case OSD_COMPASS_BAR: { int16_t h = DECIDEGREES_TO_DEGREES(attitude.values.yaw); h = osdGetHeadingIntoDiscreteDirections(h, 16); memcpy(buff, compassBar + h, 9); buff[9]=0; break; } case OSD_ALTITUDE: { osdFormatAltitudeString(buff, getEstimatedAltitude(), true); break; } case OSD_ITEM_TIMER_1: case OSD_ITEM_TIMER_2: { const int timer = item - OSD_ITEM_TIMER_1; osdFormatTimer(buff, true, timer); break; } case OSD_FLYMODE: { char *p = "ACRO"; if (isAirmodeActive()) p = "AIR"; if (FLIGHT_MODE(FAILSAFE_MODE)) p = "!FS!"; else if (FLIGHT_MODE(ANGLE_MODE)) p = "STAB"; else if (FLIGHT_MODE(HORIZON_MODE)) p = "HOR"; displayWrite(osdDisplayPort, elemPosX, elemPosY, p); return; } case OSD_CRAFT_NAME: if (strlen(pilotConfig()->name) == 0) strcpy(buff, "CRAFT_NAME"); else { for (unsigned int i = 0; i < MAX_NAME_LENGTH; i++) { buff[i] = toupper((unsigned char)pilotConfig()->name[i]); if (pilotConfig()->name[i] == 0) break; } } break; case OSD_THROTTLE_POS: buff[0] = SYM_THR; buff[1] = SYM_THR1; tfp_sprintf(buff + 2, "%d", (constrain(rcData[THROTTLE], PWM_RANGE_MIN, PWM_RANGE_MAX) - PWM_RANGE_MIN) * 100 / (PWM_RANGE_MAX - PWM_RANGE_MIN)); break; #if defined(VTX_COMMON) case OSD_VTX_CHANNEL: { uint8_t band=0, channel=0; vtxCommonGetBandAndChannel(&band,&channel); uint8_t power = 0; vtxCommonGetPowerIndex(&power); const char vtxBandLetter = vtx58BandLetter[band]; const char *vtxChannelName = vtx58ChannelNames[channel]; tfp_sprintf(buff, "%c:%s:%d", vtxBandLetter, vtxChannelName, power); break; } #endif case OSD_CROSSHAIRS: elemPosX = 14 - 1; // Offset for 1 char to the left elemPosY = 6; if (displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL) { ++elemPosY; } buff[0] = SYM_AH_CENTER_LINE; buff[1] = SYM_AH_CENTER; buff[2] = SYM_AH_CENTER_LINE_RIGHT; buff[3] = 0; break; case OSD_ARTIFICIAL_HORIZON: { elemPosX = 14; elemPosY = 6 - 4; // Top center of the AH area // Get pitch and roll limits in tenths of degrees const int maxPitch = osdConfig()->ahMaxPitch * 10; const int maxRoll = osdConfig()->ahMaxRoll * 10; const int rollAngle = constrain(attitude.values.roll, -maxRoll, maxRoll); int pitchAngle = constrain(attitude.values.pitch, -maxPitch, maxPitch); if (displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL) { ++elemPosY; } // Convert pitchAngle to y compensation value // (maxPitch / 25) divisor matches previous settings of fixed divisor of 8 and fixed max AHI pitch angle of 20.0 degrees pitchAngle = ((pitchAngle * 25) / maxPitch) - 41; // 41 = 4 * AH_SYMBOL_COUNT + 5 for (int x = -4; x <= 4; x++) { const int y = ((-rollAngle * x) / 64) - pitchAngle; if (y >= 0 && y <= 81) { displayWriteChar(osdDisplayPort, elemPosX + x, elemPosY + (y / AH_SYMBOL_COUNT), (SYM_AH_BAR9_0 + (y % AH_SYMBOL_COUNT))); } } osdDrawSingleElement(OSD_HORIZON_SIDEBARS); return; } case OSD_HORIZON_SIDEBARS: { elemPosX = 14; elemPosY = 6; if (displayScreenSize(osdDisplayPort) == VIDEO_BUFFER_CHARS_PAL) { ++elemPosY; } // Draw AH sides const int8_t hudwidth = AH_SIDEBAR_WIDTH_POS; const int8_t hudheight = AH_SIDEBAR_HEIGHT_POS; for (int y = -hudheight; y <= hudheight; y++) { displayWriteChar(osdDisplayPort, elemPosX - hudwidth, elemPosY + y, SYM_AH_DECORATION); displayWriteChar(osdDisplayPort, elemPosX + hudwidth, elemPosY + y, SYM_AH_DECORATION); } // AH level indicators displayWriteChar(osdDisplayPort, elemPosX - hudwidth + 1, elemPosY, SYM_AH_LEFT); displayWriteChar(osdDisplayPort, elemPosX + hudwidth - 1, elemPosY, SYM_AH_RIGHT); return; } case OSD_ROLL_PIDS: { const pidProfile_t *pidProfile = currentPidProfile; osdFormatPID(buff, "ROL", &pidProfile->pid[PID_ROLL]); break; } case OSD_PITCH_PIDS: { const pidProfile_t *pidProfile = currentPidProfile; osdFormatPID(buff, "PIT", &pidProfile->pid[PID_PITCH]); break; } case OSD_YAW_PIDS: { const pidProfile_t *pidProfile = currentPidProfile; osdFormatPID(buff, "YAW", &pidProfile->pid[PID_YAW]); break; } case OSD_POWER: tfp_sprintf(buff, "%4dW", getAmperage() * getBatteryVoltage() / 1000); break; case OSD_PIDRATE_PROFILE: { const uint8_t pidProfileIndex = getCurrentPidProfileIndex(); const uint8_t rateProfileIndex = getCurrentControlRateProfileIndex(); tfp_sprintf(buff, "%d-%d", pidProfileIndex + 1, rateProfileIndex + 1); break; } case OSD_WARNINGS: { uint16_t enabledWarnings = osdConfig()->enabledWarnings; const batteryState_e batteryState = getBatteryState(); if (enabledWarnings & OSD_WARNING_BATTERY_CRITICAL && batteryState == BATTERY_CRITICAL) { tfp_sprintf(buff, " LAND NOW"); break; } /* Warn when in flip over after crash mode */ if ((enabledWarnings & OSD_WARNING_CRASH_FLIP) && (isModeActivationConditionPresent(BOXFLIPOVERAFTERCRASH)) && IS_RC_MODE_ACTIVE(BOXFLIPOVERAFTERCRASH)) { tfp_sprintf(buff, "CRASH FLIP"); break; } /* Show most severe reason for arming being disabled */ if (enabledWarnings & OSD_WARNING_ARMING_DISABLE && IS_RC_MODE_ACTIVE(BOXARM) && isArmingDisabled()) { const armingDisableFlags_e flags = getArmingDisableFlags(); for (int i = 0; i < NUM_ARMING_DISABLE_FLAGS; i++) { if (flags & (1 << i)) { tfp_sprintf(buff, "%s", armingDisableFlagNames[i]); break; } } break; } if (enabledWarnings & OSD_WARNING_BATTERY_WARNING && batteryState == BATTERY_WARNING) { tfp_sprintf(buff, "LOW BATTERY"); break; } /* Show warning if battery is not fresh */ if (enabledWarnings & OSD_WARNING_BATTERY_NOT_FULL && !ARMING_FLAG(WAS_EVER_ARMED) && (getBatteryState() == BATTERY_OK) && getBatteryAverageCellVoltage() < batteryConfig()->vbatfullcellvoltage) { tfp_sprintf(buff, "BATT NOT FULL"); break; } /* Visual beeper */ if (enabledWarnings & OSD_WARNING_VISUAL_BEEPER && showVisualBeeper) { tfp_sprintf(buff, " * * * *"); break; } return; } case OSD_AVG_CELL_VOLTAGE: { const int cellV = osdGetBatteryAverageCellVoltage(); buff[0] = osdGetBatterySymbol(cellV); tfp_sprintf(buff + 1, "%d.%02d%c", cellV / 100, cellV % 100, SYM_VOLT); break; } case OSD_DEBUG: tfp_sprintf(buff, "DBG %5d %5d %5d %5d", debug[0], debug[1], debug[2], debug[3]); break; case OSD_PITCH_ANGLE: case OSD_ROLL_ANGLE: { const int angle = (item == OSD_PITCH_ANGLE) ? attitude.values.pitch : attitude.values.roll; tfp_sprintf(buff, "%c%02d.%01d", angle < 0 ? '-' : ' ', abs(angle / 10), abs(angle % 10)); break; } case OSD_MAIN_BATT_USAGE: { //Set length of indicator bar #define MAIN_BATT_USAGE_STEPS 11 // Use an odd number so the bar can be centralised. //Calculate constrained value float value = constrain(batteryConfig()->batteryCapacity - getMAhDrawn(), 0, batteryConfig()->batteryCapacity); //Calculate mAh used progress uint8_t mAhUsedProgress = ceil((value / (batteryConfig()->batteryCapacity / MAIN_BATT_USAGE_STEPS))); //Create empty battery indicator bar buff[0] = SYM_PB_START; for (uint8_t i = 1; i <= MAIN_BATT_USAGE_STEPS; i++) { if (i <= mAhUsedProgress) buff[i] = SYM_PB_FULL; else buff[i] = SYM_PB_EMPTY; } buff[MAIN_BATT_USAGE_STEPS+1] = SYM_PB_CLOSE; if (mAhUsedProgress > 0 && mAhUsedProgress < MAIN_BATT_USAGE_STEPS) { buff[1+mAhUsedProgress] = SYM_PB_END; } buff[MAIN_BATT_USAGE_STEPS+2] = 0; break; } case OSD_DISARMED: if (!ARMING_FLAG(ARMED)) { tfp_sprintf(buff, "DISARMED"); break; } else { return; } case OSD_NUMERICAL_HEADING: { const int heading = DECIDEGREES_TO_DEGREES(attitude.values.yaw); tfp_sprintf(buff, "%c%03d", osdGetDirectionSymbolFromHeading(heading), heading); break; } case OSD_NUMERICAL_VARIO: { const int verticalSpeed = osdGetMetersToSelectedUnit(getEstimatedVario()); const char directionSymbol = verticalSpeed < 0 ? SYM_ARROW_SOUTH : SYM_ARROW_NORTH; tfp_sprintf(buff, "%c%01d.%01d", directionSymbol, abs(verticalSpeed / 100), abs((verticalSpeed % 100) / 10)); break; } #ifdef USE_ESC_SENSOR case OSD_ESC_TMP: tfp_sprintf(buff, "%d%c", escData == NULL ? 0 : escData->temperature, SYM_TEMP_C); break; case OSD_ESC_RPM: tfp_sprintf(buff, "%d", escData == NULL ? 0 : escData->rpm); break; #endif default: return; } displayWrite(osdDisplayPort, elemPosX + elemOffsetX, elemPosY, buff); } static void osdDrawElements(void) { displayClearScreen(osdDisplayPort); /* Hide OSD when OSDSW mode is active */ if (IS_RC_MODE_ACTIVE(BOXOSD)) return; if (sensors(SENSOR_ACC)) { osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON); } osdDrawSingleElement(OSD_MAIN_BATT_VOLTAGE); osdDrawSingleElement(OSD_RSSI_VALUE); osdDrawSingleElement(OSD_CROSSHAIRS); osdDrawSingleElement(OSD_ITEM_TIMER_1); osdDrawSingleElement(OSD_ITEM_TIMER_2); osdDrawSingleElement(OSD_FLYMODE); osdDrawSingleElement(OSD_THROTTLE_POS); osdDrawSingleElement(OSD_VTX_CHANNEL); osdDrawSingleElement(OSD_CURRENT_DRAW); osdDrawSingleElement(OSD_MAH_DRAWN); osdDrawSingleElement(OSD_CRAFT_NAME); osdDrawSingleElement(OSD_ALTITUDE); osdDrawSingleElement(OSD_ROLL_PIDS); osdDrawSingleElement(OSD_PITCH_PIDS); osdDrawSingleElement(OSD_YAW_PIDS); osdDrawSingleElement(OSD_POWER); osdDrawSingleElement(OSD_PIDRATE_PROFILE); osdDrawSingleElement(OSD_WARNINGS); osdDrawSingleElement(OSD_AVG_CELL_VOLTAGE); osdDrawSingleElement(OSD_DEBUG); osdDrawSingleElement(OSD_PITCH_ANGLE); osdDrawSingleElement(OSD_ROLL_ANGLE); osdDrawSingleElement(OSD_MAIN_BATT_USAGE); osdDrawSingleElement(OSD_DISARMED); osdDrawSingleElement(OSD_NUMERICAL_HEADING); osdDrawSingleElement(OSD_NUMERICAL_VARIO); osdDrawSingleElement(OSD_COMPASS_BAR); #ifdef USE_GPS if (sensors(SENSOR_GPS)) { osdDrawSingleElement(OSD_GPS_SATS); osdDrawSingleElement(OSD_GPS_SPEED); osdDrawSingleElement(OSD_GPS_LAT); osdDrawSingleElement(OSD_GPS_LON); osdDrawSingleElement(OSD_HOME_DIST); osdDrawSingleElement(OSD_HOME_DIR); } #endif // GPS #ifdef USE_ESC_SENSOR if (feature(FEATURE_ESC_SENSOR)) { osdDrawSingleElement(OSD_ESC_TMP); osdDrawSingleElement(OSD_ESC_RPM); } #endif } void pgResetFn_osdConfig(osdConfig_t *osdConfig) { /* Position elements near centre of screen and disabled by default */ for (int i = 0; i < OSD_ITEM_COUNT; i++) { osdConfig->item_pos[i] = OSD_POS(10, 6); } /* Always enable warnings elements by default */ osdConfig->item_pos[OSD_WARNINGS] = OSD_POS(9, 10) | VISIBLE_FLAG; osdConfig->enabled_stats[OSD_STAT_MAX_SPEED] = true; osdConfig->enabled_stats[OSD_STAT_MIN_BATTERY] = true; osdConfig->enabled_stats[OSD_STAT_MIN_RSSI] = true; osdConfig->enabled_stats[OSD_STAT_MAX_CURRENT] = true; osdConfig->enabled_stats[OSD_STAT_USED_MAH] = true; osdConfig->enabled_stats[OSD_STAT_MAX_ALTITUDE] = false; osdConfig->enabled_stats[OSD_STAT_BLACKBOX] = true; osdConfig->enabled_stats[OSD_STAT_END_BATTERY] = false; osdConfig->enabled_stats[OSD_STAT_MAX_DISTANCE] = false; osdConfig->enabled_stats[OSD_STAT_BLACKBOX_NUMBER] = true; osdConfig->enabled_stats[OSD_STAT_TIMER_1] = false; osdConfig->enabled_stats[OSD_STAT_TIMER_2] = true; osdConfig->units = OSD_UNIT_METRIC; /* Enable all warnings by default */ osdConfig->enabledWarnings = UINT16_MAX; osdConfig->timers[OSD_TIMER_1] = OSD_TIMER(OSD_TIMER_SRC_ON, OSD_TIMER_PREC_SECOND, 10); osdConfig->timers[OSD_TIMER_2] = OSD_TIMER(OSD_TIMER_SRC_TOTAL_ARMED, OSD_TIMER_PREC_SECOND, 10); osdConfig->rssi_alarm = 20; osdConfig->cap_alarm = 2200; osdConfig->alt_alarm = 100; // meters or feet depend on configuration osdConfig->ahMaxPitch = 20; // 20 degrees osdConfig->ahMaxRoll = 40; // 40 degrees } static void osdDrawLogo(int x, int y) { // display logo and help int fontOffset = 160; for (int row = 0; row < 4; row++) { for (int column = 0; column < 24; column++) { if (fontOffset <= SYM_END_OF_FONT) displayWriteChar(osdDisplayPort, x + column, y + row, fontOffset++); } } } void osdInit(displayPort_t *osdDisplayPortToUse) { if (!osdDisplayPortToUse) return; BUILD_BUG_ON(OSD_POS_MAX != OSD_POS(31,31)); osdDisplayPort = osdDisplayPortToUse; #ifdef USE_CMS cmsDisplayPortRegister(osdDisplayPort); #endif armState = ARMING_FLAG(ARMED); memset(blinkBits, 0, sizeof(blinkBits)); displayClearScreen(osdDisplayPort); osdDrawLogo(3, 1); char string_buffer[30]; tfp_sprintf(string_buffer, "V%s", FC_VERSION_STRING); displayWrite(osdDisplayPort, 20, 6, string_buffer); #ifdef USE_CMS displayWrite(osdDisplayPort, 7, 8, CMS_STARTUP_HELP_TEXT1); displayWrite(osdDisplayPort, 11, 9, CMS_STARTUP_HELP_TEXT2); displayWrite(osdDisplayPort, 11, 10, CMS_STARTUP_HELP_TEXT3); #endif displayResync(osdDisplayPort); resumeRefreshAt = micros() + (4 * REFRESH_1S); } void osdUpdateAlarms(void) { // This is overdone? int32_t alt = osdGetMetersToSelectedUnit(getEstimatedAltitude()) / 100; if (statRssi < osdConfig()->rssi_alarm) SET_BLINK(OSD_RSSI_VALUE); else CLR_BLINK(OSD_RSSI_VALUE); if (getBatteryState() == BATTERY_OK) { CLR_BLINK(OSD_WARNINGS); CLR_BLINK(OSD_MAIN_BATT_VOLTAGE); CLR_BLINK(OSD_AVG_CELL_VOLTAGE); } else { SET_BLINK(OSD_WARNINGS); SET_BLINK(OSD_MAIN_BATT_VOLTAGE); SET_BLINK(OSD_AVG_CELL_VOLTAGE); } if (STATE(GPS_FIX) == 0) SET_BLINK(OSD_GPS_SATS); else CLR_BLINK(OSD_GPS_SATS); for (int i = 0; i < OSD_TIMER_COUNT; i++) { const uint16_t timer = osdConfig()->timers[i]; const timeUs_t time = osdGetTimerValue(OSD_TIMER_SRC(timer)); const timeUs_t alarmTime = OSD_TIMER_ALARM(timer) * 60000000; // convert from minutes to us if (alarmTime != 0 && time >= alarmTime) SET_BLINK(OSD_ITEM_TIMER_1 + i); else CLR_BLINK(OSD_ITEM_TIMER_1 + i); } if (getMAhDrawn() >= osdConfig()->cap_alarm) { SET_BLINK(OSD_MAH_DRAWN); SET_BLINK(OSD_MAIN_BATT_USAGE); } else { CLR_BLINK(OSD_MAH_DRAWN); CLR_BLINK(OSD_MAIN_BATT_USAGE); } if (alt >= osdConfig()->alt_alarm) SET_BLINK(OSD_ALTITUDE); else CLR_BLINK(OSD_ALTITUDE); } void osdResetAlarms(void) { CLR_BLINK(OSD_RSSI_VALUE); CLR_BLINK(OSD_MAIN_BATT_VOLTAGE); CLR_BLINK(OSD_WARNINGS); CLR_BLINK(OSD_GPS_SATS); CLR_BLINK(OSD_MAH_DRAWN); CLR_BLINK(OSD_ALTITUDE); CLR_BLINK(OSD_AVG_CELL_VOLTAGE); CLR_BLINK(OSD_MAIN_BATT_USAGE); CLR_BLINK(OSD_ITEM_TIMER_1); CLR_BLINK(OSD_ITEM_TIMER_2); } static void osdResetStats(void) { stats.max_current = 0; stats.max_speed = 0; stats.min_voltage = 500; stats.max_current = 0; stats.min_rssi = 99; stats.max_altitude = 0; stats.max_distance = 0; stats.armed_time = 0; } static void osdUpdateStats(void) { int16_t value = 0; #ifdef USE_GPS value = CM_S_TO_KM_H(gpsSol.groundSpeed); #endif if (stats.max_speed < value) stats.max_speed = value; if (stats.min_voltage > getBatteryVoltage()) stats.min_voltage = getBatteryVoltage(); value = getAmperage() / 100; if (stats.max_current < value) stats.max_current = value; if (stats.min_rssi > statRssi) stats.min_rssi = statRssi; if (stats.max_altitude < getEstimatedAltitude()) stats.max_altitude = getEstimatedAltitude(); #ifdef USE_GPS if (STATE(GPS_FIX) && STATE(GPS_FIX_HOME) && (stats.max_distance < GPS_distanceToHome)) { stats.max_distance = GPS_distanceToHome; } #endif } #ifdef USE_BLACKBOX static void osdGetBlackboxStatusString(char * buff) { bool storageDeviceIsWorking = false; uint32_t storageUsed = 0; uint32_t storageTotal = 0; switch (blackboxConfig()->device) { #ifdef USE_SDCARD case BLACKBOX_DEVICE_SDCARD: storageDeviceIsWorking = sdcard_isInserted() && sdcard_isFunctional() && (afatfs_getFilesystemState() == AFATFS_FILESYSTEM_STATE_READY); if (storageDeviceIsWorking) { storageTotal = sdcard_getMetadata()->numBlocks / 2000; storageUsed = storageTotal - (afatfs_getContiguousFreeSpace() / 1024000); } break; #endif #ifdef USE_FLASHFS case BLACKBOX_DEVICE_FLASH: storageDeviceIsWorking = flashfsIsReady(); if (storageDeviceIsWorking) { const flashGeometry_t *geometry = flashfsGetGeometry(); storageTotal = geometry->totalSize / 1024; storageUsed = flashfsGetOffset() / 1024; } break; #endif default: storageDeviceIsWorking = true; } if (storageDeviceIsWorking) { const uint16_t storageUsedPercent = (storageUsed * 100) / storageTotal; tfp_sprintf(buff, "%d%%", storageUsedPercent); } else { tfp_sprintf(buff, "FAULT"); } } #endif static void osdDisplayStatisticLabel(uint8_t y, const char * text, const char * value) { displayWrite(osdDisplayPort, 2, y, text); displayWrite(osdDisplayPort, 20, y, ":"); displayWrite(osdDisplayPort, 22, y, value); } /* * Test if there's some stat enabled */ static bool isSomeStatEnabled(void) { for (int i = 0; i < OSD_STAT_COUNT; i++) { if (osdConfig()->enabled_stats[i]) { return true; } } return false; } static void osdShowStats(void) { uint8_t top = 2; char buff[10]; displayClearScreen(osdDisplayPort); displayWrite(osdDisplayPort, 2, top++, " --- STATS ---"); if (osdConfig()->enabled_stats[OSD_STAT_TIMER_1]) { osdFormatTimer(buff, false, OSD_TIMER_1); osdDisplayStatisticLabel(top++, osdTimerSourceNames[OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_1])], buff); } if (osdConfig()->enabled_stats[OSD_STAT_TIMER_2]) { osdFormatTimer(buff, false, OSD_TIMER_2); osdDisplayStatisticLabel(top++, osdTimerSourceNames[OSD_TIMER_SRC(osdConfig()->timers[OSD_TIMER_2])], buff); } if (osdConfig()->enabled_stats[OSD_STAT_MAX_SPEED] && STATE(GPS_FIX)) { itoa(stats.max_speed, buff, 10); osdDisplayStatisticLabel(top++, "MAX SPEED", buff); } if (osdConfig()->enabled_stats[OSD_STAT_MAX_DISTANCE]) { tfp_sprintf(buff, "%d%c", osdGetMetersToSelectedUnit(stats.max_distance), osdGetMetersToSelectedUnitSymbol()); osdDisplayStatisticLabel(top++, "MAX DISTANCE", buff); } if (osdConfig()->enabled_stats[OSD_STAT_MIN_BATTERY]) { tfp_sprintf(buff, "%d.%1d%c", stats.min_voltage / 10, stats.min_voltage % 10, SYM_VOLT); osdDisplayStatisticLabel(top++, "MIN BATTERY", buff); } if (osdConfig()->enabled_stats[OSD_STAT_END_BATTERY]) { tfp_sprintf(buff, "%d.%1d%c", getBatteryVoltage() / 10, getBatteryVoltage() % 10, SYM_VOLT); osdDisplayStatisticLabel(top++, "END BATTERY", buff); } if (osdConfig()->enabled_stats[OSD_STAT_MIN_RSSI]) { itoa(stats.min_rssi, buff, 10); strcat(buff, "%"); osdDisplayStatisticLabel(top++, "MIN RSSI", buff); } if (batteryConfig()->currentMeterSource != CURRENT_METER_NONE) { if (osdConfig()->enabled_stats[OSD_STAT_MAX_CURRENT]) { itoa(stats.max_current, buff, 10); strcat(buff, "A"); osdDisplayStatisticLabel(top++, "MAX CURRENT", buff); } if (osdConfig()->enabled_stats[OSD_STAT_USED_MAH]) { tfp_sprintf(buff, "%d%c", getMAhDrawn(), SYM_MAH); osdDisplayStatisticLabel(top++, "USED MAH", buff); } } if (osdConfig()->enabled_stats[OSD_STAT_MAX_ALTITUDE]) { osdFormatAltitudeString(buff, stats.max_altitude, false); osdDisplayStatisticLabel(top++, "MAX ALTITUDE", buff); } #ifdef USE_BLACKBOX if (osdConfig()->enabled_stats[OSD_STAT_BLACKBOX] && blackboxConfig()->device && blackboxConfig()->device != BLACKBOX_DEVICE_SERIAL) { osdGetBlackboxStatusString(buff); osdDisplayStatisticLabel(top++, "BLACKBOX", buff); } if (osdConfig()->enabled_stats[OSD_STAT_BLACKBOX_NUMBER] && blackboxConfig()->device && blackboxConfig()->device != BLACKBOX_DEVICE_SERIAL) { itoa(blackboxGetLogNumber(), buff, 10); osdDisplayStatisticLabel(top++, "BB LOG NUM", buff); } #endif /* Reset time since last armed here to ensure this timer is at zero when back at "main" OSD screen */ stats.armed_time = 0; } static void osdShowArmed(void) { displayClearScreen(osdDisplayPort); displayWrite(osdDisplayPort, 12, 7, "ARMED"); } STATIC_UNIT_TESTED void osdRefresh(timeUs_t currentTimeUs) { static timeUs_t lastTimeUs = 0; #ifdef USE_OSD_ADJUSTMENTS if (isAnyAdjustmentFunctionBusy()) { return; } #endif // detect arm/disarm if (armState != ARMING_FLAG(ARMED)) { if (ARMING_FLAG(ARMED)) { osdResetStats(); osdShowArmed(); resumeRefreshAt = currentTimeUs + (REFRESH_1S / 2); } else if (isSomeStatEnabled()) { osdShowStats(); resumeRefreshAt = currentTimeUs + (60 * REFRESH_1S); } armState = ARMING_FLAG(ARMED); } statRssi = scaleRange(rssi, 0, 1024, 0, 100); osdUpdateStats(); if (ARMING_FLAG(ARMED)) { timeUs_t deltaT = currentTimeUs - lastTimeUs; flyTime += deltaT; stats.armed_time += deltaT; } lastTimeUs = currentTimeUs; if (resumeRefreshAt) { if (cmp32(currentTimeUs, resumeRefreshAt) < 0) { // in timeout period, check sticks for activity to resume display. if (IS_HI(THROTTLE) || IS_HI(PITCH)) { resumeRefreshAt = 0; } displayHeartbeat(osdDisplayPort); return; } else { displayClearScreen(osdDisplayPort); resumeRefreshAt = 0; } } blinkState = (currentTimeUs / 200000) % 2; #ifdef USE_ESC_SENSOR if (feature(FEATURE_ESC_SENSOR)) { escData = getEscSensorData(ESC_SENSOR_COMBINED); } #endif #ifdef USE_CMS if (!displayIsGrabbed(osdDisplayPort)) { osdUpdateAlarms(); osdDrawElements(); displayHeartbeat(osdDisplayPort); #ifdef OSD_CALLS_CMS } else { cmsUpdate(currentTimeUs); #endif } #endif } /* * Called periodically by the scheduler */ void osdUpdate(timeUs_t currentTimeUs) { static uint32_t counter = 0; if (isBeeperOn()) { showVisualBeeper = true; } #ifdef MAX7456_DMA_CHANNEL_TX // don't touch buffers if DMA transaction is in progress if (displayIsTransferInProgress(osdDisplayPort)) { return; } #endif // MAX7456_DMA_CHANNEL_TX // redraw values in buffer #ifdef USE_MAX7456 #define DRAW_FREQ_DENOM 5 #else #define DRAW_FREQ_DENOM 10 // MWOSD @ 115200 baud ( #endif #ifdef USE_SLOW_MSP_DISPLAYPORT_RATE_WHEN_UNARMED static uint32_t idlecounter = 0; if (!ARMING_FLAG(ARMED)) { if (idlecounter++ % 4 != 0) { return; } } #endif if (counter++ % DRAW_FREQ_DENOM == 0) { osdRefresh(currentTimeUs); showVisualBeeper = false; } else { // rest of time redraw screen 10 chars per idle so it doesn't lock the main idle displayDrawScreen(osdDisplayPort); } #ifdef USE_CMS // do not allow ARM if we are in menu if (displayIsGrabbed(osdDisplayPort)) { setArmingDisabled(ARMING_DISABLED_OSD_MENU); } else { unsetArmingDisabled(ARMING_DISABLED_OSD_MENU); } #endif } #ifdef USE_OSD_ADJUSTMENTS void osdShowAdjustment(const char * type, int newValue) { char buff[OSD_ELEMENT_BUFFER_LENGTH]; tfp_sprintf(buff, "%s: %3d", type, newValue); displayWrite(osdDisplayPort, round(15 - strlen(buff) / 2), 7, buff); } #endif #endif // OSD