<< Summary

NMEA multiplexing with the Raspberry PI

We will show a possibility to add data to an NMEA stream.
We will use the NMEA Console project as a starting point.

The idea is simple. It's relying on several points:

Turning the sensor data into NMEA string

No brainer. Done in the NMEA Parser (see project), class ocss.nmea.parser.StringGenerator.java, along with their reciprocal functions in ocss.nmea.parser.StringParsers.java.

Using the already existing NMEA Listeners

The Listeners we use are part of the NMEAContext, accessible from the Desktop. We need to use two different lists of listeners, NMEAListener and NMEAReaderListener, one is used to parse the incoming data and put them in the cache, the other to trigger the possible re-broadcasting (which we are specially interested in).

Flexibility

The number of sensors that can be plugged in the Raspberry PI is not limited, and growing.
A flexible approach is to use the concept of user-exits, already implemented in the Navigation Desktop. Those user-exits can be added on demand, removed, replaced, etc. Perfect for this kind of context.

The code

The user exit in made out of two classes.

Sensor2NMEA.java

      

     1  package olivsoftdesktopuserexits;
     2  
     3  import olivsoftdesktop.DesktopUserExitInterface;
     4  
     5  import olivsoftdesktopuserexits.rpisensor.AdafruitBMP180Reader;
     6  
     7  /**
     8   * Reads a sensor (BMP180) connected to the Raspberry PI
     9   * and turn the data into NMEA string to broadcast them.
    10   * 
    11   * to be used with -ue:olivsoftdesktopuserexits.Sensor2NMEA
    12   */
    13  public class Sensor2NMEA
    14    implements DesktopUserExitInterface
    15  {
    16    private AdafruitBMP180Reader sensorReader = null;
    17    
    18    public Sensor2NMEA()
    19    {
    20      super();
    21      sensorReader = new AdafruitBMP180Reader();
    22    }
    23  
    24    @Override
    25    public void start()
    26    {
    27      System.out.println("Method 'start':" + this.getClass().getName() + " User exit is starting...");
    28      // Start reading the sensor and broadcasting NMEA data
    29      Thread ue = new Thread()
    30        {
    31          public void run()
    32          {
    33            sensorReader.startReading();
    34          }
    35        };
    36      ue.start();
    37    }
    38  
    39    @Override
    40    public void stop()
    41    {
    42      System.out.println(this.getClass().getName() + " is terminating");
    43      sensorReader.stopReading();
    44    }
    45    
    46    @Override
    47    public void describe()
    48    {
    49      System.out.println("Designed to run on the Raspberry PI.");
    50      System.out.println("Reads the BMP180 sensor, and turns it Air Temperature and Barometric Pressure data into MTA & MMB NMEA Strings.");
    51      System.out.println("Those data will be treated as if they were coming from the NMEA Station.");
    52    }
    53  }
      
        
Notice the lines 21 & 33.

