#!/usr/bin/perl -w # $Id$ # # SLIMP3 Firmware updater Copyright (C) 2002-2004 Slim Devices Inc. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. # #------------- # # This program will load the PIC's program and configuration EEPROM with the specified .HEX # file(s). If more than one .HEX file is specified, then they will be "overlaid" in the # order given. This allows easy patches for localization, user configuration, etc. # # Three address ranges are programmed: # # 0x0004 - 0x001F Interrupt service routine # 0x0600 - 0x1FFF Main program # 0x2100 - 0x21DF User configuration EEPROM (will wipe to default settings unless "patched") # # The hex file will contain code outside of these ranges - this is just initialization code, # so we can run it on the ICE without having to include the boot loader firmware. These sections # of the .HEX file are ignored. The bootloader client will protect any invalid ranges, with # the exception of 0x21E0 - 0x21FF. This area is reserved for "factory settings" - i.e. the MAC # address. If you *really* wanted to monkey with this area, you could. # # Note that the HEX file is addressed in bytes, but the PIC is 14-bit words on 16-bit # boundaries. We only refer to the byte addresses when reading the HEX file. # # The client does not verify writes, we do. Each chunk of up to 64 words is written once and # verified twice. If verification fails, we attempt to re-write it up to ten times before giving # up. # #------------- use strict; use lib qw(lib); use IO::Socket; use File::Spec::Functions qw(:ALL); use FindBin qw($Bin); my $VERSION = 0x13; # the version of the bootloader protocol my $PIC_BEGIN_ISR = 0x0004; # Interrupt service routine my $PIC_END_ISR = 0x001F; my $PIC_BEGIN_MAIN = 0x0600; # Main SLIMP3 firmware my $PIC_END_MAIN = 0x1FFF; my $PIC_BEGIN_USERCONF = 0x2100; # user config (IP address, etc) my $PIC_END_USERCONF = 0x21DF; my $PIC_BEGIN_FACTORY = 0x21E0; # factory config (MAC address) my $PIC_END_FACTORY = 0x21FF; my $PIC_MAXADDR = 0x21FF; # The last programmable address my $MAXCHUNK = 64; my $NUMVERIFY = 4; my $GIVEUPAFTER = 10; my $TIMEOUT = 2; # seconds my $REMOTEPORT = 69; my $LOCALPORT = 1069; # this needs to be kept up to date if config.asm changes: my %EE; $EE{'eeIP1'} = 0x00; $EE{'eeIP2'} = 0x01; $EE{'eeIP3'} = 0x02; $EE{'eeIP4'} = 0x03; $EE{'eeMask1'} = 0x04; $EE{'eeMask2'} = 0x05; $EE{'eeMask3'} = 0x06; $EE{'eeMask4'} = 0x07; $EE{'eeServ1'} = 0x08; $EE{'eeServ2'} = 0x09; $EE{'eeServ3'} = 0x0A; $EE{'eeServ4'} = 0x0B; $EE{'eeGate1'} = 0x0C; $EE{'eeGate2'} = 0x0D; $EE{'eeGate3'} = 0x0E; $EE{'eeGate4'} = 0x0F; $EE{'eeOurPortH'} = 0x10; $EE{'eeOurPortL'} = 0x11; $EE{'eeServPortH'} = 0x12; $EE{'eeServPortL'} = 0x13; $EE{'eeRemoteID'} = 0x14; $EE{'eeConfigured'} = 0x15; $EE{'eeKioskMode'} = 0x16; $EE{'CONFIGURED_MANUALLY'} = 0x0; $EE{'CONFIGURED_DHCPONLY'} = 0x1; $EE{'CONFIGURED_DHCPSDP'} = 0x2; $EE{'eeMAC1'} = 0xE0; $EE{'eeMAC2'} = 0xE1; $EE{'eeMAC3'} = 0xE2; $EE{'eeMAC4'} = 0xE3; $EE{'eeMAC5'} = 0xE4; $EE{'eeMAC6'} = 0xE5; my $quiet = 0; my $runAsEmbeddedTool = 0; my $arp = 'arp'; if ($^O eq 'darwin') { $arp = '/usr/sbin/arp'; } sub sendWriteChunk { my ($udpsock, $remotepaddr, $imageref, $beginaddr, $len) = @_; # print "sendWriteChunk($beginaddr, $len)\n"; my $pkt='e'. # eeprom read/write pack('C', 0x03). # 03 = write, 00 = read pack('C', 0x13). # version ' '. # reserved ' '. # reserved pack('n', $beginaddr). # offset pack('n', $len). # length ' '; # reserved my $i; for ($i=$beginaddr; $i<($beginaddr+$len); $i++) { # printf("%4X %4X\n", $i, unpack('n',@$imageref[$i])); $pkt.=@$imageref[$i]; } $udpsock->send($pkt, 0, $remotepaddr); } sub sendReadChunk { my ($udpsock, $remotepaddr, $imageref, $beginaddr, $len) = @_; # print "sendReadChunk($beginaddr, $len)\n"; my $pkt='e'. # eeprom read/write pack('C', 0x00). # 01 = write, 00 = read pack('C', 0x13). # version ' '. # reserved ' '. # reserved pack('n', $beginaddr). # offset pack('n', $len). # length ' '; # reserved $udpsock->send($pkt, 0, $remotepaddr); } sub sendRunCommand { my ($udpsock, $remotepaddr) = @_; my $pkt='r '; $udpsock->send($pkt, 0, $remotepaddr); } # receives the reply from a read/write request, returns 0 on success, 1 on timeout # # the recevied data is put in buf and offset is set to the received offset. Max recv # # size is passed in len, actual recevied size returned in len # sub recvReadChunk { my ($udpsock, $remotepaddr, $chunkref, $offsetref, $lenref) = @_; my ($select_time, $rin, $rout); my ($recvpaddr, $msg); $rin=''; vec($rin, fileno($udpsock), 1) = 1; # print "rin == ".unpack("H*", $rin)."\n"; select($rout = $rin, undef, undef, $TIMEOUT); # print "Select returned ".unpack("H*", $rout)."\n"; # if UDP activity... if (vec($rout, fileno($udpsock), 1)) { # print "udpsock readable\n"; $recvpaddr = recv($udpsock,$msg,1500,0); my @octets = split(//, $msg); if (scalar(@octets) == 0) { print("zero length response from player!"); # treat it like a timeout, try again. return(0); } elsif ($octets[0] ne 'e') { print "unknown type: $octets[0]\n" ; done(1); } $$offsetref = unpack('n', $octets[6].$octets[7]); $$lenref = unpack('n', $octets[8].$octets[9]); # print("recv begins:"); # my $i; # for ($i=0; $i<=20; $i++) { # printf("%2X", unpack('C', $octets[$i+18])); # } # print "\n"; $$chunkref=join('',@octets[18..(length($msg)-1)]); return(1); } else { return(0); #timeout } } # returns 1 on success, 0 for failure sub writeAndVerifyChunk { my ($udpsock, $remotepaddr, $imageref, $beginaddr, $len) = @_; my ($attempt, $verifyfailed, $verifypass, $verifyok); my ($readchunk, $readoffset, $readlen); # print progress my $percent = int(($beginaddr+$len-$PIC_BEGIN_MAIN)/($PIC_MAXADDR-$PIC_BEGIN_MAIN) * 100) + 1; if ($percent < 0) { $percent = 0; }; while (length($percent)<3) { $percent = ' '.$percent; } my $bs = pack('C',0x08) x 13; !$quiet && print " Writing ", $percent, '%', $bs; for ($attempt=1; $attempt<=$GIVEUPAFTER; $attempt++) { &sendWriteChunk($udpsock, $remotepaddr, $imageref, $beginaddr, $len); $verifyfailed=0; for ($verifypass=1; $verifypass<=$NUMVERIFY && !$verifyfailed; $verifypass++) { if ($verifypass > 1) { &sendReadChunk($udpsock, $remotepaddr, $imageref, $beginaddr, $len); } ($readchunk, $readoffset, $readlen)=('','',''); if (&recvReadChunk($udpsock, $remotepaddr, \$readchunk, \$readoffset, \$readlen)) { if ($readoffset != $beginaddr) { $verifyfailed=1; !$quiet && printf(STDERR "Verify failed: received offset %4X, expected %4X\n", $readoffset, $beginaddr); } if ($readlen != $len) { $verifyfailed=1; !$quiet && printf(STDERR "Verify failed: received length %4X, expected %4X\n", $readlen, $len); } if ($readchunk ne join('',@$imageref[$beginaddr..($beginaddr+$len-1)])) { $verifyfailed=1; !$quiet && print(STDERR "Verify failed: data errors\n"); } } else { !$quiet && print STDERR "Timeout: Writing again, attempt $attempt\n"; $verifyfailed=1; # timed out } } if (!$verifyfailed) { return(1); } } return(0); # failed } # # writes and verifies each chunk in the specified range (inclusive). # return 0 = success, otherwise returns # failed chunks # sub writeRange { my ($udpsock, $remotepaddr, $imageref, $beginaddr, $endaddr) = @_; my ($addr, $len, $failed); $failed=0; for ($addr = $beginaddr; $addr<=$endaddr; $addr+=$MAXCHUNK) { $len = $endaddr-$addr+1; $len = $MAXCHUNK if ($len > $MAXCHUNK); if (!writeAndVerifyChunk($udpsock, $remotepaddr, $imageref, $addr, $len)) { print STDERR "Firmware installation failed. Please check the IP and MAC addresses.\n"; done(1); } } return $failed; } # # Initialize the image to all-ones (14 bits) # sub initImage { my ($imageref, $start, $end) = @_; my $i; for ($i=$start; $i<=$end; $i++) { if ($i<=0x2100) { @$imageref[$i]=pack('n',0x3FFF); } else { @$imageref[$i]=pack('n',0x00FF); } } } # # reads the specified HEX file $filename, into the array of unsigned shorts $imageref # # "holes" in the hex file will be left untouched in the image # file format is: # len addr ty [----data-------] sum # : 06 0000 00 08 30 8A 00 00 28 10 (example line, spaces added) # sub readIntelHex { my ($imageref, $maxaddr, $filename) = @_; if (!open(HEXFILE, $filename)) { print "Couln't open: $filename\n"; done(1); } my $linenum=0; my $line; my $version = undef; my ($magic, $h_len, $h_addr, $h_type, $h_data, $h_sum); my ($len, $addr, $type, $filesum); my ($lowbyte, $highbyte); my (@data, $word, $picaddr, $i, $mysum); $type=0; while (defined($line = ) && ($type != 1)) { chomp $line; $mysum = 0; $magic = substr($line, 0, 1); # lines starting with # are comments if ($magic eq 'V' || $magic eq 'v') { $version = substr($line, 1); next; } if ($magic eq '') { next; } if ($magic eq '#') { next; } if ($magic eq ' ') { next; } if ($magic eq ' ') { next; } if ($magic ne ':') { print "Line did not start with \":\" instead there was $magic"; done(1); } $len = hex ($h_len = substr($line, 1, 2)); $addr = hex ($h_addr = substr($line, 3, 4)); $type = hex ($h_type = substr($line, 7, 2)); $h_data = substr($line, 9, length($line) - 11); $filesum = hex ($h_sum = substr($line, -2, 2)); #print "$h_len, $h_addr, $h_type, $h_data, $h_sum\n"; if (length($h_data)/2 != $len) { print "length field doesn't match line length\n"; done(1); } if ($len%2 != 0) { print "length is not a multiple of 2\n"; done(1); } for ($i = 0; $i<$len; $i+=2) { $lowbyte=hex(substr($h_data, $i*2, 2)); $highbyte=hex(substr($h_data, $i*2+2, 2)); $word = pack('n', $highbyte * 256 + $lowbyte); $mysum+= $highbyte+$lowbyte; $picaddr = ($addr+$i)/2; if ($picaddr > $maxaddr) { print "Address out of range: $picaddr\n"; done(1); } @$imageref[$picaddr] = $word; #printf "%4X %4X\n", $picaddr, $word; #printf "%4X \n", $mysum; } # checksum = 1's complement of (sum of data + starting address + length + record type); $mysum= 255 & (1 + (255 & ~($mysum + int($addr/256) + ($addr%256) + $len + $type))); if ($mysum ne $filesum) { print "Wrong checkum - I got $mysum, file says $filesum\n"; done(1); } } return $version; } sub main { # Order of parameters no longer matters: retrieves ip, mac, any options by checking # against expressions. Extra parameters ignored. # # runAsEmbeddedTool (--embedded-tool) is used to suppress any invalid prompts or actions # (such as calling system in an authorized context in OS X, which crashes). my $mac; my $ip; my $path; my $newmac; foreach (@ARGV) { if (/^--embedded-tool$/i) { $runAsEmbeddedTool = 1; } elsif (/^--newmac=(([0-9a-f]{1,2}:){5}[0-9a-f]{1,2})$/i) { $newmac = $1; } elsif (/^([0-9a-f]{1,2}:){5}[0-9a-f]{1,2}$/i) { $mac = $_; } elsif (/^(\d{1,3}.){3}\d{1,3}$/) { $ip = $_; } else { $path = $_; } } if ($runAsEmbeddedTool) { $quiet = 1; } autoflush STDERR; autoflush STDOUT; my @image; &initImage(\@image, 0, $PIC_MAXADDR); if (!defined($path)) { $path = catdir($Bin,'MAIN.HEX'); } my $version = &readIntelHex(\@image, $PIC_MAXADDR, $path); # make sure we're root if ($> != 0) { print STDERR "This program needs to be run as administrator or root.\n"; done(1); } !$quiet && print "\n\n\n Welcome to the SLIMP3 Firmware Updater\n\n"; !$quiet && print "This software will update the firmware in your SLIMP3 Player"; if (!$quiet) { if ($version) { print " to version $version.\n"; } else { print ".\n"; } } if ($mac && $ip) { goto SKIP_PROMPTS } elsif ($runAsEmbeddedTool) { print STDERR "Both the MAC and IP address must be specified when using --embedded-tool.\n"; done(1); } print " To get started, make sure your player is connected to the same ethernet network as this computer. Also, make sure that the SLIMP3 Server software has been stopped. Unplug the player, and then plug it back in while holding down any button on the remote. This will start the firmware update mode. The player's display will read: ----------------------------------------- | Slim Devices Bootloader V.03 | | My MAC is: 00:04:20:XX:XX:XX | ----------------------------------------- Please enter the player's MAC address as shown on the display, including the colons: MAC Address --> "; my $macok=0; while (!$macok) { $mac=; chomp $mac; if ($mac=~/[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]/) { $macok=1; } else { print "Please try again. The number should be something like \"00:04:20:XX:XX:XX\"\n\nMAC Address --> "; } } print " Now we'll need an IP address for the player. Please enter any available IP address on your network.\n\nIP Address --> "; my $ipok=0; while (!$ipok) { $ip=; chomp($ip); if ($ip=~/(\d+)\.(\d+)\.(\d+)\.(\d+)/ && $1<255 && $2<255 && $3<255 && $4<255) { $ipok=1; } else { print "Please try again. The IP address should be four number separated by periods, eg\"192.168.0.20\".\n\nIP Address --> "; } } print " \"Kiosk mode\" is a special feature which bypasses this SLIMP3 splash screen and setup menus. This allows the SLIMP3 to start up unattended, and to be used in public places where it is desirable to lock out access to the setup screens. Instead of using the configuration screens on the player, the setting are installed now, during firmware installation. Kiosk mode can only be enabled or disabled by running this firmware updater. Experts only: enable kiosk mode? [y/N] --> "; goto SKIP_PROMPTS unless (=~/^[yY]/); $image[$PIC_BEGIN_USERCONF + $EE{'eeKioskMode'}]=pack('n', 0x0001); print " KIOSK SETTINGS - Choose a configuration method: [A]utomatic (DHCP & Slim Discovery Protocol) [D]HCP only (specify server IP manually) [S]tatic IP addresses (enter everything manually) "; my $configmethod; my $configmethodok=0; while (!$configmethodok) { print "Configuration method? [a/d/s]--> "; $configmethod=; chomp $configmethod; $configmethodok=($configmethod=~/^[ads]$/); } if ($configmethod eq 'a') { $image[$PIC_BEGIN_USERCONF + $EE{'eeConfigured'}]=pack('n', $EE{'CONFIGURED_DHCPSDP'}); goto SKIP_PROMPTS; } elsif ($configmethod eq 'd') { $image[$PIC_BEGIN_USERCONF + $EE{'eeConfigured'}]=pack('n', $EE{'CONFIGURED_DHCPONLY'}); goto SKIP_SERVER_IP; } elsif ($configmethod eq 's') { $image[$PIC_BEGIN_USERCONF + $EE{'eeConfigured'}]=pack('n', $EE{'CONFIGURED_MANUALLY'}); } my $addr; $ipok=0; while (!$ipok) { print "\nServer's IP address --> "; $addr=; chomp($addr); if ($addr=~/(\d+)\.(\d+)\.(\d+)\.(\d+)/ && $1<255 && $2<255 && $3<255 && $4<255) { $image[$PIC_BEGIN_USERCONF + $EE{'eeServ1'}]=pack('n', $1); $image[$PIC_BEGIN_USERCONF + $EE{'eeServ2'}]=pack('n', $2); $image[$PIC_BEGIN_USERCONF + $EE{'eeServ3'}]=pack('n', $3); $image[$PIC_BEGIN_USERCONF + $EE{'eeServ4'}]=pack('n', $4); $ipok=1; } } SKIP_SERVER_IP: $ipok=0; while (!$ipok) { print "\nPlayer's IP address --> "; $addr=; chomp($addr); if ($addr=~/(\d+)\.(\d+)\.(\d+)\.(\d+)/ && $1<255 && $2<255 && $3<255 && $4<255) { $image[$PIC_BEGIN_USERCONF + $EE{'eeIP1'}]=pack('n', $1); $image[$PIC_BEGIN_USERCONF + $EE{'eeIP2'}]=pack('n', $2); $image[$PIC_BEGIN_USERCONF + $EE{'eeIP3'}]=pack('n', $3); $image[$PIC_BEGIN_USERCONF + $EE{'eeIP4'}]=pack('n', $4); $ipok=1; } } $ipok=0; while (!$ipok) { print "\nNetmask --> "; $addr=; chomp($addr); if ($addr=~/(\d+)\.(\d+)\.(\d+)\.(\d+)/ && $1<=255 && $2<=255 && $3<=255 && $4<=255) { $image[$PIC_BEGIN_USERCONF + $EE{'eeMask1'}]=pack('n', $1); $image[$PIC_BEGIN_USERCONF + $EE{'eeMask2'}]=pack('n', $2); $image[$PIC_BEGIN_USERCONF + $EE{'eeMask3'}]=pack('n', $3); $image[$PIC_BEGIN_USERCONF + $EE{'eeMask4'}]=pack('n', $4); $ipok=1; } } $ipok=0; while (!$ipok) { print "\nGateway --> "; $addr=; chomp($addr); if ($addr=~/(\d+)\.(\d+)\.(\d+)\.(\d+)/ && $1<255 && $2<255 && $3<255 && $4<255) { $image[$PIC_BEGIN_USERCONF + $EE{'eeGate1'}]=pack('n', $1); $image[$PIC_BEGIN_USERCONF + $EE{'eeGate2'}]=pack('n', $2); $image[$PIC_BEGIN_USERCONF + $EE{'eeGate3'}]=pack('n', $3); $image[$PIC_BEGIN_USERCONF + $EE{'eeGate4'}]=pack('n', $4); $ipok=1; } } SKIP_PROMPTS: if ($^O =~ /^m?s?win/i) { $mac=~s/:/-/g; system('net stop slimp3svc'); } !$quiet && print "Creating static ARP entry. \"arp -s $ip $mac\"\n"; if (!$runAsEmbeddedTool && system("$arp -s $ip $mac")) { done(1); } my $udpsock = IO::Socket::INET->new( Proto => 'udp', # LocalPort => $LOCALPORT, ); if (!$udpsock) { print "socket: $!"; done(1); } my $remotepaddr = sockaddr_in($REMOTEPORT, inet_aton($ip)); # set remote port to "3483" $image[0x2112]=pack('n', 0x000D); $image[0x2113]=pack('n', 0x009B); &writeRange($udpsock, $remotepaddr, \@image, $PIC_BEGIN_ISR, $PIC_END_ISR); &writeRange($udpsock, $remotepaddr, \@image, $PIC_BEGIN_MAIN, $PIC_END_MAIN); &writeRange($udpsock, $remotepaddr, \@image, $PIC_BEGIN_USERCONF, $PIC_END_USERCONF); #------------------------------------------------------ # This will allow the user to set a new MAC address if (defined($newmac)) { if ($newmac =~ /([0-9A-Fa-f][0-9A-Fa-f]):([0-9A-Fa-f][0-9A-Fa-f]):([0-9A-Fa-f][0-9A-Fa-f]):([0-9A-Fa-f][0-9A-Fa-f]):([0-9A-Fa-f][0-9A-Fa-f]):([0-9A-Fa-f][0-9A-Fa-f])/) { $image[0x21E0] = pack('n', hex("0x00". $1)); $image[0x21E1] = pack('n', hex("0x00". $2)); $image[0x21E2] = pack('n', hex("0x00". $3)); $image[0x21E3] = pack('n', hex("0x00". $4)); $image[0x21E4] = pack('n', hex("0x00". $5)); $image[0x21E5] = pack('n', hex("0x00". $6)); &writeRange($udpsock, $remotepaddr, \@image, $PIC_BEGIN_FACTORY, $PIC_END_FACTORY); } } #------------------------------------------------------ !$runAsEmbeddedTool && !$quiet && print "\n\n"; ($runAsEmbeddedTool || !$quiet) && print "Firmware installation was successful!\nThe SLIMP3 will now reboot automatically.\n\n"; &sendRunCommand($udpsock, $remotepaddr); !$runAsEmbeddedTool && system("$arp -d $ip"); if ($^O =~ /^m?s?win/i) { system('net start slimp3svc'); } done(0); } main(); sub done { my $ret = shift; if (!$quiet && !$runAsEmbeddedTool) { print "\n\nPress return to exit.\n"; }; exit $ret; } __END__