mameau%CWD%:> █
OFFLINE
$ cat article_0x04.txt
#==============================================================================
# UDEV, SDL, systemd and Multiple Controllers and Linux 
# VERSION: 0x04
# AUTHOR: sairuk
# CONTACT: mameau.com
#==============================================================================


#C | Changelog
#==============================================================================
0x04 - Added steam controllers and madcatz mc2 xbox wheel
     - moved the nubytext arcade controller to external to maintain naming 
       standards
     - looking longterm i'll probably stop using udev completely for this use 
       case and just base it on systemd path monitoring
0x03 - Add some dependancy changes for systemd since boot was screaming
0x02 - Change the system timer to a path file
0x01 - Initial release, prob typos


#X | Contents:
#==============================================================================
#0|Introduction
#1|SDL input override
#2|UDEV rules.d example
#3|systemd management
#4|bash scripts


#0 | Introduction
#==============================================================================
This article looks at UDEV, SDL and general controller maintenance under linux.

Needed a better way to manage multiple controller types across varying inputs
under linux so began looking at udev as an option. Found a number of 
shortcomings (based on my limited understanding of udev perhaps) so found other
solutions.

The shortcomings worked around (in no particular order)
- linux kernel indexes js devices dynamically based on order discovered, 
  influenced by connection order and/or usb bus order, can change on boot
- udev kernel device detection and firing is too fast for general shell scripts
  to action against
- udev external command wont take wildcards(?)
- Multiplayer usb adapters do not provide properties to match each player port 
  accurately against to create a static name

The goal was to take a device node e.g. /dev/input/js0 and symlink it to a 
static name based on the device type and player port e.g. /dev/input/snesp1 so 
that programs could be configured to use the static name instead of the device 
that may change index.

The problem exists that if we were to plug another joystick device into a usb 
port with a higher index on the usb bus and reboot, our existing device 
/dev/input/js0 would become /dev/input/js1 with the device on the higher port
now being /dev/input/js0 and when starting our program would then be using the 
incorrect joystick device.

If we instead configure our application (either through the application or a 
SDL input override see section 1) to use the static device name instead of the
dynamic name it will not matter if the js device index changes so long as we 
mange the static name link to the device.

The final solution was to use a number of external means to create and manage
the static names.

Noting, this is still very much a work in progress


#1 | SDL input override
#==============================================================================
Set the env var SDL_JOYSTICK_DEVICE before launching the SDL application to add
additional devices instead of the start search paths. Handy if you have udev 
static naming for your devices

A good example of this is Supermodel where you cannot easily set the joy devices

Running supermodel -print-inputs will output devices in standard SDL search 
paths for inputs /dev/js* /dev/input/js* /dev/input/event*

{{{

Input System Settings
---------------------

Input System: SDL

Keyboards:
 System Keyboard
Mice:
 System Mouse
Joysticks:
 1: HID 0c12:0005
 2: Twin USB Joystick
 3: Twin USB Joystick


In this example:
1 is js0, my Nubytech Arcade controller
2 is js1, 
3 is js2

}}}

But for say Scud race it would be better to configure one of the Twin USB 
Joysticks with analogs over the arcade controller.

Supermodel assigns joystick devices automatically based on their device index

We can override that and tell it to also add additional devices, devices passed
with the env var will become SDL index 1 so if we launch supermodel as per below
we will see that the Twin USB Joystick gets listed twice

SDL_JOYSTICK_DEVICE=/dev/input/psxp1:/dev/input/psxp2

{{{

$ SDL_JOYSTICK_DEVICE=/dev/input/psxp1:/dev/input/psxp2 ./bin/supermodel -print-inputs

Input System Settings
---------------------

Input System: SDL

Keyboards:
 System Keyboard
Mice:
 System Mouse
Joysticks:
 1: Twin USB Joystick
 2: Twin USB Joystick
 3: HID 0c12:0005
 4: Twin USB Joystick
 5: Twin USB Joystick

}}}

So now our static devices are index 1 and 2 and subsequently player
1 and 2


#2 | UDEV rules.d example
#==============================================================================
/etc/udev/rules.d/99-my-controllers.rules

