diff --git a/conf/airframes/ENAC/hybrid/ranger_1.xml b/conf/airframes/ENAC/hybrid/ranger_1.xml
new file mode 100644
index 0000000000..61f724f340
--- /dev/null
+++ b/conf/airframes/ENAC/hybrid/ranger_1.xml
@@ -0,0 +1,356 @@
+
+
+
+
+
+ HEEWING Ranger T1
+ Hybrid Quad/Plane with tilt motors
+ Tawaki v1.0 Chibios
+ Xbee API
+ Ublox
+ SBUS Futaba
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conf/autopilot/heewing_vtol.xml b/conf/autopilot/heewing_vtol.xml
new file mode 100644
index 0000000000..4958471e78
--- /dev/null
+++ b/conf/autopilot/heewing_vtol.xml
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conf/modules/control_mixing_heewing.xml b/conf/modules/control_mixing_heewing.xml
new file mode 100644
index 0000000000..89c499419d
--- /dev/null
+++ b/conf/modules/control_mixing_heewing.xml
@@ -0,0 +1,23 @@
+
+
+
+ Control mixing specific to the Heewing T1 Ranger
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conf/modules/guidance_plane_pid_rotorcraft.xml b/conf/modules/guidance_plane_pid_rotorcraft.xml
new file mode 100644
index 0000000000..b780a63744
--- /dev/null
+++ b/conf/modules/guidance_plane_pid_rotorcraft.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+ Basic guidance code for fixedwing planes using the rotorcraft firmware
+ with PID control, no airspeed control
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @stabilization,guidance_rotorcraft
+ guidance,attitude_command
+
+
+
+
+
+
+
diff --git a/conf/modules/stabilization_plane_pid.xml b/conf/modules/stabilization_plane_pid.xml
new file mode 100644
index 0000000000..9d684dfed9
--- /dev/null
+++ b/conf/modules/stabilization_plane_pid.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+ Basic fixed-wing PID controller for rotorcraft firmware using float euler implementation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stabilization_rotorcraft,@attitude_command
+ commands
+
+
+
+
+
+
+
diff --git a/conf/simulator/jsbsim/aircraft/Systems/aerodynamics_ranger_t1.xml b/conf/simulator/jsbsim/aircraft/Systems/aerodynamics_ranger_t1.xml
new file mode 100644
index 0000000000..f463213ea7
--- /dev/null
+++ b/conf/simulator/jsbsim/aircraft/Systems/aerodynamics_ranger_t1.xml
@@ -0,0 +1,204 @@
+
+
+
+
+
+ Lift due to alpha
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+
+ aero/alpha-rad
+
+ -1.57 0.
+ -0.20 -0.750
+ 0.00 0.250
+ 0.23 1.400
+ 0.60 0.710
+ 1.57 0.
+
+
+
+
+
+
+
+
+
+
+ Drag at zero lift
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ 0.15
+
+
+
+
+ Induced drag
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ aero/cl-squared
+ 0.3
+
+
+
+
+
+
+
+
+ Side force due to beta
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ aero/beta-rad
+ -0.1
+
+
+
+
+
+
+
+
+ Roll moment due to beta
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+
+ aero/beta-rad
+
+ -0.01
+
+
+
+
+ Roll moment due to roll rate
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ aero/bi2vel
+ velocities/p-aero-rad_sec
+ -0.4
+
+
+
+
+ Roll moment due to yaw rate
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ aero/bi2vel
+ velocities/r-aero-rad_sec
+
+ 0.
+
+
+
+
+ Roll moment due to aileron
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ fcs/aileron_lag
+ 0.17
+
+
+
+
+
+
+
+
+ Pitch moment due to alpha
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/cbarw-ft
+ aero/alpha-rad
+ -0.5
+
+ aero/alpha-rad
+
+ -1.57 0.
+ -0.60 0.3
+ 0.00 0.
+ 0.60 -0.3
+ 1.57 0.
+
+
+
+
+
+
+ Pitch moment due to elevator
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/cbarw-ft
+ fcs/elevator_lag
+ 1.1
+
+
+
+
+ Pitch moment due to pitch rate
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/cbarw-ft
+ aero/ci2vel
+ velocities/q-aero-rad_sec
+ -12
+
+
+
+
+
+
+
+
+ Yaw moment due to beta
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+
+ aero/beta-rad
+
+ 0.12
+
+
+
+
+ Yaw moment due to yaw rate
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ aero/bi2vel
+ velocities/r-aero-rad_sec
+ -0.15
+
+
+
+
+ Adverse yaw
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ fcs/aileron_lag
+ -0.01
+
+
+
+
+
+
diff --git a/conf/simulator/jsbsim/aircraft/ranger_t1.xml b/conf/simulator/jsbsim/aircraft/ranger_t1.xml
new file mode 100644
index 0000000000..84eb6fde90
--- /dev/null
+++ b/conf/simulator/jsbsim/aircraft/ranger_t1.xml
@@ -0,0 +1,442 @@
+
+
+
+
+
+ ENAC
+ 23-01-2025
+ 1.0
+ Heewing Ranger T1 VTOL
+
+
+
+ 0.09
+ 0.73
+ 0.15
+ 0.029
+ 0.4
+ 0.01
+ 0.4
+ 0
+
+ 0
+ 0
+ 0
+
+
+ 0
+ 0
+ 0
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0.005
+ 0.005
+ 0.015
+ 0.
+ 0.
+ 0.
+ 0.67
+
+ 0
+ 0
+ 0
+
+
+
+
+
+
+ -0.12
+ 0.0
+ -0.1
+
+ 0.8
+ 0.5
+ 500
+ 100
+ 1000
+ 0.0
+ NONE
+ 0
+
+
+
+
+ 0.12
+ 0.0
+ -0.1
+
+ 0.8
+ 0.5
+ 500
+ 100
+ 1000
+ 0.0
+ NONE
+ 0
+
+
+
+
+ 0.0
+ 0.12
+ -0.1
+
+ 0.8
+ 0.5
+ 500
+ 100
+ 1000
+ 0.0
+ NONE
+ 0
+
+
+
+
+ 0.0
+ -0.12
+ -0.1
+
+ 0.8
+ 0.5
+ 500
+ 100
+ 1000
+ 0.0
+ NONE
+ 0
+
+
+
+
+ fcs/motor_lag
+ fcs/servo_lag
+ fcs/tilt_min_rad
+ fcs/tilt_max_rad
+
+
+
+
+ fcs/MOTOR_RIGHT
+
+ 0
+ 1
+
+ fcs/motor_lag
+
+
+
+ fcs/MOTOR_LEFT
+
+ 0
+ 1
+
+ fcs/motor_lag
+
+
+
+ fcs/MOTOR_TAIL
+
+ 0
+ 1
+
+ fcs/motor_lag
+
+
+
+
+ fcs/ROLL
+ fcs/servo_lag
+
+
+
+ fcs/PITCH
+ fcs/servo_lag
+
+
+
+
+
+
+
+
+
+ fcs/TILT
+ fcs/YAW
+
+
+
+
+
+ fcs/tilt_right
+ false
+
+ 0.
+ 1.
+
+
+ 0.
+ 1.73329
+
+
+
+
+ fcs/tilt_right_rad
+ 3.5
+
+ fcs/tilt_min_rad
+ fcs/tilt_max_rad
+
+
+
+
+
+ -fcs/tilt_right_lag
+
+
+
+
+
+ -fcs/tilt_right_lag
+
+
+
+
+
+ -fcs/tilt_right_lag
+
+
+
+
+
+ -fcs/tilt_right_lag
+
+
+
+
+
+
+
+ fcs/TILT
+ -fcs/YAW
+
+
+
+
+
+ fcs/tilt_left
+ false
+
+ 0.
+ 1.
+
+
+ 0.
+ 1.73329
+
+
+
+
+ fcs/tilt_left_rad
+ 3.5
+
+ fcs/tilt_min_rad
+ fcs/tilt_max_rad
+
+
+
+
+
+ -fcs/tilt_left_rad
+
+
+
+
+
+ -fcs/tilt_left_rad
+
+
+
+
+
+
+ -1.
+ -fcs/tilt_left_rad
+
+
+
+
+
+
+
+ -1.
+ -fcs/tilt_left_rad
+
+
+
+
+
+
+
+
+
+
+ fcs/MOTOR_RIGHT
+ fcs/MOTOR_LEFT
+ fcs/MOTOR_TAIL
+ fcs/PITCH
+ fcs/ROLL
+ fcs/YAW
+ fcs/THRUST
+ fcs/TILT
+ fcs/motor_right_lag
+ fcs/motor_left_lag
+ fcs/motor_tail_lag
+ fcs/aileron_lag
+ fcs/elevator_lag
+ fcs/tilt_left_lag
+ fcs/tilt_right_lag
+
+ fcs/motor_max_thrust
+
+ fcs/motor_max_torque
+
+
+
+
+
+
+ fcs/motor_right_lag
+ fcs/motor_max_thrust
+ 0.224808943
+
+
+
+ -0.09
+ 0.12
+ 0.
+
+
+ 1
+ 0
+ 0
+
+
+
+
+
+ fcs/motor_left_lag
+ fcs/motor_max_thrust
+ 0.224808943
+
+
+
+ -0.09
+ -0.12
+ 0
+
+
+ 1
+ 0
+ 0
+
+
+
+
+
+ fcs/motor_tail_lag
+ fcs/motor_max_thrust
+ 0.224808943
+
+
+
+ 0.25
+ 0
+ 0
+
+
+ 0
+ 0
+ -1
+
+
+
+
+
+
+
+
+ fcs/motor_right_lag
+ fcs/motor_max_torque
+ 0.738
+
+
+
+ 0.09
+ 0.12
+ 0.
+
+
+ 1
+ 0
+ 0
+
+
+
+
+
+
+
+ fcs/motor_left_lag
+ fcs/motor_max_torque
+ 0.738
+
+
+
+ 0.09
+ -0.12
+ 0.
+
+
+ -1
+ 0
+ 0
+
+
+
+
+
+
+ fcs/motor_tail_lag
+ fcs/motor_max_torque
+ 0.738
+
+
+
+ -0.25
+ 0
+ 0
+
+
+ 0
+ 0
+ -1
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sw/airborne/firmwares/rotorcraft/guidance/guidance_plane.c b/sw/airborne/firmwares/rotorcraft/guidance/guidance_plane.c
new file mode 100644
index 0000000000..3faa6fd83a
--- /dev/null
+++ b/sw/airborne/firmwares/rotorcraft/guidance/guidance_plane.c
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2024 Gautier Hattenberger
+ *
+ * This file is part of paparazzi.
+ *
+ * paparazzi is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * paparazzi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with paparazzi; see the file COPYING. If not, see
+ * .
+ */
+
+/** @file firmwares/rotorcraft/guidance/guidance_plane.c
+ * Guidance controller for planes with PID
+ * compatible with the rotorcraft firmware
+ * no airspeed control
+ *
+ */
+
+#include "firmwares/rotorcraft/guidance/guidance_plane.h"
+#include "firmwares/rotorcraft/navigation.h"
+#include "firmwares/rotorcraft/guidance.h"
+#include "firmwares/rotorcraft/stabilization.h"
+#include "generated/airframe.h"
+#include "generated/flight_plan.h"
+#include "modules/nav/waypoints.h"
+#include "state.h"
+
+#ifndef GUIDANCE_PLANE_MAX_BANK
+#define GUIDANCE_PLANE_MAX_BANK RadOfDeg(45.f)
+#endif
+
+#ifndef GUIDANCE_PLANE_MAX_PITCH
+#define GUIDANCE_PLANE_MAX_PITCH RadOfDeg(30.f)
+#endif
+
+#ifndef GUIDANCE_PLANE_MIN_PITCH
+#define GUIDANCE_PLANE_MIN_PITCH RadOfDeg(-20.f)
+#endif
+
+#ifndef GUIDANCE_PLANCE_COURSE_PRE_BANK
+#define GUIDANCE_PLANCE_COURSE_PRE_BANK 1.f
+#endif
+
+#ifndef GUIDANCE_PLANE_MAX_CLIMB
+#define GUIDANCE_PLANE_MAX_CLIMB 2.f
+#endif
+
+#ifndef GUIDANCE_PLANE_PITCH_OF_VZ
+#define GUIDANCE_PLANE_PITCH_OF_VZ RadOfDeg(5.f)
+#endif
+
+#ifndef GUIDANCE_PLANE_PITCH_TRIM
+#define GUIDANCE_PLANE_PITCH_TRIM RadOfDeg(0.f)
+#endif
+
+#ifndef GUIDANCE_PLANE_CLIMB_THROTTLE_INCREMENT
+#define GUIDANCE_PLANE_CLIMB_THROTTLE_INCREMENT 0.1f
+#endif
+
+/*
+ * external variables
+ */
+
+struct GuidancePlane guidance_plane;
+
+/*
+ * internal variables
+ */
+
+static float pitch_sum_err;
+#define GUIDANCE_PLANE_PITCH_MAX_SUM_ERR (RadOfDeg(10.))
+
+static float throttle_sum_err;
+#define GUIDANCE_PLANE_THROTTLE_MAX_SUM_ERR 0.4
+
+#if PERIODIC_TELEMETRY
+#include "modules/datalink/telemetry.h"
+
+#endif
+
+void guidance_plane_init(void)
+{
+ guidance_plane.roll_max_setpoint = GUIDANCE_PLANE_MAX_BANK;
+ guidance_plane.course_setpoint = 0.f;
+ guidance_plane.course_kp = GUIDANCE_PLANE_COURSE_KP;
+ guidance_plane.course_kd = GUIDANCE_PLANE_COURSE_KD;
+ guidance_plane.course_pre_bank_correction = GUIDANCE_PLANCE_COURSE_PRE_BANK;
+ guidance_plane.roll_cmd = 0.f;
+
+ guidance_plane.pitch_max_setpoint = GUIDANCE_PLANE_MAX_PITCH;
+ guidance_plane.pitch_min_setpoint = GUIDANCE_PLANE_MIN_PITCH;
+ guidance_plane.climb_max_setpoint = GUIDANCE_PLANE_MAX_CLIMB;
+ guidance_plane.altitude_setpoint = 0.f;
+ guidance_plane.climb_setpoint = 0.f;
+ guidance_plane.climb_kp = GUIDANCE_PLANE_CLIMB_KP;
+ guidance_plane.pitch_trim = GUIDANCE_PLANE_PITCH_TRIM;
+ guidance_plane.pitch_of_vz = GUIDANCE_PLANE_PITCH_OF_VZ;
+ guidance_plane.climb_throttle_increment = GUIDANCE_PLANE_CLIMB_THROTTLE_INCREMENT;
+ guidance_plane.p_kp = GUIDANCE_PLANE_PITCH_KP;
+ guidance_plane.p_kd = GUIDANCE_PLANE_PITCH_KD;
+ guidance_plane.p_ki = GUIDANCE_PLANE_PITCH_KI;
+ guidance_plane.t_kp = GUIDANCE_PLANE_THROTTLE_KP;
+ guidance_plane.t_kd = GUIDANCE_PLANE_THROTTLE_KD;
+ guidance_plane.t_ki = GUIDANCE_PLANE_THROTTLE_KI;
+ guidance_plane.cruise_throttle = GUIDANCE_PLANE_NOMINAL_THROTTLE;
+ guidance_plane.pitch_cmd = 0.f;
+ guidance_plane.throttle_cmd = 0;
+
+ pitch_sum_err = 0.f;
+ throttle_sum_err = 0.f;
+
+#if PERIODIC_TELEMETRY
+#endif
+}
+
+/**
+ * run horizontal control loop for position and speed control
+ */
+struct StabilizationSetpoint guidance_plane_attitude_from_nav(bool in_flight UNUSED)
+{
+ struct FloatEulers att_sp;
+
+ // course control loop
+ if (nav.horizontal_mode == NAV_HORIZONTAL_MODE_NONE || nav.horizontal_mode == NAV_HORIZONTAL_MODE_GUIDED) {
+ guidance_plane.roll_cmd = 0.f;
+ guidance_plane.pitch_cmd = 0.f;
+ } else if (nav.horizontal_mode == NAV_HORIZONTAL_MODE_ATTITUDE) {
+ if (nav.setpoint_mode == NAV_SETPOINT_MODE_QUAT) {
+ struct FloatEulers e;
+ float_eulers_of_quat(&e, &nav.quat);
+ guidance_plane.roll_cmd = e.phi;
+ guidance_plane.pitch_cmd = e.theta;
+ }
+ else {
+ guidance_plane.roll_cmd = nav.roll;
+ guidance_plane.pitch_cmd = nav.pitch;
+ }
+ } else {
+ // update carrot for GCS display and convert ENU float -> NED int
+ // even if sp is changed later
+ // FIXME really needed here ?
+ guidance_h.sp.pos.x = POS_BFP_OF_REAL(nav.carrot.y);
+ guidance_h.sp.pos.y = POS_BFP_OF_REAL(nav.carrot.x);
+
+ switch (nav.setpoint_mode) {
+ case NAV_SETPOINT_MODE_POS:
+ guidance_plane.course_setpoint = atan2f(nav.carrot.x - stateGetPositionEnu_f()->x, nav.carrot.y - stateGetPositionNed_f()->y);
+ break;
+ case NAV_SETPOINT_MODE_SPEED:
+ guidance_plane.course_setpoint = atan2f(nav.speed.x, nav.speed.y);
+ break;
+ case NAV_SETPOINT_MODE_ACCEL:
+ // not supported, flying towards HOME waypoint
+ guidance_plane.course_setpoint = atan2f(waypoint_get_x(WP_HOME) - stateGetPositionEnu_f()->x,
+ waypoint_get_y(WP_HOME) - stateGetPositionEnu_f()->y);
+ break;
+ default:
+ // nothing to do for other cases at the moment
+ break;
+ }
+ /* final attitude setpoint */
+
+ // Ground path error
+ static float last_err = 0.f;
+ float err = guidance_plane.course_setpoint - stateGetHorizontalSpeedDir_f();
+ NormRadAngle(err);
+ float d_err = err - last_err;
+ last_err = err;
+ NormRadAngle(d_err);
+ //float course_pre_bank = 0.f; // TODO: compute if flying a circle -> pb is that it is coming from nav
+ // guidance_plane.course_pre_bank_correction * course_pre_bank
+
+ guidance_plane.roll_cmd = guidance_plane.course_kp * err + guidance_plane.course_kd * d_err;
+ BoundAbs(guidance_plane.roll_cmd, guidance_plane.roll_max_setpoint);
+ }
+
+ att_sp.phi = guidance_plane.roll_cmd;
+ att_sp.theta = guidance_plane.pitch_cmd;
+ att_sp.psi = stateGetNedToBodyEulers_f()->psi;
+ return stab_sp_from_eulers_f(&att_sp);
+}
+
+
+static void guidance_plane_set_pitch(bool in_flight)
+{
+ static float last_err = 0.f;
+
+ if (!in_flight) {
+ pitch_sum_err = 0.f;
+ }
+
+ // Compute errors
+ float err = guidance_plane.climb_setpoint - stateGetSpeedEnu_f()->z;
+ float d_err = err - last_err;
+ last_err = err;
+
+ if (guidance_plane.p_ki > 0.) {
+ pitch_sum_err += err * (1. / PERIODIC_FREQUENCY);
+ BoundAbs(pitch_sum_err, GUIDANCE_PLANE_PITCH_MAX_SUM_ERR / guidance_plane.p_ki);
+ }
+
+ // PID loop + feedforward ctl
+ guidance_plane.pitch_cmd = nav.pitch
+ + guidance_plane.pitch_trim
+ + guidance_plane.pitch_of_vz * guidance_plane.climb_setpoint
+ + guidance_plane.p_kp * err
+ + guidance_plane.p_kd * d_err
+ + guidance_plane.p_ki * pitch_sum_err;
+
+}
+
+static void guidance_plane_set_throttle(bool in_flight)
+{
+ static float last_err = 0.;
+
+ if (!in_flight) {
+ throttle_sum_err = 0;
+ }
+
+ // Compute errors
+ float err = guidance_plane.climb_setpoint - stateGetSpeedEnu_f()->z;
+ float d_err = err - last_err;
+ last_err = err;
+
+ if (guidance_plane.t_ki > 0.) {
+ throttle_sum_err += err * (1. / PERIODIC_FREQUENCY);
+ BoundAbs(throttle_sum_err, GUIDANCE_PLANE_THROTTLE_MAX_SUM_ERR / guidance_plane.t_ki);
+ }
+
+ // PID loop + feedforward ctl
+ float th_cmd = guidance_plane.cruise_throttle
+ + guidance_plane.climb_throttle_increment * guidance_plane.climb_setpoint
+ + guidance_plane.t_kp * err
+ + guidance_plane.t_kd * d_err
+ + guidance_plane.t_ki * throttle_sum_err;
+
+ guidance_plane.throttle_cmd = TRIM_UPPRZ(MAX_PPRZ * th_cmd);
+
+}
+
+/**
+ * run vertical control loop for position and speed control
+ */
+struct ThrustSetpoint guidance_plane_thrust_from_nav(bool in_flight)
+{
+ if (nav.vertical_mode == NAV_VERTICAL_MODE_MANUAL) {
+ guidance_plane.throttle_cmd = (int32_t) nav.throttle;
+ } else {
+ if (nav.vertical_mode == NAV_VERTICAL_MODE_ALT) {
+ guidance_plane.altitude_setpoint = nav.nav_altitude;
+ float alt_err = guidance_plane.altitude_setpoint - stateGetPositionEnu_f()->z;
+ guidance_plane.climb_setpoint = guidance_plane.climb_kp * alt_err;
+ } else if (nav.vertical_mode == NAV_VERTICAL_MODE_CLIMB) {
+ guidance_plane.climb_setpoint = nav.climb;
+ } else {
+ guidance_plane.climb_setpoint = 0.f;
+ }
+ BoundAbs(guidance_plane.climb_setpoint, guidance_plane.climb_max_setpoint);
+
+ guidance_plane_set_pitch(in_flight);
+ guidance_plane_set_throttle(in_flight);
+ }
+
+ return th_sp_from_thrust_i(guidance_plane.throttle_cmd, THRUST_AXIS_X);
+}
+
+void guidance_plane_enter(void)
+{
+ /* set nav_heading to current heading */
+ //nav.heading = stateGetNedToBodyEulers_f()->psi;
+
+ pitch_sum_err = 0.f;
+ throttle_sum_err = 0.f;
+}
+
diff --git a/sw/airborne/firmwares/rotorcraft/guidance/guidance_plane.h b/sw/airborne/firmwares/rotorcraft/guidance/guidance_plane.h
new file mode 100644
index 0000000000..4e4e9dcba5
--- /dev/null
+++ b/sw/airborne/firmwares/rotorcraft/guidance/guidance_plane.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 Gautier Hattenberger
+ *
+ * This file is part of paparazzi.
+ *
+ * paparazzi is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * paparazzi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with paparazzi; see the file COPYING. If not, see
+ * .
+ */
+
+/** @file firmwares/rotorcraft/guidance/guidance_plane.h
+ * Guidance controller for planes in rotorcraft firmware
+ * using basic PID controller
+ * no airspeed control
+ *
+ */
+
+#ifndef GUIDANCE_PLANE_H
+#define GUIDANCE_PLANE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "std.h"
+#include "math/pprz_algebra_int.h"
+#include "math/pprz_algebra_float.h"
+
+struct GuidancePlane {
+ // horizontal
+ float roll_max_setpoint;
+ float pitch_max_setpoint;
+ float pitch_min_setpoint;
+ float course_setpoint;
+ // horizontal gains
+ float course_kp;
+ float course_kd;
+ float course_pre_bank_correction;
+
+ // vertical
+ float altitude_setpoint;
+ float climb_max_setpoint;
+ float climb_setpoint;
+ float pitch_trim;
+ // vertical gains, pitch and throttle
+ float cruise_throttle;
+ float climb_kp;
+ float pitch_of_vz;
+ float climb_throttle_increment;
+ float p_kp;
+ float p_kd;
+ float p_ki;
+ float t_kp;
+ float t_kd;
+ float t_ki;
+
+ // outputs
+ float roll_cmd;
+ float pitch_cmd;
+ int32_t throttle_cmd; // Throttle command (in pprz_t)
+};
+
+/** Guidance PID structyre
+ */
+extern struct GuidancePlane guidance_plane;
+
+extern void guidance_plane_init(void);
+extern void guidance_plane_enter(void);
+extern struct StabilizationSetpoint guidance_plane_attitude_from_nav(bool in_flight);
+extern struct ThrustSetpoint guidance_plane_thrust_from_nav(bool in_flight);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GUIDANCE_PLANE_H */
diff --git a/sw/airborne/firmwares/rotorcraft/stabilization/stabilization_attitude_plane_pid.c b/sw/airborne/firmwares/rotorcraft/stabilization/stabilization_attitude_plane_pid.c
new file mode 100644
index 0000000000..e19dc8136f
--- /dev/null
+++ b/sw/airborne/firmwares/rotorcraft/stabilization/stabilization_attitude_plane_pid.c
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 Gautier Hattenberger
+ *
+ * This file is part of paparazzi
+ *
+ * paparazzi is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * paparazzi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with paparazzi; see the file COPYING. If not, see
+ * .
+ */
+
+/**
+ * @file stabilization_attitude_plane_pid.c
+ *
+ * Basic fixed-wing attitude stabilization in euler float version.
+ */
+
+#include "generated/airframe.h"
+
+#include "firmwares/rotorcraft/stabilization/stabilization_attitude_plane_pid.h"
+
+#include "std.h"
+#include "paparazzi.h"
+#include "math/pprz_algebra_float.h"
+#include "state.h"
+
+struct PlaneAttitudeGains stab_plane_gains;
+struct FloatEulers stab_plane_att_sum_err;
+
+static struct FloatEulers stab_att_sp_euler;
+
+float stab_plane_att_cmd[COMMANDS_NB];
+
+#if PERIODIC_TELEMETRY
+#include "modules/datalink/telemetry.h"
+
+static void send_att(struct transport_tx *trans, struct link_device *dev)
+{
+ struct FloatRates *body_rate = stateGetBodyRates_f();
+ struct FloatEulers *att = stateGetNedToBodyEulers_f();
+ float foo = 0.0;
+ pprz_msg_send_STAB_ATTITUDE_FLOAT(trans, dev, AC_ID,
+ &(body_rate->p), &(body_rate->q), &(body_rate->r),
+ &(att->phi), &(att->theta), &(att->psi),
+ &stab_att_sp_euler.phi,
+ &stab_att_sp_euler.theta,
+ &stab_att_sp_euler.psi,
+ &stab_plane_att_sum_err.phi,
+ &stab_plane_att_sum_err.theta,
+ &stab_plane_att_sum_err.psi,
+ &stab_plane_att_cmd[COMMAND_ROLL],
+ &stab_plane_att_cmd[COMMAND_PITCH],
+ &foo, &foo, &foo, &foo,
+ &stabilization.cmd[COMMAND_ROLL],
+ &stabilization.cmd[COMMAND_PITCH],
+ &stabilization.cmd[COMMAND_YAW],
+ &foo, &foo, &foo);
+}
+
+#endif
+
+void stabilization_attitude_plane_pid_init(void)
+{
+ VECT3_ASSIGN(stab_plane_gains.p,
+ STABILIZATION_PLANE_PHI_PGAIN,
+ STABILIZATION_PLANE_THETA_PGAIN,
+ 0.f);
+
+ VECT3_ASSIGN(stab_plane_gains.d,
+ STABILIZATION_PLANE_PHI_DGAIN,
+ STABILIZATION_PLANE_THETA_DGAIN,
+ 0.f);
+
+ VECT3_ASSIGN(stab_plane_gains.i,
+ STABILIZATION_PLANE_PHI_IGAIN,
+ STABILIZATION_PLANE_THETA_IGAIN,
+ 0.f);
+
+ FLOAT_EULERS_ZERO(stab_plane_att_sum_err);
+
+#if PERIODIC_TELEMETRY
+ register_periodic_telemetry(DefaultPeriodic, PPRZ_MSG_ID_STAB_ATTITUDE_FLOAT, send_att);
+#endif
+}
+
+void stabilization_attitude_plane_pid_enter(void)
+{
+ FLOAT_EULERS_ZERO(stab_plane_att_sum_err);
+}
+
+#define MAX_SUM_ERR 200
+
+void stabilization_attitude_plane_pid_run(bool in_flight, struct StabilizationSetpoint *sp, struct ThrustSetpoint *thrust, int32_t *cmd)
+{
+
+ /* Compute feedback */
+ /* attitude error */
+ stab_att_sp_euler = stab_sp_to_eulers_f(sp);
+ struct FloatEulers *att_float = stateGetNedToBodyEulers_f();
+ struct FloatEulers att_err;
+ EULERS_DIFF(att_err, stab_att_sp_euler, *att_float);
+ att_err.psi = 0.f;
+
+ /* update integrator */
+ if (in_flight) {
+ EULERS_ADD(stab_plane_att_sum_err, att_err);
+ EULERS_BOUND_CUBE(stab_plane_att_sum_err, -MAX_SUM_ERR, MAX_SUM_ERR);
+ } else {
+ FLOAT_EULERS_ZERO(stab_plane_att_sum_err);
+ }
+
+ /* rate error */
+ struct FloatRates *rate_float = stateGetBodyRates_f();
+ struct FloatRates rate_sp, rate_err;
+ FLOAT_RATES_ZERO(rate_sp);
+ RATES_DIFF(rate_err, rate_sp, *rate_float);
+
+ /* PID */
+ stab_plane_att_cmd[COMMAND_ROLL] =
+ stab_plane_gains.p.x * att_err.phi +
+ stab_plane_gains.d.x * rate_err.p +
+ stab_plane_gains.i.x * stab_plane_att_sum_err.phi;
+
+ stab_plane_att_cmd[COMMAND_PITCH] =
+ stab_plane_gains.p.y * att_err.theta +
+ stab_plane_gains.d.y * rate_err.q +
+ stab_plane_gains.i.y * stab_plane_att_sum_err.theta;
+
+ cmd[COMMAND_ROLL] = (int32_t)stab_plane_att_cmd[COMMAND_ROLL];
+ cmd[COMMAND_PITCH] = (int32_t)stab_plane_att_cmd[COMMAND_PITCH];
+ cmd[COMMAND_YAW] = 0;
+ cmd[COMMAND_THRUST] = th_sp_to_thrust_i(thrust, 0, THRUST_AXIS_X);
+
+ /* bound the result */
+ BoundAbs(cmd[COMMAND_ROLL], MAX_PPRZ);
+ BoundAbs(cmd[COMMAND_PITCH], MAX_PPRZ);
+ BoundAbs(cmd[COMMAND_YAW], MAX_PPRZ);
+ BoundAbs(cmd[COMMAND_THRUST], MAX_PPRZ);
+}
+
diff --git a/sw/airborne/firmwares/rotorcraft/stabilization/stabilization_attitude_plane_pid.h b/sw/airborne/firmwares/rotorcraft/stabilization/stabilization_attitude_plane_pid.h
new file mode 100644
index 0000000000..96f1327c44
--- /dev/null
+++ b/sw/airborne/firmwares/rotorcraft/stabilization/stabilization_attitude_plane_pid.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 Gautier Hattenberger
+ *
+ * This file is part of paparazzi
+ *
+ * paparazzi is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * paparazzi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with paparazzi; see the file COPYING. If not, see
+ * .
+ */
+
+/**
+ * @file stabilization_attitude_plane_pid.h
+ *
+ * Basic fixed-wing attitude stabilization in euler float version.
+ */
+
+#ifndef STABILIZATION_ATTITUDE_PLANE_PID_H
+#define STABILIZATION_ATTITUDE_PLANE_PID_H
+
+#include "firmwares/rotorcraft/stabilization/stabilization_attitude.h"
+
+struct PlaneAttitudeGains {
+ struct FloatVect3 p;
+ struct FloatVect3 i;
+ struct FloatVect3 d;
+};
+
+extern void stabilization_attitude_plane_pid_init(void);
+extern void stabilization_attitude_plane_pid_enter(void);
+extern void stabilization_attitude_plane_pid_run(bool in_flight, struct StabilizationSetpoint *sp, struct ThrustSetpoint *thrust, int32_t *cmd);
+
+extern struct PlaneAttitudeGains stab_plane_gains;
+extern struct FloatEulers stab_plane_att_sum_err;
+
+#endif /* STABILIZATION_ATTITUDE_PLANE_PID_H */
diff --git a/sw/airborne/modules/ctrl/control_mixing_heewing.c b/sw/airborne/modules/ctrl/control_mixing_heewing.c
new file mode 100644
index 0000000000..10eb7575cf
--- /dev/null
+++ b/sw/airborne/modules/ctrl/control_mixing_heewing.c
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2024 Gautier Hattenberger
+ *
+ * This file is part of paparazzi
+ *
+ * paparazzi is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * paparazzi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with paparazzi; see the file COPYING. If not, see
+ * .
+ */
+
+/** @file "modules/ctrl/control_mixing_heewing.c"
+ * @author Gautier Hattenberger
+ * Control mixing specific to the Heewing T1 Ranger
+ */
+
+#include "modules/ctrl/control_mixing_heewing.h"
+#include "modules/radio_control/radio_control.h"
+#include "generated/modules.h"
+#include "modules/core/commands.h"
+#include "modules/actuators/actuators.h"
+#include "autopilot.h"
+#include "state.h"
+#include "firmwares/rotorcraft/stabilization.h"
+#include "firmwares/rotorcraft/guidance.h"
+#include "firmwares/rotorcraft/navigation.h"
+#include "firmwares/rotorcraft/guidance/guidance_plane.h"
+#include "firmwares/rotorcraft/stabilization/stabilization_indi.h"
+#include "firmwares/rotorcraft/stabilization/stabilization_attitude_plane_pid.h"
+
+// Tilt position in forward flight
+#ifndef CMH_TILT_FORWARD
+#define CMH_TILT_FORWARD 0
+#endif
+
+// Tilt vertical position for hovering
+#ifndef CMH_TILT_VERTICAL
+#define CMH_TILT_VERTICAL 8700
+#endif
+
+// Max tilt differential for yaw
+#ifndef CMH_TILT_DIFF_MAX
+#define CMH_TILT_DIFF_MAX (MAX_PPRZ-CMH_TILT_VERTICAL)
+#endif
+
+// Motor idle position
+#ifndef CMH_MOTOR_IDLE
+#define CMH_MOTOR_IDLE 800
+#endif
+
+// Time for forward transition
+#ifndef CMH_TRANSITION_TIME
+#define CMH_TRANSITION_TIME 4.f
+#endif
+
+// Transition airspeed, set a negative value to disable
+#ifndef CMH_TRANSITION_AIRSPEED
+#define CMH_TRANSITION_AIRSPEED 10.f
+#endif
+
+#define TRANSITION_TO_HOVER false
+#define TRANSITION_TO_FORWARD true
+
+static float transition_ratio;
+static const float transition_increment = 1.f / (CMH_TRANSITION_TIME * PERIODIC_FREQUENCY);
+
+static void transition_run(bool to_forward) {
+ if (to_forward && transition_ratio < 1.f) {
+ if (stateIsAirspeedValid()) {
+ if ((stateGetAirspeed_f() < CMH_TRANSITION_AIRSPEED && transition_ratio < 0.4f)
+ || stateGetAirspeed_f() >= CMH_TRANSITION_AIRSPEED) {
+ transition_ratio += transition_increment;
+ }
+ else if (stateGetAirspeed_f() < CMH_TRANSITION_AIRSPEED && transition_ratio > 0.4f) {
+ transition_ratio -= transition_increment;
+ }
+ }
+ } else if (!to_forward && transition_ratio > 0.f) {
+ transition_ratio = 0.f; // immediately switch back to hover
+ }
+ Bound(transition_ratio, 0.f, 1.f);
+}
+
+static int32_t command_from_transition(int32_t hover_cmd, int32_t forward_cmd) {
+ Bound(transition_ratio, 0.f, 1.f);
+ return (int32_t) (hover_cmd + transition_ratio * (forward_cmd - hover_cmd));
+}
+
+void control_mixing_heewing_init(void)
+{
+ transition_ratio = 0.f; // 0. for hover to 1. for forward flight
+}
+
+void control_mixing_heewing_manual(void)
+{
+ transition_run(TRANSITION_TO_FORWARD);
+ commands[COMMAND_ROLL] = radio_control_get(RADIO_ROLL);
+ commands[COMMAND_PITCH] = radio_control_get(RADIO_PITCH);
+ commands[COMMAND_YAW] = 0;
+ commands[COMMAND_TILT] = CMH_TILT_FORWARD;
+ commands[COMMAND_MOTOR_RIGHT] = radio_control_get(RADIO_THROTTLE);
+ commands[COMMAND_MOTOR_LEFT] = radio_control_get(RADIO_THROTTLE);
+ commands[COMMAND_MOTOR_TAIL] = MIN_PPRZ;
+ commands[COMMAND_THRUST] = (commands[COMMAND_MOTOR_RIGHT] + commands[COMMAND_MOTOR_LEFT]) / 2;
+ autopilot.throttle = commands[COMMAND_THRUST];
+}
+
+void control_mixing_heewing_attitude_direct(void)
+{
+ transition_run(TRANSITION_TO_HOVER);
+ commands[COMMAND_TILT] = CMH_TILT_VERTICAL;
+ commands[COMMAND_ROLL] = 0;
+ commands[COMMAND_PITCH] = 0;
+ struct ThrustSetpoint th_sp = guidance_v_run(autopilot_in_flight());
+ stabilization_run(autopilot_in_flight(), &stabilization.rc_sp, &th_sp, stabilization.cmd);
+ if (autopilot_get_motors_on()) {
+ commands[COMMAND_MOTOR_RIGHT] = actuators_pprz[CMH_ACT_MOTOR_RIGHT];
+ commands[COMMAND_MOTOR_LEFT] = actuators_pprz[CMH_ACT_MOTOR_LEFT];
+ commands[COMMAND_MOTOR_TAIL] = actuators_pprz[CMH_ACT_MOTOR_TAIL];
+ if (autopilot_in_flight()) {
+ commands[COMMAND_YAW] = actuators_pprz[CMH_ACT_YAW];
+ } else {
+ commands[CMH_ACT_YAW] = 0;
+ }
+ commands[COMMAND_THRUST] = stabilization.cmd[COMMAND_THRUST];
+ } else {
+ commands[COMMAND_MOTOR_RIGHT] = -MAX_PPRZ;
+ commands[COMMAND_MOTOR_LEFT] = -MAX_PPRZ;
+ commands[COMMAND_MOTOR_TAIL] = -MAX_PPRZ;
+ commands[COMMAND_YAW] = 0;
+ commands[COMMAND_THRUST] = 0;
+ }
+ autopilot.throttle = commands[COMMAND_THRUST];
+}
+
+void control_mixing_heewing_attitude_direct_enter(void)
+{
+ guidance_h_mode_changed(GUIDANCE_H_MODE_NONE);
+ guidance_v_mode_changed(GUIDANCE_V_MODE_RC_DIRECT);
+ stabilization_mode_changed(STABILIZATION_MODE_NONE, STABILIZATION_ATT_SUBMODE_HEADING); // force mode change to always reset heading
+ stabilization_mode_changed(STABILIZATION_MODE_ATTITUDE, STABILIZATION_ATT_SUBMODE_HEADING);
+}
+
+void stabilization_indi_set_wls_settings(void)
+{
+ // Calculate the min and max increments
+ wls_stab_p.u_min[CMH_ACT_MOTOR_RIGHT] = CMH_MOTOR_IDLE;
+ wls_stab_p.u_max[CMH_ACT_MOTOR_RIGHT] = MAX_PPRZ;
+ wls_stab_p.u_pref[CMH_ACT_MOTOR_RIGHT] = act_pref[CMH_ACT_MOTOR_RIGHT];
+
+ wls_stab_p.u_min[CMH_ACT_MOTOR_LEFT] = CMH_MOTOR_IDLE;
+ wls_stab_p.u_max[CMH_ACT_MOTOR_LEFT] = MAX_PPRZ;
+ wls_stab_p.u_pref[CMH_ACT_MOTOR_LEFT] = act_pref[CMH_ACT_MOTOR_LEFT];
+
+ wls_stab_p.u_min[CMH_ACT_MOTOR_TAIL] = CMH_MOTOR_IDLE;
+ wls_stab_p.u_max[CMH_ACT_MOTOR_TAIL] = MAX_PPRZ;
+ wls_stab_p.u_pref[CMH_ACT_MOTOR_TAIL] = act_pref[CMH_ACT_MOTOR_TAIL];
+
+ wls_stab_p.u_min[CMH_ACT_YAW] = -CMH_TILT_DIFF_MAX;
+ wls_stab_p.u_max[CMH_ACT_YAW] = CMH_TILT_DIFF_MAX;
+ wls_stab_p.u_pref[CMH_ACT_YAW] = act_pref[CMH_ACT_YAW];
+}
+
+void control_mixing_heewing_attitude_plane_enter(void)
+{
+ // don't use forward submode to avoid pitch offset on RC input
+ guidance_h_mode_changed(GUIDANCE_H_MODE_NONE);
+ guidance_v_mode_changed(GUIDANCE_V_MODE_RC_DIRECT);
+ stabilization_mode_changed(STABILIZATION_MODE_ATTITUDE, STABILIZATION_ATT_SUBMODE_HEADING);
+ stabilization_attitude_plane_pid_enter();
+}
+
+void control_mixing_heewing_attitude_plane(void)
+{
+ transition_run(TRANSITION_TO_FORWARD);
+
+ if (transition_ratio < 0.5f) {
+ struct ThrustSetpoint hover_th_sp = guidance_v_run(autopilot_in_flight());
+ stabilization_run(autopilot_in_flight(), &stabilization.rc_sp, &hover_th_sp, stabilization.cmd);
+ }
+ struct ThrustSetpoint th_sp = th_sp_from_thrust_i(radio_control_get(RADIO_THROTTLE), THRUST_AXIS_X);
+ struct FloatEulers rc_sp = {
+ .phi = STABILIZATION_ATTITUDE_SP_MAX_PHI * radio_control_get(RADIO_ROLL) / MAX_PPRZ,
+ .theta = STABILIZATION_ATTITUDE_SP_MAX_THETA * radio_control_get(RADIO_PITCH) / MAX_PPRZ,
+ .psi = 0.f
+ };
+ struct StabilizationSetpoint stab_sp = stab_sp_from_eulers_f(&rc_sp);
+ stabilization_attitude_plane_pid_run(transition_ratio < 0.8 ? false : autopilot_in_flight(), &stab_sp, &th_sp, stabilization.cmd);
+
+ commands[COMMAND_ROLL] = stabilization.cmd[COMMAND_ROLL];
+ commands[COMMAND_PITCH] = stabilization.cmd[COMMAND_PITCH];
+ commands[COMMAND_THRUST] = stabilization.cmd[COMMAND_THRUST];
+ if (autopilot_in_flight()) {
+ if (transition_ratio < 0.5f) {
+ // only hover stabilization
+ commands[COMMAND_MOTOR_RIGHT] = actuators_pprz[CMH_ACT_MOTOR_RIGHT];
+ commands[COMMAND_MOTOR_LEFT] = actuators_pprz[CMH_ACT_MOTOR_LEFT];
+ commands[COMMAND_MOTOR_TAIL] = actuators_pprz[CMH_ACT_MOTOR_TAIL];
+ } else {
+ // blend with plane control
+ commands[COMMAND_MOTOR_RIGHT] = command_from_transition(actuators_pprz[CMH_ACT_MOTOR_RIGHT], stabilization.cmd[COMMAND_THRUST]);
+ commands[COMMAND_MOTOR_LEFT] = command_from_transition(actuators_pprz[CMH_ACT_MOTOR_LEFT], stabilization.cmd[COMMAND_THRUST]);
+ commands[COMMAND_MOTOR_TAIL] = command_from_transition(actuators_pprz[CMH_ACT_MOTOR_TAIL], 0);
+ }
+ } else {
+ commands[COMMAND_MOTOR_RIGHT] = stabilization.cmd[COMMAND_THRUST];
+ commands[COMMAND_MOTOR_LEFT] = stabilization.cmd[COMMAND_THRUST];
+ commands[COMMAND_MOTOR_TAIL] = 0;
+ }
+ commands[COMMAND_TILT] = command_from_transition(CMH_TILT_VERTICAL, CMH_TILT_FORWARD);
+ commands[COMMAND_YAW] = 0;
+ autopilot.throttle = commands[COMMAND_THRUST];
+
+}
+
+void control_mixing_heewing_nav_enter(void)
+{
+ guidance_h_mode_changed(GUIDANCE_H_MODE_NAV);
+ guidance_v_mode_changed(GUIDANCE_V_MODE_NAV);
+ stabilization_mode_changed(STABILIZATION_MODE_ATTITUDE, STABILIZATION_ATT_SUBMODE_HEADING);
+}
+
+void control_mixing_heewing_nav_run(void)
+{
+ if (nav.horizontal_mode == NAV_HORIZONTAL_MODE_ROUTE ||
+ nav.horizontal_mode == NAV_HORIZONTAL_MODE_CIRCLE) {
+ transition_run(TRANSITION_TO_FORWARD);
+
+ struct ThrustSetpoint th_sp = guidance_plane_thrust_from_nav(transition_ratio < 0.8f ? false : autopilot_in_flight());
+ if (transition_ratio < 0.8f) {
+ guidance_plane.pitch_cmd = 0.f;
+ }
+ struct StabilizationSetpoint stab_sp = guidance_plane_attitude_from_nav(autopilot_in_flight());
+ if (transition_ratio < 0.5f) {
+ stabilization_attitude_plane_pid_run(transition_ratio < 0.8f ? false : autopilot_in_flight(), &stab_sp, &th_sp, stabilization.cmd);
+ struct ThrustSetpoint hover_th_sp = guidance_v_run(autopilot_in_flight());
+ struct FloatEulers eulers_sp = { .phi = 0.f , .theta = 0.f, .psi = stateGetNedToBodyEulers_f()->psi };
+ struct StabilizationSetpoint hover_stab_sp = stab_sp_from_eulers_f(&eulers_sp);
+ stabilization_run(autopilot_in_flight(), &hover_stab_sp, &hover_th_sp, stabilization.cmd); // will overwrite COMMAND_THRUST
+ } else {
+ stabilization_attitude_plane_pid_run(transition_ratio < 0.8f ? false : autopilot_in_flight(), &stab_sp, &th_sp, stabilization.cmd);
+ }
+ nav.heading = stateGetNedToBodyEulers_f()->psi; // overwrite nav heading to avoid problems when transition to hover
+
+ commands[COMMAND_TILT] = command_from_transition(CMH_TILT_VERTICAL, CMH_TILT_FORWARD);
+ commands[COMMAND_ROLL] = stabilization.cmd[COMMAND_ROLL];
+ commands[COMMAND_PITCH] = stabilization.cmd[COMMAND_PITCH];
+ commands[COMMAND_YAW] = command_from_transition(actuators_pprz[CMH_ACT_YAW], 0);
+ if (autopilot_get_motors_on()) {
+ if (transition_ratio < 0.5f) {
+ // only hover stabilization
+ commands[COMMAND_MOTOR_RIGHT] = actuators_pprz[CMH_ACT_MOTOR_RIGHT];
+ commands[COMMAND_MOTOR_LEFT] = actuators_pprz[CMH_ACT_MOTOR_LEFT];
+ commands[COMMAND_MOTOR_TAIL] = actuators_pprz[CMH_ACT_MOTOR_TAIL];
+ } else {
+ // blend with plane control
+ commands[COMMAND_MOTOR_RIGHT] = command_from_transition(actuators_pprz[CMH_ACT_MOTOR_RIGHT], stabilization.cmd[COMMAND_THRUST]);
+ commands[COMMAND_MOTOR_LEFT] = command_from_transition(actuators_pprz[CMH_ACT_MOTOR_LEFT], stabilization.cmd[COMMAND_THRUST]);
+ commands[COMMAND_MOTOR_TAIL] = command_from_transition(actuators_pprz[CMH_ACT_MOTOR_TAIL], 0);
+ }
+ commands[COMMAND_THRUST] = stabilization.cmd[COMMAND_THRUST];
+ } else {
+ commands[COMMAND_THRUST] = 0;
+ commands[COMMAND_MOTOR_RIGHT] = MIN_PPRZ;
+ commands[COMMAND_MOTOR_LEFT] = MIN_PPRZ;
+ commands[COMMAND_MOTOR_TAIL] = 0;
+ }
+
+ } else {
+ // all other nav modes are in rotorcraft flight mode
+ transition_run(TRANSITION_TO_HOVER);
+ commands[COMMAND_TILT] = command_from_transition(CMH_TILT_VERTICAL, CMH_TILT_FORWARD);
+ commands[COMMAND_ROLL] = 0;
+ commands[COMMAND_PITCH] = 0;
+
+ struct ThrustSetpoint th_sp = guidance_v_run(autopilot_in_flight());
+ struct StabilizationSetpoint stab_sp = guidance_h_run(autopilot_in_flight());
+ stabilization_run(autopilot_in_flight(), &stab_sp, &th_sp, stabilization.cmd);
+
+ if (autopilot_get_motors_on()) {
+ commands[COMMAND_MOTOR_RIGHT] = actuators_pprz[CMH_ACT_MOTOR_RIGHT];
+ commands[COMMAND_MOTOR_LEFT] = actuators_pprz[CMH_ACT_MOTOR_LEFT];
+ commands[COMMAND_MOTOR_TAIL] = command_from_transition(actuators_pprz[CMH_ACT_MOTOR_TAIL], 0);
+ if (autopilot_in_flight()) {
+ commands[COMMAND_YAW] = actuators_pprz[CMH_ACT_YAW];
+ } else {
+ commands[CMH_ACT_YAW] = 0;
+ }
+ commands[COMMAND_THRUST] = stabilization.cmd[COMMAND_THRUST];
+ } else {
+ commands[COMMAND_MOTOR_RIGHT] = -MAX_PPRZ;
+ commands[COMMAND_MOTOR_LEFT] = -MAX_PPRZ;
+ commands[COMMAND_MOTOR_TAIL] = -MAX_PPRZ;
+ commands[COMMAND_YAW] = 0;
+ commands[COMMAND_THRUST] = 0;
+ }
+ }
+ autopilot.throttle = commands[COMMAND_THRUST];
+}
diff --git a/sw/airborne/modules/ctrl/control_mixing_heewing.h b/sw/airborne/modules/ctrl/control_mixing_heewing.h
new file mode 100644
index 0000000000..189c71d066
--- /dev/null
+++ b/sw/airborne/modules/ctrl/control_mixing_heewing.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 Gautier Hattenberger
+ *
+ * This file is part of paparazzi
+ *
+ * paparazzi is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * paparazzi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with paparazzi; see the file COPYING. If not, see
+ * .
+ */
+
+/** @file "modules/ctrl/control_mixing_heewing.h"
+ * @author Gautier Hattenberger
+ * Control mixing specific to the Heewing T1 Ranger
+ */
+
+#ifndef CONTROL_MIXING_HEEWING_H
+#define CONTROL_MIXING_HEEWING_H
+
+// INDI actuators output indexes
+#define CMH_ACT_MOTOR_RIGHT 0
+#define CMH_ACT_MOTOR_LEFT 1
+#define CMH_ACT_MOTOR_TAIL 2
+#define CMH_ACT_YAW 3
+
+extern void control_mixing_heewing_init(void);
+
+/** Direct manual control in plane style flight
+ */
+extern void control_mixing_heewing_manual(void);
+
+/** Stabilization in attitude direct mode
+ */
+extern void control_mixing_heewing_attitude_direct(void);
+extern void control_mixing_heewing_attitude_direct_enter(void);
+extern void control_mixing_heewing_attitude_plane(void);
+extern void control_mixing_heewing_attitude_plane_enter(void);
+extern void control_mixing_heewing_nav_enter(void);
+extern void control_mixing_heewing_nav_run(void);
+
+#endif // CONTROL_MIXING_HEEWING_H