├── README.md └── gearVRC.py /README.md: -------------------------------------------------------------------------------- 1 | # gear-vr-controller-linux 2 | 3 | This little mapper creates a mouse + web navigator device from Samsung Gear VR Controller. 4 | My work was inspired by this project 5 | https://github.com/jsyang/gearvr-controller-webbluetooth 6 | 7 | # How To use? 8 | 1. $ pip3 install python-uinput pygatt gatt more-itertools --user 9 | 2. Change the bluetooth MAC address in the .py file to yours!!! 10 | 3. Pair Controller with (Linux) PC 11 | 4. Connect Controller 12 | 5. As root load uinput: # modprobe uinput 13 | 6. As root enable user access to uinput: # chmod 666 /dev/uinput 14 | 7. Run my program 15 | 8. Enjoy 16 | 17 | To avoid steps 4 and 5: 18 | 19 | As root do this once, a change YOURGROUP to the group of your user 20 | 21 | echo 'KERNEL=="uinput", MODE="0660", GROUP="YOURGROUP", OPTIONS+="static_node=uinput"' > /etc/udev/rules.d/99-uinput.rules 22 | 23 | 24 | # What's the usecase? a.k.a. Why? 25 | 26 | I am sitting at my desk while holding my litte son while he sleeps, but I sill need do nerdy stuff like browsing the web. An air mouse is not that convenient, but this controller ... is a perfect match. 27 | It could be considered as the one hand equivalent of the Steam Controller. 28 | BTW I hope sc-controller will intergate this controller in his project as well. 29 | 30 | # ISSUES 31 | Consider this project at this point as a Proof Of Concept. 32 | 33 | # Plans for the future 34 | I had the idea to implement the esp-GearVRController-Mouse-adapter, but I am stuck with it. 35 | ESP32 https://www.espressif.com/en/products/hardware/esp32/overview was my best choice. Really good piece of hardware, but handling the 128bit uuid notification event stucks by waiting for a semaphore in the readValue() ... 36 | 37 | NodeJS: as the original reverse engineering was done in Javascript and nodejs has mouse emulation libraries, this could work. 38 | 39 | Android-GearVRController-Mouse-adapter: 40 | https://github.com/rdady/BLE-HID-Peripheral-for-Android 41 | An Android phone could pretend to be a BLE mouse / joystick. 42 | Advantage is, no special hardware is needed and no special knowledge is needed for its programming, but installing an app. 43 | I am working on this one, wait for it :) 44 | 45 | -------------------------------------------------------------------------------- /gearVRC.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # The library is free. 4 | # MIT License 5 | # Copyright (c) 2019, Robert K. Dady 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | # Prerequisite: 11 | # $ pip3 install python-uinput pygatt gatt more-itertools cmath --user 12 | # # modprobe uinput 13 | 14 | import gatt 15 | import uinput 16 | import signal 17 | import math 18 | import time 19 | import numpy as np 20 | import sys 21 | import os 22 | import cmath 23 | import more_itertools as mit 24 | 25 | manager = gatt.DeviceManager(adapter_name='hci0') 26 | 27 | def ror(l, n): 28 | return l[-n:] + l[:-n] 29 | 30 | class AnyDevice(gatt.Device): 31 | def connect_succeeded(self): 32 | super().connect_succeeded() 33 | print("[%s] Connected" % (self.mac_address)) 34 | 35 | def connect_failed(self, error): 36 | super().connect_failed(error) 37 | print("[%s] Connection failed: %s" % (self.mac_address, str(error))) 38 | 39 | def disconnect_succeeded(self): 40 | super().disconnect_succeeded() 41 | print("[%s] Disconnected" % (self.mac_address)) 42 | sys.exit(0) 43 | 44 | def write(self, cmd, times): 45 | for i in range(times - 1): 46 | self.__setup_characteristic.write_value(cmd) 47 | 48 | def services_resolved(self): 49 | super().services_resolved() 50 | 51 | controller_data_service = next( 52 | s for s in self.services 53 | if s.uuid == '4f63756c-7573-2054-6872-65656d6f7465') 54 | 55 | controller_setup_data_characteristic = next( 56 | c for c in controller_data_service.characteristics 57 | if c.uuid == 'c8c51726-81bc-483b-a052-f7a14ea3d282') 58 | 59 | controller_data_characteristic = next( 60 | c for c in controller_data_service.characteristics 61 | if c.uuid == 'c8c51726-81bc-483b-a052-f7a14ea3d281') 62 | 63 | self.__setup_characteristic = controller_setup_data_characteristic 64 | self.__sensor_characteristic = controller_data_characteristic 65 | 66 | self.write(bytearray(b'\x01\x00'), 3) 67 | self.write(bytearray(b'\x06\x00'), 1) 68 | self.write(bytearray(b'\x07\x00'), 1) 69 | self.write(bytearray(b'\x08\x00'), 3) 70 | 71 | self.__max = 315 72 | self.__r = self.__max / 2 73 | self.__axisX = self.__axisY = 0 74 | self.__altX = self.__altY = 0 75 | self.__device = uinput.Device([uinput.REL_X, uinput.REL_Y, uinput.BTN_LEFT, uinput.BTN_RIGHT, uinput.KEY_LEFTCTRL, uinput.KEY_LEFTALT, uinput.KEY_HOME, uinput.KEY_UP, uinput.KEY_DOWN, uinput.KEY_LEFT, uinput.KEY_RIGHT, uinput.KEY_VOLUMEUP, uinput.KEY_VOLUMEDOWN, uinput.KEY_KPPLUS, uinput.KEY_KPMINUS, uinput.KEY_PAGEUP, uinput.KEY_PAGEDOWN, uinput.KEY_KP0, uinput.KEY_SCROLLDOWN, uinput.KEY_SCROLLUP ]) # , uinput.BTN_TOUCH, uinput.ABS_PRESSURE 76 | 77 | self.__reset = self.__volbtn = self.__tchbtn = self.__trig = True 78 | 79 | self.__time = round(time.time()) + 10 80 | self.__lastupdated = 0 81 | self.__updatecounts = 0 82 | self.__wheelPos = -1 83 | self.__useWheel = False 84 | self.__c_numberOfWheelPositions = 64 85 | [self.__l_top, self.__l_right, self.__l_bottom, self.__l_left] = [list(x) for x in mit.divide(4, ror([i for i in range(0, self.__c_numberOfWheelPositions)], self.__c_numberOfWheelPositions // 8))] 86 | self.__wheelMultiplier = 2 87 | self.__useTouch = False 88 | self.__dirUp = False 89 | self.__dirDown = False 90 | self.__VR = False 91 | 92 | controller_data_characteristic.enable_notifications() 93 | print("setup done") 94 | 95 | def keepalive(self): 96 | # test time and each minute send a keepalive 97 | if (time.time() > self.__time): 98 | self.__time = round(time.time()) + 10 99 | cmd = bytearray(b'\x04\x00') 100 | for i in range(4): 101 | self.__setup_characteristic.write_value(cmd) 102 | 103 | def characteristic_value_updated(self, characteristic, value): 104 | if (characteristic == self.__sensor_characteristic): 105 | if self.__VR == False: 106 | self.__updatecounts += 1 107 | if self.__updatecounts == 20: 108 | now = time.time() 109 | self.__updatecounts = 0 110 | deltatime = now - self.__lastupdated 111 | self.__lastupdated = now 112 | if deltatime > 0.23: 113 | self.write(bytearray(b'\x06\x00'), 1) 114 | self.write(bytearray(b'\x08\x00'), 3) 115 | self.write(bytearray(b'\x07\x00'), 1) 116 | else: 117 | print("VR mode enabled ", deltatime) 118 | self.__VR = True 119 | 120 | self.keepalive() 121 | int_values = [x for x in value] 122 | if (len(int_values) < 60): 123 | self.__VR = True 124 | print("VR mode is activated") 125 | self.write(bytearray(b'\x01\x00'), 3) 126 | self.__sensor_characteristic.enable_notifications() 127 | return 128 | 129 | axisX = (((int_values[54] & 0xF) << 6) + ((int_values[55] & 0xFC) >> 2)) & 0x3FF 130 | axisY = (((int_values[55] & 0x3) << 8) + ((int_values[56] & 0xFF) >> 0)) & 0x3FF 131 | accelX = np.uint16((int_values[4] << 8) + int_values[5]) * 10000.0 * 9.80665 / 2048.0 132 | accelY = np.uint16((int_values[6] << 8) + int_values[7]) * 10000.0 * 9.80665 / 2048.0 133 | accelZ = np.uint16((int_values[8] << 8) + int_values[9]) * 10000.0 * 9.80665 / 2048.0 134 | gyroX = np.uint16((int_values[10] << 8) + int_values[11]) * 10000.0 * 0.017453292 / 14.285 135 | gyroY = np.uint16((int_values[12] << 8) + int_values[13]) * 10000.0 * 0.017453292 / 14.285 136 | gyroZ = np.uint16((int_values[14] << 8) + int_values[15]) * 10000.0 * 0.017453292 / 14.285 137 | magnetX = np.uint16((int_values[32] << 8) + int_values[33]) * 0.06 138 | magnetY = np.uint16((int_values[34] << 8) + int_values[35]) * 0.06 139 | magnetZ = np.uint16((int_values[36] << 8) + int_values[37]) * 0.06 140 | 141 | triggerButton = True if ((int_values[58] & 1) == 1) else False 142 | homeButton = True if ((int_values[58] & 2) == 2) else False 143 | backButton = True if ((int_values[58] & 4) == 4) else False 144 | touchpadButton = True if ((int_values[58] & 8) == 8) else False 145 | volumeUpButton = True if ((int_values[58] & 16) == 16) else False 146 | volumeDownButton = True if ((int_values[58] & 32) == 32) else False 147 | NoButton = True if ((int_values[58] & 64) == 64) else False 148 | 149 | idelta = 30 150 | odelta = 25 151 | 152 | if (touchpadButton == True and self.__trig == True): 153 | self.__useWheel = not self.__useWheel 154 | #self.__useTouch = not self.__useTouch 155 | self.__trig = False 156 | elif (touchpadButton == False and self.__trig == False): 157 | self.__trig = True 158 | 159 | outerCircle = True if (axisX - self.__r)**2 + (axisY - self.__r)**2 > (self.__r - odelta)**2 else False 160 | wheelPos = self.wheelPos(axisX, axisY) 161 | T = True if (outerCircle and int(wheelPos) in self.__l_top) else False # Top 162 | R = True if (outerCircle and int(wheelPos) in self.__l_right) else False # Right 163 | B = True if (outerCircle and int(wheelPos) in self.__l_bottom) else False # Bottom 164 | L = True if (outerCircle and int(wheelPos) in self.__l_left) else False # Left 165 | 166 | delta_X = delta_Y = 0 167 | delta_X = axisX - self.__axisX 168 | delta_Y = axisY - self.__axisY 169 | delta_X = round(delta_X * 1.2) 170 | delta_Y = round(delta_Y * 1.2) 171 | 172 | if (self.__useWheel): 173 | if (abs(self.__wheelPos - wheelPos) > 1 and abs((self.__wheelPos + 1) % self.__c_numberOfWheelPositions - (wheelPos + 1) % self.__c_numberOfWheelPositions) > 1): 174 | self.__wheelPos = wheelPos 175 | return 176 | if ((self.__wheelPos - wheelPos) == 1 or ((self.__wheelPos + 1) % self.__c_numberOfWheelPositions - (wheelPos + 1) % self.__c_numberOfWheelPositions) == 1): 177 | self.__wheelPos = wheelPos 178 | for i in range(self.__wheelMultiplier): 179 | self.__device.emit(uinput.KEY_UP, 1) 180 | self.__device.emit(uinput.KEY_UP, 0) 181 | return 182 | if ((wheelPos - self.__wheelPos) == 1 or ((wheelPos + 1) % self.__c_numberOfWheelPositions - (self.__wheelPos + 1) % self.__c_numberOfWheelPositions) == 1): 183 | self.__wheelPos = wheelPos 184 | for i in range(self.__wheelMultiplier): 185 | self.__device.emit(uinput.KEY_DOWN, 1) 186 | self.__device.emit(uinput.KEY_DOWN, 0) 187 | return 188 | return 189 | 190 | if (self.__useTouch): 191 | if (abs(delta_X) < 50): 192 | if (axisX == 0 and axisY == 0): 193 | self.__dirUp = False 194 | self.__dirDown = False 195 | self.__axisX = axisX 196 | self.__axisY = axisY 197 | return 198 | elif (self.__dirUp == False and self.__dirDown == False): 199 | if (delta_X > 0): 200 | self.__dirUp = True 201 | else: 202 | self.__dirDown = True 203 | if (self.__dirUp == True and abs(delta_X) > 1): 204 | self.__device.emit(uinput.KEY_UP, 1) 205 | self.__device.emit(uinput.KEY_UP, 0) 206 | elif (self.__dirDown == True and abs(delta_X) > 1): 207 | self.__device.emit(uinput.KEY_DOWN, 1) 208 | self.__device.emit(uinput.KEY_DOWN, 0) 209 | self.__axisX = axisX 210 | self.__axisY = axisY 211 | print(delta_X) 212 | return 213 | 214 | if (triggerButton == True): 215 | self.__device.emit(uinput.BTN_LEFT, 1) 216 | else: 217 | self.__device.emit(uinput.BTN_LEFT, 0) 218 | 219 | if (homeButton == True and self.__volbtn == True): 220 | self.__device.emit(uinput.KEY_LEFTALT, 1) 221 | self.__device.emit(uinput.KEY_HOME, 1) 222 | self.__device.emit(uinput.KEY_HOME, 0) 223 | self.__device.emit(uinput.KEY_LEFTALT, 0) 224 | self.__volbtn = False 225 | return 226 | 227 | if (backButton == True and self.__volbtn == True): 228 | self.__device.emit(uinput.KEY_LEFTALT, 1) 229 | self.__device.emit(uinput.KEY_LEFT, 1) 230 | self.__device.emit(uinput.KEY_LEFT, 0) 231 | self.__device.emit(uinput.KEY_LEFTALT, 0) 232 | self.__volbtn = False 233 | return 234 | 235 | if (volumeDownButton == True and self.__volbtn == True): 236 | self.__device.emit(uinput.KEY_LEFTCTRL, 1, syn = False) 237 | self.__device.emit(uinput.KEY_KP0, 1, syn = True) 238 | self.__device.emit(uinput.KEY_KP0, 0, syn = False) 239 | self.__device.emit(uinput.KEY_LEFTCTRL, 0, syn = True) 240 | self.__volbtn = False 241 | return 242 | 243 | if (volumeUpButton == True and self.__volbtn == True): 244 | self.__volbtn = False 245 | self.__device.emit(uinput.KEY_LEFTCTRL, 1, syn = False) 246 | self.__device.emit(uinput.KEY_KPPLUS, 1, syn = True) 247 | self.__device.emit(uinput.KEY_KPPLUS, 0, syn = False) 248 | self.__device.emit(uinput.KEY_LEFTCTRL, 0, syn = True) 249 | self.__volbtn = False 250 | return 251 | 252 | if (NoButton == True): 253 | self.__volbtn = True 254 | self.__tchbtn = True 255 | self.__trig = True 256 | self.__device.emit(uinput.BTN_LEFT, 0) 257 | self.__device.emit(uinput.KEY_PAGEUP, 0) 258 | self.__device.emit(uinput.KEY_PAGEDOWN, 0) 259 | self.__device.emit(uinput.KEY_UP, 0) 260 | self.__device.emit(uinput.KEY_DOWN, 0) 261 | self.__device.emit(uinput.KEY_LEFT, 0) 262 | self.__device.emit(uinput.KEY_RIGHT, 0) 263 | 264 | # No standalone button handling behind this point 265 | 266 | if (axisX == 0 and axisY == 0): 267 | self.__reset = True 268 | return 269 | 270 | if (self.__reset == True): 271 | self.__reset = False 272 | self.__axisX = axisX 273 | self.__axisY = axisY 274 | self.__altX = gyroX 275 | self.__altY = gyroY 276 | return 277 | 278 | self.movePointerREL(delta_X, delta_Y) 279 | 280 | self.__axisX = axisX 281 | self.__axisY = axisY 282 | self.__altX = gyroX 283 | self.__altY = gyroY 284 | else: 285 | print("got somethig else ", characteristic, " ", len(value)) 286 | 287 | def movePointerREL(self, dx, dy): 288 | incx = 0 if dx == 0 else round((0 - dx)/abs(dx)) 289 | incy = 0 if dy == 0 else round((0 - dy)/abs(dy)) 290 | 291 | while (dx != 0 or dy != 0): 292 | if (dx != 0): 293 | self.__device.emit(uinput.REL_X, -incx, syn = True) 294 | dx += incx 295 | if (dy != 0): 296 | self.__device.emit(uinput.REL_Y, -incy, syn = True) 297 | dy += incy 298 | 299 | # circle segments from 0 .. self.__c_numberOfWheelPositions clockwise 300 | def wheelPos(self, x, y): 301 | pos = 0 302 | if (x == 0 and y == 0): 303 | pos = -1 304 | r, phi = cmath.polar(complex(x-157, y-157)) 305 | pos = math.floor(math.degrees(phi) / 360 * self.__c_numberOfWheelPositions) 306 | return pos 307 | 308 | def defint(): 309 | global device 310 | device.write(bytearray(b'\x00\x00'), 3) 311 | ##device.disconnect() 312 | sys.exit(0) 313 | 314 | signal.signal(signal.SIGINT, lambda x,y: defint()) 315 | 316 | print("Samsung Gear VR Controller mapper running ...") 317 | print("Press Ctrl+C to terminate") 318 | 319 | device = AnyDevice(mac_address='2C:BA:BA:25:6A:A1', manager=manager) 320 | device.connect() 321 | 322 | manager.run() 323 | --------------------------------------------------------------------------------