AdafruitBMP180Reader

      

     1  package olivsoftdesktopuserexits.rpisensor;
     2  
     3  
     4  import com.pi4j.io.i2c.I2CBus;
     5  import com.pi4j.io.i2c.I2CDevice;
     6  import com.pi4j.io.i2c.I2CFactory;
     7  
     8  import java.io.IOException;
     9  
    10  import nmea.event.NMEAReaderListener;
    11  
    12  import nmea.server.ctx.NMEAContext;
    13  
    14  import ocss.nmea.api.NMEAEvent;
    15  import ocss.nmea.api.NMEAListener;
    16  import ocss.nmea.parser.StringGenerator;
    17  
    18  /*
    19   * Altitude, Pressure, Temperature
    20   */
    21  public class AdafruitBMP180Reader
    22  {
    23    // Minimal constants carried over from Arduino library
    24    /*
    25    Prompt> sudo i2cdetect -y 1
    26         0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
    27    00:          -- -- -- -- -- -- -- -- -- -- -- -- --
    28    10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    29    20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    30    30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    31    40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    32    50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    33    60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    34    70: -- -- -- -- -- -- -- 77
    35     */
    36    // Those 2 next addresses are returned by "sudo i2cdetect -y 1", see above.
    37    public final static int BMP180_ADDRESS = 0x77; 
    38    // Operating Modes
    39    public final static int BMP180_ULTRALOWPOWER     = 0;
    40    public final static int BMP180_STANDARD          = 1;
    41    public final static int BMP180_HIGHRES           = 2;
    42    public final static int BMP180_ULTRAHIGHRES      = 3;
    43  
    44    // BMP085 Registers
    45    public final static int BMP180_CAL_AC1           = 0xAA;  // R   Calibration data (16 bits)
    46    public final static int BMP180_CAL_AC2           = 0xAC;  // R   Calibration data (16 bits)
    47    public final static int BMP180_CAL_AC3           = 0xAE;  // R   Calibration data (16 bits)
    48    public final static int BMP180_CAL_AC4           = 0xB0;  // R   Calibration data (16 bits)
    49    public final static int BMP180_CAL_AC5           = 0xB2;  // R   Calibration data (16 bits)
    50    public final static int BMP180_CAL_AC6           = 0xB4;  // R   Calibration data (16 bits)
    51    public final static int BMP180_CAL_B1            = 0xB6;  // R   Calibration data (16 bits)
    52    public final static int BMP180_CAL_B2            = 0xB8;  // R   Calibration data (16 bits)
    53    public final static int BMP180_CAL_MB            = 0xBA;  // R   Calibration data (16 bits)
    54    public final static int BMP180_CAL_MC            = 0xBC;  // R   Calibration data (16 bits)
    55    public final static int BMP180_CAL_MD            = 0xBE;  // R   Calibration data (16 bits)
    56    public final static int BMP180_CONTROL           = 0xF4;
    57    public final static int BMP180_TEMPDATA          = 0xF6;
    58    public final static int BMP180_PRESSUREDATA      = 0xF6;
    59    public final static int BMP180_READTEMPCMD       = 0x2E;
    60    public final static int BMP180_READPRESSURECMD   = 0x34;
    61  
    62    private int cal_AC1 = 0;
    63    private int cal_AC2 = 0;
    64    private int cal_AC3 = 0;
    65    private int cal_AC4 = 0;
    66    private int cal_AC5 = 0;
    67    private int cal_AC6 = 0;
    68    private int cal_B1  = 0;
    69    private int cal_B2  = 0;
    70    private int cal_MB  = 0;
    71    private int cal_MC  = 0;
    72    private int cal_MD  = 0;
    73  
    74    private static boolean verbose = false;
    75    
    76    private I2CBus bus;
    77    private I2CDevice bmp180;
    78    private int mode = BMP180_STANDARD;
    79    
    80    public AdafruitBMP180Reader()
    81    {
    82      this(BMP180_ADDRESS);
    83    }
    84    
    85    public AdafruitBMP180Reader(int address)
    86    {
    87      try
    88      {
    89        // Get i2c bus
    90        bus = I2CFactory.getInstance(I2CBus.BUS_1); // Depends onthe RasPI version
    91        if (verbose)
    92          System.out.println("Connected to bus. OK.");
    93  
    94        // Get device itself
    95        bmp180 = bus.getDevice(address);
    96        if (verbose)
    97          System.out.println("Connected to device. OK.");
    98        
    99        try { this.readCalibrationData(); }
   100        catch (Exception ex)
   101        { ex.printStackTrace(); }            
   102      }
   103      catch (IOException e)
   104      {
   105        System.err.println(e.getMessage());
   106      }
   107    }
   108    
   109    private int readU8(int reg) throws Exception
   110    {
   111      // "Read an unsigned byte from the I2C device"
   112      int result = 0;
   113      try
   114      {
   115        result = this.bmp180.read(reg);
   116        if (verbose)
   117          System.out.println("I2C: Device " + BMP180_ADDRESS + " returned " + result + " from reg " + reg);
   118      }
   119      catch (Exception ex)
   120      { ex.printStackTrace(); }
   121      return result;
   122    }
   123    
   124    private int readS8(int reg) throws Exception
   125    {
   126      // "Reads a signed byte from the I2C device"
   127      int result = 0;
   128      try
   129      {
   130        result = this.bmp180.read(reg);
   131        if (result > 127)
   132          result -= 256;
   133        if (verbose)
   134          System.out.println("I2C: Device " + BMP180_ADDRESS + " returned " + result + " from reg " + reg);
   135      }
   136      catch (Exception ex)
   137      { ex.printStackTrace(); }
   138      return result;
   139    }
   140    
   141    private int readU16(int register) throws Exception
   142    {
   143      int hi = this.readU8(register);
   144      int lo = this.readU8(register + 1);
   145      return (hi << 8) + lo;
   146    }
   147  
   148    private int readS16(int register) throws Exception
   149    {
   150      int hi = this.readS8(register);
   151      int lo = this.readU8(register + 1);
   152      return (hi << 8) + lo;
   153    }
   154  
   155    public void readCalibrationData() throws Exception
   156    {
   157      // "Reads the calibration data from the IC"
   158      cal_AC1 = readS16(BMP180_CAL_AC1);   // INT16
   159      cal_AC2 = readS16(BMP180_CAL_AC2);   // INT16
   160      cal_AC3 = readS16(BMP180_CAL_AC3);   // INT16
   161      cal_AC4 = readU16(BMP180_CAL_AC4);   // UINT16
   162      cal_AC5 = readU16(BMP180_CAL_AC5);   // UINT16
   163      cal_AC6 = readU16(BMP180_CAL_AC6);   // UINT16
   164      cal_B1 =  readS16(BMP180_CAL_B1);    // INT16
   165      cal_B2 =  readS16(BMP180_CAL_B2);    // INT16
   166      cal_MB =  readS16(BMP180_CAL_MB);    // INT16
   167      cal_MC =  readS16(BMP180_CAL_MC);    // INT16
   168      cal_MD =  readS16(BMP180_CAL_MD);    // INT16
   169      if (verbose)
   170        showCalibrationData();
   171    }
   172          
   173    private void showCalibrationData()
   174    {
   175      // "Displays the calibration values for debugging purposes"
   176      System.out.println("DBG: AC1 = " + cal_AC1);
   177      System.out.println("DBG: AC2 = " + cal_AC2);
   178      System.out.println("DBG: AC3 = " + cal_AC3);
   179      System.out.println("DBG: AC4 = " + cal_AC4);
   180      System.out.println("DBG: AC5 = " + cal_AC5);
   181      System.out.println("DBG: AC6 = " + cal_AC6);
   182      System.out.println("DBG: B1  = " + cal_B1);
   183      System.out.println("DBG: B2  = " + cal_B2);
   184      System.out.println("DBG: MB  = " + cal_MB);
   185      System.out.println("DBG: MC  = " + cal_MC);
   186      System.out.println("DBG: MD  = " + cal_MD);
   187    }
   188                
   189    public int readRawTemp() throws Exception
   190    {
   191      // "Reads the raw (uncompensated) temperature from the sensor"
   192      bmp180.write(BMP180_CONTROL, (byte)BMP180_READTEMPCMD);
   193      waitfor(5);  // Wait 5ms
   194      int raw = readU16(BMP180_TEMPDATA);
   195      if (verbose)
   196        System.out.println("DBG: Raw Temp: " + (raw & 0xFFFF) + ", " + raw);
   197      return raw;
   198    }
   199        
   200    public int readRawPressure() throws Exception
   201    {
   202      // "Reads the raw (uncompensated) pressure level from the sensor"
   203      bmp180.write(BMP180_CONTROL, (byte)(BMP180_READPRESSURECMD + (this.mode << 6)));
   204      if (this.mode == BMP180_ULTRALOWPOWER)
   205        waitfor(5);
   206      else if (this.mode == BMP180_HIGHRES)
   207        waitfor(14);
   208      else if (this.mode == BMP180_ULTRAHIGHRES)
   209        waitfor(26);
   210      else
   211        waitfor(8);
   212      int msb = bmp180.read(BMP180_PRESSUREDATA);
   213      int lsb = bmp180.read(BMP180_PRESSUREDATA + 1);
   214      int xlsb = bmp180.read(BMP180_PRESSUREDATA + 2);
   215      int raw = ((msb << 16) + (lsb << 8) + xlsb) >> (8 - this.mode);
   216      if (verbose)
   217        System.out.println("DBG: Raw Pressure: " + (raw & 0xFFFF) + ", " + raw);
   218      return raw;
   219    }
   220        
   221    public float readTemperature() throws Exception
   222    {
   223      // "Gets the compensated temperature in degrees celcius"
   224      int UT = 0;
   225      int X1 = 0;
   226      int X2 = 0;
   227      int B5 = 0;
   228      float temp = 0.0f;
   229  
   230      // Read raw temp before aligning it with the calibration values
   231      UT = this.readRawTemp();
   232      X1 = ((UT - this.cal_AC6) * this.cal_AC5) >> 15;
   233      X2 = (this.cal_MC << 11) / (X1 + this.cal_MD);
   234      B5 = X1 + X2;
   235      temp = ((B5 + 8) >> 4) / 10.0f;
   236      if (verbose)
   237        System.out.println("DBG: Calibrated temperature = " + temp + " C");
   238      return temp;
   239    }
   240  
   241    public float readPressure() throws Exception
   242    {
   243      // "Gets the compensated pressure in pascal"
   244      int UT = 0;
   245      int UP = 0;
   246      int B3 = 0;
   247      int B5 = 0;
   248      int B6 = 0;
   249      int X1 = 0;
   250      int X2 = 0;
   251      int X3 = 0;
   252      int p = 0;
   253      int B4 = 0;
   254      int B7 = 0;
   255  
   256      UT = this.readRawTemp();
   257      UP = this.readRawPressure();
   258  
   259      // You can use the datasheet values to test the conversion results
   260      // boolean dsValues = true;
   261      boolean dsValues = false;
   262  
   263      if (dsValues)
   264      {
   265        UT = 27898;
   266        UP = 23843;
   267        this.cal_AC6 = 23153;
   268        this.cal_AC5 = 32757;
   269        this.cal_MB = -32768;
   270        this.cal_MC = -8711;
   271        this.cal_MD = 2868;
   272        this.cal_B1 = 6190;
   273        this.cal_B2 = 4;
   274        this.cal_AC3 = -14383;
   275        this.cal_AC2 = -72;
   276        this.cal_AC1 = 408;
   277        this.cal_AC4 = 32741;
   278        this.mode = BMP180_ULTRALOWPOWER;
   279        if (verbose)
   280          this.showCalibrationData();
   281      }
   282      // True Temperature Calculations
   283      X1 = (int)((UT - this.cal_AC6) * this.cal_AC5) >> 15;
   284      X2 = (this.cal_MC << 11) / (X1 + this.cal_MD);
   285      B5 = X1 + X2;
   286      if (verbose)
   287      {
   288        System.out.println("DBG: X1 = " + X1);
   289        System.out.println("DBG: X2 = " + X2);
   290        System.out.println("DBG: B5 = " + B5);
   291        System.out.println("DBG: True Temperature = " + (((B5 + 8) >> 4) / 10.0)  + " C");
   292      } 
   293      // Pressure Calculations
   294      B6 = B5 - 4000;
   295      X1 = (this.cal_B2 * (B6 * B6) >> 12) >> 11;
   296      X2 = (this.cal_AC2 * B6) >> 11;
   297      X3 = X1 + X2;
   298      B3 = (((this.cal_AC1 * 4 + X3) << this.mode) + 2) / 4;
   299      if (verbose)
   300      {
   301        System.out.println("DBG: B6 = " + B6);
   302        System.out.println("DBG: X1 = " + X1);
   303        System.out.println("DBG: X2 = " + X2);
   304        System.out.println("DBG: X3 = " + X3);
   305        System.out.println("DBG: B3 = " + B3);
   306      }
   307      X1 = (this.cal_AC3 * B6) >> 13;
   308      X2 = (this.cal_B1 * ((B6 * B6) >> 12)) >> 16;
   309      X3 = ((X1 + X2) + 2) >> 2;
   310      B4 = (this.cal_AC4 * (X3 + 32768)) >> 15;
   311      B7 = (UP - B3) * (50000 >> this.mode);
   312      if (verbose)
   313      {
   314        System.out.println("DBG: X1 = " + X1);
   315        System.out.println("DBG: X2 = " + X2);
   316        System.out.println("DBG: X3 = " + X3);
   317        System.out.println("DBG: B4 = " + B4);
   318        System.out.println("DBG: B7 = " + B7);
   319      }
   320      if (B7 < 0x80000000)
   321        p = (B7 * 2) / B4;
   322      else
   323        p = (B7 / B4) * 2;
   324  
   325      if (verbose)
   326        System.out.println("DBG: X1 = " + X1);
   327        
   328      X1 = (p >> 8) * (p >> 8);
   329      X1 = (X1 * 3038) >> 16;
   330      X2 = (-7357 * p) >> 16;
   331      if (verbose)
   332      {
   333        System.out.println("DBG: p  = " + p);
   334        System.out.println("DBG: X1 = " + X1);
   335        System.out.println("DBG: X2 = " + X2);
   336      }
   337      p = p + ((X1 + X2 + 3791) >> 4);
   338      if (verbose)
   339        System.out.println("DBG: Pressure = " + p + " Pa");
   340  
   341      return p;
   342    }
   343        
   344    private int standardSeaLevelPressure = 101325;
   345  
   346    public void setStandardSeaLevelPressure(int standardSeaLevelPressure)
   347    {
   348      this.standardSeaLevelPressure = standardSeaLevelPressure;
   349    }
   350  
   351    public double readAltitude() throws Exception
   352    {
   353      // "Calculates the altitude in meters"
   354      double altitude = 0.0;
   355      float pressure = readPressure();
   356      altitude = 44330.0 * (1.0 - Math.pow(pressure / standardSeaLevelPressure, 0.1903));
   357      if (verbose)
   358        System.out.println("DBG: Altitude = " + altitude);
   359      return altitude;
   360    }
   361        
   362    private static void waitfor(long howMuch)
   363    {
   364      try 
   365      { 
   366        synchronized (Thread.currentThread())
   367        {
   368          Thread.currentThread().wait(howMuch); 
   369        }
   370      } catch (InterruptedException ie) { ie.printStackTrace(); }
   371    }
   372    
   373    
   374    private boolean go = true;
   375    
   376    public void stopReading()
   377    {
   378      go = false;
   379      synchronized (Thread.currentThread())
   380      {
   381        System.out.println("Stopping the reader");
   382        Thread.currentThread().notify();
   383      }
   384    }
   385    
   386    public void startReading()
   387    {
   388      System.out.println("Starting " + this.getClass().getName() + "...");
   389      go = true;
   390      while (go)
   391      {
   392        float press = 0;
   393        float temp  = 0;
   394        double alt  = 0;
   395    
   396        try { press = this.readPressure(); } 
   397        catch (Exception ex) 
   398        { 
   399          System.err.println(ex.getMessage()); 
   400          ex.printStackTrace();
   401        }
   402        this.setStandardSeaLevelPressure((int)press); // As we ARE at the sea level (in San Francisco).
   403        try { alt = this.readAltitude(); } 
   404        catch (Exception ex) 
   405        { 
   406          System.err.println(ex.getMessage()); 
   407          ex.printStackTrace();
   408        }
   409        try { temp = this.readTemperature(); } 
   410        catch (Exception ex) 
   411        { 
   412          System.err.println(ex.getMessage()); 
   413          ex.printStackTrace();
   414        }
   415        
   416        String nmeaMMB = StringGenerator.generateMMB("II", (press / 100)); // Bars
   417        String nmeaMTA = StringGenerator.generateMTA("II", temp);          // In Celcius
   418  //    System.out.println("... " + nmeaMMB + ", " + nmeaMTA + ", " + NMEAContext.getInstance().getNMEAListeners().size() + ", " + NMEAContext.getInstance().getReaderListeners().size() + " listener(s)");
   419        broadcastNMEASentence(nmeaMMB);
   420        broadcastNMEASentence(nmeaMTA);
   421          
   422        waitfor(1000L); // One sec.
   423      }
   424      System.out.println("Reader stopped.");
   425    }
   426    
   427    private void broadcastNMEASentence(String nmea)
   428    {
   429      for (NMEAListener l : NMEAContext.getInstance().getNMEAListeners())
   430        l.dataDetected(new NMEAEvent(this, nmea));
   431      for (NMEAReaderListener l : NMEAContext.getInstance().getReaderListeners())
   432        l.manageNMEAString(nmea);
   433    }
   434  }
      
        
