Completed implementation.
This commit is contained in:
parent
0915ea059a
commit
58abd4cc34
10 changed files with 260 additions and 66 deletions
|
@ -95,7 +95,8 @@ func main() {
|
||||||
case ChannelEventInput:
|
case ChannelEventInput:
|
||||||
switch channelEvent.Event.Type {
|
switch channelEvent.Event.Type {
|
||||||
case evdev.EV_SYN:
|
case evdev.EV_SYN:
|
||||||
// We've received a SYN_REPORT, so now we send all of our pending events
|
// We've received a SYN_REPORT, so now we send all pending events; since SYN_REPORTs
|
||||||
|
// might come from multiple input devices, we'll always flush, just to be sure.
|
||||||
for _, buffer := range vBuffersByName {
|
for _, buffer := range vBuffersByName {
|
||||||
buffer.SendEvents()
|
buffer.SendEvents()
|
||||||
}
|
}
|
||||||
|
@ -114,6 +115,8 @@ func main() {
|
||||||
case ChannelEventTimer:
|
case ChannelEventTimer:
|
||||||
// Timer events give us the device and event to use directly
|
// Timer events give us the device and event to use directly
|
||||||
vBuffersByDevice[channelEvent.Device].AddEvent(channelEvent.Event)
|
vBuffersByDevice[channelEvent.Device].AddEvent(channelEvent.Event)
|
||||||
|
// If we get a timer event, flush the output device buffer immediately
|
||||||
|
vBuffersByDevice[channelEvent.Device].SendEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -5,6 +5,7 @@ go 1.24.4
|
||||||
require (
|
require (
|
||||||
github.com/goccy/go-yaml v1.18.0
|
github.com/goccy/go-yaml v1.18.0
|
||||||
github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1
|
github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1
|
||||||
|
github.com/jonboulle/clockwork v0.5.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -4,6 +4,8 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1 h1:92OsBIf5KB1Tatx+uUGOhah73jyNUrt7DmfDRXXJ5Xo=
|
github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1 h1:92OsBIf5KB1Tatx+uUGOhah73jyNUrt7DmfDRXXJ5Xo=
|
||||||
github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||||
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
|
|
@ -35,6 +35,7 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
|
||||||
map[evdev.EvType][]evdev.EvCode{
|
map[evdev.EvType][]evdev.EvCode{
|
||||||
evdev.EV_KEY: makeButtons(int(deviceConfig.Buttons)),
|
evdev.EV_KEY: makeButtons(int(deviceConfig.Buttons)),
|
||||||
evdev.EV_ABS: makeAxes(int(deviceConfig.Axes)),
|
evdev.EV_ABS: makeAxes(int(deviceConfig.Axes)),
|
||||||
|
evdev.EV_REL: makeRelativeAxes(deviceConfig.RelativeAxes),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,3 +117,20 @@ func makeAxes(numAxes int) []evdev.EvCode {
|
||||||
|
|
||||||
return axes
|
return axes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeRelativeAxes(axes []string) []evdev.EvCode {
|
||||||
|
codes := make([]evdev.EvCode, 0)
|
||||||
|
|
||||||
|
for _, axis := range axes {
|
||||||
|
code, ok := evdev.RELFromString[axis]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
logger.Logf("Relative axis '%s' invalid. Skipping.", axis)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
codes = append(codes, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
return codes
|
||||||
|
}
|
||||||
|
|
|
@ -16,12 +16,13 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceConfig struct {
|
type DeviceConfig struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
DeviceName string `yaml:"device_name,omitempty"`
|
DeviceName string `yaml:"device_name,omitempty"`
|
||||||
Uuid string `yaml:"uuid,omitempty"`
|
Uuid string `yaml:"uuid,omitempty"`
|
||||||
Buttons int `yaml:"buttons,omitempty"`
|
Buttons int `yaml:"buttons,omitempty"`
|
||||||
Axes int `yaml:"axes,omitempty"`
|
Axes int `yaml:"axes,omitempty"`
|
||||||
|
RelativeAxes []string `yaml:"rel_axes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleConfig struct {
|
type RuleConfig struct {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MappingRuleAxisToButton represents a rule that converts an axis input into a (potentially repeating)
|
// MappingRuleAxisToButton represents a rule that converts an axis input into a (potentially repeating)
|
||||||
|
@ -16,7 +17,10 @@ type MappingRuleAxisToButton struct {
|
||||||
RepeatRateMax int
|
RepeatRateMax int
|
||||||
nextEvent time.Duration
|
nextEvent time.Duration
|
||||||
lastEvent time.Time
|
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 {
|
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,
|
RepeatRateMax: repeatRateMax,
|
||||||
lastEvent: time.Now(),
|
lastEvent: time.Now(),
|
||||||
nextEvent: NoNextEvent,
|
nextEvent: NoNextEvent,
|
||||||
|
repeat: repeatRateMin != 0 && repeatRateMax != 0,
|
||||||
pressed: false,
|
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 we're inside the deadzone, unset the next event
|
||||||
if rule.Input.InDeadZone(event.Value) {
|
if rule.Input.InDeadZone(event.Value) {
|
||||||
rule.nextEvent = NoNextEvent
|
rule.nextEvent = NoNextEvent
|
||||||
|
rule.active = false
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we aren't repeating, we trigger the event immediately
|
// 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...
|
// We also only set this if active == false, so that only one
|
||||||
if rule.RepeatRateMin == 0 || rule.RepeatRateMax == 0 {
|
// event can be emitted per "active" period
|
||||||
rule.nextEvent = time.Millisecond
|
if !rule.repeat && !rule.active {
|
||||||
|
rule.nextEvent = 0
|
||||||
|
rule.active = true
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +66,7 @@ func (rule *MappingRuleAxisToButton) MatchEvent(device RuleTargetDevice, event *
|
||||||
strength := 1.0 - rule.Input.GetAxisStrength(event.Value)
|
strength := 1.0 - rule.Input.GetAxisStrength(event.Value)
|
||||||
rate := int64(LerpInt(rule.RepeatRateMax, rule.RepeatRateMin, strength))
|
rate := int64(LerpInt(rule.RepeatRateMax, rule.RepeatRateMin, strength))
|
||||||
rule.nextEvent = time.Duration(rate * int64(time.Millisecond))
|
rule.nextEvent = time.Duration(rate * int64(time.Millisecond))
|
||||||
|
rule.active = true
|
||||||
|
|
||||||
return nil, nil
|
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)
|
// TimerEvent returns an event when enough time has passed (compared to the last recorded axis value)
|
||||||
// to emit an event.
|
// to emit an event.
|
||||||
func (rule *MappingRuleAxisToButton) TimerEvent() *evdev.InputEvent {
|
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 {
|
if rule.pressed {
|
||||||
rule.pressed = false
|
rule.pressed = false
|
||||||
return rule.Output.CreateEvent(0, nil)
|
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 {
|
if rule.nextEvent == NoNextEvent {
|
||||||
rule.lastEvent = time.Now()
|
rule.lastEvent = rule.clock.Now()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().Compare(rule.lastEvent.Add(rule.nextEvent)) > -1 {
|
if rule.clock.Now().Compare(rule.lastEvent.Add(rule.nextEvent)) > -1 {
|
||||||
rule.lastEvent = time.Now()
|
rule.lastEvent = rule.clock.Now()
|
||||||
rule.pressed = true
|
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)
|
return rule.Output.CreateEvent(1, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,52 +37,150 @@ func (t *MappingRuleAxisToButtonTests) SetupTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
||||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0)
|
|
||||||
|
|
||||||
// A valid input should set a nextevent
|
// A valid input should set a nextevent
|
||||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
t.Run("No Repeat", func() {
|
||||||
Type: evdev.EV_ABS,
|
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0)
|
||||||
Code: evdev.ABS_X,
|
|
||||||
Value: 1001,
|
|
||||||
}, t.mode)
|
|
||||||
t.NotEqual(NoNextEvent, testRule.nextEvent)
|
|
||||||
|
|
||||||
// And a deadzone value should clear it
|
t.Run("Valid Input", func() {
|
||||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||||
Type: evdev.EV_ABS,
|
Type: evdev.EV_ABS,
|
||||||
Code: evdev.ABS_X,
|
Code: evdev.ABS_X,
|
||||||
Value: 500,
|
Value: 1001,
|
||||||
}, t.mode)
|
}, t.mode)
|
||||||
t.Equal(NoNextEvent, testRule.nextEvent)
|
t.NotEqual(NoNextEvent, testRule.nextEvent)
|
||||||
|
})
|
||||||
|
|
||||||
testRule = NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250)
|
t.Run("Deadzone Input", func() {
|
||||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||||
Type: evdev.EV_ABS,
|
Type: evdev.EV_ABS,
|
||||||
Code: evdev.ABS_X,
|
Code: evdev.ABS_X,
|
||||||
Value: 10000,
|
Value: 500,
|
||||||
}, t.mode)
|
}, t.mode)
|
||||||
t.Equal(time.Duration(250*time.Millisecond), testRule.nextEvent)
|
t.Equal(NoNextEvent, testRule.nextEvent)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
t.Run("Repeat", func() {
|
||||||
Type: evdev.EV_ABS,
|
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250)
|
||||||
Code: evdev.ABS_X,
|
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||||
Value: 1001,
|
Type: evdev.EV_ABS,
|
||||||
}, t.mode)
|
Code: evdev.ABS_X,
|
||||||
t.True(testRule.nextEvent > time.Duration(700*time.Millisecond))
|
Value: 10000,
|
||||||
|
}, t.mode)
|
||||||
|
t.Equal(time.Duration(250*time.Millisecond), testRule.nextEvent)
|
||||||
|
|
||||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||||
Type: evdev.EV_ABS,
|
Type: evdev.EV_ABS,
|
||||||
Code: evdev.ABS_X,
|
Code: evdev.ABS_X,
|
||||||
Value: 5500,
|
Value: 1001,
|
||||||
}, t.mode)
|
}, t.mode)
|
||||||
t.Equal(time.Duration(500*time.Millisecond), testRule.nextEvent)
|
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() {
|
||||||
// func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
t.Run("No Repeat", func() {
|
||||||
// // STUB
|
// 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) {
|
func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) {
|
||||||
suite.Run(t, new(MappingRuleAxisToButtonTests))
|
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"
|
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: add tests
|
// 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)
|
// 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 {
|
type MappingRuleAxisToRelaxis struct {
|
||||||
MappingRuleBase
|
MappingRuleBase
|
||||||
Input *RuleTargetAxis
|
Input *RuleTargetAxis
|
||||||
|
@ -22,6 +21,7 @@ type MappingRuleAxisToRelaxis struct {
|
||||||
Increment int32
|
Increment int32
|
||||||
nextEvent time.Duration
|
nextEvent time.Duration
|
||||||
lastEvent time.Time
|
lastEvent time.Time
|
||||||
|
clock clockwork.Clock
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMappingRuleAxisToRelaxis(
|
func NewMappingRuleAxisToRelaxis(
|
||||||
|
@ -39,6 +39,7 @@ func NewMappingRuleAxisToRelaxis(
|
||||||
Increment: int32(increment),
|
Increment: int32(increment),
|
||||||
lastEvent: time.Now(),
|
lastEvent: time.Now(),
|
||||||
nextEvent: NoNextEvent,
|
nextEvent: NoNextEvent,
|
||||||
|
clock: clockwork.NewRealClock(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,12 +82,12 @@ func (rule *MappingRuleAxisToRelaxis) MatchEvent(
|
||||||
func (rule *MappingRuleAxisToRelaxis) TimerEvent() *evdev.InputEvent {
|
func (rule *MappingRuleAxisToRelaxis) TimerEvent() *evdev.InputEvent {
|
||||||
// This indicates that we should not emit another event
|
// This indicates that we should not emit another event
|
||||||
if rule.nextEvent == NoNextEvent {
|
if rule.nextEvent == NoNextEvent {
|
||||||
rule.lastEvent = time.Now()
|
rule.lastEvent = rule.clock.Now()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().Compare(rule.lastEvent.Add(rule.nextEvent)) > -1 {
|
if rule.clock.Now().Compare(rule.lastEvent.Add(rule.nextEvent)) > -1 {
|
||||||
rule.lastEvent = time.Now()
|
rule.lastEvent = rule.clock.Now()
|
||||||
return rule.Output.CreateEvent(rule.Increment, nil)
|
return rule.Output.CreateEvent(rule.Increment, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,12 +77,7 @@ func NewRuleTargetAxis(device_name string,
|
||||||
// in the deadzone, among other things.
|
// in the deadzone, among other things.
|
||||||
func (target *RuleTargetAxis) NormalizeValue(value int32) int32 {
|
func (target *RuleTargetAxis) NormalizeValue(value int32) int32 {
|
||||||
axisStrength := target.GetAxisStrength(value)
|
axisStrength := target.GetAxisStrength(value)
|
||||||
|
return LerpInt(AxisValueMin, AxisValueMax, axisStrength)
|
||||||
if target.Inverted {
|
|
||||||
axisStrength = 1.0 - axisStrength
|
|
||||||
}
|
|
||||||
normalizedValue := LerpInt(AxisValueMin, AxisValueMax, axisStrength)
|
|
||||||
return normalizedValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.InputEvent {
|
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
|
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 {
|
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))
|
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) {
|
func TestRunnerRuleTargetAxisTests(t *testing.T) {
|
||||||
suite.Run(t, new(RuleTargetAxisTests))
|
suite.Run(t, new(RuleTargetAxisTests))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue