#!/usr/bin/perl -w

use threads;
use Thread::Queue;

use strict;
use Time::HiRes qw(time);
use Getopt::Std;

my $version      = "1.2.0";
my ($executable) = $0 =~ /([^\/\\]+)$/;
my $helpstr      = "$executable [-d <serial port>] (-r <backup file> [-s <start adress> -e <end address>] | -w <flash file> [-b <ucl file>])\n";

$|++; # disable buffering

BEGIN {
    if( index($^O, 'Win') >= 0 ) {   # MSWin32 (and not darwin, cygwin, ...)
        eval("require Win32::SerialPort") or die "Error: Failed dependencies!\nPlease install Win32::SerialPort via ppm or CPAN!\n";
        import  Win32::SerialPort;
    } else {
        eval("require Device::SerialPort") or die "Error: Failed dependencies!\nPlease install Device::SerialPort via CPAN!\n";
        import  Device::SerialPort;
    }
}

# Starting thread that asynchronously prints the progress lines
# dequeue() blocks until new data arrives in the queue, so we don't have to care about anything
my $printqueue = Thread::Queue->new();
my $printthread = threads->create( sub { while (my $line = $printqueue->dequeue()) { print $line } });
# instantly detach printing thread, as we never wanna get data back from it 
$printthread->detach();

