#!/usr/bin/perl

# Details:
#Filename: sqmlelp.pl
# Description: Sky Quality Meter-LE Logging and plotting utility
# Takes details from sqmleconf.txt file in same directory.
# The configuration file is read at every reading interval to allow dynamic changes
# Produces log files in subdirectory
# Takes one reading every x minutes (defined below)
# Provides new graph after every reading
# Rotate graph each hour (with current hour showing live readings)
# Append records to text log file with date as name, i.e. 20090121.log


use warnings;
use Cwd;
use IO::Socket;
use DateTime;
use DateTime::TimeZone;
#--------------------------------------
$verbose = 0;
$n_readings=1;
#exit;

@DataArray=();

# ---- Grab command line arguments ----
  foreach (@ARGV) {

     #Print help option
     if(substr($_, 0, 2) eq "-h") {
	printf "\n$0  [options]\n";
	printf "Description: Sky Quality Meter-LE Logging and plotting utility\n";
	printf " -z Print all possible time zones\n";
	printf " -tZONE   Print current time for a specific zone.\n";
	printf " -v Verbose mode prints readings from each site as they come in\n";
	printf " -nNUMBER  number of readings to take. Default 1 reading. 0=endless.\n";
	printf " -wWORKINGDIRECTORY the directory where the configuration and log files are kept.\n";
	printf " -h This help screen\n\n";
	exit;
     }


     #Print all possible time zones
     if(substr($_, 0, 2) eq "-z") {
	foreach $zone (DateTime::TimeZone::all_names) {
		print $zone."\n";
	}
	exit;
     }

     if(substr($_, 0, 2) eq "-n") {
       $n_readings=substr($_, 2);
     }

     if(substr($_, 0, 2) eq "-w") {
	$WorkingDirectory=substr($_, 2);
     }


     #Print current time for testing time zones
     if(substr($_, 0, 2) eq "-t") {
	if (length($_)>2) {
		my $dt = DateTime->now->set_time_zone(substr($_, 2, length($_)-2));
		print $dt."\n";
		exit;
	} else {
		print "Use a valid time zone name. List all time zone names as follows:\n";
		print "$0 -z\n";
		exit;
	}
     }

     #Verbose mode prints readings from each site as they come in
     if(substr($_, 0, 2) eq "-v") {
	$verbose = 1;
     }

  }
#--------------------------------------



# ----- Define constants --------------
$sleep_sec = 60;
$WorkingDirectory="F:\\205-154-251-19\\sqm-le";

# ----- Define variables --------------
$connectionfailurereported=0;

#default Gnuplot executable
$gnuplot="gnuplot";

# Graph y axis settings
$y_lower_limit=-25.0;
$y_upper_limit=-16.0;
sub ylimit($){
	my $value=shift;
	if ($value>$y_upper_limit) {$value=$y_upper_limit;}
	if ($value<$y_lower_limit) {$value=$y_lower_limit;}
	return $value;
}

# Initialize internal error checking
#$yesterday_log_error=0;
$today_log_error=0;

#--------------------------------------
sub log10 {
	my $n = shift;
	return log($n)/log(10);
}

sub trim($) {
	my $string = shift;
	$string =~ s/^\s+//;
	$string =~ s/\s+$//;
	return $string;
}

# Convert mags to NELM
# from http://unihedron.com/projects/darksky/NELM2BCalc.html
# NELM=7.93-5*log(10^(4.316-(Bmpsas/5))+1)
sub NELM($) {
	my $mags = shift;
	return sprintf("%.2f", 7.93-5*log10(10**(4.316-($mags/5))+1));
}