{{{

###############################################################################
# My controller(s) setup
# - sairuk
# 
# Save to `/etc/udev/rules.d` for use.
#
# Goal(s):
# -----------------------------------------------------------------------------
# 1. Where possible use udev
#
# 2. Provide static names for USB controllers regardless of port or order the
# devices are plugged into the system
#
# 3. Devices should be available when a controller is plugged in and any 
# properties removed when the device is no longer connected
#
# Problem(s):
# -----------------------------------------------------------------------------
# Linux indexes the js devices by the order in which they are connected OR the
# order in which the device appears on the usb bus. This does not allow for 
# static identifcation of a device because there is the possibility it will 
# change on boot or reconnection.
#
# Multiplayer USB adapters do not provide properties where each player can
# be confidently identified by udev.
#
# Therefore using SYMLINK+="input/p%n" with KERNEL=="js[0-9*" or similar 
# will not permit you create consistent device naming. The resulting name will 
# be either:
# - determined by the device index
# - only create the initial symlink
# - overwrite the existing the symlink
# 
# Using the udev RUN= or RUN+= to execute and external script will fire too
# quickly for the shell to process actions and you end up with a race condition
# when determining if a symlink exists for each player
#
# RUN does not appear to accept wildcards
#
# Solution
# -----------------------------------------------------------------------------
# When a device is added it is indexed from the first player through to the 
# last therefore all incrementing indexes for a device should start at player1 
# as lowest device index. We should be able to loop over the js devices and
# match on properties to determine the type of adapter then force our naming
# convention for symlinks
#
# Where it is not possible to accurately identify a each indiviual player on
# an adapter use an external script to create symlinks processesed outside of 
# udev to meet goal 2
#
# let udev clean up the device to meet goals 1 and 3
#
###############################################################################


###############################################################################
# UDEV
#
# udev is used where a device has a property to allow for it to be identified
# accuractely and actioned appropriately. Perhaps the adapter is a single
# player adapter or has a property we can use to identify it.
#
# e.g. XBOX Dragon USB Adapter or a DS3 controller where we can match on the 
# uniq device property.
###############################################################################

### DUALSHOCK 3
#KERNEL=="event*", SUBSYSTEM=="input", ATTRS{uniq}=="00:1b:fb:Bbd:0b:bd", SYMLINK+="input/dualshock3"

###############################################################################
# EXTERNAL
#
# These USB adapters do not have a property available to determine which player
# the device is for so we handle that externally once the devices are connected
# 
# The script /usr/local/bin/linkdev_js is run as a systemd service file by a
# systemd timer the timer executes the service every 5s
#
# systemd unit files
# /etc/systemd/system/controllers.service
# /etc/systemd/system/controllers.timer
#
# linkdev_js loops over the js devices checking if a symlink exists with a 
# predetermined naming convention based on the SERIAL property of the device
#
# udev handles the symlink cleanup when the device is removed this is completed
# by the external script /usr/local/bin/linkdev_rm
#
###############################################################################

### XBOX Dragon USB Adapter
# Nubytech 3in1 Arcade Controller
# REQUIRES: None
# SERIAL: 0c12_0005 
# LSUSB: Bus 002 Device 072: ID 0c12:8801 Zeroplus Xbox Controller
ACTION=="add", SUBSYSTEM=="input", ATTRS{idVendor}=="0c12", GROUP="input", MODE="0660"
ACTION=="remove", SUBSYSTEM=="input", ATTRS{Serial}=="0c12_0005", RUN+="/usr/local/sbin/linkdev_rm arcade"
# Mat Catz MC2 Wheel
# REQUIRES: None
# SERIAL: 0738_4520
# LSUSB: Bus 002 Device 035: ID 0738:4520 Mad Catz, Inc. XBox Device
ACTION=="add", SUBSYSTEM=="input", ATTRS{idVendor}=="0738", GROUP="input", MODE="0660"
ACTION=="remove", SUBSYSTEM=="input", ATTRS{Serial}=="0738_4520", RUN+="/usr/local/sbin/linkdev_rm xmc2wheel"


### STEAM Controller
ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="28de", GROUP="input", MODE="0660"
KERNEL=="hidraw*", ATTRS{idVendor}=="28de", GROUP="input", MODE="0660"
# STEAM Joystick Emulation
KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"
# STEAM Controller x360 emulation
ACTION=="remove", SUBSYSTEM=="input", ATTRS{Serial}=="noserial", RUN+="/usr/local/sbin/linkdev_rm steam360"


### TWINUSB
# REQUIRES: external script for symlinks
# SERIAL: 0810_Twin_USB_Joystick
# LSUSB: Bus 002 Device 005: ID 0810:0001 Personal Communication Systems, Inc. Dual PSX Adaptor
ACTION=="add", SUBSYSTEM=="input", ATTRS{idVendor}=="0810", GROUP="input", MODE="0660"
ACTION=="remove", SUBSYSTEM=="input", ATTRS{idVendor}=="0810", RUN+="/usr/local/sbin/linkdev_rm psx"


### SNES 2p Adapter
# REQUIRES: external script for symlinks
# SERIAL: HuiJia_USB_GamePad
# LSUSB: Bus 002 Device 015: ID 0e8f:3013 GreenAsia Inc.
ACTION=="add", SUBSYSTEM=="input", ATTRS{idVendor}=="0e8f", GROUP="input", MODE="0660"
ACTION=="remove", SUBSYSTEM=="input", ATTRS{idVendor}=="0e8f", RUN+="/usr/local/sbin/linkdev_rm snes"


### Gamecube 4p Adapter
# REQUIRES: external script for symlinks
# SERIAL: ShanWan_Hyperkin_4_Port_Adapter
# LSUSB: Bus 002 Device 014: ID 0079:1843 DragonRise Inc. 
ACTION=="add", SUBSYSTEM=="input", ATTRS{idVendor}=="0079", GROUP="input", MODE="0660"
ACTION=="remove", SUBSYSTEM=="input", ATTRS{idVendor}=="0079", RUN+="/usr/local/sbin/linkdev_rm gc"

}}}


