diff --git a/cmd/evinfo/main.go b/cmd/evinfo/main.go index dd551d3..12a0ecb 100644 --- a/cmd/evinfo/main.go +++ b/cmd/evinfo/main.go @@ -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/configparser" + + "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 configparser.ButtonFromIndex { + for _, code := range eventcodes.ButtonFromIndex { if slices.Contains(buttons, code) { return true } diff --git a/cmd/joyful/config.go b/cmd/joyful/config.go new file mode 100644 index 0000000..997b0fc --- /dev/null +++ b/cmd/joyful/config.go @@ -0,0 +1,129 @@ +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 +} + +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()) + + // Initialize rules + rules := configparser.InitRules(config.Rules, pDevices, vDevices, modes) + 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 +} diff --git a/cmd/joyful/device_init.go b/cmd/joyful/device_init.go deleted file mode 100644 index fad6842..0000000 --- a/cmd/joyful/device_init.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "git.annabunches.net/annabunches/joyful/internal/configparser" - "git.annabunches.net/annabunches/joyful/internal/logger" - "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 devConfig.Type != configparser.DeviceTypePhysical { - continue - } - - name, device, err := initPhysicalDevice(devConfig.Config.(configparser.DeviceConfigPhysical)) - if err != nil { - logger.LogError(err, "Failed to initialize device") - } - pDeviceMap[name] = device - } - - 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 -} diff --git a/cmd/joyful/main.go b/cmd/joyful/main.go index 8a0ab15..bcdeccc 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -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/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,24 +17,6 @@ func getConfigDir(dir string) string { return os.ExpandEnv(configDir) } -func initVirtualBuffers(config *configparser.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 main() { // parse command-line var configFlag string @@ -62,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) } @@ -113,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.") } @@ -128,37 +117,3 @@ func main() { } } } - -func loadRules( - config *configparser.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 -} diff --git a/internal/configparser/devices.go b/internal/configparser/devices.go index bb60532..23029ff 100644 --- a/internal/configparser/devices.go +++ b/internal/configparser/devices.go @@ -8,74 +8,6 @@ import ( "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. // @@ -123,99 +55,3 @@ func (parser *ConfigParser) InitPhysicalDevices() map[string]*evdev.InputDevice 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 -} diff --git a/internal/configparser/make_rule_targets.go b/internal/configparser/make_rule_targets.go index 4ae2fc6..f86473c 100644 --- a/internal/configparser/make_rule_targets.go +++ b/internal/configparser/make_rule_targets.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "git.annabunches.net/annabunches/joyful/internal/eventcodes" "git.annabunches.net/annabunches/joyful/internal/mappingrules" "github.com/holoplot/go-evdev" ) @@ -14,7 +15,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfigButton, devs map[string]D return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) } - eventCode, err := parseCodeButton(targetConfig.Button) + eventCode, err := eventcodes.ParseCodeButton(targetConfig.Button) if err != nil { return nil, err } @@ -37,7 +38,7 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfigAxis, devs map[string]Devic return nil, errors.New("deadzone_end must be greater than deadzone_start") } - eventCode, err := parseCode(targetConfig.Axis, CodePrefixAxis) + eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixAxis) if err != nil { return nil, err } @@ -63,7 +64,7 @@ func makeRuleTargetRelaxis(targetConfig RuleTargetConfigRelaxis, devs map[string return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) } - eventCode, err := parseCode(targetConfig.Axis, CodePrefixRelaxis) + eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixRelaxis) if err != nil { return nil, err } @@ -83,11 +84,6 @@ func makeRuleTargetModeSelect(targetConfig RuleTargetConfigModeSelect, allModes 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 diff --git a/internal/configparser/make_rules.go b/internal/configparser/make_rules.go index 9434de2..bffd94b 100644 --- a/internal/configparser/make_rules.go +++ b/internal/configparser/make_rules.go @@ -14,9 +14,8 @@ import ( // 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[][]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 { +func InitRules(config []RuleConfig, pInputDevs map[string]*evdev.InputDevice, vInputDevs map[string]*evdev.InputDevice, modes []string) []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. @@ -29,7 +28,7 @@ func (parser *ConfigParser) InitRules(pInputDevs map[string]*evdev.InputDevice, vDevs[name] = dev } - for _, ruleConfig := range parser.config.Rules { + for _, ruleConfig := range config { var newRule mappingrules.MappingRule var err error diff --git a/internal/configparser/variables.go b/internal/configparser/variables.go index 2bcc9d0..77e2b9c 100644 --- a/internal/configparser/variables.go +++ b/internal/configparser/variables.go @@ -1,18 +1,9 @@ package configparser -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" @@ -21,368 +12,4 @@ const ( 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{ - evdev.EV_ABS: { - evdev.ABS_X, - evdev.ABS_Y, - evdev.ABS_Z, - evdev.ABS_RX, - evdev.ABS_RY, - evdev.ABS_RZ, - evdev.ABS_HAT0X, - evdev.ABS_HAT0Y, - }, - evdev.EV_KEY: { - evdev.BTN_NORTH, // Xbox 'X', Playstation 'Square' - evdev.BTN_SOUTH, // Xbox 'A', Plastation 'X' - evdev.BTN_WEST, // Xbox 'Y', Playstation 'Triangle' - evdev.BTN_EAST, // Xbox 'B', Playstation 'O' - evdev.BTN_THUMBL, - evdev.BTN_THUMBR, - evdev.BTN_TL, - evdev.BTN_TR, - evdev.BTN_SELECT, - evdev.BTN_START, - evdev.BTN_MODE, - }, - } - - CapabilitiesPresetJoystick = map[evdev.EvType][]evdev.EvCode{ - evdev.EV_ABS: { - evdev.ABS_X, - evdev.ABS_Y, - evdev.ABS_Z, - evdev.ABS_RX, - evdev.ABS_RY, - evdev.ABS_RZ, - evdev.ABS_THROTTLE, // Also called "Slider" or "Slider1" - evdev.ABS_RUDDER, // Also called "Dial", "Slider2", or "RSlider" - }, - evdev.EV_KEY: { - 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), - }, - } - - CapabilitiesPresetKeyboard = map[evdev.EvType][]evdev.EvCode{ - evdev.EV_KEY: { - evdev.KEY_ESC, - evdev.KEY_1, - evdev.KEY_2, - evdev.KEY_3, - evdev.KEY_4, - evdev.KEY_5, - evdev.KEY_6, - evdev.KEY_7, - evdev.KEY_8, - evdev.KEY_9, - evdev.KEY_0, - evdev.KEY_MINUS, - evdev.KEY_EQUAL, - evdev.KEY_BACKSPACE, - evdev.KEY_TAB, - evdev.KEY_Q, - evdev.KEY_W, - evdev.KEY_E, - evdev.KEY_R, - evdev.KEY_T, - evdev.KEY_Y, - evdev.KEY_U, - evdev.KEY_I, - evdev.KEY_O, - evdev.KEY_P, - evdev.KEY_LEFTBRACE, - evdev.KEY_RIGHTBRACE, - evdev.KEY_ENTER, - evdev.KEY_LEFTCTRL, - evdev.KEY_A, - evdev.KEY_S, - evdev.KEY_D, - evdev.KEY_F, - evdev.KEY_G, - evdev.KEY_H, - evdev.KEY_J, - evdev.KEY_K, - evdev.KEY_L, - evdev.KEY_SEMICOLON, - evdev.KEY_APOSTROPHE, - evdev.KEY_GRAVE, - evdev.KEY_LEFTSHIFT, - evdev.KEY_BACKSLASH, - evdev.KEY_Z, - evdev.KEY_X, - evdev.KEY_C, - evdev.KEY_V, - evdev.KEY_B, - evdev.KEY_N, - evdev.KEY_M, - evdev.KEY_COMMA, - evdev.KEY_DOT, - evdev.KEY_SLASH, - evdev.KEY_RIGHTSHIFT, - evdev.KEY_KPASTERISK, - evdev.KEY_LEFTALT, - evdev.KEY_SPACE, - evdev.KEY_CAPSLOCK, - evdev.KEY_F1, - evdev.KEY_F2, - evdev.KEY_F3, - evdev.KEY_F4, - evdev.KEY_F5, - evdev.KEY_F6, - evdev.KEY_F7, - evdev.KEY_F8, - evdev.KEY_F9, - evdev.KEY_F10, - evdev.KEY_NUMLOCK, - evdev.KEY_SCROLLLOCK, - evdev.KEY_KP7, - evdev.KEY_KP8, - evdev.KEY_KP9, - evdev.KEY_KPMINUS, - evdev.KEY_KP4, - evdev.KEY_KP5, - evdev.KEY_KP6, - evdev.KEY_KPPLUS, - evdev.KEY_KP1, - evdev.KEY_KP2, - evdev.KEY_KP3, - evdev.KEY_KP0, - evdev.KEY_KPDOT, - evdev.KEY_ZENKAKUHANKAKU, - evdev.KEY_102ND, - evdev.KEY_F11, - evdev.KEY_F12, - evdev.KEY_RO, - evdev.KEY_KATAKANA, - evdev.KEY_HIRAGANA, - evdev.KEY_HENKAN, - evdev.KEY_KATAKANAHIRAGANA, - evdev.KEY_MUHENKAN, - evdev.KEY_KPJPCOMMA, - evdev.KEY_KPENTER, - evdev.KEY_RIGHTCTRL, - evdev.KEY_KPSLASH, - evdev.KEY_SYSRQ, - evdev.KEY_RIGHTALT, - evdev.KEY_LINEFEED, - evdev.KEY_HOME, - evdev.KEY_UP, - evdev.KEY_PAGEUP, - evdev.KEY_LEFT, - evdev.KEY_RIGHT, - evdev.KEY_END, - evdev.KEY_DOWN, - evdev.KEY_PAGEDOWN, - evdev.KEY_INSERT, - evdev.KEY_DELETE, - evdev.KEY_MACRO, - evdev.KEY_MUTE, - evdev.KEY_VOLUMEDOWN, - evdev.KEY_VOLUMEUP, - evdev.KEY_KPEQUAL, - evdev.KEY_KPPLUSMINUS, - evdev.KEY_PAUSE, - evdev.KEY_SCALE, - evdev.KEY_KPCOMMA, - evdev.KEY_HANGEUL, - evdev.KEY_HANJA, - evdev.KEY_YEN, - evdev.KEY_LEFTMETA, - evdev.KEY_RIGHTMETA, - evdev.KEY_COMPOSE, - evdev.KEY_F13, - evdev.KEY_F14, - evdev.KEY_F15, - evdev.KEY_F16, - evdev.KEY_F17, - evdev.KEY_F18, - evdev.KEY_F19, - evdev.KEY_F20, - evdev.KEY_F21, - evdev.KEY_F22, - evdev.KEY_F23, - evdev.KEY_F24, - }, - } - - CapabilitiesPresetMouse = map[evdev.EvType][]evdev.EvCode{ - evdev.EV_REL: { - evdev.REL_X, - evdev.REL_Y, - evdev.REL_WHEEL, - evdev.REL_HWHEEL, - }, - evdev.EV_KEY: { - evdev.BTN_LEFT, - evdev.BTN_MIDDLE, - evdev.BTN_RIGHT, - evdev.BTN_SIDE, - evdev.BTN_EXTRA, - evdev.BTN_FORWARD, - evdev.BTN_BACK, - }, - } ) diff --git a/internal/configparser/codes.go b/internal/eventcodes/codes.go similarity index 81% rename from internal/configparser/codes.go rename to internal/eventcodes/codes.go index 994b2b7..a7515a8 100644 --- a/internal/configparser/codes.go +++ b/internal/eventcodes/codes.go @@ -1,4 +1,4 @@ -package configparser +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 +} diff --git a/internal/configparser/codes_test.go b/internal/eventcodes/codes_test.go similarity index 94% rename from internal/configparser/codes_test.go rename to internal/eventcodes/codes_test.go index 107ec24..4d72526 100644 --- a/internal/configparser/codes_test.go +++ b/internal/eventcodes/codes_test.go @@ -1,4 +1,4 @@ -package configparser +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) }) } diff --git a/internal/eventcodes/variables.go b/internal/eventcodes/variables.go new file mode 100644 index 0000000..d63b92d --- /dev/null +++ b/internal/eventcodes/variables.go @@ -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), + } +) diff --git a/internal/virtualdevice/cleanup.go b/internal/virtualdevice/cleanup.go deleted file mode 100644 index 9839f6b..0000000 --- a/internal/virtualdevice/cleanup.go +++ /dev/null @@ -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) - } - } - } -} diff --git a/internal/virtualdevice/eventbuffer.go b/internal/virtualdevice/eventbuffer.go index 9a46341..5364a5d 100644 --- a/internal/virtualdevice/eventbuffer.go +++ b/internal/virtualdevice/eventbuffer.go @@ -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) { diff --git a/internal/virtualdevice/eventbuffer_test.go b/internal/virtualdevice/eventbuffer_test.go index 515de5f..df8c7ff 100644 --- a/internal/virtualdevice/eventbuffer_test.go +++ b/internal/virtualdevice/eventbuffer_test.go @@ -11,10 +11,11 @@ import ( type EventBufferTests struct { suite.Suite - device *VirtualDeviceMock - writeOneCall *mock.Call + device *VirtualDeviceMock + 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) TestEventBufferSendEvents() { - t.Run("3 Events", func() { - buffer := NewEventBuffer(t.device) - buffer.AddEvent(&evdev.InputEvent{}) - buffer.AddEvent(&evdev.InputEvent{}) - buffer.AddEvent(&evdev.InputEvent{}) - errs := buffer.SendEvents() - - t.Len(errs, 0) - t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4) - }) - - t.Run("No Events", func() { - buffer := NewEventBuffer(t.device) - errs := buffer.SendEvents() - - t.Len(errs, 0) - t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0) - }) - - t.Run("Bad Event", func() { - t.writeOneCall.Unset() - t.writeOneCall = t.device.On("WriteOne").Return(errors.New("Fail")) - - buffer := NewEventBuffer(t.device) - buffer.AddEvent(&evdev.InputEvent{}) - errs := buffer.SendEvents() - t.Len(errs, 2) - }) - +func (t *EventBufferTests) TestEventBuffer() { + + 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() { + 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() { + 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() { + writeOneCall := t.device.On("WriteOne").Return(errors.New("Fail")) + + t.buffer.AddEvent(&evdev.InputEvent{}) + errs := t.buffer.SendEvents() + t.Len(errs, 2) + + writeOneCall.Unset() + }) + }) } diff --git a/internal/virtualdevice/init.go b/internal/virtualdevice/init.go new file mode 100644 index 0000000..14f1c04 --- /dev/null +++ b/internal/virtualdevice/init.go @@ -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 +} diff --git a/internal/configparser/devices_test.go b/internal/virtualdevice/init_test.go similarity index 91% rename from internal/configparser/devices_test.go rename to internal/virtualdevice/init_test.go index 509a606..a6e631c 100644 --- a/internal/configparser/devices_test.go +++ b/internal/virtualdevice/init_test.go @@ -1,4 +1,4 @@ -package configparser +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)) diff --git a/internal/virtualdevice/variables.go b/internal/virtualdevice/variables.go new file mode 100644 index 0000000..11adb46 --- /dev/null +++ b/internal/virtualdevice/variables.go @@ -0,0 +1,290 @@ +package virtualdevice + +import "github.com/holoplot/go-evdev" + +const ( + DevicePresetKeyboard = "keyboard" + DevicePresetGamepad = "gamepad" + DevicePresetJoystick = "joystick" + DevicePresetMouse = "mouse" + + VirtualDeviceMaxButtons = 74 +) + +// Device Presets +var ( + CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{ + evdev.EV_ABS: { + evdev.ABS_X, + evdev.ABS_Y, + evdev.ABS_Z, + evdev.ABS_RX, + evdev.ABS_RY, + evdev.ABS_RZ, + evdev.ABS_HAT0X, + evdev.ABS_HAT0Y, + }, + evdev.EV_KEY: { + evdev.BTN_NORTH, // Xbox 'X', Playstation 'Square' + evdev.BTN_SOUTH, // Xbox 'A', Plastation 'X' + evdev.BTN_WEST, // Xbox 'Y', Playstation 'Triangle' + evdev.BTN_EAST, // Xbox 'B', Playstation 'O' + evdev.BTN_THUMBL, + evdev.BTN_THUMBR, + evdev.BTN_TL, + evdev.BTN_TR, + evdev.BTN_SELECT, + evdev.BTN_START, + evdev.BTN_MODE, + }, + } + + CapabilitiesPresetJoystick = map[evdev.EvType][]evdev.EvCode{ + evdev.EV_ABS: { + evdev.ABS_X, + evdev.ABS_Y, + evdev.ABS_Z, + evdev.ABS_RX, + evdev.ABS_RY, + evdev.ABS_RZ, + evdev.ABS_THROTTLE, // Also called "Slider" or "Slider1" + evdev.ABS_RUDDER, // Also called "Dial", "Slider2", or "RSlider" + }, + evdev.EV_KEY: { + 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), + }, + } + + CapabilitiesPresetKeyboard = map[evdev.EvType][]evdev.EvCode{ + evdev.EV_KEY: { + evdev.KEY_ESC, + evdev.KEY_1, + evdev.KEY_2, + evdev.KEY_3, + evdev.KEY_4, + evdev.KEY_5, + evdev.KEY_6, + evdev.KEY_7, + evdev.KEY_8, + evdev.KEY_9, + evdev.KEY_0, + evdev.KEY_MINUS, + evdev.KEY_EQUAL, + evdev.KEY_BACKSPACE, + evdev.KEY_TAB, + evdev.KEY_Q, + evdev.KEY_W, + evdev.KEY_E, + evdev.KEY_R, + evdev.KEY_T, + evdev.KEY_Y, + evdev.KEY_U, + evdev.KEY_I, + evdev.KEY_O, + evdev.KEY_P, + evdev.KEY_LEFTBRACE, + evdev.KEY_RIGHTBRACE, + evdev.KEY_ENTER, + evdev.KEY_LEFTCTRL, + evdev.KEY_A, + evdev.KEY_S, + evdev.KEY_D, + evdev.KEY_F, + evdev.KEY_G, + evdev.KEY_H, + evdev.KEY_J, + evdev.KEY_K, + evdev.KEY_L, + evdev.KEY_SEMICOLON, + evdev.KEY_APOSTROPHE, + evdev.KEY_GRAVE, + evdev.KEY_LEFTSHIFT, + evdev.KEY_BACKSLASH, + evdev.KEY_Z, + evdev.KEY_X, + evdev.KEY_C, + evdev.KEY_V, + evdev.KEY_B, + evdev.KEY_N, + evdev.KEY_M, + evdev.KEY_COMMA, + evdev.KEY_DOT, + evdev.KEY_SLASH, + evdev.KEY_RIGHTSHIFT, + evdev.KEY_KPASTERISK, + evdev.KEY_LEFTALT, + evdev.KEY_SPACE, + evdev.KEY_CAPSLOCK, + evdev.KEY_F1, + evdev.KEY_F2, + evdev.KEY_F3, + evdev.KEY_F4, + evdev.KEY_F5, + evdev.KEY_F6, + evdev.KEY_F7, + evdev.KEY_F8, + evdev.KEY_F9, + evdev.KEY_F10, + evdev.KEY_NUMLOCK, + evdev.KEY_SCROLLLOCK, + evdev.KEY_KP7, + evdev.KEY_KP8, + evdev.KEY_KP9, + evdev.KEY_KPMINUS, + evdev.KEY_KP4, + evdev.KEY_KP5, + evdev.KEY_KP6, + evdev.KEY_KPPLUS, + evdev.KEY_KP1, + evdev.KEY_KP2, + evdev.KEY_KP3, + evdev.KEY_KP0, + evdev.KEY_KPDOT, + evdev.KEY_ZENKAKUHANKAKU, + evdev.KEY_102ND, + evdev.KEY_F11, + evdev.KEY_F12, + evdev.KEY_RO, + evdev.KEY_KATAKANA, + evdev.KEY_HIRAGANA, + evdev.KEY_HENKAN, + evdev.KEY_KATAKANAHIRAGANA, + evdev.KEY_MUHENKAN, + evdev.KEY_KPJPCOMMA, + evdev.KEY_KPENTER, + evdev.KEY_RIGHTCTRL, + evdev.KEY_KPSLASH, + evdev.KEY_SYSRQ, + evdev.KEY_RIGHTALT, + evdev.KEY_LINEFEED, + evdev.KEY_HOME, + evdev.KEY_UP, + evdev.KEY_PAGEUP, + evdev.KEY_LEFT, + evdev.KEY_RIGHT, + evdev.KEY_END, + evdev.KEY_DOWN, + evdev.KEY_PAGEDOWN, + evdev.KEY_INSERT, + evdev.KEY_DELETE, + evdev.KEY_MACRO, + evdev.KEY_MUTE, + evdev.KEY_VOLUMEDOWN, + evdev.KEY_VOLUMEUP, + evdev.KEY_KPEQUAL, + evdev.KEY_KPPLUSMINUS, + evdev.KEY_PAUSE, + evdev.KEY_SCALE, + evdev.KEY_KPCOMMA, + evdev.KEY_HANGEUL, + evdev.KEY_HANJA, + evdev.KEY_YEN, + evdev.KEY_LEFTMETA, + evdev.KEY_RIGHTMETA, + evdev.KEY_COMPOSE, + evdev.KEY_F13, + evdev.KEY_F14, + evdev.KEY_F15, + evdev.KEY_F16, + evdev.KEY_F17, + evdev.KEY_F18, + evdev.KEY_F19, + evdev.KEY_F20, + evdev.KEY_F21, + evdev.KEY_F22, + evdev.KEY_F23, + evdev.KEY_F24, + }, + } + + CapabilitiesPresetMouse = map[evdev.EvType][]evdev.EvCode{ + evdev.EV_REL: { + evdev.REL_X, + evdev.REL_Y, + evdev.REL_WHEEL, + evdev.REL_HWHEEL, + }, + evdev.EV_KEY: { + evdev.BTN_LEFT, + evdev.BTN_MIDDLE, + evdev.BTN_RIGHT, + evdev.BTN_SIDE, + evdev.BTN_EXTRA, + evdev.BTN_FORWARD, + evdev.BTN_BACK, + }, + } +)