#! perl -w
#######################################################################
#  Scanner.pl : Rudman scanner
#######################################################################

use strict;
use warnings;
use Win32::SerialPort qw( :STAT 0.19 );
use Win32::GUI qw( MB_ICONQUESTION MB_ICONINFORMATION MB_YESNOCANCEL MB_ICONSTOP
                   MB_OK IDYES IDCANCEL );
use Win32::TieRegistry( Delimiter => "/", ArrayValues => 0 );

my $Version = "2.1";
my $TimeBase = time;

my $CurrentFile;
my $EvilPort;

$CurrentFile = shift;
$EvilPort    = shift;

ApplyDefault($CurrentFile, '');
ApplyDefault($EvilPort, 'COM13');
NormalizeCOM($EvilPort);

# Allow translatable messages
our $LanguageFile = "$0.language.txt";
do 'Language.pl' || do '..\Language.pl' || die "Failed to load Language.pl: $@ $!";

# Port objects and history
our $PortInput;
our $SupplyPortObj;
our $EvilPortObj;
do 'Ports.pl' || do '..\Ports.pl' || die "Failed to load Ports.pl: $@ $!";

my $FindDlg;

my $YScales = 5;
my $graphmargin = 0.2;

use constant INDIAWIDTH   => 14;

use constant STOPPED      => 0;
use constant RUNNINGGUI   => 1;
use constant RUNNINGINDIA => 2;
use constant STOPPING     => 3;

my $Running = STOPPED;

my $EditFont = new Win32::GUI::Font (
	-name => "Courier New",
	-size => 12,
    );

my $LabelFont = new Win32::GUI::Font (
	-name => "Courier New",
	-size => 12,
    );

# Make Menu
my $Menu = Win32::GUI::MakeMenu(
    "&File"                => "File",
    " > &New"              => { -name => "Exit",       -onClick => \&Scanner_OnNewFile },
    " > &Open..."          => { -name => "Open",       -onClick => \&Scanner_OnOpenFile },
    " > &Save"             => { -name => "Save",       -onClick => \&Scanner_OnSaveFile },
    " > Save &As ..."      => { -name => "SaveAs",     -onClick => \&Scanner_OnSaveAsFile },
    " > -"                 => 0,
    " > Print &Setup ..."  => { -name => "PrintSetup", -enabled => 0 },
    " > &Print"            => { -name => "Print",      -enabled => 0 },
    " > -"                 => 0,
    " > E&xit"             => { -name => "Exit",        -onClick => \&Scanner_OnQuitFile },
    "&Edit"                => "Edit",
    " > &Undo"             => { -name => "Undo",
                                -onClick => sub { my $self = shift; $self->Edit->Undo(); 0; }
                              },
    " > -"                 => 0,
    " > Cu&t"              => { -name => "Cut",
                                -onClick => sub { my $self = shift; $self->Edit->Cut();  0; }
                              },
    " > &Copy"             => { -name => "Copy",
                                -onClick => sub { my $self = shift; $self->Edit->Copy(); 0; }
                              },
    " > &Paste"            => { -name => "Paste",
                                -onClick => sub { my $self = shift; $self->Edit->Paste(); 0; }
                              },
    " > &Delete"           => { -name => "Delete",
                                -onClick => sub { my $self = shift; $self->Edit->Clear(); 0; }
                              },
    " > -"                 => 0,
    " > &Select All"       => { -name => "SelectAll",
                                -onClick => sub { my $self = shift; $self->Edit->SelectAll(); 0; }
                              },
    "&View"                => "_View",
    " > &Edit Font..."     => { -name => "ChooseFont", -onClick => \&Scanner_OnChooseFont },
    " > &Label Font..."    => { -name => "ChooseFont", -onClick => \&Scanner_OnChooseLabelFont },
    "F&ind"                => "_Find",
    " > &Find..."          => { -name => "Find",       -onClick => \&Scanner_OnFind  },
    " > Find &Next"        => { -name => "FindNext",   -onClick => \&Scanner_FindText },
    "&Control"             => "_Control",
    " > &Run GUI"          => { -name => "ControlRunGUI", -onClick => \&Scanner_RunGUI },
    " > Run &India"        => { -name => "ControlRunIndia", -onClick => \&Scanner_RunIndia },
    " > &Stop"             => { -name => "ControlStop",   -onClick => \&Scanner_Stop, -enabled => 0},
#    " > Clear &History"    => { -name => "ControlHistory", -onClick => \&Scanner_History, -enabled => 0},
    " > &COM detect"       => { -name => "ControlCOM", -onClick => \&Control_COMDetect },
    " > COM and cell &detect" => { -name => "ControlCOMCell", -onClick => \&Control_COMCellDetect },
    " > Enable &logging"   => { -name => "ControlLogging", -checked => 0 },
    " > Enable CS&V"       => { -name => "ControlCSV", -checked => 0 },
    " > Si&mulate"         => { -name => "ControlSimulate", -checked => 0 },
    " > Char&ger test"     => { -name => "ChargerTest",-onClick => \&Scanner_OnChargerTest },
    " > &Terminal"         => { -name => "Term",       -onClick => \&Scanner_OnTerm },
    "&Graph"               => "_Graph",
    " >  &1 - Left to right 1" => { -name => "GraphLeftToRight1", -checked => 1 },
    " >  &2 - Left to right 2" => { -name => "GraphLeftToRight2", -enabled => 0 },
    " >  &3 - Over Under 1"    => { -name => "GraphOverUnder1" },
    " >  &4 - Over Under 2"    => { -name => "GraphOverUnder2" },
    "&Help"                => "?",
    " > &Help"             => { -name => "Help",       -onClick => \&Scanner_OnHelp },
    " > -"                 => 0,
    " > &About"            => { -name => "About",      -onClick => \&Scanner_OnAbout },
);


# Create Main Window
my $Window = new Win32::GUI::Window (
    -name     => "Window",
    -title    => "Scanner",
    -pos      => [$Registry->{"CUser/Software/Manzanita/Scanner/Left"} || 100, $Registry->{"CUser/Software/Manzanita/Scanner/Right"} || 100],
    -size     => [$Registry->{"CUser/Software/Manzanita/Scanner/Width"} || 400, $Registry->{"CUser/Software/Manzanita/Scanner/Height"} || 400],
    -menu     => $Menu,
    -onResize   => \&Scanner_OnSize,
    -onInitMenu => \&Scanner_OnInitMenu,
    -onTerminate => \&Scanner_OnQuitFile,

    -eventmodel => "both",                  # Force both event model

) or die "new Window";

# Create Textfield for manual terminal
$Window->AddTextfield(
    -name      => "Term",
    -pos       => [0, 0],
    -size      => [100, 100],
    -multiline => 1,
    -hscroll   => 1,
    -vscroll   => 1,
    -autohscroll => 1,
    -autovscroll => 1,
    -keepselection => 1 ,
    -font => $EditFont,
    -visible => 0,

    -injectcrdelay => 0,
    -processing => 0,
    -running => 0,
);

sub Term_Change
    {
    return if $Window->Term->{processing};
    my @selpos = $Window->Term->GetSel();

    my $lineindex = $Window->Term->LineFromChar($selpos[0]);
    if ($lineindex > 0 &&
        $Window->Term->GetLine($lineindex) eq '' &&
        (my $line = $Window->Term->GetLine($lineindex - 1)) ne '')
        {
        ++$Window->Term->{processing};
        if ($Menu->{ControlSimulate}->Checked)
            {
            if ($line =~ /^(\d+)v[oltage\.]*$/)
                {
                $Window->Term->Append(sprintf("%s %5.3fV\r\n", $1, 3.000 + $1 / 1000));
                }
            elsif ($line =~ /^(\d+)m(?:[inimum\.]+)$/)
                {
                $Window->Term->Append(sprintf("%s %5.3fMI\r\n", $1, 2.000 + $1 / 1000));
                }
            elsif ($line =~ /^(\d+)m(?:[aximum\.]+)$/)
                {
                $Window->Term->Append(sprintf("%s %5.3fMA\r\n", $1, 4.000 + $1 / 1000));
                }
            elsif ($line =~ /^(\d+|\*)h(?:[stclearmima\.]+)$/)
                {
                }
            else
                {
                $Window->Term->Append(qq(Unsimulated command "$line"\r\n));
                }
            }
        else
            {
            Scanner_WriteEvil("\n", $line);
            $PortInput = '(No reply)' if length($PortInput) == 0;
            $Window->Term->Append("$_\r\n") for split(/[\r\n]+/,$PortInput);
            }
        --$Window->Term->{processing};
        }

    $Window->Term->Modify(0);
    }

# Create Textfield for Edit Text
$Window->AddTextfield(
    -name      => "Edit",
    -pos       => [0, 0],
    -size      => [100, 100],
    -multiline => 1,
    -hscroll   => 1,
    -vscroll   => 1,
    -autohscroll => 1,
    -autovscroll => 1,
    -keepselection => 1 ,
    -font => $EditFont,
    -visible => 1,
);

