├── .travis.yml ├── go.mod ├── .gitignore ├── go.sum ├── joystickunsupported.go ├── LICENSE ├── ioctl_linux.go ├── README.md ├── joystick_common.go ├── joysticktest └── joysticktest.go ├── joystick_darwin.c ├── joystick_linux.go ├── joystick_windows.go └── joystick_darwin.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/0xcafed00d/joystick 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/nsf/termbox-go v1.1.1 7 | golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 8 | ) 9 | 10 | require github.com/mattn/go-runewidth v0.0.9 // indirect 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 2 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 3 | github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= 4 | github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= 5 | golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 h1:wM1k/lXfpc5HdkJJyW9GELpd8ERGdnh8sMGL6Gzq3Ho= 6 | golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | -------------------------------------------------------------------------------- /joystickunsupported.go: -------------------------------------------------------------------------------- 1 | // +build !linux,!windows,!darwin 2 | 3 | package joystick 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | // Open opens the Joystick for reading, with the supplied id 10 | // 11 | // Under linux the id is used to construct the joystick device name: 12 | // for example: id 0 will open device: "/dev/input/js0" 13 | // Under Windows the id is the actual numeric id of the joystick 14 | // 15 | // If successful, a Joystick interface is returned which can be used to 16 | // read the state of the joystick, else an error is returned 17 | func Open(id int) (Joystick, error) { 18 | return nil, errors.New("Joystick API unsupported on this platform") 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lee Witek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ioctl_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package joystick 4 | 5 | import ( 6 | "golang.org/x/sys/unix" 7 | "os" 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | const ( 13 | _IOC_NRBITS = 8 14 | _IOC_TYPEBITS = 8 15 | _IOC_SIZEBITS = 14 16 | _IOC_DIRBITS = 2 17 | 18 | _IOC_NRMASK = ((1 << _IOC_NRBITS) - 1) 19 | _IOC_TYPEMASK = ((1 << _IOC_TYPEBITS) - 1) 20 | _IOC_SIZEMASK = ((1 << _IOC_SIZEBITS) - 1) 21 | _IOC_DIRMASK = ((1 << _IOC_DIRBITS) - 1) 22 | 23 | _IOC_NRSHIFT = 0 24 | _IOC_TYPESHIFT = (_IOC_NRSHIFT + _IOC_NRBITS) 25 | _IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS) 26 | _IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS) 27 | 28 | _IOC_NONE = 0 29 | _IOC_WRITE = 1 30 | _IOC_READ = 2 31 | ) 32 | 33 | func _IOC(dir int, t int, nr int, size int) int { 34 | return (dir << _IOC_DIRSHIFT) | (t << _IOC_TYPESHIFT) | (nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT) 35 | } 36 | 37 | func _IOR(t int, nr int, size int) int { 38 | return _IOC(_IOC_READ, t, nr, size) 39 | } 40 | 41 | func _IOW(t int, nr int, size int) int { 42 | return _IOC(_IOC_WRITE, t, nr, size) 43 | } 44 | 45 | func ioctl(f *os.File, req int, ptr unsafe.Pointer) syscall.Errno { 46 | _, _, err := unix.Syscall(unix.SYS_IOCTL, uintptr(f.Fd()), uintptr(req), uintptr(ptr)) 47 | return err 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # joystick 2 | Go Joystick API 3 | 4 | [![GoDoc](https://godoc.org/github.com/0xcafed00d/joystick?status.svg)](https://godoc.org/github.com/0xcafed00d/joystick) [![Build Status](https://travis-ci.org/0xcafed00d/joystick.svg)](https://travis-ci.org/0xcafed00d/joystick) 5 | 6 | Package joystick implements a Polled API to read the state of an attached joystick. 7 | Windows, Linux & OSX are supported. 8 | Package requires no external dependencies to be installed. 9 | 10 | Mac OSX code developed by: https://github.com/ledyba 11 | 12 | ## Installation: 13 | ```bash 14 | $ go get github.com/0xcafed00d/joystick/... 15 | ``` 16 | ## Sample Program 17 | ```bash 18 | $ go install github.com/0xcafed00d/joystick/joysticktest 19 | $ joysticktest 0 20 | ``` 21 | Displays the state of the specified joystick 22 | ## Example: 23 | ```go 24 | import "github.com/0xcafed00d/joystick" 25 | ``` 26 | ```go 27 | js, err := joystick.Open(jsid) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | fmt.Printf("Joystick Name: %s", js.Name()) 33 | fmt.Printf(" Axis Count: %d", js.AxisCount()) 34 | fmt.Printf(" Button Count: %d", js.ButtonCount()) 35 | 36 | state, err := joystick.Read() 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | fmt.Printf("Axis Data: %v", state.AxisData) 42 | js.Close() 43 | ``` 44 | -------------------------------------------------------------------------------- /joystick_common.go: -------------------------------------------------------------------------------- 1 | // Package joystick implements a Polled API to read the state of an attached joystick. 2 | // currently Windows & Linux are supported. 3 | // Package is pure go and requires no external dependencies 4 | // 5 | // Installation: 6 | // go get github.com/simulatedsimian/joystick 7 | // 8 | // Example: 9 | // js, err := joystick.Open(jsid) 10 | // if err != nil { 11 | // panic(err) 12 | // } 13 | // 14 | // fmt.Printf("Joystick Name: %s", js.Name()) 15 | // fmt.Printf(" Axis Count: %d", js.AxisCount()) 16 | // fmt.Printf(" Button Count: %d", js.ButtonCount()) 17 | // 18 | // state, err := joystick.Read() 19 | // if err != nil { 20 | // panic(err) 21 | // } 22 | // 23 | // fmt.Printf("Axis Data: %v", state.AxisData) 24 | // js.Close() 25 | // 26 | package joystick 27 | 28 | // State holds the current state of the joystick 29 | type State struct { 30 | // Value of each axis as an integer in the range -32767 to 32768 31 | AxisData []int 32 | // The state of each button as a bit in a 32 bit integer. 1 = pressed, 0 = not pressed 33 | Buttons uint32 34 | } 35 | 36 | // Interface Joystick provides access to the Joystick opened with the Open() function 37 | type Joystick interface { 38 | // AxisCount returns the number of Axis supported by this Joystick 39 | AxisCount() int 40 | // ButtonCount returns the number of buttons supported by this Joystick 41 | ButtonCount() int 42 | // Name returns the string name of this Joystick 43 | Name() string 44 | // Read returns the current State of the joystick. 45 | // On an error condition (for example, joystick has been unplugged) error is not nil 46 | Read() (State, error) 47 | // Close releases this joystick resource 48 | Close() 49 | } 50 | -------------------------------------------------------------------------------- /joysticktest/joysticktest.go: -------------------------------------------------------------------------------- 1 | // Simple program that displays the state of the specified joystick 2 | // 3 | // go run joysticktest.go 2 4 | // displays state of joystick id 2 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "github.com/nsf/termbox-go" 10 | "github.com/0xcafed00d/joystick" 11 | "os" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | func printAt(x, y int, s string) { 17 | for _, r := range s { 18 | termbox.SetCell(x, y, r, termbox.ColorDefault, termbox.ColorDefault) 19 | x++ 20 | } 21 | } 22 | 23 | func readJoystick(js joystick.Joystick) { 24 | jinfo, err := js.Read() 25 | 26 | if err != nil { 27 | printAt(1, 5, "Error: "+err.Error()) 28 | return 29 | } 30 | 31 | printAt(1, 5, "Buttons:") 32 | for button := 0; button < js.ButtonCount(); button++ { 33 | if jinfo.Buttons&(1< 1 { 51 | i, err := strconv.Atoi(os.Args[1]) 52 | if err != nil { 53 | fmt.Println(err) 54 | return 55 | } 56 | jsid = i 57 | } 58 | 59 | js, jserr := joystick.Open(jsid) 60 | 61 | if jserr != nil { 62 | fmt.Println(jserr) 63 | return 64 | } 65 | 66 | err := termbox.Init() 67 | if err != nil { 68 | panic(err) 69 | } 70 | defer termbox.Close() 71 | 72 | eventQueue := make(chan termbox.Event) 73 | go func() { 74 | for { 75 | eventQueue <- termbox.PollEvent() 76 | } 77 | }() 78 | 79 | ticker := time.NewTicker(time.Millisecond * 40) 80 | 81 | for doQuit := false; !doQuit; { 82 | select { 83 | case ev := <-eventQueue: 84 | if ev.Type == termbox.EventKey { 85 | if ev.Ch == 'q' { 86 | doQuit = true 87 | } 88 | } 89 | if ev.Type == termbox.EventResize { 90 | termbox.Flush() 91 | } 92 | 93 | case <-ticker.C: 94 | printAt(1, 0, "-- Press 'q' to Exit --") 95 | printAt(1, 1, fmt.Sprintf("Joystick Name: %s", js.Name())) 96 | printAt(1, 2, fmt.Sprintf(" Axis Count: %d", js.AxisCount())) 97 | printAt(1, 3, fmt.Sprintf(" Button Count: %d", js.ButtonCount())) 98 | readJoystick(js) 99 | termbox.Flush() 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /joystick_darwin.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | // golang callback 6 | extern void addCallback(void* self, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject); 7 | extern void removeCallback(void* self, IOReturn res, void *sender); 8 | 9 | static CFDictionaryRef createMatcherElement(const UInt32 page, const UInt32 usage, int *okay) { 10 | CFDictionaryRef dict = 0; 11 | CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page); 12 | CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage); 13 | const void *keys[2] = { (void *) CFSTR(kIOHIDDeviceUsagePageKey), (void *) CFSTR(kIOHIDDeviceUsageKey) }; 14 | const void *vals[2] = { (void *) pageNumRef, (void *) usageNumRef }; 15 | 16 | if (pageNumRef && usageNumRef) { 17 | dict = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 18 | } 19 | 20 | if (pageNumRef) { 21 | CFRelease(pageNumRef); 22 | } 23 | if (usageNumRef) { 24 | CFRelease(usageNumRef); 25 | } 26 | 27 | if (!dict) { 28 | *okay = 0; 29 | } 30 | 31 | return dict; 32 | } 33 | 34 | static CFArrayRef createMatcher() { 35 | int okay = 1; 36 | const void *vals[] = { 37 | (void *) createMatcherElement(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, &okay), 38 | (void *) createMatcherElement(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, &okay), 39 | (void *) createMatcherElement(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController, &okay), 40 | }; 41 | CFArrayRef matcher = okay ? CFArrayCreate(kCFAllocatorDefault, vals, 3, &kCFTypeArrayCallBacks) : 0; 42 | for (size_t i = 0; i < 3; i++) { 43 | if (vals[i]) { 44 | CFRelease((CFTypeRef) vals[i]); 45 | } 46 | } 47 | return matcher; 48 | } 49 | 50 | #define kCFRunLoopMode CFSTR("go-joystick") 51 | 52 | IOHIDManagerRef openHIDManager() { 53 | CFRunLoopRef runloop = CFRunLoopGetCurrent(); 54 | IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); 55 | if(!manager) { 56 | return 0; 57 | } 58 | if(kIOReturnSuccess != IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone)) { 59 | CFRelease(manager); 60 | return 0; 61 | } 62 | CFArrayRef matcher = createMatcher(); 63 | IOHIDManagerSetDeviceMatchingMultiple(manager, matcher); 64 | IOHIDManagerRegisterDeviceMatchingCallback(manager, addCallback, 0); 65 | IOHIDManagerScheduleWithRunLoop(manager, runloop, kCFRunLoopMode); 66 | while (CFRunLoopRunInMode(kCFRunLoopMode, 0, TRUE) == kCFRunLoopRunHandledSource) { 67 | 68 | } 69 | CFRelease(matcher); 70 | return manager; 71 | } 72 | 73 | void closeHIDManager(IOHIDManagerRef manager) { 74 | CFRunLoopRef runloop = CFRunLoopGetCurrent(); 75 | CFRunLoopStop(runloop); 76 | IOHIDManagerUnscheduleFromRunLoop(manager, runloop, kCFRunLoopMode); 77 | CFRelease(manager); 78 | } 79 | 80 | // Helper function to convert CFStringRef to C string 81 | char* cfStringToCharPtr(CFStringRef str) { 82 | if (str == NULL) return NULL; 83 | CFIndex length = CFStringGetLength(str); 84 | CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; 85 | char* buffer = (char*)malloc(maxSize); 86 | if (CFStringGetCString(str, buffer, maxSize, kCFStringEncodingUTF8)) { 87 | return buffer; 88 | } 89 | free(buffer); 90 | return NULL; 91 | } 92 | 93 | // Helper function to extract integer value from CFTypeRef 94 | int getIntegerValue(CFTypeRef value) { 95 | if (value == NULL) return 0; 96 | if (CFGetTypeID(value) == CFNumberGetTypeID()) { 97 | int result = 0; 98 | CFNumberGetValue((CFNumberRef)value, kCFNumberIntType, &result); 99 | return result; 100 | } 101 | return 0; 102 | } -------------------------------------------------------------------------------- /joystick_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package joystick 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "fmt" 9 | "os" 10 | "strconv" 11 | "sync" 12 | "unsafe" 13 | ) 14 | 15 | const ( 16 | _JS_EVENT_BUTTON uint8 = 0x01 /* button pressed/released */ 17 | _JS_EVENT_AXIS uint8 = 0x02 /* joystick moved */ 18 | _JS_EVENT_INIT uint8 = 0x80 19 | ) 20 | 21 | var ( 22 | _JSIOCGAXES = _IOR('j', 0x11, 1) /* get number of axes */ 23 | _JSIOCGBUTTONS = _IOR('j', 0x12, 1) /* get number of buttons */ 24 | _JSIOCGNAME = func(len int) int { /* get identifier string */ 25 | return _IOR('j', 0x13, len) 26 | } 27 | ) 28 | 29 | type joystickImpl struct { 30 | file *os.File 31 | axisCount int 32 | buttonCount int 33 | name string 34 | state State 35 | mutex sync.RWMutex 36 | readerr error 37 | } 38 | 39 | // Open opens the Joystick for reading, with the supplied id 40 | // 41 | // Under linux the id is used to construct the joystick device name: 42 | // for example: id 0 will open device: "/dev/input/js0" 43 | // Under Windows the id is the actual numeric id of the joystick 44 | // 45 | // If successful, a Joystick interface is returned which can be used to 46 | // read the state of the joystick, else an error is returned 47 | func Open(id int) (Joystick, error) { 48 | f, err := os.OpenFile(fmt.Sprintf("/dev/input/js%d", id), os.O_RDONLY, 0666) 49 | 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | var axisCount uint8 = 0 55 | var buttCount uint8 = 0 56 | var buffer [256]byte 57 | 58 | ioerr := ioctl(f, _JSIOCGAXES, unsafe.Pointer(&axisCount)) 59 | if ioerr != 0 { 60 | panic(ioerr) 61 | } 62 | 63 | ioerr = ioctl(f, _JSIOCGBUTTONS, unsafe.Pointer(&buttCount)) 64 | if ioerr != 0 { 65 | panic(ioerr) 66 | } 67 | 68 | ioerr = ioctl(f, _JSIOCGNAME(len(buffer)-1), unsafe.Pointer(&buffer)) 69 | if ioerr != 0 { 70 | panic(ioerr) 71 | } 72 | 73 | js := &joystickImpl{} 74 | js.axisCount = int(axisCount) 75 | js.buttonCount = int(buttCount) 76 | js.file = f 77 | js.name = string(buffer[:]) 78 | js.state.AxisData = make([]int, axisCount, axisCount) 79 | 80 | go updateState(js) 81 | 82 | return js, nil 83 | } 84 | 85 | func updateState(js *joystickImpl) { 86 | var err error 87 | var ev event 88 | 89 | for err == nil { 90 | ev, err = js.getEvent() 91 | 92 | if ev.Type&_JS_EVENT_BUTTON != 0 { 93 | js.mutex.Lock() 94 | if ev.Value == 0 { 95 | js.state.Buttons &= ^(1 << uint(ev.Number)) 96 | } else { 97 | js.state.Buttons |= 1 << ev.Number 98 | } 99 | js.mutex.Unlock() 100 | } 101 | 102 | if ev.Type&_JS_EVENT_AXIS != 0 { 103 | js.mutex.Lock() 104 | js.state.AxisData[ev.Number] = int(ev.Value) 105 | js.mutex.Unlock() 106 | } 107 | } 108 | js.mutex.Lock() 109 | js.readerr = err 110 | js.mutex.Unlock() 111 | } 112 | 113 | func (js *joystickImpl) AxisCount() int { 114 | return js.axisCount 115 | } 116 | 117 | func (js *joystickImpl) ButtonCount() int { 118 | return js.buttonCount 119 | } 120 | 121 | func (js *joystickImpl) Name() string { 122 | return js.name 123 | } 124 | 125 | func (js *joystickImpl) Read() (State, error) { 126 | js.mutex.RLock() 127 | state, err := js.state, js.readerr 128 | js.mutex.RUnlock() 129 | return state, err 130 | } 131 | 132 | func (js *joystickImpl) Close() { 133 | js.file.Close() 134 | } 135 | 136 | type event struct { 137 | Time uint32 /* event timestamp in milliseconds */ 138 | Value int16 /* value */ 139 | Type uint8 /* event type */ 140 | Number uint8 /* axis/button number */ 141 | } 142 | 143 | func (j *event) String() string { 144 | var Type, Number string 145 | 146 | if j.Type&_JS_EVENT_INIT > 0 { 147 | Type = "Init " 148 | } 149 | if j.Type&_JS_EVENT_BUTTON > 0 { 150 | Type += "Button" 151 | Number = strconv.FormatUint(uint64(j.Number), 10) 152 | } 153 | if j.Type&_JS_EVENT_AXIS > 0 { 154 | Type = "Axis" 155 | Number = "Axis " + strconv.FormatUint(uint64(j.Number), 10) 156 | } 157 | 158 | return fmt.Sprintf("[Time: %v, Type: %v, Number: %v, Value: %v]", j.Time, Type, Number, j.Value) 159 | } 160 | 161 | func (j *joystickImpl) getEvent() (event, error) { 162 | var ev event 163 | 164 | if j.file == nil { 165 | panic("file is nil") 166 | } 167 | 168 | b := make([]byte, 8) 169 | _, err := j.file.Read(b) 170 | if err != nil { 171 | return event{}, err 172 | } 173 | 174 | data := bytes.NewReader(b) 175 | err = binary.Read(data, binary.LittleEndian, &ev) 176 | if err != nil { 177 | return event{}, err 178 | } 179 | return ev, nil 180 | } 181 | -------------------------------------------------------------------------------- /joystick_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package joystick 4 | 5 | import ( 6 | "fmt" 7 | "golang.org/x/sys/windows" 8 | "math" 9 | "unsafe" 10 | ) 11 | 12 | var PrintFunc func(x, y int, s string) 13 | 14 | const ( 15 | _MAXPNAMELEN = 32 16 | _MAX_JOYSTICKOEMVXDNAME = 260 17 | _MAX_AXIS = 6 18 | 19 | _JOY_RETURNX = 1 20 | _JOY_RETURNY = 2 21 | _JOY_RETURNZ = 4 22 | _JOY_RETURNR = 8 23 | _JOY_RETURNU = 16 24 | _JOY_RETURNV = 32 25 | _JOY_RETURNPOV = 64 26 | _JOY_RETURNBUTTONS = 128 27 | _JOY_RETURNRAWDATA = 256 28 | _JOY_RETURNPOVCTS = 512 29 | _JOY_RETURNCENTERED = 1024 30 | _JOY_USEDEADZONE = 2048 31 | _JOY_RETURNALL = (_JOY_RETURNX | _JOY_RETURNY | _JOY_RETURNZ | _JOY_RETURNR | _JOY_RETURNU | _JOY_RETURNV | _JOY_RETURNPOV | _JOY_RETURNBUTTONS) 32 | 33 | _JOYCAPS_HASZ = 0x1 34 | _JOYCAPS_HASR = 0x2 35 | _JOYCAPS_HASU = 0x4 36 | _JOYCAPS_HASV = 0x8 37 | _JOYCAPS_HASPOV = 0x10 38 | _JOYCAPS_POV4DIR = 0x20 39 | _JOYCAPS_POVCTS = 0x40 40 | ) 41 | 42 | type JOYCAPS struct { 43 | wMid uint16 44 | wPid uint16 45 | szPname [_MAXPNAMELEN]uint16 46 | wXmin uint32 47 | wXmax uint32 48 | wYmin uint32 49 | wYmax uint32 50 | wZmin uint32 51 | wZmax uint32 52 | wNumButtons uint32 53 | wPeriodMin uint32 54 | wPeriodMax uint32 55 | wRmin uint32 56 | wRmax uint32 57 | wUmin uint32 58 | wUmax uint32 59 | wVmin uint32 60 | wVmax uint32 61 | wCaps uint32 62 | wMaxAxes uint32 63 | wNumAxes uint32 64 | wMaxButtons uint32 65 | szRegKey [_MAXPNAMELEN]uint16 66 | szOEMVxD [_MAX_JOYSTICKOEMVXDNAME]uint16 67 | } 68 | 69 | type JOYINFOEX struct { 70 | dwSize uint32 71 | dwFlags uint32 72 | dwAxis [_MAX_AXIS]uint32 73 | dwButtons uint32 74 | dwButtonNumber uint32 75 | dwPOV uint32 76 | dwReserved1 uint32 77 | dwReserved2 uint32 78 | } 79 | 80 | var ( 81 | winmmdll = windows.MustLoadDLL("Winmm.dll") 82 | joyGetPosEx = winmmdll.MustFindProc("joyGetPosEx") 83 | joyGetDevCaps = winmmdll.MustFindProc("joyGetDevCapsW") 84 | ) 85 | 86 | type axisLimit struct { 87 | min, max uint32 88 | } 89 | 90 | type joystickImpl struct { 91 | id int 92 | axisCount int 93 | povAxisCount int 94 | buttonCount int 95 | name string 96 | state State 97 | axisLimits []axisLimit 98 | } 99 | 100 | func mapValue(val, srcMin, srcMax, dstMin, dstMax int64) int64 { 101 | return (val-srcMin)*(dstMax-dstMin)/(srcMax-srcMin) + dstMin 102 | } 103 | 104 | // Open opens the Joystick for reading, with the supplied id 105 | // 106 | // Under linux the id is used to construct the joystick device name: 107 | // for example: id 0 will open device: "/dev/input/js0" 108 | // 109 | // Under Windows the id is the actual numeric id of the joystick 110 | // 111 | // If successful, a Joystick interface is returned which can be used to 112 | // read the state of the joystick, else an error is returned 113 | func Open(id int) (Joystick, error) { 114 | 115 | js := &joystickImpl{} 116 | js.id = id 117 | 118 | err := js.getJoyCaps() 119 | if err == nil { 120 | return js, nil 121 | } 122 | return nil, err 123 | } 124 | 125 | func (js *joystickImpl) getJoyCaps() error { 126 | var caps JOYCAPS 127 | ret, _, _ := joyGetDevCaps.Call(uintptr(js.id), uintptr(unsafe.Pointer(&caps)), unsafe.Sizeof(caps)) 128 | 129 | if ret != 0 { 130 | return fmt.Errorf("Failed to read Joystick %d", js.id) 131 | } else { 132 | js.axisCount = int(caps.wNumAxes) 133 | js.buttonCount = int(caps.wNumButtons) 134 | js.name = windows.UTF16ToString(caps.szPname[:]) 135 | 136 | if caps.wCaps&_JOYCAPS_HASPOV != 0 { 137 | js.povAxisCount = 2 138 | } 139 | 140 | js.state.AxisData = make([]int, js.axisCount+js.povAxisCount, js.axisCount+js.povAxisCount) 141 | 142 | js.axisLimits = []axisLimit{ 143 | {caps.wXmin, caps.wXmax}, 144 | {caps.wYmin, caps.wYmax}, 145 | {caps.wZmin, caps.wZmax}, 146 | {caps.wRmin, caps.wRmax}, 147 | {caps.wUmin, caps.wUmax}, 148 | {caps.wVmin, caps.wVmax}, 149 | } 150 | 151 | return nil 152 | } 153 | } 154 | 155 | func axisFromPov(povVal float64) int { 156 | switch { 157 | case povVal < -0.5: 158 | return -32767 159 | case povVal > 0.5: 160 | return 32768 161 | default: 162 | return 0 163 | } 164 | } 165 | 166 | func (js *joystickImpl) getJoyPosEx() error { 167 | var info JOYINFOEX 168 | info.dwSize = uint32(unsafe.Sizeof(info)) 169 | info.dwFlags = _JOY_RETURNALL 170 | ret, _, _ := joyGetPosEx.Call(uintptr(js.id), uintptr(unsafe.Pointer(&info))) 171 | 172 | if ret != 0 { 173 | return fmt.Errorf("Failed to read Joystick %d", js.id) 174 | } else { 175 | js.state.Buttons = info.dwButtons 176 | 177 | for i := 0; i < js.axisCount; i++ { 178 | js.state.AxisData[i] = int(mapValue(int64(info.dwAxis[i]), 179 | int64(js.axisLimits[i].min), int64(js.axisLimits[i].max), -32767, 32768)) 180 | } 181 | 182 | if js.povAxisCount > 0 { 183 | angleDeg := float64(info.dwPOV) / 100.0 184 | if angleDeg > 359.0 { 185 | js.state.AxisData[js.axisCount] = 0 186 | js.state.AxisData[js.axisCount+1] = 0 187 | return nil 188 | } 189 | 190 | angleRad := angleDeg * math.Pi / 180.0 191 | sin, cos := math.Sincos(angleRad) 192 | 193 | js.state.AxisData[js.axisCount] = axisFromPov(sin) 194 | js.state.AxisData[js.axisCount+1] = axisFromPov(-cos) 195 | } 196 | return nil 197 | } 198 | } 199 | 200 | func (js *joystickImpl) AxisCount() int { 201 | return js.axisCount + js.povAxisCount 202 | } 203 | 204 | func (js *joystickImpl) ButtonCount() int { 205 | return js.buttonCount 206 | } 207 | 208 | func (js *joystickImpl) Name() string { 209 | return js.name 210 | } 211 | 212 | func (js *joystickImpl) Read() (State, error) { 213 | err := js.getJoyPosEx() 214 | return js.state, err 215 | } 216 | 217 | func (js *joystickImpl) Close() { 218 | // no impl under windows 219 | } 220 | -------------------------------------------------------------------------------- /joystick_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package joystick 5 | 6 | //#cgo LDFLAGS: -framework IOKit -framework CoreFoundation 7 | /* 8 | #include 9 | #include 10 | extern void removeCallback(void* ctx, IOReturn res, void *sender); 11 | extern IOHIDManagerRef openHIDManager(); 12 | extern void closeHIDManager(IOHIDManagerRef manager); 13 | extern void addHIDElement(void *value, void *parameter); 14 | extern char* cfStringToCharPtr(CFStringRef str); 15 | extern int getIntegerValue(CFTypeRef value); 16 | #define kManufacturerKey CFSTR(kIOHIDManufacturerKey) 17 | #define kProductKey CFSTR(kIOHIDProductKey) 18 | #define kSerialNumberKey CFSTR(kIOHIDSerialNumberKey) 19 | #define kLocationIDKey CFSTR(kIOHIDLocationIDKey) 20 | #define kCFRunLoopMode CFSTR("go-joystick") 21 | */ 22 | import "C" 23 | 24 | import ( 25 | "fmt" 26 | "strings" 27 | "sync" 28 | "unsafe" 29 | ) 30 | 31 | // cfStringToGoString converts a CFStringRef to a Go string 32 | func cfStringToGoString(cfStr C.CFStringRef) string { 33 | if cfStr == 0 { 34 | return "" 35 | } 36 | cStr := C.cfStringToCharPtr(cfStr) 37 | if cStr == nil { 38 | return "" 39 | } 40 | s := C.GoString(cStr) 41 | C.free(unsafe.Pointer(cStr)) 42 | return s 43 | } 44 | 45 | type joystickManager struct { 46 | ref C.IOHIDManagerRef 47 | devices map[int]*joystickImpl 48 | deviceCnt int 49 | deviceUsed int 50 | } 51 | 52 | var mgr *joystickManager 53 | var mgrMutex sync.Mutex 54 | 55 | func openManager() *joystickManager { 56 | if mgr == nil { 57 | mgr = &joystickManager{ 58 | ref: C.IOHIDManagerRef(0), 59 | devices: make(map[int]*joystickImpl), 60 | deviceCnt: 0, 61 | deviceUsed: 0, 62 | } 63 | mgr.ref = C.openHIDManager() 64 | if mgr.ref == (C.IOHIDManagerRef)(0) { 65 | return nil 66 | } 67 | } 68 | return mgr 69 | } 70 | 71 | //export addCallback 72 | func addCallback(ctx unsafe.Pointer, res C.IOReturn, sender unsafe.Pointer, device C.IOHIDDeviceRef) { 73 | if res != C.kIOReturnSuccess { 74 | return 75 | } 76 | if mgr.searchFromDeviceRef(device) != nil { 77 | return 78 | } 79 | id := mgr.deviceCnt 80 | mgr.deviceCnt++ 81 | impl := &joystickImpl{ 82 | id: id, 83 | ref: device, 84 | } 85 | 86 | // Extract device name from manufacturer and product properties 87 | var manufacturerStr, productStr, serialStr string 88 | var locationID int 89 | 90 | if manufacturer := C.IOHIDDeviceGetProperty(device, C.kManufacturerKey); manufacturer != 0 { 91 | manufacturerStr = cfStringToGoString(C.CFStringRef(manufacturer)) 92 | } 93 | if product := C.IOHIDDeviceGetProperty(device, C.kProductKey); product != 0 { 94 | productStr = cfStringToGoString(C.CFStringRef(product)) 95 | } 96 | if serial := C.IOHIDDeviceGetProperty(device, C.kSerialNumberKey); serial != 0 { 97 | serialStr = cfStringToGoString(C.CFStringRef(serial)) 98 | } 99 | if location := C.IOHIDDeviceGetProperty(device, C.kLocationIDKey); location != 0 { 100 | locationID = int(C.getIntegerValue(location)) 101 | } 102 | 103 | // Build the device name 104 | var nameParts []string 105 | if manufacturerStr != "" { 106 | nameParts = append(nameParts, manufacturerStr) 107 | } 108 | if productStr != "" { 109 | nameParts = append(nameParts, productStr) 110 | } 111 | if serialStr != "" { 112 | nameParts = append(nameParts, serialStr) 113 | } 114 | if locationID != 0 { 115 | nameParts = append(nameParts, fmt.Sprintf("(0x%x)", locationID)) 116 | } 117 | 118 | name := strings.Join(nameParts, " ") 119 | if name == "" { 120 | name = "Unknown Joystick" 121 | } 122 | impl.name = name 123 | 124 | mgr.devices[id] = impl 125 | C.IOHIDDeviceRegisterRemovalCallback(device, C.IOHIDCallback(C.removeCallback), unsafe.Pointer(uintptr(id))) 126 | C.IOHIDDeviceScheduleWithRunLoop(device, C.CFRunLoopGetCurrent(), C.kCFRunLoopMode) 127 | elems := C.IOHIDDeviceCopyMatchingElements(device, C.CFDictionaryRef(0), C.kIOHIDOptionsTypeNone) 128 | impl.addElements(elems) 129 | } 130 | 131 | func (js *joystickImpl) addElements(elems C.CFArrayRef) { 132 | max := int(C.CFArrayGetCount(elems)) 133 | for i := 0; i < max; i++ { 134 | elem := C.IOHIDElementRef(C.CFArrayGetValueAtIndex(elems, C.long(i))) 135 | //typeID := C.CFGetTypeID(C.CFTypeRef(elem)) 136 | usagePage := C.IOHIDElementGetUsagePage(elem) 137 | usage := C.IOHIDElementGetUsage(elem) 138 | switch C.IOHIDElementGetType(elem) { 139 | 140 | case C.kIOHIDElementTypeInput_Misc: 141 | fallthrough 142 | case C.kIOHIDElementTypeInput_Button: 143 | fallthrough 144 | case C.kIOHIDElementTypeInput_Axis: 145 | switch usagePage { 146 | case C.kHIDPage_GenericDesktop, C.kHIDPage_Simulation: 147 | switch usage { 148 | case C.kHIDUsage_GD_X: 149 | fallthrough 150 | case C.kHIDUsage_GD_Y: 151 | fallthrough 152 | case C.kHIDUsage_GD_Z: 153 | fallthrough 154 | case C.kHIDUsage_GD_Rx: 155 | fallthrough 156 | case C.kHIDUsage_GD_Ry: 157 | fallthrough 158 | case C.kHIDUsage_GD_Rz: 159 | fallthrough 160 | case C.kHIDUsage_GD_Slider: 161 | fallthrough 162 | case C.kHIDUsage_GD_Dial: 163 | fallthrough 164 | case C.kHIDUsage_GD_Wheel, C.kHIDUsage_Sim_Brake, C.kHIDUsage_Sim_Accelerator: 165 | if js.contains(elem) { 166 | continue 167 | } 168 | js.axes = append(js.axes, &joystickAxis{ 169 | ref: elem, 170 | min: int(C.IOHIDElementGetLogicalMin(elem)), 171 | max: int(C.IOHIDElementGetLogicalMax(elem)), 172 | center: -1, 173 | }) 174 | js.state.AxisData = append(js.state.AxisData, 0) 175 | case C.kHIDUsage_GD_Hatswitch: 176 | if js.contains(elem) { 177 | continue 178 | } 179 | js.hats = append(js.hats, &joystickHat{ 180 | ref: elem, 181 | }) 182 | js.state.AxisData = append(js.state.AxisData, 0, 0) 183 | } 184 | case C.kHIDPage_Button, C.kHIDPage_Consumer: 185 | if js.contains(elem) { 186 | continue 187 | } 188 | js.buttons = append(js.buttons, &joystickButton{ 189 | ref: elem, 190 | }) 191 | } 192 | case C.kIOHIDElementTypeCollection: 193 | if children := C.IOHIDElementGetChildren(elem); children != C.CFArrayRef(0) { 194 | js.addElements(children) 195 | } 196 | default: 197 | continue /* Nothing to do */ 198 | } 199 | } 200 | } 201 | 202 | //export removeCallback 203 | func removeCallback(self unsafe.Pointer, res C.IOReturn, sender unsafe.Pointer) { 204 | if res != C.kIOReturnSuccess { 205 | return 206 | } 207 | id := int(uintptr(self)) 208 | if mgr != nil { 209 | if impl, exists := mgr.devices[id]; exists { 210 | impl.ref = C.IOHIDDeviceRef(0) 211 | impl.removed = true 212 | } 213 | } 214 | } 215 | 216 | func (mgr *joystickManager) searchFromDeviceRef(ref C.IOHIDDeviceRef) *joystickImpl { 217 | for _, impl := range mgr.devices { 218 | if impl.ref == ref { 219 | return impl 220 | } 221 | } 222 | return nil 223 | } 224 | 225 | func (mgr *joystickManager) Close() { 226 | C.closeHIDManager(mgr.ref) 227 | } 228 | 229 | // -- elem 230 | 231 | type joystickAxis struct { 232 | ref C.IOHIDElementRef 233 | min int 234 | max int 235 | center int 236 | } 237 | 238 | type joystickButton struct { 239 | ref C.IOHIDElementRef 240 | } 241 | 242 | type joystickHat struct { 243 | ref C.IOHIDElementRef 244 | } 245 | 246 | // -- impl 247 | 248 | type joystickImpl struct { 249 | id int 250 | ref C.IOHIDDeviceRef 251 | removed bool 252 | name string 253 | axes []*joystickAxis 254 | hats []*joystickHat 255 | buttons []*joystickButton 256 | state State 257 | } 258 | 259 | func Open(id int) (Joystick, error) { 260 | mgrMutex.Lock() 261 | defer mgrMutex.Unlock() 262 | mgr := openManager() 263 | if mgr == nil { 264 | return nil, fmt.Errorf("Could not open joystick manager") 265 | } 266 | js := mgr.devices[id] 267 | if js == nil { 268 | return nil, fmt.Errorf("Device not found") 269 | } 270 | mgr.deviceUsed++ 271 | return js, nil 272 | } 273 | 274 | func (js *joystickImpl) AxisCount() int { 275 | return len(js.axes) + len(js.hats)*2 276 | } 277 | 278 | func (js *joystickImpl) ButtonCount() int { 279 | return len(js.buttons) 280 | } 281 | 282 | func (js *joystickImpl) Name() string { 283 | return js.name 284 | } 285 | 286 | func (js *joystickImpl) Read() (State, error) { 287 | min := -32767 288 | max := 32768 289 | for idx, axe := range js.axes { 290 | var valueRef C.IOHIDValueRef 291 | if C.IOHIDDeviceGetValue(js.ref, axe.ref, &valueRef) != C.kIOReturnSuccess { 292 | continue 293 | } 294 | value := int(C.IOHIDValueGetIntegerValue(valueRef)) 295 | if axe.center < 0 { 296 | axe.center = value 297 | js.state.AxisData[idx] = 0 298 | } else if axe.center != int(0.5+float64(axe.max-axe.min)/2.0) { 299 | js.state.AxisData[idx] = int(float64(value-axe.min)*float64(max-min)/float64(axe.max-axe.min)) + min 300 | } else { 301 | if value < axe.center { 302 | js.state.AxisData[idx] = int(float64(value-axe.min)*float64(0-min)/float64(axe.center-axe.min)) + min 303 | } else { 304 | js.state.AxisData[idx] = int(float64(value-axe.center)*float64(max-0)/float64(axe.max-axe.center)) + 0 305 | } 306 | } 307 | } 308 | for idx, hat := range js.hats { 309 | stateIdxX := len(js.axes) + idx*2 310 | stateIdxY := len(js.axes) + idx*2 + 1 311 | 312 | var valueRef C.IOHIDValueRef 313 | if C.IOHIDDeviceGetValue(js.ref, hat.ref, &valueRef) != C.kIOReturnSuccess { 314 | continue 315 | } 316 | 317 | value := int(int(C.IOHIDValueGetIntegerValue(valueRef))) 318 | 319 | if value == 0 { 320 | js.state.AxisData[stateIdxX] = 0 321 | js.state.AxisData[stateIdxY] = 0 322 | continue 323 | } 324 | 325 | // 1 326 | // 8 2 327 | // 7 0 3 328 | // 6 4 329 | // 5 330 | if value == 8 || value == 1 || value == 2 { 331 | js.state.AxisData[stateIdxX] = max 332 | } else if value == 4 || value == 5 || value == 6 { 333 | js.state.AxisData[stateIdxX] = min 334 | } else { 335 | js.state.AxisData[stateIdxX] = 0 336 | } 337 | 338 | if value == 2 || value == 3 || value == 4 { 339 | js.state.AxisData[stateIdxY] = max 340 | } else if value == 6 || value == 7 || value == 8 { 341 | js.state.AxisData[stateIdxY] = min 342 | } else { 343 | js.state.AxisData[stateIdxY] = 0 344 | } 345 | } 346 | buttons := uint32(0) 347 | for idx, btn := range js.buttons { 348 | var valueRef C.IOHIDValueRef 349 | if C.IOHIDDeviceGetValue(js.ref, btn.ref, &valueRef) != C.kIOReturnSuccess { 350 | continue 351 | } 352 | if int(C.IOHIDValueGetIntegerValue(valueRef)) > 0 { 353 | buttons |= uint32(1) << uint(idx) 354 | } 355 | } 356 | js.state.Buttons = buttons 357 | return js.state, nil 358 | } 359 | 360 | func (js *joystickImpl) Close() { 361 | mgrMutex.Lock() 362 | defer mgrMutex.Unlock() 363 | mgr := openManager() 364 | if mgr == nil { 365 | return 366 | } 367 | mgr.deviceUsed-- 368 | if mgr.deviceUsed == 0 { 369 | mgr.Close() 370 | mgr = nil 371 | } 372 | } 373 | 374 | func (js *joystickImpl) contains(ref C.IOHIDElementRef) bool { 375 | for _, elem := range js.axes { 376 | if elem.ref == ref { 377 | return true 378 | } 379 | } 380 | for _, elem := range js.hats { 381 | if elem.ref == ref { 382 | return true 383 | } 384 | } 385 | for _, elem := range js.buttons { 386 | if elem.ref == ref { 387 | return true 388 | } 389 | } 390 | return false 391 | } 392 | --------------------------------------------------------------------------------