Yachtlogger

Using Raspberry Pi SenseHAT and GPS for logging

For my boat I have several instruments that are functional but aged. So an obvious solution would be to expand the pool with an RPi, but at the same time be able to bring the parts back home for programming off-season.

The Sense HAT is an all-inclusive add-on board to the Pi that can be included in a standard casing. As the board has a 8×8 LED matrix for read out and a joystick for control some mechanical modifications must be made to the font panel. Adafruit has a standard case with a clear lid where the parts can be easily fitted so holes have to be drilled on top of the joystick an temperature/pressure/humidity sensors.

One disadvantage with the sense HAT is that CPU heats up the interior so the thermometers will register 5-10 deg higher than they should. Be prepared to make some adjustments to the code but there are examples of good work done where the compensation is made on behalf of CPU-temperature as seen in this post.

Apart from this the Sense HAT and RPi3 is a compact combination of sensors with a simple readout that can be clearly seen in all lighting conditions. I have programmed my sequences so clicking joystick up will raise the color value with 50 steps for each click, starting at 50 (night time) moving up to 250 as a maximum (daylight).

Next challenge was to attach a GPS and the Adafruit Ultimate GPS was selected as it connects to the USB port via a standard TTL cable. The documentation using it miss out some important details if you want to use it together with other applications and make an automatic start every time the RPi boots. The solution is to make a .sh file (script) and refer to this in rc.local. The script file needs to be executable which you do with the CHMOD -X ../path command. In the script file you need to insert delays in order to configure the GPS before the program starts:

#!/bin/sh -e
#Need to wait past boot so everything is done
sleep 10
gpsd /dev/ttyUSB0 -G -n -F /var/run/gpsd.sock
sleep 10
sudo python /home/pi/Desktop/filename.py&
exit 0

The & ensures that the program can run in loop after the script has been executed and the ttyUSB0 refers to the USB port where the GPS is attached.

Finally a little mahogany box was built and mounted on the wall in the cabin for keeping the RPi on board with the display clearly visible. The only thing to do when boarding is to mount the power and GPS cables before switching on the circuit on the panel. So the RPi can be easily detached again after the trip so log files can be read at home.

Link to a Google map with acquired coordinates for the summer trip 2017

This is how the components appear in the 12V installation

When docked and land power is connected the new 220V installation on board services charger and different ammenities:

 

CODE for the SenseHAT:

import os
from sense_hat import SenseHat
import time
from time import *
from time import sleep
from datetime import datetime, timedelta
import gps
from gps import *
import threading
 
"""
MAIA yachting sensor system
"""
 
sense = SenseHat()
count = 0
sense.compass_enabled=True
jmin=1
jmax=7
j=None
SCSP=0.08 #Scroll_speed
compass=0 #Define variables so they can be used outside def's
 
logfile_created=False #Creating new CSV for each time system is booted and GPS UTC time has been found 
GPStime_updated=False
logname=None
header=None
header =["UTC tid","System tid","Hastighed","Kurs","Bredde","Laengde",
         "Temperatur (fugt)","Temperatur (tryk)","Barometer","Baro delta","Fugtighed",
         "Gyro pitch","Gyro roll","Gyro yaw","Mag X","Mag Y","Mag Z","Compass"]
 
UTC=None
 
#global counter used for waiting for GPS. Will make undated logfile if not connected within 10 minutes?
counter=1
wait_for_GPS=True #Wait 10 min after boot then make alternative CSV if no connection.
wait_for_min=True # Wait until current Min is equal to min at boot time
 
#Time
min_boot = str(time.strftime ("%M")) #Minute at boot
min_count=None #Minutes counting 0-9
min_last=None #Value last time minute was registered
min_prev=None #Value previous minute
web_update=True #For controlling updates of website data
 
#Rotation
rot=180
sense.set_rotation(rot) #So power cable is upwards
 
#Pressure
plast=1999 #default value for last pressure reading 10 mins ago
plast_read=True #Determines if last pressure (plast) should be opened for reading
press_rising=True #If good weather is on its way (pressure is rising)
press_diff=None #For showing movements in pressure the web page
 
# GPS commands
gpsd = None #seting the global variable
TIMEZ = 2 #Offsetting GPS UTC time to local timezone 1=winther; 2=summer
 
#Something with colors
TXBR=50 #Text brightness
TXCO=[TXBR, TXBR, TXBR] #Scroll text default color white
R = (255, 0, 0)
G = (0, 255, 0)
B = (0, 0, 255)
Y = (0, 255, 255)
M = (255, 255, 0)
C = (255, 0, 255)
O = (0, 0, 0)
 
