Allow all buttons and axes on virtual devices to be specified by either number or an explicit list. #5

Merged
anna merged 8 commits from device-config into main 2025-07-17 20:04:21 +00:00
11 changed files with 1114 additions and 130 deletions

View file

@ -68,9 +68,12 @@ func main() {
// initialize the mode variable
mode := config.GetModes()[0]
logger.Logf("Initial mode set to '%s'", mode)
fmt.Println("Joyful Running! Press Ctrl+C to quit.")
fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.")
if len(config.GetModes()) > 1 {
logger.Logf("Initial mode set to '%s'", mode)
}
for {
// Get an event (blocks if necessary)
channelEvent := <-eventChannel

View file

@ -1,56 +1,218 @@
rules:
- type: simple
### Rotational Controls
- type: axis
name: pitch
input:
device: right-stick
axis: ABS_X
axis: Y
deadzone_start: 29000
deadzone_end: 31000
output:
device: main
axis: ABS_X
device: primary
axis: Y
- type: simple
- type: axis
name: roll (turret yaw)
input:
device: right-stick
axis: ABS_Y
axis: X
deadzone_start: 30000
deadzone_end: 32000
output:
device: main
axis: ABS_Y
device: primary
axis: X
- type: simple
input:
device: right-stick
axis: ABS_THROTTLE
output:
device: main
axis: ABS_THROTTLE
- type: simple
input:
device: left-stick
axis: ABS_X
output:
device: main
axis: ABS_RX
- type: simple
input:
device: left-stick
axis: ABS_Y
output:
device: main
axis: ABS_RY
- type: simple
input:
device: left-stick
axis: ABS_THROTTLE
output:
device: main
axis: ABS_RUDDER
- type: simple
- type: axis
name: yaw
input:
device: pedals
axis: ABS_Z
axis: Z
deadzone_start: 124
deadzone_end: 132
output:
device: main
axis: ABS_Z
device: primary
axis: Z
### Translation Controls
- type: axis
name: throttle
input:
device: left-stick
axis: Y
deadzone_start: 29500
deadzone_end: 31000
output:
device: primary
axis: RY
- type: axis
name: translation lateral
input:
device: left-stick
axis: X
deadzone_start: 29000
deadzone_end: 30500
output:
device: primary
axis: RX
### Freelook controls
- type: axis
name: Freelook X
input:
device: right-stick
axis: RX
deadzone_start: 29500
deadzone_end: 30250
output:
device: secondary
axis: X
- type: axis
name: Freelook Y
input:
device: right-stick
axis: RY
deadzone_start: 29500
deadzone_end: 30250
output:
device: secondary
axis: Y
# Vertical thrust is on the VPC "paddles" in the main flight mode
- type: axis
name: translation up
modes:
- main
input:
device: right-stick
axis: ABS_THROTTLE
deadzone_start: 0
deadzone_end: 500
output:
device: primary
axis: ABS_THROTTLE
- type: axis
name: translation down
modes:
- main
input:
device: left-stick
axis: ABS_THROTTLE
deadzone_start: 0
deadzone_end: 500
output:
device: primary
axis: ABS_RUDDER
# By default, the left thumbstick controls tractor beam via mousewheel
- type: axis-to-relaxis
name: tractor in
modes:
- main
repeat_rate_max: 10
repeat_rate_min: 100
increment: -1
input:
device: left-stick
axis: RY
deadzone_start: 0
deadzone_end: 30500
output:
device: mouse
axis: REL_WHEEL
- type: axis-to-relaxis
name: tractor out
modes:
- main
repeat_rate_max: 10
repeat_rate_min: 100
increment: 1
input:
device: left-stick
axis: RY
deadzone_start: 29500
deadzone_end: 64000
inverted: true
output:
device: mouse
axis: REL_WHEEL
# In Mining mode, we move vertical thrust to the left thumbstick
# and remap the right paddle to be mining laser power
- type: axis
name: translation up alternate
modes:
- mining
input:
device: left-stick
axis: RY
deadzone_start: 29250
deadzone_end: 64000
output:
device: primary
axis: ABS_THROTTLE
- type: axis
name: translation down alternate
modes:
- mining
input:
device: left-stick
axis: RY
deadzone_start: 0
deadzone_end: 30500
output:
device: primary
axis: ABS_RUDDER
- type: axis
name: mining laser
modes:
- mining
input:
device: right-stick
axis: ABS_THROTTLE
deadzone_start: 0
deadzone_end: 500
output:
device: primary
axis: RZ
# In tractor mode, most flight controls are disabled to prevent us
# from accidentally trying to fly off without throttle control
# - type: axis-to-relaxis
# name: tractor in
# modes:
# - tractor
# repeat_rate_max: 10
# repeat_rate_min: 100
# increment: -1
# input:
# device: left-stick
# axis: Y
# deadzone_start: 0
# deadzone_end: 30250
# output:
# device: mouse
# axis: REL_WHEEL
# - type: axis-to-relaxis
# name: tractor out
# modes:
# - tractor
# repeat_rate_max: 10
# repeat_rate_min: 100
# increment: 1
# input:
# device: left-stick
# axis: Y
# deadzone_start: 29500
# deadzone_end: 64000
# inverted: true
# output:
# device: mouse
# axis: REL_WHEEL

View file

@ -1,21 +1,706 @@
rules:
- name: Trigger
type: combo
# Special Rules
- name: Right Trigger
type: button-combo
inputs:
- device: right-stick
button: BTN_THUMB
- device: right-stick
button: BTN_THUMB2
output:
device: main
device: primary
button: 0
- name: Left Trigger
type: button-combo
inputs:
- device: left-stick
button: BTN_THUMB
- device: left-stick
button: BTN_THUMB2
output:
device: primary
button: 1
- name: ButtonBoxModeShift
type: mode-select
input:
device: button-box
button: BTN_BASE2
output:
modes:
- main
- mining
# Right Stick
- name: R Red Button
type: button
input:
device: right-stick
button: BTN_BASE6
output:
device: primary
button: 2
- name: R Black Button
type: button
input:
device: right-stick
button: BTN_PINKIE
output:
device: primary
button: 3
- name: R Pinkie Button
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY14
output:
device: primary
button: 4
- name: R Side Button
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY2
output:
device: primary
button: 5
- name: R Side Up
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY3
output:
device: primary
button: 6
- name: R Side Down
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY4
output:
device: primary
button: 7
- name: R High Hat
type: button
input:
device: right-stick
button: "0x12c"
output:
device: primary
button: 8
- name: R High Hat Up
type: button
input:
device: right-stick
button: "0x12d"
output:
device: primary
button: 9
- name: R High Hat Right
type: button
input:
device: right-stick
button: "0x12e"
output:
device: primary
button: 10
- name: R High Hat Down
type: button
input:
device: right-stick
button: BTN_DEAD
output:
device: primary
button: 11
- name: R High Hat Left
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY1
output:
device: primary
button: 12
- name: R Low Hat
type: button
input:
device: right-stick
button: BTN_BASE
output:
device: primary
button: 13
- name: R Low Hat Up
type: button
input:
device: right-stick
button: BTN_BASE2
output:
device: primary
button: 14
- name: R Low Hat Right
type: button
input:
device: right-stick
button: BTN_BASE3
output:
device: primary
button: 15
- name: R Low Hat Down
type: button
input:
device: right-stick
button: BTN_BASE4
output:
device: primary
button: 16
- name: R Low Hat Left
type: button
input:
device: right-stick
button: BTN_BASE5
output:
device: primary
button: 17
- name: R Thumb Hat
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY9
output:
device: primary
button: 18
- name: R Thumb Hat Up
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY10
output:
device: primary
button: 19
- name: R Thumb Hat Right
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY11
output:
device: primary
button: 20
- name: R Thumb Hat Down
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY12
output:
device: primary
button: 21
- name: R Thumb Hat Left
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY13
output:
device: primary
button: 22
- name: R Wheel
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY5
output:
device: primary
button: 23
- name: R Wheel Stage2
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY6
output:
device: primary
button: 24
- name: R Wheel Down
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY7
output:
device: primary
button: 25
- name: R Wheel Up
type: button
input:
device: right-stick
button: BTN_TRIGGER_HAPPY8
output:
device: primary
button: 26
- name: R Thumbstick
type: button
input:
device: right-stick
button: BTN_TOP2
output:
device: primary
button: 27
# Left Stick
- name: L Red Button
type: button
input:
device: left-stick
button: BTN_BASE6
output:
device: primary
button: 28
- name: L Black Button
type: button
input:
device: left-stick
button: BTN_PINKIE
output:
device: primary
button: 29
- name: L Pinkie Button
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY14
output:
device: primary
button: 30
- name: L Side Button
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY2
output:
device: primary
button: 31
- name: L Side Up
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY3
output:
device: primary
button: 32
- name: L Side Down
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY4
output:
device: primary
button: 33
- name: L High Hat
type: button
input:
device: left-stick
button: "0x12c"
output:
device: primary
button: 34
- name: L High Hat Up
type: button
input:
device: left-stick
button: "0x12d"
output:
device: primary
button: 35
- name: L High Hat Right
type: button
input:
device: left-stick
button: "0x12e"
output:
device: primary
button: 36
- name: L High Hat Down
type: button
input:
device: left-stick
button: BTN_DEAD
output:
device: primary
button: 37
- name: L High Hat Left
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY1
output:
device: primary
button: 38
- name: L Low Hat
type: button
input:
device: left-stick
button: BTN_BASE
output:
device: primary
button: 39
- name: L Low Hat Up
type: button
input:
device: left-stick
button: BTN_BASE2
output:
device: primary
button: 40
- name: L Low Hat Right
type: button
input:
device: left-stick
button: BTN_BASE3
output:
device: primary
button: 41
- name: L Low Hat Down
type: button
input:
device: left-stick
button: BTN_BASE4
output:
device: primary
button: 42
- name: L Low Hat Left
type: button
input:
device: left-stick
button: BTN_BASE5
output:
device: primary
button: 43
- name: L Thumb Hat
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY9
output:
device: primary
button: 44
- name: L Thumb Hat Up
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY10
output:
device: primary
button: 45
- name: L Thumb Hat Right
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY11
output:
device: primary
button: 46
- name: L Thumb Hat Down
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY12
output:
device: primary
button: 47
- name: L Thumb Hat Left
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY13
output:
device: primary
button: 48
- name: L Wheel
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY5
output:
device: primary
button: 49
- name: L Wheel Stage2
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY6
output:
device: primary
button: 50
- name: L Wheel Down
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY7
output:
device: primary
button: 51
- name: L Wheel Up
type: button
input:
device: left-stick
button: BTN_TRIGGER_HAPPY8
output:
device: primary
button: 52
- name: L Thumbstick
type: button
input:
device: left-stick
button: BTN_TOP2
output:
device: primary
button: 53
# Button Box
- name: BB Power1 On
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY12
output:
device: primary
button: 54
- name: BB Power1 Off
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY13
output:
device: primary
button: 55
- name: BB Power2 On
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY10
output:
device: primary
button: 56
- name: BB Power2 Off
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY11
output:
device: primary
button: 57
- name: BB Power3 On
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY8
output:
device: primary
button: 58
- name: BB Power3 Off
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY9
output:
device: primary
button: 59
- name: BB Power4 On
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY6
output:
device: primary
button: 60
- name: BB Power4 Off
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY7
output:
device: primary
button: 61
- name: BB Top Row1
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY3
output:
device: primary
button: 62
- name: BB Top Row2
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY4
output:
device: primary
button: 63
- name: BB Top Row3
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY2
output:
device: primary
button: 64
- name: BB Side1
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY1
output:
device: primary
button: 65
- name: BB Side2
type: button
input:
device: button-box
button: "0x12e"
output:
device: primary
button: 66
- name: BB Side3
type: button
input:
device: button-box
button: "0x12d"
output:
device: primary
button: 67
- name: BB Toggle1
type: button
input:
device: button-box
button: BTN_BASE3
output:
device: primary
button: 68
- name: BB Toggle2 On
type: button
input:
device: button-box
button: BTN_BASE6
output:
device: primary
button: 69
- name: BB Toggle2 Off
type: button
input:
device: button-box
button: "0x12c"
output:
device: primary
button: 70
- name: BB Toggle3 On
type: button
input:
device: button-box
button: BTN_BASE4
output:
device: primary
button: 71
- name: BB Toggle3 Off
type: button
input:
device: button-box
button: BTN_BASE5
output:
device: primary
button: 72
- name: BB Middle Row1
type: button
input:
device: button-box
button: BTN_BASE
output:
device: secondary
button: 0
- name: BB Middle Row2
type: button
input:
device: button-box
button: BTN_PINKIE
output:
device: secondary
button: 1
- name: BB Middle Row3
type: button
input:
device: button-box
button: BTN_TOP2
output:
device: secondary
button: 2
- name: BB Bottom Row1
type: button
input:
device: button-box
button: BTN_DEAD
output:
device: secondary
button: 3
- name: BB Bottom Row2
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY5
output:
device: secondary
button: 4
- name: BB Bottom Row3
type: button
input:
device: button-box
button: BTN_TRIGGER_HAPPY14
output:
device: secondary
button: 5
- name: BB Dial1 Right
type: button
input:
device: button-box
button: BTN_THUMB2
output:
device: secondary
button: 6
- name: BB Dial1 Left
type: button
input:
device: button-box
button: BTN_TOP
output:
device: secondary
button: 7
- name: BB Dial2 Right
type: button
input:
device: button-box
button: BTN_TRIGGER
- name: Trigger2
type: combo
inputs:
- device: left-stick
button: BTN_THUMB
- device: left-stick
button: BTN_THUMB2
output:
device: main
device: secondary
button: 8
- name: BB Dial2 Left
type: button
input:
device: button-box
button: BTN_THUMB
output:
device: secondary
button: 9

View file

@ -1,8 +1,18 @@
devices:
- name: main
- name: primary
type: virtual
buttons: 56
axes: 8
num_buttons: 74
num_axes: 8
- name: secondary
type: virtual
num_buttons: 74
num_axes: 2
- name: mouse
type: virtual
num_buttons: 0
num_axes: 0
rel_axes:
- REL_WHEEL
- name: right-stick
type: physical
device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2

View file

@ -0,0 +1,3 @@
modes:
- main
- mining

View file

@ -1,7 +1,7 @@
## multi-file configuration example
This directory demonstrates how to split your configuration across multiple files.
Note that we re-define the top-level `rules` element; this is by design.
Note that we re-define the top-level `rules` element in two different files; this is by design.
It also serves as a real-world example demonstrating many of the available features of the system.
It is based on the author's actual mappings for Star Citizen.
It is copied from the author's actual mappings for Star Citizen.

View file

@ -5,16 +5,15 @@ devices:
device_name: Flightstick Name From evlist
- name: main
type: virtual
axes: 8
buttons: 80
num_axes: 8
num_buttons: 80
- name: mouse
type: virtual
axes: 0
buttons: 0
relative_axes:
- REL_WHEEL
rules:
# Straightforward axis mapping
- type: axis
input:
device: flightstick
@ -58,6 +57,7 @@ rules:
device: main
button: BTN_BASE3
# The specified axis will output button presses, and repeat them faster the more the axis is engaged.
- type: axis-to-button
# The repeat rates look backwards because they are the time between repeats in milliseconds.
# So this example will produce a button press every second at the axis' minimum value,
@ -73,10 +73,12 @@ rules:
device: main
button: BTN_BASE4
# The specified axis will output "relative" axis events, commonly used in mice. This example
# simulates a mouse scrollwheel, though only in one direction.
- type: axis-to-relaxis
repeat_rate_min: 100
repeat_rate_max: 10
# This is the value to write for the axis for each repetition. If you wanted to scroll the other
# This is the value to emit for the axis on each repetition. If you wanted to scroll the other
# direction, use a negative value. It is useful to use 2 rules on the same input axis with
# "overlapping" deadzones to scroll a mousewheel in both directions.
increment: 1

View file

@ -13,12 +13,16 @@ Each entry in `devices` must have a couple of parameters:
* `device_name` - The name of the device as reported by the included `evlist` command. If your device name ends with a space, use quotation marks (`""`) around the name.
`virtual` devices must additionally define these parameters:
`virtual` devices can additionally define these parameters:
* `buttons` - a number between 0 and 74. Linux may not recognize buttons greater than 56.
* `axes` - a number between 0 and 8.
* `buttons` or `num_buttons` - Either a list of explicit buttons or a number of buttons to create. (max 74 buttons) Linux-native games may not recognize all buttons created by Joyful.
* `axes` or `num_axes` - An explicit list of `ABS_` axes or a number to create.
* `relative_axes` or `num_relative_axes` - As above, but for `REL_` axes.
Virtual devices can also define a `relative_axes` parameter; this must be a list of `REL_` event keycodes, and can be useful for a simulated mouse device. Some environments will only register mouse events if the device *only* supports mouse-like events, so it can be useful to isolate your `relative_axes` to their own virtual device.
A couple of additional notes on virtual devices:
* For all 3 of the above options, an explicit list will override the `num_` parameters if both are present.
* Some environments will only register mouse events if the device *only* supports mouse-like events, so it can be useful to isolate your `relative_axes` to their own virtual device and explicitly define the axes.
### Rules configuration
@ -33,26 +37,24 @@ All `rules` must have a `type` parameter. Valid values for this parameter are:
Configuration options for each rule type vary. See <examples/ruletypes.yml> for an example of each type with all options specified.
### Keycodes
### Event Codes
Keycodes are the values that identify buttons and axes. There are several ways to configure keycodes. All of them are case-insensitive.
Event codes are the values that identify buttons and axes. There are several ways to configure these codes. All of them are case-insensitive, so `abs_x` and `ABS_X`.
Ways to specify keycodes are:
Ways to specify event codes are:
* Using evdev's Keycodes. This is the best way to be absolutely certain about which axis you're referencing. You can specify these keycodes in two forms:
* Using evdev's identifiers. This is the best way to be absolutely certain about which axis you're referencing. You can specify these in two forms:
* Using the code's identifier from <https://github.com/holoplot/go-evdev/blob/master/codes.go>. e.g., `ABS_X`, `REL_WHEEL`, `BTN_TRIGGER`.
* Alternately, you can omit the `ABS_` type prefix, and Joyful will automatically add it from context. So for a button input, you can simply specify `button: trigger` instead of `BTN_TRIGGER`.
* You can use the hexadecimal value of the keycode directly, via `"0x<hex value>"`. This can be useful if you want to force a specific numeric value that isn't represented by a Linux keycode directly. Note however that not all keycodes will work. Only the first 8 axes are available, and see <internal/config/variables.go> for a list of valid button outputs. This is most useful with input configurations. **Note: You must use quotation marks around the hex value to prevent the yaml parser from automatically converting it to decimal.**
* For buttons, you can specify the button number, as in `button: 3`. There are 74 buttons available, and the first button is button number `0`. As a result, valid values are 0-73. Note that buttons 12-14 and buttons 55-73 may not work in all Linux-native games.
* You can use the hexadecimal value of the code directly, via `"0x<hex value>"`. This can be useful if you want to force a specific numeric value that isn't represented by a Linux event code directly. Note however that not all output codes will work, especially in Windows games. Therefore, this option is most useful with input configurations. **Note: You must use quotation marks around the hex value to prevent the yaml parser from automatically converting it to decimal.**
* For buttons, you can specify them with the above methods, or use an integer index, as in `button: 3`. There are 74 buttons available, and the first button is button number `0`. As a result, valid values are 0-73. Note that buttons 12-14 and buttons 55-73 may not work in all Linux-native games.
For input, you can figure out what keycodes your device is emitting by running the Linux utility `evtest`. `evtest` works well with `grep`, so if you just want to see button inputs, you can do:
For input, you can figure out what event codes your device is emitting by running the Linux utility `evtest`. `evtest` works well with `grep`, so if you just want to see button inputs, you can do:
```
evtest | grep BTN_
```
The authors of this tool recognize that this is currently a pain in the ass. Easier ways to represent keycodes (as well as outputting additional keycodes) is planned for the future.
## Modes
Modes are optional, and also have the simplest configuration. To define modes, add this to your configuration:

View file

@ -23,6 +23,12 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
}
name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
capabilities := map[evdev.EvType][]evdev.EvCode{
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons),
evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes),
evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes),
}
device, err := evdev.CreateDevice(
name,
// TODO: who knows what these should actually be
@ -32,11 +38,7 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
Product: 0x0816,
Version: 1,
},
map[evdev.EvType][]evdev.EvCode{
evdev.EV_KEY: makeButtons(int(deviceConfig.Buttons)),
evdev.EV_ABS: makeAxes(int(deviceConfig.Axes)),
evdev.EV_REL: makeRelativeAxes(deviceConfig.RelativeAxes),
},
capabilities,
)
if err != nil {
@ -45,7 +47,13 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
}
deviceMap[deviceConfig.Name] = device
logger.Log(fmt.Sprintf("Created virtual device '%s'", name))
logger.Log(fmt.Sprintf(
"Created virtual device '%s' with %d buttons, %d axes, and %d relative axes",
name,
len(capabilities[evdev.EV_KEY]),
len(capabilities[evdev.EV_ABS]),
len(capabilities[evdev.EV_REL]),
))
}
return deviceMap
@ -81,12 +89,31 @@ func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevi
return deviceMap
}
func makeButtons(numButtons int) []evdev.EvCode {
// TODO: these functions have a lot of duplication; we need to figure out how to refactor it cleanly
// without losing logging context...
func makeButtons(numButtons int, buttonList []string) []evdev.EvCode {
if numButtons > 0 && len(buttonList) > 0 {
logger.Log("'num_buttons' and 'buttons' both specified, ignoring 'num_buttons'")
}
if numButtons > VirtualDeviceMaxButtons {
numButtons = VirtualDeviceMaxButtons
logger.Logf("Limiting virtual device buttons to %d", VirtualDeviceMaxButtons)
}
if len(buttonList) > 0 {
buttons := make([]evdev.EvCode, 0, len(buttonList))
for _, codeStr := range buttonList {
code, err := parseCode(codeStr, "BTN")
if err != nil {
logger.LogError(err, "Failed to create button, skipping")
continue
}
buttons = append(buttons, code)
}
return buttons
}
buttons := make([]evdev.EvCode, numButtons)
startCode := 0x120
@ -104,7 +131,24 @@ func makeButtons(numButtons int) []evdev.EvCode {
return buttons
}
func makeAxes(numAxes int) []evdev.EvCode {
func makeAxes(numAxes int, axisList []string) []evdev.EvCode {
if numAxes > 0 && len(axisList) > 0 {
logger.Log("'num_axes' and 'axes' both specified, ignoring 'num_axes'")
}
if len(axisList) > 0 {
axes := make([]evdev.EvCode, 0, len(axisList))
for _, codeStr := range axisList {
code, err := parseCode(codeStr, "ABS")
if err != nil {
logger.LogError(err, "Failed to create axis, skipping")
continue
}
axes = append(axes, code)
}
return axes
}
if numAxes > 8 {
numAxes = 8
logger.Log("Limiting virtual device axes to 8")
@ -118,19 +162,33 @@ func makeAxes(numAxes int) []evdev.EvCode {
return axes
}
func makeRelativeAxes(axes []string) []evdev.EvCode {
codes := make([]evdev.EvCode, 0)
for _, axis := range axes {
code, ok := evdev.RELFromString[axis]
if !ok {
logger.Logf("Relative axis '%s' invalid. Skipping.", axis)
continue
}
codes = append(codes, code)
func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode {
if numAxes > 0 && len(axisList) > 0 {
logger.Log("'num_rel_axes' and 'rel_axes' both specified, ignoring 'num_rel_axes'")
}
return codes
if len(axisList) > 0 {
axes := make([]evdev.EvCode, 0, len(axisList))
for _, codeStr := range axisList {
code, err := parseCode(codeStr, "REL")
if err != nil {
logger.LogError(err, "Failed to create axis, skipping")
continue
}
axes = append(axes, code)
}
return axes
}
if numAxes > 10 {
numAxes = 10
logger.Log("Limiting virtual device relative axes to 10")
}
axes := make([]evdev.EvCode, numAxes)
for i := 0; i < numAxes; i++ {
axes[i] = evdev.EvCode(i)
}
return axes
}

View file

@ -15,9 +15,38 @@ func TestRunnerDevicesConfig(t *testing.T) {
suite.Run(t, new(DevicesConfigTests))
}
func (t *DevicesConfigTests) TestMakeButtons() {
t.Run("Maximum buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons, []string{})
t.Equal(VirtualDeviceMaxButtons, len(buttons))
})
t.Run("Truncated buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons+1, []string{})
t.Equal(VirtualDeviceMaxButtons, len(buttons))
})
t.Run("16 buttons", func() {
buttons := makeButtons(16, []string{})
t.Equal(16, len(buttons))
t.Contains(buttons, evdev.EvCode(evdev.BTN_DEAD))
t.NotContains(buttons, evdev.EvCode(evdev.BTN_TRIGGER_HAPPY))
})
t.Run("Explicit buttons", func() {
buttonConfig := []string{"BTN_THUMB", "top", "btn_top2", "0x2fe", "0x300", "15"}
buttons := makeButtons(0, buttonConfig)
t.Equal(len(buttonConfig), len(buttons))
t.Contains(buttons, evdev.EvCode(0x2fe))
t.Contains(buttons, evdev.EvCode(0x300))
t.Contains(buttons, evdev.EvCode(evdev.BTN_TOP))
t.Contains(buttons, evdev.EvCode(evdev.BTN_DEAD))
})
}
func (t *DevicesConfigTests) TestMakeAxes() {
t.Run("8 axes", func() {
axes := makeAxes(8)
axes := makeAxes(8, []string{})
t.Equal(8, len(axes))
t.Contains(axes, evdev.EvCode(evdev.ABS_X))
t.Contains(axes, evdev.EvCode(evdev.ABS_Y))
@ -30,34 +59,61 @@ func (t *DevicesConfigTests) TestMakeAxes() {
})
t.Run("9 axes is truncated", func() {
axes := makeAxes(9)
axes := makeAxes(9, []string{})
t.Equal(8, len(axes))
})
t.Run("3 axes", func() {
axes := makeAxes(3)
axes := makeAxes(3, []string{})
t.Equal(3, len(axes))
t.Contains(axes, evdev.EvCode(evdev.ABS_X))
t.Contains(axes, evdev.EvCode(evdev.ABS_Y))
t.Contains(axes, evdev.EvCode(evdev.ABS_Z))
})
}
func (t *DevicesConfigTests) TestMakeButtons() {
t.Run("Maximum buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons)
t.Equal(VirtualDeviceMaxButtons, len(buttons))
})
t.Run("Truncated buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons + 1)
t.Equal(VirtualDeviceMaxButtons, len(buttons))
})
t.Run("16 buttons", func() {
buttons := makeButtons(16)
t.Equal(16, len(buttons))
t.Contains(buttons, evdev.EvCode(evdev.BTN_DEAD))
t.NotContains(buttons, evdev.EvCode(evdev.BTN_TRIGGER_HAPPY))
t.Run("4 explicit axis", func() {
axes := makeAxes(0, []string{"x", "y", "throttle", "rudder"})
t.Equal(4, len(axes))
t.Contains(axes, evdev.EvCode(evdev.ABS_X))
t.Contains(axes, evdev.EvCode(evdev.ABS_Y))
t.Contains(axes, evdev.EvCode(evdev.ABS_THROTTLE))
t.Contains(axes, evdev.EvCode(evdev.ABS_RUDDER))
})
}
func (t *DevicesConfigTests) TestMakeRelativeAxes() {
t.Run("10 axes", func() {
axes := makeRelativeAxes(10, []string{})
t.Equal(10, len(axes))
t.Contains(axes, evdev.EvCode(evdev.REL_X))
t.Contains(axes, evdev.EvCode(evdev.REL_MISC))
})
t.Run("11 axes", func() {
axes := makeRelativeAxes(11, []string{})
t.Equal(10, len(axes))
})
t.Run("3 axes", func() {
axes := makeRelativeAxes(3, []string{})
t.Equal(3, len(axes))
t.Contains(axes, evdev.EvCode(evdev.REL_X))
t.Contains(axes, evdev.EvCode(evdev.REL_Y))
t.Contains(axes, evdev.EvCode(evdev.REL_Z))
})
t.Run("1 explicit axis", func() {
axes := makeRelativeAxes(0, []string{"wheel"})
t.Equal(1, len(axes))
t.Contains(axes, evdev.EvCode(evdev.REL_WHEEL))
})
t.Run("4 explicit axis", func() {
axes := makeRelativeAxes(0, []string{"x", "y", "wheel", "hwheel"})
t.Equal(4, len(axes))
t.Contains(axes, evdev.EvCode(evdev.REL_X))
t.Contains(axes, evdev.EvCode(evdev.REL_Y))
t.Contains(axes, evdev.EvCode(evdev.REL_WHEEL))
t.Contains(axes, evdev.EvCode(evdev.REL_HWHEEL))
})
}

View file

@ -16,13 +16,16 @@ type Config struct {
}
type DeviceConfig struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
DeviceName string `yaml:"device_name,omitempty"`
Uuid string `yaml:"uuid,omitempty"`
Buttons int `yaml:"buttons,omitempty"`
Axes int `yaml:"axes,omitempty"`
RelativeAxes []string `yaml:"rel_axes,omitempty"`
Name string `yaml:"name"`
Type string `yaml:"type"`
DeviceName string `yaml:"device_name,omitempty"`
Uuid string `yaml:"uuid,omitempty"`
NumButtons int `yaml:"num_buttons,omitempty"`
NumAxes int `yaml:"num_axes,omitempty"`
NumRelativeAxes int `yaml:"num_rel_axes"`
Buttons []string `yaml:"buttons,omitempty"`
Axes []string `yaml:"axes,omitempty"`
RelativeAxes []string `yaml:"rel_axes,omitempty"`
}
type RuleConfig struct {