Implement Axis targets.

This commit is contained in:
Anna Rose Wiggins 2025-07-10 13:06:24 -04:00
parent ff38db6596
commit 2f7e11e8a2
6 changed files with 79 additions and 35 deletions

1
go.mod
View file

@ -6,6 +6,7 @@ require (
github.com/goccy/go-yaml v1.18.0 github.com/goccy/go-yaml v1.18.0
github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1 github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
) )
require ( require (

2
go.sum
View file

@ -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/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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -24,7 +24,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev.
device, device,
eventCode, eventCode,
targetConfig.Inverted, targetConfig.Inverted,
), nil )
} }
func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetAxis, error) { 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, device,
eventCode, eventCode,
targetConfig.Inverted, targetConfig.Inverted,
0, 0, 0, // TODO: replace these with real values targetConfig.DeadzoneStart,
), nil targetConfig.DeadzoneEnd,
)
} }
func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) (*mappingrules.RuleTargetModeSelect, error) { func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) (*mappingrules.RuleTargetModeSelect, error) {

View file

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

View file

@ -1,6 +1,9 @@
package mappingrules package mappingrules
import ( import (
"errors"
"fmt"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
@ -11,51 +14,72 @@ type RuleTargetAxis struct {
Inverted bool Inverted bool
DeadzoneStart int32 DeadzoneStart int32
DeadzoneEnd 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, func NewRuleTargetAxis(device_name string,
device *evdev.InputDevice, device *evdev.InputDevice,
axis evdev.EvCode, axis evdev.EvCode,
inverted bool, inverted bool,
deadzone_start int32, deadzoneStart int32,
deadzone_end int32, deadzoneEnd int32) (*RuleTargetAxis, error) {
sensitivity float64) *RuleTargetAxis {
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{ return &RuleTargetAxis{
DeviceName: device_name, DeviceName: device_name,
Device: device, Device: device,
Axis: axis, Axis: axis,
Inverted: inverted, Inverted: inverted,
DeadzoneStart: deadzone_start, DeadzoneStart: deadzoneStart,
DeadzoneEnd: deadzone_end, DeadzoneEnd: deadzoneEnd,
Sensitivity: sensitivity, deadzoneSize: deadzoneSize,
} axisSize: axisSize,
}, nil
} }
// TODO: lots of fixes and decisions to make here. Should we normalize all axes to the same range? // NormalizeValue takes a raw input value and converts it to a value suitable for output.
// How do we handle deadzones in light of that? //
// 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 { func (target *RuleTargetAxis) NormalizeValue(value int32) int32 {
if !target.Inverted { axisStrength := float64(value-target.deadzoneSize) / float64(target.axisSize)
return value if target.Inverted {
axisStrength = 1.0 - axisStrength
} }
normalizedValue := LerpInt(MinAxisValue, MaxAxisValue, axisStrength)
axisRange := target.DeadzoneEnd - target.DeadzoneStart return normalizedValue
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
} }
func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.InputEvent { 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 { func (target *RuleTargetAxis) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) bool {
return device == target.Device && return device == target.Device &&
event.Type == evdev.EV_ABS && event.Type == evdev.EV_ABS &&
event.Code == target.Axis event.Code == target.Axis &&
(event.Value <= target.DeadzoneStart || event.Value >= target.DeadzoneEnd)
} }

View file

@ -9,13 +9,13 @@ type RuleTargetButton struct {
Inverted bool 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{ return &RuleTargetButton{
DeviceName: device_name, DeviceName: device_name,
Device: device, Device: device,
Button: code, Button: code,
Inverted: inverted, Inverted: inverted,
} }, nil
} }
func (target *RuleTargetButton) NormalizeValue(value int32) int32 { func (target *RuleTargetButton) NormalizeValue(value int32) int32 {