Bluerov2, Waterlinked & QGis

Hello everybody!

I just wanted to share with you our other day mission.

We were filming the correct placement of weights in a port. We tried the Waterlinked positioning system with QGis, an app that lets you visualize go-referenced images.

The result? The rov getting video with the position visualized over the plan of the port: Bluerov2, Waterlinked & QGis - YouTube

Note: sorry about the video frame rate. I had to reduce it at 15fps because the computer was not powerful enough.

To export the position I used the Waterlinked python examples to create an NMEA sentence. After I used socat to create a virtual port that QGis could easily read.
If anybody is interested in the python script let me know! :slight_smile:



Nice work. How stable was the software?

Hello @Dale_A
What part of the software are you talking about? The position reception of the system?
Actually it is pretty stable. It is true that sometimes the position can be lost for a few seconds but as you can see on the video we did not get issues.
However we did not test the 100m. The ROV was about 50 meters for the longest distance.

Here is a photograph of the location:


The QGIS water linked positioning. If it’s pretty stable I’d be interested in having a further chat about your setup. ( I have a couple of harbour jobs that might become regular runs)

No problem. What are your questions? Do you need an email?

Alexis - awesome! Nice work! This looks really good and shows off the system capabilities well.

For reference, do you know what the computer you were using was that you had to reduce to 15fps? That would be helpful to know to help us make sure we have accurate minimum system requirements.


What’s the application with the BROV2 icon?


The system works perfectly with no lag when using the water positioning system, QGis and video recording from QGroundControl. It is only when in addition of all that I make a double screen record at 24 fps that the GQC stops streaming video fluently (it lags a little every 4-5 seconds and we prefer to have a perfect control of the ROV).
The computer I use is a Getac running with:

  • Ubuntu 16.04
  • Intel® Core™ i5-4300U CPU @ 1.90GHz × 4
  • Intel® Haswell Mobile
  • 8Gb RAM

I actually switched to Linux because we had lot more of performance issues (and software bugs) with windows.

It is an app I wrote in Python to start all the process we may use during missions. It looks like like this:

You basically have some fields for variables that may usually change and some toggles to activate and deactivate functions.
At the beginning of a mission we press start and everything setups automatically. We discovered that we lost a lot of time searching for all the functionalities so this makes things easier, faster and more organized. Some of the things it does:

  • Executes different apps simultaneously.
  • Windows placement and resizing.
  • Creates a directory with the day an hour of the mission where all files (from QGC, NMEA logs, screen captures, etc) are saved.
  • Mouse bigger resizing. (It may seem stupid but actually in the boat on the sea with the sun it was hard to find it sometimes).

To configure it you have to complete a python file that looks like this:

from collections import OrderedDict
from ui import *
from output_NMEA import process_position
from input_NMEA import input_position
from executing import *
import os

def start():
def end():

fields = OrderedDict([("Path to QGC .appImage:", "/home/id-ocean/Documents/QGroundControl/QGroundControl.AppImage"),
                    ("Path to VLC .sdp file:", "/home/id-ocean/Documents/QGroundControl/secondStream.sdp"),
                    ("Path to save files:", "/home/id-ocean/Documents/SortiesROV/last"),
                    ("Serial output:", "/dev/ttyS9"),
                    ("Serial input:", "/dev/ttyUSB0")])

toggles = OrderedDict([("QGroundControl:",
                        Toggle(value=True, method=execute,
                            args=["${Path to QGC .appImage:}"],
                            kwargs={'shell':True, 'size':['0', '0', '40', '70'], 'description':"QGroundControl"})),
                    ("Second Window",
                        Toggle(value=False, method=execute,
                            args=["vlc ${Path to VLC .sdp file:}"],
                            kwargs={"size":['50', '0', '100', '100']})),
                    ("Screen Record",
                        Toggle(value=False, method=execute,
                            args=['simplescreenrecorder --start-hidden'],
                            kwargs={"beforeTerminate" : ctrlR, "afterLaunch" : ctrlR})),
                    ("Output NMEA",
                        Toggle(value=False, method=process_position,
                            kwargs={'serial_name':'${Serial output:}',
                                'path':'${Path to save files:}',
                                'GLOBAL': True})),
                    ("Input NMEA",
                        Toggle(value=False, method=input_position,
                            args=['', '${Serial input:}'])),
                        Toggle(value=False, method=execute,
                        Toggle(value=True, method=execute,
                            args=['/usr/bin/google-chrome-stable --new-window %U'],
                            kwargs={'size':['0', '0', '100', '100'], 'description':"Google"}))])

