Fixing Hotplug for the ATI Remote Wonder II on X11

EDIT: It turns out hotplug wasn’t the problem. Something else is resetting the X11 keymap without it involving the device getting reset… I’m done with this nonsense. I’ll temporarily set a user cronjob to force it back to what it’s supposed to be and then, when I have a moment, replace xbindkeys with a udev rule to loosen the security on the ATi Remote Wonder’s evdev node (it’s not as if I’ll ever be entering sensitive data using it) and a custom quick-and-dirty runs-in-my-X11-session daemon to listen to the raw evdev input events and react to them. (Sort of a knock-off LIRC for something the kernel driver exposes as a keyboard and mouse.)

EDIT (The following day): Instead of going through all this mess, just bypass the nonsense. Install xremap, set a udev rule like SUBSYSTEMS=="usb", ATTRS{idVendor}=="0471", ATTRS{idProduct}=="0602", MODE="0664", create your mappings somewhere like ~/.config/xremap.yml, and then add an autostart entry for your desktop that says sh -c "exec xremap ~/.config/xremap.yml --watch=device --device 'ATI Remote Wonder II'". That way, you’re binding directly against the evdev symbols and you have the ability to keybind to things like “A, but only from the remote, not the keyboard”, which is a big hassle otherwise. Its support for application-specific keybinds even has support for Wayland if you’re using either GNOME Shell, KWin, or something wlroots-based as your compositor.

If you’ve got an ATi Remote Wonder II and you’re using an X11-based desktop, you might have noticed that it’s a bit of a hassle to get the mappings to stick for the buttons with keycodes outside the 0-255 range that X11 supports.

First, you need to remap them into the range the X11 core protocol supports, which isn’t that difficult if you’re familiar with writing udev rules

# /etc/udev/rules.d/99-ati-remote-wonder-ii.rules
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0471", ATTRS{idProduct}=="0602", RUN+="/usr/bin/setkeycodes 0x005C 28 0x015C 28 0x025C 28 0x035C 28 0x045C 28 0x0020 192 0x0120 192 0x0220 192 0x0320 192 0x0420 192 0x0021 19
3 0x0121 193 0x0221 193 0x0321 193 0x0421 193"

…but the range from 0-255 makes it kind of difficult to find keys that both have a default X11 keysym mapping and don’t have some kind of predefined behaviour attached to them, such as altering the volume.

With Kubuntu Linux 20.04 LTS, the above line was enough but, when I upgraded to 22.04 LTS, something broke my mapping for a “trigger the talking clock from bed and log the time I asked” button.

KDE lacks the ability to do what I want through the GUI, custom keymaps are practically undocumented and apparently require editing stuff under /usr to boot, and I couldn’t just run xmodmap once a minute on a cronjob, since that exacerbated a reset bug that shows up with X11+my USB-PS2 adapter+my pre-2013-layout Unicomp keyboard, so that left me with only one option: Figuring out the “in an X11 session” equivalent to udev‘s RUN+= construct.

Enter inputplug. It’s included in the Ubuntu repos and it’s a daemon which will run a script every time the XInput hierarchy changes.

Here’s the little script I wrote (based on the usual boilerplate template I start all my Python scripts from), to fire off an xmodmap every time some kind of USB hiccup resets the ATi Remote Wonder II’s key mapping:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Helper to apply xmodmap settings every time USB connectivity for the
ATi Remote Wonder II hiccups and resets them.
"""

__author__ = "Stephan Sokolow (deitarion/SSokolow)"
__appname__ = "inputplug command for ATi Remote Wonder II"
__version__ = "0.1"
__license__ = "MIT"

import logging, os, subprocess, time
log = logging.getLogger(__name__)


def main():
    """The main entry point, compatible with setuptools entry points."""
    from argparse import ArgumentParser, RawDescriptionHelpFormatter
    parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
      description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0])
    parser.add_argument('-V', '--version', action='version',
      version="%%(prog)s v%s" % __version__)
    parser.add_argument('-v', '--verbose', action="count",
      default=2, help="Increase the verbosity. Use twice for extra effect.")
    parser.add_argument('-q', '--quiet', action="count",
      default=0, help="Decrease the verbosity. Use twice for extra effect.")
    parser.add_argument('event_type', action="store")
    parser.add_argument('device_id', action="store")
    parser.add_argument('device_type', action="store")
    parser.add_argument('device_name', action="store")

    args = parser.parse_args()

    # Set up clean logging to stderr
    log_levels = [logging.CRITICAL, logging.ERROR, logging.WARNING,
                  logging.INFO, logging.DEBUG]
    args.verbose = min(args.verbose - args.quiet, len(log_levels) - 1)
    args.verbose = max(args.verbose, 0)
    logging.basicConfig(level=logging.DEBUG,
                format='%(levelname)s: %(message)s')

    if args.device_type != 'XISlaveKeyboard':
        log.debug("Skipped event from device (type != XISlaveKeyboard): %s",
            args.device_name)
        return
    if args.device_name != 'ATI Remote Wonder II':
        log.debug("Skipped event from device (name != ATI Remote Wonder II):"
            " %s", args.device_name)
        return
    log.info("Calling xmodmap after 1-second delay")
    time.sleep(1)
    subprocess.check_call(['xmodmap', os.path.expanduser('~/.xmodmaprc')])


if __name__ == '__main__':  # pragma: nocover
    main()

# vim: set sw=4 sts=4 expandtab :

Yes, the time.sleep(1) is a hack, but I just don’t have time to be that correct about it. You run it by putting it at ~/bin/set_xmodmap_for_remote.py, chmod-ing it executable, and launching inputplug -0 -c ~/bin/set_xmodmap_for_remote.py.

(The -0 makes inputplug act as if all input devices were hot-plugged after it launched, so you don’t need to pull and plug the remote’s receiver on boot to get the initial xmodmap.)

Just run that on login through whatever approach your desktop prefers and you should be good.

I’m partial to putting this into ~/.config/autostart/inputplug.desktop rather than puttering around with the GUIs:

[Desktop Entry]
Version=1.0
Type=Application
Name=inputplug
GenericName=XInput Hierarchy Change Handler
Comment=Restore xmodmap changes after ATi Remote Wonder hotplug
Exec=sh -c "exec inputplug -0 -c ~/bin/set_xmodmap_for_remote.py"
StartupNotify=true
Terminal=false

(The sh -c "exec ..." hack is needed because .desktop files don’t support ~ or $HOME otherwise and I don’t want to hard-code the path to my home dir.)

I have noticed a weird thing in testing where the first input event from the remote after a hotplug isn’t subject to the changed settings, no matter how long I wait before trying, but I’m not sure how to diagnose it so we’ll just stop here for now.)

CC BY-SA 4.0 Fixing Hotplug for the ATI Remote Wonder II on X11 by Stephan Sokolow is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

This entry was posted in Lair Improvement. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

By submitting a comment here you grant this site a perpetual license to reproduce your words and name/web site in attribution under the same terms as the associated post.

All comments are moderated. If your comment is generic enough to apply to any post, it will be assumed to be spam. Borderline comments will have their URL field erased before being approved.