Monday, August 1, 2016

Example NMEA-0183 Sentences

Here's a log of the Raspberry Pi Multiplexer's output I wrote about previously.


$GPGGA,171059.000,3749.9201,N,12228.4985,W,2,09,1.0,-6.1,M,-25.3,M,5.0,0000*6E
$GPGSA,M,3,17,19,28,13,15,24,30,06,48,,,,1.9,1.0,1.6*3D
$GPRMC,171059.000,A,3749.9201,N,12228.4985,W,3.1,168,310716,,*03
$GPVTG,167.62,T,,M,3.1,N,,K,D*10
$GPZDA,171059.000,31,07,2016,,*5D
$IIHDM,163,M*38
$IIHDT,177,T*3D
$IIXDR,A,4,D,ROLL,A,-2,D,PTCH,A*1A
$TIROT,-0.0,A*16
$PFEC,GPatt,177,-2,4*7C
$IIMDA,29.95,I,1.01,B,19.8,C,17.0,C,68.0,,16.7,C,,,,,,,,*3F
$IIXDR,P,1.01408,B,Barometer*2B
$IIXDR,C,19.8,C,AirTemp*26
$IIMMB,29.95,I,1.01408,B*42
$IIMTA,19.8,C*05
$SDDPT,0.9,0.50*6B
$SDDBT,3.2,f,0.9,M,0.5,F*0B
$VWVHW,177,T,,M,4.2,N,,K*4D
$IIVDR,20,T,,M,1.3,N*39
$VWVLW,0.0,N,0.0,N*4C
$YXMTW,17.0,C*14

This is a great resource to get each sentence's definition, although you will find the MDA sentence here.

The "PFEC" sentence is a proprietary sentence used so that my Furuno RD30 display can read roll and pitch. I use the free NKE Marine Electronics app on my phone to display everything, so I include a few XDR sentences so that it recognizes them (same with GPVTG).

Out of curiousity, I'm going to compare the costs here between a DIY setup and a professional one (minus all the blood and sweat of course).

DIY Setup


Compass/Accelerometer
$12
Atmospheric Sensor $15
USB GPS $30
DST800 $250
Raspberry Pi $40
Assorted Cables $100
Total $447

Professional Setup

Airmar 220WX*
$1154
DST800$250
Regatta Processor** $8,324
NMEA WiFi $461
Total $10,189

* the Airmar 220WX has a compass, accelerometer, GPS, and weather station all in one unit. However, I personally don't think having a GPS at the top of a rocking mast will produce very good GPS data

** the Regatta Processor calculates the VDR sentence, or Tidal Set and Drift (speed and direction of the ocean current)

By doing the work yourself, it'll cost you about 4% of what a professional installation would cost (granted, if you're doing professional work, you should definitely get a professional setup).

Well that's pretty cool. Next up is to make an NMEA wind instrument like this or this ultrasonic wind sensor.

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.