#!/usr/bin/perl -w
# $Id: iV_curve.pl,v 1.41 2016/03/08 07:51:41 sqko Exp $
use strict;
use warnings;
use RFC::Rig;
use RFC::Visitor;
use RFC::Main;
use RFC::Header;
use Time::HiRes qw/sleep/;
use File::Copy;
use Fcntl qw/:DEFAULT :flock/;
use Graphics::GnuplotIF qw(GnuplotIF);
use sigtrap qw(die INT QUIT);
use sigtrap qw(die untrapped normal-signals stack-trace any error-signals);
$SIG{INT} = \&my_sigint;
my $visitor = RFC::DisableProglogVisitor->new();
my $rig = RFC::Rig->new(shift);
$rig->check_run;
my $plot = 0;
my $debug = 0;
for my $string (@ARGV)
{
    if ($string =~ /--debug/)
    {
        $debug = 1;
    }
}


######## Check if running iv-curve         #####
######## and lock if no is already running #####
unless ($rig->iv_start())
{
    print "iv in progress, exiting\n";
    exit;
}
my $dat = `date +"%Y:%m:%d"`;
$dat =~ /([\d\:]*)/;
$dat = $1;
my $test = $rig->session;
my $localdatadir = '/home/rig'.$rig->name.'/'.$rig->name.'test'.$test;
if ( !( -d $localdatadir ) ) 
{
    system("/bin/mkdir $localdatadir"); 
}

#### Setup gnuplot display ###
my $gpid = undef;
$gpid = Graphics::GnuplotIF->new( style=>'points') if (@ARGV);
&plot();

print "Loading configuration...\n" if ($debug);
my $pause = $rig->get_cv('IV_control','ivpause');
$pause = 5 unless ($pause);
my $pvh = $rig->get_cv('IV_control','OCV_measure_number');
$pvh = 1 unless (defined($pvh) && $pvh > 1);
my $i_min = $rig->get_cv('IV_control','epsilon');
$i_min = 0.1 unless ($i_min);
my $logcmd = get_cv('global','logfile').' '.$rig->name.' ignore';
my $area = $rig->get_cv('main','cell_area');
my $emergency = get_cv('iVcurves','minimum_cell_voltage_for_iv');
$emergency = 300 unless ($emergency); ## Hardcoded 300 mV limit for iVcurves ###
my $emergency2 = get_cv('iVcurves','maximum_cell_voltage_for_iv');
$emergency2 = 1700 unless ($emergency2); ## Hardcoded 1700 mV limit for iVcurves ###


my @tags = ('temp_label','current_label','voltage_label','voltagelimit_iv','currentstep1','currentstep2','currentstep3','epsilon','currentlimit','diff_current_limit','electrolysis_limit_voltage');

my %PSU = ();
&load_default;
$PSU{'default'}{'dev'}->Accept($visitor);

my $str = $rig->get_cv('IV_control','iv_control_names');
if ($str)
{
    foreach my $psu (split(/\s*\,\s*/,$str))
    {
	$PSU{$psu} = &clonehash($PSU{'default'});
	$PSU{$psu}{'dev'} = $rig->get_device('PSU',$psu);
	$PSU{$psu}{'dev'}->Accept($visitor);
	foreach my $t (@tags)
	{
	    my $val = $rig->get_cv('IV_'.$psu,$t);
	    next unless defined($val);
	    $PSU{$psu}{$t} = $val;
	}
    }
    ## delete the 'default' PSU ##
    delete $PSU{'default'};
}


foreach my $psu (keys %PSU)
{
    $PSU{$psu}{'dev'}->current('ocv','dummy','nolog');
}
open IVTIME, '>>', $localdatadir.'/ivcurvetimes';

sleep(1);
print "Running OCV measurements before i-V curve...\n" if ($debug);
my %c = &kanaler();
print IVTIME $c{'time'}, " ";
for ( my $extraocv = 0 ; $extraocv < $pvh ; $extraocv++ ) 
{
    sleep(5);
    %c = &kanaler();
    last if (!$rig->is_iv);
}
$rig->allow_cache(1);
foreach my $psu (keys %PSU)
{
    $PSU{$psu}{'ocvvoltage'} = $c{$PSU{$psu}{'voltage_label'}};
    $PSU{$psu}{'strom'} = 0;
    $PSU{$psu}{'c'} = $c{$PSU{$psu}{'current_label'}};
    $PSU{$psu}{'v'} = $c{$PSU{$psu}{'voltage_label'}};
    $PSU{$psu}{'stromstep'} = $PSU{$psu}{'currentstep3'};
    $PSU{$psu}{'updown'} = 1;
}