if __name__ == '__main__':
    iconPath = os.path.dirname(os.path.abspath(__file__)) + '/icon.gif'
    MainWindow(fields, toggles, icon=iconPath, startMethod=start, endMethod=end, title='BlueROV Software Setup')

I have created some documentation in french but I you consider it could be useful for other teams I could maybe create a public repository or similar with the complete code and more extensive doc in English.



Hi Alexis,

Thank you very much for your help.
Could you send me the Batch file to create serial virtual port for Python.?
I thank you in advance.

Best regards,


Hello @Geraud

I am just going to do a pull request to complete the script with the virtual port integration.

Here is what the python script looks like:

Read position from Water Linked Underwater GPS convert to NMEA and send to serial
port or on UDP socket
from __future__ import print_function
import requests
import argparse
import json
import time
from math import floor
import socket
import serial
import sys
import subprocess
import os

def get_data(url, stderr=False):
        r = requests.get(url, timeout=0.2)
    except requests.exceptions.RequestException as exc:
        if stderr:
            print("Exception occured {}".format(exc))
        return None

    if r.status_code !=
        if stderr:
            print("Got error {}: {}".format(r.status_code, r.text))
        return None

    return r.json()

def get_global_position(base_url):
    return get_data("{}/api/v1/position/global".format(base_url))

def get_master_position(base_url):
    return get_data("{}/api/v1/position/master".format(base_url))

def gen_gga(time_t, lat, lng, fix_quality, num_sats, hdop, alt_m, geoidal_sep_m, dgps_age_sec=None, dgps_ref_id=None):
    # Code is adapted from
    hhmmssss = '%02d%02d%02d%s' % (time_t.tm_hour, time_t.tm_min, time_t.tm_sec, '.%02d' if 0 != 0 else '')

    lat_abs = abs(lat)
    lat_deg = lat_abs
    lat_min = (lat_abs - floor(lat_deg)) * 60
    lat_sec = round((lat_min - floor(lat_min)) * 1000)
    lat_pole_prime = 'S' if lat < 0 else 'N'
    lat_format = '%02d%02d.%03d' % (lat_deg, lat_min, lat_sec)

    lng_abs = abs(lng)
    lng_deg = lng_abs
    lng_min = (lng_abs - floor(lng_deg)) * 60
    lng_sec = round((lng_min - floor(lng_min)) * 1000)
    lng_pole_prime = 'W' if lng < 0 else 'E'
    lng_format = '%03d%02d.%03d' % (lng_deg, lng_min, lng_sec)

    dgps_format = '%s,%s' % ('%.1f' % dgps_age_sec if dgps_age_sec is not None else '', '%04d' % dgps_ref_id if dgps_ref_id is not None else '')

    result = 'GPGGA,%s,%s,%s,%s,%s,%d,%02d,%.1f,%.1f,M,%.1f,M,%s' % (hhmmssss, lat_format, lat_pole_prime, lng_format, lng_pole_prime, fix_quality, num_sats, hdop, alt_m, geoidal_sep_m, dgps_format)
    crc = 0
    for c in result:
        crc = crc ^ ord(c)
    crc = crc & 0xFF

    return '$%s*%0.2X' % (result, crc)

def send_udp(sock, ip, port, message):
    sock.sendto(message, (ip, port))

