#!/usr/bin/perl
#############################################################################
#
# ZLog dataset reader for the HexpertSystems.com Zlog recording altimeter.
# (C)2009 Craig Lamparter, craig@sierraglider.com
# General Public License v3 http://www.gnu.org/copyleft/gpl.html
#
# This program will attempt to connect to a zlog device plugged into
# the USB port of your (linux) host.  Once connected, the user is
# asked to select form a list of recordings to be printed and graphed.
# A list of altitudes will be printed to "zlog.txt", and the graphic output
# will be printed to "zlog.png"
#
##############################################################################

#Perl modules you probably don't have:
use Device::SerialPort qw( :PARAM :STAT 0.07 );
use GD::Graph::lines;
use GD::Image;
#if you don't have them, get them with cpan:
#perl -MCPAN -e shell
#> install Device::SerialPort
#> install GD


#change this if you have more than one usb-serial device plugged in
my $DEVICE="/dev/ttyUSB0";   #ttyUSB0,ttyUSB1,ttyUSB2...    
my $port=Device::SerialPort->new("$DEVICE") or die ("Unable to open $DEVICE");
open(LOG, ">zlog.txt") or die("Could not open file zlog.txt");
my @log;             # array of altitude data points
my $STALL_DEFAULT=2; # how many seconds to wait for new input
my $dataset = 0;     # which set of data to dload
my @data;            # array that holds altitude data

$port->baudrate(115200);      # default zlog speed, can be changed
$port->parity("none");
$port->databits(8);
$port->stopbits(1);    
$port->handshake("none");     # no handshake
$port->read_char_time(0);     # don't wait for each character
$port->read_const_time(1000); # 1 second per unfulfilled "read" call

my $chars=0;   # number of bytes in the binary string from device
my $buffer=""; # holds the binary data sent from device
my $i = 0;     # iterator
my @data;      # hold the final altitude data in an array

############################################################################
#getdataset()
#
#Request a list of altitudes with frame header bytes prefixed
# send "aN" to the device where N is the data set we want
sub getdataset { 
  my ($datasetbyte) = @_;
  my $buffer;
  my $timeout=$STALL_DEFAULT;
  $datasetbyte = pack("c",$datasetbyte);  # must be 8bit binary
  $port->write("a$datasetbyte");          # send stimulus  "a<dataset>"
  while ($timeout>0) {
         my ($count,$saw)=$port->read(255); # will read _up to_ 255 chars
         if ($count > 0) {
                 $chars+=$count;
                 $buffer.=$saw;
         }
         else {
                 $timeout--;
         }
  }
return($buffer);
}
#############################################################################
#getcommand()
#
#Send a single byte command to device
sub getcommand { 
  my ($command) = @_;
  my $buffer;
  my $timeout=$STALL_DEFAULT;
  $port->write("$command"); # send stimulus byte
  while ($timeout>0) {
         my ($count,$saw)=$port->read(255); # will read _up to_ 255 chars
         if ($count > 0) {
                 $chars+=$count;
                 $buffer.=$saw;
         }
         else {
                 $timeout--;
         }
  }
return($buffer);
}

#############################################################################
#main()  
#
print ("Zlog altimeter data logger connecting to Zlog on $DEVICE...\n");
$version = &getcommand("v");
$version =~ s/(\n|\r)/ /g;
if (length($version) > 5) {
  print ("$version  Connected.");
}
else { die ("unable to connect to Zlog\n"); }
print "\n";


#user menu
print "\n";
print "[D]ownload data    [C]lear data         [Q]uit\n";
print "[R]eboot device    [F]actory defaults\n";
print "Command [D,C,R,F,Q]: ";

$input = <STDIN>;

if ($input =~ /[Cc]/) {
   getcommand("x");
   print "Altitude data erased from Zlog.\n";
   exit(0);
}

if ($input =~ /[qQ]/) {
   print "Exiting.\n";
   exit(0);
}

if ($input =~ /[Rr]/) {
   getcommand("R");
   print "Zlog rebooting.\n";
   exit(0);
}

