Implement combined axis logic and tests.

This commit is contained in:
Anna Rose Wiggins 2025-07-25 17:51:24 -04:00
parent a7e78c33f3
commit 49292ff13f
5 changed files with 192 additions and 65 deletions

View file

@ -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) {
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)
}

View file

@ -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)
_, event = rule.MatchEvent(t.inputDevice, &evdev.InputEvent{Type: evdev.EV_ABS, Code: evdev.ABS_X, Value: 0}, t.mode)
t.EqualValues(AxisValueMin, event.Value)
_, event = rule.MatchEvent(t.inputDevice, &evdev.InputEvent{Type: evdev.EV_ABS, Code: evdev.ABS_X, Value: 5000}, t.mode)
t.EqualValues(0, event.Value)
})
t.Run("Upper Input Only", func() {
})
testCases := []struct{ in, out int32 }{
{10000, AxisValueMin},
{0, 0},
{5000, AxisValueMin / 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: testCase.in},
evdev.ABS_Y: {Minimum: 0, Maximum: 10000, Value: 0},
}, nil)
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", 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
}

View file

@ -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

View file

@ -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
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.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.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.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))
}

View file

@ -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)
}