Build rules from config.

This commit is contained in:
Anna Rose Wiggins 2025-07-02 13:54:41 -04:00
parent 50474f9fb2
commit 428749a519
7 changed files with 186 additions and 106 deletions

View file

@ -1,15 +1,13 @@
package main package main
import ( import (
"fmt"
"maps"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"time" "time"
"git.annabunches.net/annabunches/joyful/internal/config" "git.annabunches.net/annabunches/joyful/internal/config"
"git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/logger"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"git.annabunches.net/annabunches/joyful/internal/virtualdevice" "git.annabunches.net/annabunches/joyful/internal/virtualdevice"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
@ -23,7 +21,7 @@ func readConfig() *config.ConfigParser {
return parser return parser
} }
func initVirtualDevices(config *config.ConfigParser) map[string]*virtualdevice.EventBuffer { func initVirtualBuffers(config *config.ConfigParser) map[string]*virtualdevice.EventBuffer {
vDevices := config.CreateVirtualDevices() vDevices := config.CreateVirtualDevices()
if len(vDevices) == 0 { if len(vDevices) == 0 {
logger.Log("Warning: no virtual devices found in configuration. No rules will work.") logger.Log("Warning: no virtual devices found in configuration. No rules will work.")
@ -36,6 +34,14 @@ func initVirtualDevices(config *config.ConfigParser) map[string]*virtualdevice.E
return vBuffers return vBuffers
} }
func getVirtualDevices(buffers map[string]*virtualdevice.EventBuffer) map[string]*evdev.InputDevice {
devices := make(map[string]*evdev.InputDevice)
for name, buffer := range buffers {
devices[name] = buffer.Device
}
return devices
}
func initPhysicalDevices(config *config.ConfigParser) map[string]*evdev.InputDevice { func initPhysicalDevices(config *config.ConfigParser) map[string]*evdev.InputDevice {
pDeviceMap := config.ConnectPhysicalDevices() pDeviceMap := config.ConnectPhysicalDevices()
if len(pDeviceMap) == 0 { if len(pDeviceMap) == 0 {
@ -48,82 +54,44 @@ func main() {
// parse configs // parse configs
config := readConfig() config := readConfig()
// Initialize virtual devices and event buffers // Initialize virtual devices with event buffers
vBuffers := initVirtualDevices(config) vBuffers := initVirtualBuffers(config)
// Initialize physical devices // Initialize physical devices
pDevices := initPhysicalDevices(config) pDevices := initPhysicalDevices(config)
// Initialize rules
rules := config.BuildRules(pDevices, getVirtualDevices(vBuffers))
// TEST CODE // TEST CODE
testDriver(vBuffers, pDevices) testDriver(vBuffers, pDevices, rules)
} }
func testDriver(vBuffers map[string]*virtualdevice.EventBuffer, pDevices map[string]*evdev.InputDevice) { func testDriver(vBuffers map[string]*virtualdevice.EventBuffer, pDevices map[string]*evdev.InputDevice, rules []mappingrules.MappingRule) {
pDevice := slices.Collect(maps.Values(pDevices))[0] pDevice := pDevices["right-stick"]
name, err := pDevice.Name()
if err != nil {
name = "Unknown"
}
fmt.Printf("Test Driver using physical device %s\n", name)
var combo int32 = 0
buffer := vBuffers["main"] buffer := vBuffers["main"]
for { for {
last := combo // Get the first event for this report
event, err := pDevice.ReadOne() event, err := pDevice.ReadOne()
logger.LogIfError(err, "Error while reading event") logger.LogIfError(err, "Error while reading event")
for event.Code != evdev.SYN_REPORT { for event.Code != evdev.SYN_REPORT {
if event.Type == evdev.EV_KEY { for _, rule := range rules {
switch event.Code { event := rule.MatchEvent(pDevice, event)
case evdev.BTN_TRIGGER: if event == nil {
if event.Value == 0 { continue
combo++
}
if event.Value == 1 {
combo--
} }
case evdev.BTN_THUMB: buffer.AddEvent(event)
if event.Value == 0 {
combo--
}
if event.Value == 1 {
combo++
}
case evdev.BTN_THUMB2:
if event.Value == 0 {
combo--
}
if event.Value == 1 {
combo++
}
}
} }
// Get the next event
event, err = pDevice.ReadOne() event, err = pDevice.ReadOne()
logger.LogIfError(err, "Error while reading event") logger.LogIfError(err, "Error while reading event")
} }
if combo > last && combo == 3 { // We've received a SYN_REPORT, so now we can send all of our events
buffer.AddEvent(&evdev.InputEvent{ // TODO: how shall we handle this when dealing with multiple devices?
Type: evdev.EV_KEY,
Code: evdev.BTN_TRIGGER,
Value: 1,
})
}
if combo < last && combo == 2 {
buffer.AddEvent(&evdev.InputEvent{
Type: evdev.EV_KEY,
Code: evdev.BTN_TRIGGER,
Value: 0,
})
}
buffer.SendEvents() buffer.SendEvents()
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)

View file

@ -8,6 +8,7 @@ import (
"strings" "strings"
"git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/logger"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
@ -131,6 +132,29 @@ func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevi
return deviceMap return deviceMap
} }
func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule {
rules := make([]mappingrules.MappingRule, 0)
for _, ruleConfig := range parser.config.Rules {
var newRule mappingrules.MappingRule
var err error
switch strings.ToLower(ruleConfig.Type) {
case RuleTypeSimple:
newRule, err = makeSimpleRule(ruleConfig, pDevs, vDevs)
case RuleTypeCombo:
newRule, err = makeComboRule(ruleConfig, pDevs, vDevs)
}
if err != nil {
logger.LogError(err, "")
continue
}
rules = append(rules, newRule)
}
return rules
}
func makeButtons(numButtons int) []evdev.EvCode { func makeButtons(numButtons int) []evdev.EvCode {
if numButtons > 56 { if numButtons > 56 {
numButtons = 56 numButtons = 56

93
internal/config/rules.go Normal file
View file

@ -0,0 +1,93 @@
package config
import (
"fmt"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev"
)
func makeSimpleRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) (mappingrules.MappingRule, error) {
input, err := makeRuleTarget(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTarget(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &mappingrules.SimpleMappingRule{
Input: input,
Output: output,
}, nil
}
func makeComboRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) (mappingrules.MappingRule, error) {
inputs := make([]mappingrules.RuleTarget, 0)
for _, inputConfig := range ruleConfig.Inputs {
input, err := makeRuleTarget(inputConfig, pDevs)
if err != nil {
return nil, err
}
inputs = append(inputs, input)
}
output, err := makeRuleTarget(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &mappingrules.ComboMappingRule{
Inputs: inputs,
Output: output,
}, nil
}
// makeInputRuleTarget takes an Input declaration from the YAML and returns a fully formed RuleTarget.
func makeRuleTarget(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (mappingrules.RuleTarget, error) {
ruleTarget := mappingrules.RuleTarget{}
device, ok := devs[targetConfig.Device]
if !ok {
return mappingrules.RuleTarget{}, fmt.Errorf("couldn't build rule due to non-existent device '%s'", targetConfig.Device)
}
ruleTarget.Device = device
eventType, eventCode, err := decodeRuleTargetValues(targetConfig)
if err != nil {
return ruleTarget, err
}
ruleTarget.Type = eventType
ruleTarget.Code = eventCode
return ruleTarget, nil
}
// decodeRuleTargetValues returns the appropriate evdev.EvType and evdev.EvCode values
// for a given RuleTargetConfig, converting the config file strings into appropriate constants
//
// Todo: support different formats for key specification
func decodeRuleTargetValues(target RuleTargetConfig) (evdev.EvType, evdev.EvCode, error) {
var eventType evdev.EvType
var eventCode evdev.EvCode
var ok bool
if target.Button != "" {
eventType = evdev.EV_KEY
eventCode, ok = evdev.KEYFromString[target.Button]
if !ok {
return 0, 0, fmt.Errorf("skipping rule due to invalid button code '%s'", target.Button)
}
}
if target.Axis != "" {
eventType = evdev.EV_ABS
eventCode, ok = evdev.ABSFromString[target.Axis]
if !ok {
return 0, 0, fmt.Errorf("skipping rule due to invalid axis code '%s'", target.Button)
}
}
return eventType, eventCode, nil
}

View file

@ -22,22 +22,15 @@ type DeviceConfig struct {
type RuleConfig struct { type RuleConfig struct {
Name string `yaml:"name,omitempty"` Name string `yaml:"name,omitempty"`
Type string `yaml:"type"` Type string `yaml:"type"`
Input RuleInputConfig `yaml:"input,omitempty"` Input RuleTargetConfig `yaml:"input,omitempty"`
Inputs []RuleInputConfig `yaml:"inputs,omitempty"` Inputs []RuleTargetConfig `yaml:"inputs,omitempty"`
Output RuleOutputConfig `yaml:"output"` Output RuleTargetConfig `yaml:"output"`
} }
type RuleInputConfig struct { type RuleTargetConfig struct {
Device string `yaml:"device"` Device string `yaml:"device"`
Button string `yaml:"button,omitempty"` Button string `yaml:"button,omitempty"`
Buttons []string `yaml:"buttons,omitempty"`
Axis string `yaml:"axis,omitempty"` Axis string `yaml:"axis,omitempty"`
Inverted bool `yaml:"inverted,omitempty"` Inverted bool `yaml:"inverted,omitempty"`
} Groups []string `yaml:"groups,omitempty"`
type RuleOutputConfig struct {
Device string `yaml:"device,omitempty"`
Button string `yaml:"button,omitempty"`
Axis string `yaml:"axis,omitempty"`
Groups string `yaml:"groups,omitempty"`
} }

View file

@ -1,27 +1,11 @@
package rules package mappingrules
import ( 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"
) )
type KeyMappingRule interface { // eventFromTarget creates an outputtable event from a RuleTarget
MatchEvent(*evdev.InputDevice, *evdev.InputEvent)
}
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
type SimpleMappingRule struct {
Input RuleTarget
Output RuleTarget
}
// A Combo Mapping Rule can require multiple physical button presses for a single output button
type ComboMappingRule struct {
Input []RuleTarget
Output RuleTarget
State int
}
func eventFromTarget(output RuleTarget, value int32) *evdev.InputEvent { func eventFromTarget(output RuleTarget, value int32) *evdev.InputEvent {
return &evdev.InputEvent{ return &evdev.InputEvent{
Type: output.Type, Type: output.Type,
@ -30,6 +14,7 @@ func eventFromTarget(output RuleTarget, value int32) *evdev.InputEvent {
} }
} }
// valueFromTarget determines the value to output from an input specification,given a RuleTarget's constraints
func valueFromTarget(rule RuleTarget, event *evdev.InputEvent) int32 { func valueFromTarget(rule RuleTarget, event *evdev.InputEvent) int32 {
// how we process inverted rules depends on the event type // how we process inverted rules depends on the event type
value := event.Value value := event.Value
@ -51,7 +36,7 @@ func valueFromTarget(rule RuleTarget, event *evdev.InputEvent) int32 {
return value return value
} }
func (rule *SimpleMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) *evdev.InputEvent { func (rule SimpleMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) *evdev.InputEvent {
if device != rule.Input.Device || if device != rule.Input.Device ||
event.Code != rule.Input.Code { event.Code != rule.Input.Code {
return nil return nil
@ -60,10 +45,10 @@ func (rule *SimpleMappingRule) MatchEvent(device *evdev.InputDevice, event *evde
return eventFromTarget(rule.Output, valueFromTarget(rule.Input, event)) return eventFromTarget(rule.Output, valueFromTarget(rule.Input, event))
} }
func (rule *ComboMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) *evdev.InputEvent { func (rule ComboMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) *evdev.InputEvent {
// Check each of the inputs, and if we find a match, proceed // Check each of the inputs, and if we find a match, proceed
var match *RuleTarget var match *RuleTarget
for _, input := range rule.Input { for _, input := range rule.Inputs {
if device == input.Device && if device == input.Device &&
event.Code == input.Code { event.Code == input.Code {
match = &input match = &input
@ -83,7 +68,7 @@ func (rule *ComboMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev
if inputValue == 1 { if inputValue == 1 {
rule.State++ rule.State++
} }
targetState := len(rule.Input) targetState := len(rule.Inputs)
if oldState == targetState-1 && rule.State == targetState { if oldState == targetState-1 && rule.State == targetState {
return eventFromTarget(rule.Output, 1) return eventFromTarget(rule.Output, 1)
} }

View file

@ -0,0 +1,27 @@
package mappingrules
import "github.com/holoplot/go-evdev"
type MappingRule interface {
MatchEvent(*evdev.InputDevice, *evdev.InputEvent) *evdev.InputEvent
}
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
type SimpleMappingRule struct {
Input RuleTarget
Output RuleTarget
}
// A Combo Mapping Rule can require multiple physical button presses for a single output button
type ComboMappingRule struct {
Inputs []RuleTarget
Output RuleTarget
State int
}
type RuleTarget struct {
Device *evdev.InputDevice
Type evdev.EvType
Code evdev.EvCode
Inverted bool
}

View file

@ -1,10 +0,0 @@
package rules
import "github.com/holoplot/go-evdev"
type RuleTarget struct {
Device *evdev.InputDevice
Type evdev.EvType
Code evdev.EvCode
Inverted bool
}