A data acquisition and telemetry program for model rockets running on the MAX32630FTHR platform.

Dependencies:   BMI160 CommandDispatcher RadioHead SDFileSystem StringFifo USBDevice USBMSD_SD gps max31865 max32630fthr swspi

Files at this revision

API Documentation at this revision

Comitter:
danjulio
Date:
Sun Jun 11 04:26:22 2017 +0000
Commit message:
Initial commit of Rocket, a model rocket data acquisition and telemetry program for the MAX32630FTHR platform.

Changed in this revision

BMI160.lib Show annotated file Show diff for this revision Revisions of this file
CommandDispatcher.lib Show annotated file Show diff for this revision Revisions of this file
RadioHead.lib Show annotated file Show diff for this revision Revisions of this file
SDFileSystem.lib Show annotated file Show diff for this revision Revisions of this file
StringFifo.lib Show annotated file Show diff for this revision Revisions of this file
USBDevice.lib Show annotated file Show diff for this revision Revisions of this file
USBMSD_SD.lib Show annotated file Show diff for this revision Revisions of this file
gps.lib Show annotated file Show diff for this revision Revisions of this file
main.cpp Show annotated file Show diff for this revision Revisions of this file
max31865.lib Show annotated file Show diff for this revision Revisions of this file
max32630fthr.lib Show annotated file Show diff for this revision Revisions of this file
mbed-os.lib Show annotated file Show diff for this revision Revisions of this file
swspi.lib Show annotated file Show diff for this revision Revisions of this file
diff -r 000000000000 -r 83f4079b2bac BMI160.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/BMI160.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://developer.mbed.org/teams/MaximIntegrated/code/BMI160/#0ae99e97bcf5
diff -r 000000000000 -r 83f4079b2bac CommandDispatcher.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CommandDispatcher.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://mbed.org/users/rominos2/code/CommandDispatcher/#ca5d575c03c8
diff -r 000000000000 -r 83f4079b2bac RadioHead.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RadioHead.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://developer.mbed.org/users/danjulio/code/RadioHead/#e69d086cb053
diff -r 000000000000 -r 83f4079b2bac SDFileSystem.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SDFileSystem.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://developer.mbed.org/teams/mbed/code/SDFileSystem/#8db0d3b02cec
diff -r 000000000000 -r 83f4079b2bac StringFifo.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/StringFifo.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://developer.mbed.org/users/danjulio/code/StringFifo/#093546398fcd
diff -r 000000000000 -r 83f4079b2bac USBDevice.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/USBDevice.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://developer.mbed.org/teams/MaximIntegrated/code/USBDevice/#c5e178adb138
diff -r 000000000000 -r 83f4079b2bac USBMSD_SD.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/USBMSD_SD.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://developer.mbed.org/users/danjulio/code/USBMSD_SD/#46888c9d5cd7
diff -r 000000000000 -r 83f4079b2bac gps.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gps.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://developer.mbed.org/users/danjulio/code/gps/#8fc18502886a
diff -r 000000000000 -r 83f4079b2bac main.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main.cpp	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1315 @@
+/*
+ * Model Rocket Data Acquisition and Telemetry system using the MAX32630FTHR.
+ *  - Acceleration using on-board BMI160.
+ *  - Rotation using ALS-PT19-315C Ambient Light Sensor with analog input
+ *  - Temperature using PT-100 RTD and MAX31865 Amplifier/Digitizer
+ *  - GPS location and altitude using external serial receiver.  Receiver
+ *    includes local battery for faster acquisition.
+ *  - Battery voltage using on-board MAX14690 PMIC.
+ *  - Radio communication for telemetry and command using a RFM95W LoRa module.
+ *
+ * Data is stored as text sentences to a file on a on-board Micro-SD Card.  Data
+ * storage is initiated via a user command prior to launch and terminated after
+ * a configurable delay set to bound the flight-time.  Highly dynamic information
+ * such as acceleration and temperature is stored at 100 samples/second.
+ *
+ * The Micro-SD Card may be made available as a USB Storage Device for data off-
+ * load.  Data is formatted as follows.
+ *
+ *  File Records - one per line - terminated with CTRL-N
+ *   ACC,<BMI160_SECS>,<X>,<Y>,<Z>,*<CHECKSUM>
+ *   TEMP,<SEQ NUM>,<TEMP>,*<CHECKSUM>
+ *   BATT,<SEQ NUM>,<VOLTS>,*<CHECKSUM>
+ *   LIGHT,<SEQ NUM>,<ADC VOLTS>,*<CHECKSUM>
+ *   GPS,<FIX>,<LATITUDE>,<LAT ORIENTATION>,<LONGITUDE>,<LON ORIENTATION>,
+ *     <ALTITUDE>,*<CHECKSUM>
+ *
+ * <CHECKSUM> is a 2-ASCII HEX value XOR of all bytes from the first character
+ * up to but not including the ',*' delimiter.
+ *
+ * GPS location (and/or other sensor information) may also be transmitted
+ * periodically via a LoRa radio to help with rocket retrieval.
+ *
+ * A simple command shell is available via the LoRa radio and diagnostic serial
+ * port supporting the following commands.  Commands are terminated with a
+ * Newline character.  Command output is echoed to the same interface that it
+ * was received from (either LoRa or diagnostic serial).  Only one interface
+ * should ever be used at a time.
+ *
+ *   "status" : Returns device status including recording state, battery voltage,
+ *       temperature and current GPS coordinates.
+ *   "file <filename>" : Set the name, <filename>, for the next recording session.
+ *       The default filename is "flight.txt".
+ *   "dump [<filename>" : Output the contents of the current or specified 
+ *       recording file.
+ *   "dur <seconds>" : Set the duration of the recording session in seconds.
+ *       The default value is 120 seconds.
+ *   "msd [on/off]" : Enable or disable USB Mass Storage.  USB must be detected
+ *       connected.
+ *   "send [accel | batt | gps | light | temp] [on/off]" : Enable or disable 
+ *       periodic sensor logging via the current output interface.
+ *   "record [on/off]" : Enable or disable a recording session.  Default is off.
+ *   "shutdown" : Shut down power (via the PMIC).  The MAX32630FTHR will
+ *       automatically repower immediately if USB Power is connected.
+ *   "help" : Output this list of commands.
+ *
+ * Status response takes the form
+ *   STATUS,<RECORDING>,<BATT VOLTS>,<TEMP>,<FIX>,<LATITUDE>,<LAT ORIENTATION>,
+ *      <LONGITUDE>,<LON ORIENTATION>,<ALTITUDE>,*<CHECKSUM>
+ *
+ * A sign-on message is generated on both daplink and LoRa interfaces containing
+ * the firmware revision.
+ *   HELLO,<VERSION_MAJOR.VERSION_MINOR>,*<CHECKSUM>
+ *
+ * A typical flight scenerio might be:
+ *   Preflight
+ *     1. Power-on system
+ *     2. Prepare rocket for launch
+ *     3. Use Status command to verify rocket has GPS acquisition
+ *     4. Configure flight recording filename
+ *     5. Enable GPS logging
+ *     6. Enable recording
+ *     7. Launch rocket
+ *   During flight
+ *     1. Record rough position using telemetried GPS location
+ *   Postflight
+ *     1. Use telemetried GPS location to plot rocket's landing position
+ *     2. Use either command or direct USB location to obtain flight data from
+ *        recording file.
+ *     3. Power down system
+ *
+ * System Architecture
+ *   1. Individual threads for sensor data aquisition.
+ *   2. Main thread handling ougoing communication.
+ *   3. Event thread handling command processing.
+ *   4. File writing and reading threads for data to/from Micro-SD Card.
+ *   5. String FIFOs used for one-way data communication between sensor 
+ *      acquisition threads and logging thread as well as between command
+ *      response, file dump, GPS acquisition thread and communication thread.
+ *   6. Atomically accessible variables used for current battery voltage,
+ *      current temperature and recording state.
+ *   7. USBMSD object for handling Micro-SD as USB Mass Storage Device.
+ *
+ * Various libraries have been used or ported (thank you to the authors of those
+ * libraries).  All application code is contained in this file even though it
+ * should be distributed in a proper system.  Note that the mbed USBMSD_SD 
+ * library was modified NOT to attempt to "connect" during the object constructor
+ * since this blocks until USB is connected.  Connection is initiated only by
+ * command and only if USB power is detected.
+ *
+ * Communication Interfaces
+ *   1. USB UART Used for diagnostic interface (daplink).
+ *   2. UART2 used for GPS interface.
+ *   3. I2CM2 used for BMI160 and MAX14690.
+ *   4. Software (bit-banged) SPI used for LoRa radio and MAX31865.  This is
+ *      because the built-in hardware SPI seems to require use of hardware
+ *      slave-select signals and only one of those is available if I2CM2 is
+ *      also being used.
+ *   5. AIN0 connected to MAX14690 MON out (battery voltage).
+ *   6. AIN1 connected to ALS-PT19-315C output (light sensor).
+ *   7. Internal 1.2V ADC ref.
+ *   8. GPIO inputs for MAX31865 ready and LoRa interrupt.
+ *   9. GPIO outputs for LoRa reset, SPI slave select, MAX31865 SPI slave select.
+ *  10. On-board RGB LED for system operation state indication.
+ *  11. GPIO output (P4_0) used for profiling during development.
+ *
+ * RGB Status LED (lowest to highest priority)
+ *  YELLOW  : System Idle, No GPS Fix
+ *  BLUE    : System Idle, GPS Fix obtained
+ *  GREEN   : System Idle, USB Detected attached
+ *  BLUE    : USB Mass Storage Enabled (USB must be attached)
+ *  MAGENTA : System Recording
+ *  RED     : System Failure (device initialization or file open failed)
+ *
+ *
+ * Copyright (c) 2017 by Dan Julio.  All rights reserved.
+ *
+ * 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 DAN JULIO 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 <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <math.h>
+#include "mbed.h"
+#include "max32630fthr.h"
+#include "bmi160.h"
+#include "CommandDispatcher.h"
+#include "gps.h"
+#include "Adafruit_MAX31865.h"
+#include "RH_RF95.h"
+#include "SDFileSystem.h"
+#include "StringFifo.h"
+#include "swspi.h"
+#include "USBMSD_SD.h"
+
+
+//
+// Version
+//
+#define VER_MAJOR 0
+#define VER_MINOR 8
+
+
+// =============================================================================
+// Configuration Constants
+// =============================================================================
+
+//
+// Length (in characters) of the FIFO used to store sensor data sentences to be
+// logged in the Micro-SD card.  This should be long enough to prevent any
+// sensor pushing data into the FIFO from hanging until there is room and ideally
+// determined by analyzing the time required to log data, the maximum flight time
+// and computing the maximum data rate.
+//
+#define LOG_FIFO_LENGTH 16384
+
+//
+// Length (in characters) for the FIFO used to store GPS data sentences, command
+// responses and file dump sentences to be transmitted via the LoRa radio (which
+// is slow when working in the most reliable configuration) or the diagnostic
+// serial interface.
+//
+#define RSP_FIFO_LENGTH 16384
+
+//
+// Time (in seconds) for default recording session.
+//
+#define DEF_RECORD_SECONDS 120
+
+//
+// Default recording file name and maximum length
+//
+#define DEF_RECORD_FILE "flight.txt"
+#define MAX_RECORD_FN_LEN 32
+
+//
+// Command and sentence limits (number of chars/line)
+//
+#define MAX_CMD_LEN      64
+#define MAX_CMD_RSP_LEN  128
+#define MAX_SENTENCE_LEN 80
+
+//
+// Low-battery auto-shutdown voltage.
+//
+#define SHUTDOWN_VOLTS 3.0f
+
+//
+// ADC Vref volts (this is fixed for MAX32630)
+//
+#define VREF 1.2f
+
+//
+// Status LED set/clear masks (fixed)
+//
+#define ST_STARTUP  0x01
+#define ST_GPS_FIX  0x02
+#define ST_USB_PWR  0x04
+#define ST_RECORD   0x10
+#define ST_MSD      0x20
+#define ST_ERROR    0x80
+
+
+
+// =============================================================================
+// Objects
+// =============================================================================
+
+//
+// Hardware-related objects
+//
+MAX32630FTHR pegasus(MAX32630FTHR::VIO_3V3);
+
+Serial daplink(P2_1, P2_0);
+
+Serial gps_serial(P3_1, P3_0);
+Adafruit_GPS gps(&gps_serial);
+
+I2C i2cBus(P5_7, P6_0);
+BMI160_I2C imu(i2cBus, BMI160_I2C::I2C_ADRS_SDO_LO);
+
+swspi sw_spi(P5_1, P5_2, P5_0, P3_4, P5_3);   // mosi, miso, sclk, ss1, ss2
+Adafruit_MAX31865 tempSense(sw_spi, 1, P3_5); // spi, ss<N>, rdyPin
+RH_RF95 radio(sw_spi, 2);                     // spi, ss<N>
+InterruptIn radioInt(P5_4);
+DigitalOut radioRstN(P5_5);
+
+AnalogIn battSensor(AIN_0);
+AnalogIn lightSensor(AIN_1);
+
+SDFileSystem sd(P0_5, P0_6, P0_4, P0_7, "sd");  // mosi, miso, sclk, cs
+USBMSD_SD ms_sd(P0_5, P0_6, P0_4, P0_7);        // mosi, miso, sclk, cs
+
+DigitalOut rLED(LED1);
+DigitalOut gLED(LED2);
+DigitalOut bLED(LED3);
+
+DigitalOut profOut(P4_0);
+
+
+//
+// Data objects
+//
+StringFifo sensorFifo(LOG_FIFO_LENGTH);
+StringFifo rspFifo(RSP_FIFO_LENGTH);
+
+CommandDispatcher cmdDispatcher;
+
+EventQueue mQueue(16*EVENTS_EVENT_SIZE);
+
+
+//
+// Threads
+//
+Thread mQueueThread;
+Thread daplinkRxThread;
+Thread accelThread;
+Thread battThread;
+Thread gpsRxThread;
+Thread gpsLogThread;
+Thread lightThread;
+Thread tempThread;
+Thread fileWriteThread;
+Thread fileReadThread;
+
+
+//
+// Sensor trigger timers
+//
+Ticker accelTicker;
+Ticker battTicker;
+Ticker gpsTicker;
+Ticker lightTicker;
+Ticker tempTicker;
+
+
+
+// =============================================================================
+// Global Variables
+// =============================================================================
+
+//
+// RGB Status LED colors
+//
+enum LedColors
+{
+    BLACK,
+    RED,
+    YELLOW,
+    GREEN,
+    CYAN,
+    BLUE,
+    MAGENTA,
+    WHITE
+};
+
+
+//
+// Indicies into sensor-related control arrays
+//
+enum SensorSelect
+{
+    ACCEL = 0,
+    BATT,
+    GPS,
+    LIGHT,
+    TEMP
+};
+    
+
+//
+// System state
+//
+bool enSnsLogging[5];          // Set to enable transmitting various sensor values
+bool enSnsTick[5];             // Set when time for a sensor to log or record info
+
+bool trigRecording = false;    // {command} --> {file writer}
+bool trigEndRecording = false; // {command, timer} --> {file writer}
+bool enRecording = false;      // Set by command to note recording, cleared by writer
+char curRecFileName[MAX_RECORD_FN_LEN + 1];
+float recTimeoutSecs = DEF_RECORD_SECONDS;
+FILE* fp;                      // Use for recording or dumping file
+
+bool trigFileDump = false;     // {command} --> {file reader}
+bool enFileDump = false;       // Set by command to note dumping, cleared by reader
+char curReadFileName[MAX_RECORD_FN_LEN + 1];
+
+bool enMassStorage = false;    // Set by command when mass storage is enabled
+
+//
+// Command and output processing
+//
+volatile bool cmdFromRadio = true;
+char cmdString[MAX_CMD_LEN+1];
+
+//
+// System status
+//
+float curBattV = 0.0f;
+float curTempC = 0.0f;
+Mutex gpsMutex;
+float gpsLat = 0.0f;
+char gpsLatOrient = ' ';
+float gpsLon = 0.0f;
+char gpsLonOrient = ' ';
+float gpsAlt = 0.0f;
+bool gpsUpdated = false;          // {GpsRx} -> {GpsLog}
+bool gpsFix = false;
+
+bool usbPower = false;            // Set while the battery thread detects ext pwr
+
+uint8_t ledStatus = 0x00;
+
+
+// =============================================================================
+// Necessary forward declarations
+// =============================================================================
+
+void CommandEvent();
+void RecordEndEvent();
+void ShutdownEvent();
+bool InitRadio();
+void SetStatusLed(uint8_t f);
+void ClearStatusLed(uint8_t f);
+void UpdateStatusLed();
+float GpsNMEAtoFrac(float c);
+void AddChecksum(char* s);
+
+
+
+// =============================================================================
+// Thread code
+// =============================================================================
+
+void daplinkRxProcess()
+{
+    int rxIndex = 0;
+    char c;
+    
+    while (1) {
+        // Wait for and then process incoming data
+        while (!daplink.readable()) {
+            Thread::yield();
+        }
+        
+        // Process a character
+        c = daplink.getc();
+        if (c == '\n') {
+            cmdString[rxIndex] = 0;
+            rxIndex = 0;
+            cmdFromRadio = false;
+            mQueue.call(CommandEvent);
+        } else if ((c != '\r') && (rxIndex < MAX_CMD_LEN)) {
+            cmdString[rxIndex++] = c;
+        }
+    }
+}
+
+
+void AccelProcess()
+{
+    BMI160::AccConfig accConfig;
+    BMI160::SensorData accData;
+    BMI160::SensorTime sensorTime;
+    char accelString[MAX_SENTENCE_LEN];
+    uint32_t seqNum = 0;
+    bool initSuccess = true;   // Will be set false if necessary
+    
+    // Setup accelerometer configuration
+    accConfig.range = BMI160::SENS_16G;
+    accConfig.us = BMI160::ACC_US_OFF;
+    accConfig.bwp = BMI160::ACC_BWP_2;
+    accConfig.odr = BMI160::ACC_ODR_8;
+    
+    // Initialize
+    if (imu.setSensorPowerMode(BMI160::GYRO, BMI160::NORMAL) != BMI160::RTN_NO_ERROR) {
+        daplink.printf("Failed to set gyro power mode\n\r");
+        initSuccess = false;
+    }
+    wait_ms(100);
+    if (imu.setSensorPowerMode(BMI160::ACC, BMI160::NORMAL) != BMI160::RTN_NO_ERROR) {
+        daplink.printf("Failed to set accelerometer power mode\n\r");
+        initSuccess = false;
+    }
+    wait_ms(100);
+    if (initSuccess){
+        if (imu.setSensorConfig(accConfig) != BMI160::RTN_NO_ERROR) {
+            daplink.printf("Failed to reconfigure accelerometer\n\r");
+            initSuccess = false;
+        }
+    }
+    
+    if (!initSuccess) {
+        SetStatusLed(ST_ERROR);
+    } else {
+        // Main loop
+        while (1) {
+            // Wait to be triggered
+            while (!enSnsTick[ACCEL]) {
+                Thread::yield();
+            }
+            enSnsTick[ACCEL] = false;
+            
+            // Read accelerometer
+            (void) imu.getSensorXYZandSensorTime(accData, sensorTime, accConfig.range);
+            
+            // Send for logging
+            sprintf(accelString, "ACC,%f,%f,%f,%f", sensorTime.seconds,
+                    accData.xAxis.scaled, accData.yAxis.scaled, accData.zAxis.scaled);
+            AddChecksum(accelString);
+            
+            if (enRecording) {
+                sensorFifo.PushString(accelString);
+            }
+        
+            if ((enSnsLogging[ACCEL] && (seqNum++ % 100) == 0)) {
+                // Only send to our user occasionally
+                rspFifo.PushString(accelString);
+            }
+        }
+    }
+}
+
+
+void BattProcess()
+{
+    char battString[MAX_SENTENCE_LEN];
+    uint32_t seqNum = 0;
+    char regVal;
+    
+    // Initialize - setup the monitor input for 1:4 scaling to fit our
+    // ADC's fullscale range
+    pegasus.max14690.monSet(pegasus.max14690.MON_BAT, pegasus.max14690.MON_DIV4);
+    
+    while (1) {
+        // Wait to be triggered
+        while (!enSnsTick[BATT]) {
+            Thread::yield();
+        }
+        enSnsTick[BATT] = false;
+        
+        // Read battery voltage and check for shutdown condition
+        curBattV = battSensor.read()*4.0f*VREF;
+        
+        if (curBattV < SHUTDOWN_VOLTS) {
+            // Attempt to tell the world we're shutting down
+            rspFifo.PushString("Low Battery Shutdown");
+            
+            // Schedule shutdown
+            mQueue.call(ShutdownEvent);
+        }
+        
+        // Send for logging
+        sprintf(battString, "BATT,%d,%1.2f", seqNum++, curBattV);
+        AddChecksum(battString);
+        
+        
+        if (enRecording) {
+            sensorFifo.PushString(battString);
+        }
+        
+        if (enSnsLogging[BATT]) {
+            rspFifo.PushString(battString);
+        }
+        
+        // Read the MAX14690 StatusB register bit[3] UsbOk to determine
+        // if we are connected to a USB system or not
+        if (pegasus.max14690.readReg(MAX14690::REG_STATUS_B, &regVal) == MAX14690_NO_ERROR) {
+            if (regVal & 0x08) {
+                if (!usbPower) {
+                    usbPower = true;
+                    SetStatusLed(ST_USB_PWR);
+                }
+            } else {
+                if (usbPower) {
+                    usbPower = false;
+                    ClearStatusLed(ST_USB_PWR);
+                }
+            }
+        }
+    }
+}
+
+
+void GpsRxProcess()
+{
+    // Initialize
+    gps.begin(9600);
+    
+    while (1) {
+        // Read complete lines from the GPS receiver (blocks until data avail)
+        gps.readNMEA();
+        
+        // Process any received NMEA sentences
+        if (gps.newNMEAreceived()) {
+            if (gps.parse(gps.lastNMEA())) {
+                if (gps.fix) {
+                    // Update our status
+                    gpsMutex.lock();
+                    gpsLat = GpsNMEAtoFrac(gps.latitude);
+                    gpsLatOrient = gps.lat;
+                    gpsLon = GpsNMEAtoFrac(gps.longitude);
+                    gpsLonOrient = gps.lon;
+                    gpsAlt = gps.altitude;
+                    gpsUpdated = true;
+                    gpsMutex.unlock();
+                }
+            }
+        }
+    }
+}
+
+
+void GpsLogProcess()
+{
+    char gpsString[MAX_SENTENCE_LEN];
+    int gpsMissedUpdateCount = 0;
+    
+    while (1) {
+        // Wait to be triggered
+        while (!enSnsTick[GPS]) {
+            Thread::yield();
+        }
+        enSnsTick[GPS] = false;
+        
+        // Evaluate fix detecton logic
+        if (gpsUpdated) {
+            gpsUpdated = false;
+            gpsMissedUpdateCount = 0;
+            if (!gpsFix) {
+                gpsFix = true;
+                SetStatusLed(ST_GPS_FIX);
+            }
+        } else {
+            if (++gpsMissedUpdateCount > 5) {
+                if (gpsFix) {
+                    gpsFix = false;
+                    ClearStatusLed(ST_GPS_FIX);
+                }
+            }
+        }
+        
+        // Send for logging
+        gpsMutex.lock();
+        sprintf(gpsString, "GPS,%d,%1.6f,%c,%1.6f,%c,%1.1f",
+                gpsFix, gpsLat, gpsLatOrient, gpsLon, gpsLonOrient, gps.altitude);
+        gpsMutex.unlock();
+        AddChecksum(gpsString);
+        
+        if (enRecording) {
+            sensorFifo.PushString(gpsString);
+        }
+                    
+        if (enSnsLogging[GPS]) {
+            rspFifo.PushString(gpsString);
+        }
+    }
+}
+
+
+void LightProcess()
+{
+    char lightString[MAX_SENTENCE_LEN];
+    uint32_t seqNum = 0;
+    float lightV;
+    
+    while (1) {
+        // Wait to be triggered
+        while (!enSnsTick[LIGHT]) {
+            Thread::yield();
+        }
+        enSnsTick[LIGHT] = false;
+        
+        // Read light sensor voltage
+        lightV = lightSensor.read()*VREF;
+        
+        // Send for logging
+        sprintf(lightString, "LIGHT,%d,%1.2f", seqNum++, lightV);
+        AddChecksum(lightString);
+        
+        if (enRecording) {
+            sensorFifo.PushString(lightString);
+        }
+        
+        if ((enSnsLogging[LIGHT] && (seqNum % 100) == 0)) {
+            // Only send to our user occasionally
+            rspFifo.PushString(lightString);
+        }
+    }
+}
+
+
+void TempProcess()
+{
+    char tempString[MAX_SENTENCE_LEN];
+    uint32_t seqNum = 0;
+    uint16_t rtd;
+    
+    // Initialize temperature sensor for continuous sensing
+    tempSense.begin();
+    tempSense.enableBias(true);
+    tempSense.autoConvert(true);
+    
+    while (1) {
+        // Wait to be triggered
+        while (!enSnsTick[TEMP]) {
+            Thread::yield();
+        }
+        enSnsTick[TEMP] = false;
+        
+        // Get the temperature if available
+        if (tempSense.isReady(&rtd)) {
+            // Using 100 ohm RTD and 430 ohm reference resistor
+            curTempC = tempSense.temperature(100.0f, 430.0f, rtd);
+            
+            // Send for logging
+            sprintf(tempString, "TEMP,%d,%1.2f", seqNum++, curTempC);
+            AddChecksum(tempString);
+        
+            if (enRecording) {
+                sensorFifo.PushString(tempString);
+            }
+        
+            if ((enSnsLogging[TEMP] && (seqNum % 10) == 0)) {
+                // Only send to our user occasionally
+                rspFifo.PushString(tempString);
+            }
+        }
+    }
+}
+
+
+void FileWriteProcess()
+{
+    char fullRecFileName[MAX_RECORD_FN_LEN + 5];
+    char logString[MAX_SENTENCE_LEN];
+    
+    while (1) {
+        // Each time through before possibly blocking on the fifo, check 
+        // if we need to close the file after recording finishes
+        if (trigEndRecording && enRecording) {
+            enRecording = false;
+            trigEndRecording = false;
+            fclose(fp);
+            ClearStatusLed(ST_RECORD);
+        }
+        
+        // Look for sensor data
+        sensorFifo.PopString(logString);
+        
+        // See if we need to start recording
+        if (trigRecording) {
+            trigRecording = false;
+            // Attempt to open the file for writing
+            strcpy(fullRecFileName, "/sd/");
+            strcpy(&fullRecFileName[4], curRecFileName);
+            fp = fopen(fullRecFileName, "w");
+            if (fp == 0) {
+                enRecording = false;
+                sprintf(logString, "Cannot open %s, errno = %d", fullRecFileName, errno);
+                rspFifo.PushString(logString);
+                SetStatusLed(ST_ERROR);
+            } else {
+                mQueue.call_in(recTimeoutSecs*1000, RecordEndEvent);
+                ClearStatusLed(ST_ERROR);  // Just in case they screwed up the filename earlier
+                SetStatusLed(ST_RECORD);
+            }
+        }
+        
+        // Write to file if we are logging (Unix style)
+        if (enRecording) {
+            profOut = 1;
+            fprintf(fp, "%s\n", logString);
+            profOut = 0;
+            Thread::yield();  // Allow other threads to run too
+        }
+    }
+}
+
+
+void FileReadProcess()
+{
+    char fullReadFileName[MAX_RECORD_FN_LEN + 5];
+    char curLine[MAX_CMD_RSP_LEN + 1];
+    char c;
+    int i;
+    
+    while (1) {
+        // Wait for a trigger to start dumping a file
+        while (!trigFileDump) { 
+            Thread::yield();
+        }
+        trigFileDump = false;
+        
+        // Attempt to open the file for reading
+        strcpy(fullReadFileName, "/sd/");
+        strcpy(&fullReadFileName[4], curReadFileName);
+        fp = fopen(fullReadFileName, "r");
+        if (fp == 0) {
+            sprintf(curLine, "Cannot open %s: errno %d", fullReadFileName, errno);
+            rspFifo.PushString(curLine);
+        } else {
+            // Dump file one line at a time, assuming Unix <LF> line endings
+            // but skipping <CR> if they occur.  They can stop us by clearing
+            // enFileDump (although we may have buffered a lot of data in
+            // rspFifo).
+            i = 0;
+            while (!feof(fp) && enFileDump) {
+                c = getc(fp);
+                if (c == '\n') {
+                    // End of line
+                    curLine[i] = 0;
+                    i = 0;
+                    rspFifo.PushString(curLine);
+                    Thread::yield();  // Allow other activity while dumping
+                } else if (c != '\r') {
+                    if (i < MAX_CMD_RSP_LEN) {
+                        curLine[i++] = c;
+                    }
+                }
+            }
+            if (i != 0) {
+                // Push any final, unterminated lines
+                rspFifo.PushString(curLine);
+            }
+            rspFifo.PushString("END");
+            fclose(fp);
+        }
+        enFileDump = false;
+    }
+}
+
+
+
+// =============================================================================
+// Command handlers
+// =============================================================================
+
+void statusCommand(unsigned int argc, char* argv[], char* result)
+{
+    gpsMutex.lock();
+    sprintf(result, "STATUS,%d,%1.2f,%1.2f,%d,%1.6f,%c,%1.6f,%c,%1.1f",
+        enRecording, curBattV, curTempC, gpsFix, gpsLat, gpsLatOrient, gpsLon,
+        gpsLonOrient, gpsAlt);
+    gpsMutex.unlock();
+    AddChecksum(result);
+}
+
+
+void fileCommand(unsigned int argc, char* argv[], char* result)
+{
+    if (argc >= 2) {
+        strcpy(curRecFileName, argv[1]);
+        sprintf(result, "Filename = %s", curRecFileName);
+    } else {
+        sprintf(result, "Must also specify filename");
+    }    
+}
+
+
+void dumpCommand(unsigned int argc, char* argv[], char* result)
+{
+    if (enRecording == true) {
+        sprintf(result, "Cannot dump while recording!");
+    } else {
+        if (!enFileDump) {
+            if (argc >= 2) {
+                // They provided a specific file to read
+                strcpy(curReadFileName, argv[1]);
+            } else {
+                // Use the existing record file name
+                strcpy(curReadFileName, curRecFileName);
+            }
+            enFileDump = true;  // Allow sensors to start filling sensorFifo
+            trigFileDump = true;
+        }
+        sprintf(result, "");
+    }
+}
+
+
+void durCommand(unsigned int argc, char* argv[], char* result)
+{
+    float f;
+    
+    if (argc >= 2) {
+        if ((f = atof(argv[1])) != 0.0f) {
+            recTimeoutSecs = f;
+        }
+        sprintf(result, "Recording timeout = %f seconds", recTimeoutSecs);
+    } else {
+        sprintf(result, "Must also specify timeout");
+    }
+}
+
+
+void massStorageCommand(unsigned int argc, char* argv[], char* result)
+{
+    if (argc >= 2) {
+        if (strcmp(argv[1], "on") == 0) {
+            if (!enMassStorage) {
+                if (!usbPower) {
+                    sprintf(result, "Cannot enable USB Mass storage with no USB");
+                } else {
+                    if (ms_sd.connect(true)) {
+                        enMassStorage = true;
+                        sprintf(result, "Mass Storage Enabled");
+                        SetStatusLed(ST_MSD);
+                    } else {
+                        sprintf(result, "Mass Storage Not Enabled!");
+                    }
+                }
+            } else {
+                sprintf(result, "Mass Storage already Enabled!");
+            }
+        } else {
+            if (enMassStorage) {
+                ms_sd.disconnect();
+                enMassStorage = false;
+                ClearStatusLed(ST_MSD);
+                sprintf(result, "Mass Storage Disabled");
+            } else {
+                sprintf(result, "Mass Storage already Disabled");
+            }
+        }
+    } else {
+        sprintf(result, "Must also specify on/off");
+    }
+}
+
+
+void sendCommand(unsigned int argc, char* argv[], char* result)
+{
+    bool en;
+    bool valid = true;
+    
+    if (argc >= 3) {
+        if (strcmp(argv[2], "on") == 0) {
+            en = true;
+        } else {
+            en = false;
+        }
+
+        if (strcmp(argv[1], "accel") == 0) {
+            enSnsLogging[ACCEL] = en;
+        } else if (strcmp(argv[1], "batt") == 0) {
+            enSnsLogging[BATT] = en;
+        } else if (strcmp(argv[1], "gps") == 0) {
+            enSnsLogging[GPS] = en;
+        } else if (strcmp(argv[1], "light") == 0) {
+            enSnsLogging[LIGHT] = en;
+        } else if (strcmp(argv[1], "temp") == 0) {
+            enSnsLogging[TEMP] = en;
+        } else {
+            valid = false;
+            sprintf(result, "Unknown sensor: %s", argv[1]);
+        }
+        
+        if (valid) {
+            sprintf(result, "%s Sending = %d", argv[1], en);
+        }
+    } else {
+        sprintf(result, "Must specify sensor and on/off");
+    }
+}
+
+
+void recordCommand(unsigned int argc, char* argv[], char* result)
+{
+    if (enFileDump) {
+        sprintf(result, ""); // Silently ignore command while dumping
+    } else if (argc >= 2) {
+        if (strcmp(argv[1], "on") == 0) {
+            if (!enRecording) {
+                enRecording = true;   // Allow sensors to start feeding writer
+                trigRecording = true;
+                sprintf(result, "Setup to record to %s", curRecFileName);
+            } else {
+                sprintf(result, "Already recording");
+            }
+        } else {
+            if (enRecording) {
+                trigEndRecording = true;
+                sprintf(result, "Stopping recording");
+            } else {
+                sprintf(result, "Recording already stopped");
+            }
+        }
+    } else {
+        sprintf(result, "Must also specify on/off");
+    }
+}
+
+
+void shutdownCommand(unsigned int argc, char* argv[], char* result)
+{
+    // Schedule shutdown
+    mQueue.call(ShutdownEvent);
+    
+    sprintf(result, "Shutdown");
+}
+
+
+void helpCommand(unsigned int argc, char* argv[], char* result)
+{
+    // Empty command response
+    sprintf(result, "");
+    
+    rspFifo.PushString("Commands: ");
+    rspFifo.PushString(" status");
+    rspFifo.PushString(" file <filename>");
+    rspFifo.PushString(" dump [<filename>");
+    rspFifo.PushString(" dur <seconds>");
+    rspFifo.PushString(" msd [on/off]");
+    rspFifo.PushString(" send [accel | batt | gps | light | temp] [on/off]");
+    rspFifo.PushString(" record [on/off]");
+    rspFifo.PushString(" shutdown");
+    rspFifo.PushString(" help");
+}
+
+
+
+// =============================================================================
+// Event Handlers
+// =============================================================================
+
+/*
+// NOTE: This function not used because attempting to catch daplink RX interrupts
+// hangs the system --> So I'm falling back to polling in another thread...
+void DaplinkRxIsr()
+{
+    static int rxIndex = 0;
+    bool sawTerm = false;
+    char c;
+    
+    // Process all incoming received data and see if we can process a command
+    while (daplink.readable() && !sawTerm) {
+        c = daplink.getc();
+        if (c == '\n') {
+            cmdString[rxIndex] = 0;
+            sawTerm = true;
+            rxIndex = 0;
+        } else if (rxIndex < MAX_CMD_LEN) {
+            cmdString[rxIndex++] = c;        
+        }
+    }
+    
+    if (sawTerm) {
+        cmdFromRadio = false;
+        mQueue.call(CommandEvent);
+    }
+    return;
+}
+*/
+
+
+void RadioIsrEvent()
+{
+    uint8_t rxLen;
+
+    // Process the Radio's Interrupt Handler - we do it in an event instead
+    // of an ISR because it may access the swspi and the current mbed
+    // environment won't correctly execute the mutex's protecting the swspi
+    // access routines (which are shared with code in other threads) in an ISR.
+    radio.handleInterrupt();
+    
+    // See if we received a packet and process it if necessary
+    if (radio.available()) {
+        rxLen = MAX_CMD_LEN;
+        if (radio.recv((uint8_t *) cmdString, &rxLen)) {
+            cmdString[rxLen+1] = 0;
+            cmdFromRadio = true;
+            mQueue.call(CommandEvent);
+        }
+    }
+}
+
+
+void CommandEvent()
+{
+    char rspString[MAX_CMD_RSP_LEN+1];
+    
+    // Process the received command
+    if (cmdDispatcher.executeCommand(cmdString, rspString)) {
+        // Push response
+        rspFifo.PushString(rspString);
+    }
+}
+
+
+void RecordEndEvent()
+{
+    if (enRecording) {
+        trigEndRecording = true;
+        rspFifo.PushString("Finishing Recording");
+    }
+}
+
+
+void ShutdownEvent()
+{
+    // Stop recording if necessary
+    if (enRecording) {
+        trigEndRecording = true;
+    }
+    
+    // Wait a moment to let any other threads clean up
+    wait_ms(100);
+    
+    // Power down
+    pegasus.max14690.shutdown();
+}
+
+
+void AccelTick()
+{
+    enSnsTick[ACCEL] = true;
+}
+
+void BattTick()
+{
+    enSnsTick[BATT] = true;
+}
+
+void GpsTick()
+{
+    enSnsTick[GPS] = true;
+}
+
+void LightTick()
+{
+    enSnsTick[LIGHT] = true;
+}
+
+void TempTick()
+{
+    enSnsTick[TEMP] = true;
+}
+
+
+
+// =============================================================================
+// Support subroutines
+// =============================================================================
+
+void InitSystem()
+{
+    // Indicate startup
+    SetStatusLed(ST_STARTUP);
+    
+    // Misc global object initialization
+    daplink.baud(115200);
+    sw_spi.init();
+    i2cBus.frequency(400000);
+    
+    // Setup the Radio
+    if (!InitRadio()) {
+        daplink.printf("Radio Init failed\n\r");
+        SetStatusLed(ST_ERROR);
+    }
+    
+    // Misc global variable initialization
+    strcpy(curRecFileName, DEF_RECORD_FILE);
+    for (int i=0; i<5; i++) {
+        enSnsLogging[i] = false;
+        enSnsTick[i] = false;
+    }
+    
+    // Commands
+    cmdDispatcher.addCommand("status", statusCommand);
+    cmdDispatcher.addCommand("file", fileCommand);
+    cmdDispatcher.addCommand("dump", dumpCommand);
+    cmdDispatcher.addCommand("dur", durCommand);
+    cmdDispatcher.addCommand("msd", massStorageCommand);
+    cmdDispatcher.addCommand("send", sendCommand);
+    cmdDispatcher.addCommand("record", recordCommand);
+    cmdDispatcher.addCommand("shutdown", shutdownCommand);
+    cmdDispatcher.addCommand("help", helpCommand);
+    
+    // Setup events
+    //daplink.attach(&DaplinkRxIsr, Serial::RxIrq);  // NOTE: This hangs the system!
+    radioInt.rise(mQueue.event(RadioIsrEvent));
+    
+    // Start threads
+    daplinkRxThread.start(callback(daplinkRxProcess));
+    accelThread.start(callback(AccelProcess));
+    battThread.start(callback(BattProcess));
+    gpsRxThread.start(callback(GpsRxProcess));
+    gpsLogThread.start(callback(GpsLogProcess));
+    lightThread.start(callback(LightProcess));
+    tempThread.start(callback(TempProcess));
+    fileWriteThread.start(callback(FileWriteProcess));
+    fileReadThread.start(callback(FileReadProcess));
+    mQueueThread.start(callback(&mQueue, &EventQueue::dispatch_forever));
+
+    // Start sensor trigger timers (this sets the frequency for each sensor's data
+    // collection)
+    accelTicker.attach(&AccelTick, 0.01f);
+    battTicker.attach(&BattTick, 5.0f);
+    gpsTicker.attach(&GpsTick, 1.0f);
+    lightTicker.attach(&LightTick, 0.01f);
+    tempTicker.attach(&TempTick, 0.1f);
+}
+
+
+bool InitRadio()
+{
+    // Reset radio
+    radioRstN = 0;
+    wait_ms(10);
+    radioRstN = 1;
+    
+    // Attempt to initialize it
+    if (!radio.init()) {
+        return(false);
+    }
+    
+    // Configure
+    radio.setFrequency(915.0);
+    radio.setTxPower(20);
+    
+    return(true);
+}
+
+
+void SetStatusLed(uint8_t f)
+{
+    ledStatus |= f;
+    
+    UpdateStatusLed();
+}
+
+
+void ClearStatusLed(uint8_t f)
+{
+    ledStatus &= ~f;
+    
+    UpdateStatusLed();
+}
+
+
+void UpdateStatusLed()
+{
+    LedColors c;
+    
+    // Priority Encoder
+    if (ledStatus & ST_ERROR)        c = RED;
+    else if (ledStatus & ST_MSD)     c = BLUE;
+    else if (ledStatus & ST_RECORD)  c = MAGENTA;
+    else if (ledStatus & ST_USB_PWR) c = GREEN;
+    else if (ledStatus & ST_GPS_FIX) c = CYAN;
+    else if (ledStatus & ST_STARTUP) c = YELLOW;
+    else                             c = BLACK;
+    
+    // Hardware control
+    switch (c) {
+        case BLACK:
+            rLED = 1; gLED = 1; bLED = 1;
+            break;
+        case RED:
+            rLED = 0; gLED = 1; bLED = 1;
+            break;
+        case YELLOW:
+            rLED = 0; gLED = 0; bLED = 1;
+            break;
+        case GREEN:
+            rLED = 1; gLED = 0; bLED = 1;
+            break;
+        case CYAN:
+            rLED = 1; gLED = 0; bLED = 0;
+            break;
+        case BLUE:
+            rLED = 1; gLED = 1; bLED = 0;
+            break;
+        case MAGENTA:
+            rLED = 0; gLED = 1; bLED = 0;
+            break;
+        case WHITE:
+            rLED = 0; gLED = 0; bLED = 0;
+            break;
+    }
+}
+
+
+void Signon()
+{
+    char msg[20];
+    
+    sprintf(msg, "HELLO,%d.%d", VER_MAJOR, VER_MINOR);
+    AddChecksum(msg);
+    daplink.printf("%s\n\r", msg);
+    radio.send((uint8_t *) msg, sizeof(msg));
+}
+
+
+float GpsNMEAtoFrac(float c)
+{
+    float intPart;
+    float fracPart;
+    
+    // Get the integer part of the coordinate
+    intPart = floor(c / 100.0f);
+    
+    // Convert the fractional part into 60ths
+    fracPart = c - (intPart * 100.0f);   // Get rid of the integer part
+    fracPart = fracPart / 60.0f;
+    
+    return(intPart + fracPart);
+}
+
+
+void AddChecksum(char* s)
+{
+    uint8_t c = 0;
+    int i = 0;
+    
+    while (*(s+i) != 0) {
+        c ^= *(s+i++);
+    }
+    
+    sprintf(s+i, ",*%X", c);
+}
+
+
+
+// =============================================================================
+// Main
+// =============================================================================
+int main()
+{    
+    char outString[MAX_CMD_RSP_LEN+1];
+    
+    InitSystem();
+    Signon();
+    
+    while (1) {
+        // Look for data to pass back to our user
+        rspFifo.PopString(outString);
+
+        if (cmdFromRadio) {
+            radio.send((uint8_t *) outString, strlen(outString));
+        } else {
+            while (!daplink.writeable()) {
+                Thread::yield();
+            }
+            daplink.printf("%s\n", outString);
+        } 
+    }    
+}
diff -r 000000000000 -r 83f4079b2bac max31865.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/max31865.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://developer.mbed.org/users/danjulio/code/max31865/#9294dd756a0c
diff -r 000000000000 -r 83f4079b2bac max32630fthr.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/max32630fthr.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+http://developer.mbed.org/teams/MaximIntegrated/code/max32630fthr/#60997adf01a2
diff -r 000000000000 -r 83f4079b2bac mbed-os.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mbed-os.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://github.com/ARMmbed/mbed-os/#269f58d75b752a4e67a6a2d8c5c698635ffd6752
diff -r 000000000000 -r 83f4079b2bac swspi.lib
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/swspi.lib	Sun Jun 11 04:26:22 2017 +0000
@@ -0,0 +1,1 @@
+https://developer.mbed.org/users/danjulio/code/swspi/#8a4db0f083fc