<?xml version="1.0" encoding="windows-1252"?>
<node id="679093" title="Linux 3D Space Navigator control" created="2008-04-08 18:25:36" updated="2008-04-08 14:25:36">
<type id="1748">
sourcecode</type>
<author id="75719">
shotgunefx</author>
<data>
<field name="doctext">
&lt;code&gt;
#!/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 symlink to the event

--primary_axis will make it act as there is one axis, whichever is dominant

--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 Navigator 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 &lt; mindeflect, the axis will be treated as having no movement.
Movement is not acknowledged until the Axis has been acted on longer than 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 from 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 separate,
                             # and only one can be active at a given time.

GetOptions(
    'd=s'         =&gt; \$DEV,
    'use_primary' =&gt; \$USE_PRIMARY_AXIS,
    'led'         =&gt; \$LED_STATE,

);

# Should cajole Linux::Input::Info to export these, constants from input.h
use constant {
    EV_SYN   =&gt; 0x00,
    EV_KEY   =&gt; 0x01,
    EV_REL   =&gt; 0x02,
    EV_LED   =&gt; 0x11,
    REL_X    =&gt; 0x00,
    REL_RZ   =&gt; 0x05,
    LED_MISC =&gt; 0x08,
    BTN_0    =&gt; 0x100,
    BTN_1    =&gt; 0x101,
};

use constant {    # Axis/button names for array indexes,
    SPN_LR       =&gt; 0,    # Left/Right
    SPN_FB       =&gt; 1,    # Fwd/Back
    SPN_UD       =&gt; 2,    # Up/Down
    SPN_TILT_FB  =&gt; 3,    # Tilt Fwd/Back
    SPN_TILT_LR  =&gt; 4,    # Tilt Left/Right (evdev reports this + to -
                          # instead of - to + as we would expect, we flip it
    SPN_ROT_LR   =&gt; 5,    # Rotation Left/Right
    SPN_BUTTON_1 =&gt; 6,
    SPN_BUTTON_2 =&gt; 7,

};

