diff --git a/cmd/joyful/main.go b/cmd/joyful/main.go index 735b4d5..acf4486 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -1,15 +1,13 @@ package main import ( - "fmt" - "maps" "os" "path/filepath" - "slices" "time" "git.annabunches.net/annabunches/joyful/internal/config" "git.annabunches.net/annabunches/joyful/internal/logger" + "git.annabunches.net/annabunches/joyful/internal/mappingrules" "git.annabunches.net/annabunches/joyful/internal/virtualdevice" "github.com/holoplot/go-evdev" ) @@ -23,7 +21,7 @@ func readConfig() *config.ConfigParser { return parser } -func initVirtualDevices(config *config.ConfigParser) map[string]*virtualdevice.EventBuffer { +func initVirtualBuffers(config *config.ConfigParser) map[string]*virtualdevice.EventBuffer { vDevices := config.CreateVirtualDevices() if len(vDevices) == 0 { 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 } +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 { pDeviceMap := config.ConnectPhysicalDevices() if len(pDeviceMap) == 0 { @@ -48,82 +54,44 @@ func main() { // parse configs config := readConfig() - // Initialize virtual devices and event buffers - vBuffers := initVirtualDevices(config) + // Initialize virtual devices with event buffers + vBuffers := initVirtualBuffers(config) // Initialize physical devices pDevices := initPhysicalDevices(config) + // Initialize rules + rules := config.BuildRules(pDevices, getVirtualDevices(vBuffers)) + // TEST CODE - testDriver(vBuffers, pDevices) + testDriver(vBuffers, pDevices, rules) } -func testDriver(vBuffers map[string]*virtualdevice.EventBuffer, pDevices map[string]*evdev.InputDevice) { - pDevice := slices.Collect(maps.Values(pDevices))[0] - - name, err := pDevice.Name() - if err != nil { - name = "Unknown" - } - fmt.Printf("Test Driver using physical device %s\n", name) - - var combo int32 = 0 +func testDriver(vBuffers map[string]*virtualdevice.EventBuffer, pDevices map[string]*evdev.InputDevice, rules []mappingrules.MappingRule) { + pDevice := pDevices["right-stick"] buffer := vBuffers["main"] for { - last := combo - + // Get the first event for this report event, err := pDevice.ReadOne() logger.LogIfError(err, "Error while reading event") for event.Code != evdev.SYN_REPORT { - if event.Type == evdev.EV_KEY { - switch event.Code { - case evdev.BTN_TRIGGER: - if event.Value == 0 { - combo++ - } - if event.Value == 1 { - combo-- - } - - case evdev.BTN_THUMB: - if event.Value == 0 { - combo-- - } - if event.Value == 1 { - combo++ - } - - case evdev.BTN_THUMB2: - if event.Value == 0 { - combo-- - } - if event.Value == 1 { - combo++ - } - + for _, rule := range rules { + event := rule.MatchEvent(pDevice, event) + if event == nil { + continue } + + buffer.AddEvent(event) } + // Get the next event event, err = pDevice.ReadOne() logger.LogIfError(err, "Error while reading event") } - if combo > last && combo == 3 { - buffer.AddEvent(&evdev.InputEvent{ - 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, - }) - } - + // We've received a SYN_REPORT, so now we can send all of our events + // TODO: how shall we handle this when dealing with multiple devices? buffer.SendEvents() time.Sleep(1 * time.Millisecond) diff --git a/internal/config/configparser.go b/internal/config/configparser.go index ae464d8..1f518c2 100644 --- a/internal/config/configparser.go +++ b/internal/config/configparser.go @@ -8,6 +8,7 @@ import ( "strings" "git.annabunches.net/annabunches/joyful/internal/logger" + "git.annabunches.net/annabunches/joyful/internal/mappingrules" "github.com/goccy/go-yaml" "github.com/holoplot/go-evdev" ) @@ -131,6 +132,29 @@ func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevi 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 { if numButtons > 56 { numButtons = 56 diff --git a/internal/config/rules.go b/internal/config/rules.go new file mode 100644 index 0000000..8078a59 --- /dev/null +++ b/internal/config/rules.go @@ -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 +} diff --git a/internal/config/schema.go b/internal/config/schema.go index 19f4aa6..bdc3826 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -20,24 +20,17 @@ type DeviceConfig struct { } type RuleConfig struct { - Name string `yaml:"name,omitempty"` - Type string `yaml:"type"` - Input RuleInputConfig `yaml:"input,omitempty"` - Inputs []RuleInputConfig `yaml:"inputs,omitempty"` - Output RuleOutputConfig `yaml:"output"` + Name string `yaml:"name,omitempty"` + Type string `yaml:"type"` + Input RuleTargetConfig `yaml:"input,omitempty"` + Inputs []RuleTargetConfig `yaml:"inputs,omitempty"` + Output RuleTargetConfig `yaml:"output"` } -type RuleInputConfig struct { +type RuleTargetConfig struct { Device string `yaml:"device"` Button string `yaml:"button,omitempty"` - Buttons []string `yaml:"buttons,omitempty"` Axis string `yaml:"axis,omitempty"` Inverted bool `yaml:"inverted,omitempty"` -} - -type RuleOutputConfig struct { - Device string `yaml:"device,omitempty"` - Button string `yaml:"button,omitempty"` - Axis string `yaml:"axis,omitempty"` - Groups string `yaml:"groups,omitempty"` + Groups []string `yaml:"groups,omitempty"` } diff --git a/internal/rules/keymapping.go b/internal/mappingrules/matching.go similarity index 69% rename from internal/rules/keymapping.go rename to internal/mappingrules/matching.go index f4ca97f..ab96f9a 100644 --- a/internal/rules/keymapping.go +++ b/internal/mappingrules/matching.go @@ -1,27 +1,11 @@ -package rules +package mappingrules import ( "git.annabunches.net/annabunches/joyful/internal/logger" "github.com/holoplot/go-evdev" ) -type KeyMappingRule interface { - 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 -} - +// eventFromTarget creates an outputtable event from a RuleTarget func eventFromTarget(output RuleTarget, value int32) *evdev.InputEvent { return &evdev.InputEvent{ 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 { // how we process inverted rules depends on the event type value := event.Value @@ -51,7 +36,7 @@ func valueFromTarget(rule RuleTarget, event *evdev.InputEvent) int32 { 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 || event.Code != rule.Input.Code { return nil @@ -60,10 +45,10 @@ func (rule *SimpleMappingRule) MatchEvent(device *evdev.InputDevice, event *evde 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 var match *RuleTarget - for _, input := range rule.Input { + for _, input := range rule.Inputs { if device == input.Device && event.Code == input.Code { match = &input @@ -83,7 +68,7 @@ func (rule *ComboMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev if inputValue == 1 { rule.State++ } - targetState := len(rule.Input) + targetState := len(rule.Inputs) if oldState == targetState-1 && rule.State == targetState { return eventFromTarget(rule.Output, 1) } diff --git a/internal/mappingrules/types.go b/internal/mappingrules/types.go new file mode 100644 index 0000000..b71bbfb --- /dev/null +++ b/internal/mappingrules/types.go @@ -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 +} diff --git a/internal/rules/types.go b/internal/rules/types.go deleted file mode 100644 index a72643e..0000000 --- a/internal/rules/types.go +++ /dev/null @@ -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 -}