* Add device "presets" to make configuring virtual devices correctly easy.

* Add support for keyboard keys.
This commit is contained in:
Anna Rose Wiggins 2025-08-04 11:54:17 -04:00
parent 32fa7d27e0
commit 97e1aa83e3
7 changed files with 313 additions and 30 deletions

View file

@ -1,19 +1,7 @@
devices: devices:
- name: primary - name: primary
type: virtual type: virtual
num_axes: 6 preset: gamepad
buttons:
- BTN_EAST
- BTN_SOUTH
- BTN_NORTH
- BTN_WEST
- BTN_TL
- BTN_TR
- BTN_SELECT
- BTN_START
- BTN_MODE
- BTN_THUMBL
- BTN_THUMBR
- name: right-stick - name: right-stick
type: physical type: physical
device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2 device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2

View file

@ -1,5 +1,5 @@
## joystick -> gamepad mapping ## joystick -> gamepad mapping
This is an incomplete example for mapping dual flightsticks (Virpil Constellation Alphas) to gamepad outputs, to support dual-joystick play in games that expect a console-style gamepad. This has been tested on Steam, and it successfully recognizes this as a gamepad. This is an incomplete example for mapping dual flightsticks (Virpil Constellation Alphas) to gamepad outputs, to support dual-joystick play in games that expect a console-style gamepad. This has been tested on Outer Wilds running in Steam.
Not every possible input is mapped here, this is just a somewhat minimal example. Not every possible input is mapped here, this is just a somewhat minimal example.

View file

@ -1,18 +1,13 @@
devices: devices:
- name: primary - name: primary
type: virtual type: virtual
num_buttons: 74 preset: joystick
num_axes: 8
- name: secondary - name: secondary
type: virtual type: virtual
num_buttons: 74 preset: joystick
num_axes: 3
- name: mouse - name: mouse
type: virtual type: virtual
num_buttons: 0 preset: mouse
num_axes: 0
rel_axes:
- REL_WHEEL
- name: right-stick - name: right-stick
type: physical type: physical
device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2 device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2

View file

