Tuesday, April 19, 2016

A Raspberry Pi Wireless NMEA-0183 Multiplexer (USB GPS, Digital Compass, and Sailboat Instrument Input)


Recommended Gear:


These are the exact devices that I ordered and am using. The big ticket items:
Smaller items that are necessary:

Step 1: Setup the Raspberry Pi


Once you purchase your Raspberry Pi, set it up by following their NOOBS guide here. I'm running this with a brand new fresh install of Raspbian Jessie on April 18th, 2016. Once you have booted up the Pi, connected your monitor/mouse/keyboard, and connected an ethernet (or connected to wifi), ensure you're software is all up to date by running these two commands from the Terminal (the "Terminal" is the program on the top bar with a logo like a black box. The Terminal will be your primary workhorse for this guide). Type the first row in and hit enter and let it run through. It may prompt you to type "Y" if you want to download the new programs. If you're really new, you should know that when you type in "sudo" before a command like this, it runs it in the "God Mode" on the Raspberry Pi and you can make important changes to the system.

sudo apt-get update
sudo apt-get upgrade

Once this is done, open up Menu->Preferences->Raspberry Pi Configuration, and change your hostname or password if you wish. But the one thing we're looking for is under the Interfaces tab; click I2C Enabled. I also recommend configuring your Localisation tab to your location. Once you're done, click OK and reboot.

Next, we're going to begin the long process of installing all the necessary software (these are all for the MPU-9250 Digital Compass--if you're not using that, then you probably don't need these except you might need python-dev).

sudo apt-get install i2c-tools
sudo apt-get install cmake
sudo apt-get install python-dev
sudo apt-get install octave

When those are finished installing, connect your MPU-9250 (see this post for more information. In fact, you can connect all your devices--see this one for the USB GPS and this one for the DST800 or any other NMEA instrument using the USB to RS-485 converters I linked to above). After connecting, verify the Raspberry Pi can actually "see" the MPU-9250 by running:

sudo i2cdetect -y 1

and you should see a 68 somewhere in the grid. If not, well... you might have to go to RichardsTech's website to troubleshoot. Let's assume it is working. Great.

These next few steps are going to come fast, so keep up. We make a working directoy called 'kts' to store everything and to stay organized (keep in mind if you use my scripts, your directoy must also be called kts (or you can just change the scripts to reflect your name)). After making the directory, we download the RTIMULib from github and install its calibration program.

mkdir kts
cd kts
git clone https://github.com/richards-tech/RTIMULib2.git
cd RTIMULib2/Linux/RTIMULibCal
make -j4
sudo make install

This installs the calibration program, which is 100% necessary for the compass to work. Again, if you get lost, just mosey on over to his github page, or my earlier and expanded writeup on the MPU-92450 with the Raspberry Pi. However, I cannot reiterate this enough: when you calibrate the compass, you MUST be working in the RTIMUEllipsoidFit folder--otherwise it WILL NOT WORK. Not only that, but there must be a copy of the RTIMUEllipsoidFit folder in the directory above the script that is using the MPU-9250. If you follow my directions, this won't be a problem.

So let's do that.

cp -r /home/pi/kts/RTIMULib2/RTEllipsoidFit/ /home/pi/kts/
cd /home/pi/kts/RTEllipsoidFit/
RTIMULibCal

When you're on your boat, and your compass is installed where it will be used (I unfortunately cannot help you with this part), then run the calibration program RTIMULibCal. It doesn't make sense to calibrate it anywhere other than its intended final location. This creates a RTIMULib.ini file in the RTEllipsoidFit folder, which you then copy over to the scripts folder. Wait, the scripts folder? Yes, let's make that and download all the scripts used for this project from my github page:

cd ..
git clone https://github.com/OldCC/scripts.git
cd scripts
cd ../RTEllipsoidFit/
cp RTIMULib.ini ../scripts/

Now we have all the programs installed, we have all the folders made, and we have all the scripts.

Bonus Step: Setup a Wifi AP on the Raspberry Pi 3 Model B


This step took forever, mainly because there's a hundred tutorials out there on how to set up a wifi router with the Pi. That's not what I want to do. I just want a wireless access point to connect a bunch of things to, with no internet. I don't need the internet (yet). Fortunately, this is super super super easy to do, thanks to this awesome script that you just run once, edit one file, reboot, and you're good. Finally.

But that script actually didn't work for me, so I modified it slightly by making it a little less user friendly (sorry). I included this script in the scripts folder, if you downloaded it from github above. Set up the apsetup.sh by

sudo chmod +x apsetup.sh
sudo nano apsetup.sh

and change PASSWORD and NAME to whatever you want for your network. Then Control + C to save and exit. Next, run that script as the root user by

sudo ./apsetup.sh

This will take a minute or two to run through everything. When it's finished, we need to make it run at startup all on its own. To do this, open this file:

sudo nano /etc/default/hostapd

and then turn this line here

#DAEMON_CONF=""

