A Wireless Arduino NMEA-0183 Multiplexer


Let's put all this together. Using the previous posts with the "multiplexer" label, it's quite easy to set up an Arduino that combines multiple instruments, filters the numbers and calculates new data, and outputs a single stream of NMEA-0183 sentences usable by any chart plotter. What you see in the above picture is quite useful: You have an Arduino that computes the sentences, which then pipes that stream over to the Raspberry Pi. When power is applied, the Raspberry Pi automatically starts a Wi-Fi network with my boat's name and runs a program called kplex, which is basically a simple multiplexer program for the Raspberry Pi that can take inputs and output over Wi-Fi. This little box (which is a $7 jewelry box from Target) sits inside my nav desk, flush with two of the sides so that it points straight forward. Then, up above, I open my laptop, start OpenCPN, connect to the Wi-Fi, and bam! It starts receiving the NMEA sentences through kplex.

Recommended Gear

You'll have to follow my other guide for connecting NMEA-0183 devices to the Arduino.

Arduino Code for an NMEA-0183 Multiplexer

The code is pasted below, but I know how these things can be. Something I'm using doesn't quite match up with what you're finding. Libraries are especially susceptible to these problems. So while the code is pasted below, I'm also including the libraries and sketches in a zip file for you to download, which is identical to the files I'm using (that work). Keep in mind, though, that these libraries will probably go out of date, and may not always work if the Arduino IDE gets updated. Use at your own risk. Remember, you must calibrate the MPU-9150 for this to work (the sketch is included in this zip); follow my write up on it to get started.
 ////////////////////////////////////////////////////////////////////////////   
  //   
  // Welcome to the King Tide Sailing Multiplexer!   
  //   
  // Visit kingtidesailing.blogspot.com for more information   
  //   
  // This file is part of RTIMULib-Arduino   
  //   
  // Copyright (c) 2014-2015, richards-tech   
  //   
  // Permission is hereby granted, free of charge, to any person obtaining a copy of    
  // this software and associated documentation files (the "Software"), to deal in    
  // the Software without restriction, including without limitation the rights to use,    
  // copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the    
  // Software, and to permit persons to whom the Software is furnished to do so,    
  // subject to the following conditions:   
  //   
  // The above copyright notice and this permission notice shall be included in all    
  // copies or substantial portions of the Software.   
  //   
  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,    
  // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A    
  // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT    
  // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION    
  // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE    
  // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.   
  //   
  ////////////////////////////////////////////////////////////////////////////   
     
  #include <Wire.h>   
  #include "I2Cdev.h"   
  #include "RTIMUSettings.h"   
  #include "RTIMU.h"   
  #include "RTFusionRTQF.h"    
  #include "CalLib.h"   
  #include <EEPROM.h>   
  #include <PString.h>   
  #include <nmea.h>   
     
  RTIMU *imu;       // the IMU object   
  RTFusionRTQF fusion;   // the fusion object   
  RTIMUSettings settings; // the settings object   
  NMEA gps(GPRMC);     // GPS data connection to decode RMC sentence types (otherwise the serial output becomes too buggy if you decode all GPS sentences)   
  NMEA nmeaDecoder(ALL);  // NMEA data decoder for all sentence types   
     
  // if you don't want to include a sentence, then just make the appropriate variable false    
  // it is recommended that you print only the minimum sentences desired to keep the Serial Port from getting too busy    
  boolean outputgps = true; // gps   
  boolean outputhsp = true; // waterspeed (hereafter referred to as hullspeed)   
  boolean outputdpt = true; // depth   
  boolean outputtmp = true; // water temp   
  boolean outputvlw = true; // distance through water   
  boolean outputhdm = false; // magnetic heading    
  boolean outputhdt = true; // true heading    
  boolean outputrot = false; // rate of turn    
  boolean outputshr = false; // pitch and roll   
  boolean outputvdr = true; // set and drift   
  boolean outputawv = false; // apparent wind velocity   
  boolean outputtwv = false; // true wind velocity   
     
  // User defined variables (for your own boat and geographic location):   
  float magd = -13.73; // magnetic deviation; if it's East, it's negative (13.73E in San Francisco 2015)   
  float tdo = -1.17; // depth sounder offset from waterline (positive) or from bottom of keel (negative) (in meters)   
  float tda = 23.0;  // depth sounder angle from straight down (in degrees)   
  float anvo = 0;   // vertical offset of anemometer from center of buoyancy   
  float anlo = 0;   // longitudinal offset of anemometer from center of buoyancy   
     
  // Variables preset with recommended values (but can be changed):   
  #define DISPLAY_INTERVAL 500 // rate (in milliseconds) in which MPU9150 results are displayed   
  int gpsworking = 20; // this ticks up everytime the MPU runs, which is defined in DISPLAY INTERVAL and is used as a    
  int hsworking = 20;  // counter so that if no GPS or Waterspeed data is received for 10 seconds, then VDR won't calculate  
  int imuworking = 5; // counter so that if the imu isn't working, then no wind corrections will be applied   
    
  // Global variables:   
  unsigned long lastDisplay;   
  unsigned long lastRate;   
  int sampleCount;   
  float r = M_PI/180.0f;   
  float d = 180.0f/M_PI;   
  boolean vlwfirst = true;   
  float tcd0;   
     
  // Variables exchanged between instruments:   
  float ts;   // timestamp of GPS signal in UTC   
  float sog;  // speed over ground   
  float cmg;  // course made good   
  float hs;   // water speed, or hull speed through the water   
  float hdm;  // magnetic heading   
  float hdt;  // true heading   
  float roll;  // vessel roll   
  float pitch; // vessel pitch   
  float yaw;  // vessel yaw (270 = 0, negative is left)   
  float ror;  // rate of roll (negative is left)   
  float rop;  // rate of pitch (negative is nose down)   
  float rot;  // rate of turn (negative is left)   
     
  void setup()   
  {   
   int errcode;   
     
   Serial.begin(4800);  // output port to computer   
   Serial1.begin(4800); // from GPS   
   Serial2.begin(4800); // from DST-800 transducer   
   Serial3.begin(4800); // from anemometer   
   Wire.begin();     // from MPU-9150   
   imu = RTIMU::createIMU(&settings);   
     
   // Checking the IMU, if it fails, simply restarting the connection seems to work sometimes   
   if ((errcode = imu->IMUInit()) < 0) {   
    Serial.print("Failed to init IMU: "); Serial.println(errcode);   
   }   
   if (imu->getCalibrationValid())   
    Serial.println("Using compass calibration");   
   else   
    Serial.println("No valid compass calibration data");   
     
   lastDisplay = lastRate = millis();   
   sampleCount = 0;   
     
   // Slerp power controls the fusion and can be between 0 and 1   
   // 0 means that only gyros are used, 1 means that only accels/compass are used   
   // In-between gives the fusion mix. 0.02 recommended.   
      
   fusion.setSlerpPower(0.02);   
      
   // use of sensors in the fusion algorithm can be controlled here   
   // change any of these to false to disable that sensor   
      
   fusion.setGyroEnable(true);   
   fusion.setAccelEnable(true);   
   fusion.setCompassEnable(true);   
  }   
     
  // calculate checksum function (thanks to https://mechinations.wordpress.com)   
  byte checksum(char* str)    
  {   
   byte cs = 0;   
   for (unsigned int n = 1; n < strlen(str) - 1; n++)    
   {   
    cs ^= str[n];   
   }   
   return cs;   
  }   
     
  void loop()   
  {    
     
  // ***** BU-353 GPS ***** //   
   if (Serial1.available()) {   
   if (gps.decode(Serial1.read())) {   
    if (outputgps == true) {   
     if (gps.term_decimal(7) == 0.0) { // this removes artifacts in cmg when sog = 0   
     char rmcSentence [71];    
     byte csg;    
     PString strg(rmcSentence, sizeof(rmcSentence));    
     strg.print("$GPRMC,");   
     strg.print(gps.term_decimal(1));   
     strg.print(",");   
     strg.print(gps.term(2));   
     strg.print(",");   
     strg.print(gps.term_decimal(3));   
     strg.print(",");   
     strg.print(gps.term(4));   
     strg.print(",");   
     strg.print(gps.term_decimal(5));   
     strg.print(",");   
     strg.print(gps.term(6));   
     strg.print(",");   
     strg.print(gps.term_decimal(7));   
     strg.print(",");   
     strg.print(hdt);   
     strg.print(",");   
     strg.print(gps.term_decimal(9));   
     strg.print(",,,");   
     strg.print(gps.term(12));   
     strg.print("*");   
     csg = checksum(rmcSentence);    
     if (csg < 0x10) strg.print('0');    
     strg.print(csg, HEX);    
     Serial.println(rmcSentence);   
     }   
     else {   
     Serial.println(gps.sentence());   
     }   
    }   
    sog = gps.term_decimal(7);   
    cmg = gps.term_decimal(8);   
    ts = gps.term_decimal(1);   
    gpsworking = 0; // resets the "gps is bad" counter back to 0 since it's good   
    }   
   }   
  // ***** End BU-353 GPS ***** //   
     
  // ***** DST-800 Transducer ***** //   
   if (Serial2.available() > 0 ) {   
   if (nmeaDecoder.decode(Serial2.read())) {   
    char* title = nmeaDecoder.term(0);   
    if (strcmp(title,"VWVHW") == 0) {   
    hs = nmeaDecoder.term_decimal(5);   
    hsworking = 0; // resets the "hullspeed is bad" counter back to 0 since it's good   
    if (outputhsp == true) {   
     char vhwSentence [49];    
     byte csw;    
     PString strw(vhwSentence, sizeof(vhwSentence));    
     strw.print("$VWVHW,");    
     strw.print(hdt);   
     strw.print(",T,");   
     strw.print(hdm);   
     strw.print(",M,");   
     strw.print(hs);   
     strw.print(",N,");   
     strw.print(hs*1.852);   
     strw.print(",K*");    
     csw = checksum(vhwSentence);    
     if (csw < 0x10) strw.print('0');    
     strw.print(csw, HEX);    
     Serial.println(vhwSentence);   
    }   
    }   
    if (outputdpt == true) {   
    if (strcmp(title,"SDDPT") == 0) {   
     char dptSentence [22];    
     byte csd;    
     PString strd(dptSentence, sizeof(dptSentence));    
     strd.print("$SDDPT,");    
     strd.print(nmeaDecoder.term_decimal(1)*cos(tda*r));   
     strd.print(",");   
     strd.print(tdo);   
     strd.print("*");    
     csd = checksum(dptSentence);    
     if (csd < 0x10) strd.print('0');    
     strd.print(csd, HEX);    
     Serial.println(dptSentence);   
    }   
    }   
    if (outputtmp == true) {   
    if (strcmp(title,"YXMTW") == 0) {   
     Serial.println(nmeaDecoder.sentence());   
    }   
    }   
    if (outputvlw == true) {   
    if (strcmp(title,"VWVLW") == 0) {   
     float tcd = nmeaDecoder.term_decimal(1);   
     if (vlwfirst == true) {   
     float tcd0 = tcd;   
     vlwfirst = false;   
     }   
     float dsr = tcd-tcd0;   
     char vlwSentence [23];    
     byte csl;    
     PString strl(vlwSentence, sizeof(vlwSentence));    
     strl.print("$VWVLW,");    
     strl.print(tcd);   
     strl.print(",N,");   
     strl.print(dsr);   
     strl.print(",N*");    
     csl = checksum(vlwSentence);    
     if (csl < 0x10) strl.print('0');    
     strl.print(csl, HEX);    
     Serial.println(vlwSentence);   
    }   
    }   
    }   
  }   
  // ***** End DST-800 Transducer ***** //   
     
  // ***** Integrated Navigation (MPU-9150 and derivatives) ***** //   
   unsigned long now = millis();   
   unsigned long delta;   
   int loopCount = 1;   
      
   while (imu->IMURead()) {   
    // this flushes remaining data in case we are falling behind   
    if (++loopCount >= 10)   
     continue;   
    fusion.newIMUData(imu->getGyro(), imu->getAccel(), imu->getCompass(), imu->getTimestamp());   
    sampleCount++;   
    if ((delta = now - lastRate) >= 1000) {     
     sampleCount = 0;   
     lastRate = now;   
    }   
    if ((now - lastDisplay) >= DISPLAY_INTERVAL) {   
     imuworking = 0; // since the imu is working, this resets the counter to 0  
     lastDisplay = now;    
     RTVector3 pose = fusion.getFusionPose();    
     RTVector3 gyro = imu->getGyro();    
     roll = pose.y()*-1*d;    
     pitch = pose.x()*d;    
     yaw = pose.z()*d;    
     ror = gyro.y()*d;   
     rop = gyro.x()*d;   
     rot = gyro.z()*d;    
     hdm = yaw-90;  // converts yaw to magnetic heading   
     if (yaw < 90 && yaw >= -179.99) {    
     hdm = yaw+270;    
     }    
     hdt = hdm-magd; // calculate true heading    
     if (hdt > 360) {    
     hdt = hdt-360;    
     }    
     if (hdt < 0.0) {    
     hdt = hdt+360;    
     }   
     
     // HDM Magnetic Heading //   
     if (outputhdm == true){    
     char hdmSentence [26];    
     byte csm;    
     PString strm(hdmSentence, sizeof(hdmSentence));    
     strm.print("$HCHDM,");    
     strm.print(hdm);    
     strm.print(",M*");    
     csm = checksum(hdmSentence);    
     if (csm < 0x10) strm.print('0');    
     strm.print(csm, HEX);    
     Serial.println(hdmSentence);    
     }   
        
     // HDT True Heading //   
     if (outputhdt == true){    
     char hdtSentence [26];    
     byte cst;    
     PString strt(hdtSentence, sizeof(hdtSentence));    
     strt.print("$HCHDT,");    
     strt.print(hdt);    
     strt.print(",T*");    
     cst = checksum(hdtSentence);    
     if (cst < 0x10) strt.print('0');    
     strt.print(cst, HEX);    
     Serial.println(hdtSentence);    
     }    
      
     // ROT Rate of Turn //   
     if (outputrot == true){    
     char rotSentence [21];    
     byte csr;    
     PString strr(rotSentence, sizeof(rotSentence));    
     strr.print("$TIROT,");    
     strr.print(rot);    
     strr.print(",A*");    
     csr = checksum(rotSentence);    
     if (csr < 0x10) strr.print('0');    
     strr.print(csr, HEX);    
     Serial.println(rotSentence);    
     }    
        
     // SHR Pitch and Roll (no heave... yet...)//   
     if (outputshr == true){    
     char shrSentence [50];    
     byte csp;    
     PString strp(shrSentence, sizeof(shrSentence));    
     strp.print("$INSHR,,");    
     strp.print(hdt);    
     strp.print(",T,");    
     strp.print(roll);    
     strp.print(",");    
     strp.print(pitch);    
     strp.print(",,,,,1,0*");    
     csp = checksum(shrSentence);    
     if (csp < 0x10) strp.print('0');    
     strp.print(csp, HEX);    
     Serial.println(shrSentence);    
     }   
        
     // VDR Set & Drift //   
     if (outputvdr == true) {   
     if (gpsworking < 20 && hsworking < 20) {   
      float set0 = (cmg-hdt)*r; // gets the difference between hdt and cmg in radians   
      if (set0 < 0) {   
      set0 = set0+(2*M_PI);   
      }   
      float drift = sqrt(pow(hs,2)+pow(sog,2)-2*hs*sog*(cos(set0)));   
      float set1 = (M_PI-atan2((sog*sin(set0)),(hs-sog*cos(set0))))*d;   
      if (hs == 0 && sog == 0) { // if you're not moving, then set = 0   
      set1 = set0;   
      }   
      float set = hdt+set1;   // converting to true instead of relative degrees   
      if (set > 360) {   
      set = set-360;   
      }   
      float setmag = set+magd;  // converting to magnetic instead of true   
      if (setmag > 360) {    
      setmag = setmag-360;    
      }    
      if (setmag < 0.0) {    
      setmag = setmag+360;    
      }   
     
      char vdrSentence [49];    
      byte csv;   
      PString strv(vdrSentence, sizeof(vdrSentence));   
      strv.print("$INVDR,");   
      strv.print(set);   
      strv.print(",T,");   
      strv.print(setmag);   
      strv.print(",M,");   
      strv.print(drift);   
      strv.print(",N*");   
      csv = checksum(vdrSentence);   
      if (csv < 0x10) strv.print('0');   
      strv.print(csv, HEX);   
      Serial.println(vdrSentence);  
      gpsworking =+ 1;     // this ticks up the timer to make sure we only calculate  
      hsworking =+ 1;      // VDR if we get good gps and dst-800 data   
     }   
     }   
    }   
   }   
   // ***** End Integrated Navigation ***** //   
     
   // ***** Anemometer ***** //   
   if (outputawv == true || outputtwv == true) {   
    if (Serial3.available() > 0 ) {    
    if (nmeaDecoder.decode(Serial3.read())) {  
    if (imuworking >= 5) {            // if there's no imu data, then just  
       Serial.println(nmeaDecoder.sentence()); // print the raw sentence with no corrections  
       imuworking = 5;  
    }  
    else {  
    imuworking =+ 1;  
    float ftk = .592484;            // feet per second to knots    
    float rwa0 = nmeaDecoder.term_decimal(1); // the raw relative wind angle    
    float ws0 = nmeaDecoder.term_decimal(3);  // the raw uncorrected wind speed    
    int quad = floor(rwa0/90);         // this gets which quadrant the wind is coming from (0,1,2,3)    
       
    // ITERATION ONE of corrections, for rotational velocity of pitch/roll/yaw    
    float sor = ror*r*anvo*ftk; // this assumes your anemometer has little or no lateral offset    
    float sop = rop*r*sqrt(pow(anvo,2)+pow(anlo,2))*ftk;    
    float sot = rot*r*anlo*ftk;    
    float wx0 = ws0*sin(rwa0*r);    
    float wy0 = ws0*cos(rwa0*r);    
    float wx1 = wx0-sor-sot;  // assume starboard tack or...    
    if (rwa0 > 180.0) {     // port    
     wx1 = wx0+sor+sot;    
    }    
    float wy1 = wy0+sop;        // assume beating upwind...    
    if (rwa0 < 270.0 && rwa0 > 90.0) { // or downwind    
     wy1 = wy0-sop;    
    }    
    // Now we have wx1 and wy1, the x and y wind components corrected for the movement of the mast    
      
    // ITERATION TWO of corrections, for absolute roll and pitch of the boat (yaw has no effect)    
    float wx2 = wx1/cos(roll*r);    
    float wy2 = wy1/cos(pitch*r);    
    float aws = sqrt(pow(wx2,2)+pow(wy2,2));    
    float awa = (atan2(wx2,wy2)*d)+90*quad;    
    if (wy2 == 0.0) { // this is for when there is no wind, and you can't divide by zero    
     awa = rwa0;    
    }    
    if (quad == 1 || quad == 3) { // we have to flip the atan function if the wind is coming from quadrant 1 or 3    
     awa = (atan2(wy2,wx2)*d)+90*quad;    
     if (wx2 == 0.0) {    
     awa = rwa0;    
     }    
    }    
    if (awa < 0) {    
     awa = awa+360;    
    }    
    if (awa > 360) {    
     awa = awa-360;    
    }    
    // Now we have aps and awa, the relative (or apparent) wind speed/angle corrected for boat roll and pitch    
      
    // ITERATION THREE of corrections, compensating for the boat's speed over ground    
    float twa0 = awa+hdt;    
    if (twa0 > 360) {    
     twa0 = twa0-360;    
    }    
    float dwa = twa0-cmg;    
    if (dwa < 0) {    
     dwa = dwa+360;    
    }    
    float tws = sqrt(pow(sog,2)+pow(aws,2)-2*aws*sog*cos(dwa*r));    
    float rwa1 = (M_PI-atan2((aws*sin(dwa*r)),(sog-aws*cos(dwa*r))))*d;    
    if (sog == 0.0 && aws == 0.0) {    
     rwa1 = awa;    
    }    
    float twa = cmg+rwa1;    
    if (twa > 360) {    
     twa = twa-360;    
    }    
    // Now we have tws, the true windspeed corrected for how fast the boat is rolling, pitching, and yawing, and for its roll/pitch angle, and for its induced wind    
    // We also have twa, which is the corrected true wind angle of ws, and rwa, the corrected relative wind angle from the boat's bow    
      
    // MWV sentence for apparent wind   
    if (outputawv == true) {    
    char mwvSentence [30];    
    byte csm;    
    PString strm(mwvSentence, sizeof(mwvSentence));    
    strm.print("$WIMWV,");    
    strm.print(awa);    
    strm.print(",R,");    
    strm.print(aws);    
    strm.print(",N,A*");    
    csm = checksum(mwvSentence);    
    if (csm < 0x10) strm.print('0');    
    strm.print(csm, HEX);    
    Serial.println(mwvSentence);   
    }   
      
    // MWV Sentence for true wind   
    if (outputtwv == true) {   
    char mwdSentence [30];    
    byte csd;    
    PString strd(mwdSentence, sizeof(mwdSentence));    
    strd.print("$WIMWV,");    
    strd.print(twa);    
    strd.print(",T,");    
    strd.print(tws);    
    strd.print(",N,A*");    
    csd = checksum(mwdSentence);    
    if (csd < 0x10) strd.print('0');    
    strd.print(csd, HEX);    
    Serial.println(mwdSentence);  
    }   
    }    
    }    
    }   
   }   
   // ***** End Anemometer ***** //   
     
  }  

