#! 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.7";
my $TimeBase = time;

my $CurrentFile;
my $EvilPort;

$CurrentFile = shift;
$EvilPort    = shift;

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

my $CSVFile = "Scanner.csv";
my $LogFile = "Scanner.txt";

# 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;

my $MaxLineWidth;
my $MinLineWidth;
my $SetlLineWidth;
my $SethLineWidth;
my $TempLineWidth;
my $XtrnLineWidth;

my $MaxLinePColor;
my $MinLinePColor;
my $SetlLinePColor;
my $SethLinePColor;
my $TempLinePColor;
my $XtrnLinePColor;

my $MaxLineBColor;
my $MinLineBColor;
my $SetlLineBColor;
my $SethLineBColor;
my $TempLineBColor;
my $XtrnLineBColor;

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 },
    " > Save &logging..."  => { -name => "ControlLogging", -checked => 0 },
    " > Save 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 },
    "&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";
my $DC = $Window->GetDC;

# 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\.]+)$/)
                {
                }
            elsif ($line =~ /^(\d+)t(?:[emperat\.]+)$/)
                {
                $Window->Term->Append(sprintf("%s %3.0fFT\r\n", $1, 70.0 + $1 / 50));
                }
            elsif ($line =~ /^(\d+)x(?:[trntemp\.]+)$/)
                {
                $Window->Term->Append(sprintf("%s %3.0fFX\r\n", $1, 60.0 + $1 / 70));
                }
            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
; This file is the GUI format file.
; See Help for file format definitions.
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 used for Run GUI and Charger Test and 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}
.) COMx
.) EMH n [C A W K V G], where n is the bus address of the EMH and one or more letters specify the EMH lines.
.) LineWidth n
.)   MaxLineWidth n
.)   MinLineWidth n
.)   SetlLineWidth n
.)   SethLineWidth n
.)   TempLineWidth n
.)   XtrnLineWidth n
.) MaxLineColor  r g b
.) MinLineColor  r g b
.) SetlLineColor r g b
.) SethLineColor r g b
.) TempLineColor r g b
.) XtrnLineColor r g b

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

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.

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 $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 $Pcyan   = new Win32::GUI::Pen([ 0,    255, 255 ]);
my $Porange = new Win32::GUI::Pen([ 255,  128,   0 ]);

my $Bwhite  = new Win32::GUI::Brush([ 255, 255, 255 ]);
my $Bred    = new Win32::GUI::Brush([ 255,   0,   0 ]);
my $Bgreen  = new Win32::GUI::Brush([   0, 255,   0 ]);
my $Bblue   = new Win32::GUI::Brush([   0,   0, 255 ]);
my $Bblack  = new Win32::GUI::Brush([   0,   0,   0 ]);
my $Bcyan   = new Win32::GUI::Brush([   0, 255, 255 ]);
my $Borange = new Win32::GUI::Brush([ 255, 128,   0 ]);

my @busaddrs;
my %emh;
my @EMHLabels = ();
my @EMHTypes =
    (
    {Legend => 'C', Evil => 'c.', Value => '([+-]\d+\.\d+)',  Format => '%.1f', Margin => 0.20},
    {Legend => 'A', Evil => 'a.', Value => '([+-]\d+\.\d+)',  Format => '%.1f', Margin => 0.20},
    {Legend => 'W', Evil => 'w.', Value => '([+-]\d+\.\d+)',  Format => '%.1f', Margin => 0.20},
    {Legend => 'K', Evil => 'k.', Value => '([+-]\d+\.\d+)',  Format => '%.1f', Margin => 0.20},
    {Legend => 'V', Evil => 'v.', Value => '(\d+\.\d+)',      Format => '%.3f', Margin => 0.20},
    {Legend => 'G', Evil => 'g.', Value => 'G\s+(\d+\.?\d*)', Format => '%.1f', Margin => 0}
    );

