/** \file
 *
 *  Contains the CTD_Seabird class implementation.
 *
 *  Copyright (c) 2007,2008,2009 MBARI
 *  MBARI Proprietary Information.  All Rights Reserved
 */

#include "CTD_Seabird.h"
#include "CTD_SeabirdIF.h"

#include "data/ConfigReader.h"
#include "data/SimSlate.h"
#include "data/UniversalDataReader.h"
#include "data/UniversalDataWriter.h"
#include "units/Units.h"
#include "bitModule/CBITIF.h"

#include <stdlib.h>
#include <cmath>        // std::pow

#define CTD_ACCURACY_COND (0.0003)   // (S/m) Per spec
#define CTD_ACCURACY_TEMP (0.002)    // (deg C) per spec
#define CTD_ACCURACY_OXYGEN (290)    // (ug/l) per spec accuracy of +/-2% of saturation (value is for T=0 C, sal=0 psu)
#define CTD_ACCURACY_DENSITY (0.01)  // per calc
#define CTD_ACCURACY_SALINITY (0.01) // per calc
#define CTD_ACCURACY_DEPTH (0.04)
#define CTD_ACCURACY_SPEED (0.15)    // per calc
#define CTD_ACCURACY_PRESSURE CTD_ACCURACY_DEPTH
#define CTD_DATA_ITEMS (4)
#define CTD_BIN_SIZE (5) // XXX hard-coded bin size to start

// The data looks like this if OutputFormat=1 (engineering units, decimal characters):
// 1174.49, 9.4867, 3.99516, 2610.58
// where:
//      1174.49 = pressure (db)
//      9.4867 = temperature (degrees C)
//      3.99516 = conductivity (S/m)
//      2610.58 = dissolved oxygen frequency (Hz)
//
// These numbers should be reasonable for the present environment of the
// instrument (for example, in air, in fresh water, or in seawater).

// Define the number of messages allowed to queue up in the buffer
const unsigned short CTD_Seabird::MAX_DEVICE_MSG_QUEUE_SIZE( 5 );

// 1Hz data streaming from sensor
const Timespan CTD_Seabird::PERIOD( 1.0 );

