#!/usr/bin/perl
##########################################################################
#
# This script is to be used with a Toyota Prius 2004,2005 in conjunction
# with a CAN-view http://hybridinterfaces.ca/
#
# Will read in MFD touchscreen coordinates sent by the CAN-view and
# translate them into Apple Front Row AppleScript commands (key presses)
# to control FrontRow
# email: Jeremy Kusnetz jeremy@kusnetz.net for details or see
# http://www.kusnetz.net/prius/
#
# This script requires Mac::AppleScript::Glue,Device::SerialPort,Time::HiRes
# Use CPAN to download and install these modules.
#
# This script will need tweaking to work on your own system.  
# If your Mac spontaneously combusts after running this scrip I am not responsible!
# Be sure the serial port is pointing to your serial port.
#
# This script will now listen to a port and spit out XML containing the CAN-view
# formated messages!

my $version     = '2006.09.27,01';

my $debug       = 2;    # Debug  0=No output, 1=Mild debug output, 2=Verbose debug output.
my $fake	= 0;	# If fake then don't do serial, but make up numbers
my $genfake	= 0;	# Generate fake data

use strict;
use Socket;
use IO::Socket;
use POSIX qw(:sys_wait_h);
use Symbol;
use Fcntl qw(:DEFAULT);
use IPC::Shareable;
use Device::SerialPort;
use POSIX qw(strftime);
use Time::HiRes qw(usleep gettimeofday sleep);
use strict;
use Mac::AppleScript::Glue;
use Mac::AppleScript qw(RunAppleScript);
use DBD::mysql;
use DBI;

$/ = "\0";

my $PORT = 7890;
my $buffer = "";
my $touchscreenScrollDirection = "";
my %CAN = ();
my $elapsedTotal = 0;
my $lastsec = 0;
my $dblastsec = 0;
my $milesTotal = 0;
my $gallonsTotal = 0;
my $lastMileElapsedTotal = 0;
my $lastMileTotal = 0;
my $lastMileGallonsTotal = 0;
my $lastMph = -1;
my $lastSOC = -1;
my $mphAvg;
my $mpgAvg;
my $lastMileMpgAvg;
my $lastMileMphAvg;
my $lastStage;
my %db = ();
# Set to your serial port number
#my $port = "/dev/ttys0";
my $port = "/dev/tty.KeySerial1";
my $baud = "115200";
my $inputData = 255;

my $ob;

my @rv = ();

my $handle = tie $buffer, 'IPC::Shareable', undef, ;
my $touchscreenHandle = tie $touchscreenScrollDirection, 'IPC::Shareable', undef, ;

#my $listen         = new Net::Shared::Handler;
#my $new_shared     = new Net::Shared::Local ( name=>"new_shared", port=>3252);
#$listen->add(\$new_shared);


$SIG{INT} = $SIG{TERM} = \&sig_handler;
$SIG{PIPE} = 'IGNORE';
#$SIG{CHLD} = \&REAPER;
$SIG{CHLD} = 'IGNORE';

print "canview.pl, version: $version\n";
daemonize() if (!$debug);

my $dbh = DBI->connect("DBI:mysql:database=canview;host=localhost;mysql_connect_timeout=10", "canview", "CANVIEW", {'RaiseError'=>0});

my $server = IO::Socket::INET->new( Proto     => 'tcp',
                                 LocalPort => $PORT,
                                 Listen    => SOMAXCONN,
                                 Reuse     => 1)
              or die "Couldn't be a tcp server on port $PORT : $@\n";

my $systemevents = new Mac::AppleScript::Glue::Application('System Events');
my $frontrow     = new Mac::AppleScript::Glue::Application('Front Row');

touchscreenScroll(); # Fork off touchscreen scroll
getCANData();        # Fork off CAN-view data processing


# Listen on Socket for PSDAnim Flash
my $client = undef;
while ($client = $server->accept()) {
   my $pid = undef;
   next if $pid = fork;
   die "fork: $!" unless defined $pid;
   close $server;
   print "= Socket Child spawned ($$)\n" if ($debug > 1);
   $client->autoflush(1);
   print "- Incoming socket!\n" if ($debug);
   while (my $req = <$client>) {
      print "REQUEST: $req\n" if ($debug > 1);
      &process_request($client);
   }
   print "= Child terminated.\n" if ($debug > 1);
   close $client;
   exit;
}
close $client;

