Build rules from config.
This commit is contained in:
parent
50474f9fb2
commit
428749a519
7 changed files with 186 additions and 106 deletions
|
@ -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:
|
|
||||||
if event.Value == 0 {
|
|
||||||
combo--
|
|
||||||
}
|
|
||||||
if event.Value == 1 {
|
|
||||||
combo++
|
|
||||||
}
|
|
||||||
|
|
||||||
case evdev.BTN_THUMB2:
|
|
||||||
if event.Value == 0 {
|
|
||||||
combo--
|
|
||||||
}
|
|
||||||
if event.Value == 1 {
|
|
||||||
combo++
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer.AddEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
|
@ -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
93
internal/config/rules.go
Normal 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
|
||||||
|
}
|
|
@ -20,24 +20,17 @@ 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"`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
27
internal/mappingrules/types.go
Normal file
27
internal/mappingrules/types.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue