joyful/internal/mappingrules/rule_target_axis.go

198 lines
5.7 KiB
Go

package mappingrules
import (
"errors"
"fmt"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
"github.com/holoplot/go-evdev"
)
type RuleTargetAxis struct {
DeviceName string
Device Device
Axis evdev.EvCode
Inverted bool
DeadzoneStart int32
DeadzoneEnd int32
OutputMin int32
OutputMax int32
axisSize int32
deadzoneSize int32
}
func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, devs map[string]Device) (*RuleTargetAxis, error) {
device, ok := devs[targetConfig.Device]
if !ok {
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart {
return nil, errors.New("deadzone_end must be greater than deadzone_start")
}
eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixAxis)
if err != nil {
return nil, err
}
deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode)
if err != nil {
return nil, err
}
return NewRuleTargetAxis(
targetConfig.Device,
device,
eventCode,
targetConfig.Inverted,
deadzoneStart,
deadzoneEnd,
)
}
// calculateDeadzones produces the deadzone start and end values in absolute terms
func calculateDeadzones(targetConfig configparser.RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) {
var deadzoneStart, deadzoneEnd int32
deadzoneStart = 0
deadzoneEnd = 0
if targetConfig.DeadzoneStart != 0 || targetConfig.DeadzoneEnd != 0 {
return targetConfig.DeadzoneStart, targetConfig.DeadzoneEnd, nil
}
var min, max int32
absInfoMap, err := device.AbsInfos()
if err != nil {
min = AxisValueMin
max = AxisValueMax
} else {
absInfo := absInfoMap[axis]
min = absInfo.Minimum
max = absInfo.Maximum
}
if targetConfig.DeadzoneCenter < min || targetConfig.DeadzoneCenter > max {
return 0, 0, fmt.Errorf("deadzone_center '%d' is out of bounds", targetConfig.DeadzoneCenter)
}
switch {
case targetConfig.DeadzoneSize != 0:
deadzoneStart = targetConfig.DeadzoneCenter - targetConfig.DeadzoneSize/2
deadzoneEnd = targetConfig.DeadzoneCenter + targetConfig.DeadzoneSize/2
case targetConfig.DeadzoneSizePercent != 0:
deadzoneSize := (max - min) / targetConfig.DeadzoneSizePercent
deadzoneStart = targetConfig.DeadzoneCenter - deadzoneSize/2
deadzoneEnd = targetConfig.DeadzoneCenter + deadzoneSize/2
}
deadzoneStart, deadzoneEnd = clampAndShift(deadzoneStart, deadzoneEnd, min, max)
return deadzoneStart, deadzoneEnd, nil
}
func NewRuleTargetAxis(device_name string,
device Device,
axis evdev.EvCode,
inverted bool,
deadzoneStart int32,
deadzoneEnd int32) (*RuleTargetAxis, error) {
info, err := device.AbsInfos()
if err != nil {
// If we can't get AbsInfo (for example, we're a virtual device)
// we set the bounds to the maximum allowable
info = map[evdev.EvCode]evdev.AbsInfo{
axis: {
Minimum: AxisValueMin,
Maximum: AxisValueMax,
},
}
}
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 := Abs(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,
OutputMin: AxisValueMin,
OutputMax: AxisValueMax,
DeadzoneStart: deadzoneStart,
DeadzoneEnd: deadzoneEnd,
deadzoneSize: deadzoneSize,
axisSize: axisSize,
}, nil
}
// 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 {
axisStrength := target.GetAxisStrength(value)
return LerpInt(target.OutputMin, target.OutputMax, axisStrength)
}
func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.InputEvent {
value = Clamp(value, AxisValueMin, AxisValueMax)
return &evdev.InputEvent{
Type: evdev.EV_ABS,
Code: target.Axis,
Value: value,
}
}
func (target *RuleTargetAxis) MatchEvent(device Device, event *evdev.InputEvent) bool {
return target.MatchEventDeviceAndCode(device, event) &&
!target.InDeadZone(event.Value)
}
// TODO: Add tests
func (target *RuleTargetAxis) MatchEventDeviceAndCode(device Device, event *evdev.InputEvent) bool {
return device == target.Device &&
event.Type == evdev.EV_ABS &&
event.Code == target.Axis
}
// TODO: Add tests
func (target *RuleTargetAxis) InDeadZone(value int32) bool {
return target.deadzoneSize > 0 && value >= target.DeadzoneStart && value <= target.DeadzoneEnd
}
// GetAxisStrength returns a float between 0.0 and 1.0, representing the proportional
// position along the axis' full range. (after factoring in deadzones)
// Calling this function with `value` inside the deadzone range will produce undefined behavior
func (target *RuleTargetAxis) GetAxisStrength(value int32) float64 {
if value > target.DeadzoneEnd {
value -= target.deadzoneSize
}
strength := float64(value) / float64(target.axisSize)
if target.Inverted {
strength = 1.0 - strength
}
return strength
}