kivy / kivy

Open source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS
https://kivy.org
MIT License
17.56k stars 3.06k forks source link

USB joystick in kivy fails on process_as_mouse_or_keyboard #5891

Open joshuacox opened 6 years ago

joshuacox commented 6 years ago

Versions

Description

I was attempting to use a DragonRise USB controller to add many buttons and a joystick to an embedded raspberrypi project.

Code and Logs

# Joystick / Gamepad example
# STOP_FIRE from https://wiki.libsdl.org/SDL_JoyAxisEvent

from kivy.app import App
from kivy.clock import Clock
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.properties import ObjectProperty, ListProperty

class Listener(Widget):
    # fire / trigger axis
    FIRE = (2, 5)
    STOP_FIRE = -32767

    # min value for user to actually trigger axis
    OFFSET = 15000

    # current values + event instance
    VALUES = ListProperty([])
    HOLD = ObjectProperty(None)

    def __init__(self, **kwargs):
        super(Listener, self).__init__(**kwargs)

        # get joystick events first
        Window.bind(on_joy_hat=self.on_joy_hat)
        Window.bind(on_joy_ball=self.on_joy_ball)
        Window.bind(on_joy_axis=self.on_joy_axis)
        Window.bind(on_joy_button_up=self.on_joy_button_up)
        Window.bind(on_joy_button_down=self.on_joy_button_down)

    # show values in console
    def print_values(self, *args):
        print(self.VALUES)

    def joy_motion(self, event, id, axis, value):
        # HAT first, returns max values
        if isinstance(value, tuple):
            if not value[0] and not value[1]:
                Clock.unschedule(self.HOLD)
            else:
                self.VALUES = [event, id, axis, value]
                self.HOLD = Clock.schedule_interval(self.print_values, 0)
            return

        # unschedule if at zero or at minimum (FIRE)
        if axis in self.FIRE and value < self.STOP_FIRE:
            Clock.unschedule(self.HOLD)
            return
        elif abs(value) < self.OFFSET or self.HOLD:
            Clock.unschedule(self.HOLD)

        # schedule if over OFFSET (to prevent accidental event with low value)
        if (axis in self.FIRE and value > self.STOP_FIRE or
                axis not in self.FIRE and abs(value) >= self.OFFSET):
            self.VALUES = [event, id, axis, value]
            self.HOLD = Clock.schedule_interval(self.print_values, 0)

    # replace window instance with identifier
    def on_joy_axis(self, win, stickid, axisid, value):
        pass
        #self.joy_motion('axis', stickid, axisid, value)

    def on_joy_ball(self, win, stickid, ballid, xvalue, yvalue):
        pass
        #self.joy_motion('ball', stickid, ballid, (xvalue, yvalue))

    def on_joy_hat(self, win, stickid, hatid, value):
        pass
        #self.joy_motion('hat', stickid, hatid, value)

    def on_joy_button_down(self, win, stickid, buttonid):
        print('button_down', stickid, buttonid)

    def on_joy_button_up(self, win, stickid, buttonid):
        print('button_up', stickid, buttonid)

class JoystickApp(App):
    def build(self):
        return Listener()

if __name__ == '__main__':
    JoystickApp().run()

output:

[INFO   ] [HIDMotionEvent] using <E-Signal/A-One USB Gaming Mouse>
[INFO   ] [HIDMotionEvent] using <SONiX USB DEVICE>
[INFO   ] [HIDMotionEvent] using <DragonRise Inc.   Generic   USB  Joystick  >
[INFO   ] [HIDMotionEvent] <DragonRise Inc.   Generic   USB  Joystick  > range ABS X position is 0 - 255
[INFO   ] [HIDMotionEvent] <DragonRise Inc.   Generic   USB  Joystick  > range ABS Y position is 0 - 255
[INFO   ] [HIDMotionEvent] using <E-Signal/A-One USB Gaming Mouse>
[INFO   ] [HIDMotionEvent] <SONiX USB DEVICE> range position X is 0 - 255
[INFO   ] [HIDMotionEvent] <SONiX USB DEVICE> range position Y is 0 - 255
[INFO   ] [HIDMotionEvent] <SONiX USB DEVICE> range pressure is 0 - 255
[INFO   ] [HIDMotionEvent] using <SONiX USB DEVICE>
 Exception in thread Thread-3:
 Traceback (most recent call last):
   File "/usr/lib/python2.7/threading.py", line 801, in __bootstrap_inner
     self.run()
   File "/usr/lib/python2.7/threading.py", line 754, in run
     self.__target(*self.__args, **self.__kwargs)
   File "/home/pi/kivy/kivy/input/providers/hidinput.py", line 700, in _thread_run
     process_as_mouse_or_keyboard(*infos)
   File "/home/pi/kivy/kivy/input/providers/hidinput.py", line 551, in process_as_mouse_or_keyboard
     z = keyboard_keys[ev_code][-1
 KeyError: 290

I have tried this on kivey 1.9, 1.10, and 1.11 on ubuntu, raspbian and arch linux, and on both x86_64 and armhf architectures with the dragonrise controller as well as some generic game controllers as well (all work under jstest)

stackoverflow where I asked about this bug first

KeyWeeUsr commented 6 years ago

I'm not quite sure why a joystick is detected as mouse/keyboard. Perhaps try to disable hidinput and see if SDL detects the device? The joystick is handled in SDL window anyway.

joshuacox commented 6 years ago

What's the best method to disable hidinput?

I see the provider code here: https://kivy.org/docs/_modules/kivy/input/providers/hidinput.html

and in the config there is this section:


[input]
mouse = mouse
%(name)s = probesysfs,provider=hidinput

should I perhaps just change the provider there? I tried removing the line entirely but now I get no output at all (joystick does not seem to respond). Is there a way for me to specify which device? like /dev/input/js0?

tshirtman commented 6 years ago

you might be interested in this documentation: https://kivy.org/docs/api-kivy.input.providers.probesysfs.html

joshuacox commented 6 years ago

@tshirtman that is indeed interesting, yet I can't help feeling that I'm hacking on a problem that is probably already solved, in pygame this is indeed pretty easy to test: https://gist.github.com/joshuacox/fa079f4e91102c8349aef0f544903dae

and while running the default code I do get lot's of spam from the joystick axis: https://github.com/kivy/kivy/blob/master/examples/miscellaneous/joystick.py

but commenting out that code and trying to listen to the buttons I get nothing.