into this line (make note that I remove the the #)

DAEMON_CONF="/etc/hostapd/hostapd.conf"

Go ahead and reboot, and when it's back online, try to connect to your wifi network with your laptop or phone. If it connects, then you're good! But there won't be any internet. That's okay, because there will be NMEA-0183 data in just a little bit.

Bonus Interim Step: Assign Persistent Names for your USB Devices


If you will have more than one USB device plugged in, follow this section. If you only have one USB device plugged in, you'll have to change your

ser = serial.Serial('/dev/gps', 4800, timeout=5)

to something like

ser = serial.Serial('/dev/ttyUSB0', 4800, timeout=5)

but if you will have more than one USB device plugged in, proceed with this step.

I followed this guide here, which I will summarize below. First, figure out which USB ports are assigned to which ttyUSB* names. Typically, it'll be ttyUSB0, ttyUSB1, ttyUSB2 and then ttyUSB3. You can easily find this by typing in


ls /dev/tty*

then unplug a device, retype that command, and whichever /dev/ttyUSB* is missing, then that port is assigned that number. However, everytime you startup the Pi, it will randomly change. This makes it nearly impossible to keep consistent coding when you're trying to get serial information over a port that changes names. So we'll assign the port name based on it's physical position. How do we do this?

One port at a time. For USB0, run this command:


udevadm info --name=/dev/ttyUSB0 --attribute-walk 

and you'll get a bunch of info about that particular USB port. Look for the fourth section, or wherever it says this:

KERNELS=="1-1.3"

That means that the physical port my USB0 is plugged into (remember, we figured out what the USB0 is in the previous step--for me, it's my GPS), it's physical port 3 (from 1.3). If you get confused, follow the original guide for more detailed instructions. Write this down, and repeat for however many USB devices you have. For my RPi3, the ports go (left to right top row, then left to right bottom row) 2, 4, 3, 5.

To create a "rules" file to permanently assign those physical ports with a persistent (permanent) name we can use in our code, run this command:


sudo nano /etc/udev/rules.d/99-usb-serial.rules

and enter the information that follows, but with your appropriate numbers/titles:


KERNELS=="1-1.3", SUBSYSTEMS=="usb", SYMLINK+="gps"
KERNELS=="1-1.4", SUBSYSTEMS=="usb", SYMLINK+="dst"
KERNELS=="1-1.2", SUBSYSTEMS=="usb", SYMLINK+="rd"
KERNELS=="1-1.5", SUBSYSTEMS=="usb", SYMLINK+="ap" 

For my setup, I have a GPS plugged into one port, my DST-800 connected to an RS-422 to USB converter for my inputs (my python scripts use those two--instead of assigning a serial port to /dev/ttyUSB0, it's /dev/gps). "rd" outputs to my RD2030 display, and the "ap" outputs to the autopilot (these are used by kplex in the kplex.conf file).

Step 2: Initialize the NMEA-0183 Instrument Scripts


Here is the monitor.py script, which runs at startup:

monitory.py


#!/usr/bin/env python

import serial
import operator
import time
import os
import sys
import socket
import select

# change the working directory to the scripts folder
os.chdir("home/pi/kts/scripts")

# log and begin instrument scripts
log = open('log', 'w')
log.write("Monitor Initialized\r\n")
log.write("Starting GPS...\r\n")
log.close()
os.system("python gps.py &")
log = open('log', 'a')
log.write("Starting IMU...\r\n")
log.close()
os.system("python imu.py &")
log = open('log', 'a')
log.write("Starting DST...\r\n")
log.close()
os.system("python dst.py &")
log = open('log', 'a')
log.write("Starting BME...\r\n")
log.close()
os.system("python bme.py &")
log = open('log', 'a')
log.write("Starting kplex...\r\n")
log.close()
os.system("sudo kplex &")

GPS_IP = "127.0.0.4"
GPS_PORT = 5005
gpssock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
gpssock.bind((GPS_IP, GPS_PORT))

IMU_IP = "127.0.0.5"
IMU_PORT = 5005
imusock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
imusock.bind((IMU_IP, IMU_PORT))

DST_IP = "127.0.0.6"
DST_PORT = 5005
dstsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dstsock.bind((DST_IP, DST_PORT))

BME_IP = "127.0.0.8"
BME_PORT = 5005
bmesock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
bmesock.bind((BME_IP, BME_PORT))

log = open('log', 'a')
log.write("Starting Loop\r\n------------------------\r\n")
log.close()

gps_hack = time.time()
imu_hack = time.time()
dst_hack = time.time()

while True:

    # monitor gps.py
    gpsready = select.select([gpssock], [], [], .1)
    if gpsready [0]:
        data, addr = gpssock.recvfrom(1024)
        gps_hack = float(data)
    if time.time() - gps_hack > 10.0:
        log = open('log', 'a')
        log.write("Restarting GPS...\r\n")
        log.close()
        os.system("pkill -9 -f gps.py")
        os.system("python gps.py &")
        gps_hack = time.time()        

    # monitor imu.py
    imuready = select.select([imusock], [], [], .1)
    if imuready [0]:
        data, addr = imusock.recvfrom(1024)
        imu_hack = float(data)
    if time.time() - imu_hack > 10.0:
        log = open('log', 'a')
        log.write("Restarting IMU...\r\n")
        log.close()
        os.system("pkill -9 -f imu.py")
        os.system("python imu.py &")
        imu_hack = time.time()

    # monitor dst.py
    dstready = select.select([dstsock], [], [], .1)
    if dstready [0]:
        data, addr = dstsock.recvfrom(1024)
        dst_hack = float(data)
    if time.time() - dst_hack > 10.0:
        log = open('log', 'a')
        log.write("Restarting DST...\r\n")
        log.close()
        os.system("pkill -9 -f dst.py")
        os.system("python dst.py &")
        dst_hack = time.time()

    # monitor bme.py
    bmeready = select.select([bmesock], [], [], .1)
    if bmeready [0]:
        data, addr = bmesock.recvfrom(1024)
        bme_hack = float(data)
    if time.time() - bme_hack > 60:
        log = open('log', 'a')
        log.write("Restarting BME...\r\n")
        log.close()
        os.system("pkill -9 -f bme.py")
        os.system("python bme.py &")
        bme_hack = time.time()


This script is quite useful, aside from starting the whole system. It first changes the working directory to the kts script folder, which is necessary for the imu script to access the right calibration data. Without this step, the MPU-9250 won't work correctly.

Throughout the monitor script, it writes down what it's doing to a log file to see what's happening with the script.

After that, it executes the python scripts which then start running in the background (thanks to the "&" symbol), starts kplex, and then sets up the UDP ports to listen to each script's health monitor.

Every half second, each instrument script sends a timestamp which is read by this monitor script. If more than 10 seconds have gone by without receiving an update from the script (which means the script has failed for whatever reason), it will kill the process and restart it. It logs each action in the log file, which is helpful if I want to see if something is failing a lot.

Step 3: The GlobalSat BU-353-S4 USB GPS Receiver Python Script


The most basic functionality.

gps.py


import sys
import serial
import math
import operator
import time
import socket
import os

GPS_IP = "127.0.0.1"
GPS_PORT = 5005

MON_IP = "127.0.0.4"
MON_PORT = 5005

mon = 0

# initialize log
log = time.time()
f = open('gpsraw', 'w')
f.write("Last 60 Seconds GPS Raw Input:\r\n")
f.close()

ser = serial.Serial('/dev/gps', 4800, timeout=5)

while True:

    # health monitor
    hack = time.time()
    if hack - mon > .5:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.sendto(str(hack), (MON_IP, MON_PORT))
        mon = hack

    # log raw input
    gps_raw = ser.readline()
    f = open('gpsraw', 'a')
    f.write(gps_raw)
    f.close()
    if hack - log > 60:
        os.remove('gpsraw')
        f = open('gpsraw', 'w')
        f.write("Last 60 Seconds GPS Raw Input:\r\n")
        f.close()
        log = hack

    # checking to see if it's a valid NMEA sentence
    if "*" in gps_raw:
        gps_split = gps_raw.split('*')
        gps_sentence = gps_split[0].strip('$')
        cs0 = gps_split[1][:-2]
        cs1 = format(reduce(operator.xor,map(ord,gps_sentence),0),'X')
        if len(cs1) == 1:
            cs1 = "0" + cs1

        # if it is a valid NMEA sentence
        if cs0 == cs1:
            gps_vars = gps_sentence.split(',')
            title = gps_vars[0]

            # recommended minimum navigation sentence
            if title == "GPRMC" and gps_vars[2] == "A":
     
                # heading from IMU
                try:
                    f = open('imu_bus', 'r')
                    line = f.readline()
                    f.close()
                    imu_split = line.split(',')
                    imu_hack = float(imu_split[0])
                    heading = float(imu_split[1])    
                except ValueError:
                    f.close()
                    time.sleep(.03)
                    f = open('imu_bus', 'r')
                    line = f.readline()
                    f.close()
                    imu_split = line.split(',')
                    imu_hack = float(imu_split[0])
                    heading = float(imu_split[1])

                # if heading is from the last 3 seconds, and groundspeed less than .1,
                # reset course to heading to eliminate low speed artifacts
                valid = gps_vars[2]
                course = float(gps_vars[8])
                groundspeed = float(gps_vars[7])
                if time.time() - imu_hack < 3.0 and groundspeed < 0.1:
                    course = heading

                # assemble the sentence with corrected course
                rmc = "GPRMC," + gps_vars[1] + ',' + gps_vars[2] + ',' + gps_vars[3] + ',' + gps_vars[4] + ',' + gps_vars[5] + ',' + gps_vars[6] + ',' + str(groundspeed) + ',' + str(int(round(course))) + ',' + gps_vars[9] + ',,'
                rmccs = format(reduce(operator.xor,map(ord,rmc),0),'X')
                if len(rmccs) == 1:
                        rmccs = "0" + rmccs
                gprmc = "$" + rmc + "*" + rmccs + "\r\n"

  vtg = "GPVTG," + str(course) + ",T,,M," + str(groundspeed) + ",N,,K,D"
  vtgcs = format(reduce(operator.xor,map(ord,vtg),0),'X')
                if len(vtgcs) == 1:
                        vtgcs = "0" + vtgcs
                gpvtg = "$" + vtg + "*" + vtgcs + "\r\n"

  zda = "GPZDA," + gps_vars[1] + "," + gps_vars[9][:2] + "," + gps_vars[9][2:4] + ",20" + gps_vars[9][4:] + ",,"
  zdacs = format(reduce(operator.xor,map(ord,zda),0),'X')
                if len(zdacs) == 1:
                        zdacs = "0" + zdacs
                gpzda = "$" + zda + "*" + zdacs + "\r\n"

                # to gps bus
                f = open('gps_bus', 'w')
                f.write(str(time.time()) + ',' + valid + ',' + str(course)  + ',' + str(groundspeed))
                f.close()

                # to kplex
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.sendto(gprmc + gpvtg + gpzda, (GPS_IP, GPS_PORT))

     # else print the sentence
     else:
  # to kplex
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.sendto(gps_raw, (GPS_IP, GPS_PORT))

First, it starts a log file to log the raw input for funsies. After that, it starts looping, and it sends the health monitor signal out every .5 seconds to the monitor.py script. It also logs the raw input from the GPS for the last 60 seconds (any and all input, regardless if it's a valid NMEA sentence). Next, it runs a quick routine to verify it really is a valid NMEA sentence, and only if it's the RMC one.

If it is, then it extracts the heading information from IMU (see next section), and if the groundspeed is less than .1 knots and heading is recent (from the last 3 seconds), then it replaces course with heading. Why? I've found that sitting at dock, even with a groundspeed of 0, the course still moves all over the place. This isn't really necessary, but it's slightly annoying since I'm a perfectionist and if I'm not moving, then the course should, by default, be the same as the boat's heading.

Then it assembles the whole sentence, writes it to the GPS bus file for the DST script, and prints it out to kplex via a UDP port.

It also creates the ZTG sentence (groundspeed and track) and ZDA (time in UTC). If there are any other valid NMEA sentences coming through, it prints them too.

It should be noted that the "try" section is there because if this script tries to read the file when the imu.py script is writing the file, it'll freeze. It's very rare that that would actually happen, but just in case, it "tries" to read it, and if it returns an error, it waits .03 seconds for the imu.py script to write and close the file, then tries again.

Step 4: The MPU-9250 Tilt-Compensated Compass Python Script


You can read all about it in my previous post, but the big take away is that you must calibrate it. Otherwise it will be useless. Remember, run the calibration command in the RTIMUEllipsoidFit folder. This will produce a RTIMULib.ini file. Copy that file over to the kts scripts folder, so it's in the same directory as the imu.py script.

Here's the python script for the IMU:

imu.py


import sys, getopt

sys.path.append('.')
import RTIMU
import os.path
import time
import math
import operator
import socket
import os

IMU_IP = "127.0.0.2"
IMU_PORT = 5005

MON_IP = "127.0.0.5"
MON_PORT = 5005

SETTINGS_FILE = "RTIMULib"

s = RTIMU.Settings(SETTINGS_FILE)
imu = RTIMU.RTIMU(s)

# offsets
yawoff = 0.0
pitchoff = 0.0
rolloff = 0.0

# timers
t_print = time.time()
t_damp = time.time()
t_fail = time.time()
t_fail_timer = 0.0
t_shutdown = 0

if (not imu.IMUInit()):
    hack = time.time()
    imu_sentence = "$IIXDR,IMU_FAILED_TO_INITIALIZE*7C"
    if (hack - t_print) > 1.0:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.sendto(imu_sentence, (IMU_IP, IMU_PORT))
        t_print = hack
        t_shutdown += 1
        if t_shutdown > 9:
            sys.exit(1)

imu.setSlerpPower(0.02)
imu.setGyroEnable(True)
imu.setAccelEnable(True)
imu.setCompassEnable(True)

poll_interval = imu.IMUGetPollInterval()

# data variables
roll = 0.0
pitch = 0.0
yaw = 0.0
heading = 0.0
rollrate = 0.0
pitchrate = 0.0
yawrate = 0.0

# magnetic deviation
f = open('mag', 'r')
magnetic_deviation = float(f.readline())
f.close()

# dampening variables
t_one = 0
t_three = 0
roll_total = 0.0
roll_run = [0] * 10
heading_cos_total = 0.0
heading_sin_total = 0.0
heading_cos_run = [0] * 30
heading_sin_run = [0] * 30

while True:

  hack = time.time()

  # if it's been longer than 5 seconds since last print
  if (hack - t_damp) > 5.0:
      
      if (hack - t_fail) > 1.0:
          t_one = 0
          t_three = 0
          roll_total = 0.0
          roll_run = [0] * 10
          heading_cos_total = 0.0
          heading_sin_total = 0.0
          heading_cos_run = [0] * 30
          heading_sin_run = [0] * 30
          t_fail_timer += 1
          imu_sentence = "IIXDR,IMU_FAIL," + str(round(t_fail_timer / 60, 1))
          cs = format(reduce(operator.xor,map(ord,imu_sentence),0),'X')
          if len(cs) == 1:
                cs = "0" + cs
          imu_sentence = "$" + imu_sentence + "*" + cs
          sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          sock.sendto(imu_sentence, (IMU_IP, IMU_PORT))
          t_fail = hack
          t_shutdown += 1

  if imu.IMURead():
    data = imu.getIMUData()
    fusionPose = data["fusionPose"]
    Gyro = data["gyro"]
    t_fail_timer = 0.0

    if (hack - t_damp) > .1:
        roll = round(math.degrees(fusionPose[0]) - rolloff, 1)
 pitch = round(math.degrees(fusionPose[1]) - pitchoff, 1)
        yaw = round(math.degrees(fusionPose[2])- yawoff, 1)
        rollrate = round(math.degrees(Gyro[0]), 1)
        pitchrate = round(math.degrees(Gyro[1]), 1)
        yawrate = round(math.degrees(Gyro[2]), 1)
 if yaw < 0.1:
            yaw = yaw + 360
 if yaw > 360:
     yaw = yaw - 360
    
        # Dampening functions
        roll_total = roll_total - roll_run[t_one]
        roll_run[t_one] = roll
        roll_total = roll_total + roll_run[t_one]
        roll = round(roll_total / 10, 1)
        heading_cos_total = heading_cos_total - heading_cos_run[t_three]
        heading_sin_total = heading_sin_total - heading_sin_run[t_three]
        heading_cos_run[t_three] = math.cos(math.radians(yaw))
        heading_sin_run[t_three] = math.sin(math.radians(yaw))
        heading_cos_total = heading_cos_total + heading_cos_run[t_three]
        heading_sin_total = heading_sin_total + heading_sin_run[t_three]
        yaw = round(math.degrees(math.atan2(heading_sin_total/30,heading_cos_total/30)),1)
        if yaw < 0.1:
            yaw = yaw + 360.0

        # yaw is magnetic heading, convert to true heading
        heading = yaw - magnetic_deviation
        if heading < 0.1:
            heading = heading + 360
 if heading > 360:
     heading = heading - 360

        t_damp = hack
        t_one += 1
        if t_one == 10:
            t_one = 0
        t_three += 1
        if t_three == 30:
            t_three = 0
  
        if (hack - t_print) > 1:

            # health monitor
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.sendto(str(hack), (MON_IP, MON_PORT))

            # iihdm magnetic heading
            hdm = "IIHDM," + str(round(yaw))[:-2] + ",M"
            hdmcs = format(reduce(operator.xor,map(ord,hdm),0),'X')
            if len(hdmcs) == 1:
                hdmcs = "0" + hdmcs
            iihdm = "$" + hdm + "*" + hdmcs

            # iihdt true heading
            hdt = "IIHDT," + str(round(heading))[:-2] + ",T"
            hdtcs = format(reduce(operator.xor,map(ord,hdt),0),'X')
            if len(hdtcs) == 1:
                hdtcs = "0" + hdtcs
            iihdt = "$" + hdt + "*" + hdtcs

     # iixdr ahrs data
            xdr = "IIXDR,A," + str(int(round(roll))) + ",D,ROLL,A,"  + str(int(round(pitch))) + ",D,PTCH,A"
            xdrcs = format(reduce(operator.xor,map(ord,xdr),0),'X')
            if len(xdrcs) == 1:
                xdrcs = "0" + xdrcs
            iixdr = "$" + xdr + "*" + xdrcs

     # tirot rate of turn
     rot = "TIROT," + str(yawrate*60) + ",A"
     rotcs = format(reduce(operator.xor,map(ord,rot),0),'X')
            if len(rotcs) == 1:
                rotcs = "0" + rotcs
            tirot = "$" + rot + "*" + rotcs

            # assemble the sentence
            imu_sentence = iihdm + '\r\n' + iihdt + '\r\n' + iixdr + '\r\n' + tirot + '\r\n'

            # to imu bus
            f = open('imu_bus', 'w')
            f.write(str(t_print) + ',' + str(heading) + ',' + str(roll)  + ',' + str(pitch))
            f.close()

            # To kplex
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.sendto(imu_sentence, (IMU_IP, IMU_PORT))

            t_print = hack
        
    time.sleep(poll_interval*1.0/1000.0)

Long story short--if it doesn't initialize, it will let you know via the XDR NMEA sentence, then shut down. The monitor will keep restarting it every 10 seconds. Also, if it stops receiving input for more than 5 seconds, it'll let you know that too.

Don't forget to update the magnetic deviation for your location. This is in the file simply called "mag" and if it's east, it's negative. For the SF Bay in 2016, it's approximately 14 degrees East, so I have -14 in there.

It dampens out heading over three seconds, and roll over one second, and prints all available AHRS information (attitude, heading, and rates of change) via a sentence. Most of this is just for gee-whiz, but the heading is necessary, and the roll/pitch is useful for other functions in the next section.

Also, there are built in "offsets." I ran this system in my slip and logged all the NMEA data for five minutes, and took the average roll, pitch, and heading. Then I made an offset such that it would subtract to get zero (if the average roll was +5.2 degrees, then the "rolloff" is 5.2). I have a compass on my boat, so I used that to compare the heading and made the appropriate offset.

Step 5: The Airmar DST800 Smart Sensor Python Script


dst.py


import serial
import math
import operator
import time
import socket
import os.path
import os

DST_IP = "127.0.0.3"
DST_PORT = 5005

MON_IP = "127.0.0.6"
MON_PORT = 5005

mon = 0

# initialize for VDR
five = 0
setx_total = 0
setx_run = [0] * 5
sety_total = 0
sety_run = [0] * 5
drift_total = 0
drift_run = [0] * 5

# initialize for VLW
vlwfirst = 1
vlwinit = 0.0

# initialize log
log = time.time()
f = open('dstraw', 'w')
f.write("Last 60 Seconds DST Raw Input:\r\n")
f.close()

ser = serial.Serial('/dev/dst', 4800, timeout=5)

while True:

    # health monitor
    hack = time.time()
    if hack - mon > .5:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.sendto(str(hack), (MON_IP, MON_PORT))
        mon = hack

    # log raw input
    dst_raw = ser.readline()
    f = open('dstraw', 'a')
    f.write(dst_raw)
    f.close()
    if hack - log > 60:
        os.remove('dstraw')
        f = open('dstraw', 'w')
        f.write("Last 60 Seconds DST Raw Input:\r\n")
        f.close()
        log = hack
    
    # checking to see if it's a valid NMEA sentence
    if "*" in dst_raw:
        dst_split = dst_raw.split('*')
        dst_sentence = dst_split[0].strip('$')
        cs0 = dst_split[1][:-2]
        cs = format(reduce(operator.xor,map(ord,dst_sentence),0),'X')
        if len(cs) == 1:
            cs = "0" + cs

        # if it is a valid NMEA sentence
        if cs0 == cs:
            dst_vars = dst_sentence.split(',')
            title = dst_vars[0]

            # depth sentence
            if title == "SDDPT":

                # roll and pitch from imu
                try:
                    f = open('imu_bus', 'r')
                    line = f.readline()
                    f.close()
                    imu_split = line.split(',')
                    imu_hack = float(imu_split[0])
                    roll = float(imu_split[2])
                    pitch = float(imu_split[3])
                    
                except ValueError:
                    time.sleep(.03)
                    f = open('imu_bus', 'r')
                    line = f.readline()
                    f.close()
                    imu_split = line.split(',')
                    imu_hack = float(imu_split[0])
                    roll = float(imu_split[2])
                    pitch = float(imu_split[3])

                # correct depth for 23 degree offset from centerline, but if roll/pitch
                # are from the last 3 seconds, correct depth for attitude
                depth = round(float(dst_vars[1])*math.cos(math.radians(23)),1)
                if time.time() - imu_hack < 3.0:
                   depth = round(float(dst_vars[1])*math.cos(math.radians(23-roll))*math.cos(math.radians(pitch)),1)

                # assemble the sentence with .5 meter offset from waterline
                dpt = "SDDPT," + str(depth) + ",0.50"
                dptcs = format(reduce(operator.xor,map(ord,dpt),0),'X')
                if len(dptcs) == 1:
                    dptcs = "0" + dptcs
                sddpt = "$" + dpt + "*" + dptcs + "\r\n"

                # to kplex
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.sendto(sddpt, (DST_IP, DST_PORT))

            # mean water temp sentence
            elif title == "YXMTW":

                # write to bus
                mtw = dst_vars[1]
  f = open('dst_bus', 'w')
  f.write(mtw)
  f.close()

                # to kplex
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.sendto(dst_raw, (DST_IP, DST_PORT))

            # vessel waterspeed sentence and current set and drift
            elif title == "VWVHW":

                # heading and roll from imu
                try:
                    f = open('imu_bus', 'r')
                    line = f.readline()
                    f.close()
                    imu_split = line.split(',')
                    imu_hack = float(imu_split[0])
                    heading = float(imu_split[1])
                    roll = float(imu_split[2])
                except ValueError:
                    f.close()
                    time.sleep(.03)
                    f = open('imu_bus', 'r')
                    line = f.readline()
                    f.close()
                    imu_split = line.split(',')
                    imu_hack = float(imu_split[0])
                    heading = float(imu_split[1])
                    roll = float(imu_split[2])                

                # course and groundspeed from gps
                try:
                    f = open('gps_bus', 'r')
                    line = f.readline()
                    gps_split = line.split(',')
                    f.close()
                    gps_hack = float(gps_split[0])
                    valid = gps_split[1]
                    course = float(gps_split[2])
                    groundspeed = float(gps_split[3])
                except ValueError:
                    time.sleep(.03)
                    f = open('gps_bus', 'r')
                    line = f.readline()
                    f.close()
                    gps_split = line.split(',')
                    gps_hack = float(gps_split[0])
                    valid = gps_split[1]
                    course = float(gps_split[2])
                    groundspeed = float(gps_split[3])

                # calculate corrected waterspeed from heel and velocity
                waterspeed = float(dst_vars[5])
                sensor_angle = 23.0
                if time.time() - imu_hack < 3.0:
                    sensor_angle = math.fabs(23.0-roll)
                five_knot_correction = -.02 * sensor_angle
                ten_knot_correction = sensor_angle * (.035 - .0065 * sensor_angle) - 2
                if sensor_angle > 10.0:
                    ten_knot_correction = -.03 * sensor_angle - 2
                if waterspeed < 5.0:
                    waterspeed = round(waterspeed + (five_knot_correction * waterspeed / 5), 1)
                else:
                    waterspeed = round((waterspeed + (waterspeed * (ten_knot_correction * (waterspeed - 5) - 2 * five_knot_correction * (waterspeed - 10))) / 50),1)

                # assemble the sentence
                vhw = "VWVHW,"
                if time.time() - imu_hack < 3.0:
                    vhw = vhw + str(int(heading))
                else: vhw = vhw + ''
                vhw = vhw + ",T,,M," + str(waterspeed) + ",N,,K"
                vhwcs = format(reduce(operator.xor,map(ord,vhw),0),'X')
                if len(vhwcs) == 1:
                    vhwcs = "0" + vhwcs
                vwvhw = "$" + vhw + "*" + vhwcs + "\r\n"

                # to kplex
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.sendto(vwvhw, (DST_IP, DST_PORT))

                # VDR set and drift
                if time.time() - imu_hack < 3.0 and time.time() - gps_hack < 3.0 and valid == "A":
                    
                    heading_radians = math.radians(heading)
                    course_radians = math.radians(course)
                    set0 = course_radians - heading_radians
                    if set0 < 0:
                        set0 = set0 + 2 * math.pi
                    set_relative = math.pi - math.atan2(groundspeed * math.sin(set0), waterspeed - groundspeed * math.cos(set0))
                    if waterspeed == 0 and groundspeed == 0:
                        set_relative = set0
                    set_radians = heading_radians + set_relative
                    if set_radians > (2 * math.pi):
                        set_radians = set_radians - (2 * math.pi)
                    drift = math.sqrt(pow(waterspeed,2) + pow(groundspeed,2) - 2 * waterspeed * groundspeed * math.cos(set0))
                    
                    # dampen out set and drift over the last five readings
                    setx_total = setx_total - setx_run[five]
                    setx_run[five] = math.cos(set_radians)
                    setx_total = setx_total + setx_run[five]
                    setx_ave = setx_total / 5
                    sety_total = sety_total - sety_run[five]
                    sety_run[five] = math.sin(set_radians)
                    sety_total = sety_total + sety_run[five]
                    sety_ave = sety_total / 5
                    set_radians = math.atan2(sety_ave, setx_ave)
                    if set_radians < 0:
                        set_radians = set_radians + 2 * math.pi
                    set_true = math.degrees(set_radians)
                    set_apparent = set_true - heading
                    if set_apparent < 0:
                        set_apparent = set_apparent + 360

                    drift_total = drift_total - drift_run[five]
                    drift_run[five] = drift
                    drift_total = drift_total + drift_run[five]
                    drift = drift_total / 5
                    five = five + 1
                    if five > 4:
                        five = 0
                    
                    # assemble the sentence
                    vdr = "IIVDR," + str(int(set_true)) + ",T,,M," + str(round(drift,1)) + ",N"
                    vdrcs = format(reduce(operator.xor,map(ord,vdr),0),'X')
                    if len(vdrcs) == 1:
                        vdrcs = "0" + vdrcs
                    iivdr = "$" + vdr + "*" + vdrcs + "\r\n"

                    # to kplex
                    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                    sock.sendto(iivdr, (DST_IP, DST_PORT))

            # voyage log sentence
            elif title == "VWVLW":

                # calculate present trip total vs overall total
                if vlwfirst == 1:
                    vlwinit = dst_vars[1]
                    vlwfirst = 0
                trip = float(dst_vars[1]) - float(vlwinit)
                vlw = "VWVLW," + dst_vars[1] + ",N," + str(trip) + ",N"
                cs = format(reduce(operator.xor,map(ord,vlw),0),'X')
                if len(cs) == 1:
                    cs = "0" + cs
                vwvlw = "$" + vlw + "*" + cs  + "\r\n"

                # to kplex
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.sendto(vwvlw, (DST_IP, DST_PORT))
   
     # if it's any other valid NMEA sentence
            else:

                # to kplex
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.sendto(dst_raw, (DST_IP, DST_PORT))

This is very similar to the GPS script, in that it checks for validity, and has a routine for each sentence. For the SDDPT sentence, it corrects for installation offset. My sensor is at a 23 degree angle from straight up and down, so the depth is skewed. Since we know the vessel pitch and roll from the previous section, we can correct the depth reading for the boat's attitude. Nice.

It writes the water temperature to a bus (for later), and it also calculates the correct VWVLW voyage log sentence. The DST only stores the total nautical mileage, and doesn't have a reset function. No problem, I built one in the script.

For the VWVHW vessel water speed sentence, it corrects the water speed reading from boat velocity and roll (see this post here for more on that). Not only that, but every time it receives a VWVHW sentence, and heading/course/groundspeed info is from the last 3 seconds, it calculates the VDR Set and Drift Sentence (see this post here). It dampens that out over the last 5 readings to give smooth data.

Atmospheric Step: Setting up the BME280 Atmospheric Sensor with the Raspberry Pi


This one was just for fun. The sensor was really cheap, and I used some strange Chinese tutorial to set it up but I actually forget where I found it.

bme.py


#coding: utf-8
import math
import smbus
import time
import socket
import operator

bus_number  = 1
i2c_address = 0x77

bus = smbus.SMBus(bus_number)

digT = []
digP = []
digH = []

t_fine = 0.0


def writeReg(reg_address, data):
 bus.write_byte_data(i2c_address,reg_address,data)

def get_calib_param():
 calib = []
 
 for i in range (0x88,0x88+24):
  calib.append(bus.read_byte_data(i2c_address,i))
 calib.append(bus.read_byte_data(i2c_address,0xA1))
 for i in range (0xE1,0xE1+7):
  calib.append(bus.read_byte_data(i2c_address,i))

 digT.append((calib[1] << 8) | calib[0])
 digT.append((calib[3] << 8) | calib[2])
 digT.append((calib[5] << 8) | calib[4])
 digP.append((calib[7] << 8) | calib[6])
 digP.append((calib[9] << 8) | calib[8])
 digP.append((calib[11]<< 8) | calib[10])
 digP.append((calib[13]<< 8) | calib[12])
 digP.append((calib[15]<< 8) | calib[14])
 digP.append((calib[17]<< 8) | calib[16])
 digP.append((calib[19]<< 8) | calib[18])
 digP.append((calib[21]<< 8) | calib[20])
 digP.append((calib[23]<< 8) | calib[22])
 digH.append( calib[24] )
 digH.append((calib[26]<< 8) | calib[25])
 digH.append( calib[27] )
 digH.append((calib[28]<< 4) | (0x0F & calib[29]))
 digH.append((calib[30]<< 4) | ((calib[29] >> 4) & 0x0F))
 digH.append( calib[31] )
 
 for i in range(1,2):
  if digT[i] & 0x8000:
   digT[i] = (-digT[i] ^ 0xFFFF) + 1

 for i in range(1,8):
  if digP[i] & 0x8000:
   digP[i] = (-digP[i] ^ 0xFFFF) + 1

 for i in range(0,6):
  if digH[i] & 0x8000:
   digH[i] = (-digH[i] ^ 0xFFFF) + 1  

def readData():
 data = []
 for i in range (0xF7, 0xF7+8):
  data.append(bus.read_byte_data(i2c_address,i))
 pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
 temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
 hum_raw  = (data[6] << 8)  |  data[7]
 
 temperature = compensate_T(temp_raw)
 pressure = compensate_P(pres_raw)
 humidity = compensate_H(hum_raw)
        return dict(temperature=temperature,
                    pressure=pressure,
                    humidity=humidity)

def compensate_P(adc_P):
 global  t_fine
 pressure = 0.0
 
 v1 = (t_fine / 2.0) - 64000.0
 v2 = (((v1 / 4.0) * (v1 / 4.0)) / 2048) * digP[5]
 v2 = v2 + ((v1 * digP[4]) * 2.0)
 v2 = (v2 / 4.0) + (digP[3] * 65536.0)
 v1 = (((digP[2] * (((v1 / 4.0) * (v1 / 4.0)) / 8192)) / 8)  + ((digP[1] * v1) / 2.0)) / 262144
 v1 = ((32768 + v1) * digP[0]) / 32768
 
 if v1 == 0:
  return 0
 pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125
 if pressure < 0x80000000:
  pressure = (pressure * 2.0) / v1
 else:
  pressure = (pressure / v1) * 2
 v1 = (digP[8] * (((pressure / 8.0) * (pressure / 8.0)) / 8192.0)) / 4096
 v2 = ((pressure / 4.0) * digP[7]) / 8192.0
 pressure = pressure + ((v1 + v2 + digP[6]) / 16.0)  

 #print "pressure : %7.2f hPa" % (pressure/100)
        return "{:.2f}".format(pressure/100)

def compensate_T(adc_T):
 global t_fine
 v1 = (adc_T / 16384.0 - digT[0] / 1024.0) * digT[1]
 v2 = (adc_T / 131072.0 - digT[0] / 8192.0) * (adc_T / 131072.0 - digT[0] / 8192.0) * digT[2]
 t_fine = v1 + v2
 temperature = t_fine / 5120.0
 #print "temp : %-6.2f ℃" % (temperature) 
        return "{:.2f}".format(temperature)

def compensate_H(adc_H):
 global t_fine
 var_h = t_fine - 76800.0
 if var_h != 0:
  var_h = (adc_H - (digH[3] * 64.0 + digH[4]/16384.0 * var_h)) * (digH[1] / 65536.0 * (1.0 + digH[5] / 67108864.0 * var_h * (1.0 + digH[2] / 67108864.0 * var_h)))
 else:
  return 0
 var_h = var_h * (1.0 - digH[0] * var_h / 524288.0)
 if var_h > 100.0:
  var_h = 100.0
 elif var_h < 0.0:
  var_h = 0.0
 #print "hum : %6.2f %" % (var_h)
        return "{:.2f}".format(var_h)

def setup():
 osrs_t = 1   #Temperature oversampling x 1
 osrs_p = 1   #Pressure oversampling x 1
 osrs_h = 1   #Humidity oversampling x 1
 mode   = 3   #Normal mode
 t_sb   = 5   #Tstandby 1000ms
 filter = 0   #Filter off
 spi3w_en = 0   #3-wire SPI Disable

 ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode
 config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en
 ctrl_hum_reg  = osrs_h

 writeReg(0xF2,ctrl_hum_reg)
 writeReg(0xF4,ctrl_meas_reg)
 writeReg(0xF5,config_reg)

setup()
get_calib_param()

BME_IP = "127.0.0.7"
BME_PORT = 5005

MON_IP = "127.0.0.8"
MON_PORT = 5005
mon = 0

t_print = 0

while True:

    # health monitor
    hack = time.time()
    if hack - mon > .5:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.sendto(str(hack), (MON_IP, MON_PORT))
        mon = hack

    if hack - t_print > 10:
 try:
         f = open('dst_bus', 'r')
                mtw = f.readline()
                f.close()
 except ValueError:
                time.sleep(.03)
                f = open('dst_bus', 'r')
                mtw = f.readline()
                f.close()

        data = readData()
        temp = round(float(data['temperature']), 1)
        bars = round(float(data['pressure']) * .001, 5)
        inch = round(bars * 29.53, 2)
        humi = round(float(data['humidity']), 1)
        dewp = round((temp + 273.15) * math.sqrt(1 + ((temp + 273.15) * math.log(humi / 100)) / ((3.167485 - .00244 * (temp + 273.15)) * 1000000 / 461.5)) - 273.15, 1)

        # MDA
        mda = "IIMDA," + str(inch) + ",I," + str(round(bars, 2)) + ",B," + str(temp) + ",C," + mtw + ",C," + str(humi) + ",," + str(dewp) + ",C,,,,,,,,"
        mdacs = format(reduce(operator.xor,map(ord,mda),0),'X')
        if len(mdacs) == 1:
            mdacs = "0" + mdacs
        iimda = "$" + mda + "*" + mdacs + "\r\n"
        
        # Simulate NKE Instruments
 xdrb = "IIXDR,P," + str(bars) + ",B,Barometer"
 xdrbcs = format(reduce(operator.xor,map(ord,xdrb),0),'X')
        if len(xdrbcs) == 1:
            xdrbcs = "0" + xdrbcs
 iixdrb = "$" + xdrb + "*" + xdrbcs + "\r\n"
 
 xdrt = "IIXDR,C," + str(temp) + ",C,AirTemp"
 xdrtcs = format(reduce(operator.xor,map(ord,xdrt),0),'X')
        if len(xdrtcs) == 1:
            xdrtcs = "0" + xdrtcs
        iixdrt = "$" + xdrt + "*" + xdrtcs + "\r\n"

 mmb = "IIMMB," + str(inch) + ",I," + str(bars) + ",B"
 mmbcs = format(reduce(operator.xor,map(ord,mmb),0),'X')
        if len(mmbcs) == 1:
            mmbcs = "0" + mmbcs
        iimmb = "$" + mmb + "*" + mmbcs + "\r\n"

 mta = "IIMTA," + str(temp) + ",C"
        mtacs = format(reduce(operator.xor,map(ord,mta),0),'X')
        if len(mtacs) == 1:
            mtacs = "0" + mtacs
        iimta = "$" + mta + "*" + mtacs + "\r\n"

 atm_sentence = iimda + iixdrb + iixdrt + iimmb + iimta

        # to kplex
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.sendto(atm_sentence, (BME_IP, BME_PORT))
        
        t_print = hack

The sensor detects temperature, pressure, and humidity. I had to dust off my old atmospheric science textbooks from college, but I found a few equations to use this data to derive all sorts of humidity. It produces the MDA Meteorological Composite Sentence (go to this page and search MDA for more info), which includes barometric pressure (inches of mercury and bars), air temperature, water temperature, relative humidity, absolute humidity, and dewpoint. Whew.

It also sends out standard NMEA sentences for that data, which spoofs NKE Marine Electronics instruments.

Step 6: Setup a Wireless Kplex Multiplexer


Kplex is a fantastic tool, but to be totally honest, the documentation kind of sucks. It isn't written for computer morons like me, so I had to do a lot of testing and searching to figure it out. First, install it by doing this (in the kts directory of course):

git clone https://github.com/stripydog/kplex.git
cd kplex
make
sudo make install

Here is my kplex.conf file that accepts UDP inputs from the previous python scripts, and prints it all to a TCP port that my laptop/phone can connect to (as well as to the display I got on eBay and an additional port that can print to my Autopilot, or receive from another instrument). This kplex.conf file is also included in the github download from my page.

kplex.conf


# From GPS
[udp]
address=127.0.0.1
port=5005
direction=in

# From IMU
[udp]
address=127.0.0.2
port=5005
direction=in

# From DST
[udp]
address=127.0.0.3
port=5005
direction=in

# From BME
[udp]
address=127.0.0.7
port=5005
direction=in

# To TCP
[tcp]
mode=server
port=10110

# To/From RD2030
[serial]
filename=/dev/rd
direction=both
baud=4800

# To/From Autopilot
[serial]
filename=/dev/ap
direction=both
baud=4800


Simply save this as kplex.conf in the kts/scripts folder (where a copy already is if you downloaded my github) and copy it into the /etc/ directory with the following command in the terminal:

sudo cp kplex.conf /etc/

But we're not done. What address should you connect to on your phone or laptop? Find out with the following command:

ifconfig

and write down the number next to inet address of wla0 (mine is 10.0.0.1). When you add a connection in OpenCPN or the NKE Marine Electronics iOS app, use that number for the address, and use the port number that is in your kplex.conf file (if you used mine, the port number is 10110). Now, when you connect, you'll have a steady stream of NMEA data!

But let's make sure everything is working before we proceed. We're going the run the imu.py script, which will output NMEA data to the kplex port, and then run kplex as well with the raw text output:

python imu.py &
sudo kplex file:direction=out

That will output a text string of whatever kplex is receiving. Great. So now our instruments are outputting data, kplex is receiving, if you connect to the WiFi network and type in the inet address on your device (OpenCPN/NKE, with the port number specified in kplex.conf) you can receive over WiFi. So now, we just need to run the monitor script on its own so all this will happen automatically.

Step 7: Execute a Python Script at Startup on a Raspberry Pi


Save yourself some trouble, and just read this blog post here about it. I followed it, and have my results right here. Basically, it runs the init.sh script at startup, which executes the monitor.py script, which then starts everything.

init.sh


#!/bin/sh

### BEGIN INIT INFO
# Provides:          myservice
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Put a short description of the service here
# Description:       Put a long description of the service here
### END INIT INFO

# Change the next 3 lines to suit where you install your script and what you want to call it
DIR=/home/pi/kts/scripts
DAEMON=$DIR/monitor.py
DAEMON_NAME=monitor

# Add any command line options for your daemon here
DAEMON_OPTS=""

# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=root

# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid

. /lib/lsb/init-functions

do_start () {
    log_daemon_msg "Starting system $DAEMON_NAME daemon"
    start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS
    log_end_msg $?
}
do_stop () {
    log_daemon_msg "Stopping system $DAEMON_NAME daemon"
    start-stop-daemon --stop --pidfile $PIDFILE --retry 10
    log_end_msg $?
}

case "$1" in

    start|stop)
        do_${1}
        ;;

    restart|reload|force-reload)
        do_stop
        do_start
        ;;

    status)
        status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
        ;;

    *)
        echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
        exit 1
        ;;