# Fill in the edit screen with default values if no file was specified
if ($CurrentFile eq '')
    {
    $Window->Edit->Append(Translate("<$_>\r\n")) for (split(/[\r\n]/m,<<ENDOFEDIT
; Rudman Mark 3 Scanner
; Click Control / COM detect or
; Control / COM and Cell detect to
; automatically detect the regulators
Graph 1
COM13
1-8
ENDOFEDIT
));
    $Window->Edit->Modify(0);  # Set Modified state to false
    }

# Create Textfield for Help
$Window->AddTextfield(
    -name      => "Help",
    -pos       => [0, 0],
    -size      => [100, 100],
    -multiline => 1,
    -autowrap  => 1,
    -vscroll   => 1,
    -autovscroll => 1,
    -keepselection => 1,
    -font => $EditFont,
    -visible => 0,
);

$Window->Help->Append(Translate("<$_>\r\n")) for (split(/[\r\n]/m,<<ENDOFHELP
Rudman Mark 3 Scanner

This scanner uses an India format file or a description file to control the display of one or more regulators.

Use Control / Run India or Control / Run GUI, depending on the format of your file.

The India format file lines may be simply COMx to switch to the given COM port, or a string of valid regulator commands separated by semicolon (;). If a command will not generate any output (e.g. 99h.) then prefix it with a exclamation point (!) otherwise --ERR-- will be displayed in the output.

The description file lines are as follows:

.) Comments start with ;
.) A single bus address. E.g.: 7
.) A range of bus addresses: E.g.: 1-8 or 8-1
.) Simulate {On, Off}
.) Graph {1-4}
.) COMx

Each bus address is displayed in the order specified. Duplicate bus addresses are illegal.

The Graph menu allows selection of how the bus addresses are displayed and organized. In all modes, the range of the graph is 0.2V beyond the actual recorded range of all bus addresses. An indicator line is present for both the low and high history for each bus address. The graph shows red up to the low history for any bus address that has been low. The graph shows blue (and resets the red) for any bus address that is currently high. The graph is green for any bus address currently between setlow and sethigh.

.) Left to right 1 (Graph 1)
The first bus address specified is at the left, and following bus addresses are displayed to the right. The lowest voltage recorded is at the bottom, and the highest is at the top.

.) Over Under 1 (Graph 3)
The first, third, etc. bus addresses specified are on the bottom. The second, fourth, etc. bus addresses specified are at the top. The highest voltages are in the middle of the graph, and the lowest voltages are at the top and bottom edges.

.) Over Under 2 (Graph 4)
The first, third, etc. bus addresses specified are on the bottom. The second, fourth, etc. bus addresses specified are at the top. The highest voltages are towards the top of the graph.

Command line:
Optionally give: filename evil_port
Specify the default with a single dash, e.g.: scanner - COM6

ENDOFHELP
));

my $StatusBar = $Window->AddStatusBar();

# Run GUI button
$Window->AddButton (
              -name  => "RunGUI",
              -text  => "RunGUI",
              -pos   => [0, 0],
              -size  => [75 , 21],
              -onClick => \&Scanner_RunGUI,
      );

# Run India button
$Window->AddButton (
              -name  => "RunIndia",
              -text  => "RunIndia",
              -pos   => [0, 0],
              -size  => [75 , 21],
              -onClick => \&Scanner_RunIndia,

              -output => [],
      );

# Stop running
$Window->AddButton (
              -name  => "Stop",
              -text  => "Stop",
              -pos   => [0, 0],
              -size  => [75 , 21],
              -onClick => \&Scanner_Stop,
      );
$Window->Stop->Enable(0);

# Done with help button
$Window->AddButton (
              -name  => "DoneHelp",
              -text  => "OK",
              -pos   => [0, 0],
              -size  => [75 , 21],
              -onClick => \&Scanner_DoneHelp,
      );
$Window->DoneHelp->Hide();

$Window->AddLabel(
    -name      => "Label_Y_$_",
    -text      => "0.000V",
    -pos       => [0, 0],
    -multiline => 0,
    -hscroll   => 0,
    -vscroll   => 0,
    -autohscroll => 0,
    -autovscroll => 0,
    -font => $LabelFont,
    -visible => 0,
) for (1..$YScales);

# Create Find DialogBox
$FindDlg = new Win32::GUI::DialogBox(
              -name  => "FindDlg",
              -title => "Find",
              -pos   => [ 150, 150 ],
              -size  => [ 360, 115 ],
              -parent => $Window,       # Set $Window as parent for be modeless but not Top.
      );

# Text to find
$FindDlg->AddTextfield (
              -name  => "FindDlg_Text",
              -pos   => [10, 12],
              -size  => [220, 21],
              -prompt  => [ "Find : ", 30],
              -tabstop => 1,
              -onChange => sub { $FindDlg->FindDlg_Find->Enable(length($FindDlg->FindDlg_Text->Text) != 0); 0; },
      );

# Case sensitive checkbox
$FindDlg->AddCheckbox (
              -name  => "FindDlg_Case",
              -text  => "Match case",
              -pos   => [10, 60],
              -size  => [100, 21],
              -tabstop => 1,
      );

#
$FindDlg->AddGroupbox(
              -text  => "Direction",
              -pos   => [140, 40],
              -size  => [120, 40],
      );
# Search Up RadioButton
$FindDlg->AddRadioButton(
              -name  => "FindDlg_FindUp",
              -text  => "Up",
              -pos   => [150, 55],
              -size  => [50,  20],
              -group => 1,
      );

# Search Down RadioButton
$FindDlg->AddRadioButton(
              -name  => "FindDlg_FindDown",
              -text  => "Down",
              -pos   => [200, 55],
              -size  => [50, 20],
              -tabstop => 1,
              -checked => 1,
      );

# Find button
$FindDlg->AddButton (
              -name  => "FindDlg_Find",
              -text  => "&Find",
              -pos   => [270, 10],
              -size  => [75 , 21],
              -onClick => sub { Scanner_FindText($Window); 0; },
              -group => 1,
              -tabstop => 1,
      );

# Cancel Button
$FindDlg->AddButton (
              -name  => "FindDlg_Cancel",
              -text  => "C&ancel",
              -pos   => [270, 40],
              -size  => [75 , 21],
              -onClick => sub { $FindDlg->Hide(); 0; },
              -tabstop => 1,
      );


Scanner_LoadFile($Window, $CurrentFile);

# Set Focus to Edit and show Window
$Window->Edit->SetFocus();
$Window->Show();
my $Pwhite = new Win32::GUI::Pen([ 255, 255, 255 ]);
my $Bwhite = new Win32::GUI::Brush([ 255, 255, 255 ]);
my $Pred   = new Win32::GUI::Pen([ 255, 0, 0 ]);
my $Pgreen = new Win32::GUI::Pen([ 0, 255, 0 ]);
my $Pblue  = new Win32::GUI::Pen([ 0, 0, 255 ]);
my $Pblack = new Win32::GUI::Pen(
                -color => [ 0, 0, 0 ],
                -width => 1,
            );

my @busaddrs;
my $MouseIndex;
my $MouseY;
my %vbottom;
my $vmin = 2.0;
my $vmax = 4.5;
my $setl = 9999;
my $seth = 0;
my $vleft;
my $vtop;
my $vright;
my $vbottom;
my $vwidth;
my $vheight;
my $delta;
my %uniquelog;

TestLoop();

sub AdjustTermText
    {
    my $pos = shift;
    my $text = shift;
    my $width = shift || INDIAWIDTH;
    my $term = $Window->Term->Text();
    substr $term,$pos,$width,sprintf("%-*s", $width, $text);
    $Window->Term->Text($term);
    }

