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

main.cpp

Committer:
danjulio
Date:
2017-06-11
Revision:
0:83f4079b2bac

File content as of revision 0:83f4079b2bac:

/*
 * 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);
        } 
    }    
}