Instructions for the King Tide Sailing Arduino NMEA-0183 Multiplexer

Which Sentences to Include

Let's just work our way down from the top. First up, you will need to alter these boolean variables to select which sentences to output (boolean simple means a variable that contains either TRUE or FALSE).


It's pretty self-explanatory; if you want to print out a sentence, make sure it's boolean is "true" instead of "false." My setup has a few caveats, however. For one, I'm using the mighty Airmar DST-800 Thru-Hull Triducer Smart Sensor, which outputs quite a few sentences (hullspeed, depth, water temp, and voyage distance). Also, I'm using the GlobalSat BU-353 USB GPS Receiver following David's Guide to connect it to the Arduino. This spits out way more NMEA-0183 sentences than I care for; so I used the NMEA Library to only accept the RMC sentence (which gives you time, position, course, and speed).

The reason for this is because the MPU-9150 is running every half second; and if I accepted all the GPS sentences, it takes longer than half a second to print them all. So what happened was that the GPS Serial port would start printing the sentences, and then the MPU-9150 would cut it off and print its own. It's pretty easy to get a little too cluttered on the one output Serial port, so I recommend only using the minimum number of sentences to output. The only benefit I can see to having the other GPS sentences is if you have some application that needs to know how many satellites are in view; for instance, navigating in dangerous waters where you need to know if you can rely on your GPS position. Or perhaps certain DSC VHF radios need those other sentences. If you wish to include them, you must change the code such that instead of only including RMC sentences, it includes ALL sentences; that would require quite a bit of change, and is left as an exercise to the reader if he or she so chooses.

