Joy device input in Linux in Tcl

Reading input from game controllers / joy devices (e.g. USB game pads, joysticks, etc) is pretty straightforward in Linux. You can do it fairly easily without extra libraries, by just opening the appropriate device file for reading.

Aside from the obvious use of controlling computer games, game controllers can come in handy for all kinds of projects - anything where you have a need for buttons and/or control sticks. Depending on your needs, it might be more convenient than alternatives such as wiring up your own buttons to GPIO pins. So this is a pretty useful feature.

Here is an example in pure Tcl, with Tk for a graphical demonstration. I made an effort to make the code adaptable, so you can cut bits out and make it do whatever you need.

Linux joypad input in tcl/tk, demonstration

# -------------------------------------------------------------------------------
# -- Simple joy device input in Tcl using an input file channel and 'filevent' --
# -------------------------------------------------------------------------------

# These procedures are called to handle the various kinds of joy device events.
# First, re-define these procedures to do whatever you need, and then call 'openJoyDevice' with the path of your joy device.
# Note that 'axisValue' is a value from -32767 to +32767, and 'eventTime' is a millisecond clock time (thousandths of a second)
proc joyButtonInitHandler {buttonNumber eventTime} {}
proc joyAxisInitHandler {axisNumber axisValue eventTime} {}
proc joyButtonPressHandler {buttonNumber eventTime} {}
proc joyButtonReleaseHandler {buttonNumber eventTime} {}
proc joyAxisMovedHandler {axisNumber axisValue eventTime} {}

# This opens the joy device file and registers a 'fileevent' to respond to joy device state changes.
proc openJoyDevice {joyDevicePath} {
 if [catch {set ::joyDevice [open $joyDevicePath {RDONLY BINARY}]}] {
  error "Couldn't open joy device '$joyDevicePath'"
 }
 fileevent $::joyDevice readable joyEventHandler
}

# This handles joy input events, reading the data from the joypad device file, and calling the appropriate handler procedure for the event.
proc joyEventHandler {} {
 # Read the data from the joy device (look up linux 'joystick.h' online for more info)
 binary scan [read $::joyDevice 8] {i s c c} joy_time joy_value joy_type joy_number
 set joy_time [expr {$joy_time & 0xffffffff}]
 set joy_type [expr {$joy_type & 0xff}]
 set joy_number [expr {$joy_number & 0xff}]
 # Interpret the data
 switch -- $joy_type {
  1 {
   # 1 : Joy button state change
   if {$joy_value} {
    joyButtonPressHandler $joy_number $joy_time
   } else {
    joyButtonReleaseHandler $joy_number $joy_time
   }
  }
  2 {
   # 2 : Joy axis state change
   joyAxisMovedHandler $joy_number $joy_value $joy_time
  }
  129 {
   # 129 (128 | 1) : Joy button init
   joyButtonInitHandler $joy_number $joy_time
  }
  130 {
   # 130 (128 | 2) : Joy axis init
   joyAxisInitHandler $joy_number $joy_value $joy_time
  }
  default {
   puts stderr "Unknown joy event, type $joy_type"
  }
 }
}

# ------------------------------------
# --------- Demonstration ------------
# ------------------------------------

package require Tk
tk appname "Joy device tester"

pack [frame .f] -fill both -expand 1
pack [frame .f.buttons ] -side left -fill both -expand 1
pack [frame .f.axes ] -side right -fill both -expand 1
pack [label .l -anchor w -relief sunken -borderwidth 1] -side bottom -fill x

# Keep track of last event time, for the timer info label
set lastEventTime 0
proc updateTimer {t} {
 global lastEventTime
 .l configure -text "Last event: [expr { ($t - $lastEventTime) / 1000.0 }] seconds ago"
 set lastEventTime $t
}

# Create buttons 
proc joyButtonInitHandler {buttonNumber eventTime} {
 pack [button .f.buttons.b$buttonNumber -text "Button $buttonNumber" -background black -disabledforeground white -state disabled]
 updateTimer $eventTime
}

# Create axis sliders
proc joyAxisInitHandler {axisNumber axisValue eventTime} {
 pack [scale .f.axes.a$axisNumber -label "Axis $axisNumber" -orient h -from -32768 -to 32767 -resolution 1 -state disabled -length 150]
 updateTimer $eventTime
}

# Update the visual state of buttons
proc joyButtonPressHandler {buttonNumber eventTime} {
 .f.buttons.b$buttonNumber configure -disabledforeground black -background white
 updateTimer $eventTime
}
proc joyButtonReleaseHandler {buttonNumber eventTime} {
 .f.buttons.b$buttonNumber configure -disabledforeground white -background black
 updateTimer $eventTime
}

# Update the visual state of axis sliders
proc joyAxisMovedHandler {axisNumber axisValue eventTime} {
 .f.axes.a$axisNumber configure -state normal
 .f.axes.a$axisNumber set $axisValue
 .f.axes.a$axisNumber configure -state disabled
 updateTimer $eventTime
}

# Open the joy device
if {[llength $argv]} {
 set joyDevicePath [lindex $argv 0]
} else {
 set joyDevicePath "/dev/input/js0"
}
openJoyDevice $joyDevicePath

# We don't want the window size to keep changing to fit the width of the timer info label
after 17 {
 wm geometry . [winfo reqwidth .]x[winfo reqheight .]
}