123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962 |
- #!/usr/bin/perl
- 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 $esp_retries = 10;
- my @FW_MAGIC = (undef, 0x7a07fbd6, 0xa924ed0b);
- my %datatypes = (
- 'end' => 0, # End of data
- 'data' => 1, # FPGA flash data
- 'target' => 2, # Firmware target string
- 'note' => 3, # Informative string
- 'espota' => 4, # ESP32 OTA image
- 'fpgainit' => 5, # FPGA bypass (transient) image during update
- 'esppart' => 6, # ESP32 partition table
- 'espsys' => 7, # ESP32 boot loader, OTA control partition...
- 'esptool' => 8, # esptool options for flashing
- 'boardinfo' => 9 # board_info base address
- );
- my @type;
- foreach my $t (keys(%datatypes)) {
- $type[$datatypes{$t}] = $t;
- }
- my $FDF_OPTIONAL = 0x0001;
- my $FDF_PRETARGET = 0x0002;
- my $STRING_MAX_LEN = 4095;
- my $boardinfo_addr = 0;
- my $boardinfo_len = 4096;
- # For debug; DC1-4 replaced with functional names
- my @ascii = qw(NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI
- DLE XON WRST XOFF WGO NAK SYN ETB CAN EM SUB ESC FS GS RS US);
- foreach my $i (9, 10, 13, 32..126) {
- $ascii[$i] = chr($i);
- }
- $ascii[127] = 'DEL';
- for (my $i = 128; $i < 256; $i++) {
- $ascii[$i] = sprintf("%02X", $i);
- }
- my @a = map { length($_) == 1 ? $_ : '<'.$_.'>' } @ascii;
- # Simple base64 encode using 3F-7E, bytewise bigendian
- sub mybaseencode {
- my $nbits = 0;
- my $bitbuf = 0;
- my $out = '';
- foreach my $s (@_) {
- foreach my $c (unpack('C*', $s)) {
- $nbits += 8;
- $bitbuf = ($bitbuf << 8) + $c;
- while ($nbits >= 6) {
- $nbits -= 6;
- $out .= pack('C', 63 + (($bitbuf >> $nbits) & 63));
- }
- }
- }
- if ($nbits) {
- $out .= pack('C', 63 + ($bitbuf & ((1 << $nbits) - 1)));
- }
- return $out;
- }
- sub getint($) {
- my($s) = @_;
- return undef
- unless ($s =~ /^(([1-9][0-9]+)|(0(x[0-9a-f]+|[0-7]*)))([kmgtpe]?)$/i);
- my $o = oct($3) + $2;
- my $p = lc($5);
- if ($p eq 'k') {
- $o <<= 10;
- } elsif ($p eq 'm') {
- $o <<= 20;
- } elsif ($p eq 'g') {
- $o <<= 30;
- } elsif ($p eq 't') {
- $o <<= 40;
- } elsif ($p eq 'p') {
- $o <<= 50;
- } elsif ($p eq 'e') {
- $o <<= 60;
- }
- return $o;
- }
- sub match_version($$) {
- my($ver,$pattern) = @_;
- return 1; # FIX THIS
- }
- sub filelen($) {
- my($f) = @_;
- my @s = stat($f);
- return $s[7];
- }
- sub unquote_cmd($) {
- my($s) = @_;
- my @a;
- $s =~ s/[\r\n]+/ /g;
- while ($s =~ /^\s*(?:\"((?:[^\"]+|\"\")*)\"|([^\"]\S*))(\s.*)?$/) {
- my $a = $1;
- $a =~ s/\"\"/\"/g;
- push(@a, $a.$2);
- $s = $3;
- }
- return @a;
- }
- # Similar to grep, but for a hash; also filters out
- sub hgrep(&%) {
- my($mfunc, %hash) = @_;
- return map { $_ => $hash{$_} } grep(&$mfunc, keys %hash);
- }
- # Wrapper for running esptool, returns a hash with info output
- # or dies on failure
- sub hash2opt($)
- {
- my($h) = @_;
- return () unless (defined($h));
- return map { $h->{$_} ne '' ? ('--'.$_, $h->{$_}) : () } sort keys %{$h};
- }
- my $esptool;
- # Try running esptool --help and look for a version string
- sub try_esptool($) {
- 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);
- push(@espcmd, hash2opt($common_options));
- push(@espcmd, unquote_cmd($cmd));
- push(@espcmd, hash2opt($cmd_options));
- push(@espcmd, @args);
- my $retries = $esp_retries;
- my $ok = 0;
- my %outinfo;
- my @output;
- while (!$ok && $retries--) {
- %outinfo = ();
- @output = ();
- print STDERR 'Command: ', join(' ', @espcmd), "\n";
- print STDERR "Running $espcmd[0] $cmd... ";
- my $esp;
- if (!open($esp, '-|', @espcmd)) {
- print STDERR $!, "\n";
- exit 1 if (defined($port));
- return undef;
- }
- while (defined(my $l = <$esp>)) {
- if ($l =~ /^Chip is (\S+)/) {
- $outinfo{'chip'} = $1;
- } elsif ($l =~ /^MAC: ([0-9a-f:]+)/) {
- $outinfo{'mac'} = $1;
- }
- push(@output, $l);
- }
- $ok = close($esp);
- if ($ok) {
- print STDERR "ok\n";
- last;
- } elsif ($retries) {
- @output = ();
- print STDERR "failed, retrying\n";
- usleep(1000000);
- } else {
- print STDERR "failed, giving up\n";
- }
- }
- print STDERR @output;
- if (!$ok) {
- if (defined($port)) {
- die "$0: $espcmd[0] $cmd failed\n";
- }
- return undef;
- }
- return %outinfo;
- }
- sub target_string_valid($)
- {
- my($v) = @_;
- return $v =~ /^(\S+) v((?:0|[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*)*)(?: ([a-zA-Z0-9]*))?$/;
- }
- sub match_version($$)
- {
- my($version,$pattern) = @_;
- my @vv = target_string_valid($version);
- return 0 unless (defined($vv[0]));
- my $v_board = $vv[0];
- my @v_ver = split(/\./, $vv[1]);
- my $v_flags = $vv[2];
- return 1 if ($pattern eq $v_board); # Board only matchall pattern
- if ($pattern !~ /^(\S+) v((?:0|[1-9][0-9]*)(?:\.\.0|\.[1-9][0-9]*)*)(?:(\-)((?:0|[1-9][0-9]*)(?:\.\.0|\.[1-9][0-9]*)*))?(?: ([\+\-a-zA-Z0-9]*))?$/) {
- return 0;
- }
- return 0 if ($1 ne $v_board);
- my @p_min = split(/\./, $2);
- my @p_max = split(/\./, $3 eq '' ? $2 : $4);
- my $p_flags = $5;
- while (scalar(@p_min) || scalar(@p_max)) {
- my $mi = shift(@p_min);
- my $ma = shift(@p_max);
- my $ve = shift(@v_ver) || 0;
- return 0 if (defined($mi) && $ve < $mi);
- return 0 if (defined($ma) && $ve > $ma);
- }
- my $flag_pol = 1;
- foreach my $c (split(//, $p_flags)) {
- if ($c eq '-') {
- $flag_pol = 0;
- } elsif ($c eq '+') {
- $flag_pol = 1;
- } else {
- return 0 if ((index($v_flags, $c) != -1) != $flag_pol);
- }
- }
- return 1;
- }
- 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($)
- {
- my($port) = @_;
- my $boardinfo = tmpfilename('boardinfo.bin');
- my %myespopt = (%espopt);
- $myespopt{'after'} = 'no_reset';
- run_esptool($port, { hgrep {!/^flash_/} \%myespopt },
- 'read_flash', { },
- 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);
- if ($bh[0] == 0x6682df97 && $bh[1] == 0xe2a0d506) {
- # Standard format board_info structure
- substr($bid, 12, 4) = "\0\0\0\0"; # Clear the CRC field
- if ($bh[2] >= 16 && $bh[2] <= $boardinfo_len &&
- crc32(substr($bid, 0, $bh[2])) == $bh[3]) {
- return $bh[4];
- }
- } elsif ($bid =~ /^([[:print:]]+)\0/) {
- # Preliminary board_info (only version string)
- return $1;
- }
- return undef;
- }
- sub read_fwfile($$) {
- my($file, $target_board) = @_;
- return undef if (!defined($file));
- open(my $fw, '<:gzip', $file)
- or die "$0: $file: $!\n";
- my @chunks = ();
- my $version_match = undef;
- 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";
- $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;
- 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);
- last if ($t eq 'end'); # End of stream
- }
- close($fw);
- 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;
- 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'};
- }
- $nc++;
- }
- my $fpgadata;
- if (defined($raw_fpgadata)) {
- open(my $fpgafh, '>:gzip', \$fpgadata) or die;
- print $fpgafh $raw_fpgadata;
- close($fpgafh);
- }
- return ([@espfiles], $fpgadata);
- }
- sub tty_read($$$;$) {
- state %old_timeout;
- my($tty,$bufref,$timeout,$count) = @_;
- $count = 1 unless (defined($count));
- if ($$bufref eq '' || !$count) {
- if (!defined($old_timeout{$tty}) || $timeout != $old_timeout{$tty}) {
- $tty->read_const_time($timeout);
- $old_timeout{$tty} = $timeout;
- }
- my($c,$d) = $tty->read(256);
- $$bufref .= $d;
- }
- my $r = substr($$bufref,0,$count);
- $$bufref = substr($$bufref,$count);
- return $r;
- }
- sub tty_write($$$) {
- my($tty,$bufref,$data) = @_;
- my $bytes = length($data);
- my $offs = 0;
- while ($bytes) {
- my $cnt = $tty->write(substr($data,$offs,$bytes));
- tty_read($tty, $bufref, 100, 0) unless ($cnt);
- $offs += $cnt;
- $bytes -= $cnt;
- }
- return $offs;
- }
- sub upload_fpgadata($$) {
- use bytes;
- my($port, $fpgadata) = @_;
- my $SOH = 001;
- my $STX = 002;
- my $ETX = 003;
- my $EOT = 004;
- my $ENQ = 005;
- my $ACK = 006;
- my $XON = 021; # A.k.a. DC1
- my $WRST = 022; # DC2: window = 0
- my $XOFF = 023; # A.k.a. DC3
- my $WGO = 024; # DC4: window += 256 bytes
- my $NAK = 025;
- my $SYN = 026;
- my $ETB = 027;
- my $CAN = 030;
- my $EM = 031; # Packet offset too high
- my $FS = 034;
- my $GS = 035;
- my $RS = 036;
- my $US = 037;
- my $WGO_CHUNK = 256; # Each WGO = 256 bytes
- 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 (Unix/MacOS) or Win32::SerialPort (Win32)\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, \$ttybuf, "\005"); # ENQ
- while (($tt = time()) - $start_enq < 30) {
- my $d = tty_read($tty, \$ttybuf, 1000);
- if ($d eq '') {
- tty_write($tty, \$ttybuf, chr($ENQ));
- } else {
- my $ix = index("\026\004\027", $d); # SYN EOT ETB
- if ($ix < 0) {
- print STDERR $d;
- next;
- }
- $found = $found == $ix ? $found+1 : 0;
- last if ($found == 3);
- }
- }
- 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";
- }
- if ($tt != $last_req) {
- # FS SOH <string> GS
- tty_write($tty, \$ttybuf,
- "\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 == $RS) {
- $winspc = 0;
- last;
- }
- }
- }
- 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 == $WRST) {
- $winspc = 0;
- } elsif ($dc == $WGO) {
- if (defined($winspc)) {
- $winspc += $WGO_CHUNK;
- }
- } elsif ($dc == $ACK) {
- $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 == $NAK || $dc == $EM || $dc == $US) {
- $offset = $last_ack;
- @pktends = ();
- undef $winspc; # Wait for WRST before resuming
- } elsif ($dc == $CAN) {
- print STDERR "\n";
- die "$0: $port: upload aborted by target system\n";
- }
- }
- }
- if (!$chunk) {
- if ($bytes > $offset) {
- if ($now != $last_enq) {
- # SYN: request window resync
- tty_write($tty, \$ttybuf, chr($SYN));
- $last_enq = $now;
- }
- }
- next;
- }
- my $data = substr($fpgadata, $offset, $chunk);
- my $hdr = pack("CvVV", $chunk-1, 0, $offset, crc32($data));
- tty_write($tty, \$ttybuf, chr($STX).mybaseencode($hdr, $data));
- push(@pktends, $offset + $chunk);
- $offset += $chunk;
- $winspc -= $chunk;
- }
- tty_write($tty, \$ttybuf, chr($EOT));
- # Final messages out
- while (1) {
- my $d = tty_read($tty, \$ttybuf, 1000);
- last if ($d eq '');
- print STDERR $d;
- }
- $tty->close;
- return 0;
- }
- my @args = @ARGV;
- my $esponly = 0;
- my $fpgaonly = 0;
- my $file;
- my $target_board = undef;
- my $setver = 0;
- my $getver = 0;
- my $port = undef;
- my $which = 0;
- while (1) {
- $file = shift(@args);
- last if ($file !~ /^\-/);
- if ($file eq '--esponly') {
- $esponly = 1;
- } elsif ($file eq '--fpgaonly') {
- $fpgaonly = 1;
- } elsif ($file eq '--which') {
- $which = 1;
- } elsif ($file eq '--setver') {
- $target_board = shift(@args);
- $setver = defined($target_board);
- } elsif ($file eq '--getver') {
- $getver = 1;
- } elsif ($file eq '--port') {
- $port = shift(@args);
- } elsif ($file eq '--') {
- $file = shift(@args);
- last;
- } else {
- undef $file; # Invalid argument, print usage
- last;
- }
- }
- %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} = '';
- }
- }
- if (defined($espopt{'port'})) {
- $port = $espopt{'port'} unless (defined($port));
- delete $espopt{'port'};
- }
- # Legacy command line support: if no --port, specify port after firmware file
- if (defined($file) && !defined($port) && $args[0] ne '--esptool') {
- $port = shift(@args);
- }
- $tmpdir = File::Temp->newdir(CLEANUP => 1);
- if (!defined($tmpdir)) {
- die "$0: failed to create temporary directory: $!\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 ${espver} found\n\n";
- if (!defined($port)) {
- die "Usage: $0 [options] [file.fw] [--esptool esptool_options...]\n".
- " Options:\n".
- " --which print the esptool command discovered, if any\n".
- " --setver <version> set the board version string\n".
- " --getver get the board version string, then exit\n".
- " --esponly flash ESP32 only\n".
- " --fpgaonly flash the FPGA only\n".
- " --port serial port to use\n";
- }
- if (! -c $port && !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 "$0: using serial port device $port\n";
- 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";
- }
- 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";
- }
- }
- if ($getver) {
- print "$0: $port: board version: $target_board\n";
- exit 0;
- }
- my $fpgadata;
- if (defined($file)) {
- my $fwfiles;
- ($fwfiles, $fpgadata) = read_fwfile($file, $target_board);
- push(@espfiles, @$fwfiles) if (defined($fwfiles));
- }
- if (!$fpgaonly && scalar(@espfiles)) {
- run_esptool($port, { hgrep {!/^flash_/} %espopt },
- 'write_flash', { hgrep {/^flash_/} %espopt },
- '-z', map { (sprintf('0x%x', $_->[0]), $_->[1]) } @espfiles);
- }
- if (!$esponly && defined($fpgadata)) {
- upload_fpgadata($port, $fpgadata);
- }
- print STDERR "\n$0: $port: update complete\n";
- exit 0;
|