#Create a virtual port in the computer. Valid for linux with socat installed.
class VirtualPort:
    def __init__(self, password, origin='/dev/ttyS8', end='/dev/ttyS9'):
        self.origin = origin
        self.p = subprocess.Popen(['/bin/bash', '-c', 'echo %s|sudo -S sudo socat PTY,link=%s PTY,link=%s' % (password, self.origin, self.end)], preexec_fn=os.setpgrp)
        time.sleep(1) #Wait for process to start['/bin/bash', '-c', 'echo %s|sudo -S sudo chmod 666 %s' % (password, self.origin)])['/bin/bash', '-c', 'echo %s|sudo -S sudo chmod 666 %s' % (password, self.end)])

    def stop(self):
        os.killpg(, signal.SIGTERM)

    def write(self, message):
        message = "echo \"" + message + "\" > " + self.origin["/bin/bash", "-c", message])

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('-u', '--url', help='IP/URL of Underwater GPS kit. Typically', type=str, default='')
    parser.add_argument('-m', '--master', help='Print master position insstead of global', action="store_true")
    parser.add_argument("-v", "--verbose", help="Print NMEA sentences", action="store_true")
    # UDP options
    parser.add_argument('-i', '--ip', help="Enable UDP output by specifying IP address to send UDP packets. Default disabled", type=str, default='')
    parser.add_argument('-p', '--port', help="Port to send UDP packet", type=int, default=5000)
    # Serial port options
    parser.add_argument('-s', '--serial', help="Enable serial port output by specifying port to use. Example: '/dev/ttyUSB0' or 'COM1' Default disabled", type=str, default='')
    parser.add_argument('-b', '--baud', help="Serial port baud rate", type=int, default=9600)
    #Virtual port options
    parser.add_argument('-w', '--password', help='Password to execute sudo commands', type=str, default='')
    parser.add_argument('-o', '--origin', help='Virtual port to write in', type=str, default='/dev/ttyS7')
    parser.add_argument('-e', '--end', help='Virtual port where to read from', type=str, default='/dev/ttyS9')
    args = parser.parse_args()

    if not (args.ip or args.serial or args.password):
        print("ERROR: Please specify either serial port to use, ip address to use or a password to create a virtual port")

    print("Using base_url: {}".format(args.url))

    ser = None
    if args.serial:
        print("Serial port: {}".format(args.serial))
        ser = serial.Serial(args.serial, args.baud)

    sock = None
    if args.ip:
        print("UDP: {} {}".format(args.ip, args.port))
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    virtualPort = None
    if args.password:
        print("Sending data from virtual port '{}' to '{}'".format(args.origin, args.end))
        virtualPort = VirtualPort(args.password, args.origin, args.end)

    while True:
        if args.master:
            pos = get_master_position(args.url)
            pos = get_global_position(args.url)

        if pos:
            #print("Current global position lat:{} lon:{}".format(pos["lat"], pos["lon"]))
            sentence = gen_gga(time.gmtime(), pos["lat"], pos["lon"], 1, 0, 0, 0, 0) #added 1 argument to fixed quality or position was rejected by other software
            if args.verbose:
            if sock:
                send_udp(sock, args.ip, args.port, sentence)
            if ser:
                ser.write(sentence + "\n")
            if virtualPort:
                #Add \ to make the simbol $ writable
                dollar_sentence = '\\' + sentence

if __name__ == "__main__":

More specifically responding to your question, to create a virtual port I used socat.
To install it in linux write in a terminal:
$ sudo apt-get update && sudo apt-get install socat

And after to create two connected virtual ports write:
$ sudo socat PTY PTY

You can check the creation of two virtual ports by running from another terminal:
$ ls /dev/pts*
Before and after executing socat. You will see the apparition of two ports, in my case 4 and 18:

My app only reads from ttyS_X ports so I created a link with:
$ sudo socat PTY,link=/dev/ttyS8 PTY,link=/dev/ttyS9

Finally you have to edit permissions to write and read in the ports so run from a second terminal:
$ sudo chmod 666 /dev/ttyS8
$ sudo chmod 666 /dev/ttyS9

You can check the communication by writing on this second terminal:
$ cat < /dev/ttyS9
and on a third new one:
$ echo "Hello World" > /dev/ttyS8

Note: this is also necessary even if you do not create the link. If you use the pts/X ports you also have to edit permisions to write or read:
$ sudo chmod 666 /dev/pts/18
$ sudo chmod 666 /dev/pts/4

Note2: You must keep open the socat terminal to maintain communication. When you close it or press ctrl+c the process is interrupted and the virtual ports destroyed.




Although it has been a while since this has been posted I would like to follow up on this. I am currently trying to implement a plugin of the waterlinked underwater GPS into QGIS on a windows system.
I aim to use a virtual port setup as is used in this case, however I do not get a connection to the GPS system through QGIS (created a virtual COM13). The attempt is terminated before a connection is established.
Since you indicated that you did have a setup for windows, although with some stability issues. Could you indicate how you managed to create this connection?

Kind regards,

Hello Alex how are you
I m Alain Durot how i can get the python script for get NMEA for waterlinked