From f2095f39e5d887e399f682fe2ac7a16681d8c170 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 17 Jul 2025 13:01:18 -0400 Subject: [PATCH 1/7] Add explicit configurations for buttons and axes, and non-explicit configurations for relative axes. --- internal/config/devices.go | 84 ++++++++++++++++++++++++++++++-------- internal/config/schema.go | 17 ++++---- 2 files changed, 76 insertions(+), 25 deletions(-) diff --git a/internal/config/devices.go b/internal/config/devices.go index 2fb0e50..38d74d8 100644 --- a/internal/config/devices.go +++ b/internal/config/devices.go @@ -33,9 +33,9 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice 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), + evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons), + evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes), + evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes), }, ) @@ -81,12 +81,29 @@ func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevi return deviceMap } -func makeButtons(numButtons int) []evdev.EvCode { +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, 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 +121,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, 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 +152,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, 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 > 8 { + numAxes = 8 + logger.Log("Limiting virtual device relative axes to 8") + } + + axes := make([]evdev.EvCode, numAxes) + for i := 0; i < numAxes; i++ { + axes[i] = evdev.EvCode(i) + } + + return axes } diff --git a/internal/config/schema.go b/internal/config/schema.go index d8edaf1..c869804 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -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 { -- 2.47.2 From 72d0d21e182d289c7cad8ec25a822576816b89af Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 17 Jul 2025 13:04:42 -0400 Subject: [PATCH 2/7] Update existing tests. --- internal/config/devices_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/config/devices_test.go b/internal/config/devices_test.go index 62a6b5f..3b979fe 100644 --- a/internal/config/devices_test.go +++ b/internal/config/devices_test.go @@ -17,7 +17,7 @@ func TestRunnerDevicesConfig(t *testing.T) { 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,12 +30,12 @@ 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)) @@ -45,17 +45,17 @@ func (t *DevicesConfigTests) TestMakeAxes() { func (t *DevicesConfigTests) TestMakeButtons() { t.Run("Maximum buttons", func() { - buttons := makeButtons(VirtualDeviceMaxButtons) + buttons := makeButtons(VirtualDeviceMaxButtons, []string{}) t.Equal(VirtualDeviceMaxButtons, len(buttons)) }) t.Run("Truncated buttons", func() { - buttons := makeButtons(VirtualDeviceMaxButtons + 1) + buttons := makeButtons(VirtualDeviceMaxButtons+1, []string{}) t.Equal(VirtualDeviceMaxButtons, len(buttons)) }) t.Run("16 buttons", func() { - buttons := makeButtons(16) + 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)) -- 2.47.2 From 10d8c67a685eef048a4b37f0b910781ca856d01c Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 17 Jul 2025 13:08:50 -0400 Subject: [PATCH 3/7] Update startup output. --- cmd/joyful/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/joyful/main.go b/cmd/joyful/main.go index 6740085..e504372 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -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 -- 2.47.2 From 56e38a9ba15df0f61285a015261a31f1e5444f60 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 17 Jul 2025 15:30:32 -0400 Subject: [PATCH 4/7] Fix explicit device capabilities and add tests. --- internal/config/devices.go | 34 ++++++++----- internal/config/devices_test.go | 90 ++++++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/internal/config/devices.go b/internal/config/devices.go index 38d74d8..15d6c1f 100644 --- a/internal/config/devices.go +++ b/internal/config/devices.go @@ -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(deviceConfig.NumButtons, deviceConfig.Buttons), - evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes), - evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, 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,6 +89,8 @@ func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevi return deviceMap } +// 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'") @@ -92,7 +102,7 @@ func makeButtons(numButtons int, buttonList []string) []evdev.EvCode { } if len(buttonList) > 0 { - buttons := make([]evdev.EvCode, len(buttonList)) + buttons := make([]evdev.EvCode, 0, len(buttonList)) for _, codeStr := range buttonList { code, err := parseCode(codeStr, "BTN") if err != nil { @@ -127,7 +137,7 @@ func makeAxes(numAxes int, axisList []string) []evdev.EvCode { } if len(axisList) > 0 { - axes := make([]evdev.EvCode, len(axisList)) + axes := make([]evdev.EvCode, 0, len(axisList)) for _, codeStr := range axisList { code, err := parseCode(codeStr, "ABS") if err != nil { @@ -158,7 +168,7 @@ func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode { } if len(axisList) > 0 { - axes := make([]evdev.EvCode, len(axisList)) + axes := make([]evdev.EvCode, 0, len(axisList)) for _, codeStr := range axisList { code, err := parseCode(codeStr, "REL") if err != nil { @@ -170,9 +180,9 @@ func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode { return axes } - if numAxes > 8 { - numAxes = 8 - logger.Log("Limiting virtual device relative axes to 8") + if numAxes > 10 { + numAxes = 10 + logger.Log("Limiting virtual device relative axes to 10") } axes := make([]evdev.EvCode, numAxes) diff --git a/internal/config/devices_test.go b/internal/config/devices_test.go index 3b979fe..ad3b624 100644 --- a/internal/config/devices_test.go +++ b/internal/config/devices_test.go @@ -15,6 +15,35 @@ 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, []string{}) @@ -41,23 +70,50 @@ func (t *DevicesConfigTests) TestMakeAxes() { 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, []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("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)) }) } -- 2.47.2 From 1852db0ce63bf5ee27e5d432d2d32ad388883d6b Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 17 Jul 2025 15:49:10 -0400 Subject: [PATCH 5/7] Update documentation and examples. --- docs/examples/multiple_files/axes.yml | 250 ++++++-- docs/examples/multiple_files/buttons.yml | 709 ++++++++++++++++++++++- docs/examples/multiple_files/devices.yml | 18 +- docs/examples/multiple_files/modes.yml | 3 + docs/examples/multiple_files/readme.md | 2 +- docs/examples/ruletypes.yml | 12 +- docs/readme.md | 10 +- 7 files changed, 933 insertions(+), 71 deletions(-) create mode 100644 docs/examples/multiple_files/modes.yml diff --git a/docs/examples/multiple_files/axes.yml b/docs/examples/multiple_files/axes.yml index 59b528f..fdd2d68 100644 --- a/docs/examples/multiple_files/axes.yml +++ b/docs/examples/multiple_files/axes.yml @@ -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 + diff --git a/docs/examples/multiple_files/buttons.yml b/docs/examples/multiple_files/buttons.yml index 73e4a59..7281e38 100644 --- a/docs/examples/multiple_files/buttons.yml +++ b/docs/examples/multiple_files/buttons.yml @@ -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 - button: BTN_THUMB \ No newline at end of file + device: secondary + button: 8 + - name: BB Dial2 Left + type: button + input: + device: button-box + button: BTN_THUMB + output: + device: secondary + button: 9 diff --git a/docs/examples/multiple_files/devices.yml b/docs/examples/multiple_files/devices.yml index ca20c34..f86df9e 100644 --- a/docs/examples/multiple_files/devices.yml +++ b/docs/examples/multiple_files/devices.yml @@ -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 @@ -14,4 +24,4 @@ devices: device_name: "CH PRODUCTS CH PRO PEDALS USB " - name: button-box type: physical - device_name: Arduino Arduino Joystick \ No newline at end of file + device_name: Arduino Arduino Joystick diff --git a/docs/examples/multiple_files/modes.yml b/docs/examples/multiple_files/modes.yml new file mode 100644 index 0000000..376a903 --- /dev/null +++ b/docs/examples/multiple_files/modes.yml @@ -0,0 +1,3 @@ +modes: + - main + - mining diff --git a/docs/examples/multiple_files/readme.md b/docs/examples/multiple_files/readme.md index 0476295..032f640 100644 --- a/docs/examples/multiple_files/readme.md +++ b/docs/examples/multiple_files/readme.md @@ -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. \ No newline at end of file diff --git a/docs/examples/ruletypes.yml b/docs/examples/ruletypes.yml index 9f63ca4..74a6b67 100644 --- a/docs/examples/ruletypes.yml +++ b/docs/examples/ruletypes.yml @@ -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 diff --git a/docs/readme.md b/docs/readme.md index 799f6b5..141f398 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -33,17 +33,17 @@ All `rules` must have a `type` parameter. Valid values for this parameter are: Configuration options for each rule type vary. See 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 . 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"`. 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 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. +* 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: -- 2.47.2 From 89d76f7b89a70dc5c7f7f4ad704660646e991249 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 17 Jul 2025 15:49:31 -0400 Subject: [PATCH 6/7] Small wording change. --- docs/examples/multiple_files/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/multiple_files/readme.md b/docs/examples/multiple_files/readme.md index 032f640..6a9d764 100644 --- a/docs/examples/multiple_files/readme.md +++ b/docs/examples/multiple_files/readme.md @@ -4,4 +4,4 @@ This directory demonstrates how to split your configuration across multiple file 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. \ No newline at end of file +It is copied from the author's actual mappings for Star Citizen. \ No newline at end of file -- 2.47.2 From 42dd1e03e7b225f86fc12453629b5991560315dc Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 17 Jul 2025 16:04:03 -0400 Subject: [PATCH 7/7] More documentation updates. --- docs/readme.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/readme.md b/docs/readme.md index 141f398..340bcd3 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -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 @@ -42,17 +46,15 @@ Ways to specify event codes are: * 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 . 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"`. 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 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.** +* You can use the hexadecimal value of the code directly, via `"0x"`. 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: -- 2.47.2