#3 | systemd management
#==============================================================================
/etc/systemd/system/controllers.service

{{{

  [Unit]
  Description=Controllers
  Wants= systemd-udev-trigger.service systemd-udevd.service
  After=systemd-user-sessions.service

  [Service]
  Type=forking
  ExecStart=/usr/local/sbin/linkdev_js
  StartLimitIntervalSec=1
  StartLimitBurst=2

  [Install]
  WantedBy=multi-user.target


}}}

/etc/systemd/system/controllers.path

{{{

  [Unit]
  Description=Monitor input changes

  [Path]
  PathChanged=/dev/input
  Unit=controllers.service

  [Install]
  WantedBy=multi-user.target

}}}


#4 | bash scripts
#==============================================================================a

/usr/local/sbin/linkdev_rm

Very basic script to remove the links from /dev/input used as a RUN action by
udev remove action

{{{

  #!/bin/bash

  /bin/rm /dev/input/$1p*

}}}


/usr/local/sbin/linkdev_js

Loop through the js devices and match based on the serial property, from there
see if the symlink exists for up to n players and if not create it.

This provides a static name based on the lowest index (p1) to the hightes (pn)

{{{

  #!/bin/bash
  #
  #
  # TODO
  # - check that symlink is correct


  function _log() {
    echo "$(date +'%Y-%m-%d %H:%M:%S') $*" >> /tmp/linkdev_js.log
  }

  ### 
  #
  # Create New nodes
  #
  function create() {
    local SOURCE=$2
    for p in $(seq 1 $3)
    do  
      local TARGET=/dev/input/$1p$p 

      _log "[TARGET]: $TARGET"
      _log "[SOURCE]: $SOURCE"

      if [ ! -L $TARGET ]
      then
        _log "[CREATE]: $SOURCE -> $TARGET"
        ln -s $SOURCE $TARGET
        return
      else
        _log "[EXISTS]: $TARGET"
      fi  

    done
  }


  for JS in $(ls /dev/input/js*)
  do
    DEV=$(udevadm info $JS | awk -F'=' /ID_SERIAL/{'print $2'})

    case $DEV in
      ShanWan_Hyperkin_4_Port_Adapter)
        create gc $JS 4
        ;;
      0810_Twin_USB_Joystick)
        create psx $JS 2
        ;;
      HuiJia_USB_GamePad)
        create snes $JS 2
        ;;
      *)
        echo "USAGE"
        ;;
    esac

  done

}}}


- sairuk
| mameau.com