From 7c0f4ea4e813885263777c5b7316886f8e72b0fa Mon Sep 17 00:00:00 2001 From: "B. Watson" Date: Sat, 11 Apr 2026 05:04:52 -0400 Subject: initial commit --- fujinet-updater | 390 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100755 fujinet-updater diff --git a/fujinet-updater b/fujinet-updater new file mode 100755 index 0000000..d301f00 --- /dev/null +++ b/fujinet-updater @@ -0,0 +1,390 @@ +#!/usr/bin/perl -w + +# these are bundled with perl. +use JSON; +use Getopt::Std; + +##use Data::Dump 'dump'; + +$VERSION="0.0.1"; + +$RELEASE_URL = "https://api.github.com/repos/FujiNetWIFI/fujinet-firmware/releases"; + +sub slurpfile { + my $file = shift; + open my $f, "<$file" or die "$file: $!\n"; + my $old = $/; + undef $/; + my $content = <$f>; + $/ = $old; + close $f; + return $content; +} + +# all the programs we want to check, will exit if called with +# no arguments. so this is simpler than expected. +sub checkpath { + my $exe = shift; + my $result = system("$exe 1>/dev/null 2>&1"); + $result >>= 8; + if($result < 0 || $result > 2) { + warn "can't find $exe on PATH.\n"; + return 0; + } + warn "found $exe on PATH\n"; + return 1; +} + +sub check_paths { + if(checkpath("wget")) { + # wget follows redirects by default. + $dlcmd = "wget -O"; + } elsif(checkpath("curl")) { + # the -L is needed to make curl follow redirects. + $dlcmd = "curl -L -o"; + } else { + die_path("curl or wget"); + } + + if(!checkpath("unzip")) { + die_path("unzip"); + } + + if(!checkpath("pio")) { + if(!$opts{n}) { + warn "can't find pio on PATH, disabling monitoring.\n"; + $opts{n} = 1; + } + } +} + +sub download_firmware { + my $url = shift; + my $file = $url; + $file =~ s,.*/,,; + + if(-e $file) { + print "$file already exists, download anyway [y/N]? "; + chomp(my $c = <>); + return $file if $c !~ /^y/i; + } + + system("$dlcmd $file $url"); + # TODO: check for errors in download + + if(! -e $file) { + die "failed to download $url!\n"; + } + + # make sure it looks OK. + open my $f, "<$file" or die "can't read $file: $!\n"; + read($f, my $bytes, 2) or die "can't read $file: $!\n"; + close $f; + if($bytes ne 'PK') { + die "$file is not a .zip file!\n"; + } + + return $file; +} + +sub show_choices { + my $tmpfile = "releases.$$.json"; + unlink $tmpfile; + system("$dlcmd releases.$$.json $RELEASE_URL"); + if(! -e $tmpfile) { + die "failed to download from $RELEASE_URL\n"; + } + + my $json = slurpfile($tmpfile); + + my $j = decode_json($json); + die "$tmpfile: JSON decoding failed, huh?\n" unless $j; + + unlink $tmpfile; + + + ##dump($j); + #for(@{$j}) { + #print "$_ " for keys %{$_}; + #} + #die; + + my $num = 1; + my @choices; + + for(@{$j}) { + for(@{$_->{assets}}) { + my $url = $_->{browser_download_url}; + next unless $url =~ /\/fujinet-[A-Z].*\.zip$/; + if($sys ne "ALL") { + next unless $url =~ /-$sys-.*\.zip$/i; + next if $sys eq "ATARI" && $url =~ /-8mb/; + } + my $file = $url; + $file =~ s,.*/,,; + $choices[$num] = $url; + print "[$num] $file\n"; + $num++; + } + } + print "-> "; + chomp(my $c = <>); + die "invalid choice.\n" unless $choices[$c]; + + return $choices[$c]; +} + +sub get_esptool_cmds { + my $file = shift; + if(! -e $file) { + die "$file doesn't exist.\n"; + } + if(-d $file) { + $dir = $file; + } else { + $dir = $file; + $dir =~ s,\.zip$,,; + $dir =~ s,.*/,,; + system("rm -rf $dir"); + mkdir $dir; + if(system("unzip $file -d $dir") != 0) { + die "failed to extract $file\n"; + } + } + chdir($dir) or die "$dir: $!\n"; + + my $json = slurpfile("release.json"); + my $j = decode_json($json) or die "$!"; + + my $sudo = $opts{s} ? "sudo " : ""; + for(@{$j->{files}}) { + my $f = $_->{filename}; + die "$f referenced in release.json, but doesn't exist.\n" + unless -e $f; + push @commands, $sudo . + "esptool.py --port $port --baud $baud " . + "write_flash $_->{offset} $f"; + } + + die "no filenames in release.json\n" unless @commands; + + return @commands; +} + +sub confirm { + print "About to run these commands to flash the firmware:\n"; + print " $_\n" for @commands; + print "Are you sure you want to proceed[y/N]? "; + chomp(my $resp = <>); + return $resp =~ /^y/i; +} + +sub interact { + mkdir($homedir); + chdir($homedir) or die "$homedir: $!\n"; + + check_paths(); + + $url = show_choices(); + $file = download_firmware($url); + + @commands = get_esptool_cmds($file); + + if(confirm()) { + return $file; + } else { + print "Aborted.\n"; + exit 0; + } +} + +sub flash { + for(@commands) { + print "===> $_\n"; + system($_); + } + + if($mon) { + my $sudo = $opts{s} ? "sudo " : ""; + system($sudo . "pio device monitor -b $baud -p $port"); + } +} + +### main() +if(@ARGV) { + for($ARGV[0]) { + if(/^--?(?:\?|h)/) { + exec "perldoc $0"; + exit 0; + } elsif(/^--?man/) { + exec "pod2man --stderr -s6 -cUrchlaysStuff -r$VERSION -u $0"; + exit 0; + } elsif(/^--?version/) { + print "$VERSION\n"; + exit 0; + } + } +} + +getopts('st:b:p:nf:', \%opts); + +# TODO: option to override this? +$homedir = "$ENV{HOME}/.fujinet-updater"; + +$baud = $opts{b} || 460800; +$port = $opts{p} || "/dev/ttyUSB0"; +$sys = uc($opts{t} || "atari"); +$mon = !$opts{n}; + +if($opts{f}) { + $file = $opts{f}; + if($file !~ /^\//) { + my $p = `pwd`; + chomp($p); + $file = $p . '/' . $file; + } + if(-d $file) { + chdir($file) or die "$file: $!\n"; + } else { + mkdir($homedir); + chdir($homedir) or die "$homedir: $!\n"; + } + @commands = get_esptool_cmds($file); +} else { + interact(); +} + +flash(); + +=pod + +=head1 NAME + +fujinet-updater - download and flash the firmware on a FujiNet device. + +=head1 SYNOPSIS + +B [B<-s>] [B<-t> I] [B<-b> I] [B<-p> I] [B<-n>] [B<-f> I] + +=head1 DESCRIPTION + +B is a command-line tool which will flash the +firmware on a FujiNet device. It will download the firmware from +the fujinet-firmware GitHub releases page with B or B, +and uses B to upload the firmware to the device and +(if installed) B to monitor the FujiNet's log messages when it +reboots after being flashed. + +Normally, B runs interactively. It searches for +firmware releases for the target system, and presents a menu from +which you can choose the version to flash. After downloading and +unzipping the firmware, you will be show the exact B +commands that will be executed, and offered a last chance to abort +before flashing the device. + +For non-interactive use, use the B<-f> option to choose an +already-downloaded (and possibly already-extracted) firmware zip file. + +=head1 OPTIONS + +=over 4 + +=item B<--help> + +Displays the documentation you're reading now, using the B(1) +command. + +=item B<--man> + +Prints this documentation in man page (troff/nroff source) format. + +=item B<-s> + +Run B commands with B(8). If you haven't already +authenticated with sudo in the current session, you'll be asked for +your password when the first command is run. + +=item B<-t> I + +Which system (platform) your FujiNet board is designed for. This is +case-insensitive. Default is I. Other choices include I, +I, I, I, etc. Use B<-t> I to see all platforms. + +I: Only I has been tested! + +=item B<-b> I + +Baud (or actually, bits-per-second) rate for the flashing process. Defaults +to I<460800>. Normally you should not need to change this. + +=item B<-b> I + +Defaults to I. If you have multiple USB devices that appear as +serial ports (including multiple FujiNets), you may need to change this to +e.g. I. + +=item B<-n> + +Do not run B to monitor the debug output from the FujiNet when it reboots +after being flashed. Normally you would want to see this output. + +=item B<-f> I + +Use a specific firmware that's already been downloaded, rather than +prompting for a file to download. With this option, B +does not prompt the user for anything, I the final +confirmation prompt before flashing the device. + +If the argument to B<-f> is a zip file, it will be extracted to a +temporary directory. Either way, the zip file or directory B +contain the B file that lists the firmware files and +their offsets, and all files mentioned there B be present. + +When using B<-f>, no network activity is done (so this can also be +considered "offline" mode). + +For I non-interactive mode, you'll want to combine B<-f> +with B<-n> (to disable monitoring). + +=back + +=head1 RATIONALE + +Why does this need to exist? There's already the official +fujinet-flasher, and the fujinet_firmware_uploader.py script in the +fujinet-firmware source repo... + +The official fujinet-flasher is a Python3 GUI program. Python3 is +still a fast-moving target, and its developers don't care about +supporting older versions of Python3 (or older Linux glibc versions, +for their binary releases). Plus, it's graphical. Some of us prefer +command-line tools. + +But, fujinet_firmware_uploader.py is a command line tool, isn't +it? Yes, but as shipped, it's broken: hardcoded weird place where it +looks for the esptool.py executable, and it only flashes 2 of the files +in the release zip file (no idea why, but this leaves the FujiNet in +an unusable state). Also, it always downloads and extracts its files +to the current directory, which can be annoying. + +B can be used non-interactively (e.g. from a Makefile +or other script), which is not true of either of the existing tools. +It also is written in Perl, and doesn't use any external Perl modules +that don't ship with Perl itself (so, no dependency hell). + +The disadvantage is, B calls external binaries: +B(1) or B(1), B, and B. It also pretty +much assumes a UNIX/POSIX like environment, so it might be a PITA to +get working on Windows. + +=head1 FILES + +B<~/.fujinet-updater/> - all downloaded zip files, and their extracted +contents, are saved here. + +=head1 AUTHOR + +B was written by B. Watson and +released under the WTFPL: Do WTF you want with this. + +=cut -- cgit v1.2.3