Beefy Boxes and Bandwidth Generously Provided by pair Networks
P is for Practical
 
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 lurking in the Monastery: (4)
As of 2014-09-24 04:49 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    How do you remember the number of days in each month?











    Results (245 votes), past polls