├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── LICENSE ├── README.md ├── pyjoycon ├── __init__.py ├── constants.py ├── device.py ├── event.py ├── gyro.py ├── joycon.py └── wrappers.py ├── requirements.txt └── setup.py /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | run: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | os: [macos-latest, ubuntu-latest, windows-latest] 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup Python 15 | uses: actions/setup-python@v1 16 | 17 | - name: Install the package 18 | run: pip install . 19 | 20 | - name: Lint with flake8 21 | run: | 22 | pip install flake8 23 | flake8 . --extend-ignore=E122,E201,E221,E203,E501 --show-source --statistics 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # environment notebooks 2 | *.ipynb 3 | *.ipynb_checkpoints 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tocoteron/joycon-python/b86bf330873e27c442479a3ccb832529aac5af79/LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![logo](https://i.gyazo.com/af04cc6000f2815ebc00d4dcf06b1eb9.png) 2 | 3 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/joycon-python) 4 | ![PyPI - Implementation](https://img.shields.io/pypi/implementation/joycon-python) 5 | 6 | Python driver for Nintendo Switch Joy-Con 7 | 8 | We are referring to [dekuNukem/Nintendo_Switch_Reverse_Engineering](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering) 9 | 10 | ## Install 11 | 12 | ```shell 13 | pip install joycon-python hidapi pyglm 14 | ``` 15 | 16 | ## Usage 17 | 18 | Quick status check 19 | 20 | ```shell 21 | cd joycon-python/ 22 | python -m pyjoycon.joycon 23 | ``` 24 | 25 | or use as module 26 | 27 | ```python 28 | from pyjoycon import JoyCon, get_R_id 29 | 30 | joycon_id = get_R_id() 31 | joycon = JoyCon(*joycon_id) 32 | 33 | joycon.get_status() 34 | ``` 35 | 36 | ## Status values 37 | 38 | ```python 39 | { 40 | 'battery': { 41 | 'charging': 0, 42 | 'level': 2 43 | }, 44 | 'buttons': { 45 | 'right': { 46 | 'y': 0, 47 | 'x': 0, 48 | 'b': 0, 49 | 'a': 0, 50 | 'sr': 0, 51 | 'sl': 0, 52 | 'r': 0, 53 | 'zr': 0 54 | }, 55 | 'shared': { 56 | 'minus': 0, 57 | 'plus': 0, 58 | 'r-stick': 0, 59 | 'l-stick': 0, 60 | 'home': 0, 61 | 'capture': 0, 62 | 'charging-grip': 0 63 | }, 64 | 'left': { 65 | 'down': 0, 66 | 'up': 0, 67 | 'right': 0, 68 | 'left': 0, 69 | 'sr': 0, 70 | 'sl': 0, 71 | 'l': 0, 72 | 'zl': 0 73 | } 74 | }, 75 | 'analog-sticks': { 76 | 'left': { 77 | 'horizontal': 0, 78 | 'vertical': 0 79 | }, 80 | 'right': { 81 | 'horizontal': 2170, 82 | 'vertical': 1644 83 | } 84 | }, 85 | 'accel': { 86 | 'x': 879, 87 | 'y': 1272, 88 | 'z': 549 89 | }, 90 | 'gyro': { 91 | 'x': -354, 92 | 'y': -7, 93 | 'z': 281 94 | } 95 | } 96 | 97 | ``` 98 | 99 | You need `cython-hidapi` to use Bluetooth / HID connection in Python. 100 | 101 | Alternatively, you can use `hid` instead if `cython-hidapi` fails to find your JoyCons. 102 | 103 | If you are on Linux you most likely will need to add [udev rules](https://wiki.debian.org/udev) for switch devices to make it work. [These rules](https://www.reddit.com/r/Stadia/comments/egcvpq/using_nintendo_switch_pro_controller_on_linux/fc5s7qm/) will work just fine. 104 | 105 | 106 | ## Gyroscope 107 | 108 | We have a specialized class which tracks the gyroscope for you, and 109 | exposes this tracked state in a simplified manner: 110 | 111 | ```python 112 | from pyjoycon import GyroTrackingJoyCon, get_R_id 113 | import time 114 | 115 | joycon_id = get_R_id() 116 | joycon = GyroTrackingJoyCon(*joycon_id) 117 | for i in range(20): 118 | print("joycon pointer: ", joycon.pointer) 119 | print("joycon rotation: ", joycon.rotation) 120 | print("joycon direction:", joycon.direction) 121 | print() 122 | time.sleep(0.05) 123 | ``` 124 | 125 | 126 | ## Button events 127 | 128 | We have a specialized class which tracks the state of the JoyCon buttons and 129 | provides changes as events. Here is an example of how it could be used with `pygame`: 130 | 131 | ```python 132 | from pyjoycon import ButtonEventJoyCon, get_R_id 133 | import pygame 134 | 135 | joycon_id = get_R_id() 136 | joycon = ButtonEventJoyCon(*joycon_id) 137 | 138 | ... 139 | 140 | while 1: 141 | pygame.time.wait(int(1000/60)) 142 | 143 | ... 144 | 145 | for event_type, status in joycon.events(): 146 | print(event_type, status) 147 | 148 | ... 149 | 150 | pygame.display.flip() 151 | ``` 152 | 153 | 154 | ## Combining multiple JoyCon helper classes 155 | 156 | ```python 157 | import pyjoycon 158 | 159 | class MyJoyCon( 160 | pyjoycon.GyroTrackingJoyCon, 161 | pyjoycon.ButtonEventJoyCon, 162 | ): pass 163 | ``` 164 | 165 | 166 | ## Environments 167 | 168 | - macOS Mojave (10.14.6) 169 | - Python (3.7.4) 170 | - hidapi (0.7.99.post21) 171 | -------------------------------------------------------------------------------- /pyjoycon/__init__.py: -------------------------------------------------------------------------------- 1 | from .joycon import JoyCon 2 | from .wrappers import PythonicJoyCon # as JoyCon 3 | from .gyro import GyroTrackingJoyCon 4 | from .event import ButtonEventJoyCon 5 | from .device import get_device_ids, get_ids_of_type 6 | from .device import is_id_L 7 | from .device import get_R_ids, get_L_ids 8 | from .device import get_R_id, get_L_id 9 | 10 | 11 | __version__ = "0.2.4" 12 | 13 | __all__ = [ 14 | "ButtonEventJoyCon", 15 | "GyroTrackingJoyCon", 16 | "JoyCon", 17 | "PythonicJoyCon", 18 | "get_L_id", 19 | "get_L_ids", 20 | "get_R_id", 21 | "get_R_ids", 22 | "get_device_ids", 23 | "get_ids_of_type", 24 | "is_id_L", 25 | ] 26 | -------------------------------------------------------------------------------- /pyjoycon/constants.py: -------------------------------------------------------------------------------- 1 | JOYCON_VENDOR_ID = 0x057E 2 | JOYCON_L_PRODUCT_ID = 0x2006 3 | JOYCON_R_PRODUCT_ID = 0x2007 4 | JOYCON_PRODUCT_IDS = (JOYCON_L_PRODUCT_ID, JOYCON_R_PRODUCT_ID) 5 | -------------------------------------------------------------------------------- /pyjoycon/device.py: -------------------------------------------------------------------------------- 1 | import hid 2 | from .constants import JOYCON_VENDOR_ID, JOYCON_PRODUCT_IDS 3 | from .constants import JOYCON_L_PRODUCT_ID, JOYCON_R_PRODUCT_ID 4 | 5 | 6 | def get_device_ids(debug=False): 7 | """ 8 | returns a list of tuples like `(vendor_id, product_id, serial_number)` 9 | """ 10 | devices = hid.enumerate(0, 0) 11 | 12 | out = [] 13 | for device in devices: 14 | vendor_id = device["vendor_id"] 15 | product_id = device["product_id"] 16 | product_string = device["product_string"] 17 | serial = device.get('serial') or device.get("serial_number") 18 | 19 | if vendor_id != JOYCON_VENDOR_ID: 20 | continue 21 | if product_id not in JOYCON_PRODUCT_IDS: 22 | continue 23 | if not product_string: 24 | continue 25 | 26 | out.append((vendor_id, product_id, serial)) 27 | 28 | if debug: 29 | print(product_string) 30 | print(f"\tvendor_id is {vendor_id!r}") 31 | print(f"\tproduct_id is {product_id!r}") 32 | print(f"\tserial is {serial!r}") 33 | 34 | return out 35 | 36 | 37 | def is_id_L(id): 38 | return id[1] == JOYCON_L_PRODUCT_ID 39 | 40 | 41 | def get_ids_of_type(lr, **kw): 42 | """ 43 | returns a list of tuples like `(vendor_id, product_id, serial_number)` 44 | 45 | arg: lr : str : put `R` or `L` 46 | """ 47 | if lr.lower() == "l": 48 | product_id = JOYCON_L_PRODUCT_ID 49 | else: 50 | product_id = JOYCON_R_PRODUCT_ID 51 | return [i for i in get_device_ids(**kw) if i[1] == product_id] 52 | 53 | 54 | def get_R_ids(**kw): 55 | """returns a list of tuple like `(vendor_id, product_id, serial_number)`""" 56 | return get_ids_of_type("R", **kw) 57 | 58 | 59 | def get_L_ids(**kw): 60 | """returns a list of tuple like `(vendor_id, product_id, serial_number)`""" 61 | return get_ids_of_type("L", **kw) 62 | 63 | 64 | def get_R_id(**kw): 65 | """returns a tuple like `(vendor_id, product_id, serial_number)`""" 66 | ids = get_R_ids(**kw) 67 | if not ids: 68 | return (None, None, None) 69 | return ids[0] 70 | 71 | 72 | def get_L_id(**kw): 73 | """returns a tuple like `(vendor_id, product_id, serial_number)`""" 74 | ids = get_L_ids(**kw) 75 | if not ids: 76 | return (None, None, None) 77 | return ids[0] 78 | -------------------------------------------------------------------------------- /pyjoycon/event.py: -------------------------------------------------------------------------------- 1 | from .wrappers import PythonicJoyCon 2 | 3 | 4 | class ButtonEventJoyCon(PythonicJoyCon): 5 | def __init__(self, *args, track_sticks=False, **kwargs): 6 | super().__init__(*args, **kwargs) 7 | 8 | self._events_buffer = [] # TODO: perhaps use a deque instead? 9 | 10 | self._event_handlers = {} 11 | self._event_track_sticks = track_sticks 12 | 13 | self._previous_stick_l_btn = 0 14 | self._previous_stick_r_btn = 0 15 | self._previous_stick_r = self._previous_stick_l = (0, 0) 16 | self._previous_r = self._previous_l = 0 17 | self._previous_zr = self._previous_zl = 0 18 | self._previous_plus = self._previous_minus = 0 19 | self._previous_a = self._previous_right = 0 20 | self._previous_b = self._previous_down = 0 21 | self._previous_x = self._previous_up = 0 22 | self._previous_y = self._previous_left = 0 23 | self._previous_home = self._previous_capture = 0 24 | self._previous_right_sr = self._previous_left_sr = 0 25 | self._previous_right_sl = self._previous_left_sl = 0 26 | 27 | if self.is_left(): 28 | self.register_update_hook(self._event_tracking_update_hook_left) 29 | else: 30 | self.register_update_hook(self._event_tracking_update_hook_right) 31 | 32 | def joycon_button_event(self, button, state): # overridable 33 | self._events_buffer.append((button, state)) 34 | 35 | def events(self): 36 | while self._events_buffer: 37 | yield self._events_buffer.pop(0) 38 | 39 | @staticmethod 40 | def _event_tracking_update_hook_right(self): 41 | if self._event_track_sticks: 42 | pressed = self.stick_r_btn 43 | if self._previous_stick_r_btn != pressed: 44 | self._previous_stick_r_btn = pressed 45 | self.joycon_button_event("stick_r_btn", pressed) 46 | pressed = self.r 47 | if self._previous_r != pressed: 48 | self._previous_r = pressed 49 | self.joycon_button_event("r", pressed) 50 | pressed = self.zr 51 | if self._previous_zr != pressed: 52 | self._previous_zr = pressed 53 | self.joycon_button_event("zr", pressed) 54 | pressed = self.plus 55 | if self._previous_plus != pressed: 56 | self._previous_plus = pressed 57 | self.joycon_button_event("plus", pressed) 58 | pressed = self.a 59 | if self._previous_a != pressed: 60 | self._previous_a = pressed 61 | self.joycon_button_event("a", pressed) 62 | pressed = self.b 63 | if self._previous_b != pressed: 64 | self._previous_b = pressed 65 | self.joycon_button_event("b", pressed) 66 | pressed = self.x 67 | if self._previous_x != pressed: 68 | self._previous_x = pressed 69 | self.joycon_button_event("x", pressed) 70 | pressed = self.y 71 | if self._previous_y != pressed: 72 | self._previous_y = pressed 73 | self.joycon_button_event("y", pressed) 74 | pressed = self.home 75 | if self._previous_home != pressed: 76 | self._previous_home = pressed 77 | self.joycon_button_event("home", pressed) 78 | pressed = self.right_sr 79 | if self._previous_right_sr != pressed: 80 | self._previous_right_sr = pressed 81 | self.joycon_button_event("right_sr", pressed) 82 | pressed = self.right_sl 83 | if self._previous_right_sl != pressed: 84 | self._previous_right_sl = pressed 85 | self.joycon_button_event("right_sl", pressed) 86 | 87 | @staticmethod 88 | def _event_tracking_update_hook_left(self): 89 | if self._event_track_sticks: 90 | pressed = self.stick_l_btn 91 | if self._previous_stick_l_btn != pressed: 92 | self._previous_stick_l_btn = pressed 93 | self.joycon_button_event("stick_l_btn", pressed) 94 | pressed = self.l 95 | if self._previous_l != pressed: 96 | self._previous_l = pressed 97 | self.joycon_button_event("l", pressed) 98 | pressed = self.zl 99 | if self._previous_zl != pressed: 100 | self._previous_zl = pressed 101 | self.joycon_button_event("zl", pressed) 102 | pressed = self.minus 103 | if self._previous_minus != pressed: 104 | self._previous_minus = pressed 105 | self.joycon_button_event("minus", pressed) 106 | pressed = self.up 107 | if self._previous_up != pressed: 108 | self._previous_up = pressed 109 | self.joycon_button_event("up", pressed) 110 | pressed = self.down 111 | if self._previous_down != pressed: 112 | self._previous_down = pressed 113 | self.joycon_button_event("down", pressed) 114 | pressed = self.left 115 | if self._previous_left != pressed: 116 | self._previous_left = pressed 117 | self.joycon_button_event("left", pressed) 118 | pressed = self.right 119 | if self._previous_right != pressed: 120 | self._previous_right = pressed 121 | self.joycon_button_event("right", pressed) 122 | pressed = self.capture 123 | if self._previous_capture != pressed: 124 | self._previous_capture = pressed 125 | self.joycon_button_event("capture", pressed) 126 | pressed = self.left_sr 127 | if self._previous_left_sr != pressed: 128 | self._previous_left_sr = pressed 129 | self.joycon_button_event("left_sr", pressed) 130 | pressed = self.left_sl 131 | if self._previous_left_sl != pressed: 132 | self._previous_left_sl = pressed 133 | self.joycon_button_event("left_sl", pressed) 134 | -------------------------------------------------------------------------------- /pyjoycon/gyro.py: -------------------------------------------------------------------------------- 1 | from .wrappers import PythonicJoyCon 2 | from glm import vec2, vec3, quat, angleAxis, eulerAngles 3 | from typing import Optional 4 | import time 5 | 6 | 7 | class GyroTrackingJoyCon(PythonicJoyCon): 8 | """ 9 | A specialized class based on PythonicJoyCon which tracks the gyroscope data 10 | and deduces the current rotation of the JoyCon. Can be used to create a 11 | pointer rotate an object or pointin a direction. Comes with the need to be 12 | calibrated. 13 | """ 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, simple_mode=False, **kwargs) 16 | 17 | # set internal state: 18 | self.reset_orientation() 19 | 20 | # register the update callback 21 | self.register_update_hook(self._gyro_update_hook) 22 | 23 | @property 24 | def pointer(self) -> Optional[vec2]: 25 | d = self.direction 26 | if d.x <= 0: 27 | return None 28 | return vec2(d.y, -d.z) / d.x 29 | 30 | @property 31 | def direction(self) -> vec3: 32 | return self.direction_X 33 | 34 | @property 35 | def rotation(self) -> vec3: 36 | return -eulerAngles(self.direction_Q) 37 | 38 | is_calibrating = False 39 | 40 | def calibrate(self, seconds=2): 41 | self.calibration_acumulator = vec3(0) 42 | self.calibration_acumulations = 0 43 | self.is_calibrating = time.time() + seconds 44 | 45 | def _set_calibration(self, gyro_offset=None): 46 | if not gyro_offset: 47 | c = vec3(1, self._ime_yz_coeff, self._ime_yz_coeff) 48 | gyro_offset = self.calibration_acumulator * c 49 | gyro_offset /= self.calibration_acumulations 50 | gyro_offset += vec3( 51 | self._GYRO_OFFSET_X, 52 | self._GYRO_OFFSET_Y, 53 | self._GYRO_OFFSET_Z, 54 | ) 55 | self.is_calibrating = False 56 | self.set_gyro_calibration(gyro_offset) 57 | 58 | def reset_orientation(self): 59 | self.direction_X = vec3(1, 0, 0) 60 | self.direction_Y = vec3(0, 1, 0) 61 | self.direction_Z = vec3(0, 0, 1) 62 | self.direction_Q = quat() 63 | 64 | @staticmethod 65 | def _gyro_update_hook(self): 66 | if self.is_calibrating: 67 | if self.is_calibrating < time.time(): 68 | self._set_calibration() 69 | else: 70 | for xyz in self.gyro: 71 | self.calibration_acumulator += xyz 72 | self.calibration_acumulations += 3 73 | 74 | for gx, gy, gz in self.gyro_in_rad: 75 | # TODO: find out why 1/86 works, and not 1/60 or 1/(60*30) 76 | rotation \ 77 | = angleAxis(gx * (-1/86), self.direction_X) \ 78 | * angleAxis(gy * (-1/86), self.direction_Y) \ 79 | * angleAxis(gz * (-1/86), self.direction_Z) 80 | 81 | self.direction_X *= rotation 82 | self.direction_Y *= rotation 83 | self.direction_Z *= rotation 84 | self.direction_Q *= rotation 85 | -------------------------------------------------------------------------------- /pyjoycon/joycon.py: -------------------------------------------------------------------------------- 1 | from .constants import JOYCON_VENDOR_ID, JOYCON_PRODUCT_IDS 2 | from .constants import JOYCON_L_PRODUCT_ID, JOYCON_R_PRODUCT_ID 3 | import hid 4 | import time 5 | import threading 6 | from typing import Optional 7 | 8 | # TODO: disconnect, power off sequence 9 | 10 | 11 | class JoyCon: 12 | _INPUT_REPORT_SIZE = 49 13 | _INPUT_REPORT_PERIOD = 0.015 14 | _RUMBLE_DATA = b'\x00\x01\x40\x40\x00\x01\x40\x40' 15 | 16 | vendor_id : int 17 | product_id : int 18 | serial : Optional[str] 19 | simple_mode: bool 20 | color_body : (int, int, int) 21 | color_btn : (int, int, int) 22 | 23 | def __init__(self, vendor_id: int, product_id: int, serial: str = None, simple_mode=False): 24 | if vendor_id != JOYCON_VENDOR_ID: 25 | raise ValueError(f'vendor_id is invalid: {vendor_id!r}') 26 | 27 | if product_id not in JOYCON_PRODUCT_IDS: 28 | raise ValueError(f'product_id is invalid: {product_id!r}') 29 | 30 | self.vendor_id = vendor_id 31 | self.product_id = product_id 32 | self.serial = serial 33 | self.simple_mode = simple_mode # TODO: It's for reporting mode 0x3f 34 | 35 | # setup internal state 36 | self._input_hooks = [] 37 | self._input_report = bytes(self._INPUT_REPORT_SIZE) 38 | self._packet_number = 0 39 | self.set_accel_calibration((0, 0, 0), (1, 1, 1)) 40 | self.set_gyro_calibration((0, 0, 0), (1, 1, 1)) 41 | 42 | # connect to joycon 43 | self._joycon_device = self._open(vendor_id, product_id, serial=None) 44 | self._read_joycon_data() 45 | self._setup_sensors() 46 | 47 | # start talking with the joycon in a daemon thread 48 | self._update_input_report_thread \ 49 | = threading.Thread(target=self._update_input_report) 50 | self._update_input_report_thread.setDaemon(True) 51 | self._update_input_report_thread.start() 52 | 53 | def _open(self, vendor_id, product_id, serial): 54 | try: 55 | if hasattr(hid, "device"): # hidapi 56 | _joycon_device = hid.device() 57 | _joycon_device.open(vendor_id, product_id, serial) 58 | elif hasattr(hid, "Device"): # hid 59 | _joycon_device = hid.Device(vendor_id, product_id, serial) 60 | else: 61 | raise Exception("Implementation of hid is not recognized!") 62 | except IOError as e: 63 | raise IOError('joycon connect failed') from e 64 | return _joycon_device 65 | 66 | def _close(self): 67 | if hasattr(self, "_joycon_device"): 68 | self._joycon_device.close() 69 | del self._joycon_device 70 | 71 | def _read_input_report(self) -> bytes: 72 | return bytes(self._joycon_device.read(self._INPUT_REPORT_SIZE)) 73 | 74 | def _write_output_report(self, command, subcommand, argument): 75 | # TODO: add documentation 76 | self._joycon_device.write(b''.join([ 77 | command, 78 | self._packet_number.to_bytes(1, byteorder='little'), 79 | self._RUMBLE_DATA, 80 | subcommand, 81 | argument, 82 | ])) 83 | self._packet_number = (self._packet_number + 1) & 0xF 84 | 85 | def _send_subcmd_get_response(self, subcommand, argument) -> (bool, bytes): 86 | # TODO: handle subcmd when daemon is running 87 | self._write_output_report(b'\x01', subcommand, argument) 88 | 89 | report = self._read_input_report() 90 | while report[0] != 0x21: # TODO, avoid this, await daemon instead 91 | report = self._read_input_report() 92 | 93 | # TODO, remove, see the todo above 94 | assert report[1:2] != subcommand, "THREAD carefully" 95 | 96 | # TODO: determine if the cut bytes are worth anything 97 | 98 | return report[13] & 0x80, report[13:] # (ack, data) 99 | 100 | def _spi_flash_read(self, address, size) -> bytes: 101 | assert size <= 0x1d 102 | argument = address.to_bytes(4, "little") + size.to_bytes(1, "little") 103 | ack, report = self._send_subcmd_get_response(b'\x10', argument) 104 | if not ack: 105 | raise IOError("After SPI read @ {address:#06x}: got NACK") 106 | 107 | if report[:2] != b'\x90\x10': 108 | raise IOError("Something else than the expected ACK was recieved!") 109 | assert report[2:7] == argument, (report[2:5], argument) 110 | 111 | return report[7:size+7] 112 | 113 | def _update_input_report(self): # daemon thread 114 | while True: 115 | report = self._read_input_report() 116 | # TODO, handle input reports of type 0x21 and 0x3f 117 | while report[0] != 0x30: 118 | report = self._read_input_report() 119 | 120 | self._input_report = report 121 | 122 | for callback in self._input_hooks: 123 | callback(self) 124 | 125 | def _read_joycon_data(self): 126 | color_data = self._spi_flash_read(0x6050, 6) 127 | 128 | # TODO: use this 129 | # stick_cal_addr = 0x8012 if self.is_left else 0x801D 130 | # stick_cal = self._spi_flash_read(stick_cal_addr, 8) 131 | 132 | # user IME data 133 | if self._spi_flash_read(0x8026, 2) == b"\xB2\xA1": 134 | # print(f"Calibrate {self.serial} IME with user data") 135 | imu_cal = self._spi_flash_read(0x8028, 24) 136 | 137 | # factory IME data 138 | else: 139 | # print(f"Calibrate {self.serial} IME with factory data") 140 | imu_cal = self._spi_flash_read(0x6020, 24) 141 | 142 | self.color_body = tuple(color_data[:3]) 143 | self.color_btn = tuple(color_data[3:]) 144 | 145 | self.set_accel_calibration(( 146 | self._to_int16le_from_2bytes(imu_cal[ 0], imu_cal[ 1]), 147 | self._to_int16le_from_2bytes(imu_cal[ 2], imu_cal[ 3]), 148 | self._to_int16le_from_2bytes(imu_cal[ 4], imu_cal[ 5]), 149 | ), ( 150 | self._to_int16le_from_2bytes(imu_cal[ 6], imu_cal[ 7]), 151 | self._to_int16le_from_2bytes(imu_cal[ 8], imu_cal[ 9]), 152 | self._to_int16le_from_2bytes(imu_cal[10], imu_cal[11]), 153 | ) 154 | ) 155 | self.set_gyro_calibration(( 156 | self._to_int16le_from_2bytes(imu_cal[12], imu_cal[13]), 157 | self._to_int16le_from_2bytes(imu_cal[14], imu_cal[15]), 158 | self._to_int16le_from_2bytes(imu_cal[16], imu_cal[17]), 159 | ), ( 160 | self._to_int16le_from_2bytes(imu_cal[18], imu_cal[19]), 161 | self._to_int16le_from_2bytes(imu_cal[20], imu_cal[21]), 162 | self._to_int16le_from_2bytes(imu_cal[22], imu_cal[23]), 163 | ) 164 | ) 165 | 166 | def _setup_sensors(self): 167 | # Enable 6 axis sensors 168 | self._write_output_report(b'\x01', b'\x40', b'\x01') 169 | # It needs delta time to update the setting 170 | time.sleep(0.02) 171 | # Change format of input report 172 | self._write_output_report(b'\x01', b'\x03', b'\x30') 173 | 174 | @staticmethod 175 | def _to_int16le_from_2bytes(hbytebe, lbytebe): 176 | uint16le = (lbytebe << 8) | hbytebe 177 | int16le = uint16le if uint16le < 32768 else uint16le - 65536 178 | return int16le 179 | 180 | def _get_nbit_from_input_report(self, offset_byte, offset_bit, nbit): 181 | byte = self._input_report[offset_byte] 182 | return (byte >> offset_bit) & ((1 << nbit) - 1) 183 | 184 | def __del__(self): 185 | self._close() 186 | 187 | def set_gyro_calibration(self, offset_xyz=None, coeff_xyz=None): 188 | if offset_xyz: 189 | self._GYRO_OFFSET_X, \ 190 | self._GYRO_OFFSET_Y, \ 191 | self._GYRO_OFFSET_Z = offset_xyz 192 | if coeff_xyz: 193 | cx, cy, cz = coeff_xyz 194 | self._GYRO_COEFF_X = 0x343b / cx if cx != 0x343b else 1 195 | self._GYRO_COEFF_Y = 0x343b / cy if cy != 0x343b else 1 196 | self._GYRO_COEFF_Z = 0x343b / cz if cz != 0x343b else 1 197 | 198 | def set_accel_calibration(self, offset_xyz=None, coeff_xyz=None): 199 | if offset_xyz: 200 | self._ACCEL_OFFSET_X, \ 201 | self._ACCEL_OFFSET_Y, \ 202 | self._ACCEL_OFFSET_Z = offset_xyz 203 | if coeff_xyz: 204 | cx, cy, cz = coeff_xyz 205 | self._ACCEL_COEFF_X = 0x4000 / cx if cx != 0x4000 else 1 206 | self._ACCEL_COEFF_Y = 0x4000 / cy if cy != 0x4000 else 1 207 | self._ACCEL_COEFF_Z = 0x4000 / cz if cz != 0x4000 else 1 208 | 209 | def register_update_hook(self, callback): 210 | self._input_hooks.append(callback) 211 | return callback # this makes it so you could use it as a decorator 212 | 213 | def is_left(self): 214 | return self.product_id == JOYCON_L_PRODUCT_ID 215 | 216 | def is_right(self): 217 | return self.product_id == JOYCON_R_PRODUCT_ID 218 | 219 | def get_battery_charging(self): 220 | return self._get_nbit_from_input_report(2, 4, 1) 221 | 222 | def get_battery_level(self): 223 | return self._get_nbit_from_input_report(2, 5, 3) 224 | 225 | def get_button_y(self): 226 | return self._get_nbit_from_input_report(3, 0, 1) 227 | 228 | def get_button_x(self): 229 | return self._get_nbit_from_input_report(3, 1, 1) 230 | 231 | def get_button_b(self): 232 | return self._get_nbit_from_input_report(3, 2, 1) 233 | 234 | def get_button_a(self): 235 | return self._get_nbit_from_input_report(3, 3, 1) 236 | 237 | def get_button_right_sr(self): 238 | return self._get_nbit_from_input_report(3, 4, 1) 239 | 240 | def get_button_right_sl(self): 241 | return self._get_nbit_from_input_report(3, 5, 1) 242 | 243 | def get_button_r(self): 244 | return self._get_nbit_from_input_report(3, 6, 1) 245 | 246 | def get_button_zr(self): 247 | return self._get_nbit_from_input_report(3, 7, 1) 248 | 249 | def get_button_minus(self): 250 | return self._get_nbit_from_input_report(4, 0, 1) 251 | 252 | def get_button_plus(self): 253 | return self._get_nbit_from_input_report(4, 1, 1) 254 | 255 | def get_button_r_stick(self): 256 | return self._get_nbit_from_input_report(4, 2, 1) 257 | 258 | def get_button_l_stick(self): 259 | return self._get_nbit_from_input_report(4, 3, 1) 260 | 261 | def get_button_home(self): 262 | return self._get_nbit_from_input_report(4, 4, 1) 263 | 264 | def get_button_capture(self): 265 | return self._get_nbit_from_input_report(4, 5, 1) 266 | 267 | def get_button_charging_grip(self): 268 | return self._get_nbit_from_input_report(4, 7, 1) 269 | 270 | def get_button_down(self): 271 | return self._get_nbit_from_input_report(5, 0, 1) 272 | 273 | def get_button_up(self): 274 | return self._get_nbit_from_input_report(5, 1, 1) 275 | 276 | def get_button_right(self): 277 | return self._get_nbit_from_input_report(5, 2, 1) 278 | 279 | def get_button_left(self): 280 | return self._get_nbit_from_input_report(5, 3, 1) 281 | 282 | def get_button_left_sr(self): 283 | return self._get_nbit_from_input_report(5, 4, 1) 284 | 285 | def get_button_left_sl(self): 286 | return self._get_nbit_from_input_report(5, 5, 1) 287 | 288 | def get_button_l(self): 289 | return self._get_nbit_from_input_report(5, 6, 1) 290 | 291 | def get_button_zl(self): 292 | return self._get_nbit_from_input_report(5, 7, 1) 293 | 294 | def get_stick_left_horizontal(self): 295 | return self._get_nbit_from_input_report(6, 0, 8) \ 296 | | (self._get_nbit_from_input_report(7, 0, 4) << 8) 297 | 298 | def get_stick_left_vertical(self): 299 | return self._get_nbit_from_input_report(7, 4, 4) \ 300 | | (self._get_nbit_from_input_report(8, 0, 8) << 4) 301 | 302 | def get_stick_right_horizontal(self): 303 | return self._get_nbit_from_input_report(9, 0, 8) \ 304 | | (self._get_nbit_from_input_report(10, 0, 4) << 8) 305 | 306 | def get_stick_right_vertical(self): 307 | return self._get_nbit_from_input_report(10, 4, 4) \ 308 | | (self._get_nbit_from_input_report(11, 0, 8) << 4) 309 | 310 | def get_accel_x(self, sample_idx=0): 311 | if sample_idx not in (0, 1, 2): 312 | raise IndexError('sample_idx should be between 0 and 2') 313 | data = self._to_int16le_from_2bytes( 314 | self._input_report[13 + sample_idx * 12], 315 | self._input_report[14 + sample_idx * 12]) 316 | return (data - self._ACCEL_OFFSET_X) * self._ACCEL_COEFF_X 317 | 318 | def get_accel_y(self, sample_idx=0): 319 | if sample_idx not in (0, 1, 2): 320 | raise IndexError('sample_idx should be between 0 and 2') 321 | data = self._to_int16le_from_2bytes( 322 | self._input_report[15 + sample_idx * 12], 323 | self._input_report[16 + sample_idx * 12]) 324 | return (data - self._ACCEL_OFFSET_Y) * self._ACCEL_COEFF_Y 325 | 326 | def get_accel_z(self, sample_idx=0): 327 | if sample_idx not in (0, 1, 2): 328 | raise IndexError('sample_idx should be between 0 and 2') 329 | data = self._to_int16le_from_2bytes( 330 | self._input_report[17 + sample_idx * 12], 331 | self._input_report[18 + sample_idx * 12]) 332 | return (data - self._ACCEL_OFFSET_Z) * self._ACCEL_COEFF_Z 333 | 334 | def get_gyro_x(self, sample_idx=0): 335 | if sample_idx not in (0, 1, 2): 336 | raise IndexError('sample_idx should be between 0 and 2') 337 | data = self._to_int16le_from_2bytes( 338 | self._input_report[19 + sample_idx * 12], 339 | self._input_report[20 + sample_idx * 12]) 340 | return (data - self._GYRO_OFFSET_X) * self._GYRO_COEFF_X 341 | 342 | def get_gyro_y(self, sample_idx=0): 343 | if sample_idx not in (0, 1, 2): 344 | raise IndexError('sample_idx should be between 0 and 2') 345 | data = self._to_int16le_from_2bytes( 346 | self._input_report[21 + sample_idx * 12], 347 | self._input_report[22 + sample_idx * 12]) 348 | return (data - self._GYRO_OFFSET_Y) * self._GYRO_COEFF_Y 349 | 350 | def get_gyro_z(self, sample_idx=0): 351 | if sample_idx not in (0, 1, 2): 352 | raise IndexError('sample_idx should be between 0 and 2') 353 | data = self._to_int16le_from_2bytes( 354 | self._input_report[23 + sample_idx * 12], 355 | self._input_report[24 + sample_idx * 12]) 356 | return (data - self._GYRO_OFFSET_Z) * self._GYRO_COEFF_Z 357 | 358 | def get_status(self) -> dict: 359 | return { 360 | "battery": { 361 | "charging": self.get_battery_charging(), 362 | "level": self.get_battery_level(), 363 | }, 364 | "buttons": { 365 | "right": { 366 | "y": self.get_button_y(), 367 | "x": self.get_button_x(), 368 | "b": self.get_button_b(), 369 | "a": self.get_button_a(), 370 | "sr": self.get_button_right_sr(), 371 | "sl": self.get_button_right_sl(), 372 | "r": self.get_button_r(), 373 | "zr": self.get_button_zr(), 374 | }, 375 | "shared": { 376 | "minus": self.get_button_minus(), 377 | "plus": self.get_button_plus(), 378 | "r-stick": self.get_button_r_stick(), 379 | "l-stick": self.get_button_l_stick(), 380 | "home": self.get_button_home(), 381 | "capture": self.get_button_capture(), 382 | "charging-grip": self.get_button_charging_grip(), 383 | }, 384 | "left": { 385 | "down": self.get_button_down(), 386 | "up": self.get_button_up(), 387 | "right": self.get_button_right(), 388 | "left": self.get_button_left(), 389 | "sr": self.get_button_left_sr(), 390 | "sl": self.get_button_left_sl(), 391 | "l": self.get_button_l(), 392 | "zl": self.get_button_zl(), 393 | } 394 | }, 395 | "analog-sticks": { 396 | "left": { 397 | "horizontal": self.get_stick_left_horizontal(), 398 | "vertical": self.get_stick_left_vertical(), 399 | }, 400 | "right": { 401 | "horizontal": self.get_stick_right_horizontal(), 402 | "vertical": self.get_stick_right_vertical(), 403 | }, 404 | }, 405 | "accel": { 406 | "x": self.get_accel_x(), 407 | "y": self.get_accel_y(), 408 | "z": self.get_accel_z(), 409 | }, 410 | "gyro": { 411 | "x": self.get_gyro_x(), 412 | "y": self.get_gyro_y(), 413 | "z": self.get_gyro_z(), 414 | }, 415 | } 416 | 417 | def set_player_lamp_on(self, on_pattern: int): 418 | self._write_output_report( 419 | b'\x01', b'\x30', 420 | (on_pattern & 0xF).to_bytes(1, byteorder='little')) 421 | 422 | def set_player_lamp_flashing(self, flashing_pattern: int): 423 | self._write_output_report( 424 | b'\x01', b'\x30', 425 | ((flashing_pattern & 0xF) << 4).to_bytes(1, byteorder='little')) 426 | 427 | def set_player_lamp(self, pattern: int): 428 | self._write_output_report( 429 | b'\x01', b'\x30', 430 | pattern.to_bytes(1, byteorder='little')) 431 | 432 | def disconnect_device(self): 433 | self._write_output_report(b'\x01', b'\x06', b'\x00') 434 | 435 | 436 | if __name__ == '__main__': 437 | import pyjoycon.device as d 438 | ids = d.get_L_id() if None not in d.get_L_id() else d.get_R_id() 439 | 440 | if None not in ids: 441 | joycon = JoyCon(*ids) 442 | lamp_pattern = 0 443 | while True: 444 | print(joycon.get_status()) 445 | joycon.set_player_lamp_on(lamp_pattern) 446 | lamp_pattern = (lamp_pattern + 1) & 0xf 447 | time.sleep(0.2) 448 | -------------------------------------------------------------------------------- /pyjoycon/wrappers.py: -------------------------------------------------------------------------------- 1 | from .joycon import JoyCon 2 | 3 | 4 | # Preferably, this class gets merged into the 5 | # parent class if approved by the original author 6 | class PythonicJoyCon(JoyCon): 7 | """ 8 | A wrapper class for the JoyCon parent class. 9 | This creates a more pythonic interface by 10 | * using properties instead of requiring java-style getters and setters, 11 | * bundles related xy/xyz data in tuples 12 | * bundles the multiple measurements of the 13 | gyroscope and accelerometer into a list 14 | * Adds the option to invert the y and z axis of the left joycon 15 | to make it match the right joycon. This is enabled by default 16 | """ 17 | 18 | def __init__(self, *a, invert_left_ime_yz=True, **kw): 19 | super().__init__(*a, **kw) 20 | self._ime_yz_coeff = -1 if invert_left_ime_yz and self.is_left() else 1 21 | 22 | is_charging = property(JoyCon.get_battery_charging) 23 | battery_level = property(JoyCon.get_battery_level) 24 | 25 | r = property(JoyCon.get_button_r) 26 | zr = property(JoyCon.get_button_zr) 27 | plus = property(JoyCon.get_button_plus) 28 | a = property(JoyCon.get_button_a) 29 | b = property(JoyCon.get_button_b) 30 | x = property(JoyCon.get_button_x) 31 | y = property(JoyCon.get_button_y) 32 | stick_r_btn = property(JoyCon.get_button_r_stick) 33 | home = property(JoyCon.get_button_home) 34 | right_sr = property(JoyCon.get_button_right_sr) 35 | right_sl = property(JoyCon.get_button_right_sl) 36 | 37 | l = property(JoyCon.get_button_l) # noqa: E741 38 | zl = property(JoyCon.get_button_zl) 39 | minus = property(JoyCon.get_button_minus) 40 | stick_l_btn = property(JoyCon.get_button_l_stick) 41 | up = property(JoyCon.get_button_up) 42 | down = property(JoyCon.get_button_down) 43 | left = property(JoyCon.get_button_left) 44 | right = property(JoyCon.get_button_right) 45 | capture = property(JoyCon.get_button_capture) 46 | left_sr = property(JoyCon.get_button_left_sr) 47 | left_sl = property(JoyCon.get_button_left_sl) 48 | 49 | set_led_on = JoyCon.set_player_lamp_on 50 | set_led_flashing = JoyCon.set_player_lamp_flashing 51 | set_led = JoyCon.set_player_lamp 52 | disconnect = JoyCon.disconnect_device 53 | 54 | @property 55 | def stick_l(self): 56 | return ( 57 | self.get_stick_left_horizontal(), 58 | self.get_stick_left_vertical(), 59 | ) 60 | 61 | @property 62 | def stick_r(self): 63 | return ( 64 | self.get_stick_right_horizontal(), 65 | self.get_stick_right_vertical(), 66 | ) 67 | 68 | @property 69 | def accel(self): 70 | c = self._ime_yz_coeff 71 | return [ 72 | ( 73 | self.get_accel_x(i), 74 | self.get_accel_y(i) * c, 75 | self.get_accel_z(i) * c, 76 | ) 77 | for i in range(3) 78 | ] 79 | 80 | @property 81 | def accel_in_g(self): 82 | c = 4.0 / 0x4000 83 | c2 = c * self._ime_yz_coeff 84 | return [ 85 | ( 86 | self.get_accel_x(i) * c, 87 | self.get_accel_y(i) * c2, 88 | self.get_accel_z(i) * c2, 89 | ) 90 | for i in range(3) 91 | ] 92 | 93 | @property 94 | def gyro(self): 95 | c = self._ime_yz_coeff 96 | return [ 97 | ( 98 | self.get_gyro_x(i), 99 | self.get_gyro_y(i) * c, 100 | self.get_gyro_z(i) * c, 101 | ) 102 | for i in range(3) 103 | ] 104 | 105 | @property 106 | def gyro_in_deg(self): 107 | c = 0.06103 108 | c2 = c * self._ime_yz_coeff 109 | return [ 110 | ( 111 | self.get_gyro_x(i) * c, 112 | self.get_gyro_y(i) * c2, 113 | self.get_gyro_z(i) * c2, 114 | ) 115 | for i in range(3) 116 | ] 117 | 118 | @property 119 | def gyro_in_rad(self): 120 | c = 0.0001694 * 3.1415926536 121 | c2 = c * self._ime_yz_coeff 122 | return [ 123 | ( 124 | self.get_gyro_x(i) * c, 125 | self.get_gyro_y(i) * c2, 126 | self.get_gyro_z(i) * c2, 127 | ) 128 | for i in range(3) 129 | ] 130 | 131 | @property 132 | def gyro_in_rot(self): 133 | c = 0.0001694 134 | c2 = c * self._ime_yz_coeff 135 | return [ 136 | ( 137 | self.get_gyro_x(i) * c, 138 | self.get_gyro_y(i) * c2, 139 | self.get_gyro_z(i) * c2, 140 | ) 141 | for i in range(3) 142 | ] 143 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | hidapi 2 | pyglm 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = "0.2.4" 4 | 5 | with open('README.md') as f: 6 | readme = f.read() 7 | 8 | with open('LICENSE') as f: 9 | license = f.read() 10 | 11 | with open('requirements.txt') as f: 12 | requirements = f.read().splitlines() 13 | 14 | setup( 15 | name='joycon-python', 16 | version=version, 17 | description='Python driver for Nintendo Switch Joy-Con', 18 | long_description=readme, 19 | long_description_content_type='text/markdown', 20 | author='tokoroten-lab, atsukoba, pbsds', 21 | author_email=', '.join([ 22 | 'tokoroten.lab@gmail.com', 23 | 'atsuya_kobayashi@yahoo.co.jp', 24 | 'pbsds@hotmail.com', 25 | ]), 26 | url='https://github.com/tokoroten-lab/joycon-python', 27 | license=license, 28 | packages=find_packages(), 29 | # install_requires=requirements, 30 | classifiers=[ 31 | 'Programming Language :: Python :: 3.7' 32 | ] 33 | ) 34 | --------------------------------------------------------------------------------