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 }