unit logcont;

{ TODO : - Barry:
 - LogCont minimize
 - Stop logging at sunrise
 - plot last 24hrs or last 1hr window on same view.
}

{$mode objfpc}

interface

uses

  Classes, SysUtils, LResources, Forms, Controls, Graphics,
  Dialogs, StrUtils, dateutils,
  SynMemo, SynEdit, TAGraph, TASeries, TAIntervalSources,
  fileview, //For opening log files.
  dlheader, //For Timezone conversions.
  appsettings, //For saving and restoring hotkeys.
  LCLType, // For kirtual key definitions used for hotkeys.
  Math, //for float identifier
  StdCtrls, ExtCtrls, Buttons, Grids, Spin, ComCtrls, PairSplitter
  , uplaysound
  , header_utils
  , moon //required for Moon calculations
  //rotstageunit, //Rotational stage form
  , synaser, synautil, blcksock
  , TACustomSeries, TARadialSeries
  , TAChartUtils, TATypes, TACustomSource
  //, TAStyles
  , TATools, TATransformations
  //TBlockSerial for Rotational stage
  , FTPSend, tlntsend
  , LazFileUtils
  , LazSysUtils //For NowUTC()
  //, Types
  ;

type

  { TFormLogCont }

  TFormLogCont = class(TForm)
    AlarmGroup: TGroupBox;
    AlarmSoundEnableCheck: TCheckBox;
    AlarmTestButton: TButton;
    AlarmThresholdFloatSpinEdit: TFloatSpinEdit;
    alert2s: TCheckBox;
    AltAzPlotpanel: TPanel;
    AnnotateButton: TButton;
    AnnotateEdit: TEdit;
    AnnotationGroupBox: TGroupBox;
    AnnotationSheet: TTabSheet;
    CurrentTime: TStaticText;
    CurrentTimeLabel: TLabel;
    FilesLogged: TStaticText;
    FilesLabel: TLabel;
    FixedAutoReadingsToggle: TToggleBox;
    FixedAutoTemperatureReadingsToggle: TToggleBox;
    FixedReadingsGroupBox: TGroupBox;
    FixedTemperatureReadingsGroupBox: TGroupBox;
    FixedTimeMemo: TMemo;
    FromReadingLabel: TLabel;
    FromTemperatureReadingLabel: TLabel;
    FromReadingSpinEdit: TSpinEdit;
    FromTemperatureUnits: TLabel;
    ToTemperatureUnits: TLabel;
    FromTemperatureReadingSpinEdit: TSpinEdit;
    GoToAzimuthDisplay: TStaticText;
    GoToGroup: TGroupBox;
    GoToLogIndicatorX: TShape;
    GoToStepDisplay: TStaticText;
    GoToStepsTotalDisplay: TStaticText;
    GoToZenithDisplay: TStaticText;
    GPSBaudSelect: TComboBox;
    GPSLogIndicator: TStaticText;
    GPSLogIndicatorX: TShape;
    FixedTimeGroupBox: TGroupBox;
    InvertScale: TCheckBox;
    Label1: TLabel;
    Label15: TLabel;
    Label17: TLabel;
    Label18: TLabel;
    FixedTimePageControl: TPageControl;
    FixedTimeRadios: TRadioGroup;
    FixedBlankSheet: TTabSheet;
    FixedFixedheet: TTabSheet;
    FixedTwilightSheet: TTabSheet;
    FixedFromSpinEdit: TSpinEdit;
    FixedToSpinEdit: TSpinEdit;
    From12hrLabel: TLabel;
    BestDarknessLabel: TLabel;
    Label19: TLabel;
    Label20: TLabel;
    ToTemperatureReadingLabel: TLabel;
    ToTemperatureReadingSpinEdit: TSpinEdit;
    TransferReadingTabSheet: TTabSheet;
    To12hrLabel: TLabel;
    Label4: TLabel;
    LabelIn: TLabel;
    Labelinfile: TLabel;
    LCThreshold: TFloatSpinEdit;
    LogFieldNames: TEdit;
    LogFieldNamesLabel: TLabel;
    LogFieldUnits: TEdit;
    LogFieldUnitsLabel: TLabel;
    LogfileNameLabel: TLabel;
    LogFileNameText: TEdit;
    NextRecordAt: TStaticText;
    NextRecordAtLabel: TLabel;
    NextRecordIn: TStaticText;
    NightCheckBox: TCheckBox;
    OpenFileButton: TBitBtn;
    PageControl3: TPageControl;
    RecordsLabel: TLabel;
    RecordsLogged: TStaticText;
    RecordsloggedLabel: TLabel;
    RecordsMissed: TStaticText;
    RecordsMissedLabel: TLabel;
    RecordsViewSynEdit: TSynEdit;
    LookSheet: TTabSheet;
    RepeatProgress: TProgressBar;
    SnoozeButton: TButton;
    SnoozeProgress: TProgressBar;
    StatusSheet: TTabSheet;
    TemperatureCheckBox: TCheckBox;
    ThresholdGroupBox: TGroupBox;
    ThresholdMet: TShape;
    ToReadingLabel: TLabel;
    ToReadingSpinEdit: TSpinEdit;
    TransferPWenable: TCheckBox;
    CoordinatesLabel: TLabel;
    LocationNameLabel: TLabel;
    TestTransfer: TButton;
    TransferDATCheck: TCheckBox;
    TransferPLOTCheck: TCheckBox;
    SplitSpinLabel: TLabel;
    SingleDatCheckBox: TCheckBox;
    SplitGroup: TGroupBox;
    SnowSettleGroup: TGroupBox;
    SnowSettleLabel: TLabel;
    SnowSettleSpin: TSpinEdit;
    MoonPhaseSeries: TLineSeries;
    MoonAxisTransforms: TChartAxisTransformations;
    MoonAxisTransformsAutoScaleAxisTransform1: TAutoScaleAxisTransform;
    MoonSeries: TLineSeries;
    GoToButtonScriptHelp: TButton;
    GPSRMCIncoming: TLabeledEdit;
    GPSGGAIncoming: TLabeledEdit;
    GPSGSVIncoming: TLabeledEdit;
    SplitSpinEdit: TSpinEdit;
    TrRdgHelpButton: TButton;
    TrRdgAddressEntry: TEdit;
    TrRdgEnableCheckBox: TCheckBox;
    TrRdgPortEdit: TEdit;
    TrRdgTestButton: TButton;
    ZenFloatSpinEdit: TFloatSpinEdit;
    AziFloatSpinEdit: TFloatSpinEdit;
    GoToZenAziButton: TButton;
    GoToCommandFileComboBox: TComboBox;
    GoToMachineSelect: TComboBox;
    GetZenAziButton: TButton;
    GoToResultMemo: TMemo;
    FreshAlertTest: TButton;
    GoToBaudSelect: TComboBox;
    LabelBaud: TLabel;
    GoToBaudLabel: TLabel;
    GoToMachinelabel: TLabel;
    AzimuthEditLabel: TLabel;
    GoToPortLabel: TLabel;
    ZenithEditLabel: TLabel;
    ScriptLabel: TLabel;
    LabelStatusAndCommands: TLabel;
    PageControl2: TPageControl;
    PreAlertSound: Tplaysound;
    FreshSound: Tplaysound;
    AlarmSound: Tplaysound;
    PreAlertTestButton: TButton;
    PreReadingAlertGroup: TGroupBox;
    ReadingAlertGroup: TRadioGroup;
    AlertsTabSheet: TTabSheet;
    GoToPortSelect: TComboBox;
    GoToCommandStringGrid: TStringGrid;
    SynScanSheet: TTabSheet;
    TabSheet1: TTabSheet;
    TabSheet2: TTabSheet;
    GoToCommBusy: TTimer;
    TransferCSVCheck: TCheckBox;
    MPSASAxisTransforms: TChartAxisTransformations;
    MPSASAxisTransformsAutoScaleAxisTransform1: TAutoScaleAxisTransform;
    MPSASAxisTransformsLinearAxisTransform1: TLinearAxisTransform;

    TemperatureAxisTransformsAutoScaleAxisTransform: TAutoScaleAxisTransform;
    TemperatureAxisTransforms: TChartAxisTransformations;
    TempSeries: TLineSeries;
    ClearSeries: TLineSeries;
    Label2: TLabel;
    LimitGroup: TGroupBox;
    RedSeries: TLineSeries;
    GreenSeries: TLineSeries;
    BlueSeries: TLineSeries;
    OptionsGroup: TCheckGroup;
    GPSEnable: TCheckBox;
    GPSQualityLabel: TLabeledEdit;
    GPSGGAStatusX: TShape;
    GPSGSVStatusX: TShape;
    GPSSAT1: TLabel;
    GPSSAT10: TLabel;
    GPSSAT11: TLabel;
    GPSSAT12: TLabel;
    GPSSAT2: TLabel;
    GPSSAT3: TLabel;
    GPSSAT4: TLabel;
    GPSSAT5: TLabel;
    GPSSAT6: TLabel;
    GPSSAT7: TLabel;
    GPSSAT8: TLabel;
    GPSSAT9: TLabel;
    GPSSatellites: TLabeledEdit;
    GPSSNR1: TProgressBar;
    GPSSNR10: TProgressBar;
    GPSSNR11: TProgressBar;
    GPSSNR12: TProgressBar;
    GPSSNR2: TProgressBar;
    GPSSNR3: TProgressBar;
    GPSSNR4: TProgressBar;
    GPSSNR5: TProgressBar;
    GPSSNR6: TProgressBar;
    GPSSNR7: TProgressBar;
    GPSSNR8: TProgressBar;
    GPSSNR9: TProgressBar;
    GPSSpeedLabel: TLabeledEdit;
    GPSDateStampLabel: TLabeledEdit;
    GPSValidityLabel: TLabeledEdit;
    GPSLatitudeLabel: TLabeledEdit;
    GPSLongitudeLabel: TLabeledEdit;
    GPSTimer: TIdleTimer;
    GPSSignalGroup: TGroupBox;
    GPS: TTabSheet;
    LabelPort: TLabel;
    GPSPortSelect: TComboBox;
    GPSElevationlabel: TLabeledEdit;
    GPSRMCStatusX: TShape;
    RecordLimitSpin: TSpinEdit;
    TransferSendResultLabel: TLabel;
    TransferSendResult: TMemo;
    TransferTimeout: TLabeledEdit;
    TransferProtocolLabel: TLabel;
    TransferAddressEntry: TLabeledEdit;
    TransferFrequencyRadioGroup: TRadioGroup;
    TransferFullResult: TMemo;
    TransferLocalFilenameDisplay: TLabeledEdit;
    TransferPasswordEntry: TLabeledEdit;
    checkSRStart: TCheckBox;
    checkMTAStop: TCheckBox;
    checkMTNStop: TCheckBox;
    checkMTCStop: TCheckBox;
    checkETCStop: TCheckBox;
    checkETNStop: TCheckBox;
    checkETAStop: TCheckBox;
    checkSSStop: TCheckBox;
    checkMTAStart: TCheckBox;
    checkMTNStart: TCheckBox;
    checkMTCStart: TCheckBox;
    checkETCStart: TCheckBox;
    checkETNStart: TCheckBox;
    checkETAStart: TCheckBox;
    checkSSStart: TCheckBox;
    checkSRStop: TCheckBox;
    CloseButton: TBitBtn;
    TransferPasswordShowHide: TButton;
    TransferPortEntry: TLabeledEdit;
    TransferProtocolSelector: TComboBox;
    TransferRemoteDirectoryEntry: TLabeledEdit;
    TransferRemoteFilename: TLabeledEdit;
    TransferSettingsGroupBox: TGroupBox;
    TransferUsernameEntry: TLabeledEdit;
    FTPResultsGroupBox: TGroupBox;
    Label10: TLabel;
    Label11: TLabel;
    Label12: TLabel;
    Label13: TLabel;
    LatitudeLabel: TLabel;
    LatitudeText: TStaticText;
    LocationGroupBox: TGroupBox;
    LongitudeLabel: TLabel;
    LongitudeText: TStaticText;
    BottomPanel: TPanel;
    PauseButton: TBitBtn;
    TransferFileTabSheet: TTabSheet;
    TransferRemoteDirectorySuccess: TShape;
    StartButton: TBitBtn;
    StartStopMemo: TMemo;
    SetLocationButton: TBitBtn;
    StopButton: TBitBtn;
    TimeSR: TLabel;
    TimeMTA: TLabel;
    TimeMTN: TLabel;
    TimeMTC: TLabel;
    TimeETC: TLabel;
    TimeETN: TLabel;
    TimeETA: TLabel;
    TimeSS: TLabel;
    Label3: TLabel;
    Label5: TLabel;
    Label6: TLabel;
    Label7: TLabel;
    Label8: TLabel;
    Label9: TLabel;
    checkNowStart: TCheckBox;
    StartLabel: TLabel;
    StopLabel: TLabel;
    checkNeverStop: TCheckBox;
    StartStopGroup: TGroupBox;
    Chart1: TChart;
    Chart2: TChart;
    Chart2LineSeries1: TLineSeries;
    DateTimeIntervalChartSource1: TDateTimeIntervalChartSource;
    Displayedcdm2: TLabel;
    DisplayedNELM: TLabel;
    DisplayedNSU: TLabel;
    DisplayedReading: TLabel;
    EastLabel: TLabel;
    EditHotkeysCheckBox: TCheckBox;
    FineTimer: TTimer;
    GDMF0Button: TButton;
    GDMF1Button: TButton;
    GDMGroupBox: TGroupBox;
    GDMSheet: TTabSheet;
    FrequencyGroup: TGroupBox;
    HotkeyStringGrid: TStringGrid;
    Label49: TLabel;
    LCTrigMinutesSpin: TSpinEdit;
    LCTrigSecondsSpin: TSpinEdit;
    MinutesLabel: TLabel;
    MPSASSeries: TLineSeries;
    NorthLabel: TLabel;
    OpenLogDialog: TOpenDialog;
    PageControl1: TPageControl;
    PairSplitter1: TPairSplitter;
    PairSplitterTop: TPairSplitterSide;
    PairSplitterBottom: TPairSplitterSide;
    PendingHotKey: TEdit;
    PendingLabel: TLabel;
    PersistentCheckBox: TCheckBox;
    RadioButton1: TRadioButton;
    RadioButton2: TRadioButton;
    RadioButton3: TRadioButton;
    RadioButton4: TRadioButton;
    RadioButton5: TRadioButton;
    RadioButton6: TRadioButton;
    RadioButton7: TRadioButton;
    RadioButton8: TRadioButton;
    ReadingSheet: TTabSheet;
    ReadingUnits: TLabel;
    RecordsViewMemo: TSynMemo;
    RotStageSheet: TTabSheet;
    RSComboBox: TComboBox;
    RSCurrentPositionAngleDisplay: TEdit;
    RSCurrentPositionStepDisplay: TEdit;
    RSDirInd: TStaticText;
    RSGroupBox: TGroupBox;
    RSLLimInd: TStaticText;
    RSMaxSteps: TLabeledEdit;
    RSPositionStepSpinEdit: TSpinEdit;
    RSRlimInd: TStaticText;
    RSSafteyInd: TStaticText;
    RSStatusBar: TStatusBar;
    SecondsLabel: TLabel;
    SouthLabel: TLabel;
    StartUpTimer: TTimer;
    SynchronizedCheckBox: TCheckBox;
    TriggerSheet: TTabSheet;
    WestLabel: TLabel;
    procedure AlarmSoundEnableCheckChange(Sender: TObject);
    procedure AlarmTestButtonClick(Sender: TObject);
    procedure AlarmThresholdFloatSpinEditChange(Sender: TObject);
    procedure alert2sChange(Sender: TObject);
    procedure AnnotateButtonClick(Sender: TObject);
    procedure FixedAutoTemperatureReadingsToggleChange(Sender: TObject);
    procedure FixedFromSpinEditChange(Sender: TObject);
    procedure FixedTimeRadiosClick(Sender: TObject);
    procedure FixedToSpinEditChange(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure FromReadingSpinEditChange(Sender: TObject);
    procedure FromTemperatureReadingSpinEditChange(Sender: TObject);
    procedure NightCheckBoxChange(Sender: TObject);
    procedure PairSplitterTopResize(Sender: TObject);
    procedure TestTransferClick(Sender: TObject);
    procedure GoToButtonScriptHelpClick(Sender: TObject);
    procedure GoToZenAziButtonClick(Sender: TObject);
    procedure FreshAlertTestClick(Sender: TObject);
    procedure GetZenAziButtonClick(Sender: TObject);
    procedure GoToBaudSelectChange(Sender: TObject);
    procedure GoToCommandFileComboBoxChange(Sender: TObject);
    procedure GoToCommBusyTimer(Sender: TObject);
    procedure GoToMachineSelectChange(Sender: TObject);
    procedure GoToPortSelectChange(Sender: TObject);
    procedure GPSBaudSelectChange(Sender: TObject);
    procedure GPSEnableClick(Sender: TObject);
    procedure PreAlertTestButtonClick(Sender: TObject);
    procedure checkMTAStartClick(Sender: TObject);
    procedure checkMTAStopClick(Sender: TObject);
    procedure checkNeverStopClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure GPSConnectButtonClick(Sender: TObject);
    procedure GPSPortSelectChange(Sender: TObject);
    procedure GPSPortSelectDropDown(Sender: TObject);
    procedure GPSTimerTimer(Sender: TObject);
    procedure InvertScaleChange(Sender: TObject);
    procedure OptionsGroupItemClick(Sender: TObject; Index: integer);
    procedure PauseButtonClick(Sender: TObject);
    procedure ReadingAlertGroupClick(Sender: TObject);
    procedure RecordLimitSpinChange(Sender: TObject);
    procedure SingleDatCheckBoxClick(Sender: TObject);
    procedure SnoozeButtonClick(Sender: TObject);
    procedure SnowSettleSpinChange(Sender: TObject);
    procedure SplitSpinEditChange(Sender: TObject);
    procedure SynScanSheetShow(Sender: TObject);
    procedure TemperatureCheckBoxChange(Sender: TObject);
    procedure FixedAutoReadingsToggleChange(Sender: TObject);
    procedure ToReadingSpinEditChange(Sender: TObject);
    procedure ToTemperatureReadingSpinEditChange(Sender: TObject);
    procedure TransferCSVCheckClick(Sender: TObject);
    procedure TransferDATCheckClick(Sender: TObject);
    procedure TransferFrequencyRadioGroupClick(Sender: TObject);
    procedure TransferLocalFilenameDisplayChange(Sender: TObject);
    procedure TransferPasswordEntryChange(Sender: TObject);
    procedure TransferPasswordShowHideClick(Sender: TObject);
    procedure TransferPLOTCheckClick(Sender: TObject);
    procedure TransferPortEntryChange(Sender: TObject);
    procedure TransferProtocolSelectorChange(Sender: TObject);
    procedure TransferPWenableChange(Sender: TObject);
    procedure TransferRemoteDirectoryEntryChange(Sender: TObject);
    procedure TransferTimeoutChange(Sender: TObject);
    procedure TransferUsernameEntryChange(Sender: TObject);
    procedure GDMF0ButtonClick(Sender: TObject);
    procedure GDMF1ButtonClick(Sender: TObject);
    procedure TransferAddressEntryChange(Sender: TObject);
    procedure LCThresholdChange(Sender: TObject);
    procedure LCTrigMinutesSpinChange(Sender: TObject);
    procedure LCTrigSecondsSpinChange(Sender: TObject);
    procedure checkNowStartClick(Sender: TObject);
    procedure OpenFileButtonClick(Sender: TObject);
    procedure CloseButtonClick(Sender: TObject);
    procedure EditHotkeysCheckBoxChange(Sender: TObject);
    procedure FineTimerTimer(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormKeyUp(Sender: TObject; var Key: word; Shift: TShiftState);
    procedure FormShow(Sender: TObject);
    procedure PersistentCheckBoxChange(Sender: TObject);
    procedure RadioButton1Click(Sender: TObject);
    procedure SetLocationButtonClick(Sender: TObject);
    procedure StartButtonClick(Sender: TObject);
    procedure StartUpTimerTimer(Sender: TObject);
    procedure StopButtonClick(Sender: TObject);
    procedure HotkeyStringGridKeyUp(Sender: TObject; var Key: word; Shift: TShiftState);
    procedure HotkeyStringGridSelectEditor(Sender: TObject; aCol, aRow: integer;
      var Editor: TWinControl);
    procedure SynchronizedCheckBoxChange(Sender: TObject);
    procedure TrRdgHelpButtonClick(Sender: TObject);
    procedure TrRdgAddressEntryChange(Sender: TObject);
    procedure TrRdgEnableCheckBoxClick(Sender: TObject);
    procedure TrRdgPortEditChange(Sender: TObject);
    procedure TrRdgTestButtonClick(Sender: TObject);
  private
    { private declarations }
    procedure TransferProtocolSelection();
    procedure GPSSNRClear();
    procedure UpdateGoToCommandFileList();
    procedure ChartColor();
    procedure SetFixedReadings();
    procedure SetFixedTemperatureReadings();
    procedure SetFixedTime();
    procedure UpdateTemperaturePlotSelection();
    procedure CheckInvert();
    procedure Text24to12hr(Hour24: Word; Receiver: TLabel);
  public
    { public declarations }
    procedure CustomizeEdit(ACol, ARow: integer; Editor: TStringCellEditor);
    procedure CheckSynchronized();
    function FTPSend(): boolean;
  end;

type GoToRecord = record
    Zenith, Azimuth:Float;
 end;

procedure GPSConnect();
procedure GPSDisConnect();
function GoToSendGet(command:string; Timeout:Integer=3000; GetAlso:boolean = True; HideStatus:boolean = False;RecvTerminator:string='') : string;
procedure GoToReadScript();
function Alt2Zen(Altitude:Double): Double;
function Zen2Alt(Zenith:Double): Double;

function GotoZenAzi(Zenith,Azimuth:Double):Boolean;
procedure GoToConnect();
procedure GoToDisConnect();
procedure GoToCommand(Command:Integer; Parm1:String=''; Parm2:String='');

procedure LogOneReading(LogResult:string='');
procedure PrepareTransferPayload();

procedure TransferReading(TransferString:string);

const
  TemperatureColor = clGreen;
  GoToCommBusyLimit = 12; //Timer preset for com busy in 100ms chunks (12=1.2s)
  lctfNever = 0; //Log Contionuous Transfer Frequency = Never
  lctfAfterEveryRecord = 1;//Log Contionuous Transfer Frequency = After Every Record
  lctfAtEndOfDay = 2;//Log Contionuous Transfer Frequency = At End Of Day


var
  FormLogCont: TFormLogCont;
  FormCreating:Boolean = False;
  LogTimePreset, LogTimeCurrent: integer;
  LCTrigSeconds: Integer;
  LCTrigMinutes: Integer;
  LCLoggedCount: integer; // Number of logged records taken during this session
  LCLogFileCount: integer; //Number of logfiles created during this session
  LCTriggerMode: integer; // radio button selection (0 - __)
  StopRecording: boolean;
  Setting: string; //Setting of recorder
  DLHeaderStyle: string;
  Recording: boolean; //Flag to indicate that record is being logged
  RecordingMode: boolean = False; //Indicate that user has pressed Record button
  TimerBusy: boolean = False; //Flag to indicate that Timer event is being handled
  MoonData: Boolean = False; //Indicates user selceted Moon data plotted on chart.
  LCThresholdValue: Double = 0.0; //The user selected threshold.
  RawFrequencyEnabled: Boolean = False; //Enable the reading of raw frequency for logs "rFx"/

  MoonElevation: extended = 0.0;
  MoonAzimuth: extended = 0.0;

  subfix: ansistring; //Used for time zone conversions

  //Command line automatic variables
  JustStarted: boolean = True;

  //Used for logging and display
  ThisMoment, ThisMomentUTC: TDateTime;
  TwilightMorningTimeStamp, TwilightEveningTimeStamp: TDateTime;
  StartPlotTimeStamp, EndPlotTimeStamp: TDateTime;
  OldStartPlotTimeStamp, OldEndPlotTimeStamp: TDateTime;
  FromHour, ToHour:Word;
  Temperature: Float = 0.0;
  Darkness: Float = 0.0;
  BestDarkness: Float = 0.0;
  BestDarknessString: String;
  BestDarknessTime: TDateTime;
  BestDarknessTimeString: String;
  DarknessString: String;
  NELM: Float = 0.0;
  NELMstring: String;
  CDM2: Float = 0.0;
  CDM2string: String;
  MCDM2string: String;
  NSU: Float = 0.0;
  NSUstring: String;


  //Used for Aurora display
  ADAFactor: Float;

  //Used to check 24 hr log file rollover
  //LCStartFileTime: TDateTime;
  LCStartFileDay: Integer;
  LCStartFileHour: Integer;
  SplitDatTime: Integer;
  SplitSpinBusy: Boolean = False;

  RecordsMissedCount: integer;
  //Determined while recording records, incrememts if meter does not respond.

  OldSecond: word; //Previous second value used to check timer rollover.

  AnnotateText: string;
  HotKeyCodes: array[1..100] of word;

  { Rotational stage variables }
  RSResult: string; //The response string from the rotarduino
  RSpieces: TStringList; //Parts of the response string from the rotarduino
  RSCurrentAngle: Float; //determined from step signal fed back from rotarduino
  RSCurrentStepNumber: integer = 0; //Counts the number of steps made
  RSCycle: integer = 0; //0=step left from 0deg, 1 = step right from 0deg

  AlarmSoundEnable:Boolean = False;
  Alert2sEnable:Boolean = False;
  AlertEnable:Integer = 0; //0=None, 1=Fresh only, 2=All

  { Plot line options }
  InvertMPSAS:Boolean = False;
  TemperaturePlotted: Boolean = False;
  NightMode: Boolean = False;
  FixedReadingRange: Boolean;
  FixedTemperatureRange: Boolean;
  FixedTimeAxisSelection: Integer;
  FromReading, ToReading: Integer;
  FromTemperatureReading, ToTemperatureReading: Integer;
  PlotCount: Integer = 86400;

  { GPS shared variables }
  GPSLatitude, GPSLongitude: Float;
  GPSAltitude: Integer;
  GPSSpeed: Float;
  GPSVisibleSatellites: Integer;
  GPSBaudrate:Integer;

  { GoTo shared variables }
  GotoMachineSelection: String = '';
  GoToPortSelection:String = '';
  GoToBaudrate:Integer;
  GoToEnabled:Boolean = False;
  GoToInDecoder:Boolean = False;
  GoToRcvLine:Integer = 0;
  SelectedGoToCommandFile:String;
  GoToStage:Integer = 0;
  GoToStages: array [0..3] of string = ('GoTo position', 'Wait for position', 'Get first fresh reading', 'Get second fresh reading');
  { - Go to desired position, then move to next stage.
    - keep scanning GoTo device until not moving, then move to next stage.
    - Keep reading until the first fresh reading arrives, then move to next stage.
    - Keep reading until the second fresh reading arrives then logonereading.}
  GoToProgramStep:Integer = 0; //step in .goto position file
  GoToProgramStepsTotal:Integer = 0; //Total number of steps.
  GoToDesiredZenith,GoToDesiredAzimuth:Double;
  GoToCommandStep:Integer;//Some commands to mahcine divided into smaller steps.
  GoToRecvTerminator:String='';
  GoToGetStableReading:Boolean=False;//Flag to identify stage of Goto command.
  GoToCommBusyTime: Integer = 0;
  GoToCommOpened: Boolean = False;
  GoToInPosition:Boolean = False;//Inidicates that the GoTo has reached the desired position
  GoToRecords: array of GoToRecord;

  { Transfer File settings}
  gfn: String; // Graphics FileName
  lgfn: String; // Local Graphics FileName
  hfn: String; // HTML FileName
  TransferDAT, TransferCSV, TransferPLOT:Boolean;
  CSVLogFileName:String;
  TransferPort, TransferPortSFTP, TransferUsername, TransferPassword, TransferAddress, TransferLocalFilename, TransferRemoteDirectory:String;
  NextRecordAtTimestamp: TDateTime;
  BashPath: String = '';
  ExpectPath: String = '';
  PWEnable: Boolean;
  LCTFrequency: Integer; //Log Continuous Frequency

  { Transfer reading settings}
  TrRdgEnabled: Boolean;
  TrRdgAddress: String;
  TrRdgPort: String;

implementation

uses
  Unit1
  , Vector //VectorTab model
  , worldmap
  , process
  , FileUtil{$IFDEF WINDOWS},mmsystem{$ELSE},asyncprocess{$ENDIF} //For playing a sound
  ;

//type
//    TPlayStyle = (psAsync,psSync); //For playing a sound
CONST
  C_UnableToPlay = 'Unable to play ';
  {$IFNDEF WINDOWS}
   // Defined in mmsystem
    SND_SYNC=0;
    SND_ASYNC=1;
    SND_NODEFAULT=2;
  {$ENDIF}

  AlarmSnoozeTimePreset = 60*5; //seconds
  AlarmRepeatTimePreset = 8; //seconds

  //GoTo constants for commands
  gtGetZenithAzimuth = 0;
  gtSetZenithAzimuth = 1; //pseudo command consisting of a few separate commands, see timer for details.

var
  Altitude: double;
  ValidStartStop: boolean;
  //Runtime-created plot series for the staged snow (LED-on) reading.
  //Created in FormCreate so no .lfm change is needed.
  SnowSeries: TLineSeries = nil;
  //Per-device MPSAS plot lines for multi-meter logging (-SUI). Element [0]
  //reuses MPSASSeries (the primary line); elements [1..] are created at runtime.
  //CurrentDevicePlotIndex is the series index of the device being logged now.
  DevicePlotSeries: array of TLineSeries;
  DeviceTempSeries: array of TLineSeries; //Per-device temperature lines ([0] reuses TempSeries).
  CurrentDevicePlotIndex: Integer = 0;
  {$IFNDEF WINDOWS}
  SoundPlayerAsyncProcess:Tasyncprocess;
  SoundPlayerSyncProcess:Tprocess;
  {$ENDIF}
  //fPlayStyle:TPlayStyle;
  //Alert sound: tells when reading is going to take place.
  //Alert2sSoundFilename:String;
  //FreshSoundFilename:String;
  //Alarm sound: tells if the reading is above the threshold.
  AlarmThreshold:Double;
  AlarmRequest: Boolean = False; //Default: Alarm off

  AlarmSnoozeTime: Integer = AlarmSnoozeTimePreset; //Number of seconds for Snooze timer
  AlarmSnoozeCurrentTime: Integer = AlarmSnoozeTimePreset; //Number of seconds since Snooze was pressed, default =snooze expired

  AlarmRepeatTime: Integer=AlarmRepeatTimePreset; //Number of seconds between repeating alarm sound.
  AlarmRepeatCurrentTime: Integer=AlarmRepeatTimePreset; //Number of seconds since Snooze was pressed, default = expired
  LocationName: String = '';
  PositionString: String = '';
  PositionEntry: String = '';


function Alt2Zen(Altitude:Double): Double;
begin
  Alt2Zen:=abs(Altitude - 90.0);
end;

function Zen2Alt(Zenith:Double): Double;
begin
  Zen2Alt:=abs(90.0 - Zenith);
end;

procedure SetRepeatProgress();
begin
  FormLogCont.RepeatProgress.Position:=  round((AlarmRepeatCurrentTime * FormLogCont.RepeatProgress.Max) / AlarmRepeatTime);
end;

procedure SetSnoozeProgress();
begin
  FormLogCont.SnoozeProgress.Position:= round((AlarmSnoozeCurrentTime *  FormLogCont.SnoozeProgress.Max) / AlarmSnoozeTime);
end;


procedure UpdateStartStopText();
var
  s: string;
begin

  s:= ''; //clear string

  //check start stop checkboxes and create a readable text string to describe the action(s).
  with FormLogCont do
  begin
    StartStopMemo.Clear;
    s:= 'Start logging:' + sLineBreak;
    //Append('Start logging immediately, and never stop.');
    if checkNowStart.Checked then
      s:= s + ' immediately' + sLineBreak;
    if checkMTAStart.Checked then
      s:= s + ' at astronomical morning twilight ' + sLineBreak;
    if checkMTNStart.Checked then
      s:= s + ' at nautical morning twilight ' + sLineBreak;
    if checkMTCStart.Checked then
      s:= s + ' at civil morning twilight ' + sLineBreak;
    if checkSRStart.Checked then
      s:= s + ' at sunrise ' + sLineBreak;
    if checkSSStart.Checked then
      s:= s + ' at sunset ' + sLineBreak;
    if checkETCStart.Checked then
      s:= s + ' at civil evening twilight ' + sLineBreak;
    if checkETNStart.Checked then
      s:= s + ' at nautical evening twilight ' + sLineBreak;
    if checkETAStart.Checked then
      s:= s + ' at astronmical evening twilight ' + sLineBreak;

    s:= s + sLineBreak + 'and stop logging:' + sLineBreak;
    if checkMTAStop.Checked then
      s:= s + ' at astronomical morning twilight ' + sLineBreak;
    if checkMTNStop.Checked then
      s:= s + ' at nautical morning twilight ' + sLineBreak;
    if checkMTCStop.Checked then
      s:= s + ' at civil morning twilight ' + sLineBreak;
    if checkSRStop.Checked then
      s:= s + ' at sunrise ' + sLineBreak;
    if checkSSStop.Checked then
      s:= s + ' at sunset ' + sLineBreak;
    if checkETCStop.Checked then
      s:= s + ' at civil evening twilight ' + sLineBreak;
    if checkETNStop.Checked then
      s:= s + ' at nautical evening twilight ' + sLineBreak;
    if checkETAStop.Checked then
      s:= s + ' at astronmical evening twilight ' + sLineBreak;
    if checkNeverStop.Checked then
      s:= s + ' never' + sLineBreak;

    StartStopMemo.Append(s);
  end;
end;

procedure TransferReading(TransferString:string);
var
  ErrorString: AnsiString;
begin
  TCPTransferEthSocket := TTCPBlockSocket.Create;
  TCPTransferEthSocket.ConvertLineEnd := True;
  //Try setting the timeout a bit longer than normal, because
  // finding Ethernet devices failed a few times on slow Windows7 netbook.
  TCPTransferEthSocket.SetRecvTimeout(2000);
  TCPTransferEthSocket.SetSendTimeout(2000);
  TCPTransferEthSocket.Connect(TrRdgAddress, TrRdgPort);

  TCPTransferEthSocket.ResetLastError;
  TCPTransferEthSocket.Purge;
  TCPTransferEthSocket.SendString(TransferString);

  StatusMessage('Sent reading string by TCP to .'+TrRdgAddress+':'+TrRdgPort);

  If (TCPTransferEthSocket.LastError<>0) then begin
     ErrorString:= ' ['+IntToStr(TCPTransferEthSocket.LastError)+']'+ TCPTransferEthSocket.LastErrorDesc;
     StatusMessage('TCP sending error:'+ErrorString);
  end;

end;

procedure CheckStartStopValidity();
var
  Balance: integer;
  StartStopSame: boolean;
  LocationMissing: boolean;
begin
  Balance:= 0;
  StartStopSame:= False;
  LocationMissing:= False;

  with FormLogCont do
  begin
    //Check number of selected Starts compared to number of selected Stops
    if checkNowStart.Checked then
      Inc(Balance);
    if checkMTAStart.Checked then
      Inc(Balance);
    if checkMTNStart.Checked then
      Inc(Balance);
    if checkMTCStart.Checked then
      Inc(Balance);
    if checkSRStart.Checked then
      Inc(Balance);
    if checkSSStart.Checked then
      Inc(Balance);
    if checkETCStart.Checked then
      Inc(Balance);
    if checkETNStart.Checked then
      Inc(Balance);
    if checkETAStart.Checked then
      Inc(Balance);

    if checkMTAStop.Checked then
      Dec(Balance);
    if checkMTNStop.Checked then
      Dec(Balance);
    if checkMTCStop.Checked then
      Dec(Balance);
    if checkSRStop.Checked then
      Dec(Balance);
    if checkSSStop.Checked then
      Dec(Balance);
    if checkETCStop.Checked then
      Dec(Balance);
    if checkETNStop.Checked then
      Dec(Balance);
    if checkETAStop.Checked then
      Dec(Balance);
    if checkNeverStop.Checked then
      Dec(Balance);

    //Check that Start is not the same as Stop
    StartStopSame:=
      (checkMTAStart.Checked and checkMTAStop.Checked) or
      (checkMTNStart.Checked and checkMTNStop.Checked) or
      (checkMTCStart.Checked and checkMTCStop.Checked) or
      (checkSRStart.Checked and checkSRStop.Checked) or
      (checkSSStart.Checked and checkSSStop.Checked) or
      (checkETCStart.Checked and checkETCStop.Checked) or
      (checkETNStart.Checked and checkETNStop.Checked) or
      (checkETAStart.Checked and checkETAStop.Checked);


    //Check for valid location
    if ((StrToFloatDef(LatitudeText.Caption, 0) = 0) or
      (StrToFloatDef(LongitudeText.Caption, 0) = 0)) then
      LocationMissing:= True;

    // Prepare memo for possible error messages
    StartStopMemo.Clear;

    //Show warnings
    if Balance <> 0 then
      StartStopMemo.Append('Number of starts ≠ stops.');
    if StartStopSame then
      StartStopMemo.Append('Start and stop time must be different.');
    if LocationMissing then
      StartStopMemo.Append('Latitude and/or Longitude not set.');

    //Set global valid flag
    ValidStartStop:= not ((Balance <> 0) or StartStopSame or LocationMissing);

    //Update textual desription if valid
    if ValidStartStop then
      UpdateStartStopText();

  end;

end;

procedure SaveStartStopSettings();
begin

  CheckStartStopValidity();

  if ValidStartStop then
  begin
    with FormLogCont do
    begin
      vConfigurations.WriteBool('LogContinuousSettings', 'StartNow',
        checkNowStart.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StartMTA',
        checkMTAStart.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StartMTN',
        checkMTNStart.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StartMTC',
        checkMTCStart.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StartSR',
        checkSRStart.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StartSS',
        checkSSStart.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StartETC',
        checkETCStart.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StartETN',
        checkETNStart.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StartETA',
        checkETAStart.Checked);

      vConfigurations.WriteBool('LogContinuousSettings', 'StopMTA',
        checkMTAStop.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StopMTN',
        checkMTNStop.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StopMTC',
        checkMTCStop.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StopSR', checkSRStop.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StopSS', checkSSStop.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StopETC',
        checkETCStop.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StopETN',
        checkETNStop.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StopETA',
        checkETAStop.Checked);
      vConfigurations.WriteBool('LogContinuousSettings', 'StopNever',
        checkNeverStop.Checked);
    end;
  end;

end;

procedure UpdateAstroTimes();
var
  ThisUTC: TDateTime;
  ThisLocation: string;
begin

  //Get a sample of the current UTC for consistent use in this procedure
  ThisUTC:= NowUTC();

  //Get the location for use in this procedure
  ThisLocation:= DLHeaderForm.TZLocationBox.Text;

  //Parts of the world will not have calculable times at certain times of the
  //year, so exceptions need to be processed.

  with FormLogCont do
  begin
    try
      TimeMTC.Caption:= FormatDateTime('tt',
        dlheader.ptz.GMTToLocalTime(Morning_Twilight_Civil(ThisUTC,
        MyLatitude, -1.0 * MyLongitude), ThisLocation, subfix));
    except
      TimeMTC.Caption:= 'None';
    end;

    try
      TimeETC.Caption:= FormatDateTime('tt',
        dlheader.ptz.GMTToLocalTime(Evening_Twilight_Civil(ThisUTC,
        MyLatitude, -1.0 * MyLongitude), ThisLocation, subfix));
    except
      TimeETC.Caption:= 'None';
    end;

    try
      TimeMTN.Caption:= FormatDateTime('tt',
        dlheader.ptz.GMTToLocalTime(Morning_Twilight_Nautical(ThisUTC,
        MyLatitude, -1.0 * MyLongitude), ThisLocation, subfix));
    except
      TimeMTN.Caption:= 'None';
    end;

    try
      TimeETN.Caption:= FormatDateTime('tt',
        dlheader.ptz.GMTToLocalTime(Evening_Twilight_Nautical(ThisUTC,
        MyLatitude, -1.0 * MyLongitude), ThisLocation, subfix));
    except
      TimeETN.Caption:= 'None';
    end;

    try
      TimeMTA.Caption:= FormatDateTime('tt',
        dlheader.ptz.GMTToLocalTime(Morning_Twilight_Astronomical(ThisUTC,
        MyLatitude, -1.0 * MyLongitude), ThisLocation, subfix));
    except
      TimeMTA.Caption:= 'None';
    end;

    try
      TimeETA.Caption:= FormatDateTime('tt',
        dlheader.ptz.GMTToLocalTime(Evening_Twilight_Astronomical(ThisUTC,
        MyLatitude, -1.0 * MyLongitude), ThisLocation, subfix));
    except
      TimeETA.Caption:= 'None';
    end;

    try
      TimeSR.Caption:= FormatDateTime('tt',
        dlheader.ptz.GMTToLocalTime(Sun_Rise(ThisUTC, MyLatitude,
        -1.0 * MyLongitude), ThisLocation, subfix));
    except
      TimeSR.Caption:= 'None';
    end;

    try
      TimeSS.Caption:= FormatDateTime('tt',
        dlheader.ptz.GMTToLocalTime(Sun_Set(ThisUTC, MyLatitude,
        -1.0 * MyLongitude), ThisLocation, subfix));
    except
      TimeSS.Caption:= 'None';
    end;
  end;

end;

procedure GetStartStopSettings();
var
  NumStart, NumStop: integer;

begin

  with FormLogCont do
  begin
    checkNowStart.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StartNow', False);
    checkMTAStart.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StartMTA', False);
    checkMTNStart.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StartMTN', False);
    checkMTCStart.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StartMTC', False);
    checkSRStart.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StartSR', False);
    checkSSStart.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StartSS', False);
    checkETCStart.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StartETC', False);
    checkETNStart.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StartETN', False);
    checkETAStart.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StartETA', False);

    checkMTAStop.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StopMTA', False);
    checkMTNStop.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StopMTN', False);
    checkMTCStop.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StopMTC', False);
    checkSRStop.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StopSR', False);
    checkSSStop.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StopSS', False);
    checkETCStop.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StopETC', False);
    checkETNStop.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StopETN', False);
    checkETAStop.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StopETA', False);
    checkNeverStop.Checked:= vConfigurations.ReadBool('LogContinuousSettings',
      'StopNever', False);

    //Get location
    LatitudeText.Caption:= Format('%0.6f', [MyLatitude]);
    LongitudeText.Caption:= Format('%0.6f', [MyLongitude]);

    //Check validity
    //ensure at least one Start
    NumStart:= 0;
    if checkMTAStart.Checked then
      Inc(NumStart);
    if checkMTNStart.Checked then
      Inc(NumStart);
    if checkMTCStart.Checked then
      Inc(NumStart);
    if checkSRStart.Checked then
      Inc(NumStart);
    if checkSSStart.Checked then
      Inc(NumStart);
    if checkETCStart.Checked then
      Inc(NumStart);
    if checkETNStart.Checked then
      Inc(NumStart);
    if checkETAStart.Checked then
      Inc(NumStart);
    if NumStart = 0 then
      checkNowStart.Checked:= True;

    //ensure at least one Stop
    NumStop:= 0;
    if checkMTAStop.Checked then
      Inc(NumStop);
    if checkMTNStop.Checked then
      Inc(NumStop);
    if checkMTCStop.Checked then
      Inc(NumStop);
    if checkSRStop.Checked then
      Inc(NumStop);
    if checkSSStop.Checked then
      Inc(NumStop);
    if checkETCStop.Checked then
      Inc(NumStop);
    if checkETNStop.Checked then
      Inc(NumStop);
    if checkETAStop.Checked then
      Inc(NumStop);
    if NumStop = 0 then
      checkNeverStop.Checked:= True;
  end;

  if ((NumStart > 0) or (NumStop > 0)) then
  begin
    SaveStartStopSettings();
    CheckStartStopValidity(); //Tell user if settings are valid
  end;

  UpdateAstroTimes(); //Show actual times on screen

end;

function MPSASToRGB(MPSAS: double): Tcolor;
  //Convert MPSAS to Tcolor RGB value
  // using plot from Dan Duriscoe ADA463050.pdf
var
  R: integer = 0;
  G: integer = 0;
  B: integer = 0;

const
  White = 18.0;// (RGB)
  Red = 18.7;// (R  )
  Yellow = 20.0;// (RG )
  Green = 20.4;// ( G )
  Blue = 21.1;// (  B)
  Black = 22.5;// (   )

  procedure MinMaxRange(InX, StartX, StopX, StartY, StopY: double; var OutY: integer);
  var
    m, b: double; //Slope, Offset
  begin
    if ((InX > StartX) and (InX <= StopX)) then
    begin
      m:= (StopY - StartY) / (StopX - StartX);
      b:= StartY - M * StartX;
      OutY:= round((m * InX + b) * 255);
    end;
  end;

begin
  MinMaxRange(MPSAS, 0, Yellow, 1, 1, R);
  MinMaxRange(MPSAS, Yellow, Green, 1, 0, R);
  MinMaxRange(MPSAS, Green, Blue, 0, 0, R);
  MinMaxRange(MPSAS, Blue, Blue + (Black - Blue) / 2, 0, 1, R);
  MinMaxRange(MPSAS, Blue + (Black - Blue) / 2, Black, 1, 0, R);

  MinMaxRange(MPSAS, 0, White, 1, 1, G);
  MinMaxRange(MPSAS, White, Red, 1, 0, G);
  MinMaxRange(MPSAS, Red, Yellow, 0, 1, G);
  MinMaxRange(MPSAS, Yellow, Green, 1, 1, G);
  MinMaxRange(MPSAS, Green, Blue, 1, 0, G);
  MinMaxRange(MPSAS, Blue, 100, 0, 0, G);

  MinMaxRange(MPSAS, 0, White, 1, 1, B);
  MinMaxRange(MPSAS, White, Red, 1, 0, B);
  MinMaxRange(MPSAS, Red, Green, 0, 0, B);
  MinMaxRange(MPSAS, Green, Blue, 0, 1, B);
  MinMaxRange(MPSAS, Blue, Black, 1, 0, B);
  MinMaxRange(MPSAS, Black, 100, 0, 0, B);

  MPSASToRGB:= RGBToColor(R, G, B);
end;

procedure ParseRotstage(Result: string);
begin
  RSpieces.DelimitedText:= Result;

  //0 current position
  RSCurrentAngle:= StrToFloatDef(RSpieces.Strings[0], 0.0) * 90.0 / 50.0;

  //1 Right limit value, 1=hit
  if (RSpieces.Strings[1] = '1') then
    FormLogCont.RSRlimInd.Color:= clRed
  else
    FormLogCont.RSRlimInd.Color:= clDefault;

  //2 Left limit value, 1=hit
  if (RSpieces.Strings[2] = '1') then
    FormLogCont.RSLlimInd.Color:= clRed
  else
    FormLogCont.RSLlimInd.Color:= clDefault;

  //3 Safety limit value
  if (RSpieces.Strings[3] = '1') then
    FormLogCont.RSSafteyInd.Color:= clRed
  else
    FormLogCont.RSSafteyInd.Color:= clDefault;

  //4 Reset value 0 = reset
  //  Writeln(RSpieces.Strings[4]);

  //5 desired Step direction  0=left, 1=right
  if (RSpieces.Strings[5] = '1') then
    FormLogCont.RSDirInd.Caption:= 'Dir=Right'
  else
    FormLogCont.RSDirInd.Caption:= 'Dir=Left';

  //6 actual step direction
  //7 Sleep value 0 = sleep

  FormLogCont.RSCurrentPositionAngleDisplay.Text:= Format('%1.1f', [RSCurrentAngle]);

end;

function keyslist(Key: word): string;
type
  TSendKey = record
    Name: ShortString;
    VKey: byte;
  end;
const
  {Array of keys that SendKeys recognizes.

  If you add to this list, you must be sure to keep it sorted alphabetically
  by Name because a binary search routine is used to scan it.

  http://delphiworld.narod.ru/base/sendkeys_vb.html}

  MaxSendKeyRecs = 77;
  SendKeyRecs: array[1..MaxSendKeyRecs] of TSendKey =
    (
    (Name: 'F1'; VKey: VK_F1),
    (Name: 'F2'; VKey: VK_F2),
    (Name: 'F3'; VKey: VK_F3),
    (Name: 'F4'; VKey: VK_F4),
    (Name: 'F5'; VKey: VK_F5),
    (Name: 'F6'; VKey: VK_F6),
    (Name: 'F7'; VKey: VK_F7),
    (Name: 'F8'; VKey: VK_F8),
    (Name: 'F9'; VKey: VK_F9),
    (Name: 'F10'; VKey: VK_F10),
    (Name: 'F11'; VKey: VK_F11),
    (Name: 'F12'; VKey: VK_F12),
    (Name: 'F13'; VKey: VK_F13),
    (Name: 'F14'; VKey: VK_F14),
    (Name: 'F15'; VKey: VK_F15),
    (Name: 'F16'; VKey: VK_F16),
    (Name: 'F17'; VKey: VK_F17),
    (Name: 'F18'; VKey: VK_F18),
    (Name: 'F19'; VKey: VK_F19),
    (Name: 'F20'; VKey: VK_F20),
    (Name: 'F21'; VKey: VK_F21),
    (Name: 'F22'; VKey: VK_F22),
    (Name: 'F23'; VKey: VK_F23),
    (Name: 'F24'; VKey: VK_F24),
    (Name: '0'; VKey: VK_0),
    (Name: '1'; VKey: VK_1),
    (Name: '2'; VKey: VK_2),
    (Name: '3'; VKey: VK_3),
    (Name: '4'; VKey: VK_4),
    (Name: '5'; VKey: VK_5),
    (Name: '6'; VKey: VK_6),
    (Name: '7'; VKey: VK_7),
    (Name: '8'; VKey: VK_8),
    (Name: '9'; VKey: VK_9),
    (Name: 'A'; VKey: VK_A),
    (Name: 'B'; VKey: VK_B),
    (Name: 'C'; VKey: VK_C),
    (Name: 'D'; VKey: VK_D),
    (Name: 'E'; VKey: VK_E),
    (Name: 'F'; VKey: VK_F),
    (Name: 'G'; VKey: VK_G),
    (Name: 'H'; VKey: VK_H),
    (Name: 'I'; VKey: VK_I),
    (Name: 'J'; VKey: VK_J),
    (Name: 'K'; VKey: VK_K),
    (Name: 'L'; VKey: VK_L),
    (Name: 'M'; VKey: VK_M),
    (Name: 'N'; VKey: VK_N),
    (Name: 'O'; VKey: VK_O),
    (Name: 'P'; VKey: VK_P),
    (Name: 'Q'; VKey: VK_Q),
    (Name: 'R'; VKey: VK_R),
    (Name: 'S'; VKey: VK_S),
    (Name: 'T'; VKey: VK_T),
    (Name: 'U'; VKey: VK_U),
    (Name: 'V'; VKey: VK_V),
    (Name: 'W'; VKey: VK_W),
    (Name: 'X'; VKey: VK_X),
    (Name: 'Y'; VKey: VK_Y),
    (Name: 'Z'; VKey: VK_Z),
    (Name: 'SPACE'; VKey: VK_SPACE),
    (Name: 'NUMPAD0'; VKey: VK_NUMPAD0),
    (Name: 'NUMPAD1'; VKey: VK_NUMPAD1),
    (Name: 'NUMPAD2'; VKey: VK_NUMPAD2),
    (Name: 'NUMPAD3'; VKey: VK_NUMPAD3),
    (Name: 'NUMPAD4'; VKey: VK_NUMPAD4),
    (Name: 'NUMPAD5'; VKey: VK_NUMPAD5),
    (Name: 'NUMPAD6'; VKey: VK_NUMPAD6),
    (Name: 'NUMPAD7'; VKey: VK_NUMPAD7),
    (Name: 'NUMPAD8'; VKey: VK_NUMPAD8),
    (Name: 'NUMPAD9'; VKey: VK_NUMPAD9),
    (Name: 'MULTIPLY'; VKey: VK_MULTIPLY),
    (Name: 'ADD'; VKey: VK_ADD),
    (Name: 'SEPARATOR'; VKey: VK_SEPARATOR),
    (Name: 'SUBTRACT'; VKey: VK_SUBTRACT),
    (Name: 'DECIMAL'; VKey: VK_DECIMAL),
    (Name: 'DIVIDE'; VKey: VK_DIVIDE)
    );

var
  A: TSendKey;

begin
  keyslist:= '';
  for A in SendKeyRecs do
  begin
    if Key = A.VKey then
      keyslist:= A.Name;
  end;

end;



function Sec2DHMS(InSeconds: integer): string;
var
  Days: integer;
  Hours: integer;
  InHours: integer;
  Minutes: integer;
  InMinutes: integer;
  Seconds: integer;
begin
  Seconds:= InSeconds mod 60;
  InMinutes:= InSeconds div 60;

  Minutes:= InMinutes mod 60;
  InHours:= InMinutes div 60;

  Hours:= InHours mod 24;
  Days:= InHours div 24;

  Sec2DHMS:= Format('%.1dd %.2d:%.2d:%.2d', [Days, Hours, Minutes, Seconds]);
end;

procedure OnTheClock(const ThisMoment: TDateTime; const Granularity: integer);
{This utility gets the next time from now based on a granularity setting,
 i.e for determining log time and difference for logging every x minutes}
begin
  // Determine next record time and calculate time until record
  // Replace seconds with 0
  NextRecordAtTimestamp:= RecodeSecond(IncMinute(ThisMoment, Granularity - MinuteOf(ThisMoment) mod Granularity),0);
  FormLogCont.NextRecordAt.Caption:=FormatDateTime('yyyy-mm-dd hh:nn:ss', NextRecordAtTimestamp);
  LogTimeCurrent:= SecondsBetween(ThisMoment, NextRecordAtTimestamp);
end;

{Distinct plot colour for each meter when logging multiple devices (-SUI).
 Index 0 matches the primary MPSASSeries colour (red).}
function DevicePlotColor(idx: Integer): TColor;
const
  Palette: array[0..7] of TColor =
    (clRed, clBlue, clGreen, clFuchsia, clTeal, clOlive, clNavy, clMaroon);
begin
  DevicePlotColor:= Palette[idx mod Length(Palette)];
end;

{Ensure one MPSAS plot line and one temperature plot line exist per selected
 meter for multi-device (-SUI) logging. Series index 0 reuses the existing
 MPSASSeries/TempSeries; indices 1.. are created once and reused. Temperature
 lines use triangle markers so they are distinct from the round-marker MPSAS
 lines, and each meter's two lines share a colour. Titles are placeholders here
 and are updated with each meter's unit serial number during logging (the serial
 is only known after the meter has been read). Called when recording starts.}
procedure SetupDevicePlotSeries;
var
  s: Integer;
begin
  if NumberOfMultipleDevices <= 0 then Exit; //Single-meter logging uses MPSASSeries/TempSeries directly.

  if Length(DevicePlotSeries) < NumberOfMultipleDevices then
    SetLength(DevicePlotSeries, NumberOfMultipleDevices);
  if Length(DeviceTempSeries) < NumberOfMultipleDevices then
    SetLength(DeviceTempSeries, NumberOfMultipleDevices);

  for s:= 0 to NumberOfMultipleDevices-1 do begin
    {MPSAS line for this meter.}
    if s = 0 then
      DevicePlotSeries[0]:= FormLogCont.MPSASSeries //Primary line: cleared with the others elsewhere.
    else begin
      if DevicePlotSeries[s] = nil then begin
        DevicePlotSeries[s]:= TLineSeries.Create(FormLogCont.Chart1);
        DevicePlotSeries[s].AxisIndexX:= FormLogCont.MPSASSeries.AxisIndexX;
        DevicePlotSeries[s].AxisIndexY:= FormLogCont.MPSASSeries.AxisIndexY;
        DevicePlotSeries[s].SeriesColor:= DevicePlotColor(s);
        FormLogCont.Chart1.AddSeries(DevicePlotSeries[s]);
      end;
      DevicePlotSeries[s].Clear; //Fresh start for this recording.
    end;

    {Temperature line for this meter, with a triangle marker and the meter colour.}
    if s = 0 then
      DeviceTempSeries[0]:= FormLogCont.TempSeries //Primary temp line: cleared elsewhere.
    else begin
      if DeviceTempSeries[s] = nil then begin
        DeviceTempSeries[s]:= TLineSeries.Create(FormLogCont.Chart1);
        DeviceTempSeries[s].AxisIndexX:= FormLogCont.TempSeries.AxisIndexX;
        DeviceTempSeries[s].AxisIndexY:= FormLogCont.TempSeries.AxisIndexY;
        FormLogCont.Chart1.AddSeries(DeviceTempSeries[s]);
      end;
      DeviceTempSeries[s].Clear;
    end;
    DeviceTempSeries[s].SeriesColor:= DevicePlotColor(s);
    DeviceTempSeries[s].Pointer.Style:= psTriangle;
    DeviceTempSeries[s].Pointer.Brush.Color:= DevicePlotColor(s);
    DeviceTempSeries[s].Pointer.Visible:= True;
    DeviceTempSeries[s].ShowPoints:= True;

    {Placeholder titles until the unit serial numbers are read during logging.}
    DevicePlotSeries[s].Title:= 'MPSAS meter '+IntToStr(s+1);
    DeviceTempSeries[s].Title:= 'Temp meter '+IntToStr(s+1);
  end;

  //Show the legend so the meters can be told apart.
  FormLogCont.Chart1.Legend.Visible:= True;
end;

{Update a meter's MPSAS and temperature line titles to its unit serial number.
 Called during logging once the meter has been read (the serial is parsed from
 the meter's info, not the USB/hardware serial used to find it).}
procedure UpdateDeviceSeriesTitle(idx: Integer);
var
  serial: string;
begin
  if idx < 0 then Exit;
  serial:= Trim(SelectedUnitSerialNumber);
  if (serial='') or (serial='0') then Exit; //Not known yet.
  if (idx <= High(DevicePlotSeries)) and Assigned(DevicePlotSeries[idx]) then
    DevicePlotSeries[idx].Title:= 'MPSAS '+serial;
  if (idx <= High(DeviceTempSeries)) and Assigned(DeviceTempSeries[idx]) then
    DeviceTempSeries[idx].Title:= 'Temp '+serial;
end;

{LogResult contains previously read string that should be logged.}
procedure LogOneReading(LogResult:string='');
var
  pieces: TStringList; //delimited result from generic read
  Result: string;
  Hpieces:TStringList; //Humidity result
  Humidity:float;
  SelectedDevicePointer: Integer = 0;
  DeviceCounter:Integer = 0;

  //Staged snow capture state (visible to the nested WriteRecord).
  SnowStaged: Boolean = False;        //True once a valid LED-on reading has been parsed.
  SnowPieces: TStringList = nil;      //Delimited LED-on reading.
  SnowResult: string = '';            //Raw LED-on reading.
  SnowDarkness: Double = 0;           //LED-on snow mpsas.
  SnowFreshState: string = '';        //LED-on reading state (F/P/S).

  {======================================================}
  function CheckRecordCount(): boolean;
  var
    ExpectedPieces:Integer;
  begin
    case SelectedModel of
      model_ADA: ExpectedPieces:=8;
      model_GDM: ExpectedPieces:=3;
      model_C: ExpectedPieces:=9;
      model_LELU, model_DL, model_V, model_LR, model_RPi, model_LL: ExpectedPieces:=6;
      model_DLS: if (SnowLoggingEnabled) then ExpectedPieces:=7 else ExpectedPieces:=6;
    end;
    {The unaveraged value + stale flag are present when Freshness is on, and also
     for the snow model, which always reads via u1x (LED toggles every record).}
    if Freshness or ((SelectedModel=model_DLS) and SnowLoggingEnabled) then
      ExpectedPieces:=ExpectedPieces+2;

    if (pieces.Count >= ExpectedPieces) then
      CheckRecordCount:= True
    else begin
      CheckRecordCount:= False;
      StatusMessage(format(
        'CheckRecordCount fail: SelectedModel = %d, Expected pieces = %d, Actual pieces= %d',
        [SelectedModel, ExpectedPieces, pieces.Count]));
    end;
  end;
  {======================================================}
  procedure WriteRecord(Special: string = '');
  var
    ComposeString: string;
    Reading1, Reading2: integer; //For ADA readings
    RawMag, CompMag: integer; //For magnetometer readings
    ReadingUA:Double;//Unaveraged reading
    ReadingState:String;// Status of reading (fresh or stale)
    //SnowLEDState:String;//Values 'S', 'D', '?'
    ReadingStateField:Integer = -1;

  begin
    ThisMomentUTC:= NowUTC();

    //ThisMoment:= dlheader.ptz.GMTToLocalTime(ThisMomentUTC,SelectedTZLocation, subfix);
    //Changed because Windows runs on Local times, and when people ignore DST, the computed proper local time will be diffenent.
    ThisMoment:= Now();

    //Update chart time range
    FormLogCont.SetFixedTime();

    Application.ProcessMessages;


    //Create new logfile if over 24hr time.
    //if (DayOf(LCStartFileTime) <> DayOf(ThisMoment)) then begin  {Original method to split aat midnight.}
    //Split file and optional hour. Possible conditions:
    // - started today before the rollover hour.
    // - Normally rolls over from a previsouly rolled over file.
    // - When current = Slit hour, only make one new file.
    if ((HourOf(ThisMoment)=SplitDatTime) and
       ((DayOf(ThisMoment)<>LCStartFileDay) or (HourOf(ThisMoment)<>LCStartFileHour))) then begin

      //FTP last file
      if LCTFrequency=lctfAtEndOfDay then
                FormLogCont.FTPSend();

      // Open new file and store header
      if not SingleDatFile then begin
        if SelectedModel = model_V then //Vector device
          WriteDLHeader(DLHeaderStyle, Format('Logged Vector continuously %s.', [setting]))
        else
          WriteDLHeader(DLHeaderStyle, Format('Logged continuously %s.', [setting]));
      end;

      //Update the time that the new file was created
      //LCStartFileTime:= ThisMoment;
      LCStartFileDay:= DayOf(ThisMoment);
      LCStartFileHour:= HourOf(ThisMoment);

      // Display number of logfiles
      Inc(LCLogFileCount);
      FormLogCont.FilesLogged.Caption:= IntToStr(LCLogFileCount);
      if LCLogFileCount = 1 then
        FormLogCont.FilesLabel.Caption:= 'file'
      else
        FormLogCont.FilesLabel.Caption:= 'files';
    end;

    // Display LogFileNameText path
    FormLogCont.LogFileNameText.Caption:= RemoveMultiSlash(LogFileName);
    if TransferDAT then TransferLocalFilename:=LogFileName;
    if TransferCSV then TransferLocalFilename:=CSVLogFileName;
    FormLogCont.TransferLocalFilenameDisplay.Text:= TransferLocalFilename;


    if (Special = '') then begin
      case SelectedModel of
        model_ADA: begin // --- ADA device ---
        { Pull in values }
        Reading1:= StrToIntDef(AnsiMidStr(pieces.Strings[2], 1, 10), 0);
        Reading2:= StrToIntDef(AnsiMidStr(pieces.Strings[4], 1, 10), 0);

        // Update factor
        if Reading2 <> 0 then
          ADAFactor:= Reading1 / Reading2
        else
          ADAFactor:= 0;

        //FormLogCont.ADAFactor.Caption:= Format('%1.3f', [ADAFactor]);

        // YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Hz;Count1;Time1;Count2;Time2
        { TODO : make header description for ADA match data }
        //r,0000000238Hz,0000000000c,0000000.000s,0000000000c,0000000.000s,000,109
        ComposeString:=
          FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', LazSysUtils.NowUTC()) //Date UTC
          + FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', Now()) //Date Local
          + Format('%d;', [StrToIntDef(AnsiMidStr(pieces.Strings[1], 1, 10), 0)])
          //Frequency
          + Format('%d;', [Reading1]) //Counter1
          + Format('%1.3f;', [StrToFloatDef(AnsiMidStr(pieces.Strings[3], 1, 11),
          0)]) //Time1
          + Format('%d;', [Reading2]) //Counter2
          + Format('%1.3f;', [StrToFloatDef(AnsiMidStr(pieces.Strings[5], 1, 11),
          0)])  //Time2
          + Format('%1.3f', [ADAFactor]);

        //Update displayed readings
        FormLogCont.DisplayedReading.Caption:=
          FormatFloat('#0.00', ADAFactor, FPointSeparator);
        FormLogCont.DisplayedNELM.Caption:= Format('%d : rdg1', [Reading1]);
        FormLogCont.Displayedcdm2.Caption:= Format('%d : rdg2', [Reading2]);
        FormLogCont.DisplayedNSU.Caption:= '';

        //Update Chart
        FormLogCont.MPSASSeries.AddXY(Now, ADAFactor);
        //Limit chart size when running for a long time
        if FormLogCont.MPSASSeries.Count > PlotCount then
          FormLogCont.MPSASSeries.ListSource.Delete(0);
      end;  //End of ADA device

      model_GDM: begin // --- GDM device ---
        { Pull in values }
        RawMag:= StrToIntDef(AnsiMidStr(pieces.Strings[1], 1, 10), 0);

        if StrToIntDef(pieces.Strings[2], 0) < 32768 then
          Temperature:= StrToFloatDef(pieces.Strings[2], 0, FPointSeparator) / 128.0
        else
          Temperature:= (StrToFloatDef(pieces.Strings[2], 0, FPointSeparator) -
            65536.0) / 128.0;

        //formula by Kyle Reiter for one sensor:
        //  By = By - k*temperature+C
        //   k = 133.6, C = 3420
        CompMag:=round(RawMag - 133.6 * Temperature + 3420);

        // YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Count;Temp
        //r,0000000000c,xxxx
        ComposeString:=
          FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', LazSysUtils.NowUTC())//Date UTC
          + FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', Now())//Date Local
          + Format('%d;', [RawMag])//Counter
          + Format('%1.3f;', [Temperature])//Temperature
          + Format('%d;', [CompMag])//Compensated magnetometer reading
        ;

        //Update displayed readings
        FormLogCont.DisplayedReading.Caption:=
          FormatFloat('#0', CompMag, FPointSeparator);
        FormLogCont.DisplayedNELM.Caption:= '';
        FormLogCont.Displayedcdm2.Caption:= '';
        FormLogCont.DisplayedNSU.Caption:= '';

        //Update Chart
        FormLogCont.MPSASSeries.AddXY(Now, CompMag);
        //Limit chart size when running for a long time
        if FormLogCont.MPSASSeries.Count > PlotCount then
          FormLogCont.MPSASSeries.ListSource.Delete(0);
      end;  //End of GDM device

      model_C: begin // --- Colour device ---
        { Pull in values }
        Temperature:= StrToFloatDef(
          AnsiLeftStr(pieces.Strings[5], Length(pieces.Strings[5]) - 1), 0,
          FPointSeparator);

        Darkness:= StrToFloatDef(AnsiLeftStr(pieces.Strings[1],Length(pieces.Strings[1]) - 1), 0, FPointSeparator);

        // YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Celsius;number;Hz;mag/arcsec^2,Scaling,Colour')
        ComposeString:=
          FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', ThisMomentUTC) //Date UTC
          + FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', ThisMoment) //Date Local (calculated)
          + FormatFloat('##0.0', Temperature, FPointSeparator) + ';' //Temperature
          + Format('%d;', [StrToIntDef(AnsiLeftStr(pieces.Strings[3],length(pieces.Strings[3]) - 1), 0)]) //counts
          + Format('%d;', [StrToIntDef(AnsiLeftStr(pieces.Strings[2],length(pieces.Strings[2]) - 2), 0)]) //Hz
          + FormatFloat('#0.00', Darkness, FPointSeparator) //mpsas value
          + Format(';%s;', [pieces.Strings[6]]) //Colour scaling
          + Format('%s;', [pieces.Strings[7]]) //Colour
          + Format('%s', [pieces.Strings[8]]) //Colour cycling
        ;
        if Freshness then begin
          ReadingUA:=StrToFloatDef(AnsiLeftStr(pieces.Strings[9],Length(pieces.Strings[1]) - 1), 0, FPointSeparator);
          ReadingState:=pieces.Strings[10];
          case ReadingState of
            'F','P': FreshReading:=True;
            else FreshReading:=False;
          end;
          ComposeString:= ComposeString + ';'+ FormatFloat('#0.00', ReadingUA, FPointSeparator)//unaveraged value
            + ';'+ReadingState; //Status value
        end;

        //Update Chart plot multiple colours
        case SelectedColour of
          0: FormLogCont.RedSeries.AddXY(Now, Darkness);//Red
          1: FormLogCont.BlueSeries.AddXY(Now, Darkness);//Blue
          2: FormLogCont.ClearSeries.AddXY(Now, Darkness);//Clear
          3: FormLogCont.GreenSeries.AddXY(Now, Darkness);//Green
        end;

        //Limit chart size when running for a long time
        if FormLogCont.RedSeries.Count > PlotCount then begin
          FormLogCont.RedSeries.ListSource.Delete(0);
          FormLogCont.GreenSeries.ListSource.Delete(0);
          FormLogCont.BlueSeries.ListSource.Delete(0);
          FormLogCont.ClearSeries.ListSource.Delete(0);
        end;

        //Change colour if fixed mode
        if not ColourCyclingFlag then begin
          SelectedColour:=(SelectedColour+1) mod 4;
          SendGet('f'+IntToStr(SelectedColourScaling)+IntToStr(SelectedColour)+'x');
        end;

        if TemperaturePlotted then begin
          FormLogCont.TempSeries.AddXY(Now, Temperature);
        end;

        if ((MoonData) and (FixedTimeAxisSelection=0)) then
          FormLogCont.MoonSeries.AddXY(Now, MoonElevation);

      end;

      else begin //must be LE or LU device

        { Pull in values }
        Temperature:= StrToFloatDef(
          AnsiLeftStr(pieces.Strings[5], Length(pieces.Strings[5]) - 1), 0,
          FPointSeparator);
        Darkness:= StrToFloatDef(AnsiLeftStr(pieces.Strings[1],
          Length(pieces.Strings[1]) - 1), 0, FPointSeparator);

        { Get best maximum darkness }
        if Darkness>BestDarkness then begin
          BestDarkness:=Darkness;
          BestDarknessTime:=Now();
          BestDarknessTimeString:=Format('%s',[FormatDateTime('yyyy-mm-dd"T"hh:nn:ss',BestDarknessTime)])
        end;

        //Get accessory values; like Humidity/temperature
        if A1Enabled then begin
          Hpieces:= TStringList.Create;
          Hpieces.Delimiter:= ',';
          Hpieces.DelimitedText:=SendGet('A1x');
          if Hpieces.Count=7 then
             Humidity:=StrToFloatDef(Hpieces.Strings[5],0)/( power(2,14) - 2)*100
          else
             Humidity:=0.0;
          if Assigned(Hpieces) then FreeAndNil(Hpieces);
        end;


        // YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Celsius;number;Hz;mag/arcsec^2')
        ComposeString:=
          FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', ThisMomentUTC) //Date UTC
          + FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', ThisMoment) //Date Local
          + FormatFloat('##0.0', Temperature, FPointSeparator) + ';' //Temperature
          + Format('%d;', [StrToIntDef(AnsiLeftStr(pieces.Strings[3], length(pieces.Strings[3]) - 1), 0)]) //counts
          + Format('%d;', [StrToIntDef(AnsiLeftStr(pieces.Strings[2], length(pieces.Strings[2]) - 2), 0)]) //Hz
          + FormatFloat('#0.00', Darkness, FPointSeparator) //mpsas value
        ;

        if GoToEnabled then begin
          ComposeString:= ComposeString + ';'+ Format('%.3f;%.3f',[GoToDesiredZenith,GoToDesiredAzimuth]);
        end;

        if RawFrequencyEnabled then begin
          ComposeString:= ComposeString + ';'+ Format('%d',[StrToIntDef(pieces.Strings[6],0)]);
        end;


        if Freshness then begin
          ReadingStateField:=7;
          ReadingUA:=StrToFloatDef(AnsiLeftStr(pieces.Strings[6],Length(pieces.Strings[1]) - 1), 0, FPointSeparator);
          ReadingState:=pieces.Strings[ReadingStateField];
          case ReadingState of
            'F','P': FreshReading:=True;
            else FreshReading:=False;
          end;
          ComposeString:= ComposeString + ';'+ FormatFloat('#0.00', ReadingUA, FPointSeparator)//unaveraged value
            + ';'+ReadingState; //Status value
        end;

        //Optional Snow factor for datalogger
        if ((SelectedModel=model_DLS) and SnowLoggingEnabled) then begin
          if SnowStaged then begin
            {Staged capture: this record already holds the regular (LED-off)
             reading above; append the LED-on (snow) reading. The leading ';1'
             flags that snow fields follow, mirroring the stored-record snow flag.
             The snow reading is always unaveraged (u1x), so its value and state
             are always meaningful and are logged regardless of the Freshness
             option. No separate unaveraged column is needed (it would duplicate
             the snow MSAS value).}
            ComposeString:= ComposeString + ';1'
              + ';' + FormatFloat('#0.00', SnowDarkness, FPointSeparator) //snow mpsas
              + ';' + SnowFreshState;                                     //snow reading state

            {Plot the LED-on (snow) reading on its own series. Skip implausible
             values (e.g. 0 from a failed/empty read) so they do not stretch the
             auto-scaled MPSAS axis and clip the darker sky reading. The value is
             still written to the log above regardless.}
            if Assigned(SnowSeries) and (SnowDarkness > 0) and (SnowDarkness < 30) then begin
              SnowSeries.AddXY(Now, SnowDarkness);
              if SnowSeries.Count > PlotCount then
                SnowSeries.ListSource.Delete(0);
            end;
          end
          else begin
            {Legacy single-flag behaviour: indicate LED on/off state only.}
            If AccSnowLEDState then
              ComposeString:= ComposeString + ';S'
            else
              ComposeString:= ComposeString + ';D';
          end;
        end;

        if SelectedModel = model_V then begin //Vector device
          // Already done in getreading
          //GetAccel();
          //GetMag(False);
          //not needed?? (included in getaccel)
          //NormalizeAccel(); //Compute acceleration values (In the future, this may be done inside the PIC)

          ComputeAzimuth();
          Altitude:= ComputeAltitude(Ax1, Ay1, Az1);
          Heading:= radtodeg(arctan2(-1 * Mz2, Mx2)) + 180;
          ComposeString:= ComposeString +
            Format(';%0.0f;%0.0f;%0.0f;%0.0f;%0.0f;%0.0f;%0.1f;%0.1f;%0.0f', [
            Ax, Ay, Az,  // Raw Accelerometer values
            Mx, My, Mz,  // Raw magnetometer values
            Altitude ,  // Altitude
            abs(Altitude - 90.0) ,  // Zenith
            Heading]);  // Azimuth
        end;

        //Update position display
        if GoToEnabled then begin
           FormLogCont.GoToZenithDisplay.Caption:=Format('Zen: %.3f',[GoToDesiredZenith]);
           FormLogCont.GoToAzimuthDisplay.Caption:=Format('Azi: %.3f',[GoToDesiredAzimuth]);
        end;

        //Update displayed readings
        if Freshness then
          Darkness:=ReadingUA;

        DarknessString:=Darkness2MPSASString(Darkness);
        FormLogCont.DisplayedReading.Caption:= DarknessString;

        BestDarknessString:=Darkness2MPSASString(BestDarkness);
        if BestDarknessString='--' then
          FormLogCont.BestDarknessLabel.Caption:=Format('Best: %s',[BestDarknessString])
        else
          FormLogCont.BestDarknessLabel.Caption:=Format('Best: %s at %s',[BestDarknessString,BestDarknessTimeString]);

        FormLogCont.LocationNameLabel.Caption:=LocationName;
        FormLogCont.CoordinatesLabel.Caption:=PositionString;

        NELM:= Darkness2NELM(Darkness);
        NELMString:=Darkness2NELMString(Darkness);
        FormLogCont.DisplayedNELM.Caption:= Format('%s NELM',[NELMString]);

        CDM2:=Darkness2CDM2(Darkness);
        CDM2string:=Darkness2CDM2String(Darkness);
        MCDM2string:=Darkness2MCDM2String(Darkness);
        FormLogCont.Displayedcdm2.Caption:= Format('%s cd/m²', [CDM2string]);

        NSU:=Darkness2NSU(Darkness);
        NSUstring:=Darkness2NSUString(Darkness);
        FormLogCont.DisplayedNSU.Caption:= NSUstring+ ' NSU';

        { Optional Moon data }
        if MoonData then begin
          //Calculate Moon position
          //Change sign for Moon calculations
          Moon_Position_Horizontal(
                                   StrToDateTime(DateTimeToStr(LazSysUtils.NowUTC())),
                                   -1.0*MyLongitude,
                                   MyLatitude,
                                   MoonElevation,
                                   MoonAzimuth);

          //Prepare string for output.
          ComposeString:= ComposeString + ';'
                         //Moon Phase angle (0 to 180 degrees).
                         + Format('%.1f;',[moon_phase_angle(StrToDateTime(DateTimeToStr(LazSysUtils.NowUTC())))])

                         //Moon elevation (positive = above horizon, negative = below horizon).
                         + Format('%.3f;',[MoonElevation])

                         //Moon illumination pecent.
                         + Format('%.1f;',[current_phase(StrToDateTime(DateTimeToStr(LazSysUtils.NowUTC())))*100.0])

                        //Moon elevation (positive = above horizon, negative = below horizon).
                        + Format('%.3f',[MoonAzimuth]);
        end;

        {Update Chart}
        if ((not Freshness) or ((Freshness) and (ReadingState<>'S'))) then begin
          {For multi-meter (-SUI) logging, plot each meter on its own line;
           otherwise use the primary MPSAS line. Trim only the line just added to,
           so the primary line is not trimmed twice.}
          if (NumberOfMultipleDevices>0)
             and (CurrentDevicePlotIndex>=0)
             and (CurrentDevicePlotIndex<=High(DevicePlotSeries))
             and Assigned(DevicePlotSeries[CurrentDevicePlotIndex]) then begin
            DevicePlotSeries[CurrentDevicePlotIndex].AddXY(Now, Darkness);
            if DevicePlotSeries[CurrentDevicePlotIndex].Count > PlotCount then
              DevicePlotSeries[CurrentDevicePlotIndex].ListSource.Delete(0);
          end
          else begin
            FormLogCont.MPSASSeries.AddXY(Now, Darkness);
            if FormLogCont.MPSASSeries.Count > PlotCount then
              FormLogCont.MPSASSeries.ListSource.Delete(0);
          end;

          if TemperaturePlotted then begin
            {Per-meter temperature line for multi-meter (-SUI) logging.}
            if (NumberOfMultipleDevices>0)
               and (CurrentDevicePlotIndex>=0)
               and (CurrentDevicePlotIndex<=High(DeviceTempSeries))
               and Assigned(DeviceTempSeries[CurrentDevicePlotIndex]) then begin
              DeviceTempSeries[CurrentDevicePlotIndex].AddXY(Now, Temperature);
              if DeviceTempSeries[CurrentDevicePlotIndex].Count > PlotCount then
                DeviceTempSeries[CurrentDevicePlotIndex].ListSource.Delete(0);
            end
            else begin
              FormLogCont.TempSeries.AddXY(Now, Temperature);
              if FormLogCont.TempSeries.Count > PlotCount then
                FormLogCont.TempSeries.ListSource.Delete(0);
            end;
          end;

          if ((MoonData) and (FixedTimeAxisSelection=0)) then
            FormLogCont.MoonSeries.AddXY(Now, MoonElevation);
        end;
      end; //End of LE LU device
      end; //End of checking models

      //Check if Rotational stage angle infromation must be added to record
      if (rotstage) then
        ComposeString:= ComposeString + ';' + FormLogCont.RSCurrentPositionAngleDisplay.Text;

      { Add GPS values if enabled }
      //'Latitude, Longitude, Elevation, Speed, Satellites'
      //**** grab actual variables, not screen text.
      if FormLogCont.GPSLogIndicatorX.Visible then
        //ComposeString:=ComposeString + ';'+
        //               FormLogCont.GPSLatitudeLabel.Text + ';' +
        //               FormLogCont.GPSLongitudeLabel.Text + ';' +
        //               FormLogCont.GPSElevationlabel.Text + ';' +
        //               FormLogCont.GPSSpeedLabel.Text + ';' +
        //               FormLogCont.GPSSatellites.Text;
      ComposeString:=ComposeString + ';'+
                     Format('%3.4f',[GPSLatitude])  + ';' +
                     Format('%3.4f',[GPSLongitude]) + ';' +
                     Format('%d',[GPSAltitude]) + ';' + //Elevation
                     Format('%.2f',[GPSSpeed]) + ';' +
                     Format('%d',[GPSVisibleSatellites]);


      { Add Humidity value if enabled }
      if A1Enabled then
        ComposeString:= ComposeString + Format(';%2.1f',[Humidity]);


      {Add serial number if multiple devices are being logged}
      if (high(SelectedDevicesArray))>0 then begin
        ComposeString:= ComposeString + Format(';%s',[SelectedUnitSerialNumber]);
      end;

      { Add possible annotation to end of record. }
      if (Length(AnnotateText) > 0) then begin
        ComposeString:= ComposeString + ';' + AnnotateText;
        if (not FormLogCont.PersistentCheckBox.Checked) then begin
          AnnotateText:= '';
          FormLogCont.PendingHotKey.Text:= '';
        end;
      end; //End of annotation.

    end //end of "special" text check when no data was available (missed record).
    else begin
      ComposeString:=
        FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', LazSysUtils.NowUTC()) //Date UTC
        + FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', Now()) //Date Local
        + Special;
    end;

    UpdateAstroTimes();

    if SelectedModel = model_V then begin
      //AltitudeSeries.AddXY(Now,Altitude);
      //AzimuthSeries.AddXY(Now,Heading);
      //Chart2PolarSeries1.AddXY(degtorad(Heading),1.0-Altitude/90.0);
      //Chart2LineSeries1.AddXY(cos(degtorad(Heading+90))*(1.0-abs(Altitude/90.0)),sin(degtorad(Heading+90))*(1.0-abs(Altitude/90.0)));
      FormLogCont.Chart2LineSeries1.Addxy(
        cos(degtorad(Heading + 90)) * (1.0 - abs(Altitude / 90.0)),
        sin(degtorad(Heading + 90)) * (1.0 - abs(Altitude / 90.0)),
        'x',
        MPSASToRGB(Darkness));
      //writeln(format('%6.2f %6.2f',[Heading, degtorad(Heading)]));
    end;

    { Analyze threshold to check if alarm should be sounded.
      Update at every reading.
      A snooze alarm feature is implemented when reading frequency is very low (i.e. every x minutes).
    }
    AlarmRequest:=((Darkness >= AlarmThreshold) and (AlarmSoundEnable));

    //Analyze threshold before writing record
    if Darkness >= LCThresholdValue then begin
      FormLogCont.ThresholdMet.Brush.Color:= clLime;
      try
        AssignFile(LCRecFile, LogFileName);
        Reset(LCRecFile);
        Append(LCRecFile); { Open file for appending. }
        SetTextLineEnding(LCRecFile, #13#10);
        Writeln(LCRecFile, ComposeString); { Write line to file. }
        Flush(LCRecFile);
        CloseFile(LCRecFile);
      except
        StatusMessage('ERROR! IORESULT: ' + IntToStr(IOResult) + ' during WriteRecord');
      end;

      if TransferCSV then begin
        try
          AssignFile(DLRecFile, CSVLogFileName);
          Reset(DLRecFile);
          Append(DLRecFile); { Open CSV file for appending. }
          SetTextLineEnding(DLRecFile, #13#10);
          {Convert .dat text to .csv and write to .csv file}
          Writeln(DLRecFile, AnsiReplaceStr(ComposeString,';',','));
          Flush(DLRecFile);
          CloseFile(DLRecFile);
        except
          StatusMessage('ERROR! IORESULT: ' + IntToStr(IOResult) + ' during WriteCSVRecord to:'+CSVLogFileName);
        end;
      end;

      if TrRdgEnabled then begin
         //Just send Rx value which contains serial number.
         TransferReading(SendGet('Rx'));
      end;



      //Put the compose string on the screen
      FormLogCont.RecordsViewSynEdit.Append(ComposeString);


      //Ensure that latest append is displayed at bottom of window.
      FormLogCont.RecordsViewSynEdit.CaretY:=FormLogCont.RecordsViewSynEdit.Lines.Count;
      Inc(LCLoggedCount);
      FormLogCont.RecordsLogged.Caption:= IntToStr(LCLoggedCount);

      //Check if optional record limit has been reached
      if ((FormLogCont.RecordLimitSpin.Value>0) and (LCLoggedCount>=FormLogCont.RecordLimitSpin.Value)) then
         StopRecording:=True;

    end
    else begin
      FormLogCont.ThresholdMet.Brush.Color:= clRed;
    end;

    //Perform rotational stage operation if enabled
    if (rotstage) then begin
      //Sweep from one side to the other by x steps
      RSser.SendString('S' + IntToStr(FormLogCont.RSPositionStepSpinEdit.Value) +
        chr(13) + chr(10));

      //update step display
      Inc(RSCurrentStepNumber);
      FormLogCont.RSCurrentPositionStepDisplay.Text:= IntToStr(RSCurrentStepNumber);

      ParseRotstage(Trim(RSser.Recvstring(20000)));
      //update status display of rotational stage hardware

      if (StrToIntDef(FormLogCont.RSCurrentPositionStepDisplay.Text, 0) >=
        StrToIntDef(FormLogCont.RSMaxSteps.Text, 0)) then
        StopRecording:= True;
      Application.ProcessMessages;
    end; //end of rotstage

  end;
  {======================================================}
  {Poll the meter until a fresh reading is reported, or until MaxTries is
   exhausted, and return the raw reading string. The caller verifies the piece
   count. The freshness state is located via header_utils' FreshnessField (set by
   the GetReading just performed), not the last field, because the snow-LED status
   (S/D) is appended after the freshness state. If the current reading carries no
   freshness field (FreshnessField < 0) the state cannot be determined, so a
   single reading is returned. The snow model always reads via u1x, so its
   readings carry the field and are waited on regardless of the Freshness toggle.}
  function GetFreshReading(MaxTries: Integer): string;
  var
    fpieces: TStringList;
    tries: Integer;
    state: string;
    res: string;
  begin
    res:= GetReading;
    fpieces:= TStringList.Create;
    try
      fpieces.Delimiter:= ',';
      fpieces.StrictDelimiter:= False;
      tries:= 0;
      repeat
        state:= '';
        fpieces.DelimitedText:= res;
        if FreshnessField < 0 then Break; //No freshness field present; cannot detect.
        if fpieces.Count > FreshnessField then begin
          state:= fpieces.Strings[FreshnessField];
          if (state='F') or (state='P') then Break; //Fresh frequency or fresh period.
        end;
        Inc(tries);
        StatusMessage(Format('Waiting for fresh reading (try %d of %d), state: %s',
          [tries, MaxTries, state]));
        Application.ProcessMessages;
        if tries >= MaxTries then Break;
        res:= GetReading;
      until False;
    finally
      fpieces.Free;
    end;
    GetFreshReading:= res;
  end;
  {======================================================}
  {Perform a staged snow capture for the currently-selected DLS meter: an
   LED-off (regular) reading, then an LED-on (snow) reading, combined into one
   record by WriteRecord. The caller has already confirmed this meter is a
   snow-logging DLS, so this works for a single meter or for one meter within
   the multi-meter (-SUI) loop.}
  procedure DoSnowStagedCapture;
  var
    SkipIndex: Integer; //Local: a for-loop counter cannot be an enclosing-scope variable.
  begin
    SnowStaged:= False;
    SnowDarkness:= 0;
    SnowFreshState:= '';

    if not Assigned(SnowPieces) then SnowPieces:= TStringList.Create;
    SnowPieces.Delimiter:= ',';
    SnowPieces.StrictDelimiter:= False;

    {Stage 1: ensure the snow LED is OFF, then capture the regular reading.}
    if AccSnowLEDState then
      Form1.SnowLEDStatus(SendGet('A50x'));
    StatusMessage('Snow staging 1/3: getting regular (LED-off) reading.');
    Result:= GetFreshReading(SnowFreshMaxTries); //LED-off (ambient) reading.
    pieces.DelimitedText:= Result;

    if CheckRecordCount then begin
      {Stage 2: turn the snow LED ON.}
      StatusMessage('Snow staging 2/3: turning snow LED on.');
      Form1.SnowLEDStatus(SendGet('A51x'));

      {Stage 3: discard SnowReadSkip fresh readings so the sensor settles to the
       LED-on level, then capture a fresh snow reading. The snow model reads via
       u1x, whose stale flag stays 'S' until a genuine fresh window completes,
       so the capture waits for the sensor to integrate the new (LED-on) light.}
      for SkipIndex:= 1 to SnowReadSkip do begin
        StatusMessage(Format('Snow staging: discarding settle reading %d of %d.',
          [SkipIndex, SnowReadSkip]));
        GetFreshReading(SnowFreshMaxTries); //Discarded; lets the sensor settle.
      end;
      StatusMessage('Snow staging 3/3: getting snow (LED-on) reading.');
      SnowResult:= GetFreshReading(SnowFreshMaxTries); //LED-on (snow) reading.
      SnowPieces.DelimitedText:= SnowResult;

      {Return the LED to OFF so it is not left on between records.}
      Form1.SnowLEDStatus(SendGet('A50x'));

      {Parse the LED-on reading using header_utils' field indices (set by the
       GetReading just performed). The snow mpsas is field 1; the freshness state
       is at FreshnessField.}
      if SnowPieces.Count >= 6 then begin
        SnowDarkness:= StrToFloatDef(
          AnsiLeftStr(SnowPieces.Strings[1], Length(SnowPieces.Strings[1])-1),
          0, FPointSeparator);
        if (FreshnessField >= 0) and (SnowPieces.Count > FreshnessField) then
          SnowFreshState:= SnowPieces.Strings[FreshnessField]
        else
          SnowFreshState:= 'S';
        SnowStaged:= True;
      end
      else
        StatusMessage('Snow staging: LED-on reading incomplete, count='
          + IntToStr(SnowPieces.Count));

      {Combine: WriteRecord builds the regular record and, because SnowStaged is
       set, appends the LED-on values and plots the snow series.}
      WriteRecord;
    end
    else begin
      {Regular (LED-off) reading failed: record a miss.}
      Inc(RecordsMissedCount);
      WriteRecord(';;;');
      FormLogCont.RecordsMissed.Color:= clRed;
      FormLogCont.RecordsMissed.Font.Color:= clWhite;
      FormLogCont.RecordsMissed.Caption:= IntToStr(RecordsMissedCount);
    end;
  end;
  {======================================================}

begin

  Recording:= True;
  pieces:= TStringList.Create;
  pieces.Delimiter:= ',';
  pieces.StrictDelimiter:= False; //Parse spaces also


  if LogResult='' then begin //There is no prepared result, get it from the meter(s)

  { Cycle through the list of available LogContinuous devices. Each meter is
    handled per its own model and snow setting: a snow-logging DLS meter does a
    staged LED-off/LED-on capture; any other meter does a single regular read.}
   DeviceCounter:=NumberOfMultipleDevices;
   CurrentDevicePlotIndex:=0; //Single-meter case plots to the primary line.
  repeat
    if DeviceCounter>0 then begin //set the selected device to one on the list
      Form1.FoundDevices.Selected[SelectedDevicesArray[DeviceCounter-1].Index]:=True;
      Form1.SelectDevice;
      StatusMessage('SelectedUnitSerialNumber: '+ SelectedUnitSerialNumber);
      {Series index of this meter: the first meter in the -SUI list (highest
       DeviceCounter) is index 0, so it keeps the primary MPSASSeries line.}
      CurrentDevicePlotIndex:=NumberOfMultipleDevices-DeviceCounter;
      Dec(DeviceCounter); {Go to the next device after this device has been accessed below}
    end;

    {Refresh this meter's model and snow-enabled state before deciding how to read
     it. SelectDevice (multi-meter) clears SelectedModel, so GetVersion re-queries
     the meter, including its per-meter snow setting via A5x. For a single,
     already-identified meter SelectedModel is non-zero, so this is skipped and
     behaviour is unchanged.}
    if SelectedModel=0 then GetVersion;

    {Label this meter's plot lines with its unit serial number (not USB serial).}
    UpdateDeviceSeriesTitle(CurrentDevicePlotIndex);

    if (SelectedModel=model_DLS) and SnowLoggingEnabled and SnowStagedLogging then
      {Snow-logging DLS meter: staged LED-off/LED-on capture.}
      DoSnowStagedCapture
    else begin
      {Any other meter: a single regular reading, with up to three tries.}
      StatusMessage('Getting log record.');
      Result:= GetReading;
      pieces.DelimitedText:= Result;
      if CheckRecordCount then WriteRecord
      else begin {Try a second time}
        Result:= GetReading;
        pieces.DelimitedText:= Result;
        if CheckRecordCount then WriteRecord
        else begin {Try a third time}
          Result:= GetReading;
          pieces.DelimitedText:= Result;
          if CheckRecordCount then WriteRecord
          else begin {Indicate that tries failed}
            Inc(RecordsMissedCount);
            WriteRecord(';;;'); {Empty fields}
            FormLogCont.RecordsMissed.Color:= clRed;
            FormLogCont.RecordsMissed.Font.Color:= clWhite;
            FormLogCont.RecordsMissed.Caption:= IntToStr(RecordsMissedCount);
          end;
        end;
      end;
    end;
  until DeviceCounter <1;

  end else begin {was passed a result to log}
    Result:=LogResult;
    pieces.DelimitedText:= Result;
    if CheckRecordCount then WriteRecord
    else StatusMessage('Failed CheckRecordCount, got:'+IntToStr(pieces.Count));
  end;

  {Perform FTP transfer (if required) regardless of threshold setting so that server does not think system is dead.
   This code to send the file must be after the file is closed by regular logging because Windows complains that
    the file cannot be shared (EFOpenError) if the file is not closed.}
  if LCTFrequency=lctfAfterEveryRecord then begin
      try
      FormLogCont.FTPSend();

      except
        StatusMessage('FTP send error during LogOneReading');
        ShowMessage('FTP send error during LogOneReading'
           + sLineBreak
           + 'Check Log Continuous Transfer tab, and ensure all entries are filled properly, or set frequency to Never.');
        StopRecording:=True;
      end;

    end;

  Recording:= False;
  if Assigned(pieces) then FreeAndNil(pieces);
  if Assigned(SnowPieces) then FreeAndNil(SnowPieces);
end;

{ TFormLogCont }

procedure TFormLogCont.OpenFileButtonClick(Sender: TObject);
begin
  OpenLogDialog.InitialDir:= appsettings.LogsDirectory;
  if OpenLogDialog.Execute then
  begin
    Form2.Memo1.Lines.LoadFromFile(OpenLogDialog.Filename);
    Form2.Show;
  end;

end;

procedure TFormLogCont.AnnotateButtonClick(Sender: TObject);
begin
  //Initiate a recording with annotated text while already in record-mode.
  AnnotateText:= FormLogCont.AnnotateEdit.Text;
  PendingHotKey.Text:= AnnotateText;
  if (not SynchronizedCheckBox.Checked) then
    LogOneReading;
end;

procedure TFormLogCont.Text24to12hr(Hour24: Word; Receiver: TLabel);
var
  suffix:String;
  hour:word;
begin
  if Hour24>11 then suffix:= 'PM' else suffix:= 'AM';
  if Hour24>12 then hour:=Hour24-12 else if Hour24=0 then hour:=12 else hour:=Hour24;
  Receiver.Caption:=Format('%d%s',[hour, suffix]);
end;

procedure TFormLogCont.FixedFromSpinEditChange(Sender: TObject);
begin
  if not FormCreating then begin
    FromHour:=FixedFromSpinEdit.Value;
    vConfigurations.WriteString('LogContinuousSettings','FromHour',IntToStr(FromHour));
    Text24to12hr(FromHour,From12hrLabel );
    SetFixedTime();
  end;
end;

procedure TFormLogCont.FixedToSpinEditChange(Sender: TObject);
begin
  if not FormCreating then begin
    ToHour:=FixedToSpinEdit.Value;
    vConfigurations.WriteString('LogContinuousSettings','ToHour',IntToStr(ToHour));
    Text24to12hr(ToHour,To12hrLabel );
    SetFixedTime();
  end;
end;

procedure TFormLogCont.FixedTimeRadiosClick(Sender: TObject);
begin

  FixedTimeAxisSelection:=FixedTimeRadios.ItemIndex;
  case FixedTimeAxisSelection of
    0: begin
      FixedTimePageControl.PageIndex:=0;
      MoonSeries.Clear;
    end;
    1: FixedTimePageControl.PageIndex:=1;
    2..5: FixedTimePageControl.PageIndex:=2;
  end;

  vConfigurations.WriteString('LogContinuousSettings','FixedTimeRangeSelection',IntToStr(FixedTimeAxisSelection));
  SetFixedTime();

end;

procedure TFormLogCont.FormResize(Sender: TObject);
var
  FormHeight, FormWidth:Integer;
begin

  if not FormCreating then begin
   FormHeight:=FormLogCont.Height;
   vConfigurations.WriteString('LogContinuousSettings','Height',IntToStr(FormHeight));
   FormWidth:=FormLogCont.Width;
   vConfigurations.WriteString('LogContinuousSettings','Width',IntToStr(FormWidth));
  end;

end;

procedure TFormLogCont.FixedAutoReadingsToggleChange(Sender: TObject);
begin
  FixedReadingRange:=FixedAutoReadingsToggle.Checked;
  vConfigurations.WriteBool('LogContinuousSettings','FixedReadingRange',FixedReadingRange);
  SetFixedReadings();
end;
procedure TFormLogCont.FromReadingSpinEditChange(Sender: TObject);
begin
  if not FormCreating then begin
    FromReading:=FromReadingSpinEdit.Value;
    vConfigurations.WriteString('LogContinuousSettings','FromReading',IntToStr(FromReading));
    SetFixedReadings();
  end;
end;
procedure TFormLogCont.ToReadingSpinEditChange(Sender: TObject);
begin
  if not FormCreating then begin
    ToReading:=ToReadingSpinEdit.Value;
    vConfigurations.WriteString('LogContinuousSettings','ToReading',inttostr(ToReading));
    SetFixedReadings();
  end;
end;

procedure TFormLogCont.FixedAutoTemperatureReadingsToggleChange(Sender: TObject);
begin
  FixedTemperatureRange:=FixedAutoTemperatureReadingsToggle.Checked;
  vConfigurations.WriteBool('LogContinuousSettings','FixedTemperatureReadingRange',FixedTemperatureRange);
  SetFixedTemperatureReadings();
end;
procedure TFormLogCont.FromTemperatureReadingSpinEditChange(Sender: TObject);
begin
  if not FormCreating then begin
    FromTemperatureReading:=FromTemperatureReadingSpinEdit.Value;
    vConfigurations.WriteString('LogContinuousSettings','FromTemperatureReading',IntToStr(FromTemperatureReading));
    SetFixedTemperatureReadings();
  end;
end;
procedure TFormLogCont.ToTemperatureReadingSpinEditChange(Sender: TObject);
begin
  if not FormCreating then begin
    ToTemperatureReading:=ToTemperatureReadingSpinEdit.Value;
    vConfigurations.WriteString('LogContinuousSettings','ToTemperatureReading',inttostr(ToTemperatureReading));
    SetFixedTemperatureReadings();
  end;
end;

procedure TFormLogCont.SetFixedTime();
var
  LoopTime:TDateTime;
begin

  //StatusMessage('SetFixedTime');  Application.ProcessMessages;

  if SelectedTZLocation<>'' then begin

   ThisMomentUTC:= NowUTC();
   ThisMoment:= Now();

  { Determine plotting X axes
    get morning and evening twilight times for current date:}
  case FixedTimeAxisSelection of
    1:begin //Fixed hours
      TwilightEveningTimeStamp:=RecodeHour(ThisMomentUTC,FromHour);
      TwilightMorningTimeStamp:=RecodeHour(ThisMomentUTC,ToHour);
    end;
    2:begin //Sunset
      TwilightMorningTimeStamp:=ptz.GMTToLocalTime(Sun_Rise(ThisMomentUTC,MyLatitude,-1.0 * MyLongitude),SelectedTZLocation, subfix);
      TwilightEveningTimeStamp:=ptz.GMTToLocalTime(Sun_Set(ThisMomentUTC,MyLatitude,-1.0 * MyLongitude),SelectedTZLocation, subfix);
      end;
    3:begin //Civil
      TwilightMorningTimeStamp:=ptz.GMTToLocalTime(Morning_Twilight_Civil(ThisMomentUTC,MyLatitude,-1.0 * MyLongitude),SelectedTZLocation, subfix);
      TwilightEveningTimeStamp:=ptz.GMTToLocalTime(Evening_Twilight_Civil(ThisMomentUTC,MyLatitude,-1.0 * MyLongitude),SelectedTZLocation, subfix);
      end;
    4:begin //Nautical
      TwilightMorningTimeStamp:=ptz.GMTToLocalTime(Morning_Twilight_Nautical(ThisMomentUTC,MyLatitude,-1.0 * MyLongitude),SelectedTZLocation, subfix);
      TwilightEveningTimeStamp:=ptz.GMTToLocalTime(Evening_Twilight_Nautical(ThisMomentUTC,MyLatitude,-1.0 * MyLongitude),SelectedTZLocation, subfix);
      end;
    5:begin //Astronomical
      TwilightMorningTimeStamp:=ptz.GMTToLocalTime(Morning_Twilight_Astronomical(ThisMomentUTC,MyLatitude,-1.0 * MyLongitude),SelectedTZLocation, subfix);
      TwilightEveningTimeStamp:=ptz.GMTToLocalTime(Evening_Twilight_Astronomical(ThisMomentUTC,MyLatitude,-1.0 * MyLongitude),SelectedTZLocation, subfix);
      end;
  end;

  {Fix up timestamps to be for today as Astronomical evening might be for yesterday due to UTC calcs.}
  TwilightMorningTimeStamp:=RecodeDate(TwilightMorningTimeStamp, YearOf(ThisMoment), MonthOf(ThisMoment), DayOf(ThisMoment));
  TwilightEveningTimeStamp:=RecodeDate(TwilightEveningTimeStamp, YearOf(ThisMoment), MonthOf(ThisMoment), DayOf(ThisMoment));

  {Show current night viewing and only change when a new night viewing is available:
     | N | N | N | N | N | N | N | N | N  N=Now
     |   |   |   |   |   |   |   |   |
     E<----->M   E<----->M   E<----->M    E=Evening to M=Morning plot
     --->|<--day---->|<--day---->|<--day
  }

  if ThisMoment>TwilightEveningTimeStamp then begin
    StartPlotTimeStamp:=TwilightEveningTimeStamp; //This evening
    EndPlotTimeStamp:=IncDay(TwilightMorningTimeStamp) //Tomorrow morning
  end
  else begin
    StartPlotTimeStamp:=IncDay(TwilightEveningTimeStamp,-1); //Yesterday evening
    EndPlotTimeStamp:=TwilightMorningTimeStamp; //This morning
  end;
  FormLogCont.FixedTimeMemo.Text:=
    'Evening range:' + sLineBreak+
    FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', StartPlotTimeStamp)+ sLineBreak+
    FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', EndPlotTimeStamp)+ sLineBreak
    //+'N='+FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', ThisMoment)+ sLineBreak
    //+'E='+FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', TwilightEveningTimeStamp)+ sLineBreak
    //+'M='+FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', TwilightMorningTimeStamp)
    ;


  case FixedTimeAxisSelection of
    0: begin
      FixedTimeMemo.Visible:=False;
      Chart1.AxisList[0].Range.UseMax:=False;
      Chart1.AxisList[0].Range.UseMin:=False;
      { Update the X axis title.}
      try
      if MPSASSeries.Count>0 then begin
        StartPlotTimeStamp:=MPSASSeries.GetXValue(0);
        EndPlotTimeStamp:=MPSASSeries.GetXValue(MPSASSeries.Count-1);
        if (DateOf(StartPlotTimeStamp)=DateOf(EndPlotTimeStamp)) then
          FormLogCont.Chart1.AxisList[0].Title.Caption:=FormatDateTime('yyyy-mm-dd',StartPlotTimeStamp)
        else
          FormLogCont.Chart1.AxisList[0].Title.Caption:=FormatDateTime('yyyy-mm-dd',StartPlotTimeStamp)  +' to ' + FormatDateTime('yyyy-mm-dd',EndPlotTimeStamp);
        end;
      except
        StatusMessage('Could not get Start/End plot times for x-axis title.');
      end;
    end;
    1..5: begin
      FixedTimeMemo.Visible:=True;
      Chart1.AxisList[0].Range.Min:= StartPlotTimeStamp;
      Chart1.AxisList[0].Range.Max:= EndPlotTimeStamp;
      Chart1.AxisList[0].Range.UseMax:=True;
      Chart1.AxisList[0].Range.UseMin:=True;

      { Update the X axis title.}
      FormLogCont.Chart1.AxisList[0].Title.Caption:=FormatDateTime('yyyy-mm-dd', StartPlotTimeStamp)+' to '+ FormatDateTime('yyyy-mm-dd', EndPlotTimeStamp);

      { Check for change in plot time and redraw Moon plot if fixed x axis.}
      if ((OldStartPlotTimeStamp<>StartPlotTimeStamp) or (OldEndPlotTimeStamp<>EndPlotTimeStamp))then begin
        if BestDarknessTime<StartPlotTimeStamp then begin
          BestDarknessTime:=StartPlotTimeStamp;
          BestDarkness:=0;
        end;
        if MoonData then begin
          MoonSeries.Clear;
          LoopTime:=StartPlotTimeStamp;
          while LoopTime<EndPlotTimeStamp do begin
            try
              Moon_Position_Horizontal(
                                       StrToDateTime(DateTimeToStr(ptz.LocalTimeToGMT(LoopTime, SelectedTZLocation))),
                                       -1.0*MyLongitude,
                                       MyLatitude,
                                       MoonElevation,
                                       MoonAzimuth);
              FormLogCont.MoonSeries.AddXY(LoopTime, MoonElevation);
            except
              //DST spring may have caused missing time, record in log file only and skip plotting.
              StatusMessage('ERROR! IORESULT: ' + IntToStr(IOResult) + ' during Moon plotting');
            end;
            LoopTime:=IncMinute(LoopTime);
          end;
        end;//end checking moondata
        OldStartPlotTimeStamp:=StartPlotTimeStamp;
        OldEndPlotTimeStamp:=EndPlotTimeStamp;
      end;//End checking plot change
    end;
  end;




  end;



  Application.ProcessMessages;

end;

procedure TFormLogCont.SetFixedReadings();
begin
  Chart1.AxisList[1].Range.Min:=FromReading;
  Chart1.AxisList[1].Range.Max:=ToReading;
  if FixedReadingRange then begin
    FromReadingLabel.Visible:=True;
    FromReadingSpinEdit.Visible:=True;
    ToReadingLabel.Visible:=True;
    ToReadingSpinEdit.Visible:=True;
    FixedAutoReadingsToggle.Caption:='Fixed';
    Chart1.AxisList[1].Range.UseMax:=True;
    Chart1.AxisList[1].Range.UseMin:=True;
    Chart1.AxisList[1].Minors[0].Intervals.Count:=2;
    Chart1.AxisList[1].Minors[0].Intervals.Options:=[aipUseCount, aipUseMinLength];
    Chart1.AxisList[1].Minors[0].Visible:=True;
  end else begin
    FromReadingLabel.Visible:=False;
    FromReadingSpinEdit.Visible:=False;
    ToReadingLabel.Visible:=False;
    ToReadingSpinEdit.Visible:=False;
    FixedAutoReadingsToggle.Caption:='Auto';
    Chart1.AxisList[1].Range.UseMax:=False;
    Chart1.AxisList[1].Range.UseMin:=False;
    Chart1.AxisList[1].Minors[0].Visible:=False;
  end;
end;

procedure TFormLogCont.SetFixedTemperatureReadings();
begin
  Chart1.AxisList[2].Range.Min:=FromTemperatureReading;
  Chart1.AxisList[2].Range.Max:=ToTemperatureReading;
  if FixedTemperatureRange then begin
    FromTemperatureReadingLabel.Visible:=True;
    FromTemperatureReadingSpinEdit.Visible:=True;
    FromTemperatureUnits.Visible:=True;
    ToTemperatureReadingLabel.Visible:=True;
    ToTemperatureReadingSpinEdit.Visible:=True;
    ToTemperatureUnits.Visible:=True;
    FixedAutoTemperatureReadingsToggle.Caption:='Fixed';
    Chart1.AxisList[2].Range.UseMax:=True;
    Chart1.AxisList[2].Range.UseMin:=True;
    if (FromTemperatureReadingSpinEdit.Value>=ToTemperatureReadingSpinEdit.Value) then
      FromTemperatureReadingSpinEdit.Color:=clRed
    else
      FromTemperatureReadingSpinEdit.Color:=clDefault;
    if (ToTemperatureReadingSpinEdit.Value<=FromTemperatureReadingSpinEdit.Value) then
      ToTemperatureReadingSpinEdit.Color:=clRed
    else
      ToTemperatureReadingSpinEdit.Color:=clDefault;
    //Chart1.AxisList[2].Minors[0].Intervals.Count:=2;
    //Chart1.AxisList[2].Minors[0].Intervals.Options:=[aipUseCount, aipUseMinLength];
    //Chart1.AxisList[2].Minors[0].Visible:=True;
  end else begin
    FromTemperatureReadingLabel.Visible:=False;
    FromTemperatureReadingSpinEdit.Visible:=False;
    FromTemperatureUnits.Visible:=False;
    ToTemperatureReadingLabel.Visible:=False;
    ToTemperatureReadingSpinEdit.Visible:=False;
    ToTemperatureUnits.Visible:=False;
    FixedAutoTemperatureReadingsToggle.Caption:='Auto';
    Chart1.AxisList[2].Range.UseMax:=False;
    Chart1.AxisList[2].Range.UseMin:=False;
    //Chart1.AxisList[2].Minors[0].Visible:=False;
  end;
end;

procedure TFormLogCont.NightCheckBoxChange(Sender: TObject);
begin
  NightMode:=NightCheckBox.Checked;
  vConfigurations.WriteBool('LogContinuousSettings','NightMode',NightMode);
  ChartColor();
end;

procedure TFormLogCont.PairSplitterTopResize(Sender: TObject);
var
  SliderPosition:Integer;
begin

  if not FormCreating then begin
   SliderPosition:=PairSplitter1.Position;
   vConfigurations.WriteString('LogContinuousSettings','Slider',IntToStr(SliderPosition));
  end;
end;


procedure TFormLogCont.ChartColor();
const
  clNightBackground = $261d14;
  clNightLines = $473424;
  clNightText = $5d1fc5;
  clNightGridText =clWhite;

  clDayBackground = clWhite;
  clDayLines = clBlack;
  clDayText = clRed;
  clDayGridText=clDefault;

begin

  if NightMode then begin
    Chart1.BackColor:=clNightBackground;
    Chart1.Color:=clNightBackground;
    Chart1.Frame.Color:=clNightLines;
    Chart1.BottomAxis.Grid.Color:=clNightLines;
    Chart1.BottomAxis.TickColor:=clNightLines;
    Chart1.BottomAxis.Marks.LabelFont.Color:=clNightGridText;
    Chart1.BottomAxis.Title.LabelFont.Color:=clNightGridText;
    Chart1.LeftAxis.Grid.Color:=clNightLines;
    Chart1.LeftAxis.TickColor:=clNightLines;
    Chart1.AxisList[3].Marks.LabelFont.Color:=clNightGridText;
    Chart1.AxisList[3].Title.LabelFont.Color:=clNightGridText;
    MoonSeries.LinePen.Color:=clNightGridText;
    Chart1.AxisList[1].Minors[0].Grid.Color:=clNightLines;
  end
  else begin //Daytime mode
    Chart1.BackColor:=clDayBackground;
    Chart1.Color:=clDayBackground;
    Chart1.Frame.Color:=clDayLines;
    Chart1.BottomAxis.Grid.Color:=clDayLines;
    Chart1.BottomAxis.TickColor:=clDayLines;
    Chart1.BottomAxis.Marks.LabelFont.Color:=clDayGridText;
    Chart1.BottomAxis.Title.LabelFont.Color:=clDayGridText;
    Chart1.LeftAxis.Grid.Color:=clDayLines;
    Chart1.LeftAxis.TickColor:=clDayLines;
    Chart1.AxisList[3].Marks.LabelFont.Color:=clDayGridText;
    Chart1.AxisList[3].Title.LabelFont.Color:=clDayGridText;
    MoonSeries.LinePen.Color:=clDayLines;
    Chart1.AxisList[1].Minors[0].Grid.Color:=clDayLines;
  end;

  MPSASSeries.SeriesColor:=clRed;
  TempSeries.SeriesColor:=TemperatureColor;
  Chart1.AxisList[2].TickColor:=TemperatureColor;
  Chart1.AxisList[2].Title.LabelFont.Color:=TemperatureColor;
  Chart1.AxisList[2].Marks.LabelFont.Color:=TemperatureColor;


end;

procedure TFormLogCont.TestTransferClick(Sender: TObject);
begin
  FTPSend();
end;

procedure TFormLogCont.GoToButtonScriptHelpClick(Sender: TObject);
begin
  MessageDlg(
     'GoTo script file location',
     'GoTo script files are located here:'+sLineBreak+DataDirectory+'*.goto',
     mtInformation,
     [mbOK],'');
end;

procedure TFormLogCont.GoToZenAziButtonClick(Sender: TObject);
begin
  GoToDesiredZenith:=ZenFloatSpinEdit.Value;
  GoToDesiredAzimuth:=AziFloatSpinEdit.Value;
  GoToCommand(gtSetZenithAzimuth);
end;

procedure TFormLogCont.FreshAlertTestClick(Sender: TObject);
begin
  FreshSound.StopSound;
  FreshSound.Execute;
end;

procedure TFormLogCont.GetZenAziButtonClick(Sender: TObject);
begin
  GoToCommand(gtGetZenithAzimuth);
end;

procedure TFormLogCont.GoToBaudSelectChange(Sender: TObject);
begin
  { Save the GoTo baud selection. }
  vConfigurations.WriteString('GoToSettings','GoTo Baud',GoToBaudSelect.Text);
end;

procedure ChangeGotoScript(ScriptName: String);
begin
  SelectedGoToCommandFile:=ScriptName;
  {store new selection}
  vConfigurations.WriteString('GoTo','CommandFile',SelectedGoToCommandFile);
  GoToReadScript();
end;

procedure TFormLogCont.GoToCommandFileComboBoxChange(Sender: TObject);
begin
  {Get selected GoTo script name. }
  ChangeGotoScript(GoToCommandFileComboBox.Text);
end;

procedure TFormLogCont.GoToCommBusyTimer(Sender: TObject);
begin

    Inc(GoToCommBusyTime);
    if GoToCommBusyTime>=GoToCommBusyLimit then begin
      GoToDisConnect();
    end;

end;

procedure TFormLogCont.GoToMachineSelectChange(Sender: TObject);
begin
  {Get the GoTo machine selection. }
  GotoMachineSelection:=GoToMachineSelect.Text;

  { Save the GoTo machine selection. }
  vConfigurations.WriteString('GoToSettings','GoTo Machine',GotoMachineSelection);
end;

procedure TFormLogCont.GoToPortSelectChange(Sender: TObject);
begin
  { Save the GoTo port selection. }
  GoToPortSelection:=GoToPortSelect.Text;
  vConfigurations.WriteString('GoToSettings','GoTo Port',GoToPortSelection);
end;

procedure TFormLogCont.GPSEnableClick(Sender: TObject);
begin
  GPSLogIndicatorX.Visible:=GPSEnable.Checked;
  GPSLogIndicator.Visible:=GPSLogIndicatorX.Visible;

  { Save the GPS port selection. }
  vConfigurations.WriteBool(SerialINIsection,'GPS Enabled',GPSEnable.Checked);

  if GPSEnable.Checked then begin
    GPSConnect();
    GPSSNRClear();
  end
  else
    GPSDisconnect();

  GPSPortSelect.Enabled:=not GPSEnable.Checked;
  GPSBaudSelect.Enabled:=not GPSEnable.Checked;

end;

procedure TFormLogCont.alert2sChange(Sender: TObject);
begin
  Alert2sEnable:=alert2s.Checked;
  vConfigurations.WriteBool('LogContinuousSettings', 'Alert2s', Alert2sEnable);
end;

procedure TFormLogCont.AlarmTestButtonClick(Sender: TObject);
begin
  AlarmSound.StopSound;
  AlarmSound.Execute;
end;

procedure TFormLogCont.AlarmSoundEnableCheckChange(Sender: TObject);
begin
  vConfigurations.WriteBool('LogContinuousSettings', 'AlarmSound', AlarmSoundEnableCheck.Checked);
  AlarmSoundEnable:=AlarmSoundEnableCheck.Checked;
end;

procedure TFormLogCont.AlarmThresholdFloatSpinEditChange(Sender: TObject);
begin
  //Save value in registry
  vConfigurations.WriteString('LogContinuousSettings', 'AlarmThreshold', format('%f',[AlarmThresholdFloatSpinEdit.Value]));
  AlarmThreshold:=AlarmThresholdFloatSpinEdit.Value;
end;

procedure TFormLogCont.PreAlertTestButtonClick(Sender: TObject);
begin
  PreAlertSound.StopSound;
  PreAlertSound.Execute;
end;

procedure TFormLogCont.checkMTAStartClick(Sender: TObject);
begin
  //check if NowStart should be checked
  if checkMTAStart.Checked or checkMTNStart.Checked or
    checkMTCStart.Checked or checkSRStart.Checked or
    checkSSStart.Checked or checkETCStart.Checked or
    checkETNStart.Checked or checkETAStart.Checked then
  begin
    checkNowStart.Checked:= False;
  end
  else
  begin
    checkNowStart.Checked:= True;
  end;

  SaveStartStopSettings();
end;

procedure TFormLogCont.checkMTAStopClick(Sender: TObject);
begin
  //check if NeverStop should be checked
  if checkMTAStop.Checked or checkMTNStop.Checked or
    checkMTCStop.Checked or checkSRStop.Checked or checkSSStop.Checked or
    checkETCStop.Checked or checkETNStop.Checked or checkETAStop.Checked then
  begin
    checkNeverStop.Checked:= False;
  end
  else
  begin
    checkNeverStop.Checked:= True;
  end;

  SaveStartStopSettings();

end;

procedure TFormLogCont.checkNeverStopClick(Sender: TObject);
begin
  //Clear off other STOP checkboxes if checked
  if checkNeverStop.Checked then
  begin
    checkMTAStop.Checked:= False;
    checkMTNStop.Checked:= False;
    checkMTCStop.Checked:= False;
    checkSRStop.Checked:= False;
    checkSSStop.Checked:= False;
    checkETCStop.Checked:= False;
    checkETNStop.Checked:= False;
    checkETAStop.Checked:= False;
  end;
  SaveStartStopSettings(); //Save to ini file
end;

procedure TFormLogCont.FormCreate(Sender: TObject);
var
  Info : TSearchRec;
  Count : Longint;
  s,st: string; //used in For..in..do loop
  runret: AnsiString;
begin

  FormCreating:=True;

  FormLogCont.Caption:=FormLogCont.Caption+'  ('+UDMversion+')';

  {Restore height and width and slider}
  FormLogCont.Height:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','Height','0'),0);
  FormLogCont.Width:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','Width','0'),0);
  PairSplitter1.Position:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','Slider','0'),0);;

  OldStartPlotTimeStamp:=Now();
  OldEndPlotTimeStamp:=Now();

  {$ifdef Linux}
   if RunCommand('which',['bash'],runret) then BashPath:=Trim(runret);
   if RunCommand('which',['expect'],runret) then ExpectPath:=Trim(runret);
  {$endif}
  {$ifdef Darwin}
   if RunCommand('which',['bash'],runret) then BashPath:=Trim(runret);
   if RunCommand('which',['expect'],runret) then ExpectPath:=Trim(runret);
  {$endif}


  //Get previous settings from configuration file
  LCTrigSecondsSpin.Value:= LimitInteger(
    StrToIntDef(vConfigurations.ReadString('LogContinuousSettings',
    'Seconds', '1'), 1), 1, 255);
  LCTrigMinutesSpin.Value:= LimitInteger(
    StrToIntDef(vConfigurations.ReadString('LogContinuousSettings',
    'Minutes', '1'), 1), 1, 255);
  LCThresholdValue:= StrToFloatDef(vConfigurations.ReadString('LogContinuousSettings', 'Threshold', '0'), 0);
  LCThreshold.Value:=LCThresholdValue;

  InvertMPSAS:=vConfigurations.ReadBool('LogContinuousSettings','InvertScale');
  InvertScale.Checked:=InvertMPSAS;
  CheckInvert();

  TemperaturePlotted:=vConfigurations.ReadBool('LogContinuousSettings','TemperaturePlotted',False);
  TemperatureCheckBox.Checked:=TemperaturePlotted;
  UpdateTemperaturePlotSelection();

  NightMode:=vConfigurations.ReadBool('LogContinuousSettings','NightMode',False);
  NightCheckBox.Checked:=NightMode;

  FixedTimeAxisSelection:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','FixedTimeRangeSelection'),0);
  FixedTimeRadios.ItemIndex:=FixedTimeAxisSelection;

  FixedReadingRange:=vConfigurations.ReadBool('LogContinuousSettings','FixedReadingRange',False);
  FixedAutoReadingsToggle.Checked:=FixedReadingRange;

  FromHour:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','FromHour'),0);
  FixedFromSpinEdit.Value:=FromHour;
  Text24to12hr(FromHour,From12hrLabel);

  ToHour:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','ToHour'),0);
  FixedToSpinEdit.Value:=ToHour;
  Text24to12hr(ToHour,To12hrLabel);

  FromReading:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','FromReading'),0);
  FromReadingSpinEdit.Value:=FromReading;
  ToReading:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','ToReading'),0);
  ToReadingSpinEdit.Value:=ToReading;
  SetFixedReadings();

  FixedTemperatureRange:=vConfigurations.ReadBool('LogContinuousSettings','FixedTemperatureReadingRange',False);
  FixedAutoTemperatureReadingsToggle.Checked:=FixedTemperatureRange;
  FromTemperatureReading:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','FromTemperatureReading'),0);
  FromTemperatureReadingSpinEdit.Value:=FromTemperatureReading;
  ToTemperatureReading:=StrToIntDef(vConfigurations.ReadString('LogContinuousSettings','ToTemperatureReading'),0);
  ToTemperatureReadingSpinEdit.Value:=ToTemperatureReading;
  SetFixedTemperatureReadings();

  ChartColor();

  //Create the runtime series used to plot the staged snow (LED-on) reading.
  //Shares the MPSAS series' axes so both curves use the same mag/arcsec^2 scale.
  if SnowSeries = nil then begin
    SnowSeries:= TLineSeries.Create(FormLogCont.Chart1);
    SnowSeries.Title:= 'Snow (LED on)';
    SnowSeries.SeriesColor:= clSkyBlue;
    SnowSeries.AxisIndexX:= MPSASSeries.AxisIndexX;
    SnowSeries.AxisIndexY:= MPSASSeries.AxisIndexY;
    FormLogCont.Chart1.AddSeries(SnowSeries);
  end;

  {Temperature line uses a triangle marker so it is easy to tell apart from the
   round-marker MPSAS line. This covers single-meter logging; the per-meter
   temperature lines created for multi-meter (-SUI) logging set their own
   triangle markers.}
  TempSeries.Pointer.Style:= psTriangle;
  TempSeries.Pointer.Brush.Color:= TempSeries.SeriesColor;
  TempSeries.Pointer.Visible:= True;
  TempSeries.ShowPoints:= True;

  RecordLimitSpin.Value:=LimitInteger(
    StrToIntDef(vConfigurations.ReadString('LogContinuousSettings',
    'Record Limit', '0'), 0), RecordLimitSpin.MinValue, RecordLimitSpin.MaxValue);

  //Get previous FTP Settings from configuration file
  TransferTimeout.Text:= vConfigurations.ReadString('FTPSettings', 'Timeout');
  TransferTimeout.Text:= IntToStr(StrToIntDef(TransferTimeout.Text,1000));

  TransferAddress:=vConfigurations.ReadString('FTPSettings', 'FTP address');
  TransferAddressEntry.Text:= TransferAddress;

  TransferUsername:= vConfigurations.ReadString('FTPSettings', 'FTP username');
  TransferUsernameEntry.Text:= TransferUsername;

  TransferPassword:=vConfigurations.ReadString('FTPSettings', 'FTP password');
  TransferPasswordEntry.Text:= TransferPassword;

  TransferRemoteDirectory:= vConfigurations.ReadString('FTPSettings', 'FTP remote directory');
  TransferRemoteDirectoryEntry.Text:= TransferRemoteDirectory;

  TransferFrequencyRadioGroup.ItemIndex:=
    StrToIntDef(vConfigurations.ReadString('FTPSettings', 'FTP frequency index'), 0);

  TransferProtocolSelector.Items.Add('FTP');
  TransferProtocolSelector.Items.Add('SCP');
  {$IFNDEF WINDOWS}//For now, do not allow SFTP on Windows because bash does not exist there for printf to work.
  TransferProtocolSelector.Items.Add('SFTP');
  {$ENDIF}


  TransferProtocolSelector.Text:=
    vConfigurations.ReadString('FTPSettings', 'FTP protocol selection');

  TransferDAT:=vConfigurations.ReadBool('FTPSettings',  'TransferDAT',False);  TransferDATCheck.Checked:=TransferDAT;
  TransferCSV:=vConfigurations.ReadBool('FTPSettings',  'TransferCSV',False);  TransferCSVCheck.Checked:=TransferCSV;
  TransferPLOT:=vConfigurations.ReadBool('FTPSettings', 'TransferPLOT',False);TransferPLOTCheck.Checked:=TransferPLOT;

  //Get associated port number depending on selected protocol
  case TransferProtocolSelector.Text of
    'FTP': TransferPort:=vConfigurations.ReadString('FTPSettings', 'FTP port',cFtpProtocol);
    'SCP': TransferPort:= vConfigurations.ReadString('FTPSettings', 'SCP port',cSSHProtocol);
    'SFTP': TransferPort:= vConfigurations.ReadString('FTPSettings', 'SFTP port',cSSHProtocol);
  end;
  TransferPortEntry.Text:= TransferPort;

  PWEnable:=vConfigurations.ReadBool('FTPSettings', 'PW enable', True);
  TransferPWenable.Checked:=PWEnable;

  TransferProtocolSelection();

  {Get transfer reading settings}
  TrRdgEnabled:=vConfigurations.ReadBool('FTPSettings', 'TrRdgEnabled',False);
  TrRdgEnableCheckBox.Checked:=TrRdgEnabled;

  TrRdgPort:=vConfigurations.ReadString('FTPSettings','TrRdgPort','0');
  TrRdgPortEdit.Text:=TrRdgPort;

  TrRdgAddress:=vConfigurations.ReadString('FTPSettings','TrRdgAddress','0.0.0.0');
  TrRdgAddressEntry.text:=TrRdgAddress;



  {See commandlineoptions.txt for more details (overrides configuration settings)}

  //Get named GoTo script from command line
  if ParameterCommand('-LCGN') then
    ChangeGotoScript(ParameterValue.Strings[1]);

  //Check for Goto setting (Run then shut down)
  if ParameterCommand('-LCGRS') then begin
    GoToEnabled:=True;
  end else begin
    GoToEnabled:=vConfigurations.ReadBool('GoToSettings','GoTo Enabled',False);
  end;
  OptionsGroup.Checked[2]:=GoToEnabled;
  GoToGroup.Visible:=GoToEnabled;
  GoToLogIndicatorX.Visible:=GoToEnabled;

  //Check for threshold setting
  if ParameterCommand('-LCTH') then begin
    LCThresholdValue:= StrToFloatDef(ParameterValue.Strings[1], 0.0);
    LCThreshold.Value:= LCThresholdValue;
  end;

  //Check for mode setting
  if (ParameterCommand('-LCM') or ParameterCommand('-LCMS') or
    ParameterCommand('-LCMM')) then begin
    if ((ParameterCommand('-LCM')) and (ParameterValue.Count > 1)) then begin
      //Set mode from command line
      case StrToIntDef(ParameterValue.Strings[1], 0) of
        1: LCTriggerMode:= 2;//Every 1 minute on the minute
        5: LCTriggerMode:= 3;//Every 5 minutes on the 1/12th hour
        10: LCTriggerMode:= 4;//Every 10 minutes on the 1/6th hour
        15: LCTriggerMode:= 5;//Every 15 minutes on the 1/4 hour
        30: LCTriggerMode:= 6;//Every 30 minutes on the 1/2 hour
        60: LCTriggerMode:= 7;//Every hour on the hour
      end;
    end;

    //Set seconds value from command line
    if ((ParameterCommand('-LCMS')) and (ParameterValue.Count > 1)) then begin
      LCTrigSecondsSpin.Value:= StrToIntDef(ParameterValue.Strings[1], 0);
      LCTriggerMode:= 0;//Every x seconds
    end;
    if ((ParameterCommand('-LCMM')) and (ParameterValue.Count > 1)) then
      //Set minutes value from command line
    begin
      LCTrigMinutesSpin.Value:= StrToIntDef(ParameterValue.Strings[1], 0);
      LCTriggerMode:= 1;  //Every x minutes
    end;
  end
  else begin
    //read mode from stored configuration file data
    LCTriggerMode:= StrToIntDef(vConfigurations.ReadString('LogContinuousSettings',
      'Mode', '2'), 2);
  end;

  // Set trigger mode accordingly from above determinations
  case LCTriggerMode of
    0: RadioButton1.Checked:= True;
    1: RadioButton2.Checked:= True;
    2: RadioButton3.Checked:= True;
    3: RadioButton4.Checked:= True;
    4: RadioButton5.Checked:= True;
    5: RadioButton6.Checked:= True;
    6: RadioButton7.Checked:= True;
    7: RadioButton8.Checked:= True;
  end;

  { Restore pre-reading alert sound setting }
  alert2sEnable:=vConfigurations.ReadBool('LogContinuousSettings', 'Alert2s',False);
  alert2s.Checked:=alert2sEnable;

  { Restore reading alert sound setting }
  AlertEnable:= LimitInteger(StrToIntDef(vConfigurations.ReadString('LogContinuousSettings', 'Alert','0'),0),0,2);
  ReadingAlertGroup.ItemIndex:=AlertEnable;

  { Restore Alarm sound setting }
  AlarmSoundEnable:=vConfigurations.ReadBool('LogContinuousSettings', 'AlarmSound',False);
  AlarmSoundEnableCheck.Checked:=AlarmSoundEnable;

  { Sound player set up}
  PreAlertSound.PlayStyle:=psAsync;
  FreshSound.PlayStyle:=psAsync;
  AlarmSound.PlayStyle:=psAsync;
  PreAlertSound.PlayCommand:=''; //Preload empty so that routine can find it's own command
  FreshSound.PlayCommand:='';
  AlarmSound.PlayCommand:='';


  { Point to sound files}
  PreAlertSound.SoundFile:=appsettings.DataDirectory+'prereading.wav';;
  FreshSound.SoundFile:=appsettings.DataDirectory+'freshreading.wav';
  AlarmSound.SoundFile:=appsettings.DataDirectory+'alarmsound.wav';
  AlarmThreshold:= StrToFloatDef(vConfigurations.ReadString('LogContinuousSettings', 'AlarmThreshold', '0'), 0);
  AlarmThresholdFloatSpinEdit.Value:= AlarmThreshold;
  SetRepeatProgress();
  SetSnoozeProgress();

  //Initialize Rotational stage variables and display
  RSpieces:= TStringList.Create;
  RSpieces.Delimiter:= ',';
  RSpieces.StrictDelimiter:= True; //Do not parse spaces
  try
  if ((Paramcount > 0) and (ParamStr(1) = 'rotstage')) then
  begin
    rotstage:= True;
  end; finally
    if Assigned(RSpieces) then
      FreeAndNil(RSpieces);
  end;

  GPSSNRClear();  //Clear GPS Signal strength indicators

  //Initialize GPS if enabled
  if GPSEnable.Checked then GPSConnect();

  //Restore MoonData Option setting
  MoonData:=vConfigurations.ReadBool('LogContinuousSettings', 'MoonData',False);
  OptionsGroup.Checked[0]:=MoonData;

  //Restore Freshness Option setting
  Freshness:=vConfigurations.ReadBool('LogContinuousSettings', 'Freshness',False);
  OptionsGroup.Checked[1]:=Freshness;

  //Restore Split Option setting
  SingleDatFile:=vConfigurations.ReadBool('LogContinuousSettings', 'SingleDatFile',False);
  SingleDatCheckBox.Checked:=SingleDatFile;
  SplitSpinEdit.Enabled:=not SingleDatFile;
  SplitSpinLabel.Enabled:=not SingleDatFile;
  SplitDatTime:=StrToInt(vConfigurations.ReadString('LogContinuousSettings', 'SplitDatTime','0'));
  if SplitDatTime>23 then SplitDatTime:=23;
  if SplitDatTime<0 then SplitDatTime:=0;
  SplitSpinEdit.Value:=SplitDatTime;

  //Recall the saved GoTo machine selection
  GotoMachineSelection:='';//Default to no selection
  st:=vConfigurations.ReadString('GoToSettings','GoTo Machine','');
  for s in GoToMachineSelect.Items do begin
    if st = s then
      GotoMachineSelection:=s;
  end;
  GoToMachineSelect.Text:=GotoMachineSelection;

  GoToLogIndicatorX.Brush.Color:=clGray;


  // Restore RawFrequencyEnabled setting
  RawFrequencyEnabled:=vConfigurations.ReadBool('LogContinuousSettings', 'RawFrequency',False);
  OptionsGroup.Checked[3]:=RawFrequencyEnabled;


  //Populate GoTo port dropdown options
    {$IFDEF Linux}
    GoToPortSelect.Clear;
    Count:=0;

    If FindFirst ('/dev/ttyUSB*',faAnyFile ,Info)=0 then begin
      Repeat
        Inc(Count);
        With Info do begin
          GoToPortSelect.AddItem('/dev/'+Name,Nil);
        end;
      Until FindNext(info)<>0;
    end;

    If FindFirst ('/dev/ttyS*',faAnyFile ,Info)=0 then begin
      Repeat
        Inc(Count);
        With Info do begin
          GoToPortSelect.AddItem('/dev/'+Name,Nil);
        end;
      Until FindNext(info)<>0;
    end;

    FindClose(Info);
    GoToPortSelect.Sorted:=True;
    {$ENDIF Linux}
    {$IFDEF Darwin}
    GoToPortSelect.Clear;
    Count:=0;

    If FindFirst ('/dev/usbserial*',faAnyFile ,Info)=0 then begin
      Repeat
        Inc(Count);
        With Info do begin
          GoToPortSelect.AddItem('/dev/'+Name,Nil);
        end;
      Until FindNext(info)<>0;
    end;

    If FindFirst ('/dev/cu.*',faAnyFile ,Info)=0 then begin
      Repeat
        Inc(Count);
        With Info do begin
          GoToPortSelect.AddItem('/dev/'+Name,Nil);
        end;
      Until FindNext(info)<>0;
    end;

    If FindFirst ('/dev/tty.*',faAnyFile ,Info)=0 then begin
      Repeat
        Inc(Count);
        With Info do begin
          GoToPortSelect.AddItem('/dev/'+Name,Nil);
        end;
      Until FindNext(info)<>0;
    end;

    FindClose(Info);
    GoToPortSelect.Sorted:=True;
    {$ENDIF Darwin}
    //Recall the saved GoTo port selection
    GotoPortSelection:=vConfigurations.ReadString('GoToSettings','GoTo Port','');
    GoToPortSelect.Text:=GotoPortSelection;

    //Populate GoTo command files dropdown selection
    UpdateGoToCommandFileList();

    //Read in goto script
    if GoToEnabled then
      GoToReadScript();

    FormCreating:=False;

end;

//Clear GPS Signal strength indicators
procedure TFormLogCont.GPSSNRClear();
begin
  GPSSNR1.Position:=0;
  GPSSNR2.Position:=0;
  GPSSNR3.Position:=0;
  GPSSNR4.Position:=0;
  GPSSNR5.Position:=0;
  GPSSNR6.Position:=0;
  GPSSNR7.Position:=0;
  GPSSNR8.Position:=0;
  GPSSNR9.Position:=0;
  GPSSNR10.Position:=0;
  GPSSNR11.Position:=0;
  GPSSNR12.Position:=0;
end;

procedure TFormLogCont.GPSConnectButtonClick(Sender: TObject);
begin
  GPSConnect();
end;

procedure GPSConnect();
var
  GPSPort: String;
begin
  GPSser.LinuxLock:=False; //lock file sometimes persists stuck if program closes before port
  GPSPort:=FormLogCont.GPSPortSelect.Text;
  if GPSPort=PortName then begin
    StatusMessage('WARNING: GPS port: ['+GPSPort+'] cannot be the same as SQM port: ['+PortName+']');
    Application.MessageBox(PChar('WARNING: GPS port: ['+GPSPort+'] cannot be the same as SQM port: ['+PortName+']'),
                                     'Warning communication port conflict!', MB_ICONEXCLAMATION)  ;

  end
  else begin
    StatusMessage('Connecting to GPS on: '+GPSPort);
    GPSser.Connect(GPSPort);
    GPSser.config(GPSBaudrate, 8, 'N', SB1, False, False);
  end;
end;

// Send a command strings then return the result
function GoToSendGet(command:string; Timeout:Integer=3000; GetAlso:boolean = True; HideStatus:boolean = False; RecvTerminator:string='') : string;
var
   MesageString:String='';
begin

     {Start up Comm busy timer}
     GoToCommBusyTime:=0; //Reset count
     FormLogCont.GoToCommBusy.Enabled:=True;

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

     {Request to open communications, even if already opened. }
     if not GoToCommOpened then
       GoToConnect();

     { Check selected communication method. }
      GoToser.Purge;//debug (does not seem to work with some Macs )

      while GoToser.CanRead(10) do //Try another purge method
         GoToser.RecvByte(10);

      GoToser.SendString(command);

      if (GetAlso) then begin {Get response}
        if Length(RecvTerminator)>0 then
          GoToSendGet:=GoToser.RecvTerminated(Timeout,RecvTerminator)
        else
          GoToSendGet:=chr(GoToser.RecvByte(Timeout));
      end;
      If CompareStr(GoToser.LastErrorDesc,'OK')<>0 then
         MesageString:='Error: '+GoToser.LastErrorDesc+'. ';

     if not HideStatus then begin    //Comment this out to allow all messages through to logging
          if GetAlso then
            MesageString:=MesageString+'Sent: '+command+'   To: '+GoToPortSelection+'   Received: '+GoToSendGet
          else
            MesageString:=MesageString+'Sent: '+command+'   To: '+GoToPortSelection;

          StatusMessage(MesageString);
          FormLogCont.GoToResultMemo.Append(MesageString);
     end;

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

end;


procedure GoToConnect();
var
  GoToPort: String;
begin
  GoToser.LinuxLock:=False; //lock file sometimes persists stuck if program closes before port
  GoToPort:=FormLogCont.GoToPortSelect.Text;
  if GoToPort=PortName then begin
    StatusMessage('WARNING: GoTo port: ['+GoToPort+'] cannot be the same as SQM port: ['+PortName+']');
    Application.MessageBox(PChar('WARNING: GoTo port: ['+GoToPort+'] cannot be the same as SQM port: ['+PortName+']'),
                                     'Warning communication port conflict!', MB_ICONEXCLAMATION)  ;

  end
  else begin
    StatusMessage('Connecting to GoTo on: '+GoToPort);
    GoToser.Connect(GoToPort);
    GoToser.config(GoToBaudrate, 8, 'N', 1, False, False);
    FormLogCont.GoToResultMemo.Append('Connect: '+IntToStr(GoToBaudrate) +'baud, '+ GoToser.LastErrorDesc);
    GoToCommBusyTime:=0;
    FormLogCont.GoToCommBusy.Enabled:=True;
    FormLogCont.GoToLogIndicatorX.Brush.Color:=clLime;
    GoToCommOpened:=True;
  end;
end;

function GotoZenAzi(Zenith,Azimuth:Double):Boolean;
begin
  GoToDesiredZenith:=Zenith;
  GoToDesiredAzimuth:=Azimuth;

  GoToRecvTerminator:='';
  GoToCommandStep:=0;
  GoToCommand(gtSetZenithAzimuth);

  GotoZenAzi:=True; //Command accepted, but will take some time to arrive at destination.
end;

procedure GoToCommand(Command:Integer; Parm1:String=''; Parm2:String='');
var
  //CommandString:String;
  Sign:String;
  GoToResult:String;
  GotoCommandString:String;
  pieces:TStringList;
  Zenith, Azimuth: Double;
  GoToSystemStatus:Integer=-1;//default undefined value
  {================================================}
  {Function to convert precise Synscan Hex to float}
  function GoToSynScanprecise(HexString:String):Float ;
  begin
    if length(HexString)=8 then begin
      GoToSynScanprecise:=float(Hex2Dec(LeftStr(HexString,6)))/16777216.0 *360.0;
    end
    else
      GoToSynScanprecise:=0;
  end;
  {================================================}
  {Function to convert float to precise Synscan Hex}
  function GoToSynScanPreciseHex(Angle:Float):String;
  begin
      GoToSynScanPreciseHex:=IntToHex(trunc((Angle*16777216.0)/360.0),6)+'00';
  end;
  {================================================}

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

  case GotoMachineSelection of
    'iOptron8408': begin

      case Command of
        gtGetZenithAzimuth: Begin
          GoToResult:=GoToSendGet(':GAC#',1000,True,False,'#');
          pieces.DelimitedText:=GoToResult;
          if pieces.Count>0 then begin
            if (Length(pieces.Strings[0])=18) then begin
             //Convert text string (0.01 arc-seconds) to degrees.
             Zenith:=Alt2Zen(StrToFloatDef(AnsiLeftStr(pieces.Strings[0],9),0.0)/360000.0);
             Azimuth:=StrToFloatDef(AnsiRightStr(pieces.Strings[0],9),0.0)/360000.0;
             StatusMessage(Format('Recvd: %s : Zenith=%f , Azimuth=%f',[GoToResult,Zenith,Azimuth]));
             //Set float spin edits
             FormLogCont.ZenFloatSpinEdit.Value:=Zenith;
             FormLogCont.AziFloatSpinEdit.Value:=Azimuth;
            end
            else
              StatusMessage('Need 18 chars, got '+IntToStr(length(pieces.Strings[0])));
          end
          else
            StatusMessage('Goto :GAC# got nothing useful: '+GoToResult + 'Check GoTo Test Status window for details.');
        end;

        gtSetZenithAzimuth: begin
          GoToRecvTerminator:='';

          if GoToDesiredZenith <0 then
            Sign:='-'
          else
            Sign:='+';

          try
            {Send the desired Zenith (Altitude)}
            GotoCommandString:=Format(':Sa%s%s#',[Sign, AddChar('0',FloatToStr(Zen2Alt(GoToDesiredZenith)*360000.0),8)]);
            GoToSendGet(GotoCommandString);
            StatusMessage(Format('Sending: %s : where Zenith=%f',[GotoCommandString,GoToDesiredZenith]));

            {Send the desired Azimuth}
            GotoCommandString:=Format(':Sz%s#',[AddChar('0',FloatToStr(GoToDesiredAzimuth*360000.0),9)]);
            GoToSendGet(GotoCommandString);
            StatusMessage(Format('Sending: %s : where Azimuth=%f',[GotoCommandString,GoToDesiredAzimuth]));

            {Slew the mount to position.}
            GoToSendGet(':MS#');

            {wait for not slewing (2nd char = 2 = slewing), then stop :ST0#}
            StatusMessage('Waiting for mount to reach destination.');
            while (GoToSystemStatus<>0) do begin
              GoToSendGet(':ST0#',3000,True,True,'');
              GoToResult:=GoToSendGet(':GAS#',3000,True,True,'#');
              if Length(GoToResult)>2 then
                GoToSystemStatus:=StrToInt(AnsiMidStr(GoToResult,2,1))
              else begin
                StatusMessage('Error in getting GoTo :GAS#, GoToResult: '+GoToResult);
                StopRecording:=True;
                exit;
              end;
            end;
            StatusMessage('Mount reached destination.');

            //Idle for 1 second before getting an update on the position.
            Sleep(1000);
            GoToCommand(gtGetZenithAzimuth);

            GoToInPosition:=True;
          except
            StatusMessage('Error in gtSetZenithAzimuth.');
          end;

        end;

      end;

    end;

  'SynscanV4': begin
    case Command of

      gtGetZenithAzimuth: Begin
        GoToResult:=GoToSendGet('z'+#13+#10,1000,True,False,'#');
        pieces.DelimitedText:=GoToResult;
        if pieces.Count=2 then begin
         {Convert text string (0.01 arc-seconds) to degrees.}
         Azimuth:=GoToSynScanprecise(pieces.Strings[0]);
         Zenith:= Alt2Zen(GoToSynScanprecise(pieces.Strings[1]));
         StatusMessage(Format('Recvd: %s : Zenith=%f , Azimuth=%f',[GoToResult,Zenith,Azimuth]));
         //Set float spin edits
         FormLogCont.ZenFloatSpinEdit.Value:=Zenith;
         FormLogCont.AziFloatSpinEdit.Value:=Azimuth;
        end //End checking validity of response
        else
          StatusMessage('Need 2 parts, got '+IntToStr(pieces.Count));
      end; //end of gtGetZenithAzimuth command

      gtSetZenithAzimuth: begin
        GoToRecvTerminator:='#';
        GotoCommandString:=Format('b%s,%s',[
           GoToSynScanPreciseHex(GoToDesiredAzimuth),
           GoToSynScanPreciseHex(Zen2Alt(GoToDesiredZenith))
           ]);
        StatusMessage(Format('Sending: %s : where Zenith=%f, and Azimuth=%f',[GotoCommandString,GoToDesiredZenith,GoToDesiredAzimuth]));
        GoToSendGet(GotoCommandString+#13+#10);

        {Wait for GOTO not in progress (1st char = 1 = GoTo in progress)}
        while (GoToSystemStatus<>0) do begin
          GoToResult:=GoToSendGet('L'+#13+#10,3000,True,True,'#');
          if Length(GoToResult)>0 then
            GoToSystemStatus:=StrToInt(AnsiMidStr(GoToResult,1,1))
          else begin
            StatusMessage('Error in getting GoTo "L", GoToResult: '+GoToResult+' , Stopped recording.');
            StopRecording:=True;
            exit;
          end;
        end;

        if not StopRecording then begin
          //Idle for 1 second before getting an update on the position.
          Sleep(1000);
          GoToCommand(gtGetZenithAzimuth);

          GoToInPosition:=True;
        end;

      end;

    end;

    end;
  else
    exit;
  end;

  if Assigned(pieces) then FreeAndNil(pieces);

end;

procedure GPSDisconnect();
begin
  GPSser.CloseSocket;
end;

procedure GoToDisconnect();
begin

       FormLogCont.GoToCommBusy.Enabled:=False; //Prevent further triggers to the comm. busy timer.
       GoToCommBusyTime:=0; //Reset comm. busy count.
       GoToCommOpened:=False; //Indicate that the comm. port is closed.

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

       GoToCommOpened:=False;
       FormLogCont.GoToLogIndicatorX.Brush.Color:=clGray;
end;


procedure TFormLogCont.GPSPortSelectChange(Sender: TObject);
begin
     { Save the GPS port selection. }
     vConfigurations.WriteString(SerialINIsection,'GPS Port',GPSPortSelect.Text);
end;

procedure TFormLogCont.GPSPortSelectDropDown(Sender: TObject);
Var Info : TSearchRec;
    Count : Longint;

Begin
  GPSPortSelect.Clear;
  Count:=0;

  {$ifdef Windows}
  {$ENDIF Windows}

  {$ifdef Darwin}
  If FindFirst ('/dev/cu.*',faAnyFile ,Info)=0 then begin
    Repeat
      Inc(Count);
      With Info do begin
        //Writeln (Name:40,Size:15);
        GPSPortSelect.AddItem('/dev/'+Name,Nil);
      end;
    Until FindNext(info)<>0;
  end;
  {$ENDIF Darwin}

  {$IFDEF Linux}
  If FindFirst ('/dev/ttyS*',faAnyFile ,Info)=0 then begin
    Repeat
      Inc(Count);
      With Info do begin
        //Writeln (Name:40,Size:15);
        GPSPortSelect.AddItem('/dev/'+Name,Nil);
      end;
    Until FindNext(info)<>0;
  end;

  If FindFirst ('/dev/ttyACM*',faAnyFile ,Info)=0 then begin
    Repeat
      Inc(Count);
      With Info do begin
        //Writeln (Name:40,Size:15);
        GPSPortSelect.AddItem('/dev/'+Name,Nil);
      end;
    Until FindNext(info)<>0;
  end;

  If FindFirst ('/dev/ttyUSB*',faAnyFile ,Info)=0 then begin
    Repeat
      Inc(Count);
      With Info do begin
        //Writeln (Name:40,Size:15);
        GPSPortSelect.AddItem('/dev/'+Name,Nil);
      end;
    Until FindNext(info)<>0;
  end;

  {$ENDIF Linux}

  FindClose(Info);
  GPSPortSelect.Sorted:=True;
end;

procedure TFormLogCont.GPSTimerTimer(Sender: TObject);
var
  pieces:TStringList;
  DecPos: Integer; //Decimal place position
  GPSString: AnsiString;
  GPSStage: AnsiString;
  GPSformatsettings:TFormatSettings;
  LastStart:Integer;//Position of Last start $ character.
const
  MinBG = 70; //Minimum background color
begin

 if GPSEnable.Checked then begin
  pieces:= TStringList.Create;
  pieces.Delimiter:= ',';
  GPSformatsettings.DecimalSeparator:='.';

  //Update health indicators.
  //Reduce brightness of all indicators by one notch,
  //  will be increased further below if associated command comes in.
  if Green(GPSRMCStatusX.Brush.Color)-4>MinBG then
     GPSRMCStatusX.Brush.Color:=RGBToColor(MinBG,Green(GPSRMCStatusX.Brush.Color)-4,MinBG);
  if Green(GPSGGAStatusX.Brush.Color)-4>MinBG then
     GPSGGAStatusX.Brush.Color:=RGBToColor(MinBG,Green(GPSGGAStatusX.Brush.Color)-4,MinBG);
  if Green(GPSGSVStatusX.Brush.Color)-4>MinBG then
     GPSGSVStatusX.Brush.Color:=RGBToColor(MinBG,Green(GPSGSVStatusX.Brush.Color)-4,MinBG);
  if not GPSEnable.Checked then begin
    GPSRMCStatusX.Brush.Color:=RGBToColor(MinBG,MinBG,MinBG);
    GPSGGAStatusX.Brush.Color:=RGBToColor(MinBG,MinBG,MinBG);
    GPSGSVStatusX.Brush.Color:=RGBToColor(MinBG,MinBG,MinBG);
    GPSValidityLabel.Text:='Off';
    GPSQualityLabel.Text:='Off';
    GPSSatellites.Text:='0';
    GPSSignalGroup.Visible:=False;
  end else GPSSignalGroup.Visible:=True;

  while (GPSser.CanRead(5) and GPSEnable.Checked) do begin

    { TODO : Checksum validation , read in first then parse later.}

    if GPSLoggingEnabled then StatusMessage('Trying to read GPSString');
    GPSString:=StringReplace(GPSser.Recvstring(5),'*',',',[rfReplaceAll]);
    if GPSLoggingEnabled then StatusMessage('GPSString = '+GPSString);

    try //Missing fields may cause a failure, so just ignore them
    GPSStage:='Initial.';

    LastStart:=LastDelimiter('$',GPSString);
    //LastStart:=0;//debug

    if ((Length(GPSString)>0) and( LastStart>0)) then begin

      GPSStage:='Passed length check.';

      //Remove anything preceding the $ character
      GPSString:= AnsiRightStr(GPSString,Length(GPSString)-LastStart+1);
      //StatusMessage('GPSString='+GPSString);
      pieces.DelimitedText:=GPSString;

      //**** force failure for debugging
      //if (pieces.Strings[200]='$GPGGA') then StatusMessage('passes gps [200] test');

      //GPS Satellites in view
      if ((pieces.Strings[0]='$GPGSV') or (pieces.Strings[0]='$GNGSV'))then begin
        GPSStage:='Passed Satellites in view check.';
        GPSGSVIncoming.Text:=GPSString; //Show incoming string for troubleshooting purposes.
        //1    = Total number of messages of this type in this cycle
        //2    = Message number
        //3    = Total number of SVs in view
        //4    = SV PRN number
        //5    = Elevation in degrees, 90 maximum
        //6    = Azimuth, degrees from true north, 000 to 359
        //7    = SNR, 00-99 dB (null when not tracking)
        //8-11 = Information about second SV, same as field 4-7
        //12-15= Information about third SV, same as field 4-7
        //16-19= Information about fourth SV, same as field 4-7
        GPSGSVStatusX.Brush.Color:=RGBToColor(MinBG,255,MinBG);
        case pieces.Strings[2] of
          '1': begin
            //Enable status bars per visible satellites
            GPSVisibleSatellites:=StrToIntDef(pieces.Strings[3],0);
            GPSSatellites.Text:=Format('%d',[GPSVisibleSatellites]);
              GPSSNR1.Visible:=GPSVisibleSatellites>0;
              GPSSAT1.Visible:=GPSVisibleSatellites>0;
              GPSSNR2.Visible:=GPSVisibleSatellites>1;
              GPSSAT2.Visible:=GPSVisibleSatellites>1;
              GPSSNR3.Visible:=GPSVisibleSatellites>2;
              GPSSAT3.Visible:=GPSVisibleSatellites>2;
              GPSSNR4.Visible:=GPSVisibleSatellites>3;
              GPSSAT4.Visible:=GPSVisibleSatellites>3;
              GPSSNR5.Visible:=GPSVisibleSatellites>4;
              GPSSAT5.Visible:=GPSVisibleSatellites>4;
              GPSSNR6.Visible:=GPSVisibleSatellites>5;
              GPSSAT6.Visible:=GPSVisibleSatellites>5;
              GPSSNR7.Visible:=GPSVisibleSatellites>6;
              GPSSAT7.Visible:=GPSVisibleSatellites>6;
              GPSSNR8.Visible:=GPSVisibleSatellites>7;
              GPSSAT8.Visible:=GPSVisibleSatellites>7;
              GPSSNR9.Visible:=GPSVisibleSatellites>8;
              GPSSAT9.Visible:=GPSVisibleSatellites>8;
              GPSSNR10.Visible:=GPSVisibleSatellites>9;
              GPSSAT10.Visible:=GPSVisibleSatellites>9;
              GPSSNR11.Visible:=GPSVisibleSatellites>10;
              GPSSAT11.Visible:=GPSVisibleSatellites>10;
              GPSSNR12.Visible:=GPSVisibleSatellites>11;
              GPSSAT12.Visible:=GPSVisibleSatellites>11;
            //Parse satellite data
            GPSSNR1.Position:=StrToIntDef(pieces.Strings[7],0);
            GPSSNR2.Position:=StrToIntDef(pieces.Strings[11],0);
            GPSSNR3.Position:=StrToIntDef(pieces.Strings[15],0);
            GPSSNR4.Position:=StrToIntDef(pieces.Strings[19],0);
            GPSSAT1.Caption:=pieces.Strings[4];
            GPSSAT2.Caption:=pieces.Strings[8];
            GPSSAT3.Caption:=pieces.Strings[12];
            GPSSAT4.Caption:=pieces.Strings[16];
          end;
          '2': begin
            //Parse satellite data
            GPSSNR5.Position:=StrToIntDef(pieces.Strings[7],0);
            GPSSNR6.Position:=StrToIntDef(pieces.Strings[11],0);
            GPSSNR7.Position:=StrToIntDef(pieces.Strings[15],0);
            GPSSNR8.Position:=StrToIntDef(pieces.Strings[19],0);
            GPSSAT5.Caption:=pieces.Strings[4];
            GPSSAT6.Caption:=pieces.Strings[8];
            GPSSAT7.Caption:=pieces.Strings[12];
            GPSSAT8.Caption:=pieces.Strings[16];
          end;
          '3': begin
            //Parse satellite data
            GPSSNR9.Position:=StrToIntDef(pieces.Strings[7],0);
            GPSSAT9.Caption:=pieces.Strings[4];
            if pieces.Count>10 then begin
              GPSSNR10.Position:=StrToIntDef(pieces.Strings[11],0);
              GPSSAT10.Caption:=pieces.Strings[8];
            end;
            if pieces.Count>15 then begin
              GPSSNR11.Position:=StrToIntDef(pieces.Strings[15],0);
              GPSSAT11.Caption:=pieces.Strings[12];
            end;
            if pieces.Count>19 then begin
              GPSSNR12.Position:=StrToIntDef(pieces.Strings[19],0);
              GPSSAT12.Caption:=pieces.Strings[16];
            end;
          end;
        end;

      end;

      //Global Positioning System Fix Data
      if (pieces.Count=16) then begin
        if ((pieces.Strings[0]='$GPGGA') or (pieces.Strings[0]='$GNGGA'))then begin
          GPSStage:='Passed Global Positioning System Fix Data check.';
          GPSGGAIncoming.Text:=GPSString; //Show incoming string for troubleshooting purposes.
          //1    = UTC of Position
          //2    = Latitude
          //3    = N or S
          //4    = Longitude
          //5    = E or W
          //6    = GPS quality indicator (0=invalid; 1=GPS fix; 2=Diff. GPS fix)
          //7    = Number of satellites in use [not those in view]
          //8    = Horizontal dilution of position
          //9    = Antenna altitude above/below mean sea level (geoid)
          //10   = Meters  (Antenna height unit)
          //11   = Geoidal separation (Diff. between WGS-84 earth ellipsoid and
          //       mean sea level.  -=geoid is below WGS-84 ellipsoid)
          //12   = Meters  (Units of geoidal separation)
          //13   = Age in seconds since last update from diff. reference station
          //14   = Diff. reference station ID#
          //15   = Checksum
          GPSGGAStatusX.Brush.Color:=RGBToColor(MinBG,255,MinBG);
          GPSAltitude:=round(StrToFloatDef(pieces.Strings[9],0,GPSformatsettings));
          GPSElevationlabel.Text:=Format('%d',[GPSAltitude]);
          case pieces.Strings[6] of
            '0': GPSQualityLabel.Text:='Invalid';
            '1': GPSQualityLabel.Text:='GPS fix';
            '2': GPSQualityLabel.Text:='Diff. GPS fix';
            else GPSQualityLabel.Text:='Unknown';
          end;
        end;
      end;

      //Recommended minimum specific GPS/Transit data
      try
        if ((AnsiStartsStr('$GPRMC', pieces.Strings[0])) or (AnsiStartsStr('$GNRMC', pieces.Strings[0])))then begin
           GPSStage:='Passed Recommended minimum specific GPS/Transit data check.';
           GPSRMCIncoming.Text:=GPSString; //Show incoming string for troubleshooting purposes.
          //1   220516     Time Stamp
          //2   A          validity - A-ok, V-invalid
          //3   5133.82    current Latitude
          //4   N          North/South
          //5   00042.24   current Longitude
          //6   W          East/West
          //7   173.8      Speed in knots
          //8   231.8      True course
          //9   130694     Date Stamp
          //10  004.2      Magnetic variation degrees (Easterly var. subtracts from true course)
          //11  W          East/West
          //12  *70        checksum
          GPSRMCStatusX.Brush.Color:=RGBToColor(MinBG,255,MinBG);
          case pieces.Strings[2] of
           'A': GPSValidityLabel.Text:='OK';
           'V': GPSValidityLabel.Text:='Warning';
           else
             GPSValidityLabel.Text:='Unknown';
          end;
          //Convert Latitude minutes to degrees
          if Length(pieces.Strings[3])>4 then begin
            DecPos:=AnsiPos('.',pieces.Strings[3]);
            GPSLatitude:= StrToFloatDef(AnsiLeftStr(pieces.Strings[3],DecPos-3),0) +
                          StrToFloatDef(AnsiRightStr(pieces.Strings[3],Length(pieces.Strings[3])-DecPos+3),0,GPSformatsettings)/60.0;
            if pieces.Strings[4]='S' then
               GPSLatitude:=-1*GPSLatitude;
            GPSLatitudeLabel.Text:=Format('%3.4f',[GPSLatitude]);
          end
          else
              GPSLatitudeLabel.Text:='';

          //Convert Longitude minutes to degrees
          if Length(pieces.Strings[5])>4 then begin
            DecPos:=AnsiPos('.',pieces.Strings[5]);
            GPSLongitude:= StrToFloatDef(AnsiLeftStr(pieces.Strings[5],DecPos-3),0) +
                           StrToFloatDef(AnsiRightStr(pieces.Strings[5],Length(pieces.Strings[5])-DecPos+3),0,GPSformatsettings)/60.0;
            if pieces.Strings[6]='W' then GPSLongitude:=-1*GPSLongitude;
            GPSLongitudeLabel.Text:=Format('%3.4f',[GPSLongitude]);
          end
          else GPSLongitudeLabel.Text:='';

          //Convert knots to m/s
          GPSSpeed:=StrToFloatDef(pieces.Strings[7],0,GPSformatsettings)*0.514444444444;
          GPSSpeedLabel.Text:=Format('%.2f',[GPSSpeed]);

          //Parse Date
          GPSDateStampLabel.Text:='20'+AnsiRightStr(pieces.Strings[9],2)+':'+
                                  AnsiMidStr(pieces.Strings[9],3,2)+':' +
                                  AnsiLeftStr(pieces.Strings[9],2)+'T' +
                                  AnsiLeftStr(pieces.Strings[1],2)+':' +
                                  AnsiMidStr(pieces.Strings[1],3,2)+':' +
                                  AnsiMidStr(pieces.Strings[1],5,Length(pieces.Strings[1])-4);

        end;
      finally
      end;

    end;

    except
      StatusMessage('GPS exception at: '+GPSStage+'   GPSString = '+GPSString);
    end;

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

  end; //end checking for GPS enabled

end;

procedure TFormLogCont.CheckInvert();
begin
 if InvertMPSAS then
   MPSASAxisTransformsLinearAxisTransform1.Scale:=1 // Light at bottom (inverted)
 else
   MPSASAxisTransformsLinearAxisTransform1.Scale:=-1; //Dark at bottom (normal)
end;

procedure TFormLogCont.InvertScaleChange(Sender: TObject);
begin
 InvertMPSAS:=InvertScale.Checked;
  vConfigurations.WriteBool('LogContinuousSettings','InvertScale',InvertMPSAS);
  CheckInvert();
  SetFixedReadings();
end;

procedure TFormLogCont.OptionsGroupItemClick(Sender: TObject; Index: integer);
begin
  //Save Moon option logging setting
  case Index of
    0: begin //Moon data
      MoonData:=OptionsGroup.Checked[0];
      vConfigurations.WriteBool('LogContinuousSettings', 'MoonData', MoonData);
    end;
    1: begin //Freshness
      Freshness:=OptionsGroup.Checked[1];
      vConfigurations.WriteBool('LogContinuousSettings', 'Freshness', Freshness);
    end;
    2: begin //GoTo accessory
      GoToEnabled:=OptionsGroup.Checked[2];
      vConfigurations.WriteBool('GoToSettings','GoTo Enabled',GoToEnabled);
      GoToGroup.Visible:=GoToEnabled;
      GoToLogIndicatorX.Visible:=GoToEnabled;
      GoToReadScript();
    end;
    3: begin //Raw frequency
      RawFrequencyEnabled:=OptionsGroup.Checked[3];
      vConfigurations.WriteBool('LogContinuousSettings', 'RawFrequency', RawFrequencyEnabled);
    end;
  end;
end;

procedure TFormLogCont.PauseButtonClick(Sender: TObject);
begin
  if FineTimer.Enabled then begin
    FineTimer.Enabled:=False;
    PauseButton.Caption:='Resume';
    StatusMessage('Logging paused by user.');
  end else begin
    FineTimer.Enabled:=True;
    PauseButton.Caption:='Pause';
    StatusMessage('Logging resumed by user.');
  end;

end;

procedure TFormLogCont.ReadingAlertGroupClick(Sender: TObject);
begin
  AlertEnable:=ReadingAlertGroup.ItemIndex;
  vConfigurations.WriteString('LogContinuousSettings', 'Alert', IntToStr(AlertEnable));
end;

procedure TFormLogCont.RecordLimitSpinChange(Sender: TObject);
begin
  vConfigurations.WriteString('LogContinuousSettings', 'Record Limit',
    IntToStr(RecordLimitSpin.Value));
end;

procedure TFormLogCont.SingleDatCheckBoxClick(Sender: TObject);
begin
  SingleDatFile:=SingleDatCheckBox.Checked;
  SplitSpinEdit.Enabled:=not SingleDatFile;
  SplitSpinLabel.Enabled:=not SingleDatFile;
  vConfigurations.WriteBool('LogContinuousSettings', 'SingleDatFile', SingleDatFile);
end;

procedure TFormLogCont.SnoozeButtonClick(Sender: TObject);
begin
  AlarmSnoozeCurrentTime:=0; //Reset Snooze timer
  StatusMessage('Snooze button pressed.');
end;

procedure TFormLogCont.SplitSpinEditChange(Sender: TObject);
begin

 if SplitSpinBusy then exit;

 //Block recurrent entry since this routine changes its own value.
 SplitSpinBusy:=True;

 If SplitSpinEdit.Value > 23 Then SplitDatTime:= 0 else
    if SplitSpinEdit.Value < 0 then SplitDatTime:= 23 else
      SplitDatTime:=SplitSpinEdit.Value;

  SplitSpinEdit.Value:=SplitDatTime;

  //Convert to 12hr time for label.
  if SplitDatTime =12 then
    SplitSpinLabel.Caption:='hr (12PM)'
  else
  if SplitDatTime >12 then
    SplitSpinLabel.Caption:='hr ('+IntToStr(SplitDatTime - 12)+'PM)'
  else
  if SplitDatTime >=1 then
    SplitSpinLabel.Caption:='hr ('+IntToStr(SplitDatTime)+'AM)'
    else
        SplitSpinLabel.Caption:='hr (12AM)';


  //Save to configuration file
  vConfigurations.WriteString('LogContinuousSettings', 'SplitDatTime', IntToStr(SplitDatTime));


  SplitSpinBusy:=False;
end;

procedure TFormLogCont.SnowSettleSpinChange(Sender: TObject);
begin
  {Number of readings to discard after the snow LED turns on, so the sensor
   settles before the staged snow reading is captured. Shares the SnowReadSkip
   value and config key with the main-window control.}
  SnowReadSkip:= SnowSettleSpin.Value;
  vConfigurations.WriteString('LogContinuousSettings', 'SnowReadSkip', IntToStr(SnowReadSkip));
end;

procedure TFormLogCont.SynScanSheetShow(Sender: TObject);
begin
  {update the actual position if enabled}
  GoToCommand(gtGetZenithAzimuth);
end;

procedure TFormLogCont.GPSBaudSelectChange(Sender: TObject);
begin
  { Save the GPS baud selection. }
  GPSBaudrate:=StrToIntDef(GPSBaudSelect.Text,4800);
  vConfigurations.WriteString(SerialINIsection,'GPS Baud',IntToStr(GPSBaudrate));
end;

procedure TFormLogCont.TemperatureCheckBoxChange(Sender: TObject);
begin
  TemperaturePlotted:=TemperatureCheckBox.Checked;
  vConfigurations.WriteBool('LogContinuousSettings','TemperaturePlotted',TemperaturePlotted);
  Chart1.AxisList[2].Visible:=TemperaturePlotted;
  UpdateTemperaturePlotSelection();
end;
procedure TFormLogCont.UpdateTemperaturePlotSelection();
begin
  if TemperaturePlotted then begin
    TempSeries.LinePen.Style:=psSolid;
    Chart1.AxisList[2].Visible:=True;
    FixedTemperatureReadingsGroupBox.Visible:=True;
  end
  else begin
    TempSeries.LinePen.Style:=psClear;
    Chart1.AxisList[2].Visible:=False;
    FixedTemperatureReadingsGroupBox.Visible:=False;
  end;
end;

procedure TFormLogCont.TransferCSVCheckClick(Sender: TObject);
begin
  if TransferCSVCheck.Checked then begin
   TransferDATCheck.Checked:=False;
   TransferDAT:=False;
   vConfigurations.WriteBool('FTPSettings', 'TransferDAT',False);
  end;
  TransferCSV:=TransferCSVCheck.Checked;
  vConfigurations.WriteBool('FTPSettings', 'TransferCSV',TransferCSV);
end;

procedure TFormLogCont.TransferDATCheckClick(Sender: TObject);
begin
  if TransferDATCheck.Checked then begin
   TransferCSVCheck.Checked:=False;
   TransferCSV:=False;
   vConfigurations.WriteBool('FTPSettings', 'TransferCSV',False);
  end;
  TransferDAT:=TransferDATCheck.Checked;
  vConfigurations.WriteBool('FTPSettings', 'TransferDAT',TransferDAT);
end;

procedure TFormLogCont.TransferPLOTCheckClick(Sender: TObject);
begin
  TransferPLOT:=TransferPLOTCheck.Checked;
  vConfigurations.WriteBool('FTPSettings', 'TransferPLOT',TransferPLOT);
end;

procedure TFormLogCont.TransferFrequencyRadioGroupClick(Sender: TObject);
begin
  LCTFrequency:=TransferFrequencyRadioGroup.ItemIndex;
  vConfigurations.WriteString('FTPSettings', 'FTP frequency index',IntToStr(LCTFrequency));
  vConfigurations.WriteString('FTPSettings', 'FTP frequency description',TransferFrequencyRadioGroup.Items[LCTFrequency]);
end;

procedure TFormLogCont.TransferLocalFilenameDisplayChange(Sender: TObject);
begin

end;

procedure TFormLogCont.TransferPasswordEntryChange(Sender: TObject);
begin
  TransferPassword:=TransferPasswordEntry.Text;
  vConfigurations.WriteString('FTPSettings', 'FTP password', TransferPassword);
end;

procedure TFormLogCont.TransferPasswordShowHideClick(Sender: TObject);
begin
  case TransferPasswordShowHide.Caption of
    'Show':
    begin
      TransferPasswordShowHide.Caption:= 'Hide';
      TransferPasswordEntry.EchoMode:= emNormal;
    end;
    'Hide':
    begin
      TransferPasswordShowHide.Caption:= 'Show';
      TransferPasswordEntry.EchoMode:= emPassword;
    end;
  end;
end;

procedure TFormLogCont.TransferPortEntryChange(Sender: TObject);
begin
  TransferPort:=TransferPortEntry.Text;
  case TransferProtocolSelector.Text of
    'FTP': vConfigurations.WriteString('FTPSettings', 'FTP port', TransferPort);
    'SCP': vConfigurations.WriteString('FTPSettings', 'SCP port', TransferPort);
    'SFTP': vConfigurations.WriteString('FTPSettings', 'SFTP port', TransferPort);
  end;
end;

procedure TFormLogCont.TransferProtocolSelectorChange(Sender: TObject);
begin
  vConfigurations.WriteString('FTPSettings', 'FTP protocol selection',
  TransferProtocolSelector.Text);
  //Get associated port number depending on selected protocol
  case TransferProtocolSelector.Text of
    'FTP': TransferPort:= vConfigurations.ReadString('FTPSettings', 'FTP port',cFtpProtocol);
    'SCP': TransferPort:= vConfigurations.ReadString('FTPSettings', 'SCP port',cSSHProtocol);
    'SFTP': TransferPort:= vConfigurations.ReadString('FTPSettings', 'SFTP port',cSSHProtocol);
  end;
  TransferPortEntry.Text:= TransferPort;

  TransferProtocolSelection();
end;

procedure TFormLogCont.TransferPWenableChange(Sender: TObject);
begin
  PWEnable:=TransferPWenable.Checked;
  vConfigurations.WriteBool('FTPSettings', 'PW enable', PWEnable);
  TransferProtocolSelection();
end;

procedure TFormLogCont.TransferProtocolSelection();
begin
  case TransferProtocolSelector.Text of
  'FTP': begin
    TransferUsernameEntry.Visible:=True;
    TransferPasswordEntry.Visible:=True;
    TransferPasswordShowHide.Visible:=True;
    TransferPWenable.Visible:=False;
  end;
  'SCP': begin
    TransferUsernameEntry.Visible:=True;
    TransferPasswordEntry.Visible:=False;
    TransferPasswordShowHide.Visible:=False;
    TransferPWenable.Visible:=False;
  end;
  'SFTP': begin
    TransferUsernameEntry.Visible:=True;
    TransferPasswordEntry.Visible:=PWEnable;
    TransferPasswordShowHide.Visible:=PWEnable;
    TransferPWenable.Visible:=True;
  end;
  end;
end;

procedure TFormLogCont.TransferRemoteDirectoryEntryChange(Sender: TObject);
begin
  TransferRemoteDirectory:=TransferRemoteDirectoryEntry.Text;
  vConfigurations.WriteString('FTPSettings', 'FTP remote directory', TransferRemoteDirectory);
end;

procedure TFormLogCont.TransferTimeoutChange(Sender: TObject);
begin
  vConfigurations.WriteString('FTPSettings', 'Timeout', TransferTimeout.Text);
end;

procedure TFormLogCont.TransferUsernameEntryChange(Sender: TObject);
begin
  TransferUsername:=TransferUsernameEntry.Text;
  vConfigurations.WriteString('FTPSettings', 'FTP username', TransferUsername);
end;

procedure TFormLogCont.GDMF0ButtonClick(Sender: TObject);
begin
  SendGet('f0x'); //clear feedback coil current
end;

procedure TFormLogCont.GDMF1ButtonClick(Sender: TObject);
begin
  SendGet('f1x'); //clear feedback coil current
end;

procedure TFormLogCont.TransferAddressEntryChange(Sender: TObject);
begin
  TransferAddress:= TransferAddressEntry.Text;
  vConfigurations.WriteString('FTPSettings', 'FTP address', TransferAddress);
end;

procedure TFormLogCont.LCThresholdChange(Sender: TObject);
begin
  //Save value in registry
  LCThresholdValue:=LCThreshold.Value;
  vConfigurations.WriteString('LogContinuousSettings', 'Threshold', FloatToStr(LCThresholdValue));
end;

procedure TFormLogCont.LCTrigMinutesSpinChange(Sender: TObject);
begin
  LCTrigMinutes:=LimitInteger(LCTrigMinutesSpin.Value,1,255);
  Application.ProcessMessages;
  vConfigurations.WriteString('LogContinuousSettings', 'Minutes', IntToStr(LCTrigMinutes));
end;

procedure TFormLogCont.LCTrigSecondsSpinChange(Sender: TObject);
begin
  LCTrigSeconds:=LCTrigSecondsSpin.Value;
  Application.ProcessMessages;
  vConfigurations.WriteString('LogContinuousSettings', 'Seconds', IntToStr(LCTrigSeconds));
end;

procedure TFormLogCont.checkNowStartClick(Sender: TObject);
begin
  //Clear off other START checkboxes if checked
  if checkNowStart.Checked then
  begin
    checkMTAStart.Checked:= False;
    checkMTNStart.Checked:= False;
    checkMTCStart.Checked:= False;
    checkSRStart.Checked:= False;
    checkSSStart.Checked:= False;
    checkETCStart.Checked:= False;
    checkETNStart.Checked:= False;
    checkETAStart.Checked:= False;
  end;

  SaveStartStopSettings(); //Save to ini file
end;

procedure TFormLogCont.CloseButtonClick(Sender: TObject);
begin
  Close;
end;

procedure TFormLogCont.EditHotkeysCheckBoxChange(Sender: TObject);
begin
  if EditHotkeysCheckBox.Checked then
  begin
    HotkeyStringGrid.Enabled:= True;
    HotkeyStringGrid.Font.Color:= clDefault;
  end
  else
  begin
    HotkeyStringGrid.Enabled:= False;
    HotkeyStringGrid.Font.Color:= clInactiveCaption;
  end;

end;

procedure TFormLogCont.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin

  if not CloseButton.Enabled then
    StopRecording:= True;

  CloseButton.Enabled:=False;

  {$IFNDEF WINDOWS}
  FreeAndNil(SoundPlayerSyncProcess);
  FreeAndNil(SoundPlayerAsyncProcess);
  {$ENDIF}

  {Allow main page to be shown after make-believe showmodal.}
  Unit1.Form1.Enabled:=True;

end;

procedure TFormLogCont.FormKeyUp(Sender: TObject; var Key: word; Shift: TShiftState);
var
  i: integer;

begin
  if RecordingMode and not (AnnotateEdit.Focused or EditHotkeysCheckBox.Checked) then
  begin
    //Search for key in hotkey list
    //writeln(char(key),' = ',Key);
    keyslist(Key);
    for i:= 1 to HotkeyStringGrid.RowCount - 1 do begin
      if (Key = HotKeyCodes[i]) then begin
        AnnotateText:= vConfigurations.ReadString('HotKeys', 'a' +
          IntToStr(i - 1), '');
        PendingHotKey.Text:= AnnotateText;
        if (not SynchronizedCheckBox.Checked) then begin
          LogOneReading;
        end
        else
          PendingHotKey.Text:= AnnotateText;
      end;
    end;
  end;
end;

procedure TFormLogCont.FormShow(Sender: TObject);
var
  i: integer;

begin
  RecordingMode:= False;
  StopButton.Enabled:= False;
  PauseButton.Enabled:=False;
  PauseButton.Caption:='Pause';
  StartButton.Enabled:= True;
  LogTimePreset:= 0;
  LogTimeCurrent:= 0;
  NextRecordIn.Caption:= '';
  LCLoggedCount:= 0;
  RecordsLogged.Caption:= IntToStr(LCLoggedCount);
  LCLogFileCount:= 0;
  FilesLogged.Caption:= IntToStr(LCLogFileCount);
  OldSecond:= SecondOf(Now());
  RecordsMissedCount:= 0;
  RecordsMissed.Caption:= IntToStr(RecordsMissedCount);
  AnnotateButton.Enabled:= False;

  {Reflect the current snow LED settle count (shared with the main window).}
  SnowSettleSpin.Value:= SnowReadSkip;

  for i:= 1 to HotkeyStringGrid.RowCount - 1 do begin
    HotKeyCodes[i]:= StrToIntDef(vConfigurations.ReadString('HotKeys','h' + IntToStr(i - 1), ''), 0);
    HotkeyStringGrid.Cells[0, i]:=keyslist(StrToIntDef(vConfigurations.ReadString('HotKeys', 'h' +IntToStr(i - 1), ''), 0));
    HotkeyStringGrid.Cells[1, i]:=vConfigurations.ReadString('HotKeys', 'a' + IntToStr(i - 1), '');
  end;

  SynchronizedCheckBox.Checked:= vConfigurations.ReadBool('HotKeys', 'sync', False);
  PersistentCheckBox.Checked:= vConfigurations.ReadBool('HotKeys', 'pers', False);

  CheckSynchronized();

  if (rotstage) then
    RSGroupBox.Visible:= True;

  if SelectedModel = model_GDM then
    GDMGroupBox.Visible:= True
  else
    GDMGroupBox.Visible:= False;

  {Check if instant recording is to be done.}
  if (ParameterCommand('-LCR') or ParameterCommand('-LCGRS')) then begin
    StartLogging:=True;
  end;

  if SelectedModel = model_V then
  begin
    AltAzPlotpanel.Visible:= True;
  end
  else
  begin
    AltAzPlotpanel.Visible:= False;
  end;

  LocationName:= DLHeaderForm.LocationNameEntry.Text;
  PositionEntry:=DLHeaderForm.PositionEntry.Text;

  {Check if user pressed Logging Continuous button}
  If StartLogging then begin
    StartLogging:=False;
    StartButtonClick(FormLogCont);
    StartUpTimer.Enabled:= True;
    {Initially show the Reading tab.}
    PageControl1.PageIndex:= 1;
  end
  else begin
    {Initially show the Trigger tab.}
    PageControl1.PageIndex:= 0;
  end;


  GetStartStopSettings();

  SetFixedTime();
end;

procedure TFormLogCont.PersistentCheckBoxChange(Sender: TObject);
begin
  if (PersistentCheckBox.Checked) then
    vConfigurations.WriteBool('HotKeys', 'pers', True)
  else
    vConfigurations.WriteBool('HotKeys', 'pers', False);
end;

procedure TFormLogCont.RadioButton1Click(Sender: TObject);
begin
  // Check all radio buttons in this group, and save value
  if RadioButton1.Checked then
    LCTriggerMode:= 0;
  if RadioButton2.Checked then
    LCTriggerMode:= 1;
  if RadioButton3.Checked then
    LCTriggerMode:= 2;
  if RadioButton4.Checked then
    LCTriggerMode:= 3;
  if RadioButton5.Checked then
    LCTriggerMode:= 4;
  if RadioButton6.Checked then
    LCTriggerMode:= 5;
  if RadioButton7.Checked then
    LCTriggerMode:= 6;
  if RadioButton8.Checked then
    LCTriggerMode:= 7;

  vConfigurations.WriteString('LogContinuousSettings', 'Mode', IntToStr(LCTriggerMode));

end;

procedure TFormLogCont.SetLocationButtonClick(Sender: TObject);
begin

  worldmap.FormWorldmap.ShowModal;

  GetStartStopSettings();

end;

procedure GoToReadScript();
var
  FilePath: String;
  tfIn: TextFile;
  s: string;
  i:Integer = 0; //line counter
  pieces: TStringList; //delimited result from generic read
  Lst: TStringList;

begin

  pieces:= TStringList.Create;
  pieces.Delimiter:= ';';
  pieces.StrictDelimiter:= False; //Parse spaces also

  {Initialize display list}
  Lst:=TStringList.Create;
  Lst.CommaText:= 'Step,Zenith,Azimuth';
  FormLogCont.GoToCommandStringGrid.Clean;
  FormLogCont.GoToCommandStringGrid.Rows[0]:=Lst;
  FormLogCont.GoToCommandStringGrid.row:= 1;


  {Read in steps from selected .goto script file into array}
  FilePath:= RemoveMultiSlash(DataDirectory + DirectorySeparator + SelectedGoToCommandFile+'.goto');
  StatusMessage('Selected GoTo script file:'+FilePath);

  { Read the selected colour scheme }
  if FileExists(FilePath) then begin
    AssignFile(tfIn, FilePath);

    try
      reset(tfIn);      // Open the file for reading

      // Keep reading lines until the end of the file is reached
      while not eof(tfIn) do begin
        readln(tfIn, s);
        if (not AnsiStartsStr('#',s)) then begin
          {Parse data into fields}
          pieces.DelimitedText:= s;
          if pieces.Count=2 then begin
            SetLength(GoToRecords,i+1);
            GoToRecords[i].Zenith:=StrToFloatDef(pieces[0],0);
            GoToRecords[i].Azimuth:=StrToIntDef(pieces[1],0);
            FormLogCont.GoToCommandStringGrid.InsertRowWithValues(i+1,[IntToStr(i+1),pieces[0],pieces[1]]);
            Inc(i);
          end
          else begin
            StatusMessage('Looking for 2 columns, got '+IntToStr(pieces.Count)+', '+s);
            exit;
          end;

        end;
      end;

      CloseFile(tfIn);      // Done, so close the file.

    except
      on E: EInOutError do
       StatusMessage('GoTo script file handling error occurred. Details: '+ E.Message + 'On row:'+IntToStr(i));
    end;

    GoToProgramStepsTotal:=Length(GoToRecords);
    FormLogCont.GoToStepsTotalDisplay.Caption:=IntToStr(GoToProgramStepsTotal);
  end //End checking if file exists
  else begin //file did not exist
    StatusMessage('GoTo script does not exits:'+ FilePath);
  end;

  if Assigned(pieces) then FreeAndNil(pieces);
  Lst.Free;

end;

procedure TFormLogCont.FineTimerTimer(Sender: TObject);
var
  result:String;
  pieces: TStringList; //delimited result from generic read

  {==== Function to convert float to precise Synscan Hex ====}
  function GoToSynScanHex(Angle:Float):String;
  begin
      GoToSynScanHex:=IntToHex(round((Angle*16777216.0*256.0)/360.0),4);
  end;
  {==========================================================}
begin
  // Only trigger out once per second with ~20ms accuracy.
  // This prevents general drift in recordings.

  pieces:= TStringList.Create;
  pieces.Delimiter:= ',';
  pieces.StrictDelimiter:= False; //Parse spaces also

  FineTimer.Enabled:= False;

  if (SecondOf(Now()) <> OldSecond) then begin
    OldSecond:= SecondOf(Now());
    if (RecordingMode) then begin
      //Gets triggered by the fine timer once per second if RecordingMode is true.

      if (Recording = False) then begin  //Prevent more recording while already busy saving a record
        ThisMoment:= RecodeMilliSecond(Now, 0);
        CurrentTime.Caption:= FormatDateTime('yyyy-mm-dd hh:nn:ss', ThisMoment);

        { Handle GoTo
         - setup from Record button ***}

        // All stages GoToStageLabel
        if GoToEnabled then begin

         {For each position; pull up positions from script file.
         - increment program GoToProgramStep.
         - stop when after reading from last step.
         }

         {**** check that command step is within range}
         GoToDesiredZenith:=GoToRecords[GoToCommandStep].Zenith;
         GoToDesiredAzimuth:=GoToRecords[GoToCommandStep].Azimuth;

         case GoToStage of

         0: begin  {Move to position}
           StatusMessage(Format('Stage 0; request move to position _n_ of _total_ : %.3f, %.3f',
              [GoToDesiredZenith,GoToDesiredAzimuth]));
           GoToInPosition:=False; //Default to "no in position".
           GoToCommand(gtSetZenithAzimuth);
           Inc(GoToStage);
         end;

         1: Begin  { keep scanning GoTo device until not moving, then move to next stage.}
             if GoToInPosition then begin //Check if done
               StatusMessage('GoTo device has moved to position.');
               Inc(GoToStage);  //then move to next GoToStage.
             end else begin
               StatusMessage('Waiting until GoTo device has moved to position.');
             end;
           end;

         {Keep reading until the first fresh reading arrives, then move to next stage.}
         2: Begin
           result:=GetReading();
           pieces.DelimitedText:= result;
           if pieces.Count=8 then begin
            if ((pieces.Strings[pieces.Count-1]='F') or (pieces.Strings[pieces.Count-1]='P')) then begin
              StatusMessage('Got first fresh reading.');
              Inc(GoToStage);
            end else begin
              StatusMessage('Got stale reading: '+pieces.Strings[pieces.Count-1]);
            end;
           end
           else begin
             StatusMessage('Expecting 8 pieces, got '+IntToStr(pieces.Count));
             result:=GetReading();
             GoToStage:=-1;
             StopRecording:=True;
           end;

         end;

         {Keep reading until the second fresh reading arrives then logonereading.}
         3: begin
             StatusMessage('Waiting for second fresh reading.');
             result:=GetReading();
             pieces.DelimitedText:= result;
             if pieces.Count=8 then begin
              if ((pieces.Strings[7]='F') or (pieces.Strings[7]='P')) then begin
                StatusMessage('Got second fresh reading, logging: ');
                LogOneReading(result);//Log the fresh reading just taken.
                GoToStage:=0;//point back to initial stage for next reading
                Inc(GoToCommandStep);//Point to next record in .goto script
                if (GoToCommandStep>Length(GoToRecords)-1) then begin
                 GoToCommandStep:=0;
                 GoToStage:=-1;
                 StopRecording:=True;
                end;
              end;
             end
             else begin
               StatusMessage('Expecting 8 pieces, got '+IntToStr(pieces.Count));
             end;
           end

           else begin
             StatusMessage('Unknown stage: '+IntToStr(GoToStage));
             GoToStage:=-1;
             StopRecording:=True;
           end;
         end;


        end
        else begin {Goto not enabled}

          // Check if counter has expired while in Seconds or Minutes mode
          case LCTriggerMode of
            0..1: begin // Seconds or Minutes mode selected
              Dec(LogTimeCurrent);
                if ((LogTimeCurrent = 2) and (Alert2sEnable)) then begin
                  PreAlertSound.StopSound;
                  PreAlertSound.Execute;
                end;

                if (LogTimeCurrent <= 0) then begin

                //Next recording time should really be calculated here in case the logging/transferring takes too long. But. that might require som major re-writing.

                // Restart counter display for continuous logging
                LogTimeCurrent:= LogTimePreset;
                NextRecordAtTimestamp:=IncSecond(ThisMoment, LogTimeCurrent);
                NextRecordAt.Caption:= FormatDateTime('yyyy-mm-dd hh:nn:ss',NextRecordAtTimestamp);
                NextRecordIn.Caption:= Sec2DHMS(LogTimeCurrent);

                // Log value//* threshold checking
                LogOneReading;

                if ((AlertEnable=2) or ((AlertEnable=1) and FreshReading)) then begin
                  FreshSound.StopSound;
                  FreshSound.Execute;
                end;


                RecordsLogged.Caption:= IntToStr(LCLoggedCount);
              end
              else
                NextRecordIn.Caption:= Sec2DHMS(LogTimeCurrent);

              Application.ProcessMessages;
            end;

            2..7:
            begin // On-the-clock mode selected
              if not Freshness then begin
                if (((CompareDateTime(ThisMoment, NextRecordAtTimestamp-2*OneSecond))=0) and (Alert2sEnable)) then begin
                  PreAlertSound.StopSound;
                  PreAlertSound.Execute;
                end;
              end;

              if (CompareDateTime(ThisMoment, NextRecordAtTimestamp) >= 0) then begin

                case LCTriggerMode of
                  2: OnTheClock(ThisMoment, 1); //Every 1 minute
                  3: OnTheClock(ThisMoment, 5); //Every 5 minutes
                  4: OnTheClock(ThisMoment, 10); //Every 10 minutes
                  5: OnTheClock(ThisMoment, 15); //Every 15 minutes
                  6: OnTheClock(ThisMoment, 30); //Every 30 minutes
                  7: OnTheClock(ThisMoment, 60); //Every hour
                end;

                // Log value//* threshold checking
                LogOneReading;

                RecordsLogged.Caption:= IntToStr(LCLoggedCount);
              end
              else begin//continue counting
                LogTimeCurrent:= Round(SecondSpan(ThisMoment, NextRecordAtTimestamp));
                NextRecordIn.Caption:= Sec2DHMS(LogTimeCurrent);
              end;
            end;
          end;
        end;

        if StopRecording then begin
          RecordingMode:= False;
          StopButton.Enabled:= False;
          PauseButton.Enabled:=False;
          PauseButton.Caption:='Pause';
          StartButton.Enabled:= True;
          LCTrigSecondsSpin.Enabled:= True;
          LCTrigMinutesSpin.Enabled:= True;
          LCThreshold.Enabled:= True;
          LogTimePreset:= 0;
          LogTimeCurrent:= 0;
          NextRecordIn.Caption:= '';
          AnnotateButton.Enabled:= False;
          EditHotkeysCheckBox.Checked:= True;
          EditHotkeysCheckBox.Enabled:= False;
          HotkeyStringGrid.Enabled:= True;
          HotkeyStringGrid.Font.Color:= clDefault;
          GPSEnable.Enabled:=True;
          OptionsGroup.Enabled:=True;

          if (rotstage) then begin
            RSser.SendString('E0' + chr(13) + chr(10));
            RSser.Free;
          end;

          {Exit program if command line GoTo run stop mode is active}
          if ParameterCommand('-LCGRS') then begin
             unit1.Form1.Close;
          end;

          CloseButton.Enabled:= True;
          FrequencyGroup.Enabled:= True;

          {Clear persistent flag indicating that logging has stopped. Used for crash recovery. }
          vConfigurations.WriteBool('LogContinuousPersistence', 'LoggingUnderway', False);

        end;
      end; //End of checking Recording flag

      { ======== Alarm sounding ========= }
      {Sound alarm if requested and not snoozing}
      //Check if alarm has been requested and enabled
      If (AlarmRequest and AlarmSoundEnable) then begin

       {Only play alarm if snooze has not been pressed and repeat time is active}
       If ((AlarmSnoozeCurrentTime>=AlarmSnoozeTime) and (AlarmRepeatCurrentTime>=AlarmRepeatTime)) then begin

        If LazFileUtils.FileExistsUTF8(AlarmSound.SoundFile) then begin
          StatusMessage('Alarm sounded.');
          AlarmSound.StopSound;
          AlarmSound.Execute;
        end;

        AlarmRepeatCurrentTime:=-1; //Reset repeat timer

       end;

      end;

      { update Repeat timer. }
      Inc(AlarmRepeatCurrentTime);
      EnsureRange(AlarmRepeatCurrentTime,0,AlarmRepeatTime);
      SetRepeatProgress();

      {Alarm request may come and go depending on actual darkness,
       and the snooze timer should continue regardless.}
      Inc(AlarmSnoozeCurrentTime);
      EnsureRange(AlarmSnoozeCurrentTime,0,AlarmSnoozeTime);
      SetSnoozeProgress();

      { ======== End of Alarm sounding ========= }

    end; //End of checking RecordingMode

  end; //End of checking once per second

  //Set length of timer and start it if still recording
  if not StopRecording then begin
    FineTimer.Interval:= 1000 - MilliSecondOf(Now());
    FineTimer.Enabled:= True;
  end;

end;


procedure TFormLogCont.StartButtonClick(Sender: TObject);  //Start recording
var
  Hpieces:TStringList; //Humidity result
begin
  StopRecording:= False;
  StatusMessage('Start recording Log continuous');

  //Clear out old plots.
  MPSASSeries.Clear;
  RedSeries.Clear;
  GreenSeries.Clear;
  BlueSeries.Clear;
  ClearSeries.Clear;
  TempSeries.Clear;
  MoonSeries.Clear;
  if Assigned(SnowSeries) then SnowSeries.Clear;

  {Multi-meter (-SUI) logging: ensure one MPSAS line per meter, titled by serial.}
  SetupDevicePlotSeries;

  {Single-meter snow staging plots two lines on the MPSAS axis: the regular sky
   reading (LED off) on MPSASSeries and the LED-on snow reading on SnowSeries.
   Label both so the legend is unambiguous; without a title the primary line
   appears blank and looks swapped with the named "Snow (LED on)" line.}
  if NumberOfMultipleDevices=0 then begin
    if (SelectedModel=model_DLS) and SnowLoggingEnabled and SnowStagedLogging then begin
      MPSASSeries.Title:= 'Sky (LED off)';
      if Assigned(SnowSeries) then SnowSeries.Title:= 'Snow (LED on)';
      Chart1.Legend.Visible:= True;
    end
    else
      MPSASSeries.Title:= ''; //Plain single-meter logging needs no legend label.
  end;

  SetFixedReadings();

  OldStartPlotTimeStamp:=Now();
  BestDarknessTime:=Now();
  SetFixedTime();

  if (rotstage) then begin
    //0. Connect to the ardiuno rotation stage serial port.
    //1. Center between limit switches, move to actual center offset, set position to zero
    //2. Take a reading
    //3. Step to the right
    //   goto step 2 until right limit switch is hit

    RSser:= TBlockSerial.Create;
    RSser.LinuxLock:= False;
    //lock file sometimes persists stuck if program closes before port
    //RSser.Connect('/dev/ttyUSB2'); //ComPort
    RSser.Connect(FormLogCont.RSComboBox.Text); //ComPort
    RSser.config(9600, 8, 'N', SB1, False, False);
    WriteLn('Device: ' + RSser.Device + '   Status: ' + RSser.LastErrorDesc +
      ' ' + IntToStr(RSser.LastError));

    //Update position display
    FormLogCont.RSStatusBar.Panels.Items[0].Text:= 'Connected';
    FormLogCont.RSCurrentPositionAngleDisplay.Text:= '';
    Application.ProcessMessages;

    //Turn LIGHT on
    sleep(1500);
    RSser.SendString('C4' + chr(13) + chr(10));
    ParseRotstage(Trim(RSser.Recvstring(20000)));

    //Enable motor
    sleep(1500);
    RSser.SendString('E1' + chr(13) + chr(10));
    ParseRotstage(Trim(RSser.Recvstring(20000)));

    // Center the stage
    FormLogCont.RSStatusBar.Panels.Items[0].Text:= 'Finding limits';
    Application.ProcessMessages;
    RSser.SendString('C0' + chr(13) + chr(10));
    ParseRotstage(Trim(RSser.Recvstring(20000)));

    FormLogCont.RSStatusBar.Panels.Items[0].Text:= 'Found limits';
    Application.ProcessMessages;
    sleep(1000);
    // Move to actual center offset
    FormLogCont.RSStatusBar.Panels.Items[0].Text:= 'Applying center offset';
    Application.ProcessMessages;
    RSser.SendString('M0' + chr(13) + chr(10));
    ParseRotstage(Trim(RSser.Recvstring(20000)));
    FormLogCont.RSStatusBar.Panels.Items[0].Text:= 'Mechanically centered';
    FormLogCont.RSCurrentPositionAngleDisplay.Text:= '0.0';

    Application.ProcessMessages;

    sleep(1000);
    // Set position to zero
    RSser.SendString('C3' + chr(13) + chr(10));
    FormLogCont.RSStatusBar.Panels.Items[0].Text:= 'Position centered';
    //Update position display
    FormLogCont.RSCurrentPositionStepDisplay.Text:= '0';
    Application.ProcessMessages;

  end;

  //Grab selected time, convert to seconds, store in LogTimePreset


  //Grab current time:
  ThisMoment:= RecodeMilliSecond(Now, 0); //Strip fractional seconds for display
  CurrentTime.Caption:= FormatDateTime('yyyy-mm-dd hh:nn:ss', ThisMoment);

    case LCTriggerMode of
      0: begin //Every x seconds (on the second)
        LogTimePreset:= LCTrigSeconds;
        LogTimeCurrent:= LogTimePreset;
        NextRecordAtTimestamp:=IncSecond(ThisMoment, LCTrigSeconds);
        NextRecordAt.Caption:= FormatDateTime('yyyy-mm-dd hh:nn:ss',NextRecordAtTimestamp);
        if LCTrigSeconds = 1 then
          Setting:= Format('every %d second', [LCTrigSeconds])
        else
          Setting:= Format('every %d seconds', [LCTrigSeconds]);
      end;
      1: begin //Every x minutes (at 0 seconds)
        { TODO : ontheclock x minutes }
        LCTrigMinutes:=LimitInteger(LCTrigMinutes,1,255);
        LogTimePreset:= LCTrigMinutes * 60;
        LogTimeCurrent:= LogTimePreset;
        NextRecordAtTimestamp:=IncSecond(ThisMoment, LogTimeCurrent);
        NextRecordAt.Caption:= FormatDateTime('yyyy-mm-dd hh:nn:ss',NextRecordAtTimestamp);
        if LCTrigMinutes = 1 then
          Setting:= Format('every 1 minute', [LCTrigMinutes])
        else
          Setting:= Format('every %d minutes', [LCTrigMinutes]);
      end;
      2: begin //Every 1 minutes
        OnTheClock(ThisMoment, 1);
        Setting:= 'every 1 minute on the minute';
      end;
      3: begin //Every 5 minutes
        OnTheClock(ThisMoment, 5);
        Setting:= 'every 5 minutes on the 1/12th hour';
      end;
      4: begin //Every 10 minutes
        OnTheClock(ThisMoment, 10);
        Setting:= 'every 10 minutes on the 1/6th hour';
      end;
      5: begin //Every 15 minutes
        OnTheClock(ThisMoment, 15);
        Setting:= 'every 15 minutes on the 1/4 hour';
      end;
      6: begin //Every 30 minutes
        OnTheClock(ThisMoment, 30);
        Setting:= 'every 30 minutes on the 1/2 hour';
      end;
      7: begin //Every hour
        OnTheClock(ThisMoment, 60);
        Setting:= 'every hour on the hour';
      end;
    end;


  //Threshold setting
  Setting:= Setting + ',Threshold = ' + FloatToStr(LCThresholdValue) + ' mpsas';

  //Limit setting
  if RecordLimitSpin.Value>0 then
    Setting:= Setting + ', Record limit = ' + IntToStr(RecordLimitSpin.Value) ;

  NextRecordIn.Caption:= Sec2DHMS(LogTimeCurrent);
  RecordingMode:= True;
  Chart2LineSeries1.Clear; //Clear chart
  PageControl1.PageIndex:= 1;   //show the chart tab
  PauseButton.Enabled:=True;
  PauseButton.Caption:='Pause';
  StopButton.Enabled:= True;
  StartButton.Enabled:= False;
  CloseButton.Enabled:= False;
  LCTrigSecondsSpin.Enabled:= False;
  LCTrigMinutesSpin.Enabled:= False;
  LCThreshold.Enabled:= False;
  LCLoggedCount:= 0;
  RecordsLogged.Caption:= IntToStr(LCLoggedCount);
  FrequencyGroup.Enabled:= False;
  RecordsViewSynEdit.Clear;
  AnnotateButton.Enabled:= True;
  EditHotkeysCheckBox.Checked:= False;
  EditHotkeysCheckBox.Enabled:= True;
  HotkeyStringGrid.Enabled:= False;
  HotkeyStringGrid.Font.Color:= clInactiveCaption;
  RecordsMissed.Color:= clDefault;
  RecordsMissed.Font.Color:= clDefault;
  RecordsMissedCount:= 0;
  RecordsMissed.Caption:= IntToStr(RecordsMissedCount);
  RSCurrentStepNumber:= 0;
  GPSEnable.Enabled:=False;
  OptionsGroup.Enabled:=False;
  GoToProgramStep:=0;//Beginning of position steps
  GoToStage:=0; //Beginning of GoTo stage

  AlarmSnoozeCurrentTime:=AlarmSnoozeTimePreset; //Number of seconds since Snooze was pressed, default =snooze expired
  AlarmRepeatCurrentTime:=AlarmRepeatTimePreset; //Number of seconds since Snooze was pressed, default = expired

  //Check if Humidity accessory is enabled
  if ((SelectedModel=model_LELU) and (StrToIntDef(SelectedFeature,0)>=51)) then begin
    Hpieces:= TStringList.Create;
    Hpieces.Delimiter:= ',';
    Hpieces.DelimitedText:=SendGet('A1x');
    if Hpieces.Count=7 then
      if Hpieces.Strings[2]='E' then  //Enabled.
       A1Enabled:=True
      else
       A1Enabled:=False;
  end;

  //Set defdault scaling and labels
  CheckInvert();

  ReadingUnits.Caption:= 'mags/arcsec²';
  Chart1.LeftAxis.Title.Caption:= 'MPSAS';

  //Set custom scaling and labels
  case SelectedModel of
    model_ADA: begin //ADA model selected
      ReadingUnits.Caption:= 'ADA factor';
      DLHeaderStyle:= 'ADA';
      Chart1.LeftAxis.Title.Caption:= 'ADAFactor';
      Chart1.AxisList[1].Range.Max:= 20;
      // Set fixed vertical range for easier identification of Aurora.
      Chart1.AxisList[1].Range.Min:= 0;
      Chart1.AxisList[1].Range.UseMax:= True;
      Chart1.AxisList[1].Range.UseMin:= True;
    end;
    model_GDM: begin //Geomagnetic Disturbance Monitor model selected
      ReadingUnits.Caption:= 'counts';
      DLHeaderStyle:= 'GDM';
      Chart1.LeftAxis.Title.Caption:= 'counts';
    end;
    model_V: begin //Vector model selected
      DLHeaderStyle:= 'DL-V-Log';
    end;
    model_C: begin //Colour model selected
      DLHeaderStyle:= 'C';
    end;
    else begin //All other models
      DLHeaderStyle:= 'LE';
    end;
  end; //end of checking models

  {Enable Moon data plotting}
  Chart1.AxisList[3].Visible:=MoonData;


  {Open previous two days worth of log files if LoggingUnderWay already (restarted after a crash)}
  if LCLoggingUnderway then begin
   {Get todays and yesterdays dates.}
   {Import all files from those dates.}
  end;



  try
    { Open file and store header }
    OpenComm();
    WriteDLHeader(DLHeaderStyle, Format('Logged continuously %s.', [setting]));
    LogFieldNames.Text:=RightStr(header_utils.FieldNames,length(header_utils.FieldNames)-2);
    LogFieldUnits.Text:=RightStr(header_utils.FieldUnits,length(header_utils.FieldUnits)-2);
    LCLogFileCount:= 1;
    FilesLogged.Caption:= IntToStr(LCLogFileCount);
    FilesLabel.Caption:= 'file';

    { Save initial log time for 24 rollover check later }
    LCStartFileDay:= DayOf(ThisMoment);
    LCStartFileHour:= HourOf(ThisMoment);

    {Set persistent flag indicating that logging is underway. Used for crash recovery. }
    vConfigurations.WriteBool('LogContinuousPersistence', 'LoggingUnderway', True);


    LogOneReading; {First reading}

  except
    StatusMessage('ERROR! IORESULT: ' + IntToStr(IOResult) + ' during StartButtonClick');

    {Stop recording right away}
    ShowMessage('Problem starting logging, check header timezone information.'
       + sLineBreak
       + 'ERROR! IORESULT: ' + IntToStr(IOResult) + ' during StartButtonClick');

    Close;
  end;

  //Set length of timer and start it
  FineTimer.Interval:= 1000 - MilliSecondOf(Now());
  FineTimer.Enabled:= True;

end;

procedure TFormLogCont.StartUpTimerTimer(Sender: TObject);
begin
  //Check if window should be minimized automatically when instant recording is desired
  if ParameterCommand('-LCMIN') then
  begin
    Application.Minimize;
    Application.ProcessMessages;
  end;
  StartUpTimer.Enabled:= False;
end;

procedure TFormLogCont.StopButtonClick(Sender: TObject);
begin
  StopRecording:= True;
end;

procedure TFormLogCont.HotkeyStringGridKeyUp(Sender: TObject;
  var Key: word; Shift: TShiftState);
var
  i: integer;

  function StrippedOfNonAscii(const s: string): string;
  var
    i, Count: integer;
  begin
    SetLength(Result, Length(s));
    Count:= 0;
    for i:= 1 to Length(s) do
    begin
      if ((s[i] >= #32) and (s[i] < #127)) or (s[i] in [#10, #13]) then
      begin
        Inc(Count);
        Result[Count]:= s[i];
      end;
    end;
    SetLength(Result, Count);
  end;

begin
  if HotkeyStringGrid.Col = 0 then
  begin
    HotkeyStringGrid.Cells[HotkeyStringGrid.Col, HotkeyStringGrid.Row]:= keyslist(Key);
    HotKeyCodes[HotkeyStringGrid.Row]:= Key;
  end;

  for i:= 1 to HotkeyStringGrid.RowCount - 1 do
  begin
    vConfigurations.WriteString('HotKeys', 'h' + IntToStr(i - 1),
      IntToStr(HotKeyCodes[i]));
    vConfigurations.WriteString('HotKeys', 'a' + IntToStr(i - 1),
      StrippedOfNonAscii(HotkeyStringGrid.Cells[1, i]));
  end;
end;

procedure TFormLogCont.HotkeyStringGridSelectEditor(Sender: TObject;
  aCol, aRow: integer; var Editor: TWinControl);
begin
  if Editor is TStringCellEditor then
    CustomizeEdit(aCol, aRow, Editor as TStringCellEditor);
end;

procedure TFormLogCont.CheckSynchronized();
begin
  if (SynchronizedCheckBox.Checked) then
  begin
    PendingHotKey.Enabled:= True;
    PendingLabel.Enabled:= True;
    vConfigurations.WriteBool('HotKeys', 'sync', True);
  end
  else
  begin
    PendingHotKey.Enabled:= False;
    PendingLabel.Enabled:= False;
    vConfigurations.WriteBool('HotKeys', 'sync', False);
  end;
end;

procedure TFormLogCont.SynchronizedCheckBoxChange(Sender: TObject);
begin
  CheckSynchronized();
end;

procedure TFormLogCont.TrRdgHelpButtonClick(Sender: TObject);
begin
    Application.MessageBox(
    'This sends each reading by TCP to a server.'+ sLineBreak+ sLineBreak+
    'Sign up to the Globe at Night server (email globeatnight.network@gmail.com) and provide your location, coordinates, and serial number to receive the IP address and port number.'+ sLineBreak+
    ' ', '', MB_HELP)  ;
end;

procedure TFormLogCont.TrRdgAddressEntryChange(Sender: TObject);
begin
  if not FormCreating then begin
    TrRdgAddress:=TrRdgAddressEntry.text;
    vConfigurations.WriteString('FTPSettings','TrRdgAddress',TrRdgAddress);
  end;
end;

procedure TFormLogCont.TrRdgPortEditChange(Sender: TObject);
begin
  if not FormCreating then begin
    TrRdgPort:=TrRdgPortEdit.Text;
    vConfigurations.WriteString('FTPSettings','TrRdgPort',TrRdgPort);
  end;
end;

procedure TFormLogCont.TrRdgTestButtonClick(Sender: TObject);
begin
      TransferReading(SendGet('Rx'));
end;

procedure TFormLogCont.TrRdgEnableCheckBoxClick(Sender: TObject);
begin
  if not FormCreating then begin
    TrRdgEnabled:=TrRdgEnableCheckBox.Checked;
    vConfigurations.WriteBool('FTPSettings', 'TrRdgEnabled',TrRdgEnabled);
  end;
end;

procedure TFormLogCont.CustomizeEdit(ACol, ARow: integer; Editor: TStringCellEditor);
begin
  if aCol = 1 then
    Editor.MaxLength:= 0;

end;

{ Transfer the .dat or .csv file to a server. }
function TFormLogCont.FTPSend(): boolean;
var
  FTPClient: TFTPSend;
  rc: boolean = False;
  RemoteDir: string;
  RemoteFilename: string;
  CommandsString: array of ansistring;
  OutputString: ansistring = '';
  ExitStatus: Integer;
  SFTPCommand: String;
  Process: TProcess;
  StartTime: TDateTime;

begin
  StartTime:=Now();
  Process:= TProcess.Create(nil);
  TransferFullResult.Clear;
  TransferSendResult.Clear;
  TransferSendResult.Append('Transferring file(s)...');
  Application.ProcessMessages;

  ThisMomentUTC:=NowUTC(); // Start recording elapsed time to perform the transfer

  if TransferPLOT then begin
    {Graphics File Name}
    gfn:=FormatDateTime('YYYYMMDD', StartPlotTimeStamp)+'.png';
    lgfn:=RemoveMultiSlash(LogsDirectory+DirectorySeparator+gfn);
    {HTML File Name}
    hfn:=RemoveMultiSlash(LogsDirectory+DirectorySeparator+'sqm.html');
  end;

  case TransferProtocolSelector.Items[TransferProtocolSelector.ItemIndex] of
    'FTP': begin

      if (TransferAddress <> '') then begin
        // Create the FTP Client object and set the FTP parameters
        FTPClient:= TFTPSend.Create;
        FTPClient.Timeout:= StrToIntDef(TransferTimeout.Text,1000); //milliseconds
        with FTPClient do begin
          if (TransferPort = '') then begin
            TransferPort:= cFtpProtocol;
            TransferPortEntry.Text:=TransferPort;
          end;
          TargetPort:= TransferPort;
          TargetHost:= TransferAddress;
          UserName:= TransferUsername;
          Password:= TransferPassword;
          RemoteDir:= TransferRemoteDirectory;

          if (TransferLocalFilename = '') then begin
           TransferLocalFilename:='test.txt';
           TransferLocalFilenameDisplay.Caption:=TransferLocalFilename;
          end;

          RemoteFilename:= ExtractFileName(TransferLocalFilename);
          TransferRemoteFilename.Text:= RemoteFilename;
          TransferSendResult.Text:= 'Pending transfer ...';

          //-----------------------------------------------------------------------
          // bail out if the FTP connect fails
          if not LogIn then begin
            TransferSendResult.Text:= 'Unable to login within '+IntToStr(Timeout)+'ms.';
            exit;
            end;
          //------------------------------------------------------------------------

          // Set filename to FTP
          DirectFileName:= TransferLocalFilename;
          DirectFile:= True;
          //------------------------------------------------------------------------

          // change directory if requested
          if RemoteDir <> '' then
            if ChangeWorkingDir(RemoteDir) then
              TransferRemoteDirectorySuccess.Brush.Color:= clLime
            else
              TransferRemoteDirectorySuccess.Brush.Color:= clRed;
          //------------------------------------------------------------------------

          // STORE file to FTP server, and try to append lines as necessary (proven with wireshark).
          try
             rc:= StoreFile(RemoteDir + RemoteFilename, True);
          except
            on e:Exception do
            TransferSendResult.Lines.Append(e.ClassName + '  ' + e.Message);
          end;

          { Check if storefile failed, and add timestamp}
          if rc then
            TransferSendResult.Lines.Append('Sent: '+FormatDateTime('YYYY-MM-DD hh:nn:ss', Now()))
          else
            TransferSendResult.Lines.Append('Failed: '+FormatDateTime('YYYY-MM-DD hh:nn:ss', Now()));

          TransferSendResult.Lines.Append('Completed in '+ IntToStr(MilliSecondsBetween(ThisMomentUTC,NowUTC()))+' ms.');

          TransferFullResult.Lines.Assign(FullResult);
          //------------------------------------------------------------------------


          //------------------------------------------------------------------------
          if TransferPLOT then begin
           PrepareTransferPayload();

           RemoteFilename:='sqm.html';
           try
              rc:= StoreFile(RemoteDir + RemoteFilename, True);
           except
             on e:Exception do
             TransferSendResult.Lines.Append(e.ClassName + '  ' + e.Message);
           end;
           { Check if storefile failed, and add timestamp}
           if rc then
             TransferSendResult.Lines.Append('Sent html: '+FormatDateTime('YYYY-MM-DD hh:nn:ss', Now()))
           else
             TransferSendResult.Lines.Append('Failed sending html: '+FormatDateTime('YYYY-MM-DD hh:nn:ss', Now()));
           TransferSendResult.Lines.Append('Completed sending html in '+ IntToStr(MilliSecondsBetween(ThisMomentUTC,NowUTC()))+' ms.');
           TransferFullResult.Lines.Assign(FullResult);


           DirectFileName:=lgfn;
           RemoteFilename:='plot.png';
           try
              rc:= StoreFile(RemoteDir + RemoteFilename, True);
           except
             on e:Exception do
             TransferSendResult.Lines.Append(e.ClassName + '  ' + e.Message);
           end;
           { Check if storefile failed, and add timestamp}
           if rc then
             TransferSendResult.Lines.Append('Sent plot: '+FormatDateTime('YYYY-MM-DD hh:nn:ss', Now()))
           else
             TransferSendResult.Lines.Append('Failed sending plot: '+FormatDateTime('YYYY-MM-DD hh:nn:ss', Now()));
           TransferSendResult.Lines.Append('Completed sending plot in '+ IntToStr(MilliSecondsBetween(ThisMomentUTC,NowUTC()))+' ms.');
           TransferFullResult.Lines.Assign(FullResult);

          end;

          //------------------------------------------------------------------------



          // close the connection
          LogOut;
          //------------------------------------------------------------------------
          // free the FTP client object
          Free;
          //------------------------------------------------------------------------
        end;
      end;  //End checking if FTP address was assigned
    end;


    'SFTP': begin
      TransferFullResult.Lines.Clear;

      //Check if sftp command exists
      if ((length(FindDefaultExecutablePath('sftp'))>0) and(Length(ExpectPath)>0))  then begin

       if (TransferPort<>'') then
        TransferPortSFTP:=TransferPort
       else
        TransferPortSFTP:='';

       //if (TransferDAT or TransferCSV) then begin
       //  SFTPCommand:='ex.sh '+  TransferLocalFilename+'\n" | sftp -o PasswordAuthentication=no '+TransferPortSFTP+' -b- '+TransferUsername+'@'+TransferAddress+':'+TransferRemoteDirectory;
       //  if RunCommandInDir(LogsDirectory,BashPath,['-c',SFTPCommand], OutputString) then
       //      TransferSendResult.Append('Passed (.dat and/or .csv)')
       //  else
       //      TransferSendResult.Append('Failed (.dat and/or .csv)');
       //  TransferFullResult.Lines.Append('SFTP command:'+BashPath+' -c '+SFTPCommand);
       //  TransferFullResult.Lines.Append('SFTP put .dat and/or .csv result:'+OutputString);
       //end;

        if TransferPLOT then begin
         PrepareTransferPayload();
         //example: sftpoptions= sftp://pi@192.168.1.118:22/path
          try
            Process.Executable:= ExpectPath;
            Process.Parameters.Add(Format('%s%sex.tcl',[DataDirectory,DirectorySeparator]));
            Process.Parameters.Add(Format('sftp://%s@%s:%s',[TransferUsername,TransferAddress,TransferPortSFTP]));
            Process.Parameters.Add(TransferPassword);
            Process.Parameters.Add(LogsDirectory);
            Process.Parameters.Add(TransferRemoteDirectory);
            Process.Parameters.Add(lgfn);
            Process.Options:= Process.Options + [poWaitOnExit];
            TransferFullResult.Append(ExpectPath);
            TransferFullResult.Append(Process.Parameters.Strings[0]);
            TransferFullResult.Append(Process.Parameters.Strings[1]);
            TransferFullResult.Append(Process.Parameters.Strings[2]);
            TransferFullResult.Append(Process.Parameters.Strings[3]);
            TransferFullResult.Append(Process.Parameters.Strings[4]);
            TransferFullResult.Append(Process.Parameters.Strings[5]);
            Process.Execute;
          except
            TransferSendResult.Append('TProcess sftp transfer of html and plot failed.');
          end;
        end;

      end
      else begin
          TransferSendResult.Text:='The sftp or expect executable does not exist.';
          StatusMessage('SFTP send error: The sftp or bash executable does not exist.');

          {Stop recording right away}
          ShowMessage('SFTP send error: The sftp or bash executable does not exist.'
             + sLineBreak
             + 'Check LogContinuout Transfer tab.');

          Close;
      end; //End of check for sftp executable existence

    end;//End of SFTP case

    {Secure CoPy is done by executing a command which must be present (scp)}
    'SCP':  begin
      TransferFullResult.Lines.Clear;

      //Check if scp command exists
      if length(FindDefaultExecutablePath('scp'))>0 then begin
        //Compose the scp command

        SetLength(CommandsString,4);
        CommandsString[0]:='-P'; //Port
        CommandsString[1]:=TransferPort; //Port definition
        CommandsString[2]:=TransferLocalFilename; //Local source file
        CommandsString[3]:=TransferUsername+'@'+TransferAddress+':'+TransferRemoteDirectory;//Destination server and file
        if RunCommand('scp', CommandsString, OutputString) then
            TransferSendResult.Text:='Passed (.dat and/or .csv)'
        else
            TransferSendResult.Text:='Failed (.dat and/or .csv)';

        TransferFullResult.Lines.Append('SCP result:'+OutputString);

        if TransferPLOT then begin
         PrepareTransferPayload();

         SetLength(CommandsString,4);
         CommandsString[0]:='-P'; //Port
         CommandsString[1]:=TransferPort; //Port definition
         CommandsString[2]:=hfn; //Local source file
         CommandsString[3]:=TransferUsername+'@'+TransferAddress+':'+TransferRemoteDirectory;//Destination server and file
         if RunCommand('scp', CommandsString, OutputString) then
             TransferSendResult.Append('Passed (html)')
         else
             TransferSendResult.Append('Failed (html)');
         TransferFullResult.Lines.Append('SCP html result:'+OutputString);

         SetLength(CommandsString,4);
         CommandsString[0]:='-P'; //Port
         CommandsString[1]:=TransferPort; //Port definition
         CommandsString[2]:=lgfn; //Local source file
         CommandsString[3]:=TransferUsername+'@'+TransferAddress+':'+TransferRemoteDirectory;//Destination server and file
         if RunCommand('scp', CommandsString, OutputString) then
             TransferSendResult.Append('Passed (plot)')
         else
             TransferSendResult.Append('Failed (plot)');
         TransferFullResult.Lines.Append('SCP plot result:'+OutputString);


        end;

      end //End of check for scp executable existence
      else begin
          TransferSendResult.Text:='The scp executable does not exist.';
          StatusMessage('SCP send error: The scp executable does not exist.');

          {Stop recording right away}
          ShowMessage('SCP send error: The scp executable does not exist.'
             + sLineBreak
             + 'Check LogContinuout Transfer tab.');

          Close;
      end;

    end; //End of SCP case
  end; //Endof protocol case

  Result:= rc;
  TransferSendResult.Append(Format('Transfer time: %0.3fs',[MilliSecondsBetween(Now,StartTime)/1000]));
  //===========================================================================
end;

procedure PrepareTransferPayload();
const
  SkeletonFilename = 'sqm-skeleton.html';
  TemplateFilename = 'sqm-template.html';
  OutputFilename = 'sqm.html';

var
  Infile: TStringList;
  OutFileName:String;
  OutFile: TextFile;

  s,sout: String; //Temporary strings
  pieces:TStringList;
  PositionLatitude, PositionLongitude: Float;

begin

  pieces:= TStringList.Create;
  pieces.Delimiter:= ',';
  pieces.DelimitedText:=PositionEntry;
  if pieces.Count>1 then begin
   PositionLatitude:=StrToFloat(pieces.Strings[0]);
   PositionLongitude:=StrToFloat(pieces.Strings[1]);
   if PositionLatitude>0 then PositionString:='N' else PositionString:='S';
   PositionString:=Format('%s%0.5f°',[PositionString,Abs(PositionLatitude)]);
   if PositionLongitude>0 then PositionString:=PositionString+' E' else PositionString:=PositionString+' W';
   PositionString:=Format('%s%0.5f°',[PositionString,Abs(PositionLongitude)]);

  end;


  Infile:= TStringList.Create;

  //Save chart for transferring later
  FormLogCont.Chart1.SaveToFile(TPortableNetworkGraphic, RemoveMultiSlash(lgfn));

  //Prepare html file
  // - copy Template file if required
  if (not FileExists(appsettings.LogsDirectory+ DirectorySeparator+TemplateFilename)) then begin
    CopyFile(
      appsettings.DataDirectory+DirectorySeparator+SkeletonFilename,
      appsettings.LogsDirectory+ DirectorySeparator+TemplateFilename
      );
  end;

  //Modify sqm-template.html by replacing variables then write to sqm.html

  //Open template html file
  Infile.LoadFromFile(appsettings.LogsDirectory+ DirectorySeparator+TemplateFilename);

  //Open output html file
  OutFileName:=appsettings.LogsDirectory+ DirectorySeparator+OutputFilename;
  AssignFile(OutFile, OutFileName);
  Rewrite(OutFile); //Open file for writing


        //Replace variables from sqm-template.html into sqm.html

        for s in Infile do begin
            if AnsiStartsStr('<!-- SQM-',s) then begin
              case s of
                '<!-- SQM-location -->'      :sout:=Format('%s',[LocationName]);
                '<!-- SQM-coordinates -->'   :sout:=Format('%s',[PositionString]);
                '<!-- SQM-timestamp -->'     :sout:=FormatDateTime('yyyy-mm-dd hh:nn:ss', ThisMoment);
                '<!-- SQM-nexttimestamp -->' :sout:=FormatDateTime('yyyy-mm-dd hh:nn:ss', NextRecordAtTimestamp);
                '<!-- SQM-mpsas -->'         :sout:=Format('%s',[DarknessString]);
                '<!-- SQM-nelm -->'          :sout:=Format('%s',[NELMstring]);
                '<!-- SQM-cdm2 -->'          :sout:=Format('%s',[CDM2String]);
                '<!-- SQM-mcdm2 -->'         :sout:=Format('%s',[MCDM2String]);
                '<!-- SQM-NSU -->'           :sout:=Format('%s',[NSUstring]);
                '<!-- SQM-Best -->'          :sout:=Format('%s',[BestDarknessString]);
                '<!-- SQM-BestTime -->'      :sout:=Format('%s',[BestDarknessTimeString]);
                '<!-- SQM-imagename -->'     :sout:=Format('%s',[gfn]);
                else
                  sout:=s;
              end;

            end else
              sout:=s;

            writeln(OutFile, sout);
        end;

        CloseFile(OutFile);
        Infile.Free;


end;

{Update GoTo script file list}
procedure TFormLogCont.UpdateGoToCommandFileList();
var
  FileName, FileNameOnly, FilePath : string;
  sr : TSearchRec;
  i: integer=0;
begin
Application.ProcessMessages;
GoToCommandFileComboBox.Clear;

FilePath:= ExtractFilePath(DataDirectory + DirectorySeparator);

{Check if any files match criteria}
StatusMessage('Looking for .goto files here: '+FilePath+'*.goto');
if FindFirstUTF8(FilePath+'*.goto',faAnyFile,sr)=0 then
  repeat
    {Get formatted file properties}
    FileName:= ExtractFileName(sr.Name);
    FileNameOnly:= ExtractFileNameOnly(sr.Name);
    {Display found filename and timestamp}
    StatusMessage(IntToStr(i)+': '+FileName);
    GoToCommandFileComboBox.items.Add(FileNameOnly);
    {Prepare for next file display}
    Inc(i);
  until FindNextUTF8(sr)<>0;
  FindCloseUTF8(sr);

  SelectedGoToCommandFile:=vConfigurations.ReadString('GoTo','CommandFile','default');
  {Check if saved goto commandfile exists}
  if not FileExists(FilePath+SelectedGoToCommandFile+'.goto') then begin
    // too irritating to pop up everytime, just uise statusmessage
    //MessageDlg ('Saved GoTo commandfile file setting does not exist:'+sLineBreak + FilePath+SelectedGoToCommandFile+'.goto' +sLineBreak + 'Removing GoTo commandfile setting.', mtConfirmation,[mbOK],0);
    StatusMessage('Saved GoTo commandfile file setting does not exist:' + FilePath+SelectedGoToCommandFile+'.goto'  + 'Removing GoTo commandfile setting.');
    {Reset}
    SelectedGoToCommandFile:='';
    {Remove setting}
    vConfigurations.WriteDeletKey('GoTo','CommandFile');
  end;

  GoToCommandFileComboBox.Text:=SelectedGoToCommandFile;

end;

initialization
  {$I logcont.lrs}

end.

