* Move all physical device initialization logic to main functions

* Move all virtual device initialization to virtualbuffer package.
* Factor out common eventcode helper logic into a new package.
This commit is contained in:
Anna Rose Wiggins 2025-08-11 14:54:02 -04:00
parent 1b374bccc6
commit 727985f91c
17 changed files with 777 additions and 771 deletions

View file

@ -5,7 +5,8 @@ import (
"slices" "slices"
// TODO: using config here feels like bad coupling... ButtonFromIndex might need a refactor / move // 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" "git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
@ -20,7 +21,7 @@ func isJoystickLike(device *evdev.InputDevice) bool {
if slices.Contains(types, evdev.EV_KEY) { if slices.Contains(types, evdev.EV_KEY) {
buttons := device.CapableEvents(evdev.EV_KEY) buttons := device.CapableEvents(evdev.EV_KEY)
for _, code := range configparser.ButtonFromIndex { for _, code := range eventcodes.ButtonFromIndex {
if slices.Contains(buttons, code) { if slices.Contains(buttons, code) {
return true return true
} }

129
cmd/joyful/config.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -1,19 +1,15 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"sync"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"git.annabunches.net/annabunches/joyful/internal/configparser" "git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/logger"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"git.annabunches.net/annabunches/joyful/internal/virtualdevice"
) )
func getConfigDir(dir string) string { func getConfigDir(dir string) string {
@ -21,24 +17,6 @@ func getConfigDir(dir string) string {
return os.ExpandEnv(configDir) 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() { func main() {
// parse command-line // parse command-line
var configFlag string var configFlag string
@ -62,20 +40,26 @@ func main() {
// Initialize physical devices // Initialize physical devices
pDevices := initPhysicalDevices(config) pDevices := initPhysicalDevices(config)
// Load the rules // initialize the mode variables
rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName) var mode string
modes := config.Modes
if len(modes) == 0 {
mode = "*"
} else {
mode = config.Modes[0]
}
// initialize the mode variable // Load the rules
mode := config.GetModes()[0] rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName, modes)
// initialize TTS phrases for modes // initialize TTS phrases for modes
for _, m := range config.GetModes() { for _, m := range modes {
tts.AddMessage(m) tts.AddMessage(m)
logger.LogDebugf("Added TTS message '%s'", m) logger.LogDebugf("Added TTS message '%s'", m)
} }
fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.") 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) logger.Logf("Initial mode set to '%s'", mode)
} }
@ -113,13 +97,18 @@ func main() {
case ChannelEventReload: case ChannelEventReload:
// stop existing channels // 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.") fmt.Println("Reloading rules.")
cancel() cancel()
fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.") fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.")
wg.Wait() wg.Wait()
fmt.Println("Listeners exited. Parsing config.") fmt.Println("Listeners exited. Loading new rules.")
config := readConfig(configDir) // reload the config rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName, modes)
rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName)
fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.") 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
}

View file

@ -8,74 +8,6 @@ import (
"github.com/holoplot/go-evdev" "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 // InitPhysicalDevices will create InputDevices corresponding to any registered
// devices with type = physical. // devices with type = physical.
// //
@ -123,99 +55,3 @@ func (parser *ConfigParser) InitPhysicalDevices() map[string]*evdev.InputDevice
return deviceMap 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
}

View file

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
"git.annabunches.net/annabunches/joyful/internal/mappingrules" "git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev" "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) return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
} }
eventCode, err := parseCodeButton(targetConfig.Button) eventCode, err := eventcodes.ParseCodeButton(targetConfig.Button)
if err != nil { if err != nil {
return nil, err 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") 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 { if err != nil {
return nil, err return nil, err
} }
@ -63,7 +64,7 @@ func makeRuleTargetRelaxis(targetConfig RuleTargetConfigRelaxis, devs map[string
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) 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 { if err != nil {
return nil, err return nil, err
} }
@ -83,11 +84,6 @@ func makeRuleTargetModeSelect(targetConfig RuleTargetConfigModeSelect, allModes
return mappingrules.NewRuleTargetModeSelect(targetConfig.Modes) 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 // 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, // TODO: on the one hand, this logic feels betten encapsulated in mappingrules. On the other hand,
// passing even more parameters to NewRuleTargetAxis feels terrible // passing even more parameters to NewRuleTargetAxis feels terrible

View file

@ -14,9 +14,8 @@ import (
// This would speed up rule matching by only checking relevant rules for a given input event. // 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 // 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. // 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) rules := make([]mappingrules.MappingRule, 0)
modes := parser.GetModes()
// Golang can't inspect the concrete map type to determine interface conformance, // Golang can't inspect the concrete map type to determine interface conformance,
// so we handle that here. // so we handle that here.
@ -29,7 +28,7 @@ func (parser *ConfigParser) InitRules(pInputDevs map[string]*evdev.InputDevice,
vDevs[name] = dev vDevs[name] = dev
} }
for _, ruleConfig := range parser.config.Rules { for _, ruleConfig := range config {
var newRule mappingrules.MappingRule var newRule mappingrules.MappingRule
var err error var err error

View file

@ -1,18 +1,9 @@
package configparser package configparser
import (
"github.com/holoplot/go-evdev"
)
const ( const (
DeviceTypePhysical = "physical" DeviceTypePhysical = "physical"
DeviceTypeVirtual = "virtual" DeviceTypeVirtual = "virtual"
DevicePresetKeyboard = "keyboard"
DevicePresetGamepad = "gamepad"
DevicePresetJoystick = "joystick"
DevicePresetMouse = "mouse"
RuleTypeButton = "button" RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo" RuleTypeButtonCombo = "button-combo"
RuleTypeButtonLatched = "button-latched" RuleTypeButtonLatched = "button-latched"
@ -21,368 +12,4 @@ const (
RuleTypeAxisToButton = "axis-to-button" RuleTypeAxisToButton = "axis-to-button"
RuleTypeAxisToRelaxis = "axis-to-relaxis" RuleTypeAxisToRelaxis = "axis-to-relaxis"
RuleTypeModeSelect = "mode-select" 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,
},
}
) )

View file

@ -1,4 +1,4 @@
package configparser package eventcodes
import ( import (
"fmt" "fmt"
@ -8,17 +8,17 @@ import (
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
func parseCodeButton(code string) (evdev.EvCode, error) { func ParseCodeButton(code string) (evdev.EvCode, error) {
prefix := CodePrefixButton prefix := CodePrefixButton
if strings.HasPrefix(code, CodePrefixKey+"_") { if strings.HasPrefix(code, CodePrefixKey+"_") {
prefix = 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) code = strings.ToUpper(code)
var codeLookup map[string]evdev.EvCode var codeLookup map[string]evdev.EvCode
@ -70,3 +70,8 @@ func parseCode(code, prefix string) (evdev.EvCode, error) {
return eventCode, nil return eventCode, nil
} }
} }
// hasError exists solely to switch on errors in conditional and case statements
func hasError(_ any, err error) bool {
return err != nil
}

View file

@ -1,4 +1,4 @@
package configparser package eventcodes
import ( import (
"fmt" "fmt"
@ -18,7 +18,7 @@ func TestRunnerEventCodeParserTests(t *testing.T) {
func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) { func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) {
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() { t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
code, err := parseCode(in, prefix) code, err := ParseCode(in, prefix)
t.Nil(err) t.Nil(err)
t.EqualValues(out, code) t.EqualValues(out, code)
}) })
@ -38,7 +38,7 @@ func (t *EventCodeParserTests) TestParseCodeButton() {
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.in, func() { t.Run(testCase.in, func() {
code, err := parseCodeButton(testCase.in) code, err := ParseCodeButton(testCase.in)
t.Nil(err) t.Nil(err)
t.EqualValues(code, testCase.out) t.EqualValues(code, testCase.out)
}) })
@ -134,7 +134,7 @@ func (t *EventCodeParserTests) TestParseCode() {
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() { 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) t.NotNil(err)
}) })
} }

View 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),
}
)

