Hovercraft-Style Drone Concept — Feasible in ArduPilot?

Hi everyone,

I’m currently exploring a custom multirotor concept and before I start modifying firmware, I’d like to understand whether what I want to do is realistically achievable within ArduPilot’s current architecture.

The vehicle would basically be a quadcopter that behaves more like an airborne hovercraft rather than a conventional drone.

Configuration:

  • 4 vertical motors in a standard X quad layout
  • 1 additional horizontal motor dedicated only to forward propulsion

The idea is:

  • the 4 vertical motors handle lift + stabilization only

  • the aircraft remains level during forward motion

  • forward movement comes exclusively from the 5th horizontal motor

  • the pilot only controls:

    • yaw
    • forward thrust (proportional throttle)
    • Altitude
  • pilot pitch and roll inputs would be disabled/restricted

So unlike a normal drone, I do NOT want the vehicle to tilt intentionally in order to move forward.

My main questions are:

  1. Is this concept feasible using the existing ArduPilot Copter architecture?

  2. Would this require:

    • only parameter/configuration changes,
    • a custom mixer/frame definition,
    • Lua scripting,
    • or an entirely new/custom flight mode?
  3. Can the existing attitude controller continuously compensate for the pitch moment introduced by the horizontal thrust motor while still maintaining stable hover?

  4. Would modes like Stabilize or AltHold be a better starting point than Loiter/PosHold, considering translational movement would not come from tilt?

  5. Is there already a mechanism in ArduPilot to suppress pilot pitch/roll commands while still allowing the FC to fully control pitch/roll stabilization internally?

Mechanically, I understand the horizontal thrust motor should be aligned as closely as possible with the vehicle’s CG to minimize pitch torque, but I’d like to know whether this overall control approach is considered practical and how difficult it would be from the ArduPilot side.

Has anyone attempted something similar before, or are there existing frame types/control strategies that could serve as a starting point?

Any guidance would be greatly appreciated.

If you’re using quad+ propulsion, I think you could achieve this using AP_MotorsMatrix_6DoF_Scripting.cpp called by Lua script.

The following was created by Gemini.
I used it in Japanese. Therefore, the following is the translation from that time.

My oder:
A Luascript that calls AP_MotorsMatrix_6DoF_Scripting, supporting 4 motors + 2 thrusters.

Result:

– 6DoF mixing script for a 4-main rotor + 2-thruster configuration
– FRAME_CLASS = 16 (Scripting) must be set.

local num_motors = 6

– Argument configuration for Motors_6DoF:add_motor:
– (motor_num, roll, pitch, yaw, throttle, forward, right, reversible, testing_order)
– \*Each factor, ranging from -1.0 to 1.0, specifies how much of the output to that axis is blended.

gcs:send_text(6, “6DoF Scripting: Initializing Custom Matrix…”)

– 1. Main Rotor (Assuming Quad X configuration)
– Responsible for vertical thrust (Throttle) and attitude control (Roll, Pitch, Yaw).
– Contribution to direct translation (Forward, Right) should be set to 0.

– Motor 1: Front Right (Counterclockwise - CCW)Motors_6DoF:
add_motor(0, -0.5, 0.5, 0.5, 1.0, 0.0, 0.0, false, 1)

– Motor 2: Rear Left (Counterclockwise - CCW)Motors_6DoF:
add_motor(1, 0.5, -0.5, 0.5, 1.0, 0.0, 0.0, false, 2)

– Motor 3: Front Left (Clockwise - CW)Motors_6DoF:
add_motor(2, 0.5, 0.5, -0.5, 1.0, 0.0, 0.0, false, 3)

– Motor 4: Rear Right (Clockwise - CW)Motors_6DoF
:add_motor(3, -0.5, -0.5, -0.5, 1.0, 0.0, 0.0, false, 4)

– 2. Dedicated Thrusters (2 units)
– These are responsible for direct forward and rightward thrust along the aircraft’s axis.
– Since a reversible ESC (bidirectional drive) is assumed, set reversible to true.
– \*To prevent interference with throttle and attitude angle (R/P/Y) control, set those factors to 0.

– Motor 5: Main thruster for forward and backward propulsion (located at the rear of the aircraft, generating forward thrust)
– Assign 1.0 only to the vertical axis control (Forward). Motors_6DoF:
add_motor(4, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, true, 5)

