unit cli_utils;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, StrUtils
  , synaser, synautil, blcksock

  {$IFDEF Unix}, BaseUnix{$ENDIF}//for fpreadlink
  , vinfo
  , lazutf8sysutils //Defines NOWUTC.
  , upascaltz //Defines TPascalTZ
  , dateutils //Defines DayOf
  , Math //for Float identifier
  //, epiktimer //define Epiktimer
  ;

procedure FindUSBtty(serialnumber:string);
function OpenComm() : boolean;
function CloseComm() : boolean;
function SendGet(command:string; LeaveOpen:boolean = False; Timeout:Integer=3000; GetAlso:boolean = True; HideStatus:boolean = False) : string;
procedure WriteDLHeader(Setting: String = '' );
procedure LogOneReading();
procedure OnTheClock(const ThisMoment: TDateTime; const Granularity: integer);
procedure testfptimer();
procedure verbose(level:integer; message:string);

var
  verbosity: Integer = 0;
  SelectedPort: String;
  DLRecFile: TextFile;
  RamStatusFile: TextFile;
  LogFileName: AnsiString;
  SelectedUnitSerialNumber: String;
  INISection: String;
  ptz :TPascalTZ;
  TZRegion, TZLocation:String;

  //Used for logging and display
  Temperature, Darkness: Float;

  //Used to check 24 hr log file rollover
  LCStartFileTime: TDateTime;

  Setting: string; //Setting of recorder

  //Fix for multi-language issues
  FPointSeparator, FCommaSeparator: TFormatSettings;

  Recording: boolean; //Flag to indicate that record is being logged
  RecordingMode: boolean = False; //Indicate that user has pressed Record button

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

  //Log continuous mode
  LCMode:String;
  LCFreq:Integer;
  LogTimePreset, LogTimeCurrent: Integer;
  DesiredLogTime: TDateTime;
  StopRecording: boolean;
  //ET: TEpikTimer;

const
  verbose_error=1;
  verbose_action=2;
  verbose_debug=3;



implementation

uses
    appsettings;


var
   ser: TBlockSerial;

