Beefy Boxes and Bandwidth Generously Provided by pair Networks
Your skill will accomplish
what the force of many cannot
 
PerlMonks  

Linux 3D Space Navigator control

by shotgunefx (Parson)
on Apr 08, 2008 at 22:25 UTC ( #679093=sourcecode: print w/ replies, xml ) Need Help??

Category: Fun Stuff
Author/Contact Info Lee Pumphret - www.leeland.net
Description: Rudimentary code for using a 3Dconnexion SpaceNavigatorô under Linux using the /dev/input/eventN interface. Currently what I'm using to control Auto-DAC, my Automotive media player. (video of the navigator in use, here )
#!/usr/bin/perl
# $version 0.3

=head2 Description
usage: perl ptest.pl -d DEVICENAME [--primary_axis]  [--led]

Needs to be run sudo or as root. 
-d Device name should be in the form of /dev/input/event[num] or a sym
+link to the event

--primary_axis will make it act as there is one axis, whichever is dom
+inant

--led will light the leds

Prerequisites from CPAN:
Linux::Input
Linux::Input::Info

Originally based on spacenavi.c by Simon Budig.
http://www.home.unix-ag.org/simon/files/spacenavi.c


This is a skeleton program to interact with a 3DConnexion Space Naviga
+tor through evdev
It supports calibration, scaling of inputs, action debouncing and more
+.

=head1 Modes of Operation
Normal Operation:
In normal operation, it will simply report back device input based on 
+configuration. 
If the movement is < mindeflect, the axis will be treated as having no
+ movement.
Movement is not acknowledged until the Axis has been acted on longer t
+han debounce time.

Primary Axis:
If "Primary Axis" is enabled, the pointer will act as 8 buttons. Only 
+the axis most acted upon
will be acknowledged. If the Axis determined to be primary differs fro
+m the previous, a release event
will be generated for the previous primary axis (as if you had let it 
+go).

LEDs:
The sub that handles the LEDs (set_spnvled) has the input event format
+ hardcoded. Not portable.

=head1 TODO
Add a lot more command line options for individual axes and options
Add support for reading in calibration data.
Better docs, read the code for now :)

=head1 AUTHOR
Lee Pumphret
http://www.leeland.net/3d-space-navigator-linux.html

=cut

use strict;
use warnings;
no warnings qw/uninitialized/;

use Time::HiRes qw/time/;    # Need higher res time for debouncing
use Linux::Input::Info;
use Linux::Input v1.03;
use Getopt::Long;
use Fcntl;

our( $USE_PRIMARY_AXIS, $DEV, $LED_STATE );

#$SIG{ALRM} = sub { die "timeout" };

# Set defaults
$USE_PRIMARY_AXIS = 0;       # If true, we pretend that each Axis is s
+eparate,
                             # and only one can be active at a given t
+ime.

GetOptions(
    'd=s'         => \$DEV,
    'use_primary' => \$USE_PRIMARY_AXIS,
    'led'         => \$LED_STATE,

);

# Should cajole Linux::Input::Info to export these, constants from inp
+ut.h
use constant {
    EV_SYN   => 0x00,
    EV_KEY   => 0x01,
    EV_REL   => 0x02,
    EV_LED   => 0x11,
    REL_X    => 0x00,
    REL_RZ   => 0x05,
    LED_MISC => 0x08,
    BTN_0    => 0x100,
    BTN_1    => 0x101,
};

use constant {    # Axis/button names for array indexes,
    SPN_LR       => 0,    # Left/Right
    SPN_FB       => 1,    # Fwd/Back
    SPN_UD       => 2,    # Up/Down
    SPN_TILT_FB  => 3,    # Tilt Fwd/Back
    SPN_TILT_LR  => 4,    # Tilt Left/Right (evdev reports this + to -
                          # instead of - to + as we would expect, we f
+lip it
    SPN_ROT_LR   => 5,    # Rotation Left/Right
    SPN_BUTTON_1 => 6,
    SPN_BUTTON_2 => 7,

};

use constant {
    SPN_SCALE => 1000,   # We scale the inputs so they are all ~ +/- S
+PN_SCALE/2
    SPN_MIN_DEFLECTION =>
      250,    # Default minimum delflection, we ignore movement less t
+han this
    SPN_DEBOUNCE => 0.02,    # Fractional seconds to debounce actions
    SPN_AXIS_MAX => 5,       # How many axes does the device have?
};