maya_logo = [
R, O, R, O, O, O, B, O,
R, R, R, O, O, B, B, B,
R, O, R, O, O, B, O, B,
R, O, R, O, O, B, O, B,
O, Y, O, O, O, O, G, O,
O, Y, O, O, O, G, G, G,
O, Y, O, O, O, G, O, G,
O, Y, O, O, O, G, O, G,
]
              
class GpsPoller(threading.Thread):
  def __init__(self):
    threading.Thread.__init__(self)
    global gpsd #bring it in scope
    gpsd = gps(mode=WATCH_ENABLE) #starting the stream of info
    self.current_value = None
    self.running = True #setting the thread running to true
  
  def run(self):
    global gpsd
    while gpsp.running:
      gpsd.next() #this will continue to loop and grab EACH set of gpsd info to clear the buffer
 
def Counter():
    global counter
    counter=counter+1
#    print counter
#    sleep (1)
 
def Compass():
  #sense.show_message("Compass %s " % "{:.0f}".format(compass), scroll_speed=SCSP, text_colour=TXCO)
  sense.show_message("Kompas %s " % compass, scroll_speed=SCSP, text_colour=TXCO)
  print "Kompasset viser", compass
 
def DateAndTime():
  timenow=str(datetime.now())
  timenow_formatted = "{:.16}".format(timenow)
  sense.show_message(timenow_formatted, scroll_speed=SCSP, text_colour=TXCO)
  
def GPSdata():
  global TXCO
  sense.show_message("Br %s " % "{:.6}".format(lati), scroll_speed=SCSP, text_colour=TXCO)
  sense.show_message("Lgd %s " % "{:.6}".format(longi), scroll_speed=SCSP, text_colour=TXCO)
#  fart=gpsspeed / 1.852 #speed in knots
  sense.show_message("Fart %s knob" % gpsspeed, scroll_speed=SCSP, text_colour=TXCO)
  sense.show_message("Kurs %s " % kurs, scroll_speed=SCSP, text_colour=TXCO)
 
def Graphics():
  v=None #Variable, pos 1
  n=None #Min in range, pos 2
  a=None #Max in range, pos 3
  l=None #Color of bar, pos 5
  d=None #Delta - difference between min and max (a-n)
  i=None #Difference between reading and minimum value (v-n)
  c=None #Compare: diff versus delta (d-i)
 
#  for i in range (8):
 
  for x in range (8):
      for y in range (8):
        v=float(var_range[x][1]) #Read variable current value
        n=float(var_range[x][2]) #Read Minimum in range
        a=float(var_range[x][3]) #Read Maximum in range
        l=(var_range[x][5]) #Read color
        d=a-n
        i=v-n
        c=8*i/d
        if y < c:
          l=l
        else:
          l=O      
        sense.set_pixel(x, y, l)
  time.sleep(1)
 
def Gyroscope():
  sense.show_message("Pitch %s uT" % "{:.0}".format(pit), scroll_speed=SCSP, text_colour=TXCO)
  sense.show_message("Roll %s uT" % "{:.0}".format(rol), scroll_speed=SCSP, text_colour=TXCO)
  sense.show_message("Yaw %s uT" % "{:.0}".format(yaw), scroll_speed=SCSP, text_colour=TXCO)
 
def Humidity():
  sense.show_message("Fugt %s pct" % "{:.0f}".format(humi), scroll_speed=SCSP, text_colour=TXCO)
 
def log_create(timestamp):
    global logname
  #Creating new file after GPS time is received and inserting header
    logname=str('/home/pi/Desktop/csv/' + timestamp + '.csv')
    with open(logname,"w") as f:
        f.write(",".join(str(value) for value in header)+ "\n")
    print "New log created: " + logname
 
def log_update():
  global UTC
  with open(logname,"a") as f:
    f.write(",".join(str(value) for value in log_data)+ "\n")
  #write the file for use at the web server
 
def log_website():
  l = open('/home/pi/Desktop/csv/lognow.csv', 'w')
  for i in range(len(header)): #Order data vertically
      l.write(str(header[i])+ "," + str(log_data[i]) + "\n")
#  l.write(str(header) + "\n" + str(log_data))
  l.close()
 
def Magnetic():
  sense.show_message("MagneticX %s gr" % "{:.0f}".format(magX), scroll_speed=SCSP, text_colour=TXCO)
  sense.show_message("MagneticY %s gr" % "{:.0f}".format(magY), scroll_speed=SCSP, text_colour=TXCO)
  sense.show_message("MagneticZ %s gr" % "{:.0f}".format(magZ), scroll_speed=SCSP, text_colour=TXCO)
 
