From 2f7e11e8a2912c4875ecca6c15bff89dbbcb69c0 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 10 Jul 2025 13:06:24 -0400 Subject: [PATCH] Implement Axis targets. --- go.mod | 1 + go.sum | 2 + internal/config/make_rule_targets.go | 7 +- internal/mappingrules/math.go | 15 ++++ internal/mappingrules/rule_target_axis.go | 85 +++++++++++++-------- internal/mappingrules/rule_target_button.go | 4 +- 6 files changed, 79 insertions(+), 35 deletions(-) create mode 100644 internal/mappingrules/math.go diff --git a/go.mod b/go.mod index 2388397..9d0599a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/goccy/go-yaml v1.18.0 github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1 github.com/stretchr/testify v1.10.0 + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b ) require ( diff --git a/go.sum b/go.sum index 539dcc7..cb5a0d8 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/config/make_rule_targets.go b/internal/config/make_rule_targets.go index 36b5e3c..28afedc 100644 --- a/internal/config/make_rule_targets.go +++ b/internal/config/make_rule_targets.go @@ -24,7 +24,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev. device, eventCode, targetConfig.Inverted, - ), nil + ) } func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetAxis, error) { @@ -43,8 +43,9 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.In device, eventCode, targetConfig.Inverted, - 0, 0, 0, // TODO: replace these with real values - ), nil + targetConfig.DeadzoneStart, + targetConfig.DeadzoneEnd, + ) } func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) (*mappingrules.RuleTargetModeSelect, error) { diff --git a/internal/mappingrules/math.go b/internal/mappingrules/math.go new file mode 100644 index 0000000..bcf0487 --- /dev/null +++ b/internal/mappingrules/math.go @@ -0,0 +1,15 @@ +package mappingrules + +import ( + "golang.org/x/exp/constraints" +) + +func AbsInt[T constraints.Integer](value T) T { + return max(value, -value) +} + +// LerpInt linearly interpolates between two integer values using +// a float64 index value +func LerpInt[T constraints.Integer](min, max T, t float64) T { + return T((1-t)*float64(min) + t*float64(max)) +} diff --git a/internal/mappingrules/rule_target_axis.go b/internal/mappingrules/rule_target_axis.go index 680c6d5..2780010 100644 --- a/internal/mappingrules/rule_target_axis.go +++ b/internal/mappingrules/rule_target_axis.go @@ -1,6 +1,9 @@ package mappingrules import ( + "errors" + "fmt" + "github.com/holoplot/go-evdev" ) @@ -11,51 +14,72 @@ type RuleTargetAxis struct { Inverted bool DeadzoneStart int32 DeadzoneEnd int32 - Sensitivity float64 + Sensitivity float64 // TODO: is this even a value that makes sense? + axisSize int32 + deadzoneSize int32 } +const ( + MinAxisValue = int32(-32768) + MaxAxisValue = int32(32767) +) + func NewRuleTargetAxis(device_name string, device *evdev.InputDevice, axis evdev.EvCode, inverted bool, - deadzone_start int32, - deadzone_end int32, - sensitivity float64) *RuleTargetAxis { + deadzoneStart int32, + deadzoneEnd int32) (*RuleTargetAxis, error) { + + info, err := device.AbsInfos() + if err != nil { + return nil, err + } + + if _, ok := info[axis]; !ok { + 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 := AbsInt(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 + axisSize := info[axis].Maximum - info[axis].Minimum - deadzoneSize + + if axisSize == 0 { + return nil, errors.New("axis has size 0") + } return &RuleTargetAxis{ DeviceName: device_name, Device: device, Axis: axis, Inverted: inverted, - DeadzoneStart: deadzone_start, - DeadzoneEnd: deadzone_end, - Sensitivity: sensitivity, - } + DeadzoneStart: deadzoneStart, + DeadzoneEnd: deadzoneEnd, + deadzoneSize: deadzoneSize, + axisSize: axisSize, + }, nil } -// TODO: lots of fixes and decisions to make here. Should we normalize all axes to the same range? -// How do we handle deadzones in light of that? +// NormalizeValue takes a raw input value and converts it to a value suitable for output. +// +// Axis inputs are normalized to the full signed int32 range to match the virtual device's axis +// characteristics. +// +// 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 { - if !target.Inverted { - return value + axisStrength := float64(value-target.deadzoneSize) / float64(target.axisSize) + if target.Inverted { + axisStrength = 1.0 - axisStrength } - - axisRange := target.DeadzoneEnd - target.DeadzoneStart - axisMid := target.DeadzoneEnd - axisRange/2 - delta := value - axisMid - if delta < 0 { - delta = -delta - } - - if value < axisMid { - return axisMid + delta - } else if value > axisMid { - return axisMid - delta - } - - // If we reach here, we're either exactly at the midpoint or something - // strange has happened. Either way, just return the value. - return value + normalizedValue := LerpInt(MinAxisValue, MaxAxisValue, axisStrength) + return normalizedValue } func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.InputEvent { @@ -71,5 +95,6 @@ func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.Inpu func (target *RuleTargetAxis) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) bool { return device == target.Device && event.Type == evdev.EV_ABS && - event.Code == target.Axis + event.Code == target.Axis && + (event.Value <= target.DeadzoneStart || event.Value >= target.DeadzoneEnd) } diff --git a/internal/mappingrules/rule_target_button.go b/internal/mappingrules/rule_target_button.go index 67fe5d6..511249c 100644 --- a/internal/mappingrules/rule_target_button.go +++ b/internal/mappingrules/rule_target_button.go @@ -9,13 +9,13 @@ type RuleTargetButton struct { Inverted bool } -func NewRuleTargetButton(device_name string, device *evdev.InputDevice, code evdev.EvCode, inverted bool) *RuleTargetButton { +func NewRuleTargetButton(device_name string, device *evdev.InputDevice, code evdev.EvCode, inverted bool) (*RuleTargetButton, error) { return &RuleTargetButton{ DeviceName: device_name, Device: device, Button: code, Inverted: inverted, - } + }, nil } func (target *RuleTargetButton) NormalizeValue(value int32) int32 {