From fce8888c7798f002379372abf6b3ac4784be8836 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Mon, 15 Sep 2025 13:17:40 -0400 Subject: [PATCH] Add support for Joystick hats. --- docs/examples/ruletypes.yml | 21 +++++++-- docs/readme.md | 1 + internal/configparser/ruletarget.go | 6 +++ internal/configparser/schema.go | 5 +++ internal/mappingrules/interfaces.go | 4 +- internal/mappingrules/mapping_rule_hat.go | 45 +++++++++++++++++++ internal/mappingrules/rule_target_hat.go | 53 +++++++++++++++++++++++ readme.md | 12 ++--- 8 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 internal/mappingrules/mapping_rule_hat.go create mode 100644 internal/mappingrules/rule_target_hat.go diff --git a/docs/examples/ruletypes.yml b/docs/examples/ruletypes.yml index fe54b15..8bc0fe8 100644 --- a/docs/examples/ruletypes.yml +++ b/docs/examples/ruletypes.yml @@ -70,6 +70,17 @@ rules: device: main axis: RZ + # Hat mapping. Hats are technically an axis, but only output -1, 0, or 1, so we don't normalize + # them to an output range, we just pass them through mostly unmodified + - type: hat + input: + device: flightstick + inverted: true # hats do support inversion. As with other rule types, this only has an effect on *inputs*. + hat: hat0x # a typical joystick hat actually has 2 hat axes: x and y + output: + device: main + hat: hat0x + # Straightforward button mapping - type: button input: @@ -111,8 +122,9 @@ rules: input: device: flightstick axis: ABS_RY # This axis commonly represents thumbsticks - deadzone_start: 0 - deadzone_end: 30000 + deadzones: + - start: 0 + end: 30000 output: device: main button: BTN_BASE4 @@ -129,8 +141,9 @@ rules: input: device: flightstick axis: ABS_Z - deadzone_start: 0 - deadzone_end: 500 + deadzones: + - start: 0 + end: 500 output: device: mouse button: REL_WHEEL \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md index 5544f1b..7ac1945 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -48,6 +48,7 @@ All `rules` must have a `type` parameter. Valid values for this parameter are: * `axis-combined` - a mapping that combines 2 input axes into a single output axis. * `axis-to-button` - causes an axis input to produce a button output. This can be repeated with variable speed proportional to the axis' input value * `axis-to-relaxis` - like axis-to-button, but produces a "relative axis" output value. This is useful for simulating mouse scrollwheel and movement events. +* `hat` - a special type of axis with ternary output. Each joystick hat will typically be 2 hat axes named `ABS_HAT0X` / `ABS_HAT0Y`, where the `0` is an index between 0 - 3. So for a typical hat you would define 2 `hat` rules. Configuration options for each rule type vary. See [examples/ruletypes.yml](examples/ruletypes.yml) for an example of each type with all options specified. diff --git a/internal/configparser/ruletarget.go b/internal/configparser/ruletarget.go index 094ea7b..2a2a12a 100644 --- a/internal/configparser/ruletarget.go +++ b/internal/configparser/ruletarget.go @@ -31,3 +31,9 @@ type RuleTargetConfigRelaxis struct { type RuleTargetConfigModeSelect struct { Modes []string } + +type RuleTargetConfigHat struct { + Device string + Hat string + Inverted bool +} diff --git a/internal/configparser/schema.go b/internal/configparser/schema.go index f7a0035..55ddb24 100644 --- a/internal/configparser/schema.go +++ b/internal/configparser/schema.go @@ -40,6 +40,11 @@ type RuleConfigAxis struct { Output RuleTargetConfigAxis } +type RuleConfigHat struct { + Input RuleTargetConfigHat + Output RuleTargetConfigHat +} + type RuleConfigAxisCombined struct { InputLower RuleTargetConfigAxis `yaml:"input_lower,omitempty"` InputUpper RuleTargetConfigAxis `yaml:"input_upper,omitempty"` diff --git a/internal/mappingrules/interfaces.go b/internal/mappingrules/interfaces.go index 33b290a..96594c6 100644 --- a/internal/mappingrules/interfaces.go +++ b/internal/mappingrules/interfaces.go @@ -22,9 +22,6 @@ type RuleTarget interface { // (e.g., inverting the value if Inverted == true) NormalizeValue(int32) int32 - // MatchEvent returns true if the provided device and input event are a match for this rule target - ValidateEvent(*evdev.InputDevice, *evdev.InputEvent) bool - // CreateEvent creates an event that can be emitted on a virtual device. // For RuleTargetModeSelect, this method modifies the active mode and returns nil. // @@ -35,6 +32,7 @@ type RuleTarget interface { // for most implementations. CreateEvent(int32, *string) *evdev.InputEvent + // MatchEvent returns true if the provided device and input event are a match for this rule target MatchEvent(device Device, event *evdev.InputEvent) bool } diff --git a/internal/mappingrules/mapping_rule_hat.go b/internal/mappingrules/mapping_rule_hat.go new file mode 100644 index 0000000..ba04323 --- /dev/null +++ b/internal/mappingrules/mapping_rule_hat.go @@ -0,0 +1,45 @@ +package mappingrules + +import ( + "git.annabunches.net/annabunches/joyful/internal/configparser" + "github.com/holoplot/go-evdev" +) + +// A Simple Mapping Rule can map a button to a button or an axis to an axis. +type MappingRuleHat struct { + MappingRuleBase + Input *RuleTargetHat + Output *RuleTargetHat +} + +func NewMappingRuleHat(ruleConfig configparser.RuleConfigHat, + pDevs map[string]Device, + vDevs map[string]Device, + base MappingRuleBase) (*MappingRuleHat, error) { + + input, err := NewRuleTargetHatFromConfig(ruleConfig.Input, pDevs) + if err != nil { + return nil, err + } + + output, err := NewRuleTargetHatFromConfig(ruleConfig.Output, vDevs) + if err != nil { + return nil, err + } + + return &MappingRuleHat{ + MappingRuleBase: base, + Input: input, + Output: output, + }, nil +} + +func (rule *MappingRuleHat) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { + if !rule.MappingRuleBase.modeCheck(mode) || + !rule.Input.MatchEvent(device, event) { + return nil, nil + } + + // The cast here is safe because the interface is only ever different for unit tests + return rule.Output.Device.(*evdev.InputDevice), rule.Output.CreateEvent(rule.Input.NormalizeValue(event.Value), mode) +} diff --git a/internal/mappingrules/rule_target_hat.go b/internal/mappingrules/rule_target_hat.go new file mode 100644 index 0000000..464e559 --- /dev/null +++ b/internal/mappingrules/rule_target_hat.go @@ -0,0 +1,53 @@ +package mappingrules + +import ( + "fmt" + + "git.annabunches.net/annabunches/joyful/internal/configparser" + "git.annabunches.net/annabunches/joyful/internal/eventcodes" + "github.com/holoplot/go-evdev" +) + +type RuleTargetHat struct { + Device Device + Hat evdev.EvCode + Inverted bool +} + +func NewRuleTargetHatFromConfig(config configparser.RuleTargetConfigHat, devs map[string]Device) (*RuleTargetHat, error) { + dev, ok := devs[config.Device] + if !ok { + return nil, fmt.Errorf("device '%s' not found", config.Device) + } + + code, err := eventcodes.ParseCode(config.Hat, eventcodes.CodePrefixAxis) + if err != nil { + return nil, err + } + + return &RuleTargetHat{ + Device: dev, + Hat: code, + Inverted: config.Inverted, + }, nil +} + +func (target *RuleTargetHat) NormalizeValue(value int32) int32 { + if !target.Inverted { + return value + } + + return value * -1 +} + +func (target *RuleTargetHat) CreateEvent(value int32, _ *string) *evdev.InputEvent { + return &evdev.InputEvent{ + Type: evdev.EV_ABS, + Code: target.Hat, + Value: value, + } +} + +func (target *RuleTargetHat) MatchEvent(device Device, event *evdev.InputEvent) bool { + return device == target.Device && event.Code == target.Hat +} diff --git a/readme.md b/readme.md index f9c0e88..afa5c8f 100644 --- a/readme.md +++ b/readme.md @@ -10,13 +10,13 @@ Joyful is ideal for Linux gamers who enjoy space and flight sims and miss the fe ### Current Features -* Create virtual devices with up to 8 axes and 74 buttons. +* Create virtual devices with up to 8 axes, 4 hats, and 74 buttons. * Flexible rule system that allows several different types of rules, including: - * Simple 1:1 mappings of buttons and axes: Button1 -> VirtualButtonA + * Simple 1:1 mappings of buttons, axes, and hats: Button1 -> VirtualButtonA * Combination mappings: Button1 + Button2 -> VirtualButtonA * "Split" axis mapping: map sections of an axis to different outputs using deadzones. * "Combined" axis mapping: map two physical axes to one virtual axis. - * Axis -> button mapping with optional "proportional" repeat speed (i.e. repeat faster as the axis is engaged further) + * Axis -> Button mapping with optional "proportional" repeat speed (i.e. repeat faster as the axis is engaged further) * Axis -> Relative Axis mapping, for converting a joystick axis to mouse movement and scrollwheel events. * Define keyboard, mouse, and gamepad outputs in addition to joysticks. * Configure per-rule configurable deadzones for axes, with multiple ways to specify deadzones. @@ -27,10 +27,10 @@ Joyful is ideal for Linux gamers who enjoy space and flight sims and miss the fe * Macros - have a single input produce a sequence of button presses with configurable pauses. * Sequence combos - Button1, Button2, Button3 -> VirtualButtonA -* Hat support -* HIDRAW support for more button options. +* Hat -> Button and Button -> Hat support. +* HIDRAW support for more button options * Sensitivity Curves? -* Packaged builds non-Arch distributions. +* Packaged builds for non-Arch distributions. ## Configure