@ -2,7 +2,7 @@
Configuration is divided into three sections: `devices`, `modes`, and `rules`. Each yaml file can have any number of these sections; joyful will combine the configuration from all files at runtime. Configuration is divided into three sections: `devices`, `modes`, and `rules`. Each yaml file can have any number of these sections; joyful will combine the configuration from all files at runtime.
### Device configuration ## Device configuration
Each entry in `devices` must have a couple of parameters: Each entry in `devices` must have a couple of parameters:
@ -15,16 +15,17 @@ Each entry in `devices` must have a couple of parameters:
`virtual` devices can additionally define these parameters: `virtual` devices can additionally define these parameters:
* `preset` - Can be `joystick`, `gamepad`, `mouse`, or `keyboard`, and will configure the virtual device to look like and emit an appropriate set of outputs based on the name. For exactly which axes and buttons are defined for each type, see the `Capabilities` values in [internal/config/variables.go](internal/config/variables.go).
* `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. * `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. * `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. * `relative_axes` or `num_relative_axes` - As above, but for `REL_` axes.
A couple of additional notes on virtual devices: 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. * For all 3 of the above options, there is a priority order. If you specify a `preset`, it will be used ignoring any other settings. An explicit list will override the corresponding `num_` parameter.
* 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. * Some environments/applications are prescriptive about what combinations make sense; for example, they will only register mouse events if the device *only* supports mouse-like events. The `presets` attempt to take this into account. If you are defining capabilities manually and attempt to mix and match button codes, you may also run into this problem.
### Rules configuration ## Rules configuration
All `rules` must have a `type` parameter. Valid values for this parameter are: All `rules` must have a `type` parameter. Valid values for this parameter are:

View file

@ -23,10 +23,25 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
} }
name := fmt.Sprintf("joyful-%s", deviceConfig.Name) name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
capabilities := map[evdev.EvType][]evdev.EvCode{
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons), var capabilities map[evdev.EvType][]evdev.EvCode
evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes),
evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes), // todo: add tests for presets
switch deviceConfig.Preset {
case DevicePresetGamepad:
capabilities = CapabilitiesPresetGamepad
case DevicePresetKeyboard:
capabilities = CapabilitiesPresetKeyboard
case DevicePresetJoystick:
capabilities = CapabilitiesPresetJoystick
case DevicePresetMouse:
capabilities = CapabilitiesPresetMouse
default:
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( device, err := evdev.CreateDevice(

View file

@ -20,6 +20,7 @@ type DeviceConfig struct {
Type string `yaml:"type"` Type string `yaml:"type"`
DeviceName string `yaml:"device_name,omitempty"` DeviceName string `yaml:"device_name,omitempty"`
Uuid string `yaml:"uuid,omitempty"` Uuid string `yaml:"uuid,omitempty"`
Preset string `yaml:"preset,omitempty"`
NumButtons int `yaml:"num_buttons,omitempty"` NumButtons int `yaml:"num_buttons,omitempty"`
NumAxes int `yaml:"num_axes,omitempty"` NumAxes int `yaml:"num_axes,omitempty"`
NumRelativeAxes int `yaml:"num_rel_axes"` NumRelativeAxes int `yaml:"num_rel_axes"`

View file

@ -8,6 +8,11 @@ const (
DeviceTypePhysical = "physical" DeviceTypePhysical = "physical"
DeviceTypeVirtual = "virtual" DeviceTypeVirtual = "virtual"
DevicePresetKeyboard = "keyboard"
DevicePresetGamepad = "gamepad"
DevicePresetJoystick = "joystick"
DevicePresetMouse = "mouse"
RuleTypeButton = "button" RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo" RuleTypeButtonCombo = "button-combo"
RuleTypeLatched = "button-latched" RuleTypeLatched = "button-latched"
@ -103,3 +108,281 @@ var (
evdev.EvCode(0x2ff), evdev.EvCode(0x2ff),
} }
) )
// Device Presets
var (
CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{
evdev.EV_ABS: {
evdev.ABS_X,
evdev.ABS_Y,
evdev.ABS_Z,
evdev.ABS_RX,
evdev.ABS_RY,
evdev.ABS_RZ,
evdev.ABS_HAT0X,
evdev.ABS_HAT0Y,
},
evdev.EV_KEY: {
evdev.BTN_NORTH, // Xbox 'X', Playstation 'Square'
evdev.BTN_SOUTH, // Xbox 'A', Plastation 'X'
evdev.BTN_WEST, // Xbox 'Y', Playstation 'Triangle'
evdev.BTN_EAST, // Xbox 'B', Playstation 'O'
evdev.BTN_THUMBL,
evdev.BTN_THUMBR,
evdev.BTN_TL,
evdev.BTN_TR,
evdev.BTN_SELECT,
evdev.BTN_START,
evdev.BTN_MODE,
},
}
CapabilitiesPresetJoystick = map[evdev.EvType][]evdev.EvCode{
evdev.EV_ABS: {
evdev.ABS_X,
evdev.ABS_Y,
evdev.ABS_Z,
evdev.ABS_RX,
evdev.ABS_RY,
evdev.ABS_RZ,
evdev.ABS_THROTTLE, // Also called "Slider" or "Slider1"
evdev.ABS_RUDDER, // Also called "Dial", "Slider2", or "RSlider"
},
evdev.EV_KEY: {
evdev.BTN_TRIGGER,
evdev.BTN_THUMB,
evdev.BTN_THUMB2,
evdev.BTN_TOP,
evdev.BTN_TOP2,
evdev.BTN_PINKIE,
evdev.BTN_BASE,
evdev.BTN_BASE2,
evdev.BTN_BASE3,
evdev.BTN_BASE4,
evdev.BTN_BASE5,
evdev.BTN_BASE6,
evdev.EvCode(0x12c), // decimal 300
evdev.EvCode(0x12d), // decimal 301
evdev.EvCode(0x12e), // decimal 302
evdev.BTN_DEAD,
evdev.BTN_TRIGGER_HAPPY1,
evdev.BTN_TRIGGER_HAPPY2,
evdev.BTN_TRIGGER_HAPPY3,
evdev.BTN_TRIGGER_HAPPY4,
evdev.BTN_TRIGGER_HAPPY5,
evdev.BTN_TRIGGER_HAPPY6,
evdev.BTN_TRIGGER_HAPPY7,
evdev.BTN_TRIGGER_HAPPY8,
evdev.BTN_TRIGGER_HAPPY9,
evdev.BTN_TRIGGER_HAPPY10,
evdev.BTN_TRIGGER_HAPPY11,
evdev.BTN_TRIGGER_HAPPY12,
evdev.BTN_TRIGGER_HAPPY13,
evdev.BTN_TRIGGER_HAPPY14,
evdev.BTN_TRIGGER_HAPPY15,
evdev.BTN_TRIGGER_HAPPY16,
evdev.BTN_TRIGGER_HAPPY17,
evdev.BTN_TRIGGER_HAPPY18,
evdev.BTN_TRIGGER_HAPPY19,
evdev.BTN_TRIGGER_HAPPY20,
evdev.BTN_TRIGGER_HAPPY21,
evdev.BTN_TRIGGER_HAPPY22,
evdev.BTN_TRIGGER_HAPPY23,
evdev.BTN_TRIGGER_HAPPY24,
evdev.BTN_TRIGGER_HAPPY25,
evdev.BTN_TRIGGER_HAPPY26,
evdev.BTN_TRIGGER_HAPPY27,
evdev.BTN_TRIGGER_HAPPY28,
evdev.BTN_TRIGGER_HAPPY29,
evdev.BTN_TRIGGER_HAPPY30,
evdev.BTN_TRIGGER_HAPPY31,
evdev.BTN_TRIGGER_HAPPY32,
evdev.BTN_TRIGGER_HAPPY33,
evdev.BTN_TRIGGER_HAPPY34,
evdev.BTN_TRIGGER_HAPPY35,
evdev.BTN_TRIGGER_HAPPY36,
evdev.BTN_TRIGGER_HAPPY37,
evdev.BTN_TRIGGER_HAPPY38,
evdev.BTN_TRIGGER_HAPPY39,
evdev.BTN_TRIGGER_HAPPY40,
evdev.EvCode(0x2e8),
evdev.EvCode(0x2e9),
evdev.EvCode(0x2f0),
evdev.EvCode(0x2f1),
evdev.EvCode(0x2f2),
evdev.EvCode(0x2f3),
evdev.EvCode(0x2f4),
evdev.EvCode(0x2f5),
evdev.EvCode(0x2f6),
evdev.EvCode(0x2f7),
evdev.EvCode(0x2f8),
evdev.EvCode(0x2f9),
evdev.EvCode(0x2fa),
evdev.EvCode(0x2fb),
evdev.EvCode(0x2fc),
evdev.EvCode(0x2fd),
evdev.EvCode(0x2fe),
evdev.EvCode(0x2ff),
},
}
CapabilitiesPresetKeyboard = map[evdev.EvType][]evdev.EvCode{
evdev.EV_KEY: {
evdev.KEY_ESC,
evdev.KEY_1,
evdev.KEY_2,
evdev.KEY_3,
evdev.KEY_4,
evdev.KEY_5,
evdev.KEY_6,
evdev.KEY_7,
evdev.KEY_8,
evdev.KEY_9,
evdev.KEY_0,
evdev.KEY_MINUS,
evdev.KEY_EQUAL,
evdev.KEY_BACKSPACE,
evdev.KEY_TAB,
evdev.KEY_Q,
evdev.KEY_W,
evdev.KEY_E,
evdev.KEY_R,
evdev.KEY_T,
evdev.KEY_Y,
evdev.KEY_U,
evdev.KEY_I,
evdev.KEY_O,
evdev.KEY_P,
evdev.KEY_LEFTBRACE,
evdev.KEY_RIGHTBRACE,
evdev.KEY_ENTER,
evdev.KEY_LEFTCTRL,
evdev.KEY_A,
evdev.KEY_S,
evdev.KEY_D,
evdev.KEY_F,
evdev.KEY_G,
evdev.KEY_H,
evdev.KEY_J,
evdev.KEY_K,
evdev.KEY_L,
evdev.KEY_SEMICOLON,
evdev.KEY_APOSTROPHE,
evdev.KEY_GRAVE,
evdev.KEY_LEFTSHIFT,
evdev.KEY_BACKSLASH,
evdev.KEY_Z,
evdev.KEY_X,
evdev.KEY_C,
evdev.KEY_V,
evdev.KEY_B,
evdev.KEY_N,
evdev.KEY_M,
evdev.KEY_COMMA,
evdev.KEY_DOT,
evdev.KEY_SLASH,
evdev.KEY_RIGHTSHIFT,
evdev.KEY_KPASTERISK,
evdev.KEY_LEFTALT,
evdev.KEY_SPACE,
evdev.KEY_CAPSLOCK,
evdev.KEY_F1,
evdev.KEY_F2,
evdev.KEY_F3,
evdev.KEY_F4,
evdev.KEY_F5,
evdev.KEY_F6,
evdev.KEY_F7,
evdev.KEY_F8,
evdev.KEY_F9,
evdev.KEY_F10,
evdev.KEY_NUMLOCK,
evdev.KEY_SCROLLLOCK,
evdev.KEY_KP7,
evdev.KEY_KP8,
evdev.KEY_KP9,
evdev.KEY_KPMINUS,
evdev.KEY_KP4,
evdev.KEY_KP5,
evdev.KEY_KP6,
evdev.KEY_KPPLUS,
evdev.KEY_KP1,
evdev.KEY_KP2,
evdev.KEY_KP3,
evdev.KEY_KP0,
evdev.KEY_KPDOT,
evdev.KEY_ZENKAKUHANKAKU,
evdev.KEY_102ND,
evdev.KEY_F11,
evdev.KEY_F12,
evdev.KEY_RO,
evdev.KEY_KATAKANA,
evdev.KEY_HIRAGANA,
evdev.KEY_HENKAN,
evdev.KEY_KATAKANAHIRAGANA,
evdev.KEY_MUHENKAN,
evdev.KEY_KPJPCOMMA,
evdev.KEY_KPENTER,
evdev.KEY_RIGHTCTRL,
evdev.KEY_KPSLASH,
evdev.KEY_SYSRQ,
evdev.KEY_RIGHTALT,
evdev.KEY_LINEFEED,
evdev.KEY_HOME,
evdev.KEY_UP,
evdev.KEY_PAGEUP,
evdev.KEY_LEFT,
evdev.KEY_RIGHT,
evdev.KEY_END,
evdev.KEY_DOWN,
evdev.KEY_PAGEDOWN,
evdev.KEY_INSERT,
evdev.KEY_DELETE,
evdev.KEY_MACRO,
evdev.KEY_MUTE,
evdev.KEY_VOLUMEDOWN,
evdev.KEY_VOLUMEUP,
evdev.KEY_KPEQUAL,
evdev.KEY_KPPLUSMINUS,
evdev.KEY_PAUSE,
evdev.KEY_SCALE,
evdev.KEY_KPCOMMA,
evdev.KEY_HANGEUL,
evdev.KEY_HANJA,
evdev.KEY_YEN,
evdev.KEY_LEFTMETA,
evdev.KEY_RIGHTMETA,
evdev.KEY_COMPOSE,
evdev.KEY_F13,
evdev.KEY_F14,
evdev.KEY_F15,
evdev.KEY_F16,
evdev.KEY_F17,
evdev.KEY_F18,
evdev.KEY_F19,
evdev.KEY_F20,
evdev.KEY_F21,
evdev.KEY_F22,
evdev.KEY_F23,
evdev.KEY_F24,
},
}
CapabilitiesPresetMouse = map[evdev.EvType][]evdev.EvCode{
evdev.EV_REL: {
evdev.REL_X,
evdev.REL_Y,
evdev.REL_WHEEL,
evdev.REL_HWHEEL,
},
evdev.EV_KEY: {
evdev.BTN_LEFT,
evdev.BTN_MIDDLE,
evdev.BTN_RIGHT,
evdev.BTN_SIDE,
evdev.BTN_EXTRA,
evdev.BTN_FORWARD,
evdev.BTN_BACK,
},
}
)