diff --git a/README.md b/README.md index f3735fe..6646a4d 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,14 @@ https://youtu.be/f_1IkUHFdH8 - No random disconnection. - Support up to 6 players. - Support all platforms (in theory): - - Xbox Series X/S - - Nintendo Switch - - Xbox One (not tested) - - Stadia (not tested) - - PlayStation 4/5 (not tested) + +| | Xbox Series | Xbox One | PS4/5 | NSW | Stadia | PC | +|-----------|-------------|----------|-------|-----|--------|----| +| 2020-2022 | ✅ | ❓ | ❓ | ✅ | ❓ | | +| 2016-2019 | | ❓ | ❓ | ✅ | | ❓ | + +✅ = confirmed working, ❓ = not tested + ## How does it work? It pretends to be the [Just Dance Controller app](https://play.google.com/store/apps/details?id=com.ubisoft.dance.justdance2015companion), sends movements of the Joy-Con to your game console. @@ -105,6 +108,12 @@ sudo udevadm trigger - Slower, but supports all platforms (including Xbox Series and Stadia). - Requires pairing code. - Requires host's private IP address. + - Old + - For JD 2016-2019 (including PC). + - Connect instantly. + - Doesn't require pairing code. + - Requires PC/console's private IP address. + - *Important*: Can't use buttons on Joy-Con to navigate the UI (you'll have to use controllers/keyboard). - **Host's Private IP Address**: - The private IP address of your PC/Mac/Linux. Find this in the Wi-Fi settings. diff --git a/dance.py b/dance.py index 331e1bb..1b88422 100644 --- a/dance.py +++ b/dance.py @@ -13,7 +13,8 @@ from pyjoycon import ButtonEventJoyCon, JoyCon from pyjoycon.constants import JOYCON_PRODUCT_IDS, JOYCON_VENDOR_ID from joydance import JoyDance, PairingState -from joydance.constants import DEFAULT_CONFIG, JOYDANCE_VERSION +from joydance.constants import (DEFAULT_CONFIG, JOYDANCE_VERSION, + WsSubprotocolVersion) logging.getLogger('asyncio').setLevel(logging.WARNING) @@ -28,6 +29,7 @@ class WsCommand(Enum): class PairingMethod(Enum): DEFAULT = 'default' FAST = 'fast' + OLD = 'old' REGEX_PAIRING_CODE = re.compile(r'^\d{6}$') @@ -137,14 +139,24 @@ async def connect_joycon(app, ws, data): config_parser['joydance'] = config save_config(config_parser) - app['joycons_info'][serial]['pairing_code'] = pairing_code + if pairing_method == PairingMethod.DEFAULT.value: + app['joycons_info'][serial]['pairing_code'] = pairing_code + else: + app['joycons_info'][serial]['pairing_code'] = '' + joycon = ButtonEventJoyCon(vendor_id, product_id, serial) if pairing_method == PairingMethod.DEFAULT.value: console_ip_addr = None + if pairing_method == PairingMethod.OLD.value: + protocol_version = WsSubprotocolVersion.V1 + else: + protocol_version = WsSubprotocolVersion.V2 + joydance = JoyDance( joycon, + protocol_version=protocol_version, pairing_code=pairing_code, host_ip_addr=host_ip_addr, console_ip_addr=console_ip_addr, @@ -162,7 +174,7 @@ async def disconnect_joycon(app, ws, data): print(data) serial = data['joycon_serial'] joydance = app['joydance_connections'][serial] - app['joycons_info'][serial]['state'] = PairingState.IDLE + app['joycons_info'][serial]['state'] = PairingState.IDLE.value await joydance.disconnect() try: @@ -222,7 +234,7 @@ def is_valid_ip_address(val): def is_valid_pairing_method(val): - return val in [PairingMethod.DEFAULT.value, PairingMethod.FAST.value] + return val in [PairingMethod.DEFAULT.value, PairingMethod.FAST.value, PairingMethod.OLD.value] def get_host_ip(): diff --git a/joydance/__init__.py b/joydance/__init__.py index 62286f5..0164fe5 100644 --- a/joydance/__init__.py +++ b/joydance/__init__.py @@ -12,7 +12,8 @@ import websockets from .constants import (ACCEL_ACQUISITION_FREQ_HZ, ACCEL_ACQUISITION_LATENCY, ACCEL_MAX_RANGE, ACCEL_SEND_RATE, JOYCON_UPDATE_RATE, SHORTCUT_MAPPING, UBI_APP_ID, UBI_SKU_ID, - WS_SUBPROTOCOL, Command, JoyConButton) + WS_SUBPROTOCOLS, Command, JoyConButton, + WsSubprotocolVersion) class PairingState(Enum): @@ -36,6 +37,7 @@ class JoyDance: def __init__( self, joycon, + protocol_version, pairing_code=None, host_ip_addr=None, console_ip_addr=None, @@ -45,6 +47,7 @@ class JoyDance: on_state_changed=None): self.joycon = joycon self.joycon_is_left = joycon.is_left() + self.protocol_version = protocol_version if on_state_changed: self.on_state_changed = on_state_changed @@ -271,7 +274,10 @@ class JoyDance: delta_time += (end - start) * 1000 async def send_command(self): - ''' Capture Joycon's input and send to console ''' + ''' Capture Joycon's input and send to console. Only works on procol v2 ''' + if self.protocol_version == WsSubprotocolVersion.V1: + return + while True: try: if self.disconnected: @@ -348,17 +354,23 @@ class JoyDance: await self.disconnect() async def connect_ws(self): - ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - ssl_context.set_ciphers('ALL') - ssl_context.options &= ~ssl.OP_NO_SSLv3 - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE + if self.protocol_version == WsSubprotocolVersion.V1: + ssl_context = None + server_hostname = None + else: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.set_ciphers('ALL') + ssl_context.options &= ~ssl.OP_NO_SSLv3 + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE - server_hostname = self.console_conn.getpeername()[0] if self.console_conn else None + server_hostname = self.console_conn.getpeername()[0] if self.console_conn else None + + subprotocol = WS_SUBPROTOCOLS[self.protocol_version.value] try: async with websockets.connect( self.pairing_url, - subprotocols=[WS_SUBPROTOCOL], + subprotocols=[subprotocol], sock=self.console_conn, ssl=ssl_context, ping_timeout=None, @@ -391,7 +403,10 @@ class JoyDance: try: if self.console_ip_addr: await self.on_state_changed(self.joycon.serial, PairingState.CONNECTING) - self.pairing_url = 'wss://{}:8080/smartphone'.format(self.console_ip_addr) + if self.protocol_version == WsSubprotocolVersion.V1: + self.pairing_url = 'ws://{}:8080/smartphone'.format(self.console_ip_addr) + else: + self.pairing_url = 'wss://{}:8080/smartphone'.format(self.console_ip_addr) else: await self.on_state_changed(self.joycon.serial, PairingState.GETTING_TOKEN) print('Getting authorication token...') diff --git a/joydance/constants.py b/joydance/constants.py index a820c0e..fdcb3e0 100644 --- a/joydance/constants.py +++ b/joydance/constants.py @@ -1,10 +1,18 @@ from enum import Enum - -JOYDANCE_VERSION = '0.1' +JOYDANCE_VERSION = '0.2' UBI_APP_ID = '210da0fb-d6a5-4ed1-9808-01e86f0de7fb' UBI_SKU_ID = 'jdcompanion-android' -WS_SUBPROTOCOL = 'v2.phonescoring.jd.ubisoft.com' + + +class WsSubprotocolVersion(Enum): + V1 = 'v1', + V2 = 'v2', + + +WS_SUBPROTOCOLS = {} +WS_SUBPROTOCOLS[WsSubprotocolVersion.V1.value] = 'v1.phonescoring.jd.ubisoft.com' +WS_SUBPROTOCOLS[WsSubprotocolVersion.V2.value] = 'v2.phonescoring.jd.ubisoft.com' JOYCON_UPDATE_RATE = 0.02 # 50Hz ACCEL_SEND_RATE = 40 # ms diff --git a/static/js/app.js b/static/js/app.js index b8146d1..d4db99d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -17,7 +17,8 @@ const BATTERY_LEVEL = { const PairingMethod = { DEFAULT: 'default', - FAST: 'fast' + FAST: 'fast', + OLD: 'old', } const WsCommand = { @@ -83,8 +84,13 @@ class PairingMethodPicker extends Component { return html` ` } @@ -132,7 +138,8 @@ class PrivateIpAddress extends Component { } componentDidMount() { - window.mitty.emit('update_addr', this.state.host_ip_addr) + let addr = this.props.pairing_method == PairingMethod.DEFAULT ? this.state.host_ip_addr : this.state.console_ip_addr + window.mitty.emit('update_addr', addr) } render(props, state) { @@ -141,14 +148,14 @@ class PrivateIpAddress extends Component { return html` ${pairing_method == PairingMethod.DEFAULT && state.lock_host && html` `} - ${(pairing_method == PairingMethod.FAST || !state.lock_host) && html` + ${(pairing_method != PairingMethod.DEFAULT || !state.lock_host) && html` `} @@ -181,7 +188,7 @@ class PairingCode extends Component { ${props.pairing_method == PairingMethod.DEFAULT && html` !/[0-9]/.test(e.key) && e.preventDefault()} onChange=${this.onChange} /> `} - ${props.pairing_method == PairingMethod.FAST && html` + ${props.pairing_method != PairingMethod.DEFAULT && html` `} `