The way to deal with the sensor is the same as the one described in this document.
See how the data are turned into an NMEA Sentence on lines 416 & 417. The NMEA Strings are MMB & MTA. I know that XDR is recommended (and available in the NMEA Parser project), but still.
See how they are broadcasted to the listeners on lines 419 & 420, invoking the broadcastNMEASentence function, defined line 427.

To know how to compile, archive, and use the code as a user-exit in the NMEA Console, take a look at this document.

Demo

We show here how to integrate in the NMEA data flow the values coming from a BMP180 sensor, providing Air Temperature, Atmospheric Pressure, and Altitude (based on pressure), which we don't care about, for some reason...
We will turn the sensor data into NMEA MBB and MTA Strings.
To see how to process the signal from the BMP180, see here, for the code and details.


The Raspberry PI, with its BPM180 sensor, on a Slice of PI.
Note: After doing some tests, this is not the best location for the BMP180, it is too close to the CPU of the Raspberry PI, that generates heat. The temperature readings are impacted by the CPU temperature...


From another machine, connect on the RasPI using - for example - PuTTY, and start the vncserver.


VNC is now started, we can connect.


VNC connection...


Start the "olivsoft" console, and choose HC, for Headless Console.


Option for the Headless Console...


That's it! From another machine, access the RasPI (TCP:7001 in this case), and check out the Data Viewer. The Air Temperature and Atmospheric Pressure - coming from the BMP180 connected on the RasPI - are available in the list, good for display, for logging, rendering..., whatever can be done with all the other data.