Vessel Information

Next up is the information specific to your vessel; geographic location for magnetic deviation, depth sounder offset, and anemometer offset.


The variable "magd" is simply your magnetic deviation. You will have to update this whenever you move somewhere where the value changes. Not sure what your magnetic deviation is? Check out this page, and it'll tell you precisely what it is. If the value is given as "East" such as 14E, then type in "magd = -14.0" since east values are considered to be negative values.

"tdo" is simply the transducer offset from either the waterline or bottom of the keel. If you want the absolute depth of water from the surface of the water, enter in the positive distance between the waterline on your boat and the location of the depth sounder. If you want the depth below the keel (like me), then enter in the distance between the depth sounder and the bottom of the keel as a negative number. Also, this is measured in meters, not feet.

"tda" is the angle offset of the depth sounder. Most people have it pointed straight down (as it should be), but if you're like me, you may have been forced to place the depth sounder at an angle from straight down. Simply enter this angle here in degrees (mine is pointing 23 degrees to starboard). This will compensate for the offset, assuming the ocean/lake/bay floor is flat.

"anvo" and "anlo" are the vertical and longitudinal offsets of the anemometer, respectively. Here is a picture of my boat pulled from http://sailboatdata.com/ with colored lines added to measure both numbers: 

The green circle represents the center of buoyancy (at least a rough estimate). It's approximately 2/3 the way forward of the keel, and I just guessed for the vertical location. The redline is the vertical offset, and the blue line is the longitudinal offset. If you know the length of the waterline of your boat, then you can do a quick comparison to figure out how long the offsets are. For mine, the anvo is approximately 53 feet, and the anlo is approximately 5 feet.