CTD_Seabird::CTD_Seabird( const Module* module )
    : AsyncComponent( CTD_SeabirdIF::NAME, module, PERIOD ),
      debug_( false ),
      verbosity_( 0 ),
      loadControl_( CTD_SeabirdIF::LOAD_CONTROL, !simulateHardware(), logger_, this ),
      startTime_( Timestamp::NOT_SET_TIME ),
      ctdTimeout_( 7.0 ),
      poTimeout_( 7.0 ), // determined empiraclly in the test tank 12/2018
      uart_( CTD_SeabirdIF::UART, CTD_SeabirdIF::BAUD, 2.0, logger_, 4095, true ),
      sbe43FSerialNumber_( 0 ),
      pressureOffset_( 0 ),
      maxPressBound_( 499 ),
      minPressBound_( -9 ),
      maxSalinityBound_( 38 ),
      minSalinityBound_( 28 ),
      latitude_( nanf( "" ) ),
      pressureDB_( 0 ),
      temperature_( 0 ),
      conductivity_( 0 ),
      oxygenFreq_( 0 ),
      depth_( 0 ),
      salinity_( 0 ),
      density_( 0 ),
      soundSpeed_( 0 ),
      oxygenConcentration_( 0 ),
      badPressure_( true ),
      badTemperature_( true ),
      badConductivity_( true ),
      badSalinity_( true ),
      temperatureBin_( CTD_BIN_SIZE ),
      salinityBin_( CTD_BIN_SIZE )
{

    // Configuration inputs for SBE 43F oxygen calibration
    sbe43FSerialNumberCfgReader_    = newConfigReader( CTD_SeabirdIF::SBE_43F_SERIAL_NUMBER );
    oxygenCalCoeffFOffsetCfgReader_ = newConfigReader( CTD_SeabirdIF::OXYGEN_CAL_COEFF_FOFFSET );
    oxygenCalCoeffSocCfgReader_     = newConfigReader( CTD_SeabirdIF::OXYGEN_CAL_COEFF_SOC );
    oxygenCalCoeffACfgReader_       = newConfigReader( CTD_SeabirdIF::OXYGEN_CAL_COEFF_A );
    oxygenCalCoeffBCfgReader_       = newConfigReader( CTD_SeabirdIF::OXYGEN_CAL_COEFF_B );
    oxygenCalCoeffCCfgReader_       = newConfigReader( CTD_SeabirdIF::OXYGEN_CAL_COEFF_C );
    oxygenCalCoeffECfgReader_       = newConfigReader( CTD_SeabirdIF::OXYGEN_CAL_COEFF_E );

    // Additional configuration inputs
    maxPressBoundCfgReader_    = newConfigReader( CTD_SeabirdIF::MAX_PRESS_BOUND );
    minPressBoundCfgReader_    = newConfigReader( CTD_SeabirdIF::MIN_PRESS_BOUND );
    maxSalinityBoundCfgReader_ = newConfigReader( CTD_SeabirdIF::MAX_SALINITY_BOUND );
    minSalinityBoundCfgReader_ = newConfigReader( CTD_SeabirdIF::MIN_SALINITY_BOUND );
    offsetCfgReader_           = newConfigReader( CTD_SeabirdIF::OFFSET );
    verbosityCfgReader_        = newConfigReader( CTD_SeabirdIF::VERBOSITY_CFG );

    // Slate inputs
    depthReader_               = newUniversalReader( UniversalURI::DEPTH );
    latitudeReader_            = newUniversalReader( UniversalURI::LATITUDE );
    gfScanActiveReader_        = newDataReader( CBITIF::GF_ACTIVE_STATE );

    // initialize universal writers with specified accuracy
    conductivityWriter_        = newUniversalWriter( UniversalURI::SEA_WATER_ELECTRICAL_CONDUCTIVITY, Units::MILLIMHO_PER_CENTIMETER, CTD_ACCURACY_COND );
    temperatureWriter_         = newUniversalWriter( UniversalURI::SEA_WATER_TEMPERATURE, Units::CELSIUS, CTD_ACCURACY_TEMP );
    pressureWriter_            = newUniversalWriter( UniversalURI::SEA_WATER_PRESSURE, Units::DECIBAR, CTD_ACCURACY_PRESSURE );
    depthWriter_               = newUniversalWriter( UniversalURI::DEPTH, Units::METER, CTD_ACCURACY_DEPTH );
    salinityWriter_            = newUniversalWriter( UniversalURI::SEA_WATER_SALINITY, Units::PRACTICAL_SALINITY_UNIT, CTD_ACCURACY_SALINITY );
    densityWriter_             = newUniversalWriter( UniversalURI::SEA_WATER_DENSITY, Units::KILOGRAM_PER_CUBIC_METER, CTD_ACCURACY_DENSITY );
    speedOfSoundWriter_        = newUniversalWriter( UniversalURI::SPEED_OF_SOUND_IN_SEA_WATER, Units::METER_PER_SECOND, CTD_ACCURACY_SPEED );
    oxygenConcentrationWriter_ = newUniversalWriter( UniversalURI::MASS_CONCENTRATION_OF_OXYGEN_IN_SEA_WATER, Units::MICROGRAM_PER_LITER, CTD_ACCURACY_OXYGEN );

    // Component slate outputs
    oxygenFreqWriter_ = newDataWriter( CTD_SeabirdIF::OXYGEN_FREQUENCY );

    // Component binned slate outputs
    Str binChannel = getName() + "_BINNED";
    binMedianTemperatureWriter_            = newDataWriter( binChannel, CTD_SeabirdIF::BIN_MEDIAN_SEA_WATER_TEMPERATURE );
    binMeanTemperatureWriter_              = newDataWriter( binChannel, CTD_SeabirdIF::BIN_MEAN_SEA_WATER_TEMPERATURE );
    binStandardDeviationTemperatureWriter_ = newDataWriter( binChannel, CTD_SeabirdIF::BIN_STANDARD_DEVIATION_SEA_WATER_TEMPERATURE );
    binMedianSalinityWriter_               = newDataWriter( binChannel, CTD_SeabirdIF::BIN_MEDIAN_SEA_WATER_SALINITY );
    binMeanSalinityWriter_                 = newDataWriter( binChannel, CTD_SeabirdIF::BIN_MEAN_SEA_WATER_SALINITY );
    binStandardDeviationSalinityWriter_    = newDataWriter( binChannel, CTD_SeabirdIF::BIN_STANDARD_DEVIATION_SEA_WATER_SALINITY );

    this->setFailureMissionCritical( false );

    // This configures the advanced run modes.
    setRunState( START );
}

CTD_Seabird::~CTD_Seabird()
{
}

void CTD_Seabird::run()
{
}


void CTD_Seabird::uninitialize()
{
    if( !simulateHardware() )
    {
        logger_.syslog( "Powering down", Syslog::INFO );
        if( !loadControl_.powerDown() )
        {
            logger_.syslog( "Failed to power down", Syslog::FAULT );
            this->setFailure( FailureMode::HARDWARE );
        }
        uart_.close().flush();
    }
}


Component::RunState CTD_Seabird::start()
{
    if( debug_ ) logger_.syslog( "Start", Syslog::INFO );
    logger_.syslog( "Initializing CTD_Seabird." );

    // Get a latitude
    Slate::ReadOnce( "Config/workSite", "initLat", Units::RADIAN, latitude_, logger_ );

    readConfig();

    if( simulateHardware() )
    {
        startTime_ = Timestamp::Now();
        return STARTING;
    }

    deviceResponse_[0] = '\0';
    this->setAllowableFailures( 3 );
    this->setRetryTimeout( 150 );
    if( !loadControl_.powerUp() )
    {
        logger_.syslog( "Failed to power up CTD_Seabird load controller.", Syslog::FAULT );
        this->setFailure( FailureMode::HARDWARE );
        return START;
    }

    // Open the uart
    uart_.open().flush();
    if( uart_.hasError() )
    {
        logger_.syslog( "Error opening port: ", uart_.errorString(), Syslog::ERROR );
        this->setFailure( FailureMode::COMMUNICATIONS );
        return STOP;
    }
    startTime_ = Timestamp::Now();
    runTime_ = Timestamp::Now();
    return STARTING;
}


/// Might follow a STOP...START sequence
Component::RunState CTD_Seabird::starting()
{
    if( debug_ ) logger_.syslog( "Starting", Syslog::INFO );
    if( simulateHardware() )
    {
        return RUNNABLE;
    }

    // Allow time for the pump to complete a full flush through the CTD's plumbing
    if( runTime_.elapsed() < poTimeout_ )
    {
        startTime_ = Timestamp::Now();
        return STARTING;
    }

    if( getUartData() )
    {
        // OK to run in the next cycle
        startTime_ = Timestamp::Now();
        return RUNNABLE;
    }

    if( startTime_.elapsed() > ctdTimeout_ )
    {
        logger_.syslog( "Failed to initialize within timeout.", Syslog::FAULT );
        this->setFailure( FailureMode::COMMUNICATIONS );
        return STOP;
    }

    return STARTING;
}


/// Pause for a short period (indicated by pauseTime)
Component::RunState CTD_Seabird::pause()
{
    if( debug_ ) logger_.syslog( "Pause", Syslog::INFO );

    setWritersInvalid();

    if( !simulateHardware() )
    {
        if( !loadControl_.powerDown() )
        {
            logger_.syslog( "Failed to power down", Syslog::FAULT );
            this->setFailure( FailureMode::HARDWARE );
            return STOP;
        }
        uart_.close().flush();
    }

    return PAUSED;
}


/// Should eventually follow a PAUSE request: should set continueTime
Component::RunState CTD_Seabird::paused()
{
    // if( debug_ ) logger_.syslog( "Paused", Syslog::INFO );
    if( isDataRequested() )
    {
        return resume();
    }

    return PAUSED;
}


Component::RunState CTD_Seabird::resume()
{
    if( debug_ ) logger_.syslog( "Resume", Syslog::INFO );
    if( !simulateHardware() )
    {
        // Open the uart
        uart_.open();
        if( uart_.hasError() )
        {
            logger_.syslog( "Error opening port on resume: ", uart_.errorString(), Syslog::ERROR );
            this->setFailure( FailureMode::COMMUNICATIONS );
            return STOP;
        }

        if( !simulateHardware() && !loadControl_.powerUp() )
        {
            logger_.syslog( "Failed to power up", Syslog::FAULT );
            this->setFailure( FailureMode::HARDWARE );
            return STOP;
        }
    }
    return RESUMING;
}


