Cannot arm or control a navigator-based ROV via pymavlink

Hi, I’m building a three-thruster ROV with BlueRobotics products. I am using the navigator + Raspberry Pi 4B for flight control. The goal is the robot will execute some predefined patterns autonomously, such as a circle or a square. But I’m stuck at the very beginning. I cannot control the ROV with pymavlink.

I’m running the BlueOS ROS1 extension, provided in this docker container.

To communicate with the autopilot, I created a UDP client (udp:localhost:14550) from BlueOS web UI. Please see the image below. I am not sure if a new client was necessary, or I could just use the default MAVLink2Rest client (port 14000).

Then I’m trying this code from inside the Docker container:

from pymavlink import mavutil
import time
import threading

# Connect to autopilot via localhost port 14550
master = mavutil.mavlink_connection('udp:localhost:14550', source_system=250)

# Wait for heartbeat
master.wait_heartbeat() # <- I'm receiving heartbeat perfectly fine
print(f"Connected to system {master.target_system}, component {master.target_component}")

# Start heartbeat thread
def send_heartbeat():
    while True:
        master.mav.heartbeat_send(
            mavutil.mavlink.MAV_TYPE_GCS,
            mavutil.mavlink.MAV_AUTOPILOT_INVALID,
            0, 0, 0
        )
        time.sleep(1)

# I did not use this thread initially, 
# GPT suggested that continuous heartbeat sending is necessary. 
# So I gave it a try. But no luck!
threading.Thread(target=send_heartbeat, daemon=True).start()

time.sleep(2)  # Let heartbeat settle

# Set mode to MANUAL
master.set_mode('MANUAL')
time.sleep(1)

# Arm motors
master.arducopter_arm()
print("Arm command sent")
time.sleep(2)

# Check arming state
print("Motors armed?", master.motors_armed()) # <- always returns 0 

# Manual Thruster Command

# Example: forward surge only
for _ in range(20):  # send 20 times to keep signal alive
    master.mav.manual_control_send(
        master.target_system,
        100,    # x: forward
        0,      # y: lateral
        500,    # z: throttle (range 0-1000)
        0,      # r: yaw
        0       # buttons
    )
    time.sleep(0.5)

print("Manual control command sent")


master.arducopter_disarm()
print("Disarm command sent")

Observation vs expectation:

  1. I am receiving heartbeat. I am receiving all rostopics. When I send the arm command, I can see “Armed” appears on BlueOS web UI. But if I check using master.motors_armed(), it always returns false. I expected it to be true.

  2. Since arming is not working, I cannot control the thrusters either. I tried both master.command_long_send() and master.manual_control_send(), but don’t see any response from the thrusters.

Possible reasons:
Am I using the UDP client in the wrong way?
Is Docker creating a communication barrier and preventing the commands from reaching autopilot?
Did I miss any initial configuration?

Any help is appreciated!

Reason that I can discard:
I can arm/move the ROV with a joystick via QGroundControl. So I’m pretty sure hardware and connections are correct. It’s just some network issue that I cannot figure out.