procedure verbose(level:integer; message:string);
begin
  if (verbosity >= level) then
    writeln(FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz": "', Now)+message);
end;


// Open the communications port
function OpenComm() : boolean;
begin
     verbose(verbose_debug,'Opening port: '+SelectedPort);
     ser.LinuxLock:=False; //lock file sometimes persists stuck if program closes before port
     verbose(verbose_debug,'Trying to connect ...');
     ser.Connect(SelectedPort);
     verbose(verbose_debug,'Connected to: '+SelectedPort);
     ser.config(115200, 8, 'N', SB1, False, False);
     OpenComm:=True;    //Indicate success
     verbose(verbose_debug,'Opened port: '+SelectedPort);
end;

// Close the communications port
function CloseComm() : boolean;
begin
     ser.CloseSocket;
     CloseComm:=True;       //Indicate success
end;

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

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

     if (not LeaveOpen) then
        begin
                OpenComm();
        end;
     ErrorString:='';

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


     if (not LeaveOpen) then
        begin
                CloseComm();
        end;
     if not HideStatus then
        begin
          if GetAlso then
            verbose(verbose_debug,'Sent: '+command+'   To: '+SelectedPort+'   Received: '+SendGet+ErrorString)
          else
            verbose(verbose_debug,'Sent: '+command+'   To: '+SelectedPort);
        end;
end;

//Return the tty port name of a specific USB serial number (if found)
procedure FindUSBtty(serialnumber:string);
const
    USBDevicePath = '/dev/serial/by-id/';
Var Info : TSearchRec;
    Count : Longint;
    USBDeviceSerial: String;
    LinuxDeviceFile: String;
    pieces:TStringList;
Begin
  pieces := TStringList.Create;
  pieces.Delimiter := '-';

  verbose(verbose_debug,'Searching for Linux USB devices ...'); //Troubleshooting information
  verbose(verbose_debug,'Searching here : '+USBDevicePath+'usb-FTDI_*'); //Troubleshooting information

  Count:=0;
  If FindFirst (USBDevicePath+'usb-FTDI_*',faAnyFile ,Info)=0 then
    begin
    Repeat
      Inc(Count);
      With Info do
        begin
        verbose(verbose_debug,'Found this : '+Name); //Troubleshooting information
        pieces.DelimitedText:=Name;
        USBDeviceSerial:=AnsiRightStr(pieces.Strings[pieces.Count-3],8);
        LinuxDeviceFile:=ExpandFileName(USBDevicePath+fpReadLink(USBDevicePath+Name));
        verbose(verbose_debug,'USB device serial number='+USBDeviceSerial);

        // Check if srial number matches selected, or
        //   if no serial number was defined, then just use the last one found
        if (serialnumber=USBDeviceSerial) or (serialnumber='') then begin
          SelectedPort:=LinuxDeviceFile;
          verbose(verbose_debug,'Found a match: '+serialnumber+' : ' + LinuxDeviceFile);
        end;

        verbose(verbose_debug,'Connection='+LinuxDeviceFile);

        end;
    Until FindNext(info)<>0;
    end;
  FindClose(Info);
  verbose(verbose_debug,'Finished Linux USB search. Found '+IntToStr(Count)+' devices');


  //If no matches are found, then try to select if only one device is found.
  if not (SelectedPort='') then //Selected port has been set
      begin
        pieces.Delimiter := ',';
        pieces.DelimitedText := SendGet('ix');
        verbose(verbose_debug,'pieces.DelimitedText='+pieces.DelimitedText);
        SelectedUnitSerialNumber:=IntToStr(StrToIntDef(pieces.Strings[4],0));
        verbose(verbose_debug,'SelectedUnitSerialNumber='+SelectedUnitSerialNumber);
        INISection:='Serial:'+SelectedUnitSerialNumber;
        try
          //TZRegion:= vConfigurations.ReadString(INISection,'Local region');
          TZRegion:= appsettings.vConfigurations.ReadString('','','');//vConfigurations.ReadString(INISection,'Local region');
          ptz.ParseDatabaseFromFile(appsettings.TZDirectory+TZRegion);
          verbose(verbose_debug,'INISection='+INISection);
          TZLocation:=vConfigurations.ReadString(INISection,'Local time zone');
          verbose(verbose_debug,'TZLocation='+TZLocation);
        except begin //Default to GMT since no proper definition could be found
           verbose(verbose_error,'failed tz find');
           TZRegion:= 'etcetera';
           ptz.ParseDatabaseFromFile(appsettings.TZDirectory+TZRegion);
           verbose(verbose_debug,'INISection='+INISection);
           TZLocation:=vConfigurations.ReadString(INISection,'Local time zone');
           if (TZLocation='') then begin
             verbose(verbose_error,'TZLocation checking='+TZLocation);
             TZLocation:='Etc/GMT'
           end;
         end;

        end;
      end
    else
        verbose(verbose_error,'No selected port defined and no device found.');

End;

procedure WriteDLHeader(Setting: String = '' );
//Setting describes how UDM was used to create this logfile.
var
  result, HeaderFirmwareVersion: AnsiString;
  ProtocolNumber,ModelNumber,FeatureNumber,SerialNumber : Integer;
  Info:     TVersionInfo;

begin
    verbose(verbose_action,'*****Writing header...');

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

    LogFileName:=Format(
        '%s%s_%s.dat',
        [appsettings.LogsDirectory + DirectorySeparator,
         FormatDateTime('yyyymmdd"_"hhnnss',Now),
         vConfigurations.ReadString(INISection,'Instrument ID')
         ]);

    verbose(verbose_action,'LogFileName='+LogFileName);

    //Application.ProcessMessages; //why is this here?
    AssignFile(DLRecFile,LogFileName);

    Rewrite(DLRecFile); //Open file for writing

    { Write header }
    SetTextLineEnding(DLRecFile,#13#10);
    Writeln(DLRecFile,'# Light Pollution Monitoring Data Format 1.0');
    Writeln(DLRecFile,'# URL: http://www.darksky.org/measurements');
    Writeln(DLRecFile,'# Number of header lines: 35');
    Writeln(DLRecFile,'# This data is released under the following license: ODbL 1.0 http://opendatacommons.org/licenses/odbl/summary/');
    Writeln(DLRecFile,'# Device type: USB');
    Writeln(DLRecFile,'# Instrument ID: '+vConfigurations.ReadString(INISection,'Instrument ID'));
    Writeln(DLRecFile,'# Data supplier: '+vConfigurations.ReadString(INISection,'Data Supplier'));
    Writeln(DLRecFile,'# Location name: '+vConfigurations.ReadString(INISection,'Location Name'));
    Writeln(DLRecFile,'# Position (lat, lon, elev(m)): '+vConfigurations.ReadString(INISection,'Position'));
    Writeln(DLRecFile,'# Local timezone: '+TZLocation);
    Writeln(DLRecFile,'# Time Synchronization: '+vConfigurations.ReadString(INISection,'Time Synchronization'));
    Writeln(DLRecFile,'# Moving / Stationary position: '+vConfigurations.ReadString(INISection,'Moving Stationary Position'));
    Writeln(DLRecFile,'# Moving / Fixed look direction: '+vConfigurations.ReadString(INISection,'Moving Stationary Direction'));
    Writeln(DLRecFile,'# Number of channels: '+vConfigurations.ReadString(INISection,'Number Of Channels'));
    Writeln(DLRecFile,'# Filters per channel: '+vConfigurations.ReadString(INISection,'Filters Per Channel'));
    Writeln(DLRecFile,'# Measurement direction per channel: '+vConfigurations.ReadString(INISection,'Measurement Direction Per Channel'));
    Writeln(DLRecFile,'# Field of view (degrees): '+vConfigurations.ReadString(INISection,'Field Of View'));
    Writeln(DLRecFile,'# Number of fields per line: 6');
    Writeln(DLRecFile,Format('# SQM serial number: %d',[SerialNumber]));
    Writeln(DLRecFile,'# SQM firmware version: '+HeaderFirmwareVersion);
    Writeln(DLRecFile,'# SQM cover offset value: '+vConfigurations.ReadString(INISection,'CoverOffset'));
    Writeln(DLRecFile,'# SQM readout test ix: '+result);
    Writeln(DLRecFile,'# SQM readout test rx: '+sendget('rx'));
    Writeln(DLRecFile,'# SQM readout test cx: '+sendget('cx'));
    Writeln(DLRecFile,'# Comment: '+vConfigurations.ReadString(INISection,'UserComment1'));
    Writeln(DLRecFile,'# Comment: '+vConfigurations.ReadString(INISection,'UserComment2'));
    Writeln(DLRecFile,'# Comment: '+vConfigurations.ReadString(INISection,'UserComment3'));
    Writeln(DLRecFile,'# Comment: '+vConfigurations.ReadString(INISection,'UserComment4'));
    Writeln(DLRecFile,'# Comment: '+vConfigurations.ReadString(INISection,'UserComment5'));


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

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

    Writeln(DLRecFile,'# blank line 32');
    Writeln(DLRecFile,'# UTC Date & Time, Local Date & Time, Temperature, Counts, Frequency, MSAS');
    Writeln(DLRecFile,'# YYYY-MM-DDTHH:mm:ss.fff;YYYY-MM-DDTHH:mm:ss.fff;Celsius;number;Hz;mag/arcsec^2');
    Writeln(DLRecFile,'# END OF HEADER');
    Flush(DLRecFile);
end;

procedure LogOneReading();
var
  pieces: TStringList;
  Result: string;
  ThisMoment, ThisMomentUTC: TDateTime;
  subfix: ansistring;

const
    WebPageFile = '/tmp/index.html';

  function CheckRecordCount(): boolean;
  begin
    if (pieces.Count = 6) then
      CheckRecordCount := True
    else
    begin
      CheckRecordCount := False;
      verbose(verbose_error,format( 'CheckRecordCount fail: pieces.Count = %d',[pieces.Count]));
    end;
  end;

  procedure WriteRecord(Special: string = '');
  var
    ComposeString: string;
  begin
    ThisMomentUTC := NowUTC;

    vConfigurations.ReadString(INISection,'Local time zone');
    verbose(verbose_debug,'INISection='+INISection);
    if (TZLocation='') then verbose(verbose_error,'TZLocation is empty');
    verbose(verbose_debug,'TZLocation='+TZLocation);
    ThisMoment := ptz.GMTToLocalTime(ThisMomentUTC, TZLocation, subfix);

    //Create new logfile if over 24hr UTC time.
    if (DayOf(LCStartFileTime) <> DayOf(ThisMoment)) then begin
        // Open new file and store header
        WriteDLHeader(Format('Logged continuously %s.', [setting]));
        LCStartFileTime := ThisMoment;
      end
    else
      begin
        AssignFile(DLRecFile, LogFileName);
        Append(DLRecFile); //Open file for appending
        SetTextLineEnding(DLRecFile, #13#10);
      end;

    // Display LogFileNameText path
    verbose(verbose_debug,'LogFileName='+LogFileName);

    if (Special = '') then begin
      { 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')
      ComposeString :=
        FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', ThisMomentUTC) //Date UTC
        //+ FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', Now) //Date Local
        //+ FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', DesiredLogTime)
        + 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
      ;
    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";"', NowUTC) //Date UTC
        + FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz";"', Now) //Date Local
        + Special;
    end;

    verbose(verbose_action,'Logging one reading.');
    Writeln(DLRecFile, ComposeString);

    //Write HTML page to ram disk (/tmp)
     try
       AssignFile(RamStatusFile,WebPageFile);
       Rewrite(RamStatusFile); //Open file for writing

       //save in text file : last reading with all data and timestamp
       Writeln(RamStatusFile, '<html><head><title>UDMC</title>');
       //Writeln(RamStatusFile, '<meta http-equiv="refresh" content="1" />');
       Writeln(RamStatusFile, '</head><body>');
       Writeln(RamStatusFile,'UTC: '+FormatDateTime('yyyy-mm-dd" "hh:nn:ss', ThisMomentUTC)+'<br>'); //Date UTC
       Writeln(RamStatusFile,'Local: '+FormatDateTime('yyyy-mm-dd" "hh:nn:ss', ThisMoment)+'<br>'); //Local time (calculated)
       Writeln(RamStatusFile,'Darkness: '+FormatFloat('#0.00', Darkness, FPointSeparator) +' mag/arcsec&sup2;<br>');
       Writeln(RamStatusFile,'Temperature at light sensor : ' + FormatFloat('##0.0', Temperature, FPointSeparator) +'&deg;C<br>');
       Writeln(RamStatusFile, '</body></html>');
       Flush(RamStatusFile);
       Close(RamStatusFile);
       verbose(verbose_action,'Wrote to '+WebPageFile);

     //****update graph (if feature exists)
     except
       verbose(verbose_error,'Failed ' + WebPageFile + ' write.');
     end;

  end;

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

  //Try a first time
  Result := SendGet('rx');
  pieces.DelimitedText := Result;
  if CheckRecordCount then
    WriteRecord
  else begin //Try a second time
    Result := SendGet('rx');
    pieces.DelimitedText := Result;
    if CheckRecordCount then
      WriteRecord
    else begin //Try a second time
      Result := SendGet('rx');
      pieces.DelimitedText := Result;
      if CheckRecordCount then
        WriteRecord
      else
        WriteRecord(';;;'); //Empty fields
    end;
  end;

  Flush(DLRecFile);
  Close(DLRecFile);
  Recording := False;
end;

procedure FineTimerTimer();
var
  ThisMoment: TDateTime;
begin
  // Only trigger out once per second with ~20ms accuracy.
  // This prevents general drift in recordings.


  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
        //Prevent more recording while already busy saving a record
      begin
        ThisMoment := RecodeMilliSecond(Now, 0);
        //CurrentTime.Caption := FormatDateTime('yyyy-mm-dd hh:nn:ss', ThisMoment);

        // Check if counter has expired while in Seconds or Minutes mode
        if ((LCMode='LCMS') or (LCMode='LCMM')) then begin
          Dec(LogTimeCurrent);
          if (LogTimeCurrent <= 0) then begin

            // Log value//* threshold checking
            LogOneReading;

            // Restart counter for continuous logging
            LogTimeCurrent := LogTimePreset;
          end;
        end;

        if LCMode='LCM' then begin // On-the-clock mode selected
            if (CompareDateTime(ThisMoment, DesiredLogTime) >= 0) then begin
                // Log value//* threshold checking
                LogOneReading;
                OnTheClock(ThisMoment, LCFreq); //Every __ minutes
              end
            else begin //continue counting
                LogTimeCurrent := Round(SecondSpan(ThisMoment, DesiredLogTime));
              end;
        end;

        if StopRecording then begin
          RecordingMode := False;
          LogTimePreset := 0;
          LogTimeCurrent := 0;
        end;
      end; //End of checking Recording flag

      //*** Update reading value to ram file for web access

      //*** Update Chart (when one is created) for web access

    end;

  end;


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 new record time and calculate time until record
  DesiredLogTime := ThisMoment; //Copy current time for manipulation
  DesiredLogTime := IncMinute(DesiredLogTime, Granularity -
    MinuteOf(ThisMoment) mod Granularity);
  //Replace seconds with 0
  DesiredLogTime := RecodeSecond(DesiredLogTime, 0);
  LogTimeCurrent := SecondsBetween(ThisMoment, DesiredLogTime);
end;

procedure testfptimer();
begin
  writeln('test fp timer');
end;

initialization

  ser:=TBlockSerial.Create;
  ptz := TPascalTZ.Create();

  // Format seetings to convert a string to a float
  FPointSeparator := DefaultFormatSettings;
  FPointSeparator.DecimalSeparator := '.';
  FPointSeparator.ThousandSeparator := '#';// disable the thousand separator
  FCommaSeparator := DefaultFormatSettings;
  FCommaSeparator.DecimalSeparator := ',';
  FCommaSeparator.ThousandSeparator := '#';// disable the thousand separator

end.

