package Lab::Bus::USBtmc;
our $VERSION = '3.510';

require "sys/ioctl.ph";

# Created using h2ph
eval 'sub USBTMC_IOC_NR () {91;}' unless defined(&USBTMC_IOC_NR);
eval 'sub USBTMC_IOCTL_INDICATOR_PULSE () { &_IO( &USBTMC_IOC_NR, 1);}' unless defined(&USBTMC_IOCTL_INDICATOR_PULSE);
eval 'sub USBTMC_IOCTL_CLEAR () { &_IO( &USBTMC_IOC_NR, 2);}' unless defined(&USBTMC_IOCTL_CLEAR);
eval 'sub USBTMC_IOCTL_ABORT_BULK_OUT () { &_IO( &USBTMC_IOC_NR, 3);}' unless defined(&USBTMC_IOCTL_ABORT_BULK_OUT);
eval 'sub USBTMC_IOCTL_ABORT_BULK_IN () { &_IO( &USBTMC_IOC_NR, 4);}' unless defined(&USBTMC_IOCTL_ABORT_BULK_IN);
eval 'sub USBTMC_IOCTL_CLEAR_OUT_HALT () { &_IO( &USBTMC_IOC_NR, 6);}' unless defined(&USBTMC_IOCTL_CLEAR_OUT_HALT);
eval 'sub USBTMC_IOCTL_CLEAR_IN_HALT () { &_IO( &USBTMC_IOC_NR, 7);}' unless defined(&USBTMC_IOCTL_CLEAR_IN_HALT);


use strict;
use Scalar::Util qw(weaken);
use Time::HiRes qw (usleep sleep);
use Lab::Bus;
use Data::Dumper;

our @ISA = ("Lab::Bus");


our %fields = (
	type => 'USBtmc',
	brutal => 0,
    read_length=>1000, # bytes
    wait_query=>10e-6, # sec;
	);


sub new {
	my $proto = shift;
	my $class = ref($proto) || $proto;
	my $twin = undef;
	my $self = $class->SUPER::new(@_); # getting fields and _permitted from parent class
	$self->${\(__PACKAGE__.'::_construct')}(__PACKAGE__);

	# search for twin in %Lab::Bus::BusList. If there's none, place $self there and weaken it.
	if( $class eq __PACKAGE__ ) { # careful - do only if this is not a parent class constructor
		if($twin = $self->_search_twin()) {
			undef $self;
			return $twin;	# ...and that's it.
		}
		else {
			$Lab::Bus::BusList{$self->type()}->{'default'} = $self;
			weaken($Lab::Bus::BusList{$self->type()}->{'default'});
		}
	}
	
	return $self;
}



sub connection_new { # { tmc_address => primary address }
	my $self = shift;
	my $args = undef;
	if (ref $_[0] eq 'HASH') { $args=shift } # try to be flexible about options as hash/hashref
	else { $args={@_} }
	
	my $fn;
	my $usb_vendor;
	my $usb_product;
	my $usb_serial; #TODO: Unsupported

	if(defined $args->{'tmc_address'} && $args->{'tmc_address'} =~ /^[0-9]*$/ ) 
	{
	    $fn = "/dev/usbtmc".$args->{'tmc_address'};
	} else {
	    if (defined $args->{'visa_name'} && 
	       ($args->{'visa_name'} =~ /USB::0x([0-9A-Fa-f]{4})::0x([0-9A-Fa-f]{4})::[^:]*::INSTR/))
	    {
	        $usb_vendor = hex($1);
	        $usb_product = hex($2);
	        $usb_serial = $3;
	    } else 
	    {
	        $usb_vendor = hex($args->{'usb_vendor'});
            $usb_product = hex($args->{'usb_product'});
	    }
	}
	
	if (!defined $fn && (!defined $usb_vendor || !defined $usb_product)) 
    {
        Lab::Exception::CorruptParameter->throw (
            error => "No valid USB TMC address given to " . __PACKAGE__ . "::connection_new()\n",
        );
    }
	
    foreach my $file (glob("/dev/usbtmc*"))
    {
        if (defined $fn) {last;}
        $file =~ /\/dev\/(.*)/;
        open(SYS_FS_HANDLE, "<", "/sys/class/usb/$1/device/uevent");
        while (<SYS_FS_HANDLE>)
        {
            if (/PRODUCT=([0-9A-Fa-f]+)\/([0-9A-Fa-f]+)\// &&
                hex($1) == $usb_vendor && hex($2) == $usb_product)
            {
                $fn = $file;
                last;
            }
        }
        close(SYS_FS_HANDLE);
    }
    
    if (!defined $fn) 
    {
        Lab::Exception::CorruptParameter->throw (
            error => sprintf("Could not find specified device 0x%04x/0x%04x in " . __PACKAGE__ . "::connection_new()\n", $usb_vendor, $usb_product),
        );
    }


	my $connection_handle = undef;
	my $tmc_handle = undef;
	
	open($tmc_handle, "+<", $fn) || Lab::Exception::CorruptParameter->throw(error => $!.": '$fn'\n");
	binmode($tmc_handle);
	$tmc_handle->autoflush;
	
	$connection_handle =  { valid => 1, type => "USBtmc", tmc_handle => $tmc_handle };  
	return $connection_handle;
}

