Completed implementation.
This commit is contained in:
parent
0915ea059a
commit
58abd4cc34
10 changed files with 260 additions and 66 deletions
|
@ -4,6 +4,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
||||
// MappingRuleAxisToButton represents a rule that converts an axis input into a (potentially repeating)
|
||||
|
@ -16,7 +17,10 @@ type MappingRuleAxisToButton struct {
|
|||
RepeatRateMax int
|
||||
nextEvent time.Duration
|
||||
lastEvent time.Time
|
||||
pressed bool
|
||||
repeat bool
|
||||
pressed bool // "pressed" indicates that we've sent the output button signal, but still need to send the button release
|
||||
active bool // "active" is true whenever the input is not in a deadzone
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetButton, repeatRateMin, repeatRateMax int) *MappingRuleAxisToButton {
|
||||
|
@ -28,7 +32,10 @@ func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, out
|
|||
RepeatRateMax: repeatRateMax,
|
||||
lastEvent: time.Now(),
|
||||
nextEvent: NoNextEvent,
|
||||
repeat: repeatRateMin != 0 && repeatRateMax != 0,
|
||||
pressed: false,
|
||||
active: false,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,13 +49,16 @@ func (rule *MappingRuleAxisToButton) MatchEvent(device RuleTargetDevice, event *
|
|||
// If we're inside the deadzone, unset the next event
|
||||
if rule.Input.InDeadZone(event.Value) {
|
||||
rule.nextEvent = NoNextEvent
|
||||
rule.active = false
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If we aren't repeating, we trigger the event immediately
|
||||
// TODO: we aren't using pressed correctly; that should be set *and released* in here...
|
||||
if rule.RepeatRateMin == 0 || rule.RepeatRateMax == 0 {
|
||||
rule.nextEvent = time.Millisecond
|
||||
// We also only set this if active == false, so that only one
|
||||
// event can be emitted per "active" period
|
||||
if !rule.repeat && !rule.active {
|
||||
rule.nextEvent = 0
|
||||
rule.active = true
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -56,6 +66,7 @@ func (rule *MappingRuleAxisToButton) MatchEvent(device RuleTargetDevice, 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))
|
||||
rule.active = true
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -63,21 +74,30 @@ func (rule *MappingRuleAxisToButton) MatchEvent(device RuleTargetDevice, event *
|
|||
// 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 {
|
||||
// If we pressed the button last tick, release it
|
||||
// If we pressed the button last tick, release it before doing anything else
|
||||
if rule.pressed {
|
||||
rule.pressed = false
|
||||
return rule.Output.CreateEvent(0, nil)
|
||||
}
|
||||
|
||||
// This indicates that we should not emit another event
|
||||
// If we should not emit another event,
|
||||
// we just update lastEvent for station keeping
|
||||
if rule.nextEvent == NoNextEvent {
|
||||
rule.lastEvent = time.Now()
|
||||
rule.lastEvent = rule.clock.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
if time.Now().Compare(rule.lastEvent.Add(rule.nextEvent)) > -1 {
|
||||
rule.lastEvent = time.Now()
|
||||
if rule.clock.Now().Compare(rule.lastEvent.Add(rule.nextEvent)) > -1 {
|
||||
rule.lastEvent = rule.clock.Now()
|
||||
rule.pressed = true
|
||||
|
||||
// The default case here is to leave nextEvent at whatever
|
||||
// it has been set to by MatchEvent. Since nextEvent is a delta,
|
||||
// this will naturally cause the repeat to happen
|
||||
if !rule.repeat {
|
||||
rule.nextEvent = NoNextEvent
|
||||
}
|
||||
|
||||
return rule.Output.CreateEvent(1, nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
|
@ -36,52 +37,150 @@ func (t *MappingRuleAxisToButtonTests) SetupTest() {
|
|||
}
|
||||
|
||||
func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0)
|
||||
|
||||
// A valid input should set a nextevent
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 1001,
|
||||
}, t.mode)
|
||||
t.NotEqual(NoNextEvent, testRule.nextEvent)
|
||||
t.Run("No Repeat", func() {
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0)
|
||||
|
||||
// And a deadzone value should clear it
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 500,
|
||||
}, t.mode)
|
||||
t.Equal(NoNextEvent, testRule.nextEvent)
|
||||
t.Run("Valid Input", func() {
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 1001,
|
||||
}, t.mode)
|
||||
t.NotEqual(NoNextEvent, testRule.nextEvent)
|
||||
})
|
||||
|
||||
testRule = NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250)
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 10000,
|
||||
}, t.mode)
|
||||
t.Equal(time.Duration(250*time.Millisecond), testRule.nextEvent)
|
||||
t.Run("Deadzone Input", func() {
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 500,
|
||||
}, t.mode)
|
||||
t.Equal(NoNextEvent, testRule.nextEvent)
|
||||
})
|
||||
})
|
||||
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 1001,
|
||||
}, t.mode)
|
||||
t.True(testRule.nextEvent > time.Duration(700*time.Millisecond))
|
||||
t.Run("Repeat", func() {
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250)
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 10000,
|
||||
}, t.mode)
|
||||
t.Equal(time.Duration(250*time.Millisecond), testRule.nextEvent)
|
||||
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 5500,
|
||||
}, t.mode)
|
||||
t.Equal(time.Duration(500*time.Millisecond), testRule.nextEvent)
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 1001,
|
||||
}, t.mode)
|
||||
t.True(testRule.nextEvent > time.Duration(700*time.Millisecond))
|
||||
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
Value: 5500,
|
||||
}, t.mode)
|
||||
t.Equal(time.Duration(500*time.Millisecond), testRule.nextEvent)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: to add TimerEvent tests we need to use an interface to mock time.
|
||||
// func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
||||
// // STUB
|
||||
// }
|
||||
func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
||||
t.Run("No Repeat", func() {
|
||||
// Get event if called immediately
|
||||
t.Run("Event is available immediately", func() {
|
||||
testRule, _ := buildTimerRule(t, 0, 0, 0)
|
||||
|
||||
event := testRule.TimerEvent()
|
||||
|
||||
t.EqualValues(1, event.Value)
|
||||
t.Equal(true, testRule.pressed)
|
||||
})
|
||||
|
||||
// Off event on second call
|
||||
t.Run("Event emits off on second call", func() {
|
||||
testRule, _ := buildTimerRule(t, 0, 0, 0)
|
||||
|
||||
testRule.TimerEvent()
|
||||
event := testRule.TimerEvent()
|
||||
|
||||
t.EqualValues(0, event.Value)
|
||||
t.Equal(false, testRule.pressed)
|
||||
})
|
||||
|
||||
// No further event, even if we wait a while
|
||||
t.Run("Additional events are not emitted while still active.", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 0, 0, 0)
|
||||
|
||||
testRule.TimerEvent()
|
||||
testRule.TimerEvent()
|
||||
|
||||
mockClock.Advance(10 * time.Millisecond)
|
||||
event := testRule.TimerEvent()
|
||||
t.Nil(event)
|
||||
t.Equal(false, testRule.pressed)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Repeat", func() {
|
||||
t.Run("No event if called immediately", func() {
|
||||
testRule, _ := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
event := testRule.TimerEvent()
|
||||
t.Nil(event)
|
||||
})
|
||||
|
||||
t.Run("No event after 49ms", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
mockClock.Advance(49 * time.Millisecond)
|
||||
|
||||
event := testRule.TimerEvent()
|
||||
|
||||
t.Nil(event)
|
||||
})
|
||||
|
||||
t.Run("Event after 50ms", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
mockClock.Advance(50 * time.Millisecond)
|
||||
|
||||
event := testRule.TimerEvent()
|
||||
|
||||
t.EqualValues(1, event.Value)
|
||||
t.Equal(true, testRule.pressed)
|
||||
})
|
||||
|
||||
t.Run("Additional event at 100ms", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
|
||||
mockClock.Advance(50 * time.Millisecond)
|
||||
testRule.TimerEvent()
|
||||
testRule.TimerEvent()
|
||||
|
||||
mockClock.Advance(50 * time.Millisecond)
|
||||
event := testRule.TimerEvent()
|
||||
|
||||
t.NotNil(event)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) {
|
||||
suite.Run(t, new(MappingRuleAxisToButtonTests))
|
||||
}
|
||||
|
||||
// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock
|
||||
func buildTimerRule(t *MappingRuleAxisToButtonTests,
|
||||
repeatMin,
|
||||
repeatMax int,
|
||||
nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) {
|
||||
|
||||
mockClock := clockwork.NewFakeClock()
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, repeatMin, repeatMax)
|
||||
testRule.clock = mockClock
|
||||
testRule.lastEvent = testRule.clock.Now()
|
||||
testRule.nextEvent = nextEvent
|
||||
if nextEvent != NoNextEvent {
|
||||
testRule.active = true
|
||||
}
|
||||
return testRule, mockClock
|
||||
}
|
||||
|
|
|
@ -5,14 +5,13 @@ import (
|
|||
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
||||
// TODO: add tests
|
||||
|
||||
// TODO: deadzones seem to calculate correctly in one direction but not the other when computing axis strength...
|
||||
|
||||
// MappingRuleAxisToRelaxis represents a rule that converts an axis input into a (potentially repeating)
|
||||
// button output.
|
||||
// relative axis output. This is most commonly used to generate mouse output events
|
||||
type MappingRuleAxisToRelaxis struct {
|
||||
MappingRuleBase
|
||||
Input *RuleTargetAxis
|
||||
|
@ -22,6 +21,7 @@ type MappingRuleAxisToRelaxis struct {
|
|||
Increment int32
|
||||
nextEvent time.Duration
|
||||
lastEvent time.Time
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewMappingRuleAxisToRelaxis(
|
||||
|
@ -39,6 +39,7 @@ func NewMappingRuleAxisToRelaxis(
|
|||
Increment: int32(increment),
|
||||
lastEvent: time.Now(),
|
||||
nextEvent: NoNextEvent,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,12 +82,12 @@ func (rule *MappingRuleAxisToRelaxis) MatchEvent(
|
|||
func (rule *MappingRuleAxisToRelaxis) TimerEvent() *evdev.InputEvent {
|
||||
// This indicates that we should not emit another event
|
||||
if rule.nextEvent == NoNextEvent {
|
||||
rule.lastEvent = time.Now()
|
||||
rule.lastEvent = rule.clock.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
if time.Now().Compare(rule.lastEvent.Add(rule.nextEvent)) > -1 {
|
||||
rule.lastEvent = time.Now()
|
||||
if rule.clock.Now().Compare(rule.lastEvent.Add(rule.nextEvent)) > -1 {
|
||||
rule.lastEvent = rule.clock.Now()
|
||||
return rule.Output.CreateEvent(rule.Increment, nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -77,12 +77,7 @@ func NewRuleTargetAxis(device_name string,
|
|||
// in the deadzone, among other things.
|
||||
func (target *RuleTargetAxis) NormalizeValue(value int32) int32 {
|
||||
axisStrength := target.GetAxisStrength(value)
|
||||
|
||||
if target.Inverted {
|
||||
axisStrength = 1.0 - axisStrength
|
||||
}
|
||||
normalizedValue := LerpInt(AxisValueMin, AxisValueMax, axisStrength)
|
||||
return normalizedValue
|
||||
return LerpInt(AxisValueMin, AxisValueMax, axisStrength)
|
||||
}
|
||||
|
||||
func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.InputEvent {
|
||||
|
@ -111,6 +106,16 @@ func (target *RuleTargetAxis) InDeadZone(value int32) bool {
|
|||
return 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 {
|
||||
return float64(value-target.deadzoneSize) / float64(target.axisSize)
|
||||
if value > target.DeadzoneEnd {
|
||||
value -= target.deadzoneSize
|
||||
}
|
||||
strength := float64(value) / float64(target.axisSize)
|
||||
if target.Inverted {
|
||||
strength = 1.0 - strength
|
||||
}
|
||||
return strength
|
||||
}
|
||||
|
|
|
@ -135,6 +135,50 @@ func (t *RuleTargetAxisTests) TestCreateEvent() {
|
|||
t.EqualValues(expected, ruleTarget.CreateEvent(testValue, nil))
|
||||
}
|
||||
|
||||
func (t *RuleTargetAxisTests) TestGetAxisStrength() {
|
||||
t.Run("With no deadzone", func() {
|
||||
ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0)
|
||||
t.Equal(0.0, ruleTarget.GetAxisStrength(0))
|
||||
t.Equal(1.0, ruleTarget.GetAxisStrength(10000))
|
||||
t.Equal(0.5, ruleTarget.GetAxisStrength(5000))
|
||||
})
|
||||
|
||||
t.Run("With low deadzone", func() {
|
||||
ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 5000)
|
||||
t.InDelta(0.0, ruleTarget.GetAxisStrength(5001), 0.01)
|
||||
t.InDelta(0.5, ruleTarget.GetAxisStrength(7500), 0.01)
|
||||
t.Equal(1.0, ruleTarget.GetAxisStrength(10000))
|
||||
})
|
||||
|
||||
t.Run("With high deadzone", func() {
|
||||
ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 5000, 10000)
|
||||
t.Equal(0.0, ruleTarget.GetAxisStrength(0))
|
||||
t.InDelta(0.5, ruleTarget.GetAxisStrength(2500), 0.01)
|
||||
t.InDelta(1.0, ruleTarget.GetAxisStrength(4999), 0.01)
|
||||
})
|
||||
|
||||
t.Run("Inverted", func() {
|
||||
ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 0)
|
||||
t.Equal(1.0, ruleTarget.GetAxisStrength(0))
|
||||
t.Equal(0.5, ruleTarget.GetAxisStrength(5000))
|
||||
t.Equal(0.0, ruleTarget.GetAxisStrength(10000))
|
||||
})
|
||||
|
||||
t.Run("Inverted with low deadzone", func() {
|
||||
ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 5000)
|
||||
t.InDelta(1.0, ruleTarget.GetAxisStrength(5001), 0.01)
|
||||
t.InDelta(0.5, ruleTarget.GetAxisStrength(7500), 0.01)
|
||||
t.Equal(0.0, ruleTarget.GetAxisStrength(10000))
|
||||
})
|
||||
|
||||
t.Run("Inverted with high deadzone", func() {
|
||||
ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 5000, 10000)
|
||||
t.InDelta(0.0, ruleTarget.GetAxisStrength(4999), 0.01)
|
||||
t.InDelta(0.5, ruleTarget.GetAxisStrength(2500), 0.01)
|
||||
t.Equal(1.0, ruleTarget.GetAxisStrength(0))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunnerRuleTargetAxisTests(t *testing.T) {
|
||||
suite.Run(t, new(RuleTargetAxisTests))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue