BlueROV2 Python control script not working

Following on from this thread, I have updated the BlueOS and ArduSub versions, but now the keyboard controls through a python script does not work at all. How can I control the ROV through a python script?

Hi @atanu585,

I’ve moved your comment to a new thread because it’s not related to your previous issue of some of the thrusters not working.

It’s very hard to help without even knowing what kind of control commands you’re sending to the autopilot. Are you able to share the script you’re using?

Assuming you’ve already confirmed your script is establishing a MAVLink connection to the autopilot, common issues to check would be that you’re sending regular heartbeats and control inputs (or have disabled the relevant failsafes), and making sure you’re arming the vehicle before trying to command it to move.

Hi @EliotBR ,
Thank you for creating this new thread. Here is the script I am using for controlling the ROV:

from pymavlink import mavutil
import keyboard
import cv2
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd  
import datetime
import time

import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst

class Video():
    def __init__(self, port=5600):
        Gst.init(None)
        self.port = port
        self.latest_frame = self._new_frame = None

        # [Software component diagram](https://www.ardusub.com/software/components.html)
        # UDP video stream (:5600)
        self.video_source = 'udpsrc port={}'.format(self.port)

        # [Rasp raw image](http://picamera.readthedocs.io/en/release-0.7/recipes2.html#raw-image-capture-yuv-format)
        # Cam -> CSI-2 -> H264 Raw (YUV 4-4-4 (12bits) I420)
        self.video_codec = '! application/x-rtp, payload=96 ! rtph264depay ! h264parse ! avdec_h264'

        # Python don't have nibble, convert YUV nibbles (4-4-4) to OpenCV standard BGR bytes (8-8-8)
        self.video_decode = \
            '! decodebin ! videoconvert ! video/x-raw,format=(string)BGR ! videoconvert'

        # Create a sink to get data
        self.video_sink_conf = \
            '! appsink emit-signals=true sync=false max-buffers=2 drop=true'

        self.video_pipe = None
        self.video_sink = None

        self.run()


    def start_gst(self, config=None):
        """ Start gstreamer pipeline and sink
        Pipeline description list e.g:
            [
                'videotestsrc ! decodebin', \
                '! videoconvert ! video/x-raw,format=(string)BGR ! videoconvert',
                '! appsink'
            ]

        Args:
            config (list, optional): Gstreamer pileline description list
        """
        if not config:
            config = \
                [
                    'videotestsrc ! decodebin',
                    '! videoconvert ! video/x-raw,format=(string)BGR ! videoconvert',
                    '! appsink'
                ]

        command = ' '.join(config)
        self.video_pipe = Gst.parse_launch(command)
        self.video_pipe.set_state(Gst.State.PLAYING)
        self.video_sink = self.video_pipe.get_by_name('appsink0')

    @staticmethod
    def gst_to_opencv(sample):
        """Transform byte array into np array

        Args:
            sample (TYPE): Description

        Returns:
            TYPE: Description
        """
        buf = sample.get_buffer()
        caps_structure = sample.get_caps().get_structure(0)
        array = np.ndarray(
            (
                caps_structure.get_value('height'),
                caps_structure.get_value('width'),
                3
            ),
            buffer=buf.extract_dup(0, buf.get_size()), dtype=np.uint8)
        return array

    def frame(self):
        """ Get Frame

        Returns:
            np.ndarray: latest retrieved image frame
        """
        if self.frame_available:
            self.latest_frame = self._new_frame
            # reset to indicate latest frame has been 'consumed'
            self._new_frame = None

        return self.latest_frame

    def frame_available(self):
        """Check if a new frame is available

        Returns:
            bool: true if a new frame is available
        """
        return self._new_frame is not None

    def run(self):
        """ Get frame to update _new_frame
        """
        self.start_gst(
            [
                self.video_source,
                self.video_codec,
                self.video_decode,
                self.video_sink_conf
            ])

        self.video_sink.connect('new-sample', self.callback)

    def callback(self, sink):
        sample = sink.emit('pull-sample')
        self._new_frame = self.gst_to_opencv(sample)

        return Gst.FlowReturn.OK


############################################################## function to send RC values

def set_rc_channel_pwm(Channel_id, pwm=1500):
    if Channel_id < 0 or Channel_id > 18:
        print("Channel does not exist.")
        return

    if Channel_id < 10:
        rc_channel_values = [65535 for _ in range(10)]
        rc_channel_values[Channel_id - 1] = pwm
        master.mav.rc_channels_override_send(
            master.target_system,  # target_system
            master.target_component,  # target_component
            *rc_channel_values)  # RC channel list, in microseconds.


############################################################## function to send camera tilt angle

def look_at(tilt, roll=0, pan=0):
    print(f"Tilt angle is set as: {tilt_angle / 100} degrees")
    master.mav.command_long_send(
        master.target_system,
        master.target_component,
        mavutil.mavlink.MAV_CMD_DO_MOUNT_CONTROL,
        1,
        tilt,
        roll,
        pan,
        0, 0, 0,
        mavutil.mavlink.MAV_MOUNT_MODE_MAVLINK_TARGETING)


########################################Control##########################################################

