#! /usr/bin/perl
#
# Proof of concept for Mac <-> Agilent U1272A communications
#
# Copyright (c) 2011 M J Oldfield <m@mjo.tc>
#
# Version 0.1 2011-06-17 M J Oldfield
#

use strict;
use warnings;

package My::U1272;

use Time::HiRes qw(time);

sub new
  {
    my ($class, $trace, $b19200) = @_;

    my $port = $class->get_portname;

    my $dev = Device::SerialPort->new($port)
      or die "Unable to open $port, ";

    print "Opened $port :)\n";

    $dev->databits(8);
    $dev->baudrate($b19200 ? 19200 : 9600);
    $dev->parity("none");
    $dev->stopbits(1);
    
    $dev->read_char_time(5);

    $dev->write_settings;
    
    return bless { dev => $dev, trace => $trace, t0 => time() }, $class;
  }

sub get_portname
  {
    my ($port, @others) = glob "/dev/tty.PL*";

    die "No device found,  " unless $port;
    die "Multiple devices, " if @others;

    return $port;
  }

sub trace
  {
    my $s = shift;
    return unless $s->{trace};

    my $t = sprintf "%0.3f ", time() - $s->{t0};
    print $t, @_, "\n";
  }

# Execute a simple command and read back the result, assuming that there's one line
# terminated by "\n". Chop the line terminators off the response
#
# Assuming that $ignore_err isn't true, if we get an error ('*E') back ask why
sub cmd
  {
    my ($s, $cmd, $ignore_err) = @_;
    
    my $dev = $s->{dev};

    $dev->write($cmd . "\n");
    $s->trace(">: $cmd");

    my $response = '';
    while(1)
      {
	my ($count, $txt) = $dev->read(1);
	next unless $count;
	
	$response .= $txt;
	last if $txt eq "\n";
      }

    $response =~ s/[\n\r]+$//;

    $s->trace("<: $response");

    if ($response eq "*E" && !$ignore_err)
      {
	my $err = $s->cmd('SYST:ERR?');
	print "Error detected: $err\n";
      }

    return $response;
  }

# A bunch of SCPI commands which the meter understands
sub identity { shift->cmd('*IDN?')       }
sub battery  { shift->cmd('SYST:BATT?'); }
sub config   { shift->cmd('CONF?');      }
sub reading  { shift->cmd('FETC?');      }
sub reading2 { shift->cmd('FETC? @2');   }

# Read the log
sub get_log
  {
    my ($s, $log) = @_;

    my $cmd = "LOG:$log %d";

    my @log;

    print "Grabbing $log log:";
    my $i = 1;
    while(1)
      {
	if    ($i % 1000 == 1) { printf "\n  %4d: ", $i - 1; }
	elsif ($i %  100 == 1) { printf ".";              }

	my $res = $s->cmd(sprintf($cmd,$i), 1);
	last if $res eq '*E';

	# This is, quite frankly, a guess at the decoding!
	my ($pre, $data, $post) = ($res =~ /^"(\d{2})(\d{5})(\d{6})"$/)
	  or die "Can't parse reading $i : $res\n";

	push(@log, [ $i, $data, $pre, $post ]);

	$i++;
      }

    print "\nFinished\n";

    return \@log;
  }
    

package main;

use Getopt::Long;
use YAML qw(Dump DumpFile);

use Device::SerialPort;
use Pod::Usage;

$| = 1; # unbuffer stdout

my %Opt;
GetOptions(\%Opt, "help!", "info!", "trace!", "19200!", "get_log=s", "cmd=s");

pod2usage(-verbose => 2)
    if $Opt{help} || $Opt{info};

my $dev = My::U1272->new($Opt{trace}, $Opt{19200});

if (my $cmd = $Opt{cmd})
  {
    my $res = $dev->cmd($cmd);
    print "$res\n";
  }
elsif (my $log = $Opt{get_log})
  {
    my $file = lc($log) . ".txt";
    
    die "Dumpfile $file already exists and I refuse to overwrite it.\n"
      if -f $file;

    my $data = $dev->get_log($log);

    open(my $fh, '>', $file) or die "Couldn't open $file: $!\n";
    print {$fh} map { sprintf("%5d %6d %6d %6d\n", @$_) } @$data;

    my $n = @$data;
    print "$n data from $log log written to $file\n";
  }
else
  {
    my %data;
    foreach my $k (qw(identity battery config reading reading2))
      {
	no strict 'refs';
	$data{$k} = $dev->$k;
      }
    
    print Dump(\%data);
  }

__END__

=head1 NAME
 
u1272a - A toy application which talks to a U1272A
 
=head1 USAGE

    $ u1272a

    $ u1272a --trace

    $ u1272a --trace --cmd='*IDN?'

    $ u1272a --get_log=AUTO

 =head1 DESCRIPTION

A toy application which talks to an Agilent U1272A DMM over Agilent's
IR interface cable.

This is really just a proof of concept, so you shouldn't rely on its
reliability or functionality.

=head1 OPTIONS

By default the program just queries a bunch of data and prints it. You can change:

=over

=item --help, --info

Display this page.

=item --trace

Print the low-level communications with the meter.

=item --19200

Talk to the meter at 19200 baud (the default is 9600).

=item --cmd=foo

Execute the foo command and print the (one-line) result.

=item --log=AUTO|HAND

Retrieve logged data from the meter. This barely works: it's slow
(roughly one minute per thousand points), and I don't really
understand the format.

=back
 
=head1 CONFIGURATION AND ENVIRONMENT

The software assumes that there's precisely one device of the form
/dev/tty.PL* and that said device is the meter you're trying to prod.

We also assume that the meter is set to 9600 baud, 8-bit, no-parity,
and 1 stop-bit (the meter's default settings). If you want to try the
giddy speeds of 19200 baud use the --19200 option.

The meter settings can be changed in the meter's setup mode: see
Agilent's User Manual.


=head1 DEPENDENCIES

All the serial stuff is done via Device::SerialPort.

It seems moderately likely that I'm not using that module optimally.

=head1 BUGS AND LIMITATIONS

This program is a quick hack: it is not production quality code. In
most cases no parsing of the data from the meter is done; logged data
are parsed, but the algorithm is just a guess.

The serial port interface should be about three times faster.

Please report problems to the author.

Patches are welcome.
 
=head1 AUTHOR

M J Oldfield, m@mjo.tc
 
=head1 LICENCE AND COPYRIGHT
 
Copyright (c) 2011, M J Oldfield
 
This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.
 
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. 