#TODO: Status, Errors?
sub connection_read { # @_ = ( $connection_handle, $args = { read_length, brutal }
	my $self = shift;
	my $connection_handle=shift;
	my $args = undef;
	if (ref $_[0] eq 'HASH') { $args=shift } # try to be flexible about options as hash/hashref
	else { $args={@_} }

	my $brutal = $args->{'brutal'} || $self->brutal();
	my $read_length = $args->{'read_length'} || $self->read_length();

	my $result = undef;
	my $fragment = undef;
	
	my $tmc_handle = $connection_handle->{'tmc_handle'};
	sysread($tmc_handle, $result, $read_length);

	# strip spaces and null byte
	$result =~ s/[\n\r\x00]*$//;

	#
	# timeout occured - throw exception, but include the received data
	# if the "Brutal" option is present, ignore the timeout and just return the data
	#
# 	if( $ib_bits->{'ERR'} && $ib_bits->{'TIMO'} && !$brutal ) {
# 		Lab::Exception::GPIBTimeout->throw(
# 			error => sprintf("ibrd failed with a timeout, ibstatus %x\n", $ibstatus),
# 			ibsta => $ibstatus,
# 			ibsta_hash => $ib_bits,
# 			data => $result
# 		);
# 	}
	# no timeout, regular return
	return $result;
}


#TODO: Undocumented
sub connection_query { # @_ = ( $connection_handle, $args = { command, read_length, wait_status, wait_query, brutal }
	my $self = shift;
	my $connection_handle=shift;
	my $args = undef;
	if (ref $_[0] eq 'HASH') { $args=shift } # try to be flexible about options as hash/hashref
	else { $args={@_} }

	my $wait_query = $args->{'wait_query'} || $self->wait_query();
	my $result = undef;

	$self->connection_write($args);

    sleep($wait_query); #<---ensures that asked data presented from the device

    $result = $self->connection_read($args);
    return $result;
}



#TODO: Error checking
sub connection_write { # @_ = ( $connection_handle, $args = { command, wait_status }
	my $self = shift;
	my $connection_handle = shift;
	my $args = undef;
	if (ref $_[0] eq 'HASH') { $args=shift } # try to be flexible about options as hash/hashref
	else { $args={@_} }

	my $command = $args->{'command'} || undef;
# 	my $brutal = $args->{'brutal'} || $self->brutal();
# 	my $read_length = $args->{'read_length'} || $self->read_length();

# 	my $result = undef;
# 	my $raw = "";
# 	my $ib_bits=undef;	# hash ref
# 	my $ibstatus = undef;
# 	my $ibsta_verbose = "";
# 	my $decimal = 0;


	if(!defined $command) {
		Lab::Exception::CorruptParameter->throw(
			error => "No command given to " . __PACKAGE__ . "::connection_write().\n",
		);
	}
	
    print { $connection_handle->{'tmc_handle'} } $command;
#     $ibstatus=ibwrt($connection_handle->{'gpib_handle'}, $command, length($command));


# 	$ib_bits=$self->ParseIbstatus($ibstatus);
# 	foreach my $key ( keys %IbBits ) {
# 		print "$key: $ib_bits{$key}\n";
# 	}

	# Todo: better Error checking
# 	if($ib_bits->{'ERR'}==1) {
# 		if($ib_bits->{'TIMO'} == 1) {
# 			Lab::Exception::GPIBTimeout->throw(
# 				error => sprintf("Timeout in " . __PACKAGE__ . "::connection_write() while executing $command: ibwrite failed with status %x\n", $ibstatus) . Dumper($ib_bits),
# 				ibsta => $ibstatus,
# 				ibsta_hash => $ib_bits,
# 			);
# 		}
# 		else {
# 			Lab::Exception::GPIBError->throw(
# 				error => sprintf("Error in " . __PACKAGE__ . "::connection_write() while executing $command: ibwrite failed with status %x\n", $ibstatus) . Dumper($ib_bits),
# 				ibsta => $ibstatus,
# 				ibsta_hash => $ib_bits,
# 			);
# 		}
# 	}

	return 1;
}



