/***********************************************************************
  adsendxs.c

  Earthworm digitizer using National Instruments X-series DAQ modules
  and Trimble TSIP GPS units.  Uses a 1-PPS pulse from the GPS to trigger
  collection of N samples, using NI Retrigerable Analog Input mode.
  This program replaces the adsend program, which works with an older-
  an older-model NI PCI-6040E board configured for +-2.5V analog signals.

  Requirements:

   1. NI X-series DAQ device.  PCI-Express and USB models are available.
      Program tested with a model NI PCIe-6343 DAQ board.  Inputs are
      connected through SCB-68 patch panels.

   2. GPS receiver/clock capable of producing a 1-PPS output on the
      second.  Configuration and time access are provided by RS-232
      serial port, using TSIP protocol.  Program tested with a Trimble
      ThunderBolt E GPS Disciplined Clock.

  Features and Notes:

   1. NI X-series modules are available with 16 or 32 input channels.
      To achieve a higher channel count, more than one NI module may
      be connected to the same computer.

   2. NI modules were tested in NSRE mode, in which input voltages are
      referenced to AISENSE (pin 62).  We jumper AISENSE to AIGND (pin 29).

   3. The T-Bolt E contains a double-ovenized, disciplined clock that
      will survive GPS signal losses of days or weeks with no
      appreciable loss of timing accuracy.

   4. Sample rate is configurable.  Tested sample rates are 100, 200,
      500, and 1000 sps.  Higher sample rates are possible but limited
      by the maximum tracebuf message length of 4096 bytes.

   5. Tracebuf messages are all one-second in length, and they start
      on the second.

   6. Program will optionally set PC system clock to GPS time.

   7. Program creates TYPE_SNW messages for loss-of-satellite synch
      and GPS antenna open.  These can be converted to files using
      the Earthworm ew2file program.  The snwclient program can pick
      up these files and send them to a SeisnetWatch server.
************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <string.h>
#include <sys/timeb.h>
#include <NIDAQmx.h>

#include <earthworm.h>
#include <transport.h>
#include <trace_buf.h>
#include <time_ew.h>
#include "adsendxs.h"

/* Function declarations
   *********************/
void  DAQmxErrorDetected( TaskHandle taskHandle );
void  ExitAdsendxs( TaskHandle taskHandle );
int   GetGpsTime( time_t *tgps, int *antennaOpen );
int   InitPort( int ComPort, int BaudRate );
void  ClosePort( void );
int   DisableAutoPackets( void );
int   SetUtcMode( void );
int   GetPpsOutputStatus( void );
int   GetConfig( char *configfilename );
void  LogConfig( void );
void  SendHeartbeat( void );
void  ReportErrorToStatmgr( int errNum, char *errmsg );
void  FloatToShort( int Nchan, uInt32 nSamples, short *idata );
void  ReportTimeSinceLastLockToSNW( time_t tgps, int gpsTimeStatus, FILE *gpsLockFile );
void  ReportAntennaStatusToSNW( int antennaOpen );
void  SetPcClock( time_t tgps );
int32 CVICALLBACK EveryNCallback( TaskHandle taskHandle, int32 everyNsamplesEventType,
                                  uInt32 nSamples, void *callbackData );
int32 CVICALLBACK DoneCallback( TaskHandle taskHandle, int32 status, void *callbackData );

/* Declared in getconfig.c
   ***********************/
extern unsigned char ModuleId;     // Data source id placed in trace header
extern long    OutKey;             // Key to ring where traces will live
extern int     Nchan;              // Number of channels to digitize
extern int     SampRate;           // Samples per second per channel
extern SCNL    *ChanList;          // Array to fill with SCNL values
extern char    TriggerChan[];      // Physical channel where trigger is found
extern int     ComPort;            // Com port for reading GPS time
extern int     BaudRate;           // Baud rate of com port
extern int     GetGpsDelay;        // Wait this many msec before requesting GPS time
extern int     UpdatePcClock;      // If 1, update PC clock from GPS
extern int     PcClockUpdInt;      // Seconds between PC clock updates from GPS
extern int     Debug;              // If > 0, log GPS flags
extern int     OnePps;             // Set to one if a 1-PPS pulse is available from the GPS.