View file

@ -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)
}
}
}
}

View file

@ -11,13 +11,7 @@ import (
type EventBuffer struct { type EventBuffer struct {
events []*evdev.InputEvent events []*evdev.InputEvent
Device VirtualDevice Device VirtualDevice
} Name string
func NewEventBuffer(device VirtualDevice) *EventBuffer {
return &EventBuffer{
events: make([]*evdev.InputEvent, 0, 100),
Device: device,
}
} }
func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) { func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) {

View file

@ -11,10 +11,11 @@ import (
type EventBufferTests struct { type EventBufferTests struct {
suite.Suite suite.Suite
device *VirtualDeviceMock device *VirtualDeviceMock
writeOneCall *mock.Call buffer *EventBuffer
} }
// Mocks
type VirtualDeviceMock struct { type VirtualDeviceMock struct {
mock.Mock mock.Mock
} }
@ -24,65 +25,65 @@ func (m *VirtualDeviceMock) WriteOne(event *evdev.InputEvent) error {
return args.Error(0) return args.Error(0)
} }
// Setup
func TestRunnerEventBufferTests(t *testing.T) { func TestRunnerEventBufferTests(t *testing.T) {
suite.Run(t, new(EventBufferTests)) suite.Run(t, new(EventBufferTests))
} }
func (t *EventBufferTests) SetupTest() {
t.device = new(VirtualDeviceMock)
}
func (t *EventBufferTests) SetupSubTest() { func (t *EventBufferTests) SetupSubTest() {
t.device = new(VirtualDeviceMock) t.device = new(VirtualDeviceMock)
t.writeOneCall = t.device.On("WriteOne").Return(nil) t.buffer = &EventBuffer{Device: t.device}
}
func (t *EventBufferTests) TearDownSubTest() {
t.writeOneCall.Unset()
} }
// Tests
func (t *EventBufferTests) TestNewEventBuffer() { func (t *EventBufferTests) TestNewEventBuffer() {
buffer := NewEventBuffer(t.device) t.Equal(t.device, t.buffer.Device)
t.Equal(t.device, buffer.Device) t.Len(t.buffer.events, 0)
t.Len(buffer.events, 0)
} }
func (t *EventBufferTests) TestEventBufferAddEvent() { func (t *EventBufferTests) TestEventBuffer() {
buffer := NewEventBuffer(t.device)
buffer.AddEvent(&evdev.InputEvent{}) t.Run("AddEvent", func() {
buffer.AddEvent(&evdev.InputEvent{}) t.buffer.AddEvent(&evdev.InputEvent{})
buffer.AddEvent(&evdev.InputEvent{}) t.buffer.AddEvent(&evdev.InputEvent{})
t.Len(buffer.events, 3) t.buffer.AddEvent(&evdev.InputEvent{})
} t.Len(t.buffer.events, 3)
})
func (t *EventBufferTests) TestEventBufferSendEvents() {
t.Run("3 Events", func() { t.Run("SendEvents", func() {
buffer := NewEventBuffer(t.device) t.Run("3 Events", func() {
buffer.AddEvent(&evdev.InputEvent{}) writeOneCall := t.device.On("WriteOne").Return(nil)
buffer.AddEvent(&evdev.InputEvent{})
buffer.AddEvent(&evdev.InputEvent{}) t.buffer.AddEvent(&evdev.InputEvent{})
errs := buffer.SendEvents() t.buffer.AddEvent(&evdev.InputEvent{})
t.buffer.AddEvent(&evdev.InputEvent{})
t.Len(errs, 0) errs := t.buffer.SendEvents()
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4)
}) t.Len(errs, 0)
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4)
t.Run("No Events", func() {
buffer := NewEventBuffer(t.device) writeOneCall.Unset()
errs := buffer.SendEvents() })
t.Len(errs, 0) t.Run("No Events", func() {
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0) writeOneCall := t.device.On("WriteOne").Return(nil)
})
errs := t.buffer.SendEvents()
t.Run("Bad Event", func() {
t.writeOneCall.Unset() t.Len(errs, 0)
t.writeOneCall = t.device.On("WriteOne").Return(errors.New("Fail")) t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0)
buffer := NewEventBuffer(t.device) writeOneCall.Unset()
buffer.AddEvent(&evdev.InputEvent{}) })
errs := buffer.SendEvents()
t.Len(errs, 2) 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()
})
})
} }

View 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
}

View file

@ -1,4 +1,4 @@
package configparser package virtualdevice
import ( import (
"testing" "testing"
@ -7,15 +7,15 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
type DevicesConfigTests struct { type InitTests struct {
suite.Suite suite.Suite
} }
func TestRunnerDevicesConfig(t *testing.T) { func TestRunnerInit(t *testing.T) {
suite.Run(t, new(DevicesConfigTests)) suite.Run(t, new(InitTests))
} }
func (t *DevicesConfigTests) TestMakeButtons() { func (t *InitTests) TestMakeButtons() {
t.Run("Maximum buttons", func() { t.Run("Maximum buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons, []string{}) buttons := makeButtons(VirtualDeviceMaxButtons, []string{})
t.Equal(VirtualDeviceMaxButtons, len(buttons)) 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() { t.Run("8 axes", func() {
axes := makeAxes(8, []string{}) axes := makeAxes(8, []string{})
t.Equal(8, len(axes)) 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() { t.Run("10 axes", func() {
axes := makeRelativeAxes(10, []string{}) axes := makeRelativeAxes(10, []string{})
t.Equal(10, len(axes)) t.Equal(10, len(axes))

View file

@ -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,
},
}
)