#!/usr/bin/perl -w # $Id: radioplayertag,v 1.3 2008/11/07 05:34:44 tomj Exp tomj $ # for ubuntu 8.04 # sudo apt-get install libcurses-perl libdbd-csv-perl libdevice-serialport-perl libmp3-tag-perl # POSSIBLE PROBLEM: non-response due to sleep 5 in init_radioplayer # when controller not responding? This might need overhaul -- maybe # a back-off timer to prevent no-controller from blocking. # _ _ _ # _ __ __ _ __| (_) ___ _ __ | | __ _ _ _ ___ _ __ # | '__/ _` |/ _` | |/ _ \| '_ \| |/ _` | | | |/ _ \ '__| # | | | (_| | (_| | | (_) | |_) | | (_| | |_| | __/ | # |_| \__,_|\__,_|_|\___/| .__/|_|\__,_|\__, |\___|_| # |_| |___/ # # Linux topend for the hacked radioplayer music system. # This program handles user interface, music playing, # and power management. It will attempt to run no matter what # errors occur and recover from unplugged cables and the like. # # http://wps.com/projects/MP3-system # # Copyright 2005, 2006, 2008 Tom Jennings # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public # License along with this program; if not, write to the Free # Software Foundation, Inc., 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. # # Does not use X. Runs in console or xterm, uses curses for # status display, but console display is not needed. # # For mpg321, see /usr/share/doc/packages/mpg321/README.remote # # The player process plays .mp3 songs from first to last, # left unmolested. This DOES NOT USE ID3 tag data. It uses # only pathname data. This player knows the music world # only as a list of discs (albums) (directories). # Period. That's all the user interface supports. Period. # CDs, podcasts, ripped vinyl and cassettes, captured streams, # all fit in this model just fine. You're not sitting in front # of a keyboard and display; you are sitting in an automobile. # RADIO PLAYER DEVICE # # This is a summary only; please see radio.c # # The radioplayer has two rotary encoders, two momentary push switches # on the encoder shafts, and one extra pushbutton on the front panel. # # left knob front panel right knob # VOL+ EXTRA FWD # VOL- BACK # PLAY (push) FASTF (velocity) # BACKF (velocity) # EXTRA (press, hold) # # The radioplayer outputs single-character commands, which # through no coincidence map to a numeric keypad in NUMLOCK state. # # Total interface states: # # *** POWER ON: retrieves stored state (album, track, frame and # volume) then jumps to PLAYING state, PAUSEd. A transient state. # Keys: none. # # *** PLAYING: Songs in current list play until exhausted, then # stops. # PLAY: alternates play/pause. # VOL+, VOL- # FWD, BACK: moves through the song list # PICK: enter PICK state # QUIT: exit player # EXTRA: toggle FWD/BACK forward-within-song/move through song list # # *** PICK: Selects disc. Music keeps playing. # pick-disc: # FWD, BACK: moves through disc list # FASTF, FASTB: moves through disc list alphabetically # PICK: exits to PLAYING with new play list # PLAY: exits pick, back to PLAYING, current song list. # # RADIO::USB CONNECTION # # The kernel connects the radioplayer via USB serial. The serial port # used is configured in here. # # In the real world things fail. This code assumes the radioplayer # could fail, be unplugged and replugged. # # The radioplayer read and write code checks for errors, and disconnects # from the device if failure, then continuously retries connecting # to it. If it fails/disconnects, writes to it are stopped. # # When the radioplayer connects (first time or after disconnect) writes # to it do not happen until we've successfully read a command # from it (eg. it's continuous power status commands). This means # we're past the bootloader code in the Arduino. Sending it data # during bootloader phase seems to make it hang (at least that's # the current theory). # Exit codes: # # 1 mpg321 errored during play # 2 problem starting up # 101 restart player (long-press volume, ignition on) # 102 shutdown computer (long-press volume, ignition off) my $VERSION= "1.27"; use strict; use Getopt::Std; # command line gunk use Curses; # epithets use IPC::Open3; # pipes to other programs use Symbol; # I forget! use Term::ReadKey; # get-keyboard-character-no-blocking use File::Glob qw(:globally); # many files at once use Device::SerialPort; # serial port to radioplayer use MP3::Tag; # We rely on the path for access to the programs below. # delete @ENV {'PATH', 'IFS','CDPATH','ENV','BASH_ENV'}; # $ENV{'PATH'}= ""; # Configurable items. Other people put them in config files. Nay I say! # ---------------------------------------------------------------- # Paths to stuff. my $conffn= "config"; # ingenious, eh? my $statefn= "state"; # where we store state my $logf= "radio.log"; # debug log # Inter-program ignition-off flag files. my $pfflagfile="IGNITION-OFF"; # output: ignition off my $netflagfile="NETOK"; # output: OK to run networking my $netdatafile="NETUP"; # input: networking data # ---------------------------------------------------------------- # This is the list of catalogs ("collections") from the config # file. The first one in the list is default. my %COLL= (); my $collection= ""; # A catalog is the off-line-created discname->directory mappings # for the a given collection. my %CAT= (); my $catalog= ""; # name of currently selected collection my $disc= ""; # name of currently selected disc my @TRACKS= (); # tracks available in disc directory my $trackN= 0; # index of currently-playing track # ---------------------------------------------------------------- # Radio controller; see WPS/Car-computer/Radio/radio.c. # An Arduino and a serial LCD (Sparkfun). my $tty= "/dev/ttyUSB0"; # the radioplayer controller port my $bps= 9600; # serial speed my $MAGIC= chr 124; # radioplayer LCD command sequence prefix my $CURSE= chr 254; # radioplayer LCD cursor pos sequence prefix my $CURSEOFF= 128; # LCD cursor-position offset my $RSQ= chr 0xd0; # LCD Hitachi 44780 long-dash char my $LCDPWR= chr 150; # power-cycle the LCD command # Aumix settings. my $aumixpcm= "75"; # PCM gain, good default. # Temperature monitor. my $thermfile= "/proc/acpi/thermal_zone/TZ00/temperature"; my $thermlimit= 60; # degrees CENTIGRADE we reboot my $thermcurr= 25; # current temperature # LCD display. The radioplayer display lines are addressed implicitly # by the arguments to &display ("line 1", "line2"). Messages are # simple text. The most commonly-seen screen is: # # SONG TITLE # #12 23:59 23:59 # # In which case the seemingly-formatted line is assembled by # &playing() by concatenating text generated by &poll_mpg321(). my $RCOLS= 16; # LCD number of columns my $RLINES= 2; # LCD number of lines # ---------------------------------------------------------------- # Programs. Assumes things are in the path. my $sudo= "sudo"; my $renice= "$sudo renice -10"; # eg '$renice $pid' my $devch= "$sudo chmod a+rwx $tty"; # eg. '$devch $tty' my $mpg321= "mpg321"; # our lovely player my @mpgargs= ( "-R", "dummyarg", # see README "--stereo", # probably useless "--skip-printing-frames=2" # less chatter ); my $aumix= "aumix"; my $killmpg= "$sudo killall mpg321"; # kill all/any players my $kill= "$sudo kill"; # my $shutdown= "$sudo shutdown"; # shutdown command (plus args) my $sdflag= "-h"; # try -z for suspend # ---------------------------------------------------------------- # Power management. # Shutdown time, in minutes, for each of 24 hours in a day. Eg. if # ignition=OFF at 810am, shutdown time is $SDT[8]. my @SDT= ( 30, 30, 30, 30, # midnight - 3am 30, 30, 30, 30, # 4am - 7am 125, 125, 125, 125, # 8am - 11am 65, 65, 65, 65, # noon - 3pm 125, 125, 125, 125, # 4pm - 7pm 65, 65, 65, 30, # 8pm - 11pm ); # If the radioplayer fails, we have no idea when ignition goes on # or off. After this many seconds of silence from the radioplayer, # shutdown. Note that SDT_NORADIO must be less time than $NORADIO # else it will continually re-start shutdown! my $SDT_IMMEDIATELY= 999; # phony value means NOW my $NORADIO= 1800; # 30 minutes of no radioplayer, shutdown my $SDT_NORADIO= 5; # no radio attached, blind and deaf my $SDT_HEAT= $SDT_IMMEDIATELY; # overheating # ---------------------------------------------------------------- # LCD brightness; the Sparkfun serial backpack is harcoded # 128=off, 157=brightest. However, they shipped me an old-code # version where backlight off (128) doesn't work. Doh. my $BLBASE= 128; # command offset my $BLOFF= 1; # backlight off (not really, 0=off) my $BLDIM= 22; my $BLMAX= 29; # ---------------------------------------------------------------- # Abstracted keyboard commands; keystrokes (and # radioplayer commands) map to these. my $PLAY= 1; my $VOLUP= 2; my $VOLDN= 3; my $PICK= 4; my $FWD= 5; my $BACK= 6; my $FASTF= 7; my $FASTB= 8; my $EXTRA= 9; my $QUIT= 10; my $PWRON= 11; my $PWROFF= 12; my $EXTON= 14; my $EXTOFF= 15; # And this is the keyboard/radioplayer controller mapper. my %CMDS= ( "\r" => $PLAY, "Z" => $PLAY, "8" => $VOLUP, "2" => $VOLDN, "+" => $PICK, "6" => $FWD, "4" => $BACK, "9" => $FASTF, "3" => $FASTB, "/" => $EXTRA, "*" => $QUIT, "E" => $PWROFF, "F" => $PWRON, "G" => $EXTON, "H" => $EXTOFF, "q" => $QUIT, # local keyboard "Q" => $QUIT, # local keyboard ); # Player status messages; text returned and displayed. my $PLAYMSG= "PLAY"; my $PAUSEMSG= "PAUSE"; my $DONEMSG= "DONE"; # ----------------------------------------------------------- # Screen dimensions and locations. # # 1 2 3 4 5 # 012345678901234567890123456789012345678901234567890123456789 # 4 Dawntreader-Mission # PLAY 23:59 23:59 # # Ignition: ON # Console display line numbers for status display. my $IGLINE= 5; # ignition state my $NDLINE= 6; # net enable state my $RDLINE= 7; # radioplayer state my $XTLINE= 8; # extern state # ----------------------------------------------------------- # Volume control trivia. my $volmax= 100; # aumix max volume (do not change) my $volmin= 2; # lowest possible volume (must be > 1) my $voldelta= 2; # vol. adjust per click # ---------------------------------------------------------------- # Other junk. my $pid= 0; # player PID my $pf= 0; # 1 == power failed, from radioplayer my $dimmer= 0; # 1 == headlights on, from radioplayer my @CMDIN= (); # radioplayer command buffer my $CMDMAX= 23; # max. radioplayer commands we queue up my @RLINECACHE= (); # cache of radioplayer display lines my $AFEWSEC= 100; # "a few seconds" of playing, in frames # These control the sliding display cut/splice. my $SQAMT= 5; # shift this many at a time my $sqpos= 0; # current position my $sqtime= time; # when to slide my $sqstring= ""; # current string my $ignplayer= 0; # -r ignore radioplayer my $ignign= 0; # -i ignore ignition state (yellow wire) my $blbug= 0; # -b sparkfun display without backlight bug my $lcdonetime= 0; # -Y one time init LCD serial backpack my $lastbl= -1; # last-set backlight value my $heardplayer= 0; # time since we recieved radioplayer command my $writeinit= 0; # 1 == write init commands to display my $writeOK= 0; # 1 == OK to write to the display my $Lopen= 0; # is open or not my $netdefon= 0; # 1 == networking enabled at startup # ---------------------------------------------------------- # # This code is a bit old-fashioned; pretty much all machine and # interface state is stored in these flat vars, updated by # low-level-ish routines that keep them current. Eh, it's a small # program. # mpg321 machine state. It's slightly tricky, in that # if mpg321 has nothing to say, and we read from the # pipe, it blocks. To prevent this we have a two-state # machine ("playout") that tells us whether we are expecting # mpg321 to say something. We maintain the state by # interpreting responses (eg. silence follows "STOP") # and by issued commands. my $playout; # 1 == more output expected from mpg321 my $paused; # 1 == mpg321 paused; cmd P continues play my $stop; # 1 == no more songs to play my $playing; # 1 == song is playing or paused my $track= ""; # playing track name my $msg= ""; # text reflecting mpg321 state PLAY, PAUSE, etc my $t1= ""; # play time 23:59 my $t2= ""; # remaining time 23:59 my $first_time; # special-case first PLAY # This stateful embarrassment determines whether FWD/BACK # advances through the song list, or moves within a song. my $fwdmode= 0; # 1 == move within song # Data used for display, generated by various sources. my $title= ""; # playing song name my $num= 0; # playing song number my $stats= ""; # player message; "PLAY 23:59 23:59" my $frame= 0; # current playing-song frame position my $frameR= 0; # remaining frames in song my $frameT= 0; # total frames in song (calc'ed) my $frameI= 0; # 1/100th total frames (calc'ed) my $volume= 0; # current volume level my $R= gensym(); # the plumbing to mpg321 my $W= gensym(); my $P= 0; # Device::SerialPort object, starts closed my $logmsgs= 0; # 1 == log all mpg321 messages my $exitcode; # parse when told to exit my $debug= 0; use vars qw/$opt_c $opt_d $opt_g $opt_r $opt_b $opt_i $opt_z $opt_h $opt_l $opt_Y/; getopts ('c:dglrbhziY'); &usage if $opt_h; $ignplayer= 1 if $opt_r; # -r no radioplayer attached $ignign= 1 if $opt_i; # -i ignore ignition wire $logmsgs= 1 if $opt_l; # -l log voluminously $blbug= 0 if $opt_b; # -b backlight bug disable exit if defined $opt_z; # -z check for warnings only! $conffn= $opt_c if defined $opt_c; # -c config file $lcdonetime= $opt_Y if defined $opt_Y; # one time LCD init $debug= 1 if $opt_d; # -d debug # Startup: log immediately. Get the display ready to use (to # complain on if errors), start up the player, then enter the # main play/commmand loop. We cancel any shutdown, because the # radioplayer will tell us power status and then we # can make our own decision. unlink($pfflagfile); # say power is on, $heardplayer= time; # (poll() looks at it!) &open_log(1); # start logging, &loadconfig($conffn); &shutdown(-1); # cancel any shutdown, $|= 1; if (! $debug) { ReadMode 4; # turn off something something initscr; # Curses init clear; # fill screen with " " } &start_mpg321(); # won't return until ready &load_state(); # load previous state, &load_catalog(); # load song ID3 catalog &vol(0); # restore original volume @TRACKS= &mp3list ($CAT{$disc}) if exists $CAT{$disc}; # re-load track list &playtrack(undef); # queue up saved track # If we cannot init the radioplayer, enable networking so we have some # way into the system. THIS IS A MAJOR SAVE! Hardly foolproof, # but better than nothing... if (not &init_radioplayer()) { &log ("Radio not found! Want to enable network here, but it would make us crash"); system("touch $netflagfile"); } $heardplayer= time; # time of last player command # This is the main loop that runs the player and executes commands # from the user. # # There are three modes from the user's point of view, that affect # key command interpretation: POWER-OFF which blocks all execution, # the PICK key which enters the pick-list code, and the default # mode that controls playing. while (1) { my ($key, $count)= &poll(); # manage the player, if ($key == $PICK) { &pick(); # pick tracks to play $fwdmode= 0; # back to default mode } elsif ($key == $QUIT) { $writeinit= 1; &sayonara(101, "exit player") if &ask("Restart player"); } elsif ($key) { if (!$debug and $key eq $PLAY) {# interface hack: clear; # force-refresh the refresh; # local screen } &command($key, $count); # process commands &save_state(); # checkpoint play state } elsif (not $playout) { # no key, not playing, &delay(200); # sleep a bit, } (my $s, $stats)= &playing(); # prep display data, &display(&slidetext($s), $stats); } # --- INTERFACE ------------------------------------------ # # &pick(); # # Picks a disc (album) to play, and starts playing # or exits with nothing playing. # # FWD and BACK navigate through the pick list. # Pressing EXTRA chooses the music collection. # # PICK DISC # PICK selects disc, starts playing 1st song # PLAY exits without changing selection # FWD, BACK navigate selections via choose() # sub pick { my $key; my $n; $n= 0; # wants to remember current position in the list! while (1) { my @A= sort keys %CAT; # linear list of discs ($n, $key)= &choose ("DISC", $n, @A); last if ($key == $PLAY); # cancel picking if play, if ($key == $EXTRA) { # &pick_catalog(); # choose music collection, next; } # else must be PICK, $disc= $A[$n]; # name of disc, @TRACKS= &mp3list ($CAT{$A[$n]}); # load it's tracks $trackN= 0; # start from first song &playtrack (0); # start playing! last; # we use while only for next's } } # --- INTERFACE ------------------------------------------- # # &pick_catalog(); # # Pick a music catalog; display names, PICK selects the # displayed collection name, PLAY exits. sub pick_catalog { my ($key, $n); $n= 0; # wants to be remembered! while (1) { my @A= sort keys %COLL; my ($n, $key)= &choose ("COLLECTION", $n, @A); last if ($key == $PLAY); # cancel picking if play, if ($key == $PICK) { # when selected, $collection= $A[$n]; $catalog= $COLL{$collection};# pick collection &load_catalog(); # load the catalog last; } } } # --- INTERFACE ------------------------------------------- # # ($n, $key)= &choose ("PROMPT", $n, @LIST); # # Pick an item from a list, starting with the Nth item, # and return the index of the selected item in the list # and the key that caused us to exit. # # BACK and FWD navigate the list; # FASTF and FASTB navigate "fast" (next/prev alpha) # PLAY, PICK, EXTRA and POWER exit. # All other keys are handled normally. # sub choose { my $prompt= shift; my $n= shift; # current selection, my @LIST= @_; # the list, my $N; # number of items in list. my $saved; # for big motion my $initial; my ($c, $count); $N= scalar @LIST - 1; # upper bound, return (0, $PLAY) if $N < 0; # oops, empty list! $n= $N if $n > $N; # bound it! while (1) { &display ($LIST[$n], $prompt); # Poll for keyboard commands; the ones we handle here get eaten # or dropped ('last'). ($c, $count)= &poll(); # check for a key, last if $c == $EXTRA; # last if $c == $PLAY; # PLAY/PAUSE terminates last if $c == $PICK; # exit &slidetext("") if $count; if ($c == $BACK) { --$n if $n > 0; $c= 0; # eat it } if ($c == $FWD) { ++$n if $n < $N; $c= 0; } # Assuming the list is sorted, FASTx moves to the next/previous # list item beginning with the next/previous letter; eg. if the $nth # item begins with "G", FASTF skips to the first item beginning with # "H" or subsequent letter. # # DDDEEEEEEEEEEEEEEEEEEEEEEEEFFFFFGGGGHHHHIIII # ^ here moves to there ^ # FASTF --------------------> if ($c == $FASTF) { $saved= $n; ($initial)= $LIST[$n] =~ /^(.)/; while (++$n < $N) { last if $LIST[$n] !~ /^$initial/; } $n= $saved if $n > $N; $c= 0; # eat the key } # Going backwards is different. If the item before this one # begins with the same letter we move to the first item in # the list that begins with this letter. # # (case 1) # CCCDDDDDDDDDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEE # ^ <-- to there ^ here moves # # but if the item before this one begins with a different # letter, we move to the first item that begins with that # letter: # # (case 2) # DDDEEEEEEEEEEEEEEEEEEEEEEEEFFFFFGGGGHHHHIIII # ^ <-- to there ^ here moves # # SHORTCUT: If case 1, we bump the index ($n) back 1, then # do case 2. if (($c == $FASTB) and ($n > 0)) { ($initial)= $LIST[$n] =~ /^(.)/; --$n if $LIST[$n - 1] !~ /^$initial/; ($initial)= $LIST[$n] =~ /^(.)/; while ($n > 0) { last if $LIST[$n - 1] !~ /^$initial/; --$n; } $c= 0; # eat the key } # Any keys we don't eat here, process as non-modal commands. &command($c, $count) if $c; # process other keys } return ($n, $c); } # --- INTERFACE ------------------------------------------ # # ($local_song_name, $stats)= &playing(); # # Given the current song number ($SONGn) and song list, # fill out the global display info and return # a highly munged display song name and stats for display. # A lot of ad hoc and subjective string munging. sub playing { my ($ltrack,$lstats); if (scalar @TRACKS < 1) { # bad &log("Empty track list!"); &load_state_init(); # brutal, but reliable } my $f= $fwdmode ? "*" : ""; # flag for FWD mode if ($msg eq $PLAYMSG) { # PLAY $stats= "#$num $f$t1 $t2"; # is implicit; status says so } elsif ($msg eq $PAUSEMSG) { # PAUSE is explicit $stats= "#$num $t1 $msg"; } else { # DONE is explicit $stats= "#$num $msg"; } if (time - $heardplayer > 10) { # brutal but effective $stats= "X$stats"; } return ($track, $stats); } # --- INTERFACE -------------------------------------------- # # ($key, $count)= &poll_keyboard(); # # Polls for keyboard or radioplayer commands; translates # them to the abstracted commands. Returns the command and # repeat count (see below) or (0, 0) if none. # # Characters are read as fast as possible and stuffed into a # ring buffer, and the top of the buffer is returned, if any. # However, since there can be many of the same character in a row # (NEXT knob spun quickly, etc) we pull identical commands in a # row out fo the buffer, and return the command and count. # # The power status from the radioplayer is very reliable, # assuming we are getting messages from it, since it's hard-wired # to IGNITION power and measures current state and not just # deltas like ACPI. # # If we get a valid command, mark as having heard from the radioplayer # (even if it came from the keyboard). sub poll_keyboard { my ($c, $count); &poll_temperature(); # prevent disasters &poll_radioplayer(); # pull commands from the player $c= ReadKey (-1); # non-blocking keyboard read push (@CMDIN, $c) if $c; # add to buffer, # Count the number of identical characters at the top of the # buffer, pulling them off as we count. $c= shift @CMDIN if scalar @CMDIN; # char from top $count= 1; while (scalar(@CMDIN) && ($c eq $CMDIN[0])) { # while same character, shift @CMDIN; # remove it, ++$count; # count it, } $heardplayer= time if $ignplayer; # fake this if ignoring return(0, 0) if not $c; # no key, return(0, 0) if not $CMDS{$c}; # unwanted key. $c= $CMDS{$c}; # xlate key to command # Before we return, process certain critical system commands. # Power status commands from the radioplayer are very reliable. # They also mean that the radioplayer is alive. When we get a # "power" command it's clearly OK to talk to the controller. # (Power commands are entangled in display reset.) if ($c == $PWROFF) { $writeOK= 1; # OK to write to display $heardplayer= time; $pf= 1; $c= $count= 0; } elsif ($c == $PWRON) { $writeOK= 1; # OK to write to display $heardplayer= time; $pf= 0; $c= $count= 0; # The green wire is connected to headlamps. Dim the display # when headlamps are on. } elsif ($c == $EXTOFF) { &backlight($BLDIM); $c= $count= 0; } elsif ($c == $EXTON) { &backlight($BLMAX); $c= $count= 0; } return ($c, $count); } # Convert decimal seconds into minutes:seconds. sub cvtsec { my $t= shift; ($t)= $t =~ /^(\d+)/; # clip off fraction, $t= 0 + $t; # force to a number, return sprintf ("%d:%02d", $t / 60, $t % 60); } # --- INTERFACE -------------------------------------------- # # &infodisp (); # # Display extra data on the local screen. sub infodisp { # args in @_ accessed in loop addstr ($IGLINE, 0, $pf ? "Ignition=OFF" : "Ignition=ON "); addstr ($NDLINE, 0, &netstatus()); addstr ($XTLINE, 0, $dimmer ? "Headlamps=ON " : "Headlamps=OFF"); addstr ($XTLINE+1, 0, "Temp=$thermcurr Limit=$thermlimit"); my $n= time - $heardplayer; addstr ($RDLINE, 0, $P ? "Radio=ON " : "Radio off=$n Limit=$NORADIO"); move ($LINES-1, $COLS-1); # put cursor off somewhere refresh; } # --- INTERFACE -------------------------------------------- # # init_radioplayer(); # # Open the radioplayer interface and ready it for use, if it is # not already open. If the device doesn't exist (which happens # when the the USB cable is out, assuming a USB device) nothing # happens. Once the device exists, we could hang here for some time # trying to open it. The idea is, a cable unplugged is either for # testing with curses only, or if a failure mode while installed # in the car, not much the software can do anyways except wait. # Should be called often enough to detect cable-disconnects etc. # # The radioplayer uses the Atmel bootloader; this means that # it is not ready to talk for 10 seconds after it is plugged in. # # Returns true if radioplayer is open, else 0. sub init_radioplayer { return 1 if $P; # already open return 1 if $ignplayer; # no player return 0 if ! -e $tty; # no such device system("$devch"); # force permissions (dumb) &log("Open Device::SerialPort ($tty)"); $P= new Device::SerialPort ($tty,undef,undef); if (not $P) { &log("Device::SerialPort $tty failed; cable?"); sleep (5); # people don't move that fast return 0; } $P-> baudrate($bps); # it's bitrate, dammit! $P-> read_char_time(0); # how long we wait $P-> read_const_time(0); # for a character &log("$tty open OK"); # Now initialize the display cache; this is a mirror image of the # display contents to minimize/optimize output. The cache is a simple # array, the start of each line's worth is at (line * cols). for (my $i= 0; $i < $RLINES * $RCOLS; $i++) { $RLINECACHE[$i]= 'X'; } # This triggers display() to write one-time initialization # commands to the display -- display writes are deferred until # we receive valid input from it. This eliminates the delay-until- # bootloader-complete issue. $writeinit= 1; # need to write commands # ... but it's not ready to write to until we're positively # heard from the radiocontroller. $writeOK= 0; &honk (1); &log ("beep: init_radioplayer"); return 1; } # --- INTERFACE ---------------------------------------------- # # &init_radioplayer_commands(); # # Write initialization commands to the radioplayer. This is # deferred until we hear a command from the radioplayer, so # that we know it is open. # # Display reset (LCDPWR) tells the radio controller to # power-cycle the display; the radio controller also defers # subsequent PWR* commands so that the receipt of one means # it's OK to write to the display again. The delay is likely # redundant, but hey, redundancy is cheap insurance. sub init_radioplayer_commands { return if not $writeinit; $writeinit= 0; &log ("Write display init"); # Power cycling the display certainly flushes out incomplete # commands and the like. $P->purge_tx(); # purge pending output, $P-> write (" "); # for the Arduino.. $P-> write ($CURSE); $P-> write ($LCDPWR); sleep (2); # reset time if ($lcdonetime) { &log ("LCD one time init"); $lcdonetime= 0; sleep (2); # $P-> write ($MAGIC); $P-> write (chr 4); # command 16 char width $P-> write ($MAGIC); $P-> write (chr 9); # TOGGLES splash display } for (my $i= 0; $i < $RLINES * $RCOLS; $i++) { $RLINECACHE[$i]= 'X'; # clear cache; force redraw } $writeOK= 0; # wait for PWR command $lastbl= -1; # force update } # --- INTERFACE ---------------------------------------------- # # close_radioplayer(); # # Close the radioplayer connection. Usually called if there is a # failure of some sort. sub close_radioplayer { return if not $P; # not open $P->purge_tx(); # throw away pending output &log("Closing Device::SerialPort $tty"); $P->close or &log ("Close Device::SerialPort failed"); $writeinit= 0; # probably not set $writeOK= 0; # not OK to write to display $P= 0; # mark as closed. } # --- INTERFACE ---------------------------------------------- # # &poll_radioplayer(); # # Read command(s) from the radioplayer, stuff them into # a ring buffer. Note that we read 1-to-many characters from # the controller; they are added to a FIFO from where they are # returned; only the last N characters are saved. For now I'm # assuming that it's possible to spin the dial(s) and queue up # far too many commands, and that we should honor only the last # few to make any sense of it. # # This is where a dead/broken/disconnected radioplayer is detected, # if it managed to open in the first place. sub poll_radioplayer { my $s; my $n; return undef if ! $P; # not open, reads nothing. while (1) { ($n, $s)= $P-> read(255); # drain the controller if (not defined $n) { # happens if port closes $stats= "DISCONNECT!"; # cable pulls out &log ("player read failed"); # oops &honk (2); # beep twice &log ("beep beep: poll_radioplayer"); &close_radioplayer(); # close it, it will reopen, $n= 0; # we're screwed. } last if $n == 0; push (@CMDIN, split (//, $s)) # add char(s) to the list if scalar @CMDIN < $CMDMAX; # if room (grows here > MAX) $heardplayer= time; # flag command received } splice (@CMDIN, 0, $n - $CMDMAX) # truncate the list if (scalar @CMDIN > $CMDMAX); # if too long } # --- INTERFACE -------------------------------------------- # # &display ($l1, $l2); # # Update the display with the given fields. The data # is truncated and processed to fit. Heavy text slidetext # for a compact, if inaccurate, display. sub display { # args in @_ accessed in loop my $wbuf; my $col; # Always do the local display. for (my $line= 0; $line < $RLINES; $line++) { clrtoeol ($line, 0); # update local display next if not $_[$line]; # addstr ($line, 0, $_[$line]); } &infodisp(); # additional info, refresh; return if not $P; # must be open return if not $writeOK; # OK to talk if we've heard &init_radioplayer_commands(); # send commands, once # The chr 255 is output to complete any escape sequence truncated # when we purge the TX buffer before the actual write. The logic # is, if we are given stuff to output to the display, anything # previously queued up is now meaningless, and should be thrown # away. So we purge TX. But there is a tiny possiblity that the # MAGIC/CURSE character is output, but the 2nd character is purged; # this would mess up the display (or worse) if the next character # output was eaten by the Sparkfun Serial LCD Backpack. Hence we # output a "safe" character before the real characters. Long story, # simple fix. my $wrote= 0; # did not write to radioplayer $wbuf= "" . chr 32; # nothing to output yet for (my $line= 0; $line < $RLINES; $line++) { my $t= shift @_; $t= "" if not defined $t; my @T= split (//, $t); # make it fit, while (scalar @T < $RCOLS) { push (@T, " "); # pad out with spaces } # This compares the new string to output against the current # radioplayer display, from left to right, and only outputs from the # left-most position that has changed. It's a limited optimization, # but works well enough here; the top line doesn't change often, # and the bottom line usually says #13 mm:ss mm:ss, and only # the last half changes often. Plus it's easy to code. for ($col= 0; $col < $RCOLS; ++$col) { last if $T[$col] ne $RLINECACHE[$line * $RCOLS + $col]; } next if $col >= $RCOLS; # no change; no output # $col points to the first character that doesn't match the cache; # copy the changed text into the cache. for (my $i= $col; $i < $RCOLS; ++$i) { $RLINECACHE[$line * $RCOLS + $i]= $T[$i]; } splice (@T, 0, $col); # remove chars to not output $wbuf .= $CURSE; # cursor escape prefix $wbuf .= (chr ($line * 64 + $col & 255 | $CURSEOFF)); # column, $wbuf .= join ("", @T); # text, } # wbuf now contains a command string to update the LCD display. # If there is anything to output, do it now. Note that we append # an escape sequence to put the cursor off-screen. my $i= length $wbuf; if ($i > 1) { # if something to write, $P->purge_tx(); # purge pending output, $wbuf .= $CURSE; # append cursor-off-screen, $wbuf .= (chr ($RCOLS & 255 | $CURSEOFF)); $i= length $wbuf; # (cursor cmd added) my $n= $P->write($wbuf); # attempt to write, if (not $n or ($n != $i)) { # write error, assume $stats= "DISCONNECT!"; # cable pulled out, etc &log ("player write failed"); # oops &honk (3); # beep twice &log ("beep beep beep: display"); &close_radioplayer(); # close it, it will reopen, } } } # --- INTERFACE -------------------------------------------- # # &backlight ($n); # # 0 < n < 28 # # 0 is off, 28 is brightest. sub backlight { return if ! $P; # do nothing if not open return if $blbug; # or backlight bug &log ("backlight out of bounds $b") if not defined($b) or ($b < $BLOFF) or ($b > $BLMAX); $b= $BLMAX if not defined($b) or ($b > $BLMAX); $b= $BLOFF if $b < $BLOFF; # bound new value return if $b == $lastbl; # no change, why bother $lastbl= $b; $P->write($MAGIC); $P->write(chr ($BLBASE + $b)); &log ("backlight $b"); } # --- INTERFACE ---------------------------------------- # # Ask a question, return true if EXTRA was pressed. sub ask { my $question= shift; my $prompt= "<--NO YES -->"; $question .= "?"; &display ($question, $prompt); while (1) { my ($c, $count)= &poll (); next if not $c; return 1 if $c == $PICK; return 0 if $c == $PLAY; } } # --- INTERFACE -------------------------------------------- # # $s= &slidetext ($s); # # Returns a "sliding windowed" portion of the input string. # This maintains a state machine such that repeated calls # returns a sliding portion of the text string to fit # onto the small display. It resets the state machine # when the input string is different than last call. # (Call with "" to force a reset.) # sub slidetext { my ($i, $n); my $s= shift; if ($s ne $sqstring) { $sqstring= $s; # string changed, reset $sqpos= 0; # anchor on left, $sqtime= time + 1; # initial display pause, } return "" if not $s; # empty/missing arg my $orig= $s; # for reporting bad names if ($s =~ /^\s*$/) { &log ("ERROR: squashing /$orig/ resulted in empty string! " . "From $TRACKS[$trackN]"); return ""; # went too far! } my @foo= split (//, $s); # string to array my $len= scalar @foo; # length of whole string # Fits in screen; no cutting needed. return $s if $len <= $RCOLS; # Slide the text through the field. push (@foo, " ", "-", "-", " "); $len= scalar @foo; # length of whole string for ($s="", $n= $RCOLS, $i= $sqpos; --$n > 0;) { $i= 0 if $i >= $len; $s .= $foo[$i++]; } if (time > $sqtime) { $sqpos= 0 if ($sqpos += $SQAMT) >= $len; $sqtime= time; } return $s; } # --- MACHINE ------------------------------------------ # # $error= &start_mpg321(); # # Start mpg321, our mp3 playing engine. We launch the program # and wait for it's initial "R" response. We don't return until # it's running, retrying forever. May be manually aborted. sub start_mpg321 { my ($c, $count); while (1) { system ("$killmpg"); # kill any lingering # &stop_mpg321(); # probably redundant &log ("launch $mpg321"); &display ("Launch mpg321"); $pid= open3 ($W, $R, "", # open our pipes, $mpg321, @mpgargs); if ($pid) { &log ("mpg321 pid $pid OK"); last; } &log ("$mpg321 failed to start"); &display ("mpg321 retry", "abort -->"); # point to right sw sleep (5); # Please note that we call poll_keyboard(), not poll(), which # would attempt to talk to the (not yet running) mpg321. ($c, $count)= &poll_keyboard(); &sayonara (2, "Launch aborted1") if ($c == $QUIT) && &ask("Abort launch"); } &log ("$mpg321 launched; wait for ready"); # Set up initial communication state then wait for the "R" # ready message. $playout= 1; # mpg321 will issue "R" response $paused= 0; # not paused $stop= 0; # not stop $playing= 0; # not playing $frame= 0; # start of track &display ("Await mpg321"); # Please note that we call poll() here, since mpg321 should be running # and we need to use the state machine to extract sense from it. do { ($c, $count)= &poll(); &sayonara (2, "Launch aborted2") if ($c == $QUIT) && &ask("Abort mpg321"); } while ($msg !~ /^R/); &log ("$mpg321 says \"$msg\""); # Gapping audio is annoying. Nice it to high priority. # # $c= `$renice $pid`; # is this necessary? # chomp $c; # &log ("renice mpg321 says \"$c\""); # Special-case the first PLAY. $first_time= 1; # special-case first PLAY $msg= $PAUSEMSG; $t1= ""; &display ("mpg321 ready"); } # --- MACHINE ------------------------------------------ # # &stop_mpg321(); # # Kill any/all mpg321's. This is safer than killing PIDs; we're # the only user and success is required. sub stop_mpg321 { return if not $pid; # not open system ("$kill -9 $pid"); my $n= $? >> 8; &log ("$kill -9 $pid returns $n"); } # --- MACHINE -------------------------------------------------- # # ($key, $count)= &poll(); # # This routine handles things considered to be "background" tasks; # it polls the radioplayer for commands, the keyboard for commands, # polls mpg321, feeds mpg321 it's next track when it runs out, # and watches for radioplayer failure and ignition off. # # If ignition goes off, control is passed to the exit code, # which shuts down various components, schedules a shutdown, then # waits for ignition-on, which may never come (in which case the # system shuts down). When power is restored, the program exits, # ready for another iteration. # sub poll { my ($c, $count); # command key &ignition_off(); # look for reasons to go OFF &poll_mpg321(); # run the machine, &play_next_track(); # occasionally advances track, &init_radioplayer(); # open radioplayer, as necessary ($c, $count)= &poll_keyboard(); # eat keys, watch power status return ($c, $count); # the keystroke/player command. } # --- MACHINE -------------------------------------------------- # # &ignition_off (); # # This watches for and deals with ignition-off state. If # ignition is on, or we're ignoring it (-i) nothing # happens (almost). # # (We do watch for silence from the radioplayer control head; # if it goes too long without a command output we schedule a # shutdown. Without it we'll never know ignition state.) # # At ignition-off, we pause playing, save player state and # schedule a shutdown, and then await ignition-on. # # While ignition is off we allow a few limited manual commands. # We also have to watch for USB cable unplug. However, while # ignition is off, there's no need to care about radioplayer # silence (eg.if it's unplugged during ignition off) since we # have a shutdown scheduled anyways. sub ignition_off { my ($c, $count); if (time - $heardplayer > $NORADIO) { # if radioplayer is silent, $heardplayer= time; # prevent fast looping! &log ("Shutdown due to silent radioplayer"); &shutdown ($SDT_NORADIO); # very soon } return if $ignign; # -i, ignore ignition state return if ! $pf; # return if ignition not off &pause_playing(); # stop playing, &save_state(); # save stuff, &log ("Ignition off"); &display ("Ignition off"); &shutdown(0); # schedule shutdown, &display ("Log checkpoint"); &log ("Checkpointing the log"); &close_log(); # close then open &open_log(1); # checkpoints the log system ("touch $pfflagfile"); # indicate ignition is off clear; # shutdown pees on screen &display ("Begin pf loop"); # Loop here while ignition is off. We allow some manual commands. # Flag $pf is set by &poll_; it's a command from the radioplayer. # The delay() relinquishes CPU, lowers load, and hence heat. It's done # only when there are no radioplayer commands to process. while ($pf) { ($c, $count)= &poll_keyboard(); # eat keys, watch power status my $net= &slidetext(&netstatus()); if ($c == $QUIT) { # QUIT shuts down &display($net, "Shutdown");# can't call ask() &sayonara (102, "Shutdown computer"); # doesn't return. } elsif ($c == $PLAY) { &display($net, "Boing!"); sleep(2); /* This is stupid and doesn't work. } elsif ($c == $PICK) { if (-e $netflagfile) { unlink ($netdatafile); unlink ($netflagfile); &log ("Manual net off"); } else { system ("touch $netflagfile"); &log ("Manual net on"); } sleep(2); */ } elsif ($count == 0) { # no command, &delay(200); # relinquish } &init_radioplayer(); # open radioplayer, as necessary &display($net, "SYSTEM ARMED"); } # Ignition is on again; return to normal, with the player paused. &display ("Ignition on"); &log ("Ignition on"); unlink ($pfflagfile); # ignition now on &shutdown (-1); # cancel shutdown clear; # shutdown pees on screen } # --- MACHINE -------------------------------------------------- # # $essid= &netstatus(); # # Returns a string describing network status; or the # ESSID of the connected network. sub netstatus { my $e= ""; my $a= ""; if (-e $netdatafile) { $e= "(net data error)"; if (open (NF, "<$netdatafile")) { $e= ; # 1st line is ESSID ; # 2nd is MAC $a= ; # 3rd is IP close NF; chomp $e; chomp $a; $e= "$e ($a)"; } } else { $e= "No network"; } return $e; } # --- MACHINE -------------------------------------------------- # # &poll_mpg321(); # # global $msg, $msg1 set. # # Keep mpg321, running in the background, happy (sic). # # Talking to mpg321 is tricky. Basically this is a state # machine with many branching states. The most-recent # status message strongly hints (sic) whether or not # further output will be available; attempts to read when # output isn't ready will BLOCK halting everything. # # Flag 'playout' is set if we expect more output from # mpg321; it's cleared otherwise to prevent blocking read. # # Junk from mpg321 is sometimes formatted into two message # texts, msg amd msg2. msg2 is is the track play time, and # msg the general message. $msg is made fixed width for # simpler display. sub poll_mpg321 { my $l; # line o' crap we read from the player, my $r; # the player status word (ID) my $a; # sometimes things follow ID my @MSG; # most-recent message from player, atomized if ($playout) { # don't block! $l= <$R>; # read from the player, if (! defined $l) { $playout= 0; $l= ""; # at least not undef $msg= "NO RESPONSE"; # eh &log("WARNING: $msg"); # eh } chomp $l; # remove newline, # Raw responses from the player look like # @S abc 123 abc 123 # Split them here; make response r=S (eg. @S) and args a= the rest of the line. ($r, $a)= $l =~ /^.(.)\s+(.*)/; # @S abc 123 abc 123 if ($l =~ /^ALSA/) { # AARGH! Why does this &log ("ERROR: $l") # talk to us? unless $l =~ /underrun/; } elsif (!defined $a or !defined $r) { $playout= 0; &log ("ERROR: BAD RESPONSE /$l/"); &display ("mpg321 died", "restarting"); sleep (5); &sayonara (1, "mpg321 failure"); } elsif ($r eq "E") { # mpg321 error $playout= 0; &log ("ERROR: mpg321 error /$a/"); } elsif ($r eq "R") { # program startup $playout= 0; $msg= "R $a"; # explicitly read at startup } elsif ($r eq "F") { # FRAME $playout= 1; # yes, we expect more coming, $playing= 1; # yup, it's playing @MSG= split (/\s+/, $a); $frame= 0 + $MSG[0]; # currently playing frame $frameR= 0 + $MSG[1]; # frames remaining $msg= $PLAYMSG; $t1= &cvtsec ($MSG[2]); $t2= &cvtsec ($MSG[3]); } elsif ($r eq "I") { # ID3 info $playout= 1; $a =~ s/[_\s]+/ /g; # squash spaces, &log ("INFO $a") if $logmsgs; $msg= $PLAYMSG; } elsif ($r eq "S") { # stats on the mp3 loaded $playout= 1; &log ("STATS $a") if $logmsgs; $msg= $PLAYMSG; } elsif ($r eq "P" && $a eq "0") { # PAUSE 0 $playout= 0; # output stops (not paused!) $playing= 0; # no longer playing, $paused= 0; # not paused, can't restart $msg= $DONEMSG; # eg. track end, } elsif ($r eq "P" && $a eq "1") { # PAUSE 1 $playout= 0; # output paused $playing= 1; # is otherwise "playing" $paused= 1; # paused, can restart $msg= $PAUSEMSG; # eg. pause command, } elsif ($r eq "P" && $a eq "2") { # PAUSE 2 $playout= 1; # output restarts $playing= 1; # is playing $paused= 0; # no longer paused $msg= $PLAYMSG; # PLAY after pause } else { $playout= 0; &log ("ERROR: /$l/ playing $TRACKS[$trackN]"); } } } # --- MACHINE -------------------------------------------- # # command ($key, $count); # # This handles all key commands/events that are not eaten by # modal code. Play/pause/etc keys control mpg321 machine state, # which is somewhat arbitrary (change at your own risk). For some # commands we honor the key count (volume, next, etc). sub command { my $c= shift; my $count= shift; if ($c == $PLAY) { # PLAY button, if ($stop) { # if stopped, $trackN= 0; # restart current list from 1st track &playtrack (0); $stop= 0; # no longer stopped } elsif ($paused) { # PLAY button, if paused, &cmd ("P"); # un-pauses } elsif ($playing) { # PLAY button, if playing, &cmd ("P"); # pauses # This is the start-up kludge. } elsif ($first_time) { &playtrack ($frame); $first_time= 0; } # FWD and BACK are modal, if EXTRA pressed while playing FWD # and BACK move within the playing track, else move through the # track list. } elsif ($c == $FASTF) { # fast fwd skips next track if (not $fwdmode) { # next track if default state &next_track($count); &playtrack(0); } elsif ($playing) { # else move within track $frame= &track ($frame, 1); &cmd ("J $frame"); } } elsif ($c == $FASTB) { # fast back skips prev track if (not $fwdmode) { # in track list &prev_track($count) if $frame < $AFEWSEC; &playtrack(0); } elsif ($playing) { # else within track $frame= &track ($frame, -1); &cmd ("J $frame"); } } elsif ($c == $FWD) { # FORWARD, if (not $fwdmode) { # next track if default state &next_track($count); &playtrack(0); } elsif ($playing) { # else move within track $frame= &track ($frame, 1); &cmd ("J $frame"); } } elsif ($c == $BACK) { # BACK if (not $fwdmode) { # in track list &prev_track($count) if $frame < $AFEWSEC; &playtrack(0); } elsif ($playing) { # else within track $frame= &tracknav ($frame, -1); &cmd ("J $frame"); } } elsif ($c == $EXTRA) { # and this is the mode select $fwdmode= $fwdmode ? 0 : 1; # These meanings never change. } elsif ($c == $VOLUP) { &vol ($count); } elsif ($c == $VOLDN) { &vol (-$count); } return $c; } # --- MACHINE -------------------------------------------- # # # $frame= $tracknav ($frame, $displacement); # # Moves through a track forward ($displacement > 0) or backwards # ($displacement < 0) within the bounds of track length, in frames. # FASTF will have a larger magnitude displacement than FWD, etc. # # We want every click of the encoder to be 1% of track play # time. Since mpg321 gives us current-frame and remaining-frames # (frequently, once started) we calc it once per track. playtrack() # clears it. It means that this command doesn't work until a # frame has played and mpg321 issues and @F output. sub tracknav { my $f= shift; my $disp= shift; return $f if $frameR == 0; # no @F yet if ($frameI == 0) { # calc once per track $frameT= $f + $frameR; # calc total frames, $frameI= int ($frameT / 100); # calc increment $frameI= 1 if $frameI < 1; &log ("$frameT frames, $frameI increment"); } my $t= $f + ($disp * $frameI); # new trial motion return $f if $t <= 0; # bound "back" return $f if $t >= $frameT; # bound "fwd" return $t; # else new value. } # --- MACHINE -------------------------------------------- # # &playtrack ($f); # &playtrack (undef); # # Begin playing the selected track ($TRACK[$trackN]) if the # frame number is defined, else just queue itup for later play. # sub playtrack { my $frame= shift; return if $trackN >= scalar @TRACKS; # no more tracks, &poll_mpg321(); if (not -e $TRACKS[$trackN]) { # oops, no file! &log ("playtrack says \"no file!\""); @TRACKS= (); # empty the list, $trackN= 0; $stop= 1; $msg= "none"; return; } if (defined $frame) { &log ("playtrack plays \"$TRACKS[$trackN]\""); &cmd ("L $TRACKS[$trackN]"); $stop= 0; # assume play it, &cmd ("J $frame") if $frame; # go to stated frame } # Fetch ID3 data for display. We hope! that the track number # matches directory file order! my $mp3 = MP3::Tag->new($TRACKS[$trackN]); my @tags= $mp3->get_tags(); # &log ("Tags for $TRACKS[$trackN]: " . join (" ", @tags)); # bug! something in MP3::Tag pees on the screen! clear; refresh; # This looks simpler. # my ($title, $track, $artist, $album, $comment, $year, $genre) = $mp3->autoinfo(); if (exists $mp3->{ID3v1}) { $num= $mp3->{ID3v1}->track; my $a= $mp3->{ID3v1}->album; my $b= $mp3->{ID3v1}->artist; my $c= $mp3->{ID3v1}->title; $track= "$a/$b/$c"; } else { &log ("No ID3v1 data for $TRACKS[$trackN]"); $num= $trackN; $track= $TRACKS[$trackN]; } $mp3->close(); $frameI= $frameT= 0; # trigger recalc &slidetext(""); # restart track display } # --- MACHINE -------------------------------------------- # # &play_next_track(); # # This watches for the status $msg from the player state machine, # and advances to the next track in the directory when the end of # the current track is reached. sub play_next_track { if (($msg =~ /^$DONEMSG/) && !$stop) { if (&next_track(1) == -1) { # end of track list, $stop= 1; # same as STOP button, } else { &playtrack(0); } &save_state(); # save changed track selection &slidetext(""); } } # --- MACHINE -------------------------------------------- # # $n= &next_track($n); # $n= &prev_track($n); # # Increment/decrement the track index by $n, bounding to the # size of the track array. This returns the next track number, # if valid, else -1 to indicate "past the end". The global # track index however is always valid. sub next_track { my $n= shift; return $trackN += $n if ($trackN + $n) < scalar @TRACKS; return -1; } sub prev_track { my $n= shift; return $trackN -= $n if ($trackN - $n) >= 0; return -1; } # --- MACHINE -------------------------------------------- # # &pause_playing(); # # Pause playing the current track, if playing and not # already paused. sub pause_playing { &cmd ("P") if $playing && !$paused; } # --- MACHINE ---------------------------------------------- # # cmd ("command string"); # # Issues the given command to the player, and sets the "playout" # flag, since we know if we can expect more output from here. sub cmd { my $s= shift; print $W "$s\n"; # issue command, $playout= 1; } # --- SUPPORT ---------------------------------------------- # # &shutdown(dir); # # Control system shutdown. -1 means cancel shutdown; 0 means # as per schedule; > 0 means that many minutes from now. sub shutdown { my $dir= shift; my $m; system("$shutdown -c >/dev/null 2>&1 &"); # blindly cancel shutdown &log("previous shutdown cancelled") if $? == 0; # shutdown was running if ($dir < 0) { # cancel only return; } elsif ($dir == $SDT_IMMEDIATELY) { # immediately $m= "now"; # (we repurposed "0") } elsif ($dir > 0) { $m= $dir; # specific schedule } else { # otherwise per schedule my $h= (localtime(time))[2]; # current hour, $m= $SDT[$h]; # pick from table, if (not $m) { &log("programmer is an idiot calcing shutdown sked"); $m= 10; # drop-dead failsafe. } $m= "+$m"; # yup } system ("$shutdown $sdflag $m >/dev/null 2>&1 &"); # start a shutdown &log("\"$shutdown $sdflag $m\" says \"$?\""); } # --- SUPPORT ---------------------------------------------- # # &poll_temperature # # Watch the system ACPI temperature for equal to or over # our threshhold. If so, halt ASAP. sub poll_temperature { my $s; # temp scale, F or C return if $thermlimit == 0; # skip if flagged as defunct if (-e $thermfile) { my @A= `cat $thermfile`; foreach (@A) { ($thermcurr, $s)= /\s(\d+)\s*([CF])/;# eg. 45C, 120F, last if defined $s; } } else { # no ACPI? &log ("$thermfile doesn't exist"); $thermlimit= 0; # never check again } # If no valid temperature found give up. if (not defined $thermcurr or not defined $s) { &log("ACPI temperature not found at $thermfile, abandoning"); $thermlimit= 0; $thermcurr= 0; return; } $thermcurr= ($thermcurr - 32) * 5 / 9 if $s eq "F";# convert to C if ($thermcurr >= $thermlimit) { &log("TOO HOT!! T=$thermcurr, maxT=$thermlimit, immediate shutdown"); &display ("TOO HOT! ${thermcurr}C", "Shutting down"); &honk (5); $thermlimit= 0; # prevent recursion! &shutdown ($SDT_HEAT); # halt. } } # --- SUPPORT ---------------------------------------------- # # &open_log($reopen); # # Open the log file, or if error, flag for stderr output. If # reopen is true, the file is not rolled over or truncated. sub open_log { my $reopen= shift; my $f= ">>"; # assume reopen, if (not $reopen) { rename ($logf, "$logf.previous");# rollover the log, $f= ">"; } if (open (L, "$f$logf")) { $Lopen= 1; &log ("$0 logging $f started"); # say hello } else { $Lopen= 0; &log ("Open /$logf/ failed! stderr only"); } } # --- SUPPORT ---------------------------------------------- # # &close_log(); # # Closes the log file. sub close_log { close L; # end logging $Lopen= 0; # not open } # --- SUPPORT ---------------------------------------------- # # # Write to the log file. Writes to stderr is the log isn't open. # sub log { my $msg= shift; my $d= `/bin/date +"%d-%b-%y.%H:%M:%S"`; chomp $d; $d= "$d $msg\n"; if ($Lopen) { print L $d; } else { print STDERR $d; } } # --- SUPPORT ------------------------------------------ # # $OK= load_catalog(); # # Load the currently-selected mp3 catalog to the # hash. Clears current-selection if there is a problem # loading it. Returns true if successful. # sub load_catalog { my $c= $COLL{$collection}; if (! defined ($c) or !open (F, "<$c")) { &log ("Can't open catalog \"$collection\""); $collection= ""; return 0; } foreach () { next if /^#/; chomp; my ($coll, $path)= split (/=/); next if not defined $path; $CAT{$coll}= $path; } return 1; } # --- SUPPORT ------------------------------------------ # # load_state (); # save_state (); # # Load and save current player state in a file for later # restart. We recreate the playlist from the stored indices, # if possible. Load is a bit slow, but save is fast. # # The state file format is simple: # # C a b c d e f .. Z \n # # Where C and Z are required fixed-text markers, and # a .. are the saved datums. C and Z make for simple assurance # that the regexp matched right without a lot of testing. sub load_state { my $version= ""; return &load_state_init() if not open (F, "<$statefn"); foreach () { chomp; next if /^#/; next if /^$/; my ($k, $a)= split (/=/, $_, 2); $version= $a if $k eq "Version"; $collection= $a if $k eq "Collection"; $disc= $a if $k eq "Disc"; $trackN= $a if $k eq "Track"; $frame= $a if $k eq "Frame"; $frameR= $a if $k eq "FrameR"; $volume= $a if $k eq "Volume"; } return &load_state_init() if $collection eq "" || $version ne $VERSION || not $volume; } # # Previous state not available or invalid; initialize to safe values. sub load_state_init { $collection= (sort keys %COLL)[0]; # first catalog @TRACKS= (); # initially nothing, $trackN= 0; # init picklist history, $volume= 30; # safe volume, $frame= 0; $frameR= 0; } # --- SUPPORT ------------------------------------------ # # &save_state(); # # Save player state for future restart. sub save_state { if (not open (F, ">$statefn")) { &log ("Can't create $statefn"); return; } my $d= `/bin/date +"%d-%b-%y.%H:%M:%S"`; print F "# Radioplayer state, saved $d\n"; print F "Version=$VERSION\n"; print F "Collection=$collection\n"; print F "Disc=$disc\n"; print F "Track=$trackN\n"; print F "Frame=$frame\n"; print F "FrameR=$frameR\n"; print F "Volume=$volume\n"; close F; } # --- SUPPORT ------------------------------------------ # # sayonara (code, message); # # Terminate the player, saving state, setting the exit code. sub sayonara { my $x= shift; my $m= shift; &save_state(); # save current state, &stop_mpg321(); $m= "no message" if not $m; &log ("$0 exit $x, $m"); &display ($m, "Code $x"); # write message, &close_radioplayer(); # flush, close, disconnect, &close_log(); ReadMode 0; # restore console, exit ($x); } # --- SUPPORT -------------------------------------------- # # &vol (n); # # Change volume by N (positive or negative). 0 sets volume # to the current value (sic). (Useful upon startup.) sub vol { my $n= shift; $volume += $n * $voldelta; # set new value, $volume= $volmin if $volume < $volmin; # bound it $volume= $volmax if $volume > $volmax; # &log("$aumix -v $volume"); system ("$aumix -w $aumixpcm -v $volume"); } # --- FILESYSTEM ------------------------------------------ # # list= &dirlist ($path); # # This returns a sorted list of directories within the current # directory. Only the directoryname is returned. # Ignores files, things beginning with dot, etc. sub dirlist { my $path= shift; my @L; $path =~ s/ /\\ /g; # escape all the spaces foreach (<$path/*>) { next if not -d $_; # only dirs next if /^\./; # ignore dotted names, chomp; s/^$path\///; # clip leading path, push @L, $_; # add to list, } return sort @L; } # --- FILESYSTEM ------------------------------------------ # # list= &mp3list (path); # # Returns a sorted list of fully path qualified .mp3 files # in the given path. Ignores directories, things beginning # with dot, etc. sub mp3list { my $path= shift; my @L; $path =~ s/ /\\ /g; # escape all the spaces foreach (<$path/*.mp3>) { next if not -f $_; # only files next if /^\./; # ignore dotted names, chomp; push @L, $_; # add to list, } return sort @L; } ############################################################ # # &loadconf("filename"); # # Load configuration items. sub loadconfig { my $f= shift; if (! open (LOADCONFIGF, "<$f")) { &log ("Can't load configuration file '$f'!\n"); return; } while () { chomp; s/^\s*//; # clip leading spaces, s/\s*$//; # clip trailing spaces, next if /^$/; # ignore blank lines, next if /^#/; # and comments my ($o, $a)= /^([\.\w]+)\s*=?\s*(.*)/; # split opt=arg # Allow limited quoted strings: if the first character is a detectable # quote character, then strip off an outside pair of them. if ($a =~ /^(["'])/) { # if a quote char, my $q= $1; # remember it, $a =~ s/$q(.*)$q/$1/; # dequote } $_= $o; # Stuff collection items into the hash. if (/radioplayer.collection/) { my ($name, $path)= split (/,/, $a); if ($path) { $COLL{$name}= $path; &log("Collection $name=$path"); } } $tty= $a if /radioplayer.dev/; $bps= $a if /radioplayer.bps/; $aumixpcm= $a if /aumix.pcm/; $pfflagfile= $a if /ignoff/; $netdatafile= $a if /netup/; $netflagfile= $a if /netok/; $thermfile= $a if /therm.file/; $thermlimit= $a if /therm.limit/; $netdefon= $a if /radioplayer.neton/; } close LOADCONFIGF; } ############################################################ # # Beep N times, with a delay if N > 1. # sub honk { my $N= shift; $N= 1 if not $N; # be reasonable $N= 5 if $N > 5; # likewise, I'm sure while ($N--) { beep; &delay (250) if $N; } } ############################################################ # # delay (N); milliseconds # # Delay for N milliseconds. sub delay { my $N= shift; $N /= 1000; # mS to sec select (undef, undef, undef, $N); # relinquish } # Feh. sub usage { print << "EHELP"; radioplayer radioplayer [ -i -b -l -r -d -Y -z ] -i Ignore ignition state. For testing, else +12V must be applied to the yellow wire for the radioplayer say it's OK to play. -b Sparkfun display WITHOUT the backlight bug. -l Logorrhea all pointless player messages. -r Ignore radioplayer; prevents shutdown of computer if no radioplayer attached and responding. -d Debug; do not init curses display, etc. -Y Issue a Sparkfun serial LCD backpack toggle splash display command, once. -z Terminates radioplayer without doing anything. Lets you see Perl compiletime warnings that disappear when curses clears the screen! EHELP exit 1; }