unit header_utils;

// Header file utilities

{$mode objfpc}{$H+}
interface

uses
  Classes, SysUtils, StrUtils, Forms, Graphics, dateutils
  , appsettings, dlheader, viewlog
  , synaser, synautil, blcksock
  , math // required for radtodeg
  , Printers, Process
  , LazSysUtils //For NowUTC()
  ;

function OpenComm() : boolean;
function CloseComm() : boolean;
procedure WriteDLHeader(Style:String; Setting:String=''; Ext: String = '.dat');
function SendGet(command:string; LeaveOpen:boolean = False; Timeout:Integer=3000; GetAlso:boolean = True; HideStatus:boolean = False) : string;
function GetReading(): String;
procedure DisplayedReading(Darkness:Double);
procedure StatusMessage(Mstring:string);
procedure GetVersion;
procedure ClearLockVisibility;
procedure CheckLockVisibility;
procedure UpdateCalReport;
procedure PrintLine(LabelText:String; DataText: String='');
function FixDate(incoming:AnsiString): AnsiString;
function ParameterCommand(Command:String): Boolean;

var
   ParameterValue: TStringList;
   FieldNames:String; //Data line entries in human format
   FieldUnits:String; //Units of data line entries

   //Field indices of the most recent GetReading, so other units can locate the
   //unaveraged value, freshness state, and snow-LED status without recomputing
   //the layout. -1 means the field is not present in the current reading.
   ReadingUAField:Integer = -1;
   FreshnessField:Integer = -1;
   SnowLEDField:Integer = -1;

implementation

uses
  Unit1
  , vinfo
  , dlerase //Data logger erase-all form and some variables
  , Vector //VectorTab model
  , Logcont
  ;

{ Open the communications port
   - Will only open if not already opened. }
function OpenComm() : boolean;
begin

   if not CommOpened then begin

     { Clear out old port name. }
     PortName:='';

     { Check selected communications interface }
     case SelectedInterface of
         'USB': begin
              PortName:=Form1.USBPort.Text;
              if (PortName<>'') then begin
                ser.LinuxLock:=False; //lock file sometimes persists stuck if program closes before port
                SelectedPort:=PortName;
                ser.Connect(SelectedPort);
                ser.config(115200, 8, 'N', SB1, False, False); //original
                //ser.config(115200, 8, 'N', SB1, False,True); //test hardware flow control
                PortName:=SelectedPort;
                OpenComm:=True;    //Indicate success
              end;
         end;

         { The XPort Ethernet module may have been set to auto-disconnect in a few seconds.
           Each communication with it must be checked for availability.}
         'Eth','WiFi': begin

              PortName:=Form1.EthernetIP.Text;
              if (PortName<>'') then begin
                EthSocket := TTCPBlockSocket.Create;
                EthSocket.ConvertLineEnd := True;
                //Try setting the timeout a bit longer than normal, because
                // finding Ethernet devices failed a few times on slow Windows7 netbook.
                EthSocket.SetRecvTimeout(2000);
                EthSocket.SetSendTimeout(2000);
                SelectedIP:=PortName;
                SelectedPort:=Form1.EthernetPort.Text;
                EthSocket.Connect(SelectedIP, SelectedPort);
                EthConnected:=True;
                PortName:=SelectedIP;
                OpenComm:=True;    //Indicate success
               end;
         end;

         'RS232': begin
              ser.LinuxLock:=False; //lock file sometimes persists stuck if program closes before port
              //writeln(ser.LastError, ' ', ser.LastErrorDesc);
              //writeln('RS232PortName=', RS232PortName);
              ser.Connect(RS232PortName);
              //writeln(ser.LastError, ' ', ser.LastErrorDesc);
              //writeln('RS232PortBaud=', RS232PortBaud);
              ser.config(RS232PortBaud, 8, 'N', SB1, False, False);
              PortName:=RS232PortName;
              //if ser.InstanceActive then
              //  writeln('active')
              //else begin
              //    writeln('not active');
              //    writeln(ser.LastError, ' ', ser.LastErrorDesc);
              //end;
              OpenComm:=True;    //Indicate success
         end;
     end;

     if OpenComm then begin
         CommOpened:=True;
         //StatusMessage('OpenComm');
         form1.CommOpen.Brush.Color:=clLime;
       end
     else
       CommOpened:=False;
   end;

end;
// Close the communications port
function CloseComm() : boolean;
begin

     unit1.Form1.CommBusy.Enabled:=False; //Prevent further triggers to the comm. busy timer.
     CommBusyTime:=0; //Reset comm. busy count.
     CommOpened:=False; //Indicate that the comm. port is closed.

     {Close all UDM communication ports.
       - Ports not already opened will have an ignored exception.}
     try
        ser.Purge;
        ser.CloseSocket;
     except
        StatusMessage('ser.CloseSocket exception');
     end;

     try
        if EthConnected then begin
          EthSocket.CloseSocket;
          EthSocket.Free;
          EthConnected:=False;
        end;
     except
        StatusMessage('EthSocket.CloseSocket exception');
     end;

     CloseComm:=True;       //Indicate success
     Unit1.Form1.CommOpen.Brush.Color:=clGray;

end;

procedure WriteDLHeader(Style:String; Setting: String = ''; Ext: String = '.dat' );
//Style:
//  DL-Log (Short record)
//  LE (Long record) default
//  ADA (Auroral Detection Alarm)
//  DL-V-Log (Vector model)
//  DL-V-HSLog (Vector model hard soft calibration log)
//Setting describes how UDM was used to create this logfile.
var
  HeaderFirmwareVersion: AnsiString;
  result: AnsiString; //General purpose result
  result_ix: AnsiString; //Information result
  ProtocolNumber,ModelNumber,FeatureNumber,SerialNumber : Integer;
  Info:     TVersionInfo;
  AccCalPos: Integer; //Accelerometer position
  ClockDiffSeconds:Integer;
  ThisMomentUTC, UnitTime: TDateTime;
  UnitClock: AnsiString;
  NumberOfFields:Integer;
  ExternalGPSHeader: Boolean = False;
  HeaderLines: TStringList; //Contrains all headerlines for easier counting up later.
  s:String; // General purpose string
  LocationName: String = '';
  InstrumentIDName: String = '';

