Browse Source

flashmax.pl: allow --setver alone, add --which command

Allow --setver to be used without a firmware file.
Add heuristic searching for esptool(.py).
Add --which command to detect where esptool comes from.

Signed-off-by: H. Peter Anvin <hpa@zytor.com>
H. Peter Anvin 1 year ago
parent
commit
87f5b21766
1 changed files with 538 additions and 339 deletions
  1. 538 339
      tools/flashmax.pl

+ 538 - 339
tools/flashmax.pl

@@ -2,16 +2,15 @@
 
 use strict;
 use integer;
+use IO::Handle;
 use PerlIO::gzip;
 use File::Temp;
 use File::Spec;
+use File::HomeDir;
 use Time::HiRes qw(usleep tv_interval);
 use Digest::CRC qw(crc32);
 use v5.10;			# For "state"
 
-my $esptool = 'esptool';
-$esptool = $ENV{'ESPTOOL'} || $esptool;
-
 my $esp_retries = 10;
 
 my @FW_MAGIC = (undef, 0x7a07fbd6, 0xa924ed0b);
@@ -120,8 +119,10 @@ sub unquote_cmd($) {
     my @a;
 
     $s =~ s/[\r\n]+/ /g;
-    while ($s =~ /^\s*(?:\"((?:[^\"]+|\"\")*)\"|(\S+))(\s.*)?$/) {
-	push(@a, $1.$2);
+    while ($s =~ /^\s*(?:\"((?:[^\"]+|\"\")*)\"|([^\"]\S*))(\s.*)?$/) {
+	my $a = $1;
+	$a =~ s/\"\"/\"/g;
+	push(@a, $a.$2);
 	$s = $3;
     }
 
@@ -145,12 +146,120 @@ sub hash2opt($)
     return map { $h->{$_} ne '' ? ('--'.$_, $h->{$_}) : () } sort keys %{$h};
 }
 
+my $esptool;
+
+# Try running esptool --help and look for a version string
+sub try_esptool($) {
+    use Data::Dump 'pp';
+
+    my($cmd) = @_;
+
+    return undef if ($cmd eq '');
+
+    my @espcmd = unquote_cmd($cmd);
+
+    # Make sure stderr is unbuffered
+    STDERR->autoflush(1);
+
+    open(my $old_stderr, '>&', \*STDERR) or die;
+    $old_stderr->autoflush(1);
+
+    open(STDERR, '>', File::Spec->devnull()) or return undef;
+    STDERR->autoflush(1);
+
+    my $ver;
+    my $espok = open(my $esp, '-|', @espcmd, '--help');
+    if ($espok) {
+	while (defined(my $l = <$esp>)) {
+	    if (!defined($ver) &&
+		$l =~ /\besptool\S*\s+(v\S+)/) {
+		$ver = $1;
+	    }
+	}
+	close($esp);
+    }
+
+    open(STDERR, '>&', $old_stderr) or die;
+    close($old_stderr);
+
+    if ($ver) {
+	$esptool = $cmd;
+    }
+    return $ver;
+}
+
+sub updir($) {
+    my($dir) = @_;
+    return File::Spec->catdir($dir, File::Spec->updir());
+}
+
+sub find_esptool() {
+    my $ver;
+    my $python = '"' . ($ENV{'PYTHON'} || 'python') . '"';
+
+    # The easy variants
+    foreach my $et ($ENV{'ESPTOOL'}, 'esptool', 'esptool.py') {
+	next unless ($et ne '');
+	$ver = try_esptool($et);
+	return $ver if ($ver);
+    }
+
+    # More complicated
+    foreach my $p ($ENV{'ESPTOOL'}, File::Spec->path()) {
+	next unless ($p ne '');
+	next unless ( -d $p );
+	my $et = File::Spec->catfile($p, 'esptool.py');
+	if ( -f $et ) {
+	    $ver = try_esptool("${python} \"${et}\"");
+	    return $ver if ($ver);
+	}
+    }
+
+    # Try to find it in an Arduino directory
+    foreach my $dp (File::HomeDir->my_data,
+		    File::HomeDir->my_documents,
+		    updir(File::HomeDir->my_data),
+		    File::HomeDir->my_home) {
+	next unless ( -d $dp );
+	foreach my $dn ('.arduino15', 'Arduino15', 'ArduinoData') {
+	    my $etr = File::Spec->catdir($dp, $dn,
+					 qw(packages esp32 tools esptool_py));
+	    opendir(my $dh, $etr) or next;
+	    while (defined(my $sd = readdir($dh))) {
+		next if ($sd eq File::Spec->curdir);
+		next if ($sd eq File::Spec->updir);
+		my $etd = File::Spec->catdir($etr, $sd);
+		next unless ( -d $etd );
+
+		my $esppath = File::Spec->catfile($etd, 'esptool');
+		if ( ! -d $esppath ) {
+		    $ver = try_esptool("\"$esppath\"");
+		    last if ($ver);
+		}
+
+		$esppath .= '.py';
+		if ( -f $esppath ) {
+		    $ver = try_esptool("${python} \"$esppath\"");
+		    last if ($ver);
+		}
+	    }
+	    closedir($dh);
+
+	    return $ver if ($ver);
+	}
+    }
+
+    return $ver;
+}
+
 sub run_esptool($$$$@)
 {
     my($port,$common_options,$cmd,$cmd_options,@args) = @_;
 
     my @espcmd = unquote_cmd($esptool);
-    push(@espcmd, '--port', $port);
+    if (defined($port) && $common_options->{'port'} ne '') {
+	push(@espcmd, '--port', $port);
+    }
     push(@espcmd, hash2opt($common_options));
     push(@espcmd, unquote_cmd($cmd));
     push(@espcmd, hash2opt($cmd_options));
@@ -171,7 +280,8 @@ sub run_esptool($$$$@)
 	my $esp;
 	if (!open($esp, '-|', @espcmd)) {
 	    print STDERR $!, "\n";
-	    exit 1;
+	    exit 1 if (defined($port));
+	    return undef;
 	}
 
 	while (defined(my $l = <$esp>)) {
@@ -198,7 +308,12 @@ sub run_esptool($$$$@)
 
     print STDERR @output;
 
-    die "$0: $espcmd[0] $cmd failed\n" unless ($ok);
+    if (!$ok) {
+	if (defined($port)) {
+	    die "$0: $espcmd[0] $cmd failed\n";
+	}
+	return undef;
+    }
     return %outinfo;
 }
 
@@ -260,21 +375,36 @@ sub match_version($$)
 my %espopt = ('before' => 'default_reset', 'after' => 'hard_reset',
 	      'connect-attempts' => 8);
 
+my $tmpdir;
+# Get a filename in $tmpdir
+sub tmpfilename($) {
+    my($filename) = @_;
+
+    if (!defined($tmpdir)) {
+	die "$0: tmpfilename() called before tmpdir exists\n";
+    }
+
+    return File::Spec->catfile($tmpdir, $filename);
+}
 
-sub get_target_board($$)
+sub get_target_board($)
 {
-    my($port,$boardinfo) = @_;
+    my($port) = @_;
+
+    my $boardinfo = tmpfilename('boardinfo.bin');
 
     my %myespopt = (%espopt);
     $myespopt{'after'} = 'no_reset';
-    run_esptool($port, \%myespopt,
+    run_esptool($port, { hgrep {!/^flash_/} \%myespopt },
 		'read_flash', { },
-		''.$boardinfo_addr, ''.$boardinfo_len, $boardinfo);
+		sprintf('0x%x', $boardinfo_addr),
+		$boardinfo_len, $boardinfo);
 
     open(my $bi, '<:raw', $boardinfo) or return undef;
     my $bid;
     read($bi, $bid, $boardinfo_len);
     close($bi);
+    unlink($boardinfo);
     return undef if (length($bid) != $boardinfo_len);
 
     my @bh = unpack('VVVVZ[256]', $bid);
@@ -293,250 +423,126 @@ sub get_target_board($$)
     return undef;
 }
 
-my @args = @ARGV;
-my $esponly = 0;
-my $file;
-my $target_board = undef;
-my $setver = 0;
-
-while (1) {
-    $file = shift(@args);
-    last if ($file !~ /^\-/);
-
-    if ($file eq '--esponly') {
-	$esponly = 1;
-    } elsif ($file eq '--setver') {
-	$target_board = shift(@args);
-	$setver = defined($target_board);
-    } elsif ($file eq '--') {
-	$file = shift(@args);
-	last;
-    } else {
-	undef $file;		# Invalid argument, print usage
-	last;
-    }
-}
-
-my $port = shift(@args);
-if (!defined($port)) {
-    die "Usage: $0 [--esponly][--setver versionoptions] file.fw port [esptool_options...]\n";
-}
-
-if (!File::Spec->file_name_is_absolute($port)) {
-    if (-c "/dev/$port") {
-	$port = "/dev/$port";
-    } elsif (-c "/dev/tty$port") {
-	$port = "/dev/tty$port";
-    } elsif ($^O eq 'MSWin32') {
-	$port = "\\\\.\\$port";
-    } else {
-	die "$0: no such serial port: $port\n";
-    }
-}
-print STDERR "Using serial port device $port\n";
-
-my $td = File::Temp->newdir(CLEANUP => 1);
-my $boardinfo = File::Spec->catfile($td, 'boardinfo.bin');
-
-my @espfiles = ();
-
-if (defined($target_board)) {
-    # Forcibly write target board version
-    if (!target_string_valid($target_board)) {
-	die "$0: $port: invalid firmware target string: $target_board\n";
-    }
-
-    open(my $bi, '>:raw', $boardinfo)
-	or die "$0: $port: $boardinfo: $!\n";
-    my $bid = $target_board . "\0";
-    $bid .= "\xff" x ($boardinfo_len - length($bid));
-    print $bi $bid;
-    close($bi);
-    push(@espfiles, ''.$boardinfo_addr, $boardinfo);
-} else {
-    # Get the board version from target flash
+sub read_fwfile($$) {
+    my($file, $target_board) = @_;
 
-    $target_board = get_target_board($port, $boardinfo);
-    if (!defined($target_board) || !target_string_valid($target_board)) {
-	die "$0: $port: board version not programmed, specify with --setver\n";
-    }
-}
+    return undef if (!defined($file));
 
-open(my $fw, '<:gzip', $file)
-    or die "$0: $file: $!\n";
+    open(my $fw, '<:gzip', $file)
+	or die "$0: $file: $!\n";
 
-my @chunks = ();
-my $version_match = undef;
+    my @chunks = ();
+    my $version_match = undef;
 
-my $err = 0;
+    my $err = 0;
 
-my $hdr;
-while (read($fw, $hdr, 16) == 16) {
-    # magic type flags data_len addr
-    my @h = unpack('VvvVV', $hdr);
-    my $c = { 'hdr' => $hdr, 'magic' => $h[0], 'type' => $h[1],
-	      'flags' => $h[2], 'len' => $h[3], 'addr' => $h[4],
-	      'vmatch' => 0, 'vmask' => 0, 'vmin' => 0, 'vmax' => 0xffff };
-    if ($c->{'magic'} == $FW_MAGIC[2]) {
-	if (read($fw, $hdr, 16) != 16) {
-	    print STDERR "$0: $file: short header read\n";
+    my $hdr;
+    while (read($fw, $hdr, 16) == 16) {
+	# magic type flags data_len addr
+	my @h = unpack('VvvVV', $hdr);
+	my $c = { 'hdr' => $hdr, 'magic' => $h[0], 'type' => $h[1],
+		      'flags' => $h[2], 'len' => $h[3], 'addr' => $h[4],
+		      'vmatch' => 0, 'vmask' => 0, 'vmin' => 0, 'vmax' => 0xffff };
+	if ($c->{'magic'} == $FW_MAGIC[2]) {
+	    if (read($fw, $hdr, 16) != 16) {
+		print STDERR "$0: $file: short header read\n";
+		$err = 1;
+		last;
+	    }
+	    my @h = unpack('VVvvV', $hdr);
+	    $c->{'hdr'}   .= $hdr;
+	    $c->{'vmatch'} = $h[0];
+	    $c->{'vmask'}  = $h[1];
+	    $c->{'vmin'}   = $h[2];
+	    $c->{'vmax'}   = $h[3];
+	} elsif ($c->{'magic'} != $FW_MAGIC[1]) {
+	    print STDERR "$0: $file: bad chunk magic\n";
 	    $err = 1;
 	    last;
 	}
-	my @h = unpack('VVvvV', $hdr);
-	$c->{'hdr'}   .= $hdr;
-	$c->{'vmatch'} = $h[0];
-	$c->{'vmask'}  = $h[1];
-	$c->{'vmin'}   = $h[2];
-	$c->{'vmax'}   = $h[3];
-    } elsif ($c->{'magic'} != $FW_MAGIC[1]) {
-	print STDERR "$0: $file: bad chunk magic\n";
-	$err = 1;
-	last;
-    }
-
-    my $t = $type[$c->{'type'}];
 
-    my $d;
-    if (read($fw, $d, $c->{'len'}) != $c->{'len'}) {
-	print STDERR "$0: $file: short chunk read\n";
-	$err = 1;
-	last;
-    }
-    $c->{'data'} = $d;
+	my $t = $type[$c->{'type'}];
 
-    if ($t eq 'target') {
-	my $is_match = match_version($target_board, $d);
-	if ($c->{'magic'} == $FW_MAGIC[1] || $is_match) {
-	    $version_match = $c;
-	}
-	print STDERR "$0: $file: supports hardware: $d",
-	    ($is_match ? ' (match)' : ''), "\n";
-    } elsif ($t eq 'end' || $t eq 'note' || ($c->{'flags'} & $FDF_PRETARGET)) {
-	# Not filtered
-    } else {
-	if (!defined($version_match)) {
-	    print STDERR "$0: $file: hardware version $target_board not supported\n";
+	my $d;
+	if (read($fw, $d, $c->{'len'}) != $c->{'len'}) {
+	    print STDERR "$0: $file: short chunk read\n";
 	    $err = 1;
 	    last;
 	}
+	$c->{'data'} = $d;
 
-	if ($c->{'vmin'} > $version_match->{'vmax'} ||
-	    $c->{'vmax'} < $version_match->{'vmin'} ||
-	    (($c->{'vmatch'} ^ $version_match->{'vmatch'}) & $c->{'vmask'})) {
-	    # Not applicable to this board
-	    next;
+	if ($t eq 'target') {
+	    my $is_match = match_version($target_board, $d);
+	    if ($c->{'magic'} == $FW_MAGIC[1] || $is_match) {
+		$version_match = $c;
+	    }
+	    print STDERR "$0: $file: supports hardware: $d",
+		($is_match ? ' (match)' : ''), "\n";
+	} elsif ($t eq 'end' || $t eq 'note' || ($c->{'flags'} & $FDF_PRETARGET)) {
+	    # Not filtered
+	} else {
+	    if (!defined($version_match)) {
+		print STDERR "$0: $file: hardware version $target_board not supported\n";
+		$err = 1;
+		last;
+	    }
+
+	    if ($c->{'vmin'} > $version_match->{'vmax'} ||
+		$c->{'vmax'} < $version_match->{'vmin'} ||
+		(($c->{'vmatch'} ^ $version_match->{'vmatch'}) & $c->{'vmask'})) {
+		# Not applicable to this board
+		next;
+	    }
 	}
-    }
 
-    push(@chunks, $c);
+	push(@chunks, $c);
 
-    last if ($t eq 'end'); # End of stream
-}
+	last if ($t eq 'end'); # End of stream
+    }
 
-close($fw);
-exit $err if ($err);
+    close($fw);
 
-%espopt = (%espopt,
-	   'baud' => 115200, 'port' => undef, 'chip' => undef,
-	   'flash_mode' => undef, 'flash_freq' => undef,
-	   'flash_size' => undef);
+    exit 1 if ($err);
+
+    # Sort out the ESP chunks as files for esptool versus the ones
+    # that should be uploaded once the ESP software is running
+    my @espfiles;
+    my $raw_fpgadata;
 
-# Create a compressed data buffer without the ESP32 chunks
-my $fpgadata;
-open(my $fpgafh, '>:gzip', \$fpgadata) or die;
-
-my $nc = 0;
-foreach my $c ( @chunks ) {
-    my $t = $type[$c->{'type'}];
-    if ($t eq 'esptool') {
-	my $s = $c->{'data'};
-	$s =~ s/[\r\n]+/ /g;
-	while ($s =~ /^\s*(\S+)\s+(\S+)(.*)/) {
-	    $espopt{$1} = $2;
-	    $s = $3;
+    my $nc = 0;
+    foreach my $c ( @chunks ) {
+	my $t = $type[$c->{'type'}];
+	if ($t eq 'esptool') {
+	    my $s = $c->{'data'};
+	    $s =~ s/[\r\n]+/ /g;
+	    while ($s =~ /^\s*(\S+)\s+(\S+)(.*)/) {
+		$espopt{$1} = $2;
+		$s = $3;
+	    }
+	} elsif ($t =~ /^esp/ && $c->{'addr'}) {
+	    my $addr = sprintf('%x', $c->{'addr'});
+	    my $tff = tmpfilename($t.$nc.'_'.$addr.'.bin');
+	    open(my $tf, '>', $tff) or die "$0: $tff: $!\n";
+	    print $tf $c->{'data'};
+	    close($tf);
+	    push(@espfiles, [$c->{'addr'}, $tff]);
+	} elsif ($t eq 'note' || ($t eq 'target' && $c != $version_match)) {
+	    # Skip
+	} else {
+	    $raw_fpgadata .= $c->{'hdr'};
+	    $raw_fpgadata .= $c->{'data'};
 	}
-    } elsif ($t =~ /^esp/ && $c->{'addr'}) {
-	my $addr = sprintf('%x', $c->{'addr'});
-	my $tff = File::Spec->catfile($td, $t.$nc.'_'.$addr.'.bin');
-	open(my $tf, '>', $tff) or die "$0: $tff: $!\n";
-	print $tf $c->{'data'};
-	close($tf);
-	push(@espfiles, '0x'.$addr, $tff);
-    } elsif ($t eq 'note' || ($t eq 'target' && $c != $version_match)) {
-	# Skip
-    } else {
-	print $fpgafh $c->{'hdr'};
-	print $fpgafh $c->{'data'};
+	$nc++;
     }
-    $nc++;
-}
 
-close($fpgafh);
-
-foreach my $e (keys %espopt) {
-    my $ev = $ENV{"ESP\U$e"};
-    if (defined($ev)) {
-	$espopt{$e} = $ev;
+    my $fpgadata;
+    if (defined($raw_fpgadata)) {
+	open(my $fpgafh, '>:gzip', \$fpgadata) or die;
+	print $fpgafh $raw_fpgadata;
+	close($fpgafh);
     }
-}
 
-run_esptool($port, { hgrep {!/^flash_/} %espopt },
-	    'write_flash', { hgrep {/^flash_/} %espopt },
-	    '-z', @espfiles);
-
-my $SerialPort = eval {
-    require Device::SerialPort;
-    Device::SerialPort->import(qw(:PARAM));
-    'Device::SerialPort';
-} || eval {
-    require Win32::SerialPort;
-    Win32::SerialPort->import(qw(:STAT));
-    'Win32::SerialPort';
-} || die "$0: need Device::SerialPort or Win32::SerialPort\n";
-
-print STDERR "Waiting for reinit...\n";
-usleep(4000000);
-
-my $tty = $SerialPort->new($port);
-die "$0: $port: $!\n" if (!$tty);
-
-$tty->buffers($tty->buffer_max);
-$tty->reset_error;
-
-$tty->user_msg(1);
-$tty->error_msg(1);
-
-$tty->handshake('none');
-$tty->databits(8);
-$tty->baudrate(115200);
-$tty->stopbits(1);
-$tty->parity('none');
-$tty->stty_istrip(0);
-$tty->stty_inpck(0);
-$tty->datatype('raw');
-$tty->stty_icanon(0);
-$tty->stty_opost(0);
-$tty->stty_ignbrk(0);
-$tty->stty_inlcr(0);
-$tty->stty_igncr(0);
-$tty->stty_icrnl(0);
-$tty->stty_echo(0);
-$tty->stty_echonl(0);
-$tty->stty_isig(0);
-$tty->read_char_time(0);
-$tty->write_settings;
-
-# In case DTR and/or RTS was asserted on the physical serial port.
-# Note that DTR needs to be deasserted before RTS!
-# However, deasserting DTR on the ACM port prevents it from working,
-# so only do this if we don't see CTS (which is always enabled on ACM)...
-if ($tty->can_modemlines && !($tty->modemlines & $tty->MS_CTS_ON)) {
-    usleep(100000);
-    $tty->dtr_active(0);
-    usleep(100000);
-    $tty->rts_active(0);
-    usleep(100000);
+    return ([@espfiles], $fpgadata);
 }
 
 sub tty_read {
@@ -559,7 +565,6 @@ sub tty_read {
 
     return $r;
 }
-my $ttybuf = '';
 
 sub tty_write($$) {
     my($tty,$data) = @_;
@@ -576,139 +581,333 @@ sub tty_write($$) {
     return $offs;
 }
 
-my $found = 0;
-my $tt = time();
-my $start_enq = $tt;
-tty_write($tty, "\005");	# ENQ
-while (($tt = time()) - $start_enq < 30) {
-    my $d = tty_read($tty, \$ttybuf, 1000);
-    if ($d eq '') {
-	tty_write($tty, "\005");	# ENQ
-    } else {
-	my $ix = index("\026\004\027", $d);
-	if ($ix < 0) {
-	    print STDERR $d;
-	    next;
+sub upload_fpgadata($$) {
+    my($port, $fpgadata) = @_;
+
+    my $SerialPort = eval {
+	require Device::SerialPort;
+	Device::SerialPort->import(qw(:PARAM));
+	'Device::SerialPort';
+    } || eval {
+	require Win32::SerialPort;
+	Win32::SerialPort->import(qw(:STAT));
+	'Win32::SerialPort';
+    } || die "$0: need Device::SerialPort or Win32::SerialPort\n";
+
+    print STDERR "Waiting for reinit...\n";
+    usleep(4000000);
+
+    my $tty = $SerialPort->new($port);
+    die "$0: $port: $!\n" if (!$tty);
+
+    $tty->buffers($tty->buffer_max);
+    $tty->reset_error;
+
+    $tty->user_msg(1);
+    $tty->error_msg(1);
+
+    $tty->handshake('none');
+    $tty->databits(8);
+    $tty->baudrate(115200);
+    $tty->stopbits(1);
+    $tty->parity('none');
+    $tty->stty_istrip(0);
+    $tty->stty_inpck(0);
+    $tty->datatype('raw');
+    $tty->stty_icanon(0);
+    $tty->stty_opost(0);
+    $tty->stty_ignbrk(0);
+    $tty->stty_inlcr(0);
+    $tty->stty_igncr(0);
+    $tty->stty_icrnl(0);
+    $tty->stty_echo(0);
+    $tty->stty_echonl(0);
+    $tty->stty_isig(0);
+    $tty->read_char_time(0);
+    $tty->write_settings;
+
+    # In case DTR and/or RTS was asserted on the physical serial port.
+    # Note that DTR needs to be deasserted before RTS!
+    # However, deasserting DTR on the ACM port prevents it from working,
+    # so only do this if we don't see CTS (which is always enabled on ACM)...
+    if ($tty->can_modemlines && !($tty->modemlines & $tty->MS_CTS_ON)) {
+	usleep(100000);
+	$tty->dtr_active(0);
+	usleep(100000);
+	$tty->rts_active(0);
+	usleep(100000);
+    }
+
+    my $ttybuf = '';
+
+
+    my $found = 0;
+    my $tt = time();
+    my $start_enq = $tt;
+    tty_write($tty, "\005");	# ENQ
+    while (($tt = time()) - $start_enq < 30) {
+	my $d = tty_read($tty, \$ttybuf, 1000);
+	if ($d eq '') {
+	    tty_write($tty, "\005");	# ENQ
+	} else {
+	    my $ix = index("\026\004\027", $d);
+	    if ($ix < 0) {
+		print STDERR $d;
+		next;
+	    }
+	    $found = $found == $ix ? $found+1 : 0;
+	    last if ($found == 3);
 	}
-	$found = $found == $ix ? $found+1 : 0;
-	last if ($found == 3);
     }
-}
 
-if ($found < 3) {
-    die "$0: $port: no MAX80 ESP32 detected\n";
-}
+    if ($found < 3) {
+	die "$0: $port: no MAX80 ESP32 detected\n";
+    }
 
-$start_enq = $tt;
-my $last_req;
-my $winspc;
-while (!defined($winspc)) {
-    $tt = time();
-    if ($tt - $start_enq >= 10) {
-	die "$0: $port: failed to start FPGA firmware upload\n";
+    $start_enq = $tt;
+    my $last_req;
+    my $winspc;
+    while (!defined($winspc)) {
+	$tt = time();
+	if ($tt - $start_enq >= 10) {
+	    die "$0: $port: failed to start FPGA firmware upload\n";
+	}
+	if ($tt != $last_req) {
+	    tty_write($tty, "\034\001: /// MAX80 FW UPLOAD \~\@\~ \$\r\n\035");
+	    $last_req = $tt;
+	}
+	my $d;
+	while (1) {
+	    $d = tty_read($tty, \$ttybuf, 100);
+	    last if ($d eq '');
+	    my $dc = unpack('C', $d);
+	    print STDERR $a[$dc];
+	    if ($dc == 036) {
+		$winspc = 0;
+		last;
+	    }
+	}
     }
-    if ($tt != $last_req) {
-	tty_write($tty, "\034\001: /// MAX80 FW UPLOAD \~\@\~ \$\r\n\035");
-	$last_req = $tt;
+
+    my $bytes  = length($fpgadata);
+    my $offset = 0;
+    my $maxchunk = 64;
+    my $maxahead = 256;
+    my $last_ack = 0;
+    my @pktends  = ();
+    my $last_enq = 0;
+    my $last_ack_time = 0;
+
+    print STDERR "\nStarting packet transmit...\n";
+
+    while ($last_ack < $bytes) {
+	my $chunk;
+	my $now;
+
+	while (1) {
+	    $chunk = $bytes - $offset;
+	    $chunk = $winspc if ($chunk > $winspc);
+	    if ($offset + $chunk > $last_ack + $maxahead) {
+		$chunk = $last_ack + $maxahead - $offset;
+	    }
+	    $chunk = 0 if ($chunk <= 0);
+	    $chunk = $maxchunk if ($chunk > $maxchunk);
+
+	    my $d = tty_read($tty, \$ttybuf, $chunk ? 0 : $maxchunk/10);
+	    $now = time();
+	    last if ($d eq '');
+
+	    my $dc = unpack('C', $d);
+	    if ($dc == 022) {
+		$winspc = 0;
+	    } elsif ($dc == 024) {
+		if (defined($winspc)) {
+		    $winspc += 256;
+		}
+	    } elsif ($dc == 006) {
+		$last_ack = shift(@pktends) || $last_ack;
+		if ($now != $last_ack_time) {
+		    printf STDERR "%s: %s: %d/%d (%d%%) uploaded\n",
+			$0, $port, $last_ack, $bytes, $last_ack*100/$bytes;
+		    $last_ack_time = $now;
+		}
+	    } else {
+		print STDERR $a[$dc];
+		if ($dc == 025 || $dc == 031 || $dc == 037) {
+		    $offset = $last_ack;
+		    @pktends = ();
+		    undef $winspc;	# Wait for WRST before resuming
+		} elsif ($dc == 030) {
+		    print STDERR "\n";
+		    die "$0: $port: upload aborted by target system\n";
+		}
+	    }
+	}
+
+	if (!$chunk) {
+	    if ($bytes > $offset) {
+		if ($now != $last_enq) {
+		    tty_write($tty, "\026"); # SYN: request window resync
+		    $last_enq = $now;
+		}
+	    }
+	    next;
+	}
+
+	my $data = substr($fpgadata, $offset, $chunk);
+	my $hdr = pack("CvVV", $chunk-1, 0, $offset, crc32($data));
+
+	tty_write($tty, "\002".mybaseencode($hdr, $data));
+
+	push(@pktends, $offset + $chunk);
+	$offset += $chunk;
+	$winspc -= $chunk;
     }
-    my $d;
+
+    tty_write($tty, "\004");		# EOT
+
+    # Final messages out
     while (1) {
-	$d = tty_read($tty, \$ttybuf, 100);
+	my $d = tty_read($tty, \$ttybuf, 1000);
 	last if ($d eq '');
-	my $dc = unpack('C', $d);
-	print STDERR $a[$dc];
-	if ($dc == 036) {
-	    $winspc = 0;
-	    last;
-	}
+	print STDERR $d;
     }
+    $tty->close;
+
+    return 0;
 }
 
-my $bytes  = length($fpgadata);
-my $offset = 0;
-my $maxchunk = 64;
-my $maxahead = 256;
-my $last_ack = 0;
-my @pktends  = ();
-my $last_enq = 0;
-my $last_ack_time = 0;
+my @args = @ARGV;
+my $esponly = 0;
+my $file;
+my $target_board = undef;
+my $setver = 0;
+my $port = undef;
+my $which = 0;
 
-print STDERR "\nStarting packet transmit...\n";
+while (1) {
+    $file = shift(@args);
+    last if ($file !~ /^\-/);
 
-while ($last_ack < $bytes) {
-    my $chunk;
-    my $now;
+    if ($file eq '--esponly') {
+	$esponly = 1;
+    } elsif ($file eq '--which') {
+	$which = 1;
+    } elsif ($file eq '--setver') {
+	$target_board = shift(@args);
+	$setver = defined($target_board);
+    } elsif ($file eq '--port') {
+	$port = shift(@args);
+    } elsif ($file eq '--') {
+	$file = shift(@args);
+	last;
+    } else {
+	undef $file;		# Invalid argument, print usage
+	last;
+    }
+}
 
-    while (1) {
-	$chunk = $bytes - $offset;
-	$chunk = $winspc if ($chunk > $winspc);
-	if ($offset + $chunk > $last_ack + $maxahead) {
-	    $chunk = $last_ack + $maxahead - $offset;
-	}
-	$chunk = 0 if ($chunk <= 0);
-	$chunk = $maxchunk if ($chunk > $maxchunk);
+# Legacy command line support: if no --port, specify port after firmware file
+if (defined($file) && !defined($port) && $args[0] ne '--esptool') {
+    $port = shift(@args);
+}
 
-	my $d = tty_read($tty, \$ttybuf, $chunk ? 0 : $maxchunk/10);
-	$now = time();
-	last if ($d eq '');
+$tmpdir = File::Temp->newdir(CLEANUP => 1);
+if (!defined($tmpdir)) {
+    die "$0: failed to create temporary directory: $!\n"
+}
 
-	my $dc = unpack('C', $d);
-	if ($dc == 022) {
-	    $winspc = 0;
-	} elsif ($dc == 024) {
-	    if (defined($winspc)) {
-		$winspc += 256;
-	    }
-	} elsif ($dc == 006) {
-	    $last_ack = shift(@pktends) || $last_ack;
-	    if ($now != $last_ack_time) {
-		printf STDERR "%s: %s: %d/%d (%d%%) uploaded\n",
-		    $0, $port, $last_ack, $bytes, $last_ack*100/$bytes;
-		$last_ack_time = $now;
-	    }
-	} else {
-	    print STDERR $a[$dc];
-	    if ($dc == 025 || $dc == 031 || $dc == 037) {
-		$offset = $last_ack;
-		@pktends = ();
-		undef $winspc;	# Wait for WRST before resuming
-	    } elsif ($dc == 030) {
-		print STDERR "\n";
-		die "$0: $port: upload aborted by target system\n";
-	    }
-	}
+my $espver = find_esptool();
+if (!$espver) {
+    die "$0: cannot find esptool, please set ESPTOOL in the environment\n";
+}
+
+if ($which) {
+    print "esptool ${espver}\n";
+    print $esptool, "\n";
+    exit 0;
+}
+
+print STDERR "esptool v${espver} found\n\n";
+
+if (!defined($port)) {
+    die "Usage: $0 [--which][--esponly][--setver version][--port port]\n".
+	"       [file.fw] [--esptool esptool_options...]\n";
+}
+
+if (!File::Spec->file_name_is_absolute($port)) {
+    if (-c "/dev/$port") {
+	$port = "/dev/$port";
+    } elsif (-c "/dev/tty$port") {
+	$port = "/dev/tty$port";
+    } elsif ($^O eq 'MSWin32') {
+	$port = "\\\\.\\$port";
+    } else {
+	die "$0: no such serial port: $port\n";
     }
+}
+print STDERR "Using serial port device $port\n";
 
-    if (!$chunk) {
-	if ($bytes > $offset) {
-	    if ($now != $last_enq) {
-		tty_write($tty, "\026"); # SYN: request window resync
-		$last_enq = $now;
-	    }
-	}
-	next;
+%espopt = (%espopt,
+	   'baud' => 115200, 'port' => undef, 'chip' => undef,
+	   'flash_mode' => undef, 'flash_freq' => undef,
+	   'flash_size' => undef);
+
+foreach my $e (keys %espopt) {
+    my $ev = $ENV{"ESP\U$e"};
+    if (defined($ev)) {
+	$espopt{$e} = $ev;
+    }
+}
+
+while (defined(my $eo = shift(@args))) {
+    if ($eo =~ /^([^=]+)=(.*)$/) {
+	$espopt{$1} = $2;
+    } elsif ($args[0] !~ /^-/) {
+	$espopt{$eo} = shift(@args);
+    } else {
+	$espopt{$eo} = '';
     }
+}
 
-    my $data = substr($fpgadata, $offset, $chunk);
-    my $hdr = pack("CvVV", $chunk-1, 0, $offset, crc32($data));
+my @espfiles = ();
 
-    tty_write($tty, "\002".mybaseencode($hdr, $data));
+if (defined($target_board)) {
+    # Forcibly write target board version
+    if (!target_string_valid($target_board)) {
+	die "$0: $port: invalid firmware target string: $target_board\n";
+    }
 
-    push(@pktends, $offset + $chunk);
-    $offset += $chunk;
-    $winspc -= $chunk;
+    my $boardinfo = tmpfilename('boardinfo.bin');
+    open(my $bi, '>:raw', $boardinfo)
+	or die "$0: $port: $boardinfo: $!\n";
+    my $bid = $target_board . "\0";
+    $bid .= "\xff" x ($boardinfo_len - length($bid));
+    print $bi $bid;
+    close($bi);
+    push(@espfiles, [$boardinfo_addr, $boardinfo]);
+} else {
+    # Get the board version from target flash
+
+    $target_board = get_target_board($port);
+    if (!defined($target_board) || !target_string_valid($target_board)) {
+	die "$0: $port: board version not programmed, specify with --setver\n";
+    }
 }
 
-tty_write($tty, "\004");		# EOT
+my($fwfiles, $fpgadata) = read_fwfile($file, $target_board);
+push(@espfiles, @$fwfiles);
 
-# Final messages out
-while (1) {
-    my $d = tty_read($tty, \$ttybuf, 1000);
-    last if ($d eq '');
-    print STDERR $d;
+if (scalar(@espfiles)) {
+    run_esptool($port, { hgrep {!/^flash_/} %espopt },
+		'write_flash', { hgrep {/^flash_/} %espopt },
+		'-z', map { (sprintf('0x%x', $_->[0]), $_->[1]) } @espfiles);
+}
+
+if (defined($fpgadata)) {
+    upload_fpgadata($port, $fpgadata);
 }
-$tty->close;
 
-print STDERR "\n$0: $port: firmware upload complete\n";
+print STDERR "\n$0: $port: update complete\n";
 
 exit 0;