if ($input =~ /[Ff]/) {
   getcommand("*");
   print "Loading factory defaults.\n";
   exit(0);
}

#default action is to download datasets

print "\nGetting list of datasets from device...\n";
$sets = &getcommand("s");     #list of 0,0 pairs separated by \r\n
@list = split(/\r\n/,$sets);  #split each line into an array element
for (@list) {
   s/,.*$//;                  #strip off the ",0" leaving only first numbers
}

if (scalar @list) {
  #ask user which dataset they'd like to download
  $" = ","; # uses commas instead of spaces when printing arrays
  print ("Data set to download [@list]: ");
  $dataset = <STDIN>;
  chomp $dataset;
} 
else {
  print "No datasets found - Please record something then try again.\n";
  exit(0);
}

if (!$dataset) {   #assume user wants dataset 0 if they just hit enter
  $dataset = 0;
}
print ("\nAcquiring data for dataset $dataset...\n");
$datasetbyte = $dataset;
$buffer = &getdataset("$datasetbyte"); # ask for dataset  

print sprintf("Received %d bytes from Zlog (", length($buffer)); 

#look for 0x80 header signature
my $index = 0;
my $signature = 0;
@bytes = unpack("C*", $buffer);
foreach $byte (@bytes) {
  if ($byte == 0x80) {
    $signature++;
    $index++;
    last;
  }
  $index++;
} 

#read data after signature if we found the signature
if ( $signature ) {
  #rate: 2 bytes
  $rate = (($bytes[$index] * 256) + $bytes[$index + 1]);
  $index = ($index + 2);

  #samples: 2 bytes
  $samples = (($bytes[$index] * 256) + $bytes[$index + 1]);
  $index = ($index + 2);
  
  #trigger: 1 byte
  $trigger = $bytes[$index];
  $index = ($index + 1);
  
  print "rate=", ($rate / 10), "sec";

  #read 16 bit altitude values until end of data
  if ($samples == 0) {                     #user powerfailed during recording
    $samples = ((length($buffer) - 6) / 2);#guess number of samples by
  }                                        #removing header, bytes to words
  print ", samples=", $samples, ", " ;
  $samplecounter = $samples;
  my $i = 0;
  while ($samplecounter) {
    $data[$i]  = (($bytes[$index] * 256) + $bytes[$index + 1]);
    push @log, $data[$i++];        #append altitude to log array
    $index = ($index + 2);
    $samplecounter--;
  }
  for (@data) {
    print (LOG $_, "\n");
  }
  @log = sort { $b <=> $a } @log;  # reverse numeric sort
  print "peak=",@log[0],"')\n";
}
else {
  print ("No valid altitude data found)\n");
  exit(1);
}

$port->close;
close(FP);

print("Putting altitude data in zlog.txt...\n");
print ("Generating graph in zlog.png...\n");

#load x-axis with time intervals
my $samplecounter = 0;
my @times;
while ($samplecounter <= $samples) {
  $times[$samplecounter] = (++$samplecounter * $rate / 10);
}

#GD wants (xaxis, yaxis) arrays
my @graphdata = (\@times, \@data);

my $graph = GD::Graph::lines->new(800, 600);

$graph->set(
    x_label     => 'Seconds',
    y_label     => 'Altitude',
    y_tick_number => 'auto',    #'auto'?
    x_tick_number => 'auto',    #'auto'?
#    y_tick_number => '10',    #'auto'?
#    x_tick_number => '8',    #'auto'?
#    x_label_skip => 60,
    long_ticks => 1,
    title       => 'ZLog Altitude',
    # Draw datasets in 'solid', 'dashed' and 'dotted-dashed' lines
    line_types  => [1, 2, 4],
    line_width  => 3,
    # Set colors for datasets
    dclrs       => ['blue', 'green', 'cyan'],
    transparent => 0,
    t_margin => 20,
    b_margin => 20,
    l_margin => 20,
    r_margin => 20,
    box_axis => 1
) or warn $graph->error;

my $gd = $graph->plot(\@graphdata) or die $graph->error;

open(IMG, '>zlog.png') or die $!;
binmode IMG;
print IMG $gd->png;
close(IMG);