my $MouseIndex;
my $MouseY;
my %vbottom;
my $vmin = 2.0;
my $vmax = 4.5;
my $cmin;
my $cmax;
my $setl = 9999;
my $seth = 0;
my $tempwarm = 9999;
my $tempoff = 0;
my $xtrnwarm = 80;
my $xtrnoff = 120;
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 DrawHbar
    {
    my ($DC, $index, $min, $current, $max, $margin) = @_;

    my $delta  = int($vtop / @EMHLabels);
    my $top    = $index * $delta;
    my $bottom = $top + $delta - 2;
    my $left;
    my $right;

    $DC->SelectObject($Pwhite);
    $DC->SelectObject($Bwhite);
    $DC->Rectangle($vleft, $top, $vright, $bottom);

    # Zero margin means a simple min-max bar
    if ($margin == 0)
        {
        $left = $vleft;
        if ($max == $min) { $right = $vright; }
        else              { $right = $vleft + ($current - $min) / ($max - $min) * ($vright - $vleft); }
        $DC->SelectObject($Pblue);
        $DC->SelectObject($Bblue);
        $DC->Rectangle($left, $top, $right, $bottom);

        # Erase to the right
        $DC->SelectObject($Pwhite);
        $DC->SelectObject($Bwhite);
        $DC->Rectangle($right, $top, $vright, $bottom);
        return;
        }

    # Nonzero margin means 0.000 is in the center.
    $max = max(-$min, $max);
    $min = -$max;
    $min -= $margin;
    $max += $margin;
    if ($current < 0)
        {
        $right = $vleft + ($vright - $vleft) / 2;
        $left  = $vleft + ($current - $min) / ($max - $min) * ($vright - $vleft);
        }
    else
        {
        $left  = $vleft + ($vright - $vleft) / 2;
        $right = $vleft + ($current - $min) / ($max - $min) * ($vright - $vleft);
        }
    $DC->SelectObject($current < 0 ? $Pred : $Pgreen);
    $DC->SelectObject($current < 0 ? $Bred : $Bgreen);
    $DC->Rectangle($left, $top, $right, $bottom);
    }

sub ProcessNotRunning
    {
    Win32::Sleep(10);

    if ($Window->Term->{running})
        {
        if (!$Menu->{ControlSimulate}->Checked)
            {
            GetPortInput($EvilPortObj);
            }
        ++$Window->Term->{processing};
        if (defined($PortInput) && 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};
        }

    return 1 if $Running[0] == 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->{Help}->Enabled(1);
    $Menu->{Term}->Enabled(1);

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

    $Running[0] = STOPPED;
    StatusBar_Update();
    return 1;
    }

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

        my $command;
        my $outputfield = 0;
        while (length($line))
            {
            Win32::GUI::DoEvents();
            last unless $Running[0] == 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[0] = 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 (defined $_ && @{$_})
                        {
                        $output .= join(' ', @{$_}) . "\r\n";
                        }
                    else
                        {
                        $output .= "\r\n";
                        }
                    }
                $Window->Term->Text($output);
                }
            }
        ++$outputline;
        }
    return 1;
    }