– Motor 6: Side thruster for lateral movement (located near the aircraft’s center of gravity, generating thrust to the right)
– Assign 1.0 only to lateral axis control (Right). Motors_6DoF:
add_motor(5, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, true, 6)

– Initialize and confirm the mixing matrix
assert(Motors_6DoF:init(num_motors), “6DoF Scripting: Failed to initialize matrix!”)

– Customize the frame string displayed on the flight controller (GCS) motors:
set_frame_string(“Quad + 2 Thruster 6DoF”)
gcs:send_text(6, “6DoF Scripting: Matrix successfully loaded!”)

Thank you, this is really helpful.

I actually tried to adapt the same idea for my setup (4 lift motors + 1 forward thruster), but something is not working correctly and I’m still debugging it.

In my case I want:

  • quad motors = lift + stabilization + yaw

  • motor 5 = forward thrust only

I tried doing it through Lua scripting using AP_MotorsMatrix_6DoF_Scripting, and the matrix seems to initialize correctly, but I’m not sure whether the issue is:

  • RC interception

  • set_forward()

  • channel override logic

  • or my motor mapping itself.

Here’s the script I’m currently testing:

-- ==========================================================
-- 4+1 HOVERCRAFT MOTOR MIXER & CONTROL SCRIPT
-- Configuration: Quad-X Lift + 1 Forward Thruster
-- ==========================================================

-- ----------------------------------------------------------
-- PART 1: 6DoF MATRIX INITIALIZATION
-- ----------------------------------------------------------
local num_motors = 5
gcs:send_text(6, "Hovercraft: Initializing Custom Matrix...")

-- Main Lift Rotors (Standard Quad X mapping)
Motors_6DoF:add_motor(0, -0.5,  0.5,  0.5, 1.0, 0.0, 0.0, false, 1)
Motors_6DoF:add_motor(1,  0.5, -0.5,  0.5, 1.0, 0.0, 0.0, false, 2)
Motors_6DoF:add_motor(2,  0.5,  0.5, -0.5, 1.0, 0.0, 0.0, false, 3)
Motors_6DoF:add_motor(3, -0.5, -0.5, -0.5, 1.0, 0.0, 0.0, false, 4)

-- Forward Thruster (Motor 5)
Motors_6DoF:add_motor(4,  0.0,  0.0,  0.0, 0.0, 1.0, 0.0, false, 5)

assert(Motors_6DoF:init(num_motors), "Hovercraft: Failed to initialize matrix!")
motors:set_frame_string("Hovercraft 4+1")
gcs:send_text(6, "Hovercraft: Matrix Loaded Successfully!")

-- ----------------------------------------------------------
-- PART 2: ACTIVE CONTROL LOOP (RC INTERCEPTION)
-- ----------------------------------------------------------
local initialized = false
local start_time = millis()
local roll_chan = nil
local pitch_chan = nil

function update_control()
    if not initialized then
        if (millis() - start_time) < 5000 then
            return update_control, 500
        end
        initialized = true
        gcs:send_text(6, "Hovercraft: Handshake finished, starting control...")
    end

    if not roll_chan then roll_chan = rc:get_channel(1) end
    if not pitch_chan then pitch_chan = rc:get_channel(2) end

    if not roll_chan or not pitch_chan then
        return update_control, 1000
    end

    local success, raw_pitch = pcall(function() return pitch_chan:get_radio_in() end)

    if not success then
        success, raw_pitch = pcall(function() return pitch_chan:get_pwm() end)
    end

    if not success or not raw_pitch or raw_pitch == 0 then
        return update_control, 1000
    end

    roll_chan:set_override(1500)

    local forward_cmd = 0.0
    local pitch_override = 1500
    local forward_threshold = 1550
    local braking_threshold = 1450

    if raw_pitch > forward_threshold then
        forward_cmd = (raw_pitch - forward_threshold) / 450.0
        if forward_cmd > 1.0 then forward_cmd = 1.0 end
        pitch_override = 1500
    elseif raw_pitch < braking_threshold then
        forward_cmd = 0.0
        pitch_override = raw_pitch
    else
        forward_cmd = 0.0
        pitch_override = 1500
    end

    gcs:send_text(6, "Motor5 Output: " .. tostring(forward_cmd))

    motors:set_forward(forward_cmd)
    pitch_chan:set_override(pitch_override)

    return update_control, 20