/* Variables global to this file
   *****************************/
static float64  *fldata;            // NIDAQmx routines fill this buffer
static char     *traceBuf;          // Where the trace message is assembled
static TRACE2_HEADER *traceHead;    // Where the trace header is stored
static short    *traceDat;          // Where the data points are stored in the trace msg
static int      traceBufSize;       // In bytes, including tracebuf header
static MSG_LOGO tbufLogo;           // Logo of tracebuf message to put out
static int      callbackExitFlag=0; // If 1, a callback function wants the program to exit
static time_t   callbackTime;       // When the last callback occurred
static FILE     *gpsLockFile;       // Contains time of last GPS lock
static int      antennaOpenPrev = UNKNOWN;

/* Share these with other files
   ****************************/
SHM_INFO        Region;              // Structure for shared-memory region
pid_t           MyPid;               // Process id, sent with heartbeat
MSG_LOGO        snwLogo;             // Logo of SeisnetWatch message to be sent


/* The main program starts here
   ****************************/
int main( int argc, char *argv[] )
{
   int32         rc;
   TaskHandle    taskHandle=0;      // For NI callback task
   unsigned char InstId;            // Installation id placed in trace header
   int           chan;              // Channel number

/* Get command line arguments
   **************************/
   if ( argc < 2 )
   {
      printf( "Usage: adsendxs <config file>\n" );
      return -1;
   }

/* Initialize name of log file and open it
   ***************************************/
   logit_init( argv[1], 0, 1024, 1 );

/* Read and log configuration parameters
   *************************************/
   if ( GetConfig( argv[1] ) < 0 )
   {
      logit( "e", "GetConfig() error.  Exiting.\n" );
      return -1;
   }
   LogConfig();

/* Print a warning message
   ***********************/
   if ( OnePps == 0 )
   {
      logit( "e", "\n" );
      logit( "e", "***************************************************************\n" );
      logit( "e", "WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING\n" );
      logit( "e", "The OnePps parameter is set to zero in the configuration file,\n" );
      logit( "e", "digitizing will occur without regard to an an external 1-PPS\n" );
      logit( "e", "trigger pulse.  Tracebuf timestamps will not be synched to GPS\n" );
      logit( "e", "time, and timing errors of up to one second can be expected.\n" );
      logit( "e", "***************************************************************\n\n" );
   }

/* Get our process id for restart purposes
   ***************************************/
   MyPid = _getpid();
   if( MyPid == -1 )
   {
      logit( "e", "Error. Cannot get PID. Exiting.\n" );
      return -1;
   }

/* Get logos of outgoing waveform and SNW messages
   ***********************************************/
   if ( GetLocalInst( &InstId ) < 0 )
   {
      logit( "e", "Error getting local installation id. Exiting.\n" );
      return -1;
   }
   logit( "t", "Local installation id: %u\n", InstId );

   tbufLogo.instid = InstId;
   tbufLogo.mod    = ModuleId;
   if ( GetType( "TYPE_TRACEBUF2", &tbufLogo.type ) == -1 )
   {
      logit( "e", "Error getting type code TYPE_TRACEBUF2. Exiting.\n" );
      return -1;
   }

   snwLogo.instid = InstId;
   snwLogo.mod    = ModuleId;
   if ( GetType( "TYPE_SNW", &snwLogo.type ) == -1 )
   {
      logit( "e", "Error getting type code TYPE_SNW. Exiting.\n" );
      return -1;
   }

/* Allocate some array space.
   fldata = Buffer for 64-bit float data from NIDAQmx
   traceBuf = Earthworm message buffer
   **************************************************/
   if ( (fldata = malloc(Nchan * SampRate * sizeof(float64))) == NULL )
   {
      logit( "e", "Error allocating fldata array.  Exiting.\n" );
      return 0;
   }
   traceBufSize = sizeof(TRACE2_HEADER) + (SampRate * sizeof(short));
   logit( "t", "Trace buffer size: %d bytes\n", traceBufSize );
   traceBuf = (char *) malloc( traceBufSize );
   if ( traceBuf == NULL )
   {
      logit( "e", "Error: Cannot allocate trace buffer. Exiting.\n" );
      return -1;
   }
   traceHead = (TRACE2_HEADER *) &traceBuf[0];
   traceDat  = (short *) &traceBuf[sizeof(TRACE2_HEADER)];

/* Open file containing time of last GPS lock to satellites.
   File will be opened for updating (reading and writing).
   In mode r+, the file must exist.
   In mode w+, a new empty file is created.
   ********************************************************/
   gpsLockFile = fopen( "gps_lock_file", "r+" );
   if ( gpsLockFile == NULL )                     // File does not exist
   {
      gpsLockFile = fopen( "gps_lock_file", "w+" );
      if ( gpsLockFile == NULL )                  // Can't create file
      {
         logit( "et", "Error creating gps_lock_file.  Exiting.\n" );
         return -1;
      }
   }

/* Attach to Earthworm transport ring
   **********************************/
   tport_attach( &Region, OutKey );
   logit( "et", "Attached to Earthworm transport ring: %d\n", OutKey );

/* Now, start setting up the Trimble GPS receiver.
   First, initialize the com port that will talk to the GPS.
   ********************************************************/
   logit( "et", "Opening com port.\n" );
   if ( InitPort(ComPort, BaudRate) < 0 )
   {
      logit( "et", "InitPort error on GPS com port.  Exiting.\n" );
      return -1;
   }

/* Disable automatic output packets from the GPS.  By default,
   the primary and supplemental timing packets are sent after
   every one-second pulse.  Not sure if this feature stays
   in effect if power is cycled on the GPS.
   **********************************************************/
   logit( "et", "Disabling automatic packet output from GPS.\n" );
   if ( DisableAutoPackets() < 0 )
   {
      logit( "et", "DisableAutoPackets error.\n" );
      ExitAdsendxs( taskHandle );
   }

/* Get 1-PPS status byte.
   *********************/
   logit( "et", "Getting 1-PPS status byte.\n" );
   if ( GetPpsOutputStatus() < 0 )
   {
      logit( "et", "GetPpsOutputStatus error.\n" );
      ExitAdsendxs( taskHandle );
   }

/* Put the GPS into UTC mode.  This will cause leap seconds
   to be applied to GPS time, and the one-PPS pulse will be
   referenced to UTC time.  This feature is stored in NV
   memory, so it will survive a GPS reboot or power cycle.
   ********************************************************/
   logit( "et", "Putting GPS into UTC mode.\n" );
   if ( SetUtcMode() < 0 )
   {
      logit( "et", "SetUtcMode error.\n" );
      ExitAdsendxs( taskHandle );
   }

/* Start setting up the X-series DAQ board.
   First, we create an analog input task.
   ***************************************/
   logit( "et", "Creating the NI analog input task.\n" );
   rc = DAQmxCreateTask( "AnalogInputTask", &taskHandle );
   if ( DAQmxFailed(rc) ) DAQmxErrorDetected( taskHandle );

/* Use non-referenced, single-ended (NRSE) inputs.
   Set up min and max input voltages for each channel.
   **************************************************/
   logit( "et", "Setting up NI NRSE inputs for each channel to digitize.\n" );
   for ( chan = 0; chan < Nchan; chan++ )
   {
      rc = DAQmxCreateAIVoltageChan( taskHandle,
           ChanList[chan].daqChan,  // Physical channel name
           "",                      // Virtual name to assign to channel
           DAQmx_Val_NRSE,          // Input terminal config for channel
           -2.5,                    // Min voltage you expect to measure
           +2.5,                    // Max voltage you expect to measure
           DAQmx_Val_Volts,         // Values are returned in volts
           NULL );                  // Custom scale name (not used)

      if ( DAQmxFailed(rc) ) DAQmxErrorDetected( taskHandle );
   }

/* Specify sample clock timing.  If a 1-PPS trigger is available,
   use it to trigger 100 sample scans.  Otherwise, use the
   internal NI clock to collect continuous samples, in which
   case data timestamps will be approximate.
   *************************************************************/
   logit( "et", "Configuring NI sample clock timing.\n" );
   {
      int32 sampleMode = OnePps ? DAQmx_Val_FiniteSamps : DAQmx_Val_ContSamps;

      rc = DAQmxCfgSampClkTiming( taskHandle,
           NULL,                     // Use on-board clock
           (float64)SampRate,        // Sampling rate
           DAQmx_Val_Rising,         // Acquire on rising edge of sample clock
           sampleMode,               // Specify finite or continuous data collection
           (uInt64)SampRate );       // Samples per channel to acquire
      if ( DAQmxFailed(rc) ) DAQmxErrorDetected( taskHandle );
   }

/* Specify the physical channel of the start trigger for the AI task.
   This channel is connected to the 1-pps output from the GPS receiver.
   Make the analog input task retriggerable.  X-series NI boards
   support retriggerable AI tasks, but E and M-series boards do not.
   *******************************************************************/
   if ( OnePps )
   {
      logit( "et", "Configuring the NI digital edge start trigger.\n" );
      rc = DAQmxCfgDigEdgeStartTrig( taskHandle,
           TriggerChan,           // Configurable, but normally PFI0
           DAQmx_Val_Rising );    // Trigger on rising edge
      if ( DAQmxFailed(rc) ) DAQmxErrorDetected( taskHandle );

      logit( "et", "Making the NI start trigger retriggerable.\n" );
      rc = DAQmxSetStartTrigRetriggerable( taskHandle, TRUE );
      if ( DAQmxFailed(rc) ) DAQmxErrorDetected( taskHandle );
   }

/* The EveryNCallback function is invoked after SampRate samples are acquired.
   **************************************************************************/
   logit( "et", "Registering the NI acquisition callback function.\n" );
   rc = DAQmxRegisterEveryNSamplesEvent( taskHandle,
        DAQmx_Val_Acquired_Into_Buffer,  // Data are being acquired
        (uInt32)SampRate,                // After SampRate samples are collected,
                                         //    the callback event will occur.
        0,                               // Function called in a DAQmx thread
        EveryNCallback,                  // Name of callback function
        NULL );                          // Value passed to the EveryNCallback function

   if ( DAQmxFailed(rc) ) DAQmxErrorDetected( taskHandle );

/* The DoneCallback function is invoked if the analog
   task stops due to an error.
   **************************************************/
   logit( "et", "Registering the NI done-event callback function.\n" );
   rc = DAQmxRegisterDoneEvent( taskHandle,
        0,                      // Function called in a DAQmx thread
        DoneCallback,           // Name of callback function
        NULL );                 // Value passed to the callback function

   if ( DAQmxFailed(rc) ) DAQmxErrorDetected( taskHandle );

/* Start the analog input task
   ***************************/
   logit( "et", "Starting the analog input task.\n" );
   rc = DAQmxStartTask( taskHandle );
   if ( DAQmxFailed(rc) ) DAQmxErrorDetected( taskHandle );
   logit( "et", "The analog input task is started.\n" );

   time( &callbackTime );     // Remember when analog input began.

/* The main program loop begins here.  Just send heartbeats
   to statmgr and wait for a program termination request.
   ********************************************************/
   while ( 1 )
   {
      SendHeartbeat();        // Send heartbeat to statmgr

/* If no callbacks occur for callbackWaitTime seconds, exit
   the program.  The external trigger pulse was probably lost.
   **********************************************************/
      if ( OnePps )
      {
         const int callbackWaitTime = 600;
         time_t now;

         time( &now );
         if ( (now - callbackTime) >= callbackWaitTime )
         {
            char errMsg[80];
            logit( "et", "Error: Callback function not invoked for %d seconds, probably\n",
                   callbackWaitTime );
            logit( "et", "because no 1-PPS trigger pulses were received from the T-Bolt.\n" );
            logit( "et", "Exiting.\n" );
            sprintf( errMsg, "Error: NI-DAQ not receiving trigger pulses from GPS.\n" );
            ReportErrorToStatmgr( NOPULSE_ERROR, errMsg );
            ExitAdsendxs( taskHandle );
         }
         callbackTime = now;
      }

/* If the callback function detects a serious error,
   exit the program.
   ************************************************/
      if ( callbackExitFlag == 1 )
      {
         logit( "et", "The NI callback function detected a serious error. Exiting.\n" );
         ExitAdsendxs( taskHandle );
      }

/* Startstop asked this program to exit
   ************************************/
      if ( tport_getflag( &Region ) == TERMINATE  ||
           tport_getflag( &Region ) == MyPid )
         ExitAdsendxs( taskHandle );

      sleep_ew( 500 );
   }
}


