#!/usr/bin/perl use strict; use Device::SerialPort; use warnings; use MacPerl 'DoAppleScript'; use Time::HiRes qw(time usleep); $|++; # version 3.3 # the PIC code was changed to include a volume variable. It is the last # variable sent by the PIC before the Z. The incoming stream looks like this: # "a 1 2 3 4 5 6 7 8 V z" where 1-8 are addresses (or 0 if empty), and the v is # up to 4 digits of volume # version 3.2 # the sleep delay wasn't helping too much so we changed the way it # gets the slot values to read in all the slot values at once # version 3.1 # added a sleep delay of 5 milliseconds between each read/write call # version 3.0 # Last modified November 30 # This version connects via bluetooth and uses our 2nd-generation # addressing scheme, where each case has a PIC inside that gives out a value, # and we address each case to change its internal LEDs my $debug = 0; my $debug2 = 1; ################################################ ####### DECLARE VARIABLES ###################### ################################################ ######### CONSTANTS ########## my $ASK_FOR_ID = "?"; # the character to send when asking for the ID number # at a slot position my $TOWER_ADDRESS = "8"; # the character used to address commands to the tower ## colors my $RED = "h"; my $GREEN = "b"; my $BLUE = "d"; my $YELLOW = "g"; my $PURPLE = "e"; my $WARMUP_COLOR = $YELLOW; my $COOLDOWN_COLOR = $PURPLE; my $PLAYING_COLOR = $GREEN; my $WELCOME_COLOR = $BLUE; ## play modes my $ITJ_STANDARD_MODE = 1; my $ITJ_RANDOM_MODE = 2; my $PREFS_PATH = "preferences/"; my $SLEEP_TIME = 5000; # number of microseconds to sleep between read/write calls # in this case, sleep for 5 millisecond (= 5000 microseconds) ###### PLAYLISTS ###### my $playingPlaylist = "ITJ"; my $sourcePlaylist = "ITJ source"; my $randomPlaylist = "ITJ random"; ##### DEVICES ###### my $port; #### VARIABLES ###### my $previousSlot = -1; my $currentSlot = -1; my $nextSlot = -1; my @playedSlots = (0,0,0,0,0,0,0,0); # which slots have been played? my @lastValues = (0,0,0,0,0,0,0,0); # what were the last values read for these # slots? my @slotValues = (0,0,0,0,0,0,0,0); # hold the values read from the PIC chip my @lights = (0,0,0,0,0,0,0,0); #my %tracksValues = getPrefs(1); # this associative array holds the possible # values and track numbers. KEY is the # track value, VALUE is the track number my %tracksValues = ('$' => '1', 'A' => '2', 'B' => '3', 'C' => '4', 'D' => '5', 'E' => '6', 'F' => '7'); my $volume = "50"; my $portPath = getPrefs(3); my $playMode = $ITJ_STANDARD_MODE; ################################################# ################################################# mainLoop(); closePort(); sub mainLoop { setup(); while (1) { print "ENTERING LOOP!\n" if $debug2; lightEmUp(); print "PLAYMOD IS $playMode\n" if $debug2; @lastValues = @slotValues; print "last values are @lastValues\n" if $debug2; @slotValues = getSlotValues(); #setVolume(smoothVolume($volume)); print "set volume to " . smoothVolume($volume) . "\n" if $debug2; print "slot values are @slotValues\n" if $debug2; @playedSlots = updatePlayedSlots(\@playedSlots,\@slotValues,\@lastValues); print "played slots are @playedSlots\n" if $debug2; welcome(getNewSlots(\@slotValues,\@lastValues)); # tell any newly inserted # cases to turn on to the # "welcome" color $nextSlot = getNextSlot($currentSlot,\@slotValues,\@playedSlots); #print "next slot is $nextSlot\n" if $debug2; if ($playMode == $ITJ_STANDARD_MODE) { if (!(ASplaying())) { # If we aren't already playing anything, go ahead and play # the song in the next slot if something is available print "standard mode, we are not already playing anything\n" if $debug2; if ($nextSlot != -1) { # if there is a next slot available print "standard mode, we aren't playing, and a next slot is availalbe...next slot is $nextSlot, prev is $previousSlot, current is $currentSlot\n" if $debug2; $previousSlot = $currentSlot; $currentSlot = $nextSlot; print "starting a song to play.. playing song with value $slotValues[$currentSlot] at slot $currentSlot\n" if $debug2; ASplayValue($slotValues[$currentSlot]); twinkle($currentSlot); $playedSlots[$currentSlot] = 1; coolDown($previousSlot) unless ($previousSlot == -1); } else { # if we aren't playing, and there is no next slot, go to random mode coolDown($currentSlot) unless ($currentSlot == -1); $playMode = $ITJ_RANDOM_MODE; print "standard mode we aren't playing anything and there is no next slot...goingn to random\n" if $debug2; } } else { # We are already playing print "it is standard mode and we are already playing something. we are playing slot $currentSlot, the next slot is $nextSlot, the previous slot is $previousSlot\n" if $debug2; if (ASgetSecondsLeft() < 11) { warmupSlot($nextSlot) unless ($nextSlot == -1); } # If we are already playing something we will only switch songs if the # currently playing slot has changed...if the next slot is the same as the # current slot, that means the current slot must have changed. if ($nextSlot == $currentSlot) { print "we are already playing, and the next slot now equals the current slot, so we are going to change the song and next slot eq $nextSlot, currentSlot eq $currentSlot\n" if $debug2; $previousSlot = $currentSlot; $currentSlot = $nextSlot; ASplayValue($slotValues[$currentSlot]); twinkle($currentSlot); $playedSlots[$currentSlot] = 1; coolDown($previousSlot) unless ($previousSlot == -1); } # likewise, if the current slot value is now 0, we will want to see if # there is any next slot cued and if there is we will jump to it. But if # not then we will continue playing the current song and switch on random mode if ($slotValues[$currentSlot] eq "0") { print "standrad mode, already playing, and the current slot value now equals 0 even though a song is playing." if $debug2; if ($nextSlot != -1) { $previousSlot = $currentSlot; $currentSlot = $nextSlot; print "standard mode already playing, current slot equals 0 and there is a next slot, so we are switching to slot $currentSlot\n" if $debug2; ASplayValue($slotValues[$currentSlot]); twinkle($currentSlot); $playedSlots[$currentSlot] = 1; coolDown($previousSlot) unless ($previousSlot == -1); } else { print "standard mode, already playing, there is nothing new coming up and the current slot equals 0, so we switch to random mode but right now it does nothing" if $debug2; # the current slot is 0 and there is nothing coming up, so we go to random mode #$playMode = $ITJ_RANDOM_MODE; } } } # end the if !playing - else } # end if standard mode if ($playMode == $ITJ_RANDOM_MODE) { # check to see if we can switch back to standard mode if ($nextSlot != -1) { print "RANDOM MODE and there is a next slot! next slot is $nextSlot\n" if $debug2; # there is a next slot lined up! let's play it and change modes $previousSlot = $currentSlot; $currentSlot = $nextSlot; ASplayValue($slotValues[$currentSlot]); twinkle($currentSlot); $playedSlots[$currentSlot] = 1; coolDown($previousSlot) unless ($previousSlot == -1); $playMode = $ITJ_STANDARD_MODE; } if (!(ASplaying())) { print "RANDOM MODE and nothing is playing, so we play a random song\n" if $debug2; # If nothing is playing, play something! (this probably means a song was playing but # its case was pulled out and so the mode changed to random and then the song ended) ASrandomMode(); # start playing a random song } } # end if random mode } } sub lightEmUp() { for (my $i = 0; $i < 8; $i++) { if ($lights[$i] ne "0") { sendVal($i); #tell the pic to address this slot sendVal($lights[$i]); # send the color we have for this light } } } ###################################### [twinkle] ############################################## # tells the slot that is currently playing to turn the "currently playing" color ###### sub twinkle { my $slot = shift; sendVal($slot); # we are talking to this slot sendVal($PLAYING_COLOR); # tell it to turn the "currently playing" color $lights[$slot] = $PLAYING_COLOR; } ###################################### [warmupSlot] ############################################## # tells the slot that is about to be playing to turn a special color ###### sub warmupSlot { my $slot = shift; sendVal($slot); # we are talking to this slot sendVal($WARMUP_COLOR); # tell it to turn the warmup color $lights[$slot] = $WARMUP_COLOR; } ###################################### [coolDown] ############################################## # tells the slot that just finished playing to turn the "already played" color ###### sub coolDown { my $slot = shift; sendVal($slot); # we are talking to this slot sendVal($COOLDOWN_COLOR); # tell it to turn the cooldown color $lights[$slot] = $COOLDOWN_COLOR; } ######################### [getNextSlot] ############################## # returns the next available slot # looks through the next 7 slots, looping "over the top" (i.e., 8 -> 1) # using modulo division. # NOTE: Remember to pass by reference (use getNextSlot(..,\@slotsValues,....)) # returns -1 if no new empty slot is found. (good time to switch the playmode ########### sub getNextSlot { my ($curSlot,$slotValuesPtr,$playedSlotsPtr) = @_; my @slots = @$slotValuesPtr; # Because of the way perl passes arrays my @played = @$playedSlotsPtr; # we need to pass pointers to these # arrays. This de-references them my $deep_debug = 0; # turn this on to get a good look at what is going on for (my $i = 0; $i < 8; $i++) { # we will cycle through all 8 # slots here. We do this because # we do want to check and see if # the current slot has changed, too $curSlot = ($curSlot + 1) % 8; # look at the next slot print "getNextSlot: examining slot $curSlot\n" if $deep_debug; unless ($played[$curSlot]) { print "getNextSlot: $curSlot has not been played\n" if $deep_debug; if ($slots[$curSlot] ne "0") { print "getNextSlot: value at $curSlot is $slots[$curSlot]\n" if $deep_debug; print "getNextSlot: returning value $curSlot\n" if $deep_debug; return $curSlot; } } } return -1; # we did not find a new empty slot } ############################ [updatePlayedSlots] ########################## # after you get new slot values, use this function to compare the new against # the old and it returns a list of which slots have been played. # NOTE: pass the arrays by reference (i.e, use \@playedSlots) ####### sub updatePlayedSlots { my ($playedPtr, $slotsPtr, $oldSlotsPtr) = @_; my @played = @$playedPtr; # de-reference the arrays my @slots = @$slotsPtr; # de-reference the arrays my @oldSlots = @$oldSlotsPtr; # de-reference the arrays my $curSlot; for (my $i = 0; $i < 8; $i++) { if ($slots[$i] ne $oldSlots[$i]) { # if the current slot is different # from the last recorded value $played[$i] = 0; # reset the playcount for that slot } } return @played; } ############################ [welcome] ########################## # "welcomes" each new case by telling it to turn to the "welcome" # color ##### sub welcome { my (@newSlots) = @_; my $slot; foreach $slot (@newSlots) { sendVal($slot); # we are talking to this slot sendVal($WELCOME_COLOR); # acknowledge this slot $lights[$slot] = $WELCOME_COLOR; } } ############################ [getNewSlots] ########################## # similar to updatePlayedSlots in that it looks for changes, # but this routine returns all the slots that have new values in them ####### sub getNewSlots { my ( $slotsPtr, $oldSlotsPtr) = @_; my @slots = @$slotsPtr; # de-reference the arrays my @oldSlots = @$oldSlotsPtr; # de-reference the arrays my @newSlots; for (my $i = 0; $i < 8; $i++) { if (($slots[$i] ne $oldSlots[$i]) && ($slots[$i] ne "0")) { push(@newSlots,$i); } } return @newSlots; } sub getSlotValues { my @try1; my @try2; my @try3; my @returnArray; @try1 = getSlotValuesOnce(); @try2 = getSlotValuesOnce(); @try3 = getSlotValuesOnce(); for (my $i = 0; $i < 9; $i++) { if ( ! ( ($try1[$i] eq $try2[$i]) && ($try1[$i] eq $try3[$i])) ) { print "DISCREPANCY READING SLOT VALUES!!! TRYING AGAIN!!!!" if $debug2; return getSlotValues(); } } @returnArray = @try1; return @returnArray; } sub smoothVolume { my $newVolume = shift; # volume will be between 0 and 1063 $newVolume = $newVolume / 10; if ($newVolume >= 100) { return 100; } else { return $newVolume; } } sub setVolume { my $newVolume = shift; if ( ($newVolume < 0) || ($newVolume > 100) ) { warn "sub setVolume: an incorrect volume parameter ($newVolume) was entered."; } my $script = qq| tell application "iTunes" set the sound volume to $newVolume end tell |; AppleScript($script); } ############################### [getSlotValues] ############################# sub getSlotValuesOnce { print "sub getSlotValues: \n" if $debug; my @readArray; my @returnArray; my $concatString; my $totalBytes; sendVal($ASK_FOR_ID); usleep($SLEEP_TIME); # sleep $concatString = ""; $totalBytes = 0; while (1) { (my $count_in, my $string_in) = $port->read(1); # read 1 byte $concatString .= $string_in; $totalBytes += $count_in; if ($count_in == 0) { last; #exit } } print "read in $totalBytes bytes. the string is: $concatString\n" if $debug; chop($concatString); chop($concatString); @readArray = split(/ /,$concatString); if ($#readArray != 10) { # total length should be 11 warn "length of the slot values is wrong! length is $#readArray + 1\n"; } if ($readArray[0] ne "a") { warn "first letter of slot values is not 'a'. First letter is $readArray[0]\n"; } if ($readArray[10] ne 'z') { warn "last letter of slot values is not 'z'. Last letter is $readArray[9]\n"; } @returnArray = @readArray[1..8]; $volume = $readArray[9]; print "volume is $volume\n" if $debug2; print "returning @returnArray\n" if $debug; return @returnArray; } ############################# [getSlotID] ##################################### sub getSlotID { my $slot = shift; # the slot to get the ID of print "sub getSlotID: checking slot $slot\n" if $debug; sendVal($slot); usleep($SLEEP_TIME); # pause for a bit my $id = sendReadVal($ASK_FOR_ID,1); return $id; } ################################## [ASrandomMode] ############################## sub ASrandomMode { my $script = qq| tell application "iTunes" if (not (user playlist "$randomPlaylist" exists)) then set shuffle of playlist "library" to true set shuffle of playlist "library" to false set shuffle of playlist "library" to true play playlist "library" else if (count tracks of playlist "$randomPlaylist") is 0 then set shuffle of playlist "library" to true set shuffle of playlist "library" to false set shuffle of playlist "library" to true play playlist "library" else set shuffle of playlist "$randomPlaylist" to true set shuffle of playlist "$randomPlaylist" to false set shuffle of playlist "$randomPlaylist" to true play playlist "ITJ random" end if end tell|; AppleScript($script); } ############################ [lightTower] ##################################### sub lightTower { my $color_code = shift; sendVal($TOWER_ADDRESS); # tell the tower we are talking to it my $result = sendVal($color_code); return $result; } ############################ [_ASgetPlayState] ################################## sub _ASgetPlayState { my $script = qq| tell application "iTunes" get player state end tell |; my $result = AppleScript($script); print "_ASgetPlayState: result is: $result\n" if $debug; return $result; } ############################ [ASplaying] ##################################### sub ASplaying { print "play state is " . _ASgetPlayState . "\n" if $debug2; if (_ASgetPlayState eq "playing") { print "we are playing !\n" if $debug2; return 1; } else { print "not plyaing!\n" if $debug2; return 0; } } ############################ [ASgetSecondsLeft ] ############################## sub ASgetSecondsLeft { my $script = qq| tell application "iTunes" set pos to player position set theEnd to finish of current track return (theEnd - pos) end tell |; my $result = AppleScript($script); print "$result seconds left in current track!\n" if $debug; return $result; } ########################### [_lookupTrack] ################################### sub _lookupTrack { my $testValue = shift; my $trackNumber = $tracksValues{$testValue}; if ($trackNumber) { return $trackNumber; } else { warn "Unable to lookup value for $testValue\n" if $debug; return -1; } } ########################### [ASplayValue] ################################### sub ASplayValue { my $valToLookup = shift; print "ASplayValue: looking up value for $valToLookup\n" if $debug2; my $trackNumber = _lookupTrack($valToLookup); print "ASplayValue: playing track $trackNumber from playlist $sourcePlaylist into user playlist $playingPlaylist\n" if $debug2; unless ($trackNumber == -1) { _ASemptyPlayingPlaylist(); my $script = qq| tell application "iTunes" duplicate track $trackNumber of user playlist "$sourcePlaylist" to user playlist "$playingPlaylist" play user playlist "$playingPlaylist" end tell |; print "the appliscript that doesn't work is:\n$script\n" if $debug2; my $result = AppleScript($script); print "the result of this rotten applescript is: $result\n" if $debug2; } } ########################### [_ASemptyPlayingPlaylist] ########################## sub _ASemptyPlayingPlaylist { my $script = qq| tell application "iTunes" delete user playlist "$playingPlaylist" make user playlist with properties {name:"$playingPlaylist", shuffle:false} end tell |; print "the other applescript is\n$script\n" if $debug2; AppleScript($script); } ############################# [sendVal] ######################### sub sendVal { my $valToSend = shift; print "sub sendVal: sending value $valToSend\n" if $debug; usleep($SLEEP_TIME); my $count_out = $port->write($valToSend); warn "write failed during subroutine \"sendVal\"" unless ($count_out == 1); } ############################# [readVal] ######################### sub readVal { my $length_to_read = shift; $length_to_read = 1 unless ($length_to_read); # default to read 1 byte print "readVal reading $length_to_read bytes\n" if $debug; (my $count_in, my $string_in) = $port->read($length_to_read); print "readVal: read $count_in bytes, the string is: $string_in\n" if $debug; return $string_in; } ############################# [sendReadVal] ######################### sub sendReadVal { (my $sendVal, my $readLength) = shift; $readLength = 1 unless ($readLength); # default to read 1 byte print "sub sendReadVal sending byte $sendVal, reading $readLength bytes\n" if $debug; sendVal($sendVal); usleep($SLEEP_TIME); my $returnVal = readVal(1); return $returnVal; } ########################### [getPrefs] ################################### sub getPrefs { my $prefsType = shift; my $file; if ($prefsType == 1) { $file = $PREFS_PATH . "tracksHash.txt"; } if ($prefsType == 3) { $file = $PREFS_PATH . "serialPort.txt"; } open(F, "< $file") or die "Can't open preferences file $file"; my @lines = ; close(F); # read the hash of the tracks Values...tab-separated VALUE \t TRACKNUMBER unless ($prefsType == 3 ) { my %returnHash = (); my $line; foreach $line (@lines) { my ($key, $val) = split(/\t/,$line); $returnHash{$key} = $val; } return %returnHash; } if ($prefsType == 3) { # serial port return $lines[0]; } } ########################### [AppleScript] #################################### Subroutine to actually run a bit of AppleScript. sub AppleScript { my($script) = shift @_; my $result = DoAppleScript($script); return $result; } ########################### [setup] ################################### sub setup { $port = openSerialPort(); my $script = qq| tell application "iTunes" set fixed indexing to true if not (user playlist "$playingPlaylist" exists) then make user playlist with properties {name:"$playingPlaylist", shuffle:false} end if if not (user playlist "$sourcePlaylist" exists) then make user playlist with properties {name:"$sourcePlaylist", shuffle:false} end if activate end tell |; AppleScript($script); if (1) { for (my $i = 0; $i < 2; $i++) { my $result = sendVal("8"); print "result is $result\n" if $debug; } } lightTower($GREEN); # tell the world that we have a connection } ########################### [openSerialPort] ################################### sub openSerialPort { my $thePort=Device::SerialPort->new("/dev/tty.KeySerial1",1) || die "can't open the serial port $^E\n"; #$thePort->user_msg(ON); $thePort->databits(8); $thePort->baudrate(9600); $thePort->parity("none"); $thePort->stopbits(1); $thePort->handshake("none"); #none, rts or xoff $thePort->read_char_time(30); # 5 ms wait for each character $thePort->read_const_time(100); # 0.250 second per unfulfilled "read" call print "port is $thePort\n" if $debug; $thePort->write_settings || warn "didn't write settings"; return $thePort; } ########################### [closePort] ################################### sub closePort { $port->close || die "failed to close serial port"; undef $port; }