Whatever understands NMEA cannot tell the difference. Here is OpenCPN.

Here is a sample of the logging (regular logging) that can be done with the sensors injecting data in the NMEA stream:


  $IIHDG,003,,,15,E*15
  $IIMTW,+15.0,C*3C
  $IIMWV,155,R,06.2,N,A*16
  $IIMWV,155,T,06.6,N,A*14
  $IIMMB,29.9870,I,1.0154,B*75
  $IIMTA,25.6,C*04
  $IIRMC,214009,A,3730.080,N,12228.857,W,00.0,080,090714,15,E,A*19
  $IIXDR,P,1.0154,B,0*73
  $IIVHW,,,003,M,00.0,N,,*67
  $WIMDA,29.984,I,1.015,B,25.6,C,15.0,C,,,,,172.0,T,173.0,M,6.2,N,3.2,M*7D
  $IIVLW,08195,N,000.0,N*56
  $IIVWR,153,R,06.1,N,,,,*61
  $IIGLL,3730.081,N,12228.856,W,214011,A,A*4B
  $IIHDG,003,,,15,E*15
  $IIMTW,+15.0,C*3C
  $IIMWV,153,R,06.1,N,A*13
  $IIMWV,155,T,06.2,N,A*10
  $IIRMB,A,0.00,L,,HMB-3   ,,,,,001.20,184,,V,A*00
  $IIMMB,29.9900,I,1.0155,B*72
  $IIMTA,25.6,C*04
  $IIXDR,P,1.0155,B,0*72
  $WIMDA,29.987,I,1.015,B,25.6,C,15.0,C,,,,,170.0,T,171.0,M,6.1,N,3.1,M*7E
  $IIRMC,214011,A,3730.081,N,12228.856,W,00.0,080,090714,15,E,A*10
  $IIVHW,,,003,M,00.0,N,,*67
  $IIVLW,08195,N,000.0,N*56
  $IIVWR,150,R,05.9,N,,,,*69
  $IIGLL,3730.081,N,12228.856,W,214011,A,A*4B
  $IIHDG,003,,,15,E*15
  $IIMTW,+15.0,C*3C
  $IIMWV,150,R,05.9,N,A*1B
  $IIMWV,153,T,06.1,N,A*15
  $XXBAT,13.34,V,910,88*12
  $IIMMB,29.9882,I,1.0154,B*78
  $IIMTA,25.6,C*04
  $IIRMB,A,0.00,L,,HMB-3   ,,,,,001.20,184,,V,A*00
  $IIXDR,P,1.0154,B,0*73
  $IIRMC,214013,A,3730.080,N,12228.856,W,00.0,080,090714,15,E,A*13
      

Source code

Available here.

A small detail: the BMP180 is only $9.95...


© 2014, OlivSoft