# Where we store device Axis info
# Eventually, a hash of array refs, keyed by application, all data fro
+m config files
my @ax_info = (

    #SPN_LR()
    {
        label       => 'SPN_LR',     # Name for printing for convience
        val         => 0,            # Where we keep the value of the 
+axis
        cmin        => -342,         # Calibration minimum
        cmax        => 382,          # Calibration maximum
        mindeflect  => 200,          # Movement < this ignored
        sensitivity => SPN_SCALE,    # 0 - SPN_SCALE, higher is more s
+ensitive.
             # let's us favor say, tilt front/back over movement front
+/back
        active     => 0,   # Internally set, flag if axis is active, l
+eave it ;)
        press_time => 0,   # time we went active, leave it ;)
        event_sent => 0,   # Internally set, if an event was generated
+ yet evebt
        ignore     => 0,   # If true, axis is ignored
    },

    #SPN_FB()
    {
        label       => 'SPN_FB',
        val         => 0,
        cmin        => -412,
        cmax        => 371,
        mindeflect  => 200,
        sensitivity => SPN_SCALE,
        active      => 0,
        press_time  => 0,
        event_sent  => 0,
        ignore      => 0,
    },

    #SPN_UD()
    {
        label       => 'SPN_UD',
        val         => 0,
        cmin        => -448,
        cmax        => 397,
        mindeflect  => 200,
        sensitivity => SPN_SCALE,
        active      => 0,
        press_time  => 0,
        event_sent  => 0,
        ignore      => 0,
    },

    #SPN_TILT_FB()
    {
        label       => 'SPN_TILT_FB',
        val         => 0,
        cmin        => -374,
        cmax        => 393,
        mindeflect  => 200,
        sensitivity => SPN_SCALE,
        active      => 0,
        press_time  => 0,
        event_sent  => 0,
        ignore      => 0,
    },

    #SPN_TILT_LR()
    {
        label       => 'SPN_TILT_LR',
        val         => 0,
        cmin        => 369,
        cmax        => -374,
        mindeflect  => 200,
        sensitivity => SPN_SCALE,
        active      => 0,
        press_time  => 0,
        event_sent  => 0,
        ignore      => 0,
    },

    #SPN_ROT_LR()
    {
        label       => 'SPN_ROT_LR',
        val         => 0,
        cmin        => -439,
        cmax        => 381,
        mindeflect  => 200,
        sensitivity => SPN_SCALE,
        active      => 0,
        press_time  => 0,
        event_sent  => 0,
        ignore      => 0,
    },
    {
        label => 'SPN_BUTTON_1',
        val   => 0,
    },
    {
        label => 'SPN_BUTTON_2',
        val   => 0,
    },

);

#############################################
#  Generate scaling data from calibration   #
#############################################
for my $c ( 0 .. SPN_AXIS_MAX ) {
    my $mul =
      $ax_info[$c]->{sensitivity} /
      ( $ax_info[$c]->{cmin} - $ax_info[$c]->{cmax} );

    # Range check here
    if ( $ax_info[$c]{mindeflect} >= SPN_SCALE / 2 ) {
        warn "Axis:$c minimum deflection too high!, ignoring\n";
        $ax_info[$c]{mindeflect} = SPN_MIN_DEFLECTION;
    }

    $ax_info[$c]{mindeflect} = SPN_MIN_DEFLECTION
      unless exists $ax_info[$c]{mindeflect};

    printf(
        "% 4d : % 4d\n", $ax_info[$c]{cmin} * $mul,
        $ax_info[$c]{cmax} * $mul
    );
    $ax_info[$c]{scale} = $mul;
}

die "No device specified!" unless $DEV;    # Whoops, no input device

my $en = $DEV;

if ( -l $en ) {

    # If symlink, get the actual name.  I use udev to symlink it
    $en = readlink($DEV) or die $!;
}