Component::RunState CTD_Seabird::resuming()
{
    if( debug_ ) logger_.syslog( "Resuming", Syslog::INFO );

    startTime_ = Timestamp::Now();
    return RUNNABLE;
}


Component::RunState CTD_Seabird::runnable()
{
    // if( debug_ ) logger_.syslog( "Runnable", Syslog::INFO );

    // Pause if we don't want data
    if( !isDataRequested() ) return PAUSE;

    readConfig();

    if( !simulateHardware() )
    {
        // Log voltage and current and check for any faults
        loadControl_.requestVoltageAndCurrent();
        if( loadControl_.hasError() )
        {
            logger_.syslog( "LCB fault: " + loadControl_.errorString(), Syslog::FAULT );
            this->setFailure( FailureMode::HARDWARE );
            setRunState( STOP );
        }
    }

    // Always include simulated values, even if hardware exists.
    // If we are in the water, there will be no simulated values available.
    // (Trying to be consistent with this, but to me it seems it would be better to be explicit about using sim or hardware without allowing a potentially confusing mix.)
    if( getSimulatedData() || getUartData() ) // use simulated data or try to receive data from the UART (blocking w/ timeout)
    {
        preprocessData();
        writeData();
        updateTemporalBin();
        startTime_ = Timestamp::Now(); // Reset the time
        this->resetFailCount();
    }
    else if( startTime_.elapsed() > ctdTimeout_ )
    {
        logger_.syslog( "Failed to acquire real or simulated CTD data within timeout.", Syslog::FAULT );
        this->setFailure( FailureMode::COMMUNICATIONS );
        setRunState( STOP );
    }

    return RUNNABLE;
}


Component::RunState CTD_Seabird::stop()
{
    if( debug_ ) logger_.syslog( "Stop", Syslog::INFO );

    setWritersInvalid();

    if( !simulateHardware() )
    {
        uninitialize(); // First power down then query for faults next cycle
    }

    return STOPPING;
}


Component::RunState CTD_Seabird::stopping()
{
    if( debug_ ) logger_.syslog( "Stopping", Syslog::INFO );
    if( !simulateHardware() )
    {
        loadControl_.readFaults(); // See if anything went wrong that may have caused this request for uninitialize
        if( loadControl_.hasError() )
        {
            logger_.syslog( "LCB fault: " + loadControl_.errorString(), Syslog::FAULT );
            this->setFailure( FailureMode::HARDWARE );
        }

    }
    // skipping pasusePeriod here as we can run straight through to stopped
    return STOPPED;
}


Component::RunState CTD_Seabird::stopped()
{
    // if( debug_ ) logger_.syslog( "Stopped", Syslog::INFO );
    if( isDataRequested() )
    {
        return start();
    }

    if( !simulateHardware() )
    {
        if( ( loadControl_.getPowerState() != LoadControl::OFF ) && ( loadControl_.getPowerState() != LoadControl::POWER_DOWN ) )
        {
            return stop();
        }

        // Close if the uart if it is open
        if( uart_.isReadable() )
        {
            uart_.close();
        }
    }

    return STOPPED;
}


void CTD_Seabird::readConfig()
{
    sbe43FSerialNumberCfgReader_->read( Units::NONE_INT, sbe43FSerialNumber_ );
    oxygenCalCoeffFOffsetCfgReader_->read( Units::NONE, FOFFSET_ );
    oxygenCalCoeffSocCfgReader_->read( Units::NONE, SOC_ );
    oxygenCalCoeffACfgReader_->read( Units::NONE, A_ );
    oxygenCalCoeffBCfgReader_->read( Units::NONE, B_ );
    oxygenCalCoeffCCfgReader_->read( Units::NONE, C_ );
    oxygenCalCoeffECfgReader_->read( Units::NONE, E_ );
    maxPressBoundCfgReader_->read( Units::DECIBAR, maxPressBound_ );
    minPressBoundCfgReader_->read( Units::DECIBAR, minPressBound_ );
    maxSalinityBoundCfgReader_->read( Units::PRACTICAL_SALINITY_UNIT, maxSalinityBound_ );
    minSalinityBoundCfgReader_->read( Units::PRACTICAL_SALINITY_UNIT, minSalinityBound_ );
    offsetCfgReader_->read( Units::DECIBAR, pressureOffset_ );
    verbosityCfgReader_->read( Units::COUNT, verbosity_ );
}

