Move initialization code closer to the appropriate structs. (#17)
Reviewed-on: #17 Co-authored-by: Anna Rose Wiggins <annabunches@gmail.com> Co-committed-by: Anna Rose Wiggins <annabunches@gmail.com>
This commit is contained in:
parent
d9babf5dc0
commit
8d2b15a7c8
40 changed files with 1087 additions and 1109 deletions
|
@ -5,7 +5,8 @@ import (
|
|||
"slices"
|
||||
|
||||
// TODO: using config here feels like bad coupling... ButtonFromIndex might need a refactor / move
|
||||
"git.annabunches.net/annabunches/joyful/internal/config"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
@ -20,7 +21,7 @@ func isJoystickLike(device *evdev.InputDevice) bool {
|
|||
if slices.Contains(types, evdev.EV_KEY) {
|
||||
buttons := device.CapableEvents(evdev.EV_KEY)
|
||||
|
||||
for _, code := range config.ButtonFromIndex {
|
||||
for _, code := range eventcodes.ButtonFromIndex {
|
||||
if slices.Contains(buttons, code) {
|
||||
return true
|
||||
}
|
||||
|
|
147
cmd/joyful/config.go
Normal file
147
cmd/joyful/config.go
Normal file
|
@ -0,0 +1,147 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"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"
|
||||
)
|
||||
|
||||
func initPhysicalDevices(conf *configparser.Config) map[string]*evdev.InputDevice {
|
||||
pDeviceMap := make(map[string]*evdev.InputDevice)
|
||||
|
||||
for _, devConfig := range conf.Devices {
|
||||
if strings.ToLower(devConfig.Type) != configparser.DeviceTypePhysical {
|
||||
continue
|
||||
}
|
||||
|
||||
innerConfig := devConfig.Config.(configparser.DeviceConfigPhysical)
|
||||
name, device, err := initPhysicalDevice(innerConfig)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to initialize physical device")
|
||||
continue
|
||||
}
|
||||
|
||||
pDeviceMap[name] = device
|
||||
|
||||
displayName := innerConfig.DeviceName
|
||||
if innerConfig.DevicePath != "" {
|
||||
displayName = innerConfig.DevicePath
|
||||
}
|
||||
logger.Logf("Connected to '%s' as '%s'", displayName, name)
|
||||
}
|
||||
|
||||
if len(pDeviceMap) == 0 {
|
||||
logger.Log("Warning: no physical devices found in configuration. No rules will work.")
|
||||
}
|
||||
return pDeviceMap
|
||||
}
|
||||
|
||||
func initPhysicalDevice(config configparser.DeviceConfigPhysical) (string, *evdev.InputDevice, error) {
|
||||
name := config.Name
|
||||
var device *evdev.InputDevice
|
||||
var err error
|
||||
|
||||
if config.DevicePath != "" {
|
||||
device, err = evdev.Open(config.DevicePath)
|
||||
} else {
|
||||
device, err = evdev.OpenByName(config.DeviceName)
|
||||
}
|
||||
|
||||
if config.Lock && err == nil {
|
||||
grabErr := device.Grab()
|
||||
logger.LogIfError(grabErr, "Failed to lock device for exclusive access")
|
||||
}
|
||||
|
||||
return name, device, err
|
||||
}
|
||||
|
||||
// TODO: juggling all these maps is a pain. Is there a better solution here?
|
||||
func initVirtualBuffers(config *configparser.Config) (map[string]*evdev.InputDevice,
|
||||
map[string]*virtualdevice.EventBuffer,
|
||||
map[*evdev.InputDevice]*virtualdevice.EventBuffer) {
|
||||
|
||||
vDevicesByName := make(map[string]*evdev.InputDevice)
|
||||
vBuffersByName := make(map[string]*virtualdevice.EventBuffer)
|
||||
vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer)
|
||||
|
||||
for _, devConfig := range config.Devices {
|
||||
if strings.ToLower(devConfig.Type) != configparser.DeviceTypeVirtual {
|
||||
continue
|
||||
}
|
||||
|
||||
vConfig := devConfig.Config.(configparser.DeviceConfigVirtual)
|
||||
buffer, err := virtualdevice.NewEventBuffer(vConfig)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create virtual device, skipping")
|
||||
continue
|
||||
}
|
||||
vDevicesByName[buffer.Name] = buffer.Device.(*evdev.InputDevice)
|
||||
vBuffersByName[buffer.Name] = buffer
|
||||
vBuffersByDevice[buffer.Device.(*evdev.InputDevice)] = buffer
|
||||
}
|
||||
|
||||
if len(vDevicesByName) == 0 {
|
||||
logger.Log("Warning: no virtual devices found in configuration. No rules will work.")
|
||||
}
|
||||
|
||||
return vDevicesByName, vBuffersByName, vBuffersByDevice
|
||||
}
|
||||
|
||||
// TODO: At some point it would *very likely* make sense to map each rule to all of the physical devices that can
|
||||
// trigger it, and return that instead. Something like a map[Device][]mappingrule.MappingRule.
|
||||
// This would speed up rule matching by only checking relevant rules for a given input event.
|
||||
// We could take this further and make it a map[<struct of *inputdevice, type, and code>][]rule
|
||||
// For very large rule-bases this may be helpful for staying performant.
|
||||
func loadRules(
|
||||
config *configparser.Config,
|
||||
pDevices map[string]*evdev.InputDevice,
|
||||
vDevices map[string]*evdev.InputDevice,
|
||||
modes []string) ([]mappingrules.MappingRule, <-chan ChannelEvent, func(), *sync.WaitGroup) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
eventChannel := make(chan ChannelEvent, 1000)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Setup device mapping for the mappingrules package
|
||||
pDevs := mappingrules.ConvertDeviceMap(pDevices)
|
||||
vDevs := mappingrules.ConvertDeviceMap(vDevices)
|
||||
|
||||
// Initialize rules
|
||||
rules := make([]mappingrules.MappingRule, 0)
|
||||
for _, ruleConfig := range config.Rules {
|
||||
newRule, err := mappingrules.NewRule(ruleConfig, pDevs, vDevs, modes)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create rule, skipping")
|
||||
continue
|
||||
}
|
||||
rules = append(rules, newRule)
|
||||
}
|
||||
|
||||
logger.Logf("Created %d mapping rules.", len(rules))
|
||||
|
||||
// start listening for events on devices and timers
|
||||
for _, device := range pDevices {
|
||||
wg.Add(1)
|
||||
go eventWatcher(device, eventChannel, ctx, &wg)
|
||||
}
|
||||
|
||||
timerCount := 0
|
||||
for _, rule := range rules {
|
||||
if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok {
|
||||
wg.Add(1)
|
||||
go timerWatcher(timedRule, eventChannel, ctx, &wg)
|
||||
timerCount++
|
||||
}
|
||||
}
|
||||
logger.Logf("Registered %d timers.", timerCount)
|
||||
|
||||
go consoleWatcher(eventChannel)
|
||||
|
||||
return rules, eventChannel, cancel, &wg
|
||||
}
|
|
@ -1,19 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/holoplot/go-evdev"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/config"
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||
"git.annabunches.net/annabunches/joyful/internal/virtualdevice"
|
||||
)
|
||||
|
||||
func getConfigDir(dir string) string {
|
||||
|
@ -21,39 +17,6 @@ func getConfigDir(dir string) string {
|
|||
return os.ExpandEnv(configDir)
|
||||
}
|
||||
|
||||
func readConfig(configDir string) *config.ConfigParser {
|
||||
parser := &config.ConfigParser{}
|
||||
err := parser.Parse(configDir)
|
||||
logger.FatalIfError(err, "Failed to parse config")
|
||||
return parser
|
||||
}
|
||||
|
||||
func initVirtualBuffers(config *config.ConfigParser) (map[string]*evdev.InputDevice,
|
||||
map[string]*virtualdevice.EventBuffer,
|
||||
map[*evdev.InputDevice]*virtualdevice.EventBuffer) {
|
||||
|
||||
vDevices := config.InitVirtualDevices()
|
||||
if len(vDevices) == 0 {
|
||||
logger.Log("Warning: no virtual devices found in configuration. No rules will work.")
|
||||
}
|
||||
|
||||
vBuffersByName := make(map[string]*virtualdevice.EventBuffer)
|
||||
vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer)
|
||||
for name, device := range vDevices {
|
||||
vBuffersByName[name] = virtualdevice.NewEventBuffer(device)
|
||||
vBuffersByDevice[device] = vBuffersByName[name]
|
||||
}
|
||||
return vDevices, vBuffersByName, vBuffersByDevice
|
||||
}
|
||||
|
||||
func initPhysicalDevices(config *config.ConfigParser) map[string]*evdev.InputDevice {
|
||||
pDeviceMap := config.InitPhysicalDevices()
|
||||
if len(pDeviceMap) == 0 {
|
||||
logger.Log("Warning: no physical devices found in configuration. No rules will work.")
|
||||
}
|
||||
return pDeviceMap
|
||||
}
|
||||
|
||||
func main() {
|
||||
// parse command-line
|
||||
var configFlag string
|
||||
|
@ -64,7 +27,8 @@ func main() {
|
|||
|
||||
// parse configs
|
||||
configDir := getConfigDir(configFlag)
|
||||
config := readConfig(configDir)
|
||||
config, err := configparser.ParseConfig(configDir)
|
||||
logger.FatalIfError(err, "Failed to parse configuration")
|
||||
|
||||
// initialize TTS
|
||||
tts, err := newTTS(ttsOps)
|
||||
|
@ -76,20 +40,26 @@ func main() {
|
|||
// Initialize physical devices
|
||||
pDevices := initPhysicalDevices(config)
|
||||
|
||||
// Load the rules
|
||||
rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName)
|
||||
// initialize the mode variables
|
||||
var mode string
|
||||
modes := config.Modes
|
||||
if len(modes) == 0 {
|
||||
mode = "*"
|
||||
} else {
|
||||
mode = config.Modes[0]
|
||||
}
|
||||
|
||||
// initialize the mode variable
|
||||
mode := config.GetModes()[0]
|
||||
// Load the rules
|
||||
rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName, modes)
|
||||
|
||||
// initialize TTS phrases for modes
|
||||
for _, m := range config.GetModes() {
|
||||
for _, m := range modes {
|
||||
tts.AddMessage(m)
|
||||
logger.LogDebugf("Added TTS message '%s'", m)
|
||||
}
|
||||
|
||||
fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.")
|
||||
if len(config.GetModes()) > 1 {
|
||||
if len(modes) > 0 {
|
||||
logger.Logf("Initial mode set to '%s'", mode)
|
||||
}
|
||||
|
||||
|
@ -127,13 +97,18 @@ func main() {
|
|||
|
||||
case ChannelEventReload:
|
||||
// stop existing channels
|
||||
config, err := configparser.ParseConfig(configDir) // reload the config
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to parse config, no changes made")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println("Reloading rules.")
|
||||
cancel()
|
||||
fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.")
|
||||
wg.Wait()
|
||||
fmt.Println("Listeners exited. Parsing config.")
|
||||
config := readConfig(configDir) // reload the config
|
||||
rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName)
|
||||
fmt.Println("Listeners exited. Loading new rules.")
|
||||
rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName, modes)
|
||||
fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.")
|
||||
}
|
||||
|
||||
|
@ -142,37 +117,3 @@ func main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadRules(
|
||||
config *config.ConfigParser,
|
||||
pDevices map[string]*evdev.InputDevice,
|
||||
vDevices map[string]*evdev.InputDevice) ([]mappingrules.MappingRule, <-chan ChannelEvent, func(), *sync.WaitGroup) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
eventChannel := make(chan ChannelEvent, 1000)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Initialize rules
|
||||
rules := config.InitRules(pDevices, vDevices)
|
||||
logger.Logf("Created %d mapping rules.", len(rules))
|
||||
|
||||
// start listening for events on devices and timers
|
||||
for _, device := range pDevices {
|
||||
wg.Add(1)
|
||||
go eventWatcher(device, eventChannel, ctx, &wg)
|
||||
}
|
||||
|
||||
timerCount := 0
|
||||
for _, rule := range rules {
|
||||
if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok {
|
||||
wg.Add(1)
|
||||
go timerWatcher(timedRule, eventChannel, ctx, &wg)
|
||||
timerCount++
|
||||
}
|
||||
}
|
||||
logger.Logf("Registered %d timers.", timerCount)
|
||||
|
||||
go consoleWatcher(eventChannel)
|
||||
|
||||
return rules, eventChannel, cancel, &wg
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
// The ConfigParser is the main structure you'll interact with when using this package.
|
||||
//
|
||||
// Example usage:
|
||||
// config := &config.ConfigParser{}
|
||||
// config.Parse(<some directory containing YAML files>)
|
||||
// virtualDevices := config.CreateVirtualDevices()
|
||||
// physicalDevices := config.ConnectVirtualDevices()
|
||||
// modes := config.GetModes()
|
||||
// rules := config.BuildRules(physicalDevices, virtualDevices, modes)
|
||||
//
|
||||
// nb: there are methods defined on ConfigParser in other files in this package!
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
type ConfigParser struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// Parse all the config files and store the config data for further use
|
||||
func (parser *ConfigParser) Parse(directory string) error {
|
||||
parser.config = Config{}
|
||||
|
||||
// Find the config files in the directory
|
||||
dirEntries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
err = os.Mkdir(directory, 0755)
|
||||
if err != nil {
|
||||
return errors.New("Failed to create config directory at " + directory)
|
||||
}
|
||||
}
|
||||
|
||||
// Open each yaml file and add its contents to the global config
|
||||
for _, file := range dirEntries {
|
||||
name := file.Name()
|
||||
if file.IsDir() || !(strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")) {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(directory, name)
|
||||
if strings.HasSuffix(filePath, ".yaml") || strings.HasSuffix(filePath, ".yml") {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Error while opening config file")
|
||||
continue
|
||||
}
|
||||
newConfig := Config{}
|
||||
err = yaml.Unmarshal(data, &newConfig)
|
||||
logger.LogIfError(err, "Error parsing YAML")
|
||||
parser.config.Rules = append(parser.config.Rules, newConfig.Rules...)
|
||||
parser.config.Devices = append(parser.config.Devices, newConfig.Devices...)
|
||||
parser.config.Modes = append(parser.config.Modes, newConfig.Modes...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(parser.config.Devices) == 0 {
|
||||
return errors.New("Found no devices in configuration. Please add configuration at " + directory)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (parser *ConfigParser) GetModes() []string {
|
||||
if len(parser.config.Modes) == 0 {
|
||||
return []string{"*"}
|
||||
}
|
||||
return parser.config.Modes
|
||||
}
|
|
@ -1,221 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// InitVirtualDevices will register any configured devices with type = virtual
|
||||
// using /dev/uinput, and return a map of those devices.
|
||||
//
|
||||
// This function assumes Parse() has been called.
|
||||
//
|
||||
// This function should only be called once, unless we want to create duplicate devices for some reason.
|
||||
func (parser *ConfigParser) InitVirtualDevices() map[string]*evdev.InputDevice {
|
||||
deviceMap := make(map[string]*evdev.InputDevice)
|
||||
|
||||
for _, deviceConfig := range parser.config.Devices {
|
||||
if strings.ToLower(deviceConfig.Type) != DeviceTypeVirtual {
|
||||
continue
|
||||
}
|
||||
|
||||
deviceConfig := deviceConfig.Config.(DeviceConfigVirtual)
|
||||
|
||||
name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
|
||||
|
||||
var capabilities map[evdev.EvType][]evdev.EvCode
|
||||
|
||||
// todo: add tests for presets
|
||||
switch deviceConfig.Preset {
|
||||
case DevicePresetGamepad:
|
||||
capabilities = CapabilitiesPresetGamepad
|
||||
case DevicePresetKeyboard:
|
||||
capabilities = CapabilitiesPresetKeyboard
|
||||
case DevicePresetJoystick:
|
||||
capabilities = CapabilitiesPresetJoystick
|
||||
case DevicePresetMouse:
|
||||
capabilities = CapabilitiesPresetMouse
|
||||
default:
|
||||
capabilities = map[evdev.EvType][]evdev.EvCode{
|
||||
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons),
|
||||
evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes),
|
||||
evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes),
|
||||
}
|
||||
}
|
||||
|
||||
device, err := evdev.CreateDevice(
|
||||
name,
|
||||
// TODO: who knows what these should actually be
|
||||
evdev.InputID{
|
||||
BusType: 0x03,
|
||||
Vendor: 0x4711,
|
||||
Product: 0x0816,
|
||||
Version: 1,
|
||||
},
|
||||
capabilities,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.LogIfError(err, "Failed to create virtual device")
|
||||
continue
|
||||
}
|
||||
|
||||
deviceMap[deviceConfig.Name] = device
|
||||
logger.Log(fmt.Sprintf(
|
||||
"Created virtual device '%s' with %d buttons, %d axes, and %d relative axes",
|
||||
name,
|
||||
len(capabilities[evdev.EV_KEY]),
|
||||
len(capabilities[evdev.EV_ABS]),
|
||||
len(capabilities[evdev.EV_REL]),
|
||||
))
|
||||
}
|
||||
|
||||
return deviceMap
|
||||
}
|
||||
|
||||
// InitPhysicalDevices will create InputDevices corresponding to any registered
|
||||
// devices with type = physical.
|
||||
//
|
||||
// This function assumes Parse() has been called.
|
||||
//
|
||||
// This function should only be called once.
|
||||
func (parser *ConfigParser) InitPhysicalDevices() map[string]*evdev.InputDevice {
|
||||
deviceMap := make(map[string]*evdev.InputDevice)
|
||||
|
||||
for _, deviceConfig := range parser.config.Devices {
|
||||
if strings.ToLower(deviceConfig.Type) != DeviceTypePhysical {
|
||||
continue
|
||||
}
|
||||
|
||||
deviceConfig := deviceConfig.Config.(DeviceConfigPhysical)
|
||||
|
||||
var infoName string
|
||||
var device *evdev.InputDevice
|
||||
var err error
|
||||
|
||||
if deviceConfig.DevicePath != "" {
|
||||
infoName = deviceConfig.DevicePath
|
||||
device, err = evdev.Open(deviceConfig.DevicePath)
|
||||
} else {
|
||||
infoName = deviceConfig.DeviceName
|
||||
device, err = evdev.OpenByName(deviceConfig.DeviceName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to open physical device, skipping. Confirm the device name or path with 'evinfo'")
|
||||
continue
|
||||
}
|
||||
|
||||
if deviceConfig.Lock {
|
||||
logger.LogDebugf("Locking device '%s'", infoName)
|
||||
err := device.Grab()
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to grab device for exclusive access")
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log(fmt.Sprintf("Connected to '%s' as '%s'", infoName, deviceConfig.Name))
|
||||
deviceMap[deviceConfig.Name] = device
|
||||
}
|
||||
|
||||
return deviceMap
|
||||
}
|
||||
|
||||
// TODO: these functions have a lot of duplication; we need to figure out how to refactor it cleanly
|
||||
// without losing logging context...
|
||||
func makeButtons(numButtons int, buttonList []string) []evdev.EvCode {
|
||||
if numButtons > 0 && len(buttonList) > 0 {
|
||||
logger.Log("'num_buttons' and 'buttons' both specified, ignoring 'num_buttons'")
|
||||
}
|
||||
|
||||
if numButtons > VirtualDeviceMaxButtons {
|
||||
numButtons = VirtualDeviceMaxButtons
|
||||
logger.Logf("Limiting virtual device buttons to %d", VirtualDeviceMaxButtons)
|
||||
}
|
||||
|
||||
if len(buttonList) > 0 {
|
||||
buttons := make([]evdev.EvCode, 0, len(buttonList))
|
||||
for _, codeStr := range buttonList {
|
||||
code, err := parseCode(codeStr, "BTN")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create button, skipping")
|
||||
continue
|
||||
}
|
||||
buttons = append(buttons, code)
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
buttons := make([]evdev.EvCode, numButtons)
|
||||
|
||||
for i := 0; i < numButtons; i++ {
|
||||
buttons[i] = ButtonFromIndex[i]
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
func makeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||
if numAxes > 0 && len(axisList) > 0 {
|
||||
logger.Log("'num_axes' and 'axes' both specified, ignoring 'num_axes'")
|
||||
}
|
||||
|
||||
if len(axisList) > 0 {
|
||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||
for _, codeStr := range axisList {
|
||||
code, err := parseCode(codeStr, "ABS")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create axis, skipping")
|
||||
continue
|
||||
}
|
||||
axes = append(axes, code)
|
||||
}
|
||||
return axes
|
||||
}
|
||||
|
||||
if numAxes > 8 {
|
||||
numAxes = 8
|
||||
logger.Log("Limiting virtual device axes to 8")
|
||||
}
|
||||
|
||||
axes := make([]evdev.EvCode, numAxes)
|
||||
for i := 0; i < numAxes; i++ {
|
||||
axes[i] = evdev.EvCode(i)
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
||||
|
||||
func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||
if numAxes > 0 && len(axisList) > 0 {
|
||||
logger.Log("'num_rel_axes' and 'rel_axes' both specified, ignoring 'num_rel_axes'")
|
||||
}
|
||||
|
||||
if len(axisList) > 0 {
|
||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||
for _, codeStr := range axisList {
|
||||
code, err := parseCode(codeStr, "REL")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create axis, skipping")
|
||||
continue
|
||||
}
|
||||
axes = append(axes, code)
|
||||
}
|
||||
return axes
|
||||
}
|
||||
|
||||
if numAxes > 10 {
|
||||
numAxes = 10
|
||||
logger.Log("Limiting virtual device relative axes to 10")
|
||||
}
|
||||
|
||||
axes := make([]evdev.EvCode, numAxes)
|
||||
for i := 0; i < numAxes; i++ {
|
||||
axes[i] = evdev.EvCode(i)
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package config
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
|
||||
type Device interface {
|
||||
AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error)
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
func makeRuleTargetButton(targetConfig RuleTargetConfigButton, devs map[string]Device) (*mappingrules.RuleTargetButton, error) {
|
||||
device, ok := devs[targetConfig.Device]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||
}
|
||||
|
||||
eventCode, err := parseCodeButton(targetConfig.Button)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewRuleTargetButton(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
targetConfig.Inverted,
|
||||
)
|
||||
}
|
||||
|
||||
func makeRuleTargetAxis(targetConfig RuleTargetConfigAxis, devs map[string]Device) (*mappingrules.RuleTargetAxis, error) {
|
||||
device, ok := devs[targetConfig.Device]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||
}
|
||||
|
||||
if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart {
|
||||
return nil, errors.New("deadzone_end must be greater than deadzone_start")
|
||||
}
|
||||
|
||||
eventCode, err := parseCode(targetConfig.Axis, CodePrefixAxis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewRuleTargetAxis(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
targetConfig.Inverted,
|
||||
deadzoneStart,
|
||||
deadzoneEnd,
|
||||
)
|
||||
}
|
||||
|
||||
func makeRuleTargetRelaxis(targetConfig RuleTargetConfigRelaxis, devs map[string]Device) (*mappingrules.RuleTargetRelaxis, error) {
|
||||
device, ok := devs[targetConfig.Device]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||
}
|
||||
|
||||
eventCode, err := parseCode(targetConfig.Axis, CodePrefixRelaxis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewRuleTargetRelaxis(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
)
|
||||
}
|
||||
|
||||
func makeRuleTargetModeSelect(targetConfig RuleTargetConfigModeSelect, allModes []string) (*mappingrules.RuleTargetModeSelect, error) {
|
||||
if ok := validateModes(targetConfig.Modes, allModes); !ok {
|
||||
return nil, errors.New("undefined mode in mode select list")
|
||||
}
|
||||
|
||||
return mappingrules.NewRuleTargetModeSelect(targetConfig.Modes)
|
||||
}
|
||||
|
||||
// hasError exists solely to switch on errors in case statements
|
||||
func hasError(_ any, err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// calculateDeadzones produces the deadzone start and end values in absolute terms
|
||||
// TODO: on the one hand, this logic feels betten encapsulated in mappingrules. On the other hand,
|
||||
// passing even more parameters to NewRuleTargetAxis feels terrible
|
||||
func calculateDeadzones(targetConfig RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) {
|
||||
|
||||
var deadzoneStart, deadzoneEnd int32
|
||||
deadzoneStart = 0
|
||||
deadzoneEnd = 0
|
||||
|
||||
if targetConfig.DeadzoneStart != 0 || targetConfig.DeadzoneEnd != 0 {
|
||||
return targetConfig.DeadzoneStart, targetConfig.DeadzoneEnd, nil
|
||||
}
|
||||
|
||||
var min, max int32
|
||||
absInfoMap, err := device.AbsInfos()
|
||||
|
||||
if err != nil {
|
||||
min = mappingrules.AxisValueMin
|
||||
max = mappingrules.AxisValueMax
|
||||
} else {
|
||||
absInfo := absInfoMap[axis]
|
||||
min = absInfo.Minimum
|
||||
max = absInfo.Maximum
|
||||
}
|
||||
|
||||
if targetConfig.DeadzoneCenter < min || targetConfig.DeadzoneCenter > max {
|
||||
return 0, 0, fmt.Errorf("deadzone_center '%d' is out of bounds", targetConfig.DeadzoneCenter)
|
||||
}
|
||||
|
||||
switch {
|
||||
case targetConfig.DeadzoneSize != 0:
|
||||
deadzoneStart = targetConfig.DeadzoneCenter - targetConfig.DeadzoneSize/2
|
||||
deadzoneEnd = targetConfig.DeadzoneCenter + targetConfig.DeadzoneSize/2
|
||||
case targetConfig.DeadzoneSizePercent != 0:
|
||||
deadzoneSize := (max - min) / targetConfig.DeadzoneSizePercent
|
||||
deadzoneStart = targetConfig.DeadzoneCenter - deadzoneSize/2
|
||||
deadzoneEnd = targetConfig.DeadzoneCenter + deadzoneSize/2
|
||||
}
|
||||
|
||||
deadzoneStart, deadzoneEnd = clampAndShift(deadzoneStart, deadzoneEnd, min, max)
|
||||
return deadzoneStart, deadzoneEnd, nil
|
||||
}
|
||||
|
||||
func clampAndShift(start, end, min, max int32) (int32, int32) {
|
||||
if start < min {
|
||||
end += min - start
|
||||
start = min
|
||||
}
|
||||
if end > max {
|
||||
start -= end - max
|
||||
end = max
|
||||
}
|
||||
|
||||
return start, end
|
||||
}
|
|
@ -1,237 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// TODO: At some point it would *very likely* make sense to map each rule to all of the physical devices that can
|
||||
// trigger it, and return that instead. Something like a map[Device][]mappingrule.MappingRule.
|
||||
// This would speed up rule matching by only checking relevant rules for a given input event.
|
||||
// We could take this further and make it a map[<struct of *inputdevice, type, and code>][]rule
|
||||
// For very large rule-bases this may be helpful for staying performant.
|
||||
func (parser *ConfigParser) InitRules(pInputDevs map[string]*evdev.InputDevice, vInputDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule {
|
||||
rules := make([]mappingrules.MappingRule, 0)
|
||||
modes := parser.GetModes()
|
||||
|
||||
// Golang can't inspect the concrete map type to determine interface conformance,
|
||||
// so we handle that here.
|
||||
pDevs := make(map[string]Device)
|
||||
for name, dev := range pInputDevs {
|
||||
pDevs[name] = dev
|
||||
}
|
||||
vDevs := make(map[string]Device)
|
||||
for name, dev := range vInputDevs {
|
||||
vDevs[name] = dev
|
||||
}
|
||||
|
||||
for _, ruleConfig := range parser.config.Rules {
|
||||
var newRule mappingrules.MappingRule
|
||||
var err error
|
||||
|
||||
if ok := validateModes(ruleConfig.Modes, modes); !ok {
|
||||
logger.Logf("Skipping rule '%s', mode list specifies undefined mode.", ruleConfig.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
base := mappingrules.NewMappingRuleBase(ruleConfig.Name, ruleConfig.Modes)
|
||||
|
||||
switch strings.ToLower(ruleConfig.Type) {
|
||||
case RuleTypeButton:
|
||||
newRule, err = makeMappingRuleButton(ruleConfig.Config.(RuleConfigButton), pDevs, vDevs, base)
|
||||
case RuleTypeButtonCombo:
|
||||
newRule, err = makeMappingRuleCombo(ruleConfig.Config.(RuleConfigButtonCombo), pDevs, vDevs, base)
|
||||
case RuleTypeButtonLatched:
|
||||
newRule, err = makeMappingRuleLatched(ruleConfig.Config.(RuleConfigButtonLatched), pDevs, vDevs, base)
|
||||
case RuleTypeAxis:
|
||||
newRule, err = makeMappingRuleAxis(ruleConfig.Config.(RuleConfigAxis), pDevs, vDevs, base)
|
||||
case RuleTypeAxisCombined:
|
||||
newRule, err = makeMappingRuleAxisCombined(ruleConfig.Config.(RuleConfigAxisCombined), pDevs, vDevs, base)
|
||||
case RuleTypeAxisToButton:
|
||||
newRule, err = makeMappingRuleAxisToButton(ruleConfig.Config.(RuleConfigAxisToButton), pDevs, vDevs, base)
|
||||
case RuleTypeAxisToRelaxis:
|
||||
newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig.Config.(RuleConfigAxisToRelaxis), pDevs, vDevs, base)
|
||||
case RuleTypeModeSelect:
|
||||
newRule, err = makeMappingRuleModeSelect(ruleConfig.Config.(RuleConfigModeSelect), pDevs, modes, base)
|
||||
default:
|
||||
err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.LogErrorf(err, "Failed to build rule '%s'", ruleConfig.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
rules = append(rules, newRule)
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
// TODO: how much of these functions could we fold into the unmarshaling logic itself? The main problem
|
||||
// is that we don't have access to the device maps in those functions... could we set device names
|
||||
// as stand-ins and do a post-processing pass that *just* handles device linking and possibly mode
|
||||
// checking?
|
||||
//
|
||||
// In other words - can we unmarshal the config directly into our target structs and remove most of
|
||||
// this library?
|
||||
func makeMappingRuleButton(ruleConfig RuleConfigButton,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) {
|
||||
|
||||
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleButton(base, input, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleCombo(ruleConfig RuleConfigButtonCombo,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) {
|
||||
|
||||
inputs := make([]*mappingrules.RuleTargetButton, 0)
|
||||
for _, inputConfig := range ruleConfig.Inputs {
|
||||
input, err := makeRuleTargetButton(inputConfig, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inputs = append(inputs, input)
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleLatched(ruleConfig RuleConfigButtonLatched,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) {
|
||||
|
||||
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleButtonLatched(base, input, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleAxis(ruleConfig RuleConfigAxis,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) {
|
||||
|
||||
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleAxis(base, input, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleAxisCombined(ruleConfig RuleConfigAxisCombined,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisCombined, error) {
|
||||
|
||||
inputLower, err := makeRuleTargetAxis(ruleConfig.InputLower, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inputUpper, err := makeRuleTargetAxis(ruleConfig.InputUpper, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleAxisCombined(base, inputLower, inputUpper, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleAxisToButton(ruleConfig RuleConfigAxisToButton,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) {
|
||||
|
||||
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleAxisToButton(base, input, output, ruleConfig.RepeatRateMin, ruleConfig.RepeatRateMax), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfigAxisToRelaxis,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) {
|
||||
|
||||
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetRelaxis(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleAxisToRelaxis(base,
|
||||
input, output,
|
||||
ruleConfig.RepeatRateMin,
|
||||
ruleConfig.RepeatRateMax,
|
||||
ruleConfig.Increment), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleModeSelect(ruleConfig RuleConfigModeSelect,
|
||||
pDevs map[string]Device,
|
||||
modes []string,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) {
|
||||
|
||||
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetModeSelect(ruleConfig.Output, modes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleModeSelect(base, input, output), nil
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package config
|
||||
|
||||
import "slices"
|
||||
|
||||
// validateModes checks the provided modes against a larger subset of modes (usually all defined ones)
|
||||
// and returns false if any of the modes are not defined.
|
||||
func validateModes(modes []string, allModes []string) bool {
|
||||
if len(modes) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
if !slices.Contains(allModes, mode) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
67
internal/configparser/configparser.go
Normal file
67
internal/configparser/configparser.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package configparser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
func ParseConfig(directory string) (*Config, error) {
|
||||
config := new(Config)
|
||||
|
||||
configFiles, err := getConfigFilePaths(directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open each yaml file and add its contents to the global config
|
||||
for _, filePath := range configFiles {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Error while opening config file")
|
||||
continue
|
||||
}
|
||||
|
||||
newConfig := Config{}
|
||||
err = yaml.Unmarshal(data, &newConfig)
|
||||
logger.LogIfError(err, "Error parsing YAML")
|
||||
config.Rules = append(config.Rules, newConfig.Rules...)
|
||||
config.Devices = append(config.Devices, newConfig.Devices...)
|
||||
config.Modes = append(config.Modes, newConfig.Modes...)
|
||||
}
|
||||
|
||||
if len(config.Devices) == 0 {
|
||||
return nil, errors.New("Found no devices in configuration. Please add configuration at " + directory)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func getConfigFilePaths(directory string) ([]string, error) {
|
||||
paths := make([]string, 0)
|
||||
|
||||
dirEntries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
err = os.Mkdir(directory, 0755)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to create config directory at " + directory)
|
||||
} else {
|
||||
return nil, errors.New("no config files found at " + directory)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range dirEntries {
|
||||
name := strings.ToLower(file.Name())
|
||||
if file.IsDir() || !(strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")) {
|
||||
continue
|
||||
}
|
||||
|
||||
paths = append(paths, filepath.Join(directory, file.Name()))
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// These types comprise the YAML schema for configuring Joyful.
|
||||
// The config files will be combined and then unmarshalled into this
|
||||
|
||||
package config
|
||||
package configparser
|
||||
|
||||
import (
|
||||
"fmt"
|
15
internal/configparser/variables.go
Normal file
15
internal/configparser/variables.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package configparser
|
||||
|
||||
const (
|
||||
DeviceTypePhysical = "physical"
|
||||
DeviceTypeVirtual = "virtual"
|
||||
|
||||
RuleTypeButton = "button"
|
||||
RuleTypeButtonCombo = "button-combo"
|
||||
RuleTypeButtonLatched = "button-latched"
|
||||
RuleTypeAxis = "axis"
|
||||
RuleTypeAxisCombined = "axis-combined"
|
||||
RuleTypeAxisToButton = "axis-to-button"
|
||||
RuleTypeAxisToRelaxis = "axis-to-relaxis"
|
||||
RuleTypeModeSelect = "mode-select"
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
package config
|
||||
package eventcodes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -8,17 +8,17 @@ import (
|
|||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
func parseCodeButton(code string) (evdev.EvCode, error) {
|
||||
func ParseCodeButton(code string) (evdev.EvCode, error) {
|
||||
prefix := CodePrefixButton
|
||||
|
||||
if strings.HasPrefix(code, CodePrefixKey+"_") {
|
||||
prefix = CodePrefixKey
|
||||
}
|
||||
|
||||
return parseCode(code, prefix)
|
||||
return ParseCode(code, prefix)
|
||||
}
|
||||
|
||||
func parseCode(code, prefix string) (evdev.EvCode, error) {
|
||||
func ParseCode(code, prefix string) (evdev.EvCode, error) {
|
||||
code = strings.ToUpper(code)
|
||||
|
||||
var codeLookup map[string]evdev.EvCode
|
||||
|
@ -70,3 +70,8 @@ func parseCode(code, prefix string) (evdev.EvCode, error) {
|
|||
return eventCode, nil
|
||||
}
|
||||
}
|
||||
|
||||
// hasError exists solely to switch on errors in conditional and case statements
|
||||
func hasError(_ any, err error) bool {
|
||||
return err != nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package config
|
||||
package eventcodes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -18,7 +18,7 @@ func TestRunnerEventCodeParserTests(t *testing.T) {
|
|||
|
||||
func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) {
|
||||
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
|
||||
code, err := parseCode(in, prefix)
|
||||
code, err := ParseCode(in, prefix)
|
||||
t.Nil(err)
|
||||
t.EqualValues(out, code)
|
||||
})
|
||||
|
@ -38,7 +38,7 @@ func (t *EventCodeParserTests) TestParseCodeButton() {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.in, func() {
|
||||
code, err := parseCodeButton(testCase.in)
|
||||
code, err := ParseCodeButton(testCase.in)
|
||||
t.Nil(err)
|
||||
t.EqualValues(code, testCase.out)
|
||||
})
|
||||
|
@ -134,7 +134,7 @@ func (t *EventCodeParserTests) TestParseCode() {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() {
|
||||
_, err := parseCode(testCase.in, testCase.prefix)
|
||||
_, err := ParseCode(testCase.in, testCase.prefix)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
90
internal/eventcodes/variables.go
Normal file
90
internal/eventcodes/variables.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package eventcodes
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
|
||||
const (
|
||||
CodePrefixButton = "BTN"
|
||||
CodePrefixKey = "KEY"
|
||||
CodePrefixAxis = "ABS"
|
||||
CodePrefixRelaxis = "REL"
|
||||
)
|
||||
|
||||
var (
|
||||
// Map joystick buttons to integer indices
|
||||
ButtonFromIndex = []evdev.EvCode{
|
||||
evdev.BTN_TRIGGER,
|
||||
evdev.BTN_THUMB,
|
||||
evdev.BTN_THUMB2,
|
||||
evdev.BTN_TOP,
|
||||
evdev.BTN_TOP2,
|
||||
evdev.BTN_PINKIE,
|
||||
evdev.BTN_BASE,
|
||||
evdev.BTN_BASE2,
|
||||
evdev.BTN_BASE3,
|
||||
evdev.BTN_BASE4,
|
||||
evdev.BTN_BASE5,
|
||||
evdev.BTN_BASE6,
|
||||
evdev.EvCode(0x12c), // decimal 300
|
||||
evdev.EvCode(0x12d), // decimal 301
|
||||
evdev.EvCode(0x12e), // decimal 302
|
||||
evdev.BTN_DEAD,
|
||||
evdev.BTN_TRIGGER_HAPPY1,
|
||||
evdev.BTN_TRIGGER_HAPPY2,
|
||||
evdev.BTN_TRIGGER_HAPPY3,
|
||||
evdev.BTN_TRIGGER_HAPPY4,
|
||||
evdev.BTN_TRIGGER_HAPPY5,
|
||||
evdev.BTN_TRIGGER_HAPPY6,
|
||||
evdev.BTN_TRIGGER_HAPPY7,
|
||||
evdev.BTN_TRIGGER_HAPPY8,
|
||||
evdev.BTN_TRIGGER_HAPPY9,
|
||||
evdev.BTN_TRIGGER_HAPPY10,
|
||||
evdev.BTN_TRIGGER_HAPPY11,
|
||||
evdev.BTN_TRIGGER_HAPPY12,
|
||||
evdev.BTN_TRIGGER_HAPPY13,
|
||||
evdev.BTN_TRIGGER_HAPPY14,
|
||||
evdev.BTN_TRIGGER_HAPPY15,
|
||||
evdev.BTN_TRIGGER_HAPPY16,
|
||||
evdev.BTN_TRIGGER_HAPPY17,
|
||||
evdev.BTN_TRIGGER_HAPPY18,
|
||||
evdev.BTN_TRIGGER_HAPPY19,
|
||||
evdev.BTN_TRIGGER_HAPPY20,
|
||||
evdev.BTN_TRIGGER_HAPPY21,
|
||||
evdev.BTN_TRIGGER_HAPPY22,
|
||||
evdev.BTN_TRIGGER_HAPPY23,
|
||||
evdev.BTN_TRIGGER_HAPPY24,
|
||||
evdev.BTN_TRIGGER_HAPPY25,
|
||||
evdev.BTN_TRIGGER_HAPPY26,
|
||||
evdev.BTN_TRIGGER_HAPPY27,
|
||||
evdev.BTN_TRIGGER_HAPPY28,
|
||||
evdev.BTN_TRIGGER_HAPPY29,
|
||||
evdev.BTN_TRIGGER_HAPPY30,
|
||||
evdev.BTN_TRIGGER_HAPPY31,
|
||||
evdev.BTN_TRIGGER_HAPPY32,
|
||||
evdev.BTN_TRIGGER_HAPPY33,
|
||||
evdev.BTN_TRIGGER_HAPPY34,
|
||||
evdev.BTN_TRIGGER_HAPPY35,
|
||||
evdev.BTN_TRIGGER_HAPPY36,
|
||||
evdev.BTN_TRIGGER_HAPPY37,
|
||||
evdev.BTN_TRIGGER_HAPPY38,
|
||||
evdev.BTN_TRIGGER_HAPPY39,
|
||||
evdev.BTN_TRIGGER_HAPPY40,
|
||||
evdev.EvCode(0x2e8),
|
||||
evdev.EvCode(0x2e9),
|
||||
evdev.EvCode(0x2f0),
|
||||
evdev.EvCode(0x2f1),
|
||||
evdev.EvCode(0x2f2),
|
||||
evdev.EvCode(0x2f3),
|
||||
evdev.EvCode(0x2f4),
|
||||
evdev.EvCode(0x2f5),
|
||||
evdev.EvCode(0x2f6),
|
||||
evdev.EvCode(0x2f7),
|
||||
evdev.EvCode(0x2f8),
|
||||
evdev.EvCode(0x2f9),
|
||||
evdev.EvCode(0x2fa),
|
||||
evdev.EvCode(0x2fb),
|
||||
evdev.EvCode(0x2fc),
|
||||
evdev.EvCode(0x2fd),
|
||||
evdev.EvCode(0x2fe),
|
||||
evdev.EvCode(0x2ff),
|
||||
}
|
||||
)
|
|
@ -1,9 +1,12 @@
|
|||
package config
|
||||
// TODO: these tests should live with their rule_target_* counterparts
|
||||
|
||||
package mappingrules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
@ -48,45 +51,45 @@ func (t *MakeRuleTargetsTests) SetupSuite() {
|
|||
}
|
||||
|
||||
func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() {
|
||||
config := RuleTargetConfigButton{Device: "test"}
|
||||
config := configparser.RuleTargetConfigButton{Device: "test"}
|
||||
|
||||
t.Run("Standard keycode", func() {
|
||||
config.Button = "BTN_TRIGGER"
|
||||
rule, err := makeRuleTargetButton(config, t.devs)
|
||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.BTN_TRIGGER, rule.Button)
|
||||
})
|
||||
|
||||
t.Run("Hex code", func() {
|
||||
config.Button = "0x2fd"
|
||||
rule, err := makeRuleTargetButton(config, t.devs)
|
||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.EvCode(0x2fd), rule.Button)
|
||||
})
|
||||
|
||||
t.Run("Index", func() {
|
||||
config.Button = "3"
|
||||
rule, err := makeRuleTargetButton(config, t.devs)
|
||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.BTN_TOP, rule.Button)
|
||||
})
|
||||
|
||||
t.Run("Index too high", func() {
|
||||
config.Button = "74"
|
||||
_, err := makeRuleTargetButton(config, t.devs)
|
||||
_, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
t.Run("Un-prefixed keycode", func() {
|
||||
config.Button = "pinkie"
|
||||
rule, err := makeRuleTargetButton(config, t.devs)
|
||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.BTN_PINKIE, rule.Button)
|
||||
})
|
||||
|
||||
t.Run("Invalid keycode", func() {
|
||||
config.Button = "foo"
|
||||
_, err := makeRuleTargetButton(config, t.devs)
|
||||
_, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
||||
|
@ -103,9 +106,9 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
|||
|
||||
for _, tc := range codeTestCases {
|
||||
t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() {
|
||||
config := RuleTargetConfigAxis{Device: "test"}
|
||||
config := configparser.RuleTargetConfigAxis{Device: "test"}
|
||||
config.Axis = tc.input
|
||||
rule, err := makeRuleTargetAxis(config, t.devs)
|
||||
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(tc.output, rule.Axis)
|
||||
|
||||
|
@ -113,18 +116,18 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
|||
}
|
||||
|
||||
t.Run("Invalid code", func() {
|
||||
config := RuleTargetConfigAxis{Device: "test"}
|
||||
config := configparser.RuleTargetConfigAxis{Device: "test"}
|
||||
config.Axis = "foo"
|
||||
_, err := makeRuleTargetAxis(config, t.devs)
|
||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
t.Run("Invalid deadzone", func() {
|
||||
config := RuleTargetConfigAxis{Device: "test"}
|
||||
config := configparser.RuleTargetConfigAxis{Device: "test"}
|
||||
config.Axis = "x"
|
||||
config.DeadzoneEnd = 100
|
||||
config.DeadzoneStart = 1000
|
||||
_, err := makeRuleTargetAxis(config, t.devs)
|
||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
|
@ -141,13 +144,13 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
|||
|
||||
for _, tc := range relDeadzoneTestCases {
|
||||
t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() {
|
||||
config := RuleTargetConfigAxis{
|
||||
config := configparser.RuleTargetConfigAxis{
|
||||
Device: "test",
|
||||
Axis: "x",
|
||||
DeadzoneCenter: tc.inCenter,
|
||||
DeadzoneSize: tc.inSize,
|
||||
}
|
||||
rule, err := makeRuleTargetAxis(config, t.devs)
|
||||
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
|
||||
t.Nil(err)
|
||||
t.Equal(tc.outStart, rule.DeadzoneStart)
|
||||
|
@ -156,13 +159,13 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
|||
}
|
||||
|
||||
t.Run("Deadzone center/size invalid center", func() {
|
||||
config := RuleTargetConfigAxis{
|
||||
config := configparser.RuleTargetConfigAxis{
|
||||
Device: "test",
|
||||
Axis: "x",
|
||||
DeadzoneCenter: 20000,
|
||||
DeadzoneSize: 500,
|
||||
}
|
||||
_, err := makeRuleTargetAxis(config, t.devs)
|
||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
|
@ -179,13 +182,13 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
|||
|
||||
for _, tc := range relDeadzonePercentTestCases {
|
||||
t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() {
|
||||
config := RuleTargetConfigAxis{
|
||||
config := configparser.RuleTargetConfigAxis{
|
||||
Device: "test",
|
||||
Axis: "x",
|
||||
DeadzoneCenter: tc.inCenter,
|
||||
DeadzoneSizePercent: tc.inSizePercent,
|
||||
}
|
||||
rule, err := makeRuleTargetAxis(config, t.devs)
|
||||
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
|
||||
t.Nil(err)
|
||||
t.Equal(tc.outStart, rule.DeadzoneStart)
|
||||
|
@ -194,50 +197,50 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
|||
}
|
||||
|
||||
t.Run("Deadzone center/percent invalid center", func() {
|
||||
config := RuleTargetConfigAxis{
|
||||
config := configparser.RuleTargetConfigAxis{
|
||||
Device: "test",
|
||||
Axis: "x",
|
||||
DeadzoneCenter: 20000,
|
||||
DeadzoneSizePercent: 10,
|
||||
}
|
||||
_, err := makeRuleTargetAxis(config, t.devs)
|
||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
|
||||
config := RuleTargetConfigRelaxis{Device: "test"}
|
||||
config := configparser.RuleTargetConfigRelaxis{Device: "test"}
|
||||
|
||||
t.Run("Standard keycode", func() {
|
||||
config.Axis = "REL_WHEEL"
|
||||
rule, err := makeRuleTargetRelaxis(config, t.devs)
|
||||
rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
|
||||
})
|
||||
|
||||
t.Run("Hex keycode", func() {
|
||||
config.Axis = "0x00"
|
||||
rule, err := makeRuleTargetRelaxis(config, t.devs)
|
||||
rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.REL_X, rule.Axis)
|
||||
})
|
||||
|
||||
t.Run("Un-prefixed keycode", func() {
|
||||
config.Axis = "wheel"
|
||||
rule, err := makeRuleTargetRelaxis(config, t.devs)
|
||||
rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
|
||||
})
|
||||
|
||||
t.Run("Invalid keycode", func() {
|
||||
config.Axis = "foo"
|
||||
_, err := makeRuleTargetRelaxis(config, t.devs)
|
||||
_, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
t.Run("Incorrect axis type", func() {
|
||||
config.Axis = "ABS_X"
|
||||
_, err := makeRuleTargetRelaxis(config, t.devs)
|
||||
_, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
79
internal/mappingrules/init_rules.go
Normal file
79
internal/mappingrules/init_rules.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package mappingrules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
func ConvertDeviceMap(inputDevs map[string]*evdev.InputDevice) map[string]Device {
|
||||
// Golang can't inspect the concrete map type to determine interface conformance,
|
||||
// so we handle that here.
|
||||
devices := make(map[string]Device)
|
||||
for name, dev := range inputDevs {
|
||||
devices[name] = dev
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
// NewRule parses a RuleConfig struct and creates and returns the appropriate rule type.
|
||||
// You can remap a map[string]*evdev.InputDevice to our interface type with ConvertDeviceMap
|
||||
func NewRule(config configparser.RuleConfig, pDevs map[string]Device, vDevs map[string]Device, modes []string) (MappingRule, error) {
|
||||
var newRule MappingRule
|
||||
var err error
|
||||
|
||||
if !validateModes(config.Modes, modes) {
|
||||
return nil, errors.New("mode list specifies undefined mode")
|
||||
}
|
||||
|
||||
base := NewMappingRuleBase(config.Name, config.Modes)
|
||||
|
||||
switch strings.ToLower(config.Type) {
|
||||
case RuleTypeButton:
|
||||
newRule, err = NewMappingRuleButton(config.Config.(configparser.RuleConfigButton), pDevs, vDevs, base)
|
||||
case RuleTypeButtonCombo:
|
||||
newRule, err = NewMappingRuleButtonCombo(config.Config.(configparser.RuleConfigButtonCombo), pDevs, vDevs, base)
|
||||
case RuleTypeButtonLatched:
|
||||
newRule, err = NewMappingRuleButtonLatched(config.Config.(configparser.RuleConfigButtonLatched), pDevs, vDevs, base)
|
||||
case RuleTypeAxis:
|
||||
newRule, err = NewMappingRuleAxis(config.Config.(configparser.RuleConfigAxis), pDevs, vDevs, base)
|
||||
case RuleTypeAxisCombined:
|
||||
newRule, err = NewMappingRuleAxisCombined(config.Config.(configparser.RuleConfigAxisCombined), pDevs, vDevs, base)
|
||||
case RuleTypeAxisToButton:
|
||||
newRule, err = NewMappingRuleAxisToButton(config.Config.(configparser.RuleConfigAxisToButton), pDevs, vDevs, base)
|
||||
case RuleTypeAxisToRelaxis:
|
||||
newRule, err = NewMappingRuleAxisToRelaxis(config.Config.(configparser.RuleConfigAxisToRelaxis), pDevs, vDevs, base)
|
||||
case RuleTypeModeSelect:
|
||||
newRule, err = NewMappingRuleModeSelect(config.Config.(configparser.RuleConfigModeSelect), pDevs, modes, base)
|
||||
default:
|
||||
err = fmt.Errorf("bad rule type '%s' for rule '%s'", config.Type, config.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.LogErrorf(err, "Failed to build rule '%s'", config.Name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newRule, nil
|
||||
}
|
||||
|
||||
// validateModes checks the provided modes against a larger subset of modes (usually all defined ones)
|
||||
// and returns false if any of the modes are not defined.
|
||||
func validateModes(modes []string, allModes []string) bool {
|
||||
if len(modes) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
if !slices.Contains(allModes, mode) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
|
||||
type MappingRuleAxis struct {
|
||||
|
@ -9,12 +12,26 @@ type MappingRuleAxis struct {
|
|||
Output *RuleTargetAxis
|
||||
}
|
||||
|
||||
func NewMappingRuleAxis(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxis {
|
||||
func NewMappingRuleAxis(ruleConfig configparser.RuleConfigAxis,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleAxis, error) {
|
||||
|
||||
input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetAxisFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleAxis{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package mappingrules
|
||||
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
@ -12,7 +13,26 @@ type MappingRuleAxisCombined struct {
|
|||
Output *RuleTargetAxis
|
||||
}
|
||||
|
||||
func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis, inputUpper *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxisCombined {
|
||||
func NewMappingRuleAxisCombined(ruleConfig configparser.RuleConfigAxisCombined,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleAxisCombined, error) {
|
||||
|
||||
inputLower, err := NewRuleTargetAxisFromConfig(ruleConfig.InputLower, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inputUpper, err := NewRuleTargetAxisFromConfig(ruleConfig.InputUpper, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetAxisFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inputLower.OutputMax = 0
|
||||
inputUpper.OutputMin = 0
|
||||
return &MappingRuleAxisCombined{
|
||||
|
@ -20,7 +40,7 @@ func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis
|
|||
InputLower: inputLower,
|
||||
InputUpper: inputUpper,
|
||||
Output: output,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -38,7 +38,9 @@ func (t *MappingRuleAxisCombinedTests) SetupTest() {
|
|||
}, nil)
|
||||
|
||||
t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0)
|
||||
t.inputTargetLower.OutputMax = 0
|
||||
t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0)
|
||||
t.inputTargetUpper.OutputMin = 0
|
||||
|
||||
t.outputDevice = &evdev.InputDevice{}
|
||||
t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0)
|
||||
|
@ -57,19 +59,30 @@ func (t *MappingRuleAxisCombinedTests) TearDownSubTest() {
|
|||
t.inputDevice.Reset()
|
||||
}
|
||||
|
||||
// TODO: this test sucks
|
||||
func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() {
|
||||
t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{
|
||||
evdev.ABS_X: {Minimum: 0, Maximum: 10000},
|
||||
evdev.ABS_Y: {Minimum: 0, Maximum: 10000},
|
||||
}, nil)
|
||||
|
||||
rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget)
|
||||
rule := &MappingRuleAxisCombined{
|
||||
MappingRuleBase: t.base,
|
||||
InputLower: t.inputTargetLower,
|
||||
InputUpper: t.inputTargetUpper,
|
||||
Output: t.outputTarget,
|
||||
}
|
||||
t.EqualValues(0, rule.InputLower.OutputMax)
|
||||
t.EqualValues(0, rule.InputUpper.OutputMin)
|
||||
}
|
||||
|
||||
func (t *MappingRuleAxisCombinedTests) TestMatchEvent() {
|
||||
rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget)
|
||||
rule := &MappingRuleAxisCombined{
|
||||
MappingRuleBase: t.base,
|
||||
InputLower: t.inputTargetLower,
|
||||
InputUpper: t.inputTargetUpper,
|
||||
Output: t.outputTarget,
|
||||
}
|
||||
|
||||
t.Run("Lower Input", func() {
|
||||
testCases := []struct{ in, out int32 }{
|
||||
|
|
|
@ -3,6 +3,7 @@ package mappingrules
|
|||
import (
|
||||
"time"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
@ -23,20 +24,34 @@ type MappingRuleAxisToButton struct {
|
|||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetButton, repeatRateMin, repeatRateMax int) *MappingRuleAxisToButton {
|
||||
func NewMappingRuleAxisToButton(ruleConfig configparser.RuleConfigAxisToButton,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleAxisToButton, error) {
|
||||
|
||||
input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleAxisToButton{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
RepeatRateMin: repeatRateMin,
|
||||
RepeatRateMax: repeatRateMax,
|
||||
RepeatRateMin: ruleConfig.RepeatRateMin,
|
||||
RepeatRateMax: ruleConfig.RepeatRateMax,
|
||||
lastEvent: time.Now(),
|
||||
nextEvent: NoNextEvent,
|
||||
repeat: repeatRateMin != 0 && repeatRateMax != 0,
|
||||
repeat: ruleConfig.RepeatRateMin != 0 && ruleConfig.RepeatRateMax != 0,
|
||||
pressed: false,
|
||||
active: false,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -19,6 +19,44 @@ type MappingRuleAxisToButtonTests struct {
|
|||
base MappingRuleBase
|
||||
}
|
||||
|
||||
func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) {
|
||||
suite.Run(t, new(MappingRuleAxisToButtonTests))
|
||||
}
|
||||
|
||||
// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock
|
||||
func (t *MappingRuleAxisToButtonTests) buildTimerRule(
|
||||
repeatMin,
|
||||
repeatMax int,
|
||||
nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) {
|
||||
|
||||
mockClock := clockwork.NewFakeClock()
|
||||
testRule := t.buildRule(repeatMin, repeatMax)
|
||||
testRule.clock = mockClock
|
||||
testRule.lastEvent = testRule.clock.Now()
|
||||
testRule.nextEvent = nextEvent
|
||||
if nextEvent != NoNextEvent {
|
||||
testRule.active = true
|
||||
}
|
||||
return testRule, mockClock
|
||||
}
|
||||
|
||||
// Todo: don't love this repeated logic...
|
||||
func (t *MappingRuleAxisToButtonTests) buildRule(repeatMin, repeatMax int) *MappingRuleAxisToButton {
|
||||
return &MappingRuleAxisToButton{
|
||||
MappingRuleBase: t.base,
|
||||
Input: t.inputRule,
|
||||
Output: t.outputRule,
|
||||
RepeatRateMin: repeatMin,
|
||||
RepeatRateMax: repeatMax,
|
||||
lastEvent: time.Now(),
|
||||
nextEvent: NoNextEvent,
|
||||
repeat: repeatMin != 0 && repeatMax != 0,
|
||||
pressed: false,
|
||||
active: false,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MappingRuleAxisToButtonTests) SetupTest() {
|
||||
mode := "*"
|
||||
t.mode = &mode
|
||||
|
@ -40,7 +78,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
|||
|
||||
// A valid input should set a nextevent
|
||||
t.Run("No Repeat", func() {
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0)
|
||||
testRule := t.buildRule(0, 0)
|
||||
|
||||
t.Run("Valid Input", func() {
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
|
@ -62,7 +100,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
|||
})
|
||||
|
||||
t.Run("Repeat", func() {
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250)
|
||||
testRule := t.buildRule(750, 250)
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
|
@ -90,7 +128,7 @@ 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)
|
||||
testRule, _ := t.buildTimerRule(0, 0, 0)
|
||||
|
||||
event := testRule.TimerEvent()
|
||||
|
||||
|
@ -100,7 +138,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
|
||||
// Off event on second call
|
||||
t.Run("Event emits off on second call", func() {
|
||||
testRule, _ := buildTimerRule(t, 0, 0, 0)
|
||||
testRule, _ := t.buildTimerRule(0, 0, 0)
|
||||
|
||||
testRule.TimerEvent()
|
||||
event := testRule.TimerEvent()
|
||||
|
@ -111,7 +149,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
|
||||
// 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, mockClock := t.buildTimerRule(0, 0, 0)
|
||||
|
||||
testRule.TimerEvent()
|
||||
testRule.TimerEvent()
|
||||
|
@ -125,13 +163,13 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
|
||||
t.Run("Repeat", func() {
|
||||
t.Run("No event if called immediately", func() {
|
||||
testRule, _ := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
testRule, _ := t.buildTimerRule(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)
|
||||
testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
||||
mockClock.Advance(49 * time.Millisecond)
|
||||
|
||||
event := testRule.TimerEvent()
|
||||
|
@ -140,7 +178,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
})
|
||||
|
||||
t.Run("Event after 50ms", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
||||
mockClock.Advance(50 * time.Millisecond)
|
||||
|
||||
event := testRule.TimerEvent()
|
||||
|
@ -150,7 +188,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
})
|
||||
|
||||
t.Run("Additional event at 100ms", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
||||
|
||||
mockClock.Advance(50 * time.Millisecond)
|
||||
testRule.TimerEvent()
|
||||
|
@ -163,24 +201,3 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package mappingrules
|
|||
import (
|
||||
"time"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
@ -23,23 +24,32 @@ type MappingRuleAxisToRelaxis struct {
|
|||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewMappingRuleAxisToRelaxis(
|
||||
base MappingRuleBase,
|
||||
input *RuleTargetAxis,
|
||||
output *RuleTargetRelaxis,
|
||||
repeatRateMin, repeatRateMax, increment int) *MappingRuleAxisToRelaxis {
|
||||
func NewMappingRuleAxisToRelaxis(ruleConfig configparser.RuleConfigAxisToRelaxis,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleAxisToRelaxis, error) {
|
||||
|
||||
input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetRelaxisFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleAxisToRelaxis{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
RepeatRateMin: repeatRateMin,
|
||||
RepeatRateMax: repeatRateMax,
|
||||
Increment: int32(increment),
|
||||
RepeatRateMin: ruleConfig.RepeatRateMin,
|
||||
RepeatRateMax: ruleConfig.RepeatRateMax,
|
||||
Increment: int32(ruleConfig.Increment),
|
||||
lastEvent: time.Now(),
|
||||
nextEvent: NoNextEvent,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleAxisToRelaxis) MatchEvent(
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
|
||||
type MappingRuleButton struct {
|
||||
|
@ -9,16 +12,26 @@ type MappingRuleButton struct {
|
|||
Output *RuleTargetButton
|
||||
}
|
||||
|
||||
func NewMappingRuleButton(
|
||||
base MappingRuleBase,
|
||||
input *RuleTargetButton,
|
||||
output *RuleTargetButton) *MappingRuleButton {
|
||||
func NewMappingRuleButton(ruleConfig configparser.RuleConfigButton,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleButton, error) {
|
||||
|
||||
input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleButton{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// A Combo Mapping Rule can require multiple physical button presses for a single output button
|
||||
type MappingRuleButtonCombo struct {
|
||||
|
@ -10,17 +13,31 @@ type MappingRuleButtonCombo struct {
|
|||
State int
|
||||
}
|
||||
|
||||
func NewMappingRuleButtonCombo(
|
||||
base MappingRuleBase,
|
||||
inputs []*RuleTargetButton,
|
||||
output *RuleTargetButton) *MappingRuleButtonCombo {
|
||||
func NewMappingRuleButtonCombo(ruleConfig configparser.RuleConfigButtonCombo,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleButtonCombo, error) {
|
||||
|
||||
inputs := make([]*RuleTargetButton, 0)
|
||||
for _, inputConfig := range ruleConfig.Inputs {
|
||||
input, err := NewRuleTargetButtonFromConfig(inputConfig, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inputs = append(inputs, input)
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleButtonCombo{
|
||||
MappingRuleBase: base,
|
||||
Inputs: inputs,
|
||||
Output: output,
|
||||
State: 0,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
type MappingRuleButtonLatched struct {
|
||||
MappingRuleBase
|
||||
|
@ -9,17 +12,27 @@ type MappingRuleButtonLatched struct {
|
|||
State bool
|
||||
}
|
||||
|
||||
func NewMappingRuleButtonLatched(
|
||||
base MappingRuleBase,
|
||||
input *RuleTargetButton,
|
||||
output *RuleTargetButton) *MappingRuleButtonLatched {
|
||||
func NewMappingRuleButtonLatched(ruleConfig configparser.RuleConfigButtonLatched,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleButtonLatched, error) {
|
||||
|
||||
input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleButtonLatched{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
State: false,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -28,7 +28,11 @@ func (t *MappingRuleButtonTests) SetupTest() {
|
|||
func (t *MappingRuleButtonTests) TestMatchEvent() {
|
||||
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false)
|
||||
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
|
||||
testRule := NewMappingRuleButton(t.base, inputButton, outputButton)
|
||||
testRule := &MappingRuleButton{
|
||||
MappingRuleBase: t.base,
|
||||
Input: inputButton,
|
||||
Output: outputButton,
|
||||
}
|
||||
|
||||
// A matching input event should produce an output event
|
||||
expected := &evdev.InputEvent{
|
||||
|
@ -58,7 +62,11 @@ func (t *MappingRuleButtonTests) TestMatchEvent() {
|
|||
func (t *MappingRuleButtonTests) TestMatchEventInverted() {
|
||||
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true)
|
||||
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
|
||||
testRule := NewMappingRuleButton(t.base, inputButton, outputButton)
|
||||
testRule := &MappingRuleButton{
|
||||
MappingRuleBase: t.base,
|
||||
Input: inputButton,
|
||||
Output: outputButton,
|
||||
}
|
||||
|
||||
// A matching input event should produce an output event
|
||||
expected := &evdev.InputEvent{
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
type MappingRuleModeSelect struct {
|
||||
MappingRuleBase
|
||||
|
@ -8,17 +11,26 @@ type MappingRuleModeSelect struct {
|
|||
Output *RuleTargetModeSelect
|
||||
}
|
||||
|
||||
func NewMappingRuleModeSelect(
|
||||
base MappingRuleBase,
|
||||
input *RuleTargetButton,
|
||||
output *RuleTargetModeSelect,
|
||||
) *MappingRuleModeSelect {
|
||||
func NewMappingRuleModeSelect(ruleConfig configparser.RuleConfigModeSelect,
|
||||
pDevs map[string]Device,
|
||||
modes []string,
|
||||
base MappingRuleBase) (*MappingRuleModeSelect, error) {
|
||||
|
||||
input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetModeSelectFromConfig(ruleConfig.Output, modes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleModeSelect{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleModeSelect) MatchEvent(
|
||||
|
|
|
@ -28,3 +28,16 @@ func Clamp[T Numeric](value, min, max T) T {
|
|||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func clampAndShift(start, end, min, max int32) (int32, int32) {
|
||||
if start < min {
|
||||
end += min - start
|
||||
start = min
|
||||
}
|
||||
if end > max {
|
||||
start -= end - max
|
||||
end = max
|
||||
}
|
||||
|
||||
return start, end
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
|
@ -20,6 +22,77 @@ type RuleTargetAxis struct {
|
|||
deadzoneSize int32
|
||||
}
|
||||
|
||||
func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, devs map[string]Device) (*RuleTargetAxis, error) {
|
||||
device, ok := devs[targetConfig.Device]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||
}
|
||||
|
||||
if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart {
|
||||
return nil, errors.New("deadzone_end must be greater than deadzone_start")
|
||||
}
|
||||
|
||||
eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixAxis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRuleTargetAxis(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
targetConfig.Inverted,
|
||||
deadzoneStart,
|
||||
deadzoneEnd,
|
||||
)
|
||||
}
|
||||
|
||||
// calculateDeadzones produces the deadzone start and end values in absolute terms
|
||||
func calculateDeadzones(targetConfig configparser.RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) {
|
||||
|
||||
var deadzoneStart, deadzoneEnd int32
|
||||
deadzoneStart = 0
|
||||
deadzoneEnd = 0
|
||||
|
||||
if targetConfig.DeadzoneStart != 0 || targetConfig.DeadzoneEnd != 0 {
|
||||
return targetConfig.DeadzoneStart, targetConfig.DeadzoneEnd, nil
|
||||
}
|
||||
|
||||
var min, max int32
|
||||
absInfoMap, err := device.AbsInfos()
|
||||
|
||||
if err != nil {
|
||||
min = AxisValueMin
|
||||
max = AxisValueMax
|
||||
} else {
|
||||
absInfo := absInfoMap[axis]
|
||||
min = absInfo.Minimum
|
||||
max = absInfo.Maximum
|
||||
}
|
||||
|
||||
if targetConfig.DeadzoneCenter < min || targetConfig.DeadzoneCenter > max {
|
||||
return 0, 0, fmt.Errorf("deadzone_center '%d' is out of bounds", targetConfig.DeadzoneCenter)
|
||||
}
|
||||
|
||||
switch {
|
||||
case targetConfig.DeadzoneSize != 0:
|
||||
deadzoneStart = targetConfig.DeadzoneCenter - targetConfig.DeadzoneSize/2
|
||||
deadzoneEnd = targetConfig.DeadzoneCenter + targetConfig.DeadzoneSize/2
|
||||
case targetConfig.DeadzoneSizePercent != 0:
|
||||
deadzoneSize := (max - min) / targetConfig.DeadzoneSizePercent
|
||||
deadzoneStart = targetConfig.DeadzoneCenter - deadzoneSize/2
|
||||
deadzoneEnd = targetConfig.DeadzoneCenter + deadzoneSize/2
|
||||
}
|
||||
|
||||
deadzoneStart, deadzoneEnd = clampAndShift(deadzoneStart, deadzoneEnd, min, max)
|
||||
return deadzoneStart, deadzoneEnd, nil
|
||||
}
|
||||
|
||||
func NewRuleTargetAxis(device_name string,
|
||||
device Device,
|
||||
axis evdev.EvCode,
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
type RuleTargetButton struct {
|
||||
DeviceName string
|
||||
|
@ -9,6 +15,25 @@ type RuleTargetButton struct {
|
|||
Inverted bool
|
||||
}
|
||||
|
||||
func NewRuleTargetButtonFromConfig(targetConfig configparser.RuleTargetConfigButton, devs map[string]Device) (*RuleTargetButton, error) {
|
||||
device, ok := devs[targetConfig.Device]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||
}
|
||||
|
||||
eventCode, err := eventcodes.ParseCodeButton(targetConfig.Button)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRuleTargetButton(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
targetConfig.Inverted,
|
||||
)
|
||||
}
|
||||
|
||||
func NewRuleTargetButton(device_name string, device Device, code evdev.EvCode, inverted bool) (*RuleTargetButton, error) {
|
||||
return &RuleTargetButton{
|
||||
DeviceName: device_name,
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"slices"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
@ -12,6 +13,14 @@ type RuleTargetModeSelect struct {
|
|||
Modes []string
|
||||
}
|
||||
|
||||
func NewRuleTargetModeSelectFromConfig(targetConfig configparser.RuleTargetConfigModeSelect, allModes []string) (*RuleTargetModeSelect, error) {
|
||||
if ok := validateModes(targetConfig.Modes, allModes); !ok {
|
||||
return nil, errors.New("undefined mode in mode select list")
|
||||
}
|
||||
|
||||
return NewRuleTargetModeSelect(targetConfig.Modes)
|
||||
}
|
||||
|
||||
func NewRuleTargetModeSelect(modes []string) (*RuleTargetModeSelect, error) {
|
||||
if len(modes) == 0 {
|
||||
return nil, errors.New("cannot create RuleTargetModeSelect: mode list is empty")
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package mappingrules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
|
@ -10,12 +14,30 @@ type RuleTargetRelaxis struct {
|
|||
Axis evdev.EvCode
|
||||
}
|
||||
|
||||
func NewRuleTargetRelaxis(device_name string,
|
||||
func NewRuleTargetRelaxisFromConfig(targetConfig configparser.RuleTargetConfigRelaxis, devs map[string]Device) (*RuleTargetRelaxis, error) {
|
||||
device, ok := devs[targetConfig.Device]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||
}
|
||||
|
||||
eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixRelaxis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRuleTargetRelaxis(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
)
|
||||
}
|
||||
|
||||
func NewRuleTargetRelaxis(deviceName string,
|
||||
device Device,
|
||||
axis evdev.EvCode) (*RuleTargetRelaxis, error) {
|
||||
|
||||
return &RuleTargetRelaxis{
|
||||
DeviceName: device_name,
|
||||
DeviceName: deviceName,
|
||||
Device: device,
|
||||
Axis: axis,
|
||||
}, nil
|
||||
|
|
12
internal/mappingrules/variables.go
Normal file
12
internal/mappingrules/variables.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package mappingrules
|
||||
|
||||
const (
|
||||
RuleTypeButton = "button"
|
||||
RuleTypeButtonCombo = "button-combo"
|
||||
RuleTypeButtonLatched = "button-latched"
|
||||
RuleTypeAxis = "axis"
|
||||
RuleTypeAxisCombined = "axis-combined"
|
||||
RuleTypeAxisToButton = "axis-to-button"
|
||||
RuleTypeAxisToRelaxis = "axis-to-relaxis"
|
||||
RuleTypeModeSelect = "mode-select"
|
||||
)
|
|
@ -1,35 +0,0 @@
|
|||
// Functions for cleaning up stale virtual devices
|
||||
|
||||
package virtualdevice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
func CleanupStaleVirtualDevices() {
|
||||
devices, err := evdev.ListDevicePaths()
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't list devices while running cleanup: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, devicePath := range devices {
|
||||
if strings.HasPrefix(devicePath.Name, "joyful-joystick") {
|
||||
device, err := evdev.Open(devicePath.Path)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open existing joyful device at '%s': %s\n", devicePath.Path, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
err = evdev.DestroyDevice(device)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to destroy existing joyful device '%s' at '%s': %s\n", devicePath.Name, devicePath.Path, err.Error())
|
||||
} else {
|
||||
fmt.Printf("Destroyed stale joyful device '%s'\n", devicePath.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,13 +11,7 @@ import (
|
|||
type EventBuffer struct {
|
||||
events []*evdev.InputEvent
|
||||
Device VirtualDevice
|
||||
}
|
||||
|
||||
func NewEventBuffer(device VirtualDevice) *EventBuffer {
|
||||
return &EventBuffer{
|
||||
events: make([]*evdev.InputEvent, 0, 100),
|
||||
Device: device,
|
||||
}
|
||||
Name string
|
||||
}
|
||||
|
||||
func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) {
|
||||
|
|
|
@ -12,9 +12,10 @@ import (
|
|||
type EventBufferTests struct {
|
||||
suite.Suite
|
||||
device *VirtualDeviceMock
|
||||
writeOneCall *mock.Call
|
||||
buffer *EventBuffer
|
||||
}
|
||||
|
||||
// Mocks
|
||||
type VirtualDeviceMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
@ -24,65 +25,65 @@ func (m *VirtualDeviceMock) WriteOne(event *evdev.InputEvent) error {
|
|||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Setup
|
||||
func TestRunnerEventBufferTests(t *testing.T) {
|
||||
suite.Run(t, new(EventBufferTests))
|
||||
}
|
||||
|
||||
func (t *EventBufferTests) SetupTest() {
|
||||
t.device = new(VirtualDeviceMock)
|
||||
}
|
||||
|
||||
func (t *EventBufferTests) SetupSubTest() {
|
||||
t.device = new(VirtualDeviceMock)
|
||||
t.writeOneCall = t.device.On("WriteOne").Return(nil)
|
||||
}
|
||||
|
||||
func (t *EventBufferTests) TearDownSubTest() {
|
||||
t.writeOneCall.Unset()
|
||||
t.buffer = &EventBuffer{Device: t.device}
|
||||
}
|
||||
|
||||
// Tests
|
||||
func (t *EventBufferTests) TestNewEventBuffer() {
|
||||
buffer := NewEventBuffer(t.device)
|
||||
t.Equal(t.device, buffer.Device)
|
||||
t.Len(buffer.events, 0)
|
||||
t.Equal(t.device, t.buffer.Device)
|
||||
t.Len(t.buffer.events, 0)
|
||||
}
|
||||
|
||||
func (t *EventBufferTests) TestEventBufferAddEvent() {
|
||||
buffer := NewEventBuffer(t.device)
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.Len(buffer.events, 3)
|
||||
}
|
||||
func (t *EventBufferTests) TestEventBuffer() {
|
||||
|
||||
func (t *EventBufferTests) TestEventBufferSendEvents() {
|
||||
t.Run("AddEvent", func() {
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.Len(t.buffer.events, 3)
|
||||
})
|
||||
|
||||
t.Run("SendEvents", func() {
|
||||
t.Run("3 Events", func() {
|
||||
buffer := NewEventBuffer(t.device)
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
errs := buffer.SendEvents()
|
||||
writeOneCall := t.device.On("WriteOne").Return(nil)
|
||||
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
errs := t.buffer.SendEvents()
|
||||
|
||||
t.Len(errs, 0)
|
||||
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4)
|
||||
|
||||
writeOneCall.Unset()
|
||||
})
|
||||
|
||||
t.Run("No Events", func() {
|
||||
buffer := NewEventBuffer(t.device)
|
||||
errs := buffer.SendEvents()
|
||||
writeOneCall := t.device.On("WriteOne").Return(nil)
|
||||
|
||||
errs := t.buffer.SendEvents()
|
||||
|
||||
t.Len(errs, 0)
|
||||
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0)
|
||||
|
||||
writeOneCall.Unset()
|
||||
})
|
||||
|
||||
t.Run("Bad Event", func() {
|
||||
t.writeOneCall.Unset()
|
||||
t.writeOneCall = t.device.On("WriteOne").Return(errors.New("Fail"))
|
||||
writeOneCall := t.device.On("WriteOne").Return(errors.New("Fail"))
|
||||
|
||||
buffer := NewEventBuffer(t.device)
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
errs := buffer.SendEvents()
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
errs := t.buffer.SendEvents()
|
||||
t.Len(errs, 2)
|
||||
})
|
||||
|
||||
writeOneCall.Unset()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
165
internal/virtualdevice/init.go
Normal file
165
internal/virtualdevice/init.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package virtualdevice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// NewEventBuffer takes a virtual device config specification, creates the underlying
|
||||
// evdev.InputDevice, and wraps it in a buffered event emitter.
|
||||
func NewEventBuffer(config configparser.DeviceConfigVirtual) (*EventBuffer, error) {
|
||||
deviceMap := make(map[string]*evdev.InputDevice)
|
||||
|
||||
name := fmt.Sprintf("joyful-%s", config.Name)
|
||||
|
||||
var capabilities map[evdev.EvType][]evdev.EvCode
|
||||
|
||||
// todo: add tests for presets
|
||||
switch config.Preset {
|
||||
case DevicePresetGamepad:
|
||||
capabilities = CapabilitiesPresetGamepad
|
||||
case DevicePresetKeyboard:
|
||||
capabilities = CapabilitiesPresetKeyboard
|
||||
case DevicePresetJoystick:
|
||||
capabilities = CapabilitiesPresetJoystick
|
||||
case DevicePresetMouse:
|
||||
capabilities = CapabilitiesPresetMouse
|
||||
default:
|
||||
capabilities = map[evdev.EvType][]evdev.EvCode{
|
||||
evdev.EV_KEY: makeButtons(config.NumButtons, config.Buttons),
|
||||
evdev.EV_ABS: makeAxes(config.NumAxes, config.Axes),
|
||||
evdev.EV_REL: makeRelativeAxes(config.NumRelativeAxes, config.RelativeAxes),
|
||||
}
|
||||
}
|
||||
|
||||
device, err := evdev.CreateDevice(
|
||||
name,
|
||||
// TODO: placeholders. Who knows what these should actually be...
|
||||
evdev.InputID{
|
||||
BusType: 0x03,
|
||||
Vendor: 0x4711,
|
||||
Product: 0x0816,
|
||||
Version: 1,
|
||||
},
|
||||
capabilities,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deviceMap[config.Name] = device
|
||||
logger.Log(fmt.Sprintf(
|
||||
"Created virtual device '%s' with %d buttons, %d axes, and %d relative axes",
|
||||
name,
|
||||
len(capabilities[evdev.EV_KEY]),
|
||||
len(capabilities[evdev.EV_ABS]),
|
||||
len(capabilities[evdev.EV_REL]),
|
||||
))
|
||||
|
||||
return &EventBuffer{
|
||||
events: make([]*evdev.InputEvent, 0, 100),
|
||||
Device: device,
|
||||
Name: config.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO: these functions have a lot of duplication; we need to figure out how to refactor it cleanly
|
||||
// without losing logging context...
|
||||
func makeButtons(numButtons int, buttonList []string) []evdev.EvCode {
|
||||
if numButtons > 0 && len(buttonList) > 0 {
|
||||
logger.Log("'num_buttons' and 'buttons' both specified, ignoring 'num_buttons'")
|
||||
}
|
||||
|
||||
if numButtons > VirtualDeviceMaxButtons {
|
||||
numButtons = VirtualDeviceMaxButtons
|
||||
logger.Logf("Limiting virtual device buttons to %d", VirtualDeviceMaxButtons)
|
||||
}
|
||||
|
||||
if len(buttonList) > 0 {
|
||||
buttons := make([]evdev.EvCode, 0, len(buttonList))
|
||||
for _, codeStr := range buttonList {
|
||||
code, err := eventcodes.ParseCode(codeStr, "BTN")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create button, skipping")
|
||||
continue
|
||||
}
|
||||
buttons = append(buttons, code)
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
buttons := make([]evdev.EvCode, numButtons)
|
||||
|
||||
for i := 0; i < numButtons; i++ {
|
||||
buttons[i] = eventcodes.ButtonFromIndex[i]
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
func makeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||
if numAxes > 0 && len(axisList) > 0 {
|
||||
logger.Log("'num_axes' and 'axes' both specified, ignoring 'num_axes'")
|
||||
}
|
||||
|
||||
if len(axisList) > 0 {
|
||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||
for _, codeStr := range axisList {
|
||||
code, err := eventcodes.ParseCode(codeStr, "ABS")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create axis, skipping")
|
||||
continue
|
||||
}
|
||||
axes = append(axes, code)
|
||||
}
|
||||
return axes
|
||||
}
|
||||
|
||||
if numAxes > 8 {
|
||||
numAxes = 8
|
||||
logger.Log("Limiting virtual device axes to 8")
|
||||
}
|
||||
|
||||
axes := make([]evdev.EvCode, numAxes)
|
||||
for i := 0; i < numAxes; i++ {
|
||||
axes[i] = evdev.EvCode(i)
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
||||
|
||||
func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||
if numAxes > 0 && len(axisList) > 0 {
|
||||
logger.Log("'num_rel_axes' and 'rel_axes' both specified, ignoring 'num_rel_axes'")
|
||||
}
|
||||
|
||||
if len(axisList) > 0 {
|
||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||
for _, codeStr := range axisList {
|
||||
code, err := eventcodes.ParseCode(codeStr, "REL")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create axis, skipping")
|
||||
continue
|
||||
}
|
||||
axes = append(axes, code)
|
||||
}
|
||||
return axes
|
||||
}
|
||||
|
||||
if numAxes > 10 {
|
||||
numAxes = 10
|
||||
logger.Log("Limiting virtual device relative axes to 10")
|
||||
}
|
||||
|
||||
axes := make([]evdev.EvCode, numAxes)
|
||||
for i := 0; i < numAxes; i++ {
|
||||
axes[i] = evdev.EvCode(i)
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package config
|
||||
package virtualdevice
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -7,15 +7,15 @@ import (
|
|||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type DevicesConfigTests struct {
|
||||
type InitTests struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestRunnerDevicesConfig(t *testing.T) {
|
||||
suite.Run(t, new(DevicesConfigTests))
|
||||
func TestRunnerInit(t *testing.T) {
|
||||
suite.Run(t, new(InitTests))
|
||||
}
|
||||
|
||||
func (t *DevicesConfigTests) TestMakeButtons() {
|
||||
func (t *InitTests) TestMakeButtons() {
|
||||
t.Run("Maximum buttons", func() {
|
||||
buttons := makeButtons(VirtualDeviceMaxButtons, []string{})
|
||||
t.Equal(VirtualDeviceMaxButtons, len(buttons))
|
||||
|
@ -44,7 +44,7 @@ func (t *DevicesConfigTests) TestMakeButtons() {
|
|||
})
|
||||
}
|
||||
|
||||
func (t *DevicesConfigTests) TestMakeAxes() {
|
||||
func (t *InitTests) TestMakeAxes() {
|
||||
t.Run("8 axes", func() {
|
||||
axes := makeAxes(8, []string{})
|
||||
t.Equal(8, len(axes))
|
||||
|
@ -81,7 +81,7 @@ func (t *DevicesConfigTests) TestMakeAxes() {
|
|||
})
|
||||
}
|
||||
|
||||
func (t *DevicesConfigTests) TestMakeRelativeAxes() {
|
||||
func (t *InitTests) TestMakeRelativeAxes() {
|
||||
t.Run("10 axes", func() {
|
||||
axes := makeRelativeAxes(10, []string{})
|
||||
t.Equal(10, len(axes))
|
|
@ -1,114 +1,16 @@
|
|||
package config
|
||||
package virtualdevice
|
||||
|
||||
import (
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
import "github.com/holoplot/go-evdev"
|
||||
|
||||
const (
|
||||
DeviceTypePhysical = "physical"
|
||||
DeviceTypeVirtual = "virtual"
|
||||
|
||||
DevicePresetKeyboard = "keyboard"
|
||||
DevicePresetGamepad = "gamepad"
|
||||
DevicePresetJoystick = "joystick"
|
||||
DevicePresetMouse = "mouse"
|
||||
|
||||
RuleTypeButton = "button"
|
||||
RuleTypeButtonCombo = "button-combo"
|
||||
RuleTypeButtonLatched = "button-latched"
|
||||
RuleTypeAxis = "axis"
|
||||
RuleTypeAxisCombined = "axis-combined"
|
||||
RuleTypeAxisToButton = "axis-to-button"
|
||||
RuleTypeAxisToRelaxis = "axis-to-relaxis"
|
||||
RuleTypeModeSelect = "mode-select"
|
||||
|
||||
CodePrefixButton = "BTN"
|
||||
CodePrefixKey = "KEY"
|
||||
CodePrefixAxis = "ABS"
|
||||
CodePrefixRelaxis = "REL"
|
||||
|
||||
VirtualDeviceMaxButtons = 74
|
||||
)
|
||||
|
||||
var (
|
||||
ButtonFromIndex = []evdev.EvCode{
|
||||
evdev.BTN_TRIGGER,
|
||||
evdev.BTN_THUMB,
|
||||
evdev.BTN_THUMB2,
|
||||
evdev.BTN_TOP,
|
||||
evdev.BTN_TOP2,
|
||||
evdev.BTN_PINKIE,
|
||||
evdev.BTN_BASE,
|
||||
evdev.BTN_BASE2,
|
||||
evdev.BTN_BASE3,
|
||||
evdev.BTN_BASE4,
|
||||
evdev.BTN_BASE5,
|
||||
evdev.BTN_BASE6,
|
||||
evdev.EvCode(0x12c), // decimal 300
|
||||
evdev.EvCode(0x12d), // decimal 301
|
||||
evdev.EvCode(0x12e), // decimal 302
|
||||
evdev.BTN_DEAD,
|
||||
evdev.BTN_TRIGGER_HAPPY1,
|
||||
evdev.BTN_TRIGGER_HAPPY2,
|
||||
evdev.BTN_TRIGGER_HAPPY3,
|
||||
evdev.BTN_TRIGGER_HAPPY4,
|
||||
evdev.BTN_TRIGGER_HAPPY5,
|
||||
evdev.BTN_TRIGGER_HAPPY6,
|
||||
evdev.BTN_TRIGGER_HAPPY7,
|
||||
evdev.BTN_TRIGGER_HAPPY8,
|
||||
evdev.BTN_TRIGGER_HAPPY9,
|
||||
evdev.BTN_TRIGGER_HAPPY10,
|
||||
evdev.BTN_TRIGGER_HAPPY11,
|
||||
evdev.BTN_TRIGGER_HAPPY12,
|
||||
evdev.BTN_TRIGGER_HAPPY13,
|
||||
evdev.BTN_TRIGGER_HAPPY14,
|
||||
evdev.BTN_TRIGGER_HAPPY15,
|
||||
evdev.BTN_TRIGGER_HAPPY16,
|
||||
evdev.BTN_TRIGGER_HAPPY17,
|
||||
evdev.BTN_TRIGGER_HAPPY18,
|
||||
evdev.BTN_TRIGGER_HAPPY19,
|
||||
evdev.BTN_TRIGGER_HAPPY20,
|
||||
evdev.BTN_TRIGGER_HAPPY21,
|
||||
evdev.BTN_TRIGGER_HAPPY22,
|
||||
evdev.BTN_TRIGGER_HAPPY23,
|
||||
evdev.BTN_TRIGGER_HAPPY24,
|
||||
evdev.BTN_TRIGGER_HAPPY25,
|
||||
evdev.BTN_TRIGGER_HAPPY26,
|
||||
evdev.BTN_TRIGGER_HAPPY27,
|
||||
evdev.BTN_TRIGGER_HAPPY28,
|
||||
evdev.BTN_TRIGGER_HAPPY29,
|
||||
evdev.BTN_TRIGGER_HAPPY30,
|
||||
evdev.BTN_TRIGGER_HAPPY31,
|
||||
evdev.BTN_TRIGGER_HAPPY32,
|
||||
evdev.BTN_TRIGGER_HAPPY33,
|
||||
evdev.BTN_TRIGGER_HAPPY34,
|
||||
evdev.BTN_TRIGGER_HAPPY35,
|
||||
evdev.BTN_TRIGGER_HAPPY36,
|
||||
evdev.BTN_TRIGGER_HAPPY37,
|
||||
evdev.BTN_TRIGGER_HAPPY38,
|
||||
evdev.BTN_TRIGGER_HAPPY39,
|
||||
evdev.BTN_TRIGGER_HAPPY40,
|
||||
evdev.EvCode(0x2e8),
|
||||
evdev.EvCode(0x2e9),
|
||||
evdev.EvCode(0x2f0),
|
||||
evdev.EvCode(0x2f1),
|
||||
evdev.EvCode(0x2f2),
|
||||
evdev.EvCode(0x2f3),
|
||||
evdev.EvCode(0x2f4),
|
||||
evdev.EvCode(0x2f5),
|
||||
evdev.EvCode(0x2f6),
|
||||
evdev.EvCode(0x2f7),
|
||||
evdev.EvCode(0x2f8),
|
||||
evdev.EvCode(0x2f9),
|
||||
evdev.EvCode(0x2fa),
|
||||
evdev.EvCode(0x2fb),
|
||||
evdev.EvCode(0x2fc),
|
||||
evdev.EvCode(0x2fd),
|
||||
evdev.EvCode(0x2fe),
|
||||
evdev.EvCode(0x2ff),
|
||||
}
|
||||
)
|
||||
|
||||
// Device Presets
|
||||
var (
|
||||
CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{
|
Loading…
Add table
Add a link
Reference in a new issue