Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event Device Support (evdev) #1943

Merged
merged 26 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5c1cb16
Add event device plugin
votti Dec 30, 2022
87e8aa1
Adds documentation for the event device plugin
votti Jan 2, 2023
f1caebf
Allow empty button config
votti Jan 2, 2023
0408775
Update README.rst
pabera Jan 4, 2023
5243583
Improve README
votti Jan 7, 2023
a93104b
Give more meaningfull name to listener thread variable
votti Jan 7, 2023
23b107c
Update to markdown docs and merge develop
Dec 29, 2023
39e69f9
Link to old documentation
Dec 29, 2023
49839ad
Remove README and fix typo
Dec 29, 2023
68ee2e0
Remove unecessary plain=1
Dec 31, 2023
3bfa01c
Remove indents in example code
Jan 17, 2024
38b4cd4
Add example how to access evdev in docker
Jan 17, 2024
2f36718
Example for new Evdev config structure
Jan 18, 2024
90de6a4
Move evdev config to separate file
Jan 18, 2024
bd00f05
Adapt evdev code to new config
Jan 18, 2024
62171ef
Update documentation for new evdev config format
Jan 18, 2024
d77a69b
Fix wrong reference
Jan 18, 2024
4274e92
Fix bug to correctly check supported input device type
Jan 19, 2024
a3b328b
Add more tests for evdev init
Jan 19, 2024
3773556
Fix wrong attribute name
Jan 19, 2024
2df6adf
Validate input config better
Jan 19, 2024
75161b5
Add pyzmq installation for github action
Jan 19, 2024
51d9915
Revert "Add pyzmq installation for github action"
Jan 19, 2024
a181f8c
Mock `jukebox.publishing` in tests to avoid zmq
Jan 19, 2024
bc6610c
Merge branch 'future3/develop' into future3/evdev_plugin
votti Jan 29, 2024
f807152
Merge branch 'future3/develop' into future3/evdev_plugin
s-martin Feb 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/builders/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* [Soundcards](./components/soundcards/)
* [HiFiBerry Boards](./components/soundcards/hifiberry.md)
* [RFID Readers](./../developers/rfid/README.md)
* [Event devices (USB and other buttons)](./event-devices.md)

## Web Application

Expand Down
120 changes: 120 additions & 0 deletions documentation/builders/event-devices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Event devices

## Background
Event devices are generic input devices that are exposed in `/dev/input`.
This includes USB peripherals (Keyboards, Controllers, Joysticks or Mouse) as well as potentially bluetooth devices.

A specific usecase for this could be, if a Zero Delay Arcade USB Encoder is used to wire arcade buttons instead of using GPIO pins.

votti marked this conversation as resolved.
Show resolved Hide resolved
These device interface support various kinds of input events, such as press, movements and potentially also outputs (eg. rumble, led lights, ...). Currently only the usage of button presses as input is supported.

