unit uCom;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, AdProtcl, AdPStat, OoMisc, AdPort, StdCtrls, ExtCtrls, ComCtrls,
  Menus, FileCtrl, Psock, NMFtp, Buttons, INIFiles;

type
  tDirEntry = record
    // Directory Result from Reciever
    Index: integer;
    Transfered: integer;
    Filename: string[15];
    LastRecording: tDateTime;
    Filesize: integer;
    Fileopen: integer;
    FileAge: extended; // number of seconds file has been closed
  end;
  tDirEntries = array of tDirEntry;
  TfMain = class(TForm)
    LogMemo: TMemo;
    Com: TApdComPort;
    ApdProtocolStatus1: TApdProtocolStatus;
    ZMODEM: TApdProtocol;
    Status: TStatusBar;
    MainMenu: TMainMenu;
    File1: TMenuItem;
    Exit1: TMenuItem;
    ReprocessRawFile1: TMenuItem;
    ReprocessDirectory1: TMenuItem;
    Options1: TMenuItem;
    Settings1: TMenuItem;
    ConfigureReceiver1: TMenuItem;
    NMFTP1: TNMFTP;
    Special1: TMenuItem;
    bQUIT: TBitBtn;
    Timer1: TTimer;
    cbTimerOK: TCheckBox;
    Force38400baud1: TMenuItem;
    procedure Delay( f: extended );
    procedure FormShow(Sender: TObject);
    procedure ZMODEMProtocolError(CP: TObject; ErrorCode: Integer);
    procedure bQUITClick(Sender: TObject);
    procedure Exit1Click(Sender: TObject);
    procedure ReprocessRawFile1Click(Sender: TObject);
    procedure Settings1Click(Sender: TObject);
    procedure ProcessFilesInFolder( P: string );
    procedure ReprocessDirectory1Click(Sender: TObject);
    procedure Special1Click(Sender: TObject);
    procedure ComTriggerAvail(CP: TObject; Count: Word);
    procedure FormDestroy(Sender: TObject);
    procedure ProcessNewFilesInGPS(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure ConfigureReceiver1Click(Sender: TObject);
    procedure Force38400baud1Click(Sender: TObject);
  private
    { Private declarations }
    InBuf: string;
    Busy: boolean;
    LastTimer: tDateTime;
  public
    { Public declarations }
    procedure Log( S: string );
    procedure RotateLog;
    procedure InitChannel;
    procedure Send( S: string );
    function  GetW( var RStr:   string      // returned packet
                            ; W: string         // Wait until W string is recieved or timeout
                            ; ExpectAck: boolean  // Check for ACK and NAK if TRUE
              ): boolean;                                   // TRUE if OK
    function  Get( var RStr:   string ): boolean;
    function  ZStop: boolean;
    function  ZStart: boolean;
    function  OneCommand( CMD: string ): boolean;
    function  ReceiverThere( BaudRate: integer ): boolean;
    function  FindReceiverBaud: integer;
    function  GetRID( var RecType, ChannelOptn, NAVFW, RecOptn, BootFW: string  ): boolean;
    function  GetCurrentSerialPort( var P: char ): boolean;
    function  GetPow( var Cap, Remn, Pow: integer; var Volt: extended ): boolean;
    function  GetGPSTime( var GPST: tDateTime; var dT: extended ): boolean;
    function  ForceBaud( NewBaud: integer ): BOOLEAN;
    function  GetSessionList( SL: string ): boolean;
    function  NewSesson: boolean;
    function  SetSessions(   SessionLength: integer       // length in minutes of each session. Must be 15, 30, 60 or 120!
                               ; RecInt: extended             // recording interval in seconds. Suggested 5.0, 15.0, 30.0 or 60.0
                               ; ElevMask: integer            // minimum elevation mask in degrees. Suggested 10
                               ; SVs: integer                 // minimum number of SV's to record
                               ; Smoothing: boolean           // apply smoothing corrections to the data.
                             ): boolean;
    function GetZFileSystemStatus( var CurrentFile: integer;     // Free memory in KBytes
                                        var NumFiles: integer;    // number of files in receiver
                                        var MountPC: integer ): boolean;
    function WaitForZMemoryMount: boolean;
    function  _GetZDir(   var FreeMem: integer;     // Free memory in KBytes
                          var NumFiles: integer;    // number of files in receiver
                          var D: tDirEntries ): boolean;
    function  GetZDir( var FreeMem: integer;     // Free memory in KBytes
                       var NumFiles: integer;    // number of files in receiver
                       var D: tDirEntries ): boolean;
    function  GetZDirNS( var FreeMem: integer;     // Free memory in KBytes
                         var NumFiles: integer;    // number of files in receiver
                         var D: tDirEntries ): boolean;
    function  GetZDirNS2( var FreeMem: integer;     // Free memory in KBytes
                          var NumFiles: integer;    // number of files in receiver
                          var D: tDirEntries ): boolean;
    function  GetZFile( InFN, OutFN: string ): boolean;  // filename to get
    procedure ConvertUFile( FN: string );
    function  DeleteZFile( FN: string ): boolean;
    function  DeleteZFileByIndex( Idx: integer ): boolean;
  end;

var
  fMain: TfMain;

implementation

uses uUtility, uNMEA, uComUtil, uOptions, uTagLink, Version, uMonitors;

{$R *.dfm}

  const
    ACOL = 12;
    MaxLogLines = 250;
    LastHour: integer = -1;
    SecondsPerDay = 24 * 60 * 60;
    WantToQuit: boolean = FALSE;

  procedure TfMain.Log( S: string );
  // enter a note in the system log
  var
    F: System.Text;
    FN: string;
  begin
    if LogMemo.Lines.Count > MaxLogLines + 25 then
    begin
      LogMemo.Lines.BeginUpdate;
        while LogMemo.Lines.Count > MaxLogLines do LogMemo.Lines.Delete(0);
      LogMemo.Lines.EndUpdate;
    end;
    LogMemo.Lines.Append( S );
    LogMemo.SelStart := Length( LogMemo.Text ) - 5;
    LogMemo.SelLength := 1;
    // Log to text file
    try
      {$I+}
      FN := fOptions.GetEntry('Logfile');
      if NOT DirectoryExists(ExtractFilePath(FN)) then ForceDirectories(ExtractFilePath(FN));
      AssignFile( F, FN );
      try
        try
          Append( F );
        except
          ReWrite( F );
        end;
        WriteLn( F, S );
      finally
        CloseFile( F );
      end;
      {$I-}
    except
      LogMemo.Lines.Append(' ** LOG FILE '+FN+' may not be complete!');
    end;
  end;

  procedure TfMain.RotateLog;
  var
    FN,NFN: string;
    Ndt, Fdt: tDateTime;
    Y,M,D: word;
    i: integer;
  begin
    FN := fOptions.GetEntry('Logfile');
    Ndt := Now;
    Fdt := GetFileDateDT( FN );
    // If Fdt is a different day, then rotate
    if Trunc(Ndt) <> Trunc(Fdt) then
    begin
      DecodeDate( Fdt, Y, M, D );
      NFN := AddBackSlash( ExtractFilePath( FN ) )+Int2StrN(Y,4)+Int2StrN(M,2)+Int2StrN(D,2)+'.log';
      i := 1;
      while FileExists( NFN ) do
      begin
        Inc(i);
        NFN := ChangeFileExt( NFN, '.log'+IntToStr(i) );
      end;
      RenameFile( FN, NFN );
      Log('Rotated log file '+FN+' to '+NFN);
    end;
  end;

  procedure Stat( S: string );
  begin
    fMain.Status.SimpleText := S;
    Application.ProcessMessages;
  end;

  procedure TfMain.Delay( f: extended );
  var
    st: tDateTime;
    dt: extended;
    i: integer;
  begin
    st := Now;
    repeat
      dt := DeltaTimeSeconds(st);
      Stat('Delay '+ExtToStr(dt,1)+' more seconds.');
      for i:=1 to 50 do Application.ProcessMessages;
    until dt >= f;
  end;


  procedure TfMain.InitChannel;
  var
    i: integer;
  begin
    Com.FlushInBuffer;
    Application.ProcessMessages;
    Com.PutString( Char(0) );
    for i:=0 to 8 do
    begin
      Application.ProcessMessages;
      Com.PutString( Char(24) );
    end;
    Application.ProcessMessages;
    // Dump input buffer
    i := 0;
    while InBuf<>'' do
    begin
      Inc( i, Length( InBuf ) );
      InBuf := '';
      Application.ProcessMessages;
    end; // of while characters exist in the input que
    Log('Input Channel cleared of '+IntToStrComma(i)+' characters.');
  end;

  procedure TfMain.Send(
                            S: string
  );
  // Send the passed NMEA string. Leading $ and trailing Checksum and CRLF are
  // added.
  var
    OStr: string;
    CS: byte;
  begin
    Com.FlushInBuffer;
    InBuf := '';
    OStr := NMEA_SetChecksum( S, CS );
    Log(EPad('PC:',' ',ACol)+' '''+ACode(OStr)+'''' );
    Com.PutString( OStr );   // send
  end;

  procedure TfMain.ComTriggerAvail(CP: TObject; Count: Word);
  // Called when data is available for us to process
  // THIS SHOULD BE THE ONLY INTERFACE THAT GETS CHARACTERS FROM THE COM PORT
  var
    i: integer;
  begin
    for i:=1 to Count do InBuf := InBuf + COM.GetChar
  end;

  function TfMain.Get( var RStr: string ): boolean;
  begin
    result := GetW( RStr, '', TRUE );
  end;

  function TfMain.GetW( var RStr:   string      // returned packet
                            ; W: string         // Wait until W string is recieved or timeout
                            ; ExpectAck: boolean  // Check for ACK and NAK if TRUE
  ): boolean;                                   // TRUE if OK
  // Get a NMEA buffer from the remote device
  const
    TimeoutSeconds = 2.0;
  var
    Ch: char;                                   // Current character
    cs: byte;
    StartTime: tDateTime;                       // Time we started waiting
    Done, Timedout, CSOK: boolean;             // conditions that end wait
    Note: string;
  begin
    result    := FALSE;
    RStr      := '';                            // receive buffer
    StartTime := Now;                           // Mark the starting time
    Done      := FALSE;                         // if W='' we don't need to wait for a string
    Note      := '';
    repeat
      Application.ProcessMessages;              // Let everything else run
      // process input characters until the end of message
      while (NOT Done) AND (Length(InBuf)>0) do
      begin
        if Length(InBuf) > 0 then
        begin
          // Get the input character
          Ch := FirstChar(InBuf);
          Delete( InBuf, 1, 1 );
          RStr := RStr + Ch;
          // Check for complete transaction
          if (Ch=^J) then
          begin
            Done := Done OR (W='') OR (Pos( W, UpperCase(RStr) ) > 0);
            result := result OR Done;
          end;
        end;
      end; // of while characters exist in the input que
      // Check for timeout
      TimedOut := DeltaTimeSeconds(StartTime) > TimeoutSeconds;
    until result OR TimedOut;
    if TimedOut then Note := Note + 'TimedOut, ';
    if result then
    begin
      if ExpectAck then
      begin
        if FirstChar(RStr) = ^U then
        begin
          Delete( RStr, 1, 1 );
          Note := Note + 'NAK, ';
          result := FALSE;
        end;
        if FirstChar(RStr) = ^F then
        begin
          Delete( RStr, 1, 1 );
          Note := Note + 'ACK, ';
        end;
        CSOK := NMEA_TestChecksum( RStr, cs );
        if NOT CSOK then Note := Note + 'NAK/BadChecksum, ';
        result := result AND CSOK;
      end;
    end;
    if ExpectAck then
    begin
      Log(EPad('GPS: '+Note,' ', ACol)+' '''+ACode(RStr)+'''' );
    end;
  end;

  function TfMain.ZStop: boolean;
  var
    S: string;
  begin
    Send( 'PASHS,FIL,STOP' );
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR Unable to FIL,STOP');
    end;
  end;

  function TfMain.ZStart: boolean;
  var
    S: string;
  begin
    Send( 'PASHS,FIL,START' );
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR Unable to FIL,START');
    end;
  end;

  function TfMain.OneCommand( CMD: string ): boolean;
  var
    S: string;
  begin
    Send( CMD );
    result := Get( S );
  end;

  function TfMain.ReceiverThere( BaudRate: integer ): boolean;
  var
    S: string;
  begin
    result := FALSE;
    if WantToQuit then exit;
    Log('> BAUD Rate set to '+IntToStr(BaudRate));
    COM.Baud := BaudRate;
    Stat(' looking for receiver on COM'+IntToStr(Com.ComNumber)+' at '+IntToStrComma(BaudRate)+' baud.');
    InitChannel;
    S := '';
    Com.FlushInBuffer;
    Application.ProcessMessages;
    Send( 'PASHQ,PRT' );
    Result := Get( S ) AND (Pos('$PASHR,PRT,',S)>0);
  end;

  function TfMain.FindReceiverBaud: integer;
  const
    BaudRates: array[0..9] of integer = ( 9600, 19200, 57600, 115200, 38400, 300, 600, 1200, 2400, 4800 );
  var
    i: integer;
  begin
    // Try current baud rate
    result := COM.Baud;
    if ReceiverThere( result ) then exit;
    for i:= Low(BaudRates) to High(BaudRates) do
    begin
      if WantToQuit then Exit;
      if ReceiverThere( BaudRates[i] ) then
      begin
        result := BaudRates[i];
        Log('Found receiver on COM'+IntToStr(Com.ComNumber)+' at '+IntToStrComma(result)+' baud.');
        exit;
      end;
    end;
    result := 0;
    Log('ERROR! Unable to find reciever on COM'+IntToStr(Com.ComNumber));
  end;

  function TfMain.GetCurrentSerialPort( var P: char ): boolean;
  var
    tokens: tStrArray;
    S: string;
  begin
    Log('@ Get receiver port (PRT)');
    // $PASHQ,PRT...
    // $PASHR,PRT,prt,spd,0A01*7A
    Send( 'PASHQ,PRT' );
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR Unable to get receiver port (PRT).');
    end else
    begin
      NMEA_Parse( S, tokens );
      if High(tokens)>=3 then
      begin
        result := TRUE;
        P := FirstChar(tokens[2]);
        exit;
      end;
    end;
    P := ' ';
    result := FALSE;
  end;

  function TfMain.GetRID( var RecType, ChannelOptn, NAVFW, RecOptn, BootFW: string ): boolean;
  // $PASHR,RID,s1,d2,s3,s4,s5*cc <Enter>
  //   Return Parameter Description Range
  //   s1 Receiver type UZ
  //   d2 Channel option 3 (C/A, PL1, PL2)
  //   s3 NAV firmware version 4-character string
  //   s4 Receiver options Refer to Table 1.2 on page 7.
  //   s5 Boot version 4-character string
  var
    tokens: tStrArray;
    S: string;
  begin
    Log('@ Get receiver information (RID)');
    RecType := ''; ChannelOptn := ''; NavFW := ''; RecOptn := ''; BootFW := '';
    // $PASHQ,RID*28<CR><LF>
    // $PASHR,RID,ZA,30,MD00,BUE-MFT3JKIG--,0A01*7A
    Send( 'PASHQ,RID' );
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR Unable to get receiver ID (RID).');
    end else
    begin
      NMEA_Parse( S, tokens );
      if High(tokens)>=2 then RecType := tokens[2];
      if High(tokens)>=3 then ChannelOptn := tokens[3];
      if High(tokens)>=4 then NAVFW := tokens[4];
      if High(tokens)>=5 then RecOptn := tokens[5];
      if High(tokens)>=6 then BootFW := tokens[6];
      Log( ' RecType '''+RecType+'''; '+
           ' ChanOptn '''+ChannelOptn+
           '''; Navigation Firmware Version '''+NavFW+
           '''; Recivier Options '''+RecOptn+
           '''; Boot Firmware Version '''+BootFW+'''');
      if RecOptn <> '' then
      begin
        if Pos('B',RecOptn)>0 then Log('   B RTCM Base');
        if Pos('B',RecOptn)>0 then Log('   U RTCM Rover');
        if Pos('B',RecOptn)>0 then Log('   E Event Marker');
        if Pos('B',RecOptn)>0 then Log('   M GSM Modem');
        if Pos('B',RecOptn)>0 then Log('   F Fast Data Output');
        if Pos('B',RecOptn)>0 then Log('   T Point Positioning');
        if Pos('B',RecOptn)>0 then Log('   3 Observables');
        if Pos('B',RecOptn)>0 then Log('   J RTK Rover');
        if Pos('B',RecOptn)>0 then Log('   K RTK Base');
        if Pos('B',RecOptn)>0 then Log('   I Instant RTK');
        if Pos('B',RecOptn)>0 then Log('   G Long Range RTK');
        if Pos('B',RecOptn)>0 then Log('   H Synchronized 5 Hz RTK');
      end;
    end;
  end;

  function TfMain.GetGPSTime( var GPST: tDateTime; var dT: extended ): boolean;
  // $PASHS,REC,R*cc <Enter>
  var
    tokens: tStrArray;
    S: string;
    h,m,se,ms,yr,mn,da: word;
    nt: integer;
    nw: tDateTime;
  begin
    GPST := 0.0;  // default return value
    dt   := 0.0;
    result := FALSE; // assume failure
    Log('@ Get time and date from GPS');
    Send( 'PASHQ,ZDA' );
    nw     := Now;
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR Unable to get GPS Date/Time (ZDA).');
      exit;
    end;
    // Message:  HHMMSS.ss,dd,mm,yyyy,timezonehouroffset,timezoneminoffset
    try
      nt := NMEA_Parse( S, tokens );
      if nt >= 4 then
      begin
        h  := Str2Int( Copy(tokens[1],1,2) );
        m  := Str2Int( Copy(tokens[1],3,2) );
        se := Str2Int( Copy(tokens[1],5,2) );
        ms := Str2Int( Copy(tokens[1],8,2) );
        da := Str2Int( tokens[2] );
        mn := Str2Int( tokens[3] );
        yr := Str2Int( tokens[4] );
        GPST := EncodeDate( yr, mn, da ) + EncodeTime( h, m, se, ms );
        dt   := ( GPST - Nw ) ;
        result := TRUE;
        Log('    GPS Time: '+DateTimeToStr(GPST));
        Log('    CPU Time: '+DateTimeToStr(nw));
        Log('          dT: '+Ext2Str( dt * SecondsPerDay, 3 ) + '; ' + ElapsedTimeStr( Nw, GPST, True ) );
      end;
    except
    end;
  end;

  function TfMain.NewSesson: boolean;
  // $PASHS,REC,R*cc <Enter>
  var
    S: string;
  begin
    Log('@ Get File information');
    Send( 'PASHQ,FSS' );
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR Unable to get file information (FLS).');
      exit;
    end;

    Log('@ Close current recording session (FIL,C)');
    Send( 'PASHS,FIL,C' );
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR Unable to close current session (FIL,C).');
    end;
  end;

  function TfMain.GetSessionList( SL: string ): boolean;
  begin
    result := FALSE;
    Log('@ Get session list (SES)');
    Send( 'PASHQ,SES' );
    result := GetW( SL, 'TODAY:', FALSE );
    if NOT result then
    begin
      log('ERROR Get sessions (SES) failed, perhaps there are no sessions defined.');
    end else
    begin
      log('Currently Programmed Sessions'+^M^J+SL);
    end;
  end;

  function IntToSessionName( I: integer ): string;
  // returns a session identifier: A-X (sessions 1-24)
  //                               AA-XA (sessions 25 - 48)
  //                               AB-XB (sessions 49 - 72)
  //                               AC-XC (sessions 73 - 96)
  begin
    case i of
      1..24:  result := Char( Ord('A') + i - 1 );
      25..48: result := IntToSessionName( i-24 ) + 'A';
      49..72: result := IntToSessionName( i-48 ) + 'B';
      73..96: result := IntToSessionName( i-72 ) + 'C';
      else    result := '?';
    end;
  end;

  function FloatToRecInt( f: extended ): string;
  begin
    if f <= 0.9 then
      result := Ext2Str( f, 1 )
    else
      result := Ext2Str( Round( f ), 1 );
  end;

  function Sec2HMS( i: integer ): string;
  var
    r: integer;
  begin
    r := i MOD 60;
    result := Int2StrN( r, 2 );
    i := i DIV 60;
    r := i MOD 60;
    i := i DIV 60;
    result := Int2StrN( i, 2 ) + Int2StrN( r, 2 ) + result;
  end;

  function TfMain.SetSessions(   SessionLength: integer       // length in minutes of each session. Must be 15, 30, 60 or 120!
                               ; RecInt: extended             // recording interval in seconds. Suggested 5.0, 15.0, 30.0 or 60.0
                               ; ElevMask: integer            // minimum elevation mask in degrees. Suggested 10
                               ; SVs: integer                 // minimum number of SV's to record
                               ; Smoothing: boolean           // apply smoothing corrections to the data.
                             ): boolean;
  var
    i,n: integer;
    StartTime: integer;
    S, T, smooth: string;
  begin
    result := FALSE;
    Log( '@ Set receiver sessions. Called with '+IntToStr(SessionLength)+' minute sessions, '+
         Ext2Str( RecInt, 3 )+' second spacing, '+
         IntToStr( SVs )+' SVs minimum, '+
         IntToStr( ElevMask )+' degree elevation mask.');
    // Check session length
    if (SessionLength < 15 ) then
    begin
      Log('ERROR Session Length MUST be 15 minutes or longer!');
      exit;
    end;
    // SessionLength must result in an even number of sessions. For example 15 minutes resuls in 96
    // sessions, whilst 60 minute sessions results in 24 sessions.
    if (1440 MOD SessionLength) <> 0 then
    begin
      Log('ERROR Session Length MUST result in an even number of sesssions in 24 hours!');
      exit;
    end;
    if (RecInt < 0.1) then
    begin
      Log('ERROR Session recording interval must be 0.1 seconds or longer.');
      exit;
    end;
    if (RecInt > 0.6) and (RecInt < 0.8) then
    begin
      Log('ERROR Session recording interval may not be 0.7 seconds!');
      exit;
    end;
    // Delete any sessions
    T := 'PASHS,SES,DEL';
    Send( T );
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR Unable to clear existing sessions (SES,DEL), perhaps there are no sessions defined!');
    end;
    // Prepare smoothing settings
    if Smoothing then
      Smooth := '7,Y'
    else
      Smooth := '0';
    // make sure the elevation mask is reasonable
    iForceInRange( ElevMask, 0, 90 );
    N := 1440 DIV SessionLength;
    StartTime := 0;
    for i:=1 to N do
    begin
      T  :='PASHS,SES,SET,'+
            IntToSessionName(i)+',Y,'+                     // Session identifier A..X,AA,,,AX  ...
            Sec2HMS(StartTime)+','+                        // Start time HHMMSS
            Sec2HMS(StartTime+(SessionLength*60)-1)+','+   // End time HHMMSS
            FloatToRecInt(RecInt)+','+                     // Recording interval
            Int2Str(ElevMask)+','+                         // Elevation mask in degrees
            Int2Str(SVs)+','+                               // minimum number of SV's to record;
            Smooth;
      // T := 'PASHS,SES,SET,A,Y,010000,030000,10.0,10,3,0';
      //      $PASHS,SES,SET,A,Y,000000,010000,15,5,3,7,Y
      Send( T );
      result := Get( S );
      Inc( StartTime, SessionLength*60 );
      if NOT result then
      begin
        Log('ERROR while programming session '+IntToStr(i));
        exit;
      end;
    end;
    // Set master session parameters
    T := 'PASHS,SES,PAR,Y,1,0';
    Send( T );
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR while setting session parameters.');
      exit;
    end;
  end;

  function TfMain.GetZFileSystemStatus( var CurrentFile: integer; // Index of file currently being written to
                                        var NumFiles: integer;    // number of files in receiver
                                        var MountPC: integer ): boolean;
  // tDirEntry
  //     Site: string[4];
  //   LEpoch: string[9];
  //     Size: string[10];
  var
    T: tStrArray;
    nt: integer;
    S: string;
  begin
    result := FALSE;  // assume failure
    CurrentFile := -1;
    NumFiles    := -1;
    MountPC     := 0;
    try
      Send( 'PASHQ,FSS' );
      if Get( S ) then
      begin
        nt := NMEA_Parse( S, T );
        if nt >= 8 then
        begin
          CurrentFile := Str2Int( T[5] );
          NumFiles    := Str2Int( T[6] );
          MountPC     := Str2Int( T[7] );
          Log('GetZFileSystemStatus: Current File '+IntToStr(CurrentFile)+'; Number of files '+IntToStr(NumFiles)+'; Mount = '+IntToStr(MountPC)+'%');
          result := TRUE;
        end else
        begin
          Log('GetZFileSystemStatus: Error getting FSS, expected 6 tokens but only got '+IntToStr(nt) );
        end;
      end;
    except
      Log('GetZFileSystemStatus: Exception while retrieving directory.');
    end;
  end;

  function TfMain.WaitForZMemoryMount: boolean;
  // Wait for the memory card to fully mount
  var
    ErrCnt: integer;
    CurrentFile, NumFiles, MountPC: integer;
  begin
    result := FALSE;
    ErrCnt := 0;
    repeat
      result := GetZFileSystemStatus( CurrentFile, NumFiles, MountPC);
      Stat('Mount = '+IntToStr(MountPC)+'%');
      if NOT result then Inc( ErrCnt );
      if ErrCnt > 3 then
      begin
        Log('WaitForZMemoryMount: Too many errors while checking mount percentage.');
        exit;
      end;
      if WantToQuit then
      begin
        Log('ERROR User aborts WaitForZMemoryMount by pressing QUIT button.');
        exit;
      end;
    until result AND ( MountPC >= 100 );
    Stat('');
    Log('GetZFileSystemStatus: Z Memory card is 100% mounted.');
  end;

  function TfMain.GetZDir( var FreeMem: integer;     // Free memory in KBytes
                          var NumFiles: integer;    // number of files in receiver
                          var D: tDirEntries ): boolean;
  var
    Trys: integer;
  begin
    ZStop;
    try
      Trys := 0;
      repeat
        Inc( Trys );
        result := _GetZDir( FreeMem, NumFiles, D );
        if result then exit;
      until Trys > 2;
      result := FALSE; // must already be this way
    finally
      ZStart;
    end;
  end;

  function TfMain._GetZDir( var FreeMem: integer;     // Free memory in KBytes
                          var NumFiles: integer;    // number of files in receiver
                          var D: tDirEntries ): boolean;
  // tDirEntry
  //     Transfered: integer;
  //     Filename: string[15];
  //     LastRecording: tDateTime;
  //     Filesize: integer;
  //     Fileopen: integer;
  var
    H,nt: integer;
    Hour, Mint, Sec, MSec, Day, Month, Year: word;
    T: tStrArray;
    S: string;
    Complete: boolean;
    GPST: tdateTime;
    dT: extended; // offset in seconds from
  begin
    Log('@ Get directory from GPS receiver');
    result := FALSE;  // assume failure
    SetLength(D,0);   // Directory entries
    // We need the current GPS time so we can fill in the file age
    GetGPSTime( GPST, dT );
    try
      Complete := FALSE;
      Send( 'PASHQ,FIL,FIRST,*.*,10000' );
      repeat
        if Get( S ) then
        begin
          nt := NMEA_Parse( S, T );
          // $PASHR,FIL,FIRST,OK,1,UIG10T07.005,205914.00,05,01,07,0,756167*58(0D)(0A)
          //   OR
          // $PASHR,FIL,NEXT,OK,1,UIG10U07.005,215914.00,05,01,07,0,750751*00(0D)(0A)
          //   OR
          // $PASHR,FIL,NEXT,NOMATCH
          // where               transfered
          //                       filename
          //                                    time of last recording HHMMSS.msec
          //                                              dd,mm,yy,? (fileopen?)
          //                                                         filesize
          // $PASHR,FIL,FIRST,OK,1,UIG10T07.005,205914.00,05,01,07,0,756167*58(0D)(0A)
          //  0     1   2     3  4 5            6         7  8  9  10 11
          if nt >= 4 then
          begin
            if T[3]='NOMATCH' then Complete := TRUE
            else if T[3]='OK' then
            begin
              if nt >= 12 then
              begin
                SetLength( D, High(D)+2 );
                H := High( D );
                D[H].Transfered := Str2IntD( T[4], -1 );
                D[H].Filename   := T[5];
                Hour := Str2IntD( Copy( T[6], 1, 2 ), 0 );
                Mint := Str2IntD( Copy( T[6], 3, 2 ), 0 );
                Sec  := Str2IntD( Copy( T[6], 5, 2 ), 0 );
                MSec := Str2IntD( Copy( T[6], 8, 2 ), 0 );
                Day  := Str2IntD( T[7], 0 );
                Month:= Str2IntD( T[8], 0 );
                Year := Str2IntD( T[9], 0 ) + 2000;
                D[H].Index          := H;
                D[H].LastRecording  := EncodeTime( Hour, Mint, Sec, MSec ) +
                                       EncodeDate( Year, Month, Day );
                D[H].FileOpen       := Str2IntD( T[10], -1 );
                D[H].FileSize       := Str2IntD( T[11], -1 );
                D[H].FileAge        := (Now+dT-D[H].LastRecording) * SecondsPerday;
              end else
              begin
                Log( 'GetZDir: ERROR, directory entry contains too few tokens.');
                exit;
              end;
            end; // of 'OK'
          end else
          begin
            Log('GetZDir: ERROR, directory entry not understood.');
            exit;
          end;
        end else
        begin
          Log('GetZDir: ERROR timeout while waiting for directory entry from receiver.');
          exit;
        end;
        if WantToQuit then
        begin
          Log('ERROR User aborted directory listing by pressing QUIT button.');
          exit;
        end;
        if NOT Complete then Send( 'PASHQ,FIL,NEXT' );
      until Complete;
      Log( 'GetZDir: '+IntToStrComma(High(D)+1)+' entries; '+
           ' Free Memory: '+IntToStrComma( FreeMem ) );
      for h:=0 to High(D) do
      begin
        Log('  '+D[H].Filename+', '+
               Pad( IntToStrComma(D[H].Filesize)+' bytes, ', 20 )+
               Pad( DateTimeToStr(D[H].LastRecording)+', ', 24 )+
               Pad( _ElapsedTimeStr( D[H].FileAge / SecondsPerday, TRUE )+', ', 12 )
               // 'Open='+IntToStr(D[H].Fileopen)+', Transfered='+IntToStr(D[H].Transfered)
            );
      end;
      result := TRUE;
    except
      Log('GetZDir: Exception while retrieving directory.');
    end;
  end;

  function GPSTimeToDT( S: string ): tDateTime;
  // wwwwdhhmm to tDateTime
  var
    W, D, H, M: integer;
  const
    SecondsPerDay  = 24 * 60 * 60;
  begin
    result := 0.0;
    if Length( S ) <> 9 then exit;
    W := StrToInt( Copy( S, 1, 4 ) );
    D := StrToInt( Copy( S, 5, 1 ) );
    H := StrToInt( Copy( S, 6, 2 ) );
    M := StrToInt( Copy( S, 8, 2 ) );
    result := EncodeDate( 1980, 1, 6 ) +
              W * 7 +
              (D - 1) +
              (M/60.0 + H) / 24;
    // Account for rollover, assume GPS time is always within 512 weeks of current time
    while result < (Now - 512 * 7) do
    begin
      result := result + 1024 * 7;
    end;
  end;

  function GPSTimeToDT2( S: string ): tDateTime;
  // wwwwdhhmm to tDateTime
  var
    W, D, H, M: integer;
  const
    SecondsPerDay  = 24 * 60 * 60;
  begin
    result := 0.0;
    // 14142215900.00
    // 12345678901234
    if Length( S ) <> 14 then exit;
    W := StrToInt( Copy( S, 1, 4 ) );
    D := StrToInt( Copy( S, 5, 1 ) );
    H := StrToInt( Copy( S, 6, 2 ) );
    M := StrToInt( Copy( S, 8, 2 ) );
    result := EncodeDate( 1980, 1, 6 ) +
              W * 7 +
              (D - 1) +
              (M/60.0 + H) / 24;
    // Account for rollover, assume GPS time is always within 512 weeks of current time
    while result < (Now - 512 * 7) do
    begin
      result := result + 1024 * 7;
    end;
  end;


  function SiteTime2FN( Site: string; LastRecording: tDateTime ): string;
  var
    H, M, S, ms, D, Mn, Y: word;
  begin
    //  UIG10T07.005
    DecodeTime( LastRecording, H, M, S, ms );
    DecodeDate( LastRecording, Y, Mn, D );
    result := 'U' +                      // raw file
              EPad( Site, '_', 4 ) +     // base name
              Char( Ord('A') + H ) +     // A .. X
              Copy( IntToStr( Y ), 3, 2 ) +
              '.' +
              Int2StrN( Trunc( LastRecording ) - Trunc( EncodeDate( Y, 1, 1 )) + 1, 3 );
  end;


  function TfMain.GetZDirNS( var FreeMem: integer;     // Free memory in KBytes
                          var NumFiles: integer;    // number of files in receiver
                          var D: tDirEntries ): boolean;
  // tDirEntry
  //     Transfered: integer;
  //     Filename: string[15];
  //     LastRecording: tDateTime;
  //     Filesize: integer;
  //     Fileopen: integer;
  var
    Fnum, H,nt: integer;
    MaxFile, Fcnt, i, j: integer;
    T: tStrArray;
    S: string;
    GPST: tdateTime;
    dT: extended; // offset in seconds from
  begin
    Log('@ Get directory from GPS receiver without closing current file');
    result := FALSE;  // assume failure
    SetLength(D,0);   // Directory entries
    // We need the current GPS time so we can fill in the file age
    GetGPSTime( GPST, dT );
    try
      Fnum    := 0;
      MaxFile := 0;
      FCnt    := 0;
      repeat
        Send( 'PASHQ,FLS,'+IntToStr(Fnum) );
        if Get( S ) then
        begin
          nt := NMEA_Parse( S, T );
          // PASHR,FLS,d1,d2,d3,n(s4,m5,d6)*cc<ENTER>
          // d1=free memory in kbytes
          // d2=total  number of files
          // d3=number of files in this responce
          // s4=4 character stite name
          // m5=wwwwdhhmm  (week, day (1-7), hhmm hours and minutes
          // d6=size of file in KBytes
          if nt >= 4 then
          begin
            FreeMem := Str2Int( T[2] ) * 1024;  // Free memory in receiver
            MaxFile := Str2Int( T[3] );  // total number of files
            FCnt    := Str2Int( T[4] );  // files in this message
            if nt >= (5 + FCnt * 3) then
            begin
              j       := 5;
              for i:=1 to FCnt do
              begin
                SetLength( D, High(D)+2 );
                H := High( D );
                D[H].Index          := H;
                D[H].LastRecording := GPSTimeToDT( T[J+1] );
                D[H].Filename      := SiteTime2FN( T[J], D[H].LastRecording );
                D[H].Filesize      := Str2Int( T[J+2] ) * 1024;
                D[H].FileAge       := (Now+dT-D[H].LastRecording) * SecondsPerday;
                Inc( j, 3 );
              end;
            end;
          end else
          begin
            Log('GetZDirNS: ERROR, directory entry not understood.');
            exit;
          end;
        end else
        begin
          Log('GetZDirNS: ERROR, unexpected reply to directory entry request.');
          exit;
        end;
        if WantToQuit then
        begin
          Log('ERROR User aborted directory (GetZDirNS) listing by pressing QUIT button.');
          exit;
        end;
        Inc( Fnum, FCnt );
      until FNum >= MaxFile;
      Log( 'GetZDirNS: '+IntToStrComma(High(D)+1)+' entries; '+
           ' Free Memory: '+IntToStrComma( FreeMem ) );
      for h:=0 to High(D) do
      begin
        Log('  '+
               Int2StrN( D[H].Index, 3 ) + '  ' +
               D[H].Filename+', '+
               Pad( IntToStrComma(D[H].Filesize)+' bytes, ', 20 )+
               Pad( DateTimeToStr(D[H].LastRecording)+', ', 24 )+
               Pad( _ElapsedTimeStr( D[H].FileAge / SecondsPerday, TRUE )+', ', 12 )
               // 'Open='+IntToStr(D[H].Fileopen)+', Transfered='+IntToStr(D[H].Transfered)
            );
      end;
      result := TRUE;
    except
      Log('GetZDir: Exception while retrieving directory.');
    end;
  end;

  function TfMain.GetZDirNS2( var FreeMem: integer;     // Free memory in KBytes
                              var NumFiles: integer;    // number of files in receiver
                              var D: tDirEntries ): boolean;
  // tDirEntry
  //     Transfered: integer;
  //     Filename: string[15];
  //     LastRecording: tDateTime;
  //     Filesize: integer;
  //     Fileopen: integer;
  var
    Fnum, H,nt: integer;
    Fcnt, i, j,Cnt: integer;
    T: tStrArray;
    S: string;
    Complete: boolean;
    GPST: tdateTime;
    dT: extended; // offset in seconds from
  begin
    Log('@ Get directory from GPS receiver without closing current file');
    result := FALSE;  // assume failure
    SetLength(D,0);   // Directory entries
    // We need the current GPS time so we can fill in the file age
    Cnt := 0;
    repeat
      Inc( Cnt );
      result := GetGPSTime( GPST, dT );
    until result or (Cnt > 3);
    try
      Complete := FALSE;
      Fnum := 0;
      Fcnt := 0;
      NumFiles := 0;
      repeat
        Send( 'PASHQ,FIF,'+IntToStr(Fnum) );
        if Get( S ) then
        begin
          nt := NMEA_Parse( S, T );
          // PASHR,FLS,d1,d2,d3,n(s4,m5,d6)*cc<ENTER>
          // d1=free memory in kbytes
          // d2=total  number of files
          // d3=number of files in this responce
          // s4=4 character stite name
          // m5=wwwwdhhmm  (week, day (1-7), hhmm hours and minutes
          // d6=size of file in KBytes
          if nt >= 4 then
          begin
            FreeMem  := Str2Int( T[2] ) * 1024;  // Free memory in receiver
            NumFiles := Str2Int( T[3] );  // total number of files
            FCnt     := Str2Int( T[4] );  // files in this message
            if nt >= (5 + FCnt * 4) then
            begin
              j       := 5;
              for i:=1 to FCnt do
              begin
                SetLength( D, High(D)+2 );
                H := High( D );
                D[H].Index         := Str2Int( T[J] );
                D[H].Filename      := T[J+1];
                D[H].LastRecording := GPSTimeToDT2( T[J+3] );
                D[H].Filesize      := Str2Int( T[J+4] ) * 1024;
                D[H].FileAge       := (Now+dT-D[H].LastRecording) * SecondsPerday;
                Inc( j, 6 );
              end;
            end;
          end else
          begin
            Log('GetZDirNS: ERROR, directory entry not understood.');
            exit;
          end;
        end else
        begin
          Log('GetZDirNS: ERROR, unexpected reply to directory entry request.');
          exit;
        end;
        if WantToQuit then
        begin
          Log('ERROR User aborted directory (GetZDirNS) listing by pressing QUIT button.');
          exit;
        end;
        Inc( Fnum, FCnt );
      until FNum >= NumFiles;
      Log( 'GetZDirNS: '+IntToStrComma(High(D)+1)+' entries; '+
           ' Free Memory: '+IntToStrComma( FreeMem ) );
      for h:=0 to High(D) do
      begin
        Log('  '+
               Int2StrN( D[H].Index, 3 ) + '  ' +
               D[H].Filename+', '+
               Pad( IntToStrComma(D[H].Filesize)+' bytes, ', 20 )+
               Pad( DateTimeToStr(D[H].LastRecording)+', ', 24 )+
               Pad( _ElapsedTimeStr( D[H].FileAge / SecondsPerday, TRUE )+', ', 12 )
               // 'Open='+IntToStr(D[H].Fileopen)+', Transfered='+IntToStr(D[H].Transfered)
            );
      end;
      result := TRUE;
    except
      Log('GetZDir: Exception while retrieving directory.');
    end;
  end;

  function TfMain.DeleteZFile( FN: string ): boolean;
  var
    S: string;
    i, FreeMem, NumFiles, CurFile, MountPC: integer;
    D: tDirEntries;
  begin
    result := FALSE;
    //Stat('Wait 5 seconds before attempting to delete file.');
    //Delay( 10.0 );
    Stat('Get ZFile status...');
    if NOT GetZFileSystemStatus( CurFile, NumFiles, MountPC ) then
    begin
      Log('Unable to get system file status, unable to delete file '+FN);
      exit;
    end;
    Stat('Get directory listing...');
    if NOT GetZDirNS2( FreeMem, NumFiles, D ) then
    begin
      Log('Failed to get directory from GPS, unable to delete file '+FN);
      exit;
    end;
    // Look for the passed file
    i := 0;
    while i <= High(D) do
    begin
      if (UpperCase(FN)=UpperCase(D[i].Filename)) then
      begin
        if i = CurFile then
        begin
          Log('Can not delete the current file '+FN);
          exit;
        end;
        // $PASHS,FIL,D,1*0B(0D)(0A)
        Send( 'PASHS,FIL,D,'+IntTostr(I) );
        result := Get( S ) AND (Pos('BUSY',S)<1);
        if result then
          Log('Deleted GPS file number '+IntToStr(I))
        else
          Log('Delete failed on GPS File number '+IntToStr(I));
        Stat('');
        exit;
      end;
    end;
    Log('Unable to find a matching entry for '+FN);
  end;

  function TfMain.DeleteZFileByIndex( Idx: integer ): boolean;
  var
    S: string;
  begin
    // $PASHS,FIL,D,1*0B(0D)(0A)
    // Stat('Wait 5 seconds before attempting to delete file.');
    // Delay( 5.0 );
    Send( 'PASHS,FIL,D,'+IntTostr(Idx) );
    result := Get( S ) AND (Pos('BUSY',S)<1);
    if result then
      Log('Deleted GPS file number '+IntToStr(Idx))
    else
      Log('Delete failed on GPS File number '+IntToStr(Idx));
  end;

  function TfMain.GetZFile( InFN, OutFN: string ): boolean;  // filename to get
  const
    TimeoutSeconds = (60.0);
  var
    S: string;
    TimedOut: boolean;
    StartTime, HangTime: tDateTime;
    dT, hT: extended;
    LastToGo: integer;
    ready: boolean;
  begin
    result := FALSE;
    ZMODEM.DestinationDirectory := ExtractFilePath( OutFN );
    ZModem.FileName             := ExtractFileName( OutFN );
    ZStop;
    try
      // PASHS,FIL,DOWNLOAD,<file name>,ZMODEM
      try
        StartTime := Now;
        repeat
          Send( 'PASHS,FIL,DOWNLOAD,'+InFN+',ZMODEM' );
          Ready    := Get( S ) AND (Pos('BUSY',S)<1);
          if Ready then
          begin
            TimedOut := FALSE;
          end else
          begin
            Stat('Delay 5 seconds for GPS receiver...');
            Delay( 5.0 );
            dT       := DeltaTimeSeconds(StartTime);
            TimedOut := dT > 15.0;  // Wait upto 30 seconds for reciever to indicate ready
          end;

          // Bail if user requeste
          if WantToQuit then exit;

        until (NOT TimedOut) AND Ready;

        if Ready then
        begin
          ApdProtocolStatus1.Visible := TRUE;
          try
            ZMODEM.StartReceive;
            StartTime := Now;
            HangTime  := Now;
            LastToGo  := -1;
            repeat
              Application.ProcessMessages;
              if ZMODEM.BytesRemaining <> LastToGo then
              begin
                HangTime := Now;
                LastToGo := ZMODEM.BytesRemaining;
              end;
              dt       := DeltaTimeSeconds(StartTime);
              hT       := DeltaTimeSeconds(HangTime);
              TimedOut := hT > TimeoutSeconds;
              Stat( 'Transfering '+InFN+', '+Ext2Str( dt, 1 )+' seconds, '+
                    'InProgress '+BoolToYN(ZMODEM.InProgress)+', '+
                    'Bytes Remaining '+IntToStrComma(ZMODEM.BytesRemaining)+', '+
                    'Errors '+IntToStrComma(ZMODEM.TotalErrors)+', '+
                    'Hang Timer '+Ext2Str( hT, 1 )+' seconds'
              );
              if WantToQuit then
              begin
                Log('ERROR User aborted file download by pressing QUIT button!');
                ZMODEM.CancelProtocol;
                result := FALSE;
                exit;
              end;
            until (NOT ZMODEM.InProgress) OR TimedOut;

            result := (NOT TimedOut) AND FileExists( OutFN );
            if result then
            begin
              Log( ZMODEM.FileName +' downloaded.' );
            end else
            begin
              Log('ERROR Timeout while downloading '+ZMODEM.Filename );
            end;
          finally
            ApdProtocolStatus1.Visible := FALSE;
          end;
        end;
      except
        Log('GetZDir: Exception while retrieving directory.');
      end;
    finally
      ZStart;
    end;
  end;

  function FN2Path( FN: string ): string;
  // NGS: /cors/rinex/2007/033/arfy/
  // 123456789012
  // UCOLVB07.037 -> /2007/037/colv/
  begin
    result := '';
    if Length(FN)<>12 then exit;
    result := '20'+FN[7]+FN[8]+'\'+FN[10]+FN[11]+FN[12]+'\'+FN[2]+FN[3]+FN[4]+FN[5]+'\';
  end;

  function AshFN2OBS( FN: string ): string;
  //           123456789012
  // ASHTECH:  UIG10E07.045
  // RINEX data    ig10045e.07o
  begin
    result := '';
    if Length(FN)<>12 then exit;
    FN := LowerCase( FN );
    result := Copy(FN,2,4) + Copy( FN, 10, 3 ) + FN[6] + '.' + FN[7] + FN[8] + 'o';
  end;

  function AshFN2NAV( FN: string ): string;
  //           123456789012
  // ASHTECH:  UIG10E07.045
  // RINEX data    ig10045e.07n
  begin
    result := '';
    if Length(FN)<>12 then exit;
    FN := LowerCase( FN );
    result := Copy(FN,2,4) + Copy( FN, 10, 3 ) + FN[6] + '.' + FN[7] + FN[8] + 'n';
  end;


  function TfMain.GetPow( var Cap, Remn, Pow: integer; var Volt: extended ): boolean;
  var
    tokens: tStrArray;
    S: string;
    nt: integer;
  begin
    Log('@ Get battery information (POW)');
    Cap := -1; Remn := -1; Pow := -1; Volt := 0.0;
    Send( 'PASHQ,POW' );
    result := Get( S );
    if NOT result then
    begin
      Log('ERROR Unable to get battery power (POW).');
    end else
    begin
      nt := NMEA_Parse( S, tokens );
      if nt>=6 then
      begin
        Cap := Str2IntD( tokens[2], -1 );
        Remn := Str2IntD( tokens[3], -1 );
        Pow := Str2IntD( tokens[4], -1 );
        Volt := Str2FloatD( tokens[5], 0.0 );
        Log(' Total Battery Capacity '+IntToStrComma(Cap)+ ' minutes, '+
              'Remaining '+IntToStrComma(Remn)+' minutes, '+
              'Power '+IntToStrComma(Pow)+' mAH, '+
              'Voltage '+ExtToStr( Volt, 1 )+' Volts.');
      end else
      begin
        Log('ERROR: GetPow wrong number of returned values');
      end;
    end;
  end;




procedure TfMain.ZMODEMProtocolError(CP: TObject; ErrorCode: Integer);
begin
  Log(' ZMODEM Protocol Error '+IntToStr(ErrorCode));
end;

procedure TfMain.FormShow(Sender: TObject);
var
  INIFile: tINIFile;
begin
  // Get screen position
  INIFile := tINIFile.Create( ChangeFileExt( Application.EXEName, '.ini' ));
  try
    Left   := INIFile.ReadInteger( 'FORM', 'Left', Left );
    Top    := INIFile.ReadInteger( 'FORM', 'Top', Top );
    Width  := INIFile.ReadInteger( 'FORM', 'Width', Width );
    Height := INIFile.ReadInteger( 'FORM', 'Height', Height );
  finally
    INIFile.Free;
  end;
  InsureOnScreen( Self );
  // Clear the grid values
  fOptions.ClearEntries;
  fOptions.AddEntry( '[BASE INFORMATION]', '', oesSeparator, '', '' );
  fOptions.AddEntry( 'Station Name', 'IG10', oesSimple, '',
                     'Name of base station. Four characters A..Z, 0..9, _.' );
  fOptions.AddEntry( 'Data Recording Interval', '5', oesSimple, '',
                     'Data Recording Interval in Seconds.' );
  fOptions.AddEntry( 'Data Elevation Mask', '10', oesSimple, '',
                     'Elevation mask for data recording.' );
  fOptions.AddEntry( 'RTK Name', '0010', oesSimple, '',
                     'Base Identifier for RTCM, integer in range 0000..1023.' );
  fOptions.AddEntry( 'RTCM Elevation Mask', '9', oesSimple, '',
                     'Elevation mask for RTCM corrections.' );
  fOptions.AddEntry( 'ARP', '4044.1512213,N,11149.4259863,W,1425.290', oesLatLon, '',
                     'Position of base station ARP DD.MMmmmmmmm,N,DDD.MMmmmmmmmmm,W' );
  fOptions.AddEntry( 'HI', '0.3622', oesSimple,'',
                     'Antenna offset from ARP in meters. (ZMax with REC = 0.3622M' );
  fOptions.AddEntry( 'MessageType', '18&19', oesPickList, 'RTCM_18,19;RTCM_20,21',
                     'Message type to output on PORT B of the receiver.' );
  fOptions.AddEntry( 'Antenna Number', '00000000', oesSimple, '',
                     'The Antenna Number to include in OBS files.');
  fOptions.AddEntry( 'Antenna Type', 'THA800961+REC', oesSimple, '',
                     'The Antenna type to include in OBS files.');
  fOptions.AddEntry( 'Operating Agency', '', oesSimple, '',
                     'The name of the base operator to include in OBS files.');
  fOptions.AddEntry( 'OBS Comment', '', oesSimple, '',
                     'Optional comment to include in OBS files.');
  fOptions.AddEntry( 'Receiver Serial Number', '', oesSimple, '',
                     'GPS receiver serial number to include in OBS files.');

  fOptions.AddEntry( '[INTERFACE]', '', oesSeparator, '', '' );
  fOptions.AddEntry( 'COM Port', '1', oesPickList, '1;2;3;4;5;6;7;8',
                     'Enter the PC COM port number that is connected to the control port (A) on the GPS receiver' );
  fOptions.AddEntry( 'Baud Rate', '115200', oesPickList, '300;600;1,200;2,400;4,800;9,600;19,200;38,400;57,600;115,200',
                     'Enter the Baud Rate of the GPS COM port. If the rate is incorrect the program will search for a GPS at the possible rates' );
  fOptions.AddEntry( 'RTCM Port', 'B', oesPickList, 'A;B',
                     'Which port should broadcast RTCM?' );
  fOptions.AddEntry( 'Control Port', 'A', oesPickList, 'A;B',
                     'Which port is ZBase connected to?' );

  fOptions.AddEntry( '[DIRECTORIES]', '', oesSeparator, '', '' );
  fOptions.AddEntry( 'Temp Folder', 'C:\ZBase\temp', oesFolder, '',
                     'Enter the folder where the program may store temp files.' );
  fOptions.AddEntry( 'Logfile', 'C:\ZBase\log\ZBase.log', oesFilename, 'All files (*.*)|*.*',
                     'Enter the name and path of the file to store log entries to.' );
  fOptions.AddEntry( 'Raw Folder', 'C:\ftproot\native\', oesFolder, '',
                     'Enter the folder where raw data files are placed.' );
  fOptions.AddEntry( 'Raw Backup Folder', 'C:\ftproot_backup\native\', oesFolder, '',
                     'Enter the folder where backup raw data files are placed.' );
  fOptions.AddEntry( 'RINEX Folder', 'C:\ftproot\rinex\', oesFolder, '',
                     'Enter the folder where RINEX data files are placed.' );

  fOptions.AddEntry( '[TEQC CONFIGURATION]', '', oesSeparator, '', '' );
  fOptions.AddEntry( 'TEQC Process Command',
                     '-O.r "%rb%" '+
                     '-O.mo "%mn%" '+
                     '-O.an "%an%" '+
                     '-O.at "%at%" '+
                     '-O.ag "%ag%" '+
                     '-O.c "%cmt%" '+
                     '-O.rn "%rsn%" '+
                     '-O.obs "L1L2C1P1P2D1D2S1S2" '+
                     '+err %lf% '+
                     // '+out %lf% '+
                     '+nav %nf% '+
                     '+obs %of% '+
                     '-ash u ', oesSimple, '',
                     'The TEQC command to convert U file to NAV and OBS. %if% %of% %nf% are filename tokens' );
  Log('----- ZBase starting '+DateTimeToStr(Now)+' -------------------------------------------------');
end;

procedure TfMain.bQUITClick(Sender: TObject);
begin
  cbTimerOK.Checked := FALSE;
  WantToQuit := TRUE;
  Log('!!! Attempt to QUIT !!! User pressed ''QUIT NOW!'' button!!!');
end;

function FileExistsClosed( FN: string;             // filename to check
                           maxwait: extended;      // maximum time to wait
                           MinSize: integer ): boolean;  // minimum filesize to allow
// wait up to maxwait for FN to exist, have a filesize other than 0 and have a filedatetime
// that is 1/5 of a second old.
var
  StartTime: tDateTime;
  dT, FileAge: extended;
begin
  try
    StartTime := Now;
    result := TRUE; // assume success!
    repeat
      if WantToQuit then exit;
      if FileExists( FN ) then
      begin
        if (uFileSize( FN ) >= MinSize) then
        begin
          FileAge := DeltaTimeSeconds( GetFileDateDT( FN ) );
          if FileAge > 0.2 then exit;
        end;
      end;
      Delay( 0.2 );
      dT := DeltaTimeSeconds( StartTime );
      Stat('Waiting for '+FN+' to close: '+ExtToStrComma(dT,1)+'/'+ExtToStrComma(maxwait,1)+' seconds.');
    until dT > MaxWait;
    result := FALSE;
  finally
    Stat('');
  end;
end;

function ConvertTEQCCommand2File( CMD, FN: string ): boolean;
// You can pass a long string full of TEQC commands to this method and it will write them
// out to the file FN. Each command will be on a separate line which makes
// reading the commands easier for humans.
//  This  process is slightly complicated by the presence of quote " delimeted commands that
// can not be broken up and the existance of commands that begin with -- or ++ or perhaps -+.
var
  T: string;
  cf: System.Text;
  i: integer;
begin
  result := FALSE;
  // Now write out config file
  System.AssignFile( cf, FN );
  System.Rewrite( cf );
  try
    i := 1;
    T := '';
    while i <= Length(CMD) do
    begin
      case CMD[i] of
        '-', '+':    begin
                       if (T<>'') AND
                          (NOT (LastChar(T) IN['-','+'])) then
                       begin
                         WriteLn( cf, T );
                         T := '';
                       end;
                       T := T + CMD[i];
                     end;
        '"':         begin
                       T := T + CMD[i];
                       Inc( i );
                       while (i <= Length( CMD )) AND (CMD[i] <> '"') do
                       begin
                         T := T + CMD[i];
                         Inc( i );
                       end;
                       if (i <= Length( CMD )) then
                       begin
                         T := T + CMD[i];
                       end;
                     end;
        else         T := T + CMD[i];
      end;
      Inc( i );
    end;
    WriteLn( cf, T );
  finally
    System.CloseFile( cf );
    Delay( 0.2 );
  end;
  result := TRUE;
end;

procedure TfMain.ConvertUFile( FN: string );
var
  StartTime: tDateTime;
  cfn, rdir, navF, obsF, logF, TEQCFN, CMD, TempDir, GZFN: string;
const
  zipoptn = '--best --force';
begin
  StartTime := Now;

  Log('[');
  Log('CONVERT U-file: '+FN+' to RINEX.Z' );

  // Make sure we have a temp folder to work in
  Stat('Make TempDir...');
  TempDir := AddBackSlash( fOptions.GetEntry('Temp Folder') );
  // Make Sure TempDir exists
  if NOT DirectoryExists( ExtractFilePath( TempDir ) ) then
  begin
    Log(' @ Make temp folder '+TempDir );
    ForceDirectories( ExtractFilePath( TempDir ) );
  end;

  // Delete all the files in TempDir
  Stat('Clear TempDir...');
  DeleteAll( TempDir, '*.*' );

  // Make sure the output folder exists
  Stat('Make output folder');
  rdir := AddBackSlash( fOptions.GetEntry('RINEX Folder') ) + FN2Path( ExtractFilename( FN ) );
  rdir := LowerCase( rdir );
  // Make sure the output folder exists
  if NOT DirectoryExists( ExtractFilePath( rdir ) ) then
  begin
    Log(' @ Make output folder '+rdir);
    ForceDirectories( ExtractFilePath( rdir ) );
  end;

  navF := rdir + AshFN2NAV( ExtractFilename( FN ) );
  obsF := rdir + AshFN2OBS( ExtractFilename( FN ) );
  logF := ChangeFileExt( navF, '.log' );
  if FileExists( logF ) then DeleteFile( logF );

  // Use TEQC to generate observation and navigation files
  Stat(']] Convert U file to obs and nav files: '+FN);
  TEQCFN    := _EXEDir + 'teqc.exe';
  cfn       := TempDir + 'tcfg.txt';

  // Load up CMD string
  CMD       := fOptions.GetEntry('TEQC Process Command');
  CMD       := StrReplaceNC( CMD, '%of%', obsF );
  CMD       := StrReplaceNC( CMD, '%nf%', navF );
  CMD       := StrReplaceNC( CMD, '%lf%', logF );

  // Receiver, Agency Information
  CMD       := StrReplaceNC( CMD, '%an%', fOptions.GetEntry('Antenna Number') );
  CMD       := StrReplaceNC( CMD, '%at%', fOptions.GetEntry('Antenna Type') );
  CMD       := StrReplaceNC( CMD, '%ag%', fOptions.GetEntry('Operating Agency') );
  CMD       := StrReplaceNC( CMD, '%cmt%', fOptions.GetEntry('OBS Comment') );
  CMD       := StrReplaceNC( CMD, '%rsn%', fOptions.GetEntry('Receiver Serial Number') );
  CMD       := StrReplaceNC( CMD, '%mn%',  fOptions.GetEntry('Station Name' ) );
  CMD       := StrReplaceNC( CMD, '%rb%',  'ZBASE_B'+_BuildNumber );

  ConvertTEQCCommand2File( CMD, cfn );

  CMD := ' -config '+ExtractFilename( cfn ) + ' ' + FN;
  Log(' @Executing '+TEQCFN+' '+CMD );
  LinkTo( TEQCFN, ExtractFilePath( cfn ), CMD );
  Stat('Wait for conversion to finish and close files...');
  if NOT (FileExistsClosed( navF, 10, 0 ) AND
          FileExistsClosed( obsF, 5, 0 ) ) then
  begin
    Log('!! Error! TEQC output files were not created within allocated time.');
    exit;
  end;

  // GZip the files
  Stat('GZIP '+navF);
  GZFN      := _EXEDir + 'gzip.exe';
  CMD       := zipoptn+' '+navF;
  Log(' @Executing '+GZFN+' '+CMD );
  LinkTo( GZFN, ExtractFilePath( TempDir ), CMD );
  if NOT FileExistsClosed( navf+'.gz', 4, 5 ) then
  begin
    Log('!! ERROR! GZip of '+navf+' did not complete!');
    exit;
  end;

  Stat('GZIP '+obsF);
  GZFN      := _EXEDir + 'gzip.exe';
  CMD       := zipoptn+' '+obsF;
  Log(' @Executing '+GZFN+' '+CMD );
  LinkTo( GZFN, ExtractFilePath( TempDir ), CMD );
  if NOT FileExistsClosed( obsf+'.gz', 4, 5 ) then
  begin
    Log('!!ERROR! GZip of '+obsf+' did not complete!');
    exit;
  end;

  // if there were no issues, delete the log file
  if FileExists( logF ) then DeleteFile( logF );

  Log('CONVERT complete '+FN+' in '+ExtToStrComma(DeltaTimeSeconds(StartTime),1)+' seconds.');
  Log(']');
  Log('');

end;

procedure TfMain.Exit1Click(Sender: TObject);
// Main Menu option "File: Exit"
begin
  if mrYES = MessageDlg( 'Are you sure you want to abort any running processes and exit the ZBase program?',
                        mtConfirmation, [mbYES,mbNO], 0 ) then
  begin
    WantToQuit := TRUE; // Abort current processes
    Close;
  end;
end;

procedure TfMain.ReprocessRawFile1Click(Sender: TObject);
var
  OpnUDlg: tOpenDialog;
begin
  if BUSY then
  begin
    Log('Already busy! Try again later...');
    exit;
  end;
  BUSY := TRUE;
  try
    WantToQuit := FALSE;
    // Reprocess a single file
    OpnUDlg := tOpenDialog.Create( Self );
    try
      OpnUDlg.InitialDir := fOptions.GetEntry('Raw Folder');
      OpnUDlg.Filter     := 'Raw Files|U*.*|All Files|*.*';
      OpnUDlg.Title      := 'Raw File to Convert to Rinex.Z';
      if OpnUDlg.Execute then ConvertUFile( OpnUDlg.FileName )
    finally
      OpnUDlg.Free;
    end;
  finally
    BUSY := FALSE;
  end;
end;

procedure TfMain.ProcessFilesInFolder( P: string );
// Process any file in the folder P that begins with the letter 'U'
var
  SR: tSearchRec;
  OK: boolean;
begin
  P := AddBackSlash( P );
  Log(' Process folder '+P );
  OK := 0 = FindFirst( P+'*.*', faANYFILE, SR );
  try
    while OK AND (NOT WantToQuit) do
    begin
      if FirstChar( SR.Name ) <> '.' then
      begin
        if (SR.Attr AND faDirectory) > 0 then
        begin
          ProcessFilesInFolder( P+SR.Name );
        end else
        begin
          if UpCase( FirstChar( SR.Name ) ) = 'U' then ConvertUFile( P+SR.Name );
        end;
      end;
      OK := 0 = FindNext( SR );
    end;
  finally
    FindClose( SR );
  end;
end;

procedure TfMain.ReprocessDirectory1Click(Sender: TObject);
var
  P: string;
begin
  if BUSY then
  begin
    Log('Already busy! Try again later...');
    exit;
  end;
  BUSY := TRUE;
  try
    WantToQuit := FALSE;
    P := fOptions.GetEntry('Raw Folder');
    if SelectDirectory( 'Path to process for U files', '', P ) then
    begin
      Log('>>>>>> Begin processing path '+P);
      ProcessFilesInFolder( P );
      Log('>>>>>> Processing complete on path '+P);
    end;
  finally
    BUSY := FALSE;
  end;
end;

procedure TfMain.Settings1Click(Sender: TObject);
// Main Menu: "Options: Settings..."
begin
  fOptions.ShowModal;
end;

procedure TfMain.ProcessNewFilesInGPS(Sender: TObject);
var
  RecType, ChannelOptn, NAVFW, RecOptn, BootFW: string;
  NumFiles: integer;    // number of files in receiver
  i: integer;
  CurFiles, MountPC, FreeMem, Baud, SmallFile, smi, CurFile: integer;
  D: tDirEntries;
  RawFN, RawBUFN, P: string;
  FileMatched: boolean;
begin
  WantToQuit := FALSE;
  COM.ComNumber := Str2IntD( fOptions.GetEntry('COM Port'), 1 );
  COM.Baud      := Str2IntD( fOptions.GetEntry('Baud Rate'), 9600 );
  COM.Open      := TRUE;
  try
    InitChannel; // reset channel
    Baud := FindReceiverBaud;
    if Baud < 300 then
    begin
      Log('Unable to contact GPS receiver');
      exit;
    end;
    // Make sure there is a receiver there
    // if NOT GetRID( RecType, ChannelOptn, NAVFW, RecOptn, BootFW ) then exit;
    // Save baud rate
    fOptions.SetEntry( 'Baud Rate', IntToStr(Baud) );

    if WantToQuit then exit;

    // Wait for the SD Card to fully mount!
    if WantToQuit then exit;
    if NOT WaitForZMemoryMount then exit;

    if WantToQuit then exit;
    if NOT GetZFileSystemStatus( CurFile, NumFiles, MountPC ) then
    begin
      Log('Unable to get system file status, unable to delete file '+FN);
      exit;
    end;

    // Read the reciever directory
    if WantToQuit then exit;
    if NOT GetZDirNS2( FreeMem, NumFiles, D ) then
    begin
      Log('Failed to get directory from GPS');
      exit;
    end;

    if NumFiles < 1 then
    begin
      Log('No files in receiver.');
      exit;
    end;

    // Check each file for closed files. If closed file is found, copy, process and delete
    for i:=0 to High( D ) do
    begin

      if WantToQuit then exit;

      if i = CurFile then
      begin
        // Don't delete the current file!
        Log('File '+D[i].Filename+' is the current file.');
      end else
      if D[i].FileSize > 2500000 then
      begin
        // Delete the file. It is WAY TOO BIG!
        Log('@ Delete file '''+D[i].Filename+''' it is too big to be of use or real. ');
        DeleteZFileByIndex( i );
        exit;  // Bail, file is way too big!

      end else
      if D[i].FileAge < 90.0 then
      begin
        // File is probably open
        Log('Skipping '+D[i].Filename+', it is only '+ExtToStr(D[i].FileAge,1)+' seconds old.');
      end else
      begin
        // Transfer the file
        RawFN := LowerCase( AddBackSlash( fOptions.GetEntry('Raw Folder')) + FN2Path(D[i].Filename)
                            + D[i].Filename );

        // Make sure folder exists
        P := ExtractFilePath( RawFN );
        if NOT DirectoryExists(P) then ForceDirectories( P );

        FileMatched := FileExists( RawFN ) AND
                       ( abs( uFileSize( RawFN ) - D[i].FileSize ) < 1024) AND
                       ( abs(GetFileDateDT(RawFN)-D[i].LastRecording)*SecondsPerDay < 2.0 );

        // Copy the file
        if NOT FileMatched then
        begin
          if NOT GetZFile( D[i].Filename, RawFN ) then
          begin
            Log('Unable to transfer file...aborting...');
            exit;
          end;
          SetFileDate( RawFN, D[i].LastRecording );
          FileMatched := FileExists( RawFN ) AND
                       ( abs( uFileSize( RawFN ) - D[i].FileSize ) < 1024) AND
                       ( abs(GetFileDateDT(RawFN)-D[i].LastRecording)*SecondsPerDay < 2.0 );
        end;

        if FileMatched then
        begin
          if Trim(fOptions.GetEntry('Raw Backup Folder'))<>'' then
          begin
            // Make backup copy
            RawBUFN := LowerCase( AddBackSlash( fOptions.GetEntry('Raw Backup Folder')) + FN2Path(D[i].Filename) )
                 + D[i].Filename;
            if NOT DirectoryExists( ExtractFilePath( RawBUFN ) ) then
            begin
              ForceDirectories( ExtractFilePath( RawBUFN ) );
            end;
            if NOT FileExists( RawBUFN ) then
            begin
              Log('@ Make backup copy of raw file '+D[i].Filename);
              CopyFile( PChar(RawFN), PChar(RawBUFN), FALSE );
              SetFileDate( RawBUFN, D[i].LastRecording );
            end;

          end;

          // Process the U file to RINEX
          ConvertUFile( RawFN );

          if FileMatched  then
          begin
            // Delete the matching file on the GPS
            Log('@ Delete file '''+D[i].Filename+''' on GPS reciever: ');
            DeleteZFile( D[i].Filename );
            exit; // We can only delete one file per directory read, because the directory gets reordered.
          end;

        end;

        if WantToQuit then
        begin
          Log('ERROR User aborted file transfer, backup, delete by pressing ABORT button!');
          exit;
        end;

      end; // if file is old enough to transfer
    end; // for i:=0 to High(D)

  finally
    COM.Open := FALSE;
  end;

end;


procedure TfMain.Special1Click(Sender: TObject);
begin
  if BUSY then
  begin
    Log('Already busy! Try again later...');
    exit;
  end;
  BUSY := TRUE;
  try
    ProcessNewFilesInGPS(Sender);
  finally
    BUSY := FALSE;
  end;
end;

procedure TfMain.FormDestroy(Sender: TObject);
var
  INIFile: tINIFile;
begin
  // Get screen position
  INIFile := tINIFile.Create( ChangeFileExt( Application.EXEName, '.ini' ));
  try
    INIFile.WriteInteger( 'FORM', 'Left', Left );
    INIFile.WriteInteger( 'FORM', 'Top', Top );
    INIFile.WriteInteger( 'FORM', 'Width', Width );
    INIFile.WriteInteger( 'FORM', 'Height', Height );
  finally
    INIFile.Free;
  end;
  Log('----- ZBase ending '+DateTimeToStr(Now)+' ---------------------------------------------------');
end;

procedure TfMain.FormCreate(Sender: TObject);
begin
  BUSY      := FALSE;
  LastTimer := Now;
end;

procedure TfMain.Timer1Timer(Sender: TObject);
var
  dT: extended;
const
  CheckEvery = 30.0;
begin
  Timer1.Enabled := FALSE;
  try
    dT := DeltaTimeSeconds( LastTimer );
    if NOT cbTimerOK.Checked then exit;   // bail if not enabled
    if dT > CheckEvery then
    begin
      LastTimer := Now;
      // Main Timer, checks receiver status
      if BUSY then
      begin
        Log('Already busy! Try again later...');
        exit;
      end;
      BUSY := TRUE;
      try
        Stat('Check for log file rotation');
        RotateLog;
        Stat('Check for new file...');
        ProcessNewFilesInGPS(Sender);
        LastTimer := Now;
      finally
        BUSY := FALSE;
      end;
    end else
    begin
      // Display information for user
      Stat('will check in '+ExtToStr( CheckEvery - dT, 1 )+' seconds.');
    end;
  finally
    Timer1.Interval := Timer1.Interval;
    Timer1.Enabled  := TRUE;
  end;

end;

function TfMain.ForceBaud( NewBaud: integer ): boolean;
var
  RecType, ChannelOptn, NAVFW, RecOptn, BootFW, SL, S: string;
  iB, Baud: integer;
  GPSPort: char;
begin
  result := FALSE; // assume failure

  case NewBaud of
    300: iB := 0;
    600: iB := 1;
    1200: iB := 2;
    2400: iB := 3;
    4800: iB := 4;
    9600: iB := 5;
    19200: iB := 6;
    38400: iB := 7;
    56800: iB := 8;
    115200: iB := 9;
    else   begin
             Log('Selected baud rate "'+IntToStrComma(NewBaud)+'" is unknown.');
             exit;
           end;
  end;

  COM.ComNumber := Str2IntD( fOptions.GetEntry('COM Port'), 1 );
  COM.Baud      := Str2IntD( fOptions.GetEntry('Baud Rate'), 9600 );
  COM.Open      := TRUE;
  try
    InitChannel; // reset channel
    // Get hold of the reciever
    Baud := FindReceiverBaud;
    if Baud < 0 then
    begin
      Log('Unable to reach reciever. Check connections, power...');
      exit;
    end;

    // Set control port baud rate
    if WantToQuit then exit;

    // Set the control port to 38,400
    if NOT GetCurrentSerialPort( GPSPort ) then
    begin
      Log('Unable to determine which GPS port is connected for control.');
      exit;
    end;

    // Set control port baud rate
    if WantToQuit then exit;

    // Set the NewBaud rate
    Stat('Set baud rate to '+IntToStrComma(NewBaud));
    OneCommand( 'PASHS,SPD,'+GPSPort+','+IntToStr(iB) );

    if WantToQuit then exit;
    if NOT ReceiverThere( NewBaud ) then
    begin
      Log('Unable to commuicate with GPS after setting speed to 38,400 baud.');
      exit;
    end;
    Baud := NewBaud;

    // Save baud rate
    fOptions.SetEntry( 'Baud Rate', IntToStr(Baud) );
    Stat('');

  finally
    COM.Open := FALSE;
  end;
end;


procedure TfMain.ConfigureReceiver1Click(Sender: TObject);
var
  RecType, ChannelOptn, NAVFW, RecOptn, BootFW, SL, S: string;
  Baud: integer;
  GPSPort: char;
  RecInt: extended;
  ElvMask: integer;
begin
  COM.ComNumber := Str2IntD( fOptions.GetEntry('COM Port'), 1 );
  COM.Baud      := Str2IntD( fOptions.GetEntry('Baud Rate'), 9600 );
  COM.Open      := TRUE;
  try
    InitChannel; // reset channel
    Baud := FindReceiverBaud;
    // Make sure there is a receiver there
    if NOT GetRID( RecType, ChannelOptn, NAVFW, RecOptn, BootFW ) then
    begin
      Log('Unable to connect to GPS! Unable to configure receiver.');
      exit;
    end;

    // Save baud rate
    fOptions.SetEntry( 'Baud Rate', IntToStr(Baud) );

    // Set control port baud rate
    if WantToQuit then exit;

    // Stop the current Session
    if NOT ZStop then
    begin
      Log('Unable to STOP sessions on receiver.');
      exit;
    end;

    // Wait for SD card to mount
    if WantToQuit then exit;
    if NOT WaitForZMemoryMount then
    begin
      Log('Reciever memory never mounted.');
      exit;
    end;

    // Turn off SBAS
    if WantToQuit then exit;
    if NOT OneCommand( 'PASHS,SBA,OFF' ) then
    begin
      Log('Unable to turn off SBAS (WAAS).');
      exit;
    end;

    // Set the SITE NAME
    if WantToQuit then exit;
    S := fOptions.GetEntry('Station Name');
    S := Copy( S, 1, 4 );
    S := EPad( S, '_', 4 );
    if NOT OneCommand( 'PASHS,SIT,'+S ) then
    begin
      Log('Unable to set Station Name to '''+S+'''.' );
      exit;
    end;

    // Set the Baud Rate of RTCM Port
    if WantToQuit then exit;
    S := fOptions.GetEntry('RTCM Port');
    if NOT OneCommand( 'PASHS,SPD,'+FirstChar(S)+',6' ) then
    begin
      Log('Unable to set RTCM Port to 19,200 baud.');
      exit;
    end;

    // Set the Antenna Position
    if WantToQuit then exit;
    S := fOptions.GetEntry('ARP');
    if NOT OneCommand( 'PASHS,POS,'+S ) then
    begin
      Log('Unable to set Antenna Reference Position.');
      exit;
    end;

    // Set the Antenna Offset
    if WantToQuit then exit;
    S := fOptions.GetEntry('HI');
    if NOT OneCommand( 'PASHS,ANT,'+S+',0,0,0,0' ) then
    begin
      Log('Unable to set antenna offset (HI).');
      exit;
    end;

    if WantToQuit then exit;
    if NOT OneCommand( 'PASHS,ANR,ON' ) then
    begin
      Log('Unable to turn on antenna reduction.');
      exit;
    end;

    // Set the Elevation Mask for RTK
    if WantToQuit then exit;
    S := fOptions.GetEntry( 'RTCM Elevation Mask' );
    if NOT OneCommand( 'PASHS,PEM,'+S ) then
    begin
      Log('Unable to set RTCM elevation mask.');
      exit;
    end;

    if WantToQuit then exit;
    if NOT OneCommand( 'PASHS,RTC,BAS,B' ) then
    begin
      Log('Unable to set base to port B.');
      exit;
    end;

    if WantToQuit then exit;
    if NOT OneCommand( 'PASHS,RTC,TYP,1,0' ) then
    begin
      Log('Unable to set RTCM Type 1 message to 0.');
      exit;
    end;

    // Enable postion once per minute
    if WantToQuit then exit;
    if NOT OneCommand( 'PASHS,RTC,TYP,3,1' ) then
    begin
      Log('Unable to set RTCM Type 3 message to 1.');
      exit;
    end;

    // Enable type 18 message once per second
    if WantToQuit then exit;
    if NOT OneCommand( 'PASHS,RTC,TYP,18,1' ) then
    begin
      Log('Unable to set RTCM Type 18 message to 1.');
      exit;
    end;

    // Enable type 22 message
    if WantToQuit then exit;
    if NOT OneCommand( 'PASHS,RTC,TYP,22,1' ) then
    begin
      Log('Unable to set RTCM Type 22 message to 1.');
      exit;
    end;

    // RTCM data should stream at maximum rate
    if WantToQuit then exit;
    if NOT OneCommand( 'PASHS,RTC,SPD,9' ) then
    begin
      Log('Unable to set RTCM internal rate to burst.');
      exit;
    end;

    // Set sessions to 24, 1-hour sessions.
    if WantToQuit then exit;
    S :=fOptions.GetEntry('Data Recording Interval');
    RecInt := Str2ExtD( S, 5.0 );
    S :=fOptions.GetEntry('Data Elevation Mask');
    ElvMask := Str2INTD( S, 10 );
    SetSessions( 60, RecInt, ElvMask, 1, FALSE );
    GetSessionList( SL );

    if WantToQuit then exit;
    if NOT OneCommand( 'PASHS,SAV,Y' ) then
    begin
      Log('Unable to SAVE parameters to NV ram.');
      exit;
    end;
    // Start receiver sessions
    ZStart;
    
  finally
    COM.Open := FALSE;
  end;
end;

procedure TfMain.Force38400baud1Click(Sender: TObject);
begin
  ForceBaud( 38400 );
end;

end.