# END MAIN


# PSDAnim Flash Socket Functions
sub process_request {
        my $client      = shift;
	&transmit($client);
}

sub transmit {
        my $client = shift;

        # Decode, and transmit all at once. We do this so we
        # can tell when to send the "done transmitting" message.

        #$buffer = $listen->retrieve($new_shared);
        print $client "$buffer\0";
        print "= Socket closed.\n" if ($debug);

}

# CAN-view data functions
sub getCANData {
   srand;
   my $pid = undef;
   return if ($pid = fork);
   die "fork: $!" unless defined $pid;
   print "= CAN-view Child spawned ($$)\n" if ($debug > 1);
 
   $SIG{INT} = $SIG{TERM} = \&sig_handler;
   $SIG{PIPE} = 'IGNORE';
   $SIG{CHLD} = \&REAPER;

   if (!$fake) {
      print "Setting up $port\n" if ($debug);
      $ob = Device::SerialPort->new ($port,1) || die "Can't open $port: $!";
      $ob->baudrate($baud)                    || die "fail setting baudrate";
      $ob->parity("even")                     || die "fail setting parity";
      $ob->stty_inpck;
      $ob->stty_istrip;
      $ob->databits(8)                        || die "fail setting databits";
      $ob->stopbits(1)                        || die "fail setting stopbits";
      $ob->handshake("none")                  || die "fail setting handshake";
      # timeouts
      #$ob->read_char_time(5);     # time between chars
      $ob->read_const_time(90); # timeout after n
      $ob->write_settings || die "no settings";
      print "Finished setting up $port\n" if ($debug);
   }

   my %CAN = (); 

   open(XML,">canview.xml") if ($fake);
   while (1) {
      %CAN = readCANview(\%CAN);
      $handle->shlock();
      
      $buffer = "<MESSAGE";
      foreach (sort keys %CAN) {
         $buffer .= " $_=\"$CAN{$_}\"";
      }
      $buffer .= " />";

      #print "$buffer\n";
      print XML "$buffer\n" if ($fake);

      #$listen->store($new_shared, $buffer);
      $handle->shunlock();
   }
   close(XML) if ($fake);
   exit;
}