sub ProcessEMH
    {
    for (0..$#EMHLabels)
        {
        my %row = %{$EMHLabels[$_]};
        Scanner_WriteEvil("\n", "%s%s", Address($emh{BA}), $row{Evil});
        $emh{$row{Legend}} = $1 if $PortInput =~ /$row{Value}/;
        $emh{"$row{Legend}MIN"} = min($emh{$row{Legend}}, $emh{"$row{Legend}MIN"});
        $emh{"$row{Legend}MAX"} = max($emh{$row{Legend}}, $emh{"$row{Legend}MAX"});
        DrawHbar($DC, $_, $emh{"$row{Legend}MIN"}, $emh{$row{Legend}}, $emh{"$row{Legend}MAX"}, $row{Margin});
        }

    Labels_EMH_Update();
    return 0;
    }

sub ProcessRunningGUI
    {
    my $pass = $_[0];
    my ($width, $height) = ($Window->Edit->Width(), $Window->Edit->Height());
    my $ba;

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

    Scanner_WriteEvil("\n", "%s%s", Address($_), 't.');
    ($$ba{TEMP}) = $1 if $PortInput =~ /(\d+)F/;
    uniquelogprint($_, 't.', $1);
    Scanner_WriteEvil("\n", "%s%s", Address($_), 'x.');
    ($$ba{XTRN}) = $1 if $PortInput =~ /(\d+)F/;
    uniquelogprint($_, 'x.', $1);


    StatusBar_Update();

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

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

        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, @temp, @xtrn);
        my ($left, $right, $bottom, $min, $max);

        $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 + $MaxLineWidth]);
        @min    = ([$left,         $min], [$right,         $min + $MinLineWidth]);
        @setl   = ([$left, $vtop + barheight($vheight, $$ba{SETL})], [$right, $vtop + barheight($vheight, $$ba{SETL}) + $SetlLineWidth]);
        @seth   = ([$left, $vtop + barheight($vheight, $$ba{SETH})], [$right, $vtop + barheight($vheight, $$ba{SETH}) + $SethLineWidth]);

        my $tempsetl = ${$setl[0]}[1];
        my $tempseth = ${$seth[0]}[1];
        my $tempscaled = ($$ba{TEMP} - $tempwarm) * ($tempsetl - $tempseth) / ($tempoff - $tempwarm);
        @temp   = ([$left, $tempsetl - $tempscaled], [$right, $tempsetl - $tempscaled + $TempLineWidth]);

        my $xtrnsetl = ${$setl[0]}[1];
        my $xtrnseth = ${$seth[0]}[1];
        my $xtrnscaled = ($$ba{XTRN} - $xtrnwarm) * ($xtrnsetl - $xtrnseth) / ($xtrnoff - $xtrnwarm);
        @xtrn   = ([$left, $xtrnsetl - $xtrnscaled], [$right, $xtrnsetl - $xtrnscaled + $XtrnLineWidth]);

        $$ba{TOP}    = $vtop;
        $$ba{BOTTOM} = $vbottom;

        $$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($MaxLinePColor);      # Draw the max line
        $DC->SelectObject($MaxLineBColor);
        $DC->Rectangle(@{$max[0]}, @{$max[1]});

        $DC->SelectObject($MinLinePColor);      # Draw the min line
        $DC->SelectObject($MinLineBColor);
        $DC->Rectangle(@{$min[0]}, @{$min[1]});

        $DC->SelectObject($SetlLinePColor);     # Draw the low line
        $DC->SelectObject($SetlLineBColor);
        $DC->Rectangle(@{$setl[0]}, @{$setl[1]});

        $DC->SelectObject($SethLinePColor);     # Draw the high line
        $DC->SelectObject($SethLineBColor);
        $DC->Rectangle(@{$seth[0]}, @{$seth[1]});

        if ($vtop <= ${$temp[0]}[1] && ${$temp[0]}[1] <= $vbottom)
            {
            $DC->SelectObject($TempLinePColor);      # Draw the temp. line if it's on the screen
            $DC->SelectObject($TempLineBColor);
            $DC->Rectangle(@{$temp[0]}, @{$temp[1]});
            }

        if ($vtop <= ${$xtrn[0]}[1] && ${$xtrn[0]}[1] <= $vbottom)
            {
            $DC->SelectObject($XtrnLinePColor);      # Draw the xtrntemp line if it's on the screen
            $DC->SelectObject($XtrnLineBColor);
            $DC->Rectangle(@{$xtrn[0]}, @{$xtrn[1]});
            }
        }
    }

