aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorB. Watson <urchlay@slackware.uk>2026-04-11 05:04:52 -0400
committerB. Watson <urchlay@slackware.uk>2026-04-11 05:04:52 -0400
commit7c0f4ea4e813885263777c5b7316886f8e72b0fa (patch)
treec40cedc3414cb038902ba7c3d99e7d5dfefffb1c
downloadfujinet-updater-7c0f4ea4e813885263777c5b7316886f8e72b0fa.tar.gz
initial commit
-rwxr-xr-xfujinet-updater390
1 files changed, 390 insertions, 0 deletions
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<fujinet-updater> [B<-s>] [B<-t> I<board-type>] [B<-b> I<baud>] [B<-p> I<serial-port>] [B<-n>] [B<-f> I<zipfile-or-directory>]
+
+=head1 DESCRIPTION
+
+B<fujinet-updater> 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<wget> or B<curl>,
+and uses B<esptool.py> to upload the firmware to the device and
+(if installed) B<pio> to monitor the FujiNet's log messages when it
+reboots after being flashed.
+
+Normally, B<fujinet-updater> 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<esptool.py>
+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<perldoc>(1)
+command.
+
+=item B<--man>
+
+Prints this documentation in man page (troff/nroff source) format.
+
+=item B<-s>
+
+Run B<esptool.py> commands with B<sudo>(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<board-type>
+
+Which system (platform) your FujiNet board is designed for. This is
+case-insensitive. Default is I<atari>. Other choices include I<atari-8mb>,
+I<adam>, I<apple>, I<coco>, etc. Use B<-t> I<all> to see all platforms.
+
+I<NOTE>: Only I<atari> has been tested!
+
+=item B<-b> I<baud>
+
+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<serial-port>
+
+Defaults to I</dev/ttyUSB0>. If you have multiple USB devices that appear as
+serial ports (including multiple FujiNets), you may need to change this to
+e.g. I</dev/ttyUSB1>.
+
+=item B<-n>
+
+Do not run B<pio> 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<zipfile-or-directory>
+
+Use a specific firmware that's already been downloaded, rather than
+prompting for a file to download. With this option, B<fujinet-updater>
+does not prompt the user for anything, I<including> 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<must>
+contain the B<release.json> file that lists the firmware files and
+their offsets, and all files mentioned there B<must> be present.
+
+When using B<-f>, no network activity is done (so this can also be
+considered "offline" mode).
+
+For I<totally> 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<fujinet-updater> 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<fujinet-updater> calls external binaries:
+B<wget>(1) or B<curl>(1), B<esptool.py>, and B<pio>. 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<fujinet-updater> was written by B. Watson <urchlay@slackware.uk> and
+released under the WTFPL: Do WTF you want with this.
+
+=cut