From cd67c181f017312ee3c37948844151e9ea9fee3d Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Tue, 12 May 2026 17:00:23 +0300 Subject: [PATCH 01/26] implement vtol mission auto transition --- docs/MixerProfile.md | 22 +++ docs/Navigation.md | 31 ++++ src/main/fc/settings.yaml | 26 ++++ src/main/flight/mixer_profile.c | 62 ++++++-- src/main/flight/mixer_profile.h | 5 +- src/main/navigation/navigation.c | 182 ++++++++++++++++++++++- src/main/navigation/navigation.h | 11 ++ src/main/navigation/navigation_private.h | 1 + 8 files changed, 320 insertions(+), 20 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 685e17a4e70..1fdd9132dad 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -72,6 +72,28 @@ This feature is mainly for RTH in a failsafe event. When set properly, model wil Set `mixer_automated_switch` to `ON` in mixer_profile for MC mode. Set `mixer_switch_trans_timer` in mixer_profile for MC mode for the time required to gain airspeed for your model before entering to FW mode. When `mixer_automated_switch`:`OFF` is set for all mixer_profiles(defaults). Model will not perform automated transition at all. +### Mission-authorized VTOL transition (waypoint User Action) + +INAV supports mission-requested VTOL transitions through the existing automated transition path. This is configured with: + +- `nav_vtol_mission_transition_user_action` (`OFF`, `USER1`, `USER2`, `USER3`, `USER4`) +- `nav_vtol_mission_transition_min_altitude_cm` (optional, `0` disables minimum-altitude check) +- `mixer_switch_trans_airspeed_cm_s` (optional, airspeed-based MC->FW switch threshold) + +On each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`), the configured USER action bit is used as absolute target selector: + +- selected USER bit = `0` -> transition to MC / MULTIROTOR profile +- selected USER bit = `1` -> transition to FW / AIRPLANE profile +- This is **not** a toggle command. +- If already in the requested profile type, the action is treated as complete (idempotent). + +The mission pauses while transition is in progress and resumes after completion. + +For MC -> FW mission transitions, navigation uses a straight acceleration segment (no loiter) to build speed before hot-switch. +When `mixer_switch_trans_airspeed_cm_s > 0` and valid pitot airspeed is available, automated MC->FW switching uses this airspeed target. If airspeed is unavailable, transition falls back to `mixer_switch_trans_timer`. + +Manual RC switching (`MIXER PROFILE 2`, `MIXER TRANSITION`) remains blocked during normal active navigation. Mission VTOL transition does not bypass the hot-switch safety guard; it only authorizes switching inside the automated transition state. + ## TailSitter (planned for INAV 7.1) TailSitter is supported by add a 90deg offset to the board alignment. Set the board aliment normally in the mixer_profile for FW mode(`set platform_type = AIRPLANE`), The motor trust axis should be same direction as the airplane nose. Then, in the mixer_profile for takeoff and landing set `tailsitter_orientation_offset = ON ` to apply orientation offset. orientation offset will also add a 45deg orientation offset. diff --git a/docs/Navigation.md b/docs/Navigation.md index 4d7b9740d91..c716925ba77 100755 --- a/docs/Navigation.md +++ b/docs/Navigation.md @@ -102,6 +102,37 @@ Parameters: * `` - Last waypoint must have `flag` set to 165 (0xA5). +### Mission VTOL transition using existing User Actions + +Mission VTOL transition can be requested. + +Configuration: + +- `nav_vtol_mission_transition_user_action` selects which waypoint User Action (`USER1..USER4`) is used as the mission VTOL target selector. +- `nav_vtol_mission_transition_min_altitude_cm` optionally enforces a minimum altitude before transition start (`0` disables check). +- `nav_vtol_mission_transition_track_distance_cm` configures straight-line MC->FW transition guidance distance. +- `mixer_switch_trans_airspeed_cm_s` configures MC->FW airspeed threshold for automated profile switching. + +Behavior on each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`): + +- The configured USER bit is an **absolute target selector**: + - `0`: transition to MC / MULTIROTOR profile + - `1`: transition to FW / AIRPLANE profile +- This command is **not** a toggle. +- The command is idempotent: if already in the requested target profile type, the mission continues immediately. +- If a transition is needed, mission progression pauses while automated transition runs, then resumes only after completion. + +Transition behavior in this MVP: + +- MC -> FW: straight-line acceleration segment (no loiter), heading from the next waypoint bearing when available, otherwise current heading. +- MC -> FW switch point: if valid pitot airspeed is available and `mixer_switch_trans_airspeed_cm_s > 0`, switch to FW occurs at/above the configured airspeed. If airspeed is unavailable, timer-based fallback (`mixer_switch_trans_timer`) is used. +- FW -> MC: mission pauses during automated transition, then resumes after switching back to MC profile. +- Strict altitude hold is not enforced during MC -> FW transition; natural climb is allowed. + +Safety and scope: + +- This path uses authorized automated transition state handling; it does not permit manual mixer profile switching during normal waypoint navigation. + `wp save` - Checks list of waypoints and save from FC to EEPROM (warning: it also saves all unsaved CLI settings like normal `save`). `wp reset` - Resets the list, sets the number of waypoints to 0 and marks the list as invalid (but doesn't delete the waypoint definitions). diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index c53ef66852f..44e34452fee 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -171,6 +171,9 @@ tables: - name: nav_wp_mission_restart enum: navMissionRestart_e values: ["START", "RESUME", "SWITCH"] + - name: nav_wp_user_action + enum: navMissionUserAction_e + values: ["OFF", "USER1", "USER2", "USER3", "USER4"] - name: djiRssiSource values: ["RSSI", "CRSF_LQ"] enum: djiRssiSource_e @@ -1277,6 +1280,12 @@ groups: field: mixer_config.switchTransitionTimer min: 0 max: 200 + - name: mixer_switch_trans_airspeed_cm_s + description: "Airspeed threshold [cm/s] for MC->FW automated profile switch. If > 0 and valid pitot airspeed is available, transition will switch to FW only after this speed is reached. If airspeed is unavailable, timer-based fallback (`mixer_switch_trans_timer`) is used." + default_value: 0 + field: mixer_config.switchTransitionAirspeed + min: 0 + max: 10000 - name: tailsitter_orientation_offset description: "Apply a 90 deg pitch offset in sensor aliment for tailsitter flying mode" default_value: OFF @@ -2623,6 +2632,23 @@ groups: default_value: "RESUME" field: general.flags.waypoint_mission_restart table: nav_wp_mission_restart + - name: nav_vtol_mission_transition_user_action + description: "Selects which waypoint USER action bit (`USER1`..`USER4`) is used as mission VTOL target selector. OFF disables this feature. On navigable mission waypoints: selected USER bit = 1 requests FW profile, selected USER bit = 0 requests MC profile." + default_value: "OFF" + field: general.vtol_mission_transition_user_action + table: nav_wp_user_action + - name: nav_vtol_mission_transition_min_altitude_cm + description: "Minimum altitude [cm] required to start a mission-authorized VTOL transition. Set to 0 to disable the minimum-altitude check." + default_value: 0 + field: general.vtol_mission_transition_min_altitude + min: 0 + max: 50000 + - name: nav_vtol_mission_transition_track_distance_cm + description: "Straight-line target distance [cm] used during mission-authorized MC->FW transition guidance. This controls how far ahead the transition heading target is placed." + default_value: 100000 + field: general.vtol_mission_transition_track_distance + min: 1000 + max: 500000 - name: nav_wp_multi_mission_index description: "Index of active mission selected from multi mission WP entry loaded in flight controller. Limited to a maximum of 9 missions." default_value: 1 diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index a39fbfeedd9..4362f418476 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -17,6 +17,7 @@ #include "flight/failsafe.h" #include "navigation/navigation.h" #include "navigation/navigation_private.h" +#include "sensors/pitotmeter.h" #include "fc/fc_core.h" #include "fc/config.h" @@ -37,7 +38,7 @@ bool isMixerTransitionMixing_requested; mixerProfileAT_t mixerProfileAT; int nextMixerProfileIndex; -PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 1); +PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 2); void pgResetFn_mixerProfiles(mixerProfile_t *instance) { @@ -53,6 +54,7 @@ void pgResetFn_mixerProfiles(mixerProfile_t *instance) .controlProfileLinking = SETTING_MIXER_CONTROL_PROFILE_LINKING_DEFAULT, .automated_switch = SETTING_MIXER_AUTOMATED_SWITCH_DEFAULT, .switchTransitionTimer = SETTING_MIXER_SWITCH_TRANS_TIMER_DEFAULT, + .switchTransitionAirspeed = SETTING_MIXER_SWITCH_TRANS_AIRSPEED_CM_S_DEFAULT, .tailsitterOrientationOffset = SETTING_TAILSITTER_ORIENTATION_OFFSET_DEFAULT, .transition_PID_mmix_multiplier_roll = SETTING_TRANSITION_PID_MMIX_MULTIPLIER_ROLL_DEFAULT, .transition_PID_mmix_multiplier_pitch = SETTING_TRANSITION_PID_MMIX_MULTIPLIER_PITCH_DEFAULT, @@ -112,6 +114,25 @@ void setMixerProfileAT(void) mixerProfileAT.transitionTransEndTime = mixerProfileAT.transitionStartTime + (timeMs_t)currentMixerConfig.switchTransitionTimer * 100; } +static bool requestTransitionsToFixedWing(const mixerProfileATRequest_e required_action) +{ + return required_action == MIXERAT_REQUEST_RTH || required_action == MIXERAT_REQUEST_MISSION_TO_FW; +} + +static bool mixerATReadyForHotSwitch(const mixerProfileATRequest_e required_action) +{ +#ifdef USE_PITOT + if (requestTransitionsToFixedWing(required_action) && + currentMixerConfig.switchTransitionAirspeed > 0 && + pitotValidForAirspeed()) { + return getAirspeedEstimate() >= currentMixerConfig.switchTransitionAirspeed; + } +#endif + + // Timer remains a fallback when airspeed is not configured/available. + return millis() > mixerProfileAT.transitionTransEndTime; +} + bool platformTypeConfigured(flyingPlatformType_e platformType) { if (!isModeActivationConditionPresent(BOXMIXERPROFILE)){ @@ -120,6 +141,18 @@ bool platformTypeConfigured(flyingPlatformType_e platformType) return mixerConfigByIndex(nextMixerProfileIndex)->platformType == platformType; } +static bool missionTransitionToMultirotorTypeConfigured(void) +{ + if (!isModeActivationConditionPresent(BOXMIXERPROFILE)) { + return false; + } + + const flyingPlatformType_e nextPlatformType = mixerConfigByIndex(nextMixerProfileIndex)->platformType; + return nextPlatformType == PLATFORM_MULTIROTOR || + nextPlatformType == PLATFORM_TRICOPTER || + nextPlatformType == PLATFORM_HELICOPTER; +} + bool checkMixerATRequired(mixerProfileATRequest_e required_action) { //return false if mixerAT condition is not required or setting is not valid @@ -132,17 +165,22 @@ bool checkMixerATRequired(mixerProfileATRequest_e required_action) return false; } - if(currentMixerConfig.automated_switch){ - if ((required_action == MIXERAT_REQUEST_RTH) && STATE(MULTIROTOR)) - { - return true; - } - if ((required_action == MIXERAT_REQUEST_LAND) && STATE(AIRPLANE)) - { - return true; - } + switch (required_action) { + case MIXERAT_REQUEST_RTH: + return currentMixerConfig.automated_switch && STATE(MULTIROTOR) && platformTypeConfigured(PLATFORM_AIRPLANE); + + case MIXERAT_REQUEST_LAND: + return currentMixerConfig.automated_switch && STATE(AIRPLANE) && missionTransitionToMultirotorTypeConfigured(); + + case MIXERAT_REQUEST_MISSION_TO_FW: + return STATE(MULTIROTOR) && platformTypeConfigured(PLATFORM_AIRPLANE); + + case MIXERAT_REQUEST_MISSION_TO_MC: + return STATE(AIRPLANE) && missionTransitionToMultirotorTypeConfigured(); + + default: + return false; } - return false; } bool mixerATUpdateState(mixerProfileATRequest_e required_action) @@ -173,7 +211,7 @@ bool mixerATUpdateState(mixerProfileATRequest_e required_action) break; case MIXERAT_PHASE_TRANSITIONING: isMixerTransitionMixing_requested = true; - if (millis() > mixerProfileAT.transitionTransEndTime){ + if (mixerATReadyForHotSwitch(required_action)){ isMixerTransitionMixing_requested = false; outputProfileHotSwitch(nextMixerProfileIndex); mixerProfileAT.phase = MIXERAT_PHASE_IDLE; diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index 715732d0685..d44d65e67b8 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -18,6 +18,7 @@ typedef struct mixerConfig_s { bool controlProfileLinking; bool automated_switch; int16_t switchTransitionTimer; + uint16_t switchTransitionAirspeed; bool tailsitterOrientationOffset; int16_t transition_PID_mmix_multiplier_roll; int16_t transition_PID_mmix_multiplier_pitch; @@ -34,6 +35,8 @@ typedef enum { MIXERAT_REQUEST_NONE, //no request, stats checking only MIXERAT_REQUEST_RTH, MIXERAT_REQUEST_LAND, + MIXERAT_REQUEST_MISSION_TO_FW, + MIXERAT_REQUEST_MISSION_TO_MC, MIXERAT_REQUEST_ABORT, } mixerProfileATRequest_e; @@ -81,4 +84,4 @@ bool outputProfileHotSwitch(int profile_index); bool checkMixerProfileHotSwitchAvalibility(void); void activateMixerConfig(void); void mixerConfigInit(void); -void outputProfileUpdateTask(timeUs_t currentTimeUs); \ No newline at end of file +void outputProfileUpdateTask(timeUs_t currentTimeUs); diff --git a/src/main/navigation/navigation.c b/src/main/navigation/navigation.c index 8f60155b68c..bb824f81147 100644 --- a/src/main/navigation/navigation.c +++ b/src/main/navigation/navigation.c @@ -119,7 +119,7 @@ STATIC_ASSERT(NAV_MAX_WAYPOINTS < 254, NAV_MAX_WAYPOINTS_exceeded_allowable_rang PG_REGISTER_ARRAY(navWaypoint_t, NAV_MAX_WAYPOINTS, nonVolatileWaypointList, PG_WAYPOINT_MISSION_STORAGE, 2); #endif -PG_REGISTER_WITH_RESET_TEMPLATE(navConfig_t, navConfig, PG_NAV_CONFIG, 7); +PG_REGISTER_WITH_RESET_TEMPLATE(navConfig_t, navConfig, PG_NAV_CONFIG, 8); PG_RESET_TEMPLATE(navConfig_t, navConfig, .general = { @@ -149,6 +149,9 @@ PG_RESET_TEMPLATE(navConfig_t, navConfig, .pos_failure_timeout = SETTING_NAV_POSITION_TIMEOUT_DEFAULT, // 5 sec .waypoint_radius = SETTING_NAV_WP_RADIUS_DEFAULT, // 2m diameter .waypoint_safe_distance = SETTING_NAV_WP_MAX_SAFE_DISTANCE_DEFAULT, // Metres - first waypoint should be closer than this + .vtol_mission_transition_user_action = SETTING_NAV_VTOL_MISSION_TRANSITION_USER_ACTION_DEFAULT, + .vtol_mission_transition_min_altitude = SETTING_NAV_VTOL_MISSION_TRANSITION_MIN_ALTITUDE_CM_DEFAULT, + .vtol_mission_transition_track_distance = SETTING_NAV_VTOL_MISSION_TRANSITION_TRACK_DISTANCE_CM_DEFAULT, #ifdef USE_MULTI_MISSION .waypoint_multi_mission_index = SETTING_NAV_WP_MULTI_MISSION_INDEX_DEFAULT, // mission index selected from multi mission WP entry #endif @@ -270,6 +273,22 @@ uint16_t navEPV; int16_t navAccNEU[3]; //End of blackbox states +typedef struct navMixerATMissionTransition_s { + mixerProfileATRequest_e request; + int32_t heading; + bool active; +} navMixerATMissionTransition_t; + +typedef enum { + NAV_MISSION_VTOL_TRANSITION_NONE = 0, + NAV_MISSION_VTOL_TRANSITION_CONTINUE, + NAV_MISSION_VTOL_TRANSITION_START, + NAV_MISSION_VTOL_TRANSITION_REJECT, +} navMissionVtolTransitionDisposition_e; + +static navigationFSMState_t navMixerATPendingState = NAV_STATE_IDLE; +static navMixerATMissionTransition_t navMixerATMissionTransition; + static fpVector3_t * rthGetHomeTargetPosition(rthTargetMode_e mode); static void updateDesiredRTHAltitude(void); static void resetAltitudeController(bool useTerrainFollowing); @@ -288,6 +307,7 @@ static void resetJumpCounter(void); static void clearJumpCounters(void); static void calculateAndSetActiveWaypoint(const navWaypoint_t * waypoint); +static bool getLocalPosNextWaypoint(fpVector3_t * nextWpPos); void calculateInitialHoldPosition(fpVector3_t * pos); void calculateFarAwayPos(fpVector3_t * farAwayPos, const fpVector3_t *start, int32_t bearing, int32_t distance); void calculateFarAwayTarget(fpVector3_t * farAwayPos, int32_t bearing, int32_t distance); @@ -295,6 +315,9 @@ bool isWaypointAltitudeReached(void); static void mapWaypointToLocalPosition(fpVector3_t * localPos, const navWaypoint_t * waypoint, geoAltitudeConversionMode_e altConv); static navigationFSMEvent_t nextForNonGeoStates(void); static bool isWaypointMissionValid(void); +static void clearMissionVTOLTransitionState(void); +static navMissionVtolTransitionDisposition_e prepareMissionVTOLTransition(const navWaypoint_t *waypoint); +static void updateMissionTransitionGuidance(void); void missionPlannerSetWaypoint(void); void initializeRTHSanityChecker(void); @@ -1046,6 +1069,7 @@ static const navigationFSMStateDescriptor_t navFSM[NAV_STATE_COUNT] = { [NAV_FSM_EVENT_SWITCH_TO_IDLE] = NAV_STATE_MIXERAT_ABORT, [NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME] = NAV_STATE_RTH_HEAD_HOME, //switch to its pending state [NAV_FSM_EVENT_SWITCH_TO_RTH_LANDING] = NAV_STATE_RTH_LANDING, //switch to its pending state + [NAV_FSM_EVENT_MIXERAT_MISSION_RESUME] = NAV_STATE_WAYPOINT_IN_PROGRESS, } }, [NAV_STATE_MIXERAT_ABORT] = { @@ -1258,7 +1282,17 @@ static const navigationFSMStateDescriptor_t navFSM[NAV_STATE_COUNT] = { static navigationFSMStateFlags_t navGetStateFlags(navigationFSMState_t state) { - return navFSM[state].stateFlags; + navigationFSMStateFlags_t stateFlags = navFSM[state].stateFlags; + + // During mission-authorized MC->FW transition, enable XY/YAW control to fly a straight acceleration segment. + if ((state == NAV_STATE_MIXERAT_INITIALIZE || state == NAV_STATE_MIXERAT_IN_PROGRESS) && + navMixerATPendingState == NAV_STATE_WAYPOINT_PRE_ACTION && + navMixerATMissionTransition.active && + navMixerATMissionTransition.request == MIXERAT_REQUEST_MISSION_TO_FW) { + stateFlags |= NAV_CTL_POS | NAV_CTL_YAW; + } + + return stateFlags; } flightModeFlags_e navGetMappedFlightModes(navigationFSMState_t state) @@ -1282,6 +1316,8 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_IDLE(navigationFSMState { UNUSED(previousState); + navMixerATPendingState = NAV_STATE_IDLE; + clearMissionVTOLTransitionState(); resetAltitudeController(false); resetHeadingController(); resetPositionController(); @@ -1965,6 +2001,110 @@ static navigationFSMEvent_t nextForNonGeoStates(void) } } +static uint16_t missionUserActionMask(const navMissionUserAction_e userAction) +{ + switch (userAction) { + case NAV_MISSION_USER_ACTION_1: + return NAV_WP_USER1; + case NAV_MISSION_USER_ACTION_2: + return NAV_WP_USER2; + case NAV_MISSION_USER_ACTION_3: + return NAV_WP_USER3; + case NAV_MISSION_USER_ACTION_4: + return NAV_WP_USER4; + default: + return 0; + } +} + +static bool isMissionTransitionToMultirotorType(const flyingPlatformType_e platformType) +{ + return platformType == PLATFORM_MULTIROTOR || + platformType == PLATFORM_TRICOPTER || + platformType == PLATFORM_HELICOPTER; +} + +static void clearMissionVTOLTransitionState(void) +{ + navMixerATMissionTransition.active = false; + navMixerATMissionTransition.request = MIXERAT_REQUEST_NONE; + navMixerATMissionTransition.heading = posControl.actualState.yaw; +} + +static navMissionVtolTransitionDisposition_e prepareMissionVTOLTransition(const navWaypoint_t *waypoint) +{ + const navMissionUserAction_e configuredUserAction = (navMissionUserAction_e)navConfig()->general.vtol_mission_transition_user_action; + const uint16_t configuredUserActionMask = missionUserActionMask(configuredUserAction); + + if (!configuredUserActionMask) { + return NAV_MISSION_VTOL_TRANSITION_NONE; + } + + // The configured USER action bit itself is the absolute target selector: + // 0 -> MC target, 1 -> FW target. + const bool transitionToFixedWing = ((((uint16_t)waypoint->p3) & configuredUserActionMask) != 0); + const mixerProfileATRequest_e requestedAction = transitionToFixedWing ? MIXERAT_REQUEST_MISSION_TO_FW : MIXERAT_REQUEST_MISSION_TO_MC; + + // Idempotent mission command: continue immediately if already in requested platform state. + if ((transitionToFixedWing && STATE(AIRPLANE)) || (!transitionToFixedWing && STATE(MULTIROTOR))) { + return NAV_MISSION_VTOL_TRANSITION_CONTINUE; + } + + if (!ARMING_FLAG(ARMED) || + FLIGHT_MODE(FAILSAFE_MODE) || + areSensorsCalibrating() || + posControl.flags.estPosStatus < EST_USABLE || + posControl.flags.estHeadingStatus < EST_USABLE || + !isModeActivationConditionPresent(BOXMIXERPROFILE) || + !checkMixerProfileHotSwitchAvalibility() || + mixerProfileAT.phase != MIXERAT_PHASE_IDLE) { + return NAV_MISSION_VTOL_TRANSITION_REJECT; + } + + const uint16_t transitionMinAltitude = navConfig()->general.vtol_mission_transition_min_altitude; + if (transitionMinAltitude > 0 && navGetCurrentActualPositionAndVelocity()->pos.z < transitionMinAltitude) { + return NAV_MISSION_VTOL_TRANSITION_REJECT; + } + + const flyingPlatformType_e targetPlatformType = mixerConfigByIndex(nextMixerProfileIndex)->platformType; + if ((transitionToFixedWing && targetPlatformType != PLATFORM_AIRPLANE) || + (!transitionToFixedWing && !isMissionTransitionToMultirotorType(targetPlatformType))) { + return NAV_MISSION_VTOL_TRANSITION_REJECT; + } + + if (!checkMixerATRequired(requestedAction)) { + return NAV_MISSION_VTOL_TRANSITION_REJECT; + } + + int32_t transitionHeading = posControl.actualState.yaw; + if (transitionToFixedWing) { + fpVector3_t nextWpPos; + if (getLocalPosNextWaypoint(&nextWpPos)) { + transitionHeading = calculateBearingToDestination(&nextWpPos); + } + } + + navMixerATMissionTransition.request = requestedAction; + navMixerATMissionTransition.heading = wrap_36000(transitionHeading); + navMixerATMissionTransition.active = true; + return NAV_MISSION_VTOL_TRANSITION_START; +} + +static void updateMissionTransitionGuidance(void) +{ + if (navMixerATMissionTransition.active && + navMixerATMissionTransition.request == MIXERAT_REQUEST_MISSION_TO_FW && + STATE(MULTIROTOR)) { + fpVector3_t transitionTarget; + const uint32_t transitionTrackDistance = navConfig()->general.vtol_mission_transition_track_distance; + calculateFarAwayTarget(&transitionTarget, navMixerATMissionTransition.heading, transitionTrackDistance); + setDesiredPosition(&transitionTarget, navMixerATMissionTransition.heading, NAV_POS_UPDATE_XY | NAV_POS_UPDATE_Z | NAV_POS_UPDATE_HEADING); + return; + } + + setDesiredPosition(&navGetCurrentActualPositionAndVelocity()->pos, posControl.actualState.yaw, NAV_POS_UPDATE_Z); +} + static navigationFSMEvent_t navOnEnteringState_NAV_STATE_WAYPOINT_PRE_ACTION(navigationFSMState_t previousState) { /* A helper function to do waypoint-specific action */ @@ -1973,12 +2113,24 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_WAYPOINT_PRE_ACTION(nav switch ((navWaypointActions_e)posControl.waypointList[posControl.activeWaypointIndex].action) { case NAV_WP_ACTION_HOLD_TIME: case NAV_WP_ACTION_WAYPOINT: - case NAV_WP_ACTION_LAND: - calculateAndSetActiveWaypoint(&posControl.waypointList[posControl.activeWaypointIndex]); + case NAV_WP_ACTION_LAND: { + const navWaypoint_t * const activeWaypoint = &posControl.waypointList[posControl.activeWaypointIndex]; + calculateAndSetActiveWaypoint(activeWaypoint); posControl.wpInitialDistance = calculateDistanceToDestination(&posControl.activeWaypoint.pos); posControl.wpInitialAltitude = posControl.actualState.abs.pos.z; posControl.wpAltitudeReached = false; + + clearMissionVTOLTransitionState(); + const navMissionVtolTransitionDisposition_e transitionAction = prepareMissionVTOLTransition(activeWaypoint); + if (transitionAction == NAV_MISSION_VTOL_TRANSITION_START) { + return NAV_FSM_EVENT_SWITCH_TO_MIXERAT; + } + if (transitionAction == NAV_MISSION_VTOL_TRANSITION_REJECT) { + return NAV_FSM_EVENT_ERROR; + } + return NAV_FSM_EVENT_SUCCESS; // will switch to NAV_STATE_WAYPOINT_IN_PROGRESS + } case NAV_WP_ACTION_JUMP: // We use p3 as the volatile jump counter (p2 is the static value) @@ -2284,7 +2436,6 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_LAUNCH_IN_PROGRESS(navi return NAV_FSM_EVENT_NONE; } -navigationFSMState_t navMixerATPendingState = NAV_STATE_IDLE; static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_INITIALIZE(navigationFSMState_t previousState) { const navigationFSMStateFlags_t prevFlags = navGetStateFlags(previousState); @@ -2294,7 +2445,12 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_INITIALIZE(navi resetAltitudeController(false); setupAltitudeController(); } - setDesiredPosition(&navGetCurrentActualPositionAndVelocity()->pos, posControl.actualState.yaw, NAV_POS_UPDATE_Z); + + if (previousState != NAV_STATE_WAYPOINT_PRE_ACTION) { + clearMissionVTOLTransitionState(); + } + + updateMissionTransitionGuidance(); navMixerATPendingState = previousState; return NAV_FSM_EVENT_SUCCESS; } @@ -2311,6 +2467,9 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(nav case NAV_STATE_RTH_LANDING: required_action = MIXERAT_REQUEST_LAND; break; + case NAV_STATE_WAYPOINT_PRE_ACTION: + required_action = navMixerATMissionTransition.active ? navMixerATMissionTransition.request : MIXERAT_REQUEST_NONE; + break; default: required_action = MIXERAT_REQUEST_NONE; break; @@ -2320,6 +2479,8 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(nav resetPositionController(); resetAltitudeController(false); // Make sure surface tracking is not enabled uses global altitude, not AGL mixerATUpdateState(MIXERAT_REQUEST_ABORT); + const bool missionTransitionWasActive = navMixerATMissionTransition.active; + clearMissionVTOLTransitionState(); switch (navMixerATPendingState) { case NAV_STATE_RTH_HEAD_HOME: @@ -2330,13 +2491,17 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(nav setupAltitudeController(); return NAV_FSM_EVENT_SWITCH_TO_RTH_LANDING; break; + case NAV_STATE_WAYPOINT_PRE_ACTION: + setupAltitudeController(); + return missionTransitionWasActive ? NAV_FSM_EVENT_MIXERAT_MISSION_RESUME : NAV_FSM_EVENT_SWITCH_TO_IDLE; + break; default: return NAV_FSM_EVENT_SWITCH_TO_IDLE; break; } } - setDesiredPosition(&navGetCurrentActualPositionAndVelocity()->pos, posControl.actualState.yaw, NAV_POS_UPDATE_Z); + updateMissionTransitionGuidance(); return NAV_FSM_EVENT_NONE; } @@ -2345,6 +2510,7 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_ABORT(navigatio { UNUSED(previousState); mixerATUpdateState(MIXERAT_REQUEST_ABORT); + clearMissionVTOLTransitionState(); return NAV_FSM_EVENT_SUCCESS; } @@ -5062,6 +5228,8 @@ void navigationInit(void) { /* Initial state */ posControl.navState = NAV_STATE_IDLE; + navMixerATPendingState = NAV_STATE_IDLE; + clearMissionVTOLTransitionState(); posControl.flags.horizontalPositionDataNew = false; posControl.flags.verticalPositionDataNew = false; diff --git a/src/main/navigation/navigation.h b/src/main/navigation/navigation.h index 398781fa596..f8c009dcd07 100644 --- a/src/main/navigation/navigation.h +++ b/src/main/navigation/navigation.h @@ -328,6 +328,14 @@ typedef enum { WP_MISSION_SWITCH, } navMissionRestart_e; +typedef enum { + NAV_MISSION_USER_ACTION_OFF = 0, + NAV_MISSION_USER_ACTION_1, + NAV_MISSION_USER_ACTION_2, + NAV_MISSION_USER_ACTION_3, + NAV_MISSION_USER_ACTION_4, +} navMissionUserAction_e; + typedef enum { RTH_TRACKBACK_OFF, RTH_TRACKBACK_ON, @@ -413,6 +421,9 @@ typedef struct navConfig_s { uint8_t pos_failure_timeout; // Time to wait before switching to emergency landing (0 - disable) uint16_t waypoint_radius; // if we are within this distance to a waypoint then we consider it reached (distance is in cm) uint16_t waypoint_safe_distance; // Waypoint mission sanity check distance + uint8_t vtol_mission_transition_user_action; // User action slot that requests mission VTOL transition + uint16_t vtol_mission_transition_min_altitude; // Minimum altitude [cm] to start mission VTOL transition (0 = disabled) + uint32_t vtol_mission_transition_track_distance; // Straight-segment target distance [cm] used during MC->FW mission transition #ifdef USE_MULTI_MISSION uint8_t waypoint_multi_mission_index; // Index of mission to be loaded in multi mission entry #endif diff --git a/src/main/navigation/navigation_private.h b/src/main/navigation/navigation_private.h index a1f07e470c0..947bc3406ea 100644 --- a/src/main/navigation/navigation_private.h +++ b/src/main/navigation/navigation_private.h @@ -183,6 +183,7 @@ typedef enum { NAV_FSM_EVENT_SWITCH_TO_NAV_STATE_RTH_TRACKBACK = NAV_FSM_EVENT_STATE_SPECIFIC_2, NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME = NAV_FSM_EVENT_STATE_SPECIFIC_3, NAV_FSM_EVENT_SWITCH_TO_RTH_LOITER_ABOVE_HOME = NAV_FSM_EVENT_STATE_SPECIFIC_4, + NAV_FSM_EVENT_MIXERAT_MISSION_RESUME = NAV_FSM_EVENT_STATE_SPECIFIC_4, NAV_FSM_EVENT_SWITCH_TO_RTH_LANDING = NAV_FSM_EVENT_STATE_SPECIFIC_5, NAV_FSM_EVENT_COUNT, From 58b9fc7274d77aec9a2b797b3c0ae5ce59361211 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Tue, 12 May 2026 21:09:56 +0300 Subject: [PATCH 02/26] implement VTOL transition automation for smoothing transition mission WP auto transmision available --- docs/MixerProfile.md | 45 ++++- docs/Navigation.md | 6 +- src/main/fc/settings.yaml | 46 +++++ src/main/flight/mixer.c | 24 ++- src/main/flight/mixer_profile.c | 327 ++++++++++++++++++++++++++++--- src/main/flight/mixer_profile.h | 34 ++++ src/main/flight/servos.c | 3 +- src/main/navigation/navigation.c | 8 +- 8 files changed, 455 insertions(+), 38 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 1fdd9132dad..d8688ec5d85 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -22,6 +22,17 @@ Transition input is disabled when navigation mode is activate The use of Transition Mode is recommended to enable further features and future developments like fail-safe support. Mapping motor to servo output, or servo with logic conditions is **not** recommended +`MIXER TRANSITION` now behaves as a transition trigger/request (edge-triggered), not a continuous blend hold: + +- A rising edge starts one transition (MC->FW or FW->MC depending on current profile). +- The transition state machine runs automatically to completion. +- Keeping the mode ON does not repeatedly restart transitions. +- A new transition requires mode OFF then ON again. +- If switched OFF before hot-switch completes, the manual transition request is aborted. + +This edge-triggered behavior is enabled by `manual_vtol_transition_controller`. +When `manual_vtol_transition_controller = OFF`, manual transition keeps legacy behavior. + ## Servo `Mixer Transition` is the input source for transition input; use this to tilt motor to gain airspeed. @@ -72,13 +83,43 @@ This feature is mainly for RTH in a failsafe event. When set properly, model wil Set `mixer_automated_switch` to `ON` in mixer_profile for MC mode. Set `mixer_switch_trans_timer` in mixer_profile for MC mode for the time required to gain airspeed for your model before entering to FW mode. When `mixer_automated_switch`:`OFF` is set for all mixer_profiles(defaults). Model will not perform automated transition at all. +### Unified VTOL transition controller + +Manual `MIXER TRANSITION` and mission-authorized VTOL transition both use the same internal transition controller. +This controller computes transition progress, controls mixer transition scaling, and performs profile hot-switch only inside the authorized transition state. + +### Airspeed-first completion + +When pitot airspeed is healthy and available, transition completion uses pitot thresholds: + +- `vtol_transition_to_fw_min_airspeed_cm_s` for MC->FW +- `vtol_transition_to_mc_max_airspeed_cm_s` for FW->MC + +If pitot is unavailable/unhealthy (or threshold is `0`), timer fallback is used (`mixer_switch_trans_timer`). +Ground speed is not used for transition completion/progress. + +Optional safety timeout: + +- `vtol_transition_airspeed_timeout_ms` can abort transition if airspeed condition is not met in time. + +### Dynamic scaling (optional) + +When `vtol_transition_dynamic_mixer = ON`, transition progress scales: + +- pusher contribution (`-2.0 < throttle < -1.0` motors) from configured max toward 0/100% depending on direction, +- lift motor throttle contribution (`vtol_transition_lift_end_percent`), +- MC stabilization authority (`vtol_transition_mc_authority_end_percent`), +- FW authority start level (`vtol_transition_fw_authority_start_percent`, servo transition input blend). + +Default is OFF to preserve existing behavior. + ### Mission-authorized VTOL transition (waypoint User Action) INAV supports mission-requested VTOL transitions through the existing automated transition path. This is configured with: - `nav_vtol_mission_transition_user_action` (`OFF`, `USER1`, `USER2`, `USER3`, `USER4`) - `nav_vtol_mission_transition_min_altitude_cm` (optional, `0` disables minimum-altitude check) -- `mixer_switch_trans_airspeed_cm_s` (optional, airspeed-based MC->FW switch threshold) +- `mixer_switch_trans_airspeed_cm_s` (legacy MC->FW threshold fallback/compatibility) On each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`), the configured USER action bit is used as absolute target selector: @@ -90,7 +131,7 @@ On each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`), the con The mission pauses while transition is in progress and resumes after completion. For MC -> FW mission transitions, navigation uses a straight acceleration segment (no loiter) to build speed before hot-switch. -When `mixer_switch_trans_airspeed_cm_s > 0` and valid pitot airspeed is available, automated MC->FW switching uses this airspeed target. If airspeed is unavailable, transition falls back to `mixer_switch_trans_timer`. +Mission path uses the same controller and completion logic as manual transition (airspeed-first, timer fallback). Manual RC switching (`MIXER PROFILE 2`, `MIXER TRANSITION`) remains blocked during normal active navigation. Mission VTOL transition does not bypass the hot-switch safety guard; it only authorizes switching inside the automated transition state. diff --git a/docs/Navigation.md b/docs/Navigation.md index c716925ba77..02000d0e487 100755 --- a/docs/Navigation.md +++ b/docs/Navigation.md @@ -111,7 +111,7 @@ Configuration: - `nav_vtol_mission_transition_user_action` selects which waypoint User Action (`USER1..USER4`) is used as the mission VTOL target selector. - `nav_vtol_mission_transition_min_altitude_cm` optionally enforces a minimum altitude before transition start (`0` disables check). - `nav_vtol_mission_transition_track_distance_cm` configures straight-line MC->FW transition guidance distance. -- `mixer_switch_trans_airspeed_cm_s` configures MC->FW airspeed threshold for automated profile switching. +- VTOL transition completion logic is shared with manual MIXER TRANSITION and uses the mixer transition settings. Behavior on each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`): @@ -125,7 +125,9 @@ Behavior on each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`) Transition behavior in this MVP: - MC -> FW: straight-line acceleration segment (no loiter), heading from the next waypoint bearing when available, otherwise current heading. -- MC -> FW switch point: if valid pitot airspeed is available and `mixer_switch_trans_airspeed_cm_s > 0`, switch to FW occurs at/above the configured airspeed. If airspeed is unavailable, timer-based fallback (`mixer_switch_trans_timer`) is used. +- MC -> FW and FW -> MC completion uses pitot airspeed thresholds when healthy/available (`vtol_transition_to_fw_min_airspeed_cm_s`, `vtol_transition_to_mc_max_airspeed_cm_s`). +- If pitot is unavailable/unhealthy (or threshold disabled), timer fallback (`mixer_switch_trans_timer`) is used. +- Ground speed is not used for transition progress/completion. - FW -> MC: mission pauses during automated transition, then resumes after switching back to MC profile. - Strict altitude hold is not enforced during MC -> FW transition; natural climb is allowed. diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 44e34452fee..68b058e8d37 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1286,6 +1286,52 @@ groups: field: mixer_config.switchTransitionAirspeed min: 0 max: 10000 + - name: vtol_transition_dynamic_mixer + description: "Enables dynamic VTOL transition progress/scaling controller shared by mission-authorized and manual MIXER TRANSITION paths." + default_value: OFF + field: mixer_config.vtolTransitionDynamicMixer + type: bool + - name: manual_vtol_transition_controller + description: "Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior." + default_value: OFF + field: mixer_config.manualVtolTransitionController + type: bool + - name: vtol_transition_to_fw_min_airspeed_cm_s + description: "Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. If 0, `mixer_switch_trans_airspeed_cm_s` is used for MC->FW." + default_value: 0 + field: mixer_config.vtolTransitionToFwMinAirspeed + min: 0 + max: 20000 + - name: vtol_transition_to_mc_max_airspeed_cm_s + description: "Maximum pitot airspeed [cm/s] allowed to complete FW->MC transition when airspeed is healthy and available. If 0, FW->MC uses timer fallback." + default_value: 0 + field: mixer_config.vtolTransitionToMcMaxAirspeed + min: 0 + max: 20000 + - name: vtol_transition_airspeed_timeout_ms + description: "Safety timeout [ms] for airspeed-controlled transitions. If non-zero and required airspeed condition is not met in time, transition aborts instead of force-completing." + default_value: 0 + field: mixer_config.vtolTransitionAirspeedTimeoutMs + min: 0 + max: 60000 + - name: vtol_transition_lift_end_percent + description: "Target vertical-lift throttle scale at transition end, in percent. Used only when `vtol_transition_dynamic_mixer` is ON." + default_value: 100 + field: mixer_config.vtolTransitionLiftEndPercent + min: 0 + max: 100 + - name: vtol_transition_mc_authority_end_percent + description: "Target multicopter stabilization authority scale at transition end, in percent. Used only when `vtol_transition_dynamic_mixer` is ON." + default_value: 100 + field: mixer_config.vtolTransitionMcAuthorityEndPercent + min: 0 + max: 100 + - name: vtol_transition_fw_authority_start_percent + description: "Initial fixed-wing authority scale at transition start, in percent. Used only when `vtol_transition_dynamic_mixer` is ON." + default_value: 100 + field: mixer_config.vtolTransitionFwAuthorityStartPercent + min: 0 + max: 100 - name: tailsitter_orientation_offset description: "Apply a 90 deg pitch offset in sensor aliment for tailsitter flying mode" default_value: OFF diff --git a/src/main/flight/mixer.c b/src/main/flight/mixer.c index a80992b772d..030d56cc0ca 100644 --- a/src/main/flight/mixer.c +++ b/src/main/flight/mixer.c @@ -48,6 +48,7 @@ #include "flight/failsafe.h" #include "flight/imu.h" #include "flight/mixer.h" +#include "flight/mixer_profile.h" #include "flight/pid.h" #include "flight/servos.h" @@ -520,10 +521,11 @@ void FAST_CODE mixTable(void) input[ROLL] = axisPID[ROLL]; input[PITCH] = axisPID[PITCH]; input[YAW] = axisPID[YAW]; - if(isMixerTransitionMixing){ - input[ROLL] = input[ROLL] * (currentMixerConfig.transition_PID_mmix_multiplier_roll / 1000.0f); - input[PITCH] = input[PITCH] * (currentMixerConfig.transition_PID_mmix_multiplier_pitch / 1000.0f); - input[YAW] = input[YAW] * (currentMixerConfig.transition_PID_mmix_multiplier_yaw / 1000.0f); + if (isMixerTransitionMixing) { + const float mcAuthorityScale = mixerATGetMcAuthorityScale(); + input[ROLL] = input[ROLL] * (currentMixerConfig.transition_PID_mmix_multiplier_roll / 1000.0f) * mcAuthorityScale; + input[PITCH] = input[PITCH] * (currentMixerConfig.transition_PID_mmix_multiplier_pitch / 1000.0f) * mcAuthorityScale; + input[YAW] = input[YAW] * (currentMixerConfig.transition_PID_mmix_multiplier_yaw / 1000.0f) * mcAuthorityScale; } } @@ -624,8 +626,16 @@ void FAST_CODE mixTable(void) // Now add in the desired throttle, but keep in a range that doesn't clip adjusted // roll/pitch/yaw. This could move throttle down, but also up for those low throttle flips. + const float liftScale = isMixerTransitionMixing ? mixerATGetLiftScale() : 1.0f; + const float pusherScale = isMixerTransitionMixing ? mixerATGetPusherScale() : 1.0f; + for (int i = 0; i < motorCount; i++) { - motor[i] = rpyMix[i] + constrain(mixerThrottleCommand * currentMixer[i].throttle, throttleMin, throttleMax); + float motorThrottle = mixerThrottleCommand * currentMixer[i].throttle; + if (currentMixer[i].throttle > 0.0f) { + motorThrottle *= liftScale; + } + + motor[i] = rpyMix[i] + constrain(motorThrottle, throttleMin, throttleMax); if (failsafeIsActive()) { motor[i] = constrain(motor[i], motorConfig()->mincommand, getMaxThrottle()); @@ -639,7 +649,7 @@ void FAST_CODE mixTable(void) } //spin stopped motors only in mixer transition mode if (isMixerTransitionMixing && currentMixer[i].throttle <= -1.05f && currentMixer[i].throttle >= -2.0f && !feature(FEATURE_REVERSIBLE_MOTORS)) { - motor[i] = -currentMixer[i].throttle * 1000; + motor[i] = -currentMixer[i].throttle * 1000 * pusherScale; motor[i] = constrain(motor[i], throttleRangeMin, throttleRangeMax); } } @@ -737,4 +747,4 @@ uint16_t getMaxThrottle(void) { } return throttle; -} \ No newline at end of file +} diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 4362f418476..208b0664d68 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -10,6 +10,7 @@ #include "drivers/pwm_output.h" #include "drivers/pwm_mapping.h" #include "drivers/time.h" +#include "common/maths.h" #include "flight/mixer.h" #include "common/axis.h" #include "flight/pid.h" @@ -18,6 +19,7 @@ #include "navigation/navigation.h" #include "navigation/navigation_private.h" #include "sensors/pitotmeter.h" +#include "sensors/sensors.h" #include "fc/fc_core.h" #include "fc/config.h" @@ -37,6 +39,8 @@ bool isMixerTransitionMixing; bool isMixerTransitionMixing_requested; mixerProfileAT_t mixerProfileAT; int nextMixerProfileIndex; +static bool manualTransitionModeWasActive; +static bool manualTransitionReadyForEdge = true; PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 2); @@ -55,6 +59,14 @@ void pgResetFn_mixerProfiles(mixerProfile_t *instance) .automated_switch = SETTING_MIXER_AUTOMATED_SWITCH_DEFAULT, .switchTransitionTimer = SETTING_MIXER_SWITCH_TRANS_TIMER_DEFAULT, .switchTransitionAirspeed = SETTING_MIXER_SWITCH_TRANS_AIRSPEED_CM_S_DEFAULT, + .vtolTransitionDynamicMixer = SETTING_VTOL_TRANSITION_DYNAMIC_MIXER_DEFAULT, + .manualVtolTransitionController = SETTING_MANUAL_VTOL_TRANSITION_CONTROLLER_DEFAULT, + .vtolTransitionToFwMinAirspeed = SETTING_VTOL_TRANSITION_TO_FW_MIN_AIRSPEED_CM_S_DEFAULT, + .vtolTransitionToMcMaxAirspeed = SETTING_VTOL_TRANSITION_TO_MC_MAX_AIRSPEED_CM_S_DEFAULT, + .vtolTransitionAirspeedTimeoutMs = SETTING_VTOL_TRANSITION_AIRSPEED_TIMEOUT_MS_DEFAULT, + .vtolTransitionLiftEndPercent = SETTING_VTOL_TRANSITION_LIFT_END_PERCENT_DEFAULT, + .vtolTransitionMcAuthorityEndPercent = SETTING_VTOL_TRANSITION_MC_AUTHORITY_END_PERCENT_DEFAULT, + .vtolTransitionFwAuthorityStartPercent = SETTING_VTOL_TRANSITION_FW_AUTHORITY_START_PERCENT_DEFAULT, .tailsitterOrientationOffset = SETTING_TAILSITTER_ORIENTATION_OFFSET_DEFAULT, .transition_PID_mmix_multiplier_roll = SETTING_TRANSITION_PID_MMIX_MULTIPLIER_ROLL_DEFAULT, .transition_PID_mmix_multiplier_pitch = SETTING_TRANSITION_PID_MMIX_MULTIPLIER_PITCH_DEFAULT, @@ -110,27 +122,171 @@ void mixerConfigInit(void) void setMixerProfileAT(void) { - mixerProfileAT.transitionStartTime = millis(); - mixerProfileAT.transitionTransEndTime = mixerProfileAT.transitionStartTime + (timeMs_t)currentMixerConfig.switchTransitionTimer * 100; + const timeMs_t now = millis(); + const uint32_t transitionDurationMs = MAX(0, currentMixerConfig.switchTransitionTimer) * 100; + + mixerProfileAT.transitionStartTime = now; + mixerProfileAT.transitionStabEndTime = now; + mixerProfileAT.transitionTransEndTime = now + transitionDurationMs; + mixerProfileAT.aborted = false; + mixerProfileAT.hotSwitchDone = false; + mixerProfileAT.usedAirspeed = false; + mixerProfileAT.progress = 0.0f; + mixerProfileAT.blendToFw = mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW ? 0.0f : 1.0f; + mixerProfileAT.pusherScale = 1.0f; + mixerProfileAT.liftScale = 1.0f; + mixerProfileAT.mcAuthorityScale = 1.0f; + mixerProfileAT.fwAuthorityScale = 1.0f; } static bool requestTransitionsToFixedWing(const mixerProfileATRequest_e required_action) { - return required_action == MIXERAT_REQUEST_RTH || required_action == MIXERAT_REQUEST_MISSION_TO_FW; + return required_action == MIXERAT_REQUEST_RTH || + required_action == MIXERAT_REQUEST_MISSION_TO_FW || + required_action == MIXERAT_REQUEST_MANUAL_TO_FW; } -static bool mixerATReadyForHotSwitch(const mixerProfileATRequest_e required_action) +static mixerProfileATDirection_e directionForRequest(const mixerProfileATRequest_e required_action) +{ + if (requestTransitionsToFixedWing(required_action)) { + return MIXERAT_DIRECTION_TO_FW; + } + + if (required_action == MIXERAT_REQUEST_LAND || + required_action == MIXERAT_REQUEST_MISSION_TO_MC || + required_action == MIXERAT_REQUEST_MANUAL_TO_MC) { + return MIXERAT_DIRECTION_TO_MC; + } + + return MIXERAT_DIRECTION_NONE; +} + +static void resetTransitionScales(void) +{ + mixerProfileAT.progress = 0.0f; + mixerProfileAT.blendToFw = 0.0f; + mixerProfileAT.pusherScale = 0.0f; + mixerProfileAT.liftScale = 1.0f; + mixerProfileAT.mcAuthorityScale = 1.0f; + mixerProfileAT.fwAuthorityScale = 1.0f; +} + +static float blendScale(float from, float to, float progress) +{ + return from + (to - from) * constrainf(progress, 0.0f, 1.0f); +} + +static bool hasTrustedPitotAirspeed(float *airspeedCmS) { #ifdef USE_PITOT - if (requestTransitionsToFixedWing(required_action) && - currentMixerConfig.switchTransitionAirspeed > 0 && - pitotValidForAirspeed()) { - return getAirspeedEstimate() >= currentMixerConfig.switchTransitionAirspeed; + if (!sensors(SENSOR_PITOT) || !pitotValidForAirspeed() || pitotHasFailed()) { + return false; } + + if (detectedSensors[SENSOR_INDEX_PITOT] == PITOT_NONE || + detectedSensors[SENSOR_INDEX_PITOT] == PITOT_VIRTUAL) { + return false; + } + + *airspeedCmS = pitot.airSpeed; + return true; +#else + UNUSED(airspeedCmS); + return false; #endif +} + +static uint16_t getAirspeedThresholdForDirection(const mixerProfileATDirection_e direction) +{ + if (direction == MIXERAT_DIRECTION_TO_FW) { + if (currentMixerConfig.vtolTransitionToFwMinAirspeed > 0) { + return currentMixerConfig.vtolTransitionToFwMinAirspeed; + } + return currentMixerConfig.switchTransitionAirspeed; + } - // Timer remains a fallback when airspeed is not configured/available. - return millis() > mixerProfileAT.transitionTransEndTime; + if (direction == MIXERAT_DIRECTION_TO_MC) { + return currentMixerConfig.vtolTransitionToMcMaxAirspeed; + } + + return 0; +} + +static void updateTransitionScales(void) +{ + if (!currentMixerConfig.vtolTransitionDynamicMixer) { + mixerProfileAT.blendToFw = 1.0f; + mixerProfileAT.pusherScale = 1.0f; + mixerProfileAT.liftScale = 1.0f; + mixerProfileAT.mcAuthorityScale = 1.0f; + mixerProfileAT.fwAuthorityScale = 1.0f; + return; + } + + const float liftFloor = constrainf(currentMixerConfig.vtolTransitionLiftEndPercent / 100.0f, 0.0f, 1.0f); + const float mcFloor = constrainf(currentMixerConfig.vtolTransitionMcAuthorityEndPercent / 100.0f, 0.0f, 1.0f); + const float fwFloor = constrainf(currentMixerConfig.vtolTransitionFwAuthorityStartPercent / 100.0f, 0.0f, 1.0f); + + if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW) { + mixerProfileAT.pusherScale = blendScale(0.0f, 1.0f, mixerProfileAT.progress); + mixerProfileAT.liftScale = blendScale(1.0f, liftFloor, mixerProfileAT.progress); + mixerProfileAT.mcAuthorityScale = blendScale(1.0f, mcFloor, mixerProfileAT.progress); + mixerProfileAT.fwAuthorityScale = blendScale(fwFloor, 1.0f, mixerProfileAT.progress); + } else if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_MC) { + mixerProfileAT.pusherScale = blendScale(1.0f, 0.0f, mixerProfileAT.progress); + mixerProfileAT.liftScale = blendScale(liftFloor, 1.0f, mixerProfileAT.progress); + mixerProfileAT.mcAuthorityScale = blendScale(mcFloor, 1.0f, mixerProfileAT.progress); + mixerProfileAT.fwAuthorityScale = blendScale(1.0f, fwFloor, mixerProfileAT.progress); + } + + mixerProfileAT.blendToFw = constrainf(mixerProfileAT.fwAuthorityScale, 0.0f, 1.0f); +} + +static void abortTransition(void) +{ + const bool wasActive = mixerProfileAT.phase != MIXERAT_PHASE_IDLE; + isMixerTransitionMixing_requested = false; + mixerProfileAT.phase = MIXERAT_PHASE_IDLE; + mixerProfileAT.aborted = wasActive; + mixerProfileAT.hotSwitchDone = false; + mixerProfileAT.request = MIXERAT_REQUEST_NONE; + mixerProfileAT.direction = MIXERAT_DIRECTION_NONE; + resetTransitionScales(); +} + +static bool mixerATReadyForHotSwitch(const mixerProfileATRequest_e required_action) +{ + const mixerProfileATDirection_e direction = directionForRequest(required_action); + const uint16_t airspeedThresholdCmS = getAirspeedThresholdForDirection(direction); + const uint32_t elapsedMs = millis() - mixerProfileAT.transitionStartTime; + const uint32_t transitionTimerMs = MAX(0, currentMixerConfig.switchTransitionTimer) * 100; + float airspeedCmS = 0.0f; + + if (direction == MIXERAT_DIRECTION_NONE) { + mixerProfileAT.progress = 0.0f; + mixerProfileAT.usedAirspeed = false; + return false; + } + + if (airspeedThresholdCmS > 0 && hasTrustedPitotAirspeed(&airspeedCmS)) { + mixerProfileAT.usedAirspeed = true; + if (direction == MIXERAT_DIRECTION_TO_FW) { + mixerProfileAT.progress = constrainf(airspeedCmS / airspeedThresholdCmS, 0.0f, 1.0f); + return airspeedCmS >= airspeedThresholdCmS; + } + + mixerProfileAT.progress = constrainf((airspeedThresholdCmS - airspeedCmS) / airspeedThresholdCmS, 0.0f, 1.0f); + return airspeedCmS <= airspeedThresholdCmS; + } + + mixerProfileAT.usedAirspeed = false; + if (transitionTimerMs > 0) { + mixerProfileAT.progress = constrainf((float)elapsedMs / (float)transitionTimerMs, 0.0f, 1.0f); + } else { + mixerProfileAT.progress = 1.0f; + } + + return elapsedMs >= transitionTimerMs; } bool platformTypeConfigured(flyingPlatformType_e platformType) @@ -178,6 +334,12 @@ bool checkMixerATRequired(mixerProfileATRequest_e required_action) case MIXERAT_REQUEST_MISSION_TO_MC: return STATE(AIRPLANE) && missionTransitionToMultirotorTypeConfigured(); + case MIXERAT_REQUEST_MANUAL_TO_FW: + return STATE(MULTIROTOR) && platformTypeConfigured(PLATFORM_AIRPLANE); + + case MIXERAT_REQUEST_MANUAL_TO_MC: + return STATE(AIRPLANE) && missionTransitionToMultirotorTypeConfigured(); + default: return false; } @@ -190,34 +352,57 @@ bool mixerATUpdateState(mixerProfileATRequest_e required_action) do { reprocessState=false; - if (required_action==MIXERAT_REQUEST_ABORT){ - isMixerTransitionMixing_requested = false; - mixerProfileAT.phase = MIXERAT_PHASE_IDLE; + if (required_action == MIXERAT_REQUEST_ABORT) { + abortTransition(); return true; } - switch (mixerProfileAT.phase){ + switch (mixerProfileAT.phase) { case MIXERAT_PHASE_IDLE: //check if mixerAT is required - if (checkMixerATRequired(required_action)){ - mixerProfileAT.phase=MIXERAT_PHASE_TRANSITION_INITIALIZE; + if (checkMixerATRequired(required_action)) { + mixerProfileAT.request = required_action; + mixerProfileAT.direction = directionForRequest(required_action); + mixerProfileAT.phase = MIXERAT_PHASE_TRANSITION_INITIALIZE; reprocessState = true; + } else { + resetTransitionScales(); } break; case MIXERAT_PHASE_TRANSITION_INITIALIZE: - // LOG_INFO(PWM, "MIXERAT_PHASE_IDLE"); + mixerProfileAT.request = required_action; + mixerProfileAT.direction = directionForRequest(required_action); setMixerProfileAT(); mixerProfileAT.phase = MIXERAT_PHASE_TRANSITIONING; reprocessState = true; break; case MIXERAT_PHASE_TRANSITIONING: isMixerTransitionMixing_requested = true; - if (mixerATReadyForHotSwitch(required_action)){ + if (required_action != MIXERAT_REQUEST_NONE && required_action != mixerProfileAT.request) { + abortTransition(); + return true; + } + + if (mixerATReadyForHotSwitch(mixerProfileAT.request)) { isMixerTransitionMixing_requested = false; - outputProfileHotSwitch(nextMixerProfileIndex); + if (!outputProfileHotSwitch(nextMixerProfileIndex)) { + abortTransition(); + return true; + } + mixerProfileAT.hotSwitchDone = true; + mixerProfileAT.progress = 1.0f; + updateTransitionScales(); mixerProfileAT.phase = MIXERAT_PHASE_IDLE; + mixerProfileAT.request = MIXERAT_REQUEST_NONE; + mixerProfileAT.direction = MIXERAT_DIRECTION_NONE; reprocessState = true; - //transition is done + } else if (mixerProfileAT.usedAirspeed && + currentMixerConfig.vtolTransitionAirspeedTimeoutMs > 0 && + (millis() - mixerProfileAT.transitionStartTime) >= currentMixerConfig.vtolTransitionAirspeedTimeoutMs) { + abortTransition(); + return true; } + + updateTransitionScales(); return false; break; default: @@ -240,17 +425,111 @@ bool checkMixerProfileHotSwitchAvalibility(void) void outputProfileUpdateTask(timeUs_t currentTimeUs) { UNUSED(currentTimeUs); - if(cliMode) return; - bool mixerAT_inuse = mixerProfileAT.phase != MIXERAT_PHASE_IDLE; + if (cliMode) { + return; + } + + bool mixerAT_inuse = mixerATIsActive(); + const bool transitionModeActive = IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION); + const bool transitionModeRisingEdge = transitionModeActive && !manualTransitionModeWasActive; + const bool manualTransitionAllowed = (posControl.navState == NAV_STATE_IDLE) || + (posControl.navState == NAV_STATE_ALTHOLD_IN_PROGRESS); + const bool missionActive = (navGetCurrentStateFlags() & NAV_AUTO_WP) != 0; + const bool manualControllerEnabled = currentMixerConfig.manualVtolTransitionController && !missionActive; + + if (mixerAT_inuse && (!ARMING_FLAG(ARMED) || FLIGHT_MODE(FAILSAFE_MODE) || areSensorsCalibrating())) { + abortTransition(); + mixerAT_inuse = false; + } + // transition mode input for servo mix and motor mix if (!FLIGHT_MODE(FAILSAFE_MODE) && (!mixerAT_inuse)) { if (isModeActivationConditionPresent(BOXMIXERPROFILE)){ outputProfileHotSwitch(IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) == 0 ? 0 : 1); } - isMixerTransitionMixing_requested = IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION); } - isMixerTransitionMixing = isMixerTransitionMixing_requested && ((posControl.navState == NAV_STATE_IDLE) || mixerAT_inuse ||(posControl.navState == NAV_STATE_ALTHOLD_IN_PROGRESS)); + + if (!manualControllerEnabled) { + // Backward-compatible manual path: level-controlled transition mixing request. + if (!FLIGHT_MODE(FAILSAFE_MODE) && (!mixerAT_inuse)) { + isMixerTransitionMixing_requested = transitionModeActive; + } + manualTransitionReadyForEdge = true; + } else { + if (!transitionModeActive) { + manualTransitionReadyForEdge = true; + if (!mixerAT_inuse) { + isMixerTransitionMixing_requested = false; + } + } else if (transitionModeRisingEdge && manualTransitionReadyForEdge && manualTransitionAllowed && !mixerAT_inuse) { + manualTransitionReadyForEdge = false; + if (STATE(MULTIROTOR)) { + mixerATUpdateState(MIXERAT_REQUEST_MANUAL_TO_FW); + } else if (STATE(AIRPLANE)) { + mixerATUpdateState(MIXERAT_REQUEST_MANUAL_TO_MC); + } + mixerAT_inuse = mixerATIsActive(); + } + + if (!transitionModeActive && + mixerAT_inuse && + !mixerProfileAT.hotSwitchDone && + (mixerProfileAT.request == MIXERAT_REQUEST_MANUAL_TO_FW || mixerProfileAT.request == MIXERAT_REQUEST_MANUAL_TO_MC)) { + abortTransition(); + mixerAT_inuse = false; + } + + if (mixerAT_inuse && + (mixerProfileAT.request == MIXERAT_REQUEST_MANUAL_TO_FW || mixerProfileAT.request == MIXERAT_REQUEST_MANUAL_TO_MC)) { + mixerATUpdateState(mixerProfileAT.request); + mixerAT_inuse = mixerATIsActive(); + } + } + + manualTransitionModeWasActive = transitionModeActive; + + isMixerTransitionMixing = isMixerTransitionMixing_requested && + ((posControl.navState == NAV_STATE_IDLE) || mixerAT_inuse || (posControl.navState == NAV_STATE_ALTHOLD_IN_PROGRESS)); + + if (!isMixerTransitionMixing) { + resetTransitionScales(); + } +} + +bool mixerATIsActive(void) +{ + return mixerProfileAT.phase != MIXERAT_PHASE_IDLE; +} + +bool mixerATWasAborted(void) +{ + return mixerProfileAT.aborted; +} + +float mixerATGetPusherScale(void) +{ + return constrainf(mixerProfileAT.pusherScale, 0.0f, 1.0f); +} + +float mixerATGetLiftScale(void) +{ + return constrainf(mixerProfileAT.liftScale, 0.0f, 1.0f); +} + +float mixerATGetMcAuthorityScale(void) +{ + return constrainf(mixerProfileAT.mcAuthorityScale, 0.0f, 1.0f); +} + +float mixerATGetFwAuthorityScale(void) +{ + return constrainf(mixerProfileAT.fwAuthorityScale, 0.0f, 1.0f); +} + +float mixerATGetBlendToFw(void) +{ + return constrainf(mixerProfileAT.blendToFw, 0.0f, 1.0f); } // switch mixerprofile without reboot diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index d44d65e67b8..1b3ae5f6f86 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -19,6 +19,14 @@ typedef struct mixerConfig_s { bool automated_switch; int16_t switchTransitionTimer; uint16_t switchTransitionAirspeed; + bool vtolTransitionDynamicMixer; + bool manualVtolTransitionController; + uint16_t vtolTransitionToFwMinAirspeed; + uint16_t vtolTransitionToMcMaxAirspeed; + uint16_t vtolTransitionAirspeedTimeoutMs; + uint8_t vtolTransitionLiftEndPercent; + uint8_t vtolTransitionMcAuthorityEndPercent; + uint8_t vtolTransitionFwAuthorityStartPercent; bool tailsitterOrientationOffset; int16_t transition_PID_mmix_multiplier_roll; int16_t transition_PID_mmix_multiplier_pitch; @@ -37,9 +45,17 @@ typedef enum { MIXERAT_REQUEST_LAND, MIXERAT_REQUEST_MISSION_TO_FW, MIXERAT_REQUEST_MISSION_TO_MC, + MIXERAT_REQUEST_MANUAL_TO_FW, + MIXERAT_REQUEST_MANUAL_TO_MC, MIXERAT_REQUEST_ABORT, } mixerProfileATRequest_e; +typedef enum { + MIXERAT_DIRECTION_NONE = 0, + MIXERAT_DIRECTION_TO_FW, + MIXERAT_DIRECTION_TO_MC, +} mixerProfileATDirection_e; + //mixerProfile Automated Transition PHASE typedef enum { MIXERAT_PHASE_IDLE, @@ -50,7 +66,18 @@ typedef enum { typedef struct mixerProfileAT_s { mixerProfileATState_e phase; + mixerProfileATDirection_e direction; + mixerProfileATRequest_e request; bool transitionInputMixing; + bool aborted; + bool hotSwitchDone; + bool usedAirspeed; + float progress; + float blendToFw; + float pusherScale; + float liftScale; + float mcAuthorityScale; + float fwAuthorityScale; timeMs_t transitionStartTime; timeMs_t transitionStabEndTime; timeMs_t transitionTransEndTime; @@ -58,6 +85,13 @@ typedef struct mixerProfileAT_s { extern mixerProfileAT_t mixerProfileAT; bool checkMixerATRequired(mixerProfileATRequest_e required_action); bool mixerATUpdateState(mixerProfileATRequest_e required_action); +bool mixerATIsActive(void); +bool mixerATWasAborted(void); +float mixerATGetPusherScale(void); +float mixerATGetLiftScale(void); +float mixerATGetMcAuthorityScale(void); +float mixerATGetFwAuthorityScale(void); +float mixerATGetBlendToFw(void); extern mixerConfig_t currentMixerConfig; extern int currentMixerProfileIndex; diff --git a/src/main/flight/servos.c b/src/main/flight/servos.c index 9f6eb4851a7..8c9276a1fbc 100755 --- a/src/main/flight/servos.c +++ b/src/main/flight/servos.c @@ -51,6 +51,7 @@ #include "flight/imu.h" #include "flight/mixer.h" +#include "flight/mixer_profile.h" #include "flight/pid.h" #include "flight/servos.h" @@ -353,7 +354,7 @@ void servoMixer(float dT) input[INPUT_STABILIZED_THROTTLE] = mixerThrottleCommand - 1000 - 500; // Since it derives from rcCommand or mincommand and must be [-500:+500] - input[INPUT_MIXER_TRANSITION] = isMixerTransitionMixing * 500; //fixed value + input[INPUT_MIXER_TRANSITION] = isMixerTransitionMixing ? lrintf(mixerATGetBlendToFw() * 500.0f) : 0; input[INPUT_MIXER_SWITCH_HELPER] = 0; // no input, used to apply speed limit filter from previous servo rules // center the RC input value around the RC middle value diff --git a/src/main/navigation/navigation.c b/src/main/navigation/navigation.c index bb824f81147..c78f7d8964d 100644 --- a/src/main/navigation/navigation.c +++ b/src/main/navigation/navigation.c @@ -2057,7 +2057,7 @@ static navMissionVtolTransitionDisposition_e prepareMissionVTOLTransition(const posControl.flags.estHeadingStatus < EST_USABLE || !isModeActivationConditionPresent(BOXMIXERPROFILE) || !checkMixerProfileHotSwitchAvalibility() || - mixerProfileAT.phase != MIXERAT_PHASE_IDLE) { + mixerATIsActive()) { return NAV_MISSION_VTOL_TRANSITION_REJECT; } @@ -2476,6 +2476,7 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(nav } if (mixerATUpdateState(required_action)){ // MixerAT is done, switch to next state + const bool transitionAborted = mixerATWasAborted(); resetPositionController(); resetAltitudeController(false); // Make sure surface tracking is not enabled uses global altitude, not AGL mixerATUpdateState(MIXERAT_REQUEST_ABORT); @@ -2493,7 +2494,10 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(nav break; case NAV_STATE_WAYPOINT_PRE_ACTION: setupAltitudeController(); - return missionTransitionWasActive ? NAV_FSM_EVENT_MIXERAT_MISSION_RESUME : NAV_FSM_EVENT_SWITCH_TO_IDLE; + if (missionTransitionWasActive) { + return transitionAborted ? NAV_FSM_EVENT_SWITCH_TO_IDLE : NAV_FSM_EVENT_MIXERAT_MISSION_RESUME; + } + return NAV_FSM_EVENT_SWITCH_TO_IDLE; break; default: return NAV_FSM_EVENT_SWITCH_TO_IDLE; From ae15cb97043d80f0892b2742003bf8ae62affd04 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Tue, 12 May 2026 23:01:09 +0300 Subject: [PATCH 03/26] feat(vtol): add unified manual/mission transition controller with airspeed-first logic and dynamic scaling - Introduce a common VTOL transition controller path used by: - manual MIXER TRANSITION (edge-triggered mode, optional via manual_vtol_transition_controller) - mission-authorized VTOL transition via nav_vtol_mission_transition_user_action - Keep profile hot-switch safety boundaries intact: - no broad manual mixer switching in active waypoint navigation - switching remains authorized only through transition state handling - Add airspeed-first completion behavior: - MC->FW threshold via vtol_transition_to_fw_min_airspeed_cm_s - FW->MC threshold via vtol_transition_to_mc_max_airspeed_cm_s - timer fallback only when pitot is unavailable/unhealthy - timeout/abort support via vtol_transition_airspeed_timeout_ms - Add optional dynamic mixer scaling (vtol_transition_dynamic_mixer): - pusher contribution ramping - lift throttle scaling (vtol_transition_lift_end_percent) - MC authority scaling (vtol_transition_mc_authority_end_percent) - FW authority blend scaling (vtol_transition_fw_authority_start_percent) - Fix transition scaling/progress details: - pusher ramp uses idle-to-target interpolation - FW->MC progress uses captured transition start airspeed for smooth deceleration-based ramp - Improve transition abort/reset robustness: - clear transition/nav mission transition state on disarm/failsafe/abort paths - avoid blind mission resume after half-complete transition - Add mission VTOL settings and behavior: - nav_vtol_mission_transition_user_action - nav_vtol_mission_transition_min_altitude_cm - nav_vtol_mission_transition_track_distance_cm - mission pause/resume around transition, straight-line MC->FW transition segment - Update documentation: - MixerProfile.md, Navigation.md, VTOL.md - document unified controller, manual semantics, mission semantics, airspeed precedence, dynamic scaling, and CLI usage --- docs/MixerProfile.md | 21 ++++- docs/Navigation.md | 6 +- docs/VTOL.md | 147 ++++++++++++++++++++++++++++++- src/main/fc/settings.yaml | 4 +- src/main/flight/mixer.c | 4 +- src/main/flight/mixer_profile.c | 31 ++++++- src/main/flight/mixer_profile.h | 2 + src/main/navigation/navigation.c | 7 ++ 8 files changed, 214 insertions(+), 8 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index d8688ec5d85..64f9bc7a3c1 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -86,7 +86,8 @@ When `mixer_automated_switch`:`OFF` is set for all mixer_profiles(defaults). Mod ### Unified VTOL transition controller Manual `MIXER TRANSITION` and mission-authorized VTOL transition both use the same internal transition controller. -This controller computes transition progress, controls mixer transition scaling, and performs profile hot-switch only inside the authorized transition state. +This controller always computes transition progress/completion and performs profile hot-switch only inside the authorized transition state. +When `vtol_transition_dynamic_mixer = ON`, that progress is also used for pusher/lift/authority scaling. ### Airspeed-first completion @@ -94,6 +95,7 @@ When pitot airspeed is healthy and available, transition completion uses pitot t - `vtol_transition_to_fw_min_airspeed_cm_s` for MC->FW - `vtol_transition_to_mc_max_airspeed_cm_s` for FW->MC +- If `vtol_transition_to_fw_min_airspeed_cm_s = 0`, MC->FW falls back to legacy `mixer_switch_trans_airspeed_cm_s`. If pitot is unavailable/unhealthy (or threshold is `0`), timer fallback is used (`mixer_switch_trans_timer`). Ground speed is not used for transition completion/progress. @@ -112,6 +114,7 @@ When `vtol_transition_dynamic_mixer = ON`, transition progress scales: - FW authority start level (`vtol_transition_fw_authority_start_percent`, servo transition input blend). Default is OFF to preserve existing behavior. +With dynamic scaling enabled, `vtol_transition_fw_authority_start_percent = 100` preserves legacy FW authority handoff; lower values provide smoother ramp-in. ### Mission-authorized VTOL transition (waypoint User Action) @@ -119,12 +122,14 @@ INAV supports mission-requested VTOL transitions through the existing automated - `nav_vtol_mission_transition_user_action` (`OFF`, `USER1`, `USER2`, `USER3`, `USER4`) - `nav_vtol_mission_transition_min_altitude_cm` (optional, `0` disables minimum-altitude check) -- `mixer_switch_trans_airspeed_cm_s` (legacy MC->FW threshold fallback/compatibility) +- `vtol_transition_to_fw_min_airspeed_cm_s` (preferred MC->FW threshold) +- `mixer_switch_trans_airspeed_cm_s` (legacy MC->FW fallback when preferred threshold is `0`) On each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`), the configured USER action bit is used as absolute target selector: - selected USER bit = `0` -> transition to MC / MULTIROTOR profile - selected USER bit = `1` -> transition to FW / AIRPLANE profile +- When `nav_vtol_mission_transition_user_action != OFF`, each navigable waypoint encodes a target state via that selected bit. - This is **not** a toggle command. - If already in the requested profile type, the action is treated as complete (idempotent). @@ -135,6 +140,18 @@ Mission path uses the same controller and completion logic as manual transition Manual RC switching (`MIXER PROFILE 2`, `MIXER TRANSITION`) remains blocked during normal active navigation. Mission VTOL transition does not bypass the hot-switch safety guard; it only authorizes switching inside the automated transition state. +### Validation Matrix (PR / SITL / HITL) + +- MC->FW manual, pitot healthy/available. +- MC->FW manual, no pitot (timer fallback). +- FW->MC manual, pitot healthy/available. +- FW->MC manual, no pitot (timer fallback). +- `MIXER TRANSITION` held ON after completion (no repeated starts). +- `MIXER TRANSITION` OFF before hot-switch (safe abort). +- Mission transition with selected USER bit = `1` (TO_FW). +- Mission transition with selected USER bit = `0` (TO_MC). +- Failsafe/disarm during active transition (abort and no blind mission resume). + ## TailSitter (planned for INAV 7.1) TailSitter is supported by add a 90deg offset to the board alignment. Set the board aliment normally in the mixer_profile for FW mode(`set platform_type = AIRPLANE`), The motor trust axis should be same direction as the airplane nose. Then, in the mixer_profile for takeoff and landing set `tailsitter_orientation_offset = ON ` to apply orientation offset. orientation offset will also add a 45deg orientation offset. diff --git a/docs/Navigation.md b/docs/Navigation.md index 02000d0e487..730ce10f7d4 100755 --- a/docs/Navigation.md +++ b/docs/Navigation.md @@ -111,13 +111,17 @@ Configuration: - `nav_vtol_mission_transition_user_action` selects which waypoint User Action (`USER1..USER4`) is used as the mission VTOL target selector. - `nav_vtol_mission_transition_min_altitude_cm` optionally enforces a minimum altitude before transition start (`0` disables check). - `nav_vtol_mission_transition_track_distance_cm` configures straight-line MC->FW transition guidance distance. -- VTOL transition completion logic is shared with manual MIXER TRANSITION and uses the mixer transition settings. +- VTOL transition completion logic is shared with manual MIXER TRANSITION and uses mixer transition settings: + - preferred MC->FW threshold: `vtol_transition_to_fw_min_airspeed_cm_s` + - legacy MC->FW fallback (when preferred threshold is `0`): `mixer_switch_trans_airspeed_cm_s` + - FW->MC threshold: `vtol_transition_to_mc_max_airspeed_cm_s` Behavior on each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`): - The configured USER bit is an **absolute target selector**: - `0`: transition to MC / MULTIROTOR profile - `1`: transition to FW / AIRPLANE profile +- When `nav_vtol_mission_transition_user_action != OFF`, each navigable waypoint always encodes target state via that selected USER bit. - This command is **not** a toggle. - The command is idempotent: if already in the requested target profile type, the mission continues immediately. - If a transition is needed, mission progression pauses while automated transition runs, then resumes only after completion. diff --git a/docs/VTOL.md b/docs/VTOL.md index 8341c81086d..5efcd250951 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -239,7 +239,7 @@ If you have set up the mixer as suggested in STEP1 and STEP2, you may have to de # STEP 5: Transition Mixing (Multi-Rotor Profile)(Recommended) ### Transition Mixing is typically useful in multi-copter profile to gain airspeed in prior to entering the fixed-wing profile. When the `MIXER TRANSITION` mode is activated, the associated motor or servo will move according to your configured Transition Mixing. -Please note that transition input is disabled when a navigation mode is activated. The use of Transition Mixing is necessary to enable additional features such as VTOL RTH with out stalling. +Please note that manual transition input is disabled when a navigation mode is active. Mission-authorized VTOL transition (via configured waypoint User Action) still works through the automated transition state. ## Servo 'Transition Mixing': Tilting rotor configuration. Add new servo mixer rules, and select 'Mixer Transition' in input. Set the weight/rate according to your desired angle. This will allow tilting the motor for tilting rotor model. @@ -283,6 +283,151 @@ set mixer_automated_switch = ON If you set `mixer_automated_switch` to `OFF` for all mixer profiles (the default setting), the model will not perform automated transitions. You can always enable navigation modes after performing a manual transition. +## Unified VTOL Transition Controller (Manual + Mission) + +INAV now uses one internal VTOL transition controller for both: +- manual `MIXER TRANSITION` requests, and +- mission-authorized VTOL transitions. + +This keeps one safety boundary for profile hot-switching and avoids separate transition implementations. + +### Behavior summary + +- Transition progress is always computed internally. +- Pitot airspeed is the primary source for transition completion when healthy/available. +- Timer is used as fallback when pitot is unavailable/unhealthy. +- Ground speed is not used for transition completion. +- Mission transition uses the same controller and does not directly manipulate motors. +- Manual `MIXER PROFILE` / `MIXER TRANSITION` bypass during normal waypoint navigation is still blocked. + +### Manual transition semantics + +With `manual_vtol_transition_controller = ON`: +- `MIXER TRANSITION` acts as an edge-triggered request. +- A rising edge starts one transition. +- Transition then runs autonomously to completion. +- Keeping the mode ON does not repeatedly retrigger transition. +- To start another transition, mode must go OFF then ON again. +- If mode is turned OFF before hot-switch, transition request is aborted safely. + +With `manual_vtol_transition_controller = OFF`: +- legacy manual behavior is preserved for backward compatibility. + +### Mission-authorized transition semantics + +Mission transition is configured with `nav_vtol_mission_transition_user_action`. + +- `OFF`: feature disabled. +- `USER1`..`USER4`: selected User Action bit is used as target selector on navigable waypoints. +- selected bit `0` -> target MC profile +- selected bit `1` -> target FW profile +- Mission progression pauses during transition and resumes only after completion. +- If already in requested target profile, command is idempotent (no new transition). + +For MC -> FW mission transition: +- guidance uses a straight acceleration segment (no loiter), +- normal waypoint advancement is paused during transition. + +### Airspeed-first completion logic + +MC -> FW: +- completion threshold: `vtol_transition_to_fw_min_airspeed_cm_s` +- if this is `0`, legacy `mixer_switch_trans_airspeed_cm_s` is used. + +FW -> MC: +- completion threshold: `vtol_transition_to_mc_max_airspeed_cm_s` + +Timeout: +- `vtol_transition_airspeed_timeout_ms` can abort transition if condition is not achieved in time. + +### Dynamic mixer scaling + +When `vtol_transition_dynamic_mixer = ON`, transition progress additionally scales: +- pusher transition contribution, +- vertical lift contribution, +- MC stabilization authority, +- FW transition input authority blend. + +When `vtol_transition_dynamic_mixer = OFF`, legacy static transition mixing behavior is preserved. + +### Detailed effect of the three percentage settings + +These three settings are active only when `vtol_transition_dynamic_mixer = ON`. + +1. `vtol_transition_lift_end_percent` +- Defines lift throttle scale at transition end. +- MC -> FW: lift goes from `100%` at start to `lift_end_percent` at end. +- FW -> MC: lift goes from `lift_end_percent` at start to `100%` at end. + +Example (`vtol_transition_lift_end_percent = 20`): +- MC -> FW at 50% progress: lift scale is about 60%. +- FW -> MC at 50% progress: lift scale is about 60%. + +2. `vtol_transition_mc_authority_end_percent` +- Defines MC stabilization authority scale at transition end. +- MC -> FW: MC authority goes from `100%` at start to `mc_authority_end_percent` at end. +- FW -> MC: MC authority goes from `mc_authority_end_percent` at start to `100%` at end. + +Example (`vtol_transition_mc_authority_end_percent = 30`): +- MC -> FW at 50% progress: MC authority is about 65%. +- FW -> MC at 50% progress: MC authority is about 65%. + +3. `vtol_transition_fw_authority_start_percent` +- Defines FW authority scale at transition start. +- MC -> FW: FW authority goes from `fw_authority_start_percent` at start to `100%` at end. +- FW -> MC: FW authority goes from `100%` at start to `fw_authority_start_percent` at end. + +Example (`vtol_transition_fw_authority_start_percent = 25`): +- MC -> FW at 50% progress: FW authority is about 62.5%. +- FW -> MC at 50% progress: FW authority is about 62.5%. + +Backward-compatible note: +- `vtol_transition_fw_authority_start_percent = 100` preserves legacy FW authority handoff behavior. +- Lower values provide smoother FW authority ramp-in/out. + +## CLI Commands (English) + +Use these commands in CLI (`set ...`, then `save`): + +- `set manual_vtol_transition_controller = ON|OFF` + - Enables edge-triggered manual transition controller. + +- `set vtol_transition_dynamic_mixer = ON|OFF` + - Enables/disables dynamic progress-based scaling. + +- `set vtol_transition_to_fw_min_airspeed_cm_s = ` + - Preferred MC -> FW completion threshold (pitot airspeed). + +- `set mixer_switch_trans_airspeed_cm_s = ` + - Legacy MC -> FW threshold, used when `vtol_transition_to_fw_min_airspeed_cm_s = 0`. + +- `set mixer_switch_trans_timer = ` + - Timer-based transition duration fallback (used when pitot airspeed is unavailable/unhealthy). + +- `set vtol_transition_to_mc_max_airspeed_cm_s = ` + - FW -> MC completion threshold (pitot airspeed). + +- `set vtol_transition_airspeed_timeout_ms = ` + - Transition timeout/abort window. + +- `set vtol_transition_lift_end_percent = <0..100>` + - Lift scale endpoint for dynamic transition. + +- `set vtol_transition_mc_authority_end_percent = <0..100>` + - MC authority endpoint for dynamic transition. + +- `set vtol_transition_fw_authority_start_percent = <0..100>` + - FW authority start level for dynamic transition. + +- `set nav_vtol_mission_transition_user_action = OFF|USER1|USER2|USER3|USER4` + - Selects waypoint User Action bit used for mission VTOL target selector. + +- `set nav_vtol_mission_transition_min_altitude_cm = ` + - Optional minimum altitude check before mission transition start (`0` disables). + +- `set nav_vtol_mission_transition_track_distance_cm = ` + - Straight-line transition guidance distance for mission MC -> FW segment. + # Notes and Experiences ## General diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 68b058e8d37..f9f0204e768 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1281,7 +1281,7 @@ groups: min: 0 max: 200 - name: mixer_switch_trans_airspeed_cm_s - description: "Airspeed threshold [cm/s] for MC->FW automated profile switch. If > 0 and valid pitot airspeed is available, transition will switch to FW only after this speed is reached. If airspeed is unavailable, timer-based fallback (`mixer_switch_trans_timer`) is used." + description: "Legacy MC->FW airspeed threshold [cm/s] for automated profile switch. Used when `vtol_transition_to_fw_min_airspeed_cm_s = 0`. If airspeed is unavailable, timer-based fallback (`mixer_switch_trans_timer`) is used." default_value: 0 field: mixer_config.switchTransitionAirspeed min: 0 @@ -1297,7 +1297,7 @@ groups: field: mixer_config.manualVtolTransitionController type: bool - name: vtol_transition_to_fw_min_airspeed_cm_s - description: "Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. If 0, `mixer_switch_trans_airspeed_cm_s` is used for MC->FW." + description: "Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. Overrides `mixer_switch_trans_airspeed_cm_s` when > 0. If 0, legacy setting is used." default_value: 0 field: mixer_config.vtolTransitionToFwMinAirspeed min: 0 diff --git a/src/main/flight/mixer.c b/src/main/flight/mixer.c index 030d56cc0ca..77038141281 100644 --- a/src/main/flight/mixer.c +++ b/src/main/flight/mixer.c @@ -649,7 +649,9 @@ void FAST_CODE mixTable(void) } //spin stopped motors only in mixer transition mode if (isMixerTransitionMixing && currentMixer[i].throttle <= -1.05f && currentMixer[i].throttle >= -2.0f && !feature(FEATURE_REVERSIBLE_MOTORS)) { - motor[i] = -currentMixer[i].throttle * 1000 * pusherScale; + const float pusherTarget = -currentMixer[i].throttle * 1000.0f; + const float pusherIdle = throttleRangeMin; + motor[i] = pusherIdle + (pusherTarget - pusherIdle) * pusherScale; motor[i] = constrain(motor[i], throttleRangeMin, throttleRangeMax); } } diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 208b0664d68..8cead491bae 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -131,7 +131,9 @@ void setMixerProfileAT(void) mixerProfileAT.aborted = false; mixerProfileAT.hotSwitchDone = false; mixerProfileAT.usedAirspeed = false; + mixerProfileAT.transitionStartAirspeedCaptured = false; mixerProfileAT.progress = 0.0f; + mixerProfileAT.transitionStartAirspeedCmS = 0.0f; mixerProfileAT.blendToFw = mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW ? 0.0f : 1.0f; mixerProfileAT.pusherScale = 1.0f; mixerProfileAT.liftScale = 1.0f; @@ -171,6 +173,16 @@ static void resetTransitionScales(void) mixerProfileAT.fwAuthorityScale = 1.0f; } +static void setLegacyTransitionScales(void) +{ + mixerProfileAT.progress = 1.0f; + mixerProfileAT.blendToFw = 1.0f; + mixerProfileAT.pusherScale = 1.0f; + mixerProfileAT.liftScale = 1.0f; + mixerProfileAT.mcAuthorityScale = 1.0f; + mixerProfileAT.fwAuthorityScale = 1.0f; +} + static float blendScale(float from, float to, float progress) { return from + (to - from) * constrainf(progress, 0.0f, 1.0f); @@ -251,6 +263,8 @@ static void abortTransition(void) mixerProfileAT.hotSwitchDone = false; mixerProfileAT.request = MIXERAT_REQUEST_NONE; mixerProfileAT.direction = MIXERAT_DIRECTION_NONE; + mixerProfileAT.transitionStartAirspeedCaptured = false; + mixerProfileAT.transitionStartAirspeedCmS = 0.0f; resetTransitionScales(); } @@ -275,7 +289,19 @@ static bool mixerATReadyForHotSwitch(const mixerProfileATRequest_e required_acti return airspeedCmS >= airspeedThresholdCmS; } - mixerProfileAT.progress = constrainf((airspeedThresholdCmS - airspeedCmS) / airspeedThresholdCmS, 0.0f, 1.0f); + if (!mixerProfileAT.transitionStartAirspeedCaptured) { + mixerProfileAT.transitionStartAirspeedCmS = airspeedCmS; + mixerProfileAT.transitionStartAirspeedCaptured = true; + } + + const float startAirspeed = mixerProfileAT.transitionStartAirspeedCmS; + const float thresholdAirspeed = airspeedThresholdCmS; + if (startAirspeed <= thresholdAirspeed) { + mixerProfileAT.progress = 1.0f; + } else { + mixerProfileAT.progress = constrainf((startAirspeed - airspeedCmS) / (startAirspeed - thresholdAirspeed), 0.0f, 1.0f); + } + return airspeedCmS <= airspeedThresholdCmS; } @@ -454,6 +480,9 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) // Backward-compatible manual path: level-controlled transition mixing request. if (!FLIGHT_MODE(FAILSAFE_MODE) && (!mixerAT_inuse)) { isMixerTransitionMixing_requested = transitionModeActive; + if (isMixerTransitionMixing_requested) { + setLegacyTransitionScales(); + } } manualTransitionReadyForEdge = true; } else { diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index 1b3ae5f6f86..46fa19bf7d7 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -72,7 +72,9 @@ typedef struct mixerProfileAT_s { bool aborted; bool hotSwitchDone; bool usedAirspeed; + bool transitionStartAirspeedCaptured; float progress; + float transitionStartAirspeedCmS; float blendToFw; float pusherScale; float liftScale; diff --git a/src/main/navigation/navigation.c b/src/main/navigation/navigation.c index c78f7d8964d..1c0a09310c6 100644 --- a/src/main/navigation/navigation.c +++ b/src/main/navigation/navigation.c @@ -2458,6 +2458,13 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_INITIALIZE(navi static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(navigationFSMState_t previousState) { UNUSED(previousState); + + if (!ARMING_FLAG(ARMED) || FLIGHT_MODE(FAILSAFE_MODE)) { + mixerATUpdateState(MIXERAT_REQUEST_ABORT); + clearMissionVTOLTransitionState(); + return NAV_FSM_EVENT_SWITCH_TO_IDLE; + } + mixerProfileATRequest_e required_action; switch (navMixerATPendingState) { From 232dfb5ca0de1ad738ec1b4211ebc53eca8a1535 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Tue, 12 May 2026 23:13:59 +0300 Subject: [PATCH 04/26] - document explicit USER-bit semantics as absolute per-waypoint platform target (0=MC, 1=FW) - document dependency on existing mixer profile switching infrastructure (two profiles + MIXER PROFILE 2 mode condition) - update docs: MixerProfile.md, Navigation.md, VTOL.md with behavior, safety boundaries, tuning examples, and CLI reference --- docs/MixerProfile.md | 8 ++++++++ docs/Navigation.md | 2 ++ docs/VTOL.md | 12 +++++++++++- src/main/fc/settings.yaml | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 64f9bc7a3c1..a05fcc1e36c 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -130,6 +130,7 @@ On each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`), the con - selected USER bit = `0` -> transition to MC / MULTIROTOR profile - selected USER bit = `1` -> transition to FW / AIRPLANE profile - When `nav_vtol_mission_transition_user_action != OFF`, each navigable waypoint encodes a target state via that selected bit. +- This is a per-waypoint target-state declaration (not an event trigger). Users should intentionally set/clear the selected USER bit on each navigable waypoint. - This is **not** a toggle command. - If already in the requested profile type, the action is treated as complete (idempotent). @@ -139,6 +140,13 @@ For MC -> FW mission transitions, navigation uses a straight acceleration segmen Mission path uses the same controller and completion logic as manual transition (airspeed-first, timer fallback). Manual RC switching (`MIXER PROFILE 2`, `MIXER TRANSITION`) remains blocked during normal active navigation. Mission VTOL transition does not bypass the hot-switch safety guard; it only authorizes switching inside the automated transition state. +Mission VTOL transition still relies on normal profile-switch infrastructure: configure two mixer profiles and a valid `MIXER PROFILE 2` mode activation condition. + +Example smooth-start values (optional tuning baseline): +- `set vtol_transition_dynamic_mixer = ON` +- `set vtol_transition_lift_end_percent = 30` +- `set vtol_transition_mc_authority_end_percent = 20` +- `set vtol_transition_fw_authority_start_percent = 20` ### Validation Matrix (PR / SITL / HITL) diff --git a/docs/Navigation.md b/docs/Navigation.md index 730ce10f7d4..9fb02f8a7a0 100755 --- a/docs/Navigation.md +++ b/docs/Navigation.md @@ -122,6 +122,7 @@ Behavior on each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`) - `0`: transition to MC / MULTIROTOR profile - `1`: transition to FW / AIRPLANE profile - When `nav_vtol_mission_transition_user_action != OFF`, each navigable waypoint always encodes target state via that selected USER bit. +- This means every navigable waypoint implicitly declares desired VTOL platform state when this feature is enabled; users must intentionally set/clear that bit on each waypoint. - This command is **not** a toggle. - The command is idempotent: if already in the requested target profile type, the mission continues immediately. - If a transition is needed, mission progression pauses while automated transition runs, then resumes only after completion. @@ -138,6 +139,7 @@ Transition behavior in this MVP: Safety and scope: - This path uses authorized automated transition state handling; it does not permit manual mixer profile switching during normal waypoint navigation. +- It still depends on valid mixer profile switching infrastructure (two configured mixer profiles and a valid `MIXER PROFILE 2` mode activation condition). `wp save` - Checks list of waypoints and save from FC to EEPROM (warning: it also saves all unsaved CLI settings like normal `save`). diff --git a/docs/VTOL.md b/docs/VTOL.md index 5efcd250951..c1f4ffa29c7 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -321,6 +321,7 @@ Mission transition is configured with `nav_vtol_mission_transition_user_action`. - `USER1`..`USER4`: selected User Action bit is used as target selector on navigable waypoints. - selected bit `0` -> target MC profile - selected bit `1` -> target FW profile +- when enabled, every navigable waypoint implicitly declares desired VTOL platform state through that selected bit, so users should set/clear it intentionally per waypoint - Mission progression pauses during transition and resumes only after completion. - If already in requested target profile, command is idempotent (no new transition). @@ -350,6 +351,12 @@ When `vtol_transition_dynamic_mixer = ON`, transition progress additionally scal When `vtol_transition_dynamic_mixer = OFF`, legacy static transition mixing behavior is preserved. +Optional smooth-start baseline: +- `vtol_transition_dynamic_mixer = ON` +- `vtol_transition_lift_end_percent = 30` +- `vtol_transition_mc_authority_end_percent = 20` +- `vtol_transition_fw_authority_start_percent = 20` + ### Detailed effect of the three percentage settings These three settings are active only when `vtol_transition_dynamic_mixer = ON`. @@ -420,7 +427,7 @@ Use these commands in CLI (`set ...`, then `save`): - FW authority start level for dynamic transition. - `set nav_vtol_mission_transition_user_action = OFF|USER1|USER2|USER3|USER4` - - Selects waypoint User Action bit used for mission VTOL target selector. + - Selects waypoint User Action bit used for mission VTOL target selector (absolute per-waypoint desired state). - `set nav_vtol_mission_transition_min_altitude_cm = ` - Optional minimum altitude check before mission transition start (`0` disables). @@ -428,6 +435,9 @@ Use these commands in CLI (`set ...`, then `save`): - `set nav_vtol_mission_transition_track_distance_cm = ` - Straight-line transition guidance distance for mission MC -> FW segment. +Mission profile-switch dependency: +- Mission VTOL transition uses the existing profile hot-switch path, so two valid mixer profiles and a configured `MIXER PROFILE 2` mode activation condition are required. + # Notes and Experiences ## General diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index f9f0204e768..a5b8898f6e6 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -2679,7 +2679,7 @@ groups: field: general.flags.waypoint_mission_restart table: nav_wp_mission_restart - name: nav_vtol_mission_transition_user_action - description: "Selects which waypoint USER action bit (`USER1`..`USER4`) is used as mission VTOL target selector. OFF disables this feature. On navigable mission waypoints: selected USER bit = 1 requests FW profile, selected USER bit = 0 requests MC profile." + description: "Selects which waypoint USER action bit (`USER1`..`USER4`) is used as mission VTOL target selector. OFF disables this feature. On navigable mission waypoints: selected USER bit = 1 requests FW profile, selected USER bit = 0 requests MC profile. This is an absolute per-waypoint target-state selector and relies on existing mixer profile switching infrastructure (two profiles and valid MIXER PROFILE 2 mode activation condition)." default_value: "OFF" field: general.vtol_mission_transition_user_action table: nav_wp_user_action From 8ab311a1745337ab22933e4e7af5a899b39766d3 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Wed, 13 May 2026 15:32:37 +0300 Subject: [PATCH 05/26] feat(vtol): add optional dynamic scaling ramp timer and clarify legacy/3-pos switch behavior - add new mixer setting `vtol_transition_scale_ramp_time_ms` (default 0) - keep backward compatibility: - `0` => scaling stays coupled to transition progress (existing behavior) - `>0` => pusher/lift/authority scaling uses time-based ramp - keep transition completion logic unchanged: - airspeed-first when pitot is healthy/available - timer fallback via `mixer_switch_trans_timer` when pitot is unavailable/unhealthy - wire new setting into mixer profile config/reset path - update VTOL and MixerProfile docs: - explicitly state intent is not to replace legacy manual behavior - document 3-position workflow with edge-trigger controller - document new ramp timer semantics with practical examples --- docs/MixerProfile.md | 19 +++++++++- docs/VTOL.md | 67 +++++++++++++++++++++++++++++++++ src/main/fc/settings.yaml | 6 +++ src/main/flight/mixer_profile.c | 32 ++++++++++++---- src/main/flight/mixer_profile.h | 1 + 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index a05fcc1e36c..a62cc909f3f 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -87,7 +87,9 @@ When `mixer_automated_switch`:`OFF` is set for all mixer_profiles(defaults). Mod Manual `MIXER TRANSITION` and mission-authorized VTOL transition both use the same internal transition controller. This controller always computes transition progress/completion and performs profile hot-switch only inside the authorized transition state. -When `vtol_transition_dynamic_mixer = ON`, that progress is also used for pusher/lift/authority scaling. +When `vtol_transition_dynamic_mixer = ON`, pusher/lift/authority scaling is enabled and is driven by: +- transition progress (default), or +- `vtol_transition_scale_ramp_time_ms` when configured (>0). ### Airspeed-first completion @@ -116,6 +118,21 @@ When `vtol_transition_dynamic_mixer = ON`, transition progress scales: Default is OFF to preserve existing behavior. With dynamic scaling enabled, `vtol_transition_fw_authority_start_percent = 100` preserves legacy FW authority handoff; lower values provide smoother ramp-in. +Optional scaling ramp timer: + +- `vtol_transition_scale_ramp_time_ms = 0` (default): scaling remains coupled to transition progress (legacy-compatible behavior). +- `vtol_transition_scale_ramp_time_ms > 0`: scaling uses this timer, while transition completion stays airspeed-first (or timer fallback if pitot unavailable/unhealthy). + +Example: + +- `mixer_switch_trans_timer = 50` (5s fallback completion timer) +- `vtol_transition_scale_ramp_time_ms = 1200` + +Result: +- scaling reaches target levels in ~1.2s, +- transition completion still follows airspeed threshold when pitot is healthy, +- timer fallback completion still uses 5s when pitot is unavailable/unhealthy. + ### Mission-authorized VTOL transition (waypoint User Action) INAV supports mission-requested VTOL transitions through the existing automated transition path. This is configured with: diff --git a/docs/VTOL.md b/docs/VTOL.md index c1f4ffa29c7..0807ffe4ef6 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -302,6 +302,8 @@ This keeps one safety boundary for profile hot-switching and avoids separate tra ### Manual transition semantics +Intent: this does not replace legacy manual behavior. Legacy remains available and selectable. + With `manual_vtol_transition_controller = ON`: - `MIXER TRANSITION` acts as an edge-triggered request. - A rising edge starts one transition. @@ -313,6 +315,15 @@ With `manual_vtol_transition_controller = ON`: With `manual_vtol_transition_controller = OFF`: - legacy manual behavior is preserved for backward compatibility. +Typical 3-position switch workflow (edge-trigger mode enabled): +- Position 1: MC +- Position 2: Transition (trigger AUTO transition sequence) +- Position 3: FW + +Operational example: +- fly in MC (pos1) -> move to Transition (pos2) to start automatic MC->FW transition -> after completion move to FW (pos3), +- reverse order for FW->MC. + ### Mission-authorized transition semantics Mission transition is configured with `nav_vtol_mission_transition_user_action`. @@ -351,6 +362,19 @@ When `vtol_transition_dynamic_mixer = ON`, transition progress additionally scal When `vtol_transition_dynamic_mixer = OFF`, legacy static transition mixing behavior is preserved. +Optional decoupled scaling ramp: +- `vtol_transition_scale_ramp_time_ms = 0` (default): scaling follows transition progress (legacy-compatible behavior). +- `vtol_transition_scale_ramp_time_ms > 0`: scaling uses this ramp timer, while completion logic remains unchanged (airspeed-first; timer fallback when pitot is unavailable/unhealthy). + +Example: +- `mixer_switch_trans_timer = 50` (5s fallback completion timer) +- `vtol_transition_scale_ramp_time_ms = 1200` + +Result: +- pusher/lift/authority scaling reaches target levels in ~1.2s, +- transition completion still follows airspeed thresholds when pitot is healthy, +- if pitot is unavailable/unhealthy, completion fallback still uses 5s. + Optional smooth-start baseline: - `vtol_transition_dynamic_mixer = ON` - `vtol_transition_lift_end_percent = 30` @@ -417,6 +441,9 @@ Use these commands in CLI (`set ...`, then `save`): - `set vtol_transition_airspeed_timeout_ms = ` - Transition timeout/abort window. +- `set vtol_transition_scale_ramp_time_ms = ` + - Optional dynamic scaling ramp duration in milliseconds. `0` keeps legacy progress-coupled scaling. `>0` decouples scaling ramp time from completion timing. + - `set vtol_transition_lift_end_percent = <0..100>` - Lift scale endpoint for dynamic transition. @@ -452,3 +479,43 @@ Mission profile-switch dependency: - There will be a time window that tilting motors is providing up lift but rear motor isn't. Result in a sudden pitch raise on the entering of the mode. Use the max speed or faster speed in tiling servo to reduce the time window. OR lower the throttle on the entering of the FW mode to mitigate the effect. ## Dedicated forward motor - Easiest way to setup a vtol. and efficiency can be improved by using different motor/prop for hover and forward flight + +## Pitot-based transition logic (reference) + +When pitot is healthy/available, transition progress is airspeed-driven (not timer-driven). + +- MC -> FW: + - progress = `constrain(airspeed / to_fw_threshold, 0..1)` + - completion condition = `airspeed >= to_fw_threshold` + +- FW -> MC: + - capture `startAirspeed` when transition starts + - progress = `constrain((startAirspeed - airspeed) / (startAirspeed - to_mc_threshold), 0..1)` + - completion condition = `airspeed <= to_mc_threshold` + +Dynamic mixer scaling (`vtol_transition_dynamic_mixer = ON`) uses this progress: + +- MC -> FW: + - pusher scale ramps `0 -> 1` + - lift scale ramps `1 -> vtol_transition_lift_end_percent` + - MC authority ramps `1 -> vtol_transition_mc_authority_end_percent` + - FW authority ramps `vtol_transition_fw_authority_start_percent -> 1` + +- FW -> MC: + - pusher scale ramps `1 -> 0` + - lift scale ramps `vtol_transition_lift_end_percent -> 1` + - MC authority ramps `vtol_transition_mc_authority_end_percent -> 1` + - FW authority ramps `1 -> vtol_transition_fw_authority_start_percent` + +If `vtol_transition_scale_ramp_time_ms > 0`, dynamic scaling uses that timer-based ramp instead of transition-progress coupling. +This changes only scaling shape. Transition completion logic remains airspeed-first (with timer fallback when pitot is unavailable/unhealthy). + +For transition/pusher motors (`-2.0 < throttle < -1.0`), output is interpolated from idle to target: + +`motor = idle + (target - idle) * pusherScale` + +where: +- `target = -mixerThrottle * 1000` +- `idle = throttleRangeMin` + +If pitot is unavailable/unhealthy, timer fallback is used (`mixer_switch_trans_timer`). diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index a5b8898f6e6..9ea3b64966e 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1314,6 +1314,12 @@ groups: field: mixer_config.vtolTransitionAirspeedTimeoutMs min: 0 max: 60000 + - name: vtol_transition_scale_ramp_time_ms + description: "Optional dynamic scaling ramp duration [ms]. When > 0 and `vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling uses this timer instead of transition completion progress. Set to 0 to keep legacy progress-coupled scaling behavior." + default_value: 0 + field: mixer_config.vtolTransitionScaleRampTimeMs + min: 0 + max: 60000 - name: vtol_transition_lift_end_percent description: "Target vertical-lift throttle scale at transition end, in percent. Used only when `vtol_transition_dynamic_mixer` is ON." default_value: 100 diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 8cead491bae..47d5632762c 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -64,6 +64,7 @@ void pgResetFn_mixerProfiles(mixerProfile_t *instance) .vtolTransitionToFwMinAirspeed = SETTING_VTOL_TRANSITION_TO_FW_MIN_AIRSPEED_CM_S_DEFAULT, .vtolTransitionToMcMaxAirspeed = SETTING_VTOL_TRANSITION_TO_MC_MAX_AIRSPEED_CM_S_DEFAULT, .vtolTransitionAirspeedTimeoutMs = SETTING_VTOL_TRANSITION_AIRSPEED_TIMEOUT_MS_DEFAULT, + .vtolTransitionScaleRampTimeMs = SETTING_VTOL_TRANSITION_SCALE_RAMP_TIME_MS_DEFAULT, .vtolTransitionLiftEndPercent = SETTING_VTOL_TRANSITION_LIFT_END_PERCENT_DEFAULT, .vtolTransitionMcAuthorityEndPercent = SETTING_VTOL_TRANSITION_MC_AUTHORITY_END_PERCENT_DEFAULT, .vtolTransitionFwAuthorityStartPercent = SETTING_VTOL_TRANSITION_FW_AUTHORITY_START_PERCENT_DEFAULT, @@ -188,6 +189,20 @@ static float blendScale(float from, float to, float progress) return from + (to - from) * constrainf(progress, 0.0f, 1.0f); } +static float getScalingProgress(void) +{ + if (!currentMixerConfig.vtolTransitionDynamicMixer) { + return 1.0f; + } + + if (currentMixerConfig.vtolTransitionScaleRampTimeMs > 0) { + const uint32_t elapsedMs = millis() - mixerProfileAT.transitionStartTime; + return constrainf((float)elapsedMs / (float)currentMixerConfig.vtolTransitionScaleRampTimeMs, 0.0f, 1.0f); + } + + return constrainf(mixerProfileAT.progress, 0.0f, 1.0f); +} + static bool hasTrustedPitotAirspeed(float *airspeedCmS) { #ifdef USE_PITOT @@ -238,17 +253,18 @@ static void updateTransitionScales(void) const float liftFloor = constrainf(currentMixerConfig.vtolTransitionLiftEndPercent / 100.0f, 0.0f, 1.0f); const float mcFloor = constrainf(currentMixerConfig.vtolTransitionMcAuthorityEndPercent / 100.0f, 0.0f, 1.0f); const float fwFloor = constrainf(currentMixerConfig.vtolTransitionFwAuthorityStartPercent / 100.0f, 0.0f, 1.0f); + const float scaleProgress = getScalingProgress(); if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW) { - mixerProfileAT.pusherScale = blendScale(0.0f, 1.0f, mixerProfileAT.progress); - mixerProfileAT.liftScale = blendScale(1.0f, liftFloor, mixerProfileAT.progress); - mixerProfileAT.mcAuthorityScale = blendScale(1.0f, mcFloor, mixerProfileAT.progress); - mixerProfileAT.fwAuthorityScale = blendScale(fwFloor, 1.0f, mixerProfileAT.progress); + mixerProfileAT.pusherScale = blendScale(0.0f, 1.0f, scaleProgress); + mixerProfileAT.liftScale = blendScale(1.0f, liftFloor, scaleProgress); + mixerProfileAT.mcAuthorityScale = blendScale(1.0f, mcFloor, scaleProgress); + mixerProfileAT.fwAuthorityScale = blendScale(fwFloor, 1.0f, scaleProgress); } else if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_MC) { - mixerProfileAT.pusherScale = blendScale(1.0f, 0.0f, mixerProfileAT.progress); - mixerProfileAT.liftScale = blendScale(liftFloor, 1.0f, mixerProfileAT.progress); - mixerProfileAT.mcAuthorityScale = blendScale(mcFloor, 1.0f, mixerProfileAT.progress); - mixerProfileAT.fwAuthorityScale = blendScale(1.0f, fwFloor, mixerProfileAT.progress); + mixerProfileAT.pusherScale = blendScale(1.0f, 0.0f, scaleProgress); + mixerProfileAT.liftScale = blendScale(liftFloor, 1.0f, scaleProgress); + mixerProfileAT.mcAuthorityScale = blendScale(mcFloor, 1.0f, scaleProgress); + mixerProfileAT.fwAuthorityScale = blendScale(1.0f, fwFloor, scaleProgress); } mixerProfileAT.blendToFw = constrainf(mixerProfileAT.fwAuthorityScale, 0.0f, 1.0f); diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index 46fa19bf7d7..784463eba7d 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -24,6 +24,7 @@ typedef struct mixerConfig_s { uint16_t vtolTransitionToFwMinAirspeed; uint16_t vtolTransitionToMcMaxAirspeed; uint16_t vtolTransitionAirspeedTimeoutMs; + uint16_t vtolTransitionScaleRampTimeMs; uint8_t vtolTransitionLiftEndPercent; uint8_t vtolTransitionMcAuthorityEndPercent; uint8_t vtolTransitionFwAuthorityStartPercent; From 50612a2a2570a5d1bfb7f4b267a2e95491e828cd Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Thu, 14 May 2026 16:17:39 +0300 Subject: [PATCH 06/26] docs: regenerate Settings.md from settings definitions --- docs/Settings.md | 134 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/docs/Settings.md b/docs/Settings.md index d49067357bc..7b0e1073d61 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -2824,6 +2824,16 @@ Servo travel multiplier for the ROLL axis in `MANUAL` flight mode [0-100]% --- +### manual_vtol_transition_controller + +Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior. + +| Default | Min | Max | +| --- | --- | --- | +| OFF | OFF | ON | + +--- + ### manual_yaw_rate Servo travel multiplier for the YAW axis in `MANUAL` flight mode [0-100]% @@ -3199,6 +3209,16 @@ If enabled, control_profile_index will follow mixer_profile index. Set to OFF(de --- +### mixer_switch_trans_airspeed_cm_s + +Legacy MC->FW airspeed threshold [cm/s] for automated profile switch. Used when `vtol_transition_to_fw_min_airspeed_cm_s = 0`. If airspeed is unavailable, timer-based fallback (`mixer_switch_trans_timer`) is used. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 10000 | + +--- + ### mixer_switch_trans_timer If switch another mixer_profile is scheduled by mixer_automated_switch or mixer_automated_switch. Activate Mixertransion motor/servo mixing for this many decisecond(0.1s) before the actual mixer_profile switch. @@ -4626,6 +4646,40 @@ Defines how Pitch/Roll input from RC receiver affects flight in POSHOLD mode: AT --- +### nav_vtol_mission_transition_min_altitude_cm + +Minimum altitude [cm] required to start a mission-authorized VTOL transition. Set to 0 to disable the minimum-altitude check. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 50000 | + +--- + +### nav_vtol_mission_transition_track_distance_cm + +Straight-line target distance [cm] used during mission-authorized MC->FW transition guidance. This controls how far ahead the transition heading target is placed. + +| Default | Min | Max | +| --- | --- | --- | +| 100000 | 1000 | 500000 | + +--- + +### nav_vtol_mission_transition_user_action + +Selects which waypoint USER action bit (`USER1`..`USER4`) is used as mission VTOL target selector. OFF disables this feature. On navigable mission waypoints: selected USER bit = 1 requests FW profile, selected USER bit = 0 requests MC profile. This is an absolute per-waypoint target-state selector and relies on existing mixer profile switching infrastructure (two profiles and valid MIXER PROFILE 2 mode activation condition). + +| Allowed Values | | +| --- | --- | +| OFF | Default | +| USER1 | | +| USER2 | | +| USER3 | | +| USER4 | | + +--- + ### nav_wp_enforce_altitude Forces craft to achieve the set WP altitude as well as position before moving to next WP. Position is held and altitude adjusted as required before moving on. 0 = disabled, otherwise setting defines altitude capture tolerance [cm], e.g. 100 means required altitude is achieved when within 100cm of waypoint altitude setting. @@ -6974,6 +7028,86 @@ Warning voltage per cell, this triggers battery-warning alarms, in 0.01V units, --- +### vtol_transition_airspeed_timeout_ms + +Safety timeout [ms] for airspeed-controlled transitions. If non-zero and required airspeed condition is not met in time, transition aborts instead of force-completing. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 60000 | + +--- + +### vtol_transition_dynamic_mixer + +Enables dynamic VTOL transition progress/scaling controller shared by mission-authorized and manual MIXER TRANSITION paths. + +| Default | Min | Max | +| --- | --- | --- | +| OFF | OFF | ON | + +--- + +### vtol_transition_fw_authority_start_percent + +Initial fixed-wing authority scale at transition start, in percent. Used only when `vtol_transition_dynamic_mixer` is ON. + +| Default | Min | Max | +| --- | --- | --- | +| 100 | 0 | 100 | + +--- + +### vtol_transition_lift_end_percent + +Target vertical-lift throttle scale at transition end, in percent. Used only when `vtol_transition_dynamic_mixer` is ON. + +| Default | Min | Max | +| --- | --- | --- | +| 100 | 0 | 100 | + +--- + +### vtol_transition_mc_authority_end_percent + +Target multicopter stabilization authority scale at transition end, in percent. Used only when `vtol_transition_dynamic_mixer` is ON. + +| Default | Min | Max | +| --- | --- | --- | +| 100 | 0 | 100 | + +--- + +### vtol_transition_scale_ramp_time_ms + +Optional dynamic scaling ramp duration [ms]. When > 0 and `vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling uses this timer instead of transition completion progress. Set to 0 to keep legacy progress-coupled scaling behavior. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 60000 | + +--- + +### vtol_transition_to_fw_min_airspeed_cm_s + +Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. Overrides `mixer_switch_trans_airspeed_cm_s` when > 0. If 0, legacy setting is used. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 20000 | + +--- + +### vtol_transition_to_mc_max_airspeed_cm_s + +Maximum pitot airspeed [cm/s] allowed to complete FW->MC transition when airspeed is healthy and available. If 0, FW->MC uses timer fallback. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 20000 | + +--- + ### vtx_band Configure the VTX band. Bands: 1: A, 2: B, 3: E, 4: F, 5: Race. From 8d781e9e0278dadca216ab4571e9c19f657d8106 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Sat, 16 May 2026 16:59:47 +0300 Subject: [PATCH 07/26] vtol: split global vs per-mixer transition settings, rename manual switch controller setting - Rename per-mixer manual transition setting: - `mixer_manual_vtol_transition_controller` -> `mixer_vtol_manualswitch_autotransition_controller` - Keep per-mixer scope only where profile-specific behavior is intended: - `mixer_vtol_transition_dynamic_mixer` - `mixer_vtol_transition_airspeed_timeout_ms` - `mixer_vtol_transition_scale_ramp_time_ms` - legacy/profile-switch settings (`mixer_automated_switch`, `mixer_switch_trans_*`) - Move these transition tuning parameters from mixer profile scope to global system scope: - `vtol_transition_to_fw_min_airspeed_cm_s` - `vtol_transition_to_mc_max_airspeed_cm_s` - `vtol_transition_lift_end_percent` - `vtol_transition_mc_authority_end_percent` - `vtol_transition_fw_authority_start_percent` - Update transition logic to read moved fields from `systemConfig()` instead of `currentMixerConfig` - Remove moved fields from `mixerConfig_t`; add them to `systemConfig_t` - Bump PG versions for layout changes: - `PG_MIXER_PROFILE`: 2 -> 3 - `PG_SYSTEM_CONFIG`: 7 -> 8 - Update docs and regenerate CLI settings docs: - explicit per-mixer vs global scope notes in VTOL/MixerProfile docs - `docs/Settings.md` regenerated via `update_cli_docs.py` --- docs/MixerProfile.md | 40 +++++++++++---- docs/Settings.md | 86 ++++++++++++++++----------------- docs/VTOL.md | 61 +++++++++++++++++------ src/main/fc/config.c | 7 ++- src/main/fc/config.h | 5 ++ src/main/fc/settings.yaml | 70 +++++++++++++-------------- src/main/flight/mixer_profile.c | 27 +++++------ src/main/flight/mixer_profile.h | 5 -- 8 files changed, 175 insertions(+), 126 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index a62cc909f3f..60f103eb6e4 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -30,8 +30,8 @@ The use of Transition Mode is recommended to enable further features and future - A new transition requires mode OFF then ON again. - If switched OFF before hot-switch completes, the manual transition request is aborted. -This edge-triggered behavior is enabled by `manual_vtol_transition_controller`. -When `manual_vtol_transition_controller = OFF`, manual transition keeps legacy behavior. +This edge-triggered behavior is enabled by `mixer_vtol_manualswitch_autotransition_controller`. +When `mixer_vtol_manualswitch_autotransition_controller = OFF`, manual transition keeps legacy behavior. ## Servo @@ -87,9 +87,9 @@ When `mixer_automated_switch`:`OFF` is set for all mixer_profiles(defaults). Mod Manual `MIXER TRANSITION` and mission-authorized VTOL transition both use the same internal transition controller. This controller always computes transition progress/completion and performs profile hot-switch only inside the authorized transition state. -When `vtol_transition_dynamic_mixer = ON`, pusher/lift/authority scaling is enabled and is driven by: +When `mixer_vtol_transition_dynamic_mixer = ON`, pusher/lift/authority scaling is enabled and is driven by: - transition progress (default), or -- `vtol_transition_scale_ramp_time_ms` when configured (>0). +- `mixer_vtol_transition_scale_ramp_time_ms` when configured (>0). ### Airspeed-first completion @@ -104,11 +104,11 @@ Ground speed is not used for transition completion/progress. Optional safety timeout: -- `vtol_transition_airspeed_timeout_ms` can abort transition if airspeed condition is not met in time. +- `mixer_vtol_transition_airspeed_timeout_ms` can abort transition if airspeed condition is not met in time. ### Dynamic scaling (optional) -When `vtol_transition_dynamic_mixer = ON`, transition progress scales: +When `mixer_vtol_transition_dynamic_mixer = ON`, transition progress scales: - pusher contribution (`-2.0 < throttle < -1.0` motors) from configured max toward 0/100% depending on direction, - lift motor throttle contribution (`vtol_transition_lift_end_percent`), @@ -120,13 +120,13 @@ With dynamic scaling enabled, `vtol_transition_fw_authority_start_percent = 100` Optional scaling ramp timer: -- `vtol_transition_scale_ramp_time_ms = 0` (default): scaling remains coupled to transition progress (legacy-compatible behavior). -- `vtol_transition_scale_ramp_time_ms > 0`: scaling uses this timer, while transition completion stays airspeed-first (or timer fallback if pitot unavailable/unhealthy). +- `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): scaling remains coupled to transition progress (legacy-compatible behavior). +- `mixer_vtol_transition_scale_ramp_time_ms > 0`: scaling uses this timer, while transition completion stays airspeed-first (or timer fallback if pitot unavailable/unhealthy). Example: - `mixer_switch_trans_timer = 50` (5s fallback completion timer) -- `vtol_transition_scale_ramp_time_ms = 1200` +- `mixer_vtol_transition_scale_ramp_time_ms = 1200` Result: - scaling reaches target levels in ~1.2s, @@ -142,6 +142,26 @@ INAV supports mission-requested VTOL transitions through the existing automated - `vtol_transition_to_fw_min_airspeed_cm_s` (preferred MC->FW threshold) - `mixer_switch_trans_airspeed_cm_s` (legacy MC->FW fallback when preferred threshold is `0`) +Scope note: + +- Per-mixer-profile settings: + - `mixer_automated_switch` + - `mixer_switch_trans_timer` + - `mixer_switch_trans_airspeed_cm_s` + - `mixer_vtol_transition_dynamic_mixer` + - `mixer_vtol_manualswitch_autotransition_controller` + - `mixer_vtol_transition_airspeed_timeout_ms` + - `mixer_vtol_transition_scale_ramp_time_ms` +- Global settings: + - `vtol_transition_to_fw_min_airspeed_cm_s` + - `vtol_transition_to_mc_max_airspeed_cm_s` + - `vtol_transition_lift_end_percent` + - `vtol_transition_mc_authority_end_percent` + - `vtol_transition_fw_authority_start_percent` + - `nav_vtol_mission_transition_user_action` + - `nav_vtol_mission_transition_min_altitude_cm` + - `nav_vtol_mission_transition_track_distance_cm` + On each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`), the configured USER action bit is used as absolute target selector: - selected USER bit = `0` -> transition to MC / MULTIROTOR profile @@ -160,7 +180,7 @@ Manual RC switching (`MIXER PROFILE 2`, `MIXER TRANSITION`) remains blocked duri Mission VTOL transition still relies on normal profile-switch infrastructure: configure two mixer profiles and a valid `MIXER PROFILE 2` mode activation condition. Example smooth-start values (optional tuning baseline): -- `set vtol_transition_dynamic_mixer = ON` +- `set mixer_vtol_transition_dynamic_mixer = ON` - `set vtol_transition_lift_end_percent = 30` - `set vtol_transition_mc_authority_end_percent = 20` - `set vtol_transition_fw_authority_start_percent = 20` diff --git a/docs/Settings.md b/docs/Settings.md index 7b0e1073d61..7d7c0e06f18 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -2824,16 +2824,6 @@ Servo travel multiplier for the ROLL axis in `MANUAL` flight mode [0-100]% --- -### manual_vtol_transition_controller - -Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior. - -| Default | Min | Max | -| --- | --- | --- | -| OFF | OFF | ON | - ---- - ### manual_yaw_rate Servo travel multiplier for the YAW axis in `MANUAL` flight mode [0-100]% @@ -3229,6 +3219,46 @@ If switch another mixer_profile is scheduled by mixer_automated_switch or mixer_ --- +### mixer_vtol_manualswitch_autotransition_controller + +Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior. + +| Default | Min | Max | +| --- | --- | --- | +| OFF | OFF | ON | + +--- + +### mixer_vtol_transition_airspeed_timeout_ms + +Safety timeout [ms] for airspeed-controlled transitions. If non-zero and required airspeed condition is not met in time, transition aborts instead of force-completing. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 60000 | + +--- + +### mixer_vtol_transition_dynamic_mixer + +Enables dynamic VTOL transition progress/scaling controller shared by mission-authorized and manual MIXER TRANSITION paths. + +| Default | Min | Max | +| --- | --- | --- | +| OFF | OFF | ON | + +--- + +### mixer_vtol_transition_scale_ramp_time_ms + +Optional dynamic scaling ramp duration [ms]. When > 0 and `mixer_vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling uses this timer instead of transition completion progress. Set to 0 to keep legacy progress-coupled scaling behavior. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 60000 | + +--- + ### mode_range_logic_operator Control how Mode selection works in flight modes. If you example have Angle mode configured on two different Aux channels, this controls if you need both activated ( AND ) or if you only need one activated ( OR ) to active angle mode. @@ -7028,29 +7058,9 @@ Warning voltage per cell, this triggers battery-warning alarms, in 0.01V units, --- -### vtol_transition_airspeed_timeout_ms - -Safety timeout [ms] for airspeed-controlled transitions. If non-zero and required airspeed condition is not met in time, transition aborts instead of force-completing. - -| Default | Min | Max | -| --- | --- | --- | -| 0 | 0 | 60000 | - ---- - -### vtol_transition_dynamic_mixer - -Enables dynamic VTOL transition progress/scaling controller shared by mission-authorized and manual MIXER TRANSITION paths. - -| Default | Min | Max | -| --- | --- | --- | -| OFF | OFF | ON | - ---- - ### vtol_transition_fw_authority_start_percent -Initial fixed-wing authority scale at transition start, in percent. Used only when `vtol_transition_dynamic_mixer` is ON. +Initial fixed-wing authority scale at transition start, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. | Default | Min | Max | | --- | --- | --- | @@ -7060,7 +7070,7 @@ Initial fixed-wing authority scale at transition start, in percent. Used only wh ### vtol_transition_lift_end_percent -Target vertical-lift throttle scale at transition end, in percent. Used only when `vtol_transition_dynamic_mixer` is ON. +Target vertical-lift throttle scale at transition end, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. | Default | Min | Max | | --- | --- | --- | @@ -7070,7 +7080,7 @@ Target vertical-lift throttle scale at transition end, in percent. Used only whe ### vtol_transition_mc_authority_end_percent -Target multicopter stabilization authority scale at transition end, in percent. Used only when `vtol_transition_dynamic_mixer` is ON. +Target multicopter stabilization authority scale at transition end, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. | Default | Min | Max | | --- | --- | --- | @@ -7078,16 +7088,6 @@ Target multicopter stabilization authority scale at transition end, in percent. --- -### vtol_transition_scale_ramp_time_ms - -Optional dynamic scaling ramp duration [ms]. When > 0 and `vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling uses this timer instead of transition completion progress. Set to 0 to keep legacy progress-coupled scaling behavior. - -| Default | Min | Max | -| --- | --- | --- | -| 0 | 0 | 60000 | - ---- - ### vtol_transition_to_fw_min_airspeed_cm_s Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. Overrides `mixer_switch_trans_airspeed_cm_s` when > 0. If 0, legacy setting is used. diff --git a/docs/VTOL.md b/docs/VTOL.md index 0807ffe4ef6..d3e4c96ab40 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -304,7 +304,7 @@ This keeps one safety boundary for profile hot-switching and avoids separate tra Intent: this does not replace legacy manual behavior. Legacy remains available and selectable. -With `manual_vtol_transition_controller = ON`: +With `mixer_vtol_manualswitch_autotransition_controller = ON`: - `MIXER TRANSITION` acts as an edge-triggered request. - A rising edge starts one transition. - Transition then runs autonomously to completion. @@ -312,7 +312,7 @@ With `manual_vtol_transition_controller = ON`: - To start another transition, mode must go OFF then ON again. - If mode is turned OFF before hot-switch, transition request is aborted safely. -With `manual_vtol_transition_controller = OFF`: +With `mixer_vtol_manualswitch_autotransition_controller = OFF`: - legacy manual behavior is preserved for backward compatibility. Typical 3-position switch workflow (edge-trigger mode enabled): @@ -350,25 +350,25 @@ FW -> MC: - completion threshold: `vtol_transition_to_mc_max_airspeed_cm_s` Timeout: -- `vtol_transition_airspeed_timeout_ms` can abort transition if condition is not achieved in time. +- `mixer_vtol_transition_airspeed_timeout_ms` can abort transition if condition is not achieved in time. ### Dynamic mixer scaling -When `vtol_transition_dynamic_mixer = ON`, transition progress additionally scales: +When `mixer_vtol_transition_dynamic_mixer = ON`, transition progress additionally scales: - pusher transition contribution, - vertical lift contribution, - MC stabilization authority, - FW transition input authority blend. -When `vtol_transition_dynamic_mixer = OFF`, legacy static transition mixing behavior is preserved. +When `mixer_vtol_transition_dynamic_mixer = OFF`, legacy static transition mixing behavior is preserved. Optional decoupled scaling ramp: -- `vtol_transition_scale_ramp_time_ms = 0` (default): scaling follows transition progress (legacy-compatible behavior). -- `vtol_transition_scale_ramp_time_ms > 0`: scaling uses this ramp timer, while completion logic remains unchanged (airspeed-first; timer fallback when pitot is unavailable/unhealthy). +- `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): scaling follows transition progress (legacy-compatible behavior). +- `mixer_vtol_transition_scale_ramp_time_ms > 0`: scaling uses this ramp timer, while completion logic remains unchanged (airspeed-first; timer fallback when pitot is unavailable/unhealthy). Example: - `mixer_switch_trans_timer = 50` (5s fallback completion timer) -- `vtol_transition_scale_ramp_time_ms = 1200` +- `mixer_vtol_transition_scale_ramp_time_ms = 1200` Result: - pusher/lift/authority scaling reaches target levels in ~1.2s, @@ -376,14 +376,14 @@ Result: - if pitot is unavailable/unhealthy, completion fallback still uses 5s. Optional smooth-start baseline: -- `vtol_transition_dynamic_mixer = ON` +- `mixer_vtol_transition_dynamic_mixer = ON` - `vtol_transition_lift_end_percent = 30` - `vtol_transition_mc_authority_end_percent = 20` - `vtol_transition_fw_authority_start_percent = 20` ### Detailed effect of the three percentage settings -These three settings are active only when `vtol_transition_dynamic_mixer = ON`. +These three settings are active only when `mixer_vtol_transition_dynamic_mixer = ON`. 1. `vtol_transition_lift_end_percent` - Defines lift throttle scale at transition end. @@ -416,14 +416,43 @@ Backward-compatible note: - `vtol_transition_fw_authority_start_percent = 100` preserves legacy FW authority handoff behavior. - Lower values provide smoother FW authority ramp-in/out. +## Setting Scope (Important) + +The new VTOL settings are split into two groups: + +### Per-mixer-profile settings + +These can differ between mixer profile 1 (typically MC) and mixer profile 2 (typically FW): + +- `mixer_automated_switch` +- `mixer_switch_trans_timer` +- `mixer_switch_trans_airspeed_cm_s` +- `mixer_vtol_transition_dynamic_mixer` +- `mixer_vtol_manualswitch_autotransition_controller` +- `mixer_vtol_transition_airspeed_timeout_ms` +- `mixer_vtol_transition_scale_ramp_time_ms` + +### Global settings + +These are shared system-wide and are not profile-specific: + +- `vtol_transition_to_fw_min_airspeed_cm_s` +- `vtol_transition_to_mc_max_airspeed_cm_s` +- `vtol_transition_lift_end_percent` +- `vtol_transition_mc_authority_end_percent` +- `vtol_transition_fw_authority_start_percent` +- `nav_vtol_mission_transition_user_action` +- `nav_vtol_mission_transition_min_altitude_cm` +- `nav_vtol_mission_transition_track_distance_cm` + ## CLI Commands (English) Use these commands in CLI (`set ...`, then `save`): -- `set manual_vtol_transition_controller = ON|OFF` +- `set mixer_vtol_manualswitch_autotransition_controller = ON|OFF` - Enables edge-triggered manual transition controller. -- `set vtol_transition_dynamic_mixer = ON|OFF` +- `set mixer_vtol_transition_dynamic_mixer = ON|OFF` - Enables/disables dynamic progress-based scaling. - `set vtol_transition_to_fw_min_airspeed_cm_s = ` @@ -438,10 +467,10 @@ Use these commands in CLI (`set ...`, then `save`): - `set vtol_transition_to_mc_max_airspeed_cm_s = ` - FW -> MC completion threshold (pitot airspeed). -- `set vtol_transition_airspeed_timeout_ms = ` +- `set mixer_vtol_transition_airspeed_timeout_ms = ` - Transition timeout/abort window. -- `set vtol_transition_scale_ramp_time_ms = ` +- `set mixer_vtol_transition_scale_ramp_time_ms = ` - Optional dynamic scaling ramp duration in milliseconds. `0` keeps legacy progress-coupled scaling. `>0` decouples scaling ramp time from completion timing. - `set vtol_transition_lift_end_percent = <0..100>` @@ -493,7 +522,7 @@ When pitot is healthy/available, transition progress is airspeed-driven (not tim - progress = `constrain((startAirspeed - airspeed) / (startAirspeed - to_mc_threshold), 0..1)` - completion condition = `airspeed <= to_mc_threshold` -Dynamic mixer scaling (`vtol_transition_dynamic_mixer = ON`) uses this progress: +Dynamic mixer scaling (`mixer_vtol_transition_dynamic_mixer = ON`) uses this progress: - MC -> FW: - pusher scale ramps `0 -> 1` @@ -507,7 +536,7 @@ Dynamic mixer scaling (`vtol_transition_dynamic_mixer = ON`) uses this progress: - MC authority ramps `vtol_transition_mc_authority_end_percent -> 1` - FW authority ramps `1 -> vtol_transition_fw_authority_start_percent` -If `vtol_transition_scale_ramp_time_ms > 0`, dynamic scaling uses that timer-based ramp instead of transition-progress coupling. +If `mixer_vtol_transition_scale_ramp_time_ms > 0`, dynamic scaling uses that timer-based ramp instead of transition-progress coupling. This changes only scaling shape. Transition completion logic remains airspeed-first (with timer fallback when pitot is unavailable/unhealthy). For transition/pusher motors (`-2.0 < throttle < -1.0`), output is interpolated from idle to target: diff --git a/src/main/fc/config.c b/src/main/fc/config.c index d3021317ae5..23d62fa40ac 100755 --- a/src/main/fc/config.c +++ b/src/main/fc/config.c @@ -103,7 +103,7 @@ PG_RESET_TEMPLATE(featureConfig_t, featureConfig, .enabledFeatures = DEFAULT_FEATURES | COMMON_DEFAULT_FEATURES ); -PG_REGISTER_WITH_RESET_TEMPLATE(systemConfig_t, systemConfig, PG_SYSTEM_CONFIG, 7); +PG_REGISTER_WITH_RESET_TEMPLATE(systemConfig_t, systemConfig, PG_SYSTEM_CONFIG, 8); PG_RESET_TEMPLATE(systemConfig_t, systemConfig, .current_profile_index = 0, @@ -117,6 +117,11 @@ PG_RESET_TEMPLATE(systemConfig_t, systemConfig, .i2c_speed = SETTING_I2C_SPEED_DEFAULT, #endif .throttle_tilt_compensation_strength = SETTING_THROTTLE_TILT_COMP_STR_DEFAULT, // 0-100, 0 - disabled + .vtolTransitionToFwMinAirspeed = SETTING_VTOL_TRANSITION_TO_FW_MIN_AIRSPEED_CM_S_DEFAULT, + .vtolTransitionToMcMaxAirspeed = SETTING_VTOL_TRANSITION_TO_MC_MAX_AIRSPEED_CM_S_DEFAULT, + .vtolTransitionLiftEndPercent = SETTING_VTOL_TRANSITION_LIFT_END_PERCENT_DEFAULT, + .vtolTransitionMcAuthorityEndPercent = SETTING_VTOL_TRANSITION_MC_AUTHORITY_END_PERCENT_DEFAULT, + .vtolTransitionFwAuthorityStartPercent = SETTING_VTOL_TRANSITION_FW_AUTHORITY_START_PERCENT_DEFAULT, .craftName = SETTING_NAME_DEFAULT, .pilotName = SETTING_NAME_DEFAULT ); diff --git a/src/main/fc/config.h b/src/main/fc/config.h index e3bde5f3eb7..c7f2501fceb 100644 --- a/src/main/fc/config.h +++ b/src/main/fc/config.h @@ -78,6 +78,11 @@ typedef struct systemConfig_s { uint8_t i2c_speed; #endif uint8_t throttle_tilt_compensation_strength; // the correction that will be applied at throttle_correction_angle. + uint16_t vtolTransitionToFwMinAirspeed; + uint16_t vtolTransitionToMcMaxAirspeed; + uint8_t vtolTransitionLiftEndPercent; + uint8_t vtolTransitionMcAuthorityEndPercent; + uint8_t vtolTransitionFwAuthorityStartPercent; char craftName[MAX_NAME_LENGTH + 1]; char pilotName[MAX_NAME_LENGTH + 1]; } systemConfig_t; diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 9ea3b64966e..24c64b88f53 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1286,58 +1286,28 @@ groups: field: mixer_config.switchTransitionAirspeed min: 0 max: 10000 - - name: vtol_transition_dynamic_mixer + - name: mixer_vtol_transition_dynamic_mixer description: "Enables dynamic VTOL transition progress/scaling controller shared by mission-authorized and manual MIXER TRANSITION paths." default_value: OFF field: mixer_config.vtolTransitionDynamicMixer type: bool - - name: manual_vtol_transition_controller + - name: mixer_vtol_manualswitch_autotransition_controller description: "Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior." default_value: OFF field: mixer_config.manualVtolTransitionController type: bool - - name: vtol_transition_to_fw_min_airspeed_cm_s - description: "Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. Overrides `mixer_switch_trans_airspeed_cm_s` when > 0. If 0, legacy setting is used." - default_value: 0 - field: mixer_config.vtolTransitionToFwMinAirspeed - min: 0 - max: 20000 - - name: vtol_transition_to_mc_max_airspeed_cm_s - description: "Maximum pitot airspeed [cm/s] allowed to complete FW->MC transition when airspeed is healthy and available. If 0, FW->MC uses timer fallback." - default_value: 0 - field: mixer_config.vtolTransitionToMcMaxAirspeed - min: 0 - max: 20000 - - name: vtol_transition_airspeed_timeout_ms + - name: mixer_vtol_transition_airspeed_timeout_ms description: "Safety timeout [ms] for airspeed-controlled transitions. If non-zero and required airspeed condition is not met in time, transition aborts instead of force-completing." default_value: 0 field: mixer_config.vtolTransitionAirspeedTimeoutMs min: 0 max: 60000 - - name: vtol_transition_scale_ramp_time_ms - description: "Optional dynamic scaling ramp duration [ms]. When > 0 and `vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling uses this timer instead of transition completion progress. Set to 0 to keep legacy progress-coupled scaling behavior." + - name: mixer_vtol_transition_scale_ramp_time_ms + description: "Optional dynamic scaling ramp duration [ms]. When > 0 and `mixer_vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling uses this timer instead of transition completion progress. Set to 0 to keep legacy progress-coupled scaling behavior." default_value: 0 field: mixer_config.vtolTransitionScaleRampTimeMs min: 0 max: 60000 - - name: vtol_transition_lift_end_percent - description: "Target vertical-lift throttle scale at transition end, in percent. Used only when `vtol_transition_dynamic_mixer` is ON." - default_value: 100 - field: mixer_config.vtolTransitionLiftEndPercent - min: 0 - max: 100 - - name: vtol_transition_mc_authority_end_percent - description: "Target multicopter stabilization authority scale at transition end, in percent. Used only when `vtol_transition_dynamic_mixer` is ON." - default_value: 100 - field: mixer_config.vtolTransitionMcAuthorityEndPercent - min: 0 - max: 100 - - name: vtol_transition_fw_authority_start_percent - description: "Initial fixed-wing authority scale at transition start, in percent. Used only when `vtol_transition_dynamic_mixer` is ON." - default_value: 100 - field: mixer_config.vtolTransitionFwAuthorityStartPercent - min: 0 - max: 100 - name: tailsitter_orientation_offset description: "Apply a 90 deg pitch offset in sensor aliment for tailsitter flying mode" default_value: OFF @@ -4035,6 +4005,36 @@ groups: field: throttle_tilt_compensation_strength min: 0 max: 100 + - name: vtol_transition_to_fw_min_airspeed_cm_s + description: "Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. Overrides `mixer_switch_trans_airspeed_cm_s` when > 0. If 0, legacy setting is used." + default_value: 0 + field: vtolTransitionToFwMinAirspeed + min: 0 + max: 20000 + - name: vtol_transition_to_mc_max_airspeed_cm_s + description: "Maximum pitot airspeed [cm/s] allowed to complete FW->MC transition when airspeed is healthy and available. If 0, FW->MC uses timer fallback." + default_value: 0 + field: vtolTransitionToMcMaxAirspeed + min: 0 + max: 20000 + - name: vtol_transition_lift_end_percent + description: "Target vertical-lift throttle scale at transition end, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." + default_value: 100 + field: vtolTransitionLiftEndPercent + min: 0 + max: 100 + - name: vtol_transition_mc_authority_end_percent + description: "Target multicopter stabilization authority scale at transition end, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." + default_value: 100 + field: vtolTransitionMcAuthorityEndPercent + min: 0 + max: 100 + - name: vtol_transition_fw_authority_start_percent + description: "Initial fixed-wing authority scale at transition start, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." + default_value: 100 + field: vtolTransitionFwAuthorityStartPercent + min: 0 + max: 100 - name: name description: "Craft name" default_value: "" diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 47d5632762c..0fbce4deba5 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -42,7 +42,7 @@ int nextMixerProfileIndex; static bool manualTransitionModeWasActive; static bool manualTransitionReadyForEdge = true; -PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 2); +PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 3); void pgResetFn_mixerProfiles(mixerProfile_t *instance) { @@ -59,15 +59,10 @@ void pgResetFn_mixerProfiles(mixerProfile_t *instance) .automated_switch = SETTING_MIXER_AUTOMATED_SWITCH_DEFAULT, .switchTransitionTimer = SETTING_MIXER_SWITCH_TRANS_TIMER_DEFAULT, .switchTransitionAirspeed = SETTING_MIXER_SWITCH_TRANS_AIRSPEED_CM_S_DEFAULT, - .vtolTransitionDynamicMixer = SETTING_VTOL_TRANSITION_DYNAMIC_MIXER_DEFAULT, - .manualVtolTransitionController = SETTING_MANUAL_VTOL_TRANSITION_CONTROLLER_DEFAULT, - .vtolTransitionToFwMinAirspeed = SETTING_VTOL_TRANSITION_TO_FW_MIN_AIRSPEED_CM_S_DEFAULT, - .vtolTransitionToMcMaxAirspeed = SETTING_VTOL_TRANSITION_TO_MC_MAX_AIRSPEED_CM_S_DEFAULT, - .vtolTransitionAirspeedTimeoutMs = SETTING_VTOL_TRANSITION_AIRSPEED_TIMEOUT_MS_DEFAULT, - .vtolTransitionScaleRampTimeMs = SETTING_VTOL_TRANSITION_SCALE_RAMP_TIME_MS_DEFAULT, - .vtolTransitionLiftEndPercent = SETTING_VTOL_TRANSITION_LIFT_END_PERCENT_DEFAULT, - .vtolTransitionMcAuthorityEndPercent = SETTING_VTOL_TRANSITION_MC_AUTHORITY_END_PERCENT_DEFAULT, - .vtolTransitionFwAuthorityStartPercent = SETTING_VTOL_TRANSITION_FW_AUTHORITY_START_PERCENT_DEFAULT, + .vtolTransitionDynamicMixer = SETTING_MIXER_VTOL_TRANSITION_DYNAMIC_MIXER_DEFAULT, + .manualVtolTransitionController = SETTING_MIXER_VTOL_MANUALSWITCH_AUTOTRANSITION_CONTROLLER_DEFAULT, + .vtolTransitionAirspeedTimeoutMs = SETTING_MIXER_VTOL_TRANSITION_AIRSPEED_TIMEOUT_MS_DEFAULT, + .vtolTransitionScaleRampTimeMs = SETTING_MIXER_VTOL_TRANSITION_SCALE_RAMP_TIME_MS_DEFAULT, .tailsitterOrientationOffset = SETTING_TAILSITTER_ORIENTATION_OFFSET_DEFAULT, .transition_PID_mmix_multiplier_roll = SETTING_TRANSITION_PID_MMIX_MULTIPLIER_ROLL_DEFAULT, .transition_PID_mmix_multiplier_pitch = SETTING_TRANSITION_PID_MMIX_MULTIPLIER_PITCH_DEFAULT, @@ -226,14 +221,14 @@ static bool hasTrustedPitotAirspeed(float *airspeedCmS) static uint16_t getAirspeedThresholdForDirection(const mixerProfileATDirection_e direction) { if (direction == MIXERAT_DIRECTION_TO_FW) { - if (currentMixerConfig.vtolTransitionToFwMinAirspeed > 0) { - return currentMixerConfig.vtolTransitionToFwMinAirspeed; + if (systemConfig()->vtolTransitionToFwMinAirspeed > 0) { + return systemConfig()->vtolTransitionToFwMinAirspeed; } return currentMixerConfig.switchTransitionAirspeed; } if (direction == MIXERAT_DIRECTION_TO_MC) { - return currentMixerConfig.vtolTransitionToMcMaxAirspeed; + return systemConfig()->vtolTransitionToMcMaxAirspeed; } return 0; @@ -250,9 +245,9 @@ static void updateTransitionScales(void) return; } - const float liftFloor = constrainf(currentMixerConfig.vtolTransitionLiftEndPercent / 100.0f, 0.0f, 1.0f); - const float mcFloor = constrainf(currentMixerConfig.vtolTransitionMcAuthorityEndPercent / 100.0f, 0.0f, 1.0f); - const float fwFloor = constrainf(currentMixerConfig.vtolTransitionFwAuthorityStartPercent / 100.0f, 0.0f, 1.0f); + const float liftFloor = constrainf(systemConfig()->vtolTransitionLiftEndPercent / 100.0f, 0.0f, 1.0f); + const float mcFloor = constrainf(systemConfig()->vtolTransitionMcAuthorityEndPercent / 100.0f, 0.0f, 1.0f); + const float fwFloor = constrainf(systemConfig()->vtolTransitionFwAuthorityStartPercent / 100.0f, 0.0f, 1.0f); const float scaleProgress = getScalingProgress(); if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW) { diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index 784463eba7d..1107ca030aa 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -21,13 +21,8 @@ typedef struct mixerConfig_s { uint16_t switchTransitionAirspeed; bool vtolTransitionDynamicMixer; bool manualVtolTransitionController; - uint16_t vtolTransitionToFwMinAirspeed; - uint16_t vtolTransitionToMcMaxAirspeed; uint16_t vtolTransitionAirspeedTimeoutMs; uint16_t vtolTransitionScaleRampTimeMs; - uint8_t vtolTransitionLiftEndPercent; - uint8_t vtolTransitionMcAuthorityEndPercent; - uint8_t vtolTransitionFwAuthorityStartPercent; bool tailsitterOrientationOffset; int16_t transition_PID_mmix_multiplier_roll; int16_t transition_PID_mmix_multiplier_pitch; From 587460e90a0cf848cf5ff30a9b7d5e3d96c841ac Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Sun, 17 May 2026 12:14:50 +0300 Subject: [PATCH 08/26] vtol: clean transition state internals and clarify manual switch setup - remove unused automated-transition artifacts: - drop MIXERAT_PHASE_DONE - drop unused mixerProfileAT fields (transitionInputMixing, transitionStabEndTime, transitionTransEndTime) - fix transition finalize ordering: - apply final progress/scaling before profile hot-switch - avoid final scale computation using post-switch mixer profile config - document airspeed-timeout behavior explicitly: - mixer_vtol_transition_airspeed_timeout_ms applies only in trusted pitot (airspeed-controlled) path - timer fallback path uses mixer_switch_trans_timer when pitot is unavailable/unhealthy - update VTOL and MixerProfile docs with practical test presets: - three English test profiles for VTOL ~1.0m wingspan / ~1720g AUW - legacy-compatible baseline - airspeed-first dynamic scaling - mission-authorized transition integration - add explicit safety guidance for manual RC setup: - require dedicated 3-position switch mapping - warn that overlapping MIXER PROFILE 2 and MIXER TRANSITION modes can cause order-dependent, unpredictable behavior --- docs/MixerProfile.md | 68 +++++++++++++++++++++++- docs/VTOL.md | 94 +++++++++++++++++++++++++++++++-- src/main/flight/mixer_profile.c | 7 +-- src/main/flight/mixer_profile.h | 4 -- 4 files changed, 158 insertions(+), 15 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 60f103eb6e4..11ee50b7494 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -33,6 +33,15 @@ The use of Transition Mode is recommended to enable further features and future This edge-triggered behavior is enabled by `mixer_vtol_manualswitch_autotransition_controller`. When `mixer_vtol_manualswitch_autotransition_controller = OFF`, manual transition keeps legacy behavior. +Recommended switch topology (explicit): +- Use a dedicated 3-position mapping: + - Pos1 = MC (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) + - Pos2 = Transition trigger (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) + - Pos3 = FW (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) +- Avoid overlapping FW selection and transition trigger in the same position. +- Avoid 2-position setups where one position activates both `MIXER PROFILE 2` and `MIXER TRANSITION`. +- Overlapping mode activation can produce order-dependent behavior (direct profile switch path vs transition-controller path), which is unpredictable and not recommended. + ## Servo `Mixer Transition` is the input source for transition input; use this to tilt motor to gain airspeed. @@ -105,6 +114,8 @@ Ground speed is not used for transition completion/progress. Optional safety timeout: - `mixer_vtol_transition_airspeed_timeout_ms` can abort transition if airspeed condition is not met in time. +- This timeout is only active while transition completion is using trusted pitot airspeed. +- If pitot is unavailable/unhealthy, transition completion falls back to `mixer_switch_trans_timer`; timeout does not force abort in that fallback path. ### Dynamic scaling (optional) @@ -179,11 +190,66 @@ Mission path uses the same controller and completion logic as manual transition Manual RC switching (`MIXER PROFILE 2`, `MIXER TRANSITION`) remains blocked during normal active navigation. Mission VTOL transition does not bypass the hot-switch safety guard; it only authorizes switching inside the automated transition state. Mission VTOL transition still relies on normal profile-switch infrastructure: configure two mixer profiles and a valid `MIXER PROFILE 2` mode activation condition. -Example smooth-start values (optional tuning baseline): +### Example test presets (VTOL ~1.0m wingspan, ~1720g AUW) + +These are practical starting points for first validation flights. They are examples, not universal defaults. + +#### Test 1 - Legacy-compatible baseline (manual transition check) + +CLI: +- `set mixer_vtol_manualswitch_autotransition_controller = ON` +- `set mixer_vtol_transition_dynamic_mixer = OFF` +- `set mixer_switch_trans_timer = 45` +- `set vtol_transition_to_fw_min_airspeed_cm_s = 0` +- `set mixer_switch_trans_airspeed_cm_s = 0` +- `set vtol_transition_to_mc_max_airspeed_cm_s = 900` +- `set mixer_vtol_transition_airspeed_timeout_ms = 0` +- `set mixer_vtol_transition_scale_ramp_time_ms = 0` +- `set nav_vtol_mission_transition_user_action = OFF` + +Behavior: +- Preserves legacy-style transition mixing while still using the new controller path. +- Useful as a known-safe baseline before enabling dynamic scaling. + +#### Test 2 - Airspeed-first + dynamic scaling (manual tuning) + +CLI: +- `set mixer_vtol_manualswitch_autotransition_controller = ON` - `set mixer_vtol_transition_dynamic_mixer = ON` +- `set vtol_transition_to_fw_min_airspeed_cm_s = 1300` +- `set vtol_transition_to_mc_max_airspeed_cm_s = 850` +- `set mixer_switch_trans_timer = 50` +- `set mixer_vtol_transition_airspeed_timeout_ms = 6500` +- `set mixer_vtol_transition_scale_ramp_time_ms = 1200` - `set vtol_transition_lift_end_percent = 30` - `set vtol_transition_mc_authority_end_percent = 20` - `set vtol_transition_fw_authority_start_percent = 20` +- `set nav_vtol_mission_transition_user_action = OFF` + +Behavior: +- Uses pitot-first completion logic with timer fallback only when pitot is unavailable/unhealthy. +- Adds fast, smooth pusher/lift/authority ramping to reduce abrupt transitions. + +#### Test 3 - Mission-authorized transition (mission integration) + +CLI: +- `set mixer_vtol_manualswitch_autotransition_controller = ON` +- `set mixer_vtol_transition_dynamic_mixer = ON` +- `set vtol_transition_to_fw_min_airspeed_cm_s = 1300` +- `set vtol_transition_to_mc_max_airspeed_cm_s = 850` +- `set mixer_switch_trans_timer = 50` +- `set mixer_vtol_transition_airspeed_timeout_ms = 6500` +- `set mixer_vtol_transition_scale_ramp_time_ms = 1200` +- `set vtol_transition_lift_end_percent = 30` +- `set vtol_transition_mc_authority_end_percent = 20` +- `set vtol_transition_fw_authority_start_percent = 20` +- `set nav_vtol_mission_transition_user_action = USER1` +- `set nav_vtol_mission_transition_min_altitude_cm = 1200` +- `set nav_vtol_mission_transition_track_distance_cm = 4000` + +Behavior: +- Uses USER1 as per-waypoint absolute target selector (clear=MC, set=FW). +- Pauses mission progression during transition and resumes only after transition completion. ### Validation Matrix (PR / SITL / HITL) diff --git a/docs/VTOL.md b/docs/VTOL.md index d3e4c96ab40..d8be0cad35e 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -324,6 +324,15 @@ Operational example: - fly in MC (pos1) -> move to Transition (pos2) to start automatic MC->FW transition -> after completion move to FW (pos3), - reverse order for FW->MC. +Important RC mapping constraint: +- Use a dedicated 3-position mapping where: + - Pos1 = MC (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) + - Pos2 = Transition trigger (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) + - Pos3 = FW (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) +- Do not overlap/merge FW selection and transition trigger in the same switch position. +- Do not use a 2-position mapping where one position enables both `MIXER PROFILE 2` and `MIXER TRANSITION`. +- Mixing these mode conditions can cause race/order-dependent behavior (direct profile switch versus transition state machine), which is unpredictable in flight. + ### Mission-authorized transition semantics Mission transition is configured with `nav_vtol_mission_transition_user_action`. @@ -351,6 +360,8 @@ FW -> MC: Timeout: - `mixer_vtol_transition_airspeed_timeout_ms` can abort transition if condition is not achieved in time. +- This timeout is applied only while the transition is airspeed-controlled (trusted pitot in use). +- If pitot becomes unavailable/unhealthy, completion falls back to `mixer_switch_trans_timer` and this timeout no longer drives the decision. ### Dynamic mixer scaling @@ -375,11 +386,84 @@ Result: - transition completion still follows airspeed thresholds when pitot is healthy, - if pitot is unavailable/unhealthy, completion fallback still uses 5s. -Optional smooth-start baseline: -- `mixer_vtol_transition_dynamic_mixer = ON` -- `vtol_transition_lift_end_percent = 30` -- `vtol_transition_mc_authority_end_percent = 20` -- `vtol_transition_fw_authority_start_percent = 20` +### Example test presets (VTOL ~1.0m wingspan, ~1720g AUW) + +These are example starting points for initial testing. They are not universal values; tune after bench tests and short flight tests. + +#### Test 1 - Legacy-compatible baseline (manual transition check) + +Goal: +- Verify that the new controller does not change legacy behavior when dynamic scaling is disabled. +- Good first test after flashing. + +CLI: +- `set mixer_vtol_manualswitch_autotransition_controller = ON` +- `set mixer_vtol_transition_dynamic_mixer = OFF` +- `set mixer_switch_trans_timer = 45` +- `set vtol_transition_to_fw_min_airspeed_cm_s = 0` +- `set mixer_switch_trans_airspeed_cm_s = 0` +- `set vtol_transition_to_mc_max_airspeed_cm_s = 900` +- `set mixer_vtol_transition_airspeed_timeout_ms = 0` +- `set mixer_vtol_transition_scale_ramp_time_ms = 0` +- `set nav_vtol_mission_transition_user_action = OFF` + +What this does: +- Keeps transition mixing behavior close to legacy mode. +- Uses timer-driven completion when no trusted pitot threshold is configured. +- Uses conservative FW->MC completion threshold. +- Disables mission-authorized transition while validating manual behavior. + +#### Test 2 - Airspeed-first + dynamic scaling (manual transition tuning) + +Goal: +- Enable the full new behavior: airspeed-first completion and smooth authority/pusher scaling. + +CLI: +- `set mixer_vtol_manualswitch_autotransition_controller = ON` +- `set mixer_vtol_transition_dynamic_mixer = ON` +- `set vtol_transition_to_fw_min_airspeed_cm_s = 1300` +- `set vtol_transition_to_mc_max_airspeed_cm_s = 850` +- `set mixer_switch_trans_timer = 50` +- `set mixer_vtol_transition_airspeed_timeout_ms = 6500` +- `set mixer_vtol_transition_scale_ramp_time_ms = 1200` +- `set vtol_transition_lift_end_percent = 30` +- `set vtol_transition_mc_authority_end_percent = 20` +- `set vtol_transition_fw_authority_start_percent = 20` +- `set nav_vtol_mission_transition_user_action = OFF` + +What this does: +- MC->FW completes primarily on pitot airspeed (1300 cm/s), with timer fallback only if pitot is unavailable/unhealthy. +- FW->MC completes when airspeed drops to 850 cm/s. +- Scaling ramps quickly (1.2 s) to reduce step torque and abrupt authority handoff. +- Timeout abort protects against staying too long in airspeed-controlled transition without reaching threshold. + +#### Test 3 - Mission-authorized transition (end-to-end mission flow) + +Goal: +- Validate mission User Action integration and pause/resume behavior. + +CLI: +- `set mixer_vtol_manualswitch_autotransition_controller = ON` +- `set mixer_vtol_transition_dynamic_mixer = ON` +- `set vtol_transition_to_fw_min_airspeed_cm_s = 1300` +- `set vtol_transition_to_mc_max_airspeed_cm_s = 850` +- `set mixer_switch_trans_timer = 50` +- `set mixer_vtol_transition_airspeed_timeout_ms = 6500` +- `set mixer_vtol_transition_scale_ramp_time_ms = 1200` +- `set vtol_transition_lift_end_percent = 30` +- `set vtol_transition_mc_authority_end_percent = 20` +- `set vtol_transition_fw_authority_start_percent = 20` +- `set nav_vtol_mission_transition_user_action = USER1` +- `set nav_vtol_mission_transition_min_altitude_cm = 1200` +- `set nav_vtol_mission_transition_track_distance_cm = 4000` + +What this does: +- Uses USER1 as the absolute per-waypoint target selector: + - USER1 bit clear -> target MC + - USER1 bit set -> target FW +- Pauses mission progression during transition and resumes after completion. +- Uses straight MC->FW acceleration segment (no loiter) with a 40 m transition track distance. +- Adds a minimum altitude gate (12 m) before mission transition starts. ### Detailed effect of the three percentage settings diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 0fbce4deba5..504fcaa918a 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -119,11 +119,8 @@ void mixerConfigInit(void) void setMixerProfileAT(void) { const timeMs_t now = millis(); - const uint32_t transitionDurationMs = MAX(0, currentMixerConfig.switchTransitionTimer) * 100; mixerProfileAT.transitionStartTime = now; - mixerProfileAT.transitionStabEndTime = now; - mixerProfileAT.transitionTransEndTime = now + transitionDurationMs; mixerProfileAT.aborted = false; mixerProfileAT.hotSwitchDone = false; mixerProfileAT.usedAirspeed = false; @@ -421,13 +418,13 @@ bool mixerATUpdateState(mixerProfileATRequest_e required_action) if (mixerATReadyForHotSwitch(mixerProfileAT.request)) { isMixerTransitionMixing_requested = false; + mixerProfileAT.progress = 1.0f; + updateTransitionScales(); if (!outputProfileHotSwitch(nextMixerProfileIndex)) { abortTransition(); return true; } mixerProfileAT.hotSwitchDone = true; - mixerProfileAT.progress = 1.0f; - updateTransitionScales(); mixerProfileAT.phase = MIXERAT_PHASE_IDLE; mixerProfileAT.request = MIXERAT_REQUEST_NONE; mixerProfileAT.direction = MIXERAT_DIRECTION_NONE; diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index 1107ca030aa..f5eec0ff9db 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -57,14 +57,12 @@ typedef enum { MIXERAT_PHASE_IDLE, MIXERAT_PHASE_TRANSITION_INITIALIZE, MIXERAT_PHASE_TRANSITIONING, - MIXERAT_PHASE_DONE, } mixerProfileATState_e; typedef struct mixerProfileAT_s { mixerProfileATState_e phase; mixerProfileATDirection_e direction; mixerProfileATRequest_e request; - bool transitionInputMixing; bool aborted; bool hotSwitchDone; bool usedAirspeed; @@ -77,8 +75,6 @@ typedef struct mixerProfileAT_s { float mcAuthorityScale; float fwAuthorityScale; timeMs_t transitionStartTime; - timeMs_t transitionStabEndTime; - timeMs_t transitionTransEndTime; } mixerProfileAT_t; extern mixerProfileAT_t mixerProfileAT; bool checkMixerATRequired(mixerProfileATRequest_e required_action); From a73bc707a2c04ed3eaae74027d349995f7acc497 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Sun, 17 May 2026 12:31:26 +0300 Subject: [PATCH 09/26] vtol: harden transition controller docs and per-loop mode handling - recompute manual transition-controller enable flag after potential direct MIXER PROFILE 2 hot-switch in outputProfileUpdateTask(), so per-profile manual-controller config is applied with current profile context --- docs/MixerProfile.md | 5 +++++ docs/VTOL.md | 3 +++ src/main/flight/mixer_profile.c | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 11ee50b7494..e222b72c7a6 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -33,6 +33,10 @@ The use of Transition Mode is recommended to enable further features and future This edge-triggered behavior is enabled by `mixer_vtol_manualswitch_autotransition_controller`. When `mixer_vtol_manualswitch_autotransition_controller = OFF`, manual transition keeps legacy behavior. +Important path split: +- `MIXER PROFILE 2` remains a direct manual profile-switch path. +- Smooth VTOL transition state-machine behavior is triggered by `MIXER TRANSITION` when `mixer_vtol_manualswitch_autotransition_controller = ON`. + Recommended switch topology (explicit): - Use a dedicated 3-position mapping: - Pos1 = MC (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) @@ -116,6 +120,7 @@ Optional safety timeout: - `mixer_vtol_transition_airspeed_timeout_ms` can abort transition if airspeed condition is not met in time. - This timeout is only active while transition completion is using trusted pitot airspeed. - If pitot is unavailable/unhealthy, transition completion falls back to `mixer_switch_trans_timer`; timeout does not force abort in that fallback path. +- For airspeed-first setups, configure non-zero `mixer_switch_trans_timer` fallback (typical `40..60`, i.e. `4..6s`) so pitot-loss fallback does not complete immediately. ### Dynamic scaling (optional) diff --git a/docs/VTOL.md b/docs/VTOL.md index d8be0cad35e..2886fb87abf 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -299,6 +299,8 @@ This keeps one safety boundary for profile hot-switching and avoids separate tra - Ground speed is not used for transition completion. - Mission transition uses the same controller and does not directly manipulate motors. - Manual `MIXER PROFILE` / `MIXER TRANSITION` bypass during normal waypoint navigation is still blocked. +- `MIXER PROFILE 2` remains a direct profile-switch path when used manually. +- Smooth/automatic transition behavior is triggered by `MIXER TRANSITION` (with manual auto-controller ON) or by mission-authorized transition requests. ### Manual transition semantics @@ -362,6 +364,7 @@ Timeout: - `mixer_vtol_transition_airspeed_timeout_ms` can abort transition if condition is not achieved in time. - This timeout is applied only while the transition is airspeed-controlled (trusted pitot in use). - If pitot becomes unavailable/unhealthy, completion falls back to `mixer_switch_trans_timer` and this timeout no longer drives the decision. +- For airspeed-first setups, configure a non-zero `mixer_switch_trans_timer` fallback (typical: `40..60`, i.e. `4..6s`) to avoid immediate fallback completion when pitot is unavailable and timer fallback becomes active. ### Dynamic mixer scaling diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 504fcaa918a..efcfd4b2b78 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -469,7 +469,7 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) const bool manualTransitionAllowed = (posControl.navState == NAV_STATE_IDLE) || (posControl.navState == NAV_STATE_ALTHOLD_IN_PROGRESS); const bool missionActive = (navGetCurrentStateFlags() & NAV_AUTO_WP) != 0; - const bool manualControllerEnabled = currentMixerConfig.manualVtolTransitionController && !missionActive; + bool manualControllerEnabled = false; if (mixerAT_inuse && (!ARMING_FLAG(ARMED) || FLIGHT_MODE(FAILSAFE_MODE) || areSensorsCalibrating())) { abortTransition(); @@ -484,6 +484,9 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) } } + // Recompute after potential direct profile hot-switch because this flag is per-mixer-profile. + manualControllerEnabled = currentMixerConfig.manualVtolTransitionController && !missionActive; + if (!manualControllerEnabled) { // Backward-compatible manual path: level-controlled transition mixing request. if (!FLIGHT_MODE(FAILSAFE_MODE) && (!mixerAT_inuse)) { From 421a55c8d2e111d11ee4d164dbc6eb4c1761a01b Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Sun, 17 May 2026 12:52:49 +0300 Subject: [PATCH 10/26] vtol: add dedicated transition debug mode and clarify transition paths - add new debug mode `VTOL_TRANSITION` - extend debug enum with `DEBUG_VTOL_TRANSITION` - register CLI/debug name `VTOL_TRANSITION` - add settings table entry for `debug_mode` - instrument VTOL transition controller in mixer_profile task loop: - debug[0] = phase - debug[1] = request - debug[2] = direction - debug[3] = progress x1000 - debug[4] = pusherScale x1000 - debug[5] = liftScale x1000 - debug[6] = blendToFw x1000 - debug[7] = flags bitfield (active, usedAirspeed, hotSwitchDone, aborted) - improve controller consistency: - recompute manual transition-controller enable flag after potential direct MIXER PROFILE 2 hot-switch so per-profile setting is evaluated in current context - docs updates (VTOL/MixerProfile): - clarify direct `MIXER PROFILE 2` path vs controller-driven `MIXER TRANSITION` path - document airspeed-timeout scope (airspeed-controlled path only) - recommend non-zero `mixer_switch_trans_timer` fallback for airspeed-first setups - add explicit 3-position switch mapping warning and note that overlapping PROFILE2/TRANSITION activation is order-dependent and unpredictable - add VTOL transition debug mode usage and channel map - `docs/Settings.md` after debug mode table update --- docs/MixerProfile.md | 25 ++++++++++++++++++++++++- docs/Settings.md | 1 + src/main/build/debug.h | 1 + src/main/fc/cli.c | 3 ++- src/main/fc/settings.yaml | 2 +- src/main/flight/mixer_profile.c | 18 ++++++++++++++++++ 6 files changed, 47 insertions(+), 3 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index e222b72c7a6..30a2f43a6b7 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -99,7 +99,8 @@ When `mixer_automated_switch`:`OFF` is set for all mixer_profiles(defaults). Mod ### Unified VTOL transition controller Manual `MIXER TRANSITION` and mission-authorized VTOL transition both use the same internal transition controller. -This controller always computes transition progress/completion and performs profile hot-switch only inside the authorized transition state. +This controller always computes transition progress/completion and performs its own profile hot-switch only inside the authorized transition state. +Direct manual `MIXER PROFILE 2` switching remains a separate path when no transition controller path is active. When `mixer_vtol_transition_dynamic_mixer = ON`, pusher/lift/authority scaling is enabled and is driven by: - transition progress (default), or - `mixer_vtol_transition_scale_ramp_time_ms` when configured (>0). @@ -268,6 +269,28 @@ Behavior: - Mission transition with selected USER bit = `0` (TO_MC). - Failsafe/disarm during active transition (abort and no blind mission resume). +### VTOL transition debug mode (Blackbox / OSD debug) + +For transition troubleshooting, use: + +- `set debug_mode = VTOL_TRANSITION` +- `save` + +Debug channels: + +- `debug[0]` = transition phase (`0=IDLE`, `1=TRANSITION_INITIALIZE`, `2=TRANSITIONING`) +- `debug[1]` = active request (`MIXERAT_REQUEST_*` enum value) +- `debug[2]` = direction (`0=NONE`, `1=TO_FW`, `2=TO_MC`) +- `debug[3]` = progress x1000 (`0..1000`) +- `debug[4]` = pusher scale x1000 (`0..1000`) +- `debug[5]` = lift scale x1000 (`0..1000`) +- `debug[6]` = FW blend/authority scale x1000 (`0..1000`) +- `debug[7]` = flags bitfield: + - bit0: transition active + - bit1: airspeed-controlled path in use + - bit2: hot-switch done + - bit3: transition aborted + ## TailSitter (planned for INAV 7.1) TailSitter is supported by add a 90deg offset to the board alignment. Set the board aliment normally in the mixer_profile for FW mode(`set platform_type = AIRPLANE`), The motor trust axis should be same direction as the airplane nose. Then, in the mixer_profile for takeoff and landing set `tailsitter_orientation_offset = ON ` to apply orientation offset. orientation offset will also add a 45deg orientation offset. diff --git a/docs/Settings.md b/docs/Settings.md index 7d7c0e06f18..189cbb36eaa 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -782,6 +782,7 @@ Defines debug values exposed in debug variables (developer / debugging setting) | LULU | | | SBUS2 | | | OSD_REFRESH | | +| VTOL_TRANSITION | | --- diff --git a/src/main/build/debug.h b/src/main/build/debug.h index b33868af8b2..8e585d299ab 100644 --- a/src/main/build/debug.h +++ b/src/main/build/debug.h @@ -80,6 +80,7 @@ typedef enum { DEBUG_LULU, DEBUG_SBUS2, DEBUG_OSD_REFRESH, + DEBUG_VTOL_TRANSITION, DEBUG_COUNT // also update debugModeNames in cli.c } debugType_e; diff --git a/src/main/fc/cli.c b/src/main/fc/cli.c index 6f809432648..44845cd313f 100644 --- a/src/main/fc/cli.c +++ b/src/main/fc/cli.c @@ -223,7 +223,8 @@ static const char *debugModeNames[DEBUG_COUNT] = { "GPS", "LULU", "SBUS2", - "OSD_REFRESH" + "OSD_REFRESH", + "VTOL_TRANSITION" }; /* Sensor names (used in lookup tables for *_hardware settings and in status diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 24c64b88f53..b355b7b1969 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -84,7 +84,7 @@ tables: "VIBE", "CRUISE", "REM_FLIGHT_TIME", "SMARTAUDIO", "ACC", "NAV_YAW", "PCF8574", "DYN_GYRO_LPF", "AUTOLEVEL", "ALTITUDE", "AUTOTRIM", "AUTOTUNE", "RATE_DYNAMICS", "LANDING", "POS_EST", - "ADAPTIVE_FILTER", "HEADTRACKER", "GPS", "LULU", "SBUS2", "OSD_REFRESH"] + "ADAPTIVE_FILTER", "HEADTRACKER", "GPS", "LULU", "SBUS2", "OSD_REFRESH", "VTOL_TRANSITION"] - name: aux_operator values: ["OR", "AND"] enum: modeActivationOperator_e diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index efcfd4b2b78..881024e7608 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -32,6 +32,7 @@ #include "navigation/navigation.h" #include "common/log.h" +#include "build/debug.h" mixerConfig_t currentMixerConfig; int currentMixerProfileIndex; @@ -535,6 +536,23 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) if (!isMixerTransitionMixing) { resetTransitionScales(); } + + // VTOL transition debug channels (DEBUG_VTOL_TRANSITION): + // [0] phase, [1] request, [2] direction, [3] progress x1000, + // [4] pusherScale x1000, [5] liftScale x1000, [6] fwBlend x1000, + // [7] flags bitfield: bit0 active, bit1 usedAirspeed, bit2 hotSwitchDone, bit3 aborted + DEBUG_SET(DEBUG_VTOL_TRANSITION, 0, mixerProfileAT.phase); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 1, mixerProfileAT.request); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 2, mixerProfileAT.direction); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 3, lrintf(constrainf(mixerProfileAT.progress, 0.0f, 1.0f) * 1000.0f)); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 4, lrintf(constrainf(mixerProfileAT.pusherScale, 0.0f, 1.0f) * 1000.0f)); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 5, lrintf(constrainf(mixerProfileAT.liftScale, 0.0f, 1.0f) * 1000.0f)); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 6, lrintf(constrainf(mixerProfileAT.blendToFw, 0.0f, 1.0f) * 1000.0f)); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 7, + (mixerATIsActive() ? 1 : 0) | + (mixerProfileAT.usedAirspeed ? 1 << 1 : 0) | + (mixerProfileAT.hotSwitchDone ? 1 << 2 : 0) | + (mixerProfileAT.aborted ? 1 << 3 : 0)); } bool mixerATIsActive(void) From 73707a95d4ed202a2ea2e44d985d1ad7f5aa11a1 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Sun, 17 May 2026 13:35:04 +0300 Subject: [PATCH 11/26] - capture debug values before transition scale cleanup so final successful transition frames remain visible in Blackbox - document VTOL debug mode usage and debug channel meanings --- docs/MixerProfile.md | 10 +++++----- src/main/flight/mixer_profile.c | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 30a2f43a6b7..7e5b802384c 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -284,12 +284,12 @@ Debug channels: - `debug[3]` = progress x1000 (`0..1000`) - `debug[4]` = pusher scale x1000 (`0..1000`) - `debug[5]` = lift scale x1000 (`0..1000`) -- `debug[6]` = FW blend/authority scale x1000 (`0..1000`) +- `debug[6]` = FW authority/blend scale x1000 (`0..1000`) - `debug[7]` = flags bitfield: - - bit0: transition active - - bit1: airspeed-controlled path in use - - bit2: hot-switch done - - bit3: transition aborted + - bit0: transition active + - bit1: airspeed-controlled path in use + - bit2: hot-switch done + - bit3: transition aborted ## TailSitter (planned for INAV 7.1) TailSitter is supported by add a 90deg offset to the board alignment. Set the board aliment normally in the mixer_profile for FW mode(`set platform_type = AIRPLANE`), The motor trust axis should be same direction as the airplane nose. Then, in the mixer_profile for takeoff and landing set `tailsitter_orientation_offset = ON ` to apply orientation offset. orientation offset will also add a 45deg orientation offset. diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 881024e7608..d330d931949 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -533,13 +533,9 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) isMixerTransitionMixing = isMixerTransitionMixing_requested && ((posControl.navState == NAV_STATE_IDLE) || mixerAT_inuse || (posControl.navState == NAV_STATE_ALTHOLD_IN_PROGRESS)); - if (!isMixerTransitionMixing) { - resetTransitionScales(); - } - // VTOL transition debug channels (DEBUG_VTOL_TRANSITION): // [0] phase, [1] request, [2] direction, [3] progress x1000, - // [4] pusherScale x1000, [5] liftScale x1000, [6] fwBlend x1000, + // [4] pusherScale x1000, [5] liftScale x1000, [6] fwAuthority/blend x1000, // [7] flags bitfield: bit0 active, bit1 usedAirspeed, bit2 hotSwitchDone, bit3 aborted DEBUG_SET(DEBUG_VTOL_TRANSITION, 0, mixerProfileAT.phase); DEBUG_SET(DEBUG_VTOL_TRANSITION, 1, mixerProfileAT.request); @@ -553,6 +549,10 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) (mixerProfileAT.usedAirspeed ? 1 << 1 : 0) | (mixerProfileAT.hotSwitchDone ? 1 << 2 : 0) | (mixerProfileAT.aborted ? 1 << 3 : 0)); + + if (!isMixerTransitionMixing) { + resetTransitionScales(); + } } bool mixerATIsActive(void) From 762c3103b40ddf6c6cc290019b17e9e3b6e16ac9 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Mon, 18 May 2026 20:12:50 +0300 Subject: [PATCH 12/26] fix(vtol): report actual MIXER TRANSITION/MIXER PROFILE 2 activity in mode flags - Report MIXER TRANSITION from internal transition activity when manual auto-transition controller is enabled - Report MIXER PROFILE 2 from the currently active mixer profile (not raw RC request) - Preserve legacy MIXER TRANSITION reporting when manual auto-transition controller is disabled --- docs/MixerProfile.md | 2 ++ src/main/blackbox/blackbox.c | 20 ++++++++++++++++++-- src/main/fc/fc_msp_box.c | 4 ++-- src/main/flight/mixer_profile.c | 24 ++++++++++++++++++++++++ src/main/flight/mixer_profile.h | 2 ++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 7e5b802384c..58eed8050d4 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -32,6 +32,8 @@ The use of Transition Mode is recommended to enable further features and future This edge-triggered behavior is enabled by `mixer_vtol_manualswitch_autotransition_controller`. When `mixer_vtol_manualswitch_autotransition_controller = OFF`, manual transition keeps legacy behavior. +With manual auto-transition enabled, Active Modes `MIXER TRANSITION` now indicates that the internal transition controller/mixing is actually active, not merely that the RC `MIXER TRANSITION` switch is active. +Active Modes `MIXER PROFILE 2` indicates the currently active mixer profile. Important path split: - `MIXER PROFILE 2` remains a direct manual profile-switch path. diff --git a/src/main/blackbox/blackbox.c b/src/main/blackbox/blackbox.c index f5225ca3e1e..7ad94f88263 100644 --- a/src/main/blackbox/blackbox.c +++ b/src/main/blackbox/blackbox.c @@ -59,6 +59,7 @@ #include "flight/failsafe.h" #include "flight/imu.h" #include "flight/mixer.h" +#include "flight/mixer_profile.h" #include "flight/pid.h" #include "flight/servos.h" #include "flight/rpm_filter.h" @@ -1366,10 +1367,25 @@ static void writeSlowFrame(void) */ static void loadSlowState(blackboxSlowState_t *slow) { + boxBitmask_t reportedRcModeFlags = rcModeActivationMask; + slow->activeWpNumber = getActiveWpNumber(); - slow->rcModeFlags = rcModeActivationMask.bits[0]; // first 32 bits of boxId_e - slow->rcModeFlags2 = rcModeActivationMask.bits[1]; // remaining bits of boxId_e + // Keep these two mode bits aligned with actual VTOL state/profile activity for status reporting. + if (isMixerProfile2ModeReportedActive()) { + bitArraySet(reportedRcModeFlags.bits, BOXMIXERPROFILE); + } else { + bitArrayClr(reportedRcModeFlags.bits, BOXMIXERPROFILE); + } + + if (isMixerTransitionModeReportedActive()) { + bitArraySet(reportedRcModeFlags.bits, BOXMIXERTRANSITION); + } else { + bitArrayClr(reportedRcModeFlags.bits, BOXMIXERTRANSITION); + } + + slow->rcModeFlags = reportedRcModeFlags.bits[0]; // first 32 bits of boxId_e + slow->rcModeFlags2 = reportedRcModeFlags.bits[1]; // remaining bits of boxId_e // Also log Nav auto enabled flight modes rather than just those selected by boxmode if (navigationGetHeadingControlState() == NAV_HEADING_CONTROL_AUTO) { diff --git a/src/main/fc/fc_msp_box.c b/src/main/fc/fc_msp_box.c index a20a23e0b24..ba6f3f69593 100644 --- a/src/main/fc/fc_msp_box.c +++ b/src/main/fc/fc_msp_box.c @@ -447,8 +447,8 @@ void packBoxModeFlags(boxBitmask_t * mspBoxModeFlags) CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXMULTIFUNCTION)), BOXMULTIFUNCTION); #endif #if (MAX_MIXER_PROFILE_COUNT > 1) - CHECK_ACTIVE_BOX(IS_ENABLED(currentMixerProfileIndex), BOXMIXERPROFILE); - CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION)), BOXMIXERTRANSITION); + CHECK_ACTIVE_BOX(IS_ENABLED(isMixerProfile2ModeReportedActive()), BOXMIXERPROFILE); + CHECK_ACTIVE_BOX(IS_ENABLED(isMixerTransitionModeReportedActive()), BOXMIXERTRANSITION); #endif CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXANGLEHOLD)), BOXANGLEHOLD); diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index d330d931949..69e721549e6 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -590,6 +590,30 @@ float mixerATGetBlendToFw(void) return constrainf(mixerProfileAT.blendToFw, 0.0f, 1.0f); } +bool isMixerProfile2ModeReportedActive(void) +{ +#if (MAX_MIXER_PROFILE_COUNT > 1) + return currentMixerProfileIndex > 0; +#else + return false; +#endif +} + +bool isMixerTransitionModeReportedActive(void) +{ + // Transition is actively running in the internal controller. + if (mixerATIsActive()) { + return true; + } + + // With manual auto-transition enabled, treat the RC box as a trigger/request only. + if (currentMixerConfig.manualVtolTransitionController) { + return false; + } + + return IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION); +} + // switch mixerprofile without reboot bool outputProfileHotSwitch(int profile_index) { diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index f5eec0ff9db..2d23fa7994d 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -86,6 +86,8 @@ float mixerATGetLiftScale(void); float mixerATGetMcAuthorityScale(void); float mixerATGetFwAuthorityScale(void); float mixerATGetBlendToFw(void); +bool isMixerProfile2ModeReportedActive(void); +bool isMixerTransitionModeReportedActive(void); extern mixerConfig_t currentMixerConfig; extern int currentMixerProfileIndex; From 025852169962f092bbed2782e4fb757e7ab3b7a6 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Thu, 21 May 2026 10:05:59 +0300 Subject: [PATCH 13/26] debug data enchancment --- docs/MixerProfile.md | 27 ++++++++++++++++++------- src/main/flight/mixer_profile.c | 35 +++++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 58eed8050d4..6ecf950bb6d 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -282,16 +282,29 @@ Debug channels: - `debug[0]` = transition phase (`0=IDLE`, `1=TRANSITION_INITIALIZE`, `2=TRANSITIONING`) - `debug[1]` = active request (`MIXERAT_REQUEST_*` enum value) -- `debug[2]` = direction (`0=NONE`, `1=TO_FW`, `2=TO_MC`) +- `debug[2]` = packed transition flags: + - bits 0-1: transition direction (`0=NONE`, `1=TO_FW`, `2=TO_MC`) + - bit2: auto-transition controller active + - bit3: transition mixing output active (`isMixerTransitionMixing`) + - bit4: RC `MIXERTRANSITION` mode active + - bit5: airspeed-controlled path in use + - bit6: hot-switch done + - bit7: transition aborted + - bit8: manual VTOL auto-transition controller enabled in current mixer config + - bit9: dynamic transition mixer enabled in current mixer config + - bits 10-11: current mixer profile index + - bits 12-13: next mixer profile index + - bit14: manual transition currently allowed by navigation state + - bit15: mission mode active + - bit16: transition mixing requested (`isMixerTransitionMixing_requested`) + - bit17: failsafe mode active + - bit18: manual VTOL auto-transition controller effective after mission gating + - bit19: RC `MIXERPROFILE` mode active - `debug[3]` = progress x1000 (`0..1000`) - `debug[4]` = pusher scale x1000 (`0..1000`) - `debug[5]` = lift scale x1000 (`0..1000`) -- `debug[6]` = FW authority/blend scale x1000 (`0..1000`) -- `debug[7]` = flags bitfield: - - bit0: transition active - - bit1: airspeed-controlled path in use - - bit2: hot-switch done - - bit3: transition aborted +- `debug[6]` = MC authority scale x1000 (`0..1000`) +- `debug[7]` = current mixer profile pitch transition PID multiplier (`transition_PID_mmix_multiplier_pitch`) ## TailSitter (planned for INAV 7.1) TailSitter is supported by add a 90deg offset to the board alignment. Set the board aliment normally in the mixer_profile for FW mode(`set platform_type = AIRPLANE`), The motor trust axis should be same direction as the airplane nose. Then, in the mixer_profile for takeoff and landing set `tailsitter_orientation_offset = ON ` to apply orientation offset. orientation offset will also add a 45deg orientation offset. diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 69e721549e6..65d220dcd3e 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -533,22 +533,37 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) isMixerTransitionMixing = isMixerTransitionMixing_requested && ((posControl.navState == NAV_STATE_IDLE) || mixerAT_inuse || (posControl.navState == NAV_STATE_ALTHOLD_IN_PROGRESS)); + const uint32_t transitionDebugFlags = + ((uint32_t)mixerProfileAT.direction & 0x3U) | + (mixerATIsActive() ? 1U << 2 : 0U) | + (isMixerTransitionMixing ? 1U << 3 : 0U) | + (transitionModeActive ? 1U << 4 : 0U) | + (mixerProfileAT.usedAirspeed ? 1U << 5 : 0U) | + (mixerProfileAT.hotSwitchDone ? 1U << 6 : 0U) | + (mixerProfileAT.aborted ? 1U << 7 : 0U) | + (currentMixerConfig.manualVtolTransitionController ? 1U << 8 : 0U) | + (currentMixerConfig.vtolTransitionDynamicMixer ? 1U << 9 : 0U) | + (((uint32_t)currentMixerProfileIndex & 0x3U) << 10) | + (((uint32_t)nextMixerProfileIndex & 0x3U) << 12) | + (manualTransitionAllowed ? 1U << 14 : 0U) | + (missionActive ? 1U << 15 : 0U) | + (isMixerTransitionMixing_requested ? 1U << 16 : 0U) | + (FLIGHT_MODE(FAILSAFE_MODE) ? 1U << 17 : 0U) | + (manualControllerEnabled ? 1U << 18 : 0U) | + (IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) ? 1U << 19 : 0U); + // VTOL transition debug channels (DEBUG_VTOL_TRANSITION): - // [0] phase, [1] request, [2] direction, [3] progress x1000, - // [4] pusherScale x1000, [5] liftScale x1000, [6] fwAuthority/blend x1000, - // [7] flags bitfield: bit0 active, bit1 usedAirspeed, bit2 hotSwitchDone, bit3 aborted + // [0] phase, [1] request, [2] packed transition flags, [3] progress x1000, + // [4] pusherScale x1000, [5] liftScale x1000, [6] mcAuthorityScale x1000, + // [7] transition_PID_mmix_multiplier_pitch from currentMixerConfig DEBUG_SET(DEBUG_VTOL_TRANSITION, 0, mixerProfileAT.phase); DEBUG_SET(DEBUG_VTOL_TRANSITION, 1, mixerProfileAT.request); - DEBUG_SET(DEBUG_VTOL_TRANSITION, 2, mixerProfileAT.direction); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 2, (int32_t)transitionDebugFlags); DEBUG_SET(DEBUG_VTOL_TRANSITION, 3, lrintf(constrainf(mixerProfileAT.progress, 0.0f, 1.0f) * 1000.0f)); DEBUG_SET(DEBUG_VTOL_TRANSITION, 4, lrintf(constrainf(mixerProfileAT.pusherScale, 0.0f, 1.0f) * 1000.0f)); DEBUG_SET(DEBUG_VTOL_TRANSITION, 5, lrintf(constrainf(mixerProfileAT.liftScale, 0.0f, 1.0f) * 1000.0f)); - DEBUG_SET(DEBUG_VTOL_TRANSITION, 6, lrintf(constrainf(mixerProfileAT.blendToFw, 0.0f, 1.0f) * 1000.0f)); - DEBUG_SET(DEBUG_VTOL_TRANSITION, 7, - (mixerATIsActive() ? 1 : 0) | - (mixerProfileAT.usedAirspeed ? 1 << 1 : 0) | - (mixerProfileAT.hotSwitchDone ? 1 << 2 : 0) | - (mixerProfileAT.aborted ? 1 << 3 : 0)); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 6, lrintf(constrainf(mixerProfileAT.mcAuthorityScale, 0.0f, 1.0f) * 1000.0f)); + DEBUG_SET(DEBUG_VTOL_TRANSITION, 7, currentMixerConfig.transition_PID_mmix_multiplier_pitch); if (!isMixerTransitionMixing) { resetTransitionScales(); From c2cdbfce37aee9e1be169f8a3190757163f721cc Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Thu, 28 May 2026 12:45:07 +0300 Subject: [PATCH 14/26] feat(nav): add VTOL transition retry and configurable fail actions Add global NAV settings for transition failure handling: - nav_vtol_transition_fail_action_mc_to_fw (IDLE/POSH/RTH/EMERGENCY_LANDING) - nav_vtol_transition_fail_action_fw_to_mc (IDLE/LOITER/RTH/EMERGENCY_LANDING/FORCE_SWITCH, default LOITER) - nav_vtol_transition_retry_on_airspeed_timeout Implement one-shot MC->FW retry after airspeed timeout (pitot-gated), including yaw scan/alignment. Handle fail actions in MIXERAT paths (mission and RTH), including FORCE_SWITCH fallback. Expose airspeed-timeout abort reason from mixer transition state. Regenerate settings docs. --- docs/Settings.md | 37 ++++ src/main/fc/settings.yaml | 21 +++ src/main/flight/mixer_profile.c | 22 ++- src/main/flight/mixer_profile.h | 2 + src/main/navigation/navigation.c | 306 ++++++++++++++++++++++++++++--- src/main/navigation/navigation.h | 18 ++ 6 files changed, 377 insertions(+), 29 deletions(-) diff --git a/docs/Settings.md b/docs/Settings.md index 189cbb36eaa..8c66d05270c 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -4711,6 +4711,43 @@ Selects which waypoint USER action bit (`USER1`..`USER4`) is used as mission VTO --- +### nav_vtol_transition_fail_action_fw_to_mc + +Action executed after a final FW->MC transition failure. FORCE_SWITCH attempts an immediate mixer hot-switch even after failed criteria. + +| Allowed Values | | +| --- | --- | +| IDLE | | +| LOITER | Default | +| RTH | | +| EMERGENCY_LANDING | | +| FORCE_SWITCH | | + +--- + +### nav_vtol_transition_fail_action_mc_to_fw + +Action executed after a final MC->FW transition failure (after retry logic, if enabled). + +| Allowed Values | | +| --- | --- | +| IDLE | Default | +| POSH | | +| RTH | | +| EMERGENCY_LANDING | | + +--- + +### nav_vtol_transition_retry_on_airspeed_timeout + +If ON, allows one retry for failed airspeed-gated MC->FW auto-transition (mission or RTH head-home): hold position, perform a 360deg yaw scan, align to best measured pitot airspeed heading, and retry transition once. + +| Default | Min | Max | +| --- | --- | --- | +| OFF | OFF | ON | + +--- + ### nav_wp_enforce_altitude Forces craft to achieve the set WP altitude as well as position before moving to next WP. Position is held and altitude adjusted as required before moving on. 0 = disabled, otherwise setting defines altitude capture tolerance [cm], e.g. 100 means required altitude is achieved when within 100cm of waypoint altitude setting. diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index b355b7b1969..b9125271452 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -174,6 +174,12 @@ tables: - name: nav_wp_user_action enum: navMissionUserAction_e values: ["OFF", "USER1", "USER2", "USER3", "USER4"] + - name: nav_vtol_transition_fail_action_mc_to_fw + enum: navVtolTransitionFailActionMcToFw_e + values: ["IDLE", "POSH", "RTH", "EMERGENCY_LANDING"] + - name: nav_vtol_transition_fail_action_fw_to_mc + enum: navVtolTransitionFailActionFwToMc_e + values: ["IDLE", "LOITER", "RTH", "EMERGENCY_LANDING", "FORCE_SWITCH"] - name: djiRssiSource values: ["RSSI", "CRSF_LQ"] enum: djiRssiSource_e @@ -2671,6 +2677,21 @@ groups: field: general.vtol_mission_transition_track_distance min: 1000 max: 500000 + - name: nav_vtol_transition_retry_on_airspeed_timeout + description: "If ON, allows one retry for failed airspeed-gated MC->FW auto-transition (mission or RTH head-home): hold position, perform a 360deg yaw scan, align to best measured pitot airspeed heading, and retry transition once." + default_value: OFF + field: general.vtol_transition_retry_on_airspeed_timeout + type: bool + - name: nav_vtol_transition_fail_action_mc_to_fw + description: "Action executed after a final MC->FW transition failure (after retry logic, if enabled)." + default_value: "IDLE" + field: general.vtol_transition_fail_action_mc_to_fw + table: nav_vtol_transition_fail_action_mc_to_fw + - name: nav_vtol_transition_fail_action_fw_to_mc + description: "Action executed after a final FW->MC transition failure. FORCE_SWITCH attempts an immediate mixer hot-switch even after failed criteria." + default_value: "LOITER" + field: general.vtol_transition_fail_action_fw_to_mc + table: nav_vtol_transition_fail_action_fw_to_mc - name: nav_wp_multi_mission_index description: "Index of active mission selected from multi mission WP entry loaded in flight controller. Limited to a maximum of 9 missions." default_value: 1 diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 65d220dcd3e..803de1a755a 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -123,6 +123,7 @@ void setMixerProfileAT(void) mixerProfileAT.transitionStartTime = now; mixerProfileAT.aborted = false; + mixerProfileAT.abortedByAirspeedTimeout = false; mixerProfileAT.hotSwitchDone = false; mixerProfileAT.usedAirspeed = false; mixerProfileAT.transitionStartAirspeedCaptured = false; @@ -263,15 +264,17 @@ static void updateTransitionScales(void) mixerProfileAT.blendToFw = constrainf(mixerProfileAT.fwAuthorityScale, 0.0f, 1.0f); } -static void abortTransition(void) +static void abortTransition(const bool byAirspeedTimeout) { const bool wasActive = mixerProfileAT.phase != MIXERAT_PHASE_IDLE; isMixerTransitionMixing_requested = false; mixerProfileAT.phase = MIXERAT_PHASE_IDLE; mixerProfileAT.aborted = wasActive; + mixerProfileAT.abortedByAirspeedTimeout = wasActive && byAirspeedTimeout; mixerProfileAT.hotSwitchDone = false; mixerProfileAT.request = MIXERAT_REQUEST_NONE; mixerProfileAT.direction = MIXERAT_DIRECTION_NONE; + mixerProfileAT.usedAirspeed = false; mixerProfileAT.transitionStartAirspeedCaptured = false; mixerProfileAT.transitionStartAirspeedCmS = 0.0f; resetTransitionScales(); @@ -388,7 +391,7 @@ bool mixerATUpdateState(mixerProfileATRequest_e required_action) { reprocessState=false; if (required_action == MIXERAT_REQUEST_ABORT) { - abortTransition(); + abortTransition(false); return true; } switch (mixerProfileAT.phase) { @@ -413,7 +416,7 @@ bool mixerATUpdateState(mixerProfileATRequest_e required_action) case MIXERAT_PHASE_TRANSITIONING: isMixerTransitionMixing_requested = true; if (required_action != MIXERAT_REQUEST_NONE && required_action != mixerProfileAT.request) { - abortTransition(); + abortTransition(false); return true; } @@ -422,7 +425,7 @@ bool mixerATUpdateState(mixerProfileATRequest_e required_action) mixerProfileAT.progress = 1.0f; updateTransitionScales(); if (!outputProfileHotSwitch(nextMixerProfileIndex)) { - abortTransition(); + abortTransition(false); return true; } mixerProfileAT.hotSwitchDone = true; @@ -433,7 +436,7 @@ bool mixerATUpdateState(mixerProfileATRequest_e required_action) } else if (mixerProfileAT.usedAirspeed && currentMixerConfig.vtolTransitionAirspeedTimeoutMs > 0 && (millis() - mixerProfileAT.transitionStartTime) >= currentMixerConfig.vtolTransitionAirspeedTimeoutMs) { - abortTransition(); + abortTransition(true); return true; } @@ -473,7 +476,7 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) bool manualControllerEnabled = false; if (mixerAT_inuse && (!ARMING_FLAG(ARMED) || FLIGHT_MODE(FAILSAFE_MODE) || areSensorsCalibrating())) { - abortTransition(); + abortTransition(false); mixerAT_inuse = false; } @@ -517,7 +520,7 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) mixerAT_inuse && !mixerProfileAT.hotSwitchDone && (mixerProfileAT.request == MIXERAT_REQUEST_MANUAL_TO_FW || mixerProfileAT.request == MIXERAT_REQUEST_MANUAL_TO_MC)) { - abortTransition(); + abortTransition(false); mixerAT_inuse = false; } @@ -580,6 +583,11 @@ bool mixerATWasAborted(void) return mixerProfileAT.aborted; } +bool mixerATWasAbortedByAirspeedTimeout(void) +{ + return mixerProfileAT.abortedByAirspeedTimeout; +} + float mixerATGetPusherScale(void) { return constrainf(mixerProfileAT.pusherScale, 0.0f, 1.0f); diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index 2d23fa7994d..59fe4384809 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -64,6 +64,7 @@ typedef struct mixerProfileAT_s { mixerProfileATDirection_e direction; mixerProfileATRequest_e request; bool aborted; + bool abortedByAirspeedTimeout; bool hotSwitchDone; bool usedAirspeed; bool transitionStartAirspeedCaptured; @@ -81,6 +82,7 @@ bool checkMixerATRequired(mixerProfileATRequest_e required_action); bool mixerATUpdateState(mixerProfileATRequest_e required_action); bool mixerATIsActive(void); bool mixerATWasAborted(void); +bool mixerATWasAbortedByAirspeedTimeout(void); float mixerATGetPusherScale(void); float mixerATGetLiftScale(void); float mixerATGetMcAuthorityScale(void); diff --git a/src/main/navigation/navigation.c b/src/main/navigation/navigation.c index 1c0a09310c6..bed157d1a53 100644 --- a/src/main/navigation/navigation.c +++ b/src/main/navigation/navigation.c @@ -61,6 +61,7 @@ #include "rx/rx.h" #include "sensors/sensors.h" +#include "sensors/pitotmeter.h" #include "sensors/acceleration.h" #include "sensors/boardalignment.h" #include "sensors/battery.h" @@ -83,6 +84,11 @@ #define FW_LAND_LOITER_MIN_TIME 30000000 // usec (30 sec) #define FW_LAND_LOITER_ALT_TOLERANCE 150 +// One-shot MC->FW mission retry after airspeed-timeout: hold position, yaw scan, align to best pitot heading. +#define NAV_MIXERAT_RETRY_SCAN_STEP_CD DEGREES_TO_CENTIDEGREES(20) +#define NAV_MIXERAT_RETRY_HEADING_TOL_CD DEGREES_TO_CENTIDEGREES(5) +#define NAV_MIXERAT_RETRY_HEADING_SETTLE_MS 500 + /*----------------------------------------------------------- * Compatibility for home position *-----------------------------------------------------------*/ @@ -152,6 +158,9 @@ PG_RESET_TEMPLATE(navConfig_t, navConfig, .vtol_mission_transition_user_action = SETTING_NAV_VTOL_MISSION_TRANSITION_USER_ACTION_DEFAULT, .vtol_mission_transition_min_altitude = SETTING_NAV_VTOL_MISSION_TRANSITION_MIN_ALTITUDE_CM_DEFAULT, .vtol_mission_transition_track_distance = SETTING_NAV_VTOL_MISSION_TRANSITION_TRACK_DISTANCE_CM_DEFAULT, + .vtol_transition_retry_on_airspeed_timeout = SETTING_NAV_VTOL_TRANSITION_RETRY_ON_AIRSPEED_TIMEOUT_DEFAULT, + .vtol_transition_fail_action_mc_to_fw = SETTING_NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_DEFAULT, + .vtol_transition_fail_action_fw_to_mc = SETTING_NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_DEFAULT, #ifdef USE_MULTI_MISSION .waypoint_multi_mission_index = SETTING_NAV_WP_MULTI_MISSION_INDEX_DEFAULT, // mission index selected from multi mission WP entry #endif @@ -277,8 +286,23 @@ typedef struct navMixerATMissionTransition_s { mixerProfileATRequest_e request; int32_t heading; bool active; + bool retryAttempted; + uint8_t retryStage; + int32_t retryScanStartHeading; + int32_t retryTargetHeading; + int32_t retryBestHeading; + int32_t retryScannedCd; + float retryBestAirspeedCmS; + timeMs_t retryStepStartTimeMs; + fpVector3_t retryHoldPos; } navMixerATMissionTransition_t; +typedef enum { + NAV_MIXERAT_RETRY_STAGE_IDLE = 0, + NAV_MIXERAT_RETRY_STAGE_SCAN, + NAV_MIXERAT_RETRY_STAGE_ALIGN, +} navMixerATRetryStage_e; + typedef enum { NAV_MISSION_VTOL_TRANSITION_NONE = 0, NAV_MISSION_VTOL_TRANSITION_CONTINUE, @@ -318,6 +342,11 @@ static bool isWaypointMissionValid(void); static void clearMissionVTOLTransitionState(void); static navMissionVtolTransitionDisposition_e prepareMissionVTOLTransition(const navWaypoint_t *waypoint); static void updateMissionTransitionGuidance(void); +static bool isTransitionRetryToFixedWingRequest(const mixerProfileATRequest_e request); +static bool hasAirspeedSensorForTransitionRetry(void); +static bool canRetryTransitionAfterAirspeedTimeout(const mixerProfileATRequest_e request); +static void beginMissionTransitionRetryScan(const mixerProfileATRequest_e request); +static void updateMissionTransitionRetryScan(void); void missionPlannerSetWaypoint(void); void initializeRTHSanityChecker(void); @@ -1067,6 +1096,9 @@ static const navigationFSMStateDescriptor_t navFSM[NAV_STATE_COUNT] = { .onEvent = { [NAV_FSM_EVENT_TIMEOUT] = NAV_STATE_MIXERAT_IN_PROGRESS, // re-process the state [NAV_FSM_EVENT_SWITCH_TO_IDLE] = NAV_STATE_MIXERAT_ABORT, + [NAV_FSM_EVENT_SWITCH_TO_POSHOLD_3D] = NAV_STATE_POSHOLD_3D_INITIALIZE, + [NAV_FSM_EVENT_SWITCH_TO_RTH] = NAV_STATE_RTH_INITIALIZE, + [NAV_FSM_EVENT_SWITCH_TO_EMERGENCY_LANDING] = NAV_STATE_EMERGENCY_LANDING_INITIALIZE, [NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME] = NAV_STATE_RTH_HEAD_HOME, //switch to its pending state [NAV_FSM_EVENT_SWITCH_TO_RTH_LANDING] = NAV_STATE_RTH_LANDING, //switch to its pending state [NAV_FSM_EVENT_MIXERAT_MISSION_RESUME] = NAV_STATE_WAYPOINT_IN_PROGRESS, @@ -1283,15 +1315,23 @@ static const navigationFSMStateDescriptor_t navFSM[NAV_STATE_COUNT] = { static navigationFSMStateFlags_t navGetStateFlags(navigationFSMState_t state) { navigationFSMStateFlags_t stateFlags = navFSM[state].stateFlags; + const bool mixerATState = (state == NAV_STATE_MIXERAT_INITIALIZE || state == NAV_STATE_MIXERAT_IN_PROGRESS); // During mission-authorized MC->FW transition, enable XY/YAW control to fly a straight acceleration segment. - if ((state == NAV_STATE_MIXERAT_INITIALIZE || state == NAV_STATE_MIXERAT_IN_PROGRESS) && + if (mixerATState && navMixerATPendingState == NAV_STATE_WAYPOINT_PRE_ACTION && navMixerATMissionTransition.active && navMixerATMissionTransition.request == MIXERAT_REQUEST_MISSION_TO_FW) { stateFlags |= NAV_CTL_POS | NAV_CTL_YAW; } + // During one-shot retry scan/alignment, hold position and command heading in MC. + if (mixerATState && + navMixerATMissionTransition.retryStage != NAV_MIXERAT_RETRY_STAGE_IDLE && + isTransitionRetryToFixedWingRequest(navMixerATMissionTransition.request)) { + stateFlags |= NAV_CTL_POS | NAV_CTL_YAW; + } + return stateFlags; } @@ -2024,11 +2064,191 @@ static bool isMissionTransitionToMultirotorType(const flyingPlatformType_e platf platformType == PLATFORM_HELICOPTER; } +#ifdef USE_PITOT +static bool hasTrustedPitotAirspeed(float *airspeedCmS) +{ + if (!sensors(SENSOR_PITOT) || !pitotValidForAirspeed() || pitotHasFailed()) { + return false; + } + + if (detectedSensors[SENSOR_INDEX_PITOT] == PITOT_NONE || + detectedSensors[SENSOR_INDEX_PITOT] == PITOT_VIRTUAL) { + return false; + } + + *airspeedCmS = pitot.airSpeed; + return true; +} +#else +static bool hasTrustedPitotAirspeed(float *airspeedCmS) +{ + UNUSED(airspeedCmS); + return false; +} +#endif + +static bool isTransitionRetryToFixedWingRequest(const mixerProfileATRequest_e request) +{ + return request == MIXERAT_REQUEST_MISSION_TO_FW || request == MIXERAT_REQUEST_RTH; +} + +static bool hasAirspeedSensorForTransitionRetry(void) +{ +#ifdef USE_PITOT + if (!sensors(SENSOR_PITOT) || pitotHasFailed()) { + return false; + } + + if (detectedSensors[SENSOR_INDEX_PITOT] == PITOT_NONE || + detectedSensors[SENSOR_INDEX_PITOT] == PITOT_VIRTUAL) { + return false; + } + + return true; +#else + return false; +#endif +} + +static bool canRetryTransitionAfterAirspeedTimeout(const mixerProfileATRequest_e request) +{ + return navConfig()->general.vtol_transition_retry_on_airspeed_timeout && + isTransitionRetryToFixedWingRequest(request) && + hasAirspeedSensorForTransitionRetry(); +} + +static bool isTransitionToMultirotorRequest(const mixerProfileATRequest_e request) +{ + return request == MIXERAT_REQUEST_LAND || request == MIXERAT_REQUEST_MISSION_TO_MC; +} + +static navigationFSMEvent_t getMcToFwTransitionFailEvent(const mixerProfileATRequest_e request) +{ + switch ((navVtolTransitionFailActionMcToFw_e)navConfig()->general.vtol_transition_fail_action_mc_to_fw) { + case NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_POSH: + return NAV_FSM_EVENT_SWITCH_TO_POSHOLD_3D; + case NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_RTH: + return request == MIXERAT_REQUEST_RTH ? NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME : NAV_FSM_EVENT_SWITCH_TO_RTH; + case NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_EMERGENCY_LANDING: + return NAV_FSM_EVENT_SWITCH_TO_EMERGENCY_LANDING; + case NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_IDLE: + default: + return NAV_FSM_EVENT_SWITCH_TO_IDLE; + } +} + +static navigationFSMEvent_t getFwToMcTransitionFailEvent(const mixerProfileATRequest_e request) +{ + switch ((navVtolTransitionFailActionFwToMc_e)navConfig()->general.vtol_transition_fail_action_fw_to_mc) { + case NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_RTH: + return NAV_FSM_EVENT_SWITCH_TO_RTH; + case NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_EMERGENCY_LANDING: + return NAV_FSM_EVENT_SWITCH_TO_EMERGENCY_LANDING; + case NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_LOITER: + case NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_FORCE_SWITCH: + return request == MIXERAT_REQUEST_LAND ? NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME : NAV_FSM_EVENT_SWITCH_TO_RTH; + case NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_IDLE: + default: + return NAV_FSM_EVENT_SWITCH_TO_IDLE; + } +} + +static navigationFSMEvent_t getTransitionFailEvent(const mixerProfileATRequest_e request) +{ + if (isTransitionRetryToFixedWingRequest(request)) { + return getMcToFwTransitionFailEvent(request); + } + + if (isTransitionToMultirotorRequest(request)) { + return getFwToMcTransitionFailEvent(request); + } + + return NAV_FSM_EVENT_SWITCH_TO_IDLE; +} + +static bool tryForceSwitchAfterFwToMcFailure(const mixerProfileATRequest_e request) +{ + if (!isTransitionToMultirotorRequest(request)) { + return false; + } + + if ((navVtolTransitionFailActionFwToMc_e)navConfig()->general.vtol_transition_fail_action_fw_to_mc != NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_FORCE_SWITCH) { + return false; + } + + return STATE(AIRPLANE) && outputProfileHotSwitch(nextMixerProfileIndex); +} + static void clearMissionVTOLTransitionState(void) { navMixerATMissionTransition.active = false; navMixerATMissionTransition.request = MIXERAT_REQUEST_NONE; navMixerATMissionTransition.heading = posControl.actualState.yaw; + navMixerATMissionTransition.retryAttempted = false; + navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_IDLE; + navMixerATMissionTransition.retryScanStartHeading = posControl.actualState.yaw; + navMixerATMissionTransition.retryTargetHeading = posControl.actualState.yaw; + navMixerATMissionTransition.retryBestHeading = posControl.actualState.yaw; + navMixerATMissionTransition.retryScannedCd = 0; + navMixerATMissionTransition.retryBestAirspeedCmS = -1.0f; + navMixerATMissionTransition.retryStepStartTimeMs = 0; + navMixerATMissionTransition.retryHoldPos = navGetCurrentActualPositionAndVelocity()->pos; +} + +static void beginMissionTransitionRetryScan(const mixerProfileATRequest_e request) +{ + navMixerATMissionTransition.request = request; + navMixerATMissionTransition.retryAttempted = true; + navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_SCAN; + navMixerATMissionTransition.retryScanStartHeading = wrap_36000(posControl.actualState.yaw); + navMixerATMissionTransition.retryTargetHeading = navMixerATMissionTransition.retryScanStartHeading; + navMixerATMissionTransition.retryBestHeading = navMixerATMissionTransition.retryScanStartHeading; + navMixerATMissionTransition.retryScannedCd = 0; + navMixerATMissionTransition.retryBestAirspeedCmS = -1.0f; + navMixerATMissionTransition.retryStepStartTimeMs = millis(); + navMixerATMissionTransition.retryHoldPos = navGetCurrentActualPositionAndVelocity()->pos; +} + +static void updateMissionTransitionRetryScan(void) +{ + if (navMixerATMissionTransition.retryStage == NAV_MIXERAT_RETRY_STAGE_IDLE) { + return; + } + + const int32_t headingError = ABS(wrap_18000(navMixerATMissionTransition.retryTargetHeading - posControl.actualState.yaw)); + if (headingError > NAV_MIXERAT_RETRY_HEADING_TOL_CD) { + return; + } + + if ((millis() - navMixerATMissionTransition.retryStepStartTimeMs) < NAV_MIXERAT_RETRY_HEADING_SETTLE_MS) { + return; + } + + if (navMixerATMissionTransition.retryStage == NAV_MIXERAT_RETRY_STAGE_SCAN) { + float airspeedCmS = 0.0f; + if (hasTrustedPitotAirspeed(&airspeedCmS) && airspeedCmS > navMixerATMissionTransition.retryBestAirspeedCmS) { + navMixerATMissionTransition.retryBestAirspeedCmS = airspeedCmS; + navMixerATMissionTransition.retryBestHeading = navMixerATMissionTransition.retryTargetHeading; + } + + navMixerATMissionTransition.retryScannedCd += NAV_MIXERAT_RETRY_SCAN_STEP_CD; + if (navMixerATMissionTransition.retryScannedCd >= DEGREES_TO_CENTIDEGREES(360)) { + navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_ALIGN; + navMixerATMissionTransition.retryTargetHeading = wrap_36000(navMixerATMissionTransition.retryBestHeading); + navMixerATMissionTransition.retryStepStartTimeMs = millis(); + return; + } + + navMixerATMissionTransition.retryTargetHeading = + wrap_36000(navMixerATMissionTransition.retryScanStartHeading + navMixerATMissionTransition.retryScannedCd); + navMixerATMissionTransition.retryStepStartTimeMs = millis(); + return; + } + + if (navMixerATMissionTransition.retryStage == NAV_MIXERAT_RETRY_STAGE_ALIGN) { + navMixerATMissionTransition.heading = wrap_36000(navMixerATMissionTransition.retryBestHeading); + navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_IDLE; + } } static navMissionVtolTransitionDisposition_e prepareMissionVTOLTransition(const navWaypoint_t *waypoint) @@ -2092,6 +2312,15 @@ static navMissionVtolTransitionDisposition_e prepareMissionVTOLTransition(const static void updateMissionTransitionGuidance(void) { + if (navMixerATMissionTransition.retryStage != NAV_MIXERAT_RETRY_STAGE_IDLE && + isTransitionRetryToFixedWingRequest(navMixerATMissionTransition.request) && + STATE(MULTIROTOR)) { + setDesiredPosition(&navMixerATMissionTransition.retryHoldPos, + navMixerATMissionTransition.retryTargetHeading, + NAV_POS_UPDATE_XY | NAV_POS_UPDATE_Z | NAV_POS_UPDATE_HEADING); + return; + } + if (navMixerATMissionTransition.active && navMixerATMissionTransition.request == MIXERAT_REQUEST_MISSION_TO_FW && STATE(MULTIROTOR)) { @@ -2481,35 +2710,68 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(nav required_action = MIXERAT_REQUEST_NONE; break; } + + if (navMixerATMissionTransition.retryStage != NAV_MIXERAT_RETRY_STAGE_IDLE) { + updateMissionTransitionRetryScan(); + if (navMixerATMissionTransition.retryStage == NAV_MIXERAT_RETRY_STAGE_IDLE) { + mixerATUpdateState(required_action); + } + updateMissionTransitionGuidance(); + return NAV_FSM_EVENT_NONE; + } + if (mixerATUpdateState(required_action)){ // MixerAT is done, switch to next state - const bool transitionAborted = mixerATWasAborted(); + bool transitionAborted = mixerATWasAborted(); + const bool transitionTimeout = mixerATWasAbortedByAirspeedTimeout(); + const bool missionTransitionWasActive = navMixerATMissionTransition.active; + if (transitionAborted && + transitionTimeout && + !navMixerATMissionTransition.retryAttempted && + canRetryTransitionAfterAirspeedTimeout(required_action) && + STATE(MULTIROTOR) && + ((required_action == MIXERAT_REQUEST_MISSION_TO_FW && missionTransitionWasActive) || + required_action == MIXERAT_REQUEST_RTH)) { + mixerATUpdateState(MIXERAT_REQUEST_ABORT); + beginMissionTransitionRetryScan(required_action); + updateMissionTransitionGuidance(); + return NAV_FSM_EVENT_NONE; + } + + if (transitionAborted && tryForceSwitchAfterFwToMcFailure(required_action)) { + transitionAborted = false; + } + + navigationFSMEvent_t nextEvent = NAV_FSM_EVENT_SWITCH_TO_IDLE; + if (transitionAborted) { + nextEvent = getTransitionFailEvent(required_action); + } else { + switch (navMixerATPendingState) + { + case NAV_STATE_RTH_HEAD_HOME: + nextEvent = NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME; + break; + case NAV_STATE_RTH_LANDING: + nextEvent = NAV_FSM_EVENT_SWITCH_TO_RTH_LANDING; + break; + case NAV_STATE_WAYPOINT_PRE_ACTION: + if (missionTransitionWasActive) { + nextEvent = NAV_FSM_EVENT_MIXERAT_MISSION_RESUME; + } + break; + default: + break; + } + } + resetPositionController(); resetAltitudeController(false); // Make sure surface tracking is not enabled uses global altitude, not AGL mixerATUpdateState(MIXERAT_REQUEST_ABORT); - const bool missionTransitionWasActive = navMixerATMissionTransition.active; clearMissionVTOLTransitionState(); - switch (navMixerATPendingState) - { - case NAV_STATE_RTH_HEAD_HOME: - setupAltitudeController(); - return NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME; - break; - case NAV_STATE_RTH_LANDING: - setupAltitudeController(); - return NAV_FSM_EVENT_SWITCH_TO_RTH_LANDING; - break; - case NAV_STATE_WAYPOINT_PRE_ACTION: + if (nextEvent != NAV_FSM_EVENT_SWITCH_TO_IDLE) { setupAltitudeController(); - if (missionTransitionWasActive) { - return transitionAborted ? NAV_FSM_EVENT_SWITCH_TO_IDLE : NAV_FSM_EVENT_MIXERAT_MISSION_RESUME; - } - return NAV_FSM_EVENT_SWITCH_TO_IDLE; - break; - default: - return NAV_FSM_EVENT_SWITCH_TO_IDLE; - break; } + return nextEvent; } updateMissionTransitionGuidance(); diff --git a/src/main/navigation/navigation.h b/src/main/navigation/navigation.h index f8c009dcd07..03fc33f3d81 100644 --- a/src/main/navigation/navigation.h +++ b/src/main/navigation/navigation.h @@ -336,6 +336,21 @@ typedef enum { NAV_MISSION_USER_ACTION_4, } navMissionUserAction_e; +typedef enum { + NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_IDLE = 0, + NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_POSH, + NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_RTH, + NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_EMERGENCY_LANDING, +} navVtolTransitionFailActionMcToFw_e; + +typedef enum { + NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_IDLE = 0, + NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_LOITER, + NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_RTH, + NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_EMERGENCY_LANDING, + NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_FORCE_SWITCH, +} navVtolTransitionFailActionFwToMc_e; + typedef enum { RTH_TRACKBACK_OFF, RTH_TRACKBACK_ON, @@ -424,6 +439,9 @@ typedef struct navConfig_s { uint8_t vtol_mission_transition_user_action; // User action slot that requests mission VTOL transition uint16_t vtol_mission_transition_min_altitude; // Minimum altitude [cm] to start mission VTOL transition (0 = disabled) uint32_t vtol_mission_transition_track_distance; // Straight-segment target distance [cm] used during MC->FW mission transition + bool vtol_transition_retry_on_airspeed_timeout; // Enables one-shot yaw-scan retry for failed airspeed-gated MC->FW auto-transition + uint8_t vtol_transition_fail_action_mc_to_fw; // Action after final MC->FW transition failure + uint8_t vtol_transition_fail_action_fw_to_mc; // Action after final FW->MC transition failure #ifdef USE_MULTI_MISSION uint8_t waypoint_multi_mission_index; // Index of mission to be loaded in multi mission entry #endif From 799995b9aa19c1984f080b96fe2eed324277d4b0 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Thu, 28 May 2026 14:11:34 +0300 Subject: [PATCH 15/26] fix(nav): harden VTOL retry scan and map FW->MC LOITER fail action to POSHOLD Add step/overall timeouts to MC->FW retry scan, fail retry when no trusted pitot sample is collected, and require valid pitot data before starting retry. Update FW->MC LOITER/FORCE_SWITCH fail-event mapping to POSHOLD_3D and align settings/docs description. --- docs/Settings.md | 2 +- src/main/fc/settings.yaml | 2 +- src/main/navigation/navigation.c | 92 +++++++++++++++++++++++++------- 3 files changed, 75 insertions(+), 21 deletions(-) diff --git a/docs/Settings.md b/docs/Settings.md index 8c66d05270c..fa029d17cff 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -4713,7 +4713,7 @@ Selects which waypoint USER action bit (`USER1`..`USER4`) is used as mission VTO ### nav_vtol_transition_fail_action_fw_to_mc -Action executed after a final FW->MC transition failure. FORCE_SWITCH attempts an immediate mixer hot-switch even after failed criteria. +Action executed after a final FW->MC transition failure. LOITER switches to POSHOLD hold at current position (fixed-wing loiter/orbit around current point). FORCE_SWITCH attempts an immediate mixer hot-switch even after failed criteria. | Allowed Values | | | --- | --- | diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index b9125271452..23f2daf2b9c 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -2688,7 +2688,7 @@ groups: field: general.vtol_transition_fail_action_mc_to_fw table: nav_vtol_transition_fail_action_mc_to_fw - name: nav_vtol_transition_fail_action_fw_to_mc - description: "Action executed after a final FW->MC transition failure. FORCE_SWITCH attempts an immediate mixer hot-switch even after failed criteria." + description: "Action executed after a final FW->MC transition failure. LOITER switches to POSHOLD hold at current position (fixed-wing loiter/orbit around current point). FORCE_SWITCH attempts an immediate mixer hot-switch even after failed criteria." default_value: "LOITER" field: general.vtol_transition_fail_action_fw_to_mc table: nav_vtol_transition_fail_action_fw_to_mc diff --git a/src/main/navigation/navigation.c b/src/main/navigation/navigation.c index bed157d1a53..6704db89029 100644 --- a/src/main/navigation/navigation.c +++ b/src/main/navigation/navigation.c @@ -88,6 +88,8 @@ #define NAV_MIXERAT_RETRY_SCAN_STEP_CD DEGREES_TO_CENTIDEGREES(20) #define NAV_MIXERAT_RETRY_HEADING_TOL_CD DEGREES_TO_CENTIDEGREES(5) #define NAV_MIXERAT_RETRY_HEADING_SETTLE_MS 500 +#define NAV_MIXERAT_RETRY_HEADING_STEP_TIMEOUT_MS 6000 +#define NAV_MIXERAT_RETRY_MAX_TOTAL_MS 45000 /*----------------------------------------------------------- * Compatibility for home position @@ -293,6 +295,8 @@ typedef struct navMixerATMissionTransition_s { int32_t retryBestHeading; int32_t retryScannedCd; float retryBestAirspeedCmS; + bool retryHadTrustedAirspeedSample; + timeMs_t retryStartTimeMs; timeMs_t retryStepStartTimeMs; fpVector3_t retryHoldPos; } navMixerATMissionTransition_t; @@ -303,6 +307,12 @@ typedef enum { NAV_MIXERAT_RETRY_STAGE_ALIGN, } navMixerATRetryStage_e; +typedef enum { + NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS = 0, + NAV_MIXERAT_RETRY_SCAN_READY_TO_RETRY, + NAV_MIXERAT_RETRY_SCAN_FAILED, +} navMixerATRetryScanResult_e; + typedef enum { NAV_MISSION_VTOL_TRANSITION_NONE = 0, NAV_MISSION_VTOL_TRANSITION_CONTINUE, @@ -346,7 +356,7 @@ static bool isTransitionRetryToFixedWingRequest(const mixerProfileATRequest_e re static bool hasAirspeedSensorForTransitionRetry(void); static bool canRetryTransitionAfterAirspeedTimeout(const mixerProfileATRequest_e request); static void beginMissionTransitionRetryScan(const mixerProfileATRequest_e request); -static void updateMissionTransitionRetryScan(void); +static navMixerATRetryScanResult_e updateMissionTransitionRetryScan(void); void missionPlannerSetWaypoint(void); void initializeRTHSanityChecker(void); @@ -2095,7 +2105,7 @@ static bool isTransitionRetryToFixedWingRequest(const mixerProfileATRequest_e re static bool hasAirspeedSensorForTransitionRetry(void) { #ifdef USE_PITOT - if (!sensors(SENSOR_PITOT) || pitotHasFailed()) { + if (!sensors(SENSOR_PITOT) || !pitotValidForAirspeed() || pitotHasFailed()) { return false; } @@ -2139,6 +2149,8 @@ static navigationFSMEvent_t getMcToFwTransitionFailEvent(const mixerProfileATReq static navigationFSMEvent_t getFwToMcTransitionFailEvent(const mixerProfileATRequest_e request) { + UNUSED(request); + switch ((navVtolTransitionFailActionFwToMc_e)navConfig()->general.vtol_transition_fail_action_fw_to_mc) { case NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_RTH: return NAV_FSM_EVENT_SWITCH_TO_RTH; @@ -2146,7 +2158,7 @@ static navigationFSMEvent_t getFwToMcTransitionFailEvent(const mixerProfileATReq return NAV_FSM_EVENT_SWITCH_TO_EMERGENCY_LANDING; case NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_LOITER: case NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_FORCE_SWITCH: - return request == MIXERAT_REQUEST_LAND ? NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME : NAV_FSM_EVENT_SWITCH_TO_RTH; + return NAV_FSM_EVENT_SWITCH_TO_POSHOLD_3D; case NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_IDLE: default: return NAV_FSM_EVENT_SWITCH_TO_IDLE; @@ -2191,6 +2203,8 @@ static void clearMissionVTOLTransitionState(void) navMixerATMissionTransition.retryBestHeading = posControl.actualState.yaw; navMixerATMissionTransition.retryScannedCd = 0; navMixerATMissionTransition.retryBestAirspeedCmS = -1.0f; + navMixerATMissionTransition.retryHadTrustedAirspeedSample = false; + navMixerATMissionTransition.retryStartTimeMs = 0; navMixerATMissionTransition.retryStepStartTimeMs = 0; navMixerATMissionTransition.retryHoldPos = navGetCurrentActualPositionAndVelocity()->pos; } @@ -2205,50 +2219,80 @@ static void beginMissionTransitionRetryScan(const mixerProfileATRequest_e reques navMixerATMissionTransition.retryBestHeading = navMixerATMissionTransition.retryScanStartHeading; navMixerATMissionTransition.retryScannedCd = 0; navMixerATMissionTransition.retryBestAirspeedCmS = -1.0f; - navMixerATMissionTransition.retryStepStartTimeMs = millis(); + navMixerATMissionTransition.retryHadTrustedAirspeedSample = false; + navMixerATMissionTransition.retryStartTimeMs = millis(); + navMixerATMissionTransition.retryStepStartTimeMs = navMixerATMissionTransition.retryStartTimeMs; navMixerATMissionTransition.retryHoldPos = navGetCurrentActualPositionAndVelocity()->pos; } -static void updateMissionTransitionRetryScan(void) +static navMixerATRetryScanResult_e updateMissionTransitionRetryScan(void) { if (navMixerATMissionTransition.retryStage == NAV_MIXERAT_RETRY_STAGE_IDLE) { - return; + return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; + } + + const timeMs_t nowMs = millis(); + const bool overallTimedOut = (nowMs - navMixerATMissionTransition.retryStartTimeMs) >= NAV_MIXERAT_RETRY_MAX_TOTAL_MS; + if (overallTimedOut) { + navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_IDLE; + return NAV_MIXERAT_RETRY_SCAN_FAILED; } const int32_t headingError = ABS(wrap_18000(navMixerATMissionTransition.retryTargetHeading - posControl.actualState.yaw)); - if (headingError > NAV_MIXERAT_RETRY_HEADING_TOL_CD) { - return; + const bool headingReached = headingError <= NAV_MIXERAT_RETRY_HEADING_TOL_CD; + const bool stepTimedOut = (nowMs - navMixerATMissionTransition.retryStepStartTimeMs) >= NAV_MIXERAT_RETRY_HEADING_STEP_TIMEOUT_MS; + + if (!headingReached && !stepTimedOut) { + return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; } - if ((millis() - navMixerATMissionTransition.retryStepStartTimeMs) < NAV_MIXERAT_RETRY_HEADING_SETTLE_MS) { - return; + if (headingReached && + !stepTimedOut && + (nowMs - navMixerATMissionTransition.retryStepStartTimeMs) < NAV_MIXERAT_RETRY_HEADING_SETTLE_MS) { + return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; } if (navMixerATMissionTransition.retryStage == NAV_MIXERAT_RETRY_STAGE_SCAN) { float airspeedCmS = 0.0f; - if (hasTrustedPitotAirspeed(&airspeedCmS) && airspeedCmS > navMixerATMissionTransition.retryBestAirspeedCmS) { - navMixerATMissionTransition.retryBestAirspeedCmS = airspeedCmS; - navMixerATMissionTransition.retryBestHeading = navMixerATMissionTransition.retryTargetHeading; + if (hasTrustedPitotAirspeed(&airspeedCmS)) { + navMixerATMissionTransition.retryHadTrustedAirspeedSample = true; + if (airspeedCmS > navMixerATMissionTransition.retryBestAirspeedCmS) { + navMixerATMissionTransition.retryBestAirspeedCmS = airspeedCmS; + navMixerATMissionTransition.retryBestHeading = wrap_36000(posControl.actualState.yaw); + } } navMixerATMissionTransition.retryScannedCd += NAV_MIXERAT_RETRY_SCAN_STEP_CD; if (navMixerATMissionTransition.retryScannedCd >= DEGREES_TO_CENTIDEGREES(360)) { + if (!navMixerATMissionTransition.retryHadTrustedAirspeedSample) { + navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_IDLE; + return NAV_MIXERAT_RETRY_SCAN_FAILED; + } + navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_ALIGN; navMixerATMissionTransition.retryTargetHeading = wrap_36000(navMixerATMissionTransition.retryBestHeading); - navMixerATMissionTransition.retryStepStartTimeMs = millis(); - return; + navMixerATMissionTransition.retryStepStartTimeMs = nowMs; + return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; } navMixerATMissionTransition.retryTargetHeading = wrap_36000(navMixerATMissionTransition.retryScanStartHeading + navMixerATMissionTransition.retryScannedCd); - navMixerATMissionTransition.retryStepStartTimeMs = millis(); - return; + navMixerATMissionTransition.retryStepStartTimeMs = nowMs; + return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; } if (navMixerATMissionTransition.retryStage == NAV_MIXERAT_RETRY_STAGE_ALIGN) { + if (!headingReached) { + navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_IDLE; + return NAV_MIXERAT_RETRY_SCAN_FAILED; + } + navMixerATMissionTransition.heading = wrap_36000(navMixerATMissionTransition.retryBestHeading); navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_IDLE; + return NAV_MIXERAT_RETRY_SCAN_READY_TO_RETRY; } + + return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; } static navMissionVtolTransitionDisposition_e prepareMissionVTOLTransition(const navWaypoint_t *waypoint) @@ -2712,9 +2756,19 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(nav } if (navMixerATMissionTransition.retryStage != NAV_MIXERAT_RETRY_STAGE_IDLE) { - updateMissionTransitionRetryScan(); - if (navMixerATMissionTransition.retryStage == NAV_MIXERAT_RETRY_STAGE_IDLE) { + const navMixerATRetryScanResult_e retryResult = updateMissionTransitionRetryScan(); + if (retryResult == NAV_MIXERAT_RETRY_SCAN_READY_TO_RETRY) { mixerATUpdateState(required_action); + } else if (retryResult == NAV_MIXERAT_RETRY_SCAN_FAILED) { + const navigationFSMEvent_t nextEvent = getTransitionFailEvent(required_action); + resetPositionController(); + resetAltitudeController(false); // Make sure surface tracking is not enabled uses global altitude, not AGL + mixerATUpdateState(MIXERAT_REQUEST_ABORT); + clearMissionVTOLTransitionState(); + if (nextEvent != NAV_FSM_EVENT_SWITCH_TO_IDLE) { + setupAltitudeController(); + } + return nextEvent; } updateMissionTransitionGuidance(); return NAV_FSM_EVENT_NONE; From 7fd7163436e6e9758b9c39779e7e00388f2d264a Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Thu, 28 May 2026 16:33:14 +0300 Subject: [PATCH 16/26] fix(vtol): gate manual profile switch during transition and improve retry settle timing Suppress direct BOXMIXERPROFILE hot-switch while manual transition trigger is active to prevent FW->MC direction regression. Bump PG_NAV_CONFIG to 9 and make retry settle timing start after heading tolerance is actually reached. --- src/main/flight/mixer_profile.c | 10 ++++++---- src/main/navigation/navigation.c | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 803de1a755a..2590f9255c0 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -473,22 +473,24 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) const bool manualTransitionAllowed = (posControl.navState == NAV_STATE_IDLE) || (posControl.navState == NAV_STATE_ALTHOLD_IN_PROGRESS); const bool missionActive = (navGetCurrentStateFlags() & NAV_AUTO_WP) != 0; - bool manualControllerEnabled = false; + const bool manualControllerConfigured = currentMixerConfig.manualVtolTransitionController && !missionActive; + bool manualControllerEnabled = manualControllerConfigured; if (mixerAT_inuse && (!ARMING_FLAG(ARMED) || FLIGHT_MODE(FAILSAFE_MODE) || areSensorsCalibrating())) { abortTransition(false); mixerAT_inuse = false; } - // transition mode input for servo mix and motor mix - if (!FLIGHT_MODE(FAILSAFE_MODE) && (!mixerAT_inuse)) + // For manual auto-transition control, suppress direct profile hotswitch while transition trigger is active. + const bool suppressDirectProfileSwitch = manualControllerConfigured && transitionModeActive; + if (!FLIGHT_MODE(FAILSAFE_MODE) && !mixerAT_inuse && !suppressDirectProfileSwitch) { if (isModeActivationConditionPresent(BOXMIXERPROFILE)){ outputProfileHotSwitch(IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) == 0 ? 0 : 1); } } - // Recompute after potential direct profile hot-switch because this flag is per-mixer-profile. + // Recompute after a potential direct profile hot-switch because this flag is per-mixer-profile. manualControllerEnabled = currentMixerConfig.manualVtolTransitionController && !missionActive; if (!manualControllerEnabled) { diff --git a/src/main/navigation/navigation.c b/src/main/navigation/navigation.c index 6704db89029..10a9618a82a 100644 --- a/src/main/navigation/navigation.c +++ b/src/main/navigation/navigation.c @@ -127,7 +127,7 @@ STATIC_ASSERT(NAV_MAX_WAYPOINTS < 254, NAV_MAX_WAYPOINTS_exceeded_allowable_rang PG_REGISTER_ARRAY(navWaypoint_t, NAV_MAX_WAYPOINTS, nonVolatileWaypointList, PG_WAYPOINT_MISSION_STORAGE, 2); #endif -PG_REGISTER_WITH_RESET_TEMPLATE(navConfig_t, navConfig, PG_NAV_CONFIG, 8); +PG_REGISTER_WITH_RESET_TEMPLATE(navConfig_t, navConfig, PG_NAV_CONFIG, 9); PG_RESET_TEMPLATE(navConfig_t, navConfig, .general = { @@ -298,6 +298,7 @@ typedef struct navMixerATMissionTransition_s { bool retryHadTrustedAirspeedSample; timeMs_t retryStartTimeMs; timeMs_t retryStepStartTimeMs; + timeMs_t retryHeadingReachedTimeMs; fpVector3_t retryHoldPos; } navMixerATMissionTransition_t; @@ -2206,6 +2207,7 @@ static void clearMissionVTOLTransitionState(void) navMixerATMissionTransition.retryHadTrustedAirspeedSample = false; navMixerATMissionTransition.retryStartTimeMs = 0; navMixerATMissionTransition.retryStepStartTimeMs = 0; + navMixerATMissionTransition.retryHeadingReachedTimeMs = 0; navMixerATMissionTransition.retryHoldPos = navGetCurrentActualPositionAndVelocity()->pos; } @@ -2222,6 +2224,7 @@ static void beginMissionTransitionRetryScan(const mixerProfileATRequest_e reques navMixerATMissionTransition.retryHadTrustedAirspeedSample = false; navMixerATMissionTransition.retryStartTimeMs = millis(); navMixerATMissionTransition.retryStepStartTimeMs = navMixerATMissionTransition.retryStartTimeMs; + navMixerATMissionTransition.retryHeadingReachedTimeMs = 0; navMixerATMissionTransition.retryHoldPos = navGetCurrentActualPositionAndVelocity()->pos; } @@ -2242,13 +2245,21 @@ static navMixerATRetryScanResult_e updateMissionTransitionRetryScan(void) const bool headingReached = headingError <= NAV_MIXERAT_RETRY_HEADING_TOL_CD; const bool stepTimedOut = (nowMs - navMixerATMissionTransition.retryStepStartTimeMs) >= NAV_MIXERAT_RETRY_HEADING_STEP_TIMEOUT_MS; + if (headingReached) { + if (!navMixerATMissionTransition.retryHeadingReachedTimeMs) { + navMixerATMissionTransition.retryHeadingReachedTimeMs = nowMs; + } + } else { + navMixerATMissionTransition.retryHeadingReachedTimeMs = 0; + } + if (!headingReached && !stepTimedOut) { return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; } if (headingReached && !stepTimedOut && - (nowMs - navMixerATMissionTransition.retryStepStartTimeMs) < NAV_MIXERAT_RETRY_HEADING_SETTLE_MS) { + (nowMs - navMixerATMissionTransition.retryHeadingReachedTimeMs) < NAV_MIXERAT_RETRY_HEADING_SETTLE_MS) { return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; } @@ -2272,12 +2283,14 @@ static navMixerATRetryScanResult_e updateMissionTransitionRetryScan(void) navMixerATMissionTransition.retryStage = NAV_MIXERAT_RETRY_STAGE_ALIGN; navMixerATMissionTransition.retryTargetHeading = wrap_36000(navMixerATMissionTransition.retryBestHeading); navMixerATMissionTransition.retryStepStartTimeMs = nowMs; + navMixerATMissionTransition.retryHeadingReachedTimeMs = 0; return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; } navMixerATMissionTransition.retryTargetHeading = wrap_36000(navMixerATMissionTransition.retryScanStartHeading + navMixerATMissionTransition.retryScannedCd); navMixerATMissionTransition.retryStepStartTimeMs = nowMs; + navMixerATMissionTransition.retryHeadingReachedTimeMs = 0; return NAV_MIXERAT_RETRY_SCAN_IN_PROGRESS; } From 093c0ec76775d4711fe552f4380c52828597deef Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Thu, 28 May 2026 17:12:24 +0300 Subject: [PATCH 17/26] =?UTF-8?q?=D0=9F=D0=BE-=D0=B7=D0=B4=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BE:=20code=20latch=20=D0=B7=D0=B0=20=E2=80=9Cmanual?= =?UTF-8?q?=20transition=20session=20active=E2=80=9D=20=D0=B4=D0=BE=20TRAN?= =?UTF-8?q?SITION=20switch=20OFF=20edge,=20=D0=B7=D0=B0=20=D0=B4=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D1=81=D0=BC=D0=B5=D0=BD=D1=8F=D0=BC=D0=B5=20sem?= =?UTF-8?q?antics=20mid-session.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/MixerProfile.md | 2 ++ docs/Settings.md | 2 +- docs/VTOL.md | 2 ++ src/main/fc/settings.yaml | 2 +- src/main/flight/mixer_profile.c | 25 +++++++++++++++++++------ 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 6ecf950bb6d..6a05894a46d 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -31,6 +31,7 @@ The use of Transition Mode is recommended to enable further features and future - If switched OFF before hot-switch completes, the manual transition request is aborted. This edge-triggered behavior is enabled by `mixer_vtol_manualswitch_autotransition_controller`. +Set `mixer_vtol_manualswitch_autotransition_controller = ON` in both mixer profiles (MC and FW) used for switching to keep manual transition semantics consistent after profile hot-switch. When `mixer_vtol_manualswitch_autotransition_controller = OFF`, manual transition keeps legacy behavior. With manual auto-transition enabled, Active Modes `MIXER TRANSITION` now indicates that the internal transition controller/mixing is actually active, not merely that the RC `MIXER TRANSITION` switch is active. Active Modes `MIXER PROFILE 2` indicates the currently active mixer profile. @@ -44,6 +45,7 @@ Recommended switch topology (explicit): - Pos1 = MC (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) - Pos2 = Transition trigger (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) - Pos3 = FW (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) +- Keep `mixer_vtol_manualswitch_autotransition_controller` ON in both profiles used by this mapping. - Avoid overlapping FW selection and transition trigger in the same position. - Avoid 2-position setups where one position activates both `MIXER PROFILE 2` and `MIXER TRANSITION`. - Overlapping mode activation can produce order-dependent behavior (direct profile switch path vs transition-controller path), which is unpredictable and not recommended. diff --git a/docs/Settings.md b/docs/Settings.md index fa029d17cff..ece1eb3655e 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -3222,7 +3222,7 @@ If switch another mixer_profile is scheduled by mixer_automated_switch or mixer_ ### mixer_vtol_manualswitch_autotransition_controller -Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior. +Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior. For consistent manual transition semantics, enable this in both mixer profiles. | Default | Min | Max | | --- | --- | --- | diff --git a/docs/VTOL.md b/docs/VTOL.md index 2886fb87abf..f4f93eefb25 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -307,6 +307,7 @@ This keeps one safety boundary for profile hot-switching and avoids separate tra Intent: this does not replace legacy manual behavior. Legacy remains available and selectable. With `mixer_vtol_manualswitch_autotransition_controller = ON`: +- Enable this setting in both mixer profiles (MC and FW) for consistent edge-triggered behavior across profile hot-switches. - `MIXER TRANSITION` acts as an edge-triggered request. - A rising edge starts one transition. - Transition then runs autonomously to completion. @@ -331,6 +332,7 @@ Important RC mapping constraint: - Pos1 = MC (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) - Pos2 = Transition trigger (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) - Pos3 = FW (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) +- Keep `mixer_vtol_manualswitch_autotransition_controller` ON in both profiles used by this mapping. - Do not overlap/merge FW selection and transition trigger in the same switch position. - Do not use a 2-position mapping where one position enables both `MIXER PROFILE 2` and `MIXER TRANSITION`. - Mixing these mode conditions can cause race/order-dependent behavior (direct profile switch versus transition state machine), which is unpredictable in flight. diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 23f2daf2b9c..ef9ae02a2fb 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1298,7 +1298,7 @@ groups: field: mixer_config.vtolTransitionDynamicMixer type: bool - name: mixer_vtol_manualswitch_autotransition_controller - description: "Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior." + description: "Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior. For consistent manual transition semantics, enable this in both mixer profiles." default_value: OFF field: mixer_config.manualVtolTransitionController type: bool diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 2590f9255c0..a7f01253cb0 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -42,6 +42,7 @@ mixerProfileAT_t mixerProfileAT; int nextMixerProfileIndex; static bool manualTransitionModeWasActive; static bool manualTransitionReadyForEdge = true; +static bool manualTransitionSessionLatched; PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 3); @@ -470,19 +471,29 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) bool mixerAT_inuse = mixerATIsActive(); const bool transitionModeActive = IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION); const bool transitionModeRisingEdge = transitionModeActive && !manualTransitionModeWasActive; + const bool transitionModeFallingEdge = !transitionModeActive && manualTransitionModeWasActive; const bool manualTransitionAllowed = (posControl.navState == NAV_STATE_IDLE) || (posControl.navState == NAV_STATE_ALTHOLD_IN_PROGRESS); const bool missionActive = (navGetCurrentStateFlags() & NAV_AUTO_WP) != 0; const bool manualControllerConfigured = currentMixerConfig.manualVtolTransitionController && !missionActive; - bool manualControllerEnabled = manualControllerConfigured; + bool manualControllerEnabled = manualControllerConfigured || manualTransitionSessionLatched; + + if (manualControllerConfigured && transitionModeRisingEdge) { + manualTransitionSessionLatched = true; + } + + if (transitionModeFallingEdge) { + manualTransitionSessionLatched = false; + } if (mixerAT_inuse && (!ARMING_FLAG(ARMED) || FLIGHT_MODE(FAILSAFE_MODE) || areSensorsCalibrating())) { abortTransition(false); + manualTransitionSessionLatched = false; mixerAT_inuse = false; } // For manual auto-transition control, suppress direct profile hotswitch while transition trigger is active. - const bool suppressDirectProfileSwitch = manualControllerConfigured && transitionModeActive; + const bool suppressDirectProfileSwitch = manualControllerEnabled && transitionModeActive; if (!FLIGHT_MODE(FAILSAFE_MODE) && !mixerAT_inuse && !suppressDirectProfileSwitch) { if (isModeActivationConditionPresent(BOXMIXERPROFILE)){ @@ -491,7 +502,7 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) } // Recompute after a potential direct profile hot-switch because this flag is per-mixer-profile. - manualControllerEnabled = currentMixerConfig.manualVtolTransitionController && !missionActive; + manualControllerEnabled = (currentMixerConfig.manualVtolTransitionController && !missionActive) || manualTransitionSessionLatched; if (!manualControllerEnabled) { // Backward-compatible manual path: level-controlled transition mixing request. @@ -504,6 +515,7 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) manualTransitionReadyForEdge = true; } else { if (!transitionModeActive) { + manualTransitionSessionLatched = false; manualTransitionReadyForEdge = true; if (!mixerAT_inuse) { isMixerTransitionMixing_requested = false; @@ -555,7 +567,8 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) (isMixerTransitionMixing_requested ? 1U << 16 : 0U) | (FLIGHT_MODE(FAILSAFE_MODE) ? 1U << 17 : 0U) | (manualControllerEnabled ? 1U << 18 : 0U) | - (IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) ? 1U << 19 : 0U); + (IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) ? 1U << 19 : 0U) | + (manualTransitionSessionLatched ? 1U << 20 : 0U); // VTOL transition debug channels (DEBUG_VTOL_TRANSITION): // [0] phase, [1] request, [2] packed transition flags, [3] progress x1000, @@ -631,8 +644,8 @@ bool isMixerTransitionModeReportedActive(void) return true; } - // With manual auto-transition enabled, treat the RC box as a trigger/request only. - if (currentMixerConfig.manualVtolTransitionController) { + // With manual auto-transition enabled (or session latched), treat RC as trigger only. + if (currentMixerConfig.manualVtolTransitionController || manualTransitionSessionLatched) { return false; } From cb213fd3ea30337d858926e9f165c1d64a3f6950 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Wed, 3 Jun 2026 11:55:15 +0300 Subject: [PATCH 18/26] docs(vtol): align profile order guidance and clarify switch examples --- docs/MixerProfile.md | 31 ++++++---- docs/Navigation.md | 3 +- docs/Settings.md | 27 +++++---- docs/VTOL.md | 47 +++++++++------ src/main/fc/config.c | 3 +- src/main/fc/config.h | 1 + src/main/fc/settings.yaml | 18 +++--- src/main/flight/mixer_profile.c | 101 ++++++++++++++++++++++++++++---- src/main/flight/mixer_profile.h | 2 +- 9 files changed, 169 insertions(+), 64 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 6a05894a46d..69d543338dc 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -12,6 +12,12 @@ For VTOL setup. one mixer_profile is used for multi-rotor(MR) and the other is u By default, switching between profiles requires reboot to take affect. However, using the RC mode: `MIXER PROFILE 2` will allow in flight switching for things like VTOL operation . And will re-initialize pid and navigation controllers for current MC or FW flying mode. +For consistency, this document uses the long-standing VTOL order: +- Profile 1 = fixed-wing (FW) +- Profile 2 = multicopter (MC) + +The firmware can work with the profiles swapped, but the examples below keep this order so the switch logic is easier to follow. + Please note that this is an emerging / experimental capability that will require some effort by the pilot to implement. ## Mixer Transition input @@ -29,6 +35,8 @@ The use of Transition Mode is recommended to enable further features and future - Keeping the mode ON does not repeatedly restart transitions. - A new transition requires mode OFF then ON again. - If switched OFF before hot-switch completes, the manual transition request is aborted. +- If valid pitot is present and MC->FW threshold is configured, direct manual profile hot-switch to FW is blocked until threshold is reached. +- Optional FW protection: `vtol_fw_to_mc_auto_switch_airspeed_cm_s` can auto-request FW->MC transition when valid pitot airspeed drops to/below the configured value (`0` disables). This edge-triggered behavior is enabled by `mixer_vtol_manualswitch_autotransition_controller`. Set `mixer_vtol_manualswitch_autotransition_controller = ON` in both mixer profiles (MC and FW) used for switching to keep manual transition semantics consistent after profile hot-switch. @@ -40,15 +48,19 @@ Important path split: - `MIXER PROFILE 2` remains a direct manual profile-switch path. - Smooth VTOL transition state-machine behavior is triggered by `MIXER TRANSITION` when `mixer_vtol_manualswitch_autotransition_controller = ON`. -Recommended switch topology (explicit): +Recommended switch topology example (clear 3-position setup): +- This example assumes the usual VTOL order used in this document: + - Profile 1 = FW + - Profile 2 = MC - Use a dedicated 3-position mapping: - - Pos1 = MC (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) + - Pos1 = FW (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) - Pos2 = Transition trigger (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) - - Pos3 = FW (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) + - Pos3 = MC (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) - Keep `mixer_vtol_manualswitch_autotransition_controller` ON in both profiles used by this mapping. - Avoid overlapping FW selection and transition trigger in the same position. - Avoid 2-position setups where one position activates both `MIXER PROFILE 2` and `MIXER TRANSITION`. - Overlapping mode activation can produce order-dependent behavior (direct profile switch path vs transition-controller path), which is unpredictable and not recommended. +- If you intentionally swap the profile order, the same idea still works; just swap the FW and MC end positions. ## Servo @@ -115,7 +127,6 @@ When pitot airspeed is healthy and available, transition completion uses pitot t - `vtol_transition_to_fw_min_airspeed_cm_s` for MC->FW - `vtol_transition_to_mc_max_airspeed_cm_s` for FW->MC -- If `vtol_transition_to_fw_min_airspeed_cm_s = 0`, MC->FW falls back to legacy `mixer_switch_trans_airspeed_cm_s`. If pitot is unavailable/unhealthy (or threshold is `0`), timer fallback is used (`mixer_switch_trans_timer`). Ground speed is not used for transition completion/progress. @@ -141,8 +152,9 @@ With dynamic scaling enabled, `vtol_transition_fw_authority_start_percent = 100` Optional scaling ramp timer: -- `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): scaling remains coupled to transition progress (legacy-compatible behavior). -- `mixer_vtol_transition_scale_ramp_time_ms > 0`: scaling uses this timer, while transition completion stays airspeed-first (or timer fallback if pitot unavailable/unhealthy). +- trusted pitot available/healthy: scaling follows airspeed-based transition progress. +- `mixer_vtol_transition_scale_ramp_time_ms > 0`: if trusted pitot becomes unavailable/unhealthy, scaling falls back to this timer. +- `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): if trusted pitot is unavailable/unhealthy, scaling falls back to transition progress/timer behavior. Example: @@ -150,7 +162,8 @@ Example: - `mixer_vtol_transition_scale_ramp_time_ms = 1200` Result: -- scaling reaches target levels in ~1.2s, +- when trusted pitot is healthy, scaling still follows airspeed progress, +- if trusted pitot becomes unavailable/unhealthy, scaling reaches target levels in ~1.2s, - transition completion still follows airspeed threshold when pitot is healthy, - timer fallback completion still uses 5s when pitot is unavailable/unhealthy. @@ -161,14 +174,12 @@ INAV supports mission-requested VTOL transitions through the existing automated - `nav_vtol_mission_transition_user_action` (`OFF`, `USER1`, `USER2`, `USER3`, `USER4`) - `nav_vtol_mission_transition_min_altitude_cm` (optional, `0` disables minimum-altitude check) - `vtol_transition_to_fw_min_airspeed_cm_s` (preferred MC->FW threshold) -- `mixer_switch_trans_airspeed_cm_s` (legacy MC->FW fallback when preferred threshold is `0`) Scope note: - Per-mixer-profile settings: - `mixer_automated_switch` - `mixer_switch_trans_timer` - - `mixer_switch_trans_airspeed_cm_s` - `mixer_vtol_transition_dynamic_mixer` - `mixer_vtol_manualswitch_autotransition_controller` - `mixer_vtol_transition_airspeed_timeout_ms` @@ -176,6 +187,7 @@ Scope note: - Global settings: - `vtol_transition_to_fw_min_airspeed_cm_s` - `vtol_transition_to_mc_max_airspeed_cm_s` + - `vtol_fw_to_mc_auto_switch_airspeed_cm_s` - `vtol_transition_lift_end_percent` - `vtol_transition_mc_authority_end_percent` - `vtol_transition_fw_authority_start_percent` @@ -211,7 +223,6 @@ CLI: - `set mixer_vtol_transition_dynamic_mixer = OFF` - `set mixer_switch_trans_timer = 45` - `set vtol_transition_to_fw_min_airspeed_cm_s = 0` -- `set mixer_switch_trans_airspeed_cm_s = 0` - `set vtol_transition_to_mc_max_airspeed_cm_s = 900` - `set mixer_vtol_transition_airspeed_timeout_ms = 0` - `set mixer_vtol_transition_scale_ramp_time_ms = 0` diff --git a/docs/Navigation.md b/docs/Navigation.md index 9fb02f8a7a0..1a96d947d40 100755 --- a/docs/Navigation.md +++ b/docs/Navigation.md @@ -113,7 +113,6 @@ Configuration: - `nav_vtol_mission_transition_track_distance_cm` configures straight-line MC->FW transition guidance distance. - VTOL transition completion logic is shared with manual MIXER TRANSITION and uses mixer transition settings: - preferred MC->FW threshold: `vtol_transition_to_fw_min_airspeed_cm_s` - - legacy MC->FW fallback (when preferred threshold is `0`): `mixer_switch_trans_airspeed_cm_s` - FW->MC threshold: `vtol_transition_to_mc_max_airspeed_cm_s` Behavior on each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`): @@ -131,7 +130,7 @@ Transition behavior in this MVP: - MC -> FW: straight-line acceleration segment (no loiter), heading from the next waypoint bearing when available, otherwise current heading. - MC -> FW and FW -> MC completion uses pitot airspeed thresholds when healthy/available (`vtol_transition_to_fw_min_airspeed_cm_s`, `vtol_transition_to_mc_max_airspeed_cm_s`). -- If pitot is unavailable/unhealthy (or threshold disabled), timer fallback (`mixer_switch_trans_timer`) is used. +- If pitot is unavailable/unhealthy (or threshold is `0`), timer fallback (`mixer_switch_trans_timer`) is used. - Ground speed is not used for transition progress/completion. - FW -> MC: mission pauses during automated transition, then resumes after switching back to MC profile. - Strict altitude hold is not enforced during MC -> FW transition; natural climb is allowed. diff --git a/docs/Settings.md b/docs/Settings.md index ece1eb3655e..d83167504f1 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -3200,20 +3200,12 @@ If enabled, control_profile_index will follow mixer_profile index. Set to OFF(de --- -### mixer_switch_trans_airspeed_cm_s - -Legacy MC->FW airspeed threshold [cm/s] for automated profile switch. Used when `vtol_transition_to_fw_min_airspeed_cm_s = 0`. If airspeed is unavailable, timer-based fallback (`mixer_switch_trans_timer`) is used. - -| Default | Min | Max | -| --- | --- | --- | -| 0 | 0 | 10000 | - ---- - ### mixer_switch_trans_timer If switch another mixer_profile is scheduled by mixer_automated_switch or mixer_automated_switch. Activate Mixertransion motor/servo mixing for this many decisecond(0.1s) before the actual mixer_profile switch. +If trusted pitot is unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms = 0`, dynamic scaling also falls back to this transition progress/timer behavior. + | Default | Min | Max | | --- | --- | --- | | 0 | 0 | 200 | @@ -3252,7 +3244,7 @@ Enables dynamic VTOL transition progress/scaling controller shared by mission-au ### mixer_vtol_transition_scale_ramp_time_ms -Optional dynamic scaling ramp duration [ms]. When > 0 and `mixer_vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling uses this timer instead of transition completion progress. Set to 0 to keep legacy progress-coupled scaling behavior. +Optional dynamic scaling fallback ramp duration [ms]. When > 0 and `mixer_vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling still follows trusted pitot-based transition progress when available; if trusted pitot becomes unavailable/unhealthy, scaling falls back to this timer. If set to 0, scaling falls back to transition progress/timer behavior. | Default | Min | Max | | --- | --- | --- | @@ -7096,6 +7088,16 @@ Warning voltage per cell, this triggers battery-warning alarms, in 0.01V units, --- +### vtol_fw_to_mc_auto_switch_airspeed_cm_s + +Automatic FW->MC protection threshold [cm/s] used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. If set above 0 and valid pitot airspeed is at/below this value while in FW, controller requests FW->MC transition automatically. Set to 0 to disable. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 20000 | + +--- + ### vtol_transition_fw_authority_start_percent Initial fixed-wing authority scale at transition start, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. @@ -7128,7 +7130,7 @@ Target multicopter stabilization authority scale at transition end, in percent. ### vtol_transition_to_fw_min_airspeed_cm_s -Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. Overrides `mixer_switch_trans_airspeed_cm_s` when > 0. If 0, legacy setting is used. +Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. If 0, MC->FW uses timer fallback (`mixer_switch_trans_timer`). | Default | Min | Max | | --- | --- | --- | @@ -7299,4 +7301,3 @@ Defines rotation rate on YAW axis that UAV will try to archive on max. stick def | 20 | 1 | 180 | --- - diff --git a/docs/VTOL.md b/docs/VTOL.md index f4f93eefb25..7221abf8400 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -23,15 +23,21 @@ We highly value your feedback as it plays a crucial role in the development and # VTOL Configuration Steps ### The VTOL functionality is achieved by switching/transitioning between two configurations stored in the FC. VTOL specific configurations are Mixer Profiles with associated control profiles. One profile set is for fixed-wing(FW) mode, One is for multi-copter(MC) mode. Configuration/Settings other than Mixer/control profiles are shared among two modes + +This guide uses the long-standing VTOL setup order: +- Profile 1 = fixed-wing (FW) +- Profile 2 = multicopter (MC) + +The firmware can work with the profiles swapped, but keeping one order in the guide makes the setup steps easier to follow. ![Alt text](Screenshots/mixerprofile_flow.png) 0. **Find a DIFF ALL file for your model and start from there if possible** - Be aware that `MIXER PROFILE 2` RC mode setting introduced by diff file can get your stuck in a mixer_profile. remove or change channel to proceed 1. **Setup Profile 1:** - - Configure it as a normal fixed-wing/multi-copter. + - Configure it as your normal fixed-wing setup. 2. **Setup Profile 2:** - - Configure it as a normal multi-copter/fixed-wing. + - Configure it as your normal multicopter setup. 3. **Mode Tab Settings:** - Set up switching in the mode tab. @@ -125,8 +131,8 @@ save save ``` -2. **Configure the fixed-wing/Multi-Copter:** - - Configure your fixed-wing/Multi-Copter as you normally would, or you can copy and paste default settings to expedite the process. +2. **Configure the fixed-wing:** + - Configure your fixed-wing as you normally would, or you can copy and paste default settings to speed things up. - Dshot esc protocol availability might be limited depends on outputs and fc board you are using. change the motor wiring or use oneshot/multishot esc protocol and calibrate throttle range. - You can use throttle = -1 as a placeholder for the motor you wish to stop if the motor isn't the last motor - Consider conducting a test flight to ensure that everything operates as expected. And tune the settings, trim the servos. @@ -147,8 +153,8 @@ You must also assign the tilting servos values using the MAX values. If you don save ``` -2. **Configure the Multicopter/tricopter:** - - Set up your multi-copter/fixed-wing as usual, this time for mixer_profile 2 and control_profile 2. +2. **Configure the multicopter/tricopter:** + - Set up your multicopter/tricopter as usual, this time for mixer_profile 2 and control_profile 2. - Utilize the 'MAX' input in the servo mixer to tilt the motors without altering the servo midpoint. - At this stage, focus on configuring profile-specific settings. You can streamline this process by copying and pasting the default PID settings. - you can set -1 in motor mixer throttle as a place holder: this will disable that motor but will load following the motor rules @@ -314,6 +320,8 @@ With `mixer_vtol_manualswitch_autotransition_controller = ON`: - Keeping the mode ON does not repeatedly retrigger transition. - To start another transition, mode must go OFF then ON again. - If mode is turned OFF before hot-switch, transition request is aborted safely. +- If valid pitot is present and MC->FW airspeed threshold is configured, direct manual profile hot-switch to FW is blocked until threshold is reached. +- Optional FW safety fallback: set `vtol_fw_to_mc_auto_switch_airspeed_cm_s > 0` to auto-request FW->MC transition when valid pitot airspeed drops to/below the configured value. With `mixer_vtol_manualswitch_autotransition_controller = OFF`: - legacy manual behavior is preserved for backward compatibility. @@ -357,7 +365,7 @@ For MC -> FW mission transition: MC -> FW: - completion threshold: `vtol_transition_to_fw_min_airspeed_cm_s` -- if this is `0`, legacy `mixer_switch_trans_airspeed_cm_s` is used. +- if this is `0`, MC->FW uses timer fallback (`mixer_switch_trans_timer`). FW -> MC: - completion threshold: `vtol_transition_to_mc_max_airspeed_cm_s` @@ -379,15 +387,17 @@ When `mixer_vtol_transition_dynamic_mixer = ON`, transition progress additionall When `mixer_vtol_transition_dynamic_mixer = OFF`, legacy static transition mixing behavior is preserved. Optional decoupled scaling ramp: -- `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): scaling follows transition progress (legacy-compatible behavior). -- `mixer_vtol_transition_scale_ramp_time_ms > 0`: scaling uses this ramp timer, while completion logic remains unchanged (airspeed-first; timer fallback when pitot is unavailable/unhealthy). +- trusted pitot available/healthy: scaling follows airspeed-based transition progress. +- `mixer_vtol_transition_scale_ramp_time_ms > 0`: if trusted pitot becomes unavailable/unhealthy, scaling falls back to this ramp timer. +- `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): if trusted pitot is unavailable/unhealthy, scaling falls back to transition progress/timer behavior. Example: - `mixer_switch_trans_timer = 50` (5s fallback completion timer) - `mixer_vtol_transition_scale_ramp_time_ms = 1200` Result: -- pusher/lift/authority scaling reaches target levels in ~1.2s, +- when trusted pitot is healthy, pusher/lift/authority scaling still follows airspeed progress, +- if trusted pitot becomes unavailable/unhealthy, scaling reaches target levels in ~1.2s, - transition completion still follows airspeed thresholds when pitot is healthy, - if pitot is unavailable/unhealthy, completion fallback still uses 5s. @@ -406,7 +416,6 @@ CLI: - `set mixer_vtol_transition_dynamic_mixer = OFF` - `set mixer_switch_trans_timer = 45` - `set vtol_transition_to_fw_min_airspeed_cm_s = 0` -- `set mixer_switch_trans_airspeed_cm_s = 0` - `set vtol_transition_to_mc_max_airspeed_cm_s = 900` - `set mixer_vtol_transition_airspeed_timeout_ms = 0` - `set mixer_vtol_transition_scale_ramp_time_ms = 0` @@ -511,11 +520,11 @@ The new VTOL settings are split into two groups: ### Per-mixer-profile settings -These can differ between mixer profile 1 (typically MC) and mixer profile 2 (typically FW): +In the examples in this guide, mixer profile 1 is FW and mixer profile 2 is MC. +These settings can differ between the two mixer profiles: - `mixer_automated_switch` - `mixer_switch_trans_timer` -- `mixer_switch_trans_airspeed_cm_s` - `mixer_vtol_transition_dynamic_mixer` - `mixer_vtol_manualswitch_autotransition_controller` - `mixer_vtol_transition_airspeed_timeout_ms` @@ -527,6 +536,7 @@ These are shared system-wide and are not profile-specific: - `vtol_transition_to_fw_min_airspeed_cm_s` - `vtol_transition_to_mc_max_airspeed_cm_s` +- `vtol_fw_to_mc_auto_switch_airspeed_cm_s` - `vtol_transition_lift_end_percent` - `vtol_transition_mc_authority_end_percent` - `vtol_transition_fw_authority_start_percent` @@ -547,15 +557,15 @@ Use these commands in CLI (`set ...`, then `save`): - `set vtol_transition_to_fw_min_airspeed_cm_s = ` - Preferred MC -> FW completion threshold (pitot airspeed). -- `set mixer_switch_trans_airspeed_cm_s = ` - - Legacy MC -> FW threshold, used when `vtol_transition_to_fw_min_airspeed_cm_s = 0`. - - `set mixer_switch_trans_timer = ` - Timer-based transition duration fallback (used when pitot airspeed is unavailable/unhealthy). - `set vtol_transition_to_mc_max_airspeed_cm_s = ` - FW -> MC completion threshold (pitot airspeed). +- `set vtol_fw_to_mc_auto_switch_airspeed_cm_s = ` + - Optional low-airspeed FW protection threshold for manual auto-transition controller (`0` disables). + - `set mixer_vtol_transition_airspeed_timeout_ms = ` - Transition timeout/abort window. @@ -625,8 +635,9 @@ Dynamic mixer scaling (`mixer_vtol_transition_dynamic_mixer = ON`) uses this pro - MC authority ramps `vtol_transition_mc_authority_end_percent -> 1` - FW authority ramps `1 -> vtol_transition_fw_authority_start_percent` -If `mixer_vtol_transition_scale_ramp_time_ms > 0`, dynamic scaling uses that timer-based ramp instead of transition-progress coupling. -This changes only scaling shape. Transition completion logic remains airspeed-first (with timer fallback when pitot is unavailable/unhealthy). +Dynamic scaling prefers trusted pitot-based transition progress whenever available. +If trusted pitot becomes unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms > 0`, scaling falls back to that timer-based ramp. +If trusted pitot is unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms = 0`, scaling falls back to transition progress/timer behavior (`mixer_switch_trans_timer`). For transition/pusher motors (`-2.0 < throttle < -1.0`), output is interpolated from idle to target: diff --git a/src/main/fc/config.c b/src/main/fc/config.c index 23d62fa40ac..f5cafa62ae7 100755 --- a/src/main/fc/config.c +++ b/src/main/fc/config.c @@ -103,7 +103,7 @@ PG_RESET_TEMPLATE(featureConfig_t, featureConfig, .enabledFeatures = DEFAULT_FEATURES | COMMON_DEFAULT_FEATURES ); -PG_REGISTER_WITH_RESET_TEMPLATE(systemConfig_t, systemConfig, PG_SYSTEM_CONFIG, 8); +PG_REGISTER_WITH_RESET_TEMPLATE(systemConfig_t, systemConfig, PG_SYSTEM_CONFIG, 9); PG_RESET_TEMPLATE(systemConfig_t, systemConfig, .current_profile_index = 0, @@ -119,6 +119,7 @@ PG_RESET_TEMPLATE(systemConfig_t, systemConfig, .throttle_tilt_compensation_strength = SETTING_THROTTLE_TILT_COMP_STR_DEFAULT, // 0-100, 0 - disabled .vtolTransitionToFwMinAirspeed = SETTING_VTOL_TRANSITION_TO_FW_MIN_AIRSPEED_CM_S_DEFAULT, .vtolTransitionToMcMaxAirspeed = SETTING_VTOL_TRANSITION_TO_MC_MAX_AIRSPEED_CM_S_DEFAULT, + .vtolFwToMcAutoSwitchAirspeed = SETTING_VTOL_FW_TO_MC_AUTO_SWITCH_AIRSPEED_CM_S_DEFAULT, .vtolTransitionLiftEndPercent = SETTING_VTOL_TRANSITION_LIFT_END_PERCENT_DEFAULT, .vtolTransitionMcAuthorityEndPercent = SETTING_VTOL_TRANSITION_MC_AUTHORITY_END_PERCENT_DEFAULT, .vtolTransitionFwAuthorityStartPercent = SETTING_VTOL_TRANSITION_FW_AUTHORITY_START_PERCENT_DEFAULT, diff --git a/src/main/fc/config.h b/src/main/fc/config.h index c7f2501fceb..1478754f350 100644 --- a/src/main/fc/config.h +++ b/src/main/fc/config.h @@ -80,6 +80,7 @@ typedef struct systemConfig_s { uint8_t throttle_tilt_compensation_strength; // the correction that will be applied at throttle_correction_angle. uint16_t vtolTransitionToFwMinAirspeed; uint16_t vtolTransitionToMcMaxAirspeed; + uint16_t vtolFwToMcAutoSwitchAirspeed; uint8_t vtolTransitionLiftEndPercent; uint8_t vtolTransitionMcAuthorityEndPercent; uint8_t vtolTransitionFwAuthorityStartPercent; diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index ef9ae02a2fb..75dd54e4ae4 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1281,17 +1281,11 @@ groups: field: mixer_config.automated_switch type: bool - name: mixer_switch_trans_timer - description: "If switch another mixer_profile is scheduled by mixer_automated_switch or mixer_automated_switch. Activate Mixertransion motor/servo mixing for this many decisecond(0.1s) before the actual mixer_profile switch." + description: "If switch another mixer_profile is scheduled by mixer_automated_switch or mixer_automated_switch. Activate Mixertransion motor/servo mixing for this many decisecond(0.1s) before the actual mixer_profile switch. If trusted pitot is unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms = 0`, dynamic scaling also falls back to this transition progress/timer behavior." default_value: 0 field: mixer_config.switchTransitionTimer min: 0 max: 200 - - name: mixer_switch_trans_airspeed_cm_s - description: "Legacy MC->FW airspeed threshold [cm/s] for automated profile switch. Used when `vtol_transition_to_fw_min_airspeed_cm_s = 0`. If airspeed is unavailable, timer-based fallback (`mixer_switch_trans_timer`) is used." - default_value: 0 - field: mixer_config.switchTransitionAirspeed - min: 0 - max: 10000 - name: mixer_vtol_transition_dynamic_mixer description: "Enables dynamic VTOL transition progress/scaling controller shared by mission-authorized and manual MIXER TRANSITION paths." default_value: OFF @@ -1309,7 +1303,7 @@ groups: min: 0 max: 60000 - name: mixer_vtol_transition_scale_ramp_time_ms - description: "Optional dynamic scaling ramp duration [ms]. When > 0 and `mixer_vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling uses this timer instead of transition completion progress. Set to 0 to keep legacy progress-coupled scaling behavior." + description: "Optional dynamic scaling fallback ramp duration [ms]. When > 0 and `mixer_vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling still follows trusted pitot-based transition progress when available; if trusted pitot becomes unavailable/unhealthy, scaling falls back to this timer. If set to 0, scaling falls back to transition progress/timer behavior." default_value: 0 field: mixer_config.vtolTransitionScaleRampTimeMs min: 0 @@ -4027,7 +4021,7 @@ groups: min: 0 max: 100 - name: vtol_transition_to_fw_min_airspeed_cm_s - description: "Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. Overrides `mixer_switch_trans_airspeed_cm_s` when > 0. If 0, legacy setting is used." + description: "Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. If 0, MC->FW uses timer fallback (`mixer_switch_trans_timer`)." default_value: 0 field: vtolTransitionToFwMinAirspeed min: 0 @@ -4038,6 +4032,12 @@ groups: field: vtolTransitionToMcMaxAirspeed min: 0 max: 20000 + - name: vtol_fw_to_mc_auto_switch_airspeed_cm_s + description: "Automatic FW->MC protection threshold [cm/s] used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. If set above 0 and valid pitot airspeed is at/below this value while in FW, controller requests FW->MC transition automatically. Set to 0 to disable." + default_value: 0 + field: vtolFwToMcAutoSwitchAirspeed + min: 0 + max: 20000 - name: vtol_transition_lift_end_percent description: "Target vertical-lift throttle scale at transition end, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." default_value: 100 diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index a7f01253cb0..f305b57459b 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -44,7 +44,7 @@ static bool manualTransitionModeWasActive; static bool manualTransitionReadyForEdge = true; static bool manualTransitionSessionLatched; -PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 3); +PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 4); void pgResetFn_mixerProfiles(mixerProfile_t *instance) { @@ -60,7 +60,6 @@ void pgResetFn_mixerProfiles(mixerProfile_t *instance) .controlProfileLinking = SETTING_MIXER_CONTROL_PROFILE_LINKING_DEFAULT, .automated_switch = SETTING_MIXER_AUTOMATED_SWITCH_DEFAULT, .switchTransitionTimer = SETTING_MIXER_SWITCH_TRANS_TIMER_DEFAULT, - .switchTransitionAirspeed = SETTING_MIXER_SWITCH_TRANS_AIRSPEED_CM_S_DEFAULT, .vtolTransitionDynamicMixer = SETTING_MIXER_VTOL_TRANSITION_DYNAMIC_MIXER_DEFAULT, .manualVtolTransitionController = SETTING_MIXER_VTOL_MANUALSWITCH_AUTOTRANSITION_CONTROLLER_DEFAULT, .vtolTransitionAirspeedTimeoutMs = SETTING_MIXER_VTOL_TRANSITION_AIRSPEED_TIMEOUT_MS_DEFAULT, @@ -129,6 +128,7 @@ void setMixerProfileAT(void) mixerProfileAT.usedAirspeed = false; mixerProfileAT.transitionStartAirspeedCaptured = false; mixerProfileAT.progress = 0.0f; + mixerProfileAT.scalingProgress = 0.0f; mixerProfileAT.transitionStartAirspeedCmS = 0.0f; mixerProfileAT.blendToFw = mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW ? 0.0f : 1.0f; mixerProfileAT.pusherScale = 1.0f; @@ -162,6 +162,7 @@ static mixerProfileATDirection_e directionForRequest(const mixerProfileATRequest static void resetTransitionScales(void) { mixerProfileAT.progress = 0.0f; + mixerProfileAT.scalingProgress = 0.0f; mixerProfileAT.blendToFw = 0.0f; mixerProfileAT.pusherScale = 0.0f; mixerProfileAT.liftScale = 1.0f; @@ -172,6 +173,7 @@ static void resetTransitionScales(void) static void setLegacyTransitionScales(void) { mixerProfileAT.progress = 1.0f; + mixerProfileAT.scalingProgress = 1.0f; mixerProfileAT.blendToFw = 1.0f; mixerProfileAT.pusherScale = 1.0f; mixerProfileAT.liftScale = 1.0f; @@ -190,12 +192,24 @@ static float getScalingProgress(void) return 1.0f; } + if (mixerProfileAT.usedAirspeed) { + mixerProfileAT.scalingProgress = constrainf(mixerProfileAT.progress, 0.0f, 1.0f); + return mixerProfileAT.scalingProgress; + } + if (currentMixerConfig.vtolTransitionScaleRampTimeMs > 0) { const uint32_t elapsedMs = millis() - mixerProfileAT.transitionStartTime; - return constrainf((float)elapsedMs / (float)currentMixerConfig.vtolTransitionScaleRampTimeMs, 0.0f, 1.0f); + const float rampProgress = constrainf((float)elapsedMs / (float)currentMixerConfig.vtolTransitionScaleRampTimeMs, 0.0f, 1.0f); + + // Preserve already-applied scaling if pitot drops out mid-transition. + mixerProfileAT.scalingProgress = MAX(mixerProfileAT.scalingProgress, rampProgress); + return mixerProfileAT.scalingProgress; } - return constrainf(mixerProfileAT.progress, 0.0f, 1.0f); + // Last-resort compatibility path: with no trusted pitot and no dedicated + // scaling ramp configured, reuse transition progress/timer behavior. + mixerProfileAT.scalingProgress = MAX(mixerProfileAT.scalingProgress, constrainf(mixerProfileAT.progress, 0.0f, 1.0f)); + return mixerProfileAT.scalingProgress; } static bool hasTrustedPitotAirspeed(float *airspeedCmS) @@ -218,13 +232,28 @@ static bool hasTrustedPitotAirspeed(float *airspeedCmS) #endif } +static bool hasPitotSensorForManualProtection(void) +{ +#ifdef USE_PITOT + if (!sensors(SENSOR_PITOT) || pitotHasFailed()) { + return false; + } + + if (detectedSensors[SENSOR_INDEX_PITOT] == PITOT_NONE || + detectedSensors[SENSOR_INDEX_PITOT] == PITOT_VIRTUAL) { + return false; + } + + return true; +#else + return false; +#endif +} + static uint16_t getAirspeedThresholdForDirection(const mixerProfileATDirection_e direction) { if (direction == MIXERAT_DIRECTION_TO_FW) { - if (systemConfig()->vtolTransitionToFwMinAirspeed > 0) { - return systemConfig()->vtolTransitionToFwMinAirspeed; - } - return currentMixerConfig.switchTransitionAirspeed; + return systemConfig()->vtolTransitionToFwMinAirspeed; } if (direction == MIXERAT_DIRECTION_TO_MC) { @@ -234,6 +263,48 @@ static uint16_t getAirspeedThresholdForDirection(const mixerProfileATDirection_e return 0; } +static bool shouldBlockManualDirectSwitchToFixedWing(const bool manualControllerEnabled, const int requestedProfileIndex) +{ + if (!manualControllerEnabled || !STATE(MULTIROTOR) || requestedProfileIndex == currentMixerProfileIndex) { + return false; + } + + if (mixerConfigByIndex(requestedProfileIndex)->platformType != PLATFORM_AIRPLANE) { + return false; + } + + const uint16_t thresholdCmS = getAirspeedThresholdForDirection(MIXERAT_DIRECTION_TO_FW); + if (thresholdCmS == 0 || !hasPitotSensorForManualProtection()) { + return false; + } + + float airspeedCmS = 0.0f; + if (!hasTrustedPitotAirspeed(&airspeedCmS)) { + return true; + } + + return airspeedCmS < thresholdCmS; +} + +static bool shouldRequestManualFwToMcProtection(const bool manualControllerEnabled) +{ + if (!manualControllerEnabled || !STATE(AIRPLANE)) { + return false; + } + + const uint16_t thresholdCmS = systemConfig()->vtolFwToMcAutoSwitchAirspeed; + if (thresholdCmS == 0 || !hasPitotSensorForManualProtection()) { + return false; + } + + float airspeedCmS = 0.0f; + if (!hasTrustedPitotAirspeed(&airspeedCmS)) { + return false; + } + + return airspeedCmS <= thresholdCmS; +} + static void updateTransitionScales(void) { if (!currentMixerConfig.vtolTransitionDynamicMixer) { @@ -477,6 +548,8 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) const bool missionActive = (navGetCurrentStateFlags() & NAV_AUTO_WP) != 0; const bool manualControllerConfigured = currentMixerConfig.manualVtolTransitionController && !missionActive; bool manualControllerEnabled = manualControllerConfigured || manualTransitionSessionLatched; + const bool mixerProfileModePresent = isModeActivationConditionPresent(BOXMIXERPROFILE); + const int requestedProfileIndex = IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) == 0 ? 0 : 1; if (manualControllerConfigured && transitionModeRisingEdge) { manualTransitionSessionLatched = true; @@ -496,14 +569,22 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) const bool suppressDirectProfileSwitch = manualControllerEnabled && transitionModeActive; if (!FLIGHT_MODE(FAILSAFE_MODE) && !mixerAT_inuse && !suppressDirectProfileSwitch) { - if (isModeActivationConditionPresent(BOXMIXERPROFILE)){ - outputProfileHotSwitch(IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) == 0 ? 0 : 1); + if (mixerProfileModePresent && + !shouldBlockManualDirectSwitchToFixedWing(manualControllerEnabled, requestedProfileIndex)) { + outputProfileHotSwitch(requestedProfileIndex); } } // Recompute after a potential direct profile hot-switch because this flag is per-mixer-profile. manualControllerEnabled = (currentMixerConfig.manualVtolTransitionController && !missionActive) || manualTransitionSessionLatched; + if (!mixerAT_inuse && + shouldRequestManualFwToMcProtection(manualControllerEnabled) && + checkMixerATRequired(MIXERAT_REQUEST_MANUAL_TO_MC)) { + mixerATUpdateState(MIXERAT_REQUEST_MANUAL_TO_MC); + mixerAT_inuse = mixerATIsActive(); + } + if (!manualControllerEnabled) { // Backward-compatible manual path: level-controlled transition mixing request. if (!FLIGHT_MODE(FAILSAFE_MODE) && (!mixerAT_inuse)) { diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index 59fe4384809..13f690b5362 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -18,7 +18,6 @@ typedef struct mixerConfig_s { bool controlProfileLinking; bool automated_switch; int16_t switchTransitionTimer; - uint16_t switchTransitionAirspeed; bool vtolTransitionDynamicMixer; bool manualVtolTransitionController; uint16_t vtolTransitionAirspeedTimeoutMs; @@ -69,6 +68,7 @@ typedef struct mixerProfileAT_s { bool usedAirspeed; bool transitionStartAirspeedCaptured; float progress; + float scalingProgress; float transitionStartAirspeedCmS; float blendToFw; float pusherScale; From 061c7f23a20d2b3c05417cbfcb9f93f0736c5cdb Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Wed, 3 Jun 2026 19:35:59 +0300 Subject: [PATCH 19/26] vtol: decouple MC->FW pusher ramp from handoff scaling Use mixer_vtol_transition_scale_ramp_time_ms for MC->FW pusher ramp independently of airspeed progress, so the pusher can reach full authority without being limited by low initial airspeed. Keep lift, MC authority and FW authority scaling on the existing handoff path: airspeed-based when trusted pitot is available, with timer/progress fallback when it is not. Update VTOL and mixer profile docs to describe the split scaling behavior. --- docs/MixerProfile.md | 22 +++++++----- docs/Settings.md | 2 +- docs/VTOL.md | 28 +++++++++++----- src/main/fc/settings.yaml | 2 +- src/main/flight/mixer_profile.c | 59 +++++++++++++++++++++------------ src/main/flight/mixer_profile.h | 2 +- 6 files changed, 73 insertions(+), 42 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 69d543338dc..53dc7272fbc 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -117,9 +117,9 @@ When `mixer_automated_switch`:`OFF` is set for all mixer_profiles(defaults). Mod Manual `MIXER TRANSITION` and mission-authorized VTOL transition both use the same internal transition controller. This controller always computes transition progress/completion and performs its own profile hot-switch only inside the authorized transition state. Direct manual `MIXER PROFILE 2` switching remains a separate path when no transition controller path is active. -When `mixer_vtol_transition_dynamic_mixer = ON`, pusher/lift/authority scaling is enabled and is driven by: -- transition progress (default), or -- `mixer_vtol_transition_scale_ramp_time_ms` when configured (>0). +When `mixer_vtol_transition_dynamic_mixer = ON`, transition scaling is split into: +- a pusher ramp for MC->FW, and +- handoff scaling for lift / MC authority / FW authority. ### Airspeed-first completion @@ -152,9 +152,14 @@ With dynamic scaling enabled, `vtol_transition_fw_authority_start_percent = 100` Optional scaling ramp timer: -- trusted pitot available/healthy: scaling follows airspeed-based transition progress. -- `mixer_vtol_transition_scale_ramp_time_ms > 0`: if trusted pitot becomes unavailable/unhealthy, scaling falls back to this timer. -- `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): if trusted pitot is unavailable/unhealthy, scaling falls back to transition progress/timer behavior. +- MC->FW pusher: + - `mixer_vtol_transition_scale_ramp_time_ms > 0`: pusher ramps from `0 -> 100%` over this time, even when trusted pitot is healthy. + - `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): pusher goes to `100%` immediately. +- Lift / MC authority / FW authority handoff: + - trusted pitot available/healthy: follows airspeed-based transition progress. + - `mixer_vtol_transition_scale_ramp_time_ms > 0`: if trusted pitot becomes unavailable/unhealthy, handoff scaling falls back to this timer. + - `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): if trusted pitot is unavailable/unhealthy, handoff scaling falls back to transition progress/timer behavior. +- FW->MC keeps the existing handoff-based scaling behavior. Example: @@ -162,8 +167,9 @@ Example: - `mixer_vtol_transition_scale_ramp_time_ms = 1200` Result: -- when trusted pitot is healthy, scaling still follows airspeed progress, -- if trusted pitot becomes unavailable/unhealthy, scaling reaches target levels in ~1.2s, +- in MC->FW, pusher reaches full scale in ~1.2s, +- when trusted pitot is healthy, lift/MC/FW handoff still follows airspeed progress, +- if trusted pitot becomes unavailable/unhealthy, handoff scaling reaches target levels in ~1.2s, - transition completion still follows airspeed threshold when pitot is healthy, - timer fallback completion still uses 5s when pitot is unavailable/unhealthy. diff --git a/docs/Settings.md b/docs/Settings.md index d83167504f1..5a949bc2aee 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -3244,7 +3244,7 @@ Enables dynamic VTOL transition progress/scaling controller shared by mission-au ### mixer_vtol_transition_scale_ramp_time_ms -Optional dynamic scaling fallback ramp duration [ms]. When > 0 and `mixer_vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling still follows trusted pitot-based transition progress when available; if trusted pitot becomes unavailable/unhealthy, scaling falls back to this timer. If set to 0, scaling falls back to transition progress/timer behavior. +Optional VTOL transition scaling ramp duration [ms]. In MC->FW, pusher scaling uses this timer regardless of pitot availability; if set to 0, pusher goes to full scale immediately. Lift/MC/FW handoff scaling still follows trusted pitot-based transition progress when available; if trusted pitot becomes unavailable/unhealthy, it falls back to this timer. If set to 0, handoff scaling falls back to transition progress/timer behavior. | Default | Min | Max | | --- | --- | --- | diff --git a/docs/VTOL.md b/docs/VTOL.md index 7221abf8400..fb2afc85dba 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -387,17 +387,23 @@ When `mixer_vtol_transition_dynamic_mixer = ON`, transition progress additionall When `mixer_vtol_transition_dynamic_mixer = OFF`, legacy static transition mixing behavior is preserved. Optional decoupled scaling ramp: -- trusted pitot available/healthy: scaling follows airspeed-based transition progress. -- `mixer_vtol_transition_scale_ramp_time_ms > 0`: if trusted pitot becomes unavailable/unhealthy, scaling falls back to this ramp timer. -- `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): if trusted pitot is unavailable/unhealthy, scaling falls back to transition progress/timer behavior. +- MC->FW pusher: + - `mixer_vtol_transition_scale_ramp_time_ms > 0`: pusher ramps from `0 -> 100%` over this time, even when trusted pitot is healthy. + - `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): pusher goes to `100%` immediately. +- Lift / MC authority / FW authority handoff: + - trusted pitot available/healthy: follows airspeed-based transition progress. + - `mixer_vtol_transition_scale_ramp_time_ms > 0`: if trusted pitot becomes unavailable/unhealthy, handoff scaling falls back to this ramp timer. + - `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): if trusted pitot is unavailable/unhealthy, handoff scaling falls back to transition progress/timer behavior. +- FW->MC keeps the existing handoff-based scaling behavior. Example: - `mixer_switch_trans_timer = 50` (5s fallback completion timer) - `mixer_vtol_transition_scale_ramp_time_ms = 1200` Result: -- when trusted pitot is healthy, pusher/lift/authority scaling still follows airspeed progress, -- if trusted pitot becomes unavailable/unhealthy, scaling reaches target levels in ~1.2s, +- in MC->FW, pusher reaches full scale in ~1.2s, +- when trusted pitot is healthy, lift/MC/FW handoff still follows airspeed progress, +- if trusted pitot becomes unavailable/unhealthy, handoff scaling reaches target levels in ~1.2s, - transition completion still follows airspeed thresholds when pitot is healthy, - if pitot is unavailable/unhealthy, completion fallback still uses 5s. @@ -448,7 +454,8 @@ CLI: What this does: - MC->FW completes primarily on pitot airspeed (1300 cm/s), with timer fallback only if pitot is unavailable/unhealthy. - FW->MC completes when airspeed drops to 850 cm/s. -- Scaling ramps quickly (1.2 s) to reduce step torque and abrupt authority handoff. +- In MC->FW, pusher ramps to full scale in 1.2 s while lift/MC/FW handoff still follows airspeed progress. +- The pusher ramp is quick enough (1.2 s) to reduce step torque while still allowing strong acceleration. - Timeout abort protects against staying too long in airspeed-controlled transition without reaching threshold. #### Test 3 - Mission-authorized transition (end-to-end mission flow) @@ -635,9 +642,12 @@ Dynamic mixer scaling (`mixer_vtol_transition_dynamic_mixer = ON`) uses this pro - MC authority ramps `vtol_transition_mc_authority_end_percent -> 1` - FW authority ramps `1 -> vtol_transition_fw_authority_start_percent` -Dynamic scaling prefers trusted pitot-based transition progress whenever available. -If trusted pitot becomes unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms > 0`, scaling falls back to that timer-based ramp. -If trusted pitot is unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms = 0`, scaling falls back to transition progress/timer behavior (`mixer_switch_trans_timer`). +Dynamic scaling splits MC->FW pusher ramp from lift/authority handoff scaling. +For MC->FW, pusher scaling uses `mixer_vtol_transition_scale_ramp_time_ms`; if this is `0`, pusher goes full immediately. +Lift / MC authority / FW authority handoff still prefers trusted pitot-based transition progress whenever available. +If trusted pitot becomes unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms > 0`, handoff scaling falls back to that timer-based ramp. +If trusted pitot is unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms = 0`, handoff scaling falls back to transition progress/timer behavior (`mixer_switch_trans_timer`). +FW->MC keeps the existing handoff-based scaling behavior. For transition/pusher motors (`-2.0 < throttle < -1.0`), output is interpolated from idle to target: diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 75dd54e4ae4..c13536c5971 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1303,7 +1303,7 @@ groups: min: 0 max: 60000 - name: mixer_vtol_transition_scale_ramp_time_ms - description: "Optional dynamic scaling fallback ramp duration [ms]. When > 0 and `mixer_vtol_transition_dynamic_mixer` is ON, pusher/lift/authority scaling still follows trusted pitot-based transition progress when available; if trusted pitot becomes unavailable/unhealthy, scaling falls back to this timer. If set to 0, scaling falls back to transition progress/timer behavior." + description: "Optional VTOL transition scaling ramp duration [ms]. In MC->FW, pusher scaling uses this timer regardless of pitot availability; if set to 0, pusher goes to full scale immediately. Lift/MC/FW handoff scaling still follows trusted pitot-based transition progress when available; if trusted pitot becomes unavailable/unhealthy, it falls back to this timer. If set to 0, handoff scaling falls back to transition progress/timer behavior." default_value: 0 field: mixer_config.vtolTransitionScaleRampTimeMs min: 0 diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index f305b57459b..31451dc0366 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -128,7 +128,7 @@ void setMixerProfileAT(void) mixerProfileAT.usedAirspeed = false; mixerProfileAT.transitionStartAirspeedCaptured = false; mixerProfileAT.progress = 0.0f; - mixerProfileAT.scalingProgress = 0.0f; + mixerProfileAT.handoffScalingProgress = 0.0f; mixerProfileAT.transitionStartAirspeedCmS = 0.0f; mixerProfileAT.blendToFw = mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW ? 0.0f : 1.0f; mixerProfileAT.pusherScale = 1.0f; @@ -162,7 +162,7 @@ static mixerProfileATDirection_e directionForRequest(const mixerProfileATRequest static void resetTransitionScales(void) { mixerProfileAT.progress = 0.0f; - mixerProfileAT.scalingProgress = 0.0f; + mixerProfileAT.handoffScalingProgress = 0.0f; mixerProfileAT.blendToFw = 0.0f; mixerProfileAT.pusherScale = 0.0f; mixerProfileAT.liftScale = 1.0f; @@ -173,7 +173,7 @@ static void resetTransitionScales(void) static void setLegacyTransitionScales(void) { mixerProfileAT.progress = 1.0f; - mixerProfileAT.scalingProgress = 1.0f; + mixerProfileAT.handoffScalingProgress = 1.0f; mixerProfileAT.blendToFw = 1.0f; mixerProfileAT.pusherScale = 1.0f; mixerProfileAT.liftScale = 1.0f; @@ -186,30 +186,43 @@ static float blendScale(float from, float to, float progress) return from + (to - from) * constrainf(progress, 0.0f, 1.0f); } -static float getScalingProgress(void) +static float getPusherRampProgress(void) +{ + if (!currentMixerConfig.vtolTransitionDynamicMixer) { + return 1.0f; + } + + if (currentMixerConfig.vtolTransitionScaleRampTimeMs <= 0) { + return 1.0f; + } + + const uint32_t elapsedMs = millis() - mixerProfileAT.transitionStartTime; + return constrainf((float)elapsedMs / (float)currentMixerConfig.vtolTransitionScaleRampTimeMs, 0.0f, 1.0f); +} + +static float getHandoffScalingProgress(void) { if (!currentMixerConfig.vtolTransitionDynamicMixer) { return 1.0f; } if (mixerProfileAT.usedAirspeed) { - mixerProfileAT.scalingProgress = constrainf(mixerProfileAT.progress, 0.0f, 1.0f); - return mixerProfileAT.scalingProgress; + mixerProfileAT.handoffScalingProgress = constrainf(mixerProfileAT.progress, 0.0f, 1.0f); + return mixerProfileAT.handoffScalingProgress; } if (currentMixerConfig.vtolTransitionScaleRampTimeMs > 0) { - const uint32_t elapsedMs = millis() - mixerProfileAT.transitionStartTime; - const float rampProgress = constrainf((float)elapsedMs / (float)currentMixerConfig.vtolTransitionScaleRampTimeMs, 0.0f, 1.0f); + const float rampProgress = getPusherRampProgress(); - // Preserve already-applied scaling if pitot drops out mid-transition. - mixerProfileAT.scalingProgress = MAX(mixerProfileAT.scalingProgress, rampProgress); - return mixerProfileAT.scalingProgress; + // Preserve already-applied handoff scaling if pitot drops out mid-transition. + mixerProfileAT.handoffScalingProgress = MAX(mixerProfileAT.handoffScalingProgress, rampProgress); + return mixerProfileAT.handoffScalingProgress; } // Last-resort compatibility path: with no trusted pitot and no dedicated // scaling ramp configured, reuse transition progress/timer behavior. - mixerProfileAT.scalingProgress = MAX(mixerProfileAT.scalingProgress, constrainf(mixerProfileAT.progress, 0.0f, 1.0f)); - return mixerProfileAT.scalingProgress; + mixerProfileAT.handoffScalingProgress = MAX(mixerProfileAT.handoffScalingProgress, constrainf(mixerProfileAT.progress, 0.0f, 1.0f)); + return mixerProfileAT.handoffScalingProgress; } static bool hasTrustedPitotAirspeed(float *airspeedCmS) @@ -319,18 +332,20 @@ static void updateTransitionScales(void) const float liftFloor = constrainf(systemConfig()->vtolTransitionLiftEndPercent / 100.0f, 0.0f, 1.0f); const float mcFloor = constrainf(systemConfig()->vtolTransitionMcAuthorityEndPercent / 100.0f, 0.0f, 1.0f); const float fwFloor = constrainf(systemConfig()->vtolTransitionFwAuthorityStartPercent / 100.0f, 0.0f, 1.0f); - const float scaleProgress = getScalingProgress(); + const float handoffProgress = getHandoffScalingProgress(); if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW) { - mixerProfileAT.pusherScale = blendScale(0.0f, 1.0f, scaleProgress); - mixerProfileAT.liftScale = blendScale(1.0f, liftFloor, scaleProgress); - mixerProfileAT.mcAuthorityScale = blendScale(1.0f, mcFloor, scaleProgress); - mixerProfileAT.fwAuthorityScale = blendScale(fwFloor, 1.0f, scaleProgress); + const float pusherProgress = getPusherRampProgress(); + + mixerProfileAT.pusherScale = blendScale(0.0f, 1.0f, pusherProgress); + mixerProfileAT.liftScale = blendScale(1.0f, liftFloor, handoffProgress); + mixerProfileAT.mcAuthorityScale = blendScale(1.0f, mcFloor, handoffProgress); + mixerProfileAT.fwAuthorityScale = blendScale(fwFloor, 1.0f, handoffProgress); } else if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_MC) { - mixerProfileAT.pusherScale = blendScale(1.0f, 0.0f, scaleProgress); - mixerProfileAT.liftScale = blendScale(liftFloor, 1.0f, scaleProgress); - mixerProfileAT.mcAuthorityScale = blendScale(mcFloor, 1.0f, scaleProgress); - mixerProfileAT.fwAuthorityScale = blendScale(1.0f, fwFloor, scaleProgress); + mixerProfileAT.pusherScale = blendScale(1.0f, 0.0f, handoffProgress); + mixerProfileAT.liftScale = blendScale(liftFloor, 1.0f, handoffProgress); + mixerProfileAT.mcAuthorityScale = blendScale(mcFloor, 1.0f, handoffProgress); + mixerProfileAT.fwAuthorityScale = blendScale(1.0f, fwFloor, handoffProgress); } mixerProfileAT.blendToFw = constrainf(mixerProfileAT.fwAuthorityScale, 0.0f, 1.0f); diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index 13f690b5362..cbc69066ea0 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -68,7 +68,7 @@ typedef struct mixerProfileAT_s { bool usedAirspeed; bool transitionStartAirspeedCaptured; float progress; - float scalingProgress; + float handoffScalingProgress; float transitionStartAirspeedCmS; float blendToFw; float pusherScale; From 94a6b345a8fc4956db0e3df28d253727705f5a32 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Thu, 4 Jun 2026 15:27:25 +0300 Subject: [PATCH 20/26] vtol: restore manual profile hot-switch during transition --- docs/MixerProfile.md | 1 - docs/VTOL.md | 1 - src/main/flight/mixer_profile.c | 30 ++---------------------------- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 53dc7272fbc..b0609100772 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -35,7 +35,6 @@ The use of Transition Mode is recommended to enable further features and future - Keeping the mode ON does not repeatedly restart transitions. - A new transition requires mode OFF then ON again. - If switched OFF before hot-switch completes, the manual transition request is aborted. -- If valid pitot is present and MC->FW threshold is configured, direct manual profile hot-switch to FW is blocked until threshold is reached. - Optional FW protection: `vtol_fw_to_mc_auto_switch_airspeed_cm_s` can auto-request FW->MC transition when valid pitot airspeed drops to/below the configured value (`0` disables). This edge-triggered behavior is enabled by `mixer_vtol_manualswitch_autotransition_controller`. diff --git a/docs/VTOL.md b/docs/VTOL.md index fb2afc85dba..33275f5a899 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -320,7 +320,6 @@ With `mixer_vtol_manualswitch_autotransition_controller = ON`: - Keeping the mode ON does not repeatedly retrigger transition. - To start another transition, mode must go OFF then ON again. - If mode is turned OFF before hot-switch, transition request is aborted safely. -- If valid pitot is present and MC->FW airspeed threshold is configured, direct manual profile hot-switch to FW is blocked until threshold is reached. - Optional FW safety fallback: set `vtol_fw_to_mc_auto_switch_airspeed_cm_s > 0` to auto-request FW->MC transition when valid pitot airspeed drops to/below the configured value. With `mixer_vtol_manualswitch_autotransition_controller = OFF`: diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 31451dc0366..7f477b9b6a4 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -276,29 +276,6 @@ static uint16_t getAirspeedThresholdForDirection(const mixerProfileATDirection_e return 0; } -static bool shouldBlockManualDirectSwitchToFixedWing(const bool manualControllerEnabled, const int requestedProfileIndex) -{ - if (!manualControllerEnabled || !STATE(MULTIROTOR) || requestedProfileIndex == currentMixerProfileIndex) { - return false; - } - - if (mixerConfigByIndex(requestedProfileIndex)->platformType != PLATFORM_AIRPLANE) { - return false; - } - - const uint16_t thresholdCmS = getAirspeedThresholdForDirection(MIXERAT_DIRECTION_TO_FW); - if (thresholdCmS == 0 || !hasPitotSensorForManualProtection()) { - return false; - } - - float airspeedCmS = 0.0f; - if (!hasTrustedPitotAirspeed(&airspeedCmS)) { - return true; - } - - return airspeedCmS < thresholdCmS; -} - static bool shouldRequestManualFwToMcProtection(const bool manualControllerEnabled) { if (!manualControllerEnabled || !STATE(AIRPLANE)) { @@ -580,12 +557,9 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) mixerAT_inuse = false; } - // For manual auto-transition control, suppress direct profile hotswitch while transition trigger is active. - const bool suppressDirectProfileSwitch = manualControllerEnabled && transitionModeActive; - if (!FLIGHT_MODE(FAILSAFE_MODE) && !mixerAT_inuse && !suppressDirectProfileSwitch) + if (!FLIGHT_MODE(FAILSAFE_MODE) && !mixerAT_inuse) { - if (mixerProfileModePresent && - !shouldBlockManualDirectSwitchToFixedWing(manualControllerEnabled, requestedProfileIndex)) { + if (mixerProfileModePresent) { outputProfileHotSwitch(requestedProfileIndex); } } From 02e7f86f5b9f916c461a9d96ff29e5a7ffd64ed6 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Thu, 4 Jun 2026 19:13:19 +0300 Subject: [PATCH 21/26] vtol: simplify transition docs and remove mission track-distance setting Rewrite VTOL docs and setting descriptions in simpler user-facing language, including clearer explanations for transition behavior, mission USER flags, and smooth mixer handover. Remove nav_vtol_mission_transition_track_distance_cm from user-facing configuration and use a fixed internal MC->FW mission run-up distance instead. Bump PG_NAV_CONFIG after removing the stored nav config field. --- docs/MixerProfile.md | 187 +++++++++++++------------ docs/Navigation.md | 2 +- docs/Settings.md | 44 +++--- docs/VTOL.md | 232 +++++++++++++++---------------- src/main/fc/settings.yaml | 38 +++-- src/main/navigation/navigation.c | 7 +- src/main/navigation/navigation.h | 1 - 7 files changed, 251 insertions(+), 260 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index b0609100772..c966cf8c501 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -22,44 +22,50 @@ Please note that this is an emerging / experimental capability that will require ## Mixer Transition input -Typically, 'transition input' will be useful in MR mode to gain airspeed. -The associated motor or servo will then move accordingly when transition mode is activated. -Transition input is disabled when navigation mode is activate +`MIXER TRANSITION` is mainly used while the model is still in multicopter mode, so a forward motor or tilt servo can help the aircraft build forward speed before the switch to fixed-wing. -The use of Transition Mode is recommended to enable further features and future developments like fail-safe support. Mapping motor to servo output, or servo with logic conditions is **not** recommended +This feature is recommended for VTOL setups. It is normally blocked while navigation modes are active. +Mapping a motor to a servo output, or using servo logic conditions for this feature, is **not** recommended. -`MIXER TRANSITION` now behaves as a transition trigger/request (edge-triggered), not a continuous blend hold: +If `mixer_vtol_manualswitch_autotransition_controller = ON`, `MIXER TRANSITION` works like a start switch for one transition: -- A rising edge starts one transition (MC->FW or FW->MC depending on current profile). -- The transition state machine runs automatically to completion. -- Keeping the mode ON does not repeatedly restart transitions. -- A new transition requires mode OFF then ON again. -- If switched OFF before hot-switch completes, the manual transition request is aborted. -- Optional FW protection: `vtol_fw_to_mc_auto_switch_airspeed_cm_s` can auto-request FW->MC transition when valid pitot airspeed drops to/below the configured value (`0` disables). +- Each time you move `MIXER TRANSITION` from OFF to ON, iNAV starts one transition. +- The same switch can be used in both directions: + - MC -> transition -> FW + - FW -> transition -> MC +- After it starts, the transition keeps running until the speed target or timer target is reached. +- Leaving the switch ON does not keep restarting the transition. +- To start another transition, turn the switch OFF and then ON again. +- If you turn the switch OFF before the profile change happens, that transition request is cancelled. +- Optional extra protection: `vtol_fw_to_mc_auto_switch_airspeed_cm_s` can automatically start FW->MC if airspeed gets too low. -This edge-triggered behavior is enabled by `mixer_vtol_manualswitch_autotransition_controller`. -Set `mixer_vtol_manualswitch_autotransition_controller = ON` in both mixer profiles (MC and FW) used for switching to keep manual transition semantics consistent after profile hot-switch. -When `mixer_vtol_manualswitch_autotransition_controller = OFF`, manual transition keeps legacy behavior. -With manual auto-transition enabled, Active Modes `MIXER TRANSITION` now indicates that the internal transition controller/mixing is actually active, not merely that the RC `MIXER TRANSITION` switch is active. -Active Modes `MIXER PROFILE 2` indicates the currently active mixer profile. +This behavior is controlled by `mixer_vtol_manualswitch_autotransition_controller`. +Turn it ON in both mixer profiles if you want the same switch behavior in both directions. +If it is OFF, manual transition keeps the older behavior. -Important path split: -- `MIXER PROFILE 2` remains a direct manual profile-switch path. -- Smooth VTOL transition state-machine behavior is triggered by `MIXER TRANSITION` when `mixer_vtol_manualswitch_autotransition_controller = ON`. +In Active Modes: + +- `MIXER TRANSITION` shows that the internal transition logic is actually running. +- `MIXER PROFILE 2` shows that mixer profile 2 is currently active. + +There are two separate manual paths: + +- `MIXER PROFILE 2` is still a direct manual profile switch. +- `MIXER TRANSITION` starts the smooth automatic transition sequence when `mixer_vtol_manualswitch_autotransition_controller = ON`. + +Recommended 3-position switch example: -Recommended switch topology example (clear 3-position setup): - This example assumes the usual VTOL order used in this document: - Profile 1 = FW - Profile 2 = MC - Use a dedicated 3-position mapping: - Pos1 = FW (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) - - Pos2 = Transition trigger (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) + - Pos2 = Transition request (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) - Pos3 = MC (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) - Keep `mixer_vtol_manualswitch_autotransition_controller` ON in both profiles used by this mapping. -- Avoid overlapping FW selection and transition trigger in the same position. -- Avoid 2-position setups where one position activates both `MIXER PROFILE 2` and `MIXER TRANSITION`. -- Overlapping mode activation can produce order-dependent behavior (direct profile switch path vs transition-controller path), which is unpredictable and not recommended. -- If you intentionally swap the profile order, the same idea still works; just swap the FW and MC end positions. +- Avoid a switch position that turns ON both `MIXER PROFILE 2` and `MIXER TRANSITION`. +- If both are ON together, iNAV may switch profile immediately instead of running the smooth transition. +- If you intentionally swap the profile order, keep the same idea and swap the FW and MC end positions. ## Servo @@ -95,7 +101,10 @@ It is recommend that the pilot uses a RC mode switch to activate modes or switch Profile files Switching is not available until the runtime sensor calibration is done. Switching is NOT available when navigation mode is activate. `mixer_profile` 1 will be used as default, `mixer_profile` 2 will be used when the `MIXER PROFILE 2` mode box is activated. -Set `MIXER TRANSITION` accordingly when you want to use `MIXER TRANSITION` input for motors and servos. Here is sample of using these RC modes: +Set `MIXER TRANSITION` accordingly when you want to use `MIXER TRANSITION` input for motors and servos. + +The example below is a **legacy manual switch** example, where `MIXER TRANSITION` is used as a live transition input and `mixer_vtol_manualswitch_autotransition_controller = OFF`. +It is not the recommended mapping for the newer automatic transition controller. ![Alt text](Screenshots/mixer_profile.png) @@ -105,60 +114,67 @@ Set `MIXER TRANSITION` accordingly when you want to use `MIXER TRANSITION` input It is also possible to set it as 4 state switch by adding FW(profile1) with transition on. +If `mixer_vtol_manualswitch_autotransition_controller = ON`, do **not** use this overlap style where `MIXER PROFILE 2` and `MIXER TRANSITION` are ON together. +For the newer smooth automatic transition behavior, use the dedicated 3-position mapping shown earlier in this document. + ## Automated Transition -This feature is mainly for RTH in a failsafe event. When set properly, model will use the FW mode to fly home efficiently, And land in the MC mode for easier landing. -`ON` for a mixer_profile\`s `mixer_automated_switch` means to schedule a Automated Transition when RTH head home(applies for MC mixer_profile) or RTH Land(applies for FW mixer_profile) is requested by navigation controller. -Set `mixer_automated_switch` to `ON` in mixer_profile for MC mode. Set `mixer_switch_trans_timer` in mixer_profile for MC mode for the time required to gain airspeed for your model before entering to FW mode. -When `mixer_automated_switch`:`OFF` is set for all mixer_profiles(defaults). Model will not perform automated transition at all. +This feature is mainly used for RTH and failsafe. +When set up correctly, the aircraft can use fixed-wing for the efficient flight home and then return to multicopter for easier landing. + +If `mixer_automated_switch = ON` in a mixer profile, iNAV is allowed to run an automated transition when the navigation logic asks for it. + +- Use `mixer_automated_switch = ON` in the MC mixer profile if you want automated MC->FW transition during the head-home part of RTH. +- Use `mixer_automated_switch = ON` in the FW mixer profile if you want automated FW->MC transition for the landing part. +- Set `mixer_switch_trans_timer` in each profile to a sensible backup time for that direction. + +If `mixer_automated_switch = OFF` in all mixer profiles, automated VTOL transition is disabled. ### Unified VTOL transition controller -Manual `MIXER TRANSITION` and mission-authorized VTOL transition both use the same internal transition controller. -This controller always computes transition progress/completion and performs its own profile hot-switch only inside the authorized transition state. -Direct manual `MIXER PROFILE 2` switching remains a separate path when no transition controller path is active. -When `mixer_vtol_transition_dynamic_mixer = ON`, transition scaling is split into: -- a pusher ramp for MC->FW, and -- handoff scaling for lift / MC authority / FW authority. +Manual `MIXER TRANSITION` and mission-requested VTOL transition both use the same internal transition controller. +That means the same airspeed checks, timer fallback, and smooth power changes are reused in both cases. + +Direct manual `MIXER PROFILE 2` switching is still a separate path when you want an immediate profile change. ### Airspeed-first completion -When pitot airspeed is healthy and available, transition completion uses pitot thresholds: +When valid pitot airspeed is available, iNAV uses airspeed to decide when the transition is complete: - `vtol_transition_to_fw_min_airspeed_cm_s` for MC->FW - `vtol_transition_to_mc_max_airspeed_cm_s` for FW->MC -If pitot is unavailable/unhealthy (or threshold is `0`), timer fallback is used (`mixer_switch_trans_timer`). +If pitot is not available, not healthy, or the threshold is set to `0`, iNAV uses `mixer_switch_trans_timer` instead. Ground speed is not used for transition completion/progress. -Optional safety timeout: +Optional timeout: -- `mixer_vtol_transition_airspeed_timeout_ms` can abort transition if airspeed condition is not met in time. -- This timeout is only active while transition completion is using trusted pitot airspeed. -- If pitot is unavailable/unhealthy, transition completion falls back to `mixer_switch_trans_timer`; timeout does not force abort in that fallback path. -- For airspeed-first setups, configure non-zero `mixer_switch_trans_timer` fallback (typical `40..60`, i.e. `4..6s`) so pitot-loss fallback does not complete immediately. +- `mixer_vtol_transition_airspeed_timeout_ms` limits how long iNAV waits for the required airspeed. +- This timeout is only used while pitot airspeed is actually controlling the transition. +- If pitot is lost, iNAV falls back to `mixer_switch_trans_timer` and this timeout no longer decides the outcome. +- For pitot-based setups, use a non-zero `mixer_switch_trans_timer` as a sensible backup time, typically `40..60` (`4..6s`). -### Dynamic scaling (optional) +### Smooth power changes during transition (optional) -When `mixer_vtol_transition_dynamic_mixer = ON`, transition progress scales: +When `mixer_vtol_transition_dynamic_mixer = ON`, iNAV can smoothly change: -- pusher contribution (`-2.0 < throttle < -1.0` motors) from configured max toward 0/100% depending on direction, -- lift motor throttle contribution (`vtol_transition_lift_end_percent`), -- MC stabilization authority (`vtol_transition_mc_authority_end_percent`), -- FW authority start level (`vtol_transition_fw_authority_start_percent`, servo transition input blend). +- forward motor power, +- lift motor power, +- multicopter stabilisation strength, +- fixed-wing control strength. Default is OFF to preserve existing behavior. -With dynamic scaling enabled, `vtol_transition_fw_authority_start_percent = 100` preserves legacy FW authority handoff; lower values provide smoother ramp-in. +With it ON, `vtol_transition_fw_authority_start_percent = 100` keeps the old fixed-wing control behavior. Lower values bring fixed-wing control in more gently. -Optional scaling ramp timer: +How `mixer_vtol_transition_scale_ramp_time_ms` works: - MC->FW pusher: - - `mixer_vtol_transition_scale_ramp_time_ms > 0`: pusher ramps from `0 -> 100%` over this time, even when trusted pitot is healthy. - - `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): pusher goes to `100%` immediately. -- Lift / MC authority / FW authority handoff: - - trusted pitot available/healthy: follows airspeed-based transition progress. - - `mixer_vtol_transition_scale_ramp_time_ms > 0`: if trusted pitot becomes unavailable/unhealthy, handoff scaling falls back to this timer. - - `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): if trusted pitot is unavailable/unhealthy, handoff scaling falls back to transition progress/timer behavior. -- FW->MC keeps the existing handoff-based scaling behavior. + - `> 0`: forward motor power ramps from `0 -> 100%` over this time, even when pitot is working normally. + - `= 0` (default): forward motor power goes to `100%` immediately. +- Lift motor power, MC stabilisation, and FW control: + - with valid pitot airspeed, they still follow transition progress based on airspeed. + - if pitot stops being usable and this setting is `> 0`, they use this same timer as a backup ramp. + - if pitot stops being usable and this setting is `0`, they fall back to the normal transition timer/progress behavior. +- FW->MC keeps the existing style of smooth handover. Example: @@ -166,21 +182,22 @@ Example: - `mixer_vtol_transition_scale_ramp_time_ms = 1200` Result: -- in MC->FW, pusher reaches full scale in ~1.2s, -- when trusted pitot is healthy, lift/MC/FW handoff still follows airspeed progress, -- if trusted pitot becomes unavailable/unhealthy, handoff scaling reaches target levels in ~1.2s, -- transition completion still follows airspeed threshold when pitot is healthy, -- timer fallback completion still uses 5s when pitot is unavailable/unhealthy. +- in MC->FW, the forward motor reaches full power in about `1.2s`, +- when pitot is working, lift power and control handover still follow airspeed, +- if pitot stops being usable, the same handover reaches its target in about `1.2s`, +- transition completion still uses airspeed when pitot is working, +- backup completion time is still `5s` if pitot is not usable. ### Mission-authorized VTOL transition (waypoint User Action) -INAV supports mission-requested VTOL transitions through the existing automated transition path. This is configured with: +INAV can also change between MC and FW from the mission itself. +This is configured with: - `nav_vtol_mission_transition_user_action` (`OFF`, `USER1`, `USER2`, `USER3`, `USER4`) - `nav_vtol_mission_transition_min_altitude_cm` (optional, `0` disables minimum-altitude check) - `vtol_transition_to_fw_min_airspeed_cm_s` (preferred MC->FW threshold) -Scope note: +Setting scope: - Per-mixer-profile settings: - `mixer_automated_switch` @@ -198,24 +215,21 @@ Scope note: - `vtol_transition_fw_authority_start_percent` - `nav_vtol_mission_transition_user_action` - `nav_vtol_mission_transition_min_altitude_cm` - - `nav_vtol_mission_transition_track_distance_cm` -On each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`), the configured USER action bit is used as absolute target selector: +On each navigable mission waypoint (`WAYPOINT`, `POSHOLD_TIME`, `LAND`), the chosen USER flag tells iNAV which flight mode should be active there: -- selected USER bit = `0` -> transition to MC / MULTIROTOR profile -- selected USER bit = `1` -> transition to FW / AIRPLANE profile -- When `nav_vtol_mission_transition_user_action != OFF`, each navigable waypoint encodes a target state via that selected bit. -- This is a per-waypoint target-state declaration (not an event trigger). Users should intentionally set/clear the selected USER bit on each navigable waypoint. -- This is **not** a toggle command. -- If already in the requested profile type, the action is treated as complete (idempotent). +- selected USER flag = `0` -> target MC / MULTIROTOR profile +- selected USER flag = `1` -> target FW / AIRPLANE profile +- This is set per waypoint. It is **not** a toggle command. +- If the aircraft is already in the requested mode, iNAV does nothing and continues. The mission pauses while transition is in progress and resumes after completion. -For MC -> FW mission transitions, navigation uses a straight acceleration segment (no loiter) to build speed before hot-switch. -Mission path uses the same controller and completion logic as manual transition (airspeed-first, timer fallback). +For MC->FW mission transitions, navigation uses a built-in straight acceleration run to build speed before the switch to fixed-wing. +Mission transition uses the same transition logic as manual transition: airspeed first, timer as backup. -Manual RC switching (`MIXER PROFILE 2`, `MIXER TRANSITION`) remains blocked during normal active navigation. Mission VTOL transition does not bypass the hot-switch safety guard; it only authorizes switching inside the automated transition state. -Mission VTOL transition still relies on normal profile-switch infrastructure: configure two mixer profiles and a valid `MIXER PROFILE 2` mode activation condition. +Manual RC switching (`MIXER PROFILE 2`, `MIXER TRANSITION`) is still blocked during normal active navigation. +Mission VTOL transition still relies on the normal two-profile setup, so you must configure both mixer profiles and a valid `MIXER PROFILE 2` mode condition. ### Example test presets (VTOL ~1.0m wingspan, ~1720g AUW) @@ -234,10 +248,10 @@ CLI: - `set nav_vtol_mission_transition_user_action = OFF` Behavior: -- Preserves legacy-style transition mixing while still using the new controller path. -- Useful as a known-safe baseline before enabling dynamic scaling. +- Keeps behavior close to the older transition setup. +- Good as a known-safe starting point before enabling the smoother power changes. -#### Test 2 - Airspeed-first + dynamic scaling (manual tuning) +#### Test 2 - Airspeed-first + smooth power changes (manual tuning) CLI: - `set mixer_vtol_manualswitch_autotransition_controller = ON` @@ -253,8 +267,8 @@ CLI: - `set nav_vtol_mission_transition_user_action = OFF` Behavior: -- Uses pitot-first completion logic with timer fallback only when pitot is unavailable/unhealthy. -- Adds fast, smooth pusher/lift/authority ramping to reduce abrupt transitions. +- Uses pitot airspeed first, with timer fallback only if pitot is not usable. +- Adds smoother forward-motor, lift-motor, and control handover to reduce abrupt transitions. #### Test 3 - Mission-authorized transition (mission integration) @@ -271,10 +285,9 @@ CLI: - `set vtol_transition_fw_authority_start_percent = 20` - `set nav_vtol_mission_transition_user_action = USER1` - `set nav_vtol_mission_transition_min_altitude_cm = 1200` -- `set nav_vtol_mission_transition_track_distance_cm = 4000` Behavior: -- Uses USER1 as per-waypoint absolute target selector (clear=MC, set=FW). +- Uses USER1 as the per-waypoint target selector (`clear = MC`, `set = FW`). - Pauses mission progression during transition and resumes only after transition completion. ### Validation Matrix (PR / SITL / HITL) @@ -284,7 +297,7 @@ Behavior: - FW->MC manual, pitot healthy/available. - FW->MC manual, no pitot (timer fallback). - `MIXER TRANSITION` held ON after completion (no repeated starts). -- `MIXER TRANSITION` OFF before hot-switch (safe abort). +- `MIXER TRANSITION` OFF before profile change (safe abort). - Mission transition with selected USER bit = `1` (TO_FW). - Mission transition with selected USER bit = `0` (TO_MC). - Failsafe/disarm during active transition (abort and no blind mission resume). @@ -306,7 +319,7 @@ Debug channels: - bit3: transition mixing output active (`isMixerTransitionMixing`) - bit4: RC `MIXERTRANSITION` mode active - bit5: airspeed-controlled path in use - - bit6: hot-switch done + - bit6: profile change done - bit7: transition aborted - bit8: manual VTOL auto-transition controller enabled in current mixer config - bit9: dynamic transition mixer enabled in current mixer config @@ -321,7 +334,7 @@ Debug channels: - `debug[3]` = progress x1000 (`0..1000`) - `debug[4]` = pusher scale x1000 (`0..1000`) - `debug[5]` = lift scale x1000 (`0..1000`) -- `debug[6]` = MC authority scale x1000 (`0..1000`) +- `debug[6]` = MC stabilisation scale x1000 (`0..1000`) - `debug[7]` = current mixer profile pitch transition PID multiplier (`transition_PID_mmix_multiplier_pitch`) ## TailSitter (planned for INAV 7.1) diff --git a/docs/Navigation.md b/docs/Navigation.md index 1a96d947d40..e3598cf5fc0 100755 --- a/docs/Navigation.md +++ b/docs/Navigation.md @@ -110,7 +110,7 @@ Configuration: - `nav_vtol_mission_transition_user_action` selects which waypoint User Action (`USER1..USER4`) is used as the mission VTOL target selector. - `nav_vtol_mission_transition_min_altitude_cm` optionally enforces a minimum altitude before transition start (`0` disables check). -- `nav_vtol_mission_transition_track_distance_cm` configures straight-line MC->FW transition guidance distance. +- During MC->FW mission transition, iNAV uses a built-in straight run-up target to help the model build speed before switching to fixed-wing. - VTOL transition completion logic is shared with manual MIXER TRANSITION and uses mixer transition settings: - preferred MC->FW threshold: `vtol_transition_to_fw_min_airspeed_cm_s` - FW->MC threshold: `vtol_transition_to_mc_max_airspeed_cm_s` diff --git a/docs/Settings.md b/docs/Settings.md index 5a949bc2aee..14062ec4a33 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -3202,9 +3202,7 @@ If enabled, control_profile_index will follow mixer_profile index. Set to OFF(de ### mixer_switch_trans_timer -If switch another mixer_profile is scheduled by mixer_automated_switch or mixer_automated_switch. Activate Mixertransion motor/servo mixing for this many decisecond(0.1s) before the actual mixer_profile switch. - -If trusted pitot is unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms = 0`, dynamic scaling also falls back to this transition progress/timer behavior. +Time, in deciseconds (0.1s), that transition motors or servos stay active before iNAV changes to the other mixer profile during an automated VTOL switch. This is also the backup transition time when pitot airspeed is not available. If `mixer_vtol_transition_scale_ramp_time_ms = 0`, the other smooth transition power changes also fall back to this timing. | Default | Min | Max | | --- | --- | --- | @@ -3214,7 +3212,7 @@ If trusted pitot is unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_ ### mixer_vtol_manualswitch_autotransition_controller -Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior. For consistent manual transition semantics, enable this in both mixer profiles. +Makes `MIXER TRANSITION` start one automatic VTOL transition each time the switch moves from OFF to ON, when not in waypoint mission. Turn this ON in both mixer profiles if you want the same behavior in both directions. OFF keeps the older manual switch behavior. | Default | Min | Max | | --- | --- | --- | @@ -3224,7 +3222,7 @@ Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` ### mixer_vtol_transition_airspeed_timeout_ms -Safety timeout [ms] for airspeed-controlled transitions. If non-zero and required airspeed condition is not met in time, transition aborts instead of force-completing. +How long iNAV will wait for the required pitot airspeed during an airspeed-based transition. If the target airspeed is not reached in time, the transition is aborted. Set to 0 to disable. | Default | Min | Max | | --- | --- | --- | @@ -3234,7 +3232,7 @@ Safety timeout [ms] for airspeed-controlled transitions. If non-zero and require ### mixer_vtol_transition_dynamic_mixer -Enables dynamic VTOL transition progress/scaling controller shared by mission-authorized and manual MIXER TRANSITION paths. +Turns on smooth VTOL transition power changes. This affects forward motor ramp-up, lift motor power reduction, multicopter stabilisation reduction, and fixed-wing control fade-in. Used by both manual `MIXER TRANSITION` and mission-requested VTOL transitions. | Default | Min | Max | | --- | --- | --- | @@ -3244,7 +3242,7 @@ Enables dynamic VTOL transition progress/scaling controller shared by mission-au ### mixer_vtol_transition_scale_ramp_time_ms -Optional VTOL transition scaling ramp duration [ms]. In MC->FW, pusher scaling uses this timer regardless of pitot availability; if set to 0, pusher goes to full scale immediately. Lift/MC/FW handoff scaling still follows trusted pitot-based transition progress when available; if trusted pitot becomes unavailable/unhealthy, it falls back to this timer. If set to 0, handoff scaling falls back to transition progress/timer behavior. +Ramp-up time [ms] for the forward motor during MC->FW when smooth VTOL transition power changes are ON. `0` gives full forward-motor power immediately. The same timer is also used as a backup for lift motor power, multicopter stabilisation, and fixed-wing control fade if pitot airspeed is lost or unavailable. | Default | Min | Max | | --- | --- | --- | @@ -4671,7 +4669,7 @@ Defines how Pitch/Roll input from RC receiver affects flight in POSHOLD mode: AT ### nav_vtol_mission_transition_min_altitude_cm -Minimum altitude [cm] required to start a mission-authorized VTOL transition. Set to 0 to disable the minimum-altitude check. +Do not start a mission-requested VTOL transition below this altitude [cm]. Set to 0 to disable the altitude check. | Default | Min | Max | | --- | --- | --- | @@ -4679,19 +4677,9 @@ Minimum altitude [cm] required to start a mission-authorized VTOL transition. Se --- -### nav_vtol_mission_transition_track_distance_cm - -Straight-line target distance [cm] used during mission-authorized MC->FW transition guidance. This controls how far ahead the transition heading target is placed. - -| Default | Min | Max | -| --- | --- | --- | -| 100000 | 1000 | 500000 | - ---- - ### nav_vtol_mission_transition_user_action -Selects which waypoint USER action bit (`USER1`..`USER4`) is used as mission VTOL target selector. OFF disables this feature. On navigable mission waypoints: selected USER bit = 1 requests FW profile, selected USER bit = 0 requests MC profile. This is an absolute per-waypoint target-state selector and relies on existing mixer profile switching infrastructure (two profiles and valid MIXER PROFILE 2 mode activation condition). +Chooses which waypoint USER flag (`USER1`..`USER4`) tells iNAV which flight mode to use at each navigable waypoint. Selected USER flag ON means fixed-wing. Selected USER flag OFF means multicopter. OFF disables this feature. Requires two mixer profiles and a working `MIXER PROFILE 2` mode setup. | Allowed Values | | | --- | --- | @@ -4705,7 +4693,7 @@ Selects which waypoint USER action bit (`USER1`..`USER4`) is used as mission VTO ### nav_vtol_transition_fail_action_fw_to_mc -Action executed after a final FW->MC transition failure. LOITER switches to POSHOLD hold at current position (fixed-wing loiter/orbit around current point). FORCE_SWITCH attempts an immediate mixer hot-switch even after failed criteria. +What iNAV should do if FW->MC transition fails. `LOITER` keeps the aircraft near its current position. `FORCE_SWITCH` changes to the other mixer profile immediately even though the normal switch conditions were not met. | Allowed Values | | | --- | --- | @@ -4719,7 +4707,7 @@ Action executed after a final FW->MC transition failure. LOITER switches to POSH ### nav_vtol_transition_fail_action_mc_to_fw -Action executed after a final MC->FW transition failure (after retry logic, if enabled). +What iNAV should do if MC->FW transition still fails after the final attempt. | Allowed Values | | | --- | --- | @@ -4732,7 +4720,7 @@ Action executed after a final MC->FW transition failure (after retry logic, if e ### nav_vtol_transition_retry_on_airspeed_timeout -If ON, allows one retry for failed airspeed-gated MC->FW auto-transition (mission or RTH head-home): hold position, perform a 360deg yaw scan, align to best measured pitot airspeed heading, and retry transition once. +If ON, iNAV gets one extra MC->FW attempt after an airspeed timeout during mission or RTH. It pauses, yaws around to find the best airspeed direction, then tries once more. | Default | Min | Max | | --- | --- | --- | @@ -7090,7 +7078,7 @@ Warning voltage per cell, this triggers battery-warning alarms, in 0.01V units, ### vtol_fw_to_mc_auto_switch_airspeed_cm_s -Automatic FW->MC protection threshold [cm/s] used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. If set above 0 and valid pitot airspeed is at/below this value while in FW, controller requests FW->MC transition automatically. Set to 0 to disable. +Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV can start FW->MC transition automatically. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable. | Default | Min | Max | | --- | --- | --- | @@ -7100,7 +7088,7 @@ Automatic FW->MC protection threshold [cm/s] used only when `mixer_vtol_manualsw ### vtol_transition_fw_authority_start_percent -Initial fixed-wing authority scale at transition start, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. +How much fixed-wing control is available at the start of transition, in percent. `100` gives full fixed-wing control immediately. Lower values bring in fixed-wing control more gently. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. | Default | Min | Max | | --- | --- | --- | @@ -7110,7 +7098,7 @@ Initial fixed-wing authority scale at transition start, in percent. Used only wh ### vtol_transition_lift_end_percent -Target vertical-lift throttle scale at transition end, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. +How much lift motor power remains at the end of transition, in percent. `100` keeps full lift power. Lower values reduce lift motor power more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. | Default | Min | Max | | --- | --- | --- | @@ -7120,7 +7108,7 @@ Target vertical-lift throttle scale at transition end, in percent. Used only whe ### vtol_transition_mc_authority_end_percent -Target multicopter stabilization authority scale at transition end, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. +How much multicopter stabilisation remains at the end of transition, in percent. `100` keeps full multicopter stabilisation. Lower values reduce it more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. | Default | Min | Max | | --- | --- | --- | @@ -7130,7 +7118,7 @@ Target multicopter stabilization authority scale at transition end, in percent. ### vtol_transition_to_fw_min_airspeed_cm_s -Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. If 0, MC->FW uses timer fallback (`mixer_switch_trans_timer`). +Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete. If set to 0, iNAV uses the transition timer instead. | Default | Min | Max | | --- | --- | --- | @@ -7140,7 +7128,7 @@ Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspe ### vtol_transition_to_mc_max_airspeed_cm_s -Maximum pitot airspeed [cm/s] allowed to complete FW->MC transition when airspeed is healthy and available. If 0, FW->MC uses timer fallback. +When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower. If set to 0, iNAV uses the transition timer instead. | Default | Min | Max | | --- | --- | --- | diff --git a/docs/VTOL.md b/docs/VTOL.md index 33275f5a899..484ce28c47b 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -180,6 +180,11 @@ You must also assign the tilting servos values using the MAX values. If you don | :-- | :-- | :-- | | Profile1(FW) with transition off | Profile2(MC) with transition on | Profile2(MC) with transition off | +- This is a **legacy manual switch** example, where `MIXER TRANSITION` is used as a live transition input. +- It is suitable for the older manual behavior with `mixer_vtol_manualswitch_autotransition_controller = OFF`. +- If you use `mixer_vtol_manualswitch_autotransition_controller = ON`, do **not** use this overlap mapping. +- For the newer smooth automatic transition behavior, use the dedicated 3-position mapping described later in this document, where `MIXER PROFILE 2` and `MIXER TRANSITION` are not ON together. + - Profile file switching becomes available after completing the runtime sensor calibration (15-30s after booting). And It is **not available** when a navigation mode or position hold is active. - By default, `mixer_profile 1` is used. `mixer_profile 2` is used when the `MIXER PROFILE 2` mode is activate. Once configured successfully, you will notice that the profiles and model preview changes accordingly when you refresh the relevant INAV Configurator tabs. @@ -295,116 +300,114 @@ INAV now uses one internal VTOL transition controller for both: - manual `MIXER TRANSITION` requests, and - mission-authorized VTOL transitions. -This keeps one safety boundary for profile hot-switching and avoids separate transition implementations. +This keeps one safety boundary for profile changes and avoids separate transition implementations. ### Behavior summary -- Transition progress is always computed internally. -- Pitot airspeed is the primary source for transition completion when healthy/available. -- Timer is used as fallback when pitot is unavailable/unhealthy. +- iNAV always tracks transition progress internally. +- If valid pitot airspeed is available, airspeed is the main way iNAV decides when transition is complete. +- If pitot is not available, iNAV falls back to a timer. - Ground speed is not used for transition completion. -- Mission transition uses the same controller and does not directly manipulate motors. -- Manual `MIXER PROFILE` / `MIXER TRANSITION` bypass during normal waypoint navigation is still blocked. -- `MIXER PROFILE 2` remains a direct profile-switch path when used manually. -- Smooth/automatic transition behavior is triggered by `MIXER TRANSITION` (with manual auto-controller ON) or by mission-authorized transition requests. +- Mission VTOL transition uses the same controller and does not directly drive the motors by itself. +- During normal waypoint navigation, manual `MIXER PROFILE` and `MIXER TRANSITION` switching is still blocked. +- `MIXER PROFILE 2` is still a direct manual profile switch when you are flying manually. +- Smooth automatic transition is started by `MIXER TRANSITION` when the manual auto-controller is ON, or by a mission transition request. ### Manual transition semantics -Intent: this does not replace legacy manual behavior. Legacy remains available and selectable. +This does not remove the older manual behavior. The older behavior is still available if you want it. With `mixer_vtol_manualswitch_autotransition_controller = ON`: -- Enable this setting in both mixer profiles (MC and FW) for consistent edge-triggered behavior across profile hot-switches. -- `MIXER TRANSITION` acts as an edge-triggered request. -- A rising edge starts one transition. -- Transition then runs autonomously to completion. -- Keeping the mode ON does not repeatedly retrigger transition. -- To start another transition, mode must go OFF then ON again. -- If mode is turned OFF before hot-switch, transition request is aborted safely. -- Optional FW safety fallback: set `vtol_fw_to_mc_auto_switch_airspeed_cm_s > 0` to auto-request FW->MC transition when valid pitot airspeed drops to/below the configured value. +- Turn this ON in both mixer profiles if you want the same behavior in both directions. +- Each time `MIXER TRANSITION` moves from OFF to ON, iNAV starts one transition. +- After it starts, the transition keeps running until the speed target or timer target is reached. +- Leaving the switch ON does not keep restarting the transition. +- To start another transition, turn the switch OFF and then ON again. +- If you turn the switch OFF before the profile change happens, that transition request is cancelled. +- Optional extra protection: set `vtol_fw_to_mc_auto_switch_airspeed_cm_s > 0` if you want FW->MC to start automatically when pitot airspeed becomes too low. With `mixer_vtol_manualswitch_autotransition_controller = OFF`: -- legacy manual behavior is preserved for backward compatibility. +- the older manual behavior is preserved. -Typical 3-position switch workflow (edge-trigger mode enabled): -- Position 1: MC -- Position 2: Transition (trigger AUTO transition sequence) -- Position 3: FW +Typical 3-position switch workflow: +- Position 1: FW +- Position 2: Transition request +- Position 3: MC Operational example: -- fly in MC (pos1) -> move to Transition (pos2) to start automatic MC->FW transition -> after completion move to FW (pos3), -- reverse order for FW->MC. +- fly in MC (pos3) -> move to Transition (pos2) to start automatic MC->FW transition -> after completion move to FW (pos1) +- reverse the order for FW->MC Important RC mapping constraint: - Use a dedicated 3-position mapping where: - - Pos1 = MC (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) + - Pos1 = FW (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) - Pos2 = Transition trigger (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) - - Pos3 = FW (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) + - Pos3 = MC (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) - Keep `mixer_vtol_manualswitch_autotransition_controller` ON in both profiles used by this mapping. -- Do not overlap/merge FW selection and transition trigger in the same switch position. -- Do not use a 2-position mapping where one position enables both `MIXER PROFILE 2` and `MIXER TRANSITION`. -- Mixing these mode conditions can cause race/order-dependent behavior (direct profile switch versus transition state machine), which is unpredictable in flight. +- Avoid a switch position that turns ON both `MIXER PROFILE 2` and `MIXER TRANSITION`. +- If both are ON together, iNAV may switch profile immediately instead of running the smooth transition. ### Mission-authorized transition semantics Mission transition is configured with `nav_vtol_mission_transition_user_action`. - `OFF`: feature disabled. -- `USER1`..`USER4`: selected User Action bit is used as target selector on navigable waypoints. -- selected bit `0` -> target MC profile -- selected bit `1` -> target FW profile -- when enabled, every navigable waypoint implicitly declares desired VTOL platform state through that selected bit, so users should set/clear it intentionally per waypoint +- `USER1`..`USER4`: the selected USER flag becomes the flight-mode selector on navigable waypoints. +- selected flag `0` -> target MC profile +- selected flag `1` -> target FW profile +- when this feature is ON, every navigable waypoint should intentionally have that USER flag either clear or set - Mission progression pauses during transition and resumes only after completion. -- If already in requested target profile, command is idempotent (no new transition). +- If the aircraft is already in the requested mode, iNAV does nothing and continues. For MC -> FW mission transition: -- guidance uses a straight acceleration segment (no loiter), +- guidance uses a straight acceleration run, - normal waypoint advancement is paused during transition. ### Airspeed-first completion logic MC -> FW: -- completion threshold: `vtol_transition_to_fw_min_airspeed_cm_s` -- if this is `0`, MC->FW uses timer fallback (`mixer_switch_trans_timer`). +- `vtol_transition_to_fw_min_airspeed_cm_s` is the target airspeed. +- If it is `0`, MC->FW uses `mixer_switch_trans_timer` instead. FW -> MC: -- completion threshold: `vtol_transition_to_mc_max_airspeed_cm_s` +- `vtol_transition_to_mc_max_airspeed_cm_s` is the airspeed that must be reached or lower. Timeout: -- `mixer_vtol_transition_airspeed_timeout_ms` can abort transition if condition is not achieved in time. -- This timeout is applied only while the transition is airspeed-controlled (trusted pitot in use). -- If pitot becomes unavailable/unhealthy, completion falls back to `mixer_switch_trans_timer` and this timeout no longer drives the decision. -- For airspeed-first setups, configure a non-zero `mixer_switch_trans_timer` fallback (typical: `40..60`, i.e. `4..6s`) to avoid immediate fallback completion when pitot is unavailable and timer fallback becomes active. +- `mixer_vtol_transition_airspeed_timeout_ms` limits how long iNAV waits for the required airspeed. +- This timeout only matters while pitot airspeed is actually controlling the transition. +- If pitot stops being usable, completion falls back to `mixer_switch_trans_timer` and this timeout no longer decides the outcome. +- For pitot-based setups, use a non-zero `mixer_switch_trans_timer` as a sensible backup time, typically `40..60` (`4..6s`). -### Dynamic mixer scaling +### Smooth power changes during transition -When `mixer_vtol_transition_dynamic_mixer = ON`, transition progress additionally scales: -- pusher transition contribution, -- vertical lift contribution, -- MC stabilization authority, -- FW transition input authority blend. +When `mixer_vtol_transition_dynamic_mixer = ON`, iNAV can smoothly change: +- forward motor power, +- lift motor power, +- multicopter stabilisation strength, +- fixed-wing control strength. -When `mixer_vtol_transition_dynamic_mixer = OFF`, legacy static transition mixing behavior is preserved. +When `mixer_vtol_transition_dynamic_mixer = OFF`, the older static behavior is preserved. -Optional decoupled scaling ramp: +How `mixer_vtol_transition_scale_ramp_time_ms` works: - MC->FW pusher: - - `mixer_vtol_transition_scale_ramp_time_ms > 0`: pusher ramps from `0 -> 100%` over this time, even when trusted pitot is healthy. - - `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): pusher goes to `100%` immediately. -- Lift / MC authority / FW authority handoff: - - trusted pitot available/healthy: follows airspeed-based transition progress. - - `mixer_vtol_transition_scale_ramp_time_ms > 0`: if trusted pitot becomes unavailable/unhealthy, handoff scaling falls back to this ramp timer. - - `mixer_vtol_transition_scale_ramp_time_ms = 0` (default): if trusted pitot is unavailable/unhealthy, handoff scaling falls back to transition progress/timer behavior. -- FW->MC keeps the existing handoff-based scaling behavior. + - `> 0`: forward motor power ramps from `0 -> 100%` over this time, even when pitot is working normally. + - `= 0` (default): forward motor power goes to `100%` immediately. +- Lift motor power, MC stabilisation, and FW control: + - with valid pitot airspeed, they still follow airspeed-based transition progress. + - if pitot stops being usable and this setting is `> 0`, they use this same timer as a backup ramp. + - if pitot stops being usable and this setting is `0`, they fall back to the normal transition timer/progress behavior. +- FW->MC keeps the existing style of smooth handover. Example: - `mixer_switch_trans_timer = 50` (5s fallback completion timer) - `mixer_vtol_transition_scale_ramp_time_ms = 1200` Result: -- in MC->FW, pusher reaches full scale in ~1.2s, -- when trusted pitot is healthy, lift/MC/FW handoff still follows airspeed progress, -- if trusted pitot becomes unavailable/unhealthy, handoff scaling reaches target levels in ~1.2s, -- transition completion still follows airspeed thresholds when pitot is healthy, -- if pitot is unavailable/unhealthy, completion fallback still uses 5s. +- in MC->FW, the forward motor reaches full power in about `1.2s`, +- when pitot is working, lift power and control handover still follow airspeed, +- if pitot stops being usable, the same handover reaches its target in about `1.2s`, +- transition completion still uses airspeed when pitot is working, +- backup completion time is still `5s` if pitot is not usable. ### Example test presets (VTOL ~1.0m wingspan, ~1720g AUW) @@ -413,7 +416,7 @@ These are example starting points for initial testing. They are not universal va #### Test 1 - Legacy-compatible baseline (manual transition check) Goal: -- Verify that the new controller does not change legacy behavior when dynamic scaling is disabled. +- Verify that the new controller does not change legacy behavior when smooth power changes are disabled. - Good first test after flashing. CLI: @@ -432,10 +435,10 @@ What this does: - Uses conservative FW->MC completion threshold. - Disables mission-authorized transition while validating manual behavior. -#### Test 2 - Airspeed-first + dynamic scaling (manual transition tuning) +#### Test 2 - Airspeed-first + smooth power changes (manual transition tuning) Goal: -- Enable the full new behavior: airspeed-first completion and smooth authority/pusher scaling. +- Enable the full new behavior: airspeed-first completion and smooth forward-motor and control handover. CLI: - `set mixer_vtol_manualswitch_autotransition_controller = ON` @@ -453,7 +456,7 @@ CLI: What this does: - MC->FW completes primarily on pitot airspeed (1300 cm/s), with timer fallback only if pitot is unavailable/unhealthy. - FW->MC completes when airspeed drops to 850 cm/s. -- In MC->FW, pusher ramps to full scale in 1.2 s while lift/MC/FW handoff still follows airspeed progress. +- In MC->FW, the forward motor ramps to full power in `1.2s` while lift power and control handover still follow airspeed progress. - The pusher ramp is quick enough (1.2 s) to reduce step torque while still allowing strong acceleration. - Timeout abort protects against staying too long in airspeed-controlled transition without reaching threshold. @@ -475,14 +478,13 @@ CLI: - `set vtol_transition_fw_authority_start_percent = 20` - `set nav_vtol_mission_transition_user_action = USER1` - `set nav_vtol_mission_transition_min_altitude_cm = 1200` -- `set nav_vtol_mission_transition_track_distance_cm = 4000` What this does: - Uses USER1 as the absolute per-waypoint target selector: - USER1 bit clear -> target MC - USER1 bit set -> target FW - Pauses mission progression during transition and resumes after completion. -- Uses straight MC->FW acceleration segment (no loiter) with a 40 m transition track distance. +- Uses a straight MC->FW acceleration segment (no loiter) before the switch to fixed-wing. - Adds a minimum altitude gate (12 m) before mission transition starts. ### Detailed effect of the three percentage settings @@ -490,35 +492,35 @@ What this does: These three settings are active only when `mixer_vtol_transition_dynamic_mixer = ON`. 1. `vtol_transition_lift_end_percent` -- Defines lift throttle scale at transition end. -- MC -> FW: lift goes from `100%` at start to `lift_end_percent` at end. -- FW -> MC: lift goes from `lift_end_percent` at start to `100%` at end. +- Sets how much lift motor power remains at the end of transition. +- MC -> FW: lift power goes from `100%` at start to `lift_end_percent` at the end. +- FW -> MC: lift power goes from `lift_end_percent` at start to `100%` at the end. Example (`vtol_transition_lift_end_percent = 20`): -- MC -> FW at 50% progress: lift scale is about 60%. -- FW -> MC at 50% progress: lift scale is about 60%. +- MC -> FW at 50% progress: lift power is about `60%`. +- FW -> MC at 50% progress: lift power is about `60%`. 2. `vtol_transition_mc_authority_end_percent` -- Defines MC stabilization authority scale at transition end. -- MC -> FW: MC authority goes from `100%` at start to `mc_authority_end_percent` at end. -- FW -> MC: MC authority goes from `mc_authority_end_percent` at start to `100%` at end. +- Sets how much multicopter stabilisation remains at the end of transition. +- MC -> FW: MC stabilisation goes from `100%` at start to `mc_authority_end_percent` at the end. +- FW -> MC: MC stabilisation goes from `mc_authority_end_percent` at start to `100%` at the end. Example (`vtol_transition_mc_authority_end_percent = 30`): -- MC -> FW at 50% progress: MC authority is about 65%. -- FW -> MC at 50% progress: MC authority is about 65%. +- MC -> FW at 50% progress: MC stabilisation is about `65%`. +- FW -> MC at 50% progress: MC stabilisation is about `65%`. 3. `vtol_transition_fw_authority_start_percent` -- Defines FW authority scale at transition start. -- MC -> FW: FW authority goes from `fw_authority_start_percent` at start to `100%` at end. -- FW -> MC: FW authority goes from `100%` at start to `fw_authority_start_percent` at end. +- Sets how much fixed-wing control is already available at the start of transition. +- MC -> FW: fixed-wing control goes from `fw_authority_start_percent` at start to `100%` at the end. +- FW -> MC: fixed-wing control goes from `100%` at start to `fw_authority_start_percent` at the end. Example (`vtol_transition_fw_authority_start_percent = 25`): -- MC -> FW at 50% progress: FW authority is about 62.5%. -- FW -> MC at 50% progress: FW authority is about 62.5%. +- MC -> FW at 50% progress: fixed-wing control is about `62.5%`. +- FW -> MC at 50% progress: fixed-wing control is about `62.5%`. Backward-compatible note: -- `vtol_transition_fw_authority_start_percent = 100` preserves legacy FW authority handoff behavior. -- Lower values provide smoother FW authority ramp-in/out. +- `vtol_transition_fw_authority_start_percent = 100` keeps the older fixed-wing control behavior. +- Lower values bring fixed-wing control in and out more gently. ## Setting Scope (Important) @@ -548,56 +550,52 @@ These are shared system-wide and are not profile-specific: - `vtol_transition_fw_authority_start_percent` - `nav_vtol_mission_transition_user_action` - `nav_vtol_mission_transition_min_altitude_cm` -- `nav_vtol_mission_transition_track_distance_cm` ## CLI Commands (English) Use these commands in CLI (`set ...`, then `save`): - `set mixer_vtol_manualswitch_autotransition_controller = ON|OFF` - - Enables edge-triggered manual transition controller. + - Makes `MIXER TRANSITION` start one automatic transition each time you turn it ON. - `set mixer_vtol_transition_dynamic_mixer = ON|OFF` - - Enables/disables dynamic progress-based scaling. + - Turns smooth transition power changes ON or OFF. - `set vtol_transition_to_fw_min_airspeed_cm_s = ` - Preferred MC -> FW completion threshold (pitot airspeed). - `set mixer_switch_trans_timer = ` - - Timer-based transition duration fallback (used when pitot airspeed is unavailable/unhealthy). + - Backup transition time used when pitot airspeed is not available. - `set vtol_transition_to_mc_max_airspeed_cm_s = ` - FW -> MC completion threshold (pitot airspeed). - `set vtol_fw_to_mc_auto_switch_airspeed_cm_s = ` - - Optional low-airspeed FW protection threshold for manual auto-transition controller (`0` disables). + - Optional low-speed protection threshold for fixed-wing (`0` disables). - `set mixer_vtol_transition_airspeed_timeout_ms = ` - - Transition timeout/abort window. + - How long iNAV waits for required pitot airspeed before aborting. - `set mixer_vtol_transition_scale_ramp_time_ms = ` - - Optional dynamic scaling ramp duration in milliseconds. `0` keeps legacy progress-coupled scaling. `>0` decouples scaling ramp time from completion timing. + - Ramp-up time for the forward motor, and backup ramp time for the other smooth transition power changes. - `set vtol_transition_lift_end_percent = <0..100>` - - Lift scale endpoint for dynamic transition. + - How much lift motor power remains at the end of transition. - `set vtol_transition_mc_authority_end_percent = <0..100>` - - MC authority endpoint for dynamic transition. + - How much multicopter stabilisation remains at the end of transition. - `set vtol_transition_fw_authority_start_percent = <0..100>` - - FW authority start level for dynamic transition. + - How much fixed-wing control is already available at the start of transition. - `set nav_vtol_mission_transition_user_action = OFF|USER1|USER2|USER3|USER4` - - Selects waypoint User Action bit used for mission VTOL target selector (absolute per-waypoint desired state). + - Selects which waypoint USER flag tells iNAV to use MC or FW at each waypoint. - `set nav_vtol_mission_transition_min_altitude_cm = ` - - Optional minimum altitude check before mission transition start (`0` disables). - -- `set nav_vtol_mission_transition_track_distance_cm = ` - - Straight-line transition guidance distance for mission MC -> FW segment. + - Optional minimum altitude before mission transition may start (`0` disables). Mission profile-switch dependency: -- Mission VTOL transition uses the existing profile hot-switch path, so two valid mixer profiles and a configured `MIXER PROFILE 2` mode activation condition are required. +- Mission VTOL transition uses the existing profile-change path, so two valid mixer profiles and a configured `MIXER PROFILE 2` mode activation condition are required. # Notes and Experiences @@ -627,26 +625,26 @@ When pitot is healthy/available, transition progress is airspeed-driven (not tim - progress = `constrain((startAirspeed - airspeed) / (startAirspeed - to_mc_threshold), 0..1)` - completion condition = `airspeed <= to_mc_threshold` -Dynamic mixer scaling (`mixer_vtol_transition_dynamic_mixer = ON`) uses this progress: +Smooth transition power changes (`mixer_vtol_transition_dynamic_mixer = ON`) use this progress: - MC -> FW: - - pusher scale ramps `0 -> 1` - - lift scale ramps `1 -> vtol_transition_lift_end_percent` - - MC authority ramps `1 -> vtol_transition_mc_authority_end_percent` - - FW authority ramps `vtol_transition_fw_authority_start_percent -> 1` + - forward motor power ramps `0 -> 1` + - lift motor power ramps `1 -> vtol_transition_lift_end_percent` + - MC stabilisation ramps `1 -> vtol_transition_mc_authority_end_percent` + - FW control ramps `vtol_transition_fw_authority_start_percent -> 1` - FW -> MC: - - pusher scale ramps `1 -> 0` - - lift scale ramps `vtol_transition_lift_end_percent -> 1` - - MC authority ramps `vtol_transition_mc_authority_end_percent -> 1` - - FW authority ramps `1 -> vtol_transition_fw_authority_start_percent` - -Dynamic scaling splits MC->FW pusher ramp from lift/authority handoff scaling. -For MC->FW, pusher scaling uses `mixer_vtol_transition_scale_ramp_time_ms`; if this is `0`, pusher goes full immediately. -Lift / MC authority / FW authority handoff still prefers trusted pitot-based transition progress whenever available. -If trusted pitot becomes unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms > 0`, handoff scaling falls back to that timer-based ramp. -If trusted pitot is unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms = 0`, handoff scaling falls back to transition progress/timer behavior (`mixer_switch_trans_timer`). -FW->MC keeps the existing handoff-based scaling behavior. + - forward motor power ramps `1 -> 0` + - lift motor power ramps `vtol_transition_lift_end_percent -> 1` + - MC stabilisation ramps `vtol_transition_mc_authority_end_percent -> 1` + - FW control ramps `1 -> vtol_transition_fw_authority_start_percent` + +MC->FW uses separate forward-motor ramp-up and control handover behavior. +For MC->FW, forward motor power uses `mixer_vtol_transition_scale_ramp_time_ms`; if this is `0`, the motor goes to full power immediately. +Lift motor power, MC stabilisation, and FW control still prefer pitot-based transition progress whenever pitot is working. +If pitot stops being usable and `mixer_vtol_transition_scale_ramp_time_ms > 0`, those other changes fall back to the same timer-based ramp. +If pitot is not usable and `mixer_vtol_transition_scale_ramp_time_ms = 0`, they fall back to the normal transition timer/progress behavior (`mixer_switch_trans_timer`). +FW->MC keeps the existing style of smooth handover. For transition/pusher motors (`-2.0 < throttle < -1.0`), output is interpolated from idle to target: diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index c13536c5971..f66dde618bd 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1281,29 +1281,29 @@ groups: field: mixer_config.automated_switch type: bool - name: mixer_switch_trans_timer - description: "If switch another mixer_profile is scheduled by mixer_automated_switch or mixer_automated_switch. Activate Mixertransion motor/servo mixing for this many decisecond(0.1s) before the actual mixer_profile switch. If trusted pitot is unavailable/unhealthy and `mixer_vtol_transition_scale_ramp_time_ms = 0`, dynamic scaling also falls back to this transition progress/timer behavior." + description: "Time, in deciseconds (0.1s), that transition motors or servos stay active before iNAV changes to the other mixer profile during an automated VTOL switch. This is also the backup transition time when pitot airspeed is not available. If `mixer_vtol_transition_scale_ramp_time_ms = 0`, the other smooth transition power changes also fall back to this timing." default_value: 0 field: mixer_config.switchTransitionTimer min: 0 max: 200 - name: mixer_vtol_transition_dynamic_mixer - description: "Enables dynamic VTOL transition progress/scaling controller shared by mission-authorized and manual MIXER TRANSITION paths." + description: "Turns on smooth VTOL transition power changes. This affects forward motor ramp-up, lift motor power reduction, multicopter stabilisation reduction, and fixed-wing control fade-in. Used by both manual `MIXER TRANSITION` and mission-requested VTOL transitions." default_value: OFF field: mixer_config.vtolTransitionDynamicMixer type: bool - name: mixer_vtol_manualswitch_autotransition_controller - description: "Enables edge-triggered manual VTOL transition controller for `MIXER TRANSITION` when not in waypoint mission. OFF keeps legacy manual transition behavior. For consistent manual transition semantics, enable this in both mixer profiles." + description: "Makes `MIXER TRANSITION` start one automatic VTOL transition each time the switch moves from OFF to ON, when not in waypoint mission. Turn this ON in both mixer profiles if you want the same behavior in both directions. OFF keeps the older manual switch behavior." default_value: OFF field: mixer_config.manualVtolTransitionController type: bool - name: mixer_vtol_transition_airspeed_timeout_ms - description: "Safety timeout [ms] for airspeed-controlled transitions. If non-zero and required airspeed condition is not met in time, transition aborts instead of force-completing." + description: "How long iNAV will wait for the required pitot airspeed during an airspeed-based transition. If the target airspeed is not reached in time, the transition is aborted. Set to 0 to disable." default_value: 0 field: mixer_config.vtolTransitionAirspeedTimeoutMs min: 0 max: 60000 - name: mixer_vtol_transition_scale_ramp_time_ms - description: "Optional VTOL transition scaling ramp duration [ms]. In MC->FW, pusher scaling uses this timer regardless of pitot availability; if set to 0, pusher goes to full scale immediately. Lift/MC/FW handoff scaling still follows trusted pitot-based transition progress when available; if trusted pitot becomes unavailable/unhealthy, it falls back to this timer. If set to 0, handoff scaling falls back to transition progress/timer behavior." + description: "Ramp-up time [ms] for the forward motor during MC->FW when smooth VTOL transition power changes are ON. `0` gives full forward-motor power immediately. The same timer is also used as a backup for lift motor power, multicopter stabilisation, and fixed-wing control fade if pitot airspeed is lost or unavailable." default_value: 0 field: mixer_config.vtolTransitionScaleRampTimeMs min: 0 @@ -2655,34 +2655,28 @@ groups: field: general.flags.waypoint_mission_restart table: nav_wp_mission_restart - name: nav_vtol_mission_transition_user_action - description: "Selects which waypoint USER action bit (`USER1`..`USER4`) is used as mission VTOL target selector. OFF disables this feature. On navigable mission waypoints: selected USER bit = 1 requests FW profile, selected USER bit = 0 requests MC profile. This is an absolute per-waypoint target-state selector and relies on existing mixer profile switching infrastructure (two profiles and valid MIXER PROFILE 2 mode activation condition)." + description: "Chooses which waypoint USER flag (`USER1`..`USER4`) tells iNAV which flight mode to use at each navigable waypoint. Selected USER flag ON means fixed-wing. Selected USER flag OFF means multicopter. OFF disables this feature. Requires two mixer profiles and a working `MIXER PROFILE 2` mode setup." default_value: "OFF" field: general.vtol_mission_transition_user_action table: nav_wp_user_action - name: nav_vtol_mission_transition_min_altitude_cm - description: "Minimum altitude [cm] required to start a mission-authorized VTOL transition. Set to 0 to disable the minimum-altitude check." + description: "Do not start a mission-requested VTOL transition below this altitude [cm]. Set to 0 to disable the altitude check." default_value: 0 field: general.vtol_mission_transition_min_altitude min: 0 max: 50000 - - name: nav_vtol_mission_transition_track_distance_cm - description: "Straight-line target distance [cm] used during mission-authorized MC->FW transition guidance. This controls how far ahead the transition heading target is placed." - default_value: 100000 - field: general.vtol_mission_transition_track_distance - min: 1000 - max: 500000 - name: nav_vtol_transition_retry_on_airspeed_timeout - description: "If ON, allows one retry for failed airspeed-gated MC->FW auto-transition (mission or RTH head-home): hold position, perform a 360deg yaw scan, align to best measured pitot airspeed heading, and retry transition once." + description: "If ON, iNAV gets one extra MC->FW attempt after an airspeed timeout during mission or RTH. It pauses, yaws around to find the best airspeed direction, then tries once more." default_value: OFF field: general.vtol_transition_retry_on_airspeed_timeout type: bool - name: nav_vtol_transition_fail_action_mc_to_fw - description: "Action executed after a final MC->FW transition failure (after retry logic, if enabled)." + description: "What iNAV should do if MC->FW transition still fails after the final attempt." default_value: "IDLE" field: general.vtol_transition_fail_action_mc_to_fw table: nav_vtol_transition_fail_action_mc_to_fw - name: nav_vtol_transition_fail_action_fw_to_mc - description: "Action executed after a final FW->MC transition failure. LOITER switches to POSHOLD hold at current position (fixed-wing loiter/orbit around current point). FORCE_SWITCH attempts an immediate mixer hot-switch even after failed criteria." + description: "What iNAV should do if FW->MC transition fails. `LOITER` keeps the aircraft near its current position. `FORCE_SWITCH` changes to the other mixer profile immediately even though the normal switch conditions were not met." default_value: "LOITER" field: general.vtol_transition_fail_action_fw_to_mc table: nav_vtol_transition_fail_action_fw_to_mc @@ -4021,37 +4015,37 @@ groups: min: 0 max: 100 - name: vtol_transition_to_fw_min_airspeed_cm_s - description: "Minimum pitot airspeed [cm/s] required to complete MC->FW transition when airspeed is healthy and available. If 0, MC->FW uses timer fallback (`mixer_switch_trans_timer`)." + description: "Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete. If set to 0, iNAV uses the transition timer instead." default_value: 0 field: vtolTransitionToFwMinAirspeed min: 0 max: 20000 - name: vtol_transition_to_mc_max_airspeed_cm_s - description: "Maximum pitot airspeed [cm/s] allowed to complete FW->MC transition when airspeed is healthy and available. If 0, FW->MC uses timer fallback." + description: "When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower. If set to 0, iNAV uses the transition timer instead." default_value: 0 field: vtolTransitionToMcMaxAirspeed min: 0 max: 20000 - name: vtol_fw_to_mc_auto_switch_airspeed_cm_s - description: "Automatic FW->MC protection threshold [cm/s] used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. If set above 0 and valid pitot airspeed is at/below this value while in FW, controller requests FW->MC transition automatically. Set to 0 to disable." + description: "Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV can start FW->MC transition automatically. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable." default_value: 0 field: vtolFwToMcAutoSwitchAirspeed min: 0 max: 20000 - name: vtol_transition_lift_end_percent - description: "Target vertical-lift throttle scale at transition end, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." + description: "How much lift motor power remains at the end of transition, in percent. `100` keeps full lift power. Lower values reduce lift motor power more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." default_value: 100 field: vtolTransitionLiftEndPercent min: 0 max: 100 - name: vtol_transition_mc_authority_end_percent - description: "Target multicopter stabilization authority scale at transition end, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." + description: "How much multicopter stabilisation remains at the end of transition, in percent. `100` keeps full multicopter stabilisation. Lower values reduce it more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." default_value: 100 field: vtolTransitionMcAuthorityEndPercent min: 0 max: 100 - name: vtol_transition_fw_authority_start_percent - description: "Initial fixed-wing authority scale at transition start, in percent. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." + description: "How much fixed-wing control is available at the start of transition, in percent. `100` gives full fixed-wing control immediately. Lower values bring in fixed-wing control more gently. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." default_value: 100 field: vtolTransitionFwAuthorityStartPercent min: 0 diff --git a/src/main/navigation/navigation.c b/src/main/navigation/navigation.c index 10a9618a82a..ef0af043612 100644 --- a/src/main/navigation/navigation.c +++ b/src/main/navigation/navigation.c @@ -90,6 +90,7 @@ #define NAV_MIXERAT_RETRY_HEADING_SETTLE_MS 500 #define NAV_MIXERAT_RETRY_HEADING_STEP_TIMEOUT_MS 6000 #define NAV_MIXERAT_RETRY_MAX_TOTAL_MS 45000 +#define NAV_MIXERAT_MISSION_TRANSITION_TRACK_DISTANCE_CM 4000 /*----------------------------------------------------------- * Compatibility for home position @@ -127,7 +128,7 @@ STATIC_ASSERT(NAV_MAX_WAYPOINTS < 254, NAV_MAX_WAYPOINTS_exceeded_allowable_rang PG_REGISTER_ARRAY(navWaypoint_t, NAV_MAX_WAYPOINTS, nonVolatileWaypointList, PG_WAYPOINT_MISSION_STORAGE, 2); #endif -PG_REGISTER_WITH_RESET_TEMPLATE(navConfig_t, navConfig, PG_NAV_CONFIG, 9); +PG_REGISTER_WITH_RESET_TEMPLATE(navConfig_t, navConfig, PG_NAV_CONFIG, 10); PG_RESET_TEMPLATE(navConfig_t, navConfig, .general = { @@ -159,7 +160,6 @@ PG_RESET_TEMPLATE(navConfig_t, navConfig, .waypoint_safe_distance = SETTING_NAV_WP_MAX_SAFE_DISTANCE_DEFAULT, // Metres - first waypoint should be closer than this .vtol_mission_transition_user_action = SETTING_NAV_VTOL_MISSION_TRANSITION_USER_ACTION_DEFAULT, .vtol_mission_transition_min_altitude = SETTING_NAV_VTOL_MISSION_TRANSITION_MIN_ALTITUDE_CM_DEFAULT, - .vtol_mission_transition_track_distance = SETTING_NAV_VTOL_MISSION_TRANSITION_TRACK_DISTANCE_CM_DEFAULT, .vtol_transition_retry_on_airspeed_timeout = SETTING_NAV_VTOL_TRANSITION_RETRY_ON_AIRSPEED_TIMEOUT_DEFAULT, .vtol_transition_fail_action_mc_to_fw = SETTING_NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_DEFAULT, .vtol_transition_fail_action_fw_to_mc = SETTING_NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_DEFAULT, @@ -2382,8 +2382,7 @@ static void updateMissionTransitionGuidance(void) navMixerATMissionTransition.request == MIXERAT_REQUEST_MISSION_TO_FW && STATE(MULTIROTOR)) { fpVector3_t transitionTarget; - const uint32_t transitionTrackDistance = navConfig()->general.vtol_mission_transition_track_distance; - calculateFarAwayTarget(&transitionTarget, navMixerATMissionTransition.heading, transitionTrackDistance); + calculateFarAwayTarget(&transitionTarget, navMixerATMissionTransition.heading, NAV_MIXERAT_MISSION_TRANSITION_TRACK_DISTANCE_CM); setDesiredPosition(&transitionTarget, navMixerATMissionTransition.heading, NAV_POS_UPDATE_XY | NAV_POS_UPDATE_Z | NAV_POS_UPDATE_HEADING); return; } diff --git a/src/main/navigation/navigation.h b/src/main/navigation/navigation.h index 03fc33f3d81..bff60d96d0e 100644 --- a/src/main/navigation/navigation.h +++ b/src/main/navigation/navigation.h @@ -438,7 +438,6 @@ typedef struct navConfig_s { uint16_t waypoint_safe_distance; // Waypoint mission sanity check distance uint8_t vtol_mission_transition_user_action; // User action slot that requests mission VTOL transition uint16_t vtol_mission_transition_min_altitude; // Minimum altitude [cm] to start mission VTOL transition (0 = disabled) - uint32_t vtol_mission_transition_track_distance; // Straight-segment target distance [cm] used during MC->FW mission transition bool vtol_transition_retry_on_airspeed_timeout; // Enables one-shot yaw-scan retry for failed airspeed-gated MC->FW auto-transition uint8_t vtol_transition_fail_action_mc_to_fw; // Action after final MC->FW transition failure uint8_t vtol_transition_fail_action_fw_to_mc; // Action after final FW->MC transition failure From 8fe633734a2dc1670281631dab03fb6ca5d378da Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Fri, 5 Jun 2026 09:08:41 +0300 Subject: [PATCH 22/26] docs(vtol): clarify transition timer roles and fallback behavior --- docs/MixerProfile.md | 9 +++++---- docs/Settings.md | 11 ++++++----- docs/VTOL.md | 13 +++++++++---- src/main/fc/settings.yaml | 10 +++++----- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index c966cf8c501..107bd1ba396 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -146,11 +146,11 @@ When valid pitot airspeed is available, iNAV uses airspeed to decide when the tr If pitot is not available, not healthy, or the threshold is set to `0`, iNAV uses `mixer_switch_trans_timer` instead. Ground speed is not used for transition completion/progress. -Optional timeout: +The three timer settings do different jobs: -- `mixer_vtol_transition_airspeed_timeout_ms` limits how long iNAV waits for the required airspeed. -- This timeout is only used while pitot airspeed is actually controlling the transition. -- If pitot is lost, iNAV falls back to `mixer_switch_trans_timer` and this timeout no longer decides the outcome. +- `mixer_switch_trans_timer` is the original VTOL transition timer. It is still the backup completion timer when trusted pitot airspeed is not being used. +- `mixer_vtol_transition_airspeed_timeout_ms` is only a maximum wait time for the required airspeed while pitot is still usable. It does not complete the transition by itself; it aborts that airspeed-controlled attempt. +- If pitot becomes unavailable during transition, iNAV stops using the airspeed timeout and falls back to `mixer_switch_trans_timer`. - For pitot-based setups, use a non-zero `mixer_switch_trans_timer` as a sensible backup time, typically `40..60` (`4..6s`). ### Smooth power changes during transition (optional) @@ -170,6 +170,7 @@ How `mixer_vtol_transition_scale_ramp_time_ms` works: - MC->FW pusher: - `> 0`: forward motor power ramps from `0 -> 100%` over this time, even when pitot is working normally. - `= 0` (default): forward motor power goes to `100%` immediately. +- This timer does not decide when the transition completes. - Lift motor power, MC stabilisation, and FW control: - with valid pitot airspeed, they still follow transition progress based on airspeed. - if pitot stops being usable and this setting is `> 0`, they use this same timer as a backup ramp. diff --git a/docs/Settings.md b/docs/Settings.md index 14062ec4a33..bf5aed2876f 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -3202,7 +3202,7 @@ If enabled, control_profile_index will follow mixer_profile index. Set to OFF(de ### mixer_switch_trans_timer -Time, in deciseconds (0.1s), that transition motors or servos stay active before iNAV changes to the other mixer profile during an automated VTOL switch. This is also the backup transition time when pitot airspeed is not available. If `mixer_vtol_transition_scale_ramp_time_ms = 0`, the other smooth transition power changes also fall back to this timing. +Original VTOL transition timer, still used as the backup completion time. If trusted pitot airspeed is not being used, iNAV completes the transition from this timer instead. If `mixer_vtol_transition_scale_ramp_time_ms = 0`, lift motor power, multicopter stabilisation, and fixed-wing control handoff also fall back to this timing. | Default | Min | Max | | --- | --- | --- | @@ -3222,7 +3222,7 @@ Makes `MIXER TRANSITION` start one automatic VTOL transition each time the switc ### mixer_vtol_transition_airspeed_timeout_ms -How long iNAV will wait for the required pitot airspeed during an airspeed-based transition. If the target airspeed is not reached in time, the transition is aborted. Set to 0 to disable. +Maximum wait time [ms] for the required pitot airspeed during an airspeed-controlled transition. This timer does not complete the transition; it only aborts it if the target airspeed is still not reached in time. If pitot becomes unavailable, iNAV falls back to `mixer_switch_trans_timer` instead. Set to 0 to disable. | Default | Min | Max | | --- | --- | --- | @@ -3242,7 +3242,7 @@ Turns on smooth VTOL transition power changes. This affects forward motor ramp-u ### mixer_vtol_transition_scale_ramp_time_ms -Ramp-up time [ms] for the forward motor during MC->FW when smooth VTOL transition power changes are ON. `0` gives full forward-motor power immediately. The same timer is also used as a backup for lift motor power, multicopter stabilisation, and fixed-wing control fade if pitot airspeed is lost or unavailable. +When smooth VTOL transition power changes are ON, this always controls the MC->FW forward motor ramp. `0` gives full forward-motor power immediately. This timer does not decide when the transition is complete. For lift motor power, multicopter stabilisation, and fixed-wing control handoff, trusted pitot airspeed still controls the change while pitot is usable; this timer is only their backup ramp if pitot becomes unavailable. | Default | Min | Max | | --- | --- | --- | @@ -7118,7 +7118,7 @@ How much multicopter stabilisation remains at the end of transition, in percent. ### vtol_transition_to_fw_min_airspeed_cm_s -Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete. If set to 0, iNAV uses the transition timer instead. +Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this target is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted. | Default | Min | Max | | --- | --- | --- | @@ -7128,7 +7128,7 @@ Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered comp ### vtol_transition_to_mc_max_airspeed_cm_s -When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower. If set to 0, iNAV uses the transition timer instead. +When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this condition is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted. | Default | Min | Max | | --- | --- | --- | @@ -7289,3 +7289,4 @@ Defines rotation rate on YAW axis that UAV will try to archive on max. stick def | 20 | 1 | 180 | --- + diff --git a/docs/VTOL.md b/docs/VTOL.md index 484ce28c47b..e4093197c6b 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -367,15 +367,16 @@ For MC -> FW mission transition: MC -> FW: - `vtol_transition_to_fw_min_airspeed_cm_s` is the target airspeed. -- If it is `0`, MC->FW uses `mixer_switch_trans_timer` instead. +- If pitot stops being usable, or if this is `0`, MC->FW uses `mixer_switch_trans_timer` instead. FW -> MC: - `vtol_transition_to_mc_max_airspeed_cm_s` is the airspeed that must be reached or lower. +- If pitot stops being usable, or if this is `0`, FW->MC uses `mixer_switch_trans_timer` instead. Timeout: -- `mixer_vtol_transition_airspeed_timeout_ms` limits how long iNAV waits for the required airspeed. -- This timeout only matters while pitot airspeed is actually controlling the transition. -- If pitot stops being usable, completion falls back to `mixer_switch_trans_timer` and this timeout no longer decides the outcome. +- `mixer_switch_trans_timer` is the original VTOL transition timer. It is still the backup completion timer when trusted pitot airspeed is not being used. +- `mixer_vtol_transition_airspeed_timeout_ms` is only a maximum wait time for the required airspeed while pitot is still usable. It does not complete the transition by itself; it aborts that airspeed-controlled attempt. +- If pitot stops being usable, iNAV stops using the airspeed timeout and falls back to `mixer_switch_trans_timer`. - For pitot-based setups, use a non-zero `mixer_switch_trans_timer` as a sensible backup time, typically `40..60` (`4..6s`). ### Smooth power changes during transition @@ -388,6 +389,9 @@ When `mixer_vtol_transition_dynamic_mixer = ON`, iNAV can smoothly change: When `mixer_vtol_transition_dynamic_mixer = OFF`, the older static behavior is preserved. +`mixer_vtol_transition_scale_ramp_time_ms` always controls the MC->FW forward-motor ramp when this feature is ON. +It does not decide when the transition completes. + How `mixer_vtol_transition_scale_ramp_time_ms` works: - MC->FW pusher: - `> 0`: forward motor power ramps from `0 -> 100%` over this time, even when pitot is working normally. @@ -641,6 +645,7 @@ Smooth transition power changes (`mixer_vtol_transition_dynamic_mixer = ON`) use MC->FW uses separate forward-motor ramp-up and control handover behavior. For MC->FW, forward motor power uses `mixer_vtol_transition_scale_ramp_time_ms`; if this is `0`, the motor goes to full power immediately. +This timer does not decide when the transition completes. Lift motor power, MC stabilisation, and FW control still prefer pitot-based transition progress whenever pitot is working. If pitot stops being usable and `mixer_vtol_transition_scale_ramp_time_ms > 0`, those other changes fall back to the same timer-based ramp. If pitot is not usable and `mixer_vtol_transition_scale_ramp_time_ms = 0`, they fall back to the normal transition timer/progress behavior (`mixer_switch_trans_timer`). diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index f66dde618bd..2ed28bb6509 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1281,7 +1281,7 @@ groups: field: mixer_config.automated_switch type: bool - name: mixer_switch_trans_timer - description: "Time, in deciseconds (0.1s), that transition motors or servos stay active before iNAV changes to the other mixer profile during an automated VTOL switch. This is also the backup transition time when pitot airspeed is not available. If `mixer_vtol_transition_scale_ramp_time_ms = 0`, the other smooth transition power changes also fall back to this timing." + description: "Original VTOL transition timer, still used as the backup completion time. If trusted pitot airspeed is not being used, iNAV completes the transition from this timer instead. If `mixer_vtol_transition_scale_ramp_time_ms = 0`, lift motor power, multicopter stabilisation, and fixed-wing control handoff also fall back to this timing." default_value: 0 field: mixer_config.switchTransitionTimer min: 0 @@ -1297,13 +1297,13 @@ groups: field: mixer_config.manualVtolTransitionController type: bool - name: mixer_vtol_transition_airspeed_timeout_ms - description: "How long iNAV will wait for the required pitot airspeed during an airspeed-based transition. If the target airspeed is not reached in time, the transition is aborted. Set to 0 to disable." + description: "Maximum wait time [ms] for the required pitot airspeed during an airspeed-controlled transition. This timer does not complete the transition; it only aborts it if the target airspeed is still not reached in time. If pitot becomes unavailable, iNAV falls back to `mixer_switch_trans_timer` instead. Set to 0 to disable." default_value: 0 field: mixer_config.vtolTransitionAirspeedTimeoutMs min: 0 max: 60000 - name: mixer_vtol_transition_scale_ramp_time_ms - description: "Ramp-up time [ms] for the forward motor during MC->FW when smooth VTOL transition power changes are ON. `0` gives full forward-motor power immediately. The same timer is also used as a backup for lift motor power, multicopter stabilisation, and fixed-wing control fade if pitot airspeed is lost or unavailable." + description: "When smooth VTOL transition power changes are ON, this always controls the MC->FW forward motor ramp. `0` gives full forward-motor power immediately. This timer does not decide when the transition is complete. For lift motor power, multicopter stabilisation, and fixed-wing control handoff, trusted pitot airspeed still controls the change while pitot is usable; this timer is only their backup ramp if pitot becomes unavailable." default_value: 0 field: mixer_config.vtolTransitionScaleRampTimeMs min: 0 @@ -4015,13 +4015,13 @@ groups: min: 0 max: 100 - name: vtol_transition_to_fw_min_airspeed_cm_s - description: "Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete. If set to 0, iNAV uses the transition timer instead." + description: "Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this target is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted." default_value: 0 field: vtolTransitionToFwMinAirspeed min: 0 max: 20000 - name: vtol_transition_to_mc_max_airspeed_cm_s - description: "When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower. If set to 0, iNAV uses the transition timer instead." + description: "When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this condition is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted." default_value: 0 field: vtolTransitionToMcMaxAirspeed min: 0 From 1d2cd6f15ffcb024ae617d9b2b71be95931e2cac Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Fri, 5 Jun 2026 09:31:54 +0300 Subject: [PATCH 23/26] vtol: support overlap mapping with manual transition controller --- docs/MixerProfile.md | 22 +++++++++++++--------- docs/VTOL.md | 14 +++++++------- src/main/flight/mixer_profile.c | 3 ++- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 107bd1ba396..108a784cf55 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -50,21 +50,20 @@ In Active Modes: There are two separate manual paths: -- `MIXER PROFILE 2` is still a direct manual profile switch. +- `MIXER PROFILE 2` is still a direct manual profile switch when `MIXER TRANSITION` is OFF. - `MIXER TRANSITION` starts the smooth automatic transition sequence when `mixer_vtol_manualswitch_autotransition_controller = ON`. +- If both are ON together while the automatic transition controller is enabled, the controller temporarily owns the profile switching. When `MIXER TRANSITION` turns OFF again, direct `MIXER PROFILE 2` switching becomes active again. -Recommended 3-position switch example: +3-position switch example: - This example assumes the usual VTOL order used in this document: - Profile 1 = FW - Profile 2 = MC -- Use a dedicated 3-position mapping: +- One supported mapping is: - Pos1 = FW (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) - Pos2 = Transition request (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) - Pos3 = MC (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) - Keep `mixer_vtol_manualswitch_autotransition_controller` ON in both profiles used by this mapping. -- Avoid a switch position that turns ON both `MIXER PROFILE 2` and `MIXER TRANSITION`. -- If both are ON together, iNAV may switch profile immediately instead of running the smooth transition. - If you intentionally swap the profile order, keep the same idea and swap the FW and MC end positions. ## Servo @@ -103,8 +102,9 @@ Profile files Switching is not available until the runtime sensor calibration is `mixer_profile` 1 will be used as default, `mixer_profile` 2 will be used when the `MIXER PROFILE 2` mode box is activated. Set `MIXER TRANSITION` accordingly when you want to use `MIXER TRANSITION` input for motors and servos. -The example below is a **legacy manual switch** example, where `MIXER TRANSITION` is used as a live transition input and `mixer_vtol_manualswitch_autotransition_controller = OFF`. -It is not the recommended mapping for the newer automatic transition controller. +Another supported mapping is where one switch position turns ON both `MIXER PROFILE 2` and `MIXER TRANSITION`. +With `mixer_vtol_manualswitch_autotransition_controller = OFF`, `MIXER TRANSITION` is used as a live transition input. +With `mixer_vtol_manualswitch_autotransition_controller = ON`, that same overlap position is used as a controller-owned transition position. ![Alt text](Screenshots/mixer_profile.png) @@ -114,8 +114,12 @@ It is not the recommended mapping for the newer automatic transition controller. It is also possible to set it as 4 state switch by adding FW(profile1) with transition on. -If `mixer_vtol_manualswitch_autotransition_controller = ON`, do **not** use this overlap style where `MIXER PROFILE 2` and `MIXER TRANSITION` are ON together. -For the newer smooth automatic transition behavior, use the dedicated 3-position mapping shown earlier in this document. +If `mixer_vtol_manualswitch_autotransition_controller = ON` and this overlap position is active: +- the smooth transition controller runs while `MIXER TRANSITION` stays ON +- direct `MIXER PROFILE 2` switching is deferred during that time +- when `MIXER TRANSITION` turns OFF, `MIXER PROFILE 2` again decides which stable mixer profile should be active + +This overlap style is supported too. ## Automated Transition This feature is mainly used for RTH and failsafe. diff --git a/docs/VTOL.md b/docs/VTOL.md index e4093197c6b..6d692489192 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -180,10 +180,11 @@ You must also assign the tilting servos values using the MAX values. If you don | :-- | :-- | :-- | | Profile1(FW) with transition off | Profile2(MC) with transition on | Profile2(MC) with transition off | -- This is a **legacy manual switch** example, where `MIXER TRANSITION` is used as a live transition input. -- It is suitable for the older manual behavior with `mixer_vtol_manualswitch_autotransition_controller = OFF`. -- If you use `mixer_vtol_manualswitch_autotransition_controller = ON`, do **not** use this overlap mapping. -- For the newer smooth automatic transition behavior, use the dedicated 3-position mapping described later in this document, where `MIXER PROFILE 2` and `MIXER TRANSITION` are not ON together. +- This is one supported mapping, where one switch position turns ON both `MIXER PROFILE 2` and `MIXER TRANSITION`. +- With `mixer_vtol_manualswitch_autotransition_controller = OFF`, `MIXER TRANSITION` is used as a live transition input. +- With `mixer_vtol_manualswitch_autotransition_controller = ON`, that same overlap position is used as a controller-owned transition position. +- While both are ON, the smooth transition controller runs and direct `MIXER PROFILE 2` switching is deferred. +- When `MIXER TRANSITION` turns OFF again, `MIXER PROFILE 2` once more decides which stable mixer profile should be active. - Profile file switching becomes available after completing the runtime sensor calibration (15-30s after booting). And It is **not available** when a navigation mode or position hold is active. @@ -339,13 +340,12 @@ Operational example: - reverse the order for FW->MC Important RC mapping constraint: -- Use a dedicated 3-position mapping where: +- One supported mapping is: - Pos1 = FW (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` OFF) - Pos2 = Transition trigger (`MIXER PROFILE 2` OFF, `MIXER TRANSITION` ON) - Pos3 = MC (`MIXER PROFILE 2` ON, `MIXER TRANSITION` OFF) - Keep `mixer_vtol_manualswitch_autotransition_controller` ON in both profiles used by this mapping. -- Avoid a switch position that turns ON both `MIXER PROFILE 2` and `MIXER TRANSITION`. -- If both are ON together, iNAV may switch profile immediately instead of running the smooth transition. +- Another supported mapping is the overlap version: while both `MIXER PROFILE 2` and `MIXER TRANSITION` are ON, the transition controller owns the switching until `MIXER TRANSITION` turns OFF again. ### Mission-authorized transition semantics diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 7f477b9b6a4..f24363ca997 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -540,6 +540,7 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) const bool missionActive = (navGetCurrentStateFlags() & NAV_AUTO_WP) != 0; const bool manualControllerConfigured = currentMixerConfig.manualVtolTransitionController && !missionActive; bool manualControllerEnabled = manualControllerConfigured || manualTransitionSessionLatched; + const bool transitionControllerOwnsProfileSwitch = manualControllerEnabled && transitionModeActive; const bool mixerProfileModePresent = isModeActivationConditionPresent(BOXMIXERPROFILE); const int requestedProfileIndex = IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) == 0 ? 0 : 1; @@ -559,7 +560,7 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) if (!FLIGHT_MODE(FAILSAFE_MODE) && !mixerAT_inuse) { - if (mixerProfileModePresent) { + if (mixerProfileModePresent && !transitionControllerOwnsProfileSwitch) { outputProfileHotSwitch(requestedProfileIndex); } } From cc85129038fc08a4eb1d6dd41e54b011cb6bfee3 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Fri, 5 Jun 2026 15:37:57 +0300 Subject: [PATCH 24/26] vtol: latch fw-to-mc protection and reuse multirotor platform helper --- docs/MixerProfile.md | 3 ++- docs/Settings.md | 2 +- docs/VTOL.md | 4 ++-- src/main/fc/settings.yaml | 2 +- src/main/flight/mixer.h | 7 +++++++ src/main/flight/mixer_profile.c | 26 ++++++++++++++++++++++---- src/main/navigation/navigation.c | 4 +--- 7 files changed, 36 insertions(+), 12 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 108a784cf55..a55f8d10962 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -37,7 +37,7 @@ If `mixer_vtol_manualswitch_autotransition_controller = ON`, `MIXER TRANSITION` - Leaving the switch ON does not keep restarting the transition. - To start another transition, turn the switch OFF and then ON again. - If you turn the switch OFF before the profile change happens, that transition request is cancelled. -- Optional extra protection: `vtol_fw_to_mc_auto_switch_airspeed_cm_s` can automatically start FW->MC if airspeed gets too low. +- Optional extra protection: `vtol_fw_to_mc_auto_switch_airspeed_cm_s` can automatically start FW->MC if airspeed gets too low. After that switch, iNAV stays in MC until you deliberately command another manual profile change. This behavior is controlled by `mixer_vtol_manualswitch_autotransition_controller`. Turn it ON in both mixer profiles if you want the same switch behavior in both directions. @@ -53,6 +53,7 @@ There are two separate manual paths: - `MIXER PROFILE 2` is still a direct manual profile switch when `MIXER TRANSITION` is OFF. - `MIXER TRANSITION` starts the smooth automatic transition sequence when `mixer_vtol_manualswitch_autotransition_controller = ON`. - If both are ON together while the automatic transition controller is enabled, the controller temporarily owns the profile switching. When `MIXER TRANSITION` turns OFF again, direct `MIXER PROFILE 2` switching becomes active again. +- If low-speed protection switches the model from FW to MC, iNAV keeps the MC profile until you deliberately command another manual profile change. 3-position switch example: diff --git a/docs/Settings.md b/docs/Settings.md index bf5aed2876f..76ef09a71e5 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -7078,7 +7078,7 @@ Warning voltage per cell, this triggers battery-warning alarms, in 0.01V units, ### vtol_fw_to_mc_auto_switch_airspeed_cm_s -Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV can start FW->MC transition automatically. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable. +Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV automatically starts FW->MC. After the switch to MC, iNAV keeps the MC profile until you deliberately command another manual profile change. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable. | Default | Min | Max | | --- | --- | --- | diff --git a/docs/VTOL.md b/docs/VTOL.md index 6d692489192..396deb0c5e3 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -325,7 +325,7 @@ With `mixer_vtol_manualswitch_autotransition_controller = ON`: - Leaving the switch ON does not keep restarting the transition. - To start another transition, turn the switch OFF and then ON again. - If you turn the switch OFF before the profile change happens, that transition request is cancelled. -- Optional extra protection: set `vtol_fw_to_mc_auto_switch_airspeed_cm_s > 0` if you want FW->MC to start automatically when pitot airspeed becomes too low. +- Optional extra protection: set `vtol_fw_to_mc_auto_switch_airspeed_cm_s > 0` if you want FW->MC to start automatically when pitot airspeed becomes too low. After that switch, iNAV stays in MC until you deliberately command another manual profile change. With `mixer_vtol_manualswitch_autotransition_controller = OFF`: - the older manual behavior is preserved. @@ -575,7 +575,7 @@ Use these commands in CLI (`set ...`, then `save`): - FW -> MC completion threshold (pitot airspeed). - `set vtol_fw_to_mc_auto_switch_airspeed_cm_s = ` - - Optional low-speed protection threshold for fixed-wing (`0` disables). + - Optional low-speed protection threshold for fixed-wing. After it switches to MC, iNAV stays in MC until you deliberately command another manual profile change (`0` disables). - `set mixer_vtol_transition_airspeed_timeout_ms = ` - How long iNAV waits for required pitot airspeed before aborting. diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 2ed28bb6509..e9045323abd 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -4027,7 +4027,7 @@ groups: min: 0 max: 20000 - name: vtol_fw_to_mc_auto_switch_airspeed_cm_s - description: "Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV can start FW->MC transition automatically. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable." + description: "Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV automatically starts FW->MC. After the switch to MC, iNAV keeps the MC profile until you deliberately command another manual profile change. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable." default_value: 0 field: vtolFwToMcAutoSwitchAirspeed min: 0 diff --git a/src/main/flight/mixer.h b/src/main/flight/mixer.h index 12688bd2c09..584c05b0888 100644 --- a/src/main/flight/mixer.h +++ b/src/main/flight/mixer.h @@ -43,6 +43,13 @@ typedef enum { PLATFORM_BOAT = 5 } flyingPlatformType_e; +static inline bool isMultirotorTypePlatform(const flyingPlatformType_e platformType) +{ + return platformType == PLATFORM_MULTIROTOR || + platformType == PLATFORM_TRICOPTER || + platformType == PLATFORM_HELICOPTER; +} + typedef enum { OUTPUT_MODE_AUTO = 0, diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index f24363ca997..bdd322e3301 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -43,6 +43,7 @@ int nextMixerProfileIndex; static bool manualTransitionModeWasActive; static bool manualTransitionReadyForEdge = true; static bool manualTransitionSessionLatched; +static bool manualFwToMcProtectionLatched; PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 4); @@ -406,9 +407,7 @@ static bool missionTransitionToMultirotorTypeConfigured(void) } const flyingPlatformType_e nextPlatformType = mixerConfigByIndex(nextMixerProfileIndex)->platformType; - return nextPlatformType == PLATFORM_MULTIROTOR || - nextPlatformType == PLATFORM_TRICOPTER || - nextPlatformType == PLATFORM_HELICOPTER; + return isMultirotorTypePlatform(nextPlatformType); } bool checkMixerATRequired(mixerProfileATRequest_e required_action) @@ -543,24 +542,40 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) const bool transitionControllerOwnsProfileSwitch = manualControllerEnabled && transitionModeActive; const bool mixerProfileModePresent = isModeActivationConditionPresent(BOXMIXERPROFILE); const int requestedProfileIndex = IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) == 0 ? 0 : 1; + const bool requestedMultirotorProfile = mixerProfileModePresent && + isMultirotorTypePlatform(mixerConfigByIndex(requestedProfileIndex)->platformType); + // If low-speed protection already moved the model back to MC, keep direct + // switching from forcing FW again until the pilot makes a new manual choice. + const bool fwToMcProtectionOwnsProfileSwitch = manualFwToMcProtectionLatched && + STATE(MULTIROTOR) && + !requestedMultirotorProfile; if (manualControllerConfigured && transitionModeRisingEdge) { manualTransitionSessionLatched = true; } + if (transitionModeRisingEdge) { + manualFwToMcProtectionLatched = false; + } + if (transitionModeFallingEdge) { manualTransitionSessionLatched = false; } + if (requestedMultirotorProfile || (!mixerAT_inuse && !STATE(MULTIROTOR))) { + manualFwToMcProtectionLatched = false; + } + if (mixerAT_inuse && (!ARMING_FLAG(ARMED) || FLIGHT_MODE(FAILSAFE_MODE) || areSensorsCalibrating())) { abortTransition(false); manualTransitionSessionLatched = false; + manualFwToMcProtectionLatched = false; mixerAT_inuse = false; } if (!FLIGHT_MODE(FAILSAFE_MODE) && !mixerAT_inuse) { - if (mixerProfileModePresent && !transitionControllerOwnsProfileSwitch) { + if (mixerProfileModePresent && !transitionControllerOwnsProfileSwitch && !fwToMcProtectionOwnsProfileSwitch) { outputProfileHotSwitch(requestedProfileIndex); } } @@ -573,6 +588,9 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) checkMixerATRequired(MIXERAT_REQUEST_MANUAL_TO_MC)) { mixerATUpdateState(MIXERAT_REQUEST_MANUAL_TO_MC); mixerAT_inuse = mixerATIsActive(); + if (mixerAT_inuse || STATE(MULTIROTOR)) { + manualFwToMcProtectionLatched = true; + } } if (!manualControllerEnabled) { diff --git a/src/main/navigation/navigation.c b/src/main/navigation/navigation.c index ef0af043612..7d7120dd85c 100644 --- a/src/main/navigation/navigation.c +++ b/src/main/navigation/navigation.c @@ -2070,9 +2070,7 @@ static uint16_t missionUserActionMask(const navMissionUserAction_e userAction) static bool isMissionTransitionToMultirotorType(const flyingPlatformType_e platformType) { - return platformType == PLATFORM_MULTIROTOR || - platformType == PLATFORM_TRICOPTER || - platformType == PLATFORM_HELICOPTER; + return isMultirotorTypePlatform(platformType); } #ifdef USE_PITOT From a32326a9faf1d871da2e7e7bbb66ba441ba079bf Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Sun, 7 Jun 2026 12:32:28 +0300 Subject: [PATCH 25/26] feat(vtol): add INPUT_AUTOTRANSITION_TARGET_STABILIZED* input source calculated by fwAuthorityScale define USE_AUTO_TRANSITION and end keep auto transition logic off for 512 KB targets to preserve flash space --- docs/MixerProfile.md | 12 +- docs/Navigation.md | 2 + docs/Settings.md | 30 +- docs/VTOL.md | 11 + src/main/blackbox/blackbox.c | 7 + src/main/fc/config.c | 2 + src/main/fc/config.h | 2 + src/main/fc/fc_msp_box.c | 5 + src/main/fc/settings.yaml | 45 ++- src/main/flight/mixer.c | 17 +- src/main/flight/mixer_profile.c | 117 ++++++- src/main/flight/mixer_profile.h | 16 + src/main/flight/pid.c | 425 ++++++++++++++++++++++- src/main/flight/pid.h | 8 +- src/main/flight/servos.c | 112 ++++++ src/main/flight/servos.h | 11 + src/main/navigation/navigation.c | 76 ++++ src/main/navigation/navigation.h | 4 + src/main/navigation/navigation_private.h | 2 + src/main/programming/logic_condition.c | 21 ++ src/main/programming/logic_condition.h | 5 + src/main/target/common.h | 4 +- 22 files changed, 894 insertions(+), 40 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index a55f8d10962..25b05fda5b2 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -6,7 +6,9 @@ A MixerProfile is a set of motor mixer, servo-mixer and platform type configurat Not limited to VTOL. air/land/sea mixed vehicle is also achievable with this feature. Model behaves according to current mixer_profile's platform_type and configured custom motor/servo mixer -Currently two profiles are supported on targets other than F411(due to resource constraints on F411). i.e VTOL transition is not available on F411. +Two mixer profiles and smooth VTOL auto-transition are available only on targets with enough flash space. +In standard INAV builds this means targets with more than 512 KB flash, compiled with `USE_AUTO_TRANSITION`. +Targets with 512 KB flash keep the older single-profile / legacy transition behavior and do not include the smooth auto-transition settings. For VTOL setup. one mixer_profile is used for multi-rotor(MR) and the other is used for fixed-wing(FW) By default, switching between profiles requires reboot to take affect. However, using the RC mode: `MIXER PROFILE 2` will allow in flight switching for things like VTOL operation @@ -139,6 +141,8 @@ If `mixer_automated_switch = OFF` in all mixer profiles, automated VTOL transiti Manual `MIXER TRANSITION` and mission-requested VTOL transition both use the same internal transition controller. That means the same airspeed checks, timer fallback, and smooth power changes are reused in both cases. +This section applies only to targets with more than 512 KB flash, compiled with `USE_AUTO_TRANSITION`. + Direct manual `MIXER PROFILE 2` switching is still a separate path when you want an immediate profile change. ### Airspeed-first completion @@ -168,7 +172,11 @@ When `mixer_vtol_transition_dynamic_mixer = ON`, iNAV can smoothly change: - fixed-wing control strength. Default is OFF to preserve existing behavior. -With it ON, `vtol_transition_fw_authority_start_percent = 100` keeps the old fixed-wing control behavior. Lower values bring fixed-wing control in more gently. +With it ON, you can configure `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` servo rules in the MC mixer profile. +During MC->FW they drive the selected servo outputs from the target FW controller before the hot-switch. +During FW->MC the same MC mixer rules mark which FW servo outputs should fade down as fixed-wing authority is reduced and motor stabilisation comes back in. +These inputs are active only while the smooth autotransition controller is running. If `mixer_vtol_transition_dynamic_mixer = OFF`, they stay at full authority while the controller is active. If `mixer_vtol_transition_dynamic_mixer = ON`, they follow the normal fixed-wing authority scaling. +`INPUT_MIXER_TRANSITION` remains available for transition-progress servo movement such as tilt or helper servos. How `mixer_vtol_transition_scale_ramp_time_ms` works: diff --git a/docs/Navigation.md b/docs/Navigation.md index e3598cf5fc0..f73d894c981 100755 --- a/docs/Navigation.md +++ b/docs/Navigation.md @@ -105,6 +105,8 @@ Parameters: ### Mission VTOL transition using existing User Actions Mission VTOL transition can be requested. +This is available only on targets with more than 512 KB flash, compiled with `USE_AUTO_TRANSITION`. +Targets with 512 KB flash do not include these mission VTOL transition settings. Configuration: diff --git a/docs/Settings.md b/docs/Settings.md index 76ef09a71e5..90ab68a7527 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -3212,7 +3212,7 @@ Original VTOL transition timer, still used as the backup completion time. If tru ### mixer_vtol_manualswitch_autotransition_controller -Makes `MIXER TRANSITION` start one automatic VTOL transition each time the switch moves from OFF to ON, when not in waypoint mission. Turn this ON in both mixer profiles if you want the same behavior in both directions. OFF keeps the older manual switch behavior. +Makes `MIXER TRANSITION` start one automatic VTOL transition each time the switch moves from OFF to ON, when not in waypoint mission. Turn this ON in both mixer profiles if you want the same behavior in both directions. OFF keeps the older manual switch behavior. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -3222,7 +3222,7 @@ Makes `MIXER TRANSITION` start one automatic VTOL transition each time the switc ### mixer_vtol_transition_airspeed_timeout_ms -Maximum wait time [ms] for the required pitot airspeed during an airspeed-controlled transition. This timer does not complete the transition; it only aborts it if the target airspeed is still not reached in time. If pitot becomes unavailable, iNAV falls back to `mixer_switch_trans_timer` instead. Set to 0 to disable. +Maximum wait time [ms] for the required pitot airspeed during an airspeed-controlled transition. This timer does not complete the transition; it only aborts it if the target airspeed is still not reached in time. If pitot becomes unavailable, iNAV falls back to `mixer_switch_trans_timer` instead. Set to 0 to disable. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -3232,7 +3232,7 @@ Maximum wait time [ms] for the required pitot airspeed during an airspeed-contro ### mixer_vtol_transition_dynamic_mixer -Turns on smooth VTOL transition power changes. This affects forward motor ramp-up, lift motor power reduction, multicopter stabilisation reduction, and fixed-wing control fade-in. Used by both manual `MIXER TRANSITION` and mission-requested VTOL transitions. +Turns on smooth VTOL transition power changes. This affects forward motor ramp-up, lift motor power reduction, multicopter stabilisation reduction, and fixed-wing control fade-in. Used by both manual `MIXER TRANSITION` and mission-requested VTOL transitions. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -3242,7 +3242,7 @@ Turns on smooth VTOL transition power changes. This affects forward motor ramp-u ### mixer_vtol_transition_scale_ramp_time_ms -When smooth VTOL transition power changes are ON, this always controls the MC->FW forward motor ramp. `0` gives full forward-motor power immediately. This timer does not decide when the transition is complete. For lift motor power, multicopter stabilisation, and fixed-wing control handoff, trusted pitot airspeed still controls the change while pitot is usable; this timer is only their backup ramp if pitot becomes unavailable. +When smooth VTOL transition power changes are ON, this always controls the MC->FW forward motor ramp. `0` gives full forward-motor power immediately. This timer does not decide when the transition is complete. For lift motor power, multicopter stabilisation, and fixed-wing control handoff, trusted pitot airspeed still controls the change while pitot is usable; this timer is only their backup ramp if pitot becomes unavailable. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -4669,7 +4669,7 @@ Defines how Pitch/Roll input from RC receiver affects flight in POSHOLD mode: AT ### nav_vtol_mission_transition_min_altitude_cm -Do not start a mission-requested VTOL transition below this altitude [cm]. Set to 0 to disable the altitude check. +Do not start a mission-requested VTOL transition below this altitude [cm]. Set to 0 to disable the altitude check. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -4679,7 +4679,7 @@ Do not start a mission-requested VTOL transition below this altitude [cm]. Set t ### nav_vtol_mission_transition_user_action -Chooses which waypoint USER flag (`USER1`..`USER4`) tells iNAV which flight mode to use at each navigable waypoint. Selected USER flag ON means fixed-wing. Selected USER flag OFF means multicopter. OFF disables this feature. Requires two mixer profiles and a working `MIXER PROFILE 2` mode setup. +Chooses which waypoint USER flag (`USER1`..`USER4`) tells iNAV which flight mode to use at each navigable waypoint. Selected USER flag ON means fixed-wing. Selected USER flag OFF means multicopter. OFF disables this feature. Requires two mixer profiles, a working `MIXER PROFILE 2` mode setup, and a target with more than 512 KB flash. | Allowed Values | | | --- | --- | @@ -4693,7 +4693,7 @@ Chooses which waypoint USER flag (`USER1`..`USER4`) tells iNAV which flight mode ### nav_vtol_transition_fail_action_fw_to_mc -What iNAV should do if FW->MC transition fails. `LOITER` keeps the aircraft near its current position. `FORCE_SWITCH` changes to the other mixer profile immediately even though the normal switch conditions were not met. +What iNAV should do if FW->MC transition fails. `LOITER` keeps the aircraft near its current position. `FORCE_SWITCH` changes to the other mixer profile immediately even though the normal switch conditions were not met. Available only on targets with more than 512 KB flash. | Allowed Values | | | --- | --- | @@ -4707,7 +4707,7 @@ What iNAV should do if FW->MC transition fails. `LOITER` keeps the aircraft near ### nav_vtol_transition_fail_action_mc_to_fw -What iNAV should do if MC->FW transition still fails after the final attempt. +What iNAV should do if MC->FW transition still fails after the final attempt. Available only on targets with more than 512 KB flash. | Allowed Values | | | --- | --- | @@ -4720,7 +4720,7 @@ What iNAV should do if MC->FW transition still fails after the final attempt. ### nav_vtol_transition_retry_on_airspeed_timeout -If ON, iNAV gets one extra MC->FW attempt after an airspeed timeout during mission or RTH. It pauses, yaws around to find the best airspeed direction, then tries once more. +If ON, iNAV gets one extra MC->FW attempt after an airspeed timeout during mission or RTH. It pauses, yaws around to find the best airspeed direction, then tries once more. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -7078,7 +7078,7 @@ Warning voltage per cell, this triggers battery-warning alarms, in 0.01V units, ### vtol_fw_to_mc_auto_switch_airspeed_cm_s -Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV automatically starts FW->MC. After the switch to MC, iNAV keeps the MC profile until you deliberately command another manual profile change. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable. +Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV automatically starts FW->MC. After the switch to MC, iNAV keeps the MC profile until you deliberately command another manual profile change. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -7088,7 +7088,7 @@ Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to th ### vtol_transition_fw_authority_start_percent -How much fixed-wing control is available at the start of transition, in percent. `100` gives full fixed-wing control immediately. Lower values bring in fixed-wing control more gently. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. +How much fixed-wing control is available at the start of transition, in percent. `100` gives full fixed-wing control immediately. Lower values bring it in and out more gently. With `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` rules configured in the MC mixer profile, this same setting scales their servo authority during MC->FW and scales down the matching FW servo stabilisation during FW->MC. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -7098,7 +7098,7 @@ How much fixed-wing control is available at the start of transition, in percent. ### vtol_transition_lift_end_percent -How much lift motor power remains at the end of transition, in percent. `100` keeps full lift power. Lower values reduce lift motor power more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. +How much lift motor power remains at the end of transition, in percent. `100` keeps full lift power. Lower values reduce lift motor power more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -7108,7 +7108,7 @@ How much lift motor power remains at the end of transition, in percent. `100` ke ### vtol_transition_mc_authority_end_percent -How much multicopter stabilisation remains at the end of transition, in percent. `100` keeps full multicopter stabilisation. Lower values reduce it more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. +How much multicopter stabilisation remains at the end of transition, in percent. `100` keeps full multicopter stabilisation. Lower values reduce it more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -7118,7 +7118,7 @@ How much multicopter stabilisation remains at the end of transition, in percent. ### vtol_transition_to_fw_min_airspeed_cm_s -Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this target is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted. +Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this target is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -7128,7 +7128,7 @@ Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered comp ### vtol_transition_to_mc_max_airspeed_cm_s -When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this condition is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted. +When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this condition is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | diff --git a/docs/VTOL.md b/docs/VTOL.md index 396deb0c5e3..9895f1a03d5 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -297,6 +297,10 @@ If you set `mixer_automated_switch` to `OFF` for all mixer profiles (the default ## Unified VTOL Transition Controller (Manual + Mission) +This feature is available only on targets with more than 512 KB flash. +In standard INAV builds those targets are compiled with `USE_AUTO_TRANSITION`. +Targets with 512 KB flash keep the older VTOL mixer transition behavior and do not include the smooth auto-transition settings. + INAV now uses one internal VTOL transition controller for both: - manual `MIXER TRANSITION` requests, and - mission-authorized VTOL transitions. @@ -388,6 +392,11 @@ When `mixer_vtol_transition_dynamic_mixer = ON`, iNAV can smoothly change: - fixed-wing control strength. When `mixer_vtol_transition_dynamic_mixer = OFF`, the older static behavior is preserved. +When it is ON, you can configure `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` servo rules in the MC mixer profile. +During MC->FW they drive the selected servo outputs from the target FW controller before the hot-switch. +During FW->MC the same MC mixer rules mark which FW servo outputs should fade down as fixed-wing authority is reduced and motor stabilisation comes back in. +These inputs are active only while the smooth autotransition controller is running. If `mixer_vtol_transition_dynamic_mixer = OFF`, they stay at full authority while the controller is active. If `mixer_vtol_transition_dynamic_mixer = ON`, they follow the normal fixed-wing authority scaling. +`INPUT_MIXER_TRANSITION` remains available for transition-progress servo movement such as tilt or helper servos. `mixer_vtol_transition_scale_ramp_time_ms` always controls the MC->FW forward-motor ramp when this feature is ON. It does not decide when the transition completes. @@ -517,6 +526,8 @@ Example (`vtol_transition_mc_authority_end_percent = 30`): - Sets how much fixed-wing control is already available at the start of transition. - MC -> FW: fixed-wing control goes from `fw_authority_start_percent` at start to `100%` at the end. - FW -> MC: fixed-wing control goes from `100%` at start to `fw_authority_start_percent` at the end. +- During MC -> FW, this same setting also scales `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` servo rules configured in the MC mixer profile. +- During FW -> MC, the same setting scales down the matching FW servo stabilisation on the outputs marked by those MC mixer rules. Example (`vtol_transition_fw_authority_start_percent = 25`): - MC -> FW at 50% progress: fixed-wing control is about `62.5%`. diff --git a/src/main/blackbox/blackbox.c b/src/main/blackbox/blackbox.c index 7ad94f88263..4414fed501d 100644 --- a/src/main/blackbox/blackbox.c +++ b/src/main/blackbox/blackbox.c @@ -1367,10 +1367,13 @@ static void writeSlowFrame(void) */ static void loadSlowState(blackboxSlowState_t *slow) { +#ifdef USE_AUTO_TRANSITION boxBitmask_t reportedRcModeFlags = rcModeActivationMask; +#endif slow->activeWpNumber = getActiveWpNumber(); +#ifdef USE_AUTO_TRANSITION // Keep these two mode bits aligned with actual VTOL state/profile activity for status reporting. if (isMixerProfile2ModeReportedActive()) { bitArraySet(reportedRcModeFlags.bits, BOXMIXERPROFILE); @@ -1386,6 +1389,10 @@ static void loadSlowState(blackboxSlowState_t *slow) slow->rcModeFlags = reportedRcModeFlags.bits[0]; // first 32 bits of boxId_e slow->rcModeFlags2 = reportedRcModeFlags.bits[1]; // remaining bits of boxId_e +#else + slow->rcModeFlags = rcModeActivationMask.bits[0]; // first 32 bits of boxId_e + slow->rcModeFlags2 = rcModeActivationMask.bits[1]; // remaining bits of boxId_e +#endif // Also log Nav auto enabled flight modes rather than just those selected by boxmode if (navigationGetHeadingControlState() == NAV_HEADING_CONTROL_AUTO) { diff --git a/src/main/fc/config.c b/src/main/fc/config.c index f5cafa62ae7..f9254e41aa1 100755 --- a/src/main/fc/config.c +++ b/src/main/fc/config.c @@ -117,12 +117,14 @@ PG_RESET_TEMPLATE(systemConfig_t, systemConfig, .i2c_speed = SETTING_I2C_SPEED_DEFAULT, #endif .throttle_tilt_compensation_strength = SETTING_THROTTLE_TILT_COMP_STR_DEFAULT, // 0-100, 0 - disabled +#ifdef USE_AUTO_TRANSITION .vtolTransitionToFwMinAirspeed = SETTING_VTOL_TRANSITION_TO_FW_MIN_AIRSPEED_CM_S_DEFAULT, .vtolTransitionToMcMaxAirspeed = SETTING_VTOL_TRANSITION_TO_MC_MAX_AIRSPEED_CM_S_DEFAULT, .vtolFwToMcAutoSwitchAirspeed = SETTING_VTOL_FW_TO_MC_AUTO_SWITCH_AIRSPEED_CM_S_DEFAULT, .vtolTransitionLiftEndPercent = SETTING_VTOL_TRANSITION_LIFT_END_PERCENT_DEFAULT, .vtolTransitionMcAuthorityEndPercent = SETTING_VTOL_TRANSITION_MC_AUTHORITY_END_PERCENT_DEFAULT, .vtolTransitionFwAuthorityStartPercent = SETTING_VTOL_TRANSITION_FW_AUTHORITY_START_PERCENT_DEFAULT, +#endif .craftName = SETTING_NAME_DEFAULT, .pilotName = SETTING_NAME_DEFAULT ); diff --git a/src/main/fc/config.h b/src/main/fc/config.h index 1478754f350..fd433e9e657 100644 --- a/src/main/fc/config.h +++ b/src/main/fc/config.h @@ -78,12 +78,14 @@ typedef struct systemConfig_s { uint8_t i2c_speed; #endif uint8_t throttle_tilt_compensation_strength; // the correction that will be applied at throttle_correction_angle. +#ifdef USE_AUTO_TRANSITION uint16_t vtolTransitionToFwMinAirspeed; uint16_t vtolTransitionToMcMaxAirspeed; uint16_t vtolFwToMcAutoSwitchAirspeed; uint8_t vtolTransitionLiftEndPercent; uint8_t vtolTransitionMcAuthorityEndPercent; uint8_t vtolTransitionFwAuthorityStartPercent; +#endif char craftName[MAX_NAME_LENGTH + 1]; char pilotName[MAX_NAME_LENGTH + 1]; } systemConfig_t; diff --git a/src/main/fc/fc_msp_box.c b/src/main/fc/fc_msp_box.c index ba6f3f69593..bfcfcbf6099 100644 --- a/src/main/fc/fc_msp_box.c +++ b/src/main/fc/fc_msp_box.c @@ -447,8 +447,13 @@ void packBoxModeFlags(boxBitmask_t * mspBoxModeFlags) CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXMULTIFUNCTION)), BOXMULTIFUNCTION); #endif #if (MAX_MIXER_PROFILE_COUNT > 1) +#ifdef USE_AUTO_TRANSITION CHECK_ACTIVE_BOX(IS_ENABLED(isMixerProfile2ModeReportedActive()), BOXMIXERPROFILE); CHECK_ACTIVE_BOX(IS_ENABLED(isMixerTransitionModeReportedActive()), BOXMIXERTRANSITION); +#else + CHECK_ACTIVE_BOX(IS_ENABLED(currentMixerProfileIndex), BOXMIXERPROFILE); + CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION)), BOXMIXERTRANSITION); +#endif #endif CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXANGLEHOLD)), BOXANGLEHOLD); diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index e9045323abd..a65788dd1f0 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1287,23 +1287,27 @@ groups: min: 0 max: 200 - name: mixer_vtol_transition_dynamic_mixer - description: "Turns on smooth VTOL transition power changes. This affects forward motor ramp-up, lift motor power reduction, multicopter stabilisation reduction, and fixed-wing control fade-in. Used by both manual `MIXER TRANSITION` and mission-requested VTOL transitions." + description: "Turns on smooth VTOL transition power changes. This affects forward motor ramp-up, lift motor power reduction, multicopter stabilisation reduction, and fixed-wing control fade-in. Used by both manual `MIXER TRANSITION` and mission-requested VTOL transitions. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: OFF field: mixer_config.vtolTransitionDynamicMixer type: bool - name: mixer_vtol_manualswitch_autotransition_controller - description: "Makes `MIXER TRANSITION` start one automatic VTOL transition each time the switch moves from OFF to ON, when not in waypoint mission. Turn this ON in both mixer profiles if you want the same behavior in both directions. OFF keeps the older manual switch behavior." + description: "Makes `MIXER TRANSITION` start one automatic VTOL transition each time the switch moves from OFF to ON, when not in waypoint mission. Turn this ON in both mixer profiles if you want the same behavior in both directions. OFF keeps the older manual switch behavior. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: OFF field: mixer_config.manualVtolTransitionController type: bool - name: mixer_vtol_transition_airspeed_timeout_ms - description: "Maximum wait time [ms] for the required pitot airspeed during an airspeed-controlled transition. This timer does not complete the transition; it only aborts it if the target airspeed is still not reached in time. If pitot becomes unavailable, iNAV falls back to `mixer_switch_trans_timer` instead. Set to 0 to disable." + description: "Maximum wait time [ms] for the required pitot airspeed during an airspeed-controlled transition. This timer does not complete the transition; it only aborts it if the target airspeed is still not reached in time. If pitot becomes unavailable, iNAV falls back to `mixer_switch_trans_timer` instead. Set to 0 to disable. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: 0 field: mixer_config.vtolTransitionAirspeedTimeoutMs min: 0 max: 60000 - name: mixer_vtol_transition_scale_ramp_time_ms - description: "When smooth VTOL transition power changes are ON, this always controls the MC->FW forward motor ramp. `0` gives full forward-motor power immediately. This timer does not decide when the transition is complete. For lift motor power, multicopter stabilisation, and fixed-wing control handoff, trusted pitot airspeed still controls the change while pitot is usable; this timer is only their backup ramp if pitot becomes unavailable." + description: "When smooth VTOL transition power changes are ON, this always controls the MC->FW forward motor ramp. `0` gives full forward-motor power immediately. This timer does not decide when the transition is complete. For lift motor power, multicopter stabilisation, and fixed-wing control handoff, trusted pitot airspeed still controls the change while pitot is usable; this timer is only their backup ramp if pitot becomes unavailable. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: 0 field: mixer_config.vtolTransitionScaleRampTimeMs min: 0 @@ -2655,28 +2659,33 @@ groups: field: general.flags.waypoint_mission_restart table: nav_wp_mission_restart - name: nav_vtol_mission_transition_user_action - description: "Chooses which waypoint USER flag (`USER1`..`USER4`) tells iNAV which flight mode to use at each navigable waypoint. Selected USER flag ON means fixed-wing. Selected USER flag OFF means multicopter. OFF disables this feature. Requires two mixer profiles and a working `MIXER PROFILE 2` mode setup." + description: "Chooses which waypoint USER flag (`USER1`..`USER4`) tells iNAV which flight mode to use at each navigable waypoint. Selected USER flag ON means fixed-wing. Selected USER flag OFF means multicopter. OFF disables this feature. Requires two mixer profiles, a working `MIXER PROFILE 2` mode setup, and a target with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: "OFF" field: general.vtol_mission_transition_user_action table: nav_wp_user_action - name: nav_vtol_mission_transition_min_altitude_cm - description: "Do not start a mission-requested VTOL transition below this altitude [cm]. Set to 0 to disable the altitude check." + description: "Do not start a mission-requested VTOL transition below this altitude [cm]. Set to 0 to disable the altitude check. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: 0 field: general.vtol_mission_transition_min_altitude min: 0 max: 50000 - name: nav_vtol_transition_retry_on_airspeed_timeout - description: "If ON, iNAV gets one extra MC->FW attempt after an airspeed timeout during mission or RTH. It pauses, yaws around to find the best airspeed direction, then tries once more." + description: "If ON, iNAV gets one extra MC->FW attempt after an airspeed timeout during mission or RTH. It pauses, yaws around to find the best airspeed direction, then tries once more. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: OFF field: general.vtol_transition_retry_on_airspeed_timeout type: bool - name: nav_vtol_transition_fail_action_mc_to_fw - description: "What iNAV should do if MC->FW transition still fails after the final attempt." + description: "What iNAV should do if MC->FW transition still fails after the final attempt. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: "IDLE" field: general.vtol_transition_fail_action_mc_to_fw table: nav_vtol_transition_fail_action_mc_to_fw - name: nav_vtol_transition_fail_action_fw_to_mc - description: "What iNAV should do if FW->MC transition fails. `LOITER` keeps the aircraft near its current position. `FORCE_SWITCH` changes to the other mixer profile immediately even though the normal switch conditions were not met." + description: "What iNAV should do if FW->MC transition fails. `LOITER` keeps the aircraft near its current position. `FORCE_SWITCH` changes to the other mixer profile immediately even though the normal switch conditions were not met. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: "LOITER" field: general.vtol_transition_fail_action_fw_to_mc table: nav_vtol_transition_fail_action_fw_to_mc @@ -4015,37 +4024,43 @@ groups: min: 0 max: 100 - name: vtol_transition_to_fw_min_airspeed_cm_s - description: "Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this target is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted." + description: "Minimum pitot airspeed [cm/s] needed before MC->FW transition is considered complete while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this target is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: 0 field: vtolTransitionToFwMinAirspeed min: 0 max: 20000 - name: vtol_transition_to_mc_max_airspeed_cm_s - description: "When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this condition is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted." + description: "When slowing down from FW to MC, the transition is considered complete once pitot airspeed falls to this value [cm/s] or lower while pitot remains usable. If pitot becomes unavailable, or if this is set to 0, iNAV uses `mixer_switch_trans_timer` instead. If pitot remains usable but this condition is still not reached before `mixer_vtol_transition_airspeed_timeout_ms` expires, the transition is aborted. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: 0 field: vtolTransitionToMcMaxAirspeed min: 0 max: 20000 - name: vtol_fw_to_mc_auto_switch_airspeed_cm_s - description: "Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV automatically starts FW->MC. After the switch to MC, iNAV keeps the MC profile until you deliberately command another manual profile change. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable." + description: "Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to this value or lower while in FW, iNAV automatically starts FW->MC. After the switch to MC, iNAV keeps the MC profile until you deliberately command another manual profile change. Used only when `mixer_vtol_manualswitch_autotransition_controller` is ON. Set to 0 to disable. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: 0 field: vtolFwToMcAutoSwitchAirspeed min: 0 max: 20000 - name: vtol_transition_lift_end_percent - description: "How much lift motor power remains at the end of transition, in percent. `100` keeps full lift power. Lower values reduce lift motor power more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." + description: "How much lift motor power remains at the end of transition, in percent. `100` keeps full lift power. Lower values reduce lift motor power more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: 100 field: vtolTransitionLiftEndPercent min: 0 max: 100 - name: vtol_transition_mc_authority_end_percent - description: "How much multicopter stabilisation remains at the end of transition, in percent. `100` keeps full multicopter stabilisation. Lower values reduce it more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." + description: "How much multicopter stabilisation remains at the end of transition, in percent. `100` keeps full multicopter stabilisation. Lower values reduce it more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: 100 field: vtolTransitionMcAuthorityEndPercent min: 0 max: 100 - name: vtol_transition_fw_authority_start_percent - description: "How much fixed-wing control is available at the start of transition, in percent. `100` gives full fixed-wing control immediately. Lower values bring in fixed-wing control more gently. Used only when `mixer_vtol_transition_dynamic_mixer` is ON." + description: "How much fixed-wing control is available at the start of transition, in percent. `100` gives full fixed-wing control immediately. Lower values bring it in and out more gently. With `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` rules configured in the MC mixer profile, this same setting scales their servo authority during MC->FW and scales down the matching FW servo stabilisation during FW->MC. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash." + condition: USE_AUTO_TRANSITION default_value: 100 field: vtolTransitionFwAuthorityStartPercent min: 0 diff --git a/src/main/flight/mixer.c b/src/main/flight/mixer.c index 77038141281..10a96272c42 100644 --- a/src/main/flight/mixer.c +++ b/src/main/flight/mixer.c @@ -522,10 +522,16 @@ void FAST_CODE mixTable(void) input[PITCH] = axisPID[PITCH]; input[YAW] = axisPID[YAW]; if (isMixerTransitionMixing) { +#ifdef USE_AUTO_TRANSITION const float mcAuthorityScale = mixerATGetMcAuthorityScale(); input[ROLL] = input[ROLL] * (currentMixerConfig.transition_PID_mmix_multiplier_roll / 1000.0f) * mcAuthorityScale; input[PITCH] = input[PITCH] * (currentMixerConfig.transition_PID_mmix_multiplier_pitch / 1000.0f) * mcAuthorityScale; input[YAW] = input[YAW] * (currentMixerConfig.transition_PID_mmix_multiplier_yaw / 1000.0f) * mcAuthorityScale; +#else + input[ROLL] = input[ROLL] * (currentMixerConfig.transition_PID_mmix_multiplier_roll / 1000.0f); + input[PITCH] = input[PITCH] * (currentMixerConfig.transition_PID_mmix_multiplier_pitch / 1000.0f); + input[YAW] = input[YAW] * (currentMixerConfig.transition_PID_mmix_multiplier_yaw / 1000.0f); +#endif } } @@ -626,14 +632,14 @@ void FAST_CODE mixTable(void) // Now add in the desired throttle, but keep in a range that doesn't clip adjusted // roll/pitch/yaw. This could move throttle down, but also up for those low throttle flips. - const float liftScale = isMixerTransitionMixing ? mixerATGetLiftScale() : 1.0f; - const float pusherScale = isMixerTransitionMixing ? mixerATGetPusherScale() : 1.0f; - for (int i = 0; i < motorCount; i++) { float motorThrottle = mixerThrottleCommand * currentMixer[i].throttle; +#ifdef USE_AUTO_TRANSITION + const float liftScale = isMixerTransitionMixing ? mixerATGetLiftScale() : 1.0f; if (currentMixer[i].throttle > 0.0f) { motorThrottle *= liftScale; } +#endif motor[i] = rpyMix[i] + constrain(motorThrottle, throttleMin, throttleMax); @@ -649,9 +655,14 @@ void FAST_CODE mixTable(void) } //spin stopped motors only in mixer transition mode if (isMixerTransitionMixing && currentMixer[i].throttle <= -1.05f && currentMixer[i].throttle >= -2.0f && !feature(FEATURE_REVERSIBLE_MOTORS)) { +#ifdef USE_AUTO_TRANSITION + const float pusherScale = mixerATGetPusherScale(); const float pusherTarget = -currentMixer[i].throttle * 1000.0f; const float pusherIdle = throttleRangeMin; motor[i] = pusherIdle + (pusherTarget - pusherIdle) * pusherScale; +#else + motor[i] = -currentMixer[i].throttle * 1000; +#endif motor[i] = constrain(motor[i], throttleRangeMin, throttleRangeMax); } } diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index bdd322e3301..67f2e025b56 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -40,10 +40,12 @@ bool isMixerTransitionMixing; bool isMixerTransitionMixing_requested; mixerProfileAT_t mixerProfileAT; int nextMixerProfileIndex; +#ifdef USE_AUTO_TRANSITION static bool manualTransitionModeWasActive; static bool manualTransitionReadyForEdge = true; static bool manualTransitionSessionLatched; static bool manualFwToMcProtectionLatched; +#endif PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 4); @@ -61,10 +63,12 @@ void pgResetFn_mixerProfiles(mixerProfile_t *instance) .controlProfileLinking = SETTING_MIXER_CONTROL_PROFILE_LINKING_DEFAULT, .automated_switch = SETTING_MIXER_AUTOMATED_SWITCH_DEFAULT, .switchTransitionTimer = SETTING_MIXER_SWITCH_TRANS_TIMER_DEFAULT, +#ifdef USE_AUTO_TRANSITION .vtolTransitionDynamicMixer = SETTING_MIXER_VTOL_TRANSITION_DYNAMIC_MIXER_DEFAULT, .manualVtolTransitionController = SETTING_MIXER_VTOL_MANUALSWITCH_AUTOTRANSITION_CONTROLLER_DEFAULT, .vtolTransitionAirspeedTimeoutMs = SETTING_MIXER_VTOL_TRANSITION_AIRSPEED_TIMEOUT_MS_DEFAULT, .vtolTransitionScaleRampTimeMs = SETTING_MIXER_VTOL_TRANSITION_SCALE_RAMP_TIME_MS_DEFAULT, +#endif .tailsitterOrientationOffset = SETTING_TAILSITTER_ORIENTATION_OFFSET_DEFAULT, .transition_PID_mmix_multiplier_roll = SETTING_TRANSITION_PID_MMIX_MULTIPLIER_ROLL_DEFAULT, .transition_PID_mmix_multiplier_pitch = SETTING_TRANSITION_PID_MMIX_MULTIPLIER_PITCH_DEFAULT, @@ -120,6 +124,7 @@ void mixerConfigInit(void) void setMixerProfileAT(void) { +#ifdef USE_AUTO_TRANSITION const timeMs_t now = millis(); mixerProfileAT.transitionStartTime = now; @@ -136,8 +141,13 @@ void setMixerProfileAT(void) mixerProfileAT.liftScale = 1.0f; mixerProfileAT.mcAuthorityScale = 1.0f; mixerProfileAT.fwAuthorityScale = 1.0f; +#else + mixerProfileAT.transitionStartTime = millis(); + mixerProfileAT.transitionTransEndTime = mixerProfileAT.transitionStartTime + (timeMs_t)currentMixerConfig.switchTransitionTimer * 100; +#endif } +#ifdef USE_AUTO_TRANSITION static bool requestTransitionsToFixedWing(const mixerProfileATRequest_e required_action) { return required_action == MIXERAT_REQUEST_RTH || @@ -391,6 +401,7 @@ static bool mixerATReadyForHotSwitch(const mixerProfileATRequest_e required_acti return elapsedMs >= transitionTimerMs; } +#endif bool platformTypeConfigured(flyingPlatformType_e platformType) { @@ -400,6 +411,7 @@ bool platformTypeConfigured(flyingPlatformType_e platformType) return mixerConfigByIndex(nextMixerProfileIndex)->platformType == platformType; } +#ifdef USE_AUTO_TRANSITION static bool missionTransitionToMultirotorTypeConfigured(void) { if (!isModeActivationConditionPresent(BOXMIXERPROFILE)) { @@ -409,6 +421,7 @@ static bool missionTransitionToMultirotorTypeConfigured(void) const flyingPlatformType_e nextPlatformType = mixerConfigByIndex(nextMixerProfileIndex)->platformType; return isMultirotorTypePlatform(nextPlatformType); } +#endif bool checkMixerATRequired(mixerProfileATRequest_e required_action) { @@ -422,6 +435,7 @@ bool checkMixerATRequired(mixerProfileATRequest_e required_action) return false; } +#ifdef USE_AUTO_TRANSITION switch (required_action) { case MIXERAT_REQUEST_RTH: return currentMixerConfig.automated_switch && STATE(MULTIROTOR) && platformTypeConfigured(PLATFORM_AIRPLANE); @@ -444,14 +458,28 @@ bool checkMixerATRequired(mixerProfileATRequest_e required_action) default: return false; } +#else + if(currentMixerConfig.automated_switch){ + if ((required_action == MIXERAT_REQUEST_RTH) && STATE(MULTIROTOR)) + { + return true; + } + if ((required_action == MIXERAT_REQUEST_LAND) && STATE(AIRPLANE)) + { + return true; + } + } + return false; +#endif } bool mixerATUpdateState(mixerProfileATRequest_e required_action) { +#ifdef USE_AUTO_TRANSITION //return true if mixerAT is done or not required bool reprocessState; do - { + { reprocessState=false; if (required_action == MIXERAT_REQUEST_ABORT) { abortTransition(false); @@ -512,6 +540,48 @@ bool mixerATUpdateState(mixerProfileATRequest_e required_action) } while (reprocessState); return true; +#else + //return true if mixerAT is done or not required + bool reprocessState; + do + { + reprocessState=false; + if (required_action==MIXERAT_REQUEST_ABORT){ + isMixerTransitionMixing_requested = false; + mixerProfileAT.phase = MIXERAT_PHASE_IDLE; + return true; + } + switch (mixerProfileAT.phase){ + case MIXERAT_PHASE_IDLE: + //check if mixerAT is required + if (checkMixerATRequired(required_action)){ + mixerProfileAT.phase=MIXERAT_PHASE_TRANSITION_INITIALIZE; + reprocessState = true; + } + break; + case MIXERAT_PHASE_TRANSITION_INITIALIZE: + setMixerProfileAT(); + mixerProfileAT.phase = MIXERAT_PHASE_TRANSITIONING; + reprocessState = true; + break; + case MIXERAT_PHASE_TRANSITIONING: + isMixerTransitionMixing_requested = true; + if (millis() > mixerProfileAT.transitionTransEndTime){ + isMixerTransitionMixing_requested = false; + outputProfileHotSwitch(nextMixerProfileIndex); + mixerProfileAT.phase = MIXERAT_PHASE_IDLE; + reprocessState = true; + //transition is done + } + return false; + break; + default: + break; + } + } + while (reprocessState); + return true; +#endif } bool checkMixerProfileHotSwitchAvalibility(void) @@ -530,6 +600,7 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) return; } +#ifdef USE_AUTO_TRANSITION bool mixerAT_inuse = mixerATIsActive(); const bool transitionModeActive = IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION); const bool transitionModeRisingEdge = transitionModeActive && !manualTransitionModeWasActive; @@ -675,6 +746,18 @@ void outputProfileUpdateTask(timeUs_t currentTimeUs) if (!isMixerTransitionMixing) { resetTransitionScales(); } +#else + bool mixerAT_inuse = mixerProfileAT.phase != MIXERAT_PHASE_IDLE; + // transition mode input for servo mix and motor mix + if (!FLIGHT_MODE(FAILSAFE_MODE) && (!mixerAT_inuse)) + { + if (isModeActivationConditionPresent(BOXMIXERPROFILE)){ + outputProfileHotSwitch(IS_RC_MODE_ACTIVE(BOXMIXERPROFILE) == 0 ? 0 : 1); + } + isMixerTransitionMixing_requested = IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION); + } + isMixerTransitionMixing = isMixerTransitionMixing_requested && ((posControl.navState == NAV_STATE_IDLE) || mixerAT_inuse ||(posControl.navState == NAV_STATE_ALTHOLD_IN_PROGRESS)); +#endif } bool mixerATIsActive(void) @@ -684,37 +767,65 @@ bool mixerATIsActive(void) bool mixerATWasAborted(void) { +#ifdef USE_AUTO_TRANSITION return mixerProfileAT.aborted; +#else + return false; +#endif } bool mixerATWasAbortedByAirspeedTimeout(void) { +#ifdef USE_AUTO_TRANSITION return mixerProfileAT.abortedByAirspeedTimeout; +#else + return false; +#endif } float mixerATGetPusherScale(void) { +#ifdef USE_AUTO_TRANSITION return constrainf(mixerProfileAT.pusherScale, 0.0f, 1.0f); +#else + return 1.0f; +#endif } float mixerATGetLiftScale(void) { +#ifdef USE_AUTO_TRANSITION return constrainf(mixerProfileAT.liftScale, 0.0f, 1.0f); +#else + return 1.0f; +#endif } float mixerATGetMcAuthorityScale(void) { +#ifdef USE_AUTO_TRANSITION return constrainf(mixerProfileAT.mcAuthorityScale, 0.0f, 1.0f); +#else + return 1.0f; +#endif } float mixerATGetFwAuthorityScale(void) { +#ifdef USE_AUTO_TRANSITION return constrainf(mixerProfileAT.fwAuthorityScale, 0.0f, 1.0f); +#else + return 1.0f; +#endif } float mixerATGetBlendToFw(void) { +#ifdef USE_AUTO_TRANSITION return constrainf(mixerProfileAT.blendToFw, 0.0f, 1.0f); +#else + return 1.0f; +#endif } bool isMixerProfile2ModeReportedActive(void) @@ -728,6 +839,7 @@ bool isMixerProfile2ModeReportedActive(void) bool isMixerTransitionModeReportedActive(void) { +#ifdef USE_AUTO_TRANSITION // Transition is actively running in the internal controller. if (mixerATIsActive()) { return true; @@ -739,6 +851,9 @@ bool isMixerTransitionModeReportedActive(void) } return IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION); +#else + return IS_RC_MODE_ACTIVE(BOXMIXERTRANSITION); +#endif } // switch mixerprofile without reboot diff --git a/src/main/flight/mixer_profile.h b/src/main/flight/mixer_profile.h index cbc69066ea0..05a4a9d1bc8 100644 --- a/src/main/flight/mixer_profile.h +++ b/src/main/flight/mixer_profile.h @@ -18,10 +18,12 @@ typedef struct mixerConfig_s { bool controlProfileLinking; bool automated_switch; int16_t switchTransitionTimer; +#ifdef USE_AUTO_TRANSITION bool vtolTransitionDynamicMixer; bool manualVtolTransitionController; uint16_t vtolTransitionAirspeedTimeoutMs; uint16_t vtolTransitionScaleRampTimeMs; +#endif bool tailsitterOrientationOffset; int16_t transition_PID_mmix_multiplier_roll; int16_t transition_PID_mmix_multiplier_pitch; @@ -38,28 +40,36 @@ typedef enum { MIXERAT_REQUEST_NONE, //no request, stats checking only MIXERAT_REQUEST_RTH, MIXERAT_REQUEST_LAND, +#ifdef USE_AUTO_TRANSITION MIXERAT_REQUEST_MISSION_TO_FW, MIXERAT_REQUEST_MISSION_TO_MC, MIXERAT_REQUEST_MANUAL_TO_FW, MIXERAT_REQUEST_MANUAL_TO_MC, +#endif MIXERAT_REQUEST_ABORT, } mixerProfileATRequest_e; +#ifdef USE_AUTO_TRANSITION typedef enum { MIXERAT_DIRECTION_NONE = 0, MIXERAT_DIRECTION_TO_FW, MIXERAT_DIRECTION_TO_MC, } mixerProfileATDirection_e; +#endif //mixerProfile Automated Transition PHASE typedef enum { MIXERAT_PHASE_IDLE, MIXERAT_PHASE_TRANSITION_INITIALIZE, MIXERAT_PHASE_TRANSITIONING, +#ifndef USE_AUTO_TRANSITION + MIXERAT_PHASE_DONE, +#endif } mixerProfileATState_e; typedef struct mixerProfileAT_s { mixerProfileATState_e phase; +#ifdef USE_AUTO_TRANSITION mixerProfileATDirection_e direction; mixerProfileATRequest_e request; bool aborted; @@ -76,6 +86,12 @@ typedef struct mixerProfileAT_s { float mcAuthorityScale; float fwAuthorityScale; timeMs_t transitionStartTime; +#else + bool transitionInputMixing; + timeMs_t transitionStartTime; + timeMs_t transitionStabEndTime; + timeMs_t transitionTransEndTime; +#endif } mixerProfileAT_t; extern mixerProfileAT_t mixerProfileAT; bool checkMixerATRequired(mixerProfileATRequest_e required_action); diff --git a/src/main/flight/pid.c b/src/main/flight/pid.c index 58fa056e14f..f36f6912653 100644 --- a/src/main/flight/pid.c +++ b/src/main/flight/pid.c @@ -119,6 +119,33 @@ typedef struct { fwPidAttenuation_t attenuation; } pidState_t; +#ifdef USE_AUTO_TRANSITION +typedef struct { + float kP; + float kI; + float kD; + float kFF; + float gyroRate; + float rateTarget; + float errorGyroIf; + float errorGyroIfLimit; + pt1Filter_t ptermLpfState; + filter_t dtermLpfState; + float previousRateTarget; + float previousRateGyro; +#ifdef USE_D_BOOST + pt1Filter_t dBoostLpf; + biquadFilter_t dBoostGyroLpf; + float dBoostTargetAcceleration; +#endif + filterApply4FnPtr ptermFilterApplyFn; +#ifdef USE_SMITH_PREDICTOR + smithPredictor_t smithPredictor; +#endif + fwPidAttenuation_t attenuation; +} autoTransitionTargetPidState_t; +#endif + STATIC_FASTRAM bool pidFiltersConfigured = false; static EXTENDED_FASTRAM float headingHoldCosZLimit; static EXTENDED_FASTRAM int16_t headingHoldTarget; @@ -128,6 +155,9 @@ static EXTENDED_FASTRAM pt1Filter_t fixedWingTpaFilter; // Thrust PID Attenuation factor. 0.0f means fully attenuated, 1.0f no attenuation is applied STATIC_FASTRAM bool pidGainsUpdateRequired= true; FASTRAM int16_t axisPID[FLIGHT_DYNAMICS_INDEX_COUNT]; +#ifdef USE_AUTO_TRANSITION +FASTRAM int16_t autoTransitionTargetAxisPID[FLIGHT_DYNAMICS_INDEX_COUNT]; +#endif #ifdef USE_BLACKBOX int32_t axisPID_P[FLIGHT_DYNAMICS_INDEX_COUNT]; @@ -138,8 +168,17 @@ int32_t axisPID_Setpoint[FLIGHT_DYNAMICS_INDEX_COUNT]; #endif static EXTENDED_FASTRAM pidState_t pidState[FLIGHT_DYNAMICS_INDEX_COUNT]; +#ifdef USE_AUTO_TRANSITION +static EXTENDED_FASTRAM autoTransitionTargetPidState_t autoTransitionTargetPidState[FLIGHT_DYNAMICS_INDEX_COUNT]; +#endif static EXTENDED_FASTRAM pt1Filter_t windupLpf[XYZ_AXIS_COUNT]; static EXTENDED_FASTRAM uint8_t itermRelax; +#ifdef USE_AUTO_TRANSITION +static EXTENDED_FASTRAM pt1Filter_t autoTransitionTargetTpaFilter; +static EXTENDED_FASTRAM filterApplyFnPtr autoTransitionTargetDTermLpfFilterApplyFn; +static EXTENDED_FASTRAM uint8_t autoTransitionTargetYawLpfHz; +static EXTENDED_FASTRAM int8_t autoTransitionTargetControlProfileIndex = -1; +#endif #ifdef USE_ANTIGRAVITY static EXTENDED_FASTRAM pt1Filter_t antigravityThrottleLpf; @@ -180,6 +219,8 @@ static EXTENDED_FASTRAM timeMs_t pidLoopNowMs; static EXTENDED_FASTRAM float fixedWingLevelTrim; static EXTENDED_FASTRAM pidController_t fixedWingLevelTrimController; +static void applyItermLimiting(pidState_t *pidState); + PG_REGISTER_PROFILE_WITH_RESET_TEMPLATE(pidProfile_t, pidProfile, PG_PID_PROFILE, 11); PG_RESET_TEMPLATE(pidProfile_t, pidProfile, @@ -395,6 +436,11 @@ void pidResetErrorAccumulators(void) for (int axis = 0; axis < 3; axis++) { pidState[axis].errorGyroIf = 0.0f; pidState[axis].errorGyroIfLimit = 0.0f; +#ifdef USE_AUTO_TRANSITION + autoTransitionTargetPidState[axis].errorGyroIf = 0.0f; + autoTransitionTargetPidState[axis].errorGyroIfLimit = 0.0f; + autoTransitionTargetAxisPID[axis] = 0; +#endif } } @@ -414,6 +460,372 @@ float getAxisIterm(uint8_t axis) return pidState[axis].errorGyroIf; } +#ifdef USE_AUTO_TRANSITION +static void resetAutoTransitionTargetPidState(void) +{ + memset(autoTransitionTargetPidState, 0, sizeof(autoTransitionTargetPidState)); + memset(autoTransitionTargetAxisPID, 0, sizeof(autoTransitionTargetAxisPID)); + memset(&autoTransitionTargetTpaFilter, 0, sizeof(autoTransitionTargetTpaFilter)); + autoTransitionTargetControlProfileIndex = -1; + autoTransitionTargetDTermLpfFilterApplyFn = (filterApplyFnPtr)nullFilterApply; + autoTransitionTargetYawLpfHz = 0; +} + +static bool isAutoTransitionTargetPidActive(void) +{ + return isMixerTransitionMixing && + mixerATIsActive() && + mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW && + isMultirotorTypePlatform(currentMixerConfig.platformType) && + nextMixerProfileIndex >= 0 && + nextMixerProfileIndex < MAX_PROFILE_COUNT && + mixerConfigByIndex(nextMixerProfileIndex)->platformType == PLATFORM_AIRPLANE; +} + +static uint8_t getAutoTransitionTargetControlProfileIndex(void) +{ + if (currentMixerConfig.controlProfileLinking && + nextMixerProfileIndex >= 0 && + nextMixerProfileIndex < MAX_CONTROL_PROFILE_COUNT && + nextMixerProfileIndex < MAX_PROFILE_COUNT) { + return nextMixerProfileIndex; + } + + return getConfigProfile(); +} + +static const controlConfig_t *getAutoTransitionTargetControlProfile(uint8_t profileIndex) +{ + if (currentMixerConfig.controlProfileLinking && + profileIndex < MAX_CONTROL_PROFILE_COUNT) { + return controlProfiles(profileIndex); + } + + return currentControlProfile; +} + +static const pidProfile_t *getAutoTransitionTargetPidProfile(uint8_t profileIndex) +{ + if (currentMixerConfig.controlProfileLinking && profileIndex < MAX_PROFILE_COUNT) { + const pgRegistry_t *pidProfileRegistry = pgFind(PG_PID_PROFILE); + if (pidProfileRegistry) { + return (const pidProfile_t *)(pidProfileRegistry->address + (pgSize(pidProfileRegistry) * profileIndex)); + } + } + + return pidProfile(); +} + +static void initAutoTransitionTargetPidState(const uint8_t profileIndex, const controlConfig_t *controlProfile, const pidProfile_t *targetPidProfile) +{ + const uint32_t refreshRate = getLooptime(); + + resetAutoTransitionTargetPidState(); + + autoTransitionTargetControlProfileIndex = profileIndex; + autoTransitionTargetYawLpfHz = targetPidProfile->yaw_lpf_hz; + + assignFilterApplyFn(targetPidProfile->dterm_lpf_type, targetPidProfile->dterm_lpf_hz, &autoTransitionTargetDTermLpfFilterApplyFn); + + if (controlProfile->throttle.fixedWingTauMs > 0) { + pt1FilterInitRC(&autoTransitionTargetTpaFilter, MS2S(controlProfile->throttle.fixedWingTauMs), US2S(TASK_PERIOD_HZ(TASK_AUX_RATE_HZ))); + pt1FilterReset(&autoTransitionTargetTpaFilter, getThrottleIdleValue()); + } + + for (uint8_t axis = FD_ROLL; axis <= FD_YAW; axis++) { +#ifdef USE_D_BOOST + autoTransitionTargetPidState[axis].dBoostTargetAcceleration = controlProfile->stabilized.rates[axis] * 10 * 10; +#endif + + if (axis == FD_YAW && autoTransitionTargetYawLpfHz) { + autoTransitionTargetPidState[axis].ptermFilterApplyFn = (filterApply4FnPtr)pt1FilterApply4; + } else { + autoTransitionTargetPidState[axis].ptermFilterApplyFn = (filterApply4FnPtr)nullFilterApply4; + } + + initFilter(targetPidProfile->dterm_lpf_type, &autoTransitionTargetPidState[axis].dtermLpfState, targetPidProfile->dterm_lpf_hz, refreshRate); + +#ifdef USE_D_BOOST + biquadFilterInitLPF(&autoTransitionTargetPidState[axis].dBoostGyroLpf, targetPidProfile->dBoostGyroDeltaLpfHz, refreshRate); +#endif + +#ifdef USE_SMITH_PREDICTOR + smithPredictorInit( + &autoTransitionTargetPidState[axis].smithPredictor, + targetPidProfile->smithPredictorDelay, + targetPidProfile->smithPredictorStrength, + targetPidProfile->smithPredictorFilterHz, + refreshRate + ); +#endif + } +} + +static float calculateAutoTransitionTargetAirspeedTPAFactor(const pidProfile_t *targetPidProfile, const controlConfig_t *controlProfile) +{ + const float airspeed = constrainf(getAirspeedEstimate(), 100.0f, 20000.0f); + const float referenceAirspeed = targetPidProfile->fixedWingReferenceAirspeed; + float tpaFactor = powf(referenceAirspeed / airspeed, controlProfile->throttle.apa_pow / 100.0f); + + return constrainf(tpaFactor, 0.3f, 2.0f); +} + +static float calculateAutoTransitionTargetAirspeedITermFactor(const pidProfile_t *targetPidProfile, const controlConfig_t *controlProfile) +{ + const float airspeed = constrainf(getAirspeedEstimate(), 100.0f, 20000.0f); + const float referenceAirspeed = targetPidProfile->fixedWingReferenceAirspeed; + const float apaPow = controlProfile->throttle.apa_pow; + + if (apaPow <= 100.0f) { + return 1.0f; + } + + return constrainf(powf(referenceAirspeed / airspeed, (apaPow / 100.0f) - 1.0f), 0.3f, 1.5f); +} + +static uint16_t calculateAutoTransitionTargetTPAThrottle(const controlConfig_t *controlProfile) +{ + if (controlProfile->throttle.fixedWingTauMs > 0) { + static const fpVector3_t vDown = { .v = { 0.0f, 0.0f, 1.0f } }; + fpVector3_t vForward = { .v = { HeadVecEFFiltered.x, -HeadVecEFFiltered.y, -HeadVecEFFiltered.z } }; + const float groundCos = vectorDotProduct(&vForward, &vDown); + const int16_t throttleAdjustment = controlProfile->throttle.tpa_pitch_compensation * groundCos * 90.0f / 1.57079632679f; + const uint16_t throttleAdjusted = rcCommand[THROTTLE] + constrain(throttleAdjustment, -1000, 1000); + return pt1FilterApply(&autoTransitionTargetTpaFilter, constrain(throttleAdjusted, 1000, 2000)); + } + + return rcCommand[THROTTLE]; +} + +static float calculateAutoTransitionTargetTPAFactor(const controlConfig_t *controlProfile) +{ + const uint16_t throttle = calculateAutoTransitionTargetTPAThrottle(controlProfile); + float tpaFactor; + + if (controlProfile->throttle.dynPID != 0 && + controlProfile->throttle.pa_breakpoint > getThrottleIdleValue() && + !FLIGHT_MODE(AUTO_TUNE) && + ARMING_FLAG(ARMED)) { + if (throttle > getThrottleIdleValue()) { + tpaFactor = 0.5f + ((float)(controlProfile->throttle.pa_breakpoint - getThrottleIdleValue()) / (throttle - getThrottleIdleValue()) / 2.0f); + } else { + tpaFactor = 2.0f; + } + + tpaFactor = 1.0f + (tpaFactor - 1.0f) * (controlProfile->throttle.dynPID / 100.0f); + tpaFactor = constrainf(tpaFactor, 0.3f, 2.0f); + } else { + tpaFactor = 1.0f; + } + + return tpaFactor; +} + +static void updateAutoTransitionTargetPIDCoefficients(const controlConfig_t *controlProfile, const pidProfile_t *targetPidProfile) +{ + float tpaFactor = 1.0f; + float iTermFactor = 1.0f; + + if (controlProfile->throttle.apa_pow > 0 && + pitotValidForAirspeed()) { + tpaFactor = calculateAutoTransitionTargetAirspeedTPAFactor(targetPidProfile, controlProfile); + iTermFactor = calculateAutoTransitionTargetAirspeedITermFactor(targetPidProfile, controlProfile); + } else { + tpaFactor = calculateAutoTransitionTargetTPAFactor(controlProfile); + iTermFactor = tpaFactor; + } + + for (uint8_t axis = FD_ROLL; axis <= FD_YAW; axis++) { + autoTransitionTargetPidState[axis].kP = targetPidProfile->bank_fw.pid[axis].P / FP_PID_RATE_P_MULTIPLIER * tpaFactor; + autoTransitionTargetPidState[axis].kI = targetPidProfile->bank_fw.pid[axis].I / FP_PID_RATE_I_MULTIPLIER * iTermFactor; + autoTransitionTargetPidState[axis].kD = targetPidProfile->bank_fw.pid[axis].D / FP_PID_RATE_D_MULTIPLIER * tpaFactor; + autoTransitionTargetPidState[axis].kFF = targetPidProfile->bank_fw.pid[axis].FF / FP_PID_RATE_FF_MULTIPLIER * tpaFactor; + } +} + +static float autoTransitionTargetPTermProcess(autoTransitionTargetPidState_t *pidState, flight_dynamics_index_t axis, float rateError, float dT) +{ + const float newPTerm = rateError * pidState->kP; + UNUSED(axis); + + return pidState->ptermFilterApplyFn(&pidState->ptermLpfState, newPTerm, autoTransitionTargetYawLpfHz, dT); +} + +#ifdef USE_D_BOOST +static float applyAutoTransitionTargetDBoost(autoTransitionTargetPidState_t *pidState, const pidProfile_t *targetPidProfile, float currentRateTarget, float dT, float dT_inv) +{ + float dBoost = 1.0f; + const float dBoostGyroDelta = (pidState->gyroRate - pidState->previousRateGyro) * dT_inv; + const float dBoostGyroAcceleration = fabsf(biquadFilterApply(&pidState->dBoostGyroLpf, dBoostGyroDelta)); + const float dBoostRateAcceleration = fabsf((currentRateTarget - pidState->previousRateTarget) * dT_inv); + + if (dBoostGyroAcceleration >= dBoostRateAcceleration) { + dBoost = scaleRangef(dBoostGyroAcceleration, 0.0f, targetPidProfile->dBoostMaxAtAlleceleration, 1.0f, targetPidProfile->dBoostMax); + } else { + dBoost = scaleRangef(dBoostRateAcceleration, 0.0f, pidState->dBoostTargetAcceleration, 1.0f, targetPidProfile->dBoostMin); + } + + dBoost = pt1FilterApply4(&pidState->dBoostLpf, dBoost, D_BOOST_LPF_HZ, dT); + return constrainf(dBoost, targetPidProfile->dBoostMin, targetPidProfile->dBoostMax); +} +#else +static float applyAutoTransitionTargetDBoost(autoTransitionTargetPidState_t *pidState, const pidProfile_t *targetPidProfile, float currentRateTarget, float dT, float dT_inv) +{ + UNUSED(pidState); + UNUSED(targetPidProfile); + UNUSED(currentRateTarget); + UNUSED(dT); + UNUSED(dT_inv); + return 1.0f; +} +#endif + +static float autoTransitionTargetDTermProcess(autoTransitionTargetPidState_t *pidState, const pidProfile_t *targetPidProfile, float currentRateTarget, float dT, float dT_inv) +{ + if (pidState->kD == 0.0f) { + return 0.0f; + } + + float delta = pidState->previousRateGyro - pidState->gyroRate; + delta = autoTransitionTargetDTermLpfFilterApplyFn((filter_t *)&pidState->dtermLpfState, delta); + + return delta * (pidState->kD * dT_inv) * applyAutoTransitionTargetDBoost(pidState, targetPidProfile, currentRateTarget, dT, dT_inv); +} + +static void applyAutoTransitionTargetItermLock(autoTransitionTargetPidState_t *pidState, const controlConfig_t *controlProfile, const pidProfile_t *targetPidProfile, flight_dynamics_index_t axis, const float rateTarget, const float rateError) +{ + const float maxRate = controlProfile->stabilized.rates[axis] * 10.0f; + const float dampingFactor = attenuation(rateTarget, maxRate * targetPidProfile->fwItermLockRateLimit / 100.0f); + const bool errorThresholdReached = fabsf(rateError) > maxRate * targetPidProfile->fwItermLockEngageThreshold / 100.0f; + + if (fabsf(rateTarget) > maxRate * 0.2f) { + pidState->attenuation.targetOverThresholdTimeMs = pidLoopNowMs; + } + + if (!errorThresholdReached) { + pidState->attenuation.targetOverThresholdTimeMs = 0; + } + + pidState->attenuation.aI = MIN(dampingFactor, (errorThresholdReached && (pidLoopNowMs - pidState->attenuation.targetOverThresholdTimeMs) < targetPidProfile->fwItermLockTimeMaxMs) ? 0.0f : 1.0f); + pidState->attenuation.aP = dampingFactor; + pidState->attenuation.aD = dampingFactor; +} + +static bool checkAutoTransitionTargetItermLimitingActive(void) +{ + return STATE(ANTI_WINDUP); +} + +static void applyAutoTransitionTargetItermLimiting(autoTransitionTargetPidState_t *pidState, const bool itermLimitActive) +{ + if (itermLimitActive) { + pidState->errorGyroIf = constrainf(pidState->errorGyroIf, -pidState->errorGyroIfLimit, pidState->errorGyroIfLimit); + } else { + pidState->errorGyroIfLimit = fabsf(pidState->errorGyroIf); + } +} + +static bool checkAutoTransitionTargetItermFreezingActive(const pidProfile_t *targetPidProfile, flight_dynamics_index_t axis) +{ + if (targetPidProfile->fixedWingYawItermBankFreeze != 0 && + axis == FD_YAW) { + const float bankAngle = DECIDEGREES_TO_DEGREES(attitude.values.roll); + return fabsf(bankAngle) > targetPidProfile->fixedWingYawItermBankFreeze && !(FLIGHT_MODE(AUTO_TUNE) || FLIGHT_MODE(TURN_ASSISTANT)); + } + + return false; +} + +static int16_t applyAutoTransitionTargetFixedWingRateController(autoTransitionTargetPidState_t *pidState, const controlConfig_t *controlProfile, const pidProfile_t *targetPidProfile, flight_dynamics_index_t axis, const bool itermLimitActive, const bool itermFreezeActive, float dT, float dT_inv) +{ + const float rateTarget = getFlightAxisRateOverride(axis, pidState->rateTarget); + const float rateError = rateTarget - pidState->gyroRate; + + applyAutoTransitionTargetItermLock(pidState, controlProfile, targetPidProfile, axis, rateTarget, rateError); + + const float newPTerm = autoTransitionTargetPTermProcess(pidState, axis, rateError, dT) * pidState->attenuation.aP; + const float newDTerm = autoTransitionTargetDTermProcess(pidState, targetPidProfile, rateTarget, dT, dT_inv) * pidState->attenuation.aD; + const float newFFTerm = rateTarget * pidState->kFF; + + if (!itermFreezeActive) { + pidState->errorGyroIf += rateError * pidState->kI * dT * pidState->attenuation.aI; + } + + applyAutoTransitionTargetItermLimiting(pidState, itermLimitActive); + + const uint16_t limit = 500; + + if (targetPidProfile->pidItermLimitPercent != 0) { + const float itermLimit = limit * targetPidProfile->pidItermLimitPercent * 0.01f; + pidState->errorGyroIf = constrainf(pidState->errorGyroIf, -itermLimit, +itermLimit); + } + + pidState->previousRateGyro = pidState->gyroRate; + pidState->previousRateTarget = rateTarget; + + return constrainf(newPTerm + newFFTerm + pidState->errorGyroIf + newDTerm, -limit, +limit); +} + +static void updateAutoTransitionTargetAxisPID(float dT) +{ + if (!pidFiltersConfigured || !isAutoTransitionTargetPidActive()) { + resetAutoTransitionTargetPidState(); + return; + } + + const uint8_t profileIndex = getAutoTransitionTargetControlProfileIndex(); + const controlConfig_t *controlProfile = getAutoTransitionTargetControlProfile(profileIndex); + const pidProfile_t *targetPidProfile = getAutoTransitionTargetPidProfile(profileIndex); + + if (autoTransitionTargetControlProfileIndex != profileIndex) { + initAutoTransitionTargetPidState(profileIndex, controlProfile, targetPidProfile); + } + + updateAutoTransitionTargetPIDCoefficients(controlProfile, targetPidProfile); + + const float dT_inv = 1.0f / dT; + + for (uint8_t axis = FD_ROLL; axis <= FD_YAW; axis++) { + autoTransitionTargetPidState[axis].gyroRate = gyro.gyroADCf[axis]; + autoTransitionTargetPidState[axis].rateTarget = pidState[axis].rateTarget; + +#ifdef USE_SMITH_PREDICTOR + autoTransitionTargetPidState[axis].gyroRate = applySmithPredictor(axis, &autoTransitionTargetPidState[axis].smithPredictor, autoTransitionTargetPidState[axis].gyroRate); +#endif + + const bool itermLimitActive = checkAutoTransitionTargetItermLimitingActive(); + const bool itermFreezeActive = checkAutoTransitionTargetItermFreezingActive(targetPidProfile, axis); + + autoTransitionTargetAxisPID[axis] = applyAutoTransitionTargetFixedWingRateController(&autoTransitionTargetPidState[axis], controlProfile, targetPidProfile, axis, itermLimitActive, itermFreezeActive, dT, dT_inv); + } +} + +int16_t getAutoTransitionTargetStabilizedInput(flight_dynamics_index_t axis) +{ + if (!(isMixerTransitionMixing && + mixerATIsActive() && + mixerProfileAT.direction != MIXERAT_DIRECTION_NONE)) { + return 0; + } + + const float scale = currentMixerConfig.vtolTransitionDynamicMixer ? mixerATGetFwAuthorityScale() : 1.0f; + + if (FLIGHT_MODE(MANUAL_MODE)) { + return lrintf(rcCommand[axis] * scale); + } + + if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW) { + return lrintf(autoTransitionTargetAxisPID[axis] * scale); + } + + if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_MC) { + return lrintf(axisPID[axis] * scale); + } + + return 0; +} +#endif + static float pidRcCommandToAngle(int16_t stick, int16_t maxInclination) { stick = constrain(stick, -500, 500); @@ -1226,6 +1638,9 @@ void FAST_CODE pidController(float dT) pidLoopNowMs = millis(); if (!pidFiltersConfigured) { +#ifdef USE_AUTO_TRANSITION + resetAutoTransitionTargetPidState(); +#endif return; } @@ -1326,6 +1741,10 @@ void FAST_CODE pidController(float dT) pidControllerApplyFn(&pidState[axis], dT, dT_inv); } + +#ifdef USE_AUTO_TRANSITION + updateAutoTransitionTargetAxisPID(dT); +#endif } pidType_e pidIndexGetType(pidIndex_e pidIndex) @@ -1427,6 +1846,10 @@ void pidInit(void) 0.0f ); +#ifdef USE_AUTO_TRANSITION + resetAutoTransitionTargetPidState(); +#endif + } const pidBank_t * pidBank(void) { @@ -1508,4 +1931,4 @@ uint16_t getPidSumLimit(const flight_dynamics_index_t axis) { } else { return 500; } -} \ No newline at end of file +} diff --git a/src/main/flight/pid.h b/src/main/flight/pid.h index ff2e85031b7..5c613d37a16 100644 --- a/src/main/flight/pid.h +++ b/src/main/flight/pid.h @@ -187,7 +187,13 @@ const pidBank_t * pidBank(void); pidBank_t * pidBankMutable(void); extern int16_t axisPID[]; +#ifdef USE_AUTO_TRANSITION +extern int16_t autoTransitionTargetAxisPID[]; +#endif extern int32_t axisPID_P[], axisPID_I[], axisPID_D[], axisPID_F[], axisPID_Setpoint[]; +#ifdef USE_AUTO_TRANSITION +int16_t getAutoTransitionTargetStabilizedInput(flight_dynamics_index_t axis); +#endif void pidInit(void); bool pidInitFilters(void); @@ -227,4 +233,4 @@ bool isFixedWingLevelTrimActive(void); void updateFixedWingLevelTrim(timeUs_t currentTimeUs); float getFixedWingLevelTrim(void); bool isAngleHoldLevel(void); -uint16_t getPidSumLimit(const flight_dynamics_index_t axis); \ No newline at end of file +uint16_t getPidSumLimit(const flight_dynamics_index_t axis); diff --git a/src/main/flight/servos.c b/src/main/flight/servos.c index 8c9276a1fbc..b3b7e11a3b9 100755 --- a/src/main/flight/servos.c +++ b/src/main/flight/servos.c @@ -277,6 +277,79 @@ static void filterServos(void) } } +#ifdef USE_AUTO_TRANSITION +static bool isAutoTransitionTargetInputSource(const uint8_t inputSource) +{ + return inputSource >= INPUT_AUTOTRANSITION_TARGET_STABILIZED_ROLL && + inputSource <= INPUT_AUTOTRANSITION_TARGET_STABILIZED_YAW_MINUS; +} + +static bool isStabilizedAxisInputSource(const uint8_t inputSource) +{ + switch (inputSource) { + case INPUT_STABILIZED_ROLL: + case INPUT_STABILIZED_PITCH: + case INPUT_STABILIZED_YAW: + case INPUT_STABILIZED_ROLL_PLUS: + case INPUT_STABILIZED_ROLL_MINUS: + case INPUT_STABILIZED_PITCH_PLUS: + case INPUT_STABILIZED_PITCH_MINUS: + case INPUT_STABILIZED_YAW_PLUS: + case INPUT_STABILIZED_YAW_MINUS: + return true; + default: + return false; + } +} + +static int getAutoTransitionServoProfileIndex(void) +{ + if (isMultirotorTypePlatform(currentMixerConfig.platformType)) { + return currentMixerProfileIndex; + } + + if (nextMixerProfileIndex >= 0 && + nextMixerProfileIndex < MAX_MIXER_PROFILE_COUNT && + isMultirotorTypePlatform(mixerConfigByIndex(nextMixerProfileIndex)->platformType)) { + return nextMixerProfileIndex; + } + + return -1; +} + +static void collectAutoTransitionServoTargets(bool targets[MAX_SUPPORTED_SERVOS]) +{ + memset(targets, 0, MAX_SUPPORTED_SERVOS * sizeof(targets[0])); + + const int profileIndex = getAutoTransitionServoProfileIndex(); + if (profileIndex < 0) { + return; + } + + for (int i = 0; i < MAX_SERVO_RULES; i++) { + const servoMixer_t *rule = &mixerServoMixersByIndex(profileIndex)[i]; + + if (rule->rate == 0) { + break; + } + + if (!isAutoTransitionTargetInputSource(rule->inputSource)) { + continue; + } + +#ifdef USE_PROGRAMMING_FRAMEWORK + if (!logicConditionGetValue(rule->conditionId)) { + continue; + } +#endif + + if (rule->targetChannel < MAX_SUPPORTED_SERVOS) { + targets[rule->targetChannel] = true; + } + } +} +#endif + void writeServos(void) { filterServos(); @@ -305,6 +378,22 @@ void writeServos(void) void servoMixer(float dT) { int16_t input[INPUT_SOURCE_COUNT]; // Range [-500:+500] +#ifdef USE_AUTO_TRANSITION + const bool autoTransitionInputsActive = isMixerTransitionMixing && + mixerATIsActive() && + mixerProfileAT.direction != MIXERAT_DIRECTION_NONE; + const bool scaleCurrentFwServoRules = autoTransitionInputsActive && + mixerProfileAT.direction == MIXERAT_DIRECTION_TO_MC && + !isMultirotorTypePlatform(currentMixerConfig.platformType); + const float currentFwServoScale = currentMixerConfig.vtolTransitionDynamicMixer ? mixerATGetFwAuthorityScale() : 1.0f; + bool autoTransitionServoTargets[MAX_SUPPORTED_SERVOS]; + + if (scaleCurrentFwServoRules) { + collectAutoTransitionServoTargets(autoTransitionServoTargets); + } else { + memset(autoTransitionServoTargets, 0, sizeof(autoTransitionServoTargets)); + } +#endif if (FLIGHT_MODE(MANUAL_MODE)) { input[INPUT_STABILIZED_ROLL] = rcCommand[ROLL]; @@ -323,12 +412,26 @@ void servoMixer(float dT) } } +#ifdef USE_AUTO_TRANSITION + input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_ROLL] = getAutoTransitionTargetStabilizedInput(FD_ROLL); + input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_PITCH] = getAutoTransitionTargetStabilizedInput(FD_PITCH); + input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_YAW] = getAutoTransitionTargetStabilizedInput(FD_YAW); +#endif + input[INPUT_STABILIZED_ROLL_PLUS] = constrain(input[INPUT_STABILIZED_ROLL], 0, 1000); input[INPUT_STABILIZED_ROLL_MINUS] = constrain(input[INPUT_STABILIZED_ROLL], -1000, 0); input[INPUT_STABILIZED_PITCH_PLUS] = constrain(input[INPUT_STABILIZED_PITCH], 0, 1000); input[INPUT_STABILIZED_PITCH_MINUS] = constrain(input[INPUT_STABILIZED_PITCH], -1000, 0); input[INPUT_STABILIZED_YAW_PLUS] = constrain(input[INPUT_STABILIZED_YAW], 0, 1000); input[INPUT_STABILIZED_YAW_MINUS] = constrain(input[INPUT_STABILIZED_YAW], -1000, 0); +#ifdef USE_AUTO_TRANSITION + input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_ROLL_PLUS] = constrain(input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_ROLL], 0, 1000); + input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_ROLL_MINUS] = constrain(input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_ROLL], -1000, 0); + input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_PITCH_PLUS] = constrain(input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_PITCH], 0, 1000); + input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_PITCH_MINUS] = constrain(input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_PITCH], -1000, 0); + input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_YAW_PLUS] = constrain(input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_YAW], 0, 1000); + input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_YAW_MINUS] = constrain(input[INPUT_AUTOTRANSITION_TARGET_STABILIZED_YAW], -1000, 0); +#endif input[INPUT_FEATURE_FLAPS] = FLIGHT_MODE(FLAPERON) ? servoConfig()->flaperon_throw_offset : 0; @@ -455,6 +558,15 @@ void servoMixer(float dT) inputRaw = 0; } #endif + +#ifdef USE_AUTO_TRANSITION + if (scaleCurrentFwServoRules && + autoTransitionServoTargets[target] && + isStabilizedAxisInputSource(from)) { + inputRaw = lrintf(inputRaw * currentFwServoScale); + } +#endif + /* * Apply mixer speed limit. 1 [one] speed unit is defined as 10us/s: * 0 = no limiting diff --git a/src/main/flight/servos.h b/src/main/flight/servos.h index b16dd7ca915..593f5774fe5 100644 --- a/src/main/flight/servos.h +++ b/src/main/flight/servos.h @@ -85,6 +85,17 @@ typedef enum { INPUT_RC_CH33 = 58, INPUT_RC_CH34 = 59, INPUT_MIXER_SWITCH_HELPER = 60, +#ifdef USE_AUTO_TRANSITION + INPUT_AUTOTRANSITION_TARGET_STABILIZED_ROLL = 61, + INPUT_AUTOTRANSITION_TARGET_STABILIZED_PITCH = 62, + INPUT_AUTOTRANSITION_TARGET_STABILIZED_YAW = 63, + INPUT_AUTOTRANSITION_TARGET_STABILIZED_ROLL_PLUS = 64, + INPUT_AUTOTRANSITION_TARGET_STABILIZED_ROLL_MINUS = 65, + INPUT_AUTOTRANSITION_TARGET_STABILIZED_PITCH_PLUS = 66, + INPUT_AUTOTRANSITION_TARGET_STABILIZED_PITCH_MINUS = 67, + INPUT_AUTOTRANSITION_TARGET_STABILIZED_YAW_PLUS = 68, + INPUT_AUTOTRANSITION_TARGET_STABILIZED_YAW_MINUS = 69, +#endif INPUT_SOURCE_COUNT } inputSource_e; diff --git a/src/main/navigation/navigation.c b/src/main/navigation/navigation.c index 7d7120dd85c..21bfdd4875f 100644 --- a/src/main/navigation/navigation.c +++ b/src/main/navigation/navigation.c @@ -84,6 +84,7 @@ #define FW_LAND_LOITER_MIN_TIME 30000000 // usec (30 sec) #define FW_LAND_LOITER_ALT_TOLERANCE 150 +#ifdef USE_AUTO_TRANSITION // One-shot MC->FW mission retry after airspeed-timeout: hold position, yaw scan, align to best pitot heading. #define NAV_MIXERAT_RETRY_SCAN_STEP_CD DEGREES_TO_CENTIDEGREES(20) #define NAV_MIXERAT_RETRY_HEADING_TOL_CD DEGREES_TO_CENTIDEGREES(5) @@ -91,6 +92,7 @@ #define NAV_MIXERAT_RETRY_HEADING_STEP_TIMEOUT_MS 6000 #define NAV_MIXERAT_RETRY_MAX_TOTAL_MS 45000 #define NAV_MIXERAT_MISSION_TRANSITION_TRACK_DISTANCE_CM 4000 +#endif /*----------------------------------------------------------- * Compatibility for home position @@ -128,7 +130,11 @@ STATIC_ASSERT(NAV_MAX_WAYPOINTS < 254, NAV_MAX_WAYPOINTS_exceeded_allowable_rang PG_REGISTER_ARRAY(navWaypoint_t, NAV_MAX_WAYPOINTS, nonVolatileWaypointList, PG_WAYPOINT_MISSION_STORAGE, 2); #endif +#ifdef USE_AUTO_TRANSITION PG_REGISTER_WITH_RESET_TEMPLATE(navConfig_t, navConfig, PG_NAV_CONFIG, 10); +#else +PG_REGISTER_WITH_RESET_TEMPLATE(navConfig_t, navConfig, PG_NAV_CONFIG, 7); +#endif PG_RESET_TEMPLATE(navConfig_t, navConfig, .general = { @@ -158,11 +164,13 @@ PG_RESET_TEMPLATE(navConfig_t, navConfig, .pos_failure_timeout = SETTING_NAV_POSITION_TIMEOUT_DEFAULT, // 5 sec .waypoint_radius = SETTING_NAV_WP_RADIUS_DEFAULT, // 2m diameter .waypoint_safe_distance = SETTING_NAV_WP_MAX_SAFE_DISTANCE_DEFAULT, // Metres - first waypoint should be closer than this +#ifdef USE_AUTO_TRANSITION .vtol_mission_transition_user_action = SETTING_NAV_VTOL_MISSION_TRANSITION_USER_ACTION_DEFAULT, .vtol_mission_transition_min_altitude = SETTING_NAV_VTOL_MISSION_TRANSITION_MIN_ALTITUDE_CM_DEFAULT, .vtol_transition_retry_on_airspeed_timeout = SETTING_NAV_VTOL_TRANSITION_RETRY_ON_AIRSPEED_TIMEOUT_DEFAULT, .vtol_transition_fail_action_mc_to_fw = SETTING_NAV_VTOL_TRANSITION_FAIL_ACTION_MC_TO_FW_DEFAULT, .vtol_transition_fail_action_fw_to_mc = SETTING_NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_DEFAULT, +#endif #ifdef USE_MULTI_MISSION .waypoint_multi_mission_index = SETTING_NAV_WP_MULTI_MISSION_INDEX_DEFAULT, // mission index selected from multi mission WP entry #endif @@ -284,6 +292,7 @@ uint16_t navEPV; int16_t navAccNEU[3]; //End of blackbox states +#ifdef USE_AUTO_TRANSITION typedef struct navMixerATMissionTransition_s { mixerProfileATRequest_e request; int32_t heading; @@ -323,6 +332,9 @@ typedef enum { static navigationFSMState_t navMixerATPendingState = NAV_STATE_IDLE; static navMixerATMissionTransition_t navMixerATMissionTransition; +#else +static navigationFSMState_t navMixerATPendingState = NAV_STATE_IDLE; +#endif static fpVector3_t * rthGetHomeTargetPosition(rthTargetMode_e mode); static void updateDesiredRTHAltitude(void); @@ -342,7 +354,9 @@ static void resetJumpCounter(void); static void clearJumpCounters(void); static void calculateAndSetActiveWaypoint(const navWaypoint_t * waypoint); +#ifdef USE_AUTO_TRANSITION static bool getLocalPosNextWaypoint(fpVector3_t * nextWpPos); +#endif void calculateInitialHoldPosition(fpVector3_t * pos); void calculateFarAwayPos(fpVector3_t * farAwayPos, const fpVector3_t *start, int32_t bearing, int32_t distance); void calculateFarAwayTarget(fpVector3_t * farAwayPos, int32_t bearing, int32_t distance); @@ -350,6 +364,7 @@ bool isWaypointAltitudeReached(void); static void mapWaypointToLocalPosition(fpVector3_t * localPos, const navWaypoint_t * waypoint, geoAltitudeConversionMode_e altConv); static navigationFSMEvent_t nextForNonGeoStates(void); static bool isWaypointMissionValid(void); +#ifdef USE_AUTO_TRANSITION static void clearMissionVTOLTransitionState(void); static navMissionVtolTransitionDisposition_e prepareMissionVTOLTransition(const navWaypoint_t *waypoint); static void updateMissionTransitionGuidance(void); @@ -358,6 +373,7 @@ static bool hasAirspeedSensorForTransitionRetry(void); static bool canRetryTransitionAfterAirspeedTimeout(const mixerProfileATRequest_e request); static void beginMissionTransitionRetryScan(const mixerProfileATRequest_e request); static navMixerATRetryScanResult_e updateMissionTransitionRetryScan(void); +#endif void missionPlannerSetWaypoint(void); void initializeRTHSanityChecker(void); @@ -1107,12 +1123,16 @@ static const navigationFSMStateDescriptor_t navFSM[NAV_STATE_COUNT] = { .onEvent = { [NAV_FSM_EVENT_TIMEOUT] = NAV_STATE_MIXERAT_IN_PROGRESS, // re-process the state [NAV_FSM_EVENT_SWITCH_TO_IDLE] = NAV_STATE_MIXERAT_ABORT, +#ifdef USE_AUTO_TRANSITION [NAV_FSM_EVENT_SWITCH_TO_POSHOLD_3D] = NAV_STATE_POSHOLD_3D_INITIALIZE, [NAV_FSM_EVENT_SWITCH_TO_RTH] = NAV_STATE_RTH_INITIALIZE, [NAV_FSM_EVENT_SWITCH_TO_EMERGENCY_LANDING] = NAV_STATE_EMERGENCY_LANDING_INITIALIZE, +#endif [NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME] = NAV_STATE_RTH_HEAD_HOME, //switch to its pending state [NAV_FSM_EVENT_SWITCH_TO_RTH_LANDING] = NAV_STATE_RTH_LANDING, //switch to its pending state +#ifdef USE_AUTO_TRANSITION [NAV_FSM_EVENT_MIXERAT_MISSION_RESUME] = NAV_STATE_WAYPOINT_IN_PROGRESS, +#endif } }, [NAV_STATE_MIXERAT_ABORT] = { @@ -1326,6 +1346,7 @@ static const navigationFSMStateDescriptor_t navFSM[NAV_STATE_COUNT] = { static navigationFSMStateFlags_t navGetStateFlags(navigationFSMState_t state) { navigationFSMStateFlags_t stateFlags = navFSM[state].stateFlags; +#ifdef USE_AUTO_TRANSITION const bool mixerATState = (state == NAV_STATE_MIXERAT_INITIALIZE || state == NAV_STATE_MIXERAT_IN_PROGRESS); // During mission-authorized MC->FW transition, enable XY/YAW control to fly a straight acceleration segment. @@ -1342,6 +1363,7 @@ static navigationFSMStateFlags_t navGetStateFlags(navigationFSMState_t state) isTransitionRetryToFixedWingRequest(navMixerATMissionTransition.request)) { stateFlags |= NAV_CTL_POS | NAV_CTL_YAW; } +#endif return stateFlags; } @@ -1367,8 +1389,10 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_IDLE(navigationFSMState { UNUSED(previousState); +#ifdef USE_AUTO_TRANSITION navMixerATPendingState = NAV_STATE_IDLE; clearMissionVTOLTransitionState(); +#endif resetAltitudeController(false); resetHeadingController(); resetPositionController(); @@ -2052,6 +2076,7 @@ static navigationFSMEvent_t nextForNonGeoStates(void) } } +#ifdef USE_AUTO_TRANSITION static uint16_t missionUserActionMask(const navMissionUserAction_e userAction) { switch (userAction) { @@ -2387,6 +2412,7 @@ static void updateMissionTransitionGuidance(void) setDesiredPosition(&navGetCurrentActualPositionAndVelocity()->pos, posControl.actualState.yaw, NAV_POS_UPDATE_Z); } +#endif static navigationFSMEvent_t navOnEnteringState_NAV_STATE_WAYPOINT_PRE_ACTION(navigationFSMState_t previousState) { @@ -2403,6 +2429,7 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_WAYPOINT_PRE_ACTION(nav posControl.wpInitialAltitude = posControl.actualState.abs.pos.z; posControl.wpAltitudeReached = false; +#ifdef USE_AUTO_TRANSITION clearMissionVTOLTransitionState(); const navMissionVtolTransitionDisposition_e transitionAction = prepareMissionVTOLTransition(activeWaypoint); if (transitionAction == NAV_MISSION_VTOL_TRANSITION_START) { @@ -2411,6 +2438,7 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_WAYPOINT_PRE_ACTION(nav if (transitionAction == NAV_MISSION_VTOL_TRANSITION_REJECT) { return NAV_FSM_EVENT_ERROR; } +#endif return NAV_FSM_EVENT_SUCCESS; // will switch to NAV_STATE_WAYPOINT_IN_PROGRESS } @@ -2729,11 +2757,15 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_INITIALIZE(navi setupAltitudeController(); } +#ifdef USE_AUTO_TRANSITION if (previousState != NAV_STATE_WAYPOINT_PRE_ACTION) { clearMissionVTOLTransitionState(); } updateMissionTransitionGuidance(); +#else + setDesiredPosition(&navGetCurrentActualPositionAndVelocity()->pos, posControl.actualState.yaw, NAV_POS_UPDATE_Z); +#endif navMixerATPendingState = previousState; return NAV_FSM_EVENT_SUCCESS; } @@ -2742,6 +2774,7 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(nav { UNUSED(previousState); +#ifdef USE_AUTO_TRANSITION if (!ARMING_FLAG(ARMED) || FLIGHT_MODE(FAILSAFE_MODE)) { mixerATUpdateState(MIXERAT_REQUEST_ABORT); clearMissionVTOLTransitionState(); @@ -2841,13 +2874,54 @@ static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_IN_PROGRESS(nav updateMissionTransitionGuidance(); return NAV_FSM_EVENT_NONE; +#else + mixerProfileATRequest_e required_action; + switch (navMixerATPendingState) + { + case NAV_STATE_RTH_HEAD_HOME: + required_action = MIXERAT_REQUEST_RTH; + break; + case NAV_STATE_RTH_LANDING: + required_action = MIXERAT_REQUEST_LAND; + break; + default: + required_action = MIXERAT_REQUEST_NONE; + break; + } + if (mixerATUpdateState(required_action)){ + // MixerAT is done, switch to next state + resetPositionController(); + resetAltitudeController(false); // Make sure surface tracking is not enabled uses global altitude, not AGL + mixerATUpdateState(MIXERAT_REQUEST_ABORT); + switch (navMixerATPendingState) + { + case NAV_STATE_RTH_HEAD_HOME: + setupAltitudeController(); + return NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME; + break; + case NAV_STATE_RTH_LANDING: + setupAltitudeController(); + return NAV_FSM_EVENT_SWITCH_TO_RTH_LANDING; + break; + default: + return NAV_FSM_EVENT_SWITCH_TO_IDLE; + break; + } + } + + setDesiredPosition(&navGetCurrentActualPositionAndVelocity()->pos, posControl.actualState.yaw, NAV_POS_UPDATE_Z); + + return NAV_FSM_EVENT_NONE; +#endif } static navigationFSMEvent_t navOnEnteringState_NAV_STATE_MIXERAT_ABORT(navigationFSMState_t previousState) { UNUSED(previousState); mixerATUpdateState(MIXERAT_REQUEST_ABORT); +#ifdef USE_AUTO_TRANSITION clearMissionVTOLTransitionState(); +#endif return NAV_FSM_EVENT_SUCCESS; } @@ -5565,8 +5639,10 @@ void navigationInit(void) { /* Initial state */ posControl.navState = NAV_STATE_IDLE; +#ifdef USE_AUTO_TRANSITION navMixerATPendingState = NAV_STATE_IDLE; clearMissionVTOLTransitionState(); +#endif posControl.flags.horizontalPositionDataNew = false; posControl.flags.verticalPositionDataNew = false; diff --git a/src/main/navigation/navigation.h b/src/main/navigation/navigation.h index bff60d96d0e..42f7c7737f0 100644 --- a/src/main/navigation/navigation.h +++ b/src/main/navigation/navigation.h @@ -328,6 +328,7 @@ typedef enum { WP_MISSION_SWITCH, } navMissionRestart_e; +#ifdef USE_AUTO_TRANSITION typedef enum { NAV_MISSION_USER_ACTION_OFF = 0, NAV_MISSION_USER_ACTION_1, @@ -350,6 +351,7 @@ typedef enum { NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_EMERGENCY_LANDING, NAV_VTOL_TRANSITION_FAIL_ACTION_FW_TO_MC_FORCE_SWITCH, } navVtolTransitionFailActionFwToMc_e; +#endif typedef enum { RTH_TRACKBACK_OFF, @@ -436,11 +438,13 @@ typedef struct navConfig_s { uint8_t pos_failure_timeout; // Time to wait before switching to emergency landing (0 - disable) uint16_t waypoint_radius; // if we are within this distance to a waypoint then we consider it reached (distance is in cm) uint16_t waypoint_safe_distance; // Waypoint mission sanity check distance +#ifdef USE_AUTO_TRANSITION uint8_t vtol_mission_transition_user_action; // User action slot that requests mission VTOL transition uint16_t vtol_mission_transition_min_altitude; // Minimum altitude [cm] to start mission VTOL transition (0 = disabled) bool vtol_transition_retry_on_airspeed_timeout; // Enables one-shot yaw-scan retry for failed airspeed-gated MC->FW auto-transition uint8_t vtol_transition_fail_action_mc_to_fw; // Action after final MC->FW transition failure uint8_t vtol_transition_fail_action_fw_to_mc; // Action after final FW->MC transition failure +#endif #ifdef USE_MULTI_MISSION uint8_t waypoint_multi_mission_index; // Index of mission to be loaded in multi mission entry #endif diff --git a/src/main/navigation/navigation_private.h b/src/main/navigation/navigation_private.h index 947bc3406ea..aec088cf02a 100644 --- a/src/main/navigation/navigation_private.h +++ b/src/main/navigation/navigation_private.h @@ -183,7 +183,9 @@ typedef enum { NAV_FSM_EVENT_SWITCH_TO_NAV_STATE_RTH_TRACKBACK = NAV_FSM_EVENT_STATE_SPECIFIC_2, NAV_FSM_EVENT_SWITCH_TO_RTH_HEAD_HOME = NAV_FSM_EVENT_STATE_SPECIFIC_3, NAV_FSM_EVENT_SWITCH_TO_RTH_LOITER_ABOVE_HOME = NAV_FSM_EVENT_STATE_SPECIFIC_4, +#ifdef USE_AUTO_TRANSITION NAV_FSM_EVENT_MIXERAT_MISSION_RESUME = NAV_FSM_EVENT_STATE_SPECIFIC_4, +#endif NAV_FSM_EVENT_SWITCH_TO_RTH_LANDING = NAV_FSM_EVENT_STATE_SPECIFIC_5, NAV_FSM_EVENT_COUNT, diff --git a/src/main/programming/logic_condition.c b/src/main/programming/logic_condition.c index 801239f4851..81f6125575d 100644 --- a/src/main/programming/logic_condition.c +++ b/src/main/programming/logic_condition.c @@ -687,6 +687,13 @@ static int logicConditionGetWaypointOperandValue(int operand) { } } +#ifdef USE_AUTO_TRANSITION +static int logicConditionGetAutoTransitionTargetStabilizedOperandValue(flight_dynamics_index_t axis) +{ + return getAutoTransitionTargetStabilizedInput(axis); +} +#endif + static int logicConditionGetFlightOperandValue(int operand) { switch (operand) { @@ -896,6 +903,20 @@ static int logicConditionGetFlightOperandValue(int operand) { return axisPID[PITCH]; break; +#ifdef USE_AUTO_TRANSITION + case LOGIC_CONDITION_OPERAND_FLIGHT_AUTOTRANSITION_TARGET_STABILIZED_ROLL: + return logicConditionGetAutoTransitionTargetStabilizedOperandValue(FD_ROLL); + break; + + case LOGIC_CONDITION_OPERAND_FLIGHT_AUTOTRANSITION_TARGET_STABILIZED_PITCH: + return logicConditionGetAutoTransitionTargetStabilizedOperandValue(FD_PITCH); + break; + + case LOGIC_CONDITION_OPERAND_FLIGHT_AUTOTRANSITION_TARGET_STABILIZED_YAW: + return logicConditionGetAutoTransitionTargetStabilizedOperandValue(FD_YAW); + break; +#endif + case LOGIC_CONDITION_OPERAND_FLIGHT_3D_HOME_DISTANCE: //in m return constrain(calc_length_pythagorean_2D(GPS_distanceToHome, getEstimatedActualPosition(Z) / 100.0f), 0, INT32_MAX); break; diff --git a/src/main/programming/logic_condition.h b/src/main/programming/logic_condition.h index 34155ea082e..d4896025025 100644 --- a/src/main/programming/logic_condition.h +++ b/src/main/programming/logic_condition.h @@ -156,6 +156,11 @@ typedef enum { LOGIC_CONDITION_OPERAND_FLIGHT_HORIZONTAL_WIND_SPEED, // cm/s // 47 LOGIC_CONDITION_OPERAND_FLIGHT_WIND_DIRECTION, // deg // 48 LOGIC_CONDITION_OPERAND_FLIGHT_RELATIVE_WIND_OFFSET, // deg // 49 +#ifdef USE_AUTO_TRANSITION + LOGIC_CONDITION_OPERAND_FLIGHT_AUTOTRANSITION_TARGET_STABILIZED_ROLL, // 50 + LOGIC_CONDITION_OPERAND_FLIGHT_AUTOTRANSITION_TARGET_STABILIZED_PITCH, // 51 + LOGIC_CONDITION_OPERAND_FLIGHT_AUTOTRANSITION_TARGET_STABILIZED_YAW, // 52 +#endif } logicFlightOperands_e; typedef enum { diff --git a/src/main/target/common.h b/src/main/target/common.h index 55ca1653f5a..6b61357069a 100644 --- a/src/main/target/common.h +++ b/src/main/target/common.h @@ -201,8 +201,9 @@ #define USE_TELEMETRY_SBUS2 #endif -//Designed to free space of F722 and F411 MCUs +// Keep larger optional features off 512 KB targets to preserve flash space. #if (MCU_FLASH_SIZE > 512) +#define USE_AUTO_TRANSITION #define USE_TELEMETRY_SIM #define USE_VTX_FFPV #define USE_SERIALRX_SUMD @@ -228,4 +229,3 @@ #define USE_EZ_TUNE #define USE_ADAPTIVE_FILTER - From 8218e511a9d2a4154ba90f37db3f2ce2b7ca12d6 Mon Sep 17 00:00:00 2001 From: Martin Petrov Date: Sun, 7 Jun 2026 15:46:39 +0300 Subject: [PATCH 26/26] Refine VTOL auto-transition preview and scaling docs - improve target fixed-wing servo preview during auto-transition - rename VTOL transition percentage settings to *_min_percent - separate motor ramp timing from transition handoff timing - update CLI-generated and narrative docs to match the behavior --- docs/MixerProfile.md | 41 +++--- docs/Settings.md | 16 +-- docs/VTOL.md | 110 +++++++-------- src/main/fc/config.c | 11 +- src/main/fc/config.h | 6 +- src/main/fc/settings.yaml | 22 +-- src/main/flight/mixer_profile.c | 31 ++-- src/main/flight/pid.c | 242 +++++++++++++++++++++++++++++++- 8 files changed, 362 insertions(+), 117 deletions(-) diff --git a/docs/MixerProfile.md b/docs/MixerProfile.md index 25b05fda5b2..6a6eb44cc2b 100644 --- a/docs/MixerProfile.md +++ b/docs/MixerProfile.md @@ -173,22 +173,22 @@ When `mixer_vtol_transition_dynamic_mixer = ON`, iNAV can smoothly change: Default is OFF to preserve existing behavior. With it ON, you can configure `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` servo rules in the MC mixer profile. -During MC->FW they drive the selected servo outputs from the target FW controller before the hot-switch. +During MC->FW they give those servos a preview of the fixed-wing stabilisation that will take over after the hot-switch. +This preview uses the target fixed-wing PID bank, rates, angle limits, heading-hold limits, and turn-assist gains, but it still follows the current transition stick shaping until the actual profile switch. During FW->MC the same MC mixer rules mark which FW servo outputs should fade down as fixed-wing authority is reduced and motor stabilisation comes back in. These inputs are active only while the smooth autotransition controller is running. If `mixer_vtol_transition_dynamic_mixer = OFF`, they stay at full authority while the controller is active. If `mixer_vtol_transition_dynamic_mixer = ON`, they follow the normal fixed-wing authority scaling. `INPUT_MIXER_TRANSITION` remains available for transition-progress servo movement such as tilt or helper servos. How `mixer_vtol_transition_scale_ramp_time_ms` works: -- MC->FW pusher: - - `> 0`: forward motor power ramps from `0 -> 100%` over this time, even when pitot is working normally. - - `= 0` (default): forward motor power goes to `100%` immediately. +- Motor ramp-in: + - MC->FW: forward motor power ramps from `0 -> 100%` over this time. + - FW->MC: lift motor power ramps from `vtol_transition_lift_min_percent -> 100%` over this time. + - `= 0` (default): those motor-power changes happen immediately. - This timer does not decide when the transition completes. -- Lift motor power, MC stabilisation, and FW control: - - with valid pitot airspeed, they still follow transition progress based on airspeed. - - if pitot stops being usable and this setting is `> 0`, they use this same timer as a backup ramp. - - if pitot stops being usable and this setting is `0`, they fall back to the normal transition timer/progress behavior. -- FW->MC keeps the existing style of smooth handover. +- Lift motor reduction in MC->FW, plus MC/FW control handoff in both directions: + - with valid pitot airspeed, they follow transition progress based on airspeed. + - without trusted pitot, they fall back to the normal transition timer/progress behavior (`mixer_switch_trans_timer`). Example: @@ -197,8 +197,9 @@ Example: Result: - in MC->FW, the forward motor reaches full power in about `1.2s`, -- when pitot is working, lift power and control handover still follow airspeed, -- if pitot stops being usable, the same handover reaches its target in about `1.2s`, +- in FW->MC, lift motor power returns to full in about `1.2s`, +- when pitot is working, control handover still follows airspeed, +- if pitot stops being usable, handover falls back to `mixer_switch_trans_timer`, - transition completion still uses airspeed when pitot is working, - backup completion time is still `5s` if pitot is not usable. @@ -224,9 +225,9 @@ Setting scope: - `vtol_transition_to_fw_min_airspeed_cm_s` - `vtol_transition_to_mc_max_airspeed_cm_s` - `vtol_fw_to_mc_auto_switch_airspeed_cm_s` - - `vtol_transition_lift_end_percent` - - `vtol_transition_mc_authority_end_percent` - - `vtol_transition_fw_authority_start_percent` + - `vtol_transition_lift_min_percent` + - `vtol_transition_mc_authority_min_percent` + - `vtol_transition_fw_authority_min_percent` - `nav_vtol_mission_transition_user_action` - `nav_vtol_mission_transition_min_altitude_cm` @@ -275,9 +276,9 @@ CLI: - `set mixer_switch_trans_timer = 50` - `set mixer_vtol_transition_airspeed_timeout_ms = 6500` - `set mixer_vtol_transition_scale_ramp_time_ms = 1200` -- `set vtol_transition_lift_end_percent = 30` -- `set vtol_transition_mc_authority_end_percent = 20` -- `set vtol_transition_fw_authority_start_percent = 20` +- `set vtol_transition_lift_min_percent = 30` +- `set vtol_transition_mc_authority_min_percent = 20` +- `set vtol_transition_fw_authority_min_percent = 20` - `set nav_vtol_mission_transition_user_action = OFF` Behavior: @@ -294,9 +295,9 @@ CLI: - `set mixer_switch_trans_timer = 50` - `set mixer_vtol_transition_airspeed_timeout_ms = 6500` - `set mixer_vtol_transition_scale_ramp_time_ms = 1200` -- `set vtol_transition_lift_end_percent = 30` -- `set vtol_transition_mc_authority_end_percent = 20` -- `set vtol_transition_fw_authority_start_percent = 20` +- `set vtol_transition_lift_min_percent = 30` +- `set vtol_transition_mc_authority_min_percent = 20` +- `set vtol_transition_fw_authority_min_percent = 20` - `set nav_vtol_mission_transition_user_action = USER1` - `set nav_vtol_mission_transition_min_altitude_cm = 1200` diff --git a/docs/Settings.md b/docs/Settings.md index 90ab68a7527..53a83477010 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -3202,7 +3202,7 @@ If enabled, control_profile_index will follow mixer_profile index. Set to OFF(de ### mixer_switch_trans_timer -Original VTOL transition timer, still used as the backup completion time. If trusted pitot airspeed is not being used, iNAV completes the transition from this timer instead. If `mixer_vtol_transition_scale_ramp_time_ms = 0`, lift motor power, multicopter stabilisation, and fixed-wing control handoff also fall back to this timing. +Original VTOL transition timer, still used as the backup completion time. If trusted pitot airspeed is not being used, iNAV completes the transition from this timer instead. With smooth VTOL transition power changes ON, lift motor power, multicopter stabilisation, and fixed-wing control handoff also fall back to this timing whenever trusted pitot is not usable. | Default | Min | Max | | --- | --- | --- | @@ -3242,7 +3242,7 @@ Turns on smooth VTOL transition power changes. This affects forward motor ramp-u ### mixer_vtol_transition_scale_ramp_time_ms -When smooth VTOL transition power changes are ON, this always controls the MC->FW forward motor ramp. `0` gives full forward-motor power immediately. This timer does not decide when the transition is complete. For lift motor power, multicopter stabilisation, and fixed-wing control handoff, trusted pitot airspeed still controls the change while pitot is usable; this timer is only their backup ramp if pitot becomes unavailable. Available only on targets with more than 512 KB flash. +When smooth VTOL transition power changes are ON, this controls motor ramp-in time only. In MC->FW it ramps the forward motor from idle to full target power. In FW->MC it ramps the lift motors from their configured minimum back to full power. `0` applies those motor-power changes immediately. This timer does not decide when the transition completes and it does not control the multicopter/fixed-wing control handoff. Handoff still follows trusted pitot airspeed when pitot is usable, otherwise `mixer_switch_trans_timer`. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -7086,9 +7086,9 @@ Extra low-speed protection for fixed-wing flight [cm/s]. If airspeed falls to th --- -### vtol_transition_fw_authority_start_percent +### vtol_transition_fw_authority_min_percent -How much fixed-wing control is available at the start of transition, in percent. `100` gives full fixed-wing control immediately. Lower values bring it in and out more gently. With `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` rules configured in the MC mixer profile, this same setting scales their servo authority during MC->FW and scales down the matching FW servo stabilisation during FW->MC. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash. +Lowest fixed-wing stabilisation used during transition, in percent. In MC->FW, fixed-wing stabilisation starts from this value and rises to full strength. In FW->MC, it fades down from full strength to this value. With `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` rules configured in the MC mixer profile, this same setting scales their servo authority during MC->FW and scales down the matching FW servo stabilisation during FW->MC. `100` keeps full fixed-wing stabilisation through the whole transition. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -7096,9 +7096,9 @@ How much fixed-wing control is available at the start of transition, in percent. --- -### vtol_transition_lift_end_percent +### vtol_transition_lift_min_percent -How much lift motor power remains at the end of transition, in percent. `100` keeps full lift power. Lower values reduce lift motor power more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash. +Lowest lift motor power used during transition, in percent. In MC->FW, lift power fades down to this value. In FW->MC, lift power starts from this value and rises back to full power. `100` keeps full lift power through the whole transition. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | @@ -7106,9 +7106,9 @@ How much lift motor power remains at the end of transition, in percent. `100` ke --- -### vtol_transition_mc_authority_end_percent +### vtol_transition_mc_authority_min_percent -How much multicopter stabilisation remains at the end of transition, in percent. `100` keeps full multicopter stabilisation. Lower values reduce it more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash. +Lowest multicopter stabilisation used during transition, in percent. In MC->FW, multicopter stabilisation fades down to this value. In FW->MC, it starts from this value and rises back to full stabilisation. `100` keeps full multicopter stabilisation through the whole transition. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash. | Default | Min | Max | | --- | --- | --- | diff --git a/docs/VTOL.md b/docs/VTOL.md index 9895f1a03d5..df0aab8b91f 100644 --- a/docs/VTOL.md +++ b/docs/VTOL.md @@ -393,23 +393,23 @@ When `mixer_vtol_transition_dynamic_mixer = ON`, iNAV can smoothly change: When `mixer_vtol_transition_dynamic_mixer = OFF`, the older static behavior is preserved. When it is ON, you can configure `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` servo rules in the MC mixer profile. -During MC->FW they drive the selected servo outputs from the target FW controller before the hot-switch. +During MC->FW they give those servos a preview of the fixed-wing stabilisation that will take over after the hot-switch. +This preview uses the target fixed-wing PID bank, rates, angle limits, heading-hold limits, and turn-assist gains, but it still follows the current transition stick shaping until the actual profile switch. During FW->MC the same MC mixer rules mark which FW servo outputs should fade down as fixed-wing authority is reduced and motor stabilisation comes back in. These inputs are active only while the smooth autotransition controller is running. If `mixer_vtol_transition_dynamic_mixer = OFF`, they stay at full authority while the controller is active. If `mixer_vtol_transition_dynamic_mixer = ON`, they follow the normal fixed-wing authority scaling. `INPUT_MIXER_TRANSITION` remains available for transition-progress servo movement such as tilt or helper servos. -`mixer_vtol_transition_scale_ramp_time_ms` always controls the MC->FW forward-motor ramp when this feature is ON. +`mixer_vtol_transition_scale_ramp_time_ms` controls motor ramp-in timing when this feature is ON. It does not decide when the transition completes. How `mixer_vtol_transition_scale_ramp_time_ms` works: -- MC->FW pusher: - - `> 0`: forward motor power ramps from `0 -> 100%` over this time, even when pitot is working normally. - - `= 0` (default): forward motor power goes to `100%` immediately. -- Lift motor power, MC stabilisation, and FW control: +- Motor ramp-in: + - MC->FW: forward motor power ramps from `0 -> 100%` over this time. + - FW->MC: lift motor power ramps from `vtol_transition_lift_min_percent -> 100%` over this time. + - `= 0` (default): those motor-power changes happen immediately. +- Lift motor reduction in MC->FW, plus MC/FW control handoff in both directions: - with valid pitot airspeed, they still follow airspeed-based transition progress. - - if pitot stops being usable and this setting is `> 0`, they use this same timer as a backup ramp. - - if pitot stops being usable and this setting is `0`, they fall back to the normal transition timer/progress behavior. -- FW->MC keeps the existing style of smooth handover. + - if pitot is not usable, they fall back to the normal transition timer/progress behavior (`mixer_switch_trans_timer`). Example: - `mixer_switch_trans_timer = 50` (5s fallback completion timer) @@ -417,8 +417,9 @@ Example: Result: - in MC->FW, the forward motor reaches full power in about `1.2s`, -- when pitot is working, lift power and control handover still follow airspeed, -- if pitot stops being usable, the same handover reaches its target in about `1.2s`, +- in FW->MC, lift motor power returns to full in about `1.2s`, +- when pitot is working, control handover still follows airspeed, +- if pitot is not usable, handover falls back to `mixer_switch_trans_timer`, - transition completion still uses airspeed when pitot is working, - backup completion time is still `5s` if pitot is not usable. @@ -461,9 +462,9 @@ CLI: - `set mixer_switch_trans_timer = 50` - `set mixer_vtol_transition_airspeed_timeout_ms = 6500` - `set mixer_vtol_transition_scale_ramp_time_ms = 1200` -- `set vtol_transition_lift_end_percent = 30` -- `set vtol_transition_mc_authority_end_percent = 20` -- `set vtol_transition_fw_authority_start_percent = 20` +- `set vtol_transition_lift_min_percent = 30` +- `set vtol_transition_mc_authority_min_percent = 20` +- `set vtol_transition_fw_authority_min_percent = 20` - `set nav_vtol_mission_transition_user_action = OFF` What this does: @@ -486,9 +487,9 @@ CLI: - `set mixer_switch_trans_timer = 50` - `set mixer_vtol_transition_airspeed_timeout_ms = 6500` - `set mixer_vtol_transition_scale_ramp_time_ms = 1200` -- `set vtol_transition_lift_end_percent = 30` -- `set vtol_transition_mc_authority_end_percent = 20` -- `set vtol_transition_fw_authority_start_percent = 20` +- `set vtol_transition_lift_min_percent = 30` +- `set vtol_transition_mc_authority_min_percent = 20` +- `set vtol_transition_fw_authority_min_percent = 20` - `set nav_vtol_mission_transition_user_action = USER1` - `set nav_vtol_mission_transition_min_altitude_cm = 1200` @@ -504,37 +505,37 @@ What this does: These three settings are active only when `mixer_vtol_transition_dynamic_mixer = ON`. -1. `vtol_transition_lift_end_percent` -- Sets how much lift motor power remains at the end of transition. -- MC -> FW: lift power goes from `100%` at start to `lift_end_percent` at the end. -- FW -> MC: lift power goes from `lift_end_percent` at start to `100%` at the end. +1. `vtol_transition_lift_min_percent` +- Sets the lowest lift motor power used during transition. +- MC -> FW: lift power goes from `100%` at start down to `lift_min_percent`. +- FW -> MC: lift power goes from `lift_min_percent` at start up to `100%`. -Example (`vtol_transition_lift_end_percent = 20`): +Example (`vtol_transition_lift_min_percent = 20`): - MC -> FW at 50% progress: lift power is about `60%`. - FW -> MC at 50% progress: lift power is about `60%`. -2. `vtol_transition_mc_authority_end_percent` -- Sets how much multicopter stabilisation remains at the end of transition. -- MC -> FW: MC stabilisation goes from `100%` at start to `mc_authority_end_percent` at the end. -- FW -> MC: MC stabilisation goes from `mc_authority_end_percent` at start to `100%` at the end. +2. `vtol_transition_mc_authority_min_percent` +- Sets the lowest multicopter stabilisation used during transition. +- MC -> FW: MC stabilisation goes from `100%` at start down to `mc_authority_min_percent`. +- FW -> MC: MC stabilisation goes from `mc_authority_min_percent` at start up to `100%`. -Example (`vtol_transition_mc_authority_end_percent = 30`): +Example (`vtol_transition_mc_authority_min_percent = 30`): - MC -> FW at 50% progress: MC stabilisation is about `65%`. - FW -> MC at 50% progress: MC stabilisation is about `65%`. -3. `vtol_transition_fw_authority_start_percent` -- Sets how much fixed-wing control is already available at the start of transition. -- MC -> FW: fixed-wing control goes from `fw_authority_start_percent` at start to `100%` at the end. -- FW -> MC: fixed-wing control goes from `100%` at start to `fw_authority_start_percent` at the end. +3. `vtol_transition_fw_authority_min_percent` +- Sets the lowest fixed-wing control used during transition. +- MC -> FW: fixed-wing control goes from `fw_authority_min_percent` at start up to `100%`. +- FW -> MC: fixed-wing control goes from `100%` at start down to `fw_authority_min_percent`. - During MC -> FW, this same setting also scales `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` servo rules configured in the MC mixer profile. - During FW -> MC, the same setting scales down the matching FW servo stabilisation on the outputs marked by those MC mixer rules. -Example (`vtol_transition_fw_authority_start_percent = 25`): +Example (`vtol_transition_fw_authority_min_percent = 25`): - MC -> FW at 50% progress: fixed-wing control is about `62.5%`. - FW -> MC at 50% progress: fixed-wing control is about `62.5%`. -Backward-compatible note: -- `vtol_transition_fw_authority_start_percent = 100` keeps the older fixed-wing control behavior. +Practical note: +- `vtol_transition_fw_authority_min_percent = 100` keeps full fixed-wing control through the whole transition. - Lower values bring fixed-wing control in and out more gently. ## Setting Scope (Important) @@ -560,9 +561,9 @@ These are shared system-wide and are not profile-specific: - `vtol_transition_to_fw_min_airspeed_cm_s` - `vtol_transition_to_mc_max_airspeed_cm_s` - `vtol_fw_to_mc_auto_switch_airspeed_cm_s` -- `vtol_transition_lift_end_percent` -- `vtol_transition_mc_authority_end_percent` -- `vtol_transition_fw_authority_start_percent` +- `vtol_transition_lift_min_percent` +- `vtol_transition_mc_authority_min_percent` +- `vtol_transition_fw_authority_min_percent` - `nav_vtol_mission_transition_user_action` - `nav_vtol_mission_transition_min_altitude_cm` @@ -592,16 +593,16 @@ Use these commands in CLI (`set ...`, then `save`): - How long iNAV waits for required pitot airspeed before aborting. - `set mixer_vtol_transition_scale_ramp_time_ms = ` - - Ramp-up time for the forward motor, and backup ramp time for the other smooth transition power changes. + - Ramp-in time for the MC->FW forward motor and the FW->MC lift motors. -- `set vtol_transition_lift_end_percent = <0..100>` - - How much lift motor power remains at the end of transition. +- `set vtol_transition_lift_min_percent = <0..100>` + - Lowest lift motor power used during transition. -- `set vtol_transition_mc_authority_end_percent = <0..100>` - - How much multicopter stabilisation remains at the end of transition. +- `set vtol_transition_mc_authority_min_percent = <0..100>` + - Lowest multicopter stabilisation used during transition. -- `set vtol_transition_fw_authority_start_percent = <0..100>` - - How much fixed-wing control is already available at the start of transition. +- `set vtol_transition_fw_authority_min_percent = <0..100>` + - Lowest fixed-wing control used during transition. - `set nav_vtol_mission_transition_user_action = OFF|USER1|USER2|USER3|USER4` - Selects which waypoint USER flag tells iNAV to use MC or FW at each waypoint. @@ -644,23 +645,22 @@ Smooth transition power changes (`mixer_vtol_transition_dynamic_mixer = ON`) use - MC -> FW: - forward motor power ramps `0 -> 1` - - lift motor power ramps `1 -> vtol_transition_lift_end_percent` - - MC stabilisation ramps `1 -> vtol_transition_mc_authority_end_percent` - - FW control ramps `vtol_transition_fw_authority_start_percent -> 1` + - lift motor power ramps `1 -> vtol_transition_lift_min_percent` + - MC stabilisation ramps `1 -> vtol_transition_mc_authority_min_percent` + - FW control ramps `vtol_transition_fw_authority_min_percent -> 1` - FW -> MC: - forward motor power ramps `1 -> 0` - - lift motor power ramps `vtol_transition_lift_end_percent -> 1` - - MC stabilisation ramps `vtol_transition_mc_authority_end_percent -> 1` - - FW control ramps `1 -> vtol_transition_fw_authority_start_percent` + - lift motor power ramps `vtol_transition_lift_min_percent -> 1` + - MC stabilisation ramps `vtol_transition_mc_authority_min_percent -> 1` + - FW control ramps `1 -> vtol_transition_fw_authority_min_percent` -MC->FW uses separate forward-motor ramp-up and control handover behavior. +Motor ramp-in and control handover are separate. For MC->FW, forward motor power uses `mixer_vtol_transition_scale_ramp_time_ms`; if this is `0`, the motor goes to full power immediately. +For FW->MC, lift motor power uses the same timer to rise from `vtol_transition_lift_min_percent` back to full power; if this is `0`, that lift power returns immediately. This timer does not decide when the transition completes. -Lift motor power, MC stabilisation, and FW control still prefer pitot-based transition progress whenever pitot is working. -If pitot stops being usable and `mixer_vtol_transition_scale_ramp_time_ms > 0`, those other changes fall back to the same timer-based ramp. -If pitot is not usable and `mixer_vtol_transition_scale_ramp_time_ms = 0`, they fall back to the normal transition timer/progress behavior (`mixer_switch_trans_timer`). -FW->MC keeps the existing style of smooth handover. +Lift motor reduction in MC->FW, plus MC stabilisation and FW control handoff in both directions, still prefer pitot-based transition progress whenever pitot is working. +If pitot is not usable, those handoff changes fall back to the normal transition timer/progress behavior (`mixer_switch_trans_timer`). For transition/pusher motors (`-2.0 < throttle < -1.0`), output is interpolated from idle to target: diff --git a/src/main/fc/config.c b/src/main/fc/config.c index f9254e41aa1..8cdca161e34 100755 --- a/src/main/fc/config.c +++ b/src/main/fc/config.c @@ -103,7 +103,12 @@ PG_RESET_TEMPLATE(featureConfig_t, featureConfig, .enabledFeatures = DEFAULT_FEATURES | COMMON_DEFAULT_FEATURES ); +// Keep PG version split because USE_AUTO_TRANSITION changes the stored layout only on >512 KB targets. +#ifdef USE_AUTO_TRANSITION PG_REGISTER_WITH_RESET_TEMPLATE(systemConfig_t, systemConfig, PG_SYSTEM_CONFIG, 9); +#else +PG_REGISTER_WITH_RESET_TEMPLATE(systemConfig_t, systemConfig, PG_SYSTEM_CONFIG, 7); +#endif PG_RESET_TEMPLATE(systemConfig_t, systemConfig, .current_profile_index = 0, @@ -121,9 +126,9 @@ PG_RESET_TEMPLATE(systemConfig_t, systemConfig, .vtolTransitionToFwMinAirspeed = SETTING_VTOL_TRANSITION_TO_FW_MIN_AIRSPEED_CM_S_DEFAULT, .vtolTransitionToMcMaxAirspeed = SETTING_VTOL_TRANSITION_TO_MC_MAX_AIRSPEED_CM_S_DEFAULT, .vtolFwToMcAutoSwitchAirspeed = SETTING_VTOL_FW_TO_MC_AUTO_SWITCH_AIRSPEED_CM_S_DEFAULT, - .vtolTransitionLiftEndPercent = SETTING_VTOL_TRANSITION_LIFT_END_PERCENT_DEFAULT, - .vtolTransitionMcAuthorityEndPercent = SETTING_VTOL_TRANSITION_MC_AUTHORITY_END_PERCENT_DEFAULT, - .vtolTransitionFwAuthorityStartPercent = SETTING_VTOL_TRANSITION_FW_AUTHORITY_START_PERCENT_DEFAULT, + .vtolTransitionLiftMinPercent = SETTING_VTOL_TRANSITION_LIFT_MIN_PERCENT_DEFAULT, + .vtolTransitionMcAuthorityMinPercent = SETTING_VTOL_TRANSITION_MC_AUTHORITY_MIN_PERCENT_DEFAULT, + .vtolTransitionFwAuthorityMinPercent = SETTING_VTOL_TRANSITION_FW_AUTHORITY_MIN_PERCENT_DEFAULT, #endif .craftName = SETTING_NAME_DEFAULT, .pilotName = SETTING_NAME_DEFAULT diff --git a/src/main/fc/config.h b/src/main/fc/config.h index fd433e9e657..b22e292b032 100644 --- a/src/main/fc/config.h +++ b/src/main/fc/config.h @@ -82,9 +82,9 @@ typedef struct systemConfig_s { uint16_t vtolTransitionToFwMinAirspeed; uint16_t vtolTransitionToMcMaxAirspeed; uint16_t vtolFwToMcAutoSwitchAirspeed; - uint8_t vtolTransitionLiftEndPercent; - uint8_t vtolTransitionMcAuthorityEndPercent; - uint8_t vtolTransitionFwAuthorityStartPercent; + uint8_t vtolTransitionLiftMinPercent; + uint8_t vtolTransitionMcAuthorityMinPercent; + uint8_t vtolTransitionFwAuthorityMinPercent; #endif char craftName[MAX_NAME_LENGTH + 1]; char pilotName[MAX_NAME_LENGTH + 1]; diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index a65788dd1f0..16cc5bcfb17 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -1281,7 +1281,7 @@ groups: field: mixer_config.automated_switch type: bool - name: mixer_switch_trans_timer - description: "Original VTOL transition timer, still used as the backup completion time. If trusted pitot airspeed is not being used, iNAV completes the transition from this timer instead. If `mixer_vtol_transition_scale_ramp_time_ms = 0`, lift motor power, multicopter stabilisation, and fixed-wing control handoff also fall back to this timing." + description: "Original VTOL transition timer, still used as the backup completion time. If trusted pitot airspeed is not being used, iNAV completes the transition from this timer instead. With smooth VTOL transition power changes ON, lift motor power, multicopter stabilisation, and fixed-wing control handoff also fall back to this timing whenever trusted pitot is not usable." default_value: 0 field: mixer_config.switchTransitionTimer min: 0 @@ -1306,7 +1306,7 @@ groups: min: 0 max: 60000 - name: mixer_vtol_transition_scale_ramp_time_ms - description: "When smooth VTOL transition power changes are ON, this always controls the MC->FW forward motor ramp. `0` gives full forward-motor power immediately. This timer does not decide when the transition is complete. For lift motor power, multicopter stabilisation, and fixed-wing control handoff, trusted pitot airspeed still controls the change while pitot is usable; this timer is only their backup ramp if pitot becomes unavailable. Available only on targets with more than 512 KB flash." + description: "When smooth VTOL transition power changes are ON, this controls motor ramp-in time only. In MC->FW it ramps the forward motor from idle to full target power. In FW->MC it ramps the lift motors from their configured minimum back to full power. `0` applies those motor-power changes immediately. This timer does not decide when the transition completes and it does not control the multicopter/fixed-wing control handoff. Handoff still follows trusted pitot airspeed when pitot is usable, otherwise `mixer_switch_trans_timer`. Available only on targets with more than 512 KB flash." condition: USE_AUTO_TRANSITION default_value: 0 field: mixer_config.vtolTransitionScaleRampTimeMs @@ -4044,25 +4044,25 @@ groups: field: vtolFwToMcAutoSwitchAirspeed min: 0 max: 20000 - - name: vtol_transition_lift_end_percent - description: "How much lift motor power remains at the end of transition, in percent. `100` keeps full lift power. Lower values reduce lift motor power more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash." + - name: vtol_transition_lift_min_percent + description: "Lowest lift motor power used during transition, in percent. In MC->FW, lift power fades down to this value. In FW->MC, lift power starts from this value and rises back to full power. `100` keeps full lift power through the whole transition. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash." condition: USE_AUTO_TRANSITION default_value: 100 - field: vtolTransitionLiftEndPercent + field: vtolTransitionLiftMinPercent min: 0 max: 100 - - name: vtol_transition_mc_authority_end_percent - description: "How much multicopter stabilisation remains at the end of transition, in percent. `100` keeps full multicopter stabilisation. Lower values reduce it more. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash." + - name: vtol_transition_mc_authority_min_percent + description: "Lowest multicopter stabilisation used during transition, in percent. In MC->FW, multicopter stabilisation fades down to this value. In FW->MC, it starts from this value and rises back to full stabilisation. `100` keeps full multicopter stabilisation through the whole transition. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash." condition: USE_AUTO_TRANSITION default_value: 100 - field: vtolTransitionMcAuthorityEndPercent + field: vtolTransitionMcAuthorityMinPercent min: 0 max: 100 - - name: vtol_transition_fw_authority_start_percent - description: "How much fixed-wing control is available at the start of transition, in percent. `100` gives full fixed-wing control immediately. Lower values bring it in and out more gently. With `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` rules configured in the MC mixer profile, this same setting scales their servo authority during MC->FW and scales down the matching FW servo stabilisation during FW->MC. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash." + - name: vtol_transition_fw_authority_min_percent + description: "Lowest fixed-wing stabilisation used during transition, in percent. In MC->FW, fixed-wing stabilisation starts from this value and rises to full strength. In FW->MC, it fades down from full strength to this value. With `INPUT_AUTOTRANSITION_TARGET_STABILIZED_*` rules configured in the MC mixer profile, this same setting scales their servo authority during MC->FW and scales down the matching FW servo stabilisation during FW->MC. `100` keeps full fixed-wing stabilisation through the whole transition. Used only when `mixer_vtol_transition_dynamic_mixer` is ON. Available only on targets with more than 512 KB flash." condition: USE_AUTO_TRANSITION default_value: 100 - field: vtolTransitionFwAuthorityStartPercent + field: vtolTransitionFwAuthorityMinPercent min: 0 max: 100 - name: name diff --git a/src/main/flight/mixer_profile.c b/src/main/flight/mixer_profile.c index 67f2e025b56..8475f1ba29e 100644 --- a/src/main/flight/mixer_profile.c +++ b/src/main/flight/mixer_profile.c @@ -47,7 +47,12 @@ static bool manualTransitionSessionLatched; static bool manualFwToMcProtectionLatched; #endif +// Keep PG version split because USE_AUTO_TRANSITION changes the stored mixer profile layout only on >512 KB targets. +#ifdef USE_AUTO_TRANSITION PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 4); +#else +PG_REGISTER_ARRAY_WITH_RESET_FN(mixerProfile_t, MAX_MIXER_PROFILE_COUNT, mixerProfiles, PG_MIXER_PROFILE, 1); +#endif void pgResetFn_mixerProfiles(mixerProfile_t *instance) { @@ -197,7 +202,7 @@ static float blendScale(float from, float to, float progress) return from + (to - from) * constrainf(progress, 0.0f, 1.0f); } -static float getPusherRampProgress(void) +static float getMotorRampProgress(void) { if (!currentMixerConfig.vtolTransitionDynamicMixer) { return 1.0f; @@ -222,16 +227,8 @@ static float getHandoffScalingProgress(void) return mixerProfileAT.handoffScalingProgress; } - if (currentMixerConfig.vtolTransitionScaleRampTimeMs > 0) { - const float rampProgress = getPusherRampProgress(); - - // Preserve already-applied handoff scaling if pitot drops out mid-transition. - mixerProfileAT.handoffScalingProgress = MAX(mixerProfileAT.handoffScalingProgress, rampProgress); - return mixerProfileAT.handoffScalingProgress; - } - - // Last-resort compatibility path: with no trusted pitot and no dedicated - // scaling ramp configured, reuse transition progress/timer behavior. + // Preserve already-applied handoff scaling if pitot drops out mid-transition. + // Without trusted pitot, handoff returns to the normal transition timer/progress behavior. mixerProfileAT.handoffScalingProgress = MAX(mixerProfileAT.handoffScalingProgress, constrainf(mixerProfileAT.progress, 0.0f, 1.0f)); return mixerProfileAT.handoffScalingProgress; } @@ -317,21 +314,23 @@ static void updateTransitionScales(void) return; } - const float liftFloor = constrainf(systemConfig()->vtolTransitionLiftEndPercent / 100.0f, 0.0f, 1.0f); - const float mcFloor = constrainf(systemConfig()->vtolTransitionMcAuthorityEndPercent / 100.0f, 0.0f, 1.0f); - const float fwFloor = constrainf(systemConfig()->vtolTransitionFwAuthorityStartPercent / 100.0f, 0.0f, 1.0f); + const float liftFloor = constrainf(systemConfig()->vtolTransitionLiftMinPercent / 100.0f, 0.0f, 1.0f); + const float mcFloor = constrainf(systemConfig()->vtolTransitionMcAuthorityMinPercent / 100.0f, 0.0f, 1.0f); + const float fwFloor = constrainf(systemConfig()->vtolTransitionFwAuthorityMinPercent / 100.0f, 0.0f, 1.0f); const float handoffProgress = getHandoffScalingProgress(); if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_FW) { - const float pusherProgress = getPusherRampProgress(); + const float pusherProgress = getMotorRampProgress(); mixerProfileAT.pusherScale = blendScale(0.0f, 1.0f, pusherProgress); mixerProfileAT.liftScale = blendScale(1.0f, liftFloor, handoffProgress); mixerProfileAT.mcAuthorityScale = blendScale(1.0f, mcFloor, handoffProgress); mixerProfileAT.fwAuthorityScale = blendScale(fwFloor, 1.0f, handoffProgress); } else if (mixerProfileAT.direction == MIXERAT_DIRECTION_TO_MC) { + const float liftRampProgress = getMotorRampProgress(); + mixerProfileAT.pusherScale = blendScale(1.0f, 0.0f, handoffProgress); - mixerProfileAT.liftScale = blendScale(liftFloor, 1.0f, handoffProgress); + mixerProfileAT.liftScale = blendScale(liftFloor, 1.0f, liftRampProgress); mixerProfileAT.mcAuthorityScale = blendScale(mcFloor, 1.0f, handoffProgress); mixerProfileAT.fwAuthorityScale = blendScale(1.0f, fwFloor, handoffProgress); } diff --git a/src/main/flight/pid.c b/src/main/flight/pid.c index f36f6912653..a334069a871 100644 --- a/src/main/flight/pid.c +++ b/src/main/flight/pid.c @@ -129,6 +129,8 @@ typedef struct { float rateTarget; float errorGyroIf; float errorGyroIfLimit; + pt1Filter_t angleFilterState; + rateLimitFilter_t axisAccelFilter; pt1Filter_t ptermLpfState; filter_t dtermLpfState; float previousRateTarget; @@ -174,6 +176,7 @@ static EXTENDED_FASTRAM autoTransitionTargetPidState_t autoTransitionTargetPidSt static EXTENDED_FASTRAM pt1Filter_t windupLpf[XYZ_AXIS_COUNT]; static EXTENDED_FASTRAM uint8_t itermRelax; #ifdef USE_AUTO_TRANSITION +static EXTENDED_FASTRAM pt1Filter_t autoTransitionTargetHeadingHoldRateFilter; static EXTENDED_FASTRAM pt1Filter_t autoTransitionTargetTpaFilter; static EXTENDED_FASTRAM filterApplyFnPtr autoTransitionTargetDTermLpfFilterApplyFn; static EXTENDED_FASTRAM uint8_t autoTransitionTargetYawLpfHz; @@ -220,6 +223,9 @@ static EXTENDED_FASTRAM float fixedWingLevelTrim; static EXTENDED_FASTRAM pidController_t fixedWingLevelTrimController; static void applyItermLimiting(pidState_t *pidState); +static float pidRcCommandToAngle(int16_t stick, int16_t maxInclination); +float pidRcCommandToRate(int16_t stick, uint8_t rate); +int16_t angleFreefloatDeadband(int16_t deadband, flight_dynamics_index_t axis); PG_REGISTER_PROFILE_WITH_RESET_TEMPLATE(pidProfile_t, pidProfile, PG_PID_PROFILE, 11); @@ -465,6 +471,7 @@ static void resetAutoTransitionTargetPidState(void) { memset(autoTransitionTargetPidState, 0, sizeof(autoTransitionTargetPidState)); memset(autoTransitionTargetAxisPID, 0, sizeof(autoTransitionTargetAxisPID)); + memset(&autoTransitionTargetHeadingHoldRateFilter, 0, sizeof(autoTransitionTargetHeadingHoldRateFilter)); memset(&autoTransitionTargetTpaFilter, 0, sizeof(autoTransitionTargetTpaFilter)); autoTransitionTargetControlProfileIndex = -1; autoTransitionTargetDTermLpfFilterApplyFn = (filterApplyFnPtr)nullFilterApply; @@ -643,6 +650,239 @@ static void updateAutoTransitionTargetPIDCoefficients(const controlConfig_t *con } } +static float calcAutoTransitionTargetHorizonRateMagnitude(const pidProfile_t *targetPidProfile) +{ + const int32_t stickPosAil = ABS(getRcStickDeflection(FD_ROLL)); + const int32_t stickPosEle = ABS(getRcStickDeflection(FD_PITCH)); + const float mostDeflectedStickPos = constrain(MAX(stickPosAil, stickPosEle), 0, 500) / 500.0f; + const float modeTransitionStickPos = constrain(targetPidProfile->bank_fw.pid[PID_LEVEL].D, 0, 100) / 100.0f; + + if (modeTransitionStickPos <= 0.0f) { + return 1.0f; + } + + if (mostDeflectedStickPos <= modeTransitionStickPos) { + return mostDeflectedStickPos / modeTransitionStickPos; + } + + return 1.0f; +} + +static uint8_t getAutoTransitionTargetHeadingHoldState(const pidProfile_t *targetPidProfile) +{ + const float headingHoldCosLimit = + cos_approx(DECIDEGREES_TO_RADIANS(targetPidProfile->max_angle_inclination[FD_ROLL])) * + cos_approx(DECIDEGREES_TO_RADIANS(targetPidProfile->max_angle_inclination[FD_PITCH])); + + if (calculateCosTiltAngle() < headingHoldCosLimit) { + return HEADING_HOLD_DISABLED; + } + + const int navHeadingState = navigationGetHeadingControlState(); + if (navHeadingState != NAV_HEADING_CONTROL_NONE) { + if (navHeadingState == NAV_HEADING_CONTROL_AUTO) { + return HEADING_HOLD_ENABLED; + } + } else if (ABS(rcCommand[YAW]) == 0 && FLIGHT_MODE(HEADING_MODE)) { + return HEADING_HOLD_ENABLED; + } + + return HEADING_HOLD_UPDATE_HEADING; +} + +static float pidAutoTransitionTargetHeadingHold(const pidProfile_t *targetPidProfile, float dT) +{ + int16_t error = DECIDEGREES_TO_DEGREES(attitude.values.yaw) - headingHoldTarget; + + if (error > 180) { + error -= 360; + } else if (error < -180) { + error += 360; + } + + float headingHoldRate = error * targetPidProfile->bank_fw.pid[PID_HEADING].P / 30.0f; + headingHoldRate = constrainf(headingHoldRate, -targetPidProfile->heading_hold_rate_limit, targetPidProfile->heading_hold_rate_limit); + headingHoldRate = pt1FilterApply4(&autoTransitionTargetHeadingHoldRateFilter, headingHoldRate, HEADING_HOLD_ERROR_LPF_FREQ, dT); + + return headingHoldRate; +} + +static float computeAutoTransitionTargetPidLevelTarget(const pidProfile_t *targetPidProfile, flight_dynamics_index_t axis) +{ + float angleTarget; + +#ifdef USE_PROGRAMMING_FRAMEWORK + angleTarget = pidRcCommandToAngle(getRcCommandOverride(rcCommand, axis), targetPidProfile->max_angle_inclination[axis]); +#else + angleTarget = pidRcCommandToAngle(rcCommand[axis], targetPidProfile->max_angle_inclination[axis]); +#endif + + if (axis == FD_PITCH) { +#ifdef USE_FW_AUTOLAND + if (FLIGHT_MODE(ANGLE_MODE) && !navigationIsControllingThrottle() && !FLIGHT_MODE(NAV_FW_AUTOLAND)) { +#else + if (FLIGHT_MODE(ANGLE_MODE) && !navigationIsControllingThrottle()) { +#endif + angleTarget += scaleRange( + MAX(0, currentBatteryProfile->nav.fw.cruise_throttle - rcCommand[THROTTLE]), + 0, + currentBatteryProfile->nav.fw.cruise_throttle - PWM_RANGE_MIN, + 0, + navConfig()->fw.minThrottleDownPitchAngle + ); + } + + angleTarget -= DEGREES_TO_DECIDEGREES(targetPidProfile->fixedWingLevelTrim); + } + + return angleTarget; +} + +static void pidAutoTransitionTargetLevel( + const float angleTarget, + autoTransitionTargetPidState_t *pidState, + const controlConfig_t *controlProfile, + const pidProfile_t *targetPidProfile, + flight_dynamics_index_t axis, + float horizonRateMagnitude, + float dT) +{ + float angleErrorDeg = DECIDEGREES_TO_DEGREES(angleTarget - attitude.raw[axis]); + + if (FLIGHT_MODE(SOARING_MODE) && axis == FD_PITCH && calculateRollPitchCenterStatus() == CENTERED) { + angleErrorDeg = DECIDEGREES_TO_DEGREES((float)angleFreefloatDeadband(DEGREES_TO_DECIDEGREES(navConfig()->fw.soaring_pitch_deadband), FD_PITCH)); + if (!angleErrorDeg) { + pidState->errorGyroIf = 0.0f; + pidState->errorGyroIfLimit = 0.0f; + } + } + + float angleRateTarget = constrainf( + angleErrorDeg * (targetPidProfile->bank_fw.pid[PID_LEVEL].P * FP_PID_LEVEL_P_MULTIPLIER), + -controlProfile->stabilized.rates[axis] * 10.0f, + controlProfile->stabilized.rates[axis] * 10.0f + ); + + if (targetPidProfile->bank_fw.pid[PID_LEVEL].I) { + angleRateTarget = pt1FilterApply4(&pidState->angleFilterState, angleRateTarget, targetPidProfile->bank_fw.pid[PID_LEVEL].I, dT); + } + + if (FLIGHT_MODE(HORIZON_MODE)) { + pidState->rateTarget = (1.0f - horizonRateMagnitude) * angleRateTarget + horizonRateMagnitude * pidState->rateTarget; + } else { + pidState->rateTarget = angleRateTarget; + } +} + +static void pidApplyAutoTransitionTargetSetpointRateLimiting( + autoTransitionTargetPidState_t *pidState, + const pidProfile_t *targetPidProfile, + flight_dynamics_index_t axis, + float dT) +{ + const uint32_t axisAccelLimit = (axis == FD_YAW) ? targetPidProfile->axisAccelerationLimitYaw : targetPidProfile->axisAccelerationLimitRollPitch; + + if (axisAccelLimit > AXIS_ACCEL_MIN_LIMIT) { + pidState->rateTarget = rateLimitFilterApply4(&pidState->axisAccelFilter, pidState->rateTarget, (float)axisAccelLimit, dT); + } +} + +static void pidAutoTransitionTargetTurnAssistant( + autoTransitionTargetPidState_t *pidState, + const controlConfig_t *controlProfile, + const pidProfile_t *targetPidProfile, + float bankAngleTarget, + float pitchAngleTarget) +{ + fpVector3_t targetRates; + targetRates.x = 0.0f; + targetRates.y = 0.0f; + + if (calculateCosTiltAngle() < 0.173648f) { + return; + } + +#if defined(USE_PITOT) + float airspeedForCoordinatedTurn = sensors(SENSOR_PITOT) && pitotIsHealthy() ? getAirspeedEstimate() : targetPidProfile->fixedWingReferenceAirspeed; +#else + float airspeedForCoordinatedTurn = targetPidProfile->fixedWingReferenceAirspeed; +#endif + + airspeedForCoordinatedTurn = constrainf(airspeedForCoordinatedTurn, 300.0f, 6000.0f); + bankAngleTarget = constrainf(bankAngleTarget, -DEGREES_TO_RADIANS(60), DEGREES_TO_RADIANS(60)); + + const float turnRatePitchAdjustmentFactor = cos_approx(fabsf(pitchAngleTarget)); + const float coordinatedTurnRateEarthFrame = GRAVITY_CMSS * tan_approx(-bankAngleTarget) / airspeedForCoordinatedTurn * turnRatePitchAdjustmentFactor; + + targetRates.z = RADIANS_TO_DEGREES(coordinatedTurnRateEarthFrame); + + imuTransformVectorEarthToBody(&targetRates); + + pidState[ROLL].rateTarget = constrainf( + pidState[ROLL].rateTarget + targetRates.x, + -controlProfile->stabilized.rates[ROLL] * 10.0f, + controlProfile->stabilized.rates[ROLL] * 10.0f + ); + pidState[PITCH].rateTarget = constrainf( + pidState[PITCH].rateTarget + targetRates.y * targetPidProfile->fixedWingCoordinatedPitchGain, + -controlProfile->stabilized.rates[PITCH] * 10.0f, + controlProfile->stabilized.rates[PITCH] * 10.0f + ); + pidState[YAW].rateTarget = constrainf( + pidState[YAW].rateTarget + targetRates.z * targetPidProfile->fixedWingCoordinatedYawGain, + -controlProfile->stabilized.rates[YAW] * 10.0f, + controlProfile->stabilized.rates[YAW] * 10.0f + ); +} + +static void updateAutoTransitionTargetRateTargets(const controlConfig_t *controlProfile, const pidProfile_t *targetPidProfile, float dT) +{ + const uint8_t headingHoldState = getAutoTransitionTargetHeadingHoldState(targetPidProfile); + + for (uint8_t axis = FD_ROLL; axis <= FD_YAW; axis++) { + float rateTarget; + + if (axis == FD_YAW && headingHoldState == HEADING_HOLD_ENABLED) { + rateTarget = pidAutoTransitionTargetHeadingHold(targetPidProfile, dT); + } else { +#ifdef USE_PROGRAMMING_FRAMEWORK + rateTarget = pidRcCommandToRate(getRcCommandOverride(rcCommand, axis), controlProfile->stabilized.rates[axis]); +#else + rateTarget = pidRcCommandToRate(rcCommand[axis], controlProfile->stabilized.rates[axis]); +#endif + } + + autoTransitionTargetPidState[axis].rateTarget = constrainf(rateTarget, -GYRO_SATURATION_LIMIT, +GYRO_SATURATION_LIMIT); + } + + const float horizonRateMagnitude = FLIGHT_MODE(HORIZON_MODE) ? calcAutoTransitionTargetHorizonRateMagnitude(targetPidProfile) : 0.0f; + + for (uint8_t axis = FD_ROLL; axis <= FD_PITCH; axis++) { + if (FLIGHT_MODE(ANGLE_MODE) || FLIGHT_MODE(HORIZON_MODE) || FLIGHT_MODE(ANGLEHOLD_MODE) || isFlightAxisAngleOverrideActive(axis)) { + float angleTarget = getFlightAxisAngleOverride(axis, computeAutoTransitionTargetPidLevelTarget(targetPidProfile, axis)); + + if (STATE(TAILSITTER) && isMixerTransitionMixing && axis == FD_PITCH) { + angleTarget += DEGREES_TO_DECIDEGREES(45); + } + + // Preview the target fixed-wing angle controller using the current transition stick command. + // Airplane angle-hold target memory is not duplicated here, so this remains a close preview + // rather than a bit-for-bit copy of the post-switch controller state. + pidAutoTransitionTargetLevel(angleTarget, &autoTransitionTargetPidState[axis], controlProfile, targetPidProfile, axis, horizonRateMagnitude, dT); + } + } + + if (FLIGHT_MODE(TURN_ASSISTANT) && (FLIGHT_MODE(ANGLE_MODE) || FLIGHT_MODE(HORIZON_MODE))) { + const float bankAngleTarget = DECIDEGREES_TO_RADIANS(pidRcCommandToAngle(rcCommand[FD_ROLL], targetPidProfile->max_angle_inclination[FD_ROLL])); + const float pitchAngleTarget = DECIDEGREES_TO_RADIANS(pidRcCommandToAngle(rcCommand[FD_PITCH], targetPidProfile->max_angle_inclination[FD_PITCH])); + pidAutoTransitionTargetTurnAssistant(autoTransitionTargetPidState, controlProfile, targetPidProfile, bankAngleTarget, pitchAngleTarget); + } + + for (uint8_t axis = FD_ROLL; axis <= FD_YAW; axis++) { + pidApplyAutoTransitionTargetSetpointRateLimiting(&autoTransitionTargetPidState[axis], targetPidProfile, axis, dT); + } +} + static float autoTransitionTargetPTermProcess(autoTransitionTargetPidState_t *pidState, flight_dynamics_index_t axis, float rateError, float dT) { const float newPTerm = rateError * pidState->kP; @@ -782,12 +1022,12 @@ static void updateAutoTransitionTargetAxisPID(float dT) } updateAutoTransitionTargetPIDCoefficients(controlProfile, targetPidProfile); + updateAutoTransitionTargetRateTargets(controlProfile, targetPidProfile, dT); const float dT_inv = 1.0f / dT; for (uint8_t axis = FD_ROLL; axis <= FD_YAW; axis++) { autoTransitionTargetPidState[axis].gyroRate = gyro.gyroADCf[axis]; - autoTransitionTargetPidState[axis].rateTarget = pidState[axis].rateTarget; #ifdef USE_SMITH_PREDICTOR autoTransitionTargetPidState[axis].gyroRate = applySmithPredictor(axis, &autoTransitionTargetPidState[axis].smithPredictor, autoTransitionTargetPidState[axis].gyroRate);