This functionality was previously implemented under the name of [USB buttons](https://github.com/MiczFlor/RPi-Jukebox-RFID/blob/develop/components/controls/buttons_usb_encoder/README.md).

The devices and their button mappings need to be mapped in the configuration file.

## Configuration

To configure event devices, first add the plugin as an entry to the module list of your main configuration file ``shared/settings/jukebox.yaml``:

``` yaml
modules:
named:
event_devices: controls.event_devices
```

And add the following section with the plugin specific configuration:
``` yaml
evdev:
enabled: true
config_file: ../../shared/settings/evdev.yaml
```

The actual configuration itself is stored in a separate file. In this case in ``../../shared/settings/evdev.yaml``.

The configuration is structured akin to the configuration of the [GPIO devices](./gpio.md).

In contrast to `gpio`, multiple devices (eg arcade controllser, keyboards, joysticks, mice, ...) are supported, each with their own `input_devices` (=buttons). `output_devices` or actions other than `on_press` are currently not yet supported.

``` yaml
devices: # list of devices to listen for
{device nickname}: # config for a specific device
device_name: {device_name} # name of the device
exact: False/True # optional to require exact match. Otherwise it is sufficient that a part of the name matches
input_devices: # list of buttons to listen for for this device
{button nickname}:
type: Button
kwargs:
key_code: {key-code}: # evdev event id
actions:
on_press: # Currently only the on_press action is supported
{rpc_command_definition} # eg `alias: toggle`
```
The `{device nickname}` is only for your own orientation and can be choosen freely.
For each device you need to figure out the `{device_name}` and the `{event_id}` corresponding to key strokes, as indicated in the sections below.

### Identifying the `{device_name}`

The `{device_name}` can be identified using the following Python snippet:

``` Python
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the spirit of run_*.py files, would it make sense to provide this code as a configurator? Or at least as a runable script so users don't have to write the code themselves?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me this is a 'nice to have' and something to add as an issue for a future pull request once this is merged.

import evdev
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
for device in devices:
print(device.path, device.name, device.phys)
```

The output could be in the style of:

```
/dev/input/event1 Dell Dell USB Keyboard usb-0000:00:12.1-2/input0
/dev/input/event0 Dell USB Optical Mouse usb-0000:00:12.0-2/input0
```

In this example, the `{device_name}` could be `DELL USB Optical Mouse`.
Note that if you use the option `exact: False`, it would be sufficient to add a substring such as `USB Keyboard`.

### Identifying the `{key-code}`

The key code for a button press can be determined using the following code snippet:

``` Python
import evdev
device = evdev.InputDevice('/dev/input/event0')
device.capabilities(verbose=True)[('EV_KEY', evdev.ecodes.EV_KEY)]
```

With the `InputDevice` corresponding to the path from the output of the section `{device_name}` (eg. in the example `/dev/input/event0`
would correspond to `Dell Dell USB Keyboard`).

If the naming is not clear, it is also possible to empirically check for the key code by listening for events:

``` Python
from evdev import InputDevice, categorize, ecodes
dev = InputDevice('/dev/input/event1')
print(dev)
for event in dev.read_loop():
if event.type == ecodes.EV_KEY:
print(categorize(event))
```
The output could be of the form:
```
device /dev/input/event1, name "DragonRise Inc. Generic USB Joystick ", phys "usb-3f980000.usb-1.2/input0"
key event at 1672569673.124168, 297 (BTN_BASE4), down
key event at 1672569673.385170, 297 (BTN_BASE4), up
```

In this example output, the `{key-code}` would be `297`

Alternatively, the device could also be setup without a mapping.
Afterwards, when pressing keys, the key codes can be found in the log files. Press various buttons on your device,
while watching the logs with `tail -f shared/logs/app.log`.
Look for entries like `No callback registered for button ...`.

### Specifying the `{rpc_command_definition}`

The RPC command follows the regular RPC command rules as defined in the [following documentation](./rpc-commands.md).


## Full example config

A complete configuration example for a USB Joystick controller can be found in the [examples](../../resources/default-settings/evdev.example.yaml).
14 changes: 14 additions & 0 deletions documentation/developers/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,20 @@ $ docker run -it --rm \
--name jukebox jukebox
```

## Testing EVDEV devices in Linux
To test the [event device capabilities](../builders/event-devices.md) in docker, the device needs to be made available to the container.

### Linux
Mount the device into the container by configuring the appropriate device in a `devices` section of the `jukebox` service in the docker compose file. For example:

```yaml
jukebox:
...
devices:
- /dev/input/event3:/dev/input/event3
```


### Resources

#### Mac
Expand Down
59 changes: 59 additions & 0 deletions resources/default-settings/evdev.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
devices: # A list of evdev devices each containing one or multiple input/output devices
joystick: # A nickname for a device
device_name: DragonRise Inc. Generic USB # Device name
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just a pretty name or actually relevant to access/call the device?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'device_name' is relevant as it is used to get the device number (in contrast to the nickname line 2 which is cosmetic).

The docs contain a section how to look it up: https://github.com/MiczFlor/RPi-Jukebox-RFID/blob/49839adb01019cb296d67dc29c06cd9a6af0bc62/documentation/builders/event-devices.md#identifying-the-device_name

exact: false # If true, the device name must match exactly, otherwise it is sufficient to contain the name
input_devices:
TogglePlayback:
type: Button
kwargs:
key_code: 299
actions:
on_press:
alias: toggle
NextSong:
type: Button
kwargs:
key_code: 298
actions:
on_press:
alias: next_song
PrevSong:
type: Button
kwargs:
key_code: 297
actions:
on_press:
alias: prev_song
VolumeUp:
type: Button
kwargs:
key_code: 296
actions:
on_press:
alias: change_volume
args: 5
VolumeDown:
type: Button
kwargs:
key_code: 295
actions:
on_press:
alias: change_volume
args: -5
VolumeReset:
type: Button
kwargs:
key_code: 291
actions:
on_press:
package: volume
plugin: ctrl
method: set_volume
args: [18]
Shutdown:
type: Button
kwargs:
key_code: 292
actions:
on_press:
alias: shutdown
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def activate(device_name: str, exact: bool = True, open_initial_delay: float = 0
# Do a bit of housekeeping: Delete dead threads
listener = list(filter(lambda x: x.is_alive(), listener))
# Check that there is no running thread for this device already
for ll in listener:
if ll.device_request == device_name and ll.is_alive():
for thread in listener:
if thread.device_request == device_name and thread.is_alive():
logger.debug(f"Button listener thread already active for '{device_name}'")
return

Expand Down
Loading
Loading