From 47fac539da74437fbe2b8fe2351dbbacb9382144 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 10 Jul 2025 23:18:34 -0400 Subject: [PATCH] Implement MappingRuleAxisToButton. --- internal/mappingrules/interfaces.go | 5 ++ .../mapping_rule_axis_to_button.go | 78 ++++++++++++++----- internal/mappingrules/rule_target_axis.go | 25 ++++-- 3 files changed, 81 insertions(+), 27 deletions(-) diff --git a/internal/mappingrules/interfaces.go b/internal/mappingrules/interfaces.go index 316ec44..8ea4f39 100644 --- a/internal/mappingrules/interfaces.go +++ b/internal/mappingrules/interfaces.go @@ -33,3 +33,8 @@ type RuleTarget interface { type RuleTargetDevice interface { AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) } + +const ( + AxisValueMin = int32(-32768) + AxisValueMax = int32(32767) +) diff --git a/internal/mappingrules/mapping_rule_axis_to_button.go b/internal/mappingrules/mapping_rule_axis_to_button.go index f28703b..ec4145c 100644 --- a/internal/mappingrules/mapping_rule_axis_to_button.go +++ b/internal/mappingrules/mapping_rule_axis_to_button.go @@ -6,45 +6,85 @@ import ( "github.com/holoplot/go-evdev" ) -// TODO: This whole file is still WIP +// MappingRuleAxisToButton represents a rule that converts an axis input into a (potentially repeating) +// button output. +// +// TODO: Add Tests type MappingRuleAxisToButton struct { MappingRuleBase - Input *RuleTargetAxis - Output *RuleTargetButton - RepeatSpeedMin int32 - RepeatSpeedMax int32 - lastValue int32 - lastEvent time.Time + Input *RuleTargetAxis + Output *RuleTargetButton + RepeatRateMin int + RepeatRateMax int + nextEvent time.Duration + lastEvent time.Time + pressed bool +} + +const ( + NoNextEvent = time.Duration(-1) +) + +func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetButton, repeatRateMin, repeatRateMax int) *MappingRuleAxisToButton { + return &MappingRuleAxisToButton{ + MappingRuleBase: base, + Input: input, + Output: output, + RepeatRateMin: repeatRateMin, + RepeatRateMax: repeatRateMax, + lastEvent: time.Now(), + nextEvent: NoNextEvent, + pressed: false, + } } func (rule *MappingRuleAxisToButton) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { + // TODO: we're using this instead of the RuleTarget's MatchEvent because we need to check inside the deadzone + // We should find a cleaner way to do this... if !rule.MappingRuleBase.modeCheck(mode) || - !rule.Input.MatchEvent(device, event) { + !rule.Input.MatchEventDeviceAndCode(device, event) { return nil, nil } - // set the last value to the normalized input value - rule.lastValue = rule.Input.NormalizeValue(event.Value) + // If we're inside the deadzone, unset the next event + if rule.Input.InDeadZone(event.Value) { + rule.nextEvent = NoNextEvent + return nil, nil + } + + // If we aren't repeating, we trigger the event immediately + if rule.RepeatRateMin == 0 || rule.RepeatRateMax == 0 { + rule.nextEvent = time.Millisecond + return nil, nil + } + + // use the axis value and the repeat rate to set a target time until the next event + strength := 1.0 - rule.Input.GetAxisStrength(event.Value) + rate := int64(LerpInt(rule.RepeatRateMax, rule.RepeatRateMin, strength)) + rule.nextEvent = time.Duration(rate * int64(time.Millisecond)) + return nil, nil } // TimerEvent returns an event when enough time has passed (compared to the last recorded axis value) // to emit an event. func (rule *MappingRuleAxisToButton) TimerEvent() *evdev.InputEvent { - // This is tighter coupling than we'd like, but it will do for now. - // TODO: maybe it would be better to just be more declarative about event types and their inputs and outputs. - if rule.lastValue < rule.Input.DeadzoneStart { + // If we pressed the button last tick, release it + if rule.pressed { + rule.pressed = false + return rule.Output.CreateEvent(0, nil) + } + + // This indicates that we should not emit another event + if rule.nextEvent == -1 { rule.lastEvent = time.Now() return nil } - // calculate target time until next event press - // nextEvent := rule.LastEvent + (rule.LastValue) - - // TODO: figure out what the condition should be - if false { - // TODO: emit event + if time.Now().Compare(rule.lastEvent.Add(rule.nextEvent)) > -1 { rule.lastEvent = time.Now() + rule.pressed = true + return rule.Output.CreateEvent(1, nil) } return nil diff --git a/internal/mappingrules/rule_target_axis.go b/internal/mappingrules/rule_target_axis.go index 8701b2c..5808e43 100644 --- a/internal/mappingrules/rule_target_axis.go +++ b/internal/mappingrules/rule_target_axis.go @@ -18,11 +18,6 @@ type RuleTargetAxis struct { deadzoneSize int32 } -const ( - AxisValueMin = int32(-32768) - AxisValueMax = int32(32767) -) - func NewRuleTargetAxis(device_name string, device RuleTargetDevice, axis evdev.EvCode, @@ -81,7 +76,7 @@ func NewRuleTargetAxis(device_name string, // 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 := float64(value-target.deadzoneSize) / float64(target.axisSize) + axisStrength := target.GetAxisStrength(value) if target.Inverted { axisStrength = 1.0 - axisStrength @@ -100,8 +95,22 @@ func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.Inpu } func (target *RuleTargetAxis) MatchEvent(device RuleTargetDevice, event *evdev.InputEvent) bool { + return target.MatchEventDeviceAndCode(device, event) && + !target.InDeadZone(event.Value) +} + +// TODO: Add tests +func (target *RuleTargetAxis) MatchEventDeviceAndCode(device RuleTargetDevice, event *evdev.InputEvent) bool { return device == target.Device && event.Type == evdev.EV_ABS && - event.Code == target.Axis && - (event.Value < target.DeadzoneStart || event.Value > target.DeadzoneEnd) + event.Code == target.Axis +} + +// TODO: Add tests +func (target *RuleTargetAxis) InDeadZone(value int32) bool { + return value >= target.DeadzoneStart && value <= target.DeadzoneEnd +} + +func (target *RuleTargetAxis) GetAxisStrength(value int32) float64 { + return float64(value-target.deadzoneSize) / float64(target.axisSize) }