end

return update_control()

If you notice something obviously wrong with the logic or with how I’m using the 6DoF scripting API, I’d really appreciate the feedback.

What about Arduplane quadplane? 4+1 configuration. The Q_ASSIST parameter will keep the 4 quad motors running during forward flight, or in quad mode you can set the thrust motor to help provide forward thrust so it doesn’t want to tilt. Might not be exactly what you’re looking for but there might be enough basis there for you to start from without re-inventing the whole system.

1 Like

If you just want a pilot-controlled proof of concept, configure as a bone stock Quad X. Put your forward thrust motor on an RC passthrough channel. That would allow hovercraft-like operation in ALT HOLD with a little transmitter mixing (like putting pitch and roll on channels fixed at a trim value and not assigned to the sticks). Yaw control could easily still be differential torque from the 4 main motors.

That would not allow for anything more assisted, as it would just start using pitch/roll for x/y motion control (so no LOITER, AUTO, GUIDED, etc).

1 Like

This gives you the option to use the thrusters or not.

– For 6DoF aircraft: Script for switching between normal flight and independent translational flight using RC10
– Requires use with AP_MotorsMatrix_6DOF_Script

local RC_CH_ROLL = 1 – Roll input channel
local RC_CH_PITCH = 2 – Pitch input channel
local RC_CH_SWITCH = 10 – Mode switching channel (RC10)

local PWM_THRESHOLD = 1500 – Switching threshold PWM

– Initialize 6DoF control object
local motors_6df = motors_6df
if not motors_6df then
    gcs:send_text(0, “6DoF: motors_6df object not found!”)
    return
end
gcs:send_text(6, “6DoF: Thruster Switch Script Loaded”)

– Variable to remember the previous mode state (for limiting the frequency of logging to GCS)
local last_mode = -1

function update()
    – Check the validity of the motor object
    if not motors:dynamic_arrays_match() then
        return update, 1000
    end
    – Get the PWM value of RC10
    local sw_pwm = rc:get_pwm(RC_CH_SWITCH)
    if not sw_pwm then
        return update, 100 – If RC input cannot be obtained, fast retry
    end
    – Determine the state of the switch (1: Normal mode, 2: Translation mode)
    local current_mode = 1
    if sw_pwm > PWM_THRESHOLD then
        current_mode = 2
    end
    if current_mode == 1 then
        – 1. Normal Flight Mode
        if last_mode \~= 1 then
            gcs:send_text(6, “6DoF Mode: Standard (Tilt to Move)”)
            last_mode = 1
        end
        -Set independent thruster output to zero (operate as a normal multicopter)
        motors_6df:set_forward_offset(0.0)
        motors_6df:set_lateral_offset(0.0)
 else
        – 2. Independent Translation Mode
        if last_mode \~= 2 then
            gcs:send_text(6, “6DoF Mode: Translation (Keep Horizontal)”)
            last_mode = 2
        end
        -Get pilot stick input (-1.0 to 1.0)
        local roll_input = rc:get_input_pwm(RC_CH_ROLL)
        local pitch_input = rc:get_input_pwm(RC_CH_PITCH)
        -Calculate and apply the thruster offset command value
        -※ Adjust the sign (-1.0) according to the aircraft’s thruster orientation and mixing.
        local fwd_output = pitch_input
        local lat_output = roll_input
        motors_6df:set_forward_offset(fwd_output)
        motors_6df:set_lateral_offset(lat_output)
        – \[Important\] Processing to maintain the aircraft level during translational mode
        – Disables tilt requests from pilot input to the attitude controller (maintains level)
        – \*Depending on the specifications of the 6DoF script and the flight mode (LOITER/ALT_HOLD),
        – If necessary, insert attitude_control:set_pitch_target_to_current_rate etc. here,
        – Maintain aircraft level using the 6DoF gain on the parameter side.
    end
    return update, 20 – Loop execution at 50Hz
end
return update()

Hi Allister. That was my first approach but the Vtol didn’t work, buecause the 4 motors were cutting power when the horizontal thrus was enable

Okay, I’ll try this approach. I thought that by adding the 5th motor as a pass through, the drone would see its movement as a disturbance rather than as an intentional/premeditated movement. Thanks!

It will. Just like wind… Hence the suggestion for AltHold.