diff --git a/internal/mappingrules/mapping_rule_axis_combined.go b/internal/mappingrules/mapping_rule_axis_combined.go index c6b3980..c113e2f 100644 --- a/internal/mappingrules/mapping_rule_axis_combined.go +++ b/internal/mappingrules/mapping_rule_axis_combined.go @@ -1,15 +1,51 @@ package mappingrules -import "github.com/holoplot/go-evdev" +import ( + "git.annabunches.net/annabunches/joyful/internal/logger" + "github.com/holoplot/go-evdev" +) type MappingRuleAxisCombined struct { MappingRuleBase + InputLower *RuleTargetAxis + InputUpper *RuleTargetAxis + Output *RuleTargetAxis } func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis, inputUpper *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxisCombined { - return nil + inputLower.OutputMax = 0 + inputUpper.OutputMin = 0 + return &MappingRuleAxisCombined{ + MappingRuleBase: base, + InputLower: inputLower, + InputUpper: inputUpper, + Output: output, + } } func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { - return nil, nil + if !rule.MappingRuleBase.modeCheck(mode) || + !(rule.InputLower.MatchEvent(device, event) || + rule.InputUpper.MatchEvent(device, event)) { + logger.Log("DEBUG: Did not match event") + return nil, nil + } + + // Since lower and upper are guaranteed to have opposite signs, + // we can just sum them. + var value int32 + value += getValueFromAbs(rule.InputLower) + value += getValueFromAbs(rule.InputUpper) + + return rule.Output.Device.(*evdev.InputDevice), rule.Output.CreateEvent(value, mode) +} + +func getValueFromAbs(ruleTarget *RuleTargetAxis) int32 { + absInfo, err := ruleTarget.Device.AbsInfos() + if err != nil { + logger.LogErrorf(err, "WARNING: Couldn't get axis data for device '%s'", ruleTarget.DeviceName) + return 0 + } + + return ruleTarget.NormalizeValue(absInfo[ruleTarget.Axis].Value) } diff --git a/internal/mappingrules/mapping_rule_axis_combined_test.go b/internal/mappingrules/mapping_rule_axis_combined_test.go index d1a9852..631d7a0 100644 --- a/internal/mappingrules/mapping_rule_axis_combined_test.go +++ b/internal/mappingrules/mapping_rule_axis_combined_test.go @@ -1,6 +1,7 @@ package mappingrules import ( + "fmt" "testing" "github.com/holoplot/go-evdev" @@ -29,43 +30,113 @@ func TestRunnerMappingRuleAxisCombined(t *testing.T) { func (t *MappingRuleAxisCombinedTests) SetupTest() { mode := "*" t.mode = &mode - t.inputDevice = new(InputDeviceMock) - t.inputDevice.On("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{ - evdev.ABS_X: { - Minimum: 0, - Maximum: 10000, - }, - evdev.ABS_Y: { - Minimum: 0, - Maximum: 10000, - }, + + t.inputDevice = NewInputDeviceMock() + t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{ + evdev.ABS_X: {Minimum: 0, Maximum: 10000}, + evdev.ABS_Y: {Minimum: 0, Maximum: 10000}, }, nil) + t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0) + t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0) + t.outputDevice = &evdev.InputDevice{} t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0) + t.base = NewMappingRuleBase("", []string{"*"}) + + // We clear the AbsInfo call here so it can be cleanly set by the (sub-)tests + t.inputDevice.Reset() } -func (t *MappingRuleAxisCombinedTests) TestMatchEventSplitAxis() { - t.inputTargetLower, _ = NewRuleTargetAxisPartial("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0, AxisValueMin, 0) - t.inputTargetUpper, _ = NewRuleTargetAxisPartial("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0, 0, AxisValueMax) +func (t *MappingRuleAxisCombinedTests) TearDownTest() { + t.inputDevice.Reset() +} +func (t *MappingRuleAxisCombinedTests) TearDownSubTest() { + t.inputDevice.Reset() +} + +func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() { + t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{ + evdev.ABS_X: {Minimum: 0, Maximum: 10000}, + evdev.ABS_Y: {Minimum: 0, Maximum: 10000}, + }, nil) + + rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget) + t.EqualValues(0, rule.InputLower.OutputMax) + t.EqualValues(0, rule.InputUpper.OutputMin) +} + +func (t *MappingRuleAxisCombinedTests) TestMatchEvent() { rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget) t.Run("Lower Input", func() { - device, event := rule.MatchEvent(t.inputDevice, &evdev.InputEvent{Type: evdev.EV_ABS, Code: evdev.ABS_X, Value: 10000}, t.mode) - t.Equal(t.outputDevice, device) - t.EqualValues(0, event.Value) + testCases := []struct{ in, out int32 }{ + {10000, AxisValueMin}, + {0, 0}, + {5000, AxisValueMin / 2}, + } - _, event = rule.MatchEvent(t.inputDevice, &evdev.InputEvent{Type: evdev.EV_ABS, Code: evdev.ABS_X, Value: 0}, t.mode) - t.EqualValues(AxisValueMin, event.Value) + for _, testCase := range testCases { + t.Run(fmt.Sprintf("%d->%d", testCase.in, testCase.out), func() { + t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{ + evdev.ABS_X: {Minimum: 0, Maximum: 10000, Value: testCase.in}, + evdev.ABS_Y: {Minimum: 0, Maximum: 10000, Value: 0}, + }, nil) - _, event = rule.MatchEvent(t.inputDevice, &evdev.InputEvent{Type: evdev.EV_ABS, Code: evdev.ABS_X, Value: 5000}, t.mode) - t.EqualValues(0, event.Value) + device, event := rule.MatchEvent(t.inputDevice, &evdev.InputEvent{Type: evdev.EV_ABS, Code: evdev.ABS_X, Value: testCase.in}, t.mode) + + t.Equal(t.outputDevice, device) + t.InDelta(testCase.out, event.Value, 1) + }) + } }) - t.Run("Upper Input Only", func() { + t.Run("Upper Input", func() { + testCases := []struct{ in, out int32 }{ + {10000, AxisValueMax}, + {0, 0}, + {5000, AxisValueMax / 2}, + } + for _, testCase := range testCases { + t.Run(fmt.Sprintf("%d->%d", testCase.in, testCase.out), func() { + t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{ + evdev.ABS_X: {Minimum: 0, Maximum: 10000, Value: 0}, + evdev.ABS_Y: {Minimum: 0, Maximum: 10000, Value: testCase.in}, + }, nil) + + device, event := rule.MatchEvent(t.inputDevice, &evdev.InputEvent{Type: evdev.EV_ABS, Code: evdev.ABS_Y, Value: testCase.in}, t.mode) + + t.Equal(t.outputDevice, device) + t.InDelta(testCase.out, event.Value, 1) + }) + } }) + t.Run("Combined Inputs", func() { + testCases := []struct{ x, y, out int32 }{ + {0, 0, 0}, + {5000, 5000, 0}, + {10000, 10000, 0}, + {5000, 10000, AxisValueMax / 2}, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("%d,%d->%d", testCase.x, testCase.y, testCase.out), func() { + t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{ + evdev.ABS_X: {Minimum: 0, Maximum: 10000, Value: testCase.x}, + evdev.ABS_Y: {Minimum: 0, Maximum: 10000, Value: testCase.y}, + }, nil) + + device, event := rule.MatchEvent(t.inputDevice, &evdev.InputEvent{Type: evdev.EV_ABS, Code: evdev.ABS_Y, Value: testCase.y}, t.mode) + + t.Equal(t.outputDevice, device) + t.InDelta(testCase.out, event.Value, 1) + }) + } + }) + + // TODO: add tests for exception cases } diff --git a/internal/mappingrules/rule_target_axis.go b/internal/mappingrules/rule_target_axis.go index 8a19456..fece9b8 100644 --- a/internal/mappingrules/rule_target_axis.go +++ b/internal/mappingrules/rule_target_axis.go @@ -26,26 +26,6 @@ func NewRuleTargetAxis(device_name string, inverted bool, deadzoneStart int32, deadzoneEnd int32) (*RuleTargetAxis, error) { - return NewRuleTargetAxisPartial( - device_name, - device, - axis, - inverted, - deadzoneStart, - deadzoneEnd, - AxisValueMin, - AxisValueMax, - ) -} - -func NewRuleTargetAxisPartial(device_name string, - device Device, - axis evdev.EvCode, - inverted bool, - deadzoneStart int32, - deadzoneEnd int32, - minOutput int32, - maxOutput int32) (*RuleTargetAxis, error) { info, err := device.AbsInfos() @@ -83,6 +63,8 @@ func NewRuleTargetAxisPartial(device_name string, Device: device, Axis: axis, Inverted: inverted, + OutputMin: AxisValueMin, + OutputMax: AxisValueMax, DeadzoneStart: deadzoneStart, DeadzoneEnd: deadzoneEnd, deadzoneSize: deadzoneSize, @@ -125,7 +107,7 @@ func (target *RuleTargetAxis) MatchEventDeviceAndCode(device Device, event *evde // TODO: Add tests func (target *RuleTargetAxis) InDeadZone(value int32) bool { - return value >= target.DeadzoneStart && value <= target.DeadzoneEnd + return target.deadzoneSize > 0 && value >= target.DeadzoneStart && value <= target.DeadzoneEnd } // GetAxisStrength returns a float between 0.0 and 1.0, representing the proportional diff --git a/internal/mappingrules/rule_target_axis_test.go b/internal/mappingrules/rule_target_axis_test.go index 28d2fd1..5125b94 100644 --- a/internal/mappingrules/rule_target_axis_test.go +++ b/internal/mappingrules/rule_target_axis_test.go @@ -15,6 +15,10 @@ type RuleTargetAxisTests struct { call *mock.Call } +func TestRunnerRuleTargetAxisTests(t *testing.T) { + suite.Run(t, new(RuleTargetAxisTests)) +} + func (t *RuleTargetAxisTests) SetupTest() { t.mock = new(InputDeviceMock) t.call = t.mock.On("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{ @@ -68,26 +72,41 @@ func (t *RuleTargetAxisTests) TestNewRuleTargetAxis() { func (t *RuleTargetAxisTests) TestNormalizeValue() { // Basic normalization should work - 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))) + t.Run("Simple normalization", func() { + 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))) + }) // Normalization with a deadzone should work - ruleTarget, _ = NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 5000) - t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(10000))) - t.True(ruleTarget.NormalizeValue(int32(5001)) < int32(-31000)) - t.EqualValues(0, ruleTarget.NormalizeValue(int32(7500))) + t.Run("With Deadzone", func() { + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 5000) + t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(10000))) + t.True(ruleTarget.NormalizeValue(int32(5001)) < int32(-31000)) + t.EqualValues(0, ruleTarget.NormalizeValue(int32(7500))) + }) - // Normalization on an inverted axis should work - 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("Inverted", func() { + ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 0) + t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(0))) + t.Equal(AxisValueMin, ruleTarget.NormalizeValue(int32(10000))) + }) - // Normalization past the stated axis bounds should clamp - 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("Out of bounds", func() { // Normalization past the stated axis bounds should clamp + 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, 0, 0) + ruleTarget.OutputMin = 0 + ruleTarget.OutputMax = AxisValueMax + t.EqualValues(0, ruleTarget.NormalizeValue(int32(0))) + t.EqualValues(AxisValueMax, ruleTarget.NormalizeValue(int32(10000))) + t.InDelta(AxisValueMax/2, ruleTarget.NormalizeValue(int32(5000)), 10.0) + }) } func (t *RuleTargetAxisTests) TestMatchEvent() { @@ -178,7 +197,3 @@ func (t *RuleTargetAxisTests) TestGetAxisStrength() { t.Equal(1.0, ruleTarget.GetAxisStrength(0)) }) } - -func TestRunnerRuleTargetAxisTests(t *testing.T) { - suite.Run(t, new(RuleTargetAxisTests)) -} diff --git a/internal/mappingrules/test_mocks.go b/internal/mappingrules/test_mocks.go index 9838731..d8c39c5 100644 --- a/internal/mappingrules/test_mocks.go +++ b/internal/mappingrules/test_mocks.go @@ -7,9 +7,32 @@ import ( type InputDeviceMock struct { mock.Mock + calls []*mock.Call +} + +func NewInputDeviceMock() *InputDeviceMock { + m := new(InputDeviceMock) + m.calls = make([]*mock.Call, 0, 10) + return m } func (m *InputDeviceMock) AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) { args := m.Called() return args.Get(0).(map[evdev.EvCode]evdev.AbsInfo), args.Error(1) } + +// TODO: this would make a great library class functioning as a slight improvement on testify's +// mocks for instances where we want to redefine behavior more often... +// (alternately, this is possibly an anti-pattern in Go, in which case find a cleaner way to do this and remove) +func (m *InputDeviceMock) Stub(method string) *mock.Call { + call := m.On(method) + m.calls = append(m.calls, call) + return call +} + +func (m *InputDeviceMock) Reset() { + for _, call := range m.calls { + call.Unset() + } + m.calls = make([]*mock.Call, 0, 10) +}