sub formatstr($) {
	$_ = shift;
	s/([^\d\w :#\-\+])/'<#'.ord($1).'>'/ge;
	s/<#13>/<#13>\n/g;
	$_;
}

print <<WARR_END;

$executable Ver $version

*** No Warranty
***
*** This program is distributed in the hope that it will be useful,
*** but is provided AS IS with ABSOLUTELY NO WARRANTY;
*** The entire risk as to the quality and performance
*** of the program is with you. Should the program prove defective,
*** you assume the cost of all necessary servicing, repair or correction.
*** In no event will any of the developers, or any other party,
*** be liable to anyone for damages arising out of the use or inability
*** to use the program.

WARR_END

my ($serial, $filehnd, $test, $line, %opts);
my ($dev, $backupfilename, $germsfilename, $uclfilename, $startaddr, $endaddr);
my $ostype = $^O;

# predefine parameters here. command line parameters always have precedence
$dev            = $ostype =~ /Win/ ? 'COM1' : '/dev/ttyUSB0';
$startaddr      = 0x40000;
$endaddr        = 0x7FFFFF;

getopts('d:r:w:b:s:e:', \%opts) or die $helpstr;

$dev            = $opts{d} if $opts{d};
$backupfilename = $opts{r} if $opts{r};
$germsfilename  = $opts{w} if $opts{w};
$uclfilename    = $opts{b} if $opts{b};
$startaddr      = hex($opts{s}) if $opts{s};
$endaddr        = hex($opts{e}) if $opts{e};

die "Error: Not enough or wrong parameters!\n$helpstr" unless $dev and ($backupfilename or $germsfilename or $uclfilename);
#die "Error: please use either backup or write, not both at once!\n" if $backupfilename and ($germsfilename or $uclfilename);

print  "Device         : $dev\n"             if $dev;
print  "Backup filename: $backupfilename\n"  if $backupfilename;
print  "Flash filename : $germsfilename\n"   if $germsfilename;
print  "UCL filename   : $uclfilename\n"     if $uclfilename;
printf "Start address  : %08x\n", $startaddr if $startaddr and $backupfilename;
printf "End address    : %08x\n", $endaddr   if $endaddr and $backupfilename;

die "\nError: '$germsfilename' not found!\n" if ($germsfilename && ! -f $germsfilename);
die "\nError: '$uclfilename' not found!\n"   if ($uclfilename && ! -f $uclfilename);

if ($ostype =~ /Win/) {
	$serial = Win32::SerialPort->new($dev);
} else {
	$serial = Device::SerialPort->new($dev);
}
die ("Error: can't open serial port. Typo? (e.g. COM1, /dev/ttyUSB0)\n") unless ref $serial;

$serial->baudrate(115200);
$serial->databits(8);
$serial->parity('none');
$serial->stopbits(1);
$serial->purge_all();
$serial->rts_active(0);
$serial->dtr_active(0);
$serial->read_const_time(1000);
if ($ostype =~ /Win/) {
	$serial->read_interval(100);
	$serial->read_char_time(5);
}
$serial->write_settings or die ("Error: can't init serial port.\n");

# -------------------------------------------------------------------------------------

if ($backupfilename) {
	my ($starttime, $count, $cur) = (time, 0, 0);

#	$startaddr = $startaddr ? hex($startaddr) : 0x40000;
#	$endaddr = $endaddr ? hex($endaddr) : 0x7fffff;
	die "Error: Start address has to be smaller than end address!\n" if $startaddr >= $endaddr;
	$printqueue->enqueue(sprintf "\n--- Backing up firmware from %08x to %08x...\n", $startaddr, $endaddr);
	my ($step, $steps) = (0, int(($endaddr - $startaddr) / 0x100) + 1);
	open($filehnd, '>', $backupfilename) or die "Error: Couldn't create/rewrite file '$backupfilename'!\n";
	print $filehnd "#############################################\n# $executable Ver $version\n";
	print $filehnd "# Download Flash from: " . localtime() . "\n";
	printf $filehnd "# Addr: 0x%08x to 0x%08x\n#############################################\nr0\n", $startaddr, $endaddr;
	my $estart = int($startaddr / 0x10000) * 0x10000;
	while ($estart < $endaddr) {
		printf $filehnd "e%06x\n", $estart;
		$estart += 0x10000;
	} 
	my ($firmware, $elapsedtime);
	while ($startaddr < $endaddr) {
		my $span = $startaddr + 0x100 < $endaddr ? 0x100 : $endaddr - $startaddr;
		$printqueue->enqueue(sprintf "Reading %08x-%08x ", $startaddr, $startaddr+$span);
		$serial->write(sprintf "M%08x-%08x\r", $startaddr, $startaddr+$span); # append a CR 
		# waiting for response
		my ($readcount, $buffer) = (0, '');
#		my $estbufsize = 	19 +						# "Mstart-end\r"
#					5 * int(($span + 1) / 2) +			# 5 * count of 4-byte-values
#					12 * int(($span + 16) / 16) + #12 * ($span % 16) +	# 12 * count of lines
#					1;						# 1 for the plus sign
#		print " $span - $estbufsize\n";
# works not very well, so we fall back to 852, what's the size of a 256-byte-buffer
		my $estbufsize = 852;
#		my $estbufsize = 13332; # for $span = 0x1000
		while ($buffer !~ /\+/) {
			my (undef, $str) = $serial->read($estbufsize);
			$buffer = "$buffer$str";
			$printqueue->enqueue('.');
			if ($readcount++ > 10) {
				$buffer = formatstr($buffer);
				die <<RTOEND;
\nError: Timeout while reading from DSO!
Response was: '$buffer'
Firmware readout was NOT successful!
Please fix the connection issue and retry!
RTOEND
			}
		}
		$startaddr += $span;
		$firmware = "$firmware$buffer";

		my $factor = ++$step / $steps;
		$elapsedtime = time - $starttime;
		my $esttime = ($elapsedtime / $factor) - $elapsedtime;
		my $speed = 0;
		$speed = (0.01 * length($firmware) / $elapsedtime) if ($elapsedtime > 0); # factor 0.01 because of kbps and startbit/stopbit => 10 Bit/Byte
                $printqueue->enqueue(" OK - " . sprintf("%.1f", $factor * 100) . "% - " . sprintf("%.1f",$elapsedtime) .
					" sec / " . sprintf("%.0f",$esttime) . " sec left / interface-speed: ".sprintf("%.2f",$speed) . " kbps        \r");
	}
	$printqueue->enqueue("Successfully read out firmware in " . sprintf("%.1f",$elapsedtime) . " seconds! Writing firmware to $backupfilename...                   \n");
	for (split /\r/, $firmware) {					# line-wise
		next unless /^#/;
		next if /FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF/;
		chomp;
		my ($address, @data) = split;
		$address =~ s/[#:]//g;
		my ($dcount, $idx, $chksum) = qw(5 0 0);		# $dcount = 5 -> 4 addr bytes + 1 chksum byte
		for (@data) {						# endianess and strtoint...
			s/([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})/$2$1/;
			$_ = hex;
			$dcount += 2;
		}
		my $bytestr = sprintf "%02X%08X"."%04X"x@data, $dcount, hex($address), @data;
		while ($idx < length($bytestr)) {
			$chksum += hex(substr($bytestr, $idx, 2)) & 0xff;
			$idx += 2;
		}
		printf $filehnd "S3%s%02X\n", $bytestr, ~$chksum & 0xff;
	}
	print $filehnd "r0\n";
	close($filehnd);

}

if ($germsfilename) {
	my ($starttime, $count, $cur) = (time, 0, 0);

	open($filehnd, $germsfilename) or die "Error: can't open '$germsfilename' or file not found.\n";
	my @temp = <$filehnd>;
	close($filehnd);

	$count = @temp;
	my $elapsedtime;

	# detect #UCL comment
	for (@temp) {
		if (/^#\s?UCL"(.+)"/) {
			$uclfilename = $1;
			die "\nError: '$uclfilename' not found!\n"   unless -f $uclfilename;
			$printqueue->enqueue("\n#UCL detected! Automagically chaining UCL file $uclfilename.\n");
		}
	}

	$printqueue->enqueue("\n--- Writing GERMS firmware...\n");

	LINE: for $line (@temp) {

		$cur++;

		$line =~ s/[\r\n]//g; # get rid of any CR and LF
                $printqueue->enqueue(sprintf "Writing line %06u of %06u: ", $cur, $count);
		if ($line =~ /^#.*/) {
			$printqueue->enqueue("Comment not transferred.\r");
			next;
		}

		my $writecount = 0;
	SERWRT:	while (1) {
			$serial->write("$line\r"); # append a CR 
			
			if ($line =~ /^S([789])/) {
				$printqueue->enqueue("S$1 detected, end of GERMS transmission.\n");
				last LINE;
			}

			# waiting for response
			my ($readcount, $buffer) = (0, '');
			while ($buffer !~ /[+!]/) {
				my (undef, $str) = $serial->read(length($line) + 2); # $line + "\r" + "+"
				$buffer = "$buffer$str";
				$printqueue->enqueue('.');
				if ($readcount++ > 10) {
					$buffer = formatstr($buffer);
					die <<TOEND;
\nError: Timeout while reading response from DSO!
Response was: '$buffer'
Firmware update was NOT successful!
Please fix the connection issue and retry!
TOEND
				}
			}

			if ($buffer =~ /^$line\r\+$/) {
				last SERWRT;
			} else {
				$printqueue->enqueue("X");
				if ($writecount++ > 10) {
					die <<WEEND;
\nError: Unknown transmission error.
Firmware update was NOT successful!
Please fix the issue and retry!
WEEND
				}
			}
		}
		my $factor = $cur / $count;
		$elapsedtime = time - $starttime;
		my $esttime = ($elapsedtime / $factor) - $elapsedtime;
                $printqueue->enqueue(" OK - " . sprintf("%.1f", $factor * 100) . "% - " . sprintf("%.1f",$elapsedtime) .
					" sec / " . sprintf("%.0f",$esttime) . " sec left  \r");
	}
	$printqueue->enqueue("Successfully wrote GERMS firmware in " . sprintf("%.1f", $elapsedtime) . " seconds!                    \n");
	$printqueue->enqueue("Please reboot DSO if it didn't restart automatically.\n") unless $uclfilename;
}

if ($uclfilename) {
	my ($starttime, $count, $cur) = (time, 0, 0);
	my ($elapsedtime, $buffer);

	my $filesize = -s $uclfilename;
	my (undef, $buffersize) = $serial->buffer_max;		# returns max buffer sizes (dummy under POSIX, returns always 4096, 4096 there)
	my $chunk = $buffersize > 4096 ? 4096 : $buffersize;	# max chunk size shall be 4096, and 4096 shall be the size of the chunks
	$count = int(($filesize + $chunk - 1) / $chunk);	# how many fractions

	open($filehnd, $uclfilename) or die "Error: can't open '$uclfilename' or file not found.\n";
	binmode $filehnd;
	
	$printqueue->enqueue("\n--- Writing compressed firmware ($filesize bytes / $count chunks of $chunk bytes)...\n");

	# Sucking old GERMS prompt out of the RX buffer after GERMS flash, as otherwise the DSO's RX buffer seems to overflow and that
	# leads to a reboot of the DSO whilst UCL flash. Maybe we should investigate this further...
	if ($germsfilename) {
		while (1) {
			my ($count_in, $str) = $serial->read(1000);	# 1000 bytes should be enough...
			last if $count_in < 1000;			# read less bytes than 1000 -> OK
		}
	}

	while (read ($filehnd, $buffer, $chunk)) {
		$cur++;
		$printqueue->enqueue("Writing chunk $cur of $count - ");
		$serial->write($buffer);
		$serial->write_drain unless $ostype =~ /Win/;	# waiting for buffer draining, as Linux serial->write doesn't block
		my $factor = $cur / $count;
		$elapsedtime = time - $starttime;
		my $esttime = ($elapsedtime / $factor) - $elapsedtime;
		$printqueue->enqueue(sprintf("%.1f", $factor * 100) . "% - " . sprintf("%.1f",$elapsedtime) .
					" sec / " . sprintf("%.0f",$esttime) . " sec left  \r");
	}
	close($filehnd);

	# waiting for response: a single character when done
	my $readcount = 0;
	$buffer = '';
	while ($buffer !~ /[0-9]/) {
		my (undef, $str) = $serial->read(1); # error code '0' for OK, 1-9 for error
		$buffer .= $str;
		if ($readcount++ > 10) {
			die <<TOEND;
\nError: Timeout while reading response from DSO!
Firmware update was NOT successful!
Please fix the connection issue and retry!
TOEND
		}
	}
	
	if ($buffer ne '0') {
			die <<TOEND;
\nError: bad response from DSO!
Error response was: '$buffer'
Firmware update was NOT successful!
TOEND
	}

	$elapsedtime = time - $starttime;
	$printqueue->enqueue("Successfully wrote compressed firmware in " . sprintf("%.1f", $elapsedtime) . " seconds!                    \n" .
				"Please reboot DSO if it didn't restart automatically.\n");
}

$printqueue->enqueue("\nREADY!\nThanks for all the fish.\n");