while (!&is_stop && $rig->is_iv)
{
    %c = &kanaler();
    foreach my $p (keys %PSU)
    {
	my %psu = %{$PSU{$p}};
	$psu{'v'} = $c{$psu{'voltage_label'}};
	$psu{'c'} = $c{$psu{'current_label'}};
	next if ($psu{'stop'});
	$psu{'dev'}->current($psu{'strom'},'dummy','nolog');
	if ($psu{'cont'} && abs($psu{'c'}) > 5 * $i_min)
	{
	    $psu{'emode'} = 1 if ($psu{'c'} < 0);
	    $psu{'cont'} = 0; ## Check only once ##
	}
	my $v = $psu{'v'};
	my $c1 = $psu{'currentstep1'};
	my $c2 = $psu{'currentstep2'};
	my $c3 = $psu{'currentstep3'};
	my $elimit = $psu{'electrolysis_limit_voltage'};
	my $vlimit = $psu{'voltagelimit_iv'};
	my $stromstep = $psu{'stromstep'};
	### Check for emergencies ###
	if (!$psu{'emode'} &&  $v > $elimit)
	{
	    #### Fuel cell mode ####
	    ### Bail if i-V curve is run in reverse (likely switched wires) ###
	    $psu{'stop'} = 1;
	    $psu{'dev'}->current('ocv','dummy','nolog');
	    my $string  = "Electrolysis limit ($elimit mV) reached for PSU $p, exiting fuel cell iV curve hard! (measured value $v), CHECK WIRES!\n";
	    $rig->proglog($string);
	    print $string;
	}
	if ($psu{'emode'} &&  $v < $vlimit)
	{
	    #### Electrolyser mode ###
	    ### Bail if i-V curve is run in reverse (likely switched wires) ###
	    $psu{'stop'} = 1;
	    $psu{'dev'}->current('ocv','dummy','nolog');
	    my $string  = "Fuel cell limit ($vlimit mV) reached for PSU $p exiting electrolyser iV curve hard! (measured value $v), CHECK WIRES\n";
	    $rig->proglog($string);
	    print $string;
	}
	if ($v < $emergency || $v > $emergency2){
	    ### Bail if cell voltage suddenly drops too low or far too high ###
	    $psu{'stop'} = 1;
	    $psu{'dev'}->current('ocv','dummy','nolog');
	    my $string  = "Minimum or maximum cell voltage limit ($emergency and $emergency2 mV respectively) exceeded exiting iV curve hard! (measured value $v)\n";
	    $rig->proglog($string);
	    print $string;
	}
	my $diffcurrent = $psu{'strom'} - abs($psu{'c'});
	if (( $diffcurrent > $psu{'diff_current_limit'} ) && ($psu{'diffcurrent_set'})){
	    ### Bail if current does not match expected value (likely due to undervoltage cutoff or bad conections) ##
	    $psu{'dev'}->current('ocv','dummy','nolog');
	    $psu{'stop'} = 1;
	    my $cc = abs($psu{'c'});
	    my $string =  "Current does not match expected value: measure: $cc. Expected: ".$psu{'strom'}.". iV curve terminated.\n";
	    $rig->proglog($string);
	    print $string;
	}
	### Check for current limit ###
	if ($psu{'updown'} > 0 && abs($psu{'strom'}) > $psu{'currentlimit'})
	{
	    $psu{'updown'} = -1;
	    $stromstep = $c2;    
	}
	### Check for current steps ###
	if ($psu{'updown'} > 0)
	{
	    if ($psu{'emode'})
	    {
		#### Electrolysis mode ####
		if ($v > $elimit)
		{
		    $psu{'updown'} = -1;
		    $stromstep = $c2;
		}
		$stromstep = $c2 unless ( $psu{'strom'} < 1 );
		$stromstep = $c1 if ( $v > $elimit - 100 ); 
		$stromstep = $c3 if ( $v > $elimit - 25 ); 
	    }
	    else
	    {
		#### Fuel cell mode ####
		if ($v < $vlimit)
		{
		    $psu{'updown'} = -1; 
		    $stromstep = $c2;
		}
		$stromstep = $c2 unless ( $psu{'strom'} < 1 );
		$stromstep = $c1 if ( $v < $vlimit + 100 );
		$stromstep = $c3 if ( $v < $vlimit + 25 ); 
	    }
	}
	else
	{
	    $stromstep = $c2;
	    $stromstep = $c1 if ( abs($psu{'strom'}) < 1 + $i_min );
	}
	$psu{'stromstep'} = $stromstep;
	#### Restore hash content in data structure ###
	$PSU{$p} = \%psu;
    }
    open( IVCURVE, '>>', $localdatadir.'/ivcurve.'.$dat);
    foreach my $p (keys %PSU)
    {
	my %psu = %{$PSU{$p}};
	$psu{'v'} = $c{$psu{'voltage_label'}};
	$psu{'c'} = $c{$psu{'current_label'}};
	$c{$psu{'temp_label'}} = 0 unless defined($c{$psu{'temp_label'}});
	my $slope = 0;
	if ( abs($c{$psu{'current_label'}}) > $i_min ) {
	    $slope = ( $psu{'ocvvoltage'} - $psu{'v'} ) * $area / (1000 * $c{$psu{'current_label'}});
	}
	printf("%s  %6.6f  %6.6f %10.1f %10.3f  %10.1f \n", $p,$psu{'strom'}, $c{$psu{'current_label'}},$psu{'v'},$slope,$c{$psu{'temp_label'}}) if ($debug);
	printf IVCURVE "%s   %6.6f  %6.6f %10.1f %10.3f  %10.1f   ", $p,$psu{'strom'},$c{$psu{'current_label'}},$psu{'v'},$slope,$c{$psu{'temp_label'}};
	$psu{'strom'} += $psu{'updown'} * $psu{'stromstep'} unless ($psu{'stop'});
	$psu{'strom'} = sprintf("%.3g",$psu{'strom'}); ## Format to .xxx to awoid parsing zero errors.. ##
	if ($psu{'strom'} < 0 ) 
	{	
	    $psu{'strom'} = 0;
	    $psu{'stop'} = 1;
	    $psu{'dev'}->current('ocv','dummy','nolog');
	}
	my $elimit = $psu{'electrolysis_limit_voltage'};
	my $vlimit = $psu{'voltagelimit_iv'};
	if (!$psu{'stop'} && ( ( ( $psu{'v'} > $vlimit ) && ( $psu{'v'} < $elimit) && ( $psu{'strom'} < $psu{'currentlimit'} ) ) || ( $psu{'updown'} != 1 ) ) )
	{
	    $psu{'dev'}->current($psu{'strom'},'dummy','nolog');
	    $psu{'diffcurrent_set'} = 1;
	}
	else
	{
	    $psu{'diffcurrent_set'} = 0;
	}
	#### Restore hash content in data structure ###
	$PSU{$p} = \%psu;
    }
    print IVCURVE "\n";
    close(IVCURVE);
    &plot();
    sleep($pause);
}

