Is "ground steering" right for the boat?

I try to make my boat to steer itself using ArduPilot on Cube Orange, and I fail. The boat is 3.6m long. It is driven by an electric outboard motor steered by servo-motor-actuator. ArduPilot should change direction only. Speed is set manually and it is constant all the time - between 1.5 and 2 m/s.

Manually it works fine, I can drive by a straight line. In guided or auto mode, the boat starts “waving” left and right and does not go straight. The story is similar to this post. Those “waves” are not large but extremely uncomfortable if you are in the boat.

My concern is following: is “ground steering” the right steering mode for the boat? Logics of steering on the ground and on the water are very different. I guess it might be OK for a small boats, but as the size of the boat increases the problem increases too. For the large boat with high inertia, you have to stop turning well before the boat gets to the desired direction. You even have to start turning in the opposite direction to stop the turning inertia.

Are there any comments or suggestions on how to make the boat going on a straight line? Are there better modes of steering?

It sounds like you have thrust vectoring setup, have you configured your boat for thrust vectoring as described in the documentation?

Yes, this is a thrust vectoring setup as this is an outboard motor. However, I did not configured it. My understanding (possibly incorrect) from this page is that this configuration makes sense only for the system with throttle control, which I don’t have. So, the outboard motor works as a normal rudder.

To answer my question: No, ground steering is not good for a large slow boat with passengers. PID tuning is voodoo magic.

What is good for the large slow boat: “Yaw-Rate Control Law, often used in aircraft and advanced marine autopilots” as it was proposed and then implemented in Lua script by ChatGPT. The approach is to maintain comfortable turning rate 2-3 degrees per second, and be sure that turning rate goes to zero when the boat gets to the right direction.

It works bloody beautiful. Boat goes in straight lines form WP to WP. Hours of river cruising without a need to touch the control. Everything I wanted but could not get from PID steering. There was 45kg dog walking from one board to another and changing the boat direction by moving centre of mass. The script adjusted every time the dog moved.

Code is below. No tuning. Increase KP and KR if you need a more aggressive turning. Too high numbers might result in overshot and oscillating a little bit around the direction line.

I have zero experience with ArduPilot and Lua programming, so all copyrights go to ChatGPT.

-- Yaw-Rate Control Steering Script for ArduPilot (Boat or Rover)

-- --- External Parameters ---
local PARAM_TABLE_PREFIX = "ASCR_"
local PARAM_TABLE_KEY = 22

assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 10), "ASCR: could not add param table")

local rc_steering_channel = 1

-- --- Register and Initialize Parameters ---

local K_p_param = "ASCR_KP"       -- Heading error to target yaw rate gain
local K_r_param = "ASCR_KR"       -- Yaw rate error to rudder control gain
local rudder_limit_param = "ASCR_RUD_LIM"  -- Max rudder angle (degrees)
local max_yaw_rate_param = "ASCR_MAX_YR"   -- Max comfortable yaw rate (deg/s)
local servo_channel_param = "ASCR_SERVO_CH" -- Servo channel (0-based)
local alpha_param = "ASCR_ALPHA"  -- Low-pass filter smoothing factor
local pwm_threshold_param = "ASCR_PWM_DB"  -- Deadband threshold for PWM changes
local update_interval_param = "ASCR_UPD_INT"  -- Update interval in ms


param:add_param(PARAM_TABLE_KEY, 1, "KP", 0.15)
param:add_param(PARAM_TABLE_KEY, 2, "KR", 0.85)
param:add_param(PARAM_TABLE_KEY, 3, "RUD_LIM", 15)
param:add_param(PARAM_TABLE_KEY, 4, "MAX_YR", 2.0)
param:add_param(PARAM_TABLE_KEY, 5, "SERVO_CH", 0)
param:add_param(PARAM_TABLE_KEY, 6, "ALPHA", 0.2)
param:add_param(PARAM_TABLE_KEY, 7, "PWM_DB", 5)  -- Default 5us deadband
param:add_param(PARAM_TABLE_KEY, 8, "UPD_INT", 500)  -- Default 500ms update interval

-- MAVLink Message Helper
local function send_status(text)
    gcs:send_text(6, text)
end

send_status("##### Script loaded #####")

-- --- State for Low-Pass Filtering and PWM Tracking ---
local last_rudder_cmd = 0
local last_pwm_int = -1  -- Invalid initial value to ensure first write
local alpha = param:get(alpha_param) or 0.2  -- Load smoothing factor dynamically