How Often to Calculate Certain Sentences


DISPLAY_INTERVAL is how often the MPU-9150 will run its script, measured in milliseconds. So a value of 500 means it will run twice per second, which is a pretty good resolution without clogging the Serial port or taxing the system. I recommend leaving it at 500, but you can tweak with this and see if you can get it working smoothly at faster rates. Keep in mind if you change this, though, you will have to change the next three variables.

"gpsworking," and "hsworking," are simply counters that tick up every time the MPU runs. Every time the GPS outputs a sentence, it resets the counter back to 0. Same thing with the hullspeed. When they reach 20 (so after 10 seconds, since the MPU runs every half second), then VDR won't calculate because you are no longer receiving GPS data or hullspeed data.

"imuworking," follows the same idea, only kind of backwards; if the IMU isn't working (the MPU-9150), then this counter ticks up each time the wind anemometer outputs a sentence. So basically, the anemometer will output 5 sentences, and if the IMU hasn't ran in that time, it will stop correcting for pitch/roll/yaw, and will only spit out the raw relative wind sentence.

Final Thoughts

This works fine, good enough for most day sailing/racing. But you may want to add some increased functionality based on your needs. What I would like to do next is output every sentence, including all the GPS sentences, in a way that doesn't clog up the Serial port. I think this is possible by using the PString library, so that anytime a sentence comes in from its respective Serial port, the script appends the sentence to a giant array of sentences instead of repeating it immediately to the output Serial port. Then at the end of the whole script, it would print the sentence every time the MPU runs. 