### Do one last measurement and plot before quit ###
$rig->allow_cache(0);
%c = &kanaler();
open( IVCURVE, '>>', $localdatadir.'/ivcurve.'.$dat);
foreach my $p (keys %PSU)
{
    my %psu = %{$PSU{$p}};
    $psu{'v'} = $c{$psu{'voltage_label'}};
    $psu{'c'} = $c{$psu{'current_label'}};
    my $slope = 0;
    if ( abs($c{$psu{'current_label'}}) > $i_min ) {
	$slope = ( $psu{'ocvvoltage'} - $psu{'v'} ) * $area / (1000 * $c{$psu{'current_label'}});
    }
    $c{$psu{'temp_label'}} = 0 unless defined($c{$psu{'temp_label'}});
    printf("%s  %6.6f  %6.6f %10.1f %10.3f  %10.1f \n", $p,$psu{'strom'}, $c{$psu{'current_label'}},$psu{'v'},$slope,$c{$psu{'temp_label'}})  if ($debug);
    printf IVCURVE "%s   %6.6f  %6.6f %10.1f %10.3f  %10.1f   ", $p,$psu{'strom'},$c{$psu{'current_label'}},$psu{'v'},$slope,$c{$psu{'temp_label'}};
}
print IVCURVE "\n";
close(IVCURVE);
&plot();