sub connection_settermchar { # @_ = ( $connection_handle, $termchar
# 	my $self = shift;
# 	my $connection_handle=shift;
# 	my $termchar =shift; # string termination character as string
# 
# 	my $ib_bits=undef;	# hash ref
# 	my $ibstatus = undef;
# 
#         my $h=$connection_handle->{'gpib_handle'};
# 
#         my $arg=ord($termchar);
# 
# 	$ibstatus=ibconfig($connection_handle->{'gpib_handle'}, 15, $arg);
# 
# 	$ib_bits=$self->ParseIbstatus($ibstatus);
# 
# 	if($ib_bits->{'ERR'}==1) {
# 		Lab::Exception::GPIBError->throw(
# 			error => sprintf("Error in " . __PACKAGE__ . "::connection_settermchar(): ibeos failed with status %x\n", $ibstatus) . Dumper($ib_bits),
# 			ibsta => $ibstatus,
# 			ibsta_hash => $ib_bits,
# 		);
# 	}

	return 1;
}

sub connection_enabletermchar { # @_ = ( $connection_handle, 0/1 off/on
# 	my $self = shift;
# 	my $connection_handle=shift;
# 	my $arg=shift;
# 
# 	my $ib_bits=undef;	# hash ref
# 	my $ibstatus = undef;
# 
#     my $h=$connection_handle->{'tmc_handle'};
# 
# 	$ibstatus=ibconfig($connection_handle->{'gpib_handle'}, 12, $arg);
# 
# 	$ib_bits=$self->ParseIbstatus($ibstatus);
# 
# 	if($ib_bits->{'ERR'}==1) {
# 		Lab::Exception::GPIBError->throw(
# 			error => sprintf("Error in " . __PACKAGE__ . "::connection_enabletermchar(): ibeos failed with status %x\n", $ibstatus) . Dumper($ib_bits),
# 			ibsta => $ibstatus,
# 			ibsta_hash => $ib_bits,
# 		);
# 	}

	return 1;
}

sub serial_poll {
	my $self = shift;
	my $connection_handle = shift;
	my $sbyte = undef;
# 	
# 	my $ibstatus = ibrsp($connection_handle->{'gpib_handle'}, $sbyte);
# 	
# 	my $ib_bits=$self->ParseIbstatus($ibstatus);
# 
# 	if($ib_bits->{'ERR'}==1) {
# 		Lab::Exception::GPIBError->throw(
# 			error => sprintf("ibrsp (serial poll) failed with status %x\n", $ibstatus) . Dumper($ib_bits),
# 			ibsta => $ibstatus,
# 			ibsta_hash => $ib_bits,
# 		);
# 	}
# 	
	return $sbyte;
}

sub connection_clear {
	my $self = shift;
	my $connection_handle=shift;

	close($connection_handle->{'tmc_handle'});
}

sub connection_device_clear
{
	my $self = shift;
	my $connection_handle=shift;
	
	my $unused = 0;

    ioctl($connection_handle->{'tmc_handle'}, USBTMC_IOCTL_ABORT_BULK_OUT(), $unused);
    ioctl($connection_handle->{'tmc_handle'}, USBTMC_IOCTL_ABORT_BULK_IN(), $unused);
    ioctl($connection_handle->{'tmc_handle'}, USBTMC_IOCTL_CLEAR_OUT_HALT(), $unused);
    ioctl($connection_handle->{'tmc_handle'}, USBTMC_IOCTL_CLEAR_IN_HALT(), $unused);
    ioctl($connection_handle->{'tmc_handle'}, USBTMC_IOCTL_CLEAR(), $unused);
}