/**********************************************************
  DAQmxErrorDetected

  This gets called if the DAQ board cannot be initialized.
 **********************************************************/

void DAQmxErrorDetected( TaskHandle taskHandle )
{
   char errBuff[2048] = {'\0'};

   DAQmxGetExtendedErrorInfo( errBuff, 2048 );
   logit( "et", "DAQmx Error: %s\n", errBuff );
   ExitAdsendxs( taskHandle );
}


/**********************************************************
  ExitAdsendxs

  Clean up and exit this program.
 **********************************************************/

void ExitAdsendxs( TaskHandle taskHandle )
{
   if ( taskHandle != 0 )
   {
      DAQmxStopTask( taskHandle );
      DAQmxClearTask( taskHandle );
   }
   ClosePort();
   logit( "et", "adsendxs exiting.\n" );
   exit( 0 );
}


/**************************************************************
  EveryNCallback

  This function is called after nSamples samples are read
  from the NIDAQmx device.
 **************************************************************/

int32 CVICALLBACK EveryNCallback( TaskHandle taskHandle,
                                  int32 everyNsamplesEventType,
                                  uInt32 nScansRequested,
                                  void *callbackData )
{
   int    i;
   int32  rc;
   char   errBuff[2048] = {'\0'};   // Message returned by NIDAQmx
   char   errMsg[80];               // Message sent to statmgr
   int32  nScansRead;
   short  *idata = (short *)fldata;
   time_t tgps;
   int    gpsTimeStatus;        // Possible values are CRITICAL ERROR,
                                // GPS TIME_UNAVAILABLE,
                                // UNLOCKED_GPS_TIME_AVAILABLE,
                                // or LOCKED_GPS_TIME_AVAILABLE
   int    antennaOpen;          // Possible values are TRUE, FALSE, or UNKNOWN
   static int sendingTracebuf = FALSE;  // Sending tracebuf messages

/* Keep track of when this function is called
   ******************************************/
   time( &callbackTime );

/* Read DAQ samples in floating-point format.
   A read error will occur if data processing falls behind
   the collection rate, causing the program to exit.
   *******************************************************/
   rc = DAQmxReadAnalogF64( taskHandle,
        -1,                           // Read all samples that were requested
        0.0,                          // Timeout in seconds.  If 0, read all
                                      // available samples and return immediately.
        DAQmx_Val_GroupByScanNumber,  // Multiplexed data
        fldata,                       // f64 array to contain data
        Nchan*nScansRequested,        // Size of fldata array, in samples
        &nScansRead,                  // Samples per channel read
        NULL );                       // Reserved by NI

   if ( DAQmxFailed(rc) )
   {
      DAQmxGetExtendedErrorInfo( errBuff, 2048 );        // Log the error
      logit( "t", "DAQmxReadAnalogF64 error.\n" );
      logit( "", "%s\n", errBuff );

      sprintf( errMsg, "DAQmxReadAnalogF64 error.\n" );  // Report error to statmgr
      ReportErrorToStatmgr( NI_ERROR, errMsg );
      callbackExitFlag = 1;                              // Tell main program to exit
      return 0;
   }

/* Did we get all data requested from the DAQ module?
   *************************************************/
   if ( (uInt32)nScansRead < nScansRequested )
   {
      logit( "et", "Error.  We requested %u scans, but\n", nScansRequested );
      logit( "et", "   DAQmxReadAnalogF64 returned only %d scans.\n", nScansRead );
      logit( "et", "   Discarding this one-second packet.\n" );
      return 0;
   }

/* Convert floating-point volts to short integers,
   to emulate output from an NI PCI-6040E board.
   **********************************************/
   FloatToShort( Nchan, nScansRead, idata );

/* Sleep until reliable timing packets are available
   *************************************************/
   sleep_ew( GetGpsDelay );

/* Get time from GPS clock.  Possible return values are:
   CRITICAL ERROR, GPS TIME_UNAVAILABLE,
   UNLOCKED_GPS_TIME_AVAILABLE, or LOCKED_GPS_TIME_AVAILABLE
   *********************************************************/
   gpsTimeStatus = GetGpsTime( &tgps, &antennaOpen );

   if ( gpsTimeStatus == CRITICAL_ERROR )
   {
      callbackExitFlag = 1;     // Tell main program to exit
      return 0;
   }

/* Log and send email if changes occur in the antenna-open
   status.  Send antenna status to SNW periodically.
   *******************************************************/
   if ( (antennaOpen == TRUE) && (antennaOpenPrev != TRUE) )
   {
      sprintf( errMsg, "GPS antenna open." );
      logit( "et", "%s\n", errMsg );
      ReportErrorToStatmgr( GPS_ERROR, errMsg );
   }
   if ( (antennaOpen != TRUE) && (antennaOpenPrev == TRUE) )
   {
      sprintf( errMsg, "GPS antenna not open." );
      logit( "et", "%s\n", errMsg );
      ReportErrorToStatmgr( GPS_ERROR, errMsg );
   }
   antennaOpenPrev = antennaOpen;

   ReportAntennaStatusToSNW( antennaOpen );

/* Log if we start or stop sending tracebuf messages
   *************************************************/
   if ( gpsTimeStatus == GPS_TIME_UNAVAILABLE )
   {
       if ( sendingTracebuf == TRUE )
          logit( "et", "Stopped sending tracebuf messages to transport ring.\n" );
       sendingTracebuf = FALSE;
       return 0;                // Wait for next callback to occur
   }
   else
   {
       if ( sendingTracebuf == FALSE)
          logit( "et", "Sending tracebuf messages to transport ring.\n" );
       sendingTracebuf = TRUE;
   }

/* We got time from the GPS clock, although the clock may not
   be locked to the satellites.  Report time since GPS was last
   locked to SNW.  Then, update gpsLockFile if GPS clock is
   currently locked to satellites.
   ************************************************************/
   ReportTimeSinceLastLockToSNW( tgps, gpsTimeStatus, gpsLockFile );

/* Set PC clock to GPS time
   ************************/
   if ( UpdatePcClock )
      if ( (tgps % PcClockUpdInt) == 0 )
      {
         SetPcClock( tgps );
         logit( "et", "PC clock synched to GPS time.\n" );
      }

/* Fill in tracebuf start and end times, assuming first sample
   of tracebuf message occurred at time of previous trigger
   pulse from GPS.  Then, fill in tracebuf header with other
   stuff that's the same for all channels.
   ***********************************************************/
   traceHead->starttime  = tgps - 1.0;
   traceHead->endtime    = traceHead->starttime + (nScansRead - 1)/(double)nScansRead;
   traceHead->nsamp      = nScansRead;          // Number of samples in tbuf msg
   traceHead->samprate   = (double)nScansRead;  // Sample rate
   traceHead->version[0] = TRACE2_VERSION0;     // Header version number
   traceHead->version[1] = TRACE2_VERSION1;     // Header version number
   traceHead->quality[0] = 0;                   // Not used
   traceHead->quality[1] = 0;                   // Not used
   strcpy( traceHead->datatype, "i2" );         // Data format code (short integer)

/* Send one tracebuf packet per channel to the EW transport ring
   *************************************************************/
   for ( i = 0; i < Nchan; i++ )
   {
      int rc;
      int index = i;
      int j;

/* Put the SCNL and pin number into the traceBuf header
   ****************************************************/
      strcpy( traceHead->sta,  ChanList[i].sta );  // Site name
      strcpy( traceHead->net,  ChanList[i].net );  // Network name
      strcpy( traceHead->chan, ChanList[i].comp ); // Component code
      strcpy( traceHead->loc,  ChanList[i].loc );  // Location code
      traceHead->pinno = ChanList[i].pin;          // Pin number

/* Demux data samples and copy to the traceBuf message
   ***************************************************/
      for ( j = 0; j < nScansRead; j++ )
      {
         traceDat[j] = idata[index];
         index += Nchan;
      }

/* Send tracebuf message to transport ring
   ***************************************/
      rc = tport_putmsg( &Region, &tbufLogo, traceBufSize, traceBuf );
      if ( rc == PUT_TOOBIG )
         logit( "", "Tracebuf message for channel %d too big\n", i );
      if ( rc == PUT_NOTRACK )
         logit( "", "Tracking error while sending channel %d\n", i );
   }
   return 0;
}


