Implement axis targets, axis -> button and axis -> relative axis mappings. #1

Merged
anna merged 11 commits from axis-features into main 2025-07-15 19:55:20 +00:00
6 changed files with 79 additions and 35 deletions
Showing only changes of commit 2f7e11e8a2 - Show all commits

1
go.mod
View file

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

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/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=

View file

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

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

View file

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