esac
exit 0

You already have the monitor.py script from above, so now all that's left to do is make these two scripts "executable," and copy this init.sh to the right directory.

That's easy, all you have to run is the following commands, assuming you're in the kts/scripts folder:

sudo chmod a+x init.sh
sudo chmod a+x monitor.py
sudo cp init.sh /etc/init.d/
sudo update-rc.d init.sh defaults

Now it will execute init.sh at startup, which will execute monitor.py, which then executes all the instrument scripts and kplex.

Make note of two things: the very first line of monitor.py is "#!/usr/bin/env python" because it needs to know its a python script. Also, the first step in monitor.py is to change the working directory to the scripts folder for IMU calibration.






Final Thoughts


Now when you turn on your Raspberry Pi, all these scripts should start up and you'll receive good NMEA data through the wifi connection. If you want to check out the log files, you can ssh in with the same inet address we've been using. With your laptop connected to the RPi's Wifi, use Terminal and type the following in:

ssh -Y pi@10.0.0.1
# Enter your password (the default is "raspberry")
cd kts/scripts
sudo nano log

or just nano whatever file you want to see. This way, you can debug/see what's up without a screen or a keyboard on your Pi when you're down on your boat with your laptop. Pretty useful, huh?

All in all, this is what we're looking at:

Raspberry Pi $40
USB GPS$30
MPU-9250 $12
DST800 $250
BME280 ~$15
Cables/Misc ~$100
Total:~$450