/**************************************************************
  DoneCallback

  This function is called if an error causes the analog task
  to stop.
 **************************************************************/

int32 CVICALLBACK DoneCallback( TaskHandle taskHandle,
                                int32      status,
                                void       *callbackData )
{
   char errBuff[2048] = {'\0'};

   if ( DAQmxFailed( status ) )
   {
      DAQmxGetExtendedErrorInfo( errBuff, 2048 );
      DAQmxClearTask( taskHandle );
      logit( "", "Analog input task stopped due to error: %s\n", errBuff );
   }
   callbackExitFlag = 1;       // Tell the main program to exit
   return 0;
}


/*******************************************************************
  FloatToShort

  NI X-series devices collect data in floating-point volts.
  Here we convert these values to short integers which emulate
  output from a PCI-6040E DAQ board.
  For a PCI-6040E with gain setting of 2, the input range is +-2.5
  volts, and the digital samples are set to 819.0 * volts.
  The minimum sample value is -2048 at -2.5 volts input.
  The maximum sample value is 2047 at +2.5 volts input.
  Sample counts are clipped if the absolute value of the input
  voltage is > 2.5.
 *******************************************************************/
#define ROUND(x) (((x)<0.0) ? ((x)-0.5) : ((x)+0.5))

void FloatToShort( int Nchan, uInt32 nSamples, short *idata )
{
   unsigned i;

   for ( i = 0; i < Nchan*nSamples; i++ )
   {
      if ( fldata[i] < -2.5 )
         idata[i] = -2048;
      else if ( fldata[i] >= +2.5 )
         idata[i] = +2047;
      else
         idata[i] = (short) ROUND(819.0 * fldata[i]);
   }
   return;
}
