#!/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.
https://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 < 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 :)
=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' => \$DEV,
'use_primary' => \$USE_PRIMARY_AXIS,
'led' => \$LED_STATE,
);
# Should cajole Linux::Input::Info to export these, constants from input.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 flip 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 ~ +/- SPN_SCALE/2
SPN_MIN_DEFLECTION =>
250, # Default minimum delflection, we ignore movement less than 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 from 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 sensitive.
# let's us favor say, tilt front/back over movement front/back
active => 0, # Internally set, flag if axis is active, leave 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 event
foreach my $e (@events) {
# Process 'em
my $max_val = 0;
if ( $e->{type} == EV_REL ) { # Relative moment event
if ( $e->{code} <= REL_RZ ) {
# Scale and save the data unless we are set to ignore
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 release
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]{mindeflect} ) {
$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, generate 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(); # 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 < 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 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->[1] == 1 ) {
$ax_info[ $e->[0] ]{event_sent} = 1;
#! Here is where you would actually do something on press
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 press
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 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;
}