But it works fine now, and the next step is to writeup how to get the Raspberry Pi working to pipe the sentences wirelessly, and to get a Kindle to display the information over Wi-Fi. But I also think it's quite easy to get too deep in the weeds with this, and spend a lot of time and resources trying to perfect this sturdy little device.

Sometimes, it might just be better to go out sailing.

Comments

  1. Hello Connor,

    Thanks for the excellent blog. I made the compass and it talks fine with OpenCPN. However, I cannot make it talk direct to my Garmin chartplotter. It will talk via the output of OpenCPN. Many different wirings tested.

    Can I connect serial tx/rx direct from the board of an Arduino Mega direct to the NMEA cable of the plotter or will I blow something up?

    Thanks again.
    Mike

    ReplyDelete
    Replies
    1. Which model do you have?

      Here are a few things to consider:

      - Make sure it accepts NMEA 0183, not NMEA 2000
      - The Arduino's output fluctuates between 0V and 5V; the standard NMEA voltage is 0-12V signals. So this means that if you plug it directly in, you're not going to "fry" your electronics from over voltage.
      - But it also may not recognize that sort of signal unless it goes up to 12V or so.

      So...

      You're left with really two options.

      1. You can plug the TX cable from the Arduino directly into the Receive port on your chartplotter, *if* it has only one wire input (usually labeled NMEA RX or something like that). If it doesn't work, then you need a TTL to RS232 converter, but this is probably going to be difficult because it will invert the signal, meaning every 1 will be a 0 and so forth... so you would have to edit the code so that it Transmits not over Serial Port 1, but a *digital* Serial port using a digital pin and set the "Invert" parameter to true.

      2. If you have TWO NMEA input wires (usually labelled NMEA + and NMEA -), then you need a TTL to RS422 converter. This will also invert your signal, but it's easy to fix; if it isn't recognizing the signal, then just swap the A and B wires on the converter going directly to the chartplotter. See this post for more info about all that: http://kingtidesailing.blogspot.com/2015/09/how-to-connect-any-nmea-0183-device-to.html

      Unfortunately without knowing the model number of the Chartplotter, I can't help you much more than that. A picture of your input ports would be good too (also your setup, but that's just more out of curiosity).

      Delete
    2. Oh, also, there would be no need to hook up a Transmit cable from your chart plotter to the Receive pin on the Arduino, since there is no communication back to the Arduino (unless you modified the code)

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Thanks for the detailed reply. My chartplotter is the Garmin GPSMAP 4010.

    I went back to the wiring of the rs232 this morning. Success at last.

    I needed to connect both rs-out and rs-in (and gnd) of the level shifter to Port 1 of the plotter. This is set for NMEA 0183, standard speed and has tx and rx. It was the missing tx from the plotter that was causing the problem.

    Only the compass output of the multiplexer is being used and this seems to run at 10Hz without a problem. At this speed radar stabilisation should be possible and also MARPA. Fantastic.

    Thanks again for your help. This is the best compass setup I have found on the Web. The tilt stabilisation is impressive.

    Mike

    ReplyDelete
  4. This is awesome! Proud of you that you finally got it done, woohoo!

    ReplyDelete
  5. Your work is AMAZING :) Can i ask you something ? have you tried using IMU (MPU) with NRF24 ?

    ReplyDelete
    Replies
    1. I have not, and I actually don't know what that is. But I think it's also important to note that I'm not a programmer or engineer by trade, so there's a whole lot about all of this I don't know. It's kind of a miracle I've made it this far.

      Delete
  6. 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. Have you tried removing the GPS and only trying to send the echo sounder sentences? My original depth sounder was incompatible with this project since it used analogue signals instead of NMEA, so I had to get a different one.

      Delete

Post a Comment

Popular Posts