Not too bad for a wireless multiplexer, especially considering what it can do. Tilt-compensated compass. Data dampening. Wireless multiplexing. VDR Set and Drift. Correct paddle wheel readings. I am always interested in hearing what other applications people can find with the little device.

In another comment section, someone mentioned using it to automatically modulate a windlass based on depth--something that could be useful if you always want to drop out seven times the depth worth of chain for your anchor, or in his case, I believe it was fishing nets. Use the comments below to tell me your ideas.

27 comments:

  1. I looked at the link you provided for the depth sounder. How did you convert this from NMEA 2000 into NMEA 0183 Nice project.

    ReplyDelete
  2. I looked at the link you provided for the depth sounder. How did you convert this from NMEA 2000 into NMEA 0183 Nice project.

    ReplyDelete
    Replies
    1. Thanks for pointing that out. I linked to the wrong model, so I've updated it to reflect the NMEA-0183 version (unfortunately, it's out of stock at the moment).

      I do recall it being a little misleading, so I matched it up with the Garmin serial number found here (http://www.airmar.com/productdescription.html?id=110). The NMEA-0183 version has the Garmin serial 010-11051-10, while the NMEA-2000 has 010-11051-00 (note the last two digits). Amazon, while incredibly useful for online commerce, sometimes lacks appropriately descriptive titles. So while the title may say 0183 or 2000, always check the serial number in the advanced description.

      Thanks for pointing that out!

      Delete
  3. Hello,
    great project, thanks for the effort!!!

    I do have a version of your system basically up and running but there is one issue, I couldn’t solve.

    Configuration:
    RASPI 3 running Jessie
    U-Blox NEO-6 GPS with Serial to USB converter (delivers NMEA strings on /dev/ttyUSB0)
    Second serial-to-USB converter w/o any HW connected (this will take input from existing NMEA devices on the boat)
    9 DOF IMU (http://www.ebay.de/itm/181877281002?_trksid=p2057872.m2749.l2649&ssPageName=STRK%3AMEBIDX%3AIT), delivers NMEA strings using the imu.py script.
    No further HW connected yet.
    SW installation as described above

    My observations:
    I had to change line 12 in monitor.py from os.chdir("home/pi/kts/scripts") to os.chdir("/home/pi/kts/scripts").
    Your remark about
    I2C read error from 104, 114 - Failed to read fifo count
    then one of your cords probably became unplugged :/ (it happened to me).
    In the “How to Setup the MPU-9250 on a Raspberry Pi” blog prevented me from beating the kids and burning the apartment (my cable was not unplugged but internally broken ;-( )

    Without a second dummy USB converter, the script crashes
    File "dst.py", line 38, in
    ser = serial.Serial('/dev/ttyUSB' + dst_port, 4800, timeout=5)
    File "/usr/lib/python2.7/dist-packages/serial/serialutil.py", line 261, in __init__ self.open()
    File "/usr/lib/python2.7/dist-packages/serial/serialposix.py", line 278, in open raise SerialException("could not open port %s: %s" % (self._port, msg))
    serial.serialutil.SerialException: could not open port /dev/ttyUSB1: [Errno 2] No such file or directory: '/dev/ttyUSB1'
    Traceback (most recent call last): File "gps.py", line 83, in
    course = float(gps_vars[8])
    ValueError: could not convert string to float:
    [2]+ Exit 1 python monitor.py

    With the USB converter connected, the error message is as follows:
    traceback (most recent call last):
    File "gps.py", line 83, in
    course = float(gps_vars[8])
    ValueError: could not convert string to float:
    and randomly
    File "gps.py", line 96, in
    f = open('gps_bus', 'w')
    IOError: [Errno 13] Permission denied: 'gps_bus'
    Traceback (most recent call last):
    File "gps.py", line 83, in …………………..

    As an output from kplex, I do only receive IMU data. GPS is missing completely.
    “ps –ax” shows monitor.py, imu.py and kplex running, no gps.py.

    Does this tell you anything? Your feedback is very much appreciated!

    Matthias

    ReplyDelete
    Replies
    1. Okay, it *sounds* like what's happening here is that the names/directories of the USB devices are changing. When you plug a USB device into the Pi, it automatically assigns it (at random) USB0 or USB1 or USB2 or USB3, depending on how many are plugged in. If you only have one USB device plugged in, then it will only assign it USB0. When I say USB0, I mean the path: /dev/ttyUSB0 vs /dev/ttyUSB1

      So if you have two USB devices plugged in, you will have them randomly assigned to USB0 and USB1 every time the Pi powers up. This makes the script damn near impossible to use. This is why I included a very clunky way in init.py to wait for USB reception from either of the ports, then write down which port that is, then start the dst.py and gps.py, which then reads the USB port which was determined in init.py.

      This is not a good way to do this.

      A better way, which I have not updated this post to reflect yet, is to assign each physical USB plugin a specific port. It's not THAT complicated to do, but my RPi box is down on my boat, so I can't update this post until I get that back (another two weeks or so).

      Basically, with the setup above (as of right now), you'll always need a USB GPS plugged in, otherwise it won't work properly.

      As for your other issue (could not convert string to float)... it's trying to open the gps_bus file to write the groundspeed and course to that file, which is then read by the dst.py script. However, typically whenever I got that "Permission denied" error, as you did, I had to delete the gps_bus file, restart the whole system, and then try again from a clean slate.

      I hope this helps. If it doesn't, please post a little more detailed sequence of events to getting that second error and I'll see if I can help more (again... I might have to wait two weeks until I get the box back up here).

      Delete
  4. Thanks for the reply and hints. I'll try to delete and restart and keep you posted on the result.
    regards Matthias

    ReplyDelete
    Replies
    1. I've updated the post and all the scripts here and on github to reflect to new changes. You'll want to check out the section called "Bonus Interim Step: Assign Persistent Names for your USB Devices"

      This will hopefully resolve your problem, but keep in mind, if you make these changes, then you'll have to update your other scripts (/dev/ttyUSB0 becomes /dev/gps or whatever you assign it to).

      Delete
  5. Hi There, I am also new to Raspberry. I have a Pi3 and an iPad with Cellular, GPS and wifi. I use the Navaid App which uses the internal GPS. I understand Navaid outputs NMEA data over wifi using port 10110 as per your setup. I want to use this output via the raspberry to input data to my autopilot.
    Power consumption is not an issue as it is on a cruiser ( Yes, a stink boat )
    As you state that your setup will not run without the usb GPS I am keen to get your advice on a setup that will use the raspberry as a wifi receiver of NMEA data and rebroadcast on usb possibly in UDP if autopilot will accept. Initial setup will be only one USB in use for connection to Autopilot.
    Hoping you are happy to assist as I am also new to Linux, however learning fast
    Thanks in advance
    Keith

    ReplyDelete
    Replies
    1. Yes, this is quite simple to do. I actually have set this up so that you don't need a USB GPS for it to work, however I'm on holiday right now so I can't get anything until I get back.

      What you want to do is quite simple, and you have two ways to do this.

      The first way is to setup a wifi network on the Raspberry Pi 3 like above, and then connect your iPad to that Wifi network. If you go this route, then simply follow the Setup a Wifi AP on the Raspberry Pi 3 Model B section above and proceed.

      The second way is to setup a wifi network on your iPad, and connect the Raspberry Pi 3 to that wifi network. You'll have to do this once and then the RPi should be able to reconnect over and over again on your boat without any input. I recommend this option, because I think if your iPad connects to an external wifi network (like on the RPi3), it will attempt to connect to the internet through that wifi connection, and you'll get stuck in a situation where the iPad won't download any data through the cellular network because it keeps trying to connect through wifi. However, if you setup a wifi network on the iPad, it'll bridge the connection and your RPi3 will actually be able to connect to the internet that way.

      So my recommendation is to setup a WiFi network on your iPad, connect the RPi3 to that (and make sure it automatically connects), and then setup kplex to work with Wifi.

      Here's how to do that. You'll sort of work backwards from above, and you might have to reference the kplex website to get it setup correctly, but I think you can still use the TCP Server mode in kplex. Your app will probably have better documentation, but basically, you'll be connected to your iPad through wifi, and you'll have the same kplex config file as above, only make sure your port number matches what your app has (like you say... 10110). I *believe* that should work. So now kplex will be sucking in any data from that port on the wifi network (which should be whatever NMEA data it's broadcasting), and then it will interface that with the other config file ports. So, you can just setup a USB connection from the RPi3 to your autopilot.

      Here's a sample kplex config file to take in from the WiFi and output to USB:

      #from wifi
      [tcp]
      mode=client
      port=10110

      #to USB port
      [serial]
      filename=/dev/ttyUSB0
      direction=out
      baud=4800

      If you only have one USB plugged in to your RPi, it should be ttyUSB0 (the number zero, not the letter). If you have more than one USB plugged in, it'll get much more complicated but still doable.

      You may also need to add this line to the TCP portion of that file:

      address=10.0.0.1

      and change that address to whatever is specified on your iPad (either on your settings or in your app... I'm not sure on this).

      And then just plug in your USB and it *should* be good to go. Let me know if this works.

      Delete
  6. Just a little bit of additional information. The input specs for the autopilot are NMEA 0183 Ver 3.0
    Baud Rate 4800,
    Character format : Start bit, 8 bits, LSB first MSB (bit 7) = 0 , no parity bit, 1 or 2 stop bits
    Polarity -- Idle, stop bit, logic "1" -- Line A <0.5 volt above Line B
    Start bit, logic "0" -- Line A > 4 volt
    Serial In opto isolated 1000 Ohm Min
    Thanks
    Keith

    ReplyDelete
  7. Hi Connor, Thanks for the advice. I hope to get back to this little project over the next couple of days, so I will keep you posted.
    If I set up as above and I connect a monitor to the Pi will I be able to see the nmea data flow to test the setup? Do I need a separate command or configure setting??

    ReplyDelete
    Replies
    1. You will be able to see the NMEA stream. In fact, I highly recommend doing that as it will save you headaches later. First, I would recommend running kplex in the terminal window to make sure kplex is getting data.

      If you have kplex running automatically, you'll have to kill it first by typing "sudo pkill kplex" then you can run it and see the kplex output by typing "sudo kplex file:direction=out"

      Once you have verified that kplex is indeed receiving and outputting data (whatever it outputs in the terminal window, will output over the Serial/USB port), you can restart the RPi so kplex automatically starts. Then connect your serial monitor to the USB port and verify that it's registering. This way you can see if you have a bad USB/Serial converter or whatever, or if the code isn't working properly.

      Keep me posted.

      Delete
    2. I should add that you don't need different configure settings. If your serial monitor can see it, then your NMEA device can see it. If it can't see it, then your NMEA device can't either.

      Delete
  8. Looks like a very nice setup. Not sure why would you feed all the data over wifi to any device. But just an idea of this is excellent. I stumbled upon this article when I was researching how can mux nmea streams from various sources into one. I have a ST1000+ autohelm and GPSMAP60csx handheld with batimetric maps. I managed to make them talk and now they guide my little dinghy through waters no problems . Now I want to find a way to use VWR sentences from any wind device and mux them with my GPSMAP 60 and feed that to ST1000+ to do wind and track navigation. Is there a way to do the mux and output on Raspbery Pi so I could use it as gateway? Any thoughts?

    ReplyDelete
    Replies
    1. Yes, that should be pretty easy, but I'm not sure I fully understand what you're trying to do. Do you simply want to have your wind instrument connect to your GPSMAP 60? If so, I'm not sure how to do that. I looked that up, and it doesn't look like it supports data input, but I could've been looking at the wrong thing.

      Oh, and about the wifi... I have it connect to my laptop and phone while underway, it's nice to just take a quick look on my phone to see the depth :)

      Delete
  9. Another source for the DST800 is the Furuno 235DST-MSE.
    It is the DST800 with a 10m furuno plug on the end, wiring diagram here:
    http://airmartechnology.com/uploads/wiringdiagrams/91_592.pdf

    ReplyDelete
  10. Hi, can you say anything how you turn off the raspberry pi since it is headless to avoid sd card corruption when you turn off the power?

    ReplyDelete
    Replies
    1. The only way that would work is if you have some sort of button or switch attached to the Raspberry Pi that triggers a script to shut it down. Something that would execute a "sudo shutdown" I believe.

      Or, you can SSH in on your laptop and type that as well (I believe it's "sudo shutdown" but I may be wrong).

      Delete
  11. Hello,
    I want to do multiplexor with Arduino so I can mix the sentences of two instruments NMEA 0183 (an echo sounder and a GPS device). For this I have an Arduino Uno, a Fishfinder 100 blue echosounder and a SIM 808 GPRS / GSM + GPS + Bluetooth Shield DuinoPeak.
    Separately I can rescue the information of each instrument, but when trying to join the sentences only appears in the monitor the GPS information. For this I have used the NMEA library, softwareSerial and the manufacturer's library Duinopeak_GPRS_SIMCOM. I would like to help me with this problem, which can guide me in the subject, what other considerations should I have in programming, what details I am missing, if I need another additional device, or as a last option if I need to change any instrument or the Arduino. Thank you

    ReplyDelete
    Replies
    1. Best asking on the arduino forum. This is based on the Raspberry pi.
      The UNO has one serial port! Whilst you can use software serial, you would need to use a MEGA unit as that has 4 hardware serial ports. Use the tinyGPS labrary read in two serial devices, parse them, combine the two lines into the third serial port.

      Delete
  12. Hi,
    Can I use this one -> http://www.ebay.de/itm/5PCS-USB-485-USB-to-RS485-Converter-Adapter-Window-7-8-XP-Linux-Vista-TE322/311434301041?ssPageName=STRK%3AMEBIDX%3AIT&_trksid=p2055119.m1438.l2649

    Thanks!

    ReplyDelete
    Replies
    1. I'm 99% sure that will work fine. The only issue with smaller ones like that is the wires/connections can be weak and come disconnected, so just don't jerk it around and you should be fine. :)

      Delete
    2. Ok, good that I get direct 5 units ;-)
      The Idea is to connect the output from a Raymarine E85001 PC/NMEA/Seatalk Interface Box to a raspberrypi running Signal K.
      I have already Signal K running but can you help me with the next steps?

      Thanks!

      Delete
    3. Yeah that should work fine, it'll basically send an NMEA-0183 serial stream to the port that you plug it into... however, I know very little about Signal-K. It sounds cool, and needs to happen, but I don't know anything else.

      Delete
    4. If you don't use signal K, how would you spread the information from NMEA-0183 to multiple devices (pad, phone...) I wanted to use Signal K because already have some apps for lots of devices. However can not speak with opencpn.
      Do you have any other suggestion?

      Thanks!

      Delete
    5. Well I only use three different devices. The first is the RD-30 display, which accepts an NMEA-0183 serial output from a USB on the raspberry pi. The second is my laptop, receiving the NMEA-0183 stream over WiFi TCP using OpenCPN (my chart plotter), and the third is the NKE Display Pro app on my iPhone (it's free and displays names data, again over WiFi TCP). That's my setup!

      Delete
  13. Great article and something I'm going to try to use with my lowrance elite 4 chirp so that I can get a two way communication going between my Nexus 7 running the navionics app and the lowrance.

    ReplyDelete