void CTD_Seabird::setWritersInvalid()
{
    densityWriter_->setInvalid( true );
    depthWriter_->setInvalid( true );
    pressureWriter_->setInvalid( true );
    salinityWriter_->setInvalid( true );
    temperatureWriter_->setInvalid( true );
    conductivityWriter_->setInvalid( true );
    speedOfSoundWriter_->setInvalid( true );
    if( sbe43FSerialNumber_ != 0 ) oxygenConcentrationWriter_->setInvalid( true );
}

bool CTD_Seabird::isDataRequested()
{
    return densityWriter_->isDataRequested()
           || depthWriter_->isDataRequested()
           || pressureWriter_->isDataRequested()
           || salinityWriter_->isDataRequested()
           || temperatureWriter_->isDataRequested()
           || conductivityWriter_->isDataRequested()
           || speedOfSoundWriter_->isDataRequested()
           || ( sbe43FSerialNumber_ != 0 && oxygenConcentrationWriter_->isDataRequested() )
           || isDepthNeeded();
}

// This method should power the CTD if for any reason the main depth sensor is not writing to the universal
bool CTD_Seabird::isDepthNeeded( void )
{
    if( !depthReader_->isActive() && !depthReader_->wasTouchedSinceLastRun( this ) )
    {
        return true;
    }
    return false;
}


bool CTD_Seabird::getUartData()
{
    bool validResponse( false );

    if( readUntilLatest() )
    {
        // We have a recent line to parse.
        if( parseResponse() )
        {
            validResponse = true;
        }
        else
        {
            validResponse = false;
        }
        uart_.flushCRLF();
    }

    return validResponse;
}


bool CTD_Seabird::readUntilLatest()
{
    int msgCount( 0 );
    bool processPacket( false );

    if( !simulateHardware() )
    {
        // Read most recent device data
        while( uart_.dataAvailable() && ( msgCount < MAX_DEVICE_MSG_QUEUE_SIZE ) )
        {
            deviceResponse_[0] = '\0';

            // The uart explicitly blocks here so we don't run in a fast loop
            if( uart_.readUntil( deviceResponse_, MAX_DEVICE_RESPONSE_SIZE, "\r\n", 1 ).flushCRLF().hasError() )
            {
                if( uart_.getError() != UartStream::TIMEOUT )
                {
                    logger_.syslog( "Uart error: ", uart_.errorString(), Syslog::ERROR );
                    this->setFailure( FailureMode::COMMUNICATIONS );
                    setRunState( STOP );
                }
                processPacket = false;
                break;
            }
            else
            {
                // Got device response
                if( verbosity_ > 2 ) logger_.syslog( "Got response: " + Str( deviceResponse_ ) + ". Skip count: " + Str( msgCount ) + ".", Syslog::INFO );
                dataTime_ = Timestamp::Now();
                processPacket = true;
            }

            ++msgCount;
        }

        if( msgCount >= MAX_DEVICE_MSG_QUEUE_SIZE )
        {
            // Flush the buffer in case we're out of lockstep
            logger_.syslog( "Device message queue exceeded the allowed limit.", Syslog::ERROR );
            processPacket = false;
            uart_.flush();
        }
    }

    return processPacket;
}


bool CTD_Seabird::parseResponse()
{
    pressureDB_ = nanf( "" );
    temperature_ = nanf( "" );
    conductivity_ = nanf( "" );
    oxygenFreq_  = nanf( "" );

    if( deviceResponse_ == NULL )
    {
        logger_.syslog( "CTD sample equals NULL.", Syslog::ERROR );
        return false;
    }

    // Remove whitespaces from response, as they vary depending on device configurations
    trimResponse();

    if( CTD_DATA_ITEMS != sscanf( deviceResponse_, "%f,%f,%f,%f", &pressureDB_, &temperature_, &conductivity_, &oxygenFreq_ ) )
    {
        logger_.syslog( Str( "Failed to parse device response: " ) + deviceResponse_, Syslog::ERROR );
    }
    else
    {
        if( verbosity_ > 0 ) logger_.syslog( Str( "Pressure=" + Str( pressureDB_ ) + " dBar, Temperature=" + Str( temperature_ ) + " degC, Conductivity=" + Str( conductivity_ ) + " S/m, OxygenFreq=" + Str( oxygenFreq_ ) ) + " Hz.", Syslog::INFO );
        return true;
    }

    return false;
}