def Message(besked):
  sense.show_message(besked, scroll_speed=SCSP, text_colour=TXCO)
 
def Orientation():
  sense.show_message("p: {pitch}, r: {roll}, y: {yaw}".format(**orien), scroll_speed=SCSP, text_colour=TXCO)
 
def Pressure():
  if press_rising==False:
    sense.show_message("Tryk %s mb" % "{:.0f}".format(press), scroll_speed=SCSP, text_colour=[TXBR, 0, 0])    
  else:
    sense.show_message("Tryk %s mb" % "{:.0f}".format(press), scroll_speed=SCSP, text_colour=TXCO)
 
def Temperature():
  sense.show_message("Temp %s gr." % "{:.0f}".format(temph), scroll_speed=SCSP, text_colour=TXCO)
#  sense.show_message("TempP %s gr.C" % "{:.0f}".format(tempp), scroll_speed=SCSP, text_colour=TXCO)
 
def var_read(var): #For reading single variables that are stored for later comparison
  try:
    t = open('/home/pi/Desktop/var/' + str(var) + '.txt', 'r')
    return float(t.read())
    t.close()
  except:
    Message( " File read problem ")
 
def var_write(varname, value): #For storing single variables for later comparison
  try:
    t = open('/home/pi/Desktop/var/' + str(varname) + '.txt', 'w')
    t.write (str(value))
    t.close()
  except:
    Message ("File write problem ")
 
##########  START #####################
     
j = var_read("joystick") #read joystick position before last shutdown
print "Datetime:", datetime.now()
print "Loop sequence starting"
 
sense.set_pixels(maya_logo)
time.sleep(2)
for x in range (8):
    for y in range (8):
      sense.set_pixel(x, y, O)
 
#sense.low_light = True
 
if __name__ == '__main__':
    gpsp = GpsPoller() # create the thread
    try:
        gpsp.start() # start it up
        while True:
            b=threading.Thread(target=Counter)
            b.start()
            temph = sense.get_temperature_from_humidity()
            tempp = sense.get_temperature_from_pressure()
            press = sense.get_pressure()
            humi = sense.get_humidity()
            lati=str("{:.7}".format(gpsd.fix.latitude))
            longi=str("{:.7}".format(gpsd.fix.longitude))
            gpstime=gpsd.utc
            gpsspeed=str("{:3.1f}".format(gpsd.fix.speed * 3.6 / 1.852)) #set to knots instead of m/s 
            kurs=str("{:3.0f}".format(gpsd.fix.track))
            gyro = sense.get_gyroscope()
            pit = "{pitch}".format(**gyro)
            rol = "{roll}".format(**gyro)
            yaw = "{yaw}".format(**gyro)
            mag = sense.get_compass_raw()
            magX = mag["x"]
            magY = mag["y"]
            magZ = mag["z"]
            orien = sense.get_orientation_degrees()
            comp = sense.get_compass()
            compass=str("{:3.0f}".format(comp))
            tnow=str(datetime.now())
            timenow = "{:.19}".format(tnow)
 
            log_data = [UTC, timenow,gpsspeed, kurs, lati, longi,
                        temph, tempp, press, press_diff, humi, pit,
                        rol, yaw, magX, magY, magZ, compass]
 
 
            #Array setting the data range and color for each sensor: Name, Var, Min, Max, Column, Color
            var_range = ["Pressure", press, "960", "1080", "0", R],\
                        ["Temperature", temph, "0", "40", "1", G],\
                        ["Heading", kurs, "0", "360", "2", B],\
                        ["Speed", gpsspeed, "0", "10", "3", Y],\
                        ["Pitch", pit, "0", "360", "4", M],\
                        ["Roll", rol, "0", "360", "5", C],\
                        ["Yaw", yaw, "0", "360", "6", G],\
                        ["Compass", comp, "0", "360", "7", B],\
 
            # Wait for the UTC time from GPS
            #if counter<40:         
            if gpstime != None and gpstime != '':
                if GPStime_updated==False:
                    #Update system time with GPS time and create new logfile with timestamp
                    print "GPS UTC time:", gpstime
                    UTC = gpstime[0:4] + gpstime[5:7] + gpstime[8:10] + ' ' + gpstime[11:13] + gpstime[13:19]                        
                    tzhour = int(gpsd.utc[11:13])+TIMEZ
                    if (tzhour>23):
                        tzhour = (int(gpstime[11:13])+TIMEZ)-24
                    print "TZhour:", tzhour
                    print "Datetime:", datetime.now()
                    utctime_converted = gpstime[0:4] + gpstime[5:7] + gpstime[8:10] + ' ' + str(tzhour) + gpstime[13:19]
                    print "GPS time corrected", utctime_converted
                    print 'Setting system time to local time...'
                    os.system('sudo date --set="%s"' % utctime_converted)
                    print "New System time", datetime.now() #                        
                    GPStime_updated=True
                    #Create new logfile when time is updated and converted (only once per session)
                    if logfile_created==False:
                        timenow = str(utctime_converted)
                        log_create(timenow)
                        logfile_created=True
