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:
|
||||
switch channelEvent.Event.Type {
|
||||
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 {
|
||||
buffer.SendEvents()
|
||||
}
|
||||
|
@ -114,6 +115,8 @@ func main() {
|
|||
case ChannelEventTimer:
|
||||
// Timer events give us the device and event to use directly
|
||||
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 (
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1
|
||||
github.com/jonboulle/clockwork v0.5.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
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/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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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{
|
||||
evdev.EV_KEY: makeButtons(int(deviceConfig.Buttons)),
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ type DeviceConfig struct {
|
|||
Uuid string `yaml:"uuid,omitempty"`
|
||||
Buttons int `yaml:"buttons,omitempty"`
|
||||
Axes int `yaml:"axes,omitempty"`
|
||||
RelativeAxes []string `yaml:"rel_axes,omitempty"`
|
||||
}
|
||||
|
||||
type RuleConfig struct {
|
||||
|
|
|
@ -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,25 +37,32 @@ 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
|
||||
t.Run("No Repeat", func() {
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// And a deadzone value should clear it
|
||||
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 = NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250)
|
||||
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,
|
||||
|
@ -75,13 +83,104 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
|||
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