void CTD_Seabird::trimResponse( void )
{
    int i = 0, j = 0;
    deviceResponse_[uart_.bytesRead()] = '\0';

    while( deviceResponse_[i] )
    {
        if( deviceResponse_[i] != ' ' )
            deviceResponse_[j++] = deviceResponse_[i];
        i++;
    }
    deviceResponse_[j] = '\0';
}


bool CTD_Seabird::getSimulatedData()
{
    if( debug_ )
    {
        logger_.syslog( "getSimulatedData()" );
    }

    if( SimSlate::Read( SimSlate::DEPTH_METER, depth_ )
            && SimSlate::Read( SimSlate::SALINITY_PART_PER_THOUSAND, salinity_ ) // Looks like we're reading the simulated salinity in PPT, but using it as PSU
            && SimSlate::Read( SimSlate::TEMPERATURE_DEGREE_CELSIUS, temperature_ ) )
    {
        if( debug_ )
        {
            logger_.syslog( "getSimulatedData(); got data from slate" );
        }
        dataTime_ = Timestamp::Now();

        // Truncate out-of-bounds salinity for sim
        if( salinity_ < minSalinityBound_ ) salinity_ = minSalinityBound_;
        if( salinity_ > maxSalinityBound_ ) salinity_ = maxSalinityBound_;

        conductivity_ = 4.0; // hack to make sure there is something to operate on

        return true;
    }
    else
    {
        if( debug_ )
        {
            logger_.syslog( "getSimulatedData() - return false" );
        }
        return false;
    }
}


// TODO: account for lag in pumped path - we measured ~7 sec from intake to exhaust in the tank using fluorescein 12/03/2018
void CTD_Seabird::oxygenConcentration( void )
{
    // Kelvin
    double K = temperature_ + 273.15;

    // Oxygen solubility
    double oxsol = AuvMath::Oxsol( temperature_, salinity_ );

    double OxyCo = SOC_ * ( oxygenFreq_ + FOFFSET_ ) *
                   ( 1.0 + A_ * temperature_ + B_ * pow( temperature_, 2 ) + C_ * pow( temperature_, 3 ) ) *
                   oxsol * exp( E_ * pressureDB_ / K );

    // Convert Oxygen concentration units to ug/L. [ug/L] = [ml/L] * 1.42903 * 1000
    oxygenConcentration_ = OxyCo * 1429.03;
}


void CTD_Seabird::preprocessData()
{
    bool gfScanActive( false );
    float latitude( nanf( "" ) );

    density_ = nanf( "" );
    soundSpeed_ = nanf( "" );
    oxygenConcentration_ = nanf( "" );
    badConductivity_ = badTemperature_ = badPressure_ = badSalinity_ = false;

    // Get a latitude
    if( latitudeReader_->isActive() && latitudeReader_->read( Units::RADIAN, latitude ) && isnan( latitude ) )
    {
        latitude_ = latitude;
    }

    if( simulateHardware() )
    {
        pressureDB_ = AuvMath::OceanPressure( depth_, latitude_ ) / 10000;
        density_ = AuvMath::Density( salinity_, temperature_ + 273.15, pressureDB_ * 10000 );
        soundSpeed_ = AuvMath::SoundSpeed( salinity_, temperature_, pressureDB_ );
    }
    else
    {
        pressureDB_ += pressureOffset_;
        if( ( pressureDB_ < minPressBound_ ) || ( pressureDB_ > maxPressBound_ ) )
        {
            logger_.syslog( "Pressure reading out of range: " + Str( pressureDB_ ) + " decibar", Syslog::ERROR );
            badPressure_ = true;
            badSalinity_ = true;
        }
        else
        {
            // Pressure is within bounds, convert pressure (Pascal!) to depth
            depth_ = AuvMath::OceanDepth( pressureDB_ * 10000.0, latitude_ );

            if( gfScanActiveReader_->read( gfScanActive ) && gfScanActive )
            {
                logger_.syslog( "Ground Fault scan is active; will mark data as invalid.", Syslog::INFO );
                badConductivity_ = true;
                badSalinity_ = true;
            }
            else
            {
                // Convert conductivity reading (in mS/cm) to salinity
                salinity_ = AuvMath::PracticalSalinity( ( conductivity_ * 10 ), temperature_, pressureDB_ );

                if( ( salinity_ < minSalinityBound_ ) || ( salinity_ > maxSalinityBound_ ) )
                {
                    logger_.syslog( "Salinity reading out of range: " + Str( salinity_ ) + " psu", Syslog::ERROR );
                    badSalinity_ = true;
                }
                else
                {
                    density_ = AuvMath::Density( salinity_, temperature_ + 273.15, pressureDB_ * 10000 );
                    soundSpeed_ = AuvMath::SoundSpeed( salinity_, temperature_, pressureDB_ );
                    if( sbe43FSerialNumber_ != 0 ) oxygenConcentration();
                }
            }
        }
    }
}