die "$en doesn't look like a valid input!"
  unless ( $en =~ s#^.*event(\d+)$#$1# );    # Should look like eventN

# Get dev info for debugging
my $i = Linux::Input::Info->new($en);
printf "$DEV\n";
printf "\tbustype  : %s\n",   $i->bustype;
printf "\tvendor   : 0x%x\n", $i->vendor;
printf "\tproduct  : 0x%x\n", $i->product;
printf "\tversion  : %d\n",   $i->version;
printf "\tname     : %s\n",   $i->name;
printf "\tuniq     : %s\n",   $i->uniq;
printf "\tphys     : %s\n",   $i->phys;
printf "\tbits ev  :";
printf " %s", $i->ev_name($_) for $i->bits;
printf "\n";

set_spnvled( $DEV, $LED_STATE );

# Get a handle for the input device
my $spnv = Linux::Input->new($DEV);    # Open the input device

print "Press Ctrl-C to quit\n";

while (1) {                            # Event loop
    my ( $max_axis, @min, @max );      # Used to figure which axis is 
+primary
    my @generated_events;

    while ( my @events = $spnv->poll(0.001) ) {    # Check for an even
+t

        foreach my $e (@events) {

            # Process 'em

            my $max_val = 0;

            if ( $e->{type} == EV_REL ) {          # Relative moment e
+vent

                if ( $e->{code} <= REL_RZ ) {

                    # Scale and save the data unless we are set to ign
+ore
                    my $axis = $e->{code} - REL_X;
                    $ax_info[$axis]{val} =
                      $ax_info[$axis]{ignore} 
                      ? 0
                      : $e->{value} * $ax_info[$axis]{scale};

                }
                else {
                    warn "Unknown code: $e->{code}!\n";
                }

            }
            elsif ( $e->{type} == EV_KEY ) {    # Button press or rele
+ase
                if ( $e->{code} >= BTN_0 && $e->{code} <= BTN_1 ) {
                    my $button =
                      $e->{code} - BTN_0 ? SPN_BUTTON_2: SPN_BUTTON_1;
                    push @generated_events,
                      [
                        $button, $e->{'value'} ? 1 : -1,
                        $ax_info[$button]{val}
                      ];
                    print $ax_info[ $button]{label},
                      $e->{'value'} ? " pressed!\n" : " released!\n";
                }
                else {
                    warn "Unknown button: $e->{code}!\n";
                }
            }
            elsif ( $e->{type} == EV_SYN ) {

                # EV_SYN = sync, the kernel has passed as all the axis
+/button info
                for my $m ( 0 .. SPN_AXIS_MAX ) {
                    if ( abs( $ax_info[$m]{val} ) < $ax_info[$m]{minde
+flect} ) {
                        $ax_info[$m]{val} =
                          0;    # Set value to 0 if < Axis mindeflect
                    }

                    if ( $ax_info[$m]{val} ) {

                        # We have an event we care about
                        if ( $ax_info[$m]{press_time}
                            and !$ax_info[$m]{active} )
                        {       # Debounce
                            if (
                                time() - $ax_info[$m]{press_time} >
                                SPN_DEBOUNCE )
                            {

                                # It's been active long enough, genera
+te an event
                                $ax_info[$m]{active} =
                                  $ax_info[$m]{val} > 0 ? -1 : 1;
                                push @generated_events,
                                  [ $m, 1, $ax_info[$m]{val} ];
                            }
                        }
                        else {    # Wasn't active, is now, set time
                            $ax_info[$m]{press_time} = time();   # Cha
+nge to app
                        }

                    }
                    else {

                        # Clean up as needed
                        if ( $ax_info[$m]{active} ) {

                            # Generate release
                            $ax_info[$m]{active} = 0;
                            push @generated_events,
                              [ $m, -1, $ax_info[$m]{val} ]
                              if $ax_info[$m]{event_sent};
                        }
                        $ax_info[$m]{press_time} = undef;    # Reset p
+ress time
                    }

                    if ( $max_val < abs( $ax_info[$m]{val} ) ) {

                        # Save the primary axis
                        $max_axis = $m;
                        $max_val  = abs $ax_info[$m]{val};
                    }
                }

            }

        }

    }

    # Now we have that events that need to be processed
    if (@generated_events) {

        # If we want only a primary axis, we need to make sure to send
        # a release event for any Axis previously considered active
        if ($USE_PRIMARY_AXIS) {
            @generated_events =
              grep {
                $_->[0] == $max_axis
                  or $_->[1] < 0    # Allow  primary and release event
+s
              } @generated_events;    # Filter pending events

            # Fake release events for any other axis still active
            for ( 0 .. SPN_AXIS_MAX ) {
                next if $_ == $max_axis;    # Skip the primary
                     # Generate a release even if a "press" has been s
+ent
                push @generated_events, [ $_, -1, $ax_info[$_]{value} 
+]
                  if $ax_info[$_]{active}
                  and $ax_info[$_]{event_sent};

            }

        }

        while (@generated_events) {
            my $e = shift @generated_events;

            # Track that the event has been sent so we can surpress
            # false releases due to USE_PRIMARY_AXIS  as needed
            if ( $e->[1] == 1 ) {
                $ax_info[ $e->[0] ]{event_sent} = 1;

                #! Here is where you would actually do something on pr
+ess
                print $ax_info[ $e->[0] ]{label}, " pressed!\n";

            }
            else {
                $ax_info[ $e->[0] ]{event_sent} = 0;

                #! Here is where you would actually do something on pr
+ess
                print $ax_info[ $e->[0] ]{label}, " released!\n";
            }

        }

    }

}

set_spnvled( $en, 0 );

sub set_spnvled {
    my ( $dev, $state ) = @_;
    sysopen( DEV, $dev, O_WRONLY )
      or die "Couldn't open $dev for writing: $!\n";

    # Highly unportable, need to match  struct input_event event in in
+put.h
    my $ev = pack( 'L!L!S!S!i!', time(), '0', EV_LED, LED_MISC, $state
+ );
    my $written = syswrite( DEV, $ev );
    die "Write failed:[$written] != [" . length($ev) . "]  $!"
      unless $written == length $ev;
}

Comment on Linux 3D Space Navigator control
Download Code

Back to Code Catacombs

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: sourcecode [id://679093]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others musing on the Monastery: (7)
As of 2014-12-29 07:27 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    Is guessing a good strategy for surviving in the IT business?





    Results (185 votes), past polls