sub TestLoop
    {
    my $index = 0;
    my $testminmax = 0;
    for (;;)
        {
        Win32::GUI::DoEvents();
        if ($Running != RUNNINGGUI && $Running != RUNNINGINDIA)
            {
            Win32::Sleep(10);

            if ($Window->Term->{running})
                {
                GetPortInput($EvilPortObj);
                ++$Window->Term->{processing};
                if (length($PortInput) != 0)
                    {
                    $PortInput =~ s/[\r\n]+/\r\n/goim;
                    $Window->Term->Append($PortInput);
                    $Window->Term->{injectcrdelay} = ($PortInput =~ /[\r\n]$/oim) ? 0 : 5;
                    }
                elsif ($Window->Term->{injectcrdelay} && --$Window->Term->{injectcrdelay} == 0)
                    {
                    $Window->Term->Append("\r\n");
                    }
                $Window->Term->Modify(0);
                --$Window->Term->{processing};
                }

            next if $Running == STOPPED;

            if (!$Menu->{ControlSimulate}->Checked)
                {
                PortClose($EvilPortObj, OPENFAIL => {SUB => \&Evil_OpenCloseFail, PARMS => [$Window, Translate("<Error trying to close the evil port>:")]});
                }
            $Window->Edit->Show();
            $Window->Edit->SetFocus();
            $Window->Term->Hide();
            $Window->RunGUI->Enable(1);
            $Window->RunIndia->Enable(1);
            $Window->Stop->Enable(0);
            $Menu->{ControlRunGUI}->Enabled(1);
            $Menu->{ControlRunIndia}->Enabled(1);
            $Menu->{ControlStop}->Enabled(0);

            $Menu->{GraphLeftToRight1}->Enabled(1);
            $Menu->{GraphLeftToRight2}->Enabled(0);
            $Menu->{GraphOverUnder1}->Enabled(1);
            $Menu->{GraphOverUnder2}->Enabled(1);

            $Menu->{Help}->Enabled(1);
            $Menu->{Term}->Enabled(1);

            $Window->{"Label_$_"}->Hide() for map { ${$busaddrs[$_]}{BA} } (0..$#busaddrs);
            $Window->{"Label_Y_$_"}->Hide() for (1..$YScales);

            $Running = STOPPED;
            StatusBar_Update();
            next;
            }

        if ($Running == RUNNINGINDIA)
            {
            my $outputline = 0;
            for my $line (split(/\r\n/m, $Window->Edit->Text()))
                {
                last unless $Running == RUNNINGINDIA;
                next if $line =~ /^\s*;/;

                my $command;
                my $outputfield = 0;
                while (length($line))
                    {
                    Win32::GUI::DoEvents();
                    last unless $Running == RUNNINGINDIA;

                    if ($line =~ s/^(.*?);//)
                        {
                        $command = $1;
                        }
                    else
                        {
                        $command = $line;
                        $line = '';
                        }

                    if ($command =~ /COM\d+/io)
                        {
                        NormalizeCOM($command);
                        if ($EvilPort ne $command)
                            {
                            if (!$Menu->{ControlSimulate}->Checked)
                                {
                                my $SaveEvilPort = $EvilPort;
                                $EvilPort = $command;
                                my $NewEvilPortObj = PortOpen(PORT => $command, CRTERMINATE => 1, TRANSLATECR => 1, TIMEOUT => 200,
                                                        OPENFAIL => {SUB => \&Evil_OpenCloseFail, PARMS => [$Window, Translate("<Error trying to open the evil port>:")]});
                                if (!defined($NewEvilPortObj))
                                    {
                                    $Running = STOPPING;
                                    $EvilPort = $SaveEvilPort;
                                    next;
                                    }
                                PortClose($EvilPortObj, OPENFAIL => {SUB => \&Evil_OpenCloseFail, PARMS => [$Window, Translate("<Error trying to close the evil port>:")]});
                                $EvilPortObj = $NewEvilPortObj;
                                }
                            }
                        next;
                        }

                    my ($silent) = ($command =~ s/^!//);
                    Scanner_WriteEvil("\n", $command);
                    if (!$silent)
                        {
                        ${${$Window->RunIndia->{output}}[$outputline]}[$outputfield++] = (length($PortInput) == 0 ? "--ERR--" : $PortInput);
                        my $output;
                        for (@{$Window->RunIndia->{output}})
                            {
                            if (@{$_})
                                {
                                $output .= join(' ', @{$_}) . "\r\n";
                                }
                            else
                                {
                                $output .= "\r\n";
                                }
                            }
                        $Window->Term->Text($output);
                        }
                    }
                ++$outputline;
                }
            next;
            }

        my ($width, $height) = ($Window->Edit->Width(), $Window->Edit->Height());
        my $DC = $Window->GetDC;

        Visible_Update();

        for (my $index = 0; $index < @busaddrs; ++$index)
            {
            my $ba = $busaddrs[$index];
            $_ = $$ba{BA};
            Win32::GUI::DoEvents();
            last if $Running != RUNNINGGUI;

            Scanner_WriteEvil("\n", "%s%s", Address($_), 'v.');
            $$ba{V} = $1 if $PortInput =~ /(\d+\.\d+)/;
            uniquelogprint($_, 'v.', $1);
            SaveMinMax($index);

            # These things do not need to be read every channel
            if (++$testminmax == (1+@busaddrs))
                {
                $testminmax = 0;
                Scanner_WriteEvil("\n", "%s%s", Address($_), 'mi.');
                ($$ba{VMIN}) = $PortInput =~ /(\d+\.\d+)/;
                uniquelogprint($_, 'mi.', $1);
                $vmin = min($vmin, $$ba{VMIN});
                Scanner_WriteEvil("\n", "%s%s", Address($_), 'ma.');
                ($$ba{VMAX}) = $PortInput =~ /(\d+\.\d+)/;
                uniquelogprint($_, 'ma.', $1);
                $vmax = max($vmax, $$ba{VMAX});
                StatusBar_Update();
                }

            my $color = vcolor($index);
            my $bcolor = defined($vbottom{$_}) ? $vbottom{$_} : $color;
            my $B = new Win32::GUI::Brush($color);
            my $Bbottom = new Win32::GUI::Brush($bcolor);

            my (@clear, @mid, @bottom, @min, @max, @setl, @seth);
            my ($left, $right, $bottom, $min, $max);

            if ($Menu->{GraphLeftToRight1}->Checked)
                {
                $left   = $vleft + $index * $delta;
                $right  = $left + $delta - 2;
                $min    = $vtop + barheight($vheight, $$ba{VMIN});
                $max    = $vtop + barheight($vheight, $$ba{VMAX});

                @clear  = ($left,  $vtop,                                $right, $vtop + barheight($vheight, $$ba{V}));
                @mid    = ($left,  $vtop + barheight($vheight, $$ba{V}), $right,         $min);
                @bottom = ($left,          $min,                         $right, $vbottom);
                @max    = ([$left,         $max], [$right,         $max]);
                @min    = ([$left,         $min], [$right,         $min]);
                @setl   = ([$left, $vtop + barheight($vheight, $$ba{SETL})], [$right, $vtop + barheight($vheight, $$ba{SETL})]);
                @seth   = ([$left, $vtop + barheight($vheight, $$ba{SETH})], [$right, $vtop + barheight($vheight, $$ba{SETH})]);

                $$ba{TOP}    = $vtop;
                $$ba{BOTTOM} = $vbottom;
                }
            elsif ($Menu->{GraphLeftToRight2}->Checked)
                {
                }
            elsif ($Menu->{GraphOverUnder1}->Checked)    # Rendered above / below, max in the middle
                {
                my $yxl = ($index & 1) ? sub {$vtop + $_[0] + ($vheight >> 1)} : sub {$vtop + ($vheight >> 1) - $_[0]};
                $left   = $vleft + ($index >> 1) * $delta;
                $right  = $left + $delta - 2;
                $min    = barheight($vheight / 2, $$ba{VMIN});
                $max    = barheight($vheight / 2, $$ba{VMAX});

                @clear  = ($left,  &$yxl(0),                                               $right, &$yxl(barheight($vheight / 2, $$ba{V})));
                @mid    = ($left,  &$yxl(barheight($vheight / 2, $$ba{V})), $right, &$yxl($min));
                @bottom = ($left,  &$yxl($min),                                            $right, &$yxl($vheight / 2));
                @max    = ([$left, &$yxl($max)], [$right, &$yxl($max)]);
                @min    = ([$left, &$yxl($min)], [$right, &$yxl($min)]);
                @setl   = ([$left, &$yxl(barheight($vheight / 2, $$ba{SETL}))], [$right, &$yxl(barheight($vheight / 2, $$ba{SETL}))]);
                @seth   = ([$left, &$yxl(barheight($vheight / 2, $$ba{SETH}))], [$right, &$yxl(barheight($vheight / 2, $$ba{SETH}))]);

                $$ba{TOP}    = ($index & 1) ? 0 : $vheight / 2;
                $$ba{BOTTOM} = ($index & 1) ? $vheight / 2 : $vheight;
                }
            elsif ($Menu->{GraphOverUnder2}->Checked)    # Rendered above / below, max at top
                {
                my $yxl = ($index & 1) ? sub {$vtop + $_[0] + ($vheight >> 1)} : sub {$vtop + $_[0]};
                $left   = ($index >> 1) * $delta;
                $right  = $left + $delta - 2;
                $min    = barheight($vheight / 2, $$ba{VMIN});
                $max    = barheight($vheight / 2, $$ba{VMAX});

                @clear  = ($left,  &$yxl(0),                                               $right, &$yxl(barheight($vheight / 2, $$ba{V})));
                @mid    = ($left,  &$yxl(barheight($vheight / 2, $$ba{V})), $right, &$yxl($min));
                @bottom = ($left,  &$yxl($min),                                            $right, &$yxl($vheight / 2));
                @max    = ([$left, &$yxl($max)], [$right, &$yxl($max)]);
                @min    = ([$left, &$yxl($min)], [$right, &$yxl($min)]);
                @setl   = ([$left, &$yxl(barheight($vheight / 2, $$ba{SETL}))], [$right, &$yxl(barheight($vheight / 2, $$ba{SETL}))]);
                @seth   = ([$left, &$yxl(barheight($vheight / 2, $$ba{SETH}))], [$right, &$yxl(barheight($vheight / 2, $$ba{SETH}))]);

                $$ba{TOP}    = ($index & 1) ? 0 : $vheight / 2;
                $$ba{BOTTOM} = ($index & 1) ? $vheight / 2 : $vheight;
                }

            $$ba{LEFT}  = $left;
            $$ba{RIGHT} = $right;

            $DC->SelectObject($Pwhite);                         # Erase over the box top
            $DC->SelectObject($Bwhite);
            $DC->Rectangle(@clear);

            $DC->SelectObject($Pblack);                         # Draw the box top
            $DC->SelectObject($B);
            $DC->Rectangle(@mid);
            $DC->SelectObject($Bbottom);                        # Draw the box bottom
            $DC->Rectangle(@bottom);

            $DC->SelectObject($Pblue);                          # Draw the max line
            $DC->BeginPath();
            $DC->MoveTo(@{$max[0]});
            $DC->LineTo(@{$max[1]});
            $DC->EndPath();
            $DC->StrokePath();

            $DC->SelectObject($Pblue);                          # Draw the min line
            $DC->BeginPath();
            $DC->MoveTo(@{$min[0]});
            $DC->LineTo(@{$min[1]});
            $DC->EndPath();
            $DC->StrokePath();

            $DC->SelectObject($Pred);                           # Draw the low line
            $DC->BeginPath();
            $DC->MoveTo(@{$setl[0]});
            $DC->LineTo(@{$setl[1]});
            $DC->EndPath();
            $DC->StrokePath();

            $DC->SelectObject($Pred);                           # Draw the high line
            $DC->BeginPath();
            $DC->MoveTo(@{$seth[0]});
            $DC->LineTo(@{$seth[1]});
            $DC->EndPath();
            $DC->StrokePath();
            }
        }
    }

sub Visible_Update
    {
    my ($width, $height) = ($Window->Edit->Width(), $Window->Edit->Height());

    if ($Menu->{GraphLeftToRight1}->Checked)
        {
        $vleft   = 60;
        $vtop    = 30;
        $vright  = $width;
        $vbottom = $height - 30;
        $vwidth  = $vright - $vleft;
        $vheight = $vbottom - $vtop;

        $delta  = int($vwidth / @busaddrs);
        }
    elsif ($Menu->{GraphLeftToRight2}->Checked)
        {
        }
    elsif ($Menu->{GraphOverUnder1}->Checked)    # Rendered above / below, max in the middle
        {
        $vleft   = 60;
        $vtop    = 30;
        $vright  = $width;
        $vbottom = $height - 30;
        $vwidth  = $vright - $vleft;
        $vheight = $vbottom - $vtop;

        $delta  = int($vwidth / ((1 + @busaddrs) >> 1));
        }
    elsif ($Menu->{GraphOverUnder2}->Checked)    # Rendered above / below, max at top
        {
        $vleft   = 60;
        $vtop    = 30;
        $vright  = $width;
        $vbottom = $height - 30;
        $vwidth  = $vright - $vleft;
        $vheight = $vbottom - $vtop;

        $delta  = int($vwidth / ((1 + @busaddrs) >> 1));
        }
    }

sub vcolor
    {
    my $index = shift;
    my $ba = $busaddrs[$index];
    my $volt = $$ba{V};

    return $vbottom{$$ba{BA}} = [255, 0, 0  ] if $volt < ($$ba{SETL} || 2.500);
    return [0, 255, 0] if $volt < ($$ba{SETH} || 3.500);
    undef $vbottom{$$ba{BA}};
    return [0, 0, 255];
    }

sub barheight
    {
    my $total = shift;
    my $current = shift;
    my $min = min($setl, $vmin) - $graphmargin;
    my $max = max($seth, $vmax) + $graphmargin;

    int($total * (1 - ($current - $min) / ($max - $min)));
    }

sub SaveMinMax
    {
    my $index = shift;
    my $ba = $busaddrs[$index];
    if ($$ba{V} < $$ba{VMIN})
        {
        $$ba{VMIN} = $$ba{V};
        SaveMin($$ba{V});
        StatusBar_Update();
        }
    if ($$ba{V} > $$ba{VMAX})
        {
        $$ba{VMAX} = $$ba{V};
        SaveMax($$ba{V});
        StatusBar_Update();
        }
    }

#######################################################################
#  Window Event

# Resize Window
sub Scanner_OnSize {
    my ($self) = @_;

    $Registry->{"CUser/Software/Manzanita/Scanner/Left"}   = $self->Left();
    $Registry->{"CUser/Software/Manzanita/Scanner/Right"}  = $self->Top();
    $Registry->{"CUser/Software/Manzanita/Scanner/Width"}  = $self->Width();
    $Registry->{"CUser/Software/Manzanita/Scanner/Height"} = $self->Height();

    my ($width, $height) = ($self->GetClientRect())[2..3];
    $self->Edit->Resize($width+1, $height+1 - $StatusBar->Height() - $Window->RunGUI->Height() - 2) if exists $self->{Edit};
    $self->Help->Resize($width+1, $height+1 - $StatusBar->Height() - $Window->RunGUI->Height() - 2) if exists $self->{Help};
    $self->Term->Resize($width+1, $height+1 - $StatusBar->Height() - $Window->RunGUI->Height() - 2) if exists $self->{Term};

    ($width, $height) = ($Window->ScaleWidth(), $Window->ScaleHeight());

    $StatusBar->Move( 0, ($Window->ScaleHeight() - $StatusBar->Height()) );
    $StatusBar->Resize( $Window->ScaleWidth(), $StatusBar->Height() );
    StatusBar_Update();
    Labels_Update();

    sub buttonleft { $Window->ScaleWidth() - $_[0] * (2 + $Window->RunGUI->Width()); }

    my $buttontop = $height - $StatusBar->Height() - $Window->RunGUI->Height();

    $Window->RunGUI->Move(buttonleft(1), $buttontop);
    $Window->RunIndia->Move(buttonleft(2), $buttontop);
    $Window->DoneHelp->Move(buttonleft(1), $buttontop);
    $Window->Stop->Move(buttonleft(3), $buttontop);
    $Window->InvalidateRect(1);
}

# Refresh menu item
sub Scanner_OnInitMenu {
  my $self = shift;

  # File Menu

  # Edit Menu
  my $bSel = $self->Edit->HaveSel();

  $Menu->{Undo}->Enabled($self->Edit->CanUndo());
  $Menu->{Cut}->Enabled($bSel);
  $Menu->{Copy}->Enabled($bSel);
  $Menu->{Paste}->Enabled($self->Edit->CanPaste());
  $Menu->{Delete}->Enabled($bSel);

  # Find Menu
  $Menu->{FindNext}->Enabled(length($FindDlg->FindDlg_Text->Text) != 0);

  0;
}

#
#######################################################################

sub Scanner_CheckSaveCurrentFile {
  my $self = shift;

  # Check if need to save
  if ($self->Edit->Modify()) {

    my $ret = $self->MessageBox ( "Save current file ?", "Scanner", MB_ICONQUESTION | MB_YESNOCANCEL);

    if ($ret == IDYES) {
        Scanner_OnSaveFile($self);
    }
    elsif ($ret == IDCANCEL) {
      return IDCANCEL;
    }
  }

  return IDYES;
}

# Find Text in Edit (search is defined by FindDlg)
sub Scanner_FindText {
  my $self = shift;

  my $text = $self->Edit->Text;                          # Get Edit text
  my $find = $FindDlg->FindDlg_Text->Text;               # Get text to find
  my ($start, $end) = $self->Edit->GetSel();             # Get current Edit Selection
  my $index;

  unless ( $FindDlg->FindDlg_Case->Checked() ) {         # Check for case unsensitive search
    $text = lc $text;                                    # Transform both to lowercase
    $find = lc $find;
  }

  if ($FindDlg->FindDlg_FindDown->Checked()) {           # Search to botton ?
    $index = index($text, $find, $end);
  } else {                                               # Or Search to Up ?
    $index = rindex($text, $find, $start-1);
  }

  if ($index >= 0) {
    $self->Edit->SetSel($index, $index + length $find);  # Select text found
    $self->Edit->ScrollCaret();                          # Make selected text visible
  } else {
    Win32::GUI::MessageBeep;                             # Sound if not found
  }

  0;
}

#######################################################################
#  Menu Event

#
#  File menu
#

# New File
sub Scanner_OnNewFile {
  my $self = shift;

  # Need to save current file ?
  return 0 if Scanner_CheckSaveCurrentFile($self) == IDCANCEL;

  # Set Default filename
  $CurrentFile = "";
  UpdateLogging();
  StatusBar_Update();
  $self->Text("Scanner");                                # Change main window Title

  # Reset Edit
  $self->Edit->Text("");                                 # Set Edit text Empty
  $self->Edit->EmptyUndoBuffer();                        # Empty Undo buffer
  $self->Edit->Modify(0);                                # Set Modified state to false
  $self->Edit->SetFocus();                               # Focus to Edit
  0;
}

# Open file
sub Scanner_OnOpenFile {
  my $self = shift;

  # Need to save current file ?
  return 0 if Scanner_CheckSaveCurrentFile($self) == IDCANCEL;

  # Open a file
  my $file = Win32::GUI::GetOpenFileName(
                   -owner  => $Window,
                   -title  => "Open a text file",
                   -filter => [
                       'Mark 3 scanner (*.mk3s)' => '*.mk3s',
                       'Text file (*.txt)' => '*.txt',
                       'All files' => '*.*',
                    ],
                   );

  Scanner_LoadFile($self, $file);
  0;
}

sub Scanner_LoadFile
{
  my $self = shift;
  my $file = shift;

  # Load file
  if ($file) {
     # Keep Current file.
     $CurrentFile = $file;
     UpdateLogging();
     StatusBar_Update();
     $self->Text( "$CurrentFile - Scanner");

     # Load File in Edit
     $self->Edit->LockWindowUpdate;                       # Lock Painting on Edit
     $self->Edit->Text("");                               # Empty Edit

     open F, "<$file" or warn "Open file : $file";
     while ( <F> ) {
       chomp;
       $self->Edit->Append($_."\r\n");                    # Need to add \r
     }
     close F;

     # Reset Edit
     $self->Edit->EmptyUndoBuffer();
     $self->Edit->Modify(0);
     $self->Edit->Select(0,0);                            # Set Cursor on first char
     $self->Edit->ScrollCaret();                          # Make cursor visible
     $self->Edit->LockWindowUpdate(1);                    # Unlock Painting on Edit
     $self->Edit->SetFocus();

  } elsif (Win32::GUI::CommDlgExtendedError()) {
     $self->MessageBox ("ERROR : ".Win32::GUI::CommDlgExtendedError(),
                        "GetOpenFileName Error");
  }
}

# Save File
sub Scanner_OnSaveFile {
  my $self = shift;

  # No filename, call SaveAs.
  return Scanner_OnSaveAsFile($self) unless (-f $CurrentFile );

  # Retrieve Text
  my $text = $self->Edit->Text;
  $text =~ s/\r\n/\n/mg;                                  # Need to remove \r

  # Save file
  open F, ">$CurrentFile" or die "Open file : $CurrentFile";
  print F $text;
  close F;

  # Reset Edit state
  $self->Edit->Modify(0);
  $self->Edit->SetFocus();

  0;
}

# Save File
sub Scanner_OnSaveAsFile {
  my $self = shift;

  # Open a file
  my $file = Win32::GUI::GetSaveFileName(
                   -owner  => $Window,
                   -title  => "Save As",
                   -file   => $CurrentFile,
                   -filter => [
                       'Mark 3 scanner (*.mk3s)' => '*.mk3s',
                       'Text file (*.txt)' => '*.txt',
                       'All files' => '*.*',
                    ],
                   -defaultextension => "pl",
                   -createprompt => 1,
                   );

  # Save file
  if (defined $file) {
    # Keep Current file.
    $CurrentFile = $file;
    UpdateLogging();
    StatusBar_Update();
    $self->Text( "$CurrentFile - Scanner");

    # Retrieve Text
    my $text = $self->Edit->Text();
    $text =~ s/\r\n/\n/mg;

    # Save file
    open F, ">$file" or die "Open file : $file";
    print F $text;
    close F;

    # Reset Edit state
    $self->Edit->Modify(0);
    $self->Edit->SetFocus();

  } elsif (Win32::GUI::CommDlgExtendedError()) {
     $self->MessageBox ("ERROR : ".Win32::GUI::CommDlgExtendedError(),
                        "GetSaveFileName Error");
  }

  0;
}

# Quit Scanner
sub Scanner_OnQuitFile {
  my $self = shift;

  # Need to save current file ?
  if (Scanner_CheckSaveCurrentFile($self) == IDCANCEL) {
    return 0;
  } else {
    exit 0;
  }
}

#
#  Edit menu
#

sub Scanner_OnChooseFont {
  my $self = shift;

  # Choose font
  my @font = Win32::GUI::ChooseFont(
                   -owner  => $Window,
                   $EditFont->Info()
                   );

  if ($#font) {
    $EditFont = new Win32::GUI::Font (@font);
    $self->Edit->Change( -font => $EditFont);
    $self->Edit->Update();
    $self->Help->Change( -font => $EditFont);
    $self->Help->Update();
    $self->Term->Change( -font => $EditFont);
    $self->Term->Update();
    $Window->InvalidateRect(1);
  }

  0;
}

sub Scanner_OnChooseLabelFont {
  my $self = shift;

  # Choose font
  my @font = Win32::GUI::ChooseFont(
                   -owner  => $Window,
                   $LabelFont->Info()
                   );

  if ($#font) {
    $LabelFont = new Win32::GUI::Font (@font);
    Labels_Update();
  }

  0;
}

#
#  Find menu
#

sub Scanner_OnFind {

  # Set focus and Enable Find button as appropriate
  $FindDlg->FindDlg_Text->SetFocus;
  $FindDlg->FindDlg_Find->Enable(length($FindDlg->FindDlg_Text->Text) != 0);

  $FindDlg->Show();
  0;
}

#
#  Help menu
#

# About box
sub Scanner_OnAbout {
  my $self = shift;

  $self->MessageBox(
     "Perl Scanner, version $Version\r\n" .
     "Copyright BSD, 2010",
     "About...",
     MB_ICONINFORMATION | MB_OK,
  );

  0;
}

#
#######################################################################

sub StatusBar_Update
    {
    my $extra = '';
    if ($Running == RUNNINGGUI && defined $MouseIndex)
        {
        my $ba = $busaddrs[$MouseIndex];
        $extra .= " Cell $$ba{BA} mi.$$ba{VMIN} v.$$ba{V} ma.$$ba{VMAX}";

        my $min = min($setl, $vmin) - $graphmargin;
        my $max = max($seth, $vmax) + $graphmargin;
        my $range = $max - $min;
        $extra .= sprintf(" mouse:%.3fV", $max - ($MouseY - $vtop) / ($vheight) * $range);

        }
    my $evil = $EvilPort;
    $evil =~ s/^[\\\.]+//;
    $StatusBar->Text( qq("$CurrentFile" $evil $extra @_) );
    }

sub UpdateLogging
    {
    local $_;
    # Set up the log file (if requested)
    my $LogFile = "Scanner.txt";
    if ($Menu->{ControlLogging}->Checked)
        {
        mkdir 'Logs';
        if (!open(LOG, '>', $_ = "Logs\\$LogFile"))
            {
            $Window->MessageBox ( qq($! "$_"), "Scanner", MB_ICONSTOP | MB_OK);
            $Menu->{ControlLogging}->Checked(0);
            }
        }
    else
        {
        close LOG;
        }
    }

sub UpdateCSV
    {
    # Set up the log file (if requested)
    my $CSVFile = "Scanner.csv";
    %uniquelog = ();
    if ($Menu->{ControlCSV}->Checked)
        {
        mkdir 'Logs';
        if (!open(CSV, '>', $_ = "Logs\\$CSVFile"))
            {
            $Window->MessageBox ( qq($! "$_"), "Scanner", MB_ICONSTOP | MB_OK);
            $Menu->{ControlCSV}->Checked(0);
            }
        }
    else
        {
        close CSV;
        }
    }

sub ControlLogging_Click {
    $Menu->{ControlLogging}->Checked(!$Menu->{ControlLogging}->Checked);
    UpdateLogging();
    $Window->InvalidateRect(1);
}

sub ControlCSV_Click {
    $Menu->{ControlCSV}->Checked(!$Menu->{ControlCSV}->Checked);
    UpdateCSV();
    $Window->InvalidateRect(1);
}

sub ControlSimulate_Click {
    $Menu->{ControlSimulate}->Checked(!$Menu->{ControlSimulate}->Checked);
    $Window->InvalidateRect(1);
}

sub GraphLeftToRight1_Click {
    $Menu->{GraphLeftToRight1}->Checked(1);
    $Menu->{GraphLeftToRight2}->Checked(0);
    $Menu->{GraphOverUnder1}->Checked(0);
    $Menu->{GraphOverUnder2}->Checked(0);
    Labels_Update();
    $Window->InvalidateRect(1);
}

sub GraphLeftToRight2_Click {
    $Menu->{GraphLeftToRight1}->Checked(0);
    $Menu->{GraphLeftToRight2}->Checked(1);
    $Menu->{GraphOverUnder1}->Checked(0);
    $Menu->{GraphOverUnder2}->Checked(0);
    Labels_Update();
    $Window->InvalidateRect(1);
}

sub GraphOverUnder1_Click {
    $Menu->{GraphLeftToRight1}->Checked(0);
    $Menu->{GraphLeftToRight2}->Checked(0);
    $Menu->{GraphOverUnder1}->Checked(1);
    $Menu->{GraphOverUnder2}->Checked(0);
    Labels_Update();
    $Window->InvalidateRect(1);
}

sub GraphOverUnder2_Click {
    $Menu->{GraphLeftToRight1}->Checked(0);
    $Menu->{GraphLeftToRight2}->Checked(0);
    $Menu->{GraphOverUnder1}->Checked(0);
    $Menu->{GraphOverUnder2}->Checked(1);
    Labels_Update();
    $Window->InvalidateRect(1);
}

sub min
    {
    my $retval = shift;
    for (@_) { $retval = $_ if $_ < $retval; }
    $retval;
    }

sub max
    {
    my $retval = shift;
    for (@_) { $retval = $_ if defined($_) && $_ > $retval; }
    $retval;
    }

sub Scanner_Stop
    {
    my $self = shift;

    $Running = STOPPING;
    }

sub ScanWindowText
    {
    my $requireaddrs = shift(@_);
    my $crterminate = @_ > 0 ? $_[0] : 1;
    my $translatecr = @_ > 1 ? $_[1] : 1;

    @busaddrs = ();
    my %busaddrs;
    # Retrieve Text
    my @text = split(/\r\n/m, $Window->Edit->Text());
    for (my $line = 1; $line <= @text; ++$line)
        {
        $_ = $text[$line - 1];
        $_ =~ s/\s*;.*//;
        next unless /\S/;
        if (my ($begin, $end) = /^(\d+)(?:-(\d+))?$/)
            {
            $end = $begin unless defined $end;
            if ($begin > 255 || $end > 255)
                {
                $Window->MessageBox ( qq(Illegal bus address "$_" in line $line), "Scanner", MB_ICONSTOP | MB_OK);
                return undef;
                }
            for (my $busaddr = $begin; ; $busaddr += ($begin < $end) ? +1 : -1)
                {
                if ($busaddrs{$busaddr})
                    {
                    $Window->MessageBox ( qq(Duplicate bus address "$busaddr" in lines $busaddrs{$busaddr} and $line), "Scanner", MB_ICONSTOP | MB_OK);
                    return undef;
                    }
                $busaddrs{$busaddr} = $line;
                push @busaddrs, {BA => $busaddr, FIRST => $busaddr == $begin};

                last if $busaddr == $end;
                }
            }
        elsif (my ($simulate) = /^Simulate\s+(.*?)\s*$/i)
            {
            if ($simulate !~ /^(On|Off)$/i)
                {
                $Window->MessageBox ( qq(Expected On or Off "$simulate" "$_" in line $line), "Scanner", MB_ICONSTOP | MB_OK);
                return undef;
                }
            $Menu->{ControlSimulate}->Checked($simulate =~ /On/i);
            }
        elsif (my ($graphtype) = /^Graph\s+(\d+)\s*/i)
            {
            if ($graphtype !~ /^[134]$/)
                {
                $Window->MessageBox ( qq(Illegal graph number "$_" in line $line), "Scanner", MB_ICONSTOP | MB_OK);
                return undef;
                }
            $Menu->{GraphLeftToRight1}->Checked($graphtype eq '1');
            $Menu->{GraphLeftToRight2}->Checked($graphtype eq '2');
            $Menu->{GraphOverUnder1}->Checked  ($graphtype eq '3');
            $Menu->{GraphOverUnder2}->Checked  ($graphtype eq '4');
            }
        elsif (/^COM\d+$/i)
            {
            NormalizeCOM($EvilPort = $_);
            StatusBar_Update();
            }
        else
            {
            $Window->MessageBox ( qq(Unknown command "$_" in line $line), "Scanner", MB_ICONSTOP | MB_OK);
            return undef;
            }
        }

    if ($requireaddrs)
        {
        if (@busaddrs == 0)
            {
            $Window->MessageBox ( qq(Missing bus address range), "Scanner", MB_ICONSTOP | MB_OK);
            return undef;
            }
        elsif (@busaddrs == 1)
            {
            GraphLeftToRight1_Click();
            $Menu->{GraphLeftToRight1}->Enabled(1);
            $Menu->{GraphLeftToRight2}->Enabled(0);
            $Menu->{GraphOverUnder1}->Enabled(0);
            $Menu->{GraphOverUnder2}->Enabled(0);
            }
        else
            {
            $Menu->{GraphLeftToRight1}->Enabled(1);
            $Menu->{GraphLeftToRight2}->Enabled(0);
            $Menu->{GraphOverUnder1}->Enabled(1);
            $Menu->{GraphOverUnder2}->Enabled(1);
            }
        }

    if (!$Menu->{ControlSimulate}->Checked)
        {
        $EvilPortObj = PortOpen(PORT => $EvilPort, CRTERMINATE => $crterminate, TRANSLATECR => $translatecr, TIMEOUT => 200,
                                OPENFAIL => {SUB => \&Evil_OpenCloseFail, PARMS => [$Window, Translate("<Error trying to open the evil port>:")]});
        return undef unless $EvilPortObj;
        }

    return $requireaddrs ? @busaddrs : 1;
    }

sub SaveMin
    {
    my $newmin = min(@_);
    return if $vmin && $newmin >= $vmin;
    $vmin = $newmin;

    Visible_Update();
    Labels_Y_Update();
    }

sub SaveMax
    {
    my $newmax = max(@_);
    return if $vmax && $newmax <= $vmax;
    $vmax = $newmax;

    Visible_Update();
    Labels_Y_Update();
    }

sub Scanner_RunGUI
    {
    my $self = shift;

    my @busaddrs = ScanWindowText(1);
    return unless @busaddrs > 0;

    $vmin = $vmax = undef;
    for my $ba (@busaddrs)
        {
        $_ = $$ba{BA};
        Evil_StatusBar("\n", "%s%s", Address($_), 'mi.');
        ($$ba{VMIN}) = $PortInput =~ /(\d+\.\d+)/;
        SaveMin($$ba{VMIN});
        Evil_StatusBar("\n", "%s%s", Address($_), 'v.');
        ($$ba{V}) = $PortInput =~ /(\d+\.\d+)/;
        Evil_StatusBar("\n", "%s%s", Address($_), 'ma.');
        ($$ba{VMAX}) = $PortInput =~ /(\d+\.\d+)/;
        SaveMax($$ba{VMAX});
        Evil_StatusBar("\n", "%s%s", Address($_), 'r.');
        ($$ba{SETL}) = $PortInput =~ /(\d+\.\d+)/;
        $setl = min($setl, $$ba{SETL});
        SaveMin($setl);
        Evil_StatusBar("\n", "%s%s", Address($_), 'g.');
        ($$ba{SETH}) = $PortInput =~ /(\d+\.\d+)/;
        $seth = max($seth, $$ba{SETH});
        SaveMax($seth);

        if (!exists $Window->{"Label_$$ba{BA}"})
            {
            $Window->AddLabel(
                -name      => "Label_$$ba{BA}",
                -text      => "$$ba{BA}",
                -pos       => [0, 0],
                -multiline => 0,
                -hscroll   => 0,
                -vscroll   => 0,
                -autohscroll => 0,
                -autovscroll => 0,
                -font => $LabelFont,
                -visible => 0,
            );
            }
        }

    for (0..$#busaddrs) { $vbottom{$busaddrs[$_]} = vcolor($_) };

    $Running = RUNNINGGUI;
    Labels_Update();
    $Window->Edit->Hide();
    $Window->RunGUI->Enable(0);
    $Window->RunIndia->Enable(0);
    $Window->Stop->Enable(1);
    $Menu->{ControlRunGUI}->Enabled(0);
    $Menu->{ControlRunIndia}->Enabled(0);
    $Menu->{ControlStop}->Enabled(1);
    $Menu->{Help}->Enabled(0);
    $Menu->{Term}->Enabled(0);
    StatusBar_Update();
    }

sub Scanner_RunIndia
    {
    my $self = shift;

    $Window->Term->Text('');

    my @output;
    my $foundcom = 0;
    for my $line (split(/\r\n/m, $Window->Edit->Text()))
        {
        last if $foundcom;
        next if $line =~ /^\s*;/;

        my $command;
        while (length($line))
            {
            if ($line =~ s/^(.*?);//)
                {
                $command = $1;
                }
            else
                {
                $command = $line;
                $line = '';
                }

            if (!$foundcom && $command =~ /COM\d+/io)
                {
                NormalizeCOM($EvilPort = $command);
                $foundcom = 1;
                last;
                }
            }
        push @output, [];
        }
    $Window->RunIndia->{output} = \@output;

    if (!$Menu->{ControlSimulate}->Checked)
        {
        $EvilPortObj = PortOpen(PORT => $EvilPort, CRTERMINATE => 1, TRANSLATECR => 1, TIMEOUT => 200,
                                OPENFAIL => {SUB => \&Evil_OpenCloseFail, PARMS => [$Window, Translate("<Error trying to open the evil port>:")]});
        return undef unless $EvilPortObj;
        }

    $Running = RUNNINGINDIA;
    $Window->Edit->Hide();
    $Window->Term->Show();
    $Window->Term->SetFocus();
    $Window->RunGUI->Enable(0);
    $Window->RunIndia->Enable(0);
    $Window->Stop->Enable(1);
    $Menu->{ControlRunGUI}->Enabled(0);
    $Menu->{ControlRunIndia}->Enabled(0);
    $Menu->{ControlStop}->Enabled(1);
    $Menu->{Help}->Enabled(0);
    $Menu->{Term}->Enabled(0);
    StatusBar_Update();
    }


sub Scanner_OnChargerTest
    {
    my $self = shift;
    my @busaddrs;

    if ($Window->Term->{running})
        {
        $Window->Term->{processing} = 2;
        return;
        }
    else
        {
        @busaddrs = ScanWindowText(1);
        return unless @busaddrs > 0;
        $Window->Term->{processing} = 0;
        $Window->Term->{running} = 1;
        $Window->Edit->Hide();
        $Window->Help->Hide();
        $Window->RunGUI->Hide();
        $Window->RunIndia->Hide();
        $Window->Stop->Hide();
        $Window->DoneHelp->Show();
        $Window->Term->Text('');
        $Window->Term->Show();
        $Window->Term->SetFocus();
        }

    $Window->Term->{processing} = 1;

    for my $ba (@busaddrs)
        {
        next unless $$ba{FIRST};
        $_ = $$ba{BA};

        Evil_StatusBar("\n", "%s%s", Address($_), 'd.');

        $Window->Term->Append(sprintf("%s%s", Address($_), 'g.'));
        Evil_StatusBar("\n", "%s%s", Address($_), 'g.');
        my ($seth) = $PortInput =~ /(\d+\.\d+)/;
        $Window->Term->Append(" $PortInput\r\n");

        $Window->Term->Append(sprintf("%s%s", Address($_), 'v.'));
        Evil_StatusBar("\n", "%s%s", Address($_), 'v.');
        my ($v) = $PortInput =~ /(\d+\.\d+)/;
        $Window->Term->Append(" $PortInput\r\n");

        my $trigger = sprintf("%.4f", $v - 0.100);
        $trigger =~ s/\D//g;
        $trigger =~ s/(....).*/$1/;
        $Window->Term->Append(sprintf("%s%s%s", Address($_), 'seth.', $trigger));
        Evil_StatusBar("\n", "%s%s%s", Address($_), 'seth.', $trigger);
        $Window->Term->Append(" $PortInput\r\n");
        for (0..50)
            {
            Win32::Sleep(100);
            Win32::GUI::DoEvents();
            last unless $Window->Term->{processing} == 1;
            }

        $Window->Term->Append(sprintf("%s%s%s", Address($_), 'seth.', $seth));
        Evil_StatusBar("\n", "%s%s%s", Address($_), 'seth.', $seth);
        $Window->Term->Append(" $PortInput\r\n");

        Evil_StatusBar("\n", "%s%s", Address($_), 'e.');
        for (0..10)
            {
            Win32::Sleep(100);
            Win32::GUI::DoEvents();
            last unless $Window->Term->{processing} == 1;
            }
        last if $Window->Term->{processing} == 2;
        }

    $Window->Term->Append("Done\r\n");
    Scanner_DoneHelp($self);
    $Window->Term->{processing} = 0;
    Scanner_OnTerm($self);
    }

sub Labels_Update
    {
    return unless $Running == RUNNINGGUI;
    my ($width, $height) = ($Window->Edit->Width(), $Window->Edit->Height());

    my ($left, $top);

    Visible_Update();

    for my $index (0..$#busaddrs)
        {
        my $ba = $busaddrs[$index];
        if ($Menu->{GraphLeftToRight1}->Checked)
            {
            $delta  = int($vwidth / @busaddrs);
            $left   = $vleft + $index * $delta;
            $top    = $height - 30;
            }
        elsif ($Menu->{GraphLeftToRight2}->Checked)
            {
            }
        elsif ($Menu->{GraphOverUnder1}->Checked)    # Rendered above / below, max in the middle
            {
            $delta  = int($vwidth / ((1 + @busaddrs) >> 1));
            $left   = $vleft + ($index >> 1) * $delta;
            $top = ($index & 1) ? 0 : $height - 30;
            }
        elsif ($Menu->{GraphOverUnder2}->Checked)    # Rendered above / below, max at top
            {
            $delta  = int($vwidth / ((1 + @busaddrs) >> 1));
            $left   = $vleft + ($index >> 1) * $delta;
            $top = ($index & 1) ? 0 : $height - 30;
            }
        $left += ($delta - 30) >> 1 if $delta > 30;

        $Window->{"Label_$$ba{BA}"}->Hide();
        $Window->{"Label_$$ba{BA}"}->Left($left);
        $Window->{"Label_$$ba{BA}"}->Width($delta - 2);
        $Window->{"Label_$$ba{BA}"}->Top($top);
        $Window->{"Label_$$ba{BA}"}->Change( -font => $LabelFont );
        $Window->{"Label_$$ba{BA}"}->Show();

        }

    Labels_Y_Update();
    }

sub Labels_Y_Update
    {
    return unless $Running == RUNNINGGUI;

    my $min = min($setl, $vmin) - $graphmargin;
    my $max = max($seth, $vmax) + $graphmargin;
    my $range = $max - $min;
    for (1..$YScales)
        {
        $Window->{"Label_Y_$_"}->Hide();
        $Window->{"Label_Y_$_"}->Left(0);
        $Window->{"Label_Y_$_"}->Text(sprintf("%.3fV", $max - ($_ - 1) * $range / ($YScales - 1)));
        $Window->{"Label_Y_$_"}->Top($vtop + ($_ - 1) * $vheight / ($YScales - 1));
        $Window->{"Label_Y_$_"}->Change( -font => $LabelFont );
        $Window->{"Label_Y_$_"}->Show();
        }
    }

sub Scanner_OnTerm
    {
    my $self = shift;

    if ($Window->Term->{running})
        {
        Scanner_DoneHelp($self);
        }
    else
        {
        return unless ScanWindowText(0, 0, 0);
        $Window->Term->{running} = 1;
        $Window->Edit->Hide();
        $Window->Help->Hide();
        $Window->RunGUI->Hide();
        $Window->RunIndia->Hide();
        $Window->Stop->Hide();
        $Window->DoneHelp->Show();
        $Window->Term->Show();
        $Window->Term->SetFocus();
        }
    }

sub Scanner_OnHelp
    {
    my $self = shift;
    $Window->Edit->Hide();
    $Window->RunGUI->Hide();
    $Window->RunIndia->Hide();
    $Window->Stop->Hide();
    $Window->DoneHelp->Show();
    $Window->Help->Show();
    }

sub Scanner_DoneHelp
    {
    my $self = shift;

    if ($Window->Term->{running})
        {
        if (!$Menu->{ControlSimulate}->Checked)
            {
            PortClose($EvilPortObj, OPENFAIL => {SUB => \&Evil_OpenCloseFail, PARMS => [$Window, Translate("<Error trying to close the evil port>:")]});
            }
        $Window->Term->{running} = 0;
        $Window->Term->{processing} = 0;
        }

    $Window->DoneHelp->Hide();
    $Window->Help->Hide();
    $Window->Term->Hide();
    $Window->Edit->Show();
    $Window->RunGUI->Show();
    $Window->RunText->Show();
    $Window->Stop->Show();
    }

sub NormalizeCOM
    {
    if ($_[0] =~ m(\\|(?:^N/A$))io)
        {
        $_[0] = uc($_[0]);
        }
    else
        {
        $_[0] =~ /(\d+)/;
        $_[0] = "\\\\.\\COM$1";
        }
    }

sub ApplyDefault
    {
    $_[0] = $_[1] if !defined($_[0]) || $_[0] eq '-';
    }


sub Death
    {
    warn(@_) if @_;

    # Release resources
    PortClose($EvilPortObj, Translate("<Error trying to close the evil port>")) if $EvilPortObj;

    # Pause before exiting in case this was started from a shortcut and would otherwise vanish.
    @ARGV = ();
    print(Translate("<Hit Enter to finish>\n"));
    $_ = <>;
    exit @_;
    }

sub logprint
    {
    return unless $Menu->{ControlLogging}->Checked;
    print LOG @_;
    }

sub logprintf
    {
    return unless $Menu->{ControlLogging}->Checked;
    my $fmt = shift(@_);
    printf LOG $fmt, @_;
    }

sub Evil_StatusBar
    {
    my $eos = shift;
    my $fmt = shift;
    StatusBar_Update(sprintf($fmt, @_));
    Scanner_WriteEvil($eos, $fmt, @_);
    Win32::GUI::DoEvents();
    }

sub Evil_OpenCloseFail
    {
    my $error = shift;
    my $self = shift;
    $self->MessageBox ( qq(@_ $error), $EvilPort, MB_ICONSTOP | MB_OK);
    }

sub Scanner_WriteEvil
    {
    if (!$Menu->{ControlSimulate}->Checked)
        {
        WriteEvil(@_);
        return;
        }

    my $eos = shift;
    my $format = shift;
    my $evilcommand = sprintf($format, @_);
    if ($evilcommand =~ /(\d+)v\./)
        {
        my $busaddr = $1;
        my @scales = (0.00, 0.05, 0.15, 0.25, 0.50, 0.75, 0.85, 0.95, 1.00);
        push @scales, reverse(@scales);
        my $scale = @scales[(time + $busaddr) % @scales];
        my $delta = 0.500;
        for (my $index = 0; $index < @busaddrs; ++$index)
            {
            my $ba = $busaddrs[$index];
            next unless $busaddr == $$ba{BA};
            my $indexscale = (1 + $index) / @busaddrs;
            $delta = $indexscale - 0.5;
            }

        my $low   = $setl - $delta || 1.5;
        my $high  = $seth + $delta || 4.5;
        my $volt  = $low + ($high - $low) * $scale;

        $PortInput = sprintf("%sV %5.3f", $busaddr, $volt);
        }
    elsif ($evilcommand =~ /(\d+)mi\./)
        {
        my $busaddr = $1;
        my $volt = 2.5;

        for (my $index = 0; $index < @busaddrs; ++$index)
            {
            my $ba = $busaddrs[$index];
            next unless $busaddr == $$ba{BA};

            $volt = $$ba{VMIN} || $volt;
            last;
            }

        $PortInput = sprintf("%sMI %5.3f", $busaddr, $volt);
        }
    elsif ($evilcommand =~ /(\d+)ma\./)
        {
        my $busaddr = $1;
        my $volt = 3.5;

        for (my $index = 0; $index < @busaddrs; ++$index)
            {
            my $ba = $busaddrs[$index];
            next unless $busaddr == $$ba{BA};

            $volt = $$ba{VMAX} || $volt;
            last;
            }

        $PortInput = sprintf("%sMA %5.3f", $busaddr, $volt);
        }
    elsif ($evilcommand =~ /(\d+)h\./)
        {
        $PortInput = '';
        }
    elsif ($evilcommand =~ /(\d+)r\./)
        {
        $PortInput = "$1R 2.000V";
        }
    elsif ($evilcommand =~ /(\d+)g\./)
        {
        $PortInput = "$1G 4.000V";
        }
    elsif ($evilcommand =~ /(\d+)t\./)
        {
        my $busaddr = $1;
        my $seconds = time - $TimeBase;
        my $minutes = int($seconds / 60);
        my $hours = int($minutes / 60);
        $minutes %= 60;
        $PortInput = sprintf("%sT %04u:%02u", $busaddr, $hours, $minutes);
        }
    elsif ($evilcommand =~ /(\d+)s\./)
        {
        $PortInput = "$1S 00";
        }
    elsif ($evilcommand =~ /(\d+)c\./)
        {
        $PortInput = "$1C -0037.4A";
        }
    elsif ($evilcommand =~ /(\d+)wh\./)
        {
        $PortInput = "$1WH -00594.6WH";
        }
    elsif ($evilcommand =~ /(\d+)ah\./)
        {
        $PortInput = "$1AH -048.91AH";
        }
    else
        {
        Death(Translate(qq(<Unknown simulation request>: "$evilcommand"\n)));
        }
    }

# MouseMove event
sub Window_MouseMove
    {
    return unless $Running == RUNNINGGUI;
    my ($x, $y) = @_;
    for my $index (0..$#busaddrs)
        {
        my $ba = $busaddrs[$index];
        if (exists $$ba{LEFT} && exists $$ba{RIGHT} && exists $$ba{TOP} && exists $$ba{BOTTOM})
            {
            if ($$ba{LEFT} < $x && $x < $$ba{RIGHT} &&
                $$ba{TOP}  < $y && $y < $$ba{BOTTOM})
                {
                $MouseY = $y;
                $MouseIndex = $index;
                last;
                }
            }
        }
    StatusBar_Update();
    }

sub Window_MouseDown
    {
    return unless $Running == RUNNINGGUI;
    }

sub Window_MouseUp
    {
    return unless $Running == RUNNINGGUI;
    }

sub Control_COMDetect
    {
    my @original = split(/\r\n/m, $Window->Edit->Text());
    my @new;
    for (@original)
        {
        push @new, $_ unless /^COM/i;
        }
    $Window->Edit->Text(join("\r\n", @new) . "\r\n");
    for (my $com = 4; $com <= 255; ++$com)
        {
        local($EvilPortObj);
        my $portname;
        NormalizeCOM($portname = "COM$com");
        StatusBar_Update("Testing $portname");
        Win32::GUI::DoEvents();
        $EvilPortObj = PortOpen(PORT => $portname, CRTERMINATE => 1, TIMEOUT => 50, OPENFAIL => {SUB => sub { }});
        if ($EvilPortObj)
            {
            $EvilPort = "COM$com";
            unshift @new, $EvilPort;
            $Window->Edit->Text(join("\r\n", @new) . "\r\n");
            NormalizeCOM($EvilPort);
            PortClose($EvilPortObj, Translate("<Error trying to close the detected port>"));
            }
        }

    StatusBar_Update();
    }

sub Control_COMCellDetect
    {
    my @original = split(/\r\n/m, $Window->Edit->Text());
    my @new;
    for (@original)
        {
        push @new, $_ unless /^(COM|\d)/i;
        }
    $Window->Edit->Text(join("\r\n", @new));
    $Window->Edit->Append("\r\n");
    for (my $com = 4; $com <= 255; ++$com)
        {
        local($EvilPortObj);
        my $portname;
        NormalizeCOM($portname = "COM$com");
        StatusBar_Update("Testing $portname");
        Win32::GUI::DoEvents();
        $EvilPortObj = PortOpen(PORT => $portname, CRTERMINATE => 1, TIMEOUT => 50, OPENFAIL => {SUB => sub { }});
        if ($EvilPortObj)
            {
            $Window->Edit->Append("COM$com\r\n");
            StatusBar_Update("Testing $portname");
            my $base = undef;
            my $last = undef;
            for my $busaddr (1..255)
                {
                Win32::GUI::DoEvents();
                StatusBar_Update("Testing $portname ${busaddr}v.");
                Scanner_WriteEvil("\n", "%s%s", Address($busaddr), 'v.');
                if ($PortInput =~ /\d\.\d/)
                    {
                    if (!defined($last))
                        {
                        $Window->Edit->Append($busaddr);
                        $base = $busaddr;
                        }
                    elsif ($busaddr != $last + 1)
                        {
                        $Window->Edit->Append("-$last\r\n");
                        undef $base;
                        }
                    $last = $busaddr;
                    }
                else
                    {
                    if ($last)
                        {
                        $Window->Edit->Append("-$last") if $last && $busaddr != $base + 1;
                        $Window->Edit->Append("\r\n");
                        }
                    undef $base;
                    undef $last;
                    }
                }
            $Window->Edit->Append("-$last\r\n") if $last;
            PortClose($EvilPortObj, Translate("<Error trying to close the detected port>"));
            }
        }

    StatusBar_Update();
    }

sub TimeStamp
    {
    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
    return sprintf("%04u-%02u-%02u %02u:%02u:%02u", 1900 + $year, $mon + 1, $mday, $hour, $min, $sec);
    }

sub uniquelogprint
    {
    return unless $Menu->{ControlCSV}->Checked;
    my ($busaddr, $command, $value) = @_;

    return if exists $uniquelog{"$busaddr$command"} && $uniquelog{"$busaddr$command"} eq $value;

    $uniquelog{"$busaddr$command"} = $value;
    print CSV (join(',', TimeStamp(), $value, Address($busaddr), $command), "\n");
    }

sub Address
    {
    if ($_[0] < 99)
        {
        return sprintf("%02u", $_[0]);
        }
    else
        {
        return sprintf("%03u", $_[0]);
        }
    }