void CTD_Seabird::writeData()
{
    conductivityWriter_->setInvalid( badConductivity_ );
    temperatureWriter_->setInvalid( badTemperature_ );
    pressureWriter_->setInvalid( badPressure_ );
    depthWriter_->setInvalid( badPressure_ );
    salinityWriter_->setInvalid( badSalinity_ );
    densityWriter_->setInvalid( badTemperature_ || badPressure_ || badSalinity_ );
    speedOfSoundWriter_->setInvalid( badTemperature_ || badPressure_ );

    conductivityWriter_->write( Units::MILLIMHO_PER_CENTIMETER, conductivity_, dataTime_ );
    temperatureWriter_->write( Units::CELSIUS, temperature_, dataTime_ );
    pressureWriter_->write( Units::DECIBAR, pressureDB_, dataTime_ );
    depthWriter_->write( Units::METER, depth_, dataTime_ );
    salinityWriter_->write( Units::PRACTICAL_SALINITY_UNIT, salinity_, dataTime_ );
    densityWriter_->write( Units::KILOGRAM_PER_CUBIC_METER, density_, dataTime_ );
    speedOfSoundWriter_->write( Units::METER_PER_SECOND, soundSpeed_, dataTime_ );

    if( sbe43FSerialNumber_ != 0 )
    {
        oxygenConcentrationWriter_->setInvalid( badTemperature_ || badPressure_ || badSalinity_ );
        oxygenConcentrationWriter_->write( Units::MICROGRAM_PER_LITER, oxygenConcentration_, dataTime_ );
        oxygenFreqWriter_->write( Units::HERTZ, oxygenFreq_, dataTime_ );
    }
}

// TODO: data binning really doesn't belong in a device driver. Move it out of here.
void CTD_Seabird::updateTemporalBin()
{
    if( badTemperature_ || badSalinity_ )
    {
        logger_.syslog( "some bad data, not updating bins", Syslog::INFO );
    }
    else
    {
        temperatureBin_.add( temperature_, dataTime_ );
        salinityBin_.add( salinity_, dataTime_ );
        Timestamp binTime;
        if( temperatureBin_.window_refreshed() )
        {
            binTime = temperatureBin_.median_time();
            binMedianTemperatureWriter_->write( Units::CELSIUS, temperatureBin_.median(), binTime );
            binMeanTemperatureWriter_->write( Units::CELSIUS, temperatureBin_.mean(), binTime );
            binStandardDeviationTemperatureWriter_->write( Units::CELSIUS, std::sqrt( temperatureBin_.variance() ), binTime );

        }
        if( salinityBin_.window_refreshed() )
        {
            binTime = salinityBin_.median_time();
            binMedianSalinityWriter_->write( Units::PRACTICAL_SALINITY_UNIT, salinityBin_.median(), binTime );
            binMeanSalinityWriter_->write( Units::PRACTICAL_SALINITY_UNIT, salinityBin_.mean(), binTime );
            binStandardDeviationSalinityWriter_->write( Units::PRACTICAL_SALINITY_UNIT, std::sqrt( salinityBin_.variance() ), binTime );
        }
    }
}

/// Should return [myNamespace]::SIMULATE_HARDWARE, or [myNamespace]::POWER, etc
ConfigURI CTD_Seabird::getConfigURI( ConfigOption configOption ) const
{
    switch( configOption )
    {
    case CONFIG_POWER:
        return CTD_SeabirdIF::POWER;
    case CONFIG_SIMULATE_HARDWARE:
        return CTD_SeabirdIF::SIMULATE_HARDWARE;
    default:
        return ConfigURI::NO_CONFIG_URI;
    }
}