sub timeout {
	my $self=shift;
	my $connection_handle=shift;
	my $timo=shift;
	my $timoval=undef;
	
	Lab::Exception::CorruptParameter->throw( error => "The timeout value has to be a positive decimal number of seconds, ranging 0-1000.\n" )
    	if($timo !~ /^([+]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ || $timo <0 || $timo>1000);
    
    if($timo == 0)			{ $timoval=0} # never time out
    if($timo <= 1e-5)		{ $timoval=1 }
    elsif($timo <= 3e-5)	{ $timoval=2 }
    elsif($timo <= 1e-4)	{ $timoval=3 }
    elsif($timo <= 3e-4)	{ $timoval=4 }
    elsif($timo <= 1e-3)	{ $timoval=5 }
    elsif($timo <= 3e-3)	{ $timoval=6 }
    elsif($timo <= 1e-2)	{ $timoval=7 }
    elsif($timo <= 3e-2)	{ $timoval=8 }
    elsif($timo <= 1e-1)	{ $timoval=9 }
    elsif($timo <= 3e-1)	{ $timoval=10 }
    elsif($timo <= 1)		{ $timoval=11 }
    elsif($timo <= 3)		{ $timoval=12 }
    elsif($timo <= 10)		{ $timoval=13 }
    elsif($timo <= 30)		{ $timoval=14 }
    elsif($timo <= 100)		{ $timoval=15 }
    elsif($timo <= 300)		{ $timoval=16 }
    elsif($timo <= 1000)	{ $timoval=17 }
    
# 	my $ibstatus=ibtmo($connection_handle->{'gpib_handle'}, $timoval);
# 	
# 	my $ib_bits=$self->ParseIbstatus($ibstatus);
# 
# 	if($ib_bits->{'ERR'}==1) {
# 		Lab::Exception::GPIBError->throw(
# 			error => sprintf("Error in " . __PACKAGE__ . "::timeout(): ibtmo failed with status %x\n", $ibstatus) . Dumper($ib_bits),
# 			ibsta => $ibstatus,
# 			ibsta_hash => $ib_bits,
# 		);
# 	}
#    print "timeout(): not implemented!\n";
}


sub ParseIbstatus { # Ibstatus http://linux-gpib.sourceforge.net/doc_html/r634.html
    print "ParseIbstatus not supported\n";
# 	my $self = shift;
# 	my $ibstatus = shift;	# 16 Bit int
# 	my @ibbits = ();
# 
# 	if( $ibstatus !~ /[0-9]*/ || $ibstatus < 0 || $ibstatus > 0xFFFF ) {	# should be a 16 bit integer
# 		Lab::Exception::CorruptParameter->throw( error => 'Lab::Bus::GPIB::VerboseIbstatus() got an invalid ibstatus.', InvalidParameter => $ibstatus );
# 	}
# 
# 	for (my $i=0; $i<16; $i++) {
# 		$ibbits[$i] = 0x0001 & ($ibstatus >> $i);
# 	}
# 
# 	my %Ib = ();
# 	( $Ib{'DCAS'}, $Ib{'DTAS'}, $Ib{'LACS'}, $Ib{'TACS'}, $Ib{'ATN'}, $Ib{'CIC'}, $Ib{'REM'}, $Ib{'LOK'}, $Ib{'CMPL'}, $Ib{'EVENT'}, $Ib{'SPOLL'}, $Ib{'RQS'}, $Ib{'SRQI'}, $Ib{'END'}, $Ib{'TIMO'}, $Ib{'ERR'} ) = @ibbits;
# 
# 	return \%Ib;

} # return: ($ERR, $TIMO, $END, $SRQI, $RQS, $SPOLL, $EVENT, $CMPL, $LOK, $REM, $CIC, $ATN, $TACS, $LACS, $DTAS, $DCAS)

sub VerboseIbstatus {
	my $self = shift;
	my $ibstatus = shift;
	my $ibstatus_verbose = "";

	if(ref(\$ibstatus) =~ /SCALAR/) {
		$ibstatus = $self->ParseIbstatus($ibstatus);
	}
	elsif(ref($ibstatus) !~ /HASH/) {
		Lab::Exception::CorruptParameter->throw( error => 'Lab::Bus::GPIB::VerboseIbstatus() got an invalid ibstatus.', InvalidParameter => $ibstatus );
	}

	while( my ($k, $v) = each %$ibstatus ) {
        $ibstatus_verbose .= "$k: $v\n";
    }

	return $ibstatus_verbose;
}


#
# search and return an instance of the same type in %Lab::Bus::BusList
#
sub _search_twin {
	my $self=shift;

	if(!$self->ignore_twins()) {
		for my $conn ( values %{$Lab::Bus::BusList{$self->type()}} ) {
			return $conn; # if $conn->gpib_board() == $self->gpib_board();
		}
	}
	return undef;
}


1;

=pod

=encoding utf-8

=head1 NAME

Lab::Bus::LinuxGPIB - LinuxGPIB bus

=head1 SYNOPSIS

This is the USB TMC (Test & Measurement Class) bus class.

  my $tmc = new Lab::Bus::USBtmc({ });

or implicit through instrument and connection creation:

  my $instrument = new Lab::Instrument::HP34401A({
    connection_type => 'USBtmc',
    tmc_address=>1,
  }

=head1 DESCRIPTION

Driver for the interface provided by the usbtmc linux kernel module.

Obviously, this will work for Linux systems only. 
On Windows, please use L<Lab::Bus::VISA>. The interfaces are (errr, will be) identical.

Note: you don't need to explicitly handle bus objects. The Instruments will create them themselves, and existing bus will
be automagically reused.


=head1 CONSTRUCTOR

=head2 new

 my $bus = Lab::Bus::USBtmc({
  });

Return blessed $self, with @_ accessible through $self->config().



=head1 Thrown Exceptions

Lab::Bus::USBtmc throws

  Lab::Exception::TMCOpenFileError
  
  Lab::Exception::CorruptParameter

=head1 METHODS

=head2 connection_new

  $tmc->connection_new({ tmc_address => $addr });

Creates a new connection ("instrument handle") for this bus. The argument is a hash, whose contents depend on the bus type.
For TMC at least 'tmc_address' is needed.

The handle is usually stored in an instrument object and given to connection_read, connection_write etc.
to identify and handle the calling instrument:

  $InstrumentHandle = $GPIB->connection_new({ gpib_address => 13 });
  $result = $GPIB->connection_read($self->InstrumentHandle(), { options });

See C<Lab::Instrument::Read()>.

=head2 connection_write

  $GPIB->connection_write( $InstrumentHandle, { Cmd => $Command } );

Sends $Command to the instrument specified by the handle.


=head2 connection_read

  $GPIB->connection_read( $InstrumentHandle, { Cmd => $Command, ReadLength => $readlength, Brutal => 0/1 } );

Sends $Command to the instrument specified by the handle. Reads back a maximum of $readlength bytes. If a timeout or
an error occurs, Lab::Exception::GPIBError or Lab::Exception::Timeout are thrown, respectively. The Timeout object
carries the data received up to the timeout event, accessible through $Exception->Data().

Setting C<Brutal> to a true value will result in timeouts being ignored, and the gathered data returned without error.

=head2 timeout

  $GPIB->timeout( $connection_handle, $timeout );

Sets the timeout in seconds for GPIB operations on the device/connection specified by $connection_handle.

=head2 config

Provides unified access to the fields in initial @_ to all the child classes.
E.g.

 $GPIB_Address=$instrument->config(gpib_address);

Without arguments, returns a reference to the complete $self->config aka @_ of the constructor.

 $config = $bus->config();
 $GPIB_PAddress = $bus->config()->{'gpib_address'};

=head1 CAVEATS/BUGS

Sysfs settings for timeout not supported, yet.

=head1 SEE ALSO

=over 4

=item 

L<Lab::Bus>

=item

and many more...

=back

=head1 AUTHOR/COPYRIGHT

 Copyright 2004-2006 Daniel Schröer <schroeer@cpan.org>, 
           2009-2010 Daniel Schröer, Andreas K. Hüttel (L<http://www.akhuettel.de/>) and David Kalok,
           2010      Matthias Völker <mvoelker@cpan.org>
           2011      Florian Olbrich, Andreas K. Hüttel
           2012      Hermann Kraus

This library is free software; you can redistribute it and/or modify it under the same
terms as Perl itself.

=cut







1;