sub readCANview {
   my ($temphash) = @_;
   my %CANOLD = %$temphash;
   my ($s, $usec) = gettimeofday();
   my $cursec = ($usec / 1000000) + $s;
   $lastsec = $cursec if (!$lastsec);
   my $elapsed = ($cursec - $lastsec);
   $elapsedTotal += $elapsed;
   $lastMileElapsedTotal += $elapsed;
   $lastsec = $cursec;

   my ($grabbed, $datagrabbed) = ();
   if (!$fake) {
      ($grabbed, $datagrabbed) = $ob->read($inputData);
   } else {
      if ($genfake) {
         $grabbed = 30;
         for (my $i = 1; $i<$grabbed; $i++) {
            my $byte = int(rand(255));
            $datagrabbed .= pack("C",$byte);
         }
         sleep(0.1666);
      } else {
         @rv = getRecordedData() if (scalar(@rv) == 0);
         my $hash = shift(@rv);
         $grabbed = $$hash{grabbed};
         $datagrabbed = $$hash{datagrabbed};
         my $sec = $$hash{epoch_sec};
         my $usec = $$hash{epoch_usec};
         my $dbsec = ($usec / 1000000) + $sec;
         $dblastsec = $dbsec if (!$dblastsec);
         my $dbelapsed = ($dbsec - $dblastsec);
         #print "$dbsec - $dblastsec = $dbelapsed\n";
         $dbelapsed = 1.666 if ($dbelapsed > 5);
         sleep($dbelapsed) if ($dbelapsed > 0);
         $dblastsec = $dbsec;
      }
   }

   insertDatabase($s,$usec,$grabbed,$datagrabbed) if ((!$fake) || ($genfake));

   my $coord = -1;

   my %POS = ();
   $POS{VOLTS} = 0;
   $POS{CURRENT} = 1;
   $POS{BATKW} = 2;
   $POS{JOINTKW} = 3;
   $POS{SOC} = 4;
   $POS{ICERPM} = 8;
   $POS{FUELFLOW} = 10;
   $POS{MG2RPM} = 11;
   $POS{ICETEMP} = 13;
   $POS{THROTTLE} = 14;
   $POS{MG1RPM} = 16;
   $POS{BRAKE} = 17;
   $POS{MPG} = 19;
   $POS{SPECIAL} = 27;

   if ($grabbed) {
      my @data = unpack('C*', $datagrabbed);
      #print "$grabbed: " . join(",", @data) . "\n";
      if ($grabbed >= 30) {
         my $offset = 0;
         if ($grabbed == 31) {
            $offset = 1;
         } elsif ($grabbed >= 32) {
            if (sprintf("%02X", $data[30]) eq "EE") {
               $coord = $data[31];
            }
            if (sprintf("%02X", $data[31]) eq "EE") {
               $coord = $data[32];
               $offset = 1;
            }
         }
         foreach (keys %POS) {
            my $tempoffset = 0;
            $tempoffset = -1 if (($offset) && (($_ eq "VOLTS") || ($_ eq "CURRENT")));
            $CAN{$_} = $data[$POS{$_} + $offset + $tempoffset];
         }
      } elsif (($grabbed == 1) || ($grabbed == 2)) {
         $coord = $data[0];
      }
    
      if ($coord >= 0) { 
         printf("TOUCHSCREEN: %02X\n", $coord) if ($debug > 2);
         touchscreen($coord);
      }

      my $gear = "DRIVE";
      my $polarity = 1;
      my $special = $CAN{SPECIAL};
      if ($CAN{SPECIAL} >= 128) {
         $special = $special - 128;
         $polarity = 1;
      } else {
         $polarity = -1;
      }
      $gear = "PARK" if ($special == 0);
      $gear = "REVERSE" if ($special == 1);
      $gear = "NEUTRAL" if ($special == 2);

      $CAN{GEAR} = $gear;

      $CAN{ICETEMP}  = sprintf("%.2f", $CAN{ICETEMP} * 1.8 + 32);
      $CAN{VOLTS}    = $CAN{VOLTS} + 100;
      $CAN{MG1RPM}   = $CAN{MG1RPM} * 100;
      $CAN{MG2RPM}   = $CAN{MG2RPM} * 25;
      $CAN{MG2RPM}   = $CAN{MG2RPM} * -1 if ($gear eq "REVERSE");
      $CAN{ICERPM}   = $CAN{ICERPM} * 25;
      $CAN{FUELFLOW} = $CAN{FUELFLOW} / 10;
      $CAN{BATKW}    = $CAN{BATKW} / 10;
      $CAN{MG1RPM}   = $CAN{MG1RPM} - 25000 if ($CAN{MG1RPM} > 12700);
      my $mph        = $CAN{MG2RPM} / 55.177;
      #my $mph        = $CAN{MG2RPM} / 52.986;
      $CAN{MPH}      = sprintf("%0.1f", $mph);
      $CAN{CURRENT}  = $CAN{CURRENT} * $polarity;
      my $batkw = $CAN{BATKW} + .5;
      $CAN{BATKW}  = $CAN{BATKW} * $polarity * -1;
      $batkw  = $batkw * $polarity * -1;

      my $milesElapsed = ($mph / 3600) * $elapsed;
      $milesTotal += $milesElapsed;
      $lastMileTotal += $milesElapsed;

      $lastSOC = -1 if (!$lastSOC);
      $lastMph = $CAN{MPH} if (($lastMph == -1) && ($CAN{MPH}));
      $lastSOC = $CAN{SOC} if (($lastSOC == -1) && ($CAN{SOC}) > 0);

      if ($lastMileTotal > 1) {
         printf("%.1f: %.3fMPH %.3fMPG\n",$lastMileTotal,$lastMileMphAvg,$lastMileMpgAvg);
         $lastMileTotal = $milesElapsed;
         $lastMileElapsedTotal = $elapsed;
         $lastMileGallonsTotal = 0;
         # Do Database Insert Here (Future)
      }

      my $gallonsElapsed = 0;
      $gallonsElapsed = $milesElapsed / $CAN{MPG} if (($CAN{MPG} > 0) && ($CAN{MPG} < 255));
      $gallonsTotal += $gallonsElapsed;
      $lastMileGallonsTotal += $gallonsElapsed;

      $mpgAvg = $milesTotal / $gallonsTotal if ($gallonsTotal > 0);
      $mphAvg = $milesTotal / ($elapsedTotal / 3600) if ($elapsedTotal > 0);
      $lastMileMpgAvg = $lastMileTotal / $lastMileGallonsTotal if ($lastMileGallonsTotal > 0);
      $lastMileMphAvg = $lastMileTotal / ($lastMileElapsedTotal / 3600) if ($lastMileElapsedTotal > 0);

      $CAN{MILES}    = sprintf("%0.3f", $milesTotal);
      $CAN{TIME}     = sprintf("%0.3f", $elapsedTotal);
      $CAN{MPHAVG}   = sprintf("%0.3f", $mphAvg); 
      $CAN{GALLONS}  = sprintf("%0.3f", $gallonsTotal); 
      $CAN{MPGAVG}   = sprintf("%0.3f", $mpgAvg); 

      if (($CAN{THROTTLE} < 5) && ($CAN{BATKW} < 0)) {
         $CAN{JOINTKW} = $CAN{JOINTKW} * -1;
      }

      $CAN{ICEKW}    = $CAN{JOINTKW} - int($batkw);
      $CAN{ICEKW} = 0 if ($CAN{ICEKW} < 0); 
      $CAN{ICEKW} = 0 if ($CAN{THROTTLE} == 0); 

      if ($CAN{ICEKW} == 0){
         if ($CAN{MG2RPM} == 0) {
            $CAN{MODE} = "STOP"
         } elsif ($CAN{BATKW} > 0) {
            $CAN{MODE} = "EV"
         }
      }
      if (($CAN{ICEKW} < 1) && ($CAN{ICERPM} > 0) && ($CAN{MPH} == 0)) {
         $CAN{MODE} = "ICE_WARM";
      }
      if ($CAN{BATKW} < 0) {
         if (($CAN{BRAKE} > 0) && ($CAN{MPH} > 7)) {
            $CAN{MODE} = "REGEN_BRAKE"
         } elsif (($CAN{ICEKW} == 0) && ($CAN{MG2RPM} > 0)) {
            $CAN{MODE} = "REGEN_COAST"
         } elsif (($CAN{ICERPM} > 0) && ($CAN{MG2RPM} == 0)) {
            $CAN{MODE} = "STOP_CHARGE"
         } elsif (($CAN{BATKW} * -1) < $CAN{ICEKW}) {
            $CAN{MODE} = "ICE_MOT_CHARGE"
         } else {
            $CAN{MODE} = "ICE_CHARGE"
         }
      } elsif ($CAN{ICEKW} > 0){
         if ($CAN{BATKW} == 0) {
            $CAN{MODE} = "ICE_MOT"
         } elsif ($CAN{BATKW} > 0) {
            $CAN{MODE} = "FULL"
         }
      } elsif (($CAN{ICEKW} == 0) && ($CAN{BATKW} > -1) && ($CAN{MG2RPM} > 0) && ($CAN{BATKW} < 1.3)) {
         $CAN{MODE} = "GLIDE"
      }

      $CAN{STAGE} = $lastStage;
      if ($CAN{ICETEMP} < 104) {
         $CAN{STAGE} = "S1";
      } elsif ($CAN{ICETEMP} < 163.4) {
         $CAN{STAGE} = "S2";
      } elsif (($CAN{STAGE} eq "S2") || ($CAN{STAGE} eq "S1") || (!$CAN{STAGE})) { 
         $CAN{STAGE} = "S3a";
      } elsif (($CAN{STAGE} =~ "S3") && ($CAN{MPH} >= 34) && ($CAN{STAGE} ne "S4")) {
         $CAN{STAGE} = "S3b";
      } elsif (($CAN{STAGE} =~ "S3") && ($CAN{MPH} < 34) && ($CAN{MPH} > 0) && ($CAN{STAGE} ne "S4")) {
         $CAN{STAGE} = "S3a";
      } elsif (($CAN{STAGE} =~ "S3") && ($CAN{MPH} == 0) && ($CAN{ICERPM} == 0)) {
         $CAN{STAGE} = "S4";
      } else {
         $CAN{STAGE} = "S4";
      }
      $lastStage = $CAN{STAGE};
   }

   if (($CAN{JOINTKW} > 80) || ($CAN{SOC} > 85) || ($CAN{THROTTLE} > 100) || ($CAN{BRAKE} > 100) || ($CAN{MPH} > 120) || (($CAN{MPH} - $lastMph > 10) || ($CAN{MPH} - $lastMph < -10)) || (($CAN{SOC} - $lastSOC > 5) || ($CAN{SOC} - $lastSOC < -5))) {
      #print "Skipped Packet\n";
      return(%CANOLD);
   } else {
      $lastMph = $CAN{MPH};
      $lastSOC = $CAN{SOC};
      #print "Good Packet\n";

      if ($debug) {
         print "MPH/AVG: $CAN{MPH}/$CAN{MPHAVG}\tMPG/AVG: $CAN{MPG}/$CAN{MPGAVG}\tGallons: $CAN{GALLONS}\n";
         print "Time: $CAN{TIME}\tMiles: $CAN{MILES}\n\n";
         print "RPMs: ICE: $CAN{ICERPM}\tMG1: $CAN{MG1RPM}\tMG2: $CAN{MG2RPM}\n\n";
         print "Current/Amps: $CAN{CURRENT}\n";
         print "Volts: $CAN{VOLTS}\n";
         print "SoC: $CAN{SOC}\n\n";
         print "BatKW: $CAN{BATKW}\n";
         print "ICEKW: $CAN{ICEKW}\n";
         print "JointKW: $CAN{JOINTKW}\n\n";
         print "Mode: $CAN{MODE}\n\n";
         print "Stage: $CAN{STAGE}\tICE-Temp: $CAN{ICETEMP}\n\n";
         print "Gear: $CAN{GEAR}\tThrottle: $CAN{THROTTLE}\tBrake: $CAN{BRAKE}\n\n";
      }
      return(%CAN);
   }
}