print "Running OCV measurements after i-V curve...\n" if ($debug);
foreach my $psu (keys %PSU)
{
    $PSU{$psu}{'dev'}->current('ocv','dummy','nolog');
}
for ( my $extraocv = 0 ; $extraocv < $pvh ; $extraocv++ ) 
{
    last if (!$rig->is_iv);
    sleep(5);
    &kanaler();
}
sleep(5);
%c = &kanaler();
print IVTIME " ", $c{'time'}, "\n";
close IVTIME;
open IVCURVE, '>>', $localdatadir.'/ivcurve.'.$dat;
print IVCURVE "\n";
close IVCURVE;
$rig->iv_stop;
$rig->DESTROY();
exit;



sub kanaler()
{
    my %ch = ();
    my @t = localtime(time);
    $t[5] += 1900;
    $t[4]++;
    my $res;
    $ch{'time'} = sprintf("%04d:%02d:%02d:%02d:%02d:%02d",$t[5],$t[4],$t[3],$t[2],$t[1],$t[0]);
    if ($rig->allow_cache)
    {
	$res = `$logcmd cache`;
    }
    else
    {
	$res = `$logcmd`;	
    }
    my @l = split(/[\s\t]+/,$res);
    for (my $x = 0; $x < @l;$x+= 4)
    {
	$ch{$l[$x]} = $l[$x+3];
    }
    if ($l[1] =~ /([\d\:]+)/)
    {
	$ch{'time'} = $1;
    }
    ## Add data for simplechannels not listed by logcommand (typically tyecast evices)
    foreach my $p (keys %PSU)
    {
	my %psu = %{$PSU{$p}};
	foreach my $label ('voltage_label','current_label')
	{
	    next if exists($ch{$psu{$label}});
	    my $dev = $rig->get_device('simplechannel',$psu{$label});
	    $ch{$psu{$label}} = $dev->read() if ($dev);
	}
    }
    return %ch;
}


sub my_sigint
{
    ### Exits gracefully when catching a sigint (kill -2)!! ###
    $SIG{INT} = 'IGNORE';  ## Ignores further SIGINT ###
    foreach my $psu (keys %PSU)
    {
        ### Try to set OCV condition ###
	$rig->current('ocv',$PSU{$psu}{'control'});
    }
    $rig->iv_stop;
    $rig->proglog("iV_curve.pl caught SIGINT, exiting!!!\n");
    die "iV-curve was terminated by SIGINT!\n";
}


sub clonehash($)
{
    my $ref = shift;
    die "Rerference $ref is not a hash ref!" unless (ref($ref) eq 'HASH');
    my %h = %{$ref};
    my %hash = ();
    foreach (keys %h)
    {
	$hash{$_} = $h{$_};
    }
    return \%hash;
}


sub is_stop()
{
    my $stop = 0;
    foreach my $psu (keys %PSU)
    {
	$stop = $PSU{$psu}{'stop'} if ($PSU{$psu}{'stop'});
    }
    return $stop;
}

sub load_default()
{
    my $currentlabel = $rig->get_cv('IV_control','current_label');
    my $voltlabel = $rig->get_cv('IV_control','voltage_label');
    $currentlabel = 'current' unless ($currentlabel); ## Default value for backwards compatibility ###
    $voltlabel = 'cell_voltage' unless ($voltlabel);  ## Default value for backwards compatibility ###
    my $templabel = $rig->get_cv('IV_control','temp_label');
    $templabel = 'T_center' unless ($templabel); ## Default value for backwards compatibility ###
    my $c1 = $rig->get_cv('IV_control','currentstep1');
    my $c2 = $rig->get_cv('IV_control','currentstep2');
    my $c3 = $rig->get_cv('IV_control','currentstep3');
    my $vlimit = $rig->get_cv('IV_control','voltagelimit_iv');
    $c1 = 0.1 unless ($c1);
    $c2 = 0.2 unless ($c2);
    $c3 = 0.05 unless ($c3);
    $vlimit = 450 unless ($vlimit); ## Normal 450 mV limit for iVcurves ###
    my $currentlimit = $rig->get_cv('IV_control','currentlimit');
    my $diffcurrentlimit = $rig->get_cv('IV_control','diff_current_limit');
    $currentlimit = 10 unless ($currentlimit);
    $diffcurrentlimit = 0.5 unless ($diffcurrentlimit);
    my $elimit = $rig->get_cv('IV_control','electrolysis_limit_voltage');
    $elimit = 1200 unless ($elimit); ### Hardcoded limit for eleclysis ###
    my $emode = 0; ## Default normal iV curve.. 1 for electrolysis iV curve ###
    my @psus = $rig->list_devices_control('PSU');
    $PSU{'default'} = {
	'control' => $psus[0],
	'dev' => $rig->get_device('PSU',$psus[0]),
	'currentstep1' => $c1,
	'currentstep2' => $c2,
	'currentstep3' => $c3,
	'voltage_label' => $voltlabel,
	'current_label' => $currentlabel,
	'emergency' => $emergency,
	'emergency2' => $emergency2,
	'voltagelimit_iv' => $vlimit,
	'currentlimit' => $currentlimit,
	'diff_current_limit' => $diffcurrentlimit,
	'electrolysis_limit_voltage' => $elimit,
	'stop' => 0,
	'cont' => 1,
	'updown' => 0,
	'temp_label' => $templabel,
    };
}