sub TestLoop
    {
    my $index = 0;
    my $testminmax = 0;
    for (my $pass = 0; ; ++$pass)
        {
        Win32::GUI::DoEvents();

        if ($Running[0] != RUNNINGGUI && $Running[0] != RUNNINGINDIA)
            {
            next if ProcessNotRunning();
            }

        if ($Running[0] == RUNNINGINDIA)
            {
            next if ProcessIndia();
            }

        Visible_Update();

        if (%emh)
            {
            next if ProcessEMH();
            }

        ProcessRunningGUI($pass);
        }
    }

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

    $vleft   = 100;
    $vtop    = %emh ? 25 * @EMHLabels : 30;
    $vright  = $width;
    $vbottom = $height - 30;
    $vwidth  = $vright - $vleft;
    $vheight = $vbottom - $vtop;

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

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 = "";
  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;
     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 => "mk3s",
                   -createprompt => 1,
                   );

  # Save file
  if (defined $file) {
    # Keep Current file.
    $CurrentFile = $file;
    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[0] == 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)
    if ($Menu->{ControlLogging}->Checked)
        {
        my $newfile = Win32::GUI::GetSaveFileName(
                   -owner  => $Window,
                   -title  => "Save Log",
                   -file   => $LogFile,
                   -filter => [
                       'Text file (*.txt)' => '*.txt',
                       'All files' => '*.*',
                    ],
                   -defaultextension => "txt",
                   -createprompt => 1,
                   );

        if (!defined $newfile)
            {
            $Menu->{ControlLogging}->Checked(0);
            return;
            }
        $LogFile = $newfile;

        if (!open(LOG, '>', $_ = $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)
    %uniquelog = ();
    if ($Menu->{ControlCSV}->Checked)
        {
        my $newfile = Win32::GUI::GetSaveFileName(
                   -owner  => $Window,
                   -title  => "Save CSV",
                   -file   => $CSVFile,
                   -filter => [
                       'Text file (*.csv)' => '*.csv',
                       'All files' => '*.*',
                    ],
                   -defaultextension => "csv",
                   -createprompt => 1,
                   );

        if (!defined $newfile)
            {
            $Menu->{ControlLogging}->Checked(0);
            return;
            }
        $CSVFile = $newfile;

        if (!open(CSV, '>', $_ = $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 min
    {
    my $retval = shift;
    for (@_) { $retval = $_ if defined($_) && $_ < $retval; }
    $retval;
    }

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

sub Scanner_Stop
    {
    my $self = shift;

    $Running[0] = STOPPING;
    }

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

    $MaxLineWidth  = 2;
    $MinLineWidth  = 2;
    $SetlLineWidth = 2;
    $SethLineWidth = 2;
    $TempLineWidth = 8;
    $XtrnLineWidth = 8;

    $MaxLinePColor  = $Pblue;
    $MinLinePColor  = $Pblue;
    $SetlLinePColor = $Pred;
    $SethLinePColor = $Pred;
    $TempLinePColor = $Porange;
    $XtrnLinePColor = $Pcyan;

    $MaxLineBColor  = $Bred;
    $MinLineBColor  = $Bred;
    $SetlLineBColor = $Bblue;
    $SethLineBColor = $Bblue;
    $TempLineBColor = $Borange;
    $XtrnLineBColor = $Bcyan;


    @busaddrs = ();
    %emh = ();
    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 (/((Max|Min|Setl|Seth|Temp|Xtrn)LineColor)\s+(\d+)[,\s]+(\d+)[,\s]+(\d+)\s*$/io)
            {
            my $colors = [$3,$4,$5];
            my $var = "\L$2";
            eval "\$\u${var}LinePColor = new Win32::GUI::Pen  ($colors)";
            eval "\$\u${var}LineBColor = new Win32::GUI::Brush($colors)";
            }

        elsif (/((Max|Min|Setl|Seth|Temp|Xtrn)LineWidth)\s+(\d+)\s*$/io)
            {
            my $var = "\L$2";
            eval "\$\u${var}LineWidth = $3";
            }

        elsif (/LineWidth\s+(\d+)\s*$/io)
            {
            $MaxLineWidth  =
            $MinLineWidth  =
            $SetlLineWidth =
            $SethLineWidth =
            $TempLineWidth =
            $XtrnLineWidth = $1;
            }

        elsif (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 ();
                }
            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 ();
                    }
                $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 ();
                }
            $Menu->{ControlSimulate}->Checked($simulate =~ /On/i);
            }
        elsif (my ($graphtype) = /^Graph\s+(\d+)\s*/i)
            {
            # This is ignored for historical compatibility
            }
        elsif (/^(?:EMH|SOC)\s+(\d+)\s*(.*)$/i)
            {
            my $busaddr = $1;
            my $legend  = $2;
            $emh{BA} = $1;
            $emh{GMIN} = 0;
            $emh{GMAX} = 100;
            if ($legend)
                {
                my $legends = join('', map { ${$EMHTypes[$_]}{Legend} } (0..$#EMHTypes));
                @EMHLabels = ();
                for ($legend =~ /\s/ ? split(/\s+/, $legend) : split(//, $legend))
                    {
                    for (my $i = 0; ; ++$i)
                        {
                        if ($i > $#EMHTypes)
                            {
                            $Window->MessageBox ( qq(Expected one of "$legends" instead of "$_" in line $line), "Scanner", MB_ICONSTOP | MB_OK);
                            return ();
                            }
                        if ("\U$_" eq "\U${$EMHTypes[$i]}{Legend}")
                            {
                            push @EMHLabels, $EMHTypes[$i];
                            last;
                            }
                        }
                    }
                }
            else
                {
                @EMHLabels = @EMHTypes;
                }

            for (0..$#EMHLabels)
                {
                if (!exists($Window->{"Label_EMH_$_"}))
                    {
                    my $letter = ${$EMHLabels[$_]}{Legend};
                    $Window->AddLabel(
                        -name      => "Label_EMH_$_",
                        -text      => "$letter",
                        -pos       => [0, 0],
                        -multiline => 0,
                        -hscroll   => 0,
                        -vscroll   => 0,
                        -autohscroll => 0,
                        -autovscroll => 0,
                        -font => $LabelFont,
                        -visible => 0,
                    );
                    }
                }

            }
        elsif (/^COM\d+$/i)
            {
            NormalizeCOM($EvilPort = $_);
            StatusBar_Update();
            }
        else
            {
            $Window->MessageBox ( qq(Unknown command "$_" in line $line), "Scanner", MB_ICONSTOP | MB_OK);
            return ();
            }
        }

    if ($requireaddrs)
        {
        if (@busaddrs == 0)
            {
            $Window->MessageBox ( qq(Missing bus address range), "Scanner", MB_ICONSTOP | MB_OK);
            return ();
            }
        }

    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 () 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;
    $tempoff = 0;
    $tempwarm = 9999;
    for my $ba (@busaddrs)
        {
        $_ = $$ba{BA};
        Evil_StatusBar("\n", "%s%s", Address($_), 'mi.');
        ($$ba{VMIN}) = $PortInput =~ /(\d+\.\d+)/;
        uniquelogprint($_, 'mi.', $1);
        SaveMin($$ba{VMIN});
        Evil_StatusBar("\n", "%s%s", Address($_), 'v.');
        ($$ba{V}) = $PortInput =~ /(\d+\.\d+)/;
        uniquelogprint($_, 'v.', $1);
        Evil_StatusBar("\n", "%s%s", Address($_), 'ma.');
        ($$ba{VMAX}) = $PortInput =~ /(\d+\.\d+)/;
        uniquelogprint($_, 'ma.', $1);
        SaveMax($$ba{VMAX});
        Evil_StatusBar("\n", "%s%s", Address($_), 'r.');
        ($$ba{SETL}) = $PortInput =~ /(\d+\.\d+)/;
        uniquelogprint($_, 'r.', $1);
        $setl = min($setl, $$ba{SETL});
        SaveMin($setl);
        Evil_StatusBar("\n", "%s%s", Address($_), 'g.');
        ($$ba{SETH}) = $PortInput =~ /(\d+\.\d+)/;
        uniquelogprint($_, 'g.', $1);
        $seth = max($seth, $$ba{SETH});
        SaveMax($seth);

        Evil_StatusBar("\n", "%s%s", Address($_), 't.');
        ($$ba{TEMP}) = $PortInput =~ /(\d+)F/;
        uniquelogprint($_, 't.', $1);
        Evil_StatusBar("\n", "%s%s", Address($_), 'x.');
        ($$ba{XTRN}) = $PortInput =~ /(\d+)F/;
        uniquelogprint($_, 'x.', $1);

        Evil_StatusBar("\n", "%s%s", Address($_), 'tempoff.');
        ($$ba{TEMPOFF}) = $PortInput =~ /(\d+)F/;
        uniquelogprint($_, 'tempoff.', $1);
        $tempoff = max($tempoff, $$ba{TEMPOFF});
        Evil_StatusBar("\n", "%s%s", Address($_), 'tempwarm.');
        ($$ba{TEMPWARM}) = $PortInput =~ /(\d+)F/;
        uniquelogprint($_, 'tempwarm.', $1);
        $tempwarm = min($tempoff, $$ba{TEMPWARM});

        $$ba{TEMPWARM}-- if $$ba{TEMPOFF} == $$ba{TEMPWARM};

        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[0] = 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[0] = 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[0] == RUNNINGGUI;
    my ($width, $height) = ($Window->Edit->Width(), $Window->Edit->Height());

    my ($left, $top);

    Visible_Update();

    for my $index (0..$#busaddrs)
        {
        my $ba = $busaddrs[$index];
        $delta  = int($vwidth / @busaddrs);
        $left   = $vleft + $index * $delta;
        $top    = $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();
    Labels_EMH_Update();
    }

sub Labels_Y_Update
    {
    return unless $Running[0] == 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 Labels_EMH_Update
    {
    return unless %emh && $Running[0] == RUNNINGGUI;

    my $delta  = int($vtop / @EMHLabels);

    for (0..$#EMHLabels)
        {
        my %row = %{$EMHLabels[$_]};
        my $value = '';
        if (exists($emh{$row{Legend}}))
            {
            $value = sprintf($row{Format}, $emh{$row{Legend}});
            }

        $Window->{"Label_EMH_$_"}->Left(0);
        $Window->{"Label_EMH_$_"}->Width($vleft);
        $Window->{"Label_EMH_$_"}->Top($_ * $delta);
        $Window->{"Label_EMH_$_"}->Text("$row{Legend}$value");
        $Window->{"Label_EMH_$_"}->Change( -font => $LabelFont );
        $Window->{"Label_EMH_$_"}->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->RunIndia->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 @scales = (0.00, 0.05, 0.15, 0.25, 0.50, 0.75, 0.85, 0.95, 1.00);
    push @scales, reverse(@scales);

    my $eos = shift;
    my $format = shift;
    my $evilcommand = sprintf($format, @_);
    Win32::Sleep(length($evilcommand));
    if ($evilcommand =~ /(\d+)v\./)
        {
        my $busaddr = $1;
        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\./ && (!%emh || $1 != $emh{BA}))
        {
        $PortInput = " $1G 4.000V";
        }
    elsif ($evilcommand =~ /(\d+)tempoff\./)
        {
        $PortInput = " $1TO 180F";
        }
    elsif ($evilcommand =~ /(\d+)tempwarm\./)
        {
        $PortInput = " $1TW 150F";
        }
    elsif ($evilcommand =~ /(\d+)t\./ && %emh && $1 == $emh{BA})
        {
        my $busaddr = $1;
        my $seconds = time - $TimeBase;
        my $minutes = int($seconds / 60);
        $seconds %= 60;
        $minutes = min(9999, $minutes);
        $PortInput = sprintf(" %sT %04u:%02u", $busaddr, $minutes, $seconds);
        }
    elsif ($evilcommand =~ /(\d+)t\./ && (!%emh || $1 != $emh{BA}))
        {
        my $busaddr = $1;
        my $scale = @scales[(time + $busaddr) % @scales];

        my $low   = $tempwarm - 10;
        my $high  = $tempoff + 10;
        my $temp  = $low + ($high - $low) * $scale;

        $PortInput = sprintf(" %sT %3.0fF", $busaddr, $temp);
        }
    elsif ($evilcommand =~ /(\d+)x\./ && (!%emh || $1 != $emh{BA}))
        {
        my $busaddr = $1;
        my $scale = @scales[(time + $busaddr + 1) % @scales];

        my $low   = $xtrnwarm - 10;
        my $high  = $xtrnoff + 10;
        my $temp  = $low + ($high - $low) * $scale;

        $PortInput = sprintf(" %sX %3.0fF", $busaddr, $temp);
        }
    elsif ($evilcommand =~ /(\d+)s\./)
        {
        $PortInput = " $1S 00";
        }
    elsif ($evilcommand =~ /(\d+)c\./)
        {
        my $busaddr = $1;
        my $scale = 2 * (@scales[(time + 0) % @scales] - 0.50);
        my $reply = $scale * 9999.9;
        $PortInput = sprintf(" %sC +%06.1fA", $busaddr, $reply);
        }
    elsif ($evilcommand =~ /(\d+)wh?\./)
        {
        my $busaddr = $1;
        my $scale = 2 * (@scales[(time + 1) % @scales] - 0.50);
        my $reply = $scale * 99999.9;
        $PortInput = sprintf(" %sC +%07.1fWH", $busaddr, $reply);
        }
    elsif ($evilcommand =~ /(\d+)ah?\./)
        {
        my $busaddr = $1;
        my $scale = 2 * (@scales[(time + 2) % @scales] - 0.50);
        my $reply = $scale * 999.99;
        $PortInput = sprintf(" %sC +%06.2fAH", $busaddr, $reply);
        }
    elsif ($evilcommand =~ /(\d+)k\./)
        {
        my $busaddr = $1;
        my $scale = 2 * (@scales[(time + 3) % @scales] - 0.50);
        my $reply = $scale * 999.9;
        $PortInput = sprintf(" %sC +%05.1fKW", $busaddr, $reply);
        }
    elsif ($evilcommand =~ /(\d+)ga?\./ && %emh && $1 == $emh{BA})
        {
        my $busaddr = $1;
        my $scale = @scales[(time + 4) % @scales];
        my $reply = $scale * 100.0;
        $PortInput = $reply == 100 ? " ${busaddr}G 100%" : sprintf(" %sG %04.1f%%", $busaddr, $reply);
        }
    else
        {
        Death(Translate(qq(<Unknown simulation request>: "$evilcommand"\n)));
        }

    Win32::Sleep(length($PortInput));
    }

# MouseMove event
sub Window_MouseMove
    {
    return unless $Running[0] == 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[0] == RUNNINGGUI;
    }

sub Window_MouseUp
    {
    return unless $Running[0] == 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]);
        }
    }