# Touch screen Functions
sub touchscreenScroll {
   my $pid = undef;
   return if ($pid = fork);
   die "fork: $!" unless defined $pid;
   print "= Touchscreen Scroll Child spawned ($$)\n" if ($debug > 1);
   $SIG{INT} = $SIG{TERM} = \&sig_handler;
   $SIG{PIPE} = 'IGNORE';
   $SIG{CHLD} = \&REAPER;

   my $systemevents = new Mac::AppleScript::Glue::Application('System Events');

   while(1) {
      if ($touchscreenScrollDirection eq "UP") {
         #$systemevents->key_code(126);
         RunAppleScript(qq(tell application "System Events"\nkey code(126)\nkey code(126)\nend tell))
      }
      if ($touchscreenScrollDirection eq "DOWN") {
         #$systemevents->key_code(125);
         RunAppleScript(qq(tell application "System Events"\nkey code(125)\nkey code(125)\nend tell))
      }
   }
   exit;
}

sub touchscreen {
   my @data = @_;
   my $BYTE = 0;

   $touchscreenHandle->shlock();

   foreach (@data) {
      next if ($_[0] == 0);
      $BYTE = sprintf("%02X ", $data[0]);
      my ($x, $y) = split(//,$BYTE);
      $x = hex($x);
      $y = hex($y);

      print "$BYTE: $x,$y: " if ($debug);

      if (($y <= 5) && ($x > 10)) {
         if (!$touchscreenScrollDirection) {
            print "FrontRow Down Scroll Start\n" if ($debug);
            $touchscreenScrollDirection = "DOWN";
         } else {
            $touchscreenScrollDirection = "";
         }
      } elsif (($y > 10) && ($x > 10)) {
         if (!$touchscreenScrollDirection) {
            print "FrontRow Up Scroll Start\n" if ($debug);
            $touchscreenScrollDirection = "UP";
         } else {
            $touchscreenScrollDirection = "";
         }
      } else {
         fr_right() if (($x > 10) && ($y>5) && ($y<=10));
         fr_left() if (($x <= 5) && ($y>5) && ($y<=10));
         fr_down() if (($y <= 5) && ($x>5) && ($x<=10));
         fr_up() if (($y > 10) && ($x>5) && ($x<=10));
         fr_enter() if (($y <= 10) && ($y > 5) && ($x > 5) && ($x <=10)); 
         fr_esc() if (($y > 10) && ($x < 5));
         fr_start() if (($y <= 5) && ($x < 5));
         $touchscreenScrollDirection = "";
      }
   }

   $touchscreenHandle->shunlock();
}

sub fr_right {
   print "FrontRow Right\n" if ($debug);
   $systemevents->key_code(124);
}

sub fr_left {
   print "FrontRow Left\n" if ($debug);
   $systemevents->key_code(123);
}

sub fr_up {
   print "FrontRow Up\n" if ($debug);
   $systemevents->key_code(126);
}

sub fr_down {
   print "FrontRow Down\n" if ($debug);
   $systemevents->key_code(125);
}

sub fr_enter {
   print "FrontRow Enter\n" if ($debug);
   $systemevents->key_code(36);
}

sub fr_esc {
   print "FrontRow Escape\n" if ($debug);
   $frontrow->activate;
   $systemevents->key_code(53);
}

sub fr_start {
   print "FrontRow Start\n" if ($debug);
   $frontrow->activate;
   $systemevents->key_code(53);
   usleep(5);
   $systemevents->key_code(53);
}

# Database Functions
sub insertDatabase {
   my ($sec, $usec, $grabbed, $datagrabbed) = @_;
   %db = () if (scalar(keys %db) > 29);
   $db{"$sec.$usec"}{grabbed} = $grabbed;
   $db{"$sec.$usec"}{datagrabbed} = $datagrabbed;
   if (scalar(keys %db) > 29) {
      my $pid = undef;
      FORK: {
         if ($pid = fork) {
            # Parent here
            return;
         } elsif (defined $pid) {
            # Child here
            #$amChild = 1;   # I AM A CHILD!!!
         } elsif ($! =~ /No more process/) {
            sleep 3;
            redo FORK;
         } else {
            return;
         }
      }

      # Only child processes get here. Parent returns.

      my $dbhc = DBI->connect("DBI:mysql:database=canview;host=localhost;mysql_connect_timeout=10", "canview", "CANVIEW", {'RaiseError'=>0});
      my $sql = "INSERT INTO CANview VALUES (?,?,?,?)";
      my $sth = $dbhc->prepare($sql) or die $dbhc->errstr;
      foreach (sort keys %db) {
         my ($sec, $usec, $grabbed, $datagrabbed) = ();
         if ($_ =~ /(^\d+)\.(\d+)/) {
            $sec = $1; $usec = $2;
            $grabbed = $db{$_}{grabbed};
            $datagrabbed = $db{$_}{datagrabbed};
            #print "DB: $sec $usec $grabbed\n";
            $sth->execute(($sec,$usec,$grabbed,$datagrabbed)) or die $dbhc->errstr;
         }
      }
      $sth->finish();
      $dbhc->disconnect or die $dbhc->errstr;
      exit;
    }
}

sub getRecordedData {
   my $where = "";
   my $sql = "SELECT * from CANview $where order by epoch_sec,epoch_usec";
   my $sth = $dbh->prepare($sql) or die $dbh->errstr;
   $sth->execute() or die $dbh->errstr;
   while (my $row = $sth->fetchrow_hashref()) {
      push(@rv,$row);
   }
   $sth->finish();
   return(@rv);
}

# Process handler Functions
sub sig_handler {
  print "Kill signal received... dying $$\n";
  $handle->clean_up_all();
  $touchscreenHandle->clean_up_all();
  $dbh->disconnect or die $dbh->errstr;
  exit;
}

sub REAPER {
        my $pid;
        while (($pid = waitpid(-1, &WNOHANG)) > 0) {
                #print "Child on PID $pid is dead.\n";
        }
        $SIG{CHLD} = \&REAPER;
}

sub daemonize {
  my $pid = fork;
  exit if $pid;
  die "Couldn't fork: $!" unless defined($pid);

  POSIX::setsid() or die "Can't start new session: $!";

  $SIG{INT} = $SIG{TERM} = \&sig_handler;
  $SIG{PIPE} = 'IGNORE';
  $SIG{CHLD} = 'IGNORE';
}
