├── README.md ├── demo.gif ├── go.mod ├── go.sum ├── gohome.go ├── lights.go ├── main.go └── scout /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # go-scout 4 | 5 | ## Intro 6 | 7 | go-scout is a tool that allows you to control a Moorebot Scout robot from your computer (without using the mobile app). The robot is controlled using an XBOX controller, and video is displayed in a new window. 8 | 9 | ## Demo 10 | 11 | ![go-scout demo](demo.gif) 12 | ## Installation 13 | 14 | To install go-scout, simply download the latest release from the releases page and run the installer, or compile using the steps below. 15 | ``` 16 | go install github.com/shell-company/go-scout 17 | 18 | OR 19 | 20 | git clone https://github.com/shell-company/go-scout && cd go-scout && go build 21 | ``` 22 | 23 | 24 | ## Usage 25 | 26 | To use go-scout, connect an XBOX controller to your computer and launch the application. The left stick is used for forward, reverse, and turning left or right. The right stick is used for strafing. The left bumper lowers the max speed, the right bumper raises max speed. Both bumpers pressed together tell the robot to stop moving. The start button exits the application. 27 | 28 | ## Controls 29 | 30 | | Button | Action | 31 | | --- | --- | 32 | | Left Stick | Forward, Reverse, Left, Right | 33 | | Right Stick | Strafe | 34 | | Left Bumper | Lower Max Speed | 35 | | Right Bumper | Raise Max Speed | 36 | | Both Bumpers | Stop | 37 | | Start | Exit | 38 | | Keyboard 0 | -Night Vision brightness | 39 | | Keyboard 9 | +Night Vision brightness | 40 | | Keyboard s | Save image to disk | 41 | | Keyboard h | Return to charging station | 42 | 43 | 44 | ``` 45 | Usage of ./scout: 46 | -h string 47 | ROS endpoint such as IP_ADDRESS:PORT (default "192.168.1.224:11311") 48 | -v verbose 49 | 50 | 51 | 52 | ╰─$ ./scout -h "192.168.1.225:11311" 53 | Starting go-scout controller for Moorebot Scout 54 | ROS endpoint: 192.168.1.225:11311 55 | 2022/09/02 15:49:54 Please connect a joystick 56 | ``` 57 | 58 | # 59 | 60 | ## Considerations 61 | 62 | Moorebot added a fair amount of bloatware to an otherwise great hardware platform. I took the following steps to increase privacy and reduce resource usage on the robot. 63 | 64 | **Follow the steps below at your own risk** as they may void warranties and potentially violate Moorebot terms of service. 65 | 66 | - [ ] SSH Access via `root:plt` to bot IP 67 | ``` 68 | ssh root@ 69 | ``` 70 | - [ ] Remove /opt/sockproxy/proxy_list.json 71 | ``` 72 | rm /opt/sockproxy/proxy_list.json 73 | ``` 74 | - [ ] Disable sockproxy service 75 | ``` 76 | systemctl disable sockproxy.service 77 | ``` 78 | - [ ] Null route what appear to be backdoors in the CloudNode 79 | ``` 80 | route add 62.210.208.47 gw 127.0.0.1 lo 81 | route add 45.35.33.24 gw 127.0.0.1 lo 82 | route add 118.107.244.35 gw 127.0.0.1 lo 83 | ``` 84 | 85 | # 86 | ## To-Do 87 | 88 | - Add support for other controllers 89 | - Create a proper Heads-Up-Display 90 | - Add support for more features of the robot 91 | - Add battery status to HUD 92 | - Add compass to HUD 93 | - Add sensor data to HUD -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shell-Company/go-scout/dbcd03cc2189deb1a1ea9767025241220f21ae4f/demo.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shell-company/scout 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/aler9/goroslib v0.0.0-20220831065204-cd7cf8973f37 7 | github.com/hajimehoshi/ebiten/v2 v2.4.0 8 | github.com/simulatedsimian/joystick v1.0.1 9 | ) 10 | 11 | require ( 12 | github.com/ebitengine/purego v0.0.0-20220816145426-8dbe340b03f1 // indirect 13 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad // indirect 14 | github.com/gookit/color v1.5.0 // indirect 15 | github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 // indirect 16 | github.com/jezek/xgb v1.0.1 // indirect 17 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect 18 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 19 | golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect 20 | golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 // indirect 21 | golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 2 | github.com/aler9/goroslib v0.0.0-20220831065204-cd7cf8973f37 h1:d2XPF2haVWLu/sYhS+Xod2Dkd61niWtT5KIgjEegH70= 3 | github.com/aler9/goroslib v0.0.0-20220831065204-cd7cf8973f37/go.mod h1:adehbok4XISDGl+3WZqEetqleuE6Ww+WHI6nVrvF4n8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/ebitengine/purego v0.0.0-20220816145426-8dbe340b03f1 h1:kf5uxeNGrfkESJQe81OvrxJG1jZaafBjssteag0GbPQ= 7 | github.com/ebitengine/purego v0.0.0-20220816145426-8dbe340b03f1/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg= 8 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad h1:kX51IjbsJPCvzV9jUoVQG9GEUqIq5hjfYzXTqQ52Rh8= 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 10 | github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw= 11 | github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= 12 | github.com/hajimehoshi/bitmapfont/v2 v2.2.1/go.mod h1:wjrYAy8vKgj9JsFgnYAOK346/uvE22TlmqouzdnYIs0= 13 | github.com/hajimehoshi/ebiten/v2 v2.4.0 h1:IDs5vvXGRgjLoRCFBi0UU0bXLzMXExY5znzADCORVsI= 14 | github.com/hajimehoshi/ebiten/v2 v2.4.0/go.mod h1:z9L4JZ3XHQqvCuVuAQ1qTByIV2YHGS1w6keUSVC9NDA= 15 | github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 h1:s01qIIRG7vN/5ndLwkDktjx44ulFk6apvAjVBYR50Yo= 16 | github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= 17 | github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= 18 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 19 | github.com/hajimehoshi/oto/v2 v2.3.0/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= 20 | github.com/jakecoffman/cp v1.2.1/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= 21 | github.com/jezek/xgb v1.0.1 h1:YUGhxps0aR7J2Xplbs23OHnV1mWaxFVcOl9b+1RQkt8= 22 | github.com/jezek/xgb v1.0.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 23 | github.com/jfreymuth/oggvorbis v1.0.4/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII= 24 | github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ= 25 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/simulatedsimian/joystick v1.0.1 h1:bZYHP+qaEmx0oc6oFIYQCt+rYL46G6rZRke+bipEEpY= 29 | github.com/simulatedsimian/joystick v1.0.1/go.mod h1:V+4pJKB2SmVXfp8w7EW5RR9InUXO5M/WhVEHwosjujA= 30 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 31 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 32 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 33 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= 34 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= 35 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 36 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 37 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 38 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 39 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 40 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 41 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 42 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= 43 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 44 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 45 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 46 | golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU= 47 | golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= 48 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 49 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 50 | golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 h1:3vUV5x5+3LfQbgk7paCM6INOaJG9xXQbn79xoNkwfIk= 51 | golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= 52 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 53 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 54 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 55 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 56 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 57 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 58 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 59 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 60 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 61 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 62 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U= 78 | golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 80 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 81 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 82 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 83 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 84 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 85 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 86 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 87 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 88 | golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 89 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 90 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 92 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 95 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | -------------------------------------------------------------------------------- /gohome.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/aler9/goroslib" 7 | "github.com/aler9/goroslib/pkg/msg" 8 | ) 9 | 10 | type goHomeRequest struct { 11 | } 12 | 13 | type goHomeResponse struct { 14 | } 15 | 16 | type goHomeService struct { 17 | msg.Package `ros:"/nav_low_bat"` 18 | Request goHomeRequest 19 | Response goHomeResponse 20 | } 21 | 22 | // call the /CoreNode/adjust_light service with a value of 1 to turn on the light 23 | func scoutGoHome() { 24 | // create a node and connect to the master 25 | n, err := goroslib.NewNode(goroslib.NodeConf{ 26 | Name: "scout-go-home", 27 | MasterAddress: *flagROSHostAddress, 28 | }) 29 | if err != nil { 30 | panic(err) 31 | } 32 | defer n.Close() 33 | // create a service client 34 | cl, err := goroslib.NewServiceClient(goroslib.ServiceClientConf{ 35 | Node: n, 36 | Name: "/nav_low_bat", 37 | Srv: &goHomeService{ 38 | Request: goHomeRequest{}, 39 | }}) 40 | if err != nil { 41 | log.Println("An error occured while trying to send the bot home", err) 42 | } 43 | defer cl.Close() 44 | // call the service 45 | req := goHomeRequest{} 46 | res := goHomeResponse{} 47 | 48 | err = cl.Call(&req, &res) 49 | if err != nil { 50 | panic(err) 51 | } 52 | log.Println("Command received: Scout returning home in 5 seconds") 53 | } 54 | -------------------------------------------------------------------------------- /lights.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/aler9/goroslib" 5 | "github.com/aler9/goroslib/pkg/msg" 6 | ) 7 | 8 | type adjustLightRequest struct { 9 | Cmd int32 `ros:"cmd int32"` 10 | } 11 | 12 | type adjustLightResponse struct { 13 | } 14 | 15 | type adjustLightService struct { 16 | msg.Package `ros:"/CoreNode/adjust_light"` 17 | Request adjustLightRequest 18 | Response adjustLightResponse 19 | } 20 | 21 | // call the /CoreNode/adjust_light service with a value of 1 to turn on the light 22 | func turnOnLight(lightValue int32) { 23 | // create a node and connect to the master 24 | n, err := goroslib.NewNode(goroslib.NodeConf{ 25 | Name: "scout-lights", 26 | MasterAddress: *flagROSHostAddress, 27 | }) 28 | if err != nil { 29 | panic(err) 30 | } 31 | defer n.Close() 32 | 33 | // create a service client 34 | cl, err := goroslib.NewServiceClient(goroslib.ServiceClientConf{ 35 | Node: n, 36 | Name: "/CoreNode/adjust_light", 37 | Srv: &adjustLightService{ 38 | Request: adjustLightRequest{ 39 | Cmd: lightValue, 40 | }, 41 | }}) 42 | if err != nil { 43 | panic(err) 44 | } 45 | defer cl.Close() 46 | 47 | // call the service 48 | req := adjustLightRequest{ 49 | Cmd: lightValue, 50 | } 51 | res := adjustLightResponse{} 52 | 53 | err = cl.Call(&req, &res) 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | _ "image/jpeg" 8 | _ "image/png" 9 | "log" 10 | "math" 11 | "os" 12 | "os/signal" 13 | "time" 14 | 15 | "github.com/aler9/goroslib" 16 | "github.com/aler9/goroslib/pkg/msg" 17 | "github.com/aler9/goroslib/pkg/msgs/geometry_msgs" 18 | "github.com/hajimehoshi/ebiten/v2" 19 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 20 | "github.com/simulatedsimian/joystick" 21 | ) 22 | 23 | var ( 24 | cameraData = make(chan []byte) 25 | controlData string 26 | ROSHostAddress = "192.168.1.224:11311" 27 | localhostAddress = "127.0.0.1" 28 | flagWindowX = flag.Int("windowX", 1920, "window width") 29 | flagWindowY = flag.Int("windowY", 1080, "window height") 30 | WindowX int 31 | WindowY int 32 | flagROSHostAddress = flag.String("h", ROSHostAddress, "ROS endpoint such as IP_ADDRESS:PORT") 33 | flaglocalhost = flag.String("l", localhostAddress, "localhost address") 34 | flagControlScheme = flag.String("c", "keyboard", "control scheme, keyboard or joystick") 35 | flagVerbose = flag.Bool("v", false, "verbose") 36 | joystickLeftX float64 37 | joystickLeftY float64 38 | joystickRightX float64 39 | joystickRightY float64 40 | pub *goroslib.Publisher 41 | forwardSpeed = .2 42 | maxForwardSpeed = 1.0 43 | minForwardSpeed = 0.1 44 | n *goroslib.Node 45 | err error 46 | ) 47 | 48 | const ( 49 | Frame_VIDEO_STREAM_H264 int8 = 0 50 | Frame_VIDEO_STREAM_JPG int8 = 1 51 | Frame_AUDIO_STREAM_AAC int8 = 2 52 | ) 53 | 54 | type Game struct{} 55 | 56 | func (g *Game) Draw(screen *ebiten.Image) { 57 | // retrieve the image from the channel 58 | imageFromCamera := <-cameraData 59 | // convert to image to an ebiten image 60 | cameraFeed, _, err := ebitenutil.NewImageFromReader(bytes.NewReader(imageFromCamera)) 61 | if err != nil { 62 | fmt.Println(err) 63 | } 64 | // draw the image on the screen 65 | screen.DrawImage(cameraFeed, nil) 66 | // print some HUD data 67 | controlData = fmt.Sprintf("lX: %f lY: %f rX:%f rY:%f speedModifier:%f", joystickLeftX, joystickLeftY, joystickRightX, joystickRightY, forwardSpeed) 68 | ebitenutil.DebugPrint(screen, controlData) 69 | // print bottom HUD data 70 | ebitenutil.DebugPrintAt(screen, "🥷 Scout 🤖", 10, WindowY-20) 71 | } 72 | 73 | func main() { 74 | flag.Parse() 75 | 76 | n, err = goroslib.NewNode(goroslib.NodeConf{ 77 | Name: "scout-access", 78 | MasterAddress: *flagROSHostAddress, 79 | Host: *flaglocalhost, 80 | }) 81 | 82 | WindowX = *flagWindowX 83 | WindowY = *flagWindowY 84 | // Start up messages 85 | fmt.Println("Starting go-scout controller for Moorebot Scout") 86 | fmt.Println("ROS endpoint:", *flagROSHostAddress) 87 | 88 | // listen for ctrl+c 89 | c := make(chan os.Signal, 1) 90 | signal.Notify(c, os.Interrupt) 91 | go func() { 92 | sig := <-c 93 | log.Fatal("Received", sig, "signal") 94 | }() 95 | 96 | // create a node 97 | if *flagVerbose { 98 | log.Println(fmt.Sprintf("creating camera access node using %s", *flagROSHostAddress)) 99 | } 100 | 101 | if err != nil { 102 | panic(err) 103 | } 104 | defer n.Close() 105 | 106 | // create a subscriber 107 | subby := goroslib.SubscriberConf{ 108 | Node: n, 109 | Topic: "/CoreNode/jpg", 110 | Callback: onMessageFrame, 111 | QueueSize: 0, 112 | } 113 | 114 | sub, err := goroslib.NewSubscriber(subby) 115 | if err != nil { 116 | panic(err) 117 | } 118 | defer sub.Close() 119 | 120 | // init robo controller 121 | if *flagControlScheme == "joystick" { 122 | go robotControl() 123 | } else { 124 | go robotControlKeyboard() 125 | } 126 | 127 | ebiten.SetWindowSize(WindowX, WindowY) 128 | ebiten.SetWindowTitle("🥷 Scout 🤖") 129 | ebiten.SetWindowResizable(true) 130 | if err := ebiten.RunGame(&Game{}); err != nil { 131 | log.Fatal(err) 132 | } 133 | } 134 | 135 | // onMessageFrame is called when a message is received from the CoreNode/jpg topic (camera) 136 | func onMessageFrame(msg *Frame) { 137 | // write camera data to channel 138 | cameraData <- msg.Data 139 | } 140 | 141 | // robotControl is a goroutine that will read the joystick and publish the control data to the robot 142 | func robotControl() { 143 | // Print gamepad status and pos 144 | jsid := 0 145 | js, err := joystick.Open(jsid) 146 | if err != nil { 147 | log.Fatal("Please connect a joystick ") 148 | 149 | } 150 | 151 | for { 152 | // save the current image to a file 153 | if ebiten.IsKeyPressed(ebiten.KeySpace) { 154 | saveScreenshot() 155 | 156 | } 157 | 158 | if ebiten.IsKeyPressed(ebiten.KeyH) { 159 | scoutGoHome() 160 | // if so, increase the forward speed 161 | } 162 | if ebiten.IsKeyPressed(ebiten.Key9) { 163 | turnOnLight(0) 164 | // if so, increase the forward speed 165 | } 166 | if ebiten.IsKeyPressed(ebiten.Key0) { 167 | turnOnLight(1) 168 | // if so, increase the forward speed 169 | } 170 | 171 | state, err := js.Read() 172 | if err != nil { 173 | panic(err) 174 | } 175 | // -32767 to 32768 176 | joystickLeftX = squashToFloat(state.AxisData[0]) 177 | joystickLeftY = squashToFloat(state.AxisData[1]) * -1 178 | joystickRightX = squashToFloat(state.AxisData[2]) 179 | joystickRightY = squashToFloat(state.AxisData[3]) * -1 180 | if joystickLeftY == -0.0 { 181 | joystickLeftY = 0 182 | } 183 | if joystickRightY == -0.0 { 184 | joystickRightY = 0 185 | } 186 | 187 | // forward speed to hud data 188 | buttonPress := state.Buttons 189 | if *flagVerbose { 190 | fmt.Println("Left Stick:", joystickLeftX, joystickLeftY, "\n") 191 | fmt.Println("Right Stick:", joystickRightX, joystickRightY, "\n") 192 | fmt.Println("Button Press value", buttonPress, forwardSpeed) 193 | } 194 | 195 | defer js.Close() 196 | 197 | if buttonPress == 2048 { 198 | os.Exit(0) 199 | } 200 | // right bumper 201 | if buttonPress == 128 { 202 | forwardSpeed = forwardSpeed + .1 203 | } 204 | // left bumper 205 | if buttonPress == 64 { 206 | forwardSpeed = forwardSpeed - .1 207 | if forwardSpeed < 0 { 208 | forwardSpeed = 0 209 | } 210 | 211 | } 212 | // hold both bumpers to halt the robot 213 | if buttonPress == 192 { 214 | forwardSpeed = 0 215 | joystickRightX = 0 216 | } 217 | if forwardSpeed > maxForwardSpeed { 218 | forwardSpeed = 1.0 219 | } 220 | if forwardSpeed < minForwardSpeed { 221 | forwardSpeed = .1 222 | } 223 | msg := &geometry_msgs.Twist{ 224 | Linear: geometry_msgs.Vector3{ 225 | X: joystickRightX * .2, // strafe l r 226 | Y: joystickLeftY * forwardSpeed, // move fwd/back 227 | }, 228 | Angular: geometry_msgs.Vector3{ 229 | Z: joystickLeftX * -2.9, // rotate l r 230 | }, 231 | } 232 | 233 | // create a publisher 234 | pub, err = goroslib.NewPublisher(goroslib.PublisherConf{ 235 | Node: n, 236 | Topic: "/cmd_vel", 237 | Msg: msg, 238 | Latch: true, 239 | }) 240 | if err != nil { 241 | log.Fatal(err) 242 | } 243 | // send the message to the robot 244 | pub.Write(msg) 245 | // wait a bit 246 | time.Sleep(time.Millisecond * 600) 247 | pub.Close() 248 | } 249 | } 250 | 251 | // robotControlKeyboard is a goroutine that will read the keyboard and publish the control data to the robot 252 | func robotControlKeyboard() { 253 | for { 254 | // exit the program 255 | if ebiten.IsKeyPressed(ebiten.KeyEscape) { 256 | os.Exit(0) 257 | } 258 | // keyboard control w,s,a,d, q,e, space for screenshot, h for home, 9 for light 1, 0 for light 2 259 | if ebiten.IsKeyPressed(ebiten.KeyW) { 260 | joystickLeftY = 1 261 | } else if ebiten.IsKeyPressed(ebiten.KeyS) { 262 | joystickLeftY = -1 263 | } else { 264 | joystickLeftY = 0 265 | } 266 | 267 | if ebiten.IsKeyPressed(ebiten.KeyA) { 268 | joystickLeftX = -1 269 | } else if ebiten.IsKeyPressed(ebiten.KeyD) { 270 | joystickLeftX = 1 271 | } else { 272 | joystickLeftX = 0 273 | } 274 | 275 | if ebiten.IsKeyPressed(ebiten.KeyQ) { 276 | joystickRightX = -1 277 | } else if ebiten.IsKeyPressed(ebiten.KeyE) { 278 | joystickRightX = 1 279 | } else { 280 | joystickRightX = 0 281 | } 282 | 283 | // add all controls from the joystiock function 284 | if ebiten.IsKeyPressed(ebiten.KeyH) { 285 | scoutGoHome() 286 | } 287 | if ebiten.IsKeyPressed(ebiten.Key9) { 288 | turnOnLight(0) 289 | } 290 | if ebiten.IsKeyPressed(ebiten.Key0) { 291 | turnOnLight(1) 292 | } 293 | // screenshot 294 | if ebiten.IsKeyPressed(ebiten.KeySpace) { 295 | saveScreenshot() 296 | } 297 | // max speed 298 | if ebiten.IsKeyPressed(ebiten.KeyP) { 299 | if forwardSpeed < maxForwardSpeed { 300 | forwardSpeed += +.1 301 | } 302 | } 303 | // min speed 304 | if ebiten.IsKeyPressed(ebiten.KeyO) { 305 | if forwardSpeed > minForwardSpeed { 306 | forwardSpeed += -.1 307 | } 308 | } 309 | 310 | msg := &geometry_msgs.Twist{ 311 | Linear: geometry_msgs.Vector3{ 312 | X: joystickRightX * .2, // strafe l r 313 | Y: joystickLeftY * forwardSpeed, // move fwd/back 314 | }, 315 | Angular: geometry_msgs.Vector3{ 316 | Z: joystickLeftX * -1.8, // rotate l r 317 | }, 318 | } 319 | 320 | // create a publisher 321 | pub, err = goroslib.NewPublisher(goroslib.PublisherConf{ 322 | Node: n, 323 | Topic: "/cmd_vel", 324 | Msg: msg, 325 | Latch: true, 326 | }) 327 | if err != nil { 328 | log.Fatal(err) 329 | } 330 | // send the message to the robot 331 | pub.Write(msg) 332 | // wait a bit 333 | time.Sleep(time.Millisecond * 180) 334 | pub.Close() 335 | 336 | } 337 | } 338 | 339 | func (g *Game) Update() error { 340 | return nil 341 | } 342 | 343 | func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { 344 | return WindowX, WindowY 345 | } 346 | 347 | // squashToFloat converts a signed 16 bit integer to a float between -1 and 1 348 | func squashToFloat(n int) (f float64) { 349 | if n < -32767 { 350 | f = -1.0 351 | } else if n > 32768 { 352 | f = 1.0 353 | } else { 354 | value := float64(n) / 32768.0 355 | if math.Abs(value) < 0.015 { 356 | f = 0.0 357 | } else { 358 | f = value 359 | } 360 | } 361 | return f * .5 362 | } 363 | 364 | // Frame is a struct that holds the image data from the camera topic roller_bot/frame 365 | type Frame struct { 366 | msg.Package `ros:"roller_eye"` 367 | msg.Definitions `ros:"int8 VIDEO_STREAM_H264=0,int8 VIDEO_STREAM_JPG=1,int8 AUDIO_STREAM_AAC=2"` 368 | Seq uint32 369 | Stamp uint64 370 | Session uint32 371 | Type int8 372 | Oseq uint32 373 | Par1 int32 374 | Par2 int32 375 | Par3 int32 376 | Par4 int32 377 | Data []uint8 378 | } 379 | 380 | func saveScreenshot() { 381 | // save the current image to a file 382 | // retrieve the image from the channel 383 | imageFromCamera := <-cameraData 384 | // convert to image to an ebiten image 385 | // save the image to a file, write bytes to file 386 | err := os.WriteFile(fmt.Sprintf("scout-%s.jpg", time.Now().Format("2006-01-02-15-04-05")), imageFromCamera, 0644) 387 | 388 | if err != nil { 389 | fmt.Println(err) 390 | } else { 391 | log.Println("Image saved") 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /scout: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shell-Company/go-scout/dbcd03cc2189deb1a1ea9767025241220f21ae4f/scout --------------------------------------------------------------------------------