class Control:
    def __init__(self):
        self.backward_start_time = None  # Initialize backward start time
        self.light = 1100
        self.mode = 'manual'
        self.state = 'CONTROL'

    def set_control_mode(self):
        k_manual_mode = keyboard.is_pressed('ctrl+m')
        k_auto_mode = keyboard.is_pressed('ctrl+t')
        if k_manual_mode:
            self.mode = 'manual'

    def stop_moving(self):
        set_rc_channel_pwm(3, 1500)
        set_rc_channel_pwm(4, 1500)
        set_rc_channel_pwm(5, 1500)
        set_rc_channel_pwm(6, 1500)
        set_rc_channel_pwm(9, 1600)
        self.state = 'STOP'

    def manual_control(self):
        k_w = keyboard.is_pressed('w')
        k_s = keyboard.is_pressed('s')
        k_a = keyboard.is_pressed('a')
        k_d = keyboard.is_pressed('d')
        k_q = keyboard.is_pressed('q')
        k_e = keyboard.is_pressed('e')
        k_up = keyboard.is_pressed('up')
        k_down = keyboard.is_pressed('down')
        k_plus = keyboard.is_pressed('+')
        k_minus = keyboard.is_pressed('-')

        if k_w:
            channel_n = 5
            set_rc_channel_pwm(channel_n, 1580)
            print('w')
        elif k_s:
            channel_n = 5
            set_rc_channel_pwm(channel_n, 1450)
            print('s')
        else:
            set_rc_channel_pwm(5, 1500)

        if k_a:
            channel_n = 6
            set_rc_channel_pwm(channel_n, 1450)
            print('a')
        elif k_d:
            channel_n = 6
            set_rc_channel_pwm(channel_n, 1550)
            print('d')
        else:
            set_rc_channel_pwm(6, 1500)

        if k_q:
            channel_n = 4
            set_rc_channel_pwm(channel_n, 1450)
            print('q')
        elif k_e:
            channel_n = 4
            set_rc_channel_pwm(channel_n, 1550)
            print('e')
        else:
            set_rc_channel_pwm(4, 1500)

        if k_up:
            channel_n = 3
            set_rc_channel_pwm(channel_n, 1550)
            print('up')
        elif k_down:
            channel_n = 3
            set_rc_channel_pwm(channel_n, 1450)
            print('down')
        else:
            set_rc_channel_pwm(3, 1500)

        if k_plus:
            channel_n = 9
            self.light += 10
            self.light = min(self.light, 1900)
            set_rc_channel_pwm(channel_n, self.light)
            print(self.light)
        elif k_minus:
            channel_n = 9
            self.light -= 10
            self.light = max(self.light, 1100)
            set_rc_channel_pwm(channel_n, self.light)
            print(self.light)


#####################################################

if __name__ == '__main__':
    # Create the connection
    master = mavutil.mavlink_connection('udpin:192.168.2.1:14550')

    # Wait a heartbeat before sending commands
    master.wait_heartbeat()

    # Arm
    master.mav.command_long_send(
        master.target_system,
        master.target_component,
        mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
        0,
        1, 0, 0, 0, 0, 0, 0)

    # Initialize Video object
    video = Video()

    # Initialize Control object
    control = Control()

    # Initial tilt angle
    tilt_angle = -30 # in centidegrees
    look_at(tilt_angle\*100)

    # Generate filename with current time
    current_time = datetime.datetime.now().strftime("%Y%m%d\_%H%M%S")
    video_filename = f"output_{current_time}.avi"

    # Initialize VideoWriter object to save the video
    #fourcc = cv2.VideoWriter_fourcc(*'XVID')
    #out = cv2.VideoWriter(video_filename, fourcc, 15.0, (640, 480))

    detection_scores_log = []  # Log for detection scores

    print('Initialising stream...')
    waited = 0
    start_time = time.time()
    while not video.frame_available():
        waited += 1
        print('\r  Frame not available (x{})'.format(waited), end='')
        cv2.waitKey(30)

    print('\nSuccess!\nStarting streaming - press "Esc" to quit.')

    running = True
    while running:
        start_time = time.time()
        control.set_control_mode()

        if video.frame_available():
            frame = video.frame()
            # angle, ... = flange_detector.detect_and_draw(frame)  # 注释掉
            cv2.imshow('Camera', frame)
            #out.write(frame)

        # 给 loop 一个固定的频率,比如 10 Hz
        elapsed = time.time() - start_time
        sleep_time = max(0, (1/30) - elapsed)
        time.sleep(sleep_time)

        if control.mode == 'manual':
            control.manual_control()

        # Check for key presses to adjust tilt angle
        if keyboard.is_pressed('p'):
            tilt_angle -= 1000  # Increase tilt angle by 1 degree
            look_at(tilt_angle)
            print(f"Tilt angle increased to: {tilt_angle / 100} degrees")
        elif keyboard.is_pressed('l'):
            tilt_angle += 1000 # Decrease tilt angle by 1 degree
            look_at(tilt_angle)
            print(f"Tilt angle decreased to: {tilt_angle / 100} degrees")

        # Allow frame to display, and check if user wants to quit
        if cv2.waitKey(1) & 0xFF == 27: # ESC is 27
            running = False
            # control._log(save_plot=True, plot_filename=f"plot_{current_time}.png", csv_filename=f"logs_{current_time}.csv")

    # Properly shut down the GStreamer pipeline
    video.video_pipe.set_state(Gst.State.NULL)
    cv2.destroyAllWindows()
    # Release the VideoWriter object
    #out.release()

Hi @atanu585 -

Can you share what you’re trying to accomplish with your script? Are you trying to drive the vehicle via your keyboard? While also streaming video? Is this in an effort to make your own GCS (Ground Control Software) ?

If so, have you tried Cockpit? Driving with the keyboard has been accomplished by some users, and in general it is very easy to customize to your needs. Recording video and snapshots with metadata is also possible.