#            else:         
#                if logfile_created==False:
#                    timenow=str(datetime.now())
#                    newlogname = str("no GPS " + timenow)
#                    log_create (newlogname)
#                    logfile_created=True                    
 
            #Controlling scroll colors depending on status
            if GPStime_updated==True:
                TXCO=[0, 0, TXBR] #Scroll text color to blue and check for changes in lumination 
            else:
                if logfile_created==True:
                    TXCO=[TXBR, 0, TXBR] #Scroll text color green until GPS is working
                else:
                    TXCO=[TXBR, TXBR, 0] #set text color to yellow until a new logfile has been created
 
            hour_count = str(time.strftime ("%H")) #Updating hours
            min_count = str(time.strftime ("%M"))# Updating minutes for triggering saving of data - especially "pressure"
            sec_count = str(time.strftime ("%S")) #Updating seconds
 
            if min_last!=min_count:
                min_prev=min_last
                if logfile_created==True:
                    log_update() #updating the CSV log every minute with all data
                min_last=min_count
                 
            #TXCO=[0, 0, TXBR] #Scroll text color
 
        #Check if pressure is going down or up
            if min_count[-1:] == "5": #Look up, Update and save Pressure every 10 minutes
                if plast_read == True:
                    plast = var_read("pressure")
                    press_diff = press - plast
                    if press > plast:
                        press_rising = True
                    else:
                        press_rising = False
                var_write("pressure", press) #saves new value
                plast_read == False
 
            if min_count[-1:] == "0":
                plast_read = True #End every 10 min cycle with reset of all reading variables
 
            #if sec_count[-1:] == "7":
            #    if web_update == True:
            log_website() #Update CSV file for website
            #        web_update == False
 
            if sec_count[-1:] == "0":
                web_update = True
 
            events = sense.stick.get_events()
            for event in events:
                if event.action != "released":
                    if event.direction == "right":
                        if j==jmin:
                            j=jmax
                        else:
                            j=j-1
                        var_write("joystick", j)
                         
                    elif event.direction == "left":
                        if j==jmax:
                            j=jmin
                        else:
                            j=j+1
                        var_write("joystick", j)
                    elif event.direction == "down":
                        if (TXBR < 201):
                            TXBR=TXBR+50
                        else:
                            TXBR=TXBR
                    elif event.direction == "up":
                        if (TXBR > 99):
                            TXBR=TXBR-50
                        else:
                            TXBR=TXBR
                    elif event.direction == "middle":
                        Message ("God tur :-)")
 
            if j==1:
                Message("MAIA")
                DateAndTime()
#                Temperature()
#                Pressure()
#                Humidity()
#                GPSdata()
#                Graphics()
#                Compass()
            if j==2:
                GPSdata()
            if j==3:
                Pressure()
            if j==4:
                Humidity()
            if j==5:
                Temperature()
            if j==6:
                DateAndTime()
            if j==7:
                Graphics()
            if j==8:
                Compass()
            if j==9:
                Gyroscope()
            if j==10:
                Magnetic()
            if j==19:
                Orientation()
                '''        
                          os.system('clear')
 
                          print
                          print ' GPS reading'
                          print '----------------------------------------'
                          print 'latitude    ' , gpsd.fix.latitude
                          print 'longitude   ' , gpsd.fix.longitude
                          print 'time utc    ' , gpsd.utc,' + ', gpsd.fix.time
                          print 'altitude (m)' , gpsd.fix.altitude
                          print 'eps         ' , gpsd.fix.eps
                          print 'epx         ' , gpsd.fix.epx
                          print 'epv         ' , gpsd.fix.epv
                          print 'ept         ' , gpsd.fix.ept
                          print 'speed (m/s) ' , gpsd.fix.speed
                          print 'climb       ' , gpsd.fix.climb
                          print 'track       ' , gpsd.fix.track
                          print 'mode        ' , gpsd.fix.mode
                          print
                          print 'sats        ' , gpsd.satellites
 
                          time.sleep(5) #set to whatever
        '''
    except (KeyboardInterrupt, SystemExit): #when you press ctrl+c
        print "\nKilling Thread..."
        gpsp.running = False
        gpsp.join() # wait for the thread to finish what it's doing
    print "Done.\nExiting."
 
    time.sleep(1)