diff --git a/cmd/joyful-config/main.go b/cmd/joyful-config/main.go new file mode 100644 index 0000000..8ca321a --- /dev/null +++ b/cmd/joyful-config/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "git.annabunches.net/annabunches/joyful/internal/configparser" + "git.annabunches.net/annabunches/joyful/internal/logger" + flag "github.com/spf13/pflag" +) + +func getConfigDir(dir string) string { + configDir := strings.ReplaceAll(dir, "~", "${HOME}") + return os.ExpandEnv(configDir) +} + +func main() { + var configDir string + flag.StringVarP(&configDir, "config", "c", "~/.config/joyful", "Directory to read configuration from.") + flag.Parse() + configDir = getConfigDir(configDir) + + config, err := configparser.ParseConfig(configDir) + switch err.(type) { + case *configparser.EmptyConfigError: + config = &configparser.Config{} + default: + logger.FatalIfError(err, "Fatal error reading config") + } + + fmt.Printf("Config: %v\n", config) +} diff --git a/cmd/joyful/config.go b/cmd/joyful/config.go index 64d6b2d..2b43380 100644 --- a/cmd/joyful/config.go +++ b/cmd/joyful/config.go @@ -2,6 +2,7 @@ package main import ( "context" + "strings" "sync" "git.annabunches.net/annabunches/joyful/internal/configparser" @@ -15,7 +16,7 @@ func initPhysicalDevices(conf *configparser.Config) map[string]*evdev.InputDevic pDeviceMap := make(map[string]*evdev.InputDevice) for _, devConfig := range conf.Devices { - if devConfig.Type != configparser.DeviceTypePhysical { + if strings.ToLower(devConfig.Type) != configparser.DeviceTypePhysical { continue } @@ -70,7 +71,7 @@ func initVirtualBuffers(config *configparser.Config) (map[string]*evdev.InputDev vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer) for _, devConfig := range config.Devices { - if devConfig.Type != configparser.DeviceTypeVirtual { + if strings.ToLower(devConfig.Type) != configparser.DeviceTypeVirtual { continue } diff --git a/docs/examples/multiple_files/axes.yml b/docs/examples/multiple_files/axes.yml index 6f7947d..3056df3 100644 --- a/docs/examples/multiple_files/axes.yml +++ b/docs/examples/multiple_files/axes.yml @@ -92,9 +92,8 @@ rules: input: device: left-stick axis: RY - deadzones: - - start: 0 - end: 30500 + deadzone_start: 0 + deadzone_end: 30500 output: device: mouse axis: REL_WHEEL @@ -109,9 +108,8 @@ rules: input: device: left-stick axis: RY - deadzones: - - start: 29500 - end: 64000 + deadzone_start: 29500 + deadzone_end: 64000 inverted: true output: device: mouse diff --git a/docs/examples/multiple_files/devices.yml b/docs/examples/multiple_files/devices.yml index 779f0f5..391e4c8 100644 --- a/docs/examples/multiple_files/devices.yml +++ b/docs/examples/multiple_files/devices.yml @@ -1,6 +1,6 @@ devices: - name: primary - type: Virtual + type: virtual preset: joystick - name: secondary type: virtual diff --git a/docs/examples/ruletypes.yml b/docs/examples/ruletypes.yml index 8bc0fe8..7cb4b3a 100644 --- a/docs/examples/ruletypes.yml +++ b/docs/examples/ruletypes.yml @@ -18,9 +18,8 @@ rules: input: device: flightstick # To find reasonable values for your device's deadzones, use the evtest command - deadzones: - - start: 28000 - end: 30000 + deadzone_start: 28000 + deadzone_end: 30000 inverted: false axis: ABS_X output: @@ -34,9 +33,8 @@ rules: # size value. This will create a deadzone that covers a range of deadzone_size, # centered on the center value. Note that if your deadzone_center is at the lower or upper end # of the axis, the total size will still be as given; the deadzone will be "shifted" into bounds. - deadzones: - - center: 29000 - size: 2000 + deadzone_center: 29000 + deadzone_size: 2000 inverted: false axis: Y # The ABS_ prefix is optional output: @@ -48,9 +46,8 @@ rules: device: flightstick # A final way to specify deadzones is to use a size percentage instead of an absolute size. # This works exactly like deadzone_size, but calculates a percentage of the axis' total range. - deadzones: - - center: 29000 - size_percent: 5 + deadzone_center: 29000 + deadzone_size_percent: 5 inverted: false axis: Y # The ABS_ prefix is optional output: @@ -70,17 +67,6 @@ 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: @@ -122,9 +108,8 @@ rules: input: device: flightstick axis: ABS_RY # This axis commonly represents thumbsticks - deadzones: - - start: 0 - end: 30000 + deadzone_start: 0 + deadzone_end: 30000 output: device: main button: BTN_BASE4 @@ -141,9 +126,8 @@ rules: input: device: flightstick axis: ABS_Z - deadzones: - - start: 0 - end: 500 + deadzone_start: 0 + deadzone_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 7ac1945..f6e7f37 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -48,7 +48,6 @@ 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. @@ -74,17 +73,13 @@ evtest | grep BTN_ **NOTE: For most axis mappings, you probably don't want to specify a deadzone!** Use deadzone configurations in your target game instead. Joyful-configured deadzones are intended to be used in conjunction with the `axis-to-button` and `axis-to-relaxis` input types, or when splitting an axis into multiple outputs. Using them with standard `axis` mappings may result in a loss of fidelity and "stuck" inputs. -Axis inputs can define a list of deadzones. Each deadzone can be specified a few ways: +There are three ways to specify deadzones: -* Define `start` and `end` to explicitly set the deadzone bounds. -* Define `center` and `size`; this will create a deadzone of the indicated size centered at the given axis position. -* Define `center` and `size_percent` to use a percentage of the total axis size. +* Define `deadzone_start` and `deadzone_end` to explicitly set the deadzone bounds. +* Define `deadzone_center` and `deadzone_size`; this will create a deadzone of the indicated size centered at the given axis position. +* Define `deadzone_center` and `deadzone_size_percent` to use a percentage of the total axis size. -In addition, deadzones can set `emit` to `true` and `emit_value` to a value that should be emitted when inside the deadzone. - -**Note**: The `emit_value` is the final output value and should be between -32,768 and 32,767. - -See the directory for usage examples. +See for usage examples. ## Modes diff --git a/internal/configparser/configparser.go b/internal/configparser/configparser.go index 3daa217..deceb8a 100644 --- a/internal/configparser/configparser.go +++ b/internal/configparser/configparser.go @@ -50,7 +50,7 @@ func getConfigFilePaths(directory string) ([]string, error) { if err != nil { return nil, errors.New("failed to create config directory at " + directory) } else { - return nil, errors.New("no config files found at " + directory) + return nil, &EmptyConfigError{directory} } } @@ -63,5 +63,9 @@ func getConfigFilePaths(directory string) ([]string, error) { paths = append(paths, filepath.Join(directory, file.Name())) } + if len(paths) == 0 { + return nil, &EmptyConfigError{directory} + } + return paths, nil } diff --git a/internal/configparser/deviceconfig.go b/internal/configparser/deviceconfig.go deleted file mode 100644 index eafd8ca..0000000 --- a/internal/configparser/deviceconfig.go +++ /dev/null @@ -1,31 +0,0 @@ -package configparser - -// These top-level structs use custom unmarshaling to unpack each available sub-type -type DeviceConfig struct { - Type DeviceType - Config interface{} -} - -func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error { - metaConfig := &struct { - Type DeviceType - }{} - err := unmarshal(metaConfig) - if err != nil { - return err - } - dc.Type = metaConfig.Type - - err = nil - switch metaConfig.Type { - case DeviceTypePhysical: - config := DeviceConfigPhysical{} - err = unmarshal(&config) - dc.Config = config - case DeviceTypeVirtual: - config := DeviceConfigVirtual{} - err = unmarshal(&config) - dc.Config = config - } - return err -} diff --git a/internal/configparser/deviceconfigphysical.go b/internal/configparser/deviceconfigphysical.go deleted file mode 100644 index ecb5255..0000000 --- a/internal/configparser/deviceconfigphysical.go +++ /dev/null @@ -1,35 +0,0 @@ -package configparser - -type DeviceConfigPhysical struct { - Name string - DeviceName string `yaml:"device_name,omitempty"` - DevicePath string `yaml:"device_path,omitempty"` - Lock bool -} - -// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work -// just to set a single default value? -func (dc *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error { - var raw struct { - Name string - DeviceName string `yaml:"device_name"` - DevicePath string `yaml:"device_path"` - Lock bool `yaml:"lock,omitempty"` - } - - // Set non-standard defaults - raw.Lock = true - - err := unmarshal(&raw) - if err != nil { - return err - } - - *dc = DeviceConfigPhysical{ - Name: raw.Name, - DeviceName: raw.DeviceName, - DevicePath: raw.DevicePath, - Lock: raw.Lock, - } - return nil -} diff --git a/internal/configparser/devicetype.go b/internal/configparser/devicetype.go deleted file mode 100644 index 7640304..0000000 --- a/internal/configparser/devicetype.go +++ /dev/null @@ -1,40 +0,0 @@ -package configparser - -import ( - "fmt" - "strings" -) - -type DeviceType string - -const ( - DeviceTypeNone DeviceType = "" - DeviceTypePhysical DeviceType = "physical" - DeviceTypeVirtual DeviceType = "virtual" -) - -var ( - deviceTypeMap = map[string]DeviceType{ - "physical": DeviceTypePhysical, - "virtual": DeviceTypeVirtual, - } -) - -func ParseDeviceType(in string) (DeviceType, error) { - deviceType, ok := deviceTypeMap[strings.ToLower(in)] - if !ok { - return DeviceTypeNone, fmt.Errorf("invalid rule type '%s'", in) - } - return deviceType, nil -} - -func (rt *DeviceType) UnmarshalYAML(unmarshal func(data interface{}) error) error { - var raw string - err := unmarshal(&raw) - if err != nil { - return err - } - - *rt, err = ParseDeviceType(raw) - return err -} diff --git a/internal/configparser/errors.go b/internal/configparser/errors.go new file mode 100644 index 0000000..4698152 --- /dev/null +++ b/internal/configparser/errors.go @@ -0,0 +1,11 @@ +package configparser + +import "fmt" + +type EmptyConfigError struct { + directory string +} + +func (e *EmptyConfigError) Error() string { + return fmt.Sprintf("no config files found at %s", e.directory) +} diff --git a/internal/configparser/ruleconfig.go b/internal/configparser/ruleconfig.go deleted file mode 100644 index 53c3c35..0000000 --- a/internal/configparser/ruleconfig.go +++ /dev/null @@ -1,64 +0,0 @@ -package configparser - -type RuleConfig struct { - Type RuleType - Name string - Modes []string - Config interface{} -} - -func (dc *RuleConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error { - metaConfig := &struct { - Type RuleType - Name string - Modes []string - }{} - err := unmarshal(metaConfig) - if err != nil { - return err - } - dc.Type = metaConfig.Type - dc.Name = metaConfig.Name - dc.Modes = metaConfig.Modes - - switch dc.Type { - case RuleTypeButton: - config := RuleConfigButton{} - err = unmarshal(&config) - dc.Config = config - case RuleTypeButtonCombo: - config := RuleConfigButtonCombo{} - err = unmarshal(&config) - dc.Config = config - case RuleTypeButtonLatched: - config := RuleConfigButtonLatched{} - err = unmarshal(&config) - dc.Config = config - case RuleTypeAxis: - config := RuleConfigAxis{} - err = unmarshal(&config) - dc.Config = config - case RuleTypeAxisCombined: - config := RuleConfigAxisCombined{} - err = unmarshal(&config) - dc.Config = config - case RuleTypeAxisToButton: - config := RuleConfigAxisToButton{} - err = unmarshal(&config) - dc.Config = config - case RuleTypeAxisToRelaxis: - config := RuleConfigAxisToRelaxis{} - err = unmarshal(&config) - dc.Config = config - case RuleTypeModeSelect: - config := RuleConfigModeSelect{} - err = unmarshal(&config) - dc.Config = config - case RuleTypeHat: - config := RuleConfigHat{} - err = unmarshal(&config) - dc.Config = config - } - - return err -} diff --git a/internal/configparser/ruletarget.go b/internal/configparser/ruletarget.go deleted file mode 100644 index 2a2a12a..0000000 --- a/internal/configparser/ruletarget.go +++ /dev/null @@ -1,39 +0,0 @@ -package configparser - -type RuleTargetConfigButton struct { - Device string - Button string - Inverted bool -} - -type RuleTargetConfigAxis struct { - Device string - Axis string - Inverted bool - Deadzones []DeadzoneConfig -} - -type DeadzoneConfig struct { - Center int32 `yaml:"center,omitempty"` - Size int32 `yaml:"size,omitempty"` - SizePercent int32 `yaml:"size_percent,omitempty"` - Start int32 `yaml:"start,omitempty"` - End int32 `yaml:"end,omitempty"` - Emit bool `yaml:"emit,omitempty"` - Value int32 `yaml:"emit_value,omitempty"` -} - -type RuleTargetConfigRelaxis struct { - Device string - Axis string -} - -type RuleTargetConfigModeSelect struct { - Modes []string -} - -type RuleTargetConfigHat struct { - Device string - Hat string - Inverted bool -} diff --git a/internal/configparser/ruletype.go b/internal/configparser/ruletype.go deleted file mode 100644 index d305570..0000000 --- a/internal/configparser/ruletype.go +++ /dev/null @@ -1,55 +0,0 @@ -package configparser - -import ( - "fmt" - "strings" -) - -// TODO: maybe these want to live somewhere other than configparser? -type RuleType string - -const ( - RuleTypeNone RuleType = "" - RuleTypeButton RuleType = "button" - RuleTypeButtonCombo RuleType = "button-combo" - RuleTypeButtonLatched RuleType = "button-latched" - RuleTypeAxis RuleType = "axis" - RuleTypeAxisCombined RuleType = "axis-combined" - RuleTypeAxisToButton RuleType = "axis-to-button" - RuleTypeAxisToRelaxis RuleType = "axis-to-relaxis" - RuleTypeModeSelect RuleType = "mode-select" - RuleTypeHat RuleType = "hat" -) - -var ( - ruleTypeMap = map[string]RuleType{ - "button": RuleTypeButton, - "button-combo": RuleTypeButtonCombo, - "button-latched": RuleTypeButtonLatched, - "axis": RuleTypeAxis, - "axis-combined": RuleTypeAxisCombined, - "axis-to-button": RuleTypeAxisToButton, - "axis-to-relaxis": RuleTypeAxisToRelaxis, - "mode-select": RuleTypeModeSelect, - "hat": RuleTypeHat, - } -) - -func ParseRuleType(in string) (RuleType, error) { - ruleType, ok := ruleTypeMap[strings.ToLower(in)] - if !ok { - return RuleTypeNone, fmt.Errorf("invalid rule type '%s'", in) - } - return ruleType, nil -} - -func (rt *RuleType) UnmarshalYAML(unmarshal func(data interface{}) error) error { - var raw string - err := unmarshal(&raw) - if err != nil { - return err - } - - *rt, err = ParseRuleType(raw) - return err -} diff --git a/internal/configparser/schema.go b/internal/configparser/schema.go index 55ddb24..8b70521 100644 --- a/internal/configparser/schema.go +++ b/internal/configparser/schema.go @@ -1,13 +1,38 @@ -// These types comprise the YAML schema that doesn't need custom unmarshalling. +// These types comprise the YAML schema for configuring Joyful. +// The config files will be combined and then unmarshalled into this package configparser +import ( + "fmt" +) + type Config struct { Devices []DeviceConfig Modes []string Rules []RuleConfig } +// These top-level structs use custom unmarshaling to unpack each available sub-type +type DeviceConfig struct { + Type string + Config interface{} +} + +type RuleConfig struct { + Type string + Name string + Modes []string + Config interface{} +} + +type DeviceConfigPhysical struct { + Name string + DeviceName string `yaml:"device_name,omitempty"` + DevicePath string `yaml:"device_path,omitempty"` + Lock bool +} + // TODO: configure custom unmarshaling so we can overload Buttons, Axes, and RelativeAxes... type DeviceConfigVirtual struct { Name string @@ -40,11 +65,6 @@ 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"` @@ -70,3 +90,136 @@ type RuleConfigModeSelect struct { Input RuleTargetConfigButton Output RuleTargetConfigModeSelect } + +type RuleTargetConfigButton struct { + Device string + Button string + Inverted bool +} + +type RuleTargetConfigAxis struct { + Device string + Axis string + DeadzoneCenter int32 `yaml:"deadzone_center,omitempty"` + DeadzoneSize int32 `yaml:"deadzone_size,omitempty"` + DeadzoneSizePercent int32 `yaml:"deadzone_size_percent,omitempty"` + DeadzoneStart int32 `yaml:"deadzone_start,omitempty"` + DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"` + Inverted bool +} + +type RuleTargetConfigRelaxis struct { + Device string + Axis string +} + +type RuleTargetConfigModeSelect struct { + Modes []string +} + +func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error { + metaConfig := &struct { + Type string + }{} + err := unmarshal(metaConfig) + if err != nil { + return err + } + dc.Type = metaConfig.Type + + err = nil + switch metaConfig.Type { + case DeviceTypePhysical: + config := DeviceConfigPhysical{} + err = unmarshal(&config) + dc.Config = config + case DeviceTypeVirtual: + config := DeviceConfigVirtual{} + err = unmarshal(&config) + dc.Config = config + default: + err = fmt.Errorf("invalid device type '%s'", dc.Type) + } + return err +} + +func (dc *RuleConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error { + metaConfig := &struct { + Type string + Name string + Modes []string + }{} + err := unmarshal(metaConfig) + if err != nil { + return err + } + dc.Type = metaConfig.Type + dc.Name = metaConfig.Name + dc.Modes = metaConfig.Modes + + switch dc.Type { + case RuleTypeButton: + config := RuleConfigButton{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeButtonCombo: + config := RuleConfigButtonCombo{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeButtonLatched: + config := RuleConfigButtonLatched{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeAxis: + config := RuleConfigAxis{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeAxisCombined: + config := RuleConfigAxisCombined{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeAxisToButton: + config := RuleConfigAxisToButton{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeAxisToRelaxis: + config := RuleConfigAxisToRelaxis{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeModeSelect: + config := RuleConfigModeSelect{} + err = unmarshal(&config) + dc.Config = config + default: + err = fmt.Errorf("invalid rule type '%s'", dc.Type) + } + + return err +} + +// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work +// just to set a single default value? +func (dc *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error { + var raw struct { + Name string + DeviceName string `yaml:"device_name"` + DevicePath string `yaml:"device_path"` + Lock bool `yaml:"lock,omitempty"` + } + + // Set non-standard defaults + raw.Lock = true + + err := unmarshal(&raw) + if err != nil { + return err + } + + *dc = DeviceConfigPhysical{ + Name: raw.Name, + DeviceName: raw.DeviceName, + DevicePath: raw.DevicePath, + Lock: raw.Lock, + } + return nil +} diff --git a/internal/configparser/variables.go b/internal/configparser/variables.go new file mode 100644 index 0000000..77e2b9c --- /dev/null +++ b/internal/configparser/variables.go @@ -0,0 +1,15 @@ +package configparser + +const ( + DeviceTypePhysical = "physical" + DeviceTypeVirtual = "virtual" + + RuleTypeButton = "button" + RuleTypeButtonCombo = "button-combo" + RuleTypeButtonLatched = "button-latched" + RuleTypeAxis = "axis" + RuleTypeAxisCombined = "axis-combined" + RuleTypeAxisToButton = "axis-to-button" + RuleTypeAxisToRelaxis = "axis-to-relaxis" + RuleTypeModeSelect = "mode-select" +) diff --git a/internal/mappingrules/deadzone.go b/internal/mappingrules/deadzone.go deleted file mode 100644 index 23af465..0000000 --- a/internal/mappingrules/deadzone.go +++ /dev/null @@ -1,98 +0,0 @@ -package mappingrules - -import ( - "errors" - "fmt" - - "git.annabunches.net/annabunches/joyful/internal/configparser" - "github.com/holoplot/go-evdev" -) - -// TODO: need tests for multiple deadzones -// TODO: need tests for emitting deadzones - -type Deadzone struct { - Start int32 - End int32 - Size int32 - Emit bool - EmitValue int32 -} - -// DeadzoneState indicates whether a value is in a Deadzone and, if it is, whether the deadzone -// should emit an event -type DeadzoneState int - -const ( - // DeadzoneClear indicates the value is *not* in the deadzone. - DeadzoneClear DeadzoneState = iota - DeadzoneEmit - DeadzoneNoEmit -) - -// calculateDeadzones produces the deadzone start and end values in absolute terms -func NewDeadzoneFromConfig(dzConfig configparser.DeadzoneConfig, device Device, axis evdev.EvCode) (Deadzone, error) { - dz := Deadzone{} - dz.Emit = dzConfig.Emit - dz.EmitValue = dzConfig.Value - - var min, max int32 - absInfoMap, err := device.AbsInfos() - - if err != nil { - return dz, err - } else { - absInfo := absInfoMap[axis] - min = absInfo.Minimum - max = absInfo.Maximum - } - - if dzConfig.Start != 0 || dzConfig.End != 0 { - dz.Start = Clamp(dzConfig.Start, min, max) - dz.End = Clamp(dzConfig.End, min, max) - if dz.Start > dz.End { - return dz, errors.New("deadzone end must be greater than deadzone start") - } - } else { - center := Clamp(dzConfig.Center, min, max) - var deadzoneSize int32 - - switch { - case dzConfig.Size != 0: - deadzoneSize = dzConfig.Size - case dzConfig.SizePercent != 0: - deadzoneSize = (max - min) / dzConfig.SizePercent - default: - return dz, fmt.Errorf("deadzone configured incorrectly; must define start and end or center and size") - } - - dz.Start = center - deadzoneSize/2 - dz.End = center + deadzoneSize/2 - dz.Start, dz.End = clampAndShift(dz.Start, dz.End, min, max) - } - - dz.Size = dz.End - dz.Start - return dz, nil -} - -func CalculateDeadzoneSize(dzs []Deadzone) int32 { - var size int32 - - for _, dz := range dzs { - size += dz.Size - } - - return size -} - -// Match checks whether the target value is inside the deadzone. -// It returns a DeadzoneState enum and possibly an int32. -func (dz Deadzone) Match(value int32) (DeadzoneState, int32) { - if value < dz.Start || value > dz.End { - return DeadzoneClear, value - } - if dz.Emit { - return DeadzoneEmit, dz.EmitValue - } - return DeadzoneNoEmit, value -} diff --git a/internal/mappingrules/init_rule_targets_test.go b/internal/mappingrules/init_rule_targets_test.go index 3b349e9..168b02d 100644 --- a/internal/mappingrules/init_rule_targets_test.go +++ b/internal/mappingrules/init_rule_targets_test.go @@ -125,12 +125,8 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { t.Run("Invalid deadzone", func() { config := configparser.RuleTargetConfigAxis{Device: "test"} config.Axis = "x" - config.Deadzones = []configparser.DeadzoneConfig{ - { - End: 100, - Start: 1000, - }, - } + config.DeadzoneEnd = 100 + config.DeadzoneStart = 1000 _, err := NewRuleTargetAxisFromConfig(config, t.devs) t.NotNil(err) }) @@ -149,21 +145,30 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { for _, tc := range relDeadzoneTestCases { t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() { config := configparser.RuleTargetConfigAxis{ - Device: "test", - Axis: "x", - Deadzones: []configparser.DeadzoneConfig{{ - Center: tc.inCenter, - Size: tc.inSize, - }}, + Device: "test", + Axis: "x", + DeadzoneCenter: tc.inCenter, + DeadzoneSize: tc.inSize, } rule, err := NewRuleTargetAxisFromConfig(config, t.devs) t.Nil(err) - t.Equal(tc.outStart, rule.Deadzones[0].Start) - t.Equal(tc.outEnd, rule.Deadzones[0].End) + t.Equal(tc.outStart, rule.DeadzoneStart) + t.Equal(tc.outEnd, rule.DeadzoneEnd) }) } + t.Run("Deadzone center/size invalid center", func() { + config := configparser.RuleTargetConfigAxis{ + Device: "test", + Axis: "x", + DeadzoneCenter: 20000, + DeadzoneSize: 500, + } + _, err := NewRuleTargetAxisFromConfig(config, t.devs) + t.NotNil(err) + }) + relDeadzonePercentTestCases := []struct { inCenter int32 inSizePercent int32 @@ -178,20 +183,29 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { for _, tc := range relDeadzonePercentTestCases { t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() { config := configparser.RuleTargetConfigAxis{ - Device: "test", - Axis: "x", - Deadzones: []configparser.DeadzoneConfig{{ - Center: tc.inCenter, - SizePercent: tc.inSizePercent, - }}, + Device: "test", + Axis: "x", + DeadzoneCenter: tc.inCenter, + DeadzoneSizePercent: tc.inSizePercent, } rule, err := NewRuleTargetAxisFromConfig(config, t.devs) t.Nil(err) - t.Equal(tc.outStart, rule.Deadzones[0].Start) - t.Equal(tc.outEnd, rule.Deadzones[0].End) + t.Equal(tc.outStart, rule.DeadzoneStart) + t.Equal(tc.outEnd, rule.DeadzoneEnd) }) } + + t.Run("Deadzone center/percent invalid center", func() { + config := configparser.RuleTargetConfigAxis{ + Device: "test", + Axis: "x", + DeadzoneCenter: 20000, + DeadzoneSizePercent: 10, + } + _, err := NewRuleTargetAxisFromConfig(config, t.devs) + t.NotNil(err) + }) } func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { diff --git a/internal/mappingrules/init_rules.go b/internal/mappingrules/init_rules.go index 28d4ea8..7ea0ea4 100644 --- a/internal/mappingrules/init_rules.go +++ b/internal/mappingrules/init_rules.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "slices" + "strings" "git.annabunches.net/annabunches/joyful/internal/configparser" "git.annabunches.net/annabunches/joyful/internal/logger" @@ -32,27 +33,24 @@ func NewRule(config configparser.RuleConfig, pDevs map[string]Device, vDevs map[ base := NewMappingRuleBase(config.Name, config.Modes) - switch config.Type { - case configparser.RuleTypeButton: + switch strings.ToLower(config.Type) { + case RuleTypeButton: newRule, err = NewMappingRuleButton(config.Config.(configparser.RuleConfigButton), pDevs, vDevs, base) - case configparser.RuleTypeButtonCombo: + case RuleTypeButtonCombo: newRule, err = NewMappingRuleButtonCombo(config.Config.(configparser.RuleConfigButtonCombo), pDevs, vDevs, base) - case configparser.RuleTypeButtonLatched: + case RuleTypeButtonLatched: newRule, err = NewMappingRuleButtonLatched(config.Config.(configparser.RuleConfigButtonLatched), pDevs, vDevs, base) - case configparser.RuleTypeAxis: + case RuleTypeAxis: newRule, err = NewMappingRuleAxis(config.Config.(configparser.RuleConfigAxis), pDevs, vDevs, base) - case configparser.RuleTypeAxisCombined: + case RuleTypeAxisCombined: newRule, err = NewMappingRuleAxisCombined(config.Config.(configparser.RuleConfigAxisCombined), pDevs, vDevs, base) - case configparser.RuleTypeAxisToButton: + case RuleTypeAxisToButton: newRule, err = NewMappingRuleAxisToButton(config.Config.(configparser.RuleConfigAxisToButton), pDevs, vDevs, base) - case configparser.RuleTypeAxisToRelaxis: + case RuleTypeAxisToRelaxis: newRule, err = NewMappingRuleAxisToRelaxis(config.Config.(configparser.RuleConfigAxisToRelaxis), pDevs, vDevs, base) - case configparser.RuleTypeModeSelect: + case RuleTypeModeSelect: newRule, err = NewMappingRuleModeSelect(config.Config.(configparser.RuleConfigModeSelect), pDevs, modes, base) - case configparser.RuleTypeHat: - newRule, err = NewMappingRuleHat(config.Config.(configparser.RuleConfigHat), pDevs, vDevs, base) default: - // Shouldn't actually be possible to get here... err = fmt.Errorf("bad rule type '%s' for rule '%s'", config.Type, config.Name) } diff --git a/internal/mappingrules/interfaces.go b/internal/mappingrules/interfaces.go index 96594c6..33b290a 100644 --- a/internal/mappingrules/interfaces.go +++ b/internal/mappingrules/interfaces.go @@ -22,6 +22,9 @@ 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. // @@ -32,7 +35,6 @@ 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_axis_combined_test.go b/internal/mappingrules/mapping_rule_axis_combined_test.go index 967f454..c514ed7 100644 --- a/internal/mappingrules/mapping_rule_axis_combined_test.go +++ b/internal/mappingrules/mapping_rule_axis_combined_test.go @@ -28,7 +28,6 @@ func TestRunnerMappingRuleAxisCombined(t *testing.T) { } func (t *MappingRuleAxisCombinedTests) SetupTest() { - noDeadzone := make([]Deadzone, 0) mode := "*" t.mode = &mode @@ -38,13 +37,13 @@ func (t *MappingRuleAxisCombinedTests) SetupTest() { evdev.ABS_Y: {Minimum: 0, Maximum: 10000}, }, nil) - t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, noDeadzone) + t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0) t.inputTargetLower.OutputMax = 0 - t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, noDeadzone) + t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0) t.inputTargetUpper.OutputMin = 0 t.outputDevice = &evdev.InputDevice{} - t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, noDeadzone) + t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0) t.base = NewMappingRuleBase("", []string{"*"}) @@ -68,10 +67,10 @@ func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() { }, nil) rule := &MappingRuleAxisCombined{ - // MappingRuleBase: t.base, - InputLower: t.inputTargetLower, - InputUpper: t.inputTargetUpper, - // Output: t.outputTarget, + MappingRuleBase: t.base, + InputLower: t.inputTargetLower, + InputUpper: t.inputTargetUpper, + Output: t.outputTarget, } t.EqualValues(0, rule.InputLower.OutputMax) t.EqualValues(0, rule.InputUpper.OutputMin) diff --git a/internal/mappingrules/mapping_rule_axis_to_button_test.go b/internal/mappingrules/mapping_rule_axis_to_button_test.go index 24fcd64..0da086a 100644 --- a/internal/mappingrules/mapping_rule_axis_to_button_test.go +++ b/internal/mappingrules/mapping_rule_axis_to_button_test.go @@ -67,7 +67,7 @@ func (t *MappingRuleAxisToButtonTests) SetupTest() { Maximum: 10000, }, }, nil) - t.inputRule, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, false, []Deadzone{{Start: 0, End: 1000}}) + t.inputRule, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, false, int32(0), int32(1000)) t.outputDevice = &evdev.InputDevice{} t.outputRule, _ = NewRuleTargetButton("test-output", t.outputDevice, evdev.ABS_X, false) @@ -113,16 +113,14 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() { Code: evdev.ABS_X, Value: 1001, }, t.mode) - // Allow leeway since time passes during the test - t.True(testRule.nextEvent > time.Duration(650*time.Millisecond)) + t.True(testRule.nextEvent > time.Duration(700*time.Millisecond)) testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{ Type: evdev.EV_ABS, Code: evdev.ABS_X, Value: 5500, }, t.mode) - // Allow up to 50 ms leeway since time passes during the test - t.InDelta(time.Duration(500*time.Millisecond), testRule.nextEvent, 50000000) + t.Equal(time.Duration(500*time.Millisecond), testRule.nextEvent) }) } diff --git a/internal/mappingrules/mapping_rule_hat.go b/internal/mappingrules/mapping_rule_hat.go deleted file mode 100644 index ba04323..0000000 --- a/internal/mappingrules/mapping_rule_hat.go +++ /dev/null @@ -1,45 +0,0 @@ -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_axis.go b/internal/mappingrules/rule_target_axis.go index fcf1dcb..1d92d37 100644 --- a/internal/mappingrules/rule_target_axis.go +++ b/internal/mappingrules/rule_target_axis.go @@ -10,15 +10,16 @@ import ( ) type RuleTargetAxis struct { - DeviceName string - Device Device - Axis evdev.EvCode - Inverted bool - Deadzones []Deadzone - OutputMin int32 - OutputMax int32 - axisSize int32 - deadzoneSize int32 + DeviceName string + Device Device + Axis evdev.EvCode + Inverted bool + DeadzoneStart int32 + DeadzoneEnd int32 + OutputMin int32 + OutputMax int32 + axisSize int32 + deadzoneSize int32 } func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, devs map[string]Device) (*RuleTargetAxis, error) { @@ -27,18 +28,18 @@ func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) } + if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart { + return nil, errors.New("deadzone_end must be greater than deadzone_start") + } + eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixAxis) if err != nil { return nil, err } - deadzones := make([]Deadzone, 0) - for _, dzConfig := range targetConfig.Deadzones { - dz, err := NewDeadzoneFromConfig(dzConfig, device, eventCode) - if err != nil { - return nil, err - } - deadzones = append(deadzones, dz) + deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode) + if err != nil { + return nil, err } return NewRuleTargetAxis( @@ -46,15 +47,58 @@ func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, device, eventCode, targetConfig.Inverted, - deadzones, + deadzoneStart, + deadzoneEnd, ) } +// calculateDeadzones produces the deadzone start and end values in absolute terms +func calculateDeadzones(targetConfig configparser.RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) { + + var deadzoneStart, deadzoneEnd int32 + deadzoneStart = 0 + deadzoneEnd = 0 + + if targetConfig.DeadzoneStart != 0 || targetConfig.DeadzoneEnd != 0 { + return targetConfig.DeadzoneStart, targetConfig.DeadzoneEnd, nil + } + + var min, max int32 + absInfoMap, err := device.AbsInfos() + + if err != nil { + min = AxisValueMin + max = AxisValueMax + } else { + absInfo := absInfoMap[axis] + min = absInfo.Minimum + max = absInfo.Maximum + } + + if targetConfig.DeadzoneCenter < min || targetConfig.DeadzoneCenter > max { + return 0, 0, fmt.Errorf("deadzone_center '%d' is out of bounds", targetConfig.DeadzoneCenter) + } + + switch { + case targetConfig.DeadzoneSize != 0: + deadzoneStart = targetConfig.DeadzoneCenter - targetConfig.DeadzoneSize/2 + deadzoneEnd = targetConfig.DeadzoneCenter + targetConfig.DeadzoneSize/2 + case targetConfig.DeadzoneSizePercent != 0: + deadzoneSize := (max - min) / targetConfig.DeadzoneSizePercent + deadzoneStart = targetConfig.DeadzoneCenter - deadzoneSize/2 + deadzoneEnd = targetConfig.DeadzoneCenter + deadzoneSize/2 + } + + deadzoneStart, deadzoneEnd = clampAndShift(deadzoneStart, deadzoneEnd, min, max) + return deadzoneStart, deadzoneEnd, nil +} + func NewRuleTargetAxis(device_name string, device Device, axis evdev.EvCode, inverted bool, - deadzones []Deadzone) (*RuleTargetAxis, error) { + deadzoneStart int32, + deadzoneEnd int32) (*RuleTargetAxis, error) { info, err := device.AbsInfos() @@ -73,7 +117,11 @@ func NewRuleTargetAxis(device_name string, return nil, fmt.Errorf("device does not support axis %v", axis) } - deadzoneSize := CalculateDeadzoneSize(deadzones) + if deadzoneStart > deadzoneEnd { + return nil, errors.New("deadzone_end must be a higher value than deadzone_start") + } + + deadzoneSize := Abs(deadzoneEnd - deadzoneStart) // Our output range is limited to 16 bits, but we represent values internally with 32 bits. // As a result, we shouldn't need to worry about integer overruns @@ -84,15 +132,16 @@ func NewRuleTargetAxis(device_name string, } return &RuleTargetAxis{ - DeviceName: device_name, - Device: device, - Axis: axis, - Inverted: inverted, - OutputMin: AxisValueMin, - OutputMax: AxisValueMax, - Deadzones: deadzones, - deadzoneSize: deadzoneSize, - axisSize: axisSize, + DeviceName: device_name, + Device: device, + Axis: axis, + Inverted: inverted, + OutputMin: AxisValueMin, + OutputMax: AxisValueMax, + DeadzoneStart: deadzoneStart, + DeadzoneEnd: deadzoneEnd, + deadzoneSize: deadzoneSize, + axisSize: axisSize, }, nil } @@ -101,17 +150,9 @@ func NewRuleTargetAxis(device_name string, // Axis inputs are normalized to the full signed int32 range to match the virtual device's axis // characteristics. // -// If the raw value is inside the deadzone, we either emit no event, or we emit the deadzoneValue. // Typically this function is called after RuleTargetAxis.MatchEvent, which checks whether we are // in the deadzone, among other things. func (target *RuleTargetAxis) NormalizeValue(value int32) int32 { - for _, dz := range target.Deadzones { - state, dzValue := dz.Match(value) - if state == DeadzoneEmit { - return Clamp(dzValue, target.OutputMin, target.OutputMax) - } - } - axisStrength := target.GetAxisStrength(value) return LerpInt(target.OutputMin, target.OutputMax, axisStrength) } @@ -137,30 +178,19 @@ func (target *RuleTargetAxis) MatchEventDeviceAndCode(device Device, event *evde event.Code == target.Axis } -// InDeadZone checks each deadzone for whether the target value falls within it. -// If *any* non-emitting deadzone matches, we return true. // TODO: Add tests func (target *RuleTargetAxis) InDeadZone(value int32) bool { - for _, dz := range target.Deadzones { - state, _ := dz.Match(value) - if state == DeadzoneNoEmit { - return true - } - } - return false + return target.deadzoneSize > 0 && value >= target.DeadzoneStart && value <= target.DeadzoneEnd } // GetAxisStrength returns a float between 0.0 and 1.0, representing the proportional // position along the axis' full range. (after factoring in deadzones) // Calling this function with `value` inside the deadzone range will produce undefined behavior func (target *RuleTargetAxis) GetAxisStrength(value int32) float64 { - adjValue := value - for _, dz := range target.Deadzones { - if value > dz.End { - adjValue -= dz.Size - } + if value > target.DeadzoneEnd { + value -= target.deadzoneSize } - strength := float64(adjValue) / float64(target.axisSize) + strength := float64(value) / float64(target.axisSize) if target.Inverted { strength = 1.0 - strength } diff --git a/internal/mappingrules/rule_target_axis_test.go b/internal/mappingrules/rule_target_axis_test.go index 6e1d3c3..5125b94 100644 --- a/internal/mappingrules/rule_target_axis_test.go +++ b/internal/mappingrules/rule_target_axis_test.go @@ -38,42 +38,42 @@ func (t *RuleTargetAxisTests) TearDownTest() { } func (t *RuleTargetAxisTests) TestNewRuleTargetAxis() { - noDeadzone := make([]Deadzone, 0) - // RuleTargets should get created - ruleTarget, err := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) + ruleTarget, err := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0) t.Nil(err) t.EqualValues(10000, ruleTarget.axisSize) - ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, noDeadzone) + ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, 0, 0) t.Nil(err) t.EqualValues(20000, ruleTarget.axisSize) // Creating a rule with a deadzone should work and reduce the axisSize - ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, []Deadzone{{Start: -500, End: 500, Size: 1000}}) + ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, -500, 500) t.Nil(err) t.EqualValues(19000, ruleTarget.axisSize) - t.EqualValues(-500, ruleTarget.Deadzones[0].Start) - t.EqualValues(500, ruleTarget.Deadzones[0].End) + t.EqualValues(-500, ruleTarget.DeadzoneStart) + t.EqualValues(500, ruleTarget.DeadzoneEnd) + + // Creating a rule with a deadzone should fail if end > start + _, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, 500, -500) + t.NotNil(err) // Creating a rule on a non-existent axis should err - _, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Z, false, noDeadzone) + _, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Z, false, 0, 0) t.NotNil(err) // If Absinfo has an error, we should create a device with permissive bounds t.call.Unset() t.mock.On("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{}, errors.New("Test Error")) - ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) + ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0) t.Nil(err) t.Equal(AxisValueMax-AxisValueMin, ruleTarget.axisSize) } func (t *RuleTargetAxisTests) TestNormalizeValue() { - noDeadzone := make([]Deadzone, 0) - // Basic normalization should work t.Run("Simple normalization", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0) t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(10000))) t.Equal(AxisValueMin, ruleTarget.NormalizeValue(int32(0))) t.EqualValues(0, ruleTarget.NormalizeValue(int32(5000))) @@ -81,26 +81,26 @@ func (t *RuleTargetAxisTests) TestNormalizeValue() { // Normalization with a deadzone should work t.Run("With Deadzone", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, []Deadzone{{Start: 0, End: 5000, Size: 5000}}) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 5000) t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(10000))) - t.InDelta(int32(-32000), ruleTarget.NormalizeValue(int32(5001)), 1000) + t.True(ruleTarget.NormalizeValue(int32(5001)) < int32(-31000)) t.EqualValues(0, ruleTarget.NormalizeValue(int32(7500))) }) t.Run("Inverted", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, noDeadzone) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 0) t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(0))) t.Equal(AxisValueMin, ruleTarget.NormalizeValue(int32(10000))) }) t.Run("Out of bounds", func() { // Normalization past the stated axis bounds should clamp - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0) t.Equal(AxisValueMin, ruleTarget.NormalizeValue(int32(-30000))) t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(30000))) }) t.Run("With partial output range", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0) ruleTarget.OutputMin = 0 ruleTarget.OutputMax = AxisValueMax t.EqualValues(0, ruleTarget.NormalizeValue(int32(0))) @@ -110,7 +110,7 @@ func (t *RuleTargetAxisTests) TestNormalizeValue() { } func (t *RuleTargetAxisTests) TestMatchEvent() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, []Deadzone{{Start: -500, End: 500}}) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, -500, 500) validEvent := &evdev.InputEvent{ Type: evdev.EV_ABS, Code: evdev.ABS_Y, @@ -133,9 +133,7 @@ func (t *RuleTargetAxisTests) TestMatchEvent() { } func (t *RuleTargetAxisTests) TestCreateEvent() { - noDeadzone := make([]Deadzone, 0) - - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0) expected := &evdev.InputEvent{ Type: evdev.EV_ABS, Code: evdev.ABS_X, @@ -157,45 +155,43 @@ func (t *RuleTargetAxisTests) TestCreateEvent() { } func (t *RuleTargetAxisTests) TestGetAxisStrength() { - noDeadzone := make([]Deadzone, 0) - t.Run("With no deadzone", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0) t.Equal(0.0, ruleTarget.GetAxisStrength(0)) t.Equal(1.0, ruleTarget.GetAxisStrength(10000)) t.Equal(0.5, ruleTarget.GetAxisStrength(5000)) }) t.Run("With low deadzone", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, []Deadzone{{Start: 0, End: 5000, Size: 5000}}) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 5000) t.InDelta(0.0, ruleTarget.GetAxisStrength(5001), 0.01) t.InDelta(0.5, ruleTarget.GetAxisStrength(7500), 0.01) t.Equal(1.0, ruleTarget.GetAxisStrength(10000)) }) t.Run("With high deadzone", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, []Deadzone{{Start: 5000, End: 10000, Size: 5000}}) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 5000, 10000) t.Equal(0.0, ruleTarget.GetAxisStrength(0)) t.InDelta(0.5, ruleTarget.GetAxisStrength(2500), 0.01) t.InDelta(1.0, ruleTarget.GetAxisStrength(4999), 0.01) }) t.Run("Inverted", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, noDeadzone) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 0) t.Equal(1.0, ruleTarget.GetAxisStrength(0)) t.Equal(0.5, ruleTarget.GetAxisStrength(5000)) t.Equal(0.0, ruleTarget.GetAxisStrength(10000)) }) t.Run("Inverted with low deadzone", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, []Deadzone{{Start: 0, End: 5000, Size: 5000}}) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 5000) t.InDelta(1.0, ruleTarget.GetAxisStrength(5001), 0.01) t.InDelta(0.5, ruleTarget.GetAxisStrength(7500), 0.01) t.Equal(0.0, ruleTarget.GetAxisStrength(10000)) }) t.Run("Inverted with high deadzone", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, []Deadzone{{Start: 5000, End: 10000, Size: 5000}}) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 5000, 10000) t.InDelta(0.0, ruleTarget.GetAxisStrength(4999), 0.01) t.InDelta(0.5, ruleTarget.GetAxisStrength(2500), 0.01) t.Equal(1.0, ruleTarget.GetAxisStrength(0)) diff --git a/internal/mappingrules/rule_target_hat.go b/internal/mappingrules/rule_target_hat.go deleted file mode 100644 index 464e559..0000000 --- a/internal/mappingrules/rule_target_hat.go +++ /dev/null @@ -1,53 +0,0 @@ -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/internal/mappingrules/variables.go b/internal/mappingrules/variables.go new file mode 100644 index 0000000..d9a171b --- /dev/null +++ b/internal/mappingrules/variables.go @@ -0,0 +1,12 @@ +package mappingrules + +const ( + RuleTypeButton = "button" + RuleTypeButtonCombo = "button-combo" + RuleTypeButtonLatched = "button-latched" + RuleTypeAxis = "axis" + RuleTypeAxisCombined = "axis-combined" + RuleTypeAxisToButton = "axis-to-button" + RuleTypeAxisToRelaxis = "axis-to-relaxis" + RuleTypeModeSelect = "mode-select" +) diff --git a/internal/virtualdevice/variables.go b/internal/virtualdevice/variables.go index 7102bd5..11adb46 100644 --- a/internal/virtualdevice/variables.go +++ b/internal/virtualdevice/variables.go @@ -49,15 +49,6 @@ var ( evdev.ABS_RZ, evdev.ABS_THROTTLE, // Also called "Slider" or "Slider1" evdev.ABS_RUDDER, // Also called "Dial", "Slider2", or "RSlider" - // Hats - evdev.ABS_HAT0X, - evdev.ABS_HAT0Y, - evdev.ABS_HAT1X, - evdev.ABS_HAT1Y, - evdev.ABS_HAT2X, - evdev.ABS_HAT2Y, - evdev.ABS_HAT3X, - evdev.ABS_HAT3Y, }, evdev.EV_KEY: { evdev.BTN_TRIGGER, diff --git a/readme.md b/readme.md index afa5c8f..f9c0e88 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, 4 hats, and 74 buttons. +* Create virtual devices with up to 8 axes and 74 buttons. * Flexible rule system that allows several different types of rules, including: - * Simple 1:1 mappings of buttons, axes, and hats: Button1 -> VirtualButtonA + * Simple 1:1 mappings of buttons and axes: 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 -> Button and Button -> Hat support. -* HIDRAW support for more button options +* Hat support +* HIDRAW support for more button options. * Sensitivity Curves? -* Packaged builds for non-Arch distributions. +* Packaged builds non-Arch distributions. ## Configure