begin

    HeaderLines:=TStringList.Create;

    //Only use external GPS header information if external GPS is enabled and not getting from logger.
    ExternalGPSHeader:=(FormLogCont.GPSLogIndicator.Visible and not AnsiContainsStr(Setting,'retrieve'));

    {Gather information about the selected unit}
    result_ix:=SendGet('ix');
    ProtocolNumber:=StrToIntDef(AnsiMidStr(result_ix,3,8),0);
    ModelNumber:=StrToIntDef(AnsiMidStr(result_ix,12,8),0);
    FeatureNumber:=StrToIntDef(AnsiMidStr(result_ix,21,8),0);
    SerialNumber:=StrToIntDef(AnsiMidStr(result_ix,30,8),0);
    HeaderFirmwareVersion:=
      IntToStr(ProtocolNumber)+'-'+
      IntToStr(ModelNumber)+'-'+
      IntToStr(FeatureNumber);

    {Prepend LocationName and InstrumentID}
    //LogFileName:=Format('%s%s_%s'+Ext,[RemoveMultiSlash(appsettings.LogsDirectory + DirectorySeparator),FormatDateTime('yyyymmdd"_"hhnnss',Now()),DLHeaderForm.InstrumentIDEntry.Text]);
    LocationName:=CleanFileName(DLHeaderForm.LocationNameEntry.Text);
    if Length(LocationName)>0 then LocationName:=LocationName+'_';

    InstrumentIDName:=CleanFileName(DLHeaderForm.InstrumentIDEntry.Text);
    if  Length(InstrumentIDName)>0 then InstrumentIDName:=InstrumentIDName+'_';

    LogFileName:=Format('%s%s%s_%s'+Ext,[
        RemoveMultiSlash(appsettings.LogsDirectory +DirectorySeparator),
        LocationName,
        InstrumentIDName,
        FormatDateTime('yyyymmdd"_"hhnnss',Now())
        ]);

    CSVLogFileName:=Format('%s%s_%s.csv',[RemoveMultiSlash(appsettings.LogsDirectory + DirectorySeparator),FormatDateTime('yyyymmdd"_"hhnnss',Now()),DLHeaderForm.InstrumentIDEntry.Text]);
    if TransferCSV then begin
      AssignFile(DLRecFile,CSVLogFileName);
      Rewrite(DLRecFile); //Open file for writing
      WriteLn(DLRecFile,''); //Write to empty file as placeholder
      Flush(DLRecFile);
      CloseFile(DLRecFile);
    end;

    AssignFile(DLRecFile,LogFileName);
    Rewrite(DLRecFile); //Open file for writing

    { Write header }
    SetTextLineEnding(DLRecFile,#13#10);
    HeaderLines.Add('# Light Pollution Monitoring Data Format 1.0');
    HeaderLines.Add('# URL: https://darksky.org/app/uploads/bsk-pdf-manager/47_SKYGLOW_DEFINITIONS.PDF');

    //Determine number of header lines based on style of logging
    //if SelectedModel=model_V then
    //  HeaderLines.Add('# Number of header lines: 46')
    //else
    //  HeaderLines.Add('# Number of header lines: 36');

    HeaderLines.Add('# This data is released under the following license: ODbL 1.0 http://opendatacommons.org/licenses/odbl/summary/');
    HeaderLines.Add('# Device type: '+SelectedModelDescription);
    HeaderLines.Add('# Instrument ID: '+DLHeaderForm.InstrumentIDEntry.Text);
    HeaderLines.Add('# Data supplier: '+DLHeaderForm.DataSupplierEntry.Text);
    HeaderLines.Add('# Location name: '+DLHeaderForm.LocationNameEntry.Text);
    HeaderLines.Add('# Position (lat, lon, elev(m)): '+DLHeaderForm.PositionEntry.Text);
    HeaderLines.Add('# Local timezone: '+DLHeaderForm.TZLocationBox.Text);
    HeaderLines.Add('# Time Synchronization: '+DLHeaderForm.TimeSynchEntry.Text);

    { GPS moving platform }
    if ExternalGPSHeader then
      HeaderLines.Add('# Moving / Stationary position: MOVING')
    else
      HeaderLines.Add('# Moving / Stationary position: '+DLHeaderForm.MovingStationaryPositionCombo.Text);

    HeaderLines.Add('# Moving / Fixed look direction: '+DLHeaderForm.MovingStationaryDirectionCombo.Text);
    HeaderLines.Add('# Number of channels: '+DLHeaderForm.NumberOfChannelsEntry.Text);
    HeaderLines.Add('# Filters per channel: '+DLHeaderForm.FiltersPerChannelEntry.Text);
    HeaderLines.Add('# Measurement direction per channel: '+DLHeaderForm.MeasurementDirectionPerChannelEntry.Text);
    HeaderLines.Add('# Field of view (degrees): '+DLHeaderForm.FieldOfViewEntry.Text);

    {Determing number of field that are recorded}
    if Style = 'DL-Log' then {Short record}
      NumberOfFields:=5
    else if Style = 'DL-V-Log' then //vector model
      NumberOfFields:=14
    else if Style = 'DL-V-HSLog' then //vector model Hard/soft calibration log
      NumberOfFields:=3
    else if Style = 'ADA' then //aurora
      NumberOfFields:=7
    else if Style = 'GDM' then //geomagnetic disturbance meter
      NumberOfFields:=4
    else if Style = 'C' then //color model
      NumberOfFields:=8
    else {Long record}
       NumberOfFields:=6;

    if GoToEnabled then
      NumberOfFields:=NumberOfFields+2;

    if Freshness then
      NumberOfFields:=NumberOfFields+2;

    {Snow factor columns for the live continuous log (DL-Log retrieval style is
     handled by its own field logic above). Staged logging always emits a flag,
     the snow MSAS value, and the snow status (the snow reading is always the
     unaveraged u1x reading, so its status is always meaningful); legacy logging
     emits a single S/D status column.}
    if ((SelectedModel=model_DLS) and SnowLoggingEnabled and (Style<>'DL-Log')) then begin
      if SnowStagedLogging then
        NumberOfFields:=NumberOfFields+3   //flag, Snow MSAS, Snow Status
      else
        NumberOfFields:=NumberOfFields+1;  //legacy Snow/Dark LED status
    end;

    if ExternalGPSHeader then
      NumberOfFields:=NumberOfFields+5;

    if NumberOfMultipleDevices>0 then
      NumberOfFields:=NumberOfFields+1;

    HeaderLines.Add('# Number of fields per line: ' + IntToStr(NumberOfFields));


    HeaderLines.Add(Format('# SQM serial number: %d',[SerialNumber]));
    HeaderLines.Add('# SQM hardware identity: ' + SelectedHardwareID);
    HeaderLines.Add('# SQM firmware version: '+HeaderFirmwareVersion);
    HeaderLines.Add('# SQM cover offset value: '+DLHeaderForm.CoverOffsetEntry.Text);
    HeaderLines.Add('# SQM readout test ix (Information): '+result_ix);
    HeaderLines.Add('# SQM readout test rx (Reading): '+sendget('rx'));

    if not (Style = 'GDM') then
      HeaderLines.Add('# SQM readout test cx (Calibration): '+sendget('cx'));

    HeaderLines.Add('# SQM readout test Ix (Report Interval): '+sendget('Ix'));

    {Log the time difference, and logging threshold}
    case SelectedModel of
        model_DL, model_V, model_GPS, model_DLS: begin
        result:=sendget('Lcx'); { Read the RTC }
        ThisMomentUTC:=LazSysUtils.NowUTC();
         if Length(result)>=21 then begin
             UnitClock:=FixDate(AnsiMidStr(Trim(result),4,19));
             try
                UnitTime:=ScanDateTime('yy-mm-dd hh:nn:ss',LeftStr(UnitClock,9)+RightStr(UnitClock,8));
             except
                   StatusMessage('Invalid RTC from device = '+UnitClock);
                   UnitTime:=ThisMomentUTC;
             end;
             ClockDiffSeconds:=SecondsBetween(ThisMomentUTC,UnitTime);
             if ThisMomentUTC>UnitTime then ClockDiffSeconds:=ClockDiffSeconds*-1;
             HeaderLines.Add('# DL time difference (seconds): '+IntToStr(ClockDiffSeconds));
           end
         else
           HeaderLines.Add('# DL time difference: ???');

         HeaderLines.Add('# DL retrieved at (UTC): '+FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz',ThisMomentUTC));

         result:=sendget('Lmx');  {Read logging trigger mode}
         HeaderLines.Add('# DL trigger mode : '+Unit1.form1.TriggerComboBox.Items[StrToIntDef(AnsiMidStr(result,4,1),0)]);

         result:=sendget('LIx');  {Read the logging trigger and threshold setting}
         HeaderLines.Add('# DL trigger seconds : '+IntToStr(StrToIntDef(AnsiMidStr(result,4,10),0)));
         HeaderLines.Add('# DL trigger minutes : '+IntToStr(StrToIntDef(AnsiMidStr(result,16,10),0)));
         HeaderLines.Add('# DL trigger threshold : '+FloatToStr(StrToFloatDef(AnsiMidStr(result,52,11),0,FPointSeparator)));

      end;
    end;


    { TODO : vector cal info }
    //if vector model then write accellerometer and magnetic calibration values
    if SelectedModel=model_V then begin
      for AccCalPos:=1 to 6 do begin
        HeaderLines.Add(Format('# Acceleration position %d:  %6.0f  %6.0f  %6.0f',[AccCalPos,
                   w.getv(AccCalPos-1, 0),
                   w.getv(AccCalPos-1, 1),
                   w.getv(AccCalPos-1, 2)]));
        end;
        HeaderLines.Add(Format('# Magnetic maximum XYZ: %7.0f %7.0f %7.0f',[Mxmax,Mymax,Mzmax]));
        HeaderLines.Add(Format('# Magnetic minimum XYZ: %7.0f %7.0f %7.0f',[Mxmin,Mymin,Mzmin]));
    end;

    HeaderLines.Add('# Comment: '+DLHeaderForm.UserComment1.Text);
    HeaderLines.Add('# Comment: '+DLHeaderForm.UserComment2.Text);
    HeaderLines.Add('# Comment: '+DLHeaderForm.UserComment3.Text);
    HeaderLines.Add('# Comment: '+DLHeaderForm.UserComment4.Text);
    HeaderLines.Add('# Comment: '+DLHeaderForm.UserComment5.Text);

    // Log the UDM version.
    Info := TVersionInfo.Create;
    Info.Load(HINSTANCE);
    HeaderLines.Add(Format('# UDM version: %s',
                                [IntToStr(Info.FixedInfo.FileVersion[0])
                                +'.'+IntToStr(Info.FixedInfo.FileVersion[1])
                                +'.'+IntToStr(Info.FixedInfo.FileVersion[2])
                                +'.'+IntToStr(Info.FixedInfo.FileVersion[3])]));
    Info.Free;

    //Log the current UDM settings that were passed here.
    HeaderLines.Add(Format('# UDM setting: %s',[Setting]));

    HeaderLines.Add('# blank line');

    //Determing Field names and units
    if Style = 'DL-Log' then begin //Short record
      FieldNames:='# UTC Date & Time, Local Date & Time, Temperature, Voltage, MSAS';
      FieldUnits:='# YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Celsius;Volts;mag/arcsec^2';
      if StrToInt(SelectedFeature)>=49 then begin
        FieldNames:=FieldNames+', Record type';
        FieldUnits:=FieldUnits+';Init/Subs';
      end;
    end
    else if Style = 'DL-V-Log' then begin
      if Setting ='One record logged' then begin
        FieldNames:='# UTC Date & Time, Local Date & Time, Temperature, Counts, Frequency, MSAS, Ax, Ay, Az, Mx, My, Mz, Altitude, Zenith, Azimuth, Vibration';
        FieldUnits:='# YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Celsius;number;Hz;mag/arcsec^2;Ax;Ay;Az;Mx;My;Mz;degrees;degrees;degrees;count';
      end
      else if ((Setting='DL Retrieve All') or (Setting='DL-V binary retrieve')) then begin
        FieldNames:='# UTC Date & Time, Local Date & Time, Temperature, Voltage, MSAS, Ax, Ay, Az, Mx, My, Mz, Altitude, Zenith, Azimuth, Vibration';
        FieldUnits:='# YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Celsius;Volts;mag/arcsec^2;Ax;Ay;Az;Mx;My;Mz;degrees;degrees;degrees;count';
      end
      else begin
        FieldNames:='# UTC Date & Time, Local Date & Time, Temperature, Counts, Frequency, MSAS, Ax, Ay, Az, Mx, My, Mz, Altitude, Zenith, Azimuth, Vibration';
        FieldUnits:='# YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Celsius;number;Hz;mag/arcsec^2;Ax;Ay;Az;Mx;My;Mz;degrees;degrees;degrees;count';
      end;

      //Newer DL units have identification for initial or subsequent records.
      if StrToInt(SelectedFeature)>=49 then begin
        FieldNames:=FieldNames+', Record type';
        FieldUnits:=FieldUnits+';Init/Subs';
      end;
    end
    else if Style = 'DL-V-HSLog' then begin
      FieldNames:='# Counts, Counts, Counts';
      FieldUnits:='# Mx, My, Mz';
    end
    else if Style = 'ADA' then begin
      FieldNames:='# UTC Date & Time, Local Date & Time, Frequency, Counts1, Time1, Counts2, Time2, ADAFactor';
      FieldUnits:='# YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Hz;Counts;Seconds;Counts;Seconds;Ratio';
    end
    else if Style = 'GDM' then begin
      FieldNames:='# UTC Date & Time, Local Date & Time, RawMag, Temperature, CompMag';
      FieldUnits:='# YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Hz;Counts;Celcius;Counts';
    end
    else if Style = 'C' then begin
      FieldNames:='# UTC Date & Time, Local Date & Time, Temperature, Counts, Frequency, MSAS, Scale, Color, Cycling';
      FieldUnits:='# YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Celsius;number;Hz;mag/arcsec^2;scale;color;F/C';
    end
    else begin //Long record
        FieldNames:='# UTC Date & Time, Local Date & Time, Temperature, Counts, Frequency, MSAS';
        FieldUnits:='# YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Celsius;number;Hz;mag/arcsec^2';
    end;

    { Optional GoTo position data }
    if GoToEnabled then begin
      FieldNames:=FieldNames+', Zenith, Azimuth';
      FieldUnits:=FieldUnits+';deg;deg';
    end;

    { Optional Raw frequency}
    if ((SelectedModel=model_DLS) and RawFrequencyEnabled) then begin
      case Style of
          'DL-Log':begin
          end;
          else begin
             FieldNames:=FieldNames+', Raw frequency';
             FieldUnits:=FieldUnits+';Hz';
          end;
      end;
    end;

    { Optional Moving platform data }
    if (Freshness and not(AnsiContainsStr(LowerCase(Setting),'retrieve'))) then begin
      FieldNames:=FieldNames+', MSASraw, Status';
      FieldUnits:=FieldUnits+';mag/arcsec^2;F/P/S';
    end;

    { Optional Snow factor for datalogger.
      Placed after the Freshness/Moving-platform block so the header column order
      matches the record order produced by LogOneReading/WriteRecord, which emits
      the freshness fields before the snow fields (the device's native order).}
    if ((SelectedModel=model_DLS) and SnowLoggingEnabled) then begin
      case Style of
          'DL-Log':begin
             FieldNames:=FieldNames+', Std lin., Snow MSAS, Snow lin.';
             FieldUnits:=FieldUnits+';n;mag/arcsec^2;n';
          end;
          else begin
             if SnowStagedLogging then begin
               {Staged capture: snow flag, snow MSAS, and snow status. The snow
                reading is always the unaveraged u1x reading, so the status is
                always logged (independent of the Freshness option).}
               FieldNames:=FieldNames+', Snow flag, Snow MSAS, Snow Status';
               FieldUnits:=FieldUnits+';n;mag/arcsec^2;F/P/S';
             end
             else begin
               {Legacy single-flag behaviour.}
               FieldNames:=FieldNames+', Snow/Dark LED status';
               FieldUnits:=FieldUnits+';S/D';
             end;
          end;
      end;
    end;

    //Optional Moon data
    if FormLogCont.OptionsGroup.Checked[0] then begin
      FieldNames:=FieldNames+', MoonPhaseDeg, MoonElevDeg, MoonIllum, MoonAzimuth';
      FieldUnits:=FieldUnits+';Degrees;Degrees;Percent;Degrees';
    end;

    //Optional GPS data
    if ExternalGPSHeader then begin
      FieldNames:=FieldNames+', Latitude, Longitude, Elevation, Speed, Satellites';
      FieldUnits:=FieldUnits+';Degrees;Degrees;meters;meters/second;Number';
    end;

    //Optional Humidity data
    if A1Enabled then begin
      FieldNames:=FieldNames+', Humidity';
      FieldUnits:=FieldUnits+';Percent';
    end;

    {Optoinal Serial number}
    if NumberOfMultipleDevices>0 then begin
      FieldNames:=FieldNames+', SerialNumber';
      FieldUnits:=FieldUnits+';S/N';
    end;

    HeaderLines.Add(FieldNames);
    HeaderLines.Add(FieldUnits);

    if Ext='.dat' then
       HeaderLines.Add('# END OF HEADER');


    {Count header lines and insert number in second line.}
    HeaderLines.Insert(2,'# Number of header lines: '+ IntToStr(HeaderLines.Count+1));

    { Write header to file }
    for s in HeaderLines do begin
         Writeln(DLRecFile, s);
    end;

    HeaderLines.Destroy;
    Flush(DLRecFile);
    CloseFile(DLRecFile);
end;

// Send a command strings then return the result
function SendGet(command:string; LeaveOpen:boolean = False; Timeout:Integer=3000; GetAlso:boolean = True; HideStatus:boolean = False) : string;
//LeaveOpen indicates that the communication port should be left open
var
   //ErrorNumber: Integer;
   ErrorString: AnsiString;
begin

     {Start up Comm busy timer}
     CommBusyTime:=0; //Reset count
     unit1.Form1.CommBusy.Enabled:=True;

     //Initialze output string to nothing.
     SendGet:='';

     ErrorString:='';

     {Request to open communications, even if already opened. }
     OpenComm();

     { Check selected communication method. }
     case SelectedInterface of

         'USB','RS232': begin
              ser.Purge;//debug (does not seem to work with some Macs )

              while ser.CanRead(10) do //Try another purge method
                 ser.Recvbyte(10);//was recvstring

              ser.SendString(command);
              if (GetAlso) then
                 SendGet:=ser.Recvstring(Timeout);
              If CompareStr(ser.LastErrorDesc,'OK')<>0 then
                 ErrorString:='Error: '+ser.LastErrorDesc;
         end;

         'Eth','WiFi': begin
              EthSocket.ResetLastError;
              EthSocket.Purge;
              EthSocket.SendString(command);

              if (GetAlso) then
                 SendGet:=EthSocket.RecvString(Timeout);

              {If not connected, then retry the connection. }
              If ((EthSocket.LastError=104) or (EthSocket.LastError=10054))then begin
                OpenComm();
                EthSocket.ResetLastError;
                EthSocket.SendString(command);
                if (GetAlso) then
                   SendGet:=EthSocket.RecvString(Timeout);
              end;
              If (EthSocket.LastError<>0) then begin
                 ErrorString:= ' ['+IntToStr(EthSocket.LastError)+']'+ EthSocket.LastErrorDesc;
              end;
         end;

     end;

     if not HideStatus then     //Comment this out to allow all messages through to logging
        begin
          if GetAlso then
            StatusMessage('Sent: '+command+'   To: '+PortName+'   Received: '+SendGet + ErrorString)
          else
            StatusMessage('Sent: '+command+'   To: '+PortName);
        end;

     {Reset comm. counter in case incoming response took a while. }
     CommBusyTime:=0; //Reset count

end;

procedure DisplayedReading(Darkness:Double);
begin
  //Update displayed readings
  Form1.DisplayedReading.Caption :=Darkness2MPSASString(Darkness);
  Form1.DisplayedNELM.Caption    :=Darkness2NELMString(Darkness)+' NELM';
  Form1.Displayedcdm2.Caption    :=Darkness2CDM2String(Darkness)+' cd/m²';
  Form1.DisplayedNSU.Caption     :=Darkness2NSUString(Darkness) +' NSU';

end;

function GetReading(): String;
const
   {$WRITEABLECONST ON}
   IsInside:Boolean=False;
   {$WRITEABLECONST OFF}
var
   //result:string;
   pieces: TStringList;
   compose:string;
   NoResultString:String = 'No Response:'+sLineBreak+
     ' - Check Report Interval.'+sLineBreak+
     ' - Check Accessories.';
   Reading:Double; //Averaged reading.
   ReadingUA:Double; //Unaveraged reading.
   ExpectedPieces:Integer;
   Statustext:String;
   command:String; //Command to be sent
begin

     if IsInSide then begin
       StatusMessage('Is inside GetReading already.');
       Exit;
     end;
     IsInside:=True;
     try

     if not gettingreading then begin
       ExpectedPieces:=6;
       //Reset field indices each reading; set below only when those fields exist.
       ReadingUAField:=-1;
       FreshnessField:=-1;
       SnowLEDField:=-1;
       gettingreading:=True;
       //StatusMessage('GetReading called.');//debug

       //Clear out existing results
       Form1.ReadingListBox.Items.Clear;

  //Try to ensure a model version has been found.
  if SelectedModel=0 then
     GetVersion;

  //Get response to "Request"
  pieces := TStringList.Create;
  pieces.StrictDelimiter := true; //Do not parse spaces

  pieces.Delimiter := ',';

  if rotstage then
    command:='ux' //Rotational stage requires unaveraged values.
  else
    if ((SelectedModel=model_DLS) and SnowLoggingEnabled and (StrToIntDef(SelectedFeature,0)>=58)) then begin
      command:='u1x'; //Snow model: unaveraged value + stale flag. The snow LED
                      //toggles every record, so the averaged value blends LED-on
                      //and LED-off light; use the unaveraged single-window value.
      Inc(ExpectedPieces,2); //Account for unaveraged value + stale flag.
      ReadingUAField:=ExpectedPieces-2;
      FreshnessField:=ExpectedPieces-1;
    end
  else
    if ((Freshness) and (StrToInt(SelectedFeature)>=58)) then begin
      command:='r1x'; //Get averaged, unaveraged, Stale flag.
      Inc(ExpectedPieces,2); //Account for Freshness reading
      ReadingUAField:=ExpectedPieces-2;
      FreshnessField:=ExpectedPieces-1;
    end
  else begin
       if RawFrequencyEnabled then begin ;
           command:='rFx'; //get raw frequency readings also.
       end
       else begin
           command:='rx'; //Normally the averaged values are desired.
       end;
  end;

  //Are we expecting Snow LED status
  if ((SelectedModel=model_DLS) and SnowLoggingEnabled) then begin
    Inc(ExpectedPieces);
    SnowLEDField:=ExpectedPieces-1;
  end;

  result:=SendGet(command); //Normally the averaged values are desired.

  pieces.DelimitedText := result;

  case SelectedModel of
    model_LELU,model_LR,model_DL,model_GPS, model_DLS, model_RPi, model_LL: begin
       if (pieces.count>=ExpectedPieces) then begin
           Reading:=StrToFloatDef(AnsiMidStr(pieces.Strings[1],1,6),0,FPointSeparator);
           DisplayedReading(Reading);
           Form1.ReadingListBox.Items.Add(Format('  Reading: %1.2fmpsas',[Reading]));
           Form1.ReadingListBox.Items.Add(Format('Frequency: %dHz',      [StrToIntDef  (AnsiMidStr(pieces.Strings[2],1,10),0)]));
           Form1.ReadingListBox.Items.Add(Format('  Counter: %dcounts',  [StrToIntDef  (AnsiMidStr(pieces.Strings[3],1,10),0)]));
           Form1.ReadingListBox.Items.Add(Format('     Time: %1.3fs',    [StrToFloatDef(AnsiMidStr(pieces.Strings[4],1,11),0,FPointSeparator)]));
           Form1.ReadingListBox.Items.Add(Format('     Tint: %1.1fC',    [StrToFloatDef(AnsiMidStr(pieces.Strings[5],0,6),0,FPointSeparator)]));

           //Freshness
           if (command='r1x') or (command='u1x') then begin
               ReadingUA:=StrToFloatDef(AnsiMidStr(pieces.Strings[ReadingUAField],1,6),0,FPointSeparator);
               Form1.ReadingListBox.Items.Add(Format('  Reading: %1.2fmpsas unaveraged',[ReadingUA]));
               case pieces.Strings[FreshnessField] of
                 'F': Statustext:='Fresh frequency';
                 'P': Statustext:='Fresh period';
                 'S': Statustext:='Stale reading';
                 else Statustext:='????';
               end;
               Form1.ReadingListBox.Items.Add(Format('   Status: %s',[Statustext]));
           end;

           //Indicate if Snow LED is on/off
           if ((SelectedModel=model_DLS) and SnowLoggingEnabled) then begin
             case pieces.Strings[SnowLEDField] of
               'S': Statustext:='On';
               'D': Statustext:='Off';
               else
                 Statustext:='???';
             end;
             Form1.ReadingListBox.Items.Add(Format(' Snow LED: %s',[Statustext]));
          end;

       end
       else begin
           Form1.ReadingListBox.Items.Add(NoResultString);
           StatusMessage('GetReading failed. Sent: '+command+' Received: '+result);
           end;
    end;

    model_V: begin
       if ((pieces.count=6) or (pieces.count=8)) then begin
           Reading:=StrToFloatDef(AnsiMidStr(pieces.Strings[1],1,6),0,FPointSeparator);
           DisplayedReading(Reading);
           Form1.ReadingListBox.Items.Add(Format('  Reading: %1.2fmpsas', [Reading]));
           Form1.ReadingListBox.Items.Add(Format('Frequency: %dHz',       [StrToIntDef  (AnsiMidStr(pieces.Strings[2],1,10),0)]));
           Form1.ReadingListBox.Items.Add(Format('  Counter: %dcounts',   [StrToIntDef  (AnsiMidStr(pieces.Strings[3],1,10),0)]));
           Form1.ReadingListBox.Items.Add(Format('     Time: %1.3fs',     [StrToFloatDef(AnsiMidStr(pieces.Strings[4],1,11),0,FPointSeparator)]));
           Form1.ReadingListBox.Items.Add(Format('     Tint: %1.1fC',     [StrToFloatDef(AnsiMidStr(pieces.Strings[5],0,6),0,FPointSeparator)]));
           GetAccel();
           //Ax:=-1.0 * StrToFloatDef(pieces.Strings[6],0);
           //Ay:=StrToFloatDef(pieces.Strings[7],0);
           //Az:=StrToFloatDef(pieces.Strings[8],0);
           //Form1.ReadingListBox.Items.Add(Format('    Accel: %6.0fx %6.0fy %6.0fz',[Ax,Ay,Az]));
           //Mx:=StrToFloatDef(pieces.Strings[ 9],0);
           //My:=StrToFloatDef(pieces.Strings[10],0);
           //Mz:=StrToFloatDef(pieces.Strings[11],0);
           GetMag(False);
           //Form1.ReadingListBox.Items.Add(Format('      Mag: %6.0fx %6.0fy %6.0fz',[Mx,My,Mz]));

           NormalizeAccel(); //Compute acceleration values (In the future, this may be done inside the PIC)
           //Form1.ReadingListBox.Items.Add(Format(' Altitude: %4.0f°',[radtodeg(arcsin(-1.0*Ax1))]));
           Form1.ReadingListBox.Items.Add(Format(' Altitude: %4.1f°',[ComputeAltitude(Ax1, Ay1, Az1)]));

           ComputeAzimuth();
           Heading:=radtodeg(arctan2(-1*Mz2,Mx2))+180;
           Form1.ReadingListBox.Items.Add(Format('  Azimuth: %4.0f°',[Heading]));
         end
       else
           Form1.ReadingListBox.Items.Add(NoResultString);
    end;

    model_GDM: begin         //Magnetometer
       if pieces.count=3 then begin
         Form1.ReadingListBox.Items.Add(Format('M1: %dc',    [StrToIntDef(AnsiMidStr(pieces.Strings[1],1,10),0)]));
         if StrToIntDef(pieces.Strings[2],0) < 32768 then
            Form1.ReadingListBox.Items.Add(Format('T1: %10.7fC',[StrToFloatDef(pieces.Strings[2],0,FPointSeparator)/128.0]))
         else
             Form1.ReadingListBox.Items.Add(Format('T1: %10.7fC',[(StrToFloatDef(pieces.Strings[2],0,FPointSeparator)-65536.0)/128.0]));

       end
       else
         Form1.ReadingListBox.Items.Add(NoResultString);
    end;
    model_TC: begin         //Temperature chamber
       if pieces.count=3 then begin
         Form1.ReadingListBox.Items.Add(Format('M1: %dc',    [StrToIntDef(AnsiMidStr(pieces.Strings[1],1,10),0)]));
         if StrToIntDef(pieces.Strings[2],0) < 32768 then
            Form1.ReadingListBox.Items.Add(Format('T1: %10.7fC',[StrToFloatDef(pieces.Strings[2],0,FPointSeparator)/128.0]))
         else
             Form1.ReadingListBox.Items.Add(Format('T1: %10.7fC',[(StrToFloatDef(pieces.Strings[2],0,FPointSeparator)-65536.0)/128.0]));

       end
       else
         Form1.ReadingListBox.Items.Add(NoResultString);
    end;
    model_ADA: begin        //ADA
       if pieces.count=8 then begin
           Form1.ReadingListBox.Items.Add(Format('Frequency: %dHz',      [StrToIntDef(AnsiMidStr(pieces.Strings[1],1,10),0)]));
           Form1.ReadingListBox.Items.Add(Format(' Counter1: %dcounts',  [StrToIntDef(AnsiMidStr(pieces.Strings[2],1,10),0)]));
           Form1.ReadingListBox.Items.Add(Format('    Time1: %1.3fs',    [StrToFloatDef(AnsiMidStr(pieces.Strings[3],1,11),0,FPointSeparator)]));
           Form1.ReadingListBox.Items.Add(Format(' Counter2: %dcounts',  [StrToIntDef(AnsiMidStr(pieces.Strings[4],1,10),0)]));
           Form1.ReadingListBox.Items.Add(Format('    Time2: %1.3fs',    [StrToFloatDef(AnsiMidStr(pieces.Strings[5],1,11),0,FPointSeparator)]));
         end
       else
           Form1.ReadingListBox.Items.Add(NoResultString);
    end;

    model_C: begin         //Colour
       if ((pieces.count=9) or (pieces.count=11)) then begin
           Form1.ReadingListBox.Items.Add(Format('  Reading: %1.2fmpsas',[StrToFloatDef(AnsiMidStr(pieces.Strings[1],1,6),0,FPointSeparator)]));
           Form1.ReadingListBox.Items.Add(Format('Frequency: %dHz',      [StrToIntDef  (AnsiMidStr(pieces.Strings[2],1,10),0)]));
           Form1.ReadingListBox.Items.Add(Format('  Counter: %dcounts',  [StrToIntDef  (AnsiMidStr(pieces.Strings[3],1,10),0)]));
           Form1.ReadingListBox.Items.Add(Format('     Time: %1.3fs',    [StrToFloatDef(AnsiMidStr(pieces.Strings[4],1,11),0,FPointSeparator)]));
           Form1.ReadingListBox.Items.Add(Format('     Tint: %1.1fC',    [StrToFloatDef(AnsiMidStr(pieces.Strings[5],0,6),0,FPointSeparator)]));

           //Report on colour settings
           Unit1.ColourUpdating:=True; //Prevent colour radios from self triggering.
             Unit1.SelectedColourScaling:=StrToInt(pieces.Strings[6]);
             Unit1.Form1.ColourScalingRadio.ItemIndex:=Unit1.SelectedColourScaling;
             case Unit1.SelectedColourScaling of
               0: compose:='Power down';
               1: compose:='2% Frequency scaling';
               2: compose:='20% Frequency scaling';
               3: compose:='100% Frequency scaling';
               else compose:='Error :'+ pieces.Strings[6];
             end;
             Form1.ReadingListBox.Items.Add(Format('  Scaling: %s',        [compose]));

             Unit1.SelectedColour:=StrToInt(pieces.Strings[7]);{*** put in variable}
             Unit1.Form1.ColourRadio.ItemIndex:=Unit1.SelectedColour;
             case Unit1.SelectedColour of
               0: compose:='Red';
               1: compose:='Blue';
               2: compose:='Clear';
               3: compose:='Green';
               else compose:='Error :'+ pieces.Strings[7];
             end;
             Form1.ReadingListBox.Items.Add(Format('   Colour: %s',        [compose]));

             {Get colour cycling flag, assume Fixed}
             case pieces.Strings[8] of
                'C': begin
                       ColourCyclingFlag:=True;
                       Unit1.Form1.ColourCyclingRadio.ItemIndex:=1;
                       Unit1.Form1.ColourRadio.Visible:=False;
                     end;
               else begin //Assume F (Fixed colour)
                       ColourCyclingFlag:=False;
                       Unit1.Form1.ColourCyclingRadio.ItemIndex:=0;
                       Unit1.Form1.ColourRadio.Visible:=True;
                     end;
               end;


             { Report freshness }
             if pieces.count=11 then begin
               ReadingUA:=StrToFloatDef(AnsiMidStr(pieces.Strings[9],1,6),0,FPointSeparator);
               Form1.ReadingListBox.Items.Add(Format('  Reading: %1.2fmpsas unaveraged',[ReadingUA]));
               case pieces.Strings[10] of
                 'F': Statustext:='Fresh frequency';
                 'P': Statustext:='Fresh period';
                 'S': Statustext:='Stale reading';
               end;
               Form1.ReadingListBox.Items.Add(Format('   Status: %s',[Statustext]));
            end;

           Unit1.ColourUpdating:=False; //Allow colour radios to be triggered.

         end

       else
           Form1.ReadingListBox.Items.Add('Expected 8 fields, got '+IntToStr(pieces.Count)+'.');

    end;
  otherwise
     Form1.ReadingListBox.Items.Add('Could not get version.');
  end;

  gettingreading:=False;
  end; //end of checking if getreading was called
 if Assigned(pieces) then FreeAndNil(pieces);

     finally
         IsInside:=False;
     end;

end;

procedure GetVersion;
const
  {$WRITEABLECONST ON}
      IsInside:Boolean=False;
  {$WRITEABLECONST OFF}
  {$IFDEF Linux}
      READ_BYTES = 2048;
  {$ENDIF}
var
   ixresult:string; {Information settings}
   Intvresult:string; {Interval settings}
   pieces,pieces2: TStringList;
   ResultCount:Integer;
   OurProcess: TProcess;
   MemStream: TMemoryStream;
   OutputLines: TStringList;
   NumBytes: LongInt;
   BytesRead: LongInt;
   RTCType:Integer=0;
   SnowResult:String;
begin
     if IsInSide then begin
       //StatusMessage('Is inside GetVersion already.');
       Exit;
     end;
     IsInside:=True;
     try

       StatusMessage('GetVersion called.');

       //Clear out existing results
       Form1.VersionListBox.Items.Clear;

       ResultCount:=0;

       pieces := TStringList.Create;
       pieces.Delimiter := ',';
       pieces2 := TStringList.Create;
       pieces2.Delimiter := ',';

       ixresult:=SendGet('ix');
       pieces.DelimitedText := ixresult;

       //Check size of array. 5 Sections normally.
       //There is a case where other software might be accessing the device,
       //  another immediate request should get the proper data, and a warning
       //  that possibly other software is accessing the device.
       if ((pieces.Count>=5) and (pieces.Strings[0]='i')) then begin
              ResultCount:=1;
         end
       else begin //Try once again. Sometimes dual responses get through here.
           ixresult:=SendGet('ix');
           pieces.DelimitedText := ixresult;
            if ((pieces.Count>=5) and (pieces.Strings[0]='i')) then
                 ResultCount:=2
            else begin
               { Check failed dialout usb connection; groups, suggest adduser then relogin. or administration->users and groups->manage groups->dialout->properties}
                 {$ifdef Linux}

                   MemStream := TMemoryStream.Create;
                   BytesRead := 0;

                   pieces := TStringList.Create;
                   pieces.Delimiter := '=';
                   OurProcess := TProcess.Create(nil);
                   OurProcess.Executable := 'groups';
                   OurProcess.Options := [poUsePipes];
                   OurProcess.Execute;
                   while OurProcess.Running do begin
                     MemStream.SetSize(BytesRead + READ_BYTES);
                     NumBytes := OurProcess.Output.Read((MemStream.Memory + BytesRead)^, READ_BYTES);
                     if NumBytes > 0
                     then begin
                       Inc(BytesRead, NumBytes);
                     end
                     else begin
                       Sleep(100);
                     end;
                   end;
                   repeat
                     MemStream.SetSize(BytesRead + READ_BYTES);
                     NumBytes := OurProcess.Output.Read((MemStream.Memory + BytesRead)^, READ_BYTES);
                     if NumBytes > 0 then begin
                       Inc(BytesRead, NumBytes);
                     end;
                   until NumBytes <= 0;
                     if BytesRead > 0 then
                        WriteLn; //blank line written for linux only.
                     MemStream.SetSize(BytesRead);

                     OutputLines := TStringList.Create;
                     OutputLines.LoadFromStream(MemStream);

                     if not AnsiContainsStr(OutputLines[0],'dialout') then begin
                         Form1.VersionListBox.Items.Add('This user is not part of the');
                         Form1.VersionListBox.Items.Add('  dialout group.');
                         Form1.VersionListBox.Items.Add('Use the "adduser", or');
                         Form1.VersionListBox.Items.Add('"sudo usermod -aG uucp username", or:');
                         Form1.VersionListBox.Items.Add('  administration->');
                         Form1.VersionListBox.Items.Add('    users and groups->');
                         Form1.VersionListBox.Items.Add('      manage groups->');
                         Form1.VersionListBox.Items.Add('        dialout->');
                         Form1.VersionListBox.Items.Add('          properties');
                         Form1.VersionListBox.Items.Add('add user name, then re-login');
                         StatusMessage('User not in dialout group.');
                       end;

                     OutputLines.Free;
                     OurProcess.Free;
                     MemStream.Free;
                 {$else}
                   Form1.VersionListBox.Items.Add('Other software may be ');
                   Form1.VersionListBox.Items.Add('accessing the device!');
                   StatusMessage('Other software may be accessing the device!');
                 {$endif}
               end;
           end;

       if ResultCount>0 then begin
           SelectedProtocol:=IntToStr(StrToIntDef(pieces.Strings[1],0));
           Form1.VersionListBox.Items.Add('Protocol: '+ SelectedProtocol);
           SelectedModel:=StrToIntDef(pieces.Strings[2],0);
           Case SelectedModel of
             model_ADA : SelectedModelDescription:='ADA';
             model_LELU: begin
               if Form1.CommNotebook.PageIndex=0 then
                 SelectedModelDescription:='SQM-LU'
               else
                 SelectedModelDescription:='SQM-LE';
               end;
             model_C : SelectedModelDescription:='SQM-C';
             model_LR: SelectedModelDescription:='SQM-LR';
             model_DL: begin
               if Form1.CommNotebook.PageIndex=0 then
                 SelectedModelDescription:='SQM-LU-DL'
               else
                 SelectedModelDescription:='SQM-W';
               end;
             model_GPS: SelectedModelDescription:='SQM-LU-GPS';
             model_GDM: SelectedModelDescription:='Magnetometer';
             model_TC : SelectedModelDescription:='Temp. Chamber';
             model_V  : SelectedModelDescription:='SQM-LU-DL-V';
             model_DLS: SelectedModelDescription:='SQM-LU-DLS';
             model_RPI: SelectedModelDescription:='SQM-RPi';
             model_LL: SelectedModelDescription:='SQM-LL';

           otherwise
             SelectedModelDescription:='Unknown';
           end;

           //Check if Selected model has an RTC
           case SelectedModel of
               model_DL, model_GPS, model_V, model_DLS:
             SelectedHasRTC:=True
           else
             SelectedHasRTC:=False;
           end;

           SelectedFeature:=IntToStr(StrToIntDef(pieces.Strings[3],0));
           SelectedUnitSerialNumber:=IntToStr(StrToIntDef(pieces.Strings[4],0));

           //Check for Lens model types;
           // L = default (as entered above)
           // 2 = 3D holder, Half-Ball lens, Interfernece filter
           // '' = no lens (remove the L from the description
           if StrToIntDef(SelectedFeature,0)>=35 then begin //Enable lens model selections
             LHFCheck('');
             //Results sent as m_x, received as m_,123
             //SelectedLH:=StrToInt(AnsiMidStr(SendGet('m0x'),4,3));
             //SelectedLens:=StrToInt(AnsiMidStr(SendGet('m1x'),4,3));
             //SelectedFilter:=StrToInt(AnsiMidStr(SendGet('m2x'),4,3));
             //replace L in 5th place with L2
             if ((SelectedLH=2) and (SelectedLens=2) and (SelectedFilter=2)) then
               SelectedModelDescription:=StuffString(SelectedModelDescription,5,1,'L2');
             //replace L with blank (no lens or holder, only Hoya filter)
             if ((SelectedLH=0) and (SelectedLens=0) and (SelectedFilter=1)) then
               SelectedModelDescription:=AnsiLeftStr(SelectedModelDescription,4) + AnsiRightStr(SelectedModelDescription,Length(SelectedModelDescription)-5);
             //replace L with blank (no lens, no holder, no filter), and add suffix -NF
             if ((SelectedLH=0) and (SelectedLens=0) and (SelectedFilter=0)) then begin
               SelectedModelDescription:=AnsiLeftStr(SelectedModelDescription,4) + AnsiRightStr(SelectedModelDescription,Length(SelectedModelDescription)-5);
               SelectedModelDescription:=SelectedModelDescription+'-NF';
               end;
           end;

           { Check for RTC type }
           if SelectedHasRTC then begin
              if StrToIntDef(SelectedFeature,0)>=38 then begin;
                pieces2.DelimitedText:=SendGet('Lvx');
                if pieces2.Count>0 then begin
                  SelectedRTC:=pieces2.Strings[1];
                  case StrToIntDef(SelectedRTC,0) of
                      0: begin
                         RTCType:=0;
                      end;
                      1: begin
                         RTCType:=1;
                         SelectedModelDescription:=SelectedModelDescription+'-R1';
                      end;
                      2: begin
                         RTCType:=2;
                         SelectedModelDescription:=SelectedModelDescription+'-R2';
                      end;
                  otherwise
                      RTCType:=-1;
                  end;
                end;
              end;
           end;

           //The continuous functions are available on feature version is 40 and higher
           if StrToIntDef(SelectedFeature,0)>=40 then begin
              Form1.ContCheckGroup.Visible:=True;//Show group of options
              Form1.ContCheck('Yx');//Show continuous selections
              Form1.DataNoteBook.Page[10].TabVisible:=True; //Show Accessory tab
           end else begin
              Form1.ContCheckGroup.Visible:=False;
              if Form1.DataNoteBook.ActivePageIndex=10 then
                Form1.DataNoteBook.ActivePageIndex:=0;//must not be on active page othewise UDM crashes.
              Form1.DataNoteBook.Page[10].TabVisible:=False; //Hide Accessory tab.
           end;

           //Snow settings
           if StrToIntDef(SelectedFeature,0)>=62 then begin
             SnowResult:=SendGet('A5x');
             Unit1.Form1.SnowLEDStatus(SnowResult);
           end;

           Form1.VersionListBox.Items.Add('   Model: '+ IntToStr(SelectedModel)+ ' ('+ SelectedModelDescription + ')');

           Form1.VersionListBox.Items.Add(' Feature: '+ SelectedFeature);

           DLHeaderForm.SerialNumber.Text:=SelectedUnitSerialNumber;

           { Read initialization file. }
           DLHeaderForm.ReadINI;

           Form1.VersionListBox.Items.Add('  Serial: '+ SelectedUnitSerialNumber);

           // List RTC type in version box if new RTC
           if SelectedHasRTC then begin
             case RTCType of
                0: Form1.VersionListBox.Items.Add('     RTC: '+'DS1305');
                1: Form1.VersionListBox.Items.Add('     RTC: '+'DS3234');
                2: Form1.VersionListBox.Items.Add('     RTC: '+'DS1390');
             otherwise
                Form1.VersionListBox.Items.Add('     RTC: '+'Unknown');
             end;
           end;


           //Vector tab: only for SQM-LU-DL-V
           if (SelectedModel=model_V) then
             begin
                 Form1.DataNoteBook.Page[9].TabVisible:=True;
             end;

         { Colour model }
         if SelectedModel=model_C then begin
           Unit1.ColourUpdating:=True;
           Unit1.Form1.ColourControls.Visible:=True;
           SelectedColourScaling:=-1;
           Unit1.Form1.ColourScalingRadio.ItemIndex:=SelectedColourScaling;
           SelectedColour:=-1;
           Unit1.Form1.ColourRadio.ItemIndex:=SelectedColour;
           Unit1.ColourUpdating:=False;
         end
         else
           Unit1.Form1.ColourControls.Visible:=False;

         //Datalogging tab: for: SQM-LU-DL, SQM-LU-GPS, SQM-LU-DL-V
         case SelectedModel of
           model_DL, model_GPS, model_V, model_DLS: begin
              form1.DLGetSettings();
              Form1.DataNoteBook.Page[4].TabVisible:=True;
              DataLoggingAvailable:=True;
             end;
           else begin
                  Form1.DataNoteBook.Page[4].TabVisible:=False;
                  DataLoggingAvailable:=False;
                end;
           end;


         //GPS tab: only for SQM-LU-GPS
         if (SelectedModel=model_GPS) then
              Form1.DataNoteBook.Page[6].TabVisible:=True
         else
              Form1.DataNoteBook.Page[6].TabVisible:=False;

         //StatusMessage('Checking lock visibility');//debug
         CheckLockVisibility();
         //StatusMessage('Checked lock visibility');//debug
       end;

       //Interval settings
       //StatusMessage('Checking Ix');//debug
       Intvresult:=SendGet('Ix');
       //StatusMessage('Checked Ix');//debug
       Form1.ParseReportInterval(Intvresult);

       gettingversion:=False;

       Application.ProcessMessages;

   UpdateCalReport;

   // header button enable if serial number is not null
   //if DLHeaderForm.SerialNumber.Text<>'' then
   //  Form1.HeaderButton.Enabled:=True;

   if Assigned(pieces) then FreeAndNil(pieces);
   if Assigned(pieces2) then FreeAndNil(pieces2);

     finally
         IsInside:=False;
     end;

     if StrToIntDef(SelectedUnitSerialNumber,0)>0 then begin
       StatusMessage('GetVersion result: ' + ixresult);
       StatusMessage('Reading Interval Settings: ' + Intvresult);
       end
     else begin
       StatusMessage('GetVersion: No serial number');
       end;
end;

{Write new genertic or appended serial-number-specific logfile ex. SN1234.log}
procedure StatusMessage(Mstring:string);
begin
   Form1.StatusBar1.Panels.Items[0].Text:=Mstring;
   if not(Mstring='') then begin
        if StrToIntDef(SelectedUnitSerialNumber,0)>0 then begin {A serial number has been selected}
          SNLogFileName:=RemoveMultiSlash(appsettings.LogsDirectory + DirectorySeparator)+'SN'+SelectedUnitSerialNumber+'.log';
          AssignFile(SNLogFile,SNLogFileName);
          if FileExists(SNLogFileName) then
            Append(SNLogFile)
          else
            Rewrite(SNLogFile);
          if IOResult <> 0 then
          begin
            writeln('Unable to open file: ',SNLogFileName);
            Exit;
          end;
          WriteLn(SNLogFile,FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',NowUTC()) + ' : SN ' + SelectedUnitSerialNumber + ' : ' + Mstring);
          Flush(SNLogFile);
          CloseFile(SNLogFile);
        end else begin {No serial number has been selected}
          { Write to udm.log logfile}
             Append(UDMLogFile); //File is opened for write, but NOT emptied. Any text written to it is appended.
             WriteLn(UDMLogFile,FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',NowUTC()) + ' : ' + Mstring); //Append message.
             Flush(UDMLogFile);
             Close(UDMLogFile); //Allow other instances to write to file.
        end;
        {Write to log viewer screen}
        ViewedLog.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',NowUTC()) + ' : ' + SelectedUnitSerialNumber + ' : ' + Mstring);
        if ViewingLog then begin
           Form5.SynEdit1.Lines.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',NowUTC()) + ' : ' + SelectedUnitSerialNumber + ' : ' + Mstring);
           {Automatically scroll up.}
           Form5.SynEdit1.TopLine:=Form5.SynEdit1.Lines.Count-Form5.SynEdit1.LinesInWindow+1;
           Form5.SynEdit1.Refresh;
        end;
      end;
   Application.ProcessMessages;
end;

procedure ClearLockVisibility;
begin
  Form1.CheckLockResult.Text:='';
  Form1.CheckLockButton.Visible:=False;
  Form1.CheckLockResult.Visible:=False;
  Form1.bXPortDefaults.Enabled:=False;
end;

procedure CheckLockVisibility;
const
   {$WRITEABLECONST ON}
   IsInside:Boolean=False;
   {$WRITEABLECONST OFF}
begin
   if IsInSide then begin
     StatusMessage('Is inside CheckLockVisibility already.');
     Exit;
   end;
   IsInside:=True;
   try

      StatusMessage('CheckLockVisibility()');

      //CheckLock enable/disable
      Form1.CheckLockResult.Text:='';
      if (((SelectedModel=model_LELU) or (SelectedModel=model_C)) and (Form1.CommNotebook.PageIndex=1)or (SelectedModel=model_LL)) then //SQM-LE/U or Colour, and Ethernet
         begin
           Form1.CheckLockButton.Visible:=True;
           Form1.CheckLockResult.Visible:=True;
         end
      else
          begin
           Form1.CheckLockButton.Visible:=False;
           Form1.CheckLockResult.Visible:=False;
          end;

      //Check XPort default button enable/disable
      if (Form1.CommNotebook.PageIndex=1) then //Ethernet
         begin
           Form1.bXPortDefaults.Enabled:=True;
         end
      else
          begin
           Form1.bXPortDefaults.Enabled:=False;
          end;

   finally
       IsInside:=False;
   end;

end;

procedure UpdateCalReport;
var
   AccCalPos: Integer; //Accelerometer position
begin
     Form1.ConfRecWarning.Caption:='';

     Application.ProcessMessages;
     if (Form1.FoundDevices.SelCount=0) and (Form1.CommNotebook.PageIndex<>2) then //not for RS232
       begin
            Form1.Panel1.Canvas.Clear;
       end
     else
       begin
         PrintingLine:=0;
         PrintLine(SelectedModelDescription+' Calibration data ');
         PrintLine(' Report Date ', format(' %s',[FormatDateTime('yyyy-mm-dd',Now())]));
         PrintLine(' Serial Number ', format(' %s',[SelectedUnitSerialNumber]));
         //check if USB device:
         if ((Form1.CommNotebook.PageIndex=0) and not ((SelectedModel=model_LR)or (SelectedModel=model_LL))) then    // USB device but not RS232 or LL model
            PrintLine(' USB Serial Number ', format(' %s',[Form1.USBSerialNumber.text]));
         //check if Ethernet device:
         if (Form1.CommNotebook.PageIndex=1) then
            PrintLine(' MAC ', format(' %s',[Form1.EthernetMAC.text]));
         PrintLine(' Model Number ', format(' %s',[Inttostr(SelectedModel) + ' ('+SelectedModelDescription+')']));
         PrintLine(' Feature version ', format(' %s',[SelectedFeature]));
         PrintLine(' Protocol version ', format(' %s',[SelectedProtocol]));
         if SelectedHasRTC then begin
           if StrToIntDef(SelectedFeature,0)>=38 then begin
             case StrToIntDef(SelectedRTC,0) of
                 0: PrintLine(' Real Time Clock ', ' DS1305 (±20ppm)');
                 1: PrintLine(' Real Time Clock ', ' DS3234 (±3.5ppm)');
                 2: PrintLine(' Real Time Clock ', ' DS1390 (±5ppm)');
                 otherwise
                   PrintLine(' Real Time Clock ', ' Unknown');
             end;
           end;
           PrintLine(' Data logging capacity ', Format(' %d ',[DLEStorageCapacity])+' records');
           //Warning if EEPROM is missing:
           if DLEStorageCapacity<1000 then begin
             Form1.ConfRecWarning.Caption:='Records';
             Form1.ConfRecWarning.Color:=clRed;
             Form1.ConfRecWarning.Font.Color:=clWhite;
           end
           else begin
              Form1.ConfRecWarning.Caption:='';
              Form1.ConfRecWarning.Color:=clNone;
              Form1.ConfRecWarning.Font.Color:=clNone;
           end;
         end;

         PrintLine(' Light calibration offset ', Format(' %2.2f ',[ConfCalmpsas])+' mags/arcsec²');
         PrintLine(' Light calibration temperature ', Format(' %2.1f ',[ConfCalLightTemp])+' °C');
         PrintLine(' Dark calibration period ', Format(' %2.3f ',[ConfCalPeriod])+' seconds');
         PrintLine(' Dark calibration temperature ', Format(' %2.1f ',[ConfCalDarkTemp])+' °C');
         PrintLine(' Calibration offset ', ' 8.71 mags/arcsec² ');
         if SelectedModel=model_V then begin
             for AccCalPos:=1 to 6 do begin
                PrintLine(format(' Acceleration position %d ',[AccCalPos]),
                   format('  %6.0f  %6.0f  %6.0f ',[
                        w.getv(AccCalPos-1, 0),
                        w.getv(AccCalPos-1, 1),
                        w.getv(AccCalPos-1, 2)]));
             end;
             PrintLine(' Magnetic maximum XYZ ',format(' %7.0f %7.0f %7.0f',[Mxmax,Mymax,Mzmax]));
             PrintLine(' Magnetic minimum XYZ ',format(' %7.0f %7.0f %7.0f',[Mxmin,Mymin,Mzmin]));
         end;

       end;
end;
procedure PrintLine(LabelText:String; DataText: String='');
var
  H,W,PageTop,PageWidth,CellMargin,CH: Integer;
  WhereTo:TCanvas;
begin
     if CalPrint then //output=printer
       begin
         WhereTo:=Printer.Canvas;
         PageTop:=500;
         CellMargin:=27;
         PageWidth:=Printer.PageWidth;
         H := WhereTo.TextHeight(LabelText);
         W := WhereTo.TextWidth(LabelText);
         CH:=H+2*CellMargin;
         //Draw the rectangle around the text:
         WhereTo.Rectangle(
            Rect(  PageWidth div 5,
                   PageTop + PrintingLine * CH,
                   4 * (PageWidth div 5),
                   PageTop + PrintingLine * CH + CH+1));
       end
     else //output = preview on screen
       begin
            WhereTo:=Form1.Panel1.Canvas;
            PageTop:=1;
            CellMargin:=5;
            PageWidth:=Form1.Panel1.Width;
            H := WhereTo.TextHeight(LabelText);
            W := WhereTo.TextWidth(LabelText);
            CH:=H+2*CellMargin;
            //Draw the rectangle around the text:
            WhereTo.Rectangle(
               Rect(  1,
                      PageTop + PrintingLine * CH,
                      PageWidth-1,
                      PageTop + PrintingLine * CH + CH+1));
       end;

     //Check for two parameters
     if Length(DataText)>0 then begin
       //Draw centerline
       WhereTo.Line(
                 PageWidth div 2,
                 PageTop + PrintingLine * CH,
                 PageWidth div 2,
                 PageTop + PrintingLine * CH + CH);
       //Place Left text:
       WhereTo.TextOut((
                 PageWidth div 2) - W - CellMargin,
                 PageTop + (PrintingLine * CH) + CellMargin,
                 LabelText);
       //Place Right text:
       WhereTo.TextOut((
                 PageWidth div 2) + CellMargin,
                 PageTop + (PrintingLine * CH) + CellMargin,
                 DataText);
       end
     else begin //one text paramater
       //Place the text:
       WhereTo.TextOut((
                 PageWidth div 2) - (W div 2),
                 PageTop + (PrintingLine * CH) + CellMargin,
                 LabelText);
     end;
     Inc(PrintingLine);
end;

function FixDate(incoming:AnsiString): AnsiString;
{ Fix the date from the DataLogging unit by converting the DOW value to
  a readable string. }
var
   dowval:Integer;
   weekday: Array[1..7] of string = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
begin
  dowval:=StrToInt(AnsiMidStr(incoming,10,1));
  if ((dowval>=1) and (dowval<=7)) then
    FixDate:=AnsiMidStr(incoming,1,9)+weekday[dowval]+AnsiMidStr(incoming,11,9)
  else
    FixDate:=AnsiMidStr(incoming,1,9)+'???'+AnsiMidStr(incoming,11,9);
end;
{Looks through command line pramaters for a command.
 Returns true if the comand is found.
 Parse the comma separated command into the global ParameterValue array.
 Startup options are also availble through the startup setting.
 Both, Paramater settings, and Startup options are considered here.
 }
function ParameterCommand(Command:String): Boolean;
var
   ParmeterPointer:Integer=1;
   StartupParmeterPointer:Integer=0;
   pieces: TStringList;
begin
  ParameterCommand:=False; { Initially assumes nothing was found.}

  { Read command line parameters. }
  pieces := TStringList.Create;
  pieces.Delimiter := ',';
  if (Paramcount > 0) then begin
      while ((ParmeterPointer>=1) and (ParmeterPointer<=Paramcount)) do begin
           pieces.DelimitedText:=ParamStr(ParmeterPointer);
           if pieces.Strings[0]=Command then begin
               ParameterValue.Assign(pieces);
               ParameterCommand:=True;
               ParmeterPointer:=0;
             end
           else
             inc(ParmeterPointer);
        end;
  end;
  pieces.Destroy;

  { Read Startup options. }
  pieces := TStringList.Create;
  pieces.Delimiter := ',';
  if (StartupParamcount > 0) then begin
      while ((StartupParmeterPointer>=0) and (StartupParmeterPointer<=(StartupParamcount-1))) do begin
           pieces.DelimitedText:=StartupParamStrings[StartupParmeterPointer];
           if pieces.Strings[0]=Command then begin
               ParameterValue.Assign(pieces);
               ParameterCommand:=True;
               StartupParmeterPointer:=-1;
             end
           else
             inc(StartupParmeterPointer);
        end;
  end;
  pieces.Destroy;


end;

end.