-- --- Main Update Function ---
function update()
    -- Load new control parameters
    local pwm_threshold = param:get(pwm_threshold_param) or 5
    local update_interval = param:get(update_interval_param) or 100
  
    -- Load parameters dynamically
    local K_p = param:get(K_p_param)
    local K_r = param:get(K_r_param)
    local rudder_limit = param:get(rudder_limit_param)
    local max_comfortable_yaw_rate = param:get(max_yaw_rate_param)
    local servo_channel = math.floor(param:get(servo_channel_param) + 0.5)

    local servo_min = param:get(string.format("SERVO%d_MIN", servo_channel + 1))
    local servo_max = param:get(string.format("SERVO%d_MAX", servo_channel + 1))
    local servo_trim = param:get(string.format("SERVO%d_TRIM", servo_channel + 1))

    -- Only run in Auto or Guided mode
    local mode = vehicle:get_mode()
    if mode ~= 10 and mode ~= 15 then
		local rc_input_pwm = rc:get_pwm(rc_steering_channel) or servo_trim
		if last_pwm_int ~= rc_input_pwm then
			SRV_Channels:set_output_pwm_chan(servo_channel, rc_input_pwm)
			last_pwm_int = rc_input_pwm
		end
        return update, update_interval
    end

    -- Get Current Heading (degrees)
    local current_heading = ahrs:get_yaw() * 180 / math.pi
    if current_heading < 0 then
        current_heading = current_heading + 360
    end

    -- Get Desired Heading (from active waypoint)
    local target_heading = vehicle:get_wp_bearing_deg()
    if not target_heading then
        SRV_Channels:set_output_pwm_chan(servo_channel, servo_trim)
        return update, update_interval
    end

    -- Compute Heading Error [-180, 180]
    local heading_error = target_heading - current_heading
    heading_error = (heading_error + 180) % 360 - 180

    -- Compute Target Yaw Rate and Clamp
    local target_yaw_rate = K_p * heading_error
    target_yaw_rate = math.max(math.min(target_yaw_rate, max_comfortable_yaw_rate), -max_comfortable_yaw_rate)

    -- Current Yaw Rate from Gyro (deg/s)
    local gyro_z = ahrs:get_gyro():z()
    local current_yaw_rate = gyro_z * 180 / math.pi

    -- Rudder Command
    local yaw_rate_error = target_yaw_rate - current_yaw_rate
    local rudder_cmd = K_r * yaw_rate_error
    rudder_cmd = math.max(math.min(rudder_cmd, rudder_limit), -rudder_limit)

    -- Apply Low-Pass Filter to Rudder Command
    rudder_cmd = last_rudder_cmd + alpha * (rudder_cmd - last_rudder_cmd)
    last_rudder_cmd = rudder_cmd

    -- Map Rudder to PWM
    local pwm = servo_trim + rudder_cmd / rudder_limit * (servo_max - servo_trim)
    pwm = math.max(math.min(pwm, servo_max), servo_min)
    local pwm_int = math.floor(pwm + 0.5)

    -- Only send PWM if it changed
    if math.abs(pwm_int - last_pwm_int) >= pwm_threshold then
        SRV_Channels:set_output_pwm_chan(servo_channel, pwm_int)
        last_pwm_int = pwm_int
    end

    -- MAVLink Logging
    local msg = string.format("HDG_ERR: %.1f | T_YR: %.1f | C_YR: %.1f | RUD: %.1f | PWM: %d", 
                              heading_error, target_yaw_rate, current_yaw_rate, rudder_cmd, last_pwm_int)
    send_status(msg)

    return update, param:get(update_interval_param) or 100
end

return update, param:get(update_interval_param) or 100

the waving is just a sign of bad tuning, if you were just running out the box settings then I’m not surprised at all at that result. , for something that big you need to turn the pid and FF way down. stock pids are designed for small rovers.

I had that idea, so there was my question. I tried to make PID working, and I failed. PID tuning is voodoo magic. It should not be that difficult.
I am not alone. Gerry from parallel thread had the same problem, and his solution still does not so good.
I got many post with problems through the search but no solutions. So, I decided that custom script might be the way to go.

Your script is effectively just a proportional controller. It’s like zeroing FF, I, and D when tuning. There is no voodoo magic either in your script nor in the native tuning parameters.

So to answer your question, ground steering is the correct output for your boat, but you chose to make ChatGPT write a custom proportional controller because you didn’t understand how to tune.

I’m glad you got it working, and there’s probably no reason to change it now, but it’s unnecessary to go to these lengths.

1 Like

Dear Yuri, thank you for you input. I call PID tuning a voodoo magic precisely as I don’t understand how it works and I see no instructions to learn it. From what I see, tuning is just a guessing game. Unfortunately, quicktune script does not work for me - it keeps telling “should be armed and moving”, while the boat IS armed and moving.

Custom script is a crutch compared to a native application. However, it works without tuning. But it it limited to work only for some modes.

Geofrancis tells “way down” but does not tell how far down. You advise zeros for some parameters “when tuning” - what parameters when driving? What does it mean tuning?

I didn’t advise anything. I described what you did because it appears you don’t even understand the script you have.

1 Like

Could you please advise something for tuning big slow moving boat?

Are you not happy with your results?

No I can’t advise anything without a log.

As I said, custom script is always just a crutch. It makes nice driving but it is limited in its application. For example, I had to do that dirty trick to incorporate RC steering, ArduPilot keeps telling me to check config for steering and throttle and does not want to arm normally.

What log do you need? What kind of driving should I do to make it easier to see the situation?

The greatest help for me and for many others struggling with tuning would be an explanation of principles for PID driving and corresponding tuning. Otherwise it will stay a voodoo magic.