├── examples ├── VR │ ├── gamepad[SYSV64].elf │ └── gamepad.go ├── play │ ├── README.md │ └── play.go ├── basic │ ├── README.md │ └── basic.go ├── play.go └── hybrid │ └── regions.go ├── doc.go ├── LICENSE ├── modifiers.go ├── README.md ├── joysticks_linux.go ├── joysticks_test.go └── joysticks.go /examples/VR/gamepad[SYSV64].elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splace/joysticks/HEAD/examples/VR/gamepad[SYSV64].elf -------------------------------------------------------------------------------- /examples/play/README.md: -------------------------------------------------------------------------------- 1 | plays notes, using, '[aplay](https://en.wikipedia.org/wiki/Aplay)' command, when you press buttons on the controller 2 | -------------------------------------------------------------------------------- /examples/play/play.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import . "github.com/splace/joysticks" 4 | 5 | func main() { 6 | evts := Capture( 7 | Channel{1, HID.OnClose}, // event[0] chan set to receive button #1 closes events 8 | ) 9 | <-evts[0] 10 | } 11 | 12 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | only needs 2 buttons and one hat. 2 | 3 | prints button press and hat move event. 4 | 5 | also prints current hat#1 position when pressing button #2. 6 | 7 | output: 8 | 9 | HID#1:- Buttons:12, Hats:3 10 | Timeout in 10 secs. 11 | button #1 pressed 12 | button #2 pressed 13 | current hat #1 position: [0 0] 14 | hat #1 moved too: -0.07217628 0 15 | hat #1 moved too: -0.1340373 0 16 | hat #1 moved too: -0.16498306 0 17 | hat #1 moved too: -0.1752678 0 18 | hat #1 moved too: -0.18558306 0 19 | hat #1 moved too: -0.19589831 0 20 | hat #1 moved too: -0.18558306 0 21 | hat #1 moved too: -0.19589831 0 22 | hat #1 moved too: -0.16498306 0 23 | hat #1 moved too: -0.15466781 0 24 | hat #1 moved too: -0.1340373 0 25 | hat #1 moved too: -0.12372204 0 26 | hat #1 moved too: -0.113406785 0 27 | hat #1 moved too: -0.12372204 0 28 | hat #1 moved too: -0.113406785 0 29 | hat #1 moved too: -0.12372204 0 30 | hat #1 moved too: -0.113406785 0 31 | hat #1 moved too: -0.12372204 0 32 | hat #1 moved too: -0.113406785 0 33 | button #2 pressed 34 | current hat #1 position: [-0.113406785 0] 35 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package joysticks, provides simplified event routing, through channels, from the Linux joystick driver File-like interface. 3 | 4 | events can be listened for from any thread, dynamically re-mapped and simulated. 5 | 6 | Highlevel Usage 7 | 8 | 'Capture', one call to setup and start basic 'Event' channeling on the first available device. 9 | 10 | Midlevel 11 | 12 | 'Connect(index)' to a HID. 13 | 14 | Use methods to add (or alter) 'Event' channels. 15 | 16 | Start running by calling 'ParcelOutEvents()'. 17 | 18 | (unlike highlevel, event index to channel mappings can be changed dynamically.) 19 | 20 | Lowlevel 21 | 22 | 'Connect' to a HID by index number. 23 | 24 | handle all events directly appearing on the returned HID's OSEvents channel. 25 | 26 | Interface 27 | 28 | 'Event' interface, provides a time.Duration through a call to the Moment() method, returning whatever the underlying Linux driver provides as the events timestamp, as a time.Duration. 29 | 30 | returned 'Event's need asserting to their underlying type ( '***Event' ) to access data other than moment. 31 | 32 | */ 33 | package joysticks 34 | -------------------------------------------------------------------------------- /examples/basic/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import . "github.com/splace/joysticks" 4 | import "fmt" 5 | import "time" 6 | 7 | func main() { 8 | device := Connect(1) 9 | 10 | if device == nil { 11 | panic("no HIDs") 12 | } 13 | fmt.Printf("HID#1:- Buttons:%d, Hats:%d\n", len(device.Buttons), len(device.HatAxes)/2) 14 | 15 | // make channels for specific events 16 | b1press := device.OnClose(1) 17 | b2press := device.OnClose(2) 18 | h1move := device.OnMove(1) 19 | 20 | // feed OS events onto the event channels. 21 | go device.ParcelOutEvents() 22 | 23 | // handle event channels 24 | go func(){ 25 | for{ 26 | select { 27 | case <-b1press: 28 | fmt.Println("button #1 pressed") 29 | case <-b2press: 30 | fmt.Println("button #2 pressed") 31 | coords := make([]float32, 2) 32 | device.HatCoords(1,coords) 33 | fmt.Println("current hat #1 position:",coords) 34 | case h := <-h1move: 35 | hpos:=h.(CoordsEvent) 36 | fmt.Println("hat #1 moved too:", hpos.X,hpos.Y) 37 | } 38 | } 39 | }() 40 | 41 | fmt.Println("Timeout in 10 secs.") 42 | <-time.After(time.Second*10) 43 | fmt.Println("Shutting down due to timeout.") 44 | } 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /examples/play.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os/exec" 6 | "time" 7 | "math" 8 | "os" 9 | "os/signal" 10 | "log" 11 | ) 12 | 13 | import . "github.com/splace/joysticks" 14 | 15 | import . "github.com/splace/sounds" 16 | 17 | 18 | func main() { 19 | stopChan := make(chan os.Signal) 20 | signal.Notify(stopChan, os.Interrupt) 21 | 22 | events := Capture( 23 | Channel{10, HID.OnLong}, // event[0] button #10 long pressed 24 | Channel{1, HID.OnClose}, // event[1] button #1 closes 25 | Channel{7, HID.OnOpen}, // event[2] button #7 opens 26 | Channel{8, HID.OnOpen}, // event[3] button #8 opens 27 | Channel{2, HID.OnRotate}, // event[4] hat #1 rotates 28 | Channel{1, HID.OnEdge}, // event[5] hat #2 rotates 29 | ) 30 | var volume float32 = .5 31 | var octave =5 32 | var note int =1 33 | for { 34 | select { 35 | case <-stopChan: // wait for SIGINT 36 | log.Println("Interrupted") 37 | return 38 | case <-events[0]: 39 | return 40 | case <-events[1]: 41 | play(NewSound(NewTone(Period(octave,note), float64(volume)), time.Second/3)) 42 | case <-events[2]: 43 | octave++ 44 | case <-events[3]: 45 | octave-- 46 | case h := <-events[4]: 47 | volume = h.(AngleEvent).Angle/6.28 + .5 48 | case h := <-events[5]: 49 | //f = time.Duration(100*math.Pow(2, float64(h.(HatAngleEvent).Angle)/6.28)) * time.Second / 44000 50 | note = int(h.(AngleEvent).Angle*6 / math.Pi)+6 51 | play(NewSound(NewTone(Period(octave,note), float64(volume)), time.Second/3)) 52 | } 53 | } 54 | } 55 | 56 | func play(s Sound) { 57 | cmd := exec.Command("aplay") 58 | out, in := io.Pipe() 59 | go func() { 60 | Encode(in, 2, 44100, s) 61 | in.Close() 62 | }() 63 | cmd.Stdin = out 64 | err := cmd.Run() 65 | if err != nil { 66 | panic(err) 67 | } 68 | log.Println("Playing:",s) 69 | } 70 | 71 | -------------------------------------------------------------------------------- /examples/hybrid/regions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/splace/joysticks" 4 | import "log" 5 | import "time" 6 | 7 | type Region struct{ 8 | minX,minY,maxX,maxY float32 9 | } 10 | 11 | type RegionEvent struct { 12 | time time.Duration 13 | Index int 14 | } 15 | 16 | func (b RegionEvent) Moment() time.Duration { 17 | return b.time 18 | } 19 | 20 | 21 | func OnRegionChanged(trigger,position chan joysticks.Event, regions []Region) (chan RegionEvent,chan RegionEvent){ 22 | in := make(chan RegionEvent) 23 | out := make(chan RegionEvent) 24 | states:=make([]bool,len(regions)) 25 | var pevt joysticks.CoordsEvent 26 | go func(){ 27 | for e:=range(position){ 28 | switch v := e.(type) { 29 | case joysticks.CoordsEvent: 30 | pevt=v 31 | } 32 | } 33 | }() 34 | go func(){ 35 | for e:=range(trigger){ 36 | for ri,r:=range(regions){ 37 | if pevt.X>=r.minX && pevt.X<=r.maxX && pevt.Y>=r.minY && pevt.Y<=r.maxY{ 38 | if !states[ri]{ 39 | in <-RegionEvent{e.Moment(),ri} 40 | states[ri]=true 41 | } 42 | }else{ 43 | if states[ri]{ 44 | out <-RegionEvent{e.Moment(),ri} 45 | states[ri]=false 46 | } 47 | } 48 | } 49 | } 50 | close(in) 51 | close(out) 52 | }() 53 | return in,out 54 | } 55 | 56 | 57 | func main() { 58 | device := joysticks.Connect(1) 59 | if device == nil { 60 | log.Println("no HIDs") 61 | return 62 | } 63 | log.Printf("HID#1:- Buttons:%d, Hats:%d\n", len(device.Buttons), len(device.HatAxes)/2) 64 | 65 | // make channels for specific events 66 | regionEntering,regionExiting:=OnRegionChanged(device.OnHat(1),device.OnMove(1),[]Region{Region{.5,.5,1,1},Region{-1,-1,-.5,-.5}}) 67 | 68 | // feed OS events onto the event channels. 69 | go device.ParcelOutEvents() 70 | 71 | // handle event channels 72 | go func(){ 73 | for{ 74 | select { 75 | case r:= <-regionEntering: 76 | log.Println("enter region:",r.Index) 77 | case r:= <-regionExiting: 78 | log.Println("leave region:",r.Index) 79 | } 80 | } 81 | }() 82 | 83 | log.Println("Timeout in 10 secs.") 84 | <-time.After(time.Second*10) 85 | log.Println("Shutting down due to timeout.") 86 | } 87 | 88 | 89 | -------------------------------------------------------------------------------- /examples/VR/gamepad.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/nsf/termbox-go" 6 | "github.com/simulatedsimian/joystick" 7 | "os" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func printAt(x, y int, s string) { 13 | for _, r := range s { 14 | termbox.SetCell(x, y, r, termbox.ColorDefault, termbox.ColorDefault) 15 | x++ 16 | } 17 | } 18 | 19 | func readJoystick(js joystick.Joystick) { 20 | jinfo, err := js.Read() 21 | 22 | if err != nil { 23 | printAt(1, 5, "Error: "+err.Error()) 24 | return 25 | } 26 | 27 | printAt(1, 5, "Buttons:") 28 | for button := 0; button < js.ButtonCount(); button++ { 29 | if jinfo.Buttons&(1< 1 { 47 | i, err := strconv.Atoi(os.Args[1]) 48 | if err != nil { 49 | fmt.Println(err) 50 | return 51 | } 52 | jsid = i 53 | } 54 | 55 | js, jserr := joystick.Open(jsid) 56 | 57 | if jserr != nil { 58 | fmt.Println(jserr) 59 | return 60 | } 61 | 62 | err := termbox.Init() 63 | if err != nil { 64 | panic(err) 65 | } 66 | defer termbox.Close() 67 | 68 | eventQueue := make(chan termbox.Event) 69 | go func() { 70 | for { 71 | eventQueue <- termbox.PollEvent() 72 | } 73 | }() 74 | 75 | ticker := time.NewTicker(time.Millisecond * 40) 76 | 77 | for doQuit := false; !doQuit; { 78 | select { 79 | case ev := <-eventQueue: 80 | if ev.Type == termbox.EventKey { 81 | if ev.Ch == 'q' { 82 | doQuit = true 83 | } 84 | } 85 | if ev.Type == termbox.EventResize { 86 | termbox.Flush() 87 | } 88 | 89 | case <-ticker.C: 90 | printAt(1, 0, "-- Press 'q' to Exit --") 91 | printAt(1, 1, fmt.Sprintf("Joystick Name: %s", js.Name())) 92 | printAt(1, 2, fmt.Sprintf(" Axis Count: %d", js.AxisCount())) 93 | printAt(1, 3, fmt.Sprintf(" Button Count: %d", js.ButtonCount())) 94 | readJoystick(js) 95 | termbox.Flush() 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /modifiers.go: -------------------------------------------------------------------------------- 1 | package joysticks 2 | 3 | import ( 4 | "time" 5 | //"fmt" 6 | ) 7 | 8 | // TODO drag event 9 | // TODO move plus edge continue events (self generating) 10 | // TODO smoother from PID 11 | // TODO two pans to a coord 12 | // TODO coords to two pans 13 | // TODO 1-d integrator 14 | 15 | var DefaultRepeat = time.Second /4 16 | var VelocityRepeat = time.Second / 10 17 | 18 | 19 | // duplicate event onto two chan's 20 | func Duplicator(c chan Event)(chan Event,chan Event){ 21 | c1 := make(chan Event) 22 | c2 := make(chan Event) 23 | go func(){ 24 | for e:=range c{ 25 | c1 <- e 26 | c2 <- e 27 | } 28 | close(c1) 29 | close(c2) 30 | }() 31 | return c1,c2 32 | } 33 | 34 | 35 | // creates a chan on which you get CoordsEvent's that are the time integration of the CoordsEvent's on the parameter chan. 36 | func PositionFromVelocity(c chan Event) chan Event{ 37 | extra := make(chan Event) 38 | var x,y,vx,vy float32 39 | var startTime time.Time 40 | var startMoment time.Duration 41 | var m time.Duration 42 | var lt time.Time 43 | ticker:=time.NewTicker(VelocityRepeat) 44 | // receiving chan processor 45 | go func(){ 46 | e:= <-c 47 | startTime=time.Now() 48 | startMoment=e.Moment() 49 | lm:=startMoment 50 | for e:=range c{ 51 | if ce,ok:=e.(CoordsEvent);ok{ 52 | lt=time.Now() 53 | m=e.Moment() 54 | dt:=float32((m-lm).Seconds()) 55 | x+=vx*dt 56 | y+=vy*dt 57 | vx,vy=ce.X,ce.Y 58 | lm= m 59 | } 60 | } 61 | ticker.Stop() 62 | }() 63 | // output chan processor 64 | go func(){ 65 | var lx,ly,nx,ny,dt float32 66 | for t:=range ticker.C{ 67 | dt=float32(t.Sub(lt).Seconds()) 68 | nx,ny=x+dt*vx,y+dt*vy 69 | if nx!=lx || ny!=ly { 70 | extra <-CoordsEvent{when{startMoment+t.Sub(startTime)},nx,ny} 71 | lx,ly=nx,ny 72 | } 73 | } 74 | }() 75 | 76 | return extra 77 | } 78 | 79 | 80 | // creates a channel that, after receiving any event on the first parameter chan, and until any event on second chan parameter, regularly receives 'when' events. 81 | // the repeat interval is DefaultRepeat, and is stored, so retriggering is not effected by changing DefaultRepeat. 82 | func Repeater(c1,c2 chan Event)(chan Event){ 83 | c := make(chan Event) 84 | go func(){ 85 | interval:=DefaultRepeat 86 | var ticker *time.Ticker 87 | for { 88 | e:= <-c1 89 | go func(interval time.Duration, startTime time.Time){ 90 | ticker=time.NewTicker(interval) 91 | for t:=range ticker.C{ 92 | c <- when{e.Moment()+t.Sub(startTime)} 93 | } 94 | }(interval, time.Now()) 95 | <-c2 96 | ticker.Stop() 97 | } 98 | }() 99 | return c 100 | } 101 | 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # joysticks 2 | 3 | Go language joystick/controller/gamepad input. 4 | 5 | uses Linux kernel 'input' interface, available on a wide range of linux devices, to receive events directly, no polling. 6 | 7 | uses go channels to pipe around events, for flexibility and multi-threading. 8 | 9 | ## Using 10 | 11 | basically, after some setup, you handle events on the channels returned by methods on the HID type. each specific to an event type; `HID.On<>(<>)` 12 | 13 | the Capture() method automates this by returning a slice of channels for all the required events. 14 | 15 | package also has some higher-level event modifiers for common UI abstractions, to help standardise advanced usage. 16 | 17 | Overview/docs: [![GoDoc](https://godoc.org/github.com/splace/joysticks?status.svg)](https://godoc.org/github.com/splace/joysticks) 18 | 19 | ## Installation: 20 | 21 | go get github.com/splace/joysticks 22 | 23 | ## Examples: 24 | 25 | ### highlevel 26 | 27 | automates Connection to an available device, event chan creation and parcelling out those events 28 | 29 | ```` Go 30 | // block until button one pressed. 31 | package main 32 | 33 | import . "github.com/splace/joysticks" 34 | 35 | func main() { 36 | evts := Capture( 37 | Channel{1, HID.OnClose}, // evts[0] set to receive button #1 closes events 38 | ) 39 | <-evts[0] 40 | } 41 | ```` 42 | 43 | ### Midlevel 44 | 45 | allows device interrogation and event re-assigning. 46 | 47 | ```` Go 48 | // log a description of events when pressing button #1 or moving hat#1. 49 | // 10sec timeout. 50 | package main 51 | 52 | import . "github.com/splace/joysticks" 53 | import "log" 54 | import "time" 55 | 56 | func main() { 57 | // try connecting to specific controller. 58 | // the index is system assigned, typically it increments on each new controller added. 59 | // indexes remain fixed for a given controller, if/when other controller(s) are removed. 60 | device := Connect(1) 61 | 62 | if device == nil { 63 | panic("no HIDs") 64 | } 65 | 66 | // using Connect allows a device to be interrogated 67 | log.Printf("HID#1:- Buttons:%d, Hats:%d\n", len(device.Buttons), len(device.HatAxes)/2) 68 | 69 | // get/assign channels for specific events 70 | b1press := device.OnClose(1) 71 | h1move := device.OnMove(1) 72 | 73 | // start feeding OS events onto the event channels. 74 | go device.ParcelOutEvents() 75 | 76 | // handle event channels 77 | go func(){ 78 | for{ 79 | select { 80 | case <-b1press: 81 | log.Println("button #1 pressed") 82 | case h := <-h1move: 83 | hpos:=h.(CoordsEvent) 84 | log.Println("hat #1 moved too:", hpos.X,hpos.Y) 85 | } 86 | } 87 | }() 88 | 89 | log.Println("Timeout in 10 secs.") 90 | time.Sleep(time.Second*10) 91 | log.Println("Shutting down due to timeout.") 92 | } 93 | ```` 94 | 95 | Note: "jstest-gtk" - gtk mapping and calibration for joysticks. 96 | 97 | 98 | -------------------------------------------------------------------------------- /joysticks_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package joysticks 4 | 5 | import ( 6 | "encoding/binary" 7 | "io" 8 | "os" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // see; https://www.kernel.org/doc/Documentation/input/joystick-api.txt 14 | type osEventRecord struct { 15 | Time uint32 // event timestamp, unknown base, in milliseconds 32bit, so about a month 16 | Value int16 // value 17 | Type uint8 // event type 18 | Index uint8 // axis/button 19 | } 20 | 21 | const maxValue = 1<<15 - 1 22 | 23 | // common path root, so Connect and DeviceExists are not thread safe. 24 | var inputPathSlice = []byte("/dev/input/js ")[0:13] 25 | 26 | // see if Device exists. 27 | func DeviceExists(index uint8) bool { 28 | _,err:=os.Stat(string(strconv.AppendUint(inputPathSlice, uint64(index-1), 10))) 29 | return err==nil 30 | } 31 | 32 | // Connect sets up a go routine that puts a joysticks events onto registered channels. 33 | // to register channels use the returned HID object's On(index) methods. 34 | // Note: only one event, of each type '', for each 'index', so re-registering, or deleting, an event stops events going on the old channel. 35 | // It Needs the HID objects ParcelOutEvents() method to be running to perform routing.(so usually in a go routine.) 36 | func Connect(index int) (d *HID) { 37 | r, e := os.OpenFile(string(strconv.AppendUint(inputPathSlice, uint64(index-1), 10)), os.O_RDWR, 0) 38 | if e != nil { 39 | return nil 40 | } 41 | d = &HID{make(chan osEventRecord), make(map[uint8]button), make(map[uint8]hatAxis), make(map[eventSignature]chan Event)} 42 | // start thread to read joystick events to the joystick.state osEvent channel 43 | go eventPipe(r, d.OSEvents) 44 | d.populate() 45 | return d 46 | } 47 | 48 | // fill in the joysticks available events from the synthetic events burst produced initially by the driver. 49 | func (d HID) populate() { 50 | for buttonNumber, hatNumber, axisNumber := 1, 1, 1; ; { 51 | evt := <-d.OSEvents 52 | switch evt.Type { 53 | case 0x81: 54 | d.Buttons[evt.Index] = button{uint8(buttonNumber), toDuration(evt.Time), evt.Value != 0} 55 | buttonNumber += 1 56 | case 0x82: 57 | d.HatAxes[evt.Index] = hatAxis{uint8(hatNumber), uint8(axisNumber), false, toDuration(evt.Time), float32(evt.Value) / maxValue} 58 | axisNumber += 1 59 | if axisNumber > 2 { 60 | axisNumber = 1 61 | hatNumber += 1 62 | } 63 | default: 64 | go func() { d.OSEvents <- evt }() // have to consume a real event to know we reached the end of the synthetic burst, so refire it. 65 | return 66 | } 67 | } 68 | return 69 | } 70 | 71 | // pipe any readable events onto channel. 72 | func eventPipe(r io.Reader, c chan osEventRecord) { 73 | var evt osEventRecord 74 | for { 75 | if binary.Read(r, binary.LittleEndian, &evt) != nil { 76 | close(c) 77 | return 78 | } 79 | c <- evt 80 | } 81 | } 82 | 83 | func toDuration(m uint32) time.Duration { 84 | return time.Duration(m) * 1000000 85 | } 86 | -------------------------------------------------------------------------------- /joysticks_test.go: -------------------------------------------------------------------------------- 1 | package joysticks 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | import . "github.com/splace/sounds" 8 | 9 | import ( 10 | "io" 11 | "os/exec" 12 | "time" 13 | ) 14 | import "math" 15 | 16 | func TestHIDsMutipleCapture(t *testing.T) { 17 | if !DeviceExists(1) && !DeviceExists(2) && !DeviceExists(3) && !DeviceExists(4){ 18 | panic("no HIDs") 19 | } 20 | 21 | buttonEvents := Capture( 22 | Channel{10, HID.OnLong}, // button #10 long pressed 23 | Channel{1, HID.OnClose}, 24 | ) 25 | hatEvents := Capture( 26 | Channel{1, HID.OnHat}, 27 | Channel{2, HID.OnSpeedX}, 28 | Channel{2, HID.OnPanX}, 29 | ) 30 | 31 | var x float32 = .5 32 | var f time.Duration = time.Second / 440 33 | for { 34 | select { 35 | case <-buttonEvents[0]: 36 | return 37 | case <-buttonEvents[1]: 38 | play(NewSound(NewTone(f, float64(x)), time.Second/3)) 39 | case h := <-hatEvents[0]: 40 | f = time.Duration(100*math.Pow(2, float64(h.(CoordsEvent).Y))) * time.Second / 44000 41 | case h := <-hatEvents[1]: 42 | fmt.Printf("hat 2 X speed %+v\n",h.(AxisEvent).V) 43 | case h := <-hatEvents[2]: 44 | fmt.Printf("hat 2 X pan %+v\n",h.(AxisEvent).V) 45 | } 46 | } 47 | } 48 | 49 | func TestHIDsAdvanced(t *testing.T) { 50 | js1 := Connect(1) 51 | 52 | if js1 == nil { 53 | panic("no HID index 1") 54 | } 55 | if len(js1.Buttons) < 10 || len(js1.HatAxes) < 6 { 56 | t.Errorf("HID#1, available buttons %d, Hats %d\n", len(js1.Buttons), len(js1.HatAxes)/2) 57 | } 58 | 59 | b1 := js1.OnClose(1) 60 | b2 := js1.OnClose(2) 61 | b3 := js1.OnClose(3) 62 | b4 := js1.OnClose(4) 63 | DefaultRepeat=time.Second/10 64 | b5r := Repeater(js1.OnClose(5),js1.OnOpen(5)) 65 | 66 | quit := js1.OnOpen(10) 67 | h3 := PositionFromVelocity(js1.OnMove(1)) 68 | h4 := js1.OnPanX(2) 69 | h5 := js1.OnPanY(2) 70 | h6 := js1.OnEdge(1) 71 | h7 := js1.OnCenter(3) 72 | go js1.ParcelOutEvents() 73 | time.AfterFunc(time.Second*10, func() { js1.InsertSyntheticEvent(1, 1, 1) }) // value=1 (close),type=1 (button), index=1, so fires b1 after 10 seconds 74 | 75 | for { 76 | select { 77 | case <-quit: 78 | return 79 | case <-b1: 80 | DefaultRepeat=time.Second 81 | play(NewSound(NewTone(time.Second/440, 1), time.Second/3)) 82 | case <-b2: 83 | play(NewSound(NewTone(time.Second/660, 1), time.Second/3)) 84 | b5r = Repeater(js1.OnClose(5),js1.OnOpen(5)) 85 | case <-b3: 86 | play(NewSound(NewTone(time.Second/250, 1), time.Second/3)) 87 | case <-b4: 88 | play(NewSound(NewTone(time.Second/150, 1), time.Second/3)) 89 | case <-b5r: 90 | go play(NewSound(NewTone(time.Second/440, 1), time.Second/20)) 91 | case h := <-h3: 92 | fmt.Printf("hat 1 moved %+v\n", h) 93 | case h := <-h4: 94 | fmt.Println("hat 2 X moved", h.(AxisEvent).V) 95 | case h := <-h5: 96 | fmt.Printf("hat 2 Y moved %+v\n", h) 97 | case h := <-h6: 98 | fmt.Println("hat 1 edged", h.(AngleEvent).Angle) 99 | case <-h7: 100 | fmt.Println("hat 3 centered") 101 | } 102 | } 103 | } 104 | 105 | func TestHIDsCapture(t *testing.T) { 106 | events := Capture( 107 | Channel{10, HID.OnDouble}, // event[0] button #10 double press 108 | Channel{1, HID.OnClose}, // event[1] button #1 closes 109 | Channel{1, HID.OnRotate}, // event[2] hat #1 rotates 110 | Channel{2, HID.OnRotate}, // event[2] hat #2 rotates 111 | ) 112 | var x float32 = .5 113 | var f time.Duration = time.Second / 440 114 | for { 115 | select { 116 | case <-events[0]: 117 | return 118 | case <-events[1]: 119 | play(NewSound(NewTone(f, float64(x)), time.Second/3)) 120 | case h := <-events[2]: 121 | fmt.Println(h.(AngleEvent).Angle) 122 | x = h.(AngleEvent).Angle/6.28 + .5 123 | case h := <-events[3]: 124 | fmt.Println(h.(AngleEvent).Angle) 125 | f = time.Duration(100*math.Pow(2, float64(h.(AngleEvent).Angle)/6.28)) * time.Second / 44000 126 | } 127 | } 128 | } 129 | 130 | func play(s Sound) { 131 | out, in := io.Pipe() 132 | go func() { 133 | Encode(in, 2, 44100, s) 134 | in.Close() 135 | }() 136 | cmd := exec.Command("aplay") 137 | cmd.Stdin = out 138 | err := cmd.Run() 139 | if err != nil { 140 | panic(err) 141 | } 142 | } 143 | 144 | 145 | -------------------------------------------------------------------------------- /joysticks.go: -------------------------------------------------------------------------------- 1 | package joysticks 2 | 3 | import ( 4 | "math" 5 | "time" 6 | //"fmt" 7 | ) 8 | 9 | var LongPressDelay = time.Second / 2 10 | var DoublePressDelay = time.Second / 10 11 | 12 | type hatAxis struct { 13 | number uint8 14 | axis uint8 15 | reversed bool 16 | time time.Duration 17 | value float32 18 | } 19 | 20 | type button struct { 21 | number uint8 22 | time time.Duration 23 | value bool 24 | } 25 | 26 | type eventType uint8 27 | 28 | const ( 29 | buttonChange eventType = iota 30 | buttonClose 31 | buttonOpen 32 | buttonLongPress 33 | buttonDoublePress 34 | hatChange 35 | hatPanX 36 | hatPanY 37 | hatPosition 38 | hatAngle 39 | hatRadius 40 | hatCentered 41 | hatEdge 42 | hatVelocityX 43 | hatVelocityY 44 | ) 45 | 46 | // signature of an event 47 | type eventSignature struct { 48 | eventType 49 | number uint8 50 | } 51 | 52 | // HID holds the in-coming event channel, available button and hat indexes, and registered events, for a human interface device. 53 | // It has methods to control and adjust behaviour. 54 | type HID struct { 55 | OSEvents chan osEventRecord 56 | Buttons map[uint8]button 57 | HatAxes map[uint8]hatAxis 58 | Events map[eventSignature]chan Event 59 | } 60 | 61 | // Events always have the time they occurred. 62 | type Event interface { 63 | Moment() time.Duration 64 | } 65 | 66 | type when struct { 67 | Time time.Duration 68 | } 69 | 70 | func (b when) Moment() time.Duration { 71 | return b.Time 72 | } 73 | 74 | 75 | // button changed 76 | type ButtonEvent struct { 77 | when 78 | number uint8 79 | value bool 80 | } 81 | 82 | // hat changed 83 | type HatEvent struct { 84 | when 85 | number uint8 86 | axis uint8 87 | value float32 88 | } 89 | 90 | // Hat position event type. X,Y{-1...1} 91 | type CoordsEvent struct { 92 | when 93 | X, Y float32 94 | } 95 | 96 | // Hat Axis event type. V{-1...1} 97 | type AxisEvent struct { 98 | when 99 | V float32 100 | } 101 | 102 | // Hat angle event type. Angle{-Pi...Pi} 103 | type AngleEvent struct { 104 | when 105 | Angle float32 106 | } 107 | 108 | // Hat radius event type. Radius{0...√2} 109 | type RadiusEvent struct { 110 | when 111 | Radius float32 112 | } 113 | 114 | // ParcelOutEvents waits on the HID.OSEvents channel (so is blocking), then puts any events matching onto any registered channel(s). 115 | func (d HID) ParcelOutEvents() { 116 | for evt := range d.OSEvents { 117 | switch evt.Type { 118 | case 1: 119 | b := d.Buttons[evt.Index] 120 | if c, ok := d.Events[eventSignature{buttonChange, b.number}]; ok { 121 | c <- ButtonEvent{when{toDuration(evt.Time)}, b.number, evt.Value == 1} 122 | } 123 | if evt.Value == 0 { 124 | if c, ok := d.Events[eventSignature{buttonOpen, b.number}]; ok { 125 | c <- when{toDuration(evt.Time)} 126 | } 127 | if c, ok := d.Events[eventSignature{buttonLongPress, b.number}]; ok { 128 | if toDuration(evt.Time) > b.time+LongPressDelay { 129 | c <- when{toDuration(evt.Time)} 130 | } 131 | } 132 | } 133 | if evt.Value == 1 { 134 | if c, ok := d.Events[eventSignature{buttonClose, b.number}]; ok { 135 | c <- when{toDuration(evt.Time)} 136 | } 137 | if c, ok := d.Events[eventSignature{buttonDoublePress, b.number}]; ok { 138 | if toDuration(evt.Time) < b.time+DoublePressDelay { 139 | c <- when{toDuration(evt.Time)} 140 | } 141 | } 142 | } 143 | d.Buttons[evt.Index] = button{b.number, toDuration(evt.Time), evt.Value != 0} 144 | case 2: 145 | h := d.HatAxes[evt.Index] 146 | v := float32(evt.Value) / maxValue 147 | if h.reversed { 148 | v = -v 149 | } 150 | if c, ok := d.Events[eventSignature{hatChange, h.number}]; ok { 151 | c <- HatEvent{when{toDuration(evt.Time)}, h.number, h.axis, v} 152 | } 153 | switch h.axis { 154 | case 1: 155 | if c, ok := d.Events[eventSignature{hatPanY, h.number}]; ok { 156 | c <- AxisEvent{when{toDuration(evt.Time)}, v} 157 | } 158 | if c, ok := d.Events[eventSignature{hatVelocityY, h.number}]; ok { 159 | c <- AxisEvent{when{toDuration(evt.Time)}, (v-d.HatAxes[evt.Index].value)/float32((toDuration(evt.Time)-d.HatAxes[evt.Index].time).Seconds())} 160 | } 161 | case 2: 162 | if c, ok := d.Events[eventSignature{hatPanX, h.number}]; ok { 163 | c <- AxisEvent{when{toDuration(evt.Time)}, v} 164 | } 165 | if c, ok := d.Events[eventSignature{hatVelocityX, h.number}]; ok { 166 | c <- AxisEvent{when{toDuration(evt.Time)}, (v-d.HatAxes[evt.Index].value)/float32((toDuration(evt.Time)-d.HatAxes[evt.Index].time).Seconds())} 167 | } 168 | } 169 | if c, ok := d.Events[eventSignature{hatPosition, h.number}]; ok { 170 | switch h.axis { 171 | case 1: 172 | c <- CoordsEvent{when{toDuration(evt.Time)}, v ,d.HatAxes[evt.Index+1].value} 173 | case 2: 174 | c <- CoordsEvent{when{toDuration(evt.Time)}, d.HatAxes[evt.Index-1].value,v} 175 | } 176 | } 177 | if c, ok := d.Events[eventSignature{hatAngle, h.number}]; ok { 178 | switch h.axis { 179 | case 1: 180 | c <- AngleEvent{when{toDuration(evt.Time)}, float32(math.Atan2(float64(d.HatAxes[evt.Index+1].value), float64(v)))} 181 | case 2: 182 | c <- AngleEvent{when{toDuration(evt.Time)}, float32(math.Atan2(float64(v), float64(d.HatAxes[evt.Index-1].value)))} 183 | } 184 | } 185 | if c, ok := d.Events[eventSignature{hatRadius, h.number}]; ok { 186 | switch h.axis { 187 | case 1: 188 | c <- RadiusEvent{when{toDuration(evt.Time)}, float32(math.Sqrt(float64(d.HatAxes[evt.Index+1].value)*float64(d.HatAxes[evt.Index+1].value) + float64(v)*float64(v)))} 189 | case 2: 190 | c <- RadiusEvent{when{toDuration(evt.Time)}, float32(math.Sqrt(float64(v)*float64(v) + float64(d.HatAxes[evt.Index-1].value)*float64(d.HatAxes[evt.Index-1].value)))} 191 | } 192 | } 193 | if c, ok := d.Events[eventSignature{hatEdge, h.number}]; ok { 194 | // fmt.Println(v,h) 195 | if (v == 1 || v == -1) && h.value != 1 && h.value != -1 { 196 | switch h.axis { 197 | case 1: 198 | c <- AngleEvent{when{toDuration(evt.Time)}, float32(math.Atan2(float64(d.HatAxes[evt.Index+1].value), float64(v)))} 199 | case 2: 200 | c <- AngleEvent{when{toDuration(evt.Time)}, float32(math.Atan2(float64(v), float64(d.HatAxes[evt.Index-1].value)))} 201 | } 202 | } 203 | } 204 | if c, ok := d.Events[eventSignature{hatCentered, h.number}]; ok { 205 | if v == 0 && h.value != 0 { 206 | switch h.axis { 207 | case 2: 208 | if d.HatAxes[evt.Index-1].value == 0 { 209 | c <- when{toDuration(evt.Time)} 210 | } 211 | case 1: 212 | if d.HatAxes[evt.Index+1].value == 0 { 213 | c <- when{toDuration(evt.Time)} 214 | } 215 | } 216 | } 217 | } 218 | d.HatAxes[evt.Index] = hatAxis{h.number, h.axis, h.reversed, toDuration(evt.Time), v} 219 | default: 220 | // log.Println("unknown input type. ",evt.Type & 0x7f) 221 | } 222 | } 223 | } 224 | 225 | // Type of register-able methods and the index they are called with. (Note: the event type is indicated by the method.) 226 | type Channel struct { 227 | Number uint8 228 | Method func(HID, uint8) chan Event 229 | } 230 | 231 | // Capture is highlevel automation of the setup of event channels. 232 | // Returned is a slice of chan's, matching each registree, which then receive events of the type and index the registree indicated. 233 | // It uses the first available joystick, from a max of 4. 234 | // Since it doesn't return a HID object, channels are immutable. 235 | func Capture(registrees ...Channel) []chan Event { 236 | d := Connect(1) 237 | for i := 2; d == nil && i < 5; i++ { 238 | d = Connect(i) 239 | } 240 | if d == nil { 241 | return nil 242 | } 243 | go d.ParcelOutEvents() 244 | chans := make([]chan Event, len(registrees)) 245 | for i, fns := range registrees { 246 | chans[i] = fns.Method(*d, fns.Number) 247 | } 248 | return chans 249 | } 250 | 251 | 252 | // button changes event channel. 253 | func (d HID) OnButton(index uint8) chan Event { 254 | c := make(chan Event) 255 | d.Events[eventSignature{buttonChange, index}] = c 256 | return c 257 | } 258 | 259 | // button goes open event channel. 260 | func (d HID) OnOpen(index uint8) chan Event { 261 | c := make(chan Event) 262 | d.Events[eventSignature{buttonOpen, index}] = c 263 | return c 264 | } 265 | 266 | // button goes closed event channel. 267 | func (d HID) OnClose(index uint8) chan Event { 268 | c := make(chan Event) 269 | d.Events[eventSignature{buttonClose, index}] = c 270 | return c 271 | } 272 | 273 | // button goes open and the previous event, closed, was more than LongPressDelay ago, event channel. 274 | func (d HID) OnLong(index uint8) chan Event { 275 | c := make(chan Event) 276 | d.Events[eventSignature{buttonLongPress, index}] = c 277 | return c 278 | } 279 | 280 | // button goes closed and the previous event, open, was less than DoublePressDelay ago, event channel. 281 | func (d HID) OnDouble(index uint8) chan Event { 282 | c := make(chan Event) 283 | d.Events[eventSignature{buttonDoublePress, index}] = c 284 | return c 285 | } 286 | 287 | // hat moved event channel. 288 | func (d HID) OnHat(index uint8) chan Event { 289 | c := make(chan Event) 290 | d.Events[eventSignature{hatChange, index}] = c 291 | return c 292 | } 293 | 294 | // hat position changed event channel. 295 | func (d HID) OnMove(index uint8) chan Event { 296 | c := make(chan Event) 297 | d.Events[eventSignature{hatPosition, index}] = c 298 | return c 299 | } 300 | 301 | // hat axis-X moved event channel. 302 | func (d HID) OnPanX(index uint8) chan Event { 303 | c := make(chan Event) 304 | d.Events[eventSignature{hatPanX, index}] = c 305 | return c 306 | } 307 | 308 | // hat axis-Y moved event channel. 309 | func (d HID) OnPanY(index uint8) chan Event { 310 | c := make(chan Event) 311 | d.Events[eventSignature{hatPanY, index}] = c 312 | return c 313 | } 314 | 315 | // hat axis-X speed changed event channel. 316 | func (d HID) OnSpeedX(index uint8) chan Event { 317 | c := make(chan Event) 318 | d.Events[eventSignature{hatVelocityX, index}] = c 319 | return c 320 | } 321 | 322 | // hat axis-Y speed changed event channel. 323 | func (d HID) OnSpeedY(index uint8) chan Event { 324 | c := make(chan Event) 325 | d.Events[eventSignature{hatVelocityY, index}] = c 326 | return c 327 | } 328 | 329 | // hat angle changed event channel. 330 | func (d HID) OnRotate(index uint8) chan Event { 331 | c := make(chan Event) 332 | d.Events[eventSignature{hatAngle, index}] = c 333 | return c 334 | } 335 | 336 | // hat moved event channel. 337 | func (d HID) OnCenter(index uint8) chan Event { 338 | c := make(chan Event) 339 | d.Events[eventSignature{hatCentered, index}] = c 340 | return c 341 | } 342 | 343 | // hat moved to edge 344 | func (d HID) OnEdge(index uint8) chan Event { 345 | c := make(chan Event) 346 | d.Events[eventSignature{hatEdge, index}] = c 347 | return c 348 | } 349 | 350 | // hat integrate 351 | //func (d HID) OnIntegrate(c Channel) chan Event { 352 | // var e,le Event 353 | // e:=Event{} 354 | // c := make(chan Event) 355 | // d.Events[eventSignature{hatEdge, hat}] = c 356 | // return c 357 | //} 358 | 359 | 360 | // see if Button exists. 361 | func (d HID) ButtonExists(index uint8) (ok bool) { 362 | for _, v := range d.Buttons { 363 | if v.number == index { 364 | return true 365 | } 366 | } 367 | return 368 | } 369 | 370 | // see if Hat exists. 371 | func (d HID) HatExists(index uint8) (ok bool) { 372 | for _, v := range d.HatAxes { 373 | if v.number == index { 374 | return true 375 | } 376 | } 377 | return 378 | } 379 | 380 | // Button current state. 381 | func (d HID) ButtonClosed(index uint8) bool { 382 | return d.Buttons[index].value 383 | } 384 | 385 | // Hat latest position. 386 | // provided coords slice needs to be long enough to hold all the hat's axis. 387 | func (d HID) HatCoords(index uint8, coords []float32) { 388 | for _, h := range d.HatAxes { 389 | if h.number == index { 390 | coords[h.axis-1] = h.value 391 | } 392 | } 393 | return 394 | } 395 | 396 | // insert events as if from hardware. 397 | func (d HID) InsertSyntheticEvent(v int16, t uint8, i uint8) { 398 | d.OSEvents <- osEventRecord{Value: v, Type: t, Index: i} 399 | } 400 | 401 | 402 | --------------------------------------------------------------------------------