From 2650159a81b0b43e0052881e2c424fc35ea09a33 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Sun, 14 Sep 2025 23:11:56 +0000 Subject: [PATCH] Better deadzones (#19) Reviewed-on: https://git.annabunches.net/anna/joyful/pulls/19 Co-authored-by: Anna Rose Wiggins Co-committed-by: Anna Rose Wiggins --- docs/examples/multiple_files/axes.yml | 10 +- docs/examples/multiple_files/devices.yml | 2 +- docs/examples/ruletypes.yml | 15 +- docs/readme.md | 14 +- internal/configparser/ruletarget.go | 33 +++++ internal/configparser/schema.go | 26 ---- internal/mappingrules/deadzone.go | 99 +++++++++++++ .../mappingrules/init_rule_targets_test.go | 58 +++----- .../mapping_rule_axis_combined_test.go | 15 +- .../mapping_rule_axis_to_button_test.go | 8 +- internal/mappingrules/rule_target_axis.go | 133 +++++++----------- .../mappingrules/rule_target_axis_test.go | 54 +++---- 12 files changed, 273 insertions(+), 194 deletions(-) create mode 100644 internal/configparser/ruletarget.go create mode 100644 internal/mappingrules/deadzone.go diff --git a/docs/examples/multiple_files/axes.yml b/docs/examples/multiple_files/axes.yml index 3056df3..6f7947d 100644 --- a/docs/examples/multiple_files/axes.yml +++ b/docs/examples/multiple_files/axes.yml @@ -92,8 +92,9 @@ rules: input: device: left-stick axis: RY - deadzone_start: 0 - deadzone_end: 30500 + deadzones: + - start: 0 + end: 30500 output: device: mouse axis: REL_WHEEL @@ -108,8 +109,9 @@ rules: input: device: left-stick axis: RY - deadzone_start: 29500 - deadzone_end: 64000 + deadzones: + - start: 29500 + end: 64000 inverted: true output: device: mouse diff --git a/docs/examples/multiple_files/devices.yml b/docs/examples/multiple_files/devices.yml index 391e4c8..779f0f5 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 7cb4b3a..fe54b15 100644 --- a/docs/examples/ruletypes.yml +++ b/docs/examples/ruletypes.yml @@ -18,8 +18,9 @@ rules: input: device: flightstick # To find reasonable values for your device's deadzones, use the evtest command - deadzone_start: 28000 - deadzone_end: 30000 + deadzones: + - start: 28000 + end: 30000 inverted: false axis: ABS_X output: @@ -33,8 +34,9 @@ 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. - deadzone_center: 29000 - deadzone_size: 2000 + deadzones: + - center: 29000 + size: 2000 inverted: false axis: Y # The ABS_ prefix is optional output: @@ -46,8 +48,9 @@ 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. - deadzone_center: 29000 - deadzone_size_percent: 5 + deadzones: + - center: 29000 + size_percent: 5 inverted: false axis: Y # The ABS_ prefix is optional output: diff --git a/docs/readme.md b/docs/readme.md index f6e7f37..5544f1b 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -73,13 +73,17 @@ 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. -There are three ways to specify deadzones: +Axis inputs can define a list of deadzones. Each deadzone can be specified a few ways: -* 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. +* 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. -See for usage examples. +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. ## Modes diff --git a/internal/configparser/ruletarget.go b/internal/configparser/ruletarget.go new file mode 100644 index 0000000..094ea7b --- /dev/null +++ b/internal/configparser/ruletarget.go @@ -0,0 +1,33 @@ +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 +} diff --git a/internal/configparser/schema.go b/internal/configparser/schema.go index 942f873..f7a0035 100644 --- a/internal/configparser/schema.go +++ b/internal/configparser/schema.go @@ -65,29 +65,3 @@ 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 -} diff --git a/internal/mappingrules/deadzone.go b/internal/mappingrules/deadzone.go new file mode 100644 index 0000000..c3e39b9 --- /dev/null +++ b/internal/mappingrules/deadzone.go @@ -0,0 +1,99 @@ +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 + fmt.Printf("DEBUG: %d, %d\n", dzConfig.Value, dz.EmitValue) + + 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 168b02d..3b349e9 100644 --- a/internal/mappingrules/init_rule_targets_test.go +++ b/internal/mappingrules/init_rule_targets_test.go @@ -125,8 +125,12 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { t.Run("Invalid deadzone", func() { config := configparser.RuleTargetConfigAxis{Device: "test"} config.Axis = "x" - config.DeadzoneEnd = 100 - config.DeadzoneStart = 1000 + config.Deadzones = []configparser.DeadzoneConfig{ + { + End: 100, + Start: 1000, + }, + } _, err := NewRuleTargetAxisFromConfig(config, t.devs) t.NotNil(err) }) @@ -145,30 +149,21 @@ 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", - DeadzoneCenter: tc.inCenter, - DeadzoneSize: tc.inSize, + Device: "test", + Axis: "x", + Deadzones: []configparser.DeadzoneConfig{{ + Center: tc.inCenter, + Size: tc.inSize, + }}, } rule, err := NewRuleTargetAxisFromConfig(config, t.devs) t.Nil(err) - t.Equal(tc.outStart, rule.DeadzoneStart) - t.Equal(tc.outEnd, rule.DeadzoneEnd) + t.Equal(tc.outStart, rule.Deadzones[0].Start) + t.Equal(tc.outEnd, rule.Deadzones[0].End) }) } - 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 @@ -183,29 +178,20 @@ 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", - DeadzoneCenter: tc.inCenter, - DeadzoneSizePercent: tc.inSizePercent, + Device: "test", + Axis: "x", + Deadzones: []configparser.DeadzoneConfig{{ + Center: tc.inCenter, + SizePercent: tc.inSizePercent, + }}, } rule, err := NewRuleTargetAxisFromConfig(config, t.devs) t.Nil(err) - t.Equal(tc.outStart, rule.DeadzoneStart) - t.Equal(tc.outEnd, rule.DeadzoneEnd) + t.Equal(tc.outStart, rule.Deadzones[0].Start) + t.Equal(tc.outEnd, rule.Deadzones[0].End) }) } - - 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/mapping_rule_axis_combined_test.go b/internal/mappingrules/mapping_rule_axis_combined_test.go index c514ed7..967f454 100644 --- a/internal/mappingrules/mapping_rule_axis_combined_test.go +++ b/internal/mappingrules/mapping_rule_axis_combined_test.go @@ -28,6 +28,7 @@ func TestRunnerMappingRuleAxisCombined(t *testing.T) { } func (t *MappingRuleAxisCombinedTests) SetupTest() { + noDeadzone := make([]Deadzone, 0) mode := "*" t.mode = &mode @@ -37,13 +38,13 @@ func (t *MappingRuleAxisCombinedTests) SetupTest() { evdev.ABS_Y: {Minimum: 0, Maximum: 10000}, }, nil) - t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0) + t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, noDeadzone) t.inputTargetLower.OutputMax = 0 - t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0) + t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, noDeadzone) t.inputTargetUpper.OutputMin = 0 t.outputDevice = &evdev.InputDevice{} - t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0) + t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, noDeadzone) t.base = NewMappingRuleBase("", []string{"*"}) @@ -67,10 +68,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 0da086a..24fcd64 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, int32(0), int32(1000)) + t.inputRule, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, false, []Deadzone{{Start: 0, End: 1000}}) t.outputDevice = &evdev.InputDevice{} t.outputRule, _ = NewRuleTargetButton("test-output", t.outputDevice, evdev.ABS_X, false) @@ -113,14 +113,16 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() { Code: evdev.ABS_X, Value: 1001, }, t.mode) - t.True(testRule.nextEvent > time.Duration(700*time.Millisecond)) + // Allow leeway since time passes during the test + t.True(testRule.nextEvent > time.Duration(650*time.Millisecond)) testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{ Type: evdev.EV_ABS, Code: evdev.ABS_X, Value: 5500, }, t.mode) - t.Equal(time.Duration(500*time.Millisecond), testRule.nextEvent) + // Allow up to 50 ms leeway since time passes during the test + t.InDelta(time.Duration(500*time.Millisecond), testRule.nextEvent, 50000000) }) } diff --git a/internal/mappingrules/rule_target_axis.go b/internal/mappingrules/rule_target_axis.go index 1d92d37..6fa62f6 100644 --- a/internal/mappingrules/rule_target_axis.go +++ b/internal/mappingrules/rule_target_axis.go @@ -10,16 +10,15 @@ import ( ) type RuleTargetAxis struct { - DeviceName string - Device Device - Axis evdev.EvCode - Inverted bool - DeadzoneStart int32 - DeadzoneEnd int32 - OutputMin int32 - OutputMax int32 - axisSize int32 - deadzoneSize int32 + DeviceName string + Device Device + Axis evdev.EvCode + Inverted bool + Deadzones []Deadzone + OutputMin int32 + OutputMax int32 + axisSize int32 + deadzoneSize int32 } func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, devs map[string]Device) (*RuleTargetAxis, error) { @@ -28,18 +27,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 } - deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode) - 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) } return NewRuleTargetAxis( @@ -47,58 +46,15 @@ func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, device, eventCode, targetConfig.Inverted, - deadzoneStart, - deadzoneEnd, + deadzones, ) } -// 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, - deadzoneStart int32, - deadzoneEnd int32) (*RuleTargetAxis, error) { + deadzones []Deadzone) (*RuleTargetAxis, error) { info, err := device.AbsInfos() @@ -117,11 +73,7 @@ func NewRuleTargetAxis(device_name string, return nil, fmt.Errorf("device does not support axis %v", axis) } - if deadzoneStart > deadzoneEnd { - return nil, errors.New("deadzone_end must be a higher value than deadzone_start") - } - - deadzoneSize := Abs(deadzoneEnd - deadzoneStart) + deadzoneSize := CalculateDeadzoneSize(deadzones) // 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 @@ -132,16 +84,15 @@ func NewRuleTargetAxis(device_name string, } return &RuleTargetAxis{ - DeviceName: device_name, - Device: device, - Axis: axis, - Inverted: inverted, - OutputMin: AxisValueMin, - OutputMax: AxisValueMax, - DeadzoneStart: deadzoneStart, - DeadzoneEnd: deadzoneEnd, - deadzoneSize: deadzoneSize, - axisSize: axisSize, + DeviceName: device_name, + Device: device, + Axis: axis, + Inverted: inverted, + OutputMin: AxisValueMin, + OutputMax: AxisValueMax, + Deadzones: deadzones, + deadzoneSize: deadzoneSize, + axisSize: axisSize, }, nil } @@ -150,14 +101,23 @@ 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) } func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.InputEvent { + fmt.Println("DEBUG: Emitting event") value = Clamp(value, AxisValueMin, AxisValueMax) return &evdev.InputEvent{ Type: evdev.EV_ABS, @@ -178,19 +138,30 @@ 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 { - return target.deadzoneSize > 0 && value >= target.DeadzoneStart && value <= target.DeadzoneEnd + for _, dz := range target.Deadzones { + state, _ := dz.Match(value) + if state == DeadzoneNoEmit { + return true + } + } + return false } // 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 { - if value > target.DeadzoneEnd { - value -= target.deadzoneSize + adjValue := value + for _, dz := range target.Deadzones { + if value > dz.End { + adjValue -= dz.Size + } } - strength := float64(value) / float64(target.axisSize) + strength := float64(adjValue) / 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 5125b94..6e1d3c3 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, 0, 0) + ruleTarget, err := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) t.Nil(err) t.EqualValues(10000, ruleTarget.axisSize) - ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, 0, 0) + ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, noDeadzone) 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, -500, 500) + ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, []Deadzone{{Start: -500, End: 500, Size: 1000}}) t.Nil(err) t.EqualValues(19000, ruleTarget.axisSize) - 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) + t.EqualValues(-500, ruleTarget.Deadzones[0].Start) + t.EqualValues(500, ruleTarget.Deadzones[0].End) // Creating a rule on a non-existent axis should err - _, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Z, false, 0, 0) + _, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Z, false, noDeadzone) 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, 0, 0) + ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) 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, 0, 0) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) 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, 0, 5000) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, []Deadzone{{Start: 0, End: 5000, Size: 5000}}) t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(10000))) - t.True(ruleTarget.NormalizeValue(int32(5001)) < int32(-31000)) + t.InDelta(int32(-32000), ruleTarget.NormalizeValue(int32(5001)), 1000) t.EqualValues(0, ruleTarget.NormalizeValue(int32(7500))) }) t.Run("Inverted", func() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 0) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, noDeadzone) 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, 0, 0) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) 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, 0, 0) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) 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, -500, 500) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, []Deadzone{{Start: -500, End: 500}}) validEvent := &evdev.InputEvent{ Type: evdev.EV_ABS, Code: evdev.ABS_Y, @@ -133,7 +133,9 @@ func (t *RuleTargetAxisTests) TestMatchEvent() { } func (t *RuleTargetAxisTests) TestCreateEvent() { - ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0) + noDeadzone := make([]Deadzone, 0) + + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) expected := &evdev.InputEvent{ Type: evdev.EV_ABS, Code: evdev.ABS_X, @@ -155,43 +157,45 @@ 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, 0, 0) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone) 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, 0, 5000) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, []Deadzone{{Start: 0, End: 5000, Size: 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, 5000, 10000) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, []Deadzone{{Start: 5000, End: 10000, Size: 5000}}) 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, 0, 0) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, noDeadzone) 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, 0, 5000) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, []Deadzone{{Start: 0, End: 5000, Size: 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, 5000, 10000) + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, []Deadzone{{Start: 5000, End: 10000, Size: 5000}}) 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))