sub plot()
{
    my $plotfile = $RFC::Header::htmldir.'png/rig_'.$rig->name.'_iv.png';
    my $plotcmd = "f(x) = 0\nplot f(x) linetype -1";
    my $datfile = $localdatadir.'/ivcurve.'.$dat;
    my $linenr = 0;
    if (-e $datfile)
    {
	my $cmd = "perl -e 'while (<>){next if /\\w/; print \$_}' < $datfile";
	my @l = `$cmd`;
	my $ivnr = @l-1;
	unless ($ivnr < 0)
	{
	    $cmd = "perl -e 'while (<>){\$l++; next if (/\\w/); \$c++; last if (\$c > $ivnr)} print \$l,\"\\n\"' < $datfile";
	    $linenr = `$cmd`;
	    chomp $linenr;
	}
    }
    my $lines = 0;
    foreach my $psu (keys %PSU)
    {
	$lines++;
    }
    if ($plot)
    {
	$plotcmd = "f(x) = (abs(x)>0.001?x:1/0)\n";
	$plotcmd .= "plot '<awk \"NR>$linenr\" < $datfile' u 3:(\$4/1000) w lines, '' u 3:(f(\$5)) axes x1y2 w lines";
	my $x = 0;
	while ($x++ < ($lines-1))
	{
	    my $xx = 5*$x+3;
	    my $y = 5*$x+4;
	    $plotcmd .= ", '' u $xx:(\$$y/1000) w lines";
	    my $z = 5*$x+5;
	    $plotcmd .= ", '' u $xx:(f(\$$z)) axes x1y2 w lines";
	}
    }
    unless ($gpid)
    {
	my $tmpfile = $plotfile.'.tmp';
	### initialise gnuplot and run in sub to ensure close (file not updated otherwise) ###
	{
	    my $gp = Graphics::GnuplotIF->new( style=>'points');
	    $gp->gnuplot_cmd('set nokey');
	    $gp->gnuplot_cmd('set zeroaxis linetype -1');
	    $gp->gnuplot_cmd("set yla 'U (V)'");
	    $gp->gnuplot_cmd("set y2la 'R (Ohm*cm^2)'");
	    $gp->gnuplot_cmd("set xla 'I (A)'");
	    $gp->gnuplot_cmd("set term png");
	    $gp->gnuplot_cmd("set out '$tmpfile'");
	    $gp->gnuplot_cmd('set size 0.75,0.70');
            $gp->gnuplot_cmd('set y2tics nomirror');
            $gp->gnuplot_cmd('set ytics nomirror');
	    $gp->gnuplot_cmd($plotcmd);
	}
	move($tmpfile,$plotfile);
        $plot = 1;
        return undef;
    }
    #### Gnuplot commands for command line display ####
    #### Setup ranges here (in case they change) ###
    if ($plot)
    {
        $gpid->gnuplot_cmd("rep");
    }
    else
    {
        ### Setup all gnuplot settings here ####
        $gpid->gnuplot_cmd('set nokey');
        $gpid->gnuplot_cmd('set zeroaxis linetype -1');
        $gpid->gnuplot_cmd("set yla 'U (V)'");
        $gpid->gnuplot_cmd("set y2la 'R (Ohm*cm^)'");
        $gpid->gnuplot_cmd("set xla 'I (A)'");
	$gpid->gnuplot_cmd('set y2tics nomirror');
	$gpid->gnuplot_cmd('set ytics nomirror');
        $gpid->gnuplot_cmd($plotcmd);
        $plot = 1;
    }
    return undef;
}