use constant {
    SPN_SCALE =&gt; 1000,   # We scale the inputs so they are all ~ +/- SPN_SCALE/2
    SPN_MIN_DEFLECTION =&gt;
      250,    # Default minimum delflection, we ignore movement less than this
    SPN_DEBOUNCE =&gt; 0.02,    # Fractional seconds to debounce actions
    SPN_AXIS_MAX =&gt; 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 from config files
my @ax_info = (

    #SPN_LR()
    {
        label       =&gt; 'SPN_LR',     # Name for printing for convience
        val         =&gt; 0,            # Where we keep the value of the axis
        cmin        =&gt; -342,         # Calibration minimum
        cmax        =&gt; 382,          # Calibration maximum
        mindeflect  =&gt; 200,          # Movement &lt; this ignored
        sensitivity =&gt; SPN_SCALE,    # 0 - SPN_SCALE, higher is more sensitive.
             # let's us favor say, tilt front/back over movement front/back
        active     =&gt; 0,   # Internally set, flag if axis is active, leave it ;)
        press_time =&gt; 0,   # time we went active, leave it ;)
        event_sent =&gt; 0,   # Internally set, if an event was generated yet evebt
        ignore     =&gt; 0,   # If true, axis is ignored
    },

    #SPN_FB()
    {
        label       =&gt; 'SPN_FB',
        val         =&gt; 0,
        cmin        =&gt; -412,
        cmax        =&gt; 371,
        mindeflect  =&gt; 200,
        sensitivity =&gt; SPN_SCALE,
        active      =&gt; 0,
        press_time  =&gt; 0,
        event_sent  =&gt; 0,
        ignore      =&gt; 0,
    },

    #SPN_UD()
    {
        label       =&gt; 'SPN_UD',
        val         =&gt; 0,
        cmin        =&gt; -448,
        cmax        =&gt; 397,
        mindeflect  =&gt; 200,
        sensitivity =&gt; SPN_SCALE,
        active      =&gt; 0,
        press_time  =&gt; 0,
        event_sent  =&gt; 0,
        ignore      =&gt; 0,
    },

    #SPN_TILT_FB()
    {
        label       =&gt; 'SPN_TILT_FB',
        val         =&gt; 0,
        cmin        =&gt; -374,
        cmax        =&gt; 393,
        mindeflect  =&gt; 200,
        sensitivity =&gt; SPN_SCALE,
        active      =&gt; 0,
        press_time  =&gt; 0,
        event_sent  =&gt; 0,
        ignore      =&gt; 0,
    },

    #SPN_TILT_LR()
    {
        label       =&gt; 'SPN_TILT_LR',
        val         =&gt; 0,
        cmin        =&gt; 369,
        cmax        =&gt; -374,
        mindeflect  =&gt; 200,
        sensitivity =&gt; SPN_SCALE,
        active      =&gt; 0,
        press_time  =&gt; 0,
        event_sent  =&gt; 0,
        ignore      =&gt; 0,
    },

    #SPN_ROT_LR()
    {
        label       =&gt; 'SPN_ROT_LR',
        val         =&gt; 0,
        cmin        =&gt; -439,
        cmax        =&gt; 381,
        mindeflect  =&gt; 200,
        sensitivity =&gt; SPN_SCALE,
        active      =&gt; 0,
        press_time  =&gt; 0,
        event_sent  =&gt; 0,
        ignore      =&gt; 0,
    },
    {
        label =&gt; 'SPN_BUTTON_1',
        val   =&gt; 0,
    },
    {
        label =&gt; 'SPN_BUTTON_2',
        val   =&gt; 0,
    },

);

#############################################
#  Generate scaling data from calibration   #
#############################################
for my $c ( 0 .. SPN_AXIS_MAX ) {
    my $mul =
      $ax_info[$c]-&gt;{sensitivity} /
      ( $ax_info[$c]-&gt;{cmin} - $ax_info[$c]-&gt;{cmax} );

    # Range check here
    if ( $ax_info[$c]{mindeflect} &gt;= 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-&gt;new($en);
printf "$DEV\n";
printf "\tbustype  : %s\n",   $i-&gt;bustype;
printf "\tvendor   : 0x%x\n", $i-&gt;vendor;
printf "\tproduct  : 0x%x\n", $i-&gt;product;
printf "\tversion  : %d\n",   $i-&gt;version;
printf "\tname     : %s\n",   $i-&gt;name;
printf "\tuniq     : %s\n",   $i-&gt;uniq;
printf "\tphys     : %s\n",   $i-&gt;phys;
printf "\tbits ev  :";
printf " %s", $i-&gt;ev_name($_) for $i-&gt;bits;
printf "\n";

set_spnvled( $DEV, $LED_STATE );

# Get a handle for the input device
my $spnv = Linux::Input-&gt;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-&gt;poll(0.001) ) {    # Check for an event

        foreach my $e (@events) {

            # Process 'em

            my $max_val = 0;

            if ( $e-&gt;{type} == EV_REL ) {          # Relative moment event

                if ( $e-&gt;{code} &lt;= REL_RZ ) {

                    # Scale and save the data unless we are set to ignore
                    my $axis = $e-&gt;{code} - REL_X;
                    $ax_info[$axis]{val} =
                      $ax_info[$axis]{ignore} 
                      ? 0
                      : $e-&gt;{value} * $ax_info[$axis]{scale};

                }
                else {
                    warn "Unknown code: $e-&gt;{code}!\n";
                }

            }
            elsif ( $e-&gt;{type} == EV_KEY ) {    # Button press or release
                if ( $e-&gt;{code} &gt;= BTN_0 &amp;&amp; $e-&gt;{code} &lt;= BTN_1 ) {
                    my $button =
                      $e-&gt;{code} - BTN_0 ? SPN_BUTTON_2: SPN_BUTTON_1;
                    push @generated_events,
                      [
                        $button, $e-&gt;{'value'} ? 1 : -1,
                        $ax_info[$button]{val}
                      ];
                    print $ax_info[ $button]{label},
                      $e-&gt;{'value'} ? " pressed!\n" : " released!\n";
                }
                else {
                    warn "Unknown button: $e-&gt;{code}!\n";
                }
            }
            elsif ( $e-&gt;{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} ) &lt; $ax_info[$m]{mindeflect} ) {
                        $ax_info[$m]{val} =
                          0;    # Set value to 0 if &lt; 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} &gt;
                                SPN_DEBOUNCE )
                            {

                                # It's been active long enough, generate an event
                                $ax_info[$m]{active} =
                                  $ax_info[$m]{val} &gt; 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();   # Change 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 press time
                    }

                    if ( $max_val &lt; 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 {
                $_-&gt;[0] == $max_axis
                  or $_-&gt;[1] &lt; 0    # Allow  primary and release events
              } @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 sent
                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-&gt;[1] == 1 ) {
                $ax_info[ $e-&gt;[0] ]{event_sent} = 1;

                #! Here is where you would actually do something on press
                print $ax_info[ $e-&gt;[0] ]{label}, " pressed!\n";

            }
            else {
                $ax_info[ $e-&gt;[0] ]{event_sent} = 0;

                #! Here is where you would actually do something on press
                print $ax_info[ $e-&gt;[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 input.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;
}
&lt;/code&gt;</field>
<field name="codedescription">
Rudimentary code for using a [http://www.3dconnexion.com/3dmouse/spacenavigator.php|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, [http://www.youtube.com/watch?v=Gtqj2N8hc3Q|here] )

</field>
<field name="codecategory">
Fun Stuff</field>
<field name="codeauthor">
Lee Pumphret - www.leeland.net</field>
</data>
</node>