#left trim, covering some edgecase errors popping up now
# By Angela Farrow 2019-08-12
sub ltrim { my $s = shift; $s =~ s/^\s+//;       return $s };

$count=0;
while($count<=$n_readings){

  if ($n_readings>0){
    $count++
  }

#--- Read in configuration file -----------------
open(ConfigFile, "$WorkingDirectory\\sqmleconf.txt")|| die("Cannot open  $WorkingDirectory\\sqmleconf.txt : $!");
@ConfigFileData=<ConfigFile>;
close(ConfigFile);

#--- Parse configuration file -------------------
#Create an empty Hash
%sites=(); 
foreach $Line_read (@ConfigFileData) {
	if ( (substr($Line_read,0,1) ne "#") and (length(trim($Line_read)) ge 1 )) {	# Process uncommented lines only
		@elements=split(' ',$Line_read);
		if (lc($elements[0]) eq "host"){
			$HostName = $elements[1];
		} else {
			$sites{$HostName}{lc($elements[0])} = $elements[1];
		}
	}
}

#Print parsed data
#for $site ( keys %sites ) {
#    print "$site: ";
#    for $item ( keys %{ $sites{$site} } ) {
#         print "$item=$sites{$site}{$item} ";
#    }
#    print "\n";
#}

for $site ( keys %sites ) {
  $SQM_LE_Name = $site;
  $SQM_LE_Address = $sites{$site}{ip};
  if (exists $sites{$site}{port}) {$SQM_LE_Port=$sites{$site}{port};} else {$SQM_LE_Port=10001;}
  $SQM_LE_Timezone = $sites{$site}{tz};
  if (exists $sites{$site}{x}) {$Xsize=$sites{$site}{x};} else {$Xsize=318;}
  if (exists $sites{$site}{y}) {$Ysize=$sites{$site}{y};} else {$Ysize=183;}
  if (exists $sites{$site}{units}) {$Units=$sites{$site}{units};} else {$Units="mags";}
  if (exists $sites{$site}{zero}) {$zero=$sites{$site}{zero};} else {$zero="top";}
  #--- Check if reversed plot is required, then swap limits ------
  if ($zero eq "bottom"){
    ($y_lower_limit, $y_upper_limit) = ($y_upper_limit, $y_lower_limit);
    $y_lower_limit=$y_lower_limit*-1;
    $y_upper_limit=$y_upper_limit*-1;
  }
  #---------------------------------------------------------------
  if (exists $sites{$site}{linecolor}) {$linecolor=$sites{$site}{linecolor};} else {$linecolor=ff0000;}
  if (exists $sites{$site}{plottitle}) {$plottitle=$sites{$site}{plottitle};} else {$plottitle=No;}
  if (exists $sites{$site}{timescale}) {$timescale=$sites{$site}{timescale};} else {$timescale=24;}
  if (exists $sites{$site}{transparent}) {$transparent=$sites{$site}{transparent};} else {$transparent=Yes;}
  if (exists $sites{$site}{csf}) {$CSF=1} else {$CSF=0}

  #Gather and check FTP values
  if (exists $sites{$site}{ftpsite}) {$ftpsite=$sites{$site}{ftpsite};}
  if (exists $sites{$site}{ftpusername}) {$ftpusername=$sites{$site}{ftpusername};}
  if (exists $sites{$site}{ftppassword}) {$ftppassword=$sites{$site}{ftppassword};}
  if (exists $sites{$site}{ftpsitedirectory}) {$ftpsitedirectory=$sites{$site}{ftpsitedirectory};}
  if (exists $sites{$site}{ftplcd}) {$ftplcd=$sites{$site}{ftplcd};}
  $ftpexists=($ftpsite and $ftpusername and $ftppassword and $ftpsitedirectory and $ftplcd);

  #Gather and check scp values
  if (exists $sites{$site}{scplocation}) {$scplocation=$sites{$site}{scplocation};} else {$scplocation=undef;}
  if (exists $sites{$site}{scpport}) {$scpport=$sites{$site}{scpport};} else {$scpport=undef;}
  $scpexists=($scplocation and $scpport);


  #-- Initialize response string
  $str=" ";

  # open socket
  $connectionfailed = 0;
  $remote = IO::Socket::INET->new(PeerAddr => $SQM_LE_Address,
 	                        PeerPort => $SQM_LE_Port,
                                Proto    => 'tcp') 
                               or $connectionfailed = 1;

  if ($connectionfailed == 0){

	if ($connectionfailurereported==1){
		($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
		printf ("%04d-%02d-%02d %02d:%02d:%02d ",$year+1900,$mon+1,$mday,$hour,$min,$sec);
		print "Connection recovered.\n";
		$connectionfailurereported=0;
	}

  # Flush buffer after each print or write
  $remote->autoflush(1);

  #-- Send request to remote SQM
  print $remote "rx";

    #Encapsulate in eval so that timeout can interrupt
    eval {
        local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required
        #Set timeout in seconds
        alarm 2;

	#-- Add response to string until end of line
	while($str !~ /\n/){
		$str .= <$remote>;
	}
        alarm 0;
    };
	$str = ltrim($str);
    if ($@) {
        die unless $@ eq "alarm\n";   # propagate unexpected errors
	if ($connectionfailurereported==0){
		($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
		$logfile =sprintf("%04d%02d%02d.log",$year+1900,$mon+1,$mday);
		printf ("%04d-%02d-%02d %02d:%02d:%02d ",$year+1900,$mon+1,$mday,$hour,$min,$sec);
		print "Cannot connect to: $SQM_LE_Name , port=$SQM_LE_Port address=$SQM_LE_Address . ";
		print "Will try later.\n";
		$connectionfailurereported=1;
	}
    }
    else {
        # didn't timeout


  #-- If newline, print string and start next count
  if ($str =~ /\n/) {
	#The string looks something like this, second and third lines are just a ruler:
	#  r,-02.21m,0000000007Hz,0000059399c,0000000.129s, 037.0C
	#            1         2         3         4         5
	#  0123456789 123456789 123456789 123456789 123456789 1234

	$mpsas=substr($str,2,6);
	#$frequency=substr($str,10,10);
	#$counts=substr($str,23,10);
	#$period=substr($str,35,11);
	$temperature=substr($str,48,6);

	#Get the current timestamp now for this site
	my $dt = DateTime->now->set_time_zone($SQM_LE_Timezone);

	#Make directory to store logfiles if it does not exist.
	if (!-e "$WorkingDirectory/$SQM_LE_Name") {
		mkdir "$WorkingDirectory/$SQM_LE_Name";
	}

	$logfile=sprintf("%s/%s/%04d%02d%02d.log",$WorkingDirectory,$SQM_LE_Name,$dt->year,$dt->month,$dt->day);
	open (LOGFILE , ">>".$logfile)|| die("Cannot open $logfile : $!");

	printf LOGFILE  "%02d:%02d:%02d %sm %sC\n",$dt->hour,$dt->min,$dt->second,$mpsas,$temperature;

	if ($verbose==1){
		printf ("%20s %04d-%02d-%02d ",$SQM_LE_Name,$dt->year,$dt->month+1,$dt->day);
		printf "%02d:%02d:%02d %s",$dt->hour,$dt->min,$dt->second,$str;
	}

	close LOGFILE;
	
	$logfile=sprintf("%s/%s/now.txt",$WorkingDirectory,$SQM_LE_Name);
	open (LOGFILE , ">".$logfile)|| die("Cannot open $logfile : $!");

	printf LOGFILE  "%s\n",$mpsas;

	close LOGFILE;
	
	

	#-- Close remote socket
	$remote->close;

	#Clear out previous data points
	open(DATAFILE,">","$WorkingDirectory/tmpdata.txt") or die "Failed to create temporary data file.";
	print DATAFILE "";
	close(DATAFILE);
	@DataArray=();

	#Determine yesterday (Previous date)
	$pdt = $dt->clone();
	$pdt->subtract( days => 1 );

	#Reset the plot point counter, used to determine if a plot is required.
	$plotcount=0;

	#Reset best reading of the day
	$max_value=0;

	#read in yesterdays logfile
	$logfile=sprintf("%s/%04d%02d%02d.log",$SQM_LE_Name,$pdt->year,$pdt->month,$pdt->day);
	$fileopenedfailed=0;
	open(LOGFILE, "$WorkingDirectory/$logfile") or $fileopenedfailed=1;
	if ($fileopenedfailed==0){
		@raw_data=<LOGFILE>;
		close(LOGFILE);
		open(DATAFILE,">","$WorkingDirectory/tmpdata.txt") or die "Failed to create temporary data file.";
		open(DATACFILE,">","$WorkingDirectory/tmpcdata.txt") or die "Failed to create temporary datac file.";
		$max_value_time="0";
		foreach $LINE_VAR (@raw_data) {
			#Only gather data from past 24 hours for plotting 
			($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
			if (substr($LINE_VAR,0,2)>=$hour+1){
				$plotcount++;
				# Get x value
				$timestring=substr($LINE_VAR,0,8);
				$timepoint=substr($LINE_VAR,0,2)+substr($LINE_VAR,3,2)/60.0+substr($LINE_VAR,6,2)/3600.0-24;
				# Get y value
				$raw_y_value=substr($LINE_VAR,8,7);
				if ($raw_y_value>$max_value){$max_value=$raw_y_value;$max_value_time=$timestring}
				if ($zero eq "bottom"){
					$y_value=$raw_y_value;
				}else{
					$y_value=$raw_y_value*-1;
				}
				#Apply limits to plotted line
				$y_value=ylimit($y_value);
				if ($Units eq "NELM"){$y_value=NELM($raw_y_value);}
				$ArraySize = push (@DataArray,$raw_y_value);
				print DATAFILE "$timepoint $y_value\n";
				cfcalc();
			}
		}
		close(DATAFILE);
		close(DATACFILE);
	}

	#read in todays logfile
	$logfile=sprintf("%s/%04d%02d%02d.log",$SQM_LE_Name,$dt->year,$dt->month,$dt->day);
	$fileopenedfailed=0;
	open(LOGFILE, "$WorkingDirectory/$logfile") or $fileopenedfailed=1;
	if ($fileopenedfailed==0){
		$today_log_error=0;
		@raw_data=<LOGFILE>;
		close(LOGFILE);
		open(DATAFILE,">>","$WorkingDirectory/tmpdata.txt") or die "Failed to create temporary data file.";
		open(DATACFILE,">>","$WorkingDirectory/tmpcdata.txt") or die "Failed to create temporary datac file.";
		$max_value_time="0";
		foreach $LINE_VAR (@raw_data) {
			$plotcount++;
			# Get x value
			$timestring=substr($LINE_VAR,0,8);
			$timepoint=substr($LINE_VAR,0,2)+substr($LINE_VAR,3,2)/60.0+substr($LINE_VAR,6,2)/3600.0;
			# Get y value
			$raw_y_value=substr($LINE_VAR,8,7);
			if ($raw_y_value>$max_value){$max_value=$raw_y_value;$max_value_time=$timestring}

			if ($zero eq "bottom"){
				$y_value=$raw_y_value;
			}else{
				$y_value=$raw_y_value*-1;
			}
			#Apply limits to plotted line
			$y_value=ylimit($y_value);
			if ($Units eq "NELM"){$y_value=NELM($raw_y_value);}
			$ArraySize = push (@DataArray,$raw_y_value);
			print DATAFILE "$timepoint $y_value\n";
			cfcalc();
		}
		close(DATAFILE);
		close(DATACFILE);
	}
	# Prepare graph title showing days covered in graph
	$yesterday = sprintf("%04d-%02d-%02d",$pdt->year,$pdt->month,$pdt->day);
	$today = sprintf("%04d-%02d-%02d",$dt->year,$dt->month,$dt->day);

	#Only plot if there is some data to be plotted
	if ($plotcount>0) {

	#Clear Sky Factor calculation
	sub cfcalc{
		if (@DataArray>9) {
			$CSDValue = 0;
			$LastCSDValue = $DataArray[0];
			for ($i=1;$i<10;$i++){
				$CurrentCSDValue = $DataArray[$i];
				#Only compute when getting lighter (lower number)
				if ($CurrentCSDValue < $LastCSDValue) {
					$CSDValue = $CSDValue + abs($CurrentCSDValue - $LastCSDValue);
				}
				$LastCSDValue = $CurrentCSDValue;
			}
			shift(@DataArray);#get rid of oldest value
			$y2_value = sprintf ("%0.2f",$CSDValue * 2.0);
			print DATACFILE "$timepoint $y2_value\n";
		} else {
			print DATACFILE "$timepoint 10\n"; #indicate not enough readings to determine clear sky
		}
	}


	#Time Of Day label correction
	sub todlabel{
		# Get time of day parameter
		my $xlval=shift;
		# Correct for negative time labels
		if ($xlval<0) {$xlval=$xlval+24}
		if ($timescale == 24) {
			return $xlval."h";
		} else {
			# Correct for AM/PM
			$m="a"; # default
			if ($xlval==12) {$m="p"}
			if ($xlval==0) {$m="a";$xlval=12;}
			if ($xlval>12) {$m="p";$xlval=$xlval-12;}
			#Return corrected time of day suitable for plot x axis label
			#return $xlval. ":00".$m;
			return $xlval.$m;
		}
	}

	$xvalstr='';
	for ($xlval = $dt->hour-23; $xlval < $dt->hour+1; $xlval++){
		#Time of day, major tics
		$xvalstr=$xvalstr . " '" . todlabel($xlval). "' " . $xlval. " 0,";
		#Empty labels, minor tics
		for ($i=1; $i < 4; $i++){
			$xlval++;
			$xvalstr=$xvalstr . "' ' " . $xlval . " 1,";
		}
	}
	# Current hour is last label
	$endhour=$dt->hour+1;
	$xvalstr=$xvalstr . " '" . todlabel($endhour% 24) . "' " . $endhour. " 0";


	#Create temporary plot file with properties of the chart
	open(PLOTFILE,">","$WorkingDirectory/tmpplot.txt") or die "Failed to create temporary plot file";
	print PLOTFILE "set output '$WorkingDirectory/$SQM_LE_Name/sqmleg.gif'\n";
	if ($transparent eq "No") {
		print PLOTFILE "set terminal gif size $Xsize,$Ysize".' font "regular,9" '."\n";
	} else {
		print PLOTFILE "set terminal gif transparent size $Xsize,$Ysize".' font "regular,9" '."\n";
	}
	$xrangemin=$dt->hour-23;
	$xrangemax=$dt->hour+1;
	print PLOTFILE "set xrange [$xrangemin:$xrangemax]\n";

	if ($Units eq "NELM") {
		print PLOTFILE "set ylabel 'NELM' noenhanced textcolor rgb \"#$linecolor\" \n";
		print PLOTFILE "set yrange [0:10]\n";
	} else {
		print PLOTFILE "set ylabel 'mpsas' noenhanced textcolor rgb \"#$linecolor\" \n";
		print PLOTFILE "set yrange [$y_lower_limit:$y_upper_limit]\n";
	}

	#Print Clear Sky Factor label if enabled
	if ($CSF){
		print PLOTFILE "set y2label 'Clear Sky Factor' noenhanced textcolor rgb \"#007f00\" \n";
		print PLOTFILE "set y2range [0:3]\n";
	}

	print PLOTFILE "set border 11\n"; #sum of border lines; bottom=1, left=2, top=4, and right=8
	if ($^O eq "MSWin32"){
		print PLOTFILE "set lmargin 10.5\n";
		print PLOTFILE "set rmargin 1.8\n";
	}

	$Title='set title "';
	if ($plottitle eq "Yes") {$Title.='Mount Laguna Observatory - Current Sky Quality Meter Chart \n ';}
	$Title .= "$yesterday - $today";
	#$Title .= ' font \"Helvetica,10\" ';

	#Print current reading in title	
	if ($Units eq "NELM"){
		$Title .= ' \n Current reading:'.NELM($raw_y_value).' NELM \n';
		$Title .= 'Best in 24hr:'.NELM($max_value).' NELM @ '.$max_value_time.'"'."\n";
	} else {
		$Title .= ' \n Current reading:'.$raw_y_value.'m  @ '.$timestring.' ';
		if ($CSF){
			$Title .= $y2_value.'csf'; #Clear Sky Factor
		}
		$Title .= ' \n Best in 24hr:'.$max_value.'m @ '.$max_value_time.'"'."\n";
	}

#	print PLOTFILE "set title ".$Title." noenhanced\n";
#	print PLOTFILE "set title '$yesterday - $today' font ',30'\n";
	print PLOTFILE $Title;

	printf PLOTFILE "set xtics nomirror out scale 3,1 (%s)\n",$xvalstr;
	print PLOTFILE "set grid mxtics mytics xtics ytics\n";

	if ($Units eq "NELM"){
		print PLOTFILE "set ytics nomirror out scale 3,1 ('2' 2 0,' ' 3 1, '4' 4 0,' ' 5 1,'6' 6 0,' ' 7 1,'8' 8 0,' ' 9 1,'10' 10 0)\n";
	} elsif ($zero eq "bottom"){
		print PLOTFILE "set ytics nomirror out scale 3,1 ('14' 14 0,' ' 15 1, '16' 16 0,' ' 17 1,'18' 18 0,' ' 19 1,'20' 20 0,' ' 21 1,'";
		if ($CSF){
			print PLOTFILE "(Dark) ";
		}
		print PLOTFILE "22' 22 0)\n";
	}else{
		print PLOTFILE "set ytics nomirror out scale 3,1 ('14' -14 0,' ' -15 1, '16' -16 0,' ' -17 1,'18' -18 0,' ' -19 1,'20' -20 0,' ' -21 1,'22' -22 0,' ' -23 1,'24' -24 0,' ' -25 1,'";
		if ($CSF){
			print PLOTFILE "(Dark) ";
		}
		print PLOTFILE "22' -22 0)\n";
	}

#	print PLOTFILE "set y2tics nomirror out scale 3,1 ('1' 1 0,'2' 2 0)\n";
	print PLOTFILE "set y2tics nomirror out scale 3,1 ('0 (Clear)' 0 0,'1' 1 0,'2' 2 0,'3' 3 0)\n";

	#Plot Clear Sky Factor data set if enabled
	if ($CSF){
		print PLOTFILE "plot '$WorkingDirectory/tmpdata.txt' title '' with lines linecolor rgb \"#$linecolor\" , '$WorkingDirectory/tmpcdata.txt' axes x1y2 title ''  with lines linecolor rgb \"#00007f\"\n" ;
	} else {
		print PLOTFILE "plot '$WorkingDirectory/tmpdata.txt' title '' with lines linecolor rgb \"#$linecolor\" \n" ;
	}


	close(PLOTFILE);

	#Set the gnuplot executable location, be specific if it is not in the PATH.
	$gnuplot="gnuplot";

	# Plot the data set on the chart
	system("$gnuplot $WorkingDirectory/tmpplot.txt");

	#Copy plot to another computer using scp
	if ($scpexists) {
		system "scp -P ".$scpport." $WorkingDirectory/$SQM_LE_Name/sqmleg.gif ".$scplocation." > null";
	}

	#Copy plot to an ftp server using FTP
	if ($ftpexists) {
		open(FTPFILE,">","$WorkingDirectory/tmpftp.txt") or die "Failed to create temporary ftp file";
		print FTPFILE "open $ftpsite\n";
		print FTPFILE "$ftpusername\n";
		print FTPFILE "$ftppassword\n";
		print FTPFILE "bin\n";
		print FTPFILE "hash\n";
		print FTPFILE "lcd $WorkingDirectory/$ftplcd\n";
		print FTPFILE "cd $ftpsitedirectory\n";
		print FTPFILE "put $WorkingDirectory/sqmleg.gif\n";
		print FTPFILE "quit\n";
		close (FTPFILE);
		system "ftp -i -s:tmpftp.txt > null";
	}

	}
	#------------------------


  }#end logging and plotting of reading from a site

 } #End of timeout check
}
}

	#Is this a once only reading, or multiple readings required
	if ($n_readings==1) {
		exit 0;
	} else {
		if ($verbose==1){print " --- sleeping ---\n";}
		sleep($sleep_sec);
	}

}# end of while "forever"

